Compare commits

..

62 Commits

Author SHA1 Message Date
Dmitry Lyalin 32526523bb fix(cli): keep plan-exit mode choice user-controlled 2026-02-12 23:51:17 -05:00
Dmitry Lyalin 5ab0c1e31b feat(cli): add deep work completion summary stats 2026-02-12 23:36:31 -05:00
Dmitry Lyalin c57b55fa03 feat(cli): add deep work mode with integrated execution flow 2026-02-12 23:21:11 -05:00
Tommaso Sciortino d82f66973f Fix drag and drop escaping (#18965) 2026-02-13 02:27:56 +00:00
Abhi 00f73b73bc refactor(cli): finalize event-driven transition and remove interaction bridge (#18569) 2026-02-13 02:14:35 +00:00
Aishanee Shah b62c6566be refactor(core): centralize tool definitions (Group 1: replace, search, grep) (#18944) 2026-02-13 02:05:33 +00:00
g-samroberts ca374dcf47 Update installation guide (#18823) 2026-02-13 01:21:10 +00:00
Allen Hutchison 696198be87 feat(policy): add --policy flag for user defined policies (#18500)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-02-13 00:25:23 +00:00
Philippe 5b4884692b fix(vim): vim support that feels (more) complete (#18755)
Co-authored-by: Tommaso Sciortino <sciortino@gmail.com>
2026-02-13 00:22:39 +00:00
Jacob Richman 55ec0f043c feat(cli): disable ctrl-s shortcut outside of alternate buffer mode (#18887) 2026-02-12 23:00:13 +00:00
Shreya Keshive 44bcba323f feat(admin): Add admin controls documentation (#18644) 2026-02-12 22:39:42 +00:00
Jacob Richman c00de442e5 bug(cli) fix flicker due to AppContainer continuous initialization (#18958) 2026-02-12 22:16:46 +00:00
Jerop Kipruto 8275871963 Hide AskUser tool validation errors from UI (agent self-corrects) (#18954) 2026-02-12 21:49:07 +00:00
Jerop Kipruto e8e681c670 feat(ui): align AskUser color scheme with UX spec (#18943) 2026-02-12 21:10:25 +00:00
christine betts 991b2c6002 Add generic searchable list to back settings and extensions (#18838) 2026-02-12 20:50:41 +00:00
Jacob Richman 207ac6f2dc ui(polish) blend background color with theme (#18802) 2026-02-12 19:56:07 +00:00
Dmitry Lyalin db00c5abf3 feat(cli): prototype clean UI toggle and minimal-mode bleed-through (#18683) 2026-02-12 19:25:24 +00:00
Jack Wotherspoon b0cfbc6cd8 fix: character truncation in raw markdown mode (#18938) 2026-02-12 19:16:56 +00: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
Pyush Sinha b8008695db refactor(cli): Reactive useSettingsStore hook (#14915) 2026-02-11 23:40:27 +00:00
Christian Gunderman 6c1773170e More grep prompt tweaks (#18846) 2026-02-11 21:55:27 +00:00
Dev Randalpura 00966062b8 Removed getPlainTextLength (#18848) 2026-02-11 21:47:02 +00:00
Adam Weidman 4138667bae feat(a2a): add value-resolver for auth credential resolution (#18653) 2026-02-11 21:23:28 +00:00
Sandy Tao bfa791e13d feat(core): update internal utility models to Gemini 3 (#18773) 2026-02-11 20:20:14 +00:00
Adib234 e9a9474810 Revert unintended credentials exposure (#18840) 2026-02-11 20:06:28 +00:00
Jerop Kipruto 02adfe2bca docs(plan): add ask_user tool documentation (#18830) 2026-02-11 20:04:01 +00:00
Christian Gunderman 2a08456ed0 Update prompt and grep tool definition to limit context size (#18780) 2026-02-11 19:20:51 +00:00
christine betts 2dac98dc8d Remove experimental note in extension settings docs (#18822) 2026-02-11 19:17:51 +00:00
Jerop Kipruto 77849cadac docs(plan): add documentation for plan mode tools (#18827) 2026-02-11 18:53:43 +00:00
Adib234 84ce53aafa feat(plan): allow skills to be enabled in plan mode (#18817)
Co-authored-by: Jerop Kipruto <jerop@google.com>
2026-02-11 17:59:03 +00:00
Sandy Tao b19b026f30 fix(cli): update F12 behavior to only open drawer if browser fails (#18829) 2026-02-11 17:45:43 +00:00
Jacob Richman eb9223b6a4 Fix pressing any key to exit select mode. (#18421) 2026-02-11 17:38:01 +00:00
Jerop Kipruto 65d26e73a2 feat(plan): document and validate Plan Mode policy overrides (#18825) 2026-02-11 17:32:02 +00:00
Brad Dux 0080589939 fix(cli): resolve double rendering in shpool and address vscode lint warnings (#18704) 2026-02-11 17:29:18 +00:00
Sehoon Shon 34a47a51f4 fix(core): cache CLI version to ensure consistency during sessions (#18793) 2026-02-11 17:01:50 +00:00
Dmitry Lyalin f5dd1068f6 fix(core): complete MCP discovery when configured servers are skipped (#18586)
Co-authored-by: christine betts <chrstn@uw.edu>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-02-11 16:30:46 +00:00
matt korwel c202cf3145 perf(cli): truncate large debug logs and limit message history (#18663) 2026-02-11 15:17:01 +00:00
Jack Wotherspoon 5baad108d9 feat: multi-line text answers in ask-user tool (#18741) 2026-02-11 14:14:53 +00:00
Dev Randalpura 63e9d5d15f perf(ui): optimize table rendering by memoizing styled characters (#18770) 2026-02-11 13:10:30 +00:00
Abhi 3776c4d613 feat(masking): enable tool output masking by default (#18564) 2026-02-11 06:21:55 +00:00
Sandy Tao 2af5a3a01e fix(cli): allow closing debug console after auto-open via flicker (#18795) 2026-02-11 05:37:23 +00:00
Christian Gunderman 0d034b8c18 Introduce limits for search results. (#18767) 2026-02-11 03:50:10 +00:00
Jerop Kipruto 49d55d972e feat(core): formalize 5-phase sequential planning workflow (#18759) 2026-02-11 03:02:20 +00:00
Dmitry Lyalin 7a02d7261a feat(cli): add setting to hide shortcuts hint UI (#18562) 2026-02-11 02:46:20 +00:00
Jerop Kipruto 9c11ff2d58 test(evals): mark all save_memory evals as USUALLY_PASSES due to unreliability (#18786) 2026-02-11 02:16:52 +00:00
258 changed files with 14584 additions and 6282 deletions
-3
View File
@@ -1,8 +1,5 @@
{
"experimental": {
"toolOutputMasking": {
"enabled": true
},
"plan": true
},
"general": {
+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`.
+3 -4
View File
@@ -29,10 +29,9 @@ Learn all about Gemini CLI in our [documentation](https://geminicli.com/docs/).
## 📦 Installation
### Pre-requisites before installation
- Node.js version 20 or higher
- macOS, Linux, or Windows
See
[Gemini CLI installation, execution, and releases](./docs/get-started/installation.md)
for recommended system specifications and a detailed installation guide.
### Quick Install
+115
View File
@@ -0,0 +1,115 @@
# Enterprise Admin Controls
Gemini CLI empowers enterprise administrators to manage and enforce security
policies and configuration settings across their entire organization. Secure
defaults are enabled automatically for all enterprise users, but can be
customized via the [Management Console](https://goo.gle/manage-gemini-cli).
**Enterprise Admin Controls are enforced globally and cannot be overridden by
users locally**, ensuring a consistent security posture.
## Admin Controls vs. System Settings
While [System-wide settings](../cli/settings.md) act as convenient configuration
overrides, they can still be modified by users with sufficient privileges. In
contrast, admin controls are immutable at the local level, making them the
preferred method for enforcing policy.
## Available Controls
### Strict Mode
**Enabled/Disabled** | Default: enabled
If enabled, users will not be able to enter yolo mode.
### Extensions
**Enabled/Disabled** | Default: disabled
If disabled, users will not be able to use or install extensions. See
[Extensions](../extensions/index.md) for more details.
### MCP
#### Enabled/Disabled
**Enabled/Disabled** | Default: disabled
If disabled, users will not be able to use MCP servers. See
[MCP Server Integration](../tools/mcp-server.md) for more details.
#### MCP Servers (preview)
**Default**: empty
Allows administrators to define an explicit allowlist of MCP servers. This
guarantees that users can only connect to trusted MCP servers defined by the
organization.
**Allowlist Format:**
```json
{
"mcpServers": {
"external-provider": {
"url": "https://api.mcp-provider.com",
"type": "sse",
"trust": true,
"includeTools": ["toolA", "toolB"],
"excludeTools": []
},
"internal-corp-tool": {
"url": "https://mcp.internal-tool.corp",
"type": "http",
"includeTools": [],
"excludeTools": ["adminTool"]
}
}
}
```
**Supported Fields:**
- `url`: (Required) The full URL of the MCP server endpoint.
- `type`: (Required) The connection type (e.g., `sse` or `http`).
- `trust`: (Optional) If set to `true`, the server is trusted and tool execution
will not require user approval.
- `includeTools`: (Optional) An explicit list of tool names to allow. If
specified, only these tools will be available.
- `excludeTools`: (Optional) A list of tool names to hide. These tools will be
blocked.
**Client Enforcement Logic:**
- **Empty Allowlist**: If the admin allowlist is empty, the client uses the
users local configuration as is (unless the MCP toggle above is disabled).
- **Active Allowlist**: If the allowlist contains one or more servers, **all
locally configured servers not present in the allowlist are ignored**.
- **Configuration Merging**: For a server to be active, it must exist in
**both** the admin allowlist and the users local configuration (matched by
name). The client merges these definitions as follows:
- **Override Fields**: The `url`, `type`, & `trust` are always taken from the
admin allowlist, overriding any local values.
- **Tools Filtering**: If `includeTools` or `excludeTools` are defined in the
allowlist, the admins rules are used exclusively. If both are undefined in
the admin allowlist, the client falls back to the users local tool
settings.
- **Cleared Fields**: To ensure security and consistency, the client
automatically clears local execution fields (`command`, `args`, `env`,
`cwd`, `httpUrl`, `tcp`). This prevents users from overriding the connection
method.
- **Other Fields**: All other MCP fields are pulled from the users local
configuration.
- **Missing Allowlisted Servers**: If a server appears in the admin allowlist
but is missing from the local configuration, it will not be initialized. This
ensures users maintain final control over which permitted servers are actually
active in their environment.
### Unmanaged Capabilities
**Enabled/Disabled** | Default: disabled
If disabled, users will not be able to use certain features. Currently, this
control disables Agent Skills. See [Agent Skills](../cli/skills.md) for more
details.
+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
+2
View File
@@ -120,6 +120,8 @@ Slash commands provide meta-level control over the CLI itself.
- **`/shortcuts`**
- **Description:** Toggle the shortcuts panel above the input.
- **Shortcut:** Press `?` when the prompt is empty.
- **Note:** This is separate from the clean UI detail toggle on double-`Tab`,
which switches between minimal and full UI chrome.
- **`/hooks`**
- **Description:** Manage hooks, which allow you to intercept and customize
+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.
+40 -33
View File
@@ -8,12 +8,12 @@ available combinations.
#### Basic Controls
| Action | Keys |
| --------------------------------------------------------------- | ---------- |
| Confirm the current selection or choice. | `Enter` |
| Dismiss dialogs or cancel the current focus. | `Esc` |
| Cancel the current request or quit the CLI when input is empty. | `Ctrl + C` |
| Exit the CLI when the input buffer is empty. | `Ctrl + D` |
| Action | Keys |
| --------------------------------------------------------------- | --------------------- |
| Confirm the current selection or choice. | `Enter` |
| Dismiss dialogs or cancel the current focus. | `Esc`<br />`Ctrl + [` |
| Cancel the current request or quit the CLI when input is empty. | `Ctrl + C` |
| Exit the CLI when the input buffer is empty. | `Ctrl + D` |
#### Cursor Movement
@@ -96,31 +96,31 @@ available combinations.
#### App Controls
| Action | Keys |
| ----------------------------------------------------------------------------------------------------- | -------------------------- |
| Toggle detailed error information. | `F12` |
| Toggle the full TODO list. | `Ctrl + T` |
| Show IDE context details. | `Ctrl + G` |
| Toggle Markdown rendering. | `Alt + M` |
| Toggle copy mode when in alternate buffer mode. | `Ctrl + S` |
| Toggle YOLO (auto-approval) mode for tool calls. | `Ctrl + Y` |
| Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only). | `Shift + Tab` |
| Expand a height-constrained response to show additional lines when not in alternate buffer mode. | `Ctrl + O`<br />`Ctrl + S` |
| Expand or collapse a paste placeholder when cursor is over placeholder. | `Ctrl + O` |
| Toggle current background shell visibility. | `Ctrl + B` |
| Toggle background shell list. | `Ctrl + L` |
| Kill the active background shell. | `Ctrl + K` |
| Confirm selection in background shell list. | `Enter` |
| Dismiss background shell list. | `Esc` |
| Move focus from background shell to Gemini. | `Shift + Tab` |
| Move focus from background shell list to Gemini. | `Tab (no Shift)` |
| Show warning when trying to unfocus background shell via Tab. | `Tab (no Shift)` |
| Show warning when trying to unfocus shell input via Tab. | `Tab (no Shift)` |
| Move focus from Gemini to the active shell. | `Tab (no Shift)` |
| 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` |
| Action | Keys |
| ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------- |
| Toggle detailed error information. | `F12` |
| Toggle the full TODO list. | `Ctrl + T` |
| Show IDE context details. | `Ctrl + G` |
| Toggle Markdown rendering. | `Alt + M` |
| Toggle copy mode when in alternate buffer mode. | `Ctrl + S` |
| Toggle YOLO (auto-approval) mode for tool calls. | `Ctrl + Y` |
| Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), plan (read-only), and deep_work (iterative; when enabled). | `Shift + Tab` |
| Expand and collapse blocks of content when not in alternate buffer mode. | `Ctrl + O` |
| Expand or collapse a paste placeholder when cursor is over placeholder. | `Ctrl + O` |
| Toggle current background shell visibility. | `Ctrl + B` |
| Toggle background shell list. | `Ctrl + L` |
| Kill the active background shell. | `Ctrl + K` |
| Confirm selection in background shell list. | `Enter` |
| Dismiss background shell list. | `Esc` |
| Move focus from background shell to Gemini. | `Shift + Tab` |
| Move focus from background shell list to Gemini. | `Tab (no Shift)` |
| Show warning when trying to move focus away from background shell. | `Tab (no Shift)` |
| Show warning when trying to move focus away from shell input. | `Tab (no Shift)` |
| Move focus from Gemini to the active shell. | `Tab (no Shift)` |
| 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 CLI and move it to the background. | `Ctrl + Z` |
<!-- KEYBINDINGS-AUTOGEN:END -->
@@ -130,8 +130,15 @@ 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.
`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.
- `Tab` + `Tab` (while typing in the prompt): Toggle between minimal and full UI
details when no completion/search interaction is active. The selected mode is
remembered for future sessions. Full UI remains the default on first run, and
single `Tab` keeps its existing completion/focus behavior.
- `Shift + Tab` (while typing in the prompt): Cycle approval modes.
- `\` (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,
+88 -8
View File
@@ -30,6 +30,8 @@ implementation strategy.
- [The Planning Workflow](#the-planning-workflow)
- [Exiting Plan Mode](#exiting-plan-mode)
- [Tool Restrictions](#tool-restrictions)
- [Customizing Planning with Skills](#customizing-planning-with-skills)
- [Customizing Policies](#customizing-policies)
## Starting in Plan Mode
@@ -61,15 +63,18 @@ You can enter Plan Mode in three ways:
1. **Keyboard Shortcut:** Press `Shift+Tab` to cycle through approval modes
(`Default` -> `Plan` -> `Auto-Edit`).
2. **Command:** Type `/plan` in the input box.
3. **Natural Language:** Ask the agent to "start a plan for...".
3. **Natural Language:** Ask the agent to "start a plan for...". The agent will
then call the [`enter_plan_mode`] tool to switch modes.
### The Planning Workflow
1. **Requirements:** The agent clarifies goals using `ask_user`.
1. **Requirements:** The agent clarifies goals using [`ask_user`].
2. **Exploration:** The agent uses read-only tools (like [`read_file`]) to map
the codebase and validate assumptions.
3. **Planning:** A detailed plan is written to a temporary Markdown file.
4. **Review:** You review the plan.
3. **Design:** The agent proposes alternative approaches with a recommended
solution for you to choose from.
4. **Planning:** A detailed plan is written to a temporary Markdown file.
5. **Review:** You review the plan.
- **Approve:** Exit Plan Mode and start implementation (switching to
Auto-Edit or Default approval mode).
- **Iterate:** Provide feedback to refine the plan.
@@ -79,8 +84,8 @@ You can enter Plan Mode in three ways:
To exit Plan Mode:
1. **Keyboard Shortcut:** Press `Shift+Tab` to cycle to the desired mode.
1. **Tool:** The agent calls the `exit_plan_mode` tool to present the finalized
plan for your approval.
2. **Tool:** The agent calls the [`exit_plan_mode`] tool to present the
finalized plan for your approval.
## Tool Restrictions
@@ -90,11 +95,80 @@ These are the only allowed tools:
- **FileSystem (Read):** [`read_file`], [`list_directory`], [`glob`]
- **Search:** [`grep_search`], [`google_web_search`]
- **Interaction:** `ask_user`
- **Interaction:** [`ask_user`]
- **MCP Tools (Read):** Read-only [MCP tools] (e.g., `github_read_issue`,
`postgres_read_schema`) are allowed.
- **Planning (Write):** [`write_file`] and [`replace`] ONLY allowed for `.md`
files in the `~/.gemini/tmp/<project>/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)
### Customizing Planning with Skills
You can leverage [Agent Skills](./skills.md) to customize how Gemini CLI
approaches planning for specific types of tasks. When a skill is activated
during Plan Mode, its specialized instructions and procedural workflows will
guide the research and design phases.
For example:
- A **"Database Migration"** skill could ensure the plan includes data safety
checks and rollback strategies.
- A **"Security Audit"** skill could prompt the agent to look for specific
vulnerabilities during codebase exploration.
- A **"Frontend Design"** skill could guide the agent to use specific UI
components and accessibility standards in its proposal.
To use a skill in Plan Mode, you can explicitly ask the agent to "use the
[skill-name] skill to plan..." or the agent may autonomously activate it based
on the task description.
### Customizing Policies
Plan Mode is designed to be read-only by default to ensure safety during the
research phase. However, you may occasionally need to allow specific tools to
assist in your planning.
Because user policies (Tier 2) have a higher base priority than built-in
policies (Tier 1), you can override Plan Mode's default restrictions by creating
a rule in your `~/.gemini/policies/` directory.
#### Example: Allow `git status` and `git diff` in Plan Mode
This rule allows you to check the repository status and see changes while in
Plan Mode.
`~/.gemini/policies/git-research.toml`
```toml
[[rule]]
toolName = "run_shell_command"
commandPrefix = ["git status", "git diff"]
decision = "allow"
priority = 100
modes = ["plan"]
```
#### Example: Enable research sub-agents in Plan Mode
You can enable [experimental research sub-agents] like `codebase_investigator`
to help gather architecture details during the planning phase.
`~/.gemini/policies/research-subagents.toml`
```toml
[[rule]]
toolName = "codebase_investigator"
decision = "allow"
priority = 100
modes = ["plan"]
```
Tell the agent it can use these tools in your prompt, for example: _"You can
check ongoing changes in git."_
For more information on how the policy engine works, see the [Policy Engine
Guide].
[`list_directory`]: /docs/tools/file-system.md#1-list_directory-readfolder
[`read_file`]: /docs/tools/file-system.md#2-read_file-readfile
@@ -104,3 +178,9 @@ These are the only allowed tools:
[`google_web_search`]: /docs/tools/web-search.md
[`replace`]: /docs/tools/file-system.md#6-replace-edit
[MCP tools]: /docs/tools/mcp-server.md
[`activate_skill`]: /docs/cli/skills.md
[experimental research sub-agents]: /docs/core/subagents.md
[Policy Engine Guide]: /docs/core/policy-engine.md
[`enter_plan_mode`]: /docs/tools/planning.md#1-enter_plan_mode-enterplanmode
[`exit_plan_mode`]: /docs/tools/planning.md#2-exit_plan_mode-exitplanmode
[`ask_user`]: /docs/tools/ask-user.md
+3 -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
+15 -12
View File
@@ -22,14 +22,14 @@ they appear in the UI.
### General
| UI Label | Setting | Description | Default |
| ------------------------ | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- |
| Vim Mode | `general.vimMode` | Enable Vim keybindings | `false` |
| Default Approval Mode | `general.defaultApprovalMode` | The default approval mode for tool execution. 'default' prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is read-only mode. 'yolo' is not supported yet. | `"default"` |
| Enable Auto Update | `general.enableAutoUpdate` | Enable automatic updates. | `true` |
| Enable Prompt Completion | `general.enablePromptCompletion` | Enable AI-powered prompt completion suggestions while typing. | `false` |
| Debug Keystroke Logging | `general.debugKeystrokeLogging` | Enable debug logging of keystrokes to the console. | `false` |
| Enable Session Cleanup | `general.sessionRetention.enabled` | Enable automatic session cleanup | `false` |
| UI Label | Setting | Description | Default |
| ------------------------ | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
| Vim Mode | `general.vimMode` | Enable Vim keybindings | `false` |
| Default Approval Mode | `general.defaultApprovalMode` | The default approval mode for tool execution. 'default' prompts for approval, 'auto_edit' auto-approves edit tools, 'plan' is read-only mode, and 'deep_work' is iterative execution mode. 'yolo' is not supported yet. | `"default"` |
| Enable Auto Update | `general.enableAutoUpdate` | Enable automatic updates. | `true` |
| Enable Prompt Completion | `general.enablePromptCompletion` | Enable AI-powered prompt completion suggestions while typing. | `false` |
| Debug Keystroke Logging | `general.debugKeystrokeLogging` | Enable debug logging of keystrokes to the console. | `false` |
| Enable Session Cleanup | `general.sessionRetention.enabled` | Enable automatic session cleanup | `false` |
### Output
@@ -49,6 +49,7 @@ they appear in the UI.
| Dynamic Window Title | `ui.dynamicWindowTitle` | Update the terminal window title with current status icons (Ready: ◇, Action Required: ✋, Working: ✦) | `true` |
| Show Home Directory Warning | `ui.showHomeDirectoryWarning` | Show a warning when running Gemini CLI in the home directory. | `true` |
| Hide Tips | `ui.hideTips` | Hide helpful tips in the UI | `false` |
| Show Shortcuts Hint | `ui.showShortcutsHint` | Show the "? for shortcuts" hint above the input. | `true` |
| Hide Banner | `ui.hideBanner` | Hide the application banner | `false` |
| Hide Context Summary | `ui.hideContextSummary` | Hide the context summary (GEMINI.md, MCP servers) above the input. | `false` |
| Hide CWD | `ui.footer.hideCWD` | Hide the current working directory path in the footer. | `false` |
@@ -124,10 +125,12 @@ they appear in the UI.
### Experimental
| UI Label | Setting | Description | Default |
| ---------------- | ---------------------------- | ----------------------------------------------------------------------------------- | ------- |
| Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 sequence for pasting instead of clipboardy (useful for remote sessions). | `false` |
| Plan | `experimental.plan` | Enable planning features (Plan Mode and tools). | `false` |
| UI Label | Setting | Description | Default |
| -------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------- | ------- |
| Enable Tool Output Masking | `experimental.toolOutputMasking.enabled` | Enables tool output masking to save tokens. | `true` |
| Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 sequence for pasting instead of clipboardy (useful for remote sessions). | `false` |
| Plan | `experimental.plan` | Enable planning features (Plan Mode and tools). | `false` |
| Deep Work | `experimental.deepWork` | Enable Deep Work mode (iterative execution mode and tools). | `false` |
### Skills
+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**:
+13 -3
View File
@@ -119,9 +119,17 @@ For example:
Approval modes allow the policy engine to apply different sets of rules based on
the CLI's operational mode. A rule can be associated with one or more modes
(e.g., `yolo`, `autoEdit`). The rule will only be active if the CLI is running
in one of its specified modes. If a rule has no modes specified, it is always
active.
(e.g., `yolo`, `autoEdit`, `plan`). The rule will only be active if the CLI is
running in one of its specified modes. If a rule has no modes specified, it is
always active.
- `default`: The standard interactive mode where most write tools require
confirmation.
- `autoEdit`: Optimized for automated code editing; some write tools may be
auto-approved.
- `plan`: A strict, read-only mode for research and design. See [Customizing
Plan Mode Policies].
- `yolo`: A mode where all tools are auto-approved (use with extreme caution).
## Rule matching
@@ -303,3 +311,5 @@ out-of-the-box experience.
- In **`yolo`** mode, a high-priority rule allows all tools.
- In **`autoEdit`** mode, rules allow certain write operations to happen without
prompting.
[Customizing Plan Mode Policies]: /docs/cli/plan-mode.md#customizing-policies
-3
View File
@@ -179,9 +179,6 @@ precedence.
### Settings
_Note: This is an experimental feature. We do not yet recommend extension
authors introduce settings as part of their core flows._
Extensions can define settings that the user will be prompted to provide upon
installation. This is useful for things like API keys, URLs, or other
configuration that the extension needs to function.
+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
+60 -12
View File
@@ -96,6 +96,13 @@ their corresponding top-level category object in your `settings.json` file.
<!-- SETTINGS-AUTOGEN:START -->
#### `policyPaths`
- **`policyPaths`** (array):
- **Description:** Additional policy files or directories to load.
- **Default:** `[]`
- **Requires restart:** Yes
#### `general`
- **`general.preferredEditor`** (string):
@@ -108,10 +115,11 @@ their corresponding top-level category object in your `settings.json` file.
- **`general.defaultApprovalMode`** (enum):
- **Description:** The default approval mode for tool execution. 'default'
prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is
read-only mode. 'yolo' is not supported yet.
prompts for approval, 'auto_edit' auto-approves edit tools, 'plan' is
read-only mode, and 'deep_work' is iterative execution mode. 'yolo' is not
supported yet.
- **Default:** `"default"`
- **Values:** `"default"`, `"auto_edit"`, `"plan"`
- **Values:** `"default"`, `"auto_edit"`, `"plan"`, `"deep_work"`
- **`general.devtools`** (boolean):
- **Description:** Enable DevTools inspector on launch.
@@ -220,6 +228,10 @@ their corresponding top-level category object in your `settings.json` file.
- **Description:** Hide helpful tips in the UI
- **Default:** `false`
- **`ui.showShortcutsHint`** (boolean):
- **Description:** Show the "? for shortcuts" hint above the input.
- **Default:** `true`
- **`ui.hideBanner`** (boolean):
- **Description:** Hide the application banner
- **Default:** `false`
@@ -443,6 +455,12 @@ their corresponding top-level category object in your `settings.json` file.
"model": "gemini-2.5-flash"
}
},
"gemini-3-flash-base": {
"extends": "base",
"modelConfig": {
"model": "gemini-3-flash-preview"
}
},
"classifier": {
"extends": "base",
"modelConfig": {
@@ -498,7 +516,7 @@ their corresponding top-level category object in your `settings.json` file.
}
},
"web-search": {
"extends": "gemini-2.5-flash-base",
"extends": "gemini-3-flash-base",
"modelConfig": {
"generateContentConfig": {
"tools": [
@@ -510,7 +528,7 @@ their corresponding top-level category object in your `settings.json` file.
}
},
"web-fetch": {
"extends": "gemini-2.5-flash-base",
"extends": "gemini-3-flash-base",
"modelConfig": {
"generateContentConfig": {
"tools": [
@@ -522,25 +540,25 @@ their corresponding top-level category object in your `settings.json` file.
}
},
"web-fetch-fallback": {
"extends": "gemini-2.5-flash-base",
"extends": "gemini-3-flash-base",
"modelConfig": {}
},
"loop-detection": {
"extends": "gemini-2.5-flash-base",
"extends": "gemini-3-flash-base",
"modelConfig": {}
},
"loop-detection-double-check": {
"extends": "base",
"modelConfig": {
"model": "gemini-2.5-pro"
"model": "gemini-3-pro-preview"
}
},
"llm-edit-fixer": {
"extends": "gemini-2.5-flash-base",
"extends": "gemini-3-flash-base",
"modelConfig": {}
},
"next-speaker-checker": {
"extends": "gemini-2.5-flash-base",
"extends": "gemini-3-flash-base",
"modelConfig": {}
},
"chat-compression-3-pro": {
@@ -570,7 +588,7 @@ their corresponding top-level category object in your `settings.json` file.
},
"chat-compression-default": {
"modelConfig": {
"model": "gemini-2.5-pro"
"model": "gemini-3-pro-preview"
}
}
}
@@ -848,6 +866,28 @@ their corresponding top-level category object in your `settings.json` file.
#### `experimental`
- **`experimental.toolOutputMasking.enabled`** (boolean):
- **Description:** Enables tool output masking to save tokens.
- **Default:** `true`
- **Requires restart:** Yes
- **`experimental.toolOutputMasking.toolProtectionThreshold`** (number):
- **Description:** Minimum number of tokens to protect from masking (most
recent tool outputs).
- **Default:** `50000`
- **Requires restart:** Yes
- **`experimental.toolOutputMasking.minPrunableTokensThreshold`** (number):
- **Description:** Minimum prunable tokens required to trigger a masking pass.
- **Default:** `30000`
- **Requires restart:** Yes
- **`experimental.toolOutputMasking.protectLatestTurn`** (boolean):
- **Description:** Ensures the absolute latest turn is never masked,
regardless of token count.
- **Default:** `true`
- **Requires restart:** Yes
- **`experimental.enableAgents`** (boolean):
- **Description:** Enable local and remote subagents. Warning: Experimental
feature, uses YOLO mode for subagents
@@ -889,6 +929,11 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `false`
- **Requires restart:** Yes
- **`experimental.deepWork`** (boolean):
- **Description:** Enable Deep Work mode (iterative execution mode and tools).
- **Default:** `false`
- **Requires restart:** Yes
#### `skills`
- **`skills.enabled`** (boolean):
@@ -1258,7 +1303,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`).
+112 -79
View File
@@ -1,43 +1,98 @@
# Gemini CLI installation, execution, and deployment
# Gemini CLI installation, execution, and releases
Install and run Gemini CLI. This document provides an overview of Gemini CLI's
installation methods and deployment architecture.
This document provides an overview of Gemini CLI's sytem requriements,
installation methods, and release types.
## How to install and/or run Gemini CLI
## Recommended system specifications
There are several ways to run Gemini CLI. The recommended option depends on how
you intend to use Gemini CLI.
- **Operating System:**
- macOS 15+
- Windows 11 24H2+
- Ubuntu 20.04+
- **Hardware:**
- "Casual" usage: 4GB+ RAM (short sessions, common tasks and edits)
- "Power" usage: 16GB+ RAM (long sessions, large codebases, deep context)
- **Runtime:** Node.js 20.0.0+
- **Shell:** Bash or Zsh
- **Location:**
[Gemini Code Assist supported locations](https://developers.google.com/gemini-code-assist/resources/available-locations#americas)
- **Internet connection required**
- As a standard installation. This is the most straightforward method of using
Gemini CLI.
## Install Gemini CLI
We recommend most users install Gemini CLI using one of the following
installation methods:
- npm
- Homebrew
- MacPorts
- Anaconda
Note that Gemini CLI comes pre-installed on
[**Cloud Shell**](https://docs.cloud.google.com/shell/docs) and
[**Cloud Workstations**](https://cloud.google.com/workstations).
### Install globally with npm
```bash
npm install -g @google/gemini-cli
```
### Install globally with Homebrew (macOS/Linux)
```bash
brew install gemini-cli
```
### Install globally with MacPorts (macOS)
```bash
sudo port install gemini-cli
```
### Install with Anaconda (for restricted environments)
```bash
# Create and activate a new environment
conda create -y -n gemini_env -c conda-forge nodejs
conda activate gemini_env
# Install Gemini CLI globally via npm (inside the environment)
npm install -g @google/gemini-cli
```
## Run Gemini CLI
For most users, we recommend running Gemini CLI with the `gemini` command:
```bash
gemini
```
For a list of options and additional commands, see the
[CLI cheatsheet](/docs/cli/cli-reference.md).
You can also run Gemini CLI using one of the following advanced methods:
- Run instantly with npx. You can run Gemini CLI without permanent installation.
- In a sandbox. This method offers increased security and isolation.
- From the source. This is recommended for contributors to the project.
### 1. Standard installation (recommended for standard users)
### Run instantly with npx
This is the recommended way for end-users to install Gemini CLI. It involves
downloading the Gemini CLI package from the NPM registry.
```bash
# Using npx (no installation required)
npx @google/gemini-cli
```
- **Global install:**
You can also execute the CLI directly from the main branch on GitHub, which is
helpful for testing features still in development:
```bash
npm install -g @google/gemini-cli
```
```bash
npx https://github.com/google-gemini/gemini-cli
```
Then, run the CLI from anywhere:
```bash
gemini
```
- **NPX execution:**
```bash
# Execute the latest version from NPM without a global install
npx @google/gemini-cli
```
### 2. Run in a sandbox (Docker/Podman)
### Run in a sandbox (Docker/Podman)
For security and isolation, Gemini CLI can be run inside a container. This is
the default way that the CLI executes tools that might have side effects.
@@ -56,7 +111,7 @@ the default way that the CLI executes tools that might have side effects.
gemini --sandbox -y -p "your prompt here"
```
### 3. Run from source (recommended for Gemini CLI contributors)
### Run from source (recommended for Gemini CLI contributors)
Contributors to the project will want to run the CLI directly from the source
code.
@@ -79,63 +134,41 @@ code.
gemini
```
---
## Releases
### 4. Running the latest Gemini CLI commit from GitHub
Gemini CLI has three release channels: nightly, preview, and stable. For most
users, we recommend the stable release, which is the default installation.
You can run the most recently committed version of Gemini CLI directly from the
GitHub repository. This is useful for testing features still in development.
### Stable
New stable releases are published each week. The stable release is the promotion
of last week's `preview` release along with any bug fixes. The stable release
uses `latest` tag, but omitting the tag also installs the latest stable release
by default:
```bash
# Execute the CLI directly from the main branch on GitHub
npx https://github.com/google-gemini/gemini-cli
# Both commands install the latest stable release.
npm install -g @google/gemini-cli
npm install -g @google/gemini-cli@latest
```
## Deployment architecture
### Preview
The execution methods described above are made possible by the following
architectural components and processes:
New preview releases will be published each week. These releases are not fully
vetted and may contain regressions or other outstanding issues. Try out the
preview release by using the `preview` tag:
**NPM packages**
```bash
npm install -g @google/gemini-cli@preview
```
Gemini CLI project is a monorepo that publishes two core packages to the NPM
registry:
### Nightly
- `@google/gemini-cli-core`: The backend, handling logic and tool execution.
- `@google/gemini-cli`: The user-facing frontend.
Nightly releases are published every day. The nightly release includes all
changes from the main branch at time of release. It should be assumed there are
pending validations and issues. You can help test the latest changes by
installing with the `nightly` tag:
These packages are used when performing the standard installation and when
running Gemini CLI from the source.
**Build and packaging processes**
There are two distinct build processes used, depending on the distribution
channel:
- **NPM publication:** For publishing to the NPM registry, the TypeScript source
code in `@google/gemini-cli-core` and `@google/gemini-cli` is transpiled into
standard JavaScript using the TypeScript Compiler (`tsc`). The resulting
`dist/` directory is what gets published in the NPM package. This is a
standard approach for TypeScript libraries.
- **GitHub `npx` execution:** When running the latest version of Gemini CLI
directly from GitHub, a different process is triggered by the `prepare` script
in `package.json`. This script uses `esbuild` to bundle the entire application
and its dependencies into a single, self-contained JavaScript file. This
bundle is created on-the-fly on the user's machine and is not checked into the
repository.
**Docker sandbox image**
The Docker-based execution method is supported by the `gemini-cli-sandbox`
container image. This image is published to a container registry and contains a
pre-installed, global version of Gemini CLI.
## Release process
The release process is automated through GitHub Actions. The release workflow
performs the following actions:
1. Build the NPM packages using `tsc`.
2. Publish the NPM packages to the artifact registry.
3. Create GitHub releases with bundled assets.
```bash
npm install -g @google/gemini-cli@nightly
```
+4
View File
@@ -45,6 +45,10 @@
{ "label": "Checkpointing", "slug": "docs/cli/checkpointing" },
{ "label": "Custom commands", "slug": "docs/cli/custom-commands" },
{ "label": "Enterprise features", "slug": "docs/cli/enterprise" },
{
"label": "Enterprise admin controls",
"slug": "docs/admin/enterprise-controls"
},
{ "label": "Headless mode & scripting", "slug": "docs/cli/headless" },
{ "label": "Sandboxing", "slug": "docs/cli/sandbox" },
{ "label": "System prompt override", "slug": "docs/cli/system-prompt" },
+95
View File
@@ -0,0 +1,95 @@
# Ask User Tool
The `ask_user` tool allows the agent to ask you one or more questions to gather
preferences, clarify requirements, or make decisions. It supports multiple
question types including multiple-choice, free-form text, and Yes/No
confirmation.
## `ask_user` (Ask User)
- **Tool name:** `ask_user`
- **Display name:** Ask User
- **File:** `ask-user.ts`
- **Parameters:**
- `questions` (array of objects, required): A list of 1 to 4 questions to ask.
Each question object has the following properties:
- `question` (string, required): The complete question text.
- `header` (string, required): A short label (max 16 chars) displayed as a
chip/tag (e.g., "Auth", "Database").
- `type` (string, optional): The type of question. Defaults to `'choice'`.
- `'choice'`: Multiple-choice with options (supports multi-select).
- `'text'`: Free-form text input.
- `'yesno'`: Yes/No confirmation.
- `options` (array of objects, optional): Required for `'choice'` type. 2-4
selectable options.
- `label` (string, required): Display text (1-5 words).
- `description` (string, required): Brief explanation.
- `multiSelect` (boolean, optional): For `'choice'` type, allows selecting
multiple options.
- `placeholder` (string, optional): Hint text for input fields.
- **Behavior:**
- Presents an interactive dialog to the user with the specified questions.
- Pauses execution until the user provides answers or dismisses the dialog.
- Returns the user's answers to the model.
- **Output (`llmContent`):** A JSON string containing the user's answers,
indexed by question position (e.g.,
`{"answers":{"0": "Option A", "1": "Some text"}}`).
- **Confirmation:** Yes. The tool inherently involves user interaction.
## Usage Examples
### Multiple Choice Question
```json
{
"questions": [
{
"header": "Database",
"question": "Which database would you like to use?",
"type": "choice",
"options": [
{
"label": "PostgreSQL",
"description": "Powerful, open source object-relational database system."
},
{
"label": "SQLite",
"description": "C-library that implements a SQL database engine."
}
]
}
]
}
```
### Text Input Question
```json
{
"questions": [
{
"header": "Project Name",
"question": "What is the name of your new project?",
"type": "text",
"placeholder": "e.g., my-awesome-app"
}
]
}
```
### Yes/No Question
```json
{
"questions": [
{
"header": "Deploy",
"question": "Do you want to deploy the application now?",
"type": "yesno"
}
]
}
```
+3
View File
@@ -86,6 +86,9 @@ Gemini CLI's built-in tools can be broadly categorized as follows:
information across sessions.
- **[Todo Tool](./todos.md) (`write_todos`):** For managing subtasks of complex
requests.
- **[Planning Tools](./planning.md):** For entering and exiting Plan Mode.
- **[Ask User Tool](./ask-user.md) (`ask_user`):** For gathering user input and
making decisions.
Additionally, these tools incorporate:
+1 -12
View File
@@ -739,21 +739,10 @@ The MCP integration tracks several states:
cautiously and only for servers you completely control
- **Access tokens:** Be security-aware when configuring environment variables
containing API keys or tokens
- **Environment variable redaction:** By default, the Gemini CLI redacts
sensitive environment variables (such as `GEMINI_API_KEY`, `GOOGLE_API_KEY`,
and variables matching patterns like `*TOKEN*`, `*SECRET*`, `*PASSWORD*`) when
spawning MCP servers using the `stdio` transport. This prevents unintended
exposure of your credentials to third-party servers.
- **Explicit environment variables:** If you need to pass a specific environment
variable to an MCP server, you should define it explicitly in the `env`
property of the server configuration in `settings.json`.
- **Sandbox compatibility:** When using sandboxing, ensure MCP servers are
available within the sandbox environment.
available within the sandbox environment
- **Private data:** Using broadly scoped personal access tokens can lead to
information leakage between repositories.
- **Untrusted servers:** Be extremely cautious when adding MCP servers from
untrusted or third-party sources. Malicious servers could attempt to
exfiltrate data or perform unauthorized actions through the tools they expose.
### Performance and resource management
+55
View File
@@ -0,0 +1,55 @@
# Gemini CLI planning tools
Planning tools allow the Gemini model to switch into a safe, read-only "Plan
Mode" for researching and planning complex changes, and to signal the
finalization of a plan to the user.
## 1. `enter_plan_mode` (EnterPlanMode)
`enter_plan_mode` switches the CLI to Plan Mode. This tool is typically called
by the agent when you ask it to "start a plan" using natural language. In this
mode, the agent is restricted to read-only tools to allow for safe exploration
and planning.
- **Tool name:** `enter_plan_mode`
- **Display name:** Enter Plan Mode
- **File:** `enter-plan-mode.ts`
- **Parameters:**
- `reason` (string, optional): A short reason explaining why the agent is
entering plan mode (e.g., "Starting a complex feature implementation").
- **Behavior:**
- Switches the CLI's approval mode to `PLAN`.
- Notifies the user that the agent has entered Plan Mode.
- **Output (`llmContent`):** A message indicating the switch, e.g.,
`Switching to Plan mode.`
- **Confirmation:** Yes. The user is prompted to confirm entering Plan Mode.
## 2. `exit_plan_mode` (ExitPlanMode)
`exit_plan_mode` signals that the planning phase is complete. It presents the
finalized plan to the user and requests approval to start the implementation.
- **Tool name:** `exit_plan_mode`
- **Display name:** Exit Plan Mode
- **File:** `exit-plan-mode.ts`
- **Parameters:**
- `plan_path` (string, required): The path to the finalized Markdown plan
file. This file MUST be located within the project's temporary plans
directory (e.g., `~/.gemini/tmp/<project>/plans/`).
- **Behavior:**
- Validates that the `plan_path` is within the allowed directory and that the
file exists and has content.
- Presents the plan to the user for review.
- If the user approves the plan:
- Switches the CLI's approval mode to the user's chosen approval mode (
`DEFAULT` or `AUTO_EDIT`).
- Marks the plan as approved for implementation.
- If the user rejects the plan:
- Stays in Plan Mode.
- Returns user feedback to the model to refine the plan.
- **Output (`llmContent`):**
- On approval: A message indicating the plan was approved and the new approval
mode.
- On rejection: A message containing the user's feedback.
- **Confirmation:** Yes. Shows the finalized plan and asks for user approval to
proceed with implementation.
+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:
+144
View File
@@ -0,0 +1,144 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect } from 'vitest';
import { evalTest } from './test-helper.js';
/**
* Evals to verify that the agent uses search tools efficiently (frugally)
* by utilizing limiting parameters like `total_max_matches` and `max_matches_per_file`.
* This ensures the agent doesn't flood the context window with unnecessary search results.
*/
describe('Frugal Search', () => {
const getGrepParams = (call: any): any => {
let args = call.toolRequest.args;
if (typeof args === 'string') {
try {
args = JSON.parse(args);
} catch (e) {
// Ignore parse errors
}
}
return args;
};
evalTest('USUALLY_PASSES', {
name: 'should use targeted search with limit',
prompt: 'find me a sample usage of path.resolve() in the codebase',
files: {
'package.json': JSON.stringify({
name: 'test-project',
version: '1.0.0',
main: 'dist/index.js',
scripts: {
build: 'tsc',
test: 'vitest',
},
dependencies: {
typescript: '^5.0.0',
'@types/node': '^20.0.0',
vitest: '^1.0.0',
},
}),
'src/index.ts': `
import { App } from './app.ts';
const app = new App();
app.start();
`,
'src/app.ts': `
import * as path from 'path';
import { UserController } from './controllers/user.ts';
export class App {
constructor() {
console.log('App initialized');
}
public start(): void {
const userController = new UserController();
console.log('Static path:', path.resolve(__dirname, '../public'));
}
}
`,
'src/utils.ts': `
import * as path from 'path';
import * as fs from 'fs';
export function resolvePath(p: string): string {
return path.resolve(process.cwd(), p);
}
export function ensureDir(dirPath: string): void {
const absolutePath = path.resolve(dirPath);
if (!fs.existsSync(absolutePath)) {
fs.mkdirSync(absolutePath, { recursive: true });
}
}
`,
'src/config.ts': `
import * as path from 'path';
export const config = {
dbPath: path.resolve(process.cwd(), 'data/db.sqlite'),
logLevel: 'info',
};
`,
'src/controllers/user.ts': `
import * as path from 'path';
export class UserController {
public getUsers(): any[] {
console.log('Loading users from:', path.resolve('data/users.json'));
return [{ id: 1, name: 'Alice' }];
}
}
`,
'tests/app.test.ts': `
import { describe, it, expect } from 'vitest';
import * as path from 'path';
describe('App', () => {
it('should resolve paths', () => {
const p = path.resolve('test');
expect(p).toBeDefined();
});
});
`,
},
assert: async (rig) => {
const toolCalls = rig.readToolLogs();
const grepCalls = toolCalls.filter(
(call) => call.toolRequest.name === 'grep_search',
);
expect(grepCalls.length).toBeGreaterThan(0);
const grepParams = grepCalls.map(getGrepParams);
const hasTotalMaxLimit = grepParams.some(
(p) => p.total_max_matches !== undefined && p.total_max_matches <= 100,
);
expect(
hasTotalMaxLimit,
`Expected agent to use a small total_max_matches (<= 100) for a sample usage request. Actual values: ${JSON.stringify(
grepParams.map((p) => p.total_max_matches),
)}`,
).toBe(true);
const hasMaxMatchesPerFileLimit = grepParams.some(
(p) =>
p.max_matches_per_file !== undefined && p.max_matches_per_file <= 5,
);
expect(
hasMaxMatchesPerFileLimit,
`Expected agent to use a small max_matches_per_file (<= 5) for a sample usage request. Actual values: ${JSON.stringify(
grepParams.map((p) => p.max_matches_per_file),
)}`,
).toBe(true);
},
});
});
+2 -2
View File
@@ -14,7 +14,7 @@ import {
describe('save_memory', () => {
const TEST_PREFIX = 'Save memory test: ';
const rememberingFavoriteColor = "Agent remembers user's favorite color";
evalTest('ALWAYS_PASSES', {
evalTest('USUALLY_PASSES', {
name: rememberingFavoriteColor,
params: {
settings: { tools: { core: ['save_memory'] } },
@@ -57,7 +57,7 @@ describe('save_memory', () => {
});
const rememberingWorkflow = 'Agent remembers workflow preferences';
evalTest('ALWAYS_PASSES', {
evalTest('USUALLY_PASSES', {
name: rememberingWorkflow,
params: {
settings: { tools: { core: ['save_memory'] } },
+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",
-7
View File
@@ -128,13 +128,6 @@ async function addMcpServer(
settings.setValue(settingsScope, 'mcpServers', mcpServers);
if (transport === 'stdio') {
debugLogger.warn(
'Security Warning: Running MCP servers with stdio transport can expose inherited environment variables. ' +
'While the Gemini CLI redacts common API keys and secrets by default, you should only run servers from trusted sources.',
);
}
if (isExistingServer) {
debugLogger.log(`MCP server "${name}" updated in ${scope} settings.`);
} else {
+103 -1
View File
@@ -141,6 +141,10 @@ vi.mock('@google/gemini-cli-core', async () => {
defaultDecision: ServerConfig.PolicyDecision.ASK_USER,
approvalMode: ServerConfig.ApprovalMode.DEFAULT,
})),
getAdminErrorMessage: vi.fn(
(_feature) =>
`YOLO mode is disabled by your administrator. To enable it, please request an update to the settings at: https://goo.gle/manage-gemini-cli`,
),
isHeadlessMode: vi.fn((opts) => {
if (process.env['VITEST'] === 'true') {
return (
@@ -1247,6 +1251,31 @@ describe('Approval mode tool exclusion logic', () => {
expect(excludedTools).toContain(ASK_USER_TOOL_NAME);
});
it('should exclude all interactive tools in non-interactive mode with deep work approval mode', async () => {
process.argv = [
'node',
'script.js',
'--approval-mode',
'deep_work',
'-p',
'test',
];
const settings = createTestMergedSettings({
experimental: {
deepWork: true,
},
});
const argv = await parseArguments(createTestMergedSettings());
const config = await loadCliConfig(settings, 'test-session', argv);
const excludedTools = config.getExcludeTools();
expect(excludedTools).toContain(SHELL_TOOL_NAME);
expect(excludedTools).toContain(EDIT_TOOL_NAME);
expect(excludedTools).toContain(WRITE_FILE_TOOL_NAME);
expect(excludedTools).toContain(ASK_USER_TOOL_NAME);
});
it('should exclude only ask_user in non-interactive mode with legacy yolo flag', async () => {
process.argv = ['node', 'script.js', '--yolo', '-p', 'test'];
const argv = await parseArguments(createTestMergedSettings());
@@ -1337,7 +1366,7 @@ describe('Approval mode tool exclusion logic', () => {
await expect(
loadCliConfig(settings, 'test-session', invalidArgv as CliArgs),
).rejects.toThrow(
'Invalid approval mode: invalid_mode. Valid values are: yolo, auto_edit, plan, default',
'Invalid approval mode: invalid_mode. Valid values are: yolo, auto_edit, plan, deep_work, default',
);
});
});
@@ -2530,6 +2559,18 @@ describe('loadCliConfig approval mode', () => {
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.PLAN);
});
it('should set Deep Work approval mode when --approval-mode=deep_work is used and experimental.deepWork is enabled', async () => {
process.argv = ['node', 'script.js', '--approval-mode', 'deep_work'];
const argv = await parseArguments(createTestMergedSettings());
const settings = createTestMergedSettings({
experimental: {
deepWork: true,
},
});
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEEP_WORK);
});
it('should ignore "yolo" in settings.tools.approvalMode and fall back to DEFAULT', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
@@ -2567,6 +2608,20 @@ describe('loadCliConfig approval mode', () => {
);
});
it('should throw error when --approval-mode=deep_work is used but experimental.deepWork is disabled', async () => {
process.argv = ['node', 'script.js', '--approval-mode', 'deep_work'];
const argv = await parseArguments(createTestMergedSettings());
const settings = createTestMergedSettings({
experimental: {
deepWork: false,
},
});
await expect(loadCliConfig(settings, 'test-session', argv)).rejects.toThrow(
'Approval mode "deep_work" is only available when experimental.deepWork is enabled.',
);
});
// --- Untrusted Folder Scenarios ---
describe('when folder is NOT trusted', () => {
beforeEach(() => {
@@ -2667,6 +2722,19 @@ describe('loadCliConfig approval mode', () => {
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.PLAN);
});
it('should respect deep work mode from settings when experimental.deepWork is enabled', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
general: { defaultApprovalMode: 'deep_work' },
experimental: { deepWork: true },
});
const argv = await parseArguments(settings);
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getApprovalMode()).toBe(
ServerConfig.ApprovalMode.DEEP_WORK,
);
});
it('should throw error if plan mode is in settings but experimental.plan is disabled', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
@@ -2680,6 +2748,20 @@ describe('loadCliConfig approval mode', () => {
'Approval mode "plan" is only available when experimental.plan is enabled.',
);
});
it('should throw error if deep work mode is in settings but experimental.deepWork is disabled', async () => {
process.argv = ['node', 'script.js'];
const settings = createTestMergedSettings({
general: { defaultApprovalMode: 'deep_work' },
experimental: { deepWork: false },
});
const argv = await parseArguments(settings);
await expect(
loadCliConfig(settings, 'test-session', argv),
).rejects.toThrow(
'Approval mode "deep_work" is only available when experimental.deepWork is enabled.',
);
});
});
});
@@ -3192,6 +3274,26 @@ describe('Policy Engine Integration in loadCliConfig', () => {
expect.anything(),
);
});
it('should pass user-provided policy paths from --policy flag to createPolicyEngineConfig', async () => {
process.argv = [
'node',
'script.js',
'--policy',
'/path/to/policy1.toml,/path/to/policy2.toml',
];
const settings = createTestMergedSettings();
const argv = await parseArguments(settings);
await loadCliConfig(settings, 'test-session', argv);
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
policyPaths: ['/path/to/policy1.toml', '/path/to/policy2.toml'],
}),
expect.anything(),
);
});
});
describe('loadCliConfig disableYoloMode', () => {
+41 -7
View File
@@ -75,6 +75,7 @@ export interface CliArgs {
yolo: boolean | undefined;
approvalMode: string | undefined;
policy: string[] | undefined;
allowedMcpServerNames: string[] | undefined;
allowedTools: string[] | undefined;
experimentalAcp: boolean | undefined;
@@ -154,9 +155,24 @@ export async function parseArguments(
.option('approval-mode', {
type: 'string',
nargs: 1,
choices: ['default', 'auto_edit', 'yolo', 'plan'],
choices: ['default', 'auto_edit', 'yolo', 'plan', 'deep_work'],
description:
'Set the approval mode: default (prompt for approval), auto_edit (auto-approve edit tools), yolo (auto-approve all tools), plan (read-only mode)',
'Set the approval mode: default (prompt for approval), auto_edit (auto-approve edit tools), yolo (auto-approve all tools), plan (read-only mode), deep_work (iterative execution mode)',
})
.option('policy', {
type: 'array',
string: true,
nargs: 1,
description:
'Additional policy files or directories to load (comma-separated or multiple --policy)',
coerce: (policies: string[]) =>
// Handle comma-separated values
policies.flatMap((p) =>
p
.split(',')
.map((s) => s.trim())
.filter(Boolean),
),
})
.option('experimental-acp', {
type: 'boolean',
@@ -177,7 +193,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 +462,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
@@ -539,12 +560,20 @@ export async function loadCliConfig(
}
approvalMode = ApprovalMode.PLAN;
break;
case 'deep_work':
if (!(settings.experimental?.deepWork ?? false)) {
throw new Error(
'Approval mode "deep_work" is only available when experimental.deepWork is enabled.',
);
}
approvalMode = ApprovalMode.DEEP_WORK;
break;
case 'default':
approvalMode = ApprovalMode.DEFAULT;
break;
default:
throw new Error(
`Invalid approval mode: ${rawApprovalMode}. Valid values are: yolo, auto_edit, plan, default`,
`Invalid approval mode: ${rawApprovalMode}. Valid values are: yolo, auto_edit, plan, deep_work, default`,
);
}
} else {
@@ -602,8 +631,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 || [];
@@ -635,6 +663,10 @@ export async function loadCliConfig(
// TODO(#16625): Replace this default exclusion logic with specific rules for plan mode.
extraExcludes.push(...defaultExcludes.filter(toolExclusionFilter));
break;
case ApprovalMode.DEEP_WORK:
// Deep Work still requires interactive confirmation for mutating tools.
extraExcludes.push(...defaultExcludes.filter(toolExclusionFilter));
break;
case ApprovalMode.DEFAULT:
// In default non-interactive mode, all tools that require approval are excluded.
extraExcludes.push(...defaultExcludes.filter(toolExclusionFilter));
@@ -666,6 +698,7 @@ export async function loadCliConfig(
...settings.mcp,
allowed: argv.allowedMcpServerNames ?? settings.mcp?.allowed,
},
policyPaths: argv.policy,
};
const policyEngineConfig = await createPolicyEngineConfig(
@@ -789,6 +822,7 @@ export async function loadCliConfig(
enableExtensionReloading: settings.experimental?.extensionReloading,
enableAgents: settings.experimental?.enableAgents,
plan: settings.experimental?.plan,
deepWork: settings.experimental?.deepWork,
enableEventDrivenScheduler: true,
skillsSupport: settings.skills?.enabled ?? true,
disabledSkills: settings.skills?.disabled,
+7 -10
View File
@@ -129,7 +129,7 @@ export type KeyBindingConfig = {
export const defaultKeyBindings: KeyBindingConfig = {
// Basic Controls
[Command.RETURN]: [{ key: 'return' }],
[Command.ESCAPE]: [{ key: 'escape' }],
[Command.ESCAPE]: [{ key: 'escape' }, { key: '[', ctrl: true }],
[Command.QUIT]: [{ key: 'c', ctrl: true }],
[Command.EXIT]: [{ key: 'd', ctrl: true }],
@@ -286,10 +286,7 @@ export const defaultKeyBindings: KeyBindingConfig = {
[Command.SHOW_SHELL_INPUT_UNFOCUS_WARNING]: [{ key: 'tab', shift: false }],
[Command.BACKGROUND_SHELL_SELECT]: [{ key: 'return' }],
[Command.BACKGROUND_SHELL_ESCAPE]: [{ key: 'escape' }],
[Command.SHOW_MORE_LINES]: [
{ key: 'o', ctrl: true },
{ key: 's', ctrl: true },
],
[Command.SHOW_MORE_LINES]: [{ key: 'o', ctrl: true }],
[Command.EXPAND_PASTE]: [{ key: 'o', ctrl: true }],
[Command.FOCUS_SHELL_INPUT]: [{ key: 'tab', shift: false }],
[Command.UNFOCUS_SHELL_INPUT]: [{ key: 'tab', shift: true }],
@@ -499,9 +496,9 @@ export const commandDescriptions: Readonly<Record<Command, string>> = {
[Command.TOGGLE_COPY_MODE]: 'Toggle copy mode when in alternate buffer mode.',
[Command.TOGGLE_YOLO]: 'Toggle YOLO (auto-approval) mode for tool calls.',
[Command.CYCLE_APPROVAL_MODE]:
'Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only).',
'Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), plan (read-only), and deep_work (iterative; when enabled).',
[Command.SHOW_MORE_LINES]:
'Expand a height-constrained response to show additional lines when not in alternate buffer mode.',
'Expand and collapse blocks of content when not in alternate buffer mode.',
[Command.EXPAND_PASTE]:
'Expand or collapse a paste placeholder when cursor is over placeholder.',
[Command.BACKGROUND_SHELL_SELECT]:
@@ -516,12 +513,12 @@ export const commandDescriptions: Readonly<Record<Command, string>> = {
[Command.UNFOCUS_BACKGROUND_SHELL_LIST]:
'Move focus from background shell list to Gemini.',
[Command.SHOW_BACKGROUND_SHELL_UNFOCUS_WARNING]:
'Show warning when trying to unfocus background shell via Tab.',
'Show warning when trying to move focus away from background shell.',
[Command.SHOW_SHELL_INPUT_UNFOCUS_WARNING]:
'Show warning when trying to unfocus shell input via Tab.',
'Show warning when trying to move focus away from shell input.',
[Command.FOCUS_SHELL_INPUT]: 'Move focus from Gemini to the active shell.',
[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
];
+1
View File
@@ -25,6 +25,7 @@ export async function createPolicyEngineConfig(
mcp: settings.mcp,
tools: settings.tools,
mcpServers: settings.mcpServers,
policyPaths: settings.policyPaths,
};
return createCorePolicyEngineConfig(policySettings, approvalMode);
+44
View File
@@ -2546,6 +2546,50 @@ describe('Settings Loading and Merging', () => {
});
});
describe('Reactivity & Snapshots', () => {
let loadedSettings: LoadedSettings;
beforeEach(() => {
const emptySettingsFile: SettingsFile = {
path: '/mock/path',
settings: {},
originalSettings: {},
};
loadedSettings = new LoadedSettings(
{ ...emptySettingsFile, path: getSystemSettingsPath() },
{ ...emptySettingsFile, path: getSystemDefaultsPath() },
{ ...emptySettingsFile, path: USER_SETTINGS_PATH },
{ ...emptySettingsFile, path: MOCK_WORKSPACE_SETTINGS_PATH },
true, // isTrusted
[],
);
});
it('getSnapshot() should return stable reference if no changes occur', () => {
const snap1 = loadedSettings.getSnapshot();
const snap2 = loadedSettings.getSnapshot();
expect(snap1).toBe(snap2);
});
it('setValue() should create a new snapshot reference and emit event', () => {
const oldSnapshot = loadedSettings.getSnapshot();
const oldUserRef = oldSnapshot.user.settings;
loadedSettings.setValue(SettingScope.User, 'ui.theme', 'high-contrast');
const newSnapshot = loadedSettings.getSnapshot();
expect(newSnapshot).not.toBe(oldSnapshot);
expect(newSnapshot.user.settings).not.toBe(oldUserRef);
expect(newSnapshot.user.settings.ui?.theme).toBe('high-contrast');
expect(newSnapshot.system.settings).not.toBe(oldSnapshot.system.settings);
expect(mockCoreEvents.emitSettingsChanged).toHaveBeenCalled();
});
});
describe('Security and Sandbox', () => {
let originalArgv: string[];
let originalEnv: NodeJS.ProcessEnv;
+48
View File
@@ -10,6 +10,7 @@ import { platform } from 'node:os';
import * as dotenv from 'dotenv';
import process from 'node:process';
import {
CoreEvent,
FatalConfigError,
GEMINI_DIR,
getErrorMessage,
@@ -284,6 +285,20 @@ export function createTestMergedSettings(
) as MergedSettings;
}
/**
* An immutable snapshot of settings state.
* Used with useSyncExternalStore for reactive updates.
*/
export interface LoadedSettingsSnapshot {
system: SettingsFile;
systemDefaults: SettingsFile;
user: SettingsFile;
workspace: SettingsFile;
isTrusted: boolean;
errors: SettingsError[];
merged: MergedSettings;
}
export class LoadedSettings {
constructor(
system: SettingsFile,
@@ -303,6 +318,7 @@ export class LoadedSettings {
: this.createEmptyWorkspace(workspace);
this.errors = errors;
this._merged = this.computeMergedSettings();
this._snapshot = this.computeSnapshot();
}
readonly system: SettingsFile;
@@ -314,6 +330,7 @@ export class LoadedSettings {
private _workspaceFile: SettingsFile;
private _merged: MergedSettings;
private _snapshot: LoadedSettingsSnapshot;
private _remoteAdminSettings: Partial<Settings> | undefined;
get merged(): MergedSettings {
@@ -368,6 +385,36 @@ export class LoadedSettings {
return merged;
}
private computeSnapshot(): LoadedSettingsSnapshot {
const cloneSettingsFile = (file: SettingsFile): SettingsFile => ({
path: file.path,
rawJson: file.rawJson,
settings: structuredClone(file.settings),
originalSettings: structuredClone(file.originalSettings),
});
return {
system: cloneSettingsFile(this.system),
systemDefaults: cloneSettingsFile(this.systemDefaults),
user: cloneSettingsFile(this.user),
workspace: cloneSettingsFile(this.workspace),
isTrusted: this.isTrusted,
errors: [...this.errors],
merged: structuredClone(this._merged),
};
}
// Passing this along with getSnapshot to useSyncExternalStore allows for idiomatic reactivity on settings changes
// React will pass a listener fn into this subscribe fn
// that listener fn will perform an object identity check on the snapshot and trigger a React re render if the snapshot has changed
subscribe(listener: () => void): () => void {
coreEvents.on(CoreEvent.SettingsChanged, listener);
return () => coreEvents.off(CoreEvent.SettingsChanged, listener);
}
getSnapshot(): LoadedSettingsSnapshot {
return this._snapshot;
}
forScope(scope: LoadableSettingScope): SettingsFile {
switch (scope) {
case SettingScope.User:
@@ -409,6 +456,7 @@ export class LoadedSettings {
}
this._merged = this.computeMergedSettings();
this._snapshot = this.computeSnapshot();
coreEvents.emitSettingsChanged();
}
@@ -186,6 +186,9 @@ describe('SettingsSchema', () => {
expect(getSettingsSchema().ui.properties.hideTips.showInDialog).toBe(
true,
);
expect(
getSettingsSchema().ui.properties.showShortcutsHint.showInDialog,
).toBe(true);
expect(getSettingsSchema().ui.properties.hideBanner.showInDialog).toBe(
true,
);
@@ -328,6 +331,28 @@ describe('SettingsSchema', () => {
).toBe('Enable debug logging of keystrokes to the console.');
});
it('should have showShortcutsHint setting in schema', () => {
expect(getSettingsSchema().ui.properties.showShortcutsHint).toBeDefined();
expect(getSettingsSchema().ui.properties.showShortcutsHint.type).toBe(
'boolean',
);
expect(getSettingsSchema().ui.properties.showShortcutsHint.category).toBe(
'UI',
);
expect(getSettingsSchema().ui.properties.showShortcutsHint.default).toBe(
true,
);
expect(
getSettingsSchema().ui.properties.showShortcutsHint.requiresRestart,
).toBe(false);
expect(
getSettingsSchema().ui.properties.showShortcutsHint.showInDialog,
).toBe(true);
expect(
getSettingsSchema().ui.properties.showShortcutsHint.description,
).toBe('Show the "? for shortcuts" hint above the input.');
});
it('should have enableAgents setting in schema', () => {
const setting = getSettingsSchema().experimental.properties.enableAgents;
expect(setting).toBeDefined();
@@ -365,6 +390,19 @@ describe('SettingsSchema', () => {
);
});
it('should have deepWork setting in schema', () => {
const setting = getSettingsSchema().experimental.properties.deepWork;
expect(setting).toBeDefined();
expect(setting.type).toBe('boolean');
expect(setting.category).toBe('Experimental');
expect(setting.default).toBe(false);
expect(setting.requiresRestart).toBe(true);
expect(setting.showInDialog).toBe(true);
expect(setting.description).toBe(
'Enable Deep Work mode (iterative execution mode and tools).',
);
});
it('should have hooksConfig.notifications setting in schema', () => {
const setting = getSettingsSchema().hooksConfig?.properties.notifications;
expect(setting).toBeDefined();
+37 -4
View File
@@ -152,6 +152,18 @@ const SETTINGS_SCHEMA = {
},
},
policyPaths: {
type: 'array',
label: 'Policy Paths',
category: 'Advanced',
requiresRestart: true,
default: [] as string[],
description: 'Additional policy files or directories to load.',
showInDialog: false,
items: { type: 'string' },
mergeStrategy: MergeStrategy.UNION,
},
general: {
type: 'object',
label: 'General',
@@ -188,13 +200,15 @@ const SETTINGS_SCHEMA = {
description: oneLine`
The default approval mode for tool execution.
'default' prompts for approval, 'auto_edit' auto-approves edit tools,
and 'plan' is read-only mode. 'yolo' is not supported yet.
'plan' is read-only mode, and 'deep_work' is iterative execution mode.
'yolo' is not supported yet.
`,
showInDialog: true,
options: [
{ value: 'default', label: 'Default' },
{ value: 'auto_edit', label: 'Auto Edit' },
{ value: 'plan', label: 'Plan' },
{ value: 'deep_work', label: 'Deep Work' },
],
},
devtools: {
@@ -462,6 +476,15 @@ const SETTINGS_SCHEMA = {
description: 'Hide helpful tips in the UI',
showInDialog: true,
},
showShortcutsHint: {
type: 'boolean',
label: 'Show Shortcuts Hint',
category: 'UI',
requiresRestart: false,
default: true,
description: 'Show the "? for shortcuts" hint above the input.',
showInDialog: true,
},
hideBanner: {
type: 'boolean',
label: 'Hide Banner',
@@ -1462,7 +1485,7 @@ const SETTINGS_SCHEMA = {
label: 'Tool Output Masking',
category: 'Experimental',
requiresRestart: true,
ignoreInDocs: true,
ignoreInDocs: false,
default: {},
description:
'Advanced settings for tool output masking to manage context window efficiency.',
@@ -1473,9 +1496,9 @@ const SETTINGS_SCHEMA = {
label: 'Enable Tool Output Masking',
category: 'Experimental',
requiresRestart: true,
default: false,
default: true,
description: 'Enables tool output masking to save tokens.',
showInDialog: false,
showInDialog: true,
},
toolProtectionThreshold: {
type: 'number',
@@ -1584,6 +1607,16 @@ const SETTINGS_SCHEMA = {
description: 'Enable planning features (Plan Mode and tools).',
showInDialog: true,
},
deepWork: {
type: 'boolean',
label: 'Deep Work',
category: 'Experimental',
requiresRestart: true,
default: false,
description:
'Enable Deep Work mode (iterative execution mode and tools).',
showInDialog: true,
},
},
},
@@ -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,
);
}
+11 -19
View File
@@ -238,18 +238,15 @@ vi.mock('./validateNonInterActiveAuth.js', () => ({
}));
describe('gemini.tsx main function', () => {
let originalEnvGeminiSandbox: string | undefined;
let originalEnvSandbox: string | undefined;
let originalIsTTY: boolean | undefined;
let initialUnhandledRejectionListeners: NodeJS.UnhandledRejectionListener[] =
[];
beforeEach(() => {
// Store and clear sandbox-related env variables to ensure a consistent test environment
originalEnvGeminiSandbox = process.env['GEMINI_SANDBOX'];
originalEnvSandbox = process.env['SANDBOX'];
delete process.env['GEMINI_SANDBOX'];
delete process.env['SANDBOX'];
vi.stubEnv('GEMINI_SANDBOX', '');
vi.stubEnv('SANDBOX', '');
vi.stubEnv('SHPOOL_SESSION_NAME', '');
initialUnhandledRejectionListeners =
process.listeners('unhandledRejection');
@@ -260,18 +257,6 @@ describe('gemini.tsx main function', () => {
});
afterEach(() => {
// Restore original env variables
if (originalEnvGeminiSandbox !== undefined) {
process.env['GEMINI_SANDBOX'] = originalEnvGeminiSandbox;
} else {
delete process.env['GEMINI_SANDBOX'];
}
if (originalEnvSandbox !== undefined) {
process.env['SANDBOX'] = originalEnvSandbox;
} else {
delete process.env['SANDBOX'];
}
const currentListeners = process.listeners('unhandledRejection');
currentListeners.forEach((listener) => {
if (!initialUnhandledRejectionListeners.includes(listener)) {
@@ -282,6 +267,7 @@ describe('gemini.tsx main function', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(process.stdin as any).isTTY = originalIsTTY;
vi.unstubAllEnvs();
vi.restoreAllMocks();
});
@@ -478,6 +464,7 @@ describe('gemini.tsx main function kitty protocol', () => {
query: undefined,
yolo: undefined,
approvalMode: undefined,
policy: undefined,
allowedMcpServerNames: undefined,
allowedTools: undefined,
experimentalAcp: undefined,
@@ -1209,7 +1196,12 @@ describe('startInteractiveUI', () => {
registerTelemetryConfig: vi.fn(),
}));
beforeEach(() => {
vi.stubEnv('SHPOOL_SESSION_NAME', '');
});
afterEach(() => {
vi.unstubAllEnvs();
vi.restoreAllMocks();
});
@@ -1308,7 +1300,7 @@ describe('startInteractiveUI', () => {
// Verify all startup tasks were called
expect(getVersion).toHaveBeenCalledTimes(1);
expect(registerCleanup).toHaveBeenCalledTimes(3);
expect(registerCleanup).toHaveBeenCalledTimes(4);
// Verify cleanup handler is registered with unmount function
const cleanupFn = vi.mocked(registerCleanup).mock.calls[0][0];
+56 -20
View File
@@ -57,8 +57,8 @@ import {
writeToStderr,
disableMouseEvents,
enableMouseEvents,
enterAlternateScreen,
disableLineWrapping,
enableLineWrapping,
shouldEnterAlternateScreen,
startupProfiler,
ExitCodes,
@@ -103,6 +103,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;
@@ -214,9 +215,12 @@ export async function startInteractiveUI(
const { stdout: inkStdout, stderr: inkStderr } = createWorkingStdio();
const isShpool = !!process.env['SHPOOL_SESSION_NAME'];
// Create wrapper component to use hooks inside render
const AppWrapper = () => {
useKittyKeyboardProtocol();
return (
<SettingsContext.Provider value={settings}>
<KeypressProvider
@@ -250,6 +254,17 @@ export async function startInteractiveUI(
);
};
if (isShpool) {
// Wait a moment for shpool to stabilize terminal size and state.
// shpool is a persistence tool that restores terminal state by replaying it.
// This delay gives shpool time to finish its restoration replay and send
// the actual terminal size (often via an immediate SIGWINCH) before we
// render the first TUI frame. Without this, the first frame may be
// garbled or rendered at an incorrect size, which disabling incremental
// rendering alone cannot fix for the initial frame.
await new Promise((resolve) => setTimeout(resolve, 100));
}
const instance = render(
process.env['DEBUG'] ? (
<React.StrictMode>
@@ -273,10 +288,19 @@ export async function startInteractiveUI(
patchConsole: false,
alternateBuffer: useAlternateBuffer,
incrementalRendering:
settings.merged.ui.incrementalRendering !== false && useAlternateBuffer,
settings.merged.ui.incrementalRendering !== false &&
useAlternateBuffer &&
!isShpool,
},
);
if (useAlternateBuffer) {
disableLineWrapping();
registerCleanup(() => {
enableLineWrapping();
});
}
checkForUpdates(settings)
.then((info) => {
handleAutoUpdate(info, settings, config.getProjectRoot());
@@ -309,6 +333,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();
@@ -335,6 +364,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);
@@ -590,26 +639,13 @@ export async function main() {
// input showing up in the output.
process.stdin.setRawMode(true);
if (
shouldEnterAlternateScreen(
isAlternateBufferEnabled(settings),
config.getScreenReader(),
)
) {
enterAlternateScreen();
disableLineWrapping();
// Ink will cleanup so there is no need for us to manually cleanup.
}
// This cleanup isn't strictly needed but may help in certain situations.
const restoreRawMode = () => {
process.on('SIGTERM', () => {
process.stdin.setRawMode(wasRaw);
};
process.off('SIGTERM', restoreRawMode);
process.on('SIGTERM', restoreRawMode);
process.off('SIGINT', restoreRawMode);
process.on('SIGINT', restoreRawMode);
});
process.on('SIGINT', () => {
process.stdin.setRawMode(wasRaw);
});
}
await setupTerminalAndTheme(config, settings);
@@ -111,6 +111,16 @@ vi.mock('../ui/commands/planCommand.js', async () => {
},
};
});
vi.mock('../ui/commands/deepworkCommand.js', async () => {
const { CommandKind } = await import('../ui/commands/types.js');
return {
deepworkCommand: {
name: 'deepwork',
description: 'Deep Work command',
kind: CommandKind.BUILT_IN,
},
};
});
vi.mock('../ui/commands/mcpCommand.js', () => ({
mcpCommand: {
@@ -130,6 +140,7 @@ describe('BuiltinCommandLoader', () => {
mockConfig = {
getFolderTrust: vi.fn().mockReturnValue(true),
isPlanEnabled: vi.fn().mockReturnValue(false),
isDeepWorkEnabled: vi.fn().mockReturnValue(false),
getEnableExtensionReloading: () => false,
getEnableHooks: () => false,
getEnableHooksUI: () => false,
@@ -247,6 +258,22 @@ describe('BuiltinCommandLoader', () => {
expect(planCmd).toBeUndefined();
});
it('should include deepwork command when deep work mode is enabled', async () => {
(mockConfig.isDeepWorkEnabled as Mock).mockReturnValue(true);
const loader = new BuiltinCommandLoader(mockConfig);
const commands = await loader.loadCommands(new AbortController().signal);
const deepworkCmd = commands.find((c) => c.name === 'deepwork');
expect(deepworkCmd).toBeDefined();
});
it('should exclude deepwork command when deep work mode is disabled', async () => {
(mockConfig.isDeepWorkEnabled as Mock).mockReturnValue(false);
const loader = new BuiltinCommandLoader(mockConfig);
const commands = await loader.loadCommands(new AbortController().signal);
const deepworkCmd = commands.find((c) => c.name === 'deepwork');
expect(deepworkCmd).toBeUndefined();
});
it('should exclude agents command when agents are disabled', async () => {
mockConfig.isAgentsEnabled = vi.fn().mockReturnValue(false);
const loader = new BuiltinCommandLoader(mockConfig);
@@ -288,6 +315,7 @@ describe('BuiltinCommandLoader profile', () => {
mockConfig = {
getFolderTrust: vi.fn().mockReturnValue(false),
isPlanEnabled: vi.fn().mockReturnValue(false),
isDeepWorkEnabled: vi.fn().mockReturnValue(false),
getCheckpointingEnabled: () => false,
getEnableExtensionReloading: () => false,
getEnableHooks: () => false,
@@ -41,6 +41,7 @@ import { memoryCommand } from '../ui/commands/memoryCommand.js';
import { modelCommand } from '../ui/commands/modelCommand.js';
import { oncallCommand } from '../ui/commands/oncallCommand.js';
import { permissionsCommand } from '../ui/commands/permissionsCommand.js';
import { deepworkCommand } from '../ui/commands/deepworkCommand.js';
import { planCommand } from '../ui/commands/planCommand.js';
import { policiesCommand } from '../ui/commands/policiesCommand.js';
import { privacyCommand } from '../ui/commands/privacyCommand.js';
@@ -146,6 +147,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
modelCommand,
...(this.config?.getFolderTrust() ? [permissionsCommand] : []),
...(this.config?.isPlanEnabled() ? [planCommand] : []),
...(this.config?.isDeepWorkEnabled?.() ? [deepworkCommand] : []),
policiesCommand,
privacyCommand,
...(isDevelopment ? [profileCommand] : []),
@@ -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'),
+18 -1
View File
@@ -33,6 +33,9 @@ import { makeFakeConfig, type Config } from '@google/gemini-cli-core';
import { FakePersistentState } from './persistentStateFake.js';
import { AppContext, type AppState } from '../ui/contexts/AppContext.js';
import { createMockSettings } from './settings.js';
import { themeManager, DEFAULT_THEME } from '../ui/themes/theme-manager.js';
import { DefaultLight } from '../ui/themes/default-light.js';
import { pickDefaultThemeName } from '../ui/themes/theme.js';
export const persistentStateMock = new FakePersistentState();
@@ -150,7 +153,8 @@ const baseMockUiState = {
terminalWidth: 120,
terminalHeight: 40,
currentModel: 'gemini-pro',
terminalBackgroundColor: undefined,
terminalBackgroundColor: 'black',
cleanUiDetailsVisible: false,
activePtyId: undefined,
backgroundShells: new Map(),
backgroundShellHeight: 0,
@@ -204,6 +208,10 @@ const mockUIActions: UIActions = {
handleApiKeyCancel: vi.fn(),
setBannerVisible: vi.fn(),
setShortcutsHelpVisible: vi.fn(),
setCleanUiDetailsVisible: vi.fn(),
toggleCleanUiDetailsVisible: vi.fn(),
revealCleanUiDetailsTemporarily: vi.fn(),
handleWarning: vi.fn(),
setEmbeddedShellFocused: vi.fn(),
dismissBackgroundShell: vi.fn(),
setActiveBackgroundShellPid: vi.fn(),
@@ -293,6 +301,15 @@ export const renderWithProviders = (
mainAreaWidth,
};
themeManager.setTerminalBackground(baseState.terminalBackgroundColor);
const themeName = pickDefaultThemeName(
baseState.terminalBackgroundColor,
themeManager.getAllThemes(),
DEFAULT_THEME.name,
DefaultLight.name,
);
themeManager.setActiveTheme(themeName);
const finalUIActions = { ...mockUIActions, ...uiActions };
const allToolCalls = (finalUiState.pendingHistoryItems || [])
+1 -4
View File
@@ -66,6 +66,7 @@ describe('App', () => {
const mockUIState: Partial<UIState> = {
streamingState: StreamingState.Idle,
cleanUiDetailsVisible: true,
quittingMessages: null,
dialogsVisible: false,
mainControlsRef: {
@@ -220,10 +221,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);
+305 -153
View File
@@ -14,13 +14,13 @@ import {
type Mock,
type MockedObject,
} from 'vitest';
import { render } from '../test-utils/render.js';
import { render, persistentStateMock } from '../test-utils/render.js';
import { waitFor } from '../test-utils/async.js';
import { cleanup } from 'ink-testing-library';
import { act, useContext, type ReactElement } from 'react';
import { AppContainer } from './AppContainer.js';
import { SettingsContext } from './contexts/SettingsContext.js';
import { type TrackedToolCall } from './hooks/useReactToolScheduler.js';
import { type TrackedToolCall } from './hooks/useToolScheduler.js';
import {
type Config,
makeFakeConfig,
@@ -84,7 +84,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
};
});
import ansiEscapes from 'ansi-escapes';
import { type LoadedSettings, mergeSettings } from '../config/settings.js';
import { mergeSettings, type LoadedSettings } from '../config/settings.js';
import type { InitializationResult } from '../core/initializer.js';
import { useQuotaAndFallback } from './hooks/useQuotaAndFallback.js';
import { UIStateContext, type UIState } from './contexts/UIStateContext.js';
@@ -92,6 +92,7 @@ import {
UIActionsContext,
type UIActions,
} from './contexts/UIActionsContext.js';
import { KeypressProvider } from './contexts/KeypressContext.js';
// Mock useStdout to capture terminal title writes
vi.mock('ink', async (importOriginal) => {
@@ -133,8 +134,8 @@ vi.mock('./hooks/useGeminiStream.js');
vi.mock('./hooks/vim.js');
vi.mock('./hooks/useFocus.js');
vi.mock('./hooks/useBracketedPaste.js');
vi.mock('./hooks/useKeypress.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');
@@ -198,6 +199,8 @@ import { useLogger } from './hooks/useLogger.js';
import { useLoadingIndicator } from './hooks/useLoadingIndicator.js';
import { useInputHistoryStore } from './hooks/useInputHistoryStore.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 {
@@ -232,13 +235,15 @@ describe('AppContainer State Management', () => {
resumedSessionData?: ResumedSessionData;
} = {}) => (
<SettingsContext.Provider value={settings}>
<AppContainer
config={config}
version={version}
initializationResult={initResult}
startupWarnings={startupWarnings}
resumedSessionData={resumedSessionData}
/>
<KeypressProvider config={config}>
<AppContainer
config={config}
version={version}
initializationResult={initResult}
startupWarnings={startupWarnings}
resumedSessionData={resumedSessionData}
/>
</KeypressProvider>
</SettingsContext.Provider>
);
@@ -268,7 +273,7 @@ describe('AppContainer State Management', () => {
const mockedUseTextBuffer = useTextBuffer as Mock;
const mockedUseLogger = useLogger as Mock;
const mockedUseLoadingIndicator = useLoadingIndicator as Mock;
const mockedUseKeypress = useKeypress as Mock;
const mockedUseSuspend = useSuspend as Mock;
const mockedUseInputHistoryStore = useInputHistoryStore as Mock;
const mockedUseHookDisplayState = useHookDisplayState as Mock;
const mockedUseTerminalTheme = useTerminalTheme as Mock;
@@ -294,6 +299,7 @@ describe('AppContainer State Management', () => {
};
beforeEach(() => {
persistentStateMock.reset();
vi.clearAllMocks();
mockIdeClient.getInstance.mockReturnValue(new Promise(() => {}));
@@ -400,6 +406,9 @@ describe('AppContainer State Management', () => {
elapsedTime: '0.0s',
currentLoadingPhrase: '',
});
mockedUseSuspend.mockReturnValue({
handleSuspend: vi.fn(),
});
mockedUseHookDisplayState.mockReturnValue([]);
mockedUseTerminalTheme.mockReturnValue(undefined);
mockedUseShellInactivityStatus.mockReturnValue({
@@ -439,8 +448,8 @@ describe('AppContainer State Management', () => {
...defaultMergedSettings.ui,
showStatusInTitle: false,
hideWindowTitle: false,
useAlternateBuffer: false,
},
useAlternateBuffer: false,
},
} as unknown as LoadedSettings;
@@ -480,6 +489,37 @@ describe('AppContainer State Management', () => {
await waitFor(() => expect(capturedUIState).toBeTruthy());
unmount!();
});
it('shows full UI details by default', async () => {
let unmount: () => void;
await act(async () => {
const result = renderAppContainer();
unmount = result.unmount;
});
await waitFor(() => {
expect(capturedUIState.cleanUiDetailsVisible).toBe(true);
});
unmount!();
});
it('starts in minimal UI mode when Focus UI preference is persisted', async () => {
persistentStateMock.get.mockReturnValueOnce(true);
let unmount: () => void;
await act(async () => {
const result = renderAppContainer({
settings: mockSettings,
});
unmount = result.unmount;
});
await waitFor(() => {
expect(capturedUIState.cleanUiDetailsVisible).toBe(false);
});
expect(persistentStateMock.get).toHaveBeenCalledWith('focusUiEnabled');
unmount!();
});
});
describe('State Initialization', () => {
@@ -726,10 +766,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({
@@ -760,11 +800,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({
@@ -800,10 +842,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,
@@ -834,10 +876,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: {
@@ -890,10 +932,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: {
@@ -943,10 +985,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,
@@ -1770,47 +1812,36 @@ describe('AppContainer State Management', () => {
});
describe('Keyboard Input Handling (CTRL+C / CTRL+D)', () => {
let handleGlobalKeypress: (key: Key) => boolean;
let mockHandleSlashCommand: Mock;
let mockCancelOngoingRequest: Mock;
let rerender: () => void;
let unmount: () => void;
let stdin: ReturnType<typeof render>['stdin'];
// Helper function to reduce boilerplate in tests
const setupKeypressTest = async () => {
const renderResult = renderAppContainer();
stdin = renderResult.stdin;
await act(async () => {
vi.advanceTimersByTime(0);
});
rerender = () => renderResult.rerender(getAppContainer());
rerender = () => {
renderResult.rerender(getAppContainer());
};
unmount = renderResult.unmount;
};
const pressKey = (key: Partial<Key>, times = 1) => {
const pressKey = (sequence: string, times = 1) => {
for (let i = 0; i < times; i++) {
act(() => {
handleGlobalKeypress({
name: 'c',
shift: false,
alt: false,
ctrl: false,
cmd: false,
...key,
} as Key);
stdin.write(sequence);
});
rerender();
}
};
beforeEach(() => {
// Capture the keypress handler from the AppContainer
mockedUseKeypress.mockImplementation(
(callback: (key: Key) => boolean) => {
handleGlobalKeypress = callback;
},
);
// Mock slash command handler
mockHandleSlashCommand = vi.fn();
mockedUseSlashCommandProcessor.mockReturnValue({
@@ -1855,7 +1886,7 @@ describe('AppContainer State Management', () => {
});
await setupKeypressTest();
pressKey({ name: 'c', ctrl: true });
pressKey('\x03'); // Ctrl+C
expect(mockCancelOngoingRequest).toHaveBeenCalledTimes(1);
expect(mockHandleSlashCommand).not.toHaveBeenCalled();
@@ -1865,7 +1896,7 @@ describe('AppContainer State Management', () => {
it('should quit on second press', async () => {
await setupKeypressTest();
pressKey({ name: 'c', ctrl: true }, 2);
pressKey('\x03', 2); // Ctrl+C
expect(mockCancelOngoingRequest).toHaveBeenCalledTimes(2);
expect(mockHandleSlashCommand).toHaveBeenCalledWith(
@@ -1880,7 +1911,7 @@ describe('AppContainer State Management', () => {
it('should reset press count after a timeout', async () => {
await setupKeypressTest();
pressKey({ name: 'c', ctrl: true });
pressKey('\x03'); // Ctrl+C
expect(mockHandleSlashCommand).not.toHaveBeenCalled();
// Advance timer past the reset threshold
@@ -1888,7 +1919,7 @@ describe('AppContainer State Management', () => {
vi.advanceTimersByTime(WARNING_PROMPT_DURATION_MS + 1);
});
pressKey({ name: 'c', ctrl: true });
pressKey('\x03'); // Ctrl+C
expect(mockHandleSlashCommand).not.toHaveBeenCalled();
unmount();
});
@@ -1898,7 +1929,7 @@ describe('AppContainer State Management', () => {
it('should quit on second press if buffer is empty', async () => {
await setupKeypressTest();
pressKey({ name: 'd', ctrl: true }, 2);
pressKey('\x04', 2); // Ctrl+D
expect(mockHandleSlashCommand).toHaveBeenCalledWith(
'/quit',
@@ -1909,7 +1940,7 @@ describe('AppContainer State Management', () => {
unmount();
});
it('should NOT quit if buffer is not empty (bubbles from InputPrompt)', async () => {
it('should NOT quit if buffer is not empty', async () => {
mockedUseTextBuffer.mockReturnValue({
text: 'some text',
setText: vi.fn(),
@@ -1919,30 +1950,12 @@ describe('AppContainer State Management', () => {
});
await setupKeypressTest();
// Capture return value
let result = true;
const originalPressKey = (key: Partial<Key>) => {
act(() => {
result = handleGlobalKeypress({
name: 'd',
shift: false,
alt: false,
ctrl: true,
cmd: false,
...key,
} as Key);
});
rerender();
};
pressKey('\x04'); // Ctrl+D
originalPressKey({ name: 'd', ctrl: true });
// AppContainer's handler should return true if it reaches it
expect(result).toBe(true);
// But it should only be called once, so count is 1, not quitting yet.
// Should only be called once, so count is 1, not quitting yet.
expect(mockHandleSlashCommand).not.toHaveBeenCalled();
originalPressKey({ name: 'd', ctrl: true });
pressKey('\x04'); // Ctrl+D
// Now count is 2, it should quit.
expect(mockHandleSlashCommand).toHaveBeenCalledWith(
'/quit',
@@ -1956,7 +1969,7 @@ describe('AppContainer State Management', () => {
it('should reset press count after a timeout', async () => {
await setupKeypressTest();
pressKey({ name: 'd', ctrl: true });
pressKey('\x04'); // Ctrl+D
expect(mockHandleSlashCommand).not.toHaveBeenCalled();
// Advance timer past the reset threshold
@@ -1964,12 +1977,25 @@ describe('AppContainer State Management', () => {
vi.advanceTimersByTime(WARNING_PROMPT_DURATION_MS + 1);
});
pressKey({ name: 'd', ctrl: true });
pressKey('\x04'); // Ctrl+D
expect(mockHandleSlashCommand).not.toHaveBeenCalled();
unmount();
});
});
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
@@ -1982,7 +2008,7 @@ describe('AppContainer State Management', () => {
it('should focus shell input on Tab', async () => {
await setupKeypressTest();
pressKey({ name: 'tab', shift: false });
pressKey('\t');
expect(capturedUIState.embeddedShellFocused).toBe(true);
unmount();
@@ -1992,11 +2018,11 @@ describe('AppContainer State Management', () => {
await setupKeypressTest();
// Focus first
pressKey({ name: 'tab', shift: false });
pressKey('\t');
expect(capturedUIState.embeddedShellFocused).toBe(true);
// Unfocus via Shift+Tab
pressKey({ name: 'tab', shift: true });
pressKey('\x1b[Z');
expect(capturedUIState.embeddedShellFocused).toBe(false);
unmount();
});
@@ -2015,13 +2041,7 @@ describe('AppContainer State Management', () => {
// Focus it
act(() => {
handleGlobalKeypress({
name: 'tab',
shift: false,
alt: false,
ctrl: false,
cmd: false,
} as Key);
renderResult.stdin.write('\t');
});
expect(capturedUIState.embeddedShellFocused).toBe(true);
@@ -2056,7 +2076,7 @@ describe('AppContainer State Management', () => {
expect(capturedUIState.embeddedShellFocused).toBe(false);
// Press Tab
pressKey({ name: 'tab', shift: false });
pressKey('\t');
// Should be focused
expect(capturedUIState.embeddedShellFocused).toBe(true);
@@ -2084,7 +2104,7 @@ describe('AppContainer State Management', () => {
expect(capturedUIState.embeddedShellFocused).toBe(false);
// Press Ctrl+B
pressKey({ name: 'b', ctrl: true });
pressKey('\x02');
// Should have toggled (closed) the shell
expect(mockToggleBackgroundShell).toHaveBeenCalled();
@@ -2113,7 +2133,7 @@ describe('AppContainer State Management', () => {
});
// Press Ctrl+B
pressKey({ name: 'b', ctrl: true });
pressKey('\x02');
// Should have toggled (shown) the shell
expect(mockToggleBackgroundShell).toHaveBeenCalled();
@@ -2125,12 +2145,137 @@ describe('AppContainer State Management', () => {
});
});
describe('Copy Mode (CTRL+S)', () => {
describe('Shortcuts Help Visibility', () => {
let handleGlobalKeypress: (key: Key) => boolean;
let mockedUseKeypress: Mock;
let rerender: () => void;
let unmount: () => void;
const setupCopyModeTest = async (isAlternateMode = false) => {
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;
let stdin: ReturnType<typeof render>['stdin'];
const setupCopyModeTest = async (
isAlternateMode = false,
childHandler?: Mock,
) => {
// Update settings for this test run
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
const testSettings = {
@@ -2144,23 +2289,39 @@ describe('AppContainer State Management', () => {
},
} as unknown as LoadedSettings;
const renderResult = renderAppContainer({ settings: testSettings });
function TestChild() {
useKeypress(childHandler || (() => {}), {
isActive: !!childHandler,
priority: true,
});
return null;
}
const getTree = (settings: LoadedSettings) => (
<SettingsContext.Provider value={settings}>
<KeypressProvider config={mockConfig}>
<AppContainer
config={mockConfig}
version="1.0.0"
initializationResult={mockInitResult}
/>
<TestChild />
</KeypressProvider>
</SettingsContext.Provider>
);
const renderResult = render(getTree(testSettings));
stdin = renderResult.stdin;
await act(async () => {
vi.advanceTimersByTime(0);
});
rerender = () =>
renderResult.rerender(getAppContainer({ settings: testSettings }));
rerender = () => renderResult.rerender(getTree(testSettings));
unmount = renderResult.unmount;
};
beforeEach(() => {
mocks.mockStdout.write.mockClear();
mockedUseKeypress.mockImplementation(
(callback: (key: Key) => boolean) => {
handleGlobalKeypress = callback;
},
);
vi.useFakeTimers();
});
@@ -2186,15 +2347,7 @@ describe('AppContainer State Management', () => {
mocks.mockStdout.write.mockClear(); // Clear initial enable call
act(() => {
handleGlobalKeypress({
name: 's',
shift: false,
alt: false,
ctrl: true,
cmd: false,
insertable: false,
sequence: '\x13',
});
stdin.write('\x13'); // Ctrl+S
});
rerender();
@@ -2213,30 +2366,14 @@ describe('AppContainer State Management', () => {
// Turn it on (disable mouse)
act(() => {
handleGlobalKeypress({
name: 's',
shift: false,
alt: false,
ctrl: true,
cmd: false,
insertable: false,
sequence: '\x13',
});
stdin.write('\x13'); // Ctrl+S
});
rerender();
expect(disableMouseEvents).toHaveBeenCalled();
// Turn it off (enable mouse)
act(() => {
handleGlobalKeypress({
name: 'any', // Any key should exit copy mode
shift: false,
alt: false,
ctrl: false,
cmd: false,
insertable: true,
sequence: 'a',
});
stdin.write('a'); // Any key should exit copy mode
});
rerender();
@@ -2249,15 +2386,7 @@ describe('AppContainer State Management', () => {
// Enter copy mode
act(() => {
handleGlobalKeypress({
name: 's',
shift: false,
alt: false,
ctrl: true,
cmd: false,
insertable: false,
sequence: '\x13',
});
stdin.write('\x13'); // Ctrl+S
});
rerender();
@@ -2265,15 +2394,7 @@ describe('AppContainer State Management', () => {
// Press any other key
act(() => {
handleGlobalKeypress({
name: 'a',
shift: false,
alt: false,
ctrl: false,
cmd: false,
insertable: true,
sequence: 'a',
});
stdin.write('a');
});
rerender();
@@ -2281,6 +2402,37 @@ describe('AppContainer State Management', () => {
expect(enableMouseEvents).toHaveBeenCalled();
unmount();
});
it('should have higher priority than other priority listeners when enabled', async () => {
// 1. Initial state with a child component's priority listener (already subscribed)
// It should NOT handle Ctrl+S so we can enter copy mode.
const childHandler = vi.fn().mockReturnValue(false);
await setupCopyModeTest(true, childHandler);
// 2. Enter copy mode
act(() => {
stdin.write('\x13'); // Ctrl+S
});
rerender();
// 3. Verify we are in copy mode
expect(disableMouseEvents).toHaveBeenCalled();
// 4. Press any key
childHandler.mockClear();
// Now childHandler should return true for other keys, simulating a greedy listener
childHandler.mockReturnValue(true);
act(() => {
stdin.write('a');
});
rerender();
// 5. Verify that the exit handler took priority and childHandler was NOT called
expect(childHandler).not.toHaveBeenCalled();
expect(enableMouseEvents).toHaveBeenCalled();
unmount();
});
}
});
});
+207 -53
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';
@@ -42,6 +49,7 @@ import {
type UserTierId,
type UserFeedbackPayload,
type AgentDefinition,
type ApprovalMode,
IdeClient,
ideContextStore,
getErrorMessage,
@@ -87,7 +95,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';
@@ -101,6 +108,7 @@ import { type LoadableSettingScope, SettingScope } from '../config/settings.js';
import { type InitializationResult } from '../core/initializer.js';
import { useFocus } from './hooks/useFocus.js';
import { useKeypress, type Key } from './hooks/useKeypress.js';
import { KeypressPriority } from './contexts/KeypressContext.js';
import { keyMatchers, Command } from './keyMatchers.js';
import { useLoadingIndicator } from './hooks/useLoadingIndicator.js';
import { useShellInactivityStatus } from './hooks/useShellInactivityStatus.js';
@@ -126,6 +134,7 @@ import { ShellFocusContext } from './contexts/ShellFocusContext.js';
import { type ExtensionManager } from '../config/extension-manager.js';
import { requestConsentInteractive } from '../config/extensions/consent.js';
import { useSessionBrowser } from './hooks/useSessionBrowser.js';
import { persistentState } from '../utils/persistentState.js';
import { useSessionResume } from './hooks/useSessionResume.js';
import { useIncludeDirsTrust } from './hooks/useIncludeDirsTrust.js';
import { isWorkspaceTrusted } from '../config/trustedFolders.js';
@@ -145,7 +154,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) => {
@@ -176,6 +186,9 @@ interface AppContainerProps {
resumedSessionData?: ResumedSessionData;
}
const APPROVAL_MODE_REVEAL_DURATION_MS = 1200;
const FOCUS_UI_ENABLED_STATE_KEY = 'focusUiEnabled';
/**
* The fraction of the terminal width to allocate to the shell.
* This provides horizontal padding.
@@ -199,6 +212,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
@@ -345,7 +359,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();
@@ -363,7 +377,9 @@ export const AppContainer = (props: AppContainerProps) => {
(async () => {
// Note: the program will not work if this fails so let errors be
// handled by the global catch.
await config.initialize();
if (!config.isInitialized()) {
await config.initialize();
}
setConfigInitialized(true);
startupProfiler.flush(config);
@@ -480,7 +496,7 @@ export const AppContainer = (props: AppContainerProps) => {
);
coreEvents.off(CoreEvent.AgentsDiscovered, handleAgentsDiscovered);
};
}, []);
}, [settings]);
const { consoleMessages, clearConsoleMessages: clearConsoleMessagesState } =
useConsoleMessages();
@@ -532,10 +548,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();
@@ -545,7 +564,7 @@ export const AppContainer = (props: AppContainerProps) => {
}
terminalCapabilityManager.enableSupportedModes();
refreshStatic();
}, [refreshStatic, isAlternateBuffer, app, config]);
}, [refreshStatic, shouldUseAlternateScreen, app]);
const [editorError, setEditorError] = useState<string | null>(null);
const {
@@ -593,7 +612,7 @@ export const AppContainer = (props: AppContainerProps) => {
);
// Poll for terminal background color changes to auto-switch theme
useTerminalTheme(handleThemeSelect, config);
useTerminalTheme(handleThemeSelect, config, refreshStatic);
const {
authState,
@@ -782,7 +801,65 @@ Logging in with Google... Restarting Gemini CLI to continue.
const setIsBackgroundShellListOpenRef = useRef<(open: boolean) => void>(
() => {},
);
const [focusUiEnabledByDefault] = useState(
() => persistentState.get(FOCUS_UI_ENABLED_STATE_KEY) === true,
);
const [shortcutsHelpVisible, setShortcutsHelpVisible] = useState(false);
const [cleanUiDetailsVisible, setCleanUiDetailsVisibleState] = useState(
!focusUiEnabledByDefault,
);
const modeRevealTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const cleanUiDetailsPinnedRef = useRef(!focusUiEnabledByDefault);
const clearModeRevealTimeout = useCallback(() => {
if (modeRevealTimeoutRef.current) {
clearTimeout(modeRevealTimeoutRef.current);
modeRevealTimeoutRef.current = null;
}
}, []);
const persistFocusUiPreference = useCallback((isFullUiVisible: boolean) => {
persistentState.set(FOCUS_UI_ENABLED_STATE_KEY, !isFullUiVisible);
}, []);
const setCleanUiDetailsVisible = useCallback(
(visible: boolean) => {
clearModeRevealTimeout();
cleanUiDetailsPinnedRef.current = visible;
setCleanUiDetailsVisibleState(visible);
persistFocusUiPreference(visible);
},
[clearModeRevealTimeout, persistFocusUiPreference],
);
const toggleCleanUiDetailsVisible = useCallback(() => {
clearModeRevealTimeout();
setCleanUiDetailsVisibleState((visible) => {
const nextVisible = !visible;
cleanUiDetailsPinnedRef.current = nextVisible;
persistFocusUiPreference(nextVisible);
return nextVisible;
});
}, [clearModeRevealTimeout, persistFocusUiPreference]);
const revealCleanUiDetailsTemporarily = useCallback(
(durationMs: number = APPROVAL_MODE_REVEAL_DURATION_MS) => {
if (cleanUiDetailsPinnedRef.current) {
return;
}
clearModeRevealTimeout();
setCleanUiDetailsVisibleState(true);
modeRevealTimeoutRef.current = setTimeout(() => {
if (!cleanUiDetailsPinnedRef.current) {
setCleanUiDetailsVisibleState(false);
}
modeRevealTimeoutRef.current = null;
}, durationMs);
},
[clearModeRevealTimeout],
);
useEffect(() => () => clearModeRevealTimeout(), [clearModeRevealTimeout]);
const slashCommandActions = useMemo(
() => ({
@@ -1043,11 +1120,25 @@ Logging in with Google... Restarting Gemini CLI to continue.
const shouldShowActionRequiredTitle = inactivityStatus === 'action_required';
const shouldShowSilentWorkingTitle = inactivityStatus === 'silent_working';
const handleApprovalModeChangeWithUiReveal = useCallback(
(mode: ApprovalMode) => {
void handleApprovalModeChange(mode);
if (!cleanUiDetailsVisible) {
revealCleanUiDetailsTemporarily(APPROVAL_MODE_REVEAL_DURATION_MS);
}
},
[
handleApprovalModeChange,
cleanUiDetailsVisible,
revealCleanUiDetailsTemporarily,
],
);
// Auto-accept indicator
const showApprovalModeIndicator = useApprovalModeIndicator({
config,
addItem: historyManager.addItem,
onApprovalModeChange: handleApprovalModeChange,
onApprovalModeChange: handleApprovalModeChangeWithUiReveal,
isActive: !embeddedShellFocused,
});
@@ -1363,9 +1454,30 @@ Logging in with Google... Restarting Gemini CLI to continue.
if (tabFocusTimeoutRef.current) {
clearTimeout(tabFocusTimeoutRef.current);
}
if (modeRevealTimeoutRef.current) {
clearTimeout(modeRevealTimeoutRef.current);
}
};
}, [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.
@@ -1481,18 +1593,15 @@ Logging in with Google... Restarting Gemini CLI to continue.
const handleGlobalKeypress = useCallback(
(key: Key): boolean => {
if (copyModeEnabled) {
setCopyModeEnabled(false);
enableMouseEvents();
// We don't want to process any other keys if we're in copy mode.
return true;
}
// Debug log keystrokes if enabled
if (settings.merged.general.debugKeystrokeLogging) {
debugLogger.log('[DEBUG] Keystroke:', JSON.stringify(key));
}
if (shortcutsHelpVisible && shouldDismissShortcutsHelpOnHotkey(key)) {
setShortcutsHelpVisible(false);
}
if (isAlternateBuffer && keyMatchers[Command.TOGGLE_COPY_MODE](key)) {
setCopyModeEnabled(true);
disableMouseEvents();
@@ -1509,6 +1618,17 @@ 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();
} else if (
keyMatchers[Command.TOGGLE_COPY_MODE](key) &&
!isAlternateBuffer
) {
showTransientMessage({
text: 'Use Ctrl+O to expand and collapse blocks of content.',
type: TransientMessageType.Warning,
});
return true;
}
let enteringConstrainHeightMode = false;
@@ -1520,42 +1640,20 @@ Logging in with Google... Restarting Gemini CLI to continue.
if (keyMatchers[Command.SHOW_ERROR_DETAILS](key)) {
if (settings.merged.general.devtools) {
void (async () => {
try {
const { startDevToolsServer } = await import(
'../utils/devtoolsService.js'
);
const { openBrowserSecurely, shouldLaunchBrowser } = await import(
'@google/gemini-cli-core'
);
const url = await startDevToolsServer(config);
if (shouldLaunchBrowser()) {
try {
await openBrowserSecurely(url);
} catch (e) {
setShowErrorDetails((prev) => !prev);
debugLogger.warn('Failed to open browser securely:', e);
}
} else {
setShowErrorDetails((prev) => !prev);
}
} catch (e) {
setShowErrorDetails(true);
debugLogger.error('Failed to start DevTools server:', e);
}
const { toggleDevToolsPanel } = await import(
'../utils/devtoolsService.js'
);
await toggleDevToolsPanel(
config,
showErrorDetails,
() => setShowErrorDetails((prev) => !prev),
() => setShowErrorDetails(true),
);
})();
} else {
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;
@@ -1664,26 +1762,42 @@ Logging in with Google... Restarting Gemini CLI to continue.
handleSlashCommand,
cancelOngoingRequest,
activePtyId,
handleSuspend,
embeddedShellFocused,
settings.merged.general.debugKeystrokeLogging,
refreshStatic,
setCopyModeEnabled,
copyModeEnabled,
tabFocusTimeoutRef,
isAlternateBuffer,
shortcutsHelpVisible,
backgroundCurrentShell,
toggleBackgroundShell,
backgroundShells,
isBackgroundShellVisible,
setIsBackgroundShellListOpen,
lastOutputTimeRef,
tabFocusTimeoutRef,
showTransientMessage,
settings.merged.general.devtools,
showErrorDetails,
],
);
useKeypress(handleGlobalKeypress, { isActive: true, priority: true });
useKeypress(
() => {
setCopyModeEnabled(false);
enableMouseEvents();
return true;
},
{
isActive: copyModeEnabled,
// We need to receive keypresses first so they do not bubble to other
// handlers.
priority: KeypressPriority.Critical,
},
);
useEffect(() => {
// Respect hideWindowTitle settings
if (settings.merged.ui.hideWindowTitle) return;
@@ -1814,6 +1928,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
@@ -1921,6 +2065,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
ctrlDPressedOnce: ctrlDPressCount >= 1,
showEscapePrompt,
shortcutsHelpVisible,
cleanUiDetailsVisible,
isFocused,
elapsedTime,
currentLoadingPhrase,
@@ -2031,6 +2176,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
ctrlDPressCount,
showEscapePrompt,
shortcutsHelpVisible,
cleanUiDetailsVisible,
isFocused,
elapsedTime,
currentLoadingPhrase,
@@ -2132,6 +2278,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
handleApiKeyCancel,
setBannerVisible,
setShortcutsHelpVisible,
setCleanUiDetailsVisible,
toggleCleanUiDetailsVisible,
revealCleanUiDetailsTemporarily,
handleWarning,
setEmbeddedShellFocused,
dismissBackgroundShell,
setActiveBackgroundShellPid,
@@ -2208,6 +2358,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
handleApiKeyCancel,
setBannerVisible,
setShortcutsHelpVisible,
setCleanUiDetailsVisible,
toggleCleanUiDetailsVisible,
revealCleanUiDetailsTemporarily,
handleWarning,
setEmbeddedShellFocused,
dismissBackgroundShell,
setActiveBackgroundShellPid,
@@ -2243,7 +2397,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>
@@ -110,6 +110,7 @@ describe('ApiAuthDialog', () => {
keypressHandler({
name: keyName,
shift: false,
alt: false,
ctrl: false,
cmd: false,
sequence,
+2 -2
View File
@@ -15,7 +15,7 @@ export const Colors: ColorsTheme = {
return themeManager.getActiveTheme().colors.Foreground;
},
get Background() {
return themeManager.getActiveTheme().colors.Background;
return themeManager.getColors().Background;
},
get LightBlue() {
return themeManager.getActiveTheme().colors.LightBlue;
@@ -51,7 +51,7 @@ export const Colors: ColorsTheme = {
return themeManager.getActiveTheme().colors.Gray;
},
get DarkGray() {
return themeManager.getActiveTheme().colors.DarkGray;
return themeManager.getColors().DarkGray;
},
get GradientColors() {
return themeManager.getActiveTheme().colors.GradientColors;
@@ -0,0 +1,89 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { deepworkCommand } from './deepworkCommand.js';
import { type CommandContext } from './types.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
import { ApprovalMode, coreEvents } from '@google/gemini-cli-core';
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
coreEvents: {
emitFeedback: vi.fn(),
},
};
});
describe('deepworkCommand', () => {
let mockContext: CommandContext;
beforeEach(() => {
mockContext = createMockCommandContext({
services: {
config: {
isDeepWorkEnabled: vi.fn(),
setApprovalMode: vi.fn(),
getApprovalMode: vi.fn(),
getApprovedPlanPath: vi.fn(),
},
},
ui: {
addItem: vi.fn(),
},
} as unknown as CommandContext);
vi.clearAllMocks();
});
it('should have the correct name and description', () => {
expect(deepworkCommand.name).toBe('deepwork');
expect(deepworkCommand.description).toBe(
'Switch to Deep Work mode for iterative execution',
);
});
it('should switch to deep work mode when enabled', async () => {
vi.mocked(mockContext.services.config!.isDeepWorkEnabled).mockReturnValue(
true,
);
vi.mocked(mockContext.services.config!.getApprovalMode).mockReturnValue(
ApprovalMode.DEFAULT,
);
vi.mocked(mockContext.services.config!.getApprovedPlanPath).mockReturnValue(
undefined,
);
if (!deepworkCommand.action) throw new Error('Action missing');
await deepworkCommand.action(mockContext, '');
expect(mockContext.services.config!.setApprovalMode).toHaveBeenCalledWith(
ApprovalMode.DEEP_WORK,
);
expect(coreEvents.emitFeedback).toHaveBeenCalledWith(
'info',
'Switched to Deep Work mode.',
);
});
it('should emit error when deep work mode is disabled', async () => {
vi.mocked(mockContext.services.config!.isDeepWorkEnabled).mockReturnValue(
false,
);
if (!deepworkCommand.action) throw new Error('Action missing');
await deepworkCommand.action(mockContext, '');
expect(mockContext.services.config!.setApprovalMode).not.toHaveBeenCalled();
expect(coreEvents.emitFeedback).toHaveBeenCalledWith(
'error',
'Deep Work mode is disabled. Enable experimental.deepWork in settings first.',
);
});
});
@@ -0,0 +1,47 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { CommandKind, type SlashCommand } from './types.js';
import { ApprovalMode, coreEvents, debugLogger } from '@google/gemini-cli-core';
export const deepworkCommand: SlashCommand = {
name: 'deepwork',
description: 'Switch to Deep Work mode for iterative execution',
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (context) => {
const config = context.services.config;
if (!config) {
debugLogger.debug(
'Deep Work command: config is not available in context',
);
return;
}
if (!config.isDeepWorkEnabled()) {
coreEvents.emitFeedback(
'error',
'Deep Work mode is disabled. Enable experimental.deepWork in settings first.',
);
return;
}
const previousApprovalMode = config.getApprovalMode();
config.setApprovalMode(ApprovalMode.DEEP_WORK);
if (previousApprovalMode !== ApprovalMode.DEEP_WORK) {
coreEvents.emitFeedback('info', 'Switched to Deep Work mode.');
}
const approvedPlanPath = config.getApprovedPlanPath();
if (approvedPlanPath) {
coreEvents.emitFeedback(
'info',
`Deep Work will follow approved plan: ${approvedPlanPath}`,
);
}
},
};
@@ -106,6 +106,12 @@ describe('policiesCommand', () => {
expect(content).toContain(
'### Yolo Mode Policies (combined with normal mode policies)',
);
expect(content).toContain(
'### Plan Mode Policies (combined with normal mode policies)',
);
expect(content).toContain(
'### Deep Work Mode Policies (combined with normal mode policies)',
);
expect(content).toContain(
'**DENY** tool: `dangerousTool` [Priority: 10]',
);
@@ -12,6 +12,8 @@ interface CategorizedRules {
normal: PolicyRule[];
autoEdit: PolicyRule[];
yolo: PolicyRule[];
plan: PolicyRule[];
deepWork: PolicyRule[];
}
const categorizeRulesByMode = (
@@ -21,6 +23,8 @@ const categorizeRulesByMode = (
normal: [],
autoEdit: [],
yolo: [],
plan: [],
deepWork: [],
};
const ALL_MODES = Object.values(ApprovalMode);
rules.forEach((rule) => {
@@ -29,6 +33,8 @@ const categorizeRulesByMode = (
if (modeSet.has(ApprovalMode.DEFAULT)) result.normal.push(rule);
if (modeSet.has(ApprovalMode.AUTO_EDIT)) result.autoEdit.push(rule);
if (modeSet.has(ApprovalMode.YOLO)) result.yolo.push(rule);
if (modeSet.has(ApprovalMode.PLAN)) result.plan.push(rule);
if (modeSet.has(ApprovalMode.DEEP_WORK)) result.deepWork.push(rule);
});
return result;
};
@@ -82,6 +88,12 @@ const listPoliciesCommand: SlashCommand = {
const uniqueYolo = categorized.yolo.filter(
(rule) => !normalRulesSet.has(rule),
);
const uniquePlan = categorized.plan.filter(
(rule) => !normalRulesSet.has(rule),
);
const uniqueDeepWork = categorized.deepWork.filter(
(rule) => !normalRulesSet.has(rule),
);
let content = '**Active Policies**\n\n';
content += formatSection('Normal Mode Policies', categorized.normal);
@@ -93,6 +105,14 @@ const listPoliciesCommand: SlashCommand = {
'Yolo Mode Policies (combined with normal mode policies)',
uniqueYolo,
);
content += formatSection(
'Plan Mode Policies (combined with normal mode policies)',
uniquePlan,
);
content += formatSection(
'Deep Work Mode Policies (combined with normal mode policies)',
uniqueDeepWork,
);
context.ui.addItem(
{
@@ -182,7 +182,6 @@ describe('AlternateBufferQuittingDisplay', () => {
type: 'info',
title: 'Confirm Tool',
prompt: 'Confirm this action?',
onConfirm: async () => {},
},
},
],
@@ -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}
/>
+10 -1
View File
@@ -17,9 +17,10 @@ import { useTips } from '../hooks/useTips.js';
interface AppHeaderProps {
version: string;
showDetails?: boolean;
}
export const AppHeader = ({ version }: AppHeaderProps) => {
export const AppHeader = ({ version, showDetails = true }: AppHeaderProps) => {
const settings = useSettings();
const config = useConfig();
const { nightly, terminalWidth, bannerData, bannerVisible } = useUIState();
@@ -27,6 +28,14 @@ export const AppHeader = ({ version }: AppHeaderProps) => {
const { bannerText } = useBanner(bannerData);
const { showTips } = useTips();
if (!showDetails) {
return (
<Box flexDirection="column">
<Header version={version} nightly={false} />
</Box>
);
}
return (
<Box flexDirection="column">
{!(settings.merged.ui.hideBanner || config.getScreenReader()) && (
@@ -40,6 +40,27 @@ describe('ApprovalModeIndicator', () => {
expect(output).toContain('shift+tab to accept edits');
});
it('renders correctly for PLAN mode with deep work enabled', () => {
const { lastFrame } = render(
<ApprovalModeIndicator
approvalMode={ApprovalMode.PLAN}
isDeepWorkEnabled={true}
/>,
);
const output = lastFrame();
expect(output).toContain('plan');
expect(output).toContain('shift+tab to deep work');
});
it('renders correctly for DEEP_WORK mode', () => {
const { lastFrame } = render(
<ApprovalModeIndicator approvalMode={ApprovalMode.DEEP_WORK} />,
);
const output = lastFrame();
expect(output).toContain('deep work');
expect(output).toContain('shift+tab to accept edits');
});
it('renders correctly for YOLO mode', () => {
const { lastFrame } = render(
<ApprovalModeIndicator approvalMode={ApprovalMode.YOLO} />,
@@ -67,4 +88,15 @@ describe('ApprovalModeIndicator', () => {
const output = lastFrame();
expect(output).toContain('shift+tab to plan');
});
it('renders correctly for DEFAULT mode with deep work enabled', () => {
const { lastFrame } = render(
<ApprovalModeIndicator
approvalMode={ApprovalMode.DEFAULT}
isDeepWorkEnabled={true}
/>,
);
const output = lastFrame();
expect(output).toContain('shift+tab to deep work');
});
});
@@ -12,11 +12,13 @@ import { ApprovalMode } from '@google/gemini-cli-core';
interface ApprovalModeIndicatorProps {
approvalMode: ApprovalMode;
isPlanEnabled?: boolean;
isDeepWorkEnabled?: boolean;
}
export const ApprovalModeIndicator: React.FC<ApprovalModeIndicatorProps> = ({
approvalMode,
isPlanEnabled,
isDeepWorkEnabled,
}) => {
let textColor = '';
let textContent = '';
@@ -31,6 +33,13 @@ export const ApprovalModeIndicator: React.FC<ApprovalModeIndicatorProps> = ({
case ApprovalMode.PLAN:
textColor = theme.status.success;
textContent = 'plan';
subText = isDeepWorkEnabled
? 'shift+tab to deep work'
: 'shift+tab to accept edits';
break;
case ApprovalMode.DEEP_WORK:
textColor = theme.status.success;
textContent = 'deep work';
subText = 'shift+tab to accept edits';
break;
case ApprovalMode.YOLO:
@@ -44,7 +53,9 @@ export const ApprovalModeIndicator: React.FC<ApprovalModeIndicatorProps> = ({
textContent = '';
subText = isPlanEnabled
? 'shift+tab to plan'
: 'shift+tab to accept edits';
: isDeepWorkEnabled
? 'shift+tab to deep work'
: 'shift+tab to accept edits';
break;
}
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, afterEach } from 'vitest';
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
import { act } from 'react';
import { renderWithProviders } from '../../test-utils/render.js';
import { waitFor } from '../../test-utils/async.js';
@@ -21,6 +21,14 @@ const writeKey = (stdin: { write: (data: string) => void }, key: string) => {
};
describe('AskUserDialog', () => {
// Ensure keystrokes appear spaced in time to avoid bufferFastReturn
// converting Enter into Shift+Enter during synchronous test execution.
let mockTime: number;
beforeEach(() => {
mockTime = 0;
vi.spyOn(Date, 'now').mockImplementation(() => (mockTime += 50));
});
afterEach(() => {
vi.restoreAllMocks();
});
@@ -158,6 +166,57 @@ describe('AskUserDialog', () => {
});
});
it('supports multi-line input for "Other" option in choice questions', async () => {
const authQuestionWithOther: Question[] = [
{
question: 'Which authentication method?',
header: 'Auth',
options: [{ label: 'OAuth 2.0', description: '' }],
multiSelect: false,
},
];
const onSubmit = vi.fn();
const { stdin, lastFrame } = renderWithProviders(
<AskUserDialog
questions={authQuestionWithOther}
onSubmit={onSubmit}
onCancel={vi.fn()}
width={120}
/>,
{ width: 120 },
);
// Navigate to "Other" option
writeKey(stdin, '\x1b[B'); // Down to "Other"
// Type first line
for (const char of 'Line 1') {
writeKey(stdin, char);
}
// Insert newline using \ + Enter (handled by bufferBackslashEnter)
writeKey(stdin, '\\');
writeKey(stdin, '\r');
// Type second line
for (const char of 'Line 2') {
writeKey(stdin, char);
}
await waitFor(() => {
expect(lastFrame()).toContain('Line 1');
expect(lastFrame()).toContain('Line 2');
});
// Press Enter to submit
writeKey(stdin, '\r');
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({ '0': 'Line 1\nLine 2' });
});
});
describe.each([
{ useAlternateBuffer: true, expectedArrows: false },
{ useAlternateBuffer: false, expectedArrows: true },
@@ -763,7 +822,7 @@ describe('AskUserDialog', () => {
});
});
it('does not submit empty text', () => {
it('submits empty text as unanswered', async () => {
const textQuestion: Question[] = [
{
question: 'Enter the class name:',
@@ -785,8 +844,9 @@ describe('AskUserDialog', () => {
writeKey(stdin, '\r');
// onSubmit should not be called for empty text
expect(onSubmit).not.toHaveBeenCalled();
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({});
});
});
it('clears text on Ctrl+C', async () => {
@@ -93,6 +93,7 @@ type AskUserDialogAction =
payload: {
index: number;
answer: string;
submit?: boolean;
};
}
| { type: 'SET_EDITING_CUSTOM'; payload: { isEditing: boolean } }
@@ -114,7 +115,7 @@ function askUserDialogReducerLogic(
switch (action.type) {
case 'SET_ANSWER': {
const { index, answer } = action.payload;
const { index, answer, submit } = action.payload;
const hasAnswer =
answer !== undefined && answer !== null && answer.trim() !== '';
const newAnswers = { ...state.answers };
@@ -128,6 +129,7 @@ function askUserDialogReducerLogic(
return {
...state,
answers: newAnswers,
submitted: submit ? true : state.submitted,
};
}
case 'SET_EDITING_CUSTOM': {
@@ -283,8 +285,8 @@ const TextQuestionView: React.FC<TextQuestionViewProps> = ({
const buffer = useTextBuffer({
initialText: initialAnswer,
viewport: { width: Math.max(1, bufferWidth), height: 1 },
singleLine: true,
viewport: { width: Math.max(1, bufferWidth), height: 3 },
singleLine: false,
});
const { text: textValue } = buffer;
@@ -317,9 +319,7 @@ const TextQuestionView: React.FC<TextQuestionViewProps> = ({
const handleSubmit = useCallback(
(val: string) => {
if (val.trim()) {
onAnswer(val.trim());
}
onAnswer(val.trim());
},
[onAnswer],
);
@@ -561,8 +561,8 @@ const ChoiceQuestionView: React.FC<ChoiceQuestionViewProps> = ({
const customBuffer = useTextBuffer({
initialText: initialCustomText,
viewport: { width: Math.max(1, bufferWidth), height: 1 },
singleLine: true,
viewport: { width: Math.max(1, bufferWidth), height: 3 },
singleLine: false,
});
const customOptionText = customBuffer.text;
@@ -850,9 +850,22 @@ const ChoiceQuestionView: React.FC<ChoiceQuestionViewProps> = ({
buffer={customBuffer}
placeholder={placeholder}
focus={context.isSelected}
onSubmit={() => handleSelect(optionItem)}
onSubmit={(val) => {
if (question.multiSelect) {
const fullAnswer = buildAnswerString(
selectedIndices,
true,
val,
);
if (fullAnswer) {
onAnswer(fullAnswer);
}
} else if (val.trim()) {
onAnswer(val.trim());
}
}}
/>
{isChecked && !question.multiSelect && (
{isChecked && !question.multiSelect && !context.isSelected && (
<Text color={theme.status.success}> </Text>
)}
</Box>
@@ -1012,21 +1025,27 @@ export const AskUserDialog: React.FC<AskUserDialogProps> = ({
(answer: string) => {
if (submitted) return;
dispatch({
type: 'SET_ANSWER',
payload: {
index: currentQuestionIndex,
answer,
},
});
if (questions.length > 1) {
dispatch({
type: 'SET_ANSWER',
payload: {
index: currentQuestionIndex,
answer,
},
});
goToNextTab();
} else {
dispatch({ type: 'SUBMIT' });
dispatch({
type: 'SET_ANSWER',
payload: {
index: currentQuestionIndex,
answer,
submit: true,
},
});
}
},
[currentQuestionIndex, questions.length, submitted, goToNextTab],
[currentQuestionIndex, questions, submitted, goToNextTab],
);
const handleReviewSubmit = useCallback(() => {
+276 -10
View File
@@ -4,9 +4,10 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, afterEach } from 'vitest';
import { beforeEach, afterEach, describe, it, expect, vi } from 'vitest';
import { render } from '../../test-utils/render.js';
import { Box, Text } from 'ink';
import { useEffect } from 'react';
import { Composer } from './Composer.js';
import { UIStateContext, type UIState } from '../contexts/UIStateContext.js';
import {
@@ -23,13 +24,18 @@ vi.mock('../contexts/VimModeContext.js', () => ({
vimMode: 'INSERT',
})),
}));
import { ApprovalMode } from '@google/gemini-cli-core';
import { ApprovalMode, tokenLimit } from '@google/gemini-cli-core';
import type { Config } from '@google/gemini-cli-core';
import { StreamingState, ToolCallStatus } from '../types.js';
import { TransientMessageType } from '../../utils/events.js';
import type { LoadedSettings } from '../../config/settings.js';
import type { SessionMetrics } from '../contexts/SessionContext.js';
const composerTestControls = vi.hoisted(() => ({
suggestionsVisible: false,
isAlternateBuffer: false,
}));
// Mock child components
vi.mock('./LoadingIndicator.js', () => ({
LoadingIndicator: ({
@@ -90,9 +96,19 @@ vi.mock('./DetailedMessagesDisplay.js', () => ({
}));
vi.mock('./InputPrompt.js', () => ({
InputPrompt: ({ placeholder }: { placeholder?: string }) => (
<Text>InputPrompt: {placeholder}</Text>
),
InputPrompt: ({
placeholder,
onSuggestionsVisibilityChange,
}: {
placeholder?: string;
onSuggestionsVisibilityChange?: (visible: boolean) => void;
}) => {
useEffect(() => {
onSuggestionsVisibilityChange?.(composerTestControls.suggestionsVisible);
}, [onSuggestionsVisibilityChange]);
return <Text>InputPrompt: {placeholder}</Text>;
},
calculatePromptWidths: vi.fn(() => ({
inputWidth: 80,
suggestionsWidth: 40,
@@ -100,6 +116,10 @@ vi.mock('./InputPrompt.js', () => ({
})),
}));
vi.mock('../hooks/useAlternateBuffer.js', () => ({
useAlternateBuffer: () => composerTestControls.isAlternateBuffer,
}));
vi.mock('./Footer.js', () => ({
Footer: () => <Text>Footer</Text>,
}));
@@ -154,15 +174,19 @@ const createMockUIState = (overrides: Partial<UIState> = {}): UIState =>
ctrlDPressedOnce: false,
showEscapePrompt: false,
shortcutsHelpVisible: false,
cleanUiDetailsVisible: true,
ideContextState: null,
geminiMdFileCount: 0,
renderMarkdown: true,
filteredConsoleMessages: [],
history: [],
sessionStats: {
sessionId: 'test-session',
sessionStartTime: new Date(),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
metrics: {} as any,
lastPromptTokenCount: 0,
sessionTokenCount: 0,
totalPrompts: 0,
promptCount: 0,
},
branchName: 'main',
debugMessage: '',
@@ -187,8 +211,12 @@ const createMockUIActions = (): UIActions =>
handleFinalSubmit: vi.fn(),
handleClearScreen: vi.fn(),
setShellModeActive: vi.fn(),
setCleanUiDetailsVisible: vi.fn(),
toggleCleanUiDetailsVisible: vi.fn(),
revealCleanUiDetailsTemporarily: vi.fn(),
onEscapePromptChange: vi.fn(),
vimHandleInput: vi.fn(),
setShortcutsHelpVisible: vi.fn(),
}) as Partial<UIActions> as UIActions;
const createMockConfig = (overrides = {}): Config =>
@@ -232,6 +260,11 @@ const renderComposer = (
);
describe('Composer', () => {
beforeEach(() => {
composerTestControls.suggestionsVisible = false;
composerTestControls.isAlternateBuffer = false;
});
afterEach(() => {
vi.restoreAllMocks();
});
@@ -337,17 +370,18 @@ 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,
cleanUiDetailsVisible: false,
});
const { lastFrame } = renderComposer(uiState);
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', () => {
@@ -513,6 +547,21 @@ describe('Composer', () => {
});
describe('Input and Indicators', () => {
it('hides non-essential UI details in clean mode', () => {
const uiState = createMockUIState({
cleanUiDetailsVisible: false,
});
const { lastFrame } = renderComposer(uiState);
const output = lastFrame();
expect(output).toContain('ShortcutsHint');
expect(output).toContain('InputPrompt');
expect(output).not.toContain('Footer');
expect(output).not.toContain('ApprovalModeIndicator');
expect(output).not.toContain('ContextSummaryDisplay');
});
it('renders InputPrompt when input is active', () => {
const uiState = createMockUIState({
isInputActive: true,
@@ -537,6 +586,7 @@ describe('Composer', () => {
[ApprovalMode.DEFAULT],
[ApprovalMode.AUTO_EDIT],
[ApprovalMode.PLAN],
[ApprovalMode.DEEP_WORK],
[ApprovalMode.YOLO],
])(
'shows ApprovalModeIndicator when approval mode is %s and shell mode is inactive',
@@ -581,6 +631,93 @@ describe('Composer', () => {
expect(lastFrame()).not.toContain('raw markdown mode');
});
it.each([
[ApprovalMode.YOLO, 'YOLO'],
[ApprovalMode.PLAN, 'plan'],
[ApprovalMode.DEEP_WORK, 'deep work'],
[ApprovalMode.AUTO_EDIT, 'auto edit'],
])(
'shows minimal mode badge "%s" when clean UI details are hidden',
(mode, label) => {
const uiState = createMockUIState({
cleanUiDetailsVisible: false,
showApprovalModeIndicator: mode,
});
const { lastFrame } = renderComposer(uiState);
expect(lastFrame()).toContain(label);
},
);
it('hides minimal mode badge while loading in clean mode', () => {
const uiState = createMockUIState({
cleanUiDetailsVisible: false,
streamingState: StreamingState.Responding,
elapsedTime: 1,
showApprovalModeIndicator: ApprovalMode.PLAN,
});
const { lastFrame } = renderComposer(uiState);
const output = lastFrame();
expect(output).toContain('LoadingIndicator');
expect(output).not.toContain('plan');
expect(output).not.toContain('ShortcutsHint');
});
it('hides minimal mode badge while action-required state is active', () => {
const uiState = createMockUIState({
cleanUiDetailsVisible: false,
showApprovalModeIndicator: ApprovalMode.PLAN,
customDialog: (
<Box>
<Text>Prompt</Text>
</Box>
),
});
const { lastFrame } = renderComposer(uiState);
const output = lastFrame();
expect(output).not.toContain('plan');
expect(output).not.toContain('ShortcutsHint');
});
it('shows Esc rewind prompt in minimal mode without showing full UI', () => {
const uiState = createMockUIState({
cleanUiDetailsVisible: false,
showEscapePrompt: true,
history: [{ id: 1, type: 'user', text: 'msg' }],
});
const { lastFrame } = renderComposer(uiState);
const output = lastFrame();
expect(output).toContain('ToastDisplay');
expect(output).not.toContain('ContextSummaryDisplay');
});
it('shows context usage bleed-through when over 60%', () => {
const model = 'gemini-2.5-pro';
const uiState = createMockUIState({
cleanUiDetailsVisible: false,
currentModel: model,
sessionStats: {
sessionId: 'test-session',
sessionStartTime: new Date(),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
metrics: {} as any,
lastPromptTokenCount: Math.floor(tokenLimit(model) * 0.7),
promptCount: 0,
},
});
const settings = createMockSettings({
ui: {
footer: { hideContextPercentage: false },
},
});
const { lastFrame } = renderComposer(uiState, settings);
expect(lastFrame()).toContain('%');
});
});
describe('Error Details Display', () => {
@@ -650,6 +787,19 @@ describe('Composer', () => {
});
describe('Shortcuts Hint', () => {
it('hides shortcuts hint when showShortcutsHint setting is false', () => {
const uiState = createMockUIState();
const settings = createMockSettings({
ui: {
showShortcutsHint: false,
},
});
const { lastFrame } = renderComposer(uiState, settings);
expect(lastFrame()).not.toContain('ShortcutsHint');
});
it('hides shortcuts hint when a action is required (e.g. dialog is open)', () => {
const uiState = createMockUIState({
customDialog: (
@@ -666,11 +816,127 @@ describe('Composer', () => {
});
it('keeps shortcuts hint visible when no action is required', () => {
const uiState = createMockUIState();
const uiState = createMockUIState({
cleanUiDetailsVisible: false,
});
const { lastFrame } = renderComposer(uiState);
expect(lastFrame()).toContain('ShortcutsHint');
});
it('shows shortcuts hint when full UI details are visible', () => {
const uiState = createMockUIState({
cleanUiDetailsVisible: true,
});
const { lastFrame } = renderComposer(uiState);
expect(lastFrame()).toContain('ShortcutsHint');
});
it('hides shortcuts hint while loading in minimal mode', () => {
const uiState = createMockUIState({
cleanUiDetailsVisible: false,
streamingState: StreamingState.Responding,
elapsedTime: 1,
});
const { lastFrame } = renderComposer(uiState);
expect(lastFrame()).not.toContain('ShortcutsHint');
});
it('shows shortcuts help in minimal mode when toggled on', () => {
const uiState = createMockUIState({
cleanUiDetailsVisible: false,
shortcutsHelpVisible: true,
});
const { lastFrame } = renderComposer(uiState);
expect(lastFrame()).toContain('ShortcutsHelp');
});
it('hides shortcuts hint when suggestions are visible above input in alternate buffer', () => {
composerTestControls.isAlternateBuffer = true;
composerTestControls.suggestionsVisible = true;
const uiState = createMockUIState({
cleanUiDetailsVisible: false,
showApprovalModeIndicator: ApprovalMode.PLAN,
});
const { lastFrame } = renderComposer(uiState);
expect(lastFrame()).not.toContain('ShortcutsHint');
expect(lastFrame()).not.toContain('plan');
});
it('hides approval mode indicator when suggestions are visible above input in alternate buffer', () => {
composerTestControls.isAlternateBuffer = true;
composerTestControls.suggestionsVisible = true;
const uiState = createMockUIState({
cleanUiDetailsVisible: true,
showApprovalModeIndicator: ApprovalMode.YOLO,
});
const { lastFrame } = renderComposer(uiState);
expect(lastFrame()).not.toContain('ApprovalModeIndicator');
});
it('keeps shortcuts hint when suggestions are visible below input in regular buffer', () => {
composerTestControls.isAlternateBuffer = false;
composerTestControls.suggestionsVisible = true;
const uiState = createMockUIState({
cleanUiDetailsVisible: false,
});
const { lastFrame } = renderComposer(uiState);
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');
});
});
});
+254 -78
View File
@@ -4,8 +4,9 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { useState } from 'react';
import { Box, useIsScreenReaderEnabled } from 'ink';
import { useState, useEffect, useMemo } from 'react';
import { Box, Text, useIsScreenReaderEnabled } from 'ink';
import { ApprovalMode, tokenLimit } from '@google/gemini-cli-core';
import { LoadingIndicator } from './LoadingIndicator.js';
import { StatusDisplay } from './StatusDisplay.js';
import { ToastDisplay, shouldShowToast } from './ToastDisplay.js';
@@ -19,6 +20,7 @@ import { InputPrompt } from './InputPrompt.js';
import { Footer } from './Footer.js';
import { ShowMoreLines } from './ShowMoreLines.js';
import { QueuedMessageDisplay } from './QueuedMessageDisplay.js';
import { ContextUsageDisplay } from './ContextUsageDisplay.js';
import { HorizontalLine } from './shared/HorizontalLine.js';
import { OverflowProvider } from '../contexts/OverflowContext.js';
import { isNarrowWidth } from '../utils/isNarrowWidth.js';
@@ -28,10 +30,15 @@ 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';
import { theme } from '../semantic-colors.js';
export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
const config = useConfig();
@@ -48,14 +55,23 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
const isAlternateBuffer = useAlternateBuffer();
const { showApprovalModeIndicator } = uiState;
const showUiDetails = uiState.cleanUiDetailsVisible;
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,13 +81,83 @@ 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 hasToast = shouldShowToast(uiState);
const showLoadingIndicator =
(!uiState.embeddedShellFocused || uiState.isBackgroundShellVisible) &&
uiState.streamingState === StreamingState.Responding &&
!hasPendingActionRequired;
const showApprovalIndicator = !uiState.shellModeActive;
const hideUiDetailsForSuggestions =
suggestionsVisible && suggestionsPosition === 'above';
const showApprovalIndicator =
!uiState.shellModeActive && !hideUiDetailsForSuggestions;
const showRawMarkdownIndicator = !uiState.renderMarkdown;
const modeBleedThrough =
showApprovalModeIndicator === ApprovalMode.YOLO
? { text: 'YOLO', color: theme.status.error }
: showApprovalModeIndicator === ApprovalMode.PLAN
? { text: 'plan', color: theme.status.success }
: showApprovalModeIndicator === ApprovalMode.DEEP_WORK
? { text: 'deep work', color: theme.status.success }
: showApprovalModeIndicator === ApprovalMode.AUTO_EDIT
? { text: 'auto edit', color: theme.status.warning }
: null;
const hideMinimalModeHintWhileBusy =
!showUiDetails && (showLoadingIndicator || hasPendingActionRequired);
const minimalModeBleedThrough = hideMinimalModeHintWhileBusy
? null
: modeBleedThrough;
const hasMinimalStatusBleedThrough = shouldShowToast(uiState);
const contextTokenLimit =
typeof uiState.currentModel === 'string' && uiState.currentModel.length > 0
? tokenLimit(uiState.currentModel)
: 0;
const showMinimalContextBleedThrough =
!settings.merged.ui.footer.hideContextPercentage &&
typeof uiState.currentModel === 'string' &&
uiState.currentModel.length > 0 &&
contextTokenLimit > 0 &&
uiState.sessionStats.lastPromptTokenCount / contextTokenLimit > 0.6;
const hideShortcutsHintForSuggestions = hideUiDetailsForSuggestions;
const showShortcutsHint =
settings.merged.ui.showShortcutsHint &&
!hideShortcutsHintForSuggestions &&
!hideMinimalModeHintWhileBusy &&
!hasPendingActionRequired &&
(!showUiDetails || !showLoadingIndicator);
const showMinimalModeBleedThrough =
!hideUiDetailsForSuggestions && Boolean(minimalModeBleedThrough);
const showMinimalInlineLoading = !showUiDetails && showLoadingIndicator;
const showMinimalBleedThroughRow =
!showUiDetails &&
(showMinimalModeBleedThrough ||
hasMinimalStatusBleedThrough ||
showMinimalContextBleedThrough);
const showMinimalMetaRow =
!showUiDetails &&
(showMinimalInlineLoading ||
showMinimalBleedThroughRow ||
showShortcutsHint);
return (
<Box
@@ -88,9 +174,11 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
/>
)}
<QueuedMessageDisplay messageQueue={uiState.messageQueue} />
{showUiDetails && (
<QueuedMessageDisplay messageQueue={uiState.messageQueue} />
)}
<TodoTray />
{showUiDetails && <TodoTray />}
<Box marginTop={1} width="100%" flexDirection="column">
<Box
@@ -106,7 +194,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
alignItems="center"
flexGrow={1}
>
{showLoadingIndicator && (
{showUiDetails && showLoadingIndicator && (
<LoadingIndicator
inline
thought={
@@ -133,86 +221,172 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
flexDirection="column"
alignItems={isNarrow ? 'flex-start' : 'flex-end'}
>
{!hasPendingActionRequired && <ShortcutsHint />}
{showUiDetails && showShortcutsHint && <ShortcutsHint />}
</Box>
</Box>
{uiState.shortcutsHelpVisible && <ShortcutsHelp />}
<HorizontalLine />
<Box
justifyContent={
settings.merged.ui.hideContextSummary
? 'flex-start'
: 'space-between'
}
width="100%"
flexDirection={isNarrow ? 'column' : 'row'}
alignItems={isNarrow ? 'flex-start' : 'center'}
>
{showMinimalMetaRow && (
<Box
marginLeft={1}
marginRight={isNarrow ? 0 : 1}
flexDirection="row"
alignItems="center"
flexGrow={1}
justifyContent="space-between"
width="100%"
flexDirection={isNarrow ? 'column' : 'row'}
alignItems={isNarrow ? 'flex-start' : 'center'}
>
{hasToast ? (
<ToastDisplay />
) : (
!showLoadingIndicator && (
<Box
marginLeft={1}
marginRight={isNarrow ? 0 : 1}
flexDirection="row"
alignItems={isNarrow ? 'flex-start' : 'center'}
flexGrow={1}
>
{showMinimalInlineLoading && (
<LoadingIndicator
inline
thought={
uiState.streamingState ===
StreamingState.WaitingForConfirmation ||
config.getAccessibility()?.enableLoadingPhrases === false
? undefined
: uiState.thought
}
currentLoadingPhrase={
config.getAccessibility()?.enableLoadingPhrases === false
? undefined
: uiState.currentLoadingPhrase
}
thoughtLabel={
inlineThinkingMode === 'full' ? 'Thinking ...' : undefined
}
elapsedTime={uiState.elapsedTime}
/>
)}
{showMinimalModeBleedThrough && minimalModeBleedThrough && (
<Text color={minimalModeBleedThrough.color}>
{minimalModeBleedThrough.text}
</Text>
)}
{hasMinimalStatusBleedThrough && (
<Box
flexDirection={isNarrow ? 'column' : 'row'}
alignItems={isNarrow ? 'flex-start' : 'center'}
marginLeft={
showMinimalInlineLoading || showMinimalModeBleedThrough
? 1
: 0
}
>
{showApprovalIndicator && (
<ApprovalModeIndicator
approvalMode={showApprovalModeIndicator}
isPlanEnabled={config.isPlanEnabled()}
/>
)}
{uiState.shellModeActive && (
<Box
marginLeft={showApprovalIndicator && !isNarrow ? 1 : 0}
marginTop={showApprovalIndicator && isNarrow ? 1 : 0}
>
<ShellModeIndicator />
</Box>
)}
{showRawMarkdownIndicator && (
<Box
marginLeft={
(showApprovalIndicator || uiState.shellModeActive) &&
!isNarrow
? 1
: 0
}
marginTop={
(showApprovalIndicator || uiState.shellModeActive) &&
isNarrow
? 1
: 0
}
>
<RawMarkdownIndicator />
</Box>
)}
<ToastDisplay />
</Box>
)
)}
</Box>
{(showMinimalContextBleedThrough || showShortcutsHint) && (
<Box
marginTop={isNarrow && showMinimalBleedThroughRow ? 1 : 0}
flexDirection={isNarrow ? 'column' : 'row'}
alignItems={isNarrow ? 'flex-start' : 'flex-end'}
>
{showMinimalContextBleedThrough && (
<ContextUsageDisplay
promptTokenCount={uiState.sessionStats.lastPromptTokenCount}
model={uiState.currentModel}
terminalWidth={uiState.terminalWidth}
/>
)}
{showShortcutsHint && (
<Box
marginLeft={
showMinimalContextBleedThrough && !isNarrow ? 1 : 0
}
marginTop={
showMinimalContextBleedThrough && isNarrow ? 1 : 0
}
>
<ShortcutsHint />
</Box>
)}
</Box>
)}
</Box>
)}
{showShortcutsHelp && <ShortcutsHelp />}
{showUiDetails && <HorizontalLine />}
{showUiDetails && (
<Box
marginTop={isNarrow ? 1 : 0}
flexDirection="column"
alignItems={isNarrow ? 'flex-start' : 'flex-end'}
justifyContent={
settings.merged.ui.hideContextSummary
? 'flex-start'
: 'space-between'
}
width="100%"
flexDirection={isNarrow ? 'column' : 'row'}
alignItems={isNarrow ? 'flex-start' : 'center'}
>
{!showLoadingIndicator && (
<StatusDisplay hideContextSummary={hideContextSummary} />
)}
<Box
marginLeft={1}
marginRight={isNarrow ? 0 : 1}
flexDirection="row"
alignItems="center"
flexGrow={1}
>
{hasToast ? (
<ToastDisplay />
) : (
!showLoadingIndicator && (
<Box
flexDirection={isNarrow ? 'column' : 'row'}
alignItems={isNarrow ? 'flex-start' : 'center'}
>
{showApprovalIndicator && (
<ApprovalModeIndicator
approvalMode={showApprovalModeIndicator}
isPlanEnabled={config.isPlanEnabled()}
isDeepWorkEnabled={
config.isDeepWorkEnabled?.() ?? false
}
/>
)}
{uiState.shellModeActive && (
<Box
marginLeft={showApprovalIndicator && !isNarrow ? 1 : 0}
marginTop={showApprovalIndicator && isNarrow ? 1 : 0}
>
<ShellModeIndicator />
</Box>
)}
{showRawMarkdownIndicator && (
<Box
marginLeft={
(showApprovalIndicator || uiState.shellModeActive) &&
!isNarrow
? 1
: 0
}
marginTop={
(showApprovalIndicator || uiState.shellModeActive) &&
isNarrow
? 1
: 0
}
>
<RawMarkdownIndicator />
</Box>
)}
</Box>
)
)}
</Box>
<Box
marginTop={isNarrow ? 1 : 0}
flexDirection="column"
alignItems={isNarrow ? 'flex-start' : 'flex-end'}
>
{!showLoadingIndicator && (
<StatusDisplay hideContextSummary={hideContextSummary} />
)}
</Box>
</Box>
</Box>
)}
</Box>
{uiState.showErrorDetails && (
{showUiDetails && uiState.showErrorDetails && (
<OverflowProvider>
<Box flexDirection="column">
<DetailedMessagesDisplay
@@ -264,7 +438,9 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
/>
)}
{!settings.merged.ui.hideFooter && !isScreenReaderEnabled && <Footer />}
{showUiDetails &&
!settings.merged.ui.hideFooter &&
!isScreenReaderEnabled && <Footer />}
</Box>
);
};
@@ -47,6 +47,13 @@ const writeKey = (stdin: { write: (data: string) => void }, key: string) => {
act(() => {
stdin.write(key);
});
// Advance timers to simulate time passing between keystrokes.
// This avoids bufferFastReturn converting Enter to Shift+Enter.
if (vi.isFakeTimers()) {
act(() => {
vi.advanceTimersByTime(50);
});
}
};
describe('ExitPlanModeDialog', () => {
@@ -130,10 +137,16 @@ Implement a comprehensive authentication system with multiple providers.
vi.restoreAllMocks();
});
const renderDialog = (options?: { useAlternateBuffer?: boolean }) =>
const renderDialog = (options?: {
useAlternateBuffer?: boolean;
deepWorkEnabled?: boolean;
recommendedApprovalMode?: ApprovalMode;
}) =>
renderWithProviders(
<ExitPlanModeDialog
planPath={mockPlanFullPath}
deepWorkEnabled={options?.deepWorkEnabled}
recommendedApprovalMode={options?.recommendedApprovalMode}
onApprove={onApprove}
onFeedback={onFeedback}
onCancel={onCancel}
@@ -201,6 +214,33 @@ Implement a comprehensive authentication system with multiple providers.
});
});
it('calls onApprove with DEEP_WORK when deep work is recommended and selected', async () => {
const { stdin, lastFrame } = renderDialog({
useAlternateBuffer,
deepWorkEnabled: true,
recommendedApprovalMode: ApprovalMode.DEEP_WORK,
});
await act(async () => {
vi.runAllTimers();
});
await waitFor(() => {
expect(lastFrame()).toContain('Add user authentication');
expect(lastFrame()).toContain('Deep Work');
expect(lastFrame()).toContain(
'Yes, start Deep Work execution (Recommended)',
);
});
writeKey(stdin, '\x1b[B'); // Down arrow to Deep Work
writeKey(stdin, '\r');
await waitFor(() => {
expect(onApprove).toHaveBeenCalledWith(ApprovalMode.DEEP_WORK);
});
});
it('calls onApprove with DEFAULT when second option is selected', async () => {
const { stdin, lastFrame } = renderDialog({ useAlternateBuffer });
@@ -234,7 +274,6 @@ Implement a comprehensive authentication system with multiple providers.
// Navigate to feedback option
writeKey(stdin, '\x1b[B'); // Down arrow
writeKey(stdin, '\x1b[B'); // Down arrow
writeKey(stdin, '\r'); // Select to focus input
// Type feedback
for (const char of 'Add tests') {
@@ -512,7 +551,6 @@ Implement a comprehensive authentication system with multiple providers.
// Navigate to feedback option and start typing
writeKey(stdin, '\x1b[B'); // Down arrow
writeKey(stdin, '\x1b[B'); // Down arrow
writeKey(stdin, '\r'); // Select to focus input
// Type some feedback
for (const char of 'test') {
@@ -21,6 +21,9 @@ import { AskUserDialog } from './AskUserDialog.js';
export interface ExitPlanModeDialogProps {
planPath: string;
recommendedApprovalMode?: ApprovalMode;
recommendationReason?: string;
deepWorkEnabled?: boolean;
onApprove: (approvalMode: ApprovalMode) => void;
onFeedback: (feedback: string) => void;
onCancel: () => void;
@@ -41,10 +44,53 @@ interface PlanContentState {
}
enum ApprovalOption {
DeepWork = 'Yes, start Deep Work execution',
Auto = 'Yes, automatically accept edits',
Manual = 'Yes, manually accept edits',
}
const DEEP_WORK_SIGNALS = [
'iterate',
'iteration',
'loop',
'phases',
'phase',
'refactor',
'migrate',
'end-to-end',
'e2e',
'comprehensive',
'cross-cutting',
'multi-step',
'verification',
'test suite',
];
function recommendApprovalModeFromPlan(
planContent: string,
deepWorkEnabled: boolean,
): ApprovalMode {
if (!deepWorkEnabled) {
return ApprovalMode.AUTO_EDIT;
}
const normalized = planContent.toLowerCase();
const stepCount = (normalized.match(/^\s*\d+\.\s+/gm) ?? []).length;
const signalCount = DEEP_WORK_SIGNALS.filter((signal) =>
normalized.includes(signal),
).length;
if (
stepCount >= 6 ||
(stepCount >= 4 && signalCount >= 2) ||
signalCount >= 4
) {
return ApprovalMode.DEEP_WORK;
}
return ApprovalMode.AUTO_EDIT;
}
/**
* A tiny component for loading and error states with consistent styling.
*/
@@ -127,6 +173,9 @@ function usePlanContent(planPath: string, config: Config): PlanContentState {
export const ExitPlanModeDialog: React.FC<ExitPlanModeDialogProps> = ({
planPath,
recommendedApprovalMode,
recommendationReason,
deepWorkEnabled = false,
onApprove,
onFeedback,
onCancel,
@@ -183,6 +232,67 @@ export const ExitPlanModeDialog: React.FC<ExitPlanModeDialogProps> = ({
);
}
const computedRecommendation = recommendApprovalModeFromPlan(
planContent,
deepWorkEnabled,
);
const effectiveRecommendation =
recommendedApprovalMode === ApprovalMode.DEEP_WORK && deepWorkEnabled
? ApprovalMode.DEEP_WORK
: recommendedApprovalMode === ApprovalMode.AUTO_EDIT
? ApprovalMode.AUTO_EDIT
: computedRecommendation;
const autoOptionLabel =
effectiveRecommendation === ApprovalMode.AUTO_EDIT
? `${ApprovalOption.Auto} (Recommended)`
: ApprovalOption.Auto;
const deepWorkOptionLabel =
effectiveRecommendation === ApprovalMode.DEEP_WORK
? `${ApprovalOption.DeepWork} (Recommended)`
: ApprovalOption.DeepWork;
const manualOptionLabel =
effectiveRecommendation === ApprovalMode.DEFAULT
? `${ApprovalOption.Manual} (Recommended)`
: ApprovalOption.Manual;
const approvalOptions = deepWorkEnabled
? [
{
label: autoOptionLabel,
description:
'Approves plan and runs regular implementation with automatic edits.',
},
{
label: deepWorkOptionLabel,
description:
'Approves plan and uses iterative Deep Work execution with readiness checks.',
},
{
label: manualOptionLabel,
description:
'Approves plan but requires confirmation before each tool call.',
},
]
: [
{
label: autoOptionLabel,
description: 'Approves plan and allows tools to run automatically.',
},
{
label: manualOptionLabel,
description: 'Approves plan but requires confirmation for each tool.',
},
];
const recommendationText =
recommendationReason && recommendationReason.trim().length > 0
? recommendationReason.trim()
: effectiveRecommendation === ApprovalMode.DEEP_WORK
? 'Recommendation: Deep Work execution for iterative implementation.'
: 'Recommendation: Regular execution.';
const promptWithRecommendation = `${recommendationText}\n\n${planContent}`;
return (
<Box flexDirection="column" width={width}>
<AskUserDialog
@@ -190,28 +300,19 @@ export const ExitPlanModeDialog: React.FC<ExitPlanModeDialogProps> = ({
{
type: QuestionType.CHOICE,
header: 'Approval',
question: planContent,
options: [
{
label: ApprovalOption.Auto,
description:
'Approves plan and allows tools to run automatically',
},
{
label: ApprovalOption.Manual,
description:
'Approves plan but requires confirmation for each tool',
},
],
question: promptWithRecommendation,
options: approvalOptions,
placeholder: 'Type your feedback...',
multiSelect: false,
},
]}
onSubmit={(answers) => {
const answer = answers['0'];
if (answer === ApprovalOption.Auto) {
if (answer?.startsWith(ApprovalOption.DeepWork)) {
onApprove(ApprovalMode.DEEP_WORK);
} else if (answer?.startsWith(ApprovalOption.Auto)) {
onApprove(ApprovalMode.AUTO_EDIT);
} else if (answer === ApprovalOption.Manual) {
} else if (answer?.startsWith(ApprovalOption.Manual)) {
onApprove(ApprovalMode.DEFAULT);
} else if (answer) {
onFeedback(answer);
@@ -210,7 +210,6 @@ describe('<HistoryItemDisplay />', () => {
command: 'echo "\u001b[31mhello\u001b[0m"',
rootCommand: 'echo',
rootCommands: ['echo'],
onConfirm: async () => {},
},
},
],
@@ -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}
@@ -149,8 +149,14 @@ describe('InputPrompt', () => {
);
const mockedUseKittyKeyboardProtocol = vi.mocked(useKittyKeyboardProtocol);
const mockSetEmbeddedShellFocused = vi.fn();
const mockSetCleanUiDetailsVisible = vi.fn();
const mockToggleCleanUiDetailsVisible = vi.fn();
const mockRevealCleanUiDetailsTemporarily = vi.fn();
const uiActions = {
setEmbeddedShellFocused: mockSetEmbeddedShellFocused,
setCleanUiDetailsVisible: mockSetCleanUiDetailsVisible,
toggleCleanUiDetailsVisible: mockToggleCleanUiDetailsVisible,
revealCleanUiDetailsTemporarily: mockRevealCleanUiDetailsTemporarily,
};
beforeEach(() => {
@@ -1543,7 +1549,6 @@ describe('InputPrompt', () => {
{ color: 'black', name: 'black' },
{ color: '#000000', name: '#000000' },
{ color: '#000', name: '#000' },
{ color: undefined, name: 'default (black)' },
{ color: 'white', name: 'white' },
{ color: '#ffffff', name: '#ffffff' },
{ color: '#fff', name: '#fff' },
@@ -1613,6 +1618,11 @@ describe('InputPrompt', () => {
const { stdout, unmount } = renderWithProviders(
<InputPrompt {...props} />,
{
uiState: {
terminalBackgroundColor: 'black',
} as Partial<UIState>,
},
);
await waitFor(() => {
@@ -2945,29 +2955,29 @@ describe('InputPrompt', () => {
});
});
describe('Tab focus toggle', () => {
describe('Tab clean UI toggle', () => {
it.each([
{
name: 'should toggle focus in on Tab when no suggestions or ghost text',
name: 'should toggle clean UI details on double-Tab when no suggestions or ghost text',
showSuggestions: false,
ghostText: '',
suggestions: [],
expectedFocusToggle: true,
expectedUiToggle: true,
},
{
name: 'should accept ghost text and NOT toggle focus on Tab',
name: 'should accept ghost text and NOT toggle clean UI details on Tab',
showSuggestions: false,
ghostText: 'ghost text',
suggestions: [],
expectedFocusToggle: false,
expectedUiToggle: false,
expectedAcceptCall: true,
},
{
name: 'should NOT toggle focus on Tab when suggestions are present',
name: 'should NOT toggle clean UI details on Tab when suggestions are present',
showSuggestions: true,
ghostText: '',
suggestions: [{ label: 'test', value: 'test' }],
expectedFocusToggle: false,
expectedUiToggle: false,
},
])(
'$name',
@@ -2975,7 +2985,7 @@ describe('InputPrompt', () => {
showSuggestions,
ghostText,
suggestions,
expectedFocusToggle,
expectedUiToggle,
expectedAcceptCall,
}) => {
const mockAccept = vi.fn();
@@ -2997,21 +3007,24 @@ describe('InputPrompt', () => {
<InputPrompt {...props} />,
{
uiActions,
uiState: { activePtyId: 1 },
uiState: {},
},
);
await act(async () => {
stdin.write('\t');
if (expectedUiToggle) {
stdin.write('\t');
}
});
await waitFor(() => {
if (expectedFocusToggle) {
expect(uiActions.setEmbeddedShellFocused).toHaveBeenCalledWith(
true,
);
if (expectedUiToggle) {
expect(uiActions.toggleCleanUiDetailsVisible).toHaveBeenCalled();
} else {
expect(uiActions.setEmbeddedShellFocused).not.toHaveBeenCalled();
expect(
uiActions.toggleCleanUiDetailsVisible,
).not.toHaveBeenCalled();
}
if (expectedAcceptCall) {
@@ -3021,6 +3034,75 @@ describe('InputPrompt', () => {
unmount();
},
);
it('should not reveal clean UI details on Shift+Tab when hidden', async () => {
mockedUseCommandCompletion.mockReturnValue({
...mockCommandCompletion,
showSuggestions: false,
suggestions: [],
promptCompletion: {
text: '',
accept: vi.fn(),
clear: vi.fn(),
isLoading: false,
isActive: false,
markSelected: vi.fn(),
},
});
const { stdin, unmount } = renderWithProviders(
<InputPrompt {...props} />,
{
uiActions,
uiState: { activePtyId: 1, cleanUiDetailsVisible: false },
},
);
await act(async () => {
stdin.write('\x1b[Z');
});
await waitFor(() => {
expect(
uiActions.revealCleanUiDetailsTemporarily,
).not.toHaveBeenCalled();
});
unmount();
});
it('should toggle clean UI details on double-Tab by default', async () => {
mockedUseCommandCompletion.mockReturnValue({
...mockCommandCompletion,
showSuggestions: false,
suggestions: [],
promptCompletion: {
text: '',
accept: vi.fn(),
clear: vi.fn(),
isLoading: false,
isActive: false,
markSelected: vi.fn(),
},
});
const { stdin, unmount } = renderWithProviders(
<InputPrompt {...props} />,
{
uiActions,
uiState: {},
},
);
await act(async () => {
stdin.write('\t');
stdin.write('\t');
});
await waitFor(() => {
expect(uiActions.toggleCleanUiDetailsVisible).toHaveBeenCalled();
});
unmount();
});
});
describe('mouse interaction', () => {
@@ -4296,6 +4378,30 @@ describe('InputPrompt', () => {
});
describe('shortcuts help visibility', () => {
it('opens shortcuts help with ? on empty prompt even when showShortcutsHint is false', async () => {
const setShortcutsHelpVisible = vi.fn();
const settings = createMockSettings({
ui: { showShortcutsHint: false },
});
const { stdin, unmount } = renderWithProviders(
<InputPrompt {...props} />,
{
settings,
uiActions: { setShortcutsHelpVisible },
},
);
await act(async () => {
stdin.write('?');
});
await waitFor(() => {
expect(setShortcutsHelpVisible).toHaveBeenCalledWith(true);
});
unmount();
});
it.each([
{
name: 'terminal paste event occurs',
@@ -4318,6 +4424,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 }) => {
+47 -3
View File
@@ -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
@@ -143,6 +144,8 @@ export function isLargePaste(text: string): boolean {
);
}
const DOUBLE_TAB_CLEAN_UI_TOGGLE_WINDOW_MS = 350;
/**
* Attempt to toggle expansion of a paste placeholder in the buffer.
* Returns true if a toggle action was performed or hint was shown, false otherwise.
@@ -210,18 +213,22 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
const { merged: settings } = useSettings();
const kittyProtocol = useKittyKeyboardProtocol();
const isShellFocused = useShellFocusState();
const { setEmbeddedShellFocused, setShortcutsHelpVisible } = useUIActions();
const {
setEmbeddedShellFocused,
setShortcutsHelpVisible,
toggleCleanUiDetailsVisible,
} = useUIActions();
const {
terminalWidth,
activePtyId,
history,
terminalBackgroundColor,
backgroundShells,
backgroundShellHeight,
shortcutsHelpVisible,
} = useUIState();
const [suppressCompletion, setSuppressCompletion] = useState(false);
const escPressCount = useRef(0);
const lastPlainTabPressTimeRef = useRef<number | null>(null);
const [showEscapePrompt, setShowEscapePrompt] = useState(false);
const escapeTimerRef = useRef<NodeJS.Timeout | null>(null);
const [recentUnsafePasteTime, setRecentUnsafePasteTime] = useState<
@@ -623,6 +630,33 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
return false;
}
const isPlainTab =
key.name === 'tab' && !key.shift && !key.alt && !key.ctrl && !key.cmd;
const hasTabCompletionInteraction =
completion.showSuggestions ||
Boolean(completion.promptCompletion.text) ||
reverseSearchActive ||
commandSearchActive;
if (isPlainTab) {
if (!hasTabCompletionInteraction) {
const now = Date.now();
const isDoubleTabPress =
lastPlainTabPressTimeRef.current !== null &&
now - lastPlainTabPressTimeRef.current <=
DOUBLE_TAB_CLEAN_UI_TOGGLE_WINDOW_MS;
if (isDoubleTabPress) {
lastPlainTabPressTimeRef.current = null;
toggleCleanUiDetailsVisible();
return true;
}
lastPlainTabPressTimeRef.current = now;
} else {
lastPlainTabPressTimeRef.current = null;
}
} else {
lastPlainTabPressTimeRef.current = null;
}
if (key.name === 'paste') {
if (shortcutsHelpVisible) {
setShortcutsHelpVisible(false);
@@ -661,6 +695,10 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
return true;
}
if (shortcutsHelpVisible && shouldDismissShortcutsHelpOnHotkey(key)) {
setShortcutsHelpVisible(false);
}
if (shortcutsHelpVisible) {
if (key.sequence === '?' && key.insertable) {
setShortcutsHelpVisible(false);
@@ -1167,6 +1205,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
kittyProtocol.enabled,
shortcutsHelpVisible,
setShortcutsHelpVisible,
toggleCleanUiDetailsVisible,
tryLoadQueuedMessages,
setBannerVisible,
onSubmit,
@@ -1312,7 +1351,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
const useBackgroundColor = config.getUseBackgroundColor();
const isLowColor = isLowColorDepth();
const terminalBg = terminalBackgroundColor || 'black';
const terminalBg = theme.background.primary || 'black';
// We should fallback to lines if the background color is disabled OR if it is
// enabled but we are in a low color depth terminal where we don't have a safe
@@ -1339,6 +1378,8 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
!shellModeActive && approvalMode === ApprovalMode.YOLO;
const showPlanStyling =
!shellModeActive && approvalMode === ApprovalMode.PLAN;
const showDeepWorkStyling =
!shellModeActive && approvalMode === ApprovalMode.DEEP_WORK;
let statusColor: string | undefined;
let statusText = '';
@@ -1351,6 +1392,9 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
} else if (showPlanStyling) {
statusColor = theme.status.success;
statusText = 'Plan mode';
} else if (showDeepWorkStyling) {
statusColor = theme.status.success;
statusText = 'Deep Work mode';
} else if (showAutoAcceptStyling) {
statusColor = theme.status.warning;
statusText = 'Accepting edits';
@@ -9,11 +9,15 @@ import { waitFor } from '../../test-utils/async.js';
import { MainContent } from './MainContent.js';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { Box, Text } from 'ink';
import type React from 'react';
import { act, useState, type JSX } from 'react';
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
import { ToolCallStatus } from '../types.js';
import { SHELL_COMMAND_NAME } from '../constants.js';
import type { UIState } from '../contexts/UIStateContext.js';
import {
UIStateContext,
useUIState,
type UIState,
} from '../contexts/UIStateContext.js';
// Mock dependencies
vi.mock('../contexts/SettingsContext.js', async () => {
@@ -45,7 +49,9 @@ vi.mock('../hooks/useAlternateBuffer.js', () => ({
}));
vi.mock('./AppHeader.js', () => ({
AppHeader: () => <Text>AppHeader</Text>,
AppHeader: ({ showDetails = true }: { showDetails?: boolean }) => (
<Text>{showDetails ? 'AppHeader(full)' : 'AppHeader(minimal)'}</Text>
),
}));
vi.mock('./ShowMoreLines.js', () => ({
@@ -58,7 +64,7 @@ vi.mock('./shared/ScrollableList.js', () => ({
renderItem,
}: {
data: unknown[];
renderItem: (props: { item: unknown }) => React.JSX.Element;
renderItem: (props: { item: unknown }) => JSX.Element;
}) => (
<Box flexDirection="column">
<Text>ScrollableList</Text>
@@ -87,6 +93,7 @@ describe('MainContent', () => {
activePtyId: undefined,
embeddedShellFocused: false,
historyRemountKey: 0,
cleanUiDetailsVisible: true,
bannerData: { defaultText: '', warningText: '' },
bannerVisible: false,
copyModeEnabled: false,
@@ -101,7 +108,7 @@ describe('MainContent', () => {
const { lastFrame } = renderWithProviders(<MainContent />, {
uiState: defaultMockUiState as Partial<UIState>,
});
await waitFor(() => expect(lastFrame()).toContain('AppHeader'));
await waitFor(() => expect(lastFrame()).toContain('AppHeader(full)'));
const output = lastFrame();
expect(output).toContain('Hello');
@@ -116,11 +123,81 @@ describe('MainContent', () => {
await waitFor(() => expect(lastFrame()).toContain('ScrollableList'));
const output = lastFrame();
expect(output).toContain('AppHeader');
expect(output).toContain('AppHeader(full)');
expect(output).toContain('Hello');
expect(output).toContain('Hi there');
});
it('renders minimal header in minimal mode (alternate buffer)', async () => {
vi.mocked(useAlternateBuffer).mockReturnValue(true);
const { lastFrame } = renderWithProviders(<MainContent />, {
uiState: {
...defaultMockUiState,
cleanUiDetailsVisible: false,
} as Partial<UIState>,
});
await waitFor(() => expect(lastFrame()).toContain('Hello'));
const output = lastFrame();
expect(output).toContain('AppHeader(minimal)');
expect(output).not.toContain('AppHeader(full)');
expect(output).toContain('Hello');
});
it('restores full header details after toggle in alternate buffer mode', async () => {
vi.mocked(useAlternateBuffer).mockReturnValue(true);
let setShowDetails: ((visible: boolean) => void) | undefined;
const ToggleHarness = () => {
const outerState = useUIState();
const [showDetails, setShowDetailsState] = useState(
outerState.cleanUiDetailsVisible,
);
setShowDetails = setShowDetailsState;
return (
<UIStateContext.Provider
value={{ ...outerState, cleanUiDetailsVisible: showDetails }}
>
<MainContent />
</UIStateContext.Provider>
);
};
const { lastFrame } = renderWithProviders(<ToggleHarness />, {
uiState: {
...defaultMockUiState,
cleanUiDetailsVisible: false,
} as Partial<UIState>,
});
await waitFor(() => expect(lastFrame()).toContain('AppHeader(minimal)'));
if (!setShowDetails) {
throw new Error('setShowDetails was not initialized');
}
const setShowDetailsSafe = setShowDetails;
act(() => {
setShowDetailsSafe(true);
});
await waitFor(() => expect(lastFrame()).toContain('AppHeader(full)'));
});
it('always renders full header details in normal buffer mode', async () => {
vi.mocked(useAlternateBuffer).mockReturnValue(false);
const { lastFrame } = renderWithProviders(<MainContent />, {
uiState: {
...defaultMockUiState,
cleanUiDetailsVisible: false,
} as Partial<UIState>,
});
await waitFor(() => expect(lastFrame()).toContain('AppHeader(full)'));
expect(lastFrame()).not.toContain('AppHeader(minimal)');
});
it('does not constrain height in alternate buffer mode', async () => {
vi.mocked(useAlternateBuffer).mockReturnValue(true);
const { lastFrame } = renderWithProviders(<MainContent />, {
@@ -129,7 +206,9 @@ describe('MainContent', () => {
await waitFor(() => expect(lastFrame()).toContain('Hello'));
const output = lastFrame();
expect(output).toMatchSnapshot();
expect(output).toContain('AppHeader(full)');
expect(output).toContain('Hello');
expect(output).toContain('Hi there');
});
describe('MainContent Tool Output Height Logic', () => {
@@ -210,6 +289,7 @@ describe('MainContent', () => {
isEditorDialogOpen: false,
slashCommands: [],
historyRemountKey: 0,
cleanUiDetailsVisible: true,
bannerData: {
defaultText: '',
warningText: '',
+17 -8
View File
@@ -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);
@@ -51,7 +48,9 @@ export const MainContent = () => {
mainAreaWidth,
staticAreaMaxItemHeight,
availableTerminalHeight,
cleanUiDetailsVisible,
} = uiState;
const showHeaderDetails = cleanUiDetailsVisible;
const historyItems = useMemo(
() =>
@@ -89,7 +88,6 @@ export const MainContent = () => {
terminalWidth={mainAreaWidth}
item={{ ...item, id: 0 }}
isPending={true}
isFocused={!uiState.isEditorDialogOpen}
activeShellPtyId={uiState.activePtyId}
embeddedShellFocused={uiState.embeddedShellFocused}
/>
@@ -105,7 +103,6 @@ export const MainContent = () => {
isAlternateBuffer,
availableTerminalHeight,
mainAreaWidth,
uiState.isEditorDialogOpen,
uiState.activePtyId,
uiState.embeddedShellFocused,
showConfirmationQueue,
@@ -125,7 +122,13 @@ export const MainContent = () => {
const renderItem = useCallback(
({ item }: { item: (typeof virtualizedData)[number] }) => {
if (item.type === 'header') {
return <MemoizedAppHeader key="app-header" version={version} />;
return (
<MemoizedAppHeader
key="app-header"
version={version}
showDetails={showHeaderDetails}
/>
);
} else if (item.type === 'history') {
return (
<MemoizedHistoryItemDisplay
@@ -142,7 +145,13 @@ export const MainContent = () => {
return pendingItems;
}
},
[version, mainAreaWidth, uiState.slashCommands, pendingItems],
[
showHeaderDetails,
version,
mainAreaWidth,
uiState.slashCommands,
pendingItems,
],
);
if (isAlternateBuffer) {
+10 -112
View File
@@ -7,7 +7,6 @@
import type React from 'react';
import { useState, useEffect, useMemo, useCallback } from 'react';
import { Text } from 'ink';
import { AsyncFzf } from 'fzf';
import type { Key } from '../hooks/useKeypress.js';
import { theme } from '../semantic-colors.js';
import type {
@@ -32,27 +31,17 @@ import {
getEffectiveValue,
} from '../../utils/settingsUtils.js';
import { useVimMode } from '../contexts/VimModeContext.js';
import { getCachedStringWidth } from '../utils/textUtils.js';
import {
type SettingsValue,
TOGGLE_TYPES,
} from '../../config/settingsSchema.js';
import { coreEvents, debugLogger } from '@google/gemini-cli-core';
import type { Config } from '@google/gemini-cli-core';
import { useUIState } from '../contexts/UIStateContext.js';
import { useTextBuffer } from './shared/text-buffer.js';
import {
BaseSettingsDialog,
type SettingsDialogItem,
BaseSettingsDialog,
} from './shared/BaseSettingsDialog.js';
interface FzfResult {
item: string;
start: number;
end: number;
score: number;
positions?: number[];
}
import { useFuzzyList } from '../hooks/useFuzzyList.js';
interface SettingsDialogProps {
settings: LoadedSettings;
@@ -81,60 +70,6 @@ export function SettingsDialog({
const [showRestartPrompt, setShowRestartPrompt] = useState(false);
// Search state
const [searchQuery, setSearchQuery] = useState('');
const [filteredKeys, setFilteredKeys] = useState<string[]>(() =>
getDialogSettingKeys(),
);
const { fzfInstance, searchMap } = useMemo(() => {
const keys = getDialogSettingKeys();
const map = new Map<string, string>();
const searchItems: string[] = [];
keys.forEach((key) => {
const def = getSettingDefinition(key);
if (def?.label) {
searchItems.push(def.label);
map.set(def.label.toLowerCase(), key);
}
});
const fzf = new AsyncFzf(searchItems, {
fuzzy: 'v2',
casing: 'case-insensitive',
});
return { fzfInstance: fzf, searchMap: map };
}, []);
// Perform search
useEffect(() => {
let active = true;
if (!searchQuery.trim() || !fzfInstance) {
setFilteredKeys(getDialogSettingKeys());
return;
}
const doSearch = async () => {
const results = await fzfInstance.find(searchQuery);
if (!active) return;
const matchedKeys = new Set<string>();
results.forEach((res: FzfResult) => {
const key = searchMap.get(res.item.toLowerCase());
if (key) matchedKeys.add(key);
});
setFilteredKeys(Array.from(matchedKeys));
};
// eslint-disable-next-line @typescript-eslint/no-floating-promises
doSearch();
return () => {
active = false;
};
}, [searchQuery, fzfInstance, searchMap]);
// Local pending settings state for the selected scope
const [pendingSettings, setPendingSettings] = useState<Settings>(() =>
// Deep clone to avoid mutation
@@ -182,49 +117,8 @@ export function SettingsDialog({
setShowRestartPrompt(newRestartRequired.size > 0);
}, [selectedScope, settings, globalPendingChanges]);
// Calculate max width for the left column (Label/Description) to keep values aligned or close
const maxLabelOrDescriptionWidth = useMemo(() => {
const allKeys = getDialogSettingKeys();
let max = 0;
for (const key of allKeys) {
const def = getSettingDefinition(key);
if (!def) continue;
const scopeMessage = getScopeMessageForSetting(
key,
selectedScope,
settings,
);
const label = def.label || key;
const labelFull = label + (scopeMessage ? ` ${scopeMessage}` : '');
const lWidth = getCachedStringWidth(labelFull);
const dWidth = def.description
? getCachedStringWidth(def.description)
: 0;
max = Math.max(max, lWidth, dWidth);
}
return max;
}, [selectedScope, settings]);
// Get mainAreaWidth for search buffer viewport
const { mainAreaWidth } = useUIState();
const viewportWidth = mainAreaWidth - 8;
// Search input buffer
const searchBuffer = useTextBuffer({
initialText: '',
initialCursorOffset: 0,
viewport: {
width: viewportWidth,
height: 1,
},
singleLine: true,
onChange: (text) => setSearchQuery(text),
});
// Generate items for BaseSettingsDialog
const settingKeys = searchQuery ? filteredKeys : getDialogSettingKeys();
// Generate items for SearchableList
const settingKeys = useMemo(() => getDialogSettingKeys(), []);
const items: SettingsDialogItem[] = useMemo(() => {
const scopeSettings = settings.forScope(selectedScope).settings;
const mergedSettings = settings.merged;
@@ -270,6 +164,10 @@ export function SettingsDialog({
});
}, [settingKeys, selectedScope, settings, modifiedSettings, pendingSettings]);
const { filteredItems, searchBuffer, maxLabelWidth } = useFuzzyList({
items,
});
// Scope selection handler
const handleScopeChange = useCallback((scope: LoadableSettingScope) => {
setSelectedScope(scope);
@@ -696,12 +594,12 @@ export function SettingsDialog({
borderColor={showRestartPrompt ? theme.status.warning : undefined}
searchEnabled={showSearch}
searchBuffer={searchBuffer}
items={items}
items={filteredItems}
showScopeSelector={showScopeSelection}
selectedScope={selectedScope}
onScopeChange={handleScopeChange}
maxItemsToShow={effectiveMaxItemsToShow}
maxLabelWidth={maxLabelOrDescriptionWidth}
maxLabelWidth={maxLabelWidth}
onItemToggle={handleItemToggle}
onEditCommit={handleEditCommit}
onItemClear={handleItemClear}
@@ -46,4 +46,10 @@ describe('ShortcutsHelp', () => {
expect(lastFrame()).toMatchSnapshot();
},
);
it('always shows Tab Tab focus UI shortcut', () => {
const rendered = renderWithProviders(<ShortcutsHelp />);
expect(rendered.lastFrame()).toContain('Tab Tab');
rendered.unmount();
});
});
@@ -22,13 +22,14 @@ const buildShortcutItems = (): ShortcutItem[] => {
return [
{ key: '!', description: 'shell mode' },
{ key: '@', description: 'select file or folder' },
{ key: 'Esc Esc', description: 'clear & rewind' },
{ key: 'Tab Tab', description: 'focus UI' },
{ key: 'Ctrl+Y', description: 'YOLO mode' },
{ key: 'Shift+Tab', description: 'cycle mode' },
{ key: 'Ctrl+V', description: 'paste images' },
{ key: '@', description: 'select file or folder' },
{ key: 'Ctrl+Y', description: 'YOLO mode' },
{ key: 'Ctrl+R', description: 'reverse-search history' },
{ key: 'Esc Esc', description: 'clear prompt / rewind' },
{ key: `${altLabel}+M`, description: 'raw markdown mode' },
{ key: 'Ctrl+R', description: 'reverse-search history' },
{ key: 'Ctrl+X', description: 'open external editor' },
];
};
@@ -46,15 +47,29 @@ const Shortcut: React.FC<{ item: ShortcutItem }> = ({ item }) => (
export const ShortcutsHelp: React.FC = () => {
const { terminalWidth } = useUIState();
const items = buildShortcutItems();
const isNarrow = isNarrowWidth(terminalWidth);
const items = buildShortcutItems();
const itemsForDisplay = isNarrow
? items
: [
// Keep first column stable: !, @, Esc Esc, Tab Tab.
items[0],
items[5],
items[6],
items[1],
items[4],
items[7],
items[2],
items[8],
items[9],
items[3],
];
return (
<Box flexDirection="column" width="100%">
<SectionHeader title="Shortcuts (for more, see /help)" />
<Box flexDirection="row" flexWrap="wrap" paddingLeft={1} paddingRight={2}>
{items.map((item, index) => (
{itemsForDisplay.map((item, index) => (
<Box
key={`${item.key}-${index}`}
width={isNarrow ? '100%' : '33%'}
@@ -10,7 +10,12 @@ import { theme } from '../semantic-colors.js';
import { useUIState } from '../contexts/UIStateContext.js';
export const ShortcutsHint: React.FC = () => {
const { shortcutsHelpVisible } = useUIState();
const { cleanUiDetailsVisible, shortcutsHelpVisible } = useUIState();
if (!cleanUiDetailsVisible) {
return <Text color={theme.text.secondary}> press tab twice for more </Text>;
}
const highlightColor = shortcutsHelpVisible
? theme.text.accent
: theme.text.secondary;
+19 -28
View File
@@ -9,7 +9,7 @@ import { useCallback, useState } from 'react';
import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
import { themeManager, DEFAULT_THEME } from '../themes/theme-manager.js';
import { pickDefaultThemeName } from '../themes/theme.js';
import { pickDefaultThemeName, type Theme } from '../themes/theme.js';
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
import { DiffRenderer } from './messages/DiffRenderer.js';
import { colorizeCode } from '../utils/CodeColorizer.js';
@@ -27,7 +27,10 @@ import { useUIState } from '../contexts/UIStateContext.js';
interface ThemeDialogProps {
/** Callback function when a theme is selected */
onSelect: (themeName: string, scope: LoadableSettingScope) => void;
onSelect: (
themeName: string,
scope: LoadableSettingScope,
) => void | Promise<void>;
/** Callback function when the dialog is cancelled */
onCancel: () => void;
@@ -40,24 +43,21 @@ interface ThemeDialogProps {
terminalWidth: number;
}
import {
getThemeTypeFromBackgroundColor,
resolveColor,
} from '../themes/color-utils.js';
import { resolveColor } from '../themes/color-utils.js';
function generateThemeItem(
name: string,
typeDisplay: string,
themeType: string,
themeBackground: string | undefined,
fullTheme: Theme | undefined,
terminalBackgroundColor: string | undefined,
terminalThemeType: 'light' | 'dark' | undefined,
) {
const isCompatible =
themeType === 'custom' ||
terminalThemeType === undefined ||
themeType === 'ansi' ||
themeType === terminalThemeType;
const isCompatible = fullTheme
? themeManager.isThemeCompatible(fullTheme, terminalBackgroundColor)
: true;
const themeBackground = fullTheme
? resolveColor(fullTheme.colors.Background)
: undefined;
const isBackgroundMatch =
terminalBackgroundColor &&
@@ -111,26 +111,17 @@ export function ThemeDialog({
const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
const terminalThemeType = getThemeTypeFromBackgroundColor(
terminalBackgroundColor,
);
// Generate theme items
const themeItems = themeManager
.getAvailableThemes()
.map((theme) => {
const fullTheme = themeManager.getTheme(theme.name);
const themeBackground = fullTheme
? resolveColor(fullTheme.colors.Background)
: undefined;
return generateThemeItem(
theme.name,
capitalize(theme.type),
theme.type,
themeBackground,
fullTheme,
terminalBackgroundColor,
terminalThemeType,
);
})
.sort((a, b) => {
@@ -149,8 +140,8 @@ export function ThemeDialog({
const safeInitialThemeIndex = initialThemeIndex >= 0 ? initialThemeIndex : 0;
const handleThemeSelect = useCallback(
(themeName: string) => {
onSelect(themeName, selectedScope);
async (themeName: string) => {
await onSelect(themeName, selectedScope);
refreshStatic();
},
[onSelect, selectedScope, refreshStatic],
@@ -166,8 +157,8 @@ export function ThemeDialog({
}, []);
const handleScopeSelect = useCallback(
(scope: LoadableSettingScope) => {
onSelect(highlightedThemeName, scope);
async (scope: LoadableSettingScope) => {
await onSelect(highlightedThemeName, scope);
refreshStatic();
},
[onSelect, highlightedThemeName, refreshStatic],
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi } from 'vitest';
import { describe, it, expect } from 'vitest';
import { Box } from 'ink';
import { ToolConfirmationQueue } from './ToolConfirmationQueue.js';
import { ToolCallStatus, StreamingState } from '../types.js';
@@ -12,6 +12,31 @@ import { renderWithProviders } from '../../test-utils/render.js';
import { waitFor } from '../../test-utils/async.js';
import type { Config } from '@google/gemini-cli-core';
import type { ConfirmingToolState } from '../hooks/useConfirmingTool.js';
import { theme } from '../semantic-colors.js';
vi.mock('./StickyHeader.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('./StickyHeader.js')>();
return {
...actual,
StickyHeader: vi.fn((props) => actual.StickyHeader(props)),
};
});
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
validatePlanPath: vi.fn().mockResolvedValue(undefined),
validatePlanContent: vi.fn().mockResolvedValue(undefined),
processSingleFileContent: vi.fn().mockResolvedValue({
llmContent: 'Plan content goes here',
error: undefined,
}),
};
});
const { StickyHeader } = await import('./StickyHeader.js');
describe('ToolConfirmationQueue', () => {
const mockConfig = {
@@ -19,8 +44,19 @@ describe('ToolConfirmationQueue', () => {
getIdeMode: () => false,
getModel: () => 'gemini-pro',
getDebugMode: () => false,
getTargetDir: () => '/mock/target/dir',
getFileSystemService: () => ({
readFile: vi.fn().mockResolvedValue('Plan content'),
}),
storage: {
getProjectTempPlansDir: () => '/mock/temp/plans',
},
} as unknown as Config;
beforeEach(() => {
vi.clearAllMocks();
});
it('renders the confirming tool with progress indicator', () => {
const confirmingTool = {
tool: {
@@ -34,7 +70,6 @@ describe('ToolConfirmationQueue', () => {
command: 'ls',
rootCommand: 'ls',
rootCommands: ['ls'],
onConfirm: vi.fn(),
},
},
index: 1,
@@ -60,6 +95,9 @@ describe('ToolConfirmationQueue', () => {
expect(output).toContain('list files'); // Tool description
expect(output).toContain("Allow execution of: 'ls'?");
expect(output).toMatchSnapshot();
const stickyHeaderProps = vi.mocked(StickyHeader).mock.calls[0][0];
expect(stickyHeaderProps.borderColor).toBe(theme.status.warning);
});
it('returns null if tool has no confirmation details', () => {
@@ -105,7 +143,6 @@ describe('ToolConfirmationQueue', () => {
fileDiff: longDiff,
originalContent: 'old',
newContent: 'new',
onConfirm: vi.fn(),
},
},
index: 1,
@@ -153,7 +190,6 @@ describe('ToolConfirmationQueue', () => {
fileDiff: longDiff,
originalContent: 'old',
newContent: 'new',
onConfirm: vi.fn(),
},
},
index: 1,
@@ -203,7 +239,6 @@ describe('ToolConfirmationQueue', () => {
fileDiff: longDiff,
originalContent: 'old',
newContent: 'new',
onConfirm: vi.fn(),
},
},
index: 1,
@@ -229,4 +264,80 @@ describe('ToolConfirmationQueue', () => {
expect(output).not.toContain('Press ctrl-o to show more lines');
expect(output).toMatchSnapshot();
});
it('renders AskUser tool confirmation with Success color', () => {
const confirmingTool = {
tool: {
callId: 'call-1',
name: 'ask_user',
description: 'ask user',
status: ToolCallStatus.Confirming,
confirmationDetails: {
type: 'ask_user' as const,
questions: [],
onConfirm: vi.fn(),
},
},
index: 1,
total: 1,
};
const { lastFrame } = renderWithProviders(
<ToolConfirmationQueue
confirmingTool={confirmingTool as unknown as ConfirmingToolState}
/>,
{
config: mockConfig,
uiState: {
terminalWidth: 80,
},
},
);
const output = lastFrame();
expect(output).toMatchSnapshot();
const stickyHeaderProps = vi.mocked(StickyHeader).mock.calls[0][0];
expect(stickyHeaderProps.borderColor).toBe(theme.status.success);
});
it('renders ExitPlanMode tool confirmation with Success color', async () => {
const confirmingTool = {
tool: {
callId: 'call-1',
name: 'exit_plan_mode',
description: 'exit plan mode',
status: ToolCallStatus.Confirming,
confirmationDetails: {
type: 'exit_plan_mode' as const,
planPath: '/path/to/plan',
onConfirm: vi.fn(),
},
},
index: 1,
total: 1,
};
const { lastFrame } = renderWithProviders(
<ToolConfirmationQueue
confirmingTool={confirmingTool as unknown as ConfirmingToolState}
/>,
{
config: mockConfig,
uiState: {
terminalWidth: 80,
},
},
);
await waitFor(() => {
expect(lastFrame()).toContain('Plan content goes here');
});
const output = lastFrame();
expect(output).toMatchSnapshot();
const stickyHeaderProps = vi.mocked(StickyHeader).mock.calls[0][0];
expect(stickyHeaderProps.borderColor).toBe(theme.status.success);
});
});
@@ -70,10 +70,11 @@ export const ToolConfirmationQueue: React.FC<ToolConfirmationQueueProps> = ({
? Math.max(maxHeight - 6, 4)
: undefined;
const borderColor = theme.status.warning;
const hideToolIdentity =
const isRoutine =
tool.confirmationDetails?.type === 'ask_user' ||
tool.confirmationDetails?.type === 'exit_plan_mode';
const borderColor = isRoutine ? theme.status.success : theme.status.warning;
const hideToolIdentity = isRoutine;
return (
<OverflowProvider>
@@ -90,7 +91,7 @@ export const ToolConfirmationQueue: React.FC<ToolConfirmationQueueProps> = ({
marginBottom={hideToolIdentity ? 0 : 1}
justifyContent="space-between"
>
<Text color={theme.status.warning} bold>
<Text color={borderColor} bold>
{getConfirmationHeader(tool.confirmationDetails)}
</Text>
{total > 1 && (
@@ -54,14 +54,3 @@ exports[`<BackgroundShellDisplay /> > scrolls to active shell when list opens 1`
│ ● 2. tail -f log.txt (PID: 1002) │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘"
`;
exports[`<BackgroundShellDisplay /> > selects the current process and closes the list when Ctrl+L is pressed in list view 1`] = `
"┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 1: npm sta... (PID: 1001) (Focused) Close (Ctrl+B) | Kill (Ctrl+K) | List (Ctrl+L) │
│ │
│ Select Process (Enter to select, Ctrl+K to kill, Esc to cancel): │
│ │
│ ● 1. npm start (PID: 1001) │
│ 2. tail -f log.txt (PID: 1002) │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘"
`;
@@ -1,7 +1,9 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`ExitPlanModeDialog > useAlternateBuffer: false > bubbles up Ctrl+C when feedback is empty while editing 1`] = `
"Overview
"Recommendation: Regular execution.
Overview
Add user authentication to the CLI application.
@@ -13,21 +15,21 @@ Implementation Steps
4. Add tests in src/auth/__tests__/
Files to Modify
... last 3 lines hidden ...
- src/index.ts - Add auth middleware
- src/config.ts - Add auth configuration options
1. Yes, automatically accept edits
Approves plan and allows tools to run automatically
1. Yes, automatically accept edits (Recommended)
Approves plan and allows tools to run automatically.
2. Yes, manually accept edits
Approves plan but requires confirmation for each tool
● 3. Type your feedback...
Approves plan but requires confirmation for each tool.
● 3. Type your feedback...
Enter to submit · Esc to cancel"
`;
exports[`ExitPlanModeDialog > useAlternateBuffer: false > calls onFeedback when feedback is typed and submitted 1`] = `
"Overview
"Recommendation: Regular execution.
Overview
Add user authentication to the CLI application.
@@ -39,15 +41,13 @@ Implementation Steps
4. Add tests in src/auth/__tests__/
Files to Modify
... last 3 lines hidden ...
- src/index.ts - Add auth middleware
- src/config.ts - Add auth configuration options
1. Yes, automatically accept edits
Approves plan and allows tools to run automatically
1. Yes, automatically accept edits (Recommended)
Approves plan and allows tools to run automatically.
2. Yes, manually accept edits
Approves plan but requires confirmation for each tool
● 3. Add tests
Approves plan but requires confirmation for each tool.
● 3. Add tests
Enter to submit · Esc to cancel"
`;
@@ -55,7 +55,9 @@ Enter to submit · Esc to cancel"
exports[`ExitPlanModeDialog > useAlternateBuffer: false > displays error state when file read fails 1`] = `" Error reading plan: File not found"`;
exports[`ExitPlanModeDialog > useAlternateBuffer: false > handles long plan content appropriately 1`] = `
"Overview
"Recommendation: Regular execution.
Overview
Implement a comprehensive authentication system with multiple providers.
@@ -67,21 +69,21 @@ Implementation Steps
4. Add OAuth2 provider support in src/auth/providers/OAuth2Provider.ts
5. Add SAML provider support in src/auth/providers/SAMLProvider.ts
6. Add LDAP provider support in src/auth/providers/LDAPProvider.ts
7. Create token refresh mechanism in src/auth/TokenManager.ts
8. Add multi-factor authentication in src/auth/MFAService.ts
... last 22 lines hidden ...
... last 24 lines hidden ...
● 1. Yes, automatically accept edits
Approves plan and allows tools to run automatically
● 1. Yes, automatically accept edits (Recommended)
Approves plan and allows tools to run automatically.
2. Yes, manually accept edits
Approves plan but requires confirmation for each tool
Approves plan but requires confirmation for each tool.
3. Type your feedback...
Enter to select · ↑/↓ to navigate · Esc to cancel"
`;
exports[`ExitPlanModeDialog > useAlternateBuffer: false > renders correctly with plan content 1`] = `
"Overview
"Recommendation: Regular execution.
Overview
Add user authentication to the CLI application.
@@ -93,21 +95,21 @@ Implementation Steps
4. Add tests in src/auth/__tests__/
Files to Modify
... last 3 lines hidden ...
- src/index.ts - Add auth middleware
- src/config.ts - Add auth configuration options
● 1. Yes, automatically accept edits
Approves plan and allows tools to run automatically
● 1. Yes, automatically accept edits (Recommended)
Approves plan and allows tools to run automatically.
2. Yes, manually accept edits
Approves plan but requires confirmation for each tool
Approves plan but requires confirmation for each tool.
3. Type your feedback...
Enter to select · ↑/↓ to navigate · Esc to cancel"
`;
exports[`ExitPlanModeDialog > useAlternateBuffer: true > bubbles up Ctrl+C when feedback is empty while editing 1`] = `
"Overview
"Recommendation: Regular execution.
Overview
Add user authentication to the CLI application.
@@ -123,17 +125,19 @@ Files to Modify
- src/index.ts - Add auth middleware
- src/config.ts - Add auth configuration options
1. Yes, automatically accept edits
Approves plan and allows tools to run automatically
1. Yes, automatically accept edits (Recommended)
Approves plan and allows tools to run automatically.
2. Yes, manually accept edits
Approves plan but requires confirmation for each tool
● 3. Type your feedback...
Approves plan but requires confirmation for each tool.
● 3. Type your feedback...
Enter to submit · Esc to cancel"
`;
exports[`ExitPlanModeDialog > useAlternateBuffer: true > calls onFeedback when feedback is typed and submitted 1`] = `
"Overview
"Recommendation: Regular execution.
Overview
Add user authentication to the CLI application.
@@ -149,11 +153,11 @@ Files to Modify
- src/index.ts - Add auth middleware
- src/config.ts - Add auth configuration options
1. Yes, automatically accept edits
Approves plan and allows tools to run automatically
1. Yes, automatically accept edits (Recommended)
Approves plan and allows tools to run automatically.
2. Yes, manually accept edits
Approves plan but requires confirmation for each tool
● 3. Add tests
Approves plan but requires confirmation for each tool.
● 3. Add tests
Enter to submit · Esc to cancel"
`;
@@ -161,7 +165,9 @@ Enter to submit · Esc to cancel"
exports[`ExitPlanModeDialog > useAlternateBuffer: true > displays error state when file read fails 1`] = `" Error reading plan: File not found"`;
exports[`ExitPlanModeDialog > useAlternateBuffer: true > handles long plan content appropriately 1`] = `
"Overview
"Recommendation: Regular execution.
Overview
Implement a comprehensive authentication system with multiple providers.
@@ -198,17 +204,19 @@ Testing Strategy
- Security penetration testing
- Load testing for session management
● 1. Yes, automatically accept edits
Approves plan and allows tools to run automatically
● 1. Yes, automatically accept edits (Recommended)
Approves plan and allows tools to run automatically.
2. Yes, manually accept edits
Approves plan but requires confirmation for each tool
Approves plan but requires confirmation for each tool.
3. Type your feedback...
Enter to select · ↑/↓ to navigate · Esc to cancel"
`;
exports[`ExitPlanModeDialog > useAlternateBuffer: true > renders correctly with plan content 1`] = `
"Overview
"Recommendation: Regular execution.
Overview
Add user authentication to the CLI application.
@@ -224,10 +232,10 @@ Files to Modify
- src/index.ts - Add auth middleware
- src/config.ts - Add auth configuration options
● 1. Yes, automatically accept edits
Approves plan and allows tools to run automatically
● 1. Yes, automatically accept edits (Recommended)
Approves plan and allows tools to run automatically.
2. Yes, manually accept edits
Approves plan but requires confirmation for each tool
Approves plan but requires confirmation for each tool.
3. Type your feedback...
Enter to select · ↑/↓ to navigate · Esc to cancel"
@@ -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
@@ -2,7 +2,7 @@
exports[`MainContent > MainContent Tool Output Height Logic > 'ASB mode - Focused shell should expand' 1`] = `
"ScrollableList
AppHeader
AppHeader(full)
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
│ ⊷ Shell Command Running a long command... │
│ │
@@ -33,7 +33,7 @@ ShowMoreLines"
exports[`MainContent > MainContent Tool Output Height Logic > 'ASB mode - Unfocused shell' 1`] = `
"ScrollableList
AppHeader
AppHeader(full)
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
│ ⊷ Shell Command Running a long command... │
│ │
@@ -57,7 +57,7 @@ ShowMoreLines"
`;
exports[`MainContent > MainContent Tool Output Height Logic > 'Normal mode - Constrained height' 1`] = `
"AppHeader
"AppHeader(full)
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
│ ⊷ Shell Command Running a long command... │
│ │
@@ -81,7 +81,7 @@ ShowMoreLines"
`;
exports[`MainContent > MainContent Tool Output Height Logic > 'Normal mode - Unconstrained height' 1`] = `
"AppHeader
"AppHeader(full)
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
│ ⊷ Shell Command Running a long command... │
│ │
@@ -103,14 +103,3 @@ exports[`MainContent > MainContent Tool Output Height Logic > 'Normal mode - Unc
╰──────────────────────────────────────────────────────────────────────────────────────────────╯
ShowMoreLines"
`;
exports[`MainContent > does not constrain height in alternate buffer mode 1`] = `
"ScrollableList
AppHeader
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
> Hello
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
✦ Hi there
ShowMoreLines
"
`;
@@ -3,39 +3,43 @@
exports[`ShortcutsHelp > renders correctly in 'narrow' mode on 'linux' 1`] = `
"── Shortcuts (for more, see /help) ─────
! shell mode
@ select file or folder
Esc Esc clear & rewind
Tab Tab focus UI
Ctrl+Y YOLO mode
Shift+Tab cycle mode
Ctrl+V paste images
@ select file or folder
Ctrl+Y YOLO mode
Ctrl+R reverse-search history
Esc Esc clear prompt / rewind
Alt+M raw markdown mode
Ctrl+R reverse-search history
Ctrl+X open external editor"
`;
exports[`ShortcutsHelp > renders correctly in 'narrow' mode on 'mac' 1`] = `
"── Shortcuts (for more, see /help) ─────
! shell mode
@ select file or folder
Esc Esc clear & rewind
Tab Tab focus UI
Ctrl+Y YOLO mode
Shift+Tab cycle mode
Ctrl+V paste images
@ select file or folder
Ctrl+Y YOLO mode
Ctrl+R reverse-search history
Esc Esc clear prompt / rewind
Option+M raw markdown mode
Ctrl+R reverse-search history
Ctrl+X open external editor"
`;
exports[`ShortcutsHelp > renders correctly in 'wide' mode on 'linux' 1`] = `
"── Shortcuts (for more, see /help) ─────────────────────────────────────────────────────────────────
! shell mode Shift+Tab cycle mode Ctrl+V paste images
@ select file or folder Ctrl+Y YOLO mode Ctrl+R reverse-search history
Esc Esc clear prompt / rewind Alt+M raw markdown mode Ctrl+X open external editor"
@ select file or folder Ctrl+Y YOLO mode Alt+M raw markdown mode
Esc Esc clear & rewind Ctrl+R reverse-search history Ctrl+X open external editor
Tab Tab focus UI"
`;
exports[`ShortcutsHelp > renders correctly in 'wide' mode on 'mac' 1`] = `
"── Shortcuts (for more, see /help) ─────────────────────────────────────────────────────────────────
! shell mode Shift+Tab cycle mode Ctrl+V paste images
@ select file or folder Ctrl+Y YOLO mode Ctrl+R reverse-search history
Esc Esc clear prompt / rewind Option+M raw markdown mode Ctrl+X open external editor"
@ select file or folder Ctrl+Y YOLO mode Option+M raw markdown mode
Esc Esc clear & rewind Ctrl+R reverse-search history Ctrl+X open external editor
Tab Tab focus UI"
`;
@@ -90,18 +90,18 @@ exports[`ThemeDialog Snapshots > should render correctly in theme selection mode
│ │
│ > Select Theme Preview │
│ ▲ ┌────────────────────────────────────────────────────────────┐ │
1. ANSI Dark │ │ │
│ 2. ANSI Light Light │ 1 # function │ │
│ 3. Atom One Dark │ 2 def fibonacci(n): │ │
│ 4. Ayu Dark │ 3 a, b = 0, 1 │ │
│ 5. Ayu Light Light │ 4 for _ in range(n): │ │
6. Default Dark │ 5 a, b = b, a + b │ │
│ 7. Default Light Light │ 6 return a │ │
│ 8. Dracula Dark │ │ │
│ 9. GitHub Dark │ 1 - print("Hello, " + name) │ │
│ 10. GitHub Light Light │ 1 + print(f"Hello, {name}!") │ │
│ 11. Google Code Light │ │ │
│ 12. Holiday Dark └────────────────────────────────────────────────────────────┘ │
1. ANSI Dark (Matches terminal) │ │ │
│ 2. Atom One Dark │ 1 # function │ │
│ 3. Ayu Dark │ 2 def fibonacci(n): │ │
│ 4. Default Dark │ 3 a, b = 0, 1 │ │
│ 5. Dracula Dark │ 4 for _ in range(n): │ │
6. GitHub Dark │ 5 a, b = b, a + b │ │
│ 7. Holiday Dark │ 6 return a │ │
│ 8. Shades Of Purple Dark │ │ │
│ 9. ANSI Light Light (Incompatible) │ 1 - print("Hello, " + name) │ │
│ 10. Ayu Light Light (Incompatible) │ 1 + print(f"Hello, {name}!") │ │
│ 11. Default Light Light (Incompatible) │ │ │
│ 12. GitHub Light Light (Incompatible) └────────────────────────────────────────────────────────────┘ │
│ ▼ │
│ │
│ (Use Enter to select, Tab to configure scope, Esc to close) │
@@ -40,6 +40,35 @@ exports[`ToolConfirmationQueue > does not render expansion hint when constrainHe
╰──────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`ToolConfirmationQueue > renders AskUser tool confirmation with Success color 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ Answer Questions │
│ │
│ Review your answers: │
│ │
│ │
│ Enter to submit · Tab/Shift+Tab to edit answers · Esc to cancel │
╰──────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`ToolConfirmationQueue > renders ExitPlanMode tool confirmation with Success color 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ Ready to start implementation? │
│ │
│ Recommendation: Regular execution. │
│ │
│ Plan content goes here │
│ │
│ ● 1. Yes, automatically accept edits │
│ Approves plan and allows tools to run automatically. │
│ 2. Yes, manually accept edits │
│ Approves plan but requires confirmation for each tool. │
│ 3. Type your feedback... │
│ │
│ Enter to select · ↑/↓ to navigate · Esc to cancel │
╰──────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`ToolConfirmationQueue > renders expansion hint when content is long and constrained 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ Action Required │
@@ -46,4 +46,21 @@ describe('<GeminiMessage /> - Raw Markdown Display Snapshots', () => {
expect(lastFrame()).toMatchSnapshot();
},
);
it('wraps long lines correctly in raw markdown mode', () => {
const terminalWidth = 20;
const text =
'This is a long line that should wrap correctly without truncation';
const { lastFrame } = renderWithProviders(
<GeminiMessage
text={text}
isPending={false}
terminalWidth={terminalWidth}
/>,
{
uiState: { renderMarkdown: false, streamingState: StreamingState.Idle },
},
);
expect(lastFrame()).toMatchSnapshot();
});
});
@@ -47,7 +47,7 @@ export const GeminiMessage: React.FC<GeminiMessageProps> = ({
? undefined
: Math.max(availableTerminalHeight - 1, 1)
}
terminalWidth={terminalWidth}
terminalWidth={Math.max(terminalWidth - prefixWidth, 0)}
renderMarkdown={renderMarkdown}
/>
<Box

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