diff --git a/.gemini/settings.json b/.gemini/settings.json index ea7f0947cc..1a4c889066 100644 --- a/.gemini/settings.json +++ b/.gemini/settings.json @@ -1,7 +1,8 @@ { "experimental": { "plan": true, - "extensionReloading": true + "extensionReloading": true, + "modelSteering": true }, "general": { "devtools": true diff --git a/.gemini/skills/docs-changelog/SKILL.md b/.gemini/skills/docs-changelog/SKILL.md index d3a2f63623..f175260abd 100644 --- a/.gemini/skills/docs-changelog/SKILL.md +++ b/.gemini/skills/docs-changelog/SKILL.md @@ -59,6 +59,10 @@ To standardize the process of updating changelog files (`latest.md`, *Use this path if the version number ends in `.0`.* +**Important:** Based on the version, you must choose to follow either section +A.1 for stable releases or A.2 for preview releases. Do not follow the +instructions for the other section. + ### A.1: Stable Release (e.g., `v0.28.0`) For a stable release, you will generate two distinct summaries from the @@ -73,7 +77,8 @@ detailed **highlights** section for the release-specific page. use the existing announcements in `docs/changelogs/index.md` and the example within `.gemini/skills/docs-changelog/references/index_template.md` as your - guide. This format includes PR links and authors. + guide. This format includes PR links and authors. Stick to 1 or 2 PR + links and authors. - Add this new announcement to the top of `docs/changelogs/index.md`. 2. **Create Highlights and Update `latest.md`**: @@ -105,6 +110,10 @@ detailed **highlights** section for the release-specific page. *Use this path if the version number does **not** end in `.0`.* +**Important:** Based on the version, you must choose to follow either section +B.1 for stable patches or B.2 for preview patches. Do not follow the +instructions for the other section. + ### B.1: Stable Patch (e.g., `v0.28.1`) - **Target File**: `docs/changelogs/latest.md` @@ -113,10 +122,12 @@ detailed **highlights** section for the release-specific page. `# Latest stable release: {{version}}` 2. Update the rease date. The line should read, `Released: {{release_date_month_dd_yyyy}}` - 3. **Prepend** the processed "What's Changed" list from the temporary file + 3. Determine if a "What's Changed" section exists in the temporary file + If so, continue to step 4. Otherwise, skip to step 5. + 4. **Prepend** the processed "What's Changed" list from the temporary file to the existing "What's Changed" list in `latest.md`. Do not change or replace the existing list, **only add** to the beginning of it. - 4. In the "Full Changelog", edit **only** the end of the URL. Identify the + 5. In the "Full Changelog", edit **only** the end of the URL. Identify the last part of the URL that looks like `...{previous_version}` and update it to be `...{version}`. @@ -133,10 +144,12 @@ detailed **highlights** section for the release-specific page. `# Preview release: {{version}}` 2. Update the rease date. The line should read, `Released: {{release_date_month_dd_yyyy}}` - 3. **Prepend** the processed "What's Changed" list from the temporary file + 3. Determine if a "What's Changed" section exists in the temporary file + If so, continue to step 4. Otherwise, skip to step 5. + 4. **Prepend** the processed "What's Changed" list from the temporary file to the existing "What's Changed" list in `preview.md`. Do not change or replace the existing list, **only add** to the beginning of it. - 4. In the "Full Changelog", edit **only** the end of the URL. Identify the + 5. In the "Full Changelog", edit **only** the end of the URL. Identify the last part of the URL that looks like `...{previous_version}` and update it to be `...{version}`. @@ -149,5 +162,5 @@ detailed **highlights** section for the release-specific page. ## Finalize -- After making changes, run `npm run format` to ensure consistency. +- After making changes, run `npm run format` ONLY to ensure consistency. - Delete any temporary files created during the process. diff --git a/.github/workflows/gemini-automated-issue-triage.yml b/.github/workflows/gemini-automated-issue-triage.yml index 9e50f11433..fe4c52292a 100644 --- a/.github/workflows/gemini-automated-issue-triage.yml +++ b/.github/workflows/gemini-automated-issue-triage.yml @@ -158,7 +158,7 @@ jobs: }, "coreTools": [ "run_shell_command(echo)" - ], + ] } prompt: |- ## Role diff --git a/.github/workflows/release-notes.yml b/.github/workflows/release-notes.yml index 08a3625822..8a681dadf6 100644 --- a/.github/workflows/release-notes.yml +++ b/.github/workflows/release-notes.yml @@ -56,7 +56,18 @@ jobs: GH_TOKEN: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}' BODY: '${{ github.event.inputs.body || github.event.release.body }}' + - name: 'Validate version' + id: 'validate_version' + run: | + if echo "${{ steps.release_info.outputs.VERSION }}" | grep -q "nightly"; then + echo "Nightly release detected. Stopping workflow." + echo "CONTINUE=false" >> "$GITHUB_OUTPUT" + else + echo "CONTINUE=true" >> "$GITHUB_OUTPUT" + fi + - name: 'Generate Changelog with Gemini' + if: "steps.validate_version.outputs.CONTINUE == 'true'" uses: 'google-github-actions/run-gemini-cli@a3bf79042542528e91937b3a3a6fbc4967ee3c31' # ratchet:google-github-actions/run-gemini-cli@v0 with: gemini_api_key: '${{ secrets.GEMINI_API_KEY }}' @@ -70,7 +81,10 @@ jobs: Execute the release notes generation process using the information provided. + When you are done, please output your thought process and the steps you took for future debugging purposes. + - name: 'Create Pull Request' + if: "steps.validate_version.outputs.CONTINUE == 'true'" uses: 'peter-evans/create-pull-request@v6' with: token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}' diff --git a/.gitignore b/.gitignore index a2a6553cd3..0438549485 100644 --- a/.gitignore +++ b/.gitignore @@ -61,4 +61,4 @@ gemini-debug.log .genkit .gemini-clipboard/ .eslintcache -evals/logs/ +evals/logs/ \ No newline at end of file diff --git a/docs/changelogs/index.md b/docs/changelogs/index.md index 3cff4c123b..4a20557df7 100644 --- a/docs/changelogs/index.md +++ b/docs/changelogs/index.md @@ -18,6 +18,28 @@ on GitHub. | [Preview](preview.md) | Experimental features ready for early feedback. | | [Stable](latest.md) | Stable, recommended for general use. | +## Announcements: v0.30.0 - 2026-02-25 + +- **SDK & Custom Skills:** Introduced the initial SDK package, enabling dynamic + system instructions, `SessionContext` for SDK tool calls, and support for + custom skills + ([#18861](https://github.com/google-gemini/gemini-cli/pull/18861) by + @mbleigh). +- **Policy Engine Enhancements:** Added a new `--policy` flag for user-defined + policies, introduced strict seatbelt profiles, and deprecated + `--allowed-tools` in favor of the policy engine + ([#18500](https://github.com/google-gemini/gemini-cli/pull/18500) by + @allenhutchison). +- **UI & Themes:** Added a generic searchable list for settings and extensions, + new Solarized themes, text wrapping for markdown tables, and a clean UI toggle + prototype ([#19064](https://github.com/google-gemini/gemini-cli/pull/19064) by + @rmedranollamas). +- **Vim & Terminal Interaction:** Improved Vim support to feel more complete and + added support for Ctrl-Z terminal suspension + ([#18755](https://github.com/google-gemini/gemini-cli/pull/18755) by + @ppgranger, [#18931](https://github.com/google-gemini/gemini-cli/pull/18931) + by @scidomino). + ## Announcements: v0.29.0 - 2026-02-17 - **Plan Mode:** A new comprehensive planning capability with `/plan`, diff --git a/docs/changelogs/latest.md b/docs/changelogs/latest.md index 91d669ba77..8fb3f6aa87 100644 --- a/docs/changelogs/latest.md +++ b/docs/changelogs/latest.md @@ -1,6 +1,6 @@ -# Latest stable release: v0.29.0 +# Latest stable release: v0.30.0 -Released: February 17, 2026 +Released: February 25, 2026 For most users, our latest stable release is the recommended release. Install the latest stable version with: @@ -11,371 +11,323 @@ npm install -g @google/gemini-cli ## Highlights -- **Plan Mode:** Introduce a dedicated "Plan Mode" to help you architect complex - changes before implementation. Use `/plan` to get started. -- **Gemini 3 by Default:** Gemini 3 is now the default model family, bringing - improved performance and reasoning capabilities to all users without needing a - feature flag. -- **Extension Discovery:** Easily discover and install extensions with the new - exploration features and registry client. -- **Enhanced Admin Controls:** New administrative capabilities allow for - allowlisting MCP server configurations, giving organizations more control over - available tools. -- **Sub-agent Improvements:** Sub-agents have been transitioned to a new format - with improved definitions and system prompts for better reliability. +- **SDK & Custom Skills**: Introduced the initial SDK package, dynamic system + instructions, `SessionContext` for SDK tool calls, and support for custom + skills. +- **Policy Engine Enhancements**: Added a `--policy` flag for user-defined + policies, strict seatbelt profiles, and transitioned away from + `--allowed-tools`. +- **UI & Themes**: Introduced a generic searchable list for settings and + extensions, added Solarized Dark and Light themes, text wrapping capabilities + to markdown tables, and a clean UI toggle prototype. +- **Vim Support & Ctrl-Z**: Improved Vim support to provide a more complete + experience and added support for Ctrl-Z suspension. +- **Plan Mode & Tools**: Plan Mode now supports project exploration without + planning and skills can be enabled in plan mode. Tool output masking is + enabled by default, and core tool definitions have been centralized. ## What's Changed -- fix: remove `ask_user` tool from non-interactive modes by @jackwotherspoon in - [#18154](https://github.com/google-gemini/gemini-cli/pull/18154) -- fix(cli): allow restricted .env loading in untrusted sandboxed folders by - @galz10 in [#17806](https://github.com/google-gemini/gemini-cli/pull/17806) -- Encourage agent to utilize ecosystem tools to perform work by @gundermanc in - [#17881](https://github.com/google-gemini/gemini-cli/pull/17881) -- feat(plan): unify workflow location in system prompt to optimize caching by - @jerop in [#18258](https://github.com/google-gemini/gemini-cli/pull/18258) -- feat(core): enable getUserTierName in config by @sehoon38 in - [#18265](https://github.com/google-gemini/gemini-cli/pull/18265) -- feat(core): add default execution limits for subagents by @abhipatel12 in - [#18274](https://github.com/google-gemini/gemini-cli/pull/18274) -- Fix issue where agent gets stuck at interactive commands. by @gundermanc in - [#18272](https://github.com/google-gemini/gemini-cli/pull/18272) -- chore(release): bump version to 0.29.0-nightly.20260203.71f46f116 by +- feat(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 - [#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 - [#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) 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 + [#18772](https://github.com/google-gemini/gemini-cli/pull/18772) +- chore: cleanup unused and add unlisted dependencies in packages/core 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 + [#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 - [#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 + [#19053](https://github.com/google-gemini/gemini-cli/pull/19053) +- fix(acp): Wait for mcp initialization in acp (#18893) by @Mervap in + [#18894](https://github.com/google-gemini/gemini-cli/pull/18894) +- docs: format UTC times in releases doc by @pavan-sh in + [#18169](https://github.com/google-gemini/gemini-cli/pull/18169) +- Docs: Clarify extensions documentation. by @jkcinouye in + [#19277](https://github.com/google-gemini/gemini-cli/pull/19277) +- refactor(core): modularize tool definitions by model family by @aishaneeshah + in [#19269](https://github.com/google-gemini/gemini-cli/pull/19269) +- fix(paths): Add cross-platform path normalization by @spencer426 in + [#18939](https://github.com/google-gemini/gemini-cli/pull/18939) +- feat(core): experimental in-progress steering hints (1 of 3) by @joshualitt in + [#19008](https://github.com/google-gemini/gemini-cli/pull/19008) +- fix(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 - [#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 + [#19490](https://github.com/google-gemini/gemini-cli/pull/19490) +- fix(patch): cherry-pick c43500c to release/v0.30.0-preview.1-pr-19502 to patch + version v0.30.0-preview.1 and create version 0.30.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 + [#19521](https://github.com/google-gemini/gemini-cli/pull/19521) +- fix(patch): cherry-pick aa9163d to release/v0.30.0-preview.3-pr-19991 to patch + version v0.30.0-preview.3 and create version 0.30.0-preview.4 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 + [#20040](https://github.com/google-gemini/gemini-cli/pull/20040) +- fix(patch): cherry-pick 2c1d6f8 to release/v0.30.0-preview.4-pr-19369 to patch + version v0.30.0-preview.4 and create version 0.30.0-preview.5 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 + [#20086](https://github.com/google-gemini/gemini-cli/pull/20086) +- fix(patch): cherry-pick d96bd05 to release/v0.30.0-preview.5-pr-19867 to patch + version v0.30.0-preview.5 and create version 0.30.0-preview.6 by @gemini-cli-robot in - [#19274](https://github.com/google-gemini/gemini-cli/pull/19274) + [#20112](https://github.com/google-gemini/gemini-cli/pull/20112) **Full Changelog**: -https://github.com/google-gemini/gemini-cli/compare/v0.28.2...v0.29.0 +https://github.com/google-gemini/gemini-cli/compare/v0.29.7...v0.30.0 diff --git a/docs/changelogs/preview.md b/docs/changelogs/preview.md index 646106fa50..588573a37c 100644 --- a/docs/changelogs/preview.md +++ b/docs/changelogs/preview.md @@ -1,6 +1,6 @@ -# Preview release: v0.30.0-preview.5 +# Preview release: v0.31.0-preview.0 -Released: February 24, 2026 +Released: February 25, 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,306 +13,400 @@ npm install -g @google/gemini-cli@preview ## Highlights -- **Initial SDK Package:** Introduced the initial SDK package with support for - custom skills and dynamic system instructions. -- **Refined Plan Mode:** Refined Plan Mode with support for enabling skills, - improved agentic execution, and project exploration without planning. -- **Enhanced CLI UI:** Enhanced CLI UI with a new clean UI toggle, minimal-mode - bleed-through, and support for Ctrl-Z suspension. -- **`--policy` flag:** Added the `--policy` flag to support user-defined - policies. -- **New Themes:** Added Solarized Dark and Solarized Light themes. +- **Plan Mode Enhancements**: Numerous additions including automatic model + switching, custom storage directory configuration, message injection upon + manual exit, enforcement of read-only constraints, and centralized tool + visibility in the policy engine. +- **Policy Engine Updates**: Project-level policy support added, alongside MCP + server wildcard support, tool annotation propagation and matching, and + workspace-level "Always Allow" persistence. +- **MCP Integration Improvements**: Better integration through support for MCP + progress updates with input validation and throttling, environment variable + expansion for servers, and full details expansion on tool approval. +- **CLI & Core UX Enhancements**: Several UI and quality-of-life updates such as + Alt+D for forward word deletion, macOS run-event notifications, enhanced + folder trust configurations with security warnings, improved startup warnings, + and a new experimental browser agent. +- **Security & Stability**: Introduced the Conseca framework, deceptive URL and + Unicode character detection, stricter access checks, rate limits on web fetch, + and resolved multiple dependency vulnerabilities. ## What's Changed -- fix(patch): cherry-pick 2c1d6f8 to release/v0.30.0-preview.4-pr-19369 to patch - version v0.30.0-preview.4 and create version 0.30.0-preview.5 by - @gemini-cli-robot in - [#20086](https://github.com/google-gemini/gemini-cli/pull/20086) -- 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 +- Use ranged reads and limited searches and fuzzy editing improvements by + @gundermanc in + [#19240](https://github.com/google-gemini/gemini-cli/pull/19240) +- Fix bottom border color by @jacob314 in + [#19266](https://github.com/google-gemini/gemini-cli/pull/19266) +- Release note generator fix by @g-samroberts in + [#19363](https://github.com/google-gemini/gemini-cli/pull/19363) +- test(evals): add behavioral tests for tool output masking by @NTaylorMullen in + [#19172](https://github.com/google-gemini/gemini-cli/pull/19172) +- docs: clarify preflight instructions in GEMINI.md by @NTaylorMullen in + [#19377](https://github.com/google-gemini/gemini-cli/pull/19377) +- feat(cli): add gemini --resume hint on exit by @Mag1ck in + [#16285](https://github.com/google-gemini/gemini-cli/pull/16285) +- fix: optimize height calculations for ask_user dialog by @jackwotherspoon in + [#19017](https://github.com/google-gemini/gemini-cli/pull/19017) +- feat(cli): add Alt+D for forward word deletion by @scidomino in + [#19300](https://github.com/google-gemini/gemini-cli/pull/19300) +- Disable failing eval test by @chrstnb in + [#19455](https://github.com/google-gemini/gemini-cli/pull/19455) +- fix(cli): support legacy onConfirm callback in ToolActionsContext 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 + [#19369](https://github.com/google-gemini/gemini-cli/pull/19369) +- chore(deps): bump tar from 7.5.7 to 7.5.8 by dependabot[bot] in + [#19367](https://github.com/google-gemini/gemini-cli/pull/19367) +- fix(plan): allow safe fallback when experiment setting for plan is not enabled + but approval mode at startup is plan by @Adib234 in + [#19439](https://github.com/google-gemini/gemini-cli/pull/19439) +- Add explicit color-convert dependency by @chrstnb in + [#19460](https://github.com/google-gemini/gemini-cli/pull/19460) +- feat(devtools): migrate devtools package into monorepo by @SandyTao520 in + [#18936](https://github.com/google-gemini/gemini-cli/pull/18936) +- fix(core): clarify plan mode constraints and exit mechanism by @jerop in + [#19438](https://github.com/google-gemini/gemini-cli/pull/19438) +- feat(cli): add macOS run-event notifications (interactive only) 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 + [#19056](https://github.com/google-gemini/gemini-cli/pull/19056) +- Changelog for v0.29.0 by @gemini-cli-robot in + [#19361](https://github.com/google-gemini/gemini-cli/pull/19361) +- fix(ui): preventing empty history items from being added by @devr0306 in + [#19014](https://github.com/google-gemini/gemini-cli/pull/19014) +- Changelog for v0.30.0-preview.0 by @gemini-cli-robot in + [#19364](https://github.com/google-gemini/gemini-cli/pull/19364) +- feat(core): add support for MCP progress updates by @NTaylorMullen in + [#19046](https://github.com/google-gemini/gemini-cli/pull/19046) +- fix(core): ensure directory exists before writing conversation file by + @godwiniheuwa in + [#18429](https://github.com/google-gemini/gemini-cli/pull/18429) +- fix(ui): move margin from top to bottom in ToolGroupMessage by @imadraude in + [#17198](https://github.com/google-gemini/gemini-cli/pull/17198) +- fix(cli): treat unknown slash commands as regular input instead of showing + error by @skyvanguard in + [#17393](https://github.com/google-gemini/gemini-cli/pull/17393) +- feat(core): experimental in-progress steering hints (2 of 2) by @joshualitt in + [#19307](https://github.com/google-gemini/gemini-cli/pull/19307) +- docs(plan): add documentation for plan mode command by @Adib234 in + [#19467](https://github.com/google-gemini/gemini-cli/pull/19467) +- fix(core): ripgrep fails when pattern looks like ripgrep flag by @syvb in + [#18858](https://github.com/google-gemini/gemini-cli/pull/18858) +- fix(cli): disable auto-completion on Shift+Tab to preserve mode cycling by + @NTaylorMullen in + [#19451](https://github.com/google-gemini/gemini-cli/pull/19451) +- use issuer instead of authorization_endpoint for oauth discovery by + @garrettsparks in + [#17332](https://github.com/google-gemini/gemini-cli/pull/17332) +- feat(cli): include `/dir add` directories in @ autocomplete suggestions by + @jasmeetsb in [#19246](https://github.com/google-gemini/gemini-cli/pull/19246) +- feat(admin): Admin settings should only apply if adminControlsApplicable = + true and fetch errors should be fatal by @skeshive in + [#19453](https://github.com/google-gemini/gemini-cli/pull/19453) +- Format strict-development-rules command by @g-samroberts in + [#19484](https://github.com/google-gemini/gemini-cli/pull/19484) +- feat(core): centralize compatibility checks and add TrueColor detection by + @spencer426 in + [#19478](https://github.com/google-gemini/gemini-cli/pull/19478) +- Remove unused files and update index and sidebar. by @g-samroberts in + [#19479](https://github.com/google-gemini/gemini-cli/pull/19479) +- Migrate core render util to use xterm.js as part of the rendering loop. by + @jacob314 in [#19044](https://github.com/google-gemini/gemini-cli/pull/19044) +- Changelog for v0.30.0-preview.1 by @gemini-cli-robot in + [#19496](https://github.com/google-gemini/gemini-cli/pull/19496) +- build: replace deprecated built-in punycode with userland package by @jacob314 + in [#19502](https://github.com/google-gemini/gemini-cli/pull/19502) +- Speculative fixes to try to fix react error. by @jacob314 in + [#19508](https://github.com/google-gemini/gemini-cli/pull/19508) +- fix spacing by @jacob314 in + [#19494](https://github.com/google-gemini/gemini-cli/pull/19494) +- fix(core): ensure user rejections update tool outcome for telemetry by + @abhiasap in [#18982](https://github.com/google-gemini/gemini-cli/pull/18982) +- fix(acp): Initialize config (#18897) by @Mervap in + [#18898](https://github.com/google-gemini/gemini-cli/pull/18898) +- fix(core): add error logging for IDE fetch failures by @yuvrajangadsingh in + [#17981](https://github.com/google-gemini/gemini-cli/pull/17981) +- feat(acp): support set_mode interface (#18890) by @Mervap in + [#18891](https://github.com/google-gemini/gemini-cli/pull/18891) +- fix(core): robust workspace-based IDE connection discovery by @ehedlund in + [#18443](https://github.com/google-gemini/gemini-cli/pull/18443) +- Deflake windows tests. by @jacob314 in + [#19511](https://github.com/google-gemini/gemini-cli/pull/19511) +- Fix: Avoid tool confirmation timeout when no UI listeners are present by + @pdHaku0 in [#17955](https://github.com/google-gemini/gemini-cli/pull/17955) +- format md file by @scidomino in + [#19474](https://github.com/google-gemini/gemini-cli/pull/19474) +- feat(cli): add experimental.useOSC52Copy setting by @scidomino in + [#19488](https://github.com/google-gemini/gemini-cli/pull/19488) +- feat(cli): replace loading phrases boolean with enum setting by @LyalinDotCom + in [#19347](https://github.com/google-gemini/gemini-cli/pull/19347) +- Update skill to adjust for generated results. by @g-samroberts in + [#19500](https://github.com/google-gemini/gemini-cli/pull/19500) +- Fix message too large issue. by @gundermanc in + [#19499](https://github.com/google-gemini/gemini-cli/pull/19499) +- fix(core): prevent duplicate tool approval entries in auto-saved.toml by + @Abhijit-2592 in + [#19487](https://github.com/google-gemini/gemini-cli/pull/19487) +- fix(core): resolve crash in ClearcutLogger when os.cpus() is empty by @Adib234 + in [#19555](https://github.com/google-gemini/gemini-cli/pull/19555) +- chore(core): improve encapsulation and remove unused exports by @adamfweidman + in [#19556](https://github.com/google-gemini/gemini-cli/pull/19556) +- Revert "Add generic searchable list to back settings and extensions (… by + @chrstnb in [#19434](https://github.com/google-gemini/gemini-cli/pull/19434) +- fix(core): improve error type extraction for telemetry by @yunaseoul in + [#19565](https://github.com/google-gemini/gemini-cli/pull/19565) +- fix: remove extra padding in Composer by @jackwotherspoon in + [#19529](https://github.com/google-gemini/gemini-cli/pull/19529) +- feat(plan): support configuring custom plans storage directory by @jerop in + [#19577](https://github.com/google-gemini/gemini-cli/pull/19577) +- Migrate files to resource or references folder. by @g-samroberts in + [#19503](https://github.com/google-gemini/gemini-cli/pull/19503) +- feat(policy): implement project-level policy support by @Abhijit-2592 in + [#18682](https://github.com/google-gemini/gemini-cli/pull/18682) +- feat(core): Implement parallel FC for read only tools. by @joshualitt in + [#18791](https://github.com/google-gemini/gemini-cli/pull/18791) +- chore(skills): adds pr-address-comments skill to work on PR feedback by + @mbleigh in [#19576](https://github.com/google-gemini/gemini-cli/pull/19576) +- refactor(sdk): introduce session-based architecture by @mbleigh in + [#19180](https://github.com/google-gemini/gemini-cli/pull/19180) +- fix(ci): add fallback JSON extraction to issue triage workflow by @bdmorgan in + [#19593](https://github.com/google-gemini/gemini-cli/pull/19593) +- feat(core): refine Edit and WriteFile tool schemas for Gemini 3 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 + [#19476](https://github.com/google-gemini/gemini-cli/pull/19476) +- Changelog for v0.30.0-preview.3 by @gemini-cli-robot in + [#19585](https://github.com/google-gemini/gemini-cli/pull/19585) +- fix(plan): exclude EnterPlanMode tool from YOLO mode by @Adib234 in + [#19570](https://github.com/google-gemini/gemini-cli/pull/19570) +- chore: resolve build warnings and update dependencies by @mattKorwel in + [#18880](https://github.com/google-gemini/gemini-cli/pull/18880) +- feat(ui): add source indicators to slash commands by @ehedlund in + [#18839](https://github.com/google-gemini/gemini-cli/pull/18839) +- docs: refine Plan Mode documentation structure and workflow by @jerop in + [#19644](https://github.com/google-gemini/gemini-cli/pull/19644) +- Docs: Update release information regarding Gemini 3.1 by @jkcinouye in + [#19568](https://github.com/google-gemini/gemini-cli/pull/19568) +- fix(security): rate limit web_fetch tool to mitigate DDoS via prompt injection + by @mattKorwel in + [#19567](https://github.com/google-gemini/gemini-cli/pull/19567) +- Add initial implementation of /extensions explore command by @chrstnb in + [#19029](https://github.com/google-gemini/gemini-cli/pull/19029) +- fix: use discoverOAuthFromWWWAuthenticate for reactive OAuth flow (#18760) by + @maximus12793 in + [#19038](https://github.com/google-gemini/gemini-cli/pull/19038) +- Search updates by @alisa-alisa in + [#19482](https://github.com/google-gemini/gemini-cli/pull/19482) +- feat(cli): add support for numpad SS3 sequences by @scidomino in + [#19659](https://github.com/google-gemini/gemini-cli/pull/19659) +- feat(cli): enhance folder trust with configuration discovery and security + warnings by @galz10 in + [#19492](https://github.com/google-gemini/gemini-cli/pull/19492) +- feat(ui): improve startup warnings UX with dismissal and show-count limits by + @spencer426 in + [#19584](https://github.com/google-gemini/gemini-cli/pull/19584) +- feat(a2a): Add API key authentication provider by @adamfweidman in + [#19548](https://github.com/google-gemini/gemini-cli/pull/19548) +- Send accepted/removed lines with ACCEPT_FILE telemetry. by @gundermanc in + [#19670](https://github.com/google-gemini/gemini-cli/pull/19670) +- feat(models): support Gemini 3.1 Pro Preview and fixes by @sehoon38 in + [#19676](https://github.com/google-gemini/gemini-cli/pull/19676) +- feat(plan): enforce read-only constraints in Plan Mode by @mattKorwel in + [#19433](https://github.com/google-gemini/gemini-cli/pull/19433) +- fix(cli): allow perfect match @scripts/test-windows-paths.js completions to + submit on Enter by @spencer426 in + [#19562](https://github.com/google-gemini/gemini-cli/pull/19562) +- fix(core): treat 503 Service Unavailable as retryable quota error by @sehoon38 + in [#19642](https://github.com/google-gemini/gemini-cli/pull/19642) +- Update sidebar.json for to allow top nav tabs. by @g-samroberts in + [#19595](https://github.com/google-gemini/gemini-cli/pull/19595) +- security: strip deceptive Unicode characters from terminal output by @ehedlund + in [#19026](https://github.com/google-gemini/gemini-cli/pull/19026) +- Fixes 'input.on' is not a function error in Gemini CLI by @gundermanc in + [#19691](https://github.com/google-gemini/gemini-cli/pull/19691) +- Revert "feat(ui): add source indicators to slash commands" by @ehedlund in + [#19695](https://github.com/google-gemini/gemini-cli/pull/19695) +- security: implement deceptive URL detection and disclosure in tool + confirmations by @ehedlund in + [#19288](https://github.com/google-gemini/gemini-cli/pull/19288) +- fix(core): restore auth consent in headless mode and add unit tests by + @ehedlund in [#19689](https://github.com/google-gemini/gemini-cli/pull/19689) +- Fix unsafe assertions in code_assist folder. by @gundermanc in + [#19706](https://github.com/google-gemini/gemini-cli/pull/19706) +- feat(cli): make JetBrains warning more specific by @jacob314 in + [#19687](https://github.com/google-gemini/gemini-cli/pull/19687) +- fix(cli): extensions dialog UX polish by @jacob314 in + [#19685](https://github.com/google-gemini/gemini-cli/pull/19685) +- fix(cli): use getDisplayString for manual model selection in dialog by + @sehoon38 in [#19726](https://github.com/google-gemini/gemini-cli/pull/19726) +- feat(policy): repurpose "Always Allow" persistence to workspace level by + @Abhijit-2592 in + [#19707](https://github.com/google-gemini/gemini-cli/pull/19707) +- fix(cli): re-enable CLI banner by @sehoon38 in + [#19741](https://github.com/google-gemini/gemini-cli/pull/19741) +- Disallow and suppress unsafe assignment by @gundermanc in + [#19736](https://github.com/google-gemini/gemini-cli/pull/19736) +- feat(core): migrate read_file to 1-based start_line/end_line parameters by + @adamfweidman in + [#19526](https://github.com/google-gemini/gemini-cli/pull/19526) +- feat(cli): improve CTRL+O experience for both standard and alternate screen + buffer (ASB) modes by @jwhelangoog in + [#19010](https://github.com/google-gemini/gemini-cli/pull/19010) +- Utilize pipelining of grep_search -> read_file to eliminate turns by + @gundermanc in + [#19574](https://github.com/google-gemini/gemini-cli/pull/19574) +- refactor(core): remove unsafe type assertions in error utils (Phase 1.1) 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 + [#19750](https://github.com/google-gemini/gemini-cli/pull/19750) +- Disallow unsafe returns. by @gundermanc in + [#19767](https://github.com/google-gemini/gemini-cli/pull/19767) +- fix(cli): filter subagent sessions from resume history by @abhipatel12 in + [#19698](https://github.com/google-gemini/gemini-cli/pull/19698) +- chore(lint): fix lint errors seen when running npm run lint by @abhipatel12 in + [#19844](https://github.com/google-gemini/gemini-cli/pull/19844) +- feat(core): remove unnecessary login verbiage from Code Assist auth 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 + [#19861](https://github.com/google-gemini/gemini-cli/pull/19861) +- fix(plan): time share by approval mode dashboard reporting negative time + shares by @Adib234 in + [#19847](https://github.com/google-gemini/gemini-cli/pull/19847) +- fix(core): allow any preview model in quota access check by @bdmorgan in + [#19867](https://github.com/google-gemini/gemini-cli/pull/19867) +- fix(core): prevent omission placeholder deletions in replace/write_file by + @nsalerni in [#19870](https://github.com/google-gemini/gemini-cli/pull/19870) +- fix(core): add uniqueness guard to edit tool by @Shivangisharma4 in + [#19890](https://github.com/google-gemini/gemini-cli/pull/19890) +- refactor(config): remove enablePromptCompletion from settings by @sehoon38 in + [#19974](https://github.com/google-gemini/gemini-cli/pull/19974) +- refactor(core): move session conversion logic to core by @abhipatel12 in + [#19972](https://github.com/google-gemini/gemini-cli/pull/19972) +- Fix: Persist manual model selection on restart #19864 by @Nixxx19 in + [#19891](https://github.com/google-gemini/gemini-cli/pull/19891) +- fix(core): increase default retry attempts and add quota error backoff by + @sehoon38 in [#19949](https://github.com/google-gemini/gemini-cli/pull/19949) +- feat(core): add policy chain support for Gemini 3.1 by @sehoon38 in + [#19991](https://github.com/google-gemini/gemini-cli/pull/19991) +- Updates command reference and /stats command. by @g-samroberts in + [#19794](https://github.com/google-gemini/gemini-cli/pull/19794) +- Fix for silent failures in non-interactive mode by @owenofbrien in + [#19905](https://github.com/google-gemini/gemini-cli/pull/19905) +- fix(plan): allow plan mode writes on Windows and fix prompt paths by @Adib234 + in [#19658](https://github.com/google-gemini/gemini-cli/pull/19658) +- fix(core): prevent OAuth server crash on unexpected requests by @reyyanxahmed + in [#19668](https://github.com/google-gemini/gemini-cli/pull/19668) +- feat: Map tool kinds to explicit ACP.ToolKind values and update test … by + @sripasg in [#19547](https://github.com/google-gemini/gemini-cli/pull/19547) +- chore: restrict gemini-automted-issue-triage to only allow echo by @galz10 in + [#20047](https://github.com/google-gemini/gemini-cli/pull/20047) +- Allow ask headers longer than 16 chars by @scidomino in + [#20041](https://github.com/google-gemini/gemini-cli/pull/20041) +- fix(core): prevent state corruption in McpClientManager during collis by @h30s + in [#19782](https://github.com/google-gemini/gemini-cli/pull/19782) +- fix(bundling): copy devtools package to bundle for runtime resolution 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 + [#19766](https://github.com/google-gemini/gemini-cli/pull/19766) +- feat(policy): Support MCP Server Wildcards in Policy Engine by @jerop in + [#20024](https://github.com/google-gemini/gemini-cli/pull/20024) +- docs(CONTRIBUTING): update React DevTools version to 6 by @mmgok in + [#20014](https://github.com/google-gemini/gemini-cli/pull/20014) +- feat(core): optimize tool descriptions and schemas for Gemini 3 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) + [#19643](https://github.com/google-gemini/gemini-cli/pull/19643) +- feat(core): implement experimental direct web fetch by @mbleigh in + [#19557](https://github.com/google-gemini/gemini-cli/pull/19557) +- feat(core): replace expected_replacements with allow_multiple in replace tool + by @SandyTao520 in + [#20033](https://github.com/google-gemini/gemini-cli/pull/20033) +- fix(sandbox): harden image packaging integrity checks by @aviralgarg05 in + [#19552](https://github.com/google-gemini/gemini-cli/pull/19552) +- fix(core): allow environment variable expansion and explicit overrides for MCP + servers by @galz10 in + [#18837](https://github.com/google-gemini/gemini-cli/pull/18837) +- feat(policy): Implement Tool Annotation Matching in Policy Engine by @jerop in + [#20029](https://github.com/google-gemini/gemini-cli/pull/20029) +- fix(core): prevent utility calls from changing session active model by + @adamfweidman in + [#20035](https://github.com/google-gemini/gemini-cli/pull/20035) +- fix(cli): skip workspace policy loading when in home directory by + @Abhijit-2592 in + [#20054](https://github.com/google-gemini/gemini-cli/pull/20054) +- fix(scripts): Add Windows (win32/x64) support to lint.js by @ZafeerMahmood in + [#16193](https://github.com/google-gemini/gemini-cli/pull/16193) +- fix(a2a-server): Remove unsafe type assertions in agent by @Nixxx19 in + [#19723](https://github.com/google-gemini/gemini-cli/pull/19723) +- Fix: Handle corrupted token file gracefully when switching auth types (#19845) + by @Nixxx19 in + [#19850](https://github.com/google-gemini/gemini-cli/pull/19850) +- fix critical dep vulnerability by @scidomino in + [#20087](https://github.com/google-gemini/gemini-cli/pull/20087) +- Add new setting to configure maxRetries by @kevinjwang1 in + [#20064](https://github.com/google-gemini/gemini-cli/pull/20064) +- Stabilize tests. by @gundermanc in + [#20095](https://github.com/google-gemini/gemini-cli/pull/20095) +- make windows tests mandatory by @scidomino in + [#20096](https://github.com/google-gemini/gemini-cli/pull/20096) +- Add 3.1 pro preview to behavioral evals. by @gundermanc in + [#20088](https://github.com/google-gemini/gemini-cli/pull/20088) +- feat:PR-rate-limit by @JagjeevanAK in + [#19804](https://github.com/google-gemini/gemini-cli/pull/19804) +- feat(cli): allow expanding full details of MCP tool on approval by @y-okt in + [#19916](https://github.com/google-gemini/gemini-cli/pull/19916) +- feat(security): Introduce Conseca framework by @shrishabh in + [#13193](https://github.com/google-gemini/gemini-cli/pull/13193) +- fix(cli): Remove unsafe type assertions in activityLogger #19713 by @Nixxx19 + in [#19745](https://github.com/google-gemini/gemini-cli/pull/19745) +- feat: implement AfterTool tail tool calls by @googlestrobe in + [#18486](https://github.com/google-gemini/gemini-cli/pull/18486) +- ci(actions): fix PR rate limiter excluding maintainers by @scidomino in + [#20117](https://github.com/google-gemini/gemini-cli/pull/20117) +- Shortcuts: Move SectionHeader title below top line and refine styling by + @keithguerin in + [#18721](https://github.com/google-gemini/gemini-cli/pull/18721) +- refactor(ui): Update and simplify use of gray colors in themes by @keithguerin + in [#20141](https://github.com/google-gemini/gemini-cli/pull/20141) +- fix punycode2 by @jacob314 in + [#20154](https://github.com/google-gemini/gemini-cli/pull/20154) +- feat(ide): add GEMINI_CLI_IDE_PID env var to override IDE process detection by + @kiryltech in [#15842](https://github.com/google-gemini/gemini-cli/pull/15842) +- feat(policy): Propagate Tool Annotations for MCP Servers by @jerop in + [#20083](https://github.com/google-gemini/gemini-cli/pull/20083) +- fix(a2a-server): pass allowedTools settings to core Config by @reyyanxahmed in + [#19680](https://github.com/google-gemini/gemini-cli/pull/19680) +- feat(mcp): add progress bar, throttling, and input validation for MCP tool + progress by @jasmeetsb in + [#19772](https://github.com/google-gemini/gemini-cli/pull/19772) +- feat(policy): centralize plan mode tool visibility in policy engine by @jerop + in [#20178](https://github.com/google-gemini/gemini-cli/pull/20178) +- feat(browser): implement experimental browser agent by @gsquared94 in + [#19284](https://github.com/google-gemini/gemini-cli/pull/19284) +- feat(plan): summarize work after executing a plan by @jerop in + [#19432](https://github.com/google-gemini/gemini-cli/pull/19432) +- fix(core): create new McpClient on restart to apply updated config by @h30s in + [#20126](https://github.com/google-gemini/gemini-cli/pull/20126) +- Changelog for v0.30.0-preview.5 by @gemini-cli-robot in + [#20107](https://github.com/google-gemini/gemini-cli/pull/20107) +- Update packages. by @jacob314 in + [#20152](https://github.com/google-gemini/gemini-cli/pull/20152) +- Fix extension env dir loading issue by @chrstnb in + [#20198](https://github.com/google-gemini/gemini-cli/pull/20198) +- restrict /assign to help-wanted issues by @scidomino in + [#20207](https://github.com/google-gemini/gemini-cli/pull/20207) +- feat(plan): inject message when user manually exits Plan mode by @jerop in + [#20203](https://github.com/google-gemini/gemini-cli/pull/20203) +- feat(extensions): enforce folder trust for local extension install by @galz10 + in [#19703](https://github.com/google-gemini/gemini-cli/pull/19703) +- feat(hooks): adds support for RuntimeHook functions. by @mbleigh in + [#19598](https://github.com/google-gemini/gemini-cli/pull/19598) +- Docs: Update UI links. by @jkcinouye in + [#20224](https://github.com/google-gemini/gemini-cli/pull/20224) +- feat: prompt users to run /terminal-setup with yes/no by @ishaanxgupta in + [#16235](https://github.com/google-gemini/gemini-cli/pull/16235) +- fix: additional high vulnerabilities (minimatch, cross-spawn) by @adamfweidman + in [#20221](https://github.com/google-gemini/gemini-cli/pull/20221) +- feat(telemetry): Add context breakdown to API response event by @SandyTao520 + in [#19699](https://github.com/google-gemini/gemini-cli/pull/19699) +- Docs: Add nested sub-folders for related topics by @g-samroberts in + [#20235](https://github.com/google-gemini/gemini-cli/pull/20235) +- feat(plan): support automatic model switching for Plan Mode by @jerop in + [#20240](https://github.com/google-gemini/gemini-cli/pull/20240) -**Full changelog**: -https://github.com/google-gemini/gemini-cli/compare/v0.29.0-preview.5...v0.30.0-preview.5 +**Full Changelog**: +https://github.com/google-gemini/gemini-cli/compare/v0.30.0-preview.6...v0.31.0-preview.0 diff --git a/docs/cli/settings.md b/docs/cli/settings.md index 51dee564dc..8adccba6ae 100644 --- a/docs/cli/settings.md +++ b/docs/cli/settings.md @@ -82,12 +82,13 @@ they appear in the UI. ### Model -| UI Label | Setting | Description | Default | -| ----------------------- | ---------------------------- | -------------------------------------------------------------------------------------- | ------- | -| Max Session Turns | `model.maxSessionTurns` | Maximum number of user/model/tool turns to keep in a session. -1 means unlimited. | `-1` | -| Compression Threshold | `model.compressionThreshold` | The fraction of context usage at which to trigger context compression (e.g. 0.2, 0.3). | `0.5` | -| Disable Loop Detection | `model.disableLoopDetection` | Disable automatic detection and prevention of infinite loops. | `false` | -| Skip Next Speaker Check | `model.skipNextSpeakerCheck` | Skip the next speaker check. | `true` | +| UI Label | Setting | Description | Default | +| ----------------------- | ---------------------------- | -------------------------------------------------------------------------------------- | ----------- | +| Model | `model.name` | The Gemini model to use for conversations. | `undefined` | +| Max Session Turns | `model.maxSessionTurns` | Maximum number of user/model/tool turns to keep in a session. -1 means unlimited. | `-1` | +| Compression Threshold | `model.compressionThreshold` | The fraction of context usage at which to trigger context compression (e.g. 0.2, 0.3). | `0.5` | +| Disable Loop Detection | `model.disableLoopDetection` | Disable automatic detection and prevention of infinite loops. | `false` | +| Skip Next Speaker Check | `model.skipNextSpeakerCheck` | Skip the next speaker check. | `true` | ### Context diff --git a/docs/resources/uninstall.md b/docs/resources/uninstall.md index e96ddc5acf..1f5303e37f 100644 --- a/docs/resources/uninstall.md +++ b/docs/resources/uninstall.md @@ -19,16 +19,7 @@ can find your npm cache path by running `npm config get cache`. rm -rf "$(npm config get cache)/_npx" ``` -**For Windows** - -_Command Prompt_ - -```cmd -:: The path is typically %LocalAppData%\npm-cache\_npx -rmdir /s /q "%LocalAppData%\npm-cache\_npx" -``` - -_PowerShell_ +**For Windows (PowerShell)** ```powershell # The path is typically $env:LocalAppData\npm-cache\_npx diff --git a/evals/grep_search_functionality.eval.ts b/evals/grep_search_functionality.eval.ts index 77df3b950f..f1224b8221 100644 --- a/evals/grep_search_functionality.eval.ts +++ b/evals/grep_search_functionality.eval.ts @@ -93,7 +93,7 @@ describe('grep_search_functionality', () => { }); evalTest('USUALLY_PASSES', { - name: 'should search only within the specified include glob', + name: 'should search only within the specified include_pattern glob', files: { 'file.js': 'my_function();', 'file.ts': 'my_function();', @@ -105,19 +105,19 @@ describe('grep_search_functionality', () => { undefined, (args) => { const params = JSON.parse(args); - return params.include === '*.js'; + return params.include_pattern === '*.js'; }, ); expect( wasToolCalled, - 'Expected grep_search to be called with include: "*.js"', + 'Expected grep_search to be called with include_pattern: "*.js"', ).toBe(true); assertModelHasOutput(result); checkModelOutputContent(result, { expectedContent: [/file.js/], forbiddenContent: [/file.ts/], - testName: `${TEST_PREFIX}include glob search`, + testName: `${TEST_PREFIX}include_pattern glob search`, }); }, }); diff --git a/integration-tests/ripgrep-real.test.ts b/integration-tests/ripgrep-real.test.ts index 3ac8a0f16e..60f99c8a84 100644 --- a/integration-tests/ripgrep-real.test.ts +++ b/integration-tests/ripgrep-real.test.ts @@ -102,7 +102,10 @@ describe('ripgrep-real-direct', () => { 'console.log("hello");\n', ); - const invocation = tool.build({ pattern: 'hello', include: '*.js' }); + const invocation = tool.build({ + pattern: 'hello', + include_pattern: '*.js', + }); const result = await invocation.execute(new AbortController().signal); expect(result.llmContent).toContain('Found 1 match'); diff --git a/packages/cli/src/commands/extensions/link.test.ts b/packages/cli/src/commands/extensions/link.test.ts index bdd8c06b49..67351a5456 100644 --- a/packages/cli/src/commands/extensions/link.test.ts +++ b/packages/cli/src/commands/extensions/link.test.ts @@ -13,34 +13,21 @@ import { afterEach, type Mock, } from 'vitest'; -import { format } from 'node:util'; +import { coreEvents } from '@google/gemini-cli-core'; import { type Argv } from 'yargs'; import { handleLink, linkCommand } from './link.js'; import { ExtensionManager } from '../../config/extension-manager.js'; import { loadSettings, type LoadedSettings } from '../../config/settings.js'; import { getErrorMessage } from '../../utils/errors.js'; -// Mock dependencies -const emitConsoleLog = vi.hoisted(() => vi.fn()); -const debugLogger = vi.hoisted(() => ({ - log: vi.fn((message, ...args) => { - emitConsoleLog('log', format(message, ...args)); - }), - error: vi.fn((message, ...args) => { - emitConsoleLog('error', format(message, ...args)); - }), -})); - vi.mock('@google/gemini-cli-core', async (importOriginal) => { - const actual = - await importOriginal(); - return { - ...actual, - coreEvents: { - emitConsoleLog, - }, - debugLogger, - }; + const { mockCoreDebugLogger } = await import( + '../../test-utils/mockDebugLogger.js' + ); + return mockCoreDebugLogger( + await importOriginal(), + { stripAnsi: true }, + ); }); vi.mock('../../config/extension-manager.js'); @@ -95,7 +82,7 @@ describe('extensions link command', () => { source: '/local/path/to/extension', type: 'link', }); - expect(emitConsoleLog).toHaveBeenCalledWith( + expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'log', 'Extension "my-linked-extension" linked successfully and enabled.', ); @@ -116,7 +103,7 @@ describe('extensions link command', () => { await handleLink({ path: '/local/path/to/extension' }); - expect(emitConsoleLog).toHaveBeenCalledWith( + expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'error', 'Link failed message', ); diff --git a/packages/cli/src/commands/extensions/list.test.ts b/packages/cli/src/commands/extensions/list.test.ts index 6967719be8..f0f0168f79 100644 --- a/packages/cli/src/commands/extensions/list.test.ts +++ b/packages/cli/src/commands/extensions/list.test.ts @@ -5,33 +5,22 @@ */ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { format } from 'node:util'; +import { coreEvents } from '@google/gemini-cli-core'; import { handleList, listCommand } from './list.js'; import { ExtensionManager } from '../../config/extension-manager.js'; import { loadSettings, type LoadedSettings } from '../../config/settings.js'; import { getErrorMessage } from '../../utils/errors.js'; -// Mock dependencies -const emitConsoleLog = vi.hoisted(() => vi.fn()); -const debugLogger = vi.hoisted(() => ({ - log: vi.fn((message, ...args) => { - emitConsoleLog('log', format(message, ...args)); - }), - error: vi.fn((message, ...args) => { - emitConsoleLog('error', format(message, ...args)); - }), -})); - vi.mock('@google/gemini-cli-core', async (importOriginal) => { - const actual = - await importOriginal(); - return { - ...actual, - coreEvents: { - emitConsoleLog, + const { mockCoreDebugLogger } = await import( + '../../test-utils/mockDebugLogger.js' + ); + return mockCoreDebugLogger( + await importOriginal(), + { + stripAnsi: false, }, - debugLogger, - }; + ); }); vi.mock('../../config/extension-manager.js'); @@ -71,7 +60,7 @@ describe('extensions list command', () => { .mockResolvedValue([]); await handleList(); - expect(emitConsoleLog).toHaveBeenCalledWith( + expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'log', 'No extensions installed.', ); @@ -85,7 +74,7 @@ describe('extensions list command', () => { .mockResolvedValue([]); await handleList({ outputFormat: 'json' }); - expect(emitConsoleLog).toHaveBeenCalledWith('log', '[]'); + expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith('log', '[]'); mockCwd.mockRestore(); }); @@ -103,7 +92,7 @@ describe('extensions list command', () => { ); await handleList(); - expect(emitConsoleLog).toHaveBeenCalledWith( + expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'log', 'ext1@1.0.0\n\next2@2.0.0', ); @@ -121,7 +110,7 @@ describe('extensions list command', () => { .mockResolvedValue(extensions); await handleList({ outputFormat: 'json' }); - expect(emitConsoleLog).toHaveBeenCalledWith( + expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'log', JSON.stringify(extensions, null, 2), ); @@ -142,7 +131,7 @@ describe('extensions list command', () => { await handleList(); - expect(emitConsoleLog).toHaveBeenCalledWith( + expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'error', 'List failed message', ); diff --git a/packages/cli/src/commands/skills/disable.test.ts b/packages/cli/src/commands/skills/disable.test.ts index 4a5097471b..531a08d21b 100644 --- a/packages/cli/src/commands/skills/disable.test.ts +++ b/packages/cli/src/commands/skills/disable.test.ts @@ -5,7 +5,6 @@ */ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { format } from 'node:util'; import { handleDisable, disableCommand } from './disable.js'; import { loadSettings, @@ -14,12 +13,12 @@ import { type LoadableSettingScope, } from '../../config/settings.js'; -const emitConsoleLog = vi.hoisted(() => vi.fn()); -const debugLogger = vi.hoisted(() => ({ - log: vi.fn((message, ...args) => { - emitConsoleLog('log', format(message, ...args)); - }), -})); +const { emitConsoleLog, debugLogger } = await vi.hoisted(async () => { + const { createMockDebugLogger } = await import( + '../../test-utils/mockDebugLogger.js' + ); + return createMockDebugLogger({ stripAnsi: true }); +}); vi.mock('@google/gemini-cli-core', async (importOriginal) => { const actual = diff --git a/packages/cli/src/commands/skills/enable.test.ts b/packages/cli/src/commands/skills/enable.test.ts index e204da2f66..d34737d2df 100644 --- a/packages/cli/src/commands/skills/enable.test.ts +++ b/packages/cli/src/commands/skills/enable.test.ts @@ -5,7 +5,6 @@ */ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { format } from 'node:util'; import { handleEnable, enableCommand } from './enable.js'; import { loadSettings, @@ -13,12 +12,12 @@ import { type LoadedSettings, } from '../../config/settings.js'; -const emitConsoleLog = vi.hoisted(() => vi.fn()); -const debugLogger = vi.hoisted(() => ({ - log: vi.fn((message, ...args) => { - emitConsoleLog('log', format(message, ...args)); - }), -})); +const { emitConsoleLog, debugLogger } = await vi.hoisted(async () => { + const { createMockDebugLogger } = await import( + '../../test-utils/mockDebugLogger.js' + ); + return createMockDebugLogger({ stripAnsi: true }); +}); vi.mock('@google/gemini-cli-core', async (importOriginal) => { const actual = diff --git a/packages/cli/src/commands/skills/install.test.ts b/packages/cli/src/commands/skills/install.test.ts index 9fd05affcd..faaa7f31c6 100644 --- a/packages/cli/src/commands/skills/install.test.ts +++ b/packages/cli/src/commands/skills/install.test.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { vi, describe, it, expect, beforeEach } from 'vitest'; const mockInstallSkill = vi.hoisted(() => vi.fn()); const mockRequestConsentNonInteractive = vi.hoisted(() => vi.fn()); @@ -19,11 +19,17 @@ vi.mock('../../config/extensions/consent.js', () => ({ skillsConsentString: mockSkillsConsentString, })); +const { debugLogger, emitConsoleLog } = await vi.hoisted(async () => { + const { createMockDebugLogger } = await import( + '../../test-utils/mockDebugLogger.js' + ); + return createMockDebugLogger({ stripAnsi: true }); +}); + vi.mock('@google/gemini-cli-core', () => ({ - debugLogger: { log: vi.fn(), error: vi.fn() }, + debugLogger, })); -import { debugLogger } from '@google/gemini-cli-core'; import { handleInstall, installCommand } from './install.js'; describe('skill install command', () => { @@ -63,10 +69,12 @@ describe('skill install command', () => { expect.any(Function), expect.any(Function), ); - expect(debugLogger.log).toHaveBeenCalledWith( + expect(emitConsoleLog).toHaveBeenCalledWith( + 'log', expect.stringContaining('Successfully installed skill: test-skill'), ); - expect(debugLogger.log).toHaveBeenCalledWith( + expect(emitConsoleLog).toHaveBeenCalledWith( + 'log', expect.stringContaining('location: /mock/user/skills/test-skill'), ); expect(mockRequestConsentNonInteractive).toHaveBeenCalledWith( @@ -86,10 +94,11 @@ describe('skill install command', () => { }); expect(mockRequestConsentNonInteractive).not.toHaveBeenCalled(); - expect(debugLogger.log).toHaveBeenCalledWith( + expect(emitConsoleLog).toHaveBeenCalledWith( + 'log', 'You have consented to the following:', ); - expect(debugLogger.log).toHaveBeenCalledWith('Mock Consent String'); + expect(emitConsoleLog).toHaveBeenCalledWith('log', 'Mock Consent String'); expect(mockInstallSkill).toHaveBeenCalled(); }); @@ -106,7 +115,8 @@ describe('skill install command', () => { source: 'https://example.com/repo.git', }); - expect(debugLogger.error).toHaveBeenCalledWith( + expect(emitConsoleLog).toHaveBeenCalledWith( + 'error', 'Skill installation cancelled by user.', ); expect(process.exit).toHaveBeenCalledWith(1); @@ -137,7 +147,7 @@ describe('skill install command', () => { await handleInstall({ source: '/local/path' }); - expect(debugLogger.error).toHaveBeenCalledWith('Install failed'); + expect(emitConsoleLog).toHaveBeenCalledWith('error', 'Install failed'); expect(process.exit).toHaveBeenCalledWith(1); }); }); diff --git a/packages/cli/src/commands/skills/link.test.ts b/packages/cli/src/commands/skills/link.test.ts index 404c1d9f66..24c3d3ff64 100644 --- a/packages/cli/src/commands/skills/link.test.ts +++ b/packages/cli/src/commands/skills/link.test.ts @@ -15,8 +15,15 @@ vi.mock('../../utils/skillUtils.js', () => ({ linkSkill: mockLinkSkill, })); +const { debugLogger } = await vi.hoisted(async () => { + const { createMockDebugLogger } = await import( + '../../test-utils/mockDebugLogger.js' + ); + return createMockDebugLogger({ stripAnsi: false }); +}); + vi.mock('@google/gemini-cli-core', () => ({ - debugLogger: { log: vi.fn(), error: vi.fn() }, + debugLogger, })); vi.mock('../../config/extensions/consent.js', () => ({ @@ -24,8 +31,6 @@ vi.mock('../../config/extensions/consent.js', () => ({ skillsConsentString: mockSkillsConsentString, })); -import { debugLogger } from '@google/gemini-cli-core'; - describe('skills link command', () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/packages/cli/src/commands/skills/list.test.ts b/packages/cli/src/commands/skills/list.test.ts index e7e25a2736..c330af75ba 100644 --- a/packages/cli/src/commands/skills/list.test.ts +++ b/packages/cli/src/commands/skills/list.test.ts @@ -5,33 +5,23 @@ */ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { format } from 'node:util'; +import { coreEvents } from '@google/gemini-cli-core'; import { handleList, listCommand } from './list.js'; import { loadSettings, type LoadedSettings } from '../../config/settings.js'; import { loadCliConfig } from '../../config/config.js'; import type { Config } from '@google/gemini-cli-core'; import chalk from 'chalk'; -const emitConsoleLog = vi.hoisted(() => vi.fn()); -const debugLogger = vi.hoisted(() => ({ - log: vi.fn((message, ...args) => { - emitConsoleLog('log', format(message, ...args)); - }), - error: vi.fn((message, ...args) => { - emitConsoleLog('error', format(message, ...args)); - }), -})); - vi.mock('@google/gemini-cli-core', async (importOriginal) => { - const actual = - await importOriginal(); - return { - ...actual, - coreEvents: { - emitConsoleLog, + const { mockCoreDebugLogger } = await import( + '../../test-utils/mockDebugLogger.js' + ); + return mockCoreDebugLogger( + await importOriginal(), + { + stripAnsi: false, }, - debugLogger, - }; + ); }); vi.mock('../../config/settings.js'); @@ -67,7 +57,7 @@ describe('skills list command', () => { await handleList({}); - expect(emitConsoleLog).toHaveBeenCalledWith( + expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'log', 'No skills discovered.', ); @@ -98,23 +88,23 @@ describe('skills list command', () => { await handleList({}); - expect(emitConsoleLog).toHaveBeenCalledWith( + expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'log', chalk.bold('Discovered Agent Skills:'), ); - expect(emitConsoleLog).toHaveBeenCalledWith( + expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'log', expect.stringContaining('skill1'), ); - expect(emitConsoleLog).toHaveBeenCalledWith( + expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'log', expect.stringContaining(chalk.green('[Enabled]')), ); - expect(emitConsoleLog).toHaveBeenCalledWith( + expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'log', expect.stringContaining('skill2'), ); - expect(emitConsoleLog).toHaveBeenCalledWith( + expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'log', expect.stringContaining(chalk.red('[Disabled]')), ); @@ -146,11 +136,11 @@ describe('skills list command', () => { // Default await handleList({ all: false }); - expect(emitConsoleLog).toHaveBeenCalledWith( + expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'log', expect.stringContaining('regular'), ); - expect(emitConsoleLog).not.toHaveBeenCalledWith( + expect(coreEvents.emitConsoleLog).not.toHaveBeenCalledWith( 'log', expect.stringContaining('builtin'), ); @@ -159,15 +149,15 @@ describe('skills list command', () => { // With all: true await handleList({ all: true }); - expect(emitConsoleLog).toHaveBeenCalledWith( + expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'log', expect.stringContaining('regular'), ); - expect(emitConsoleLog).toHaveBeenCalledWith( + expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'log', expect.stringContaining('builtin'), ); - expect(emitConsoleLog).toHaveBeenCalledWith( + expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith( 'log', expect.stringContaining(chalk.gray(' [Built-in]')), ); diff --git a/packages/cli/src/commands/skills/uninstall.test.ts b/packages/cli/src/commands/skills/uninstall.test.ts index 74f1730590..ab51db5b53 100644 --- a/packages/cli/src/commands/skills/uninstall.test.ts +++ b/packages/cli/src/commands/skills/uninstall.test.ts @@ -12,11 +12,17 @@ vi.mock('../../utils/skillUtils.js', () => ({ uninstallSkill: mockUninstallSkill, })); +const { debugLogger, emitConsoleLog } = await vi.hoisted(async () => { + const { createMockDebugLogger } = await import( + '../../test-utils/mockDebugLogger.js' + ); + return createMockDebugLogger({ stripAnsi: true }); +}); + vi.mock('@google/gemini-cli-core', () => ({ - debugLogger: { log: vi.fn(), error: vi.fn() }, + debugLogger, })); -import { debugLogger } from '@google/gemini-cli-core'; import { handleUninstall, uninstallCommand } from './uninstall.js'; describe('skill uninstall command', () => { @@ -45,10 +51,12 @@ describe('skill uninstall command', () => { }); expect(mockUninstallSkill).toHaveBeenCalledWith('test-skill', 'user'); - expect(debugLogger.log).toHaveBeenCalledWith( + expect(emitConsoleLog).toHaveBeenCalledWith( + 'log', expect.stringContaining('Successfully uninstalled skill: test-skill'), ); - expect(debugLogger.log).toHaveBeenCalledWith( + expect(emitConsoleLog).toHaveBeenCalledWith( + 'log', expect.stringContaining('location: /mock/user/skills/test-skill'), ); }); @@ -71,7 +79,8 @@ describe('skill uninstall command', () => { await handleUninstall({ name: 'test-skill' }); - expect(debugLogger.error).toHaveBeenCalledWith( + expect(emitConsoleLog).toHaveBeenCalledWith( + 'error', 'Skill "test-skill" is not installed in the user scope.', ); }); @@ -81,7 +90,7 @@ describe('skill uninstall command', () => { await handleUninstall({ name: 'test-skill' }); - expect(debugLogger.error).toHaveBeenCalledWith('Uninstall failed'); + expect(emitConsoleLog).toHaveBeenCalledWith('error', 'Uninstall failed'); expect(process.exit).toHaveBeenCalledWith(1); }); }); diff --git a/packages/cli/src/config/extensions/__snapshots__/consent-consent-maybeRequestConsentOrFail-consent-string-generation-should-generate-a-consent-string-with-all-fields.snap.svg b/packages/cli/src/config/extensions/__snapshots__/consent-consent-maybeRequestConsentOrFail-consent-string-generation-should-generate-a-consent-string-with-all-fields.snap.svg new file mode 100644 index 0000000000..d42af4490c --- /dev/null +++ b/packages/cli/src/config/extensions/__snapshots__/consent-consent-maybeRequestConsentOrFail-consent-string-generation-should-generate-a-consent-string-with-all-fields.snap.svg @@ -0,0 +1,18 @@ + + + + + Installing extension "test-ext". + This extension will run the following MCP servers: + * server1 (local): npm start + * server2 (remote): https://remote.com + This extension will append info to your gemini.md context using my-context.md + This extension will exclude the following core tools: tool1,tool2 + The extension you are about to install may have been created by a third-party developer and sourced + from a public repository. Google does not vet, endorse, or guarantee the functionality or security + of extensions. Please carefully inspect any extension and its source code before installing to + understand the permissions it requires and the actions it may perform. + + \ No newline at end of file diff --git a/packages/cli/src/config/extensions/__snapshots__/consent-consent-maybeRequestConsentOrFail-consent-string-generation-should-include-warning-when-hooks-are-present.snap.svg b/packages/cli/src/config/extensions/__snapshots__/consent-consent-maybeRequestConsentOrFail-consent-string-generation-should-include-warning-when-hooks-are-present.snap.svg new file mode 100644 index 0000000000..9f4866dbdd --- /dev/null +++ b/packages/cli/src/config/extensions/__snapshots__/consent-consent-maybeRequestConsentOrFail-consent-string-generation-should-include-warning-when-hooks-are-present.snap.svg @@ -0,0 +1,14 @@ + + + + + Installing extension "test-ext". + ⚠️ This extension contains Hooks which can automatically execute commands. + The extension you are about to install may have been created by a third-party developer and sourced + from a public repository. Google does not vet, endorse, or guarantee the functionality or security + of extensions. Please carefully inspect any extension and its source code before installing to + understand the permissions it requires and the actions it may perform. + + \ No newline at end of file diff --git a/packages/cli/src/config/extensions/__snapshots__/consent-consent-maybeRequestConsentOrFail-consent-string-generation-should-request-consent-if-skills-change.snap.svg b/packages/cli/src/config/extensions/__snapshots__/consent-consent-maybeRequestConsentOrFail-consent-string-generation-should-request-consent-if-skills-change.snap.svg new file mode 100644 index 0000000000..6f5879df4c --- /dev/null +++ b/packages/cli/src/config/extensions/__snapshots__/consent-consent-maybeRequestConsentOrFail-consent-string-generation-should-request-consent-if-skills-change.snap.svg @@ -0,0 +1,28 @@ + + + + + Installing extension "test-ext". + This extension will run the following MCP servers: + * server1 (local): npm start + * server2 (remote): https://remote.com + This extension will append info to your gemini.md context using my-context.md + This extension will exclude the following core tools: tool1,tool2 + Agent Skills: + This extension will install the following agent skills: + * skill1: desc1 + (Source: /mock/temp/dir/skill1/SKILL.md) (2 items in directory) + * skill2: desc2 + (Source: /mock/temp/dir/skill2/SKILL.md) (1 items in directory) + The extension you are about to install may have been created by a third-party developer and sourced + from a public repository. Google does not vet, endorse, or guarantee the functionality or security + of extensions. Please carefully inspect any extension and its source code before installing to + understand the permissions it requires and the actions it may perform. + Agent skills inject specialized instructions and domain-specific knowledge into the agent's system + prompt. This can change how the agent interprets your requests and interacts with your environment. + Review the skill definitions at the location(s) provided below to ensure they meet your security + standards. + + \ No newline at end of file diff --git a/packages/cli/src/config/extensions/__snapshots__/consent-consent-maybeRequestConsentOrFail-consent-string-generation-should-show-a-warning-if-the-skill-directory-cannot-be-read.snap.svg b/packages/cli/src/config/extensions/__snapshots__/consent-consent-maybeRequestConsentOrFail-consent-string-generation-should-show-a-warning-if-the-skill-directory-cannot-be-read.snap.svg new file mode 100644 index 0000000000..3fff32664a --- /dev/null +++ b/packages/cli/src/config/extensions/__snapshots__/consent-consent-maybeRequestConsentOrFail-consent-string-generation-should-show-a-warning-if-the-skill-directory-cannot-be-read.snap.svg @@ -0,0 +1,22 @@ + + + + + Installing extension "test-ext". + Agent Skills: + This extension will install the following agent skills: + * locked-skill: A skill in a locked dir + (Source: /mock/temp/dir/locked/SKILL.md) + ⚠️ (Could not count items in directory) + The extension you are about to install may have been created by a third-party developer and sourced + from a public repository. Google does not vet, endorse, or guarantee the functionality or security + of extensions. Please carefully inspect any extension and its source code before installing to + understand the permissions it requires and the actions it may perform. + Agent skills inject specialized instructions and domain-specific knowledge into the agent's system + prompt. This can change how the agent interprets your requests and interacts with your environment. + Review the skill definitions at the location(s) provided below to ensure they meet your security + standards. + + \ No newline at end of file diff --git a/packages/cli/src/config/extensions/__snapshots__/consent-consent-skillsConsentString-should-generate-a-consent-string-for-skills.snap.svg b/packages/cli/src/config/extensions/__snapshots__/consent-consent-skillsConsentString-should-generate-a-consent-string-for-skills.snap.svg new file mode 100644 index 0000000000..c52724836e --- /dev/null +++ b/packages/cli/src/config/extensions/__snapshots__/consent-consent-skillsConsentString-should-generate-a-consent-string-for-skills.snap.svg @@ -0,0 +1,17 @@ + + + + + Installing agent skill(s) from "https://example.com/repo.git". + The following agent skill(s) will be installing: + * skill1: desc1 + (Source: /mock/temp/dir/skill1/SKILL.md) (1 items in directory) + Install Destination: /mock/target/dir + Agent skills inject specialized instructions and domain-specific knowledge into the agent's system + prompt. This can change how the agent interprets your requests and interacts with your environment. + Review the skill definitions at the location(s) provided below to ensure they meet your security + standards. + + \ No newline at end of file diff --git a/packages/cli/src/config/extensions/__snapshots__/consent.test.ts.snap b/packages/cli/src/config/extensions/__snapshots__/consent.test.ts.snap new file mode 100644 index 0000000000..d8fe99d004 --- /dev/null +++ b/packages/cli/src/config/extensions/__snapshots__/consent.test.ts.snap @@ -0,0 +1,93 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`consent > maybeRequestConsentOrFail > consent string generation > should generate a consent string with all fields 1`] = ` +"Installing extension "test-ext". +This extension will run the following MCP servers: + * server1 (local): npm start + * server2 (remote): https://remote.com +This extension will append info to your gemini.md context using my-context.md +This extension will exclude the following core tools: tool1,tool2 + +The extension you are about to install may have been created by a third-party developer and sourced +from a public repository. Google does not vet, endorse, or guarantee the functionality or security +of extensions. Please carefully inspect any extension and its source code before installing to +understand the permissions it requires and the actions it may perform." +`; + +exports[`consent > maybeRequestConsentOrFail > consent string generation > should include warning when hooks are present 1`] = ` +"Installing extension "test-ext". +⚠️ This extension contains Hooks which can automatically execute commands. + +The extension you are about to install may have been created by a third-party developer and sourced +from a public repository. Google does not vet, endorse, or guarantee the functionality or security +of extensions. Please carefully inspect any extension and its source code before installing to +understand the permissions it requires and the actions it may perform." +`; + +exports[`consent > maybeRequestConsentOrFail > consent string generation > should request consent if skills change 1`] = ` +"Installing extension "test-ext". +This extension will run the following MCP servers: + * server1 (local): npm start + * server2 (remote): https://remote.com +This extension will append info to your gemini.md context using my-context.md +This extension will exclude the following core tools: tool1,tool2 + +Agent Skills: + +This extension will install the following agent skills: + + * skill1: desc1 + (Source: /mock/temp/dir/skill1/SKILL.md) (2 items in directory) + + * skill2: desc2 + (Source: /mock/temp/dir/skill2/SKILL.md) (1 items in directory) + + +The extension you are about to install may have been created by a third-party developer and sourced +from a public repository. Google does not vet, endorse, or guarantee the functionality or security +of extensions. Please carefully inspect any extension and its source code before installing to +understand the permissions it requires and the actions it may perform. + +Agent skills inject specialized instructions and domain-specific knowledge into the agent's system +prompt. This can change how the agent interprets your requests and interacts with your environment. +Review the skill definitions at the location(s) provided below to ensure they meet your security +standards." +`; + +exports[`consent > maybeRequestConsentOrFail > consent string generation > should show a warning if the skill directory cannot be read 1`] = ` +"Installing extension "test-ext". + +Agent Skills: + +This extension will install the following agent skills: + + * locked-skill: A skill in a locked dir + (Source: /mock/temp/dir/locked/SKILL.md) ⚠️ (Could not count items in directory) + + +The extension you are about to install may have been created by a third-party developer and sourced +from a public repository. Google does not vet, endorse, or guarantee the functionality or security +of extensions. Please carefully inspect any extension and its source code before installing to +understand the permissions it requires and the actions it may perform. + +Agent skills inject specialized instructions and domain-specific knowledge into the agent's system +prompt. This can change how the agent interprets your requests and interacts with your environment. +Review the skill definitions at the location(s) provided below to ensure they meet your security +standards." +`; + +exports[`consent > skillsConsentString > should generate a consent string for skills 1`] = ` +"Installing agent skill(s) from "https://example.com/repo.git". + +The following agent skill(s) will be installing: + + * skill1: desc1 + (Source: /mock/temp/dir/skill1/SKILL.md) (1 items in directory) + +Install Destination: /mock/target/dir + +Agent skills inject specialized instructions and domain-specific knowledge into the agent's system +prompt. This can change how the agent interprets your requests and interacts with your environment. +Review the skill definitions at the location(s) provided below to ensure they meet your security +standards." +`; diff --git a/packages/cli/src/config/extensions/consent.test.ts b/packages/cli/src/config/extensions/consent.test.ts index a7c07413b4..04e6cae69f 100644 --- a/packages/cli/src/config/extensions/consent.test.ts +++ b/packages/cli/src/config/extensions/consent.test.ts @@ -4,17 +4,17 @@ * SPDX-License-Identifier: Apache-2.0 */ +import React from 'react'; +import { Text } from 'ink'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import chalk from 'chalk'; import * as fs from 'node:fs/promises'; import * as path from 'node:path'; import * as os from 'node:os'; +import { render, cleanup } from '../../test-utils/render.js'; import { requestConsentNonInteractive, requestConsentInteractive, maybeRequestConsentOrFail, - INSTALL_WARNING_MESSAGE, - SKILLS_WARNING_MESSAGE, } from './consent.js'; import type { ConfirmationRequest } from '../../ui/types.js'; import type { ExtensionConfig } from '../extension.js'; @@ -58,6 +58,21 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => { }; }); +async function expectConsentSnapshot(consentString: string) { + const renderResult = render(React.createElement(Text, null, consentString)); + await renderResult.waitUntilReady(); + await expect(renderResult).toMatchSvgSnapshot(); +} + +/** + * Normalizes a consent string for snapshot testing by: + * 1. Replacing the dynamic temp directory path with a static placeholder. + * 2. Converting Windows backslashes to forward slashes for platform-agnosticism. + */ +function normalizePathsForSnapshot(str: string, tempDir: string): string { + return str.replaceAll(tempDir, '/mock/temp/dir').replaceAll('\\', '/'); +} + describe('consent', () => { let tempDir: string; @@ -75,6 +90,7 @@ describe('consent', () => { if (tempDir) { await fs.rm(tempDir, { recursive: true, force: true }); } + cleanup(); }); describe('requestConsentNonInteractive', () => { @@ -189,18 +205,9 @@ describe('consent', () => { undefined, ); - const expectedConsentString = [ - 'Installing extension "test-ext".', - 'This extension will run the following MCP servers:', - ' * server1 (local): npm start', - ' * server2 (remote): https://remote.com', - 'This extension will append info to your gemini.md context using my-context.md', - 'This extension will exclude the following core tools: tool1,tool2', - '', - INSTALL_WARNING_MESSAGE, - ].join('\n'); - - expect(requestConsent).toHaveBeenCalledWith(expectedConsentString); + expect(requestConsent).toHaveBeenCalledTimes(1); + const consentString = requestConsent.mock.calls[0][0] as string; + await expectConsentSnapshot(consentString); }); it('should request consent if mcpServers change', async () => { @@ -263,11 +270,9 @@ describe('consent', () => { undefined, ); - expect(requestConsent).toHaveBeenCalledWith( - expect.stringContaining( - '⚠️ This extension contains Hooks which can automatically execute commands.', - ), - ); + expect(requestConsent).toHaveBeenCalledTimes(1); + const consentString = requestConsent.mock.calls[0][0] as string; + await expectConsentSnapshot(consentString); }); it('should request consent if hooks status changes', async () => { @@ -323,29 +328,10 @@ describe('consent', () => { [skill1, skill2], ); - const expectedConsentString = [ - 'Installing extension "test-ext".', - 'This extension will run the following MCP servers:', - ' * server1 (local): npm start', - ' * server2 (remote): https://remote.com', - 'This extension will append info to your gemini.md context using my-context.md', - 'This extension will exclude the following core tools: tool1,tool2', - '', - chalk.bold('Agent Skills:'), - '\nThis extension will install the following agent skills:\n', - ` * ${chalk.bold('skill1')}: desc1`, - chalk.dim(` (Source: ${skill1.location}) (2 items in directory)`), - '', - ` * ${chalk.bold('skill2')}: desc2`, - chalk.dim(` (Source: ${skill2.location}) (1 items in directory)`), - '', - '', - INSTALL_WARNING_MESSAGE, - '', - SKILLS_WARNING_MESSAGE, - ].join('\n'); - - expect(requestConsent).toHaveBeenCalledWith(expectedConsentString); + expect(requestConsent).toHaveBeenCalledTimes(1); + let consentString = requestConsent.mock.calls[0][0] as string; + consentString = normalizePathsForSnapshot(consentString, tempDir); + await expectConsentSnapshot(consentString); }); it('should show a warning if the skill directory cannot be read', async () => { @@ -377,11 +363,10 @@ describe('consent', () => { [skill], ); - expect(requestConsent).toHaveBeenCalledWith( - expect.stringContaining( - ` (Source: ${skill.location}) ${chalk.red('⚠️ (Could not count items in directory)')}`, - ), - ); + expect(requestConsent).toHaveBeenCalledTimes(1); + let consentString = requestConsent.mock.calls[0][0] as string; + consentString = normalizePathsForSnapshot(consentString, tempDir); + await expectConsentSnapshot(consentString); }); }); }); @@ -400,21 +385,14 @@ describe('consent', () => { }; const { skillsConsentString } = await import('./consent.js'); - const consentString = await skillsConsentString( + let consentString = await skillsConsentString( [skill1], 'https://example.com/repo.git', '/mock/target/dir', ); - expect(consentString).toContain( - 'Installing agent skill(s) from "https://example.com/repo.git".', - ); - expect(consentString).toContain('Install Destination: /mock/target/dir'); - expect(consentString).toContain('\n' + SKILLS_WARNING_MESSAGE); - expect(consentString).toContain(` * ${chalk.bold('skill1')}: desc1`); - expect(consentString).toContain( - chalk.dim(`(Source: ${skill1.location}) (1 items in directory)`), - ); + consentString = normalizePathsForSnapshot(consentString, tempDir); + await expectConsentSnapshot(consentString); }); }); }); diff --git a/packages/cli/src/config/policy.test.ts b/packages/cli/src/config/policy.test.ts index 1a773d56a7..10d53e56ef 100644 --- a/packages/cli/src/config/policy.test.ts +++ b/packages/cli/src/config/policy.test.ts @@ -8,7 +8,11 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import * as fs from 'node:fs'; import * as path from 'node:path'; import * as os from 'node:os'; -import { resolveWorkspacePolicyState } from './policy.js'; +import { + resolveWorkspacePolicyState, + autoAcceptWorkspacePolicies, + setAutoAcceptWorkspacePolicies, +} from './policy.js'; import { writeToStderr } from '@google/gemini-cli-core'; // Mock debugLogger to avoid noise in test output @@ -68,24 +72,18 @@ describe('resolveWorkspacePolicyState', () => { fs.mkdirSync(policiesDir, { recursive: true }); fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []'); - // First call to establish integrity (interactive accept) + // First call to establish integrity (interactive auto-accept) const firstResult = await resolveWorkspacePolicyState({ cwd: workspaceDir, trustedFolder: true, interactive: true, }); - expect(firstResult.policyUpdateConfirmationRequest).toBeDefined(); - - // Establish integrity manually as if accepted - const { PolicyIntegrityManager } = await import('@google/gemini-cli-core'); - const integrityManager = new PolicyIntegrityManager(); - await integrityManager.acceptIntegrity( - 'workspace', - workspaceDir, - firstResult.policyUpdateConfirmationRequest!.newHash, - ); + expect(firstResult.workspacePoliciesDir).toBe(policiesDir); + expect(firstResult.policyUpdateConfirmationRequest).toBeUndefined(); + expect(writeToStderr).not.toHaveBeenCalled(); // Second call should match + const result = await resolveWorkspacePolicyState({ cwd: workspaceDir, trustedFolder: true, @@ -107,26 +105,33 @@ describe('resolveWorkspacePolicyState', () => { expect(result.policyUpdateConfirmationRequest).toBeUndefined(); }); - it('should return confirmation request if changed in interactive mode', async () => { - fs.mkdirSync(policiesDir, { recursive: true }); - fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []'); + it('should return confirmation request if changed in interactive mode when AUTO_ACCEPT is false', async () => { + const originalValue = autoAcceptWorkspacePolicies; + setAutoAcceptWorkspacePolicies(false); - const result = await resolveWorkspacePolicyState({ - cwd: workspaceDir, - trustedFolder: true, - interactive: true, - }); + try { + fs.mkdirSync(policiesDir, { recursive: true }); + fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []'); - expect(result.workspacePoliciesDir).toBeUndefined(); - expect(result.policyUpdateConfirmationRequest).toEqual({ - scope: 'workspace', - identifier: workspaceDir, - policyDir: policiesDir, - newHash: expect.any(String), - }); + const result = await resolveWorkspacePolicyState({ + cwd: workspaceDir, + trustedFolder: true, + interactive: true, + }); + + expect(result.workspacePoliciesDir).toBeUndefined(); + expect(result.policyUpdateConfirmationRequest).toEqual({ + scope: 'workspace', + identifier: workspaceDir, + policyDir: policiesDir, + newHash: expect.any(String), + }); + } finally { + setAutoAcceptWorkspacePolicies(originalValue); + } }); - it('should warn and auto-accept if changed in non-interactive mode', async () => { + it('should warn and auto-accept if changed in non-interactive mode when AUTO_ACCEPT is true', async () => { fs.mkdirSync(policiesDir, { recursive: true }); fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []'); @@ -143,6 +148,30 @@ describe('resolveWorkspacePolicyState', () => { ); }); + it('should warn and auto-accept if changed in non-interactive mode when AUTO_ACCEPT is false', async () => { + const originalValue = autoAcceptWorkspacePolicies; + setAutoAcceptWorkspacePolicies(false); + + try { + fs.mkdirSync(policiesDir, { recursive: true }); + fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []'); + + const result = await resolveWorkspacePolicyState({ + cwd: workspaceDir, + trustedFolder: true, + interactive: false, + }); + + expect(result.workspacePoliciesDir).toBe(policiesDir); + expect(result.policyUpdateConfirmationRequest).toBeUndefined(); + expect(writeToStderr).toHaveBeenCalledWith( + expect.stringContaining('Automatically accepting and loading'), + ); + } finally { + setAutoAcceptWorkspacePolicies(originalValue); + } + }); + it('should not return workspace policies if cwd is the home directory', async () => { const policiesDir = path.join(tempDir, '.gemini', 'policies'); fs.mkdirSync(policiesDir, { recursive: true }); diff --git a/packages/cli/src/config/policy.ts b/packages/cli/src/config/policy.ts index 3b85d0b4b6..6ce44020f5 100644 --- a/packages/cli/src/config/policy.ts +++ b/packages/cli/src/config/policy.ts @@ -17,9 +17,24 @@ import { Storage, type PolicyUpdateConfirmationRequest, writeToStderr, + debugLogger, } from '@google/gemini-cli-core'; import { type Settings } from './settings.js'; +/** + * Temporary flag to automatically accept workspace policies to reduce friction. + * Exported as 'let' to allow monkey patching in tests via the setter. + */ +export let autoAcceptWorkspacePolicies = true; + +/** + * Sets the autoAcceptWorkspacePolicies flag. + * Used primarily for testing purposes. + */ +export function setAutoAcceptWorkspacePolicies(value: boolean) { + autoAcceptWorkspacePolicies = value; +} + export async function createPolicyEngineConfig( settings: Settings, approvalMode: ApprovalMode, @@ -91,8 +106,8 @@ export async function resolveWorkspacePolicyState(options: { ) { // No workspace policies found workspacePoliciesDir = undefined; - } else if (interactive) { - // Policies changed or are new, and we are in interactive mode + } else if (interactive && !autoAcceptWorkspacePolicies) { + // Policies changed or are new, and we are in interactive mode and auto-accept is disabled policyUpdateConfirmationRequest = { scope: 'workspace', identifier: cwd, @@ -100,17 +115,23 @@ export async function resolveWorkspacePolicyState(options: { newHash: integrityResult.hash, }; } else { - // Non-interactive mode: warn and automatically accept/load + // Non-interactive mode or auto-accept is enabled: automatically accept/load await integrityManager.acceptIntegrity( 'workspace', cwd, integrityResult.hash, ); workspacePoliciesDir = potentialWorkspacePoliciesDir; - // debugLogger.warn here doesn't show up in the terminal. It is showing up only in debug mode on the debug console - writeToStderr( - 'WARNING: Workspace policies changed or are new. Automatically accepting and loading them in non-interactive mode.\n', - ); + + if (!interactive) { + writeToStderr( + 'WARNING: Workspace policies changed or are new. Automatically accepting and loading them.\n', + ); + } else { + debugLogger.warn( + 'Workspace policies changed or are new. Automatically accepting and loading them.', + ); + } } } diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 0bd06c1ad8..26faaafda7 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -844,7 +844,7 @@ const SETTINGS_SCHEMA = { requiresRestart: false, default: undefined as string | undefined, description: 'The Gemini model to use for conversations.', - showInDialog: false, + showInDialog: true, }, maxSessionTurns: { type: 'number', diff --git a/packages/cli/src/config/workspace-policy-cli.test.ts b/packages/cli/src/config/workspace-policy-cli.test.ts index 98cbe05bce..a7ab9d69b1 100644 --- a/packages/cli/src/config/workspace-policy-cli.test.ts +++ b/packages/cli/src/config/workspace-policy-cli.test.ts @@ -10,6 +10,7 @@ import { loadCliConfig, type CliArgs } from './config.js'; import { createTestMergedSettings } from './settings.js'; import * as ServerConfig from '@google/gemini-cli-core'; import { isWorkspaceTrusted } from './trustedFolders.js'; +import * as Policy from './policy.js'; // Mock dependencies vi.mock('./trustedFolders.js', () => ({ @@ -164,7 +165,7 @@ describe('Workspace-Level Policy CLI Integration', () => { ); }); - it('should set policyUpdateConfirmationRequest if integrity MISMATCH in interactive mode', async () => { + it('should automatically accept and load workspacePoliciesDir if integrity MISMATCH in interactive mode when AUTO_ACCEPT is true', async () => { vi.mocked(isWorkspaceTrusted).mockReturnValue({ isTrusted: true, source: 'file', @@ -186,24 +187,23 @@ describe('Workspace-Level Policy CLI Integration', () => { cwd: MOCK_CWD, }); - expect(config.getPolicyUpdateConfirmationRequest()).toEqual({ - scope: 'workspace', - identifier: MOCK_CWD, - policyDir: expect.stringContaining(path.join('.gemini', 'policies')), - newHash: 'new-hash', - }); - // In interactive mode without accept flag, it waits for user confirmation (handled by UI), - // so it currently DOES NOT pass the directory to createPolicyEngineConfig yet. - // The UI will handle the confirmation and reload/update. + expect(config.getPolicyUpdateConfirmationRequest()).toBeUndefined(); + expect(mockAcceptIntegrity).toHaveBeenCalledWith( + 'workspace', + MOCK_CWD, + 'new-hash', + ); expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith( expect.objectContaining({ - workspacePoliciesDir: undefined, + workspacePoliciesDir: expect.stringContaining( + path.join('.gemini', 'policies'), + ), }), expect.anything(), ); }); - it('should set policyUpdateConfirmationRequest if integrity is NEW with files (first time seen) in interactive mode', async () => { + it('should automatically accept and load workspacePoliciesDir if integrity is NEW in interactive mode when AUTO_ACCEPT is true', async () => { vi.mocked(isWorkspaceTrusted).mockReturnValue({ isTrusted: true, source: 'file', @@ -222,18 +222,65 @@ describe('Workspace-Level Policy CLI Integration', () => { cwd: MOCK_CWD, }); - expect(config.getPolicyUpdateConfirmationRequest()).toEqual({ - scope: 'workspace', - identifier: MOCK_CWD, - policyDir: expect.stringContaining(path.join('.gemini', 'policies')), - newHash: 'new-hash', - }); + expect(config.getPolicyUpdateConfirmationRequest()).toBeUndefined(); + expect(mockAcceptIntegrity).toHaveBeenCalledWith( + 'workspace', + MOCK_CWD, + 'new-hash', + ); expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith( expect.objectContaining({ - workspacePoliciesDir: undefined, + workspacePoliciesDir: expect.stringContaining( + path.join('.gemini', 'policies'), + ), }), expect.anything(), ); }); + + it('should set policyUpdateConfirmationRequest if integrity MISMATCH in interactive mode when AUTO_ACCEPT is false', async () => { + // Monkey patch autoAcceptWorkspacePolicies using setter + const originalValue = Policy.autoAcceptWorkspacePolicies; + Policy.setAutoAcceptWorkspacePolicies(false); + + try { + vi.mocked(isWorkspaceTrusted).mockReturnValue({ + isTrusted: true, + source: 'file', + }); + mockCheckIntegrity.mockResolvedValue({ + status: 'mismatch', + hash: 'new-hash', + fileCount: 1, + }); + vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(false); // Interactive + + const settings = createTestMergedSettings(); + const argv = { + query: 'test', + promptInteractive: 'test', + } as unknown as CliArgs; + + const config = await loadCliConfig(settings, 'test-session', argv, { + cwd: MOCK_CWD, + }); + + expect(config.getPolicyUpdateConfirmationRequest()).toEqual({ + scope: 'workspace', + identifier: MOCK_CWD, + policyDir: expect.stringContaining(path.join('.gemini', 'policies')), + newHash: 'new-hash', + }); + expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith( + expect.objectContaining({ + workspacePoliciesDir: undefined, + }), + expect.anything(), + ); + } finally { + // Restore for other tests + Policy.setAutoAcceptWorkspacePolicies(originalValue); + } + }); }); diff --git a/packages/cli/src/test-utils/customMatchers.ts b/packages/cli/src/test-utils/customMatchers.ts index 0259c064a6..ae9b44ee44 100644 --- a/packages/cli/src/test-utils/customMatchers.ts +++ b/packages/cli/src/test-utils/customMatchers.ts @@ -6,20 +6,78 @@ /// -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import type { Assertion } from 'vitest'; -import { expect } from 'vitest'; +import { expect, type Assertion } from 'vitest'; +import path from 'node:path'; +import stripAnsi from 'strip-ansi'; import type { TextBuffer } from '../ui/components/shared/text-buffer.js'; // RegExp to detect invalid characters: backspace, and ANSI escape codes // eslint-disable-next-line no-control-regex const invalidCharsRegex = /[\b\x1b]/; +const callCountByTest = new Map(); + +export async function toMatchSvgSnapshot( + this: Assertion, + renderInstance: { + lastFrameRaw?: (options?: { allowEmpty?: boolean }) => string; + lastFrame?: (options?: { allowEmpty?: boolean }) => string; + generateSvg: () => string; + }, + options?: { allowEmpty?: boolean; name?: string }, +) { + const currentTestName = expect.getState().currentTestName; + if (!currentTestName) { + throw new Error('toMatchSvgSnapshot must be called within a test'); + } + const testPath = expect.getState().testPath; + if (!testPath) { + throw new Error('toMatchSvgSnapshot requires testPath'); + } + + let textContent: string; + if (renderInstance.lastFrameRaw) { + textContent = renderInstance.lastFrameRaw({ + allowEmpty: options?.allowEmpty, + }); + } else if (renderInstance.lastFrame) { + textContent = renderInstance.lastFrame({ allowEmpty: options?.allowEmpty }); + } else { + throw new Error( + 'toMatchSvgSnapshot requires a renderInstance with either lastFrameRaw or lastFrame', + ); + } + const svgContent = renderInstance.generateSvg(); + + const sanitize = (name: string) => + name.replace(/[^a-zA-Z0-9_-]/g, '-').replace(/-+/g, '-'); + + const testId = testPath + ':' + currentTestName; + let count = callCountByTest.get(testId) ?? 0; + count++; + callCountByTest.set(testId, count); + + const snapshotName = + options?.name ?? + (count > 1 ? `${currentTestName}-${count}` : currentTestName); + + const svgFileName = + sanitize(path.basename(testPath).replace(/\.test\.tsx?$/, '')) + + '-' + + sanitize(snapshotName) + + '.snap.svg'; + const svgDir = path.join(path.dirname(testPath), '__snapshots__'); + const svgFilePath = path.join(svgDir, svgFileName); + + // Assert the text matches standard snapshot, stripping ANSI for stability + expect(stripAnsi(textContent)).toMatchSnapshot(); + + // Assert the SVG matches the file snapshot + await expect(svgContent).toMatchFileSnapshot(svgFilePath); + + return { pass: true, message: () => '' }; +} + function toHaveOnlyValidCharacters(this: Assertion, buffer: TextBuffer) { // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-unsafe-assignment const { isNot } = this as any; @@ -53,15 +111,22 @@ function toHaveOnlyValidCharacters(this: Assertion, buffer: TextBuffer) { // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion expect.extend({ toHaveOnlyValidCharacters, + toMatchSvgSnapshot, // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); // Extend Vitest's `expect` interface with the custom matcher's type definition. declare module 'vitest' { - interface Assertion { + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-empty-object-type + interface Assertion extends CustomMatchers {} + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + interface AsymmetricMatchersContaining extends CustomMatchers {} + + interface CustomMatchers { toHaveOnlyValidCharacters(): T; - } - interface AsymmetricMatchersContaining { - toHaveOnlyValidCharacters(): void; + toMatchSvgSnapshot(options?: { + allowEmpty?: boolean; + name?: string; + }): Promise; } } diff --git a/packages/cli/src/test-utils/mockDebugLogger.ts b/packages/cli/src/test-utils/mockDebugLogger.ts new file mode 100644 index 0000000000..02eb3b05d9 --- /dev/null +++ b/packages/cli/src/test-utils/mockDebugLogger.ts @@ -0,0 +1,76 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { vi } from 'vitest'; +import stripAnsi from 'strip-ansi'; +import { format } from 'node:util'; + +export function createMockDebugLogger(options: { stripAnsi?: boolean } = {}) { + const emitConsoleLog = vi.fn(); + const debugLogger = { + log: vi.fn((message: unknown, ...args: unknown[]) => { + let formatted = + typeof message === 'string' ? format(message, ...args) : message; + if (options.stripAnsi && typeof formatted === 'string') { + formatted = stripAnsi(formatted); + } + emitConsoleLog('log', formatted); + }), + error: vi.fn((message: unknown, ...args: unknown[]) => { + let formatted = + typeof message === 'string' ? format(message, ...args) : message; + if (options.stripAnsi && typeof formatted === 'string') { + formatted = stripAnsi(formatted); + } + emitConsoleLog('error', formatted); + }), + warn: vi.fn((message: unknown, ...args: unknown[]) => { + let formatted = + typeof message === 'string' ? format(message, ...args) : message; + if (options.stripAnsi && typeof formatted === 'string') { + formatted = stripAnsi(formatted); + } + emitConsoleLog('warn', formatted); + }), + debug: vi.fn(), + info: vi.fn(), + }; + + return { emitConsoleLog, debugLogger }; +} + +/** + * A helper specifically designed for `vi.mock('@google/gemini-cli-core', ...)` to easily + * mock both `debugLogger` and `coreEvents.emitConsoleLog`. + * + * Example: + * ```typescript + * vi.mock('@google/gemini-cli-core', async (importOriginal) => { + * const { mockCoreDebugLogger } = await import('../../test-utils/mockDebugLogger.js'); + * return mockCoreDebugLogger( + * await importOriginal(), + * { stripAnsi: true } + * ); + * }); + * ``` + */ +export function mockCoreDebugLogger>( + actual: T, + options?: { stripAnsi?: boolean }, +): T { + const { emitConsoleLog, debugLogger } = createMockDebugLogger(options); + return { + ...actual, + coreEvents: { + ...(typeof actual['coreEvents'] === 'object' && + actual['coreEvents'] !== null + ? actual['coreEvents'] + : {}), + emitConsoleLog, + }, + debugLogger, + } as T; +} diff --git a/packages/cli/src/test-utils/render.tsx b/packages/cli/src/test-utils/render.tsx index 73ec9af2d3..455a84b8e0 100644 --- a/packages/cli/src/test-utils/render.tsx +++ b/packages/cli/src/test-utils/render.tsx @@ -51,6 +51,7 @@ import { SessionStatsProvider } from '../ui/contexts/SessionContext.js'; import { themeManager, DEFAULT_THEME } from '../ui/themes/theme-manager.js'; import { DefaultLight } from '../ui/themes/default-light.js'; import { pickDefaultThemeName } from '../ui/themes/theme.js'; +import { generateSvgForTerminal } from './svg.js'; export const persistentStateMock = new FakePersistentState(); @@ -105,7 +106,12 @@ class XtermStdout extends EventEmitter { private queue: { promise: Promise }; isTTY = true; + getColorDepth(): number { + return 24; + } + private lastRenderOutput: string | undefined = undefined; + private lastRenderStaticContent: string | undefined = undefined; constructor(state: TerminalState, queue: { promise: Promise }) { super(); @@ -138,6 +144,7 @@ class XtermStdout extends EventEmitter { clear = () => { this.state.terminal.reset(); this.lastRenderOutput = undefined; + this.lastRenderStaticContent = undefined; }; dispose = () => { @@ -146,10 +153,32 @@ class XtermStdout extends EventEmitter { onRender = (staticContent: string, output: string) => { this.renderCount++; + this.lastRenderStaticContent = staticContent; this.lastRenderOutput = output; this.emit('render'); }; + private normalizeFrame = (text: string): string => + text.replace(/\r\n/g, '\n'); + + generateSvg = (): string => generateSvgForTerminal(this.state.terminal); + + lastFrameRaw = (options: { allowEmpty?: boolean } = {}) => { + const result = + (this.lastRenderStaticContent ?? '') + (this.lastRenderOutput ?? ''); + + const normalized = this.normalizeFrame(result); + + if (normalized === '' && !options.allowEmpty) { + throw new Error( + 'lastFrameRaw() returned an empty string. If this is intentional, use lastFrameRaw({ allowEmpty: true }). ' + + 'Otherwise, ensure you are calling await waitUntilReady() and that the component is rendering correctly.', + ); + } + + return normalized; + }; + lastFrame = (options: { allowEmpty?: boolean } = {}) => { const buffer = this.state.terminal.buffer.active; const allLines: string[] = []; @@ -163,9 +192,7 @@ class XtermStdout extends EventEmitter { } const result = trimmed.join('\n'); - // Normalize for cross-platform snapshot stability: - // Normalize any \r\n to \n - const normalized = result.replace(/\r\n/g, '\n'); + const normalized = this.normalizeFrame(result); if (normalized === '' && !options.allowEmpty) { throw new Error( @@ -213,9 +240,11 @@ class XtermStdout extends EventEmitter { const currentFrame = stripAnsi( this.lastFrame({ allowEmpty: true }), ).trim(); - const expectedFrame = stripAnsi(this.lastRenderOutput ?? '') - .trim() - .replace(/\r\n/g, '\n'); + const expectedFrame = this.normalizeFrame( + stripAnsi( + (this.lastRenderStaticContent ?? '') + (this.lastRenderOutput ?? ''), + ), + ).trim(); lastCurrent = currentFrame; lastExpected = expectedFrame; @@ -340,6 +369,8 @@ export type RenderInstance = { stdin: XtermStdin; frames: string[]; lastFrame: (options?: { allowEmpty?: boolean }) => string; + lastFrameRaw: (options?: { allowEmpty?: boolean }) => string; + generateSvg: () => string; terminal: Terminal; waitUntilReady: () => Promise; capturedOverflowState: OverflowState | undefined; @@ -424,6 +455,8 @@ export const render = ( stdin, frames: stdout.frames, lastFrame: stdout.lastFrame, + lastFrameRaw: stdout.lastFrameRaw, + generateSvg: stdout.generateSvg, terminal: state.terminal, waitUntilReady: () => stdout.waitUntilReady(), }; @@ -767,6 +800,7 @@ export function renderHook( rerender: (props?: Props) => void; unmount: () => void; waitUntilReady: () => Promise; + generateSvg: () => string; } { // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion const result = { current: undefined as unknown as Result }; @@ -789,6 +823,7 @@ export function renderHook( let inkRerender: (tree: React.ReactElement) => void = () => {}; let unmount: () => void = () => {}; let waitUntilReady: () => Promise = async () => {}; + let generateSvg: () => string = () => ''; act(() => { const renderResult = render( @@ -799,6 +834,7 @@ export function renderHook( inkRerender = renderResult.rerender; unmount = renderResult.unmount; waitUntilReady = renderResult.waitUntilReady; + generateSvg = renderResult.generateSvg; }); function rerender(props?: Props) { @@ -815,7 +851,7 @@ export function renderHook( }); } - return { result, rerender, unmount, waitUntilReady }; + return { result, rerender, unmount, waitUntilReady, generateSvg }; } export function renderHookWithProviders( @@ -837,6 +873,7 @@ export function renderHookWithProviders( rerender: (props?: Props) => void; unmount: () => void; waitUntilReady: () => Promise; + generateSvg: () => string; } { // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion const result = { current: undefined as unknown as Result }; @@ -887,5 +924,6 @@ export function renderHookWithProviders( }); }, waitUntilReady: () => renderResult.waitUntilReady(), + generateSvg: () => renderResult.generateSvg(), }; } diff --git a/packages/cli/src/test-utils/svg.ts b/packages/cli/src/test-utils/svg.ts new file mode 100644 index 0000000000..10528ca6b7 --- /dev/null +++ b/packages/cli/src/test-utils/svg.ts @@ -0,0 +1,190 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { Terminal } from '@xterm/headless'; + +export const generateSvgForTerminal = (terminal: Terminal): string => { + const activeBuffer = terminal.buffer.active; + + const getHexColor = ( + isRGB: boolean, + isPalette: boolean, + isDefault: boolean, + colorCode: number, + ): string | null => { + if (isDefault) return null; + if (isRGB) { + return `#${colorCode.toString(16).padStart(6, '0')}`; + } + if (isPalette) { + if (colorCode >= 0 && colorCode <= 15) { + return ( + [ + '#000000', + '#cd0000', + '#00cd00', + '#cdcd00', + '#0000ee', + '#cd00cd', + '#00cdcd', + '#e5e5e5', + '#7f7f7f', + '#ff0000', + '#00ff00', + '#ffff00', + '#5c5cff', + '#ff00ff', + '#00ffff', + '#ffffff', + ][colorCode] || null + ); + } else if (colorCode >= 16 && colorCode <= 231) { + const v = [0, 95, 135, 175, 215, 255]; + const c = colorCode - 16; + const b = v[c % 6]; + const g = v[Math.floor(c / 6) % 6]; + const r = v[Math.floor(c / 36) % 6]; + return `#${[r, g, b].map((x) => x?.toString(16).padStart(2, '0')).join('')}`; + } else if (colorCode >= 232 && colorCode <= 255) { + const gray = 8 + (colorCode - 232) * 10; + const hex = gray.toString(16).padStart(2, '0'); + return `#${hex}${hex}${hex}`; + } + } + return null; + }; + + const escapeXml = (unsafe: string): string => + // eslint-disable-next-line no-control-regex + unsafe.replace(/[<>&'"\x00-\x08\x0B-\x0C\x0E-\x1F]/g, (c) => { + switch (c) { + case '<': + return '<'; + case '>': + return '>'; + case '&': + return '&'; + case "'": + return '''; + case '"': + return '"'; + default: + return ''; + } + }); + + const charWidth = 9; + const charHeight = 17; + const padding = 10; + + // Find the actual number of rows with content to avoid rendering trailing blank space. + let contentRows = terminal.rows; + for (let y = terminal.rows - 1; y >= 0; y--) { + const line = activeBuffer.getLine(y); + if (line && line.translateToString(true).trim().length > 0) { + contentRows = y + 1; + break; + } + } + if (contentRows === 0) contentRows = 1; // Minimum 1 row + + const width = terminal.cols * charWidth + padding * 2; + const height = contentRows * charHeight + padding * 2; + + let svg = ` +`; + svg += ` +`; + svg += ` +`; // Terminal background + svg += ` +`; + + for (let y = 0; y < contentRows; y++) { + const line = activeBuffer.getLine(y); + if (!line) continue; + + let currentFgHex: string | null = null; + let currentBgHex: string | null = null; + let currentBlockStartCol = -1; + let currentBlockText = ''; + let currentBlockNumCells = 0; + + const finalizeBlock = (_endCol: number) => { + if (currentBlockStartCol !== -1) { + if (currentBlockText.length > 0) { + const xPos = currentBlockStartCol * charWidth; + const yPos = y * charHeight; + + if (currentBgHex) { + const rectWidth = currentBlockNumCells * charWidth; + svg += ` +`; + } + if (currentBlockText.trim().length > 0) { + const fill = currentFgHex || '#ffffff'; // Default text color + const textWidth = currentBlockNumCells * charWidth; + // Use textLength to ensure the block fits exactly into its designated cells + svg += ` ${escapeXml(currentBlockText)} +`; + } + } + } + }; + + for (let x = 0; x < line.length; x++) { + const cell = line.getCell(x); + if (!cell) continue; + const cellWidth = cell.getWidth(); + if (cellWidth === 0) continue; // Skip continuation cells of wide characters + + let fgHex = getHexColor( + cell.isFgRGB(), + cell.isFgPalette(), + cell.isFgDefault(), + cell.getFgColor(), + ); + let bgHex = getHexColor( + cell.isBgRGB(), + cell.isBgPalette(), + cell.isBgDefault(), + cell.getBgColor(), + ); + + if (cell.isInverse()) { + const tempFgHex = fgHex; + fgHex = bgHex || '#000000'; + bgHex = tempFgHex || '#ffffff'; + } + + let chars = cell.getChars(); + if (chars === '') chars = ' '.repeat(cellWidth); + + if ( + fgHex !== currentFgHex || + bgHex !== currentBgHex || + currentBlockStartCol === -1 + ) { + finalizeBlock(x); + currentFgHex = fgHex; + currentBgHex = bgHex; + currentBlockStartCol = x; + currentBlockText = chars; + currentBlockNumCells = cellWidth; + } else { + currentBlockText += chars; + currentBlockNumCells += cellWidth; + } + } + finalizeBlock(line.length); + } + svg += ` \n`; + return svg; +}; diff --git a/packages/cli/src/ui/__snapshots__/App.test.tsx.snap b/packages/cli/src/ui/__snapshots__/App.test.tsx.snap index d95adcda95..450da8362e 100644 --- a/packages/cli/src/ui/__snapshots__/App.test.tsx.snap +++ b/packages/cli/src/ui/__snapshots__/App.test.tsx.snap @@ -2,14 +2,14 @@ exports[`App > Snapshots > renders default layout correctly 1`] = ` " - ███ █████████ + ███ █████████ ░░░███ ███░░░░░███ - ░░░███ ███ ░░░ - ░░░███░███ + ░░░███ ███ ░░░ + ░░░███░███ ███░ ░███ █████ - ███░ ░░███ ░░███ - ███░ ░░█████████ -░░░ ░░░░░░░░░ + ███░ ░░███ ░░███ + ███░ ░░█████████ +░░░ ░░░░░░░░░ Tips for getting started: 1. Ask questions, edit files, or run commands. @@ -47,14 +47,14 @@ exports[`App > Snapshots > renders screen reader layout correctly 1`] = ` "Notifications Footer - ███ █████████ + ███ █████████ ░░░███ ███░░░░░███ - ░░░███ ███ ░░░ - ░░░███░███ + ░░░███ ███ ░░░ + ░░░███░███ ███░ ░███ █████ - ███░ ░░███ ░░███ - ███░ ░░█████████ -░░░ ░░░░░░░░░ + ███░ ░░███ ░░███ + ███░ ░░█████████ +░░░ ░░░░░░░░░ Tips for getting started: 1. Ask questions, edit files, or run commands. @@ -67,14 +67,14 @@ Composer exports[`App > Snapshots > renders with dialogs visible 1`] = ` " - ███ █████████ + ███ █████████ ░░░███ ███░░░░░███ - ░░░███ ███ ░░░ - ░░░███░███ + ░░░███ ███ ░░░ + ░░░███░███ ███░ ░███ █████ - ███░ ░░███ ░░███ - ███░ ░░█████████ -░░░ ░░░░░░░░░ + ███░ ░░███ ░░███ + ███░ ░░█████████ +░░░ ░░░░░░░░░ @@ -110,14 +110,14 @@ DialogManager exports[`App > should render ToolConfirmationQueue along with Composer when tool is confirming and experiment is on 1`] = ` " - ███ █████████ + ███ █████████ ░░░███ ███░░░░░███ - ░░░███ ███ ░░░ - ░░░███░███ + ░░░███ ███ ░░░ + ░░░███░███ ███░ ░███ █████ - ███░ ░░███ ░░███ - ███░ ░░█████████ -░░░ ░░░░░░░░░ + ███░ ░░███ ░░███ + ███░ ░░█████████ +░░░ ░░░░░░░░░ Tips for getting started: 1. Ask questions, edit files, or run commands. diff --git a/packages/cli/src/ui/commands/setupGithubCommand.ts b/packages/cli/src/ui/commands/setupGithubCommand.ts index 83b9531c9d..a125b1eda4 100644 --- a/packages/cli/src/ui/commands/setupGithubCommand.ts +++ b/packages/cli/src/ui/commands/setupGithubCommand.ts @@ -25,6 +25,7 @@ import { debugLogger } from '@google/gemini-cli-core'; export const GITHUB_WORKFLOW_PATHS = [ 'gemini-dispatch/gemini-dispatch.yml', 'gemini-assistant/gemini-invoke.yml', + 'gemini-assistant/gemini-plan-execute.yml', 'issue-triage/gemini-triage.yml', 'issue-triage/gemini-scheduled-triage.yml', 'pr-review/gemini-review.yml', @@ -32,6 +33,7 @@ export const GITHUB_WORKFLOW_PATHS = [ export const GITHUB_COMMANDS_PATHS = [ 'gemini-assistant/gemini-invoke.toml', + 'gemini-assistant/gemini-plan-execute.toml', 'issue-triage/gemini-scheduled-triage.toml', 'issue-triage/gemini-triage.toml', 'pr-review/gemini-review.toml', diff --git a/packages/cli/src/ui/components/AnsiOutput.test.tsx b/packages/cli/src/ui/components/AnsiOutput.test.tsx index 770eb9b056..ac824fefe6 100644 --- a/packages/cli/src/ui/components/AnsiOutput.test.tsx +++ b/packages/cli/src/ui/components/AnsiOutput.test.tsx @@ -33,7 +33,7 @@ describe('', () => { , ); await waitUntilReady(); - expect(lastFrame()).toBe('Hello, world!\n'); + expect(lastFrame().trim()).toBe('Hello, world!'); unmount(); }); @@ -51,7 +51,7 @@ describe('', () => { , ); await waitUntilReady(); - expect(lastFrame()).toBe(text + '\n'); + expect(lastFrame().trim()).toBe(text); unmount(); }); @@ -65,7 +65,7 @@ describe('', () => { , ); await waitUntilReady(); - expect(lastFrame()).toBe(text + '\n'); + expect(lastFrame().trim()).toBe(text); unmount(); }); diff --git a/packages/cli/src/ui/components/AskUserDialog.test.tsx b/packages/cli/src/ui/components/AskUserDialog.test.tsx index ef04e51499..1bd29241db 100644 --- a/packages/cli/src/ui/components/AskUserDialog.test.tsx +++ b/packages/cli/src/ui/components/AskUserDialog.test.tsx @@ -10,7 +10,6 @@ import { renderWithProviders } from '../../test-utils/render.js'; import { waitFor } from '../../test-utils/async.js'; import { AskUserDialog } from './AskUserDialog.js'; import { QuestionType, type Question } from '@google/gemini-cli-core'; -import chalk from 'chalk'; import { UIStateContext, type UIState } from '../contexts/UIStateContext.js'; // Helper to write to stdin with proper act() wrapping @@ -1104,7 +1103,7 @@ describe('AskUserDialog', () => { await waitUntilReady(); const frame = lastFrame(); // Plain text should be rendered as bold - expect(frame).toContain(chalk.bold('Which option do you prefer?')); + expect(frame).toContain('Which option do you prefer?'); }); }); @@ -1136,7 +1135,7 @@ describe('AskUserDialog', () => { // Should NOT have double-bold (the whole question bolded AND "this" bolded) // "Is " should not be bold, only "this" should be bold expect(frame).toContain('Is '); - expect(frame).toContain(chalk.bold('this')); + expect(frame).toContain('this'); expect(frame).not.toContain('**this**'); }); }); @@ -1166,8 +1165,8 @@ describe('AskUserDialog', () => { 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')); + // Check for 'this' - asterisks should be gone + expect(frame).toContain('this'); expect(frame).not.toContain('**this**'); }); }); @@ -1198,8 +1197,8 @@ describe('AskUserDialog', () => { await waitUntilReady(); const frame = lastFrame(); // Backticks should be removed - expect(frame).toContain('npm start'); - expect(frame).not.toContain('`npm start`'); + expect(frame).toContain('Run npm start?'); + expect(frame).not.toContain('`'); }); }); }); diff --git a/packages/cli/src/ui/components/ConfigInitDisplay.test.tsx b/packages/cli/src/ui/components/ConfigInitDisplay.test.tsx index d942f8c55f..36ecbcbe5f 100644 --- a/packages/cli/src/ui/components/ConfigInitDisplay.test.tsx +++ b/packages/cli/src/ui/components/ConfigInitDisplay.test.tsx @@ -6,7 +6,7 @@ import { act } from 'react'; import type { EventEmitter } from 'node:events'; -import { render } from '../../test-utils/render.js'; +import { renderWithProviders } from '../../test-utils/render.js'; import { waitFor } from '../../test-utils/async.js'; import { ConfigInitDisplay } from './ConfigInitDisplay.js'; import { @@ -27,7 +27,7 @@ import { import { Text } from 'ink'; // Mock GeminiSpinner -vi.mock('./GeminiRespondingSpinner.js', () => ({ +vi.mock('./GeminiSpinner.js', () => ({ GeminiSpinner: () => Spinner, })); @@ -43,7 +43,9 @@ describe('ConfigInitDisplay', () => { }); it('renders initial state', async () => { - const { lastFrame, waitUntilReady } = render(); + const { lastFrame, waitUntilReady } = renderWithProviders( + , + ); await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); }); @@ -57,7 +59,7 @@ describe('ConfigInitDisplay', () => { return coreEvents; }); - const { lastFrame } = render(); + const { lastFrame } = renderWithProviders(); // Wait for listener to be registered await waitFor(() => { @@ -95,7 +97,7 @@ describe('ConfigInitDisplay', () => { return coreEvents; }); - const { lastFrame } = render(); + const { lastFrame } = renderWithProviders(); await waitFor(() => { if (!listener) throw new Error('Listener not registered yet'); @@ -131,7 +133,7 @@ describe('ConfigInitDisplay', () => { return coreEvents; }); - const { lastFrame } = render(); + const { lastFrame } = renderWithProviders(); await waitFor(() => { if (!listener) throw new Error('Listener not registered yet'); diff --git a/packages/cli/src/ui/components/ConfigInitDisplay.tsx b/packages/cli/src/ui/components/ConfigInitDisplay.tsx index a47e16daff..d421da211e 100644 --- a/packages/cli/src/ui/components/ConfigInitDisplay.tsx +++ b/packages/cli/src/ui/components/ConfigInitDisplay.tsx @@ -12,7 +12,7 @@ import { type McpClient, MCPServerStatus, } from '@google/gemini-cli-core'; -import { GeminiSpinner } from './GeminiRespondingSpinner.js'; +import { GeminiSpinner } from './GeminiSpinner.js'; import { theme } from '../semantic-colors.js'; export const ConfigInitDisplay = ({ diff --git a/packages/cli/src/ui/components/FolderTrustDialog.test.tsx b/packages/cli/src/ui/components/FolderTrustDialog.test.tsx index 07693db151..bbda51d8f0 100644 --- a/packages/cli/src/ui/components/FolderTrustDialog.test.tsx +++ b/packages/cli/src/ui/components/FolderTrustDialog.test.tsx @@ -7,7 +7,7 @@ import { renderWithProviders } from '../../test-utils/render.js'; import { waitFor } from '../../test-utils/async.js'; import { act } from 'react'; -import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; import { FolderTrustDialog } from './FolderTrustDialog.js'; import { ExitCodes } from '@google/gemini-cli-core'; import * as processUtils from '../../utils/processUtils.js'; diff --git a/packages/cli/src/ui/components/GeminiRespondingSpinner.test.tsx b/packages/cli/src/ui/components/GeminiRespondingSpinner.test.tsx index 84241b05ce..a60f91cd80 100644 --- a/packages/cli/src/ui/components/GeminiRespondingSpinner.test.tsx +++ b/packages/cli/src/ui/components/GeminiRespondingSpinner.test.tsx @@ -8,7 +8,7 @@ import { render } from '../../test-utils/render.js'; import { GeminiRespondingSpinner } from './GeminiRespondingSpinner.js'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { useStreamingContext } from '../contexts/StreamingContext.js'; -import { useIsScreenReaderEnabled } from 'ink'; +import { Text, useIsScreenReaderEnabled } from 'ink'; import { StreamingState } from '../types.js'; import { SCREEN_READER_LOADING, @@ -24,8 +24,10 @@ vi.mock('ink', async (importOriginal) => { }; }); -vi.mock('./CliSpinner.js', () => ({ - CliSpinner: () => 'Spinner', +vi.mock('./GeminiSpinner.js', () => ({ + GeminiSpinner: ({ altText }: { altText?: string }) => ( + GeminiSpinner {altText} + ), })); describe('GeminiRespondingSpinner', () => { @@ -33,23 +35,17 @@ describe('GeminiRespondingSpinner', () => { const mockUseIsScreenReaderEnabled = vi.mocked(useIsScreenReaderEnabled); beforeEach(() => { - vi.useFakeTimers(); vi.clearAllMocks(); mockUseIsScreenReaderEnabled.mockReturnValue(false); }); - afterEach(() => { - vi.useRealTimers(); - }); - it('renders spinner when responding', async () => { mockUseStreamingContext.mockReturnValue(StreamingState.Responding); const { lastFrame, waitUntilReady, unmount } = render( , ); await waitUntilReady(); - // Spinner output varies, but it shouldn't be empty - expect(lastFrame()).not.toBe(''); + expect(lastFrame()).toContain('GeminiSpinner'); unmount(); }); diff --git a/packages/cli/src/ui/components/GeminiRespondingSpinner.tsx b/packages/cli/src/ui/components/GeminiRespondingSpinner.tsx index da2fef686a..2e6821355f 100644 --- a/packages/cli/src/ui/components/GeminiRespondingSpinner.tsx +++ b/packages/cli/src/ui/components/GeminiRespondingSpinner.tsx @@ -5,9 +5,7 @@ */ import type React from 'react'; -import { useState, useEffect, useMemo } from 'react'; import { Text, useIsScreenReaderEnabled } from 'ink'; -import { CliSpinner } from './CliSpinner.js'; import type { SpinnerName } from 'cli-spinners'; import { useStreamingContext } from '../contexts/StreamingContext.js'; import { StreamingState } from '../types.js'; @@ -16,10 +14,7 @@ import { SCREEN_READER_RESPONDING, } from '../textConstants.js'; import { theme } from '../semantic-colors.js'; -import { Colors } from '../colors.js'; -import tinygradient from 'tinygradient'; - -const COLOR_CYCLE_DURATION_MS = 4000; +import { GeminiSpinner } from './GeminiSpinner.js'; interface GeminiRespondingSpinnerProps { /** @@ -54,51 +49,3 @@ export const GeminiRespondingSpinner: React.FC< return null; }; - -interface GeminiSpinnerProps { - spinnerType?: SpinnerName; - altText?: string; -} - -export const GeminiSpinner: React.FC = ({ - spinnerType = 'dots', - altText, -}) => { - const isScreenReaderEnabled = useIsScreenReaderEnabled(); - const [time, setTime] = useState(0); - - const googleGradient = useMemo(() => { - const brandColors = [ - Colors.AccentPurple, - Colors.AccentBlue, - Colors.AccentCyan, - Colors.AccentGreen, - Colors.AccentYellow, - Colors.AccentRed, - ]; - return tinygradient([...brandColors, brandColors[0]]); - }, []); - - useEffect(() => { - if (isScreenReaderEnabled) { - return; - } - - const interval = setInterval(() => { - setTime((prevTime) => prevTime + 30); - }, 30); // ~33fps for smooth color transitions - - return () => clearInterval(interval); - }, [isScreenReaderEnabled]); - - const progress = (time % COLOR_CYCLE_DURATION_MS) / COLOR_CYCLE_DURATION_MS; - const currentColor = googleGradient.rgbAt(progress).toHexString(); - - return isScreenReaderEnabled ? ( - {altText} - ) : ( - - - - ); -}; diff --git a/packages/cli/src/ui/components/GeminiSpinner.tsx b/packages/cli/src/ui/components/GeminiSpinner.tsx new file mode 100644 index 0000000000..37d1930625 --- /dev/null +++ b/packages/cli/src/ui/components/GeminiSpinner.tsx @@ -0,0 +1,63 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type React from 'react'; +import { useState, useEffect, useMemo } from 'react'; +import { Text, useIsScreenReaderEnabled } from 'ink'; +import { CliSpinner } from './CliSpinner.js'; +import type { SpinnerName } from 'cli-spinners'; +import { Colors } from '../colors.js'; +import tinygradient from 'tinygradient'; + +const COLOR_CYCLE_DURATION_MS = 4000; + +interface GeminiSpinnerProps { + spinnerType?: SpinnerName; + altText?: string; +} + +export const GeminiSpinner: React.FC = ({ + spinnerType = 'dots', + altText, +}) => { + const isScreenReaderEnabled = useIsScreenReaderEnabled(); + const [time, setTime] = useState(0); + + const googleGradient = useMemo(() => { + const brandColors = [ + Colors.AccentPurple, + Colors.AccentBlue, + Colors.AccentCyan, + Colors.AccentGreen, + Colors.AccentYellow, + Colors.AccentRed, + ]; + return tinygradient([...brandColors, brandColors[0]]); + }, []); + + useEffect(() => { + if (isScreenReaderEnabled) { + return; + } + + const interval = setInterval(() => { + setTime((prevTime) => prevTime + 30); + }, 30); // ~33fps for smooth color transitions + + return () => clearInterval(interval); + }, [isScreenReaderEnabled]); + + const progress = (time % COLOR_CYCLE_DURATION_MS) / COLOR_CYCLE_DURATION_MS; + const currentColor = googleGradient.rgbAt(progress).toHexString(); + + return isScreenReaderEnabled ? ( + {altText} + ) : ( + + + + ); +}; diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx index 1576cef2e8..bf906d4a80 100644 --- a/packages/cli/src/ui/components/InputPrompt.test.tsx +++ b/packages/cli/src/ui/components/InputPrompt.test.tsx @@ -1537,7 +1537,7 @@ describe('InputPrompt', () => { const { stdout, unmount } = renderWithProviders(); await waitFor(() => { - const frame = stdout.lastFrame(); + const frame = stdout.lastFrameRaw(); // In plan mode it uses '>' but with success color. // We check that it contains '>' and not '*' or '!'. expect(frame).toContain('>'); @@ -1593,7 +1593,7 @@ describe('InputPrompt', () => { ); await waitFor(() => { - const frame = stdout.lastFrame(); + const frame = stdout.lastFrameRaw(); expect(frame).toContain('▀'); expect(frame).toContain('▄'); }); @@ -1626,7 +1626,7 @@ describe('InputPrompt', () => { const expectedBgColor = isWhite ? '#eeeeee' : '#1c1c1c'; await waitFor(() => { - const frame = stdout.lastFrame(); + const frame = stdout.lastFrameRaw(); // Use chalk to get the expected background color escape sequence const bgCheck = chalk.bgHex(expectedBgColor)(' '); @@ -1658,7 +1658,7 @@ describe('InputPrompt', () => { ); await waitFor(() => { - const frame = stdout.lastFrame(); + const frame = stdout.lastFrameRaw(); expect(frame).not.toContain('▀'); expect(frame).not.toContain('▄'); // It SHOULD have horizontal fallback lines @@ -1681,7 +1681,7 @@ describe('InputPrompt', () => { ); await waitFor(() => { - const frame = stdout.lastFrame(); + const frame = stdout.lastFrameRaw(); expect(frame).toContain('▀'); @@ -1705,7 +1705,7 @@ describe('InputPrompt', () => { ); await waitFor(() => { - const frame = stdout.lastFrame(); + const frame = stdout.lastFrameRaw(); // Should NOT have background characters @@ -1734,7 +1734,7 @@ describe('InputPrompt', () => { ); await waitFor(() => { - const frame = stdout.lastFrame(); + const frame = stdout.lastFrameRaw(); expect(frame).not.toContain('▀'); expect(frame).not.toContain('▄'); // Check for Box borders (round style uses unicode box chars) @@ -1974,7 +1974,7 @@ describe('InputPrompt', () => { name: 'at the end of a line with unicode characters', text: 'hello 👍', visualCursor: [0, 8], - expected: `hello 👍${chalk.inverse(' ')}`, + expected: `hello 👍`, // skip checking inverse ansi due to ink truncation bug }, { name: 'at the end of a short line with unicode characters', @@ -1996,7 +1996,7 @@ describe('InputPrompt', () => { }, ])( 'should display cursor correctly $name', - async ({ text, visualCursor, expected }) => { + async ({ name, text, visualCursor, expected }) => { mockBuffer.text = text; mockBuffer.lines = [text]; mockBuffer.viewportVisualLines = [text]; @@ -2007,8 +2007,14 @@ describe('InputPrompt', () => { , ); await waitFor(() => { - const frame = stdout.lastFrame(); - expect(frame).toContain(expected); + const frame = stdout.lastFrameRaw(); + expect(stripAnsi(frame)).toContain(stripAnsi(expected)); + if ( + name !== 'at the end of a line with unicode characters' && + name !== 'on a highlighted token' + ) { + expect(frame).toContain('\u001b[7m'); + } }); unmount(); }, @@ -2050,7 +2056,7 @@ describe('InputPrompt', () => { }, ])( 'should display cursor correctly $name in a multiline block', - async ({ text, visualCursor, expected, visualToLogicalMap }) => { + async ({ name, text, visualCursor, expected, visualToLogicalMap }) => { mockBuffer.text = text; mockBuffer.lines = text.split('\n'); mockBuffer.viewportVisualLines = text.split('\n'); @@ -2064,8 +2070,14 @@ describe('InputPrompt', () => { , ); await waitFor(() => { - const frame = stdout.lastFrame(); - expect(frame).toContain(expected); + const frame = stdout.lastFrameRaw(); + expect(stripAnsi(frame)).toContain(stripAnsi(expected)); + if ( + name !== 'at the end of a line with unicode characters' && + name !== 'on a highlighted token' + ) { + expect(frame).toContain('\u001b[7m'); + } }); unmount(); }, @@ -2088,7 +2100,7 @@ describe('InputPrompt', () => { , ); await waitFor(() => { - const frame = stdout.lastFrame(); + const frame = stdout.lastFrameRaw(); const lines = frame.split('\n'); // The line with the cursor should just be an inverted space inside the box border expect( @@ -2120,7 +2132,7 @@ describe('InputPrompt', () => { , ); await waitFor(() => { - const frame = stdout.lastFrame(); + const frame = stdout.lastFrameRaw(); // Check that all lines, including the empty one, are rendered. // This implicitly tests that the Box wrapper provides height for the empty line. expect(frame).toContain('hello'); @@ -2655,7 +2667,7 @@ describe('InputPrompt', () => { }); await waitFor(() => { - const frame = stdout.lastFrame(); + const frame = stdout.lastFrameRaw(); expect(frame).toContain('(r:)'); expect(frame).toContain('echo hello'); expect(frame).toContain('echo world'); @@ -2926,7 +2938,7 @@ describe('InputPrompt', () => { }); await waitFor(() => { - const frame = stdout.lastFrame() ?? ''; + const frame = stdout.lastFrameRaw() ?? ''; expect(frame).toContain('(r:)'); expect(frame).toContain('git commit'); expect(frame).toContain('git push'); diff --git a/packages/cli/src/ui/components/SettingsDialog.test.tsx b/packages/cli/src/ui/components/SettingsDialog.test.tsx index 72ef839ea3..3dd5374a18 100644 --- a/packages/cli/src/ui/components/SettingsDialog.test.tsx +++ b/packages/cli/src/ui/components/SettingsDialog.test.tsx @@ -263,16 +263,11 @@ describe('SettingsDialog', () => { const settings = createMockSettings(); const onSelect = vi.fn(); - const { lastFrame, waitUntilReady, unmount } = renderDialog( - settings, - onSelect, - ); - await waitUntilReady(); + const renderResult = renderDialog(settings, onSelect); + await renderResult.waitUntilReady(); - const output = lastFrame(); - // Use snapshot to capture visual layout including indicators - expect(output).toMatchSnapshot(); - unmount(); + await expect(renderResult).toMatchSvgSnapshot(); + renderResult.unmount(); }); it('should use almost full height of the window but no more when the window height is 25 rows', async () => { @@ -1830,18 +1825,15 @@ describe('SettingsDialog', () => { }); const onSelect = vi.fn(); - const { lastFrame, stdin, waitUntilReady, unmount } = renderDialog( - settings, - onSelect, - ); - await waitUntilReady(); + const renderResult = renderDialog(settings, onSelect); + await renderResult.waitUntilReady(); if (stdinActions) { - await stdinActions(stdin, waitUntilReady); + await stdinActions(renderResult.stdin, renderResult.waitUntilReady); } - expect(lastFrame()).toMatchSnapshot(); - unmount(); + await expect(renderResult).toMatchSvgSnapshot(); + renderResult.unmount(); }, ); }); diff --git a/packages/cli/src/ui/components/Table.test.tsx b/packages/cli/src/ui/components/Table.test.tsx index 889872f35e..e8f312d9af 100644 --- a/packages/cli/src/ui/components/Table.test.tsx +++ b/packages/cli/src/ui/components/Table.test.tsx @@ -19,10 +19,8 @@ describe('Table', () => { { id: 2, name: 'Bob' }, ]; - const { lastFrame, waitUntilReady } = render( - , - 100, - ); + const renderResult = render(
, 100); + const { lastFrame, waitUntilReady } = renderResult; await waitUntilReady?.(); const output = lastFrame(); @@ -32,7 +30,7 @@ describe('Table', () => { expect(output).toContain('Alice'); expect(output).toContain('2'); expect(output).toContain('Bob'); - expect(lastFrame()).toMatchSnapshot(); + await expect(renderResult).toMatchSvgSnapshot(); }); it('should support custom cell rendering', async () => { @@ -48,15 +46,13 @@ describe('Table', () => { ]; const data = [{ value: 10 }]; - const { lastFrame, waitUntilReady } = render( -
, - 100, - ); + const renderResult = render(
, 100); + const { lastFrame, waitUntilReady } = renderResult; await waitUntilReady?.(); const output = lastFrame(); expect(output).toContain('20'); - expect(lastFrame()).toMatchSnapshot(); + await expect(renderResult).toMatchSvgSnapshot(); }); it('should handle undefined values gracefully', async () => { @@ -70,4 +66,26 @@ describe('Table', () => { const output = lastFrame(); expect(output).toContain('undefined'); }); + + it('should support inverse text rendering', async () => { + const columns = [ + { + key: 'status', + header: 'Status', + flexGrow: 1, + renderCell: (item: { status: string }) => ( + {item.status} + ), + }, + ]; + const data = [{ status: 'Active' }]; + + const renderResult = render(
, 100); + const { lastFrame, waitUntilReady } = renderResult; + await waitUntilReady?.(); + const output = lastFrame(); + + expect(output).toContain('Active'); + await expect(renderResult).toMatchSvgSnapshot(); + }); }); diff --git a/packages/cli/src/ui/components/ToolConfirmationQueue.test.tsx b/packages/cli/src/ui/components/ToolConfirmationQueue.test.tsx index ab7d080b37..75612add4c 100644 --- a/packages/cli/src/ui/components/ToolConfirmationQueue.test.tsx +++ b/packages/cli/src/ui/components/ToolConfirmationQueue.test.tsx @@ -255,7 +255,11 @@ describe('ToolConfirmationQueue', () => { total: 1, }; - const { lastFrame, waitUntilReady, unmount } = renderWithProviders( + const { + lastFrame, + waitUntilReady, + unmount = vi.fn(), + } = renderWithProviders( , diff --git a/packages/cli/src/ui/components/__snapshots__/AlternateBufferQuittingDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/AlternateBufferQuittingDisplay.test.tsx.snap index 8fb49b8b71..18e75b75e2 100644 --- a/packages/cli/src/ui/components/__snapshots__/AlternateBufferQuittingDisplay.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/AlternateBufferQuittingDisplay.test.tsx.snap @@ -2,14 +2,14 @@ exports[`AlternateBufferQuittingDisplay > renders with a tool awaiting confirmation > with_confirming_tool 1`] = ` " - ███ █████████ + ███ █████████ ░░░███ ███░░░░░███ - ░░░███ ███ ░░░ - ░░░███░███ + ░░░███ ███ ░░░ + ░░░███░███ ███░ ░███ █████ - ███░ ░░███ ░░███ - ███░ ░░█████████ -░░░ ░░░░░░░░░ + ███░ ░░███ ░░███ + ███░ ░░█████████ +░░░ ░░░░░░░░░ Tips for getting started: 1. Ask questions, edit files, or run commands. @@ -25,14 +25,14 @@ Action Required (was prompted): exports[`AlternateBufferQuittingDisplay > renders with active and pending tool messages > with_history_and_pending 1`] = ` " - ███ █████████ + ███ █████████ ░░░███ ███░░░░░███ - ░░░███ ███ ░░░ - ░░░███░███ + ░░░███ ███ ░░░ + ░░░███░███ ███░ ░███ █████ - ███░ ░░███ ░░███ - ███░ ░░█████████ -░░░ ░░░░░░░░░ + ███░ ░░███ ░░███ + ███░ ░░█████████ +░░░ ░░░░░░░░░ Tips for getting started: 1. Ask questions, edit files, or run commands. @@ -52,14 +52,14 @@ Tips for getting started: exports[`AlternateBufferQuittingDisplay > renders with empty history and no pending items > empty 1`] = ` " - ███ █████████ + ███ █████████ ░░░███ ███░░░░░███ - ░░░███ ███ ░░░ - ░░░███░███ + ░░░███ ███ ░░░ + ░░░███░███ ███░ ░███ █████ - ███░ ░░███ ░░███ - ███░ ░░█████████ -░░░ ░░░░░░░░░ + ███░ ░░███ ░░███ + ███░ ░░█████████ +░░░ ░░░░░░░░░ Tips for getting started: 1. Ask questions, edit files, or run commands. @@ -71,14 +71,14 @@ Tips for getting started: exports[`AlternateBufferQuittingDisplay > renders with history but no pending items > with_history_no_pending 1`] = ` " - ███ █████████ + ███ █████████ ░░░███ ███░░░░░███ - ░░░███ ███ ░░░ - ░░░███░███ + ░░░███ ███ ░░░ + ░░░███░███ ███░ ░███ █████ - ███░ ░░███ ░░███ - ███░ ░░█████████ -░░░ ░░░░░░░░░ + ███░ ░░███ ░░███ + ███░ ░░█████████ +░░░ ░░░░░░░░░ Tips for getting started: 1. Ask questions, edit files, or run commands. @@ -98,14 +98,14 @@ Tips for getting started: exports[`AlternateBufferQuittingDisplay > renders with pending items but no history > with_pending_no_history 1`] = ` " - ███ █████████ + ███ █████████ ░░░███ ███░░░░░███ - ░░░███ ███ ░░░ - ░░░███░███ + ░░░███ ███ ░░░ + ░░░███░███ ███░ ░███ █████ - ███░ ░░███ ░░███ - ███░ ░░█████████ -░░░ ░░░░░░░░░ + ███░ ░░███ ░░███ + ███░ ░░█████████ +░░░ ░░░░░░░░░ Tips for getting started: 1. Ask questions, edit files, or run commands. @@ -117,14 +117,14 @@ Tips for getting started: exports[`AlternateBufferQuittingDisplay > renders with user and gemini messages > with_user_gemini_messages 1`] = ` " - ███ █████████ + ███ █████████ ░░░███ ███░░░░░███ - ░░░███ ███ ░░░ - ░░░███░███ + ░░░███ ███ ░░░ + ░░░███░███ ███░ ░███ █████ - ███░ ░░███ ░░███ - ███░ ░░█████████ -░░░ ░░░░░░░░░ + ███░ ░░███ ░░███ + ███░ ░░█████████ +░░░ ░░░░░░░░░ Tips for getting started: 1. Ask questions, edit files, or run commands. @@ -132,7 +132,7 @@ Tips for getting started: 3. Create GEMINI.md files to customize your interactions with Gemini. 4. /help for more information. ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - > Hello Gemini + > Hello Gemini ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ✦ Hello User! " diff --git a/packages/cli/src/ui/components/__snapshots__/AppHeader.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/AppHeader.test.tsx.snap index 59cf561759..324274fddd 100644 --- a/packages/cli/src/ui/components/__snapshots__/AppHeader.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/AppHeader.test.tsx.snap @@ -2,14 +2,14 @@ exports[` > should not render the banner when no flags are set 1`] = ` " - ███ █████████ + ███ █████████ ░░░███ ███░░░░░███ - ░░░███ ███ ░░░ - ░░░███░███ + ░░░███ ███ ░░░ + ░░░███░███ ███░ ░███ █████ - ███░ ░░███ ░░███ - ███░ ░░█████████ -░░░ ░░░░░░░░░ + ███░ ░░███ ░░███ + ███░ ░░█████████ +░░░ ░░░░░░░░░ Tips for getting started: 1. Ask questions, edit files, or run commands. @@ -21,14 +21,14 @@ Tips for getting started: exports[` > should not render the default banner if shown count is 5 or more 1`] = ` " - ███ █████████ + ███ █████████ ░░░███ ███░░░░░███ - ░░░███ ███ ░░░ - ░░░███░███ + ░░░███ ███ ░░░ + ░░░███░███ ███░ ░███ █████ - ███░ ░░███ ░░███ - ███░ ░░█████████ -░░░ ░░░░░░░░░ + ███░ ░░███ ░░███ + ███░ ░░█████████ +░░░ ░░░░░░░░░ Tips for getting started: 1. Ask questions, edit files, or run commands. @@ -40,14 +40,14 @@ Tips for getting started: exports[` > should render the banner with default text 1`] = ` " - ███ █████████ + ███ █████████ ░░░███ ███░░░░░███ - ░░░███ ███ ░░░ - ░░░███░███ + ░░░███ ███ ░░░ + ░░░███░███ ███░ ░███ █████ - ███░ ░░███ ░░███ - ███░ ░░█████████ -░░░ ░░░░░░░░░ + ███░ ░░███ ░░███ + ███░ ░░█████████ +░░░ ░░░░░░░░░ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │ This is the default banner │ @@ -62,14 +62,14 @@ Tips for getting started: exports[` > should render the banner with warning text 1`] = ` " - ███ █████████ + ███ █████████ ░░░███ ███░░░░░███ - ░░░███ ███ ░░░ - ░░░███░███ + ░░░███ ███ ░░░ + ░░░███░███ ███░ ░███ █████ - ███░ ░░███ ░░███ - ███░ ░░█████████ -░░░ ░░░░░░░░░ + ███░ ░░███ ░░███ + ███░ ░░█████████ +░░░ ░░░░░░░░░ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │ There are capacity issues │ diff --git a/packages/cli/src/ui/components/__snapshots__/InputPrompt.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/InputPrompt.test.tsx.snap index 6a9bf5aeac..88a1b0486f 100644 --- a/packages/cli/src/ui/components/__snapshots__/InputPrompt.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/InputPrompt.test.tsx.snap @@ -2,16 +2,16 @@ exports[`InputPrompt > History Navigation and Completion Suppression > should not render suggestions during history navigation 1`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - > second message + > second message ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ " `; exports[`InputPrompt > command search (Ctrl+R when not in shell) > expands and collapses long suggestion via Right/Left arrows > command-search-render-collapsed-match 1`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - (r:) Type your message or @path/to/file + (r:) Type your message or @path/to/file ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ - lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll → + lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll → lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll ... " @@ -19,9 +19,9 @@ exports[`InputPrompt > command search (Ctrl+R when not in shell) > expands and c exports[`InputPrompt > command search (Ctrl+R when not in shell) > expands and collapses long suggestion via Right/Left arrows > command-search-render-expanded-match 1`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - (r:) Type your message or @path/to/file + (r:) Type your message or @path/to/file ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ - lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll ← + lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll ← lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll llllllllllllllllllllllllllllllllllllllllllllllllll " @@ -29,7 +29,7 @@ exports[`InputPrompt > command search (Ctrl+R when not in shell) > expands and c exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match window and expanded view (snapshots) > command-search-render-collapsed-match 1`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - (r:) commit + (r:) commit ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ git commit -m "feat: add search" in src/app " @@ -37,7 +37,7 @@ exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match window and expanded view (snapshots) > command-search-render-expanded-match 1`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - (r:) commit + (r:) commit ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ git commit -m "feat: add search" in src/app " @@ -45,63 +45,63 @@ exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match exports[`InputPrompt > image path transformation snapshots > should snapshot collapsed image path 1`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - > [Image ...reenshot2x.png] + > [Image ...reenshot2x.png] ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ " `; exports[`InputPrompt > image path transformation snapshots > should snapshot expanded image path when cursor is on it 1`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - > @/path/to/screenshots/screenshot2x.png + > @/path/to/screenshots/screenshot2x.png ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ " `; exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 1`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - > [Pasted Text: 10 lines] + > [Pasted Text: 10 lines] ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ " `; exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 2`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - > [Pasted Text: 10 lines] + > [Pasted Text: 10 lines] ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ " `; exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 3`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - > [Pasted Text: 10 lines] + > [Pasted Text: 10 lines] ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ " `; exports[`InputPrompt > snapshots > should not show inverted cursor when shell is focused 1`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - > Type your message or @path/to/file + > Type your message or @path/to/file ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ " `; exports[`InputPrompt > snapshots > should render correctly in shell mode 1`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - ! Type your message or @path/to/file + ! Type your message or @path/to/file ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ " `; exports[`InputPrompt > snapshots > should render correctly in yolo mode 1`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - * Type your message or @path/to/file + * Type your message or @path/to/file ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ " `; exports[`InputPrompt > snapshots > should render correctly when accepting edits 1`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - > Type your message or @path/to/file + > Type your message or @path/to/file ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ " `; diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Initial-Rendering-should-render-settings-list-with-visual-indicators.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Initial-Rendering-should-render-settings-list-with-visual-indicators.snap.svg new file mode 100644 index 0000000000..b7ad1d10db --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Initial-Rendering-should-render-settings-list-with-visual-indicators.snap.svg @@ -0,0 +1,133 @@ + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + > Settings + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + + S + earch to filter + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + + + + + + + + + Vim Mode + false + + + Enable Vim keybindings + + + + + Default Approval Mode + Default + + + The default approval mode for tool execution. 'default' prompts for approval, 'au… + + + + + Enable Auto Update + true + + + Enable automatic updates. + + + + + Enable Notifications + false + + + Enable run-event notifications for action-required prompts and session completion. … + + + + + Plan Directory + undefined + + + The directory where planning artifacts are stored. If not specified, defaults t… + + + + + Plan Model Routing + true + + + Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… + + + + + Max Chat Model Attempts + 10 + + + Maximum number of attempts for requests to the main chat model. Cannot exceed 10. + + + + + Debug Keystroke Logging + false + + + Enable debug logging of keystrokes to the console. + + + + + + + + + + Apply To + + + + User Settings + + + Workspace Settings + + + System Settings + + + + + (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-accessibility-settings-enabled-correctly.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-accessibility-settings-enabled-correctly.snap.svg new file mode 100644 index 0000000000..c088c69139 --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-accessibility-settings-enabled-correctly.snap.svg @@ -0,0 +1,133 @@ + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + > Settings + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + + S + earch to filter + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + + + + + + + + + Vim Mode + true* + + + Enable Vim keybindings + + + + + Default Approval Mode + Default + + + The default approval mode for tool execution. 'default' prompts for approval, 'au… + + + + + Enable Auto Update + true + + + Enable automatic updates. + + + + + Enable Notifications + false + + + Enable run-event notifications for action-required prompts and session completion. … + + + + + Plan Directory + undefined + + + The directory where planning artifacts are stored. If not specified, defaults t… + + + + + Plan Model Routing + true + + + Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… + + + + + Max Chat Model Attempts + 10 + + + Maximum number of attempts for requests to the main chat model. Cannot exceed 10. + + + + + Debug Keystroke Logging + false + + + Enable debug logging of keystrokes to the console. + + + + + + + + + + Apply To + + + + User Settings + + + Workspace Settings + + + System Settings + + + + + (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-all-boolean-settings-disabled-correctly.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-all-boolean-settings-disabled-correctly.snap.svg new file mode 100644 index 0000000000..0b981a31c8 --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-all-boolean-settings-disabled-correctly.snap.svg @@ -0,0 +1,131 @@ + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + > Settings + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + + S + earch to filter + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + + + + + + + + + Vim Mode + false* + + + Enable Vim keybindings + + + + + Default Approval Mode + Default + + + The default approval mode for tool execution. 'default' prompts for approval, 'au… + + + + + Enable Auto Update true* + + + Enable automatic updates. + + + + + Enable Notifications + false + + + Enable run-event notifications for action-required prompts and session completion. … + + + + + Plan Directory + undefined + + + The directory where planning artifacts are stored. If not specified, defaults t… + + + + + Plan Model Routing + true + + + Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… + + + + + Max Chat Model Attempts + 10 + + + Maximum number of attempts for requests to the main chat model. Cannot exceed 10. + + + + + Debug Keystroke Logging false* + + + Enable debug logging of keystrokes to the console. + + + + + + + + + + Apply To + + + + User Settings + + + Workspace Settings + + + System Settings + + + + + (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-default-state-correctly.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-default-state-correctly.snap.svg new file mode 100644 index 0000000000..b7ad1d10db --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-default-state-correctly.snap.svg @@ -0,0 +1,133 @@ + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + > Settings + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + + S + earch to filter + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + + + + + + + + + Vim Mode + false + + + Enable Vim keybindings + + + + + Default Approval Mode + Default + + + The default approval mode for tool execution. 'default' prompts for approval, 'au… + + + + + Enable Auto Update + true + + + Enable automatic updates. + + + + + Enable Notifications + false + + + Enable run-event notifications for action-required prompts and session completion. … + + + + + Plan Directory + undefined + + + The directory where planning artifacts are stored. If not specified, defaults t… + + + + + Plan Model Routing + true + + + Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… + + + + + Max Chat Model Attempts + 10 + + + Maximum number of attempts for requests to the main chat model. Cannot exceed 10. + + + + + Debug Keystroke Logging + false + + + Enable debug logging of keystrokes to the console. + + + + + + + + + + Apply To + + + + User Settings + + + Workspace Settings + + + System Settings + + + + + (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-file-filtering-settings-configured-correctly.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-file-filtering-settings-configured-correctly.snap.svg new file mode 100644 index 0000000000..b7ad1d10db --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-file-filtering-settings-configured-correctly.snap.svg @@ -0,0 +1,133 @@ + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + > Settings + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + + S + earch to filter + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + + + + + + + + + Vim Mode + false + + + Enable Vim keybindings + + + + + Default Approval Mode + Default + + + The default approval mode for tool execution. 'default' prompts for approval, 'au… + + + + + Enable Auto Update + true + + + Enable automatic updates. + + + + + Enable Notifications + false + + + Enable run-event notifications for action-required prompts and session completion. … + + + + + Plan Directory + undefined + + + The directory where planning artifacts are stored. If not specified, defaults t… + + + + + Plan Model Routing + true + + + Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… + + + + + Max Chat Model Attempts + 10 + + + Maximum number of attempts for requests to the main chat model. Cannot exceed 10. + + + + + Debug Keystroke Logging + false + + + Enable debug logging of keystrokes to the console. + + + + + + + + + + Apply To + + + + User Settings + + + Workspace Settings + + + System Settings + + + + + (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-focused-on-scope-selector-correctly.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-focused-on-scope-selector-correctly.snap.svg new file mode 100644 index 0000000000..81d4868518 --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-focused-on-scope-selector-correctly.snap.svg @@ -0,0 +1,131 @@ + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + Settings + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + Search to filter + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + + + + + + + + Vim Mode + false + + + Enable Vim keybindings + + + + + Default Approval Mode + Default + + + The default approval mode for tool execution. 'default' prompts for approval, 'au… + + + + + Enable Auto Update + true + + + Enable automatic updates. + + + + + Enable Notifications + false + + + Enable run-event notifications for action-required prompts and session completion. … + + + + + Plan Directory + undefined + + + The directory where planning artifacts are stored. If not specified, defaults t… + + + + + Plan Model Routing + true + + + Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… + + + + + Max Chat Model Attempts + 10 + + + Maximum number of attempts for requests to the main chat model. Cannot exceed 10. + + + + + Debug Keystroke Logging + false + + + Enable debug logging of keystrokes to the console. + + + + + + + + + + > Apply To + + + + 1. + User Settings + + + 2. Workspace Settings + + + 3. System Settings + + + + + (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-mixed-boolean-and-number-settings-correctly.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-mixed-boolean-and-number-settings-correctly.snap.svg new file mode 100644 index 0000000000..324ed5c2cb --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-mixed-boolean-and-number-settings-correctly.snap.svg @@ -0,0 +1,132 @@ + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + > Settings + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + + S + earch to filter + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + + + + + + + + + Vim Mode + false* + + + Enable Vim keybindings + + + + + Default Approval Mode + Default + + + The default approval mode for tool execution. 'default' prompts for approval, 'au… + + + + + Enable Auto Update false* + + + Enable automatic updates. + + + + + Enable Notifications + false + + + Enable run-event notifications for action-required prompts and session completion. … + + + + + Plan Directory + undefined + + + The directory where planning artifacts are stored. If not specified, defaults t… + + + + + Plan Model Routing + true + + + Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… + + + + + Max Chat Model Attempts + 10 + + + Maximum number of attempts for requests to the main chat model. Cannot exceed 10. + + + + + Debug Keystroke Logging + false + + + Enable debug logging of keystrokes to the console. + + + + + + + + + + Apply To + + + + User Settings + + + Workspace Settings + + + System Settings + + + + + (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-tools-and-security-settings-correctly.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-tools-and-security-settings-correctly.snap.svg new file mode 100644 index 0000000000..b7ad1d10db --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-tools-and-security-settings-correctly.snap.svg @@ -0,0 +1,133 @@ + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + > Settings + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + + S + earch to filter + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + + + + + + + + + Vim Mode + false + + + Enable Vim keybindings + + + + + Default Approval Mode + Default + + + The default approval mode for tool execution. 'default' prompts for approval, 'au… + + + + + Enable Auto Update + true + + + Enable automatic updates. + + + + + Enable Notifications + false + + + Enable run-event notifications for action-required prompts and session completion. … + + + + + Plan Directory + undefined + + + The directory where planning artifacts are stored. If not specified, defaults t… + + + + + Plan Model Routing + true + + + Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… + + + + + Max Chat Model Attempts + 10 + + + Maximum number of attempts for requests to the main chat model. Cannot exceed 10. + + + + + Debug Keystroke Logging + false + + + Enable debug logging of keystrokes to the console. + + + + + + + + + + Apply To + + + + User Settings + + + Workspace Settings + + + System Settings + + + + + (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-various-boolean-settings-enabled-correctly.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-various-boolean-settings-enabled-correctly.snap.svg new file mode 100644 index 0000000000..e99a5b4cdd --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-various-boolean-settings-enabled-correctly.snap.svg @@ -0,0 +1,131 @@ + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + > Settings + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + + S + earch to filter + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + + + + + + + + + Vim Mode + true* + + + Enable Vim keybindings + + + + + Default Approval Mode + Default + + + The default approval mode for tool execution. 'default' prompts for approval, 'au… + + + + + Enable Auto Update false* + + + Enable automatic updates. + + + + + Enable Notifications + false + + + Enable run-event notifications for action-required prompts and session completion. … + + + + + Plan Directory + undefined + + + The directory where planning artifacts are stored. If not specified, defaults t… + + + + + Plan Model Routing + true + + + Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… + + + + + Max Chat Model Attempts + 10 + + + Maximum number of attempts for requests to the main chat model. Cannot exceed 10. + + + + + Debug Keystroke Logging true* + + + Enable debug logging of keystrokes to the console. + + + + + + + + + + Apply To + + + + User Settings + + + Workspace Settings + + + System Settings + + + + + (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) + + + + ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap index f1bd8d3852..be2dd8d9a2 100644 --- a/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap @@ -43,8 +43,7 @@ exports[`SettingsDialog > Initial Rendering > should render settings list with v │ │ │ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │ │ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ -" +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; exports[`SettingsDialog > Snapshot Tests > should render 'accessibility settings enabled' correctly 1`] = ` @@ -90,8 +89,7 @@ exports[`SettingsDialog > Snapshot Tests > should render 'accessibility settings │ │ │ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │ │ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ -" +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; exports[`SettingsDialog > Snapshot Tests > should render 'all boolean settings disabled' correctly 1`] = ` @@ -137,8 +135,7 @@ exports[`SettingsDialog > Snapshot Tests > should render 'all boolean settings d │ │ │ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │ │ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ -" +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; exports[`SettingsDialog > Snapshot Tests > should render 'default state' correctly 1`] = ` @@ -184,8 +181,7 @@ exports[`SettingsDialog > Snapshot Tests > should render 'default state' correct │ │ │ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │ │ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ -" +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; exports[`SettingsDialog > Snapshot Tests > should render 'file filtering settings configured' correctly 1`] = ` @@ -231,8 +227,7 @@ exports[`SettingsDialog > Snapshot Tests > should render 'file filtering setting │ │ │ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │ │ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ -" +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; exports[`SettingsDialog > Snapshot Tests > should render 'focused on scope selector' correctly 1`] = ` @@ -278,8 +273,7 @@ exports[`SettingsDialog > Snapshot Tests > should render 'focused on scope selec │ │ │ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │ │ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ -" +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; exports[`SettingsDialog > Snapshot Tests > should render 'mixed boolean and number settings' correctly 1`] = ` @@ -325,8 +319,7 @@ exports[`SettingsDialog > Snapshot Tests > should render 'mixed boolean and numb │ │ │ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │ │ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ -" +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; exports[`SettingsDialog > Snapshot Tests > should render 'tools and security settings' correctly 1`] = ` @@ -372,8 +365,7 @@ exports[`SettingsDialog > Snapshot Tests > should render 'tools and security set │ │ │ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │ │ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ -" +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; exports[`SettingsDialog > Snapshot Tests > should render 'various boolean settings enabled' correctly 1`] = ` @@ -419,6 +411,5 @@ exports[`SettingsDialog > Snapshot Tests > should render 'various boolean settin │ │ │ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │ │ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ -" +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; diff --git a/packages/cli/src/ui/components/__snapshots__/Table-Table-should-render-headers-and-data-correctly.snap.svg b/packages/cli/src/ui/components/__snapshots__/Table-Table-should-render-headers-and-data-correctly.snap.svg new file mode 100644 index 0000000000..6042642abd --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/Table-Table-should-render-headers-and-data-correctly.snap.svg @@ -0,0 +1,12 @@ + + + + + ID Name + ──────────────────────────────────────────────────────────────────────────────────────────────────── + 1 Alice + 2 Bob + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/__snapshots__/Table-Table-should-support-custom-cell-rendering.snap.svg b/packages/cli/src/ui/components/__snapshots__/Table-Table-should-support-custom-cell-rendering.snap.svg new file mode 100644 index 0000000000..359b4ee76d --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/Table-Table-should-support-custom-cell-rendering.snap.svg @@ -0,0 +1,11 @@ + + + + + Value + ──────────────────────────────────────────────────────────────────────────────────────────────────── + 20 + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/__snapshots__/Table-Table-should-support-inverse-text-rendering.snap.svg b/packages/cli/src/ui/components/__snapshots__/Table-Table-should-support-inverse-text-rendering.snap.svg new file mode 100644 index 0000000000..4473a2e810 --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/Table-Table-should-support-inverse-text-rendering.snap.svg @@ -0,0 +1,12 @@ + + + + + Status + ──────────────────────────────────────────────────────────────────────────────────────────────────── + + Active + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/__snapshots__/Table.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/Table.test.tsx.snap index 27a1e6e6f6..8356ef4345 100644 --- a/packages/cli/src/ui/components/__snapshots__/Table.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/Table.test.tsx.snap @@ -4,13 +4,17 @@ exports[`Table > should render headers and data correctly 1`] = ` "ID Name ──────────────────────────────────────────────────────────────────────────────────────────────────── 1 Alice -2 Bob -" +2 Bob" `; exports[`Table > should support custom cell rendering 1`] = ` "Value ──────────────────────────────────────────────────────────────────────────────────────────────────── -20 -" +20" +`; + +exports[`Table > should support inverse text rendering 1`] = ` +"Status +──────────────────────────────────────────────────────────────────────────────────────────────────── +Active" `; diff --git a/packages/cli/src/ui/components/messages/__snapshots__/UserMessage.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/UserMessage.test.tsx.snap index 9488a20ba3..679a5885d1 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/UserMessage.test.tsx.snap +++ b/packages/cli/src/ui/components/messages/__snapshots__/UserMessage.test.tsx.snap @@ -2,29 +2,29 @@ exports[`UserMessage > renders multiline user message 1`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - > Line 1 - Line 2 + > Line 1 + Line 2 ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ " `; exports[`UserMessage > renders normal user message with correct prefix 1`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - > Hello Gemini + > Hello Gemini ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ " `; exports[`UserMessage > renders slash command message 1`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - > /help + > /help ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ " `; exports[`UserMessage > transforms image paths in user message 1`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - > Check out this image: [Image my-image.png] + > Check out this image: [Image my-image.png] ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ " `; diff --git a/packages/cli/src/ui/components/shared/ExpandableText.test.tsx b/packages/cli/src/ui/components/shared/ExpandableText.test.tsx index 3634aafa8d..00c82a009d 100644 --- a/packages/cli/src/ui/components/shared/ExpandableText.test.tsx +++ b/packages/cli/src/ui/components/shared/ExpandableText.test.tsx @@ -4,7 +4,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import chalk from 'chalk'; import { describe, it, expect } from 'vitest'; import { render } from '../../../test-utils/render.js'; import { ExpandableText, MAX_WIDTH } from './ExpandableText.js'; @@ -14,7 +13,7 @@ describe('ExpandableText', () => { const flat = (s: string | undefined) => (s ?? '').replace(/\n/g, ''); it('renders plain label when no match (short label)', async () => { - const { lastFrame, waitUntilReady, unmount } = render( + const renderResult = render( { isExpanded={false} />, ); + const { waitUntilReady, unmount } = renderResult; await waitUntilReady(); - expect(lastFrame()).toMatchSnapshot(); + await expect(renderResult).toMatchSvgSnapshot(); unmount(); }); it('truncates long label when collapsed and no match', async () => { const long = 'x'.repeat(MAX_WIDTH + 25); - const { lastFrame, waitUntilReady, unmount } = render( + const renderResult = render( { isExpanded={false} />, ); + const { lastFrame, waitUntilReady, unmount } = renderResult; await waitUntilReady(); const out = lastFrame(); const f = flat(out); expect(f.endsWith('...')).toBe(true); expect(f.length).toBe(MAX_WIDTH + 3); - expect(out).toMatchSnapshot(); + await expect(renderResult).toMatchSvgSnapshot(); unmount(); }); it('shows full long label when expanded and no match', async () => { const long = 'y'.repeat(MAX_WIDTH + 25); - const { lastFrame, waitUntilReady, unmount } = render( + const renderResult = render( { isExpanded={true} />, ); + const { lastFrame, waitUntilReady, unmount } = renderResult; await waitUntilReady(); const out = lastFrame(); const f = flat(out); expect(f.length).toBe(long.length); - expect(out).toMatchSnapshot(); + await expect(renderResult).toMatchSvgSnapshot(); unmount(); }); @@ -69,7 +71,7 @@ describe('ExpandableText', () => { const label = 'run: git commit -m "feat: add search"'; const userInput = 'commit'; const matchedIndex = label.indexOf(userInput); - const { lastFrame, waitUntilReady, unmount } = render( + const renderResult = render( { />, 100, ); + const { waitUntilReady, unmount } = renderResult; await waitUntilReady(); - expect(lastFrame()).toMatchSnapshot(); - expect(lastFrame()).toContain(chalk.inverse(userInput)); + await expect(renderResult).toMatchSvgSnapshot(); unmount(); }); @@ -91,7 +93,7 @@ describe('ExpandableText', () => { const suffix = '/and/then/some/more/components/'.repeat(3); const label = prefix + core + suffix; const matchedIndex = prefix.length; - const { lastFrame, waitUntilReady, unmount } = render( + const renderResult = render( { />, 100, ); + const { lastFrame, waitUntilReady, unmount } = renderResult; await waitUntilReady(); const out = lastFrame(); const f = flat(out); expect(f.includes(core)).toBe(true); expect(f.startsWith('...')).toBe(true); expect(f.endsWith('...')).toBe(true); - expect(out).toMatchSnapshot(); + await expect(renderResult).toMatchSvgSnapshot(); unmount(); }); @@ -117,7 +120,7 @@ describe('ExpandableText', () => { const suffix = ' in this text'; const label = prefix + core + suffix; const matchedIndex = prefix.length; - const { lastFrame, waitUntilReady, unmount } = render( + const renderResult = render( { isExpanded={false} />, ); + const { lastFrame, waitUntilReady, unmount } = renderResult; await waitUntilReady(); const out = lastFrame(); const f = flat(out); @@ -133,14 +137,14 @@ describe('ExpandableText', () => { expect(f.startsWith('...')).toBe(false); expect(f.endsWith('...')).toBe(true); expect(f.length).toBe(MAX_WIDTH + 2); - expect(out).toMatchSnapshot(); + await expect(renderResult).toMatchSvgSnapshot(); unmount(); }); it('respects custom maxWidth', async () => { const customWidth = 50; const long = 'z'.repeat(100); - const { lastFrame, waitUntilReady, unmount } = render( + const renderResult = render( { maxWidth={customWidth} />, ); + const { lastFrame, waitUntilReady, unmount } = renderResult; await waitUntilReady(); const out = lastFrame(); const f = flat(out); expect(f.endsWith('...')).toBe(true); expect(f.length).toBe(customWidth + 3); - expect(out).toMatchSnapshot(); + await expect(renderResult).toMatchSvgSnapshot(); unmount(); }); }); diff --git a/packages/cli/src/ui/components/shared/__snapshots__/EnumSelector.test.tsx.snap b/packages/cli/src/ui/components/shared/__snapshots__/EnumSelector.test.tsx.snap index 8fd19b3868..203ceb61d6 100644 --- a/packages/cli/src/ui/components/shared/__snapshots__/EnumSelector.test.tsx.snap +++ b/packages/cli/src/ui/components/shared/__snapshots__/EnumSelector.test.tsx.snap @@ -11,7 +11,7 @@ exports[` > renders with numeric options and matches snapshot 1` `; exports[` > renders with single option and matches snapshot 1`] = ` -" Only Option +" Only Option " `; diff --git a/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-creates-centered-window-around-match-when-collapsed.snap.svg b/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-creates-centered-window-around-match-when-collapsed.snap.svg new file mode 100644 index 0000000000..1f6239e48c --- /dev/null +++ b/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-creates-centered-window-around-match-when-collapsed.snap.svg @@ -0,0 +1,13 @@ + + + + + ...ry/long/path/that/keeps/going/cd_/very/long/path/that/keeps/going/ + + search-here + /and/then/some/more/ + components//and/then/some/more/components//and/... + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-highlights-matched-substring-when-expanded-text-only-visible-.snap.svg b/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-highlights-matched-substring-when-expanded-text-only-visible-.snap.svg new file mode 100644 index 0000000000..67899017a3 --- /dev/null +++ b/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-highlights-matched-substring-when-expanded-text-only-visible-.snap.svg @@ -0,0 +1,12 @@ + + + + + run: git + + commit + -m "feat: add search" + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-renders-plain-label-when-no-match-short-label-.snap.svg b/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-renders-plain-label-when-no-match-short-label-.snap.svg new file mode 100644 index 0000000000..3d858a18af --- /dev/null +++ b/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-renders-plain-label-when-no-match-short-label-.snap.svg @@ -0,0 +1,9 @@ + + + + + simple command + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-respects-custom-maxWidth.snap.svg b/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-respects-custom-maxWidth.snap.svg new file mode 100644 index 0000000000..3bca3c74e9 --- /dev/null +++ b/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-respects-custom-maxWidth.snap.svg @@ -0,0 +1,9 @@ + + + + + zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz... + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-shows-full-long-label-when-expanded-and-no-match.snap.svg b/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-shows-full-long-label-when-expanded-and-no-match.snap.svg new file mode 100644 index 0000000000..283466b773 --- /dev/null +++ b/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-shows-full-long-label-when-expanded-and-no-match.snap.svg @@ -0,0 +1,10 @@ + + + + + yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy + yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-truncates-long-label-when-collapsed-and-no-match.snap.svg b/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-truncates-long-label-when-collapsed-and-no-match.snap.svg new file mode 100644 index 0000000000..79e13d7486 --- /dev/null +++ b/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-truncates-long-label-when-collapsed-and-no-match.snap.svg @@ -0,0 +1,10 @@ + + + + + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx... + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-truncates-match-itself-when-match-is-very-long.snap.svg b/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-truncates-match-itself-when-match-is-very-long.snap.svg new file mode 100644 index 0000000000..3eeb5c3250 --- /dev/null +++ b/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText-ExpandableText-truncates-match-itself-when-match-is-very-long.snap.svg @@ -0,0 +1,12 @@ + + + + + + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx... + + \ No newline at end of file diff --git a/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText.test.tsx.snap b/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText.test.tsx.snap index 7baf47e628..8716c962ea 100644 --- a/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText.test.tsx.snap +++ b/packages/cli/src/ui/components/shared/__snapshots__/ExpandableText.test.tsx.snap @@ -2,39 +2,26 @@ exports[`ExpandableText > creates centered window around match when collapsed 1`] = ` "...ry/long/path/that/keeps/going/cd_/very/long/path/that/keeps/going/search-here/and/then/some/more/ -components//and/then/some/more/components//and/... -" +components//and/then/some/more/components//and/..." `; -exports[`ExpandableText > highlights matched substring when expanded (text only visible) 1`] = ` -"run: git commit -m "feat: add search" -" -`; +exports[`ExpandableText > highlights matched substring when expanded (text only visible) 1`] = `"run: git commit -m "feat: add search""`; -exports[`ExpandableText > renders plain label when no match (short label) 1`] = ` -"simple command -" -`; +exports[`ExpandableText > renders plain label when no match (short label) 1`] = `"simple command"`; -exports[`ExpandableText > respects custom maxWidth 1`] = ` -"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz... -" -`; +exports[`ExpandableText > respects custom maxWidth 1`] = `"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz..."`; exports[`ExpandableText > shows full long label when expanded and no match 1`] = ` "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy -yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy -" +yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy" `; exports[`ExpandableText > truncates long label when collapsed and no match 1`] = ` "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx... -" +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx..." `; exports[`ExpandableText > truncates match itself when match is very long 1`] = ` "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx... -" +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx..." `; diff --git a/packages/cli/src/ui/components/shared/__snapshots__/HalfLinePaddedBox.test.tsx.snap b/packages/cli/src/ui/components/shared/__snapshots__/HalfLinePaddedBox.test.tsx.snap index 5dcbfda73d..dbb9af2991 100644 --- a/packages/cli/src/ui/components/shared/__snapshots__/HalfLinePaddedBox.test.tsx.snap +++ b/packages/cli/src/ui/components/shared/__snapshots__/HalfLinePaddedBox.test.tsx.snap @@ -2,7 +2,7 @@ exports[` > renders iTerm2-specific blocks when iTerm2 is detected 1`] = ` "▄▄▄▄▄▄▄▄▄▄ -Content +Content ▀▀▀▀▀▀▀▀▀▀ " `; @@ -19,7 +19,7 @@ exports[` > renders nothing when useBackgroundColor is fals exports[` > renders standard background and blocks when not iTerm2 1`] = ` "▀▀▀▀▀▀▀▀▀▀ -Content +Content ▄▄▄▄▄▄▄▄▄▄ " `; diff --git a/packages/cli/src/ui/components/shared/__snapshots__/SearchableList.test.tsx.snap b/packages/cli/src/ui/components/shared/__snapshots__/SearchableList.test.tsx.snap index 35f21daee3..803ec8dd98 100644 --- a/packages/cli/src/ui/components/shared/__snapshots__/SearchableList.test.tsx.snap +++ b/packages/cli/src/ui/components/shared/__snapshots__/SearchableList.test.tsx.snap @@ -7,7 +7,7 @@ exports[`SearchableList > should match snapshot 1`] = ` │ Search... │ ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ - ● Item One + ● Item One Description for item one Item Two @@ -28,7 +28,7 @@ exports[`SearchableList > should reset selection to top when items change if res Item One Description for item one - ● Item Two + ● Item Two Description for item two Item Three @@ -43,7 +43,7 @@ exports[`SearchableList > should reset selection to top when items change if res │ One │ ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ - ● Item One + ● Item One Description for item one " `; @@ -55,7 +55,7 @@ exports[`SearchableList > should reset selection to top when items change if res │ Search... │ ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ - ● Item One + ● Item One Description for item one Item Two diff --git a/packages/cli/src/ui/hooks/usePhraseCycler.test.tsx b/packages/cli/src/ui/hooks/usePhraseCycler.test.tsx index 837d953c3c..ca89c623ac 100644 --- a/packages/cli/src/ui/hooks/usePhraseCycler.test.tsx +++ b/packages/cli/src/ui/hooks/usePhraseCycler.test.tsx @@ -337,7 +337,7 @@ describe('usePhraseCycler', () => { await act(async () => { setStateExternally?.({ isActive: true, - customPhrases: [], + customPhrases: [] as string[], }); }); await waitUntilReady(); diff --git a/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx b/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx index 98a63b6838..8dddb69f82 100644 --- a/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx +++ b/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx @@ -235,7 +235,7 @@ Another paragraph. ); await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); - expect(lastFrame()).not.toContain(' 1 '); + expect(lastFrame()).not.toContain('1 const x = 1;'); unmount(); }); @@ -246,7 +246,7 @@ Another paragraph. ); await waitUntilReady(); expect(lastFrame()).toMatchSnapshot(); - expect(lastFrame()).toContain(' 1 '); + expect(lastFrame()).toContain('1 const x = 1;'); unmount(); }); }); diff --git a/packages/cli/src/zed-integration/zedIntegration.ts b/packages/cli/src/zed-integration/zedIntegration.ts index d4f1b27b92..e89a884ab5 100644 --- a/packages/cli/src/zed-integration/zedIntegration.ts +++ b/packages/cli/src/zed-integration/zedIntegration.ts @@ -1335,6 +1335,8 @@ function toAcpToolKind(kind: Kind): acp.ToolKind { case Kind.SwitchMode: case Kind.Other: return kind as acp.ToolKind; + case Kind.Agent: + return 'think'; case Kind.Plan: case Kind.Communicate: default: diff --git a/packages/cli/test-setup.ts b/packages/cli/test-setup.ts index 541f7a6a72..dc75dd217b 100644 --- a/packages/cli/test-setup.ts +++ b/packages/cli/test-setup.ts @@ -23,6 +23,9 @@ if (process.env.NO_COLOR !== undefined) { delete process.env.NO_COLOR; } +// Force true color output for ink so that snapshots always include color information. +process.env.FORCE_COLOR = '3'; + import './src/test-utils/customMatchers.js'; let consoleErrorSpy: vi.SpyInstance; diff --git a/packages/core/src/agents/subagent-tool-wrapper.test.ts b/packages/core/src/agents/subagent-tool-wrapper.test.ts index c4f3d178c9..e433e6f7d3 100644 --- a/packages/core/src/agents/subagent-tool-wrapper.test.ts +++ b/packages/core/src/agents/subagent-tool-wrapper.test.ts @@ -70,7 +70,7 @@ describe('SubagentToolWrapper', () => { expect(wrapper.name).toBe(mockDefinition.name); expect(wrapper.displayName).toBe(mockDefinition.displayName); expect(wrapper.description).toBe(mockDefinition.description); - expect(wrapper.kind).toBe(Kind.Think); + expect(wrapper.kind).toBe(Kind.Agent); expect(wrapper.isOutputMarkdown).toBe(true); expect(wrapper.canUpdateOutput).toBe(true); }); diff --git a/packages/core/src/agents/subagent-tool-wrapper.ts b/packages/core/src/agents/subagent-tool-wrapper.ts index 57ee929205..d0e94f1b4b 100644 --- a/packages/core/src/agents/subagent-tool-wrapper.ts +++ b/packages/core/src/agents/subagent-tool-wrapper.ts @@ -45,7 +45,7 @@ export class SubagentToolWrapper extends BaseDeclarativeTool< definition.name, definition.displayName ?? definition.name, definition.description, - Kind.Think, + Kind.Agent, definition.inputConfig.inputSchema, messageBus, /* isOutputMarkdown */ true, diff --git a/packages/core/src/agents/subagent-tool.test.ts b/packages/core/src/agents/subagent-tool.test.ts index d6d6bdfd89..40db4822a2 100644 --- a/packages/core/src/agents/subagent-tool.test.ts +++ b/packages/core/src/agents/subagent-tool.test.ts @@ -7,6 +7,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { SubagentTool } from './subagent-tool.js'; import { SubagentToolWrapper } from './subagent-tool-wrapper.js'; +import { Kind } from '../tools/tools.js'; import type { LocalAgentDefinition, RemoteAgentDefinition, @@ -70,6 +71,11 @@ describe('SubAgentInvocation', () => { .mockReturnValue(mockInnerInvocation); }); + it('should have Kind.Agent', () => { + const tool = new SubagentTool(testDefinition, mockConfig, mockMessageBus); + expect(tool.kind).toBe(Kind.Agent); + }); + it('should delegate shouldConfirmExecute to the inner sub-invocation (local)', async () => { const tool = new SubagentTool(testDefinition, mockConfig, mockMessageBus); const params = {}; diff --git a/packages/core/src/agents/subagent-tool.ts b/packages/core/src/agents/subagent-tool.ts index f47b506634..8584ae97f1 100644 --- a/packages/core/src/agents/subagent-tool.ts +++ b/packages/core/src/agents/subagent-tool.ts @@ -41,7 +41,7 @@ export class SubagentTool extends BaseDeclarativeTool { definition.name, definition.displayName ?? definition.name, definition.description, - Kind.Think, + Kind.Agent, inputSchema, messageBus, /* isOutputMarkdown */ true, diff --git a/packages/core/src/config/storage.ts b/packages/core/src/config/storage.ts index f66d60ef8b..e8530887b3 100644 --- a/packages/core/src/config/storage.ts +++ b/packages/core/src/config/storage.ts @@ -289,6 +289,10 @@ export class Storage { return path.join(this.getProjectTempDir(), 'plans'); } + getProjectTempTrackerDir(): string { + return path.join(this.getProjectTempDir(), 'tracker'); + } + getPlansDir(): string { if (this.customPlansDir) { const resolvedPath = path.resolve( diff --git a/packages/core/src/core/__snapshots__/prompts.test.ts.snap b/packages/core/src/core/__snapshots__/prompts.test.ts.snap index 94351e69e1..e789efa5d6 100644 --- a/packages/core/src/core/__snapshots__/prompts.test.ts.snap +++ b/packages/core/src/core/__snapshots__/prompts.test.ts.snap @@ -32,7 +32,7 @@ Use the following guidelines to optimize your search and read patterns. -- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include\` and \`exclude\` parameters). +- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include_pattern\` and \`exclude_pattern\` parameters). - **Searching and editing:** utilize search tools like grep_search with a conservative result count and a narrow scope. Use \`context\`, \`before\`, and/or \`after\` to request enough context to avoid the need to read the file before editing matches. - **Understanding:** minimize turns needed to understand a file. It's most efficient to read small files in their entirety. - **Large files:** utilize search tools like grep_search and/or read_file called in parallel with 'start_line' and 'end_line' to reduce the impact on context. Minimize extra turns, unless unavoidable due to the file being too large. @@ -184,7 +184,7 @@ Use the following guidelines to optimize your search and read patterns. -- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include\` and \`exclude\` parameters). +- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include_pattern\` and \`exclude_pattern\` parameters). - **Searching and editing:** utilize search tools like grep_search with a conservative result count and a narrow scope. Use \`context\`, \`before\`, and/or \`after\` to request enough context to avoid the need to read the file before editing matches. - **Understanding:** minimize turns needed to understand a file. It's most efficient to read small files in their entirety. - **Large files:** utilize search tools like grep_search and/or read_file called in parallel with 'start_line' and 'end_line' to reduce the impact on context. Minimize extra turns, unless unavoidable due to the file being too large. @@ -455,7 +455,7 @@ Use the following guidelines to optimize your search and read patterns. -- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include\` and \`exclude\` parameters). +- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include_pattern\` and \`exclude_pattern\` parameters). - **Searching and editing:** utilize search tools like grep_search with a conservative result count and a narrow scope. Use \`context\`, \`before\`, and/or \`after\` to request enough context to avoid the need to read the file before editing matches. - **Understanding:** minimize turns needed to understand a file. It's most efficient to read small files in their entirety. - **Large files:** utilize search tools like grep_search and/or read_file called in parallel with 'start_line' and 'end_line' to reduce the impact on context. Minimize extra turns, unless unavoidable due to the file being too large. @@ -607,7 +607,7 @@ Use the following guidelines to optimize your search and read patterns. -- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include\` and \`exclude\` parameters). +- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include_pattern\` and \`exclude_pattern\` parameters). - **Searching and editing:** utilize search tools like grep_search with a conservative result count and a narrow scope. Use \`context\`, \`before\`, and/or \`after\` to request enough context to avoid the need to read the file before editing matches. - **Understanding:** minimize turns needed to understand a file. It's most efficient to read small files in their entirety. - **Large files:** utilize search tools like grep_search and/or read_file called in parallel with 'start_line' and 'end_line' to reduce the impact on context. Minimize extra turns, unless unavoidable due to the file being too large. @@ -763,7 +763,7 @@ Use the following guidelines to optimize your search and read patterns. -- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include\` and \`exclude\` parameters). +- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include_pattern\` and \`exclude_pattern\` parameters). - **Searching and editing:** utilize search tools like grep_search with a conservative result count and a narrow scope. Use \`context\`, \`before\`, and/or \`after\` to request enough context to avoid the need to read the file before editing matches. - **Understanding:** minimize turns needed to understand a file. It's most efficient to read small files in their entirety. - **Large files:** utilize search tools like grep_search and/or read_file called in parallel with 'start_line' and 'end_line' to reduce the impact on context. Minimize extra turns, unless unavoidable due to the file being too large. @@ -885,7 +885,7 @@ Use the following guidelines to optimize your search and read patterns. -- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include\` and \`exclude\` parameters). +- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include_pattern\` and \`exclude_pattern\` parameters). - **Searching and editing:** utilize search tools like grep_search with a conservative result count and a narrow scope. Use \`context\`, \`before\`, and/or \`after\` to request enough context to avoid the need to read the file before editing matches. - **Understanding:** minimize turns needed to understand a file. It's most efficient to read small files in their entirety. - **Large files:** utilize search tools like grep_search and/or read_file called in parallel with 'start_line' and 'end_line' to reduce the impact on context. Minimize extra turns, unless unavoidable due to the file being too large. @@ -1480,7 +1480,7 @@ Use the following guidelines to optimize your search and read patterns. -- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include\` and \`exclude\` parameters). +- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include_pattern\` and \`exclude_pattern\` parameters). - **Searching and editing:** utilize search tools like grep_search with a conservative result count and a narrow scope. Use \`context\`, \`before\`, and/or \`after\` to request enough context to avoid the need to read the file before editing matches. - **Understanding:** minimize turns needed to understand a file. It's most efficient to read small files in their entirety. - **Large files:** utilize search tools like grep_search and/or read_file called in parallel with 'start_line' and 'end_line' to reduce the impact on context. Minimize extra turns, unless unavoidable due to the file being too large. @@ -1632,7 +1632,7 @@ Use the following guidelines to optimize your search and read patterns. -- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include\` and \`exclude\` parameters). +- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include_pattern\` and \`exclude_pattern\` parameters). - **Searching and editing:** utilize search tools like grep_search with a conservative result count and a narrow scope. Use \`context\`, \`before\`, and/or \`after\` to request enough context to avoid the need to read the file before editing matches. - **Understanding:** minimize turns needed to understand a file. It's most efficient to read small files in their entirety. - **Large files:** utilize search tools like grep_search and/or read_file called in parallel with 'start_line' and 'end_line' to reduce the impact on context. Minimize extra turns, unless unavoidable due to the file being too large. @@ -1775,7 +1775,7 @@ Use the following guidelines to optimize your search and read patterns. -- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include\` and \`exclude\` parameters). +- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include_pattern\` and \`exclude_pattern\` parameters). - **Searching and editing:** utilize search tools like grep_search with a conservative result count and a narrow scope. Use \`context\`, \`before\`, and/or \`after\` to request enough context to avoid the need to read the file before editing matches. - **Understanding:** minimize turns needed to understand a file. It's most efficient to read small files in their entirety. - **Large files:** utilize search tools like grep_search and/or read_file called in parallel with 'start_line' and 'end_line' to reduce the impact on context. Minimize extra turns, unless unavoidable due to the file being too large. @@ -1918,7 +1918,7 @@ Use the following guidelines to optimize your search and read patterns. -- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include\` and \`exclude\` parameters). +- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include_pattern\` and \`exclude_pattern\` parameters). - **Searching and editing:** utilize search tools like grep_search with a conservative result count and a narrow scope. Use \`context\`, \`before\`, and/or \`after\` to request enough context to avoid the need to read the file before editing matches. - **Understanding:** minimize turns needed to understand a file. It's most efficient to read small files in their entirety. - **Large files:** utilize search tools like grep_search and/or read_file called in parallel with 'start_line' and 'end_line' to reduce the impact on context. Minimize extra turns, unless unavoidable due to the file being too large. @@ -2057,7 +2057,7 @@ Use the following guidelines to optimize your search and read patterns. -- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include\` and \`exclude\` parameters). +- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include_pattern\` and \`exclude_pattern\` parameters). - **Searching and editing:** utilize search tools like grep_search with a conservative result count and a narrow scope. Use \`context\`, \`before\`, and/or \`after\` to request enough context to avoid the need to read the file before editing matches. - **Understanding:** minimize turns needed to understand a file. It's most efficient to read small files in their entirety. - **Large files:** utilize search tools like grep_search and/or read_file called in parallel with 'start_line' and 'end_line' to reduce the impact on context. Minimize extra turns, unless unavoidable due to the file being too large. @@ -2196,7 +2196,7 @@ Use the following guidelines to optimize your search and read patterns. -- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include\` and \`exclude\` parameters). +- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include_pattern\` and \`exclude_pattern\` parameters). - **Searching and editing:** utilize search tools like grep_search with a conservative result count and a narrow scope. Use \`context\`, \`before\`, and/or \`after\` to request enough context to avoid the need to read the file before editing matches. - **Understanding:** minimize turns needed to understand a file. It's most efficient to read small files in their entirety. - **Large files:** utilize search tools like grep_search and/or read_file called in parallel with 'start_line' and 'end_line' to reduce the impact on context. Minimize extra turns, unless unavoidable due to the file being too large. @@ -2327,7 +2327,7 @@ Use the following guidelines to optimize your search and read patterns. -- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include\` and \`exclude\` parameters). +- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include_pattern\` and \`exclude_pattern\` parameters). - **Searching and editing:** utilize search tools like grep_search with a conservative result count and a narrow scope. Use \`context\`, \`before\`, and/or \`after\` to request enough context to avoid the need to read the file before editing matches. - **Understanding:** minimize turns needed to understand a file. It's most efficient to read small files in their entirety. - **Large files:** utilize search tools like grep_search and/or read_file called in parallel with 'start_line' and 'end_line' to reduce the impact on context. Minimize extra turns, unless unavoidable due to the file being too large. @@ -2465,7 +2465,7 @@ Use the following guidelines to optimize your search and read patterns. -- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include\` and \`exclude\` parameters). +- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include_pattern\` and \`exclude_pattern\` parameters). - **Searching and editing:** utilize search tools like grep_search with a conservative result count and a narrow scope. Use \`context\`, \`before\`, and/or \`after\` to request enough context to avoid the need to read the file before editing matches. - **Understanding:** minimize turns needed to understand a file. It's most efficient to read small files in their entirety. - **Large files:** utilize search tools like grep_search and/or read_file called in parallel with 'start_line' and 'end_line' to reduce the impact on context. Minimize extra turns, unless unavoidable due to the file being too large. @@ -2845,7 +2845,7 @@ Use the following guidelines to optimize your search and read patterns. -- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include\` and \`exclude\` parameters). +- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include_pattern\` and \`exclude_pattern\` parameters). - **Searching and editing:** utilize search tools like grep_search with a conservative result count and a narrow scope. Use \`context\`, \`before\`, and/or \`after\` to request enough context to avoid the need to read the file before editing matches. - **Understanding:** minimize turns needed to understand a file. It's most efficient to read small files in their entirety. - **Large files:** utilize search tools like grep_search and/or read_file called in parallel with 'start_line' and 'end_line' to reduce the impact on context. Minimize extra turns, unless unavoidable due to the file being too large. @@ -2984,7 +2984,7 @@ Use the following guidelines to optimize your search and read patterns. -- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include\` and \`exclude\` parameters). +- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include_pattern\` and \`exclude_pattern\` parameters). - **Searching and editing:** utilize search tools like grep_search with a conservative result count and a narrow scope. Use \`context\`, \`before\`, and/or \`after\` to request enough context to avoid the need to read the file before editing matches. - **Understanding:** minimize turns needed to understand a file. It's most efficient to read small files in their entirety. - **Large files:** utilize search tools like grep_search and/or read_file called in parallel with 'start_line' and 'end_line' to reduce the impact on context. Minimize extra turns, unless unavoidable due to the file being too large. @@ -3235,7 +3235,7 @@ Use the following guidelines to optimize your search and read patterns. -- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include\` and \`exclude\` parameters). +- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include_pattern\` and \`exclude_pattern\` parameters). - **Searching and editing:** utilize search tools like grep_search with a conservative result count and a narrow scope. Use \`context\`, \`before\`, and/or \`after\` to request enough context to avoid the need to read the file before editing matches. - **Understanding:** minimize turns needed to understand a file. It's most efficient to read small files in their entirety. - **Large files:** utilize search tools like grep_search and/or read_file called in parallel with 'start_line' and 'end_line' to reduce the impact on context. Minimize extra turns, unless unavoidable due to the file being too large. @@ -3374,7 +3374,7 @@ Use the following guidelines to optimize your search and read patterns. -- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include\` and \`exclude\` parameters). +- **Searching:** utilize search tools like grep_search and glob with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include_pattern\` and \`exclude_pattern\` parameters). - **Searching and editing:** utilize search tools like grep_search with a conservative result count and a narrow scope. Use \`context\`, \`before\`, and/or \`after\` to request enough context to avoid the need to read the file before editing matches. - **Understanding:** minimize turns needed to understand a file. It's most efficient to read small files in their entirety. - **Large files:** utilize search tools like grep_search and/or read_file called in parallel with 'start_line' and 'end_line' to reduce the impact on context. Minimize extra turns, unless unavoidable due to the file being too large. diff --git a/packages/core/src/prompts/snippets.ts b/packages/core/src/prompts/snippets.ts index 6f1cb43985..a96842c3f1 100644 --- a/packages/core/src/prompts/snippets.ts +++ b/packages/core/src/prompts/snippets.ts @@ -189,7 +189,7 @@ Use the following guidelines to optimize your search and read patterns. -- **Searching:** utilize search tools like ${GREP_TOOL_NAME} and ${GLOB_TOOL_NAME} with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include\` and \`exclude\` parameters). +- **Searching:** utilize search tools like ${GREP_TOOL_NAME} and ${GLOB_TOOL_NAME} with a conservative result count (\`total_max_matches\`) and a narrow scope (\`include_pattern\` and \`exclude_pattern\` parameters). - **Searching and editing:** utilize search tools like ${GREP_TOOL_NAME} with a conservative result count and a narrow scope. Use \`context\`, \`before\`, and/or \`after\` to request enough context to avoid the need to read the file before editing matches. - **Understanding:** minimize turns needed to understand a file. It's most efficient to read small files in their entirety. - **Large files:** utilize search tools like ${GREP_TOOL_NAME} and/or ${READ_FILE_TOOL_NAME} called in parallel with 'start_line' and 'end_line' to reduce the impact on context. Minimize extra turns, unless unavoidable due to the file being too large. diff --git a/packages/core/src/services/trackerService.test.ts b/packages/core/src/services/trackerService.test.ts new file mode 100644 index 0000000000..70a29d25af --- /dev/null +++ b/packages/core/src/services/trackerService.test.ts @@ -0,0 +1,142 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import * as fs from 'node:fs/promises'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { TrackerService } from './trackerService.js'; +import { TaskStatus, TaskType, type TrackerTask } from './trackerTypes.js'; + +describe('TrackerService', () => { + let testTrackerDir: string; + let service: TrackerService; + + beforeEach(async () => { + testTrackerDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'tracker-service-test-'), + ); + service = new TrackerService(testTrackerDir); + }); + + afterEach(async () => { + await fs.rm(testTrackerDir, { recursive: true, force: true }); + }); + + it('should create a task with a generated 6-char hex ID', async () => { + const taskData: Omit = { + title: 'Test Task', + description: 'Test Description', + type: TaskType.TASK, + status: TaskStatus.OPEN, + dependencies: [], + }; + + const task = await service.createTask(taskData); + expect(task.id).toMatch(/^[0-9a-f]{6}$/); + expect(task.title).toBe(taskData.title); + + const savedTask = await service.getTask(task.id); + expect(savedTask).toEqual(task); + }); + + it('should list all tasks', async () => { + await service.createTask({ + title: 'Task 1', + description: 'Desc 1', + type: TaskType.TASK, + status: TaskStatus.OPEN, + dependencies: [], + }); + await service.createTask({ + title: 'Task 2', + description: 'Desc 2', + type: TaskType.TASK, + status: TaskStatus.OPEN, + dependencies: [], + }); + + const tasks = await service.listTasks(); + expect(tasks.length).toBe(2); + expect(tasks.map((t) => t.title)).toContain('Task 1'); + expect(tasks.map((t) => t.title)).toContain('Task 2'); + }); + + it('should update a task', async () => { + const task = await service.createTask({ + title: 'Original Title', + description: 'Original Desc', + type: TaskType.TASK, + status: TaskStatus.OPEN, + dependencies: [], + }); + + const updated = await service.updateTask(task.id, { + title: 'New Title', + status: TaskStatus.IN_PROGRESS, + }); + expect(updated.title).toBe('New Title'); + expect(updated.status).toBe('in_progress'); + expect(updated.description).toBe('Original Desc'); + + const retrieved = await service.getTask(task.id); + expect(retrieved).toEqual(updated); + }); + + it('should prevent closing a task if dependencies are not closed', async () => { + const dep = await service.createTask({ + title: 'Dependency', + description: 'Must be closed first', + type: TaskType.TASK, + status: TaskStatus.OPEN, + dependencies: [], + }); + + const task = await service.createTask({ + title: 'Main Task', + description: 'Depends on dep', + type: TaskType.TASK, + status: TaskStatus.OPEN, + dependencies: [dep.id], + }); + + await expect( + service.updateTask(task.id, { status: TaskStatus.CLOSED }), + ).rejects.toThrow(/Cannot close task/); + + // Close dependency + await service.updateTask(dep.id, { status: TaskStatus.CLOSED }); + + // Now it should work + const updated = await service.updateTask(task.id, { + status: TaskStatus.CLOSED, + }); + expect(updated.status).toBe('closed'); + }); + + it('should detect circular dependencies', async () => { + const taskA = await service.createTask({ + title: 'Task A', + description: 'A', + type: TaskType.TASK, + status: TaskStatus.OPEN, + dependencies: [], + }); + + const taskB = await service.createTask({ + title: 'Task B', + description: 'B', + type: TaskType.TASK, + status: TaskStatus.OPEN, + dependencies: [taskA.id], + }); + + // Try to make A depend on B + await expect( + service.updateTask(taskA.id, { dependencies: [taskB.id] }), + ).rejects.toThrow(/Circular dependency detected/); + }); +}); diff --git a/packages/core/src/services/trackerService.ts b/packages/core/src/services/trackerService.ts new file mode 100644 index 0000000000..3203b759e1 --- /dev/null +++ b/packages/core/src/services/trackerService.ts @@ -0,0 +1,223 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { randomBytes } from 'node:crypto'; +import { debugLogger } from '../utils/debugLogger.js'; +import { coreEvents } from '../utils/events.js'; +import { + TrackerTaskSchema, + TaskStatus, + type TrackerTask, +} from './trackerTypes.js'; +import { type z } from 'zod'; + +export class TrackerService { + private readonly tasksDir: string; + + private initialized = false; + + constructor(readonly trackerDir: string) { + this.tasksDir = trackerDir; + } + + private async ensureInitialized(): Promise { + if (!this.initialized) { + await fs.mkdir(this.tasksDir, { recursive: true }); + this.initialized = true; + } + } + + /** + * Generates a 6-character hex ID. + */ + private generateId(): string { + return randomBytes(3).toString('hex'); + } + + /** + * Creates a new task and saves it to disk. + */ + async createTask(taskData: Omit): Promise { + await this.ensureInitialized(); + const id = this.generateId(); + const task: TrackerTask = { + ...taskData, + id, + }; + + await this.saveTask(task); + return task; + } + + /** + * Helper to read and validate a JSON file. + */ + private async readJsonFile( + filePath: string, + schema: z.ZodSchema, + ): Promise { + try { + const content = await fs.readFile(filePath, 'utf8'); + const data: unknown = JSON.parse(content); + return schema.parse(data); + } catch (error) { + if ( + error && + typeof error === 'object' && + 'code' in error && + error.code === 'ENOENT' + ) { + return null; + } + + const fileName = path.basename(filePath); + debugLogger.warn(`Failed to read or parse task file ${fileName}:`, error); + coreEvents.emitFeedback( + 'warning', + `Task tracker encountered an issue reading ${fileName}. The data might be corrupted.`, + error, + ); + throw error; + } + } + + /** + * Reads a task by ID. + */ + async getTask(id: string): Promise { + await this.ensureInitialized(); + const taskPath = path.join(this.tasksDir, `${id}.json`); + return this.readJsonFile(taskPath, TrackerTaskSchema); + } + + /** + * Lists all tasks in the tracker. + */ + async listTasks(): Promise { + await this.ensureInitialized(); + try { + const files = await fs.readdir(this.tasksDir); + const jsonFiles = files.filter((f: string) => f.endsWith('.json')); + const tasks = await Promise.all( + jsonFiles.map(async (f: string) => { + const taskPath = path.join(this.tasksDir, f); + return this.readJsonFile(taskPath, TrackerTaskSchema); + }), + ); + return tasks.filter((t): t is TrackerTask => t !== null); + } catch (error) { + if ( + error && + typeof error === 'object' && + 'code' in error && + error.code === 'ENOENT' + ) { + return []; + } + throw error; + } + } + + /** + * Updates an existing task and saves it to disk. + */ + async updateTask( + id: string, + updates: Partial, + ): Promise { + const task = await this.getTask(id); + if (!task) { + throw new Error(`Task with ID ${id} not found.`); + } + + const updatedTask = { ...task, ...updates }; + + // Validate status transition if closing + if ( + updatedTask.status === TaskStatus.CLOSED && + task.status !== TaskStatus.CLOSED + ) { + await this.validateCanClose(updatedTask); + } + + // Validate circular dependencies if dependencies changed + if (updates.dependencies) { + await this.validateNoCircularDependencies(updatedTask); + } + + await this.saveTask(updatedTask); + return updatedTask; + } + + /** + * Saves a task to disk. + */ + private async saveTask(task: TrackerTask): Promise { + const taskPath = path.join(this.tasksDir, `${task.id}.json`); + await fs.writeFile(taskPath, JSON.stringify(task, null, 2), 'utf8'); + } + + /** + * Validates that a task can be closed (all dependencies must be closed). + */ + private async validateCanClose(task: TrackerTask): Promise { + for (const depId of task.dependencies) { + const dep = await this.getTask(depId); + if (!dep) { + throw new Error(`Dependency ${depId} not found for task ${task.id}.`); + } + if (dep.status !== TaskStatus.CLOSED) { + throw new Error( + `Cannot close task ${task.id} because dependency ${depId} is still ${dep.status}.`, + ); + } + } + } + + /** + * Validates that there are no circular dependencies. + */ + private async validateNoCircularDependencies( + task: TrackerTask, + ): Promise { + const allTasks = await this.listTasks(); + const taskMap = new Map( + allTasks.map((t) => [t.id, t]), + ); + // Ensure the current (possibly unsaved) task state is used + taskMap.set(task.id, task); + + const visited = new Set(); + const stack = new Set(); + + const check = (currentId: string) => { + if (stack.has(currentId)) { + throw new Error( + `Circular dependency detected involving task ${currentId}.`, + ); + } + if (visited.has(currentId)) { + return; + } + + visited.add(currentId); + stack.add(currentId); + + const currentTask = taskMap.get(currentId); + if (currentTask) { + for (const depId of currentTask.dependencies) { + check(depId); + } + } + + stack.delete(currentId); + }; + + check(task.id); + } +} diff --git a/packages/core/src/services/trackerTypes.ts b/packages/core/src/services/trackerTypes.ts new file mode 100644 index 0000000000..7c48f5bcd4 --- /dev/null +++ b/packages/core/src/services/trackerTypes.ts @@ -0,0 +1,36 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { z } from 'zod'; + +export enum TaskType { + EPIC = 'epic', + TASK = 'task', + BUG = 'bug', +} +export const TaskTypeSchema = z.nativeEnum(TaskType); + +export enum TaskStatus { + OPEN = 'open', + IN_PROGRESS = 'in_progress', + BLOCKED = 'blocked', + CLOSED = 'closed', +} +export const TaskStatusSchema = z.nativeEnum(TaskStatus); + +export const TrackerTaskSchema = z.object({ + id: z.string().length(6), + title: z.string(), + description: z.string(), + type: TaskTypeSchema, + status: TaskStatusSchema, + parentId: z.string().optional(), + dependencies: z.array(z.string()), + subagentSessionId: z.string().optional(), + metadata: z.record(z.unknown()).optional(), +}); + +export type TrackerTask = z.infer; diff --git a/packages/core/src/tools/definitions/__snapshots__/coreToolsModelSnapshots.test.ts.snap b/packages/core/src/tools/definitions/__snapshots__/coreToolsModelSnapshots.test.ts.snap index effab9144d..2d5cfe8d52 100644 --- a/packages/core/src/tools/definitions/__snapshots__/coreToolsModelSnapshots.test.ts.snap +++ b/packages/core/src/tools/definitions/__snapshots__/coreToolsModelSnapshots.test.ts.snap @@ -266,7 +266,7 @@ exports[`coreTools snapshots for specific models > Model: gemini-2.5-pro > snaps "description": "Optional: A regular expression pattern to exclude from the search results. If a line matches both the pattern and the exclude_pattern, it will be omitted.", "type": "string", }, - "include": { + "include_pattern": { "description": "Optional: A glob pattern to filter which files are searched (e.g., '*.js', '*.{ts,tsx}', 'src/**'). If omitted, searches all files (respecting potential global ignores).", "type": "string", }, @@ -333,7 +333,7 @@ exports[`coreTools snapshots for specific models > Model: gemini-2.5-pro > snaps "description": "If true, treats the \`pattern\` as a literal string instead of a regular expression. Defaults to false (basic regex) if omitted.", "type": "boolean", }, - "include": { + "include_pattern": { "description": "Glob pattern to filter files (e.g., '*.ts', 'src/**'). Recommended for large repositories to reduce noise. Defaults to all files if omitted.", "type": "string", }, @@ -1053,7 +1053,7 @@ exports[`coreTools snapshots for specific models > Model: gemini-3-pro-preview > "description": "Optional: A regular expression pattern to exclude from the search results. If a line matches both the pattern and the exclude_pattern, it will be omitted.", "type": "string", }, - "include": { + "include_pattern": { "description": "Optional: A glob pattern to filter which files are searched (e.g., '*.js', '*.{ts,tsx}', 'src/**'). If omitted, searches all files (respecting potential global ignores).", "type": "string", }, @@ -1120,7 +1120,7 @@ exports[`coreTools snapshots for specific models > Model: gemini-3-pro-preview > "description": "If true, treats the \`pattern\` as a literal string instead of a regular expression. Defaults to false (basic regex) if omitted.", "type": "boolean", }, - "include": { + "include_pattern": { "description": "Glob pattern to filter files (e.g., '*.ts', 'src/**'). Recommended for large repositories to reduce noise. Defaults to all files if omitted.", "type": "string", }, diff --git a/packages/core/src/tools/definitions/model-family-sets/default-legacy.ts b/packages/core/src/tools/definitions/model-family-sets/default-legacy.ts index 569f379cd0..23f36fbb24 100644 --- a/packages/core/src/tools/definitions/model-family-sets/default-legacy.ts +++ b/packages/core/src/tools/definitions/model-family-sets/default-legacy.ts @@ -96,7 +96,7 @@ export const DEFAULT_LEGACY_SET: CoreToolSet = { 'Optional: The absolute path to the directory to search within. If omitted, searches the current working directory.', type: 'string', }, - include: { + include_pattern: { description: `Optional: A glob pattern to filter which files are searched (e.g., '*.js', '*.{ts,tsx}', 'src/**'). If omitted, searches all files (respecting potential global ignores).`, type: 'string', }, @@ -143,7 +143,7 @@ export const DEFAULT_LEGACY_SET: CoreToolSet = { "Directory or file to search. Directories are searched recursively. Relative paths are resolved against current working directory. Defaults to current working directory ('.') if omitted.", type: 'string', }, - include: { + include_pattern: { description: "Glob pattern to filter files (e.g., '*.ts', 'src/**'). Recommended for large repositories to reduce noise. Defaults to all files if omitted.", type: 'string', diff --git a/packages/core/src/tools/definitions/model-family-sets/gemini-3.ts b/packages/core/src/tools/definitions/model-family-sets/gemini-3.ts index 0cfe8ffbc2..1d50eae7e8 100644 --- a/packages/core/src/tools/definitions/model-family-sets/gemini-3.ts +++ b/packages/core/src/tools/definitions/model-family-sets/gemini-3.ts @@ -97,7 +97,7 @@ export const GEMINI_3_SET: CoreToolSet = { 'Optional: The absolute path to the directory to search within. If omitted, searches the current working directory.', type: 'string', }, - include: { + include_pattern: { description: `Optional: A glob pattern to filter which files are searched (e.g., '*.js', '*.{ts,tsx}', 'src/**'). If omitted, searches all files (respecting potential global ignores).`, type: 'string', }, @@ -144,7 +144,7 @@ export const GEMINI_3_SET: CoreToolSet = { "Directory or file to search. Directories are searched recursively. Relative paths are resolved against current working directory. Defaults to current working directory ('.') if omitted.", type: 'string', }, - include: { + include_pattern: { description: "Glob pattern to filter files (e.g., '*.ts', 'src/**'). Recommended for large repositories to reduce noise. Defaults to all files if omitted.", type: 'string', diff --git a/packages/core/src/tools/grep-utils.ts b/packages/core/src/tools/grep-utils.ts index 27c744f60c..6dd2cdc83e 100644 --- a/packages/core/src/tools/grep-utils.ts +++ b/packages/core/src/tools/grep-utils.ts @@ -139,7 +139,7 @@ export async function formatGrepResults( params: { pattern: string; names_only?: boolean; - include?: string; + include_pattern?: string; // Context params to determine if auto-context should be skipped context?: number; before?: number; @@ -148,10 +148,10 @@ export async function formatGrepResults( searchLocationDescription: string, totalMaxMatches: number, ): Promise<{ llmContent: string; returnDisplay: string }> { - const { pattern, names_only, include } = params; + const { pattern, names_only, include_pattern } = params; if (allMatches.length === 0) { - const noMatchMsg = `No matches found for pattern "${pattern}" ${searchLocationDescription}${include ? ` (filter: "${include}")` : ''}.`; + const noMatchMsg = `No matches found for pattern "${pattern}" ${searchLocationDescription}${include_pattern ? ` (filter: "${include_pattern}")` : ''}.`; return { llmContent: noMatchMsg, returnDisplay: `No matches found` }; } @@ -171,7 +171,7 @@ export async function formatGrepResults( if (names_only) { const filePaths = Object.keys(matchesByFile).sort(); let llmContent = `Found ${filePaths.length} files with matches for pattern "${pattern}" ${searchLocationDescription}${ - include ? ` (filter: "${include}")` : '' + include_pattern ? ` (filter: "${include_pattern}")` : '' }${ wasTruncated ? ` (results limited to ${totalMaxMatches} matches for performance)` @@ -184,7 +184,7 @@ export async function formatGrepResults( }; } - let llmContent = `Found ${matchCount} ${matchTerm} for pattern "${pattern}" ${searchLocationDescription}${include ? ` (filter: "${include}")` : ''}`; + let llmContent = `Found ${matchCount} ${matchTerm} for pattern "${pattern}" ${searchLocationDescription}${include_pattern ? ` (filter: "${include_pattern}")` : ''}`; if (wasTruncated) { llmContent += ` (results limited to ${totalMaxMatches} matches for performance)`; diff --git a/packages/core/src/tools/grep.test.ts b/packages/core/src/tools/grep.test.ts index f696495253..6f98b0f2fc 100644 --- a/packages/core/src/tools/grep.test.ts +++ b/packages/core/src/tools/grep.test.ts @@ -118,7 +118,7 @@ describe('GrepTool', () => { const params: GrepToolParams = { pattern: 'hello', dir_path: '.', - include: '*.txt', + include_pattern: '*.txt', }; expect(grepTool.validateToolParams(params)).toBeNull(); }); @@ -226,7 +226,10 @@ describe('GrepTool', () => { }, 30000); it('should find matches with an include glob', async () => { - const params: GrepToolParams = { pattern: 'hello', include: '*.js' }; + const params: GrepToolParams = { + pattern: 'hello', + include_pattern: '*.js', + }; const invocation = grepTool.build(params); const result = await invocation.execute(abortSignal); expect(result.llmContent).toContain( @@ -247,7 +250,7 @@ describe('GrepTool', () => { const params: GrepToolParams = { pattern: 'hello', dir_path: 'sub', - include: '*.js', + include_pattern: '*.js', }; const invocation = grepTool.build(params); const result = await invocation.execute(abortSignal); @@ -572,7 +575,7 @@ describe('GrepTool', () => { it('should generate correct description with pattern and include', () => { const params: GrepToolParams = { pattern: 'testPattern', - include: '*.ts', + include_pattern: '*.ts', }; const invocation = grepTool.build(params); expect(invocation.getDescription()).toBe("'testPattern' in *.ts"); @@ -618,7 +621,7 @@ describe('GrepTool', () => { await fs.mkdir(dirPath, { recursive: true }); const params: GrepToolParams = { pattern: 'testPattern', - include: '*.ts', + include_pattern: '*.ts', dir_path: path.join('src', 'app'), }; const invocation = grepTool.build(params); diff --git a/packages/core/src/tools/grep.ts b/packages/core/src/tools/grep.ts index 92fe58288d..3d74521513 100644 --- a/packages/core/src/tools/grep.ts +++ b/packages/core/src/tools/grep.ts @@ -48,7 +48,7 @@ export interface GrepToolParams { /** * File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}") */ - include?: string; + include_pattern?: string; /** * Optional: A regular expression pattern to exclude from the search results. @@ -227,7 +227,7 @@ class GrepToolInvocation extends BaseToolInvocation< const matches = await this.performGrepSearch({ pattern: this.params.pattern, path: searchDir, - include: this.params.include, + include_pattern: this.params.include_pattern, exclude_pattern: this.params.exclude_pattern, maxMatches: remainingLimit, max_matches_per_file: this.params.max_matches_per_file, @@ -317,7 +317,7 @@ class GrepToolInvocation extends BaseToolInvocation< private async performGrepSearch(options: { pattern: string; path: string; // Expects absolute path - include?: string; + include_pattern?: string; exclude_pattern?: string; maxMatches: number; max_matches_per_file?: number; @@ -326,7 +326,7 @@ class GrepToolInvocation extends BaseToolInvocation< const { pattern, path: absolutePath, - include, + include_pattern, exclude_pattern, maxMatches, max_matches_per_file, @@ -356,8 +356,8 @@ class GrepToolInvocation extends BaseToolInvocation< if (max_matches_per_file) { gitArgs.push('--max-count', max_matches_per_file.toString()); } - if (include) { - gitArgs.push('--', include); + if (include_pattern) { + gitArgs.push('--', include_pattern); } try { @@ -424,8 +424,8 @@ class GrepToolInvocation extends BaseToolInvocation< if (max_matches_per_file) { grepArgs.push('--max-count', max_matches_per_file.toString()); } - if (include) { - grepArgs.push(`--include=${include}`); + if (include_pattern) { + grepArgs.push(`--include=${include_pattern}`); } grepArgs.push(pattern); grepArgs.push('.'); @@ -471,7 +471,7 @@ class GrepToolInvocation extends BaseToolInvocation< 'GrepLogic: Falling back to JavaScript grep implementation.', ); strategyUsed = 'javascript fallback'; - const globPattern = include ? include : '**/*'; + const globPattern = include_pattern ? include_pattern : '**/*'; const ignorePatterns = this.fileExclusions.getGlobExcludes(); const filesStream = globStream(globPattern, { @@ -551,8 +551,8 @@ class GrepToolInvocation extends BaseToolInvocation< getDescription(): string { let description = `'${this.params.pattern}'`; - if (this.params.include) { - description += ` in ${this.params.include}`; + if (this.params.include_pattern) { + description += ` in ${this.params.include_pattern}`; } if (this.params.dir_path) { const resolvedPath = path.resolve( diff --git a/packages/core/src/tools/ripGrep.test.ts b/packages/core/src/tools/ripGrep.test.ts index 58842e9b22..0eaf5c0b68 100644 --- a/packages/core/src/tools/ripGrep.test.ts +++ b/packages/core/src/tools/ripGrep.test.ts @@ -350,7 +350,7 @@ describe('RipGrepTool', () => { }, { name: 'pattern, path, and include', - params: { pattern: 'hello', dir_path: '.', include: '*.txt' }, + params: { pattern: 'hello', dir_path: '.', include_pattern: '*.txt' }, expected: null, }, ])( @@ -526,7 +526,10 @@ describe('RipGrepTool', () => { }), ); - const params: RipGrepToolParams = { pattern: 'hello', include: '*.js' }; + const params: RipGrepToolParams = { + pattern: 'hello', + include_pattern: '*.js', + }; const invocation = grepTool.build(params); const result = await invocation.execute(abortSignal); expect(result.llmContent).toContain( @@ -564,7 +567,7 @@ describe('RipGrepTool', () => { const params: RipGrepToolParams = { pattern: 'hello', dir_path: 'sub', - include: '*.js', + include_pattern: '*.js', }; const invocation = grepTool.build(params); const result = await invocation.execute(abortSignal); @@ -1314,7 +1317,7 @@ describe('RipGrepTool', () => { const params: RipGrepToolParams = { pattern: 'content', - include: '*.{ts,tsx}', + include_pattern: '*.{ts,tsx}', }; const invocation = grepTool.build(params); const result = await invocation.execute(abortSignal); @@ -1350,7 +1353,7 @@ describe('RipGrepTool', () => { const params: RipGrepToolParams = { pattern: 'code', - include: 'src/**', + include_pattern: 'src/**', }; const invocation = grepTool.build(params); const result = await invocation.execute(abortSignal); @@ -1774,7 +1777,7 @@ describe('RipGrepTool', () => { }, { name: 'pattern and include', - params: { pattern: 'testPattern', include: '*.ts' }, + params: { pattern: 'testPattern', include_pattern: '*.ts' }, expected: "'testPattern' in *.ts within ./", }, { @@ -1849,7 +1852,7 @@ describe('RipGrepTool', () => { await fs.mkdir(dirPath, { recursive: true }); const params: RipGrepToolParams = { pattern: 'testPattern', - include: '*.ts', + include_pattern: '*.ts', dir_path: path.join('src', 'app'), }; const invocation = grepTool.build(params); diff --git a/packages/core/src/tools/ripGrep.ts b/packages/core/src/tools/ripGrep.ts index 9ad929f256..ac65cf6362 100644 --- a/packages/core/src/tools/ripGrep.ts +++ b/packages/core/src/tools/ripGrep.ts @@ -103,7 +103,7 @@ export interface RipGrepToolParams { /** * File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}") */ - include?: string; + include_pattern?: string; /** * Optional: A regular expression pattern to exclude from the search results. @@ -246,7 +246,7 @@ class GrepToolInvocation extends BaseToolInvocation< allMatches = await this.performRipgrepSearch({ pattern: this.params.pattern, path: searchDirAbs, - include: this.params.include, + include_pattern: this.params.include_pattern, exclude_pattern: this.params.exclude_pattern, case_sensitive: this.params.case_sensitive, fixed_strings: this.params.fixed_strings, @@ -329,7 +329,7 @@ class GrepToolInvocation extends BaseToolInvocation< pattern: this.params.pattern, path: uniqueFiles, basePath: searchDirAbs, - include: this.params.include, + include_pattern: this.params.include_pattern, exclude_pattern: this.params.exclude_pattern, case_sensitive: this.params.case_sensitive, fixed_strings: this.params.fixed_strings, @@ -360,7 +360,7 @@ class GrepToolInvocation extends BaseToolInvocation< pattern: string; path: string | string[]; basePath?: string; - include?: string; + include_pattern?: string; exclude_pattern?: string; case_sensitive?: boolean; fixed_strings?: boolean; @@ -376,7 +376,7 @@ class GrepToolInvocation extends BaseToolInvocation< pattern, path, basePath, - include, + include_pattern, exclude_pattern, case_sensitive, fixed_strings, @@ -419,8 +419,8 @@ class GrepToolInvocation extends BaseToolInvocation< rgArgs.push('--max-count', max_matches_per_file.toString()); } - if (include) { - rgArgs.push('--glob', include); + if (include_pattern) { + rgArgs.push('--glob', include_pattern); } if (!no_ignore) { @@ -543,8 +543,8 @@ class GrepToolInvocation extends BaseToolInvocation< */ getDescription(): string { let description = `'${this.params.pattern}'`; - if (this.params.include) { - description += ` in ${this.params.include}`; + if (this.params.include_pattern) { + description += ` in ${this.params.include_pattern}`; } const pathParam = this.params.dir_path || '.'; const resolvedPath = path.resolve(this.config.getTargetDir(), pathParam); diff --git a/packages/core/src/tools/tools.ts b/packages/core/src/tools/tools.ts index d847b596e0..3c024168d4 100644 --- a/packages/core/src/tools/tools.ts +++ b/packages/core/src/tools/tools.ts @@ -832,6 +832,7 @@ export enum Kind { Search = 'search', Execute = 'execute', Think = 'think', + Agent = 'agent', Fetch = 'fetch', Communicate = 'communicate', Plan = 'plan', diff --git a/packages/core/src/utils/fileUtils.test.ts b/packages/core/src/utils/fileUtils.test.ts index c2f413a27e..de668db3ad 100644 --- a/packages/core/src/utils/fileUtils.test.ts +++ b/packages/core/src/utils/fileUtils.test.ts @@ -20,7 +20,7 @@ import fsPromises from 'node:fs/promises'; import path from 'node:path'; import os from 'node:os'; import { fileURLToPath } from 'node:url'; -// eslint-disable-next-line import/no-internal-modules + import mime from 'mime/lite'; import { diff --git a/packages/core/src/utils/fileUtils.ts b/packages/core/src/utils/fileUtils.ts index a5b32a3cb4..42119c3f18 100644 --- a/packages/core/src/utils/fileUtils.ts +++ b/packages/core/src/utils/fileUtils.ts @@ -8,7 +8,7 @@ import fs from 'node:fs'; import fsPromises from 'node:fs/promises'; import path from 'node:path'; import type { PartUnion } from '@google/genai'; -// eslint-disable-next-line import/no-internal-modules + import mime from 'mime/lite'; import type { FileSystemService } from '../services/fileSystemService.js'; import { ToolErrorType } from '../tools/tool-error.js'; diff --git a/packages/core/src/utils/schemaValidator.ts b/packages/core/src/utils/schemaValidator.ts index e58b7b8d9b..db5dee11ba 100644 --- a/packages/core/src/utils/schemaValidator.ts +++ b/packages/core/src/utils/schemaValidator.ts @@ -6,7 +6,7 @@ import AjvPkg, { type AnySchema, type Ajv } from 'ajv'; // Ajv2020 is the documented way to use draft-2020-12: https://ajv.js.org/json-schema.html#draft-2020-12 -// eslint-disable-next-line import/no-internal-modules + import Ajv2020Pkg from 'ajv/dist/2020.js'; import * as addFormats from 'ajv-formats'; import { debugLogger } from './debugLogger.js';