Compare commits

..

12 Commits

Author SHA1 Message Date
g-samroberts 8a7e9297e2 Update links to outdated index file. 2026-02-17 21:20:19 -08:00
g-samroberts 14887ab3a6 Merge branch 'fix_commands' of https://github.com/google-gemini/gemini-cli into fix_commands 2026-02-17 21:04:11 -08:00
g-samroberts 66d76ff3da Update changed links. Delete cli/index as it's an
outdated clone of docs/index.
2026-02-17 21:03:39 -08:00
g-samroberts 6f4643eba6 Merge branch 'main' into fix_commands 2026-02-17 20:54:39 -08:00
g-samroberts 0efda7c790 Fix simlinked file to avoid link checker error 2026-02-17 19:51:58 -08:00
g-samroberts c62444e8ef Update index.md to match sidebar.json 2026-02-17 19:34:21 -08:00
g-samroberts 1542a3f7e3 Update sidebar.json 2026-02-17 19:22:25 -08:00
g-samroberts fba5a4813a Fix keybindings script based on file location 2026-02-17 18:51:19 -08:00
g-samroberts 0ab5aef2a5 Merge branch 'fix_commands' of https://github.com/google-gemini/gemini-cli into fix_commands 2026-02-17 18:45:55 -08:00
g-samroberts 2117d32a73 Update sidebar.json and reference + resource files 2026-02-17 18:37:24 -08:00
g-samroberts 4f1adacfc8 Update link to authentication. 2026-02-17 09:51:09 -08:00
g-samroberts d6ba439e97 Fix side breakage where anchors don't work as
slugs.
2026-02-17 08:29:00 -08:00
682 changed files with 8406 additions and 31918 deletions
+38 -116
View File
@@ -1,142 +1,64 @@
# Gemini CLI Strict Development Rules
These rules apply strictly to all code modifications and additions within the
Gemini CLI project.
These rules apply strictly to all code modifications and additions within the Gemini CLI project.
## Testing Guidelines
- **Async/Await**: Always use `waitFor` from
`packages/cli/src/test-utils/async.ts` instead of `vi.waitFor` for all
`waitFor` calls within `packages/cli`. NEVER use fixed waits (e.g.,
`await delay(100)`). Always use `waitFor` with a predicate to ensure tests are
stable and fast. Using the wrong `waitFor` can result in flaky tests and `act`
warnings.
- **React Testing**: Use `act` to wrap all blocks in tests that change component
state. Use `render` or `renderWithProviders` from
`packages/cli/src/test-utils/render.tsx` instead of `render` from
`ink-testing-library` directly. This prevents spurious `act` warnings. If test
cases specify providers directly, consider whether the existing
`renderWithProviders` should be modified.
- **Snapshots**: Use `toMatchSnapshot` to verify that rendering works as
expected rather than matching against the raw content of the output. When
modifying snapshots, verify the changes are intentional and do not hide
underlying bugs.
- **Parameterized Tests**: Use parameterized tests where it reduces duplicated
lines. Give the parameters explicit types to ensure the tests are type-safe.
- **Mocks Management**:
- Mock critical dependencies (`fs`, `os`, `child_process`) ONLY at the top of
the file. Ideally, avoid mocking these dependencies altogether.
- Reuse existing mocks and fakes rather than creating new ones.
- Avoid mocking the file system whenever possible. If using the real file
system is too difficult, consider writing an integration test instead.
- Always call `vi.restoreAllMocks()` in `afterEach` to prevent test pollution.
- Use `vi.useFakeTimers()` for tests involving time-based logic to avoid
flakiness.
- **Typing in Tests**: Avoid using `any` in tests; prefer proper types or
`unknown` with narrowing.
* **Async/Await**: Always use `waitFor` from `packages/cli/src/test-utils/async.ts` instead of `vi.waitFor` for all `waitFor` calls within `packages/cli`. NEVER use fixed waits (e.g., `await delay(100)`). Always use `waitFor` with a predicate to ensure tests are stable and fast. Using the wrong `waitFor` can result in flaky tests and `act` warnings.
* **React Testing**: Use `act` to wrap all blocks in tests that change component state. Use `render` or `renderWithProviders` from `packages/cli/src/test-utils/render.tsx` instead of `render` from `ink-testing-library` directly. This prevents spurious `act` warnings. If test cases specify providers directly, consider whether the existing `renderWithProviders` should be modified.
* **Snapshots**: Use `toMatchSnapshot` to verify that rendering works as expected rather than matching against the raw content of the output. When modifying snapshots, verify the changes are intentional and do not hide underlying bugs.
* **Parameterized Tests**: Use parameterized tests where it reduces duplicated lines. Give the parameters explicit types to ensure the tests are type-safe.
* **Mocks Management**:
* Mock critical dependencies (`fs`, `os`, `child_process`) ONLY at the top of the file. Ideally, avoid mocking these dependencies altogether.
* Reuse existing mocks and fakes rather than creating new ones.
* Avoid mocking the file system whenever possible. If using the real file system is too difficult, consider writing an integration test instead.
* Always call `vi.restoreAllMocks()` in `afterEach` to prevent test pollution.
* Use `vi.useFakeTimers()` for tests involving time-based logic to avoid flakiness.
* **Typing in Tests**: Avoid using `any` in tests; prefer proper types or `unknown` with narrowing.
## React Guidelines (`packages/cli`)
- **`setState` and Side Effects**: NEVER trigger side effects from within the
body of a `setState` callback. Use a reducer or `useRef` if necessary. These
cases have historically introduced multiple bugs; typically, they should be
resolved using a reducer.
- **Rendering**: Do not introduce infinite rendering loops. Avoid synchronous
file I/O in React components as it will hang the UI. Do not implement new
logic for custom string measurement or string truncation. Use Ink layout
instead, leveraging `ResizeObserver` as needed.
- **Keyboard Handling**: Keyboard handling MUST go through `useKeyPress.ts` from
the Gemini CLI package rather than the standard ink library. This library
supports reporting multiple keyboard events sequentially in the same React
frame (critical for slow terminals). Handling this correctly often requires
reducers to ensure multiple state updates are handled gracefully without
overriding values. Refer to `text-buffer.ts` for a canonical example.
- **Logging**: Do not leave `console.log`, `console.warn`, or `console.error` in
the code.
- **State & Effects**: Ensure state initialization is explicit (e.g., use
`undefined` rather than `true` as a default if the state is truly unknown).
Carefully manage `useEffect` dependencies. Prefer a reducer whenever
practical. NEVER disable `react-hooks/exhaustive-deps`; fix the code to
correctly declare dependencies instead.
- **Context & Props**: Avoid excessive property drilling. Leverage existing
providers, extend them, or propose a new one if necessary. Only use providers
for properties that are consistent across the entire application.
- **Code Structure**: Avoid complex `if` statements where `switch` statements
could be used. Keep `AppContainer` minimal; refactor complex logic into React
hooks. Evaluate whether business logic should be added to `hookSystem.ts` or
integrated into `packages/core` rather than `packages/cli`.
* **`setState` and Side Effects**: NEVER trigger side effects from within the body of a `setState` callback. Use a reducer or `useRef` if necessary. These cases have historically introduced multiple bugs; typically, they should be resolved using a reducer.
* **Rendering**: Do not introduce infinite rendering loops. Avoid synchronous file I/O in React components as it will hang the UI. Do not implement new logic for custom string measurement or string truncation. Use Ink layout instead, leveraging `ResizeObserver` as needed.
* **Keyboard Handling**: Keyboard handling MUST go through `useKeyPress.ts` from the Gemini CLI package rather than the standard ink library. This library supports reporting multiple keyboard events sequentially in the same React frame (critical for slow terminals). Handling this correctly often requires reducers to ensure multiple state updates are handled gracefully without overriding values. Refer to `text-buffer.ts` for a canonical example.
* **Logging**: Do not leave `console.log`, `console.warn`, or `console.error` in the code.
* **State & Effects**: Ensure state initialization is explicit (e.g., use `undefined` rather than `true` as a default if the state is truly unknown). Carefully manage `useEffect` dependencies. Prefer a reducer whenever practical. NEVER disable `react-hooks/exhaustive-deps`; fix the code to correctly declare dependencies instead.
* **Context & Props**: Avoid excessive property drilling. Leverage existing providers, extend them, or propose a new one if necessary. Only use providers for properties that are consistent across the entire application.
* **Code Structure**: Avoid complex `if` statements where `switch` statements could be used. Keep `AppContainer` minimal; refactor complex logic into React hooks. Evaluate whether business logic should be added to `hookSystem.ts` or integrated into `packages/core` rather than `packages/cli`.
## Core Guidelines (`packages/core`)
- **Services**: Implement services as classes with clear lifecycle management
(e.g., `initialize()` methods). Services should be stateless where possible,
or use the centralized `Storage` service for persistence.
- **Cross-Service Communication**: Prefer using the `coreEvents` bus (from
`packages/core/src/utils/events.ts`) for asynchronous communication between
services or to notify the UI of state changes. Avoid tight coupling between
services.
- **Utilities**: Use `debugLogger` from `packages/core/src/utils/debugLogger.ts`
for internal logging instead of `console`. Ensure all shell operations use
`spawnAsync` from `packages/core/src/utils/shell-utils.ts` for consistent
error handling and promise management. Handle filesystem errors gracefully
using `isNodeError` from `packages/core/src/utils/errors.ts`.
- **Exports & Tooling**: Add new tools to `packages/core/src/tools/` and
register them in `packages/core/src/tools/tool-registry.ts`. Export all new
public services, utilities, and types from `packages/core/src/index.ts`.
* **Services**: Implement services as classes with clear lifecycle management (e.g., `initialize()` methods). Services should be stateless where possible, or use the centralized `Storage` service for persistence.
* **Cross-Service Communication**: Prefer using the `coreEvents` bus (from `packages/core/src/utils/events.ts`) for asynchronous communication between services or to notify the UI of state changes. Avoid tight coupling between services.
* **Utilities**: Use `debugLogger` from `packages/core/src/utils/debugLogger.ts` for internal logging instead of `console`. Ensure all shell operations use `spawnAsync` from `packages/core/src/utils/shell-utils.ts` for consistent error handling and promise management. Handle filesystem errors gracefully using `isNodeError` from `packages/core/src/utils/errors.ts`.
* **Exports & Tooling**: Add new tools to `packages/core/src/tools/` and register them in `packages/core/src/tools/tool-registry.ts`. Export all new public services, utilities, and types from `packages/core/src/index.ts`.
## Architectural Audit (Package Boundaries)
- **Logic Placement**: Non-UI logic (e.g., model orchestration, tool
implementation, git/filesystem operations) MUST reside in `packages/core`.
`packages/cli` should ONLY contain UI/Ink components, command-line argument
parsing, and user interaction logic.
- **Environment Isolation**: Core logic must not assume a TUI environment. Use
the `ConfirmationBus` or `Output` abstractions for communicating with the user
from Core.
- **Decoupling**: Actively look for opportunities to decouple services using
`coreEvents`. If a service imports another just to notify it of a change, use
an event instead.
* **Logic Placement**: Non-UI logic (e.g., model orchestration, tool implementation, git/filesystem operations) MUST reside in `packages/core`. `packages/cli` should ONLY contain UI/Ink components, command-line argument parsing, and user interaction logic.
* **Environment Isolation**: Core logic must not assume a TUI environment. Use the `ConfirmationBus` or `Output` abstractions for communicating with the user from Core.
* **Decoupling**: Actively look for opportunities to decouple services using `coreEvents`. If a service imports another just to notify it of a change, use an event instead.
## General Gemini CLI Design Principles
- **Settings**: Use settings for user-configurable options rather than adding
new command line arguments. Add new settings to
`packages/cli/src/config/settingsSchema.ts`. If a setting has
`showInDialog: true`, it MUST be documented in
`docs/get-started/configuration.md`. Ensure `requiresRestart` is correctly
set.
- **Logging**: Use `debugLogger` for rethrown errors to avoid duplicate logging.
- **Keyboard Shortcuts**: Define all new keyboard shortcuts in
`packages/cli/src/config/keyBindings.ts` and document them in
`docs/cli/keyboard-shortcuts.md`. Be careful of keybindings that require the
`Meta` key, as only certain meta key shortcuts are supported on Mac. Avoid
function keys and shortcuts commonly bound in VSCode.
* **Settings**: Use settings for user-configurable options rather than adding new command line arguments. Add new settings to `packages/cli/src/config/settingsSchema.ts`. If a setting has `showInDialog: true`, it MUST be documented in `docs/get-started/configuration.md`. Ensure `requiresRestart` is correctly set.
* **Logging**: Use `debugLogger` for rethrown errors to avoid duplicate logging.
* **Keyboard Shortcuts**: Define all new keyboard shortcuts in `packages/cli/src/config/keyBindings.ts` and document them in `docs/cli/keyboard-shortcuts.md`. Be careful of keybindings that require the `Meta` key, as only certain meta key shortcuts are supported on Mac. Avoid function keys and shortcuts commonly bound in VSCode.
## TypeScript Best Practices
- Use `checkExhaustive` in the `default` clause of `switch` statements to ensure
all cases are handled.
- Avoid using the non-null assertion operator (`!`) unless absolutely necessary.
- **STRICT TYPING**: Strictly forbid `any` and `unknown` in both CLI and Core
packages. `unknown` is only allowed if it is immediately narrowed using type
guards or Zod validation.
- NEVER disable `@typescript-eslint/no-floating-promises`.
- Avoid making types nullable unless strictly necessary, as it hurts
readability.
* Use `checkExhaustive` in the `default` clause of `switch` statements to ensure all cases are handled.
* Avoid using the non-null assertion operator (`!`) unless absolutely necessary.
* **STRICT TYPING**: Strictly forbid `any` and `unknown` in both CLI and Core packages. `unknown` is only allowed if it is immediately narrowed using type guards or Zod validation.
* NEVER disable `@typescript-eslint/no-floating-promises`.
* Avoid making types nullable unless strictly necessary, as it hurts readability.
## TUI Best Practices
- **Terminal Compatibility**: Consider how changes might behave differently
across terminals (e.g., VSCode terminal, SSH, Kitty, default Mac terminal,
iTerm2, Windows terminal). If modifying keyboard handling, integrate deeply
with existing files like `KeypressContext.tsx` and
`terminalCapabilityManager.ts`.
- **iTerm**: Be aware that `ITERM_SESSION_ID` may be present when users run
VSCode from within iTerm, even if the terminal is not iTerm.
* **Terminal Compatibility**: Consider how changes might behave differently across terminals (e.g., VSCode terminal, SSH, Kitty, default Mac terminal, iTerm2, Windows terminal). If modifying keyboard handling, integrate deeply with existing files like `KeypressContext.tsx` and `terminalCapabilityManager.ts`.
* **iTerm**: Be aware that `ITERM_SESSION_ID` may be present when users run VSCode from within iTerm, even if the terminal is not iTerm.
## Code Cleanup
- **Refactoring**: Actively clean up code duplication, technical debt, and
boilerplate ("AI Slop") when working in the codebase.
- **Prompts**: Be aware that changes can impact the prompts sent to Gemini CLI
and affect overall quality.
* **Refactoring**: Actively clean up code duplication, technical debt, and boilerplate ("AI Slop") when working in the codebase.
* **Prompts**: Be aware that changes can impact the prompts sent to Gemini CLI and affect overall quality.
+10 -32
View File
@@ -23,8 +23,6 @@ To standardize the process of updating changelog files (`latest.md`,
## Guidelines for `latest.md` and `preview.md` Highlights
- Aim for **3-5 key highlight points**.
- Each highlight point must start with a bold-typed title that summarizes the
change (e.g., `**New Feature:** A brief description...`).
- **Prioritize** summarizing new features over other changes like bug fixes or
chores.
- **Avoid** mentioning features that are "experimental" or "in preview" in
@@ -67,8 +65,6 @@ detailed **highlights** section for the release-specific page.
1. **Create the Announcement for `index.md`**:
- Generate a concise announcement summarizing the most important changes.
Each announcement entry must start with a bold-typed title that
summarizes the change.
- **Important**: The format for this announcement is unique. You **must**
use the existing announcements in `docs/changelogs/index.md` and the
example within
@@ -109,41 +105,23 @@ detailed **highlights** section for the release-specific page.
- **Target File**: `docs/changelogs/latest.md`
- Perform the following edits on the target file:
1. Update the version in the main header. The line should read,
`# Latest stable release: {{version}}`
2. Update the rease date. The line should read,
`Released: {{release_date_month_dd_yyyy}}`
1. Update the version in the main header.
2. Update the "Released:" date.
3. **Prepend** the processed "What's Changed" list from the temporary file
to the existing "What's Changed" list in `latest.md`. Do not change or
replace the existing list, **only add** to the beginning of it.
4. In the "Full Changelog", edit **only** the end of the URL. Identify the
last part of the URL that looks like `...{previous_version}` and update
it to be `...{version}`.
Example: assume the patch version is `v0.29.1`. Change
`Full Changelog: https://github.com/google-gemini/gemini-cli/compare/v0.28.2…v0.29.0`
to
`Full Changelog: https://github.com/google-gemini/gemini-cli/compare/v0.28.2…v0.29.1`
to the existing "What's Changed" list in the file.
4. In the "Full Changelog" URL, replace only the trailing version with the
new patch version.
### B.2: Preview Patch (e.g., `v0.29.0-preview.3`)
- **Target File**: `docs/changelogs/preview.md`
- Perform the following edits on the target file:
1. Update the version in the main header. The line should read,
`# Preview release: {{version}}`
2. Update the rease date. The line should read,
`Released: {{release_date_month_dd_yyyy}}`
1. Update the version in the main header.
2. Update the "Released:" date.
3. **Prepend** the processed "What's Changed" list from the temporary file
to the existing "What's Changed" list in `preview.md`. Do not change or
replace the existing list, **only add** to the beginning of it.
4. In the "Full Changelog", edit **only** the end of the URL. Identify the
last part of the URL that looks like `...{previous_version}` and update
it to be `...{version}`.
Example: assume the patch version is `v0.29.0-preview.1`. Change
`Full Changelog: https://github.com/google-gemini/gemini-cli/compare/v0.28.2…v0.29.0-preview.0`
to
`Full Changelog: https://github.com/google-gemini/gemini-cli/compare/v0.28.2…v0.29.0-preview.1`
to the existing "What's Changed" list in the file.
4. In the "Full Changelog" URL, replace only the trailing version with the
new patch version.
---
@@ -1,13 +0,0 @@
---
name: pr-address-comments
description: Use this skill if the user asks you to help them address GitHub PR comments for their current branch of the Gemini CLI. Requires `gh` CLI tool.
---
You are helping the user address comments on their Pull Request. These comments may have come from an automated review agent or a team member.
OBJECTIVE: Help the user review and address comments on their PR.
# Comment Review Procedure
1. Run the `scripts/fetch-pr-info.js` script to get PR info and state. MAKE SURE you read the entire output of the command, even if it gets truncated.
2. Summarize the review status by analyzing the diff, commit log, and comments to see which still need to be addressed. Pay attention to the current user's comments. For resolved threads, summarize as a single line with a ✅. For open threads, provide a reference number e.g. [1] and the comment content.
3. Present your summary of the feedback and current state and allow the user to guide you as to what to fix/address/skip. DO NOT begin fixing issues automatically.
@@ -1,161 +0,0 @@
#!/usr/bin/env node
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/* eslint-env node */
/* global console, process */
import { exec } from 'node:child_process';
import { promisify } from 'node:util';
const execAsync = promisify(exec);
async function run(cmd) {
try {
const { stdout } = await execAsync(cmd, {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore'],
});
return stdout.trim();
} catch (_e) {
// eslint-disable-line @typescript-eslint/no-unused-vars
return null;
}
}
const IGNORE_MESSAGES = [
'thank you so much for your contribution to Gemini CLI!',
"I'm currently reviewing this pull request and will post my feedback shortly.",
'This pull request is being closed because it is not currently linked to an issue.',
];
const shouldIgnore = (body) => {
if (!body) return false;
return IGNORE_MESSAGES.some((msg) => body.includes(msg));
};
async function main() {
const branch = await run('git branch --show-current');
if (!branch) {
console.error('❌ Could not determine current git branch.');
process.exit(1);
}
const gqlQuery = `query($branch:String!){repository(name:"gemini-cli",owner:"google-gemini"){pullRequests(headRefName:$branch,first:100){nodes{id,number,state,comments(first:100){nodes{createdAt,isMinimized,minimizedReason,author{login},body,url,authorAssociation}},reviews(first:100){nodes{id,author{login},createdAt,isMinimized,minimizedReason,body,state,comments(first:30){nodes{id,replyTo{id},author{login},createdAt,body,isMinimized,minimizedReason,path,line,startLine,originalLine,originalStartLine}}}}}}}}`;
const [authInfo, diff, commits, rawJson] = await Promise.all([
run('gh auth status -a'),
run('gh pr diff'),
run(
'git fetch && git log origin/main..origin/$(git branch --show-current)',
),
run(`gh api graphql -F branch="${branch}" -f query='${gqlQuery}'`),
]);
if (!diff) {
console.error(`⚠️ No active PR found for branch: ${branch}`);
process.exit(1);
}
console.log(`\n# Current GitHub user info:\n\n${authInfo}\n`);
console.log(`\n# PR diff for current branch: ${branch}\n\n\`\`\``);
console.log(diff);
console.log('```');
console.log(
`\n# Commit history (origin/main..origin/${branch})\n\n${commits}`,
);
const data = JSON.parse(rawJson || '{}');
const prs = data?.data?.repository?.pullRequests?.nodes || [];
// Sort PRs by number descending so we check the newest one first
prs.sort((a, b) => b.number - a.number);
const pr = prs.find((p) => p.state === 'OPEN') || prs[0];
if (!pr) {
console.error('❌ No PR data found.');
process.exit(1);
}
console.log('\n# PR Feedback\n');
// 1. General PR Comments
const general = pr.comments.nodes.filter((c) => !shouldIgnore(c.body));
if (general.length > 0) {
console.log('\n💬 GENERAL COMMENTS:');
general.forEach((c) => {
const minimized = c.isMinimized
? ` (Minimized: ${c.minimizedReason})`
: '';
console.log(
`[${c.createdAt}] [${c.author.login}]${minimized}: ${c.body}\n`,
);
});
}
// 2. Process ALL Review Comments into a single Thread Map
const allInlineComments = pr.reviews.nodes.flatMap((r) => r.comments.nodes);
const filteredInlines = allInlineComments.filter(
(c) => !shouldIgnore(c.body),
);
console.log('🔍 CODE REVIEWS & INLINE THREADS:');
// Print Review Summaries First
pr.reviews.nodes.forEach((review) => {
if (review.body && !shouldIgnore(review.body)) {
const icon = review.state === 'APPROVED' ? '✅' : '💬';
const minimized = review.isMinimized
? ` (Minimized: ${review.minimizedReason})`
: '';
console.log(
`\n${icon} ${review.state} by ${review.author.login} at ${review.createdAt}${minimized}: "${review.body}"`,
);
}
});
// Build and Print Threads
const topLevelThreads = filteredInlines.filter((c) => !c.replyTo);
const printThread = (parentId, depth = 1) => {
const indent = ' '.repeat(depth);
filteredInlines
.filter((c) => c.replyTo?.id === parentId)
.forEach((reply) => {
const minimized = reply.isMinimized
? ` (Minimized: ${reply.minimizedReason})`
: '';
console.log(
`${indent}↳ [${reply.createdAt}] ${reply.author.login}${minimized}: ${reply.body}`,
);
printThread(reply.id, depth + 1);
});
};
topLevelThreads.forEach((c) => {
const start = c.startLine || c.originalStartLine;
const end = c.line || c.originalLine;
const range = start && end && start !== end ? `${start}-${end}` : end || '';
const fileInfo = c.path
? `(${c.path}${range ? `:${range}` : ''}) `
: range
? `(Line ${range}) `
: '';
const minimized = c.isMinimized ? ` (Minimized: ${c.minimizedReason})` : '';
console.log(
`\n💬 ${minimized}${c.author.login} | ${c.createdAt} ${fileInfo}\n${c.body}`,
);
printThread(c.id);
});
console.log('\n');
}
main().catch((err) => {
console.error('❌ Unexpected error:', err);
process.exit(1);
});
@@ -155,10 +155,7 @@ jobs:
"telemetry": {
"enabled": true,
"target": "gcp"
},
"coreTools": [
"run_shell_command(echo)"
],
}
}
prompt: |-
## Role
@@ -287,21 +284,8 @@ jobs:
return;
}
} else {
// If no markdown block, try to find a raw JSON object in the output.
// The CLI may include debug/log lines (e.g. telemetry init, YOLO mode)
// before the actual JSON response.
const jsonObjectMatch = rawOutput.match(/(\{[\s\S]*"labels_to_set"[\s\S]*\})/);
if (jsonObjectMatch) {
try {
parsedLabels = JSON.parse(jsonObjectMatch[0]);
} catch (extractError) {
core.setFailed(`Found JSON-like content but failed to parse: ${extractError.message}\nRaw output: ${rawOutput}`);
return;
}
} else {
core.setFailed(`Output is not valid JSON and does not contain extractable JSON.\nRaw output: ${rawOutput}`);
return;
}
core.setFailed(`Output is not valid JSON and does not contain a JSON markdown block.\nRaw output: ${rawOutput}`);
return;
}
}
+2 -3
View File
@@ -32,7 +32,6 @@ jobs:
with:
# The user-level skills need to be available to the workflow
fetch-depth: 0
ref: 'main'
- name: 'Set up Node.js'
uses: 'actions/setup-node@v4'
@@ -43,6 +42,7 @@ jobs:
id: 'release_info'
run: |
VERSION="${{ github.event.inputs.version || github.event.release.tag_name }}"
BODY="${{ github.event.inputs.body || github.event.release.body }}"
TIME="${{ github.event.inputs.time || github.event.release.created_at }}"
echo "VERSION=${VERSION}" >> "$GITHUB_OUTPUT"
@@ -50,11 +50,10 @@ jobs:
# Use a heredoc to preserve multiline release body
echo 'RAW_CHANGELOG<<EOF' >> "$GITHUB_OUTPUT"
printf "%s\n" "$BODY" >> "$GITHUB_OUTPUT"
printf "%s\n" "${BODY}" >> "$GITHUB_OUTPUT"
echo 'EOF' >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
BODY: '${{ github.event.inputs.body || github.event.release.body }}'
- name: 'Generate Changelog with Gemini'
uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31' # ratchet:google-github-actions/run-gemini-cli@v0
-1
View File
@@ -46,7 +46,6 @@ packages/*/coverage/
# Generated files
packages/cli/src/generated/
packages/core/src/generated/
packages/devtools/src/_client-assets.ts
.integration-tests/
packages/vscode-ide-companion/*.vsix
packages/cli/download-ripgrep*/
+1 -1
View File
@@ -546,7 +546,7 @@ Before submitting your documentation pull request, please:
If you have questions about contributing documentation:
- Check our [FAQ](/docs/resources/faq.md).
- Check our [FAQ](/docs/resources/faq).
- Review existing documentation for examples.
- Open [an issue](https://github.com/google-gemini/gemini-cli/issues) to discuss
your proposed changes.
+1 -7
View File
@@ -47,13 +47,7 @@ powerful tool for developers.
be relative to the workspace root, e.g.,
`-w @google/gemini-cli-core -- src/routing/modelRouterService.test.ts`)
- **Full Validation:** `npm run preflight` (Heaviest check; runs clean, install,
build, lint, type check, and tests. Recommended before submitting PRs. Due to
its long runtime, only run this at the very end of a code implementation task.
If it fails, use faster, targeted commands (e.g., `npm run test`,
`npm run lint`, or workspace-specific tests) to iterate on fixes before
re-running `preflight`. For simple, non-code changes like documentation or
prompting updates, skip `preflight` at the end of the task and wait for PR
validation.)
build, lint, type check, and tests. Recommended before submitting PRs.)
- **Individual Checks:** `npm run lint` / `npm run format` / `npm run typecheck`
## Development Conventions
+1 -20
View File
@@ -18,26 +18,7 @@ on GitHub.
| [Preview](preview.md) | Experimental features ready for early feedback. |
| [Stable](latest.md) | Stable, recommended for general use. |
## Announcements: v0.29.0 - 2026-02-17
- **Plan Mode:** A new comprehensive planning capability with `/plan`,
`enter_plan_mode` tool, and dedicated documentation
([#17698](https://github.com/google-gemini/gemini-cli/pull/17698) by @Adib234,
[#18324](https://github.com/google-gemini/gemini-cli/pull/18324) by @jerop).
- **Gemini 3 Default:** We've removed the preview flag and enabled Gemini 3 by
default for all users
([#18414](https://github.com/google-gemini/gemini-cli/pull/18414) by
@sehoon38).
- **Extension Exploration:** New UI and settings to explore and manage
extensions more easily
([#18686](https://github.com/google-gemini/gemini-cli/pull/18686) by
@sripasg).
- **Admin Control:** Administrators can now allowlist specific MCP server
configurations
([#18311](https://github.com/google-gemini/gemini-cli/pull/18311) by
@skeshive).
## Announcements: v0.28.0 - 2026-02-10
## Announcements: v0.28.0 - 2026-02-03
- **Slash Command:** We've added a new `/prompt-suggest` slash command to help
you generate prompt suggestions
+293 -359
View File
@@ -1,6 +1,6 @@
# Latest stable release: v0.29.0
# Latest stable release: v0.28.0
Released: February 17, 2026
Released: February 10, 2026
For most users, our latest stable release is the recommended release. Install
the latest stable version with:
@@ -11,371 +11,305 @@ npm install -g @google/gemini-cli
## Highlights
- **Plan Mode:** Introduce a dedicated "Plan Mode" to help you architect complex
changes before implementation. Use `/plan` to get started.
- **Gemini 3 by Default:** Gemini 3 is now the default model family, bringing
improved performance and reasoning capabilities to all users without needing a
feature flag.
- **Extension Discovery:** Easily discover and install extensions with the new
exploration features and registry client.
- **Enhanced Admin Controls:** New administrative capabilities allow for
allowlisting MCP server configurations, giving organizations more control over
available tools.
- **Sub-agent Improvements:** Sub-agents have been transitioned to a new format
with improved definitions and system prompts for better reliability.
- **Commands & UX Enhancements:** Introduced `/prompt-suggest` command,
alongside updated undo/redo keybindings and automatic theme switching.
- **Expanded IDE Support:** Now offering compatibility with Positron IDE,
expanding integration options for developers.
- **Enhanced Security & Authentication:** Implemented interactive and
non-interactive OAuth consent, improving both security and diagnostic
capabilities for bug reports.
- **Advanced Planning & Agent Tools:** Integrated a generic Checklist component
for structured task management and evolved subagent capabilities with dynamic
policy registration.
- **Improved Core Stability & Reliability:** Resolved critical environment
loading, authentication, and session management issues, ensuring a more robust
experience.
- **Background Shell Commands:** Enabled the execution of shell commands in the
background for increased workflow efficiency.
## What's Changed
- fix: remove `ask_user` tool from non-interactive modes by @jackwotherspoon in
[#18154](https://github.com/google-gemini/gemini-cli/pull/18154)
- fix(cli): allow restricted .env loading in untrusted sandboxed folders by
@galz10 in [#17806](https://github.com/google-gemini/gemini-cli/pull/17806)
- Encourage agent to utilize ecosystem tools to perform work by @gundermanc in
[#17881](https://github.com/google-gemini/gemini-cli/pull/17881)
- feat(plan): unify workflow location in system prompt to optimize caching by
@jerop in [#18258](https://github.com/google-gemini/gemini-cli/pull/18258)
- feat(core): enable getUserTierName in config by @sehoon38 in
[#18265](https://github.com/google-gemini/gemini-cli/pull/18265)
- feat(core): add default execution limits for subagents by @abhipatel12 in
[#18274](https://github.com/google-gemini/gemini-cli/pull/18274)
- Fix issue where agent gets stuck at interactive commands. by @gundermanc in
[#18272](https://github.com/google-gemini/gemini-cli/pull/18272)
- chore(release): bump version to 0.29.0-nightly.20260203.71f46f116 by
- feat(commands): add /prompt-suggest slash command by @NTaylorMullen in
[#17264](https://github.com/google-gemini/gemini-cli/pull/17264)
- feat(cli): align hooks enable/disable with skills and improve completion by
@sehoon38 in [#16822](https://github.com/google-gemini/gemini-cli/pull/16822)
- docs: add CLI reference documentation by @leochiu-a in
[#17504](https://github.com/google-gemini/gemini-cli/pull/17504)
- chore(release): bump version to 0.28.0-nightly.20260128.adc8e11bb by
@gemini-cli-robot in
[#18243](https://github.com/google-gemini/gemini-cli/pull/18243)
- feat(core): remove hardcoded policy bypass for local subagents by @abhipatel12
in [#18153](https://github.com/google-gemini/gemini-cli/pull/18153)
- feat(plan): implement `plan` slash command by @Adib234 in
[#17698](https://github.com/google-gemini/gemini-cli/pull/17698)
- feat: increase `ask_user` label limit to 16 characters by @jackwotherspoon in
[#18320](https://github.com/google-gemini/gemini-cli/pull/18320)
- Add information about the agent skills lifecycle and clarify docs-writer skill
metadata. by @g-samroberts in
[#18234](https://github.com/google-gemini/gemini-cli/pull/18234)
- feat(core): add `enter_plan_mode` tool by @jerop in
[#18324](https://github.com/google-gemini/gemini-cli/pull/18324)
- Stop showing an error message in `/plan` by @Adib234 in
[#18333](https://github.com/google-gemini/gemini-cli/pull/18333)
- fix(hooks): remove unnecessary logging for hook registration by @abhipatel12
in [#18332](https://github.com/google-gemini/gemini-cli/pull/18332)
- fix(mcp): ensure MCP transport is closed to prevent memory leaks by
@cbcoutinho in
[#18054](https://github.com/google-gemini/gemini-cli/pull/18054)
- feat(skills): implement linking for agent skills by @MushuEE in
[#18295](https://github.com/google-gemini/gemini-cli/pull/18295)
- Changelogs for 0.27.0 and 0.28.0-preview0 by @g-samroberts in
[#18336](https://github.com/google-gemini/gemini-cli/pull/18336)
- chore: correct docs as skills and hooks are stable by @jackwotherspoon in
[#18358](https://github.com/google-gemini/gemini-cli/pull/18358)
- feat(admin): Implement admin allowlist for MCP server configurations by
@skeshive in [#18311](https://github.com/google-gemini/gemini-cli/pull/18311)
- fix(core): add retry logic for transient SSL/TLS errors (#17318) by @ppgranger
in [#18310](https://github.com/google-gemini/gemini-cli/pull/18310)
- Add support for /extensions config command by @chrstnb in
[#17895](https://github.com/google-gemini/gemini-cli/pull/17895)
- fix(core): handle non-compliant mcpbridge responses from Xcode 26.3 by
@peterfriese in
[#18376](https://github.com/google-gemini/gemini-cli/pull/18376)
- feat(cli): Add W, B, E Vim motions and operator support by @ademuri in
[#16209](https://github.com/google-gemini/gemini-cli/pull/16209)
- fix: Windows Specific Agent Quality & System Prompt by @scidomino in
[#18351](https://github.com/google-gemini/gemini-cli/pull/18351)
- feat(plan): support `replace` tool in plan mode to edit plans by @jerop in
[#18379](https://github.com/google-gemini/gemini-cli/pull/18379)
- Improving memory tool instructions and eval testing by @alisa-alisa in
[#18091](https://github.com/google-gemini/gemini-cli/pull/18091)
- fix(cli): color extension link success message green by @MushuEE in
[#18386](https://github.com/google-gemini/gemini-cli/pull/18386)
- undo by @jacob314 in
[#18147](https://github.com/google-gemini/gemini-cli/pull/18147)
- feat(plan): add guidance on iterating on approved plans vs creating new plans
by @jerop in [#18346](https://github.com/google-gemini/gemini-cli/pull/18346)
- feat(plan): fix invalid tool calls in plan mode by @Adib234 in
[#18352](https://github.com/google-gemini/gemini-cli/pull/18352)
- feat(plan): integrate planning artifacts and tools into primary workflows by
@jerop in [#18375](https://github.com/google-gemini/gemini-cli/pull/18375)
- Fix permission check by @scidomino in
[#18395](https://github.com/google-gemini/gemini-cli/pull/18395)
- ux(polish) autocomplete in the input prompt by @jacob314 in
[#18181](https://github.com/google-gemini/gemini-cli/pull/18181)
- fix: resolve infinite loop when using 'Modify with external editor' by
@ppgranger in [#17453](https://github.com/google-gemini/gemini-cli/pull/17453)
- feat: expand verify-release to macOS and Windows by @yunaseoul in
[#18145](https://github.com/google-gemini/gemini-cli/pull/18145)
- feat(plan): implement support for MCP servers in Plan mode by @Adib234 in
[#18229](https://github.com/google-gemini/gemini-cli/pull/18229)
- chore: update folder trust error messaging by @galz10 in
[#18402](https://github.com/google-gemini/gemini-cli/pull/18402)
- feat(plan): create a metric for execution of plans generated in plan mode by
@Adib234 in [#18236](https://github.com/google-gemini/gemini-cli/pull/18236)
- perf(ui): optimize stripUnsafeCharacters with regex by @gsquared94 in
[#18413](https://github.com/google-gemini/gemini-cli/pull/18413)
- feat(context): implement observation masking for tool outputs by @abhipatel12
in [#18389](https://github.com/google-gemini/gemini-cli/pull/18389)
- feat(core,cli): implement session-linked tool output storage and cleanup by
[#17725](https://github.com/google-gemini/gemini-cli/pull/17725)
- feat(skills): final stable promotion cleanup by @abhipatel12 in
[#17726](https://github.com/google-gemini/gemini-cli/pull/17726)
- test(core): mock fetch in OAuth transport fallback tests by @jw409 in
[#17059](https://github.com/google-gemini/gemini-cli/pull/17059)
- feat(cli): include auth method in /bug by @erikus in
[#17569](https://github.com/google-gemini/gemini-cli/pull/17569)
- Add a email privacy note to bug_report template by @nemyung in
[#17474](https://github.com/google-gemini/gemini-cli/pull/17474)
- Rewind documentation by @Adib234 in
[#17446](https://github.com/google-gemini/gemini-cli/pull/17446)
- fix: verify audio/video MIME types with content check by @maru0804 in
[#16907](https://github.com/google-gemini/gemini-cli/pull/16907)
- feat(core): add support for positron ide
([#15045](https://github.com/google-gemini/gemini-cli/pull/15045)) by @kapsner
in [#15047](https://github.com/google-gemini/gemini-cli/pull/15047)
- /oncall dedup - wrap texts to nextlines by @sehoon38 in
[#17782](https://github.com/google-gemini/gemini-cli/pull/17782)
- fix(admin): rename advanced features admin setting by @skeshive in
[#17786](https://github.com/google-gemini/gemini-cli/pull/17786)
- [extension config] Make breaking optional value non-optional by @chrstnb in
[#17785](https://github.com/google-gemini/gemini-cli/pull/17785)
- Fix docs-writer skill issues by @g-samroberts in
[#17734](https://github.com/google-gemini/gemini-cli/pull/17734)
- fix(core): suppress duplicate hook failure warnings during streaming by
@abhipatel12 in
[#18416](https://github.com/google-gemini/gemini-cli/pull/18416)
- Shorten temp directory by @joshualitt in
[#17901](https://github.com/google-gemini/gemini-cli/pull/17901)
- feat(plan): add behavioral evals for plan mode by @jerop in
[#18437](https://github.com/google-gemini/gemini-cli/pull/18437)
- Add extension registry client by @chrstnb in
[#18396](https://github.com/google-gemini/gemini-cli/pull/18396)
- Enable extension config by default by @chrstnb in
[#18447](https://github.com/google-gemini/gemini-cli/pull/18447)
- Automatically generate change logs on release by @g-samroberts in
[#18401](https://github.com/google-gemini/gemini-cli/pull/18401)
- Remove previewFeatures and default to Gemini 3 by @sehoon38 in
[#18414](https://github.com/google-gemini/gemini-cli/pull/18414)
- feat(admin): apply MCP allowlist to extensions & gemini mcp list command by
@skeshive in [#18442](https://github.com/google-gemini/gemini-cli/pull/18442)
- fix(cli): improve focus navigation for interactive and background shells by
@galz10 in [#18343](https://github.com/google-gemini/gemini-cli/pull/18343)
- Add shortcuts hint and panel for discoverability by @LyalinDotCom in
[#18035](https://github.com/google-gemini/gemini-cli/pull/18035)
- fix(config): treat system settings as read-only during migration and warn user
by @spencer426 in
[#18277](https://github.com/google-gemini/gemini-cli/pull/18277)
- feat(plan): add positive test case and update eval stability policy by @jerop
in [#18457](https://github.com/google-gemini/gemini-cli/pull/18457)
- fix- windows: add shell: true for spawnSync to fix EINVAL with .cmd editors by
@zackoch in [#18408](https://github.com/google-gemini/gemini-cli/pull/18408)
- bug(core): Fix bug when saving plans. by @joshualitt in
[#18465](https://github.com/google-gemini/gemini-cli/pull/18465)
- Refactor atCommandProcessor by @scidomino in
[#18461](https://github.com/google-gemini/gemini-cli/pull/18461)
- feat(core): implement persistence and resumption for masked tool outputs by
[#17727](https://github.com/google-gemini/gemini-cli/pull/17727)
- test: add more tests for AskUser by @jackwotherspoon in
[#17720](https://github.com/google-gemini/gemini-cli/pull/17720)
- feat(cli): enable activity logging for non-interactive mode and evals by
@SandyTao520 in
[#17703](https://github.com/google-gemini/gemini-cli/pull/17703)
- feat(core): add support for custom deny messages in policy rules by
@allenhutchison in
[#17427](https://github.com/google-gemini/gemini-cli/pull/17427)
- Fix unintended credential exposure to MCP Servers by @Adib234 in
[#17311](https://github.com/google-gemini/gemini-cli/pull/17311)
- feat(extensions): add support for custom themes in extensions by @spencer426
in [#17327](https://github.com/google-gemini/gemini-cli/pull/17327)
- fix: persist and restore workspace directories on session resume by
@korade-krushna in
[#17454](https://github.com/google-gemini/gemini-cli/pull/17454)
- Update release notes pages for 0.26.0 and 0.27.0-preview. by @g-samroberts in
[#17744](https://github.com/google-gemini/gemini-cli/pull/17744)
- feat(ux): update cell border color and created test file for table rendering
by @devr0306 in
[#17798](https://github.com/google-gemini/gemini-cli/pull/17798)
- Change height for the ToolConfirmationQueue. by @jacob314 in
[#17799](https://github.com/google-gemini/gemini-cli/pull/17799)
- feat(cli): add user identity info to stats command by @sehoon38 in
[#17612](https://github.com/google-gemini/gemini-cli/pull/17612)
- fix(ux): fixed off-by-some wrapping caused by fixed-width characters by
@devr0306 in [#17816](https://github.com/google-gemini/gemini-cli/pull/17816)
- feat(cli): update undo/redo keybindings to Cmd+Z/Alt+Z and
Shift+Cmd+Z/Shift+Alt+Z by @scidomino in
[#17800](https://github.com/google-gemini/gemini-cli/pull/17800)
- fix(evals): use absolute path for activity log directory by @SandyTao520 in
[#17830](https://github.com/google-gemini/gemini-cli/pull/17830)
- test: add integration test to verify stdout/stderr routing by @ved015 in
[#17280](https://github.com/google-gemini/gemini-cli/pull/17280)
- fix(cli): list installed extensions when update target missing by @tt-a1i in
[#17082](https://github.com/google-gemini/gemini-cli/pull/17082)
- fix(cli): handle PAT tokens and credentials in git remote URL parsing by
@afarber in [#14650](https://github.com/google-gemini/gemini-cli/pull/14650)
- fix(core): use returnDisplay for error result display by @Nubebuster in
[#14994](https://github.com/google-gemini/gemini-cli/pull/14994)
- Fix detection of bun as package manager by @Randomblock1 in
[#17462](https://github.com/google-gemini/gemini-cli/pull/17462)
- feat(cli): show hooksConfig.enabled in settings dialog by @abhipatel12 in
[#17810](https://github.com/google-gemini/gemini-cli/pull/17810)
- feat(cli): Display user identity (auth, email, tier) on startup by @yunaseoul
in [#17591](https://github.com/google-gemini/gemini-cli/pull/17591)
- fix: prevent ghost border for AskUserDialog by @jackwotherspoon in
[#17788](https://github.com/google-gemini/gemini-cli/pull/17788)
- docs: mark A2A subagents as experimental in subagents.md by @adamfweidman in
[#17863](https://github.com/google-gemini/gemini-cli/pull/17863)
- Resolve error thrown for sensitive values by @chrstnb in
[#17826](https://github.com/google-gemini/gemini-cli/pull/17826)
- fix(admin): Rename secureModeEnabled to strictModeDisabled by @skeshive in
[#17789](https://github.com/google-gemini/gemini-cli/pull/17789)
- feat(ux): update truncate dots to be shorter in tables by @devr0306 in
[#17825](https://github.com/google-gemini/gemini-cli/pull/17825)
- fix(core): resolve DEP0040 punycode deprecation via patch-package by
@ATHARVA262005 in
[#17692](https://github.com/google-gemini/gemini-cli/pull/17692)
- feat(plan): create generic Checklist component and refactor Todo by @Adib234
in [#17741](https://github.com/google-gemini/gemini-cli/pull/17741)
- Cleanup post delegate_to_agent removal by @gundermanc in
[#17875](https://github.com/google-gemini/gemini-cli/pull/17875)
- fix(core): use GIT_CONFIG_GLOBAL to isolate shadow git repo configuration -
Fixes [#17877](https://github.com/google-gemini/gemini-cli/pull/17877) by
@cocosheng-g in
[#17803](https://github.com/google-gemini/gemini-cli/pull/17803)
- Disable mouse tracking e2e by @alisa-alisa in
[#17880](https://github.com/google-gemini/gemini-cli/pull/17880)
- fix(cli): use correct setting key for Cloud Shell auth by @sehoon38 in
[#17884](https://github.com/google-gemini/gemini-cli/pull/17884)
- chore: revert IDE specific ASCII logo by @jackwotherspoon in
[#17887](https://github.com/google-gemini/gemini-cli/pull/17887)
- Revert "fix(core): resolve DEP0040 punycode deprecation via patch-package" by
@sehoon38 in [#17898](https://github.com/google-gemini/gemini-cli/pull/17898)
- Refactoring of disabling of mouse tracking in e2e tests by @alisa-alisa in
[#17902](https://github.com/google-gemini/gemini-cli/pull/17902)
- feat(core): Add GOOGLE_GENAI_API_VERSION environment variable support by
@deyim in [#16177](https://github.com/google-gemini/gemini-cli/pull/16177)
- feat(core): Isolate and cleanup truncated tool outputs by @SandyTao520 in
[#17594](https://github.com/google-gemini/gemini-cli/pull/17594)
- Create skills page, update commands, refine docs by @g-samroberts in
[#17842](https://github.com/google-gemini/gemini-cli/pull/17842)
- feat: preserve EOL in files by @Thomas-Shephard in
[#16087](https://github.com/google-gemini/gemini-cli/pull/16087)
- Fix HalfLinePaddedBox in screenreader mode. by @jacob314 in
[#17914](https://github.com/google-gemini/gemini-cli/pull/17914)
- bug(ux) vim mode fixes. Start in insert mode. Fix bug blocking F12 and ctrl-X
in vim mode. by @jacob314 in
[#17938](https://github.com/google-gemini/gemini-cli/pull/17938)
- feat(core): implement interactive and non-interactive consent for OAuth by
@ehedlund in [#17699](https://github.com/google-gemini/gemini-cli/pull/17699)
- perf(core): optimize token calculation and add support for multimodal tool
responses by @abhipatel12 in
[#17835](https://github.com/google-gemini/gemini-cli/pull/17835)
- refactor(hooks): remove legacy tools.enableHooks setting by @abhipatel12 in
[#17867](https://github.com/google-gemini/gemini-cli/pull/17867)
- feat(ci): add npx smoke test to verify installability by @bdmorgan in
[#17927](https://github.com/google-gemini/gemini-cli/pull/17927)
- feat(core): implement dynamic policy registration for subagents by
@abhipatel12 in
[#18451](https://github.com/google-gemini/gemini-cli/pull/18451)
- refactor: simplify tool output truncation to single config by @SandyTao520 in
[#18446](https://github.com/google-gemini/gemini-cli/pull/18446)
- bug(core): Ensure storage is initialized early, even if config is not. by
@joshualitt in
[#18471](https://github.com/google-gemini/gemini-cli/pull/18471)
- chore: Update build-and-start script to support argument forwarding by
[#17838](https://github.com/google-gemini/gemini-cli/pull/17838)
- feat: Implement background shell commands by @galz10 in
[#14849](https://github.com/google-gemini/gemini-cli/pull/14849)
- feat(admin): provide actionable error messages for disabled features by
@skeshive in [#17815](https://github.com/google-gemini/gemini-cli/pull/17815)
- Fix bugs where Rewind and Resume showed Ugly and 100X too verbose content. by
@jacob314 in [#17940](https://github.com/google-gemini/gemini-cli/pull/17940)
- Fix broken link in docs by @chrstnb in
[#17959](https://github.com/google-gemini/gemini-cli/pull/17959)
- feat(plan): reuse standard tool confirmation for AskUser tool by @jerop in
[#17864](https://github.com/google-gemini/gemini-cli/pull/17864)
- feat(core): enable overriding CODE_ASSIST_API_VERSION with env var by
@lottielin in [#17942](https://github.com/google-gemini/gemini-cli/pull/17942)
- run npx pointing to the specific commit SHA by @sehoon38 in
[#17970](https://github.com/google-gemini/gemini-cli/pull/17970)
- Add allowedExtensions setting by @kevinjwang1 in
[#17695](https://github.com/google-gemini/gemini-cli/pull/17695)
- feat(plan): refactor ToolConfirmationPayload to union type by @jerop in
[#17980](https://github.com/google-gemini/gemini-cli/pull/17980)
- lower the default max retries to reduce contention by @sehoon38 in
[#17975](https://github.com/google-gemini/gemini-cli/pull/17975)
- fix(core): ensure YOLO mode auto-approves complex shell commands when parsing
fails by @abhipatel12 in
[#17920](https://github.com/google-gemini/gemini-cli/pull/17920)
- Fix broken link. by @g-samroberts in
[#17972](https://github.com/google-gemini/gemini-cli/pull/17972)
- Support ctrl-C and Ctrl-D correctly Refactor so InputPrompt has priority over
AppContainer for input handling. by @jacob314 in
[#17993](https://github.com/google-gemini/gemini-cli/pull/17993)
- Fix truncation for AskQuestion by @jacob314 in
[#18001](https://github.com/google-gemini/gemini-cli/pull/18001)
- fix(workflow): update maintainer check logic to be inclusive and
case-insensitive by @bdmorgan in
[#18009](https://github.com/google-gemini/gemini-cli/pull/18009)
- Fix Esc cancel during streaming by @LyalinDotCom in
[#18039](https://github.com/google-gemini/gemini-cli/pull/18039)
- feat(acp): add session resume support by @bdmorgan in
[#18043](https://github.com/google-gemini/gemini-cli/pull/18043)
- fix(ci): prevent stale PR closer from incorrectly closing new PRs by @bdmorgan
in [#18069](https://github.com/google-gemini/gemini-cli/pull/18069)
- chore: delete autoAccept setting unused in production by @victorvianna in
[#17862](https://github.com/google-gemini/gemini-cli/pull/17862)
- feat(plan): use placeholder for choice question "Other" option by @jerop in
[#18101](https://github.com/google-gemini/gemini-cli/pull/18101)
- docs: update clearContext to hookSpecificOutput by @jackwotherspoon in
[#18024](https://github.com/google-gemini/gemini-cli/pull/18024)
- docs-writer skill: Update docs writer skill by @jkcinouye in
[#17928](https://github.com/google-gemini/gemini-cli/pull/17928)
- Sehoon/oncall filter by @sehoon38 in
[#18105](https://github.com/google-gemini/gemini-cli/pull/18105)
- feat(core): add setting to disable loop detection by @SandyTao520 in
[#18008](https://github.com/google-gemini/gemini-cli/pull/18008)
- Docs: Revise docs/index.md by @jkcinouye in
[#17879](https://github.com/google-gemini/gemini-cli/pull/17879)
- Fix up/down arrow regression and add test. by @jacob314 in
[#18108](https://github.com/google-gemini/gemini-cli/pull/18108)
- fix(ui): prevent content leak in MaxSizedBox bottom overflow by @jerop in
[#17991](https://github.com/google-gemini/gemini-cli/pull/17991)
- refactor: migrate checks.ts utility to core and deduplicate by @jerop in
[#18139](https://github.com/google-gemini/gemini-cli/pull/18139)
- feat(core): implement tool name aliasing for backward compatibility by
@SandyTao520 in
[#17974](https://github.com/google-gemini/gemini-cli/pull/17974)
- docs: fix help-wanted label spelling by @pavan-sh in
[#18114](https://github.com/google-gemini/gemini-cli/pull/18114)
- feat(cli): implement automatic theme switching based on terminal background by
@Abhijit-2592 in
[#18241](https://github.com/google-gemini/gemini-cli/pull/18241)
- fix(core): prevent subagent bypass in plan mode by @jerop in
[#18484](https://github.com/google-gemini/gemini-cli/pull/18484)
- feat(cli): add WebSocket-based network logging and streaming chunk support by
@SandyTao520 in
[#18383](https://github.com/google-gemini/gemini-cli/pull/18383)
- feat(cli): update approval modes UI by @jerop in
[#18476](https://github.com/google-gemini/gemini-cli/pull/18476)
- fix(cli): reload skills and agents on extension restart by @NTaylorMullen in
[#18411](https://github.com/google-gemini/gemini-cli/pull/18411)
- fix(core): expand excludeTools with legacy aliases for renamed tools by
@SandyTao520 in
[#18498](https://github.com/google-gemini/gemini-cli/pull/18498)
- feat(core): overhaul system prompt for rigor, integrity, and intent alignment
by @NTaylorMullen in
[#17263](https://github.com/google-gemini/gemini-cli/pull/17263)
- Patch for generate changelog docs yaml file by @g-samroberts in
[#18496](https://github.com/google-gemini/gemini-cli/pull/18496)
- Code review fixes for show question mark pr. by @jacob314 in
[#18480](https://github.com/google-gemini/gemini-cli/pull/18480)
- fix(cli): add SS3 Shift+Tab support for Windows terminals by @ThanhNguyxn in
[#18187](https://github.com/google-gemini/gemini-cli/pull/18187)
- chore: remove redundant planning prompt from final shell by @jerop in
[#18528](https://github.com/google-gemini/gemini-cli/pull/18528)
- docs: require pr-creator skill for PR generation by @NTaylorMullen in
[#18536](https://github.com/google-gemini/gemini-cli/pull/18536)
- chore: update colors for ask_user dialog by @jackwotherspoon in
[#18543](https://github.com/google-gemini/gemini-cli/pull/18543)
- feat(core): exempt high-signal tools from output masking by @abhipatel12 in
[#18545](https://github.com/google-gemini/gemini-cli/pull/18545)
- refactor(core): remove memory tool instructions from Gemini 3 prompt by
[#17976](https://github.com/google-gemini/gemini-cli/pull/17976)
- fix(ide): no-op refactoring that moves the connection logic to helper
functions by @skeshive in
[#18118](https://github.com/google-gemini/gemini-cli/pull/18118)
- feat: update review-frontend-and-fix slash command to review-and-fix by
@galz10 in [#18146](https://github.com/google-gemini/gemini-cli/pull/18146)
- fix: improve Ctrl+R reverse search by @jackwotherspoon in
[#18075](https://github.com/google-gemini/gemini-cli/pull/18075)
- feat(plan): handle inconsistency in schedulers by @Adib234 in
[#17813](https://github.com/google-gemini/gemini-cli/pull/17813)
- feat(plan): add core logic and exit_plan_mode tool definition by @jerop in
[#18110](https://github.com/google-gemini/gemini-cli/pull/18110)
- feat(core): rename search_file_content tool to grep_search and add legacy
alias by @SandyTao520 in
[#18003](https://github.com/google-gemini/gemini-cli/pull/18003)
- fix(core): prioritize detailed error messages for code assist setup by
@gsquared94 in
[#17852](https://github.com/google-gemini/gemini-cli/pull/17852)
- fix(cli): resolve environment loading and auth validation issues in ACP mode
by @bdmorgan in
[#18025](https://github.com/google-gemini/gemini-cli/pull/18025)
- feat(core): add .agents/skills directory alias for skill discovery by
@NTaylorMullen in
[#18559](https://github.com/google-gemini/gemini-cli/pull/18559)
- chore: remove feedback instruction from system prompt by @NTaylorMullen in
[#18560](https://github.com/google-gemini/gemini-cli/pull/18560)
- feat(context): add remote configuration for tool output masking thresholds by
@abhipatel12 in
[#18553](https://github.com/google-gemini/gemini-cli/pull/18553)
- feat(core): pause agent timeout budget while waiting for tool confirmation by
@abhipatel12 in
[#18415](https://github.com/google-gemini/gemini-cli/pull/18415)
- refactor(config): remove experimental.enableEventDrivenScheduler setting by
@abhipatel12 in
[#17924](https://github.com/google-gemini/gemini-cli/pull/17924)
- feat(cli): truncate shell output in UI history and improve active shell
display by @jwhelangoog in
[#17438](https://github.com/google-gemini/gemini-cli/pull/17438)
- refactor(cli): switch useToolScheduler to event-driven engine by @abhipatel12
in [#18565](https://github.com/google-gemini/gemini-cli/pull/18565)
- fix(core): correct escaped interpolation in system prompt by @NTaylorMullen in
[#18557](https://github.com/google-gemini/gemini-cli/pull/18557)
- propagate abortSignal by @scidomino in
[#18477](https://github.com/google-gemini/gemini-cli/pull/18477)
- feat(core): conditionally include ctrl+f prompt based on interactive shell
setting by @NTaylorMullen in
[#18561](https://github.com/google-gemini/gemini-cli/pull/18561)
- fix(core): ensure `enter_plan_mode` tool registration respects
`experimental.plan` by @jerop in
[#18587](https://github.com/google-gemini/gemini-cli/pull/18587)
- feat(core): transition sub-agents to XML format and improve definitions by
@NTaylorMullen in
[#18555](https://github.com/google-gemini/gemini-cli/pull/18555)
- docs: Add Plan Mode documentation by @jerop in
[#18582](https://github.com/google-gemini/gemini-cli/pull/18582)
- chore: strengthen validation guidance in system prompt by @NTaylorMullen in
[#18544](https://github.com/google-gemini/gemini-cli/pull/18544)
- Fix newline insertion bug in replace tool by @werdnum in
[#18595](https://github.com/google-gemini/gemini-cli/pull/18595)
- fix(evals): update save_memory evals and simplify tool description by
@NTaylorMullen in
[#18610](https://github.com/google-gemini/gemini-cli/pull/18610)
- chore(evals): update validation_fidelity_pre_existing_errors to USUALLY_PASSES
by @NTaylorMullen in
[#18617](https://github.com/google-gemini/gemini-cli/pull/18617)
- fix: shorten tool call IDs and fix duplicate tool name in truncated output
filenames by @SandyTao520 in
[#18600](https://github.com/google-gemini/gemini-cli/pull/18600)
- feat(cli): implement atomic writes and safety checks for trusted folders by
@galz10 in [#18406](https://github.com/google-gemini/gemini-cli/pull/18406)
- Remove relative docs links by @chrstnb in
[#18650](https://github.com/google-gemini/gemini-cli/pull/18650)
- docs: add legacy snippets convention to GEMINI.md by @NTaylorMullen in
[#18597](https://github.com/google-gemini/gemini-cli/pull/18597)
- fix(chore): Support linting for cjs by @aswinashok44 in
[#18639](https://github.com/google-gemini/gemini-cli/pull/18639)
- feat: move shell efficiency guidelines to tool description by @NTaylorMullen
in [#18614](https://github.com/google-gemini/gemini-cli/pull/18614)
- Added "" as default value, since getText() used to expect a string only and
thus crashed when undefined... Fixes #18076 by @019-Abhi in
[#18099](https://github.com/google-gemini/gemini-cli/pull/18099)
- Allow @-includes outside of workspaces (with permission) by @scidomino in
[#18470](https://github.com/google-gemini/gemini-cli/pull/18470)
- chore: make `ask_user` header description more clear by @jackwotherspoon in
[#18657](https://github.com/google-gemini/gemini-cli/pull/18657)
- refactor(core): model-dependent tool definitions by @aishaneeshah in
[#18563](https://github.com/google-gemini/gemini-cli/pull/18563)
- Harded code assist converter. by @jacob314 in
[#18656](https://github.com/google-gemini/gemini-cli/pull/18656)
- bug(core): Fix minor bug in migration logic. by @joshualitt in
[#18661](https://github.com/google-gemini/gemini-cli/pull/18661)
- feat: enable plan mode experiment in settings by @jerop in
[#18636](https://github.com/google-gemini/gemini-cli/pull/18636)
- refactor: push isValidPath() into parsePastedPaths() by @scidomino in
[#18664](https://github.com/google-gemini/gemini-cli/pull/18664)
- fix(cli): correct 'esc to cancel' position and restore duration display by
@NTaylorMullen in
[#18534](https://github.com/google-gemini/gemini-cli/pull/18534)
- feat(cli): add DevTools integration with gemini-cli-devtools by @SandyTao520
in [#18648](https://github.com/google-gemini/gemini-cli/pull/18648)
- chore: remove unused exports and redundant hook files by @SandyTao520 in
[#18681](https://github.com/google-gemini/gemini-cli/pull/18681)
- Fix number of lines being reported in rewind confirmation dialog by @Adib234
in [#18675](https://github.com/google-gemini/gemini-cli/pull/18675)
- feat(cli): disable folder trust in headless mode by @galz10 in
[#18407](https://github.com/google-gemini/gemini-cli/pull/18407)
- Disallow unsafe type assertions by @gundermanc in
[#18688](https://github.com/google-gemini/gemini-cli/pull/18688)
- Change event type for release by @g-samroberts in
[#18693](https://github.com/google-gemini/gemini-cli/pull/18693)
- feat: handle multiple dynamic context filenames in system prompt by
@NTaylorMullen in
[#18598](https://github.com/google-gemini/gemini-cli/pull/18598)
- Properly parse at-commands with narrow non-breaking spaces by @scidomino in
[#18677](https://github.com/google-gemini/gemini-cli/pull/18677)
- refactor(core): centralize core tool definitions and support model-specific
schemas by @aishaneeshah in
[#18662](https://github.com/google-gemini/gemini-cli/pull/18662)
- feat(core): Render memory hierarchically in context. by @joshualitt in
[#18350](https://github.com/google-gemini/gemini-cli/pull/18350)
- feat: Ctrl+O to expand paste placeholder by @jackwotherspoon in
[#18103](https://github.com/google-gemini/gemini-cli/pull/18103)
- fix(cli): Improve header spacing by @NTaylorMullen in
[#18531](https://github.com/google-gemini/gemini-cli/pull/18531)
- Feature/quota visibility 16795 by @spencer426 in
[#18203](https://github.com/google-gemini/gemini-cli/pull/18203)
- Inline thinking bubbles with summary/full modes by @LyalinDotCom in
[#18033](https://github.com/google-gemini/gemini-cli/pull/18033)
- docs: remove TOC marker from Plan Mode header by @jerop in
[#18678](https://github.com/google-gemini/gemini-cli/pull/18678)
- fix(ui): remove redundant newlines in Gemini messages by @NTaylorMullen in
[#18538](https://github.com/google-gemini/gemini-cli/pull/18538)
- test(cli): fix AppContainer act() warnings and improve waitFor resilience by
@NTaylorMullen in
[#18676](https://github.com/google-gemini/gemini-cli/pull/18676)
- refactor(core): refine Security & System Integrity section in system prompt by
@NTaylorMullen in
[#18601](https://github.com/google-gemini/gemini-cli/pull/18601)
- Fix layout rounding. by @gundermanc in
[#18667](https://github.com/google-gemini/gemini-cli/pull/18667)
- docs(skills): enhance pr-creator safety and interactivity by @NTaylorMullen in
[#18616](https://github.com/google-gemini/gemini-cli/pull/18616)
- test(core): remove hardcoded model from TestRig by @NTaylorMullen in
[#18710](https://github.com/google-gemini/gemini-cli/pull/18710)
- feat(core): optimize sub-agents system prompt intro by @NTaylorMullen in
[#18608](https://github.com/google-gemini/gemini-cli/pull/18608)
- feat(cli): update approval mode labels and shortcuts per latest UX spec by
@jerop in [#18698](https://github.com/google-gemini/gemini-cli/pull/18698)
- fix(plan): update persistent approval mode setting by @Adib234 in
[#18638](https://github.com/google-gemini/gemini-cli/pull/18638)
- fix: move toasts location to left side by @jackwotherspoon in
[#18705](https://github.com/google-gemini/gemini-cli/pull/18705)
- feat(routing): restrict numerical routing to Gemini 3 family by @mattKorwel in
[#18478](https://github.com/google-gemini/gemini-cli/pull/18478)
- fix(ide): fix ide nudge setting by @skeshive in
[#18733](https://github.com/google-gemini/gemini-cli/pull/18733)
- fix(core): standardize tool formatting in system prompts by @NTaylorMullen in
[#18615](https://github.com/google-gemini/gemini-cli/pull/18615)
- chore: consolidate to green in ask user dialog by @jackwotherspoon in
[#18734](https://github.com/google-gemini/gemini-cli/pull/18734)
- feat: add `extensionsExplore` setting to enable extensions explore UI. by
@sripasg in [#18686](https://github.com/google-gemini/gemini-cli/pull/18686)
- feat(cli): defer devtools startup and integrate with F12 by @SandyTao520 in
[#18695](https://github.com/google-gemini/gemini-cli/pull/18695)
- ui: update & subdue footer colors and animate progress indicator by
@keithguerin in
[#18570](https://github.com/google-gemini/gemini-cli/pull/18570)
- test: add model-specific snapshots for coreTools by @aishaneeshah in
[#18707](https://github.com/google-gemini/gemini-cli/pull/18707)
- ci: shard windows tests and fix event listener leaks by @NTaylorMullen in
[#18670](https://github.com/google-gemini/gemini-cli/pull/18670)
- fix: allow `ask_user` tool in yolo mode by @jackwotherspoon in
[#18541](https://github.com/google-gemini/gemini-cli/pull/18541)
- feat: redact disabled tools from system prompt (#13597) by @NTaylorMullen in
[#18613](https://github.com/google-gemini/gemini-cli/pull/18613)
- Update Gemini.md to use the curent year on creating new files by @sehoon38 in
[#18460](https://github.com/google-gemini/gemini-cli/pull/18460)
- Code review cleanup for thinking display by @jacob314 in
[#18720](https://github.com/google-gemini/gemini-cli/pull/18720)
- fix(cli): hide scrollbars when in alternate buffer copy mode by @werdnum in
[#18354](https://github.com/google-gemini/gemini-cli/pull/18354)
- Fix issues with rip grep by @gundermanc in
[#18756](https://github.com/google-gemini/gemini-cli/pull/18756)
- fix(cli): fix history navigation regression after prompt autocomplete by
@sehoon38 in [#18752](https://github.com/google-gemini/gemini-cli/pull/18752)
- chore: cleanup unused and add unlisted dependencies in packages/cli by
[#18151](https://github.com/google-gemini/gemini-cli/pull/18151)
- chore(core): reassign telemetry keys to avoid server conflict by @mattKorwel
in [#18161](https://github.com/google-gemini/gemini-cli/pull/18161)
- Add link to rewind doc in commands.md by @Adib234 in
[#17961](https://github.com/google-gemini/gemini-cli/pull/17961)
- feat(core): add draft-2020-12 JSON Schema support with lenient fallback by
@afarber in [#15060](https://github.com/google-gemini/gemini-cli/pull/15060)
- refactor(core): robust trimPreservingTrailingNewline and regression test by
@adamfweidman in
[#18749](https://github.com/google-gemini/gemini-cli/pull/18749)
- Fix issue where Gemini CLI creates tests in a new file by @gundermanc in
[#18409](https://github.com/google-gemini/gemini-cli/pull/18409)
- feat(telemetry): Ensure experiment IDs are included in OpenTelemetry logs by
@kevin-ramdass in
[#18747](https://github.com/google-gemini/gemini-cli/pull/18747)
- fix(patch): cherry-pick e9a9474 to release/v0.29.0-preview.0-pr-18840 to patch
version v0.29.0-preview.0 and create version 0.29.0-preview.1 by
@gemini-cli-robot in
[#18841](https://github.com/google-gemini/gemini-cli/pull/18841)
- fix(patch): cherry-pick 08e8eea to release/v0.29.0-preview.1-pr-18855 to patch
version v0.29.0-preview.1 and create version 0.29.0-preview.2 by
@gemini-cli-robot in
[#18905](https://github.com/google-gemini/gemini-cli/pull/18905)
- fix(patch): cherry-pick d0c6a56 to release/v0.29.0-preview.2-pr-18976 to patch
version v0.29.0-preview.2 and create version 0.29.0-preview.3 by
@gemini-cli-robot in
[#19023](https://github.com/google-gemini/gemini-cli/pull/19023)
- fix(patch): cherry-pick e5ff202 to release/v0.29.0-preview.3-pr-19254 to patch
version v0.29.0-preview.3 and create version 0.29.0-preview.4 by
@gemini-cli-robot in
[#19264](https://github.com/google-gemini/gemini-cli/pull/19264)
- fix(patch): cherry-pick 9590a09 to release/v0.29.0-preview.4-pr-18771 to patch
version v0.29.0-preview.4 and create version 0.29.0-preview.5 by
@gemini-cli-robot in
[#19274](https://github.com/google-gemini/gemini-cli/pull/19274)
[#18196](https://github.com/google-gemini/gemini-cli/pull/18196)
- Remove MCP servers on extension uninstall by @chrstnb in
[#18121](https://github.com/google-gemini/gemini-cli/pull/18121)
- refactor: localize ACP error parsing logic to cli package by @bdmorgan in
[#18193](https://github.com/google-gemini/gemini-cli/pull/18193)
- feat(core): Add A2A auth config types by @adamfweidman in
[#18205](https://github.com/google-gemini/gemini-cli/pull/18205)
- Set default max attempts to 3 and use the common variable by @sehoon38 in
[#18209](https://github.com/google-gemini/gemini-cli/pull/18209)
- feat(plan): add exit_plan_mode ui and prompt by @jerop in
[#18162](https://github.com/google-gemini/gemini-cli/pull/18162)
- fix(test): improve test isolation and enable subagent evaluations by
@cocosheng-g in
[#18138](https://github.com/google-gemini/gemini-cli/pull/18138)
- feat(plan): use custom deny messages in plan mode policies by @Adib234 in
[#18195](https://github.com/google-gemini/gemini-cli/pull/18195)
- Match on extension ID when stopping extensions by @chrstnb in
[#18218](https://github.com/google-gemini/gemini-cli/pull/18218)
- fix(core): Respect user's .gitignore preference by @xyrolle in
[#15482](https://github.com/google-gemini/gemini-cli/pull/15482)
- docs: document GEMINI_CLI_HOME environment variable by @adamfweidman in
[#18219](https://github.com/google-gemini/gemini-cli/pull/18219)
- chore(core): explicitly state plan storage path in prompt by @jerop in
[#18222](https://github.com/google-gemini/gemini-cli/pull/18222)
- A2a admin setting by @DavidAPierce in
[#17868](https://github.com/google-gemini/gemini-cli/pull/17868)
- feat(a2a): Add pluggable auth provider infrastructure by @adamfweidman in
[#17934](https://github.com/google-gemini/gemini-cli/pull/17934)
- Fix handling of empty settings by @chrstnb in
[#18131](https://github.com/google-gemini/gemini-cli/pull/18131)
- Reload skills when extensions change by @chrstnb in
[#18225](https://github.com/google-gemini/gemini-cli/pull/18225)
- feat: Add markdown rendering to ask_user tool by @jackwotherspoon in
[#18211](https://github.com/google-gemini/gemini-cli/pull/18211)
- Add telemetry to rewind by @Adib234 in
[#18122](https://github.com/google-gemini/gemini-cli/pull/18122)
- feat(admin): add support for MCP configuration via admin controls (pt1) by
@skeshive in [#18223](https://github.com/google-gemini/gemini-cli/pull/18223)
- feat(core): require user consent before MCP server OAuth by @ehedlund in
[#18132](https://github.com/google-gemini/gemini-cli/pull/18132)
- fix(sandbox): propagate GOOGLE_GEMINI_BASE_URL&GOOGLE_VERTEX_BASE_URL env vars
by @skeshive in
[#18231](https://github.com/google-gemini/gemini-cli/pull/18231)
- feat(ui): move user identity display to header by @sehoon38 in
[#18216](https://github.com/google-gemini/gemini-cli/pull/18216)
- fix: enforce folder trust for workspace settings, skills, and context by
@galz10 in [#17596](https://github.com/google-gemini/gemini-cli/pull/17596)
**Full Changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.28.2...v0.29.0
https://github.com/google-gemini/gemini-cli/compare/v0.27.0...v0.28.0
+349 -296
View File
@@ -1,6 +1,6 @@
# Preview release: v0.30.0-preview.3
# Preview release: Release v0.29.0-preview.0
Released: February 19, 2026
Released: February 10, 2026
Our preview release includes the latest, new, and experimental features. This
release may not be as stable as our [latest weekly release](latest.md).
@@ -13,302 +13,355 @@ npm install -g @google/gemini-cli@preview
## Highlights
- **Initial SDK Package:** Introduced the initial SDK package with support for
custom skills and dynamic system instructions.
- **Refined Plan Mode:** Refined Plan Mode with support for enabling skills,
improved agentic execution, and project exploration without planning.
- **Enhanced CLI UI:** Enhanced CLI UI with a new clean UI toggle, minimal-mode
bleed-through, and support for Ctrl-Z suspension.
- **`--policy` flag:** Added the `--policy` flag to support user-defined
policies.
- **New Themes:** Added Solarized Dark and Solarized Light themes.
- **Plan Mode Enhancements**: Significant updates to Plan Mode, including new
commands, support for MCP servers, integration of planning artifacts, and
improved iteration guidance.
- **Core Agent Improvements**: Enhancements to the core agent, including better
system prompt rigor, improved subagent definitions, and enhanced tool
execution limits.
- **CLI UX/UI Updates**: Various UI and UX improvements, such as autocomplete in
the input prompt, updated approval mode labels, DevTools integration, and
improved header spacing.
- **Tooling & Extension Updates**: Improvements to existing tools like
`ask_user` and `grep_search`, and new features for extension management.
- **Bug Fixes**: Numerous bug fixes across the CLI and core, addressing issues
with interactive commands, memory leaks, permission checks, and more.
- **Context and Tool Output Management**: Features for observation masking for
tool outputs, session-linked tool output storage, and persistence for masked
tool outputs.
## What's Changed
- fix(patch): cherry-pick 261788c to release/v0.30.0-preview.0-pr-19453 to patch
version v0.30.0-preview.0 and create version 0.30.0-preview.1 by
@gemini-cli-robot in
[#19490](https://github.com/google-gemini/gemini-cli/pull/19490)
- feat(ux): added text wrapping capabilities to markdown tables by @devr0306 in
[#18240](https://github.com/google-gemini/gemini-cli/pull/18240)
- Revert "fix(mcp): ensure MCP transport is closed to prevent memory leaks" by
@skeshive in [#18771](https://github.com/google-gemini/gemini-cli/pull/18771)
- chore(release): bump version to 0.30.0-nightly.20260210.a2174751d by
@gemini-cli-robot in
[#18772](https://github.com/google-gemini/gemini-cli/pull/18772)
- chore: cleanup unused and add unlisted dependencies in packages/core by
@adamfweidman in
[#18762](https://github.com/google-gemini/gemini-cli/pull/18762)
- chore(core): update activate_skill prompt verbiage to be more direct by
@NTaylorMullen in
[#18605](https://github.com/google-gemini/gemini-cli/pull/18605)
- Add autoconfigure memory usage setting to the dialog by @jacob314 in
[#18510](https://github.com/google-gemini/gemini-cli/pull/18510)
- fix(core): prevent race condition in policy persistence by @braddux in
[#18506](https://github.com/google-gemini/gemini-cli/pull/18506)
- fix(evals): prevent false positive in hierarchical memory test by
@Abhijit-2592 in
[#18777](https://github.com/google-gemini/gemini-cli/pull/18777)
- test(evals): mark all `save_memory` evals as `USUALLY_PASSES` due to
unreliability by @jerop in
[#18786](https://github.com/google-gemini/gemini-cli/pull/18786)
- feat(cli): add setting to hide shortcuts hint UI by @LyalinDotCom in
[#18562](https://github.com/google-gemini/gemini-cli/pull/18562)
- feat(core): formalize 5-phase sequential planning workflow by @jerop in
[#18759](https://github.com/google-gemini/gemini-cli/pull/18759)
- Introduce limits for search results. by @gundermanc in
[#18767](https://github.com/google-gemini/gemini-cli/pull/18767)
- fix(cli): allow closing debug console after auto-open via flicker by
@SandyTao520 in
[#18795](https://github.com/google-gemini/gemini-cli/pull/18795)
- feat(masking): enable tool output masking by default by @abhipatel12 in
[#18564](https://github.com/google-gemini/gemini-cli/pull/18564)
- perf(ui): optimize table rendering by memoizing styled characters by @devr0306
in [#18770](https://github.com/google-gemini/gemini-cli/pull/18770)
- feat: multi-line text answers in ask-user tool by @jackwotherspoon in
[#18741](https://github.com/google-gemini/gemini-cli/pull/18741)
- perf(cli): truncate large debug logs and limit message history by @mattKorwel
in [#18663](https://github.com/google-gemini/gemini-cli/pull/18663)
- fix(core): complete MCP discovery when configured servers are skipped by
@LyalinDotCom in
[#18586](https://github.com/google-gemini/gemini-cli/pull/18586)
- fix(core): cache CLI version to ensure consistency during sessions by
@sehoon38 in [#18793](https://github.com/google-gemini/gemini-cli/pull/18793)
- fix(cli): resolve double rendering in shpool and address vscode lint warnings
by @braddux in
[#18704](https://github.com/google-gemini/gemini-cli/pull/18704)
- feat(plan): document and validate Plan Mode policy overrides by @jerop in
[#18825](https://github.com/google-gemini/gemini-cli/pull/18825)
- Fix pressing any key to exit select mode. by @jacob314 in
[#18421](https://github.com/google-gemini/gemini-cli/pull/18421)
- fix(cli): update F12 behavior to only open drawer if browser fails by
@SandyTao520 in
[#18829](https://github.com/google-gemini/gemini-cli/pull/18829)
- feat(plan): allow skills to be enabled in plan mode by @Adib234 in
[#18817](https://github.com/google-gemini/gemini-cli/pull/18817)
- docs(plan): add documentation for plan mode tools by @jerop in
[#18827](https://github.com/google-gemini/gemini-cli/pull/18827)
- Remove experimental note in extension settings docs by @chrstnb in
[#18822](https://github.com/google-gemini/gemini-cli/pull/18822)
- Update prompt and grep tool definition to limit context size by @gundermanc in
[#18780](https://github.com/google-gemini/gemini-cli/pull/18780)
- docs(plan): add `ask_user` tool documentation by @jerop in
[#18830](https://github.com/google-gemini/gemini-cli/pull/18830)
- Revert unintended credentials exposure by @Adib234 in
[#18840](https://github.com/google-gemini/gemini-cli/pull/18840)
- feat(core): update internal utility models to Gemini 3 by @SandyTao520 in
[#18773](https://github.com/google-gemini/gemini-cli/pull/18773)
- feat(a2a): add value-resolver for auth credential resolution by @adamfweidman
in [#18653](https://github.com/google-gemini/gemini-cli/pull/18653)
- Removed getPlainTextLength by @devr0306 in
[#18848](https://github.com/google-gemini/gemini-cli/pull/18848)
- More grep prompt tweaks by @gundermanc in
[#18846](https://github.com/google-gemini/gemini-cli/pull/18846)
- refactor(cli): Reactive useSettingsStore hook by @psinha40898 in
[#14915](https://github.com/google-gemini/gemini-cli/pull/14915)
- fix(mcp): Ensure that stdio MCP server execution has the `GEMINI_CLI=1` env
variable populated. by @richieforeman in
[#18832](https://github.com/google-gemini/gemini-cli/pull/18832)
- fix(core): improve headless mode detection for flags and query args by @galz10
in [#18855](https://github.com/google-gemini/gemini-cli/pull/18855)
- refactor(cli): simplify UI and remove legacy inline tool confirmation logic by
@abhipatel12 in
[#18566](https://github.com/google-gemini/gemini-cli/pull/18566)
- feat(cli): deprecate --allowed-tools and excludeTools in favor of policy
engine by @Abhijit-2592 in
[#18508](https://github.com/google-gemini/gemini-cli/pull/18508)
- fix(workflows): improve maintainer detection for automated PR actions by
@bdmorgan in [#18869](https://github.com/google-gemini/gemini-cli/pull/18869)
- refactor(cli): consolidate useToolScheduler and delete legacy implementation
by @abhipatel12 in
[#18567](https://github.com/google-gemini/gemini-cli/pull/18567)
- Update changelog for v0.28.0 and v0.29.0-preview0 by @g-samroberts in
[#18819](https://github.com/google-gemini/gemini-cli/pull/18819)
- fix(core): ensure sub-agents are registered regardless of tools.allowed by
@mattKorwel in
[#18870](https://github.com/google-gemini/gemini-cli/pull/18870)
- Show notification when there's a conflict with an extensions command by
@chrstnb in [#17890](https://github.com/google-gemini/gemini-cli/pull/17890)
- fix(cli): dismiss '?' shortcuts help on hotkeys and active states by
@LyalinDotCom in
[#18583](https://github.com/google-gemini/gemini-cli/pull/18583)
- fix(core): prioritize conditional policy rules and harden Plan Mode by
@Abhijit-2592 in
[#18882](https://github.com/google-gemini/gemini-cli/pull/18882)
- feat(core): refine Plan Mode system prompt for agentic execution by
@NTaylorMullen in
[#18799](https://github.com/google-gemini/gemini-cli/pull/18799)
- feat(plan): create metrics for usage of `AskUser` tool by @Adib234 in
[#18820](https://github.com/google-gemini/gemini-cli/pull/18820)
- feat(cli): support Ctrl-Z suspension by @scidomino in
[#18931](https://github.com/google-gemini/gemini-cli/pull/18931)
- fix(github-actions): use robot PAT for release creation to trigger release
notes by @SandyTao520 in
[#18794](https://github.com/google-gemini/gemini-cli/pull/18794)
- feat: add strict seatbelt profiles and remove unusable closed profiles by
@SandyTao520 in
[#18876](https://github.com/google-gemini/gemini-cli/pull/18876)
- chore: cleanup unused and add unlisted dependencies in packages/a2a-server by
@adamfweidman in
[#18916](https://github.com/google-gemini/gemini-cli/pull/18916)
- fix(plan): isolate plan files per session by @Adib234 in
[#18757](https://github.com/google-gemini/gemini-cli/pull/18757)
- fix: character truncation in raw markdown mode by @jackwotherspoon in
[#18938](https://github.com/google-gemini/gemini-cli/pull/18938)
- feat(cli): prototype clean UI toggle and minimal-mode bleed-through by
@LyalinDotCom in
[#18683](https://github.com/google-gemini/gemini-cli/pull/18683)
- ui(polish) blend background color with theme by @jacob314 in
[#18802](https://github.com/google-gemini/gemini-cli/pull/18802)
- Add generic searchable list to back settings and extensions by @chrstnb in
[#18838](https://github.com/google-gemini/gemini-cli/pull/18838)
- feat(ui): align `AskUser` color scheme with UX spec by @jerop in
[#18943](https://github.com/google-gemini/gemini-cli/pull/18943)
- Hide AskUser tool validation errors from UI (agent self-corrects) by @jerop in
[#18954](https://github.com/google-gemini/gemini-cli/pull/18954)
- bug(cli) fix flicker due to AppContainer continuous initialization by
@jacob314 in [#18958](https://github.com/google-gemini/gemini-cli/pull/18958)
- feat(admin): Add admin controls documentation by @skeshive in
[#18644](https://github.com/google-gemini/gemini-cli/pull/18644)
- feat(cli): disable ctrl-s shortcut outside of alternate buffer mode by
@jacob314 in [#18887](https://github.com/google-gemini/gemini-cli/pull/18887)
- fix(vim): vim support that feels (more) complete by @ppgranger in
[#18755](https://github.com/google-gemini/gemini-cli/pull/18755)
- feat(policy): add --policy flag for user defined policies by @allenhutchison
in [#18500](https://github.com/google-gemini/gemini-cli/pull/18500)
- Update installation guide by @g-samroberts in
[#18823](https://github.com/google-gemini/gemini-cli/pull/18823)
- refactor(core): centralize tool definitions (Group 1: replace, search, grep)
by @aishaneeshah in
[#18944](https://github.com/google-gemini/gemini-cli/pull/18944)
- refactor(cli): finalize event-driven transition and remove interaction bridge
by @abhipatel12 in
[#18569](https://github.com/google-gemini/gemini-cli/pull/18569)
- Fix drag and drop escaping by @scidomino in
[#18965](https://github.com/google-gemini/gemini-cli/pull/18965)
- feat(sdk): initial package bootstrap for SDK by @mbleigh in
[#18861](https://github.com/google-gemini/gemini-cli/pull/18861)
- feat(sdk): implements SessionContext for SDK tool calls by @mbleigh in
[#18862](https://github.com/google-gemini/gemini-cli/pull/18862)
- fix(plan): make question type required in AskUser tool by @Adib234 in
[#18959](https://github.com/google-gemini/gemini-cli/pull/18959)
- fix(core): ensure --yolo does not force headless mode by @NTaylorMullen in
[#18976](https://github.com/google-gemini/gemini-cli/pull/18976)
- refactor(core): adopt `CoreToolCallStatus` enum for type safety by @jerop in
[#18998](https://github.com/google-gemini/gemini-cli/pull/18998)
- Enable in-CLI extension management commands for team by @chrstnb in
[#18957](https://github.com/google-gemini/gemini-cli/pull/18957)
- Adjust lint rules to avoid unnecessary warning. by @scidomino in
[#18970](https://github.com/google-gemini/gemini-cli/pull/18970)
- fix(vscode): resolve unsafe type assertion lint errors by @ehedlund in
[#19006](https://github.com/google-gemini/gemini-cli/pull/19006)
- Remove unnecessary eslint config file by @scidomino in
[#19015](https://github.com/google-gemini/gemini-cli/pull/19015)
- fix(core): Prevent loop detection false positives on lists with long shared
prefixes by @SandyTao520 in
[#18975](https://github.com/google-gemini/gemini-cli/pull/18975)
- feat(core): fallback to chat-base when using unrecognized models for chat by
@SandyTao520 in
[#19016](https://github.com/google-gemini/gemini-cli/pull/19016)
- docs: fix inconsistent commandRegex example in policy engine by @NTaylorMullen
in [#19027](https://github.com/google-gemini/gemini-cli/pull/19027)
- fix(plan): persist the approval mode in UI even when agent is thinking by
@Adib234 in [#18955](https://github.com/google-gemini/gemini-cli/pull/18955)
- feat(sdk): Implement dynamic system instructions by @mbleigh in
[#18863](https://github.com/google-gemini/gemini-cli/pull/18863)
- Docs: Refresh docs to organize and standardize reference materials. by
@jkcinouye in [#18403](https://github.com/google-gemini/gemini-cli/pull/18403)
- fix windows escaping (and broken tests) by @scidomino in
[#19011](https://github.com/google-gemini/gemini-cli/pull/19011)
- refactor: use `CoreToolCallStatus` in the the history data model by @jerop in
[#19033](https://github.com/google-gemini/gemini-cli/pull/19033)
- feat(cleanup): enable 30-day session retention by default by @skeshive in
[#18854](https://github.com/google-gemini/gemini-cli/pull/18854)
- feat(plan): hide plan write and edit operations on plans in Plan Mode by
@jerop in [#19012](https://github.com/google-gemini/gemini-cli/pull/19012)
- bug(ui) fix flicker refreshing background color by @jacob314 in
[#19041](https://github.com/google-gemini/gemini-cli/pull/19041)
- chore: fix dep vulnerabilities by @scidomino in
[#19036](https://github.com/google-gemini/gemini-cli/pull/19036)
- Revamp automated changelog skill by @g-samroberts in
[#18974](https://github.com/google-gemini/gemini-cli/pull/18974)
- feat(sdk): implement support for custom skills by @mbleigh in
[#19031](https://github.com/google-gemini/gemini-cli/pull/19031)
- refactor(core): complete centralization of core tool definitions by
@aishaneeshah in
[#18991](https://github.com/google-gemini/gemini-cli/pull/18991)
- feat: add /commands reload to refresh custom TOML commands by @korade-krushna
in [#19078](https://github.com/google-gemini/gemini-cli/pull/19078)
- fix(cli): wrap terminal capability queries in hidden sequence by @srithreepo
in [#19080](https://github.com/google-gemini/gemini-cli/pull/19080)
- fix(workflows): fix GitHub App token permissions for maintainer detection by
@bdmorgan in [#19139](https://github.com/google-gemini/gemini-cli/pull/19139)
- test: fix hook integration test flakiness on Windows CI by @NTaylorMullen in
[#18665](https://github.com/google-gemini/gemini-cli/pull/18665)
- fix(core): Encourage non-interactive flags for scaffolding commands by
@NTaylorMullen in
[#18804](https://github.com/google-gemini/gemini-cli/pull/18804)
- fix(core): propagate User-Agent header to setup-phase CodeAssist API calls by
@gsquared94 in
[#19182](https://github.com/google-gemini/gemini-cli/pull/19182)
- docs: document .agents/skills alias and discovery precedence by @kevmoo in
[#19166](https://github.com/google-gemini/gemini-cli/pull/19166)
- feat(cli): add loading state to new agents notification by @sehoon38 in
[#19190](https://github.com/google-gemini/gemini-cli/pull/19190)
- Add base branch to workflow. by @g-samroberts in
[#19189](https://github.com/google-gemini/gemini-cli/pull/19189)
- feat(cli): handle invalid model names in useQuotaAndFallback by @sehoon38 in
[#19222](https://github.com/google-gemini/gemini-cli/pull/19222)
- docs: custom themes in extensions by @jackwotherspoon in
[#19219](https://github.com/google-gemini/gemini-cli/pull/19219)
- Disable workspace settings when starting GCLI in the home directory. by
@kevinjwang1 in
[#19034](https://github.com/google-gemini/gemini-cli/pull/19034)
- feat(cli): refactor model command to support set and manage subcommands by
@sehoon38 in [#19221](https://github.com/google-gemini/gemini-cli/pull/19221)
- Add refresh/reload aliases to slash command subcommands by @korade-krushna in
[#19218](https://github.com/google-gemini/gemini-cli/pull/19218)
- refactor: consolidate development rules and add cli guidelines by @jacob314 in
[#19214](https://github.com/google-gemini/gemini-cli/pull/19214)
- chore(ui): remove outdated tip about model routing by @sehoon38 in
[#19226](https://github.com/google-gemini/gemini-cli/pull/19226)
- feat(core): support custom reasoning models by default by @NTaylorMullen in
[#19227](https://github.com/google-gemini/gemini-cli/pull/19227)
- Add Solarized Dark and Solarized Light themes by @rmedranollamas in
[#19064](https://github.com/google-gemini/gemini-cli/pull/19064)
- fix(telemetry): replace JSON.stringify with safeJsonStringify in file
exporters by @gsquared94 in
[#19244](https://github.com/google-gemini/gemini-cli/pull/19244)
- feat(telemetry): add keychain availability and token storage metrics by
@abhipatel12 in
[#18971](https://github.com/google-gemini/gemini-cli/pull/18971)
- feat(cli): update approval mode cycle order by @jerop in
[#19254](https://github.com/google-gemini/gemini-cli/pull/19254)
- refactor(cli): code review cleanup fix for tab+tab by @jacob314 in
[#18967](https://github.com/google-gemini/gemini-cli/pull/18967)
- feat(plan): support project exploration without planning when in plan mode by
@Adib234 in [#18992](https://github.com/google-gemini/gemini-cli/pull/18992)
- feat: add role-specific statistics to telemetry and UI (cont. #15234) by
@yunaseoul in [#18824](https://github.com/google-gemini/gemini-cli/pull/18824)
- feat(cli): remove Plan Mode from rotation when actively working by @jerop in
[#19262](https://github.com/google-gemini/gemini-cli/pull/19262)
- Fix side breakage where anchors don't work in slugs. by @g-samroberts in
[#19261](https://github.com/google-gemini/gemini-cli/pull/19261)
- feat(config): add setting to make directory tree context configurable by
@kevin-ramdass in
[#19053](https://github.com/google-gemini/gemini-cli/pull/19053)
- fix(acp): Wait for mcp initialization in acp (#18893) by @Mervap in
[#18894](https://github.com/google-gemini/gemini-cli/pull/18894)
- docs: format UTC times in releases doc by @pavan-sh in
[#18169](https://github.com/google-gemini/gemini-cli/pull/18169)
- Docs: Clarify extensions documentation. by @jkcinouye in
[#19277](https://github.com/google-gemini/gemini-cli/pull/19277)
- refactor(core): modularize tool definitions by model family by @aishaneeshah
in [#19269](https://github.com/google-gemini/gemini-cli/pull/19269)
- fix(paths): Add cross-platform path normalization by @spencer426 in
[#18939](https://github.com/google-gemini/gemini-cli/pull/18939)
- feat(core): experimental in-progress steering hints (1 of 3) by @joshualitt in
[#19008](https://github.com/google-gemini/gemini-cli/pull/19008)
- fix: remove ask_user tool from non-interactive modes by jackwotherspoon in
[#18154](https://github.com/google-gemini/gemini-cli/pull/18154)
- fix(cli): allow restricted .env loading in untrusted sandboxed folders by
galz10 in [#17806](https://github.com/google-gemini/gemini-cli/pull/17806)
- Encourage agent to utilize ecosystem tools to perform work by gundermanc in
[#17881](https://github.com/google-gemini/gemini-cli/pull/17881)
- feat(plan): unify workflow location in system prompt to optimize caching by
jerop in [#18258](https://github.com/google-gemini/gemini-cli/pull/18258)
- feat(core): enable getUserTierName in config by sehoon38 in
[#18265](https://github.com/google-gemini/gemini-cli/pull/18265)
- feat(core): add default execution limits for subagents by abhipatel12 in
[#18274](https://github.com/google-gemini/gemini-cli/pull/18274)
- Fix issue where agent gets stuck at interactive commands. by gundermanc in
[#18272](https://github.com/google-gemini/gemini-cli/pull/18272)
- chore(release): bump version to 0.29.0-nightly.20260203.71f46f116 by
gemini-cli-robot in
[#18243](https://github.com/google-gemini/gemini-cli/pull/18243)
- feat(core): remove hardcoded policy bypass for local subagents by abhipatel12
in [#18153](https://github.com/google-gemini/gemini-cli/pull/18153)
- feat(plan): implement plan slash command by Adib234 in
[#17698](https://github.com/google-gemini/gemini-cli/pull/17698)
- feat: increase ask_user label limit to 16 characters by jackwotherspoon in
[#18320](https://github.com/google-gemini/gemini-cli/pull/18320)
- Add information about the agent skills lifecycle and clarify docs-writer skill
metadata. by g-samroberts in
[#18234](https://github.com/google-gemini/gemini-cli/pull/18234)
- feat(core): add enter_plan_mode tool by jerop in
[#18324](https://github.com/google-gemini/gemini-cli/pull/18324)
- Stop showing an error message in /plan by Adib234 in
[#18333](https://github.com/google-gemini/gemini-cli/pull/18333)
- fix(hooks): remove unnecessary logging for hook registration by abhipatel12 in
[#18332](https://github.com/google-gemini/gemini-cli/pull/18332)
- fix(mcp): ensure MCP transport is closed to prevent memory leaks by cbcoutinho
in [#18054](https://github.com/google-gemini/gemini-cli/pull/18054)
- feat(skills): implement linking for agent skills by MushuEE in
[#18295](https://github.com/google-gemini/gemini-cli/pull/18295)
- Changelogs for 0.27.0 and 0.28.0-preview0 by g-samroberts in
[#18336](https://github.com/google-gemini/gemini-cli/pull/18336)
- chore: correct docs as skills and hooks are stable by jackwotherspoon in
[#18358](https://github.com/google-gemini/gemini-cli/pull/18358)
- feat(admin): Implement admin allowlist for MCP server configurations by
skeshive in [#18311](https://github.com/google-gemini/gemini-cli/pull/18311)
- fix(core): add retry logic for transient SSL/TLS errors
([#17318](https://github.com/google-gemini/gemini-cli/pull/17318)) by
ppgranger in [#18310](https://github.com/google-gemini/gemini-cli/pull/18310)
- Add support for /extensions config command by chrstnb in
[#17895](https://github.com/google-gemini/gemini-cli/pull/17895)
- fix(core): handle non-compliant mcpbridge responses from Xcode 26.3 by
peterfriese in
[#18376](https://github.com/google-gemini/gemini-cli/pull/18376)
- feat(cli): Add W, B, E Vim motions and operator support by ademuri in
[#16209](https://github.com/google-gemini/gemini-cli/pull/16209)
- fix: Windows Specific Agent Quality & System Prompt by scidomino in
[#18351](https://github.com/google-gemini/gemini-cli/pull/18351)
- feat(plan): support replace tool in plan mode to edit plans by jerop in
[#18379](https://github.com/google-gemini/gemini-cli/pull/18379)
- Improving memory tool instructions and eval testing by alisa-alisa in
[#18091](https://github.com/google-gemini/gemini-cli/pull/18091)
- fix(cli): color extension link success message green by MushuEE in
[#18386](https://github.com/google-gemini/gemini-cli/pull/18386)
- undo by jacob314 in
[#18147](https://github.com/google-gemini/gemini-cli/pull/18147)
- feat(plan): add guidance on iterating on approved plans vs creating new plans
by jerop in [#18346](https://github.com/google-gemini/gemini-cli/pull/18346)
- feat(plan): fix invalid tool calls in plan mode by Adib234 in
[#18352](https://github.com/google-gemini/gemini-cli/pull/18352)
- feat(plan): integrate planning artifacts and tools into primary workflows by
jerop in [#18375](https://github.com/google-gemini/gemini-cli/pull/18375)
- Fix permission check by scidomino in
[#18395](https://github.com/google-gemini/gemini-cli/pull/18395)
- ux(polish) autocomplete in the input prompt by jacob314 in
[#18181](https://github.com/google-gemini/gemini-cli/pull/18181)
- fix: resolve infinite loop when using 'Modify with external editor' by
ppgranger in [#17453](https://github.com/google-gemini/gemini-cli/pull/17453)
- feat: expand verify-release to macOS and Windows by yunaseoul in
[#18145](https://github.com/google-gemini/gemini-cli/pull/18145)
- feat(plan): implement support for MCP servers in Plan mode by Adib234 in
[#18229](https://github.com/google-gemini/gemini-cli/pull/18229)
- chore: update folder trust error messaging by galz10 in
[#18402](https://github.com/google-gemini/gemini-cli/pull/18402)
- feat(plan): create a metric for execution of plans generated in plan mode by
Adib234 in [#18236](https://github.com/google-gemini/gemini-cli/pull/18236)
- perf(ui): optimize stripUnsafeCharacters with regex by gsquared94 in
[#18413](https://github.com/google-gemini/gemini-cli/pull/18413)
- feat(context): implement observation masking for tool outputs by abhipatel12
in [#18389](https://github.com/google-gemini/gemini-cli/pull/18389)
- feat(core,cli): implement session-linked tool output storage and cleanup by
abhipatel12 in
[#18416](https://github.com/google-gemini/gemini-cli/pull/18416)
- Shorten temp directory by joshualitt in
[#17901](https://github.com/google-gemini/gemini-cli/pull/17901)
- feat(plan): add behavioral evals for plan mode by jerop in
[#18437](https://github.com/google-gemini/gemini-cli/pull/18437)
- Add extension registry client by chrstnb in
[#18396](https://github.com/google-gemini/gemini-cli/pull/18396)
- Enable extension config by default by chrstnb in
[#18447](https://github.com/google-gemini/gemini-cli/pull/18447)
- Automatically generate change logs on release by g-samroberts in
[#18401](https://github.com/google-gemini/gemini-cli/pull/18401)
- Remove previewFeatures and default to Gemini 3 by sehoon38 in
[#18414](https://github.com/google-gemini/gemini-cli/pull/18414)
- feat(admin): apply MCP allowlist to extensions & gemini mcp list command by
skeshive in [#18442](https://github.com/google-gemini/gemini-cli/pull/18442)
- fix(cli): improve focus navigation for interactive and background shells by
galz10 in [#18343](https://github.com/google-gemini/gemini-cli/pull/18343)
- Add shortcuts hint and panel for discoverability by LyalinDotCom in
[#18035](https://github.com/google-gemini/gemini-cli/pull/18035)
- fix(config): treat system settings as read-only during migration and warn user
by spencer426 in
[#18277](https://github.com/google-gemini/gemini-cli/pull/18277)
- feat(plan): add positive test case and update eval stability policy by jerop
in [#18457](https://github.com/google-gemini/gemini-cli/pull/18457)
- fix- windows: add shell: true for spawnSync to fix EINVAL with .cmd editors by
zackoch in [#18408](https://github.com/google-gemini/gemini-cli/pull/18408)
- bug(core): Fix bug when saving plans. by joshualitt in
[#18465](https://github.com/google-gemini/gemini-cli/pull/18465)
- Refactor atCommandProcessor by scidomino in
[#18461](https://github.com/google-gemini/gemini-cli/pull/18461)
- feat(core): implement persistence and resumption for masked tool outputs by
abhipatel12 in
[#18451](https://github.com/google-gemini/gemini-cli/pull/18451)
- refactor: simplify tool output truncation to single config by SandyTao520 in
[#18446](https://github.com/google-gemini/gemini-cli/pull/18446)
- bug(core): Ensure storage is initialized early, even if config is not. by
joshualitt in [#18471](https://github.com/google-gemini/gemini-cli/pull/18471)
- chore: Update build-and-start script to support argument forwarding by
Abhijit-2592 in
[#18241](https://github.com/google-gemini/gemini-cli/pull/18241)
- fix(core): prevent subagent bypass in plan mode by jerop in
[#18484](https://github.com/google-gemini/gemini-cli/pull/18484)
- feat(cli): add WebSocket-based network logging and streaming chunk support by
SandyTao520 in
[#18383](https://github.com/google-gemini/gemini-cli/pull/18383)
- feat(cli): update approval modes UI by jerop in
[#18476](https://github.com/google-gemini/gemini-cli/pull/18476)
- fix(cli): reload skills and agents on extension restart by NTaylorMullen in
[#18411](https://github.com/google-gemini/gemini-cli/pull/18411)
- fix(core): expand excludeTools with legacy aliases for renamed tools by
SandyTao520 in
[#18498](https://github.com/google-gemini/gemini-cli/pull/18498)
- feat(core): overhaul system prompt for rigor, integrity, and intent alignment
by NTaylorMullen in
[#17263](https://github.com/google-gemini/gemini-cli/pull/17263)
- Patch for generate changelog docs yaml file by g-samroberts in
[#18496](https://github.com/google-gemini/gemini-cli/pull/18496)
- Code review fixes for show question mark pr. by jacob314 in
[#18480](https://github.com/google-gemini/gemini-cli/pull/18480)
- fix(cli): add SS3 Shift+Tab support for Windows terminals by ThanhNguyxn in
[#18187](https://github.com/google-gemini/gemini-cli/pull/18187)
- chore: remove redundant planning prompt from final shell by jerop in
[#18528](https://github.com/google-gemini/gemini-cli/pull/18528)
- docs: require pr-creator skill for PR generation by NTaylorMullen in
[#18536](https://github.com/google-gemini/gemini-cli/pull/18536)
- chore: update colors for ask_user dialog by jackwotherspoon in
[#18543](https://github.com/google-gemini/gemini-cli/pull/18543)
- feat(core): exempt high-signal tools from output masking by abhipatel12 in
[#18545](https://github.com/google-gemini/gemini-cli/pull/18545)
- refactor(core): remove memory tool instructions from Gemini 3 prompt by
NTaylorMullen in
[#18559](https://github.com/google-gemini/gemini-cli/pull/18559)
- chore: remove feedback instruction from system prompt by NTaylorMullen in
[#18560](https://github.com/google-gemini/gemini-cli/pull/18560)
- feat(context): add remote configuration for tool output masking thresholds by
abhipatel12 in
[#18553](https://github.com/google-gemini/gemini-cli/pull/18553)
- feat(core): pause agent timeout budget while waiting for tool confirmation by
abhipatel12 in
[#18415](https://github.com/google-gemini/gemini-cli/pull/18415)
- refactor(config): remove experimental.enableEventDrivenScheduler setting by
abhipatel12 in
[#17924](https://github.com/google-gemini/gemini-cli/pull/17924)
- feat(cli): truncate shell output in UI history and improve active shell
display by jwhelangoog in
[#17438](https://github.com/google-gemini/gemini-cli/pull/17438)
- refactor(cli): switch useToolScheduler to event-driven engine by abhipatel12
in [#18565](https://github.com/google-gemini/gemini-cli/pull/18565)
- fix(core): correct escaped interpolation in system prompt by NTaylorMullen in
[#18557](https://github.com/google-gemini/gemini-cli/pull/18557)
- propagate abortSignal by scidomino in
[#18477](https://github.com/google-gemini/gemini-cli/pull/18477)
- feat(core): conditionally include ctrl+f prompt based on interactive shell
setting by NTaylorMullen in
[#18561](https://github.com/google-gemini/gemini-cli/pull/18561)
- fix(core): ensure enter_plan_mode tool registration respects experimental.plan
by jerop in [#18587](https://github.com/google-gemini/gemini-cli/pull/18587)
- feat(core): transition sub-agents to XML format and improve definitions by
NTaylorMullen in
[#18555](https://github.com/google-gemini/gemini-cli/pull/18555)
- docs: Add Plan Mode documentation by jerop in
[#18582](https://github.com/google-gemini/gemini-cli/pull/18582)
- chore: strengthen validation guidance in system prompt by NTaylorMullen in
[#18544](https://github.com/google-gemini/gemini-cli/pull/18544)
- Fix newline insertion bug in replace tool by werdnum in
[#18595](https://github.com/google-gemini/gemini-cli/pull/18595)
- fix(evals): update save_memory evals and simplify tool description by
NTaylorMullen in
[#18610](https://github.com/google-gemini/gemini-cli/pull/18610)
- chore(evals): update validation_fidelity_pre_existing_errors to USUALLY_PASSES
by NTaylorMullen in
[#18617](https://github.com/google-gemini/gemini-cli/pull/18617)
- fix: shorten tool call IDs and fix duplicate tool name in truncated output
filenames by SandyTao520 in
[#18600](https://github.com/google-gemini/gemini-cli/pull/18600)
- feat(cli): implement atomic writes and safety checks for trusted folders by
galz10 in [#18406](https://github.com/google-gemini/gemini-cli/pull/18406)
- Remove relative docs links by chrstnb in
[#18650](https://github.com/google-gemini/gemini-cli/pull/18650)
- docs: add legacy snippets convention to GEMINI.md by NTaylorMullen in
[#18597](https://github.com/google-gemini/gemini-cli/pull/18597)
- fix(chore): Support linting for cjs by aswinashok44 in
[#18639](https://github.com/google-gemini/gemini-cli/pull/18639)
- feat: move shell efficiency guidelines to tool description by NTaylorMullen in
[#18614](https://github.com/google-gemini/gemini-cli/pull/18614)
- Added "" as default value, since getText() used to expect a string only and
thus crashed when undefined... Fixes #18076 by 019-Abhi in
[#18099](https://github.com/google-gemini/gemini-cli/pull/18099)
- Allow @-includes outside of workspaces (with permission) by scidomino in
[#18470](https://github.com/google-gemini/gemini-cli/pull/18470)
- chore: make ask_user header description more clear by jackwotherspoon in
[#18657](https://github.com/google-gemini/gemini-cli/pull/18657)
- refactor(core): model-dependent tool definitions by aishaneeshah in
[#18563](https://github.com/google-gemini/gemini-cli/pull/18563)
- Harded code assist converter. by jacob314 in
[#18656](https://github.com/google-gemini/gemini-cli/pull/18656)
- bug(core): Fix minor bug in migration logic. by joshualitt in
[#18661](https://github.com/google-gemini/gemini-cli/pull/18661)
- feat: enable plan mode experiment in settings by jerop in
[#18636](https://github.com/google-gemini/gemini-cli/pull/18636)
- refactor: push isValidPath() into parsePastedPaths() by scidomino in
[#18664](https://github.com/google-gemini/gemini-cli/pull/18664)
- fix(cli): correct 'esc to cancel' position and restore duration display by
NTaylorMullen in
[#18534](https://github.com/google-gemini/gemini-cli/pull/18534)
- feat(cli): add DevTools integration with gemini-cli-devtools by SandyTao520 in
[#18648](https://github.com/google-gemini/gemini-cli/pull/18648)
- chore: remove unused exports and redundant hook files by SandyTao520 in
[#18681](https://github.com/google-gemini/gemini-cli/pull/18681)
- Fix number of lines being reported in rewind confirmation dialog by Adib234 in
[#18675](https://github.com/google-gemini/gemini-cli/pull/18675)
- feat(cli): disable folder trust in headless mode by galz10 in
[#18407](https://github.com/google-gemini/gemini-cli/pull/18407)
- Disallow unsafe type assertions by gundermanc in
[#18688](https://github.com/google-gemini/gemini-cli/pull/18688)
- Change event type for release by g-samroberts in
[#18693](https://github.com/google-gemini/gemini-cli/pull/18693)
- feat: handle multiple dynamic context filenames in system prompt by
NTaylorMullen in
[#18598](https://github.com/google-gemini/gemini-cli/pull/18598)
- Properly parse at-commands with narrow non-breaking spaces by scidomino in
[#18677](https://github.com/google-gemini/gemini-cli/pull/18677)
- refactor(core): centralize core tool definitions and support model-specific
schemas by aishaneeshah in
[#18662](https://github.com/google-gemini/gemini-cli/pull/18662)
- feat(core): Render memory hierarchically in context. by joshualitt in
[#18350](https://github.com/google-gemini/gemini-cli/pull/18350)
- feat: Ctrl+O to expand paste placeholder by jackwotherspoon in
[#18103](https://github.com/google-gemini/gemini-cli/pull/18103)
- fix(cli): Improve header spacing by NTaylorMullen in
[#18531](https://github.com/google-gemini/gemini-cli/pull/18531)
- Feature/quota visibility 16795 by spencer426 in
[#18203](https://github.com/google-gemini/gemini-cli/pull/18203)
- Inline thinking bubbles with summary/full modes by LyalinDotCom in
[#18033](https://github.com/google-gemini/gemini-cli/pull/18033)
- docs: remove TOC marker from Plan Mode header by jerop in
[#18678](https://github.com/google-gemini/gemini-cli/pull/18678)
- fix(ui): remove redundant newlines in Gemini messages by NTaylorMullen in
[#18538](https://github.com/google-gemini/gemini-cli/pull/18538)
- test(cli): fix AppContainer act() warnings and improve waitFor resilience by
NTaylorMullen in
[#18676](https://github.com/google-gemini/gemini-cli/pull/18676)
- refactor(core): refine Security & System Integrity section in system prompt by
NTaylorMullen in
[#18601](https://github.com/google-gemini/gemini-cli/pull/18601)
- Fix layout rounding. by gundermanc in
[#18667](https://github.com/google-gemini/gemini-cli/pull/18667)
- docs(skills): enhance pr-creator safety and interactivity by NTaylorMullen in
[#18616](https://github.com/google-gemini/gemini-cli/pull/18616)
- test(core): remove hardcoded model from TestRig by NTaylorMullen in
[#18710](https://github.com/google-gemini/gemini-cli/pull/18710)
- feat(core): optimize sub-agents system prompt intro by NTaylorMullen in
[#18608](https://github.com/google-gemini/gemini-cli/pull/18608)
- feat(cli): update approval mode labels and shortcuts per latest UX spec by
jerop in [#18698](https://github.com/google-gemini/gemini-cli/pull/18698)
- fix(plan): update persistent approval mode setting by Adib234 in
[#18638](https://github.com/google-gemini/gemini-cli/pull/18638)
- fix: move toasts location to left side by jackwotherspoon in
[#18705](https://github.com/google-gemini/gemini-cli/pull/18705)
- feat(routing): restrict numerical routing to Gemini 3 family by mattKorwel in
[#18478](https://github.com/google-gemini/gemini-cli/pull/18478)
- fix(ide): fix ide nudge setting by skeshive in
[#18733](https://github.com/google-gemini/gemini-cli/pull/18733)
- fix(core): standardize tool formatting in system prompts by NTaylorMullen in
[#18615](https://github.com/google-gemini/gemini-cli/pull/18615)
- chore: consolidate to green in ask user dialog by jackwotherspoon in
[#18734](https://github.com/google-gemini/gemini-cli/pull/18734)
- feat: add extensionsExplore setting to enable extensions explore UI. by
sripasg in [#18686](https://github.com/google-gemini/gemini-cli/pull/18686)
- feat(cli): defer devtools startup and integrate with F12 by SandyTao520 in
[#18695](https://github.com/google-gemini/gemini-cli/pull/18695)
- ui: update & subdue footer colors and animate progress indicator by
keithguerin in
[#18570](https://github.com/google-gemini/gemini-cli/pull/18570)
- test: add model-specific snapshots for coreTools by aishaneeshah in
[#18707](https://github.com/google-gemini/gemini-cli/pull/18707)
- ci: shard windows tests and fix event listener leaks by NTaylorMullen in
[#18670](https://github.com/google-gemini/gemini-cli/pull/18670)
- fix: allow ask_user tool in yolo mode by jackwotherspoon in
[#18541](https://github.com/google-gemini/gemini-cli/pull/18541)
- feat: redact disabled tools from system prompt
([#13597](https://github.com/google-gemini/gemini-cli/pull/13597)) by
NTaylorMullen in
[#18613](https://github.com/google-gemini/gemini-cli/pull/18613)
- Update Gemini.md to use the curent year on creating new files by sehoon38 in
[#18460](https://github.com/google-gemini/gemini-cli/pull/18460)
- Code review cleanup for thinking display by jacob314 in
[#18720](https://github.com/google-gemini/gemini-cli/pull/18720)
- fix(cli): hide scrollbars when in alternate buffer copy mode by werdnum in
[#18354](https://github.com/google-gemini/gemini-cli/pull/18354)
- Fix issues with rip grep by gundermanc in
[#18756](https://github.com/google-gemini/gemini-cli/pull/18756)
- fix(cli): fix history navigation regression after prompt autocomplete by
sehoon38 in [#18752](https://github.com/google-gemini/gemini-cli/pull/18752)
- chore: cleanup unused and add unlisted dependencies in packages/cli by
adamfweidman in
[#18749](https://github.com/google-gemini/gemini-cli/pull/18749)
- Fix issue where Gemini CLI creates tests in a new file by gundermanc in
[#18409](https://github.com/google-gemini/gemini-cli/pull/18409)
- feat(telemetry): Ensure experiment IDs are included in OpenTelemetry logs by
kevin-ramdass in
[#18747](https://github.com/google-gemini/gemini-cli/pull/18747)
**Full changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.29.0-preview.5...v0.30.0-preview.3
https://github.com/google-gemini/gemini-cli/compare/v0.28.0-preview.0...v0.29.0-preview.0
+74 -129
View File
@@ -1,61 +1,50 @@
# Plan Mode (experimental)
Plan Mode is a read-only environment for architecting robust solutions before
implementation. It allows you to:
Plan Mode is a safe, read-only mode for researching and designing complex
changes. It prevents modifications while you research, design and plan an
implementation strategy.
- **Research:** Explore the project in a read-only state to prevent accidental
changes.
- **Design:** Understand problems, evaluate trade-offs, and choose a solution.
- **Plan:** Align on an execution strategy before any code is modified.
> **Note:** This is a preview feature currently under active development. Your
> feedback is invaluable as we refine this feature. If you have ideas,
> **Note: Plan Mode is currently an experimental feature.**
>
> Experimental features are subject to change. To use Plan Mode, enable it via
> `/settings` (search for `Plan`) or add the following to your `settings.json`:
>
> ```json
> {
> "experimental": {
> "plan": true
> }
> }
> ```
>
> Your feedback is invaluable as we refine this feature. If you have ideas,
> suggestions, or encounter issues:
>
> - Use the `/bug` command within the CLI to file an issue.
> - [Open an issue](https://github.com/google-gemini/gemini-cli/issues) on
> GitHub.
> - Use the **/bug** command within Gemini CLI to file an issue.
- [Enabling Plan Mode](#enabling-plan-mode)
- [Starting in Plan Mode](#starting-in-plan-mode)
- [How to use Plan Mode](#how-to-use-plan-mode)
- [Entering Plan Mode](#entering-plan-mode)
- [Planning Workflow](#planning-workflow)
- [The Planning Workflow](#the-planning-workflow)
- [Exiting Plan Mode](#exiting-plan-mode)
- [Tool Restrictions](#tool-restrictions)
- [Customizing Planning with Skills](#customizing-planning-with-skills)
- [Customizing Policies](#customizing-policies)
- [Example: Allow git commands in Plan Mode](#example-allow-git-commands-in-plan-mode)
- [Example: Enable research subagents in Plan Mode](#example-enable-research-subagents-in-plan-mode)
- [Custom Plan Directory and Policies](#custom-plan-directory-and-policies)
## Enabling Plan Mode
## Starting in Plan Mode
To use Plan Mode, enable it via **/settings** (search for **Plan**) or add the
following to your `settings.json`:
You can configure Gemini CLI to start directly in Plan Mode by default:
```json
{
"experimental": {
"plan": true
}
}
```
1. Type `/settings` in the CLI.
2. Search for `Default Approval Mode`.
3. Set the value to `Plan`.
## How to use Plan Mode
Other ways to start in Plan Mode:
### Entering Plan Mode
You can configure Gemini CLI to start in Plan Mode by default or enter it
manually during a session.
- **Configuration:** Configure Gemini CLI to start directly in Plan Mode by
default:
1. Type `/settings` in the CLI.
2. Search for **Default Approval Mode**.
3. Set the value to **Plan**.
Alternatively, use the `gemini --approval-mode=plan` CLI flag or manually
update:
- **CLI Flag:** `gemini --approval-mode=plan`
- **Manual Settings:** Manually update your `settings.json`:
```json
{
@@ -65,44 +54,42 @@ manually during a session.
}
```
- **Keyboard Shortcut:** Press `Shift+Tab` to cycle through approval modes
(`Default` -> `Auto-Edit` -> `Plan`).
## How to use Plan Mode
> **Note:** Plan Mode is automatically removed from the rotation when Gemini
> CLI is actively processing or showing confirmation dialogs.
### Entering Plan Mode
- **Command:** Type `/plan` in the input box.
You can enter Plan Mode in three ways:
- **Natural Language:** Ask Gemini CLI to "start a plan for...". Gemini CLI then
calls the [`enter_plan_mode`] tool to switch modes.
> **Note:** This tool is not available when Gemini CLI is in [YOLO mode].
1. **Keyboard Shortcut:** Press `Shift+Tab` to cycle through approval modes
(`Default` -> `Auto-Edit` -> `Plan`).
### Planning Workflow
> **Note:** Plan Mode is automatically removed from the rotation when the
> agent is actively processing or showing confirmation dialogs.
1. **Explore & Analyze:** Analyze requirements and use read-only tools to map
the codebase and validate assumptions. For complex tasks, identify at least
two viable implementation approaches.
2. **Consult:** Present a summary of the identified approaches via [`ask_user`]
to obtain a selection. For simple or canonical tasks, this step may be
skipped.
3. **Draft:** Once an approach is selected, write a detailed implementation
plan to the plans directory.
4. **Review & Approval:** Use the [`exit_plan_mode`] tool to present the plan
and formally request approval.
- **Approve:** Exit Plan Mode and start implementation.
2. **Command:** Type `/plan` in the input box.
3. **Natural Language:** Ask the agent to "start a plan for...". The agent will
then call the [`enter_plan_mode`] tool to switch modes.
### The Planning Workflow
1. **Requirements:** The agent clarifies goals using [`ask_user`].
2. **Exploration:** The agent uses read-only tools (like [`read_file`]) to map
the codebase and validate assumptions.
3. **Design:** The agent proposes alternative approaches with a recommended
solution for you to choose from.
4. **Planning:** A detailed plan is written to a temporary Markdown file.
5. **Review:** You review the plan.
- **Approve:** Exit Plan Mode and start implementation (switching to
Auto-Edit or Default approval mode).
- **Iterate:** Provide feedback to refine the plan.
For more complex or specialized planning tasks, you can
[customize the planning workflow with skills](#customizing-planning-with-skills).
### Exiting Plan Mode
To exit Plan Mode, you can:
To exit Plan Mode:
- **Keyboard Shortcut:** Press `Shift+Tab` to cycle to the desired mode.
- **Tool:** Gemini CLI calls the [`exit_plan_mode`] tool to present the
finalized plan for your approval.
1. **Keyboard Shortcut:** Press `Shift+Tab` to cycle to the desired mode.
2. **Tool:** The agent calls the [`exit_plan_mode`] tool to present the
finalized plan for your approval.
## Tool Restrictions
@@ -115,31 +102,30 @@ These are the only allowed tools:
- **Interaction:** [`ask_user`]
- **MCP Tools (Read):** Read-only [MCP tools] (e.g., `github_read_issue`,
`postgres_read_schema`) are allowed.
- **Planning (Write):** [`write_file`] and [`replace`] only allowed for `.md`
files in the `~/.gemini/tmp/<project>/<session-id>/plans/` directory or your
[custom plans directory](#custom-plan-directory-and-policies).
- **Planning (Write):** [`write_file`] and [`replace`] ONLY allowed for `.md`
files in the `~/.gemini/tmp/<project>/<session-id>/plans/` directory.
- **Skills:** [`activate_skill`] (allows loading specialized instructions and
resources in a read-only manner)
### Customizing Planning with Skills
You can use [Agent Skills](./skills.md) to customize how Gemini CLI approaches
planning for specific types of tasks. When a skill is activated during Plan
Mode, its specialized instructions and procedural workflows will guide the
research, design and planning phases.
You can leverage [Agent Skills](./skills.md) to customize how Gemini CLI
approaches planning for specific types of tasks. When a skill is activated
during Plan Mode, its specialized instructions and procedural workflows will
guide the research and design phases.
For example:
- A **"Database Migration"** skill could ensure the plan includes data safety
checks and rollback strategies.
- A **"Security Audit"** skill could prompt Gemini CLI to look for specific
- A **"Security Audit"** skill could prompt the agent to look for specific
vulnerabilities during codebase exploration.
- A **"Frontend Design"** skill could guide Gemini CLI to use specific UI
- A **"Frontend Design"** skill could guide the agent to use specific UI
components and accessibility standards in its proposal.
To use a skill in Plan Mode, you can explicitly ask Gemini CLI to "use the
`<skill-name>` skill to plan..." or Gemini CLI may autonomously activate it
based on the task description.
To use a skill in Plan Mode, you can explicitly ask the agent to "use the
[skill-name] skill to plan..." or the agent may autonomously activate it based
on the task description.
### Customizing Policies
@@ -151,7 +137,7 @@ Because user policies (Tier 2) have a higher base priority than built-in
policies (Tier 1), you can override Plan Mode's default restrictions by creating
a rule in your `~/.gemini/policies/` directory.
#### Example: Allow git commands in Plan Mode
#### Example: Allow `git status` and `git diff` in Plan Mode
This rule allows you to check the repository status and see changes while in
Plan Mode.
@@ -167,10 +153,10 @@ priority = 100
modes = ["plan"]
```
#### Example: Enable research subagents in Plan Mode
#### Example: Enable research sub-agents in Plan Mode
You can enable experimental research [subagents] like `codebase_investigator` to
help gather architecture details during the planning phase.
You can enable [experimental research sub-agents] like `codebase_investigator`
to help gather architecture details during the planning phase.
`~/.gemini/policies/research-subagents.toml`
@@ -182,51 +168,11 @@ priority = 100
modes = ["plan"]
```
Tell Gemini CLI it can use these tools in your prompt, for example: _"You can
Tell the agent it can use these tools in your prompt, for example: _"You can
check ongoing changes in git."_
For more information on how the policy engine works, see the [policy engine]
docs.
### Custom Plan Directory and Policies
By default, planning artifacts are stored in a managed temporary directory
outside your project: `~/.gemini/tmp/<project>/<session-id>/plans/`.
You can configure a custom directory for plans in your `settings.json`. For
example, to store plans in a `.gemini/plans` directory within your project:
```json
{
"general": {
"plan": {
"directory": ".gemini/plans"
}
}
}
```
To maintain the safety of Plan Mode, user-configured paths for the plans
directory are restricted to the project root. This ensures that custom planning
locations defined within a project's workspace cannot be used to escape and
overwrite sensitive files elsewhere. Any user-configured directory must reside
within the project boundary.
Using a custom directory requires updating your [policy engine] configurations
to allow `write_file` and `replace` in that specific location. For example, to
allow writing to the `.gemini/plans` directory within your project, create a
policy file at `~/.gemini/policies/plan-custom-directory.toml`:
```toml
[[rule]]
toolName = ["write_file", "replace"]
decision = "allow"
priority = 100
modes = ["plan"]
# Adjust the pattern to match your custom directory.
# This example matches any .md file in a .gemini/plans directory within the project.
argsPattern = "\"file_path\":\"[^\"]+[\\\\/]+\\.gemini[\\\\/]+plans[\\\\/]+[\\w-]+\\.md\""
```
For more information on how the policy engine works, see the [Policy Engine
Guide].
[`list_directory`]: /docs/tools/file-system.md#1-list_directory-readfolder
[`read_file`]: /docs/tools/file-system.md#2-read_file-readfile
@@ -237,9 +183,8 @@ argsPattern = "\"file_path\":\"[^\"]+[\\\\/]+\\.gemini[\\\\/]+plans[\\\\/]+[\\w-
[`replace`]: /docs/tools/file-system.md#6-replace-edit
[MCP tools]: /docs/tools/mcp-server.md
[`activate_skill`]: /docs/cli/skills.md
[subagents]: /docs/core/subagents.md
[policy engine]: /docs/reference/policy-engine.md
[experimental research sub-agents]: /docs/core/subagents.md
[Policy Engine Guide]: /docs/reference/policy-engine.md
[`enter_plan_mode`]: /docs/tools/planning.md#1-enter_plan_mode-enterplanmode
[`exit_plan_mode`]: /docs/tools/planning.md#2-exit_plan_mode-exitplanmode
[`ask_user`]: /docs/tools/ask-user.md
[YOLO mode]: /docs/reference/configuration.md#command-line-arguments
+43 -47
View File
@@ -22,16 +22,15 @@ they appear in the UI.
### General
| UI Label | Setting | Description | Default |
| ----------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- |
| Vim Mode | `general.vimMode` | Enable Vim keybindings | `false` |
| Default Approval Mode | `general.defaultApprovalMode` | The default approval mode for tool execution. 'default' prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is read-only mode. 'yolo' is not supported yet. | `"default"` |
| Enable Auto Update | `general.enableAutoUpdate` | Enable automatic updates. | `true` |
| Enable Notifications | `general.enableNotifications` | Enable run-event notifications for action-required prompts and session completion. Currently macOS only. | `false` |
| Plan Directory | `general.plan.directory` | The directory where planning artifacts are stored. If not specified, defaults to the system temporary directory. | `undefined` |
| Debug Keystroke Logging | `general.debugKeystrokeLogging` | Enable debug logging of keystrokes to the console. | `false` |
| Enable Session Cleanup | `general.sessionRetention.enabled` | Enable automatic session cleanup | `false` |
| Keep chat history | `general.sessionRetention.maxAge` | Automatically delete chats older than this time period (e.g., "30d", "7d", "24h", "1w") | `undefined` |
| UI Label | Setting | Description | Default |
| ------------------------ | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- |
| Vim Mode | `general.vimMode` | Enable Vim keybindings | `false` |
| Default Approval Mode | `general.defaultApprovalMode` | The default approval mode for tool execution. 'default' prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is read-only mode. 'yolo' is not supported yet. | `"default"` |
| Enable Auto Update | `general.enableAutoUpdate` | Enable automatic updates. | `true` |
| Enable Prompt Completion | `general.enablePromptCompletion` | Enable AI-powered prompt completion suggestions while typing. | `false` |
| Debug Keystroke Logging | `general.debugKeystrokeLogging` | Enable debug logging of keystrokes to the console. | `false` |
| Enable Session Cleanup | `general.sessionRetention.enabled` | Enable automatic session cleanup | `false` |
| Keep chat history | `general.sessionRetention.maxAge` | Automatically delete chats older than this time period (e.g., "30d", "7d", "24h", "1w") | `undefined` |
### Output
@@ -41,36 +40,35 @@ they appear in the UI.
### UI
| UI Label | Setting | Description | Default |
| ------------------------------------ | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| Auto Theme Switching | `ui.autoThemeSwitching` | Automatically switch between default light and dark themes based on terminal background color. | `true` |
| Terminal Background Polling Interval | `ui.terminalBackgroundPollingInterval` | Interval in seconds to poll the terminal background color. | `60` |
| Hide Window Title | `ui.hideWindowTitle` | Hide the window title bar | `false` |
| Inline Thinking | `ui.inlineThinkingMode` | Display model thinking inline: off or full. | `"off"` |
| Show Thoughts in Title | `ui.showStatusInTitle` | Show Gemini CLI model thoughts in the terminal window title during the working phase | `false` |
| Dynamic Window Title | `ui.dynamicWindowTitle` | Update the terminal window title with current status icons (Ready: ◇, Action Required: ✋, Working: ✦) | `true` |
| Show Home Directory Warning | `ui.showHomeDirectoryWarning` | Show a warning when running Gemini CLI in the home directory. | `true` |
| Show Compatibility Warnings | `ui.showCompatibilityWarnings` | Show warnings about terminal or OS compatibility issues. | `true` |
| Hide Tips | `ui.hideTips` | Hide helpful tips in the UI | `false` |
| Show Shortcuts Hint | `ui.showShortcutsHint` | Show the "? for shortcuts" hint above the input. | `true` |
| Hide Banner | `ui.hideBanner` | Hide the application banner | `false` |
| Hide Context Summary | `ui.hideContextSummary` | Hide the context summary (GEMINI.md, MCP servers) above the input. | `false` |
| Hide CWD | `ui.footer.hideCWD` | Hide the current working directory path in the footer. | `false` |
| Hide Sandbox Status | `ui.footer.hideSandboxStatus` | Hide the sandbox status indicator in the footer. | `false` |
| Hide Model Info | `ui.footer.hideModelInfo` | Hide the model name and context usage in the footer. | `false` |
| Hide Context Window Percentage | `ui.footer.hideContextPercentage` | Hides the context window remaining percentage. | `true` |
| Hide Footer | `ui.hideFooter` | Hide the footer from the UI | `false` |
| Show Memory Usage | `ui.showMemoryUsage` | Display memory usage information in the UI | `false` |
| Show Line Numbers | `ui.showLineNumbers` | Show line numbers in the chat. | `true` |
| Show Citations | `ui.showCitations` | Show citations for generated text in the chat. | `false` |
| Show Model Info In Chat | `ui.showModelInfoInChat` | Show the model name in the chat for each model turn. | `false` |
| Show User Identity | `ui.showUserIdentity` | Show the logged-in user's identity (e.g. email) in the UI. | `true` |
| Use Alternate Screen Buffer | `ui.useAlternateBuffer` | Use an alternate screen buffer for the UI, preserving shell history. | `false` |
| Use Background Color | `ui.useBackgroundColor` | Whether to use background colors in the UI. | `true` |
| Incremental Rendering | `ui.incrementalRendering` | Enable incremental rendering for the UI. This option will reduce flickering but may cause rendering artifacts. Only supported when useAlternateBuffer is enabled. | `true` |
| Show Spinner | `ui.showSpinner` | Show the spinner during operations. | `true` |
| Loading Phrases | `ui.loadingPhrases` | What to show while the model is working: tips, witty comments, both, or nothing. | `"tips"` |
| Screen Reader Mode | `ui.accessibility.screenReader` | Render output in plain-text to be more screen reader accessible | `false` |
| UI Label | Setting | Description | Default |
| ------------------------------------ | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| Auto Theme Switching | `ui.autoThemeSwitching` | Automatically switch between default light and dark themes based on terminal background color. | `true` |
| Terminal Background Polling Interval | `ui.terminalBackgroundPollingInterval` | Interval in seconds to poll the terminal background color. | `60` |
| Hide Window Title | `ui.hideWindowTitle` | Hide the window title bar | `false` |
| Inline Thinking | `ui.inlineThinkingMode` | Display model thinking inline: off or full. | `"off"` |
| Show Thoughts in Title | `ui.showStatusInTitle` | Show Gemini CLI model thoughts in the terminal window title during the working phase | `false` |
| Dynamic Window Title | `ui.dynamicWindowTitle` | Update the terminal window title with current status icons (Ready: ◇, Action Required: ✋, Working: ✦) | `true` |
| Show Home Directory Warning | `ui.showHomeDirectoryWarning` | Show a warning when running Gemini CLI in the home directory. | `true` |
| Hide Tips | `ui.hideTips` | Hide helpful tips in the UI | `false` |
| Show Shortcuts Hint | `ui.showShortcutsHint` | Show the "? for shortcuts" hint above the input. | `true` |
| Hide Banner | `ui.hideBanner` | Hide the application banner | `false` |
| Hide Context Summary | `ui.hideContextSummary` | Hide the context summary (GEMINI.md, MCP servers) above the input. | `false` |
| Hide CWD | `ui.footer.hideCWD` | Hide the current working directory path in the footer. | `false` |
| Hide Sandbox Status | `ui.footer.hideSandboxStatus` | Hide the sandbox status indicator in the footer. | `false` |
| Hide Model Info | `ui.footer.hideModelInfo` | Hide the model name and context usage in the footer. | `false` |
| Hide Context Window Percentage | `ui.footer.hideContextPercentage` | Hides the context window remaining percentage. | `true` |
| Hide Footer | `ui.hideFooter` | Hide the footer from the UI | `false` |
| Show Memory Usage | `ui.showMemoryUsage` | Display memory usage information in the UI | `false` |
| Show Line Numbers | `ui.showLineNumbers` | Show line numbers in the chat. | `true` |
| Show Citations | `ui.showCitations` | Show citations for generated text in the chat. | `false` |
| Show Model Info In Chat | `ui.showModelInfoInChat` | Show the model name in the chat for each model turn. | `false` |
| Show User Identity | `ui.showUserIdentity` | Show the logged-in user's identity (e.g. email) in the UI. | `true` |
| Use Alternate Screen Buffer | `ui.useAlternateBuffer` | Use an alternate screen buffer for the UI, preserving shell history. | `false` |
| Use Background Color | `ui.useBackgroundColor` | Whether to use background colors in the UI. | `true` |
| Incremental Rendering | `ui.incrementalRendering` | Enable incremental rendering for the UI. This option will reduce flickering but may cause rendering artifacts. Only supported when useAlternateBuffer is enabled. | `true` |
| Show Spinner | `ui.showSpinner` | Show the spinner during operations. | `true` |
| Enable Loading Phrases | `ui.accessibility.enableLoadingPhrases` | Enable loading phrases during operations. | `true` |
| Screen Reader Mode | `ui.accessibility.screenReader` | Render output in plain-text to be more screen reader accessible | `false` |
### IDE
@@ -128,13 +126,11 @@ they appear in the UI.
### Experimental
| UI Label | Setting | Description | Default |
| -------------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| Enable Tool Output Masking | `experimental.toolOutputMasking.enabled` | Enables tool output masking to save tokens. | `true` |
| Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 for pasting. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
| Use OSC 52 Copy | `experimental.useOSC52Copy` | Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
| Plan | `experimental.plan` | Enable planning features (Plan Mode and tools). | `false` |
| Model Steering | `experimental.modelSteering` | Enable model steering (user hints) to guide the model during tool execution. | `false` |
| UI Label | Setting | Description | Default |
| -------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------- | ------- |
| Enable Tool Output Masking | `experimental.toolOutputMasking.enabled` | Enables tool output masking to save tokens. | `true` |
| Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 sequence for pasting instead of clipboardy (useful for remote sessions). | `false` |
| Plan | `experimental.plan` | Enable planning features (Plan Mode and tools). | `false` |
### Skills
-31
View File
@@ -38,37 +38,6 @@ folder, a dialog will automatically appear, prompting you to make a choice:
Your choice is saved in a central file (`~/.gemini/trustedFolders.json`), so you
will only be asked once per folder.
## Understanding folder contents: The discovery phase
Before you make a choice, the Gemini CLI performs a **discovery phase** to scan
the folder for potential configurations. This information is displayed in the
trust dialog to help you make an informed decision.
The discovery UI lists the following categories of items found in the project:
- **Commands**: Custom `.toml` command definitions that add new functionality.
- **MCP Servers**: Configured Model Context Protocol servers that the CLI will
attempt to connect to.
- **Hooks**: System or custom hooks that can intercept and modify CLI behavior.
- **Skills**: Local agent skills that provide specialized capabilities.
- **Setting overrides**: Any project-specific configurations that override your
global user settings.
### Security warnings and errors
The trust dialog also highlights critical information that requires your
attention:
- **Security Warnings**: The CLI will explicitly flag potentially dangerous
settings, such as auto-approving certain tools or disabling the security
sandbox.
- **Discovery Errors**: If the CLI encounters issues while scanning the folder
(e.g., a malformed `settings.json` file), these errors will be displayed
prominently.
By reviewing these details, you can ensure that you only grant trust to projects
that you know are safe.
## Why trust matters: The impact of an untrusted workspace
When a folder is **untrusted**, the Gemini CLI runs in a restricted "safe mode"
+38 -38
View File
@@ -1,13 +1,13 @@
# Subagents (experimental)
# Sub-agents (experimental)
Subagents are specialized agents that operate within your main Gemini CLI
Sub-agents are specialized agents that operate within your main Gemini CLI
session. They are designed to handle specific, complex tasks—like deep codebase
analysis, documentation lookup, or domain-specific reasoning—without cluttering
the main agent's context or toolset.
> **Note: Subagents are currently an experimental feature.**
> **Note: Sub-agents are currently an experimental feature.**
>
> To use custom subagents, you must explicitly enable them in your
> To use custom sub-agents, you must explicitly enable them in your
> `settings.json`:
>
> ```json
@@ -16,31 +16,31 @@ the main agent's context or toolset.
> }
> ```
>
> **Warning:** Subagents currently operate in
> **Warning:** Sub-agents currently operate in
> ["YOLO mode"](../reference/configuration.md#command-line-arguments), meaning
> they may execute tools without individual user confirmation for each step.
> Proceed with caution when defining agents with powerful tools like
> `run_shell_command` or `write_file`.
## What are subagents?
## What are sub-agents?
Subagents are "specialists" that the main Gemini agent can hire for a specific
Sub-agents are "specialists" that the main Gemini agent can hire for a specific
job.
- **Focused context:** Each subagent has its own system prompt and persona.
- **Specialized tools:** Subagents can have a restricted or specialized set of
- **Focused context:** Each sub-agent has its own system prompt and persona.
- **Specialized tools:** Sub-agents can have a restricted or specialized set of
tools.
- **Independent context window:** Interactions with a subagent happen in a
- **Independent context window:** Interactions with a sub-agent happen in a
separate context loop, which saves tokens in your main conversation history.
Subagents are exposed to the main agent as a tool of the same name. When the
main agent calls the tool, it delegates the task to the subagent. Once the
subagent completes its task, it reports back to the main agent with its
Sub-agents are exposed to the main agent as a tool of the same name. When the
main agent calls the tool, it delegates the task to the sub-agent. Once the
sub-agent completes its task, it reports back to the main agent with its
findings.
## Built-in subagents
## Built-in sub-agents
Gemini CLI comes with the following built-in subagents:
Gemini CLI comes with the following built-in sub-agents:
### Codebase Investigator
@@ -75,15 +75,15 @@ Gemini CLI comes with the following built-in subagents:
### Generalist Agent
- **Name:** `generalist_agent`
- **Purpose:** Route tasks to the appropriate specialized subagent.
- **Purpose:** Route tasks to the appropriate specialized sub-agent.
- **When to use:** Implicitly used by the main agent for routing. Not directly
invoked by the user.
- **Configuration:** Enabled by default. No specific configuration options.
## Creating custom subagents
## Creating custom sub-agents
You can create your own subagents to automate specific workflows or enforce
specific personas. To use custom subagents, you must enable them in your
You can create your own sub-agents to automate specific workflows or enforce
specific personas. To use custom sub-agents, you must enable them in your
`settings.json`:
```json
@@ -138,20 +138,20 @@ it yourself; just report it.
### Configuration schema
| Field | Type | Required | Description |
| :------------- | :----- | :------- | :------------------------------------------------------------------------------------------------------------------------ |
| `name` | string | Yes | Unique identifier (slug) used as the tool name for the agent. Only lowercase letters, numbers, hyphens, and underscores. |
| `description` | string | Yes | Short description of what the agent does. This is visible to the main agent to help it decide when to call this subagent. |
| `kind` | string | No | `local` (default) or `remote`. |
| `tools` | array | No | List of tool names this agent can use. If omitted, it may have access to a default set. |
| `model` | string | No | Specific model to use (e.g., `gemini-2.5-pro`). Defaults to `inherit` (uses the main session model). |
| `temperature` | number | No | Model temperature (0.0 - 2.0). |
| `max_turns` | number | No | Maximum number of conversation turns allowed for this agent before it must return. Defaults to `15`. |
| `timeout_mins` | number | No | Maximum execution time in minutes. Defaults to `5`. |
| Field | Type | Required | Description |
| :------------- | :----- | :------- | :------------------------------------------------------------------------------------------------------------------------- |
| `name` | string | Yes | Unique identifier (slug) used as the tool name for the agent. Only lowercase letters, numbers, hyphens, and underscores. |
| `description` | string | Yes | Short description of what the agent does. This is visible to the main agent to help it decide when to call this sub-agent. |
| `kind` | string | No | `local` (default) or `remote`. |
| `tools` | array | No | List of tool names this agent can use. If omitted, it may have access to a default set. |
| `model` | string | No | Specific model to use (e.g., `gemini-2.5-pro`). Defaults to `inherit` (uses the main session model). |
| `temperature` | number | No | Model temperature (0.0 - 2.0). |
| `max_turns` | number | No | Maximum number of conversation turns allowed for this agent before it must return. Defaults to `15`. |
| `timeout_mins` | number | No | Maximum execution time in minutes. Defaults to `5`. |
### Optimizing your subagent
### Optimizing your sub-agent
The main agent's system prompt encourages it to use an expert subagent when one
The main agent's system prompt encourages it to use an expert sub-agent when one
is available. It decides whether an agent is a relevant expert based on the
agent's description. You can improve the reliability with which an agent is used
by updating the description to more clearly indicate:
@@ -160,7 +160,7 @@ by updating the description to more clearly indicate:
- When it should be used.
- Some example scenarios.
For example, the following subagent description should be called fairly
For example, the following sub-agent description should be called fairly
consistently for Git operations.
> Git expert agent which should be used for all local and remote git operations.
@@ -170,13 +170,13 @@ consistently for Git operations.
> - Searching for regressions with bisect
> - Interacting with source control and issues providers such as GitHub.
If you need to further tune your subagent, you can do so by selecting the model
If you need to further tune your sub-agent, you can do so by selecting the model
to optimize for with `/model` and then asking the model why it does not think
that your subagent was called with a specific prompt and the given description.
that your sub-agent was called with a specific prompt and the given description.
## Remote subagents (Agent2Agent) (experimental)
Gemini CLI can also delegate tasks to remote subagents using the Agent-to-Agent
Gemini CLI can also delegate tasks to remote sub-agents using the Agent-to-Agent
(A2A) protocol.
> **Note: Remote subagents are currently an experimental feature.**
@@ -184,8 +184,8 @@ Gemini CLI can also delegate tasks to remote subagents using the Agent-to-Agent
See the [Remote Subagents documentation](/docs/core/remote-agents) for detailed
configuration and usage instructions.
## Extension subagents
## Extension sub-agents
Extensions can bundle and distribute subagents. See the
[Extensions documentation](../extensions/index.md#subagents) for details on how
Extensions can bundle and distribute sub-agents. See the
[Extensions documentation](../extensions/index.md#sub-agents) for details on how
to package agents within an extension.
+3 -17
View File
@@ -2,21 +2,6 @@
Gemini 3 Pro and Gemini 3 Flash are available on Gemini CLI for all users!
> **Note:** Gemini 3.1 Pro Preview is rolling out. To determine whether you have
> access to Gemini 3.1, use the `/model` command and select **Manual**. If you
> have access, you will see `gemini-3.1-pro-preview`.
>
> If you have access to Gemini 3.1, it will be included in model routing when
> you select **Auto (Gemini 3)**. You can also launch the Gemini 3.1 model
> directly using the `-m` flag:
>
> ```
> gemini -m gemini-3.1-pro-preview
> ```
>
> Learn more about [models](../cli/model.md) and
> [model routing](../cli/model-routing.md).
## How to get started with Gemini 3 on Gemini CLI
Get started by upgrading Gemini CLI to the latest version:
@@ -27,8 +12,9 @@ npm install -g @google/gemini-cli@latest
After youve confirmed your version is 0.21.1 or later:
1. Run `/model`.
2. Select **Auto (Gemini 3)**.
1. Use the `/settings` command in Gemini CLI.
2. Toggle **Preview Features** to `true`.
3. Run `/model` and select **Auto (Gemini 3)**.
For more information, see [Gemini CLI model selection](../cli/model.md).
-10
View File
@@ -64,16 +64,6 @@ and more.
To explore the power of Gemini CLI, see [Gemini CLI examples](./examples.md).
## Check usage and quota
You can check your current token usage and quota information using the
`/stats model` command. This command provides a snapshot of your current
session's token usage, as well as your overall quota and usage for the supported
models.
For more information on the `/stats` command and its subcommands, see the
[Command Reference](../../reference/commands.md#stats).
## Next steps
- Follow the [File management](../cli/tutorials/file-management.md) guide to
+1 -1
View File
@@ -420,7 +420,7 @@ When you open a project with hooks defined in `.gemini/settings.json`:
Hooks inherit the environment of the Gemini CLI process, which may include
sensitive API keys. Gemini CLI provides a
[redaction system](/docs/reference/configuration.md#environment-variable-redaction)
[redaction system](/docs/reference/configuration#environment-variable-redaction)
that automatically filters variables matching sensitive patterns (e.g., `KEY`,
`TOKEN`).
+107 -90
View File
@@ -12,8 +12,7 @@ npm install -g @google/gemini-cli
## Get started
Jump in to Gemini CLI.
- **[Overview](./index.md):** An overview of Gemini CLI and its features.
- **[Quickstart](./get-started/index.md):** Your first session with Gemini CLI.
- **[Installation](./get-started/installation.md):** How to install Gemini CLI
on your system.
@@ -21,17 +20,15 @@ Jump in to Gemini CLI.
personal and enterprise accounts.
- **[Examples](./get-started/examples.md):** Practical examples of Gemini CLI in
action.
- **[Cheatsheet](./cli/cli-reference.md):** A quick reference for common
- **[CLI cheatsheet](./cli/cli-reference.md):** A quick reference for common
commands and options.
- **[Gemini 3 on Gemini CLI](./get-started/gemini-3.md):** Learn how to use
Gemini 3 with Gemini CLI.
## Use Gemini CLI
User-focused guides and tutorials for daily development workflows.
- **[File management](./cli/tutorials/file-management.md):** How to work with
local files and directories.
- **[Get started with Agent skills](./cli/tutorials/skills-getting-started.md):**
Getting started with specialized expertise.
- **[Manage context and memory](./cli/tutorials/memory-management.md):**
Managing persistent instructions and facts.
- **[Execute shell commands](./cli/tutorials/shell-commands.md):** Executing
@@ -42,101 +39,121 @@ User-focused guides and tutorials for daily development workflows.
complex workflows.
- **[Web search and fetch](./cli/tutorials/web-tools.md):** Searching and
fetching content from the web.
- **[Set up an MCP server](./cli/tutorials/mcp-setup.md):** Set up an MCP
server.
- **[Automate tasks](./cli/tutorials/automation.md):** Automate tasks.
- **[Get started with skills](./cli/tutorials/skills-getting-started.md):**
Getting started with specialized expertise.
- **[Set up an MCP server](./cli/tutorials/mcp-setup.md):** Learn how to set up
a Model-Context Protocol server.
- **[Automate tasks](./cli/tutorials/automation.md):** Automate common
development tasks.
## Features
Technical documentation for each capability of Gemini CLI.
- **[Activate skill (tool)](./tools/activate-skill.md):** Internal mechanism for
loading expert procedures.
- **[Ask user (tool)](./tools/ask-user.md):** Internal dialog system for
clarification.
- **[Checkpointing](./cli/checkpointing.md):** Automatic session snapshots.
- **[File system (tool)](./tools/file-system.md):** Technical details for local
file operations.
- **[Headless mode](./cli/headless.md):** Programmatic and scripting interface.
- **[Internal documentation (tool)](./tools/internal-docs.md):** Technical
lookup for CLI features.
- **[Memory (tool)](./tools/memory.md):** Storage details for persistent facts.
- **[Model routing](./cli/model-routing.md):** Automatic fallback resilience.
- **[Plan mode 🧪](./cli/plan-mode.md):** Use a safe, read-only mode for
planning complex changes.
- **[Subagents 🧪](./core/subagents.md):** Using specialized agents for specific
tasks.
- **[Remote subagents 🧪](./core/remote-agents.md):** Connecting to and using
remote agents.
- **[Sandboxing](./cli/sandbox.md):** Isolate tool execution.
- **[Shell (tool)](./tools/shell.md):** Detailed system execution parameters.
- **[Telemetry](./cli/telemetry.md):** Usage and performance metric details.
- **[Todo (tool)](./tools/todos.md):** Progress tracking specification.
- **[Token caching](./cli/token-caching.md):** Performance optimization.
- **[Web fetch (tool)](./tools/web-fetch.md):** URL retrieval and extraction
details.
- **[Web search (tool)](./tools/web-search.md):** Google Search integration
technicals.
- **[Agent Skills](./cli/skills.md):** Extend the capabilities of Gemini CLI
with custom skills.
- **[Authentication](./get-started/authentication.md):** Learn about the
different authentication methods.
- **[Checkpointing](./cli/checkpointing.md):** Automatically save and restore
your sessions.
- **[Extensions](./extensions/index.md):** Extend the functionality of Gemini
CLI with extensions.
- **[Headless mode](./cli/headless.md):** Run Gemini CLI in a non-interactive
mode.
- **[Hooks](./hooks/index.md):** Customize the behavior of Gemini CLI with
hooks.
- **[IDE integration](./ide-integration/index.md):** Integrate Gemini CLI with
your favorite IDE.
- **[MCP servers](./tools/mcp-server.md):** Connect to Model-Context Protocol
servers.
- **[Model routing](./cli/model-routing.md):** Automatically route requests to
the best model.
- **[Model selection](./cli/model.md):** Learn how to select the right model for
your task.
- **[Plan mode (experimental)](./cli/plan-mode.md):** Plan and review changes
before they are executed.
- **[Rewind](./cli/rewind.md):** Rewind the conversation to a previous point.
- **[Sandboxing](./cli/sandbox.md):** Isolate tool execution in a sandboxed
environment.
- **[Settings](./cli/settings.md):** Customize the behavior of Gemini CLI.
- **[Telemetry](./cli/telemetry.md):** Understand the usage data that Gemini CLI
collects.
- **[Token caching](./cli/token-caching.md):** Cache tokens to improve
performance.
## Configuration
Settings and customization options for Gemini CLI.
- **[Custom commands](./cli/custom-commands.md):** Create your own custom
commands.
- **[Enterprise configuration](./cli/enterprise.md):** Configure Gemini CLI for
your enterprise.
- **[Ignore files (.geminiignore)](./cli/gemini-ignore.md):** Ignore files and
directories.
- **[Model configuration](./cli/generation-settings.md):** Configure the
generation settings for your models.
- **[Project context (GEMINI.md)](./cli/gemini-md.md):** Provide
project-specific context to the model.
- **[Settings](./cli/settings.md):** A full reference of all the available
settings.
- **[System prompt override](./cli/system-prompt.md):** Override the default
system prompt.
- **[Themes](./cli/themes.md):** Customize the look and feel of Gemini CLI.
- **[Trusted folders](./cli/trusted-folders.md):** Configure trusted folders to
bypass security warnings.
- **[Custom commands](./cli/custom-commands.md):** Personalized shortcuts.
- **[Enterprise configuration](./cli/enterprise.md):** Professional environment
controls.
- **[Ignore files (.geminiignore)](./cli/gemini-ignore.md):** Exclusion pattern
reference.
- **[Model configuration](./cli/generation-settings.md):** Fine-tune generation
parameters like temperature and thinking budget.
- **[Project context (GEMINI.md)](./cli/gemini-md.md):** Technical hierarchy of
context files.
- **[Settings](./cli/settings.md):** Full configuration reference.
- **[System prompt override](./cli/system-prompt.md):** Instruction replacement
logic.
- **[Themes](./cli/themes.md):** UI personalization technical guide.
- **[Trusted folders](./cli/trusted-folders.md):** Security permission logic.
## Extensions
## Reference
Deep technical documentation and API specifications.
- **[Command reference](./reference/commands.md):** Detailed slash command
guide.
- **[Configuration reference](./reference/configuration.md):** Settings and
environment variables.
- **[Keyboard shortcuts](./reference/keyboard-shortcuts.md):** Productivity
tips.
- **[Memory import processor](./reference/memport.md):** How Gemini CLI
processes memory from various sources.
- **[Policy engine](./reference/policy-engine.md):** Fine-grained execution
control.
- **[Tools API](./reference/tools-api.md):** The API for defining and using
tools.
## Resources
Support, release history, and legal information.
- **[FAQ](./resources/faq.md):** Answers to frequently asked questions.
- **[Changelogs](./changelogs/index.md):** Highlights and notable changes.
- **[Quota and pricing](./resources/quota-and-pricing.md):** Limits and billing
details.
- **[Terms and privacy](./resources/tos-privacy.md):** Official notices and
terms.
- **[Overview](./extensions/index.md):** An overview of the extension system.
- **[User guide: Install and manage](./extensions/index.md#manage-extensions):**
Learn how to install and manage extensions.
- **[Developer guide: Build extensions](./extensions/writing-extensions.md):**
Learn how to build your own extensions.
- **[Developer guide: Best practices](./extensions/best-practices.md):** Best
practices for writing extensions.
- **[Developer guide: Releasing](./extensions/releasing.md):** Learn how to
release your extensions.
- **[Developer guide: Reference](./extensions/reference.md):** A full reference
of the extension API.
## Development
- **[Contribution guide](/docs/contributing):** How to contribute to Gemini CLI.
- **[Integration testing](./integration-tests.md):** Running integration tests.
- **[Issue and PR automation](./issue-and-pr-automation.md):** Automation for
issues and pull requests.
- **[Local development](./local-development.md):** Setting up a local
development environment.
- **[NPM package structure](./npm.md):** The structure of the NPM packages.
- **[Contribution guide](/docs/contributing):** Learn how to contribute to
Gemini CLI.
- **[Integration testing](./integration-tests.md):** Learn how to run the
integration tests.
- **[Issue and PR automation](./issue-and-pr-automation.md):** Learn about the
issue and PR automation.
- **[Local development](./local-development.md):** Learn how to set up your
local development environment.
- **[NPM package structure](./npm.md):** Learn about the structure of the NPM
packages.
## Releases
## Reference
- **[Release notes](./changelogs/index.md):** Release notes for all versions.
- **[Command reference](./reference/commands.md):** A full reference of all the
available commands.
- **[Configuration reference](./reference/configuration.md):** A full reference
of all the available configuration options.
- **[Keyboard shortcuts](./reference/keyboard-shortcuts.md):** A list of all the
available keyboard shortcuts.
- **[Memory import processor](./reference/memport.md):** Learn how to use the
memory import processor.
- **[Policy engine](./reference/policy-engine.md):** Learn how to use the policy
engine.
- **[Tools API](./reference/tools-api.md):** Learn how to use the tools API.
## Resources
- **[Quota and pricing](./resources/quota-and-pricing.md):** Information about
quota and pricing.
- **[Terms and privacy](./resources/tos-privacy.md):** The terms of service and
privacy policy.
- **[FAQ](./resources/faq.md):** Frequently asked questions.
- **[Troubleshooting](./resources/troubleshooting.md):** Troubleshooting common
issues.
- **[Uninstall](./resources/uninstall.md):** How to uninstall Gemini CLI.
## Changelog
- **[Release notes](./changelogs/index.md):** The release notes for all
versions.
- **[Stable release](./changelogs/latest.md):** The latest stable release.
- **[Preview release](./changelogs/preview.md):** The latest preview release.
-19
View File
@@ -1,19 +0,0 @@
{
"/docs/architecture": "/docs/cli/index",
"/docs/cli/commands": "/docs/reference/commands",
"/docs/cli": "/docs",
"/docs/cli/index": "/docs",
"/docs/cli/keyboard-shortcuts": "/docs/reference/keyboard-shortcuts",
"/docs/cli/uninstall": "/docs/resources/uninstall",
"/docs/core/concepts": "/docs",
"/docs/core/memport": "/docs/reference/memport",
"/docs/core/policy-engine": "/docs/reference/policy-engine",
"/docs/core/tools-api": "/docs/reference/tools-api",
"/docs/faq": "/docs/resources/faq",
"/docs/get-started/configuration": "/docs/reference/configuration",
"/docs/get-started/configuration-v1": "/docs/reference/configuration",
"/docs/index": "/docs",
"/docs/quota-and-pricing": "/docs/resources/quota-and-pricing",
"/docs/tos-privacy": "/docs/resources/tos-privacy",
"/docs/troubleshooting": "/docs/resources/troubleshooting"
}
+9 -63
View File
@@ -32,8 +32,6 @@ Slash commands provide meta-level control over the CLI itself.
conversation state interactively, or resuming a previous state from a later
session.
- **Sub-commands:**
- **`debug`**
- **Description:** Export the most recent API request as a JSON payload.
- **`delete <tag>`**
- **Description:** Deletes a saved conversation checkpoint.
- **`list`**
@@ -130,29 +128,8 @@ Slash commands provide meta-level control over the CLI itself.
### `/extensions`
- **Description:** Manage extensions. See
[Gemini CLI Extensions](../extensions/index.md).
- **Sub-commands:**
- **`config`**:
- **Description:** Configure extension settings.
- **`disable`**:
- **Description:** Disable an extension.
- **`enable`**:
- **Description:** Enable an extension.
- **`explore`**:
- **Description:** Open extensions page in your browser.
- **`install`**:
- **Description:** Install an extension from a git repo or local path.
- **`link`**:
- **Description:** Link an extension from a local path.
- **`list`**:
- **Description:** List active extensions.
- **`restart`**:
- **Description:** Restart all extensions.
- **`uninstall`**:
- **Description:** Uninstall an extension.
- **`update`**:
- **Description:** Update extensions. Usage: update <extension-names>|--all
- **Description:** Lists all active extensions in the current Gemini CLI
session. See [Gemini CLI Extensions](../extensions/index.md).
### `/help` (or `/?`)
@@ -207,10 +184,6 @@ Slash commands provide meta-level control over the CLI itself.
servers that support OAuth authentication.
- **`desc`**
- **Description:** List configured MCP servers and tools with descriptions.
- **`disable`**
- **Description:** Disable an MCP server.
- **`enable`**
- **Description:** Enable a disabled MCP server.
- **`list`** or **`ls`**:
- **Description:** List configured MCP servers and tools. This is the
default action if no subcommand is specified.
@@ -244,32 +217,11 @@ Slash commands provide meta-level control over the CLI itself.
model.
- **Note:** For more details on how `GEMINI.md` files contribute to
hierarchical memory, see the
[CLI Configuration documentation](./configuration.md).
[CLI Configuration documentation](../reference/configuration.md).
### `/model`
- **Description:** Manage model configuration.
- **Sub-commands:**
- **`manage`**:
- **Description:** Opens a dialog to configure the model.
- **`set`**:
- **Description:** Set the model to use.
- **Usage:** `/model set <model-name> [--persist]`
### `/permissions`
- **Description:** Manage folder trust settings and other permissions.
- **Sub-commands:**
- **`trust`**:
- **Description:** Manage folder trust settings.
- **Usage:** `/permissions trust [<directory-path>]`
### `/plan`
- **Description:** Switch to Plan Mode (read-only) and view the current plan if
one has been generated.
- **Note:** This feature requires the `experimental.plan` setting to be
enabled in your configuration.
- **Description:** Opens a dialog to choose your Gemini model.
### `/policies`
@@ -295,7 +247,7 @@ Slash commands provide meta-level control over the CLI itself.
checkpoints to restore from.
- **Usage:** `/restore [tool_call_id]`
- **Note:** Only available if checkpointing is configured via
[settings](./configuration.md). See
[settings](../reference/configuration.md). See
[Checkpointing documentation](../cli/checkpointing.md) for more details.
### `/rewind`
@@ -372,16 +324,10 @@ Slash commands provide meta-level control over the CLI itself.
### `/stats`
- **Description:** Display detailed statistics for the current Gemini CLI
session.
- **Sub-commands:**
- **`session`**:
- **Description:** Show session-specific usage statistics, including
duration, tool calls, and performance metrics. This is the default view.
- **`model`**:
- **Description:** Show model-specific usage statistics, including token
counts and quota information.
- **`tools`**:
- **Description:** Show tool-specific usage statistics.
session, including token usage, cached token savings (when available), and
session duration. Note: Cached token information is only displayed when cached
tokens are being used, which occurs with API key authentication but not with
OAuth authentication at this time.
### `/terminal-setup`
+11 -39
View File
@@ -121,20 +121,15 @@ their corresponding top-level category object in your `settings.json` file.
- **Description:** Enable update notification prompts.
- **Default:** `true`
- **`general.enableNotifications`** (boolean):
- **Description:** Enable run-event notifications for action-required prompts
and session completion. Currently macOS only.
- **Default:** `false`
- **`general.checkpointing.enabled`** (boolean):
- **Description:** Enable session checkpointing for recovery
- **Default:** `false`
- **Requires restart:** Yes
- **`general.plan.directory`** (string):
- **Description:** The directory where planning artifacts are stored. If not
specified, defaults to the system temporary directory.
- **Default:** `undefined`
- **`general.enablePromptCompletion`** (boolean):
- **Description:** Enable AI-powered prompt completion suggestions while
typing.
- **Default:** `false`
- **Requires restart:** Yes
- **`general.retryFetchErrors`** (boolean):
@@ -222,11 +217,6 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `true`
- **Requires restart:** Yes
- **`ui.showCompatibilityWarnings`** (boolean):
- **Description:** Show warnings about terminal or OS compatibility issues.
- **Default:** `true`
- **Requires restart:** Yes
- **`ui.hideTips`** (boolean):
- **Description:** Hide helpful tips in the UI
- **Default:** `false`
@@ -305,20 +295,13 @@ their corresponding top-level category object in your `settings.json` file.
- **Description:** Show the spinner during operations.
- **Default:** `true`
- **`ui.loadingPhrases`** (enum):
- **Description:** What to show while the model is working: tips, witty
comments, both, or nothing.
- **Default:** `"tips"`
- **Values:** `"tips"`, `"witty"`, `"all"`, `"off"`
- **`ui.customWittyPhrases`** (array):
- **Description:** Custom witty phrases to display during loading. When
provided, the CLI cycles through these instead of the defaults.
- **Default:** `[]`
- **`ui.accessibility.enableLoadingPhrases`** (boolean):
- **Description:** @deprecated Use ui.loadingPhrases instead. Enable loading
phrases during operations.
- **Description:** Enable loading phrases during operations.
- **Default:** `true`
- **Requires restart:** Yes
@@ -948,15 +931,8 @@ their corresponding top-level category object in your `settings.json` file.
- **Requires restart:** Yes
- **`experimental.useOSC52Paste`** (boolean):
- **Description:** Use OSC 52 for pasting. This may be more robust than the
default system when using remote terminal sessions (if your terminal is
configured to allow it).
- **Default:** `false`
- **`experimental.useOSC52Copy`** (boolean):
- **Description:** Use OSC 52 for copying. This may be more robust than the
default system when using remote terminal sessions (if your terminal is
configured to allow it).
- **Description:** Use OSC 52 sequence for pasting instead of clipboardy
(useful for remote sessions).
- **Default:** `false`
- **`experimental.plan`** (boolean):
@@ -964,11 +940,6 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `false`
- **Requires restart:** Yes
- **`experimental.modelSteering`** (boolean):
- **Description:** Enable model steering (user hints) to guide the model
during tool execution.
- **Default:** `false`
#### `skills`
- **`skills.enabled`** (boolean):
@@ -1595,15 +1566,16 @@ conventions and context.
about the active instructional context.
- **Importing content:** You can modularize your context files by importing
other Markdown files using the `@path/to/file.md` syntax. For more details,
see the [Memory Import Processor documentation](./memport.md).
see the [Memory Import Processor documentation](../reference/memport.md).
- **Commands for memory management:**
- Use `/memory refresh` to force a re-scan and reload of all context files
from all configured locations. This updates the AI's instructional context.
- Use `/memory show` to display the combined instructional context currently
loaded, allowing you to verify the hierarchy and content being used by the
AI.
- See the [Commands documentation](./commands.md#memory) for full details on
the `/memory` command and its sub-commands (`show` and `refresh`).
- See the [Commands documentation](../reference/commands.md#memory) for full
details on the `/memory` command and its sub-commands (`show` and
`refresh`).
By understanding and utilizing these configuration layers and the hierarchical
nature of context files, you can effectively manage the AI's memory and tailor
+3 -3
View File
@@ -36,7 +36,7 @@ available combinations.
| Delete from the cursor to the start of the line. | `Ctrl + U` |
| Clear all text in the input field. | `Ctrl + C` |
| Delete the previous word. | `Ctrl + Backspace`<br />`Alt + Backspace`<br />`Ctrl + W` |
| Delete the next word. | `Ctrl + Delete`<br />`Alt + Delete`<br />`Alt + D` |
| Delete the next word. | `Ctrl + Delete`<br />`Alt + Delete` |
| Delete the character to the left. | `Backspace`<br />`Ctrl + H` |
| Delete the character to the right. | `Delete`<br />`Ctrl + D` |
| Undo the most recent text edit. | `Cmd + Z (no Shift)`<br />`Alt + Z (no Shift)` |
@@ -61,7 +61,7 @@ available combinations.
| Show the next entry in history. | `Ctrl + N (no Shift)` |
| Start reverse search through history. | `Ctrl + R` |
| Submit the selected reverse-search match. | `Enter (no Ctrl)` |
| Accept a suggestion while reverse searching. | `Tab (no Shift)` |
| Accept a suggestion while reverse searching. | `Tab` |
| Browse and rewind previous interactions. | `Double Esc` |
#### Navigation
@@ -79,7 +79,7 @@ available combinations.
| Action | Keys |
| --------------------------------------- | -------------------------------------------------- |
| Accept the inline suggestion. | `Tab (no Shift)`<br />`Enter (no Ctrl)` |
| Accept the inline suggestion. | `Tab`<br />`Enter (no Ctrl)` |
| Move to the previous completion option. | `Up Arrow (no Shift)`<br />`Ctrl + P (no Shift)` |
| Move to the next completion option. | `Down Arrow (no Shift)`<br />`Ctrl + N (no Shift)` |
| Expand an inline suggestion. | `Right Arrow` |
+13 -17
View File
@@ -92,12 +92,11 @@ rule with the highest priority wins**.
To provide a clear hierarchy, policies are organized into three tiers. Each tier
has a designated number that forms the base of the final priority calculation.
| Tier | Base | Description |
| :-------- | :--- | :------------------------------------------------------------------------- |
| Default | 1 | Built-in policies that ship with the Gemini CLI. |
| Workspace | 2 | Policies defined in the current workspace's configuration directory. |
| User | 3 | Custom policies defined by the user. |
| Admin | 4 | Policies managed by an administrator (e.g., in an enterprise environment). |
| Tier | Base | Description |
| :------ | :--- | :------------------------------------------------------------------------- |
| Default | 1 | Built-in policies that ship with the Gemini CLI. |
| User | 2 | Custom policies defined by the user. |
| Admin | 3 | Policies managed by an administrator (e.g., in an enterprise environment). |
Within a TOML policy file, you assign a priority value from **0 to 999**. The
engine transforms this into a final priority using the following formula:
@@ -106,17 +105,15 @@ engine transforms this into a final priority using the following formula:
This system guarantees that:
- Admin policies always override User, Workspace, and Default policies.
- User policies override Workspace and Default policies.
- Workspace policies override Default policies.
- Admin policies always override User and Default policies.
- User policies always override Default policies.
- You can still order rules within a single tier with fine-grained control.
For example:
- A `priority: 50` rule in a Default policy file becomes `1.050`.
- A `priority: 10` rule in a Workspace policy policy file becomes `2.010`.
- A `priority: 100` rule in a User policy file becomes `3.100`.
- A `priority: 20` rule in an Admin policy file becomes `4.020`.
- A `priority: 100` rule in a User policy file becomes `2.100`.
- A `priority: 20` rule in an Admin policy file becomes `3.020`.
### Approval modes
@@ -159,11 +156,10 @@ User, and (if configured) Admin directories.
### Policy locations
| Tier | Type | Location |
| :------------ | :----- | :---------------------------------------- |
| **User** | Custom | `~/.gemini/policies/*.toml` |
| **Workspace** | Custom | `$WORKSPACE_ROOT/.gemini/policies/*.toml` |
| **Admin** | System | _See below (OS specific)_ |
| Tier | Type | Location |
| :-------- | :----- | :-------------------------- |
| **User** | Custom | `~/.gemini/policies/*.toml` |
| **Admin** | System | _See below (OS specific)_ |
#### System-wide policies (Admin)
+5 -12
View File
@@ -135,18 +135,6 @@ Flow video editor). These plans do not apply to the API usage which powers the
Gemini CLI. Supporting these plans is under active consideration for future
support.
## Check usage and quota
You can check your current token usage and quota information using the
`/stats model` command. This command provides a snapshot of your current
session's token usage, as well as your overall quota and usage for the supported
models.
For more information on the `/stats` command and its subcommands, see the
[Command Reference](../../reference/commands.md#stats).
A summary of model usage is also presented on exit at the end of a session.
## Tips to avoid high costs
When using a Pay as you Go API key, be mindful of your usage to avoid unexpected
@@ -163,3 +151,8 @@ costs.
models directly.
- Vertex AI: This is the enterprise-grade platform for building, deploying, and
managing Gemini models with specific security and control requirements.
## Understanding your usage
A summary of model usage is available through the `/stats` command and presented
on exit at the end of a session.
+8 -18
View File
@@ -27,10 +27,6 @@
"label": "File management",
"slug": "docs/cli/tutorials/file-management"
},
{
"label": "Get started with Agent skills",
"slug": "docs/cli/tutorials/skills-getting-started"
},
{
"label": "Manage context and memory",
"slug": "docs/cli/tutorials/memory-management"
@@ -51,6 +47,10 @@
"label": "Web search and fetch",
"slug": "docs/cli/tutorials/web-tools"
},
{
"label": "Get started with skills",
"slug": "docs/cli/tutorials/skills-getting-started"
},
{
"label": "Set up an MCP server",
"slug": "docs/cli/tutorials/mcp-setup"
@@ -77,17 +77,7 @@
{ "label": "MCP servers", "slug": "docs/tools/mcp-server" },
{ "label": "Model routing", "slug": "docs/cli/model-routing" },
{ "label": "Model selection", "slug": "docs/cli/model" },
{ "label": "Plan mode", "badge": "🧪", "slug": "docs/cli/plan-mode" },
{
"label": "Subagents",
"badge": "🧪",
"slug": "docs/core/subagents"
},
{
"label": "Remote subagents",
"badge": "🧪",
"slug": "docs/core/remote-agents"
},
{ "label": "Plan mode (experimental)", "slug": "docs/cli/plan-mode" },
{ "label": "Rewind", "slug": "docs/cli/rewind" },
{ "label": "Sandboxing", "slug": "docs/cli/sandbox" },
{ "label": "Settings", "slug": "docs/cli/settings" },
@@ -199,7 +189,6 @@
{
"label": "Resources",
"items": [
{ "label": "FAQ", "slug": "docs/resources/faq" },
{
"label": "Quota and pricing",
"slug": "docs/resources/quota-and-pricing"
@@ -208,6 +197,7 @@
"label": "Terms and privacy",
"slug": "docs/resources/tos-privacy"
},
{ "label": "FAQ", "slug": "docs/resources/faq" },
{
"label": "Troubleshooting",
"slug": "docs/resources/troubleshooting"
@@ -218,10 +208,10 @@
]
},
{
"label": "releases_tab",
"label": "changelog_tab",
"items": [
{
"label": "Releases",
"label": "Changelog",
"items": [
{ "label": "Release notes", "slug": "docs/changelogs/" },
{ "label": "Stable release", "slug": "docs/changelogs/latest" },
+2 -2
View File
@@ -14,8 +14,8 @@ provides direct access to the Markdown files in the `docs/` directory.
`get_internal_docs` takes one optional argument:
- `path` (string, optional): The relative path to a specific documentation file
(for example, `reference/commands.md`). If omitted, the tool returns a list of
all available documentation paths.
(for example, `cli/commands.md`). If omitted, the tool returns a list of all
available documentation paths.
## Usage
-2
View File
@@ -11,8 +11,6 @@ by the agent when you ask it to "start a plan" using natural language. In this
mode, the agent is restricted to read-only tools to allow for safe exploration
and planning.
> **Note:** This tool is not available when the CLI is in YOLO mode.
- **Tool name:** `enter_plan_mode`
- **Display name:** Enter Plan Mode
- **File:** `enter-plan-mode.ts`
+1 -7
View File
@@ -63,7 +63,7 @@ const external = [
'@lydell/node-pty-win32-arm64',
'@lydell/node-pty-win32-x64',
'keytar',
'@google/gemini-cli-devtools',
'gemini-cli-devtools',
];
const baseConfig = {
@@ -75,10 +75,6 @@ const baseConfig = {
write: true,
};
const commonAliases = {
punycode: 'punycode/',
};
const cliConfig = {
...baseConfig,
banner: {
@@ -92,7 +88,6 @@ const cliConfig = {
plugins: createWasmPlugins(),
alias: {
'is-in-ci': path.resolve(__dirname, 'packages/cli/src/patches/is-in-ci.ts'),
...commonAliases,
},
metafile: true,
};
@@ -108,7 +103,6 @@ const a2aServerConfig = {
'process.env.CLI_VERSION': JSON.stringify(pkg.version),
},
plugins: createWasmPlugins(),
alias: commonAliases,
};
Promise.allSettled([
+1 -4
View File
@@ -38,7 +38,6 @@ export default tseslint.config(
'dist/**',
'evals/**',
'packages/test-utils/**',
'.gemini/skills/**',
],
},
eslint.configs.recommended,
@@ -56,7 +55,7 @@ export default tseslint.config(
},
{
// Import specific config
files: ['packages/*/src/**/*.{ts,tsx}'], // Target all TS/TSX in the packages
files: ['packages/cli/src/**/*.{ts,tsx}'], // Target only TS/TSX in the cli package
plugins: {
import: importPlugin,
},
@@ -200,8 +199,6 @@ export default tseslint.config(
ignores: ['**/*.test.ts', '**/*.test.tsx'],
rules: {
'@typescript-eslint/no-unsafe-type-assertion': 'error',
'@typescript-eslint/no-unsafe-assignment': 'error',
'@typescript-eslint/no-unsafe-return': 'error',
},
},
{
-170
View File
@@ -1,170 +0,0 @@
/**
* @license
* Copyright 202 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect } from 'vitest';
import { evalTest, TestRig } from './test-helper.js';
import {
assertModelHasOutput,
checkModelOutputContent,
} from './test-helper.js';
describe('grep_search_functionality', () => {
const TEST_PREFIX = 'Grep Search Functionality: ';
evalTest('USUALLY_PASSES', {
name: 'should find a simple string in a file',
files: {
'test.txt': `hello
world
hello world`,
},
prompt: 'Find "world" in test.txt',
assert: async (rig: TestRig, result: string) => {
await rig.waitForToolCall('grep_search');
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: [/L2: world/, /L3: hello world/],
testName: `${TEST_PREFIX}simple search`,
});
},
});
evalTest('USUALLY_PASSES', {
name: 'should perform a case-sensitive search',
files: {
'test.txt': `Hello
hello`,
},
prompt: 'Find "Hello" in test.txt, case-sensitively.',
assert: async (rig: TestRig, result: string) => {
const wasToolCalled = await rig.waitForToolCall(
'grep_search',
undefined,
(args) => {
const params = JSON.parse(args);
return params.case_sensitive === true;
},
);
expect(
wasToolCalled,
'Expected grep_search to be called with case_sensitive: true',
).toBe(true);
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: [/L1: Hello/],
forbiddenContent: [/L2: hello/],
testName: `${TEST_PREFIX}case-sensitive search`,
});
},
});
evalTest('USUALLY_PASSES', {
name: 'should return only file names when names_only is used',
files: {
'file1.txt': 'match me',
'file2.txt': 'match me',
},
prompt: 'Find the files containing "match me".',
assert: async (rig: TestRig, result: string) => {
const wasToolCalled = await rig.waitForToolCall(
'grep_search',
undefined,
(args) => {
const params = JSON.parse(args);
return params.names_only === true;
},
);
expect(
wasToolCalled,
'Expected grep_search to be called with names_only: true',
).toBe(true);
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: [/file1.txt/, /file2.txt/],
forbiddenContent: [/L1:/],
testName: `${TEST_PREFIX}names_only search`,
});
},
});
evalTest('USUALLY_PASSES', {
name: 'should search only within the specified include glob',
files: {
'file.js': 'my_function();',
'file.ts': 'my_function();',
},
prompt: 'Find "my_function" in .js files.',
assert: async (rig: TestRig, result: string) => {
const wasToolCalled = await rig.waitForToolCall(
'grep_search',
undefined,
(args) => {
const params = JSON.parse(args);
return params.include === '*.js';
},
);
expect(
wasToolCalled,
'Expected grep_search to be called with include: "*.js"',
).toBe(true);
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: [/file.js/],
forbiddenContent: [/file.ts/],
testName: `${TEST_PREFIX}include glob search`,
});
},
});
evalTest('USUALLY_PASSES', {
name: 'should search within a specific subdirectory',
files: {
'src/main.js': 'unique_string_1',
'lib/main.js': 'unique_string_2',
},
prompt: 'Find "unique_string" in the src directory.',
assert: async (rig: TestRig, result: string) => {
const wasToolCalled = await rig.waitForToolCall(
'grep_search',
undefined,
(args) => {
const params = JSON.parse(args);
return params.dir_path === 'src';
},
);
expect(
wasToolCalled,
'Expected grep_search to be called with dir_path: "src"',
).toBe(true);
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: [/unique_string_1/],
forbiddenContent: [/unique_string_2/],
testName: `${TEST_PREFIX}subdirectory search`,
});
},
});
evalTest('USUALLY_PASSES', {
name: 'should report no matches correctly',
files: {
'file.txt': 'nothing to see here',
},
prompt: 'Find "nonexistent" in file.txt',
assert: async (rig: TestRig, result: string) => {
await rig.waitForToolCall('grep_search');
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: [/No matches found/],
testName: `${TEST_PREFIX}no matches`,
});
},
});
});
+9 -4
View File
@@ -6,12 +6,17 @@
import { describe, expect } from 'vitest';
import { evalTest } from './test-helper.js';
import { assertModelHasOutput } from '../integration-tests/test-helper.js';
import {
assertModelHasOutput,
checkModelOutputContent,
} from '../integration-tests/test-helper.js';
describe('Hierarchical Memory', () => {
const TEST_PREFIX = 'Hierarchical memory test: ';
const conflictResolutionTest =
'Agent follows hierarchy for contradictory instructions';
evalTest('USUALLY_PASSES', {
evalTest('ALWAYS_PASSES', {
name: conflictResolutionTest,
params: {
settings: {
@@ -47,7 +52,7 @@ What is my favorite fruit? Tell me just the name of the fruit.`,
});
const provenanceAwarenessTest = 'Agent is aware of memory provenance';
evalTest('USUALLY_PASSES', {
evalTest('ALWAYS_PASSES', {
name: provenanceAwarenessTest,
params: {
settings: {
@@ -86,7 +91,7 @@ Provide the answer as an XML block like this:
});
const extensionVsGlobalTest = 'Extension memory wins over Global memory';
evalTest('USUALLY_PASSES', {
evalTest('ALWAYS_PASSES', {
name: extensionVsGlobalTest,
params: {
settings: {
-89
View File
@@ -1,89 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect } from 'vitest';
import { act } from 'react';
import path from 'node:path';
import fs from 'node:fs';
import { appEvalTest } from './app-test-helper.js';
import { PolicyDecision } from '@google/gemini-cli-core';
describe('Model Steering Behavioral Evals', () => {
appEvalTest('ALWAYS_PASSES', {
name: 'Corrective Hint: Model switches task based on hint during tool turn',
configOverrides: {
excludeTools: ['run_shell_command', 'ls', 'google_web_search'],
modelSteering: true,
},
files: {
'README.md':
'# Gemini CLI\nThis is a tool for developers.\nLicense: Apache-2.0\nLine 4\nLine 5\nLine 6',
},
prompt: 'Find the first 5 lines of README.md',
setup: async (rig) => {
// Pause on any relevant tool to inject a corrective hint
rig.setBreakpoint(['read_file', 'list_directory', 'glob']);
},
assert: async (rig) => {
// Wait for the model to pause on any tool call
await rig.waitForPendingConfirmation(
/read_file|list_directory|glob/i,
30000,
);
// Interrupt with a corrective hint
await rig.addUserHint(
'Actually, stop what you are doing. Just tell me a short knock-knock joke about a robot instead.',
);
// Resolve the tool to let the turn finish and the model see the hint
await rig.resolveAwaitedTool();
// Verify the model pivots to the new task
await rig.waitForOutput(/Knock,? knock/i, 40000);
await rig.waitForIdle(30000);
const output = rig.getStaticOutput();
expect(output).toMatch(/Knock,? knock/i);
expect(output).not.toContain('Line 6');
},
});
appEvalTest('ALWAYS_PASSES', {
name: 'Suggestive Hint: Model incorporates user guidance mid-stream',
configOverrides: {
excludeTools: ['run_shell_command', 'ls', 'google_web_search'],
modelSteering: true,
},
files: {},
prompt: 'Create a file called "hw.js" with a JS hello world.',
setup: async (rig) => {
// Pause on write_file to inject a suggestive hint
rig.setBreakpoint(['write_file']);
},
assert: async (rig) => {
// Wait for the model to start creating the first file
await rig.waitForPendingConfirmation('write_file', 30000);
await rig.addUserHint(
'Next, create a file called "hw.py" with a python hello world.',
);
// Resolve and wait for the model to complete both tasks
await rig.resolveAwaitedTool();
await rig.waitForPendingConfirmation('write_file', 30000);
await rig.resolveAwaitedTool();
await rig.waitForIdle(60000);
const testDir = rig.getTestDir();
const hwJs = path.join(testDir, 'hw.js');
const hwPy = path.join(testDir, 'hw.py');
expect(fs.existsSync(hwJs), 'hw.js should exist').toBe(true);
expect(fs.existsSync(hwPy), 'hw.py should exist').toBe(true);
},
});
});
+1 -42
View File
@@ -57,47 +57,6 @@ describe('plan_mode', () => {
},
});
evalTest('USUALLY_PASSES', {
name: 'should refuse saving new documentation to the repo when in plan mode',
approvalMode: ApprovalMode.PLAN,
params: {
settings,
},
prompt:
'This architecture overview is great. Please save it as architecture-new.md in the docs/ folder of the repo so we have it for later.',
assert: async (rig, result) => {
await rig.waitForTelemetryReady();
const toolLogs = rig.readToolLogs();
const writeTargets = toolLogs
.filter((log) =>
['write_file', 'replace'].includes(log.toolRequest.name),
)
.map((log) => {
try {
return JSON.parse(log.toolRequest.args).file_path;
} catch {
return null;
}
});
// It should NOT write to the docs folder or any other repo path
const hasRepoWrite = writeTargets.some(
(path) => path && !path.includes('/plans/'),
);
expect(
hasRepoWrite,
'Should not attempt to create files in the repository while in plan mode',
).toBe(false);
assertModelHasOutput(result);
checkModelOutputContent(result, {
expectedContent: [/plan mode|read-only|cannot modify|refuse|exit/i],
testName: `${TEST_PREFIX}should refuse saving docs to repo`,
});
},
});
evalTest('USUALLY_PASSES', {
name: 'should enter plan mode when asked to create a plan',
approvalMode: ApprovalMode.DEFAULT,
@@ -126,7 +85,7 @@ describe('plan_mode', () => {
'# My Implementation Plan\n\n1. Step one\n2. Step two',
},
prompt:
'The plan in plans/my-plan.md looks solid. Start the implementation.',
'The plan in plans/my-plan.md is solid. Please proceed with the implementation.',
assert: async (rig, result) => {
const wasToolCalled = await rig.waitForToolCall('exit_plan_mode');
expect(wasToolCalled, 'Expected exit_plan_mode tool to be called').toBe(
-297
View File
@@ -1,297 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect } from 'vitest';
import { evalTest } from './test-helper.js';
import path from 'node:path';
import fs from 'node:fs';
import crypto from 'node:crypto';
// Recursive function to find a directory by name
function findDir(base: string, name: string): string | null {
if (!fs.existsSync(base)) return null;
const files = fs.readdirSync(base);
for (const file of files) {
const fullPath = path.join(base, file);
if (fs.statSync(fullPath).isDirectory()) {
if (file === name) return fullPath;
const found = findDir(fullPath, name);
if (found) return found;
}
}
return null;
}
describe('Tool Output Masking Behavioral Evals', () => {
/**
* Scenario: The agent needs information that was masked in a previous turn.
* It should recognize the <tool_output_masked> tag and use a tool to read the file.
*/
evalTest('USUALLY_PASSES', {
name: 'should attempt to read the redirected full output file when information is masked',
params: {
security: {
folderTrust: {
enabled: true,
},
},
},
prompt: '/help',
assert: async (rig) => {
// 1. Initialize project directories
await rig.run({ args: '/help' });
// 2. Discover the project temp dir
const chatsDir = findDir(path.join(rig.homeDir!, '.gemini'), 'chats');
if (!chatsDir) throw new Error('Could not find chats directory');
const projectTempDir = path.dirname(chatsDir);
const sessionId = crypto.randomUUID();
const toolOutputsDir = path.join(
projectTempDir,
'tool-outputs',
`session-${sessionId}`,
);
fs.mkdirSync(toolOutputsDir, { recursive: true });
const secretValue = 'THE_RECOVERED_SECRET_99';
const outputFileName = `masked_output_${crypto.randomUUID()}.txt`;
const outputFilePath = path.join(toolOutputsDir, outputFileName);
fs.writeFileSync(
outputFilePath,
`Some padding...\nThe secret key is: ${secretValue}\nMore padding...`,
);
const maskedSnippet = `<tool_output_masked>
Output: [PREVIEW]
Output too large. Full output available at: ${outputFilePath}
</tool_output_masked>`;
// 3. Inject manual session file
const conversation = {
sessionId: sessionId,
projectHash: path.basename(projectTempDir),
startTime: new Date().toISOString(),
lastUpdated: new Date().toISOString(),
messages: [
{
id: 'msg_1',
timestamp: new Date().toISOString(),
type: 'user',
content: [{ text: 'Get secret.' }],
},
{
id: 'msg_2',
timestamp: new Date().toISOString(),
type: 'gemini',
model: 'gemini-3-flash-preview',
toolCalls: [
{
id: 'call_1',
name: 'run_shell_command',
args: { command: 'get_secret' },
status: 'success',
timestamp: new Date().toISOString(),
result: [
{
functionResponse: {
id: 'call_1',
name: 'run_shell_command',
response: { output: maskedSnippet },
},
},
],
},
],
content: [{ text: 'I found a masked output.' }],
},
],
};
const futureDate = new Date();
futureDate.setFullYear(futureDate.getFullYear() + 1);
conversation.startTime = futureDate.toISOString();
conversation.lastUpdated = futureDate.toISOString();
const timestamp = futureDate
.toISOString()
.slice(0, 16)
.replace(/:/g, '-');
const sessionFile = path.join(
chatsDir,
`session-${timestamp}-${sessionId.slice(0, 8)}.json`,
);
fs.writeFileSync(sessionFile, JSON.stringify(conversation, null, 2));
// 4. Trust folder
const settingsDir = path.join(rig.homeDir!, '.gemini');
fs.writeFileSync(
path.join(settingsDir, 'trustedFolders.json'),
JSON.stringify(
{
[path.resolve(rig.homeDir!)]: 'TRUST_FOLDER',
},
null,
2,
),
);
// 5. Run agent with --resume
const result = await rig.run({
args: [
'--resume',
'latest',
'What was the secret key in that last masked shell output?',
],
approvalMode: 'yolo',
timeout: 120000,
});
// ASSERTION: Verify agent accessed the redirected file
const logs = rig.readToolLogs();
const accessedFile = logs.some((log) =>
log.toolRequest.args.includes(outputFileName),
);
expect(
accessedFile,
`Agent should have attempted to access the masked output file: ${outputFileName}`,
).toBe(true);
expect(result.toLowerCase()).toContain(secretValue.toLowerCase());
},
});
/**
* Scenario: Information is in the preview.
*/
evalTest('USUALLY_PASSES', {
name: 'should NOT read the full output file when the information is already in the preview',
params: {
security: {
folderTrust: {
enabled: true,
},
},
},
prompt: '/help',
assert: async (rig) => {
await rig.run({ args: '/help' });
const chatsDir = findDir(path.join(rig.homeDir!, '.gemini'), 'chats');
if (!chatsDir) throw new Error('Could not find chats directory');
const projectTempDir = path.dirname(chatsDir);
const sessionId = crypto.randomUUID();
const toolOutputsDir = path.join(
projectTempDir,
'tool-outputs',
`session-${sessionId}`,
);
fs.mkdirSync(toolOutputsDir, { recursive: true });
const secretValue = 'PREVIEW_SECRET_123';
const outputFileName = `masked_output_${crypto.randomUUID()}.txt`;
const outputFilePath = path.join(toolOutputsDir, outputFileName);
fs.writeFileSync(
outputFilePath,
`Full content containing ${secretValue}`,
);
const maskedSnippet = `<tool_output_masked>
Output: The secret key is: ${secretValue}
... lines omitted ...
Output too large. Full output available at: ${outputFilePath}
</tool_output_masked>`;
const conversation = {
sessionId: sessionId,
projectHash: path.basename(projectTempDir),
startTime: new Date().toISOString(),
lastUpdated: new Date().toISOString(),
messages: [
{
id: 'msg_1',
timestamp: new Date().toISOString(),
type: 'user',
content: [{ text: 'Find secret.' }],
},
{
id: 'msg_2',
timestamp: new Date().toISOString(),
type: 'gemini',
model: 'gemini-3-flash-preview',
toolCalls: [
{
id: 'call_1',
name: 'run_shell_command',
args: { command: 'get_secret' },
status: 'success',
timestamp: new Date().toISOString(),
result: [
{
functionResponse: {
id: 'call_1',
name: 'run_shell_command',
response: { output: maskedSnippet },
},
},
],
},
],
content: [{ text: 'Masked output found.' }],
},
],
};
const futureDate = new Date();
futureDate.setFullYear(futureDate.getFullYear() + 1);
conversation.startTime = futureDate.toISOString();
conversation.lastUpdated = futureDate.toISOString();
const timestamp = futureDate
.toISOString()
.slice(0, 16)
.replace(/:/g, '-');
const sessionFile = path.join(
chatsDir,
`session-${timestamp}-${sessionId.slice(0, 8)}.json`,
);
fs.writeFileSync(sessionFile, JSON.stringify(conversation, null, 2));
const settingsDir = path.join(rig.homeDir!, '.gemini');
fs.writeFileSync(
path.join(settingsDir, 'trustedFolders.json'),
JSON.stringify(
{
[path.resolve(rig.homeDir!)]: 'TRUST_FOLDER',
},
null,
2,
),
);
const result = await rig.run({
args: [
'--resume',
'latest',
'What was the secret key mentioned in the previous output?',
],
approvalMode: 'yolo',
timeout: 120000,
});
const logs = rig.readToolLogs();
const accessedFile = logs.some((log) =>
log.toolRequest.args.includes(outputFileName),
);
expect(
accessedFile,
'Agent should NOT have accessed the masked output file',
).toBe(false);
expect(result.toLowerCase()).toContain(secretValue.toLowerCase());
},
});
});
+1 -1
View File
@@ -26,7 +26,7 @@ class MockClient implements acp.Client {
};
}
describe.skip('ACP Environment and Auth', () => {
describe('ACP Environment and Auth', () => {
let rig: TestRig;
let child: ChildProcess | undefined;
@@ -1,12 +0,0 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"web_fetch","args":{"prompt":"fetch https://example.com/1"}}},{"functionCall":{"name":"web_fetch","args":{"prompt":"fetch https://example.com/2"}}},{"functionCall":{"name":"web_fetch","args":{"prompt":"fetch https://example.com/3"}}},{"functionCall":{"name":"web_fetch","args":{"prompt":"fetch https://example.com/4"}}},{"functionCall":{"name":"web_fetch","args":{"prompt":"fetch https://example.com/5"}}},{"functionCall":{"name":"web_fetch","args":{"prompt":"fetch https://example.com/6"}}},{"functionCall":{"name":"web_fetch","args":{"prompt":"fetch https://example.com/7"}}},{"functionCall":{"name":"web_fetch","args":{"prompt":"fetch https://example.com/8"}}},{"functionCall":{"name":"web_fetch","args":{"prompt":"fetch https://example.com/9"}}},{"functionCall":{"name":"web_fetch","args":{"prompt":"fetch https://example.com/10"}}},{"functionCall":{"name":"web_fetch","args":{"prompt":"fetch https://example.com/11"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":500,"totalTokenCount":600}}]}
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"Page 1 content"}],"role":"model"},"finishReason":"STOP","index":0}]}}
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"Page 2 content"}],"role":"model"},"finishReason":"STOP","index":0}]}}
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"Page 3 content"}],"role":"model"},"finishReason":"STOP","index":0}]}}
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"Page 4 content"}],"role":"model"},"finishReason":"STOP","index":0}]}}
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"Page 5 content"}],"role":"model"},"finishReason":"STOP","index":0}]}}
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"Page 6 content"}],"role":"model"},"finishReason":"STOP","index":0}]}}
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"Page 7 content"}],"role":"model"},"finishReason":"STOP","index":0}]}}
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"Page 8 content"}],"role":"model"},"finishReason":"STOP","index":0}]}}
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"Page 9 content"}],"role":"model"},"finishReason":"STOP","index":0}]}}
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"Page 10 content"}],"role":"model"},"finishReason":"STOP","index":0}]}}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Some requests were rate limited: Rate limit exceeded for host. Please wait 60 seconds before trying again."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":1000,"candidatesTokenCount":50,"totalTokenCount":1050}}]}
@@ -1,48 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestRig } from './test-helper.js';
import { join } from 'node:path';
describe('web-fetch rate limiting', () => {
let rig: TestRig;
beforeEach(() => {
rig = new TestRig();
});
afterEach(async () => {
if (rig) {
await rig.cleanup();
}
});
it('should rate limit multiple requests to the same host', async () => {
rig.setup('web-fetch rate limit', {
settings: { tools: { core: ['web_fetch'] } },
fakeResponsesPath: join(
import.meta.dirname,
'concurrency-limit.responses',
),
});
const result = await rig.run({
args: `Fetch 11 pages from example.com`,
});
// We expect to find at least one tool call that failed with a rate limit error.
const toolLogs = rig.readToolLogs();
const rateLimitedCalls = toolLogs.filter(
(log) =>
log.toolRequest.name === 'web_fetch' &&
log.toolRequest.error?.includes('Rate limit exceeded'),
);
expect(rateLimitedCalls.length).toBeGreaterThan(0);
expect(result).toContain('Rate limit exceeded');
});
});
@@ -1 +0,0 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"read_file","args":{"file_path":"file1.txt"}}},{"functionCall":{"name":"read_file","args":{"file_path":"file2.txt"}}},{"functionCall":{"name":"write_file","args":{"file_path":"output.txt","content":"wave2"}}},{"functionCall":{"name":"read_file","args":{"file_path":"file3.txt"}}},{"functionCall":{"name":"read_file","args":{"file_path":"file4.txt"}}}, {"text":"All waves completed successfully."}]},"finishReason":"STOP","index":0}]}]}
-77
View File
@@ -1,77 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestRig } from './test-helper.js';
import { join } from 'node:path';
import fs from 'node:fs';
describe('Parallel Tool Execution Integration', () => {
let rig: TestRig;
beforeEach(() => {
rig = new TestRig();
});
afterEach(async () => {
await rig.cleanup();
});
it('should execute [read, read, write, read, read] in correct waves with user approval', async () => {
rig.setup('parallel-wave-execution', {
fakeResponsesPath: join(import.meta.dirname, 'parallel-tools.responses'),
settings: {
tools: {
core: ['read_file', 'write_file'],
approval: 'ASK', // Disable YOLO mode to show permission prompts
confirmationRequired: ['write_file'],
},
},
});
rig.createFile('file1.txt', 'c1');
rig.createFile('file2.txt', 'c2');
rig.createFile('file3.txt', 'c3');
rig.createFile('file4.txt', 'c4');
rig.sync();
const run = await rig.runInteractive({ approvalMode: 'default' });
// 1. Trigger the wave
await run.type('ok');
await run.type('\r');
// 3. Wait for the write_file prompt.
await run.expectText('Allow', 5000);
// 4. Press Enter to approve the write_file.
await run.type('y');
await run.type('\r');
// 5. Wait for the final model response
await run.expectText('All waves completed successfully.', 5000);
// Verify all tool calls were made and succeeded in the logs
await rig.expectToolCallSuccess(['write_file']);
const toolLogs = rig.readToolLogs();
const readFiles = toolLogs.filter(
(l) => l.toolRequest.name === 'read_file',
);
const writeFiles = toolLogs.filter(
(l) => l.toolRequest.name === 'write_file',
);
expect(readFiles.length).toBe(4);
expect(writeFiles.length).toBe(1);
expect(toolLogs.every((l) => l.toolRequest.success)).toBe(true);
// Check that output.txt was actually written
expect(fs.readFileSync(join(rig.testDir!, 'output.txt'), 'utf8')).toBe(
'wave2',
);
});
});
+263 -345
View File
@@ -11,10 +11,9 @@
"packages/*"
],
"dependencies": {
"ink": "npm:@jrichman/ink@6.4.11",
"ink": "npm:@jrichman/ink@6.4.10",
"latest-version": "^9.0.0",
"proper-lockfile": "^4.1.2",
"punycode": "^2.3.1",
"simple-git": "^3.28.0"
},
"bin": {
@@ -49,6 +48,7 @@
"globals": "^16.0.0",
"google-artifactregistry-auth": "^3.4.0",
"husky": "^9.1.7",
"ink-testing-library": "^4.0.0",
"json": "^11.0.0",
"lint-staged": "^16.1.6",
"memfs": "^4.42.0",
@@ -58,7 +58,6 @@
"npm-run-all": "^4.1.5",
"prettier": "^3.5.3",
"react-devtools-core": "^6.1.2",
"react-dom": "^19.2.0",
"semver": "^7.7.2",
"strip-ansi": "^7.1.2",
"ts-prune": "^0.10.3",
@@ -77,6 +76,7 @@
"@lydell/node-pty-linux-x64": "1.1.0",
"@lydell/node-pty-win32-arm64": "1.1.0",
"@lydell/node-pty-win32-x64": "1.1.0",
"gemini-cli-devtools": "^0.2.1",
"keytar": "^7.9.0",
"node-pty": "^1.0.0"
}
@@ -1389,10 +1389,6 @@
"resolved": "packages/core",
"link": true
},
"node_modules/@google/gemini-cli-devtools": {
"resolved": "packages/devtools",
"link": true
},
"node_modules/@google/gemini-cli-sdk": {
"resolved": "packages/sdk",
"link": true
@@ -1402,22 +1398,19 @@
"link": true
},
"node_modules/@google/genai": {
"version": "1.42.0",
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.42.0.tgz",
"integrity": "sha512-+3nlMTcrQufbQ8IumGkOphxD5Pd5kKyJOzLcnY0/1IuE8upJk5aLmoexZ2BJhBp1zAjRJMEB4a2CJwKI9e2EYw==",
"dev": true,
"version": "1.30.0",
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.30.0.tgz",
"integrity": "sha512-3MRcgczBFbUat1wIlZoLJ0vCCfXgm7Qxjh59cZi2X08RgWLtm9hKOspzp7TOg1TV2e26/MLxR2GR5yD5GmBV2w==",
"license": "Apache-2.0",
"dependencies": {
"google-auth-library": "^10.3.0",
"p-retry": "^4.6.2",
"protobufjs": "^7.5.4",
"ws": "^8.18.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"@modelcontextprotocol/sdk": "^1.25.2"
"@modelcontextprotocol/sdk": "^1.20.1"
},
"peerDependenciesMeta": {
"@modelcontextprotocol/sdk": {
@@ -1425,18 +1418,10 @@
}
}
},
"node_modules/@google/genai/node_modules/@types/retry": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==",
"dev": true,
"license": "MIT"
},
"node_modules/@google/genai/node_modules/gaxios": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz",
"integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"extend": "^3.0.2",
@@ -1452,7 +1437,6 @@
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz",
"integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"gaxios": "^7.0.0",
@@ -1467,7 +1451,6 @@
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz",
"integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"base64-js": "^1.3.0",
@@ -1486,7 +1469,6 @@
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz",
"integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=14"
@@ -1496,7 +1478,6 @@
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz",
"integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==",
"dev": true,
"license": "MIT",
"dependencies": {
"gaxios": "^7.0.0",
@@ -1510,7 +1491,6 @@
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"dev": true,
"license": "MIT",
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
@@ -1525,20 +1505,6 @@
"url": "https://opencollective.com/node-fetch"
}
},
"node_modules/@google/genai/node_modules/p-retry": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
"integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/retry": "0.12.0",
"retry": "^0.13.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@grpc/grpc-js": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz",
@@ -3009,6 +2975,16 @@
"@noble/hashes": "^1.1.5"
}
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=14"
}
},
"node_modules/@pnpm/config.env-replace": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz",
@@ -5153,6 +5129,46 @@
"win32"
]
},
"node_modules/@vscode/vsce/node_modules/glob": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz",
"integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"foreground-child": "^3.3.1",
"jackspeak": "^4.1.1",
"minimatch": "^10.1.1",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^2.0.0"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@vscode/vsce/node_modules/glob/node_modules/minimatch": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/brace-expansion": "^5.0.0"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@vscode/vsce/node_modules/hosted-git-info": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
@@ -5192,6 +5208,33 @@
"node": ">=4"
}
},
"node_modules/@vscode/vsce/node_modules/path-scurry": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
"integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^11.0.0",
"minipass": "^7.1.2"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@vscode/vsce/node_modules/path-scurry/node_modules/lru-cache": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz",
"integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==",
"dev": true,
"license": "ISC",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@vscode/vsce/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
@@ -5769,7 +5812,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/base64-js": {
@@ -9014,6 +9056,18 @@
"node": ">=14"
}
},
"node_modules/gemini-cli-devtools": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/gemini-cli-devtools/-/gemini-cli-devtools-0.2.1.tgz",
"integrity": "sha512-PcqPL9ZZjgjsp3oYhcXnUc6yNeLvdZuU/UQp0aT+DA8pt3BZzPzXthlOmIrRRqHBdLjMLPwN5GD29zR5bASXtQ==",
"optional": true,
"dependencies": {
"ws": "^8.16.0"
},
"engines": {
"node": ">=20"
}
},
"node_modules/gemini-cli-vscode-ide-companion": {
"resolved": "packages/vscode-ide-companion",
"link": true
@@ -9975,9 +10029,9 @@
},
"node_modules/ink": {
"name": "@jrichman/ink",
"version": "6.4.11",
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.11.tgz",
"integrity": "sha512-93LQlzT7vvZ1XJcmOMwN4s+6W334QegendeHOMnEJBlhnpIzr8bws6/aOEHG8ZCuVD/vNeeea5m1msHIdAY6ig==",
"version": "6.4.10",
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.10.tgz",
"integrity": "sha512-kjJqZFkGVm0QyJmga/L02rsFJroF1aP2bhXEGkpuuT7clB6/W+gxAbLNw7ZaJrG6T30DgqOT92Pu6C9mK1FWyg==",
"license": "MIT",
"dependencies": {
"@alcalzone/ansi-tokenize": "^0.2.1",
@@ -10058,6 +10112,24 @@
"react": ">=18.0.0"
}
},
"node_modules/ink-testing-library": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/ink-testing-library/-/ink-testing-library-4.0.0.tgz",
"integrity": "sha512-yF92kj3pmBvk7oKbSq5vEALO//o7Z9Ck/OaLNlkzXNeYdwfpxMQkSowGTFUCS5MSu9bWfSZMewGpp7bFc66D7Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@types/react": ">=18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/ink/node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
@@ -10432,18 +10504,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-network-error": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz",
"integrity": "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==",
"license": "MIT",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-node-process": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz",
@@ -12874,21 +12934,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-retry": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-7.1.1.tgz",
"integrity": "sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==",
"license": "MIT",
"dependencies": {
"is-network-error": "^1.1.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/package-json": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/package-json/-/package-json-10.0.1.tgz",
@@ -13428,9 +13473,9 @@
}
},
"node_modules/protobufjs": {
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
"integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
"version": "7.5.3",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.3.tgz",
"integrity": "sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -13502,6 +13547,7 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -13663,9 +13709,9 @@
}
},
"node_modules/react": {
"version": "19.2.4",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"version": "19.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -13704,26 +13750,6 @@
}
}
},
"node_modules/react-dom": {
"version": "19.2.4",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"scheduler": "^0.27.0"
},
"peerDependencies": {
"react": "^19.2.4"
}
},
"node_modules/react-dom/node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"dev": true,
"license": "MIT"
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -14145,6 +14171,81 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rimraf/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/rimraf/node_modules/glob": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^3.1.2",
"minimatch": "^9.0.4",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^1.11.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rimraf/node_modules/jackspeak": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
},
"optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/rimraf/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rimraf/node_modules/path-scurry": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
"license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^10.2.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
},
"engines": {
"node": ">=16 || 14 >=14.18"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rollup": {
"version": "4.53.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz",
@@ -15463,9 +15564,9 @@
}
},
"node_modules/tar": {
"version": "7.5.8",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.8.tgz",
"integrity": "sha512-SYkBtK99u0yXa+IWL0JRzzcl7RxNpvX/U08Z+8DKnysfno7M+uExnTZH8K+VGgShf2qFPKtbNr9QBl8n7WBP6Q==",
"version": "7.5.7",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz",
"integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
@@ -15612,6 +15713,43 @@
"balanced-match": "^1.0.0"
}
},
"node_modules/test-exclude/node_modules/glob": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"dev": true,
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^3.1.2",
"minimatch": "^9.0.4",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^1.11.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/test-exclude/node_modules/jackspeak": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
},
"optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/test-exclude/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
@@ -15628,6 +15766,23 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/test-exclude/node_modules/path-scurry": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^10.2.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
},
"engines": {
"node": ">=16 || 14 >=14.18"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/text-hex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
@@ -17107,7 +17262,7 @@
"express": "^5.1.0",
"fs-extra": "^11.3.0",
"strip-json-comments": "^3.1.1",
"tar": "^7.5.8",
"tar": "^7.5.2",
"uuid": "^13.0.0",
"winston": "^3.17.0"
},
@@ -17162,7 +17317,7 @@
"dependencies": {
"@agentclientprotocol/sdk": "^0.12.0",
"@google/gemini-cli-core": "file:../core",
"@google/genai": "1.41.0",
"@google/genai": "1.30.0",
"@iarna/toml": "^2.2.5",
"@modelcontextprotocol/sdk": "^1.23.0",
"ansi-escapes": "^7.3.0",
@@ -17170,7 +17325,6 @@
"chalk": "^4.1.2",
"cli-spinners": "^2.9.2",
"clipboardy": "^5.0.0",
"color-convert": "^2.0.1",
"command-exists": "^1.2.9",
"comment-json": "^4.2.5",
"diff": "^8.0.3",
@@ -17179,7 +17333,7 @@
"fzf": "^0.5.2",
"glob": "^12.0.0",
"highlight.js": "^11.11.1",
"ink": "npm:@jrichman/ink@6.4.11",
"ink": "npm:@jrichman/ink@6.4.10",
"ink-gradient": "^3.0.0",
"ink-spinner": "^5.0.0",
"latest-version": "^9.0.0",
@@ -17194,7 +17348,7 @@
"string-width": "^8.1.0",
"strip-ansi": "^7.1.0",
"strip-json-comments": "^3.1.1",
"tar": "^7.5.8",
"tar": "^7.5.2",
"tinygradient": "^1.1.5",
"undici": "^7.10.0",
"ws": "^8.16.0",
@@ -17214,7 +17368,7 @@
"@types/shell-quote": "^1.7.5",
"@types/ws": "^8.5.10",
"@types/yargs": "^17.0.32",
"@xterm/headless": "^5.5.0",
"ink-testing-library": "^4.0.0",
"typescript": "^5.3.3",
"vitest": "^3.1.1"
},
@@ -17222,29 +17376,6 @@
"node": ">=20"
}
},
"packages/cli/node_modules/@google/genai": {
"version": "1.41.0",
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.41.0.tgz",
"integrity": "sha512-S4WGil+PG0NBQRAx+0yrQuM/TWOLn2gGEy5wn4IsoOI6ouHad0P61p3OWdhJ3aqr9kfj8o904i/jevfaGoGuIQ==",
"license": "Apache-2.0",
"dependencies": {
"google-auth-library": "^10.3.0",
"p-retry": "^7.1.1",
"protobufjs": "^7.5.4",
"ws": "^8.18.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"@modelcontextprotocol/sdk": "^1.25.2"
},
"peerDependenciesMeta": {
"@modelcontextprotocol/sdk": {
"optional": true
}
}
},
"packages/cli/node_modules/ansi-escapes": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz",
@@ -17260,93 +17391,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"packages/cli/node_modules/gaxios": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz",
"integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==",
"license": "Apache-2.0",
"dependencies": {
"extend": "^3.0.2",
"https-proxy-agent": "^7.0.1",
"node-fetch": "^3.3.2",
"rimraf": "^5.0.1"
},
"engines": {
"node": ">=18"
}
},
"packages/cli/node_modules/gcp-metadata": {
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz",
"integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==",
"license": "Apache-2.0",
"dependencies": {
"gaxios": "^7.0.0",
"google-logging-utils": "^1.0.0",
"json-bigint": "^1.0.0"
},
"engines": {
"node": ">=18"
}
},
"packages/cli/node_modules/google-auth-library": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz",
"integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==",
"license": "Apache-2.0",
"dependencies": {
"base64-js": "^1.3.0",
"ecdsa-sig-formatter": "^1.0.11",
"gaxios": "^7.0.0",
"gcp-metadata": "^8.0.0",
"google-logging-utils": "^1.0.0",
"gtoken": "^8.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=18"
}
},
"packages/cli/node_modules/google-logging-utils": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz",
"integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==",
"license": "Apache-2.0",
"engines": {
"node": ">=14"
}
},
"packages/cli/node_modules/gtoken": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz",
"integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==",
"license": "MIT",
"dependencies": {
"gaxios": "^7.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=18"
}
},
"packages/cli/node_modules/node-fetch": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"license": "MIT",
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-fetch"
}
},
"packages/cli/node_modules/string-width": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz",
@@ -17372,7 +17416,7 @@
"@google-cloud/logging": "^11.2.1",
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.21.0",
"@google-cloud/opentelemetry-cloud-trace-exporter": "^3.0.0",
"@google/genai": "1.41.0",
"@google/genai": "1.30.0",
"@iarna/toml": "^2.2.5",
"@joshua.litt/get-ripgrep": "^0.0.3",
"@modelcontextprotocol/sdk": "^1.23.0",
@@ -17404,7 +17448,7 @@
"fdir": "^6.4.6",
"fzf": "^0.5.2",
"glob": "^12.0.0",
"google-auth-library": "^10.5.0",
"google-auth-library": "^9.11.0",
"html-to-text": "^9.0.5",
"https-proxy-agent": "^7.0.6",
"ignore": "^7.0.0",
@@ -17419,7 +17463,6 @@
"shell-quote": "^1.8.3",
"simple-git": "^3.28.0",
"strip-ansi": "^7.1.0",
"strip-json-comments": "^3.1.1",
"systeminformation": "^5.25.11",
"tree-sitter-bash": "^0.25.0",
"undici": "^7.10.0",
@@ -17451,29 +17494,6 @@
"node-pty": "^1.0.0"
}
},
"packages/core/node_modules/@google/genai": {
"version": "1.41.0",
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.41.0.tgz",
"integrity": "sha512-S4WGil+PG0NBQRAx+0yrQuM/TWOLn2gGEy5wn4IsoOI6ouHad0P61p3OWdhJ3aqr9kfj8o904i/jevfaGoGuIQ==",
"license": "Apache-2.0",
"dependencies": {
"google-auth-library": "^10.3.0",
"p-retry": "^7.1.1",
"protobufjs": "^7.5.4",
"ws": "^8.18.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"@modelcontextprotocol/sdk": "^1.25.2"
},
"peerDependenciesMeta": {
"@modelcontextprotocol/sdk": {
"optional": true
}
}
},
"packages/core/node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
@@ -17504,75 +17524,6 @@
}
}
},
"packages/core/node_modules/gaxios": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz",
"integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==",
"license": "Apache-2.0",
"dependencies": {
"extend": "^3.0.2",
"https-proxy-agent": "^7.0.1",
"node-fetch": "^3.3.2",
"rimraf": "^5.0.1"
},
"engines": {
"node": ">=18"
}
},
"packages/core/node_modules/gcp-metadata": {
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz",
"integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==",
"license": "Apache-2.0",
"dependencies": {
"gaxios": "^7.0.0",
"google-logging-utils": "^1.0.0",
"json-bigint": "^1.0.0"
},
"engines": {
"node": ">=18"
}
},
"packages/core/node_modules/google-auth-library": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz",
"integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==",
"license": "Apache-2.0",
"dependencies": {
"base64-js": "^1.3.0",
"ecdsa-sig-formatter": "^1.0.11",
"gaxios": "^7.0.0",
"gcp-metadata": "^8.0.0",
"google-logging-utils": "^1.0.0",
"gtoken": "^8.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=18"
}
},
"packages/core/node_modules/google-logging-utils": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz",
"integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==",
"license": "Apache-2.0",
"engines": {
"node": ">=14"
}
},
"packages/core/node_modules/gtoken": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz",
"integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==",
"license": "MIT",
"dependencies": {
"gaxios": "^7.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=18"
}
},
"packages/core/node_modules/ignore": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
@@ -17597,24 +17548,6 @@
"node": ">=16"
}
},
"packages/core/node_modules/node-fetch": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"license": "MIT",
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-fetch"
}
},
"packages/core/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
@@ -17640,21 +17573,6 @@
"uuid": "dist-node/bin/uuid"
}
},
"packages/devtools": {
"name": "@google/gemini-cli-devtools",
"version": "0.30.0-nightly.20260210.a2174751d",
"license": "Apache-2.0",
"dependencies": {
"ws": "^8.16.0"
},
"devDependencies": {
"react": "^19.2.0",
"react-dom": "^19.2.0"
},
"engines": {
"node": ">=20"
}
},
"packages/sdk": {
"name": "@google/gemini-cli-sdk",
"version": "0.29.0-nightly.20260203.71f46f116",
+6 -8
View File
@@ -37,7 +37,7 @@
"build:all": "npm run build && npm run build:sandbox && npm run build:vscode",
"build:packages": "npm run build --workspaces",
"build:sandbox": "node scripts/build_sandbox.js",
"bundle": "npm run generate && npm run build --workspace=@google/gemini-cli-devtools && node esbuild.config.js && node scripts/copy_bundle_assets.js",
"bundle": "npm run generate && node esbuild.config.js && node scripts/copy_bundle_assets.js",
"test": "npm run test --workspaces --if-present",
"test:ci": "npm run test:ci --workspaces --if-present && npm run test:scripts",
"test:scripts": "vitest run --config ./scripts/tests/vitest.config.ts",
@@ -64,13 +64,11 @@
"pre-commit": "node scripts/pre-commit.js"
},
"overrides": {
"ink": "npm:@jrichman/ink@6.4.11",
"ink": "npm:@jrichman/ink@6.4.10",
"wrap-ansi": "9.0.2",
"cliui": {
"wrap-ansi": "7.0.0"
},
"glob": "^12.0.0",
"node-domexception": "^1.0.0"
}
},
"bin": {
"gemini": "bundle/gemini.js"
@@ -109,6 +107,7 @@
"globals": "^16.0.0",
"google-artifactregistry-auth": "^3.4.0",
"husky": "^9.1.7",
"ink-testing-library": "^4.0.0",
"json": "^11.0.0",
"lint-staged": "^16.1.6",
"memfs": "^4.42.0",
@@ -118,7 +117,6 @@
"npm-run-all": "^4.1.5",
"prettier": "^3.5.3",
"react-devtools-core": "^6.1.2",
"react-dom": "^19.2.0",
"semver": "^7.7.2",
"strip-ansi": "^7.1.2",
"ts-prune": "^0.10.3",
@@ -128,10 +126,9 @@
"yargs": "^17.7.2"
},
"dependencies": {
"ink": "npm:@jrichman/ink@6.4.11",
"ink": "npm:@jrichman/ink@6.4.10",
"latest-version": "^9.0.0",
"proper-lockfile": "^4.1.2",
"punycode": "^2.3.1",
"simple-git": "^3.28.0"
},
"optionalDependencies": {
@@ -141,6 +138,7 @@
"@lydell/node-pty-linux-x64": "1.1.0",
"@lydell/node-pty-win32-arm64": "1.1.0",
"@lydell/node-pty-win32-x64": "1.1.0",
"gemini-cli-devtools": "^0.2.1",
"keytar": "^7.9.0",
"node-pty": "^1.0.0"
},
+1 -1
View File
@@ -31,7 +31,7 @@
"express": "^5.1.0",
"fs-extra": "^11.3.0",
"strip-json-comments": "^3.1.1",
"tar": "^7.5.8",
"tar": "^7.5.2",
"uuid": "^13.0.0",
"winston": "^3.17.0"
},
+5 -7
View File
@@ -14,21 +14,19 @@ import {
type Mock,
} from 'vitest';
import { Task } from './task.js';
import type {
ToolCall,
Config,
ToolCallRequestInfo,
GitService,
CompletedToolCall,
} from '@google/gemini-cli-core';
import {
GeminiEventType,
type Config,
type ToolCallRequestInfo,
type GitService,
type CompletedToolCall,
ApprovalMode,
ToolConfirmationOutcome,
} from '@google/gemini-cli-core';
import { createMockConfig } from '../utils/testing_utils.js';
import type { ExecutionEventBus, RequestContext } from '@a2a-js/sdk/server';
import { CoderAgentEvent } from '../types.js';
import type { ToolCall } from '@google/gemini-cli-core';
const mockProcessRestorableToolCalls = vi.hoisted(() => vi.fn());
+6 -7
View File
@@ -13,7 +13,6 @@ import {
getAllMCPServerStatuses,
MCPServerStatus,
isNodeError,
getErrorMessage,
parseAndFormatApiError,
safeLiteralReplace,
DEFAULT_GUI_EDITOR,
@@ -31,7 +30,8 @@ import {
EDIT_TOOL_NAMES,
processRestorableToolCalls,
} from '@google/gemini-cli-core';
import type { RequestContext, ExecutionEventBus } from '@a2a-js/sdk/server';
import type { RequestContext } from '@a2a-js/sdk/server';
import { type ExecutionEventBus } from '@a2a-js/sdk/server';
import type {
TaskStatusUpdateEvent,
TaskArtifactUpdateEvent,
@@ -728,17 +728,16 @@ export class Task {
// Block scope for lexical declaration
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const errorEvent = event as ServerGeminiErrorEvent; // Type assertion
const errorMessage = errorEvent.value?.error
? getErrorMessage(errorEvent.value.error)
: 'Unknown error from LLM stream';
const errorMessage =
errorEvent.value?.error.message ?? 'Unknown error from LLM stream';
logger.error(
'[Task] Received error event from LLM stream:',
errorMessage,
);
let errMessage = `Unknown error from LLM stream: ${JSON.stringify(event)}`;
if (errorEvent.value?.error) {
errMessage = parseAndFormatApiError(errorEvent.value.error);
if (errorEvent.value) {
errMessage = parseAndFormatApiError(errorEvent.value);
}
this.cancelPendingTools(`LLM stream error: ${errorMessage}`);
this.setTaskStateAndPublishUpdate(
@@ -69,7 +69,6 @@ export class RestoreCommand implements Command {
throw error;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const toolCallData = JSON.parse(data);
const ToolCallDataSchema = getToolCallDataSchema();
const parseResult = ToolCallDataSchema.safeParse(toolCallData);
+3 -5
View File
@@ -8,19 +8,17 @@ import * as fs from 'node:fs';
import * as path from 'node:path';
import * as dotenv from 'dotenv';
import type {
TelemetryTarget,
ConfigParameters,
ExtensionLoader,
} from '@google/gemini-cli-core';
import type { TelemetryTarget } from '@google/gemini-cli-core';
import {
AuthType,
Config,
type ConfigParameters,
FileDiscoveryService,
ApprovalMode,
loadServerHierarchicalMemory,
GEMINI_DIR,
DEFAULT_GEMINI_EMBEDDING_MODEL,
type ExtensionLoader,
startupProfiler,
PREVIEW_GEMINI_MODEL,
homedir,
+2 -3
View File
@@ -7,8 +7,8 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import type { MCPServerConfig } from '@google/gemini-cli-core';
import {
type MCPServerConfig,
debugLogger,
GEMINI_DIR,
getErrorMessage,
@@ -122,7 +122,6 @@ export function loadSettings(workspaceDir: string): Settings {
function resolveEnvVarsInString(value: string): string {
const envVarRegex = /\$(?:(\w+)|{([^}]+)})/g; // Find $VAR_NAME or ${VAR_NAME}
return value.replace(envVarRegex, (match, varName1, varName2) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const varName = varName1 || varName2;
if (process && process.env && typeof process.env[varName] === 'string') {
return process.env[varName];
@@ -147,7 +146,7 @@ function resolveEnvVarsInObject<T>(obj: T): T {
}
if (Array.isArray(obj)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-unsafe-return
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return obj.map((item) => resolveEnvVarsInObject(item)) as unknown as T;
}
+5 -4
View File
@@ -4,11 +4,12 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type {
Config,
ToolCallConfirmationDetails,
import type { Config } from '@google/gemini-cli-core';
import {
GeminiEventType,
ApprovalMode,
type ToolCallConfirmationDetails,
} from '@google/gemini-cli-core';
import { GeminiEventType, ApprovalMode } from '@google/gemini-cli-core';
import type {
TaskStatusUpdateEvent,
SendStreamingMessageSuccessResponse,
+3 -8
View File
@@ -7,8 +7,8 @@
import express from 'express';
import type { AgentCard, Message } from '@a2a-js/sdk';
import type { TaskStore } from '@a2a-js/sdk/server';
import {
type TaskStore,
DefaultRequestHandler,
InMemoryTaskStore,
DefaultExecutionEventBus,
@@ -25,12 +25,9 @@ import { loadConfig, loadEnvironment, setTargetDir } from '../config/config.js';
import { loadSettings } from '../config/settings.js';
import { loadExtensions } from '../config/extension.js';
import { commandRegistry } from '../commands/command-registry.js';
import {
debugLogger,
SimpleExtensionLoader,
GitService,
} from '@google/gemini-cli-core';
import { debugLogger, SimpleExtensionLoader } from '@google/gemini-cli-core';
import type { Command, CommandArgument } from '../commands/types.js';
import { GitService } from '@google/gemini-cli-core';
type CommandResponse = {
name: string;
@@ -91,7 +88,6 @@ async function handleExecuteCommand(
},
) {
logger.info('[CoreAgent] Received /executeCommand request: ', req.body);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { command, args } = req.body;
try {
if (typeof command !== 'string') {
@@ -215,7 +211,6 @@ export async function createApp() {
const agentSettings = req.body.agentSettings as
| AgentSettings
| undefined;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const contextId = req.body.contextId || uuidv4();
const wrapper = await agentExecutor.createTask(
taskId,
@@ -246,7 +246,6 @@ export class GCSTaskStore implements TaskStore {
}
const [compressedMetadata] = await metadataFile.download();
const jsonData = gunzipSync(compressedMetadata).toString();
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const loadedMetadata = JSON.parse(jsonData);
logger.info(`Task ${taskId} metadata loaded from GCS.`);
@@ -283,14 +282,12 @@ export class GCSTaskStore implements TaskStore {
return {
id: taskId,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
contextId: loadedMetadata._contextId || uuidv4(),
kind: 'task',
status: {
state: persistedState._taskState,
timestamp: new Date().toISOString(),
},
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
metadata: loadedMetadata,
history: [],
artifacts: [],
+1 -16
View File
@@ -36,21 +36,7 @@ process.on('uncaughtException', (error) => {
});
main().catch(async (error) => {
// Set a timeout to force exit if cleanup hangs
const cleanupTimeout = setTimeout(() => {
writeToStderr('Cleanup timed out, forcing exit...\n');
process.exit(1);
}, 5000);
try {
await runExitCleanup();
} catch (cleanupError) {
writeToStderr(
`Error during final cleanup: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}\n`,
);
} finally {
clearTimeout(cleanupTimeout);
}
await runExitCleanup();
if (error instanceof FatalError) {
let errorMessage = error.message;
@@ -60,7 +46,6 @@ main().catch(async (error) => {
writeToStderr(errorMessage + '\n');
process.exit(error.exitCode);
}
writeToStderr('An unexpected critical error occurred:');
if (error instanceof Error) {
writeToStderr(error.stack + '\n');
+4 -5
View File
@@ -31,7 +31,7 @@
"dependencies": {
"@agentclientprotocol/sdk": "^0.12.0",
"@google/gemini-cli-core": "file:../core",
"@google/genai": "1.41.0",
"@google/genai": "1.30.0",
"@iarna/toml": "^2.2.5",
"@modelcontextprotocol/sdk": "^1.23.0",
"ansi-escapes": "^7.3.0",
@@ -39,7 +39,6 @@
"chalk": "^4.1.2",
"cli-spinners": "^2.9.2",
"clipboardy": "^5.0.0",
"color-convert": "^2.0.1",
"command-exists": "^1.2.9",
"comment-json": "^4.2.5",
"diff": "^8.0.3",
@@ -48,7 +47,7 @@
"fzf": "^0.5.2",
"glob": "^12.0.0",
"highlight.js": "^11.11.1",
"ink": "npm:@jrichman/ink@6.4.11",
"ink": "npm:@jrichman/ink@6.4.10",
"ink-gradient": "^3.0.0",
"ink-spinner": "^5.0.0",
"latest-version": "^9.0.0",
@@ -63,7 +62,7 @@
"string-width": "^8.1.0",
"strip-ansi": "^7.1.0",
"strip-json-comments": "^3.1.1",
"tar": "^7.5.8",
"tar": "^7.5.2",
"tinygradient": "^1.1.5",
"undici": "^7.10.0",
"ws": "^8.16.0",
@@ -80,7 +79,7 @@
"@types/shell-quote": "^1.7.5",
"@types/ws": "^8.5.10",
"@types/yargs": "^17.0.32",
"@xterm/headless": "^5.5.0",
"ink-testing-library": "^4.0.0",
"typescript": "^5.3.3",
"vitest": "^3.1.1"
},
@@ -47,7 +47,6 @@ const defaultRequestConfirmation: RequestConfirmationCallback = async (
message,
initial: false,
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return response.confirm;
};
+12 -56
View File
@@ -21,11 +21,7 @@ import {
type MCPServerConfig,
} from '@google/gemini-cli-core';
import { loadCliConfig, parseArguments, type CliArgs } from './config.js';
import {
type Settings,
type MergedSettings,
createTestMergedSettings,
} from './settings.js';
import { type Settings, createTestMergedSettings } from './settings.js';
import * as ServerConfig from '@google/gemini-cli-core';
import { isWorkspaceTrusted } from './trustedFolders.js';
@@ -1348,36 +1344,6 @@ describe('Approval mode tool exclusion logic', () => {
'Invalid approval mode: invalid_mode. Valid values are: yolo, auto_edit, plan, default',
);
});
it('should fall back to default approval mode if plan mode is requested but not enabled', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
general: {
defaultApprovalMode: 'plan',
},
experimental: {
plan: false,
},
});
const argv = await parseArguments(settings);
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
});
it('should allow plan approval mode if experimental plan is enabled', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
general: {
defaultApprovalMode: 'plan',
},
experimental: {
plan: true,
},
});
const argv = await parseArguments(settings);
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getApprovalMode()).toBe(ApprovalMode.PLAN);
});
});
describe('loadCliConfig with allowed-mcp-server-names', () => {
@@ -2590,8 +2556,9 @@ describe('loadCliConfig approval mode', () => {
},
});
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
await expect(loadCliConfig(settings, 'test-session', argv)).rejects.toThrow(
'Approval mode "plan" is only available when experimental.plan is enabled.',
);
});
it('should throw error when --approval-mode=plan is used but experimental.plan setting is missing', async () => {
@@ -2599,23 +2566,9 @@ describe('loadCliConfig approval mode', () => {
const argv = await parseArguments(createTestMergedSettings());
const settings = createTestMergedSettings({});
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
});
it('should pass planSettings.directory from settings to config', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
general: {
plan: {
directory: '.custom-plans',
},
},
} as unknown as MergedSettings);
const argv = await parseArguments(settings);
const config = await loadCliConfig(settings, 'test-session', argv);
const plansDir = config.storage.getPlansDir();
expect(plansDir).toContain('.custom-plans');
await expect(loadCliConfig(settings, 'test-session', argv)).rejects.toThrow(
'Approval mode "plan" is only available when experimental.plan is enabled.',
);
});
// --- Untrusted Folder Scenarios ---
@@ -2725,8 +2678,11 @@ describe('loadCliConfig approval mode', () => {
experimental: { plan: false },
});
const argv = await parseArguments(settings);
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
await expect(
loadCliConfig(settings, 'test-session', argv),
).rejects.toThrow(
'Approval mode "plan" is only available when experimental.plan is enabled.',
);
});
});
});
+5 -20
View File
@@ -56,10 +56,7 @@ import { resolvePath } from '../utils/resolvePath.js';
import { RESUME_LATEST } from '../utils/sessionUtils.js';
import { isWorkspaceTrusted } from './trustedFolders.js';
import {
createPolicyEngineConfig,
resolveWorkspacePolicyState,
} from './policy.js';
import { createPolicyEngineConfig } from './policy.js';
import { ExtensionManager } from './extension-manager.js';
import { McpServerEnablementManager } from './mcp/mcpServerEnablement.js';
import type { ExtensionEvents } from '@google/gemini-cli-core/src/utils/extensionLoader.js';
@@ -558,13 +555,11 @@ export async function loadCliConfig(
break;
case 'plan':
if (!(settings.experimental?.plan ?? false)) {
debugLogger.warn(
'Approval mode "plan" is only available when experimental.plan is enabled. Falling back to "default".',
throw new Error(
'Approval mode "plan" is only available when experimental.plan is enabled.',
);
approvalMode = ApprovalMode.DEFAULT;
} else {
approvalMode = ApprovalMode.PLAN;
}
approvalMode = ApprovalMode.PLAN;
break;
case 'default':
approvalMode = ApprovalMode.DEFAULT;
@@ -695,17 +690,9 @@ export async function loadCliConfig(
policyPaths: argv.policy,
};
const { workspacePoliciesDir, policyUpdateConfirmationRequest } =
await resolveWorkspacePolicyState({
cwd,
trustedFolder,
interactive,
});
const policyEngineConfig = await createPolicyEngineConfig(
effectiveSettings,
approvalMode,
workspacePoliciesDir,
);
policyEngineConfig.nonInteractive = !interactive;
@@ -769,7 +756,6 @@ export async function loadCliConfig(
coreTools: settings.tools?.core || undefined,
allowedTools: allowedTools.length > 0 ? allowedTools : undefined,
policyEngineConfig,
policyUpdateConfirmationRequest,
excludeTools,
toolDiscoveryCommand: settings.tools?.discoveryCommand,
toolCallCommand: settings.tools?.callCommand,
@@ -826,12 +812,10 @@ export async function loadCliConfig(
enableExtensionReloading: settings.experimental?.extensionReloading,
enableAgents: settings.experimental?.enableAgents,
plan: settings.experimental?.plan,
planSettings: settings.general.plan,
enableEventDrivenScheduler: true,
skillsSupport: settings.skills?.enabled ?? true,
disabledSkills: settings.skills?.disabled,
experimentalJitContext: settings.experimental?.jitContext,
modelSteering: settings.experimental?.modelSteering,
toolOutputMasking: settings.experimental?.toolOutputMasking,
noBrowser: !!process.env['NO_BROWSER'],
summarizeToolOutput: settings.model?.summarizeToolOutput,
@@ -848,6 +832,7 @@ export async function loadCliConfig(
enableShellOutputEfficiency:
settings.tools?.shell?.enableShellOutputEfficiency ?? true,
skipNextSpeakerCheck: settings.model?.skipNextSpeakerCheck,
enablePromptCompletion: settings.general?.enablePromptCompletion,
truncateToolOutputThreshold: settings.tools?.truncateToolOutputThreshold,
eventEmitter: coreEvents,
useWriteTodos: argv.useWriteTodos ?? settings.useWriteTodos,
@@ -866,7 +866,6 @@ Would you like to attempt to install via "git clone" instead?`,
try {
const hooksContent = await fs.promises.readFile(hooksFilePath, 'utf-8');
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const rawHooks = JSON.parse(hooksContent);
if (
@@ -224,59 +224,4 @@ describe('ExtensionRegistryClient', () => {
'Failed to fetch extensions: Not Found',
);
});
it('should not return irrelevant results', async () => {
fetchMock.mockResolvedValue({
ok: true,
json: async () => [
...mockExtensions,
{
id: 'dataplex',
extensionName: 'dataplex',
extensionDescription: 'Connect to Dataplex Universal Catalog...',
fullName: 'google-cloud/dataplex',
rank: 6,
stars: 6,
url: '',
repoDescription: '',
lastUpdated: '',
extensionVersion: '1.0.0',
avatarUrl: '',
hasMCP: false,
hasContext: false,
isGoogleOwned: true,
licenseKey: '',
hasHooks: false,
hasCustomCommands: false,
hasSkills: false,
},
{
id: 'conductor',
extensionName: 'conductor',
extensionDescription: 'A conductor extension that actually matches.',
fullName: 'someone/conductor',
rank: 100,
stars: 100,
url: '',
repoDescription: '',
lastUpdated: '',
extensionVersion: '1.0.0',
avatarUrl: '',
hasMCP: false,
hasContext: false,
isGoogleOwned: false,
licenseKey: '',
hasHooks: false,
hasCustomCommands: false,
hasSkills: false,
},
],
});
const results = await client.searchExtensions('conductor');
const ids = results.map((r) => r.id);
expect(ids).not.toContain('dataplex');
expect(ids).toContain('conductor');
});
});
@@ -79,11 +79,9 @@ export class ExtensionRegistryClient {
const fzf = new AsyncFzf(allExtensions, {
selector: (ext: RegistryExtension) =>
`${ext.extensionName} ${ext.extensionDescription} ${ext.fullName}`,
fuzzy: true,
fuzzy: 'v2',
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const results = await fzf.find(query);
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return results.map((r: { item: RegistryExtension }) => r.item);
}
@@ -110,6 +108,7 @@ export class ExtensionRegistryClient {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return (await response.json()) as RegistryExtension[];
} catch (error) {
// Clear the promise on failure so that subsequent calls can try again
ExtensionRegistryClient.fetchPromise = null;
throw error;
}
@@ -179,7 +179,6 @@ export class ExtensionEnablementManager {
readConfig(): AllExtensionsEnablementConfig {
try {
const content = fs.readFileSync(this.configFilePath, 'utf-8');
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return JSON.parse(content);
} catch (error) {
if (
@@ -156,7 +156,6 @@ export async function promptForSetting(
name: 'value',
message: `${setting.name}\n${setting.description}`,
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return response.value;
}
@@ -269,7 +269,7 @@ describe('github.ts', () => {
it('should return NOT_UPDATABLE if local extension config cannot be loaded', async () => {
vi.mocked(mockExtensionManager.loadExtensionConfig).mockImplementation(
async () => {
() => {
throw new Error('Config not found');
},
);
@@ -58,7 +58,6 @@ export function recursivelyHydrateStrings<T>(
if (Array.isArray(obj)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return obj.map((item) =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
recursivelyHydrateStrings(item, values),
) as unknown as T;
}
+2 -6
View File
@@ -178,7 +178,6 @@ export const defaultKeyBindings: KeyBindingConfig = {
[Command.DELETE_WORD_FORWARD]: [
{ key: 'delete', ctrl: true },
{ key: 'delete', alt: true },
{ key: 'd', alt: true },
],
[Command.DELETE_CHAR_LEFT]: [{ key: 'backspace' }, { key: 'h', ctrl: true }],
[Command.DELETE_CHAR_RIGHT]: [{ key: 'delete' }, { key: 'd', ctrl: true }],
@@ -212,7 +211,7 @@ export const defaultKeyBindings: KeyBindingConfig = {
[Command.REVERSE_SEARCH]: [{ key: 'r', ctrl: true }],
[Command.REWIND]: [{ key: 'double escape' }],
[Command.SUBMIT_REVERSE_SEARCH]: [{ key: 'return', ctrl: false }],
[Command.ACCEPT_SUGGESTION_REVERSE_SEARCH]: [{ key: 'tab', shift: false }],
[Command.ACCEPT_SUGGESTION_REVERSE_SEARCH]: [{ key: 'tab' }],
// Navigation
[Command.NAVIGATION_UP]: [{ key: 'up', shift: false }],
@@ -231,10 +230,7 @@ export const defaultKeyBindings: KeyBindingConfig = {
[Command.DIALOG_PREV]: [{ key: 'tab', shift: true }],
// Suggestions & Completions
[Command.ACCEPT_SUGGESTION]: [
{ key: 'tab', shift: false },
{ key: 'return', ctrl: false },
],
[Command.ACCEPT_SUGGESTION]: [{ key: 'tab' }, { key: 'return', ctrl: false }],
[Command.COMPLETION_UP]: [
{ key: 'up', shift: false },
{ key: 'p', shift: false, ctrl: true },
@@ -148,13 +148,13 @@ describe('Policy Engine Integration Tests', () => {
);
const engine = new PolicyEngine(config);
// MCP server allowed (priority 3.1) provides general allow for server
// MCP server allowed (priority 3.1) provides general allow for server
// MCP server allowed (priority 2.1) provides general allow for server
// MCP server allowed (priority 2.1) provides general allow for server
expect(
(await engine.check({ name: 'my-server__safe-tool' }, undefined))
.decision,
).toBe(PolicyDecision.ALLOW);
// But specific tool exclude (priority 3.4) wins over server allow
// But specific tool exclude (priority 2.4) wins over server allow
expect(
(await engine.check({ name: 'my-server__dangerous-tool' }, undefined))
.decision,
@@ -339,8 +339,6 @@ describe('Policy Engine Integration Tests', () => {
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/session-1/plans/my-plan.md',
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/session-1/plans/feature_auth.md',
'/home/user/.gemini/tmp/new-temp_dir_123/session-1/plans/plan.md', // new style of temp directory
'C:\\Users\\user\\.gemini\\tmp\\project-id\\session-id\\plans\\plan.md',
'D:\\gemini-cli\\.gemini\\tmp\\project-id\\session-1\\plans\\plan.md', // no session ID
];
for (const file_path of validPaths) {
@@ -366,8 +364,7 @@ describe('Policy Engine Integration Tests', () => {
const invalidPaths = [
'/project/src/file.ts', // Workspace
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/script.js', // Wrong extension
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/../../../etc/passwd.md', // Path traversal (Unix)
'C:\\Users\\user\\.gemini\\tmp\\id\\session\\plans\\..\\..\\..\\Windows\\System32\\config\\SAM', // Path traversal (Windows)
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/../../../etc/passwd.md', // Path traversal
'/home/user/.gemini/non-tmp/new-temp_dir_123/plans/plan.md', // outside of temp dir
];
@@ -415,25 +412,25 @@ describe('Policy Engine Integration Tests', () => {
// Find rules and verify their priorities
const blockedToolRule = rules.find((r) => r.toolName === 'blocked-tool');
expect(blockedToolRule?.priority).toBe(3.4); // Command line exclude
expect(blockedToolRule?.priority).toBe(2.4); // Command line exclude
const blockedServerRule = rules.find(
(r) => r.toolName === 'blocked-server__*',
);
expect(blockedServerRule?.priority).toBe(3.9); // MCP server exclude
expect(blockedServerRule?.priority).toBe(2.9); // MCP server exclude
const specificToolRule = rules.find(
(r) => r.toolName === 'specific-tool',
);
expect(specificToolRule?.priority).toBe(3.3); // Command line allow
expect(specificToolRule?.priority).toBe(2.3); // Command line allow
const trustedServerRule = rules.find(
(r) => r.toolName === 'trusted-server__*',
);
expect(trustedServerRule?.priority).toBe(3.2); // MCP trusted server
expect(trustedServerRule?.priority).toBe(2.2); // MCP trusted server
const mcpServerRule = rules.find((r) => r.toolName === 'mcp-server__*');
expect(mcpServerRule?.priority).toBe(3.1); // MCP allowed server
expect(mcpServerRule?.priority).toBe(2.1); // MCP allowed server
const readOnlyToolRule = rules.find((r) => r.toolName === 'glob');
// Priority 70 in default tier → 1.07 (Overriding Plan Mode Deny)
@@ -580,16 +577,16 @@ describe('Policy Engine Integration Tests', () => {
// Verify each rule has the expected priority
const tool3Rule = rules.find((r) => r.toolName === 'tool3');
expect(tool3Rule?.priority).toBe(3.4); // Excluded tools (user tier)
expect(tool3Rule?.priority).toBe(2.4); // Excluded tools (user tier)
const server2Rule = rules.find((r) => r.toolName === 'server2__*');
expect(server2Rule?.priority).toBe(3.9); // Excluded servers (user tier)
expect(server2Rule?.priority).toBe(2.9); // Excluded servers (user tier)
const tool1Rule = rules.find((r) => r.toolName === 'tool1');
expect(tool1Rule?.priority).toBe(3.3); // Allowed tools (user tier)
expect(tool1Rule?.priority).toBe(2.3); // Allowed tools (user tier)
const server1Rule = rules.find((r) => r.toolName === 'server1__*');
expect(server1Rule?.priority).toBe(3.1); // Allowed servers (user tier)
expect(server1Rule?.priority).toBe(2.1); // Allowed servers (user tier)
const globRule = rules.find((r) => r.toolName === 'glob');
// Priority 70 in default tier → 1.07
-145
View File
@@ -1,145 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import { resolveWorkspacePolicyState } from './policy.js';
import { writeToStderr } from '@google/gemini-cli-core';
// Mock debugLogger to avoid noise in test output
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
debugLogger: {
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
},
writeToStderr: vi.fn(),
};
});
describe('resolveWorkspacePolicyState', () => {
let tempDir: string;
let workspaceDir: string;
let policiesDir: string;
beforeEach(() => {
// Create a temporary directory for the test
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gemini-cli-test-'));
// Redirect GEMINI_CLI_HOME to the temp directory to isolate integrity storage
vi.stubEnv('GEMINI_CLI_HOME', tempDir);
workspaceDir = path.join(tempDir, 'workspace');
fs.mkdirSync(workspaceDir);
policiesDir = path.join(workspaceDir, '.gemini', 'policies');
vi.clearAllMocks();
});
afterEach(() => {
// Clean up temporary directory
fs.rmSync(tempDir, { recursive: true, force: true });
vi.unstubAllEnvs();
});
it('should return empty state if folder is not trusted', async () => {
const result = await resolveWorkspacePolicyState({
cwd: workspaceDir,
trustedFolder: false,
interactive: true,
});
expect(result).toEqual({
workspacePoliciesDir: undefined,
policyUpdateConfirmationRequest: undefined,
});
});
it('should return policy directory if integrity matches', async () => {
// Set up policies directory with a file
fs.mkdirSync(policiesDir, { recursive: true });
fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []');
// First call to establish integrity (interactive accept)
const firstResult = await resolveWorkspacePolicyState({
cwd: workspaceDir,
trustedFolder: true,
interactive: true,
});
expect(firstResult.policyUpdateConfirmationRequest).toBeDefined();
// Establish integrity manually as if accepted
const { PolicyIntegrityManager } = await import('@google/gemini-cli-core');
const integrityManager = new PolicyIntegrityManager();
await integrityManager.acceptIntegrity(
'workspace',
workspaceDir,
firstResult.policyUpdateConfirmationRequest!.newHash,
);
// Second call should match
const result = await resolveWorkspacePolicyState({
cwd: workspaceDir,
trustedFolder: true,
interactive: true,
});
expect(result.workspacePoliciesDir).toBe(policiesDir);
expect(result.policyUpdateConfirmationRequest).toBeUndefined();
});
it('should return undefined if integrity is NEW but fileCount is 0', async () => {
const result = await resolveWorkspacePolicyState({
cwd: workspaceDir,
trustedFolder: true,
interactive: true,
});
expect(result.workspacePoliciesDir).toBeUndefined();
expect(result.policyUpdateConfirmationRequest).toBeUndefined();
});
it('should return confirmation request if changed in interactive mode', async () => {
fs.mkdirSync(policiesDir, { recursive: true });
fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []');
const result = await resolveWorkspacePolicyState({
cwd: workspaceDir,
trustedFolder: true,
interactive: true,
});
expect(result.workspacePoliciesDir).toBeUndefined();
expect(result.policyUpdateConfirmationRequest).toEqual({
scope: 'workspace',
identifier: workspaceDir,
policyDir: policiesDir,
newHash: expect.any(String),
});
});
it('should warn and auto-accept if changed in non-interactive mode', async () => {
fs.mkdirSync(policiesDir, { recursive: true });
fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []');
const result = await resolveWorkspacePolicyState({
cwd: workspaceDir,
trustedFolder: true,
interactive: false,
});
expect(result.workspacePoliciesDir).toBe(policiesDir);
expect(result.policyUpdateConfirmationRequest).toBeUndefined();
expect(writeToStderr).toHaveBeenCalledWith(
expect.stringContaining('Automatically accepting and loading'),
);
});
});
+1 -74
View File
@@ -12,18 +12,12 @@ import {
type PolicySettings,
createPolicyEngineConfig as createCorePolicyEngineConfig,
createPolicyUpdater as createCorePolicyUpdater,
PolicyIntegrityManager,
IntegrityStatus,
Storage,
type PolicyUpdateConfirmationRequest,
writeToStderr,
} from '@google/gemini-cli-core';
import { type Settings } from './settings.js';
export async function createPolicyEngineConfig(
settings: Settings,
approvalMode: ApprovalMode,
workspacePoliciesDir?: string,
): Promise<PolicyEngineConfig> {
// Explicitly construct PolicySettings from Settings to ensure type safety
// and avoid accidental leakage of other settings properties.
@@ -32,7 +26,6 @@ export async function createPolicyEngineConfig(
tools: settings.tools,
mcpServers: settings.mcpServers,
policyPaths: settings.policyPaths,
workspacePoliciesDir,
};
return createCorePolicyEngineConfig(policySettings, approvalMode);
@@ -41,72 +34,6 @@ export async function createPolicyEngineConfig(
export function createPolicyUpdater(
policyEngine: PolicyEngine,
messageBus: MessageBus,
storage: Storage,
) {
return createCorePolicyUpdater(policyEngine, messageBus, storage);
}
export interface WorkspacePolicyState {
workspacePoliciesDir?: string;
policyUpdateConfirmationRequest?: PolicyUpdateConfirmationRequest;
}
/**
* Resolves the workspace policy state by checking folder trust and policy integrity.
*/
export async function resolveWorkspacePolicyState(options: {
cwd: string;
trustedFolder: boolean;
interactive: boolean;
}): Promise<WorkspacePolicyState> {
const { cwd, trustedFolder, interactive } = options;
let workspacePoliciesDir: string | undefined;
let policyUpdateConfirmationRequest:
| PolicyUpdateConfirmationRequest
| undefined;
if (trustedFolder) {
const potentialWorkspacePoliciesDir = new Storage(
cwd,
).getWorkspacePoliciesDir();
const integrityManager = new PolicyIntegrityManager();
const integrityResult = await integrityManager.checkIntegrity(
'workspace',
cwd,
potentialWorkspacePoliciesDir,
);
if (integrityResult.status === IntegrityStatus.MATCH) {
workspacePoliciesDir = potentialWorkspacePoliciesDir;
} else if (
integrityResult.status === IntegrityStatus.NEW &&
integrityResult.fileCount === 0
) {
// No workspace policies found
workspacePoliciesDir = undefined;
} else if (interactive) {
// Policies changed or are new, and we are in interactive mode
policyUpdateConfirmationRequest = {
scope: 'workspace',
identifier: cwd,
policyDir: potentialWorkspacePoliciesDir,
newHash: integrityResult.hash,
};
} else {
// Non-interactive mode: warn and automatically accept/load
await integrityManager.acceptIntegrity(
'workspace',
cwd,
integrityResult.hash,
);
workspacePoliciesDir = potentialWorkspacePoliciesDir;
// debugLogger.warn here doesn't show up in the terminal. It is showing up only in debug mode on the debug console
writeToStderr(
'WARNING: Workspace policies changed or are new. Automatically accepting and loading them in non-interactive mode.\n',
);
}
}
return { workspacePoliciesDir, policyUpdateConfirmationRequest };
return createCorePolicyUpdater(policyEngine, messageBus);
}
-79
View File
@@ -2032,85 +2032,6 @@ describe('Settings Loading and Merging', () => {
}),
}),
);
// Check that enableLoadingPhrases: false was further migrated to loadingPhrases: 'off'
expect(setValueSpy).toHaveBeenCalledWith(
SettingScope.User,
'ui',
expect.objectContaining({
loadingPhrases: 'off',
}),
);
});
it('should migrate enableLoadingPhrases: false to loadingPhrases: off', () => {
const userSettingsContent = {
ui: {
accessibility: {
enableLoadingPhrases: false,
},
},
};
const loadedSettings = createMockSettings(userSettingsContent);
const setValueSpy = vi.spyOn(loadedSettings, 'setValue');
migrateDeprecatedSettings(loadedSettings);
expect(setValueSpy).toHaveBeenCalledWith(
SettingScope.User,
'ui',
expect.objectContaining({
loadingPhrases: 'off',
}),
);
});
it('should not migrate enableLoadingPhrases: true to loadingPhrases', () => {
const userSettingsContent = {
ui: {
accessibility: {
enableLoadingPhrases: true,
},
},
};
const loadedSettings = createMockSettings(userSettingsContent);
const setValueSpy = vi.spyOn(loadedSettings, 'setValue');
migrateDeprecatedSettings(loadedSettings);
// Should not set loadingPhrases when enableLoadingPhrases is true
const uiCalls = setValueSpy.mock.calls.filter((call) => call[1] === 'ui');
for (const call of uiCalls) {
const uiValue = call[2] as Record<string, unknown>;
expect(uiValue).not.toHaveProperty('loadingPhrases');
}
});
it('should not overwrite existing loadingPhrases during migration', () => {
const userSettingsContent = {
ui: {
loadingPhrases: 'witty',
accessibility: {
enableLoadingPhrases: false,
},
},
};
const loadedSettings = createMockSettings(userSettingsContent);
const setValueSpy = vi.spyOn(loadedSettings, 'setValue');
migrateDeprecatedSettings(loadedSettings);
// Should not overwrite existing loadingPhrases
const uiCalls = setValueSpy.mock.calls.filter((call) => call[1] === 'ui');
for (const call of uiCalls) {
const uiValue = call[2] as Record<string, unknown>;
if (uiValue['loadingPhrases'] !== undefined) {
expect(uiValue['loadingPhrases']).toBe('witty');
}
}
});
it('should prioritize new settings over deprecated ones and respect removeDeprecated flag', () => {
-19
View File
@@ -165,10 +165,7 @@ export interface SummarizeToolOutputSettings {
tokenBudget?: number;
}
export type LoadingPhrasesMode = 'tips' | 'witty' | 'all' | 'off';
export interface AccessibilitySettings {
/** @deprecated Use ui.loadingPhrases instead. */
enableLoadingPhrases?: boolean;
screenReader?: boolean;
}
@@ -931,22 +928,6 @@ export function migrateDeprecatedSettings(
anyModified = true;
}
}
// Migrate enableLoadingPhrases: false → loadingPhrases: 'off'
const enableLP = newAccessibility['enableLoadingPhrases'];
if (
typeof enableLP === 'boolean' &&
newUi['loadingPhrases'] === undefined
) {
if (!enableLP) {
newUi['loadingPhrases'] = 'off';
loadedSettings.setValue(scope, 'ui', newUi);
if (!settingsFile.readOnly) {
anyModified = true;
}
}
foundDeprecated.push('ui.accessibility.enableLoadingPhrases');
}
}
}
@@ -83,19 +83,6 @@ describe('SettingsSchema', () => {
).toBe('boolean');
});
it('should have loadingPhrases enum property', () => {
const definition = getSettingsSchema().ui?.properties?.loadingPhrases;
expect(definition).toBeDefined();
expect(definition?.type).toBe('enum');
expect(definition?.default).toBe('tips');
expect(definition?.options?.map((o) => o.value)).toEqual([
'tips',
'witty',
'all',
'off',
]);
});
it('should have checkpointing nested properties', () => {
expect(
getSettingsSchema().general?.properties?.checkpointing.properties
@@ -107,16 +94,6 @@ describe('SettingsSchema', () => {
).toBe('boolean');
});
it('should have plan nested properties', () => {
expect(
getSettingsSchema().general?.properties?.plan?.properties?.directory,
).toBeDefined();
expect(
getSettingsSchema().general?.properties?.plan?.properties?.directory
.type,
).toBe('string');
});
it('should have fileFiltering nested properties', () => {
expect(
getSettingsSchema().context.properties.fileFiltering.properties
@@ -376,17 +353,6 @@ describe('SettingsSchema', () => {
).toBe('Show the "? for shortcuts" hint above the input.');
});
it('should have enableNotifications setting in schema', () => {
const setting =
getSettingsSchema().general.properties.enableNotifications;
expect(setting).toBeDefined();
expect(setting.type).toBe('boolean');
expect(setting.category).toBe('General');
expect(setting.default).toBe(false);
expect(setting.requiresRestart).toBe(false);
expect(setting.showInDialog).toBe(true);
});
it('should have enableAgents setting in schema', () => {
const setting = getSettingsSchema().experimental.properties.enableAgents;
expect(setting).toBeDefined();
+10 -78
View File
@@ -236,16 +236,6 @@ const SETTINGS_SCHEMA = {
description: 'Enable update notification prompts.',
showInDialog: false,
},
enableNotifications: {
type: 'boolean',
label: 'Enable Notifications',
category: 'General',
requiresRestart: false,
default: false,
description:
'Enable run-event notifications for action-required prompts and session completion. Currently macOS only.',
showInDialog: true,
},
checkpointing: {
type: 'object',
label: 'Checkpointing',
@@ -266,26 +256,15 @@ const SETTINGS_SCHEMA = {
},
},
},
plan: {
type: 'object',
label: 'Plan',
enablePromptCompletion: {
type: 'boolean',
label: 'Enable Prompt Completion',
category: 'General',
requiresRestart: true,
default: {},
description: 'Planning features configuration.',
showInDialog: false,
properties: {
directory: {
type: 'string',
label: 'Plan Directory',
category: 'General',
requiresRestart: true,
default: undefined as string | undefined,
description:
'The directory where planning artifacts are stored. If not specified, defaults to the system temporary directory.',
showInDialog: true,
},
},
default: false,
description:
'Enable AI-powered prompt completion suggestions while typing.',
showInDialog: true,
},
retryFetchErrors: {
type: 'boolean',
@@ -496,15 +475,6 @@ const SETTINGS_SCHEMA = {
'Show a warning when running Gemini CLI in the home directory.',
showInDialog: true,
},
showCompatibilityWarnings: {
type: 'boolean',
label: 'Show Compatibility Warnings',
category: 'UI',
requiresRestart: true,
default: true,
description: 'Show warnings about terminal or OS compatibility issues.',
showInDialog: true,
},
hideTips: {
type: 'boolean',
label: 'Hide Tips',
@@ -683,22 +653,6 @@ const SETTINGS_SCHEMA = {
description: 'Show the spinner during operations.',
showInDialog: true,
},
loadingPhrases: {
type: 'enum',
label: 'Loading Phrases',
category: 'UI',
requiresRestart: false,
default: 'tips',
description:
'What to show while the model is working: tips, witty comments, both, or nothing.',
showInDialog: true,
options: [
{ value: 'tips', label: 'Tips' },
{ value: 'witty', label: 'Witty' },
{ value: 'all', label: 'All' },
{ value: 'off', label: 'Off' },
],
},
customWittyPhrases: {
type: 'array',
label: 'Custom Witty Phrases',
@@ -727,9 +681,8 @@ const SETTINGS_SCHEMA = {
category: 'UI',
requiresRestart: true,
default: true,
description:
'@deprecated Use ui.loadingPhrases instead. Enable loading phrases during operations.',
showInDialog: false,
description: 'Enable loading phrases during operations.',
showInDialog: true,
},
screenReader: {
type: 'boolean',
@@ -1324,7 +1277,6 @@ const SETTINGS_SCHEMA = {
},
},
},
useWriteTodos: {
type: 'boolean',
label: 'Use WriteTodos',
@@ -1661,17 +1613,7 @@ const SETTINGS_SCHEMA = {
requiresRestart: false,
default: false,
description:
'Use OSC 52 for pasting. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it).',
showInDialog: true,
},
useOSC52Copy: {
type: 'boolean',
label: 'Use OSC 52 Copy',
category: 'Experimental',
requiresRestart: false,
default: false,
description:
'Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it).',
'Use OSC 52 sequence for pasting instead of clipboardy (useful for remote sessions).',
showInDialog: true,
},
plan: {
@@ -1683,16 +1625,6 @@ const SETTINGS_SCHEMA = {
description: 'Enable planning features (Plan Mode and tools).',
showInDialog: true,
},
modelSteering: {
type: 'boolean',
label: 'Model Steering',
category: 'Experimental',
requiresRestart: false,
default: false,
description:
'Enable model steering (user hints) to guide the model during tool execution.',
showInDialog: true,
},
},
},
@@ -131,6 +131,7 @@ describe('Settings Repro', () => {
},
general: {
debugKeystrokeLogging: false,
enablePromptCompletion: false,
preferredEditor: 'vim',
vimMode: false,
},
+12 -20
View File
@@ -491,33 +491,25 @@ describe('Trusted Folders', () => {
});
});
const itif = (condition: boolean) => (condition ? it : it.skip);
describe('Symlinks Support', () => {
const mockSettings: Settings = {
security: { folderTrust: { enabled: true } },
};
// TODO: issue 19387 - Enable symlink tests on Windows
itif(process.platform !== 'win32')(
'should trust a folder if the rule matches the realpath',
() => {
// Create a real directory and a symlink
const realDir = path.join(tempDir, 'real');
const symlinkDir = path.join(tempDir, 'symlink');
fs.mkdirSync(realDir);
fs.symlinkSync(realDir, symlinkDir);
it('should trust a folder if the rule matches the realpath', () => {
// Create a real directory and a symlink
const realDir = path.join(tempDir, 'real');
const symlinkDir = path.join(tempDir, 'symlink');
fs.mkdirSync(realDir);
fs.symlinkSync(realDir, symlinkDir);
// Rule uses realpath
const config = { [realDir]: TrustLevel.TRUST_FOLDER };
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
// Rule uses realpath
const config = { [realDir]: TrustLevel.TRUST_FOLDER };
fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
// Check against symlink path
expect(isWorkspaceTrusted(mockSettings, symlinkDir).isTrusted).toBe(
true,
);
},
);
// Check against symlink path
expect(isWorkspaceTrusted(mockSettings, symlinkDir).isTrusted).toBe(true);
});
});
describe('Verification: Auth and Trust Interaction', () => {
@@ -1,239 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import * as path from 'node:path';
import { loadCliConfig, type CliArgs } from './config.js';
import { createTestMergedSettings } from './settings.js';
import * as ServerConfig from '@google/gemini-cli-core';
import { isWorkspaceTrusted } from './trustedFolders.js';
// Mock dependencies
vi.mock('./trustedFolders.js', () => ({
isWorkspaceTrusted: vi.fn(),
}));
const mockCheckIntegrity = vi.fn();
const mockAcceptIntegrity = vi.fn();
vi.mock('@google/gemini-cli-core', async () => {
const actual = await vi.importActual<typeof ServerConfig>(
'@google/gemini-cli-core',
);
return {
...actual,
loadServerHierarchicalMemory: vi.fn().mockResolvedValue({
memoryContent: '',
fileCount: 0,
filePaths: [],
}),
createPolicyEngineConfig: vi.fn().mockResolvedValue({
rules: [],
checkers: [],
}),
getVersion: vi.fn().mockResolvedValue('test-version'),
PolicyIntegrityManager: vi.fn().mockImplementation(() => ({
checkIntegrity: mockCheckIntegrity,
acceptIntegrity: mockAcceptIntegrity,
})),
IntegrityStatus: { MATCH: 'match', NEW: 'new', MISMATCH: 'mismatch' },
debugLogger: {
warn: vi.fn(),
error: vi.fn(),
},
isHeadlessMode: vi.fn().mockReturnValue(false), // Default to interactive
};
});
describe('Workspace-Level Policy CLI Integration', () => {
const MOCK_CWD = process.cwd();
beforeEach(() => {
vi.clearAllMocks();
// Default to MATCH for existing tests
mockCheckIntegrity.mockResolvedValue({
status: 'match',
hash: 'test-hash',
fileCount: 1,
});
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(false);
});
it('should have getWorkspacePoliciesDir on Storage class', () => {
const storage = new ServerConfig.Storage(MOCK_CWD);
expect(storage.getWorkspacePoliciesDir).toBeDefined();
expect(typeof storage.getWorkspacePoliciesDir).toBe('function');
});
it('should pass workspacePoliciesDir to createPolicyEngineConfig when folder is trusted', async () => {
vi.mocked(isWorkspaceTrusted).mockReturnValue({
isTrusted: true,
source: 'file',
});
const settings = createTestMergedSettings();
const argv = { query: 'test' } as unknown as CliArgs;
await loadCliConfig(settings, 'test-session', argv, { cwd: MOCK_CWD });
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
workspacePoliciesDir: expect.stringContaining(
path.join('.gemini', 'policies'),
),
}),
expect.anything(),
);
});
it('should NOT pass workspacePoliciesDir to createPolicyEngineConfig when folder is NOT trusted', async () => {
vi.mocked(isWorkspaceTrusted).mockReturnValue({
isTrusted: false,
source: 'file',
});
const settings = createTestMergedSettings();
const argv = { query: 'test' } as unknown as CliArgs;
await loadCliConfig(settings, 'test-session', argv, { cwd: MOCK_CWD });
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
workspacePoliciesDir: undefined,
}),
expect.anything(),
);
});
it('should NOT pass workspacePoliciesDir if integrity is NEW but fileCount is 0', async () => {
vi.mocked(isWorkspaceTrusted).mockReturnValue({
isTrusted: true,
source: 'file',
});
mockCheckIntegrity.mockResolvedValue({
status: 'new',
hash: 'hash',
fileCount: 0,
});
const settings = createTestMergedSettings();
const argv = { query: 'test' } as unknown as CliArgs;
await loadCliConfig(settings, 'test-session', argv, { cwd: MOCK_CWD });
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
workspacePoliciesDir: undefined,
}),
expect.anything(),
);
});
it('should automatically accept and load workspacePoliciesDir if integrity MISMATCH in non-interactive mode', async () => {
vi.mocked(isWorkspaceTrusted).mockReturnValue({
isTrusted: true,
source: 'file',
});
mockCheckIntegrity.mockResolvedValue({
status: 'mismatch',
hash: 'new-hash',
fileCount: 1,
});
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(true); // Non-interactive
const settings = createTestMergedSettings();
const argv = { prompt: 'do something' } as unknown as CliArgs;
await loadCliConfig(settings, 'test-session', argv, { cwd: MOCK_CWD });
expect(mockAcceptIntegrity).toHaveBeenCalledWith(
'workspace',
MOCK_CWD,
'new-hash',
);
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
workspacePoliciesDir: expect.stringContaining(
path.join('.gemini', 'policies'),
),
}),
expect.anything(),
);
});
it('should set policyUpdateConfirmationRequest if integrity MISMATCH in interactive mode', async () => {
vi.mocked(isWorkspaceTrusted).mockReturnValue({
isTrusted: true,
source: 'file',
});
mockCheckIntegrity.mockResolvedValue({
status: 'mismatch',
hash: 'new-hash',
fileCount: 1,
});
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(false); // Interactive
const settings = createTestMergedSettings();
const argv = {
query: 'test',
promptInteractive: 'test',
} as unknown as CliArgs;
const config = await loadCliConfig(settings, 'test-session', argv, {
cwd: MOCK_CWD,
});
expect(config.getPolicyUpdateConfirmationRequest()).toEqual({
scope: 'workspace',
identifier: MOCK_CWD,
policyDir: expect.stringContaining(path.join('.gemini', 'policies')),
newHash: 'new-hash',
});
// In interactive mode without accept flag, it waits for user confirmation (handled by UI),
// so it currently DOES NOT pass the directory to createPolicyEngineConfig yet.
// The UI will handle the confirmation and reload/update.
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
workspacePoliciesDir: undefined,
}),
expect.anything(),
);
});
it('should set policyUpdateConfirmationRequest if integrity is NEW with files (first time seen) in interactive mode', async () => {
vi.mocked(isWorkspaceTrusted).mockReturnValue({
isTrusted: true,
source: 'file',
});
mockCheckIntegrity.mockResolvedValue({
status: 'new',
hash: 'new-hash',
fileCount: 5,
});
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(false); // Interactive
const settings = createTestMergedSettings();
const argv = { query: 'test' } as unknown as CliArgs;
const config = await loadCliConfig(settings, 'test-session', argv, {
cwd: MOCK_CWD,
});
expect(config.getPolicyUpdateConfirmationRequest()).toEqual({
scope: 'workspace',
identifier: MOCK_CWD,
policyDir: expect.stringContaining(path.join('.gemini', 'policies')),
newHash: 'new-hash',
});
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
workspacePoliciesDir: undefined,
}),
expect.anything(),
);
});
});
+2 -24
View File
@@ -38,8 +38,6 @@ import { appEvents, AppEvent } from './utils/events.js';
import {
type Config,
type ResumedSessionData,
type StartupWarning,
WarningPriority,
debugLogger,
coreEvents,
AuthType,
@@ -58,20 +56,6 @@ vi.mock('./nonInteractiveCli.js', () => ({
runNonInteractive: runNonInteractiveSpy,
}));
const terminalNotificationMocks = vi.hoisted(() => ({
notifyViaTerminal: vi.fn().mockResolvedValue(true),
buildRunEventNotificationContent: vi.fn(() => ({
title: 'Session complete',
body: 'done',
subtitle: 'Run finished',
})),
}));
vi.mock('./utils/terminalNotifications.js', () => ({
notifyViaTerminal: terminalNotificationMocks.notifyViaTerminal,
buildRunEventNotificationContent:
terminalNotificationMocks.buildRunEventNotificationContent,
}));
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
@@ -853,10 +837,6 @@ describe('gemini.tsx main function kitty protocol', () => {
expect(runNonInteractive).toHaveBeenCalled();
const callArgs = vi.mocked(runNonInteractive).mock.calls[0][0];
expect(callArgs.input).toBe('stdin-data\n\ntest-question');
expect(
terminalNotificationMocks.buildRunEventNotificationContent,
).not.toHaveBeenCalled();
expect(terminalNotificationMocks.notifyViaTerminal).not.toHaveBeenCalled();
expect(processExitSpy).toHaveBeenCalledWith(0);
processExitSpy.mockRestore();
});
@@ -1195,9 +1175,7 @@ describe('startInteractiveUI', () => {
},
},
} as LoadedSettings;
const mockStartupWarnings: StartupWarning[] = [
{ id: 'w1', message: 'warning1', priority: WarningPriority.High },
];
const mockStartupWarnings = ['warning1'];
const mockWorkspaceRoot = '/root';
const mockInitializationResult = {
authError: null,
@@ -1230,7 +1208,7 @@ describe('startInteractiveUI', () => {
async function startTestInteractiveUI(
config: Config,
settings: LoadedSettings,
startupWarnings: StartupWarning[],
startupWarnings: string[],
workspaceRoot: string,
resumedSessionData: ResumedSessionData | undefined,
initializationResult: InitializationResult,
+16 -33
View File
@@ -11,7 +11,6 @@ import { loadCliConfig, parseArguments } from './config/config.js';
import * as cliConfig from './config/config.js';
import { readStdin } from './utils/readStdin.js';
import { basename } from 'node:path';
import { createHash } from 'node:crypto';
import v8 from 'node:v8';
import os from 'node:os';
import dns from 'node:dns';
@@ -38,8 +37,6 @@ import {
cleanupExpiredSessions,
} from './utils/sessionCleanup.js';
import {
type StartupWarning,
WarningPriority,
type Config,
type ResumedSessionData,
type OutputPayload,
@@ -102,7 +99,6 @@ import { createPolicyUpdater } from './config/policy.js';
import { ScrollProvider } from './ui/contexts/ScrollProvider.js';
import { isAlternateBufferEnabled } from './ui/hooks/useAlternateBuffer.js';
import { TerminalProvider } from './ui/contexts/TerminalContext.js';
import { OverflowProvider } from './ui/contexts/OverflowContext.js';
import { setupTerminalAndTheme } from './utils/terminalTheme.js';
import { profiler } from './ui/components/DebugProfiler.js';
@@ -184,7 +180,7 @@ ${reason.stack}`
export async function startInteractiveUI(
config: Config,
settings: LoadedSettings,
startupWarnings: StartupWarning[],
startupWarnings: string[],
workspaceRoot: string = process.cwd(),
resumedSessionData: ResumedSessionData | undefined,
initializationResult: InitializationResult,
@@ -239,19 +235,17 @@ export async function startInteractiveUI(
>
<TerminalProvider>
<ScrollProvider>
<OverflowProvider>
<SessionStatsProvider>
<VimModeProvider settings={settings}>
<AppContainer
config={config}
startupWarnings={startupWarnings}
version={version}
resumedSessionData={resumedSessionData}
initializationResult={initializationResult}
/>
</VimModeProvider>
</SessionStatsProvider>
</OverflowProvider>
<SessionStatsProvider>
<VimModeProvider settings={settings}>
<AppContainer
config={config}
startupWarnings={startupWarnings}
version={version}
resumedSessionData={resumedSessionData}
initializationResult={initializationResult}
/>
</VimModeProvider>
</SessionStatsProvider>
</ScrollProvider>
</TerminalProvider>
</MouseProvider>
@@ -586,7 +580,7 @@ export async function main() {
const policyEngine = config.getPolicyEngine();
const messageBus = config.getMessageBus();
createPolicyUpdater(policyEngine, messageBus, config.storage);
createPolicyUpdater(policyEngine, messageBus);
// Register SessionEnd hook to fire on graceful exit
// This runs before telemetry shutdown in runExitCleanup()
@@ -674,20 +668,9 @@ export async function main() {
}
let input = config.getQuestion();
const useAlternateBuffer = shouldEnterAlternateScreen(
isAlternateBufferEnabled(settings),
config.getScreenReader(),
);
const rawStartupWarnings = await getStartupWarnings();
const startupWarnings: StartupWarning[] = [
...rawStartupWarnings.map((message) => ({
id: `startup-${createHash('sha256').update(message).digest('hex').substring(0, 16)}`,
message,
priority: WarningPriority.High,
})),
...(await getUserStartupWarnings(settings.merged, undefined, {
isAlternateBuffer: useAlternateBuffer,
})),
const startupWarnings = [
...(await getStartupWarnings()),
...(await getUserStartupWarnings(settings.merged)),
];
// Handle --resume flag
+22 -31
View File
@@ -9,6 +9,14 @@ import { main } from './gemini.js';
import { debugLogger } from '@google/gemini-cli-core';
import { type Config } from '@google/gemini-cli-core';
// Custom error to identify mock process.exit calls
class MockProcessExitError extends Error {
constructor(readonly code?: string | number | null | undefined) {
super('PROCESS_EXIT_MOCKED');
this.name = 'MockProcessExitError';
}
}
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
@@ -116,39 +124,10 @@ vi.mock('./validateNonInterActiveAuth.js', () => ({
validateNonInteractiveAuth: vi.fn().mockResolvedValue({}),
}));
vi.mock('./core/initializer.js', () => ({
initializeApp: vi.fn().mockResolvedValue({
authError: null,
themeError: null,
shouldOpenAuthDialog: false,
geminiMdFileCount: 0,
}),
}));
vi.mock('./nonInteractiveCli.js', () => ({
runNonInteractive: vi.fn().mockResolvedValue(undefined),
}));
vi.mock('./utils/cleanup.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('./utils/cleanup.js')>();
return {
...actual,
cleanupCheckpoints: vi.fn().mockResolvedValue(undefined),
registerCleanup: vi.fn(),
registerSyncCleanup: vi.fn(),
registerTelemetryConfig: vi.fn(),
runExitCleanup: vi.fn().mockResolvedValue(undefined),
};
});
vi.mock('./zed-integration/zedIntegration.js', () => ({
runZedIntegration: vi.fn().mockResolvedValue(undefined),
}));
vi.mock('./utils/readStdin.js', () => ({
readStdin: vi.fn().mockResolvedValue(''),
}));
const { cleanupMockState } = vi.hoisted(() => ({
cleanupMockState: { shouldThrow: false, called: false },
}));
@@ -190,6 +169,12 @@ describe('gemini.tsx main function cleanup', () => {
const debugLoggerErrorSpy = vi
.spyOn(debugLogger, 'error')
.mockImplementation(() => {});
const processExitSpy = vi
.spyOn(process, 'exit')
.mockImplementation((code) => {
throw new MockProcessExitError(code);
});
vi.mocked(loadSettings).mockReturnValue({
merged: { advanced: {}, security: { auth: {} }, ui: {} },
workspace: { settings: {} },
@@ -216,7 +201,7 @@ describe('gemini.tsx main function cleanup', () => {
getMcpServers: () => ({}),
getMcpClientManager: vi.fn(),
getIdeMode: vi.fn(() => false),
getExperimentalZedIntegration: vi.fn(() => true),
getExperimentalZedIntegration: vi.fn(() => false),
getScreenReader: vi.fn(() => false),
getGeminiMdFileCount: vi.fn(() => 0),
getProjectRoot: vi.fn(() => '/'),
@@ -239,12 +224,18 @@ describe('gemini.tsx main function cleanup', () => {
getRemoteAdminSettings: vi.fn(() => undefined),
} as any); // eslint-disable-line @typescript-eslint/no-explicit-any
await main();
try {
await main();
} catch (e) {
if (!(e instanceof MockProcessExitError)) throw e;
}
expect(cleanupMockState.called).toBe(true);
expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
'Failed to cleanup expired sessions:',
expect.objectContaining({ message: 'Cleanup failed' }),
);
expect(processExitSpy).toHaveBeenCalledWith(0); // Should not exit on cleanup failure
processExitSpy.mockRestore();
});
});
@@ -1,84 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, afterEach } from 'vitest';
import { AppRig } from '../test-utils/AppRig.js';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { PolicyDecision } from '@google/gemini-cli-core';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
describe('Model Steering Integration', () => {
let rig: AppRig | undefined;
afterEach(async () => {
await rig?.unmount();
});
it('should steer the model using a hint during a tool turn', async () => {
const fakeResponsesPath = path.join(
__dirname,
'../test-utils/fixtures/steering.responses',
);
rig = new AppRig({
fakeResponsesPath,
configOverrides: { modelSteering: true },
});
await rig.initialize();
rig.render();
await rig.waitForIdle();
rig.setToolPolicy('list_directory', PolicyDecision.ASK_USER);
rig.setToolPolicy('read_file', PolicyDecision.ASK_USER);
rig.setMockCommands([
{
command: /list_directory/,
result: {
output: 'file1.txt\nfile2.js\nfile3.md',
exitCode: 0,
},
},
{
command: /read_file file1.txt/,
result: {
output: 'This is file1.txt content.',
exitCode: 0,
},
},
]);
// Start a long task
await rig.type('Start long task');
await rig.pressEnter();
// Wait for the model to call 'list_directory' (Confirming state)
await rig.waitForOutput('ReadFolder');
// Injected a hint while the model is in a tool turn
await rig.addUserHint('focus on .txt');
// Resolve list_directory (Proceed)
await rig.resolveTool('ReadFolder');
// Wait for the model to process the hint and output the next action
// Based on steering.responses, it should first acknowledge the hint
await rig.waitForOutput('ACK: I will focus on .txt files now.');
// Then it should proceed with the next action
await rig.waitForOutput(
/Since you want me to focus on .txt files,[\s\S]*I will read file1.txt/,
);
await rig.waitForOutput('ReadFile');
// Resolve read_file (Proceed)
await rig.resolveTool('ReadFile');
// Wait for final completion
await rig.waitForOutput('Task complete.');
});
});
+3 -3
View File
@@ -13,7 +13,6 @@ import type {
import { isSlashCommand } from './ui/utils/commandUtils.js';
import type { LoadedSettings } from './config/settings.js';
import {
convertSessionToClientHistory,
GeminiEventType,
FatalInputError,
promptIdContext,
@@ -36,6 +35,7 @@ import type { Content, Part } from '@google/genai';
import readline from 'node:readline';
import stripAnsi from 'strip-ansi';
import { convertSessionToHistoryFormats } from './ui/hooks/useSessionBrowser.js';
import { handleSlashCommand } from './nonInteractiveCliCommands.js';
import { ConsolePatcher } from './ui/utils/ConsolePatcher.js';
import { handleAtCommand } from './ui/hooks/atCommandProcessor.js';
@@ -220,9 +220,9 @@ export async function runNonInteractive({
// Initialize chat. Resume if resume data is passed.
if (resumedSessionData) {
await geminiClient.resumeChat(
convertSessionToClientHistory(
convertSessionToHistoryFormats(
resumedSessionData.conversation.messages,
),
).clientHistory,
resumedSessionData,
);
}
+2 -49
View File
@@ -4,12 +4,10 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, afterEach, expect } from 'vitest';
import { act } from 'react';
import { describe, it, afterEach } from 'vitest';
import { AppRig } from './AppRig.js';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { debugLogger } from '@google/gemini-cli-core';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -20,47 +18,6 @@ describe('AppRig', () => {
await rig?.unmount();
});
it('should handle deterministic tool turns with breakpoints', async () => {
const fakeResponsesPath = path.join(
__dirname,
'fixtures',
'steering.responses',
);
rig = new AppRig({
fakeResponsesPath,
configOverrides: { modelSteering: true },
});
await rig.initialize();
rig.render();
await rig.waitForIdle();
// Set breakpoints on the canonical tool names
rig.setBreakpoint('list_directory');
rig.setBreakpoint('read_file');
// Start a task
debugLogger.log('[Test] Sending message: Start long task');
await rig.sendMessage('Start long task');
// Wait for the first breakpoint (list_directory)
const pending1 = await rig.waitForPendingConfirmation('list_directory');
expect(pending1.toolName).toBe('list_directory');
// Injected a hint
await rig.addUserHint('focus on .txt');
// Resolve and wait for the NEXT breakpoint (read_file)
// resolveTool will automatically remove the breakpoint policy for list_directory
await rig.resolveTool('list_directory');
const pending2 = await rig.waitForPendingConfirmation('read_file');
expect(pending2.toolName).toBe('read_file');
// Resolve and finish. Also removes read_file breakpoint.
await rig.resolveTool('read_file');
await rig.waitForOutput('Task complete.', 100000);
});
it('should render the app and handle a simple message', async () => {
const fakeResponsesPath = path.join(
__dirname,
@@ -69,11 +26,7 @@ describe('AppRig', () => {
);
rig = new AppRig({ fakeResponsesPath });
await rig.initialize();
await act(async () => {
rig!.render();
// Allow async initializations (like banners) to settle within the act boundary
await new Promise((resolve) => setTimeout(resolve, 0));
});
rig.render();
// Wait for initial render
await rig.waitForIdle();
+7 -21
View File
@@ -74,20 +74,6 @@ class MockExtensionManager extends ExtensionLoader {
setRequestSetting = vi.fn();
}
// Mock GeminiRespondingSpinner to disable animations (avoiding 'act()' warnings) without triggering screen reader mode.
vi.mock('../ui/components/GeminiRespondingSpinner.js', async () => {
const React = await import('react');
const { Text } = await import('ink');
return {
GeminiSpinner: () => React.createElement(Text, null, '...'),
GeminiRespondingSpinner: ({
nonRespondingDisplay,
}: {
nonRespondingDisplay: string;
}) => React.createElement(Text, null, nonRespondingDisplay || '...'),
};
});
export interface AppRigOptions {
fakeResponsesPath?: string;
terminalWidth?: number;
@@ -217,14 +203,13 @@ export class AppRig {
}
private stubRefreshAuth() {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any
const gcConfig = this.config as any;
gcConfig.refreshAuth = async (authMethod: AuthType) => {
gcConfig.modelAvailabilityService.reset();
const newContentGeneratorConfig = {
authType: authMethod,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
proxy: gcConfig.getProxy(),
apiKey: process.env['GEMINI_API_KEY'] || 'test-api-key',
};
@@ -464,11 +449,12 @@ export class AppRig {
this.lastAwaitedConfirmation = undefined;
}
async addUserHint(hint: string) {
async addUserHint(_hint: string) {
if (!this.config) throw new Error('AppRig not initialized');
await act(async () => {
this.config!.userHintService.addUserHint(hint);
});
// TODO(joshualitt): Land hints.
// await act(async () => {
// this.config!.addUserHint(hint);
// });
}
getConfig(): Config {
@@ -502,7 +488,7 @@ export class AppRig {
get lastFrame() {
if (!this.renderResult) return '';
return stripAnsi(this.renderResult.lastFrame({ allowEmpty: true }) || '');
return stripAnsi(this.renderResult.lastFrame() || '');
}
getStaticOutput() {
+2 -2
View File
@@ -13,14 +13,14 @@ import { vi } from 'vitest';
// The version of waitFor from vitest is still fine to use if you aren't waiting
// for React state updates.
export async function waitFor(
assertion: () => void | Promise<void>,
assertion: () => void,
{ timeout = 2000, interval = 50 } = {},
): Promise<void> {
const startTime = Date.now();
while (true) {
try {
await assertion();
assertion();
return;
} catch (error) {
if (Date.now() - startTime > timeout) {
@@ -21,7 +21,7 @@ import type { TextBuffer } from '../ui/components/shared/text-buffer.js';
const invalidCharsRegex = /[\b\x1b]/;
function toHaveOnlyValidCharacters(this: Assertion, buffer: TextBuffer) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-unsafe-assignment
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion
const { isNot } = this as any;
let pass = true;
const invalidLines: Array<{ line: number; content: string }> = [];
@@ -1,4 +0,0 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"role":"model","parts":[{"text":"Starting a long task. First, I'll list the files."},{"functionCall":{"name":"list_directory","args":{"dir_path":"."}}}]},"finishReason":"STOP"}]}]}
{"method":"generateContent","response":{"candidates":[{"content":{"role":"model","parts":[{"text":"ACK: I will focus on .txt files now."}]},"finishReason":"STOP"}]}}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"role":"model","parts":[{"text":"I see the files. Since you want me to focus on .txt files, I will read file1.txt."},{"functionCall":{"name":"read_file","args":{"file_path":"file1.txt"}}}]},"finishReason":"STOP"}]}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"role":"model","parts":[{"text":"I have read file1.txt. Task complete."}]},"finishReason":"STOP"}]}]}
@@ -45,7 +45,7 @@ export const createMockCommandContext = (
forScope: vi.fn().mockReturnValue({ settings: {} }),
} as unknown as LoadedSettings,
git: undefined as GitService | undefined,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-unsafe-assignment
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
logger: {
log: vi.fn(),
logMessage: vi.fn(),
@@ -54,7 +54,7 @@ export const createMockCommandContext = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any, // Cast because Logger is a class.
},
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-unsafe-assignment
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
ui: {
addItem: vi.fn(),
clear: vi.fn(),
@@ -94,14 +94,11 @@ export const createMockCommandContext = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const merge = (target: any, source: any): any => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const output = { ...target };
for (const key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const sourceValue = source[key];
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const targetValue = output[key];
if (
@@ -109,11 +106,9 @@ export const createMockCommandContext = (
Object.prototype.toString.call(sourceValue) === '[object Object]' &&
Object.prototype.toString.call(targetValue) === '[object Object]'
) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
output[key] = merge(targetValue, sourceValue);
} else {
// If not, we do a direct assignment. This preserves Date objects and others.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
output[key] = sourceValue;
}
}
@@ -121,6 +116,5 @@ export const createMockCommandContext = (
return output;
};
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return merge(defaultMocks, overrides);
};
@@ -128,6 +128,7 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
getShellToolInactivityTimeout: vi.fn().mockReturnValue(300000),
getShellExecutionConfig: vi.fn().mockReturnValue({}),
setShellExecutionConfig: vi.fn(),
getEnablePromptCompletion: vi.fn().mockReturnValue(false),
getEnableToolOutputTruncation: vi.fn().mockReturnValue(true),
getTruncateToolOutputThreshold: vi.fn().mockReturnValue(1000),
getTruncateToolOutputLines: vi.fn().mockReturnValue(100),
+27 -64
View File
@@ -5,48 +5,36 @@
*/
import { describe, it, expect, vi } from 'vitest';
import { useState, useEffect, act } from 'react';
import { useState, useEffect } from 'react';
import { Text } from 'ink';
import { renderHook, render } from './render.js';
import { waitFor } from './async.js';
describe('render', () => {
it('should render a component', async () => {
const { lastFrame, waitUntilReady, unmount } = render(
<Text>Hello World</Text>,
);
await waitUntilReady();
expect(lastFrame()).toBe('Hello World\n');
unmount();
it('should render a component', () => {
const { lastFrame } = render(<Text>Hello World</Text>);
expect(lastFrame()).toBe('Hello World');
});
it('should support rerender', async () => {
const { lastFrame, rerender, waitUntilReady, unmount } = render(
<Text>Hello</Text>,
);
await waitUntilReady();
expect(lastFrame()).toBe('Hello\n');
it('should support rerender', () => {
const { lastFrame, rerender } = render(<Text>Hello</Text>);
expect(lastFrame()).toBe('Hello');
await act(async () => {
rerender(<Text>World</Text>);
});
await waitUntilReady();
expect(lastFrame()).toBe('World\n');
unmount();
rerender(<Text>World</Text>);
expect(lastFrame()).toBe('World');
});
it('should support unmount', async () => {
const cleanupMock = vi.fn();
it('should support unmount', () => {
const cleanup = vi.fn();
function TestComponent() {
useEffect(() => cleanupMock, []);
useEffect(() => cleanup, []);
return <Text>Hello</Text>;
}
const { unmount, waitUntilReady } = render(<TestComponent />);
await waitUntilReady();
const { unmount } = render(<TestComponent />);
unmount();
expect(cleanupMock).toHaveBeenCalled();
expect(cleanup).toHaveBeenCalled();
});
});
@@ -60,74 +48,49 @@ describe('renderHook', () => {
return { count, value };
};
const { result, rerender, waitUntilReady, unmount } = renderHook(
useTestHook,
{
initialProps: { value: 1 },
},
);
await waitUntilReady();
const { result, rerender } = renderHook(useTestHook, {
initialProps: { value: 1 },
});
expect(result.current.value).toBe(1);
await waitFor(() => expect(result.current.count).toBe(1));
// Rerender with new props
await act(async () => {
rerender({ value: 2 });
});
await waitUntilReady();
rerender({ value: 2 });
expect(result.current.value).toBe(2);
await waitFor(() => expect(result.current.count).toBe(2));
// Rerender without arguments should use previous props (value: 2)
// This would previously crash or pass undefined if not fixed
await act(async () => {
rerender();
});
await waitUntilReady();
rerender();
expect(result.current.value).toBe(2);
// Count should not increase because value didn't change
await waitFor(() => expect(result.current.count).toBe(2));
unmount();
});
it('should handle initial render without props', async () => {
it('should handle initial render without props', () => {
const useTestHook = () => {
const [count, setCount] = useState(0);
return { count, increment: () => setCount((c) => c + 1) };
};
const { result, rerender, waitUntilReady, unmount } =
renderHook(useTestHook);
await waitUntilReady();
const { result, rerender } = renderHook(useTestHook);
expect(result.current.count).toBe(0);
await act(async () => {
rerender();
});
await waitUntilReady();
rerender();
expect(result.current.count).toBe(0);
unmount();
});
it('should update props if undefined is passed explicitly', async () => {
it('should update props if undefined is passed explicitly', () => {
const useTestHook = (val: string | undefined) => val;
const { result, rerender, waitUntilReady, unmount } = renderHook(
useTestHook,
{
initialProps: 'initial' as string | undefined,
},
);
await waitUntilReady();
const { result, rerender } = renderHook(useTestHook, {
initialProps: 'initial',
});
expect(result.current).toBe('initial');
await act(async () => {
rerender(undefined);
});
await waitUntilReady();
rerender(undefined);
expect(result.current).toBeUndefined();
unmount();
});
});
+62 -447
View File
@@ -4,17 +4,10 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {
render as inkRenderDirect,
type Instance as InkInstance,
type RenderOptions,
} from 'ink';
import { EventEmitter } from 'node:events';
import { render as inkRender } from 'ink-testing-library';
import { Box } from 'ink';
import type React from 'react';
import { Terminal } from '@xterm/headless';
import { vi } from 'vitest';
import stripAnsi from 'strip-ansi';
import { act, useState } from 'react';
import os from 'node:os';
import { LoadedSettings } from '../config/settings.js';
@@ -35,13 +28,6 @@ import { type HistoryItemToolGroup, StreamingState } from '../ui/types.js';
import { ToolActionsProvider } from '../ui/contexts/ToolActionsContext.js';
import { AskUserActionsProvider } from '../ui/contexts/AskUserActionsContext.js';
import { TerminalProvider } from '../ui/contexts/TerminalContext.js';
import {
OverflowProvider,
useOverflowActions,
useOverflowState,
type OverflowActions,
type OverflowState,
} from '../ui/contexts/OverflowContext.js';
import { makeFakeConfig, type Config } from '@google/gemini-cli-core';
import { FakePersistentState } from './persistentStateFake.js';
@@ -54,15 +40,6 @@ import { pickDefaultThemeName } from '../ui/themes/theme.js';
export const persistentStateMock = new FakePersistentState();
if (process.env['NODE_ENV'] === 'test') {
// We mock NODE_ENV to development during tests that use render.tsx
// so that animations (which check process.env.NODE_ENV !== 'test')
// are actually tested. We mutate process.env directly here because
// vi.stubEnv() is cleared by vi.unstubAllEnvs() in test-setup.ts
// after each test.
process.env['NODE_ENV'] = 'development';
}
vi.mock('../utils/persistentState.js', () => ({
persistentState: persistentStateMock,
}));
@@ -73,372 +50,51 @@ vi.mock('../ui/utils/terminalUtils.js', () => ({
isITerm2: vi.fn(() => false),
}));
type TerminalState = {
terminal: Terminal;
cols: number;
rows: number;
};
type RenderMetrics = Parameters<NonNullable<RenderOptions['onRender']>>[0];
interface InkRenderMetrics extends RenderMetrics {
output: string;
staticOutput?: string;
}
function isInkRenderMetrics(
metrics: RenderMetrics,
): metrics is InkRenderMetrics {
const m = metrics as Record<string, unknown>;
return (
typeof m === 'object' &&
m !== null &&
'output' in m &&
typeof m['output'] === 'string'
);
}
class XtermStdout extends EventEmitter {
private state: TerminalState;
private pendingWrites = 0;
private renderCount = 0;
private queue: { promise: Promise<void> };
isTTY = true;
private lastRenderOutput: string | undefined = undefined;
constructor(state: TerminalState, queue: { promise: Promise<void> }) {
super();
this.state = state;
this.queue = queue;
}
get columns() {
return this.state.terminal.cols;
}
get rows() {
return this.state.terminal.rows;
}
get frames(): string[] {
return [];
}
write = (data: string) => {
this.pendingWrites++;
this.queue.promise = this.queue.promise.then(async () => {
await new Promise<void>((resolve) =>
this.state.terminal.write(data, resolve),
);
this.pendingWrites--;
});
};
clear = () => {
this.state.terminal.reset();
this.lastRenderOutput = undefined;
};
dispose = () => {
this.state.terminal.dispose();
};
onRender = (staticContent: string, output: string) => {
this.renderCount++;
this.lastRenderOutput = output;
this.emit('render');
};
lastFrame = (options: { allowEmpty?: boolean } = {}) => {
const buffer = this.state.terminal.buffer.active;
const allLines: string[] = [];
for (let i = 0; i < buffer.length; i++) {
allLines.push(buffer.getLine(i)?.translateToString(true) ?? '');
}
const trimmed = [...allLines];
while (trimmed.length > 0 && trimmed[trimmed.length - 1] === '') {
trimmed.pop();
}
const result = trimmed.join('\n');
// Normalize for cross-platform snapshot stability:
// Normalize any \r\n to \n
const normalized = result.replace(/\r\n/g, '\n');
if (normalized === '' && !options.allowEmpty) {
throw new Error(
'lastFrame() returned an empty string. If this is intentional, use lastFrame({ allowEmpty: true }). ' +
'Otherwise, ensure you are calling await waitUntilReady() and that the component is rendering correctly.',
);
}
return normalized === '' ? normalized : normalized + '\n';
};
async waitUntilReady() {
const startRenderCount = this.renderCount;
if (!vi.isFakeTimers()) {
// Give Ink a chance to start its rendering loop
await new Promise((resolve) => setImmediate(resolve));
}
await act(async () => {
if (vi.isFakeTimers()) {
await vi.advanceTimersByTimeAsync(50);
} else {
// Wait for at least one render to be called if we haven't rendered yet or since start of this call,
// but don't wait forever as some renders might be synchronous or skipped.
if (this.renderCount === startRenderCount) {
const renderPromise = new Promise((resolve) =>
this.once('render', resolve),
);
const timeoutPromise = new Promise((resolve) =>
setTimeout(resolve, 50),
);
await Promise.race([renderPromise, timeoutPromise]);
}
}
});
let attempts = 0;
const maxAttempts = 50;
let lastCurrent = '';
let lastExpected = '';
while (attempts < maxAttempts) {
// Ensure all pending writes to the terminal are processed.
await this.queue.promise;
const currentFrame = stripAnsi(
this.lastFrame({ allowEmpty: true }),
).trim();
const expectedFrame = stripAnsi(this.lastRenderOutput ?? '')
.trim()
.replace(/\r\n/g, '\n');
lastCurrent = currentFrame;
lastExpected = expectedFrame;
const isMatch = () => {
if (expectedFrame === '...') {
return currentFrame !== '';
}
// If both are empty, it's a match.
// We consider undefined lastRenderOutput as effectively empty for this check
// to support hook testing where Ink may skip rendering completely.
if (
(this.lastRenderOutput === undefined || expectedFrame === '') &&
currentFrame === ''
) {
return true;
}
if (this.lastRenderOutput === undefined) {
return false;
}
// If Ink expects nothing but terminal has content, or vice-versa, it's NOT a match.
if (expectedFrame === '' || currentFrame === '') {
return false;
}
// Check if the current frame contains the expected content.
// We use includes because xterm might have some formatting or
// extra whitespace that Ink doesn't account for in its raw output metrics.
return currentFrame.includes(expectedFrame);
};
if (this.pendingWrites === 0 && isMatch()) {
return;
}
attempts++;
await act(async () => {
if (vi.isFakeTimers()) {
await vi.advanceTimersByTimeAsync(10);
} else {
await new Promise((resolve) => setTimeout(resolve, 10));
}
});
}
throw new Error(
`waitUntilReady() timed out after ${maxAttempts} attempts.\n` +
`Expected content (stripped ANSI):\n"${lastExpected}"\n` +
`Actual content (stripped ANSI):\n"${lastCurrent}"\n` +
`Pending writes: ${this.pendingWrites}\n` +
`Render count: ${this.renderCount}`,
);
}
}
class XtermStderr extends EventEmitter {
private state: TerminalState;
private pendingWrites = 0;
private queue: { promise: Promise<void> };
isTTY = true;
constructor(state: TerminalState, queue: { promise: Promise<void> }) {
super();
this.state = state;
this.queue = queue;
}
write = (data: string) => {
this.pendingWrites++;
this.queue.promise = this.queue.promise.then(async () => {
await new Promise<void>((resolve) =>
this.state.terminal.write(data, resolve),
);
this.pendingWrites--;
});
};
dispose = () => {
this.state.terminal.dispose();
};
lastFrame = () => '';
}
class XtermStdin extends EventEmitter {
isTTY = true;
data: string | null = null;
constructor(options: { isTTY?: boolean } = {}) {
super();
this.isTTY = options.isTTY ?? true;
}
write = (data: string) => {
this.data = data;
this.emit('readable');
this.emit('data', data);
};
setEncoding() {}
setRawMode() {}
resume() {}
pause() {}
ref() {}
unref() {}
read = () => {
const { data } = this;
this.data = null;
return data;
};
}
export type RenderInstance = {
rerender: (tree: React.ReactElement) => void;
unmount: () => void;
cleanup: () => void;
stdout: XtermStdout;
stderr: XtermStderr;
stdin: XtermStdin;
frames: string[];
lastFrame: (options?: { allowEmpty?: boolean }) => string;
terminal: Terminal;
waitUntilReady: () => Promise<void>;
capturedOverflowState: OverflowState | undefined;
capturedOverflowActions: OverflowActions | undefined;
};
const instances: InkInstance[] = [];
// Wrapper around ink's render that ensures act() is called and uses Xterm for output
// Wrapper around ink-testing-library's render that ensures act() is called
export const render = (
tree: React.ReactElement,
terminalWidth?: number,
): Omit<
RenderInstance,
'capturedOverflowState' | 'capturedOverflowActions'
> => {
const cols = terminalWidth ?? 100;
// We use 1000 rows to avoid windows with incorrect snapshots if a correct
// value was used (e.g. 40 rows). The alternatives to make things worse are
// windows unfortunately with odd duplicate content in the backbuffer
// which does not match actual behavior in xterm.js on windows.
const rows = 1000;
const terminal = new Terminal({
cols,
rows,
allowProposedApi: true,
convertEol: true,
});
const state: TerminalState = {
terminal,
cols,
rows,
};
const writeQueue = { promise: Promise.resolve() };
const stdout = new XtermStdout(state, writeQueue);
const stderr = new XtermStderr(state, writeQueue);
const stdin = new XtermStdin();
let instance!: InkInstance;
stdout.clear();
): ReturnType<typeof inkRender> => {
let renderResult: ReturnType<typeof inkRender> =
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
undefined as unknown as ReturnType<typeof inkRender>;
act(() => {
instance = inkRenderDirect(tree, {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
stdout: stdout as unknown as NodeJS.WriteStream,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
stderr: stderr as unknown as NodeJS.WriteStream,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
stdin: stdin as unknown as NodeJS.ReadStream,
debug: false,
exitOnCtrlC: false,
patchConsole: false,
onRender: (metrics: RenderMetrics) => {
if (isInkRenderMetrics(metrics)) {
stdout.onRender(metrics.staticOutput ?? '', metrics.output);
}
},
});
renderResult = inkRender(tree);
});
instances.push(instance);
if (terminalWidth !== undefined && renderResult?.stdout) {
// Override the columns getter on the stdout instance provided by ink-testing-library
Object.defineProperty(renderResult.stdout, 'columns', {
get: () => terminalWidth,
configurable: true,
});
// Trigger a rerender so Ink can pick up the new terminal width
act(() => {
renderResult.rerender(tree);
});
}
const originalUnmount = renderResult.unmount;
const originalRerender = renderResult.rerender;
return {
rerender: (newTree: React.ReactElement) => {
act(() => {
stdout.clear();
instance.rerender(newTree);
});
},
...renderResult,
unmount: () => {
act(() => {
instance.unmount();
originalUnmount();
});
},
rerender: (newTree: React.ReactElement) => {
act(() => {
originalRerender(newTree);
});
stdout.dispose();
stderr.dispose();
},
cleanup: instance.cleanup,
stdout,
stderr,
stdin,
frames: stdout.frames,
lastFrame: stdout.lastFrame,
terminal: state.terminal,
waitUntilReady: () => stdout.waitUntilReady(),
};
};
export const cleanup = () => {
for (const instance of instances) {
act(() => {
instance.unmount();
});
instance.cleanup();
}
instances.length = 0;
};
export const simulateClick = async (
stdin: XtermStdin,
stdin: ReturnType<typeof inkRender>['stdin'],
col: number,
row: number,
button: 0 | 1 | 2 = 0, // 0 for left, 1 for middle, 2 for right
@@ -495,7 +151,7 @@ export const mockSettings = new LoadedSettings(
const baseMockUiState = {
renderMarkdown: true,
streamingState: StreamingState.Idle,
terminalWidth: 100,
terminalWidth: 120,
terminalHeight: 40,
currentModel: 'gemini-pro',
terminalBackgroundColor: 'black',
@@ -510,8 +166,6 @@ const baseMockUiState = {
proQuotaRequest: null,
validationRequest: null,
},
hintMode: false,
hintBuffer: '',
};
export const mockAppState: AppState = {
@@ -543,7 +197,6 @@ const mockUIActions: UIActions = {
vimHandleInput: vi.fn(),
handleIdePromptComplete: vi.fn(),
handleFolderTrustSelect: vi.fn(),
setIsPolicyUpdateDialogOpen: vi.fn(),
setConstrainHeight: vi.fn(),
onEscapePromptChange: vi.fn(),
refreshStatic: vi.fn(),
@@ -566,24 +219,10 @@ const mockUIActions: UIActions = {
setActiveBackgroundShellPid: vi.fn(),
setIsBackgroundShellListOpen: vi.fn(),
setAuthContext: vi.fn(),
onHintInput: vi.fn(),
onHintBackspace: vi.fn(),
onHintClear: vi.fn(),
onHintSubmit: vi.fn(),
handleRestart: vi.fn(),
handleNewAgentsSelect: vi.fn(),
};
let capturedOverflowState: OverflowState | undefined;
let capturedOverflowActions: OverflowActions | undefined;
const ContextCapture: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
capturedOverflowState = useOverflowState();
capturedOverflowActions = useOverflowActions();
return <>{children}</>;
};
export const renderWithProviders = (
component: React.ReactElement,
{
@@ -613,13 +252,7 @@ export const renderWithProviders = (
};
appState?: AppState;
} = {},
): RenderInstance & {
simulateClick: (
col: number,
row: number,
button?: 0 | 1 | 2,
) => Promise<void>;
} => {
): ReturnType<typeof render> & { simulateClick: typeof simulateClick } => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const baseState: UIState = new Proxy(
{ ...baseMockUiState, ...providedUiState },
@@ -685,9 +318,6 @@ export const renderWithProviders = (
.filter((item): item is HistoryItemToolGroup => item.type === 'tool_group')
.flatMap((item) => item.tools);
capturedOverflowState = undefined;
capturedOverflowActions = undefined;
const renderResult = render(
<AppContext.Provider value={appState}>
<ConfigContext.Provider value={config}>
@@ -700,39 +330,35 @@ export const renderWithProviders = (
value={finalUiState.streamingState}
>
<UIActionsContext.Provider value={finalUIActions}>
<OverflowProvider>
<ToolActionsProvider
config={config}
toolCalls={allToolCalls}
<ToolActionsProvider
config={config}
toolCalls={allToolCalls}
>
<AskUserActionsProvider
request={null}
onSubmit={vi.fn()}
onCancel={vi.fn()}
>
<AskUserActionsProvider
request={null}
onSubmit={vi.fn()}
onCancel={vi.fn()}
>
<KeypressProvider>
<MouseProvider
mouseEventsEnabled={mouseEventsEnabled}
>
<TerminalProvider>
<ScrollProvider>
<ContextCapture>
<Box
width={terminalWidth}
flexShrink={0}
flexGrow={0}
flexDirection="column"
>
{component}
</Box>
</ContextCapture>
</ScrollProvider>
</TerminalProvider>
</MouseProvider>
</KeypressProvider>
</AskUserActionsProvider>
</ToolActionsProvider>
</OverflowProvider>
<KeypressProvider>
<MouseProvider
mouseEventsEnabled={mouseEventsEnabled}
>
<TerminalProvider>
<ScrollProvider>
<Box
width={terminalWidth}
flexShrink={0}
flexGrow={0}
flexDirection="column"
>
{component}
</Box>
</ScrollProvider>
</TerminalProvider>
</MouseProvider>
</KeypressProvider>
</AskUserActionsProvider>
</ToolActionsProvider>
</UIActionsContext.Provider>
</StreamingContext.Provider>
</SessionStatsProvider>
@@ -745,13 +371,7 @@ export const renderWithProviders = (
terminalWidth,
);
return {
...renderResult,
capturedOverflowState,
capturedOverflowActions,
simulateClick: (col: number, row: number, button?: 0 | 1 | 2) =>
simulateClick(renderResult.stdin, col, row, button),
};
return { ...renderResult, simulateClick };
};
export function renderHook<Result, Props>(
@@ -764,7 +384,6 @@ export function renderHook<Result, Props>(
result: { current: Result };
rerender: (props?: Props) => void;
unmount: () => void;
waitUntilReady: () => Promise<void>;
} {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const result = { current: undefined as unknown as Result };
@@ -786,7 +405,6 @@ export function renderHook<Result, Props>(
let inkRerender: (tree: React.ReactElement) => void = () => {};
let unmount: () => void = () => {};
let waitUntilReady: () => Promise<void> = async () => {};
act(() => {
const renderResult = render(
@@ -796,7 +414,6 @@ export function renderHook<Result, Props>(
);
inkRerender = renderResult.rerender;
unmount = renderResult.unmount;
waitUntilReady = renderResult.waitUntilReady;
});
function rerender(props?: Props) {
@@ -813,7 +430,7 @@ export function renderHook<Result, Props>(
});
}
return { result, rerender, unmount, waitUntilReady };
return { result, rerender, unmount };
}
export function renderHookWithProviders<Result, Props>(
@@ -834,7 +451,6 @@ export function renderHookWithProviders<Result, Props>(
result: { current: Result };
rerender: (props?: Props) => void;
unmount: () => void;
waitUntilReady: () => Promise<void>;
} {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const result = { current: undefined as unknown as Result };
@@ -884,6 +500,5 @@ export function renderHookWithProviders<Result, Props>(
renderResult.unmount();
});
},
waitUntilReady: () => renderResult.waitUntilReady(),
};
}
+1 -2
View File
@@ -46,7 +46,6 @@ export const createMockSettings = (
workspace,
isTrusted,
errors,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
merged: mergedOverride,
...settingsOverrides
} = overrides;
@@ -76,7 +75,7 @@ export const createMockSettings = (
// Assign any function overrides (e.g., vi.fn() for methods)
for (const key in overrides) {
if (typeof overrides[key] === 'function') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-unsafe-assignment
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(loaded as any)[key] = overrides[key];
}
}
+48 -103
View File
@@ -92,42 +92,32 @@ describe('App', () => {
backgroundShells: new Map(),
};
it('should render main content and composer when not quitting', async () => {
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<App />,
{
uiState: mockUIState,
useAlternateBuffer: false,
},
);
await waitUntilReady();
it('should render main content and composer when not quitting', () => {
const { lastFrame } = renderWithProviders(<App />, {
uiState: mockUIState,
useAlternateBuffer: false,
});
expect(lastFrame()).toContain('Tips for getting started');
expect(lastFrame()).toContain('Notifications');
expect(lastFrame()).toContain('Composer');
unmount();
});
it('should render quitting display when quittingMessages is set', async () => {
it('should render quitting display when quittingMessages is set', () => {
const quittingUIState = {
...mockUIState,
quittingMessages: [{ id: 1, type: 'user', text: 'test' }],
} as UIState;
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<App />,
{
uiState: quittingUIState,
useAlternateBuffer: false,
},
);
await waitUntilReady();
const { lastFrame } = renderWithProviders(<App />, {
uiState: quittingUIState,
useAlternateBuffer: false,
});
expect(lastFrame()).toContain('Quitting...');
unmount();
});
it('should render full history in alternate buffer mode when quittingMessages is set', async () => {
it('should render full history in alternate buffer mode when quittingMessages is set', () => {
const quittingUIState = {
...mockUIState,
quittingMessages: [{ id: 1, type: 'user', text: 'test' }],
@@ -135,38 +125,28 @@ describe('App', () => {
pendingHistoryItems: [{ type: 'user', text: 'pending item' }],
} as UIState;
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<App />,
{
uiState: quittingUIState,
useAlternateBuffer: true,
},
);
await waitUntilReady();
const { lastFrame } = renderWithProviders(<App />, {
uiState: quittingUIState,
useAlternateBuffer: true,
});
expect(lastFrame()).toContain('HistoryItemDisplay');
expect(lastFrame()).toContain('Quitting...');
unmount();
});
it('should render dialog manager when dialogs are visible', async () => {
it('should render dialog manager when dialogs are visible', () => {
const dialogUIState = {
...mockUIState,
dialogsVisible: true,
} as UIState;
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<App />,
{
uiState: dialogUIState,
},
);
await waitUntilReady();
const { lastFrame } = renderWithProviders(<App />, {
uiState: dialogUIState,
});
expect(lastFrame()).toContain('Tips for getting started');
expect(lastFrame()).toContain('Notifications');
expect(lastFrame()).toContain('DialogManager');
unmount();
});
it.each([
@@ -174,62 +154,47 @@ describe('App', () => {
{ key: 'D', stateKey: 'ctrlDPressedOnce' },
])(
'should show Ctrl+$key exit prompt when dialogs are visible and $stateKey is true',
async ({ key, stateKey }) => {
({ key, stateKey }) => {
const uiState = {
...mockUIState,
dialogsVisible: true,
[stateKey]: true,
} as UIState;
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<App />,
{
uiState,
},
);
await waitUntilReady();
const { lastFrame } = renderWithProviders(<App />, {
uiState,
});
expect(lastFrame()).toContain(`Press Ctrl+${key} again to exit.`);
unmount();
},
);
it('should render ScreenReaderAppLayout when screen reader is enabled', async () => {
it('should render ScreenReaderAppLayout when screen reader is enabled', () => {
(useIsScreenReaderEnabled as Mock).mockReturnValue(true);
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<App />,
{
uiState: mockUIState,
},
);
await waitUntilReady();
const { lastFrame } = renderWithProviders(<App />, {
uiState: mockUIState,
});
expect(lastFrame()).toContain('Notifications');
expect(lastFrame()).toContain('Footer');
expect(lastFrame()).toContain('Tips for getting started');
expect(lastFrame()).toContain('Composer');
unmount();
});
it('should render DefaultAppLayout when screen reader is not enabled', async () => {
it('should render DefaultAppLayout when screen reader is not enabled', () => {
(useIsScreenReaderEnabled as Mock).mockReturnValue(false);
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<App />,
{
uiState: mockUIState,
},
);
await waitUntilReady();
const { lastFrame } = renderWithProviders(<App />, {
uiState: mockUIState,
});
expect(lastFrame()).toContain('Tips for getting started');
expect(lastFrame()).toContain('Notifications');
expect(lastFrame()).toContain('Composer');
unmount();
});
it('should render ToolConfirmationQueue along with Composer when tool is confirming and experiment is on', async () => {
it('should render ToolConfirmationQueue along with Composer when tool is confirming and experiment is on', () => {
(useIsScreenReaderEnabled as Mock).mockReturnValue(false);
const toolCalls = [
@@ -269,64 +234,44 @@ describe('App', () => {
vi.spyOn(configWithExperiment, 'isTrustedFolder').mockReturnValue(true);
vi.spyOn(configWithExperiment, 'getIdeMode').mockReturnValue(false);
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<App />,
{
uiState: stateWithConfirmingTool,
config: configWithExperiment,
},
);
await waitUntilReady();
const { lastFrame } = renderWithProviders(<App />, {
uiState: stateWithConfirmingTool,
config: configWithExperiment,
});
expect(lastFrame()).toContain('Tips for getting started');
expect(lastFrame()).toContain('Notifications');
expect(lastFrame()).toContain('Action Required'); // From ToolConfirmationQueue
expect(lastFrame()).toContain('Composer');
expect(lastFrame()).toMatchSnapshot();
unmount();
});
describe('Snapshots', () => {
it('renders default layout correctly', async () => {
it('renders default layout correctly', () => {
(useIsScreenReaderEnabled as Mock).mockReturnValue(false);
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<App />,
{
uiState: mockUIState,
},
);
await waitUntilReady();
const { lastFrame } = renderWithProviders(<App />, {
uiState: mockUIState,
});
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders screen reader layout correctly', async () => {
it('renders screen reader layout correctly', () => {
(useIsScreenReaderEnabled as Mock).mockReturnValue(true);
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<App />,
{
uiState: mockUIState,
},
);
await waitUntilReady();
const { lastFrame } = renderWithProviders(<App />, {
uiState: mockUIState,
});
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders with dialogs visible', async () => {
it('renders with dialogs visible', () => {
const dialogUIState = {
...mockUIState,
dialogsVisible: true,
} as UIState;
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<App />,
{
uiState: dialogUIState,
},
);
await waitUntilReady();
const { lastFrame } = renderWithProviders(<App />, {
uiState: dialogUIState,
});
expect(lastFrame()).toMatchSnapshot();
unmount();
});
});
});
+18 -717
View File
@@ -14,8 +14,9 @@ import {
type Mock,
type MockedObject,
} from 'vitest';
import { render, cleanup, persistentStateMock } from '../test-utils/render.js';
import { render, persistentStateMock } from '../test-utils/render.js';
import { waitFor } from '../test-utils/async.js';
import { cleanup } from 'ink-testing-library';
import { act, useContext, type ReactElement } from 'react';
import { AppContainer } from './AppContainer.js';
import { SettingsContext } from './contexts/SettingsContext.js';
@@ -26,8 +27,6 @@ import {
CoreEvent,
type UserFeedbackPayload,
type ResumedSessionData,
type StartupWarning,
WarningPriority,
AuthType,
type AgentDefinition,
CoreToolCallStatus,
@@ -50,15 +49,6 @@ const mockIdeClient = vi.hoisted(() => ({
const mocks = vi.hoisted(() => ({
mockStdout: { write: vi.fn() },
}));
const terminalNotificationsMocks = vi.hoisted(() => ({
notifyViaTerminal: vi.fn().mockResolvedValue(true),
isNotificationsEnabled: vi.fn(() => true),
buildRunEventNotificationContent: vi.fn((event) => ({
title: 'Mock Notification',
subtitle: 'Mock Subtitle',
body: JSON.stringify(event),
})),
}));
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
@@ -105,11 +95,6 @@ import {
type UIActions,
} from './contexts/UIActionsContext.js';
import { KeypressProvider } from './contexts/KeypressContext.js';
import { OverflowProvider } from './contexts/OverflowContext.js';
import {
useOverflowActions,
type OverflowActions,
} from './contexts/OverflowContext.js';
// Mock useStdout to capture terminal title writes
vi.mock('ink', async (importOriginal) => {
@@ -125,11 +110,9 @@ vi.mock('ink', async (importOriginal) => {
// so we can assert against them in our tests.
let capturedUIState: UIState;
let capturedUIActions: UIActions;
let capturedOverflowActions: OverflowActions;
function TestContextConsumer() {
capturedUIState = useContext(UIStateContext)!;
capturedUIActions = useContext(UIActionsContext)!;
capturedOverflowActions = useOverflowActions()!;
return null;
}
@@ -182,12 +165,6 @@ vi.mock('./hooks/useShellInactivityStatus.js', () => ({
inactivityStatus: 'none',
})),
}));
vi.mock('../utils/terminalNotifications.js', () => ({
notifyViaTerminal: terminalNotificationsMocks.notifyViaTerminal,
isNotificationsEnabled: terminalNotificationsMocks.isNotificationsEnabled,
buildRunEventNotificationContent:
terminalNotificationsMocks.buildRunEventNotificationContent,
}));
vi.mock('./hooks/useTerminalTheme.js', () => ({
useTerminalTheme: vi.fn(),
}));
@@ -195,7 +172,6 @@ vi.mock('./hooks/useTerminalTheme.js', () => ({
import { useHookDisplayState } from './hooks/useHookDisplayState.js';
import { useTerminalTheme } from './hooks/useTerminalTheme.js';
import { useShellInactivityStatus } from './hooks/useShellInactivityStatus.js';
import { useFocus } from './hooks/useFocus.js';
// Mock external utilities
vi.mock('../utils/events.js');
@@ -236,10 +212,7 @@ import {
disableMouseEvents,
} from '@google/gemini-cli-core';
import { type ExtensionManager } from '../config/extension-manager.js';
import {
WARNING_PROMPT_DURATION_MS,
EXPAND_HINT_DURATION_MS,
} from './constants.js';
import { WARNING_PROMPT_DURATION_MS } from './constants.js';
describe('AppContainer State Management', () => {
let mockConfig: Config;
@@ -260,20 +233,18 @@ describe('AppContainer State Management', () => {
config?: Config;
version?: string;
initResult?: InitializationResult;
startupWarnings?: StartupWarning[];
startupWarnings?: string[];
resumedSessionData?: ResumedSessionData;
} = {}) => (
<SettingsContext.Provider value={settings}>
<KeypressProvider config={config}>
<OverflowProvider>
<AppContainer
config={config}
version={version}
initializationResult={initResult}
startupWarnings={startupWarnings}
resumedSessionData={resumedSessionData}
/>
</OverflowProvider>
<AppContainer
config={config}
version={version}
initializationResult={initResult}
startupWarnings={startupWarnings}
resumedSessionData={resumedSessionData}
/>
</KeypressProvider>
</SettingsContext.Provider>
);
@@ -309,7 +280,6 @@ describe('AppContainer State Management', () => {
const mockedUseHookDisplayState = useHookDisplayState as Mock;
const mockedUseTerminalTheme = useTerminalTheme as Mock;
const mockedUseShellInactivityStatus = useShellInactivityStatus as Mock;
const mockedUseFocusState = useFocus as Mock;
const DEFAULT_GEMINI_STREAM_MOCK = {
streamingState: 'idle',
@@ -447,10 +417,6 @@ describe('AppContainer State Management', () => {
shouldShowFocusHint: false,
inactivityStatus: 'none',
});
mockedUseFocusState.mockReturnValue({
isFocused: true,
hasReceivedFocusEvent: true,
});
// Mock Config
mockConfig = makeFakeConfig();
@@ -515,18 +481,7 @@ describe('AppContainer State Management', () => {
});
it('renders with startup warnings', async () => {
const startupWarnings: StartupWarning[] = [
{
id: 'w1',
message: 'Warning 1',
priority: WarningPriority.High,
},
{
id: 'w2',
message: 'Warning 2',
priority: WarningPriority.High,
},
];
const startupWarnings = ['Warning 1', 'Warning 2'];
let unmount: () => void;
await act(async () => {
@@ -570,358 +525,6 @@ describe('AppContainer State Management', () => {
});
describe('State Initialization', () => {
it('sends a macOS notification when confirmation is pending and terminal is unfocused', async () => {
mockedUseFocusState.mockReturnValue({
isFocused: false,
hasReceivedFocusEvent: true,
});
mockedUseGeminiStream.mockReturnValue({
...DEFAULT_GEMINI_STREAM_MOCK,
pendingHistoryItems: [
{
type: 'tool_group',
tools: [
{
callId: 'call-1',
name: 'run_shell_command',
description: 'Run command',
resultDisplay: undefined,
status: CoreToolCallStatus.AwaitingApproval,
confirmationDetails: {
type: 'exec',
title: 'Run shell command',
command: 'ls',
rootCommand: 'ls',
rootCommands: ['ls'],
},
},
],
},
],
});
let unmount: (() => void) | undefined;
await act(async () => {
const rendered = renderAppContainer();
unmount = rendered.unmount;
});
await waitFor(() =>
expect(terminalNotificationsMocks.notifyViaTerminal).toHaveBeenCalled(),
);
expect(
terminalNotificationsMocks.buildRunEventNotificationContent,
).toHaveBeenCalledWith(
expect.objectContaining({
type: 'attention',
}),
);
await act(async () => {
unmount?.();
});
});
it('does not send attention notification when terminal is focused', async () => {
mockedUseFocusState.mockReturnValue({
isFocused: true,
hasReceivedFocusEvent: true,
});
mockedUseGeminiStream.mockReturnValue({
...DEFAULT_GEMINI_STREAM_MOCK,
pendingHistoryItems: [
{
type: 'tool_group',
tools: [
{
callId: 'call-2',
name: 'run_shell_command',
description: 'Run command',
resultDisplay: undefined,
status: CoreToolCallStatus.AwaitingApproval,
confirmationDetails: {
type: 'exec',
title: 'Run shell command',
command: 'ls',
rootCommand: 'ls',
rootCommands: ['ls'],
},
},
],
},
],
});
let unmount: (() => void) | undefined;
await act(async () => {
const rendered = renderAppContainer();
unmount = rendered.unmount;
});
expect(
terminalNotificationsMocks.notifyViaTerminal,
).not.toHaveBeenCalled();
await act(async () => {
unmount?.();
});
});
it('sends attention notification when focus reporting is unavailable', async () => {
mockedUseFocusState.mockReturnValue({
isFocused: true,
hasReceivedFocusEvent: false,
});
mockedUseGeminiStream.mockReturnValue({
...DEFAULT_GEMINI_STREAM_MOCK,
pendingHistoryItems: [
{
type: 'tool_group',
tools: [
{
callId: 'call-focus-unknown',
name: 'run_shell_command',
description: 'Run command',
resultDisplay: undefined,
status: CoreToolCallStatus.AwaitingApproval,
confirmationDetails: {
type: 'exec',
title: 'Run shell command',
command: 'ls',
rootCommand: 'ls',
rootCommands: ['ls'],
},
},
],
},
],
});
let unmount: (() => void) | undefined;
await act(async () => {
const rendered = renderAppContainer();
unmount = rendered.unmount;
});
await waitFor(() =>
expect(terminalNotificationsMocks.notifyViaTerminal).toHaveBeenCalled(),
);
await act(async () => {
unmount?.();
});
});
it('sends a macOS notification when a response completes while unfocused', async () => {
mockedUseFocusState.mockReturnValue({
isFocused: false,
hasReceivedFocusEvent: true,
});
let currentStreamingState: 'idle' | 'responding' = 'responding';
mockedUseGeminiStream.mockImplementation(() => ({
...DEFAULT_GEMINI_STREAM_MOCK,
streamingState: currentStreamingState,
}));
let unmount: (() => void) | undefined;
let rerender: ((tree: ReactElement) => void) | undefined;
await act(async () => {
const rendered = renderAppContainer();
unmount = rendered.unmount;
rerender = rendered.rerender;
});
currentStreamingState = 'idle';
await act(async () => {
rerender?.(getAppContainer());
});
await waitFor(() =>
expect(
terminalNotificationsMocks.buildRunEventNotificationContent,
).toHaveBeenCalledWith(
expect.objectContaining({
type: 'session_complete',
detail: 'Gemini CLI finished responding.',
}),
),
);
expect(terminalNotificationsMocks.notifyViaTerminal).toHaveBeenCalled();
await act(async () => {
unmount?.();
});
});
it('sends completion notification when focus reporting is unavailable', async () => {
mockedUseFocusState.mockReturnValue({
isFocused: true,
hasReceivedFocusEvent: false,
});
let currentStreamingState: 'idle' | 'responding' = 'responding';
mockedUseGeminiStream.mockImplementation(() => ({
...DEFAULT_GEMINI_STREAM_MOCK,
streamingState: currentStreamingState,
}));
let unmount: (() => void) | undefined;
let rerender: ((tree: ReactElement) => void) | undefined;
await act(async () => {
const rendered = renderAppContainer();
unmount = rendered.unmount;
rerender = rendered.rerender;
});
currentStreamingState = 'idle';
await act(async () => {
rerender?.(getAppContainer());
});
await waitFor(() =>
expect(
terminalNotificationsMocks.buildRunEventNotificationContent,
).toHaveBeenCalledWith(
expect.objectContaining({
type: 'session_complete',
detail: 'Gemini CLI finished responding.',
}),
),
);
await act(async () => {
unmount?.();
});
});
it('does not send completion notification when another action-required dialog is pending', async () => {
mockedUseFocusState.mockReturnValue({
isFocused: false,
hasReceivedFocusEvent: true,
});
mockedUseQuotaAndFallback.mockReturnValue({
proQuotaRequest: { kind: 'upgrade' },
handleProQuotaChoice: vi.fn(),
});
let currentStreamingState: 'idle' | 'responding' = 'responding';
mockedUseGeminiStream.mockImplementation(() => ({
...DEFAULT_GEMINI_STREAM_MOCK,
streamingState: currentStreamingState,
}));
let unmount: (() => void) | undefined;
let rerender: ((tree: ReactElement) => void) | undefined;
await act(async () => {
const rendered = renderAppContainer();
unmount = rendered.unmount;
rerender = rendered.rerender;
});
currentStreamingState = 'idle';
await act(async () => {
rerender?.(getAppContainer());
});
expect(
terminalNotificationsMocks.notifyViaTerminal,
).not.toHaveBeenCalled();
await act(async () => {
unmount?.();
});
});
it('can send repeated attention notifications for the same key after pending state clears', async () => {
mockedUseFocusState.mockReturnValue({
isFocused: false,
hasReceivedFocusEvent: true,
});
let pendingHistoryItems = [
{
type: 'tool_group',
tools: [
{
callId: 'repeat-key-call',
name: 'run_shell_command',
description: 'Run command',
resultDisplay: undefined,
status: CoreToolCallStatus.AwaitingApproval,
confirmationDetails: {
type: 'exec',
title: 'Run shell command',
command: 'ls',
rootCommand: 'ls',
rootCommands: ['ls'],
},
},
],
},
];
mockedUseGeminiStream.mockImplementation(() => ({
...DEFAULT_GEMINI_STREAM_MOCK,
pendingHistoryItems,
}));
let unmount: (() => void) | undefined;
let rerender: ((tree: ReactElement) => void) | undefined;
await act(async () => {
const rendered = renderAppContainer();
unmount = rendered.unmount;
rerender = rendered.rerender;
});
await waitFor(() =>
expect(
terminalNotificationsMocks.notifyViaTerminal,
).toHaveBeenCalledTimes(1),
);
pendingHistoryItems = [];
await act(async () => {
rerender?.(getAppContainer());
});
pendingHistoryItems = [
{
type: 'tool_group',
tools: [
{
callId: 'repeat-key-call',
name: 'run_shell_command',
description: 'Run command',
resultDisplay: undefined,
status: CoreToolCallStatus.AwaitingApproval,
confirmationDetails: {
type: 'exec',
title: 'Run shell command',
command: 'ls',
rootCommand: 'ls',
rootCommands: ['ls'],
},
},
],
},
];
await act(async () => {
rerender?.(getAppContainer());
});
await waitFor(() =>
expect(
terminalNotificationsMocks.notifyViaTerminal,
).toHaveBeenCalledTimes(2),
);
await act(async () => {
unmount?.();
});
});
it('initializes with theme error from initialization result', async () => {
const initResultWithError = {
...mockInitResult,
@@ -2699,14 +2302,12 @@ describe('AppContainer State Management', () => {
const getTree = (settings: LoadedSettings) => (
<SettingsContext.Provider value={settings}>
<KeypressProvider config={mockConfig}>
<OverflowProvider>
<AppContainer
config={mockConfig}
version="1.0.0"
initializationResult={mockInitResult}
/>
<TestChild />
</OverflowProvider>
<AppContainer
config={mockConfig}
version="1.0.0"
initializationResult={mockInitResult}
/>
<TestChild />
</KeypressProvider>
</SettingsContext.Provider>
);
@@ -3317,306 +2918,6 @@ describe('AppContainer State Management', () => {
});
});
describe('Submission Handling', () => {
it('resets expansion state on submission when not in alternate buffer', async () => {
const { checkPermissions } = await import(
'./hooks/atCommandProcessor.js'
);
vi.mocked(checkPermissions).mockResolvedValue([]);
let unmount: () => void;
await act(async () => {
unmount = renderAppContainer({
settings: {
...mockSettings,
merged: {
...mockSettings.merged,
ui: { ...mockSettings.merged.ui, useAlternateBuffer: false },
},
} as LoadedSettings,
}).unmount;
});
await waitFor(() => expect(capturedUIActions).toBeTruthy());
// Expand first
act(() => capturedUIActions.setConstrainHeight(false));
expect(capturedUIState.constrainHeight).toBe(false);
// Reset mock stdout to clear any initial writes
mocks.mockStdout.write.mockClear();
// Submit
await act(async () => capturedUIActions.handleFinalSubmit('test prompt'));
// Should be reset
expect(capturedUIState.constrainHeight).toBe(true);
// Should refresh static (which clears terminal in non-alternate buffer)
expect(mocks.mockStdout.write).toHaveBeenCalledWith(
ansiEscapes.clearTerminal,
);
unmount!();
});
it('resets expansion state on submission when in alternate buffer without clearing terminal', async () => {
const { checkPermissions } = await import(
'./hooks/atCommandProcessor.js'
);
vi.mocked(checkPermissions).mockResolvedValue([]);
let unmount: () => void;
await act(async () => {
unmount = renderAppContainer({
settings: {
...mockSettings,
merged: {
...mockSettings.merged,
ui: { ...mockSettings.merged.ui, useAlternateBuffer: true },
},
} as LoadedSettings,
}).unmount;
});
await waitFor(() => expect(capturedUIActions).toBeTruthy());
// Expand first
act(() => capturedUIActions.setConstrainHeight(false));
expect(capturedUIState.constrainHeight).toBe(false);
// Reset mock stdout
mocks.mockStdout.write.mockClear();
// Submit
await act(async () => capturedUIActions.handleFinalSubmit('test prompt'));
// Should be reset
expect(capturedUIState.constrainHeight).toBe(true);
// Should NOT refresh static's clearTerminal in alternate buffer
expect(mocks.mockStdout.write).not.toHaveBeenCalledWith(
ansiEscapes.clearTerminal,
);
unmount!();
});
});
describe('Overflow Hint Handling', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('sets showIsExpandableHint when overflow occurs in Standard Mode and hides after 10s', async () => {
let unmount: () => void;
await act(async () => {
const result = renderAppContainer();
unmount = result.unmount;
});
await waitFor(() => expect(capturedUIState).toBeTruthy());
// Trigger overflow
act(() => {
capturedOverflowActions.addOverflowingId('test-id');
});
await waitFor(() => {
// Should show hint because we are in Standard Mode (default settings) and have overflow
expect(capturedUIState.showIsExpandableHint).toBe(true);
});
// Advance just before the timeout
act(() => {
vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS - 100);
});
expect(capturedUIState.showIsExpandableHint).toBe(true);
// Advance to hit the timeout mark
act(() => {
vi.advanceTimersByTime(100);
});
await waitFor(() => {
expect(capturedUIState.showIsExpandableHint).toBe(false);
});
unmount!();
});
it('toggles expansion state and resets the hint timer when Ctrl+O is pressed in Standard Mode', async () => {
let unmount: () => void;
let stdin: ReturnType<typeof renderAppContainer>['stdin'];
await act(async () => {
const result = renderAppContainer();
unmount = result.unmount;
stdin = result.stdin;
});
await waitFor(() => expect(capturedUIState).toBeTruthy());
// Initial state is constrainHeight = true
expect(capturedUIState.constrainHeight).toBe(true);
// Trigger overflow so the hint starts showing
act(() => {
capturedOverflowActions.addOverflowingId('test-id');
});
await waitFor(() => {
expect(capturedUIState.showIsExpandableHint).toBe(true);
});
// Advance half the duration
act(() => {
vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS / 2);
});
expect(capturedUIState.showIsExpandableHint).toBe(true);
// Simulate Ctrl+O
act(() => {
stdin.write('\x0f'); // \x0f is Ctrl+O
});
await waitFor(() => {
// constrainHeight should toggle
expect(capturedUIState.constrainHeight).toBe(false);
});
// Advance enough that the original timer would have expired if it hadn't reset
act(() => {
vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS / 2 + 1000);
});
// We expect it to still be true because Ctrl+O should have reset the timer
expect(capturedUIState.showIsExpandableHint).toBe(true);
// Advance remaining time to reach the new timeout
act(() => {
vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS / 2 - 1000);
});
await waitFor(() => {
expect(capturedUIState.showIsExpandableHint).toBe(false);
});
unmount!();
});
it('toggles Ctrl+O multiple times and verifies the hint disappears exactly after the last toggle', async () => {
let unmount: () => void;
let stdin: ReturnType<typeof renderAppContainer>['stdin'];
await act(async () => {
const result = renderAppContainer();
unmount = result.unmount;
stdin = result.stdin;
});
await waitFor(() => expect(capturedUIState).toBeTruthy());
// Initial state is constrainHeight = true
expect(capturedUIState.constrainHeight).toBe(true);
// Trigger overflow so the hint starts showing
act(() => {
capturedOverflowActions.addOverflowingId('test-id');
});
await waitFor(() => {
expect(capturedUIState.showIsExpandableHint).toBe(true);
});
// Advance half the duration
act(() => {
vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS / 2);
});
expect(capturedUIState.showIsExpandableHint).toBe(true);
// First toggle 'on' (expanded)
act(() => {
stdin.write('\x0f'); // Ctrl+O
});
await waitFor(() => {
expect(capturedUIState.constrainHeight).toBe(false);
});
// Wait 1 second
act(() => {
vi.advanceTimersByTime(1000);
});
expect(capturedUIState.showIsExpandableHint).toBe(true);
// Second toggle 'off' (collapsed)
act(() => {
stdin.write('\x0f'); // Ctrl+O
});
await waitFor(() => {
expect(capturedUIState.constrainHeight).toBe(true);
});
// Wait 1 second
act(() => {
vi.advanceTimersByTime(1000);
});
expect(capturedUIState.showIsExpandableHint).toBe(true);
// Third toggle 'on' (expanded)
act(() => {
stdin.write('\x0f'); // Ctrl+O
});
await waitFor(() => {
expect(capturedUIState.constrainHeight).toBe(false);
});
// Now we wait just before the timeout from the LAST toggle.
// It should still be true.
act(() => {
vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS - 100);
});
expect(capturedUIState.showIsExpandableHint).toBe(true);
// Wait 0.1s more to hit exactly the timeout since the last toggle.
// It should hide now.
act(() => {
vi.advanceTimersByTime(100);
});
await waitFor(() => {
expect(capturedUIState.showIsExpandableHint).toBe(false);
});
unmount!();
});
it('does NOT set showIsExpandableHint when overflow occurs in Alternate Buffer Mode', async () => {
const alternateSettings = mergeSettings({}, {}, {}, {}, true);
const settingsWithAlternateBuffer = {
merged: {
...alternateSettings,
ui: {
...alternateSettings.ui,
useAlternateBuffer: true,
},
},
} as unknown as LoadedSettings;
let unmount: () => void;
await act(async () => {
const result = renderAppContainer({
settings: settingsWithAlternateBuffer,
});
unmount = result.unmount;
});
await waitFor(() => expect(capturedUIState).toBeTruthy());
// Trigger overflow
act(() => {
capturedOverflowActions.addOverflowingId('test-id');
});
// Should NOT show hint because we are in Alternate Buffer Mode
expect(capturedUIState.showIsExpandableHint).toBe(false);
unmount!();
});
});
describe('Permission Handling', () => {
it('shows permission dialog when checkPermissions returns paths', async () => {
const { checkPermissions } = await import(
+10 -271
View File
@@ -41,7 +41,6 @@ import { checkPermissions } from './hooks/atCommandProcessor.js';
import { MessageType, StreamingState } from './types.js';
import { ToolActionsProvider } from './contexts/ToolActionsContext.js';
import {
type StartupWarning,
type EditorType,
type Config,
type IdeInfo,
@@ -80,8 +79,6 @@ import {
type AgentsDiscoveredPayload,
ChangeAuthRequestedError,
CoreToolCallStatus,
generateSteeringAckMessage,
buildUserSteeringHintPrompt,
} from '@google/gemini-cli-core';
import { validateAuthMethod } from '../config/auth.js';
import process from 'node:process';
@@ -95,10 +92,6 @@ import { useSettingsCommand } from './hooks/useSettingsCommand.js';
import { useModelCommand } from './hooks/useModelCommand.js';
import { useSlashCommandProcessor } from './hooks/slashCommandProcessor.js';
import { useVimMode } from './contexts/VimModeContext.js';
import {
useOverflowActions,
useOverflowState,
} from './contexts/OverflowContext.js';
import { useConsoleMessages } from './hooks/useConsoleMessages.js';
import { useTerminalSize } from './hooks/useTerminalSize.js';
import { calculatePromptWidths } from './components/InputPrompt.js';
@@ -155,7 +148,6 @@ import { useBackgroundShellManager } from './hooks/useBackgroundShellManager.js'
import {
WARNING_PROMPT_DURATION_MS,
QUEUE_ERROR_DISPLAY_DURATION_MS,
EXPAND_HINT_DURATION_MS,
} from './constants.js';
import { LoginWithGoogleRestartDialog } from './auth/LoginWithGoogleRestartDialog.js';
import { NewAgentsChoice } from './components/NewAgentsNotification.js';
@@ -164,8 +156,6 @@ import { useTerminalTheme } from './hooks/useTerminalTheme.js';
import { useTimedMessage } from './hooks/useTimedMessage.js';
import { shouldDismissShortcutsHelpOnHotkey } from './utils/shortcutsHelp.js';
import { useSuspend } from './hooks/useSuspend.js';
import { useRunEventNotifications } from './hooks/useRunEventNotifications.js';
import { isNotificationsEnabled } from '../utils/terminalNotifications.js';
function isToolExecuting(pendingHistoryItems: HistoryItemWithoutId[]) {
return pendingHistoryItems.some((item) => {
@@ -192,7 +182,7 @@ function isToolAwaitingConfirmation(
interface AppContainerProps {
config: Config;
startupWarnings?: StartupWarning[];
startupWarnings?: string[];
version: string;
initializationResult: InitializationResult;
resumedSessionData?: ResumedSessionData;
@@ -219,8 +209,6 @@ const SHELL_HEIGHT_PADDING = 10;
export const AppContainer = (props: AppContainerProps) => {
const { config, initializationResult, resumedSessionData } = props;
const settings = useSettings();
const { reset } = useOverflowActions()!;
const notificationsEnabled = isNotificationsEnabled(settings);
const historyManager = useHistory({
chatRecordingService: config.getGeminiClient()?.getChatRecordingService(),
@@ -268,54 +256,6 @@ export const AppContainer = (props: AppContainerProps) => {
);
const [newAgents, setNewAgents] = useState<AgentDefinition[] | null>(null);
const [constrainHeight, setConstrainHeight] = useState<boolean>(true);
const [showIsExpandableHint, setShowIsExpandableHint] = useState(false);
const expandHintTimerRef = useRef<NodeJS.Timeout | null>(null);
const overflowState = useOverflowState();
const overflowingIdsSize = overflowState?.overflowingIds.size ?? 0;
const hasOverflowState = overflowingIdsSize > 0 || !constrainHeight;
/**
* Manages the visibility and x-second timer for the expansion hint.
*
* This effect triggers the timer countdown whenever an overflow is detected
* or the user manually toggles the expansion state with Ctrl+O. We use a stable
* boolean dependency (hasOverflowState) to ensure the timer only resets on
* genuine state transitions, preventing it from infinitely resetting during
* active text streaming.
*/
useEffect(() => {
if (isAlternateBuffer) {
setShowIsExpandableHint(false);
if (expandHintTimerRef.current) {
clearTimeout(expandHintTimerRef.current);
}
return;
}
if (hasOverflowState) {
setShowIsExpandableHint(true);
if (expandHintTimerRef.current) {
clearTimeout(expandHintTimerRef.current);
}
expandHintTimerRef.current = setTimeout(() => {
setShowIsExpandableHint(false);
}, EXPAND_HINT_DURATION_MS);
}
}, [hasOverflowState, isAlternateBuffer, constrainHeight]);
/**
* Safe cleanup to ensure the expansion hint timer is cancelled when the
* component unmounts, preventing memory leaks.
*/
useEffect(
() => () => {
if (expandHintTimerRef.current) {
clearTimeout(expandHintTimerRef.current);
}
},
[],
);
const [defaultBannerText, setDefaultBannerText] = useState('');
const [warningBannerText, setWarningBannerText] = useState('');
@@ -754,7 +694,6 @@ export const AppContainer = (props: AppContainerProps) => {
settings.setValue(scope, 'security.auth.selectedType', authType);
try {
config.setRemoteAdminSettings(undefined);
await config.refreshAuth(authType);
setAuthState(AuthState.Authenticated);
} catch (e) {
@@ -1054,30 +993,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
}
}, [pendingRestorePrompt, inputHistory, historyManager.history]);
const pendingHintsRef = useRef<string[]>([]);
const [pendingHintCount, setPendingHintCount] = useState(0);
const consumePendingHints = useCallback(() => {
if (pendingHintsRef.current.length === 0) {
return null;
}
const hint = pendingHintsRef.current.join('\n');
pendingHintsRef.current = [];
setPendingHintCount(0);
return hint;
}, []);
useEffect(() => {
const hintListener = (hint: string) => {
pendingHintsRef.current.push(hint);
setPendingHintCount((prev) => prev + 1);
};
config.userHintService.onUserHint(hintListener);
return () => {
config.userHintService.offUserHint(hintListener);
};
}, [config]);
const {
streamingState,
submitQuery,
@@ -1116,7 +1031,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
terminalWidth,
terminalHeight,
embeddedShellFocused,
consumePendingHints,
);
toggleBackgroundShellRef.current = toggleBackgroundShell;
@@ -1225,51 +1139,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
],
);
const handleHintSubmit = useCallback(
(hint: string) => {
const trimmed = hint.trim();
if (!trimmed) {
return;
}
config.userHintService.addUserHint(trimmed);
// Render hints with a distinct style.
historyManager.addItem({
type: 'hint',
text: trimmed,
});
},
[config, historyManager],
);
const handleFinalSubmit = useCallback(
async (submittedValue: string) => {
reset();
// Explicitly hide the expansion hint and clear its x-second timer when a new turn begins.
setShowIsExpandableHint(false);
if (expandHintTimerRef.current) {
clearTimeout(expandHintTimerRef.current);
}
if (!constrainHeight) {
setConstrainHeight(true);
if (!isAlternateBuffer) {
refreshStatic();
}
}
const isSlash = isSlashCommand(submittedValue.trim());
const isIdle = streamingState === StreamingState.Idle;
const isAgentRunning =
streamingState === StreamingState.Responding ||
isToolExecuting([
...pendingSlashCommandHistoryItems,
...pendingGeminiHistoryItems,
]);
if (config.isModelSteeringEnabled() && isAgentRunning && !isSlash) {
handleHintSubmit(submittedValue);
addInput(submittedValue);
return;
}
if (isSlash || (isIdle && isMcpReady)) {
if (!isSlash) {
@@ -1311,35 +1184,15 @@ Logging in with Google... Restarting Gemini CLI to continue.
isMcpReady,
streamingState,
messageQueue.length,
pendingSlashCommandHistoryItems,
pendingGeminiHistoryItems,
config,
constrainHeight,
setConstrainHeight,
isAlternateBuffer,
refreshStatic,
reset,
handleHintSubmit,
],
);
const handleClearScreen = useCallback(() => {
reset();
// Explicitly hide the expansion hint and clear its x-second timer when clearing the screen.
setShowIsExpandableHint(false);
if (expandHintTimerRef.current) {
clearTimeout(expandHintTimerRef.current);
}
historyManager.clearItems();
clearConsoleMessagesState();
refreshStatic();
}, [
historyManager,
clearConsoleMessagesState,
refreshStatic,
reset,
setShowIsExpandableHint,
]);
}, [historyManager, clearConsoleMessagesState, refreshStatic]);
const { handleInput: vimHandleInput } = useVim(buffer, handleFinalSubmit);
@@ -1394,7 +1247,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
sanitizationConfig: config.sanitizationConfig,
});
const { isFocused, hasReceivedFocusEvent } = useFocus();
const isFocused = useFocus();
// Context file names computation
const contextFileNames = useMemo(() => {
@@ -1509,7 +1362,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
windowMs: WARNING_PROMPT_DURATION_MS,
onRepeat: handleExitRepeat,
});
const [constrainHeight, setConstrainHeight] = useState<boolean>(true);
const [ideContextState, setIdeContextState] = useState<
IdeContext | undefined
>();
@@ -1521,18 +1374,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
type: TransientMessageType;
}>(WARNING_PROMPT_DURATION_MS);
const {
isFolderTrustDialogOpen,
discoveryResults: folderDiscoveryResults,
handleFolderTrustSelect,
isRestarting,
} = useFolderTrust(settings, setIsTrustedFolder, historyManager.addItem);
const policyUpdateConfirmationRequest =
config.getPolicyUpdateConfirmationRequest();
const [isPolicyUpdateDialogOpen, setIsPolicyUpdateDialogOpen] = useState(
!!policyUpdateConfirmationRequest,
);
const { isFolderTrustDialogOpen, handleFolderTrustSelect, isRestarting } =
useFolderTrust(settings, setIsTrustedFolder, historyManager.addItem);
const {
needsRestart: ideNeedsRestart,
restartReason: ideTrustRestartReason,
@@ -1691,8 +1534,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
streamingState,
shouldShowFocusHint,
retryStatus,
loadingPhrasesMode: settings.merged.ui.loadingPhrases,
customWittyPhrases: settings.merged.ui.customWittyPhrases,
});
const handleGlobalKeypress = useCallback(
@@ -1739,19 +1580,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
if (!constrainHeight) {
enteringConstrainHeightMode = true;
setConstrainHeight(true);
if (keyMatchers[Command.SHOW_MORE_LINES](key)) {
// If the user manually collapses the view, show the hint and reset the x-second timer.
setShowIsExpandableHint(true);
if (expandHintTimerRef.current) {
clearTimeout(expandHintTimerRef.current);
}
expandHintTimerRef.current = setTimeout(() => {
setShowIsExpandableHint(false);
}, EXPAND_HINT_DURATION_MS);
}
if (!isAlternateBuffer) {
refreshStatic();
}
}
if (keyMatchers[Command.SHOW_ERROR_DETAILS](key)) {
@@ -1795,17 +1623,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
!enteringConstrainHeightMode
) {
setConstrainHeight(false);
// If the user manually expands the view, show the hint and reset the x-second timer.
setShowIsExpandableHint(true);
if (expandHintTimerRef.current) {
clearTimeout(expandHintTimerRef.current);
}
expandHintTimerRef.current = setTimeout(() => {
setShowIsExpandableHint(false);
}, EXPAND_HINT_DURATION_MS);
if (!isAlternateBuffer) {
refreshStatic();
}
return true;
} else if (
(keyMatchers[Command.FOCUS_SHELL_INPUT](key) ||
@@ -2029,7 +1846,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
(shouldShowRetentionWarning && retentionCheckComplete) ||
shouldShowIdePrompt ||
isFolderTrustDialogOpen ||
isPolicyUpdateDialogOpen ||
adminSettingsChanged ||
!!commandConfirmationRequest ||
!!authConsentRequest ||
@@ -2063,17 +1879,12 @@ Logging in with Google... Restarting Gemini CLI to continue.
[pendingHistoryItems],
);
const hasConfirmUpdateExtensionRequests =
confirmUpdateExtensionRequests.length > 0;
const hasLoopDetectionConfirmationRequest =
!!loopDetectionConfirmationRequest;
const hasPendingActionRequired =
hasPendingToolConfirmation ||
!!commandConfirmationRequest ||
!!authConsentRequest ||
hasConfirmUpdateExtensionRequests ||
hasLoopDetectionConfirmationRequest ||
confirmUpdateExtensionRequests.length > 0 ||
!!loopDetectionConfirmationRequest ||
!!proQuotaRequest ||
!!validationRequest ||
!!customDialog;
@@ -2091,20 +1902,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
allowPlanMode,
});
useRunEventNotifications({
notificationsEnabled,
isFocused,
hasReceivedFocusEvent,
streamingState,
hasPendingActionRequired,
pendingHistoryItems,
commandConfirmationRequest,
authConsentRequest,
permissionConfirmationRequest,
hasConfirmUpdateExtensionRequests,
hasLoopDetectionConfirmationRequest,
});
const isPassiveShortcutsHelpState =
isInputActive &&
streamingState === StreamingState.Idle &&
@@ -2120,44 +1917,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
setShortcutsHelpVisible,
]);
useEffect(() => {
if (
!isConfigInitialized ||
!config.isModelSteeringEnabled() ||
streamingState !== StreamingState.Idle ||
!isMcpReady ||
isToolAwaitingConfirmation(pendingHistoryItems)
) {
return;
}
const pendingHint = consumePendingHints();
if (!pendingHint) {
return;
}
void generateSteeringAckMessage(
config.getBaseLlmClient(),
pendingHint,
).then((ackText) => {
historyManager.addItem({
type: 'info',
text: ackText,
});
});
void submitQuery([{ text: buildUserSteeringHintPrompt(pendingHint) }]);
}, [
config,
historyManager,
isConfigInitialized,
isMcpReady,
streamingState,
submitQuery,
consumePendingHints,
pendingHistoryItems,
pendingHintCount,
]);
const allToolCalls = useMemo(
() =>
pendingHistoryItems
@@ -2186,7 +1945,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
const fetchBannerTexts = async () => {
const [defaultBanner, warningBanner] = await Promise.all([
config.getBannerTextNoCapacityIssues(),
// TODO: temporarily disabling the banner, it will be re-added.
'',
config.getBannerTextCapacityIssues(),
]);
@@ -2256,9 +2016,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
isResuming,
shouldShowIdePrompt,
isFolderTrustDialogOpen: isFolderTrustDialogOpen ?? false,
folderDiscoveryResults,
isPolicyUpdateDialogOpen,
policyUpdateConfirmationRequest,
isTrustedFolder,
constrainHeight,
showErrorDetails,
@@ -2326,14 +2083,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
isBackgroundShellListOpen,
adminSettingsChanged,
newAgents,
showIsExpandableHint,
hintMode:
config.isModelSteeringEnabled() &&
isToolExecuting([
...pendingSlashCommandHistoryItems,
...pendingGeminiHistoryItems,
]),
hintBuffer: '',
}),
[
isThemeDialogOpen,
@@ -2382,9 +2131,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
isResuming,
shouldShowIdePrompt,
isFolderTrustDialogOpen,
folderDiscoveryResults,
isPolicyUpdateDialogOpen,
policyUpdateConfirmationRequest,
isTrustedFolder,
constrainHeight,
showErrorDetails,
@@ -2453,7 +2199,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
backgroundShells,
adminSettingsChanged,
newAgents,
showIsExpandableHint,
],
);
@@ -2483,7 +2228,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
vimHandleInput,
handleIdePromptComplete,
handleFolderTrustSelect,
setIsPolicyUpdateDialogOpen,
setConstrainHeight,
onEscapePromptChange: handleEscapePromptChange,
refreshStatic,
@@ -2510,10 +2254,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
setActiveBackgroundShellPid,
setIsBackgroundShellListOpen,
setAuthContext,
onHintInput: () => {},
onHintBackspace: () => {},
onHintClear: () => {},
onHintSubmit: () => {},
handleRestart: async () => {
if (process.send) {
const remoteSettings = config.getRemoteAdminSettings();
@@ -2568,7 +2308,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
vimHandleInput,
handleIdePromptComplete,
handleFolderTrustSelect,
setIsPolicyUpdateDialogOpen,
setConstrainHeight,
handleEscapePromptChange,
refreshStatic,
@@ -11,6 +11,8 @@ import { IdeIntegrationNudge } from './IdeIntegrationNudge.js';
import { KeypressProvider } from './contexts/KeypressContext.js';
import { debugLogger } from '@google/gemini-cli-core';
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
// Mock debugLogger
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
@@ -54,129 +56,128 @@ describe('IdeIntegrationNudge', () => {
});
it('renders correctly with default options', async () => {
const { lastFrame, waitUntilReady, unmount } = render(
const { lastFrame } = render(
<KeypressProvider>
<IdeIntegrationNudge {...defaultProps} />
</KeypressProvider>,
);
await waitUntilReady();
await act(async () => {
await delay(100);
});
const frame = lastFrame();
expect(frame).toContain('Do you want to connect VS Code to Gemini CLI?');
expect(frame).toContain('Yes');
expect(frame).toContain('No (esc)');
expect(frame).toContain("No, don't ask again");
unmount();
});
it('handles "Yes" selection', async () => {
const onComplete = vi.fn();
const { stdin, waitUntilReady, unmount } = render(
const { stdin } = render(
<KeypressProvider>
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />
</KeypressProvider>,
);
await waitUntilReady();
await act(async () => {
await delay(100);
});
// "Yes" is the first option and selected by default usually.
await act(async () => {
stdin.write('\r');
await delay(100);
});
await waitUntilReady();
expect(onComplete).toHaveBeenCalledWith({
userSelection: 'yes',
isExtensionPreInstalled: false,
});
unmount();
});
it('handles "No" selection', async () => {
const onComplete = vi.fn();
const { stdin, waitUntilReady, unmount } = render(
const { stdin } = render(
<KeypressProvider>
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />
</KeypressProvider>,
);
await waitUntilReady();
await act(async () => {
await delay(100);
});
// Navigate down to "No (esc)"
await act(async () => {
stdin.write('\u001B[B'); // Down arrow
await delay(100);
});
await waitUntilReady();
await act(async () => {
stdin.write('\r'); // Enter
await delay(100);
});
await waitUntilReady();
expect(onComplete).toHaveBeenCalledWith({
userSelection: 'no',
isExtensionPreInstalled: false,
});
unmount();
});
it('handles "Dismiss" selection', async () => {
const onComplete = vi.fn();
const { stdin, waitUntilReady, unmount } = render(
const { stdin } = render(
<KeypressProvider>
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />
</KeypressProvider>,
);
await waitUntilReady();
await act(async () => {
await delay(100);
});
// Navigate down to "No, don't ask again"
await act(async () => {
stdin.write('\u001B[B'); // Down arrow
await delay(100);
});
await waitUntilReady();
await act(async () => {
stdin.write('\u001B[B'); // Down arrow
await delay(100);
});
await waitUntilReady();
await act(async () => {
stdin.write('\r'); // Enter
await delay(100);
});
await waitUntilReady();
expect(onComplete).toHaveBeenCalledWith({
userSelection: 'dismiss',
isExtensionPreInstalled: false,
});
unmount();
});
it('handles Escape key press', async () => {
const onComplete = vi.fn();
const { stdin, waitUntilReady, unmount } = render(
const { stdin } = render(
<KeypressProvider>
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />
</KeypressProvider>,
);
await waitUntilReady();
await act(async () => {
await delay(100);
});
// Press Escape
await act(async () => {
stdin.write('\u001B');
});
// Escape key has a timeout in KeypressContext, so we need to wrap waitUntilReady in act
await act(async () => {
await waitUntilReady();
await delay(100);
});
expect(onComplete).toHaveBeenCalledWith({
userSelection: 'no',
isExtensionPreInstalled: false,
});
unmount();
});
it('displays correct text and handles selection when extension is pre-installed', async () => {
@@ -184,13 +185,15 @@ describe('IdeIntegrationNudge', () => {
vi.stubEnv('GEMINI_CLI_IDE_WORKSPACE_PATH', '/tmp');
const onComplete = vi.fn();
const { lastFrame, stdin, waitUntilReady, unmount } = render(
const { lastFrame, stdin } = render(
<KeypressProvider>
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />
</KeypressProvider>,
);
await waitUntilReady();
await act(async () => {
await delay(100);
});
const frame = lastFrame();
@@ -202,13 +205,12 @@ describe('IdeIntegrationNudge', () => {
// Select "Yes"
await act(async () => {
stdin.write('\r');
await delay(100);
});
await waitUntilReady();
expect(onComplete).toHaveBeenCalledWith({
userSelection: 'yes',
isExtensionPreInstalled: true,
});
unmount();
});
});
@@ -61,8 +61,7 @@ Tips for getting started:
2. Be specific for the best results.
3. Create GEMINI.md files to customize your interactions with Gemini.
4. /help for more information.
Composer
"
Composer"
`;
exports[`App > Snapshots > renders with dialogs visible 1`] = `
@@ -125,19 +124,19 @@ Tips for getting started:
3. Create GEMINI.md files to customize your interactions with Gemini.
4. /help for more information.
HistoryItemDisplay
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Action Required │
│ │
│ ? ls list directory │
│ │
│ ls │
│ Allow execution of: 'ls'? │
│ │
│ ● 1. Allow once │
│ 2. Allow for this session │
│ 3. No, suggest changes (esc) │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
│ Action Required
│ ? ls list directory
│ ls
│ Allow execution of: 'ls'?
│ ● 1. Allow once
│ 2. Allow for this session
│ 3. No, suggest changes (esc)
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
@@ -65,24 +65,21 @@ describe('ApiAuthDialog', () => {
mockedUseTextBuffer.mockReturnValue(mockBuffer);
});
it('renders correctly', async () => {
const { lastFrame, waitUntilReady, unmount } = render(
it('renders correctly', () => {
const { lastFrame } = render(
<ApiAuthDialog onSubmit={onSubmit} onCancel={onCancel} />,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders with a defaultValue', async () => {
const { waitUntilReady, unmount } = render(
it('renders with a defaultValue', () => {
render(
<ApiAuthDialog
onSubmit={onSubmit}
onCancel={onCancel}
defaultValue="test-key"
/>,
);
await waitUntilReady();
expect(mockedUseTextBuffer).toHaveBeenCalledWith(
expect.objectContaining({
initialText: 'test-key',
@@ -91,7 +88,6 @@ describe('ApiAuthDialog', () => {
}),
}),
);
unmount();
});
it.each([
@@ -104,12 +100,9 @@ describe('ApiAuthDialog', () => {
{ keyName: 'escape', sequence: '\u001b', expectedCall: onCancel, args: [] },
])(
'calls $expectedCall.name when $keyName is pressed',
async ({ keyName, sequence, expectedCall, args }) => {
({ keyName, sequence, expectedCall, args }) => {
mockBuffer.text = 'submitted-key'; // Set for the onSubmit case
const { waitUntilReady, unmount } = render(
<ApiAuthDialog onSubmit={onSubmit} onCancel={onCancel} />,
);
await waitUntilReady();
render(<ApiAuthDialog onSubmit={onSubmit} onCancel={onCancel} />);
// calls[0] is the ApiAuthDialog's useKeypress (Ctrl+C handler)
// calls[1] is the TextInput's useKeypress (typing handler)
const keypressHandler = mockedUseKeypress.mock.calls[1][0];
@@ -124,29 +117,23 @@ describe('ApiAuthDialog', () => {
});
expect(expectedCall).toHaveBeenCalledWith(...args);
unmount();
},
);
it('displays an error message', async () => {
const { lastFrame, waitUntilReady, unmount } = render(
it('displays an error message', () => {
const { lastFrame } = render(
<ApiAuthDialog
onSubmit={onSubmit}
onCancel={onCancel}
error="Invalid API Key"
/>,
);
await waitUntilReady();
expect(lastFrame()).toContain('Invalid API Key');
unmount();
});
it('calls clearApiKey and clears buffer when Ctrl+C is pressed', async () => {
const { waitUntilReady, unmount } = render(
<ApiAuthDialog onSubmit={onSubmit} onCancel={onCancel} />,
);
await waitUntilReady();
render(<ApiAuthDialog onSubmit={onSubmit} onCancel={onCancel} />);
// Call 0 is ApiAuthDialog (isActive: true)
// Call 1 is TextInput (isActive: true, priority: true)
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
@@ -162,6 +149,5 @@ describe('ApiAuthDialog', () => {
expect(clearApiKey).toHaveBeenCalled();
expect(mockBuffer.setText).toHaveBeenCalledWith('');
});
unmount();
});
});
+27 -95
View File
@@ -139,14 +139,11 @@ describe('AuthDialog', () => {
},
])(
'correctly shows/hides COMPUTE_ADC options $desc',
async ({ env, shouldContain, shouldNotContain }) => {
({ env, shouldContain, shouldNotContain }) => {
for (const [key, value] of Object.entries(env)) {
vi.stubEnv(key, value as string);
}
const { waitUntilReady, unmount } = renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
renderWithProviders(<AuthDialog {...props} />);
const items = mockedRadioButtonSelect.mock.calls[0][0].items;
for (const item of shouldContain) {
expect(items).toContainEqual(item);
@@ -154,32 +151,23 @@ describe('AuthDialog', () => {
for (const item of shouldNotContain) {
expect(items).not.toContainEqual(item);
}
unmount();
},
);
});
it('filters auth types when enforcedType is set', async () => {
it('filters auth types when enforcedType is set', () => {
props.settings.merged.security.auth.enforcedType = AuthType.USE_GEMINI;
const { waitUntilReady, unmount } = renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
renderWithProviders(<AuthDialog {...props} />);
const items = mockedRadioButtonSelect.mock.calls[0][0].items;
expect(items).toHaveLength(1);
expect(items[0].value).toBe(AuthType.USE_GEMINI);
unmount();
});
it('sets initial index to 0 when enforcedType is set', async () => {
it('sets initial index to 0 when enforcedType is set', () => {
props.settings.merged.security.auth.enforcedType = AuthType.USE_GEMINI;
const { waitUntilReady, unmount } = renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
renderWithProviders(<AuthDialog {...props} />);
const { initialIndex } = mockedRadioButtonSelect.mock.calls[0][0];
expect(initialIndex).toBe(0);
unmount();
});
describe('Initial Auth Type Selection', () => {
@@ -211,25 +199,18 @@ describe('AuthDialog', () => {
expected: AuthType.LOGIN_WITH_GOOGLE,
desc: 'defaults to Login with Google',
},
])('selects initial auth type $desc', async ({ setup, expected }) => {
])('selects initial auth type $desc', ({ setup, expected }) => {
setup();
const { waitUntilReady, unmount } = renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
renderWithProviders(<AuthDialog {...props} />);
const { items, initialIndex } = mockedRadioButtonSelect.mock.calls[0][0];
expect(items[initialIndex].value).toBe(expected);
unmount();
});
});
describe('handleAuthSelect', () => {
it('calls onAuthError if validation fails', async () => {
it('calls onAuthError if validation fails', () => {
mockedValidateAuthMethod.mockReturnValue('Invalid method');
const { waitUntilReady, unmount } = renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
renderWithProviders(<AuthDialog {...props} />);
const { onSelect: handleAuthSelect } =
mockedRadioButtonSelect.mock.calls[0][0];
handleAuthSelect(AuthType.USE_GEMINI);
@@ -240,15 +221,11 @@ describe('AuthDialog', () => {
);
expect(props.onAuthError).toHaveBeenCalledWith('Invalid method');
expect(props.settings.setValue).not.toHaveBeenCalled();
unmount();
});
it('sets auth context with requiresRestart: true for LOGIN_WITH_GOOGLE', async () => {
mockedValidateAuthMethod.mockReturnValue(null);
const { waitUntilReady, unmount } = renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
renderWithProviders(<AuthDialog {...props} />);
const { onSelect: handleAuthSelect } =
mockedRadioButtonSelect.mock.calls[0][0];
await handleAuthSelect(AuthType.LOGIN_WITH_GOOGLE);
@@ -256,21 +233,16 @@ describe('AuthDialog', () => {
expect(props.setAuthContext).toHaveBeenCalledWith({
requiresRestart: true,
});
unmount();
});
it('sets auth context with empty object for other auth types', async () => {
mockedValidateAuthMethod.mockReturnValue(null);
const { waitUntilReady, unmount } = renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
renderWithProviders(<AuthDialog {...props} />);
const { onSelect: handleAuthSelect } =
mockedRadioButtonSelect.mock.calls[0][0];
await handleAuthSelect(AuthType.USE_GEMINI);
expect(props.setAuthContext).toHaveBeenCalledWith({});
unmount();
});
it('skips API key dialog on initial setup if env var is present', async () => {
@@ -278,10 +250,7 @@ describe('AuthDialog', () => {
vi.stubEnv('GEMINI_API_KEY', 'test-key-from-env');
// props.settings.merged.security.auth.selectedType is undefined here, simulating initial setup
const { waitUntilReady, unmount } = renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
renderWithProviders(<AuthDialog {...props} />);
const { onSelect: handleAuthSelect } =
mockedRadioButtonSelect.mock.calls[0][0];
await handleAuthSelect(AuthType.USE_GEMINI);
@@ -289,7 +258,6 @@ describe('AuthDialog', () => {
expect(props.setAuthState).toHaveBeenCalledWith(
AuthState.Unauthenticated,
);
unmount();
});
it('skips API key dialog if env var is present but empty', async () => {
@@ -297,10 +265,7 @@ describe('AuthDialog', () => {
vi.stubEnv('GEMINI_API_KEY', ''); // Empty string
// props.settings.merged.security.auth.selectedType is undefined here
const { waitUntilReady, unmount } = renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
renderWithProviders(<AuthDialog {...props} />);
const { onSelect: handleAuthSelect } =
mockedRadioButtonSelect.mock.calls[0][0];
await handleAuthSelect(AuthType.USE_GEMINI);
@@ -308,7 +273,6 @@ describe('AuthDialog', () => {
expect(props.setAuthState).toHaveBeenCalledWith(
AuthState.Unauthenticated,
);
unmount();
});
it('shows API key dialog on initial setup if no env var is present', async () => {
@@ -316,10 +280,7 @@ describe('AuthDialog', () => {
// process.env['GEMINI_API_KEY'] is not set
// props.settings.merged.security.auth.selectedType is undefined here, simulating initial setup
const { waitUntilReady, unmount } = renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
renderWithProviders(<AuthDialog {...props} />);
const { onSelect: handleAuthSelect } =
mockedRadioButtonSelect.mock.calls[0][0];
await handleAuthSelect(AuthType.USE_GEMINI);
@@ -327,7 +288,6 @@ describe('AuthDialog', () => {
expect(props.setAuthState).toHaveBeenCalledWith(
AuthState.AwaitingApiKeyInput,
);
unmount();
});
it('skips API key dialog on re-auth if env var is present (cannot edit)', async () => {
@@ -337,10 +297,7 @@ describe('AuthDialog', () => {
props.settings.merged.security.auth.selectedType =
AuthType.LOGIN_WITH_GOOGLE;
const { waitUntilReady, unmount } = renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
renderWithProviders(<AuthDialog {...props} />);
const { onSelect: handleAuthSelect } =
mockedRadioButtonSelect.mock.calls[0][0];
await handleAuthSelect(AuthType.USE_GEMINI);
@@ -348,7 +305,6 @@ describe('AuthDialog', () => {
expect(props.setAuthState).toHaveBeenCalledWith(
AuthState.Unauthenticated,
);
unmount();
});
it('exits process for Login with Google when browser is suppressed', async () => {
@@ -360,10 +316,7 @@ describe('AuthDialog', () => {
vi.mocked(props.config.isBrowserLaunchSuppressed).mockReturnValue(true);
mockedValidateAuthMethod.mockReturnValue(null);
const { waitUntilReady, unmount } = renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
renderWithProviders(<AuthDialog {...props} />);
const { onSelect: handleAuthSelect } =
mockedRadioButtonSelect.mock.calls[0][0];
await act(async () => {
@@ -377,18 +330,13 @@ describe('AuthDialog', () => {
exitSpy.mockRestore();
logSpy.mockRestore();
vi.useRealTimers();
unmount();
});
});
it('displays authError when provided', async () => {
it('displays authError when provided', () => {
props.authError = 'Something went wrong';
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
const { lastFrame } = renderWithProviders(<AuthDialog {...props} />);
expect(lastFrame()).toContain('Something went wrong');
unmount();
});
describe('useKeypress', () => {
@@ -427,47 +375,31 @@ describe('AuthDialog', () => {
expect(p.settings.setValue).not.toHaveBeenCalled();
},
},
])('$desc', async ({ setup, expectations }) => {
])('$desc', ({ setup, expectations }) => {
setup();
const { waitUntilReady, unmount } = renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
renderWithProviders(<AuthDialog {...props} />);
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
keypressHandler({ name: 'escape' });
expectations(props);
unmount();
});
});
describe('Snapshots', () => {
it('renders correctly with default props', async () => {
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
it('renders correctly with default props', () => {
const { lastFrame } = renderWithProviders(<AuthDialog {...props} />);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders correctly with auth error', async () => {
it('renders correctly with auth error', () => {
props.authError = 'Something went wrong';
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
const { lastFrame } = renderWithProviders(<AuthDialog {...props} />);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders correctly with enforced auth type', async () => {
it('renders correctly with enforced auth type', () => {
props.settings.merged.security.auth.enforcedType = AuthType.USE_GEMINI;
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
<AuthDialog {...props} />,
);
await waitUntilReady();
const { lastFrame } = renderWithProviders(<AuthDialog {...props} />);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
});
});
@@ -54,80 +54,48 @@ describe('AuthInProgress', () => {
vi.useRealTimers();
});
it('renders initial state with spinner', async () => {
const { lastFrame, waitUntilReady, unmount } = render(
<AuthInProgress onTimeout={onTimeout} />,
);
await waitUntilReady();
it('renders initial state with spinner', () => {
const { lastFrame } = render(<AuthInProgress onTimeout={onTimeout} />);
expect(lastFrame()).toContain('[Spinner] Waiting for auth...');
expect(lastFrame()).toContain('Press ESC or CTRL+C to cancel');
unmount();
});
it('calls onTimeout when ESC is pressed', async () => {
const { waitUntilReady, unmount } = render(
<AuthInProgress onTimeout={onTimeout} />,
);
await waitUntilReady();
it('calls onTimeout when ESC is pressed', () => {
render(<AuthInProgress onTimeout={onTimeout} />);
const keypressHandler = vi.mocked(useKeypress).mock.calls[0][0];
await act(async () => {
keypressHandler({ name: 'escape' } as unknown as Key);
});
// Escape key has a 50ms timeout in KeypressContext, so we need to wrap waitUntilReady in act
await act(async () => {
await waitUntilReady();
});
keypressHandler({ name: 'escape' } as unknown as Key);
expect(onTimeout).toHaveBeenCalled();
unmount();
});
it('calls onTimeout when Ctrl+C is pressed', async () => {
const { waitUntilReady, unmount } = render(
<AuthInProgress onTimeout={onTimeout} />,
);
await waitUntilReady();
it('calls onTimeout when Ctrl+C is pressed', () => {
render(<AuthInProgress onTimeout={onTimeout} />);
const keypressHandler = vi.mocked(useKeypress).mock.calls[0][0];
await act(async () => {
keypressHandler({ name: 'c', ctrl: true } as unknown as Key);
});
await waitUntilReady();
keypressHandler({ name: 'c', ctrl: true } as unknown as Key);
expect(onTimeout).toHaveBeenCalled();
unmount();
});
it('calls onTimeout and shows timeout message after 3 minutes', async () => {
const { lastFrame, waitUntilReady, unmount } = render(
<AuthInProgress onTimeout={onTimeout} />,
);
await waitUntilReady();
const { lastFrame } = render(<AuthInProgress onTimeout={onTimeout} />);
await act(async () => {
vi.advanceTimersByTime(180000);
});
await waitUntilReady();
expect(onTimeout).toHaveBeenCalled();
expect(lastFrame()).toContain('Authentication timed out');
unmount();
await vi.waitUntil(
() => lastFrame()?.includes('Authentication timed out'),
{ timeout: 1000 },
);
});
it('clears timer on unmount', async () => {
const { waitUntilReady, unmount } = render(
<AuthInProgress onTimeout={onTimeout} />,
);
await waitUntilReady();
await act(async () => {
it('clears timer on unmount', () => {
const { unmount } = render(<AuthInProgress onTimeout={onTimeout} />);
act(() => {
unmount();
});
await act(async () => {
vi.advanceTimersByTime(180000);
});
vi.advanceTimersByTime(180000);
expect(onTimeout).not.toHaveBeenCalled();
});
});
@@ -40,26 +40,23 @@ describe('LoginWithGoogleRestartDialog', () => {
vi.useRealTimers();
});
it('renders correctly', async () => {
const { lastFrame, waitUntilReady, unmount } = render(
it('renders correctly', () => {
const { lastFrame } = render(
<LoginWithGoogleRestartDialog
onDismiss={onDismiss}
config={mockConfig}
/>,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('calls onDismiss when escape is pressed', async () => {
const { waitUntilReady, unmount } = render(
it('calls onDismiss when escape is pressed', () => {
render(
<LoginWithGoogleRestartDialog
onDismiss={onDismiss}
config={mockConfig}
/>,
);
await waitUntilReady();
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
keypressHandler({
@@ -71,7 +68,6 @@ describe('LoginWithGoogleRestartDialog', () => {
});
expect(onDismiss).toHaveBeenCalledTimes(1);
unmount();
});
it.each(['r', 'R'])(
@@ -79,13 +75,12 @@ describe('LoginWithGoogleRestartDialog', () => {
async (keyName) => {
vi.useFakeTimers();
const { waitUntilReady, unmount } = render(
render(
<LoginWithGoogleRestartDialog
onDismiss={onDismiss}
config={mockConfig}
/>,
);
await waitUntilReady();
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
keypressHandler({
@@ -103,7 +98,6 @@ describe('LoginWithGoogleRestartDialog', () => {
expect(exitSpy).toHaveBeenCalledWith(RELAUNCH_EXIT_CODE);
vi.useRealTimers();
unmount();
},
);
});
@@ -14,6 +14,5 @@ exports[`ApiAuthDialog > renders correctly 1`] = `
│ │
│ (Press Enter to submit, Esc to cancel, Ctrl+C to clear stored key) │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;

Some files were not shown because too many files have changed in this diff Show More