mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-27 02:01:00 -07:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d952b20532 | |||
| 5f1a51fd77 | |||
| c43500c50b | |||
| 42a7d89999 | |||
| 04f65f3d55 | |||
| 04c52513e7 | |||
| c62340675a | |||
| ef65498031 | |||
| 261788cf91 | |||
| 012392ad0a | |||
| 037061e2e0 | |||
| 221ea360b9 | |||
| 8910b2720f | |||
| 87f5dd15d6 | |||
| 81c8893e05 | |||
| 178388d931 | |||
| e7f97dfa44 | |||
| f961e0d6b1 | |||
| 14415316c0 | |||
| 1cf05b0375 | |||
| 3099df1b7c | |||
| 758d419e33 | |||
| 78de533c48 | |||
| 8f6a711a3a | |||
| 65ad78b9c0 | |||
| 858918fe31 | |||
| 9255e69abb | |||
| e32111e609 | |||
| 2c1d6f8029 | |||
| 8a8826654c | |||
| 5f6b7c0158 | |||
| 65e0043fbf | |||
| 22763c98b0 | |||
| 05be2b51fc | |||
| f1aa38b258 | |||
| 4e11ddbf4a |
@@ -1,64 +1,142 @@
|
||||
# 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.
|
||||
|
||||
@@ -23,6 +23,8 @@ 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
|
||||
@@ -65,6 +67,8 @@ 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
|
||||
|
||||
@@ -32,6 +32,7 @@ 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'
|
||||
@@ -42,7 +43,6 @@ 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,10 +50,11 @@ 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
|
||||
|
||||
@@ -46,6 +46,7 @@ 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
@@ -546,7 +546,7 @@ Before submitting your documentation pull request, please:
|
||||
|
||||
If you have questions about contributing documentation:
|
||||
|
||||
- Check our [FAQ](/docs/resources/faq).
|
||||
- Check our [FAQ](/docs/faq.md).
|
||||
- Review existing documentation for examples.
|
||||
- Open [an issue](https://github.com/google-gemini/gemini-cli/issues) to discuss
|
||||
your proposed changes.
|
||||
|
||||
@@ -47,7 +47,13 @@ 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.)
|
||||
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.)
|
||||
- **Individual Checks:** `npm run lint` / `npm run format` / `npm run typecheck`
|
||||
|
||||
## Development Conventions
|
||||
|
||||
@@ -18,7 +18,26 @@ on GitHub.
|
||||
| [Preview](preview.md) | Experimental features ready for early feedback. |
|
||||
| [Stable](latest.md) | Stable, recommended for general use. |
|
||||
|
||||
## Announcements: v0.28.0 - 2026-02-03
|
||||
## 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
|
||||
|
||||
- **Slash Command:** We've added a new `/prompt-suggest` slash command to help
|
||||
you generate prompt suggestions
|
||||
@@ -276,8 +295,7 @@ on GitHub.
|
||||
- **Experimental permission improvements:** We are now experimenting with a new
|
||||
policy engine in Gemini CLI. This allows users and administrators to create
|
||||
fine-grained policy for tool calls. Currently behind a flag. See
|
||||
[policy engine documentation](../reference/policy-engine.md) for more
|
||||
information.
|
||||
[policy engine documentation](../core/policy-engine.md) for more information.
|
||||
- Blog:
|
||||
[https://allen.hutchison.org/2025/11/26/the-guardrails-of-autonomy/](https://allen.hutchison.org/2025/11/26/the-guardrails-of-autonomy/)
|
||||
- **Gemini 3 support for paid:** Gemini 3 support has been rolled out to all API
|
||||
|
||||
+359
-293
@@ -1,6 +1,6 @@
|
||||
# Latest stable release: v0.28.0
|
||||
# Latest stable release: v0.29.0
|
||||
|
||||
Released: February 10, 2026
|
||||
Released: February 17, 2026
|
||||
|
||||
For most users, our latest stable release is the recommended release. Install
|
||||
the latest stable version with:
|
||||
@@ -11,305 +11,371 @@ npm install -g @google/gemini-cli
|
||||
|
||||
## Highlights
|
||||
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
## What's Changed
|
||||
|
||||
- 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
|
||||
- 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
|
||||
[#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
|
||||
[#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
|
||||
@abhipatel12 in
|
||||
[#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
|
||||
[#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
|
||||
[#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
|
||||
[#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
|
||||
[#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
|
||||
[#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
|
||||
[#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
|
||||
[#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
|
||||
@adamfweidman in
|
||||
[#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)
|
||||
[#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)
|
||||
|
||||
**Full Changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.27.0...v0.28.0
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.28.2...v0.29.0
|
||||
|
||||
+296
-349
@@ -1,6 +1,6 @@
|
||||
# Preview release: Release v0.29.0-preview.0
|
||||
# Preview release: v0.30.0-preview.1
|
||||
|
||||
Released: February 10, 2026
|
||||
Released: February 19, 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,355 +13,302 @@ npm install -g @google/gemini-cli@preview
|
||||
|
||||
## Highlights
|
||||
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
## 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
|
||||
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)
|
||||
- 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)
|
||||
|
||||
**Full changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.28.0-preview.0...v0.29.0-preview.0
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.29.0-preview.5...v0.30.0-preview.1
|
||||
|
||||
+23
-23
@@ -26,29 +26,29 @@ and parameters.
|
||||
|
||||
## CLI Options
|
||||
|
||||
| Option | Alias | Type | Default | Description |
|
||||
| -------------------------------- | ----- | ------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--debug` | `-d` | boolean | `false` | Run in debug mode with verbose logging |
|
||||
| `--version` | `-v` | - | - | Show CLI version number and exit |
|
||||
| `--help` | `-h` | - | - | Show help information |
|
||||
| `--model` | `-m` | string | `auto` | Model to use. See [Model Selection](#model-selection) for available values. |
|
||||
| `--prompt` | `-p` | string | - | Prompt text. Appended to stdin input if provided. **Deprecated:** Use positional arguments instead. |
|
||||
| `--prompt-interactive` | `-i` | string | - | Execute prompt and continue in interactive mode |
|
||||
| `--sandbox` | `-s` | boolean | `false` | Run in a sandboxed environment for safer execution |
|
||||
| `--approval-mode` | - | string | `default` | Approval mode for tool execution. Choices: `default`, `auto_edit`, `yolo` |
|
||||
| `--yolo` | `-y` | boolean | `false` | **Deprecated.** Auto-approve all actions. Use `--approval-mode=yolo` instead. |
|
||||
| `--experimental-acp` | - | boolean | - | Start in ACP (Agent Code Pilot) mode. **Experimental feature.** |
|
||||
| `--experimental-zed-integration` | - | boolean | - | Run in Zed editor integration mode. **Experimental feature.** |
|
||||
| `--allowed-mcp-server-names` | - | array | - | Allowed MCP server names (comma-separated or multiple flags) |
|
||||
| `--allowed-tools` | - | array | - | **Deprecated.** Use the [Policy Engine](../reference/policy-engine.md) instead. Tools that are allowed to run without confirmation (comma-separated or multiple flags) |
|
||||
| `--extensions` | `-e` | array | - | List of extensions to use. If not provided, all extensions are enabled (comma-separated or multiple flags) |
|
||||
| `--list-extensions` | `-l` | boolean | - | List all available extensions and exit |
|
||||
| `--resume` | `-r` | string | - | Resume a previous session. Use `"latest"` for most recent or index number (e.g. `--resume 5`) |
|
||||
| `--list-sessions` | - | boolean | - | List available sessions for the current project and exit |
|
||||
| `--delete-session` | - | string | - | Delete a session by index number (use `--list-sessions` to see available sessions) |
|
||||
| `--include-directories` | - | array | - | Additional directories to include in the workspace (comma-separated or multiple flags) |
|
||||
| `--screen-reader` | - | boolean | - | Enable screen reader mode for accessibility |
|
||||
| `--output-format` | `-o` | string | `text` | The format of the CLI output. Choices: `text`, `json`, `stream-json` |
|
||||
| Option | Alias | Type | Default | Description |
|
||||
| -------------------------------- | ----- | ------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--debug` | `-d` | boolean | `false` | Run in debug mode with verbose logging |
|
||||
| `--version` | `-v` | - | - | Show CLI version number and exit |
|
||||
| `--help` | `-h` | - | - | Show help information |
|
||||
| `--model` | `-m` | string | `auto` | Model to use. See [Model Selection](#model-selection) for available values. |
|
||||
| `--prompt` | `-p` | string | - | Prompt text. Appended to stdin input if provided. **Deprecated:** Use positional arguments instead. |
|
||||
| `--prompt-interactive` | `-i` | string | - | Execute prompt and continue in interactive mode |
|
||||
| `--sandbox` | `-s` | boolean | `false` | Run in a sandboxed environment for safer execution |
|
||||
| `--approval-mode` | - | string | `default` | Approval mode for tool execution. Choices: `default`, `auto_edit`, `yolo` |
|
||||
| `--yolo` | `-y` | boolean | `false` | **Deprecated.** Auto-approve all actions. Use `--approval-mode=yolo` instead. |
|
||||
| `--experimental-acp` | - | boolean | - | Start in ACP (Agent Code Pilot) mode. **Experimental feature.** |
|
||||
| `--experimental-zed-integration` | - | boolean | - | Run in Zed editor integration mode. **Experimental feature.** |
|
||||
| `--allowed-mcp-server-names` | - | array | - | Allowed MCP server names (comma-separated or multiple flags) |
|
||||
| `--allowed-tools` | - | array | - | **Deprecated.** Use the [Policy Engine](../core/policy-engine.md) instead. Tools that are allowed to run without confirmation (comma-separated or multiple flags) |
|
||||
| `--extensions` | `-e` | array | - | List of extensions to use. If not provided, all extensions are enabled (comma-separated or multiple flags) |
|
||||
| `--list-extensions` | `-l` | boolean | - | List all available extensions and exit |
|
||||
| `--resume` | `-r` | string | - | Resume a previous session. Use `"latest"` for most recent or index number (e.g. `--resume 5`) |
|
||||
| `--list-sessions` | - | boolean | - | List available sessions for the current project and exit |
|
||||
| `--delete-session` | - | string | - | Delete a session by index number (use `--list-sessions` to see available sessions) |
|
||||
| `--include-directories` | - | array | - | Additional directories to include in the workspace (comma-separated or multiple flags) |
|
||||
| `--screen-reader` | - | boolean | - | Enable screen reader mode for accessibility |
|
||||
| `--output-format` | `-o` | string | `text` | The format of the CLI output. Choices: `text`, `json`, `stream-json` |
|
||||
|
||||
## Model selection
|
||||
|
||||
|
||||
@@ -217,12 +217,19 @@ 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](../reference/configuration.md).
|
||||
[CLI Configuration documentation](../get-started/configuration.md).
|
||||
|
||||
### `/model`
|
||||
|
||||
- **Description:** Opens a dialog to choose your Gemini model.
|
||||
|
||||
### `/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.
|
||||
|
||||
### `/policies`
|
||||
|
||||
- **Description:** Manage policies.
|
||||
@@ -247,7 +254,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](../reference/configuration.md). See
|
||||
[settings](../get-started/configuration.md). See
|
||||
[Checkpointing documentation](../cli/checkpointing.md) for more details.
|
||||
|
||||
### `/rewind`
|
||||
@@ -286,8 +293,7 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
settings that control the behavior and appearance of Gemini CLI. It is
|
||||
equivalent to manually editing the `.gemini/settings.json` file, but with
|
||||
validation and guidance to prevent errors. See the
|
||||
[settings documentation](../cli/settings.md) for a full list of available
|
||||
settings.
|
||||
[settings documentation](./settings.md) for a full list of available settings.
|
||||
- **Usage:** Simply run `/settings` and the editor will open. You can then
|
||||
browse or search for specific settings, view their current values, and modify
|
||||
them as desired. Changes to some settings are applied immediately, while
|
||||
@@ -374,8 +380,7 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
|
||||
Custom commands allow you to create personalized shortcuts for your most-used
|
||||
prompts. For detailed instructions on how to create, manage, and use them,
|
||||
please see the dedicated
|
||||
[Custom Commands documentation](../cli/custom-commands.md).
|
||||
please see the dedicated [Custom Commands documentation](./custom-commands.md).
|
||||
|
||||
## Input prompt shortcuts
|
||||
|
||||
@@ -20,7 +20,7 @@ The most powerful tools for enterprise administration are the system-wide
|
||||
settings files. These files allow you to define a baseline configuration
|
||||
(`system-defaults.json`) and a set of overrides (`settings.json`) that apply to
|
||||
all users on a machine. For a complete overview of configuration options, see
|
||||
the [Configuration documentation](../reference/configuration.md).
|
||||
the [Configuration documentation](../get-started/configuration.md).
|
||||
|
||||
Settings are merged from four files. The precedence order for single-value
|
||||
settings (like `theme`) is:
|
||||
@@ -224,8 +224,8 @@ gemini
|
||||
|
||||
You can significantly enhance security by controlling which tools the Gemini
|
||||
model can use. This is achieved through the `tools.core` setting and the
|
||||
[Policy Engine](../reference/policy-engine.md). For a list of available tools,
|
||||
see the [Tools documentation](../tools/index.md).
|
||||
[Policy Engine](../core/policy-engine.md). For a list of available tools, see
|
||||
the [Tools documentation](../tools/index.md).
|
||||
|
||||
### Allowlisting with `coreTools`
|
||||
|
||||
@@ -245,8 +245,8 @@ on the approved list.
|
||||
|
||||
### Blocklisting with `excludeTools` (Deprecated)
|
||||
|
||||
> **Deprecated:** Use the [Policy Engine](../reference/policy-engine.md) for
|
||||
> more robust control.
|
||||
> **Deprecated:** Use the [Policy Engine](../core/policy-engine.md) for more
|
||||
> robust control.
|
||||
|
||||
Alternatively, you can add specific tools that are considered dangerous in your
|
||||
environment to a blocklist.
|
||||
@@ -289,8 +289,8 @@ unintended tool execution.
|
||||
## Managing custom tools (MCP servers)
|
||||
|
||||
If your organization uses custom tools via
|
||||
[Model-Context Protocol (MCP) servers](../reference/tools-api.md), it is crucial
|
||||
to understand how server configurations are managed to apply security policies
|
||||
[Model-Context Protocol (MCP) servers](../core/tools-api.md), it is crucial to
|
||||
understand how server configurations are managed to apply security policies
|
||||
effectively.
|
||||
|
||||
### How MCP server configurations are merged
|
||||
|
||||
@@ -88,7 +88,7 @@ More content here.
|
||||
@../shared/style-guide.md
|
||||
```
|
||||
|
||||
For more details, see the [Memory Import Processor](../reference/memport.md)
|
||||
For more details, see the [Memory Import Processor](../core/memport.md)
|
||||
documentation.
|
||||
|
||||
## Customize the context file name
|
||||
|
||||
@@ -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` |
|
||||
| Delete the next word. | `Ctrl + Delete`<br />`Alt + Delete`<br />`Alt + D` |
|
||||
| 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` |
|
||||
| Accept a suggestion while reverse searching. | `Tab (no Shift)` |
|
||||
| Browse and rewind previous interactions. | `Double Esc` |
|
||||
|
||||
#### Navigation
|
||||
@@ -79,7 +79,7 @@ available combinations.
|
||||
|
||||
| Action | Keys |
|
||||
| --------------------------------------- | -------------------------------------------------- |
|
||||
| Accept the inline suggestion. | `Tab`<br />`Enter (no Ctrl)` |
|
||||
| Accept the inline suggestion. | `Tab (no Shift)`<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` |
|
||||
+1
-1
@@ -39,7 +39,7 @@ To enable Gemini 3 Pro and Gemini 3 Flash (if available), enable
|
||||
|
||||
You can also use the `--model` flag to specify a particular Gemini model on
|
||||
startup. For more details, refer to the
|
||||
[configuration documentation](../reference/configuration.md).
|
||||
[configuration documentation](../get-started/configuration.md).
|
||||
|
||||
Changes to these settings will be applied to all subsequent interactions with
|
||||
Gemini CLI.
|
||||
|
||||
@@ -184,7 +184,7 @@ Guide].
|
||||
[MCP tools]: /docs/tools/mcp-server.md
|
||||
[`activate_skill`]: /docs/cli/skills.md
|
||||
[experimental research sub-agents]: /docs/core/subagents.md
|
||||
[Policy Engine Guide]: /docs/reference/policy-engine.md
|
||||
[Policy Engine Guide]: /docs/core/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
|
||||
|
||||
+3
-3
@@ -167,6 +167,6 @@ gemini -s -p "run shell command: mount | grep workspace"
|
||||
|
||||
## Related documentation
|
||||
|
||||
- [Configuration](../reference/configuration.md): Full configuration options.
|
||||
- [Commands](../reference/commands.md): Available commands.
|
||||
- [Troubleshooting](../resources/troubleshooting.md): General troubleshooting.
|
||||
- [Configuration](../get-started/configuration.md): Full configuration options.
|
||||
- [Commands](./commands.md): Available commands.
|
||||
- [Troubleshooting](../troubleshooting.md): General troubleshooting.
|
||||
|
||||
@@ -27,6 +27,7 @@ they appear in the UI.
|
||||
| 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` |
|
||||
| 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` |
|
||||
@@ -49,6 +50,7 @@ they appear in the UI.
|
||||
| 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` |
|
||||
@@ -131,6 +133,7 @@ they appear in the UI.
|
||||
| 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` |
|
||||
| Model Steering | `experimental.modelSteering` | Enable model steering (user hints) to guide the model during tool execution. | `false` |
|
||||
|
||||
### Skills
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ Environment variables can be used to override the settings in the file.
|
||||
`true` or `1` will enable the feature. Any other value will disable it.
|
||||
|
||||
For detailed information about all configuration options, see the
|
||||
[Configuration guide](../reference/configuration.md).
|
||||
[Configuration guide](../get-started/configuration.md).
|
||||
|
||||
## Google Cloud telemetry
|
||||
|
||||
|
||||
+3
-3
@@ -41,8 +41,8 @@ can change the theme using the `/theme` command.
|
||||
### Theme persistence
|
||||
|
||||
Selected themes are saved in Gemini CLI's
|
||||
[configuration](../reference/configuration.md) so your preference is remembered
|
||||
across sessions.
|
||||
[configuration](../get-started/configuration.md) so your preference is
|
||||
remembered across sessions.
|
||||
|
||||
---
|
||||
|
||||
@@ -194,7 +194,7 @@ untrusted sources.
|
||||
- Or, set it as the default by adding `"theme": "MyCustomTheme"` to the `ui`
|
||||
object in your `settings.json`.
|
||||
- Custom themes can be set at the user, project, or system level, and follow the
|
||||
same [configuration precedence](../reference/configuration.md) as other
|
||||
same [configuration precedence](../get-started/configuration.md) as other
|
||||
settings.
|
||||
|
||||
### Themes from extensions
|
||||
|
||||
@@ -121,6 +121,6 @@ immediately. Force a reload with:
|
||||
|
||||
- Learn about [Session management](session-management.md) to see how short-term
|
||||
history works.
|
||||
- Explore the [Command reference](../../reference/commands.md) for more
|
||||
`/memory` options.
|
||||
- Explore the [Command reference](../../cli/commands.md) for more `/memory`
|
||||
options.
|
||||
- Read the technical spec for [Project context](../../cli/gemini-md.md).
|
||||
|
||||
@@ -101,5 +101,5 @@ This creates a new branch of history without losing your original work.
|
||||
- Learn about [Checkpointing](../../cli/checkpointing.md) to understand the
|
||||
underlying safety mechanism.
|
||||
- Explore [Task planning](task-planning.md) to keep complex sessions organized.
|
||||
- See the [Command reference](../../reference/commands.md) for all `/chat` and
|
||||
- See the [Command reference](../../cli/commands.md) for all `/chat` and
|
||||
`/resume` options.
|
||||
|
||||
+7
-7
@@ -9,11 +9,11 @@ requests sent from `packages/cli`. For a general overview of Gemini CLI, see the
|
||||
|
||||
- **[Sub-agents (experimental)](./subagents.md):** Learn how to create and use
|
||||
specialized sub-agents for complex tasks.
|
||||
- **[Core tools API](../reference/tools-api.md):** Information on how tools are
|
||||
defined, registered, and used by the core.
|
||||
- **[Memory Import Processor](../reference/memport.md):** Documentation for the
|
||||
modular GEMINI.md import feature using @file.md syntax.
|
||||
- **[Policy Engine](../reference/policy-engine.md):** Use the Policy Engine for
|
||||
- **[Core tools API](./tools-api.md):** Information on how tools are defined,
|
||||
registered, and used by the core.
|
||||
- **[Memory Import Processor](./memport.md):** Documentation for the modular
|
||||
GEMINI.md import feature using @file.md syntax.
|
||||
- **[Policy Engine](./policy-engine.md):** Use the Policy Engine for
|
||||
fine-grained control over tool execution.
|
||||
|
||||
## Role of the core
|
||||
@@ -92,8 +92,8 @@ This allows you to have global, project-level, and component-level context
|
||||
files, which are all combined to provide the model with the most relevant
|
||||
information.
|
||||
|
||||
You can use the [`/memory` command](../reference/commands.md) to `show`, `add`,
|
||||
and `refresh` the content of loaded `GEMINI.md` files.
|
||||
You can use the [`/memory` command](../cli/commands.md) to `show`, `add`, and
|
||||
`refresh` the content of loaded `GEMINI.md` files.
|
||||
|
||||
## Citations
|
||||
|
||||
|
||||
+39
-39
@@ -1,13 +1,13 @@
|
||||
# Sub-agents (experimental)
|
||||
# Subagents (experimental)
|
||||
|
||||
Sub-agents are specialized agents that operate within your main Gemini CLI
|
||||
Subagents 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: Sub-agents are currently an experimental feature.**
|
||||
> **Note: Subagents are currently an experimental feature.**
|
||||
>
|
||||
> To use custom sub-agents, you must explicitly enable them in your
|
||||
> To use custom subagents, you must explicitly enable them in your
|
||||
> `settings.json`:
|
||||
>
|
||||
> ```json
|
||||
@@ -16,31 +16,31 @@ the main agent's context or toolset.
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Warning:** Sub-agents currently operate in
|
||||
> ["YOLO mode"](../reference/configuration.md#command-line-arguments), meaning
|
||||
> **Warning:** Subagents currently operate in
|
||||
> ["YOLO mode"](../get-started/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 sub-agents?
|
||||
## What are subagents?
|
||||
|
||||
Sub-agents are "specialists" that the main Gemini agent can hire for a specific
|
||||
Subagents are "specialists" that the main Gemini agent can hire for a specific
|
||||
job.
|
||||
|
||||
- **Focused context:** Each sub-agent has its own system prompt and persona.
|
||||
- **Specialized tools:** Sub-agents can have a restricted or specialized set of
|
||||
- **Focused context:** Each subagent has its own system prompt and persona.
|
||||
- **Specialized tools:** Subagents can have a restricted or specialized set of
|
||||
tools.
|
||||
- **Independent context window:** Interactions with a sub-agent happen in a
|
||||
- **Independent context window:** Interactions with a subagent happen in a
|
||||
separate context loop, which saves tokens in your main conversation history.
|
||||
|
||||
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
|
||||
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
|
||||
findings.
|
||||
|
||||
## Built-in sub-agents
|
||||
## Built-in subagents
|
||||
|
||||
Gemini CLI comes with the following built-in sub-agents:
|
||||
Gemini CLI comes with the following built-in subagents:
|
||||
|
||||
### Codebase Investigator
|
||||
|
||||
@@ -75,15 +75,15 @@ Gemini CLI comes with the following built-in sub-agents:
|
||||
### Generalist Agent
|
||||
|
||||
- **Name:** `generalist_agent`
|
||||
- **Purpose:** Route tasks to the appropriate specialized sub-agent.
|
||||
- **Purpose:** Route tasks to the appropriate specialized subagent.
|
||||
- **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 sub-agents
|
||||
## Creating custom subagents
|
||||
|
||||
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
|
||||
You can create your own subagents to automate specific workflows or enforce
|
||||
specific personas. To use custom subagents, 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 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`. |
|
||||
| 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`. |
|
||||
|
||||
### Optimizing your sub-agent
|
||||
### Optimizing your subagent
|
||||
|
||||
The main agent's system prompt encourages it to use an expert sub-agent when one
|
||||
The main agent's system prompt encourages it to use an expert subagent 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 sub-agent description should be called fairly
|
||||
For example, the following subagent 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 sub-agent, you can do so by selecting the model
|
||||
If you need to further tune your subagent, 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 sub-agent was called with a specific prompt and the given description.
|
||||
that your subagent was called with a specific prompt and the given description.
|
||||
|
||||
## Remote subagents (Agent2Agent) (experimental)
|
||||
|
||||
Gemini CLI can also delegate tasks to remote sub-agents using the Agent-to-Agent
|
||||
Gemini CLI can also delegate tasks to remote subagents 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 sub-agents using the Agent-to-Agent
|
||||
See the [Remote Subagents documentation](/docs/core/remote-agents) for detailed
|
||||
configuration and usage instructions.
|
||||
|
||||
## Extension sub-agents
|
||||
## Extension subagents
|
||||
|
||||
Extensions can bundle and distribute sub-agents. See the
|
||||
[Extensions documentation](../extensions/index.md#sub-agents) for details on how
|
||||
Extensions can bundle and distribute subagents. See the
|
||||
[Extensions documentation](../extensions/index.md#subagents) for details on how
|
||||
to package agents within an extension.
|
||||
|
||||
@@ -130,7 +130,7 @@ The manifest file defines the extension's behavior and configuration.
|
||||
- `description`: A short summary shown in the extension gallery.
|
||||
- <a id="mcp-servers"></a>`mcpServers`: A map of Model Context Protocol (MCP)
|
||||
servers. Extension servers follow the same format as standard
|
||||
[CLI configuration](../reference/configuration.md).
|
||||
[CLI configuration](../get-started/configuration.md).
|
||||
- `contextFileName`: The name of the context file (defaults to `GEMINI.md`). Can
|
||||
also be an array of strings to load multiple context files.
|
||||
- `excludeTools`: An array of tools to block from the model. You can restrict
|
||||
|
||||
@@ -104,7 +104,7 @@ The Gemini CLI configuration is stored in two `settings.json` files:
|
||||
1. In your home directory: `~/.gemini/settings.json`.
|
||||
2. In your project's root directory: `./.gemini/settings.json`.
|
||||
|
||||
Refer to [Gemini CLI Configuration](../reference/configuration.md) for more
|
||||
Refer to [Gemini CLI Configuration](./get-started/configuration.md) for more
|
||||
details.
|
||||
|
||||
## Google AI Pro/Ultra and subscription FAQs
|
||||
@@ -22,8 +22,8 @@ Select the authentication method that matches your situation in the table below:
|
||||
### What is my Google account type?
|
||||
|
||||
- **Individual Google accounts:** Includes all
|
||||
[free tier accounts](../resources/quota-and-pricing.md#free-usage) such as
|
||||
Gemini Code Assist for individuals, as well as paid subscriptions for
|
||||
[free tier accounts](../quota-and-pricing/#free-usage) such as Gemini Code
|
||||
Assist for individuals, as well as paid subscriptions for
|
||||
[Google AI Pro and Ultra](https://gemini.google/subscriptions/).
|
||||
|
||||
- **Organization accounts:** Accounts using paid licenses through an
|
||||
@@ -317,5 +317,5 @@ configure authentication using environment variables:
|
||||
Your authentication method affects your quotas, pricing, Terms of Service, and
|
||||
privacy notices. Review the following pages to learn more:
|
||||
|
||||
- [Gemini CLI: Quotas and Pricing](../resources/quota-and-pricing.md).
|
||||
- [Gemini CLI: Terms of Service and Privacy Notice](../resources/tos-privacy.md).
|
||||
- [Gemini CLI: Quotas and Pricing](../quota-and-pricing.md).
|
||||
- [Gemini CLI: Terms of Service and Privacy Notice](../tos-privacy.md).
|
||||
|
||||
@@ -121,6 +121,11 @@ 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`
|
||||
@@ -217,6 +222,11 @@ 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`
|
||||
@@ -940,6 +950,11 @@ 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):
|
||||
@@ -1199,8 +1214,8 @@ within your user's home folder.
|
||||
Environment variables are a common way to configure applications, especially for
|
||||
sensitive information like API keys or for settings that might change between
|
||||
environments. For authentication setup, see the
|
||||
[Authentication documentation](../get-started/authentication.md) which covers
|
||||
all available authentication methods.
|
||||
[Authentication documentation](./authentication.md) which covers all available
|
||||
authentication methods.
|
||||
|
||||
The CLI automatically loads environment variables from an `.env` file. The
|
||||
loading order is:
|
||||
@@ -1219,8 +1234,7 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file.
|
||||
|
||||
- **`GEMINI_API_KEY`**:
|
||||
- Your API key for the Gemini API.
|
||||
- One of several available
|
||||
[authentication methods](../get-started/authentication.md).
|
||||
- One of several available [authentication methods](./authentication.md).
|
||||
- Set this in your shell profile (e.g., `~/.bashrc`, `~/.zshrc`) or an `.env`
|
||||
file.
|
||||
- **`GEMINI_MODEL`**:
|
||||
@@ -1566,16 +1580,15 @@ 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](../reference/memport.md).
|
||||
see the [Memory Import Processor documentation](../core/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](../reference/commands.md#memory) for full
|
||||
details on the `/memory` command and its sub-commands (`show` and
|
||||
`refresh`).
|
||||
- See the [Commands documentation](../cli/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
|
||||
@@ -54,7 +54,7 @@ Gemini CLI offers several ways to configure its behavior, including environment
|
||||
variables, command-line arguments, and settings files.
|
||||
|
||||
To explore your configuration options, see
|
||||
[Gemini CLI Configuration](../reference/configuration.md).
|
||||
[Gemini CLI Configuration](./configuration.md).
|
||||
|
||||
## Use
|
||||
|
||||
|
||||
@@ -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#environment-variable-redaction)
|
||||
[redaction system](/docs/get-started/configuration#environment-variable-redaction)
|
||||
that automatically filters variables matching sensitive patterns (e.g., `KEY`,
|
||||
`TOKEN`).
|
||||
|
||||
|
||||
+115
-105
@@ -12,7 +12,8 @@ npm install -g @google/gemini-cli
|
||||
|
||||
## Get started
|
||||
|
||||
- **[Overview](./index.md):** An overview of Gemini CLI and its features.
|
||||
Jump in to Gemini CLI.
|
||||
|
||||
- **[Quickstart](./get-started/index.md):** Your first session with Gemini CLI.
|
||||
- **[Installation](./get-started/installation.md):** How to install Gemini CLI
|
||||
on your system.
|
||||
@@ -20,15 +21,17 @@ npm install -g @google/gemini-cli
|
||||
personal and enterprise accounts.
|
||||
- **[Examples](./get-started/examples.md):** Practical examples of Gemini CLI in
|
||||
action.
|
||||
- **[CLI cheatsheet](./cli/cli-reference.md):** A quick reference for common
|
||||
- **[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
|
||||
@@ -39,121 +42,128 @@ npm install -g @google/gemini-cli
|
||||
complex workflows.
|
||||
- **[Web search and fetch](./cli/tutorials/web-tools.md):** Searching and
|
||||
fetching content from the web.
|
||||
- **[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.
|
||||
- **[Set up an MCP server](./cli/tutorials/mcp-setup.md):** Set up an MCP
|
||||
server.
|
||||
- **[Automate tasks](./cli/tutorials/automation.md):** Automate tasks.
|
||||
|
||||
## Features
|
||||
|
||||
- **[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.
|
||||
Technical reference documentation for each capability of Gemini CLI.
|
||||
|
||||
- **[/about](./cli/commands.md#about):** About Gemini CLI.
|
||||
- **[/auth](./get-started/authentication.md):** Authentication.
|
||||
- **[/bug](./cli/commands.md#bug):** Report a bug.
|
||||
- **[/chat](./cli/commands.md#chat):** Chat history.
|
||||
- **[/clear](./cli/commands.md#clear):** Clear screen.
|
||||
- **[/compress](./cli/commands.md#compress):** Compress context.
|
||||
- **[/copy](./cli/commands.md#copy):** Copy output.
|
||||
- **[/directory](./cli/commands.md#directory-or-dir):** Manage workspace.
|
||||
- **[/docs](./cli/commands.md#docs):** Open documentation.
|
||||
- **[/editor](./cli/commands.md#editor):** Select editor.
|
||||
- **[/extensions](./extensions/index.md):** Manage extensions.
|
||||
- **[/help](./cli/commands.md#help-or):** Show help.
|
||||
- **[/hooks](./hooks/index.md):** Hooks.
|
||||
- **[/ide](./ide-integration/index.md):** IDE integration.
|
||||
- **[/init](./cli/commands.md#init):** Initialize context.
|
||||
- **[/mcp](./tools/mcp-server.md):** MCP servers.
|
||||
- **[/memory](./cli/commands.md#memory):** Manage memory.
|
||||
- **[/model](./cli/model.md):** Model selection.
|
||||
- **[/policies](./cli/commands.md#policies):** Manage policies.
|
||||
- **[/privacy](./cli/commands.md#privacy):** Privacy notice.
|
||||
- **[/quit](./cli/commands.md#quit-or-exit):** Exit CLI.
|
||||
- **[/restore](./cli/checkpointing.md):** Restore files.
|
||||
- **[/resume](./cli/commands.md#resume):** Resume session.
|
||||
- **[/rewind](./cli/rewind.md):** Rewind.
|
||||
- **[/settings](./cli/settings.md):** Settings.
|
||||
- **[/setup-github](./cli/commands.md#setup-github):** GitHub setup.
|
||||
- **[/shells](./cli/commands.md#shells-or-bashes):** Manage processes.
|
||||
- **[/skills](./cli/skills.md):** Agent skills.
|
||||
- **[/stats](./cli/commands.md#stats):** Session statistics.
|
||||
- **[/terminal-setup](./cli/commands.md#terminal-setup):** Terminal keybindings.
|
||||
- **[/theme](./cli/themes.md):** Themes.
|
||||
- **[/tools](./cli/commands.md#tools):** List tools.
|
||||
- **[/vim](./cli/commands.md#vim):** Vim mode.
|
||||
- **[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.
|
||||
- **[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.
|
||||
- **[Subagents 🧪](./core/subagents.md):** Using specialized agents for specific
|
||||
tasks.
|
||||
- **[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.
|
||||
|
||||
## Configuration
|
||||
|
||||
- **[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.
|
||||
Settings and customization options for Gemini CLI.
|
||||
|
||||
## Extensions
|
||||
|
||||
- **[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):** 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.
|
||||
- **[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.
|
||||
|
||||
## Reference
|
||||
|
||||
- **[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.
|
||||
Deep technical documentation and API specifications.
|
||||
|
||||
- **[Command reference](./cli/commands.md):** Detailed slash command guide.
|
||||
- **[Configuration reference](./get-started/configuration.md):** Settings and
|
||||
environment variables.
|
||||
- **[Keyboard shortcuts](./cli/keyboard-shortcuts.md):** Productivity tips.
|
||||
- **[Memory import processor](./core/memport.md):** How Gemini CLI processes
|
||||
memory from various sources.
|
||||
- **[Policy engine](./core/policy-engine.md):** Fine-grained execution control.
|
||||
- **[Tools API](./core/tools-api.md):** The API for defining and using tools.
|
||||
|
||||
## 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.
|
||||
Support, release history, and legal information.
|
||||
|
||||
## Changelog
|
||||
- **[FAQ](./faq.md):** Answers to frequently asked questions.
|
||||
- **[Changelogs](./changelogs/index.md):** Highlights and notable changes.
|
||||
- **[Quota and pricing](./quota-and-pricing.md):** Limits and billing details.
|
||||
- **[Terms and privacy](./tos-privacy.md):** Official notices and terms.
|
||||
|
||||
- **[Release notes](./changelogs/index.md):** The release notes for all
|
||||
versions.
|
||||
## 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.
|
||||
|
||||
## Releases
|
||||
|
||||
- **[Release notes](./changelogs/index.md):** Release notes for all versions.
|
||||
- **[Stable release](./changelogs/latest.md):** The latest stable release.
|
||||
- **[Preview release](./changelogs/preview.md):** The latest preview release.
|
||||
|
||||
+160
-190
@@ -1,223 +1,193 @@
|
||||
[
|
||||
{
|
||||
"label": "docs_tab",
|
||||
"label": "Get started",
|
||||
"items": [
|
||||
{ "label": "Overview", "slug": "docs" },
|
||||
{ "label": "Quickstart", "slug": "docs/get-started" },
|
||||
{ "label": "Installation", "slug": "docs/get-started/installation" },
|
||||
{ "label": "Authentication", "slug": "docs/get-started/authentication" },
|
||||
{ "label": "Examples", "slug": "docs/get-started/examples" },
|
||||
{ "label": "CLI cheatsheet", "slug": "docs/cli/cli-reference" },
|
||||
{ "label": "Gemini 3 on Gemini CLI", "slug": "docs/get-started/gemini-3" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Use Gemini CLI",
|
||||
"items": [
|
||||
{
|
||||
"label": "Get started",
|
||||
"items": [
|
||||
{ "label": "Overview", "slug": "docs" },
|
||||
{ "label": "Quickstart", "slug": "docs/get-started" },
|
||||
{ "label": "Installation", "slug": "docs/get-started/installation" },
|
||||
{
|
||||
"label": "Authentication",
|
||||
"slug": "docs/get-started/authentication"
|
||||
},
|
||||
{ "label": "Examples", "slug": "docs/get-started/examples" },
|
||||
{ "label": "CLI cheatsheet", "slug": "docs/cli/cli-reference" },
|
||||
{
|
||||
"label": "Gemini 3 on Gemini CLI",
|
||||
"slug": "docs/get-started/gemini-3"
|
||||
}
|
||||
]
|
||||
"label": "File management",
|
||||
"slug": "docs/cli/tutorials/file-management"
|
||||
},
|
||||
{
|
||||
"label": "Use Gemini CLI",
|
||||
"items": [
|
||||
{
|
||||
"label": "File management",
|
||||
"slug": "docs/cli/tutorials/file-management"
|
||||
},
|
||||
{
|
||||
"label": "Manage context and memory",
|
||||
"slug": "docs/cli/tutorials/memory-management"
|
||||
},
|
||||
{
|
||||
"label": "Execute shell commands",
|
||||
"slug": "docs/cli/tutorials/shell-commands"
|
||||
},
|
||||
{
|
||||
"label": "Manage sessions and history",
|
||||
"slug": "docs/cli/tutorials/session-management"
|
||||
},
|
||||
{
|
||||
"label": "Plan tasks with todos",
|
||||
"slug": "docs/cli/tutorials/task-planning"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{ "label": "Automate tasks", "slug": "docs/cli/tutorials/automation" }
|
||||
]
|
||||
"label": "Get started with Agent skills",
|
||||
"slug": "docs/cli/tutorials/skills-getting-started"
|
||||
},
|
||||
{
|
||||
"label": "Features",
|
||||
"items": [
|
||||
{ "label": "Agent Skills", "slug": "docs/cli/skills" },
|
||||
{
|
||||
"label": "Authentication",
|
||||
"slug": "docs/get-started/authentication"
|
||||
},
|
||||
{ "label": "Checkpointing", "slug": "docs/cli/checkpointing" },
|
||||
{
|
||||
"label": "Extensions",
|
||||
"slug": "docs/extensions/index"
|
||||
},
|
||||
{ "label": "Headless mode", "slug": "docs/cli/headless" },
|
||||
{ "label": "Hooks", "slug": "docs/hooks" },
|
||||
{ "label": "IDE integration", "slug": "docs/ide-integration" },
|
||||
{ "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 (experimental)", "slug": "docs/cli/plan-mode" },
|
||||
{ "label": "Rewind", "slug": "docs/cli/rewind" },
|
||||
{ "label": "Sandboxing", "slug": "docs/cli/sandbox" },
|
||||
{ "label": "Settings", "slug": "docs/cli/settings" },
|
||||
{ "label": "Telemetry", "slug": "docs/cli/telemetry" },
|
||||
{ "label": "Token caching", "slug": "docs/cli/token-caching" }
|
||||
]
|
||||
"label": "Manage context and memory",
|
||||
"slug": "docs/cli/tutorials/memory-management"
|
||||
},
|
||||
{
|
||||
"label": "Configuration",
|
||||
"items": [
|
||||
{ "label": "Custom commands", "slug": "docs/cli/custom-commands" },
|
||||
{
|
||||
"label": "Enterprise configuration",
|
||||
"slug": "docs/cli/enterprise"
|
||||
},
|
||||
{
|
||||
"label": "Ignore files (.geminiignore)",
|
||||
"slug": "docs/cli/gemini-ignore"
|
||||
},
|
||||
{
|
||||
"label": "Model configuration",
|
||||
"slug": "docs/cli/generation-settings"
|
||||
},
|
||||
{
|
||||
"label": "Project context (GEMINI.md)",
|
||||
"slug": "docs/cli/gemini-md"
|
||||
},
|
||||
{ "label": "Settings", "slug": "docs/cli/settings" },
|
||||
{
|
||||
"label": "System prompt override",
|
||||
"slug": "docs/cli/system-prompt"
|
||||
},
|
||||
{ "label": "Themes", "slug": "docs/cli/themes" },
|
||||
{ "label": "Trusted folders", "slug": "docs/cli/trusted-folders" }
|
||||
]
|
||||
"label": "Execute shell commands",
|
||||
"slug": "docs/cli/tutorials/shell-commands"
|
||||
},
|
||||
{
|
||||
"label": "Manage sessions and history",
|
||||
"slug": "docs/cli/tutorials/session-management"
|
||||
},
|
||||
{
|
||||
"label": "Plan tasks with todos",
|
||||
"slug": "docs/cli/tutorials/task-planning"
|
||||
},
|
||||
{
|
||||
"label": "Web search and fetch",
|
||||
"slug": "docs/cli/tutorials/web-tools"
|
||||
},
|
||||
{
|
||||
"label": "Set up an MCP server",
|
||||
"slug": "docs/cli/tutorials/mcp-setup"
|
||||
},
|
||||
{ "label": "Automate tasks", "slug": "docs/cli/tutorials/automation" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Features",
|
||||
"items": [
|
||||
{ "label": "Agent Skills", "slug": "docs/cli/skills" },
|
||||
{
|
||||
"label": "Authentication",
|
||||
"slug": "docs/get-started/authentication"
|
||||
},
|
||||
{ "label": "Checkpointing", "slug": "docs/cli/checkpointing" },
|
||||
{
|
||||
"label": "Extensions",
|
||||
"items": [
|
||||
{
|
||||
"label": "Overview",
|
||||
"slug": "docs/extensions"
|
||||
},
|
||||
{
|
||||
"label": "User guide: Install and manage",
|
||||
"link": "/docs/extensions/#manage-extensions"
|
||||
},
|
||||
{
|
||||
"label": "Developer guide: Build extensions",
|
||||
"slug": "docs/extensions/writing-extensions"
|
||||
},
|
||||
{
|
||||
"label": "Developer guide: Best practices",
|
||||
"slug": "docs/extensions/best-practices"
|
||||
},
|
||||
{
|
||||
"label": "Developer guide: Releasing",
|
||||
"slug": "docs/extensions/releasing"
|
||||
},
|
||||
{
|
||||
"label": "Developer guide: Reference",
|
||||
"slug": "docs/extensions/reference"
|
||||
}
|
||||
]
|
||||
"slug": "docs/extensions/index"
|
||||
},
|
||||
{ "label": "Headless mode", "slug": "docs/cli/headless" },
|
||||
{ "label": "Help", "link": "/docs/cli/commands/#help-or" },
|
||||
{ "label": "Hooks", "slug": "docs/hooks" },
|
||||
{ "label": "IDE integration", "slug": "docs/ide-integration" },
|
||||
{ "label": "MCP servers", "slug": "docs/tools/mcp-server" },
|
||||
{
|
||||
"label": "Memory management",
|
||||
"link": "/docs/cli/commands/#memory"
|
||||
},
|
||||
{ "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": "Rewind", "slug": "docs/cli/rewind" },
|
||||
{ "label": "Sandboxing", "slug": "docs/cli/sandbox" },
|
||||
{ "label": "Settings", "slug": "docs/cli/settings" },
|
||||
{
|
||||
"label": "Shell",
|
||||
"link": "/docs/cli/commands/#shells-or-bashes"
|
||||
},
|
||||
{
|
||||
"label": "Development",
|
||||
"items": [
|
||||
{ "label": "Contribution guide", "slug": "docs/contributing" },
|
||||
{ "label": "Integration testing", "slug": "docs/integration-tests" },
|
||||
{
|
||||
"label": "Issue and PR automation",
|
||||
"slug": "docs/issue-and-pr-automation"
|
||||
},
|
||||
{ "label": "Local development", "slug": "docs/local-development" },
|
||||
{ "label": "NPM package structure", "slug": "docs/npm" }
|
||||
]
|
||||
"label": "Stats",
|
||||
"link": "/docs/cli/commands/#stats"
|
||||
},
|
||||
{ "label": "Telemetry", "slug": "docs/cli/telemetry" },
|
||||
{ "label": "Token caching", "slug": "docs/cli/token-caching" },
|
||||
{ "label": "Tools", "link": "/docs/cli/commands/#tools" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Configuration",
|
||||
"items": [
|
||||
{ "label": "Custom commands", "slug": "docs/cli/custom-commands" },
|
||||
{ "label": "Enterprise configuration", "slug": "docs/cli/enterprise" },
|
||||
{
|
||||
"label": "Ignore files (.geminiignore)",
|
||||
"slug": "docs/cli/gemini-ignore"
|
||||
},
|
||||
{
|
||||
"label": "Model configuration",
|
||||
"slug": "docs/cli/generation-settings"
|
||||
},
|
||||
{ "label": "Project context (GEMINI.md)", "slug": "docs/cli/gemini-md" },
|
||||
{ "label": "Settings", "slug": "docs/cli/settings" },
|
||||
{ "label": "System prompt override", "slug": "docs/cli/system-prompt" },
|
||||
{ "label": "Themes", "slug": "docs/cli/themes" },
|
||||
{ "label": "Trusted folders", "slug": "docs/cli/trusted-folders" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Extensions",
|
||||
"items": [
|
||||
{
|
||||
"label": "Overview",
|
||||
"slug": "docs/extensions"
|
||||
},
|
||||
{
|
||||
"label": "User guide: Install and manage",
|
||||
"link": "/docs/extensions/#manage-extensions"
|
||||
},
|
||||
{
|
||||
"label": "Developer guide: Build extensions",
|
||||
"slug": "docs/extensions/writing-extensions"
|
||||
},
|
||||
{
|
||||
"label": "Developer guide: Best practices",
|
||||
"slug": "docs/extensions/best-practices"
|
||||
},
|
||||
{
|
||||
"label": "Developer guide: Releasing",
|
||||
"slug": "docs/extensions/releasing"
|
||||
},
|
||||
{
|
||||
"label": "Developer guide: Reference",
|
||||
"slug": "docs/extensions/reference"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "reference_tab",
|
||||
"label": "Reference",
|
||||
"items": [
|
||||
{ "label": "Command reference", "slug": "docs/cli/commands" },
|
||||
{
|
||||
"label": "Reference",
|
||||
"items": [
|
||||
{ "label": "Command reference", "slug": "docs/reference/commands" },
|
||||
{
|
||||
"label": "Configuration reference",
|
||||
"slug": "docs/reference/configuration"
|
||||
},
|
||||
{
|
||||
"label": "Keyboard shortcuts",
|
||||
"slug": "docs/reference/keyboard-shortcuts"
|
||||
},
|
||||
{
|
||||
"label": "Memory import processor",
|
||||
"slug": "docs/reference/memport"
|
||||
},
|
||||
{ "label": "Policy engine", "slug": "docs/reference/policy-engine" },
|
||||
{ "label": "Tools API", "slug": "docs/reference/tools-api" }
|
||||
]
|
||||
}
|
||||
"label": "Configuration reference",
|
||||
"slug": "docs/get-started/configuration"
|
||||
},
|
||||
{ "label": "Keyboard shortcuts", "slug": "docs/cli/keyboard-shortcuts" },
|
||||
{ "label": "Memory import processor", "slug": "docs/core/memport" },
|
||||
{ "label": "Policy engine", "slug": "docs/core/policy-engine" },
|
||||
{ "label": "Tools API", "slug": "docs/core/tools-api" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "resources_tab",
|
||||
"label": "Resources",
|
||||
"items": [
|
||||
{
|
||||
"label": "Resources",
|
||||
"items": [
|
||||
{
|
||||
"label": "Quota and pricing",
|
||||
"slug": "docs/resources/quota-and-pricing"
|
||||
},
|
||||
{
|
||||
"label": "Terms and privacy",
|
||||
"slug": "docs/resources/tos-privacy"
|
||||
},
|
||||
{ "label": "FAQ", "slug": "docs/resources/faq" },
|
||||
{
|
||||
"label": "Troubleshooting",
|
||||
"slug": "docs/resources/troubleshooting"
|
||||
},
|
||||
{ "label": "Uninstall", "slug": "docs/resources/uninstall" }
|
||||
]
|
||||
}
|
||||
{ "label": "FAQ", "slug": "docs/faq" },
|
||||
{ "label": "Quota and pricing", "slug": "docs/quota-and-pricing" },
|
||||
{ "label": "Terms and privacy", "slug": "docs/tos-privacy" },
|
||||
{ "label": "Troubleshooting", "slug": "docs/troubleshooting" },
|
||||
{ "label": "Uninstall", "slug": "docs/cli/uninstall" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "changelog_tab",
|
||||
"label": "Development",
|
||||
"items": [
|
||||
{ "label": "Contribution guide", "slug": "docs/contributing" },
|
||||
{ "label": "Integration testing", "slug": "docs/integration-tests" },
|
||||
{
|
||||
"label": "Changelog",
|
||||
"items": [
|
||||
{ "label": "Release notes", "slug": "docs/changelogs/" },
|
||||
{ "label": "Stable release", "slug": "docs/changelogs/latest" },
|
||||
{ "label": "Preview release", "slug": "docs/changelogs/preview" }
|
||||
]
|
||||
}
|
||||
"label": "Issue and PR automation",
|
||||
"slug": "docs/issue-and-pr-automation"
|
||||
},
|
||||
{ "label": "Local development", "slug": "docs/local-development" },
|
||||
{ "label": "NPM package structure", "slug": "docs/npm" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Releases",
|
||||
"items": [
|
||||
{ "label": "Release notes", "slug": "docs/changelogs/" },
|
||||
{ "label": "Stable release", "slug": "docs/changelogs/latest" },
|
||||
{ "label": "Preview release", "slug": "docs/changelogs/preview" }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
+2
-2
@@ -98,5 +98,5 @@ Always review confirmation prompts carefully before allowing a tool to execute.
|
||||
## Next steps
|
||||
|
||||
- Learn how to [Provide context](../cli/gemini-md.md) to guide tool use.
|
||||
- Explore the [Command reference](../reference/commands.md) for tool-related
|
||||
slash commands.
|
||||
- Explore the [Command reference](../cli/commands.md) for tool-related slash
|
||||
commands.
|
||||
|
||||
@@ -40,7 +40,7 @@ Gemini CLI uses this tool to ensure technical accuracy:
|
||||
|
||||
## Next steps
|
||||
|
||||
- Explore the [Command reference](../reference/commands.md) for a detailed guide
|
||||
to slash commands.
|
||||
- See the [Configuration guide](../reference/configuration.md) for settings
|
||||
- Explore the [Command reference](../cli/commands.md) for a detailed guide to
|
||||
slash commands.
|
||||
- See the [Configuration guide](../get-started/configuration.md) for settings
|
||||
reference.
|
||||
|
||||
+3
-3
@@ -131,9 +131,9 @@ configuration file.
|
||||
commands. Including the generic `run_shell_command` acts as a wildcard,
|
||||
allowing any command not explicitly blocked.
|
||||
- `tools.exclude` [DEPRECATED]: To block specific commands, use the
|
||||
[Policy Engine](../reference/policy-engine.md). Historically, this setting
|
||||
allowed adding entries to the `exclude` list under the `tools` category in the
|
||||
format `run_shell_command(<command>)`. For example,
|
||||
[Policy Engine](../core/policy-engine.md). Historically, this setting allowed
|
||||
adding entries to the `exclude` list under the `tools` category in the format
|
||||
`run_shell_command(<command>)`. For example,
|
||||
`"tools": {"exclude": ["run_shell_command(rm)"]}` will block `rm` commands.
|
||||
|
||||
The validation logic is designed to be secure and flexible:
|
||||
|
||||
@@ -10,8 +10,8 @@ and Privacy Notices applicable to those services apply to such access and use.
|
||||
Your Gemini CLI Usage Statistics are handled in accordance with Google's Privacy
|
||||
Policy.
|
||||
|
||||
**Note:** See [quotas and pricing](/docs/resources/quota-and-pricing.md) for the
|
||||
quota and pricing details that apply to your usage of the Gemini CLI.
|
||||
**Note:** See [quotas and pricing](/docs/quota-and-pricing.md) for the quota and
|
||||
pricing details that apply to your usage of the Gemini CLI.
|
||||
|
||||
## Supported authentication methods
|
||||
|
||||
@@ -93,4 +93,4 @@ backend, these Terms of Service and Privacy Notice documents apply:
|
||||
|
||||
You may opt-out from sending Gemini CLI Usage Statistics to Google by following
|
||||
the instructions available here:
|
||||
[Usage Statistics Configuration](https://github.com/google-gemini/gemini-cli/blob/main/docs/reference/configuration.md#usage-statistics).
|
||||
[Usage Statistics Configuration](https://github.com/google-gemini/gemini-cli/blob/main/docs/get-started/configuration.md#usage-statistics).
|
||||
@@ -93,7 +93,7 @@ topics on:
|
||||
- **Cause:** When sandboxing is enabled, Gemini CLI may attempt operations
|
||||
that are restricted by your sandbox configuration, such as writing outside
|
||||
the project directory or system temp directory.
|
||||
- **Solution:** Refer to the [Configuration: Sandboxing](../cli/sandbox.md)
|
||||
- **Solution:** Refer to the [Configuration: Sandboxing](./cli/sandbox.md)
|
||||
documentation for more information, including how to customize your sandbox
|
||||
configuration.
|
||||
|
||||
+7
-1
@@ -63,7 +63,7 @@ const external = [
|
||||
'@lydell/node-pty-win32-arm64',
|
||||
'@lydell/node-pty-win32-x64',
|
||||
'keytar',
|
||||
'gemini-cli-devtools',
|
||||
'@google/gemini-cli-devtools',
|
||||
];
|
||||
|
||||
const baseConfig = {
|
||||
@@ -75,6 +75,10 @@ const baseConfig = {
|
||||
write: true,
|
||||
};
|
||||
|
||||
const commonAliases = {
|
||||
punycode: 'punycode/',
|
||||
};
|
||||
|
||||
const cliConfig = {
|
||||
...baseConfig,
|
||||
banner: {
|
||||
@@ -88,6 +92,7 @@ const cliConfig = {
|
||||
plugins: createWasmPlugins(),
|
||||
alias: {
|
||||
'is-in-ci': path.resolve(__dirname, 'packages/cli/src/patches/is-in-ci.ts'),
|
||||
...commonAliases,
|
||||
},
|
||||
metafile: true,
|
||||
};
|
||||
@@ -103,6 +108,7 @@ const a2aServerConfig = {
|
||||
'process.env.CLI_VERSION': JSON.stringify(pkg.version),
|
||||
},
|
||||
plugins: createWasmPlugins(),
|
||||
alias: commonAliases,
|
||||
};
|
||||
|
||||
Promise.allSettled([
|
||||
|
||||
@@ -6,17 +6,12 @@
|
||||
|
||||
import { describe, expect } from 'vitest';
|
||||
import { evalTest } from './test-helper.js';
|
||||
import {
|
||||
assertModelHasOutput,
|
||||
checkModelOutputContent,
|
||||
} from '../integration-tests/test-helper.js';
|
||||
import { assertModelHasOutput } from '../integration-tests/test-helper.js';
|
||||
|
||||
describe('Hierarchical Memory', () => {
|
||||
const TEST_PREFIX = 'Hierarchical memory test: ';
|
||||
|
||||
const conflictResolutionTest =
|
||||
'Agent follows hierarchy for contradictory instructions';
|
||||
evalTest('ALWAYS_PASSES', {
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: conflictResolutionTest,
|
||||
params: {
|
||||
settings: {
|
||||
@@ -52,7 +47,7 @@ What is my favorite fruit? Tell me just the name of the fruit.`,
|
||||
});
|
||||
|
||||
const provenanceAwarenessTest = 'Agent is aware of memory provenance';
|
||||
evalTest('ALWAYS_PASSES', {
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: provenanceAwarenessTest,
|
||||
params: {
|
||||
settings: {
|
||||
@@ -91,7 +86,7 @@ Provide the answer as an XML block like this:
|
||||
});
|
||||
|
||||
const extensionVsGlobalTest = 'Extension memory wins over Global memory';
|
||||
evalTest('ALWAYS_PASSES', {
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: extensionVsGlobalTest,
|
||||
params: {
|
||||
settings: {
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* @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);
|
||||
},
|
||||
});
|
||||
});
|
||||
+42
-1
@@ -57,6 +57,47 @@ 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,
|
||||
@@ -85,7 +126,7 @@ describe('plan_mode', () => {
|
||||
'# My Implementation Plan\n\n1. Step one\n2. Step two',
|
||||
},
|
||||
prompt:
|
||||
'The plan in plans/my-plan.md is solid. Please proceed with the implementation.',
|
||||
'The plan in plans/my-plan.md looks solid. Start 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(
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
/**
|
||||
* @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());
|
||||
},
|
||||
});
|
||||
});
|
||||
Generated
+57
-47
@@ -11,9 +11,10 @@
|
||||
"packages/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"ink": "npm:@jrichman/ink@6.4.10",
|
||||
"ink": "npm:@jrichman/ink@6.4.11",
|
||||
"latest-version": "^9.0.0",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"punycode": "^2.3.1",
|
||||
"simple-git": "^3.28.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -48,7 +49,6 @@
|
||||
"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,6 +58,7 @@
|
||||
"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",
|
||||
@@ -76,7 +77,6 @@
|
||||
"@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,6 +1389,10 @@
|
||||
"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
|
||||
@@ -9056,18 +9060,6 @@
|
||||
"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
|
||||
@@ -10029,9 +10021,9 @@
|
||||
},
|
||||
"node_modules/ink": {
|
||||
"name": "@jrichman/ink",
|
||||
"version": "6.4.10",
|
||||
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.10.tgz",
|
||||
"integrity": "sha512-kjJqZFkGVm0QyJmga/L02rsFJroF1aP2bhXEGkpuuT7clB6/W+gxAbLNw7ZaJrG6T30DgqOT92Pu6C9mK1FWyg==",
|
||||
"version": "6.4.11",
|
||||
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.11.tgz",
|
||||
"integrity": "sha512-93LQlzT7vvZ1XJcmOMwN4s+6W334QegendeHOMnEJBlhnpIzr8bws6/aOEHG8ZCuVD/vNeeea5m1msHIdAY6ig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@alcalzone/ansi-tokenize": "^0.2.1",
|
||||
@@ -10112,24 +10104,6 @@
|
||||
"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",
|
||||
@@ -13547,7 +13521,6 @@
|
||||
"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"
|
||||
@@ -13709,9 +13682,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||
"version": "19.2.4",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -13750,6 +13723,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -15564,9 +15557,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "7.5.7",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz",
|
||||
"integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==",
|
||||
"version": "7.5.8",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.8.tgz",
|
||||
"integrity": "sha512-SYkBtK99u0yXa+IWL0JRzzcl7RxNpvX/U08Z+8DKnysfno7M+uExnTZH8K+VGgShf2qFPKtbNr9QBl8n7WBP6Q==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/fs-minipass": "^4.0.0",
|
||||
@@ -17262,7 +17255,7 @@
|
||||
"express": "^5.1.0",
|
||||
"fs-extra": "^11.3.0",
|
||||
"strip-json-comments": "^3.1.1",
|
||||
"tar": "^7.5.2",
|
||||
"tar": "^7.5.8",
|
||||
"uuid": "^13.0.0",
|
||||
"winston": "^3.17.0"
|
||||
},
|
||||
@@ -17325,6 +17318,7 @@
|
||||
"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",
|
||||
@@ -17333,7 +17327,7 @@
|
||||
"fzf": "^0.5.2",
|
||||
"glob": "^12.0.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"ink": "npm:@jrichman/ink@6.4.10",
|
||||
"ink": "npm:@jrichman/ink@6.4.11",
|
||||
"ink-gradient": "^3.0.0",
|
||||
"ink-spinner": "^5.0.0",
|
||||
"latest-version": "^9.0.0",
|
||||
@@ -17348,7 +17342,7 @@
|
||||
"string-width": "^8.1.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"strip-json-comments": "^3.1.1",
|
||||
"tar": "^7.5.2",
|
||||
"tar": "^7.5.8",
|
||||
"tinygradient": "^1.1.5",
|
||||
"undici": "^7.10.0",
|
||||
"ws": "^8.16.0",
|
||||
@@ -17359,6 +17353,7 @@
|
||||
"gemini": "dist/index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@google/gemini-cli-devtools": "file:../devtools",
|
||||
"@google/gemini-cli-test-utils": "file:../test-utils",
|
||||
"@types/command-exists": "^1.2.3",
|
||||
"@types/hast": "^3.0.4",
|
||||
@@ -17368,7 +17363,7 @@
|
||||
"@types/shell-quote": "^1.7.5",
|
||||
"@types/ws": "^8.5.10",
|
||||
"@types/yargs": "^17.0.32",
|
||||
"ink-testing-library": "^4.0.0",
|
||||
"@xterm/headless": "^5.5.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vitest": "^3.1.1"
|
||||
},
|
||||
@@ -17573,6 +17568,21 @@
|
||||
"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",
|
||||
|
||||
+5
-5
@@ -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 && node esbuild.config.js && node scripts/copy_bundle_assets.js",
|
||||
"bundle": "npm run generate && npm run build --workspace=@google/gemini-cli-devtools && 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,7 +64,7 @@
|
||||
"pre-commit": "node scripts/pre-commit.js"
|
||||
},
|
||||
"overrides": {
|
||||
"ink": "npm:@jrichman/ink@6.4.10",
|
||||
"ink": "npm:@jrichman/ink@6.4.11",
|
||||
"wrap-ansi": "9.0.2",
|
||||
"cliui": {
|
||||
"wrap-ansi": "7.0.0"
|
||||
@@ -107,7 +107,6 @@
|
||||
"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",
|
||||
@@ -117,6 +116,7 @@
|
||||
"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",
|
||||
@@ -126,9 +126,10 @@
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"ink": "npm:@jrichman/ink@6.4.10",
|
||||
"ink": "npm:@jrichman/ink@6.4.11",
|
||||
"latest-version": "^9.0.0",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"punycode": "^2.3.1",
|
||||
"simple-git": "^3.28.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
@@ -138,7 +139,6 @@
|
||||
"@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"
|
||||
},
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"express": "^5.1.0",
|
||||
"fs-extra": "^11.3.0",
|
||||
"strip-json-comments": "^3.1.1",
|
||||
"tar": "^7.5.2",
|
||||
"tar": "^7.5.8",
|
||||
"uuid": "^13.0.0",
|
||||
"winston": "^3.17.0"
|
||||
},
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"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",
|
||||
@@ -47,7 +48,7 @@
|
||||
"fzf": "^0.5.2",
|
||||
"glob": "^12.0.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"ink": "npm:@jrichman/ink@6.4.10",
|
||||
"ink": "npm:@jrichman/ink@6.4.11",
|
||||
"ink-gradient": "^3.0.0",
|
||||
"ink-spinner": "^5.0.0",
|
||||
"latest-version": "^9.0.0",
|
||||
@@ -62,7 +63,7 @@
|
||||
"string-width": "^8.1.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"strip-json-comments": "^3.1.1",
|
||||
"tar": "^7.5.2",
|
||||
"tar": "^7.5.8",
|
||||
"tinygradient": "^1.1.5",
|
||||
"undici": "^7.10.0",
|
||||
"ws": "^8.16.0",
|
||||
@@ -70,6 +71,7 @@
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@google/gemini-cli-devtools": "file:../devtools",
|
||||
"@google/gemini-cli-test-utils": "file:../test-utils",
|
||||
"@types/command-exists": "^1.2.3",
|
||||
"@types/hast": "^3.0.4",
|
||||
@@ -79,7 +81,7 @@
|
||||
"@types/shell-quote": "^1.7.5",
|
||||
"@types/ws": "^8.5.10",
|
||||
"@types/yargs": "^17.0.32",
|
||||
"ink-testing-library": "^4.0.0",
|
||||
"@xterm/headless": "^5.5.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vitest": "^3.1.1"
|
||||
},
|
||||
|
||||
@@ -1344,6 +1344,36 @@ 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', () => {
|
||||
@@ -2556,9 +2586,8 @@ describe('loadCliConfig approval mode', () => {
|
||||
},
|
||||
});
|
||||
|
||||
await expect(loadCliConfig(settings, 'test-session', argv)).rejects.toThrow(
|
||||
'Approval mode "plan" is only available when experimental.plan is enabled.',
|
||||
);
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
|
||||
});
|
||||
|
||||
it('should throw error when --approval-mode=plan is used but experimental.plan setting is missing', async () => {
|
||||
@@ -2566,9 +2595,8 @@ describe('loadCliConfig approval mode', () => {
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
const settings = createTestMergedSettings({});
|
||||
|
||||
await expect(loadCliConfig(settings, 'test-session', argv)).rejects.toThrow(
|
||||
'Approval mode "plan" is only available when experimental.plan is enabled.',
|
||||
);
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
|
||||
});
|
||||
|
||||
// --- Untrusted Folder Scenarios ---
|
||||
@@ -2678,11 +2706,8 @@ describe('loadCliConfig approval mode', () => {
|
||||
experimental: { plan: false },
|
||||
});
|
||||
const argv = await parseArguments(settings);
|
||||
await expect(
|
||||
loadCliConfig(settings, 'test-session', argv),
|
||||
).rejects.toThrow(
|
||||
'Approval mode "plan" is only available when experimental.plan is enabled.',
|
||||
);
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -555,11 +555,13 @@ export async function loadCliConfig(
|
||||
break;
|
||||
case 'plan':
|
||||
if (!(settings.experimental?.plan ?? false)) {
|
||||
throw new Error(
|
||||
'Approval mode "plan" is only available when experimental.plan is enabled.',
|
||||
debugLogger.warn(
|
||||
'Approval mode "plan" is only available when experimental.plan is enabled. Falling back to "default".',
|
||||
);
|
||||
approvalMode = ApprovalMode.DEFAULT;
|
||||
} else {
|
||||
approvalMode = ApprovalMode.PLAN;
|
||||
}
|
||||
approvalMode = ApprovalMode.PLAN;
|
||||
break;
|
||||
case 'default':
|
||||
approvalMode = ApprovalMode.DEFAULT;
|
||||
@@ -816,6 +818,7 @@ export async function loadCliConfig(
|
||||
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,
|
||||
|
||||
@@ -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');
|
||||
},
|
||||
);
|
||||
|
||||
@@ -178,6 +178,7 @@ 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 }],
|
||||
@@ -211,7 +212,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' }],
|
||||
[Command.ACCEPT_SUGGESTION_REVERSE_SEARCH]: [{ key: 'tab', shift: false }],
|
||||
|
||||
// Navigation
|
||||
[Command.NAVIGATION_UP]: [{ key: 'up', shift: false }],
|
||||
@@ -230,7 +231,10 @@ export const defaultKeyBindings: KeyBindingConfig = {
|
||||
[Command.DIALOG_PREV]: [{ key: 'tab', shift: true }],
|
||||
|
||||
// Suggestions & Completions
|
||||
[Command.ACCEPT_SUGGESTION]: [{ key: 'tab' }, { key: 'return', ctrl: false }],
|
||||
[Command.ACCEPT_SUGGESTION]: [
|
||||
{ key: 'tab', shift: false },
|
||||
{ key: 'return', ctrl: false },
|
||||
],
|
||||
[Command.COMPLETION_UP]: [
|
||||
{ key: 'up', shift: false },
|
||||
{ key: 'p', shift: false, ctrl: true },
|
||||
|
||||
@@ -353,6 +353,17 @@ 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();
|
||||
|
||||
@@ -236,6 +236,16 @@ 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',
|
||||
@@ -475,6 +485,15 @@ 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',
|
||||
@@ -1625,6 +1644,16 @@ 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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -491,25 +491,33 @@ describe('Trusted Folders', () => {
|
||||
});
|
||||
});
|
||||
|
||||
const itif = (condition: boolean) => (condition ? it : it.skip);
|
||||
|
||||
describe('Symlinks Support', () => {
|
||||
const mockSettings: Settings = {
|
||||
security: { folderTrust: { enabled: true } },
|
||||
};
|
||||
|
||||
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);
|
||||
// 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);
|
||||
|
||||
// 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', () => {
|
||||
|
||||
@@ -56,6 +56,20 @@ 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')>();
|
||||
@@ -837,6 +851,10 @@ 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();
|
||||
});
|
||||
|
||||
@@ -9,14 +9,6 @@ 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')>();
|
||||
@@ -124,10 +116,39 @@ 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 },
|
||||
}));
|
||||
@@ -169,12 +190,6 @@ 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: {} },
|
||||
@@ -201,7 +216,7 @@ describe('gemini.tsx main function cleanup', () => {
|
||||
getMcpServers: () => ({}),
|
||||
getMcpClientManager: vi.fn(),
|
||||
getIdeMode: vi.fn(() => false),
|
||||
getExperimentalZedIntegration: vi.fn(() => false),
|
||||
getExperimentalZedIntegration: vi.fn(() => true),
|
||||
getScreenReader: vi.fn(() => false),
|
||||
getGeminiMdFileCount: vi.fn(() => 0),
|
||||
getProjectRoot: vi.fn(() => '/'),
|
||||
@@ -224,18 +239,12 @@ describe('gemini.tsx main function cleanup', () => {
|
||||
getRemoteAdminSettings: vi.fn(() => undefined),
|
||||
} as any); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
|
||||
try {
|
||||
await main();
|
||||
} catch (e) {
|
||||
if (!(e instanceof MockProcessExitError)) throw e;
|
||||
}
|
||||
await main();
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* @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.');
|
||||
});
|
||||
});
|
||||
@@ -4,10 +4,12 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, afterEach } from 'vitest';
|
||||
import { describe, it, afterEach, expect } from 'vitest';
|
||||
import { act } from 'react';
|
||||
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));
|
||||
|
||||
@@ -18,6 +20,47 @@ 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,
|
||||
@@ -26,7 +69,11 @@ describe('AppRig', () => {
|
||||
);
|
||||
rig = new AppRig({ fakeResponsesPath });
|
||||
await rig.initialize();
|
||||
rig.render();
|
||||
await act(async () => {
|
||||
rig!.render();
|
||||
// Allow async initializations (like banners) to settle within the act boundary
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
// Wait for initial render
|
||||
await rig.waitForIdle();
|
||||
|
||||
@@ -74,6 +74,20 @@ 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;
|
||||
@@ -449,12 +463,11 @@ export class AppRig {
|
||||
this.lastAwaitedConfirmation = undefined;
|
||||
}
|
||||
|
||||
async addUserHint(_hint: string) {
|
||||
async addUserHint(hint: string) {
|
||||
if (!this.config) throw new Error('AppRig not initialized');
|
||||
// TODO(joshualitt): Land hints.
|
||||
// await act(async () => {
|
||||
// this.config!.addUserHint(hint);
|
||||
// });
|
||||
await act(async () => {
|
||||
this.config!.userHintService.addUserHint(hint);
|
||||
});
|
||||
}
|
||||
|
||||
getConfig(): Config {
|
||||
@@ -488,7 +501,7 @@ export class AppRig {
|
||||
|
||||
get lastFrame() {
|
||||
if (!this.renderResult) return '';
|
||||
return stripAnsi(this.renderResult.lastFrame() || '');
|
||||
return stripAnsi(this.renderResult.lastFrame({ allowEmpty: true }) || '');
|
||||
}
|
||||
|
||||
getStaticOutput() {
|
||||
|
||||
@@ -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,
|
||||
assertion: () => void | Promise<void>,
|
||||
{ timeout = 2000, interval = 50 } = {},
|
||||
): Promise<void> {
|
||||
const startTime = Date.now();
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
assertion();
|
||||
await assertion();
|
||||
return;
|
||||
} catch (error) {
|
||||
if (Date.now() - startTime > timeout) {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{"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"}]}]}
|
||||
@@ -5,36 +5,48 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, act } from 'react';
|
||||
import { Text } from 'ink';
|
||||
import { renderHook, render } from './render.js';
|
||||
import { waitFor } from './async.js';
|
||||
|
||||
describe('render', () => {
|
||||
it('should render a component', () => {
|
||||
const { lastFrame } = render(<Text>Hello World</Text>);
|
||||
expect(lastFrame()).toBe('Hello World');
|
||||
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 support rerender', () => {
|
||||
const { lastFrame, rerender } = render(<Text>Hello</Text>);
|
||||
expect(lastFrame()).toBe('Hello');
|
||||
it('should support rerender', async () => {
|
||||
const { lastFrame, rerender, waitUntilReady, unmount } = render(
|
||||
<Text>Hello</Text>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toBe('Hello\n');
|
||||
|
||||
rerender(<Text>World</Text>);
|
||||
expect(lastFrame()).toBe('World');
|
||||
await act(async () => {
|
||||
rerender(<Text>World</Text>);
|
||||
});
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toBe('World\n');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should support unmount', () => {
|
||||
const cleanup = vi.fn();
|
||||
it('should support unmount', async () => {
|
||||
const cleanupMock = vi.fn();
|
||||
function TestComponent() {
|
||||
useEffect(() => cleanup, []);
|
||||
useEffect(() => cleanupMock, []);
|
||||
return <Text>Hello</Text>;
|
||||
}
|
||||
|
||||
const { unmount } = render(<TestComponent />);
|
||||
const { unmount, waitUntilReady } = render(<TestComponent />);
|
||||
await waitUntilReady();
|
||||
unmount();
|
||||
|
||||
expect(cleanup).toHaveBeenCalled();
|
||||
expect(cleanupMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,49 +60,74 @@ describe('renderHook', () => {
|
||||
return { count, value };
|
||||
};
|
||||
|
||||
const { result, rerender } = renderHook(useTestHook, {
|
||||
initialProps: { value: 1 },
|
||||
});
|
||||
const { result, rerender, waitUntilReady, unmount } = renderHook(
|
||||
useTestHook,
|
||||
{
|
||||
initialProps: { value: 1 },
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(result.current.value).toBe(1);
|
||||
await waitFor(() => expect(result.current.count).toBe(1));
|
||||
|
||||
// Rerender with new props
|
||||
rerender({ value: 2 });
|
||||
await act(async () => {
|
||||
rerender({ value: 2 });
|
||||
});
|
||||
await waitUntilReady();
|
||||
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
|
||||
rerender();
|
||||
await act(async () => {
|
||||
rerender();
|
||||
});
|
||||
await waitUntilReady();
|
||||
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', () => {
|
||||
it('should handle initial render without props', async () => {
|
||||
const useTestHook = () => {
|
||||
const [count, setCount] = useState(0);
|
||||
return { count, increment: () => setCount((c) => c + 1) };
|
||||
};
|
||||
|
||||
const { result, rerender } = renderHook(useTestHook);
|
||||
const { result, rerender, waitUntilReady, unmount } =
|
||||
renderHook(useTestHook);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(result.current.count).toBe(0);
|
||||
|
||||
rerender();
|
||||
await act(async () => {
|
||||
rerender();
|
||||
});
|
||||
await waitUntilReady();
|
||||
expect(result.current.count).toBe(0);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should update props if undefined is passed explicitly', () => {
|
||||
it('should update props if undefined is passed explicitly', async () => {
|
||||
const useTestHook = (val: string | undefined) => val;
|
||||
const { result, rerender } = renderHook(useTestHook, {
|
||||
initialProps: 'initial',
|
||||
});
|
||||
const { result, rerender, waitUntilReady, unmount } = renderHook(
|
||||
useTestHook,
|
||||
{
|
||||
initialProps: 'initial' as string | undefined,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(result.current).toBe('initial');
|
||||
|
||||
rerender(undefined);
|
||||
await act(async () => {
|
||||
rerender(undefined);
|
||||
});
|
||||
await waitUntilReady();
|
||||
expect(result.current).toBeUndefined();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,10 +4,13 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render as inkRender } from 'ink-testing-library';
|
||||
import { render as inkRenderDirect, type Instance as InkInstance } from 'ink';
|
||||
import { EventEmitter } from 'node:events';
|
||||
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';
|
||||
@@ -40,6 +43,15 @@ 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,
|
||||
}));
|
||||
@@ -50,51 +62,356 @@ vi.mock('../ui/utils/terminalUtils.js', () => ({
|
||||
isITerm2: vi.fn(() => false),
|
||||
}));
|
||||
|
||||
// Wrapper around ink-testing-library's render that ensures act() is called
|
||||
type TerminalState = {
|
||||
terminal: Terminal;
|
||||
cols: number;
|
||||
rows: number;
|
||||
};
|
||||
|
||||
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;
|
||||
private lastRenderStaticContent: 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;
|
||||
this.lastRenderStaticContent = undefined;
|
||||
};
|
||||
|
||||
dispose = () => {
|
||||
this.state.terminal.dispose();
|
||||
};
|
||||
|
||||
onRender = (staticContent: string, output: string) => {
|
||||
this.renderCount++;
|
||||
this.lastRenderStaticContent = staticContent;
|
||||
this.lastRenderOutput = output;
|
||||
this.emit('render');
|
||||
};
|
||||
|
||||
lastFrame = (options: { allowEmpty?: boolean } = {}) => {
|
||||
let result: string;
|
||||
// On Windows, xterm.js headless can sometimes have timing or rendering issues
|
||||
// that lead to duplicated content or incorrect buffer state in tests.
|
||||
// As a fallback, we can trust the raw output Ink provided during onRender.
|
||||
if (os.platform() === 'win32') {
|
||||
result =
|
||||
(this.lastRenderStaticContent ?? '') + (this.lastRenderOutput ?? '');
|
||||
} else {
|
||||
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();
|
||||
}
|
||||
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.lastRenderStaticContent ?? '') + (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>;
|
||||
};
|
||||
|
||||
const instances: InkInstance[] = [];
|
||||
|
||||
// Wrapper around ink's render that ensures act() is called and uses Xterm for output
|
||||
export const render = (
|
||||
tree: React.ReactElement,
|
||||
terminalWidth?: number,
|
||||
): 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(() => {
|
||||
renderResult = inkRender(tree);
|
||||
): RenderInstance => {
|
||||
const cols = terminalWidth ?? 100;
|
||||
const rows = 40;
|
||||
const terminal = new Terminal({
|
||||
cols,
|
||||
rows,
|
||||
allowProposedApi: true,
|
||||
convertEol: true,
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
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();
|
||||
|
||||
// Trigger a rerender so Ink can pick up the new terminal width
|
||||
act(() => {
|
||||
renderResult.rerender(tree);
|
||||
let instance!: InkInstance;
|
||||
stdout.clear();
|
||||
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: { output: string; staticOutput?: string }) => {
|
||||
stdout.onRender(metrics.staticOutput ?? '', metrics.output);
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const originalUnmount = renderResult.unmount;
|
||||
const originalRerender = renderResult.rerender;
|
||||
instances.push(instance);
|
||||
|
||||
return {
|
||||
...renderResult,
|
||||
unmount: () => {
|
||||
act(() => {
|
||||
originalUnmount();
|
||||
});
|
||||
},
|
||||
rerender: (newTree: React.ReactElement) => {
|
||||
act(() => {
|
||||
originalRerender(newTree);
|
||||
stdout.clear();
|
||||
instance.rerender(newTree);
|
||||
});
|
||||
},
|
||||
unmount: () => {
|
||||
act(() => {
|
||||
instance.unmount();
|
||||
});
|
||||
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: ReturnType<typeof inkRender>['stdin'],
|
||||
stdin: XtermStdin,
|
||||
col: number,
|
||||
row: number,
|
||||
button: 0 | 1 | 2 = 0, // 0 for left, 1 for middle, 2 for right
|
||||
@@ -151,7 +468,7 @@ export const mockSettings = new LoadedSettings(
|
||||
const baseMockUiState = {
|
||||
renderMarkdown: true,
|
||||
streamingState: StreamingState.Idle,
|
||||
terminalWidth: 120,
|
||||
terminalWidth: 100,
|
||||
terminalHeight: 40,
|
||||
currentModel: 'gemini-pro',
|
||||
terminalBackgroundColor: 'black',
|
||||
@@ -166,6 +483,8 @@ const baseMockUiState = {
|
||||
proQuotaRequest: null,
|
||||
validationRequest: null,
|
||||
},
|
||||
hintMode: false,
|
||||
hintBuffer: '',
|
||||
};
|
||||
|
||||
export const mockAppState: AppState = {
|
||||
@@ -219,6 +538,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(),
|
||||
};
|
||||
@@ -252,7 +575,13 @@ export const renderWithProviders = (
|
||||
};
|
||||
appState?: AppState;
|
||||
} = {},
|
||||
): ReturnType<typeof render> & { simulateClick: typeof simulateClick } => {
|
||||
): RenderInstance & {
|
||||
simulateClick: (
|
||||
col: number,
|
||||
row: number,
|
||||
button?: 0 | 1 | 2,
|
||||
) => Promise<void>;
|
||||
} => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const baseState: UIState = new Proxy(
|
||||
{ ...baseMockUiState, ...providedUiState },
|
||||
@@ -371,7 +700,11 @@ export const renderWithProviders = (
|
||||
terminalWidth,
|
||||
);
|
||||
|
||||
return { ...renderResult, simulateClick };
|
||||
return {
|
||||
...renderResult,
|
||||
simulateClick: (col: number, row: number, button?: 0 | 1 | 2) =>
|
||||
simulateClick(renderResult.stdin, col, row, button),
|
||||
};
|
||||
};
|
||||
|
||||
export function renderHook<Result, Props>(
|
||||
@@ -384,6 +717,7 @@ 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 };
|
||||
@@ -405,6 +739,7 @@ export function renderHook<Result, Props>(
|
||||
|
||||
let inkRerender: (tree: React.ReactElement) => void = () => {};
|
||||
let unmount: () => void = () => {};
|
||||
let waitUntilReady: () => Promise<void> = async () => {};
|
||||
|
||||
act(() => {
|
||||
const renderResult = render(
|
||||
@@ -414,6 +749,7 @@ export function renderHook<Result, Props>(
|
||||
);
|
||||
inkRerender = renderResult.rerender;
|
||||
unmount = renderResult.unmount;
|
||||
waitUntilReady = renderResult.waitUntilReady;
|
||||
});
|
||||
|
||||
function rerender(props?: Props) {
|
||||
@@ -430,7 +766,7 @@ export function renderHook<Result, Props>(
|
||||
});
|
||||
}
|
||||
|
||||
return { result, rerender, unmount };
|
||||
return { result, rerender, unmount, waitUntilReady };
|
||||
}
|
||||
|
||||
export function renderHookWithProviders<Result, Props>(
|
||||
@@ -451,6 +787,7 @@ 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 };
|
||||
@@ -500,5 +837,6 @@ export function renderHookWithProviders<Result, Props>(
|
||||
renderResult.unmount();
|
||||
});
|
||||
},
|
||||
waitUntilReady: () => renderResult.waitUntilReady(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -92,32 +92,42 @@ describe('App', () => {
|
||||
backgroundShells: new Map(),
|
||||
};
|
||||
|
||||
it('should render main content and composer when not quitting', () => {
|
||||
const { lastFrame } = renderWithProviders(<App />, {
|
||||
uiState: mockUIState,
|
||||
useAlternateBuffer: false,
|
||||
});
|
||||
it('should render main content and composer when not quitting', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<App />,
|
||||
{
|
||||
uiState: mockUIState,
|
||||
useAlternateBuffer: false,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
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', () => {
|
||||
it('should render quitting display when quittingMessages is set', async () => {
|
||||
const quittingUIState = {
|
||||
...mockUIState,
|
||||
quittingMessages: [{ id: 1, type: 'user', text: 'test' }],
|
||||
} as UIState;
|
||||
|
||||
const { lastFrame } = renderWithProviders(<App />, {
|
||||
uiState: quittingUIState,
|
||||
useAlternateBuffer: false,
|
||||
});
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<App />,
|
||||
{
|
||||
uiState: quittingUIState,
|
||||
useAlternateBuffer: false,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toContain('Quitting...');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render full history in alternate buffer mode when quittingMessages is set', () => {
|
||||
it('should render full history in alternate buffer mode when quittingMessages is set', async () => {
|
||||
const quittingUIState = {
|
||||
...mockUIState,
|
||||
quittingMessages: [{ id: 1, type: 'user', text: 'test' }],
|
||||
@@ -125,28 +135,38 @@ describe('App', () => {
|
||||
pendingHistoryItems: [{ type: 'user', text: 'pending item' }],
|
||||
} as UIState;
|
||||
|
||||
const { lastFrame } = renderWithProviders(<App />, {
|
||||
uiState: quittingUIState,
|
||||
useAlternateBuffer: true,
|
||||
});
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<App />,
|
||||
{
|
||||
uiState: quittingUIState,
|
||||
useAlternateBuffer: true,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toContain('HistoryItemDisplay');
|
||||
expect(lastFrame()).toContain('Quitting...');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render dialog manager when dialogs are visible', () => {
|
||||
it('should render dialog manager when dialogs are visible', async () => {
|
||||
const dialogUIState = {
|
||||
...mockUIState,
|
||||
dialogsVisible: true,
|
||||
} as UIState;
|
||||
|
||||
const { lastFrame } = renderWithProviders(<App />, {
|
||||
uiState: dialogUIState,
|
||||
});
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<App />,
|
||||
{
|
||||
uiState: dialogUIState,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toContain('Tips for getting started');
|
||||
expect(lastFrame()).toContain('Notifications');
|
||||
expect(lastFrame()).toContain('DialogManager');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -154,47 +174,62 @@ describe('App', () => {
|
||||
{ key: 'D', stateKey: 'ctrlDPressedOnce' },
|
||||
])(
|
||||
'should show Ctrl+$key exit prompt when dialogs are visible and $stateKey is true',
|
||||
({ key, stateKey }) => {
|
||||
async ({ key, stateKey }) => {
|
||||
const uiState = {
|
||||
...mockUIState,
|
||||
dialogsVisible: true,
|
||||
[stateKey]: true,
|
||||
} as UIState;
|
||||
|
||||
const { lastFrame } = renderWithProviders(<App />, {
|
||||
uiState,
|
||||
});
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<App />,
|
||||
{
|
||||
uiState,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toContain(`Press Ctrl+${key} again to exit.`);
|
||||
unmount();
|
||||
},
|
||||
);
|
||||
|
||||
it('should render ScreenReaderAppLayout when screen reader is enabled', () => {
|
||||
it('should render ScreenReaderAppLayout when screen reader is enabled', async () => {
|
||||
(useIsScreenReaderEnabled as Mock).mockReturnValue(true);
|
||||
|
||||
const { lastFrame } = renderWithProviders(<App />, {
|
||||
uiState: mockUIState,
|
||||
});
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<App />,
|
||||
{
|
||||
uiState: mockUIState,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
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', () => {
|
||||
it('should render DefaultAppLayout when screen reader is not enabled', async () => {
|
||||
(useIsScreenReaderEnabled as Mock).mockReturnValue(false);
|
||||
|
||||
const { lastFrame } = renderWithProviders(<App />, {
|
||||
uiState: mockUIState,
|
||||
});
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<App />,
|
||||
{
|
||||
uiState: mockUIState,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
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', () => {
|
||||
it('should render ToolConfirmationQueue along with Composer when tool is confirming and experiment is on', async () => {
|
||||
(useIsScreenReaderEnabled as Mock).mockReturnValue(false);
|
||||
|
||||
const toolCalls = [
|
||||
@@ -234,44 +269,64 @@ describe('App', () => {
|
||||
vi.spyOn(configWithExperiment, 'isTrustedFolder').mockReturnValue(true);
|
||||
vi.spyOn(configWithExperiment, 'getIdeMode').mockReturnValue(false);
|
||||
|
||||
const { lastFrame } = renderWithProviders(<App />, {
|
||||
uiState: stateWithConfirmingTool,
|
||||
config: configWithExperiment,
|
||||
});
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<App />,
|
||||
{
|
||||
uiState: stateWithConfirmingTool,
|
||||
config: configWithExperiment,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
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', () => {
|
||||
it('renders default layout correctly', async () => {
|
||||
(useIsScreenReaderEnabled as Mock).mockReturnValue(false);
|
||||
const { lastFrame } = renderWithProviders(<App />, {
|
||||
uiState: mockUIState,
|
||||
});
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<App />,
|
||||
{
|
||||
uiState: mockUIState,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders screen reader layout correctly', () => {
|
||||
it('renders screen reader layout correctly', async () => {
|
||||
(useIsScreenReaderEnabled as Mock).mockReturnValue(true);
|
||||
const { lastFrame } = renderWithProviders(<App />, {
|
||||
uiState: mockUIState,
|
||||
});
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<App />,
|
||||
{
|
||||
uiState: mockUIState,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders with dialogs visible', () => {
|
||||
it('renders with dialogs visible', async () => {
|
||||
const dialogUIState = {
|
||||
...mockUIState,
|
||||
dialogsVisible: true,
|
||||
} as UIState;
|
||||
const { lastFrame } = renderWithProviders(<App />, {
|
||||
uiState: dialogUIState,
|
||||
});
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<App />,
|
||||
{
|
||||
uiState: dialogUIState,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,9 +14,8 @@ import {
|
||||
type Mock,
|
||||
type MockedObject,
|
||||
} from 'vitest';
|
||||
import { render, persistentStateMock } from '../test-utils/render.js';
|
||||
import { render, cleanup, 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';
|
||||
@@ -49,6 +48,15 @@ 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 =
|
||||
@@ -165,6 +173,12 @@ 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(),
|
||||
}));
|
||||
@@ -172,6 +186,7 @@ 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');
|
||||
@@ -280,6 +295,7 @@ 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',
|
||||
@@ -417,6 +433,10 @@ describe('AppContainer State Management', () => {
|
||||
shouldShowFocusHint: false,
|
||||
inactivityStatus: 'none',
|
||||
});
|
||||
mockedUseFocusState.mockReturnValue({
|
||||
isFocused: true,
|
||||
hasReceivedFocusEvent: true,
|
||||
});
|
||||
|
||||
// Mock Config
|
||||
mockConfig = makeFakeConfig();
|
||||
@@ -525,6 +545,358 @@ 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,
|
||||
|
||||
@@ -79,6 +79,8 @@ import {
|
||||
type AgentsDiscoveredPayload,
|
||||
ChangeAuthRequestedError,
|
||||
CoreToolCallStatus,
|
||||
generateSteeringAckMessage,
|
||||
buildUserSteeringHintPrompt,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { validateAuthMethod } from '../config/auth.js';
|
||||
import process from 'node:process';
|
||||
@@ -156,6 +158,8 @@ 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) => {
|
||||
@@ -209,6 +213,7 @@ const SHELL_HEIGHT_PADDING = 10;
|
||||
export const AppContainer = (props: AppContainerProps) => {
|
||||
const { config, initializationResult, resumedSessionData } = props;
|
||||
const settings = useSettings();
|
||||
const notificationsEnabled = isNotificationsEnabled(settings);
|
||||
|
||||
const historyManager = useHistory({
|
||||
chatRecordingService: config.getGeminiClient()?.getChatRecordingService(),
|
||||
@@ -694,6 +699,7 @@ 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) {
|
||||
@@ -993,6 +999,30 @@ 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,
|
||||
@@ -1031,6 +1061,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
terminalWidth,
|
||||
terminalHeight,
|
||||
embeddedShellFocused,
|
||||
consumePendingHints,
|
||||
);
|
||||
|
||||
toggleBackgroundShellRef.current = toggleBackgroundShell;
|
||||
@@ -1139,10 +1170,38 @@ 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) => {
|
||||
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) {
|
||||
@@ -1184,7 +1243,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
isMcpReady,
|
||||
streamingState,
|
||||
messageQueue.length,
|
||||
pendingSlashCommandHistoryItems,
|
||||
pendingGeminiHistoryItems,
|
||||
config,
|
||||
handleHintSubmit,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1247,7 +1309,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
sanitizationConfig: config.sanitizationConfig,
|
||||
});
|
||||
|
||||
const isFocused = useFocus();
|
||||
const { isFocused, hasReceivedFocusEvent } = useFocus();
|
||||
|
||||
// Context file names computation
|
||||
const contextFileNames = useMemo(() => {
|
||||
@@ -1879,12 +1941,17 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
[pendingHistoryItems],
|
||||
);
|
||||
|
||||
const hasConfirmUpdateExtensionRequests =
|
||||
confirmUpdateExtensionRequests.length > 0;
|
||||
const hasLoopDetectionConfirmationRequest =
|
||||
!!loopDetectionConfirmationRequest;
|
||||
|
||||
const hasPendingActionRequired =
|
||||
hasPendingToolConfirmation ||
|
||||
!!commandConfirmationRequest ||
|
||||
!!authConsentRequest ||
|
||||
confirmUpdateExtensionRequests.length > 0 ||
|
||||
!!loopDetectionConfirmationRequest ||
|
||||
hasConfirmUpdateExtensionRequests ||
|
||||
hasLoopDetectionConfirmationRequest ||
|
||||
!!proQuotaRequest ||
|
||||
!!validationRequest ||
|
||||
!!customDialog;
|
||||
@@ -1902,6 +1969,20 @@ 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 &&
|
||||
@@ -1917,6 +1998,44 @@ 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
|
||||
@@ -2083,6 +2202,13 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
isBackgroundShellListOpen,
|
||||
adminSettingsChanged,
|
||||
newAgents,
|
||||
hintMode:
|
||||
config.isModelSteeringEnabled() &&
|
||||
isToolExecuting([
|
||||
...pendingSlashCommandHistoryItems,
|
||||
...pendingGeminiHistoryItems,
|
||||
]),
|
||||
hintBuffer: '',
|
||||
}),
|
||||
[
|
||||
isThemeDialogOpen,
|
||||
@@ -2254,6 +2380,10 @@ 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();
|
||||
|
||||
@@ -11,8 +11,6 @@ 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 =
|
||||
@@ -56,128 +54,129 @@ describe('IdeIntegrationNudge', () => {
|
||||
});
|
||||
|
||||
it('renders correctly with default options', async () => {
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<KeypressProvider>
|
||||
<IdeIntegrationNudge {...defaultProps} />
|
||||
</KeypressProvider>,
|
||||
);
|
||||
await act(async () => {
|
||||
await delay(100);
|
||||
});
|
||||
await waitUntilReady();
|
||||
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 } = render(
|
||||
const { stdin, waitUntilReady, unmount } = render(
|
||||
<KeypressProvider>
|
||||
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />
|
||||
</KeypressProvider>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await delay(100);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
// "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 } = render(
|
||||
const { stdin, waitUntilReady, unmount } = render(
|
||||
<KeypressProvider>
|
||||
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />
|
||||
</KeypressProvider>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await delay(100);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
// 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 } = render(
|
||||
const { stdin, waitUntilReady, unmount } = render(
|
||||
<KeypressProvider>
|
||||
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />
|
||||
</KeypressProvider>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await delay(100);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
// 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 } = render(
|
||||
const { stdin, waitUntilReady, unmount } = render(
|
||||
<KeypressProvider>
|
||||
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />
|
||||
</KeypressProvider>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await delay(100);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
// Press Escape
|
||||
await act(async () => {
|
||||
stdin.write('\u001B');
|
||||
await delay(100);
|
||||
});
|
||||
// Escape key has a timeout in KeypressContext, so we need to wrap waitUntilReady in act
|
||||
await act(async () => {
|
||||
await waitUntilReady();
|
||||
});
|
||||
|
||||
expect(onComplete).toHaveBeenCalledWith({
|
||||
userSelection: 'no',
|
||||
isExtensionPreInstalled: false,
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('displays correct text and handles selection when extension is pre-installed', async () => {
|
||||
@@ -185,15 +184,13 @@ describe('IdeIntegrationNudge', () => {
|
||||
vi.stubEnv('GEMINI_CLI_IDE_WORKSPACE_PATH', '/tmp');
|
||||
|
||||
const onComplete = vi.fn();
|
||||
const { lastFrame, stdin } = render(
|
||||
const { lastFrame, stdin, waitUntilReady, unmount } = render(
|
||||
<KeypressProvider>
|
||||
<IdeIntegrationNudge {...defaultProps} onComplete={onComplete} />
|
||||
</KeypressProvider>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await delay(100);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
const frame = lastFrame();
|
||||
|
||||
@@ -205,12 +202,13 @@ describe('IdeIntegrationNudge', () => {
|
||||
// Select "Yes"
|
||||
await act(async () => {
|
||||
stdin.write('\r');
|
||||
await delay(100);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
expect(onComplete).toHaveBeenCalledWith({
|
||||
userSelection: 'yes',
|
||||
isExtensionPreInstalled: true,
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -61,7 +61,8 @@ 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`] = `
|
||||
@@ -124,19 +125,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,21 +65,24 @@ describe('ApiAuthDialog', () => {
|
||||
mockedUseTextBuffer.mockReturnValue(mockBuffer);
|
||||
});
|
||||
|
||||
it('renders correctly', () => {
|
||||
const { lastFrame } = render(
|
||||
it('renders correctly', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<ApiAuthDialog onSubmit={onSubmit} onCancel={onCancel} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders with a defaultValue', () => {
|
||||
render(
|
||||
it('renders with a defaultValue', async () => {
|
||||
const { waitUntilReady, unmount } = render(
|
||||
<ApiAuthDialog
|
||||
onSubmit={onSubmit}
|
||||
onCancel={onCancel}
|
||||
defaultValue="test-key"
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(mockedUseTextBuffer).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
initialText: 'test-key',
|
||||
@@ -88,6 +91,7 @@ describe('ApiAuthDialog', () => {
|
||||
}),
|
||||
}),
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -100,9 +104,12 @@ describe('ApiAuthDialog', () => {
|
||||
{ keyName: 'escape', sequence: '\u001b', expectedCall: onCancel, args: [] },
|
||||
])(
|
||||
'calls $expectedCall.name when $keyName is pressed',
|
||||
({ keyName, sequence, expectedCall, args }) => {
|
||||
async ({ keyName, sequence, expectedCall, args }) => {
|
||||
mockBuffer.text = 'submitted-key'; // Set for the onSubmit case
|
||||
render(<ApiAuthDialog onSubmit={onSubmit} onCancel={onCancel} />);
|
||||
const { waitUntilReady, unmount } = render(
|
||||
<ApiAuthDialog onSubmit={onSubmit} onCancel={onCancel} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
// 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];
|
||||
@@ -117,23 +124,29 @@ describe('ApiAuthDialog', () => {
|
||||
});
|
||||
|
||||
expect(expectedCall).toHaveBeenCalledWith(...args);
|
||||
unmount();
|
||||
},
|
||||
);
|
||||
|
||||
it('displays an error message', () => {
|
||||
const { lastFrame } = render(
|
||||
it('displays an error message', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = 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 () => {
|
||||
render(<ApiAuthDialog onSubmit={onSubmit} onCancel={onCancel} />);
|
||||
const { waitUntilReady, unmount } = render(
|
||||
<ApiAuthDialog onSubmit={onSubmit} onCancel={onCancel} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
// Call 0 is ApiAuthDialog (isActive: true)
|
||||
// Call 1 is TextInput (isActive: true, priority: true)
|
||||
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
|
||||
@@ -149,5 +162,6 @@ describe('ApiAuthDialog', () => {
|
||||
expect(clearApiKey).toHaveBeenCalled();
|
||||
expect(mockBuffer.setText).toHaveBeenCalledWith('');
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -139,11 +139,14 @@ describe('AuthDialog', () => {
|
||||
},
|
||||
])(
|
||||
'correctly shows/hides COMPUTE_ADC options $desc',
|
||||
({ env, shouldContain, shouldNotContain }) => {
|
||||
async ({ env, shouldContain, shouldNotContain }) => {
|
||||
for (const [key, value] of Object.entries(env)) {
|
||||
vi.stubEnv(key, value as string);
|
||||
}
|
||||
renderWithProviders(<AuthDialog {...props} />);
|
||||
const { waitUntilReady, unmount } = renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const items = mockedRadioButtonSelect.mock.calls[0][0].items;
|
||||
for (const item of shouldContain) {
|
||||
expect(items).toContainEqual(item);
|
||||
@@ -151,23 +154,32 @@ describe('AuthDialog', () => {
|
||||
for (const item of shouldNotContain) {
|
||||
expect(items).not.toContainEqual(item);
|
||||
}
|
||||
unmount();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('filters auth types when enforcedType is set', () => {
|
||||
it('filters auth types when enforcedType is set', async () => {
|
||||
props.settings.merged.security.auth.enforcedType = AuthType.USE_GEMINI;
|
||||
renderWithProviders(<AuthDialog {...props} />);
|
||||
const { waitUntilReady, unmount } = renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
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', () => {
|
||||
it('sets initial index to 0 when enforcedType is set', async () => {
|
||||
props.settings.merged.security.auth.enforcedType = AuthType.USE_GEMINI;
|
||||
renderWithProviders(<AuthDialog {...props} />);
|
||||
const { waitUntilReady, unmount } = renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { initialIndex } = mockedRadioButtonSelect.mock.calls[0][0];
|
||||
expect(initialIndex).toBe(0);
|
||||
unmount();
|
||||
});
|
||||
|
||||
describe('Initial Auth Type Selection', () => {
|
||||
@@ -199,18 +211,25 @@ describe('AuthDialog', () => {
|
||||
expected: AuthType.LOGIN_WITH_GOOGLE,
|
||||
desc: 'defaults to Login with Google',
|
||||
},
|
||||
])('selects initial auth type $desc', ({ setup, expected }) => {
|
||||
])('selects initial auth type $desc', async ({ setup, expected }) => {
|
||||
setup();
|
||||
renderWithProviders(<AuthDialog {...props} />);
|
||||
const { waitUntilReady, unmount } = renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { items, initialIndex } = mockedRadioButtonSelect.mock.calls[0][0];
|
||||
expect(items[initialIndex].value).toBe(expected);
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleAuthSelect', () => {
|
||||
it('calls onAuthError if validation fails', () => {
|
||||
it('calls onAuthError if validation fails', async () => {
|
||||
mockedValidateAuthMethod.mockReturnValue('Invalid method');
|
||||
renderWithProviders(<AuthDialog {...props} />);
|
||||
const { waitUntilReady, unmount } = renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { onSelect: handleAuthSelect } =
|
||||
mockedRadioButtonSelect.mock.calls[0][0];
|
||||
handleAuthSelect(AuthType.USE_GEMINI);
|
||||
@@ -221,11 +240,15 @@ 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);
|
||||
renderWithProviders(<AuthDialog {...props} />);
|
||||
const { waitUntilReady, unmount } = renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { onSelect: handleAuthSelect } =
|
||||
mockedRadioButtonSelect.mock.calls[0][0];
|
||||
await handleAuthSelect(AuthType.LOGIN_WITH_GOOGLE);
|
||||
@@ -233,16 +256,21 @@ describe('AuthDialog', () => {
|
||||
expect(props.setAuthContext).toHaveBeenCalledWith({
|
||||
requiresRestart: true,
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('sets auth context with empty object for other auth types', async () => {
|
||||
mockedValidateAuthMethod.mockReturnValue(null);
|
||||
renderWithProviders(<AuthDialog {...props} />);
|
||||
const { waitUntilReady, unmount } = renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
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 () => {
|
||||
@@ -250,7 +278,10 @@ describe('AuthDialog', () => {
|
||||
vi.stubEnv('GEMINI_API_KEY', 'test-key-from-env');
|
||||
// props.settings.merged.security.auth.selectedType is undefined here, simulating initial setup
|
||||
|
||||
renderWithProviders(<AuthDialog {...props} />);
|
||||
const { waitUntilReady, unmount } = renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { onSelect: handleAuthSelect } =
|
||||
mockedRadioButtonSelect.mock.calls[0][0];
|
||||
await handleAuthSelect(AuthType.USE_GEMINI);
|
||||
@@ -258,6 +289,7 @@ describe('AuthDialog', () => {
|
||||
expect(props.setAuthState).toHaveBeenCalledWith(
|
||||
AuthState.Unauthenticated,
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('skips API key dialog if env var is present but empty', async () => {
|
||||
@@ -265,7 +297,10 @@ describe('AuthDialog', () => {
|
||||
vi.stubEnv('GEMINI_API_KEY', ''); // Empty string
|
||||
// props.settings.merged.security.auth.selectedType is undefined here
|
||||
|
||||
renderWithProviders(<AuthDialog {...props} />);
|
||||
const { waitUntilReady, unmount } = renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { onSelect: handleAuthSelect } =
|
||||
mockedRadioButtonSelect.mock.calls[0][0];
|
||||
await handleAuthSelect(AuthType.USE_GEMINI);
|
||||
@@ -273,6 +308,7 @@ describe('AuthDialog', () => {
|
||||
expect(props.setAuthState).toHaveBeenCalledWith(
|
||||
AuthState.Unauthenticated,
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('shows API key dialog on initial setup if no env var is present', async () => {
|
||||
@@ -280,7 +316,10 @@ describe('AuthDialog', () => {
|
||||
// process.env['GEMINI_API_KEY'] is not set
|
||||
// props.settings.merged.security.auth.selectedType is undefined here, simulating initial setup
|
||||
|
||||
renderWithProviders(<AuthDialog {...props} />);
|
||||
const { waitUntilReady, unmount } = renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { onSelect: handleAuthSelect } =
|
||||
mockedRadioButtonSelect.mock.calls[0][0];
|
||||
await handleAuthSelect(AuthType.USE_GEMINI);
|
||||
@@ -288,6 +327,7 @@ 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 () => {
|
||||
@@ -297,7 +337,10 @@ describe('AuthDialog', () => {
|
||||
props.settings.merged.security.auth.selectedType =
|
||||
AuthType.LOGIN_WITH_GOOGLE;
|
||||
|
||||
renderWithProviders(<AuthDialog {...props} />);
|
||||
const { waitUntilReady, unmount } = renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { onSelect: handleAuthSelect } =
|
||||
mockedRadioButtonSelect.mock.calls[0][0];
|
||||
await handleAuthSelect(AuthType.USE_GEMINI);
|
||||
@@ -305,6 +348,7 @@ describe('AuthDialog', () => {
|
||||
expect(props.setAuthState).toHaveBeenCalledWith(
|
||||
AuthState.Unauthenticated,
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('exits process for Login with Google when browser is suppressed', async () => {
|
||||
@@ -316,7 +360,10 @@ describe('AuthDialog', () => {
|
||||
vi.mocked(props.config.isBrowserLaunchSuppressed).mockReturnValue(true);
|
||||
mockedValidateAuthMethod.mockReturnValue(null);
|
||||
|
||||
renderWithProviders(<AuthDialog {...props} />);
|
||||
const { waitUntilReady, unmount } = renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const { onSelect: handleAuthSelect } =
|
||||
mockedRadioButtonSelect.mock.calls[0][0];
|
||||
await act(async () => {
|
||||
@@ -330,13 +377,18 @@ describe('AuthDialog', () => {
|
||||
exitSpy.mockRestore();
|
||||
logSpy.mockRestore();
|
||||
vi.useRealTimers();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it('displays authError when provided', () => {
|
||||
it('displays authError when provided', async () => {
|
||||
props.authError = 'Something went wrong';
|
||||
const { lastFrame } = renderWithProviders(<AuthDialog {...props} />);
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Something went wrong');
|
||||
unmount();
|
||||
});
|
||||
|
||||
describe('useKeypress', () => {
|
||||
@@ -375,31 +427,47 @@ describe('AuthDialog', () => {
|
||||
expect(p.settings.setValue).not.toHaveBeenCalled();
|
||||
},
|
||||
},
|
||||
])('$desc', ({ setup, expectations }) => {
|
||||
])('$desc', async ({ setup, expectations }) => {
|
||||
setup();
|
||||
renderWithProviders(<AuthDialog {...props} />);
|
||||
const { waitUntilReady, unmount } = renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
|
||||
keypressHandler({ name: 'escape' });
|
||||
expectations(props);
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Snapshots', () => {
|
||||
it('renders correctly with default props', () => {
|
||||
const { lastFrame } = renderWithProviders(<AuthDialog {...props} />);
|
||||
it('renders correctly with default props', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders correctly with auth error', () => {
|
||||
it('renders correctly with auth error', async () => {
|
||||
props.authError = 'Something went wrong';
|
||||
const { lastFrame } = renderWithProviders(<AuthDialog {...props} />);
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders correctly with enforced auth type', () => {
|
||||
it('renders correctly with enforced auth type', async () => {
|
||||
props.settings.merged.security.auth.enforcedType = AuthType.USE_GEMINI;
|
||||
const { lastFrame } = renderWithProviders(<AuthDialog {...props} />);
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<AuthDialog {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -54,48 +54,80 @@ describe('AuthInProgress', () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('renders initial state with spinner', () => {
|
||||
const { lastFrame } = render(<AuthInProgress onTimeout={onTimeout} />);
|
||||
it('renders initial state with spinner', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<AuthInProgress onTimeout={onTimeout} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
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', () => {
|
||||
render(<AuthInProgress onTimeout={onTimeout} />);
|
||||
it('calls onTimeout when ESC is pressed', async () => {
|
||||
const { waitUntilReady, unmount } = render(
|
||||
<AuthInProgress onTimeout={onTimeout} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const keypressHandler = vi.mocked(useKeypress).mock.calls[0][0];
|
||||
|
||||
keypressHandler({ name: 'escape' } as unknown as Key);
|
||||
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();
|
||||
});
|
||||
|
||||
expect(onTimeout).toHaveBeenCalled();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('calls onTimeout when Ctrl+C is pressed', () => {
|
||||
render(<AuthInProgress onTimeout={onTimeout} />);
|
||||
it('calls onTimeout when Ctrl+C is pressed', async () => {
|
||||
const { waitUntilReady, unmount } = render(
|
||||
<AuthInProgress onTimeout={onTimeout} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const keypressHandler = vi.mocked(useKeypress).mock.calls[0][0];
|
||||
|
||||
keypressHandler({ name: 'c', ctrl: true } as unknown as Key);
|
||||
await act(async () => {
|
||||
keypressHandler({ name: 'c', ctrl: true } as unknown as Key);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
expect(onTimeout).toHaveBeenCalled();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('calls onTimeout and shows timeout message after 3 minutes', async () => {
|
||||
const { lastFrame } = render(<AuthInProgress onTimeout={onTimeout} />);
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<AuthInProgress onTimeout={onTimeout} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(180000);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
expect(onTimeout).toHaveBeenCalled();
|
||||
await vi.waitUntil(
|
||||
() => lastFrame()?.includes('Authentication timed out'),
|
||||
{ timeout: 1000 },
|
||||
);
|
||||
expect(lastFrame()).toContain('Authentication timed out');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('clears timer on unmount', () => {
|
||||
const { unmount } = render(<AuthInProgress onTimeout={onTimeout} />);
|
||||
act(() => {
|
||||
it('clears timer on unmount', async () => {
|
||||
const { waitUntilReady, unmount } = render(
|
||||
<AuthInProgress onTimeout={onTimeout} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
await act(async () => {
|
||||
unmount();
|
||||
});
|
||||
vi.advanceTimersByTime(180000);
|
||||
|
||||
await act(async () => {
|
||||
vi.advanceTimersByTime(180000);
|
||||
});
|
||||
expect(onTimeout).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,23 +40,26 @@ describe('LoginWithGoogleRestartDialog', () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('renders correctly', () => {
|
||||
const { lastFrame } = render(
|
||||
it('renders correctly', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<LoginWithGoogleRestartDialog
|
||||
onDismiss={onDismiss}
|
||||
config={mockConfig}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('calls onDismiss when escape is pressed', () => {
|
||||
render(
|
||||
it('calls onDismiss when escape is pressed', async () => {
|
||||
const { waitUntilReady, unmount } = render(
|
||||
<LoginWithGoogleRestartDialog
|
||||
onDismiss={onDismiss}
|
||||
config={mockConfig}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
|
||||
|
||||
keypressHandler({
|
||||
@@ -68,6 +71,7 @@ describe('LoginWithGoogleRestartDialog', () => {
|
||||
});
|
||||
|
||||
expect(onDismiss).toHaveBeenCalledTimes(1);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it.each(['r', 'R'])(
|
||||
@@ -75,12 +79,13 @@ describe('LoginWithGoogleRestartDialog', () => {
|
||||
async (keyName) => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
render(
|
||||
const { waitUntilReady, unmount } = render(
|
||||
<LoginWithGoogleRestartDialog
|
||||
onDismiss={onDismiss}
|
||||
config={mockConfig}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
|
||||
|
||||
keypressHandler({
|
||||
@@ -98,6 +103,7 @@ describe('LoginWithGoogleRestartDialog', () => {
|
||||
expect(exitSpy).toHaveBeenCalledWith(RELAUNCH_EXIT_CODE);
|
||||
|
||||
vi.useRealTimers();
|
||||
unmount();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -14,5 +14,6 @@ exports[`ApiAuthDialog > renders correctly 1`] = `
|
||||
│ │
|
||||
│ (Press Enter to submit, Esc to cancel, Ctrl+C to clear stored key) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -1,57 +1,60 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`AuthDialog > Snapshots > renders correctly with auth error 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ ? Get started │
|
||||
│ │
|
||||
│ How would you like to authenticate for this project? │
|
||||
│ │
|
||||
│ (selected) Login with Google(not selected) Use Gemini API Key(not selected) Vertex AI │
|
||||
│ │
|
||||
│ Something went wrong │
|
||||
│ │
|
||||
│ (Use Enter to select) │
|
||||
│ │
|
||||
│ Terms of Services and Privacy Notice for Gemini CLI │
|
||||
│ │
|
||||
│ https://github.com/google-gemini/gemini-cli/blob/main/docs/tos-privacy.md │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ ? Get started │
|
||||
│ │
|
||||
│ How would you like to authenticate for this project? │
|
||||
│ │
|
||||
│ (selected) Login with Google(not selected) Use Gemini API Key(not selected) Vertex AI │
|
||||
│ │
|
||||
│ Something went wrong │
|
||||
│ │
|
||||
│ (Use Enter to select) │
|
||||
│ │
|
||||
│ Terms of Services and Privacy Notice for Gemini CLI │
|
||||
│ │
|
||||
│ https://github.com/google-gemini/gemini-cli/blob/main/docs/tos-privacy.md │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`AuthDialog > Snapshots > renders correctly with default props 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ ? Get started │
|
||||
│ │
|
||||
│ How would you like to authenticate for this project? │
|
||||
│ │
|
||||
│ (selected) Login with Google(not selected) Use Gemini API Key(not selected) Vertex AI │
|
||||
│ │
|
||||
│ (Use Enter to select) │
|
||||
│ │
|
||||
│ Terms of Services and Privacy Notice for Gemini CLI │
|
||||
│ │
|
||||
│ https://github.com/google-gemini/gemini-cli/blob/main/docs/tos-privacy.md │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ ? Get started │
|
||||
│ │
|
||||
│ How would you like to authenticate for this project? │
|
||||
│ │
|
||||
│ (selected) Login with Google(not selected) Use Gemini API Key(not selected) Vertex AI │
|
||||
│ │
|
||||
│ (Use Enter to select) │
|
||||
│ │
|
||||
│ Terms of Services and Privacy Notice for Gemini CLI │
|
||||
│ │
|
||||
│ https://github.com/google-gemini/gemini-cli/blob/main/docs/tos-privacy.md │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`AuthDialog > Snapshots > renders correctly with enforced auth type 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ ? Get started │
|
||||
│ │
|
||||
│ How would you like to authenticate for this project? │
|
||||
│ │
|
||||
│ (selected) Use Gemini API Key │
|
||||
│ │
|
||||
│ (Use Enter to select) │
|
||||
│ │
|
||||
│ Terms of Services and Privacy Notice for Gemini CLI │
|
||||
│ │
|
||||
│ https://github.com/google-gemini/gemini-cli/blob/main/docs/tos-privacy.md │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ ? Get started │
|
||||
│ │
|
||||
│ How would you like to authenticate for this project? │
|
||||
│ │
|
||||
│ (selected) Use Gemini API Key │
|
||||
│ │
|
||||
│ (Use Enter to select) │
|
||||
│ │
|
||||
│ Terms of Services and Privacy Notice for Gemini CLI │
|
||||
│ │
|
||||
│ https://github.com/google-gemini/gemini-cli/blob/main/docs/tos-privacy.md │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -4,5 +4,6 @@ exports[`LoginWithGoogleRestartDialog > renders correctly 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ You have successfully logged in with Google. Gemini CLI needs to be restarted. Press 'r' to │
|
||||
│ restart, or 'escape' to choose a different auth method. │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -27,9 +27,11 @@ import { uiTelemetryService } from '@google/gemini-cli-core';
|
||||
describe('clearCommand', () => {
|
||||
let mockContext: CommandContext;
|
||||
let mockResetChat: ReturnType<typeof vi.fn>;
|
||||
let mockHintClear: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockResetChat = vi.fn().mockResolvedValue(undefined);
|
||||
mockHintClear = vi.fn();
|
||||
const mockGetChatRecordingService = vi.fn();
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -50,12 +52,15 @@ describe('clearCommand', () => {
|
||||
fireSessionEndEvent: vi.fn().mockResolvedValue(undefined),
|
||||
fireSessionStartEvent: vi.fn().mockResolvedValue(undefined),
|
||||
}),
|
||||
userHintService: {
|
||||
clear: mockHintClear,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should set debug message, reset chat, reset telemetry, and clear UI when config is available', async () => {
|
||||
it('should set debug message, reset chat, reset telemetry, clear hints, and clear UI when config is available', async () => {
|
||||
if (!clearCommand.action) {
|
||||
throw new Error('clearCommand must have an action.');
|
||||
}
|
||||
@@ -68,6 +73,7 @@ describe('clearCommand', () => {
|
||||
expect(mockContext.ui.setDebugMessage).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(mockResetChat).toHaveBeenCalledTimes(1);
|
||||
expect(mockHintClear).toHaveBeenCalledTimes(1);
|
||||
expect(uiTelemetryService.setLastPromptTokenCount).toHaveBeenCalledWith(0);
|
||||
expect(uiTelemetryService.setLastPromptTokenCount).toHaveBeenCalledTimes(1);
|
||||
expect(mockContext.ui.clear).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -43,6 +43,9 @@ export const clearCommand: SlashCommand = {
|
||||
context.ui.setDebugMessage('Clearing terminal.');
|
||||
}
|
||||
|
||||
// Reset user steering hints
|
||||
config?.userHintService.clear();
|
||||
|
||||
// Start a new conversation recording with a new session ID
|
||||
if (config && chatRecordingService) {
|
||||
const newSessionId = randomUUID();
|
||||
|
||||
@@ -290,7 +290,7 @@ describe('extensionsCommand', () => {
|
||||
|
||||
it('should inform user if there are no extensions to update with --all', async () => {
|
||||
mockDispatchExtensionState.mockImplementationOnce(
|
||||
(action: ExtensionUpdateAction) => {
|
||||
async (action: ExtensionUpdateAction) => {
|
||||
if (action.type === 'SCHEDULE_UPDATE') {
|
||||
action.payload.onComplete([]);
|
||||
}
|
||||
@@ -306,7 +306,7 @@ describe('extensionsCommand', () => {
|
||||
|
||||
it('should call setPendingItem and addItem in a finally block on success', async () => {
|
||||
mockDispatchExtensionState.mockImplementationOnce(
|
||||
(action: ExtensionUpdateAction) => {
|
||||
async (action: ExtensionUpdateAction) => {
|
||||
if (action.type === 'SCHEDULE_UPDATE') {
|
||||
action.payload.onComplete([
|
||||
{
|
||||
@@ -357,7 +357,7 @@ describe('extensionsCommand', () => {
|
||||
|
||||
it('should update a single extension by name', async () => {
|
||||
mockDispatchExtensionState.mockImplementationOnce(
|
||||
(action: ExtensionUpdateAction) => {
|
||||
async (action: ExtensionUpdateAction) => {
|
||||
if (action.type === 'SCHEDULE_UPDATE') {
|
||||
action.payload.onComplete([
|
||||
{
|
||||
@@ -382,7 +382,7 @@ describe('extensionsCommand', () => {
|
||||
|
||||
it('should update multiple extensions by name', async () => {
|
||||
mockDispatchExtensionState.mockImplementationOnce(
|
||||
(action: ExtensionUpdateAction) => {
|
||||
async (action: ExtensionUpdateAction) => {
|
||||
if (action.type === 'SCHEDULE_UPDATE') {
|
||||
action.payload.onComplete([
|
||||
{
|
||||
|
||||
@@ -24,8 +24,11 @@ describe('AboutBox', () => {
|
||||
ideClient: '',
|
||||
};
|
||||
|
||||
it('renders with required props', () => {
|
||||
const { lastFrame } = renderWithProviders(<AboutBox {...defaultProps} />);
|
||||
it('renders with required props', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<AboutBox {...defaultProps} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('About Gemini CLI');
|
||||
expect(output).toContain('1.0.0');
|
||||
@@ -34,31 +37,44 @@ describe('AboutBox', () => {
|
||||
expect(output).toContain('default');
|
||||
expect(output).toContain('macOS');
|
||||
expect(output).toContain('Logged in with Google');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['gcpProject', 'my-project', 'GCP Project'],
|
||||
['ideClient', 'vscode', 'IDE Client'],
|
||||
['tier', 'Enterprise', 'Tier'],
|
||||
])('renders optional prop %s', (prop, value, label) => {
|
||||
])('renders optional prop %s', async (prop, value, label) => {
|
||||
const props = { ...defaultProps, [prop]: value };
|
||||
const { lastFrame } = renderWithProviders(<AboutBox {...props} />);
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<AboutBox {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain(label);
|
||||
expect(output).toContain(value);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders Auth Method with email when userEmail is provided', () => {
|
||||
it('renders Auth Method with email when userEmail is provided', async () => {
|
||||
const props = { ...defaultProps, userEmail: 'test@example.com' };
|
||||
const { lastFrame } = renderWithProviders(<AboutBox {...props} />);
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<AboutBox {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Logged in with Google (test@example.com)');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders Auth Method correctly when not oauth', () => {
|
||||
it('renders Auth Method correctly when not oauth', async () => {
|
||||
const props = { ...defaultProps, selectedAuthType: 'api-key' };
|
||||
const { lastFrame } = renderWithProviders(<AboutBox {...props} />);
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<AboutBox {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('api-key');
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,17 +16,24 @@ describe('AdminSettingsChangedDialog', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('renders correctly', () => {
|
||||
const { lastFrame } = renderWithProviders(<AdminSettingsChangedDialog />);
|
||||
it('renders correctly', async () => {
|
||||
const { lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<AdminSettingsChangedDialog />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('restarts on "r" key press', async () => {
|
||||
const { stdin } = renderWithProviders(<AdminSettingsChangedDialog />, {
|
||||
uiActions: {
|
||||
handleRestart: handleRestartMock,
|
||||
const { stdin, waitUntilReady } = renderWithProviders(
|
||||
<AdminSettingsChangedDialog />,
|
||||
{
|
||||
uiActions: {
|
||||
handleRestart: handleRestartMock,
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
act(() => {
|
||||
stdin.write('r');
|
||||
@@ -36,11 +43,15 @@ describe('AdminSettingsChangedDialog', () => {
|
||||
});
|
||||
|
||||
it.each(['r', 'R'])('restarts on "%s" key press', async (key) => {
|
||||
const { stdin } = renderWithProviders(<AdminSettingsChangedDialog />, {
|
||||
uiActions: {
|
||||
handleRestart: handleRestartMock,
|
||||
const { stdin, waitUntilReady } = renderWithProviders(
|
||||
<AdminSettingsChangedDialog />,
|
||||
{
|
||||
uiActions: {
|
||||
handleRestart: handleRestartMock,
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
act(() => {
|
||||
stdin.write(key);
|
||||
|
||||
@@ -118,11 +118,11 @@ describe('AgentConfigDialog', () => {
|
||||
mockOnSave = vi.fn();
|
||||
});
|
||||
|
||||
const renderDialog = (
|
||||
const renderDialog = async (
|
||||
settings: LoadedSettings,
|
||||
definition: AgentDefinition = createMockAgentDefinition(),
|
||||
) =>
|
||||
render(
|
||||
) => {
|
||||
const result = render(
|
||||
<KeypressProvider>
|
||||
<AgentConfigDialog
|
||||
agentName="test-agent"
|
||||
@@ -134,18 +134,21 @@ describe('AgentConfigDialog', () => {
|
||||
/>
|
||||
</KeypressProvider>,
|
||||
);
|
||||
await result.waitUntilReady();
|
||||
return result;
|
||||
};
|
||||
|
||||
describe('rendering', () => {
|
||||
it('should render the dialog with title', () => {
|
||||
it('should render the dialog with title', async () => {
|
||||
const settings = createMockSettings();
|
||||
const { lastFrame } = renderDialog(settings);
|
||||
|
||||
const { lastFrame, unmount } = await renderDialog(settings);
|
||||
expect(lastFrame()).toContain('Configure: Test Agent');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render all configuration fields', () => {
|
||||
it('should render all configuration fields', async () => {
|
||||
const settings = createMockSettings();
|
||||
const { lastFrame } = renderDialog(settings);
|
||||
const { lastFrame, unmount } = await renderDialog(settings);
|
||||
const frame = lastFrame();
|
||||
|
||||
expect(frame).toContain('Enabled');
|
||||
@@ -156,44 +159,53 @@ describe('AgentConfigDialog', () => {
|
||||
expect(frame).toContain('Max Output Tokens');
|
||||
expect(frame).toContain('Max Time (minutes)');
|
||||
expect(frame).toContain('Max Turns');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render scope selector', () => {
|
||||
it('should render scope selector', async () => {
|
||||
const settings = createMockSettings();
|
||||
const { lastFrame } = renderDialog(settings);
|
||||
const { lastFrame, unmount } = await renderDialog(settings);
|
||||
|
||||
expect(lastFrame()).toContain('Apply To');
|
||||
expect(lastFrame()).toContain('User Settings');
|
||||
expect(lastFrame()).toContain('Workspace Settings');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render help text', () => {
|
||||
it('should render help text', async () => {
|
||||
const settings = createMockSettings();
|
||||
const { lastFrame } = renderDialog(settings);
|
||||
const { lastFrame, unmount } = await renderDialog(settings);
|
||||
|
||||
expect(lastFrame()).toContain('Use Enter to select');
|
||||
expect(lastFrame()).toContain('Tab to change focus');
|
||||
expect(lastFrame()).toContain('Esc to close');
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('keyboard navigation', () => {
|
||||
it('should close dialog on Escape', async () => {
|
||||
const settings = createMockSettings();
|
||||
const { stdin } = renderDialog(settings);
|
||||
const { stdin, waitUntilReady, unmount } = await renderDialog(settings);
|
||||
|
||||
await act(async () => {
|
||||
stdin.write(TerminalKeys.ESCAPE);
|
||||
});
|
||||
// Escape key has a 50ms timeout in KeypressContext, so we need to wrap waitUntilReady in act
|
||||
await act(async () => {
|
||||
await waitUntilReady();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnClose).toHaveBeenCalled();
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should navigate down with arrow key', async () => {
|
||||
const settings = createMockSettings();
|
||||
const { lastFrame, stdin } = renderDialog(settings);
|
||||
const { lastFrame, stdin, waitUntilReady, unmount } =
|
||||
await renderDialog(settings);
|
||||
|
||||
// Initially first item (Enabled) should be active
|
||||
expect(lastFrame()).toContain('●');
|
||||
@@ -202,16 +214,19 @@ describe('AgentConfigDialog', () => {
|
||||
await act(async () => {
|
||||
stdin.write(TerminalKeys.DOWN_ARROW);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
await waitFor(() => {
|
||||
// Model field should now be highlighted
|
||||
expect(lastFrame()).toContain('Model');
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should switch focus with Tab', async () => {
|
||||
const settings = createMockSettings();
|
||||
const { lastFrame, stdin } = renderDialog(settings);
|
||||
const { lastFrame, stdin, waitUntilReady, unmount } =
|
||||
await renderDialog(settings);
|
||||
|
||||
// Initially settings section is focused
|
||||
expect(lastFrame()).toContain('> Configure: Test Agent');
|
||||
@@ -220,22 +235,25 @@ describe('AgentConfigDialog', () => {
|
||||
await act(async () => {
|
||||
stdin.write(TerminalKeys.TAB);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('> Apply To');
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('boolean toggle', () => {
|
||||
it('should toggle enabled field on Enter', async () => {
|
||||
const settings = createMockSettings();
|
||||
const { stdin } = renderDialog(settings);
|
||||
const { stdin, waitUntilReady, unmount } = await renderDialog(settings);
|
||||
|
||||
// Press Enter to toggle the first field (Enabled)
|
||||
await act(async () => {
|
||||
stdin.write(TerminalKeys.ENTER);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(settings.setValue).toHaveBeenCalledWith(
|
||||
@@ -245,11 +263,12 @@ describe('AgentConfigDialog', () => {
|
||||
);
|
||||
expect(mockOnSave).toHaveBeenCalled();
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('default values', () => {
|
||||
it('should show values from agent definition as defaults', () => {
|
||||
it('should show values from agent definition as defaults', async () => {
|
||||
const definition = createMockAgentDefinition({
|
||||
modelConfig: {
|
||||
model: 'gemini-2.0-flash',
|
||||
@@ -263,29 +282,31 @@ describe('AgentConfigDialog', () => {
|
||||
},
|
||||
});
|
||||
const settings = createMockSettings();
|
||||
const { lastFrame } = renderDialog(settings, definition);
|
||||
const { lastFrame, unmount } = await renderDialog(settings, definition);
|
||||
const frame = lastFrame();
|
||||
|
||||
expect(frame).toContain('gemini-2.0-flash');
|
||||
expect(frame).toContain('0.7');
|
||||
expect(frame).toContain('10');
|
||||
expect(frame).toContain('20');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should show experimental agents as disabled by default', () => {
|
||||
it('should show experimental agents as disabled by default', async () => {
|
||||
const definition = createMockAgentDefinition({
|
||||
experimental: true,
|
||||
});
|
||||
const settings = createMockSettings();
|
||||
const { lastFrame } = renderDialog(settings, definition);
|
||||
const { lastFrame, unmount } = await renderDialog(settings, definition);
|
||||
|
||||
// Experimental agents default to disabled
|
||||
expect(lastFrame()).toContain('false');
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('existing overrides', () => {
|
||||
it('should show existing override values with * indicator', () => {
|
||||
it('should show existing override values with * indicator', async () => {
|
||||
const settings = createMockSettings({
|
||||
agents: {
|
||||
overrides: {
|
||||
@@ -298,12 +319,13 @@ describe('AgentConfigDialog', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const { lastFrame } = renderDialog(settings);
|
||||
const { lastFrame, unmount } = await renderDialog(settings);
|
||||
const frame = lastFrame();
|
||||
|
||||
// Should show the overridden values
|
||||
expect(frame).toContain('custom-model');
|
||||
expect(frame).toContain('false');
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -106,9 +106,9 @@ describe('AlternateBufferQuittingDisplay', () => {
|
||||
},
|
||||
};
|
||||
|
||||
it('renders with active and pending tool messages', () => {
|
||||
it('renders with active and pending tool messages', async () => {
|
||||
persistentStateMock.setData({ tipsShown: 0 });
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<AlternateBufferQuittingDisplay />,
|
||||
{
|
||||
uiState: {
|
||||
@@ -118,12 +118,14 @@ describe('AlternateBufferQuittingDisplay', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot('with_history_and_pending');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders with empty history and no pending items', () => {
|
||||
it('renders with empty history and no pending items', async () => {
|
||||
persistentStateMock.setData({ tipsShown: 0 });
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<AlternateBufferQuittingDisplay />,
|
||||
{
|
||||
uiState: {
|
||||
@@ -133,12 +135,14 @@ describe('AlternateBufferQuittingDisplay', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot('empty');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders with history but no pending items', () => {
|
||||
it('renders with history but no pending items', async () => {
|
||||
persistentStateMock.setData({ tipsShown: 0 });
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<AlternateBufferQuittingDisplay />,
|
||||
{
|
||||
uiState: {
|
||||
@@ -148,12 +152,14 @@ describe('AlternateBufferQuittingDisplay', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot('with_history_no_pending');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders with pending items but no history', () => {
|
||||
it('renders with pending items but no history', async () => {
|
||||
persistentStateMock.setData({ tipsShown: 0 });
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<AlternateBufferQuittingDisplay />,
|
||||
{
|
||||
uiState: {
|
||||
@@ -163,10 +169,12 @@ describe('AlternateBufferQuittingDisplay', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot('with_pending_no_history');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders with a tool awaiting confirmation', () => {
|
||||
it('renders with a tool awaiting confirmation', async () => {
|
||||
persistentStateMock.setData({ tipsShown: 0 });
|
||||
const pendingHistoryItems: HistoryItemWithoutId[] = [
|
||||
{
|
||||
@@ -187,7 +195,7 @@ describe('AlternateBufferQuittingDisplay', () => {
|
||||
],
|
||||
},
|
||||
];
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<AlternateBufferQuittingDisplay />,
|
||||
{
|
||||
uiState: {
|
||||
@@ -197,20 +205,22 @@ describe('AlternateBufferQuittingDisplay', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Action Required (was prompted):');
|
||||
expect(output).toContain('confirming_tool');
|
||||
expect(output).toContain('Confirming tool description');
|
||||
expect(output).toMatchSnapshot('with_confirming_tool');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders with user and gemini messages', () => {
|
||||
it('renders with user and gemini messages', async () => {
|
||||
persistentStateMock.setData({ tipsShown: 0 });
|
||||
const history: HistoryItem[] = [
|
||||
{ id: 1, type: 'user', text: 'Hello Gemini' },
|
||||
{ id: 2, type: 'gemini', text: 'Hello User!' },
|
||||
];
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<AlternateBufferQuittingDisplay />,
|
||||
{
|
||||
uiState: {
|
||||
@@ -220,6 +230,8 @@ describe('AlternateBufferQuittingDisplay', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot('with_user_gemini_messages');
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,15 +22,19 @@ const createAnsiToken = (overrides: Partial<AnsiToken>): AnsiToken => ({
|
||||
});
|
||||
|
||||
describe('<AnsiOutputText />', () => {
|
||||
it('renders a simple AnsiOutput object correctly', () => {
|
||||
it('renders a simple AnsiOutput object correctly', async () => {
|
||||
const data: AnsiOutput = [
|
||||
[
|
||||
createAnsiToken({ text: 'Hello, ' }),
|
||||
createAnsiToken({ text: 'world!' }),
|
||||
],
|
||||
];
|
||||
const { lastFrame } = render(<AnsiOutputText data={data} width={80} />);
|
||||
expect(lastFrame()).toBe('Hello, world!');
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<AnsiOutputText data={data} width={80} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toBe('Hello, world!\n');
|
||||
unmount();
|
||||
});
|
||||
|
||||
// Note: ink-testing-library doesn't render styles, so we can only check the text.
|
||||
@@ -41,73 +45,89 @@ describe('<AnsiOutputText />', () => {
|
||||
{ style: { underline: true }, text: 'Underline' },
|
||||
{ style: { dim: true }, text: 'Dim' },
|
||||
{ style: { inverse: true }, text: 'Inverse' },
|
||||
])('correctly applies style $text', ({ style, text }) => {
|
||||
])('correctly applies style $text', async ({ style, text }) => {
|
||||
const data: AnsiOutput = [[createAnsiToken({ text, ...style })]];
|
||||
const { lastFrame } = render(<AnsiOutputText data={data} width={80} />);
|
||||
expect(lastFrame()).toBe(text);
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<AnsiOutputText data={data} width={80} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toBe(text + '\n');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ color: { fg: '#ff0000' }, text: 'Red FG' },
|
||||
{ color: { bg: '#0000ff' }, text: 'Blue BG' },
|
||||
{ color: { fg: '#00ff00', bg: '#ff00ff' }, text: 'Green FG Magenta BG' },
|
||||
])('correctly applies color $text', ({ color, text }) => {
|
||||
])('correctly applies color $text', async ({ color, text }) => {
|
||||
const data: AnsiOutput = [[createAnsiToken({ text, ...color })]];
|
||||
const { lastFrame } = render(<AnsiOutputText data={data} width={80} />);
|
||||
expect(lastFrame()).toBe(text);
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<AnsiOutputText data={data} width={80} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toBe(text + '\n');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('handles empty lines and empty tokens', () => {
|
||||
it('handles empty lines and empty tokens', async () => {
|
||||
const data: AnsiOutput = [
|
||||
[createAnsiToken({ text: 'First line' })],
|
||||
[],
|
||||
[createAnsiToken({ text: 'Third line' })],
|
||||
[createAnsiToken({ text: '' })],
|
||||
];
|
||||
const { lastFrame } = render(<AnsiOutputText data={data} width={80} />);
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<AnsiOutputText data={data} width={80} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toBeDefined();
|
||||
const lines = output!.split('\n');
|
||||
const lines = output.split('\n');
|
||||
expect(lines[0].trim()).toBe('First line');
|
||||
expect(lines[1].trim()).toBe('');
|
||||
expect(lines[2].trim()).toBe('Third line');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('respects the availableTerminalHeight prop and slices the lines correctly', () => {
|
||||
it('respects the availableTerminalHeight prop and slices the lines correctly', async () => {
|
||||
const data: AnsiOutput = [
|
||||
[createAnsiToken({ text: 'Line 1' })],
|
||||
[createAnsiToken({ text: 'Line 2' })],
|
||||
[createAnsiToken({ text: 'Line 3' })],
|
||||
[createAnsiToken({ text: 'Line 4' })],
|
||||
];
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<AnsiOutputText data={data} availableTerminalHeight={2} width={80} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).not.toContain('Line 1');
|
||||
expect(output).not.toContain('Line 2');
|
||||
expect(output).toContain('Line 3');
|
||||
expect(output).toContain('Line 4');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('respects the maxLines prop and slices the lines correctly', () => {
|
||||
it('respects the maxLines prop and slices the lines correctly', async () => {
|
||||
const data: AnsiOutput = [
|
||||
[createAnsiToken({ text: 'Line 1' })],
|
||||
[createAnsiToken({ text: 'Line 2' })],
|
||||
[createAnsiToken({ text: 'Line 3' })],
|
||||
[createAnsiToken({ text: 'Line 4' })],
|
||||
];
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<AnsiOutputText data={data} maxLines={2} width={80} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).not.toContain('Line 1');
|
||||
expect(output).not.toContain('Line 2');
|
||||
expect(output).toContain('Line 3');
|
||||
expect(output).toContain('Line 4');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('prioritizes maxLines over availableTerminalHeight if maxLines is smaller', () => {
|
||||
it('prioritizes maxLines over availableTerminalHeight if maxLines is smaller', async () => {
|
||||
const data: AnsiOutput = [
|
||||
[createAnsiToken({ text: 'Line 1' })],
|
||||
[createAnsiToken({ text: 'Line 2' })],
|
||||
@@ -115,7 +135,7 @@ describe('<AnsiOutputText />', () => {
|
||||
[createAnsiToken({ text: 'Line 4' })],
|
||||
];
|
||||
// availableTerminalHeight=3, maxLines=2 => show 2 lines
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<AnsiOutputText
|
||||
data={data}
|
||||
availableTerminalHeight={3}
|
||||
@@ -123,21 +143,25 @@ describe('<AnsiOutputText />', () => {
|
||||
width={80}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).not.toContain('Line 2');
|
||||
expect(output).toContain('Line 3');
|
||||
expect(output).toContain('Line 4');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders a large AnsiOutput object without crashing', () => {
|
||||
it('renders a large AnsiOutput object without crashing', async () => {
|
||||
const largeData: AnsiOutput = [];
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
largeData.push([createAnsiToken({ text: `Line ${i}` })]);
|
||||
}
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<AnsiOutputText data={largeData} width={80} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
// We are just checking that it renders something without crashing.
|
||||
expect(lastFrame()).toBeDefined();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ vi.mock('../utils/terminalSetup.js', () => ({
|
||||
}));
|
||||
|
||||
describe('<AppHeader />', () => {
|
||||
it('should render the banner with default text', () => {
|
||||
it('should render the banner with default text', async () => {
|
||||
const mockConfig = makeFakeConfig();
|
||||
const uiState = {
|
||||
history: [],
|
||||
@@ -29,20 +29,21 @@ describe('<AppHeader />', () => {
|
||||
bannerVisible: true,
|
||||
};
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<AppHeader version="1.0.0" />,
|
||||
{
|
||||
config: mockConfig,
|
||||
uiState,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toContain('This is the default banner');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render the banner with warning text', () => {
|
||||
it('should render the banner with warning text', async () => {
|
||||
const mockConfig = makeFakeConfig();
|
||||
const uiState = {
|
||||
history: [],
|
||||
@@ -53,20 +54,21 @@ describe('<AppHeader />', () => {
|
||||
bannerVisible: true,
|
||||
};
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<AppHeader version="1.0.0" />,
|
||||
{
|
||||
config: mockConfig,
|
||||
uiState,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toContain('There are capacity issues');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should not render the banner when no flags are set', () => {
|
||||
it('should not render the banner when no flags are set', async () => {
|
||||
const mockConfig = makeFakeConfig();
|
||||
const uiState = {
|
||||
history: [],
|
||||
@@ -76,20 +78,21 @@ describe('<AppHeader />', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<AppHeader version="1.0.0" />,
|
||||
{
|
||||
config: mockConfig,
|
||||
uiState,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).not.toContain('Banner');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should not render the default banner if shown count is 5 or more', () => {
|
||||
it('should not render the default banner if shown count is 5 or more', async () => {
|
||||
const mockConfig = makeFakeConfig();
|
||||
const uiState = {
|
||||
history: [],
|
||||
@@ -108,20 +111,21 @@ describe('<AppHeader />', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<AppHeader version="1.0.0" />,
|
||||
{
|
||||
config: mockConfig,
|
||||
uiState,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).not.toContain('This is the default banner');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should increment the version count when default banner is displayed', () => {
|
||||
it('should increment the version count when default banner is displayed', async () => {
|
||||
const mockConfig = makeFakeConfig();
|
||||
const uiState = {
|
||||
history: [],
|
||||
@@ -135,10 +139,14 @@ describe('<AppHeader />', () => {
|
||||
// and interfering with the expected persistentState.set call.
|
||||
persistentStateMock.setData({ tipsShown: 10 });
|
||||
|
||||
const { unmount } = renderWithProviders(<AppHeader version="1.0.0" />, {
|
||||
config: mockConfig,
|
||||
uiState,
|
||||
});
|
||||
const { waitUntilReady, unmount } = renderWithProviders(
|
||||
<AppHeader version="1.0.0" />,
|
||||
{
|
||||
config: mockConfig,
|
||||
uiState,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(persistentStateMock.set).toHaveBeenCalledWith(
|
||||
'defaultBannerShownCount',
|
||||
@@ -152,7 +160,7 @@ describe('<AppHeader />', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render banner text with unescaped newlines', () => {
|
||||
it('should render banner text with unescaped newlines', async () => {
|
||||
const mockConfig = makeFakeConfig();
|
||||
const uiState = {
|
||||
history: [],
|
||||
@@ -163,19 +171,20 @@ describe('<AppHeader />', () => {
|
||||
bannerVisible: true,
|
||||
};
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<AppHeader version="1.0.0" />,
|
||||
{
|
||||
config: mockConfig,
|
||||
uiState,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).not.toContain('First line\\nSecond line');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render Tips when tipsShown is less than 10', () => {
|
||||
it('should render Tips when tipsShown is less than 10', async () => {
|
||||
const mockConfig = makeFakeConfig();
|
||||
const uiState = {
|
||||
history: [],
|
||||
@@ -188,36 +197,38 @@ describe('<AppHeader />', () => {
|
||||
|
||||
persistentStateMock.setData({ tipsShown: 5 });
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<AppHeader version="1.0.0" />,
|
||||
{
|
||||
config: mockConfig,
|
||||
uiState,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toContain('Tips');
|
||||
expect(persistentStateMock.set).toHaveBeenCalledWith('tipsShown', 6);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should NOT render Tips when tipsShown is 10 or more', () => {
|
||||
it('should NOT render Tips when tipsShown is 10 or more', async () => {
|
||||
const mockConfig = makeFakeConfig();
|
||||
|
||||
persistentStateMock.setData({ tipsShown: 10 });
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<AppHeader version="1.0.0" />,
|
||||
{
|
||||
config: mockConfig,
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).not.toContain('Tips');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should show tips until they have been shown 10 times (persistence flow)', () => {
|
||||
it('should show tips until they have been shown 10 times (persistence flow)', async () => {
|
||||
persistentStateMock.setData({ tipsShown: 9 });
|
||||
|
||||
const mockConfig = makeFakeConfig();
|
||||
@@ -235,6 +246,7 @@ describe('<AppHeader />', () => {
|
||||
config: mockConfig,
|
||||
uiState,
|
||||
});
|
||||
await session1.waitUntilReady();
|
||||
|
||||
expect(session1.lastFrame()).toContain('Tips');
|
||||
expect(persistentStateMock.get('tipsShown')).toBe(10);
|
||||
@@ -244,6 +256,7 @@ describe('<AppHeader />', () => {
|
||||
const session2 = renderWithProviders(<AppHeader version="1.0.0" />, {
|
||||
config: mockConfig,
|
||||
});
|
||||
await session2.waitUntilReady();
|
||||
|
||||
expect(session2.lastFrame()).not.toContain('Tips');
|
||||
session2.unmount();
|
||||
|
||||
@@ -10,51 +10,57 @@ import { describe, it, expect } from 'vitest';
|
||||
import { ApprovalMode } from '@google/gemini-cli-core';
|
||||
|
||||
describe('ApprovalModeIndicator', () => {
|
||||
it('renders correctly for AUTO_EDIT mode', () => {
|
||||
const { lastFrame } = render(
|
||||
it('renders correctly for AUTO_EDIT mode', async () => {
|
||||
const { lastFrame, waitUntilReady } = render(
|
||||
<ApprovalModeIndicator approvalMode={ApprovalMode.AUTO_EDIT} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly for AUTO_EDIT mode with plan enabled', () => {
|
||||
const { lastFrame } = render(
|
||||
it('renders correctly for AUTO_EDIT mode with plan enabled', async () => {
|
||||
const { lastFrame, waitUntilReady } = render(
|
||||
<ApprovalModeIndicator
|
||||
approvalMode={ApprovalMode.AUTO_EDIT}
|
||||
allowPlanMode={true}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly for PLAN mode', () => {
|
||||
const { lastFrame } = render(
|
||||
it('renders correctly for PLAN mode', async () => {
|
||||
const { lastFrame, waitUntilReady } = render(
|
||||
<ApprovalModeIndicator approvalMode={ApprovalMode.PLAN} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly for YOLO mode', () => {
|
||||
const { lastFrame } = render(
|
||||
it('renders correctly for YOLO mode', async () => {
|
||||
const { lastFrame, waitUntilReady } = render(
|
||||
<ApprovalModeIndicator approvalMode={ApprovalMode.YOLO} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly for DEFAULT mode', () => {
|
||||
const { lastFrame } = render(
|
||||
it('renders correctly for DEFAULT mode', async () => {
|
||||
const { lastFrame, waitUntilReady } = render(
|
||||
<ApprovalModeIndicator approvalMode={ApprovalMode.DEFAULT} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders correctly for DEFAULT mode with plan enabled', () => {
|
||||
const { lastFrame } = render(
|
||||
it('renders correctly for DEFAULT mode with plan enabled', async () => {
|
||||
const { lastFrame, waitUntilReady } = render(
|
||||
<ApprovalModeIndicator
|
||||
approvalMode={ApprovalMode.DEFAULT}
|
||||
allowPlanMode={true}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -46,8 +46,8 @@ describe('AskUserDialog', () => {
|
||||
},
|
||||
];
|
||||
|
||||
it('renders question and options', () => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
it('renders question and options', async () => {
|
||||
const { lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={authQuestion}
|
||||
onSubmit={vi.fn()}
|
||||
@@ -57,6 +57,7 @@ describe('AskUserDialog', () => {
|
||||
{ width: 120 },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -125,7 +126,7 @@ describe('AskUserDialog', () => {
|
||||
|
||||
actions(stdin);
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
expect(onSubmit).toHaveBeenCalledWith(expectedSubmit);
|
||||
});
|
||||
});
|
||||
@@ -133,7 +134,7 @@ describe('AskUserDialog', () => {
|
||||
|
||||
it('handles custom option in single select with inline typing', async () => {
|
||||
const onSubmit = vi.fn();
|
||||
const { stdin, lastFrame } = renderWithProviders(
|
||||
const { stdin, lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={authQuestion}
|
||||
onSubmit={onSubmit}
|
||||
@@ -147,7 +148,8 @@ describe('AskUserDialog', () => {
|
||||
writeKey(stdin, '\x1b[B');
|
||||
writeKey(stdin, '\x1b[B');
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Enter a custom value');
|
||||
});
|
||||
|
||||
@@ -156,14 +158,15 @@ describe('AskUserDialog', () => {
|
||||
writeKey(stdin, char);
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('API Key');
|
||||
});
|
||||
|
||||
// Press Enter to submit the custom value
|
||||
writeKey(stdin, '\r');
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
expect(onSubmit).toHaveBeenCalledWith({ '0': 'API Key' });
|
||||
});
|
||||
});
|
||||
@@ -180,7 +183,7 @@ describe('AskUserDialog', () => {
|
||||
];
|
||||
|
||||
const onSubmit = vi.fn();
|
||||
const { stdin, lastFrame } = renderWithProviders(
|
||||
const { stdin, lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={authQuestionWithOther}
|
||||
onSubmit={onSubmit}
|
||||
@@ -207,15 +210,17 @@ describe('AskUserDialog', () => {
|
||||
writeKey(stdin, char);
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Line 1');
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Line 2');
|
||||
});
|
||||
|
||||
// Press Enter to submit
|
||||
writeKey(stdin, '\r');
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
expect(onSubmit).toHaveBeenCalledWith({ '0': 'Line 1\nLine 2' });
|
||||
});
|
||||
});
|
||||
@@ -240,7 +245,7 @@ describe('AskUserDialog', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={questions}
|
||||
onSubmit={vi.fn()}
|
||||
@@ -251,14 +256,19 @@ describe('AskUserDialog', () => {
|
||||
{ useAlternateBuffer },
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
if (expectedArrows) {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('▲');
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('▼');
|
||||
} else {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).not.toContain('▲');
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).not.toContain('▼');
|
||||
}
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -266,7 +276,7 @@ describe('AskUserDialog', () => {
|
||||
);
|
||||
|
||||
it('navigates to custom option when typing unbound characters (Type-to-Jump)', async () => {
|
||||
const { stdin, lastFrame } = renderWithProviders(
|
||||
const { stdin, lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={authQuestion}
|
||||
onSubmit={vi.fn()}
|
||||
@@ -279,10 +289,12 @@ describe('AskUserDialog', () => {
|
||||
// Type a character without navigating down
|
||||
writeKey(stdin, 'A');
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
// Should show the custom input with 'A'
|
||||
// Placeholder is hidden when text is present
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('A');
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('3. A');
|
||||
});
|
||||
|
||||
@@ -290,12 +302,13 @@ describe('AskUserDialog', () => {
|
||||
writeKey(stdin, 'P');
|
||||
writeKey(stdin, 'I');
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('API');
|
||||
});
|
||||
});
|
||||
|
||||
it('shows progress header for multiple questions', () => {
|
||||
it('shows progress header for multiple questions', async () => {
|
||||
const multiQuestions: Question[] = [
|
||||
{
|
||||
question: 'Which database should we use?',
|
||||
@@ -319,7 +332,7 @@ describe('AskUserDialog', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={multiQuestions}
|
||||
onSubmit={vi.fn()}
|
||||
@@ -329,11 +342,12 @@ describe('AskUserDialog', () => {
|
||||
{ width: 120 },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('hides progress header for single question', () => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
it('hides progress header for single question', async () => {
|
||||
const { lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={authQuestion}
|
||||
onSubmit={vi.fn()}
|
||||
@@ -343,11 +357,12 @@ describe('AskUserDialog', () => {
|
||||
{ width: 120 },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('shows keyboard hints', () => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
it('shows keyboard hints', async () => {
|
||||
const { lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={authQuestion}
|
||||
onSubmit={vi.fn()}
|
||||
@@ -357,6 +372,7 @@ describe('AskUserDialog', () => {
|
||||
{ width: 120 },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -380,7 +396,7 @@ describe('AskUserDialog', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const { stdin, lastFrame } = renderWithProviders(
|
||||
const { stdin, lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={multiQuestions}
|
||||
onSubmit={vi.fn()}
|
||||
@@ -390,17 +406,20 @@ describe('AskUserDialog', () => {
|
||||
{ width: 120 },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Which testing framework?');
|
||||
|
||||
writeKey(stdin, '\x1b[C'); // Right arrow
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Which CI provider?');
|
||||
});
|
||||
|
||||
writeKey(stdin, '\x1b[D'); // Left arrow
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Which testing framework?');
|
||||
});
|
||||
});
|
||||
@@ -424,7 +443,7 @@ describe('AskUserDialog', () => {
|
||||
];
|
||||
|
||||
const onSubmit = vi.fn();
|
||||
const { stdin, lastFrame } = renderWithProviders(
|
||||
const { stdin, lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={multiQuestions}
|
||||
onSubmit={onSubmit}
|
||||
@@ -437,40 +456,44 @@ describe('AskUserDialog', () => {
|
||||
// Answer first question (should auto-advance)
|
||||
writeKey(stdin, '\r');
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Which bundler?');
|
||||
});
|
||||
|
||||
// Navigate back
|
||||
writeKey(stdin, '\x1b[D');
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Which package manager?');
|
||||
});
|
||||
|
||||
// Navigate forward
|
||||
writeKey(stdin, '\x1b[C');
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Which bundler?');
|
||||
});
|
||||
|
||||
// Answer second question
|
||||
writeKey(stdin, '\r');
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Review your answers:');
|
||||
});
|
||||
|
||||
// Submit from Review
|
||||
writeKey(stdin, '\r');
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
expect(onSubmit).toHaveBeenCalledWith({ '0': 'pnpm', '1': 'Vite' });
|
||||
});
|
||||
});
|
||||
|
||||
it('shows Review tab in progress header for multiple questions', () => {
|
||||
it('shows Review tab in progress header for multiple questions', async () => {
|
||||
const multiQuestions: Question[] = [
|
||||
{
|
||||
question: 'Which framework?',
|
||||
@@ -494,7 +517,7 @@ describe('AskUserDialog', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={multiQuestions}
|
||||
onSubmit={vi.fn()}
|
||||
@@ -504,6 +527,7 @@ describe('AskUserDialog', () => {
|
||||
{ width: 120 },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -525,7 +549,7 @@ describe('AskUserDialog', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const { stdin, lastFrame } = renderWithProviders(
|
||||
const { stdin, lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={multiQuestions}
|
||||
onSubmit={vi.fn()}
|
||||
@@ -537,19 +561,22 @@ describe('AskUserDialog', () => {
|
||||
|
||||
writeKey(stdin, '\x1b[C'); // Right arrow
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Add documentation?');
|
||||
});
|
||||
|
||||
writeKey(stdin, '\x1b[C'); // Right arrow to Review
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
writeKey(stdin, '\x1b[D'); // Left arrow back
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Add documentation?');
|
||||
});
|
||||
});
|
||||
@@ -572,7 +599,7 @@ describe('AskUserDialog', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const { stdin, lastFrame } = renderWithProviders(
|
||||
const { stdin, lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={multiQuestions}
|
||||
onSubmit={vi.fn()}
|
||||
@@ -586,7 +613,8 @@ describe('AskUserDialog', () => {
|
||||
writeKey(stdin, '\x1b[C');
|
||||
writeKey(stdin, '\x1b[C');
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -627,13 +655,13 @@ describe('AskUserDialog', () => {
|
||||
// Submit
|
||||
writeKey(stdin, '\r');
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
expect(onSubmit).toHaveBeenCalledWith({ '0': 'Node 20' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Text type questions', () => {
|
||||
it('renders text input for type: "text"', () => {
|
||||
it('renders text input for type: "text"', async () => {
|
||||
const textQuestion: Question[] = [
|
||||
{
|
||||
question: 'What should we name this component?',
|
||||
@@ -643,7 +671,7 @@ describe('AskUserDialog', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={textQuestion}
|
||||
onSubmit={vi.fn()}
|
||||
@@ -653,10 +681,11 @@ describe('AskUserDialog', () => {
|
||||
{ width: 120 },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('shows default placeholder when none provided', () => {
|
||||
it('shows default placeholder when none provided', async () => {
|
||||
const textQuestion: Question[] = [
|
||||
{
|
||||
question: 'Enter the database connection string:',
|
||||
@@ -665,7 +694,7 @@ describe('AskUserDialog', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={textQuestion}
|
||||
onSubmit={vi.fn()}
|
||||
@@ -675,6 +704,7 @@ describe('AskUserDialog', () => {
|
||||
{ width: 120 },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -687,7 +717,7 @@ describe('AskUserDialog', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const { stdin, lastFrame } = renderWithProviders(
|
||||
const { stdin, lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={textQuestion}
|
||||
onSubmit={vi.fn()}
|
||||
@@ -701,19 +731,22 @@ describe('AskUserDialog', () => {
|
||||
writeKey(stdin, char);
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('abc');
|
||||
});
|
||||
|
||||
writeKey(stdin, '\x7f'); // Backspace
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('ab');
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).not.toContain('abc');
|
||||
});
|
||||
});
|
||||
|
||||
it('shows correct keyboard hints for text type', () => {
|
||||
it('shows correct keyboard hints for text type', async () => {
|
||||
const textQuestion: Question[] = [
|
||||
{
|
||||
question: 'Enter the variable name:',
|
||||
@@ -722,7 +755,7 @@ describe('AskUserDialog', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={textQuestion}
|
||||
onSubmit={vi.fn()}
|
||||
@@ -732,6 +765,7 @@ describe('AskUserDialog', () => {
|
||||
{ width: 120 },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -754,7 +788,7 @@ describe('AskUserDialog', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const { stdin, lastFrame } = renderWithProviders(
|
||||
const { stdin, lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={mixedQuestions}
|
||||
onSubmit={vi.fn()}
|
||||
@@ -770,13 +804,15 @@ describe('AskUserDialog', () => {
|
||||
|
||||
writeKey(stdin, '\t'); // Use Tab instead of Right arrow when text input is active
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Should it be async?');
|
||||
});
|
||||
|
||||
writeKey(stdin, '\x1b[D'); // Left arrow should work when NOT focusing a text input
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('useAuth');
|
||||
});
|
||||
});
|
||||
@@ -802,7 +838,7 @@ describe('AskUserDialog', () => {
|
||||
];
|
||||
|
||||
const onSubmit = vi.fn();
|
||||
const { stdin, lastFrame } = renderWithProviders(
|
||||
const { stdin, lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={mixedQuestions}
|
||||
onSubmit={onSubmit}
|
||||
@@ -818,23 +854,29 @@ describe('AskUserDialog', () => {
|
||||
|
||||
writeKey(stdin, '\r');
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Which styling approach?');
|
||||
});
|
||||
|
||||
writeKey(stdin, '\r');
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Review your answers:');
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Name');
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('DataTable');
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Style');
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('CSS Modules');
|
||||
});
|
||||
|
||||
writeKey(stdin, '\r');
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
'0': 'DataTable',
|
||||
'1': 'CSS Modules',
|
||||
@@ -864,7 +906,7 @@ describe('AskUserDialog', () => {
|
||||
|
||||
writeKey(stdin, '\r');
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
expect(onSubmit).toHaveBeenCalledWith({});
|
||||
});
|
||||
});
|
||||
@@ -879,7 +921,7 @@ describe('AskUserDialog', () => {
|
||||
];
|
||||
|
||||
const onCancel = vi.fn();
|
||||
const { stdin, lastFrame } = renderWithProviders(
|
||||
const { stdin, lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={textQuestion}
|
||||
onSubmit={vi.fn()}
|
||||
@@ -893,16 +935,19 @@ describe('AskUserDialog', () => {
|
||||
writeKey(stdin, char);
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('SomeText');
|
||||
});
|
||||
|
||||
// Send Ctrl+C
|
||||
writeKey(stdin, '\x03'); // Ctrl+C
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
// Text should be cleared
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).not.toContain('SomeText');
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('>');
|
||||
});
|
||||
|
||||
@@ -926,7 +971,7 @@ describe('AskUserDialog', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const { stdin, lastFrame } = renderWithProviders(
|
||||
const { stdin, lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={multiQuestions}
|
||||
onSubmit={vi.fn()}
|
||||
@@ -938,13 +983,15 @@ describe('AskUserDialog', () => {
|
||||
|
||||
// 1. Move to Text Q (Right arrow works for Choice Q)
|
||||
writeKey(stdin, '\x1b[C');
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Text Q?');
|
||||
});
|
||||
|
||||
// 2. Type something in Text Q to make isEditingCustomOption true
|
||||
writeKey(stdin, 'a');
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('a');
|
||||
});
|
||||
|
||||
@@ -952,18 +999,21 @@ describe('AskUserDialog', () => {
|
||||
// When typing 'a', cursor is at index 1.
|
||||
// We need to move cursor to index 0 first for Left arrow to work for navigation.
|
||||
writeKey(stdin, '\x1b[D'); // Left arrow moves cursor to index 0
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Text Q?');
|
||||
});
|
||||
|
||||
writeKey(stdin, '\x1b[D'); // Second Left arrow should now trigger navigation
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Choice Q?');
|
||||
});
|
||||
|
||||
// 4. Immediately try Right arrow to go back to Text Q
|
||||
writeKey(stdin, '\x1b[C');
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Text Q?');
|
||||
});
|
||||
});
|
||||
@@ -987,7 +1037,7 @@ describe('AskUserDialog', () => {
|
||||
];
|
||||
|
||||
const onSubmit = vi.fn();
|
||||
const { stdin, lastFrame } = renderWithProviders(
|
||||
const { stdin, lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={multiQuestions}
|
||||
onSubmit={onSubmit}
|
||||
@@ -1001,14 +1051,16 @@ describe('AskUserDialog', () => {
|
||||
act(() => {
|
||||
stdin.write('\r'); // Select A1 for Q1 -> triggers autoAdvance
|
||||
});
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Question 2?');
|
||||
});
|
||||
|
||||
act(() => {
|
||||
stdin.write('\r'); // Select A2 for Q2 -> triggers autoAdvance to Review
|
||||
});
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Review your answers:');
|
||||
});
|
||||
|
||||
@@ -1016,7 +1068,7 @@ describe('AskUserDialog', () => {
|
||||
stdin.write('\r'); // Submit from Review
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
'0': 'A1',
|
||||
'1': 'A2',
|
||||
@@ -1037,7 +1089,7 @@ describe('AskUserDialog', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={questions}
|
||||
onSubmit={vi.fn()}
|
||||
@@ -1048,7 +1100,8 @@ describe('AskUserDialog', () => {
|
||||
{ width: 120 },
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
const frame = lastFrame();
|
||||
// Plain text should be rendered as bold
|
||||
expect(frame).toContain(chalk.bold('Which option do you prefer?'));
|
||||
@@ -1066,7 +1119,7 @@ describe('AskUserDialog', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={questions}
|
||||
onSubmit={vi.fn()}
|
||||
@@ -1077,7 +1130,8 @@ describe('AskUserDialog', () => {
|
||||
{ width: 120 },
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
const frame = lastFrame();
|
||||
// Should NOT have double-bold (the whole question bolded AND "this" bolded)
|
||||
// "Is " should not be bold, only "this" should be bold
|
||||
@@ -1098,7 +1152,7 @@ describe('AskUserDialog', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={questions}
|
||||
onSubmit={vi.fn()}
|
||||
@@ -1109,7 +1163,8 @@ describe('AskUserDialog', () => {
|
||||
{ width: 120 },
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
const frame = lastFrame();
|
||||
// Check for chalk.bold('this') - asterisks should be gone, text should be bold
|
||||
expect(frame).toContain(chalk.bold('this'));
|
||||
@@ -1128,7 +1183,7 @@ describe('AskUserDialog', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={questions}
|
||||
onSubmit={vi.fn()}
|
||||
@@ -1139,7 +1194,8 @@ describe('AskUserDialog', () => {
|
||||
{ width: 120 },
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
const frame = lastFrame();
|
||||
// Backticks should be removed
|
||||
expect(frame).toContain('npm start');
|
||||
@@ -1148,7 +1204,7 @@ describe('AskUserDialog', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('uses availableTerminalHeight from UIStateContext if availableHeight prop is missing', () => {
|
||||
it('uses availableTerminalHeight from UIStateContext if availableHeight prop is missing', async () => {
|
||||
const questions: Question[] = [
|
||||
{
|
||||
question: 'Choose an option',
|
||||
@@ -1166,7 +1222,7 @@ describe('AskUserDialog', () => {
|
||||
availableTerminalHeight: 5, // Small height to force scroll arrows
|
||||
} as UIState;
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<UIStateContext.Provider value={mockUIState}>
|
||||
<AskUserDialog
|
||||
questions={questions}
|
||||
@@ -1179,11 +1235,13 @@ describe('AskUserDialog', () => {
|
||||
);
|
||||
|
||||
// With height 5 and alternate buffer disabled, it should show scroll arrows (▲)
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('▲');
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('▼');
|
||||
});
|
||||
|
||||
it('does NOT truncate the question when in alternate buffer mode even with small height', () => {
|
||||
it('does NOT truncate the question when in alternate buffer mode even with small height', async () => {
|
||||
const longQuestion =
|
||||
'This is a very long question ' + 'with many words '.repeat(10);
|
||||
const questions: Question[] = [
|
||||
@@ -1200,7 +1258,7 @@ describe('AskUserDialog', () => {
|
||||
availableTerminalHeight: 5,
|
||||
} as UIState;
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
const { lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<UIStateContext.Provider value={mockUIState}>
|
||||
<AskUserDialog
|
||||
questions={questions}
|
||||
@@ -1213,8 +1271,10 @@ describe('AskUserDialog', () => {
|
||||
);
|
||||
|
||||
// Should NOT contain the truncation message
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).not.toContain('hidden ...');
|
||||
// Should contain the full long question (or at least its parts)
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('This is a very long question');
|
||||
});
|
||||
|
||||
@@ -1234,7 +1294,7 @@ describe('AskUserDialog', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const { stdin, lastFrame } = renderWithProviders(
|
||||
const { stdin, lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={questions}
|
||||
onSubmit={vi.fn()}
|
||||
@@ -1248,7 +1308,8 @@ describe('AskUserDialog', () => {
|
||||
writeKey(stdin, '\x1b[B'); // Down
|
||||
writeKey(stdin, '\x1b[B'); // Down to Other
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1267,7 +1328,7 @@ describe('AskUserDialog', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const { stdin, lastFrame } = renderWithProviders(
|
||||
const { stdin, lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={questions}
|
||||
onSubmit={vi.fn()}
|
||||
@@ -1281,7 +1342,8 @@ describe('AskUserDialog', () => {
|
||||
writeKey(stdin, '\x1b[B'); // Down
|
||||
writeKey(stdin, '\x1b[B'); // Down to Other
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,8 +14,6 @@ import { type Key, type KeypressHandler } from '../contexts/KeypressContext.js';
|
||||
import { ScrollProvider } from '../contexts/ScrollProvider.js';
|
||||
import { Box } from 'ink';
|
||||
|
||||
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
// Mock dependencies
|
||||
const mockDismissBackgroundShell = vi.fn();
|
||||
const mockSetActiveBackgroundShellPid = vi.fn();
|
||||
@@ -142,81 +140,84 @@ describe('<BackgroundShellDisplay />', () => {
|
||||
});
|
||||
|
||||
it('renders the output of the active shell', async () => {
|
||||
const { lastFrame } = render(
|
||||
const width = 80;
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<ScrollProvider>
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
activePid={shell1.pid}
|
||||
width={80}
|
||||
width={width}
|
||||
height={24}
|
||||
isFocused={false}
|
||||
isListOpenProp={false}
|
||||
/>
|
||||
</ScrollProvider>,
|
||||
width,
|
||||
);
|
||||
await act(async () => {
|
||||
await delay(0);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders tabs for multiple shells', async () => {
|
||||
const { lastFrame } = render(
|
||||
const width = 100;
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<ScrollProvider>
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
activePid={shell1.pid}
|
||||
width={100}
|
||||
width={width}
|
||||
height={24}
|
||||
isFocused={false}
|
||||
isListOpenProp={false}
|
||||
/>
|
||||
</ScrollProvider>,
|
||||
width,
|
||||
);
|
||||
await act(async () => {
|
||||
await delay(0);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('highlights the focused state', async () => {
|
||||
const { lastFrame } = render(
|
||||
const width = 80;
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<ScrollProvider>
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
activePid={shell1.pid}
|
||||
width={80}
|
||||
width={width}
|
||||
height={24}
|
||||
isFocused={true} // Focused
|
||||
isFocused={true}
|
||||
isListOpenProp={false}
|
||||
/>
|
||||
</ScrollProvider>,
|
||||
width,
|
||||
);
|
||||
await act(async () => {
|
||||
await delay(0);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('resizes the PTY on mount and when dimensions change', async () => {
|
||||
const { rerender } = render(
|
||||
const width = 80;
|
||||
const { rerender, waitUntilReady, unmount } = render(
|
||||
<ScrollProvider>
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
activePid={shell1.pid}
|
||||
width={80}
|
||||
width={width}
|
||||
height={24}
|
||||
isFocused={false}
|
||||
isListOpenProp={false}
|
||||
/>
|
||||
</ScrollProvider>,
|
||||
width,
|
||||
);
|
||||
await act(async () => {
|
||||
await delay(0);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
expect(ShellExecutionService.resizePty).toHaveBeenCalledWith(
|
||||
shell1.pid,
|
||||
@@ -236,143 +237,152 @@ describe('<BackgroundShellDisplay />', () => {
|
||||
/>
|
||||
</ScrollProvider>,
|
||||
);
|
||||
await act(async () => {
|
||||
await delay(0);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
expect(ShellExecutionService.resizePty).toHaveBeenCalledWith(
|
||||
shell1.pid,
|
||||
96,
|
||||
27,
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders the process list when isListOpenProp is true', async () => {
|
||||
const { lastFrame } = render(
|
||||
const width = 80;
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<ScrollProvider>
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
activePid={shell1.pid}
|
||||
width={80}
|
||||
width={width}
|
||||
height={24}
|
||||
isFocused={true}
|
||||
isListOpenProp={true}
|
||||
/>
|
||||
</ScrollProvider>,
|
||||
width,
|
||||
);
|
||||
await act(async () => {
|
||||
await delay(0);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('selects the current process and closes the list when Ctrl+L is pressed in list view', async () => {
|
||||
render(
|
||||
const width = 80;
|
||||
const { waitUntilReady, unmount } = render(
|
||||
<ScrollProvider>
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
activePid={shell1.pid}
|
||||
width={80}
|
||||
width={width}
|
||||
height={24}
|
||||
isFocused={true}
|
||||
isListOpenProp={true}
|
||||
/>
|
||||
</ScrollProvider>,
|
||||
width,
|
||||
);
|
||||
await act(async () => {
|
||||
await delay(0);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
// Simulate down arrow to select the second process (handled by RadioButtonSelect)
|
||||
act(() => {
|
||||
await act(async () => {
|
||||
simulateKey({ name: 'down' });
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
// Simulate Ctrl+L (handled by BackgroundShellDisplay)
|
||||
act(() => {
|
||||
await act(async () => {
|
||||
simulateKey({ name: 'l', ctrl: true });
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
expect(mockSetActiveBackgroundShellPid).toHaveBeenCalledWith(shell2.pid);
|
||||
expect(mockSetIsBackgroundShellListOpen).toHaveBeenCalledWith(false);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('kills the highlighted process when Ctrl+K is pressed in list view', async () => {
|
||||
render(
|
||||
const width = 80;
|
||||
const { waitUntilReady, unmount } = render(
|
||||
<ScrollProvider>
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
activePid={shell1.pid}
|
||||
width={80}
|
||||
width={width}
|
||||
height={24}
|
||||
isFocused={true}
|
||||
isListOpenProp={true}
|
||||
/>
|
||||
</ScrollProvider>,
|
||||
width,
|
||||
);
|
||||
await act(async () => {
|
||||
await delay(0);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
// Initial state: shell1 (active) is highlighted
|
||||
|
||||
// Move to shell2
|
||||
act(() => {
|
||||
await act(async () => {
|
||||
simulateKey({ name: 'down' });
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
// Press Ctrl+K
|
||||
act(() => {
|
||||
await act(async () => {
|
||||
simulateKey({ name: 'k', ctrl: true });
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
expect(mockDismissBackgroundShell).toHaveBeenCalledWith(shell2.pid);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('kills the active process when Ctrl+K is pressed in output view', async () => {
|
||||
render(
|
||||
const width = 80;
|
||||
const { waitUntilReady, unmount } = render(
|
||||
<ScrollProvider>
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
activePid={shell1.pid}
|
||||
width={80}
|
||||
width={width}
|
||||
height={24}
|
||||
isFocused={true}
|
||||
isListOpenProp={false}
|
||||
/>
|
||||
</ScrollProvider>,
|
||||
width,
|
||||
);
|
||||
await act(async () => {
|
||||
await delay(0);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
act(() => {
|
||||
await act(async () => {
|
||||
simulateKey({ name: 'k', ctrl: true });
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
expect(mockDismissBackgroundShell).toHaveBeenCalledWith(shell1.pid);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('scrolls to active shell when list opens', async () => {
|
||||
// shell2 is active
|
||||
const { lastFrame } = render(
|
||||
const width = 80;
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<ScrollProvider>
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
activePid={shell2.pid}
|
||||
width={80}
|
||||
width={width}
|
||||
height={24}
|
||||
isFocused={true}
|
||||
isListOpenProp={true}
|
||||
/>
|
||||
</ScrollProvider>,
|
||||
width,
|
||||
);
|
||||
await act(async () => {
|
||||
await delay(0);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('keeps exit code status color even when selected', async () => {
|
||||
@@ -387,22 +397,23 @@ describe('<BackgroundShellDisplay />', () => {
|
||||
};
|
||||
mockShells.set(exitedShell.pid, exitedShell);
|
||||
|
||||
const { lastFrame } = render(
|
||||
const width = 80;
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<ScrollProvider>
|
||||
<BackgroundShellDisplay
|
||||
shells={mockShells}
|
||||
activePid={exitedShell.pid}
|
||||
width={80}
|
||||
width={width}
|
||||
height={24}
|
||||
isFocused={true}
|
||||
isListOpenProp={true}
|
||||
/>
|
||||
</ScrollProvider>,
|
||||
width,
|
||||
);
|
||||
await act(async () => {
|
||||
await delay(0);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,18 +12,22 @@ describe('Banner', () => {
|
||||
it.each([
|
||||
['warning mode', true, 'Warning Message'],
|
||||
['info mode', false, 'Info Message'],
|
||||
])('renders in %s', (_, isWarning, text) => {
|
||||
const { lastFrame } = render(
|
||||
])('renders in %s', async (_, isWarning, text) => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<Banner bannerText={text} isWarning={isWarning} width={80} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('handles newlines in text', () => {
|
||||
it('handles newlines in text', async () => {
|
||||
const text = 'Line 1\\nLine 2';
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<Banner bannerText={text} isWarning={false} width={80} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,26 +17,28 @@ describe('<Checklist />', () => {
|
||||
{ status: 'cancelled', label: 'Task 4' },
|
||||
];
|
||||
|
||||
it('renders nothing when list is empty', () => {
|
||||
const { lastFrame } = render(
|
||||
it('renders nothing when list is empty', async () => {
|
||||
const { lastFrame, waitUntilReady } = render(
|
||||
<Checklist title="Test List" items={[]} isExpanded={true} />,
|
||||
);
|
||||
expect(lastFrame()).toBe('');
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
});
|
||||
|
||||
it('renders nothing when collapsed and no active items', () => {
|
||||
it('renders nothing when collapsed and no active items', async () => {
|
||||
const inactiveItems: ChecklistItemData[] = [
|
||||
{ status: 'completed', label: 'Task 1' },
|
||||
{ status: 'cancelled', label: 'Task 2' },
|
||||
];
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, waitUntilReady } = render(
|
||||
<Checklist title="Test List" items={inactiveItems} isExpanded={false} />,
|
||||
);
|
||||
expect(lastFrame()).toBe('');
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
});
|
||||
|
||||
it('renders summary view correctly (collapsed)', () => {
|
||||
const { lastFrame } = render(
|
||||
it('renders summary view correctly (collapsed)', async () => {
|
||||
const { lastFrame, waitUntilReady } = render(
|
||||
<Checklist
|
||||
title="Test List"
|
||||
items={items}
|
||||
@@ -44,11 +46,12 @@ describe('<Checklist />', () => {
|
||||
toggleHint="toggle me"
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders expanded view correctly', () => {
|
||||
const { lastFrame } = render(
|
||||
it('renders expanded view correctly', async () => {
|
||||
const { lastFrame, waitUntilReady } = render(
|
||||
<Checklist
|
||||
title="Test List"
|
||||
items={items}
|
||||
@@ -56,17 +59,19 @@ describe('<Checklist />', () => {
|
||||
toggleHint="toggle me"
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders summary view without in-progress item if none exists', () => {
|
||||
it('renders summary view without in-progress item if none exists', async () => {
|
||||
const pendingItems: ChecklistItemData[] = [
|
||||
{ status: 'completed', label: 'Task 1' },
|
||||
{ status: 'pending', label: 'Task 2' },
|
||||
];
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, waitUntilReady } = render(
|
||||
<Checklist title="Test List" items={pendingItems} isExpanded={false} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,36 +15,39 @@ describe('<ChecklistItem />', () => {
|
||||
{ status: 'in_progress', label: 'Doing this' },
|
||||
{ status: 'completed', label: 'Done this' },
|
||||
{ status: 'cancelled', label: 'Skipped this' },
|
||||
] as ChecklistItemData[])('renders %s item correctly', (item) => {
|
||||
const { lastFrame } = render(<ChecklistItem item={item} />);
|
||||
] as ChecklistItemData[])('renders %s item correctly', async (item) => {
|
||||
const { lastFrame, waitUntilReady } = render(<ChecklistItem item={item} />);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('truncates long text when wrap="truncate"', () => {
|
||||
it('truncates long text when wrap="truncate"', async () => {
|
||||
const item: ChecklistItemData = {
|
||||
status: 'in_progress',
|
||||
label:
|
||||
'This is a very long text that should be truncated because the wrap prop is set to truncate',
|
||||
};
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, waitUntilReady } = render(
|
||||
<Box width={30}>
|
||||
<ChecklistItem item={item} wrap="truncate" />
|
||||
</Box>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('wraps long text by default', () => {
|
||||
it('wraps long text by default', async () => {
|
||||
const item: ChecklistItemData = {
|
||||
status: 'in_progress',
|
||||
label:
|
||||
'This is a very long text that should wrap because the default behavior is wrapping',
|
||||
};
|
||||
const { lastFrame } = render(
|
||||
const { lastFrame, waitUntilReady } = render(
|
||||
<Box width={30}>
|
||||
<ChecklistItem item={item} />
|
||||
</Box>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,17 +15,23 @@ describe('<CliSpinner />', () => {
|
||||
debugState.debugNumAnimatedComponents = 0;
|
||||
});
|
||||
|
||||
it('should increment debugNumAnimatedComponents on mount and decrement on unmount', () => {
|
||||
it('should increment debugNumAnimatedComponents on mount and decrement on unmount', async () => {
|
||||
expect(debugState.debugNumAnimatedComponents).toBe(0);
|
||||
const { unmount } = renderWithProviders(<CliSpinner />);
|
||||
const { waitUntilReady, unmount } = renderWithProviders(<CliSpinner />);
|
||||
await waitUntilReady();
|
||||
expect(debugState.debugNumAnimatedComponents).toBe(1);
|
||||
unmount();
|
||||
expect(debugState.debugNumAnimatedComponents).toBe(0);
|
||||
});
|
||||
|
||||
it('should not render when showSpinner is false', () => {
|
||||
it('should not render when showSpinner is false', async () => {
|
||||
const settings = createMockSettings({ ui: { showSpinner: false } });
|
||||
const { lastFrame } = renderWithProviders(<CliSpinner />, { settings });
|
||||
expect(lastFrame()).toBe('');
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<CliSpinner />,
|
||||
{ settings },
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -245,13 +245,13 @@ const createMockConfig = (overrides = {}): Config =>
|
||||
...overrides,
|
||||
}) as unknown as Config;
|
||||
|
||||
const renderComposer = (
|
||||
const renderComposer = async (
|
||||
uiState: UIState,
|
||||
settings = createMockSettings(),
|
||||
config = createMockConfig(),
|
||||
uiActions = createMockUIActions(),
|
||||
) =>
|
||||
render(
|
||||
) => {
|
||||
const result = render(
|
||||
<ConfigContext.Provider value={config as unknown as Config}>
|
||||
<SettingsContext.Provider value={settings as unknown as LoadedSettings}>
|
||||
<UIStateContext.Provider value={uiState}>
|
||||
@@ -262,6 +262,9 @@ const renderComposer = (
|
||||
</SettingsContext.Provider>
|
||||
</ConfigContext.Provider>,
|
||||
);
|
||||
await result.waitUntilReady();
|
||||
return result;
|
||||
};
|
||||
|
||||
describe('Composer', () => {
|
||||
beforeEach(() => {
|
||||
@@ -274,20 +277,20 @@ describe('Composer', () => {
|
||||
});
|
||||
|
||||
describe('Footer Display Settings', () => {
|
||||
it('renders Footer by default when hideFooter is false', () => {
|
||||
it('renders Footer by default when hideFooter is false', async () => {
|
||||
const uiState = createMockUIState();
|
||||
const settings = createMockSettings({ ui: { hideFooter: false } });
|
||||
|
||||
const { lastFrame } = renderComposer(uiState, settings);
|
||||
const { lastFrame } = await renderComposer(uiState, settings);
|
||||
|
||||
expect(lastFrame()).toContain('Footer');
|
||||
});
|
||||
|
||||
it('does NOT render Footer when hideFooter is true', () => {
|
||||
it('does NOT render Footer when hideFooter is true', async () => {
|
||||
const uiState = createMockUIState();
|
||||
const settings = createMockSettings({ ui: { hideFooter: true } });
|
||||
|
||||
const { lastFrame } = renderComposer(uiState, settings);
|
||||
const { lastFrame } = await renderComposer(uiState, settings);
|
||||
|
||||
// Check for content that only appears IN the Footer component itself
|
||||
expect(lastFrame()).not.toContain('[NORMAL]'); // Vim mode indicator
|
||||
@@ -331,7 +334,7 @@ describe('Composer', () => {
|
||||
setVimMode: vi.fn(),
|
||||
} as unknown as ReturnType<typeof useVimMode>);
|
||||
|
||||
const { lastFrame } = renderComposer(uiState, settings, config);
|
||||
const { lastFrame } = await renderComposer(uiState, settings, config);
|
||||
|
||||
expect(lastFrame()).toContain('Footer');
|
||||
// Footer should be rendered with all the state passed through
|
||||
@@ -339,7 +342,7 @@ describe('Composer', () => {
|
||||
});
|
||||
|
||||
describe('Loading Indicator', () => {
|
||||
it('renders LoadingIndicator with thought when streaming', () => {
|
||||
it('renders LoadingIndicator with thought when streaming', async () => {
|
||||
const uiState = createMockUIState({
|
||||
streamingState: StreamingState.Responding,
|
||||
thought: {
|
||||
@@ -350,13 +353,13 @@ describe('Composer', () => {
|
||||
elapsedTime: 1500,
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('LoadingIndicator: Processing');
|
||||
});
|
||||
|
||||
it('renders generic thinking text in loading indicator when full inline thinking is enabled', () => {
|
||||
it('renders generic thinking text in loading indicator when full inline thinking is enabled', async () => {
|
||||
const uiState = createMockUIState({
|
||||
streamingState: StreamingState.Responding,
|
||||
thought: {
|
||||
@@ -368,27 +371,27 @@ describe('Composer', () => {
|
||||
ui: { inlineThinkingMode: 'full' },
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState, settings);
|
||||
const { lastFrame } = await renderComposer(uiState, settings);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('LoadingIndicator: Thinking ...');
|
||||
});
|
||||
|
||||
it('hides shortcuts hint while loading', () => {
|
||||
it('hides shortcuts hint while loading', async () => {
|
||||
const uiState = createMockUIState({
|
||||
streamingState: StreamingState.Responding,
|
||||
elapsedTime: 1,
|
||||
cleanUiDetailsVisible: false,
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('LoadingIndicator');
|
||||
expect(output).not.toContain('ShortcutsHint');
|
||||
});
|
||||
|
||||
it('renders LoadingIndicator without thought when accessibility disables loading phrases', () => {
|
||||
it('renders LoadingIndicator without thought when accessibility disables loading phrases', async () => {
|
||||
const uiState = createMockUIState({
|
||||
streamingState: StreamingState.Responding,
|
||||
thought: { subject: 'Hidden', description: 'Should not show' },
|
||||
@@ -397,14 +400,14 @@ describe('Composer', () => {
|
||||
getAccessibility: vi.fn(() => ({ enableLoadingPhrases: false })),
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState, undefined, config);
|
||||
const { lastFrame } = await renderComposer(uiState, undefined, config);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('LoadingIndicator');
|
||||
expect(output).not.toContain('Should not show');
|
||||
});
|
||||
|
||||
it('does not render LoadingIndicator when waiting for confirmation', () => {
|
||||
it('does not render LoadingIndicator when waiting for confirmation', async () => {
|
||||
const uiState = createMockUIState({
|
||||
streamingState: StreamingState.WaitingForConfirmation,
|
||||
thought: {
|
||||
@@ -413,13 +416,13 @@ describe('Composer', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).not.toContain('LoadingIndicator');
|
||||
});
|
||||
|
||||
it('does not render LoadingIndicator when a tool confirmation is pending', () => {
|
||||
it('does not render LoadingIndicator when a tool confirmation is pending', async () => {
|
||||
const uiState = createMockUIState({
|
||||
streamingState: StreamingState.Responding,
|
||||
pendingHistoryItems: [
|
||||
@@ -439,27 +442,27 @@ describe('Composer', () => {
|
||||
],
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).not.toContain('LoadingIndicator');
|
||||
expect(output).not.toContain('esc to cancel');
|
||||
});
|
||||
|
||||
it('renders LoadingIndicator when embedded shell is focused but background shell is visible', () => {
|
||||
it('renders LoadingIndicator when embedded shell is focused but background shell is visible', async () => {
|
||||
const uiState = createMockUIState({
|
||||
streamingState: StreamingState.Responding,
|
||||
embeddedShellFocused: true,
|
||||
isBackgroundShellVisible: true,
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('LoadingIndicator');
|
||||
});
|
||||
|
||||
it('renders both LoadingIndicator and ApprovalModeIndicator when streaming in full UI mode', () => {
|
||||
it('renders both LoadingIndicator and ApprovalModeIndicator when streaming in full UI mode', async () => {
|
||||
const uiState = createMockUIState({
|
||||
streamingState: StreamingState.Responding,
|
||||
thought: {
|
||||
@@ -469,21 +472,21 @@ describe('Composer', () => {
|
||||
showApprovalModeIndicator: ApprovalMode.PLAN,
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('LoadingIndicator: Thinking');
|
||||
expect(output).toContain('ApprovalModeIndicator');
|
||||
});
|
||||
|
||||
it('does NOT render LoadingIndicator when embedded shell is focused and background shell is NOT visible', () => {
|
||||
it('does NOT render LoadingIndicator when embedded shell is focused and background shell is NOT visible', async () => {
|
||||
const uiState = createMockUIState({
|
||||
streamingState: StreamingState.Responding,
|
||||
embeddedShellFocused: true,
|
||||
isBackgroundShellVisible: false,
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).not.toContain('LoadingIndicator');
|
||||
@@ -491,7 +494,7 @@ describe('Composer', () => {
|
||||
});
|
||||
|
||||
describe('Message Queue Display', () => {
|
||||
it('displays queued messages when present', () => {
|
||||
it('displays queued messages when present', async () => {
|
||||
const uiState = createMockUIState({
|
||||
messageQueue: [
|
||||
'First queued message',
|
||||
@@ -500,7 +503,7 @@ describe('Composer', () => {
|
||||
],
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('First queued message');
|
||||
@@ -508,12 +511,12 @@ describe('Composer', () => {
|
||||
expect(output).toContain('Third queued message');
|
||||
});
|
||||
|
||||
it('renders QueuedMessageDisplay with empty message queue', () => {
|
||||
it('renders QueuedMessageDisplay with empty message queue', async () => {
|
||||
const uiState = createMockUIState({
|
||||
messageQueue: [],
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
// The component should render but return null for empty queue
|
||||
// This test verifies that the component receives the correct prop
|
||||
@@ -523,14 +526,14 @@ describe('Composer', () => {
|
||||
});
|
||||
|
||||
describe('Context and Status Display', () => {
|
||||
it('shows StatusDisplay and ApprovalModeIndicator in normal state', () => {
|
||||
it('shows StatusDisplay and ApprovalModeIndicator in normal state', async () => {
|
||||
const uiState = createMockUIState({
|
||||
ctrlCPressedOnce: false,
|
||||
ctrlDPressedOnce: false,
|
||||
showEscapePrompt: false,
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('StatusDisplay');
|
||||
@@ -538,12 +541,12 @@ describe('Composer', () => {
|
||||
expect(output).not.toContain('ToastDisplay');
|
||||
});
|
||||
|
||||
it('shows ToastDisplay and hides ApprovalModeIndicator when a toast is present', () => {
|
||||
it('shows ToastDisplay and hides ApprovalModeIndicator when a toast is present', async () => {
|
||||
const uiState = createMockUIState({
|
||||
ctrlCPressedOnce: true,
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('ToastDisplay');
|
||||
@@ -551,7 +554,7 @@ describe('Composer', () => {
|
||||
expect(output).toContain('StatusDisplay');
|
||||
});
|
||||
|
||||
it('shows ToastDisplay for other toast types', () => {
|
||||
it('shows ToastDisplay for other toast types', async () => {
|
||||
const uiState = createMockUIState({
|
||||
transientMessage: {
|
||||
text: 'Warning',
|
||||
@@ -559,7 +562,7 @@ describe('Composer', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('ToastDisplay');
|
||||
@@ -568,12 +571,12 @@ describe('Composer', () => {
|
||||
});
|
||||
|
||||
describe('Input and Indicators', () => {
|
||||
it('hides non-essential UI details in clean mode', () => {
|
||||
it('hides non-essential UI details in clean mode', async () => {
|
||||
const uiState = createMockUIState({
|
||||
cleanUiDetailsVisible: false,
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('ShortcutsHint');
|
||||
@@ -583,22 +586,22 @@ describe('Composer', () => {
|
||||
expect(output).not.toContain('ContextSummaryDisplay');
|
||||
});
|
||||
|
||||
it('renders InputPrompt when input is active', () => {
|
||||
it('renders InputPrompt when input is active', async () => {
|
||||
const uiState = createMockUIState({
|
||||
isInputActive: true,
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).toContain('InputPrompt');
|
||||
});
|
||||
|
||||
it('does not render InputPrompt when input is inactive', () => {
|
||||
it('does not render InputPrompt when input is inactive', async () => {
|
||||
const uiState = createMockUIState({
|
||||
isInputActive: false,
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).not.toContain('InputPrompt');
|
||||
});
|
||||
@@ -610,44 +613,44 @@ describe('Composer', () => {
|
||||
[ApprovalMode.YOLO],
|
||||
])(
|
||||
'shows ApprovalModeIndicator when approval mode is %s and shell mode is inactive',
|
||||
(mode) => {
|
||||
async (mode) => {
|
||||
const uiState = createMockUIState({
|
||||
showApprovalModeIndicator: mode,
|
||||
shellModeActive: false,
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).toMatch(/ApprovalModeIndic[\s\S]*ator/);
|
||||
},
|
||||
);
|
||||
|
||||
it('shows ShellModeIndicator when shell mode is active', () => {
|
||||
it('shows ShellModeIndicator when shell mode is active', async () => {
|
||||
const uiState = createMockUIState({
|
||||
shellModeActive: true,
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).toMatch(/ShellModeIndic[\s\S]*tor/);
|
||||
});
|
||||
|
||||
it('shows RawMarkdownIndicator when renderMarkdown is false', () => {
|
||||
it('shows RawMarkdownIndicator when renderMarkdown is false', async () => {
|
||||
const uiState = createMockUIState({
|
||||
renderMarkdown: false,
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).toContain('raw markdown mode');
|
||||
});
|
||||
|
||||
it('does not show RawMarkdownIndicator when renderMarkdown is true', () => {
|
||||
it('does not show RawMarkdownIndicator when renderMarkdown is true', async () => {
|
||||
const uiState = createMockUIState({
|
||||
renderMarkdown: true,
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).not.toContain('raw markdown mode');
|
||||
});
|
||||
@@ -658,18 +661,18 @@ describe('Composer', () => {
|
||||
[ApprovalMode.AUTO_EDIT, 'auto edit'],
|
||||
])(
|
||||
'shows minimal mode badge "%s" when clean UI details are hidden',
|
||||
(mode, label) => {
|
||||
async (mode, label) => {
|
||||
const uiState = createMockUIState({
|
||||
cleanUiDetailsVisible: false,
|
||||
showApprovalModeIndicator: mode,
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
expect(lastFrame()).toContain(label);
|
||||
},
|
||||
);
|
||||
|
||||
it('hides minimal mode badge while loading in clean mode', () => {
|
||||
it('hides minimal mode badge while loading in clean mode', async () => {
|
||||
const uiState = createMockUIState({
|
||||
cleanUiDetailsVisible: false,
|
||||
streamingState: StreamingState.Responding,
|
||||
@@ -677,14 +680,14 @@ describe('Composer', () => {
|
||||
showApprovalModeIndicator: ApprovalMode.PLAN,
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('LoadingIndicator');
|
||||
expect(output).not.toContain('plan');
|
||||
expect(output).not.toContain('ShortcutsHint');
|
||||
});
|
||||
|
||||
it('hides minimal mode badge while action-required state is active', () => {
|
||||
it('hides minimal mode badge while action-required state is active', async () => {
|
||||
const uiState = createMockUIState({
|
||||
cleanUiDetailsVisible: false,
|
||||
showApprovalModeIndicator: ApprovalMode.PLAN,
|
||||
@@ -695,26 +698,26 @@ describe('Composer', () => {
|
||||
),
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
const output = lastFrame();
|
||||
expect(output).not.toContain('plan');
|
||||
expect(output).not.toContain('ShortcutsHint');
|
||||
});
|
||||
|
||||
it('shows Esc rewind prompt in minimal mode without showing full UI', () => {
|
||||
it('shows Esc rewind prompt in minimal mode without showing full UI', async () => {
|
||||
const uiState = createMockUIState({
|
||||
cleanUiDetailsVisible: false,
|
||||
showEscapePrompt: true,
|
||||
history: [{ id: 1, type: 'user', text: 'msg' }],
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('ToastDisplay');
|
||||
expect(output).not.toContain('ContextSummaryDisplay');
|
||||
});
|
||||
|
||||
it('shows context usage bleed-through when over 60%', () => {
|
||||
it('shows context usage bleed-through when over 60%', async () => {
|
||||
const model = 'gemini-2.5-pro';
|
||||
const uiState = createMockUIState({
|
||||
cleanUiDetailsVisible: false,
|
||||
@@ -734,13 +737,13 @@ describe('Composer', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState, settings);
|
||||
const { lastFrame } = await renderComposer(uiState, settings);
|
||||
expect(lastFrame()).toContain('%');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Details Display', () => {
|
||||
it('shows DetailedMessagesDisplay when showErrorDetails is true', () => {
|
||||
it('shows DetailedMessagesDisplay when showErrorDetails is true', async () => {
|
||||
const uiState = createMockUIState({
|
||||
showErrorDetails: true,
|
||||
filteredConsoleMessages: [
|
||||
@@ -752,18 +755,18 @@ describe('Composer', () => {
|
||||
],
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).toContain('DetailedMessagesDisplay');
|
||||
expect(lastFrame()).toContain('ShowMoreLines');
|
||||
});
|
||||
|
||||
it('does not show error details when showErrorDetails is false', () => {
|
||||
it('does not show error details when showErrorDetails is false', async () => {
|
||||
const uiState = createMockUIState({
|
||||
showErrorDetails: false,
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).not.toContain('DetailedMessagesDisplay');
|
||||
});
|
||||
@@ -780,7 +783,7 @@ describe('Composer', () => {
|
||||
setVimMode: vi.fn(),
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).toContain(
|
||||
"InputPrompt: Press 'Esc' for NORMAL mode.",
|
||||
@@ -797,7 +800,7 @@ describe('Composer', () => {
|
||||
setVimMode: vi.fn(),
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).toContain(
|
||||
"InputPrompt: Press 'i' for INSERT mode.",
|
||||
@@ -806,7 +809,7 @@ describe('Composer', () => {
|
||||
});
|
||||
|
||||
describe('Shortcuts Hint', () => {
|
||||
it('hides shortcuts hint when showShortcutsHint setting is false', () => {
|
||||
it('hides shortcuts hint when showShortcutsHint setting is false', async () => {
|
||||
const uiState = createMockUIState();
|
||||
const settings = createMockSettings({
|
||||
ui: {
|
||||
@@ -814,12 +817,12 @@ describe('Composer', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState, settings);
|
||||
const { lastFrame } = await renderComposer(uiState, settings);
|
||||
|
||||
expect(lastFrame()).not.toContain('ShortcutsHint');
|
||||
});
|
||||
|
||||
it('hides shortcuts hint when a action is required (e.g. dialog is open)', () => {
|
||||
it('hides shortcuts hint when a action is required (e.g. dialog is open)', async () => {
|
||||
const uiState = createMockUIState({
|
||||
customDialog: (
|
||||
<Box>
|
||||
@@ -829,55 +832,55 @@ describe('Composer', () => {
|
||||
),
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).not.toContain('ShortcutsHint');
|
||||
});
|
||||
|
||||
it('keeps shortcuts hint visible when no action is required', () => {
|
||||
it('keeps shortcuts hint visible when no action is required', async () => {
|
||||
const uiState = createMockUIState({
|
||||
cleanUiDetailsVisible: false,
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).toContain('ShortcutsHint');
|
||||
});
|
||||
|
||||
it('shows shortcuts hint when full UI details are visible', () => {
|
||||
it('shows shortcuts hint when full UI details are visible', async () => {
|
||||
const uiState = createMockUIState({
|
||||
cleanUiDetailsVisible: true,
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).toContain('ShortcutsHint');
|
||||
});
|
||||
|
||||
it('hides shortcuts hint while loading in minimal mode', () => {
|
||||
it('hides shortcuts hint while loading in minimal mode', async () => {
|
||||
const uiState = createMockUIState({
|
||||
cleanUiDetailsVisible: false,
|
||||
streamingState: StreamingState.Responding,
|
||||
elapsedTime: 1,
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).not.toContain('ShortcutsHint');
|
||||
});
|
||||
|
||||
it('shows shortcuts help in minimal mode when toggled on', () => {
|
||||
it('shows shortcuts help in minimal mode when toggled on', async () => {
|
||||
const uiState = createMockUIState({
|
||||
cleanUiDetailsVisible: false,
|
||||
shortcutsHelpVisible: true,
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).toContain('ShortcutsHelp');
|
||||
});
|
||||
|
||||
it('hides shortcuts hint when suggestions are visible above input in alternate buffer', () => {
|
||||
it('hides shortcuts hint when suggestions are visible above input in alternate buffer', async () => {
|
||||
composerTestControls.isAlternateBuffer = true;
|
||||
composerTestControls.suggestionsVisible = true;
|
||||
|
||||
@@ -886,13 +889,13 @@ describe('Composer', () => {
|
||||
showApprovalModeIndicator: ApprovalMode.PLAN,
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).not.toContain('ShortcutsHint');
|
||||
expect(lastFrame()).not.toContain('plan');
|
||||
});
|
||||
|
||||
it('hides approval mode indicator when suggestions are visible above input in alternate buffer', () => {
|
||||
it('hides approval mode indicator when suggestions are visible above input in alternate buffer', async () => {
|
||||
composerTestControls.isAlternateBuffer = true;
|
||||
composerTestControls.suggestionsVisible = true;
|
||||
|
||||
@@ -901,12 +904,12 @@ describe('Composer', () => {
|
||||
showApprovalModeIndicator: ApprovalMode.YOLO,
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).not.toContain('ApprovalModeIndicator');
|
||||
});
|
||||
|
||||
it('keeps shortcuts hint when suggestions are visible below input in regular buffer', () => {
|
||||
it('keeps shortcuts hint when suggestions are visible below input in regular buffer', async () => {
|
||||
composerTestControls.isAlternateBuffer = false;
|
||||
composerTestControls.suggestionsVisible = true;
|
||||
|
||||
@@ -914,36 +917,36 @@ describe('Composer', () => {
|
||||
cleanUiDetailsVisible: false,
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).toContain('ShortcutsHint');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Shortcuts Help', () => {
|
||||
it('shows shortcuts help in passive state', () => {
|
||||
it('shows shortcuts help in passive state', async () => {
|
||||
const uiState = createMockUIState({
|
||||
shortcutsHelpVisible: true,
|
||||
streamingState: StreamingState.Idle,
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).toContain('ShortcutsHelp');
|
||||
});
|
||||
|
||||
it('hides shortcuts help while streaming', () => {
|
||||
it('hides shortcuts help while streaming', async () => {
|
||||
const uiState = createMockUIState({
|
||||
shortcutsHelpVisible: true,
|
||||
streamingState: StreamingState.Responding,
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).not.toContain('ShortcutsHelp');
|
||||
});
|
||||
|
||||
it('hides shortcuts help when action is required', () => {
|
||||
it('hides shortcuts help when action is required', async () => {
|
||||
const uiState = createMockUIState({
|
||||
shortcutsHelpVisible: true,
|
||||
customDialog: (
|
||||
@@ -953,20 +956,20 @@ describe('Composer', () => {
|
||||
),
|
||||
});
|
||||
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).not.toContain('ShortcutsHelp');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Snapshots', () => {
|
||||
it('matches snapshot in idle state', () => {
|
||||
it('matches snapshot in idle state', async () => {
|
||||
const uiState = createMockUIState();
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot while streaming', () => {
|
||||
it('matches snapshot while streaming', async () => {
|
||||
const uiState = createMockUIState({
|
||||
streamingState: StreamingState.Responding,
|
||||
thought: {
|
||||
@@ -974,33 +977,33 @@ describe('Composer', () => {
|
||||
description: 'Thinking about the meaning of life...',
|
||||
},
|
||||
});
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot in narrow view', () => {
|
||||
it('matches snapshot in narrow view', async () => {
|
||||
const uiState = createMockUIState({
|
||||
terminalWidth: 40,
|
||||
});
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot in minimal UI mode', () => {
|
||||
it('matches snapshot in minimal UI mode', async () => {
|
||||
const uiState = createMockUIState({
|
||||
cleanUiDetailsVisible: false,
|
||||
});
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot in minimal UI mode while loading', () => {
|
||||
it('matches snapshot in minimal UI mode while loading', async () => {
|
||||
const uiState = createMockUIState({
|
||||
cleanUiDetailsVisible: false,
|
||||
streamingState: StreamingState.Responding,
|
||||
elapsedTime: 1000,
|
||||
});
|
||||
const { lastFrame } = renderComposer(uiState);
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,8 +42,9 @@ describe('ConfigInitDisplay', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('renders initial state', () => {
|
||||
const { lastFrame } = render(<ConfigInitDisplay />);
|
||||
it('renders initial state', async () => {
|
||||
const { lastFrame, waitUntilReady } = render(<ConfigInitDisplay />);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
|
||||
@@ -31,15 +31,16 @@ describe('ConsentPrompt', () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders a string prompt with MarkdownDisplay', () => {
|
||||
it('renders a string prompt with MarkdownDisplay', async () => {
|
||||
const prompt = 'Are you sure?';
|
||||
const { unmount } = render(
|
||||
const { waitUntilReady, unmount } = render(
|
||||
<ConsentPrompt
|
||||
prompt={prompt}
|
||||
onConfirm={onConfirm}
|
||||
terminalWidth={terminalWidth}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(MockedMarkdownDisplay).toHaveBeenCalledWith(
|
||||
{
|
||||
@@ -52,15 +53,16 @@ describe('ConsentPrompt', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders a ReactNode prompt directly', () => {
|
||||
it('renders a ReactNode prompt directly', async () => {
|
||||
const prompt = <Text>Are you sure?</Text>;
|
||||
const { lastFrame, unmount } = render(
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<ConsentPrompt
|
||||
prompt={prompt}
|
||||
onConfirm={onConfirm}
|
||||
terminalWidth={terminalWidth}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(MockedMarkdownDisplay).not.toHaveBeenCalled();
|
||||
expect(lastFrame()).toContain('Are you sure?');
|
||||
@@ -69,18 +71,20 @@ describe('ConsentPrompt', () => {
|
||||
|
||||
it('calls onConfirm with true when "Yes" is selected', async () => {
|
||||
const prompt = 'Are you sure?';
|
||||
const { unmount } = render(
|
||||
const { waitUntilReady, unmount } = render(
|
||||
<ConsentPrompt
|
||||
prompt={prompt}
|
||||
onConfirm={onConfirm}
|
||||
terminalWidth={terminalWidth}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const onSelect = MockedRadioButtonSelect.mock.calls[0][0].onSelect;
|
||||
await act(async () => {
|
||||
onSelect(true);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
expect(onConfirm).toHaveBeenCalledWith(true);
|
||||
unmount();
|
||||
@@ -88,32 +92,35 @@ describe('ConsentPrompt', () => {
|
||||
|
||||
it('calls onConfirm with false when "No" is selected', async () => {
|
||||
const prompt = 'Are you sure?';
|
||||
const { unmount } = render(
|
||||
const { waitUntilReady, unmount } = render(
|
||||
<ConsentPrompt
|
||||
prompt={prompt}
|
||||
onConfirm={onConfirm}
|
||||
terminalWidth={terminalWidth}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
const onSelect = MockedRadioButtonSelect.mock.calls[0][0].onSelect;
|
||||
await act(async () => {
|
||||
onSelect(false);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
expect(onConfirm).toHaveBeenCalledWith(false);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('passes correct items to RadioButtonSelect', () => {
|
||||
it('passes correct items to RadioButtonSelect', async () => {
|
||||
const prompt = 'Are you sure?';
|
||||
const { unmount } = render(
|
||||
const { waitUntilReady, unmount } = render(
|
||||
<ConsentPrompt
|
||||
prompt={prompt}
|
||||
onConfirm={onConfirm}
|
||||
terminalWidth={terminalWidth}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(MockedRadioButtonSelect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
||||
@@ -9,19 +9,27 @@ import { ConsoleSummaryDisplay } from './ConsoleSummaryDisplay.js';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('ConsoleSummaryDisplay', () => {
|
||||
it('renders nothing when errorCount is 0', () => {
|
||||
const { lastFrame } = render(<ConsoleSummaryDisplay errorCount={0} />);
|
||||
expect(lastFrame()).toBe('');
|
||||
it('renders nothing when errorCount is 0', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<ConsoleSummaryDisplay errorCount={0} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it.each([
|
||||
[1, '1 error'],
|
||||
[5, '5 errors'],
|
||||
])('renders correct message for %i errors', (count, expectedText) => {
|
||||
const { lastFrame } = render(<ConsoleSummaryDisplay errorCount={count} />);
|
||||
])('renders correct message for %i errors', async (count, expectedText) => {
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<ConsoleSummaryDisplay errorCount={count} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain(expectedText);
|
||||
expect(output).toContain('✖');
|
||||
expect(output).toContain('(F12 for details)');
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,12 +21,14 @@ afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
const renderWithWidth = (
|
||||
const renderWithWidth = async (
|
||||
width: number,
|
||||
props: React.ComponentProps<typeof ContextSummaryDisplay>,
|
||||
) => {
|
||||
useTerminalSizeMock.mockReturnValue({ columns: width, rows: 24 });
|
||||
return render(<ContextSummaryDisplay {...props} />);
|
||||
const result = render(<ContextSummaryDisplay {...props} />);
|
||||
await result.waitUntilReady();
|
||||
return result;
|
||||
};
|
||||
|
||||
describe('<ContextSummaryDisplay />', () => {
|
||||
@@ -42,7 +44,7 @@ describe('<ContextSummaryDisplay />', () => {
|
||||
skillCount: 1,
|
||||
};
|
||||
|
||||
it('should render on a single line on a wide screen', () => {
|
||||
it('should render on a single line on a wide screen', async () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
geminiMdFileCount: 1,
|
||||
@@ -54,12 +56,12 @@ describe('<ContextSummaryDisplay />', () => {
|
||||
},
|
||||
},
|
||||
};
|
||||
const { lastFrame, unmount } = renderWithWidth(120, props);
|
||||
const { lastFrame, unmount } = await renderWithWidth(120, props);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render on multiple lines on a narrow screen', () => {
|
||||
it('should render on multiple lines on a narrow screen', async () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
geminiMdFileCount: 1,
|
||||
@@ -71,12 +73,12 @@ describe('<ContextSummaryDisplay />', () => {
|
||||
},
|
||||
},
|
||||
};
|
||||
const { lastFrame, unmount } = renderWithWidth(60, props);
|
||||
const { lastFrame, unmount } = await renderWithWidth(60, props);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should switch layout at the 80-column breakpoint', () => {
|
||||
it('should switch layout at the 80-column breakpoint', async () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
geminiMdFileCount: 1,
|
||||
@@ -90,23 +92,19 @@ describe('<ContextSummaryDisplay />', () => {
|
||||
};
|
||||
|
||||
// At 80 columns, should be on one line
|
||||
const { lastFrame: wideFrame, unmount: unmountWide } = renderWithWidth(
|
||||
80,
|
||||
props,
|
||||
);
|
||||
expect(wideFrame()!.includes('\n')).toBe(false);
|
||||
const { lastFrame: wideFrame, unmount: unmountWide } =
|
||||
await renderWithWidth(80, props);
|
||||
expect(wideFrame().trim().includes('\n')).toBe(false);
|
||||
unmountWide();
|
||||
|
||||
// At 79 columns, should be on multiple lines
|
||||
const { lastFrame: narrowFrame, unmount: unmountNarrow } = renderWithWidth(
|
||||
79,
|
||||
props,
|
||||
);
|
||||
expect(narrowFrame()!.includes('\n')).toBe(true);
|
||||
expect(narrowFrame()!.split('\n').length).toBe(4);
|
||||
const { lastFrame: narrowFrame, unmount: unmountNarrow } =
|
||||
await renderWithWidth(79, props);
|
||||
expect(narrowFrame().trim().includes('\n')).toBe(true);
|
||||
expect(narrowFrame().trim().split('\n').length).toBe(4);
|
||||
unmountNarrow();
|
||||
});
|
||||
it('should not render empty parts', () => {
|
||||
it('should not render empty parts', async () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
geminiMdFileCount: 0,
|
||||
@@ -119,7 +117,7 @@ describe('<ContextSummaryDisplay />', () => {
|
||||
},
|
||||
},
|
||||
};
|
||||
const { lastFrame, unmount } = renderWithWidth(60, props);
|
||||
const { lastFrame, unmount } = await renderWithWidth(60, props);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user