Compare commits

...

19 Commits

Author SHA1 Message Date
galz10 8efd36648f revert: fix(security): enforce strict policy directory permissions (#17353) 2026-02-13 13:57:30 -08:00
Adib234 0b3130cec7 fix(plan): isolate plan files per session (#18757) 2026-02-12 19:02:59 +00:00
Adam Weidman d243dfce14 chore: cleanup unused and add unlisted dependencies in packages/a2a-server (#18916) 2026-02-12 18:40:52 +00:00
Sandy Tao 2e91c03e08 feat: add strict seatbelt profiles and remove unusable closed profiles (#18876) 2026-02-12 18:33:54 +00:00
Sandy Tao 2d38623472 fix(github-actions): use robot PAT for release creation to trigger release notes (#18794) 2026-02-12 18:01:04 +00:00
Tommaso Sciortino 375ebca2da feat(cli): support Ctrl-Z suspension (#18931)
Co-authored-by: Bharat Kunwar <brtkwr@gmail.com>
2026-02-12 17:55:56 +00:00
Adib234 868f43927e feat(plan): create metrics for usage of AskUser tool (#18820)
Co-authored-by: Jerop Kipruto <jerop@google.com>
2026-02-12 17:46:59 +00:00
N. Taylor Mullen 27a1bae03b feat(core): refine Plan Mode system prompt for agentic execution (#18799) 2026-02-12 17:37:47 +00:00
Abhijit Balaji ddcfe5b1f2 fix(core): prioritize conditional policy rules and harden Plan Mode (#18882) 2026-02-12 17:04:39 +00:00
Dmitry Lyalin f603f4a12b fix(cli): dismiss '?' shortcuts help on hotkeys and active states (#18583)
Co-authored-by: jacob314 <jacob314@gmail.com>
2026-02-12 16:35:40 +00:00
christine betts 2ca183ffc9 Show notification when there's a conflict with an extensions command (#17890) 2026-02-12 16:29:06 +00:00
matt korwel 099aa9621c fix(core): ensure sub-agents are registered regardless of tools.allowed (#18870) 2026-02-12 02:12:01 +00:00
g-samroberts fe75de3efb Update changelog for v0.28.0 and v0.29.0-preview0 (#18819) 2026-02-12 02:03:00 +00:00
Abhi fad9f46273 refactor(cli): consolidate useToolScheduler and delete legacy implementation (#18567) 2026-02-12 01:49:30 +00:00
Bryan Morgan a1148ea1f1 fix(workflows): improve maintainer detection for automated PR actions (#18869) 2026-02-11 20:56:43 -05:00
Abhijit Balaji 0e85e021dc feat(cli): deprecate --allowed-tools and excludeTools in favor of policy engine (#18508) 2026-02-12 00:49:48 +00:00
Abhi c370d2397b refactor(cli): simplify UI and remove legacy inline tool confirmation logic (#18566) 2026-02-12 00:46:58 +00:00
Gal Zahavi 08e8eeab84 fix(core): improve headless mode detection for flags and query args (#18855) 2026-02-12 00:20:54 +00:00
Richie Foreman 941691ce72 fix(mcp): Ensure that stdio MCP server execution has the GEMINI_CLI=1 env variable populated. (#18832) 2026-02-12 00:07:51 +00:00
98 changed files with 3956 additions and 4513 deletions
+4 -1
View File
@@ -20,6 +20,9 @@ inputs:
github-token:
description: 'The GitHub token for creating the release.'
required: true
github-release-token:
description: 'The GitHub token used specifically for creating the GitHub release (to trigger other workflows).'
required: false
dry-run:
description: 'Whether to run in dry-run mode.'
type: 'string'
@@ -254,7 +257,7 @@ runs:
working-directory: '${{ inputs.working-directory }}'
if: "${{ inputs.dry-run != 'true' && inputs.skip-github-release != 'true' && inputs.npm-tag != 'dev' && inputs.npm-registry-url != 'https://npm.pkg.github.com/' }}"
env:
GITHUB_TOKEN: '${{ inputs.github-token }}'
GITHUB_TOKEN: '${{ inputs.github-release-token || inputs.github-token }}'
shell: 'bash'
run: |
gh release create "${{ inputs.release-tag }}" \
@@ -43,23 +43,56 @@ jobs:
// 1. Fetch maintainers for verification
let maintainerLogins = new Set();
let teamFetchSucceeded = false;
try {
const members = await github.paginate(github.rest.teams.listMembersInOrg, {
org: context.repo.owner,
team_slug: 'gemini-cli-maintainers'
});
maintainerLogins = new Set(members.map(m => m.login.toLowerCase()));
teamFetchSucceeded = true;
core.info(`Successfully fetched ${maintainerLogins.size} team members from gemini-cli-maintainers`);
} catch (e) {
core.warning(`Failed to fetch team members from gemini-cli-maintainers: ${e.message}. Falling back to author_association only.`);
const teams = ['gemini-cli-maintainers', 'gemini-cli-askmode-approvers', 'gemini-cli-docs'];
for (const team_slug of teams) {
try {
const members = await github.paginate(github.rest.teams.listMembersInOrg, {
org: context.repo.owner,
team_slug: team_slug
});
for (const m of members) maintainerLogins.add(m.login.toLowerCase());
core.info(`Successfully fetched ${members.length} team members from ${team_slug}`);
} catch (e) {
core.warning(`Failed to fetch team members from ${team_slug}: ${e.message}`);
}
}
const isMaintainer = (login, assoc) => {
const isGooglerCache = new Map();
const isGoogler = async (login) => {
if (isGooglerCache.has(login)) return isGooglerCache.get(login);
try {
// Check membership in 'googlers' or 'google' orgs
const orgs = ['googlers', 'google'];
for (const org of orgs) {
try {
await github.rest.orgs.checkMembershipForUser({
org: org,
username: login
});
core.info(`User ${login} is a member of ${org} organization.`);
isGooglerCache.set(login, true);
return true;
} catch (e) {
// 404 just means they aren't a member, which is fine
if (e.status !== 404) throw e;
}
}
} catch (e) {
core.warning(`Failed to check org membership for ${login}: ${e.message}`);
}
isGooglerCache.set(login, false);
return false;
};
const isMaintainer = async (login, assoc) => {
const isTeamMember = maintainerLogins.has(login.toLowerCase());
const isRepoMaintainer = ['OWNER', 'MEMBER', 'COLLABORATOR'].includes(assoc);
return isTeamMember || isRepoMaintainer;
if (isTeamMember || isRepoMaintainer) return true;
return await isGoogler(login);
};
// 2. Determine which PRs to check
@@ -81,7 +114,7 @@ jobs:
}
for (const pr of prs) {
const maintainerPr = isMaintainer(pr.user.login, pr.author_association);
const maintainerPr = await isMaintainer(pr.user.login, pr.author_association);
const isBot = pr.user.type === 'Bot' || pr.user.login.endsWith('[bot]');
// Detection Logic for Linked Issues
@@ -175,7 +208,7 @@ jobs:
pull_number: pr.number
});
for (const r of reviews) {
if (isMaintainer(r.user.login, r.author_association)) {
if (await isMaintainer(r.user.login, r.author_association)) {
const d = new Date(r.submitted_at || r.updated_at);
if (d > lastActivity) lastActivity = d;
}
@@ -186,7 +219,7 @@ jobs:
issue_number: pr.number
});
for (const c of comments) {
if (isMaintainer(c.user.login, c.author_association)) {
if (await isMaintainer(c.user.login, c.author_association)) {
const d = new Date(c.updated_at);
if (d > lastActivity) lastActivity = d;
}
@@ -35,9 +35,31 @@ jobs:
const pr_number = context.payload.pull_request.number;
// 1. Check if the PR author is a maintainer
const isGoogler = async (login) => {
try {
const orgs = ['googlers', 'google'];
for (const org of orgs) {
try {
await github.rest.orgs.checkMembershipForUser({
org: org,
username: login
});
return true;
} catch (e) {
if (e.status !== 404) throw e;
}
}
} catch (e) {
core.warning(`Failed to check org membership for ${login}: ${e.message}`);
}
return false;
};
const authorAssociation = context.payload.pull_request.author_association;
if (['OWNER', 'MEMBER', 'COLLABORATOR'].includes(authorAssociation)) {
core.info(`${username} is a maintainer (Association: ${authorAssociation}). No notification needed.`);
const isRepoMaintainer = ['OWNER', 'MEMBER', 'COLLABORATOR'].includes(authorAssociation);
if (isRepoMaintainer || await isGoogler(username)) {
core.info(`${username} is a maintainer or Googler. No notification needed.`);
return;
}
+1
View File
@@ -110,6 +110,7 @@ jobs:
wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
wombat-token-a2a-server: '${{ secrets.WOMBAT_TOKEN_A2A_SERVER }}'
github-token: '${{ secrets.GITHUB_TOKEN }}'
github-release-token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
dry-run: '${{ github.event.inputs.dry_run }}'
previous-tag: '${{ steps.release_info.outputs.PREVIOUS_TAG }}'
skip-github-release: '${{ github.event.inputs.skip_github_release }}'
+2 -1
View File
@@ -124,6 +124,7 @@ jobs:
wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
wombat-token-a2a-server: '${{ secrets.WOMBAT_TOKEN_A2A_SERVER }}'
github-token: '${{ secrets.GITHUB_TOKEN }}'
github-release-token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
dry-run: '${{ steps.vars.outputs.is_dry_run }}'
previous-tag: '${{ steps.nightly_version.outputs.PREVIOUS_TAG }}'
working-directory: './release'
@@ -144,7 +145,7 @@ jobs:
branch-name: 'release/${{ steps.nightly_version.outputs.RELEASE_TAG }}'
pr-title: 'chore/release: bump version to ${{ steps.nightly_version.outputs.RELEASE_VERSION }}'
pr-body: 'Automated version bump for nightly release.'
github-token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
github-token: '${{ secrets.GITHUB_TOKEN }}'
dry-run: '${{ steps.vars.outputs.is_dry_run }}'
working-directory: './release'
@@ -184,6 +184,7 @@ jobs:
wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
wombat-token-a2a-server: '${{ secrets.WOMBAT_TOKEN_A2A_SERVER }}'
github-token: '${{ secrets.GITHUB_TOKEN }}'
github-release-token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
dry-run: '${{ github.event.inputs.dry_run }}'
previous-tag: '${{ steps.patch_version.outputs.PREVIOUS_TAG }}'
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
+3 -1
View File
@@ -239,6 +239,7 @@ jobs:
wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
wombat-token-a2a-server: '${{ secrets.WOMBAT_TOKEN_A2A_SERVER }}'
github-token: '${{ secrets.GITHUB_TOKEN }}'
github-release-token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
dry-run: '${{ github.event.inputs.dry_run }}'
previous-tag: '${{ needs.calculate-versions.outputs.PREVIOUS_PREVIEW_TAG }}'
working-directory: './release'
@@ -305,6 +306,7 @@ jobs:
wombat-token-cli: '${{ secrets.WOMBAT_TOKEN_CLI }}'
wombat-token-a2a-server: '${{ secrets.WOMBAT_TOKEN_A2A_SERVER }}'
github-token: '${{ secrets.GITHUB_TOKEN }}'
github-release-token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
dry-run: '${{ github.event.inputs.dry_run }}'
previous-tag: '${{ needs.calculate-versions.outputs.PREVIOUS_STABLE_TAG }}'
working-directory: './release'
@@ -390,7 +392,7 @@ jobs:
branch-name: '${{ steps.release_branch.outputs.BRANCH_NAME }}'
pr-title: 'chore(release): bump version to ${{ needs.calculate-versions.outputs.NEXT_NIGHTLY_VERSION }}'
pr-body: 'Automated version bump to prepare for the next nightly release.'
github-token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
github-token: '${{ secrets.GITHUB_TOKEN }}'
dry-run: '${{ github.event.inputs.dry_run }}'
- name: 'Create Issue on Failure'
+7 -6
View File
@@ -408,12 +408,13 @@ On macOS, `gemini` uses Seatbelt (`sandbox-exec`) under a `permissive-open`
profile (see `packages/cli/src/utils/sandbox-macos-permissive-open.sb`) that
restricts writes to the project folder but otherwise allows all other operations
and outbound network traffic ("open") by default. You can switch to a
`restrictive-closed` profile (see
`packages/cli/src/utils/sandbox-macos-restrictive-closed.sb`) that declines all
operations and outbound network traffic ("closed") by default by setting
`SEATBELT_PROFILE=restrictive-closed` in your environment or `.env` file.
Available built-in profiles are `{permissive,restrictive}-{open,closed,proxied}`
(see below for proxied networking). You can also switch to a custom profile
`strict-open` profile (see
`packages/cli/src/utils/sandbox-macos-strict-open.sb`) that restricts both reads
and writes to the working directory while allowing outbound network traffic by
setting `SEATBELT_PROFILE=strict-open` in your environment or `.env` file.
Available built-in profiles are `permissive-{open,proxied}`,
`restrictive-{open,proxied}`, and `strict-{open,proxied}` (see below for proxied
networking). You can also switch to a custom profile
`SEATBELT_PROFILE=<profile>` if you also create a file
`.gemini/sandbox-macos-<profile>.sb` under your project settings directory
`.gemini`.
+20
View File
@@ -18,6 +18,26 @@ on GitHub.
| [Preview](preview.md) | Experimental features ready for early feedback. |
| [Stable](latest.md) | Stable, recommended for general use. |
## Announcements: v0.28.0 - 2026-02-03
- **Slash Command:** We've added a new `/prompt-suggest` slash command to help
you generate prompt suggestions
([#17264](https://github.com/google-gemini/gemini-cli/pull/17264) by
@NTaylorMullen).
- **IDE Support:** Gemini CLI now supports the Positron IDE
([#15047](https://github.com/google-gemini/gemini-cli/pull/15047) by
@kapsner).
- **Customization:** You can now use custom themes in extensions, and we've
implemented automatic theme switching based on your terminal's background
([#17327](https://github.com/google-gemini/gemini-cli/pull/17327) by
@spencer426, [#17976](https://github.com/google-gemini/gemini-cli/pull/17976)
by @Abhijit-2592).
- **Authentication:** We've added interactive and non-interactive consent for
OAuth, and you can now include your auth method in bug reports
([#17699](https://github.com/google-gemini/gemini-cli/pull/17699) by
@ehedlund, [#17569](https://github.com/google-gemini/gemini-cli/pull/17569) by
@erikus).
## Announcements: v0.27.0 - 2026-02-03
- **Event-Driven Architecture:** The CLI now uses a new event-driven scheduler
+295 -427
View File
@@ -1,6 +1,6 @@
# Latest stable release: v0.27.0
# Latest stable release: v0.28.0
Released: February 3, 2026
Released: February 10, 2026
For most users, our latest stable release is the recommended release. Install
the latest stable version with:
@@ -11,437 +11,305 @@ npm install -g @google/gemini-cli
## Highlights
- **Event-Driven Architecture:** The CLI now uses an event-driven scheduler for
tool execution, improving performance and responsiveness. This includes
migrating non-interactive flows and sub-agents to the new scheduler.
- **Enhanced User Experience:** This release introduces several UI/UX
improvements, including queued tool confirmations and the ability to expand
and collapse large pasted text blocks. The `Settings` dialog has been improved
to reduce jitter and preserve focus.
- **Agent and Skill Improvements:** Agent Skills have been promoted to a stable
feature. Sub-agents now use a JSON schema for input and are tracked by an
`AgentRegistry`.
- **New `/rewind` Command:** A new `/rewind` command has been implemented to
allow users to go back in their session history.
- **Improved Shell and File Handling:** The shell tool's output format has been
optimized, and the CLI now gracefully handles disk-full errors during chat
recording. A bug in detecting already added paths has been fixed.
- **Linux Clipboard Support:** Image pasting capabilities for Wayland and X11 on
Linux have been added.
- **Commands & UX Enhancements:** Introduced `/prompt-suggest` command,
alongside updated undo/redo keybindings and automatic theme switching.
- **Expanded IDE Support:** Now offering compatibility with Positron IDE,
expanding integration options for developers.
- **Enhanced Security & Authentication:** Implemented interactive and
non-interactive OAuth consent, improving both security and diagnostic
capabilities for bug reports.
- **Advanced Planning & Agent Tools:** Integrated a generic Checklist component
for structured task management and evolved subagent capabilities with dynamic
policy registration.
- **Improved Core Stability & Reliability:** Resolved critical environment
loading, authentication, and session management issues, ensuring a more robust
experience.
- **Background Shell Commands:** Enabled the execution of shell commands in the
background for increased workflow efficiency.
## What's Changed
- remove fireAgent and beforeAgent hook by @ishaanxgupta in
[#16919](https://github.com/google-gemini/gemini-cli/pull/16919)
- Remove unused modelHooks and toolHooks by @ved015 in
[#17115](https://github.com/google-gemini/gemini-cli/pull/17115)
- feat(cli): sanitize ANSI escape sequences in non-interactive output by
@sehoon38 in [#17172](https://github.com/google-gemini/gemini-cli/pull/17172)
- Update Attempt text to Retry when showing the retry happening to the … by
@sehoon38 in [#17178](https://github.com/google-gemini/gemini-cli/pull/17178)
- chore(skills): update pr-creator skill workflow by @sehoon38 in
[#17180](https://github.com/google-gemini/gemini-cli/pull/17180)
- feat(cli): implement event-driven tool execution scheduler by @abhipatel12 in
[#17078](https://github.com/google-gemini/gemini-cli/pull/17078)
- chore(release): bump version to 0.27.0-nightly.20260121.97aac696f by
- feat(commands): add /prompt-suggest slash command by @NTaylorMullen in
[#17264](https://github.com/google-gemini/gemini-cli/pull/17264)
- feat(cli): align hooks enable/disable with skills and improve completion by
@sehoon38 in [#16822](https://github.com/google-gemini/gemini-cli/pull/16822)
- docs: add CLI reference documentation by @leochiu-a in
[#17504](https://github.com/google-gemini/gemini-cli/pull/17504)
- chore(release): bump version to 0.28.0-nightly.20260128.adc8e11bb by
@gemini-cli-robot in
[#17181](https://github.com/google-gemini/gemini-cli/pull/17181)
- Remove other rewind reference in docs by @chrstnb in
[#17149](https://github.com/google-gemini/gemini-cli/pull/17149)
- feat(skills): add code-reviewer skill by @sehoon38 in
[#17187](https://github.com/google-gemini/gemini-cli/pull/17187)
- feat(plan): Extend Shift+Tab Mode Cycling to include Plan Mode by @Adib234 in
[#17177](https://github.com/google-gemini/gemini-cli/pull/17177)
- feat(plan): refactor TestRig and eval helper to support configurable approval
modes by @jerop in
[#17171](https://github.com/google-gemini/gemini-cli/pull/17171)
- feat(workflows): support recursive workstream labeling and new IDs by
@bdmorgan in [#17207](https://github.com/google-gemini/gemini-cli/pull/17207)
- Run evals for all models. by @gundermanc in
[#17123](https://github.com/google-gemini/gemini-cli/pull/17123)
- fix(github): improve label-workstream-rollup efficiency with GraphQL by
@bdmorgan in [#17217](https://github.com/google-gemini/gemini-cli/pull/17217)
- Docs: Update changelogs for v.0.25.0 and v0.26.0-preview.0 releases. by
@g-samroberts in
[#17215](https://github.com/google-gemini/gemini-cli/pull/17215)
- Migrate beforeTool and afterTool hooks to hookSystem by @ved015 in
[#17204](https://github.com/google-gemini/gemini-cli/pull/17204)
- fix(github): improve label-workstream-rollup efficiency and fix bugs by
@bdmorgan in [#17219](https://github.com/google-gemini/gemini-cli/pull/17219)
- feat(cli): improve skill enablement/disablement verbiage by @NTaylorMullen in
[#17192](https://github.com/google-gemini/gemini-cli/pull/17192)
- fix(admin): Ensure CLI commands run in non-interactive mode by @skeshive in
[#17218](https://github.com/google-gemini/gemini-cli/pull/17218)
- feat(core): support dynamic variable substitution in system prompt override by
@NTaylorMullen in
[#17042](https://github.com/google-gemini/gemini-cli/pull/17042)
- fix(core,cli): enable recursive directory access for by @galz10 in
[#17094](https://github.com/google-gemini/gemini-cli/pull/17094)
- Docs: Marking for experimental features by @jkcinouye in
[#16760](https://github.com/google-gemini/gemini-cli/pull/16760)
- Support command/ctrl/alt backspace correctly by @scidomino in
[#17175](https://github.com/google-gemini/gemini-cli/pull/17175)
- feat(plan): add approval mode instructions to system prompt by @jerop in
[#17151](https://github.com/google-gemini/gemini-cli/pull/17151)
- feat(core): enable disableLLMCorrection by default by @SandyTao520 in
[#17223](https://github.com/google-gemini/gemini-cli/pull/17223)
- Remove unused slug from sidebar by @chrstnb in
[#17229](https://github.com/google-gemini/gemini-cli/pull/17229)
- drain stdin on exit by @scidomino in
[#17241](https://github.com/google-gemini/gemini-cli/pull/17241)
- refactor(cli): decouple UI from live tool execution via ToolActionsContext by
[#17725](https://github.com/google-gemini/gemini-cli/pull/17725)
- feat(skills): final stable promotion cleanup by @abhipatel12 in
[#17726](https://github.com/google-gemini/gemini-cli/pull/17726)
- test(core): mock fetch in OAuth transport fallback tests by @jw409 in
[#17059](https://github.com/google-gemini/gemini-cli/pull/17059)
- feat(cli): include auth method in /bug by @erikus in
[#17569](https://github.com/google-gemini/gemini-cli/pull/17569)
- Add a email privacy note to bug_report template by @nemyung in
[#17474](https://github.com/google-gemini/gemini-cli/pull/17474)
- Rewind documentation by @Adib234 in
[#17446](https://github.com/google-gemini/gemini-cli/pull/17446)
- fix: verify audio/video MIME types with content check by @maru0804 in
[#16907](https://github.com/google-gemini/gemini-cli/pull/16907)
- feat(core): add support for positron ide
([#15045](https://github.com/google-gemini/gemini-cli/pull/15045)) by @kapsner
in [#15047](https://github.com/google-gemini/gemini-cli/pull/15047)
- /oncall dedup - wrap texts to nextlines by @sehoon38 in
[#17782](https://github.com/google-gemini/gemini-cli/pull/17782)
- fix(admin): rename advanced features admin setting by @skeshive in
[#17786](https://github.com/google-gemini/gemini-cli/pull/17786)
- [extension config] Make breaking optional value non-optional by @chrstnb in
[#17785](https://github.com/google-gemini/gemini-cli/pull/17785)
- Fix docs-writer skill issues by @g-samroberts in
[#17734](https://github.com/google-gemini/gemini-cli/pull/17734)
- fix(core): suppress duplicate hook failure warnings during streaming by
@abhipatel12 in
[#17183](https://github.com/google-gemini/gemini-cli/pull/17183)
- fix(core): update token count and telemetry on /chat resume history load by
@psinha40898 in
[#16279](https://github.com/google-gemini/gemini-cli/pull/16279)
- fix: /policy to display policies according to mode by @ishaanxgupta in
[#16772](https://github.com/google-gemini/gemini-cli/pull/16772)
- fix(core): simplify replace tool error message by @SandyTao520 in
[#17246](https://github.com/google-gemini/gemini-cli/pull/17246)
- feat(cli): consolidate shell inactivity and redirection monitoring by
@NTaylorMullen in
[#17086](https://github.com/google-gemini/gemini-cli/pull/17086)
- fix(scheduler): prevent stale tool re-publication and fix stuck UI state by
[#17727](https://github.com/google-gemini/gemini-cli/pull/17727)
- test: add more tests for AskUser by @jackwotherspoon in
[#17720](https://github.com/google-gemini/gemini-cli/pull/17720)
- feat(cli): enable activity logging for non-interactive mode and evals by
@SandyTao520 in
[#17703](https://github.com/google-gemini/gemini-cli/pull/17703)
- feat(core): add support for custom deny messages in policy rules by
@allenhutchison in
[#17427](https://github.com/google-gemini/gemini-cli/pull/17427)
- Fix unintended credential exposure to MCP Servers by @Adib234 in
[#17311](https://github.com/google-gemini/gemini-cli/pull/17311)
- feat(extensions): add support for custom themes in extensions by @spencer426
in [#17327](https://github.com/google-gemini/gemini-cli/pull/17327)
- fix: persist and restore workspace directories on session resume by
@korade-krushna in
[#17454](https://github.com/google-gemini/gemini-cli/pull/17454)
- Update release notes pages for 0.26.0 and 0.27.0-preview. by @g-samroberts in
[#17744](https://github.com/google-gemini/gemini-cli/pull/17744)
- feat(ux): update cell border color and created test file for table rendering
by @devr0306 in
[#17798](https://github.com/google-gemini/gemini-cli/pull/17798)
- Change height for the ToolConfirmationQueue. by @jacob314 in
[#17799](https://github.com/google-gemini/gemini-cli/pull/17799)
- feat(cli): add user identity info to stats command by @sehoon38 in
[#17612](https://github.com/google-gemini/gemini-cli/pull/17612)
- fix(ux): fixed off-by-some wrapping caused by fixed-width characters by
@devr0306 in [#17816](https://github.com/google-gemini/gemini-cli/pull/17816)
- feat(cli): update undo/redo keybindings to Cmd+Z/Alt+Z and
Shift+Cmd+Z/Shift+Alt+Z by @scidomino in
[#17800](https://github.com/google-gemini/gemini-cli/pull/17800)
- fix(evals): use absolute path for activity log directory by @SandyTao520 in
[#17830](https://github.com/google-gemini/gemini-cli/pull/17830)
- test: add integration test to verify stdout/stderr routing by @ved015 in
[#17280](https://github.com/google-gemini/gemini-cli/pull/17280)
- fix(cli): list installed extensions when update target missing by @tt-a1i in
[#17082](https://github.com/google-gemini/gemini-cli/pull/17082)
- fix(cli): handle PAT tokens and credentials in git remote URL parsing by
@afarber in [#14650](https://github.com/google-gemini/gemini-cli/pull/14650)
- fix(core): use returnDisplay for error result display by @Nubebuster in
[#14994](https://github.com/google-gemini/gemini-cli/pull/14994)
- Fix detection of bun as package manager by @Randomblock1 in
[#17462](https://github.com/google-gemini/gemini-cli/pull/17462)
- feat(cli): show hooksConfig.enabled in settings dialog by @abhipatel12 in
[#17810](https://github.com/google-gemini/gemini-cli/pull/17810)
- feat(cli): Display user identity (auth, email, tier) on startup by @yunaseoul
in [#17591](https://github.com/google-gemini/gemini-cli/pull/17591)
- fix: prevent ghost border for AskUserDialog by @jackwotherspoon in
[#17788](https://github.com/google-gemini/gemini-cli/pull/17788)
- docs: mark A2A subagents as experimental in subagents.md by @adamfweidman in
[#17863](https://github.com/google-gemini/gemini-cli/pull/17863)
- Resolve error thrown for sensitive values by @chrstnb in
[#17826](https://github.com/google-gemini/gemini-cli/pull/17826)
- fix(admin): Rename secureModeEnabled to strictModeDisabled by @skeshive in
[#17789](https://github.com/google-gemini/gemini-cli/pull/17789)
- feat(ux): update truncate dots to be shorter in tables by @devr0306 in
[#17825](https://github.com/google-gemini/gemini-cli/pull/17825)
- fix(core): resolve DEP0040 punycode deprecation via patch-package by
@ATHARVA262005 in
[#17692](https://github.com/google-gemini/gemini-cli/pull/17692)
- feat(plan): create generic Checklist component and refactor Todo by @Adib234
in [#17741](https://github.com/google-gemini/gemini-cli/pull/17741)
- Cleanup post delegate_to_agent removal by @gundermanc in
[#17875](https://github.com/google-gemini/gemini-cli/pull/17875)
- fix(core): use GIT_CONFIG_GLOBAL to isolate shadow git repo configuration -
Fixes [#17877](https://github.com/google-gemini/gemini-cli/pull/17877) by
@cocosheng-g in
[#17803](https://github.com/google-gemini/gemini-cli/pull/17803)
- Disable mouse tracking e2e by @alisa-alisa in
[#17880](https://github.com/google-gemini/gemini-cli/pull/17880)
- fix(cli): use correct setting key for Cloud Shell auth by @sehoon38 in
[#17884](https://github.com/google-gemini/gemini-cli/pull/17884)
- chore: revert IDE specific ASCII logo by @jackwotherspoon in
[#17887](https://github.com/google-gemini/gemini-cli/pull/17887)
- Revert "fix(core): resolve DEP0040 punycode deprecation via patch-package" by
@sehoon38 in [#17898](https://github.com/google-gemini/gemini-cli/pull/17898)
- Refactoring of disabling of mouse tracking in e2e tests by @alisa-alisa in
[#17902](https://github.com/google-gemini/gemini-cli/pull/17902)
- feat(core): Add GOOGLE_GENAI_API_VERSION environment variable support by
@deyim in [#16177](https://github.com/google-gemini/gemini-cli/pull/16177)
- feat(core): Isolate and cleanup truncated tool outputs by @SandyTao520 in
[#17594](https://github.com/google-gemini/gemini-cli/pull/17594)
- Create skills page, update commands, refine docs by @g-samroberts in
[#17842](https://github.com/google-gemini/gemini-cli/pull/17842)
- feat: preserve EOL in files by @Thomas-Shephard in
[#16087](https://github.com/google-gemini/gemini-cli/pull/16087)
- Fix HalfLinePaddedBox in screenreader mode. by @jacob314 in
[#17914](https://github.com/google-gemini/gemini-cli/pull/17914)
- bug(ux) vim mode fixes. Start in insert mode. Fix bug blocking F12 and ctrl-X
in vim mode. by @jacob314 in
[#17938](https://github.com/google-gemini/gemini-cli/pull/17938)
- feat(core): implement interactive and non-interactive consent for OAuth by
@ehedlund in [#17699](https://github.com/google-gemini/gemini-cli/pull/17699)
- perf(core): optimize token calculation and add support for multimodal tool
responses by @abhipatel12 in
[#17835](https://github.com/google-gemini/gemini-cli/pull/17835)
- refactor(hooks): remove legacy tools.enableHooks setting by @abhipatel12 in
[#17867](https://github.com/google-gemini/gemini-cli/pull/17867)
- feat(ci): add npx smoke test to verify installability by @bdmorgan in
[#17927](https://github.com/google-gemini/gemini-cli/pull/17927)
- feat(core): implement dynamic policy registration for subagents by
@abhipatel12 in
[#17227](https://github.com/google-gemini/gemini-cli/pull/17227)
- feat(config): default enableEventDrivenScheduler to true by @abhipatel12 in
[#17211](https://github.com/google-gemini/gemini-cli/pull/17211)
- feat(hooks): enable hooks system by default by @abhipatel12 in
[#17247](https://github.com/google-gemini/gemini-cli/pull/17247)
- feat(core): Enable AgentRegistry to track all discovered subagents by
[#17838](https://github.com/google-gemini/gemini-cli/pull/17838)
- feat: Implement background shell commands by @galz10 in
[#14849](https://github.com/google-gemini/gemini-cli/pull/14849)
- feat(admin): provide actionable error messages for disabled features by
@skeshive in [#17815](https://github.com/google-gemini/gemini-cli/pull/17815)
- Fix bugs where Rewind and Resume showed Ugly and 100X too verbose content. by
@jacob314 in [#17940](https://github.com/google-gemini/gemini-cli/pull/17940)
- Fix broken link in docs by @chrstnb in
[#17959](https://github.com/google-gemini/gemini-cli/pull/17959)
- feat(plan): reuse standard tool confirmation for AskUser tool by @jerop in
[#17864](https://github.com/google-gemini/gemini-cli/pull/17864)
- feat(core): enable overriding CODE_ASSIST_API_VERSION with env var by
@lottielin in [#17942](https://github.com/google-gemini/gemini-cli/pull/17942)
- run npx pointing to the specific commit SHA by @sehoon38 in
[#17970](https://github.com/google-gemini/gemini-cli/pull/17970)
- Add allowedExtensions setting by @kevinjwang1 in
[#17695](https://github.com/google-gemini/gemini-cli/pull/17695)
- feat(plan): refactor ToolConfirmationPayload to union type by @jerop in
[#17980](https://github.com/google-gemini/gemini-cli/pull/17980)
- lower the default max retries to reduce contention by @sehoon38 in
[#17975](https://github.com/google-gemini/gemini-cli/pull/17975)
- fix(core): ensure YOLO mode auto-approves complex shell commands when parsing
fails by @abhipatel12 in
[#17920](https://github.com/google-gemini/gemini-cli/pull/17920)
- Fix broken link. by @g-samroberts in
[#17972](https://github.com/google-gemini/gemini-cli/pull/17972)
- Support ctrl-C and Ctrl-D correctly Refactor so InputPrompt has priority over
AppContainer for input handling. by @jacob314 in
[#17993](https://github.com/google-gemini/gemini-cli/pull/17993)
- Fix truncation for AskQuestion by @jacob314 in
[#18001](https://github.com/google-gemini/gemini-cli/pull/18001)
- fix(workflow): update maintainer check logic to be inclusive and
case-insensitive by @bdmorgan in
[#18009](https://github.com/google-gemini/gemini-cli/pull/18009)
- Fix Esc cancel during streaming by @LyalinDotCom in
[#18039](https://github.com/google-gemini/gemini-cli/pull/18039)
- feat(acp): add session resume support by @bdmorgan in
[#18043](https://github.com/google-gemini/gemini-cli/pull/18043)
- fix(ci): prevent stale PR closer from incorrectly closing new PRs by @bdmorgan
in [#18069](https://github.com/google-gemini/gemini-cli/pull/18069)
- chore: delete autoAccept setting unused in production by @victorvianna in
[#17862](https://github.com/google-gemini/gemini-cli/pull/17862)
- feat(plan): use placeholder for choice question "Other" option by @jerop in
[#18101](https://github.com/google-gemini/gemini-cli/pull/18101)
- docs: update clearContext to hookSpecificOutput by @jackwotherspoon in
[#18024](https://github.com/google-gemini/gemini-cli/pull/18024)
- docs-writer skill: Update docs writer skill by @jkcinouye in
[#17928](https://github.com/google-gemini/gemini-cli/pull/17928)
- Sehoon/oncall filter by @sehoon38 in
[#18105](https://github.com/google-gemini/gemini-cli/pull/18105)
- feat(core): add setting to disable loop detection by @SandyTao520 in
[#18008](https://github.com/google-gemini/gemini-cli/pull/18008)
- Docs: Revise docs/index.md by @jkcinouye in
[#17879](https://github.com/google-gemini/gemini-cli/pull/17879)
- Fix up/down arrow regression and add test. by @jacob314 in
[#18108](https://github.com/google-gemini/gemini-cli/pull/18108)
- fix(ui): prevent content leak in MaxSizedBox bottom overflow by @jerop in
[#17991](https://github.com/google-gemini/gemini-cli/pull/17991)
- refactor: migrate checks.ts utility to core and deduplicate by @jerop in
[#18139](https://github.com/google-gemini/gemini-cli/pull/18139)
- feat(core): implement tool name aliasing for backward compatibility by
@SandyTao520 in
[#17253](https://github.com/google-gemini/gemini-cli/pull/17253)
- feat(core): Have subagents use a JSON schema type for input. by @joshualitt in
[#17152](https://github.com/google-gemini/gemini-cli/pull/17152)
- feat: replace large text pastes with [Pasted Text: X lines] placeholder by
@jackwotherspoon in
[#16422](https://github.com/google-gemini/gemini-cli/pull/16422)
- security(hooks): Wrap hook-injected context in distinct XML tags by @yunaseoul
in [#17237](https://github.com/google-gemini/gemini-cli/pull/17237)
- Enable the ability to queue specific nightly eval tests by @gundermanc in
[#17262](https://github.com/google-gemini/gemini-cli/pull/17262)
- docs(hooks): comprehensive update of hook documentation and specs by
@abhipatel12 in
[#16816](https://github.com/google-gemini/gemini-cli/pull/16816)
- refactor: improve large text paste placeholder by @jacob314 in
[#17269](https://github.com/google-gemini/gemini-cli/pull/17269)
- feat: implement /rewind command by @Adib234 in
[#15720](https://github.com/google-gemini/gemini-cli/pull/15720)
- Feature/jetbrains ide detection by @SoLoHiC in
[#16243](https://github.com/google-gemini/gemini-cli/pull/16243)
- docs: update typo in mcp-server.md file by @schifferl in
[#17099](https://github.com/google-gemini/gemini-cli/pull/17099)
- Sanitize command names and descriptions by @ehedlund in
[#17228](https://github.com/google-gemini/gemini-cli/pull/17228)
- fix(auth): don't crash when initial auth fails by @skeshive in
[#17308](https://github.com/google-gemini/gemini-cli/pull/17308)
- Added image pasting capabilities for Wayland and X11 on Linux by @devr0306 in
[#17144](https://github.com/google-gemini/gemini-cli/pull/17144)
- feat: add AskUser tool schema by @jackwotherspoon in
[#16988](https://github.com/google-gemini/gemini-cli/pull/16988)
- fix cli settings: resolve layout jitter in settings bar by @Mag1ck in
[#16256](https://github.com/google-gemini/gemini-cli/pull/16256)
- fix: show whitespace changes in edit tool diffs by @Ujjiyara in
[#17213](https://github.com/google-gemini/gemini-cli/pull/17213)
- Remove redundant calls setting linuxClipboardTool. getUserLinuxClipboardTool()
now handles the caching internally by @jacob314 in
[#17320](https://github.com/google-gemini/gemini-cli/pull/17320)
- ci: allow failure in evals-nightly run step by @gundermanc in
[#17319](https://github.com/google-gemini/gemini-cli/pull/17319)
- feat(cli): Add state management and plumbing for agent configuration dialog by
@SandyTao520 in
[#17259](https://github.com/google-gemini/gemini-cli/pull/17259)
- bug: fix ide-client connection to ide-companion when inside docker via
ssh/devcontainer by @kapsner in
[#15049](https://github.com/google-gemini/gemini-cli/pull/15049)
- Emit correct newline type return by @scidomino in
[#17331](https://github.com/google-gemini/gemini-cli/pull/17331)
- New skill: docs-writer by @g-samroberts in
[#17268](https://github.com/google-gemini/gemini-cli/pull/17268)
- fix(core): Resolve AbortSignal MaxListenersExceededWarning (#5950) by
@spencer426 in
[#16735](https://github.com/google-gemini/gemini-cli/pull/16735)
- Disable tips after 10 runs by @Adib234 in
[#17101](https://github.com/google-gemini/gemini-cli/pull/17101)
- Fix so rewind starts at the bottom and loadHistory refreshes static content.
by @jacob314 in
[#17335](https://github.com/google-gemini/gemini-cli/pull/17335)
- feat(core): Remove legacy settings. by @joshualitt in
[#17244](https://github.com/google-gemini/gemini-cli/pull/17244)
- feat(plan): add 'communicate' tool kind by @jerop in
[#17341](https://github.com/google-gemini/gemini-cli/pull/17341)
- feat(routing): A/B Test Numerical Complexity Scoring for Gemini 3 by
@mattKorwel in
[#16041](https://github.com/google-gemini/gemini-cli/pull/16041)
- feat(plan): update UI Theme for Plan Mode by @Adib234 in
[#17243](https://github.com/google-gemini/gemini-cli/pull/17243)
- fix(ui): stabilize rendering during terminal resize in alternate buffer by
@lkk214 in [#15783](https://github.com/google-gemini/gemini-cli/pull/15783)
- feat(cli): add /agents config command and improve agent discovery by
@SandyTao520 in
[#17342](https://github.com/google-gemini/gemini-cli/pull/17342)
- feat(mcp): add enable/disable commands for MCP servers (#11057) by @jasmeetsb
in [#16299](https://github.com/google-gemini/gemini-cli/pull/16299)
- fix(cli)!: Default to interactive mode for positional arguments by
@ishaanxgupta in
[#16329](https://github.com/google-gemini/gemini-cli/pull/16329)
- Fix issue #17080 by @jacob314 in
[#17100](https://github.com/google-gemini/gemini-cli/pull/17100)
- feat(core): Refresh agents after loading an extension. by @joshualitt in
[#17355](https://github.com/google-gemini/gemini-cli/pull/17355)
- fix(cli): include source in policy rule display by @allenhutchison in
[#17358](https://github.com/google-gemini/gemini-cli/pull/17358)
- fix: remove obsolete CloudCode PerDay quota and 120s terminal threshold by
[#17974](https://github.com/google-gemini/gemini-cli/pull/17974)
- docs: fix help-wanted label spelling by @pavan-sh in
[#18114](https://github.com/google-gemini/gemini-cli/pull/18114)
- feat(cli): implement automatic theme switching based on terminal background by
@Abhijit-2592 in
[#17976](https://github.com/google-gemini/gemini-cli/pull/17976)
- fix(ide): no-op refactoring that moves the connection logic to helper
functions by @skeshive in
[#18118](https://github.com/google-gemini/gemini-cli/pull/18118)
- feat: update review-frontend-and-fix slash command to review-and-fix by
@galz10 in [#18146](https://github.com/google-gemini/gemini-cli/pull/18146)
- fix: improve Ctrl+R reverse search by @jackwotherspoon in
[#18075](https://github.com/google-gemini/gemini-cli/pull/18075)
- feat(plan): handle inconsistency in schedulers by @Adib234 in
[#17813](https://github.com/google-gemini/gemini-cli/pull/17813)
- feat(plan): add core logic and exit_plan_mode tool definition by @jerop in
[#18110](https://github.com/google-gemini/gemini-cli/pull/18110)
- feat(core): rename search_file_content tool to grep_search and add legacy
alias by @SandyTao520 in
[#18003](https://github.com/google-gemini/gemini-cli/pull/18003)
- fix(core): prioritize detailed error messages for code assist setup by
@gsquared94 in
[#17236](https://github.com/google-gemini/gemini-cli/pull/17236)
- Refactor subagent delegation to be one tool per agent by @gundermanc in
[#17346](https://github.com/google-gemini/gemini-cli/pull/17346)
- fix(core): Include MCP server name in OAuth message by @jerop in
[#17351](https://github.com/google-gemini/gemini-cli/pull/17351)
- Fix pr-triage.sh script to update pull requests with tags "help wanted" and
"maintainer only" by @jacob314 in
[#17324](https://github.com/google-gemini/gemini-cli/pull/17324)
- feat(plan): implement simple workflow for planning in main agent by @jerop in
[#17326](https://github.com/google-gemini/gemini-cli/pull/17326)
- fix: exit with non-zero code when esbuild is missing by @yuvrajangadsingh in
[#16967](https://github.com/google-gemini/gemini-cli/pull/16967)
- fix: ensure @docs/cli/custom-commands.md UI message ordering and test by
@medic-code in
[#12038](https://github.com/google-gemini/gemini-cli/pull/12038)
- fix(core): add alternative command names for Antigravity editor detec… by
@baeseokjae in
[#16829](https://github.com/google-gemini/gemini-cli/pull/16829)
- Refactor: Migrate CLI appEvents to Core coreEvents by @Adib234 in
[#15737](https://github.com/google-gemini/gemini-cli/pull/15737)
- fix(core): await MCP initialization in non-interactive mode by @Ratish1 in
[#17390](https://github.com/google-gemini/gemini-cli/pull/17390)
- Fix modifyOtherKeys enablement on unsupported terminals by @seekskyworld in
[#16714](https://github.com/google-gemini/gemini-cli/pull/16714)
- fix(core): gracefully handle disk full errors in chat recording by
@godwiniheuwa in
[#17305](https://github.com/google-gemini/gemini-cli/pull/17305)
- fix(oauth): update oauth to use 127.0.0.1 instead of localhost by @skeshive in
[#17388](https://github.com/google-gemini/gemini-cli/pull/17388)
- fix(core): use RFC 9728 compliant path-based OAuth protected resource
discovery by @vrv in
[#15756](https://github.com/google-gemini/gemini-cli/pull/15756)
- Update Code Wiki README badge by @PatoBeltran in
[#15229](https://github.com/google-gemini/gemini-cli/pull/15229)
- Add conda installation instructions for Gemini CLI by @ishaanxgupta in
[#16921](https://github.com/google-gemini/gemini-cli/pull/16921)
- chore(refactor): extract BaseSettingsDialog component by @SandyTao520 in
[#17369](https://github.com/google-gemini/gemini-cli/pull/17369)
- fix(cli): preserve input text when declining tool approval (#15624) by
@ManojINaik in
[#15659](https://github.com/google-gemini/gemini-cli/pull/15659)
- chore: upgrade dep: diff 7.0.0-> 8.0.3 by @scidomino in
[#17403](https://github.com/google-gemini/gemini-cli/pull/17403)
- feat: add AskUserDialog for UI component of AskUser tool by @jackwotherspoon
in [#17344](https://github.com/google-gemini/gemini-cli/pull/17344)
- feat(ui): display user tier in about command by @sehoon38 in
[#17400](https://github.com/google-gemini/gemini-cli/pull/17400)
- feat: add clearContext to AfterAgent hooks by @jackwotherspoon in
[#16574](https://github.com/google-gemini/gemini-cli/pull/16574)
- fix(cli): change image paste location to global temp directory (#17396) by
@devr0306 in [#17396](https://github.com/google-gemini/gemini-cli/pull/17396)
- Fix line endings issue with Notice file by @scidomino in
[#17417](https://github.com/google-gemini/gemini-cli/pull/17417)
- feat(plan): implement persistent approvalMode setting by @Adib234 in
[#17350](https://github.com/google-gemini/gemini-cli/pull/17350)
- feat(ui): Move keyboard handling into BaseSettingsDialog by @SandyTao520 in
[#17404](https://github.com/google-gemini/gemini-cli/pull/17404)
- Allow prompt queueing during MCP initialization by @Adib234 in
[#17395](https://github.com/google-gemini/gemini-cli/pull/17395)
- feat: implement AgentConfigDialog for /agents config command by @SandyTao520
in [#17370](https://github.com/google-gemini/gemini-cli/pull/17370)
- fix(agents): default to all tools when tool list is omitted in subagents by
@gundermanc in
[#17422](https://github.com/google-gemini/gemini-cli/pull/17422)
- feat(cli): Moves tool confirmations to a queue UX by @abhipatel12 in
[#17276](https://github.com/google-gemini/gemini-cli/pull/17276)
- fix(core): hide user tier name by @sehoon38 in
[#17418](https://github.com/google-gemini/gemini-cli/pull/17418)
- feat: Enforce unified folder trust for /directory add by @galz10 in
[#17359](https://github.com/google-gemini/gemini-cli/pull/17359)
- migrate fireToolNotificationHook to hookSystem by @ved015 in
[#17398](https://github.com/google-gemini/gemini-cli/pull/17398)
- Clean up dead code by @scidomino in
[#17443](https://github.com/google-gemini/gemini-cli/pull/17443)
- feat(workflow): add stale pull request closer with linked-issue enforcement by
@bdmorgan in [#17449](https://github.com/google-gemini/gemini-cli/pull/17449)
- feat(workflow): expand stale-exempt labels to include help wanted and Public
Roadmap by @bdmorgan in
[#17459](https://github.com/google-gemini/gemini-cli/pull/17459)
- chore(workflow): remove redundant label-enforcer workflow by @bdmorgan in
[#17460](https://github.com/google-gemini/gemini-cli/pull/17460)
- Resolves the confusing error message `ripgrep exited with code null that
occurs when a search operation is cancelled or aborted by @maximmasiutin in
[#14267](https://github.com/google-gemini/gemini-cli/pull/14267)
- fix: detect pnpm/pnpx in ~/.local by @rwakulszowa in
[#15254](https://github.com/google-gemini/gemini-cli/pull/15254)
- docs: Add instructions for MacPorts and uninstall instructions for Homebrew by
@breun in [#17412](https://github.com/google-gemini/gemini-cli/pull/17412)
- docs(hooks): clarify mandatory 'type' field and update hook schema
documentation by @abhipatel12 in
[#17499](https://github.com/google-gemini/gemini-cli/pull/17499)
- Improve error messages on failed onboarding by @gsquared94 in
[#17357](https://github.com/google-gemini/gemini-cli/pull/17357)
- Follow up to "enableInteractiveShell for external tooling relying on a2a
server" by @DavidAPierce in
[#17130](https://github.com/google-gemini/gemini-cli/pull/17130)
- Fix/issue 17070 by @alih552 in
[#17242](https://github.com/google-gemini/gemini-cli/pull/17242)
- fix(core): handle URI-encoded workspace paths in IdeClient by @dong-jun-shin
in [#17476](https://github.com/google-gemini/gemini-cli/pull/17476)
- feat(cli): add quick clear input shortcuts in vim mode by @harshanadim in
[#17470](https://github.com/google-gemini/gemini-cli/pull/17470)
- feat(core): optimize shell tool llmContent output format by @SandyTao520 in
[#17538](https://github.com/google-gemini/gemini-cli/pull/17538)
- Fix bug in detecting already added paths. by @jacob314 in
[#17430](https://github.com/google-gemini/gemini-cli/pull/17430)
- feat(scheduler): support multi-scheduler tool aggregation and nested call IDs
by @abhipatel12 in
[#17429](https://github.com/google-gemini/gemini-cli/pull/17429)
- feat(agents): implement first-run experience for project-level sub-agents by
@gundermanc in
[#17266](https://github.com/google-gemini/gemini-cli/pull/17266)
- Update extensions docs by @chrstnb in
[#16093](https://github.com/google-gemini/gemini-cli/pull/16093)
- Docs: Refactor left nav on the website by @jkcinouye in
[#17558](https://github.com/google-gemini/gemini-cli/pull/17558)
- fix(core): stream grep/ripgrep output to prevent OOM by @adamfweidman in
[#17146](https://github.com/google-gemini/gemini-cli/pull/17146)
- feat(plan): add persistent plan file storage by @jerop in
[#17563](https://github.com/google-gemini/gemini-cli/pull/17563)
- feat(agents): migrate subagents to event-driven scheduler by @abhipatel12 in
[#17567](https://github.com/google-gemini/gemini-cli/pull/17567)
- Fix extensions config error by @chrstnb in
[#17580](https://github.com/google-gemini/gemini-cli/pull/17580)
- fix(plan): remove subagent invocation from plan mode by @jerop in
[#17593](https://github.com/google-gemini/gemini-cli/pull/17593)
- feat(ui): add solid background color option for input prompt by @jacob314 in
[#16563](https://github.com/google-gemini/gemini-cli/pull/16563)
- feat(plan): refresh system prompt when approval mode changes (Shift+Tab) by
@jerop in [#17585](https://github.com/google-gemini/gemini-cli/pull/17585)
- feat(cli): add global setting to disable UI spinners by @galz10 in
[#17234](https://github.com/google-gemini/gemini-cli/pull/17234)
- fix(security): enforce strict policy directory permissions by @yunaseoul in
[#17353](https://github.com/google-gemini/gemini-cli/pull/17353)
- test(core): fix tests in windows by @scidomino in
[#17592](https://github.com/google-gemini/gemini-cli/pull/17592)
- feat(mcp/extensions): Allow users to selectively enable/disable MCP servers
included in an extension( Issue #11057 & #17402) by @jasmeetsb in
[#17434](https://github.com/google-gemini/gemini-cli/pull/17434)
- Always map mac keys, even on other platforms by @scidomino in
[#17618](https://github.com/google-gemini/gemini-cli/pull/17618)
- Ctrl-O by @jacob314 in
[#17617](https://github.com/google-gemini/gemini-cli/pull/17617)
- feat(plan): update cycling order of approval modes by @Adib234 in
[#17622](https://github.com/google-gemini/gemini-cli/pull/17622)
- fix(cli): restore 'Modify with editor' option in external terminals by
@abhipatel12 in
[#17621](https://github.com/google-gemini/gemini-cli/pull/17621)
- Slash command for helping in debugging by @gundermanc in
[#17609](https://github.com/google-gemini/gemini-cli/pull/17609)
- feat: add double-click to expand/collapse large paste placeholders by
@jackwotherspoon in
[#17471](https://github.com/google-gemini/gemini-cli/pull/17471)
- refactor(cli): migrate non-interactive flow to event-driven scheduler by
@abhipatel12 in
[#17572](https://github.com/google-gemini/gemini-cli/pull/17572)
- fix: loadcodeassist eligible tiers getting ignored for unlicensed users
(regression) by @gsquared94 in
[#17581](https://github.com/google-gemini/gemini-cli/pull/17581)
- chore(core): delete legacy nonInteractiveToolExecutor by @abhipatel12 in
[#17573](https://github.com/google-gemini/gemini-cli/pull/17573)
- feat(core): enforce server prefixes for MCP tools in agent definitions by
@abhipatel12 in
[#17574](https://github.com/google-gemini/gemini-cli/pull/17574)
- feat (mcp): Refresh MCP prompts on list changed notification by @MrLesk in
[#14863](https://github.com/google-gemini/gemini-cli/pull/14863)
- feat(ui): pretty JSON rendering tool outputs by @medic-code in
[#9767](https://github.com/google-gemini/gemini-cli/pull/9767)
- Fix iterm alternate buffer mode issue rendering backgrounds by @jacob314 in
[#17634](https://github.com/google-gemini/gemini-cli/pull/17634)
- feat(cli): add gemini extensions list --output-format=json by @AkihiroSuda in
[#14479](https://github.com/google-gemini/gemini-cli/pull/14479)
- fix(extensions): add .gitignore to extension templates by @godwiniheuwa in
[#17293](https://github.com/google-gemini/gemini-cli/pull/17293)
- paste transform followup by @jacob314 in
[#17624](https://github.com/google-gemini/gemini-cli/pull/17624)
- refactor: rename formatMemoryUsage to formatBytes by @Nubebuster in
[#14997](https://github.com/google-gemini/gemini-cli/pull/14997)
- chore: remove extra top margin from /hooks and /extensions by @jackwotherspoon
in [#17663](https://github.com/google-gemini/gemini-cli/pull/17663)
- feat(cli): add oncall command for issue triage by @sehoon38 in
[#17661](https://github.com/google-gemini/gemini-cli/pull/17661)
- Fix sidebar issue for extensions link by @chrstnb in
[#17668](https://github.com/google-gemini/gemini-cli/pull/17668)
- Change formatting to prevent UI redressing attacks by @scidomino in
[#17611](https://github.com/google-gemini/gemini-cli/pull/17611)
- Fix cluster of bugs in the settings dialog. by @jacob314 in
[#17628](https://github.com/google-gemini/gemini-cli/pull/17628)
- Update sidebar to resolve site build issues by @chrstnb in
[#17674](https://github.com/google-gemini/gemini-cli/pull/17674)
- fix(admin): fix a few bugs related to admin controls by @skeshive in
[#17590](https://github.com/google-gemini/gemini-cli/pull/17590)
- revert bad changes to tests by @scidomino in
[#17673](https://github.com/google-gemini/gemini-cli/pull/17673)
- feat(cli): show candidate issue state reason and duplicate status in triage by
@sehoon38 in [#17676](https://github.com/google-gemini/gemini-cli/pull/17676)
- Fix missing slash commands when Gemini CLI is in a project with a package.json
that doesn't follow semantic versioning by @Adib234 in
[#17561](https://github.com/google-gemini/gemini-cli/pull/17561)
- feat(core): Model family-specific system prompts by @joshualitt in
[#17614](https://github.com/google-gemini/gemini-cli/pull/17614)
- Sub-agents documentation. by @gundermanc in
[#16639](https://github.com/google-gemini/gemini-cli/pull/16639)
- feat: wire up AskUserTool with dialog by @jackwotherspoon in
[#17411](https://github.com/google-gemini/gemini-cli/pull/17411)
- Load extension settings for hooks, agents, skills by @chrstnb in
[#17245](https://github.com/google-gemini/gemini-cli/pull/17245)
- Fix issue where Gemini CLI can make changes when simply asked a question by
@gundermanc in
[#17608](https://github.com/google-gemini/gemini-cli/pull/17608)
- Update docs-writer skill for editing and add style guide for reference. by
@g-samroberts in
[#17669](https://github.com/google-gemini/gemini-cli/pull/17669)
- fix(ux): have user message display a short path for pasted images by @devr0306
in [#17613](https://github.com/google-gemini/gemini-cli/pull/17613)
- feat(plan): enable AskUser tool in Plan mode for clarifying questions by
@jerop in [#17694](https://github.com/google-gemini/gemini-cli/pull/17694)
- GEMINI.md polish by @jacob314 in
[#17680](https://github.com/google-gemini/gemini-cli/pull/17680)
- refactor(core): centralize path validation and allow temp dir access for tools
by @NTaylorMullen in
[#17185](https://github.com/google-gemini/gemini-cli/pull/17185)
- feat(skills): promote Agent Skills to stable by @abhipatel12 in
[#17693](https://github.com/google-gemini/gemini-cli/pull/17693)
- refactor(cli): keyboard handling and AskUserDialog by @jacob314 in
[#17414](https://github.com/google-gemini/gemini-cli/pull/17414)
- docs: Add Experimental Remote Agent Docs by @adamfweidman in
[#17697](https://github.com/google-gemini/gemini-cli/pull/17697)
- revert: promote Agent Skills to stable (#17693) by @abhipatel12 in
[#17712](https://github.com/google-gemini/gemini-cli/pull/17712)
- feat(ux) Expandable (ctrl-O) and scrollable approvals in alternate buffer
mode. by @jacob314 in
[#17640](https://github.com/google-gemini/gemini-cli/pull/17640)
- feat(skills): promote skills settings to stable by @abhipatel12 in
[#17713](https://github.com/google-gemini/gemini-cli/pull/17713)
- fix(cli): Preserve settings dialog focus when searching by @SandyTao520 in
[#17701](https://github.com/google-gemini/gemini-cli/pull/17701)
- feat(ui): add terminal cursor support by @jacob314 in
[#17711](https://github.com/google-gemini/gemini-cli/pull/17711)
- docs(skills): remove experimental labels and update tutorials by @abhipatel12
in [#17714](https://github.com/google-gemini/gemini-cli/pull/17714)
- docs: remove 'experimental' syntax for hooks in docs by @abhipatel12 in
[#17660](https://github.com/google-gemini/gemini-cli/pull/17660)
- Add support for an additional exclusion file besides .gitignore and
.geminiignore by @alisa-alisa in
[#16487](https://github.com/google-gemini/gemini-cli/pull/16487)
- feat: add review-frontend-and-fix command by @galz10 in
[#17707](https://github.com/google-gemini/gemini-cli/pull/17707)
[#17852](https://github.com/google-gemini/gemini-cli/pull/17852)
- fix(cli): resolve environment loading and auth validation issues in ACP mode
by @bdmorgan in
[#18025](https://github.com/google-gemini/gemini-cli/pull/18025)
- feat(core): add .agents/skills directory alias for skill discovery by
@NTaylorMullen in
[#18151](https://github.com/google-gemini/gemini-cli/pull/18151)
- chore(core): reassign telemetry keys to avoid server conflict by @mattKorwel
in [#18161](https://github.com/google-gemini/gemini-cli/pull/18161)
- Add link to rewind doc in commands.md by @Adib234 in
[#17961](https://github.com/google-gemini/gemini-cli/pull/17961)
- feat(core): add draft-2020-12 JSON Schema support with lenient fallback by
@afarber in [#15060](https://github.com/google-gemini/gemini-cli/pull/15060)
- refactor(core): robust trimPreservingTrailingNewline and regression test by
@adamfweidman in
[#18196](https://github.com/google-gemini/gemini-cli/pull/18196)
- Remove MCP servers on extension uninstall by @chrstnb in
[#18121](https://github.com/google-gemini/gemini-cli/pull/18121)
- refactor: localize ACP error parsing logic to cli package by @bdmorgan in
[#18193](https://github.com/google-gemini/gemini-cli/pull/18193)
- feat(core): Add A2A auth config types by @adamfweidman in
[#18205](https://github.com/google-gemini/gemini-cli/pull/18205)
- Set default max attempts to 3 and use the common variable by @sehoon38 in
[#18209](https://github.com/google-gemini/gemini-cli/pull/18209)
- feat(plan): add exit_plan_mode ui and prompt by @jerop in
[#18162](https://github.com/google-gemini/gemini-cli/pull/18162)
- fix(test): improve test isolation and enable subagent evaluations by
@cocosheng-g in
[#18138](https://github.com/google-gemini/gemini-cli/pull/18138)
- feat(plan): use custom deny messages in plan mode policies by @Adib234 in
[#18195](https://github.com/google-gemini/gemini-cli/pull/18195)
- Match on extension ID when stopping extensions by @chrstnb in
[#18218](https://github.com/google-gemini/gemini-cli/pull/18218)
- fix(core): Respect user's .gitignore preference by @xyrolle in
[#15482](https://github.com/google-gemini/gemini-cli/pull/15482)
- docs: document GEMINI_CLI_HOME environment variable by @adamfweidman in
[#18219](https://github.com/google-gemini/gemini-cli/pull/18219)
- chore(core): explicitly state plan storage path in prompt by @jerop in
[#18222](https://github.com/google-gemini/gemini-cli/pull/18222)
- A2a admin setting by @DavidAPierce in
[#17868](https://github.com/google-gemini/gemini-cli/pull/17868)
- feat(a2a): Add pluggable auth provider infrastructure by @adamfweidman in
[#17934](https://github.com/google-gemini/gemini-cli/pull/17934)
- Fix handling of empty settings by @chrstnb in
[#18131](https://github.com/google-gemini/gemini-cli/pull/18131)
- Reload skills when extensions change by @chrstnb in
[#18225](https://github.com/google-gemini/gemini-cli/pull/18225)
- feat: Add markdown rendering to ask_user tool by @jackwotherspoon in
[#18211](https://github.com/google-gemini/gemini-cli/pull/18211)
- Add telemetry to rewind by @Adib234 in
[#18122](https://github.com/google-gemini/gemini-cli/pull/18122)
- feat(admin): add support for MCP configuration via admin controls (pt1) by
@skeshive in [#18223](https://github.com/google-gemini/gemini-cli/pull/18223)
- feat(core): require user consent before MCP server OAuth by @ehedlund in
[#18132](https://github.com/google-gemini/gemini-cli/pull/18132)
- fix(sandbox): propagate GOOGLE_GEMINI_BASE_URL&GOOGLE_VERTEX_BASE_URL env vars
by @skeshive in
[#18231](https://github.com/google-gemini/gemini-cli/pull/18231)
- feat(ui): move user identity display to header by @sehoon38 in
[#18216](https://github.com/google-gemini/gemini-cli/pull/18216)
- fix: enforce folder trust for workspace settings, skills, and context by
@galz10 in [#17596](https://github.com/google-gemini/gemini-cli/pull/17596)
**Full changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.26.0...v0.27.0
**Full Changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.27.0...v0.28.0
+344 -284
View File
@@ -1,6 +1,6 @@
# Preview release: Release v0.28.0-preview.0
# Preview release: Release v0.29.0-preview.0
Released: February 3, 2026
Released: February 10, 2026
Our preview release includes the latest, new, and experimental features. This
release may not be as stable as our [latest weekly release](latest.md).
@@ -13,295 +13,355 @@ npm install -g @google/gemini-cli@preview
## Highlights
- **Improved Hooks Management:** Hooks enable/disable functionality now aligns
with skills and offers improved completion.
- **Custom Themes for Extensions:** Extensions can now support custom themes,
allowing for greater personalization.
- **User Identity Display:** User identity information (auth, email, tier) is
now displayed on startup and in the `stats` command.
- **Plan Mode Enhancements:** Plan mode has been improved with a generic
`Checklist` component and refactored `Todo`.
- **Background Shell Commands:** Implementation of background shell commands.
- **Plan Mode Enhancements**: Significant updates to Plan Mode, including new
commands, support for MCP servers, integration of planning artifacts, and
improved iteration guidance.
- **Core Agent Improvements**: Enhancements to the core agent, including better
system prompt rigor, improved subagent definitions, and enhanced tool
execution limits.
- **CLI UX/UI Updates**: Various UI and UX improvements, such as autocomplete in
the input prompt, updated approval mode labels, DevTools integration, and
improved header spacing.
- **Tooling & Extension Updates**: Improvements to existing tools like
`ask_user` and `grep_search`, and new features for extension management.
- **Bug Fixes**: Numerous bug fixes across the CLI and core, addressing issues
with interactive commands, memory leaks, permission checks, and more.
- **Context and Tool Output Management**: Features for observation masking for
tool outputs, session-linked tool output storage, and persistence for masked
tool outputs.
## What's Changed
- feat(commands): add /prompt-suggest slash command by NTaylorMullen in
[#17264](https://github.com/google-gemini/gemini-cli/pull/17264)
- feat(cli): align hooks enable/disable with skills and improve completion by
sehoon38 in [#16822](https://github.com/google-gemini/gemini-cli/pull/16822)
- docs: add CLI reference documentation by leochiu-a in
[#17504](https://github.com/google-gemini/gemini-cli/pull/17504)
- chore(release): bump version to 0.28.0-nightly.20260128.adc8e11bb by
- fix: remove ask_user tool from non-interactive modes by jackwotherspoon in
[#18154](https://github.com/google-gemini/gemini-cli/pull/18154)
- fix(cli): allow restricted .env loading in untrusted sandboxed folders by
galz10 in [#17806](https://github.com/google-gemini/gemini-cli/pull/17806)
- Encourage agent to utilize ecosystem tools to perform work by gundermanc in
[#17881](https://github.com/google-gemini/gemini-cli/pull/17881)
- feat(plan): unify workflow location in system prompt to optimize caching by
jerop in [#18258](https://github.com/google-gemini/gemini-cli/pull/18258)
- feat(core): enable getUserTierName in config by sehoon38 in
[#18265](https://github.com/google-gemini/gemini-cli/pull/18265)
- feat(core): add default execution limits for subagents by abhipatel12 in
[#18274](https://github.com/google-gemini/gemini-cli/pull/18274)
- Fix issue where agent gets stuck at interactive commands. by gundermanc in
[#18272](https://github.com/google-gemini/gemini-cli/pull/18272)
- chore(release): bump version to 0.29.0-nightly.20260203.71f46f116 by
gemini-cli-robot in
[#17725](https://github.com/google-gemini/gemini-cli/pull/17725)
- feat(skills): final stable promotion cleanup by abhipatel12 in
[#17726](https://github.com/google-gemini/gemini-cli/pull/17726)
- test(core): mock fetch in OAuth transport fallback tests by jw409 in
[#17059](https://github.com/google-gemini/gemini-cli/pull/17059)
- feat(cli): include auth method in /bug by erikus in
[#17569](https://github.com/google-gemini/gemini-cli/pull/17569)
- Add a email privacy note to bug_report template by nemyung in
[#17474](https://github.com/google-gemini/gemini-cli/pull/17474)
- Rewind documentation by Adib234 in
[#17446](https://github.com/google-gemini/gemini-cli/pull/17446)
- fix: verify audio/video MIME types with content check by maru0804 in
[#16907](https://github.com/google-gemini/gemini-cli/pull/16907)
- feat(core): add support for positron ide (#15045) by kapsner in
[#15047](https://github.com/google-gemini/gemini-cli/pull/15047)
- /oncall dedup - wrap texts to nextlines by sehoon38 in
[#17782](https://github.com/google-gemini/gemini-cli/pull/17782)
- fix(admin): rename advanced features admin setting by skeshive in
[#17786](https://github.com/google-gemini/gemini-cli/pull/17786)
- [extension config] Make breaking optional value non-optional by chrstnb in
[#17785](https://github.com/google-gemini/gemini-cli/pull/17785)
- Fix docs-writer skill issues by g-samroberts in
[#17734](https://github.com/google-gemini/gemini-cli/pull/17734)
- fix(core): suppress duplicate hook failure warnings during streaming by
[#18243](https://github.com/google-gemini/gemini-cli/pull/18243)
- feat(core): remove hardcoded policy bypass for local subagents by abhipatel12
in [#18153](https://github.com/google-gemini/gemini-cli/pull/18153)
- feat(plan): implement plan slash command by Adib234 in
[#17698](https://github.com/google-gemini/gemini-cli/pull/17698)
- feat: increase ask_user label limit to 16 characters by jackwotherspoon in
[#18320](https://github.com/google-gemini/gemini-cli/pull/18320)
- Add information about the agent skills lifecycle and clarify docs-writer skill
metadata. by g-samroberts in
[#18234](https://github.com/google-gemini/gemini-cli/pull/18234)
- feat(core): add enter_plan_mode tool by jerop in
[#18324](https://github.com/google-gemini/gemini-cli/pull/18324)
- Stop showing an error message in /plan by Adib234 in
[#18333](https://github.com/google-gemini/gemini-cli/pull/18333)
- fix(hooks): remove unnecessary logging for hook registration by abhipatel12 in
[#18332](https://github.com/google-gemini/gemini-cli/pull/18332)
- fix(mcp): ensure MCP transport is closed to prevent memory leaks by cbcoutinho
in [#18054](https://github.com/google-gemini/gemini-cli/pull/18054)
- feat(skills): implement linking for agent skills by MushuEE in
[#18295](https://github.com/google-gemini/gemini-cli/pull/18295)
- Changelogs for 0.27.0 and 0.28.0-preview0 by g-samroberts in
[#18336](https://github.com/google-gemini/gemini-cli/pull/18336)
- chore: correct docs as skills and hooks are stable by jackwotherspoon in
[#18358](https://github.com/google-gemini/gemini-cli/pull/18358)
- feat(admin): Implement admin allowlist for MCP server configurations by
skeshive in [#18311](https://github.com/google-gemini/gemini-cli/pull/18311)
- fix(core): add retry logic for transient SSL/TLS errors
([#17318](https://github.com/google-gemini/gemini-cli/pull/17318)) by
ppgranger in [#18310](https://github.com/google-gemini/gemini-cli/pull/18310)
- Add support for /extensions config command by chrstnb in
[#17895](https://github.com/google-gemini/gemini-cli/pull/17895)
- fix(core): handle non-compliant mcpbridge responses from Xcode 26.3 by
peterfriese in
[#18376](https://github.com/google-gemini/gemini-cli/pull/18376)
- feat(cli): Add W, B, E Vim motions and operator support by ademuri in
[#16209](https://github.com/google-gemini/gemini-cli/pull/16209)
- fix: Windows Specific Agent Quality & System Prompt by scidomino in
[#18351](https://github.com/google-gemini/gemini-cli/pull/18351)
- feat(plan): support replace tool in plan mode to edit plans by jerop in
[#18379](https://github.com/google-gemini/gemini-cli/pull/18379)
- Improving memory tool instructions and eval testing by alisa-alisa in
[#18091](https://github.com/google-gemini/gemini-cli/pull/18091)
- fix(cli): color extension link success message green by MushuEE in
[#18386](https://github.com/google-gemini/gemini-cli/pull/18386)
- undo by jacob314 in
[#18147](https://github.com/google-gemini/gemini-cli/pull/18147)
- feat(plan): add guidance on iterating on approved plans vs creating new plans
by jerop in [#18346](https://github.com/google-gemini/gemini-cli/pull/18346)
- feat(plan): fix invalid tool calls in plan mode by Adib234 in
[#18352](https://github.com/google-gemini/gemini-cli/pull/18352)
- feat(plan): integrate planning artifacts and tools into primary workflows by
jerop in [#18375](https://github.com/google-gemini/gemini-cli/pull/18375)
- Fix permission check by scidomino in
[#18395](https://github.com/google-gemini/gemini-cli/pull/18395)
- ux(polish) autocomplete in the input prompt by jacob314 in
[#18181](https://github.com/google-gemini/gemini-cli/pull/18181)
- fix: resolve infinite loop when using 'Modify with external editor' by
ppgranger in [#17453](https://github.com/google-gemini/gemini-cli/pull/17453)
- feat: expand verify-release to macOS and Windows by yunaseoul in
[#18145](https://github.com/google-gemini/gemini-cli/pull/18145)
- feat(plan): implement support for MCP servers in Plan mode by Adib234 in
[#18229](https://github.com/google-gemini/gemini-cli/pull/18229)
- chore: update folder trust error messaging by galz10 in
[#18402](https://github.com/google-gemini/gemini-cli/pull/18402)
- feat(plan): create a metric for execution of plans generated in plan mode by
Adib234 in [#18236](https://github.com/google-gemini/gemini-cli/pull/18236)
- perf(ui): optimize stripUnsafeCharacters with regex by gsquared94 in
[#18413](https://github.com/google-gemini/gemini-cli/pull/18413)
- feat(context): implement observation masking for tool outputs by abhipatel12
in [#18389](https://github.com/google-gemini/gemini-cli/pull/18389)
- feat(core,cli): implement session-linked tool output storage and cleanup by
abhipatel12 in
[#17727](https://github.com/google-gemini/gemini-cli/pull/17727)
- test: add more tests for AskUser by jackwotherspoon in
[#17720](https://github.com/google-gemini/gemini-cli/pull/17720)
- feat(cli): enable activity logging for non-interactive mode and evals by
SandyTao520 in
[#17703](https://github.com/google-gemini/gemini-cli/pull/17703)
- feat(core): add support for custom deny messages in policy rules by
allenhutchison in
[#17427](https://github.com/google-gemini/gemini-cli/pull/17427)
- Fix unintended credential exposure to MCP Servers by Adib234 in
[#17311](https://github.com/google-gemini/gemini-cli/pull/17311)
- feat(extensions): add support for custom themes in extensions by spencer426 in
[#17327](https://github.com/google-gemini/gemini-cli/pull/17327)
- fix: persist and restore workspace directories on session resume by
korade-krushna in
[#17454](https://github.com/google-gemini/gemini-cli/pull/17454)
- Update release notes pages for 0.26.0 and 0.27.0-preview. by g-samroberts in
[#17744](https://github.com/google-gemini/gemini-cli/pull/17744)
- feat(ux): update cell border color and created test file for table rendering
by devr0306 in
[#17798](https://github.com/google-gemini/gemini-cli/pull/17798)
- Change height for the ToolConfirmationQueue. by jacob314 in
[#17799](https://github.com/google-gemini/gemini-cli/pull/17799)
- feat(cli): add user identity info to stats command by sehoon38 in
[#17612](https://github.com/google-gemini/gemini-cli/pull/17612)
- fix(ux): fixed off-by-some wrapping caused by fixed-width characters by
devr0306 in [#17816](https://github.com/google-gemini/gemini-cli/pull/17816)
- feat(cli): update undo/redo keybindings to Cmd+Z/Alt+Z and
Shift+Cmd+Z/Shift+Alt+Z by scidomino in
[#17800](https://github.com/google-gemini/gemini-cli/pull/17800)
- fix(evals): use absolute path for activity log directory by SandyTao520 in
[#17830](https://github.com/google-gemini/gemini-cli/pull/17830)
- test: add integration test to verify stdout/stderr routing by ved015 in
[#17280](https://github.com/google-gemini/gemini-cli/pull/17280)
- fix(cli): list installed extensions when update target missing by tt-a1i in
[#17082](https://github.com/google-gemini/gemini-cli/pull/17082)
- fix(cli): handle PAT tokens and credentials in git remote URL parsing by
afarber in [#14650](https://github.com/google-gemini/gemini-cli/pull/14650)
- fix(core): use returnDisplay for error result display by Nubebuster in
[#14994](https://github.com/google-gemini/gemini-cli/pull/14994)
- Fix detection of bun as package manager by Randomblock1 in
[#17462](https://github.com/google-gemini/gemini-cli/pull/17462)
- feat(cli): show hooksConfig.enabled in settings dialog by abhipatel12 in
[#17810](https://github.com/google-gemini/gemini-cli/pull/17810)
- feat(cli): Display user identity (auth, email, tier) on startup by yunaseoul
in [#17591](https://github.com/google-gemini/gemini-cli/pull/17591)
- fix: prevent ghost border for AskUserDialog by jackwotherspoon in
[#17788](https://github.com/google-gemini/gemini-cli/pull/17788)
- docs: mark A2A subagents as experimental in subagents.md by adamfweidman in
[#17863](https://github.com/google-gemini/gemini-cli/pull/17863)
- Resolve error thrown for sensitive values by chrstnb in
[#17826](https://github.com/google-gemini/gemini-cli/pull/17826)
- fix(admin): Rename secureModeEnabled to strictModeDisabled by skeshive in
[#17789](https://github.com/google-gemini/gemini-cli/pull/17789)
- feat(ux): update truncate dots to be shorter in tables by devr0306 in
[#17825](https://github.com/google-gemini/gemini-cli/pull/17825)
- fix(core): resolve DEP0040 punycode deprecation via patch-package by
ATHARVA262005 in
[#17692](https://github.com/google-gemini/gemini-cli/pull/17692)
- feat(plan): create generic Checklist component and refactor Todo by Adib234 in
[#17741](https://github.com/google-gemini/gemini-cli/pull/17741)
- Cleanup post delegate_to_agent removal by gundermanc in
[#17875](https://github.com/google-gemini/gemini-cli/pull/17875)
- fix(core): use GIT_CONFIG_GLOBAL to isolate shadow git repo configuration -
Fixes #17877 by cocosheng-g in
[#17803](https://github.com/google-gemini/gemini-cli/pull/17803)
- Disable mouse tracking e2e by alisa-alisa in
[#17880](https://github.com/google-gemini/gemini-cli/pull/17880)
- fix(cli): use correct setting key for Cloud Shell auth by sehoon38 in
[#17884](https://github.com/google-gemini/gemini-cli/pull/17884)
- chore: revert IDE specific ASCII logo by jackwotherspoon in
[#17887](https://github.com/google-gemini/gemini-cli/pull/17887)
- Revert "fix(core): resolve DEP0040 punycode deprecation via patch-package" by
sehoon38 in [#17898](https://github.com/google-gemini/gemini-cli/pull/17898)
- Refactoring of disabling of mouse tracking in e2e tests by alisa-alisa in
[#17902](https://github.com/google-gemini/gemini-cli/pull/17902)
- feat(core): Add GOOGLE_GENAI_API_VERSION environment variable support by deyim
in [#16177](https://github.com/google-gemini/gemini-cli/pull/16177)
- feat(core): Isolate and cleanup truncated tool outputs by SandyTao520 in
[#17594](https://github.com/google-gemini/gemini-cli/pull/17594)
- Create skills page, update commands, refine docs by g-samroberts in
[#17842](https://github.com/google-gemini/gemini-cli/pull/17842)
- feat: preserve EOL in files by Thomas-Shephard in
[#16087](https://github.com/google-gemini/gemini-cli/pull/16087)
- Fix HalfLinePaddedBox in screenreader mode. by jacob314 in
[#17914](https://github.com/google-gemini/gemini-cli/pull/17914)
- bug(ux) vim mode fixes. Start in insert mode. Fix bug blocking F12 and ctrl-X
in vim mode. by jacob314 in
[#17938](https://github.com/google-gemini/gemini-cli/pull/17938)
- feat(core): implement interactive and non-interactive consent for OAuth by
ehedlund in [#17699](https://github.com/google-gemini/gemini-cli/pull/17699)
- perf(core): optimize token calculation and add support for multimodal tool
responses by abhipatel12 in
[#17835](https://github.com/google-gemini/gemini-cli/pull/17835)
- refactor(hooks): remove legacy tools.enableHooks setting by abhipatel12 in
[#17867](https://github.com/google-gemini/gemini-cli/pull/17867)
- feat(ci): add npx smoke test to verify installability by bdmorgan in
[#17927](https://github.com/google-gemini/gemini-cli/pull/17927)
- feat(core): implement dynamic policy registration for subagents by abhipatel12
in [#17838](https://github.com/google-gemini/gemini-cli/pull/17838)
- feat: Implement background shell commands by galz10 in
[#14849](https://github.com/google-gemini/gemini-cli/pull/14849)
- feat(admin): provide actionable error messages for disabled features by
skeshive in [#17815](https://github.com/google-gemini/gemini-cli/pull/17815)
- Fix bugs where Rewind and Resume showed Ugly and 100X too verbose content. by
jacob314 in [#17940](https://github.com/google-gemini/gemini-cli/pull/17940)
- Fix broken link in docs by chrstnb in
[#17959](https://github.com/google-gemini/gemini-cli/pull/17959)
- feat(plan): reuse standard tool confirmation for AskUser tool by jerop in
[#17864](https://github.com/google-gemini/gemini-cli/pull/17864)
- feat(core): enable overriding CODE_ASSIST_API_VERSION with env var by
lottielin in [#17942](https://github.com/google-gemini/gemini-cli/pull/17942)
- run npx pointing to the specific commit SHA by sehoon38 in
[#17970](https://github.com/google-gemini/gemini-cli/pull/17970)
- Add allowedExtensions setting by kevinjwang1 in
[#17695](https://github.com/google-gemini/gemini-cli/pull/17695)
- feat(plan): refactor ToolConfirmationPayload to union type by jerop in
[#17980](https://github.com/google-gemini/gemini-cli/pull/17980)
- lower the default max retries to reduce contention by sehoon38 in
[#17975](https://github.com/google-gemini/gemini-cli/pull/17975)
- fix(core): ensure YOLO mode auto-approves complex shell commands when parsing
fails by abhipatel12 in
[#17920](https://github.com/google-gemini/gemini-cli/pull/17920)
- Fix broken link. by g-samroberts in
[#17972](https://github.com/google-gemini/gemini-cli/pull/17972)
- Support ctrl-C and Ctrl-D correctly Refactor so InputPrompt has priority over
AppContainer for input handling. by jacob314 in
[#17993](https://github.com/google-gemini/gemini-cli/pull/17993)
- Fix truncation for AskQuestion by jacob314 in
[#18001](https://github.com/google-gemini/gemini-cli/pull/18001)
- fix(workflow): update maintainer check logic to be inclusive and
case-insensitive by bdmorgan in
[#18009](https://github.com/google-gemini/gemini-cli/pull/18009)
- Fix Esc cancel during streaming by LyalinDotCom in
[#18039](https://github.com/google-gemini/gemini-cli/pull/18039)
- feat(acp): add session resume support by bdmorgan in
[#18043](https://github.com/google-gemini/gemini-cli/pull/18043)
- fix(ci): prevent stale PR closer from incorrectly closing new PRs by bdmorgan
in [#18069](https://github.com/google-gemini/gemini-cli/pull/18069)
- chore: delete autoAccept setting unused in production by victorvianna in
[#17862](https://github.com/google-gemini/gemini-cli/pull/17862)
- feat(plan): use placeholder for choice question "Other" option by jerop in
[#18101](https://github.com/google-gemini/gemini-cli/pull/18101)
- docs: update clearContext to hookSpecificOutput by jackwotherspoon in
[#18024](https://github.com/google-gemini/gemini-cli/pull/18024)
- docs-writer skill: Update docs writer skill by jkcinouye in
[#17928](https://github.com/google-gemini/gemini-cli/pull/17928)
- Sehoon/oncall filter by sehoon38 in
[#18105](https://github.com/google-gemini/gemini-cli/pull/18105)
- feat(core): add setting to disable loop detection by SandyTao520 in
[#18008](https://github.com/google-gemini/gemini-cli/pull/18008)
- Docs: Revise docs/index.md by jkcinouye in
[#17879](https://github.com/google-gemini/gemini-cli/pull/17879)
- Fix up/down arrow regression and add test. by jacob314 in
[#18108](https://github.com/google-gemini/gemini-cli/pull/18108)
- fix(ui): prevent content leak in MaxSizedBox bottom overflow by jerop in
[#17991](https://github.com/google-gemini/gemini-cli/pull/17991)
- refactor: migrate checks.ts utility to core and deduplicate by jerop in
[#18139](https://github.com/google-gemini/gemini-cli/pull/18139)
- feat(core): implement tool name aliasing for backward compatibility by
SandyTao520 in
[#17974](https://github.com/google-gemini/gemini-cli/pull/17974)
- docs: fix help-wanted label spelling by pavan-sh in
[#18114](https://github.com/google-gemini/gemini-cli/pull/18114)
- feat(cli): implement automatic theme switching based on terminal background by
[#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
[#17976](https://github.com/google-gemini/gemini-cli/pull/17976)
- fix(ide): no-op refactoring that moves the connection logic to helper
functions by skeshive in
[#18118](https://github.com/google-gemini/gemini-cli/pull/18118)
- feat: update review-frontend-and-fix slash command to review-and-fix by galz10
in [#18146](https://github.com/google-gemini/gemini-cli/pull/18146)
- fix: improve Ctrl+R reverse search by jackwotherspoon in
[#18075](https://github.com/google-gemini/gemini-cli/pull/18075)
- feat(plan): handle inconsistency in schedulers by Adib234 in
[#17813](https://github.com/google-gemini/gemini-cli/pull/17813)
- feat(plan): add core logic and exit_plan_mode tool definition by jerop in
[#18110](https://github.com/google-gemini/gemini-cli/pull/18110)
- feat(core): rename search_file_content tool to grep_search and add legacy
alias by SandyTao520 in
[#18003](https://github.com/google-gemini/gemini-cli/pull/18003)
- fix(core): prioritize detailed error messages for code assist setup by
gsquared94 in [#17852](https://github.com/google-gemini/gemini-cli/pull/17852)
- fix(cli): resolve environment loading and auth validation issues in ACP mode
by bdmorgan in
[#18025](https://github.com/google-gemini/gemini-cli/pull/18025)
- feat(core): add .agents/skills directory alias for skill discovery by
[#18241](https://github.com/google-gemini/gemini-cli/pull/18241)
- fix(core): prevent subagent bypass in plan mode by jerop in
[#18484](https://github.com/google-gemini/gemini-cli/pull/18484)
- feat(cli): add WebSocket-based network logging and streaming chunk support by
SandyTao520 in
[#18383](https://github.com/google-gemini/gemini-cli/pull/18383)
- feat(cli): update approval modes UI by jerop in
[#18476](https://github.com/google-gemini/gemini-cli/pull/18476)
- fix(cli): reload skills and agents on extension restart by NTaylorMullen in
[#18411](https://github.com/google-gemini/gemini-cli/pull/18411)
- fix(core): expand excludeTools with legacy aliases for renamed tools by
SandyTao520 in
[#18498](https://github.com/google-gemini/gemini-cli/pull/18498)
- feat(core): overhaul system prompt for rigor, integrity, and intent alignment
by NTaylorMullen in
[#17263](https://github.com/google-gemini/gemini-cli/pull/17263)
- Patch for generate changelog docs yaml file by g-samroberts in
[#18496](https://github.com/google-gemini/gemini-cli/pull/18496)
- Code review fixes for show question mark pr. by jacob314 in
[#18480](https://github.com/google-gemini/gemini-cli/pull/18480)
- fix(cli): add SS3 Shift+Tab support for Windows terminals by ThanhNguyxn in
[#18187](https://github.com/google-gemini/gemini-cli/pull/18187)
- chore: remove redundant planning prompt from final shell by jerop in
[#18528](https://github.com/google-gemini/gemini-cli/pull/18528)
- docs: require pr-creator skill for PR generation by NTaylorMullen in
[#18536](https://github.com/google-gemini/gemini-cli/pull/18536)
- chore: update colors for ask_user dialog by jackwotherspoon in
[#18543](https://github.com/google-gemini/gemini-cli/pull/18543)
- feat(core): exempt high-signal tools from output masking by abhipatel12 in
[#18545](https://github.com/google-gemini/gemini-cli/pull/18545)
- refactor(core): remove memory tool instructions from Gemini 3 prompt by
NTaylorMullen in
[#18151](https://github.com/google-gemini/gemini-cli/pull/18151)
- chore(core): reassign telemetry keys to avoid server conflict by mattKorwel in
[#18161](https://github.com/google-gemini/gemini-cli/pull/18161)
- Add link to rewind doc in commands.md by Adib234 in
[#17961](https://github.com/google-gemini/gemini-cli/pull/17961)
- feat(core): add draft-2020-12 JSON Schema support with lenient fallback by
afarber in [#15060](https://github.com/google-gemini/gemini-cli/pull/15060)
- refactor(core): robust trimPreservingTrailingNewline and regression test by
[#18559](https://github.com/google-gemini/gemini-cli/pull/18559)
- chore: remove feedback instruction from system prompt by NTaylorMullen in
[#18560](https://github.com/google-gemini/gemini-cli/pull/18560)
- feat(context): add remote configuration for tool output masking thresholds by
abhipatel12 in
[#18553](https://github.com/google-gemini/gemini-cli/pull/18553)
- feat(core): pause agent timeout budget while waiting for tool confirmation by
abhipatel12 in
[#18415](https://github.com/google-gemini/gemini-cli/pull/18415)
- refactor(config): remove experimental.enableEventDrivenScheduler setting by
abhipatel12 in
[#17924](https://github.com/google-gemini/gemini-cli/pull/17924)
- feat(cli): truncate shell output in UI history and improve active shell
display by jwhelangoog in
[#17438](https://github.com/google-gemini/gemini-cli/pull/17438)
- refactor(cli): switch useToolScheduler to event-driven engine by abhipatel12
in [#18565](https://github.com/google-gemini/gemini-cli/pull/18565)
- fix(core): correct escaped interpolation in system prompt by NTaylorMullen in
[#18557](https://github.com/google-gemini/gemini-cli/pull/18557)
- propagate abortSignal by scidomino in
[#18477](https://github.com/google-gemini/gemini-cli/pull/18477)
- feat(core): conditionally include ctrl+f prompt based on interactive shell
setting by NTaylorMullen in
[#18561](https://github.com/google-gemini/gemini-cli/pull/18561)
- fix(core): ensure enter_plan_mode tool registration respects experimental.plan
by jerop in [#18587](https://github.com/google-gemini/gemini-cli/pull/18587)
- feat(core): transition sub-agents to XML format and improve definitions by
NTaylorMullen in
[#18555](https://github.com/google-gemini/gemini-cli/pull/18555)
- docs: Add Plan Mode documentation by jerop in
[#18582](https://github.com/google-gemini/gemini-cli/pull/18582)
- chore: strengthen validation guidance in system prompt by NTaylorMullen in
[#18544](https://github.com/google-gemini/gemini-cli/pull/18544)
- Fix newline insertion bug in replace tool by werdnum in
[#18595](https://github.com/google-gemini/gemini-cli/pull/18595)
- fix(evals): update save_memory evals and simplify tool description by
NTaylorMullen in
[#18610](https://github.com/google-gemini/gemini-cli/pull/18610)
- chore(evals): update validation_fidelity_pre_existing_errors to USUALLY_PASSES
by NTaylorMullen in
[#18617](https://github.com/google-gemini/gemini-cli/pull/18617)
- fix: shorten tool call IDs and fix duplicate tool name in truncated output
filenames by SandyTao520 in
[#18600](https://github.com/google-gemini/gemini-cli/pull/18600)
- feat(cli): implement atomic writes and safety checks for trusted folders by
galz10 in [#18406](https://github.com/google-gemini/gemini-cli/pull/18406)
- Remove relative docs links by chrstnb in
[#18650](https://github.com/google-gemini/gemini-cli/pull/18650)
- docs: add legacy snippets convention to GEMINI.md by NTaylorMullen in
[#18597](https://github.com/google-gemini/gemini-cli/pull/18597)
- fix(chore): Support linting for cjs by aswinashok44 in
[#18639](https://github.com/google-gemini/gemini-cli/pull/18639)
- feat: move shell efficiency guidelines to tool description by NTaylorMullen in
[#18614](https://github.com/google-gemini/gemini-cli/pull/18614)
- Added "" as default value, since getText() used to expect a string only and
thus crashed when undefined... Fixes #18076 by 019-Abhi in
[#18099](https://github.com/google-gemini/gemini-cli/pull/18099)
- Allow @-includes outside of workspaces (with permission) by scidomino in
[#18470](https://github.com/google-gemini/gemini-cli/pull/18470)
- chore: make ask_user header description more clear by jackwotherspoon in
[#18657](https://github.com/google-gemini/gemini-cli/pull/18657)
- refactor(core): model-dependent tool definitions by aishaneeshah in
[#18563](https://github.com/google-gemini/gemini-cli/pull/18563)
- Harded code assist converter. by jacob314 in
[#18656](https://github.com/google-gemini/gemini-cli/pull/18656)
- bug(core): Fix minor bug in migration logic. by joshualitt in
[#18661](https://github.com/google-gemini/gemini-cli/pull/18661)
- feat: enable plan mode experiment in settings by jerop in
[#18636](https://github.com/google-gemini/gemini-cli/pull/18636)
- refactor: push isValidPath() into parsePastedPaths() by scidomino in
[#18664](https://github.com/google-gemini/gemini-cli/pull/18664)
- fix(cli): correct 'esc to cancel' position and restore duration display by
NTaylorMullen in
[#18534](https://github.com/google-gemini/gemini-cli/pull/18534)
- feat(cli): add DevTools integration with gemini-cli-devtools by SandyTao520 in
[#18648](https://github.com/google-gemini/gemini-cli/pull/18648)
- chore: remove unused exports and redundant hook files by SandyTao520 in
[#18681](https://github.com/google-gemini/gemini-cli/pull/18681)
- Fix number of lines being reported in rewind confirmation dialog by Adib234 in
[#18675](https://github.com/google-gemini/gemini-cli/pull/18675)
- feat(cli): disable folder trust in headless mode by galz10 in
[#18407](https://github.com/google-gemini/gemini-cli/pull/18407)
- Disallow unsafe type assertions by gundermanc in
[#18688](https://github.com/google-gemini/gemini-cli/pull/18688)
- Change event type for release by g-samroberts in
[#18693](https://github.com/google-gemini/gemini-cli/pull/18693)
- feat: handle multiple dynamic context filenames in system prompt by
NTaylorMullen in
[#18598](https://github.com/google-gemini/gemini-cli/pull/18598)
- Properly parse at-commands with narrow non-breaking spaces by scidomino in
[#18677](https://github.com/google-gemini/gemini-cli/pull/18677)
- refactor(core): centralize core tool definitions and support model-specific
schemas by aishaneeshah in
[#18662](https://github.com/google-gemini/gemini-cli/pull/18662)
- feat(core): Render memory hierarchically in context. by joshualitt in
[#18350](https://github.com/google-gemini/gemini-cli/pull/18350)
- feat: Ctrl+O to expand paste placeholder by jackwotherspoon in
[#18103](https://github.com/google-gemini/gemini-cli/pull/18103)
- fix(cli): Improve header spacing by NTaylorMullen in
[#18531](https://github.com/google-gemini/gemini-cli/pull/18531)
- Feature/quota visibility 16795 by spencer426 in
[#18203](https://github.com/google-gemini/gemini-cli/pull/18203)
- Inline thinking bubbles with summary/full modes by LyalinDotCom in
[#18033](https://github.com/google-gemini/gemini-cli/pull/18033)
- docs: remove TOC marker from Plan Mode header by jerop in
[#18678](https://github.com/google-gemini/gemini-cli/pull/18678)
- fix(ui): remove redundant newlines in Gemini messages by NTaylorMullen in
[#18538](https://github.com/google-gemini/gemini-cli/pull/18538)
- test(cli): fix AppContainer act() warnings and improve waitFor resilience by
NTaylorMullen in
[#18676](https://github.com/google-gemini/gemini-cli/pull/18676)
- refactor(core): refine Security & System Integrity section in system prompt by
NTaylorMullen in
[#18601](https://github.com/google-gemini/gemini-cli/pull/18601)
- Fix layout rounding. by gundermanc in
[#18667](https://github.com/google-gemini/gemini-cli/pull/18667)
- docs(skills): enhance pr-creator safety and interactivity by NTaylorMullen in
[#18616](https://github.com/google-gemini/gemini-cli/pull/18616)
- test(core): remove hardcoded model from TestRig by NTaylorMullen in
[#18710](https://github.com/google-gemini/gemini-cli/pull/18710)
- feat(core): optimize sub-agents system prompt intro by NTaylorMullen in
[#18608](https://github.com/google-gemini/gemini-cli/pull/18608)
- feat(cli): update approval mode labels and shortcuts per latest UX spec by
jerop in [#18698](https://github.com/google-gemini/gemini-cli/pull/18698)
- fix(plan): update persistent approval mode setting by Adib234 in
[#18638](https://github.com/google-gemini/gemini-cli/pull/18638)
- fix: move toasts location to left side by jackwotherspoon in
[#18705](https://github.com/google-gemini/gemini-cli/pull/18705)
- feat(routing): restrict numerical routing to Gemini 3 family by mattKorwel in
[#18478](https://github.com/google-gemini/gemini-cli/pull/18478)
- fix(ide): fix ide nudge setting by skeshive in
[#18733](https://github.com/google-gemini/gemini-cli/pull/18733)
- fix(core): standardize tool formatting in system prompts by NTaylorMullen in
[#18615](https://github.com/google-gemini/gemini-cli/pull/18615)
- chore: consolidate to green in ask user dialog by jackwotherspoon in
[#18734](https://github.com/google-gemini/gemini-cli/pull/18734)
- feat: add extensionsExplore setting to enable extensions explore UI. by
sripasg in [#18686](https://github.com/google-gemini/gemini-cli/pull/18686)
- feat(cli): defer devtools startup and integrate with F12 by SandyTao520 in
[#18695](https://github.com/google-gemini/gemini-cli/pull/18695)
- ui: update & subdue footer colors and animate progress indicator by
keithguerin in
[#18570](https://github.com/google-gemini/gemini-cli/pull/18570)
- test: add model-specific snapshots for coreTools by aishaneeshah in
[#18707](https://github.com/google-gemini/gemini-cli/pull/18707)
- ci: shard windows tests and fix event listener leaks by NTaylorMullen in
[#18670](https://github.com/google-gemini/gemini-cli/pull/18670)
- fix: allow ask_user tool in yolo mode by jackwotherspoon in
[#18541](https://github.com/google-gemini/gemini-cli/pull/18541)
- feat: redact disabled tools from system prompt
([#13597](https://github.com/google-gemini/gemini-cli/pull/13597)) by
NTaylorMullen in
[#18613](https://github.com/google-gemini/gemini-cli/pull/18613)
- Update Gemini.md to use the curent year on creating new files by sehoon38 in
[#18460](https://github.com/google-gemini/gemini-cli/pull/18460)
- Code review cleanup for thinking display by jacob314 in
[#18720](https://github.com/google-gemini/gemini-cli/pull/18720)
- fix(cli): hide scrollbars when in alternate buffer copy mode by werdnum in
[#18354](https://github.com/google-gemini/gemini-cli/pull/18354)
- Fix issues with rip grep by gundermanc in
[#18756](https://github.com/google-gemini/gemini-cli/pull/18756)
- fix(cli): fix history navigation regression after prompt autocomplete by
sehoon38 in [#18752](https://github.com/google-gemini/gemini-cli/pull/18752)
- chore: cleanup unused and add unlisted dependencies in packages/cli by
adamfweidman in
[#18196](https://github.com/google-gemini/gemini-cli/pull/18196)
- Remove MCP servers on extension uninstall by chrstnb in
[#18121](https://github.com/google-gemini/gemini-cli/pull/18121)
- refactor: localize ACP error parsing logic to cli package by bdmorgan in
[#18193](https://github.com/google-gemini/gemini-cli/pull/18193)
- feat(core): Add A2A auth config types by adamfweidman in
[#18205](https://github.com/google-gemini/gemini-cli/pull/18205)
- Set default max attempts to 3 and use the common variable by sehoon38 in
[#18209](https://github.com/google-gemini/gemini-cli/pull/18209)
- feat(plan): add exit_plan_mode ui and prompt by jerop in
[#18162](https://github.com/google-gemini/gemini-cli/pull/18162)
- fix(test): improve test isolation and enable subagent evaluations by
cocosheng-g in
[#18138](https://github.com/google-gemini/gemini-cli/pull/18138)
- feat(plan): use custom deny messages in plan mode policies by Adib234 in
[#18195](https://github.com/google-gemini/gemini-cli/pull/18195)
- Match on extension ID when stopping extensions by chrstnb in
[#18218](https://github.com/google-gemini/gemini-cli/pull/18218)
- fix(core): Respect user's .gitignore preference by xyrolle in
[#15482](https://github.com/google-gemini/gemini-cli/pull/15482)
- docs: document GEMINI_CLI_HOME environment variable by adamfweidman in
[#18219](https://github.com/google-gemini/gemini-cli/pull/18219)
- chore(core): explicitly state plan storage path in prompt by jerop in
[#18222](https://github.com/google-gemini/gemini-cli/pull/18222)
- A2a admin setting by DavidAPierce in
[#17868](https://github.com/google-gemini/gemini-cli/pull/17868)
- feat(a2a): Add pluggable auth provider infrastructure by adamfweidman in
[#17934](https://github.com/google-gemini/gemini-cli/pull/17934)
- Fix handling of empty settings by chrstnb in
[#18131](https://github.com/google-gemini/gemini-cli/pull/18131)
- Reload skills when extensions change by chrstnb in
[#18225](https://github.com/google-gemini/gemini-cli/pull/18225)
- feat: Add markdown rendering to ask_user tool by jackwotherspoon in
[#18211](https://github.com/google-gemini/gemini-cli/pull/18211)
- Add telemetry to rewind by Adib234 in
[#18122](https://github.com/google-gemini/gemini-cli/pull/18122)
- feat(admin): add support for MCP configuration via admin controls (pt1) by
skeshive in [#18223](https://github.com/google-gemini/gemini-cli/pull/18223)
- feat(core): require user consent before MCP server OAuth by ehedlund in
[#18132](https://github.com/google-gemini/gemini-cli/pull/18132)
- fix(sandbox): propagate GOOGLE_GEMINI_BASE_URL&GOOGLE_VERTEX_BASE_URL env vars
by skeshive in
[#18231](https://github.com/google-gemini/gemini-cli/pull/18231)
- feat(ui): move user identity display to header by sehoon38 in
[#18216](https://github.com/google-gemini/gemini-cli/pull/18216)
- fix: enforce folder trust for workspace settings, skills, and context by
galz10 in [#17596](https://github.com/google-gemini/gemini-cli/pull/17596)
[#18749](https://github.com/google-gemini/gemini-cli/pull/18749)
- Fix issue where Gemini CLI creates tests in a new file by gundermanc in
[#18409](https://github.com/google-gemini/gemini-cli/pull/18409)
- feat(telemetry): Ensure experiment IDs are included in OpenTelemetry logs by
kevin-ramdass in
[#18747](https://github.com/google-gemini/gemini-cli/pull/18747)
**Full changelog**:
https://github.com/google-gemini/gemini-cli/compare/v0.27.0-preview.8...v0.28.0-preview.0
https://github.com/google-gemini/gemini-cli/compare/v0.28.0-preview.0...v0.29.0-preview.0
+23 -23
View File
@@ -27,29 +27,29 @@ and parameters.
## CLI Options
| Option | Alias | Type | Default | Description |
| -------------------------------- | ----- | ------- | --------- | ---------------------------------------------------------------------------------------------------------- |
| `--debug` | `-d` | boolean | `false` | Run in debug mode with verbose logging |
| `--version` | `-v` | - | - | Show CLI version number and exit |
| `--help` | `-h` | - | - | Show help information |
| `--model` | `-m` | string | `auto` | Model to use. See [Model Selection](#model-selection) for available values. |
| `--prompt` | `-p` | string | - | Prompt text. Appended to stdin input if provided. **Deprecated:** Use positional arguments instead. |
| `--prompt-interactive` | `-i` | string | - | Execute prompt and continue in interactive mode |
| `--sandbox` | `-s` | boolean | `false` | Run in a sandboxed environment for safer execution |
| `--approval-mode` | - | string | `default` | Approval mode for tool execution. Choices: `default`, `auto_edit`, `yolo` |
| `--yolo` | `-y` | boolean | `false` | **Deprecated.** Auto-approve all actions. Use `--approval-mode=yolo` instead. |
| `--experimental-acp` | - | boolean | - | Start in ACP (Agent Code Pilot) mode. **Experimental feature.** |
| `--experimental-zed-integration` | - | boolean | - | Run in Zed editor integration mode. **Experimental feature.** |
| `--allowed-mcp-server-names` | - | array | - | Allowed MCP server names (comma-separated or multiple flags) |
| `--allowed-tools` | - | array | - | Tools that are allowed to run without confirmation (comma-separated or multiple flags) |
| `--extensions` | `-e` | array | - | List of extensions to use. If not provided, all extensions are enabled (comma-separated or multiple flags) |
| `--list-extensions` | `-l` | boolean | - | List all available extensions and exit |
| `--resume` | `-r` | string | - | Resume a previous session. Use `"latest"` for most recent or index number (e.g. `--resume 5`) |
| `--list-sessions` | - | boolean | - | List available sessions for the current project and exit |
| `--delete-session` | - | string | - | Delete a session by index number (use `--list-sessions` to see available sessions) |
| `--include-directories` | - | array | - | Additional directories to include in the workspace (comma-separated or multiple flags) |
| `--screen-reader` | - | boolean | - | Enable screen reader mode for accessibility |
| `--output-format` | `-o` | string | `text` | The format of the CLI output. Choices: `text`, `json`, `stream-json` |
| Option | Alias | Type | Default | Description |
| -------------------------------- | ----- | ------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--debug` | `-d` | boolean | `false` | Run in debug mode with verbose logging |
| `--version` | `-v` | - | - | Show CLI version number and exit |
| `--help` | `-h` | - | - | Show help information |
| `--model` | `-m` | string | `auto` | Model to use. See [Model Selection](#model-selection) for available values. |
| `--prompt` | `-p` | string | - | Prompt text. Appended to stdin input if provided. **Deprecated:** Use positional arguments instead. |
| `--prompt-interactive` | `-i` | string | - | Execute prompt and continue in interactive mode |
| `--sandbox` | `-s` | boolean | `false` | Run in a sandboxed environment for safer execution |
| `--approval-mode` | - | string | `default` | Approval mode for tool execution. Choices: `default`, `auto_edit`, `yolo` |
| `--yolo` | `-y` | boolean | `false` | **Deprecated.** Auto-approve all actions. Use `--approval-mode=yolo` instead. |
| `--experimental-acp` | - | boolean | - | Start in ACP (Agent Code Pilot) mode. **Experimental feature.** |
| `--experimental-zed-integration` | - | boolean | - | Run in Zed editor integration mode. **Experimental feature.** |
| `--allowed-mcp-server-names` | - | array | - | Allowed MCP server names (comma-separated or multiple flags) |
| `--allowed-tools` | - | array | - | **Deprecated.** Use the [Policy Engine](../core/policy-engine.md) instead. Tools that are allowed to run without confirmation (comma-separated or multiple flags) |
| `--extensions` | `-e` | array | - | List of extensions to use. If not provided, all extensions are enabled (comma-separated or multiple flags) |
| `--list-extensions` | `-l` | boolean | - | List all available extensions and exit |
| `--resume` | `-r` | string | - | Resume a previous session. Use `"latest"` for most recent or index number (e.g. `--resume 5`) |
| `--list-sessions` | - | boolean | - | List available sessions for the current project and exit |
| `--delete-session` | - | string | - | Delete a session by index number (use `--list-sessions` to see available sessions) |
| `--include-directories` | - | array | - | Additional directories to include in the workspace (comma-separated or multiple flags) |
| `--screen-reader` | - | boolean | - | Enable screen reader mode for accessibility |
| `--output-format` | `-o` | string | `text` | The format of the CLI output. Choices: `text`, `json`, `stream-json` |
## Model selection
+7 -4
View File
@@ -223,9 +223,9 @@ gemini
## Restricting tool access
You can significantly enhance security by controlling which tools the Gemini
model can use. This is achieved through the `tools.core` and `tools.exclude`
settings. For a list of available tools, see the
[Tools documentation](../tools/index.md).
model can use. This is achieved through the `tools.core` setting and the
[Policy Engine](../core/policy-engine.md). For a list of available tools, see
the [Tools documentation](../tools/index.md).
### Allowlisting with `coreTools`
@@ -243,7 +243,10 @@ on the approved list.
}
```
### Blocklisting with `excludeTools`
### Blocklisting with `excludeTools` (Deprecated)
> **Deprecated:** Use the [Policy Engine](../core/policy-engine.md) for more
> robust control.
Alternatively, you can add specific tools that are considered dangerous in your
environment to a blocklist.
+5 -4
View File
@@ -120,7 +120,7 @@ available combinations.
| Move focus from the shell back to Gemini. | `Shift + Tab` |
| Clear the terminal screen and redraw the UI. | `Ctrl + L` |
| Restart the application. | `R` |
| Suspend the application (not yet implemented). | `Ctrl + Z` |
| Suspend the CLI and move it to the background. | `Ctrl + Z` |
<!-- KEYBINDINGS-AUTOGEN:END -->
@@ -130,9 +130,10 @@ available combinations.
terminal isn't configured to send Meta with Option.
- `!` on an empty prompt: Enter or exit shell mode.
- `?` on an empty prompt: Toggle the shortcuts panel above the input. Press
`Esc`, `Backspace`, or any printable key to close it. Press `?` again to close
the panel and insert a `?` into the prompt. You can hide only the hint text
via `ui.showShortcutsHint`, without changing this keyboard behavior.
`Esc`, `Backspace`, any printable key, or a registered app hotkey to close it.
The panel also auto-hides while the agent is running/streaming or when
action-required dialogs are shown. Press `?` again to close the panel and
insert a `?` into the prompt.
- `\` (at end of a line) + `Enter`: Insert a newline without leaving single-line
mode.
- `Esc` pressed twice quickly: Clear the input prompt if it is not empty,
+1 -1
View File
@@ -99,7 +99,7 @@ These are the only allowed tools:
- **MCP Tools (Read):** Read-only [MCP tools] (e.g., `github_read_issue`,
`postgres_read_schema`) are allowed.
- **Planning (Write):** [`write_file`] and [`replace`] ONLY allowed for `.md`
files in the `~/.gemini/tmp/<project>/plans/` directory.
files in the `~/.gemini/tmp/<project>/<session-id>/plans/` directory.
- **Skills:** [`activate_skill`] (allows loading specialized instructions and
resources in a read-only manner)
+3 -2
View File
@@ -82,10 +82,11 @@ gemini -p "run the test suite"
Built-in profiles (set via `SEATBELT_PROFILE` env var):
- `permissive-open` (default): Write restrictions, network allowed
- `permissive-closed`: Write restrictions, no network
- `permissive-proxied`: Write restrictions, network via proxy
- `restrictive-open`: Strict restrictions, network allowed
- `restrictive-closed`: Maximum restrictions
- `restrictive-proxied`: Strict restrictions, network via proxy
- `strict-open`: Read and write restrictions, network allowed
- `strict-proxied`: Read and write restrictions, network via proxy
### Custom sandbox flags
+18 -4
View File
@@ -275,9 +275,9 @@ For local development and debugging, you can capture telemetry data locally:
The following section describes the structure of logs and metrics generated for
Gemini CLI.
The `session.id`, `installation.id`, and `user.email` (available only when
authenticated with a Google account) are included as common attributes on all
logs and metrics.
The `session.id`, `installation.id`, `active_approval_mode`, and `user.email`
(available only when authenticated with a Google account) are included as common
attributes on all logs and metrics.
### Logs
@@ -360,7 +360,21 @@ Captures tool executions, output truncation, and Edit behavior.
- `extension_name` (string, if applicable)
- `extension_id` (string, if applicable)
- `content_length` (int, if applicable)
- `metadata` (if applicable)
- `metadata` (if applicable), which includes for the `AskUser` tool:
- `ask_user` (object):
- `question_types` (array of strings)
- `ask_user_dismissed` (boolean)
- `ask_user_empty_submission` (boolean)
- `ask_user_answer_count` (number)
- `diffStat` (if applicable), which includes:
- `model_added_lines` (number)
- `model_removed_lines` (number)
- `model_added_chars` (number)
- `model_removed_chars` (number)
- `user_added_lines` (number)
- `user_removed_lines` (number)
- `user_added_chars` (number)
- `user_removed_chars` (number)
- `gemini_cli.tool_output_truncated`: Output of a tool call was truncated.
- **Attributes**:
-32
View File
@@ -154,38 +154,6 @@ A rule matches a tool call if all of its conditions are met:
Policies are defined in `.toml` files. The CLI loads these files from Default,
User, and (if configured) Admin directories.
### Policy locations
| Tier | Type | Location |
| :-------- | :----- | :-------------------------- |
| **User** | Custom | `~/.gemini/policies/*.toml` |
| **Admin** | System | _See below (OS specific)_ |
#### System-wide policies (Admin)
Administrators can enforce system-wide policies (Tier 3) that override all user
and default settings. These policies must be placed in specific, secure
directories:
| OS | Policy Directory Path |
| :---------- | :------------------------------------------------ |
| **Linux** | `/etc/gemini-cli/policies` |
| **macOS** | `/Library/Application Support/GeminiCli/policies` |
| **Windows** | `C:\ProgramData\gemini-cli\policies` |
**Security Requirements:**
To prevent privilege escalation, the CLI enforces strict security checks on
admin directories. If checks fail, system policies are **ignored**.
- **Linux / macOS:** Must be owned by `root` (UID 0) and NOT writable by group
or others (e.g., `chmod 755`).
- **Windows:** Must be in `C:\ProgramData`. Standard users (`Users`, `Everyone`)
must NOT have `Write`, `Modify`, or `Full Control` permissions. _Tip: If you
see a security warning, use the folder properties to remove write permissions
for non-admin groups. You may need to "Disable inheritance" in Advanced
Security Settings._
### TOML rule schema
Here is a breakdown of the fields available in a TOML policy rule:
+5 -3
View File
@@ -166,19 +166,21 @@ a few things you can try in order of recommendation:
- **Default:** All tools available for use by the Gemini model.
- **Example:** `"coreTools": ["ReadFileTool", "GlobTool", "ShellTool(ls)"]`.
- **`allowedTools`** (array of strings):
- **`allowedTools`** (array of strings) [DEPRECATED]:
- **Default:** `undefined`
- **Description:** A list of tool names that will bypass the confirmation
dialog. This is useful for tools that you trust and use frequently. The
match semantics are the same as `coreTools`.
match semantics are the same as `coreTools`. **Deprecated**: Use the
[Policy Engine](../core/policy-engine.md) instead.
- **Example:** `"allowedTools": ["ShellTool(git status)"]`.
- **`excludeTools`** (array of strings):
- **`excludeTools`** (array of strings) [DEPRECATED]:
- **Description:** Allows you to specify a list of core tool names that should
be excluded from the model. A tool listed in both `excludeTools` and
`coreTools` is excluded. You can also specify command-specific restrictions
for tools that support it, like the `ShellTool`. For example,
`"excludeTools": ["ShellTool(rm -rf)"]` will block the `rm -rf` command.
**Deprecated**: Use the [Policy Engine](../core/policy-engine.md) instead.
- **Default**: No tools excluded.
- **Example:** `"excludeTools": ["run_shell_command", "findFiles"]`.
- **Security Note:** Command-specific restrictions in `excludeTools` for
+4 -1
View File
@@ -1290,7 +1290,10 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file.
few other folders, see
`packages/cli/src/utils/sandbox-macos-permissive-open.sb`) but allows other
operations.
- `strict`: Uses a strict profile that declines operations by default.
- `restrictive-open`: Declines operations by default, allows network.
- `strict-open`: Restricts both reads and writes to the working directory,
allows network.
- `strict-proxied`: Same as `strict-open` but routes network through proxy.
- `<profile_name>`: Uses a custom profile. To define a custom profile, create
a file named `sandbox-macos-<profile_name>.sb` in your project's `.gemini/`
directory (e.g., `my-project/.gemini/sandbox-macos-custom.sb`).
+5 -4
View File
@@ -167,10 +167,11 @@ configuration file.
`"tools": {"core": ["run_shell_command(git)"]}` will only allow `git`
commands. Including the generic `run_shell_command` acts as a wildcard,
allowing any command not explicitly blocked.
- `tools.exclude`: To block specific commands, add entries to the `exclude` list
under the `tools` category in the format `run_shell_command(<command>)`. For
example, `"tools": {"exclude": ["run_shell_command(rm)"]}` will block `rm`
commands.
- `tools.exclude` [DEPRECATED]: To block specific commands, use the
[Policy Engine](../core/policy-engine.md). Historically, this setting allowed
adding entries to the `exclude` list under the `tools` category in the format
`run_shell_command(<command>)`. For example,
`"tools": {"exclude": ["run_shell_command(rm)"]}` will block `rm` commands.
The validation logic is designed to be secure and flexible:
+2
View File
@@ -17245,6 +17245,7 @@
"@google/gemini-cli-core": "file:../core",
"express": "^5.1.0",
"fs-extra": "^11.3.0",
"strip-json-comments": "^3.1.1",
"tar": "^7.5.2",
"uuid": "^13.0.0",
"winston": "^3.17.0"
@@ -17253,6 +17254,7 @@
"gemini-cli-a2a-server": "dist/a2a-server.mjs"
},
"devDependencies": {
"@google/genai": "^1.30.0",
"@types/express": "^5.0.3",
"@types/fs-extra": "^11.0.4",
"@types/supertest": "^6.0.3",
+2
View File
@@ -30,11 +30,13 @@
"@google/gemini-cli-core": "file:../core",
"express": "^5.1.0",
"fs-extra": "^11.3.0",
"strip-json-comments": "^3.1.1",
"tar": "^7.5.2",
"uuid": "^13.0.0",
"winston": "^3.17.0"
},
"devDependencies": {
"@google/genai": "^1.30.0",
"@types/express": "^5.0.3",
"@types/fs-extra": "^11.0.4",
"@types/supertest": "^6.0.3",
+8 -4
View File
@@ -177,7 +177,8 @@ export async function parseArguments(
type: 'array',
string: true,
nargs: 1,
description: 'Tools that are allowed to run without confirmation',
description:
'[DEPRECATED: Use Policy Engine instead See https://geminicli.com/docs/core/policy-engine] Tools that are allowed to run without confirmation',
coerce: (tools: string[]) =>
// Handle comma-separated values
tools.flatMap((tool) => tool.split(',').map((t) => t.trim())),
@@ -445,7 +446,11 @@ export async function loadCliConfig(
process.env['VITEST'] === 'true'
? false
: (settings.security?.folderTrust?.enabled ?? false);
const trustedFolder = isWorkspaceTrusted(settings, cwd)?.isTrusted ?? false;
const trustedFolder =
isWorkspaceTrusted(settings, cwd, undefined, {
prompt: argv.prompt,
query: argv.query,
})?.isTrusted ?? false;
// Set the context filename in the server's memoryTool module BEFORE loading memory
// TODO(b/343434939): This is a bit of a hack. The contextFileName should ideally be passed
@@ -602,8 +607,7 @@ export async function loadCliConfig(
const interactive =
!!argv.promptInteractive ||
!!argv.experimentalAcp ||
(!isHeadlessMode({ prompt: argv.prompt }) &&
!argv.query &&
(!isHeadlessMode({ prompt: argv.prompt, query: argv.query }) &&
!argv.isCommand);
const allowedTools = argv.allowedTools || settings.tools?.allowed || [];
+1 -1
View File
@@ -523,5 +523,5 @@ export const commandDescriptions: Readonly<Record<Command, string>> = {
[Command.UNFOCUS_SHELL_INPUT]: 'Move focus from the shell back to Gemini.',
[Command.CLEAR_SCREEN]: 'Clear the terminal screen and redraw the UI.',
[Command.RESTART_APP]: 'Restart the application.',
[Command.SUSPEND_APP]: 'Suspend the application (not yet implemented).',
[Command.SUSPEND_APP]: 'Suspend the CLI and move it to the background.',
};
@@ -336,9 +336,9 @@ describe('Policy Engine Integration Tests', () => {
// Valid plan file paths
const validPaths = [
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/my-plan.md',
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/feature_auth.md',
'/home/user/.gemini/tmp/new-temp_dir_123/plans/plan.md', // new style of temp directory
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/session-1/plans/my-plan.md',
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/session-1/plans/feature_auth.md',
'/home/user/.gemini/tmp/new-temp_dir_123/session-1/plans/plan.md', // new style of temp directory
];
for (const file_path of validPaths) {
@@ -365,7 +365,6 @@ describe('Policy Engine Integration Tests', () => {
'/project/src/file.ts', // Workspace
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/script.js', // Wrong extension
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/../../../etc/passwd.md', // Path traversal
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/subdir/plan.md', // Subdirectory
'/home/user/.gemini/non-tmp/new-temp_dir_123/plans/plan.md', // outside of temp dir
];
@@ -449,6 +449,14 @@ describe('Trusted Folders', () => {
false,
);
});
it('should return true for isPathTrusted when isHeadlessMode is true', async () => {
const geminiCore = await import('@google/gemini-cli-core');
vi.spyOn(geminiCore, 'isHeadlessMode').mockReturnValue(true);
const folders = loadTrustedFolders();
expect(folders.isPathTrusted('/any-untrusted-path')).toBe(true);
});
});
describe('Trusted Folders Caching', () => {
+18 -3
View File
@@ -17,6 +17,7 @@ import {
homedir,
isHeadlessMode,
coreEvents,
type HeadlessModeOptions,
} from '@google/gemini-cli-core';
import type { Settings } from './settings.js';
import stripJsonComments from 'strip-json-comments';
@@ -128,7 +129,11 @@ export class LoadedTrustedFolders {
isPathTrusted(
location: string,
config?: Record<string, TrustLevel>,
headlessOptions?: HeadlessModeOptions,
): boolean | undefined {
if (isHeadlessMode(headlessOptions)) {
return true;
}
const configToUse = config ?? this.user.config;
// Resolve location to its realpath for canonical comparison
@@ -333,6 +338,7 @@ export function isFolderTrustEnabled(settings: Settings): boolean {
function getWorkspaceTrustFromLocalConfig(
workspaceDir: string,
trustConfig?: Record<string, TrustLevel>,
headlessOptions?: HeadlessModeOptions,
): TrustResult {
const folders = loadTrustedFolders();
const configToUse = trustConfig ?? folders.user.config;
@@ -346,7 +352,11 @@ function getWorkspaceTrustFromLocalConfig(
);
}
const isTrusted = folders.isPathTrusted(workspaceDir, configToUse);
const isTrusted = folders.isPathTrusted(
workspaceDir,
configToUse,
headlessOptions,
);
return {
isTrusted,
source: isTrusted !== undefined ? 'file' : undefined,
@@ -357,8 +367,9 @@ export function isWorkspaceTrusted(
settings: Settings,
workspaceDir: string = process.cwd(),
trustConfig?: Record<string, TrustLevel>,
headlessOptions?: HeadlessModeOptions,
): TrustResult {
if (isHeadlessMode()) {
if (isHeadlessMode(headlessOptions)) {
return { isTrusted: true, source: undefined };
}
@@ -372,5 +383,9 @@ export function isWorkspaceTrusted(
}
// Fall back to the local user configuration
return getWorkspaceTrustFromLocalConfig(workspaceDir, trustConfig);
return getWorkspaceTrustFromLocalConfig(
workspaceDir,
trustConfig,
headlessOptions,
);
}
+26
View File
@@ -104,6 +104,7 @@ import { TerminalProvider } from './ui/contexts/TerminalContext.js';
import { setupTerminalAndTheme } from './utils/terminalTheme.js';
import { profiler } from './ui/components/DebugProfiler.js';
import { runDeferredCommand } from './deferred.js';
import { SlashCommandConflictHandler } from './services/SlashCommandConflictHandler.js';
const SLOW_RENDER_MS = 200;
@@ -335,6 +336,11 @@ export async function main() {
});
setupUnhandledRejectionHandler();
const slashCommandConflictHandler = new SlashCommandConflictHandler();
slashCommandConflictHandler.start();
registerCleanup(() => slashCommandConflictHandler.stop());
const loadSettingsHandle = startupProfiler.start('load_settings');
const settings = loadSettings();
loadSettingsHandle?.end();
@@ -361,6 +367,26 @@ export async function main() {
const argv = await parseArguments(settings.merged);
parseArgsHandle?.end();
if (
(argv.allowedTools && argv.allowedTools.length > 0) ||
(settings.merged.tools?.allowed && settings.merged.tools.allowed.length > 0)
) {
coreEvents.emitFeedback(
'warning',
'Warning: --allowed-tools cli argument and tools.allowed in settings.json are deprecated and will be removed in 1.0: Migrate to Policy Engine: https://geminicli.com/docs/core/policy-engine/',
);
}
if (
settings.merged.tools?.exclude &&
settings.merged.tools.exclude.length > 0
) {
coreEvents.emitFeedback(
'warning',
'Warning: tools.exclude in settings.json is deprecated and will be removed in 1.0. Migrate to Policy Engine: https://geminicli.com/docs/core/policy-engine/',
);
}
if (argv.startupMessages) {
argv.startupMessages.forEach((msg) => {
coreEvents.emitFeedback('info', msg);
@@ -350,4 +350,117 @@ describe('CommandService', () => {
expect(deployExtension).toBeDefined();
expect(deployExtension?.description).toBe('[gcp] Deploy to Google Cloud');
});
it('should report conflicts via getConflicts', async () => {
const builtinCommand = createMockCommand('deploy', CommandKind.BUILT_IN);
const extensionCommand = {
...createMockCommand('deploy', CommandKind.FILE),
extensionName: 'firebase',
};
const mockLoader = new MockCommandLoader([
builtinCommand,
extensionCommand,
]);
const service = await CommandService.create(
[mockLoader],
new AbortController().signal,
);
const conflicts = service.getConflicts();
expect(conflicts).toHaveLength(1);
expect(conflicts[0]).toMatchObject({
name: 'deploy',
winner: builtinCommand,
losers: [
{
renamedTo: 'firebase.deploy',
command: expect.objectContaining({
name: 'deploy',
extensionName: 'firebase',
}),
},
],
});
});
it('should report extension vs extension conflicts correctly', async () => {
// Both extensions try to register 'deploy'
const extension1Command = {
...createMockCommand('deploy', CommandKind.FILE),
extensionName: 'firebase',
};
const extension2Command = {
...createMockCommand('deploy', CommandKind.FILE),
extensionName: 'aws',
};
const mockLoader = new MockCommandLoader([
extension1Command,
extension2Command,
]);
const service = await CommandService.create(
[mockLoader],
new AbortController().signal,
);
const conflicts = service.getConflicts();
expect(conflicts).toHaveLength(1);
expect(conflicts[0]).toMatchObject({
name: 'deploy',
winner: expect.objectContaining({
name: 'deploy',
extensionName: 'firebase',
}),
losers: [
{
renamedTo: 'aws.deploy', // ext2 is 'aws' and it lost because it was second in the list
command: expect.objectContaining({
name: 'deploy',
extensionName: 'aws',
}),
},
],
});
});
it('should report multiple conflicts for the same command name', async () => {
const builtinCommand = createMockCommand('deploy', CommandKind.BUILT_IN);
const ext1 = {
...createMockCommand('deploy', CommandKind.FILE),
extensionName: 'ext1',
};
const ext2 = {
...createMockCommand('deploy', CommandKind.FILE),
extensionName: 'ext2',
};
const mockLoader = new MockCommandLoader([builtinCommand, ext1, ext2]);
const service = await CommandService.create(
[mockLoader],
new AbortController().signal,
);
const conflicts = service.getConflicts();
expect(conflicts).toHaveLength(1);
expect(conflicts[0].name).toBe('deploy');
expect(conflicts[0].losers).toHaveLength(2);
expect(conflicts[0].losers).toEqual(
expect.arrayContaining([
expect.objectContaining({
renamedTo: 'ext1.deploy',
command: expect.objectContaining({ extensionName: 'ext1' }),
}),
expect.objectContaining({
renamedTo: 'ext2.deploy',
command: expect.objectContaining({ extensionName: 'ext2' }),
}),
]),
);
});
});
+56 -3
View File
@@ -4,10 +4,19 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { debugLogger } from '@google/gemini-cli-core';
import { debugLogger, coreEvents } from '@google/gemini-cli-core';
import type { SlashCommand } from '../ui/commands/types.js';
import type { ICommandLoader } from './types.js';
export interface CommandConflict {
name: string;
winner: SlashCommand;
losers: Array<{
command: SlashCommand;
renamedTo: string;
}>;
}
/**
* Orchestrates the discovery and loading of all slash commands for the CLI.
*
@@ -23,8 +32,12 @@ export class CommandService {
/**
* Private constructor to enforce the use of the async factory.
* @param commands A readonly array of the fully loaded and de-duplicated commands.
* @param conflicts A readonly array of conflicts that occurred during loading.
*/
private constructor(private readonly commands: readonly SlashCommand[]) {}
private constructor(
private readonly commands: readonly SlashCommand[],
private readonly conflicts: readonly CommandConflict[],
) {}
/**
* Asynchronously creates and initializes a new CommandService instance.
@@ -63,11 +76,14 @@ export class CommandService {
}
const commandMap = new Map<string, SlashCommand>();
const conflictsMap = new Map<string, CommandConflict>();
for (const cmd of allCommands) {
let finalName = cmd.name;
// Extension commands get renamed if they conflict with existing commands
if (cmd.extensionName && commandMap.has(cmd.name)) {
const winner = commandMap.get(cmd.name)!;
let renamedName = `${cmd.extensionName}.${cmd.name}`;
let suffix = 1;
@@ -78,6 +94,19 @@ export class CommandService {
}
finalName = renamedName;
if (!conflictsMap.has(cmd.name)) {
conflictsMap.set(cmd.name, {
name: cmd.name,
winner,
losers: [],
});
}
conflictsMap.get(cmd.name)!.losers.push({
command: cmd,
renamedTo: finalName,
});
}
commandMap.set(finalName, {
@@ -86,8 +115,23 @@ export class CommandService {
});
}
const conflicts = Array.from(conflictsMap.values());
if (conflicts.length > 0) {
coreEvents.emitSlashCommandConflicts(
conflicts.flatMap((c) =>
c.losers.map((l) => ({
name: c.name,
renamedTo: l.renamedTo,
loserExtensionName: l.command.extensionName,
winnerExtensionName: c.winner.extensionName,
})),
),
);
}
const finalCommands = Object.freeze(Array.from(commandMap.values()));
return new CommandService(finalCommands);
const finalConflicts = Object.freeze(conflicts);
return new CommandService(finalCommands, finalConflicts);
}
/**
@@ -101,4 +145,13 @@ export class CommandService {
getCommands(): readonly SlashCommand[] {
return this.commands;
}
/**
* Retrieves the list of conflicts that occurred during command loading.
*
* @returns A readonly array of command conflicts.
*/
getConflicts(): readonly CommandConflict[] {
return this.conflicts;
}
}
@@ -0,0 +1,54 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
coreEvents,
CoreEvent,
type SlashCommandConflictsPayload,
} from '@google/gemini-cli-core';
export class SlashCommandConflictHandler {
private notifiedConflicts = new Set<string>();
constructor() {
this.handleConflicts = this.handleConflicts.bind(this);
}
start() {
coreEvents.on(CoreEvent.SlashCommandConflicts, this.handleConflicts);
}
stop() {
coreEvents.off(CoreEvent.SlashCommandConflicts, this.handleConflicts);
}
private handleConflicts(payload: SlashCommandConflictsPayload) {
const newConflicts = payload.conflicts.filter((c) => {
const key = `${c.name}:${c.loserExtensionName}`;
if (this.notifiedConflicts.has(key)) {
return false;
}
this.notifiedConflicts.add(key);
return true;
});
if (newConflicts.length > 0) {
const conflictMessages = newConflicts
.map((c) => {
const winnerSource = c.winnerExtensionName
? `extension '${c.winnerExtensionName}'`
: 'an existing command';
return `- Command '/${c.name}' from extension '${c.loserExtensionName}' was renamed to '/${c.renamedTo}' because it conflicts with ${winnerSource}.`;
})
.join('\n');
coreEvents.emitFeedback(
'info',
`Command conflicts detected:\n${conflictMessages}`,
);
}
}
}
@@ -18,6 +18,7 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
getSandbox: vi.fn(() => undefined),
getQuestion: vi.fn(() => ''),
isInteractive: vi.fn(() => false),
isInitialized: vi.fn(() => true),
setTerminalBackground: vi.fn(),
storage: {
getProjectTempDir: vi.fn().mockReturnValue('/tmp/gemini-test'),
-4
View File
@@ -220,10 +220,6 @@ describe('App', () => {
} as UIState;
const configWithExperiment = makeFakeConfig();
vi.spyOn(
configWithExperiment,
'isEventDrivenSchedulerEnabled',
).mockReturnValue(true);
vi.spyOn(configWithExperiment, 'isTrustedFolder').mockReturnValue(true);
vi.spyOn(configWithExperiment, 'getIdeMode').mockReturnValue(false);
+172 -28
View File
@@ -20,7 +20,7 @@ import { cleanup } from 'ink-testing-library';
import { act, useContext, type ReactElement } from 'react';
import { AppContainer } from './AppContainer.js';
import { SettingsContext } from './contexts/SettingsContext.js';
import { type TrackedToolCall } from './hooks/useReactToolScheduler.js';
import { type TrackedToolCall } from './hooks/useToolScheduler.js';
import {
type Config,
makeFakeConfig,
@@ -135,6 +135,7 @@ vi.mock('./hooks/vim.js');
vi.mock('./hooks/useFocus.js');
vi.mock('./hooks/useBracketedPaste.js');
vi.mock('./hooks/useLoadingIndicator.js');
vi.mock('./hooks/useSuspend.js');
vi.mock('./hooks/useFolderTrust.js');
vi.mock('./hooks/useIdeTrustListener.js');
vi.mock('./hooks/useMessageQueue.js');
@@ -197,7 +198,9 @@ import { useTextBuffer } from './components/shared/text-buffer.js';
import { useLogger } from './hooks/useLogger.js';
import { useLoadingIndicator } from './hooks/useLoadingIndicator.js';
import { useInputHistoryStore } from './hooks/useInputHistoryStore.js';
import { useKeypress } from './hooks/useKeypress.js';
import { useKeypress, type Key } from './hooks/useKeypress.js';
import * as useKeypressModule from './hooks/useKeypress.js';
import { useSuspend } from './hooks/useSuspend.js';
import { measureElement } from 'ink';
import { useTerminalSize } from './hooks/useTerminalSize.js';
import {
@@ -270,6 +273,7 @@ describe('AppContainer State Management', () => {
const mockedUseTextBuffer = useTextBuffer as Mock;
const mockedUseLogger = useLogger as Mock;
const mockedUseLoadingIndicator = useLoadingIndicator as Mock;
const mockedUseSuspend = useSuspend as Mock;
const mockedUseInputHistoryStore = useInputHistoryStore as Mock;
const mockedUseHookDisplayState = useHookDisplayState as Mock;
const mockedUseTerminalTheme = useTerminalTheme as Mock;
@@ -401,6 +405,9 @@ describe('AppContainer State Management', () => {
elapsedTime: '0.0s',
currentLoadingPhrase: '',
});
mockedUseSuspend.mockReturnValue({
handleSuspend: vi.fn(),
});
mockedUseHookDisplayState.mockReturnValue([]);
mockedUseTerminalTheme.mockReturnValue(undefined);
mockedUseShellInactivityStatus.mockReturnValue({
@@ -440,8 +447,8 @@ describe('AppContainer State Management', () => {
...defaultMergedSettings.ui,
showStatusInTitle: false,
hideWindowTitle: false,
useAlternateBuffer: false,
},
useAlternateBuffer: false,
},
} as unknown as LoadedSettings;
@@ -727,10 +734,10 @@ describe('AppContainer State Management', () => {
getChatRecordingService: vi.fn(() => mockChatRecordingService),
};
const configWithRecording = {
...mockConfig,
getGeminiClient: vi.fn(() => mockGeminiClient),
} as unknown as Config;
const configWithRecording = makeFakeConfig();
vi.spyOn(configWithRecording, 'getGeminiClient').mockReturnValue(
mockGeminiClient as unknown as ReturnType<Config['getGeminiClient']>,
);
expect(() => {
renderAppContainer({
@@ -761,11 +768,13 @@ describe('AppContainer State Management', () => {
setHistory: vi.fn(),
};
const configWithRecording = {
...mockConfig,
getGeminiClient: vi.fn(() => mockGeminiClient),
getSessionId: vi.fn(() => 'test-session-123'),
} as unknown as Config;
const configWithRecording = makeFakeConfig();
vi.spyOn(configWithRecording, 'getGeminiClient').mockReturnValue(
mockGeminiClient as unknown as ReturnType<Config['getGeminiClient']>,
);
vi.spyOn(configWithRecording, 'getSessionId').mockReturnValue(
'test-session-123',
);
expect(() => {
renderAppContainer({
@@ -801,10 +810,10 @@ describe('AppContainer State Management', () => {
getUserTier: vi.fn(),
};
const configWithRecording = {
...mockConfig,
getGeminiClient: vi.fn(() => mockGeminiClient),
} as unknown as Config;
const configWithRecording = makeFakeConfig();
vi.spyOn(configWithRecording, 'getGeminiClient').mockReturnValue(
mockGeminiClient as unknown as ReturnType<Config['getGeminiClient']>,
);
renderAppContainer({
config: configWithRecording,
@@ -835,10 +844,10 @@ describe('AppContainer State Management', () => {
})),
};
const configWithClient = {
...mockConfig,
getGeminiClient: vi.fn(() => mockGeminiClient),
} as unknown as Config;
const configWithClient = makeFakeConfig();
vi.spyOn(configWithClient, 'getGeminiClient').mockReturnValue(
mockGeminiClient as unknown as ReturnType<Config['getGeminiClient']>,
);
const resumedData = {
conversation: {
@@ -891,10 +900,10 @@ describe('AppContainer State Management', () => {
getChatRecordingService: vi.fn(),
};
const configWithClient = {
...mockConfig,
getGeminiClient: vi.fn(() => mockGeminiClient),
} as unknown as Config;
const configWithClient = makeFakeConfig();
vi.spyOn(configWithClient, 'getGeminiClient').mockReturnValue(
mockGeminiClient as unknown as ReturnType<Config['getGeminiClient']>,
);
const resumedData = {
conversation: {
@@ -944,10 +953,10 @@ describe('AppContainer State Management', () => {
getUserTier: vi.fn(),
};
const configWithRecording = {
...mockConfig,
getGeminiClient: vi.fn(() => mockGeminiClient),
} as unknown as Config;
const configWithRecording = makeFakeConfig();
vi.spyOn(configWithRecording, 'getGeminiClient').mockReturnValue(
mockGeminiClient as unknown as ReturnType<Config['getGeminiClient']>,
);
renderAppContainer({
config: configWithRecording,
@@ -1942,6 +1951,19 @@ describe('AppContainer State Management', () => {
});
});
describe('CTRL+Z', () => {
it('should call handleSuspend', async () => {
const handleSuspend = vi.fn();
mockedUseSuspend.mockReturnValue({ handleSuspend });
await setupKeypressTest();
pressKey('\x1A'); // Ctrl+Z
expect(handleSuspend).toHaveBeenCalledTimes(1);
unmount();
});
});
describe('Focus Handling (Tab / Shift+Tab)', () => {
beforeEach(() => {
// Mock activePtyId to enable focus
@@ -2091,6 +2113,128 @@ describe('AppContainer State Management', () => {
});
});
describe('Shortcuts Help Visibility', () => {
let handleGlobalKeypress: (key: Key) => boolean;
let mockedUseKeypress: Mock;
let rerender: () => void;
let unmount: () => void;
const setupShortcutsVisibilityTest = async () => {
const renderResult = renderAppContainer();
await act(async () => {
vi.advanceTimersByTime(0);
});
rerender = () => renderResult.rerender(getAppContainer());
unmount = renderResult.unmount;
};
const pressKey = (key: Partial<Key>) => {
act(() => {
handleGlobalKeypress({
name: 'r',
shift: false,
alt: false,
ctrl: false,
cmd: false,
insertable: false,
sequence: '',
...key,
} as Key);
});
rerender();
};
beforeEach(() => {
mockedUseKeypress = vi.spyOn(useKeypressModule, 'useKeypress') as Mock;
mockedUseKeypress.mockImplementation(
(callback: (key: Key) => boolean, options: { isActive: boolean }) => {
// AppContainer registers multiple keypress handlers; capture only
// active handlers so inactive copy-mode handler doesn't override.
if (options?.isActive) {
handleGlobalKeypress = callback;
}
},
);
vi.useFakeTimers();
});
afterEach(() => {
mockedUseKeypress.mockRestore();
vi.useRealTimers();
vi.restoreAllMocks();
});
it('dismisses shortcuts help when a registered hotkey is pressed', async () => {
await setupShortcutsVisibilityTest();
act(() => {
capturedUIActions.setShortcutsHelpVisible(true);
});
rerender();
expect(capturedUIState.shortcutsHelpVisible).toBe(true);
pressKey({ name: 'r', ctrl: true, sequence: '\x12' }); // Ctrl+R
expect(capturedUIState.shortcutsHelpVisible).toBe(false);
unmount();
});
it('dismisses shortcuts help when streaming starts', async () => {
await setupShortcutsVisibilityTest();
act(() => {
capturedUIActions.setShortcutsHelpVisible(true);
});
rerender();
expect(capturedUIState.shortcutsHelpVisible).toBe(true);
mockedUseGeminiStream.mockReturnValue({
...DEFAULT_GEMINI_STREAM_MOCK,
streamingState: 'responding',
});
await act(async () => {
rerender();
});
await waitFor(() => {
expect(capturedUIState.shortcutsHelpVisible).toBe(false);
});
unmount();
});
it('dismisses shortcuts help when action-required confirmation appears', async () => {
await setupShortcutsVisibilityTest();
act(() => {
capturedUIActions.setShortcutsHelpVisible(true);
});
rerender();
expect(capturedUIState.shortcutsHelpVisible).toBe(true);
mockedUseSlashCommandProcessor.mockReturnValue({
handleSlashCommand: vi.fn(),
slashCommands: [],
pendingHistoryItems: [],
commandContext: {},
shellConfirmationRequest: null,
confirmationRequest: {
prompt: 'Confirm this action?',
onConfirm: vi.fn(),
},
});
await act(async () => {
rerender();
});
await waitFor(() => {
expect(capturedUIState.shortcutsHelpVisible).toBe(false);
});
unmount();
});
});
describe('Copy Mode (CTRL+S)', () => {
let rerender: () => void;
let unmount: () => void;
+78 -19
View File
@@ -12,7 +12,14 @@ import {
useRef,
useLayoutEffect,
} from 'react';
import { type DOMElement, measureElement } from 'ink';
import {
type DOMElement,
measureElement,
useApp,
useStdout,
useStdin,
type AppProps,
} from 'ink';
import { App } from './App.js';
import { AppContext } from './contexts/AppContext.js';
import { UIStateContext, type UIState } from './contexts/UIStateContext.js';
@@ -87,7 +94,6 @@ import { useVimMode } from './contexts/VimModeContext.js';
import { useConsoleMessages } from './hooks/useConsoleMessages.js';
import { useTerminalSize } from './hooks/useTerminalSize.js';
import { calculatePromptWidths } from './components/InputPrompt.js';
import { useApp, useStdout, useStdin } from 'ink';
import { calculateMainAreaWidth } from './utils/ui-sizing.js';
import ansiEscapes from 'ansi-escapes';
import { basename } from 'node:path';
@@ -146,7 +152,8 @@ import { NewAgentsChoice } from './components/NewAgentsNotification.js';
import { isSlashCommand } from './utils/commandUtils.js';
import { useTerminalTheme } from './hooks/useTerminalTheme.js';
import { useTimedMessage } from './hooks/useTimedMessage.js';
import { isITerm2 } from './utils/terminalUtils.js';
import { shouldDismissShortcutsHelpOnHotkey } from './utils/shortcutsHelp.js';
import { useSuspend } from './hooks/useSuspend.js';
function isToolExecuting(pendingHistoryItems: HistoryItemWithoutId[]) {
return pendingHistoryItems.some((item) => {
@@ -200,6 +207,7 @@ export const AppContainer = (props: AppContainerProps) => {
useMemoryMonitor(historyManager);
const isAlternateBuffer = useAlternateBuffer();
const [corgiMode, setCorgiMode] = useState(false);
const [forceRerenderKey, setForceRerenderKey] = useState(0);
const [debugMessage, setDebugMessage] = useState<string>('');
const [quittingMessages, setQuittingMessages] = useState<
HistoryItem[] | null
@@ -346,7 +354,7 @@ export const AppContainer = (props: AppContainerProps) => {
const { columns: terminalWidth, rows: terminalHeight } = useTerminalSize();
const { stdin, setRawMode } = useStdin();
const { stdout } = useStdout();
const app = useApp();
const app: AppProps = useApp();
// Additional hooks moved from App.tsx
const { stats: sessionStats } = useSessionStats();
@@ -535,10 +543,13 @@ export const AppContainer = (props: AppContainerProps) => {
setHistoryRemountKey((prev) => prev + 1);
}, [setHistoryRemountKey, isAlternateBuffer, stdout]);
const shouldUseAlternateScreen = shouldEnterAlternateScreen(
isAlternateBuffer,
config.getScreenReader(),
);
const handleEditorClose = useCallback(() => {
if (
shouldEnterAlternateScreen(isAlternateBuffer, config.getScreenReader())
) {
if (shouldUseAlternateScreen) {
// The editor may have exited alternate buffer mode so we need to
// enter it again to be safe.
enterAlternateScreen();
@@ -548,7 +559,7 @@ export const AppContainer = (props: AppContainerProps) => {
}
terminalCapabilityManager.enableSupportedModes();
refreshStatic();
}, [refreshStatic, isAlternateBuffer, app, config]);
}, [refreshStatic, shouldUseAlternateScreen, app]);
const [editorError, setEditorError] = useState<string | null>(null);
const {
@@ -1369,6 +1380,24 @@ Logging in with Google... Restarting Gemini CLI to continue.
};
}, [showTransientMessage]);
const handleWarning = useCallback(
(message: string) => {
showTransientMessage({
text: message,
type: TransientMessageType.Warning,
});
},
[showTransientMessage],
);
const { handleSuspend } = useSuspend({
handleWarning,
setRawMode,
refreshStatic,
setForceRerenderKey,
shouldUseAlternateScreen,
});
useEffect(() => {
if (ideNeedsRestart) {
// IDE trust changed, force a restart.
@@ -1489,6 +1518,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
debugLogger.log('[DEBUG] Keystroke:', JSON.stringify(key));
}
if (shortcutsHelpVisible && shouldDismissShortcutsHelpOnHotkey(key)) {
setShortcutsHelpVisible(false);
}
if (isAlternateBuffer && keyMatchers[Command.TOGGLE_COPY_MODE](key)) {
setCopyModeEnabled(true);
disableMouseEvents();
@@ -1505,6 +1538,9 @@ Logging in with Google... Restarting Gemini CLI to continue.
} else if (keyMatchers[Command.EXIT](key)) {
setCtrlDPressCount((prev) => prev + 1);
return true;
} else if (keyMatchers[Command.SUSPEND_APP](key)) {
handleSuspend();
return true;
}
let enteringConstrainHeightMode = false;
@@ -1530,15 +1566,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
setShowErrorDetails((prev) => !prev);
}
return true;
} else if (keyMatchers[Command.SUSPEND_APP](key)) {
const undoMessage = isITerm2()
? 'Undo has been moved to Option + Z'
: 'Undo has been moved to Alt/Option + Z or Cmd + Z';
showTransientMessage({
text: undoMessage,
type: TransientMessageType.Warning,
});
return true;
} else if (keyMatchers[Command.SHOW_FULL_TODOS](key)) {
setShowFullTodos((prev) => !prev);
return true;
@@ -1647,18 +1674,20 @@ Logging in with Google... Restarting Gemini CLI to continue.
handleSlashCommand,
cancelOngoingRequest,
activePtyId,
handleSuspend,
embeddedShellFocused,
settings.merged.general.debugKeystrokeLogging,
refreshStatic,
setCopyModeEnabled,
tabFocusTimeoutRef,
isAlternateBuffer,
shortcutsHelpVisible,
backgroundCurrentShell,
toggleBackgroundShell,
backgroundShells,
isBackgroundShellVisible,
setIsBackgroundShellListOpen,
lastOutputTimeRef,
tabFocusTimeoutRef,
showTransientMessage,
settings.merged.general.devtools,
showErrorDetails,
@@ -1811,6 +1840,36 @@ Logging in with Google... Restarting Gemini CLI to continue.
[pendingSlashCommandHistoryItems, pendingGeminiHistoryItems],
);
const hasPendingToolConfirmation = useMemo(
() => isToolAwaitingConfirmation(pendingHistoryItems),
[pendingHistoryItems],
);
const hasPendingActionRequired =
hasPendingToolConfirmation ||
!!commandConfirmationRequest ||
!!authConsentRequest ||
confirmUpdateExtensionRequests.length > 0 ||
!!loopDetectionConfirmationRequest ||
!!proQuotaRequest ||
!!validationRequest ||
!!customDialog;
const isPassiveShortcutsHelpState =
isInputActive &&
streamingState === StreamingState.Idle &&
!hasPendingActionRequired;
useEffect(() => {
if (shortcutsHelpVisible && !isPassiveShortcutsHelpState) {
setShortcutsHelpVisible(false);
}
}, [
shortcutsHelpVisible,
isPassiveShortcutsHelpState,
setShortcutsHelpVisible,
]);
const allToolCalls = useMemo(
() =>
pendingHistoryItems
@@ -2240,7 +2299,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
>
<ToolActionsProvider config={config} toolCalls={allToolCalls}>
<ShellFocusContext.Provider value={isFocused}>
<App />
<App key={`app-${forceRerenderKey}`} />
</ShellFocusContext.Provider>
</ToolActionsProvider>
</AppContext.Provider>
@@ -12,18 +12,15 @@ import { QuittingDisplay } from './QuittingDisplay.js';
import { useAppContext } from '../contexts/AppContext.js';
import { MAX_GEMINI_MESSAGE_LINES } from '../constants.js';
import { useConfirmingTool } from '../hooks/useConfirmingTool.js';
import { useConfig } from '../contexts/ConfigContext.js';
import { ToolStatusIndicator, ToolInfo } from './messages/ToolShared.js';
import { theme } from '../semantic-colors.js';
export const AlternateBufferQuittingDisplay = () => {
const { version } = useAppContext();
const uiState = useUIState();
const config = useConfig();
const confirmingTool = useConfirmingTool();
const showPromptedTool =
config.isEventDrivenSchedulerEnabled() && confirmingTool !== null;
const showPromptedTool = confirmingTool !== null;
// We render the entire chat history and header here to ensure that the
// conversation history is visible to the user after the app quits and the
@@ -56,7 +53,6 @@ export const AlternateBufferQuittingDisplay = () => {
terminalWidth={uiState.mainAreaWidth}
item={{ ...item, id: 0 }}
isPending={true}
isFocused={false}
activeShellPtyId={uiState.activePtyId}
embeddedShellFocused={uiState.embeddedShellFocused}
/>
@@ -189,6 +189,7 @@ const createMockUIActions = (): UIActions =>
setShellModeActive: vi.fn(),
onEscapePromptChange: vi.fn(),
vimHandleInput: vi.fn(),
setShortcutsHelpVisible: vi.fn(),
}) as Partial<UIActions> as UIActions;
const createMockConfig = (overrides = {}): Config =>
@@ -337,7 +338,7 @@ describe('Composer', () => {
expect(output).toContain('LoadingIndicator: Thinking ...');
});
it('keeps shortcuts hint visible while loading', () => {
it('hides shortcuts hint while loading', () => {
const uiState = createMockUIState({
streamingState: StreamingState.Responding,
elapsedTime: 1,
@@ -347,7 +348,7 @@ describe('Composer', () => {
const output = lastFrame();
expect(output).toContain('LoadingIndicator');
expect(output).toContain('ShortcutsHint');
expect(output).not.toContain('ShortcutsHint');
});
it('renders LoadingIndicator without thought when accessibility disables loading phrases', () => {
@@ -686,4 +687,43 @@ describe('Composer', () => {
expect(lastFrame()).toContain('ShortcutsHint');
});
});
describe('Shortcuts Help', () => {
it('shows shortcuts help in passive state', () => {
const uiState = createMockUIState({
shortcutsHelpVisible: true,
streamingState: StreamingState.Idle,
});
const { lastFrame } = renderComposer(uiState);
expect(lastFrame()).toContain('ShortcutsHelp');
});
it('hides shortcuts help while streaming', () => {
const uiState = createMockUIState({
shortcutsHelpVisible: true,
streamingState: StreamingState.Responding,
});
const { lastFrame } = renderComposer(uiState);
expect(lastFrame()).not.toContain('ShortcutsHelp');
});
it('hides shortcuts help when action is required', () => {
const uiState = createMockUIState({
shortcutsHelpVisible: true,
customDialog: (
<Box>
<Text>Dialog content</Text>
</Box>
),
});
const { lastFrame } = renderComposer(uiState);
expect(lastFrame()).not.toContain('ShortcutsHelp');
});
});
});
+45 -9
View File
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { useState } from 'react';
import { useState, useEffect, useMemo } from 'react';
import { Box, useIsScreenReaderEnabled } from 'ink';
import { LoadingIndicator } from './LoadingIndicator.js';
import { StatusDisplay } from './StatusDisplay.js';
@@ -28,7 +28,11 @@ import { useVimMode } from '../contexts/VimModeContext.js';
import { useConfig } from '../contexts/ConfigContext.js';
import { useSettings } from '../contexts/SettingsContext.js';
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
import { StreamingState, ToolCallStatus } from '../types.js';
import {
StreamingState,
type HistoryItemToolGroup,
ToolCallStatus,
} from '../types.js';
import { ConfigInitDisplay } from '../components/ConfigInitDisplay.js';
import { TodoTray } from './messages/Todo.js';
import { getInlineThinkingMode } from '../utils/inlineThinkingMode.js';
@@ -51,11 +55,19 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
const suggestionsPosition = isAlternateBuffer ? 'above' : 'below';
const hideContextSummary =
suggestionsVisible && suggestionsPosition === 'above';
const hasPendingToolConfirmation = (uiState.pendingHistoryItems ?? []).some(
(item) =>
item.type === 'tool_group' &&
item.tools.some((tool) => tool.status === ToolCallStatus.Confirming),
const hasPendingToolConfirmation = useMemo(
() =>
(uiState.pendingHistoryItems ?? [])
.filter(
(item): item is HistoryItemToolGroup => item.type === 'tool_group',
)
.some((item) =>
item.tools.some((tool) => tool.status === ToolCallStatus.Confirming),
),
[uiState.pendingHistoryItems],
);
const hasPendingActionRequired =
hasPendingToolConfirmation ||
Boolean(uiState.commandConfirmationRequest) ||
@@ -65,6 +77,31 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
Boolean(uiState.quota.proQuotaRequest) ||
Boolean(uiState.quota.validationRequest) ||
Boolean(uiState.customDialog);
const isPassiveShortcutsHelpState =
uiState.isInputActive &&
uiState.streamingState === StreamingState.Idle &&
!hasPendingActionRequired;
const { setShortcutsHelpVisible } = uiActions;
useEffect(() => {
if (uiState.shortcutsHelpVisible && !isPassiveShortcutsHelpState) {
setShortcutsHelpVisible(false);
}
}, [
uiState.shortcutsHelpVisible,
isPassiveShortcutsHelpState,
setShortcutsHelpVisible,
]);
const showShortcutsHelp =
uiState.shortcutsHelpVisible &&
uiState.streamingState === StreamingState.Idle &&
!hasPendingActionRequired;
const showShortcutsHint =
settings.merged.ui.showShortcutsHint &&
uiState.streamingState === StreamingState.Idle &&
!hasPendingActionRequired;
const hasToast = shouldShowToast(uiState);
const showLoadingIndicator =
(!uiState.embeddedShellFocused || uiState.isBackgroundShellVisible) &&
@@ -133,11 +170,10 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
flexDirection="column"
alignItems={isNarrow ? 'flex-start' : 'flex-end'}
>
{settings.merged.ui.showShortcutsHint &&
!hasPendingActionRequired && <ShortcutsHint />}
{showShortcutsHint && <ShortcutsHint />}
</Box>
</Box>
{uiState.shortcutsHelpVisible && <ShortcutsHelp />}
{showShortcutsHelp && <ShortcutsHelp />}
<HorizontalLine />
<Box
justifyContent={
@@ -43,7 +43,6 @@ interface HistoryItemDisplayProps {
availableTerminalHeight?: number;
terminalWidth: number;
isPending: boolean;
isFocused?: boolean;
commands?: readonly SlashCommand[];
activeShellPtyId?: number | null;
embeddedShellFocused?: boolean;
@@ -56,7 +55,6 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
terminalWidth,
isPending,
commands,
isFocused = true,
activeShellPtyId,
embeddedShellFocused,
availableTerminalHeightGemini,
@@ -179,7 +177,6 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
groupId={itemForDisplay.id}
availableTerminalHeight={availableTerminalHeight}
terminalWidth={terminalWidth}
isFocused={isFocused}
activeShellPtyId={activeShellPtyId}
embeddedShellFocused={embeddedShellFocused}
borderTop={itemForDisplay.borderTop}
@@ -4342,6 +4342,18 @@ describe('InputPrompt', () => {
vi.mocked(clipboardy.read).mockResolvedValue('clipboard text');
},
},
{
name: 'Ctrl+R hotkey is pressed',
input: '\x12',
},
{
name: 'Ctrl+X hotkey is pressed',
input: '\x18',
},
{
name: 'F12 hotkey is pressed',
input: '\x1b[24~',
},
])(
'should close shortcuts help when a $name',
async ({ input, setupMocks, mouseEventsEnabled }) => {
@@ -75,6 +75,7 @@ import { useMouseClick } from '../hooks/useMouseClick.js';
import { useMouse, type MouseEvent } from '../contexts/MouseContext.js';
import { useUIActions } from '../contexts/UIActionsContext.js';
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
import { shouldDismissShortcutsHelpOnHotkey } from '../utils/shortcutsHelp.js';
/**
* Returns if the terminal can be trusted to handle paste events atomically
@@ -661,6 +662,10 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
return true;
}
if (shortcutsHelpVisible && shouldDismissShortcutsHelpOnHotkey(key)) {
setShortcutsHelpVisible(false);
}
if (shortcutsHelpVisible) {
if (key.sequence === '?' && key.insertable) {
setShortcutsHelpVisible(false);
@@ -19,7 +19,6 @@ import { useMemo, memo, useCallback, useEffect, useRef } from 'react';
import { MAX_GEMINI_MESSAGE_LINES } from '../constants.js';
import { useConfirmingTool } from '../hooks/useConfirmingTool.js';
import { ToolConfirmationQueue } from './ToolConfirmationQueue.js';
import { useConfig } from '../contexts/ConfigContext.js';
const MemoizedHistoryItemDisplay = memo(HistoryItemDisplay);
const MemoizedAppHeader = memo(AppHeader);
@@ -31,12 +30,10 @@ const MemoizedAppHeader = memo(AppHeader);
export const MainContent = () => {
const { version } = useAppContext();
const uiState = useUIState();
const config = useConfig();
const isAlternateBuffer = useAlternateBuffer();
const confirmingTool = useConfirmingTool();
const showConfirmationQueue =
config.isEventDrivenSchedulerEnabled() && confirmingTool !== null;
const showConfirmationQueue = confirmingTool !== null;
const scrollableListRef = useRef<VirtualizedListRef<unknown>>(null);
@@ -89,7 +86,6 @@ export const MainContent = () => {
terminalWidth={mainAreaWidth}
item={{ ...item, id: 0 }}
isPending={true}
isFocused={!uiState.isEditorDialogOpen}
activeShellPtyId={uiState.activePtyId}
embeddedShellFocused={uiState.embeddedShellFocused}
/>
@@ -105,7 +101,6 @@ export const MainContent = () => {
isAlternateBuffer,
availableTerminalHeight,
mainAreaWidth,
uiState.isEditorDialogOpen,
uiState.activePtyId,
uiState.embeddedShellFocused,
showConfirmationQueue,
@@ -77,6 +77,39 @@ exports[`InputPrompt > mouse interaction > should toggle paste expansion on doub
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄"
`;
exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 4`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
> [Pasted Text: 10 lines]
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄"
`;
exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 5`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
 > [Pasted Text: 10 lines] 
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄"
`;
exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 6`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
 > line1 
 line2 
 line3 
 line4 
 line5 
 line6 
 line7 
 line8 
 line9 
 line10 
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄"
`;
exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 7`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
 > [Pasted Text: 10 lines] 
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄"
`;
exports[`InputPrompt > snapshots > should not show inverted cursor when shell is focused 1`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
> Type your message or @path/to/file
@@ -1,128 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi } from 'vitest';
import { ToolGroupMessage } from './ToolGroupMessage.js';
import type {
ToolCallConfirmationDetails,
Config,
} from '@google/gemini-cli-core';
import { renderWithProviders } from '../../../test-utils/render.js';
import { useToolActions } from '../../contexts/ToolActionsContext.js';
import {
StreamingState,
ToolCallStatus,
type IndividualToolCallDisplay,
} from '../../types.js';
import { OverflowProvider } from '../../contexts/OverflowContext.js';
import { waitFor } from '../../../test-utils/async.js';
vi.mock('../../contexts/ToolActionsContext.js', async (importOriginal) => {
const actual =
await importOriginal<
typeof import('../../contexts/ToolActionsContext.js')
>();
return {
...actual,
useToolActions: vi.fn(),
};
});
describe('ToolConfirmationMessage Overflow', () => {
const mockConfirm = vi.fn();
vi.mocked(useToolActions).mockReturnValue({
confirm: mockConfirm,
cancel: vi.fn(),
isDiffingEnabled: false,
});
const mockConfig = {
isTrustedFolder: () => true,
getIdeMode: () => false,
getMessageBus: () => ({
subscribe: vi.fn(),
unsubscribe: vi.fn(),
publish: vi.fn(),
}),
isEventDrivenSchedulerEnabled: () => false,
getTheme: () => ({
status: { warning: 'yellow' },
text: { primary: 'white', secondary: 'gray', link: 'blue' },
border: { default: 'gray' },
ui: { symbol: 'cyan' },
}),
} as unknown as Config;
it('should display "press ctrl-o" hint when content overflows in ToolGroupMessage', async () => {
// Large diff that will definitely overflow
const diffLines = ['--- a/test.txt', '+++ b/test.txt', '@@ -1,20 +1,20 @@'];
for (let i = 0; i < 50; i++) {
diffLines.push(`+ line ${i + 1}`);
}
const fileDiff = diffLines.join('\n');
const confirmationDetails: ToolCallConfirmationDetails = {
type: 'edit',
title: 'Confirm Edit',
fileName: 'test.txt',
filePath: '/test.txt',
fileDiff,
originalContent: '',
newContent: 'lots of lines',
onConfirm: vi.fn(),
};
const toolCalls: IndividualToolCallDisplay[] = [
{
callId: 'test-call-id',
name: 'test-tool',
description: 'a test tool',
status: ToolCallStatus.Confirming,
confirmationDetails,
resultDisplay: undefined,
},
];
const { lastFrame } = renderWithProviders(
<OverflowProvider>
<ToolGroupMessage
groupId={1}
toolCalls={toolCalls}
availableTerminalHeight={15} // Small height to force overflow
terminalWidth={80}
/>
</OverflowProvider>,
{
config: mockConfig,
uiState: {
streamingState: StreamingState.WaitingForConfirmation,
constrainHeight: true,
},
},
);
// ResizeObserver might take a tick
await waitFor(() =>
expect(lastFrame()).toContain('Press ctrl-o to show more lines'),
);
const frame = lastFrame();
expect(frame).toBeDefined();
if (frame) {
expect(frame).toContain('Press ctrl-o to show more lines');
// Ensure it's AFTER the bottom border
const linesOfOutput = frame.split('\n');
const bottomBorderIndex = linesOfOutput.findLastIndex((l) =>
l.includes('╰─'),
);
const hintIndex = linesOfOutput.findIndex((l) =>
l.includes('Press ctrl-o to show more lines'),
);
expect(hintIndex).toBeGreaterThan(bottomBorderIndex);
expect(frame).toMatchSnapshot();
}
});
});
@@ -5,7 +5,6 @@
*/
import { renderWithProviders } from '../../../test-utils/render.js';
import { createMockSettings } from '../../../test-utils/settings.js';
import { describe, it, expect, vi, afterEach } from 'vitest';
import { ToolGroupMessage } from './ToolGroupMessage.js';
import type { IndividualToolCallDisplay } from '../../types.js';
@@ -35,7 +34,6 @@ describe('<ToolGroupMessage />', () => {
const baseProps = {
groupId: 1,
terminalWidth: 80,
isFocused: true,
};
const baseMockConfig = makeFakeConfig({
@@ -45,7 +43,6 @@ describe('<ToolGroupMessage />', () => {
folderTrust: false,
ideMode: false,
enableInteractiveShell: true,
enableEventDrivenScheduler: true,
});
describe('Golden Snapshots', () => {
@@ -64,7 +61,31 @@ describe('<ToolGroupMessage />', () => {
unmount();
});
it('renders multiple tool calls with different statuses', () => {
it('hides confirming tools (standard behavior)', () => {
const toolCalls = [
createToolCall({
callId: 'confirm-tool',
status: ToolCallStatus.Confirming,
confirmationDetails: {
type: 'info',
title: 'Confirm tool',
prompt: 'Do you want to proceed?',
onConfirm: vi.fn(),
},
}),
];
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
{ config: baseMockConfig },
);
// Should render nothing because all tools in the group are confirming
expect(lastFrame()).toBe('');
unmount();
});
it('renders multiple tool calls with different statuses (only visible ones)', () => {
const toolCalls = [
createToolCall({
callId: 'tool-1',
@@ -85,68 +106,7 @@ describe('<ToolGroupMessage />', () => {
status: ToolCallStatus.Error,
}),
];
const mockConfig = makeFakeConfig({
model: 'gemini-pro',
targetDir: os.tmpdir(),
enableEventDrivenScheduler: false,
});
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
{
config: mockConfig,
uiState: {
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
},
},
);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders tool call awaiting confirmation', () => {
const toolCalls = [
createToolCall({
callId: 'tool-confirm',
name: 'confirmation-tool',
description: 'This tool needs confirmation',
status: ToolCallStatus.Confirming,
confirmationDetails: {
type: 'info',
title: 'Confirm Tool Execution',
prompt: 'Are you sure you want to proceed?',
onConfirm: vi.fn(),
},
}),
];
const mockConfig = makeFakeConfig({
model: 'gemini-pro',
targetDir: os.tmpdir(),
enableEventDrivenScheduler: false,
});
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
{
config: mockConfig,
uiState: {
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
},
},
);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders shell command with yellow border', () => {
const toolCalls = [
createToolCall({
callId: 'shell-1',
name: 'run_shell_command',
description: 'Execute shell command',
status: ToolCallStatus.Success,
}),
];
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
{
@@ -156,7 +116,12 @@ describe('<ToolGroupMessage />', () => {
},
},
);
expect(lastFrame()).toMatchSnapshot();
// pending-tool should be hidden
const output = lastFrame();
expect(output).toContain('successful-tool');
expect(output).not.toContain('pending-tool');
expect(output).toContain('error-tool');
expect(output).toMatchSnapshot();
unmount();
});
@@ -181,22 +146,22 @@ describe('<ToolGroupMessage />', () => {
status: ToolCallStatus.Pending,
}),
];
const mockConfig = makeFakeConfig({
model: 'gemini-pro',
targetDir: os.tmpdir(),
enableEventDrivenScheduler: false,
});
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
{
config: mockConfig,
config: baseMockConfig,
uiState: {
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
},
},
);
expect(lastFrame()).toMatchSnapshot();
// write_file (Pending) should be hidden
const output = lastFrame();
expect(output).toContain('read_file');
expect(output).toContain('run_shell_command');
expect(output).not.toContain('write_file');
expect(output).toMatchSnapshot();
unmount();
});
@@ -233,25 +198,6 @@ describe('<ToolGroupMessage />', () => {
unmount();
});
it('renders when not focused', () => {
const toolCalls = [createToolCall()];
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage
{...baseProps}
toolCalls={toolCalls}
isFocused={false}
/>,
{
config: baseMockConfig,
uiState: {
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
},
},
);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders with narrow terminal width', () => {
const toolCalls = [
createToolCall({
@@ -384,28 +330,6 @@ describe('<ToolGroupMessage />', () => {
});
describe('Border Color Logic', () => {
it('uses yellow border when tools are pending', () => {
const toolCalls = [createToolCall({ status: ToolCallStatus.Pending })];
const mockConfig = makeFakeConfig({
model: 'gemini-pro',
targetDir: os.tmpdir(),
enableEventDrivenScheduler: false,
});
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
{
config: mockConfig,
uiState: {
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
},
},
);
// The snapshot will capture the visual appearance including border color
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('uses yellow border for shell commands even when successful', () => {
const toolCalls = [
createToolCall({
@@ -483,210 +407,6 @@ describe('<ToolGroupMessage />', () => {
});
});
describe('Confirmation Handling', () => {
it('shows confirmation dialog for first confirming tool only', () => {
const toolCalls = [
createToolCall({
callId: 'tool-1',
name: 'first-confirm',
status: ToolCallStatus.Confirming,
confirmationDetails: {
type: 'info',
title: 'Confirm First Tool',
prompt: 'Confirm first tool',
onConfirm: vi.fn(),
},
}),
createToolCall({
callId: 'tool-2',
name: 'second-confirm',
status: ToolCallStatus.Confirming,
confirmationDetails: {
type: 'info',
title: 'Confirm Second Tool',
prompt: 'Confirm second tool',
onConfirm: vi.fn(),
},
}),
];
const mockConfig = makeFakeConfig({
model: 'gemini-pro',
targetDir: os.tmpdir(),
enableEventDrivenScheduler: false,
});
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
{
config: mockConfig,
uiState: {
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
},
},
);
// Should only show confirmation for the first tool
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders confirmation with permanent approval enabled', () => {
const toolCalls = [
createToolCall({
callId: 'tool-1',
name: 'confirm-tool',
status: ToolCallStatus.Confirming,
confirmationDetails: {
type: 'info',
title: 'Confirm Tool',
prompt: 'Do you want to proceed?',
onConfirm: vi.fn(),
},
}),
];
const settings = createMockSettings({
security: { enablePermanentToolApproval: true },
});
const mockConfig = makeFakeConfig({
model: 'gemini-pro',
targetDir: os.tmpdir(),
enableEventDrivenScheduler: false,
});
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
{
settings,
config: mockConfig,
uiState: {
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
},
},
);
expect(lastFrame()).toContain('Allow for all future sessions');
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders confirmation with permanent approval disabled', () => {
const toolCalls = [
createToolCall({
callId: 'confirm-tool',
name: 'confirm-tool',
status: ToolCallStatus.Confirming,
confirmationDetails: {
type: 'info',
title: 'Confirm tool',
prompt: 'Do you want to proceed?',
onConfirm: vi.fn(),
},
}),
];
const mockConfig = makeFakeConfig({
model: 'gemini-pro',
targetDir: os.tmpdir(),
enableEventDrivenScheduler: false,
});
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
{ config: mockConfig },
);
expect(lastFrame()).not.toContain('Allow for all future sessions');
expect(lastFrame()).toMatchSnapshot();
unmount();
});
});
describe('Event-Driven Scheduler', () => {
it('hides confirming tools when event-driven scheduler is enabled', () => {
const toolCalls = [
createToolCall({
callId: 'confirm-tool',
status: ToolCallStatus.Confirming,
confirmationDetails: {
type: 'info',
title: 'Confirm tool',
prompt: 'Do you want to proceed?',
onConfirm: vi.fn(),
},
}),
];
const mockConfig = baseMockConfig;
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
{ config: mockConfig },
);
// Should render nothing because all tools in the group are confirming
expect(lastFrame()).toBe('');
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('shows only successful tools when mixed with confirming tools', () => {
const toolCalls = [
createToolCall({
callId: 'success-tool',
name: 'success-tool',
status: ToolCallStatus.Success,
}),
createToolCall({
callId: 'confirm-tool',
name: 'confirm-tool',
status: ToolCallStatus.Confirming,
confirmationDetails: {
type: 'info',
title: 'Confirm tool',
prompt: 'Do you want to proceed?',
onConfirm: vi.fn(),
},
}),
];
const mockConfig = baseMockConfig;
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
{ config: mockConfig },
);
const output = lastFrame();
expect(output).toContain('success-tool');
expect(output).not.toContain('confirm-tool');
expect(output).not.toContain('Do you want to proceed?');
expect(output).toMatchSnapshot();
unmount();
});
it('renders nothing when only tool is in-progress AskUser with borderBottom=false', () => {
// AskUser tools in progress are rendered by AskUserDialog, not ToolGroupMessage.
// When AskUser is the only tool and borderBottom=false (no border to close),
// the component should render nothing.
const toolCalls = [
createToolCall({
callId: 'ask-user-tool',
name: 'Ask User',
status: ToolCallStatus.Executing,
}),
];
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage
{...baseProps}
toolCalls={toolCalls}
borderBottom={false}
/>,
{ config: baseMockConfig },
);
// AskUser tools in progress are rendered by AskUserDialog, so we expect nothing.
expect(lastFrame()).toMatchSnapshot();
unmount();
});
});
describe('Ask User Filtering', () => {
it.each([
ToolCallStatus.Pending,
@@ -753,5 +473,30 @@ describe('<ToolGroupMessage />', () => {
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('renders nothing when only tool is in-progress AskUser with borderBottom=false', () => {
// AskUser tools in progress are rendered by AskUserDialog, not ToolGroupMessage.
// When AskUser is the only tool and borderBottom=false (no border to close),
// the component should render nothing.
const toolCalls = [
createToolCall({
callId: 'ask-user-tool',
name: ASK_USER_DISPLAY_NAME,
status: ToolCallStatus.Executing,
}),
];
const { lastFrame, unmount } = renderWithProviders(
<ToolGroupMessage
{...baseProps}
toolCalls={toolCalls}
borderBottom={false}
/>,
{ config: baseMockConfig },
);
// AskUser tools in progress are rendered by AskUserDialog, so we expect nothing.
expect(lastFrame()).toBe('');
unmount();
});
});
});
@@ -11,7 +11,6 @@ import type { IndividualToolCallDisplay } from '../../types.js';
import { ToolCallStatus } from '../../types.js';
import { ToolMessage } from './ToolMessage.js';
import { ShellToolMessage } from './ShellToolMessage.js';
import { ToolConfirmationMessage } from './ToolConfirmationMessage.js';
import { theme } from '../../semantic-colors.js';
import { useConfig } from '../../contexts/ConfigContext.js';
import { isShellTool, isThisShellFocused } from './ToolShared.js';
@@ -24,7 +23,6 @@ interface ToolGroupMessageProps {
toolCalls: IndividualToolCallDisplay[];
availableTerminalHeight?: number;
terminalWidth: number;
isFocused?: boolean;
activeShellPtyId?: number | null;
embeddedShellFocused?: boolean;
onShellInputSubmit?: (input: string) => void;
@@ -43,13 +41,11 @@ const isAskUserInProgress = (t: IndividualToolCallDisplay): boolean =>
// Main component renders the border and maps the tools using ToolMessage
const TOOL_MESSAGE_HORIZONTAL_MARGIN = 4;
const TOOL_CONFIRMATION_INTERNAL_PADDING = 4;
export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
toolCalls: allToolCalls,
availableTerminalHeight,
terminalWidth,
isFocused = true,
activeShellPtyId,
embeddedShellFocused,
borderTop: borderTopOverride,
@@ -64,24 +60,20 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
const config = useConfig();
const { constrainHeight } = useUIState();
const isEventDriven = config.isEventDrivenSchedulerEnabled();
// If Event-Driven Scheduler is enabled, we HIDE tools that are still in
// pre-execution states (Confirming, Pending) from the History log.
// They live in the Global Queue or wait for their turn.
const visibleToolCalls = useMemo(() => {
if (!isEventDriven) {
return toolCalls;
}
// Only show tools that are actually running or finished.
// We explicitly exclude Pending and Confirming to ensure they only
// appear in the Global Queue until they are approved and start executing.
return toolCalls.filter(
(t) =>
t.status !== ToolCallStatus.Pending &&
t.status !== ToolCallStatus.Confirming,
);
}, [toolCalls, isEventDriven]);
// We HIDE tools that are still in pre-execution states (Confirming, Pending)
// from the History log. They live in the Global Queue or wait for their turn.
// Only show tools that are actually running or finished.
// We explicitly exclude Pending and Confirming to ensure they only
// appear in the Global Queue until they are approved and start executing.
const visibleToolCalls = useMemo(
() =>
toolCalls.filter(
(t) =>
t.status !== ToolCallStatus.Pending &&
t.status !== ToolCallStatus.Confirming,
),
[toolCalls],
);
const isEmbeddedShellFocused = visibleToolCalls.some((t) =>
isThisShellFocused(
@@ -110,17 +102,8 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
const staticHeight = /* border */ 2 + /* marginBottom */ 1;
// Inline confirmations are ONLY used when the Global Queue is disabled.
const toolAwaitingApproval = useMemo(
() =>
isEventDriven
? undefined
: toolCalls.find((tc) => tc.status === ToolCallStatus.Confirming),
[toolCalls, isEventDriven],
);
// If all tools are filtered out (e.g., in-progress AskUser tools, confirming tools
// in event-driven mode), only render if we need to close a border from previous
// If all tools are filtered out (e.g., in-progress AskUser tools, confirming tools),
// only render if we need to close a border from previous
// tool groups. borderBottomOverride=true means we must render the closing border;
// undefined or false means there's nothing to display.
if (visibleToolCalls.length === 0 && borderBottomOverride !== true) {
@@ -163,7 +146,6 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
paddingRight={TOOL_MESSAGE_HORIZONTAL_MARGIN}
>
{visibleToolCalls.map((tool, index) => {
const isConfirming = toolAwaitingApproval?.callId === tool.callId;
const isFirst = index === 0;
const isShellToolCall = isShellTool(tool.name);
@@ -171,11 +153,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
...tool,
availableTerminalHeight: availableTerminalHeightPerToolMessage,
terminalWidth: contentWidth,
emphasis: isConfirming
? ('high' as const)
: toolAwaitingApproval
? ('low' as const)
: ('medium' as const),
emphasis: 'medium' as const,
isFirst:
borderTopOverride !== undefined
? borderTopOverride && isFirst
@@ -213,22 +191,6 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
paddingLeft={1}
paddingRight={1}
>
{tool.status === ToolCallStatus.Confirming &&
isConfirming &&
tool.confirmationDetails && (
<ToolConfirmationMessage
callId={tool.callId}
confirmationDetails={tool.confirmationDetails}
config={config}
isFocused={isFocused}
availableTerminalHeight={
availableTerminalHeightPerToolMessage
}
terminalWidth={
contentWidth - TOOL_CONFIRMATION_INTERNAL_PADDING
}
/>
)}
{tool.outputFile && (
<Box>
<Text color={theme.text.primary}>
@@ -50,76 +50,6 @@ exports[`<ToolGroupMessage /> > Border Color Logic > uses yellow border for shel
╰──────────────────────────────────────────────────────────────────────────╯"
`;
exports[`<ToolGroupMessage /> > Border Color Logic > uses yellow border when tools are pending 1`] = `
"╭──────────────────────────────────────────────────────────────────────────╮
│ o test-tool A tool for testing │
│ │
│ Test result │
╰──────────────────────────────────────────────────────────────────────────╯"
`;
exports[`<ToolGroupMessage /> > Confirmation Handling > renders confirmation with permanent approval disabled 1`] = `
"╭──────────────────────────────────────────────────────────────────────────╮
│ ? confirm-tool A tool for testing ← │
│ │
│ Test result │
│ Do you want to proceed? │
│ Do you want to proceed? │
│ │
│ ● 1. Allow once │
│ 2. Allow for this session │
│ 3. No, suggest changes (esc) │
│ │
╰──────────────────────────────────────────────────────────────────────────╯"
`;
exports[`<ToolGroupMessage /> > Confirmation Handling > renders confirmation with permanent approval enabled 1`] = `
"╭──────────────────────────────────────────────────────────────────────────╮
│ ? confirm-tool A tool for testing ← │
│ │
│ Test result │
│ Do you want to proceed? │
│ Do you want to proceed? │
│ │
│ ● 1. Allow once │
│ 2. Allow for this session │
│ 3. Allow for all future sessions │
│ 4. No, suggest changes (esc) │
│ │
╰──────────────────────────────────────────────────────────────────────────╯"
`;
exports[`<ToolGroupMessage /> > Confirmation Handling > shows confirmation dialog for first confirming tool only 1`] = `
"╭──────────────────────────────────────────────────────────────────────────╮
│ ? first-confirm A tool for testing ← │
│ │
│ Test result │
│ Confirm first tool │
│ Do you want to proceed? │
│ │
│ ● 1. Allow once │
│ 2. Allow for this session │
│ 3. No, suggest changes (esc) │
│ │
│ │
│ ? second-confirm A tool for testing │
│ │
│ Test result │
╰──────────────────────────────────────────────────────────────────────────╯"
`;
exports[`<ToolGroupMessage /> > Event-Driven Scheduler > hides confirming tools when event-driven scheduler is enabled 1`] = `""`;
exports[`<ToolGroupMessage /> > Event-Driven Scheduler > renders nothing when only tool is in-progress AskUser with borderBottom=false 1`] = `""`;
exports[`<ToolGroupMessage /> > Event-Driven Scheduler > shows only successful tools when mixed with confirming tools 1`] = `
"╭──────────────────────────────────────────────────────────────────────────╮
│ ✓ success-tool A tool for testing │
│ │
│ Test result │
╰──────────────────────────────────────────────────────────────────────────╯"
`;
exports[`<ToolGroupMessage /> > Golden Snapshots > renders empty tool calls array 1`] = `""`;
exports[`<ToolGroupMessage /> > Golden Snapshots > renders header when scrolled 1`] = `
@@ -144,37 +74,21 @@ exports[`<ToolGroupMessage /> > Golden Snapshots > renders mixed tool calls incl
│ ⊷ run_shell_command Run command │
│ │
│ Test result │
│ │
│ o write_file Write to file │
│ │
│ Test result │
╰──────────────────────────────────────────────────────────────────────────╯"
`;
exports[`<ToolGroupMessage /> > Golden Snapshots > renders multiple tool calls with different statuses 1`] = `
exports[`<ToolGroupMessage /> > Golden Snapshots > renders multiple tool calls with different statuses (only visible ones) 1`] = `
"╭──────────────────────────────────────────────────────────────────────────╮
│ ✓ successful-tool This tool succeeded │
│ │
│ Test result │
│ │
│ o pending-tool This tool is pending │
│ │
│ Test result │
│ │
│ x error-tool This tool failed │
│ │
│ Test result │
╰──────────────────────────────────────────────────────────────────────────╯"
`;
exports[`<ToolGroupMessage /> > Golden Snapshots > renders shell command with yellow border 1`] = `
"╭──────────────────────────────────────────────────────────────────────────╮
│ ✓ run_shell_command Execute shell command │
│ │
│ Test result │
╰──────────────────────────────────────────────────────────────────────────╯"
`;
exports[`<ToolGroupMessage /> > Golden Snapshots > renders single successful tool call 1`] = `
"╭──────────────────────────────────────────────────────────────────────────╮
│ ✓ test-tool A tool for testing │
@@ -183,21 +97,6 @@ exports[`<ToolGroupMessage /> > Golden Snapshots > renders single successful too
╰──────────────────────────────────────────────────────────────────────────╯"
`;
exports[`<ToolGroupMessage /> > Golden Snapshots > renders tool call awaiting confirmation 1`] = `
"╭──────────────────────────────────────────────────────────────────────────╮
│ ? confirmation-tool This tool needs confirmation ← │
│ │
│ Test result │
│ Are you sure you want to proceed? │
│ Do you want to proceed? │
│ │
│ ● 1. Allow once │
│ 2. Allow for this session │
│ 3. No, suggest changes (esc) │
│ │
╰──────────────────────────────────────────────────────────────────────────╯"
`;
exports[`<ToolGroupMessage /> > Golden Snapshots > renders tool call with outputFile 1`] = `
"╭──────────────────────────────────────────────────────────────────────────╮
│ ✓ tool-with-file Tool that saved output to file │
@@ -216,14 +115,6 @@ exports[`<ToolGroupMessage /> > Golden Snapshots > renders two tool groups where
╰──────────────────────────────────────────────────────────────────────────╯ █"
`;
exports[`<ToolGroupMessage /> > Golden Snapshots > renders when not focused 1`] = `
"╭──────────────────────────────────────────────────────────────────────────╮
│ ✓ test-tool A tool for testing │
│ │
│ Test result │
╰──────────────────────────────────────────────────────────────────────────╯"
`;
exports[`<ToolGroupMessage /> > Golden Snapshots > renders with limited terminal height 1`] = `
"╭──────────────────────────────────────────────────────────────────────────╮
│ ✓ tool-with-result Tool with output │
@@ -1,97 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`useReactToolScheduler > should handle live output updates 1`] = `
{
"callId": "liveCall",
"contentLength": 12,
"data": undefined,
"error": undefined,
"errorType": undefined,
"outputFile": undefined,
"responseParts": [
{
"functionResponse": {
"id": "liveCall",
"name": "mockToolWithLiveOutput",
"response": {
"output": "Final output",
},
},
},
],
"resultDisplay": "Final display",
}
`;
exports[`useReactToolScheduler > should handle tool requiring confirmation - approved 1`] = `
{
"callId": "callConfirm",
"contentLength": 16,
"data": undefined,
"error": undefined,
"errorType": undefined,
"outputFile": undefined,
"responseParts": [
{
"functionResponse": {
"id": "callConfirm",
"name": "mockToolRequiresConfirmation",
"response": {
"output": "Confirmed output",
},
},
},
],
"resultDisplay": "Confirmed display",
}
`;
exports[`useReactToolScheduler > should handle tool requiring confirmation - cancelled by user 1`] = `
{
"callId": "callConfirmCancel",
"contentLength": 59,
"error": undefined,
"errorType": undefined,
"responseParts": [
{
"functionResponse": {
"id": "callConfirmCancel",
"name": "mockToolRequiresConfirmation",
"response": {
"error": "[Operation Cancelled] Reason: User cancelled the operation.",
},
},
},
],
"resultDisplay": {
"fileDiff": "Mock tool requires confirmation",
"fileName": "mockToolRequiresConfirmation.ts",
"filePath": undefined,
"newContent": undefined,
"originalContent": undefined,
},
}
`;
exports[`useReactToolScheduler > should schedule and execute a tool call successfully 1`] = `
{
"callId": "call1",
"contentLength": 11,
"data": undefined,
"error": undefined,
"errorType": undefined,
"outputFile": undefined,
"responseParts": [
{
"functionResponse": {
"id": "call1",
"name": "mockTool",
"response": {
"output": "Tool output",
},
},
},
],
"resultDisplay": "Formatted tool output",
}
`;
@@ -18,10 +18,13 @@ import { FileCommandLoader } from '../../services/FileCommandLoader.js';
import { McpPromptLoader } from '../../services/McpPromptLoader.js';
import {
type GeminiClient,
type UserFeedbackPayload,
SlashCommandStatus,
makeFakeConfig,
coreEvents,
CoreEvent,
} from '@google/gemini-cli-core';
import { SlashCommandConflictHandler } from '../../services/SlashCommandConflictHandler.js';
const {
logSlashCommand,
@@ -182,6 +185,26 @@ describe('useSlashCommandProcessor', () => {
mockFileLoadCommands.mockResolvedValue(Object.freeze(fileCommands));
mockMcpLoadCommands.mockResolvedValue(Object.freeze(mcpCommands));
const conflictHandler = new SlashCommandConflictHandler();
conflictHandler.start();
const handleFeedback = (payload: UserFeedbackPayload) => {
let type = MessageType.INFO;
if (payload.severity === 'error') {
type = MessageType.ERROR;
} else if (payload.severity === 'warning') {
type = MessageType.WARNING;
}
mockAddItem(
{
type,
text: payload.message,
},
Date.now(),
);
};
coreEvents.on(CoreEvent.UserFeedback, handleFeedback);
let result!: { current: ReturnType<typeof useSlashCommandProcessor> };
let unmount!: () => void;
let rerender!: (props?: unknown) => void;
@@ -228,7 +251,11 @@ describe('useSlashCommandProcessor', () => {
rerender = hook.rerender;
});
unmountHook = async () => unmount();
unmountHook = async () => {
conflictHandler.stop();
coreEvents.off(CoreEvent.UserFeedback, handleFeedback);
unmount();
};
await waitFor(() => {
expect(result.current.slashCommands).toBeDefined();
@@ -1052,4 +1079,119 @@ describe('useSlashCommandProcessor', () => {
expect(result.current.slashCommands).toEqual([newCommand]),
);
});
describe('Conflict Notifications', () => {
it('should display a warning when a command conflict occurs', async () => {
const builtinCommand = createTestCommand({ name: 'deploy' });
const extensionCommand = createTestCommand(
{
name: 'deploy',
extensionName: 'firebase',
},
CommandKind.FILE,
);
const result = await setupProcessorHook({
builtinCommands: [builtinCommand],
fileCommands: [extensionCommand],
});
await waitFor(() => expect(result.current.slashCommands).toHaveLength(2));
expect(mockAddItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.INFO,
text: expect.stringContaining('Command conflicts detected'),
}),
expect.any(Number),
);
expect(mockAddItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.INFO,
text: expect.stringContaining(
"- Command '/deploy' from extension 'firebase' was renamed",
),
}),
expect.any(Number),
);
});
it('should deduplicate conflict warnings across re-renders', async () => {
const builtinCommand = createTestCommand({ name: 'deploy' });
const extensionCommand = createTestCommand(
{
name: 'deploy',
extensionName: 'firebase',
},
CommandKind.FILE,
);
const result = await setupProcessorHook({
builtinCommands: [builtinCommand],
fileCommands: [extensionCommand],
});
await waitFor(() => expect(result.current.slashCommands).toHaveLength(2));
// First notification
expect(mockAddItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.INFO,
text: expect.stringContaining('Command conflicts detected'),
}),
expect.any(Number),
);
mockAddItem.mockClear();
// Trigger a reload or re-render
await act(async () => {
result.current.commandContext.ui.reloadCommands();
});
// Wait a bit for effect to run
await new Promise((resolve) => setTimeout(resolve, 100));
// Should NOT have notified again
expect(mockAddItem).not.toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.INFO,
text: expect.stringContaining('Command conflicts detected'),
}),
expect.any(Number),
);
});
it('should correctly identify the winner extension in the message', async () => {
const ext1Command = createTestCommand(
{
name: 'deploy',
extensionName: 'firebase',
},
CommandKind.FILE,
);
const ext2Command = createTestCommand(
{
name: 'deploy',
extensionName: 'aws',
},
CommandKind.FILE,
);
const result = await setupProcessorHook({
fileCommands: [ext1Command, ext2Command],
});
await waitFor(() => expect(result.current.slashCommands).toHaveLength(2));
expect(mockAddItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.INFO,
text: expect.stringContaining("conflicts with extension 'firebase'"),
}),
expect.any(Number),
);
});
});
});
@@ -329,6 +329,11 @@ export const useSlashCommandProcessor = (
],
controller.signal,
);
if (controller.signal.aborted) {
return;
}
setCommands(commandService.getCommands());
})();
@@ -246,7 +246,6 @@ describe('useGeminiStream', () => {
getContentGenerator: vi.fn(),
isInteractive: () => false,
getExperiments: () => {},
isEventDrivenSchedulerEnabled: vi.fn(() => false),
getMaxSessionTurns: vi.fn(() => 100),
isJitContextEnabled: vi.fn(() => false),
getGlobalMemory: vi.fn(() => ''),
@@ -1,77 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { CoreToolScheduler } from '@google/gemini-cli-core';
import type { Config } from '@google/gemini-cli-core';
import { renderHook } from '../../test-utils/render.js';
import { vi, describe, it, expect, beforeEach } from 'vitest';
import { useReactToolScheduler } from './useReactToolScheduler.js';
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
CoreToolScheduler: vi.fn(),
};
});
const mockCoreToolScheduler = vi.mocked(CoreToolScheduler);
describe('useReactToolScheduler', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('only creates one instance of CoreToolScheduler even if props change', () => {
const onComplete = vi.fn();
const getPreferredEditor = vi.fn();
const config = {} as Config;
const { rerender } = renderHook(
(props) =>
useReactToolScheduler(
props.onComplete,
props.config,
props.getPreferredEditor,
),
{
initialProps: {
onComplete,
config,
getPreferredEditor,
},
},
);
expect(mockCoreToolScheduler).toHaveBeenCalledTimes(1);
// Rerender with a new onComplete function
const newOnComplete = vi.fn();
rerender({
onComplete: newOnComplete,
config,
getPreferredEditor,
});
expect(mockCoreToolScheduler).toHaveBeenCalledTimes(1);
// Rerender with a new getPreferredEditor function
const newGetPreferredEditor = vi.fn();
rerender({
onComplete: newOnComplete,
config,
getPreferredEditor: newGetPreferredEditor,
});
expect(mockCoreToolScheduler).toHaveBeenCalledTimes(1);
rerender({
onComplete: newOnComplete,
config,
getPreferredEditor: newGetPreferredEditor,
});
expect(mockCoreToolScheduler).toHaveBeenCalledTimes(1);
});
});
@@ -1,221 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type {
Config,
ToolCallRequestInfo,
OutputUpdateHandler,
AllToolCallsCompleteHandler,
ToolCallsUpdateHandler,
ToolCall,
EditorType,
CompletedToolCall,
ExecutingToolCall,
ScheduledToolCall,
ValidatingToolCall,
WaitingToolCall,
CancelledToolCall,
} from '@google/gemini-cli-core';
import { CoreToolScheduler } from '@google/gemini-cli-core';
import { useCallback, useState, useMemo, useEffect, useRef } from 'react';
export type ScheduleFn = (
request: ToolCallRequestInfo | ToolCallRequestInfo[],
signal: AbortSignal,
) => Promise<void>;
export type MarkToolsAsSubmittedFn = (callIds: string[]) => void;
export type CancelAllFn = (signal: AbortSignal) => void;
export type TrackedScheduledToolCall = ScheduledToolCall & {
responseSubmittedToGemini?: boolean;
};
export type TrackedValidatingToolCall = ValidatingToolCall & {
responseSubmittedToGemini?: boolean;
};
export type TrackedWaitingToolCall = WaitingToolCall & {
responseSubmittedToGemini?: boolean;
};
export type TrackedExecutingToolCall = ExecutingToolCall & {
responseSubmittedToGemini?: boolean;
};
export type TrackedCompletedToolCall = CompletedToolCall & {
responseSubmittedToGemini?: boolean;
};
export type TrackedCancelledToolCall = CancelledToolCall & {
responseSubmittedToGemini?: boolean;
};
export type TrackedToolCall =
| TrackedScheduledToolCall
| TrackedValidatingToolCall
| TrackedWaitingToolCall
| TrackedExecutingToolCall
| TrackedCompletedToolCall
| TrackedCancelledToolCall;
/**
* Legacy scheduler implementation based on CoreToolScheduler callbacks.
*
* This is currently the default implementation used by useGeminiStream.
* It will be phased out once the event-driven scheduler migration is complete.
*/
export function useReactToolScheduler(
onComplete: (tools: CompletedToolCall[]) => Promise<void>,
config: Config,
getPreferredEditor: () => EditorType | undefined,
): [
TrackedToolCall[],
ScheduleFn,
MarkToolsAsSubmittedFn,
React.Dispatch<React.SetStateAction<TrackedToolCall[]>>,
CancelAllFn,
number,
] {
const [toolCallsForDisplay, setToolCallsForDisplay] = useState<
TrackedToolCall[]
>([]);
const [lastToolOutputTime, setLastToolOutputTime] = useState<number>(0);
const onCompleteRef = useRef(onComplete);
const getPreferredEditorRef = useRef(getPreferredEditor);
useEffect(() => {
onCompleteRef.current = onComplete;
}, [onComplete]);
useEffect(() => {
getPreferredEditorRef.current = getPreferredEditor;
}, [getPreferredEditor]);
const outputUpdateHandler: OutputUpdateHandler = useCallback(
(toolCallId, outputChunk) => {
setLastToolOutputTime(Date.now());
setToolCallsForDisplay((prevCalls) =>
prevCalls.map((tc) => {
if (tc.request.callId === toolCallId && tc.status === 'executing') {
const executingTc = tc;
return { ...executingTc, liveOutput: outputChunk };
}
return tc;
}),
);
},
[],
);
const allToolCallsCompleteHandler: AllToolCallsCompleteHandler = useCallback(
async (completedToolCalls) => {
await onCompleteRef.current(completedToolCalls);
},
[],
);
const toolCallsUpdateHandler: ToolCallsUpdateHandler = useCallback(
(allCoreToolCalls: ToolCall[]) => {
setToolCallsForDisplay((prevTrackedCalls) => {
const prevCallsMap = new Map(
prevTrackedCalls.map((c) => [c.request.callId, c]),
);
return allCoreToolCalls.map((coreTc): TrackedToolCall => {
const existingTrackedCall = prevCallsMap.get(coreTc.request.callId);
const responseSubmittedToGemini =
existingTrackedCall?.responseSubmittedToGemini ?? false;
if (coreTc.status === 'executing') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const liveOutput = (existingTrackedCall as TrackedExecutingToolCall)
?.liveOutput;
return {
...coreTc,
responseSubmittedToGemini,
liveOutput,
};
} else if (
coreTc.status === 'success' ||
coreTc.status === 'error' ||
coreTc.status === 'cancelled'
) {
return {
...coreTc,
responseSubmittedToGemini,
};
} else {
return {
...coreTc,
responseSubmittedToGemini,
};
}
});
});
},
[setToolCallsForDisplay],
);
const stableGetPreferredEditor = useCallback(
() => getPreferredEditorRef.current(),
[],
);
const scheduler = useMemo(
() =>
new CoreToolScheduler({
outputUpdateHandler,
onAllToolCallsComplete: allToolCallsCompleteHandler,
onToolCallsUpdate: toolCallsUpdateHandler,
getPreferredEditor: stableGetPreferredEditor,
config,
}),
[
config,
outputUpdateHandler,
allToolCallsCompleteHandler,
toolCallsUpdateHandler,
stableGetPreferredEditor,
],
);
const schedule: ScheduleFn = useCallback(
(
request: ToolCallRequestInfo | ToolCallRequestInfo[],
signal: AbortSignal,
) => {
setToolCallsForDisplay([]);
return scheduler.schedule(request, signal);
},
[scheduler, setToolCallsForDisplay],
);
const markToolsAsSubmitted: MarkToolsAsSubmittedFn = useCallback(
(callIdsToMark: string[]) => {
setToolCallsForDisplay((prevCalls) =>
prevCalls.map((tc) =>
callIdsToMark.includes(tc.request.callId)
? { ...tc, responseSubmittedToGemini: true }
: tc,
),
);
},
[],
);
const cancelAllToolCalls = useCallback(
(signal: AbortSignal) => {
scheduler.cancelAll(signal);
},
[scheduler],
);
return [
toolCallsForDisplay,
schedule,
markToolsAsSubmitted,
setToolCallsForDisplay,
cancelAllToolCalls,
lastToolOutputTime,
];
}
@@ -12,7 +12,7 @@ import {
SHELL_SILENT_WORKING_TITLE_DELAY_MS,
} from '../constants.js';
import type { StreamingState } from '../types.js';
import { type TrackedToolCall } from './useReactToolScheduler.js';
import { type TrackedToolCall } from './useToolScheduler.js';
interface ShellInactivityStatusProps {
activePtyId: number | string | null | undefined;
@@ -0,0 +1,201 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
describe,
it,
expect,
vi,
beforeEach,
afterEach,
type Mock,
} from 'vitest';
import { act } from 'react';
import { renderHook } from '../../test-utils/render.js';
import { useSuspend } from './useSuspend.js';
import {
writeToStdout,
disableMouseEvents,
enableMouseEvents,
enterAlternateScreen,
exitAlternateScreen,
enableLineWrapping,
disableLineWrapping,
} from '@google/gemini-cli-core';
import {
cleanupTerminalOnExit,
terminalCapabilityManager,
} from '../utils/terminalCapabilityManager.js';
vi.mock('@google/gemini-cli-core', async () => {
const actual = await vi.importActual('@google/gemini-cli-core');
return {
...actual,
writeToStdout: vi.fn(),
disableMouseEvents: vi.fn(),
enableMouseEvents: vi.fn(),
enterAlternateScreen: vi.fn(),
exitAlternateScreen: vi.fn(),
enableLineWrapping: vi.fn(),
disableLineWrapping: vi.fn(),
};
});
vi.mock('../utils/terminalCapabilityManager.js', () => ({
cleanupTerminalOnExit: vi.fn(),
terminalCapabilityManager: {
enableSupportedModes: vi.fn(),
},
}));
describe('useSuspend', () => {
const originalPlatform = process.platform;
let killSpy: Mock;
const setPlatform = (platform: NodeJS.Platform) => {
Object.defineProperty(process, 'platform', {
value: platform,
configurable: true,
});
};
beforeEach(() => {
vi.useFakeTimers();
vi.clearAllMocks();
killSpy = vi
.spyOn(process, 'kill')
.mockReturnValue(true) as unknown as Mock;
// Default tests to a POSIX platform so suspend path assertions are stable.
setPlatform('linux');
});
afterEach(() => {
vi.useRealTimers();
killSpy.mockRestore();
setPlatform(originalPlatform);
});
it('cleans terminal state on suspend and restores/repaints on resume in alternate screen mode', () => {
const handleWarning = vi.fn();
const setRawMode = vi.fn();
const refreshStatic = vi.fn();
const setForceRerenderKey = vi.fn();
const enableSupportedModes =
terminalCapabilityManager.enableSupportedModes as unknown as Mock;
const { result, unmount } = renderHook(() =>
useSuspend({
handleWarning,
setRawMode,
refreshStatic,
setForceRerenderKey,
shouldUseAlternateScreen: true,
}),
);
act(() => {
result.current.handleSuspend();
});
expect(handleWarning).toHaveBeenCalledWith(
'Press Ctrl+Z again to suspend. Undo has moved to Cmd + Z or Alt/Opt + Z.',
);
act(() => {
result.current.handleSuspend();
});
expect(exitAlternateScreen).toHaveBeenCalledTimes(1);
expect(enableLineWrapping).toHaveBeenCalledTimes(1);
expect(writeToStdout).toHaveBeenCalledWith('\x1b[2J\x1b[H');
expect(disableMouseEvents).toHaveBeenCalledTimes(1);
expect(cleanupTerminalOnExit).toHaveBeenCalledTimes(1);
expect(setRawMode).toHaveBeenCalledWith(false);
expect(killSpy).toHaveBeenCalledWith(0, 'SIGTSTP');
act(() => {
process.emit('SIGCONT');
vi.runAllTimers();
});
expect(enterAlternateScreen).toHaveBeenCalledTimes(1);
expect(disableLineWrapping).toHaveBeenCalledTimes(1);
expect(enableSupportedModes).toHaveBeenCalledTimes(1);
expect(enableMouseEvents).toHaveBeenCalledTimes(1);
expect(setRawMode).toHaveBeenCalledWith(true);
expect(refreshStatic).toHaveBeenCalledTimes(1);
expect(setForceRerenderKey).toHaveBeenCalledTimes(1);
unmount();
});
it('does not toggle alternate screen or mouse restore when alternate screen mode is disabled', () => {
const handleWarning = vi.fn();
const setRawMode = vi.fn();
const refreshStatic = vi.fn();
const setForceRerenderKey = vi.fn();
const { result, unmount } = renderHook(() =>
useSuspend({
handleWarning,
setRawMode,
refreshStatic,
setForceRerenderKey,
shouldUseAlternateScreen: false,
}),
);
act(() => {
result.current.handleSuspend();
result.current.handleSuspend();
process.emit('SIGCONT');
vi.runAllTimers();
});
expect(exitAlternateScreen).not.toHaveBeenCalled();
expect(enterAlternateScreen).not.toHaveBeenCalled();
expect(enableLineWrapping).not.toHaveBeenCalled();
expect(disableLineWrapping).not.toHaveBeenCalled();
expect(enableMouseEvents).not.toHaveBeenCalled();
unmount();
});
it('warns and skips suspension on windows', () => {
setPlatform('win32');
const handleWarning = vi.fn();
const setRawMode = vi.fn();
const refreshStatic = vi.fn();
const setForceRerenderKey = vi.fn();
const { result, unmount } = renderHook(() =>
useSuspend({
handleWarning,
setRawMode,
refreshStatic,
setForceRerenderKey,
shouldUseAlternateScreen: true,
}),
);
act(() => {
result.current.handleSuspend();
});
handleWarning.mockClear();
act(() => {
result.current.handleSuspend();
});
expect(handleWarning).toHaveBeenCalledWith(
'Ctrl+Z suspend is not supported on Windows.',
);
expect(killSpy).not.toHaveBeenCalled();
expect(cleanupTerminalOnExit).not.toHaveBeenCalled();
unmount();
});
});
+155
View File
@@ -0,0 +1,155 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { useState, useRef, useEffect, useCallback } from 'react';
import {
writeToStdout,
disableMouseEvents,
enableMouseEvents,
enterAlternateScreen,
exitAlternateScreen,
enableLineWrapping,
disableLineWrapping,
} from '@google/gemini-cli-core';
import process from 'node:process';
import {
cleanupTerminalOnExit,
terminalCapabilityManager,
} from '../utils/terminalCapabilityManager.js';
import { WARNING_PROMPT_DURATION_MS } from '../constants.js';
interface UseSuspendProps {
handleWarning: (message: string) => void;
setRawMode: (mode: boolean) => void;
refreshStatic: () => void;
setForceRerenderKey: (updater: (prev: number) => number) => void;
shouldUseAlternateScreen: boolean;
}
export function useSuspend({
handleWarning,
setRawMode,
refreshStatic,
setForceRerenderKey,
shouldUseAlternateScreen,
}: UseSuspendProps) {
const [ctrlZPressCount, setCtrlZPressCount] = useState(0);
const ctrlZTimerRef = useRef<NodeJS.Timeout | null>(null);
const onResumeHandlerRef = useRef<(() => void) | null>(null);
useEffect(
() => () => {
if (ctrlZTimerRef.current) {
clearTimeout(ctrlZTimerRef.current);
ctrlZTimerRef.current = null;
}
if (onResumeHandlerRef.current) {
process.off('SIGCONT', onResumeHandlerRef.current);
onResumeHandlerRef.current = null;
}
},
[],
);
useEffect(() => {
if (ctrlZTimerRef.current) {
clearTimeout(ctrlZTimerRef.current);
ctrlZTimerRef.current = null;
}
if (ctrlZPressCount > 1) {
setCtrlZPressCount(0);
if (process.platform === 'win32') {
handleWarning('Ctrl+Z suspend is not supported on Windows.');
return;
}
if (shouldUseAlternateScreen) {
// Leave alternate buffer before suspension so the shell stays usable.
exitAlternateScreen();
enableLineWrapping();
writeToStdout('\x1b[2J\x1b[H');
}
// Cleanup before suspend.
writeToStdout('\x1b[?25h'); // Show cursor
disableMouseEvents();
cleanupTerminalOnExit();
if (process.stdin.isTTY) {
process.stdin.setRawMode(false);
}
setRawMode(false);
const onResume = () => {
try {
// Restore terminal state.
if (process.stdin.isTTY) {
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.ref();
}
setRawMode(true);
if (shouldUseAlternateScreen) {
enterAlternateScreen();
disableLineWrapping();
writeToStdout('\x1b[2J\x1b[H');
}
terminalCapabilityManager.enableSupportedModes();
writeToStdout('\x1b[?25l'); // Hide cursor
if (shouldUseAlternateScreen) {
enableMouseEvents();
}
// Force Ink to do a complete repaint by:
// 1. Emitting a resize event (tricks Ink into full redraw)
// 2. Remounting components via state changes
process.stdout.emit('resize');
// Give a tick for resize to process, then trigger remount
setImmediate(() => {
refreshStatic();
setForceRerenderKey((prev) => prev + 1);
});
} finally {
if (onResumeHandlerRef.current === onResume) {
onResumeHandlerRef.current = null;
}
}
};
if (onResumeHandlerRef.current) {
process.off('SIGCONT', onResumeHandlerRef.current);
}
onResumeHandlerRef.current = onResume;
process.once('SIGCONT', onResume);
process.kill(0, 'SIGTSTP');
} else if (ctrlZPressCount > 0) {
handleWarning(
'Press Ctrl+Z again to suspend. Undo has moved to Cmd + Z or Alt/Opt + Z.',
);
ctrlZTimerRef.current = setTimeout(() => {
setCtrlZPressCount(0);
ctrlZTimerRef.current = null;
}, WARNING_PROMPT_DURATION_MS);
}
}, [
ctrlZPressCount,
handleWarning,
setRawMode,
refreshStatic,
setForceRerenderKey,
shouldUseAlternateScreen,
]);
const handleSuspend = useCallback(() => {
setCtrlZPressCount((prev) => prev + 1);
}, []);
return { handleSuspend };
}
@@ -1,525 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { act } from 'react';
import { renderHook } from '../../test-utils/render.js';
import { useToolExecutionScheduler } from './useToolExecutionScheduler.js';
import {
MessageBusType,
ToolConfirmationOutcome,
Scheduler,
type Config,
type MessageBus,
type CompletedToolCall,
type ToolCallConfirmationDetails,
type ToolCallsUpdateMessage,
type AnyDeclarativeTool,
type AnyToolInvocation,
ROOT_SCHEDULER_ID,
} from '@google/gemini-cli-core';
import { createMockMessageBus } from '@google/gemini-cli-core/src/test-utils/mock-message-bus.js';
// Mock Core Scheduler
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
Scheduler: vi.fn().mockImplementation(() => ({
schedule: vi.fn().mockResolvedValue([]),
cancelAll: vi.fn(),
})),
};
});
const createMockTool = (
overrides: Partial<AnyDeclarativeTool> = {},
): AnyDeclarativeTool =>
({
name: 'test_tool',
displayName: 'Test Tool',
description: 'A test tool',
kind: 'function',
parameterSchema: {},
isOutputMarkdown: false,
build: vi.fn(),
...overrides,
}) as AnyDeclarativeTool;
const createMockInvocation = (
overrides: Partial<AnyToolInvocation> = {},
): AnyToolInvocation =>
({
getDescription: () => 'Executing test tool',
shouldConfirmExecute: vi.fn(),
execute: vi.fn(),
params: {},
toolLocations: [],
...overrides,
}) as AnyToolInvocation;
describe('useToolExecutionScheduler', () => {
let mockConfig: Config;
let mockMessageBus: MessageBus;
beforeEach(() => {
vi.clearAllMocks();
mockMessageBus = createMockMessageBus() as unknown as MessageBus;
mockConfig = {
getMessageBus: () => mockMessageBus,
} as unknown as Config;
});
afterEach(() => {
vi.clearAllMocks();
});
it('initializes with empty tool calls', () => {
const { result } = renderHook(() =>
useToolExecutionScheduler(
vi.fn().mockResolvedValue(undefined),
mockConfig,
() => undefined,
),
);
const [toolCalls] = result.current;
expect(toolCalls).toEqual([]);
});
it('updates tool calls when MessageBus emits TOOL_CALLS_UPDATE', () => {
const { result } = renderHook(() =>
useToolExecutionScheduler(
vi.fn().mockResolvedValue(undefined),
mockConfig,
() => undefined,
),
);
const mockToolCall = {
status: 'executing' as const,
request: {
callId: 'call-1',
name: 'test_tool',
args: {},
isClientInitiated: false,
prompt_id: 'p1',
},
tool: createMockTool(),
invocation: createMockInvocation(),
liveOutput: 'Loading...',
};
act(() => {
void mockMessageBus.publish({
type: MessageBusType.TOOL_CALLS_UPDATE,
toolCalls: [mockToolCall],
schedulerId: ROOT_SCHEDULER_ID,
} as ToolCallsUpdateMessage);
});
const [toolCalls] = result.current;
expect(toolCalls).toHaveLength(1);
// Expect Core Object structure, not Display Object
expect(toolCalls[0]).toMatchObject({
request: { callId: 'call-1', name: 'test_tool' },
status: 'executing', // Core status
liveOutput: 'Loading...',
responseSubmittedToGemini: false,
});
});
it('injects onConfirm callback for awaiting_approval tools (Adapter Pattern)', async () => {
const { result } = renderHook(() =>
useToolExecutionScheduler(
vi.fn().mockResolvedValue(undefined),
mockConfig,
() => undefined,
),
);
const mockToolCall = {
status: 'awaiting_approval' as const,
request: {
callId: 'call-1',
name: 'test_tool',
args: {},
isClientInitiated: false,
prompt_id: 'p1',
},
tool: createMockTool(),
invocation: createMockInvocation({
getDescription: () => 'Confirming test tool',
}),
confirmationDetails: { type: 'info', title: 'Confirm', prompt: 'Sure?' },
correlationId: 'corr-123',
};
act(() => {
void mockMessageBus.publish({
type: MessageBusType.TOOL_CALLS_UPDATE,
toolCalls: [mockToolCall],
schedulerId: ROOT_SCHEDULER_ID,
} as ToolCallsUpdateMessage);
});
const [toolCalls] = result.current;
const call = toolCalls[0];
if (call.status !== 'awaiting_approval') {
throw new Error('Expected status to be awaiting_approval');
}
const confirmationDetails =
call.confirmationDetails as ToolCallConfirmationDetails;
expect(confirmationDetails).toBeDefined();
expect(typeof confirmationDetails.onConfirm).toBe('function');
// Test that onConfirm publishes to MessageBus
const publishSpy = vi.spyOn(mockMessageBus, 'publish');
await confirmationDetails.onConfirm(ToolConfirmationOutcome.ProceedOnce);
expect(publishSpy).toHaveBeenCalledWith({
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
correlationId: 'corr-123',
confirmed: true,
requiresUserConfirmation: false,
outcome: ToolConfirmationOutcome.ProceedOnce,
payload: undefined,
});
});
it('injects onConfirm with payload (Inline Edit support)', async () => {
const { result } = renderHook(() =>
useToolExecutionScheduler(
vi.fn().mockResolvedValue(undefined),
mockConfig,
() => undefined,
),
);
const mockToolCall = {
status: 'awaiting_approval' as const,
request: {
callId: 'call-1',
name: 'test_tool',
args: {},
isClientInitiated: false,
prompt_id: 'p1',
},
tool: createMockTool(),
invocation: createMockInvocation(),
confirmationDetails: { type: 'edit', title: 'Edit', filePath: 'test.ts' },
correlationId: 'corr-edit',
};
act(() => {
void mockMessageBus.publish({
type: MessageBusType.TOOL_CALLS_UPDATE,
toolCalls: [mockToolCall],
schedulerId: ROOT_SCHEDULER_ID,
} as ToolCallsUpdateMessage);
});
const [toolCalls] = result.current;
const call = toolCalls[0];
if (call.status !== 'awaiting_approval') {
throw new Error('Expected awaiting_approval');
}
const confirmationDetails =
call.confirmationDetails as ToolCallConfirmationDetails;
const publishSpy = vi.spyOn(mockMessageBus, 'publish');
const mockPayload = { newContent: 'updated code' };
await confirmationDetails.onConfirm(
ToolConfirmationOutcome.ProceedOnce,
mockPayload,
);
expect(publishSpy).toHaveBeenCalledWith({
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
correlationId: 'corr-edit',
confirmed: true,
requiresUserConfirmation: false,
outcome: ToolConfirmationOutcome.ProceedOnce,
payload: mockPayload,
});
});
it('preserves responseSubmittedToGemini flag across updates', () => {
const { result } = renderHook(() =>
useToolExecutionScheduler(
vi.fn().mockResolvedValue(undefined),
mockConfig,
() => undefined,
),
);
const mockToolCall = {
status: 'success' as const,
request: {
callId: 'call-1',
name: 'test',
args: {},
isClientInitiated: false,
prompt_id: 'p1',
},
tool: createMockTool(),
invocation: createMockInvocation(),
response: {
callId: 'call-1',
resultDisplay: 'OK',
responseParts: [],
error: undefined,
errorType: undefined,
},
};
// 1. Initial success
act(() => {
void mockMessageBus.publish({
type: MessageBusType.TOOL_CALLS_UPDATE,
toolCalls: [mockToolCall],
schedulerId: ROOT_SCHEDULER_ID,
} as ToolCallsUpdateMessage);
});
// 2. Mark as submitted
act(() => {
const [, , markAsSubmitted] = result.current;
markAsSubmitted(['call-1']);
});
expect(result.current[0][0].responseSubmittedToGemini).toBe(true);
// 3. Receive another update (should preserve the true flag)
act(() => {
void mockMessageBus.publish({
type: MessageBusType.TOOL_CALLS_UPDATE,
toolCalls: [mockToolCall],
schedulerId: ROOT_SCHEDULER_ID,
} as ToolCallsUpdateMessage);
});
expect(result.current[0][0].responseSubmittedToGemini).toBe(true);
});
it('updates lastToolOutputTime when tools are executing', () => {
vi.useFakeTimers();
const { result } = renderHook(() =>
useToolExecutionScheduler(
vi.fn().mockResolvedValue(undefined),
mockConfig,
() => undefined,
),
);
const startTime = Date.now();
vi.advanceTimersByTime(1000);
act(() => {
void mockMessageBus.publish({
type: MessageBusType.TOOL_CALLS_UPDATE,
toolCalls: [
{
status: 'executing' as const,
request: {
callId: 'call-1',
name: 'test',
args: {},
isClientInitiated: false,
prompt_id: 'p1',
},
tool: createMockTool(),
invocation: createMockInvocation(),
},
],
schedulerId: ROOT_SCHEDULER_ID,
} as ToolCallsUpdateMessage);
});
const [, , , , , lastOutputTime] = result.current;
expect(lastOutputTime).toBeGreaterThan(startTime);
vi.useRealTimers();
});
it('delegates cancelAll to the Core Scheduler', () => {
const { result } = renderHook(() =>
useToolExecutionScheduler(
vi.fn().mockResolvedValue(undefined),
mockConfig,
() => undefined,
),
);
const [, , , , cancelAll] = result.current;
const signal = new AbortController().signal;
// We need to find the mock instance of Scheduler
// Since we used vi.mock at top level, we can get it from vi.mocked(Scheduler)
const schedulerInstance = vi.mocked(Scheduler).mock.results[0].value;
cancelAll(signal);
expect(schedulerInstance.cancelAll).toHaveBeenCalled();
});
it('resolves the schedule promise when scheduler resolves', async () => {
const onComplete = vi.fn().mockResolvedValue(undefined);
const completedToolCall = {
status: 'success' as const,
request: {
callId: 'call-1',
name: 'test',
args: {},
isClientInitiated: false,
prompt_id: 'p1',
},
tool: createMockTool(),
invocation: createMockInvocation(),
response: {
callId: 'call-1',
responseParts: [],
resultDisplay: 'Success',
error: undefined,
errorType: undefined,
},
};
// Mock the specific return value for this test
const { Scheduler } = await import('@google/gemini-cli-core');
vi.mocked(Scheduler).mockImplementation(
() =>
({
schedule: vi.fn().mockResolvedValue([completedToolCall]),
cancelAll: vi.fn(),
}) as unknown as Scheduler,
);
const { result } = renderHook(() =>
useToolExecutionScheduler(onComplete, mockConfig, () => undefined),
);
const [, schedule] = result.current;
const signal = new AbortController().signal;
let completedResult: CompletedToolCall[] = [];
await act(async () => {
completedResult = await schedule(
{
callId: 'call-1',
name: 'test',
args: {},
isClientInitiated: false,
prompt_id: 'p1',
},
signal,
);
});
expect(completedResult).toEqual([completedToolCall]);
expect(onComplete).toHaveBeenCalledWith([completedToolCall]);
});
it('setToolCallsForDisplay re-groups tools by schedulerId (Multi-Scheduler support)', () => {
const { result } = renderHook(() =>
useToolExecutionScheduler(
vi.fn().mockResolvedValue(undefined),
mockConfig,
() => undefined,
),
);
const callRoot = {
status: 'success' as const,
request: {
callId: 'call-root',
name: 'test',
args: {},
isClientInitiated: false,
prompt_id: 'p1',
},
tool: createMockTool(),
invocation: createMockInvocation(),
response: {
callId: 'call-root',
responseParts: [],
resultDisplay: 'OK',
error: undefined,
errorType: undefined,
},
schedulerId: ROOT_SCHEDULER_ID,
};
const callSub = {
...callRoot,
request: { ...callRoot.request, callId: 'call-sub' },
schedulerId: 'subagent-1',
};
// 1. Populate state with multiple schedulers
act(() => {
void mockMessageBus.publish({
type: MessageBusType.TOOL_CALLS_UPDATE,
toolCalls: [callRoot],
schedulerId: ROOT_SCHEDULER_ID,
} as ToolCallsUpdateMessage);
void mockMessageBus.publish({
type: MessageBusType.TOOL_CALLS_UPDATE,
toolCalls: [callSub],
schedulerId: 'subagent-1',
} as ToolCallsUpdateMessage);
});
let [toolCalls] = result.current;
expect(toolCalls).toHaveLength(2);
expect(
toolCalls.find((t) => t.request.callId === 'call-root')?.schedulerId,
).toBe(ROOT_SCHEDULER_ID);
expect(
toolCalls.find((t) => t.request.callId === 'call-sub')?.schedulerId,
).toBe('subagent-1');
// 2. Call setToolCallsForDisplay (e.g., simulate a manual update or clear)
act(() => {
const [, , , setToolCalls] = result.current;
setToolCalls((prev) =>
prev.map((t) => ({ ...t, responseSubmittedToGemini: true })),
);
});
// 3. Verify that tools are still present and maintain their scheduler IDs
// The internal map should have been re-grouped.
[toolCalls] = result.current;
expect(toolCalls).toHaveLength(2);
expect(toolCalls.every((t) => t.responseSubmittedToGemini)).toBe(true);
const updatedRoot = toolCalls.find((t) => t.request.callId === 'call-root');
const updatedSub = toolCalls.find((t) => t.request.callId === 'call-sub');
expect(updatedRoot?.schedulerId).toBe(ROOT_SCHEDULER_ID);
expect(updatedSub?.schedulerId).toBe('subagent-1');
// 4. Verify that a subsequent update to ONE scheduler doesn't wipe the other
act(() => {
void mockMessageBus.publish({
type: MessageBusType.TOOL_CALLS_UPDATE,
toolCalls: [{ ...callRoot, status: 'executing' }],
schedulerId: ROOT_SCHEDULER_ID,
} as ToolCallsUpdateMessage);
});
[toolCalls] = result.current;
expect(toolCalls).toHaveLength(2);
expect(
toolCalls.find((t) => t.request.callId === 'call-root')?.status,
).toBe('executing');
expect(
toolCalls.find((t) => t.request.callId === 'call-sub')?.schedulerId,
).toBe('subagent-1');
});
});
@@ -1,253 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
type Config,
type MessageBus,
type ToolCallRequestInfo,
type ToolCall,
type CompletedToolCall,
type ToolConfirmationPayload,
MessageBusType,
ToolConfirmationOutcome,
Scheduler,
type EditorType,
type ToolCallsUpdateMessage,
ROOT_SCHEDULER_ID,
} from '@google/gemini-cli-core';
import { useCallback, useState, useMemo, useEffect, useRef } from 'react';
// Re-exporting types compatible with legacy hook expectations
export type ScheduleFn = (
request: ToolCallRequestInfo | ToolCallRequestInfo[],
signal: AbortSignal,
) => Promise<CompletedToolCall[]>;
export type MarkToolsAsSubmittedFn = (callIds: string[]) => void;
export type CancelAllFn = (signal: AbortSignal) => void;
/**
* The shape expected by useGeminiStream.
* It matches the Core ToolCall structure + the UI metadata flag.
*/
export type TrackedToolCall = ToolCall & {
responseSubmittedToGemini?: boolean;
};
/**
* Modern tool scheduler hook using the event-driven Core Scheduler.
*
* This hook acts as an Adapter between the new MessageBus-driven Core
* and the legacy callback-based UI components.
*/
export function useToolExecutionScheduler(
onComplete: (tools: CompletedToolCall[]) => Promise<void>,
config: Config,
getPreferredEditor: () => EditorType | undefined,
): [
TrackedToolCall[],
ScheduleFn,
MarkToolsAsSubmittedFn,
React.Dispatch<React.SetStateAction<TrackedToolCall[]>>,
CancelAllFn,
number,
] {
// State stores tool calls organized by their originating schedulerId
const [toolCallsMap, setToolCallsMap] = useState<
Record<string, TrackedToolCall[]>
>({});
const [lastToolOutputTime, setLastToolOutputTime] = useState<number>(0);
const messageBus = useMemo(() => config.getMessageBus(), [config]);
const onCompleteRef = useRef(onComplete);
useEffect(() => {
onCompleteRef.current = onComplete;
}, [onComplete]);
const getPreferredEditorRef = useRef(getPreferredEditor);
useEffect(() => {
getPreferredEditorRef.current = getPreferredEditor;
}, [getPreferredEditor]);
const scheduler = useMemo(
() =>
new Scheduler({
config,
messageBus,
getPreferredEditor: () => getPreferredEditorRef.current(),
schedulerId: ROOT_SCHEDULER_ID,
}),
[config, messageBus],
);
const internalAdaptToolCalls = useCallback(
(coreCalls: ToolCall[], prevTracked: TrackedToolCall[]) =>
adaptToolCalls(coreCalls, prevTracked, messageBus),
[messageBus],
);
useEffect(() => {
const handler = (event: ToolCallsUpdateMessage) => {
// Update output timer for UI spinners (Side Effect)
if (event.toolCalls.some((tc) => tc.status === 'executing')) {
setLastToolOutputTime(Date.now());
}
setToolCallsMap((prev) => {
const adapted = internalAdaptToolCalls(
event.toolCalls,
prev[event.schedulerId] ?? [],
);
return {
...prev,
[event.schedulerId]: adapted,
};
});
};
messageBus.subscribe(MessageBusType.TOOL_CALLS_UPDATE, handler);
return () => {
messageBus.unsubscribe(MessageBusType.TOOL_CALLS_UPDATE, handler);
};
}, [messageBus, internalAdaptToolCalls]);
const schedule: ScheduleFn = useCallback(
async (request, signal) => {
// Clear state for new run
setToolCallsMap({});
// 1. Await Core Scheduler directly
const results = await scheduler.schedule(request, signal);
// 2. Trigger legacy reinjection logic (useGeminiStream loop)
// Since this hook instance owns the "root" scheduler, we always trigger
// onComplete when it finishes its batch.
await onCompleteRef.current(results);
return results;
},
[scheduler],
);
const cancelAll: CancelAllFn = useCallback(
(_signal) => {
scheduler.cancelAll();
},
[scheduler],
);
const markToolsAsSubmitted: MarkToolsAsSubmittedFn = useCallback(
(callIdsToMark: string[]) => {
setToolCallsMap((prevMap) => {
const nextMap = { ...prevMap };
for (const [sid, calls] of Object.entries(nextMap)) {
nextMap[sid] = calls.map((tc) =>
callIdsToMark.includes(tc.request.callId)
? { ...tc, responseSubmittedToGemini: true }
: tc,
);
}
return nextMap;
});
},
[],
);
// Flatten the map for the UI components that expect a single list of tools.
const toolCalls = useMemo(
() => Object.values(toolCallsMap).flat(),
[toolCallsMap],
);
// Provide a setter that maintains compatibility with legacy [].
const setToolCallsForDisplay = useCallback(
(action: React.SetStateAction<TrackedToolCall[]>) => {
setToolCallsMap((prev) => {
const currentFlattened = Object.values(prev).flat();
const nextFlattened =
typeof action === 'function' ? action(currentFlattened) : action;
if (nextFlattened.length === 0) {
return {};
}
// Re-group by schedulerId to preserve multi-scheduler state
const nextMap: Record<string, TrackedToolCall[]> = {};
for (const call of nextFlattened) {
// All tool calls should have a schedulerId from the core.
// Default to ROOT_SCHEDULER_ID as a safeguard.
const sid = call.schedulerId ?? ROOT_SCHEDULER_ID;
if (!nextMap[sid]) {
nextMap[sid] = [];
}
nextMap[sid].push(call);
}
return nextMap;
});
},
[],
);
return [
toolCalls,
schedule,
markToolsAsSubmitted,
setToolCallsForDisplay,
cancelAll,
lastToolOutputTime,
];
}
/**
* ADAPTER: Merges UI metadata (submitted flag) and injects legacy callbacks.
*/
function adaptToolCalls(
coreCalls: ToolCall[],
prevTracked: TrackedToolCall[],
messageBus: MessageBus,
): TrackedToolCall[] {
const prevMap = new Map(prevTracked.map((t) => [t.request.callId, t]));
return coreCalls.map((coreCall): TrackedToolCall => {
const prev = prevMap.get(coreCall.request.callId);
const responseSubmittedToGemini = prev?.responseSubmittedToGemini ?? false;
// Inject onConfirm adapter for tools awaiting approval.
// The Core provides data-only (serializable) confirmationDetails. We must
// inject the legacy callback function that proxies responses back to the
// MessageBus.
if (coreCall.status === 'awaiting_approval' && coreCall.correlationId) {
const correlationId = coreCall.correlationId;
return {
...coreCall,
confirmationDetails: {
...coreCall.confirmationDetails,
onConfirm: async (
outcome: ToolConfirmationOutcome,
payload?: ToolConfirmationPayload,
) => {
await messageBus.publish({
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
correlationId,
confirmed: outcome !== ToolConfirmationOutcome.Cancel,
requiresUserConfirmation: false,
outcome,
payload,
});
},
},
responseSubmittedToGemini,
};
}
return {
...coreCall,
responseSubmittedToGemini,
};
});
}
File diff suppressed because it is too large Load Diff
+254 -48
View File
@@ -4,67 +4,273 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type {
Config,
EditorType,
CompletedToolCall,
ToolCallRequestInfo,
import {
type Config,
type MessageBus,
type ToolCallRequestInfo,
type ToolCall,
type CompletedToolCall,
type ToolConfirmationPayload,
MessageBusType,
ToolConfirmationOutcome,
Scheduler,
type EditorType,
type ToolCallsUpdateMessage,
ROOT_SCHEDULER_ID,
} from '@google/gemini-cli-core';
import {
type TrackedScheduledToolCall,
type TrackedValidatingToolCall,
type TrackedWaitingToolCall,
type TrackedExecutingToolCall,
type TrackedCompletedToolCall,
type TrackedCancelledToolCall,
type MarkToolsAsSubmittedFn,
type CancelAllFn,
} from './useReactToolScheduler.js';
import {
useToolExecutionScheduler,
type TrackedToolCall,
} from './useToolExecutionScheduler.js';
import { useCallback, useState, useMemo, useEffect, useRef } from 'react';
// Re-export specific state types from Legacy, as the structures are compatible
// and useGeminiStream relies on them for narrowing.
export type {
TrackedToolCall,
TrackedScheduledToolCall,
TrackedValidatingToolCall,
TrackedWaitingToolCall,
TrackedExecutingToolCall,
TrackedCompletedToolCall,
TrackedCancelledToolCall,
MarkToolsAsSubmittedFn,
CancelAllFn,
};
// Unified Schedule function (Promise<void> | Promise<CompletedToolCall[]>)
// Re-exporting types compatible with legacy hook expectations
export type ScheduleFn = (
request: ToolCallRequestInfo | ToolCallRequestInfo[],
signal: AbortSignal,
) => Promise<void | CompletedToolCall[]>;
) => Promise<CompletedToolCall[]>;
export type UseToolSchedulerReturn = [
export type MarkToolsAsSubmittedFn = (callIds: string[]) => void;
export type CancelAllFn = (signal: AbortSignal) => void;
/**
* The shape expected by useGeminiStream.
* It matches the Core ToolCall structure + the UI metadata flag.
*/
export type TrackedToolCall = ToolCall & {
responseSubmittedToGemini?: boolean;
};
// Narrowed types for specific statuses (used by useGeminiStream)
export type TrackedScheduledToolCall = Extract<
TrackedToolCall,
{ status: 'scheduled' }
>;
export type TrackedValidatingToolCall = Extract<
TrackedToolCall,
{ status: 'validating' }
>;
export type TrackedWaitingToolCall = Extract<
TrackedToolCall,
{ status: 'awaiting_approval' }
>;
export type TrackedExecutingToolCall = Extract<
TrackedToolCall,
{ status: 'executing' }
>;
export type TrackedCompletedToolCall = Extract<
TrackedToolCall,
{ status: 'success' | 'error' }
>;
export type TrackedCancelledToolCall = Extract<
TrackedToolCall,
{ status: 'cancelled' }
>;
/**
* Modern tool scheduler hook using the event-driven Core Scheduler.
*/
export function useToolScheduler(
onComplete: (tools: CompletedToolCall[]) => Promise<void>,
config: Config,
getPreferredEditor: () => EditorType | undefined,
): [
TrackedToolCall[],
ScheduleFn,
MarkToolsAsSubmittedFn,
React.Dispatch<React.SetStateAction<TrackedToolCall[]>>,
CancelAllFn,
number,
];
] {
// State stores tool calls organized by their originating schedulerId
const [toolCallsMap, setToolCallsMap] = useState<
Record<string, TrackedToolCall[]>
>({});
const [lastToolOutputTime, setLastToolOutputTime] = useState<number>(0);
const messageBus = useMemo(() => config.getMessageBus(), [config]);
const onCompleteRef = useRef(onComplete);
useEffect(() => {
onCompleteRef.current = onComplete;
}, [onComplete]);
const getPreferredEditorRef = useRef(getPreferredEditor);
useEffect(() => {
getPreferredEditorRef.current = getPreferredEditor;
}, [getPreferredEditor]);
const scheduler = useMemo(
() =>
new Scheduler({
config,
messageBus,
getPreferredEditor: () => getPreferredEditorRef.current(),
schedulerId: ROOT_SCHEDULER_ID,
}),
[config, messageBus],
);
const internalAdaptToolCalls = useCallback(
(coreCalls: ToolCall[], prevTracked: TrackedToolCall[]) =>
adaptToolCalls(coreCalls, prevTracked, messageBus),
[messageBus],
);
useEffect(() => {
const handler = (event: ToolCallsUpdateMessage) => {
// Update output timer for UI spinners (Side Effect)
if (event.toolCalls.some((tc) => tc.status === 'executing')) {
setLastToolOutputTime(Date.now());
}
setToolCallsMap((prev) => {
const adapted = internalAdaptToolCalls(
event.toolCalls,
prev[event.schedulerId] ?? [],
);
return {
...prev,
[event.schedulerId]: adapted,
};
});
};
messageBus.subscribe(MessageBusType.TOOL_CALLS_UPDATE, handler);
return () => {
messageBus.unsubscribe(MessageBusType.TOOL_CALLS_UPDATE, handler);
};
}, [messageBus, internalAdaptToolCalls]);
const schedule: ScheduleFn = useCallback(
async (request, signal) => {
// Clear state for new run
setToolCallsMap({});
// 1. Await Core Scheduler directly
const results = await scheduler.schedule(request, signal);
// 2. Trigger legacy reinjection logic (useGeminiStream loop)
// Since this hook instance owns the "root" scheduler, we always trigger
// onComplete when it finishes its batch.
await onCompleteRef.current(results);
return results;
},
[scheduler],
);
const cancelAll: CancelAllFn = useCallback(
(_signal) => {
scheduler.cancelAll();
},
[scheduler],
);
const markToolsAsSubmitted: MarkToolsAsSubmittedFn = useCallback(
(callIdsToMark: string[]) => {
setToolCallsMap((prevMap) => {
const nextMap = { ...prevMap };
for (const [sid, calls] of Object.entries(nextMap)) {
nextMap[sid] = calls.map((tc) =>
callIdsToMark.includes(tc.request.callId)
? { ...tc, responseSubmittedToGemini: true }
: tc,
);
}
return nextMap;
});
},
[],
);
// Flatten the map for the UI components that expect a single list of tools.
const toolCalls = useMemo(
() => Object.values(toolCallsMap).flat(),
[toolCallsMap],
);
// Provide a setter that maintains compatibility with legacy [].
const setToolCallsForDisplay = useCallback(
(action: React.SetStateAction<TrackedToolCall[]>) => {
setToolCallsMap((prev) => {
const currentFlattened = Object.values(prev).flat();
const nextFlattened =
typeof action === 'function' ? action(currentFlattened) : action;
if (nextFlattened.length === 0) {
return {};
}
// Re-group by schedulerId to preserve multi-scheduler state
const nextMap: Record<string, TrackedToolCall[]> = {};
for (const call of nextFlattened) {
// All tool calls should have a schedulerId from the core.
// Default to ROOT_SCHEDULER_ID as a safeguard.
const sid = call.schedulerId ?? ROOT_SCHEDULER_ID;
if (!nextMap[sid]) {
nextMap[sid] = [];
}
nextMap[sid].push(call);
}
return nextMap;
});
},
[],
);
return [
toolCalls,
schedule,
markToolsAsSubmitted,
setToolCallsForDisplay,
cancelAll,
lastToolOutputTime,
];
}
/**
* Hook that uses the Event-Driven scheduler for tool execution.
* ADAPTER: Merges UI metadata (submitted flag) and injects legacy callbacks.
*/
export function useToolScheduler(
onComplete: (tools: CompletedToolCall[]) => Promise<void>,
config: Config,
getPreferredEditor: () => EditorType | undefined,
): UseToolSchedulerReturn {
return useToolExecutionScheduler(
onComplete,
config,
getPreferredEditor,
) as UseToolSchedulerReturn;
function adaptToolCalls(
coreCalls: ToolCall[],
prevTracked: TrackedToolCall[],
messageBus: MessageBus,
): TrackedToolCall[] {
const prevMap = new Map(prevTracked.map((t) => [t.request.callId, t]));
return coreCalls.map((coreCall): TrackedToolCall => {
const prev = prevMap.get(coreCall.request.callId);
const responseSubmittedToGemini = prev?.responseSubmittedToGemini ?? false;
// Inject onConfirm adapter for tools awaiting approval.
// The Core provides data-only (serializable) confirmationDetails. We must
// inject the legacy callback function that proxies responses back to the
// MessageBus.
if (coreCall.status === 'awaiting_approval' && coreCall.correlationId) {
const correlationId = coreCall.correlationId;
return {
...coreCall,
confirmationDetails: {
...coreCall.confirmationDetails,
onConfirm: async (
outcome: ToolConfirmationOutcome,
payload?: ToolConfirmationPayload,
) => {
await messageBus.publish({
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
correlationId,
confirmed: outcome !== ToolConfirmationOutcome.Cancel,
requiresUserConfirmation: false,
outcome,
payload,
});
},
},
responseSubmittedToGemini,
};
}
return {
...coreCall,
responseSubmittedToGemini,
};
});
}
@@ -9,7 +9,7 @@ import { renderHook } from '../../test-utils/render.js';
import { useTurnActivityMonitor } from './useTurnActivityMonitor.js';
import { StreamingState } from '../types.js';
import { hasRedirection } from '@google/gemini-cli-core';
import { type TrackedToolCall } from './useReactToolScheduler.js';
import { type TrackedToolCall } from './useToolScheduler.js';
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual = await importOriginal<Record<string, unknown>>();
@@ -7,7 +7,7 @@
import { useState, useEffect, useRef, useMemo } from 'react';
import { StreamingState } from '../types.js';
import { hasRedirection } from '@google/gemini-cli-core';
import { type TrackedToolCall } from './useReactToolScheduler.js';
import { type TrackedToolCall } from './useToolScheduler.js';
export interface TurnActivityStatus {
operationStartTime: number;
+12
View File
@@ -330,6 +330,18 @@ describe('keyMatchers', () => {
positive: [createKey('d', { ctrl: true })],
negative: [createKey('d'), createKey('c', { ctrl: true })],
},
{
command: Command.SUSPEND_APP,
positive: [
createKey('z', { ctrl: true }),
createKey('z', { ctrl: true, shift: true }),
],
negative: [
createKey('z'),
createKey('y', { ctrl: true }),
createKey('z', { alt: true }),
],
},
{
command: Command.SHOW_MORE_LINES,
positive: [
@@ -0,0 +1,12 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { Command, keyMatchers } from '../keyMatchers.js';
import type { Key } from '../hooks/useKeypress.js';
export function shouldDismissShortcutsHelpOnHotkey(key: Key): boolean {
return Object.values(Command).some((command) => keyMatchers[command](key));
}
@@ -18,6 +18,23 @@ import { parseColor } from '../themes/color-utils.js';
export type TerminalBackgroundColor = string | undefined;
const TERMINAL_CLEANUP_SEQUENCE = '\x1b[<u\x1b[>4;0m\x1b[?2004l';
export function cleanupTerminalOnExit() {
try {
if (process.stdout?.fd !== undefined) {
fs.writeSync(process.stdout.fd, TERMINAL_CLEANUP_SEQUENCE);
return;
}
} catch (e) {
debugLogger.warn('Failed to synchronously cleanup terminal modes:', e);
}
disableKittyKeyboardProtocol();
disableModifyOtherKeys();
disableBracketedPasteMode();
}
export class TerminalCapabilityManager {
private static instance: TerminalCapabilityManager | undefined;
@@ -64,14 +81,6 @@ export class TerminalCapabilityManager {
this.instance = undefined;
}
private static cleanupOnExit(): void {
// don't bother catching errors since if one write
// fails, the other probably will too
disableKittyKeyboardProtocol();
disableModifyOtherKeys();
disableBracketedPasteMode();
}
/**
* Detects terminal capabilities (Kitty protocol support, terminal name,
* background color).
@@ -85,12 +94,12 @@ export class TerminalCapabilityManager {
return;
}
process.off('exit', TerminalCapabilityManager.cleanupOnExit);
process.off('SIGTERM', TerminalCapabilityManager.cleanupOnExit);
process.off('SIGINT', TerminalCapabilityManager.cleanupOnExit);
process.on('exit', TerminalCapabilityManager.cleanupOnExit);
process.on('SIGTERM', TerminalCapabilityManager.cleanupOnExit);
process.on('SIGINT', TerminalCapabilityManager.cleanupOnExit);
process.off('exit', cleanupTerminalOnExit);
process.off('SIGTERM', cleanupTerminalOnExit);
process.off('SIGINT', cleanupTerminalOnExit);
process.on('exit', cleanupTerminalOnExit);
process.on('SIGTERM', cleanupTerminalOnExit);
process.on('SIGINT', cleanupTerminalOnExit);
return new Promise((resolve) => {
const originalRawMode = process.stdin.isRaw;
@@ -1,32 +0,0 @@
(version 1)
;; allow everything by default
(allow default)
;; deny all writes EXCEPT under specific paths
(deny file-write*)
(allow file-write*
(subpath (param "TARGET_DIR"))
(subpath (param "TMP_DIR"))
(subpath (param "CACHE_DIR"))
(subpath (string-append (param "HOME_DIR") "/.gemini"))
(subpath (string-append (param "HOME_DIR") "/.npm"))
(subpath (string-append (param "HOME_DIR") "/.cache"))
(subpath (string-append (param "HOME_DIR") "/.gitconfig"))
;; Allow writes to included directories from --include-directories
(subpath (param "INCLUDE_DIR_0"))
(subpath (param "INCLUDE_DIR_1"))
(subpath (param "INCLUDE_DIR_2"))
(subpath (param "INCLUDE_DIR_3"))
(subpath (param "INCLUDE_DIR_4"))
(literal "/dev/stdout")
(literal "/dev/stderr")
(literal "/dev/null")
)
;; deny all inbound network traffic EXCEPT on debugger port
(deny network-inbound)
(allow network-inbound (local ip "localhost:9229"))
;; deny all outbound network traffic
(deny network-outbound)
@@ -3,8 +3,43 @@
;; deny everything by default
(deny default)
;; allow reading files from anywhere on host
(allow file-read*)
;; allow reading ONLY from working directory, system paths, and essential user paths
(allow file-read*
(literal "/")
(subpath (param "TARGET_DIR"))
(subpath (param "TMP_DIR"))
(subpath (param "CACHE_DIR"))
;; Only allow reading essential dotfiles/directories under HOME, not the entire HOME
(subpath (string-append (param "HOME_DIR") "/.gemini"))
(subpath (string-append (param "HOME_DIR") "/.npm"))
(subpath (string-append (param "HOME_DIR") "/.cache"))
(literal (string-append (param "HOME_DIR") "/.gitconfig"))
(subpath (string-append (param "HOME_DIR") "/.nvm"))
(subpath (string-append (param "HOME_DIR") "/.fnm"))
(subpath (string-append (param "HOME_DIR") "/.node"))
(subpath (string-append (param "HOME_DIR") "/.config"))
;; Allow reads from included directories
(subpath (param "INCLUDE_DIR_0"))
(subpath (param "INCLUDE_DIR_1"))
(subpath (param "INCLUDE_DIR_2"))
(subpath (param "INCLUDE_DIR_3"))
(subpath (param "INCLUDE_DIR_4"))
;; System paths required for Node.js, shell, and common tools
(subpath "/usr")
(subpath "/bin")
(subpath "/sbin")
(subpath "/Library")
(subpath "/System")
(subpath "/private")
(subpath "/dev")
(subpath "/etc")
(subpath "/opt")
(subpath "/Applications")
)
;; allow path traversal everywhere (metadata only: stat/lstat, NOT readdir or file content)
;; this is needed for Node.js module resolution to traverse intermediate directories
(allow file-read-metadata)
;; allow exec/fork (children inherit policy)
(allow process-exec)
@@ -70,7 +105,7 @@
(subpath (string-append (param "HOME_DIR") "/.gemini"))
(subpath (string-append (param "HOME_DIR") "/.npm"))
(subpath (string-append (param "HOME_DIR") "/.cache"))
(subpath (string-append (param "HOME_DIR") "/.gitconfig"))
(literal (string-append (param "HOME_DIR") "/.gitconfig"))
;; Allow writes to included directories from --include-directories
(subpath (param "INCLUDE_DIR_0"))
(subpath (param "INCLUDE_DIR_1"))
@@ -90,4 +125,7 @@
(allow file-ioctl (regex #"^/dev/tty.*"))
;; allow inbound network traffic on debugger port
(allow network-inbound (local ip "localhost:9229"))
(allow network-inbound (local ip "localhost:9229"))
;; allow all outbound network traffic
(allow network-outbound)
@@ -0,0 +1,133 @@
(version 1)
;; deny everything by default
(deny default)
;; allow reading ONLY from working directory, system paths, and essential user paths
(allow file-read*
(literal "/")
(subpath (param "TARGET_DIR"))
(subpath (param "TMP_DIR"))
(subpath (param "CACHE_DIR"))
;; Only allow reading essential dotfiles/directories under HOME, not the entire HOME
(subpath (string-append (param "HOME_DIR") "/.gemini"))
(subpath (string-append (param "HOME_DIR") "/.npm"))
(subpath (string-append (param "HOME_DIR") "/.cache"))
(literal (string-append (param "HOME_DIR") "/.gitconfig"))
(subpath (string-append (param "HOME_DIR") "/.nvm"))
(subpath (string-append (param "HOME_DIR") "/.fnm"))
(subpath (string-append (param "HOME_DIR") "/.node"))
(subpath (string-append (param "HOME_DIR") "/.config"))
;; Allow reads from included directories
(subpath (param "INCLUDE_DIR_0"))
(subpath (param "INCLUDE_DIR_1"))
(subpath (param "INCLUDE_DIR_2"))
(subpath (param "INCLUDE_DIR_3"))
(subpath (param "INCLUDE_DIR_4"))
;; System paths required for Node.js, shell, and common tools
(subpath "/usr")
(subpath "/bin")
(subpath "/sbin")
(subpath "/Library")
(subpath "/System")
(subpath "/private")
(subpath "/dev")
(subpath "/etc")
(subpath "/opt")
(subpath "/Applications")
)
;; allow path traversal everywhere (metadata only: stat/lstat, NOT readdir or file content)
;; this is needed for Node.js module resolution to traverse intermediate directories
(allow file-read-metadata)
;; allow exec/fork (children inherit policy)
(allow process-exec)
(allow process-fork)
;; allow signals to self, e.g. SIGPIPE on write to closed pipe
(allow signal (target self))
;; allow read access to specific information about system
;; from https://source.chromium.org/chromium/chromium/src/+/main:sandbox/policy/mac/common.sb;l=273-319;drc=7b3962fe2e5fc9e2ee58000dc8fbf3429d84d3bd
(allow sysctl-read
(sysctl-name "hw.activecpu")
(sysctl-name "hw.busfrequency_compat")
(sysctl-name "hw.byteorder")
(sysctl-name "hw.cacheconfig")
(sysctl-name "hw.cachelinesize_compat")
(sysctl-name "hw.cpufamily")
(sysctl-name "hw.cpufrequency_compat")
(sysctl-name "hw.cputype")
(sysctl-name "hw.l1dcachesize_compat")
(sysctl-name "hw.l1icachesize_compat")
(sysctl-name "hw.l2cachesize_compat")
(sysctl-name "hw.l3cachesize_compat")
(sysctl-name "hw.logicalcpu_max")
(sysctl-name "hw.machine")
(sysctl-name "hw.ncpu")
(sysctl-name "hw.nperflevels")
(sysctl-name "hw.optional.arm.FEAT_BF16")
(sysctl-name "hw.optional.arm.FEAT_DotProd")
(sysctl-name "hw.optional.arm.FEAT_FCMA")
(sysctl-name "hw.optional.arm.FEAT_FHM")
(sysctl-name "hw.optional.arm.FEAT_FP16")
(sysctl-name "hw.optional.arm.FEAT_I8MM")
(sysctl-name "hw.optional.arm.FEAT_JSCVT")
(sysctl-name "hw.optional.arm.FEAT_LSE")
(sysctl-name "hw.optional.arm.FEAT_RDM")
(sysctl-name "hw.optional.arm.FEAT_SHA512")
(sysctl-name "hw.optional.armv8_2_sha512")
(sysctl-name "hw.packages")
(sysctl-name "hw.pagesize_compat")
(sysctl-name "hw.physicalcpu_max")
(sysctl-name "hw.tbfrequency_compat")
(sysctl-name "hw.vectorunit")
(sysctl-name "kern.hostname")
(sysctl-name "kern.maxfilesperproc")
(sysctl-name "kern.osproductversion")
(sysctl-name "kern.osrelease")
(sysctl-name "kern.ostype")
(sysctl-name "kern.osvariant_status")
(sysctl-name "kern.osversion")
(sysctl-name "kern.secure_kernel")
(sysctl-name "kern.usrstack64")
(sysctl-name "kern.version")
(sysctl-name "sysctl.proc_cputype")
(sysctl-name-prefix "hw.perflevel")
)
;; allow writes to specific paths
(allow file-write*
(subpath (param "TARGET_DIR"))
(subpath (param "TMP_DIR"))
(subpath (param "CACHE_DIR"))
(subpath (string-append (param "HOME_DIR") "/.gemini"))
(subpath (string-append (param "HOME_DIR") "/.npm"))
(subpath (string-append (param "HOME_DIR") "/.cache"))
(literal (string-append (param "HOME_DIR") "/.gitconfig"))
;; Allow writes to included directories from --include-directories
(subpath (param "INCLUDE_DIR_0"))
(subpath (param "INCLUDE_DIR_1"))
(subpath (param "INCLUDE_DIR_2"))
(subpath (param "INCLUDE_DIR_3"))
(subpath (param "INCLUDE_DIR_4"))
(literal "/dev/stdout")
(literal "/dev/stderr")
(literal "/dev/null")
)
;; allow communication with sysmond for process listing (e.g. for pgrep)
(allow mach-lookup (global-name "com.apple.sysmond"))
;; enable terminal access required by ink
;; fixes setRawMode EPERM failure (at node:tty:81:24)
(allow file-ioctl (regex #"^/dev/tty.*"))
;; allow inbound network traffic on debugger port
(allow network-inbound (local ip "localhost:9229"))
;; allow outbound network traffic through proxy on localhost:8877
;; set `GEMINI_SANDBOX_PROXY_COMMAND=<command>` to run proxy alongside sandbox
;; proxy must listen on :::8877 (see docs/examples/proxy-script.md)
(allow network-outbound (remote tcp "localhost:8877"))
+2 -2
View File
@@ -15,11 +15,11 @@ export const SANDBOX_NETWORK_NAME = 'gemini-cli-sandbox';
export const SANDBOX_PROXY_NAME = 'gemini-cli-sandbox-proxy';
export const BUILTIN_SEATBELT_PROFILES = [
'permissive-open',
'permissive-closed',
'permissive-proxied',
'restrictive-open',
'restrictive-closed',
'restrictive-proxied',
'strict-open',
'strict-proxied',
];
export function getContainerPath(hostPath: string): string {
+38
View File
@@ -1036,6 +1036,44 @@ describe('Server Config (config.ts)', () => {
expect(registeredWrappers).toHaveLength(1);
});
it('should register subagents as tools even when they are not in allowedTools', async () => {
const params: ConfigParameters = {
...baseParams,
allowedTools: ['read_file'], // codebase-investigator is NOT here
agents: {
overrides: {
codebase_investigator: { enabled: true },
},
},
};
const config = new Config(params);
const mockAgentDefinition = {
name: 'codebase-investigator',
description: 'Agent 1',
instructions: 'Inst 1',
};
const AgentRegistryMock = (
(await vi.importMock('../agents/registry.js')) as {
AgentRegistry: Mock;
}
).AgentRegistry;
AgentRegistryMock.prototype.getAllDefinitions.mockReturnValue([
mockAgentDefinition,
]);
const SubAgentToolMock = (
(await vi.importMock('../agents/subagent-tool.js')) as {
SubagentTool: Mock;
}
).SubagentTool;
await config.initialize();
expect(SubAgentToolMock).toHaveBeenCalled();
});
it('should not register subagents as tools when agents are disabled', async () => {
const params: ConfigParameters = {
...baseParams,
+22 -20
View File
@@ -383,7 +383,9 @@ export interface ConfigParameters {
question?: string;
coreTools?: string[];
/** @deprecated Use Policy Engine instead */
allowedTools?: string[];
/** @deprecated Use Policy Engine instead */
excludeTools?: string[];
toolDiscoveryCommand?: string;
toolCallCommand?: string;
@@ -516,7 +518,9 @@ export class Config {
private readonly question: string | undefined;
private readonly coreTools: string[] | undefined;
/** @deprecated Use Policy Engine instead */
private readonly allowedTools: string[] | undefined;
/** @deprecated Use Policy Engine instead */
private readonly excludeTools: string[] | undefined;
private readonly toolDiscoveryCommand: string | undefined;
private readonly toolCallCommand: string | undefined;
@@ -819,7 +823,7 @@ export class Config {
(params.shellToolInactivityTimeout ?? 300) * 1000; // 5 minutes
this.extensionManagement = params.extensionManagement ?? true;
this.enableExtensionReloading = params.enableExtensionReloading ?? false;
this.storage = new Storage(this.targetDir);
this.storage = new Storage(this.targetDir, this.sessionId);
this.fakeResponses = params.fakeResponses;
this.recordResponses = params.recordResponses;
@@ -1339,7 +1343,8 @@ export class Config {
!!sandboxConfig &&
sandboxConfig.command === 'sandbox-exec' &&
!!seatbeltProfile &&
seatbeltProfile.startsWith('restrictive-')
(seatbeltProfile.startsWith('restrictive-') ||
seatbeltProfile.startsWith('strict-'))
);
}
@@ -1487,11 +1492,12 @@ export class Config {
/**
* All the excluded tools from static configuration, loaded extensions, or
* other sources.
* other sources (like the Policy Engine).
*
* May change over time.
*/
getExcludeTools(): Set<string> | undefined {
// Right now this is present for backward compatibility with settings.json exclude
const excludeToolsSet = new Set([...(this.excludeTools ?? [])]);
for (const extension of this.getExtensionLoader().getExtensions()) {
if (!extension.isActive) {
@@ -1501,6 +1507,12 @@ export class Config {
excludeToolsSet.add(tool);
}
}
const policyExclusions = this.policyEngine.getExcludedTools();
for (const tool of policyExclusions) {
excludeToolsSet.add(tool);
}
return excludeToolsSet;
}
@@ -2458,26 +2470,16 @@ export class Config {
agentsOverrides['codebase_investigator']?.enabled !== false ||
agentsOverrides['cli_help']?.enabled !== false
) {
const allowedTools = this.getAllowedTools();
const definitions = this.agentRegistry.getAllDefinitions();
for (const definition of definitions) {
const isAllowed =
!allowedTools || allowedTools.includes(definition.name);
if (isAllowed) {
try {
const tool = new SubagentTool(
definition,
this,
this.getMessageBus(),
);
registry.registerTool(tool);
} catch (e: unknown) {
debugLogger.warn(
`Failed to register tool for agent ${definition.name}: ${getErrorMessage(e)}`,
);
}
try {
const tool = new SubagentTool(definition, this, this.getMessageBus());
registry.registerTool(tool);
} catch (e: unknown) {
debugLogger.warn(
`Failed to register tool for agent ${definition.name}: ${getErrorMessage(e)}`,
);
}
}
}
+12 -53
View File
@@ -4,12 +4,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { beforeEach, describe, it, expect, vi, afterEach } from 'vitest';
import { beforeEach, describe, it, expect, vi } from 'vitest';
vi.unmock('./storage.js');
vi.unmock('./projectRegistry.js');
vi.unmock('./storageMigration.js');
import * as os from 'node:os';
import * as path from 'node:path';
@@ -154,62 +153,22 @@ describe('Storage additional helpers', () => {
expect(Storage.getGlobalBinDir()).toBe(expected);
});
it('getProjectTempPlansDir returns ~/.gemini/tmp/<identifier>/plans', async () => {
it('getProjectTempPlansDir returns ~/.gemini/tmp/<identifier>/plans when no sessionId is provided', async () => {
await storage.initialize();
const tempDir = storage.getProjectTempDir();
const expected = path.join(tempDir, 'plans');
expect(storage.getProjectTempPlansDir()).toBe(expected);
});
});
describe('Storage - System Paths', () => {
const originalEnv = process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH'];
afterEach(() => {
if (originalEnv !== undefined) {
process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH'] = originalEnv;
} else {
delete process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH'];
}
});
it('getSystemSettingsPath returns correct path based on platform (default)', () => {
delete process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH'];
const platform = os.platform();
const result = Storage.getSystemSettingsPath();
if (platform === 'darwin') {
expect(result).toBe(
'/Library/Application Support/GeminiCli/settings.json',
);
} else if (platform === 'win32') {
expect(result).toBe('C:\\ProgramData\\gemini-cli\\settings.json');
} else {
expect(result).toBe('/etc/gemini-cli/settings.json');
}
});
it('getSystemSettingsPath follows GEMINI_CLI_SYSTEM_SETTINGS_PATH if set', () => {
const customPath = '/custom/path/settings.json';
process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH'] = customPath;
expect(Storage.getSystemSettingsPath()).toBe(customPath);
});
it('getSystemPoliciesDir returns correct path based on platform and ignores env var', () => {
process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH'] =
'/custom/path/settings.json';
const platform = os.platform();
const result = Storage.getSystemPoliciesDir();
expect(result).not.toContain('/custom/path');
if (platform === 'darwin') {
expect(result).toBe('/Library/Application Support/GeminiCli/policies');
} else if (platform === 'win32') {
expect(result).toBe('C:\\ProgramData\\gemini-cli\\policies');
} else {
expect(result).toBe('/etc/gemini-cli/policies');
}
it('getProjectTempPlansDir returns ~/.gemini/tmp/<identifier>/<sessionId>/plans when sessionId is provided', async () => {
const sessionId = 'test-session-id';
const storageWithSession = new Storage(projectRoot, sessionId);
ProjectRegistry.prototype.getShortId = vi
.fn()
.mockReturnValue(PROJECT_SLUG);
await storageWithSession.initialize();
const tempDir = storageWithSession.getProjectTempDir();
const expected = path.join(tempDir, sessionId, 'plans');
expect(storageWithSession.getProjectTempPlansDir()).toBe(expected);
});
});
+21 -13
View File
@@ -20,11 +20,13 @@ const AGENTS_DIR_NAME = '.agents';
export class Storage {
private readonly targetDir: string;
private readonly sessionId: string | undefined;
private projectIdentifier: string | undefined;
private initPromise: Promise<void> | undefined;
constructor(targetDir: string) {
constructor(targetDir: string, sessionId?: string) {
this.targetDir = targetDir;
this.sessionId = sessionId;
}
static getGlobalGeminiDir(): string {
@@ -91,25 +93,21 @@ export class Storage {
);
}
private static getSystemConfigDir(): string {
if (os.platform() === 'darwin') {
return '/Library/Application Support/GeminiCli';
} else if (os.platform() === 'win32') {
return 'C:\\ProgramData\\gemini-cli';
} else {
return '/etc/gemini-cli';
}
}
static getSystemSettingsPath(): string {
if (process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH']) {
return process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH'];
}
return path.join(Storage.getSystemConfigDir(), 'settings.json');
if (os.platform() === 'darwin') {
return '/Library/Application Support/GeminiCli/settings.json';
} else if (os.platform() === 'win32') {
return 'C:\\ProgramData\\gemini-cli\\settings.json';
} else {
return '/etc/gemini-cli/settings.json';
}
}
static getSystemPoliciesDir(): string {
return path.join(Storage.getSystemConfigDir(), 'policies');
return path.join(path.dirname(Storage.getSystemSettingsPath()), 'policies');
}
static getGlobalTempDir(): string {
@@ -242,9 +240,19 @@ export class Storage {
}
getProjectTempPlansDir(): string {
if (this.sessionId) {
return path.join(this.getProjectTempDir(), this.sessionId, 'plans');
}
return path.join(this.getProjectTempDir(), 'plans');
}
getProjectTempTasksDir(): string {
if (this.sessionId) {
return path.join(this.getProjectTempDir(), this.sessionId, 'tasks');
}
return path.join(this.getProjectTempDir(), 'tasks');
}
getExtensionsDir(): string {
return path.join(this.getGeminiDir(), 'extensions');
}
@@ -42,8 +42,8 @@ You are operating in **Plan Mode** - a structured planning workflow for designin
## Available Tools
The following read-only tools are available in Plan Mode:
- \`glob\`
- \`grep_search\`
<tool>\`glob\`</tool>
<tool>\`grep_search\`</tool>
- \`write_file\` - Save plans to the plans directory (see Plan Storage below)
- \`replace\` - Update plans in the plans directory
@@ -173,8 +173,8 @@ You are operating in **Plan Mode** - a structured planning workflow for designin
## Available Tools
The following read-only tools are available in Plan Mode:
- \`glob\`
- \`grep_search\`
<tool>\`glob\`</tool>
<tool>\`grep_search\`</tool>
- \`write_file\` - Save plans to the plans directory (see Plan Storage below)
- \`replace\` - Update plans in the plans directory
@@ -421,8 +421,8 @@ You are operating in **Plan Mode** - a structured planning workflow for designin
## Available Tools
The following read-only tools are available in Plan Mode:
- \`glob\`
- \`grep_search\`
<tool>\`glob\`</tool>
<tool>\`grep_search\`</tool>
- \`write_file\` - Save plans to the plans directory (see Plan Storage below)
- \`replace\` - Update plans in the plans directory
@@ -580,7 +580,7 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype with rich aesthetics. Users judge applications by their visual impact; ensure they feel modern, "alive," and polished through consistent spacing, interactive feedback, and platform-appropriate design.
1. **Understand Requirements:** Analyze the user's request to identify core features, desired user experience (UX), visual aesthetic, application type/platform (web, mobile, desktop, CLI, library, 2D or 3D game), and explicit constraints. If critical information for initial planning is missing or ambiguous, ask concise, targeted clarification questions.
2. **Propose Plan:** Formulate an internal development plan. Present a clear, concise, high-level summary to the user. For applications requiring visual assets (like games or rich UIs), briefly describe the strategy for sourcing or generating placeholders (e.g., simple geometric shapes, procedurally generated patterns) to ensure a visually complete initial prototype.
2. **Propose Plan:** Formulate an internal development plan. Present a clear, concise, high-level summary to the user and obtain their approval before proceeding. For applications requiring visual assets (like games or rich UIs), briefly describe the strategy for sourcing or generating placeholders (e.g., simple geometric shapes, procedurally generated patterns).
- **Styling:** **Prefer Vanilla CSS** for maximum flexibility. **Avoid TailwindCSS** unless explicitly requested; if requested, confirm the specific version (e.g., v3 or v4).
- **Default Tech Stack:**
- **Web:** React (TypeScript) or Angular with Vanilla CSS.
@@ -588,10 +588,9 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
- **Mobile:** Compose Multiplatform or Flutter.
- **Games:** HTML/CSS/JS (Three.js for 3D).
- **CLIs:** Python or Go.
3. **User Approval:** Obtain user approval for the proposed plan.
4. **Implementation:** Autonomously implement each feature per the approved plan. When starting, scaffold the application using \`run_shell_command\` for commands like 'npm init', 'npx create-react-app'. For visual assets, utilize **platform-native primitives** (e.g., stylized shapes, gradients, icons) to ensure a complete, coherent experience. Never link to external services or assume local paths for assets that have not been created.
5. **Verify:** Review work against the original request. Fix bugs and deviations. Ensure styling and interactions produce a high-quality, functional, and beautiful prototype. **Build the application and ensure there are no compile errors.**
6. **Solicit Feedback:** Provide instructions on how to start the application and request user feedback on the prototype.
3. **Implementation:** Autonomously implement each feature per the approved plan. When starting, scaffold the application using \`run_shell_command\` for commands like 'npm init', 'npx create-react-app'. For visual assets, utilize **platform-native primitives** (e.g., stylized shapes, gradients, icons) to ensure a complete, coherent experience. Never link to external services or assume local paths for assets that have not been created.
4. **Verify:** Review work against the original request. Fix bugs and deviations. Ensure styling and interactions produce a high-quality, functional, and beautiful prototype. **Build the application and ensure there are no compile errors.**
5. **Solicit Feedback:** Provide instructions on how to start the application and request user feedback on the prototype.
# Operational Guidelines
@@ -695,7 +694,7 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype with rich aesthetics. Users judge applications by their visual impact; ensure they feel modern, "alive," and polished through consistent spacing, interactive feedback, and platform-appropriate design.
1. **Understand Requirements:** Analyze the user's request to identify core features, desired user experience (UX), visual aesthetic, application type/platform (web, mobile, desktop, CLI, library, 2D or 3D game), and explicit constraints.
2. **Propose Plan:** Formulate an internal development plan. Present a clear, concise, high-level summary to the user. For applications requiring visual assets, describe the strategy for sourcing or generating placeholders.
2. **Plan:** Formulate an internal development plan. For applications requiring visual assets, describe the strategy for sourcing or generating placeholders.
- **Styling:** **Prefer Vanilla CSS** for maximum flexibility. **Avoid TailwindCSS** unless explicitly requested.
- **Default Tech Stack:**
- **Web:** React (TypeScript) or Angular with Vanilla CSS.
@@ -703,7 +702,7 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
- **Mobile:** Compose Multiplatform or Flutter.
- **Games:** HTML/CSS/JS (Three.js for 3D).
- **CLIs:** Python or Go.
3. Implementation: Autonomously implement each feature per the approved plan. When starting, scaffold the application using \`run_shell_command\`. For visual assets, utilize **platform-native primitives** (e.g., stylized shapes, gradients, icons). Never link to external services or assume local paths for assets that have not been created.
3. **Implementation:** Autonomously implement each feature per the approved plan. When starting, scaffold the application using \`run_shell_command\`. For visual assets, utilize **platform-native primitives** (e.g., stylized shapes, gradients, icons). Never link to external services or assume local paths for assets that have not been created.
4. **Verify:** Review work against the original request. Fix bugs and deviations. **Build the application and ensure there are no compile errors.**
# Operational Guidelines
@@ -791,7 +790,7 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype with rich aesthetics. Users judge applications by their visual impact; ensure they feel modern, "alive," and polished through consistent spacing, interactive feedback, and platform-appropriate design.
1. **Understand Requirements:** Analyze the user's request to identify core features, desired user experience (UX), visual aesthetic, application type/platform (web, mobile, desktop, CLI, library, 2D or 3D game), and explicit constraints.
2. **Propose Plan:** Formulate an internal development plan. Present a clear, concise, high-level summary to the user. For applications requiring visual assets, describe the strategy for sourcing or generating placeholders.
2. **Plan:** Formulate an internal development plan. For applications requiring visual assets, describe the strategy for sourcing or generating placeholders.
- **Styling:** **Prefer Vanilla CSS** for maximum flexibility. **Avoid TailwindCSS** unless explicitly requested.
- **Default Tech Stack:**
- **Web:** React (TypeScript) or Angular with Vanilla CSS.
@@ -799,7 +798,7 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
- **Mobile:** Compose Multiplatform or Flutter.
- **Games:** HTML/CSS/JS (Three.js for 3D).
- **CLIs:** Python or Go.
3. Implementation: Autonomously implement each feature per the approved plan. When starting, scaffold the application using \`run_shell_command\`. For visual assets, utilize **platform-native primitives** (e.g., stylized shapes, gradients, icons). Never link to external services or assume local paths for assets that have not been created.
3. **Implementation:** Autonomously implement each feature per the approved plan. When starting, scaffold the application using \`run_shell_command\`. For visual assets, utilize **platform-native primitives** (e.g., stylized shapes, gradients, icons). Never link to external services or assume local paths for assets that have not been created.
4. **Verify:** Review work against the original request. Fix bugs and deviations. **Build the application and ensure there are no compile errors.**
# Operational Guidelines
@@ -1385,7 +1384,7 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype with rich aesthetics. Users judge applications by their visual impact; ensure they feel modern, "alive," and polished through consistent spacing, interactive feedback, and platform-appropriate design.
1. **Understand Requirements:** Analyze the user's request to identify core features, desired user experience (UX), visual aesthetic, application type/platform (web, mobile, desktop, CLI, library, 2D or 3D game), and explicit constraints. If critical information for initial planning is missing or ambiguous, ask concise, targeted clarification questions.
2. **Propose Plan:** Formulate an internal development plan. Present a clear, concise, high-level summary to the user. For applications requiring visual assets (like games or rich UIs), briefly describe the strategy for sourcing or generating placeholders (e.g., simple geometric shapes, procedurally generated patterns) to ensure a visually complete initial prototype.
2. **Propose Plan:** Formulate an internal development plan. Present a clear, concise, high-level summary to the user and obtain their approval before proceeding. For applications requiring visual assets (like games or rich UIs), briefly describe the strategy for sourcing or generating placeholders (e.g., simple geometric shapes, procedurally generated patterns).
- **Styling:** **Prefer Vanilla CSS** for maximum flexibility. **Avoid TailwindCSS** unless explicitly requested; if requested, confirm the specific version (e.g., v3 or v4).
- **Default Tech Stack:**
- **Web:** React (TypeScript) or Angular with Vanilla CSS.
@@ -1393,10 +1392,9 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
- **Mobile:** Compose Multiplatform or Flutter.
- **Games:** HTML/CSS/JS (Three.js for 3D).
- **CLIs:** Python or Go.
3. **User Approval:** Obtain user approval for the proposed plan.
4. **Implementation:** Autonomously implement each feature per the approved plan. When starting, scaffold the application using \`run_shell_command\` for commands like 'npm init', 'npx create-react-app'. For visual assets, utilize **platform-native primitives** (e.g., stylized shapes, gradients, icons) to ensure a complete, coherent experience. Never link to external services or assume local paths for assets that have not been created.
5. **Verify:** Review work against the original request. Fix bugs and deviations. Ensure styling and interactions produce a high-quality, functional, and beautiful prototype. **Build the application and ensure there are no compile errors.**
6. **Solicit Feedback:** Provide instructions on how to start the application and request user feedback on the prototype.
3. **Implementation:** Autonomously implement each feature per the approved plan. When starting, scaffold the application using \`run_shell_command\` for commands like 'npm init', 'npx create-react-app'. For visual assets, utilize **platform-native primitives** (e.g., stylized shapes, gradients, icons) to ensure a complete, coherent experience. Never link to external services or assume local paths for assets that have not been created.
4. **Verify:** Review work against the original request. Fix bugs and deviations. Ensure styling and interactions produce a high-quality, functional, and beautiful prototype. **Build the application and ensure there are no compile errors.**
5. **Solicit Feedback:** Provide instructions on how to start the application and request user feedback on the prototype.
# Operational Guidelines
@@ -1499,7 +1497,7 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype with rich aesthetics. Users judge applications by their visual impact; ensure they feel modern, "alive," and polished through consistent spacing, interactive feedback, and platform-appropriate design.
1. **Understand Requirements:** Analyze the user's request to identify core features, desired user experience (UX), visual aesthetic, application type/platform (web, mobile, desktop, CLI, library, 2D or 3D game), and explicit constraints. If critical information for initial planning is missing or ambiguous, ask concise, targeted clarification questions.
2. **Propose Plan:** Formulate an internal development plan. Present a clear, concise, high-level summary to the user. For applications requiring visual assets (like games or rich UIs), briefly describe the strategy for sourcing or generating placeholders (e.g., simple geometric shapes, procedurally generated patterns) to ensure a visually complete initial prototype.
2. **Propose Plan:** Formulate an internal development plan. Present a clear, concise, high-level summary to the user and obtain their approval before proceeding. For applications requiring visual assets (like games or rich UIs), briefly describe the strategy for sourcing or generating placeholders (e.g., simple geometric shapes, procedurally generated patterns).
- **Styling:** **Prefer Vanilla CSS** for maximum flexibility. **Avoid TailwindCSS** unless explicitly requested; if requested, confirm the specific version (e.g., v3 or v4).
- **Default Tech Stack:**
- **Web:** React (TypeScript) or Angular with Vanilla CSS.
@@ -1507,10 +1505,9 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
- **Mobile:** Compose Multiplatform or Flutter.
- **Games:** HTML/CSS/JS (Three.js for 3D).
- **CLIs:** Python or Go.
3. **User Approval:** Obtain user approval for the proposed plan.
4. **Implementation:** Autonomously implement each feature per the approved plan. When starting, scaffold the application using \`run_shell_command\` for commands like 'npm init', 'npx create-react-app'. For visual assets, utilize **platform-native primitives** (e.g., stylized shapes, gradients, icons) to ensure a complete, coherent experience. Never link to external services or assume local paths for assets that have not been created.
5. **Verify:** Review work against the original request. Fix bugs and deviations. Ensure styling and interactions produce a high-quality, functional, and beautiful prototype. **Build the application and ensure there are no compile errors.**
6. **Solicit Feedback:** Provide instructions on how to start the application and request user feedback on the prototype.
3. **Implementation:** Autonomously implement each feature per the approved plan. When starting, scaffold the application using \`run_shell_command\` for commands like 'npm init', 'npx create-react-app'. For visual assets, utilize **platform-native primitives** (e.g., stylized shapes, gradients, icons) to ensure a complete, coherent experience. Never link to external services or assume local paths for assets that have not been created.
4. **Verify:** Review work against the original request. Fix bugs and deviations. Ensure styling and interactions produce a high-quality, functional, and beautiful prototype. **Build the application and ensure there are no compile errors.**
5. **Solicit Feedback:** Provide instructions on how to start the application and request user feedback on the prototype.
# Operational Guidelines
@@ -1617,7 +1614,7 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype with rich aesthetics. Users judge applications by their visual impact; ensure they feel modern, "alive," and polished through consistent spacing, interactive feedback, and platform-appropriate design.
1. **Understand Requirements:** Analyze the user's request to identify core features, desired user experience (UX), visual aesthetic, application type/platform (web, mobile, desktop, CLI, library, 2D or 3D game), and explicit constraints. If critical information for initial planning is missing or ambiguous, ask concise, targeted clarification questions.
2. **Propose Plan:** Formulate an internal development plan. Present a clear, concise, high-level summary to the user. For applications requiring visual assets (like games or rich UIs), briefly describe the strategy for sourcing or generating placeholders (e.g., simple geometric shapes, procedurally generated patterns) to ensure a visually complete initial prototype.
2. **Propose Plan:** Formulate an internal development plan. Present a clear, concise, high-level summary to the user and obtain their approval before proceeding. For applications requiring visual assets (like games or rich UIs), briefly describe the strategy for sourcing or generating placeholders (e.g., simple geometric shapes, procedurally generated patterns).
- **Styling:** **Prefer Vanilla CSS** for maximum flexibility. **Avoid TailwindCSS** unless explicitly requested; if requested, confirm the specific version (e.g., v3 or v4).
- **Default Tech Stack:**
- **Web:** React (TypeScript) or Angular with Vanilla CSS.
@@ -1625,10 +1622,9 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
- **Mobile:** Compose Multiplatform or Flutter.
- **Games:** HTML/CSS/JS (Three.js for 3D).
- **CLIs:** Python or Go.
3. **User Approval:** Obtain user approval for the proposed plan.
4. **Implementation:** Autonomously implement each feature per the approved plan. When starting, scaffold the application using \`run_shell_command\` for commands like 'npm init', 'npx create-react-app'. For visual assets, utilize **platform-native primitives** (e.g., stylized shapes, gradients, icons) to ensure a complete, coherent experience. Never link to external services or assume local paths for assets that have not been created.
5. **Verify:** Review work against the original request. Fix bugs and deviations. Ensure styling and interactions produce a high-quality, functional, and beautiful prototype. **Build the application and ensure there are no compile errors.**
6. **Solicit Feedback:** Provide instructions on how to start the application and request user feedback on the prototype.
3. **Implementation:** Autonomously implement each feature per the approved plan. When starting, scaffold the application using \`run_shell_command\` for commands like 'npm init', 'npx create-react-app'. For visual assets, utilize **platform-native primitives** (e.g., stylized shapes, gradients, icons) to ensure a complete, coherent experience. Never link to external services or assume local paths for assets that have not been created.
4. **Verify:** Review work against the original request. Fix bugs and deviations. Ensure styling and interactions produce a high-quality, functional, and beautiful prototype. **Build the application and ensure there are no compile errors.**
5. **Solicit Feedback:** Provide instructions on how to start the application and request user feedback on the prototype.
# Operational Guidelines
@@ -1735,7 +1731,7 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype with rich aesthetics. Users judge applications by their visual impact; ensure they feel modern, "alive," and polished through consistent spacing, interactive feedback, and platform-appropriate design.
1. **Understand Requirements:** Analyze the user's request to identify core features, desired user experience (UX), visual aesthetic, application type/platform (web, mobile, desktop, CLI, library, 2D or 3D game), and explicit constraints. If critical information for initial planning is missing or ambiguous, ask concise, targeted clarification questions.
2. **Propose Plan:** Formulate an internal development plan. Present a clear, concise, high-level summary to the user. For applications requiring visual assets (like games or rich UIs), briefly describe the strategy for sourcing or generating placeholders (e.g., simple geometric shapes, procedurally generated patterns) to ensure a visually complete initial prototype.
2. **Propose Plan:** Formulate an internal development plan. Present a clear, concise, high-level summary to the user and obtain their approval before proceeding. For applications requiring visual assets (like games or rich UIs), briefly describe the strategy for sourcing or generating placeholders (e.g., simple geometric shapes, procedurally generated patterns).
- **Styling:** **Prefer Vanilla CSS** for maximum flexibility. **Avoid TailwindCSS** unless explicitly requested; if requested, confirm the specific version (e.g., v3 or v4).
- **Default Tech Stack:**
- **Web:** React (TypeScript) or Angular with Vanilla CSS.
@@ -1743,10 +1739,9 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
- **Mobile:** Compose Multiplatform or Flutter.
- **Games:** HTML/CSS/JS (Three.js for 3D).
- **CLIs:** Python or Go.
3. **User Approval:** Obtain user approval for the proposed plan.
4. **Implementation:** Autonomously implement each feature per the approved plan. When starting, scaffold the application using \`run_shell_command\` for commands like 'npm init', 'npx create-react-app'. For visual assets, utilize **platform-native primitives** (e.g., stylized shapes, gradients, icons) to ensure a complete, coherent experience. Never link to external services or assume local paths for assets that have not been created.
5. **Verify:** Review work against the original request. Fix bugs and deviations. Ensure styling and interactions produce a high-quality, functional, and beautiful prototype. **Build the application and ensure there are no compile errors.**
6. **Solicit Feedback:** Provide instructions on how to start the application and request user feedback on the prototype.
3. **Implementation:** Autonomously implement each feature per the approved plan. When starting, scaffold the application using \`run_shell_command\` for commands like 'npm init', 'npx create-react-app'. For visual assets, utilize **platform-native primitives** (e.g., stylized shapes, gradients, icons) to ensure a complete, coherent experience. Never link to external services or assume local paths for assets that have not been created.
4. **Verify:** Review work against the original request. Fix bugs and deviations. Ensure styling and interactions produce a high-quality, functional, and beautiful prototype. **Build the application and ensure there are no compile errors.**
5. **Solicit Feedback:** Provide instructions on how to start the application and request user feedback on the prototype.
# Operational Guidelines
@@ -1835,7 +1830,7 @@ For example:
## Development Lifecycle
Operate using a **Research -> Strategy -> Execution** lifecycle. For the Execution phase, resolve each sub-task through an iterative **Plan -> Act -> Validate** cycle.
1. **Research:** Systematically map the codebase and validate assumptions. Use search tools extensively to understand file structures, existing code patterns, and conventions. Use \`read_file\` to validate all assumptions. **Prioritize empirical reproduction of reported issues to confirm the failure state.** For complex tasks, consider using the \`enter_plan_mode\` tool to enter a dedicated planning phase before starting implementation.
1. **Research:** Systematically map the codebase and validate assumptions. Use search tools extensively to understand file structures, existing code patterns, and conventions. Use \`read_file\` to validate all assumptions. **Prioritize empirical reproduction of reported issues to confirm the failure state.** If the request is ambiguous, broad in scope, or involves creating a new feature/application, you MUST use the \`enter_plan_mode\` tool to design your approach before making changes. Do NOT use Plan Mode for straightforward bug fixes, answering questions, or simple inquiries.
2. **Strategy:** Formulate a grounded plan based on your research. Share a concise summary of your strategy.
3. **Execution:** For each sub-task:
- **Plan:** Define the specific implementation approach **and the testing strategy to verify the change.**
@@ -1848,19 +1843,17 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype with rich aesthetics. Users judge applications by their visual impact; ensure they feel modern, "alive," and polished through consistent spacing, interactive feedback, and platform-appropriate design.
1. **Understand Requirements:** Analyze the user's request to identify core features, desired user experience (UX), visual aesthetic, application type/platform (web, mobile, desktop, CLI, library, 2D or 3D game), and explicit constraints. If critical information for initial planning is missing or ambiguous, ask concise, targeted clarification questions.
2. **Propose Plan:** Formulate an internal development plan. Present a clear, concise, high-level summary to the user. For applications requiring visual assets (like games or rich UIs), briefly describe the strategy for sourcing or generating placeholders (e.g., simple geometric shapes, procedurally generated patterns) to ensure a visually complete initial prototype. For complex tasks, consider using the \`enter_plan_mode\` tool to enter a dedicated planning phase before starting implementation.
- **Styling:** **Prefer Vanilla CSS** for maximum flexibility. **Avoid TailwindCSS** unless explicitly requested; if requested, confirm the specific version (e.g., v3 or v4).
- **Default Tech Stack:**
- **Web:** React (TypeScript) or Angular with Vanilla CSS.
- **APIs:** Node.js (Express) or Python (FastAPI).
- **Mobile:** Compose Multiplatform or Flutter.
- **Games:** HTML/CSS/JS (Three.js for 3D).
- **CLIs:** Python or Go.
3. **User Approval:** Obtain user approval for the proposed plan.
4. **Implementation:** Autonomously implement each feature per the approved plan. When starting, scaffold the application using \`run_shell_command\` for commands like 'npm init', 'npx create-react-app'. For visual assets, utilize **platform-native primitives** (e.g., stylized shapes, gradients, icons) to ensure a complete, coherent experience. Never link to external services or assume local paths for assets that have not been created.
5. **Verify:** Review work against the original request. Fix bugs and deviations. Ensure styling and interactions produce a high-quality, functional, and beautiful prototype. **Build the application and ensure there are no compile errors.**
6. **Solicit Feedback:** Provide instructions on how to start the application and request user feedback on the prototype.
1. **Mandatory Planning:** You MUST use the \`enter_plan_mode\` tool to draft a comprehensive design document and obtain user approval before writing any code.
2. **Design Constraints:** When drafting your plan, adhere to these defaults unless explicitly overridden by the user:
- **Goal:** Autonomously design a visually appealing, substantially complete, and functional prototype with rich aesthetics. Users judge applications by their visual impact; ensure they feel modern, "alive," and polished through consistent spacing, typography, and interactive feedback.
- **Visuals:** Describe your strategy for sourcing or generating placeholders (e.g., stylized CSS shapes, gradients, procedurally generated patterns) to ensure a visually complete prototype. Never plan for assets that cannot be locally generated.
- **Styling:** **Prefer Vanilla CSS** for maximum flexibility. **Avoid TailwindCSS** unless explicitly requested.
- **Web:** React (TypeScript) or Angular with Vanilla CSS.
- **APIs:** Node.js (Express) or Python (FastAPI).
- **Mobile:** Compose Multiplatform or Flutter.
- **Games:** HTML/CSS/JS (Three.js for 3D).
- **CLIs:** Python or Go.
3. **Implementation:** Once the plan is approved, follow the standard **Execution** cycle to build the application, utilizing platform-native primitives to realize the rich aesthetic you planned.
# Operational Guidelines
@@ -1963,7 +1956,7 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype with rich aesthetics. Users judge applications by their visual impact; ensure they feel modern, "alive," and polished through consistent spacing, interactive feedback, and platform-appropriate design.
1. **Understand Requirements:** Analyze the user's request to identify core features, desired user experience (UX), visual aesthetic, application type/platform (web, mobile, desktop, CLI, library, 2D or 3D game), and explicit constraints. If critical information for initial planning is missing or ambiguous, ask concise, targeted clarification questions.
2. **Propose Plan:** Formulate an internal development plan. Present a clear, concise, high-level summary to the user. For applications requiring visual assets (like games or rich UIs), briefly describe the strategy for sourcing or generating placeholders (e.g., simple geometric shapes, procedurally generated patterns) to ensure a visually complete initial prototype.
2. **Propose Plan:** Formulate an internal development plan. Present a clear, concise, high-level summary to the user and obtain their approval before proceeding. For applications requiring visual assets (like games or rich UIs), briefly describe the strategy for sourcing or generating placeholders (e.g., simple geometric shapes, procedurally generated patterns).
- **Styling:** **Prefer Vanilla CSS** for maximum flexibility. **Avoid TailwindCSS** unless explicitly requested; if requested, confirm the specific version (e.g., v3 or v4).
- **Default Tech Stack:**
- **Web:** React (TypeScript) or Angular with Vanilla CSS.
@@ -1971,10 +1964,9 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
- **Mobile:** Compose Multiplatform or Flutter.
- **Games:** HTML/CSS/JS (Three.js for 3D).
- **CLIs:** Python or Go.
3. **User Approval:** Obtain user approval for the proposed plan.
4. **Implementation:** Autonomously implement each feature per the approved plan. When starting, scaffold the application using \`run_shell_command\` for commands like 'npm init', 'npx create-react-app'. For visual assets, utilize **platform-native primitives** (e.g., stylized shapes, gradients, icons) to ensure a complete, coherent experience. Never link to external services or assume local paths for assets that have not been created.
5. **Verify:** Review work against the original request. Fix bugs and deviations. Ensure styling and interactions produce a high-quality, functional, and beautiful prototype. **Build the application and ensure there are no compile errors.**
6. **Solicit Feedback:** Provide instructions on how to start the application and request user feedback on the prototype.
3. **Implementation:** Autonomously implement each feature per the approved plan. When starting, scaffold the application using \`run_shell_command\` for commands like 'npm init', 'npx create-react-app'. For visual assets, utilize **platform-native primitives** (e.g., stylized shapes, gradients, icons) to ensure a complete, coherent experience. Never link to external services or assume local paths for assets that have not been created.
4. **Verify:** Review work against the original request. Fix bugs and deviations. Ensure styling and interactions produce a high-quality, functional, and beautiful prototype. **Build the application and ensure there are no compile errors.**
5. **Solicit Feedback:** Provide instructions on how to start the application and request user feedback on the prototype.
# Operational Guidelines
@@ -2316,7 +2308,7 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype with rich aesthetics. Users judge applications by their visual impact; ensure they feel modern, "alive," and polished through consistent spacing, interactive feedback, and platform-appropriate design.
1. **Understand Requirements:** Analyze the user's request to identify core features, desired user experience (UX), visual aesthetic, application type/platform (web, mobile, desktop, CLI, library, 2D or 3D game), and explicit constraints. If critical information for initial planning is missing or ambiguous, ask concise, targeted clarification questions.
2. **Propose Plan:** Formulate an internal development plan. Present a clear, concise, high-level summary to the user. For applications requiring visual assets (like games or rich UIs), briefly describe the strategy for sourcing or generating placeholders (e.g., simple geometric shapes, procedurally generated patterns) to ensure a visually complete initial prototype.
2. **Propose Plan:** Formulate an internal development plan. Present a clear, concise, high-level summary to the user and obtain their approval before proceeding. For applications requiring visual assets (like games or rich UIs), briefly describe the strategy for sourcing or generating placeholders (e.g., simple geometric shapes, procedurally generated patterns).
- **Styling:** **Prefer Vanilla CSS** for maximum flexibility. **Avoid TailwindCSS** unless explicitly requested; if requested, confirm the specific version (e.g., v3 or v4).
- **Default Tech Stack:**
- **Web:** React (TypeScript) or Angular with Vanilla CSS.
@@ -2324,10 +2316,9 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
- **Mobile:** Compose Multiplatform or Flutter.
- **Games:** HTML/CSS/JS (Three.js for 3D).
- **CLIs:** Python or Go.
3. **User Approval:** Obtain user approval for the proposed plan.
4. **Implementation:** Autonomously implement each feature per the approved plan. When starting, scaffold the application using \`run_shell_command\` for commands like 'npm init', 'npx create-react-app'. For visual assets, utilize **platform-native primitives** (e.g., stylized shapes, gradients, icons) to ensure a complete, coherent experience. Never link to external services or assume local paths for assets that have not been created.
5. **Verify:** Review work against the original request. Fix bugs and deviations. Ensure styling and interactions produce a high-quality, functional, and beautiful prototype. **Build the application and ensure there are no compile errors.**
6. **Solicit Feedback:** Provide instructions on how to start the application and request user feedback on the prototype.
3. **Implementation:** Autonomously implement each feature per the approved plan. When starting, scaffold the application using \`run_shell_command\` for commands like 'npm init', 'npx create-react-app'. For visual assets, utilize **platform-native primitives** (e.g., stylized shapes, gradients, icons) to ensure a complete, coherent experience. Never link to external services or assume local paths for assets that have not been created.
4. **Verify:** Review work against the original request. Fix bugs and deviations. Ensure styling and interactions produce a high-quality, functional, and beautiful prototype. **Build the application and ensure there are no compile errors.**
5. **Solicit Feedback:** Provide instructions on how to start the application and request user feedback on the prototype.
# Operational Guidelines
@@ -2430,7 +2421,7 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype with rich aesthetics. Users judge applications by their visual impact; ensure they feel modern, "alive," and polished through consistent spacing, interactive feedback, and platform-appropriate design.
1. **Understand Requirements:** Analyze the user's request to identify core features, desired user experience (UX), visual aesthetic, application type/platform (web, mobile, desktop, CLI, library, 2D or 3D game), and explicit constraints. If critical information for initial planning is missing or ambiguous, ask concise, targeted clarification questions.
2. **Propose Plan:** Formulate an internal development plan. Present a clear, concise, high-level summary to the user. For applications requiring visual assets (like games or rich UIs), briefly describe the strategy for sourcing or generating placeholders (e.g., simple geometric shapes, procedurally generated patterns) to ensure a visually complete initial prototype.
2. **Propose Plan:** Formulate an internal development plan. Present a clear, concise, high-level summary to the user and obtain their approval before proceeding. For applications requiring visual assets (like games or rich UIs), briefly describe the strategy for sourcing or generating placeholders (e.g., simple geometric shapes, procedurally generated patterns).
- **Styling:** **Prefer Vanilla CSS** for maximum flexibility. **Avoid TailwindCSS** unless explicitly requested; if requested, confirm the specific version (e.g., v3 or v4).
- **Default Tech Stack:**
- **Web:** React (TypeScript) or Angular with Vanilla CSS.
@@ -2438,10 +2429,9 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
- **Mobile:** Compose Multiplatform or Flutter.
- **Games:** HTML/CSS/JS (Three.js for 3D).
- **CLIs:** Python or Go.
3. **User Approval:** Obtain user approval for the proposed plan.
4. **Implementation:** Autonomously implement each feature per the approved plan. When starting, scaffold the application using \`run_shell_command\` for commands like 'npm init', 'npx create-react-app'. For visual assets, utilize **platform-native primitives** (e.g., stylized shapes, gradients, icons) to ensure a complete, coherent experience. Never link to external services or assume local paths for assets that have not been created.
5. **Verify:** Review work against the original request. Fix bugs and deviations. Ensure styling and interactions produce a high-quality, functional, and beautiful prototype. **Build the application and ensure there are no compile errors.**
6. **Solicit Feedback:** Provide instructions on how to start the application and request user feedback on the prototype.
3. **Implementation:** Autonomously implement each feature per the approved plan. When starting, scaffold the application using \`run_shell_command\` for commands like 'npm init', 'npx create-react-app'. For visual assets, utilize **platform-native primitives** (e.g., stylized shapes, gradients, icons) to ensure a complete, coherent experience. Never link to external services or assume local paths for assets that have not been created.
4. **Verify:** Review work against the original request. Fix bugs and deviations. Ensure styling and interactions produce a high-quality, functional, and beautiful prototype. **Build the application and ensure there are no compile errors.**
5. **Solicit Feedback:** Provide instructions on how to start the application and request user feedback on the prototype.
# Operational Guidelines
@@ -2655,7 +2645,7 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype with rich aesthetics. Users judge applications by their visual impact; ensure they feel modern, "alive," and polished through consistent spacing, interactive feedback, and platform-appropriate design.
1. **Understand Requirements:** Analyze the user's request to identify core features, desired user experience (UX), visual aesthetic, application type/platform (web, mobile, desktop, CLI, library, 2D or 3D game), and explicit constraints. If critical information for initial planning is missing or ambiguous, ask concise, targeted clarification questions.
2. **Propose Plan:** Formulate an internal development plan. Present a clear, concise, high-level summary to the user. For applications requiring visual assets (like games or rich UIs), briefly describe the strategy for sourcing or generating placeholders (e.g., simple geometric shapes, procedurally generated patterns) to ensure a visually complete initial prototype.
2. **Propose Plan:** Formulate an internal development plan. Present a clear, concise, high-level summary to the user and obtain their approval before proceeding. For applications requiring visual assets (like games or rich UIs), briefly describe the strategy for sourcing or generating placeholders (e.g., simple geometric shapes, procedurally generated patterns).
- **Styling:** **Prefer Vanilla CSS** for maximum flexibility. **Avoid TailwindCSS** unless explicitly requested; if requested, confirm the specific version (e.g., v3 or v4).
- **Default Tech Stack:**
- **Web:** React (TypeScript) or Angular with Vanilla CSS.
@@ -2663,10 +2653,9 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
- **Mobile:** Compose Multiplatform or Flutter.
- **Games:** HTML/CSS/JS (Three.js for 3D).
- **CLIs:** Python or Go.
3. **User Approval:** Obtain user approval for the proposed plan.
4. **Implementation:** Autonomously implement each feature per the approved plan. When starting, scaffold the application using \`run_shell_command\` for commands like 'npm init', 'npx create-react-app'. For visual assets, utilize **platform-native primitives** (e.g., stylized shapes, gradients, icons) to ensure a complete, coherent experience. Never link to external services or assume local paths for assets that have not been created.
5. **Verify:** Review work against the original request. Fix bugs and deviations. Ensure styling and interactions produce a high-quality, functional, and beautiful prototype. **Build the application and ensure there are no compile errors.**
6. **Solicit Feedback:** Provide instructions on how to start the application and request user feedback on the prototype.
3. **Implementation:** Autonomously implement each feature per the approved plan. When starting, scaffold the application using \`run_shell_command\` for commands like 'npm init', 'npx create-react-app'. For visual assets, utilize **platform-native primitives** (e.g., stylized shapes, gradients, icons) to ensure a complete, coherent experience. Never link to external services or assume local paths for assets that have not been created.
4. **Verify:** Review work against the original request. Fix bugs and deviations. Ensure styling and interactions produce a high-quality, functional, and beautiful prototype. **Build the application and ensure there are no compile errors.**
5. **Solicit Feedback:** Provide instructions on how to start the application and request user feedback on the prototype.
# Operational Guidelines
@@ -2769,7 +2758,7 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
**Goal:** Autonomously implement and deliver a visually appealing, substantially complete, and functional prototype with rich aesthetics. Users judge applications by their visual impact; ensure they feel modern, "alive," and polished through consistent spacing, interactive feedback, and platform-appropriate design.
1. **Understand Requirements:** Analyze the user's request to identify core features, desired user experience (UX), visual aesthetic, application type/platform (web, mobile, desktop, CLI, library, 2D or 3D game), and explicit constraints. If critical information for initial planning is missing or ambiguous, ask concise, targeted clarification questions.
2. **Propose Plan:** Formulate an internal development plan. Present a clear, concise, high-level summary to the user. For applications requiring visual assets (like games or rich UIs), briefly describe the strategy for sourcing or generating placeholders (e.g., simple geometric shapes, procedurally generated patterns) to ensure a visually complete initial prototype.
2. **Propose Plan:** Formulate an internal development plan. Present a clear, concise, high-level summary to the user and obtain their approval before proceeding. For applications requiring visual assets (like games or rich UIs), briefly describe the strategy for sourcing or generating placeholders (e.g., simple geometric shapes, procedurally generated patterns).
- **Styling:** **Prefer Vanilla CSS** for maximum flexibility. **Avoid TailwindCSS** unless explicitly requested; if requested, confirm the specific version (e.g., v3 or v4).
- **Default Tech Stack:**
- **Web:** React (TypeScript) or Angular with Vanilla CSS.
@@ -2777,10 +2766,9 @@ Operate using a **Research -> Strategy -> Execution** lifecycle. For the Executi
- **Mobile:** Compose Multiplatform or Flutter.
- **Games:** HTML/CSS/JS (Three.js for 3D).
- **CLIs:** Python or Go.
3. **User Approval:** Obtain user approval for the proposed plan.
4. **Implementation:** Autonomously implement each feature per the approved plan. When starting, scaffold the application using \`run_shell_command\` for commands like 'npm init', 'npx create-react-app'. For visual assets, utilize **platform-native primitives** (e.g., stylized shapes, gradients, icons) to ensure a complete, coherent experience. Never link to external services or assume local paths for assets that have not been created.
5. **Verify:** Review work against the original request. Fix bugs and deviations. Ensure styling and interactions produce a high-quality, functional, and beautiful prototype. **Build the application and ensure there are no compile errors.**
6. **Solicit Feedback:** Provide instructions on how to start the application and request user feedback on the prototype.
3. **Implementation:** Autonomously implement each feature per the approved plan. When starting, scaffold the application using \`run_shell_command\` for commands like 'npm init', 'npx create-react-app'. For visual assets, utilize **platform-native primitives** (e.g., stylized shapes, gradients, icons) to ensure a complete, coherent experience. Never link to external services or assume local paths for assets that have not been created.
4. **Verify:** Review work against the original request. Fix bugs and deviations. Ensure styling and interactions produce a high-quality, functional, and beautiful prototype. **Build the application and ensure there are no compile errors.**
5. **Solicit Feedback:** Provide instructions on how to start the application and request user feedback on the prototype.
# Operational Guidelines
+1 -1
View File
@@ -612,7 +612,7 @@ describe('Core System Prompt (prompts.ts)', () => {
const prompt = getCoreSystemPrompt(mockConfig);
expect(prompt).toContain(
'For complex tasks, consider using the `enter_plan_mode` tool to enter a dedicated planning phase before starting implementation.',
'If the request is ambiguous, broad in scope, or involves creating a new feature/application, you MUST use the `enter_plan_mode` tool to design your approach before making changes. Do NOT use Plan Mode for straightforward bug fixes, answering questions, or simple inquiries.',
);
expect(prompt).toMatchSnapshot();
});
-51
View File
@@ -10,14 +10,9 @@ import nodePath from 'node:path';
import type { PolicySettings } from './types.js';
import { ApprovalMode, PolicyDecision, InProcessCheckerType } from './types.js';
import { isDirectorySecure } from '../utils/security.js';
vi.unmock('../config/storage.js');
vi.mock('../utils/security.js', () => ({
isDirectorySecure: vi.fn().mockResolvedValue({ secure: true }),
}));
afterEach(() => {
vi.clearAllMocks();
vi.restoreAllMocks();
@@ -35,53 +30,7 @@ describe('createPolicyEngineConfig', () => {
vi.spyOn(Storage, 'getSystemPoliciesDir').mockReturnValue(
'/non/existent/system/policies',
);
// Reset security check to default secure
vi.mocked(isDirectorySecure).mockResolvedValue({ secure: true });
});
it('should filter out insecure system policy directories', async () => {
const { Storage } = await import('../config/storage.js');
const systemPolicyDir = '/insecure/system/policies';
vi.spyOn(Storage, 'getSystemPoliciesDir').mockReturnValue(systemPolicyDir);
vi.mocked(isDirectorySecure).mockImplementation(async (path: string) => {
if (nodePath.resolve(path) === nodePath.resolve(systemPolicyDir)) {
return { secure: false, reason: 'Insecure directory' };
}
return { secure: true };
});
// We need to spy on loadPoliciesFromToml to verify which directories were passed
// But it is not exported from config.js, it is imported.
// We can spy on the module it comes from.
const tomlLoader = await import('./toml-loader.js');
const loadPoliciesSpy = vi.spyOn(tomlLoader, 'loadPoliciesFromToml');
loadPoliciesSpy.mockResolvedValue({
rules: [],
checkers: [],
errors: [],
});
const { createPolicyEngineConfig } = await import('./config.js');
const settings: PolicySettings = {};
await createPolicyEngineConfig(
settings,
ApprovalMode.DEFAULT,
'/tmp/mock/default/policies',
);
// Verify loadPoliciesFromToml was called
expect(loadPoliciesSpy).toHaveBeenCalled();
const calledDirs = loadPoliciesSpy.mock.calls[0][0];
// The system directory should NOT be in the list
expect(calledDirs).not.toContain(systemPolicyDir);
// But other directories (user, default) should be there
expect(calledDirs).toContain('/non/existent/user/policies');
expect(calledDirs).toContain('/tmp/mock/default/policies');
});
it('should return ASK_USER for write tools and ALLOW for read-only tools by default', async () => {
const actualFs =
await vi.importActual<typeof import('node:fs/promises')>(
+1 -31
View File
@@ -30,8 +30,6 @@ import { debugLogger } from '../utils/debugLogger.js';
import { SHELL_TOOL_NAMES } from '../utils/shell-utils.js';
import { SHELL_TOOL_NAME } from '../tools/tool-names.js';
import { isDirectorySecure } from '../utils/security.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export const DEFAULT_CORE_POLICIES_DIR = path.join(__dirname, 'policies');
@@ -115,47 +113,19 @@ export function formatPolicyError(error: PolicyFileError): string {
return message;
}
/**
* Filters out insecure policy directories (specifically the system policy directory).
* Emits warnings if insecure directories are found.
*/
async function filterSecurePolicyDirectories(
dirs: string[],
): Promise<string[]> {
const systemPoliciesDir = path.resolve(Storage.getSystemPoliciesDir());
const results = await Promise.all(
dirs.map(async (dir) => {
// Only check security for system policies
if (path.resolve(dir) === systemPoliciesDir) {
const { secure, reason } = await isDirectorySecure(dir);
if (!secure) {
const msg = `Security Warning: Skipping system policies from ${dir}: ${reason}`;
coreEvents.emitFeedback('warning', msg);
return null;
}
}
return dir;
}),
);
return results.filter((dir): dir is string => dir !== null);
}
export async function createPolicyEngineConfig(
settings: PolicySettings,
approvalMode: ApprovalMode,
defaultPoliciesDir?: string,
): Promise<PolicyEngineConfig> {
const policyDirs = getPolicyDirectories(defaultPoliciesDir);
const securePolicyDirs = await filterSecurePolicyDirectories(policyDirs);
// Load policies from TOML files
const {
rules: tomlRules,
checkers: tomlCheckers,
errors,
} = await loadPoliciesFromToml(securePolicyDirs, (dir) =>
} = await loadPoliciesFromToml(policyDirs, (dir) =>
getPolicyTier(dir, defaultPoliciesDir),
);
+1 -1
View File
@@ -53,4 +53,4 @@ toolName = ["write_file", "replace"]
decision = "allow"
priority = 70
modes = ["plan"]
argsPattern = "\"file_path\":\"[^\"]+/\\.gemini/tmp/[a-zA-Z0-9_-]+/plans/[a-zA-Z0-9_-]+\\.md\""
argsPattern = "\"file_path\":\"[^\"]+/\\.gemini/tmp/[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+/plans/[a-zA-Z0-9_-]+\\.md\""
@@ -2031,6 +2031,253 @@ describe('PolicyEngine', () => {
});
});
describe('getExcludedTools', () => {
interface TestCase {
name: string;
rules: PolicyRule[];
approvalMode?: ApprovalMode;
nonInteractive?: boolean;
expected: string[];
}
const testCases: TestCase[] = [
{
name: 'should return empty set when no rules provided',
rules: [],
expected: [],
},
{
name: 'should apply rules without explicit modes to all modes',
rules: [{ toolName: 'tool1', decision: PolicyDecision.DENY }],
expected: ['tool1'],
},
{
name: 'should NOT exclude tool if higher priority argsPattern rule exists',
rules: [
{
toolName: 'tool1',
decision: PolicyDecision.ALLOW,
argsPattern: /safe/,
priority: 100,
modes: [ApprovalMode.DEFAULT],
},
{
toolName: 'tool1',
decision: PolicyDecision.DENY,
priority: 10,
modes: [ApprovalMode.DEFAULT],
},
],
expected: [],
},
{
name: 'should include tools with DENY decision',
rules: [
{
toolName: 'tool1',
decision: PolicyDecision.DENY,
modes: [ApprovalMode.DEFAULT],
},
{
toolName: 'tool2',
decision: PolicyDecision.ALLOW,
modes: [ApprovalMode.DEFAULT],
},
],
expected: ['tool1'],
},
{
name: 'should respect priority and ignore lower priority rules (DENY wins)',
rules: [
{
toolName: 'tool1',
decision: PolicyDecision.DENY,
priority: 100,
modes: [ApprovalMode.DEFAULT],
},
{
toolName: 'tool1',
decision: PolicyDecision.ALLOW,
priority: 10,
modes: [ApprovalMode.DEFAULT],
},
],
expected: ['tool1'],
},
{
name: 'should respect priority and ignore lower priority rules (ALLOW wins)',
rules: [
{
toolName: 'tool1',
decision: PolicyDecision.ALLOW,
priority: 100,
modes: [ApprovalMode.DEFAULT],
},
{
toolName: 'tool1',
decision: PolicyDecision.DENY,
priority: 10,
modes: [ApprovalMode.DEFAULT],
},
],
expected: [],
},
{
name: 'should NOT include ASK_USER tools even in non-interactive mode',
rules: [
{
toolName: 'tool1',
decision: PolicyDecision.ASK_USER,
modes: [ApprovalMode.DEFAULT],
},
],
nonInteractive: true,
expected: [],
},
{
name: 'should ignore rules with argsPattern',
rules: [
{
toolName: 'tool1',
decision: PolicyDecision.DENY,
argsPattern: /something/,
modes: [ApprovalMode.DEFAULT],
},
],
expected: [],
},
{
name: 'should respect approval mode (PLAN mode)',
rules: [
{
toolName: 'tool1',
decision: PolicyDecision.DENY,
modes: [ApprovalMode.PLAN],
},
],
approvalMode: ApprovalMode.PLAN,
expected: ['tool1'],
},
{
name: 'should respect approval mode (DEFAULT mode)',
rules: [
{
toolName: 'tool1',
decision: PolicyDecision.DENY,
modes: [ApprovalMode.PLAN],
},
],
approvalMode: ApprovalMode.DEFAULT,
expected: [],
},
{
name: 'should respect wildcard ALLOW rules (e.g. YOLO mode)',
rules: [
{
decision: PolicyDecision.ALLOW,
priority: 999,
modes: [ApprovalMode.YOLO],
},
{
toolName: 'dangerous-tool',
decision: PolicyDecision.DENY,
priority: 10,
modes: [ApprovalMode.YOLO],
},
],
approvalMode: ApprovalMode.YOLO,
expected: [],
},
{
name: 'should respect server wildcard DENY',
rules: [
{
toolName: 'server__*',
decision: PolicyDecision.DENY,
modes: [ApprovalMode.DEFAULT],
},
],
expected: ['server__*'],
},
{
name: 'should expand server wildcard for specific tools if already processed',
rules: [
{
toolName: 'server__*',
decision: PolicyDecision.DENY,
priority: 100,
modes: [ApprovalMode.DEFAULT],
},
{
toolName: 'server__tool1',
decision: PolicyDecision.DENY,
priority: 10,
modes: [ApprovalMode.DEFAULT],
},
],
expected: ['server__*', 'server__tool1'],
},
{
name: 'should exclude run_shell_command but NOT write_file in simulated Plan Mode',
approvalMode: ApprovalMode.PLAN,
rules: [
{
// Simulates the high-priority allow for plans directory
toolName: 'write_file',
decision: PolicyDecision.ALLOW,
priority: 70,
argsPattern: /plans/,
modes: [ApprovalMode.PLAN],
},
{
// Simulates the global deny in Plan Mode
decision: PolicyDecision.DENY,
priority: 60,
modes: [ApprovalMode.PLAN],
},
{
// Simulates a tool from another policy (e.g. write.toml)
toolName: 'run_shell_command',
decision: PolicyDecision.ASK_USER,
priority: 10,
},
],
expected: ['run_shell_command'],
},
{
name: 'should NOT exclude tool if covered by a higher priority wildcard ALLOW',
rules: [
{
toolName: 'server__*',
decision: PolicyDecision.ALLOW,
priority: 100,
modes: [ApprovalMode.DEFAULT],
},
{
toolName: 'server__tool1',
decision: PolicyDecision.DENY,
priority: 10,
modes: [ApprovalMode.DEFAULT],
},
],
expected: [],
},
];
it.each(testCases)(
'$name',
({ rules, approvalMode, nonInteractive, expected }) => {
engine = new PolicyEngine({
rules,
approvalMode: approvalMode ?? ApprovalMode.DEFAULT,
nonInteractive: nonInteractive ?? false,
});
const excluded = engine.getExcludedTools();
expect(Array.from(excluded).sort()).toEqual(expected.sort());
},
);
});
describe('YOLO mode with ask_user tool', () => {
it('should return ASK_USER for ask_user tool even in YOLO mode', async () => {
const rules: PolicyRule[] = [
+105 -3
View File
@@ -26,6 +26,22 @@ import {
} from '../utils/shell-utils.js';
import { getToolAliases } from '../tools/tool-names.js';
function isWildcardPattern(name: string): boolean {
return name.endsWith('__*');
}
function getWildcardPrefix(pattern: string): string {
return pattern.slice(0, -3);
}
function matchesWildcard(pattern: string, toolName: string): boolean {
if (!isWildcardPattern(pattern)) {
return false;
}
const prefix = getWildcardPrefix(pattern);
return toolName.startsWith(prefix + '__');
}
function ruleMatches(
rule: PolicyRule | SafetyCheckerRule,
toolCall: FunctionCall,
@@ -43,8 +59,8 @@ function ruleMatches(
// Check tool name if specified
if (rule.toolName) {
// Support wildcard patterns: "serverName__*" matches "serverName__anyTool"
if (rule.toolName.endsWith('__*')) {
const prefix = rule.toolName.slice(0, -3); // Remove "__*"
if (isWildcardPattern(rule.toolName)) {
const prefix = getWildcardPrefix(rule.toolName);
if (serverName !== undefined) {
// Robust check: if serverName is provided, it MUST match the prefix exactly.
// This prevents "malicious-server" from spoofing "trusted-server" by naming itself "trusted-server__malicious".
@@ -53,7 +69,7 @@ function ruleMatches(
}
}
// Always verify the prefix, even if serverName matched
if (!toolCall.name || !toolCall.name.startsWith(prefix + '__')) {
if (!toolCall.name || !matchesWildcard(rule.toolName, toolCall.name)) {
return false;
}
} else if (toolCall.name !== rule.toolName) {
@@ -509,6 +525,92 @@ export class PolicyEngine {
return this.hookCheckers;
}
/**
* Get tools that are effectively denied by the current rules.
* This takes into account:
* 1. Global rules (no argsPattern)
* 2. Priority order (higher priority wins)
* 3. Non-interactive mode (ASK_USER becomes DENY)
*/
getExcludedTools(): Set<string> {
const excludedTools = new Set<string>();
const processedTools = new Set<string>();
let globalVerdict: PolicyDecision | undefined;
for (const rule of this.rules) {
if (rule.argsPattern) {
if (rule.toolName && rule.decision !== PolicyDecision.DENY) {
processedTools.add(rule.toolName);
}
continue;
}
// Check if rule applies to current approval mode
if (rule.modes && rule.modes.length > 0) {
if (!rule.modes.includes(this.approvalMode)) {
continue;
}
}
// Handle Global Rules
if (!rule.toolName) {
if (globalVerdict === undefined) {
globalVerdict = rule.decision;
if (globalVerdict !== PolicyDecision.DENY) {
// Global ALLOW/ASK found.
// Since rules are sorted by priority, this overrides any lower-priority rules.
// We can stop processing because nothing else will be excluded.
break;
}
// If Global DENY, we continue to find specific tools to add to excluded set
}
continue;
}
const toolName = rule.toolName;
// Check if already processed (exact match)
if (processedTools.has(toolName)) {
continue;
}
// Check if covered by a processed wildcard
let coveredByWildcard = false;
for (const processed of processedTools) {
if (
isWildcardPattern(processed) &&
matchesWildcard(processed, toolName)
) {
// It's covered by a higher-priority wildcard rule.
// If that wildcard rule resulted in exclusion, this tool should also be excluded.
if (excludedTools.has(processed)) {
excludedTools.add(toolName);
}
coveredByWildcard = true;
break;
}
}
if (coveredByWildcard) {
continue;
}
processedTools.add(toolName);
// Determine decision
let decision: PolicyDecision;
if (globalVerdict !== undefined) {
decision = globalVerdict;
} else {
decision = rule.decision;
}
if (decision === PolicyDecision.DENY) {
excludedTools.add(toolName);
}
}
return excludedTools;
}
private applyNonInteractiveMode(decision: PolicyDecision): PolicyDecision {
// In non-interactive mode, ASK_USER becomes DENY
if (this.nonInteractive && decision === PolicyDecision.ASK_USER) {
+2 -2
View File
@@ -67,7 +67,7 @@ export class PromptProvider {
let planModeToolsList = PLAN_MODE_TOOLS.filter((t) =>
enabledToolNames.has(t),
)
.map((t) => `- \`${t}\``)
.map((t) => ` <tool>\`${t}\`</tool>`)
.join('\n');
// Add read-only MCP tools to the list
@@ -79,7 +79,7 @@ export class PromptProvider {
);
if (readOnlyMcpTools.length > 0) {
const mcpToolsList = readOnlyMcpTools
.map((t) => `- \`${t.name}\` (${t.serverName})`)
.map((t) => ` <tool>\`${t.name}\` (${t.serverName})</tool>`)
.join('\n');
planModeToolsList += `\n${mcpToolsList}`;
}
+60 -78
View File
@@ -419,78 +419,48 @@ export function renderPlanningWorkflow(
return `
# Active Approval Mode: Plan
You are operating in **Plan Mode** - a structured planning workflow for designing implementation strategies before execution.
You are operating in **Plan Mode**. Your goal is to produce a detailed implementation plan in \`${options.plansDir}/\` and get user approval before editing source code.
## Available Tools
The following read-only tools are available in Plan Mode:
<available_tools>
${options.planModeToolsList}
- ${formatToolName(WRITE_FILE_TOOL_NAME)} - Save plans to the plans directory (see Plan Storage below)
- ${formatToolName(EDIT_TOOL_NAME)} - Update plans in the plans directory
<tool>${formatToolName(WRITE_FILE_TOOL_NAME)} - Save plans to the plans directory</tool>
<tool>${formatToolName(EDIT_TOOL_NAME)} - Update plans in the plans directory</tool>
</available_tools>
## Plan Storage
- Save your plans as Markdown (.md) files ONLY within: \`${options.plansDir}/\`
- You are restricted to writing files within this directory while in Plan Mode.
- Use descriptive filenames: \`feature-name.md\` or \`bugfix-description.md\`
## Rules
1. **Read-Only:** You cannot modify source code. You may ONLY use read-only tools to explore, and you can only write to \`${options.plansDir}/\`.
2. **Efficiency:** Autonomously combine discovery and drafting phases to minimize conversational turns. If the request is ambiguous, use ${formatToolName(ASK_USER_TOOL_NAME)} to clarify. Otherwise, explore the codebase and write the draft in one fluid motion.
3. **Plan Storage:** Save plans as Markdown (.md) using descriptive filenames (e.g., \`feature-x.md\`).
## Workflow Rules
1. Sequential Execution: Complete ONE phase at a time. Do NOT skip ahead or combine phases.
2. User Confirmation: Wait for user input/approval before proceeding to the next phase.
3. Step Back Protocol: If new information discovered during Exploration or Design invalidates previous assumptions or requirements, you MUST pause, inform the user, and request to return to the appropriate previous phase.
## Required Plan Structure
When writing the plan file, you MUST include the following structure:
# Objective
(A concise summary of what needs to be built or fixed)
# Key Files & Context
(List the specific files that will be modified, including helpful context like function signatures or code snippets)
# Implementation Steps
(Iterative development steps, e.g., "1. Implement X in [File]", "2. Verify with test Y")
# Verification & Testing
(Specific unit tests, manual checks, or build commands to verify success)
## Workflow Phases
## Workflow
1. **Explore & Analyze:** Analyze requirements and use search/read tools to explore the codebase. For complex tasks, identify at least two viable implementation approaches.
2. **Consult:** Present a concise summary of the identified approaches (including pros/cons and your recommendation) to the user via ${formatToolName(ASK_USER_TOOL_NAME)} and wait for their selection. For simple or canonical tasks, you may skip this and proceed to drafting.
3. **Draft:** Write the detailed implementation plan for the selected approach to the plans directory using ${formatToolName(WRITE_FILE_TOOL_NAME)}.
4. **Review & Approval:** Present a brief summary of the drafted plan in your chat response and concurrently call the ${formatToolName(EXIT_PLAN_MODE_TOOL_NAME)} tool to formally request approval. If rejected, iterate.
### Phase 1: Requirements
- Analyze the user's request to identify core requirements and constraints.
- Proactively identify ambiguities, implicit assumptions, and edge cases.
- Categorize questions: functional requirements, non-functional constraints (performance, compatibility), and scope boundaries.
- Use the ${formatToolName(ASK_USER_TOOL_NAME)} tool with well-structured options to clarify ambiguities. Prefer providing multiple-choice options for the user to select from when possible.
### Phase 2: Exploration
- Only begin this phase after requirements are clear.
- Use the available read-only tools to explore the project.
- Map relevant code paths, dependencies, and architectural patterns.
- Identify existing utilities, patterns, and abstractions that can be reused.
- Note potential constraints (e.g., existing conventions, test infrastructure).
- Output: Summarize key findings to the user before proceeding to design.
### Phase 3: Design
- Only begin this phase after exploration is complete.
- **Identify Approaches:**
- For Complex Tasks: Identify at least 2 viable implementation approaches. Document the approach summary, pros, cons, complexity estimate, and risk factors for each.
- For Canonical Tasks: If there is only one reasonable, standard approach (e.g., a standard library pattern or specific bug fix), detail it and explicitly explain why no other viable alternatives were considered.
- Mandatory User Interaction: Present the analysis to the user via ${formatToolName(ASK_USER_TOOL_NAME)} and recommend a preferred approach.
- Wait for Selection: You MUST pause and wait for the user to select an approach before proceeding. Do NOT assume the user will agree with your recommendation.
### Phase 4: Planning
- Pre-requisite: You MUST have a user-selected approach from Phase 3 before generating the plan.
- Create a detailed implementation plan and save it to the designated plans directory.
- **Document Structure:** The plan MUST be a structured Markdown document (focused on implementation guidance, not workflow logging) using exactly these H2 headings:
- \`## Problem Statement\` - Describe the problem or need this change addresses.
- \`## Proposed Solution\` - Provide technical details of the implementation.
- \`## Implementation Plan\` - List ordered steps with specific file paths and the nature of each change.
- \`## Verification Plan\` - Define specific tests or manual steps to verify the change works and breaks nothing else.
- \`## Risks & Mitigations\` - Identify potential failure modes and mitigation strategies.
- \`## Alternatives Considered\` - Provide a brief analysis of other approaches considered and why they were rejected.
### Phase 5: Approval
- Present the plan and request approval for the finalized plan using the ${formatToolName(EXIT_PLAN_MODE_TOOL_NAME)} tool
- If plan is approved, you can begin implementation.
- If plan is rejected, address the feedback and iterate on the plan.
${renderApprovedPlanSection(options.approvedPlanPath)}
## Constraints
- You may ONLY use the read-only tools listed above
- You MUST NOT modify source code, configs, or any files
- If asked to modify code, explain you are in Plan Mode and suggest exiting Plan Mode to enable edits`.trim();
${renderApprovedPlanSection(options.approvedPlanPath)}`.trim();
}
function renderApprovedPlanSection(approvedPlanPath?: string): string {
if (!approvedPlanPath) return '';
return `## Approved Plan
An approved plan is available for this task.
- **Iterate:** You should default to refining the existing approved plan.
- **New Plan:** Only create a new plan file if the user explicitly asks for a "new plan" or if the current request is for a completely different feature or bug.
An approved plan is available for this task at \`${approvedPlanPath}\`.
- **Read First:** You MUST read this file using the ${formatToolName(READ_FILE_TOOL_NAME)} tool before proposing any changes or starting discovery.
- **Iterate:** Default to refining the existing approved plan.
- **New Plan:** Only create a new plan file if the user explicitly asks for a "new plan".
`;
}
@@ -528,7 +498,7 @@ function mandateContinueWork(interactive: boolean): string {
function workflowStepResearch(options: PrimaryWorkflowsOptions): string {
let suggestion = '';
if (options.enableEnterPlanModeTool) {
suggestion = ` For complex tasks, consider using the ${formatToolName(ENTER_PLAN_MODE_TOOL_NAME)} tool to enter a dedicated planning phase before starting implementation.`;
suggestion = ` If the request is ambiguous, broad in scope, or involves creating a new feature/application, you MUST use the ${formatToolName(ENTER_PLAN_MODE_TOOL_NAME)} tool to design your approach before making changes. Do NOT use Plan Mode for straightforward bug fixes, answering questions, or simple inquiries.`;
}
const searchTools: string[] = [];
@@ -558,7 +528,7 @@ function workflowStepResearch(options: PrimaryWorkflowsOptions): string {
function workflowStepStrategy(options: PrimaryWorkflowsOptions): string {
if (options.approvedPlan) {
return `2. **Strategy:** An approved plan is available for this task. Use this file as a guide for your implementation. You MUST read this file before proceeding. If you discover new requirements or need to change the approach, confirm with the user and update this plan file to reflect the updated design decisions or discovered requirements.`;
return `2. **Strategy:** An approved plan is available for this task. Treat this file as your single source of truth. You MUST read this file before proceeding. If you discover new requirements or need to change the approach, confirm with the user and update this plan file to reflect the updated design decisions or discovered requirements.`;
}
if (options.enableWriteTodosTool) {
@@ -582,16 +552,35 @@ function newApplicationSteps(options: PrimaryWorkflowsOptions): string {
if (options.approvedPlan) {
return `
1. **Understand:** Read the approved plan. Use this file as a guide for your implementation.
2. **Implement:** Implement the application according to the plan. If you discover new requirements or need to change the approach, confirm with the user and update this plan file to reflect the updated design decisions or discovered requirements.
3. **Verify:** Review work against the original request, the approved plan. Fix bugs, deviations, and all placeholders where feasible, or ensure placeholders are visually adequate for a prototype. Ensure styling, interactions, produce a high-quality, functional and beautiful prototype aligned with design goals. Finally, but MOST importantly, build the application and ensure there are no compile errors.
1. **Understand:** Read the approved plan. Treat this file as your single source of truth.
2. **Implement:** Implement the application according to the plan. When starting, scaffold the application using ${formatToolName(SHELL_TOOL_NAME)}. For visual assets, utilize **platform-native primitives** (e.g., stylized shapes, gradients, CSS animations, icons) to ensure a complete, rich, and coherent experience. Never link to external services or assume local paths for assets that have not been created. If you discover new requirements or need to change the approach, confirm with the user and update the plan file.
3. **Verify:** Review work against the original request and the approved plan. Fix bugs, deviations, and ensure placeholders are visually adequate. **Ensure styling and interactions produce a high-quality, polished, and beautiful prototype.** Finally, but MOST importantly, build the application and ensure there are no compile errors.
4. **Finish:** Provide a brief summary of what was built.`.trim();
}
// When Plan Mode is enabled globally, mandate its use for new apps and let the
// standard 'Execution' loop handle implementation once the plan is approved.
if (options.enableEnterPlanModeTool) {
return `
1. **Mandatory Planning:** You MUST use the ${formatToolName(ENTER_PLAN_MODE_TOOL_NAME)} tool to draft a comprehensive design document and obtain user approval before writing any code.
2. **Design Constraints:** When drafting your plan, adhere to these defaults unless explicitly overridden by the user:
- **Goal:** Autonomously design a visually appealing, substantially complete, and functional prototype with rich aesthetics. Users judge applications by their visual impact; ensure they feel modern, "alive," and polished through consistent spacing, typography, and interactive feedback.
- **Visuals:** Describe your strategy for sourcing or generating placeholders (e.g., stylized CSS shapes, gradients, procedurally generated patterns) to ensure a visually complete prototype. Never plan for assets that cannot be locally generated.
- **Styling:** **Prefer Vanilla CSS** for maximum flexibility. **Avoid TailwindCSS** unless explicitly requested.
- **Web:** React (TypeScript) or Angular with Vanilla CSS.
- **APIs:** Node.js (Express) or Python (FastAPI).
- **Mobile:** Compose Multiplatform or Flutter.
- **Games:** HTML/CSS/JS (Three.js for 3D).
- **CLIs:** Python or Go.
3. **Implementation:** Once the plan is approved, follow the standard **Execution** cycle to build the application, utilizing platform-native primitives to realize the rich aesthetic you planned.`.trim();
}
// --- FALLBACK: Legacy workflow for when Plan Mode is disabled ---
if (interactive) {
return `
1. **Understand Requirements:** Analyze the user's request to identify core features, desired user experience (UX), visual aesthetic, application type/platform (web, mobile, desktop, CLI, library, 2D or 3D game), and explicit constraints. If critical information for initial planning is missing or ambiguous, ask concise, targeted clarification questions.
2. **Propose Plan:** Formulate an internal development plan. Present a clear, concise, high-level summary to the user. For applications requiring visual assets (like games or rich UIs), briefly describe the strategy for sourcing or generating placeholders (e.g., simple geometric shapes, procedurally generated patterns) to ensure a visually complete initial prototype.${planningPhaseSuggestion(options)}
2. **Propose Plan:** Formulate an internal development plan. Present a clear, concise, high-level summary to the user and obtain their approval before proceeding. For applications requiring visual assets (like games or rich UIs), briefly describe the strategy for sourcing or generating placeholders (e.g., simple geometric shapes, procedurally generated patterns).
- **Styling:** **Prefer Vanilla CSS** for maximum flexibility. **Avoid TailwindCSS** unless explicitly requested; if requested, confirm the specific version (e.g., v3 or v4).
- **Default Tech Stack:**
- **Web:** React (TypeScript) or Angular with Vanilla CSS.
@@ -599,14 +588,14 @@ function newApplicationSteps(options: PrimaryWorkflowsOptions): string {
- **Mobile:** Compose Multiplatform or Flutter.
- **Games:** HTML/CSS/JS (Three.js for 3D).
- **CLIs:** Python or Go.
3. **User Approval:** Obtain user approval for the proposed plan.
4. **Implementation:** Autonomously implement each feature per the approved plan. When starting, scaffold the application using ${formatToolName(SHELL_TOOL_NAME)} for commands like 'npm init', 'npx create-react-app'. For visual assets, utilize **platform-native primitives** (e.g., stylized shapes, gradients, icons) to ensure a complete, coherent experience. Never link to external services or assume local paths for assets that have not been created.
5. **Verify:** Review work against the original request. Fix bugs and deviations. Ensure styling and interactions produce a high-quality, functional, and beautiful prototype. **Build the application and ensure there are no compile errors.**
6. **Solicit Feedback:** Provide instructions on how to start the application and request user feedback on the prototype.`.trim();
3. **Implementation:** Autonomously implement each feature per the approved plan. When starting, scaffold the application using ${formatToolName(SHELL_TOOL_NAME)} for commands like 'npm init', 'npx create-react-app'. For visual assets, utilize **platform-native primitives** (e.g., stylized shapes, gradients, icons) to ensure a complete, coherent experience. Never link to external services or assume local paths for assets that have not been created.
4. **Verify:** Review work against the original request. Fix bugs and deviations. Ensure styling and interactions produce a high-quality, functional, and beautiful prototype. **Build the application and ensure there are no compile errors.**
5. **Solicit Feedback:** Provide instructions on how to start the application and request user feedback on the prototype.`.trim();
}
return `
1. **Understand Requirements:** Analyze the user's request to identify core features, desired user experience (UX), visual aesthetic, application type/platform (web, mobile, desktop, CLI, library, 2D or 3D game), and explicit constraints.
2. **Propose Plan:** Formulate an internal development plan. Present a clear, concise, high-level summary to the user. For applications requiring visual assets, describe the strategy for sourcing or generating placeholders.
2. **Plan:** Formulate an internal development plan. For applications requiring visual assets, describe the strategy for sourcing or generating placeholders.
- **Styling:** **Prefer Vanilla CSS** for maximum flexibility. **Avoid TailwindCSS** unless explicitly requested.
- **Default Tech Stack:**
- **Web:** React (TypeScript) or Angular with Vanilla CSS.
@@ -614,17 +603,10 @@ function newApplicationSteps(options: PrimaryWorkflowsOptions): string {
- **Mobile:** Compose Multiplatform or Flutter.
- **Games:** HTML/CSS/JS (Three.js for 3D).
- **CLIs:** Python or Go.
3. Implementation: Autonomously implement each feature per the approved plan. When starting, scaffold the application using ${formatToolName(SHELL_TOOL_NAME)}. For visual assets, utilize **platform-native primitives** (e.g., stylized shapes, gradients, icons). Never link to external services or assume local paths for assets that have not been created.
3. **Implementation:** Autonomously implement each feature per the approved plan. When starting, scaffold the application using ${formatToolName(SHELL_TOOL_NAME)}. For visual assets, utilize **platform-native primitives** (e.g., stylized shapes, gradients, icons). Never link to external services or assume local paths for assets that have not been created.
4. **Verify:** Review work against the original request. Fix bugs and deviations. **Build the application and ensure there are no compile errors.**`.trim();
}
function planningPhaseSuggestion(options: PrimaryWorkflowsOptions): string {
if (options.enableEnterPlanModeTool) {
return ` For complex tasks, consider using the ${formatToolName(ENTER_PLAN_MODE_TOOL_NAME)} tool to enter a dedicated planning phase before starting implementation.`;
}
return '';
}
function toneAndStyleNoChitchat(isGemini3: boolean): string {
return isGemini3
? `
@@ -32,6 +32,18 @@ const { Terminal } = pkg;
const MAX_CHILD_PROCESS_BUFFER_SIZE = 16 * 1024 * 1024; // 16MB
/**
* An environment variable that is set for shell executions. This can be used
* by downstream executables and scripts to identify that they were executed
* from within Gemini CLI.
*/
export const GEMINI_CLI_IDENTIFICATION_ENV_VAR = 'GEMINI_CLI';
/**
* The value of {@link GEMINI_CLI_IDENTIFICATION_ENV_VAR}
*/
export const GEMINI_CLI_IDENTIFICATION_ENV_VAR_VALUE = '1';
// We want to allow shell outputs that are close to the context window in size.
// 300,000 lines is roughly equivalent to a large context window, ensuring
// we capture significant output from long-running commands.
@@ -302,7 +314,8 @@ export class ShellExecutionService {
detached: !isWindows,
env: {
...sanitizeEnvironment(process.env, sanitizationConfig),
GEMINI_CLI: '1',
[GEMINI_CLI_IDENTIFICATION_ENV_VAR]:
GEMINI_CLI_IDENTIFICATION_ENV_VAR_VALUE,
TERM: 'xterm-256color',
PAGER: 'cat',
GIT_PAGER: 'cat',
@@ -336,6 +336,10 @@ describe('ClearcutLogger', () => {
gemini_cli_key: EventMetadataKey.GEMINI_CLI_USER_SETTINGS,
value: logger?.getConfigJson(),
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_ACTIVE_APPROVAL_MODE,
value: 'default',
},
]),
);
});
@@ -1239,6 +1243,90 @@ describe('ClearcutLogger', () => {
EventMetadataKey.GEMINI_CLI_AI_ADDED_LINES,
);
});
it('logs AskUser tool metadata', () => {
const { logger } = setup();
const completedToolCall = {
request: {
name: 'ask_user',
args: { questions: [] },
prompt_id: 'prompt-123',
},
response: {
resultDisplay: 'User answered: ...',
data: {
ask_user: {
question_types: ['choice', 'text'],
dismissed: false,
empty_submission: false,
answer_count: 2,
},
},
},
status: 'success',
} as unknown as SuccessfulToolCall;
logger?.logToolCallEvent(new ToolCallEvent(completedToolCall));
const events = getEvents(logger!);
expect(events.length).toBe(1);
expect(events[0]).toHaveEventName(EventNames.TOOL_CALL);
expect(events[0]).toHaveMetadataValue([
EventMetadataKey.GEMINI_CLI_ASK_USER_QUESTION_TYPES,
JSON.stringify(['choice', 'text']),
]);
expect(events[0]).toHaveMetadataValue([
EventMetadataKey.GEMINI_CLI_ASK_USER_DISMISSED,
'false',
]);
expect(events[0]).toHaveMetadataValue([
EventMetadataKey.GEMINI_CLI_ASK_USER_EMPTY_SUBMISSION,
'false',
]);
expect(events[0]).toHaveMetadataValue([
EventMetadataKey.GEMINI_CLI_ASK_USER_ANSWER_COUNT,
'2',
]);
});
it('does not log AskUser tool metadata for other tools', () => {
const { logger } = setup();
const completedToolCall = {
request: {
name: 'some_other_tool',
args: {},
prompt_id: 'prompt-123',
},
response: {
resultDisplay: 'Result',
data: {
ask_user_question_types: ['choice', 'text'],
ask_user_dismissed: false,
ask_user_empty_submission: false,
ask_user_answer_count: 2,
},
},
status: 'success',
} as unknown as SuccessfulToolCall;
logger?.logToolCallEvent(new ToolCallEvent(completedToolCall));
const events = getEvents(logger!);
expect(events.length).toBe(1);
expect(events[0]).toHaveEventName(EventNames.TOOL_CALL);
expect(events[0]).not.toHaveMetadataKey(
EventMetadataKey.GEMINI_CLI_ASK_USER_QUESTION_TYPES,
);
expect(events[0]).not.toHaveMetadataKey(
EventMetadataKey.GEMINI_CLI_ASK_USER_DISMISSED,
);
expect(events[0]).not.toHaveMetadataKey(
EventMetadataKey.GEMINI_CLI_ASK_USER_EMPTY_SUBMISSION,
);
expect(events[0]).not.toHaveMetadataKey(
EventMetadataKey.GEMINI_CLI_ASK_USER_ANSWER_COUNT,
);
});
});
describe('flushIfNeeded', () => {
@@ -56,6 +56,7 @@ import {
safeJsonStringify,
safeJsonStringifyBooleanValuesOnly,
} from '../../utils/safeJsonStringify.js';
import { ASK_USER_TOOL_NAME } from '../../tools/tool-names.js';
import { FixedDeque } from 'mnemonist';
import { GIT_COMMIT_INFO, CLI_VERSION } from '../../generated/git-commit.js';
import {
@@ -704,6 +705,29 @@ export class ClearcutLogger {
user_removed_chars: EventMetadataKey.GEMINI_CLI_USER_REMOVED_CHARS,
};
if (
event.function_name === ASK_USER_TOOL_NAME &&
event.metadata['ask_user']
) {
const askUser = event.metadata['ask_user'];
const askUserMapping: { [key: string]: EventMetadataKey } = {
question_types: EventMetadataKey.GEMINI_CLI_ASK_USER_QUESTION_TYPES,
dismissed: EventMetadataKey.GEMINI_CLI_ASK_USER_DISMISSED,
empty_submission:
EventMetadataKey.GEMINI_CLI_ASK_USER_EMPTY_SUBMISSION,
answer_count: EventMetadataKey.GEMINI_CLI_ASK_USER_ANSWER_COUNT,
};
for (const [key, gemini_cli_key] of Object.entries(askUserMapping)) {
if (askUser[key] !== undefined) {
data.push({
gemini_cli_key,
value: JSON.stringify(askUser[key]),
});
}
}
}
for (const [key, gemini_cli_key] of Object.entries(metadataMapping)) {
if (event.metadata[key] !== undefined) {
data.push({
@@ -1625,6 +1649,14 @@ export class ClearcutLogger {
gemini_cli_key: EventMetadataKey.GEMINI_CLI_INTERACTIVE,
value: this.config?.isInteractive().toString() ?? 'false',
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_ACTIVE_APPROVAL_MODE,
value:
typeof this.config?.getPolicyEngine === 'function' &&
typeof this.config.getPolicyEngine()?.getApprovalMode === 'function'
? this.config.getPolicyEngine().getApprovalMode()
: '',
},
];
if (this.config?.getExperiments()) {
defaultLogMetadata.push({
@@ -7,7 +7,7 @@
// Defines valid event metadata keys for Clearcut logging.
export enum EventMetadataKey {
// Deleted enums: 24
// Next ID: 152
// Next ID: 156
GEMINI_CLI_KEY_UNKNOWN = 0,
@@ -577,4 +577,20 @@ export enum EventMetadataKey {
// Logs the total prunable tokens identified at the trigger point.
GEMINI_CLI_TOOL_OUTPUT_MASKING_TOTAL_PRUNABLE_TOKENS = 151,
// ==========================================================================
// Ask User Stats Event Keys
// ==========================================================================
// Logs the types of questions asked in the ask_user tool.
GEMINI_CLI_ASK_USER_QUESTION_TYPES = 152,
// Logs whether the ask_user dialog was dismissed.
GEMINI_CLI_ASK_USER_DISMISSED = 153,
// Logs whether the ask_user dialog was submitted empty.
GEMINI_CLI_ASK_USER_EMPTY_SUBMISSION = 154,
// Logs the number of questions answered in the ask_user tool.
GEMINI_CLI_ASK_USER_ANSWER_COUNT = 155,
}
@@ -5,6 +5,7 @@
*/
import type {
AnyDeclarativeTool,
AnyToolInvocation,
CompletedToolCall,
ContentGeneratorConfig,
@@ -1184,6 +1185,53 @@ describe('loggers', () => {
{ function_name: 'test-function' },
);
});
it('should merge data from response into metadata', () => {
const call: CompletedToolCall = {
status: 'success',
request: {
name: 'ask_user',
args: { questions: [] },
callId: 'test-call-id',
isClientInitiated: true,
prompt_id: 'prompt-id-1',
},
response: {
callId: 'test-call-id',
responseParts: [{ text: 'test-response' }],
resultDisplay: 'User answered: ...',
error: undefined,
errorType: undefined,
data: {
ask_user: {
question_types: ['choice'],
dismissed: false,
},
},
},
tool: undefined as unknown as AnyDeclarativeTool,
invocation: {} as AnyToolInvocation,
durationMs: 100,
outcome: ToolConfirmationOutcome.ProceedOnce,
};
const event = new ToolCallEvent(call);
logToolCall(mockConfig, event);
expect(mockLogger.emit).toHaveBeenCalledWith({
body: 'Tool call: ask_user. Decision: accept. Success: true. Duration: 100ms.',
attributes: expect.objectContaining({
function_name: 'ask_user',
metadata: expect.objectContaining({
ask_user: {
question_types: ['choice'],
dismissed: false,
},
}),
}),
});
});
it('should log a tool call with a reject decision', () => {
const call: ErroredToolCall = {
status: 'error',
+5
View File
@@ -304,6 +304,7 @@ export class ToolCallEvent implements BaseTelemetryEvent {
const diffStat = fileDiff.diffStat;
if (diffStat) {
this.metadata = {
...this.metadata,
model_added_lines: diffStat.model_added_lines,
model_removed_lines: diffStat.model_removed_lines,
model_added_chars: diffStat.model_added_chars,
@@ -315,6 +316,10 @@ export class ToolCallEvent implements BaseTelemetryEvent {
};
}
}
if (call.status === 'success' && call.response.data) {
this.metadata = { ...this.metadata, ...call.response.data };
}
} else {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
this.function_name = function_name as string;
+22
View File
@@ -337,6 +337,14 @@ describe('AskUserTool', () => {
expect(JSON.parse(result.llmContent as string)).toEqual({
answers: { '0': 'Quick fix (Recommended)' },
});
expect(result.data).toEqual({
ask_user: {
question_types: [QuestionType.CHOICE],
dismissed: false,
empty_submission: false,
answer_count: 1,
},
});
});
it('should display message when user submits without answering', async () => {
@@ -368,6 +376,14 @@ describe('AskUserTool', () => {
'User submitted without answering questions.',
);
expect(JSON.parse(result.llmContent as string)).toEqual({ answers: {} });
expect(result.data).toEqual({
ask_user: {
question_types: [QuestionType.CHOICE],
dismissed: false,
empty_submission: true,
answer_count: 0,
},
});
});
it('should handle cancellation', async () => {
@@ -405,6 +421,12 @@ describe('AskUserTool', () => {
expect(result.llmContent).toBe(
'User dismissed ask_user dialog without answering.',
);
expect(result.data).toEqual({
ask_user: {
question_types: [QuestionType.CHOICE],
dismissed: true,
},
});
});
});
});
+20
View File
@@ -192,16 +192,35 @@ export class AskUserInvocation extends BaseToolInvocation<
}
async execute(_signal: AbortSignal): Promise<ToolResult> {
const questionTypes = this.params.questions.map(
(q) => q.type ?? QuestionType.CHOICE,
);
if (this.confirmationOutcome === ToolConfirmationOutcome.Cancel) {
return {
llmContent: 'User dismissed ask_user dialog without answering.',
returnDisplay: 'User dismissed dialog',
data: {
ask_user: {
question_types: questionTypes,
dismissed: true,
},
},
};
}
const answerEntries = Object.entries(this.userAnswers);
const hasAnswers = answerEntries.length > 0;
const metrics: Record<string, unknown> = {
ask_user: {
question_types: questionTypes,
dismissed: false,
empty_submission: !hasAnswers,
answer_count: answerEntries.length,
},
};
const returnDisplay = hasAnswers
? `**User answered:**\n${answerEntries
.map(([index, answer]) => {
@@ -219,6 +238,7 @@ export class AskUserInvocation extends BaseToolInvocation<
return {
llmContent: JSON.stringify({ answers: this.userAnswers }),
returnDisplay,
data: metrics,
};
}
}
@@ -1639,6 +1639,28 @@ describe('mcp-client', () => {
});
});
it('sets an env variable GEMINI_CLI=1 for stdio MCP servers', async () => {
const mockedTransport = vi
.spyOn(SdkClientStdioLib, 'StdioClientTransport')
.mockReturnValue({} as SdkClientStdioLib.StdioClientTransport);
await createTransport(
'test-server',
{
command: 'test-command',
args: ['--foo', 'bar'],
env: {},
cwd: 'test/cwd',
},
false,
EMPTY_CONFIG,
);
const callArgs = mockedTransport.mock.calls[0][0];
expect(callArgs.env).toBeDefined();
expect(callArgs.env!['GEMINI_CLI']).toBe('1');
});
it('should exclude extension settings with undefined values from environment', async () => {
const mockedTransport = vi
.spyOn(SdkClientStdioLib, 'StdioClientTransport')
+6 -1
View File
@@ -67,6 +67,10 @@ import {
sanitizeEnvironment,
type EnvironmentSanitizationConfig,
} from '../services/environmentSanitization.js';
import {
GEMINI_CLI_IDENTIFICATION_ENV_VAR,
GEMINI_CLI_IDENTIFICATION_ENV_VAR_VALUE,
} from '../services/shellExecutionService.js';
export const MCP_DEFAULT_TIMEOUT_MSEC = 10 * 60 * 1000; // default to 10 minutes
@@ -1897,10 +1901,11 @@ export async function createTransport(
let transport: Transport = new StdioClientTransport({
command: mcpServerConfig.command,
args: mcpServerConfig.args || [],
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
env: {
...sanitizeEnvironment(process.env, sanitizationConfig),
...(mcpServerConfig.env || {}),
[GEMINI_CLI_IDENTIFICATION_ENV_VAR]:
GEMINI_CLI_IDENTIFICATION_ENV_VAR_VALUE,
} as Record<string, string>,
cwd: mcpServerConfig.cwd,
stderr: 'pipe',
+18
View File
@@ -127,6 +127,17 @@ export interface AgentsDiscoveredPayload {
agents: AgentDefinition[];
}
export interface SlashCommandConflict {
name: string;
renamedTo: string;
loserExtensionName?: string;
winnerExtensionName?: string;
}
export interface SlashCommandConflictsPayload {
conflicts: SlashCommandConflict[];
}
/**
* Payload for the 'quota-changed' event.
*/
@@ -155,6 +166,7 @@ export enum CoreEvent {
AgentsDiscovered = 'agents-discovered',
RequestEditorSelection = 'request-editor-selection',
EditorSelected = 'editor-selected',
SlashCommandConflicts = 'slash-command-conflicts',
QuotaChanged = 'quota-changed',
}
@@ -185,6 +197,7 @@ export interface CoreEvents extends ExtensionEvents {
[CoreEvent.AgentsDiscovered]: [AgentsDiscoveredPayload];
[CoreEvent.RequestEditorSelection]: never[];
[CoreEvent.EditorSelected]: [EditorSelectedPayload];
[CoreEvent.SlashCommandConflicts]: [SlashCommandConflictsPayload];
}
type EventBacklogItem = {
@@ -322,6 +335,11 @@ export class CoreEventEmitter extends EventEmitter<CoreEvents> {
this._emitOrQueue(CoreEvent.AgentsDiscovered, payload);
}
emitSlashCommandConflicts(conflicts: SlashCommandConflict[]): void {
const payload: SlashCommandConflictsPayload = { conflicts };
this._emitOrQueue(CoreEvent.SlashCommandConflicts, payload);
}
/**
* Notifies subscribers that the quota has changed.
*/
+44 -8
View File
@@ -99,16 +99,50 @@ describe('isHeadlessMode', () => {
expect(isHeadlessMode({ prompt: true })).toBe(true);
});
it('should return false if query is provided but it is still a TTY', () => {
// Note: per current logic, query alone doesn't force headless if TTY
// This matches the existing behavior in packages/cli/src/config/config.ts
expect(isHeadlessMode({ query: 'test query' })).toBe(false);
it('should return true if query is provided', () => {
expect(isHeadlessMode({ query: 'test query' })).toBe(true);
});
it('should return true if -p or --prompt is in process.argv as a fallback', () => {
const originalArgv = process.argv;
process.argv = ['node', 'index.js', '-p', 'hello'];
try {
expect(isHeadlessMode()).toBe(true);
} finally {
process.argv = originalArgv;
}
process.argv = ['node', 'index.js', '--prompt', 'hello'];
try {
expect(isHeadlessMode()).toBe(true);
} finally {
process.argv = originalArgv;
}
});
it('should return true if -y or --yolo is in process.argv as a fallback', () => {
const originalArgv = process.argv;
process.argv = ['node', 'index.js', '-y'];
try {
expect(isHeadlessMode()).toBe(true);
} finally {
process.argv = originalArgv;
}
process.argv = ['node', 'index.js', '--yolo'];
try {
expect(isHeadlessMode()).toBe(true);
} finally {
process.argv = originalArgv;
}
});
it('should handle undefined process.stdout gracefully', () => {
const originalStdout = process.stdout;
// @ts-expect-error - testing edge case
delete process.stdout;
Object.defineProperty(process, 'stdout', {
value: undefined,
configurable: true,
});
try {
expect(isHeadlessMode()).toBe(false);
@@ -122,8 +156,10 @@ describe('isHeadlessMode', () => {
it('should handle undefined process.stdin gracefully', () => {
const originalStdin = process.stdin;
// @ts-expect-error - testing edge case
delete process.stdin;
Object.defineProperty(process, 'stdin', {
value: undefined,
configurable: true,
});
try {
expect(isHeadlessMode()).toBe(false);
+18 -11
View File
@@ -28,18 +28,25 @@ export interface HeadlessModeOptions {
* @returns true if the environment is considered headless.
*/
export function isHeadlessMode(options?: HeadlessModeOptions): boolean {
if (process.env['GEMINI_CLI_INTEGRATION_TEST'] === 'true') {
return (
!!options?.prompt ||
(!!process.stdin && !process.stdin.isTTY) ||
(!!process.stdout && !process.stdout.isTTY)
);
if (process.env['GEMINI_CLI_INTEGRATION_TEST'] !== 'true') {
const isCI =
process.env['CI'] === 'true' || process.env['GITHUB_ACTIONS'] === 'true';
if (isCI) {
return true;
}
}
return (
process.env['CI'] === 'true' ||
process.env['GITHUB_ACTIONS'] === 'true' ||
!!options?.prompt ||
const isNotTTY =
(!!process.stdin && !process.stdin.isTTY) ||
(!!process.stdout && !process.stdout.isTTY)
(!!process.stdout && !process.stdout.isTTY);
if (isNotTTY || !!options?.prompt || !!options?.query) {
return true;
}
// Fallback: check process.argv for flags that imply headless or auto-approve mode.
return process.argv.some(
(arg) =>
arg === '-p' || arg === '--prompt' || arg === '-y' || arg === '--yolo',
);
}
-189
View File
@@ -1,189 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, afterEach } from 'vitest';
import { isDirectorySecure } from './security.js';
import * as fs from 'node:fs/promises';
import { constants, type Stats } from 'node:fs';
import * as os from 'node:os';
import { spawnAsync } from './shell-utils.js';
vi.mock('node:fs/promises');
vi.mock('node:fs');
vi.mock('node:os');
vi.mock('./shell-utils.js', () => ({
spawnAsync: vi.fn(),
}));
describe('isDirectorySecure', () => {
afterEach(() => {
vi.clearAllMocks();
});
it('returns secure=true on Windows if ACL check passes', async () => {
vi.spyOn(os, 'platform').mockReturnValue('win32');
vi.mocked(fs.stat).mockResolvedValue({
isDirectory: () => true,
} as unknown as Stats);
vi.mocked(spawnAsync).mockResolvedValue({ stdout: '', stderr: '' });
const result = await isDirectorySecure('C:\\Some\\Path');
expect(result.secure).toBe(true);
expect(spawnAsync).toHaveBeenCalledWith(
'powershell',
expect.arrayContaining(['-Command', expect.stringContaining('Get-Acl')]),
);
});
it('returns secure=false on Windows if ACL check fails', async () => {
vi.spyOn(os, 'platform').mockReturnValue('win32');
vi.mocked(fs.stat).mockResolvedValue({
isDirectory: () => true,
} as unknown as Stats);
vi.mocked(spawnAsync).mockResolvedValue({
stdout: 'BUILTIN\\Users',
stderr: '',
});
const result = await isDirectorySecure('C:\\Some\\Path');
expect(result.secure).toBe(false);
expect(result.reason).toBe(
"Directory 'C:\\Some\\Path' is insecure. The following user groups have write permissions: BUILTIN\\Users. To fix this, remove Write and Modify permissions for these groups from the directory's ACLs.",
);
});
it('returns secure=false on Windows if spawnAsync fails', async () => {
vi.spyOn(os, 'platform').mockReturnValue('win32');
vi.mocked(fs.stat).mockResolvedValue({
isDirectory: () => true,
} as unknown as Stats);
vi.mocked(spawnAsync).mockRejectedValue(
new Error('PowerShell is not installed'),
);
const result = await isDirectorySecure('C:\\Some\\Path');
expect(result.secure).toBe(false);
expect(result.reason).toBe(
"A security check for the system policy directory 'C:\\Some\\Path' failed and could not be completed. Please file a bug report. Original error: PowerShell is not installed",
);
});
it('returns secure=true if directory does not exist (ENOENT)', async () => {
vi.spyOn(os, 'platform').mockReturnValue('linux');
const error = new Error('ENOENT');
Object.assign(error, { code: 'ENOENT' });
vi.mocked(fs.stat).mockRejectedValue(error);
const result = await isDirectorySecure('/some/path');
expect(result.secure).toBe(true);
});
it('returns secure=false if path is not a directory', async () => {
vi.spyOn(os, 'platform').mockReturnValue('linux');
vi.mocked(fs.stat).mockResolvedValue({
isDirectory: () => false,
uid: 0,
mode: 0o700,
} as unknown as Stats);
const result = await isDirectorySecure('/some/file');
expect(result.secure).toBe(false);
expect(result.reason).toBe('Not a directory');
});
it('returns secure=false if not owned by root (uid 0) on POSIX', async () => {
vi.spyOn(os, 'platform').mockReturnValue('linux');
vi.mocked(fs.stat).mockResolvedValue({
isDirectory: () => true,
uid: 1000, // Non-root
mode: 0o755,
} as unknown as Stats);
const result = await isDirectorySecure('/some/path');
expect(result.secure).toBe(false);
expect(result.reason).toBe(
'Directory \'/some/path\' is not owned by root (uid 0). Current uid: 1000. To fix this, run: sudo chown root:root "/some/path"',
);
});
it('returns secure=false if writable by group (020) on POSIX', async () => {
vi.spyOn(os, 'platform').mockReturnValue('linux');
Object.assign(constants, { S_IWGRP: 0o020, S_IWOTH: 0 });
vi.mocked(fs.stat).mockResolvedValue({
isDirectory: () => true,
uid: 0,
mode: 0o775, // rwxrwxr-x (group writable)
} as unknown as Stats);
const result = await isDirectorySecure('/some/path');
expect(result.secure).toBe(false);
expect(result.reason).toBe(
'Directory \'/some/path\' is writable by group or others (mode: 775). To fix this, run: sudo chmod g-w,o-w "/some/path"',
);
});
it('returns secure=false if writable by others (002) on POSIX', async () => {
vi.spyOn(os, 'platform').mockReturnValue('linux');
Object.assign(constants, { S_IWGRP: 0, S_IWOTH: 0o002 });
vi.mocked(fs.stat).mockResolvedValue({
isDirectory: () => true,
uid: 0,
mode: 0o757, // rwxr-xrwx (others writable)
} as unknown as Stats);
const result = await isDirectorySecure('/some/path');
expect(result.secure).toBe(false);
expect(result.reason).toBe(
'Directory \'/some/path\' is writable by group or others (mode: 757). To fix this, run: sudo chmod g-w,o-w "/some/path"',
);
});
it('returns secure=true if owned by root and secure permissions on POSIX', async () => {
vi.spyOn(os, 'platform').mockReturnValue('linux');
Object.assign(constants, { S_IWGRP: 0, S_IWOTH: 0 });
vi.mocked(fs.stat).mockResolvedValue({
isDirectory: () => true,
uid: 0,
mode: 0o755, // rwxr-xr-x
} as unknown as Stats);
const result = await isDirectorySecure('/some/path');
expect(result.secure).toBe(true);
});
});
-107
View File
@@ -1,107 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import * as fs from 'node:fs/promises';
import { constants } from 'node:fs';
import * as os from 'node:os';
import { spawnAsync } from './shell-utils.js';
export interface SecurityCheckResult {
secure: boolean;
reason?: string;
}
/**
* Verifies if a directory is secure (owned by root and not writable by others).
*
* @param dirPath The path to the directory to check.
* @returns A promise that resolves to a SecurityCheckResult.
*/
export async function isDirectorySecure(
dirPath: string,
): Promise<SecurityCheckResult> {
try {
const stats = await fs.stat(dirPath);
if (!stats.isDirectory()) {
return { secure: false, reason: 'Not a directory' };
}
if (os.platform() === 'win32') {
try {
// Check ACLs using PowerShell to ensure standard users don't have write access
const escapedPath = dirPath.replace(/'/g, "''");
const script = `
$path = '${escapedPath}';
$acl = Get-Acl -LiteralPath $path;
$rules = $acl.Access | Where-Object {
$_.AccessControlType -eq 'Allow' -and
(($_.FileSystemRights -match 'Write') -or ($_.FileSystemRights -match 'Modify') -or ($_.FileSystemRights -match 'FullControl'))
};
$insecureIdentity = $rules | Where-Object {
$_.IdentityReference.Value -match 'Users' -or $_.IdentityReference.Value -eq 'Everyone'
} | Select-Object -ExpandProperty IdentityReference;
Write-Output ($insecureIdentity -join ', ');
`;
const { stdout } = await spawnAsync('powershell', [
'-NoProfile',
'-NonInteractive',
'-Command',
script,
]);
const insecureGroups = stdout.trim();
if (insecureGroups) {
return {
secure: false,
reason: `Directory '${dirPath}' is insecure. The following user groups have write permissions: ${insecureGroups}. To fix this, remove Write and Modify permissions for these groups from the directory's ACLs.`,
};
}
return { secure: true };
} catch (error) {
return {
secure: false,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
reason: `A security check for the system policy directory '${dirPath}' failed and could not be completed. Please file a bug report. Original error: ${(error as Error).message}`,
};
}
}
// POSIX checks
// Check ownership: must be root (uid 0)
if (stats.uid !== 0) {
return {
secure: false,
reason: `Directory '${dirPath}' is not owned by root (uid 0). Current uid: ${stats.uid}. To fix this, run: sudo chown root:root "${dirPath}"`,
};
}
// Check permissions: not writable by group (S_IWGRP) or others (S_IWOTH)
const mode = stats.mode;
if ((mode & (constants.S_IWGRP | constants.S_IWOTH)) !== 0) {
return {
secure: false,
reason: `Directory '${dirPath}' is writable by group or others (mode: ${mode.toString(
8,
)}). To fix this, run: sudo chmod g-w,o-w "${dirPath}"`,
};
}
return { secure: true };
} catch (error) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return { secure: true };
}
return {
secure: false,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
reason: `Failed to access directory: ${(error as Error).message}`,
};
}
}