mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-23 08:10:57 -07:00
Compare commits
101 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ff7738b5d | |||
| 1ad26adb2b | |||
| af5aec69da | |||
| dae67983a8 | |||
| 70856d5a6e | |||
| cec45a1ebc | |||
| 767d80e768 | |||
| 3e5e608a22 | |||
| 0bc2d3ab16 | |||
| 31960c3388 | |||
| 0cc4f09595 | |||
| 70336e73b1 | |||
| 7cfbb6fb71 | |||
| a105768de8 | |||
| 347f3fe7e4 | |||
| 25803e05fd | |||
| 774ae220be | |||
| 813e0c18ac | |||
| 3f6cec22e6 | |||
| 3966f3c053 | |||
| 2e3cbd6363 | |||
| 8b1dc15182 | |||
| fa9aee2bf0 | |||
| 6628cbb39d | |||
| aa9163da60 | |||
| ec0f23ae03 | |||
| ac04c388e0 | |||
| 621ddbe744 | |||
| c537fd5aec | |||
| a91bc60e18 | |||
| faa1ec3044 | |||
| d96bd05d36 | |||
| 84666e1bbc | |||
| a7d851146a | |||
| acb7f577de | |||
| d2d345f41a | |||
| dfd7721e69 | |||
| 09218572d0 | |||
| 5d98ed5820 | |||
| 727f9b67b1 | |||
| 547f5d45f5 | |||
| 58d637f919 | |||
| b746524a1b | |||
| c5baf39dbd | |||
| b48970da15 | |||
| 9a8e5d3940 | |||
| 089aec8b8d | |||
| b7555ab1e1 | |||
| c04602f209 | |||
| a01d7e9a05 | |||
| 49b2e76ee1 | |||
| aed348a99c | |||
| 7cf4c05c66 | |||
| cdf157e52a | |||
| 723f269df6 | |||
| 239aa0909c | |||
| 6cfd29ef9b | |||
| f97b04cc9a | |||
| 788a40c445 | |||
| ce03156c9f | |||
| fe428936d5 | |||
| d24f10b087 | |||
| d54702185b | |||
| 27b7fc04de | |||
| be03e0619f | |||
| 2bb7aaecd0 | |||
| 0f855fc0c4 | |||
| c7e309efc9 | |||
| 429932c663 | |||
| d08b1efc72 | |||
| c3b52b8206 | |||
| 5fd557347e | |||
| cbfb2a4e26 | |||
| fb1b1b451d | |||
| 99fa700231 | |||
| f1c0a695f8 | |||
| 6351352e54 | |||
| 2ac39b6acc | |||
| d8b24e6983 | |||
| d25c469f77 | |||
| 537e56ffae | |||
| 2cba2ab37a | |||
| 8064973899 | |||
| ddc5458451 | |||
| a468407098 | |||
| 264c7aceaa | |||
| 3408542a66 | |||
| c276d0c7b6 | |||
| a00eb3b8e6 | |||
| 372f41eab8 | |||
| 09b623fbd7 | |||
| ba3e327ba1 | |||
| 5d235952ba | |||
| 082f41f54d | |||
| 880af43b02 | |||
| 966eef14ee | |||
| b79e5ce56d | |||
| 1a8d77329e | |||
| ad9c49a604 | |||
| 70b427c7dd | |||
| a804f99fe0 |
@@ -109,23 +109,41 @@ detailed **highlights** section for the release-specific page.
|
||||
|
||||
- **Target File**: `docs/changelogs/latest.md`
|
||||
- Perform the following edits on the target file:
|
||||
1. Update the version in the main header.
|
||||
2. Update the "Released:" date.
|
||||
1. Update the version in the main header. The line should read,
|
||||
`# Latest stable release: {{version}}`
|
||||
2. Update the rease date. The line should read,
|
||||
`Released: {{release_date_month_dd_yyyy}}`
|
||||
3. **Prepend** the processed "What's Changed" list from the temporary file
|
||||
to the existing "What's Changed" list in the file.
|
||||
4. In the "Full Changelog" URL, replace only the trailing version with the
|
||||
new patch version.
|
||||
to the existing "What's Changed" list in `latest.md`. Do not change or
|
||||
replace the existing list, **only add** to the beginning of it.
|
||||
4. In the "Full Changelog", edit **only** the end of the URL. Identify the
|
||||
last part of the URL that looks like `...{previous_version}` and update
|
||||
it to be `...{version}`.
|
||||
|
||||
Example: assume the patch version is `v0.29.1`. Change
|
||||
`Full Changelog: https://github.com/google-gemini/gemini-cli/compare/v0.28.2…v0.29.0`
|
||||
to
|
||||
`Full Changelog: https://github.com/google-gemini/gemini-cli/compare/v0.28.2…v0.29.1`
|
||||
|
||||
### B.2: Preview Patch (e.g., `v0.29.0-preview.3`)
|
||||
|
||||
- **Target File**: `docs/changelogs/preview.md`
|
||||
- Perform the following edits on the target file:
|
||||
1. Update the version in the main header.
|
||||
2. Update the "Released:" date.
|
||||
1. Update the version in the main header. The line should read,
|
||||
`# Preview release: {{version}}`
|
||||
2. Update the rease date. The line should read,
|
||||
`Released: {{release_date_month_dd_yyyy}}`
|
||||
3. **Prepend** the processed "What's Changed" list from the temporary file
|
||||
to the existing "What's Changed" list in the file.
|
||||
4. In the "Full Changelog" URL, replace only the trailing version with the
|
||||
new patch version.
|
||||
to the existing "What's Changed" list in `preview.md`. Do not change or
|
||||
replace the existing list, **only add** to the beginning of it.
|
||||
4. In the "Full Changelog", edit **only** the end of the URL. Identify the
|
||||
last part of the URL that looks like `...{previous_version}` and update
|
||||
it to be `...{version}`.
|
||||
|
||||
Example: assume the patch version is `v0.29.0-preview.1`. Change
|
||||
`Full Changelog: https://github.com/google-gemini/gemini-cli/compare/v0.28.2…v0.29.0-preview.0`
|
||||
to
|
||||
`Full Changelog: https://github.com/google-gemini/gemini-cli/compare/v0.28.2…v0.29.0-preview.1`
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
name: pr-address-comments
|
||||
description: Use this skill if the user asks you to help them address GitHub PR comments for their current branch of the Gemini CLI. Requires `gh` CLI tool.
|
||||
---
|
||||
You are helping the user address comments on their Pull Request. These comments may have come from an automated review agent or a team member.
|
||||
|
||||
OBJECTIVE: Help the user review and address comments on their PR.
|
||||
|
||||
# Comment Review Procedure
|
||||
|
||||
1. Run the `scripts/fetch-pr-info.js` script to get PR info and state. MAKE SURE you read the entire output of the command, even if it gets truncated.
|
||||
2. Summarize the review status by analyzing the diff, commit log, and comments to see which still need to be addressed. Pay attention to the current user's comments. For resolved threads, summarize as a single line with a ✅. For open threads, provide a reference number e.g. [1] and the comment content.
|
||||
3. Present your summary of the feedback and current state and allow the user to guide you as to what to fix/address/skip. DO NOT begin fixing issues automatically.
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/* eslint-env node */
|
||||
/* global console, process */
|
||||
|
||||
import { exec } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
async function run(cmd) {
|
||||
try {
|
||||
const { stdout } = await execAsync(cmd, {
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'ignore'],
|
||||
});
|
||||
return stdout.trim();
|
||||
} catch (_e) {
|
||||
// eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const IGNORE_MESSAGES = [
|
||||
'thank you so much for your contribution to Gemini CLI!',
|
||||
"I'm currently reviewing this pull request and will post my feedback shortly.",
|
||||
'This pull request is being closed because it is not currently linked to an issue.',
|
||||
];
|
||||
|
||||
const shouldIgnore = (body) => {
|
||||
if (!body) return false;
|
||||
return IGNORE_MESSAGES.some((msg) => body.includes(msg));
|
||||
};
|
||||
|
||||
async function main() {
|
||||
const branch = await run('git branch --show-current');
|
||||
if (!branch) {
|
||||
console.error('❌ Could not determine current git branch.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const gqlQuery = `query($branch:String!){repository(name:"gemini-cli",owner:"google-gemini"){pullRequests(headRefName:$branch,first:100){nodes{id,number,state,comments(first:100){nodes{createdAt,isMinimized,minimizedReason,author{login},body,url,authorAssociation}},reviews(first:100){nodes{id,author{login},createdAt,isMinimized,minimizedReason,body,state,comments(first:30){nodes{id,replyTo{id},author{login},createdAt,body,isMinimized,minimizedReason,path,line,startLine,originalLine,originalStartLine}}}}}}}}`;
|
||||
|
||||
const [authInfo, diff, commits, rawJson] = await Promise.all([
|
||||
run('gh auth status -a'),
|
||||
run('gh pr diff'),
|
||||
run(
|
||||
'git fetch && git log origin/main..origin/$(git branch --show-current)',
|
||||
),
|
||||
run(`gh api graphql -F branch="${branch}" -f query='${gqlQuery}'`),
|
||||
]);
|
||||
|
||||
if (!diff) {
|
||||
console.error(`⚠️ No active PR found for branch: ${branch}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`\n# Current GitHub user info:\n\n${authInfo}\n`);
|
||||
console.log(`\n# PR diff for current branch: ${branch}\n\n\`\`\``);
|
||||
console.log(diff);
|
||||
console.log('```');
|
||||
console.log(
|
||||
`\n# Commit history (origin/main..origin/${branch})\n\n${commits}`,
|
||||
);
|
||||
|
||||
const data = JSON.parse(rawJson || '{}');
|
||||
const prs = data?.data?.repository?.pullRequests?.nodes || [];
|
||||
|
||||
// Sort PRs by number descending so we check the newest one first
|
||||
prs.sort((a, b) => b.number - a.number);
|
||||
|
||||
const pr = prs.find((p) => p.state === 'OPEN') || prs[0];
|
||||
|
||||
if (!pr) {
|
||||
console.error('❌ No PR data found.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('\n# PR Feedback\n');
|
||||
|
||||
// 1. General PR Comments
|
||||
const general = pr.comments.nodes.filter((c) => !shouldIgnore(c.body));
|
||||
if (general.length > 0) {
|
||||
console.log('\n💬 GENERAL COMMENTS:');
|
||||
general.forEach((c) => {
|
||||
const minimized = c.isMinimized
|
||||
? ` (Minimized: ${c.minimizedReason})`
|
||||
: '';
|
||||
console.log(
|
||||
`[${c.createdAt}] [${c.author.login}]${minimized}: ${c.body}\n`,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Process ALL Review Comments into a single Thread Map
|
||||
const allInlineComments = pr.reviews.nodes.flatMap((r) => r.comments.nodes);
|
||||
const filteredInlines = allInlineComments.filter(
|
||||
(c) => !shouldIgnore(c.body),
|
||||
);
|
||||
|
||||
console.log('🔍 CODE REVIEWS & INLINE THREADS:');
|
||||
|
||||
// Print Review Summaries First
|
||||
pr.reviews.nodes.forEach((review) => {
|
||||
if (review.body && !shouldIgnore(review.body)) {
|
||||
const icon = review.state === 'APPROVED' ? '✅' : '💬';
|
||||
const minimized = review.isMinimized
|
||||
? ` (Minimized: ${review.minimizedReason})`
|
||||
: '';
|
||||
console.log(
|
||||
`\n${icon} ${review.state} by ${review.author.login} at ${review.createdAt}${minimized}: "${review.body}"`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Build and Print Threads
|
||||
const topLevelThreads = filteredInlines.filter((c) => !c.replyTo);
|
||||
|
||||
const printThread = (parentId, depth = 1) => {
|
||||
const indent = ' '.repeat(depth);
|
||||
filteredInlines
|
||||
.filter((c) => c.replyTo?.id === parentId)
|
||||
.forEach((reply) => {
|
||||
const minimized = reply.isMinimized
|
||||
? ` (Minimized: ${reply.minimizedReason})`
|
||||
: '';
|
||||
console.log(
|
||||
`${indent}↳ [${reply.createdAt}] ${reply.author.login}${minimized}: ${reply.body}`,
|
||||
);
|
||||
printThread(reply.id, depth + 1);
|
||||
});
|
||||
};
|
||||
|
||||
topLevelThreads.forEach((c) => {
|
||||
const start = c.startLine || c.originalStartLine;
|
||||
const end = c.line || c.originalLine;
|
||||
const range = start && end && start !== end ? `${start}-${end}` : end || '';
|
||||
const fileInfo = c.path
|
||||
? `(${c.path}${range ? `:${range}` : ''}) `
|
||||
: range
|
||||
? `(Line ${range}) `
|
||||
: '';
|
||||
const minimized = c.isMinimized ? ` (Minimized: ${c.minimizedReason})` : '';
|
||||
console.log(
|
||||
`\n💬 ${minimized}${c.author.login} | ${c.createdAt} ${fileInfo}\n${c.body}`,
|
||||
);
|
||||
printThread(c.id);
|
||||
});
|
||||
|
||||
console.log('\n');
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('❌ Unexpected error:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -77,6 +77,14 @@ runs:
|
||||
--image google/gemini-cli-sandbox:${{ steps.image_tag.outputs.FINAL_TAG }} \
|
||||
--output-file final_image_uri.txt
|
||||
echo "uri=$(cat final_image_uri.txt)" >> $GITHUB_OUTPUT
|
||||
- name: 'verify'
|
||||
shell: 'bash'
|
||||
run: |-
|
||||
docker run --rm --entrypoint sh "${{ steps.docker_build.outputs.uri }}" -lc '
|
||||
set -e
|
||||
node -e "const fs=require(\"node:fs\"); JSON.parse(fs.readFileSync(\"/usr/local/share/npm-global/lib/node_modules/@google/gemini-cli/package.json\",\"utf8\")); JSON.parse(fs.readFileSync(\"/usr/local/share/npm-global/lib/node_modules/@google/gemini-cli-core/package.json\",\"utf8\"));"
|
||||
/usr/local/share/npm-global/bin/gemini --version >/dev/null
|
||||
'
|
||||
- name: 'publish'
|
||||
shell: 'bash'
|
||||
if: "${{ inputs.dry-run != 'true' }}"
|
||||
|
||||
@@ -155,7 +155,10 @@ jobs:
|
||||
"telemetry": {
|
||||
"enabled": true,
|
||||
"target": "gcp"
|
||||
}
|
||||
},
|
||||
"coreTools": [
|
||||
"run_shell_command(echo)"
|
||||
],
|
||||
}
|
||||
prompt: |-
|
||||
## Role
|
||||
@@ -284,8 +287,21 @@ jobs:
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
core.setFailed(`Output is not valid JSON and does not contain a JSON markdown block.\nRaw output: ${rawOutput}`);
|
||||
return;
|
||||
// If no markdown block, try to find a raw JSON object in the output.
|
||||
// The CLI may include debug/log lines (e.g. telemetry init, YOLO mode)
|
||||
// before the actual JSON response.
|
||||
const jsonObjectMatch = rawOutput.match(/(\{[\s\S]*"labels_to_set"[\s\S]*\})/);
|
||||
if (jsonObjectMatch) {
|
||||
try {
|
||||
parsedLabels = JSON.parse(jsonObjectMatch[0]);
|
||||
} catch (extractError) {
|
||||
core.setFailed(`Found JSON-like content but failed to parse: ${extractError.message}\nRaw output: ${rawOutput}`);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
core.setFailed(`Output is not valid JSON and does not contain extractable JSON.\nRaw output: ${rawOutput}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-7
@@ -372,8 +372,7 @@ specific debug settings.
|
||||
|
||||
### React DevTools
|
||||
|
||||
To debug the CLI's React-based UI, you can use React DevTools. Ink, the library
|
||||
used for the CLI's interface, is compatible with React DevTools version 4.x.
|
||||
To debug the CLI's React-based UI, you can use React DevTools.
|
||||
|
||||
1. **Start the Gemini CLI in development mode:**
|
||||
|
||||
@@ -381,20 +380,20 @@ used for the CLI's interface, is compatible with React DevTools version 4.x.
|
||||
DEV=true npm start
|
||||
```
|
||||
|
||||
2. **Install and run React DevTools version 4.28.5 (or the latest compatible
|
||||
4.x version):**
|
||||
2. **Install and run React DevTools version 6 (which matches the CLI's
|
||||
`react-devtools-core`):**
|
||||
|
||||
You can either install it globally:
|
||||
|
||||
```bash
|
||||
npm install -g react-devtools@4.28.5
|
||||
npm install -g react-devtools@6
|
||||
react-devtools
|
||||
```
|
||||
|
||||
Or run it directly using npx:
|
||||
|
||||
```bash
|
||||
npx react-devtools@4.28.5
|
||||
npx react-devtools@6
|
||||
```
|
||||
|
||||
Your running CLI application should then connect to React DevTools.
|
||||
@@ -546,7 +545,7 @@ Before submitting your documentation pull request, please:
|
||||
|
||||
If you have questions about contributing documentation:
|
||||
|
||||
- Check our [FAQ](/docs/faq.md).
|
||||
- Check our [FAQ](/docs/resources/faq.md).
|
||||
- Review existing documentation for examples.
|
||||
- Open [an issue](https://github.com/google-gemini/gemini-cli/issues) to discuss
|
||||
your proposed changes.
|
||||
|
||||
+4
-1
@@ -42,7 +42,10 @@ USER node
|
||||
# install gemini-cli and clean up
|
||||
COPY packages/cli/dist/google-gemini-cli-*.tgz /tmp/gemini-cli.tgz
|
||||
COPY packages/core/dist/google-gemini-cli-core-*.tgz /tmp/gemini-core.tgz
|
||||
RUN npm install -g /tmp/gemini-cli.tgz /tmp/gemini-core.tgz \
|
||||
RUN npm install -g /tmp/gemini-core.tgz \
|
||||
&& npm install -g /tmp/gemini-cli.tgz \
|
||||
&& node -e "const fs=require('node:fs'); JSON.parse(fs.readFileSync('/usr/local/share/npm-global/lib/node_modules/@google/gemini-cli/package.json','utf8')); JSON.parse(fs.readFileSync('/usr/local/share/npm-global/lib/node_modules/@google/gemini-cli-core/package.json','utf8'));" \
|
||||
&& gemini --version > /dev/null \
|
||||
&& npm cache clean --force \
|
||||
&& rm -f /tmp/gemini-{cli,core}.tgz
|
||||
|
||||
|
||||
@@ -295,7 +295,8 @@ on GitHub.
|
||||
- **Experimental permission improvements:** We are now experimenting with a new
|
||||
policy engine in Gemini CLI. This allows users and administrators to create
|
||||
fine-grained policy for tool calls. Currently behind a flag. See
|
||||
[policy engine documentation](../core/policy-engine.md) for more information.
|
||||
[policy engine documentation](../reference/policy-engine.md) for more
|
||||
information.
|
||||
- Blog:
|
||||
[https://allen.hutchison.org/2025/11/26/the-guardrails-of-autonomy/](https://allen.hutchison.org/2025/11/26/the-guardrails-of-autonomy/)
|
||||
- **Gemini 3 support for paid:** Gemini 3 support has been rolled out to all API
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Preview release: v0.30.0-preview.1
|
||||
# Preview release: v0.30.0-preview.3
|
||||
|
||||
Released: February 19, 2026
|
||||
|
||||
@@ -311,4 +311,4 @@ npm install -g @google/gemini-cli@preview
|
||||
[#19008](https://github.com/google-gemini/gemini-cli/pull/19008)
|
||||
|
||||
**Full changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.29.0-preview.5...v0.30.0-preview.1
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.29.0-preview.5...v0.30.0-preview.3
|
||||
|
||||
+23
-23
@@ -26,29 +26,29 @@ and parameters.
|
||||
|
||||
## CLI Options
|
||||
|
||||
| Option | Alias | Type | Default | Description |
|
||||
| -------------------------------- | ----- | ------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--debug` | `-d` | boolean | `false` | Run in debug mode with verbose logging |
|
||||
| `--version` | `-v` | - | - | Show CLI version number and exit |
|
||||
| `--help` | `-h` | - | - | Show help information |
|
||||
| `--model` | `-m` | string | `auto` | Model to use. See [Model Selection](#model-selection) for available values. |
|
||||
| `--prompt` | `-p` | string | - | Prompt text. Appended to stdin input if provided. **Deprecated:** Use positional arguments instead. |
|
||||
| `--prompt-interactive` | `-i` | string | - | Execute prompt and continue in interactive mode |
|
||||
| `--sandbox` | `-s` | boolean | `false` | Run in a sandboxed environment for safer execution |
|
||||
| `--approval-mode` | - | string | `default` | Approval mode for tool execution. Choices: `default`, `auto_edit`, `yolo` |
|
||||
| `--yolo` | `-y` | boolean | `false` | **Deprecated.** Auto-approve all actions. Use `--approval-mode=yolo` instead. |
|
||||
| `--experimental-acp` | - | boolean | - | Start in ACP (Agent Code Pilot) mode. **Experimental feature.** |
|
||||
| `--experimental-zed-integration` | - | boolean | - | Run in Zed editor integration mode. **Experimental feature.** |
|
||||
| `--allowed-mcp-server-names` | - | array | - | Allowed MCP server names (comma-separated or multiple flags) |
|
||||
| `--allowed-tools` | - | array | - | **Deprecated.** Use the [Policy Engine](../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` |
|
||||
| Option | Alias | Type | Default | Description |
|
||||
| -------------------------------- | ----- | ------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--debug` | `-d` | boolean | `false` | Run in debug mode with verbose logging |
|
||||
| `--version` | `-v` | - | - | Show CLI version number and exit |
|
||||
| `--help` | `-h` | - | - | Show help information |
|
||||
| `--model` | `-m` | string | `auto` | Model to use. See [Model Selection](#model-selection) for available values. |
|
||||
| `--prompt` | `-p` | string | - | Prompt text. Appended to stdin input if provided. **Deprecated:** Use positional arguments instead. |
|
||||
| `--prompt-interactive` | `-i` | string | - | Execute prompt and continue in interactive mode |
|
||||
| `--sandbox` | `-s` | boolean | `false` | Run in a sandboxed environment for safer execution |
|
||||
| `--approval-mode` | - | string | `default` | Approval mode for tool execution. Choices: `default`, `auto_edit`, `yolo` |
|
||||
| `--yolo` | `-y` | boolean | `false` | **Deprecated.** Auto-approve all actions. Use `--approval-mode=yolo` instead. |
|
||||
| `--experimental-acp` | - | boolean | - | Start in ACP (Agent Code Pilot) mode. **Experimental feature.** |
|
||||
| `--experimental-zed-integration` | - | boolean | - | Run in Zed editor integration mode. **Experimental feature.** |
|
||||
| `--allowed-mcp-server-names` | - | array | - | Allowed MCP server names (comma-separated or multiple flags) |
|
||||
| `--allowed-tools` | - | array | - | **Deprecated.** Use the [Policy Engine](../reference/policy-engine.md) instead. Tools that are allowed to run without confirmation (comma-separated or multiple flags) |
|
||||
| `--extensions` | `-e` | array | - | List of extensions to use. If not provided, all extensions are enabled (comma-separated or multiple flags) |
|
||||
| `--list-extensions` | `-l` | boolean | - | List all available extensions and exit |
|
||||
| `--resume` | `-r` | string | - | Resume a previous session. Use `"latest"` for most recent or index number (e.g. `--resume 5`) |
|
||||
| `--list-sessions` | - | boolean | - | List available sessions for the current project and exit |
|
||||
| `--delete-session` | - | string | - | Delete a session by index number (use `--list-sessions` to see available sessions) |
|
||||
| `--include-directories` | - | array | - | Additional directories to include in the workspace (comma-separated or multiple flags) |
|
||||
| `--screen-reader` | - | boolean | - | Enable screen reader mode for accessibility |
|
||||
| `--output-format` | `-o` | string | `text` | The format of the CLI output. Choices: `text`, `json`, `stream-json` |
|
||||
|
||||
## Model selection
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ The most powerful tools for enterprise administration are the system-wide
|
||||
settings files. These files allow you to define a baseline configuration
|
||||
(`system-defaults.json`) and a set of overrides (`settings.json`) that apply to
|
||||
all users on a machine. For a complete overview of configuration options, see
|
||||
the [Configuration documentation](../get-started/configuration.md).
|
||||
the [Configuration documentation](../reference/configuration.md).
|
||||
|
||||
Settings are merged from four files. The precedence order for single-value
|
||||
settings (like `theme`) is:
|
||||
@@ -224,8 +224,8 @@ gemini
|
||||
|
||||
You can significantly enhance security by controlling which tools the Gemini
|
||||
model can use. This is achieved through the `tools.core` setting and the
|
||||
[Policy Engine](../core/policy-engine.md). For a list of available tools, see
|
||||
the [Tools documentation](../tools/index.md).
|
||||
[Policy Engine](../reference/policy-engine.md). For a list of available tools,
|
||||
see the [Tools documentation](../tools/index.md).
|
||||
|
||||
### Allowlisting with `coreTools`
|
||||
|
||||
@@ -245,8 +245,8 @@ on the approved list.
|
||||
|
||||
### Blocklisting with `excludeTools` (Deprecated)
|
||||
|
||||
> **Deprecated:** Use the [Policy Engine](../core/policy-engine.md) for more
|
||||
> robust control.
|
||||
> **Deprecated:** Use the [Policy Engine](../reference/policy-engine.md) for
|
||||
> more robust control.
|
||||
|
||||
Alternatively, you can add specific tools that are considered dangerous in your
|
||||
environment to a blocklist.
|
||||
@@ -289,8 +289,8 @@ unintended tool execution.
|
||||
## Managing custom tools (MCP servers)
|
||||
|
||||
If your organization uses custom tools via
|
||||
[Model-Context Protocol (MCP) servers](../core/tools-api.md), it is crucial to
|
||||
understand how server configurations are managed to apply security policies
|
||||
[Model-Context Protocol (MCP) servers](../reference/tools-api.md), it is crucial
|
||||
to understand how server configurations are managed to apply security policies
|
||||
effectively.
|
||||
|
||||
### How MCP server configurations are merged
|
||||
|
||||
@@ -88,7 +88,7 @@ More content here.
|
||||
@../shared/style-guide.md
|
||||
```
|
||||
|
||||
For more details, see the [Memory Import Processor](../core/memport.md)
|
||||
For more details, see the [Memory Import Processor](../reference/memport.md)
|
||||
documentation.
|
||||
|
||||
## Customize the context file name
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ To enable Gemini 3 Pro and Gemini 3 Flash (if available), enable
|
||||
|
||||
You can also use the `--model` flag to specify a particular Gemini model on
|
||||
startup. For more details, refer to the
|
||||
[configuration documentation](../get-started/configuration.md).
|
||||
[configuration documentation](../reference/configuration.md).
|
||||
|
||||
Changes to these settings will be applied to all subsequent interactions with
|
||||
Gemini CLI.
|
||||
|
||||
+129
-74
@@ -1,50 +1,61 @@
|
||||
# Plan Mode (experimental)
|
||||
|
||||
Plan Mode is a safe, read-only mode for researching and designing complex
|
||||
changes. It prevents modifications while you research, design and plan an
|
||||
implementation strategy.
|
||||
Plan Mode is a read-only environment for architecting robust solutions before
|
||||
implementation. It allows you to:
|
||||
|
||||
> **Note: Plan Mode is currently an experimental feature.**
|
||||
>
|
||||
> Experimental features are subject to change. To use Plan Mode, enable it via
|
||||
> `/settings` (search for `Plan`) or add the following to your `settings.json`:
|
||||
>
|
||||
> ```json
|
||||
> {
|
||||
> "experimental": {
|
||||
> "plan": true
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> Your feedback is invaluable as we refine this feature. If you have ideas,
|
||||
- **Research:** Explore the project in a read-only state to prevent accidental
|
||||
changes.
|
||||
- **Design:** Understand problems, evaluate trade-offs, and choose a solution.
|
||||
- **Plan:** Align on an execution strategy before any code is modified.
|
||||
|
||||
> **Note:** This is a preview feature currently under active development. Your
|
||||
> feedback is invaluable as we refine this feature. If you have ideas,
|
||||
> suggestions, or encounter issues:
|
||||
>
|
||||
> - Use the `/bug` command within the CLI to file an issue.
|
||||
> - [Open an issue](https://github.com/google-gemini/gemini-cli/issues) on
|
||||
> GitHub.
|
||||
> - Use the **/bug** command within Gemini CLI to file an issue.
|
||||
|
||||
- [Starting in Plan Mode](#starting-in-plan-mode)
|
||||
- [Enabling Plan Mode](#enabling-plan-mode)
|
||||
- [How to use Plan Mode](#how-to-use-plan-mode)
|
||||
- [Entering Plan Mode](#entering-plan-mode)
|
||||
- [The Planning Workflow](#the-planning-workflow)
|
||||
- [Planning Workflow](#planning-workflow)
|
||||
- [Exiting Plan Mode](#exiting-plan-mode)
|
||||
- [Tool Restrictions](#tool-restrictions)
|
||||
- [Customizing Planning with Skills](#customizing-planning-with-skills)
|
||||
- [Customizing Policies](#customizing-policies)
|
||||
- [Example: Allow git commands in Plan Mode](#example-allow-git-commands-in-plan-mode)
|
||||
- [Example: Enable research subagents in Plan Mode](#example-enable-research-subagents-in-plan-mode)
|
||||
- [Custom Plan Directory and Policies](#custom-plan-directory-and-policies)
|
||||
|
||||
## Starting in Plan Mode
|
||||
## Enabling Plan Mode
|
||||
|
||||
You can configure Gemini CLI to start directly in Plan Mode by default:
|
||||
To use Plan Mode, enable it via **/settings** (search for **Plan**) or add the
|
||||
following to your `settings.json`:
|
||||
|
||||
1. Type `/settings` in the CLI.
|
||||
2. Search for `Default Approval Mode`.
|
||||
3. Set the value to `Plan`.
|
||||
```json
|
||||
{
|
||||
"experimental": {
|
||||
"plan": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Other ways to start in Plan Mode:
|
||||
## How to use Plan Mode
|
||||
|
||||
- **CLI Flag:** `gemini --approval-mode=plan`
|
||||
- **Manual Settings:** Manually update your `settings.json`:
|
||||
### Entering Plan Mode
|
||||
|
||||
You can configure Gemini CLI to start in Plan Mode by default or enter it
|
||||
manually during a session.
|
||||
|
||||
- **Configuration:** Configure Gemini CLI to start directly in Plan Mode by
|
||||
default:
|
||||
1. Type `/settings` in the CLI.
|
||||
2. Search for **Default Approval Mode**.
|
||||
3. Set the value to **Plan**.
|
||||
|
||||
Alternatively, use the `gemini --approval-mode=plan` CLI flag or manually
|
||||
update:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -54,42 +65,44 @@ Other ways to start in Plan Mode:
|
||||
}
|
||||
```
|
||||
|
||||
## How to use Plan Mode
|
||||
- **Keyboard Shortcut:** Press `Shift+Tab` to cycle through approval modes
|
||||
(`Default` -> `Auto-Edit` -> `Plan`).
|
||||
|
||||
### Entering Plan Mode
|
||||
> **Note:** Plan Mode is automatically removed from the rotation when Gemini
|
||||
> CLI is actively processing or showing confirmation dialogs.
|
||||
|
||||
You can enter Plan Mode in three ways:
|
||||
- **Command:** Type `/plan` in the input box.
|
||||
|
||||
1. **Keyboard Shortcut:** Press `Shift+Tab` to cycle through approval modes
|
||||
(`Default` -> `Auto-Edit` -> `Plan`).
|
||||
- **Natural Language:** Ask Gemini CLI to "start a plan for...". Gemini CLI then
|
||||
calls the [`enter_plan_mode`] tool to switch modes.
|
||||
> **Note:** This tool is not available when Gemini CLI is in [YOLO mode].
|
||||
|
||||
> **Note:** Plan Mode is automatically removed from the rotation when the
|
||||
> agent is actively processing or showing confirmation dialogs.
|
||||
### Planning Workflow
|
||||
|
||||
2. **Command:** Type `/plan` in the input box.
|
||||
3. **Natural Language:** Ask the agent to "start a plan for...". The agent will
|
||||
then call the [`enter_plan_mode`] tool to switch modes.
|
||||
|
||||
### The Planning Workflow
|
||||
|
||||
1. **Requirements:** The agent clarifies goals using [`ask_user`].
|
||||
2. **Exploration:** The agent uses read-only tools (like [`read_file`]) to map
|
||||
the codebase and validate assumptions.
|
||||
3. **Design:** The agent proposes alternative approaches with a recommended
|
||||
solution for you to choose from.
|
||||
4. **Planning:** A detailed plan is written to a temporary Markdown file.
|
||||
5. **Review:** You review the plan.
|
||||
- **Approve:** Exit Plan Mode and start implementation (switching to
|
||||
Auto-Edit or Default approval mode).
|
||||
1. **Explore & Analyze:** Analyze requirements and use read-only tools to map
|
||||
the codebase and validate assumptions. For complex tasks, identify at least
|
||||
two viable implementation approaches.
|
||||
2. **Consult:** Present a summary of the identified approaches via [`ask_user`]
|
||||
to obtain a selection. For simple or canonical tasks, this step may be
|
||||
skipped.
|
||||
3. **Draft:** Once an approach is selected, write a detailed implementation
|
||||
plan to the plans directory.
|
||||
4. **Review & Approval:** Use the [`exit_plan_mode`] tool to present the plan
|
||||
and formally request approval.
|
||||
- **Approve:** Exit Plan Mode and start implementation.
|
||||
- **Iterate:** Provide feedback to refine the plan.
|
||||
|
||||
For more complex or specialized planning tasks, you can
|
||||
[customize the planning workflow with skills](#customizing-planning-with-skills).
|
||||
|
||||
### Exiting Plan Mode
|
||||
|
||||
To exit Plan Mode:
|
||||
To exit Plan Mode, you can:
|
||||
|
||||
1. **Keyboard Shortcut:** Press `Shift+Tab` to cycle to the desired mode.
|
||||
2. **Tool:** The agent calls the [`exit_plan_mode`] tool to present the
|
||||
finalized plan for your approval.
|
||||
- **Keyboard Shortcut:** Press `Shift+Tab` to cycle to the desired mode.
|
||||
|
||||
- **Tool:** Gemini CLI calls the [`exit_plan_mode`] tool to present the
|
||||
finalized plan for your approval.
|
||||
|
||||
## Tool Restrictions
|
||||
|
||||
@@ -102,30 +115,31 @@ These are the only allowed tools:
|
||||
- **Interaction:** [`ask_user`]
|
||||
- **MCP Tools (Read):** Read-only [MCP tools] (e.g., `github_read_issue`,
|
||||
`postgres_read_schema`) are allowed.
|
||||
- **Planning (Write):** [`write_file`] and [`replace`] ONLY allowed for `.md`
|
||||
files in the `~/.gemini/tmp/<project>/<session-id>/plans/` directory.
|
||||
- **Planning (Write):** [`write_file`] and [`replace`] only allowed for `.md`
|
||||
files in the `~/.gemini/tmp/<project>/<session-id>/plans/` directory or your
|
||||
[custom plans directory](#custom-plan-directory-and-policies).
|
||||
- **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.
|
||||
You can use [Agent Skills](./skills.md) to customize how Gemini CLI approaches
|
||||
planning for specific types of tasks. When a skill is activated during Plan
|
||||
Mode, its specialized instructions and procedural workflows will guide the
|
||||
research, design and planning phases.
|
||||
|
||||
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
|
||||
- A **"Security Audit"** skill could prompt Gemini CLI to look for specific
|
||||
vulnerabilities during codebase exploration.
|
||||
- A **"Frontend Design"** skill could guide the agent to use specific UI
|
||||
- A **"Frontend Design"** skill could guide Gemini CLI 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.
|
||||
To use a skill in Plan Mode, you can explicitly ask Gemini CLI to "use the
|
||||
`<skill-name>` skill to plan..." or Gemini CLI may autonomously activate it
|
||||
based on the task description.
|
||||
|
||||
### Customizing Policies
|
||||
|
||||
@@ -137,7 +151,7 @@ Because user policies (Tier 2) have a higher base priority than built-in
|
||||
policies (Tier 1), you can override Plan Mode's default restrictions by creating
|
||||
a rule in your `~/.gemini/policies/` directory.
|
||||
|
||||
#### Example: Allow `git status` and `git diff` in Plan Mode
|
||||
#### Example: Allow git commands in Plan Mode
|
||||
|
||||
This rule allows you to check the repository status and see changes while in
|
||||
Plan Mode.
|
||||
@@ -153,10 +167,10 @@ priority = 100
|
||||
modes = ["plan"]
|
||||
```
|
||||
|
||||
#### Example: Enable research sub-agents in Plan Mode
|
||||
#### Example: Enable research subagents in Plan Mode
|
||||
|
||||
You can enable [experimental research sub-agents] like `codebase_investigator`
|
||||
to help gather architecture details during the planning phase.
|
||||
You can enable experimental research [subagents] like `codebase_investigator` to
|
||||
help gather architecture details during the planning phase.
|
||||
|
||||
`~/.gemini/policies/research-subagents.toml`
|
||||
|
||||
@@ -168,11 +182,51 @@ priority = 100
|
||||
modes = ["plan"]
|
||||
```
|
||||
|
||||
Tell the agent it can use these tools in your prompt, for example: _"You can
|
||||
Tell Gemini CLI 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].
|
||||
For more information on how the policy engine works, see the [policy engine]
|
||||
docs.
|
||||
|
||||
### Custom Plan Directory and Policies
|
||||
|
||||
By default, planning artifacts are stored in a managed temporary directory
|
||||
outside your project: `~/.gemini/tmp/<project>/<session-id>/plans/`.
|
||||
|
||||
You can configure a custom directory for plans in your `settings.json`. For
|
||||
example, to store plans in a `.gemini/plans` directory within your project:
|
||||
|
||||
```json
|
||||
{
|
||||
"general": {
|
||||
"plan": {
|
||||
"directory": ".gemini/plans"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To maintain the safety of Plan Mode, user-configured paths for the plans
|
||||
directory are restricted to the project root. This ensures that custom planning
|
||||
locations defined within a project's workspace cannot be used to escape and
|
||||
overwrite sensitive files elsewhere. Any user-configured directory must reside
|
||||
within the project boundary.
|
||||
|
||||
Using a custom directory requires updating your [policy engine] configurations
|
||||
to allow `write_file` and `replace` in that specific location. For example, to
|
||||
allow writing to the `.gemini/plans` directory within your project, create a
|
||||
policy file at `~/.gemini/policies/plan-custom-directory.toml`:
|
||||
|
||||
```toml
|
||||
[[rule]]
|
||||
toolName = ["write_file", "replace"]
|
||||
decision = "allow"
|
||||
priority = 100
|
||||
modes = ["plan"]
|
||||
# Adjust the pattern to match your custom directory.
|
||||
# This example matches any .md file in a .gemini/plans directory within the project.
|
||||
argsPattern = "\"file_path\":\"[^\"]+[\\\\/]+\\.gemini[\\\\/]+plans[\\\\/]+[\\w-]+\\.md\""
|
||||
```
|
||||
|
||||
[`list_directory`]: /docs/tools/file-system.md#1-list_directory-readfolder
|
||||
[`read_file`]: /docs/tools/file-system.md#2-read_file-readfile
|
||||
@@ -183,8 +237,9 @@ Guide].
|
||||
[`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
|
||||
[subagents]: /docs/core/subagents.md
|
||||
[policy engine]: /docs/reference/policy-engine.md
|
||||
[`enter_plan_mode`]: /docs/tools/planning.md#1-enter_plan_mode-enterplanmode
|
||||
[`exit_plan_mode`]: /docs/tools/planning.md#2-exit_plan_mode-exitplanmode
|
||||
[`ask_user`]: /docs/tools/ask-user.md
|
||||
[YOLO mode]: /docs/reference/configuration.md#command-line-arguments
|
||||
|
||||
+3
-3
@@ -167,6 +167,6 @@ gemini -s -p "run shell command: mount | grep workspace"
|
||||
|
||||
## Related documentation
|
||||
|
||||
- [Configuration](../get-started/configuration.md): Full configuration options.
|
||||
- [Commands](./commands.md): Available commands.
|
||||
- [Troubleshooting](../troubleshooting.md): General troubleshooting.
|
||||
- [Configuration](../reference/configuration.md): Full configuration options.
|
||||
- [Commands](../reference/commands.md): Available commands.
|
||||
- [Troubleshooting](../resources/troubleshooting.md): General troubleshooting.
|
||||
|
||||
+49
-46
@@ -22,16 +22,17 @@ they appear in the UI.
|
||||
|
||||
### General
|
||||
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ------------------------ | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- |
|
||||
| Vim Mode | `general.vimMode` | Enable Vim keybindings | `false` |
|
||||
| Default Approval Mode | `general.defaultApprovalMode` | The default approval mode for tool execution. 'default' prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is read-only mode. 'yolo' is not supported yet. | `"default"` |
|
||||
| Enable Auto Update | `general.enableAutoUpdate` | Enable automatic updates. | `true` |
|
||||
| Enable Notifications | `general.enableNotifications` | Enable run-event notifications for action-required prompts and session completion. Currently macOS only. | `false` |
|
||||
| Enable Prompt Completion | `general.enablePromptCompletion` | Enable AI-powered prompt completion suggestions while typing. | `false` |
|
||||
| Debug Keystroke Logging | `general.debugKeystrokeLogging` | Enable debug logging of keystrokes to the console. | `false` |
|
||||
| Enable Session Cleanup | `general.sessionRetention.enabled` | Enable automatic session cleanup | `false` |
|
||||
| Keep chat history | `general.sessionRetention.maxAge` | Automatically delete chats older than this time period (e.g., "30d", "7d", "24h", "1w") | `undefined` |
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ----------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- |
|
||||
| Vim Mode | `general.vimMode` | Enable Vim keybindings | `false` |
|
||||
| Default Approval Mode | `general.defaultApprovalMode` | The default approval mode for tool execution. 'default' prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is read-only mode. 'yolo' is not supported yet. | `"default"` |
|
||||
| Enable Auto Update | `general.enableAutoUpdate` | Enable automatic updates. | `true` |
|
||||
| Enable Notifications | `general.enableNotifications` | Enable run-event notifications for action-required prompts and session completion. Currently macOS only. | `false` |
|
||||
| Plan Directory | `general.plan.directory` | The directory where planning artifacts are stored. If not specified, defaults to the system temporary directory. | `undefined` |
|
||||
| Max Chat Model Attempts | `general.maxAttempts` | Maximum number of attempts for requests to the main chat model. Cannot exceed 10. | `10` |
|
||||
| Debug Keystroke Logging | `general.debugKeystrokeLogging` | Enable debug logging of keystrokes to the console. | `false` |
|
||||
| Enable Session Cleanup | `general.sessionRetention.enabled` | Enable automatic session cleanup | `false` |
|
||||
| Keep chat history | `general.sessionRetention.maxAge` | Automatically delete chats older than this time period (e.g., "30d", "7d", "24h", "1w") | `undefined` |
|
||||
|
||||
### Output
|
||||
|
||||
@@ -41,36 +42,36 @@ they appear in the UI.
|
||||
|
||||
### UI
|
||||
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ------------------------------------ | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||
| Auto Theme Switching | `ui.autoThemeSwitching` | Automatically switch between default light and dark themes based on terminal background color. | `true` |
|
||||
| Terminal Background Polling Interval | `ui.terminalBackgroundPollingInterval` | Interval in seconds to poll the terminal background color. | `60` |
|
||||
| Hide Window Title | `ui.hideWindowTitle` | Hide the window title bar | `false` |
|
||||
| Inline Thinking | `ui.inlineThinkingMode` | Display model thinking inline: off or full. | `"off"` |
|
||||
| Show Thoughts in Title | `ui.showStatusInTitle` | Show Gemini CLI model thoughts in the terminal window title during the working phase | `false` |
|
||||
| Dynamic Window Title | `ui.dynamicWindowTitle` | Update the terminal window title with current status icons (Ready: ◇, Action Required: ✋, Working: ✦) | `true` |
|
||||
| Show Home Directory Warning | `ui.showHomeDirectoryWarning` | Show a warning when running Gemini CLI in the home directory. | `true` |
|
||||
| Show Compatibility Warnings | `ui.showCompatibilityWarnings` | Show warnings about terminal or OS compatibility issues. | `true` |
|
||||
| Hide Tips | `ui.hideTips` | Hide helpful tips in the UI | `false` |
|
||||
| Show Shortcuts Hint | `ui.showShortcutsHint` | Show the "? for shortcuts" hint above the input. | `true` |
|
||||
| Hide Banner | `ui.hideBanner` | Hide the application banner | `false` |
|
||||
| Hide Context Summary | `ui.hideContextSummary` | Hide the context summary (GEMINI.md, MCP servers) above the input. | `false` |
|
||||
| Hide CWD | `ui.footer.hideCWD` | Hide the current working directory path in the footer. | `false` |
|
||||
| Hide Sandbox Status | `ui.footer.hideSandboxStatus` | Hide the sandbox status indicator in the footer. | `false` |
|
||||
| Hide Model Info | `ui.footer.hideModelInfo` | Hide the model name and context usage in the footer. | `false` |
|
||||
| Hide Context Window Percentage | `ui.footer.hideContextPercentage` | Hides the context window remaining percentage. | `true` |
|
||||
| Hide Footer | `ui.hideFooter` | Hide the footer from the UI | `false` |
|
||||
| Show Memory Usage | `ui.showMemoryUsage` | Display memory usage information in the UI | `false` |
|
||||
| Show Line Numbers | `ui.showLineNumbers` | Show line numbers in the chat. | `true` |
|
||||
| Show Citations | `ui.showCitations` | Show citations for generated text in the chat. | `false` |
|
||||
| Show Model Info In Chat | `ui.showModelInfoInChat` | Show the model name in the chat for each model turn. | `false` |
|
||||
| Show User Identity | `ui.showUserIdentity` | Show the logged-in user's identity (e.g. email) in the UI. | `true` |
|
||||
| Use Alternate Screen Buffer | `ui.useAlternateBuffer` | Use an alternate screen buffer for the UI, preserving shell history. | `false` |
|
||||
| Use Background Color | `ui.useBackgroundColor` | Whether to use background colors in the UI. | `true` |
|
||||
| Incremental Rendering | `ui.incrementalRendering` | Enable incremental rendering for the UI. This option will reduce flickering but may cause rendering artifacts. Only supported when useAlternateBuffer is enabled. | `true` |
|
||||
| Show Spinner | `ui.showSpinner` | Show the spinner during operations. | `true` |
|
||||
| Enable Loading Phrases | `ui.accessibility.enableLoadingPhrases` | Enable loading phrases during operations. | `true` |
|
||||
| Screen Reader Mode | `ui.accessibility.screenReader` | Render output in plain-text to be more screen reader accessible | `false` |
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ------------------------------------ | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
|
||||
| Auto Theme Switching | `ui.autoThemeSwitching` | Automatically switch between default light and dark themes based on terminal background color. | `true` |
|
||||
| Terminal Background Polling Interval | `ui.terminalBackgroundPollingInterval` | Interval in seconds to poll the terminal background color. | `60` |
|
||||
| Hide Window Title | `ui.hideWindowTitle` | Hide the window title bar | `false` |
|
||||
| Inline Thinking | `ui.inlineThinkingMode` | Display model thinking inline: off or full. | `"off"` |
|
||||
| Show Thoughts in Title | `ui.showStatusInTitle` | Show Gemini CLI model thoughts in the terminal window title during the working phase | `false` |
|
||||
| Dynamic Window Title | `ui.dynamicWindowTitle` | Update the terminal window title with current status icons (Ready: ◇, Action Required: ✋, Working: ✦) | `true` |
|
||||
| Show Home Directory Warning | `ui.showHomeDirectoryWarning` | Show a warning when running Gemini CLI in the home directory. | `true` |
|
||||
| Show Compatibility Warnings | `ui.showCompatibilityWarnings` | Show warnings about terminal or OS compatibility issues. | `true` |
|
||||
| Hide Tips | `ui.hideTips` | Hide helpful tips in the UI | `false` |
|
||||
| Show Shortcuts Hint | `ui.showShortcutsHint` | Show the "? for shortcuts" hint above the input. | `true` |
|
||||
| Hide Banner | `ui.hideBanner` | Hide the application banner | `false` |
|
||||
| Hide Context Summary | `ui.hideContextSummary` | Hide the context summary (GEMINI.md, MCP servers) above the input. | `false` |
|
||||
| Hide CWD | `ui.footer.hideCWD` | Hide the current working directory path in the footer. | `false` |
|
||||
| Hide Sandbox Status | `ui.footer.hideSandboxStatus` | Hide the sandbox status indicator in the footer. | `false` |
|
||||
| Hide Model Info | `ui.footer.hideModelInfo` | Hide the model name and context usage in the footer. | `false` |
|
||||
| Hide Context Window Percentage | `ui.footer.hideContextPercentage` | Hides the context window remaining percentage. | `true` |
|
||||
| Hide Footer | `ui.hideFooter` | Hide the footer from the UI | `false` |
|
||||
| Show Memory Usage | `ui.showMemoryUsage` | Display memory usage information in the UI | `false` |
|
||||
| Show Line Numbers | `ui.showLineNumbers` | Show line numbers in the chat. | `true` |
|
||||
| Show Citations | `ui.showCitations` | Show citations for generated text in the chat. | `false` |
|
||||
| Show Model Info In Chat | `ui.showModelInfoInChat` | Show the model name in the chat for each model turn. | `false` |
|
||||
| Show User Identity | `ui.showUserIdentity` | Show the logged-in user's identity (e.g. email) in the UI. | `true` |
|
||||
| Use Alternate Screen Buffer | `ui.useAlternateBuffer` | Use an alternate screen buffer for the UI, preserving shell history. | `false` |
|
||||
| Use Background Color | `ui.useBackgroundColor` | Whether to use background colors in the UI. | `true` |
|
||||
| Incremental Rendering | `ui.incrementalRendering` | Enable incremental rendering for the UI. This option will reduce flickering but may cause rendering artifacts. Only supported when useAlternateBuffer is enabled. | `true` |
|
||||
| Show Spinner | `ui.showSpinner` | Show the spinner during operations. | `true` |
|
||||
| Loading Phrases | `ui.loadingPhrases` | What to show while the model is working: tips, witty comments, both, or nothing. | `"tips"` |
|
||||
| Screen Reader Mode | `ui.accessibility.screenReader` | Render output in plain-text to be more screen reader accessible | `false` |
|
||||
|
||||
### IDE
|
||||
|
||||
@@ -128,12 +129,14 @@ they appear in the UI.
|
||||
|
||||
### Experimental
|
||||
|
||||
| UI Label | Setting | Description | Default |
|
||||
| -------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------- | ------- |
|
||||
| Enable Tool Output Masking | `experimental.toolOutputMasking.enabled` | Enables tool output masking to save tokens. | `true` |
|
||||
| Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 sequence for pasting instead of clipboardy (useful for remote sessions). | `false` |
|
||||
| Plan | `experimental.plan` | Enable planning features (Plan Mode and tools). | `false` |
|
||||
| Model Steering | `experimental.modelSteering` | Enable model steering (user hints) to guide the model during tool execution. | `false` |
|
||||
| UI Label | Setting | Description | Default |
|
||||
| -------------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||
| Enable Tool Output Masking | `experimental.toolOutputMasking.enabled` | Enables tool output masking to save tokens. | `true` |
|
||||
| Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 for pasting. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
|
||||
| Use OSC 52 Copy | `experimental.useOSC52Copy` | Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
|
||||
| Plan | `experimental.plan` | Enable planning features (Plan Mode and tools). | `false` |
|
||||
| Model Steering | `experimental.modelSteering` | Enable model steering (user hints) to guide the model during tool execution. | `false` |
|
||||
| Direct Web Fetch | `experimental.directWebFetch` | Enable web fetch behavior that bypasses LLM summarization. | `false` |
|
||||
|
||||
### Skills
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ Environment variables can be used to override the settings in the file.
|
||||
`true` or `1` will enable the feature. Any other value will disable it.
|
||||
|
||||
For detailed information about all configuration options, see the
|
||||
[Configuration guide](../get-started/configuration.md).
|
||||
[Configuration guide](../reference/configuration.md).
|
||||
|
||||
## Google Cloud telemetry
|
||||
|
||||
|
||||
+3
-3
@@ -41,8 +41,8 @@ can change the theme using the `/theme` command.
|
||||
### Theme persistence
|
||||
|
||||
Selected themes are saved in Gemini CLI's
|
||||
[configuration](../get-started/configuration.md) so your preference is
|
||||
remembered across sessions.
|
||||
[configuration](../reference/configuration.md) so your preference is remembered
|
||||
across sessions.
|
||||
|
||||
---
|
||||
|
||||
@@ -194,7 +194,7 @@ untrusted sources.
|
||||
- Or, set it as the default by adding `"theme": "MyCustomTheme"` to the `ui`
|
||||
object in your `settings.json`.
|
||||
- Custom themes can be set at the user, project, or system level, and follow the
|
||||
same [configuration precedence](../get-started/configuration.md) as other
|
||||
same [configuration precedence](../reference/configuration.md) as other
|
||||
settings.
|
||||
|
||||
### Themes from extensions
|
||||
|
||||
@@ -38,6 +38,37 @@ folder, a dialog will automatically appear, prompting you to make a choice:
|
||||
Your choice is saved in a central file (`~/.gemini/trustedFolders.json`), so you
|
||||
will only be asked once per folder.
|
||||
|
||||
## Understanding folder contents: The discovery phase
|
||||
|
||||
Before you make a choice, the Gemini CLI performs a **discovery phase** to scan
|
||||
the folder for potential configurations. This information is displayed in the
|
||||
trust dialog to help you make an informed decision.
|
||||
|
||||
The discovery UI lists the following categories of items found in the project:
|
||||
|
||||
- **Commands**: Custom `.toml` command definitions that add new functionality.
|
||||
- **MCP Servers**: Configured Model Context Protocol servers that the CLI will
|
||||
attempt to connect to.
|
||||
- **Hooks**: System or custom hooks that can intercept and modify CLI behavior.
|
||||
- **Skills**: Local agent skills that provide specialized capabilities.
|
||||
- **Setting overrides**: Any project-specific configurations that override your
|
||||
global user settings.
|
||||
|
||||
### Security warnings and errors
|
||||
|
||||
The trust dialog also highlights critical information that requires your
|
||||
attention:
|
||||
|
||||
- **Security Warnings**: The CLI will explicitly flag potentially dangerous
|
||||
settings, such as auto-approving certain tools or disabling the security
|
||||
sandbox.
|
||||
- **Discovery Errors**: If the CLI encounters issues while scanning the folder
|
||||
(e.g., a malformed `settings.json` file), these errors will be displayed
|
||||
prominently.
|
||||
|
||||
By reviewing these details, you can ensure that you only grant trust to projects
|
||||
that you know are safe.
|
||||
|
||||
## Why trust matters: The impact of an untrusted workspace
|
||||
|
||||
When a folder is **untrusted**, the Gemini CLI runs in a restricted "safe mode"
|
||||
|
||||
@@ -121,6 +121,6 @@ immediately. Force a reload with:
|
||||
|
||||
- Learn about [Session management](session-management.md) to see how short-term
|
||||
history works.
|
||||
- Explore the [Command reference](../../cli/commands.md) for more `/memory`
|
||||
options.
|
||||
- Explore the [Command reference](../../reference/commands.md) for more
|
||||
`/memory` options.
|
||||
- Read the technical spec for [Project context](../../cli/gemini-md.md).
|
||||
|
||||
@@ -101,5 +101,5 @@ This creates a new branch of history without losing your original work.
|
||||
- Learn about [Checkpointing](../../cli/checkpointing.md) to understand the
|
||||
underlying safety mechanism.
|
||||
- Explore [Task planning](task-planning.md) to keep complex sessions organized.
|
||||
- See the [Command reference](../../cli/commands.md) for all `/chat` and
|
||||
- See the [Command reference](../../reference/commands.md) for all `/chat` and
|
||||
`/resume` options.
|
||||
|
||||
+7
-7
@@ -9,11 +9,11 @@ requests sent from `packages/cli`. For a general overview of Gemini CLI, see the
|
||||
|
||||
- **[Sub-agents (experimental)](./subagents.md):** Learn how to create and use
|
||||
specialized sub-agents for complex tasks.
|
||||
- **[Core tools API](./tools-api.md):** Information on how tools are defined,
|
||||
registered, and used by the core.
|
||||
- **[Memory Import Processor](./memport.md):** Documentation for the modular
|
||||
GEMINI.md import feature using @file.md syntax.
|
||||
- **[Policy Engine](./policy-engine.md):** Use the Policy Engine for
|
||||
- **[Core tools API](../reference/tools-api.md):** Information on how tools are
|
||||
defined, registered, and used by the core.
|
||||
- **[Memory Import Processor](../reference/memport.md):** Documentation for the
|
||||
modular GEMINI.md import feature using @file.md syntax.
|
||||
- **[Policy Engine](../reference/policy-engine.md):** Use the Policy Engine for
|
||||
fine-grained control over tool execution.
|
||||
|
||||
## Role of the core
|
||||
@@ -92,8 +92,8 @@ This allows you to have global, project-level, and component-level context
|
||||
files, which are all combined to provide the model with the most relevant
|
||||
information.
|
||||
|
||||
You can use the [`/memory` command](../cli/commands.md) to `show`, `add`, and
|
||||
`refresh` the content of loaded `GEMINI.md` files.
|
||||
You can use the [`/memory` command](../reference/commands.md) to `show`, `add`,
|
||||
and `refresh` the content of loaded `GEMINI.md` files.
|
||||
|
||||
## Citations
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ the main agent's context or toolset.
|
||||
> ```
|
||||
>
|
||||
> **Warning:** Subagents currently operate in
|
||||
> ["YOLO mode"](../get-started/configuration.md#command-line-arguments), meaning
|
||||
> ["YOLO mode"](../reference/configuration.md#command-line-arguments), meaning
|
||||
> they may execute tools without individual user confirmation for each step.
|
||||
> Proceed with caution when defining agents with powerful tools like
|
||||
> `run_shell_command` or `write_file`.
|
||||
|
||||
@@ -130,7 +130,7 @@ The manifest file defines the extension's behavior and configuration.
|
||||
- `description`: A short summary shown in the extension gallery.
|
||||
- <a id="mcp-servers"></a>`mcpServers`: A map of Model Context Protocol (MCP)
|
||||
servers. Extension servers follow the same format as standard
|
||||
[CLI configuration](../get-started/configuration.md).
|
||||
[CLI configuration](../reference/configuration.md).
|
||||
- `contextFileName`: The name of the context file (defaults to `GEMINI.md`). Can
|
||||
also be an array of strings to load multiple context files.
|
||||
- `excludeTools`: An array of tools to block from the model. You can restrict
|
||||
|
||||
@@ -22,8 +22,8 @@ Select the authentication method that matches your situation in the table below:
|
||||
### What is my Google account type?
|
||||
|
||||
- **Individual Google accounts:** Includes all
|
||||
[free tier accounts](../quota-and-pricing/#free-usage) such as Gemini Code
|
||||
Assist for individuals, as well as paid subscriptions for
|
||||
[free tier accounts](../resources/quota-and-pricing.md#free-usage) such as
|
||||
Gemini Code Assist for individuals, as well as paid subscriptions for
|
||||
[Google AI Pro and Ultra](https://gemini.google/subscriptions/).
|
||||
|
||||
- **Organization accounts:** Accounts using paid licenses through an
|
||||
@@ -317,5 +317,5 @@ configure authentication using environment variables:
|
||||
Your authentication method affects your quotas, pricing, Terms of Service, and
|
||||
privacy notices. Review the following pages to learn more:
|
||||
|
||||
- [Gemini CLI: Quotas and Pricing](../quota-and-pricing.md).
|
||||
- [Gemini CLI: Terms of Service and Privacy Notice](../tos-privacy.md).
|
||||
- [Gemini CLI: Quotas and Pricing](../resources/quota-and-pricing.md).
|
||||
- [Gemini CLI: Terms of Service and Privacy Notice](../resources/tos-privacy.md).
|
||||
|
||||
@@ -2,6 +2,21 @@
|
||||
|
||||
Gemini 3 Pro and Gemini 3 Flash are available on Gemini CLI for all users!
|
||||
|
||||
> **Note:** Gemini 3.1 Pro Preview is rolling out. To determine whether you have
|
||||
> access to Gemini 3.1, use the `/model` command and select **Manual**. If you
|
||||
> have access, you will see `gemini-3.1-pro-preview`.
|
||||
>
|
||||
> If you have access to Gemini 3.1, it will be included in model routing when
|
||||
> you select **Auto (Gemini 3)**. You can also launch the Gemini 3.1 model
|
||||
> directly using the `-m` flag:
|
||||
>
|
||||
> ```
|
||||
> gemini -m gemini-3.1-pro-preview
|
||||
> ```
|
||||
>
|
||||
> Learn more about [models](../cli/model.md) and
|
||||
> [model routing](../cli/model-routing.md).
|
||||
|
||||
## How to get started with Gemini 3 on Gemini CLI
|
||||
|
||||
Get started by upgrading Gemini CLI to the latest version:
|
||||
@@ -12,9 +27,8 @@ npm install -g @google/gemini-cli@latest
|
||||
|
||||
After you’ve confirmed your version is 0.21.1 or later:
|
||||
|
||||
1. Use the `/settings` command in Gemini CLI.
|
||||
2. Toggle **Preview Features** to `true`.
|
||||
3. Run `/model` and select **Auto (Gemini 3)**.
|
||||
1. Run `/model`.
|
||||
2. Select **Auto (Gemini 3)**.
|
||||
|
||||
For more information, see [Gemini CLI model selection](../cli/model.md).
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ Gemini CLI offers several ways to configure its behavior, including environment
|
||||
variables, command-line arguments, and settings files.
|
||||
|
||||
To explore your configuration options, see
|
||||
[Gemini CLI Configuration](./configuration.md).
|
||||
[Gemini CLI Configuration](../reference/configuration.md).
|
||||
|
||||
## Use
|
||||
|
||||
@@ -64,6 +64,16 @@ and more.
|
||||
|
||||
To explore the power of Gemini CLI, see [Gemini CLI examples](./examples.md).
|
||||
|
||||
## Check usage and quota
|
||||
|
||||
You can check your current token usage and quota information using the
|
||||
`/stats model` command. This command provides a snapshot of your current
|
||||
session's token usage, as well as your overall quota and usage for the supported
|
||||
models.
|
||||
|
||||
For more information on the `/stats` command and its subcommands, see the
|
||||
[Command Reference](../../reference/commands.md#stats).
|
||||
|
||||
## Next steps
|
||||
|
||||
- Follow the [File management](../cli/tutorials/file-management.md) guide to
|
||||
|
||||
@@ -420,7 +420,7 @@ When you open a project with hooks defined in `.gemini/settings.json`:
|
||||
|
||||
Hooks inherit the environment of the Gemini CLI process, which may include
|
||||
sensitive API keys. Gemini CLI provides a
|
||||
[redaction system](/docs/get-started/configuration#environment-variable-redaction)
|
||||
[redaction system](/docs/reference/configuration.md#environment-variable-redaction)
|
||||
that automatically filters variables matching sensitive patterns (e.g., `KEY`,
|
||||
`TOKEN`).
|
||||
|
||||
|
||||
+19
-46
@@ -48,41 +48,8 @@ User-focused guides and tutorials for daily development workflows.
|
||||
|
||||
## Features
|
||||
|
||||
Technical reference documentation for each capability of Gemini CLI.
|
||||
Technical documentation for each capability of Gemini CLI.
|
||||
|
||||
- **[/about](./cli/commands.md#about):** About Gemini CLI.
|
||||
- **[/auth](./get-started/authentication.md):** Authentication.
|
||||
- **[/bug](./cli/commands.md#bug):** Report a bug.
|
||||
- **[/chat](./cli/commands.md#chat):** Chat history.
|
||||
- **[/clear](./cli/commands.md#clear):** Clear screen.
|
||||
- **[/compress](./cli/commands.md#compress):** Compress context.
|
||||
- **[/copy](./cli/commands.md#copy):** Copy output.
|
||||
- **[/directory](./cli/commands.md#directory-or-dir):** Manage workspace.
|
||||
- **[/docs](./cli/commands.md#docs):** Open documentation.
|
||||
- **[/editor](./cli/commands.md#editor):** Select editor.
|
||||
- **[/extensions](./extensions/index.md):** Manage extensions.
|
||||
- **[/help](./cli/commands.md#help-or):** Show help.
|
||||
- **[/hooks](./hooks/index.md):** Hooks.
|
||||
- **[/ide](./ide-integration/index.md):** IDE integration.
|
||||
- **[/init](./cli/commands.md#init):** Initialize context.
|
||||
- **[/mcp](./tools/mcp-server.md):** MCP servers.
|
||||
- **[/memory](./cli/commands.md#memory):** Manage memory.
|
||||
- **[/model](./cli/model.md):** Model selection.
|
||||
- **[/policies](./cli/commands.md#policies):** Manage policies.
|
||||
- **[/privacy](./cli/commands.md#privacy):** Privacy notice.
|
||||
- **[/quit](./cli/commands.md#quit-or-exit):** Exit CLI.
|
||||
- **[/restore](./cli/checkpointing.md):** Restore files.
|
||||
- **[/resume](./cli/commands.md#resume):** Resume session.
|
||||
- **[/rewind](./cli/rewind.md):** Rewind.
|
||||
- **[/settings](./cli/settings.md):** Settings.
|
||||
- **[/setup-github](./cli/commands.md#setup-github):** GitHub setup.
|
||||
- **[/shells](./cli/commands.md#shells-or-bashes):** Manage processes.
|
||||
- **[/skills](./cli/skills.md):** Agent skills.
|
||||
- **[/stats](./cli/commands.md#stats):** Session statistics.
|
||||
- **[/terminal-setup](./cli/commands.md#terminal-setup):** Terminal keybindings.
|
||||
- **[/theme](./cli/themes.md):** Themes.
|
||||
- **[/tools](./cli/commands.md#tools):** List tools.
|
||||
- **[/vim](./cli/commands.md#vim):** Vim mode.
|
||||
- **[Activate skill (tool)](./tools/activate-skill.md):** Internal mechanism for
|
||||
loading expert procedures.
|
||||
- **[Ask user (tool)](./tools/ask-user.md):** Internal dialog system for
|
||||
@@ -97,12 +64,12 @@ Technical reference documentation for each capability of Gemini CLI.
|
||||
- **[Model routing](./cli/model-routing.md):** Automatic fallback resilience.
|
||||
- **[Plan mode 🧪](./cli/plan-mode.md):** Use a safe, read-only mode for
|
||||
planning complex changes.
|
||||
- **[Subagents 🧪](./core/subagents.md):** Using specialized agents for specific
|
||||
tasks.
|
||||
- **[Remote subagents 🧪](./core/remote-agents.md):** Connecting to and using
|
||||
remote agents.
|
||||
- **[Sandboxing](./cli/sandbox.md):** Isolate tool execution.
|
||||
- **[Shell (tool)](./tools/shell.md):** Detailed system execution parameters.
|
||||
- **[Subagents 🧪](./core/subagents.md):** Using specialized agents for specific
|
||||
tasks.
|
||||
- **[Telemetry](./cli/telemetry.md):** Usage and performance metric details.
|
||||
- **[Todo (tool)](./tools/todos.md):** Progress tracking specification.
|
||||
- **[Token caching](./cli/token-caching.md):** Performance optimization.
|
||||
@@ -134,23 +101,29 @@ Settings and customization options for Gemini CLI.
|
||||
|
||||
Deep technical documentation and API specifications.
|
||||
|
||||
- **[Command reference](./cli/commands.md):** Detailed slash command guide.
|
||||
- **[Configuration reference](./get-started/configuration.md):** Settings and
|
||||
- **[Command reference](./reference/commands.md):** Detailed slash command
|
||||
guide.
|
||||
- **[Configuration reference](./reference/configuration.md):** Settings and
|
||||
environment variables.
|
||||
- **[Keyboard shortcuts](./cli/keyboard-shortcuts.md):** Productivity tips.
|
||||
- **[Memory import processor](./core/memport.md):** How Gemini CLI processes
|
||||
memory from various sources.
|
||||
- **[Policy engine](./core/policy-engine.md):** Fine-grained execution control.
|
||||
- **[Tools API](./core/tools-api.md):** The API for defining and using tools.
|
||||
- **[Keyboard shortcuts](./reference/keyboard-shortcuts.md):** Productivity
|
||||
tips.
|
||||
- **[Memory import processor](./reference/memport.md):** How Gemini CLI
|
||||
processes memory from various sources.
|
||||
- **[Policy engine](./reference/policy-engine.md):** Fine-grained execution
|
||||
control.
|
||||
- **[Tools API](./reference/tools-api.md):** The API for defining and using
|
||||
tools.
|
||||
|
||||
## Resources
|
||||
|
||||
Support, release history, and legal information.
|
||||
|
||||
- **[FAQ](./faq.md):** Answers to frequently asked questions.
|
||||
- **[FAQ](./resources/faq.md):** Answers to frequently asked questions.
|
||||
- **[Changelogs](./changelogs/index.md):** Highlights and notable changes.
|
||||
- **[Quota and pricing](./quota-and-pricing.md):** Limits and billing details.
|
||||
- **[Terms and privacy](./tos-privacy.md):** Official notices and terms.
|
||||
- **[Quota and pricing](./resources/quota-and-pricing.md):** Limits and billing
|
||||
details.
|
||||
- **[Terms and privacy](./resources/tos-privacy.md):** Official notices and
|
||||
terms.
|
||||
|
||||
## Development
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"/docs/architecture": "/docs/cli/index",
|
||||
"/docs/cli/commands": "/docs/reference/commands",
|
||||
"/docs/cli": "/docs",
|
||||
"/docs/cli/index": "/docs",
|
||||
"/docs/cli/keyboard-shortcuts": "/docs/reference/keyboard-shortcuts",
|
||||
"/docs/cli/uninstall": "/docs/resources/uninstall",
|
||||
"/docs/core/concepts": "/docs",
|
||||
"/docs/core/memport": "/docs/reference/memport",
|
||||
"/docs/core/policy-engine": "/docs/reference/policy-engine",
|
||||
"/docs/core/tools-api": "/docs/reference/tools-api",
|
||||
"/docs/faq": "/docs/resources/faq",
|
||||
"/docs/get-started/configuration": "/docs/reference/configuration",
|
||||
"/docs/get-started/configuration-v1": "/docs/reference/configuration",
|
||||
"/docs/index": "/docs",
|
||||
"/docs/quota-and-pricing": "/docs/resources/quota-and-pricing",
|
||||
"/docs/tos-privacy": "/docs/resources/tos-privacy",
|
||||
"/docs/troubleshooting": "/docs/resources/troubleshooting"
|
||||
}
|
||||
@@ -32,6 +32,8 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
conversation state interactively, or resuming a previous state from a later
|
||||
session.
|
||||
- **Sub-commands:**
|
||||
- **`debug`**
|
||||
- **Description:** Export the most recent API request as a JSON payload.
|
||||
- **`delete <tag>`**
|
||||
- **Description:** Deletes a saved conversation checkpoint.
|
||||
- **`list`**
|
||||
@@ -128,8 +130,29 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
|
||||
### `/extensions`
|
||||
|
||||
- **Description:** Lists all active extensions in the current Gemini CLI
|
||||
session. See [Gemini CLI Extensions](../extensions/index.md).
|
||||
- **Description:** Manage extensions. See
|
||||
[Gemini CLI Extensions](../extensions/index.md).
|
||||
- **Sub-commands:**
|
||||
- **`config`**:
|
||||
- **Description:** Configure extension settings.
|
||||
- **`disable`**:
|
||||
- **Description:** Disable an extension.
|
||||
- **`enable`**:
|
||||
- **Description:** Enable an extension.
|
||||
- **`explore`**:
|
||||
- **Description:** Open extensions page in your browser.
|
||||
- **`install`**:
|
||||
- **Description:** Install an extension from a git repo or local path.
|
||||
- **`link`**:
|
||||
- **Description:** Link an extension from a local path.
|
||||
- **`list`**:
|
||||
- **Description:** List active extensions.
|
||||
- **`restart`**:
|
||||
- **Description:** Restart all extensions.
|
||||
- **`uninstall`**:
|
||||
- **Description:** Uninstall an extension.
|
||||
- **`update`**:
|
||||
- **Description:** Update extensions. Usage: update <extension-names>|--all
|
||||
|
||||
### `/help` (or `/?`)
|
||||
|
||||
@@ -184,6 +207,10 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
servers that support OAuth authentication.
|
||||
- **`desc`**
|
||||
- **Description:** List configured MCP servers and tools with descriptions.
|
||||
- **`disable`**
|
||||
- **Description:** Disable an MCP server.
|
||||
- **`enable`**
|
||||
- **Description:** Enable a disabled MCP server.
|
||||
- **`list`** or **`ls`**:
|
||||
- **Description:** List configured MCP servers and tools. This is the
|
||||
default action if no subcommand is specified.
|
||||
@@ -217,11 +244,25 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
model.
|
||||
- **Note:** For more details on how `GEMINI.md` files contribute to
|
||||
hierarchical memory, see the
|
||||
[CLI Configuration documentation](../get-started/configuration.md).
|
||||
[CLI Configuration documentation](./configuration.md).
|
||||
|
||||
### `/model`
|
||||
|
||||
- **Description:** Opens a dialog to choose your Gemini model.
|
||||
- **Description:** Manage model configuration.
|
||||
- **Sub-commands:**
|
||||
- **`manage`**:
|
||||
- **Description:** Opens a dialog to configure the model.
|
||||
- **`set`**:
|
||||
- **Description:** Set the model to use.
|
||||
- **Usage:** `/model set <model-name> [--persist]`
|
||||
|
||||
### `/permissions`
|
||||
|
||||
- **Description:** Manage folder trust settings and other permissions.
|
||||
- **Sub-commands:**
|
||||
- **`trust`**:
|
||||
- **Description:** Manage folder trust settings.
|
||||
- **Usage:** `/permissions trust [<directory-path>]`
|
||||
|
||||
### `/plan`
|
||||
|
||||
@@ -254,7 +295,7 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
checkpoints to restore from.
|
||||
- **Usage:** `/restore [tool_call_id]`
|
||||
- **Note:** Only available if checkpointing is configured via
|
||||
[settings](../get-started/configuration.md). See
|
||||
[settings](./configuration.md). See
|
||||
[Checkpointing documentation](../cli/checkpointing.md) for more details.
|
||||
|
||||
### `/rewind`
|
||||
@@ -293,7 +334,8 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
settings that control the behavior and appearance of Gemini CLI. It is
|
||||
equivalent to manually editing the `.gemini/settings.json` file, but with
|
||||
validation and guidance to prevent errors. See the
|
||||
[settings documentation](./settings.md) for a full list of available settings.
|
||||
[settings documentation](../cli/settings.md) for a full list of available
|
||||
settings.
|
||||
- **Usage:** Simply run `/settings` and the editor will open. You can then
|
||||
browse or search for specific settings, view their current values, and modify
|
||||
them as desired. Changes to some settings are applied immediately, while
|
||||
@@ -330,10 +372,16 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
### `/stats`
|
||||
|
||||
- **Description:** Display detailed statistics for the current Gemini CLI
|
||||
session, including token usage, cached token savings (when available), and
|
||||
session duration. Note: Cached token information is only displayed when cached
|
||||
tokens are being used, which occurs with API key authentication but not with
|
||||
OAuth authentication at this time.
|
||||
session.
|
||||
- **Sub-commands:**
|
||||
- **`session`**:
|
||||
- **Description:** Show session-specific usage statistics, including
|
||||
duration, tool calls, and performance metrics. This is the default view.
|
||||
- **`model`**:
|
||||
- **Description:** Show model-specific usage statistics, including token
|
||||
counts and quota information.
|
||||
- **`tools`**:
|
||||
- **Description:** Show tool-specific usage statistics.
|
||||
|
||||
### `/terminal-setup`
|
||||
|
||||
@@ -380,7 +428,8 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
|
||||
Custom commands allow you to create personalized shortcuts for your most-used
|
||||
prompts. For detailed instructions on how to create, manage, and use them,
|
||||
please see the dedicated [Custom Commands documentation](./custom-commands.md).
|
||||
please see the dedicated
|
||||
[Custom Commands documentation](../cli/custom-commands.md).
|
||||
|
||||
## Input prompt shortcuts
|
||||
|
||||
@@ -131,10 +131,10 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`general.enablePromptCompletion`** (boolean):
|
||||
- **Description:** Enable AI-powered prompt completion suggestions while
|
||||
typing.
|
||||
- **Default:** `false`
|
||||
- **`general.plan.directory`** (string):
|
||||
- **Description:** The directory where planning artifacts are stored. If not
|
||||
specified, defaults to the system temporary directory.
|
||||
- **Default:** `undefined`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`general.retryFetchErrors`** (boolean):
|
||||
@@ -142,6 +142,11 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
request" errors.
|
||||
- **Default:** `false`
|
||||
|
||||
- **`general.maxAttempts`** (number):
|
||||
- **Description:** Maximum number of attempts for requests to the main chat
|
||||
model. Cannot exceed 10.
|
||||
- **Default:** `10`
|
||||
|
||||
- **`general.debugKeystrokeLogging`** (boolean):
|
||||
- **Description:** Enable debug logging of keystrokes to the console.
|
||||
- **Default:** `false`
|
||||
@@ -305,13 +310,20 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Description:** Show the spinner during operations.
|
||||
- **Default:** `true`
|
||||
|
||||
- **`ui.loadingPhrases`** (enum):
|
||||
- **Description:** What to show while the model is working: tips, witty
|
||||
comments, both, or nothing.
|
||||
- **Default:** `"tips"`
|
||||
- **Values:** `"tips"`, `"witty"`, `"all"`, `"off"`
|
||||
|
||||
- **`ui.customWittyPhrases`** (array):
|
||||
- **Description:** Custom witty phrases to display during loading. When
|
||||
provided, the CLI cycles through these instead of the defaults.
|
||||
- **Default:** `[]`
|
||||
|
||||
- **`ui.accessibility.enableLoadingPhrases`** (boolean):
|
||||
- **Description:** Enable loading phrases during operations.
|
||||
- **Description:** @deprecated Use ui.loadingPhrases instead. Enable loading
|
||||
phrases during operations.
|
||||
- **Default:** `true`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
@@ -941,8 +953,15 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.useOSC52Paste`** (boolean):
|
||||
- **Description:** Use OSC 52 sequence for pasting instead of clipboardy
|
||||
(useful for remote sessions).
|
||||
- **Description:** Use OSC 52 for pasting. This may be more robust than the
|
||||
default system when using remote terminal sessions (if your terminal is
|
||||
configured to allow it).
|
||||
- **Default:** `false`
|
||||
|
||||
- **`experimental.useOSC52Copy`** (boolean):
|
||||
- **Description:** Use OSC 52 for copying. This may be more robust than the
|
||||
default system when using remote terminal sessions (if your terminal is
|
||||
configured to allow it).
|
||||
- **Default:** `false`
|
||||
|
||||
- **`experimental.plan`** (boolean):
|
||||
@@ -955,6 +974,11 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
during tool execution.
|
||||
- **Default:** `false`
|
||||
|
||||
- **`experimental.directWebFetch`** (boolean):
|
||||
- **Description:** Enable web fetch behavior that bypasses LLM summarization.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
#### `skills`
|
||||
|
||||
- **`skills.enabled`** (boolean):
|
||||
@@ -1214,8 +1238,8 @@ within your user's home folder.
|
||||
Environment variables are a common way to configure applications, especially for
|
||||
sensitive information like API keys or for settings that might change between
|
||||
environments. For authentication setup, see the
|
||||
[Authentication documentation](./authentication.md) which covers all available
|
||||
authentication methods.
|
||||
[Authentication documentation](../get-started/authentication.md) which covers
|
||||
all available authentication methods.
|
||||
|
||||
The CLI automatically loads environment variables from an `.env` file. The
|
||||
loading order is:
|
||||
@@ -1234,7 +1258,8 @@ the `advanced.excludedEnvVars` setting in your `settings.json` file.
|
||||
|
||||
- **`GEMINI_API_KEY`**:
|
||||
- Your API key for the Gemini API.
|
||||
- One of several available [authentication methods](./authentication.md).
|
||||
- One of several available
|
||||
[authentication methods](../get-started/authentication.md).
|
||||
- Set this in your shell profile (e.g., `~/.bashrc`, `~/.zshrc`) or an `.env`
|
||||
file.
|
||||
- **`GEMINI_MODEL`**:
|
||||
@@ -1580,15 +1605,15 @@ conventions and context.
|
||||
about the active instructional context.
|
||||
- **Importing content:** You can modularize your context files by importing
|
||||
other Markdown files using the `@path/to/file.md` syntax. For more details,
|
||||
see the [Memory Import Processor documentation](../core/memport.md).
|
||||
see the [Memory Import Processor documentation](./memport.md).
|
||||
- **Commands for memory management:**
|
||||
- Use `/memory refresh` to force a re-scan and reload of all context files
|
||||
from all configured locations. This updates the AI's instructional context.
|
||||
- Use `/memory show` to display the combined instructional context currently
|
||||
loaded, allowing you to verify the hierarchy and content being used by the
|
||||
AI.
|
||||
- See the [Commands documentation](../cli/commands.md#memory) for full details
|
||||
on the `/memory` command and its sub-commands (`show` and `refresh`).
|
||||
- See the [Commands documentation](./commands.md#memory) for full details on
|
||||
the `/memory` command and its sub-commands (`show` and `refresh`).
|
||||
|
||||
By understanding and utilizing these configuration layers and the hierarchical
|
||||
nature of context files, you can effectively manage the AI's memory and tailor
|
||||
@@ -64,9 +64,11 @@ primary conditions are the tool's name and its arguments.
|
||||
|
||||
The `toolName` in the rule must match the name of the tool being called.
|
||||
|
||||
- **Wildcards**: For Model-hosting-protocol (MCP) servers, you can use a
|
||||
wildcard. A `toolName` of `my-server__*` will match any tool from the
|
||||
`my-server` MCP.
|
||||
- **Wildcards**: You can use wildcards to match multiple tools.
|
||||
- `*`: Matches **any tool** (built-in or MCP).
|
||||
- `server__*`: Matches any tool from a specific MCP server.
|
||||
- `*__toolName`: Matches a specific tool name across **all** MCP servers.
|
||||
- `*__*`: Matches **any tool from any MCP server**.
|
||||
|
||||
#### Arguments pattern
|
||||
|
||||
@@ -92,11 +94,12 @@ rule with the highest priority wins**.
|
||||
To provide a clear hierarchy, policies are organized into three tiers. Each tier
|
||||
has a designated number that forms the base of the final priority calculation.
|
||||
|
||||
| Tier | Base | Description |
|
||||
| :------ | :--- | :------------------------------------------------------------------------- |
|
||||
| Default | 1 | Built-in policies that ship with the Gemini CLI. |
|
||||
| User | 2 | Custom policies defined by the user. |
|
||||
| Admin | 3 | Policies managed by an administrator (e.g., in an enterprise environment). |
|
||||
| Tier | Base | Description |
|
||||
| :-------- | :--- | :------------------------------------------------------------------------- |
|
||||
| Default | 1 | Built-in policies that ship with the Gemini CLI. |
|
||||
| Workspace | 2 | Policies defined in the current workspace's configuration directory. |
|
||||
| User | 3 | Custom policies defined by the user. |
|
||||
| Admin | 4 | Policies managed by an administrator (e.g., in an enterprise environment). |
|
||||
|
||||
Within a TOML policy file, you assign a priority value from **0 to 999**. The
|
||||
engine transforms this into a final priority using the following formula:
|
||||
@@ -105,15 +108,17 @@ engine transforms this into a final priority using the following formula:
|
||||
|
||||
This system guarantees that:
|
||||
|
||||
- Admin policies always override User and Default policies.
|
||||
- User policies always override Default policies.
|
||||
- Admin policies always override User, Workspace, and Default policies.
|
||||
- User policies override Workspace and Default policies.
|
||||
- Workspace policies override Default policies.
|
||||
- You can still order rules within a single tier with fine-grained control.
|
||||
|
||||
For example:
|
||||
|
||||
- A `priority: 50` rule in a Default policy file becomes `1.050`.
|
||||
- A `priority: 100` rule in a User policy file becomes `2.100`.
|
||||
- A `priority: 20` rule in an Admin policy file becomes `3.020`.
|
||||
- A `priority: 10` rule in a Workspace policy policy file becomes `2.010`.
|
||||
- A `priority: 100` rule in a User policy file becomes `3.100`.
|
||||
- A `priority: 20` rule in an Admin policy file becomes `4.020`.
|
||||
|
||||
### Approval modes
|
||||
|
||||
@@ -141,9 +146,9 @@ A rule matches a tool call if all of its conditions are met:
|
||||
|
||||
1. **Tool name**: The `toolName` in the rule must match the name of the tool
|
||||
being called.
|
||||
- **Wildcards**: For Model-hosting-protocol (MCP) servers, you can use a
|
||||
wildcard. A `toolName` of `my-server__*` will match any tool from the
|
||||
`my-server` MCP.
|
||||
- **Wildcards**: You can use wildcards like `*`, `server__*`, or
|
||||
`*__toolName` to match multiple tools. See [Tool Name](#tool-name) for
|
||||
details.
|
||||
2. **Arguments pattern**: If `argsPattern` is specified, the tool's arguments
|
||||
are converted to a stable JSON string, which is then tested against the
|
||||
provided regular expression. If the arguments don't match the pattern, the
|
||||
@@ -156,10 +161,11 @@ User, and (if configured) Admin directories.
|
||||
|
||||
### Policy locations
|
||||
|
||||
| Tier | Type | Location |
|
||||
| :-------- | :----- | :-------------------------- |
|
||||
| **User** | Custom | `~/.gemini/policies/*.toml` |
|
||||
| **Admin** | System | _See below (OS specific)_ |
|
||||
| Tier | Type | Location |
|
||||
| :------------ | :----- | :---------------------------------------- |
|
||||
| **User** | Custom | `~/.gemini/policies/*.toml` |
|
||||
| **Workspace** | Custom | `$WORKSPACE_ROOT/.gemini/policies/*.toml` |
|
||||
| **Admin** | System | _See below (OS specific)_ |
|
||||
|
||||
#### System-wide policies (Admin)
|
||||
|
||||
@@ -268,13 +274,12 @@ priority = 100
|
||||
|
||||
### Special syntax for MCP tools
|
||||
|
||||
You can create rules that target tools from Model-hosting-protocol (MCP) servers
|
||||
using the `mcpName` field or a wildcard pattern.
|
||||
You can create rules that target tools from Model Context Protocol (MCP) servers
|
||||
using the `mcpName` field or composite wildcard patterns.
|
||||
|
||||
**1. Using `mcpName`**
|
||||
**1. Targeting a specific tool on a server**
|
||||
|
||||
To target a specific tool from a specific server, combine `mcpName` and
|
||||
`toolName`.
|
||||
Combine `mcpName` and `toolName` to target a single operation.
|
||||
|
||||
```toml
|
||||
# Allows the `search` tool on the `my-jira-server` MCP
|
||||
@@ -285,10 +290,10 @@ decision = "allow"
|
||||
priority = 200
|
||||
```
|
||||
|
||||
**2. Using a wildcard**
|
||||
**2. Targeting all tools on a specific server**
|
||||
|
||||
To create a rule that applies to _all_ tools on a specific MCP server, specify
|
||||
only the `mcpName`.
|
||||
Specify only the `mcpName` to apply a rule to every tool provided by that
|
||||
server.
|
||||
|
||||
```toml
|
||||
# Denies all tools from the `untrusted-server` MCP
|
||||
@@ -299,6 +304,33 @@ priority = 500
|
||||
deny_message = "This server is not trusted by the admin."
|
||||
```
|
||||
|
||||
**3. Targeting all MCP servers**
|
||||
|
||||
Use `mcpName = "*"` to create a rule that applies to **all** tools from **any**
|
||||
registered MCP server. This is useful for setting category-wide defaults.
|
||||
|
||||
```toml
|
||||
# Ask user for any tool call from any MCP server
|
||||
[[rule]]
|
||||
mcpName = "*"
|
||||
decision = "ask_user"
|
||||
priority = 10
|
||||
```
|
||||
|
||||
**4. Targeting a tool name across all servers**
|
||||
|
||||
Use `mcpName = "*"` with a specific `toolName` to target that operation
|
||||
regardless of which server provides it.
|
||||
|
||||
```toml
|
||||
# Allow the `search` tool across all connected MCP servers
|
||||
[[rule]]
|
||||
mcpName = "*"
|
||||
toolName = "search"
|
||||
decision = "allow"
|
||||
priority = 50
|
||||
```
|
||||
|
||||
## Default policies
|
||||
|
||||
The Gemini CLI ships with a set of default policies to provide a safe
|
||||
@@ -104,7 +104,7 @@ The Gemini CLI configuration is stored in two `settings.json` files:
|
||||
1. In your home directory: `~/.gemini/settings.json`.
|
||||
2. In your project's root directory: `./.gemini/settings.json`.
|
||||
|
||||
Refer to [Gemini CLI Configuration](./get-started/configuration.md) for more
|
||||
Refer to [Gemini CLI Configuration](../reference/configuration.md) for more
|
||||
details.
|
||||
|
||||
## Google AI Pro/Ultra and subscription FAQs
|
||||
@@ -135,6 +135,18 @@ Flow video editor). These plans do not apply to the API usage which powers the
|
||||
Gemini CLI. Supporting these plans is under active consideration for future
|
||||
support.
|
||||
|
||||
## Check usage and quota
|
||||
|
||||
You can check your current token usage and quota information using the
|
||||
`/stats model` command. This command provides a snapshot of your current
|
||||
session's token usage, as well as your overall quota and usage for the supported
|
||||
models.
|
||||
|
||||
For more information on the `/stats` command and its subcommands, see the
|
||||
[Command Reference](../../reference/commands.md#stats).
|
||||
|
||||
A summary of model usage is also presented on exit at the end of a session.
|
||||
|
||||
## Tips to avoid high costs
|
||||
|
||||
When using a Pay as you Go API key, be mindful of your usage to avoid unexpected
|
||||
@@ -151,8 +163,3 @@ costs.
|
||||
models directly.
|
||||
- Vertex AI: This is the enterprise-grade platform for building, deploying, and
|
||||
managing Gemini models with specific security and control requirements.
|
||||
|
||||
## Understanding your usage
|
||||
|
||||
A summary of model usage is available through the `/stats` command and presented
|
||||
on exit at the end of a session.
|
||||
@@ -10,8 +10,8 @@ and Privacy Notices applicable to those services apply to such access and use.
|
||||
Your Gemini CLI Usage Statistics are handled in accordance with Google's Privacy
|
||||
Policy.
|
||||
|
||||
**Note:** See [quotas and pricing](/docs/quota-and-pricing.md) for the quota and
|
||||
pricing details that apply to your usage of the Gemini CLI.
|
||||
**Note:** See [quotas and pricing](/docs/resources/quota-and-pricing.md) for the
|
||||
quota and pricing details that apply to your usage of the Gemini CLI.
|
||||
|
||||
## Supported authentication methods
|
||||
|
||||
@@ -93,4 +93,4 @@ backend, these Terms of Service and Privacy Notice documents apply:
|
||||
|
||||
You may opt-out from sending Gemini CLI Usage Statistics to Google by following
|
||||
the instructions available here:
|
||||
[Usage Statistics Configuration](https://github.com/google-gemini/gemini-cli/blob/main/docs/get-started/configuration.md#usage-statistics).
|
||||
[Usage Statistics Configuration](https://github.com/google-gemini/gemini-cli/blob/main/docs/reference/configuration.md#usage-statistics).
|
||||
@@ -93,7 +93,7 @@ topics on:
|
||||
- **Cause:** When sandboxing is enabled, Gemini CLI may attempt operations
|
||||
that are restricted by your sandbox configuration, such as writing outside
|
||||
the project directory or system temp directory.
|
||||
- **Solution:** Refer to the [Configuration: Sandboxing](./cli/sandbox.md)
|
||||
- **Solution:** Refer to the [Configuration: Sandboxing](../cli/sandbox.md)
|
||||
documentation for more information, including how to customize your sandbox
|
||||
configuration.
|
||||
|
||||
+200
-160
@@ -1,193 +1,233 @@
|
||||
[
|
||||
{
|
||||
"label": "Get started",
|
||||
"items": [
|
||||
{ "label": "Overview", "slug": "docs" },
|
||||
{ "label": "Quickstart", "slug": "docs/get-started" },
|
||||
{ "label": "Installation", "slug": "docs/get-started/installation" },
|
||||
{ "label": "Authentication", "slug": "docs/get-started/authentication" },
|
||||
{ "label": "Examples", "slug": "docs/get-started/examples" },
|
||||
{ "label": "CLI cheatsheet", "slug": "docs/cli/cli-reference" },
|
||||
{ "label": "Gemini 3 on Gemini CLI", "slug": "docs/get-started/gemini-3" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Use Gemini CLI",
|
||||
"label": "docs_tab",
|
||||
"items": [
|
||||
{
|
||||
"label": "File management",
|
||||
"slug": "docs/cli/tutorials/file-management"
|
||||
"label": "Get started",
|
||||
"items": [
|
||||
{ "label": "Overview", "slug": "docs" },
|
||||
{ "label": "Quickstart", "slug": "docs/get-started" },
|
||||
{ "label": "Installation", "slug": "docs/get-started/installation" },
|
||||
{
|
||||
"label": "Authentication",
|
||||
"slug": "docs/get-started/authentication"
|
||||
},
|
||||
{ "label": "Examples", "slug": "docs/get-started/examples" },
|
||||
{ "label": "CLI cheatsheet", "slug": "docs/cli/cli-reference" },
|
||||
{
|
||||
"label": "Gemini 3 on Gemini CLI",
|
||||
"slug": "docs/get-started/gemini-3"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Get started with Agent skills",
|
||||
"slug": "docs/cli/tutorials/skills-getting-started"
|
||||
"label": "Use Gemini CLI",
|
||||
"items": [
|
||||
{
|
||||
"label": "File management",
|
||||
"slug": "docs/cli/tutorials/file-management"
|
||||
},
|
||||
{
|
||||
"label": "Get started with Agent skills",
|
||||
"slug": "docs/cli/tutorials/skills-getting-started"
|
||||
},
|
||||
{
|
||||
"label": "Manage context and memory",
|
||||
"slug": "docs/cli/tutorials/memory-management"
|
||||
},
|
||||
{
|
||||
"label": "Execute shell commands",
|
||||
"slug": "docs/cli/tutorials/shell-commands"
|
||||
},
|
||||
{
|
||||
"label": "Manage sessions and history",
|
||||
"slug": "docs/cli/tutorials/session-management"
|
||||
},
|
||||
{
|
||||
"label": "Plan tasks with todos",
|
||||
"slug": "docs/cli/tutorials/task-planning"
|
||||
},
|
||||
{
|
||||
"label": "Web search and fetch",
|
||||
"slug": "docs/cli/tutorials/web-tools"
|
||||
},
|
||||
{
|
||||
"label": "Set up an MCP server",
|
||||
"slug": "docs/cli/tutorials/mcp-setup"
|
||||
},
|
||||
{ "label": "Automate tasks", "slug": "docs/cli/tutorials/automation" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Manage context and memory",
|
||||
"slug": "docs/cli/tutorials/memory-management"
|
||||
"label": "Features",
|
||||
"items": [
|
||||
{ "label": "Agent Skills", "slug": "docs/cli/skills" },
|
||||
{
|
||||
"label": "Authentication",
|
||||
"slug": "docs/get-started/authentication"
|
||||
},
|
||||
{ "label": "Checkpointing", "slug": "docs/cli/checkpointing" },
|
||||
{
|
||||
"label": "Extensions",
|
||||
"slug": "docs/extensions/index"
|
||||
},
|
||||
{ "label": "Headless mode", "slug": "docs/cli/headless" },
|
||||
{ "label": "Hooks", "slug": "docs/hooks" },
|
||||
{ "label": "IDE integration", "slug": "docs/ide-integration" },
|
||||
{ "label": "MCP servers", "slug": "docs/tools/mcp-server" },
|
||||
{ "label": "Model routing", "slug": "docs/cli/model-routing" },
|
||||
{ "label": "Model selection", "slug": "docs/cli/model" },
|
||||
{ "label": "Plan mode", "badge": "🧪", "slug": "docs/cli/plan-mode" },
|
||||
{
|
||||
"label": "Subagents",
|
||||
"badge": "🧪",
|
||||
"slug": "docs/core/subagents"
|
||||
},
|
||||
{
|
||||
"label": "Remote subagents",
|
||||
"badge": "🧪",
|
||||
"slug": "docs/core/remote-agents"
|
||||
},
|
||||
{ "label": "Rewind", "slug": "docs/cli/rewind" },
|
||||
{ "label": "Sandboxing", "slug": "docs/cli/sandbox" },
|
||||
{ "label": "Settings", "slug": "docs/cli/settings" },
|
||||
{ "label": "Telemetry", "slug": "docs/cli/telemetry" },
|
||||
{ "label": "Token caching", "slug": "docs/cli/token-caching" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Execute shell commands",
|
||||
"slug": "docs/cli/tutorials/shell-commands"
|
||||
"label": "Configuration",
|
||||
"items": [
|
||||
{ "label": "Custom commands", "slug": "docs/cli/custom-commands" },
|
||||
{
|
||||
"label": "Enterprise configuration",
|
||||
"slug": "docs/cli/enterprise"
|
||||
},
|
||||
{
|
||||
"label": "Ignore files (.geminiignore)",
|
||||
"slug": "docs/cli/gemini-ignore"
|
||||
},
|
||||
{
|
||||
"label": "Model configuration",
|
||||
"slug": "docs/cli/generation-settings"
|
||||
},
|
||||
{
|
||||
"label": "Project context (GEMINI.md)",
|
||||
"slug": "docs/cli/gemini-md"
|
||||
},
|
||||
{ "label": "Settings", "slug": "docs/cli/settings" },
|
||||
{
|
||||
"label": "System prompt override",
|
||||
"slug": "docs/cli/system-prompt"
|
||||
},
|
||||
{ "label": "Themes", "slug": "docs/cli/themes" },
|
||||
{ "label": "Trusted folders", "slug": "docs/cli/trusted-folders" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Manage sessions and history",
|
||||
"slug": "docs/cli/tutorials/session-management"
|
||||
},
|
||||
{
|
||||
"label": "Plan tasks with todos",
|
||||
"slug": "docs/cli/tutorials/task-planning"
|
||||
},
|
||||
{
|
||||
"label": "Web search and fetch",
|
||||
"slug": "docs/cli/tutorials/web-tools"
|
||||
},
|
||||
{
|
||||
"label": "Set up an MCP server",
|
||||
"slug": "docs/cli/tutorials/mcp-setup"
|
||||
},
|
||||
{ "label": "Automate tasks", "slug": "docs/cli/tutorials/automation" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Features",
|
||||
"items": [
|
||||
{ "label": "Agent Skills", "slug": "docs/cli/skills" },
|
||||
{
|
||||
"label": "Authentication",
|
||||
"slug": "docs/get-started/authentication"
|
||||
},
|
||||
{ "label": "Checkpointing", "slug": "docs/cli/checkpointing" },
|
||||
{
|
||||
"label": "Extensions",
|
||||
"slug": "docs/extensions/index"
|
||||
},
|
||||
{ "label": "Headless mode", "slug": "docs/cli/headless" },
|
||||
{ "label": "Help", "link": "/docs/cli/commands/#help-or" },
|
||||
{ "label": "Hooks", "slug": "docs/hooks" },
|
||||
{ "label": "IDE integration", "slug": "docs/ide-integration" },
|
||||
{ "label": "MCP servers", "slug": "docs/tools/mcp-server" },
|
||||
{
|
||||
"label": "Memory management",
|
||||
"link": "/docs/cli/commands/#memory"
|
||||
},
|
||||
{ "label": "Model routing", "slug": "docs/cli/model-routing" },
|
||||
{ "label": "Model selection", "slug": "docs/cli/model" },
|
||||
{ "label": "Plan mode", "badge": "🧪", "slug": "docs/cli/plan-mode" },
|
||||
{ "label": "Subagents", "badge": "🧪", "slug": "docs/core/subagents" },
|
||||
{
|
||||
"label": "Remote subagents",
|
||||
"badge": "🧪",
|
||||
"slug": "docs/core/remote-agents"
|
||||
},
|
||||
{ "label": "Rewind", "slug": "docs/cli/rewind" },
|
||||
{ "label": "Sandboxing", "slug": "docs/cli/sandbox" },
|
||||
{ "label": "Settings", "slug": "docs/cli/settings" },
|
||||
{
|
||||
"label": "Shell",
|
||||
"link": "/docs/cli/commands/#shells-or-bashes"
|
||||
"items": [
|
||||
{
|
||||
"label": "Overview",
|
||||
"slug": "docs/extensions"
|
||||
},
|
||||
{
|
||||
"label": "User guide: Install and manage",
|
||||
"link": "/docs/extensions/#manage-extensions"
|
||||
},
|
||||
{
|
||||
"label": "Developer guide: Build extensions",
|
||||
"slug": "docs/extensions/writing-extensions"
|
||||
},
|
||||
{
|
||||
"label": "Developer guide: Best practices",
|
||||
"slug": "docs/extensions/best-practices"
|
||||
},
|
||||
{
|
||||
"label": "Developer guide: Releasing",
|
||||
"slug": "docs/extensions/releasing"
|
||||
},
|
||||
{
|
||||
"label": "Developer guide: Reference",
|
||||
"slug": "docs/extensions/reference"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Stats",
|
||||
"link": "/docs/cli/commands/#stats"
|
||||
},
|
||||
{ "label": "Telemetry", "slug": "docs/cli/telemetry" },
|
||||
{ "label": "Token caching", "slug": "docs/cli/token-caching" },
|
||||
{ "label": "Tools", "link": "/docs/cli/commands/#tools" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Configuration",
|
||||
"items": [
|
||||
{ "label": "Custom commands", "slug": "docs/cli/custom-commands" },
|
||||
{ "label": "Enterprise configuration", "slug": "docs/cli/enterprise" },
|
||||
{
|
||||
"label": "Ignore files (.geminiignore)",
|
||||
"slug": "docs/cli/gemini-ignore"
|
||||
},
|
||||
{
|
||||
"label": "Model configuration",
|
||||
"slug": "docs/cli/generation-settings"
|
||||
},
|
||||
{ "label": "Project context (GEMINI.md)", "slug": "docs/cli/gemini-md" },
|
||||
{ "label": "Settings", "slug": "docs/cli/settings" },
|
||||
{ "label": "System prompt override", "slug": "docs/cli/system-prompt" },
|
||||
{ "label": "Themes", "slug": "docs/cli/themes" },
|
||||
{ "label": "Trusted folders", "slug": "docs/cli/trusted-folders" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Extensions",
|
||||
"items": [
|
||||
{
|
||||
"label": "Overview",
|
||||
"slug": "docs/extensions"
|
||||
},
|
||||
{
|
||||
"label": "User guide: Install and manage",
|
||||
"link": "/docs/extensions/#manage-extensions"
|
||||
},
|
||||
{
|
||||
"label": "Developer guide: Build extensions",
|
||||
"slug": "docs/extensions/writing-extensions"
|
||||
},
|
||||
{
|
||||
"label": "Developer guide: Best practices",
|
||||
"slug": "docs/extensions/best-practices"
|
||||
},
|
||||
{
|
||||
"label": "Developer guide: Releasing",
|
||||
"slug": "docs/extensions/releasing"
|
||||
},
|
||||
{
|
||||
"label": "Developer guide: Reference",
|
||||
"slug": "docs/extensions/reference"
|
||||
"label": "Development",
|
||||
"items": [
|
||||
{ "label": "Contribution guide", "slug": "docs/contributing" },
|
||||
{ "label": "Integration testing", "slug": "docs/integration-tests" },
|
||||
{
|
||||
"label": "Issue and PR automation",
|
||||
"slug": "docs/issue-and-pr-automation"
|
||||
},
|
||||
{ "label": "Local development", "slug": "docs/local-development" },
|
||||
{ "label": "NPM package structure", "slug": "docs/npm" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Reference",
|
||||
"label": "reference_tab",
|
||||
"items": [
|
||||
{ "label": "Command reference", "slug": "docs/cli/commands" },
|
||||
{
|
||||
"label": "Configuration reference",
|
||||
"slug": "docs/get-started/configuration"
|
||||
},
|
||||
{ "label": "Keyboard shortcuts", "slug": "docs/cli/keyboard-shortcuts" },
|
||||
{ "label": "Memory import processor", "slug": "docs/core/memport" },
|
||||
{ "label": "Policy engine", "slug": "docs/core/policy-engine" },
|
||||
{ "label": "Tools API", "slug": "docs/core/tools-api" }
|
||||
"label": "Reference",
|
||||
"items": [
|
||||
{ "label": "Command reference", "slug": "docs/reference/commands" },
|
||||
{
|
||||
"label": "Configuration reference",
|
||||
"slug": "docs/reference/configuration"
|
||||
},
|
||||
{
|
||||
"label": "Keyboard shortcuts",
|
||||
"slug": "docs/reference/keyboard-shortcuts"
|
||||
},
|
||||
{
|
||||
"label": "Memory import processor",
|
||||
"slug": "docs/reference/memport"
|
||||
},
|
||||
{ "label": "Policy engine", "slug": "docs/reference/policy-engine" },
|
||||
{ "label": "Tools API", "slug": "docs/reference/tools-api" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Resources",
|
||||
"label": "resources_tab",
|
||||
"items": [
|
||||
{ "label": "FAQ", "slug": "docs/faq" },
|
||||
{ "label": "Quota and pricing", "slug": "docs/quota-and-pricing" },
|
||||
{ "label": "Terms and privacy", "slug": "docs/tos-privacy" },
|
||||
{ "label": "Troubleshooting", "slug": "docs/troubleshooting" },
|
||||
{ "label": "Uninstall", "slug": "docs/cli/uninstall" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Development",
|
||||
"items": [
|
||||
{ "label": "Contribution guide", "slug": "docs/contributing" },
|
||||
{ "label": "Integration testing", "slug": "docs/integration-tests" },
|
||||
{
|
||||
"label": "Issue and PR automation",
|
||||
"slug": "docs/issue-and-pr-automation"
|
||||
},
|
||||
{ "label": "Local development", "slug": "docs/local-development" },
|
||||
{ "label": "NPM package structure", "slug": "docs/npm" }
|
||||
"label": "Resources",
|
||||
"items": [
|
||||
{ "label": "FAQ", "slug": "docs/resources/faq" },
|
||||
{
|
||||
"label": "Quota and pricing",
|
||||
"slug": "docs/resources/quota-and-pricing"
|
||||
},
|
||||
{
|
||||
"label": "Terms and privacy",
|
||||
"slug": "docs/resources/tos-privacy"
|
||||
},
|
||||
{
|
||||
"label": "Troubleshooting",
|
||||
"slug": "docs/resources/troubleshooting"
|
||||
},
|
||||
{ "label": "Uninstall", "slug": "docs/resources/uninstall" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Releases",
|
||||
"label": "releases_tab",
|
||||
"items": [
|
||||
{ "label": "Release notes", "slug": "docs/changelogs/" },
|
||||
{ "label": "Stable release", "slug": "docs/changelogs/latest" },
|
||||
{ "label": "Preview release", "slug": "docs/changelogs/preview" }
|
||||
{
|
||||
"label": "Releases",
|
||||
"items": [
|
||||
{ "label": "Release notes", "slug": "docs/changelogs/" },
|
||||
{ "label": "Stable release", "slug": "docs/changelogs/latest" },
|
||||
{ "label": "Preview release", "slug": "docs/changelogs/preview" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -105,10 +105,11 @@ lines containing matches, along with their file paths and line numbers.
|
||||
|
||||
## 6. `replace` (Edit)
|
||||
|
||||
`replace` replaces text within a file. By default, replaces a single occurrence,
|
||||
but can replace multiple occurrences when `expected_replacements` is specified.
|
||||
This tool is designed for precise, targeted changes and requires significant
|
||||
context around the `old_string` to ensure it modifies the correct location.
|
||||
`replace` replaces text within a file. By default, the tool expects to find and
|
||||
replace exactly ONE occurrence of `old_string`. If you want to replace multiple
|
||||
occurrences of the exact same string, set `allow_multiple` to `true`. This tool
|
||||
is designed for precise, targeted changes and requires significant context
|
||||
around the `old_string` to ensure it modifies the correct location.
|
||||
|
||||
- **Tool name:** `replace`
|
||||
- **Arguments:**
|
||||
@@ -116,6 +117,8 @@ context around the `old_string` to ensure it modifies the correct location.
|
||||
- `instruction` (string, required): Semantic description of the change.
|
||||
- `old_string` (string, required): Exact literal text to find.
|
||||
- `new_string` (string, required): Exact literal text to replace with.
|
||||
- `allow_multiple` (boolean, optional): If `true`, replaces all occurrences.
|
||||
If `false` (default), only succeeds if exactly one occurrence is found.
|
||||
- **Confirmation:** Requires manual user approval.
|
||||
|
||||
## Next steps
|
||||
|
||||
+2
-2
@@ -98,5 +98,5 @@ Always review confirmation prompts carefully before allowing a tool to execute.
|
||||
## Next steps
|
||||
|
||||
- Learn how to [Provide context](../cli/gemini-md.md) to guide tool use.
|
||||
- Explore the [Command reference](../cli/commands.md) for tool-related slash
|
||||
commands.
|
||||
- Explore the [Command reference](../reference/commands.md) for tool-related
|
||||
slash commands.
|
||||
|
||||
@@ -14,8 +14,8 @@ provides direct access to the Markdown files in the `docs/` directory.
|
||||
`get_internal_docs` takes one optional argument:
|
||||
|
||||
- `path` (string, optional): The relative path to a specific documentation file
|
||||
(for example, `cli/commands.md`). If omitted, the tool returns a list of all
|
||||
available documentation paths.
|
||||
(for example, `reference/commands.md`). If omitted, the tool returns a list of
|
||||
all available documentation paths.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -40,7 +40,7 @@ Gemini CLI uses this tool to ensure technical accuracy:
|
||||
|
||||
## Next steps
|
||||
|
||||
- Explore the [Command reference](../cli/commands.md) for a detailed guide to
|
||||
slash commands.
|
||||
- See the [Configuration guide](../get-started/configuration.md) for settings
|
||||
- Explore the [Command reference](../reference/commands.md) for a detailed guide
|
||||
to slash commands.
|
||||
- See the [Configuration guide](../reference/configuration.md) for settings
|
||||
reference.
|
||||
|
||||
@@ -163,7 +163,8 @@ Each server configuration supports the following properties:
|
||||
- **`args`** (string[]): Command-line arguments for Stdio transport
|
||||
- **`headers`** (object): Custom HTTP headers when using `url` or `httpUrl`
|
||||
- **`env`** (object): Environment variables for the server process. Values can
|
||||
reference environment variables using `$VAR_NAME` or `${VAR_NAME}` syntax
|
||||
reference environment variables using `$VAR_NAME` or `${VAR_NAME}` syntax (all
|
||||
platforms), or `%VAR_NAME%` (Windows only).
|
||||
- **`cwd`** (string): Working directory for Stdio transport
|
||||
- **`timeout`** (number): Request timeout in milliseconds (default: 600,000ms =
|
||||
10 minutes)
|
||||
@@ -184,6 +185,63 @@ Each server configuration supports the following properties:
|
||||
Service Account to impersonate. Used with
|
||||
`authProviderType: 'service_account_impersonation'`.
|
||||
|
||||
### Environment variable expansion
|
||||
|
||||
Gemini CLI automatically expands environment variables in the `env` block of
|
||||
your MCP server configuration. This allows you to securely reference variables
|
||||
defined in your shell or environment without hardcoding sensitive information
|
||||
directly in your `settings.json` file.
|
||||
|
||||
The expansion utility supports:
|
||||
|
||||
- **POSIX/Bash syntax:** `$VARIABLE_NAME` or `${VARIABLE_NAME}` (supported on
|
||||
all platforms)
|
||||
- **Windows syntax:** `%VARIABLE_NAME%` (supported only when running on Windows)
|
||||
|
||||
If a variable is not defined in the current environment, it resolves to an empty
|
||||
string.
|
||||
|
||||
**Example:**
|
||||
|
||||
```json
|
||||
"env": {
|
||||
"API_KEY": "$MY_EXTERNAL_TOKEN",
|
||||
"LOG_LEVEL": "$LOG_LEVEL",
|
||||
"TEMP_DIR": "%TEMP%"
|
||||
}
|
||||
```
|
||||
|
||||
### Security and environment sanitization
|
||||
|
||||
To protect your credentials, Gemini CLI performs environment sanitization when
|
||||
spawning MCP server processes.
|
||||
|
||||
#### Automatic redaction
|
||||
|
||||
By default, the CLI redacts sensitive environment variables from the base
|
||||
environment (inherited from the host process) to prevent unintended exposure to
|
||||
third-party MCP servers. This includes:
|
||||
|
||||
- Core project keys: `GEMINI_API_KEY`, `GOOGLE_API_KEY`, etc.
|
||||
- Variables matching sensitive patterns: `*TOKEN*`, `*SECRET*`, `*PASSWORD*`,
|
||||
`*KEY*`, `*AUTH*`, `*CREDENTIAL*`.
|
||||
- Certificates and private key patterns.
|
||||
|
||||
#### Explicit overrides
|
||||
|
||||
If an environment variable must be passed to an MCP server, you must explicitly
|
||||
state it in the `env` property of the server configuration in `settings.json`.
|
||||
Explicitly defined variables (including those from extensions) are trusted and
|
||||
are **not** subjected to the automatic redaction process.
|
||||
|
||||
This follows the security principle that if a variable is explicitly configured
|
||||
by the user for a specific server, it constitutes informed consent to share that
|
||||
specific data with that server.
|
||||
|
||||
> **Note:** Even when explicitly defined, you should avoid hardcoding secrets.
|
||||
> Instead, use environment variable expansion (e.g., `"MY_KEY": "$MY_KEY"`) to
|
||||
> securely pull the value from your host environment at runtime.
|
||||
|
||||
### OAuth support for remote MCP servers
|
||||
|
||||
The Gemini CLI supports OAuth 2.0 authentication for remote MCP servers using
|
||||
@@ -738,7 +796,9 @@ The MCP integration tracks several states:
|
||||
- **Trust settings:** The `trust` option bypasses all confirmation dialogs. Use
|
||||
cautiously and only for servers you completely control
|
||||
- **Access tokens:** Be security-aware when configuring environment variables
|
||||
containing API keys or tokens
|
||||
containing API keys or tokens. See
|
||||
[Security and environment sanitization](#security-and-environment-sanitization)
|
||||
for details on how Gemini CLI protects your credentials.
|
||||
- **Sandbox compatibility:** When using sandboxing, ensure MCP servers are
|
||||
available within the sandbox environment
|
||||
- **Private data:** Using broadly scoped personal access tokens can lead to
|
||||
|
||||
@@ -11,6 +11,8 @@ by the agent when you ask it to "start a plan" using natural language. In this
|
||||
mode, the agent is restricted to read-only tools to allow for safe exploration
|
||||
and planning.
|
||||
|
||||
> **Note:** This tool is not available when the CLI is in YOLO mode.
|
||||
|
||||
- **Tool name:** `enter_plan_mode`
|
||||
- **Display name:** Enter Plan Mode
|
||||
- **File:** `enter-plan-mode.ts`
|
||||
|
||||
+3
-3
@@ -131,9 +131,9 @@ configuration file.
|
||||
commands. Including the generic `run_shell_command` acts as a wildcard,
|
||||
allowing any command not explicitly blocked.
|
||||
- `tools.exclude` [DEPRECATED]: To block specific commands, use the
|
||||
[Policy Engine](../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,
|
||||
[Policy Engine](../reference/policy-engine.md). Historically, this setting
|
||||
allowed adding entries to the `exclude` list under the `tools` category in the
|
||||
format `run_shell_command(<command>)`. For example,
|
||||
`"tools": {"exclude": ["run_shell_command(rm)"]}` will block `rm` commands.
|
||||
|
||||
The validation logic is designed to be secure and flexible:
|
||||
|
||||
+4
-1
@@ -38,6 +38,7 @@ export default tseslint.config(
|
||||
'dist/**',
|
||||
'evals/**',
|
||||
'packages/test-utils/**',
|
||||
'.gemini/skills/**',
|
||||
],
|
||||
},
|
||||
eslint.configs.recommended,
|
||||
@@ -55,7 +56,7 @@ export default tseslint.config(
|
||||
},
|
||||
{
|
||||
// Import specific config
|
||||
files: ['packages/cli/src/**/*.{ts,tsx}'], // Target only TS/TSX in the cli package
|
||||
files: ['packages/*/src/**/*.{ts,tsx}'], // Target all TS/TSX in the packages
|
||||
plugins: {
|
||||
import: importPlugin,
|
||||
},
|
||||
@@ -199,6 +200,8 @@ export default tseslint.config(
|
||||
ignores: ['**/*.test.ts', '**/*.test.tsx'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-unsafe-type-assertion': 'error',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'error',
|
||||
'@typescript-eslint/no-unsafe-return': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 202 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, expect } from 'vitest';
|
||||
import { evalTest, TestRig } from './test-helper.js';
|
||||
import {
|
||||
assertModelHasOutput,
|
||||
checkModelOutputContent,
|
||||
} from './test-helper.js';
|
||||
|
||||
describe('grep_search_functionality', () => {
|
||||
const TEST_PREFIX = 'Grep Search Functionality: ';
|
||||
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: 'should find a simple string in a file',
|
||||
files: {
|
||||
'test.txt': `hello
|
||||
world
|
||||
hello world`,
|
||||
},
|
||||
prompt: 'Find "world" in test.txt',
|
||||
assert: async (rig: TestRig, result: string) => {
|
||||
await rig.waitForToolCall('grep_search');
|
||||
assertModelHasOutput(result);
|
||||
checkModelOutputContent(result, {
|
||||
expectedContent: [/L2: world/, /L3: hello world/],
|
||||
testName: `${TEST_PREFIX}simple search`,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: 'should perform a case-sensitive search',
|
||||
files: {
|
||||
'test.txt': `Hello
|
||||
hello`,
|
||||
},
|
||||
prompt: 'Find "Hello" in test.txt, case-sensitively.',
|
||||
assert: async (rig: TestRig, result: string) => {
|
||||
const wasToolCalled = await rig.waitForToolCall(
|
||||
'grep_search',
|
||||
undefined,
|
||||
(args) => {
|
||||
const params = JSON.parse(args);
|
||||
return params.case_sensitive === true;
|
||||
},
|
||||
);
|
||||
expect(
|
||||
wasToolCalled,
|
||||
'Expected grep_search to be called with case_sensitive: true',
|
||||
).toBe(true);
|
||||
|
||||
assertModelHasOutput(result);
|
||||
checkModelOutputContent(result, {
|
||||
expectedContent: [/L1: Hello/],
|
||||
forbiddenContent: [/L2: hello/],
|
||||
testName: `${TEST_PREFIX}case-sensitive search`,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: 'should return only file names when names_only is used',
|
||||
files: {
|
||||
'file1.txt': 'match me',
|
||||
'file2.txt': 'match me',
|
||||
},
|
||||
prompt: 'Find the files containing "match me".',
|
||||
assert: async (rig: TestRig, result: string) => {
|
||||
const wasToolCalled = await rig.waitForToolCall(
|
||||
'grep_search',
|
||||
undefined,
|
||||
(args) => {
|
||||
const params = JSON.parse(args);
|
||||
return params.names_only === true;
|
||||
},
|
||||
);
|
||||
expect(
|
||||
wasToolCalled,
|
||||
'Expected grep_search to be called with names_only: true',
|
||||
).toBe(true);
|
||||
|
||||
assertModelHasOutput(result);
|
||||
checkModelOutputContent(result, {
|
||||
expectedContent: [/file1.txt/, /file2.txt/],
|
||||
forbiddenContent: [/L1:/],
|
||||
testName: `${TEST_PREFIX}names_only search`,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: 'should search only within the specified include glob',
|
||||
files: {
|
||||
'file.js': 'my_function();',
|
||||
'file.ts': 'my_function();',
|
||||
},
|
||||
prompt: 'Find "my_function" in .js files.',
|
||||
assert: async (rig: TestRig, result: string) => {
|
||||
const wasToolCalled = await rig.waitForToolCall(
|
||||
'grep_search',
|
||||
undefined,
|
||||
(args) => {
|
||||
const params = JSON.parse(args);
|
||||
return params.include === '*.js';
|
||||
},
|
||||
);
|
||||
expect(
|
||||
wasToolCalled,
|
||||
'Expected grep_search to be called with include: "*.js"',
|
||||
).toBe(true);
|
||||
|
||||
assertModelHasOutput(result);
|
||||
checkModelOutputContent(result, {
|
||||
expectedContent: [/file.js/],
|
||||
forbiddenContent: [/file.ts/],
|
||||
testName: `${TEST_PREFIX}include glob search`,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: 'should search within a specific subdirectory',
|
||||
files: {
|
||||
'src/main.js': 'unique_string_1',
|
||||
'lib/main.js': 'unique_string_2',
|
||||
},
|
||||
prompt: 'Find "unique_string" in the src directory.',
|
||||
assert: async (rig: TestRig, result: string) => {
|
||||
const wasToolCalled = await rig.waitForToolCall(
|
||||
'grep_search',
|
||||
undefined,
|
||||
(args) => {
|
||||
const params = JSON.parse(args);
|
||||
return params.dir_path === 'src';
|
||||
},
|
||||
);
|
||||
expect(
|
||||
wasToolCalled,
|
||||
'Expected grep_search to be called with dir_path: "src"',
|
||||
).toBe(true);
|
||||
|
||||
assertModelHasOutput(result);
|
||||
checkModelOutputContent(result, {
|
||||
expectedContent: [/unique_string_1/],
|
||||
forbiddenContent: [/unique_string_2/],
|
||||
testName: `${TEST_PREFIX}subdirectory search`,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: 'should report no matches correctly',
|
||||
files: {
|
||||
'file.txt': 'nothing to see here',
|
||||
},
|
||||
prompt: 'Find "nonexistent" in file.txt',
|
||||
assert: async (rig: TestRig, result: string) => {
|
||||
await rig.waitForToolCall('grep_search');
|
||||
assertModelHasOutput(result);
|
||||
checkModelOutputContent(result, {
|
||||
expectedContent: [/No matches found/],
|
||||
testName: `${TEST_PREFIX}no matches`,
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -26,7 +26,7 @@ class MockClient implements acp.Client {
|
||||
};
|
||||
}
|
||||
|
||||
describe('ACP Environment and Auth', () => {
|
||||
describe.skip('ACP Environment and Auth', () => {
|
||||
let rig: TestRig;
|
||||
let child: ChildProcess | undefined;
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"web_fetch","args":{"prompt":"fetch https://example.com/1"}}},{"functionCall":{"name":"web_fetch","args":{"prompt":"fetch https://example.com/2"}}},{"functionCall":{"name":"web_fetch","args":{"prompt":"fetch https://example.com/3"}}},{"functionCall":{"name":"web_fetch","args":{"prompt":"fetch https://example.com/4"}}},{"functionCall":{"name":"web_fetch","args":{"prompt":"fetch https://example.com/5"}}},{"functionCall":{"name":"web_fetch","args":{"prompt":"fetch https://example.com/6"}}},{"functionCall":{"name":"web_fetch","args":{"prompt":"fetch https://example.com/7"}}},{"functionCall":{"name":"web_fetch","args":{"prompt":"fetch https://example.com/8"}}},{"functionCall":{"name":"web_fetch","args":{"prompt":"fetch https://example.com/9"}}},{"functionCall":{"name":"web_fetch","args":{"prompt":"fetch https://example.com/10"}}},{"functionCall":{"name":"web_fetch","args":{"prompt":"fetch https://example.com/11"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":500,"totalTokenCount":600}}]}
|
||||
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"Page 1 content"}],"role":"model"},"finishReason":"STOP","index":0}]}}
|
||||
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"Page 2 content"}],"role":"model"},"finishReason":"STOP","index":0}]}}
|
||||
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"Page 3 content"}],"role":"model"},"finishReason":"STOP","index":0}]}}
|
||||
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"Page 4 content"}],"role":"model"},"finishReason":"STOP","index":0}]}}
|
||||
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"Page 5 content"}],"role":"model"},"finishReason":"STOP","index":0}]}}
|
||||
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"Page 6 content"}],"role":"model"},"finishReason":"STOP","index":0}]}}
|
||||
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"Page 7 content"}],"role":"model"},"finishReason":"STOP","index":0}]}}
|
||||
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"Page 8 content"}],"role":"model"},"finishReason":"STOP","index":0}]}}
|
||||
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"Page 9 content"}],"role":"model"},"finishReason":"STOP","index":0}]}}
|
||||
{"method":"generateContent","response":{"candidates":[{"content":{"parts":[{"text":"Page 10 content"}],"role":"model"},"finishReason":"STOP","index":0}]}}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Some requests were rate limited: Rate limit exceeded for host. Please wait 60 seconds before trying again."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":1000,"candidatesTokenCount":50,"totalTokenCount":1050}}]}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { TestRig } from './test-helper.js';
|
||||
import { join } from 'node:path';
|
||||
|
||||
describe('web-fetch rate limiting', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (rig) {
|
||||
await rig.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it('should rate limit multiple requests to the same host', async () => {
|
||||
rig.setup('web-fetch rate limit', {
|
||||
settings: { tools: { core: ['web_fetch'] } },
|
||||
fakeResponsesPath: join(
|
||||
import.meta.dirname,
|
||||
'concurrency-limit.responses',
|
||||
),
|
||||
});
|
||||
|
||||
const result = await rig.run({
|
||||
args: `Fetch 11 pages from example.com`,
|
||||
});
|
||||
|
||||
// We expect to find at least one tool call that failed with a rate limit error.
|
||||
const toolLogs = rig.readToolLogs();
|
||||
const rateLimitedCalls = toolLogs.filter(
|
||||
(log) =>
|
||||
log.toolRequest.name === 'web_fetch' &&
|
||||
log.toolRequest.error?.includes('Rate limit exceeded'),
|
||||
);
|
||||
|
||||
expect(rateLimitedCalls.length).toBeGreaterThan(0);
|
||||
expect(result).toContain('Rate limit exceeded');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"read_file","args":{"file_path":"file1.txt"}}},{"functionCall":{"name":"read_file","args":{"file_path":"file2.txt"}}},{"functionCall":{"name":"write_file","args":{"file_path":"output.txt","content":"wave2"}}},{"functionCall":{"name":"read_file","args":{"file_path":"file3.txt"}}},{"functionCall":{"name":"read_file","args":{"file_path":"file4.txt"}}}, {"text":"All waves completed successfully."}]},"finishReason":"STOP","index":0}]}]}
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { TestRig } from './test-helper.js';
|
||||
import { join } from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
|
||||
describe('Parallel Tool Execution Integration', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rig.cleanup();
|
||||
});
|
||||
|
||||
it('should execute [read, read, write, read, read] in correct waves with user approval', async () => {
|
||||
rig.setup('parallel-wave-execution', {
|
||||
fakeResponsesPath: join(import.meta.dirname, 'parallel-tools.responses'),
|
||||
settings: {
|
||||
tools: {
|
||||
core: ['read_file', 'write_file'],
|
||||
approval: 'ASK', // Disable YOLO mode to show permission prompts
|
||||
confirmationRequired: ['write_file'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
rig.createFile('file1.txt', 'c1');
|
||||
rig.createFile('file2.txt', 'c2');
|
||||
rig.createFile('file3.txt', 'c3');
|
||||
rig.createFile('file4.txt', 'c4');
|
||||
rig.sync();
|
||||
|
||||
const run = await rig.runInteractive({ approvalMode: 'default' });
|
||||
|
||||
// 1. Trigger the wave
|
||||
await run.type('ok');
|
||||
await run.type('\r');
|
||||
|
||||
// 3. Wait for the write_file prompt.
|
||||
await run.expectText('Allow', 5000);
|
||||
|
||||
// 4. Press Enter to approve the write_file.
|
||||
await run.type('y');
|
||||
await run.type('\r');
|
||||
|
||||
// 5. Wait for the final model response
|
||||
await run.expectText('All waves completed successfully.', 5000);
|
||||
|
||||
// Verify all tool calls were made and succeeded in the logs
|
||||
await rig.expectToolCallSuccess(['write_file']);
|
||||
const toolLogs = rig.readToolLogs();
|
||||
|
||||
const readFiles = toolLogs.filter(
|
||||
(l) => l.toolRequest.name === 'read_file',
|
||||
);
|
||||
const writeFiles = toolLogs.filter(
|
||||
(l) => l.toolRequest.name === 'write_file',
|
||||
);
|
||||
|
||||
expect(readFiles.length).toBe(4);
|
||||
expect(writeFiles.length).toBe(1);
|
||||
expect(toolLogs.every((l) => l.toolRequest.success)).toBe(true);
|
||||
|
||||
// Check that output.txt was actually written
|
||||
expect(fs.readFileSync(join(rig.testDir!, 'output.txt'), 'utf8')).toBe(
|
||||
'wave2',
|
||||
);
|
||||
});
|
||||
});
|
||||
Generated
+633
-608
File diff suppressed because it is too large
Load Diff
+3
-1
@@ -68,7 +68,9 @@
|
||||
"wrap-ansi": "9.0.2",
|
||||
"cliui": {
|
||||
"wrap-ansi": "7.0.0"
|
||||
}
|
||||
},
|
||||
"glob": "^12.0.0",
|
||||
"node-domexception": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"gemini": "bundle/gemini.js"
|
||||
|
||||
@@ -29,6 +29,8 @@ import {
|
||||
CoderAgentEvent,
|
||||
getPersistedState,
|
||||
setPersistedState,
|
||||
getContextIdFromMetadata,
|
||||
getAgentSettingsFromMetadata,
|
||||
} from '../types.js';
|
||||
import { loadConfig, loadEnvironment, setTargetDir } from '../config/config.js';
|
||||
import { loadSettings } from '../config/settings.js';
|
||||
@@ -117,8 +119,7 @@ export class CoderAgentExecutor implements AgentExecutor {
|
||||
const agentSettings = persistedState._agentSettings;
|
||||
const config = await this.getConfig(agentSettings, sdkTask.id);
|
||||
const contextId: string =
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
(metadata['_contextId'] as string) || sdkTask.contextId;
|
||||
getContextIdFromMetadata(metadata) || sdkTask.contextId;
|
||||
const runtimeTask = await Task.create(
|
||||
sdkTask.id,
|
||||
contextId,
|
||||
@@ -141,8 +142,10 @@ export class CoderAgentExecutor implements AgentExecutor {
|
||||
agentSettingsInput?: AgentSettings,
|
||||
eventBus?: ExecutionEventBus,
|
||||
): Promise<TaskWrapper> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const agentSettings = agentSettingsInput || ({} as AgentSettings);
|
||||
const agentSettings: AgentSettings = agentSettingsInput || {
|
||||
kind: CoderAgentEvent.StateAgentSettingsEvent,
|
||||
workspacePath: process.cwd(),
|
||||
};
|
||||
const config = await this.getConfig(agentSettings, taskId);
|
||||
const runtimeTask = await Task.create(
|
||||
taskId,
|
||||
@@ -292,8 +295,7 @@ export class CoderAgentExecutor implements AgentExecutor {
|
||||
const contextId: string =
|
||||
userMessage.contextId ||
|
||||
sdkTask?.contextId ||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
(sdkTask?.metadata?.['_contextId'] as string) ||
|
||||
getContextIdFromMetadata(sdkTask?.metadata) ||
|
||||
uuidv4();
|
||||
|
||||
logger.info(
|
||||
@@ -388,10 +390,7 @@ export class CoderAgentExecutor implements AgentExecutor {
|
||||
}
|
||||
} else {
|
||||
logger.info(`[CoderAgentExecutor] Creating new task ${taskId}.`);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const agentSettings = userMessage.metadata?.[
|
||||
'coderAgent'
|
||||
] as AgentSettings;
|
||||
const agentSettings = getAgentSettingsFromMetadata(userMessage.metadata);
|
||||
try {
|
||||
wrapper = await this.createTask(
|
||||
taskId,
|
||||
|
||||
@@ -14,19 +14,21 @@ import {
|
||||
type Mock,
|
||||
} from 'vitest';
|
||||
import { Task } from './task.js';
|
||||
import type {
|
||||
ToolCall,
|
||||
Config,
|
||||
ToolCallRequestInfo,
|
||||
GitService,
|
||||
CompletedToolCall,
|
||||
} from '@google/gemini-cli-core';
|
||||
import {
|
||||
GeminiEventType,
|
||||
type Config,
|
||||
type ToolCallRequestInfo,
|
||||
type GitService,
|
||||
type CompletedToolCall,
|
||||
ApprovalMode,
|
||||
ToolConfirmationOutcome,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { createMockConfig } from '../utils/testing_utils.js';
|
||||
import type { ExecutionEventBus, RequestContext } from '@a2a-js/sdk/server';
|
||||
import { CoderAgentEvent } from '../types.js';
|
||||
import type { ToolCall } from '@google/gemini-cli-core';
|
||||
|
||||
const mockProcessRestorableToolCalls = vi.hoisted(() => vi.fn());
|
||||
|
||||
@@ -511,7 +513,10 @@ describe('Task', () => {
|
||||
{
|
||||
request: { callId: '1' },
|
||||
status: 'awaiting_approval',
|
||||
confirmationDetails: { onConfirm: onConfirmSpy },
|
||||
confirmationDetails: {
|
||||
type: 'edit',
|
||||
onConfirm: onConfirmSpy,
|
||||
},
|
||||
},
|
||||
] as unknown as ToolCall[];
|
||||
|
||||
@@ -531,7 +536,10 @@ describe('Task', () => {
|
||||
{
|
||||
request: { callId: '1' },
|
||||
status: 'awaiting_approval',
|
||||
confirmationDetails: { onConfirm: onConfirmSpy },
|
||||
confirmationDetails: {
|
||||
type: 'edit',
|
||||
onConfirm: onConfirmSpy,
|
||||
},
|
||||
},
|
||||
] as unknown as ToolCall[];
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
getAllMCPServerStatuses,
|
||||
MCPServerStatus,
|
||||
isNodeError,
|
||||
getErrorMessage,
|
||||
parseAndFormatApiError,
|
||||
safeLiteralReplace,
|
||||
DEFAULT_GUI_EDITOR,
|
||||
@@ -30,8 +31,7 @@ import {
|
||||
EDIT_TOOL_NAMES,
|
||||
processRestorableToolCalls,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { RequestContext } from '@a2a-js/sdk/server';
|
||||
import { type ExecutionEventBus } from '@a2a-js/sdk/server';
|
||||
import type { RequestContext, ExecutionEventBus } from '@a2a-js/sdk/server';
|
||||
import type {
|
||||
TaskStatusUpdateEvent,
|
||||
TaskArtifactUpdateEvent,
|
||||
@@ -59,6 +59,33 @@ import type { PartUnion, Part as genAiPart } from '@google/genai';
|
||||
|
||||
type UnionKeys<T> = T extends T ? keyof T : never;
|
||||
|
||||
type ConfirmationType = ToolCallConfirmationDetails['type'];
|
||||
|
||||
const VALID_CONFIRMATION_TYPES: readonly ConfirmationType[] = [
|
||||
'edit',
|
||||
'exec',
|
||||
'mcp',
|
||||
'info',
|
||||
'ask_user',
|
||||
'exit_plan_mode',
|
||||
] as const;
|
||||
|
||||
function isToolCallConfirmationDetails(
|
||||
value: unknown,
|
||||
): value is ToolCallConfirmationDetails {
|
||||
if (
|
||||
typeof value !== 'object' ||
|
||||
value === null ||
|
||||
!('onConfirm' in value) ||
|
||||
typeof value.onConfirm !== 'function' ||
|
||||
!('type' in value) ||
|
||||
typeof value.type !== 'string'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return (VALID_CONFIRMATION_TYPES as readonly string[]).includes(value.type);
|
||||
}
|
||||
|
||||
export class Task {
|
||||
id: string;
|
||||
contextId: string;
|
||||
@@ -376,11 +403,10 @@ export class Task {
|
||||
}
|
||||
|
||||
if (tc.status === 'awaiting_approval' && tc.confirmationDetails) {
|
||||
this.pendingToolConfirmationDetails.set(
|
||||
tc.request.callId,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
tc.confirmationDetails as ToolCallConfirmationDetails,
|
||||
);
|
||||
const details = tc.confirmationDetails;
|
||||
if (isToolCallConfirmationDetails(details)) {
|
||||
this.pendingToolConfirmationDetails.set(tc.request.callId, details);
|
||||
}
|
||||
}
|
||||
|
||||
// Only send an update if the status has actually changed.
|
||||
@@ -412,11 +438,12 @@ export class Task {
|
||||
);
|
||||
toolCalls.forEach((tc: ToolCall) => {
|
||||
if (tc.status === 'awaiting_approval' && tc.confirmationDetails) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises, @typescript-eslint/no-unsafe-type-assertion
|
||||
(tc.confirmationDetails as ToolCallConfirmationDetails).onConfirm(
|
||||
ToolConfirmationOutcome.ProceedOnce,
|
||||
);
|
||||
this.pendingToolConfirmationDetails.delete(tc.request.callId);
|
||||
const details = tc.confirmationDetails;
|
||||
if (isToolCallConfirmationDetails(details)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
details.onConfirm(ToolConfirmationOutcome.ProceedOnce);
|
||||
this.pendingToolConfirmationDetails.delete(tc.request.callId);
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
@@ -466,15 +493,13 @@ export class Task {
|
||||
T extends ToolCall | AnyDeclarativeTool,
|
||||
K extends UnionKeys<T>,
|
||||
>(from: T, ...fields: K[]): Partial<T> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const ret = {} as Pick<T, K>;
|
||||
const ret: Partial<T> = {};
|
||||
for (const field of fields) {
|
||||
if (field in from) {
|
||||
if (field in from && from[field] !== undefined) {
|
||||
ret[field] = from[field];
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
return ret as Partial<T>;
|
||||
return ret;
|
||||
}
|
||||
|
||||
private toolStatusMessage(
|
||||
@@ -485,8 +510,11 @@ export class Task {
|
||||
const messageParts: Part[] = [];
|
||||
|
||||
// Create a serializable version of the ToolCall (pick necessary
|
||||
// properties/avoid methods causing circular reference errors)
|
||||
const serializableToolCall: Partial<ToolCall> = this._pickFields(
|
||||
// properties/avoid methods causing circular reference errors).
|
||||
// Type allows tool to be Partial<AnyDeclarativeTool> for serialization.
|
||||
const serializableToolCall: Partial<Omit<ToolCall, 'tool'>> & {
|
||||
tool?: Partial<AnyDeclarativeTool>;
|
||||
} = this._pickFields(
|
||||
tc,
|
||||
'request',
|
||||
'status',
|
||||
@@ -496,8 +524,7 @@ export class Task {
|
||||
);
|
||||
|
||||
if (tc.tool) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
serializableToolCall.tool = this._pickFields(
|
||||
const toolFields = this._pickFields(
|
||||
tc.tool,
|
||||
'name',
|
||||
'displayName',
|
||||
@@ -507,7 +534,8 @@ export class Task {
|
||||
'canUpdateOutput',
|
||||
'schema',
|
||||
'parameterSchema',
|
||||
) as AnyDeclarativeTool;
|
||||
);
|
||||
serializableToolCall.tool = toolFields;
|
||||
}
|
||||
|
||||
messageParts.push({
|
||||
@@ -530,8 +558,15 @@ export class Task {
|
||||
old_string: string,
|
||||
new_string: string,
|
||||
): Promise<string> {
|
||||
// Validate path to prevent path traversal vulnerabilities
|
||||
const resolvedPath = path.resolve(this.config.getTargetDir(), file_path);
|
||||
const pathError = this.config.validatePathAccess(resolvedPath, 'read');
|
||||
if (pathError) {
|
||||
throw new Error(`Path validation failed: ${pathError}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const currentContent = await fs.readFile(file_path, 'utf8');
|
||||
const currentContent = await fs.readFile(resolvedPath, 'utf8');
|
||||
return this._applyReplacement(
|
||||
currentContent,
|
||||
old_string,
|
||||
@@ -625,15 +660,32 @@ export class Task {
|
||||
request.args['old_string'] &&
|
||||
request.args['new_string']
|
||||
) {
|
||||
const newContent = await this.getProposedContent(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
request.args['file_path'] as string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
request.args['old_string'] as string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
request.args['new_string'] as string,
|
||||
);
|
||||
return { ...request, args: { ...request.args, newContent } };
|
||||
const filePath = request.args['file_path'];
|
||||
const oldString = request.args['old_string'];
|
||||
const newString = request.args['new_string'];
|
||||
if (
|
||||
typeof filePath === 'string' &&
|
||||
typeof oldString === 'string' &&
|
||||
typeof newString === 'string'
|
||||
) {
|
||||
// Resolve and validate path to prevent path traversal (user-controlled file_path).
|
||||
const resolvedPath = path.resolve(
|
||||
this.config.getTargetDir(),
|
||||
filePath,
|
||||
);
|
||||
const pathError = this.config.validatePathAccess(
|
||||
resolvedPath,
|
||||
'read',
|
||||
);
|
||||
if (!pathError) {
|
||||
const newContent = await this.getProposedContent(
|
||||
resolvedPath,
|
||||
oldString,
|
||||
newString,
|
||||
);
|
||||
return { ...request, args: { ...request.args, newContent } };
|
||||
}
|
||||
}
|
||||
}
|
||||
return request;
|
||||
}),
|
||||
@@ -725,19 +777,27 @@ export class Task {
|
||||
break;
|
||||
case GeminiEventType.Error:
|
||||
default: {
|
||||
// Block scope for lexical declaration
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const errorEvent = event as ServerGeminiErrorEvent; // Type assertion
|
||||
const errorMessage =
|
||||
errorEvent.value?.error.message ?? 'Unknown error from LLM stream';
|
||||
// Use type guard instead of unsafe type assertion
|
||||
let errorEvent: ServerGeminiErrorEvent | undefined;
|
||||
if (
|
||||
event.type === GeminiEventType.Error &&
|
||||
event.value &&
|
||||
typeof event.value === 'object' &&
|
||||
'error' in event.value
|
||||
) {
|
||||
errorEvent = event;
|
||||
}
|
||||
const errorMessage = errorEvent?.value?.error
|
||||
? getErrorMessage(errorEvent.value.error)
|
||||
: 'Unknown error from LLM stream';
|
||||
logger.error(
|
||||
'[Task] Received error event from LLM stream:',
|
||||
errorMessage,
|
||||
);
|
||||
|
||||
let errMessage = `Unknown error from LLM stream: ${JSON.stringify(event)}`;
|
||||
if (errorEvent.value) {
|
||||
errMessage = parseAndFormatApiError(errorEvent.value);
|
||||
if (errorEvent?.value?.error) {
|
||||
errMessage = parseAndFormatApiError(errorEvent.value.error);
|
||||
}
|
||||
this.cancelPendingTools(`LLM stream error: ${errorMessage}`);
|
||||
this.setTaskStateAndPublishUpdate(
|
||||
@@ -813,12 +873,11 @@ export class Task {
|
||||
|
||||
// If `edit` tool call, pass updated payload if presesent
|
||||
if (confirmationDetails.type === 'edit') {
|
||||
const payload = part.data['newContent']
|
||||
? ({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
newContent: part.data['newContent'] as string,
|
||||
} as ToolConfirmationPayload)
|
||||
: undefined;
|
||||
const newContent = part.data['newContent'];
|
||||
const payload =
|
||||
typeof newContent === 'string'
|
||||
? ({ newContent } as ToolConfirmationPayload)
|
||||
: undefined;
|
||||
this.skipFinalTrueAfterInlineEdit = !!payload;
|
||||
try {
|
||||
await confirmationDetails.onConfirm(confirmationOutcome, payload);
|
||||
|
||||
@@ -69,6 +69,7 @@ export class RestoreCommand implements Command {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const toolCallData = JSON.parse(data);
|
||||
const ToolCallDataSchema = getToolCallDataSchema();
|
||||
const parseResult = ToolCallDataSchema.safeParse(toolCallData);
|
||||
|
||||
@@ -8,17 +8,19 @@ import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as dotenv from 'dotenv';
|
||||
|
||||
import type { TelemetryTarget } from '@google/gemini-cli-core';
|
||||
import type {
|
||||
TelemetryTarget,
|
||||
ConfigParameters,
|
||||
ExtensionLoader,
|
||||
} from '@google/gemini-cli-core';
|
||||
import {
|
||||
AuthType,
|
||||
Config,
|
||||
type ConfigParameters,
|
||||
FileDiscoveryService,
|
||||
ApprovalMode,
|
||||
loadServerHierarchicalMemory,
|
||||
GEMINI_DIR,
|
||||
DEFAULT_GEMINI_EMBEDDING_MODEL,
|
||||
type ExtensionLoader,
|
||||
startupProfiler,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
homedir,
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import type { MCPServerConfig } from '@google/gemini-cli-core';
|
||||
import {
|
||||
type MCPServerConfig,
|
||||
debugLogger,
|
||||
GEMINI_DIR,
|
||||
getErrorMessage,
|
||||
@@ -122,6 +122,7 @@ export function loadSettings(workspaceDir: string): Settings {
|
||||
function resolveEnvVarsInString(value: string): string {
|
||||
const envVarRegex = /\$(?:(\w+)|{([^}]+)})/g; // Find $VAR_NAME or ${VAR_NAME}
|
||||
return value.replace(envVarRegex, (match, varName1, varName2) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const varName = varName1 || varName2;
|
||||
if (process && process.env && typeof process.env[varName] === 'string') {
|
||||
return process.env[varName];
|
||||
@@ -146,7 +147,7 @@ function resolveEnvVarsInObject<T>(obj: T): T {
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-unsafe-return
|
||||
return obj.map((item) => resolveEnvVarsInObject(item)) as unknown as T;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,12 +4,11 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import {
|
||||
GeminiEventType,
|
||||
ApprovalMode,
|
||||
type ToolCallConfirmationDetails,
|
||||
import type {
|
||||
Config,
|
||||
ToolCallConfirmationDetails,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { GeminiEventType, ApprovalMode } from '@google/gemini-cli-core';
|
||||
import type {
|
||||
TaskStatusUpdateEvent,
|
||||
SendStreamingMessageSuccessResponse,
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
import express from 'express';
|
||||
|
||||
import type { AgentCard, Message } from '@a2a-js/sdk';
|
||||
import type { TaskStore } from '@a2a-js/sdk/server';
|
||||
import {
|
||||
type TaskStore,
|
||||
DefaultRequestHandler,
|
||||
InMemoryTaskStore,
|
||||
DefaultExecutionEventBus,
|
||||
@@ -25,9 +25,12 @@ import { loadConfig, loadEnvironment, setTargetDir } from '../config/config.js';
|
||||
import { loadSettings } from '../config/settings.js';
|
||||
import { loadExtensions } from '../config/extension.js';
|
||||
import { commandRegistry } from '../commands/command-registry.js';
|
||||
import { debugLogger, SimpleExtensionLoader } from '@google/gemini-cli-core';
|
||||
import {
|
||||
debugLogger,
|
||||
SimpleExtensionLoader,
|
||||
GitService,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { Command, CommandArgument } from '../commands/types.js';
|
||||
import { GitService } from '@google/gemini-cli-core';
|
||||
|
||||
type CommandResponse = {
|
||||
name: string;
|
||||
@@ -88,6 +91,7 @@ async function handleExecuteCommand(
|
||||
},
|
||||
) {
|
||||
logger.info('[CoreAgent] Received /executeCommand request: ', req.body);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const { command, args } = req.body;
|
||||
try {
|
||||
if (typeof command !== 'string') {
|
||||
@@ -211,6 +215,7 @@ export async function createApp() {
|
||||
const agentSettings = req.body.agentSettings as
|
||||
| AgentSettings
|
||||
| undefined;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const contextId = req.body.contextId || uuidv4();
|
||||
const wrapper = await agentExecutor.createTask(
|
||||
taskId,
|
||||
|
||||
@@ -246,6 +246,7 @@ export class GCSTaskStore implements TaskStore {
|
||||
}
|
||||
const [compressedMetadata] = await metadataFile.download();
|
||||
const jsonData = gunzipSync(compressedMetadata).toString();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const loadedMetadata = JSON.parse(jsonData);
|
||||
logger.info(`Task ${taskId} metadata loaded from GCS.`);
|
||||
|
||||
@@ -282,12 +283,14 @@ export class GCSTaskStore implements TaskStore {
|
||||
|
||||
return {
|
||||
id: taskId,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
contextId: loadedMetadata._contextId || uuidv4(),
|
||||
kind: 'task',
|
||||
status: {
|
||||
state: persistedState._taskState,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
metadata: loadedMetadata,
|
||||
history: [],
|
||||
artifacts: [],
|
||||
|
||||
@@ -122,11 +122,60 @@ export type PersistedTaskMetadata = { [k: string]: unknown };
|
||||
|
||||
export const METADATA_KEY = '__persistedState';
|
||||
|
||||
function isAgentSettings(value: unknown): value is AgentSettings {
|
||||
return (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
'kind' in value &&
|
||||
value.kind === CoderAgentEvent.StateAgentSettingsEvent &&
|
||||
'workspacePath' in value &&
|
||||
typeof value.workspacePath === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
function isPersistedStateMetadata(
|
||||
value: unknown,
|
||||
): value is PersistedStateMetadata {
|
||||
return (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
'_agentSettings' in value &&
|
||||
'_taskState' in value &&
|
||||
isAgentSettings(value._agentSettings)
|
||||
);
|
||||
}
|
||||
|
||||
export function getPersistedState(
|
||||
metadata: PersistedTaskMetadata,
|
||||
): PersistedStateMetadata | undefined {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
return metadata?.[METADATA_KEY] as PersistedStateMetadata | undefined;
|
||||
const state = metadata?.[METADATA_KEY];
|
||||
if (isPersistedStateMetadata(state)) {
|
||||
return state;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function getContextIdFromMetadata(
|
||||
metadata: PersistedTaskMetadata | undefined,
|
||||
): string | undefined {
|
||||
if (!metadata) {
|
||||
return undefined;
|
||||
}
|
||||
const contextId = metadata['_contextId'];
|
||||
return typeof contextId === 'string' ? contextId : undefined;
|
||||
}
|
||||
|
||||
export function getAgentSettingsFromMetadata(
|
||||
metadata: PersistedTaskMetadata | undefined,
|
||||
): AgentSettings | undefined {
|
||||
if (!metadata) {
|
||||
return undefined;
|
||||
}
|
||||
const coderAgent = metadata['coderAgent'];
|
||||
if (isAgentSettings(coderAgent)) {
|
||||
return coderAgent;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function setPersistedState(
|
||||
|
||||
@@ -71,6 +71,7 @@ export function createMockConfig(
|
||||
getMcpServers: vi.fn().mockReturnValue({}),
|
||||
}),
|
||||
getGitService: vi.fn(),
|
||||
validatePathAccess: vi.fn().mockReturnValue(undefined),
|
||||
...overrides,
|
||||
} as unknown as Config;
|
||||
mockConfig.getMessageBus = vi.fn().mockReturnValue(createMockMessageBus());
|
||||
|
||||
+16
-1
@@ -36,7 +36,21 @@ process.on('uncaughtException', (error) => {
|
||||
});
|
||||
|
||||
main().catch(async (error) => {
|
||||
await runExitCleanup();
|
||||
// Set a timeout to force exit if cleanup hangs
|
||||
const cleanupTimeout = setTimeout(() => {
|
||||
writeToStderr('Cleanup timed out, forcing exit...\n');
|
||||
process.exit(1);
|
||||
}, 5000);
|
||||
|
||||
try {
|
||||
await runExitCleanup();
|
||||
} catch (cleanupError) {
|
||||
writeToStderr(
|
||||
`Error during final cleanup: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}\n`,
|
||||
);
|
||||
} finally {
|
||||
clearTimeout(cleanupTimeout);
|
||||
}
|
||||
|
||||
if (error instanceof FatalError) {
|
||||
let errorMessage = error.message;
|
||||
@@ -46,6 +60,7 @@ main().catch(async (error) => {
|
||||
writeToStderr(errorMessage + '\n');
|
||||
process.exit(error.exitCode);
|
||||
}
|
||||
|
||||
writeToStderr('An unexpected critical error occurred:');
|
||||
if (error instanceof Error) {
|
||||
writeToStderr(error.stack + '\n');
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.12.0",
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
"@google/genai": "1.30.0",
|
||||
"@google/genai": "1.41.0",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@modelcontextprotocol/sdk": "^1.23.0",
|
||||
"ansi-escapes": "^7.3.0",
|
||||
@@ -71,7 +71,6 @@
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@google/gemini-cli-devtools": "file:../devtools",
|
||||
"@google/gemini-cli-test-utils": "file:../test-utils",
|
||||
"@types/command-exists": "^1.2.3",
|
||||
"@types/hast": "^3.0.4",
|
||||
|
||||
@@ -47,6 +47,7 @@ const defaultRequestConfirmation: RequestConfirmationCallback = async (
|
||||
message,
|
||||
initial: false,
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return response.confirm;
|
||||
};
|
||||
|
||||
|
||||
@@ -21,7 +21,11 @@ import {
|
||||
type MCPServerConfig,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { loadCliConfig, parseArguments, type CliArgs } from './config.js';
|
||||
import { type Settings, createTestMergedSettings } from './settings.js';
|
||||
import {
|
||||
type Settings,
|
||||
type MergedSettings,
|
||||
createTestMergedSettings,
|
||||
} from './settings.js';
|
||||
import * as ServerConfig from '@google/gemini-cli-core';
|
||||
|
||||
import { isWorkspaceTrusted } from './trustedFolders.js';
|
||||
@@ -2012,6 +2016,40 @@ describe('loadCliConfig useRipgrep', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadCliConfig directWebFetch', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
|
||||
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
|
||||
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should be false by default when directWebFetch is not set in settings', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
const settings = createTestMergedSettings();
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.getDirectWebFetch()).toBe(false);
|
||||
});
|
||||
|
||||
it('should be true when directWebFetch is set to true in settings', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
const settings = createTestMergedSettings({
|
||||
experimental: {
|
||||
directWebFetch: true,
|
||||
},
|
||||
});
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.getDirectWebFetch()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('screenReader configuration', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
@@ -2599,6 +2637,21 @@ describe('loadCliConfig approval mode', () => {
|
||||
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
|
||||
});
|
||||
|
||||
it('should pass planSettings.directory from settings to config', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings = createTestMergedSettings({
|
||||
general: {
|
||||
plan: {
|
||||
directory: '.custom-plans',
|
||||
},
|
||||
},
|
||||
} as unknown as MergedSettings);
|
||||
const argv = await parseArguments(settings);
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
const plansDir = config.storage.getPlansDir();
|
||||
expect(plansDir).toContain('.custom-plans');
|
||||
});
|
||||
|
||||
// --- Untrusted Folder Scenarios ---
|
||||
describe('when folder is NOT trusted', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -56,7 +56,10 @@ import { resolvePath } from '../utils/resolvePath.js';
|
||||
import { RESUME_LATEST } from '../utils/sessionUtils.js';
|
||||
|
||||
import { isWorkspaceTrusted } from './trustedFolders.js';
|
||||
import { createPolicyEngineConfig } from './policy.js';
|
||||
import {
|
||||
createPolicyEngineConfig,
|
||||
resolveWorkspacePolicyState,
|
||||
} from './policy.js';
|
||||
import { ExtensionManager } from './extension-manager.js';
|
||||
import { McpServerEnablementManager } from './mcp/mcpServerEnablement.js';
|
||||
import type { ExtensionEvents } from '@google/gemini-cli-core/src/utils/extensionLoader.js';
|
||||
@@ -692,9 +695,17 @@ export async function loadCliConfig(
|
||||
policyPaths: argv.policy,
|
||||
};
|
||||
|
||||
const { workspacePoliciesDir, policyUpdateConfirmationRequest } =
|
||||
await resolveWorkspacePolicyState({
|
||||
cwd,
|
||||
trustedFolder,
|
||||
interactive,
|
||||
});
|
||||
|
||||
const policyEngineConfig = await createPolicyEngineConfig(
|
||||
effectiveSettings,
|
||||
approvalMode,
|
||||
workspacePoliciesDir,
|
||||
);
|
||||
policyEngineConfig.nonInteractive = !interactive;
|
||||
|
||||
@@ -758,6 +769,7 @@ export async function loadCliConfig(
|
||||
coreTools: settings.tools?.core || undefined,
|
||||
allowedTools: allowedTools.length > 0 ? allowedTools : undefined,
|
||||
policyEngineConfig,
|
||||
policyUpdateConfirmationRequest,
|
||||
excludeTools,
|
||||
toolDiscoveryCommand: settings.tools?.discoveryCommand,
|
||||
toolCallCommand: settings.tools?.callCommand,
|
||||
@@ -814,6 +826,8 @@ export async function loadCliConfig(
|
||||
enableExtensionReloading: settings.experimental?.extensionReloading,
|
||||
enableAgents: settings.experimental?.enableAgents,
|
||||
plan: settings.experimental?.plan,
|
||||
directWebFetch: settings.experimental?.directWebFetch,
|
||||
planSettings: settings.general?.plan,
|
||||
enableEventDrivenScheduler: true,
|
||||
skillsSupport: settings.skills?.enabled ?? true,
|
||||
disabledSkills: settings.skills?.disabled,
|
||||
@@ -835,7 +849,6 @@ export async function loadCliConfig(
|
||||
enableShellOutputEfficiency:
|
||||
settings.tools?.shell?.enableShellOutputEfficiency ?? true,
|
||||
skipNextSpeakerCheck: settings.model?.skipNextSpeakerCheck,
|
||||
enablePromptCompletion: settings.general?.enablePromptCompletion,
|
||||
truncateToolOutputThreshold: settings.tools?.truncateToolOutputThreshold,
|
||||
eventEmitter: coreEvents,
|
||||
useWriteTodos: argv.useWriteTodos ?? settings.useWriteTodos,
|
||||
|
||||
@@ -866,6 +866,7 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
|
||||
try {
|
||||
const hooksContent = await fs.promises.readFile(hooksFilePath, 'utf-8');
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const rawHooks = JSON.parse(hooksContent);
|
||||
|
||||
if (
|
||||
|
||||
@@ -224,4 +224,59 @@ describe('ExtensionRegistryClient', () => {
|
||||
'Failed to fetch extensions: Not Found',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not return irrelevant results', async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => [
|
||||
...mockExtensions,
|
||||
{
|
||||
id: 'dataplex',
|
||||
extensionName: 'dataplex',
|
||||
extensionDescription: 'Connect to Dataplex Universal Catalog...',
|
||||
fullName: 'google-cloud/dataplex',
|
||||
rank: 6,
|
||||
stars: 6,
|
||||
url: '',
|
||||
repoDescription: '',
|
||||
lastUpdated: '',
|
||||
extensionVersion: '1.0.0',
|
||||
avatarUrl: '',
|
||||
hasMCP: false,
|
||||
hasContext: false,
|
||||
isGoogleOwned: true,
|
||||
licenseKey: '',
|
||||
hasHooks: false,
|
||||
hasCustomCommands: false,
|
||||
hasSkills: false,
|
||||
},
|
||||
{
|
||||
id: 'conductor',
|
||||
extensionName: 'conductor',
|
||||
extensionDescription: 'A conductor extension that actually matches.',
|
||||
fullName: 'someone/conductor',
|
||||
rank: 100,
|
||||
stars: 100,
|
||||
url: '',
|
||||
repoDescription: '',
|
||||
lastUpdated: '',
|
||||
extensionVersion: '1.0.0',
|
||||
avatarUrl: '',
|
||||
hasMCP: false,
|
||||
hasContext: false,
|
||||
isGoogleOwned: false,
|
||||
licenseKey: '',
|
||||
hasHooks: false,
|
||||
hasCustomCommands: false,
|
||||
hasSkills: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const results = await client.searchExtensions('conductor');
|
||||
const ids = results.map((r) => r.id);
|
||||
|
||||
expect(ids).not.toContain('dataplex');
|
||||
expect(ids).toContain('conductor');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -79,9 +79,11 @@ export class ExtensionRegistryClient {
|
||||
const fzf = new AsyncFzf(allExtensions, {
|
||||
selector: (ext: RegistryExtension) =>
|
||||
`${ext.extensionName} ${ext.extensionDescription} ${ext.fullName}`,
|
||||
fuzzy: 'v2',
|
||||
fuzzy: true,
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const results = await fzf.find(query);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return results.map((r: { item: RegistryExtension }) => r.item);
|
||||
}
|
||||
|
||||
@@ -108,7 +110,6 @@ export class ExtensionRegistryClient {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
return (await response.json()) as RegistryExtension[];
|
||||
} catch (error) {
|
||||
// Clear the promise on failure so that subsequent calls can try again
|
||||
ExtensionRegistryClient.fetchPromise = null;
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -179,6 +179,7 @@ export class ExtensionEnablementManager {
|
||||
readConfig(): AllExtensionsEnablementConfig {
|
||||
try {
|
||||
const content = fs.readFileSync(this.configFilePath, 'utf-8');
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return JSON.parse(content);
|
||||
} catch (error) {
|
||||
if (
|
||||
|
||||
@@ -156,6 +156,7 @@ export async function promptForSetting(
|
||||
name: 'value',
|
||||
message: `${setting.name}\n${setting.description}`,
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return response.value;
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ export function recursivelyHydrateStrings<T>(
|
||||
if (Array.isArray(obj)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
return obj.map((item) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
recursivelyHydrateStrings(item, values),
|
||||
) as unknown as T;
|
||||
}
|
||||
|
||||
@@ -132,6 +132,35 @@ describe('Policy Engine Integration Tests', () => {
|
||||
).toBe(PolicyDecision.ASK_USER);
|
||||
});
|
||||
|
||||
it('should handle global MCP wildcard (*) in settings', async () => {
|
||||
const settings: Settings = {
|
||||
mcp: {
|
||||
allowed: ['*'],
|
||||
},
|
||||
};
|
||||
|
||||
const config = await createPolicyEngineConfig(
|
||||
settings,
|
||||
ApprovalMode.DEFAULT,
|
||||
);
|
||||
const engine = new PolicyEngine(config);
|
||||
|
||||
// ANY tool with a server name should be allowed
|
||||
expect(
|
||||
(await engine.check({ name: 'mcp-server__tool' }, 'mcp-server'))
|
||||
.decision,
|
||||
).toBe(PolicyDecision.ALLOW);
|
||||
expect(
|
||||
(await engine.check({ name: 'another-server__tool' }, 'another-server'))
|
||||
.decision,
|
||||
).toBe(PolicyDecision.ALLOW);
|
||||
|
||||
// Built-in tools should NOT be allowed by the MCP wildcard
|
||||
expect(
|
||||
(await engine.check({ name: 'run_shell_command' }, undefined)).decision,
|
||||
).toBe(PolicyDecision.ASK_USER);
|
||||
});
|
||||
|
||||
it('should correctly prioritize specific tool excludes over MCP server wildcards', async () => {
|
||||
const settings: Settings = {
|
||||
mcp: {
|
||||
@@ -148,13 +177,13 @@ describe('Policy Engine Integration Tests', () => {
|
||||
);
|
||||
const engine = new PolicyEngine(config);
|
||||
|
||||
// MCP server allowed (priority 2.1) provides general allow for server
|
||||
// MCP server allowed (priority 2.1) provides general allow for server
|
||||
// MCP server allowed (priority 3.1) provides general allow for server
|
||||
// MCP server allowed (priority 3.1) provides general allow for server
|
||||
expect(
|
||||
(await engine.check({ name: 'my-server__safe-tool' }, undefined))
|
||||
.decision,
|
||||
).toBe(PolicyDecision.ALLOW);
|
||||
// But specific tool exclude (priority 2.4) wins over server allow
|
||||
// But specific tool exclude (priority 3.4) wins over server allow
|
||||
expect(
|
||||
(await engine.check({ name: 'my-server__dangerous-tool' }, undefined))
|
||||
.decision,
|
||||
@@ -323,6 +352,38 @@ describe('Policy Engine Integration Tests', () => {
|
||||
).toBe(PolicyDecision.DENY);
|
||||
});
|
||||
|
||||
it('should correctly match tool annotations', async () => {
|
||||
const settings: Settings = {};
|
||||
|
||||
const config = await createPolicyEngineConfig(
|
||||
settings,
|
||||
ApprovalMode.DEFAULT,
|
||||
);
|
||||
|
||||
// Add a manual rule with annotations to the config
|
||||
config.rules = config.rules || [];
|
||||
config.rules.push({
|
||||
toolAnnotations: { readOnlyHint: true },
|
||||
decision: PolicyDecision.ALLOW,
|
||||
priority: 10,
|
||||
});
|
||||
|
||||
const engine = new PolicyEngine(config);
|
||||
|
||||
// A tool with readOnlyHint=true should be ALLOWED
|
||||
const roCall = { name: 'some_tool', args: {} };
|
||||
const roMeta = { readOnlyHint: true };
|
||||
expect((await engine.check(roCall, undefined, roMeta)).decision).toBe(
|
||||
PolicyDecision.ALLOW,
|
||||
);
|
||||
|
||||
// A tool without the hint (or with false) should follow default decision (ASK_USER)
|
||||
const rwMeta = { readOnlyHint: false };
|
||||
expect((await engine.check(roCall, undefined, rwMeta)).decision).toBe(
|
||||
PolicyDecision.ASK_USER,
|
||||
);
|
||||
});
|
||||
|
||||
describe.each(['write_file', 'replace'])(
|
||||
'Plan Mode policy for %s',
|
||||
(toolName) => {
|
||||
@@ -339,6 +400,8 @@ describe('Policy Engine Integration Tests', () => {
|
||||
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/session-1/plans/my-plan.md',
|
||||
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/session-1/plans/feature_auth.md',
|
||||
'/home/user/.gemini/tmp/new-temp_dir_123/session-1/plans/plan.md', // new style of temp directory
|
||||
'C:\\Users\\user\\.gemini\\tmp\\project-id\\session-id\\plans\\plan.md',
|
||||
'D:\\gemini-cli\\.gemini\\tmp\\project-id\\session-1\\plans\\plan.md', // no session ID
|
||||
];
|
||||
|
||||
for (const file_path of validPaths) {
|
||||
@@ -364,7 +427,8 @@ describe('Policy Engine Integration Tests', () => {
|
||||
const invalidPaths = [
|
||||
'/project/src/file.ts', // Workspace
|
||||
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/script.js', // Wrong extension
|
||||
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/../../../etc/passwd.md', // Path traversal
|
||||
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/../../../etc/passwd.md', // Path traversal (Unix)
|
||||
'C:\\Users\\user\\.gemini\\tmp\\id\\session\\plans\\..\\..\\..\\Windows\\System32\\config\\SAM', // Path traversal (Windows)
|
||||
'/home/user/.gemini/non-tmp/new-temp_dir_123/plans/plan.md', // outside of temp dir
|
||||
];
|
||||
|
||||
@@ -412,25 +476,25 @@ describe('Policy Engine Integration Tests', () => {
|
||||
|
||||
// Find rules and verify their priorities
|
||||
const blockedToolRule = rules.find((r) => r.toolName === 'blocked-tool');
|
||||
expect(blockedToolRule?.priority).toBe(2.4); // Command line exclude
|
||||
expect(blockedToolRule?.priority).toBe(3.4); // Command line exclude
|
||||
|
||||
const blockedServerRule = rules.find(
|
||||
(r) => r.toolName === 'blocked-server__*',
|
||||
);
|
||||
expect(blockedServerRule?.priority).toBe(2.9); // MCP server exclude
|
||||
expect(blockedServerRule?.priority).toBe(3.9); // MCP server exclude
|
||||
|
||||
const specificToolRule = rules.find(
|
||||
(r) => r.toolName === 'specific-tool',
|
||||
);
|
||||
expect(specificToolRule?.priority).toBe(2.3); // Command line allow
|
||||
expect(specificToolRule?.priority).toBe(3.3); // Command line allow
|
||||
|
||||
const trustedServerRule = rules.find(
|
||||
(r) => r.toolName === 'trusted-server__*',
|
||||
);
|
||||
expect(trustedServerRule?.priority).toBe(2.2); // MCP trusted server
|
||||
expect(trustedServerRule?.priority).toBe(3.2); // MCP trusted server
|
||||
|
||||
const mcpServerRule = rules.find((r) => r.toolName === 'mcp-server__*');
|
||||
expect(mcpServerRule?.priority).toBe(2.1); // MCP allowed server
|
||||
expect(mcpServerRule?.priority).toBe(3.1); // MCP allowed server
|
||||
|
||||
const readOnlyToolRule = rules.find((r) => r.toolName === 'glob');
|
||||
// Priority 70 in default tier → 1.07 (Overriding Plan Mode Deny)
|
||||
@@ -577,16 +641,16 @@ describe('Policy Engine Integration Tests', () => {
|
||||
|
||||
// Verify each rule has the expected priority
|
||||
const tool3Rule = rules.find((r) => r.toolName === 'tool3');
|
||||
expect(tool3Rule?.priority).toBe(2.4); // Excluded tools (user tier)
|
||||
expect(tool3Rule?.priority).toBe(3.4); // Excluded tools (user tier)
|
||||
|
||||
const server2Rule = rules.find((r) => r.toolName === 'server2__*');
|
||||
expect(server2Rule?.priority).toBe(2.9); // Excluded servers (user tier)
|
||||
expect(server2Rule?.priority).toBe(3.9); // Excluded servers (user tier)
|
||||
|
||||
const tool1Rule = rules.find((r) => r.toolName === 'tool1');
|
||||
expect(tool1Rule?.priority).toBe(2.3); // Allowed tools (user tier)
|
||||
expect(tool1Rule?.priority).toBe(3.3); // Allowed tools (user tier)
|
||||
|
||||
const server1Rule = rules.find((r) => r.toolName === 'server1__*');
|
||||
expect(server1Rule?.priority).toBe(2.1); // Allowed servers (user tier)
|
||||
expect(server1Rule?.priority).toBe(3.1); // Allowed servers (user tier)
|
||||
|
||||
const globRule = rules.find((r) => r.toolName === 'glob');
|
||||
// Priority 70 in default tier → 1.07
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import { resolveWorkspacePolicyState } from './policy.js';
|
||||
import { writeToStderr } from '@google/gemini-cli-core';
|
||||
|
||||
// Mock debugLogger to avoid noise in test output
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
return {
|
||||
...actual,
|
||||
debugLogger: {
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
},
|
||||
writeToStderr: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('resolveWorkspacePolicyState', () => {
|
||||
let tempDir: string;
|
||||
let workspaceDir: string;
|
||||
let policiesDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a temporary directory for the test
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gemini-cli-test-'));
|
||||
// Redirect GEMINI_CLI_HOME to the temp directory to isolate integrity storage
|
||||
vi.stubEnv('GEMINI_CLI_HOME', tempDir);
|
||||
|
||||
workspaceDir = path.join(tempDir, 'workspace');
|
||||
fs.mkdirSync(workspaceDir);
|
||||
policiesDir = path.join(workspaceDir, '.gemini', 'policies');
|
||||
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up temporary directory
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it('should return empty state if folder is not trusted', async () => {
|
||||
const result = await resolveWorkspacePolicyState({
|
||||
cwd: workspaceDir,
|
||||
trustedFolder: false,
|
||||
interactive: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
workspacePoliciesDir: undefined,
|
||||
policyUpdateConfirmationRequest: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return policy directory if integrity matches', async () => {
|
||||
// Set up policies directory with a file
|
||||
fs.mkdirSync(policiesDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []');
|
||||
|
||||
// First call to establish integrity (interactive accept)
|
||||
const firstResult = await resolveWorkspacePolicyState({
|
||||
cwd: workspaceDir,
|
||||
trustedFolder: true,
|
||||
interactive: true,
|
||||
});
|
||||
expect(firstResult.policyUpdateConfirmationRequest).toBeDefined();
|
||||
|
||||
// Establish integrity manually as if accepted
|
||||
const { PolicyIntegrityManager } = await import('@google/gemini-cli-core');
|
||||
const integrityManager = new PolicyIntegrityManager();
|
||||
await integrityManager.acceptIntegrity(
|
||||
'workspace',
|
||||
workspaceDir,
|
||||
firstResult.policyUpdateConfirmationRequest!.newHash,
|
||||
);
|
||||
|
||||
// Second call should match
|
||||
const result = await resolveWorkspacePolicyState({
|
||||
cwd: workspaceDir,
|
||||
trustedFolder: true,
|
||||
interactive: true,
|
||||
});
|
||||
|
||||
expect(result.workspacePoliciesDir).toBe(policiesDir);
|
||||
expect(result.policyUpdateConfirmationRequest).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined if integrity is NEW but fileCount is 0', async () => {
|
||||
const result = await resolveWorkspacePolicyState({
|
||||
cwd: workspaceDir,
|
||||
trustedFolder: true,
|
||||
interactive: true,
|
||||
});
|
||||
|
||||
expect(result.workspacePoliciesDir).toBeUndefined();
|
||||
expect(result.policyUpdateConfirmationRequest).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return confirmation request if changed in interactive mode', async () => {
|
||||
fs.mkdirSync(policiesDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []');
|
||||
|
||||
const result = await resolveWorkspacePolicyState({
|
||||
cwd: workspaceDir,
|
||||
trustedFolder: true,
|
||||
interactive: true,
|
||||
});
|
||||
|
||||
expect(result.workspacePoliciesDir).toBeUndefined();
|
||||
expect(result.policyUpdateConfirmationRequest).toEqual({
|
||||
scope: 'workspace',
|
||||
identifier: workspaceDir,
|
||||
policyDir: policiesDir,
|
||||
newHash: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('should warn and auto-accept if changed in non-interactive mode', async () => {
|
||||
fs.mkdirSync(policiesDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []');
|
||||
|
||||
const result = await resolveWorkspacePolicyState({
|
||||
cwd: workspaceDir,
|
||||
trustedFolder: true,
|
||||
interactive: false,
|
||||
});
|
||||
|
||||
expect(result.workspacePoliciesDir).toBe(policiesDir);
|
||||
expect(result.policyUpdateConfirmationRequest).toBeUndefined();
|
||||
expect(writeToStderr).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Automatically accepting and loading'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not return workspace policies if cwd is the home directory', async () => {
|
||||
const policiesDir = path.join(tempDir, '.gemini', 'policies');
|
||||
fs.mkdirSync(policiesDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []');
|
||||
|
||||
// Run from HOME directory (tempDir is mocked as HOME in beforeEach)
|
||||
const result = await resolveWorkspacePolicyState({
|
||||
cwd: tempDir,
|
||||
trustedFolder: true,
|
||||
interactive: true,
|
||||
});
|
||||
|
||||
expect(result.workspacePoliciesDir).toBeUndefined();
|
||||
expect(result.policyUpdateConfirmationRequest).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not return workspace policies if cwd is a symlink to the home directory', async () => {
|
||||
const policiesDir = path.join(tempDir, '.gemini', 'policies');
|
||||
fs.mkdirSync(policiesDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []');
|
||||
|
||||
// Create a symlink to the home directory
|
||||
const symlinkDir = path.join(
|
||||
os.tmpdir(),
|
||||
`gemini-cli-symlink-${Date.now()}`,
|
||||
);
|
||||
fs.symlinkSync(tempDir, symlinkDir, 'dir');
|
||||
|
||||
try {
|
||||
// Run from symlink to HOME directory
|
||||
const result = await resolveWorkspacePolicyState({
|
||||
cwd: symlinkDir,
|
||||
trustedFolder: true,
|
||||
interactive: true,
|
||||
});
|
||||
|
||||
expect(result.workspacePoliciesDir).toBeUndefined();
|
||||
expect(result.policyUpdateConfirmationRequest).toBeUndefined();
|
||||
} finally {
|
||||
// Clean up symlink
|
||||
fs.unlinkSync(symlinkDir);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -12,12 +12,18 @@ import {
|
||||
type PolicySettings,
|
||||
createPolicyEngineConfig as createCorePolicyEngineConfig,
|
||||
createPolicyUpdater as createCorePolicyUpdater,
|
||||
PolicyIntegrityManager,
|
||||
IntegrityStatus,
|
||||
Storage,
|
||||
type PolicyUpdateConfirmationRequest,
|
||||
writeToStderr,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { type Settings } from './settings.js';
|
||||
|
||||
export async function createPolicyEngineConfig(
|
||||
settings: Settings,
|
||||
approvalMode: ApprovalMode,
|
||||
workspacePoliciesDir?: string,
|
||||
): Promise<PolicyEngineConfig> {
|
||||
// Explicitly construct PolicySettings from Settings to ensure type safety
|
||||
// and avoid accidental leakage of other settings properties.
|
||||
@@ -26,6 +32,7 @@ export async function createPolicyEngineConfig(
|
||||
tools: settings.tools,
|
||||
mcpServers: settings.mcpServers,
|
||||
policyPaths: settings.policyPaths,
|
||||
workspacePoliciesDir,
|
||||
};
|
||||
|
||||
return createCorePolicyEngineConfig(policySettings, approvalMode);
|
||||
@@ -34,6 +41,78 @@ export async function createPolicyEngineConfig(
|
||||
export function createPolicyUpdater(
|
||||
policyEngine: PolicyEngine,
|
||||
messageBus: MessageBus,
|
||||
storage: Storage,
|
||||
) {
|
||||
return createCorePolicyUpdater(policyEngine, messageBus);
|
||||
return createCorePolicyUpdater(policyEngine, messageBus, storage);
|
||||
}
|
||||
|
||||
export interface WorkspacePolicyState {
|
||||
workspacePoliciesDir?: string;
|
||||
policyUpdateConfirmationRequest?: PolicyUpdateConfirmationRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the workspace policy state by checking folder trust and policy integrity.
|
||||
*/
|
||||
export async function resolveWorkspacePolicyState(options: {
|
||||
cwd: string;
|
||||
trustedFolder: boolean;
|
||||
interactive: boolean;
|
||||
}): Promise<WorkspacePolicyState> {
|
||||
const { cwd, trustedFolder, interactive } = options;
|
||||
|
||||
let workspacePoliciesDir: string | undefined;
|
||||
let policyUpdateConfirmationRequest:
|
||||
| PolicyUpdateConfirmationRequest
|
||||
| undefined;
|
||||
|
||||
if (trustedFolder) {
|
||||
const storage = new Storage(cwd);
|
||||
|
||||
// If we are in the home directory (or rather, our target Gemini dir is the global one),
|
||||
// don't treat it as a workspace to avoid loading global policies twice.
|
||||
if (storage.isWorkspaceHomeDir()) {
|
||||
return { workspacePoliciesDir: undefined };
|
||||
}
|
||||
|
||||
const potentialWorkspacePoliciesDir = storage.getWorkspacePoliciesDir();
|
||||
const integrityManager = new PolicyIntegrityManager();
|
||||
const integrityResult = await integrityManager.checkIntegrity(
|
||||
'workspace',
|
||||
cwd,
|
||||
potentialWorkspacePoliciesDir,
|
||||
);
|
||||
|
||||
if (integrityResult.status === IntegrityStatus.MATCH) {
|
||||
workspacePoliciesDir = potentialWorkspacePoliciesDir;
|
||||
} else if (
|
||||
integrityResult.status === IntegrityStatus.NEW &&
|
||||
integrityResult.fileCount === 0
|
||||
) {
|
||||
// No workspace policies found
|
||||
workspacePoliciesDir = undefined;
|
||||
} else if (interactive) {
|
||||
// Policies changed or are new, and we are in interactive mode
|
||||
policyUpdateConfirmationRequest = {
|
||||
scope: 'workspace',
|
||||
identifier: cwd,
|
||||
policyDir: potentialWorkspacePoliciesDir,
|
||||
newHash: integrityResult.hash,
|
||||
};
|
||||
} else {
|
||||
// Non-interactive mode: warn and automatically accept/load
|
||||
await integrityManager.acceptIntegrity(
|
||||
'workspace',
|
||||
cwd,
|
||||
integrityResult.hash,
|
||||
);
|
||||
workspacePoliciesDir = potentialWorkspacePoliciesDir;
|
||||
// debugLogger.warn here doesn't show up in the terminal. It is showing up only in debug mode on the debug console
|
||||
writeToStderr(
|
||||
'WARNING: Workspace policies changed or are new. Automatically accepting and loading them in non-interactive mode.\n',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return { workspacePoliciesDir, policyUpdateConfirmationRequest };
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ import {
|
||||
import {
|
||||
FatalConfigError,
|
||||
GEMINI_DIR,
|
||||
Storage,
|
||||
type MCPServerConfig,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { updateSettingsFilePreservingFormat } from '../utils/commentJson.js';
|
||||
@@ -126,6 +127,30 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
const os = await import('node:os');
|
||||
const pathMod = await import('node:path');
|
||||
const fsMod = await import('node:fs');
|
||||
|
||||
// Helper to resolve paths using the test's mocked environment
|
||||
const testResolve = (p: string | undefined) => {
|
||||
if (!p) return '';
|
||||
try {
|
||||
// Use the mocked fs.realpathSync if available, otherwise fallback
|
||||
return fsMod.realpathSync(pathMod.resolve(p));
|
||||
} catch {
|
||||
return pathMod.resolve(p);
|
||||
}
|
||||
};
|
||||
|
||||
// Create a smarter mock for isWorkspaceHomeDir
|
||||
vi.spyOn(actual.Storage.prototype, 'isWorkspaceHomeDir').mockImplementation(
|
||||
function (this: Storage) {
|
||||
const target = testResolve(pathMod.dirname(this.getGeminiDir()));
|
||||
// Pick up the mocked home directory specifically from the 'os' mock
|
||||
const home = testResolve(os.homedir());
|
||||
return actual.normalizePath(target) === actual.normalizePath(home);
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
...actual,
|
||||
coreEvents: mockCoreEvents,
|
||||
@@ -1491,20 +1516,29 @@ describe('Settings Loading and Merging', () => {
|
||||
return pStr;
|
||||
});
|
||||
|
||||
// Force the storage check to return true for this specific test
|
||||
const isWorkspaceHomeDirSpy = vi
|
||||
.spyOn(Storage.prototype, 'isWorkspaceHomeDir')
|
||||
.mockReturnValue(true);
|
||||
|
||||
(mockFsExistsSync as Mock).mockImplementation(
|
||||
(p: string) =>
|
||||
// Only return true for workspace settings path to see if it gets loaded
|
||||
p === mockWorkspaceSettingsPath,
|
||||
);
|
||||
|
||||
const settings = loadSettings(mockSymlinkDir);
|
||||
try {
|
||||
const settings = loadSettings(mockSymlinkDir);
|
||||
|
||||
// Verify that even though the file exists, it was NOT loaded because realpath matched home
|
||||
expect(fs.readFileSync).not.toHaveBeenCalledWith(
|
||||
mockWorkspaceSettingsPath,
|
||||
'utf-8',
|
||||
);
|
||||
expect(settings.workspace.settings).toEqual({});
|
||||
// Verify that even though the file exists, it was NOT loaded because realpath matched home
|
||||
expect(fs.readFileSync).not.toHaveBeenCalledWith(
|
||||
mockWorkspaceSettingsPath,
|
||||
'utf-8',
|
||||
);
|
||||
expect(settings.workspace.settings).toEqual({});
|
||||
} finally {
|
||||
isWorkspaceHomeDirSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2032,6 +2066,85 @@ describe('Settings Loading and Merging', () => {
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
// Check that enableLoadingPhrases: false was further migrated to loadingPhrases: 'off'
|
||||
expect(setValueSpy).toHaveBeenCalledWith(
|
||||
SettingScope.User,
|
||||
'ui',
|
||||
expect.objectContaining({
|
||||
loadingPhrases: 'off',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should migrate enableLoadingPhrases: false to loadingPhrases: off', () => {
|
||||
const userSettingsContent = {
|
||||
ui: {
|
||||
accessibility: {
|
||||
enableLoadingPhrases: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const loadedSettings = createMockSettings(userSettingsContent);
|
||||
const setValueSpy = vi.spyOn(loadedSettings, 'setValue');
|
||||
|
||||
migrateDeprecatedSettings(loadedSettings);
|
||||
|
||||
expect(setValueSpy).toHaveBeenCalledWith(
|
||||
SettingScope.User,
|
||||
'ui',
|
||||
expect.objectContaining({
|
||||
loadingPhrases: 'off',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not migrate enableLoadingPhrases: true to loadingPhrases', () => {
|
||||
const userSettingsContent = {
|
||||
ui: {
|
||||
accessibility: {
|
||||
enableLoadingPhrases: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const loadedSettings = createMockSettings(userSettingsContent);
|
||||
const setValueSpy = vi.spyOn(loadedSettings, 'setValue');
|
||||
|
||||
migrateDeprecatedSettings(loadedSettings);
|
||||
|
||||
// Should not set loadingPhrases when enableLoadingPhrases is true
|
||||
const uiCalls = setValueSpy.mock.calls.filter((call) => call[1] === 'ui');
|
||||
for (const call of uiCalls) {
|
||||
const uiValue = call[2] as Record<string, unknown>;
|
||||
expect(uiValue).not.toHaveProperty('loadingPhrases');
|
||||
}
|
||||
});
|
||||
|
||||
it('should not overwrite existing loadingPhrases during migration', () => {
|
||||
const userSettingsContent = {
|
||||
ui: {
|
||||
loadingPhrases: 'witty',
|
||||
accessibility: {
|
||||
enableLoadingPhrases: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const loadedSettings = createMockSettings(userSettingsContent);
|
||||
const setValueSpy = vi.spyOn(loadedSettings, 'setValue');
|
||||
|
||||
migrateDeprecatedSettings(loadedSettings);
|
||||
|
||||
// Should not overwrite existing loadingPhrases
|
||||
const uiCalls = setValueSpy.mock.calls.filter((call) => call[1] === 'ui');
|
||||
for (const call of uiCalls) {
|
||||
const uiValue = call[2] as Record<string, unknown>;
|
||||
if (uiValue['loadingPhrases'] !== undefined) {
|
||||
expect(uiValue['loadingPhrases']).toBe('witty');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should prioritize new settings over deprecated ones and respect removeDeprecated flag', () => {
|
||||
|
||||
@@ -165,7 +165,10 @@ export interface SummarizeToolOutputSettings {
|
||||
tokenBudget?: number;
|
||||
}
|
||||
|
||||
export type LoadingPhrasesMode = 'tips' | 'witty' | 'all' | 'off';
|
||||
|
||||
export interface AccessibilitySettings {
|
||||
/** @deprecated Use ui.loadingPhrases instead. */
|
||||
enableLoadingPhrases?: boolean;
|
||||
screenReader?: boolean;
|
||||
}
|
||||
@@ -634,24 +637,8 @@ export function loadSettings(
|
||||
const systemSettingsPath = getSystemSettingsPath();
|
||||
const systemDefaultsPath = getSystemDefaultsPath();
|
||||
|
||||
// Resolve paths to their canonical representation to handle symlinks
|
||||
const resolvedWorkspaceDir = path.resolve(workspaceDir);
|
||||
const resolvedHomeDir = path.resolve(homedir());
|
||||
|
||||
let realWorkspaceDir = resolvedWorkspaceDir;
|
||||
try {
|
||||
// fs.realpathSync gets the "true" path, resolving any symlinks
|
||||
realWorkspaceDir = fs.realpathSync(resolvedWorkspaceDir);
|
||||
} catch (_e) {
|
||||
// This is okay. The path might not exist yet, and that's a valid state.
|
||||
}
|
||||
|
||||
// We expect homedir to always exist and be resolvable.
|
||||
const realHomeDir = fs.realpathSync(resolvedHomeDir);
|
||||
|
||||
const workspaceSettingsPath = new Storage(
|
||||
workspaceDir,
|
||||
).getWorkspaceSettingsPath();
|
||||
const storage = new Storage(workspaceDir);
|
||||
const workspaceSettingsPath = storage.getWorkspaceSettingsPath();
|
||||
|
||||
const load = (filePath: string): { settings: Settings; rawJson?: string } => {
|
||||
try {
|
||||
@@ -709,7 +696,7 @@ export function loadSettings(
|
||||
settings: {} as Settings,
|
||||
rawJson: undefined,
|
||||
};
|
||||
if (realWorkspaceDir !== realHomeDir) {
|
||||
if (!storage.isWorkspaceHomeDir()) {
|
||||
workspaceResult = load(workspaceSettingsPath);
|
||||
}
|
||||
|
||||
@@ -797,11 +784,11 @@ export function loadSettings(
|
||||
readOnly: false,
|
||||
},
|
||||
{
|
||||
path: realWorkspaceDir === realHomeDir ? '' : workspaceSettingsPath,
|
||||
path: storage.isWorkspaceHomeDir() ? '' : workspaceSettingsPath,
|
||||
settings: workspaceSettings,
|
||||
originalSettings: workspaceOriginalSettings,
|
||||
rawJson: workspaceResult.rawJson,
|
||||
readOnly: realWorkspaceDir === realHomeDir,
|
||||
readOnly: storage.isWorkspaceHomeDir(),
|
||||
},
|
||||
isTrusted,
|
||||
settingsErrors,
|
||||
@@ -928,6 +915,22 @@ export function migrateDeprecatedSettings(
|
||||
anyModified = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate enableLoadingPhrases: false → loadingPhrases: 'off'
|
||||
const enableLP = newAccessibility['enableLoadingPhrases'];
|
||||
if (
|
||||
typeof enableLP === 'boolean' &&
|
||||
newUi['loadingPhrases'] === undefined
|
||||
) {
|
||||
if (!enableLP) {
|
||||
newUi['loadingPhrases'] = 'off';
|
||||
loadedSettings.setValue(scope, 'ui', newUi);
|
||||
if (!settingsFile.readOnly) {
|
||||
anyModified = true;
|
||||
}
|
||||
}
|
||||
foundDeprecated.push('ui.accessibility.enableLoadingPhrases');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -83,6 +83,19 @@ describe('SettingsSchema', () => {
|
||||
).toBe('boolean');
|
||||
});
|
||||
|
||||
it('should have loadingPhrases enum property', () => {
|
||||
const definition = getSettingsSchema().ui?.properties?.loadingPhrases;
|
||||
expect(definition).toBeDefined();
|
||||
expect(definition?.type).toBe('enum');
|
||||
expect(definition?.default).toBe('tips');
|
||||
expect(definition?.options?.map((o) => o.value)).toEqual([
|
||||
'tips',
|
||||
'witty',
|
||||
'all',
|
||||
'off',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should have checkpointing nested properties', () => {
|
||||
expect(
|
||||
getSettingsSchema().general?.properties?.checkpointing.properties
|
||||
@@ -94,6 +107,16 @@ describe('SettingsSchema', () => {
|
||||
).toBe('boolean');
|
||||
});
|
||||
|
||||
it('should have plan nested properties', () => {
|
||||
expect(
|
||||
getSettingsSchema().general?.properties?.plan?.properties?.directory,
|
||||
).toBeDefined();
|
||||
expect(
|
||||
getSettingsSchema().general?.properties?.plan?.properties?.directory
|
||||
.type,
|
||||
).toBe('string');
|
||||
});
|
||||
|
||||
it('should have fileFiltering nested properties', () => {
|
||||
expect(
|
||||
getSettingsSchema().context.properties.fileFiltering.properties
|
||||
|
||||
@@ -266,15 +266,26 @@ const SETTINGS_SCHEMA = {
|
||||
},
|
||||
},
|
||||
},
|
||||
enablePromptCompletion: {
|
||||
type: 'boolean',
|
||||
label: 'Enable Prompt Completion',
|
||||
plan: {
|
||||
type: 'object',
|
||||
label: 'Plan',
|
||||
category: 'General',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description:
|
||||
'Enable AI-powered prompt completion suggestions while typing.',
|
||||
showInDialog: true,
|
||||
default: {},
|
||||
description: 'Planning features configuration.',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
directory: {
|
||||
type: 'string',
|
||||
label: 'Plan Directory',
|
||||
category: 'General',
|
||||
requiresRestart: true,
|
||||
default: undefined as string | undefined,
|
||||
description:
|
||||
'The directory where planning artifacts are stored. If not specified, defaults to the system temporary directory.',
|
||||
showInDialog: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
retryFetchErrors: {
|
||||
type: 'boolean',
|
||||
@@ -286,6 +297,16 @@ const SETTINGS_SCHEMA = {
|
||||
'Retry on "exception TypeError: fetch failed sending request" errors.',
|
||||
showInDialog: false,
|
||||
},
|
||||
maxAttempts: {
|
||||
type: 'number',
|
||||
label: 'Max Chat Model Attempts',
|
||||
category: 'General',
|
||||
requiresRestart: false,
|
||||
default: 10,
|
||||
description:
|
||||
'Maximum number of attempts for requests to the main chat model. Cannot exceed 10.',
|
||||
showInDialog: true,
|
||||
},
|
||||
debugKeystrokeLogging: {
|
||||
type: 'boolean',
|
||||
label: 'Debug Keystroke Logging',
|
||||
@@ -672,6 +693,22 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'Show the spinner during operations.',
|
||||
showInDialog: true,
|
||||
},
|
||||
loadingPhrases: {
|
||||
type: 'enum',
|
||||
label: 'Loading Phrases',
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: 'tips',
|
||||
description:
|
||||
'What to show while the model is working: tips, witty comments, both, or nothing.',
|
||||
showInDialog: true,
|
||||
options: [
|
||||
{ value: 'tips', label: 'Tips' },
|
||||
{ value: 'witty', label: 'Witty' },
|
||||
{ value: 'all', label: 'All' },
|
||||
{ value: 'off', label: 'Off' },
|
||||
],
|
||||
},
|
||||
customWittyPhrases: {
|
||||
type: 'array',
|
||||
label: 'Custom Witty Phrases',
|
||||
@@ -700,8 +737,9 @@ const SETTINGS_SCHEMA = {
|
||||
category: 'UI',
|
||||
requiresRestart: true,
|
||||
default: true,
|
||||
description: 'Enable loading phrases during operations.',
|
||||
showInDialog: true,
|
||||
description:
|
||||
'@deprecated Use ui.loadingPhrases instead. Enable loading phrases during operations.',
|
||||
showInDialog: false,
|
||||
},
|
||||
screenReader: {
|
||||
type: 'boolean',
|
||||
@@ -1296,6 +1334,7 @@ const SETTINGS_SCHEMA = {
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
useWriteTodos: {
|
||||
type: 'boolean',
|
||||
label: 'Use WriteTodos',
|
||||
@@ -1632,7 +1671,17 @@ const SETTINGS_SCHEMA = {
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description:
|
||||
'Use OSC 52 sequence for pasting instead of clipboardy (useful for remote sessions).',
|
||||
'Use OSC 52 for pasting. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it).',
|
||||
showInDialog: true,
|
||||
},
|
||||
useOSC52Copy: {
|
||||
type: 'boolean',
|
||||
label: 'Use OSC 52 Copy',
|
||||
category: 'Experimental',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description:
|
||||
'Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it).',
|
||||
showInDialog: true,
|
||||
},
|
||||
plan: {
|
||||
@@ -1654,6 +1703,16 @@ const SETTINGS_SCHEMA = {
|
||||
'Enable model steering (user hints) to guide the model during tool execution.',
|
||||
showInDialog: true,
|
||||
},
|
||||
directWebFetch: {
|
||||
type: 'boolean',
|
||||
label: 'Direct Web Fetch',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description:
|
||||
'Enable web fetch behavior that bypasses LLM summarization.',
|
||||
showInDialog: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -131,7 +131,6 @@ describe('Settings Repro', () => {
|
||||
},
|
||||
general: {
|
||||
debugKeystrokeLogging: false,
|
||||
enablePromptCompletion: false,
|
||||
preferredEditor: 'vim',
|
||||
vimMode: false,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import * as path from 'node:path';
|
||||
import { loadCliConfig, type CliArgs } from './config.js';
|
||||
import { createTestMergedSettings } from './settings.js';
|
||||
import * as ServerConfig from '@google/gemini-cli-core';
|
||||
import { isWorkspaceTrusted } from './trustedFolders.js';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('./trustedFolders.js', () => ({
|
||||
isWorkspaceTrusted: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockCheckIntegrity = vi.fn();
|
||||
const mockAcceptIntegrity = vi.fn();
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async () => {
|
||||
const actual = await vi.importActual<typeof ServerConfig>(
|
||||
'@google/gemini-cli-core',
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
loadServerHierarchicalMemory: vi.fn().mockResolvedValue({
|
||||
memoryContent: '',
|
||||
fileCount: 0,
|
||||
filePaths: [],
|
||||
}),
|
||||
createPolicyEngineConfig: vi.fn().mockResolvedValue({
|
||||
rules: [],
|
||||
checkers: [],
|
||||
}),
|
||||
getVersion: vi.fn().mockResolvedValue('test-version'),
|
||||
PolicyIntegrityManager: vi.fn().mockImplementation(() => ({
|
||||
checkIntegrity: mockCheckIntegrity,
|
||||
acceptIntegrity: mockAcceptIntegrity,
|
||||
})),
|
||||
IntegrityStatus: { MATCH: 'match', NEW: 'new', MISMATCH: 'mismatch' },
|
||||
debugLogger: {
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
isHeadlessMode: vi.fn().mockReturnValue(false), // Default to interactive
|
||||
};
|
||||
});
|
||||
|
||||
describe('Workspace-Level Policy CLI Integration', () => {
|
||||
const MOCK_CWD = process.cwd();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Default to MATCH for existing tests
|
||||
mockCheckIntegrity.mockResolvedValue({
|
||||
status: 'match',
|
||||
hash: 'test-hash',
|
||||
fileCount: 1,
|
||||
});
|
||||
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(false);
|
||||
});
|
||||
|
||||
it('should have getWorkspacePoliciesDir on Storage class', () => {
|
||||
const storage = new ServerConfig.Storage(MOCK_CWD);
|
||||
expect(storage.getWorkspacePoliciesDir).toBeDefined();
|
||||
expect(typeof storage.getWorkspacePoliciesDir).toBe('function');
|
||||
});
|
||||
|
||||
it('should pass workspacePoliciesDir to createPolicyEngineConfig when folder is trusted', async () => {
|
||||
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
||||
isTrusted: true,
|
||||
source: 'file',
|
||||
});
|
||||
|
||||
const settings = createTestMergedSettings();
|
||||
const argv = { query: 'test' } as unknown as CliArgs;
|
||||
|
||||
await loadCliConfig(settings, 'test-session', argv, { cwd: MOCK_CWD });
|
||||
|
||||
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
workspacePoliciesDir: expect.stringContaining(
|
||||
path.join('.gemini', 'policies'),
|
||||
),
|
||||
}),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should NOT pass workspacePoliciesDir to createPolicyEngineConfig when folder is NOT trusted', async () => {
|
||||
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
||||
isTrusted: false,
|
||||
source: 'file',
|
||||
});
|
||||
|
||||
const settings = createTestMergedSettings();
|
||||
const argv = { query: 'test' } as unknown as CliArgs;
|
||||
|
||||
await loadCliConfig(settings, 'test-session', argv, { cwd: MOCK_CWD });
|
||||
|
||||
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
workspacePoliciesDir: undefined,
|
||||
}),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should NOT pass workspacePoliciesDir if integrity is NEW but fileCount is 0', async () => {
|
||||
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
||||
isTrusted: true,
|
||||
source: 'file',
|
||||
});
|
||||
mockCheckIntegrity.mockResolvedValue({
|
||||
status: 'new',
|
||||
hash: 'hash',
|
||||
fileCount: 0,
|
||||
});
|
||||
|
||||
const settings = createTestMergedSettings();
|
||||
const argv = { query: 'test' } as unknown as CliArgs;
|
||||
|
||||
await loadCliConfig(settings, 'test-session', argv, { cwd: MOCK_CWD });
|
||||
|
||||
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
workspacePoliciesDir: undefined,
|
||||
}),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should automatically accept and load workspacePoliciesDir if integrity MISMATCH in non-interactive mode', async () => {
|
||||
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
||||
isTrusted: true,
|
||||
source: 'file',
|
||||
});
|
||||
mockCheckIntegrity.mockResolvedValue({
|
||||
status: 'mismatch',
|
||||
hash: 'new-hash',
|
||||
fileCount: 1,
|
||||
});
|
||||
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(true); // Non-interactive
|
||||
|
||||
const settings = createTestMergedSettings();
|
||||
const argv = { prompt: 'do something' } as unknown as CliArgs;
|
||||
|
||||
await loadCliConfig(settings, 'test-session', argv, { cwd: MOCK_CWD });
|
||||
|
||||
expect(mockAcceptIntegrity).toHaveBeenCalledWith(
|
||||
'workspace',
|
||||
MOCK_CWD,
|
||||
'new-hash',
|
||||
);
|
||||
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
workspacePoliciesDir: expect.stringContaining(
|
||||
path.join('.gemini', 'policies'),
|
||||
),
|
||||
}),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should set policyUpdateConfirmationRequest if integrity MISMATCH in interactive mode', async () => {
|
||||
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
||||
isTrusted: true,
|
||||
source: 'file',
|
||||
});
|
||||
mockCheckIntegrity.mockResolvedValue({
|
||||
status: 'mismatch',
|
||||
hash: 'new-hash',
|
||||
fileCount: 1,
|
||||
});
|
||||
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(false); // Interactive
|
||||
|
||||
const settings = createTestMergedSettings();
|
||||
const argv = {
|
||||
query: 'test',
|
||||
promptInteractive: 'test',
|
||||
} as unknown as CliArgs;
|
||||
|
||||
const config = await loadCliConfig(settings, 'test-session', argv, {
|
||||
cwd: MOCK_CWD,
|
||||
});
|
||||
|
||||
expect(config.getPolicyUpdateConfirmationRequest()).toEqual({
|
||||
scope: 'workspace',
|
||||
identifier: MOCK_CWD,
|
||||
policyDir: expect.stringContaining(path.join('.gemini', 'policies')),
|
||||
newHash: 'new-hash',
|
||||
});
|
||||
// In interactive mode without accept flag, it waits for user confirmation (handled by UI),
|
||||
// so it currently DOES NOT pass the directory to createPolicyEngineConfig yet.
|
||||
// The UI will handle the confirmation and reload/update.
|
||||
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
workspacePoliciesDir: undefined,
|
||||
}),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should set policyUpdateConfirmationRequest if integrity is NEW with files (first time seen) in interactive mode', async () => {
|
||||
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
||||
isTrusted: true,
|
||||
source: 'file',
|
||||
});
|
||||
mockCheckIntegrity.mockResolvedValue({
|
||||
status: 'new',
|
||||
hash: 'new-hash',
|
||||
fileCount: 5,
|
||||
});
|
||||
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(false); // Interactive
|
||||
|
||||
const settings = createTestMergedSettings();
|
||||
const argv = { query: 'test' } as unknown as CliArgs;
|
||||
|
||||
const config = await loadCliConfig(settings, 'test-session', argv, {
|
||||
cwd: MOCK_CWD,
|
||||
});
|
||||
|
||||
expect(config.getPolicyUpdateConfirmationRequest()).toEqual({
|
||||
scope: 'workspace',
|
||||
identifier: MOCK_CWD,
|
||||
policyDir: expect.stringContaining(path.join('.gemini', 'policies')),
|
||||
newHash: 'new-hash',
|
||||
});
|
||||
|
||||
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
workspacePoliciesDir: undefined,
|
||||
}),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -38,6 +38,8 @@ import { appEvents, AppEvent } from './utils/events.js';
|
||||
import {
|
||||
type Config,
|
||||
type ResumedSessionData,
|
||||
type StartupWarning,
|
||||
WarningPriority,
|
||||
debugLogger,
|
||||
coreEvents,
|
||||
AuthType,
|
||||
@@ -1193,7 +1195,9 @@ describe('startInteractiveUI', () => {
|
||||
},
|
||||
},
|
||||
} as LoadedSettings;
|
||||
const mockStartupWarnings = ['warning1'];
|
||||
const mockStartupWarnings: StartupWarning[] = [
|
||||
{ id: 'w1', message: 'warning1', priority: WarningPriority.High },
|
||||
];
|
||||
const mockWorkspaceRoot = '/root';
|
||||
const mockInitializationResult = {
|
||||
authError: null,
|
||||
@@ -1226,7 +1230,7 @@ describe('startInteractiveUI', () => {
|
||||
async function startTestInteractiveUI(
|
||||
config: Config,
|
||||
settings: LoadedSettings,
|
||||
startupWarnings: string[],
|
||||
startupWarnings: StartupWarning[],
|
||||
workspaceRoot: string,
|
||||
resumedSessionData: ResumedSessionData | undefined,
|
||||
initializationResult: InitializationResult,
|
||||
|
||||
+33
-16
@@ -11,6 +11,7 @@ import { loadCliConfig, parseArguments } from './config/config.js';
|
||||
import * as cliConfig from './config/config.js';
|
||||
import { readStdin } from './utils/readStdin.js';
|
||||
import { basename } from 'node:path';
|
||||
import { createHash } from 'node:crypto';
|
||||
import v8 from 'node:v8';
|
||||
import os from 'node:os';
|
||||
import dns from 'node:dns';
|
||||
@@ -37,6 +38,8 @@ import {
|
||||
cleanupExpiredSessions,
|
||||
} from './utils/sessionCleanup.js';
|
||||
import {
|
||||
type StartupWarning,
|
||||
WarningPriority,
|
||||
type Config,
|
||||
type ResumedSessionData,
|
||||
type OutputPayload,
|
||||
@@ -99,6 +102,7 @@ import { createPolicyUpdater } from './config/policy.js';
|
||||
import { ScrollProvider } from './ui/contexts/ScrollProvider.js';
|
||||
import { isAlternateBufferEnabled } from './ui/hooks/useAlternateBuffer.js';
|
||||
import { TerminalProvider } from './ui/contexts/TerminalContext.js';
|
||||
import { OverflowProvider } from './ui/contexts/OverflowContext.js';
|
||||
|
||||
import { setupTerminalAndTheme } from './utils/terminalTheme.js';
|
||||
import { profiler } from './ui/components/DebugProfiler.js';
|
||||
@@ -180,7 +184,7 @@ ${reason.stack}`
|
||||
export async function startInteractiveUI(
|
||||
config: Config,
|
||||
settings: LoadedSettings,
|
||||
startupWarnings: string[],
|
||||
startupWarnings: StartupWarning[],
|
||||
workspaceRoot: string = process.cwd(),
|
||||
resumedSessionData: ResumedSessionData | undefined,
|
||||
initializationResult: InitializationResult,
|
||||
@@ -235,17 +239,19 @@ export async function startInteractiveUI(
|
||||
>
|
||||
<TerminalProvider>
|
||||
<ScrollProvider>
|
||||
<SessionStatsProvider>
|
||||
<VimModeProvider settings={settings}>
|
||||
<AppContainer
|
||||
config={config}
|
||||
startupWarnings={startupWarnings}
|
||||
version={version}
|
||||
resumedSessionData={resumedSessionData}
|
||||
initializationResult={initializationResult}
|
||||
/>
|
||||
</VimModeProvider>
|
||||
</SessionStatsProvider>
|
||||
<OverflowProvider>
|
||||
<SessionStatsProvider>
|
||||
<VimModeProvider settings={settings}>
|
||||
<AppContainer
|
||||
config={config}
|
||||
startupWarnings={startupWarnings}
|
||||
version={version}
|
||||
resumedSessionData={resumedSessionData}
|
||||
initializationResult={initializationResult}
|
||||
/>
|
||||
</VimModeProvider>
|
||||
</SessionStatsProvider>
|
||||
</OverflowProvider>
|
||||
</ScrollProvider>
|
||||
</TerminalProvider>
|
||||
</MouseProvider>
|
||||
@@ -580,7 +586,7 @@ export async function main() {
|
||||
|
||||
const policyEngine = config.getPolicyEngine();
|
||||
const messageBus = config.getMessageBus();
|
||||
createPolicyUpdater(policyEngine, messageBus);
|
||||
createPolicyUpdater(policyEngine, messageBus, config.storage);
|
||||
|
||||
// Register SessionEnd hook to fire on graceful exit
|
||||
// This runs before telemetry shutdown in runExitCleanup()
|
||||
@@ -668,9 +674,20 @@ export async function main() {
|
||||
}
|
||||
|
||||
let input = config.getQuestion();
|
||||
const startupWarnings = [
|
||||
...(await getStartupWarnings()),
|
||||
...(await getUserStartupWarnings(settings.merged)),
|
||||
const useAlternateBuffer = shouldEnterAlternateScreen(
|
||||
isAlternateBufferEnabled(settings),
|
||||
config.getScreenReader(),
|
||||
);
|
||||
const rawStartupWarnings = await getStartupWarnings();
|
||||
const startupWarnings: StartupWarning[] = [
|
||||
...rawStartupWarnings.map((message) => ({
|
||||
id: `startup-${createHash('sha256').update(message).digest('hex').substring(0, 16)}`,
|
||||
message,
|
||||
priority: WarningPriority.High,
|
||||
})),
|
||||
...(await getUserStartupWarnings(settings.merged, undefined, {
|
||||
isAlternateBuffer: useAlternateBuffer,
|
||||
})),
|
||||
];
|
||||
|
||||
// Handle --resume flag
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
import { isSlashCommand } from './ui/utils/commandUtils.js';
|
||||
import type { LoadedSettings } from './config/settings.js';
|
||||
import {
|
||||
convertSessionToClientHistory,
|
||||
GeminiEventType,
|
||||
FatalInputError,
|
||||
promptIdContext,
|
||||
@@ -35,7 +36,6 @@ import type { Content, Part } from '@google/genai';
|
||||
import readline from 'node:readline';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
|
||||
import { convertSessionToHistoryFormats } from './ui/hooks/useSessionBrowser.js';
|
||||
import { handleSlashCommand } from './nonInteractiveCliCommands.js';
|
||||
import { ConsolePatcher } from './ui/utils/ConsolePatcher.js';
|
||||
import { handleAtCommand } from './ui/hooks/atCommandProcessor.js';
|
||||
@@ -220,9 +220,9 @@ export async function runNonInteractive({
|
||||
// Initialize chat. Resume if resume data is passed.
|
||||
if (resumedSessionData) {
|
||||
await geminiClient.resumeChat(
|
||||
convertSessionToHistoryFormats(
|
||||
convertSessionToClientHistory(
|
||||
resumedSessionData.conversation.messages,
|
||||
).clientHistory,
|
||||
),
|
||||
resumedSessionData,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -217,13 +217,14 @@ export class AppRig {
|
||||
}
|
||||
|
||||
private stubRefreshAuth() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
|
||||
const gcConfig = this.config as any;
|
||||
gcConfig.refreshAuth = async (authMethod: AuthType) => {
|
||||
gcConfig.modelAvailabilityService.reset();
|
||||
|
||||
const newContentGeneratorConfig = {
|
||||
authType: authMethod,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
proxy: gcConfig.getProxy(),
|
||||
apiKey: process.env['GEMINI_API_KEY'] || 'test-api-key',
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ import type { TextBuffer } from '../ui/components/shared/text-buffer.js';
|
||||
const invalidCharsRegex = /[\b\x1b]/;
|
||||
|
||||
function toHaveOnlyValidCharacters(this: Assertion, buffer: TextBuffer) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-unsafe-assignment
|
||||
const { isNot } = this as any;
|
||||
let pass = true;
|
||||
const invalidLines: Array<{ line: number; content: string }> = [];
|
||||
|
||||
@@ -45,7 +45,7 @@ export const createMockCommandContext = (
|
||||
forScope: vi.fn().mockReturnValue({ settings: {} }),
|
||||
} as unknown as LoadedSettings,
|
||||
git: undefined as GitService | undefined,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-unsafe-assignment
|
||||
logger: {
|
||||
log: vi.fn(),
|
||||
logMessage: vi.fn(),
|
||||
@@ -54,7 +54,7 @@ export const createMockCommandContext = (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any, // Cast because Logger is a class.
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-unsafe-assignment
|
||||
ui: {
|
||||
addItem: vi.fn(),
|
||||
clear: vi.fn(),
|
||||
@@ -94,11 +94,14 @@ export const createMockCommandContext = (
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const merge = (target: any, source: any): any => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const output = { ...target };
|
||||
|
||||
for (const key in source) {
|
||||
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const sourceValue = source[key];
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const targetValue = output[key];
|
||||
|
||||
if (
|
||||
@@ -106,9 +109,11 @@ export const createMockCommandContext = (
|
||||
Object.prototype.toString.call(sourceValue) === '[object Object]' &&
|
||||
Object.prototype.toString.call(targetValue) === '[object Object]'
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
output[key] = merge(targetValue, sourceValue);
|
||||
} else {
|
||||
// If not, we do a direct assignment. This preserves Date objects and others.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
output[key] = sourceValue;
|
||||
}
|
||||
}
|
||||
@@ -116,5 +121,6 @@ export const createMockCommandContext = (
|
||||
return output;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return merge(defaultMocks, overrides);
|
||||
};
|
||||
|
||||
@@ -128,7 +128,6 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
|
||||
getShellToolInactivityTimeout: vi.fn().mockReturnValue(300000),
|
||||
getShellExecutionConfig: vi.fn().mockReturnValue({}),
|
||||
setShellExecutionConfig: vi.fn(),
|
||||
getEnablePromptCompletion: vi.fn().mockReturnValue(false),
|
||||
getEnableToolOutputTruncation: vi.fn().mockReturnValue(true),
|
||||
getTruncateToolOutputThreshold: vi.fn().mockReturnValue(1000),
|
||||
getTruncateToolOutputLines: vi.fn().mockReturnValue(100),
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render as inkRenderDirect, type Instance as InkInstance } from 'ink';
|
||||
import {
|
||||
render as inkRenderDirect,
|
||||
type Instance as InkInstance,
|
||||
type RenderOptions,
|
||||
} from 'ink';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { Box } from 'ink';
|
||||
import type React from 'react';
|
||||
@@ -31,6 +35,13 @@ import { type HistoryItemToolGroup, StreamingState } from '../ui/types.js';
|
||||
import { ToolActionsProvider } from '../ui/contexts/ToolActionsContext.js';
|
||||
import { AskUserActionsProvider } from '../ui/contexts/AskUserActionsContext.js';
|
||||
import { TerminalProvider } from '../ui/contexts/TerminalContext.js';
|
||||
import {
|
||||
OverflowProvider,
|
||||
useOverflowActions,
|
||||
useOverflowState,
|
||||
type OverflowActions,
|
||||
type OverflowState,
|
||||
} from '../ui/contexts/OverflowContext.js';
|
||||
|
||||
import { makeFakeConfig, type Config } from '@google/gemini-cli-core';
|
||||
import { FakePersistentState } from './persistentStateFake.js';
|
||||
@@ -68,6 +79,25 @@ type TerminalState = {
|
||||
rows: number;
|
||||
};
|
||||
|
||||
type RenderMetrics = Parameters<NonNullable<RenderOptions['onRender']>>[0];
|
||||
|
||||
interface InkRenderMetrics extends RenderMetrics {
|
||||
output: string;
|
||||
staticOutput?: string;
|
||||
}
|
||||
|
||||
function isInkRenderMetrics(
|
||||
metrics: RenderMetrics,
|
||||
): metrics is InkRenderMetrics {
|
||||
const m = metrics as Record<string, unknown>;
|
||||
return (
|
||||
typeof m === 'object' &&
|
||||
m !== null &&
|
||||
'output' in m &&
|
||||
typeof m['output'] === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
class XtermStdout extends EventEmitter {
|
||||
private state: TerminalState;
|
||||
private pendingWrites = 0;
|
||||
@@ -76,7 +106,6 @@ class XtermStdout extends EventEmitter {
|
||||
isTTY = true;
|
||||
|
||||
private lastRenderOutput: string | undefined = undefined;
|
||||
private lastRenderStaticContent: string | undefined = undefined;
|
||||
|
||||
constructor(state: TerminalState, queue: { promise: Promise<void> }) {
|
||||
super();
|
||||
@@ -109,7 +138,6 @@ class XtermStdout extends EventEmitter {
|
||||
clear = () => {
|
||||
this.state.terminal.reset();
|
||||
this.lastRenderOutput = undefined;
|
||||
this.lastRenderStaticContent = undefined;
|
||||
};
|
||||
|
||||
dispose = () => {
|
||||
@@ -118,33 +146,23 @@ class XtermStdout extends EventEmitter {
|
||||
|
||||
onRender = (staticContent: string, output: string) => {
|
||||
this.renderCount++;
|
||||
this.lastRenderStaticContent = staticContent;
|
||||
this.lastRenderOutput = output;
|
||||
this.emit('render');
|
||||
};
|
||||
|
||||
lastFrame = (options: { allowEmpty?: boolean } = {}) => {
|
||||
let result: string;
|
||||
// On Windows, xterm.js headless can sometimes have timing or rendering issues
|
||||
// that lead to duplicated content or incorrect buffer state in tests.
|
||||
// As a fallback, we can trust the raw output Ink provided during onRender.
|
||||
if (os.platform() === 'win32') {
|
||||
result =
|
||||
(this.lastRenderStaticContent ?? '') + (this.lastRenderOutput ?? '');
|
||||
} else {
|
||||
const buffer = this.state.terminal.buffer.active;
|
||||
const allLines: string[] = [];
|
||||
for (let i = 0; i < buffer.length; i++) {
|
||||
allLines.push(buffer.getLine(i)?.translateToString(true) ?? '');
|
||||
}
|
||||
|
||||
const trimmed = [...allLines];
|
||||
while (trimmed.length > 0 && trimmed[trimmed.length - 1] === '') {
|
||||
trimmed.pop();
|
||||
}
|
||||
result = trimmed.join('\n');
|
||||
const buffer = this.state.terminal.buffer.active;
|
||||
const allLines: string[] = [];
|
||||
for (let i = 0; i < buffer.length; i++) {
|
||||
allLines.push(buffer.getLine(i)?.translateToString(true) ?? '');
|
||||
}
|
||||
|
||||
const trimmed = [...allLines];
|
||||
while (trimmed.length > 0 && trimmed[trimmed.length - 1] === '') {
|
||||
trimmed.pop();
|
||||
}
|
||||
const result = trimmed.join('\n');
|
||||
|
||||
// Normalize for cross-platform snapshot stability:
|
||||
// Normalize any \r\n to \n
|
||||
const normalized = result.replace(/\r\n/g, '\n');
|
||||
@@ -195,9 +213,7 @@ class XtermStdout extends EventEmitter {
|
||||
const currentFrame = stripAnsi(
|
||||
this.lastFrame({ allowEmpty: true }),
|
||||
).trim();
|
||||
const expectedFrame = stripAnsi(
|
||||
(this.lastRenderStaticContent ?? '') + (this.lastRenderOutput ?? ''),
|
||||
)
|
||||
const expectedFrame = stripAnsi(this.lastRenderOutput ?? '')
|
||||
.trim()
|
||||
.replace(/\r\n/g, '\n');
|
||||
|
||||
@@ -326,6 +342,8 @@ export type RenderInstance = {
|
||||
lastFrame: (options?: { allowEmpty?: boolean }) => string;
|
||||
terminal: Terminal;
|
||||
waitUntilReady: () => Promise<void>;
|
||||
capturedOverflowState: OverflowState | undefined;
|
||||
capturedOverflowActions: OverflowActions | undefined;
|
||||
};
|
||||
|
||||
const instances: InkInstance[] = [];
|
||||
@@ -334,9 +352,16 @@ const instances: InkInstance[] = [];
|
||||
export const render = (
|
||||
tree: React.ReactElement,
|
||||
terminalWidth?: number,
|
||||
): RenderInstance => {
|
||||
): Omit<
|
||||
RenderInstance,
|
||||
'capturedOverflowState' | 'capturedOverflowActions'
|
||||
> => {
|
||||
const cols = terminalWidth ?? 100;
|
||||
const rows = 40;
|
||||
// We use 1000 rows to avoid windows with incorrect snapshots if a correct
|
||||
// value was used (e.g. 40 rows). The alternatives to make things worse are
|
||||
// windows unfortunately with odd duplicate content in the backbuffer
|
||||
// which does not match actual behavior in xterm.js on windows.
|
||||
const rows = 1000;
|
||||
const terminal = new Terminal({
|
||||
cols,
|
||||
rows,
|
||||
@@ -367,8 +392,10 @@ export const render = (
|
||||
debug: false,
|
||||
exitOnCtrlC: false,
|
||||
patchConsole: false,
|
||||
onRender: (metrics: { output: string; staticOutput?: string }) => {
|
||||
stdout.onRender(metrics.staticOutput ?? '', metrics.output);
|
||||
onRender: (metrics: RenderMetrics) => {
|
||||
if (isInkRenderMetrics(metrics)) {
|
||||
stdout.onRender(metrics.staticOutput ?? '', metrics.output);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -516,6 +543,7 @@ const mockUIActions: UIActions = {
|
||||
vimHandleInput: vi.fn(),
|
||||
handleIdePromptComplete: vi.fn(),
|
||||
handleFolderTrustSelect: vi.fn(),
|
||||
setIsPolicyUpdateDialogOpen: vi.fn(),
|
||||
setConstrainHeight: vi.fn(),
|
||||
onEscapePromptChange: vi.fn(),
|
||||
refreshStatic: vi.fn(),
|
||||
@@ -546,6 +574,16 @@ const mockUIActions: UIActions = {
|
||||
handleNewAgentsSelect: vi.fn(),
|
||||
};
|
||||
|
||||
let capturedOverflowState: OverflowState | undefined;
|
||||
let capturedOverflowActions: OverflowActions | undefined;
|
||||
const ContextCapture: React.FC<{ children: React.ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
capturedOverflowState = useOverflowState();
|
||||
capturedOverflowActions = useOverflowActions();
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export const renderWithProviders = (
|
||||
component: React.ReactElement,
|
||||
{
|
||||
@@ -647,6 +685,9 @@ export const renderWithProviders = (
|
||||
.filter((item): item is HistoryItemToolGroup => item.type === 'tool_group')
|
||||
.flatMap((item) => item.tools);
|
||||
|
||||
capturedOverflowState = undefined;
|
||||
capturedOverflowActions = undefined;
|
||||
|
||||
const renderResult = render(
|
||||
<AppContext.Provider value={appState}>
|
||||
<ConfigContext.Provider value={config}>
|
||||
@@ -659,35 +700,39 @@ export const renderWithProviders = (
|
||||
value={finalUiState.streamingState}
|
||||
>
|
||||
<UIActionsContext.Provider value={finalUIActions}>
|
||||
<ToolActionsProvider
|
||||
config={config}
|
||||
toolCalls={allToolCalls}
|
||||
>
|
||||
<AskUserActionsProvider
|
||||
request={null}
|
||||
onSubmit={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
<OverflowProvider>
|
||||
<ToolActionsProvider
|
||||
config={config}
|
||||
toolCalls={allToolCalls}
|
||||
>
|
||||
<KeypressProvider>
|
||||
<MouseProvider
|
||||
mouseEventsEnabled={mouseEventsEnabled}
|
||||
>
|
||||
<TerminalProvider>
|
||||
<ScrollProvider>
|
||||
<Box
|
||||
width={terminalWidth}
|
||||
flexShrink={0}
|
||||
flexGrow={0}
|
||||
flexDirection="column"
|
||||
>
|
||||
{component}
|
||||
</Box>
|
||||
</ScrollProvider>
|
||||
</TerminalProvider>
|
||||
</MouseProvider>
|
||||
</KeypressProvider>
|
||||
</AskUserActionsProvider>
|
||||
</ToolActionsProvider>
|
||||
<AskUserActionsProvider
|
||||
request={null}
|
||||
onSubmit={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
>
|
||||
<KeypressProvider>
|
||||
<MouseProvider
|
||||
mouseEventsEnabled={mouseEventsEnabled}
|
||||
>
|
||||
<TerminalProvider>
|
||||
<ScrollProvider>
|
||||
<ContextCapture>
|
||||
<Box
|
||||
width={terminalWidth}
|
||||
flexShrink={0}
|
||||
flexGrow={0}
|
||||
flexDirection="column"
|
||||
>
|
||||
{component}
|
||||
</Box>
|
||||
</ContextCapture>
|
||||
</ScrollProvider>
|
||||
</TerminalProvider>
|
||||
</MouseProvider>
|
||||
</KeypressProvider>
|
||||
</AskUserActionsProvider>
|
||||
</ToolActionsProvider>
|
||||
</OverflowProvider>
|
||||
</UIActionsContext.Provider>
|
||||
</StreamingContext.Provider>
|
||||
</SessionStatsProvider>
|
||||
@@ -702,6 +747,8 @@ export const renderWithProviders = (
|
||||
|
||||
return {
|
||||
...renderResult,
|
||||
capturedOverflowState,
|
||||
capturedOverflowActions,
|
||||
simulateClick: (col: number, row: number, button?: 0 | 1 | 2) =>
|
||||
simulateClick(renderResult.stdin, col, row, button),
|
||||
};
|
||||
|
||||
@@ -46,6 +46,7 @@ export const createMockSettings = (
|
||||
workspace,
|
||||
isTrusted,
|
||||
errors,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
merged: mergedOverride,
|
||||
...settingsOverrides
|
||||
} = overrides;
|
||||
@@ -75,7 +76,7 @@ export const createMockSettings = (
|
||||
// Assign any function overrides (e.g., vi.fn() for methods)
|
||||
for (const key in overrides) {
|
||||
if (typeof overrides[key] === 'function') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-unsafe-assignment
|
||||
(loaded as any)[key] = overrides[key];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ import {
|
||||
CoreEvent,
|
||||
type UserFeedbackPayload,
|
||||
type ResumedSessionData,
|
||||
type StartupWarning,
|
||||
WarningPriority,
|
||||
AuthType,
|
||||
type AgentDefinition,
|
||||
CoreToolCallStatus,
|
||||
@@ -103,6 +105,11 @@ import {
|
||||
type UIActions,
|
||||
} from './contexts/UIActionsContext.js';
|
||||
import { KeypressProvider } from './contexts/KeypressContext.js';
|
||||
import { OverflowProvider } from './contexts/OverflowContext.js';
|
||||
import {
|
||||
useOverflowActions,
|
||||
type OverflowActions,
|
||||
} from './contexts/OverflowContext.js';
|
||||
|
||||
// Mock useStdout to capture terminal title writes
|
||||
vi.mock('ink', async (importOriginal) => {
|
||||
@@ -118,9 +125,11 @@ vi.mock('ink', async (importOriginal) => {
|
||||
// so we can assert against them in our tests.
|
||||
let capturedUIState: UIState;
|
||||
let capturedUIActions: UIActions;
|
||||
let capturedOverflowActions: OverflowActions;
|
||||
function TestContextConsumer() {
|
||||
capturedUIState = useContext(UIStateContext)!;
|
||||
capturedUIActions = useContext(UIActionsContext)!;
|
||||
capturedOverflowActions = useOverflowActions()!;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -227,7 +236,10 @@ import {
|
||||
disableMouseEvents,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { type ExtensionManager } from '../config/extension-manager.js';
|
||||
import { WARNING_PROMPT_DURATION_MS } from './constants.js';
|
||||
import {
|
||||
WARNING_PROMPT_DURATION_MS,
|
||||
EXPAND_HINT_DURATION_MS,
|
||||
} from './constants.js';
|
||||
|
||||
describe('AppContainer State Management', () => {
|
||||
let mockConfig: Config;
|
||||
@@ -248,18 +260,20 @@ describe('AppContainer State Management', () => {
|
||||
config?: Config;
|
||||
version?: string;
|
||||
initResult?: InitializationResult;
|
||||
startupWarnings?: string[];
|
||||
startupWarnings?: StartupWarning[];
|
||||
resumedSessionData?: ResumedSessionData;
|
||||
} = {}) => (
|
||||
<SettingsContext.Provider value={settings}>
|
||||
<KeypressProvider config={config}>
|
||||
<AppContainer
|
||||
config={config}
|
||||
version={version}
|
||||
initializationResult={initResult}
|
||||
startupWarnings={startupWarnings}
|
||||
resumedSessionData={resumedSessionData}
|
||||
/>
|
||||
<OverflowProvider>
|
||||
<AppContainer
|
||||
config={config}
|
||||
version={version}
|
||||
initializationResult={initResult}
|
||||
startupWarnings={startupWarnings}
|
||||
resumedSessionData={resumedSessionData}
|
||||
/>
|
||||
</OverflowProvider>
|
||||
</KeypressProvider>
|
||||
</SettingsContext.Provider>
|
||||
);
|
||||
@@ -501,7 +515,18 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
it('renders with startup warnings', async () => {
|
||||
const startupWarnings = ['Warning 1', 'Warning 2'];
|
||||
const startupWarnings: StartupWarning[] = [
|
||||
{
|
||||
id: 'w1',
|
||||
message: 'Warning 1',
|
||||
priority: WarningPriority.High,
|
||||
},
|
||||
{
|
||||
id: 'w2',
|
||||
message: 'Warning 2',
|
||||
priority: WarningPriority.High,
|
||||
},
|
||||
];
|
||||
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
@@ -2674,12 +2699,14 @@ describe('AppContainer State Management', () => {
|
||||
const getTree = (settings: LoadedSettings) => (
|
||||
<SettingsContext.Provider value={settings}>
|
||||
<KeypressProvider config={mockConfig}>
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
version="1.0.0"
|
||||
initializationResult={mockInitResult}
|
||||
/>
|
||||
<TestChild />
|
||||
<OverflowProvider>
|
||||
<AppContainer
|
||||
config={mockConfig}
|
||||
version="1.0.0"
|
||||
initializationResult={mockInitResult}
|
||||
/>
|
||||
<TestChild />
|
||||
</OverflowProvider>
|
||||
</KeypressProvider>
|
||||
</SettingsContext.Provider>
|
||||
);
|
||||
@@ -3290,6 +3317,306 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Submission Handling', () => {
|
||||
it('resets expansion state on submission when not in alternate buffer', async () => {
|
||||
const { checkPermissions } = await import(
|
||||
'./hooks/atCommandProcessor.js'
|
||||
);
|
||||
vi.mocked(checkPermissions).mockResolvedValue([]);
|
||||
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
unmount = renderAppContainer({
|
||||
settings: {
|
||||
...mockSettings,
|
||||
merged: {
|
||||
...mockSettings.merged,
|
||||
ui: { ...mockSettings.merged.ui, useAlternateBuffer: false },
|
||||
},
|
||||
} as LoadedSettings,
|
||||
}).unmount;
|
||||
});
|
||||
|
||||
await waitFor(() => expect(capturedUIActions).toBeTruthy());
|
||||
|
||||
// Expand first
|
||||
act(() => capturedUIActions.setConstrainHeight(false));
|
||||
expect(capturedUIState.constrainHeight).toBe(false);
|
||||
|
||||
// Reset mock stdout to clear any initial writes
|
||||
mocks.mockStdout.write.mockClear();
|
||||
|
||||
// Submit
|
||||
await act(async () => capturedUIActions.handleFinalSubmit('test prompt'));
|
||||
|
||||
// Should be reset
|
||||
expect(capturedUIState.constrainHeight).toBe(true);
|
||||
// Should refresh static (which clears terminal in non-alternate buffer)
|
||||
expect(mocks.mockStdout.write).toHaveBeenCalledWith(
|
||||
ansiEscapes.clearTerminal,
|
||||
);
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('resets expansion state on submission when in alternate buffer without clearing terminal', async () => {
|
||||
const { checkPermissions } = await import(
|
||||
'./hooks/atCommandProcessor.js'
|
||||
);
|
||||
vi.mocked(checkPermissions).mockResolvedValue([]);
|
||||
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
unmount = renderAppContainer({
|
||||
settings: {
|
||||
...mockSettings,
|
||||
merged: {
|
||||
...mockSettings.merged,
|
||||
ui: { ...mockSettings.merged.ui, useAlternateBuffer: true },
|
||||
},
|
||||
} as LoadedSettings,
|
||||
}).unmount;
|
||||
});
|
||||
|
||||
await waitFor(() => expect(capturedUIActions).toBeTruthy());
|
||||
|
||||
// Expand first
|
||||
act(() => capturedUIActions.setConstrainHeight(false));
|
||||
expect(capturedUIState.constrainHeight).toBe(false);
|
||||
|
||||
// Reset mock stdout
|
||||
mocks.mockStdout.write.mockClear();
|
||||
|
||||
// Submit
|
||||
await act(async () => capturedUIActions.handleFinalSubmit('test prompt'));
|
||||
|
||||
// Should be reset
|
||||
expect(capturedUIState.constrainHeight).toBe(true);
|
||||
// Should NOT refresh static's clearTerminal in alternate buffer
|
||||
expect(mocks.mockStdout.write).not.toHaveBeenCalledWith(
|
||||
ansiEscapes.clearTerminal,
|
||||
);
|
||||
unmount!();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Overflow Hint Handling', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('sets showIsExpandableHint when overflow occurs in Standard Mode and hides after 10s', async () => {
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
|
||||
// Trigger overflow
|
||||
act(() => {
|
||||
capturedOverflowActions.addOverflowingId('test-id');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
// Should show hint because we are in Standard Mode (default settings) and have overflow
|
||||
expect(capturedUIState.showIsExpandableHint).toBe(true);
|
||||
});
|
||||
|
||||
// Advance just before the timeout
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS - 100);
|
||||
});
|
||||
expect(capturedUIState.showIsExpandableHint).toBe(true);
|
||||
|
||||
// Advance to hit the timeout mark
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(100);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(capturedUIState.showIsExpandableHint).toBe(false);
|
||||
});
|
||||
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('toggles expansion state and resets the hint timer when Ctrl+O is pressed in Standard Mode', async () => {
|
||||
let unmount: () => void;
|
||||
let stdin: ReturnType<typeof renderAppContainer>['stdin'];
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
stdin = result.stdin;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
|
||||
// Initial state is constrainHeight = true
|
||||
expect(capturedUIState.constrainHeight).toBe(true);
|
||||
|
||||
// Trigger overflow so the hint starts showing
|
||||
act(() => {
|
||||
capturedOverflowActions.addOverflowingId('test-id');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(capturedUIState.showIsExpandableHint).toBe(true);
|
||||
});
|
||||
|
||||
// Advance half the duration
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS / 2);
|
||||
});
|
||||
expect(capturedUIState.showIsExpandableHint).toBe(true);
|
||||
|
||||
// Simulate Ctrl+O
|
||||
act(() => {
|
||||
stdin.write('\x0f'); // \x0f is Ctrl+O
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
// constrainHeight should toggle
|
||||
expect(capturedUIState.constrainHeight).toBe(false);
|
||||
});
|
||||
|
||||
// Advance enough that the original timer would have expired if it hadn't reset
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS / 2 + 1000);
|
||||
});
|
||||
|
||||
// We expect it to still be true because Ctrl+O should have reset the timer
|
||||
expect(capturedUIState.showIsExpandableHint).toBe(true);
|
||||
|
||||
// Advance remaining time to reach the new timeout
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS / 2 - 1000);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(capturedUIState.showIsExpandableHint).toBe(false);
|
||||
});
|
||||
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('toggles Ctrl+O multiple times and verifies the hint disappears exactly after the last toggle', async () => {
|
||||
let unmount: () => void;
|
||||
let stdin: ReturnType<typeof renderAppContainer>['stdin'];
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
stdin = result.stdin;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
|
||||
// Initial state is constrainHeight = true
|
||||
expect(capturedUIState.constrainHeight).toBe(true);
|
||||
|
||||
// Trigger overflow so the hint starts showing
|
||||
act(() => {
|
||||
capturedOverflowActions.addOverflowingId('test-id');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(capturedUIState.showIsExpandableHint).toBe(true);
|
||||
});
|
||||
|
||||
// Advance half the duration
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS / 2);
|
||||
});
|
||||
expect(capturedUIState.showIsExpandableHint).toBe(true);
|
||||
|
||||
// First toggle 'on' (expanded)
|
||||
act(() => {
|
||||
stdin.write('\x0f'); // Ctrl+O
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(capturedUIState.constrainHeight).toBe(false);
|
||||
});
|
||||
|
||||
// Wait 1 second
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(1000);
|
||||
});
|
||||
expect(capturedUIState.showIsExpandableHint).toBe(true);
|
||||
|
||||
// Second toggle 'off' (collapsed)
|
||||
act(() => {
|
||||
stdin.write('\x0f'); // Ctrl+O
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(capturedUIState.constrainHeight).toBe(true);
|
||||
});
|
||||
|
||||
// Wait 1 second
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(1000);
|
||||
});
|
||||
expect(capturedUIState.showIsExpandableHint).toBe(true);
|
||||
|
||||
// Third toggle 'on' (expanded)
|
||||
act(() => {
|
||||
stdin.write('\x0f'); // Ctrl+O
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(capturedUIState.constrainHeight).toBe(false);
|
||||
});
|
||||
|
||||
// Now we wait just before the timeout from the LAST toggle.
|
||||
// It should still be true.
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS - 100);
|
||||
});
|
||||
expect(capturedUIState.showIsExpandableHint).toBe(true);
|
||||
|
||||
// Wait 0.1s more to hit exactly the timeout since the last toggle.
|
||||
// It should hide now.
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(100);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(capturedUIState.showIsExpandableHint).toBe(false);
|
||||
});
|
||||
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('does NOT set showIsExpandableHint when overflow occurs in Alternate Buffer Mode', async () => {
|
||||
const alternateSettings = mergeSettings({}, {}, {}, {}, true);
|
||||
const settingsWithAlternateBuffer = {
|
||||
merged: {
|
||||
...alternateSettings,
|
||||
ui: {
|
||||
...alternateSettings.ui,
|
||||
useAlternateBuffer: true,
|
||||
},
|
||||
},
|
||||
} as unknown as LoadedSettings;
|
||||
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer({
|
||||
settings: settingsWithAlternateBuffer,
|
||||
});
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
|
||||
// Trigger overflow
|
||||
act(() => {
|
||||
capturedOverflowActions.addOverflowingId('test-id');
|
||||
});
|
||||
|
||||
// Should NOT show hint because we are in Alternate Buffer Mode
|
||||
expect(capturedUIState.showIsExpandableHint).toBe(false);
|
||||
|
||||
unmount!();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Permission Handling', () => {
|
||||
it('shows permission dialog when checkPermissions returns paths', async () => {
|
||||
const { checkPermissions } = await import(
|
||||
|
||||
@@ -41,6 +41,7 @@ import { checkPermissions } from './hooks/atCommandProcessor.js';
|
||||
import { MessageType, StreamingState } from './types.js';
|
||||
import { ToolActionsProvider } from './contexts/ToolActionsContext.js';
|
||||
import {
|
||||
type StartupWarning,
|
||||
type EditorType,
|
||||
type Config,
|
||||
type IdeInfo,
|
||||
@@ -94,6 +95,10 @@ import { useSettingsCommand } from './hooks/useSettingsCommand.js';
|
||||
import { useModelCommand } from './hooks/useModelCommand.js';
|
||||
import { useSlashCommandProcessor } from './hooks/slashCommandProcessor.js';
|
||||
import { useVimMode } from './contexts/VimModeContext.js';
|
||||
import {
|
||||
useOverflowActions,
|
||||
useOverflowState,
|
||||
} from './contexts/OverflowContext.js';
|
||||
import { useConsoleMessages } from './hooks/useConsoleMessages.js';
|
||||
import { useTerminalSize } from './hooks/useTerminalSize.js';
|
||||
import { calculatePromptWidths } from './components/InputPrompt.js';
|
||||
@@ -150,6 +155,7 @@ import { useBackgroundShellManager } from './hooks/useBackgroundShellManager.js'
|
||||
import {
|
||||
WARNING_PROMPT_DURATION_MS,
|
||||
QUEUE_ERROR_DISPLAY_DURATION_MS,
|
||||
EXPAND_HINT_DURATION_MS,
|
||||
} from './constants.js';
|
||||
import { LoginWithGoogleRestartDialog } from './auth/LoginWithGoogleRestartDialog.js';
|
||||
import { NewAgentsChoice } from './components/NewAgentsNotification.js';
|
||||
@@ -186,7 +192,7 @@ function isToolAwaitingConfirmation(
|
||||
|
||||
interface AppContainerProps {
|
||||
config: Config;
|
||||
startupWarnings?: string[];
|
||||
startupWarnings?: StartupWarning[];
|
||||
version: string;
|
||||
initializationResult: InitializationResult;
|
||||
resumedSessionData?: ResumedSessionData;
|
||||
@@ -213,6 +219,7 @@ const SHELL_HEIGHT_PADDING = 10;
|
||||
export const AppContainer = (props: AppContainerProps) => {
|
||||
const { config, initializationResult, resumedSessionData } = props;
|
||||
const settings = useSettings();
|
||||
const { reset } = useOverflowActions()!;
|
||||
const notificationsEnabled = isNotificationsEnabled(settings);
|
||||
|
||||
const historyManager = useHistory({
|
||||
@@ -261,6 +268,54 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
);
|
||||
|
||||
const [newAgents, setNewAgents] = useState<AgentDefinition[] | null>(null);
|
||||
const [constrainHeight, setConstrainHeight] = useState<boolean>(true);
|
||||
const [showIsExpandableHint, setShowIsExpandableHint] = useState(false);
|
||||
const expandHintTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const overflowState = useOverflowState();
|
||||
const overflowingIdsSize = overflowState?.overflowingIds.size ?? 0;
|
||||
const hasOverflowState = overflowingIdsSize > 0 || !constrainHeight;
|
||||
|
||||
/**
|
||||
* Manages the visibility and x-second timer for the expansion hint.
|
||||
*
|
||||
* This effect triggers the timer countdown whenever an overflow is detected
|
||||
* or the user manually toggles the expansion state with Ctrl+O. We use a stable
|
||||
* boolean dependency (hasOverflowState) to ensure the timer only resets on
|
||||
* genuine state transitions, preventing it from infinitely resetting during
|
||||
* active text streaming.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (isAlternateBuffer) {
|
||||
setShowIsExpandableHint(false);
|
||||
if (expandHintTimerRef.current) {
|
||||
clearTimeout(expandHintTimerRef.current);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasOverflowState) {
|
||||
setShowIsExpandableHint(true);
|
||||
if (expandHintTimerRef.current) {
|
||||
clearTimeout(expandHintTimerRef.current);
|
||||
}
|
||||
expandHintTimerRef.current = setTimeout(() => {
|
||||
setShowIsExpandableHint(false);
|
||||
}, EXPAND_HINT_DURATION_MS);
|
||||
}
|
||||
}, [hasOverflowState, isAlternateBuffer, constrainHeight]);
|
||||
|
||||
/**
|
||||
* Safe cleanup to ensure the expansion hint timer is cancelled when the
|
||||
* component unmounts, preventing memory leaks.
|
||||
*/
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (expandHintTimerRef.current) {
|
||||
clearTimeout(expandHintTimerRef.current);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const [defaultBannerText, setDefaultBannerText] = useState('');
|
||||
const [warningBannerText, setWarningBannerText] = useState('');
|
||||
@@ -1188,6 +1243,19 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
|
||||
const handleFinalSubmit = useCallback(
|
||||
async (submittedValue: string) => {
|
||||
reset();
|
||||
// Explicitly hide the expansion hint and clear its x-second timer when a new turn begins.
|
||||
setShowIsExpandableHint(false);
|
||||
if (expandHintTimerRef.current) {
|
||||
clearTimeout(expandHintTimerRef.current);
|
||||
}
|
||||
if (!constrainHeight) {
|
||||
setConstrainHeight(true);
|
||||
if (!isAlternateBuffer) {
|
||||
refreshStatic();
|
||||
}
|
||||
}
|
||||
|
||||
const isSlash = isSlashCommand(submittedValue.trim());
|
||||
const isIdle = streamingState === StreamingState.Idle;
|
||||
const isAgentRunning =
|
||||
@@ -1246,15 +1314,32 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
pendingSlashCommandHistoryItems,
|
||||
pendingGeminiHistoryItems,
|
||||
config,
|
||||
constrainHeight,
|
||||
setConstrainHeight,
|
||||
isAlternateBuffer,
|
||||
refreshStatic,
|
||||
reset,
|
||||
handleHintSubmit,
|
||||
],
|
||||
);
|
||||
|
||||
const handleClearScreen = useCallback(() => {
|
||||
reset();
|
||||
// Explicitly hide the expansion hint and clear its x-second timer when clearing the screen.
|
||||
setShowIsExpandableHint(false);
|
||||
if (expandHintTimerRef.current) {
|
||||
clearTimeout(expandHintTimerRef.current);
|
||||
}
|
||||
historyManager.clearItems();
|
||||
clearConsoleMessagesState();
|
||||
refreshStatic();
|
||||
}, [historyManager, clearConsoleMessagesState, refreshStatic]);
|
||||
}, [
|
||||
historyManager,
|
||||
clearConsoleMessagesState,
|
||||
refreshStatic,
|
||||
reset,
|
||||
setShowIsExpandableHint,
|
||||
]);
|
||||
|
||||
const { handleInput: vimHandleInput } = useVim(buffer, handleFinalSubmit);
|
||||
|
||||
@@ -1424,7 +1509,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
windowMs: WARNING_PROMPT_DURATION_MS,
|
||||
onRepeat: handleExitRepeat,
|
||||
});
|
||||
const [constrainHeight, setConstrainHeight] = useState<boolean>(true);
|
||||
|
||||
const [ideContextState, setIdeContextState] = useState<
|
||||
IdeContext | undefined
|
||||
>();
|
||||
@@ -1436,8 +1521,18 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
type: TransientMessageType;
|
||||
}>(WARNING_PROMPT_DURATION_MS);
|
||||
|
||||
const { isFolderTrustDialogOpen, handleFolderTrustSelect, isRestarting } =
|
||||
useFolderTrust(settings, setIsTrustedFolder, historyManager.addItem);
|
||||
const {
|
||||
isFolderTrustDialogOpen,
|
||||
discoveryResults: folderDiscoveryResults,
|
||||
handleFolderTrustSelect,
|
||||
isRestarting,
|
||||
} = useFolderTrust(settings, setIsTrustedFolder, historyManager.addItem);
|
||||
|
||||
const policyUpdateConfirmationRequest =
|
||||
config.getPolicyUpdateConfirmationRequest();
|
||||
const [isPolicyUpdateDialogOpen, setIsPolicyUpdateDialogOpen] = useState(
|
||||
!!policyUpdateConfirmationRequest,
|
||||
);
|
||||
const {
|
||||
needsRestart: ideNeedsRestart,
|
||||
restartReason: ideTrustRestartReason,
|
||||
@@ -1596,6 +1691,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
streamingState,
|
||||
shouldShowFocusHint,
|
||||
retryStatus,
|
||||
loadingPhrasesMode: settings.merged.ui.loadingPhrases,
|
||||
customWittyPhrases: settings.merged.ui.customWittyPhrases,
|
||||
});
|
||||
|
||||
const handleGlobalKeypress = useCallback(
|
||||
@@ -1642,6 +1739,19 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
if (!constrainHeight) {
|
||||
enteringConstrainHeightMode = true;
|
||||
setConstrainHeight(true);
|
||||
if (keyMatchers[Command.SHOW_MORE_LINES](key)) {
|
||||
// If the user manually collapses the view, show the hint and reset the x-second timer.
|
||||
setShowIsExpandableHint(true);
|
||||
if (expandHintTimerRef.current) {
|
||||
clearTimeout(expandHintTimerRef.current);
|
||||
}
|
||||
expandHintTimerRef.current = setTimeout(() => {
|
||||
setShowIsExpandableHint(false);
|
||||
}, EXPAND_HINT_DURATION_MS);
|
||||
}
|
||||
if (!isAlternateBuffer) {
|
||||
refreshStatic();
|
||||
}
|
||||
}
|
||||
|
||||
if (keyMatchers[Command.SHOW_ERROR_DETAILS](key)) {
|
||||
@@ -1685,6 +1795,17 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
!enteringConstrainHeightMode
|
||||
) {
|
||||
setConstrainHeight(false);
|
||||
// If the user manually expands the view, show the hint and reset the x-second timer.
|
||||
setShowIsExpandableHint(true);
|
||||
if (expandHintTimerRef.current) {
|
||||
clearTimeout(expandHintTimerRef.current);
|
||||
}
|
||||
expandHintTimerRef.current = setTimeout(() => {
|
||||
setShowIsExpandableHint(false);
|
||||
}, EXPAND_HINT_DURATION_MS);
|
||||
if (!isAlternateBuffer) {
|
||||
refreshStatic();
|
||||
}
|
||||
return true;
|
||||
} else if (
|
||||
(keyMatchers[Command.FOCUS_SHELL_INPUT](key) ||
|
||||
@@ -1908,6 +2029,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
(shouldShowRetentionWarning && retentionCheckComplete) ||
|
||||
shouldShowIdePrompt ||
|
||||
isFolderTrustDialogOpen ||
|
||||
isPolicyUpdateDialogOpen ||
|
||||
adminSettingsChanged ||
|
||||
!!commandConfirmationRequest ||
|
||||
!!authConsentRequest ||
|
||||
@@ -2064,8 +2186,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
|
||||
const fetchBannerTexts = async () => {
|
||||
const [defaultBanner, warningBanner] = await Promise.all([
|
||||
// TODO: temporarily disabling the banner, it will be re-added.
|
||||
'',
|
||||
config.getBannerTextNoCapacityIssues(),
|
||||
config.getBannerTextCapacityIssues(),
|
||||
]);
|
||||
|
||||
@@ -2135,6 +2256,9 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
isResuming,
|
||||
shouldShowIdePrompt,
|
||||
isFolderTrustDialogOpen: isFolderTrustDialogOpen ?? false,
|
||||
folderDiscoveryResults,
|
||||
isPolicyUpdateDialogOpen,
|
||||
policyUpdateConfirmationRequest,
|
||||
isTrustedFolder,
|
||||
constrainHeight,
|
||||
showErrorDetails,
|
||||
@@ -2202,6 +2326,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
isBackgroundShellListOpen,
|
||||
adminSettingsChanged,
|
||||
newAgents,
|
||||
showIsExpandableHint,
|
||||
hintMode:
|
||||
config.isModelSteeringEnabled() &&
|
||||
isToolExecuting([
|
||||
@@ -2257,6 +2382,9 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
isResuming,
|
||||
shouldShowIdePrompt,
|
||||
isFolderTrustDialogOpen,
|
||||
folderDiscoveryResults,
|
||||
isPolicyUpdateDialogOpen,
|
||||
policyUpdateConfirmationRequest,
|
||||
isTrustedFolder,
|
||||
constrainHeight,
|
||||
showErrorDetails,
|
||||
@@ -2325,6 +2453,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
backgroundShells,
|
||||
adminSettingsChanged,
|
||||
newAgents,
|
||||
showIsExpandableHint,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -2354,6 +2483,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
vimHandleInput,
|
||||
handleIdePromptComplete,
|
||||
handleFolderTrustSelect,
|
||||
setIsPolicyUpdateDialogOpen,
|
||||
setConstrainHeight,
|
||||
onEscapePromptChange: handleEscapePromptChange,
|
||||
refreshStatic,
|
||||
@@ -2438,6 +2568,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
vimHandleInput,
|
||||
handleIdePromptComplete,
|
||||
handleFolderTrustSelect,
|
||||
setIsPolicyUpdateDialogOpen,
|
||||
setConstrainHeight,
|
||||
handleEscapePromptChange,
|
||||
refreshStatic,
|
||||
|
||||
@@ -125,6 +125,7 @@ describe('copyCommand', () => {
|
||||
|
||||
expect(mockCopyToClipboard).toHaveBeenCalledWith(
|
||||
'Hi there! How can I help you?',
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -143,7 +144,10 @@ describe('copyCommand', () => {
|
||||
|
||||
const result = await copyCommand.action(mockContext, '');
|
||||
|
||||
expect(mockCopyToClipboard).toHaveBeenCalledWith('Part 1: Part 2: Part 3');
|
||||
expect(mockCopyToClipboard).toHaveBeenCalledWith(
|
||||
'Part 1: Part 2: Part 3',
|
||||
expect.anything(),
|
||||
);
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
@@ -170,7 +174,10 @@ describe('copyCommand', () => {
|
||||
|
||||
const result = await copyCommand.action(mockContext, '');
|
||||
|
||||
expect(mockCopyToClipboard).toHaveBeenCalledWith('Text part more text');
|
||||
expect(mockCopyToClipboard).toHaveBeenCalledWith(
|
||||
'Text part more text',
|
||||
expect.anything(),
|
||||
);
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
@@ -201,7 +208,10 @@ describe('copyCommand', () => {
|
||||
|
||||
const result = await copyCommand.action(mockContext, '');
|
||||
|
||||
expect(mockCopyToClipboard).toHaveBeenCalledWith('Second AI response');
|
||||
expect(mockCopyToClipboard).toHaveBeenCalledWith(
|
||||
'Second AI response',
|
||||
expect.anything(),
|
||||
);
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
@@ -230,6 +240,11 @@ describe('copyCommand', () => {
|
||||
messageType: 'error',
|
||||
content: `Failed to copy to the clipboard. ${clipboardError.message}`,
|
||||
});
|
||||
|
||||
expect(mockCopyToClipboard).toHaveBeenCalledWith(
|
||||
'AI response',
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle non-Error clipboard errors', async () => {
|
||||
@@ -253,6 +268,11 @@ describe('copyCommand', () => {
|
||||
messageType: 'error',
|
||||
content: `Failed to copy to the clipboard. ${rejectedValue}`,
|
||||
});
|
||||
|
||||
expect(mockCopyToClipboard).toHaveBeenCalledWith(
|
||||
'AI response',
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return info message when no text parts found in AI message', async () => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user