mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-31 04:01:01 -07:00
Compare commits
142 Commits
early-access
...
latest
| Author | SHA1 | Date | |
|---|---|---|---|
| ac24fd27e4 | |||
| d35e7d3eb1 | |||
| 8d1d40cc7a | |||
| a2a46c7c67 | |||
| 4fbffdf617 | |||
| 5fd6664c4b | |||
| 3aabb940f5 | |||
| 3ebf54f367 | |||
| 582b4861a9 | |||
| a943cee45d | |||
| 6742a1b7f9 | |||
| bf873a1d85 | |||
| d9892ada7f | |||
| 560905154c | |||
| 267173c7e8 | |||
| c55b15f705 | |||
| 24ccc9c457 | |||
| 121bba3464 | |||
| 02bd8dfeff | |||
| ee5bf842eb | |||
| 891116a6c2 | |||
| dbe217828d | |||
| b8ae12a109 | |||
| 759ad4cc96 | |||
| 4d4b95a41d | |||
| 046c7f84bc | |||
| 01ff27709d | |||
| 1078a546fe | |||
| a8763abfb7 | |||
| bb797ded7d | |||
| b6b9923dc3 | |||
| 79c647d486 | |||
| 00b24c917e | |||
| 32b2e79b62 | |||
| 5aa6b9a84b | |||
| 8c5a0b6f88 | |||
| b0cf9bcece | |||
| 31f32421a3 | |||
| eacbb3551c | |||
| 4b5ca6bc77 | |||
| 452b82162b | |||
| aa0e375508 | |||
| 6a0b8a733b | |||
| 63a7fbc5fd | |||
| 3a369ddec3 | |||
| 0915bf7d67 | |||
| 9897a2b80a | |||
| 819507c5e8 | |||
| 45b2b382cc | |||
| af4dfd9327 | |||
| 21cfe9f680 | |||
| 6991ba1387 | |||
| 452dca4301 | |||
| 852210e108 | |||
| f6c36f75e3 | |||
| a3c46c0d31 | |||
| f78479d7ce | |||
| cfc13fbd58 | |||
| 9a093e4b51 | |||
| 4cc2b27f1d | |||
| 39bfa108b5 | |||
| 268d29f05c | |||
| b6ccf12551 | |||
| fbd8725c07 | |||
| 2505af8522 | |||
| d45d414c93 | |||
| d8000c9248 | |||
| e3def2dd49 | |||
| e356949d3f | |||
| 4bf18da2b0 | |||
| 8bd5645dd4 | |||
| 1f6fe59def | |||
| 418f67086b | |||
| 13cff94b1a | |||
| 7421bf681b | |||
| db57d38d92 | |||
| 5ff00b0c5d | |||
| 75ed7aaa06 | |||
| a411c415a8 | |||
| e613cbc448 | |||
| 5099f104bc | |||
| b47a4240ff | |||
| a2ed4266aa | |||
| 324715ee8b | |||
| 9f5a625730 | |||
| d3f13c71ae | |||
| f7caca5f94 | |||
| 0abd2a644e | |||
| 104f23da90 | |||
| 37034045ae | |||
| aca034fdfe | |||
| b3741f7016 | |||
| 3012684469 | |||
| 335802f4dd | |||
| f741630572 | |||
| 8c6545bf9d | |||
| e21dbed8c8 | |||
| 160d6a6552 | |||
| b443b5e800 | |||
| 58572a6eaa | |||
| e423d20a8d | |||
| fcb8be2fb9 | |||
| 1faf53a3af | |||
| fd58d3267e | |||
| dc76bcc433 | |||
| 21e6a36cf1 | |||
| 98f3a7066e | |||
| 4d88054d35 | |||
| d2ae7af487 | |||
| 139668c1d1 | |||
| 1825105d8f | |||
| 3e25c350f2 | |||
| 4b7307accb | |||
| 631591ce79 | |||
| 07880d43d2 | |||
| 7b39dd8b28 | |||
| d0800ab917 | |||
| 99d521569d | |||
| 156feff5b1 | |||
| 523aeec544 | |||
| 64e1c7df75 | |||
| da128e725d | |||
| d8ecbde9bd | |||
| c9bea8e646 | |||
| b05b8673cd | |||
| b831ffc1b3 | |||
| 6a1c62731b | |||
| 3c656cfbc6 | |||
| cb76b08e31 | |||
| 4cfab0a893 | |||
| c9950b3cb2 | |||
| 5cf8dc4f07 | |||
| 383212034c | |||
| 0779697da6 | |||
| 63f6a497cb | |||
| e20171e7dd | |||
| 47780e984c | |||
| 076f81f130 | |||
| e255eb6124 | |||
| 99a6dc0267 | |||
| 03af6235a9 | |||
| f9b2a33732 |
@@ -1,47 +0,0 @@
|
||||
steps:
|
||||
# Step 1: Install root dependencies (includes workspaces)
|
||||
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
|
||||
entrypoint: 'npm'
|
||||
args: ['install']
|
||||
|
||||
# Step 2: Update version in root package.json
|
||||
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
|
||||
entrypoint: 'bash'
|
||||
args:
|
||||
- -c # Use bash -c to allow for command substitution and string manipulation
|
||||
- |
|
||||
current_version=$(npm pkg get version | sed 's/"//g')
|
||||
new_version="$${current_version}-$SHORT_SHA.$_REVISION"
|
||||
npm pkg set "version=$${new_version}"
|
||||
echo "Set root package.json version to: $${new_version}"
|
||||
|
||||
# Step 3: Run prerelease:dev to update workspace versions and dependencies
|
||||
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
|
||||
entrypoint: 'npm'
|
||||
args: ['run', 'prerelease:dev'] # This will run prerelease:version and prerelease:deps
|
||||
|
||||
# Step 4: Authenticate for Docker and NPM
|
||||
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
|
||||
entrypoint: 'npm'
|
||||
args: ['run', 'auth']
|
||||
|
||||
# Step 5: Run the master release script
|
||||
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
|
||||
entrypoint: 'npm'
|
||||
args: ['run', 'publish:release']
|
||||
env:
|
||||
- 'GEMINI_SANDBOX=$_CONTAINER_TOOL'
|
||||
- 'SANDBOX_IMAGE_REGISTRY=$_SANDBOX_IMAGE_REGISTRY'
|
||||
- 'SANDBOX_IMAGE_NAME=$_SANDBOX_IMAGE_NAME'
|
||||
- 'NPM_PUBLISH_TAG=$_NPM_PUBLISH_TAG'
|
||||
|
||||
options:
|
||||
defaultLogsBucketBehavior: REGIONAL_USER_OWNED_BUCKET
|
||||
dynamicSubstitutions: true
|
||||
|
||||
substitutions:
|
||||
_REVISION: '0'
|
||||
_SANDBOX_IMAGE_REGISTRY: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers'
|
||||
_SANDBOX_IMAGE_NAME: 'gemini-cli-sandbox'
|
||||
_NPM_PUBLISH_TAG: 'head'
|
||||
_CONTAINER_TOOL: 'docker'
|
||||
@@ -24,7 +24,8 @@ steps:
|
||||
|
||||
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
|
||||
entrypoint: 'npm'
|
||||
args: ['publish', '--tag=head', '--dry-run', '--workspace=@gemini-cli/cli']
|
||||
args:
|
||||
['publish', '--tag=head', '--dry-run', '--workspace=@google/gemini-cli']
|
||||
|
||||
options:
|
||||
defaultLogsBucketBehavior: REGIONAL_USER_OWNED_BUCKET
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
steps:
|
||||
# Step 1: Install root dependencies (includes workspaces)
|
||||
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
|
||||
id: 'Install Dependencies'
|
||||
entrypoint: 'npm'
|
||||
args: ['install']
|
||||
|
||||
# Step 2: Update version in root package.json
|
||||
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
|
||||
id: 'Set version in workspace root'
|
||||
entrypoint: 'bash'
|
||||
args:
|
||||
- -c # Use bash -c to allow for command substitution and string manipulation
|
||||
- |
|
||||
current_version=$(npm pkg get version | sed 's/"//g')
|
||||
if [ "$_OFFICIAL_RELEASE" = "true" ]; then
|
||||
new_version="$current_version"
|
||||
else
|
||||
new_version="${current_version}-rc.$_REVISION"
|
||||
fi
|
||||
npm pkg set "version=${new_version}"
|
||||
echo "Set root package.json version to: ${new_version}"
|
||||
|
||||
# Step 3: Binds the package versions to the version in the repo root's package.json
|
||||
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
|
||||
id: 'Bind package versions to workspace root'
|
||||
entrypoint: 'npm'
|
||||
args: ['run', 'prerelease:dev'] # This will run prerelease:version and prerelease:deps
|
||||
|
||||
# Step 4: Authenticate for Docker (so we can push images to the artifact registry)
|
||||
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
|
||||
id: 'Authenticate docker'
|
||||
entrypoint: 'npm'
|
||||
args: ['run', 'auth']
|
||||
|
||||
# Step 5: Build workspace packages
|
||||
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
|
||||
id: 'Build packages'
|
||||
entrypoint: 'npm'
|
||||
args: ['run', 'build:packages']
|
||||
|
||||
# Step 6: Prepare CLI package.json for publishing
|
||||
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
|
||||
id: 'Prepare @google/gemini-cli and @google/gemini-cli-core packages'
|
||||
entrypoint: 'npm'
|
||||
args: ['run', 'prepare:packages']
|
||||
env:
|
||||
- 'GEMINI_SANDBOX=$_CONTAINER_TOOL'
|
||||
- 'SANDBOX_IMAGE_REGISTRY=$_SANDBOX_IMAGE_REGISTRY'
|
||||
- 'SANDBOX_IMAGE_NAME=$_SANDBOX_IMAGE_NAME'
|
||||
|
||||
# Step 7: Build sandbox container image
|
||||
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
|
||||
id: 'Build sandbox Docker image'
|
||||
entrypoint: 'npm'
|
||||
args: ['run', 'build:sandbox:fast']
|
||||
env:
|
||||
- 'GEMINI_SANDBOX=$_CONTAINER_TOOL'
|
||||
- 'SANDBOX_IMAGE_REGISTRY=$_SANDBOX_IMAGE_REGISTRY'
|
||||
- 'SANDBOX_IMAGE_NAME=$_SANDBOX_IMAGE_NAME'
|
||||
|
||||
# Step 8: Publish sandbox container image
|
||||
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
|
||||
id: 'Publish sandbox Docker image'
|
||||
entrypoint: 'npm'
|
||||
args: ['run', 'publish:sandbox']
|
||||
env:
|
||||
- 'GEMINI_SANDBOX=$_CONTAINER_TOOL'
|
||||
- 'SANDBOX_IMAGE_REGISTRY=$_SANDBOX_IMAGE_REGISTRY'
|
||||
- 'SANDBOX_IMAGE_NAME=$_SANDBOX_IMAGE_NAME'
|
||||
|
||||
# Pre-Step 9: authenticate to our intermediate npm registry
|
||||
# NOTE: when running locally, run this instead (from the `packages/core` directory):
|
||||
# - `npm login --registry https://wombat-dressing-room.appspot.com`
|
||||
# - use a 24hr token
|
||||
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
|
||||
id: 'Setup @google/gemini-cli-core auth token for publishing'
|
||||
entrypoint: 'bash'
|
||||
args:
|
||||
- -c
|
||||
- |
|
||||
echo "//wombat-dressing-room.appspot.com/:_authToken=$$CORE_PACKAGE_PUBLISH_TOKEN" > $$HOME/.npmrc
|
||||
secretEnv: ['CORE_PACKAGE_PUBLISH_TOKEN']
|
||||
|
||||
# Step 9: Publish @google/gemini-cli-core to NPM
|
||||
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
|
||||
id: 'Publish @google/gemini-cli-core package'
|
||||
entrypoint: 'bash'
|
||||
args:
|
||||
- -c
|
||||
- |
|
||||
if [ "$_OFFICIAL_RELEASE" = "true" ]; then
|
||||
npm publish --workspace=@google/gemini-cli-core --tag=latest
|
||||
else
|
||||
npm publish --workspace=@google/gemini-cli-core --tag=rc
|
||||
fi
|
||||
env:
|
||||
- 'GEMINI_SANDBOX=$_CONTAINER_TOOL'
|
||||
- 'SANDBOX_IMAGE_REGISTRY=$_SANDBOX_IMAGE_REGISTRY'
|
||||
- 'SANDBOX_IMAGE_NAME=$_SANDBOX_IMAGE_NAME'
|
||||
|
||||
# Pre-Step 10: authenticate to our intermediate npm registry
|
||||
# NOTE: when running locally, run this instead (from the `packages/cli` directory)
|
||||
# - `npm login --registry https://wombat-dressing-room.appspot.com`
|
||||
# - use a 24hr token
|
||||
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
|
||||
id: 'Setup @google/gemini-cli auth token for publishing'
|
||||
entrypoint: 'bash'
|
||||
args:
|
||||
- -c
|
||||
- |
|
||||
echo "//wombat-dressing-room.appspot.com/:_authToken=$$CLI_PACKAGE_PUBLISH_TOKEN" > $$HOME/.npmrc
|
||||
secretEnv: ['CLI_PACKAGE_PUBLISH_TOKEN']
|
||||
|
||||
# Step 10: Publish @google/gemini-cli to NPM
|
||||
- name: 'us-west1-docker.pkg.dev/gemini-code-dev/gemini-code-containers/gemini-code-builder'
|
||||
id: 'Publish @google/gemini-cli package'
|
||||
entrypoint: 'bash'
|
||||
args:
|
||||
- -c
|
||||
- |
|
||||
if [ "$_OFFICIAL_RELEASE" = "true" ]; then
|
||||
npm publish --workspace=@google/gemini-cli --tag=latest
|
||||
else
|
||||
npm publish --workspace=@google/gemini-cli --tag=rc
|
||||
fi
|
||||
env:
|
||||
- 'GEMINI_SANDBOX=$_CONTAINER_TOOL'
|
||||
- 'SANDBOX_IMAGE_REGISTRY=$_SANDBOX_IMAGE_REGISTRY'
|
||||
- 'SANDBOX_IMAGE_NAME=$_SANDBOX_IMAGE_NAME'
|
||||
|
||||
options:
|
||||
defaultLogsBucketBehavior: REGIONAL_USER_OWNED_BUCKET
|
||||
dynamicSubstitutions: true
|
||||
|
||||
availableSecrets:
|
||||
secretManager:
|
||||
- versionName: ${_CLI_PACKAGE_WOMBAT_TOKEN_RESOURCE_NAME}
|
||||
env: 'CLI_PACKAGE_PUBLISH_TOKEN'
|
||||
- versionName: ${_CORE_PACKAGE_WOMBAT_TOKEN_RESOURCE_NAME}
|
||||
env: 'CORE_PACKAGE_PUBLISH_TOKEN'
|
||||
|
||||
substitutions:
|
||||
_REVISION: '0'
|
||||
_OFFICIAL_RELEASE: 'false'
|
||||
_CONTAINER_TOOL: 'docker'
|
||||
_SANDBOX_IMAGE_REGISTRY: ''
|
||||
_SANDBOX_IMAGE_NAME: ''
|
||||
_CLI_PACKAGE_WOMBAT_TOKEN_RESOURCE_NAME: ''
|
||||
_CORE_PACKAGE_WOMBAT_TOKEN_RESOURCE_NAME: ''
|
||||
@@ -0,0 +1,24 @@
|
||||
# Set the default behavior for all files to automatically handle line endings.
|
||||
# This will ensure that all text files are normalized to use LF (line feed)
|
||||
# line endings in the repository, which helps prevent cross-platform issues.
|
||||
* text=auto eol=lf
|
||||
|
||||
# Explicitly declare files that must have LF line endings for proper execution
|
||||
# on Unix-like systems.
|
||||
*.sh eol=lf
|
||||
*.bash eol=lf
|
||||
Makefile eol=lf
|
||||
|
||||
# Explicitly declare binary file types to prevent Git from attempting to
|
||||
# normalize their line endings.
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
*.pdf binary
|
||||
*.woff binary
|
||||
*.woff2 binary
|
||||
*.eot binary
|
||||
*.ttf binary
|
||||
*.otf binary
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,52 @@
|
||||
name: Bug Report
|
||||
description: Report a bug to help us improve Gemini CLI
|
||||
labels: ['kind/bug', 'status/need-triage']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report! Please search [existing issues](https://github.com/google-gemini/gemini-cli/issues) to see if an issue already exists for the bug you encountered.
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: A clear and concise description of what the bug is.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: What did you expect to happen?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: info
|
||||
attributes:
|
||||
label: Client information
|
||||
description: Please paste the full text from the `/about` command run from Gemini CLI. Also include which platform (MacOS, Windows, Linux).
|
||||
value: |
|
||||
<details>
|
||||
|
||||
```console
|
||||
$ gemini /about
|
||||
# paste output here
|
||||
```
|
||||
|
||||
</details>
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: login-info
|
||||
attributes:
|
||||
label: Login information
|
||||
description: Describe how you are logging in (e.g., Google Account, API key).
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Anything else we need to know?
|
||||
description: Add any other context about the problem here.
|
||||
@@ -0,0 +1,30 @@
|
||||
name: Feature Request
|
||||
description: Suggest an idea for this project
|
||||
labels: ['kind/enhancement', 'status/need-triage']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to suggest an enhancement! Please search [existing issues](https://github.com/google-gemini/gemini-cli/issues) to see if a similar feature has already been requested.
|
||||
|
||||
- type: textarea
|
||||
id: feature
|
||||
attributes:
|
||||
label: What would you like to be added?
|
||||
description: A clear and concise description of the enhancement.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: rationale
|
||||
attributes:
|
||||
label: Why is this needed?
|
||||
description: A clear and concise description of why this enhancement is needed.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
@@ -7,6 +7,7 @@ on:
|
||||
branches: [main, release]
|
||||
pull_request:
|
||||
branches: [main, release]
|
||||
merge_group:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -36,7 +37,7 @@ jobs:
|
||||
git diff --exit-code
|
||||
|
||||
- name: Run linter
|
||||
run: npm run lint
|
||||
run: npm run lint:ci
|
||||
|
||||
- name: Build project
|
||||
run: npm run build
|
||||
|
||||
@@ -4,9 +4,8 @@ name: E2E Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, release]
|
||||
pull_request:
|
||||
branches: [main, release]
|
||||
branches: [main]
|
||||
merge_group:
|
||||
|
||||
jobs:
|
||||
e2e-test:
|
||||
|
||||
@@ -1 +1 @@
|
||||
@gemini-cli:registry=https://us-west1-npm.pkg.dev/gemini-code-dev/gemini-code/
|
||||
@google:registry=https://wombat-dressing-room.appspot.com
|
||||
Vendored
+2
-2
@@ -12,9 +12,9 @@
|
||||
"runtimeArgs": ["run", "start"],
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "integratedTerminal",
|
||||
"env": {
|
||||
"GEMINI_SANDBOX": "false",
|
||||
"GEMINI_API_KEY": "testkey"
|
||||
"GEMINI_SANDBOX": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
+3
-3
@@ -76,9 +76,9 @@ In the PR description, explain the "why" behind your changes and link to the rel
|
||||
|
||||
## Forking
|
||||
|
||||
If you are forking the repository you will be able to run the Built, Test and Integration test workflows. However in order to make the integration tests run you'll need to add a [Github Repository Secret](<[url](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository)>) with a value of `GEMINI_API_KEY` and set that to a valid API key that you have available. Your key and secret it private to your repo; no one without access can see your key and you cannot see any secrets related to this repo.
|
||||
If you are forking the repository you will be able to run the Built, Test and Integration test workflows. However in order to make the integration tests run you'll need to add a [GitHub Repository Secret](<[url](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository)>) with a value of `GEMINI_API_KEY` and set that to a valid API key that you have available. Your key and secret are private to your repo; no one without access can see your key and you cannot see any secrets related to this repo.
|
||||
|
||||
Additionally you will need to click on the `Actions` tab and enable workflows for your repository, you'll find its the large blue button in the center of the screen.
|
||||
Additionally you will need to click on the `Actions` tab and enable workflows for your repository, you'll find it's the large blue button in the center of the screen.
|
||||
|
||||
## Development Setup and Workflow
|
||||
|
||||
@@ -198,7 +198,7 @@ This command uses Prettier to format the code according to the project's style g
|
||||
|
||||
#### Linting
|
||||
|
||||
To separately lint the code in this project, run the following command fro the root directory:
|
||||
To separately lint the code in this project, run the following command from the root directory:
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
|
||||
+4
-4
@@ -40,11 +40,11 @@ ENV PATH=$PATH:/usr/local/share/npm-global/bin
|
||||
USER node
|
||||
|
||||
# install gemini-cli and clean up
|
||||
COPY packages/cli/dist/gemini-cli-cli-*.tgz /usr/local/share/npm-global/gemini-cli-cli.tgz
|
||||
COPY packages/core/dist/gemini-cli-core-*.tgz /usr/local/share/npm-global/gemini-cli-core.tgz
|
||||
RUN npm install -g /usr/local/share/npm-global/gemini-cli-cli.tgz /usr/local/share/npm-global/gemini-cli-core.tgz \
|
||||
COPY packages/cli/dist/google-gemini-cli-*.tgz /usr/local/share/npm-global/gemini-cli.tgz
|
||||
COPY packages/core/dist/google-gemini-cli-core-*.tgz /usr/local/share/npm-global/gemini-core.tgz
|
||||
RUN npm install -g /usr/local/share/npm-global/gemini-cli.tgz /usr/local/share/npm-global/gemini-core.tgz \
|
||||
&& npm cache clean --force \
|
||||
&& rm -f /usr/local/share/npm-global/gemini-cli-{cli,core}.tgz
|
||||
&& rm -f /usr/local/share/npm-global/gemini-{cli,core}.tgz
|
||||
|
||||
# default entrypoint when none specified
|
||||
CMD ["gemini"]
|
||||
|
||||
@@ -71,7 +71,8 @@ JavaScript classes, by their nature, are designed to encapsulate internal state
|
||||
- Reduced Boilerplate and Increased Conciseness: Classes often promote the use of constructors, this binding, getters, setters, and other boilerplate that can unnecessarily bloat code. TypeScript interface and type declarations provide powerful static type checking without the runtime overhead or verbosity of class definitions. This allows for more succinct and readable code, aligning with JavaScript's strengths in functional programming.
|
||||
|
||||
- Enhanced Readability and Predictability: Plain objects, especially when their structure is clearly defined by TypeScript interfaces, are often easier to read and understand. Their properties are directly accessible, and there's no hidden internal state or complex inheritance chains to navigate. This predictability leads to fewer bugs and a more maintainable codebase.
|
||||
Simplified Immutability: While not strictly enforced, plain objects encourage an immutable approach to data. When you need to modify an object, you typically create a new one with the desired changes, rather than mutating the original. This pattern aligns perfectly with React's reconciliation process and helps prevent subtle bugs related to shared mutable state.
|
||||
|
||||
- Simplified Immutability: While not strictly enforced, plain objects encourage an immutable approach to data. When you need to modify an object, you typically create a new one with the desired changes, rather than mutating the original. This pattern aligns perfectly with React's reconciliation process and helps prevent subtle bugs related to shared mutable state.
|
||||
|
||||
- Better Serialization and Deserialization: Plain JavaScript objects are naturally easy to serialize to JSON and deserialize back, which is a common requirement in web development (e.g., for API communication or local storage). Classes, with their methods and prototypes, can complicate this process.
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
Copyright 2025 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -2,120 +2,135 @@
|
||||
|
||||
[](https://github.com/google-gemini/gemini-cli/actions/workflows/ci.yml)
|
||||
|
||||
This repository contains the Gemini CLI tool.
|
||||

|
||||
|
||||
This repository contains the Gemini CLI, a command-line AI workflow tool that connects to your
|
||||
tools, understands your code and accelerates your workflows.
|
||||
|
||||
With the Gemini CLI you can:
|
||||
|
||||
- Query and edit large codebases in and beyond Gemini's 1M token context window.
|
||||
- Generate new apps from PDFs or sketches, using Gemini's multimodal capabilities.
|
||||
- Automate operational tasks, like querying pull requests or handling complex rebases.
|
||||
- Use tools and MCP servers to connect new capabilities, including [media generation with Imagen,
|
||||
Veo or Lyria](https://github.com/GoogleCloudPlatform/vertex-ai-creative-studio/tree/main/experiments/mcp-genmedia)
|
||||
- Ground your queries with the [Google Search](https://ai.google.dev/gemini-api/docs/grounding)
|
||||
tool, built in to Gemini.
|
||||
|
||||
## Quickstart
|
||||
|
||||
1. [Install Node 18+](https://nodejs.org/en/download).
|
||||
2. [Get an API key from Google AI Studio](https://aistudio.google.com/apikey).
|
||||
3. Set the API key in your shell using the following command, replacing `YOUR_API_KEY` with the API key you obtained: `export GEMINI_API_KEY="YOUR_API_KEY"`.
|
||||
4. Run the Gemini CLI from your shell using the following command: `npx https://github.com/google-gemini/gemini-cli`
|
||||
5. Enjoy.
|
||||
1. **Prerequisites:** Ensure you have [Node.js version 18](https://nodejs.org/en/download) or higher installed.
|
||||
2. **Run the CLI:** Execute the following command in your terminal:
|
||||
|
||||
```bash
|
||||
npx https://github.com/google-gemini/gemini-cli
|
||||
```
|
||||
|
||||
Or install it with:
|
||||
|
||||
```bash
|
||||
npm install -g @google/gemini-cli
|
||||
gemini
|
||||
```
|
||||
|
||||
3. **Pick a color theme**
|
||||
4. **Authenticate:** When prompted, sign in with your personal Google account. This will grant you up to 60 model requests per minute and 1,000 model requests per day using Gemini.
|
||||
|
||||
You are now ready to use the Gemini CLI!
|
||||
|
||||
### For advanced use or increased limits:
|
||||
|
||||
If you need to use a specific model or require a higher request capacity, you can use an API key:
|
||||
|
||||
1. Generate a key from [Google AI Studio](https://aistudio.google.com/apikey).
|
||||
2. Set it as an environment variable in your terminal. Replace `YOUR_API_KEY` with your generated key.
|
||||
|
||||
```bash
|
||||
export GEMINI_API_KEY="YOUR_API_KEY"
|
||||
```
|
||||
|
||||
For other authentication methods, including Google Workspace accounts, see the [authentication](./docs/cli/authentication.md) guide.
|
||||
|
||||
## Examples
|
||||
|
||||
Once the CLI is running, you can start interacting with Gemini from your shell. Try a simple query:
|
||||
Once the CLI is running, you can start interacting with Gemini from your shell.
|
||||
|
||||
```
|
||||
> How can I build a web app?
|
||||
You can start a project from a new directory:
|
||||
|
||||
```sh
|
||||
cd new-project/
|
||||
gemini
|
||||
> Write me a Gemini Discord bot that answers questions using a FAQ.md file I will provide
|
||||
```
|
||||
|
||||
Or ask it to perform a task using its tools:
|
||||
Or work with an existing project:
|
||||
|
||||
```
|
||||
> List files in the current directory.
|
||||
```sh
|
||||
git clone https://github.com/google-gemini/gemini-cli
|
||||
cd gemini-cli
|
||||
gemini
|
||||
> Give me a summary of all of the changes that went in yesterday
|
||||
```
|
||||
|
||||
## API Key Setup
|
||||
|
||||
The Gemini CLI requires you to authenticate with Google's AI services. You'll need to configure **one** of the following authentication methods:
|
||||
|
||||
1. **Gemini Code Assist:**
|
||||
|
||||
- To enable this mode you only need set the GEMINI_CODE_ASSIST environment variable to true.
|
||||
- You can temporarily set the environment variable in your current shell session using the following command:
|
||||
```bash
|
||||
export GEMINI_CODE_ASSIST="true"
|
||||
```
|
||||
- For repeated use, you can add the environment variable to your `.env` file (located in the project directory or user home directory) or your shell's configuration file (like `~/.bashrc`, `~/.zshrc`, or `~/.profile`). For example, the following command adds the environment variable to a `~/.bashrc` file:
|
||||
```bash
|
||||
echo 'export GEMINI_CODE_ASSIST="true"' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
- There are two types of Google Accounts you can use with Gemini CLI:
|
||||
- **Personal Google Account**: This is the standard, free account you use for services like Gmail, Google Photos, and Google Drive for personal use (e.g. your-name@gmail.com).
|
||||
- **Google Workspace Account**: This is a paid service for businesses and organizations that provides a suite of productivity tools, including a custom email domain (e.g. your-name@your-company.com), enhanced security features, and administrative controls. These accounts are often managed by an employer or school.
|
||||
- Google Workspace Account must configure a Google Cloud Project Id to use. You can temporarily set the environment variable in your current shell session using the following command:
|
||||
```bash
|
||||
export GOOGLE_CLOUD_PROJECT_ID="YOUR_PROJECT_ID"
|
||||
```
|
||||
- For repeated use, you can add the environment variable to your `.env` file (located in the project directory or user home directory) or your shell's configuration file (like `~/.bashrc`, `~/.zshrc`, or `~/.profile`). For example, the following command adds the environment variable to a `~/.bashrc` file:
|
||||
```bash
|
||||
echo 'export GOOGLE_CLOUD_PROJECT_ID="YOUR_PROJECT_ID"' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
- During start up, Gemini CLI will direct you to a webpage for authentication. Once authenticated, your credentials will be cached locally so the web login can be skipped on subsequent runs. Cached credentials last about 20 hours before expiring.
|
||||
- Note that the the web login must be done in a browser that can communicate with the machine Gemini Cli is being run from. (Specifically, the browser will be redirected to a localhost url that Gemini CLI will be listening on).
|
||||
|
||||
2. **Gemini API key:**
|
||||
|
||||
- Obtain your API key from Google AI Studio: [https://aistudio.google.com/app/apikey](https://aistudio.google.com/app/apikey)
|
||||
- Set the `GEMINI_API_KEY` environment variable. In the following methods, replace `YOUR_GEMINI_API_KEY` with the API key you obtained from Google AI Studio:
|
||||
- You can temporarily set the environment variable in your current shell session using the following command:
|
||||
```bash
|
||||
export GEMINI_API_KEY="YOUR_GEMINI_API_KEY"
|
||||
```
|
||||
- For repeated use, you can add the environment variable to your `.env` file (located in the project directory or user home directory) or your shell's configuration file (like `~/.bashrc`, `~/.zshrc`, or `~/.profile`). For example, the following command adds the environment variable to a `~/.bashrc` file:
|
||||
```bash
|
||||
echo 'export GEMINI_API_KEY="YOUR_GEMINI_API_KEY"' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
3. **Google API Key (Vertex AI Express Mode):**
|
||||
|
||||
- You can use a general Google Cloud API key if it has been enabled for the Gemini API or Vertex AI.
|
||||
- Set the `GOOGLE_API_KEY` and `GOOGLE_GENAI_USE_VERTEXAI` environment variables. In the following methods, replace `YOUR_GEMINI_API_KEY` with your Google Cloud API key:
|
||||
- You can temporarily set these environment variables in your current shell session using the following commands:
|
||||
```bash
|
||||
export GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"
|
||||
export GOOGLE_GENAI_USE_VERTEXAI=true
|
||||
```
|
||||
- For repeated use, you can add the environment variables to your `.env` file (located in the project directory or user home directory) or your shell's configuration file (like `~/.bashrc`, `~/.zshrc`, or `~/.profile`). For example, the following commands adds the environment variables to a `~/.bashrc` file:
|
||||
```bash
|
||||
echo 'export GEMINI_API_KEY="YOUR_GEMINI_API_KEY"' >> ~/.bashrc
|
||||
echo 'export GOOGLE_GENAI_USE_VERTEXAI=true' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
4. **Vertex AI (Project and Location):**
|
||||
- Ensure you have a Google Cloud project and have enabled the Vertex AI API.
|
||||
- Set up Application Default Credentials (ADC), using the following command:
|
||||
```bash
|
||||
gcloud auth application-default login
|
||||
```
|
||||
For more information, see [Set up Application Default Credentials for Google Cloud](https://cloud.google.com/docs/authentication/provide-credentials-adc).
|
||||
- Set the `GOOGLE_CLOUD_PROJECT`, `GOOGLE_CLOUD_LOCATION`, and `GOOGLE_GENAI_USE_VERTEXAI` environment variables. In the following methods, replace `YOUR_PROJECT_ID` and `YOUR_PROJECT_LOCATION` with the relevant values for your project:
|
||||
- You can temporarily set these environment variables in your current shell session using the following commands:
|
||||
```bash
|
||||
export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
|
||||
export GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION" # e.g., us-central1
|
||||
export GOOGLE_GENAI_USE_VERTEXAI=true
|
||||
```
|
||||
- For repeated use, you can add the environment variables to your `.env` file (located in the project directory or user home directory) or your shell's configuration file (like `~/.bashrc`, `~/.zshrc`, or `~/.profile`). For example, the following commands adds the environment variables to a `~/.bashrc` file:
|
||||
```bash
|
||||
echo 'export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"' >> ~/.bashrc
|
||||
echo 'export GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION"' >> ~/.bashrc
|
||||
echo 'export GOOGLE_GENAI_USE_VERTEXAI=true' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
### Next Steps
|
||||
### Next steps
|
||||
|
||||
- Learn how to [contribute to or build from the source](./CONTRIBUTING.md).
|
||||
- Explore the available **[CLI Commands](./docs/cli/commands.md)**.
|
||||
- If you encounter any issues, review the **[Troubleshooting guide](./docs/troubleshooting.md)**.
|
||||
- For more comprehensive documentation, see the [full documentation](./docs/index.md).
|
||||
- Take a look at some [popular tasks](#popular-tasks) for more inspiration.
|
||||
|
||||
## Gemini APIs
|
||||
### Troubleshooting
|
||||
|
||||
This project leverages the Gemini APIs to provide AI capabilities. For details on the terms of service governing the Gemini API, please refer to the [Gemini API Terms of Service](https://ai.google.dev/gemini-api/terms).
|
||||
Head over to the [troubleshooting](docs/troubleshooting.md) guide if you're
|
||||
having issues.
|
||||
|
||||
## Popular tasks
|
||||
|
||||
### Explore a new codebase
|
||||
|
||||
Start by `cd`ing into an existing or newly-cloned repository and running `gemini`.
|
||||
|
||||
```text
|
||||
> Describe the main pieces of this system's architecture.
|
||||
```
|
||||
|
||||
```text
|
||||
> What security mechanisms are in place?
|
||||
```
|
||||
|
||||
### Work with your existing code
|
||||
|
||||
```text
|
||||
> Implement a first draft for GitHub issue #123.
|
||||
```
|
||||
|
||||
```text
|
||||
> Help me migrate this codebase to the latest version of Java. Start with a plan.
|
||||
```
|
||||
|
||||
### Automate your workflows
|
||||
|
||||
Use MCP servers to integrate your local system tools with your enterprise collaboration suite.
|
||||
|
||||
```text
|
||||
> Make me a slide deck showing the git history from the last 7 days, grouped by feature and team member.
|
||||
```
|
||||
|
||||
```text
|
||||
> Make a full-screen web app for a wall display to show our most interacted-with GitHub issues.
|
||||
```
|
||||
|
||||
### Interact with your system
|
||||
|
||||
```text
|
||||
> Convert all the images in this directory to png, and rename them to use dates from the exif data.
|
||||
```
|
||||
|
||||
```text
|
||||
> Organise my PDF invoices by month of expenditure.
|
||||
```
|
||||
|
||||
## Terms of Service and Privacy Notice
|
||||
|
||||
For details on the terms of service and privacy notice applicable to your use of Gemini CLI, see the [Terms of Service and Privacy Notice](./docs/tos-privacy.md).
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 350 KiB |
@@ -0,0 +1,75 @@
|
||||
# Checkpointing
|
||||
|
||||
The Gemini CLI includes a Checkpointing feature that automatically saves a snapshot of your project's state before any file modifications are made by AI-powered tools. This allows you to safely experiment with and apply code changes, knowing you can instantly revert back to the state before the tool was run.
|
||||
|
||||
## How It Works
|
||||
|
||||
When you approve a tool that modifies the file system (like `write_file` or `replace`), the CLI automatically creates a "checkpoint." This checkpoint includes:
|
||||
|
||||
1. **A Git Snapshot:** A commit is made in a special, shadow Git repository located in your home directory (`~/.gemini/history/<project_hash>`). This snapshot captures the complete state of your project files at that moment. It does **not** interfere with your own project's Git repository.
|
||||
2. **Conversation History:** The entire conversation you've had with the agent up to that point is saved.
|
||||
3. **The Tool Call:** The specific tool call that was about to be executed is also stored.
|
||||
|
||||
If you want to undo the change or simply go back, you can use the `/restore` command. Restoring a checkpoint will:
|
||||
|
||||
- Revert all files in your project to the state captured in the snapshot.
|
||||
- Restore the conversation history in the CLI.
|
||||
- Re-propose the original tool call, allowing you to run it again, modify it, or simply ignore it.
|
||||
|
||||
All checkpoint data, including the Git snapshot and conversation history, is stored locally on your machine. The Git snapshot is stored in the shadow repository while the conversation history and tool calls are saved in a JSON file in your project's temporary directory, typically located at `~/.gemini/tmp/<project_hash>/checkpoints`.
|
||||
|
||||
## Enabling the Feature
|
||||
|
||||
The Checkpointing feature is disabled by default. To enable it, you can either use a command-line flag or edit your `settings.json` file.
|
||||
|
||||
### Using the Command-Line Flag
|
||||
|
||||
You can enable checkpointing for the current session by using the `--checkpointing` flag when starting the Gemini CLI:
|
||||
|
||||
```bash
|
||||
gemini --checkpointing
|
||||
```
|
||||
|
||||
### Using the `settings.json` File
|
||||
|
||||
To enable checkpointing by default for all sessions, you need to edit your `settings.json` file.
|
||||
|
||||
Add the following key to your `settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"checkpointing": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Using the `/restore` Command
|
||||
|
||||
Once enabled, checkpoints are created automatically. To manage them, you use the `/restore` command.
|
||||
|
||||
### List Available Checkpoints
|
||||
|
||||
To see a list of all saved checkpoints for the current project, simply run:
|
||||
|
||||
```
|
||||
/restore
|
||||
```
|
||||
|
||||
The CLI will display a list of available checkpoint files. These file names are typically composed of a timestamp, the name of the file being modified, and the name of the tool that was about to be run (e.g., `2025-06-22T10-00-00_000Z-my-file.txt-write_file`).
|
||||
|
||||
### Restore a Specific Checkpoint
|
||||
|
||||
To restore your project to a specific checkpoint, use the checkpoint file from the list:
|
||||
|
||||
```
|
||||
/restore <checkpoint_file>
|
||||
```
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
/restore 2025-06-22T10-00-00_000Z-my-file.txt-write_file
|
||||
```
|
||||
|
||||
After running the command, your files and conversation will be immediately restored to the state they were in when the checkpoint was created, and the original tool prompt will reappear.
|
||||
@@ -0,0 +1,81 @@
|
||||
## Authentication Setup
|
||||
|
||||
The Gemini CLI requires you to authenticate with Google's AI services. On initial startup you'll need to configure **one** of the following authentication methods:
|
||||
|
||||
1. **Login with Google (Gemini Code Assist):**
|
||||
|
||||
- Use this option to log in with your google account.
|
||||
- During initial startup, Gemini CLI will direct you to a webpage for authentication. Once authenticated, your credentials will be cached locally so the web login can be skipped on subsequent runs.
|
||||
- Note that the web login must be done in a browser that can communicate with the machine Gemini CLI is being run from. (Specifically, the browser will be redirected to a localhost url that Gemini CLI will be listening on).
|
||||
- <a id="workspace-gca">Users may have to specify a GOOGLE_CLOUD_PROJECT if:</a>
|
||||
|
||||
1. You have a Google Workspace account. Google Workspace is a paid service for businesses and organizations that provides a suite of productivity tools, including a custom email domain (e.g. your-name@your-company.com), enhanced security features, and administrative controls. These accounts are often managed by an employer or school.
|
||||
1. You have recieved a free Code Assist license through the [Google Developer Program](https://developers.google.com/program/plans-and-pricing) (including qualified Google Developer Experts)
|
||||
1. You have been assigned a license to a current Gemini Code Assist standard or enterprise subscription.
|
||||
1. You are using the product outside the the [supported regions](https://developers.google.com/gemini-code-assist/resources/available-locations) for free individual usage.>
|
||||
1. You are a Google account holder under the age of 18
|
||||
|
||||
- If you fall into one of these categories, you must first configure a Google Cloud Project Id to use, [enable the Gemini for Cloud API](https://cloud.google.com/gemini/docs/discover/set-up-gemini#enable-api) and [configure access permissions](https://cloud.google.com/gemini/docs/discover/set-up-gemini#grant-iam).
|
||||
|
||||
You can temporarily set the environment variable in your current shell session using the following command:
|
||||
|
||||
```bash
|
||||
export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
|
||||
```
|
||||
|
||||
- For repeated use, you can add the environment variable to your `.env` file (located in the project directory or user home directory) or your shell's configuration file (like `~/.bashrc`, `~/.zshrc`, or `~/.profile`). For example, the following command adds the environment variable to a `~/.bashrc` file:
|
||||
|
||||
```bash
|
||||
echo 'export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
2. **<a id="gemini-api-key"></a>Gemini API key:**
|
||||
|
||||
- Obtain your API key from Google AI Studio: [https://aistudio.google.com/app/apikey](https://aistudio.google.com/app/apikey)
|
||||
- Set the `GEMINI_API_KEY` environment variable. In the following methods, replace `YOUR_GEMINI_API_KEY` with the API key you obtained from Google AI Studio:
|
||||
- You can temporarily set the environment variable in your current shell session using the following command:
|
||||
```bash
|
||||
export GEMINI_API_KEY="YOUR_GEMINI_API_KEY"
|
||||
```
|
||||
- For repeated use, you can add the environment variable to your `.env` file (located in the project directory or user home directory) or your shell's configuration file (like `~/.bashrc`, `~/.zshrc`, or `~/.profile`). For example, the following command adds the environment variable to a `~/.bashrc` file:
|
||||
```bash
|
||||
echo 'export GEMINI_API_KEY="YOUR_GEMINI_API_KEY"' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
3. **Vertex AI:**
|
||||
- If not using express mode:
|
||||
- Ensure you have a Google Cloud project and have enabled the Vertex AI API.
|
||||
- Set up Application Default Credentials (ADC), using the following command:
|
||||
```bash
|
||||
gcloud auth application-default login
|
||||
```
|
||||
For more information, see [Set up Application Default Credentials for Google Cloud](https://cloud.google.com/docs/authentication/provide-credentials-adc).
|
||||
- Set the `GOOGLE_CLOUD_PROJECT`, `GOOGLE_CLOUD_LOCATION`, and `GOOGLE_GENAI_USE_VERTEXAI` environment variables. In the following methods, replace `YOUR_PROJECT_ID` and `YOUR_PROJECT_LOCATION` with the relevant values for your project:
|
||||
- You can temporarily set these environment variables in your current shell session using the following commands:
|
||||
```bash
|
||||
export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"
|
||||
export GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION" # e.g., us-central1
|
||||
export GOOGLE_GENAI_USE_VERTEXAI=true
|
||||
```
|
||||
- For repeated use, you can add the environment variables to your `.env` file (located in the project directory or user home directory) or your shell's configuration file (like `~/.bashrc`, `~/.zshrc`, or `~/.profile`). For example, the following commands add the environment variables to a `~/.bashrc` file:
|
||||
```bash
|
||||
echo 'export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"' >> ~/.bashrc
|
||||
echo 'export GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION"' >> ~/.bashrc
|
||||
echo 'export GOOGLE_GENAI_USE_VERTEXAI=true' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
- If using express mode:
|
||||
- Set the `GOOGLE_API_KEY` environment variable. In the following methods, replace `YOUR_GOOGLE_API_KEY` with your Vertex AI API key provided by express mode:
|
||||
- You can temporarily set these environment variables in your current shell session using the following commands:
|
||||
```bash
|
||||
export GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"
|
||||
export GOOGLE_GENAI_USE_VERTEXAI=true
|
||||
```
|
||||
- For repeated use, you can add the environment variables to your `.env` file (located in the project directory or user home directory) or your shell's configuration file (like `~/.bashrc`, `~/.zshrc`, or `~/.profile`). For example, the following commands add the environment variables to a `~/.bashrc` file:
|
||||
```bash
|
||||
echo 'export GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"' >> ~/.bashrc
|
||||
echo 'export GOOGLE_GENAI_USE_VERTEXAI=true' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
+30
-1
@@ -10,6 +10,19 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
|
||||
- **Description:** File an issue about Gemini CLI. By default, the issue is filed within the GitHub repository for Gemini CLI. The string you enter after `/bug` will become the headline for the bug being filed. The default `/bug` behavior can be modified using the `bugCommand` setting in your `.gemini/settings.json` files.
|
||||
|
||||
- **`/chat`**
|
||||
|
||||
- **Description:** Save and resume conversation history for branching conversation state interactively, or resuming a previous state from a later session.
|
||||
- **Sub-commands:**
|
||||
- **`save`**
|
||||
- **Description:** Saves the current conversation history. You must add a `<tag>` for identifying the conversation state.
|
||||
- **Usage:** `/chat save <tag>`
|
||||
- **`resume`**
|
||||
- **Description:** Resumes a conversation from a previous save.
|
||||
- **Usage:** `/chat resume <tag>`
|
||||
- **`list`**
|
||||
- **Description:** Lists available tags for chat state resumption.
|
||||
|
||||
- **`/clear`**
|
||||
|
||||
- **Description:** Clear the terminal screen, including the visible session history and scrollback within the CLI. The underlying session data (for history recall) might be preserved depending on the exact implementation, but the visual display is cleared.
|
||||
@@ -43,20 +56,36 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
|
||||
- **Description:** Manage the AI's instructional context (hierarchical memory loaded from `GEMINI.md` files).
|
||||
- **Sub-commands:**
|
||||
- **`add`**:
|
||||
- **Description:** Adds the following text to the AI's memory. Usage: `/memory add <text to remember>`
|
||||
- **`show`**:
|
||||
- **Description:** Display the full, concatenated content of the current hierarchical memory that has been loaded from all `GEMINI.md` files. This lets you inspect the instructional context being provided to the Gemini model.
|
||||
- **`refresh`**:
|
||||
- **Description:** Reload the hierarchical instructional memory from all `GEMINI.md` files found in the configured locations (global, project/ancestors, and sub-directories). This command updates the model with the latest `GEMINI.md` content.
|
||||
- **Note:** For more details on how `GEMINI.md` files contribute to hierarchical memory, see the [CLI Configuration documentation](./configuration.md#4-geminimd-files-hierarchical-instructional-context).
|
||||
|
||||
- **`/restore`**
|
||||
|
||||
- **Description:** Restores the project files to the state they were in just before a tool was executed. This is particularly useful for undoing file edits made by a tool. If run without a tool call ID, it will list available checkpoints to restore from.
|
||||
- **Usage:** `/restore [tool_call_id]`
|
||||
- **Note:** Only available if the CLI is invoked with the `--checkpointing` option or configured via [settings](./configuration.md). See [Checkpointing documentation](../checkpointing.md) for more details.
|
||||
|
||||
- **`/stats`**
|
||||
|
||||
- **Description:** Display detailed statistics for the current Gemini CLI session, such as the session duration.
|
||||
- **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.
|
||||
|
||||
- [**`/theme`**](./themes.md)
|
||||
|
||||
- **Description:** Open a dialog that lets you change the visual theme of Gemini CLI.
|
||||
|
||||
- **`/auth`**
|
||||
|
||||
- **Description:** Open a dialog that lets you change the authentication method.
|
||||
|
||||
- **`/about`**
|
||||
|
||||
- **Description:** Show version info. Please share this information when filing issues.
|
||||
|
||||
- [**`/tools`**](../tools/index.md)
|
||||
|
||||
- **Description:** Display a list of tools that are currently available within Gemini CLI.
|
||||
|
||||
+116
-24
@@ -30,7 +30,6 @@ Gemini CLI uses `settings.json` files for persistent configuration. There are tw
|
||||
In addition to a project settings file, a project's `.gemini` directory can contain other project-specific files related to Gemini CLI's operation, such as:
|
||||
|
||||
- [Custom sandbox profiles](#sandboxing) (e.g., `.gemini/sandbox-macos-custom.sb`, `.gemini/sandbox.Dockerfile`).
|
||||
- [Telemtry configurations](../core/telemetry.md#running-an-otel-collector) (e.g., `.gemini/otel/`).
|
||||
|
||||
### Available settings in `settings.json`:
|
||||
|
||||
@@ -43,26 +42,28 @@ In addition to a project settings file, a project's `.gemini` directory can cont
|
||||
- **`bugCommand`** (object):
|
||||
|
||||
- **Description:** Overrides the default URL for the `/bug` command.
|
||||
- **Default:** `"urlTemplate": "https://github.com/google-gemini/gemini-cli/issues/new?template=bug_report.md&title={title}&body={body}"`
|
||||
- **Default:** `"urlTemplate": "https://github.com/google-gemini/gemini-cli/issues/new?template=bug_report.yml&title={title}&info={info}"`
|
||||
- **Properties:**
|
||||
- **`urlTemplate`** (string): A URL that can contain `{title}` and `{body}` placeholders.
|
||||
- **`urlTemplate`** (string): A URL that can contain `{title}` and `{info}` placeholders.
|
||||
- **Example:**
|
||||
```json
|
||||
"bugCommand": {
|
||||
"urlTemplate": "https://bug.example.com/new?title={title}&body={body}"
|
||||
"urlTemplate": "https://bug.example.com/new?title={title}&info={info}"
|
||||
}
|
||||
```
|
||||
|
||||
- **`fileFiltering`** (object):
|
||||
|
||||
- **Description:** Controls git-aware file filtering behavior for @ commands and file discovery tools.
|
||||
- **Default:** `"respectGitIgnore": true`
|
||||
- **Default:** `"respectGitIgnore": true, "enableRecursiveFileSearch": true`
|
||||
- **Properties:**
|
||||
- **`respectGitIgnore`** (boolean): Whether to respect .gitignore patterns when discovering files. When set to `true`, git-ignored files (like `node_modules/`, `dist/`, `.env`) are automatically excluded from @ commands and file listing operations.
|
||||
- **`enableRecursiveFileSearch`** (boolean): Whether to enable searching recursively for filenames under the current tree when completing @ prefixes in the prompt.
|
||||
- **Example:**
|
||||
```json
|
||||
"fileFiltering": {
|
||||
"respectGitIgnore": true
|
||||
"respectGitIgnore": true,
|
||||
"enableRecursiveFileSearch": false
|
||||
}
|
||||
```
|
||||
|
||||
@@ -147,9 +148,43 @@ In addition to a project settings file, a project's `.gemini` directory can cont
|
||||
}
|
||||
```
|
||||
|
||||
- **`mcpServerCommand`** (string, **deprecated**):
|
||||
- **`checkpointing`** (object):
|
||||
|
||||
- This is a legacy setting for configuring a single MCP server. Use `mcpServers` instead.
|
||||
- **Description:** Configures the checkpointing feature, which allows you to save and restore conversation and file states. See the [Checkpointing documentation](../checkpointing.md) for more details.
|
||||
- **Default:** `{"enabled": false}`
|
||||
- **Properties:**
|
||||
- **`enabled`** (boolean): When `true`, the `/restore` command is available.
|
||||
|
||||
- **`preferredEditor`** (string):
|
||||
|
||||
- **Description:** Specifies the preferred editor to use for viewing diffs.
|
||||
- **Default:** `vscode`
|
||||
- **Example:** `"preferredEditor": "vscode"`
|
||||
|
||||
- **`telemetry`** (object)
|
||||
- **Description:** Configures logging and metrics collection for Gemini CLI. For more information, see [Telemetry](../telemetry.md).
|
||||
- **Default:** `{"enabled": false, "target": "local", "otlpEndpoint": "http://localhost:4317", "logPrompts": true}`
|
||||
- **Properties:**
|
||||
- **`enabled`** (boolean): Whether or not telemetry is enabled.
|
||||
- **`target`** (string): The destination for collected telemetry. Supported values are `local` and `gcp`.
|
||||
- **`otlpEndpoint`** (string): The endpoint for the OTLP Exporter.
|
||||
- **`logPrompts`** (boolean): Whether or not to include the content of user prompts in the logs.
|
||||
- **Example:**
|
||||
```json
|
||||
"telemetry": {
|
||||
"enabled": true,
|
||||
"target": "local",
|
||||
"otlpEndpoint": "http://localhost:16686",
|
||||
"logPrompts": false
|
||||
}
|
||||
```
|
||||
- **`usageStatisticsEnabled`** (boolean):
|
||||
- **Description:** Enables or disables the collection of usage statistics. See [Usage Statistics](#usage-statistics) for more information.
|
||||
- **Default:** `true`
|
||||
- **Example:**
|
||||
```json
|
||||
"usageStatisticsEnabled": false
|
||||
```
|
||||
|
||||
### Example `settings.json`:
|
||||
|
||||
@@ -167,10 +202,25 @@ In addition to a project settings file, a project's `.gemini` directory can cont
|
||||
"command": "node",
|
||||
"args": ["mcp_server.js", "--verbose"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"telemetry": {
|
||||
"enabled": true,
|
||||
"target": "local",
|
||||
"otlpEndpoint": "http://localhost:4317",
|
||||
"logPrompts": true
|
||||
},
|
||||
"usageStatisticsEnabled": true
|
||||
}
|
||||
```
|
||||
|
||||
## Shell History
|
||||
|
||||
The CLI keeps a history of shell commands you run. To avoid conflicts between different projects, this history is stored in a project-specific directory within your user's home folder.
|
||||
|
||||
- **Location:** `~/.gemini/tmp/<project_hash>/shell_history`
|
||||
- `<project_hash>` is a unique identifier generated from your project's root path.
|
||||
- The history is stored in a file named `shell_history`.
|
||||
|
||||
## Environment Variables & `.env` Files
|
||||
|
||||
Environment variables are a common way to configure applications, especially for sensitive information like API keys or for settings that might change between environments.
|
||||
@@ -187,18 +237,21 @@ The CLI automatically loads environment variables from an `.env` file. The loadi
|
||||
- Set this in your shell profile (e.g., `~/.bashrc`, `~/.zshrc`) or an `.env` file.
|
||||
- **`GEMINI_MODEL`**:
|
||||
- Specifies the default Gemini model to use.
|
||||
- Overrides the hardcoded default, which is currently `gemini-2.5-pro-preview-05-06`.
|
||||
- Example: `export GEMINI_MODEL="gemini-1.5-flash-latest"`
|
||||
- Overrides the hardcoded default
|
||||
- Example: `export GEMINI_MODEL="gemini-2.5-flash"`
|
||||
- **`GOOGLE_API_KEY`**:
|
||||
- Your Google Cloud API key.
|
||||
- Required for using Vertex AI in express mode.
|
||||
- Ensure you have the necessary permissions and set the `GOOGLE_GENAI_USE_VERTEXAI=true` environment variable.
|
||||
- Example: `export GOOGLE_API_KEY="YOUR_GOOGLE_API_ KEY"`.
|
||||
- Example: `export GOOGLE_API_KEY="YOUR_GOOGLE_API_KEY"`.
|
||||
- **`GOOGLE_CLOUD_PROJECT`**:
|
||||
- Your Google Cloud Project ID.
|
||||
- Required for using Code Assist or Vertex AI.
|
||||
- If using Vertex AI, ensure you have the necessary permissions and set the `GOOGLE_GENAI_USE_VERTEXAI=true` environment variable.
|
||||
- Example: `export GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"`.
|
||||
- **`GOOGLE_APPLICATION_CREDENTIALS`** (string):
|
||||
- **Description:** The path to your Google Application Credentials JSON file.
|
||||
- **Example:** `export GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/credentials.json"`
|
||||
- **`OTLP_GOOGLE_CLOUD_PROJECT`**:
|
||||
- Your Google Cloud Project ID for Telemetry in Google Cloud
|
||||
- Example: `export OTLP_GOOGLE_CLOUD_PROJECT="YOUR_PROJECT_ID"`.
|
||||
@@ -207,11 +260,6 @@ The CLI automatically loads environment variables from an `.env` file. The loadi
|
||||
- Required for using Vertex AI in non express mode.
|
||||
- If using Vertex AI, ensure you have the necessary permissions and set the `GOOGLE_GENAI_USE_VERTEXAI=true` environment variable.
|
||||
- Example: `export GOOGLE_CLOUD_LOCATION="YOUR_PROJECT_LOCATION"`.
|
||||
- **`GEMINI_CODE_ASSIST`**:
|
||||
- Enables Code Assist functionality.
|
||||
- Accepts `true`, `false`, or a custom command string.
|
||||
- If you are using an Enterprise account you should also set the `GOOGLE_CLOUD_PROJECT` environment variable.
|
||||
- Example: `export GEMINI_CODE_ASSIST=true`.
|
||||
- **`GEMINI_SANDBOX`**:
|
||||
- Alternative to the `sandbox` setting in `settings.json`.
|
||||
- Accepts `true`, `false`, `docker`, `podman`, or a custom command string.
|
||||
@@ -234,19 +282,37 @@ The CLI automatically loads environment variables from an `.env` file. The loadi
|
||||
|
||||
Arguments passed directly when running the CLI can override other configurations for that specific session.
|
||||
|
||||
- **`--model <model_name>`** (or **`-m <model_name>`**):
|
||||
- **`--model <model_name>`** (**`-m <model_name>`**):
|
||||
- Specifies the Gemini model to use for this session.
|
||||
- Example: `npm start -- --model gemini-1.5-pro-latest`
|
||||
- **`--sandbox`** (or **`-s`**):
|
||||
- Enables sandbox mode for this session. The exact behavior might depend on other sandbox configurations (environment variables, settings files).
|
||||
- **`--debug_mode`** (or **`-d`**):
|
||||
- **`--prompt <your_prompt>`** (**`-p <your_prompt>`**):
|
||||
- Used to pass a prompt directly to the command. This invokes Gemini CLI in a non-interactive mode.
|
||||
- **`--sandbox`** (**`-s`**):
|
||||
- Enables sandbox mode for this session.
|
||||
- **`--sandbox-image`**:
|
||||
- Sets the sandbox image URI.
|
||||
- **`--debug_mode`** (**`-d`**):
|
||||
- Enables debug mode for this session, providing more verbose output.
|
||||
- **`--question <your_question>`** (or **`-q <your_question>`**):
|
||||
- Used to pass a question directly to the command, especially when piping input to the CLI.
|
||||
- **`--all_files`** (or **`-a`**):
|
||||
- **`--all_files`** (**`-a`**):
|
||||
- If set, recursively includes all files within the current directory as context for the prompt.
|
||||
- **`--help`** (or **`-h`**):
|
||||
- Displays help information about command-line arguments.
|
||||
- **`--show_memory_usage`**:
|
||||
- Displays the current memory usage.
|
||||
- **`--yolo`**:
|
||||
- Enables YOLO mode, which automatically approves all tool calls.
|
||||
- **`--telemetry`**:
|
||||
- Enables [telemetry](../telemetry.md).
|
||||
- **`--telemetry-target`**:
|
||||
- Sets the telemetry target. See [telemetry](../telemetry.md) for more information.
|
||||
- **`--telemetry-otlp-endpoint`**:
|
||||
- Sets the OTLP endpoint for telemetry. See [telemetry](../telemetry.md) for more information.
|
||||
- **`--telemetry-log-prompts`**:
|
||||
- Enables logging of prompts for telemetry. See [telemetry](../telemetry.md) for more information.
|
||||
- **`--checkpointing`**:
|
||||
- Enables [checkpointing](./commands.md#checkpointing-commands).
|
||||
- **`--version`**:
|
||||
- Displays the version of the CLI.
|
||||
|
||||
## Context Files (Hierarchical Instructional Context)
|
||||
|
||||
@@ -335,3 +401,29 @@ When `.gemini/sandbox.Dockerfile` exists, you can use `BUILD_SANDBOX` environmen
|
||||
```bash
|
||||
BUILD_SANDBOX=1 gemini -s
|
||||
```
|
||||
|
||||
## Usage Statistics
|
||||
|
||||
To help us improve the Gemini CLI, we collect anonymized usage statistics. This data helps us understand how the CLI is used, identify common issues, and prioritize new features.
|
||||
|
||||
**What we collect:**
|
||||
|
||||
- **Tool Calls:** We log the names of the tools that are called, whether they succeed or fail, and how long they take to execute. We do not collect the arguments passed to the tools or any data returned by them.
|
||||
- **API Requests:** We log the Gemini model used for each request, the duration of the request, and whether it was successful. We do not collect the content of the prompts or responses.
|
||||
- **Session Information:** We collect information about the configuration of the CLI, such as the enabled tools and the approval mode.
|
||||
|
||||
**What we DON'T collect:**
|
||||
|
||||
- **Personally Identifiable Information (PII):** We do not collect any personal information, such as your name, email address, or API keys.
|
||||
- **Prompt and Response Content:** We do not log the content of your prompts or the responses from the Gemini model.
|
||||
- **File Content:** We do not log the content of any files that are read or written by the CLI.
|
||||
|
||||
**How to opt out:**
|
||||
|
||||
You can opt out of usage statistics collection at any time by setting the `usageStatisticsEnabled` property to `false` in your `settings.json` file:
|
||||
|
||||
```json
|
||||
{
|
||||
"usageStatisticsEnabled": false
|
||||
}
|
||||
```
|
||||
|
||||
@@ -4,8 +4,10 @@ Within Gemini CLI, `packages/cli` is the frontend for users to send and receive
|
||||
|
||||
## Navigating this section
|
||||
|
||||
- **[Authentication](./authentication.md):** A guide to setting up authentication with Google's AI services.
|
||||
- **[Commands](./commands.md):** A reference for Gemini CLI commands (e.g., `/help`, `/tools`, `/theme`).
|
||||
- **[Configuration](./configuration.md):** A guide to tailoring Gemini CLI behavior using configuration files.
|
||||
- **[Token Caching](./token-caching.md):** Optimize API costs through token caching.
|
||||
- **[Themes](./themes.md)**: A guide to customizing the CLI's appearance with different themes.
|
||||
- **[Tutorials](tutorials.md)**: A tutorial showing how to use Gemini CLI to automate a development task.
|
||||
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ Gemini CLI comes with a selection of pre-defined themes, which you can list usin
|
||||
|
||||
### Theme Persistence
|
||||
|
||||
Selected themes are saved in Gemini CLI's [configuration](./docs/cli/configuration.md) so your preference is remembered across sessions.
|
||||
Selected themes are saved in Gemini CLI's [configuration](./configuration.md) so your preference is remembered across sessions.
|
||||
|
||||
## Dark Themes
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Token Caching and Cost Optimization
|
||||
|
||||
Gemini CLI automatically optimizes API costs through token caching when using API key authentication (Gemini API key or Vertex AI). This feature reuses previous system instructions and context to reduce the number of tokens processed in subsequent requests.
|
||||
|
||||
**Token caching is available for:**
|
||||
|
||||
- API key users (Gemini API key)
|
||||
- Vertex AI users (with project and location setup)
|
||||
|
||||
**Token caching is not available for:**
|
||||
|
||||
- OAuth users (Google Personal/Enterprise accounts) - the Code Assist API does not support cached content creation at this time
|
||||
|
||||
You can view your token usage and cached token savings using the `/stats` command. When cached tokens are available, they will be displayed in the stats output.
|
||||
+21
-43
@@ -1,76 +1,54 @@
|
||||
# Gemini CLI Core
|
||||
|
||||
The Gemini CLI Core (`packages/core`) acts as the backend engine for the Gemini CLI, handling communication with the Gemini API, managing tools, and processing requests from the CLI client. For a general overview of the Gemini CLI, see the [main documentation page](../index.md).
|
||||
Gemini CLI's core package (`packages/core`) is the backend portion of Gemini CLI, handling communication with the Gemini API, managing tools, and processing requests sent from `packages/cli`. For a general overview of Gemini CLI, see the [main documentation page](../index.md).
|
||||
|
||||
## Navigating this Section
|
||||
## Navigating this section
|
||||
|
||||
- **[Core Configuration](../cli/configuration.md):** Details on how to configure the core component, including environment variables and specific settings.
|
||||
- **[Core Tools API](./tools-api.md):** Information on how tools are defined, registered, and used by the core.
|
||||
- **[Core tools API](./tools-api.md):** Information on how tools are defined, registered, and used by the core.
|
||||
|
||||
## Role of the Core
|
||||
## Role of the core
|
||||
|
||||
The core package is a crucial part of the Gemini CLI ecosystem. While the CLI (`packages/cli`) provides the user interface, the core is responsible for:
|
||||
While the `packages/cli` portion of Gemini CLI provides the user interface, `packages/core` is responsible for:
|
||||
|
||||
- **API Interaction:** Securely communicating with the Google Gemini API, sending user prompts, and receiving model responses.
|
||||
- **Prompt Engineering:** Constructing effective prompts for the Gemini model, potentially incorporating conversation history, tool definitions, and instructional context from `GEMINI.md` files.
|
||||
- **Tool Management & Orchestration:**
|
||||
- **Gemini API interaction:** Securely communicating with the Google Gemini API, sending user prompts, and receiving model responses.
|
||||
- **Prompt engineering:** Constructing effective prompts for the Gemini model, potentially incorporating conversation history, tool definitions, and instructional context from `GEMINI.md` files.
|
||||
- **Tool management & orchestration:**
|
||||
- Registering available tools (e.g., file system tools, shell command execution).
|
||||
- Interpreting tool use requests from the Gemini model.
|
||||
- Executing the requested tools with the provided arguments.
|
||||
- Returning tool execution results to the Gemini model for further processing.
|
||||
- **Session and State Management:** Keeping track of the conversation state, including history and any relevant context required for coherent interactions.
|
||||
- **Session and state management:** Keeping track of the conversation state, including history and any relevant context required for coherent interactions.
|
||||
- **Configuration:** Managing core-specific configurations, such as API key access, model selection, and tool settings.
|
||||
|
||||
## Key Components and Functionality
|
||||
|
||||
While the exact implementation details are within the `packages/core/src/` directory, key conceptual components include:
|
||||
|
||||
- **API Client** (`client.ts`): A module responsible for making HTTP requests to the Gemini API, handling authentication, and parsing responses.
|
||||
- **Prompt Management** (`prompts.ts`): Logic for creating and formatting the prompts sent to the Gemini model. This includes integrating user queries, historical context, and tool specifications.
|
||||
- **Tool Registry and Execution** (`tool-registry.ts`, `tools.ts`, individual tool files like `read-file.ts`, `shell.ts`):
|
||||
- A system for discovering, registering, and describing available tools to the Gemini model.
|
||||
- Code for executing each tool safely and effectively, often involving interaction with the operating system or external services.
|
||||
- **Configuration (`config.ts`):** Handles loading and providing access to core-side configurations, including API keys, model choices, and potentially tool-specific settings.
|
||||
- **Turn Management (`turn.ts`):** Manages the flow of a single conversational turn, from receiving user input to generating a final response, potentially involving multiple tool calls.
|
||||
|
||||
## Interaction with the CLI
|
||||
|
||||
The CLI and Core typically communicate over a local interface (e.g., standard input/output, or a local network connection if designed for broader use, though the current structure suggests a tightly coupled Node.js application).
|
||||
|
||||
1. The CLI captures user input and forwards it to the Core.
|
||||
2. The Core processes the input, interacts with the Gemini API and tools as needed.
|
||||
3. The Core sends responses (text, tool calls, errors) back to the CLI.
|
||||
4. The CLI formats and displays these responses to the user.
|
||||
|
||||
## Security Considerations
|
||||
## Security considerations
|
||||
|
||||
The core plays a vital role in security:
|
||||
|
||||
- **API Key Management:** It handles the `GEMINI_API_KEY` and ensures it is used securely when communicating with the Gemini API.
|
||||
- **Tool Execution:** When tools interact with the local system (e.g., `run_shell_command`), the core (and its underlying tool implementations) must do so with appropriate caution, often involving sandboxing mechanisms to prevent unintended side effects.
|
||||
- **API key management:** It handles the `GEMINI_API_KEY` and ensures it's used securely when communicating with the Gemini API.
|
||||
- **Tool execution:** When tools interact with the local system (e.g., `run_shell_command`), the core (and its underlying tool implementations) must do so with appropriate caution, often involving sandboxing mechanisms to prevent unintended modifications.
|
||||
|
||||
## Chat History Compression
|
||||
## Chat history compression
|
||||
|
||||
To ensure that long conversations don't exceed the token limits of the Gemini model, the CLI includes a chat history compression feature.
|
||||
To ensure that long conversations don't exceed the token limits of the Gemini model, the core includes a chat history compression feature.
|
||||
|
||||
When a conversation approaches the token limit for the configured model, the CLI will automatically compress the conversation history before sending it to the model. This compression is designed to be lossless in terms of the information conveyed, but it reduces the overall number of tokens used.
|
||||
When a conversation approaches the token limit for the configured model, the core automatically compresses the conversation history before sending it to the model. This compression is designed to be lossless in terms of the information conveyed, but it reduces the overall number of tokens used.
|
||||
|
||||
You can find the token limits for each model in the [Google AI documentation](https://ai.google.dev/gemini-api/docs/models).
|
||||
|
||||
## Model Fallback
|
||||
## Model fallback
|
||||
|
||||
The Gemini CLI includes a model fallback mechanism to ensure that you can continue to use the CLI even if the default "pro" model is rate-limited.
|
||||
Gemini CLI includes a model fallback mechanism to ensure that you can continue to use the CLI even if the default "pro" model is rate-limited.
|
||||
|
||||
If you are using the default "pro" model and the CLI detects that you are being rate-limited, it will automatically switch to the "flash" model for the current session. This allows you to continue working without interruption.
|
||||
If you are using the default "pro" model and the CLI detects that you are being rate-limited, it automatically switches to the "flash" model for the current session. This allows you to continue working without interruption.
|
||||
|
||||
## File Discovery Service
|
||||
## File discovery service
|
||||
|
||||
The file discovery service is responsible for finding files in the project that are relevant to the current context. It is used by the `@` command and other tools that need to access files.
|
||||
|
||||
## Memory Discovery Service
|
||||
## Memory discovery service
|
||||
|
||||
The memory discovery service is responsible for finding and loading the `GEMINI.md` files that provide context to the model. It searches for these files in a hierarchical manner, starting from the current working directory and moving up to the project root and the user's home directory. It also searches in subdirectories.
|
||||
|
||||
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 show` command to see the combined content of all loaded `GEMINI.md` files, and the `/memory refresh` command to reload them.
|
||||
You can use the [`/memory` command](../cli/commands.md) to `show`, `add`, and `refresh` the content of loaded `GEMINI.md` files.
|
||||
|
||||
@@ -1,287 +0,0 @@
|
||||
# Gemini CLI Observability Guide
|
||||
|
||||
Telemetry provides crucial data about the Gemini CLI's performance, health, and usage. By enabling it, you can monitor operations, debug issues, and optimize tool usage through traces, metrics, and structured logs.
|
||||
|
||||
This entire system is built on the **[OpenTelemetry] (OTEL)** standard, allowing you to send data to any compatible backend, from your local terminal to a cloud service.
|
||||
|
||||
[OpenTelemetry]: https://opentelemetry.io/
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Telemetry with Google Cloud
|
||||
|
||||
1. **Ensure Prerequisites:**
|
||||
Ensure that:
|
||||
- You have exported the `OTLP_GOOGLE_CLOUD_PROJECT` environment variable.
|
||||
- You have authenticated with Google Cloud and have the necessary IAM roles.
|
||||
For full details, see the [Google Cloud](#google-cloud) prerequisites.
|
||||
1. **Run the Command:** Execute the following command from the project root:
|
||||
```bash
|
||||
npm run telemetry -- --target=gcp
|
||||
```
|
||||
1. **Run Gemini CLI:** In a separate terminal, run your Gemini CLI commands. This will generate telemetry data that the collector will capture.
|
||||
1. **View Data:** The script will provide links to view your telemetry data (traces, metrics, logs) in the Google Cloud Console.
|
||||
1. **Details:** Refer to documentation for telemetry in [Google Cloud](#google-cloud).
|
||||
|
||||
### Local Telemetry with Jaeger UI (for Traces)
|
||||
|
||||
1. **Run the Command:** Execute the following command from the project root:
|
||||
```bash
|
||||
npm run telemetry -- --target=local
|
||||
```
|
||||
2. **Run Gemini CLI:** In a separate terminal, run your Gemini CLI commands. This will generate telemetry data that the collector will capture.
|
||||
3. **View Logs/Metrics:** Check the `.gemini/otel/collector.log` file for raw logs and metrics.
|
||||
4. **View Traces:** Open your browser and go to `http://localhost:16686` to see traces in the Jaeger UI.
|
||||
5. **Details:** Refer to documentation for telemetry in [Local](#local).
|
||||
|
||||
## Enabling Telemetry
|
||||
|
||||
You can enable telemetry in multiple ways. [Configuration](configuration.md) is primarily managed via the `.gemini/settings.json` file and environment variables, but CLI flags can override these settings for a specific session.
|
||||
|
||||
> **A Note on Sandbox Mode:** Telemetry is not compatible with sandbox mode at this time. Turn off sandbox mode before enabling telemetry. Tracked in #894.
|
||||
|
||||
**Order of Precedence:**
|
||||
|
||||
Telemetry settings are resolved in the following order (highest precedence first):
|
||||
|
||||
1. **CLI Flags (for `gemini` command):**
|
||||
- `--telemetry` / `--no-telemetry`: Overrides `telemetry.enabled`. If this flag is not provided, telemetry is disabled unless enabled in settings files.
|
||||
- `--telemetry-target <local|gcp>`: Overrides `telemetry.target`.
|
||||
- `--telemetry-otlp-endpoint <URL>`: Overrides `telemetry.otlpEndpoint`.
|
||||
- `--telemetry-log-prompts` / `--no-telemetry-log-prompts`: Overrides `telemetry.logPrompts`.
|
||||
2. **Environment Variables:**
|
||||
- `OTEL_EXPORTER_OTLP_ENDPOINT`: Overrides `telemetry.otlpEndpoint` if no `--telemetry-otlp-endpoint` flag is present.
|
||||
3. **Workspace Settings File (`.gemini/settings.json`):** Values from the `telemetry` object in this project-specific file.
|
||||
4. **User Settings File (`~/.gemini/settings.json`):** Values from the `telemetry` object in this global user file.
|
||||
5. **Defaults:** applied if not set by any of the above.
|
||||
- `telemetry.enabled`: `false`
|
||||
- `telemetry.target`: `local`
|
||||
- `telemetry.otlpEndpoint`: `http://localhost:4317`
|
||||
- `telemetry.logPrompts`: `true`
|
||||
|
||||
**For the `npm run telemetry -- --target=<gcp|local>` script:**
|
||||
The `--target` argument to this script _only_ overrides the `telemetry.target` for the duration and purpose of that script (i.e., choosing which collector to start). It does not permanently change your `settings.json`. The script will first look at `settings.json` for a `telemetry.target` to use as its default.
|
||||
|
||||
**Example settings:**
|
||||
Add these lines to configure telemetry in your workspace (`.gemini/settings.json`) or user (`~/.gemini/settings.json`) settings for GCP:
|
||||
|
||||
```json
|
||||
{
|
||||
"telemetry": {
|
||||
"enabled": true,
|
||||
"target": "gcp"
|
||||
},
|
||||
"sandbox": false
|
||||
}
|
||||
```
|
||||
|
||||
## Running an OTEL Collector
|
||||
|
||||
An OTEL Collector is a service that receives, processes, and exports telemetry data.
|
||||
The CLI sends data using the OTLP/gRPC protocol.
|
||||
|
||||
Learn more about OTEL exporter standard configuration in [documentation][otel-config-docs].
|
||||
|
||||
[otel-config-docs]: https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/
|
||||
|
||||
### Configuration
|
||||
|
||||
1. Install [otelcol-contrib] or use [docker]
|
||||
|
||||
[otelcol-contrib]: https://github.com/open-telemetry/opentelemetry-collector-contrib
|
||||
[docker]: https://www.docker.com/
|
||||
|
||||
2. Create a folder for the OTEL configurations:
|
||||
|
||||
```
|
||||
mkdir .gemini/otel
|
||||
```
|
||||
|
||||
### Local
|
||||
|
||||
Use the `npm run telemetry -- --target=local` command which automates the entire process of setting up a local telemetry pipeline, including configuring the necessary settings in your `.gemini/settings.json` file. The underlying script installs `otelcol-contrib` (The OpenTelemetry Collector) and `jaeger` (The Jaeger UI for viewing traces). To use it:
|
||||
|
||||
1. **Run the Command**:
|
||||
Execute the command from the root of the repository:
|
||||
|
||||
```bash
|
||||
npm run telemetry -- --target=local
|
||||
```
|
||||
|
||||
The script will:
|
||||
|
||||
- Download Jaeger and OTEL if needed.
|
||||
- Start a local Jaeger instance.
|
||||
- Start an OTEL collector configured to receive data from the Gemini CLI.
|
||||
- Automatically enable telemetry in your workspace settings.
|
||||
- On exit, disable telemetry.
|
||||
|
||||
2. **View Traces**:
|
||||
Open your web browser and navigate to **http://localhost:16686** to access the Jaeger UI. Here you can inspect detailed traces of Gemini CLI operations.
|
||||
|
||||
3. **Inspect Logs and Metrics**:
|
||||
The script redirects the OTEL collector's output (which includes logs and metrics) to `.gemini/otel/collector.log`. You can monitor this file to see the raw telemetry data:
|
||||
```bash
|
||||
tail -f .gemini/otel/collector.log
|
||||
```
|
||||
4. **Stop the Services**:
|
||||
Press `Ctrl+C` in the terminal where the script is running to stop the OTEL Collector and Jaeger services.
|
||||
|
||||
### Google Cloud
|
||||
|
||||
Use the `npm run telemetry -- --target=gcp` command which automates setting up a local OpenTelemetry collector that forwards data to your Google Cloud project, including configuring the necessary settings in your `.gemini/settings.json` file. The underlying script installs `otelcol-contrib`. To use it:
|
||||
|
||||
1. **Prerequisites**:
|
||||
|
||||
- Ensure you have a Google Cloud Project ID.
|
||||
- Export the `GOOGLE_CLOUD_PROJECT` environment variable to make it available to the OTEL collector.
|
||||
```bash
|
||||
export OTLP_GOOGLE_CLOUD_PROJECT="your-project-id"
|
||||
```
|
||||
- Authenticate with Google Cloud (e.g., run `gcloud auth application-default login` or ensure `GOOGLE_APPLICATION_CREDENTIALS` is set).
|
||||
- Ensure your account/service account has the necessary roles: "Cloud Trace Agent", "Monitoring Metric Writer", and "Logs Writer".
|
||||
|
||||
2. **Run the Command**:
|
||||
Execute the command from the root of the repository:
|
||||
|
||||
```bash
|
||||
npm run telemetry -- --target=gcp
|
||||
```
|
||||
|
||||
The script will:
|
||||
|
||||
- Download the `otelcol-contrib` binary if needed.
|
||||
- Start an OTEL collector configured to receive data from the Gemini CLI and export it to your specified Google Cloud project.
|
||||
- Automatically enable telemetry and disable sandbox mode in your workspace settings (`.gemini/settings.json`).
|
||||
- Provide direct links to view traces, metrics, and logs in your Google Cloud Console.
|
||||
- On exit (Ctrl+C), it will attempt to restore your original telemetry and sandbox settings.
|
||||
|
||||
3. **Run Gemini CLI:**
|
||||
In a separate terminal, run your Gemini CLI commands. This will generate telemetry data that the collector will capture.
|
||||
|
||||
4. **View Telemetry in Google Cloud**:
|
||||
Use the links provided by the script to navigate to the Google Cloud Console and view your traces, metrics, and logs.
|
||||
|
||||
5. **Inspect Local Collector Logs**:
|
||||
The script redirects the local OTEL collector\'s output to `.gemini/otel/collector-gcp.log`. You can monitor this file for detailed information or troubleshooting:
|
||||
|
||||
```bash
|
||||
tail -f .gemini/otel/collector-gcp.log
|
||||
```
|
||||
|
||||
6. **Stop the Service**:
|
||||
Press `Ctrl+C` in the terminal where the script is running to stop the OTEL Collector.
|
||||
|
||||
## Data Reference: Logs & Metrics
|
||||
|
||||
A `sessionId` is included as a common attribute on all logs and metrics.
|
||||
|
||||
### Logs
|
||||
|
||||
These are timestamped records of specific events.
|
||||
|
||||
- `gemini_cli.config`: Fired once at startup with the CLI's configuration.
|
||||
|
||||
- **Attributes**:
|
||||
- `model` (string)
|
||||
- `embedding_model` (string)
|
||||
- `sandbox_enabled` (boolean)
|
||||
- `core_tools_enabled` (string)
|
||||
- `approval_mode` (string)
|
||||
- `api_key_enabled` (boolean)
|
||||
- `vertex_ai_enabled` (boolean)
|
||||
- `code_assist_enabled` (boolean)
|
||||
- `log_prompts_enabled` (boolean)
|
||||
- `file_filtering_respect_git_ignore` (boolean)
|
||||
- `debug_mode` (boolean)
|
||||
- `mcp_servers` (string)
|
||||
|
||||
- `gemini_cli.user_prompt`: Fired when a user submits a prompt.
|
||||
|
||||
- **Attributes**:
|
||||
- `prompt_length`
|
||||
- `prompt` (except if `log_prompts_enabled` is false)
|
||||
|
||||
- `gemini_cli.tool_call`: Fired for every function call.
|
||||
|
||||
- **Attributes**:
|
||||
- `function_name`
|
||||
- `function_args`
|
||||
- `duration_ms`
|
||||
- `success` (boolean)
|
||||
- `decision` (string: "accept", "reject", or "modify", optional)
|
||||
- `error` (optional)
|
||||
- `error_type` (optional)
|
||||
|
||||
- `gemini_cli.api_request`: Fired when making a request to the Gemini API.
|
||||
|
||||
- **Attributes**:
|
||||
- `model`
|
||||
- `request_text` (optional)
|
||||
|
||||
- `gemini_cli.api_error`: Fired if the API request fails.
|
||||
|
||||
- **Attributes**:
|
||||
- `model`
|
||||
- `error`
|
||||
- `error_type`
|
||||
- `status_code`
|
||||
- `duration_ms`
|
||||
|
||||
- `gemini_cli.api_response`: Fired upon receiving a response from the Gemini API.
|
||||
- **Attributes**:
|
||||
- `model`
|
||||
- `status_code`
|
||||
- `duration_ms`
|
||||
- `error` (optional)
|
||||
- `input_token_count`
|
||||
- `output_token_count`
|
||||
- `cached_content_token_count`
|
||||
- `thoughts_token_count`
|
||||
- `tool_token_count`
|
||||
- `response_text` (optional)
|
||||
|
||||
### Metrics
|
||||
|
||||
These are numerical measurements of behavior over time.
|
||||
|
||||
- `gemini_cli.session.count` (Counter, Int): Incremented once per CLI startup.
|
||||
|
||||
- `gemini_cli.tool.call.count` (Counter, Int): Counts tool calls.
|
||||
|
||||
- **Attributes**:
|
||||
- `function_name`
|
||||
- `success` (boolean)
|
||||
- `decision` (string: "accept", "reject", or "modify", optional)
|
||||
|
||||
- `gemini_cli.tool.call.latency` (Histogram, ms): Measures tool call latency.
|
||||
|
||||
- **Attributes**:
|
||||
- `function_name`
|
||||
- `decision` (string: "accept", "reject", or "modify", optional)
|
||||
|
||||
- `gemini_cli.api.request.count` (Counter, Int): Counts all API requests.
|
||||
|
||||
- **Attributes**:
|
||||
- `model`
|
||||
- `status_code`
|
||||
- `error_type` (optional)
|
||||
|
||||
- `gemini_cli.api.request.latency` (Histogram, ms): Measures API request latency.
|
||||
|
||||
- **Attributes**:
|
||||
- `model`
|
||||
|
||||
- `gemini_cli.token.usage` (Counter, Int): Counts the number of tokens used.
|
||||
|
||||
- **Attributes**:
|
||||
- `model`
|
||||
- `type` (string: "input", "output", "thought", "cache", or "tool")
|
||||
|
||||
- `gemini_cli.file.operation.count` (Counter, Int): Counts file operations.
|
||||
|
||||
- **Attributes**:
|
||||
- `operation` (string: "create", "read", "update"): The type of file operation.
|
||||
- `lines` (optional, Int): Number of lines in the file.
|
||||
- `mimetype` (optional, string): Mimetype of the file.
|
||||
- `extension` (optional, string): File extension of the file.
|
||||
@@ -44,6 +44,9 @@ The core comes with a suite of pre-defined tools, typically found in `packages/c
|
||||
- `ShellTool` (`shell.ts`): Executes arbitrary shell commands (requires careful sandboxing and user confirmation).
|
||||
- **Web Tools:**
|
||||
- `WebFetchTool` (`web-fetch.ts`): Fetches content from a URL.
|
||||
- `WebSearchTool` (`web-search.ts`): Performs a web search.
|
||||
- **Memory Tools:**
|
||||
- `MemoryTool` (`memoryTool.ts`): Interacts with the AI's memory.
|
||||
|
||||
Each of these tools extends `BaseTool` and implements the required methods for its specific functionality.
|
||||
|
||||
|
||||
+8
-8
@@ -16,7 +16,7 @@ This is the recommended way for end-users to install Gemini CLI. It involves dow
|
||||
|
||||
```bash
|
||||
# Install the CLI globally
|
||||
npm install -g @gemini-cli/cli
|
||||
npm install -g @google/gemini-cli
|
||||
|
||||
# Now you can run the CLI from anywhere
|
||||
gemini
|
||||
@@ -25,7 +25,7 @@ This is the recommended way for end-users to install Gemini CLI. It involves dow
|
||||
- **NPX execution:**
|
||||
```bash
|
||||
# Execute the latest version from NPM without a global install
|
||||
npx @gemini-cli/cli
|
||||
npx @google/gemini-cli
|
||||
```
|
||||
|
||||
---
|
||||
@@ -38,7 +38,7 @@ For security and isolation, Gemini CLI can be run inside a container. This is th
|
||||
You can run the published sandbox image directly. This is useful for environments where you only have Docker and want to run the CLI.
|
||||
```bash
|
||||
# Run the published sandbox image
|
||||
docker run --rm -it us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.1.0
|
||||
docker run --rm -it us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.1.1
|
||||
```
|
||||
- **Using the `--sandbox` flag:**
|
||||
If you have Gemini CLI installed locally (using the standard installation described above), you can instruct it to run inside the sandbox container.
|
||||
@@ -73,11 +73,11 @@ Contributors to the project will want to run the CLI directly from the source co
|
||||
|
||||
### 4. Running the latest Gemini CLI commit from GitHub
|
||||
|
||||
You can run the most recently committed version of Gemini CLI directly from the GitHub repository. This is useful for testing features still in developement.
|
||||
You can run the most recently committed version of Gemini CLI directly from the GitHub repository. This is useful for testing features still in development.
|
||||
|
||||
```bash
|
||||
# Execute the CLI directly from the main branch on GitHub
|
||||
npx https://github.com/google/gemini-cli
|
||||
npx https://github.com/google-gemini/gemini-cli
|
||||
```
|
||||
|
||||
## Deployment architecture
|
||||
@@ -88,8 +88,8 @@ The execution methods described above are made possible by the following archite
|
||||
|
||||
Gemini CLI project is a monorepo that publishes two core packages to the NPM registry:
|
||||
|
||||
- `@gemini-cli/core`: The backend, handling logic and tool execution.
|
||||
- `@gemini-cli/cli`: The user-facing frontend.
|
||||
- `@google/gemini-cli-core`: The backend, handling logic and tool execution.
|
||||
- `@google/gemini-cli`: The user-facing frontend.
|
||||
|
||||
These packages are used when performing the standard installation and when running Gemini CLI from the source.
|
||||
|
||||
@@ -97,7 +97,7 @@ These packages are used when performing the standard installation and when runni
|
||||
|
||||
There are two distinct build processes used, depending on the distribution channel:
|
||||
|
||||
- **NPM publication:** For publishing to the NPM registry, the TypeScript source code in `@gemini-cli/core` and `@gemini-cli/cli` is transpiled into standard JavaScript using the TypeScript Compiler (`tsc`). The resulting `dist/` directory is what gets published in the NPM package. This is a standard approach for TypeScript libraries.
|
||||
- **NPM publication:** For publishing to the NPM registry, the TypeScript source code in `@google/gemini-cli-core` and `@google/gemini-cli` is transpiled into standard JavaScript using the TypeScript Compiler (`tsc`). The resulting `dist/` directory is what gets published in the NPM package. This is a standard approach for TypeScript libraries.
|
||||
|
||||
- **GitHub `npx` execution:** When running the latest version of Gemini CLI directly from GitHub, a different process is triggered by the `prepare` script in `package.json`. This script uses `esbuild` to bundle the entire application and its dependencies into a single, self-contained JavaScript file. This bundle is created on-the-fly on the user's machine and is not checked into the repository.
|
||||
|
||||
|
||||
+4
-1
@@ -16,10 +16,11 @@ This documentation is organized into the following sections:
|
||||
- **[CLI Introduction](./cli/index.md):** Overview of the command-line interface.
|
||||
- **[Commands](./cli/commands.md):** Description of available CLI commands.
|
||||
- **[Configuration](./cli/configuration.md):** Information on configuring the CLI.
|
||||
- **[Checkpointing](./checkpointing.md):** Documentation for the checkpointing feature.
|
||||
- **[Extensions](./extension.md):** How to extend the CLI with new functionality.
|
||||
- **[Telemetry](./telemetry.md):** Overview of telemetry in the CLI.
|
||||
- **Core Details:** Documentation for `packages/core`.
|
||||
- **[Core Introduction](./core/index.md):** Overview of the core component.
|
||||
- **[Configuration](./core/configuration.md):** Information on configuring the core component.
|
||||
- **[Tools API](./core/tools-api.md):** Information on how the core manages and exposes tools.
|
||||
- **Tools:**
|
||||
- **[Tools Overview](./tools/index.md):** Overview of the available tools.
|
||||
@@ -28,7 +29,9 @@ This documentation is organized into the following sections:
|
||||
- **[Shell Tool](./tools/shell.md):** Documentation for the `run_shell_command` tool.
|
||||
- **[Web Fetch Tool](./tools/web-fetch.md):** Documentation for the `web_fetch` tool.
|
||||
- **[Web Search Tool](./tools/web-search.md):** Documentation for the `google_web_search` tool.
|
||||
- **[Memory Tool](./tools/memory.md):** Documentation for the `save_memory` tool.
|
||||
- **[Contributing & Development Guide](../CONTRIBUTING.md):** Information for contributors and developers, including setup, building, testing, and coding conventions.
|
||||
- **[Troubleshooting Guide](./troubleshooting.md):** Find solutions to common problems and FAQs.
|
||||
- **[Terms of Service and Privacy Notice](./tos-privacy.md):** Information on the terms of service and privacy notices applicable to your use of Gemini CLI.
|
||||
|
||||
We hope this documentation helps you make the most of the Gemini CLI!
|
||||
|
||||
@@ -18,7 +18,7 @@ The integration tests can also be run using the following shortcut:
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
## Running a specfic set of tests
|
||||
## Running a specific set of tests
|
||||
|
||||
To run a subset of test files, you can use `npm run <integration test command> <file_name1> ....` where <integration test command> is either `test:e2e` or `test:integration*` and `<file_name>` is any of the `.test.js` files in the `integration-tests/` directory. For example, the following command runs `list_directory.test.js` and `write_file.test.js`:
|
||||
|
||||
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
# Sandboxing in the Gemini CLI
|
||||
|
||||
This document provides a guide to sandboxing in the Gemini CLI, including prerequisites, quickstart, and configuration.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before using sandboxing, you need to install and set up the Gemini CLI:
|
||||
|
||||
```bash
|
||||
# install gemini-cli with npm
|
||||
npm install -g @google/gemini-cli
|
||||
|
||||
# Verify installation
|
||||
gemini --version
|
||||
```
|
||||
|
||||
## Overview of sandboxing
|
||||
|
||||
Sandboxing isolates potentially dangerous operations (such as shell commands or file modifications) from your host system, providing a security barrier between AI operations and your environment.
|
||||
|
||||
The benefits of sandboxing include:
|
||||
|
||||
- **Security**: Prevent accidental system damage or data loss.
|
||||
- **Isolation**: Limit file system access to project directory.
|
||||
- **Consistency**: Ensure reproducible environments across different systems.
|
||||
- **Safety**: Reduce risk when working with untrusted code or experimental commands.
|
||||
|
||||
## Sandboxing methods
|
||||
|
||||
Your ideal method of sandboxing may differ depending on your platform and your preferred container solution.
|
||||
|
||||
### 1. macOS Seatbelt (macOS only)
|
||||
|
||||
Lightweight, built-in sandboxing using `sandbox-exec`.
|
||||
|
||||
**Default profile**: `permissive-open` - restricts writes outside project directory but allows most other operations.
|
||||
|
||||
### 2. Container-based (Docker/Podman)
|
||||
|
||||
Cross-platform sandboxing with complete process isolation.
|
||||
|
||||
**Note**: Requires building the sandbox image locally or using a published image from your organization's registry.
|
||||
|
||||
## Quickstart
|
||||
|
||||
```bash
|
||||
# Enable sandboxing with command flag
|
||||
gemini -s -p "analyze the code structure"
|
||||
|
||||
# Use environment variable
|
||||
export GEMINI_SANDBOX=true
|
||||
gemini -p "run the test suite"
|
||||
|
||||
# Configure in settings.json
|
||||
{
|
||||
"sandbox": "docker"
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Enable sandboxing (in order of precedence)
|
||||
|
||||
1. **Command flag**: `-s` or `--sandbox`
|
||||
2. **Environment variable**: `GEMINI_SANDBOX=true|docker|podman|sandbox-exec`
|
||||
3. **Settings file**: `"sandbox": true` in `settings.json`
|
||||
|
||||
### macOS Seatbelt profiles
|
||||
|
||||
Built-in profiles (set via `SEATBELT_PROFILE` env var):
|
||||
|
||||
- `permissive-open` (default): Write restrictions, network allowed
|
||||
- `permissive-closed`: Write restrictions, no network
|
||||
- `permissive-proxied`: Write restrictions, network via proxy
|
||||
- `restrictive-open`: Strict restrictions, network allowed
|
||||
- `restrictive-closed`: Maximum restrictions
|
||||
|
||||
## Linux UID/GID handling
|
||||
|
||||
The sandbox automatically handles user permissions on Linux. Override these permissions with:
|
||||
|
||||
```bash
|
||||
export SANDBOX_SET_UID_GID=true # Force host UID/GID
|
||||
export SANDBOX_SET_UID_GID=false # Disable UID/GID mapping
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common issues
|
||||
|
||||
**"Operation not permitted"**
|
||||
|
||||
- Operation requires access outside sandbox.
|
||||
- Try more permissive profile or add mount points.
|
||||
|
||||
**Missing commands**
|
||||
|
||||
- Add to custom Dockerfile.
|
||||
- Install via `sandbox.bashrc`.
|
||||
|
||||
**Network issues**
|
||||
|
||||
- Check sandbox profile allows network.
|
||||
- Verify proxy configuration.
|
||||
|
||||
### Debug mode
|
||||
|
||||
```bash
|
||||
DEBUG=1 gemini -s -p "debug command"
|
||||
```
|
||||
|
||||
### Inspect sandbox
|
||||
|
||||
```bash
|
||||
# Check environment
|
||||
gemini -s -p "run shell command: env | grep SANDBOX"
|
||||
|
||||
# List mounts
|
||||
gemini -s -p "run shell command: mount | grep workspace"
|
||||
```
|
||||
|
||||
## Security notes
|
||||
|
||||
- Sandboxing reduces but doesn't eliminate all risks.
|
||||
- Use the most restrictive profile that allows your work.
|
||||
- Container overhead is minimal after first build.
|
||||
- GUI applications may not work in sandboxes.
|
||||
|
||||
## Related documentation
|
||||
|
||||
- [Configuration](./cli/configuration.md): Full configuration options.
|
||||
- [Commands](./cli/commands.md): Available commands.
|
||||
- [Troubleshooting](./troubleshooting.md): General troubleshooting.
|
||||
@@ -0,0 +1,248 @@
|
||||
# Gemini CLI Observability Guide
|
||||
|
||||
Telemetry provides data about Gemini CLI's performance, health, and usage. By enabling it, you can monitor operations, debug issues, and optimize tool usage through traces, metrics, and structured logs.
|
||||
|
||||
Gemini CLI's telemetry system is built on the **[OpenTelemetry] (OTEL)** standard, allowing you to send data to any compatible backend.
|
||||
|
||||
[OpenTelemetry]: https://opentelemetry.io/
|
||||
|
||||
## Enabling telemetry
|
||||
|
||||
You can enable telemetry in multiple ways. Configuration is primarily managed via the [`.gemini/settings.json` file](./cli/configuration.md) and environment variables, but CLI flags can override these settings for a specific session.
|
||||
|
||||
### Order of precedence
|
||||
|
||||
The following lists the precedence for applying telemetry settings, with items listed higher having greater precedence:
|
||||
|
||||
1. **CLI flags (for `gemini` command):**
|
||||
|
||||
- `--telemetry` / `--no-telemetry`: Overrides `telemetry.enabled`.
|
||||
- `--telemetry-target <local|gcp>`: Overrides `telemetry.target`.
|
||||
- `--telemetry-otlp-endpoint <URL>`: Overrides `telemetry.otlpEndpoint`.
|
||||
- `--telemetry-log-prompts` / `--no-telemetry-log-prompts`: Overrides `telemetry.logPrompts`.
|
||||
|
||||
1. **Environment variables:**
|
||||
|
||||
- `OTEL_EXPORTER_OTLP_ENDPOINT`: Overrides `telemetry.otlpEndpoint`.
|
||||
|
||||
1. **Workspace settings file (`.gemini/settings.json`):** Values from the `telemetry` object in this project-specific file.
|
||||
|
||||
1. **User settings file (`~/.gemini/settings.json`):** Values from the `telemetry` object in this global user file.
|
||||
|
||||
1. **Defaults:** applied if not set by any of the above.
|
||||
- `telemetry.enabled`: `false`
|
||||
- `telemetry.target`: `local`
|
||||
- `telemetry.otlpEndpoint`: `http://localhost:4317`
|
||||
- `telemetry.logPrompts`: `true`
|
||||
|
||||
**For the `npm run telemetry -- --target=<gcp|local>` script:**
|
||||
The `--target` argument to this script _only_ overrides the `telemetry.target` for the duration and purpose of that script (i.e., choosing which collector to start). It does not permanently change your `settings.json`. The script will first look at `settings.json` for a `telemetry.target` to use as its default.
|
||||
|
||||
### Example settings
|
||||
|
||||
The following code can be added to your workspace (`.gemini/settings.json`) or user (`~/.gemini/settings.json`) settings to enable telemetry and send the output to Google Cloud:
|
||||
|
||||
```json
|
||||
{
|
||||
"telemetry": {
|
||||
"enabled": true,
|
||||
"target": "gcp"
|
||||
},
|
||||
"sandbox": false
|
||||
}
|
||||
```
|
||||
|
||||
## Running an OTEL Collector
|
||||
|
||||
An OTEL Collector is a service that receives, processes, and exports telemetry data.
|
||||
The CLI sends data using the OTLP/gRPC protocol.
|
||||
|
||||
Learn more about OTEL exporter standard configuration in [documentation][otel-config-docs].
|
||||
|
||||
[otel-config-docs]: https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/
|
||||
|
||||
### Local
|
||||
|
||||
Use the `npm run telemetry -- --target=local` command to automate the process of setting up a local telemetry pipeline, including configuring the necessary settings in your `.gemini/settings.json` file. The underlying script installs `otelcol-contrib` (the OpenTelemetry Collector) and `jaeger` (The Jaeger UI for viewing traces). To use it:
|
||||
|
||||
1. **Run the command**:
|
||||
Execute the command from the root of the repository:
|
||||
|
||||
```bash
|
||||
npm run telemetry -- --target=local
|
||||
```
|
||||
|
||||
The script will:
|
||||
|
||||
- Download Jaeger and OTEL if needed.
|
||||
- Start a local Jaeger instance.
|
||||
- Start an OTEL collector configured to receive data from Gemini CLI.
|
||||
- Automatically enable telemetry in your workspace settings.
|
||||
- On exit, disable telemetry.
|
||||
|
||||
1. **View traces**:
|
||||
Open your web browser and navigate to **http://localhost:16686** to access the Jaeger UI. Here you can inspect detailed traces of Gemini CLI operations.
|
||||
|
||||
1. **Inspect logs and metrics**:
|
||||
The script redirects the OTEL collector output (which includes logs and metrics) to `~/.gemini/tmp/<projectHash>/otel/collector.log`. The script will provide links to view and command to tail your telemetry data (traces, metrics, logs) locally.
|
||||
|
||||
1. **Stop the services**:
|
||||
Press `Ctrl+C` in the terminal where the script is running to stop the OTEL Collector and Jaeger services.
|
||||
|
||||
### Google Cloud
|
||||
|
||||
Use the `npm run telemetry -- --target=gcp` command to automate setting up a local OpenTelemetry collector that forwards data to your Google Cloud project, including configuring the necessary settings in your `.gemini/settings.json` file. The underlying script installs `otelcol-contrib`. To use it:
|
||||
|
||||
1. **Prerequisites**:
|
||||
|
||||
- Have a Google Cloud project ID.
|
||||
- Export the `GOOGLE_CLOUD_PROJECT` environment variable to make it available to the OTEL collector.
|
||||
```bash
|
||||
export OTLP_GOOGLE_CLOUD_PROJECT="your-project-id"
|
||||
```
|
||||
- Authenticate with Google Cloud (e.g., run `gcloud auth application-default login` or ensure `GOOGLE_APPLICATION_CREDENTIALS` is set).
|
||||
- Ensure your Google Cloud account/service account has the necessary IAM roles: "Cloud Trace Agent", "Monitoring Metric Writer", and "Logs Writer".
|
||||
|
||||
1. **Run the command**:
|
||||
Execute the command from the root of the repository:
|
||||
|
||||
```bash
|
||||
npm run telemetry -- --target=gcp
|
||||
```
|
||||
|
||||
The script will:
|
||||
|
||||
- Download the `otelcol-contrib` binary if needed.
|
||||
- Start an OTEL collector configured to receive data from Gemini CLI and export it to your specified Google Cloud project.
|
||||
- Automatically enable telemetry and disable sandbox mode in your workspace settings (`.gemini/settings.json`).
|
||||
- Provide direct links to view traces, metrics, and logs in your Google Cloud Console.
|
||||
- On exit (Ctrl+C), it will attempt to restore your original telemetry and sandbox settings.
|
||||
|
||||
1. **Run Gemini CLI:**
|
||||
In a separate terminal, run your Gemini CLI commands. This generates telemetry data that the collector captures.
|
||||
|
||||
1. **View telemetry in Google Cloud**:
|
||||
Use the links provided by the script to navigate to the Google Cloud Console and view your traces, metrics, and logs.
|
||||
|
||||
1. **Inspect local collector logs**:
|
||||
The script redirects the local OTEL collector output to `~/.gemini/tmp/<projectHash>/otel/collector-gcp.log`. The script provides links to view and command to tail your collector logs locally.
|
||||
|
||||
1. **Stop the service**:
|
||||
Press `Ctrl+C` in the terminal where the script is running to stop the OTEL Collector.
|
||||
|
||||
## Logs and metric reference
|
||||
|
||||
The following section describes the structure of logs and metrics generated for Gemini CLI.
|
||||
|
||||
- A `sessionId` is included as a common attribute on all logs and metrics.
|
||||
|
||||
### Logs
|
||||
|
||||
Logs are timestamped records of specific events. The following events are logged for Gemini CLI:
|
||||
|
||||
- `gemini_cli.config`: This event occurs once at startup with the CLI's configuration.
|
||||
|
||||
- **Attributes**:
|
||||
- `model` (string)
|
||||
- `embedding_model` (string)
|
||||
- `sandbox_enabled` (boolean)
|
||||
- `core_tools_enabled` (string)
|
||||
- `approval_mode` (string)
|
||||
- `api_key_enabled` (boolean)
|
||||
- `vertex_ai_enabled` (boolean)
|
||||
- `code_assist_enabled` (boolean)
|
||||
- `log_prompts_enabled` (boolean)
|
||||
- `file_filtering_respect_git_ignore` (boolean)
|
||||
- `debug_mode` (boolean)
|
||||
- `mcp_servers` (string)
|
||||
|
||||
- `gemini_cli.user_prompt`: This event occurs when a user submits a prompt.
|
||||
|
||||
- **Attributes**:
|
||||
- `prompt_length`
|
||||
- `prompt` (this attribute is excluded if `log_prompts_enabled` is configured to be `false`)
|
||||
|
||||
- `gemini_cli.tool_call`: This event occurs for each function call.
|
||||
|
||||
- **Attributes**:
|
||||
- `function_name`
|
||||
- `function_args`
|
||||
- `duration_ms`
|
||||
- `success` (boolean)
|
||||
- `decision` (string: "accept", "reject", or "modify", if applicable)
|
||||
- `error` (if applicable)
|
||||
- `error_type` (if applicable)
|
||||
|
||||
- `gemini_cli.api_request`: This event occurs when making a request to Gemini API.
|
||||
|
||||
- **Attributes**:
|
||||
- `model`
|
||||
- `request_text` (if applicable)
|
||||
|
||||
- `gemini_cli.api_error`: This event occurs if the API request fails.
|
||||
|
||||
- **Attributes**:
|
||||
- `model`
|
||||
- `error`
|
||||
- `error_type`
|
||||
- `status_code`
|
||||
- `duration_ms`
|
||||
|
||||
- `gemini_cli.api_response`: This event occurs upon receiving a response from Gemini API.
|
||||
|
||||
- **Attributes**:
|
||||
- `model`
|
||||
- `status_code`
|
||||
- `duration_ms`
|
||||
- `error` (optional)
|
||||
- `input_token_count`
|
||||
- `output_token_count`
|
||||
- `cached_content_token_count`
|
||||
- `thoughts_token_count`
|
||||
- `tool_token_count`
|
||||
- `response_text` (if applicable)
|
||||
|
||||
### Metrics
|
||||
|
||||
Metrics are numerical measurements of behavior over time. The following metrics are collected for Gemini CLI:
|
||||
|
||||
- `gemini_cli.session.count` (Counter, Int): Incremented once per CLI startup.
|
||||
|
||||
- `gemini_cli.tool.call.count` (Counter, Int): Counts tool calls.
|
||||
|
||||
- **Attributes**:
|
||||
- `function_name`
|
||||
- `success` (boolean)
|
||||
- `decision` (string: "accept", "reject", or "modify", if applicable)
|
||||
|
||||
- `gemini_cli.tool.call.latency` (Histogram, ms): Measures tool call latency.
|
||||
|
||||
- **Attributes**:
|
||||
- `function_name`
|
||||
- `decision` (string: "accept", "reject", or "modify", if applicable)
|
||||
|
||||
- `gemini_cli.api.request.count` (Counter, Int): Counts all API requests.
|
||||
|
||||
- **Attributes**:
|
||||
- `model`
|
||||
- `status_code`
|
||||
- `error_type` (if applicable)
|
||||
|
||||
- `gemini_cli.api.request.latency` (Histogram, ms): Measures API request latency.
|
||||
|
||||
- **Attributes**:
|
||||
- `model`
|
||||
|
||||
- `gemini_cli.token.usage` (Counter, Int): Counts the number of tokens used.
|
||||
|
||||
- **Attributes**:
|
||||
- `model`
|
||||
- `type` (string: "input", "output", "thought", "cache", or "tool")
|
||||
|
||||
- `gemini_cli.file.operation.count` (Counter, Int): Counts file operations.
|
||||
|
||||
- **Attributes**:
|
||||
- `operation` (string: "create", "read", "update"): The type of file operation.
|
||||
- `lines` (Int, if applicable): Number of lines in the file.
|
||||
- `mimetype` (string, if applicable): Mimetype of the file.
|
||||
- `extension` (string, if applicable): File extension of the file.
|
||||
+132
-34
@@ -1,19 +1,33 @@
|
||||
# Gemini CLI: File System Tools
|
||||
# Gemini CLI file system tools
|
||||
|
||||
The Gemini CLI provides a comprehensive suite of tools for interacting with the local file system. These tools allow the Gemini model to read from, write to, list, search, and modify files and directories, all under your control and typically with confirmation for sensitive operations.
|
||||
|
||||
All file system tools operate within a `rootDirectory` (usually the current working directory where you launched the CLI) for security, preventing unintended access to other parts of your system. Paths provided to these tools are generally expected to be absolute or are resolved relative to this root directory.
|
||||
**Note:** All file system tools operate within a `rootDirectory` (usually the current working directory where you launched the CLI) for security. Paths that you provide to these tools are generally expected to be absolute or are resolved relative to this root directory.
|
||||
|
||||
## 1. `list_directory` (ReadFolder)
|
||||
|
||||
- **Tool Name:** `list_directory`
|
||||
- **Display Name:** ReadFolder
|
||||
`list_directory` lists the names of files and subdirectories directly within a specified directory path. It can optionally ignore entries matching provided glob patterns.
|
||||
|
||||
- **Tool name:** `list_directory`
|
||||
- **Display name:** ReadFolder
|
||||
- **File:** `ls.ts`
|
||||
- **Description:** Lists the names of files and subdirectories directly within a specified directory path. It can optionally ignore entries matching provided glob patterns.
|
||||
- **Parameters:**
|
||||
- `path` (string, required): The absolute path to the directory to list.
|
||||
- `ignore` (array of strings, optional): A list of glob patterns to exclude from the listing (e.g., `["*.log", ".git"]`).
|
||||
- `respect_git_ignore` (boolean, optional): Whether to respect .gitignore patterns when listing files. Defaults to true.
|
||||
- `respect_git_ignore` (boolean, optional): Whether to respect `.gitignore` patterns when listing files. Defaults to `true`.
|
||||
- **Behavior:**
|
||||
- Returns a list of file and directory names.
|
||||
- Indicates whether each entry is a directory.
|
||||
- Sorts entries with directories first, then alphabetically.
|
||||
- **Output (`llmContent`):** A string like: `Directory listing for /path/to/your/folder:\n[DIR] subfolder1\nfile1.txt\nfile2.png`
|
||||
- **Confirmation:** No.
|
||||
- **Tool name:** `list_directory`
|
||||
- **Display name:** ReadFolder
|
||||
- **File:** `ls.ts`
|
||||
- **Parameters:**
|
||||
- `path` (string, required): The absolute path to the directory to list.
|
||||
- `ignore` (array of strings, optional): A list of glob patterns to exclude from the listing (e.g., `["*.log", ".git"]`).
|
||||
- `respect_git_ignore` (boolean, optional): Whether to respect `.gitignore` patterns when listing files. Defaults to `true`.
|
||||
- **Behavior:**
|
||||
- Returns a list of file and directory names.
|
||||
- Indicates whether each entry is a directory.
|
||||
@@ -23,17 +37,34 @@ All file system tools operate within a `rootDirectory` (usually the current work
|
||||
|
||||
## 2. `read_file` (ReadFile)
|
||||
|
||||
- **Tool Name:** `read_file`
|
||||
- **Display Name:** ReadFile
|
||||
`read_file` reads and returns the content of a specified file. This tool handles text, images (PNG, JPG, GIF, WEBP, SVG, BMP), and PDF files. For text files, it can read specific line ranges. Other binary file types are generally skipped.
|
||||
|
||||
- **Tool name:** `read_file`
|
||||
- **Display name:** ReadFile
|
||||
- **File:** `read-file.ts`
|
||||
- **Description:** Reads and returns the content of a specified file. Handles text, images (PNG, JPG, GIF, WEBP, SVG, BMP), and PDF files. For text files, it can read specific line ranges. Other binary file types are generally skipped.
|
||||
- **Parameters:**
|
||||
- `path` (string, required): The absolute path to the file to read.
|
||||
- `offset` (number, optional): For text files, the 0-based line number to start reading from. Requires `limit` to be set.
|
||||
- `limit` (number, optional): For text files, the maximum number of lines to read. If omitted, reads a default maximum (e.g., 2000 lines) or the entire file if feasible.
|
||||
- **Behavior:**
|
||||
- For text files: Returns the content. If `offset` and `limit` are used, returns only that slice of lines. Indicates if content was truncated due to line limits or line length limits.
|
||||
- For image and PDF files: Returns the file content as a base64 encoded data structure suitable for model consumption.
|
||||
- For image and PDF files: Returns the file content as a base64-encoded data structure suitable for model consumption.
|
||||
- For other binary files: Attempts to identify and skip them, returning a message indicating it's a generic binary file.
|
||||
- **Output:** (`llmContent`):
|
||||
- For text files: The file content, potentially prefixed with a truncation message (e.g., `[File content truncated: showing lines 1-100 of 500 total lines...]\nActual file content...`).
|
||||
- For image/PDF files: An object containing `inlineData` with `mimeType` and base64 `data` (e.g., `{ inlineData: { mimeType: 'image/png', data: 'base64encodedstring' } }`).
|
||||
- For other binary files: A message like `Cannot display content of binary file: /path/to/data.bin`.
|
||||
- **Confirmation:** No.
|
||||
- **Tool name:** `read_file`
|
||||
- **Display name:** ReadFile
|
||||
- **File:** `read-file.ts`
|
||||
- **Parameters:**
|
||||
- `path` (string, required): The absolute path to the file to read.
|
||||
- `offset` (number, optional): For text files, the 0-based line number to start reading from. Requires `limit` to be set.
|
||||
- `limit` (number, optional): For text files, the maximum number of lines to read. If omitted, reads a default maximum (e.g., 2000 lines) or the entire file if feasible.
|
||||
- **Behavior:**
|
||||
- For text files: Returns the content. If `offset` and `limit` are used, returns only that slice of lines. Indicates if content was truncated due to line limits or line length limits.
|
||||
- For image and PDF files: Returns the file content as a base64-encoded data structure suitable for model consumption.
|
||||
- For other binary files: Attempts to identify and skip them, returning a message indicating it's a generic binary file.
|
||||
- **Output:** (`llmContent`):
|
||||
- For text files: The file content, potentially prefixed with a truncation message (e.g., `[File content truncated: showing lines 1-100 of 500 total lines...]\nActual file content...`).
|
||||
@@ -43,10 +74,22 @@ All file system tools operate within a `rootDirectory` (usually the current work
|
||||
|
||||
## 3. `write_file` (WriteFile)
|
||||
|
||||
- **Tool Name:** `write_file`
|
||||
- **Display Name:** WriteFile
|
||||
`write_file` writes content to a specified file. If the file exists, it will be overwritten. If the file doesn't exist, it (and any necessary parent directories) will be created.
|
||||
|
||||
- **Tool name:** `write_file`
|
||||
- **Display name:** WriteFile
|
||||
- **File:** `write-file.ts`
|
||||
- **Parameters:**
|
||||
- `file_path` (string, required): The absolute path to the file to write to.
|
||||
- `content` (string, required): The content to write into the file.
|
||||
- **Behavior:**
|
||||
- Writes the provided `content` to the `file_path`.
|
||||
- Creates parent directories if they don't exist.
|
||||
- **Output (`llmContent`):** A success message, e.g., `Successfully overwrote file: /path/to/your/file.txt` or `Successfully created and wrote to new file: /path/to/new/file.txt`.
|
||||
- **Confirmation:** Yes. Shows a diff of changes and asks for user approval before writing.
|
||||
- **Tool name:** `write_file`
|
||||
- **Display name:** WriteFile
|
||||
- **File:** `write-file.ts`
|
||||
- **Description:** Writes content to a specified file. If the file exists, it will be overwritten. If it doesn't exist, it (and any necessary parent directories) will be created.
|
||||
- **Parameters:**
|
||||
- `file_path` (string, required): The absolute path to the file to write to.
|
||||
- `content` (string, required): The content to write into the file.
|
||||
@@ -58,15 +101,30 @@ All file system tools operate within a `rootDirectory` (usually the current work
|
||||
|
||||
## 4. `glob` (FindFiles)
|
||||
|
||||
- **Tool Name:** `glob`
|
||||
- **Display Name:** FindFiles
|
||||
`glob` finds files matching specific glob patterns (e.g., `src/**/*.ts`, `*.md`), returning absolute paths sorted by modification time (newest first).
|
||||
|
||||
- **Tool name:** `glob`
|
||||
- **Display name:** FindFiles
|
||||
- **File:** `glob.ts`
|
||||
- **Description:** Efficiently finds files matching specific glob patterns (e.g., `src/**/*.ts`, `*.md`), returning absolute paths sorted by modification time (newest first).
|
||||
- **Parameters:**
|
||||
- `pattern` (string, required): The glob pattern to match against (e.g., `"*.py"`, `"src/**/*.js"`).
|
||||
- `path` (string, optional): The absolute path to the directory to search within. If omitted, searches the tool's root directory.
|
||||
- `case_sensitive` (boolean, optional): Whether the search should be case-sensitive. Defaults to false.
|
||||
- `respect_git_ignore` (boolean, optional): Whether to respect .gitignore patterns when finding files. Defaults to true.
|
||||
- `case_sensitive` (boolean, optional): Whether the search should be case-sensitive. Defaults to `false`.
|
||||
- `respect_git_ignore` (boolean, optional): Whether to respect .gitignore patterns when finding files. Defaults to `true`.
|
||||
- **Behavior:**
|
||||
- Searches for files matching the glob pattern within the specified directory.
|
||||
- Returns a list of absolute paths, sorted with the most recently modified files first.
|
||||
- Ignores common nuisance directories like `node_modules` and `.git` by default.
|
||||
- **Output (`llmContent`):** A message like: `Found 5 file(s) matching "*.ts" within src, sorted by modification time (newest first):\nsrc/file1.ts\nsrc/subdir/file2.ts...`
|
||||
- **Confirmation:** No.
|
||||
- **Tool name:** `glob`
|
||||
- **Display name:** FindFiles
|
||||
- **File:** `glob.ts`
|
||||
- **Parameters:**
|
||||
- `pattern` (string, required): The glob pattern to match against (e.g., `"*.py"`, `"src/**/*.js"`).
|
||||
- `path` (string, optional): The absolute path to the directory to search within. If omitted, searches the tool's root directory.
|
||||
- `case_sensitive` (boolean, optional): Whether the search should be case-sensitive. Defaults to `false`.
|
||||
- `respect_git_ignore` (boolean, optional): Whether to respect .gitignore patterns when finding files. Defaults to `true`.
|
||||
- **Behavior:**
|
||||
- Searches for files matching the glob pattern within the specified directory.
|
||||
- Returns a list of absolute paths, sorted with the most recently modified files first.
|
||||
@@ -76,10 +134,22 @@ All file system tools operate within a `rootDirectory` (usually the current work
|
||||
|
||||
## 5. `search_file_content` (SearchText)
|
||||
|
||||
- **Tool Name:** `search_file_content`
|
||||
- **Display Name:** SearchText
|
||||
`search_file_content` searches for a regular expression pattern within the content of files in a specified directory. Can filter files by a glob pattern. Returns the lines containing matches, along with their file paths and line numbers.
|
||||
|
||||
- **Tool name:** `search_file_content`
|
||||
- **Display name:** SearchText
|
||||
- **File:** `grep.ts`
|
||||
- **Parameters:**
|
||||
- `pattern` (string, required): The regular expression (regex) to search for (e.g., `"function\s+myFunction"`).
|
||||
- `path` (string, optional): The absolute path to the directory to search within. Defaults to the current working directory.
|
||||
- `include` (string, optional): A glob pattern to filter which files are searched (e.g., `"*.js"`, `"src/**/*.{ts,tsx}"`). If omitted, searches most files (respecting common ignores).
|
||||
- **Behavior:**
|
||||
- Uses `git grep` if available in a Git repository for speed, otherwise falls back to system `grep` or a JavaScript-based search.
|
||||
- Returns a list of matching lines, each prefixed with its file path (relative to the search directory) and line number.
|
||||
- **Output (`llmContent`):** A formatted string of matches, e.g.:
|
||||
- **Tool name:** `search_file_content`
|
||||
- **Display name:** SearchText
|
||||
- **File:** `grep.ts`
|
||||
- **Description:** Searches for a regular expression pattern within the content of files in a specified directory. Can filter files by a glob pattern. Returns the lines containing matches, along with their file paths and line numbers.
|
||||
- **Parameters:**
|
||||
- `pattern` (string, required): The regular expression (regex) to search for (e.g., `"function\s+myFunction"`).
|
||||
- `path` (string, optional): The absolute path to the directory to search within. Defaults to the current working directory.
|
||||
@@ -100,34 +170,62 @@ All file system tools operate within a `rootDirectory` (usually the current work
|
||||
---
|
||||
```
|
||||
- **Confirmation:** No.
|
||||
- **Confirmation:** No.
|
||||
|
||||
## 6. `replace` (Edit)
|
||||
|
||||
- **Tool Name:** `replace`
|
||||
- **Display Name:** 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.
|
||||
|
||||
- **Tool name:** `replace`
|
||||
- **Display name:** Edit
|
||||
- **File:** `edit.ts`
|
||||
- **Description:** 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.
|
||||
- **Parameters:**
|
||||
|
||||
- `file_path` (string, required): The absolute path to the file to modify.
|
||||
- `old_string` (string, required): The exact literal text to replace. **CRITICAL:** This string must uniquely identify the single instance to change. It should include at least 3 lines of context _before_ and _after_ the target text, matching whitespace and indentation precisely. If `old_string` is empty, the tool attempts to create a new file at `file_path` with `new_string` as content.
|
||||
- `old_string` (string, required): The exact literal text to replace.
|
||||
|
||||
**CRITICAL:** This string must uniquely identify the single instance to change. It should include at least 3 lines of context _before_ and _after_ the target text, matching whitespace and indentation precisely. If `old_string` is empty, the tool attempts to create a new file at `file_path` with `new_string` as content.
|
||||
|
||||
- `new_string` (string, required): The exact literal text to replace `old_string` with.
|
||||
- `expected_replacements` (number, optional): The number of occurrences to replace. Defaults to 1.
|
||||
- `expected_replacements` (number, optional): The number of occurrences to replace. Defaults to `1`.
|
||||
|
||||
- **Behavior:**
|
||||
- If `old_string` is empty and `file_path` does not exist, creates a new file with `new_string` as content.
|
||||
- If `old_string` is provided, it reads the `file_path` and attempts to find exactly one occurrence of `old_string`.
|
||||
- If one occurrence is found, it replaces it with `new_string`.
|
||||
- **Enhanced Reliability (Multi-Stage Edit Correction):** To significantly improve the success rate of edits, especially when the model-provided `old_string` might not be perfectly precise, the tool incorporates a multi-stage edit correction mechanism.
|
||||
- If the initial `old_string` isn't found or matches multiple locations, the tool can leverage the Gemini model to iteratively refine `old_string` (and potentially `new_string`).
|
||||
- This self-correction process attempts to identify the unique segment the model intended to modify, making the `replace` operation more robust even with slightly imperfect initial context.
|
||||
- **Failure Conditions:** Despite the correction mechanism, the tool will fail if:
|
||||
- `file_path` is not absolute or is outside the root directory.
|
||||
- `old_string` is not empty, but the `file_path` does not exist.
|
||||
- `old_string` is empty, but the `file_path` already exists.
|
||||
- `old_string` is not found in the file after attempts to correct it.
|
||||
- `old_string` is found multiple times, and the self-correction mechanism cannot resolve it to a single, unambiguous match.
|
||||
- **Output (`llmContent`):**
|
||||
- On success: `Successfully modified file: /path/to/file.txt (1 replacements).` or `Created new file: /path/to/new_file.txt with provided content.`
|
||||
- On failure: An error message explaining the reason (e.g., `Failed to edit, 0 occurrences found...`, `Failed to edit, expected 1 occurrences but found 2...`).
|
||||
- **Confirmation:** Yes. Shows a diff of the proposed changes and asks for user approval before writing to the file.
|
||||
|
||||
- `new_string` (string, required): The exact literal text to replace `old_string` with.
|
||||
- `expected_replacements` (number, optional): The number of occurrences to replace. Defaults to `1`.
|
||||
|
||||
- **Behavior:**
|
||||
- If `old_string` is empty and `file_path` does not exist, creates a new file with `new_string` as content.
|
||||
- If `old_string` is provided, it reads the `file_path` and attempts to find exactly one occurrence of `old_string`.
|
||||
- If one occurrence is found, it replaces it with `new_string`.
|
||||
- **Enhanced Reliability (Multi-Stage Edit Correction):** To significantly improve the success rate of edits, especially when the model-provided `old_string` might not be perfectly precise, the tool incorporates a multi-stage edit correction mechanism.
|
||||
- If the initial `old_string` isn't found or matches multiple locations, the tool can leverage the Gemini model to iteratively refine `old_string` (and potentially `new_string`).
|
||||
- This self-correction process attempts to identify the unique segment the model intended to modify, making the `replace` operation more robust even with slightly imperfect initial context from the AI.
|
||||
- **Failure Conditions:** Despite the correction mechanism, the tool will fail if:
|
||||
- `file_path` is not absolute or is outside the root directory.
|
||||
- `old_string` is not empty, but the `file_path` does not exist.
|
||||
- `old_string` is empty, but the `file_path` already exists.
|
||||
- `old_string` is not found in the file after attempts to correct it.
|
||||
- `old_string` is found multiple times, and the self-correction mechanism cannot resolve it to a single, unambiguous match.
|
||||
- This self-correction process attempts to identify the unique segment the model intended to modify, making the `replace` operation more robust even with slightly imperfect initial context.
|
||||
- **Failure conditions:** Despite the correction mechanism, the tool will fail if:
|
||||
- `file_path` is not absolute or is outside the root directory.
|
||||
- `old_string` is not empty, but the `file_path` does not exist.
|
||||
- `old_string` is empty, but the `file_path` already exists.
|
||||
- `old_string` is not found in the file after attempts to correct it.
|
||||
- `old_string` is found multiple times, and the self-correction mechanism cannot resolve it to a single, unambiguous match.
|
||||
- **Output (`llmContent`):**
|
||||
- On success: `Successfully modified file: /path/to/file.txt (1 replacements).` or `Created new file: /path/to/new_file.txt with provided content.`
|
||||
- On failure: An error message explaining the reason (e.g., `Failed to edit, 0 occurrences found...`, `Failed to edit, expected 1 occurrences but found 2...`).
|
||||
- **Confirmation:** Yes. Shows a diff of the proposed changes and asks for user approval before writing to the file.
|
||||
|
||||
These file system tools provide a robust foundation for the Gemini CLI to understand and interact with your local project context.
|
||||
These file system tools provide a foundation for the Gemini CLI to understand and interact with your local project context.
|
||||
|
||||
+26
-21
@@ -1,22 +1,24 @@
|
||||
# Gemini CLI Tools: Overview
|
||||
# Gemini CLI tools
|
||||
|
||||
The Gemini CLI is equipped with a powerful set of built-in tools that the Gemini model can utilize to interact with your local environment, access information, and perform actions. These tools significantly enhance the CLI's capabilities, allowing it to go beyond text generation and assist with a wide range of tasks.
|
||||
The Gemini CLI includes built-in tools that the Gemini model uses to interact with your local environment, access information, and perform actions. These tools enhance the CLI's capabilities, enabling it to go beyond text generation and assist with a wide range of tasks.
|
||||
|
||||
## What are Tools?
|
||||
## Overview of Gemini CLI tools
|
||||
|
||||
In the context of the Gemini CLI, tools are specific functions or modules that the Gemini model can request to be executed. For example, if you ask Gemini to "Summarize the contents of `my_document.txt`," the model will likely identify the need to read that file and will request the execution of the `read_file` tool.
|
||||
|
||||
The core component (`packages/core`) manages these tools, presents their definitions (schemas) to the Gemini model, executes them when requested, and returns the results to the model for further processing into a user-facing response.
|
||||
|
||||
## Why are Tools Important?
|
||||
These tools provide the following capabilities:
|
||||
|
||||
- **Access to Local Information:** Tools allow Gemini to access your local file system, read file contents, list directories, etc.
|
||||
- **Execution of Commands:** With tools like `run_shell_command`, Gemini can run shell commands (with appropriate safety measures and user confirmation).
|
||||
- **Interaction with the Web:** Tools can fetch content from URLs.
|
||||
- **Action Taking:** Tools can modify files, write new files, or perform other actions on your system (again, typically with safeguards).
|
||||
- **Grounding Responses:** By using tools to fetch real-time or specific local data, Gemini's responses can be more accurate, relevant, and grounded in your actual context.
|
||||
- **Access local information:** Tools allow Gemini to access your local file system, read file contents, list directories, etc.
|
||||
- **Execute commands:** With tools like `run_shell_command`, Gemini can run shell commands (with appropriate safety measures and user confirmation).
|
||||
- **Interact with the web:** Tools can fetch content from URLs.
|
||||
- **Take actions:** Tools can modify files, write new files, or perform other actions on your system (again, typically with safeguards).
|
||||
- **Ground responses:** By using tools to fetch real-time or specific local data, Gemini's responses can be more accurate, relevant, and grounded in your actual context.
|
||||
|
||||
## How Tools are Used
|
||||
## How to use Gemini CLI tools
|
||||
|
||||
To use Gemini CLI tools, provide a prompt to the Gemini CLI. The process works as follows:
|
||||
|
||||
1. You provide a prompt to the Gemini CLI.
|
||||
2. The CLI sends the prompt to the core.
|
||||
@@ -28,24 +30,27 @@ The core component (`packages/core`) manages these tools, presents their definit
|
||||
|
||||
You will typically see messages in the CLI indicating when a tool is being called and whether it succeeded or failed.
|
||||
|
||||
## Security and Confirmation
|
||||
## Security and confirmation
|
||||
|
||||
Many tools, especially those that can modify your file system or execute commands (`write_file`, `edit`, `run_shell_command`), are designed with safety in mind. The Gemini CLI will typically:
|
||||
|
||||
- **Require Confirmation:** Prompt you before executing potentially sensitive operations, showing you what action is about to be taken.
|
||||
- **Utilize Sandboxing:** All tools are subject to restrictions enforced by sandboxing (see [README](../../README.md#sandboxing)). This means that when operating in a sandbox, any tools (including MCP servers) you wish to use must be available _inside_ the sandbox environment. For example, to run an MCP server through `npx`, the `npx` executable must be installed within the sandbox's Docker image or be available in the `sandbox-exec` environment.
|
||||
- **Require confirmation:** Prompt you before executing potentially sensitive operations, showing you what action is about to be taken.
|
||||
- **Utilize sandboxing:** All tools are subject to restrictions enforced by sandboxing (see [README](../../README.md#sandboxing)). This means that when operating in a sandbox, any tools (including MCP servers) you wish to use must be available _inside_ the sandbox environment. For example, to run an MCP server through `npx`, the `npx` executable must be installed within the sandbox's Docker image or be available in the `sandbox-exec` environment.
|
||||
|
||||
It's important to always review confirmation prompts carefully before allowing a tool to proceed.
|
||||
|
||||
## Categories of Built-in Tools
|
||||
## Learn more about Gemini CLI's tools
|
||||
|
||||
The built-in tools can be broadly categorized as follows:
|
||||
Gemini CLI's built-in tools can be broadly categorized as follows:
|
||||
|
||||
- **[File System Tools](./file-system.md):** For interacting with files and directories (reading, writing, listing, searching, etc.).
|
||||
- **[Shell Tool](./shell.md):** For executing shell commands.
|
||||
- **[Web Fetch Tool](./web-fetch.md):** For retrieving content from URLs.
|
||||
- **[Web Search Tool](./web-search.md):** For searching the web.
|
||||
- **[Multi-File Read Tool](./multi-file.md):** A specialized tool for reading content from multiple files or directories, often used by the `@` command.
|
||||
- **[Memory Tool](./memory.md):** For saving and recalling information across sessions.
|
||||
- **[Shell Tool](./shell.md) (`run_shell_command`):** For executing shell commands.
|
||||
- **[Web Fetch Tool](./web-fetch.md) (`web_fetch`):** For retrieving content from URLs.
|
||||
- **[Web Search Tool](./web-search.md) (`web_search`):** For searching the web.
|
||||
- **[Multi-File Read Tool](./multi-file.md) (`read_many_files`):** A specialized tool for reading content from multiple files or directories, often used by the `@` command.
|
||||
- **[Memory Tool](./memory.md) (`save_memory`):** For saving and recalling information across sessions.
|
||||
|
||||
Understanding the available tools and how they work will help you make the most effective use of the Gemini CLI.
|
||||
Additionally, these tools incorporate:
|
||||
|
||||
- **[MCP servers](./mcp-server.md)**: MCP servers act as a bridge between the Gemini model and your local environment or other services like APIs.
|
||||
- **[Sandboxing](../sandbox.md)**: Sandboxing isolates the model and its changes from your environment to reduce potential risk.
|
||||
|
||||
@@ -0,0 +1,428 @@
|
||||
# MCP servers with the Gemini CLI
|
||||
|
||||
This document provides a guide to configuring and using Model Context Protocol (MCP) servers with the Gemini CLI.
|
||||
|
||||
## What is an MCP server?
|
||||
|
||||
An MCP server is an application that exposes tools and resources to the Gemini CLI through the Model Context Protocol, allowing it to interact with external systems and data sources. MCP servers act as a bridge between the Gemini model and your local environment or other services like APIs.
|
||||
|
||||
An MCP server enables the Gemini CLI to:
|
||||
|
||||
- **Discover tools:** List available tools, their descriptions, and parameters through standardized schema definitions.
|
||||
- **Execute tools:** Call specific tools with defined arguments and receive structured responses.
|
||||
- **Access resources:** Read data from specific resources (though the Gemini CLI primarily focuses on tool execution).
|
||||
|
||||
With an MCP server, you can extend the Gemini CLI's capabilities to perform actions beyond its built-in features, such as interacting with databases, APIs, custom scripts, or specialized workflows.
|
||||
|
||||
## Core Integration Architecture
|
||||
|
||||
The Gemini CLI integrates with MCP servers through a sophisticated discovery and execution system built into the core package (`packages/core/src/tools/`):
|
||||
|
||||
### Discovery Layer (`mcp-client.ts`)
|
||||
|
||||
The discovery process is orchestrated by `discoverMcpTools()`, which:
|
||||
|
||||
1. **Iterates through configured servers** from your `settings.json` `mcpServers` configuration
|
||||
2. **Establishes connections** using appropriate transport mechanisms (Stdio, SSE, or Streamable HTTP)
|
||||
3. **Fetches tool definitions** from each server using the MCP protocol
|
||||
4. **Sanitizes and validates** tool schemas for compatibility with the Gemini API
|
||||
5. **Registers tools** in the global tool registry with conflict resolution
|
||||
|
||||
### Execution Layer (`mcp-tool.ts`)
|
||||
|
||||
Each discovered MCP tool is wrapped in a `DiscoveredMCPTool` instance that:
|
||||
|
||||
- **Handles confirmation logic** based on server trust settings and user preferences
|
||||
- **Manages tool execution** by calling the MCP server with proper parameters
|
||||
- **Processes responses** for both the LLM context and user display
|
||||
- **Maintains connection state** and handles timeouts
|
||||
|
||||
### Transport Mechanisms
|
||||
|
||||
The Gemini CLI supports three MCP transport types:
|
||||
|
||||
- **Stdio Transport:** Spawns a subprocess and communicates via stdin/stdout
|
||||
- **SSE Transport:** Connects to Server-Sent Events endpoints
|
||||
- **Streamable HTTP Transport:** Uses HTTP streaming for communication
|
||||
|
||||
## How to set up your MCP server
|
||||
|
||||
The Gemini CLI uses the `mcpServers` configuration in your `settings.json` file to locate and connect to MCP servers. This configuration supports multiple servers with different transport mechanisms.
|
||||
|
||||
### Configure the MCP server in settings.json
|
||||
|
||||
You can configure MCP servers at the global level in the `~/.gemini/settings.json` file or in your project's root directory, create or open the `.gemini/settings.json` file. Within the file, add the `mcpServers` configuration block.
|
||||
|
||||
### Configuration Structure
|
||||
|
||||
Add an `mcpServers` object to your `settings.json` file:
|
||||
|
||||
```json
|
||||
{ ...file contains other config objects
|
||||
"mcpServers": {
|
||||
"serverName": {
|
||||
"command": "path/to/server",
|
||||
"args": ["--arg1", "value1"],
|
||||
"env": {
|
||||
"API_KEY": "$MY_API_TOKEN"
|
||||
},
|
||||
"cwd": "./server-directory",
|
||||
"timeout": 30000,
|
||||
"trust": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Properties
|
||||
|
||||
Each server configuration supports the following properties:
|
||||
|
||||
#### Required (one of the following)
|
||||
|
||||
- **`command`** (string): Path to the executable for Stdio transport
|
||||
- **`url`** (string): SSE endpoint URL (e.g., `"http://localhost:8080/sse"`)
|
||||
- **`httpUrl`** (string): HTTP streaming endpoint URL
|
||||
|
||||
#### Optional
|
||||
|
||||
- **`args`** (string[]): Command-line arguments for Stdio transport
|
||||
- **`env`** (object): Environment variables for the server process. Values can reference environment variables using `$VAR_NAME` or `${VAR_NAME}` syntax
|
||||
- **`cwd`** (string): Working directory for Stdio transport
|
||||
- **`timeout`** (number): Request timeout in milliseconds (default: 600,000ms = 10 minutes)
|
||||
- **`trust`** (boolean): When `true`, bypasses all tool call confirmations for this server (default: `false`)
|
||||
|
||||
### Example Configurations
|
||||
|
||||
#### Python MCP Server (Stdio)
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"pythonTools": {
|
||||
"command": "python",
|
||||
"args": ["-m", "my_mcp_server", "--port", "8080"],
|
||||
"cwd": "./mcp-servers/python",
|
||||
"env": {
|
||||
"DATABASE_URL": "$DB_CONNECTION_STRING",
|
||||
"API_KEY": "${EXTERNAL_API_KEY}"
|
||||
},
|
||||
"timeout": 15000
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Node.js MCP Server (Stdio)
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"nodeServer": {
|
||||
"command": "node",
|
||||
"args": ["dist/server.js", "--verbose"],
|
||||
"cwd": "./mcp-servers/node",
|
||||
"trust": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Docker-based MCP Server
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"dockerizedServer": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"-e",
|
||||
"API_KEY",
|
||||
"-v",
|
||||
"${PWD}:/workspace",
|
||||
"my-mcp-server:latest"
|
||||
],
|
||||
"env": {
|
||||
"API_KEY": "$EXTERNAL_SERVICE_TOKEN"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### HTTP-based MCP Server
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"httpServer": {
|
||||
"httpUrl": "http://localhost:3000/mcp",
|
||||
"timeout": 5000
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Discovery Process Deep Dive
|
||||
|
||||
When the Gemini CLI starts, it performs MCP server discovery through the following detailed process:
|
||||
|
||||
### 1. Server Iteration and Connection
|
||||
|
||||
For each configured server in `mcpServers`:
|
||||
|
||||
1. **Status tracking begins:** Server status is set to `CONNECTING`
|
||||
2. **Transport selection:** Based on configuration properties:
|
||||
- `httpUrl` → `StreamableHTTPClientTransport`
|
||||
- `url` → `SSEClientTransport`
|
||||
- `command` → `StdioClientTransport`
|
||||
3. **Connection establishment:** The MCP client attempts to connect with the configured timeout
|
||||
4. **Error handling:** Connection failures are logged and the server status is set to `DISCONNECTED`
|
||||
|
||||
### 2. Tool Discovery
|
||||
|
||||
Upon successful connection:
|
||||
|
||||
1. **Tool listing:** The client calls the MCP server's tool listing endpoint
|
||||
2. **Schema validation:** Each tool's function declaration is validated
|
||||
3. **Name sanitization:** Tool names are cleaned to meet Gemini API requirements:
|
||||
- Invalid characters (non-alphanumeric, underscore, dot, hyphen) are replaced with underscores
|
||||
- Names longer than 63 characters are truncated with middle replacement (`___`)
|
||||
|
||||
### 3. Conflict Resolution
|
||||
|
||||
When multiple servers expose tools with the same name:
|
||||
|
||||
1. **First registration wins:** The first server to register a tool name gets the unprefixed name
|
||||
2. **Automatic prefixing:** Subsequent servers get prefixed names: `serverName__toolName`
|
||||
3. **Registry tracking:** The tool registry maintains mappings between server names and their tools
|
||||
|
||||
### 4. Schema Processing
|
||||
|
||||
Tool parameter schemas undergo sanitization for Gemini API compatibility:
|
||||
|
||||
- **`$schema` properties** are removed
|
||||
- **`additionalProperties`** are stripped
|
||||
- **`anyOf` with `default`** have their default values removed (Vertex AI compatibility)
|
||||
- **Recursive processing** applies to nested schemas
|
||||
|
||||
### 5. Connection Management
|
||||
|
||||
After discovery:
|
||||
|
||||
- **Persistent connections:** Servers that successfully register tools maintain their connections
|
||||
- **Cleanup:** Servers that provide no usable tools have their connections closed
|
||||
- **Status updates:** Final server statuses are set to `CONNECTED` or `DISCONNECTED`
|
||||
|
||||
## Tool Execution Flow
|
||||
|
||||
When the Gemini model decides to use an MCP tool, the following execution flow occurs:
|
||||
|
||||
### 1. Tool Invocation
|
||||
|
||||
The model generates a `FunctionCall` with:
|
||||
|
||||
- **Tool name:** The registered name (potentially prefixed)
|
||||
- **Arguments:** JSON object matching the tool's parameter schema
|
||||
|
||||
### 2. Confirmation Process
|
||||
|
||||
Each `DiscoveredMCPTool` implements sophisticated confirmation logic:
|
||||
|
||||
#### Trust-based Bypass
|
||||
|
||||
```typescript
|
||||
if (this.trust) {
|
||||
return false; // No confirmation needed
|
||||
}
|
||||
```
|
||||
|
||||
#### Dynamic Allow-listing
|
||||
|
||||
The system maintains internal allow-lists for:
|
||||
|
||||
- **Server-level:** `serverName` → All tools from this server are trusted
|
||||
- **Tool-level:** `serverName.toolName` → This specific tool is trusted
|
||||
|
||||
#### User Choice Handling
|
||||
|
||||
When confirmation is required, users can choose:
|
||||
|
||||
- **Proceed once:** Execute this time only
|
||||
- **Always allow this tool:** Add to tool-level allow-list
|
||||
- **Always allow this server:** Add to server-level allow-list
|
||||
- **Cancel:** Abort execution
|
||||
|
||||
### 3. Execution
|
||||
|
||||
Upon confirmation (or trust bypass):
|
||||
|
||||
1. **Parameter preparation:** Arguments are validated against the tool's schema
|
||||
2. **MCP call:** The underlying `CallableTool` invokes the server with:
|
||||
|
||||
```typescript
|
||||
const functionCalls = [
|
||||
{
|
||||
name: this.serverToolName, // Original server tool name
|
||||
args: params,
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
3. **Response processing:** Results are formatted for both LLM context and user display
|
||||
|
||||
### 4. Response Handling
|
||||
|
||||
The execution result contains:
|
||||
|
||||
- **`llmContent`:** Raw response parts for the language model's context
|
||||
- **`returnDisplay`:** Formatted output for user display (often JSON in markdown code blocks)
|
||||
|
||||
## How to interact with your MCP server
|
||||
|
||||
### Using the `/mcp` Command
|
||||
|
||||
The `/mcp` command provides comprehensive information about your MCP server setup:
|
||||
|
||||
```bash
|
||||
/mcp
|
||||
```
|
||||
|
||||
This displays:
|
||||
|
||||
- **Server list:** All configured MCP servers
|
||||
- **Connection status:** `CONNECTED`, `CONNECTING`, or `DISCONNECTED`
|
||||
- **Server details:** Configuration summary (excluding sensitive data)
|
||||
- **Available tools:** List of tools from each server with descriptions
|
||||
- **Discovery state:** Overall discovery process status
|
||||
|
||||
### Example `/mcp` Output
|
||||
|
||||
```
|
||||
MCP Servers Status:
|
||||
|
||||
📡 pythonTools (CONNECTED)
|
||||
Command: python -m my_mcp_server --port 8080
|
||||
Working Directory: ./mcp-servers/python
|
||||
Timeout: 15000ms
|
||||
Tools: calculate_sum, file_analyzer, data_processor
|
||||
|
||||
🔌 nodeServer (DISCONNECTED)
|
||||
Command: node dist/server.js --verbose
|
||||
Error: Connection refused
|
||||
|
||||
🐳 dockerizedServer (CONNECTED)
|
||||
Command: docker run -i --rm -e API_KEY my-mcp-server:latest
|
||||
Tools: docker__deploy, docker__status
|
||||
|
||||
Discovery State: COMPLETED
|
||||
```
|
||||
|
||||
### Tool Usage
|
||||
|
||||
Once discovered, MCP tools are available to the Gemini model like built-in tools. The model will automatically:
|
||||
|
||||
1. **Select appropriate tools** based on your requests
|
||||
2. **Present confirmation dialogs** (unless the server is trusted)
|
||||
3. **Execute tools** with proper parameters
|
||||
4. **Display results** in a user-friendly format
|
||||
|
||||
## Status Monitoring and Troubleshooting
|
||||
|
||||
### Connection States
|
||||
|
||||
The MCP integration tracks several states:
|
||||
|
||||
#### Server Status (`MCPServerStatus`)
|
||||
|
||||
- **`DISCONNECTED`:** Server is not connected or has errors
|
||||
- **`CONNECTING`:** Connection attempt in progress
|
||||
- **`CONNECTED`:** Server is connected and ready
|
||||
|
||||
#### Discovery State (`MCPDiscoveryState`)
|
||||
|
||||
- **`NOT_STARTED`:** Discovery hasn't begun
|
||||
- **`IN_PROGRESS`:** Currently discovering servers
|
||||
- **`COMPLETED`:** Discovery finished (with or without errors)
|
||||
|
||||
### Common Issues and Solutions
|
||||
|
||||
#### Server Won't Connect
|
||||
|
||||
**Symptoms:** Server shows `DISCONNECTED` status
|
||||
|
||||
**Troubleshooting:**
|
||||
|
||||
1. **Check configuration:** Verify `command`, `args`, and `cwd` are correct
|
||||
2. **Test manually:** Run the server command directly to ensure it works
|
||||
3. **Check dependencies:** Ensure all required packages are installed
|
||||
4. **Review logs:** Look for error messages in the CLI output
|
||||
5. **Verify permissions:** Ensure the CLI can execute the server command
|
||||
|
||||
#### No Tools Discovered
|
||||
|
||||
**Symptoms:** Server connects but no tools are available
|
||||
|
||||
**Troubleshooting:**
|
||||
|
||||
1. **Verify tool registration:** Ensure your server actually registers tools
|
||||
2. **Check MCP protocol:** Confirm your server implements the MCP tool listing correctly
|
||||
3. **Review server logs:** Check stderr output for server-side errors
|
||||
4. **Test tool listing:** Manually test your server's tool discovery endpoint
|
||||
|
||||
#### Tools Not Executing
|
||||
|
||||
**Symptoms:** Tools are discovered but fail during execution
|
||||
|
||||
**Troubleshooting:**
|
||||
|
||||
1. **Parameter validation:** Ensure your tool accepts the expected parameters
|
||||
2. **Schema compatibility:** Verify your input schemas are valid JSON Schema
|
||||
3. **Error handling:** Check if your tool is throwing unhandled exceptions
|
||||
4. **Timeout issues:** Consider increasing the `timeout` setting
|
||||
|
||||
#### Sandbox Compatibility
|
||||
|
||||
**Symptoms:** MCP servers fail when sandboxing is enabled
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Docker-based servers:** Use Docker containers that include all dependencies
|
||||
2. **Path accessibility:** Ensure server executables are available in the sandbox
|
||||
3. **Network access:** Configure sandbox to allow necessary network connections
|
||||
4. **Environment variables:** Verify required environment variables are passed through
|
||||
|
||||
### Debugging Tips
|
||||
|
||||
1. **Enable debug mode:** Run the CLI with `--debug_mode` for verbose output
|
||||
2. **Check stderr:** MCP server stderr is captured and logged (INFO messages filtered)
|
||||
3. **Test isolation:** Test your MCP server independently before integrating
|
||||
4. **Incremental setup:** Start with simple tools before adding complex functionality
|
||||
5. **Use `/mcp` frequently:** Monitor server status during development
|
||||
|
||||
## Important Notes
|
||||
|
||||
### Security Considerations
|
||||
|
||||
- **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
|
||||
- **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 information leakage between repositories
|
||||
|
||||
### Performance and Resource Management
|
||||
|
||||
- **Connection persistence:** The CLI maintains persistent connections to servers that successfully register tools
|
||||
- **Automatic cleanup:** Connections to servers providing no tools are automatically closed
|
||||
- **Timeout management:** Configure appropriate timeouts based on your server's response characteristics
|
||||
- **Resource monitoring:** MCP servers run as separate processes and consume system resources
|
||||
|
||||
### Schema Compatibility
|
||||
|
||||
- **Property stripping:** The system automatically removes certain schema properties (`$schema`, `additionalProperties`) for Gemini API compatibility
|
||||
- **Name sanitization:** Tool names are automatically sanitized to meet API requirements
|
||||
- **Conflict resolution:** Tool name conflicts between servers are resolved through automatic prefixing
|
||||
|
||||
This comprehensive integration makes MCP servers a powerful way to extend the Gemini CLI's capabilities while maintaining security, reliability, and ease of use.
|
||||
+41
-22
@@ -1,25 +1,44 @@
|
||||
# Memory Tool
|
||||
# Memory Tool (`save_memory`)
|
||||
|
||||
This document provides details on the `save_memory` tool.
|
||||
This document describes the `save_memory` tool for the Gemini CLI.
|
||||
|
||||
## `save_memory`
|
||||
## Description
|
||||
|
||||
- **Purpose:** Saves a specific piece of information or fact to your long-term memory. This allows the CLI to remember key details across sessions, providing more personalized and effective assistance.
|
||||
- **Arguments:**
|
||||
- `fact` (string, required): The specific fact or piece of information to remember. This should be a clear, self-contained statement.
|
||||
- **Behavior:**
|
||||
- The tool appends the provided `fact` to a special `GEMINI.md` file located in the user's home directory (`~/.gemini/GEMINI.md`). This file can be configured to have a different name.
|
||||
- The facts are stored under a `## Gemini Added Memories` section.
|
||||
- This file is loaded as context in subsequent sessions, allowing the CLI to recall the saved information.
|
||||
- **Examples:**
|
||||
- Remembering a user preference:
|
||||
```
|
||||
save_memory(fact="My preferred programming language is Python.")
|
||||
```
|
||||
- Storing a project-specific detail:
|
||||
```
|
||||
save_memory(fact="The project I'm currently working on is called 'gemini-cli'.")
|
||||
```
|
||||
- **Important Notes:**
|
||||
- This tool should be used for concise, important facts. It is not intended for storing large amounts of data or conversational history.
|
||||
- The memory file is a plain text Markdown file, so you can view and edit it manually if needed.
|
||||
Use `save_memory` to save and recall information across your Gemini CLI sessions. With `save_memory`, you can direct the CLI to remember key details across sessions, providing personalized and directed assistance.
|
||||
|
||||
### Arguments
|
||||
|
||||
`save_memory` takes one argument:
|
||||
|
||||
- `fact` (string, required): The specific fact or piece of information to remember. This should be a clear, self-contained statement written in natural language.
|
||||
|
||||
## How to use `save_memory` with the Gemini CLI
|
||||
|
||||
The tool appends the provided `fact` to a special `GEMINI.md` file located in the user's home directory (`~/.gemini/GEMINI.md`). This file can be configured to have a different name.
|
||||
|
||||
Once added, the facts are stored under a `## Gemini Added Memories` section. This file is loaded as context in subsequent sessions, allowing the CLI to recall the saved information.
|
||||
|
||||
Usage:
|
||||
|
||||
```
|
||||
save_memory(fact="Your fact here.")
|
||||
```
|
||||
|
||||
### `save_memory` examples
|
||||
|
||||
Remember a user preference:
|
||||
|
||||
```
|
||||
save_memory(fact="My preferred programming language is Python.")
|
||||
```
|
||||
|
||||
Store a project-specific detail:
|
||||
|
||||
```
|
||||
save_memory(fact="The project I'm currently working on is called 'gemini-cli'.")
|
||||
```
|
||||
|
||||
## Important notes
|
||||
|
||||
- **General usage:** This tool should be used for concise, important facts. It is not intended for storing large amounts of data or conversational history.
|
||||
- **Memory file:** The memory file is a plain text Markdown file, so you can view and edit it manually if needed.
|
||||
|
||||
+63
-36
@@ -1,39 +1,66 @@
|
||||
# Multi-File Read Tool
|
||||
# Multi File Read Tool (`read_many_files`)
|
||||
|
||||
This document provides details on the `read_many_files` tool.
|
||||
This document describes the `read_many_files` tool for the Gemini CLI.
|
||||
|
||||
## `read_many_files`
|
||||
## Description
|
||||
|
||||
- **Purpose:** Reads content from multiple files specified by paths or glob patterns. For text files, it concatenates their content into a single string. For image (e.g., PNG, JPEG) and PDF files, it reads and returns them as base64 encoded data, provided they are explicitly requested by name or extension. This is useful for getting an overview of a codebase, finding where specific functionality is implemented, reviewing documentation, or gathering context from multiple configuration files.
|
||||
- **Arguments:**
|
||||
- `paths` (list[string], required): An array of glob patterns or paths relative to the tool's target directory (e.g., `["src/**/*.ts"]`, `["README.md", "docs/", "assets/logo.png"]`).
|
||||
- `exclude` (list[string], optional): Glob patterns for files/directories to exclude (e.g., `["**/*.log", "temp/"]`). These are added to default excludes if `useDefaultExcludes` is true.
|
||||
- `include` (list[string], optional): Additional glob patterns to include. These are merged with `paths` (e.g., `["*.test.ts"]` to specifically add test files if they were broadly excluded, or `["images/*.jpg"]` to include specific image types).
|
||||
- `recursive` (boolean, optional): Whether to search recursively. This is primarily controlled by `**` in glob patterns. Defaults to `true`.
|
||||
- `useDefaultExcludes` (boolean, optional): Whether to apply a list of default exclusion patterns (e.g., `node_modules`, `.git`, non image/pdf binary files). Defaults to `true`.
|
||||
- `respect_git_ignore` (boolean, optional): Whether to respect .gitignore patterns when finding files. Defaults to true.
|
||||
- **Behavior:**
|
||||
- The tool searches for files matching the provided `paths` and `include` patterns, while respecting `exclude` patterns and default excludes (if enabled).
|
||||
- For text files: it reads the content of each matched file (attempting to skip binary files not explicitly requested as image/PDF) and concatenates it into a single string, with a separator `--- {filePath} ---` between the content of each file. Uses UTF-8 encoding by default.
|
||||
- For image and PDF files: if explicitly requested by name or extension (e.g., `paths: ["logo.png"]` or `include: ["*.pdf"]`), the tool reads the file and returns its content as a base64 encoded string.
|
||||
- The tool attempts to detect and skip other binary files (those not matching common image/PDF types or not explicitly requested) by checking for null bytes in their initial content.
|
||||
- **Examples:**
|
||||
- Reading all TypeScript files in the `src` directory:
|
||||
```
|
||||
read_many_files(paths=["src/**/*.ts"])
|
||||
```
|
||||
- Reading the main README, all Markdown files in the `docs` directory, and a specific logo image, excluding a specific file:
|
||||
```
|
||||
read_many_files(paths=["README.md", "docs/**/*.md", "assets/logo.png"], exclude=["docs/OLD_README.md"])
|
||||
```
|
||||
- Reading all JavaScript files but explicitly including test files and all JPEGs in an `images` folder:
|
||||
```
|
||||
read_many_files(paths=["**/*.js"], include=["**/*.test.js", "images/**/*.jpg"], useDefaultExcludes=False)
|
||||
```
|
||||
- **Important Notes:**
|
||||
- **Binary File Handling:**
|
||||
- **Image/PDF Files:** The tool can read common image types (PNG, JPEG, etc.) and PDF files, returning them as base64 encoded data. These files _must_ be explicitly targeted by the `paths` or `include` patterns (e.g., by specifying the exact filename like `image.png` or a pattern like `*.jpeg`).
|
||||
- **Other Binary Files:** The tool attempts to detect and skip other types of binary files by examining their initial content for null bytes. Its behavior with such files is to exclude them from the output.
|
||||
- **Performance:** Reading a very large number of files or very large individual files can be resource-intensive.
|
||||
- **Path Specificity:** Ensure paths and glob patterns are correctly specified relative to the tool's target directory. For image/PDF files, ensure the patterns are specific enough to include them.
|
||||
- **Default Excludes:** Be aware of the default exclusion patterns (like `node_modules`, `.git`) and use `useDefaultExcludes=False` if you need to override them, but do so cautiously.
|
||||
Use `read_many_files` to read content from multiple files specified by paths or glob patterns. The behavior of this tool depends on the provided files:
|
||||
|
||||
- For text files, this tool concatenates their content into a single string.
|
||||
- For image (e.g., PNG, JPEG) and PDF files, it reads and returns them as base64-encoded data, provided they are explicitly requested by name or extension.
|
||||
|
||||
`read_many_files` can be used to perform tasks such as getting an overview of a codebase, finding where specific functionality is implemented, reviewing documentation, or gathering context from multiple configuration files.
|
||||
|
||||
### Arguments
|
||||
|
||||
`read_many_files` takes the following arguments:
|
||||
|
||||
- `paths` (list[string], required): An array of glob patterns or paths relative to the tool's target directory (e.g., `["src/**/*.ts"]`, `["README.md", "docs/", "assets/logo.png"]`).
|
||||
- `exclude` (list[string], optional): Glob patterns for files/directories to exclude (e.g., `["**/*.log", "temp/"]`). These are added to default excludes if `useDefaultExcludes` is true.
|
||||
- `include` (list[string], optional): Additional glob patterns to include. These are merged with `paths` (e.g., `["*.test.ts"]` to specifically add test files if they were broadly excluded, or `["images/*.jpg"]` to include specific image types).
|
||||
- `recursive` (boolean, optional): Whether to search recursively. This is primarily controlled by `**` in glob patterns. Defaults to `true`.
|
||||
- `useDefaultExcludes` (boolean, optional): Whether to apply a list of default exclusion patterns (e.g., `node_modules`, `.git`, non image/pdf binary files). Defaults to `true`.
|
||||
- `respect_git_ignore` (boolean, optional): Whether to respect .gitignore patterns when finding files. Defaults to true.
|
||||
|
||||
## How to use `read_many_files` with the Gemini CLI
|
||||
|
||||
`read_many_files` searches for files matching the provided `paths` and `include` patterns, while respecting `exclude` patterns and default excludes (if enabled).
|
||||
|
||||
- For text files: it reads the content of each matched file (attempting to skip binary files not explicitly requested as image/PDF) and concatenates it into a single string, with a separator `--- {filePath} ---` between the content of each file. Uses UTF-8 encoding by default.
|
||||
- For image and PDF files: if explicitly requested by name or extension (e.g., `paths: ["logo.png"]` or `include: ["*.pdf"]`), the tool reads the file and returns its content as a base64 encoded string.
|
||||
- The tool attempts to detect and skip other binary files (those not matching common image/PDF types or not explicitly requested) by checking for null bytes in their initial content.
|
||||
|
||||
Usage:
|
||||
|
||||
```
|
||||
read_many_files(paths=["Your files or paths here."], include=["Additional files to include."], exclude=["Files to exclude."], recursive=False, useDefaultExcludes=false, respect_git_ignore=true)
|
||||
```
|
||||
|
||||
## `read_many_files` examples
|
||||
|
||||
Read all TypeScript files in the `src` directory:
|
||||
|
||||
```
|
||||
read_many_files(paths=["src/**/*.ts"])
|
||||
```
|
||||
|
||||
Read the main README, all Markdown files in the `docs` directory, and a specific logo image, excluding a specific file:
|
||||
|
||||
```
|
||||
read_many_files(paths=["README.md", "docs/**/*.md", "assets/logo.png"], exclude=["docs/OLD_README.md"])
|
||||
```
|
||||
|
||||
Read all JavaScript files but explicitly including test files and all JPEGs in an `images` folder:
|
||||
|
||||
```
|
||||
read_many_files(paths=["**/*.js"], include=["**/*.test.js", "images/**/*.jpg"], useDefaultExcludes=False)
|
||||
```
|
||||
|
||||
## Important notes
|
||||
|
||||
- **Binary file handling:**
|
||||
- **Image/PDF files:** The tool can read common image types (PNG, JPEG, etc.) and PDF files, returning them as base64 encoded data. These files _must_ be explicitly targeted by the `paths` or `include` patterns (e.g., by specifying the exact filename like `image.png` or a pattern like `*.jpeg`).
|
||||
- **Other binary files:** The tool attempts to detect and skip other types of binary files by examining their initial content for null bytes. The tool excludes these files from its output.
|
||||
- **Performance:** Reading a very large number of files or very large individual files can be resource-intensive.
|
||||
- **Path specificity:** Ensure paths and glob patterns are correctly specified relative to the tool's target directory. For image/PDF files, ensure the patterns are specific enough to include them.
|
||||
- **Default excludes:** Be aware of the default exclusion patterns (like `node_modules`, `.git`) and use `useDefaultExcludes=False` if you need to override them, but do so cautiously.
|
||||
|
||||
+53
-38
@@ -1,46 +1,61 @@
|
||||
# Shell Tool
|
||||
# Shell Tool (`run_shell_command`)
|
||||
|
||||
This document provides details on the shell tool.
|
||||
This document describes the `run_shell_command` tool for the Gemini CLI.
|
||||
|
||||
## `run_shell_command`
|
||||
## Description
|
||||
|
||||
- **Purpose:** Executes a given shell command. On Windows, this will be executed with `cmd.exe /c`. On other platforms, it will be executed with `bash -c`. This tool is essential for interacting with the underlying operating system, running scripts, or performing command-line operations.
|
||||
- **Arguments:**
|
||||
- `command` (string, required): The exact shell command to execute.
|
||||
- `description` (string, optional): A brief description of the command's purpose, which will be shown to the user.
|
||||
- `directory` (string, optional): The directory (relative to the project root) in which to execute the command. If not provided, the command runs in the project root.
|
||||
- **Behavior:**
|
||||
- The command is executed as a subprocess.
|
||||
- It can start background processes using `&`.
|
||||
- The tool returns detailed information about the execution, including:
|
||||
- `Command`: The command that was executed.
|
||||
- `Directory`: The directory where the command was run.
|
||||
- `Stdout`: Output from the standard output stream.
|
||||
- `Stderr`: Output from the standard error stream.
|
||||
- `Error`: Any error message reported by the subprocess.
|
||||
- `Exit Code`: The exit code of the command.
|
||||
- `Signal`: The signal number if the command was terminated by a signal.
|
||||
- `Background PIDs`: A list of PIDs for any background processes started.
|
||||
- **Examples:**
|
||||
Use `run_shell_command` to interact with the underlying system, run scripts, or perform command-line operations. `run_shell_command` executes a given shell command. On Windows, the command will be executed with `cmd.exe /c`. On other platforms, the command will be executed with `bash -c`.
|
||||
|
||||
- Listing files in the current directory:
|
||||
### Arguments
|
||||
|
||||
```
|
||||
`run_shell_command` takes the following arguments:
|
||||
|
||||
run_shell_command(command="ls -la")
|
||||
```
|
||||
- `command` (string, required): The exact shell command to execute.
|
||||
- `description` (string, optional): A brief description of the command's purpose, which will be shown to the user.
|
||||
- `directory` (string, optional): The directory (relative to the project root) in which to execute the command. If not provided, the command runs in the project root.
|
||||
|
||||
- Running a script in a specific directory:
|
||||
```
|
||||
run_shell_command(command="./my_script.sh", directory="scripts", description="Run my custom script")
|
||||
```
|
||||
- Starting a background server:
|
||||
```
|
||||
run_shell_command(command="npm run dev &", description="Start development server in background")
|
||||
```
|
||||
## How to use `run_shell_command` with the Gemini CLI
|
||||
|
||||
- **Important Notes:**
|
||||
- **Security:** Be cautious when executing commands, especially those constructed from user input, to prevent security vulnerabilities.
|
||||
- **Interactive Commands:** Avoid commands that require interactive user input, as this can cause the tool to hang. Use non-interactive flags if available (e.g., `npm init -y`).
|
||||
- **Error Handling:** Check the `Stderr`, `Error`, and `Exit Code` fields to determine if a command executed successfully.
|
||||
- **Background Processes:** When a command is run in the background with `&`, the tool will return immediately and the process will continue to run in the background. The `Background PIDs` field will contain the process ID of the background process.
|
||||
When using `run_shell_command`, the command is executed as a subprocess. `run_shell_command` can start background processes using `&`. The tool returns detailed information about the execution, including:
|
||||
|
||||
- `Command`: The command that was executed.
|
||||
- `Directory`: The directory where the command was run.
|
||||
- `Stdout`: Output from the standard output stream.
|
||||
- `Stderr`: Output from the standard error stream.
|
||||
- `Error`: Any error message reported by the subprocess.
|
||||
- `Exit Code`: The exit code of the command.
|
||||
- `Signal`: The signal number if the command was terminated by a signal.
|
||||
- `Background PIDs`: A list of PIDs for any background processes started.
|
||||
|
||||
Usage:
|
||||
|
||||
```
|
||||
run_shell_command(command="Your commands.", description="Your description of the command.", directory="Your execution directory.")
|
||||
```
|
||||
|
||||
## `run_shell_command` examples
|
||||
|
||||
List files in the current directory:
|
||||
|
||||
```
|
||||
run_shell_command(command="ls -la")
|
||||
```
|
||||
|
||||
Run a script in a specific directory:
|
||||
|
||||
```
|
||||
run_shell_command(command="./my_script.sh", directory="scripts", description="Run my custom script")
|
||||
```
|
||||
|
||||
Start a background server:
|
||||
|
||||
```
|
||||
run_shell_command(command="npm run dev &", description="Start development server in background")
|
||||
```
|
||||
|
||||
## Important notes
|
||||
|
||||
- **Security:** Be cautious when executing commands, especially those constructed from user input, to prevent security vulnerabilities.
|
||||
- **Interactive commands:** Avoid commands that require interactive user input, as this can cause the tool to hang. Use non-interactive flags if available (e.g., `npm init -y`).
|
||||
- **Error handling:** Check the `Stderr`, `Error`, and `Exit Code` fields to determine if a command executed successfully.
|
||||
- **Background processes:** When a command is run in the background with `&`, the tool will return immediately and the process will continue to run in the background. The `Background PIDs` field will contain the process ID of the background process.
|
||||
|
||||
+41
-24
@@ -1,27 +1,44 @@
|
||||
# Web Fetch Tool
|
||||
# Web Fetch Tool (`web_fetch`)
|
||||
|
||||
This document describes the `web_fetch` tool.
|
||||
This document describes the `web_fetch` tool for the Gemini CLI.
|
||||
|
||||
## `web_fetch`
|
||||
## Description
|
||||
|
||||
- **Purpose:** Processes content from one or more URLs (up to 20) embedded in a prompt. This tool is ideal for tasks that require summarizing, comparing, or extracting specific information from web pages.
|
||||
- **Arguments:**
|
||||
- `prompt` (string, required): A comprehensive prompt that includes the URL(s) (up to 20) to fetch and specific instructions on how to process their content. For example: `"Summarize https://example.com/article and extract key points from https://another.com/data"`. The prompt must contain at least one URL starting with `http://` or `https://`.
|
||||
- **Behavior:**
|
||||
- The tool will first ask for confirmation before fetching any URLs.
|
||||
- It attempts to process URLs through the Gemini API's `urlContext` tool first.
|
||||
- If the Gemini API cannot access a URL (e.g., it's on `localhost`, a private network, or behind a firewall), the tool will fall back to fetching the content directly from the local machine.
|
||||
- The tool can fetch content from `localhost` and private network addresses via this fallback mechanism.
|
||||
- The tool formats the response, including source attribution with citations where possible, and returns it to the user.
|
||||
- **Examples:**
|
||||
- Summarizing a single article:
|
||||
```
|
||||
web_fetch(prompt="Can you summarize the main points of https://example.com/news/latest")
|
||||
```
|
||||
- Comparing two articles:
|
||||
```
|
||||
web_fetch(prompt="What are the differences in the conclusions of these two papers: https://arxiv.org/abs/2401.0001 and https://arxiv.org/abs/2401.0002?")
|
||||
```
|
||||
- **Important Notes:**
|
||||
- This tool relies on the Gemini API's ability to access and process the given URLs.
|
||||
- The quality of the output depends on the clarity of the instructions in the prompt.
|
||||
Use `web_fetch` to summarize, compare, or extract information from web pages. The `web_fetch` tool processes content from one or more URLs (up to 20) embedded in a prompt. `web_fetch` takes a natural language prompt and returns a generated response.
|
||||
|
||||
### Arguments
|
||||
|
||||
`web_fetch` takes one argument:
|
||||
|
||||
- `prompt` (string, required): A comprehensive prompt that includes the URL(s) (up to 20) to fetch and specific instructions on how to process their content. For example: `"Summarize https://example.com/article and extract key points from https://another.com/data"`. The prompt must contain at least one URL starting with `http://` or `https://`.
|
||||
|
||||
## How to use `web_fetch` with the Gemini CLI
|
||||
|
||||
To use `web_fetch` with the Gemini CLI, provide a natural language prompt that contains URLs. The tool will ask for confirmation before fetching any URLs. Once confirmed, the tool will process URLs through Gemini API's `urlContext`.
|
||||
|
||||
If the Gemini API cannot access the URL, the tool will fall back to fetching content directly from the local machine. The tool will format the response, including source attribution and citations where possible. The tool will then provide the response to the user.
|
||||
|
||||
Usage:
|
||||
|
||||
```
|
||||
web_fetch(prompt="Your prompt, including a URL such as https://google.com.")
|
||||
```
|
||||
|
||||
## `web_fetch` examples
|
||||
|
||||
Summarize a single article:
|
||||
|
||||
```
|
||||
web_fetch(prompt="Can you summarize the main points of https://example.com/news/latest")
|
||||
```
|
||||
|
||||
Compare two articles:
|
||||
|
||||
```
|
||||
web_fetch(prompt="What are the differences in the conclusions of these two papers: https://arxiv.org/abs/2401.0001 and https://arxiv.org/abs/2401.0002?")
|
||||
```
|
||||
|
||||
## Important notes
|
||||
|
||||
- **URL processing:** `web_fetch` relies on the Gemini API's ability to access and process the given URLs.
|
||||
- **Output quality:** The quality of the output will depend on the clarity of the instructions in the prompt.
|
||||
|
||||
+32
-16
@@ -1,20 +1,36 @@
|
||||
# Google Web Search Tool
|
||||
# Web Search Tool (`google_web_search`)
|
||||
|
||||
This document describes the `google_web_search` tool.
|
||||
|
||||
## `google_web_search`
|
||||
## Description
|
||||
|
||||
- **Purpose:** Performs a web search using Google Search (via the Gemini API) and returns a summary of the results with sources. This tool is useful for finding up-to-date information on the internet.
|
||||
- **Arguments:**
|
||||
- `query` (string, required): The search query.
|
||||
- **Behavior:**
|
||||
- The tool sends the query to the Gemini API, which performs a Google Search.
|
||||
- It returns a generated response based on the search results, complete with citations and a list of sources.
|
||||
- **Examples:**
|
||||
- Getting information on a topic:
|
||||
```
|
||||
google_web_search(query="latest advancements in AI-powered code generation")
|
||||
```
|
||||
- **Important Notes:**
|
||||
- The tool returns a processed summary, not a raw list of search results.
|
||||
- The response includes citations to the sources used to generate the summary.
|
||||
Use `google_web_search` to perform a web search using Google Search via the Gemini API. The `google_web_search` tool returns a summary of web results with sources.
|
||||
|
||||
### Arguments
|
||||
|
||||
`google_web_search` takes one argument:
|
||||
|
||||
- `query` (string, required): The search query.
|
||||
|
||||
## How to use `google_web_search` with the Gemini CLI
|
||||
|
||||
The `google_web_search` tool sends a query to the Gemini API, which then performs a web search. `google_web_search` will return a generated response based on the search results, including citations and sources.
|
||||
|
||||
Usage:
|
||||
|
||||
```
|
||||
google_web_search(query="Your query goes here.")
|
||||
```
|
||||
|
||||
## `google_web_search` examples
|
||||
|
||||
Get information on a topic:
|
||||
|
||||
```
|
||||
google_web_search(query="latest advancements in AI-powered code generation")
|
||||
```
|
||||
|
||||
## Important notes
|
||||
|
||||
- **Response returned:** The `google_web_search` tool returns a processed summary, not a raw list of search results.
|
||||
- **Citations:** The response includes citations to the sources used to generate the summary.
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# Gemini CLI: Terms of Service and Privacy Notice
|
||||
|
||||
Gemini CLI is an open-source tool that allows you to interact with Google's powerful language models directly from your command-line interface. The terms of service and privacy notices that apply to your usage of Gemini CLI depend on the type of account you use to authenticate with Google.
|
||||
|
||||
This article outlines the specific terms and privacy policies applicable for different auth methods.
|
||||
|
||||
## 1. Login with Google (Gemini Code Assist for [individuals](https://developers.google.com/gemini-code-assist/docs/overview#supported-features-gca))
|
||||
|
||||
For users who authenticate using their Google account to access Gemini Code Assist for individuals:
|
||||
|
||||
- Terms of Service: Your use of Gemini CLI is governed by the general [Google Terms of Service](https://policies.google.com/terms?hl=en-US).
|
||||
- Privacy Notice: The collection and use of your data are described in the [Gemini Code Assist Privacy Notice for Individuals](https://developers.google.com/gemini-code-assist/resources/privacy-notice-gemini-code-assist-individuals).
|
||||
|
||||
## 2. Gemini API Key (Using Gemini Developer [API](https://ai.google.dev/gemini-api/docs) a: Unpaid Service, b: Paid Service)
|
||||
|
||||
If you are using a Gemini API key for authentication, the following terms apply:
|
||||
|
||||
- Terms of Service: Your use is subject to the [Gemini API Terms of Service](https://ai.google.dev/gemini-api/terms). For a. [Unpaid Service](https://ai.google.dev/gemini-api/terms#unpaid-services) or b. [Paid Service](https://ai.google.dev/gemini-api/terms#paid-services)
|
||||
- Privacy Notice: Information regarding data handling and privacy is detailed in the general [Google Privacy Policy](https://policies.google.com/privacy).
|
||||
|
||||
## 3. Login with Google (for Workspace or Licensed Code Assist users)
|
||||
|
||||
For users of Standard or Enterprise [edition](https://cloud.google.com/gemini/docs/codeassist/overview#editions-overview) of Gemini Code Assist :
|
||||
|
||||
- Terms of Service: The [Google Cloud Platform Terms of Service](https://cloud.google.com/terms) govern your use of the service.
|
||||
- Privacy Notice: The handling of your data is outlined in the [Gemini Code Assist Privacy Notices](https://developers.google.com/gemini-code-assist/resources/privacy-notices).
|
||||
|
||||
## 4. Vertex AI (Using Vertex AI Gen [API](https://cloud.google.com/vertex-ai/generative-ai/docs/reference/rest))
|
||||
|
||||
If you are using an API key with a Vertex AI Gen API backend:
|
||||
|
||||
- Terms of Service: Your usage is governed by the [Google Cloud Platform Service Terms](https://cloud.google.com/terms/service-terms/).
|
||||
- Privacy Notice: The [Google Cloud Privacy Notice](https://cloud.google.com/terms/cloud-privacy-notice) describes how your data is collected and managed.
|
||||
|
||||
### Usage Statistics Opt-Out
|
||||
|
||||
You may opt-out from sending Usage Statistics to Google data by following the instructions available here: [Usage Statistics Configuration](./cli/configuration.md#usage-statistics).
|
||||
|
||||
## Frequently Asked Questions (FAQ) for Gemini CLI
|
||||
|
||||
### 1. Is my code, including prompts and answers, used to train Google's models?
|
||||
|
||||
This depends entirely on the type of auth method you use.
|
||||
|
||||
- **Auth method 1:** Yes. When you use your personal Google account, the Gemini Code Assist Privacy Notice for Individuals applies. Under this notice, your **prompts, answers, and related code are collected** and may be used to improve Google's products, which includes model training.
|
||||
- **Auth method 2a:** Yes, When you use the Gemini API key Gemini API (Unpaid Service) terms apply. Under this notice , your **prompts, answers, and related code are collected** and may be used to improve Google's products, which includes model training.
|
||||
- **Auth method 2b, 3 & 4:** No. For these accounts, your data is governed by the Google Cloud or Gemini API (Paid Service) terms, which treat your inputs as confidential. Your code, prompts, and other inputs are **not** used to train models.
|
||||
|
||||
### 2. What are "Usage Statistics" and what does the opt-out control?
|
||||
|
||||
The "Usage Statistics" setting is the single control for all optional data collection in the Gemini CLI. The data it collects depends on your account type:
|
||||
|
||||
- **Auth method 1:** When enabled, this setting allows Google to collect both anonymous telemetry (like commands run and performance metrics) and **your prompts and answers** for model improvement.
|
||||
- **Auth method 2a:** When enabled, this setting allows Google to collect both anonymous telemetry (like commands run and performance metrics) and **your prompts and answers** for model improvement. When disabled we will use your data as described in the [How Google Uses Your Data](https://ai.google.dev/gemini-api/terms#data-use-unpaid).
|
||||
- **Auth method 2b:** This setting only controls the collection of anonymous telemetry. Google logs prompts and responses for a limited period of time, solely for the purpose of detecting violations of the Prohibited Use Policy and any required legal or regulatory disclosures
|
||||
- **Auth methods 3 & 4:** This setting only controls the collection of anonymous telemetry. Your prompts and answers are never collected, regardless of this setting.
|
||||
|
||||
You can disable Usage Statistics for any account type by following the instructions in the [Usage Statistics Configuration](./cli/configuration.md#usage-statistics) documentation.
|
||||
+19
-2
@@ -2,16 +2,33 @@
|
||||
|
||||
This guide provides solutions to common issues and debugging tips.
|
||||
|
||||
## Authentication
|
||||
|
||||
- **Error: `Failed to login. Message: Request contains an invalid argument`**
|
||||
|
||||
- Users with Google Workspace accounts, or users with Google Cloud accounts
|
||||
associated with their Gmail accounts may not be able to activate the free
|
||||
tier of the Google Code Assist plan.
|
||||
- For Google Cloud accounts, you can work around this by setting
|
||||
`GOOGLE_CLOUD_PROJECT` to your project ID.
|
||||
- You can also grab an API key from [AI
|
||||
Studio](http://aistudio.google.com/app/apikey), which also includes a
|
||||
separate free tier.
|
||||
|
||||
## Frequently asked questions (FAQs)
|
||||
|
||||
- **Q: How do I update Gemini CLI to the latest version?**
|
||||
|
||||
- A: If installed globally via npm, update Gemini CLI using the command `npm install -g @gemini-cli/cli@latest`. If run from source, pull the latest changes from the repository and rebuild using `npm run build`.
|
||||
- A: If installed globally via npm, update Gemini CLI using the command `npm install -g @google/gemini-cli@latest`. If run from source, pull the latest changes from the repository and rebuild using `npm run build`.
|
||||
|
||||
- **Q: Where are Gemini CLI configuration files stored?**
|
||||
|
||||
- A: The CLI configuration is stored within two `settings.json` files: one in your home directory and one in your project's root directory. In both locations, `settings.json` is found in the `.gemini/` folder. Refer to [CLI Configuration](./cli/configuration.md) for more details.
|
||||
|
||||
- **Q: Why don't I see cached token counts in my stats output?**
|
||||
|
||||
- A: Cached token information is only displayed when cached tokens are being used. This feature is available for API key users (Gemini API key or Vertex AI) but not for OAuth users (Google Personal/Enterprise accounts) at this time, as the Code Assist API does not support cached content creation. You can still view your total token usage with the `/stats` command.
|
||||
|
||||
## Common error messages and solutions
|
||||
|
||||
- **Error: `EADDRINUSE` (Address already in use) when starting an MCP server.**
|
||||
@@ -20,7 +37,7 @@ This guide provides solutions to common issues and debugging tips.
|
||||
- **Solution:**
|
||||
Either stop the other process that is using the port or configure the MCP server to use a different port.
|
||||
|
||||
- **Error: Command not found (when attempting the run Gemini CLI).**
|
||||
- **Error: Command not found (when attempting to run Gemini CLI).**
|
||||
|
||||
- **Cause:** Gemini CLI is not correctly installed or not in your system's PATH.
|
||||
- **Solution:**
|
||||
|
||||
@@ -13,6 +13,7 @@ test('should be able to list a directory', async (t) => {
|
||||
rig.setup(t.name);
|
||||
rig.createFile('file1.txt', 'file 1 content');
|
||||
rig.mkdir('subdir');
|
||||
rig.sync();
|
||||
|
||||
const prompt = `Can you list the files in the current directory`;
|
||||
const result = await rig.run(prompt);
|
||||
|
||||
@@ -42,6 +42,11 @@ export class TestRig {
|
||||
mkdirSync(join(this.testDir, dir));
|
||||
}
|
||||
|
||||
sync() {
|
||||
// ensure file system is done before spawning
|
||||
execSync('sync', { cwd: this.testDir });
|
||||
}
|
||||
|
||||
run(prompt, ...args) {
|
||||
const output = execSync(
|
||||
`node ${this.bundlePath} --yolo --prompt "${prompt}" ${args.join(' ')}`,
|
||||
|
||||
Generated
+160
-23
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "gemini-cli",
|
||||
"version": "0.1.0",
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.1.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "gemini-cli",
|
||||
"version": "0.1.0",
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.1.6",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
@@ -18,6 +18,7 @@
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@types/minimatch": "^5.1.2",
|
||||
"@vitest/coverage-v8": "^3.1.1",
|
||||
"concurrently": "^9.2.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"esbuild": "^0.25.0",
|
||||
"eslint": "^9.24.0",
|
||||
@@ -35,6 +36,9 @@
|
||||
"react-devtools-core": "^4.28.5",
|
||||
"typescript-eslint": "^8.30.1",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@alcalzone/ansi-tokenize": {
|
||||
@@ -874,11 +878,11 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@gemini-cli/cli": {
|
||||
"node_modules/@google/gemini-cli": {
|
||||
"resolved": "packages/cli",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@gemini-cli/core": {
|
||||
"node_modules/@google/gemini-cli-core": {
|
||||
"resolved": "packages/core",
|
||||
"link": true
|
||||
},
|
||||
@@ -2614,9 +2618,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3384,9 +3388,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3817,6 +3821,48 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concurrently": {
|
||||
"version": "9.2.0",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.0.tgz",
|
||||
"integrity": "sha512-IsB/fiXTupmagMW4MNp2lx2cdSN2FfZq78vF90LBB+zZHArbIQZjQtzXCiXnvTxCZSvXanTqFLWBjw2UkLx1SQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.2",
|
||||
"lodash": "^4.17.21",
|
||||
"rxjs": "^7.8.1",
|
||||
"shell-quote": "^1.8.1",
|
||||
"supports-color": "^8.1.1",
|
||||
"tree-kill": "^1.2.2",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"conc": "dist/bin/concurrently.js",
|
||||
"concurrently": "dist/bin/concurrently.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/config-chain": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
|
||||
@@ -5638,9 +5684,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
@@ -6204,6 +6250,24 @@
|
||||
"ink": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/ink-link": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ink-link/-/ink-link-4.1.0.tgz",
|
||||
"integrity": "sha512-3nNyJXum0FJIKAXBK8qat2jEOM41nJ1J60NRivwgK9Xh92R5UMN/k4vbz0A9xFzhJVrlf4BQEmmxMgXkCE1Jeg==",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.8.1",
|
||||
"terminal-link": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ink": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/ink-select-input": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ink-select-input/-/ink-select-input-6.2.0.tgz",
|
||||
@@ -8852,6 +8916,16 @@
|
||||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rxjs": {
|
||||
"version": "7.8.2",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-array-concat": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
|
||||
@@ -9614,6 +9688,18 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-hyperlinks": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz",
|
||||
"integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0",
|
||||
"supports-color": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-preserve-symlinks-flag": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||
@@ -9633,6 +9719,46 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/terminal-link": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-3.0.0.tgz",
|
||||
"integrity": "sha512-flFL3m4wuixmf6IfhFJd1YPiLiMuxEc8uHRM1buzIeZPm22Au2pDqBJQgdo7n1WfPU1ONFGv7YDwpFBmHGF6lg==",
|
||||
"dependencies": {
|
||||
"ansi-escapes": "^5.0.0",
|
||||
"supports-hyperlinks": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/terminal-link/node_modules/ansi-escapes": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz",
|
||||
"integrity": "sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==",
|
||||
"dependencies": {
|
||||
"type-fest": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/terminal-link/node_modules/type-fest": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz",
|
||||
"integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/test-exclude": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz",
|
||||
@@ -9649,9 +9775,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/test-exclude/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -9888,6 +10014,16 @@
|
||||
"tslib": "2"
|
||||
}
|
||||
},
|
||||
"node_modules/tree-kill": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
||||
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"tree-kill": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-api-utils": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
||||
@@ -11534,10 +11670,10 @@
|
||||
}
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@gemini-cli/cli",
|
||||
"version": "0.1.0",
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.1.6",
|
||||
"dependencies": {
|
||||
"@gemini-cli/core": "file:../core",
|
||||
"@google/gemini-cli-core": "*",
|
||||
"@types/update-notifier": "^6.0.8",
|
||||
"command-exists": "^1.2.9",
|
||||
"diff": "^7.0.0",
|
||||
@@ -11547,6 +11683,7 @@
|
||||
"ink": "^5.2.0",
|
||||
"ink-big-text": "^2.0.0",
|
||||
"ink-gradient": "^3.0.0",
|
||||
"ink-link": "^4.0.0",
|
||||
"ink-select-input": "^6.0.0",
|
||||
"ink-spinner": "^5.0.0",
|
||||
"ink-text-input": "^6.0.0",
|
||||
@@ -11607,8 +11744,8 @@
|
||||
}
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@gemini-cli/core",
|
||||
"version": "0.1.0",
|
||||
"name": "@google/gemini-cli-core",
|
||||
"version": "0.1.6",
|
||||
"dependencies": {
|
||||
"@google/genai": "^1.4.0",
|
||||
"@modelcontextprotocol/sdk": "^1.11.0",
|
||||
|
||||
+20
-8
@@ -1,10 +1,17 @@
|
||||
{
|
||||
"name": "gemini-cli",
|
||||
"version": "0.1.0",
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.1.6",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"type": "module",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"repository": "google-gemini/gemini-cli",
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.1.5"
|
||||
},
|
||||
"scripts": {
|
||||
"generate": "node scripts/generate-git-commit-info.js",
|
||||
"build": "node scripts/build.js",
|
||||
@@ -20,12 +27,13 @@
|
||||
"test:integration:sandbox:docker": "GEMINI_SANDBOX=docker node integration-tests/run-tests.js",
|
||||
"test:integration:sandbox:podman": "GEMINI_SANDBOX=podman node integration-tests/run-tests.js",
|
||||
"start": "node scripts/start.js",
|
||||
"debug": "cross-env DEBUG=1 node scripts/start.js",
|
||||
"debug": "cross-env DEBUG=1 node --inspect-brk scripts/start.js",
|
||||
"lint:fix": "eslint . --fix && eslint integration-tests --fix",
|
||||
"lint": "eslint . --ext .ts,.tsx && eslint integration-tests",
|
||||
"lint:ci": "eslint . --ext .ts,.tsx --max-warnings 0 && eslint integration-tests --max-warnings 0",
|
||||
"typecheck": "npm run typecheck --workspaces --if-present",
|
||||
"format": "prettier --write .",
|
||||
"preflight": "npm run clean && npm ci && npm run format && npm run lint:fix && npm run build && npm run typecheck && npm run test:ci",
|
||||
"preflight": "npm run clean && npm ci && npm run format && npm run lint:ci && npm run build && npm run typecheck && npm run test:ci",
|
||||
"auth:npm": "npx google-artifactregistry-auth",
|
||||
"auth:docker": "gcloud auth configure-docker us-west1-docker.pkg.dev",
|
||||
"auth": "npm run auth:npm && npm run auth:docker",
|
||||
@@ -34,13 +42,15 @@
|
||||
"build:cli": "npm run build --workspace packages/cli",
|
||||
"build:core": "npm run build --workspace packages/core",
|
||||
"build:packages": "npm run build:core && npm run build:cli",
|
||||
"build:docker": "node scripts/build_sandbox.js -s",
|
||||
"build:sandbox:fast": "node scripts/build_sandbox.js --skip-npm-install-build",
|
||||
"prepare:cli-packagejson": "node scripts/prepare-cli-packagejson.js",
|
||||
"prepare:packages": "node scripts/prepare-cli-packagejson.js && node scripts/prepare-core-package.js",
|
||||
"publish:sandbox": "node scripts/publish-sandbox.js",
|
||||
"publish:npm": "npm publish --workspaces ${NPM_PUBLISH_TAG:+--tag=$NPM_PUBLISH_TAG} ${NPM_DRY_RUN:+--dry-run}",
|
||||
"publish:release": "npm run build:packages && npm run prepare:cli-packagejson && npm run build:docker && npm run publish:sandbox && npm run publish:npm",
|
||||
"publish:release": "npm run build:packages && npm run prepare:cli-packagejson && npm run build:sandbox:fast && npm run publish:sandbox && npm run publish:npm",
|
||||
"telemetry": "node scripts/telemetry.js",
|
||||
"start:gcp": "npm run telemetry -- --target=gcp & npm start"
|
||||
"start:gcp": "concurrently --raw --kill-others \"npm run telemetry -- --target=gcp\" \"npm start\"",
|
||||
"prepublishOnly": "node scripts/prepublish.js"
|
||||
},
|
||||
"bin": {
|
||||
"gemini": "bundle/gemini.js"
|
||||
@@ -55,6 +65,7 @@
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@types/minimatch": "^5.1.2",
|
||||
"@vitest/coverage-v8": "^3.1.1",
|
||||
"concurrently": "^9.2.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"esbuild": "^0.25.0",
|
||||
"eslint": "^9.24.0",
|
||||
@@ -72,5 +83,6 @@
|
||||
"react-devtools-core": "^4.28.5",
|
||||
"typescript-eslint": "^8.30.1",
|
||||
"yargs": "^17.7.2"
|
||||
}
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
# Gemini CLI
|
||||
|
||||
This package contains the core command-line interface for Gemini CLI.
|
||||
|
||||
## Setup
|
||||
|
||||
1. **Get a Gemini API Key:** Obtain your API key from Google AI Studio: [https://aistudio.google.com/app/apikey](https://aistudio.google.com/app/apikey)
|
||||
2. **Set Environment Variable:** Set the `GEMINI_API_KEY` environment variable to your obtained key. You can do this temporarily in your current shell session:
|
||||
```bash
|
||||
export GEMINI_API_KEY="YOUR_API_KEY"
|
||||
```
|
||||
Or add it to your shell's configuration file (like `~/.bashrc`, `~/.zshrc`, or `~/.profile`) for persistence:
|
||||
```bash
|
||||
echo 'export GEMINI_API_KEY="YOUR_API_KEY"' >> ~/.bashrc # Or your preferred shell config file
|
||||
source ~/.bashrc # Reload the config
|
||||
```
|
||||
Replace `"YOUR_API_KEY"` with your actual key.
|
||||
|
||||
## Building
|
||||
|
||||
To build only the CLI package, navigate to this directory (`packages/cli`) and run:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
This command executes the TypeScript compiler (`tsc`) as defined in this package's `package.json`. Ensure dependencies have been installed from the root directory (`npm install`) first.
|
||||
|
||||
## Running
|
||||
|
||||
To start the Gemini CLI directly from this directory:
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
This command executes `node dist/gemini.js` as defined in this package's `package.json`.
|
||||
|
||||
## Debugging
|
||||
|
||||
To debug the CLI application using VS Code:
|
||||
|
||||
1. Start the CLI in debug mode from this directory (`packages/cli`):
|
||||
```bash
|
||||
npm run debug
|
||||
```
|
||||
This command runs `node --inspect-brk dist/gemini.js`, pausing execution until a debugger attaches.
|
||||
2. In VS Code (opened at the root of the monorepo), use the "Attach" launch configuration (found in `.vscode/launch.json`). This configuration is set up to attach to the Node.js process listening on port 9229, which is the default port used by `--inspect-brk`.
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "@gemini-cli/cli",
|
||||
"version": "0.1.0",
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.1.6",
|
||||
"description": "Gemini CLI",
|
||||
"repository": "google-gemini/gemini-cli",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
@@ -19,8 +20,8 @@
|
||||
"typecheck": "tsc --noEmit",
|
||||
"prerelease:version": "node ../../scripts/bind_package_version.js",
|
||||
"prerelease:deps": "node ../../scripts/bind_package_dependencies.js",
|
||||
"prepublishOnly": "npm publish --workspace=@gemini-cli/core",
|
||||
"prepack": "npm run build"
|
||||
"prepack": "npm run build",
|
||||
"prepublishOnly": "node ../../scripts/prepublish.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
@@ -29,7 +30,7 @@
|
||||
"sandboxImageUri": "gemini-cli-sandbox"
|
||||
},
|
||||
"dependencies": {
|
||||
"@gemini-cli/core": "file:../core",
|
||||
"@google/gemini-cli-core": "*",
|
||||
"@types/update-notifier": "^6.0.8",
|
||||
"command-exists": "^1.2.9",
|
||||
"diff": "^7.0.0",
|
||||
@@ -41,6 +42,7 @@
|
||||
"ink-gradient": "^3.0.0",
|
||||
"ink-select-input": "^6.0.0",
|
||||
"ink-spinner": "^5.0.0",
|
||||
"ink-link": "^4.0.0",
|
||||
"ink-text-input": "^6.0.0",
|
||||
"lowlight": "^3.3.0",
|
||||
"mime-types": "^2.1.4",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { AuthType } from '@gemini-cli/core';
|
||||
import { AuthType } from '@google/gemini-cli-core';
|
||||
import { loadEnvironment } from './config.js';
|
||||
|
||||
export const validateAuthMethod = (authMethod: string): string | null => {
|
||||
@@ -13,13 +13,6 @@ export const validateAuthMethod = (authMethod: string): string | null => {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (authMethod === AuthType.LOGIN_WITH_GOOGLE_ENTERPRISE) {
|
||||
if (!process.env.GOOGLE_CLOUD_PROJECT) {
|
||||
return 'GOOGLE_CLOUD_PROJECT environment variable not found. Add that to your .env and try again, no reload needed!';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (authMethod === AuthType.USE_GEMINI) {
|
||||
if (!process.env.GEMINI_API_KEY) {
|
||||
return 'GEMINI_API_KEY environment variable not found. Add that to your .env and try again, no reload needed!';
|
||||
@@ -28,14 +21,16 @@ export const validateAuthMethod = (authMethod: string): string | null => {
|
||||
}
|
||||
|
||||
if (authMethod === AuthType.USE_VERTEX_AI) {
|
||||
if (!process.env.GOOGLE_API_KEY) {
|
||||
return 'GOOGLE_API_KEY environment variable not found. Add that to your .env and try again, no reload needed!';
|
||||
}
|
||||
if (!process.env.GOOGLE_CLOUD_PROJECT) {
|
||||
return 'GOOGLE_CLOUD_PROJECT environment variable not found. Add that to your .env and try again, no reload needed!';
|
||||
}
|
||||
if (!process.env.GOOGLE_CLOUD_LOCATION) {
|
||||
return 'GOOGLE_CLOUD_LOCATION environment variable not found. Add that to your .env and try again, no reload needed!';
|
||||
const hasVertexProjectLocationConfig =
|
||||
!!process.env.GOOGLE_CLOUD_PROJECT && !!process.env.GOOGLE_CLOUD_LOCATION;
|
||||
const hasGoogleApiKey = !!process.env.GOOGLE_API_KEY;
|
||||
if (!hasVertexProjectLocationConfig && !hasGoogleApiKey) {
|
||||
return (
|
||||
'Must specify GOOGLE_GENAI_USE_VERTEXAI=true and either:\n' +
|
||||
'• GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION environment variables.\n' +
|
||||
'• GOOGLE_API_KEY environment variable (if using express mode).\n' +
|
||||
'Update your .env and try again, no reload needed!'
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
Config,
|
||||
ConfigParameters,
|
||||
ContentGeneratorConfig,
|
||||
} from '@gemini-cli/core';
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
const TEST_CONTENT_GENERATOR_CONFIG: ContentGeneratorConfig = {
|
||||
apiKey: 'test-key',
|
||||
@@ -21,8 +21,8 @@ const TEST_CONTENT_GENERATOR_CONFIG: ContentGeneratorConfig = {
|
||||
};
|
||||
|
||||
// Mock file discovery service and tool registry
|
||||
vi.mock('@gemini-cli/core', async () => {
|
||||
const actual = await vi.importActual('@gemini-cli/core');
|
||||
vi.mock('@google/gemini-cli-core', async () => {
|
||||
const actual = await vi.importActual('@google/gemini-cli-core');
|
||||
return {
|
||||
...actual,
|
||||
FileDiscoveryService: vi.fn().mockImplementation(() => ({
|
||||
@@ -75,7 +75,9 @@ describe('Configuration Integration Tests', () => {
|
||||
sandbox: false,
|
||||
targetDir: tempDir,
|
||||
debugMode: false,
|
||||
fileFilteringRespectGitIgnore: false,
|
||||
fileFiltering: {
|
||||
respectGitIgnore: false,
|
||||
},
|
||||
};
|
||||
|
||||
const config = new Config(configParams);
|
||||
@@ -109,7 +111,9 @@ describe('Configuration Integration Tests', () => {
|
||||
sandbox: false,
|
||||
targetDir: tempDir,
|
||||
debugMode: false,
|
||||
fileFilteringRespectGitIgnore: false,
|
||||
fileFiltering: {
|
||||
respectGitIgnore: false,
|
||||
},
|
||||
};
|
||||
|
||||
const config = new Config(configParams);
|
||||
@@ -178,7 +182,9 @@ describe('Configuration Integration Tests', () => {
|
||||
sandbox: false,
|
||||
targetDir: tempDir,
|
||||
debugMode: false,
|
||||
fileFilteringRespectGitIgnore: false, // CI might need to see all files
|
||||
fileFiltering: {
|
||||
respectGitIgnore: false,
|
||||
}, // CI might need to see all files
|
||||
};
|
||||
|
||||
const config = new Config(configParams);
|
||||
@@ -204,4 +210,34 @@ describe('Configuration Integration Tests', () => {
|
||||
expect(config.getCheckpointingEnabled()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Extension Context Files', () => {
|
||||
it('should have an empty array for extension context files by default', () => {
|
||||
const configParams: ConfigParameters = {
|
||||
cwd: '/tmp',
|
||||
contentGeneratorConfig: TEST_CONTENT_GENERATOR_CONFIG,
|
||||
embeddingModel: 'test-embedding-model',
|
||||
sandbox: false,
|
||||
targetDir: tempDir,
|
||||
debugMode: false,
|
||||
};
|
||||
const config = new Config(configParams);
|
||||
expect(config.getExtensionContextFilePaths()).toEqual([]);
|
||||
});
|
||||
|
||||
it('should correctly store and return extension context file paths', () => {
|
||||
const contextFiles = ['/path/to/file1.txt', '/path/to/file2.js'];
|
||||
const configParams: ConfigParameters = {
|
||||
cwd: '/tmp',
|
||||
contentGeneratorConfig: TEST_CONTENT_GENERATOR_CONFIG,
|
||||
embeddingModel: 'test-embedding-model',
|
||||
sandbox: false,
|
||||
targetDir: tempDir,
|
||||
debugMode: false,
|
||||
extensionContextFilePaths: contextFiles,
|
||||
};
|
||||
const config = new Config(configParams);
|
||||
expect(config.getExtensionContextFilePaths()).toEqual(contextFiles);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,15 +9,13 @@ import * as os from 'os';
|
||||
import { loadCliConfig } from './config.js';
|
||||
import { Settings } from './settings.js';
|
||||
import { Extension } from './extension.js';
|
||||
import * as ServerConfig from '@gemini-cli/core';
|
||||
|
||||
const MOCK_HOME_DIR = '/mock/home/user';
|
||||
import * as ServerConfig from '@google/gemini-cli-core';
|
||||
|
||||
vi.mock('os', async (importOriginal) => {
|
||||
const actualOs = await importOriginal<typeof os>();
|
||||
return {
|
||||
...actualOs,
|
||||
homedir: vi.fn(() => MOCK_HOME_DIR),
|
||||
homedir: vi.fn(() => '/mock/home/user'),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -31,9 +29,10 @@ vi.mock('read-package-up', () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('@gemini-cli/core', async () => {
|
||||
const actualServer =
|
||||
await vi.importActual<typeof ServerConfig>('@gemini-cli/core');
|
||||
vi.mock('@google/gemini-cli-core', async () => {
|
||||
const actualServer = await vi.importActual<typeof ServerConfig>(
|
||||
'@google/gemini-cli-core',
|
||||
);
|
||||
return {
|
||||
...actualServer,
|
||||
loadEnvironment: vi.fn(),
|
||||
@@ -53,7 +52,7 @@ describe('loadCliConfig', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(os.homedir).mockReturnValue(MOCK_HOME_DIR);
|
||||
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
|
||||
process.env.GEMINI_API_KEY = 'test-api-key'; // Ensure API key is set for tests
|
||||
});
|
||||
|
||||
@@ -98,7 +97,7 @@ describe('loadCliConfig telemetry', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(os.homedir).mockReturnValue(MOCK_HOME_DIR);
|
||||
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
|
||||
process.env.GEMINI_API_KEY = 'test-api-key';
|
||||
});
|
||||
|
||||
@@ -250,7 +249,7 @@ describe('loadCliConfig telemetry', () => {
|
||||
describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(os.homedir).mockReturnValue(MOCK_HOME_DIR);
|
||||
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
|
||||
// Other common mocks would be reset here.
|
||||
});
|
||||
|
||||
@@ -310,7 +309,7 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
|
||||
// Example of a previously failing test structure:
|
||||
/*
|
||||
it('should correctly use mocked homedir for global path', async () => {
|
||||
const MOCK_GEMINI_DIR_LOCAL = path.join(MOCK_HOME_DIR, '.gemini');
|
||||
const MOCK_GEMINI_DIR_LOCAL = path.join('/mock/home/user', '.gemini');
|
||||
const MOCK_GLOBAL_PATH_LOCAL = path.join(MOCK_GEMINI_DIR_LOCAL, 'GEMINI.md');
|
||||
mockFs({
|
||||
[MOCK_GLOBAL_PATH_LOCAL]: { type: 'file', content: 'GlobalContentOnly' }
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
DEFAULT_GEMINI_EMBEDDING_MODEL,
|
||||
FileDiscoveryService,
|
||||
TelemetryTarget,
|
||||
} from '@gemini-cli/core';
|
||||
} from '@google/gemini-cli-core';
|
||||
import { Settings } from './settings.js';
|
||||
|
||||
import { Extension } from './extension.js';
|
||||
@@ -227,8 +227,13 @@ export async function loadCliConfig(
|
||||
settings.telemetry?.otlpEndpoint,
|
||||
logPrompts: argv.telemetryLogPrompts ?? settings.telemetry?.logPrompts,
|
||||
},
|
||||
usageStatisticsEnabled: settings.usageStatisticsEnabled ?? true,
|
||||
// Git-aware file filtering settings
|
||||
fileFilteringRespectGitIgnore: settings.fileFiltering?.respectGitIgnore,
|
||||
fileFiltering: {
|
||||
respectGitIgnore: settings.fileFiltering?.respectGitIgnore,
|
||||
enableRecursiveFileSearch:
|
||||
settings.fileFiltering?.enableRecursiveFileSearch,
|
||||
},
|
||||
checkpointing: argv.checkpointing || settings.checkpointing?.enabled,
|
||||
proxy:
|
||||
process.env.HTTPS_PROXY ||
|
||||
@@ -239,6 +244,7 @@ export async function loadCliConfig(
|
||||
fileDiscoveryService: fileService,
|
||||
bugCommand: settings.bugCommand,
|
||||
model: argv.model!,
|
||||
extensionContextFilePaths,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { MCPServerConfig } from '@gemini-cli/core';
|
||||
import { MCPServerConfig } from '@google/gemini-cli-core';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { SandboxConfig } from '@gemini-cli/core';
|
||||
import { SandboxConfig } from '@google/gemini-cli-core';
|
||||
import commandExists from 'command-exists';
|
||||
import * as os from 'node:os';
|
||||
import { getPackageJson } from '../utils/package.js';
|
||||
@@ -36,7 +36,12 @@ function getSandboxCommand(
|
||||
}
|
||||
|
||||
// note environment variable takes precedence over argument (from command line or settings)
|
||||
sandbox = process.env.GEMINI_SANDBOX?.toLowerCase().trim() ?? sandbox;
|
||||
const environmentConfiguredSandbox =
|
||||
process.env.GEMINI_SANDBOX?.toLowerCase().trim() ?? '';
|
||||
sandbox =
|
||||
environmentConfiguredSandbox?.length > 0
|
||||
? environmentConfiguredSandbox
|
||||
: sandbox;
|
||||
if (sandbox === '1' || sandbox === 'true') sandbox = true;
|
||||
else if (sandbox === '0' || sandbox === 'false' || !sandbox) sandbox = false;
|
||||
|
||||
@@ -53,7 +58,7 @@ function getSandboxCommand(
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
// confirm that specfied command exists
|
||||
// confirm that specified command exists
|
||||
if (commandExists.sync(sandbox)) {
|
||||
return sandbox;
|
||||
}
|
||||
|
||||
@@ -6,15 +6,13 @@
|
||||
|
||||
/// <reference types="vitest/globals" />
|
||||
|
||||
const MOCK_HOME_DIR = '/mock/home/user'; // MUST BE FIRST
|
||||
|
||||
// Mock 'os' first. Its factory uses MOCK_HOME_DIR.
|
||||
// Mock 'os' first.
|
||||
import * as osActual from 'os'; // Import for type info for the mock factory
|
||||
vi.mock('os', async (importOriginal) => {
|
||||
const actualOs = await importOriginal<typeof osActual>();
|
||||
return {
|
||||
...actualOs,
|
||||
homedir: vi.fn(() => MOCK_HOME_DIR),
|
||||
homedir: vi.fn(() => '/mock/home/user'),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -77,7 +75,7 @@ describe('Settings Loading and Merging', () => {
|
||||
mockFsMkdirSync = vi.mocked(fs.mkdirSync);
|
||||
mockStripJsonComments = vi.mocked(stripJsonComments);
|
||||
|
||||
vi.mocked(osActual.homedir).mockReturnValue(MOCK_HOME_DIR);
|
||||
vi.mocked(osActual.homedir).mockReturnValue('/mock/home/user');
|
||||
(mockStripJsonComments as unknown as Mock).mockImplementation(
|
||||
(jsonString: string) => jsonString,
|
||||
);
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
BugCommandSettings,
|
||||
TelemetrySettings,
|
||||
AuthType,
|
||||
} from '@gemini-cli/core';
|
||||
} from '@google/gemini-cli-core';
|
||||
import stripJsonComments from 'strip-json-comments';
|
||||
import { DefaultLight } from '../ui/themes/default-light.js';
|
||||
import { DefaultDark } from '../ui/themes/default.js';
|
||||
@@ -49,13 +49,16 @@ export interface Settings {
|
||||
contextFileName?: string | string[];
|
||||
accessibility?: AccessibilitySettings;
|
||||
telemetry?: TelemetrySettings;
|
||||
usageStatisticsEnabled?: boolean;
|
||||
preferredEditor?: string;
|
||||
bugCommand?: BugCommandSettings;
|
||||
checkpointing?: CheckpointingSettings;
|
||||
autoConfigureMaxOldSpaceSize?: boolean;
|
||||
|
||||
// Git-aware file filtering settings
|
||||
fileFiltering?: {
|
||||
respectGitIgnore?: boolean;
|
||||
enableRecursiveFileSearch?: boolean;
|
||||
};
|
||||
|
||||
// UI setting. Does not display the ANSI-controlled terminal title.
|
||||
|
||||
@@ -10,6 +10,9 @@ import { AppWrapper } from './ui/App.js';
|
||||
import { loadCliConfig } from './config/config.js';
|
||||
import { readStdin } from './utils/readStdin.js';
|
||||
import { basename } from 'node:path';
|
||||
import v8 from 'node:v8';
|
||||
import os from 'node:os';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { start_sandbox } from './utils/sandbox.js';
|
||||
import {
|
||||
LoadedSettings,
|
||||
@@ -30,10 +33,54 @@ import {
|
||||
sessionId,
|
||||
logUserPrompt,
|
||||
AuthType,
|
||||
} from '@gemini-cli/core';
|
||||
} from '@google/gemini-cli-core';
|
||||
import { validateAuthMethod } from './config/auth.js';
|
||||
import { setMaxSizedBoxDebugging } from './ui/components/shared/MaxSizedBox.js';
|
||||
|
||||
function getNodeMemoryArgs(config: Config): string[] {
|
||||
const totalMemoryMB = os.totalmem() / (1024 * 1024);
|
||||
const heapStats = v8.getHeapStatistics();
|
||||
const currentMaxOldSpaceSizeMb = Math.floor(
|
||||
heapStats.heap_size_limit / 1024 / 1024,
|
||||
);
|
||||
|
||||
// Set target to 50% of total memory
|
||||
const targetMaxOldSpaceSizeInMB = Math.floor(totalMemoryMB * 0.5);
|
||||
if (config.getDebugMode()) {
|
||||
console.debug(
|
||||
`Current heap size ${currentMaxOldSpaceSizeMb.toFixed(2)} MB`,
|
||||
);
|
||||
}
|
||||
|
||||
if (process.env.GEMINI_CLI_NO_RELAUNCH) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (targetMaxOldSpaceSizeInMB > currentMaxOldSpaceSizeMb) {
|
||||
if (config.getDebugMode()) {
|
||||
console.debug(
|
||||
`Need to relaunch with more memory: ${targetMaxOldSpaceSizeInMB.toFixed(2)} MB`,
|
||||
);
|
||||
}
|
||||
return [`--max-old-space-size=${targetMaxOldSpaceSizeInMB}`];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async function relaunchWithAdditionalArgs(additionalArgs: string[]) {
|
||||
const nodeArgs = [...additionalArgs, ...process.argv.slice(1)];
|
||||
const newEnv = { ...process.env, GEMINI_CLI_NO_RELAUNCH: 'true' };
|
||||
|
||||
const child = spawn(process.execPath, nodeArgs, {
|
||||
stdio: 'inherit',
|
||||
env: newEnv,
|
||||
});
|
||||
|
||||
await new Promise((resolve) => child.on('close', resolve));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
export async function main() {
|
||||
const workspaceRoot = process.cwd();
|
||||
const settings = loadSettings(workspaceRoot);
|
||||
@@ -55,7 +102,7 @@ export async function main() {
|
||||
const config = await loadCliConfig(settings.merged, extensions, sessionId);
|
||||
|
||||
// set default fallback to gemini api key
|
||||
// this has to go after load cli becuase thats where the env is set
|
||||
// this has to go after load cli because thats where the env is set
|
||||
if (!settings.merged.selectedAuthType && process.env.GEMINI_API_KEY) {
|
||||
settings.setValue(
|
||||
SettingScope.User,
|
||||
@@ -84,24 +131,38 @@ export async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
const memoryArgs = settings.merged.autoConfigureMaxOldSpaceSize
|
||||
? getNodeMemoryArgs(config)
|
||||
: [];
|
||||
|
||||
// hop into sandbox if we are outside and sandboxing is enabled
|
||||
if (!process.env.SANDBOX) {
|
||||
const sandboxConfig = config.getSandbox();
|
||||
if (sandboxConfig) {
|
||||
if (settings.merged.selectedAuthType) {
|
||||
// Validate authentication here because the sandbox will interfere with the Oauth2 web redirect.
|
||||
const err = validateAuthMethod(settings.merged.selectedAuthType);
|
||||
if (err) {
|
||||
console.error(err);
|
||||
try {
|
||||
const err = validateAuthMethod(settings.merged.selectedAuthType);
|
||||
if (err) {
|
||||
throw new Error(err);
|
||||
}
|
||||
await config.refreshAuth(settings.merged.selectedAuthType);
|
||||
} catch (err) {
|
||||
console.error('Error authenticating:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
await config.refreshAuth(settings.merged.selectedAuthType);
|
||||
}
|
||||
await start_sandbox(sandboxConfig);
|
||||
await start_sandbox(sandboxConfig, memoryArgs);
|
||||
process.exit(0);
|
||||
} else {
|
||||
// Not in a sandbox and not entering one, so relaunch with additional
|
||||
// arguments to control memory usage if needed.
|
||||
if (memoryArgs.length > 0) {
|
||||
await relaunchWithAdditionalArgs(memoryArgs);
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let input = config.getQuestion();
|
||||
const startupWarnings = await getStartupWarnings();
|
||||
|
||||
@@ -131,6 +192,8 @@ export async function main() {
|
||||
}
|
||||
|
||||
logUserPrompt(config, {
|
||||
'event.name': 'user_prompt',
|
||||
'event.timestamp': new Date().toISOString(),
|
||||
prompt: input,
|
||||
prompt_length: input.length,
|
||||
});
|
||||
|
||||
@@ -7,15 +7,14 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { runNonInteractive } from './nonInteractiveCli.js';
|
||||
import { Config, GeminiClient, ToolRegistry } from '@gemini-cli/core';
|
||||
import { Config, GeminiClient, ToolRegistry } from '@google/gemini-cli-core';
|
||||
import { GenerateContentResponse, Part, FunctionCall } from '@google/genai';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@gemini-cli/core', async () => {
|
||||
const actualCore =
|
||||
await vi.importActual<typeof import('@gemini-cli/core')>(
|
||||
'@gemini-cli/core',
|
||||
);
|
||||
vi.mock('@google/gemini-cli-core', async () => {
|
||||
const actualCore = await vi.importActual<
|
||||
typeof import('@google/gemini-cli-core')
|
||||
>('@google/gemini-cli-core');
|
||||
return {
|
||||
...actualCore,
|
||||
GeminiClient: vi.fn(),
|
||||
@@ -53,6 +52,7 @@ describe('runNonInteractive', () => {
|
||||
mockConfig = {
|
||||
getToolRegistry: vi.fn().mockReturnValue(mockToolRegistry),
|
||||
getGeminiClient: vi.fn().mockReturnValue(mockGeminiClient),
|
||||
getContentGeneratorConfig: vi.fn().mockReturnValue({}),
|
||||
} as unknown as Config;
|
||||
|
||||
mockProcessStdoutWrite = vi.fn().mockImplementation(() => true);
|
||||
@@ -109,7 +109,7 @@ describe('runNonInteractive', () => {
|
||||
};
|
||||
|
||||
const { executeToolCall: mockCoreExecuteToolCall } = await import(
|
||||
'@gemini-cli/core'
|
||||
'@google/gemini-cli-core'
|
||||
);
|
||||
vi.mocked(mockCoreExecuteToolCall).mockResolvedValue({
|
||||
callId: 'fc1',
|
||||
@@ -162,7 +162,7 @@ describe('runNonInteractive', () => {
|
||||
};
|
||||
|
||||
const { executeToolCall: mockCoreExecuteToolCall } = await import(
|
||||
'@gemini-cli/core'
|
||||
'@google/gemini-cli-core'
|
||||
);
|
||||
vi.mocked(mockCoreExecuteToolCall).mockResolvedValue({
|
||||
callId: 'fcError',
|
||||
@@ -215,8 +215,7 @@ describe('runNonInteractive', () => {
|
||||
await runNonInteractive(mockConfig, 'Initial fail');
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Error processing input:',
|
||||
apiError,
|
||||
'[API Error: API connection failed]',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
ToolRegistry,
|
||||
shutdownTelemetry,
|
||||
isTelemetrySdkInitialized,
|
||||
} from '@gemini-cli/core';
|
||||
} from '@google/gemini-cli-core';
|
||||
import {
|
||||
Content,
|
||||
Part,
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
GenerateContentResponse,
|
||||
} from '@google/genai';
|
||||
|
||||
import { parseAndFormatApiError } from './ui/utils/errorParsing.js';
|
||||
|
||||
function getResponseText(response: GenerateContentResponse): string | null {
|
||||
if (response.candidates && response.candidates.length > 0) {
|
||||
const candidate = response.candidates[0];
|
||||
@@ -45,6 +47,14 @@ export async function runNonInteractive(
|
||||
config: Config,
|
||||
input: string,
|
||||
): Promise<void> {
|
||||
// Handle EPIPE errors when the output is piped to a command that closes early.
|
||||
process.stdout.on('error', (err: NodeJS.ErrnoException) => {
|
||||
if (err.code === 'EPIPE') {
|
||||
// Exit gracefully if the pipe is closed.
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
|
||||
const geminiClient = config.getGeminiClient();
|
||||
const toolRegistry: ToolRegistry = await config.getToolRegistry();
|
||||
|
||||
@@ -89,6 +99,7 @@ export async function runNonInteractive(
|
||||
callId,
|
||||
name: fc.name as string,
|
||||
args: (fc.args ?? {}) as Record<string, unknown>,
|
||||
isClientInitiated: false,
|
||||
};
|
||||
|
||||
const toolResponse = await executeToolCall(
|
||||
@@ -125,7 +136,12 @@ export async function runNonInteractive(
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing input:', error);
|
||||
console.error(
|
||||
parseAndFormatApiError(
|
||||
error,
|
||||
config.getContentGeneratorConfig().authType,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
if (isTelemetrySdkInitialized()) {
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
ToolRegistry,
|
||||
AccessibilitySettings,
|
||||
SandboxConfig,
|
||||
} from '@gemini-cli/core';
|
||||
} from '@google/gemini-cli-core';
|
||||
import { LoadedSettings, SettingsFile, Settings } from '../config/settings.js';
|
||||
import process from 'node:process';
|
||||
|
||||
@@ -68,9 +68,10 @@ interface MockServerConfig {
|
||||
getAllGeminiMdFilenames: Mock<() => string[]>;
|
||||
}
|
||||
|
||||
// Mock @gemini-cli/core and its Config class
|
||||
vi.mock('@gemini-cli/core', async (importOriginal) => {
|
||||
const actualCore = await importOriginal<typeof import('@gemini-cli/core')>();
|
||||
// Mock @google/gemini-cli-core and its Config class
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actualCore =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
const ConfigClassMock = vi
|
||||
.fn()
|
||||
.mockImplementation((optionsPassedToConstructor) => {
|
||||
@@ -125,6 +126,7 @@ vi.mock('@gemini-cli/core', async (importOriginal) => {
|
||||
getGeminiClient: vi.fn(() => ({})),
|
||||
getCheckpointingEnabled: vi.fn(() => opts.checkpointing ?? true),
|
||||
getAllGeminiMdFilenames: vi.fn(() => ['GEMINI.md']),
|
||||
setFlashFallbackHandler: vi.fn(),
|
||||
};
|
||||
});
|
||||
return {
|
||||
@@ -260,7 +262,7 @@ describe('App UI', () => {
|
||||
|
||||
it('should display custom contextFileName in footer when set and count is 1', async () => {
|
||||
mockSettings = createMockSettings({
|
||||
contextFileName: 'AGENTS.MD',
|
||||
contextFileName: 'AGENTS.md',
|
||||
theme: 'Default',
|
||||
});
|
||||
mockConfig.getGeminiMdFileCount.mockReturnValue(1);
|
||||
@@ -275,12 +277,12 @@ describe('App UI', () => {
|
||||
);
|
||||
currentUnmount = unmount;
|
||||
await Promise.resolve();
|
||||
expect(lastFrame()).toContain('Using 1 AGENTS.MD file');
|
||||
expect(lastFrame()).toContain('Using 1 AGENTS.md file');
|
||||
});
|
||||
|
||||
it('should display the first custom contextFileName when an array is provided', async () => {
|
||||
it('should display a generic message when multiple context files with different names are provided', async () => {
|
||||
mockSettings = createMockSettings({
|
||||
contextFileName: ['AGENTS.MD', 'CONTEXT.MD'],
|
||||
contextFileName: ['AGENTS.md', 'CONTEXT.md'],
|
||||
theme: 'Default',
|
||||
});
|
||||
mockConfig.getGeminiMdFileCount.mockReturnValue(2);
|
||||
@@ -295,7 +297,7 @@ describe('App UI', () => {
|
||||
);
|
||||
currentUnmount = unmount;
|
||||
await Promise.resolve();
|
||||
expect(lastFrame()).toContain('Using 2 AGENTS.MD files');
|
||||
expect(lastFrame()).toContain('Using 2 context files');
|
||||
});
|
||||
|
||||
it('should display custom contextFileName with plural when set and count is > 1', async () => {
|
||||
|
||||
+142
-33
@@ -12,6 +12,7 @@ import {
|
||||
Static,
|
||||
Text,
|
||||
useStdin,
|
||||
useStdout,
|
||||
useInput,
|
||||
type Key as InkKeyType,
|
||||
} from 'ink';
|
||||
@@ -53,7 +54,7 @@ import {
|
||||
ApprovalMode,
|
||||
isEditorAvailable,
|
||||
EditorType,
|
||||
} from '@gemini-cli/core';
|
||||
} from '@google/gemini-cli-core';
|
||||
import { validateAuthMethod } from '../config/auth.js';
|
||||
import { useLogger } from './hooks/useLogger.js';
|
||||
import { StreamingContext } from './contexts/StreamingContext.js';
|
||||
@@ -62,10 +63,15 @@ import {
|
||||
useSessionStats,
|
||||
} from './contexts/SessionContext.js';
|
||||
import { useGitBranchName } from './hooks/useGitBranchName.js';
|
||||
import { useBracketedPaste } from './hooks/useBracketedPaste.js';
|
||||
import { useTextBuffer } from './components/shared/text-buffer.js';
|
||||
import * as fs from 'fs';
|
||||
import { UpdateNotification } from './components/UpdateNotification.js';
|
||||
import { checkForUpdates } from './utils/updateCheck.js';
|
||||
import ansiEscapes from 'ansi-escapes';
|
||||
import { OverflowProvider } from './contexts/OverflowContext.js';
|
||||
import { ShowMoreLines } from './components/ShowMoreLines.js';
|
||||
import { PrivacyNotice } from './privacy/PrivacyNotice.js';
|
||||
|
||||
const CTRL_EXIT_PROMPT_DURATION_MS = 1000;
|
||||
|
||||
@@ -82,7 +88,9 @@ export const AppWrapper = (props: AppProps) => (
|
||||
);
|
||||
|
||||
const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
|
||||
useBracketedPaste();
|
||||
const [updateMessage, setUpdateMessage] = useState<string | null>(null);
|
||||
const { stdout } = useStdout();
|
||||
|
||||
useEffect(() => {
|
||||
checkForUpdates().then(setUpdateMessage);
|
||||
@@ -98,8 +106,9 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
|
||||
const [staticNeedsRefresh, setStaticNeedsRefresh] = useState(false);
|
||||
const [staticKey, setStaticKey] = useState(0);
|
||||
const refreshStatic = useCallback(() => {
|
||||
stdout.write(ansiEscapes.clearTerminal);
|
||||
setStaticKey((prev) => prev + 1);
|
||||
}, [setStaticKey]);
|
||||
}, [setStaticKey, stdout]);
|
||||
|
||||
const [geminiMdFileCount, setGeminiMdFileCount] = useState<number>(0);
|
||||
const [debugMessage, setDebugMessage] = useState<string>('');
|
||||
@@ -109,6 +118,7 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
|
||||
const [editorError, setEditorError] = useState<string | null>(null);
|
||||
const [footerHeight, setFooterHeight] = useState<number>(0);
|
||||
const [corgiMode, setCorgiMode] = useState(false);
|
||||
const [currentModel, setCurrentModel] = useState(config.getModel());
|
||||
const [shellModeActive, setShellModeActive] = useState(false);
|
||||
const [showErrorDetails, setShowErrorDetails] = useState<boolean>(false);
|
||||
const [showToolDescriptions, setShowToolDescriptions] =
|
||||
@@ -121,6 +131,11 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
|
||||
const [ctrlDPressedOnce, setCtrlDPressedOnce] = useState(false);
|
||||
const ctrlDTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const [constrainHeight, setConstrainHeight] = useState<boolean>(true);
|
||||
const [showPrivacyNotice, setShowPrivacyNotice] = useState<boolean>(false);
|
||||
|
||||
const openPrivacyNotice = useCallback(() => {
|
||||
setShowPrivacyNotice(true);
|
||||
}, []);
|
||||
|
||||
const errorCount = useMemo(
|
||||
() => consoleMessages.filter((msg) => msg.type === 'error').length,
|
||||
@@ -168,7 +183,7 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
|
||||
addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: 'Refreshing hierarchical memory (GEMINI.md files)...',
|
||||
text: 'Refreshing hierarchical memory (GEMINI.md or other context files)...',
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
@@ -177,6 +192,7 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
|
||||
process.cwd(),
|
||||
config.getDebugMode(),
|
||||
config.getFileService(),
|
||||
config.getExtensionContextFilePaths(),
|
||||
);
|
||||
config.setUserMemory(memoryContent);
|
||||
config.setGeminiMdFileCount(fileCount);
|
||||
@@ -207,12 +223,52 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
|
||||
}
|
||||
}, [config, addItem]);
|
||||
|
||||
// Watch for model changes (e.g., from Flash fallback)
|
||||
useEffect(() => {
|
||||
const checkModelChange = () => {
|
||||
const configModel = config.getModel();
|
||||
if (configModel !== currentModel) {
|
||||
setCurrentModel(configModel);
|
||||
}
|
||||
};
|
||||
|
||||
// Check immediately and then periodically
|
||||
checkModelChange();
|
||||
const interval = setInterval(checkModelChange, 1000); // Check every second
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [config, currentModel]);
|
||||
|
||||
// Set up Flash fallback handler
|
||||
useEffect(() => {
|
||||
const flashFallbackHandler = async (
|
||||
currentModel: string,
|
||||
fallbackModel: string,
|
||||
): Promise<boolean> => {
|
||||
// Add message to UI history
|
||||
addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: `⚡ Slow response times detected. Automatically switching from ${currentModel} to ${fallbackModel} for faster responses for the remainder of this session.
|
||||
⚡ To avoid this you can either upgrade to Standard tier. See: https://goo.gle/set-up-gemini-code-assist
|
||||
⚡ Or you can utilize a Gemini API Key. See: https://goo.gle/gemini-cli-docs-auth#gemini-api-key
|
||||
⚡ You can switch authentication methods by typing /auth`,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
return true; // Always accept the fallback
|
||||
};
|
||||
|
||||
config.setFlashFallbackHandler(flashFallbackHandler);
|
||||
}, [config, addItem]);
|
||||
|
||||
const {
|
||||
handleSlashCommand,
|
||||
slashCommands,
|
||||
pendingHistoryItems: pendingSlashCommandHistoryItems,
|
||||
} = useSlashCommandProcessor(
|
||||
config,
|
||||
settings,
|
||||
history,
|
||||
addItem,
|
||||
clearItems,
|
||||
@@ -227,10 +283,12 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
|
||||
toggleCorgiMode,
|
||||
showToolDescriptions,
|
||||
setQuittingMessages,
|
||||
openPrivacyNotice,
|
||||
);
|
||||
const pendingHistoryItems = [...pendingSlashCommandHistoryItems];
|
||||
|
||||
const { rows: terminalHeight, columns: terminalWidth } = useTerminalSize();
|
||||
const isInitialMount = useRef(true);
|
||||
const { stdin, setRawMode } = useStdin();
|
||||
const isValidPath = useCallback((filePath: string): boolean => {
|
||||
try {
|
||||
@@ -285,13 +343,33 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
|
||||
);
|
||||
|
||||
useInput((input: string, key: InkKeyType) => {
|
||||
let enteringConstrainHeightMode = false;
|
||||
if (!constrainHeight) {
|
||||
// Automatically re-enter constrain height mode if the user types
|
||||
// anything. When constrainHeight==false, the user will experience
|
||||
// significant flickering so it is best to disable it immediately when
|
||||
// the user starts interacting with the app.
|
||||
enteringConstrainHeightMode = true;
|
||||
setConstrainHeight(true);
|
||||
|
||||
// If our pending history item happens to exceed the terminal height we will most likely need to refresh
|
||||
// our static collection to ensure no duplication or tearing. This is currently working around a core bug
|
||||
// in Ink which we have a PR out to fix: https://github.com/vadimdemedes/ink/pull/717
|
||||
if (pendingHistoryItemRef.current && pendingHistoryItems.length > 0) {
|
||||
const pendingItemDimensions = measureElement(
|
||||
pendingHistoryItemRef.current,
|
||||
);
|
||||
if (pendingItemDimensions.height > availableTerminalHeight) {
|
||||
refreshStatic();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (key.ctrl && input === 'o') {
|
||||
setShowErrorDetails((prev) => !prev);
|
||||
refreshStatic();
|
||||
} else if (key.ctrl && input === 't') {
|
||||
const newValue = !showToolDescriptions;
|
||||
setShowToolDescriptions(newValue);
|
||||
refreshStatic();
|
||||
|
||||
const mcpServers = config.getMcpServers();
|
||||
if (Object.keys(mcpServers || {}).length > 0) {
|
||||
@@ -305,8 +383,8 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
|
||||
return;
|
||||
}
|
||||
handleExit(ctrlDPressedOnce, setCtrlDPressedOnce, ctrlDTimerRef);
|
||||
} else if (key.ctrl && input === 's') {
|
||||
setConstrainHeight((prev) => !prev);
|
||||
} else if (key.ctrl && input === 's' && !enteringConstrainHeightMode) {
|
||||
setConstrainHeight(false);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -353,6 +431,7 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
|
||||
shellModeActive,
|
||||
getPreferredEditor,
|
||||
onAuthError,
|
||||
performMemoryRefresh,
|
||||
);
|
||||
pendingHistoryItems.push(...pendingGeminiHistoryItems);
|
||||
const { elapsedTime, currentLoadingPhrase } =
|
||||
@@ -433,6 +512,24 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
|
||||
[terminalHeight, footerHeight],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// skip refreshing Static during first mount
|
||||
if (isInitialMount.current) {
|
||||
isInitialMount.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// debounce so it doesn't fire up too often during resize
|
||||
const handler = setTimeout(() => {
|
||||
setStaticNeedsRefresh(false);
|
||||
refreshStatic();
|
||||
}, 300);
|
||||
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
}, [terminalWidth, terminalHeight, refreshStatic]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pendingHistoryItems.length) {
|
||||
return;
|
||||
@@ -493,7 +590,7 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
|
||||
);
|
||||
}
|
||||
const mainAreaWidth = Math.floor(terminalWidth * 0.9);
|
||||
const debugConsoleMaxHeight = Math.max(terminalHeight * 0.2, 5);
|
||||
const debugConsoleMaxHeight = Math.floor(Math.max(terminalHeight * 0.2, 5));
|
||||
// Arbitrary threshold to ensure that items in the static area are large
|
||||
// enough but not too large to make the terminal hard to use.
|
||||
const staticAreaMaxItemHeight = Math.max(terminalHeight * 4, 100);
|
||||
@@ -533,23 +630,27 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
|
||||
>
|
||||
{(item) => item}
|
||||
</Static>
|
||||
<Box ref={pendingHistoryItemRef}>
|
||||
{pendingHistoryItems.map((item, i) => (
|
||||
<HistoryItemDisplay
|
||||
key={i}
|
||||
availableTerminalHeight={
|
||||
constrainHeight ? availableTerminalHeight : undefined
|
||||
}
|
||||
terminalWidth={mainAreaWidth}
|
||||
// TODO(taehykim): It seems like references to ids aren't necessary in
|
||||
// HistoryItemDisplay. Refactor later. Use a fake id for now.
|
||||
item={{ ...item, id: 0 }}
|
||||
isPending={true}
|
||||
config={config}
|
||||
isFocused={!isEditorDialogOpen}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
<OverflowProvider>
|
||||
<Box ref={pendingHistoryItemRef} flexDirection="column">
|
||||
{pendingHistoryItems.map((item, i) => (
|
||||
<HistoryItemDisplay
|
||||
key={i}
|
||||
availableTerminalHeight={
|
||||
constrainHeight ? availableTerminalHeight : undefined
|
||||
}
|
||||
terminalWidth={mainAreaWidth}
|
||||
// TODO(taehykim): It seems like references to ids aren't necessary in
|
||||
// HistoryItemDisplay. Refactor later. Use a fake id for now.
|
||||
item={{ ...item, id: 0 }}
|
||||
isPending={true}
|
||||
config={config}
|
||||
isFocused={!isEditorDialogOpen}
|
||||
/>
|
||||
))}
|
||||
<ShowMoreLines constrainHeight={constrainHeight} />
|
||||
</Box>
|
||||
</OverflowProvider>
|
||||
|
||||
{showHelp && <Help commands={slashCommands} />}
|
||||
|
||||
<Box flexDirection="column" ref={mainControlsRef}>
|
||||
@@ -618,6 +719,11 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
|
||||
onExit={exitEditorDialog}
|
||||
/>
|
||||
</Box>
|
||||
) : showPrivacyNotice ? (
|
||||
<PrivacyNotice
|
||||
onExit={() => setShowPrivacyNotice(false)}
|
||||
config={config}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<LoadingIndicator
|
||||
@@ -673,13 +779,16 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
|
||||
</Box>
|
||||
|
||||
{showErrorDetails && (
|
||||
<DetailedMessagesDisplay
|
||||
messages={filteredConsoleMessages}
|
||||
maxHeight={
|
||||
constrainHeight ? debugConsoleMaxHeight : undefined
|
||||
}
|
||||
width={inputWidth}
|
||||
/>
|
||||
<OverflowProvider>
|
||||
<DetailedMessagesDisplay
|
||||
messages={filteredConsoleMessages}
|
||||
maxHeight={
|
||||
constrainHeight ? debugConsoleMaxHeight : undefined
|
||||
}
|
||||
width={inputWidth}
|
||||
/>
|
||||
<ShowMoreLines constrainHeight={constrainHeight} />
|
||||
</OverflowProvider>
|
||||
)}
|
||||
|
||||
{isInputActive && (
|
||||
@@ -732,7 +841,7 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
|
||||
</Box>
|
||||
)}
|
||||
<Footer
|
||||
model={config.getModel()}
|
||||
model={currentModel}
|
||||
targetDir={config.getTargetDir()}
|
||||
debugMode={config.getDebugMode()}
|
||||
branchName={branchName}
|
||||
|
||||
@@ -14,6 +14,8 @@ interface AboutBoxProps {
|
||||
osVersion: string;
|
||||
sandboxEnv: string;
|
||||
modelVersion: string;
|
||||
selectedAuthType: string;
|
||||
gcpProject: string;
|
||||
}
|
||||
|
||||
export const AboutBox: React.FC<AboutBoxProps> = ({
|
||||
@@ -21,6 +23,8 @@ export const AboutBox: React.FC<AboutBoxProps> = ({
|
||||
osVersion,
|
||||
sandboxEnv,
|
||||
modelVersion,
|
||||
selectedAuthType,
|
||||
gcpProject,
|
||||
}) => (
|
||||
<Box
|
||||
borderStyle="round"
|
||||
@@ -87,5 +91,29 @@ export const AboutBox: React.FC<AboutBoxProps> = ({
|
||||
<Text>{osVersion}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box flexDirection="row">
|
||||
<Box width="35%">
|
||||
<Text bold color={Colors.LightBlue}>
|
||||
Auth Method
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>
|
||||
{selectedAuthType.startsWith('oauth') ? 'OAuth' : selectedAuthType}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
{gcpProject && (
|
||||
<Box flexDirection="row">
|
||||
<Box width="35%">
|
||||
<Text bold color={Colors.LightBlue}>
|
||||
GCP Project
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>{gcpProject}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -5,11 +5,14 @@
|
||||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { AuthDialog } from './AuthDialog.js';
|
||||
import { LoadedSettings } from '../../config/settings.js';
|
||||
import { AuthType } from '@gemini-cli/core';
|
||||
import { LoadedSettings, SettingScope } from '../../config/settings.js';
|
||||
import { AuthType } from '@google/gemini-cli-core';
|
||||
|
||||
describe('AuthDialog', () => {
|
||||
const wait = (ms = 50) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
it('should show an error if the initial auth type is invalid', () => {
|
||||
const settings: LoadedSettings = new LoadedSettings(
|
||||
{
|
||||
@@ -38,4 +41,75 @@ describe('AuthDialog', () => {
|
||||
'GEMINI_API_KEY environment variable not found',
|
||||
);
|
||||
});
|
||||
|
||||
it('should prevent exiting when no auth method is selected and show error message', async () => {
|
||||
const onSelect = vi.fn();
|
||||
const settings: LoadedSettings = new LoadedSettings(
|
||||
{
|
||||
settings: {
|
||||
selectedAuthType: undefined,
|
||||
},
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: {},
|
||||
path: '',
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const { lastFrame, stdin, unmount } = render(
|
||||
<AuthDialog
|
||||
onSelect={onSelect}
|
||||
onHighlight={() => {}}
|
||||
settings={settings}
|
||||
/>,
|
||||
);
|
||||
await wait();
|
||||
|
||||
// Simulate pressing escape key
|
||||
stdin.write('\u001b'); // ESC key
|
||||
await wait();
|
||||
|
||||
// Should show error message instead of calling onSelect
|
||||
expect(lastFrame()).toContain(
|
||||
'You must select an auth method to proceed. Press Ctrl+C twice to exit.',
|
||||
);
|
||||
expect(onSelect).not.toHaveBeenCalled();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should allow exiting when auth method is already selected', async () => {
|
||||
const onSelect = vi.fn();
|
||||
const settings: LoadedSettings = new LoadedSettings(
|
||||
{
|
||||
settings: {
|
||||
selectedAuthType: AuthType.USE_GEMINI,
|
||||
},
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
settings: {},
|
||||
path: '',
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const { stdin, unmount } = render(
|
||||
<AuthDialog
|
||||
onSelect={onSelect}
|
||||
onHighlight={() => {}}
|
||||
settings={settings}
|
||||
/>,
|
||||
);
|
||||
await wait();
|
||||
|
||||
// Simulate pressing escape key
|
||||
stdin.write('\u001b'); // ESC key
|
||||
await wait();
|
||||
|
||||
// Should call onSelect with undefined to exit
|
||||
expect(onSelect).toHaveBeenCalledWith(undefined, SettingScope.User);
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Box, Text, useInput } from 'ink';
|
||||
import { Colors } from '../colors.js';
|
||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||
import { LoadedSettings, SettingScope } from '../../config/settings.js';
|
||||
import { AuthType } from '@gemini-cli/core';
|
||||
import { AuthType } from '@google/gemini-cli-core';
|
||||
import { validateAuthMethod } from '../../config/auth.js';
|
||||
|
||||
interface AuthDialogProps {
|
||||
@@ -28,32 +28,15 @@ export function AuthDialog({
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(
|
||||
initialErrorMessage || null,
|
||||
);
|
||||
const allAuthItems = [
|
||||
const items = [
|
||||
{
|
||||
label: 'Login with Google',
|
||||
value: AuthType.LOGIN_WITH_GOOGLE_PERSONAL,
|
||||
},
|
||||
{ label: 'Gemini API Key', value: AuthType.USE_GEMINI },
|
||||
{
|
||||
label: 'Login with Google Work',
|
||||
value: AuthType.LOGIN_WITH_GOOGLE_ENTERPRISE,
|
||||
},
|
||||
{ label: 'Vertex API Key', value: AuthType.USE_VERTEX_AI },
|
||||
{ label: 'Gemini API Key (AI Studio)', value: AuthType.USE_GEMINI },
|
||||
{ label: 'Vertex AI', value: AuthType.USE_VERTEX_AI },
|
||||
];
|
||||
|
||||
const isSelectedAuthInMore = allAuthItems
|
||||
.slice(2)
|
||||
.some((item) => item.value === settings.merged.selectedAuthType);
|
||||
|
||||
const [showAll, setShowAll] = useState(isSelectedAuthInMore);
|
||||
|
||||
const initialAuthItems = [
|
||||
...allAuthItems.slice(0, 2),
|
||||
{ label: 'More...', value: 'more' },
|
||||
];
|
||||
|
||||
const items = showAll ? allAuthItems : initialAuthItems;
|
||||
|
||||
let initialAuthIndex = items.findIndex(
|
||||
(item) => item.value === settings.merged.selectedAuthType,
|
||||
);
|
||||
@@ -63,10 +46,6 @@ export function AuthDialog({
|
||||
}
|
||||
|
||||
const handleAuthSelect = (authMethod: string) => {
|
||||
if (authMethod === 'more') {
|
||||
setShowAll(true);
|
||||
return;
|
||||
}
|
||||
const error = validateAuthMethod(authMethod);
|
||||
if (error) {
|
||||
setErrorMessage(error);
|
||||
@@ -78,6 +57,13 @@ export function AuthDialog({
|
||||
|
||||
useInput((_input, key) => {
|
||||
if (key.escape) {
|
||||
if (settings.merged.selectedAuthType === undefined) {
|
||||
// Prevent exiting if no auth method is set
|
||||
setErrorMessage(
|
||||
'You must select an auth method to proceed. Press Ctrl+C twice to exit.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
onSelect(undefined, SettingScope.User);
|
||||
}
|
||||
});
|
||||
@@ -106,6 +92,16 @@ export function AuthDialog({
|
||||
<Box marginTop={1}>
|
||||
<Text color={Colors.Gray}>(Use Enter to select)</Text>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text>Terms of Services and Privacy Notice for Gemini CLI</Text>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text color={Colors.AccentBlue}>
|
||||
{
|
||||
'https://github.com/google-gemini/gemini-cli/blob/main/docs/tos-privacy.md'
|
||||
}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { Colors } from '../colors.js';
|
||||
import { ApprovalMode } from '@gemini-cli/core';
|
||||
import { ApprovalMode } from '@google/gemini-cli-core';
|
||||
|
||||
interface AutoAcceptIndicatorProps {
|
||||
approvalMode: ApprovalMode;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import React from 'react';
|
||||
import { Text } from 'ink';
|
||||
import { Colors } from '../colors.js';
|
||||
import { type MCPServerConfig } from '@gemini-cli/core';
|
||||
import { type MCPServerConfig } from '@google/gemini-cli-core';
|
||||
|
||||
interface ContextSummaryDisplayProps {
|
||||
geminiMdFileCount: number;
|
||||
@@ -28,12 +28,16 @@ export const ContextSummaryDisplay: React.FC<ContextSummaryDisplayProps> = ({
|
||||
return <Text> </Text>; // Render an empty space to reserve height
|
||||
}
|
||||
|
||||
const geminiMdText =
|
||||
geminiMdFileCount > 0
|
||||
? `${geminiMdFileCount} ${contextFileNames[0]} file${
|
||||
geminiMdFileCount > 1 ? 's' : ''
|
||||
}`
|
||||
: '';
|
||||
const geminiMdText = (() => {
|
||||
if (geminiMdFileCount === 0) {
|
||||
return '';
|
||||
}
|
||||
const allNamesTheSame = new Set(contextFileNames).size < 2;
|
||||
const name = allNamesTheSame ? contextFileNames[0] : 'context';
|
||||
return `${geminiMdFileCount} ${name} file${
|
||||
geminiMdFileCount > 1 ? 's' : ''
|
||||
}`;
|
||||
})();
|
||||
|
||||
const mcpText =
|
||||
mcpServerCount > 0
|
||||
|
||||
@@ -37,7 +37,7 @@ export const DetailedMessagesDisplay: React.FC<
|
||||
>
|
||||
<Box marginBottom={1}>
|
||||
<Text bold color={Colors.Foreground}>
|
||||
Debug Console <Text color={Colors.Gray}>(ctrl+O to close)</Text>
|
||||
Debug Console <Text color={Colors.Gray}>(ctrl+o to close)</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
<MaxSizedBox maxHeight={maxHeight} maxWidth={width - borderAndPadding}>
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from '../editors/editorSettingsManager.js';
|
||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||
import { LoadedSettings, SettingScope } from '../../config/settings.js';
|
||||
import { EditorType, isEditorAvailable } from '@gemini-cli/core';
|
||||
import { EditorType, isEditorAvailable } from '@google/gemini-cli-core';
|
||||
|
||||
interface EditorDialogProps {
|
||||
onSelect: (editorType: EditorType | undefined, scope: SettingScope) => void;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { Colors } from '../colors.js';
|
||||
import { shortenPath, tildeifyPath, tokenLimit } from '@gemini-cli/core';
|
||||
import { shortenPath, tildeifyPath, tokenLimit } from '@google/gemini-cli-core';
|
||||
import { ConsoleSummaryDisplay } from './ConsoleSummaryDisplay.js';
|
||||
import process from 'node:process';
|
||||
import { MemoryUsageDisplay } from './MemoryUsageDisplay.js';
|
||||
@@ -74,7 +74,7 @@ export const Footer: React.FC<FooterProps> = ({
|
||||
</Text>
|
||||
) : (
|
||||
<Text color={Colors.AccentRed}>
|
||||
no sandbox <Text color={Colors.Gray}>(see docs)</Text>
|
||||
no sandbox <Text color={Colors.Gray}>(see /docs)</Text>
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -30,17 +30,22 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
terminalWidth >= widthOfLongLogo ? longAsciiLogo : shortAsciiLogo;
|
||||
}
|
||||
|
||||
const artWidth = getAsciiArtWidth(displayTitle);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box marginBottom={1} alignItems="flex-start">
|
||||
{Colors.GradientColors ? (
|
||||
<Gradient colors={Colors.GradientColors}>
|
||||
<Text>{displayTitle}</Text>
|
||||
</Gradient>
|
||||
) : (
|
||||
<Box
|
||||
marginBottom={1}
|
||||
alignItems="flex-start"
|
||||
width={artWidth}
|
||||
flexShrink={0}
|
||||
>
|
||||
{Colors.GradientColors ? (
|
||||
<Gradient colors={Colors.GradientColors}>
|
||||
<Text>{displayTitle}</Text>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
</Gradient>
|
||||
) : (
|
||||
<Text>{displayTitle}</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -67,6 +67,8 @@ describe('<HistoryItemDisplay />', () => {
|
||||
osVersion: 'test-os',
|
||||
sandboxEnv: 'test-env',
|
||||
modelVersion: 'test-model',
|
||||
selectedAuthType: 'test-auth',
|
||||
gcpProject: 'test-project',
|
||||
};
|
||||
const { lastFrame } = render(
|
||||
<HistoryItemDisplay {...baseItem} item={item} />,
|
||||
|
||||
@@ -18,7 +18,7 @@ import { Box } from 'ink';
|
||||
import { AboutBox } from './AboutBox.js';
|
||||
import { StatsDisplay } from './StatsDisplay.js';
|
||||
import { SessionSummaryDisplay } from './SessionSummaryDisplay.js';
|
||||
import { Config } from '@gemini-cli/core';
|
||||
import { Config } from '@google/gemini-cli-core';
|
||||
|
||||
interface HistoryItemDisplayProps {
|
||||
item: HistoryItem;
|
||||
@@ -65,6 +65,8 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
osVersion={item.osVersion}
|
||||
sandboxEnv={item.sandboxEnv}
|
||||
modelVersion={item.modelVersion}
|
||||
selectedAuthType={item.selectedAuthType}
|
||||
gcpProject={item.gcpProject}
|
||||
/>
|
||||
)}
|
||||
{item.type === 'stats' && (
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { render } from 'ink-testing-library';
|
||||
import { InputPrompt, InputPromptProps } from './InputPrompt.js';
|
||||
import type { TextBuffer } from './shared/text-buffer.js';
|
||||
import { Config } from '@gemini-cli/core';
|
||||
import { Config } from '@google/gemini-cli-core';
|
||||
import { vi } from 'vitest';
|
||||
import { useShellHistory } from '../hooks/useShellHistory.js';
|
||||
import { useCompletion } from '../hooks/useCompletion.js';
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Text, Box, useInput } from 'ink';
|
||||
import { Box, Text } from 'ink';
|
||||
import { Colors } from '../colors.js';
|
||||
import { SuggestionsDisplay } from './SuggestionsDisplay.js';
|
||||
import { useInputHistory } from '../hooks/useInputHistory.js';
|
||||
@@ -16,9 +16,10 @@ import stringWidth from 'string-width';
|
||||
import process from 'node:process';
|
||||
import { useShellHistory } from '../hooks/useShellHistory.js';
|
||||
import { useCompletion } from '../hooks/useCompletion.js';
|
||||
import { useKeypress, Key } from '../hooks/useKeypress.js';
|
||||
import { isAtCommand, isSlashCommand } from '../utils/commandUtils.js';
|
||||
import { SlashCommand } from '../hooks/slashCommandProcessor.js';
|
||||
import { Config } from '@gemini-cli/core';
|
||||
import { Config } from '@google/gemini-cli-core';
|
||||
|
||||
export interface InputPromptProps {
|
||||
buffer: TextBuffer;
|
||||
@@ -155,29 +156,29 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
],
|
||||
);
|
||||
|
||||
useInput(
|
||||
(input, key) => {
|
||||
const handleInput = useCallback(
|
||||
(key: Key) => {
|
||||
if (!focus) {
|
||||
return;
|
||||
}
|
||||
const query = buffer.text;
|
||||
|
||||
if (input === '!' && query === '' && !completion.showSuggestions) {
|
||||
if (key.sequence === '!' && query === '' && !completion.showSuggestions) {
|
||||
setShellModeActive(!shellModeActive);
|
||||
buffer.setText(''); // Clear the '!' from input
|
||||
return true;
|
||||
}
|
||||
|
||||
if (completion.showSuggestions) {
|
||||
if (key.upArrow) {
|
||||
if (key.name === 'up') {
|
||||
completion.navigateUp();
|
||||
return;
|
||||
}
|
||||
if (key.downArrow) {
|
||||
if (key.name === 'down') {
|
||||
completion.navigateDown();
|
||||
return;
|
||||
}
|
||||
if (key.tab) {
|
||||
if (key.name === 'tab') {
|
||||
if (completion.suggestions.length > 0) {
|
||||
const targetIndex =
|
||||
completion.activeSuggestionIndex === -1
|
||||
@@ -189,7 +190,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (key.return) {
|
||||
if (key.name === 'return') {
|
||||
if (completion.activeSuggestionIndex >= 0) {
|
||||
handleAutocomplete(completion.activeSuggestionIndex);
|
||||
} else if (query.trim()) {
|
||||
@@ -199,19 +200,19 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
}
|
||||
} else {
|
||||
// Keybindings when suggestions are not shown
|
||||
if (key.ctrl && input === 'l') {
|
||||
if (key.ctrl && key.name === 'l') {
|
||||
onClearScreen();
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
if (key.ctrl && input === 'p') {
|
||||
if (key.ctrl && key.name === 'p') {
|
||||
inputHistory.navigateUp();
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
if (key.ctrl && input === 'n') {
|
||||
if (key.ctrl && key.name === 'n') {
|
||||
inputHistory.navigateDown();
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
if (key.escape) {
|
||||
if (key.name === 'escape') {
|
||||
if (shellModeActive) {
|
||||
setShellModeActive(false);
|
||||
return;
|
||||
@@ -222,54 +223,55 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
}
|
||||
|
||||
// Ctrl+A (Home)
|
||||
if (key.ctrl && input === 'a') {
|
||||
if (key.ctrl && key.name === 'a') {
|
||||
buffer.move('home');
|
||||
buffer.moveToOffset(0);
|
||||
return;
|
||||
}
|
||||
// Ctrl+E (End)
|
||||
if (key.ctrl && input === 'e') {
|
||||
if (key.ctrl && key.name === 'e') {
|
||||
buffer.move('end');
|
||||
buffer.moveToOffset(cpLen(buffer.text));
|
||||
return;
|
||||
}
|
||||
// Ctrl+L (Clear Screen)
|
||||
if (key.ctrl && input === 'l') {
|
||||
if (key.ctrl && key.name === 'l') {
|
||||
onClearScreen();
|
||||
return;
|
||||
}
|
||||
// Ctrl+P (History Up)
|
||||
if (key.ctrl && input === 'p' && !completion.showSuggestions) {
|
||||
if (key.ctrl && key.name === 'p' && !completion.showSuggestions) {
|
||||
inputHistory.navigateUp();
|
||||
return;
|
||||
}
|
||||
// Ctrl+N (History Down)
|
||||
if (key.ctrl && input === 'n' && !completion.showSuggestions) {
|
||||
if (key.ctrl && key.name === 'n' && !completion.showSuggestions) {
|
||||
inputHistory.navigateDown();
|
||||
return;
|
||||
}
|
||||
|
||||
// Core text editing from MultilineTextEditor's useInput
|
||||
if (key.ctrl && input === 'k') {
|
||||
if (key.ctrl && key.name === 'k') {
|
||||
buffer.killLineRight();
|
||||
return;
|
||||
}
|
||||
if (key.ctrl && input === 'u') {
|
||||
if (key.ctrl && key.name === 'u') {
|
||||
buffer.killLineLeft();
|
||||
return;
|
||||
}
|
||||
const isCtrlX =
|
||||
(key.ctrl && (input === 'x' || input === '\x18')) || input === '\x18';
|
||||
(key.ctrl && (key.name === 'x' || key.sequence === '\x18')) ||
|
||||
key.sequence === '\x18';
|
||||
const isCtrlEFromEditor =
|
||||
(key.ctrl && (input === 'e' || input === '\x05')) ||
|
||||
input === '\x05' ||
|
||||
(key.ctrl && (key.name === 'e' || key.sequence === '\x05')) ||
|
||||
key.sequence === '\x05' ||
|
||||
(!key.ctrl &&
|
||||
input === 'e' &&
|
||||
input.length === 1 &&
|
||||
input.charCodeAt(0) === 5);
|
||||
key.name === 'e' &&
|
||||
key.sequence.length === 1 &&
|
||||
key.sequence.charCodeAt(0) === 5);
|
||||
|
||||
if (isCtrlX || isCtrlEFromEditor) {
|
||||
if (isCtrlEFromEditor && !(key.ctrl && input === 'e')) {
|
||||
if (isCtrlEFromEditor && !(key.ctrl && key.name === 'e')) {
|
||||
// Avoid double handling Ctrl+E
|
||||
buffer.openInExternalEditor();
|
||||
return;
|
||||
@@ -284,16 +286,15 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
process.env['TEXTBUFFER_DEBUG'] === '1' ||
|
||||
process.env['TEXTBUFFER_DEBUG'] === 'true'
|
||||
) {
|
||||
console.log('[InputPromptCombined] event', { input, key });
|
||||
console.log('[InputPromptCombined] event', { key });
|
||||
}
|
||||
|
||||
// Ctrl+Enter for newline, Enter for submit
|
||||
if (key.return) {
|
||||
if (key.name === 'return') {
|
||||
const [row, col] = buffer.cursor;
|
||||
const line = buffer.lines[row];
|
||||
const charBefore = col > 0 ? cpSlice(line, col - 1, col) : '';
|
||||
|
||||
if (key.ctrl || charBefore === '\\') {
|
||||
if (key.ctrl || key.meta || charBefore === '\\' || key.paste) {
|
||||
// Ctrl+Enter or escaped newline
|
||||
if (charBefore === '\\') {
|
||||
buffer.backspace();
|
||||
@@ -309,7 +310,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
}
|
||||
|
||||
// Standard arrow navigation within the buffer
|
||||
if (key.upArrow && !completion.showSuggestions) {
|
||||
if (key.name === 'up' && !completion.showSuggestions) {
|
||||
if (shellModeActive) {
|
||||
const prevCommand = shellHistory.getPreviousCommand();
|
||||
if (prevCommand !== null) {
|
||||
@@ -328,7 +329,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (key.downArrow && !completion.showSuggestions) {
|
||||
if (key.name === 'down' && !completion.showSuggestions) {
|
||||
if (shellModeActive) {
|
||||
const nextCommand = shellHistory.getNextCommand();
|
||||
if (nextCommand !== null) {
|
||||
@@ -349,13 +350,24 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
}
|
||||
|
||||
// Fallback to buffer's default input handling
|
||||
buffer.handleInput(input, key as Record<string, boolean>);
|
||||
},
|
||||
{
|
||||
isActive: focus,
|
||||
buffer.handleInput(key);
|
||||
},
|
||||
[
|
||||
focus,
|
||||
buffer,
|
||||
completion,
|
||||
shellModeActive,
|
||||
setShellModeActive,
|
||||
onClearScreen,
|
||||
inputHistory,
|
||||
handleAutocomplete,
|
||||
handleSubmitAndClear,
|
||||
shellHistory,
|
||||
],
|
||||
);
|
||||
|
||||
useKeypress(handleInput, { isActive: focus });
|
||||
|
||||
const linesToRender = buffer.viewportVisualLines;
|
||||
const [cursorVisualRowAbsolute, cursorVisualColAbsolute] =
|
||||
buffer.visualCursor;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { ThoughtSummary } from '@gemini-cli/core';
|
||||
import { ThoughtSummary } from '@google/gemini-cli-core';
|
||||
import React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { Colors } from '../colors.js';
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Box, Text } from 'ink';
|
||||
import { useOverflowState } from '../contexts/OverflowContext.js';
|
||||
import { useStreamingContext } from '../contexts/StreamingContext.js';
|
||||
import { StreamingState } from '../types.js';
|
||||
import { Colors } from '../colors.js';
|
||||
|
||||
interface ShowMoreLinesProps {
|
||||
constrainHeight: boolean;
|
||||
}
|
||||
|
||||
export const ShowMoreLines = ({ constrainHeight }: ShowMoreLinesProps) => {
|
||||
const overflowState = useOverflowState();
|
||||
const streamingState = useStreamingContext();
|
||||
|
||||
if (
|
||||
overflowState === undefined ||
|
||||
overflowState.overflowingIds.size === 0 ||
|
||||
!constrainHeight ||
|
||||
!(
|
||||
streamingState === StreamingState.Idle ||
|
||||
streamingState === StreamingState.WaitingForConfirmation
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Text color={Colors.Gray} wrap="truncate">
|
||||
Press ctrl-s to show more lines
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -76,11 +76,13 @@ export const StatsColumn: React.FC<{
|
||||
label="Thoughts Tokens"
|
||||
value={stats.thoughtsTokens.toLocaleString()}
|
||||
/>
|
||||
<StatRow
|
||||
label="Cached Tokens"
|
||||
value={cachedDisplay}
|
||||
valueColor={cachedColor}
|
||||
/>
|
||||
{stats.cachedTokens > 0 && (
|
||||
<StatRow
|
||||
label="Cached Tokens"
|
||||
value={cachedDisplay}
|
||||
valueColor={cachedColor}
|
||||
/>
|
||||
)}
|
||||
{/* Divider Line */}
|
||||
<Box
|
||||
borderTop={true}
|
||||
|
||||
@@ -115,94 +115,143 @@ export function ThemeDialog({
|
||||
1,
|
||||
);
|
||||
|
||||
const DAILOG_PADDING = 2;
|
||||
const selectThemeHeight = themeItems.length + 1;
|
||||
const SCOPE_SELECTION_HEIGHT = 4; // Height for the scope selection section + margin.
|
||||
const SPACE_BETWEEN_THEME_SELECTION_AND_APPLY_TO = 1;
|
||||
const TAB_TO_SELECT_HEIGHT = 2;
|
||||
availableTerminalHeight = availableTerminalHeight ?? Number.MAX_SAFE_INTEGER;
|
||||
availableTerminalHeight -= 2; // Top and bottom borders.
|
||||
availableTerminalHeight -= TAB_TO_SELECT_HEIGHT;
|
||||
|
||||
let totalLeftHandSideHeight =
|
||||
DAILOG_PADDING +
|
||||
selectThemeHeight +
|
||||
SCOPE_SELECTION_HEIGHT +
|
||||
SPACE_BETWEEN_THEME_SELECTION_AND_APPLY_TO;
|
||||
|
||||
let showScopeSelection = true;
|
||||
let includePadding = true;
|
||||
|
||||
// Remove content from the LHS that can be omitted if it exceeds the available height.
|
||||
if (totalLeftHandSideHeight > availableTerminalHeight) {
|
||||
includePadding = false;
|
||||
totalLeftHandSideHeight -= DAILOG_PADDING;
|
||||
}
|
||||
|
||||
if (totalLeftHandSideHeight > availableTerminalHeight) {
|
||||
// First, try hiding the scope selection
|
||||
totalLeftHandSideHeight -= SCOPE_SELECTION_HEIGHT;
|
||||
showScopeSelection = false;
|
||||
}
|
||||
|
||||
// Don't focus the scope selection if it is hidden due to height constraints.
|
||||
const currenFocusedSection = !showScopeSelection ? 'theme' : focusedSection;
|
||||
|
||||
// Vertical space taken by elements other than the two code blocks in the preview pane.
|
||||
// Includes "Preview" title, borders, padding, and margin between blocks.
|
||||
const PREVIEW_PANE_FIXED_VERTICAL_SPACE = 7;
|
||||
const availableTerminalHeightCodeBlock = availableTerminalHeight
|
||||
? Math.max(
|
||||
Math.floor(
|
||||
(availableTerminalHeight - PREVIEW_PANE_FIXED_VERTICAL_SPACE) / 2,
|
||||
),
|
||||
2,
|
||||
)
|
||||
: undefined;
|
||||
// Includes "Preview" title, borders, and margin between blocks.
|
||||
const PREVIEW_PANE_FIXED_VERTICAL_SPACE = 8;
|
||||
|
||||
// The right column doesn't need to ever be shorter than the left column.
|
||||
availableTerminalHeight = Math.max(
|
||||
availableTerminalHeight,
|
||||
totalLeftHandSideHeight,
|
||||
);
|
||||
const availableTerminalHeightCodeBlock =
|
||||
availableTerminalHeight -
|
||||
PREVIEW_PANE_FIXED_VERTICAL_SPACE -
|
||||
(includePadding ? 2 : 0) * 2;
|
||||
// Give slightly more space to the code block as it is 3 lines longer.
|
||||
const diffHeight = Math.floor(availableTerminalHeightCodeBlock / 2) - 1;
|
||||
const codeBlockHeight = Math.ceil(availableTerminalHeightCodeBlock / 2) + 1;
|
||||
|
||||
return (
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor={Colors.Gray}
|
||||
flexDirection="row"
|
||||
padding={1}
|
||||
flexDirection="column"
|
||||
paddingTop={includePadding ? 1 : 0}
|
||||
paddingBottom={includePadding ? 1 : 0}
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
width="100%"
|
||||
>
|
||||
{/* Left Column: Selection */}
|
||||
<Box flexDirection="column" width="45%" paddingRight={2}>
|
||||
<Text bold={focusedSection === 'theme'}>
|
||||
{focusedSection === 'theme' ? '> ' : ' '}Select Theme{' '}
|
||||
<Text color={Colors.Gray}>{otherScopeModifiedMessage}</Text>
|
||||
</Text>
|
||||
<RadioButtonSelect
|
||||
key={selectInputKey}
|
||||
items={themeItems}
|
||||
initialIndex={initialThemeIndex}
|
||||
onSelect={handleThemeSelect}
|
||||
onHighlight={onHighlight}
|
||||
isFocused={focusedSection === 'theme'}
|
||||
/>
|
||||
|
||||
{/* Scope Selection */}
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text bold={focusedSection === 'scope'}>
|
||||
{focusedSection === 'scope' ? '> ' : ' '}Apply To
|
||||
<Box flexDirection="row">
|
||||
{/* Left Column: Selection */}
|
||||
<Box flexDirection="column" width="45%" paddingRight={2}>
|
||||
<Text bold={currenFocusedSection === 'theme'} wrap="truncate">
|
||||
{currenFocusedSection === 'theme' ? '> ' : ' '}Select Theme{' '}
|
||||
<Text color={Colors.Gray}>{otherScopeModifiedMessage}</Text>
|
||||
</Text>
|
||||
<RadioButtonSelect
|
||||
items={scopeItems}
|
||||
initialIndex={0} // Default to User Settings
|
||||
onSelect={handleScopeSelect}
|
||||
onHighlight={handleScopeHighlight}
|
||||
isFocused={focusedSection === 'scope'}
|
||||
key={selectInputKey}
|
||||
items={themeItems}
|
||||
initialIndex={initialThemeIndex}
|
||||
onSelect={handleThemeSelect}
|
||||
onHighlight={onHighlight}
|
||||
isFocused={currenFocusedSection === 'theme'}
|
||||
/>
|
||||
|
||||
{/* Scope Selection */}
|
||||
{showScopeSelection && (
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text bold={currenFocusedSection === 'scope'} wrap="truncate">
|
||||
{currenFocusedSection === 'scope' ? '> ' : ' '}Apply To
|
||||
</Text>
|
||||
<RadioButtonSelect
|
||||
items={scopeItems}
|
||||
initialIndex={0} // Default to User Settings
|
||||
onSelect={handleScopeSelect}
|
||||
onHighlight={handleScopeHighlight}
|
||||
isFocused={currenFocusedSection === 'scope'}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text color={Colors.Gray}>
|
||||
(Use Enter to select, Tab to change focus)
|
||||
</Text>
|
||||
{/* Right Column: Preview */}
|
||||
<Box flexDirection="column" width="55%" paddingLeft={2}>
|
||||
<Text bold>Preview</Text>
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderColor={Colors.Gray}
|
||||
paddingTop={includePadding ? 1 : 0}
|
||||
paddingBottom={includePadding ? 1 : 0}
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
flexDirection="column"
|
||||
>
|
||||
{colorizeCode(
|
||||
`# function
|
||||
-def fibonacci(n):
|
||||
- a, b = 0, 1
|
||||
- for _ in range(n):
|
||||
- a, b = b, a + b
|
||||
- return a`,
|
||||
'python',
|
||||
codeBlockHeight,
|
||||
colorizeCodeWidth,
|
||||
)}
|
||||
<Box marginTop={1} />
|
||||
<DiffRenderer
|
||||
diffContent={`--- a/old_file.txt
|
||||
-+++ b/new_file.txt
|
||||
-@@ -1,4 +1,5 @@
|
||||
- This is a context line.
|
||||
--This line was deleted.
|
||||
-+This line was added.
|
||||
-`}
|
||||
availableTerminalHeight={diffHeight}
|
||||
terminalWidth={colorizeCodeWidth}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Right Column: Preview */}
|
||||
<Box flexDirection="column" width="55%" paddingLeft={2}>
|
||||
<Text bold>Preview</Text>
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderColor={Colors.Gray}
|
||||
padding={1}
|
||||
flexDirection="column"
|
||||
>
|
||||
{colorizeCode(
|
||||
`# function
|
||||
def fibonacci(n):
|
||||
a, b = 0, 1
|
||||
for _ in range(n):
|
||||
a, b = b, a + b
|
||||
return a`,
|
||||
'python',
|
||||
availableTerminalHeightCodeBlock,
|
||||
colorizeCodeWidth,
|
||||
)}
|
||||
<Box marginTop={1} />
|
||||
<DiffRenderer
|
||||
diffContent={`--- a/old_file.txt
|
||||
+++ b/new_file.txt
|
||||
@@ -1,4 +1,5 @@
|
||||
This is a context line.
|
||||
-This line was deleted.
|
||||
+This line was added.
|
||||
`}
|
||||
availableTerminalHeight={availableTerminalHeightCodeBlock}
|
||||
terminalWidth={colorizeCodeWidth}
|
||||
/>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text color={Colors.Gray} wrap="truncate">
|
||||
(Use Enter to select
|
||||
{showScopeSelection ? ', Tab to change focus' : ''})
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { Colors } from '../colors.js';
|
||||
import { type Config } from '@gemini-cli/core';
|
||||
import { type Config } from '@google/gemini-cli-core';
|
||||
|
||||
interface TipsProps {
|
||||
config: Config;
|
||||
@@ -19,27 +19,27 @@ export const Tips: React.FC<TipsProps> = ({ config }) => {
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text color={Colors.Foreground}>Tips for getting started:</Text>
|
||||
<Text color={Colors.Foreground}>
|
||||
1.{' '}
|
||||
<Text bold color={Colors.AccentPurple}>
|
||||
/help
|
||||
</Text>{' '}
|
||||
for more information.
|
||||
1. Ask questions, edit files, or run commands.
|
||||
</Text>
|
||||
<Text color={Colors.Foreground}>
|
||||
2. Ask coding questions, edit code or run commands.
|
||||
</Text>
|
||||
<Text color={Colors.Foreground}>
|
||||
3. Be specific for the best results.
|
||||
2. Be specific for the best results.
|
||||
</Text>
|
||||
{geminiMdFileCount === 0 && (
|
||||
<Text color={Colors.Foreground}>
|
||||
4. Create{' '}
|
||||
3. Create{' '}
|
||||
<Text bold color={Colors.AccentPurple}>
|
||||
GEMINI.md
|
||||
</Text>{' '}
|
||||
files to customize your interactions with Gemini.
|
||||
</Text>
|
||||
)}
|
||||
<Text color={Colors.Foreground}>
|
||||
{geminiMdFileCount === 0 ? '4.' : '3.'}{' '}
|
||||
<Text bold color={Colors.AccentPurple}>
|
||||
/help
|
||||
</Text>{' '}
|
||||
for more information.
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -33,7 +33,6 @@ exports[`<SessionSummaryDisplay /> > renders zero state correctly 1`] = `
|
||||
│ Input Tokens 0 │
|
||||
│ Output Tokens 0 │
|
||||
│ Thoughts Tokens 0 │
|
||||
│ Cached Tokens 0 │
|
||||
│ ────────────────────────── │
|
||||
│ Total Tokens 0 │
|
||||
│ │
|
||||
|
||||
@@ -31,7 +31,6 @@ exports[`<StatsDisplay /> > renders zero state correctly 1`] = `
|
||||
│ Input Tokens 0 Input Tokens 0 │
|
||||
│ Output Tokens 0 Output Tokens 0 │
|
||||
│ Thoughts Tokens 0 Thoughts Tokens 0 │
|
||||
│ Cached Tokens 0 Cached Tokens 0 │
|
||||
│ ───────────────────────────────────────────── ───────────────────────────────────────────── │
|
||||
│ Total Tokens 0 Total Tokens 0 │
|
||||
│ │
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { OverflowProvider } from '../../contexts/OverflowContext.js';
|
||||
import { render } from 'ink-testing-library';
|
||||
import { DiffRenderer } from './DiffRenderer.js';
|
||||
import * as CodeColorizer from '../../utils/CodeColorizer.js';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
describe('<DiffRenderer />', () => {
|
||||
describe('<OverflowProvider><DiffRenderer /></OverflowProvider>', () => {
|
||||
const mockColorizeCode = vi.spyOn(CodeColorizer, 'colorizeCode');
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -30,11 +31,13 @@ index 0000000..e69de29
|
||||
+print("hello world")
|
||||
`;
|
||||
render(
|
||||
<DiffRenderer
|
||||
diffContent={newFileDiffContent}
|
||||
filename="test.py"
|
||||
terminalWidth={80}
|
||||
/>,
|
||||
<OverflowProvider>
|
||||
<DiffRenderer
|
||||
diffContent={newFileDiffContent}
|
||||
filename="test.py"
|
||||
terminalWidth={80}
|
||||
/>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
expect(mockColorizeCode).toHaveBeenCalledWith(
|
||||
'print("hello world")',
|
||||
@@ -55,11 +58,13 @@ index 0000000..e69de29
|
||||
+some content
|
||||
`;
|
||||
render(
|
||||
<DiffRenderer
|
||||
diffContent={newFileDiffContent}
|
||||
filename="test.unknown"
|
||||
terminalWidth={80}
|
||||
/>,
|
||||
<OverflowProvider>
|
||||
<DiffRenderer
|
||||
diffContent={newFileDiffContent}
|
||||
filename="test.unknown"
|
||||
terminalWidth={80}
|
||||
/>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
expect(mockColorizeCode).toHaveBeenCalledWith(
|
||||
'some content',
|
||||
@@ -80,7 +85,9 @@ index 0000000..e69de29
|
||||
+some text content
|
||||
`;
|
||||
render(
|
||||
<DiffRenderer diffContent={newFileDiffContent} terminalWidth={80} />,
|
||||
<OverflowProvider>
|
||||
<DiffRenderer diffContent={newFileDiffContent} terminalWidth={80} />
|
||||
</OverflowProvider>,
|
||||
);
|
||||
expect(mockColorizeCode).toHaveBeenCalledWith(
|
||||
'some text content',
|
||||
@@ -101,11 +108,13 @@ index 0000001..0000002 100644
|
||||
+new line
|
||||
`;
|
||||
const { lastFrame } = render(
|
||||
<DiffRenderer
|
||||
diffContent={existingFileDiffContent}
|
||||
filename="test.txt"
|
||||
terminalWidth={80}
|
||||
/>,
|
||||
<OverflowProvider>
|
||||
<DiffRenderer
|
||||
diffContent={existingFileDiffContent}
|
||||
filename="test.txt"
|
||||
terminalWidth={80}
|
||||
/>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
// colorizeCode is used internally by the line-by-line rendering, not for the whole block
|
||||
expect(mockColorizeCode).not.toHaveBeenCalledWith(
|
||||
@@ -129,11 +138,13 @@ index 1234567..1234567 100644
|
||||
+++ b/file.txt
|
||||
`;
|
||||
const { lastFrame } = render(
|
||||
<DiffRenderer
|
||||
diffContent={noChangeDiff}
|
||||
filename="file.txt"
|
||||
terminalWidth={80}
|
||||
/>,
|
||||
<OverflowProvider>
|
||||
<DiffRenderer
|
||||
diffContent={noChangeDiff}
|
||||
filename="file.txt"
|
||||
terminalWidth={80}
|
||||
/>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
expect(lastFrame()).toContain('No changes detected');
|
||||
expect(mockColorizeCode).not.toHaveBeenCalled();
|
||||
@@ -141,7 +152,9 @@ index 1234567..1234567 100644
|
||||
|
||||
it('should handle empty diff content', () => {
|
||||
const { lastFrame } = render(
|
||||
<DiffRenderer diffContent="" terminalWidth={80} />,
|
||||
<OverflowProvider>
|
||||
<DiffRenderer diffContent="" terminalWidth={80} />
|
||||
</OverflowProvider>,
|
||||
);
|
||||
expect(lastFrame()).toContain('No diff content');
|
||||
expect(mockColorizeCode).not.toHaveBeenCalled();
|
||||
@@ -162,11 +175,13 @@ index 123..456 100644
|
||||
context line 11
|
||||
`;
|
||||
const { lastFrame } = render(
|
||||
<DiffRenderer
|
||||
diffContent={diffWithGap}
|
||||
filename="file.txt"
|
||||
terminalWidth={80}
|
||||
/>,
|
||||
<OverflowProvider>
|
||||
<DiffRenderer
|
||||
diffContent={diffWithGap}
|
||||
filename="file.txt"
|
||||
terminalWidth={80}
|
||||
/>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('═'); // Check for the border character used in the gap
|
||||
@@ -197,11 +212,13 @@ index abc..def 100644
|
||||
context line 15
|
||||
`;
|
||||
const { lastFrame } = render(
|
||||
<DiffRenderer
|
||||
diffContent={diffWithSmallGap}
|
||||
filename="file.txt"
|
||||
terminalWidth={80}
|
||||
/>,
|
||||
<OverflowProvider>
|
||||
<DiffRenderer
|
||||
diffContent={diffWithSmallGap}
|
||||
filename="file.txt"
|
||||
terminalWidth={80}
|
||||
/>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
const output = lastFrame();
|
||||
expect(output).not.toContain('═'); // Ensure no separator is rendered
|
||||
@@ -267,12 +284,14 @@ index 123..789 100644
|
||||
'with terminalWidth $terminalWidth and height $height',
|
||||
({ terminalWidth, height, expected }) => {
|
||||
const { lastFrame } = render(
|
||||
<DiffRenderer
|
||||
diffContent={diffWithMultipleHunks}
|
||||
filename="multi.js"
|
||||
terminalWidth={terminalWidth}
|
||||
availableTerminalHeight={height}
|
||||
/>,
|
||||
<OverflowProvider>
|
||||
<DiffRenderer
|
||||
diffContent={diffWithMultipleHunks}
|
||||
filename="multi.js"
|
||||
terminalWidth={terminalWidth}
|
||||
availableTerminalHeight={height}
|
||||
/>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
const output = lastFrame();
|
||||
expect(sanitizeOutput(output, terminalWidth)).toEqual(expected);
|
||||
@@ -297,11 +316,13 @@ fileDiff Index: file.txt
|
||||
\\ No newline at end of file
|
||||
`;
|
||||
const { lastFrame } = render(
|
||||
<DiffRenderer
|
||||
diffContent={newFileDiff}
|
||||
filename="TEST"
|
||||
terminalWidth={80}
|
||||
/>,
|
||||
<OverflowProvider>
|
||||
<DiffRenderer
|
||||
diffContent={newFileDiff}
|
||||
filename="TEST"
|
||||
terminalWidth={80}
|
||||
/>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
@@ -325,11 +346,13 @@ fileDiff Index: Dockerfile
|
||||
\\ No newline at end of file
|
||||
`;
|
||||
const { lastFrame } = render(
|
||||
<DiffRenderer
|
||||
diffContent={newFileDiff}
|
||||
filename="Dockerfile"
|
||||
terminalWidth={80}
|
||||
/>,
|
||||
<OverflowProvider>
|
||||
<DiffRenderer
|
||||
diffContent={newFileDiff}
|
||||
filename="Dockerfile"
|
||||
terminalWidth={80}
|
||||
/>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
const output = lastFrame();
|
||||
expect(output).toEqual(`1 FROM node:14
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { render } from 'ink-testing-library';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { ToolConfirmationMessage } from './ToolConfirmationMessage.js';
|
||||
import { ToolCallConfirmationDetails } from '@gemini-cli/core';
|
||||
import { ToolCallConfirmationDetails } from '@google/gemini-cli-core';
|
||||
|
||||
describe('ToolConfirmationMessage', () => {
|
||||
it('should not display urls if prompt and url are the same', () => {
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
ToolExecuteConfirmationDetails,
|
||||
ToolMcpConfirmationDetails,
|
||||
Config,
|
||||
} from '@gemini-cli/core';
|
||||
} from '@google/gemini-cli-core';
|
||||
import {
|
||||
RadioButtonSelect,
|
||||
RadioSelectItem,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { IndividualToolCallDisplay, ToolCallStatus } from '../../types.js';
|
||||
import { ToolMessage } from './ToolMessage.js';
|
||||
import { ToolConfirmationMessage } from './ToolConfirmationMessage.js';
|
||||
import { Colors } from '../../colors.js';
|
||||
import { Config } from '@gemini-cli/core';
|
||||
import { Config } from '@google/gemini-cli-core';
|
||||
|
||||
interface ToolGroupMessageProps {
|
||||
groupId: number;
|
||||
@@ -77,7 +77,6 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
marginLeft={1}
|
||||
borderDimColor={hasPending}
|
||||
borderColor={borderColor}
|
||||
marginBottom={1}
|
||||
>
|
||||
{toolCalls.map((tool) => {
|
||||
const isConfirming = toolAwaitingApproval?.callId === tool.callId;
|
||||
|
||||
@@ -18,6 +18,9 @@ const RESERVED_LINE_COUNT = 5; // for tool name, status, padding etc.
|
||||
const STATUS_INDICATOR_WIDTH = 3;
|
||||
const MIN_LINES_SHOWN = 2; // show at least this many lines
|
||||
|
||||
// Large threshold to ensure we don't cause performance issues for very large
|
||||
// outputs that will get truncated further MaxSizedBox anyway.
|
||||
const MAXIMUM_RESULT_DISPLAY_CHARACTERS = 1000000;
|
||||
export type TextEmphasis = 'high' | 'medium' | 'low';
|
||||
|
||||
export interface ToolMessageProps extends IndividualToolCallDisplay {
|
||||
@@ -44,8 +47,21 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const childWidth = terminalWidth - 3; // account for padding.
|
||||
// Long tool call response in MarkdownDisplay doesn't respect availableTerminalHeight properly,
|
||||
// we're forcing it to not render as markdown when the response is too long, it will fallback
|
||||
// to render as plain text, which is contained within the terminal using MaxSizedBox
|
||||
if (availableHeight) {
|
||||
renderOutputAsMarkdown = false;
|
||||
}
|
||||
|
||||
const childWidth = terminalWidth - 3; // account for padding.
|
||||
if (typeof resultDisplay === 'string') {
|
||||
if (resultDisplay.length > MAXIMUM_RESULT_DISPLAY_CHARACTERS) {
|
||||
// Truncate the result display to fit within the available width.
|
||||
resultDisplay =
|
||||
'...' + resultDisplay.slice(-MAXIMUM_RESULT_DISPLAY_CHARACTERS);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Box paddingX={1} paddingY={0} flexDirection="column">
|
||||
<Box minHeight={1}>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import { render } from 'ink-testing-library';
|
||||
import { OverflowProvider } from '../../contexts/OverflowContext.js';
|
||||
import { MaxSizedBox, setMaxSizedBoxDebugging } from './MaxSizedBox.js';
|
||||
import { Box, Text } from 'ink';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
@@ -18,28 +19,32 @@ describe('<MaxSizedBox />', () => {
|
||||
|
||||
it('renders children without truncation when they fit', () => {
|
||||
const { lastFrame } = render(
|
||||
<MaxSizedBox maxWidth={80} maxHeight={10}>
|
||||
<Box>
|
||||
<Text>Hello, World!</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>,
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={10}>
|
||||
<Box>
|
||||
<Text>Hello, World!</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
expect(lastFrame()).equals('Hello, World!');
|
||||
});
|
||||
|
||||
it('hides lines when content exceeds maxHeight', () => {
|
||||
const { lastFrame } = render(
|
||||
<MaxSizedBox maxWidth={80} maxHeight={2}>
|
||||
<Box>
|
||||
<Text>Line 1</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 2</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 3</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>,
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={2}>
|
||||
<Box>
|
||||
<Text>Line 1</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 2</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 3</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
expect(lastFrame()).equals(`... first 2 lines hidden ...
|
||||
Line 3`);
|
||||
@@ -47,17 +52,19 @@ Line 3`);
|
||||
|
||||
it('hides lines at the end when content exceeds maxHeight and overflowDirection is bottom', () => {
|
||||
const { lastFrame } = render(
|
||||
<MaxSizedBox maxWidth={80} maxHeight={2} overflowDirection="bottom">
|
||||
<Box>
|
||||
<Text>Line 1</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 2</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 3</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>,
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={2} overflowDirection="bottom">
|
||||
<Box>
|
||||
<Text>Line 1</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 2</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 3</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
expect(lastFrame()).equals(`Line 1
|
||||
... last 2 lines hidden ...`);
|
||||
@@ -65,11 +72,13 @@ Line 3`);
|
||||
|
||||
it('wraps text that exceeds maxWidth', () => {
|
||||
const { lastFrame } = render(
|
||||
<MaxSizedBox maxWidth={10} maxHeight={5}>
|
||||
<Box>
|
||||
<Text wrap="wrap">This is a long line of text</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>,
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={10} maxHeight={5}>
|
||||
<Box>
|
||||
<Text wrap="wrap">This is a long line of text</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
|
||||
expect(lastFrame()).equals(`This is a
|
||||
@@ -82,19 +91,21 @@ of text`);
|
||||
And has a line break.
|
||||
Leading spaces preserved.`;
|
||||
const { lastFrame } = render(
|
||||
<MaxSizedBox maxWidth={20} maxHeight={20}>
|
||||
<Box>
|
||||
<Text>Example</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>No Wrap: </Text>
|
||||
<Text wrap="wrap">{multilineText}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Longer No Wrap: </Text>
|
||||
<Text wrap="wrap">This part will wrap around.</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>,
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={20} maxHeight={20}>
|
||||
<Box>
|
||||
<Text>Example</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>No Wrap: </Text>
|
||||
<Text wrap="wrap">{multilineText}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Longer No Wrap: </Text>
|
||||
<Text wrap="wrap">This part will wrap around.</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
|
||||
expect(lastFrame()).equals(
|
||||
@@ -118,11 +129,13 @@ Longer No Wrap: This
|
||||
|
||||
it('handles words longer than maxWidth by splitting them', () => {
|
||||
const { lastFrame } = render(
|
||||
<MaxSizedBox maxWidth={5} maxHeight={5}>
|
||||
<Box>
|
||||
<Text wrap="wrap">Supercalifragilisticexpialidocious</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>,
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={5} maxHeight={5}>
|
||||
<Box>
|
||||
<Text wrap="wrap">Supercalifragilisticexpialidocious</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
|
||||
expect(lastFrame()).equals(`... …
|
||||
@@ -134,14 +147,16 @@ ious`);
|
||||
|
||||
it('does not truncate when maxHeight is undefined', () => {
|
||||
const { lastFrame } = render(
|
||||
<MaxSizedBox maxWidth={80} maxHeight={undefined}>
|
||||
<Box>
|
||||
<Text>Line 1</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 2</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>,
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={undefined}>
|
||||
<Box>
|
||||
<Text>Line 1</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 2</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
expect(lastFrame()).equals(`Line 1
|
||||
Line 2`);
|
||||
@@ -149,17 +164,19 @@ Line 2`);
|
||||
|
||||
it('shows plural "lines" when more than one line is hidden', () => {
|
||||
const { lastFrame } = render(
|
||||
<MaxSizedBox maxWidth={80} maxHeight={2}>
|
||||
<Box>
|
||||
<Text>Line 1</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 2</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 3</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>,
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={2}>
|
||||
<Box>
|
||||
<Text>Line 1</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 2</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 3</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
expect(lastFrame()).equals(`... first 2 lines hidden ...
|
||||
Line 3`);
|
||||
@@ -167,17 +184,19 @@ Line 3`);
|
||||
|
||||
it('shows plural "lines" when more than one line is hidden and overflowDirection is bottom', () => {
|
||||
const { lastFrame } = render(
|
||||
<MaxSizedBox maxWidth={80} maxHeight={2} overflowDirection="bottom">
|
||||
<Box>
|
||||
<Text>Line 1</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 2</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 3</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>,
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={2} overflowDirection="bottom">
|
||||
<Box>
|
||||
<Text>Line 1</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 2</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 3</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
expect(lastFrame()).equals(`Line 1
|
||||
... last 2 lines hidden ...`);
|
||||
@@ -185,7 +204,9 @@ Line 3`);
|
||||
|
||||
it('renders an empty box for empty children', () => {
|
||||
const { lastFrame } = render(
|
||||
<MaxSizedBox maxWidth={80} maxHeight={10}></MaxSizedBox>,
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={10}></MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
// Expect an empty string or a box with nothing in it.
|
||||
// Ink renders an empty box as an empty string.
|
||||
@@ -194,11 +215,13 @@ Line 3`);
|
||||
|
||||
it('wraps text with multi-byte unicode characters correctly', () => {
|
||||
const { lastFrame } = render(
|
||||
<MaxSizedBox maxWidth={5} maxHeight={5}>
|
||||
<Box>
|
||||
<Text wrap="wrap">你好世界</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>,
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={5} maxHeight={5}>
|
||||
<Box>
|
||||
<Text wrap="wrap">你好世界</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
|
||||
// "你好" has a visual width of 4. "世界" has a visual width of 4.
|
||||
@@ -209,11 +232,13 @@ Line 3`);
|
||||
|
||||
it('wraps text with multi-byte emoji characters correctly', () => {
|
||||
const { lastFrame } = render(
|
||||
<MaxSizedBox maxWidth={5} maxHeight={5}>
|
||||
<Box>
|
||||
<Text wrap="wrap">🐶🐶🐶🐶🐶</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>,
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={5} maxHeight={5}>
|
||||
<Box>
|
||||
<Text wrap="wrap">🐶🐶🐶🐶🐶</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
|
||||
// Each "🐶" has a visual width of 2.
|
||||
@@ -225,17 +250,19 @@ Line 3`);
|
||||
|
||||
it('accounts for additionalHiddenLinesCount', () => {
|
||||
const { lastFrame } = render(
|
||||
<MaxSizedBox maxWidth={80} maxHeight={2} additionalHiddenLinesCount={5}>
|
||||
<Box>
|
||||
<Text>Line 1</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 2</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 3</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>,
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={2} additionalHiddenLinesCount={5}>
|
||||
<Box>
|
||||
<Text>Line 1</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 2</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 3</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
// 1 line is hidden by overflow, 5 are additionally hidden.
|
||||
expect(lastFrame()).equals(`... first 7 lines hidden ...
|
||||
@@ -244,19 +271,21 @@ Line 3`);
|
||||
|
||||
it('handles React.Fragment as a child', () => {
|
||||
const { lastFrame } = render(
|
||||
<MaxSizedBox maxWidth={80} maxHeight={10}>
|
||||
<>
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={10}>
|
||||
<>
|
||||
<Box>
|
||||
<Text>Line 1 from Fragment</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 2 from Fragment</Text>
|
||||
</Box>
|
||||
</>
|
||||
<Box>
|
||||
<Text>Line 1 from Fragment</Text>
|
||||
<Text>Line 3 direct child</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>Line 2 from Fragment</Text>
|
||||
</Box>
|
||||
</>
|
||||
<Box>
|
||||
<Text>Line 3 direct child</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>,
|
||||
</MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
expect(lastFrame()).equals(`Line 1 from Fragment
|
||||
Line 2 from Fragment
|
||||
@@ -270,11 +299,13 @@ Line 3 direct child`);
|
||||
).join('\n');
|
||||
|
||||
const { lastFrame } = render(
|
||||
<MaxSizedBox maxWidth={80} maxHeight={10}>
|
||||
<Box>
|
||||
<Text>{THIRTY_LINES}</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>,
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={10}>
|
||||
<Box>
|
||||
<Text>{THIRTY_LINES}</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
|
||||
const expected = [
|
||||
@@ -292,11 +323,13 @@ Line 3 direct child`);
|
||||
).join('\n');
|
||||
|
||||
const { lastFrame } = render(
|
||||
<MaxSizedBox maxWidth={80} maxHeight={10} overflowDirection="bottom">
|
||||
<Box>
|
||||
<Text>{THIRTY_LINES}</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>,
|
||||
<OverflowProvider>
|
||||
<MaxSizedBox maxWidth={80} maxHeight={10} overflowDirection="bottom">
|
||||
<Box>
|
||||
<Text>{THIRTY_LINES}</Text>
|
||||
</Box>
|
||||
</MaxSizedBox>
|
||||
</OverflowProvider>,
|
||||
);
|
||||
|
||||
const expected = [
|
||||
|
||||
@@ -4,14 +4,22 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import React, { Fragment, useEffect, useId } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import stringWidth from 'string-width';
|
||||
import { Colors } from '../../colors.js';
|
||||
import { toCodePoints } from '../../utils/textUtils.js';
|
||||
import { useOverflowActions } from '../../contexts/OverflowContext.js';
|
||||
|
||||
let enableDebugLog = false;
|
||||
|
||||
/**
|
||||
* Minimum height for the MaxSizedBox component.
|
||||
* This ensures there is room for at least one line of content as well as the
|
||||
* message that content was truncated.
|
||||
*/
|
||||
export const MINIMUM_MAX_HEIGHT = 2;
|
||||
|
||||
export function setMaxSizedBoxDebugging(value: boolean) {
|
||||
enableDebugLog = value;
|
||||
}
|
||||
@@ -95,27 +103,18 @@ export const MaxSizedBox: React.FC<MaxSizedBoxProps> = ({
|
||||
overflowDirection = 'top',
|
||||
additionalHiddenLinesCount = 0,
|
||||
}) => {
|
||||
// When maxHeight is not set, we render the content normally rather
|
||||
// than using our custom layout logic. This should slightly improve
|
||||
// performance for the case where there is no height limit and is
|
||||
// a useful debugging tool to ensure that our layouts are consist
|
||||
// with the expected layout when there is no height limit.
|
||||
// In the future we might choose to still apply our layout logic
|
||||
// even in this case particularlly if there are cases where we
|
||||
// intentionally diverse how certain layouts are rendered.
|
||||
if (maxHeight === undefined) {
|
||||
return (
|
||||
<Box width={maxWidth} height={maxHeight} flexDirection="column">
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
const id = useId();
|
||||
const { addOverflowingId, removeOverflowingId } = useOverflowActions() || {};
|
||||
|
||||
const laidOutStyledText: StyledText[][] = [];
|
||||
const targetMaxHeight = Math.max(
|
||||
Math.round(maxHeight ?? Number.MAX_SAFE_INTEGER),
|
||||
MINIMUM_MAX_HEIGHT,
|
||||
);
|
||||
|
||||
if (maxWidth === undefined) {
|
||||
throw new Error('maxWidth must be defined when maxHeight is set.');
|
||||
}
|
||||
|
||||
const laidOutStyledText: StyledText[][] = [];
|
||||
function visitRows(element: React.ReactNode) {
|
||||
if (!React.isValidElement(element)) {
|
||||
return;
|
||||
@@ -135,22 +134,36 @@ export const MaxSizedBox: React.FC<MaxSizedBoxProps> = ({
|
||||
React.Children.forEach(children, visitRows);
|
||||
|
||||
const contentWillOverflow =
|
||||
(laidOutStyledText.length > maxHeight && maxHeight > 0) ||
|
||||
(targetMaxHeight !== undefined &&
|
||||
laidOutStyledText.length > targetMaxHeight) ||
|
||||
additionalHiddenLinesCount > 0;
|
||||
const visibleContentHeight = contentWillOverflow ? maxHeight - 1 : maxHeight;
|
||||
const visibleContentHeight =
|
||||
contentWillOverflow && targetMaxHeight !== undefined
|
||||
? targetMaxHeight - 1
|
||||
: targetMaxHeight;
|
||||
|
||||
const hiddenLinesCount = Math.max(
|
||||
0,
|
||||
laidOutStyledText.length - visibleContentHeight,
|
||||
);
|
||||
const hiddenLinesCount =
|
||||
visibleContentHeight !== undefined
|
||||
? Math.max(0, laidOutStyledText.length - visibleContentHeight)
|
||||
: 0;
|
||||
const totalHiddenLines = hiddenLinesCount + additionalHiddenLinesCount;
|
||||
|
||||
useEffect(() => {
|
||||
if (totalHiddenLines > 0) {
|
||||
addOverflowingId?.(id);
|
||||
} else {
|
||||
removeOverflowingId?.(id);
|
||||
}
|
||||
|
||||
return () => {
|
||||
removeOverflowingId?.(id);
|
||||
};
|
||||
}, [id, totalHiddenLines, addOverflowingId, removeOverflowingId]);
|
||||
|
||||
const visibleStyledText =
|
||||
hiddenLinesCount > 0
|
||||
? overflowDirection === 'top'
|
||||
? laidOutStyledText.slice(
|
||||
laidOutStyledText.length - visibleContentHeight,
|
||||
)
|
||||
? laidOutStyledText.slice(hiddenLinesCount, laidOutStyledText.length)
|
||||
: laidOutStyledText.slice(0, visibleContentHeight)
|
||||
: laidOutStyledText;
|
||||
|
||||
@@ -395,7 +408,9 @@ function layoutInkElementAsStyledText(
|
||||
) {
|
||||
lines.push(currentLine);
|
||||
}
|
||||
output.push(...lines);
|
||||
for (const line of lines) {
|
||||
output.push(line);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -511,5 +526,7 @@ function layoutInkElementAsStyledText(
|
||||
if (wrappingPart.length > 0) {
|
||||
addWrappingPartToLines();
|
||||
}
|
||||
output.push(...lines);
|
||||
for (const line of lines) {
|
||||
output.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Text } from 'ink';
|
||||
import { Text, Box } from 'ink';
|
||||
import SelectInput, {
|
||||
type ItemProps as InkSelectItemProps,
|
||||
type IndicatorProps as InkSelectIndicatorProps,
|
||||
@@ -78,12 +78,11 @@ export function RadioButtonSelect<T>({
|
||||
isSelected = false,
|
||||
}: InkSelectIndicatorProps): React.JSX.Element {
|
||||
return (
|
||||
<Text
|
||||
color={isSelected ? Colors.AccentGreen : Colors.Foreground}
|
||||
wrap="truncate"
|
||||
>
|
||||
{isSelected ? '● ' : '○ '}
|
||||
</Text>
|
||||
<Box minWidth={2} flexShrink={0}>
|
||||
<Text color={isSelected ? Colors.AccentGreen : Colors.Foreground}>
|
||||
{isSelected ? '●' : '○'}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -574,8 +574,24 @@ describe('useTextBuffer', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useTextBuffer({ viewport, isValidPath: () => false }),
|
||||
);
|
||||
act(() => result.current.handleInput('h', {}));
|
||||
act(() => result.current.handleInput('i', {}));
|
||||
act(() =>
|
||||
result.current.handleInput({
|
||||
name: 'h',
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
sequence: 'h',
|
||||
}),
|
||||
);
|
||||
act(() =>
|
||||
result.current.handleInput({
|
||||
name: 'i',
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
sequence: 'i',
|
||||
}),
|
||||
);
|
||||
expect(getBufferState(result).text).toBe('hi');
|
||||
});
|
||||
|
||||
@@ -583,7 +599,15 @@ describe('useTextBuffer', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useTextBuffer({ viewport, isValidPath: () => false }),
|
||||
);
|
||||
act(() => result.current.handleInput(undefined, { return: true }));
|
||||
act(() =>
|
||||
result.current.handleInput({
|
||||
name: 'return',
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
sequence: '\r',
|
||||
}),
|
||||
);
|
||||
expect(getBufferState(result).lines).toEqual(['', '']);
|
||||
});
|
||||
|
||||
@@ -596,7 +620,15 @@ describe('useTextBuffer', () => {
|
||||
}),
|
||||
);
|
||||
act(() => result.current.move('end'));
|
||||
act(() => result.current.handleInput(undefined, { backspace: true }));
|
||||
act(() =>
|
||||
result.current.handleInput({
|
||||
name: 'backspace',
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
sequence: '\x7f',
|
||||
}),
|
||||
);
|
||||
expect(getBufferState(result).text).toBe('');
|
||||
});
|
||||
|
||||
@@ -671,9 +703,25 @@ describe('useTextBuffer', () => {
|
||||
}),
|
||||
);
|
||||
act(() => result.current.move('end')); // cursor [0,2]
|
||||
act(() => result.current.handleInput(undefined, { leftArrow: true })); // cursor [0,1]
|
||||
act(() =>
|
||||
result.current.handleInput({
|
||||
name: 'left',
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
sequence: '\x1b[D',
|
||||
}),
|
||||
); // cursor [0,1]
|
||||
expect(getBufferState(result).cursor).toEqual([0, 1]);
|
||||
act(() => result.current.handleInput(undefined, { rightArrow: true })); // cursor [0,2]
|
||||
act(() =>
|
||||
result.current.handleInput({
|
||||
name: 'right',
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
sequence: '\x1b[C',
|
||||
}),
|
||||
); // cursor [0,2]
|
||||
expect(getBufferState(result).cursor).toEqual([0, 2]);
|
||||
});
|
||||
|
||||
@@ -683,7 +731,15 @@ describe('useTextBuffer', () => {
|
||||
);
|
||||
const textWithAnsi = '\x1B[31mHello\x1B[0m \x1B[32mWorld\x1B[0m';
|
||||
// Simulate pasting by calling handleInput with a string longer than 1 char
|
||||
act(() => result.current.handleInput(textWithAnsi, {}));
|
||||
act(() =>
|
||||
result.current.handleInput({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
sequence: textWithAnsi,
|
||||
}),
|
||||
);
|
||||
expect(getBufferState(result).text).toBe('Hello World');
|
||||
});
|
||||
|
||||
@@ -691,7 +747,15 @@ describe('useTextBuffer', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useTextBuffer({ viewport, isValidPath: () => false }),
|
||||
);
|
||||
act(() => result.current.handleInput('\r', {})); // Simulates Shift+Enter in VSCode terminal
|
||||
act(() =>
|
||||
result.current.handleInput({
|
||||
name: 'return',
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: true,
|
||||
sequence: '\r',
|
||||
}),
|
||||
); // Simulates Shift+Enter in VSCode terminal
|
||||
expect(getBufferState(result).lines).toEqual(['', '']);
|
||||
});
|
||||
|
||||
@@ -880,7 +944,15 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
|
||||
useTextBuffer({ viewport, isValidPath: () => false }),
|
||||
);
|
||||
const textWithAnsi = '\x1B[31mHello\x1B[0m';
|
||||
act(() => result.current.handleInput(textWithAnsi, {}));
|
||||
act(() =>
|
||||
result.current.handleInput({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
sequence: textWithAnsi,
|
||||
}),
|
||||
);
|
||||
expect(getBufferState(result).text).toBe('Hello');
|
||||
});
|
||||
|
||||
@@ -889,7 +961,15 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
|
||||
useTextBuffer({ viewport, isValidPath: () => false }),
|
||||
);
|
||||
const textWithControlChars = 'H\x07e\x08l\x0Bl\x0Co'; // BELL, BACKSPACE, VT, FF
|
||||
act(() => result.current.handleInput(textWithControlChars, {}));
|
||||
act(() =>
|
||||
result.current.handleInput({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
sequence: textWithControlChars,
|
||||
}),
|
||||
);
|
||||
expect(getBufferState(result).text).toBe('Hello');
|
||||
});
|
||||
|
||||
@@ -898,7 +978,15 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
|
||||
useTextBuffer({ viewport, isValidPath: () => false }),
|
||||
);
|
||||
const textWithMixed = '\u001B[4mH\u001B[0mello';
|
||||
act(() => result.current.handleInput(textWithMixed, {}));
|
||||
act(() =>
|
||||
result.current.handleInput({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
sequence: textWithMixed,
|
||||
}),
|
||||
);
|
||||
expect(getBufferState(result).text).toBe('Hello');
|
||||
});
|
||||
|
||||
@@ -907,7 +995,15 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
|
||||
useTextBuffer({ viewport, isValidPath: () => false }),
|
||||
);
|
||||
const validText = 'Hello World\nThis is a test.';
|
||||
act(() => result.current.handleInput(validText, {}));
|
||||
act(() =>
|
||||
result.current.handleInput({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
sequence: validText,
|
||||
}),
|
||||
);
|
||||
expect(getBufferState(result).text).toBe(validText);
|
||||
});
|
||||
|
||||
@@ -916,7 +1012,15 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
|
||||
useTextBuffer({ viewport, isValidPath: () => false }),
|
||||
);
|
||||
const pastedText = '\u001B[4mPasted\u001B[4m Text';
|
||||
act(() => result.current.handleInput(pastedText, {}));
|
||||
act(() =>
|
||||
result.current.handleInput({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
sequence: pastedText,
|
||||
}),
|
||||
);
|
||||
expect(getBufferState(result).text).toBe('Pasted Text');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ import os from 'os';
|
||||
import pathMod from 'path';
|
||||
import { useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import stringWidth from 'string-width';
|
||||
import { unescapePath } from '@gemini-cli/core';
|
||||
import { unescapePath } from '@google/gemini-cli-core';
|
||||
import { toCodePoints, cpLen, cpSlice } from '../../utils/textUtils.js';
|
||||
|
||||
export type Direction =
|
||||
@@ -1220,9 +1220,16 @@ export function useTextBuffer({
|
||||
);
|
||||
|
||||
const handleInput = useCallback(
|
||||
(input: string | undefined, key: Record<string, boolean>): boolean => {
|
||||
(key: {
|
||||
name: string;
|
||||
ctrl: boolean;
|
||||
meta: boolean;
|
||||
shift: boolean;
|
||||
paste: boolean;
|
||||
sequence: string;
|
||||
}): boolean => {
|
||||
const { sequence: input } = key;
|
||||
dbg('handleInput', {
|
||||
input,
|
||||
key,
|
||||
cursor: [cursorRow, cursorCol],
|
||||
visualCursor,
|
||||
@@ -1231,50 +1238,46 @@ export function useTextBuffer({
|
||||
const beforeLogicalCursor = [cursorRow, cursorCol];
|
||||
const beforeVisualCursor = [...visualCursor];
|
||||
|
||||
if (key['escape']) return false;
|
||||
if (key.name === 'escape') return false;
|
||||
|
||||
if (
|
||||
key['return'] ||
|
||||
key.name === 'return' ||
|
||||
input === '\r' ||
|
||||
input === '\n' ||
|
||||
input === '\\\r' // VSCode terminal represents shift + enter this way
|
||||
)
|
||||
newline();
|
||||
else if (key['leftArrow'] && !key['meta'] && !key['ctrl'] && !key['alt'])
|
||||
move('left');
|
||||
else if (key['ctrl'] && input === 'b') move('left');
|
||||
else if (key['rightArrow'] && !key['meta'] && !key['ctrl'] && !key['alt'])
|
||||
move('right');
|
||||
else if (key['ctrl'] && input === 'f') move('right');
|
||||
else if (key['upArrow']) move('up');
|
||||
else if (key['downArrow']) move('down');
|
||||
else if ((key['ctrl'] || key['alt']) && key['leftArrow'])
|
||||
move('wordLeft');
|
||||
else if (key['meta'] && input === 'b') move('wordLeft');
|
||||
else if ((key['ctrl'] || key['alt']) && key['rightArrow'])
|
||||
else if (key.name === 'left' && !key.meta && !key.ctrl) move('left');
|
||||
else if (key.ctrl && key.name === 'b') move('left');
|
||||
else if (key.name === 'right' && !key.meta && !key.ctrl) move('right');
|
||||
else if (key.ctrl && key.name === 'f') move('right');
|
||||
else if (key.name === 'up') move('up');
|
||||
else if (key.name === 'down') move('down');
|
||||
else if ((key.ctrl || key.meta) && key.name === 'left') move('wordLeft');
|
||||
else if (key.meta && key.name === 'b') move('wordLeft');
|
||||
else if ((key.ctrl || key.meta) && key.name === 'right')
|
||||
move('wordRight');
|
||||
else if (key['meta'] && input === 'f') move('wordRight');
|
||||
else if (key['home']) move('home');
|
||||
else if (key['ctrl'] && input === 'a') move('home');
|
||||
else if (key['end']) move('end');
|
||||
else if (key['ctrl'] && input === 'e') move('end');
|
||||
else if (key['ctrl'] && input === 'w') deleteWordLeft();
|
||||
else if (key.meta && key.name === 'f') move('wordRight');
|
||||
else if (key.name === 'home') move('home');
|
||||
else if (key.ctrl && key.name === 'a') move('home');
|
||||
else if (key.name === 'end') move('end');
|
||||
else if (key.ctrl && key.name === 'e') move('end');
|
||||
else if (key.ctrl && key.name === 'w') deleteWordLeft();
|
||||
else if (
|
||||
(key['meta'] || key['ctrl'] || key['alt']) &&
|
||||
(key['backspace'] || input === '\x7f')
|
||||
(key.meta || key.ctrl) &&
|
||||
(key.name === 'backspace' || input === '\x7f')
|
||||
)
|
||||
deleteWordLeft();
|
||||
else if ((key['meta'] || key['ctrl'] || key['alt']) && key['delete'])
|
||||
else if ((key.meta || key.ctrl) && key.name === 'delete')
|
||||
deleteWordRight();
|
||||
else if (
|
||||
key['backspace'] ||
|
||||
key.name === 'backspace' ||
|
||||
input === '\x7f' ||
|
||||
(key['ctrl'] && input === 'h') ||
|
||||
(key['delete'] && !key['shift'])
|
||||
(key.ctrl && key.name === 'h')
|
||||
)
|
||||
backspace();
|
||||
else if (key['delete'] || (key['ctrl'] && input === 'd')) del();
|
||||
else if (input && !key['ctrl'] && !key['meta']) {
|
||||
else if (key.name === 'delete' || (key.ctrl && key.name === 'd')) del();
|
||||
else if (input && !key.ctrl && !key.meta) {
|
||||
insert(input);
|
||||
}
|
||||
|
||||
@@ -1483,10 +1486,14 @@ export interface TextBuffer {
|
||||
/**
|
||||
* High level "handleInput" – receives what Ink gives us.
|
||||
*/
|
||||
handleInput: (
|
||||
input: string | undefined,
|
||||
key: Record<string, boolean>,
|
||||
) => boolean;
|
||||
handleInput: (key: {
|
||||
name: string;
|
||||
ctrl: boolean;
|
||||
meta: boolean;
|
||||
shift: boolean;
|
||||
paste: boolean;
|
||||
sequence: string;
|
||||
}) => boolean;
|
||||
/**
|
||||
* Opens the current buffer contents in the user's preferred terminal text
|
||||
* editor ($VISUAL or $EDITOR, falling back to "vi"). The method blocks
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useState,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
|
||||
interface OverflowState {
|
||||
overflowingIds: ReadonlySet<string>;
|
||||
}
|
||||
|
||||
interface OverflowActions {
|
||||
addOverflowingId: (id: string) => void;
|
||||
removeOverflowingId: (id: string) => void;
|
||||
}
|
||||
|
||||
const OverflowStateContext = createContext<OverflowState | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const OverflowActionsContext = createContext<OverflowActions | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
export const useOverflowState = (): OverflowState | undefined =>
|
||||
useContext(OverflowStateContext);
|
||||
|
||||
export const useOverflowActions = (): OverflowActions | undefined =>
|
||||
useContext(OverflowActionsContext);
|
||||
|
||||
export const OverflowProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [overflowingIds, setOverflowingIds] = useState(new Set<string>());
|
||||
|
||||
const addOverflowingId = useCallback((id: string) => {
|
||||
setOverflowingIds((prevIds) => {
|
||||
if (prevIds.has(id)) {
|
||||
return prevIds;
|
||||
}
|
||||
const newIds = new Set(prevIds);
|
||||
newIds.add(id);
|
||||
return newIds;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const removeOverflowingId = useCallback((id: string) => {
|
||||
setOverflowingIds((prevIds) => {
|
||||
if (!prevIds.has(id)) {
|
||||
return prevIds;
|
||||
}
|
||||
const newIds = new Set(prevIds);
|
||||
newIds.delete(id);
|
||||
return newIds;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const stateValue = useMemo(
|
||||
() => ({
|
||||
overflowingIds,
|
||||
}),
|
||||
[overflowingIds],
|
||||
);
|
||||
|
||||
const actionsValue = useMemo(
|
||||
() => ({
|
||||
addOverflowingId,
|
||||
removeOverflowingId,
|
||||
}),
|
||||
[addOverflowingId, removeOverflowingId],
|
||||
);
|
||||
|
||||
return (
|
||||
<OverflowStateContext.Provider value={stateValue}>
|
||||
<OverflowActionsContext.Provider value={actionsValue}>
|
||||
{children}
|
||||
</OverflowActionsContext.Provider>
|
||||
</OverflowStateContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
allowEditorTypeInSandbox,
|
||||
checkHasEditorType,
|
||||
type EditorType,
|
||||
} from '@gemini-cli/core';
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
export interface EditorDisplay {
|
||||
name: string;
|
||||
@@ -17,6 +17,7 @@ export interface EditorDisplay {
|
||||
}
|
||||
|
||||
export const EDITOR_DISPLAY_NAMES: Record<EditorType, string> = {
|
||||
zed: 'Zed',
|
||||
vscode: 'VS Code',
|
||||
windsurf: 'Windsurf',
|
||||
cursor: 'Cursor',
|
||||
@@ -27,7 +28,13 @@ class EditorSettingsManager {
|
||||
private readonly availableEditors: EditorDisplay[];
|
||||
|
||||
constructor() {
|
||||
const editorTypes: EditorType[] = ['vscode', 'windsurf', 'cursor', 'vim'];
|
||||
const editorTypes: EditorType[] = [
|
||||
'zed',
|
||||
'vscode',
|
||||
'windsurf',
|
||||
'cursor',
|
||||
'vim',
|
||||
];
|
||||
this.availableEditors = [
|
||||
{
|
||||
name: 'None',
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach, Mock } from 'vitest';
|
||||
import type { Mocked } from 'vitest';
|
||||
import { handleAtCommand } from './atCommandProcessor.js';
|
||||
import { Config, FileDiscoveryService } from '@gemini-cli/core';
|
||||
import { Config, FileDiscoveryService } from '@google/gemini-cli-core';
|
||||
import { ToolCallStatus } from '../types.js';
|
||||
import { UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||
import * as fsPromises from 'fs/promises';
|
||||
@@ -21,6 +21,7 @@ const mockConfig = {
|
||||
isSandboxed: vi.fn(() => false),
|
||||
getFileService: vi.fn(),
|
||||
getFileFilteringRespectGitIgnore: vi.fn(() => true),
|
||||
getEnableRecursiveFileSearch: vi.fn(() => true),
|
||||
} as unknown as Config;
|
||||
|
||||
const mockReadManyFilesExecute = vi.fn();
|
||||
@@ -51,8 +52,8 @@ vi.mock('fs/promises', async () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@gemini-cli/core', async () => {
|
||||
const actual = await vi.importActual('@gemini-cli/core');
|
||||
vi.mock('@google/gemini-cli-core', async () => {
|
||||
const actual = await vi.importActual('@google/gemini-cli-core');
|
||||
return {
|
||||
...actual,
|
||||
FileDiscoveryService: vi.fn(),
|
||||
@@ -720,4 +721,35 @@ describe('handleAtCommand', () => {
|
||||
expect(result.shouldProceed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when recursive file search is disabled', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(mockConfig.getEnableRecursiveFileSearch).mockReturnValue(false);
|
||||
});
|
||||
|
||||
it('should not use glob search for a nonexistent file', async () => {
|
||||
const invalidFile = 'nonexistent.txt';
|
||||
const query = `@${invalidFile}`;
|
||||
|
||||
vi.mocked(fsPromises.stat).mockRejectedValue(
|
||||
Object.assign(new Error('ENOENT'), { code: 'ENOENT' }),
|
||||
);
|
||||
|
||||
const result = await handleAtCommand({
|
||||
query,
|
||||
config: mockConfig,
|
||||
addItem: mockAddItem,
|
||||
onDebugMessage: mockOnDebugMessage,
|
||||
messageId: 300,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
expect(mockGlobExecute).not.toHaveBeenCalled();
|
||||
expect(mockOnDebugMessage).toHaveBeenCalledWith(
|
||||
`Glob tool not found. Path ${invalidFile} will be skipped.`,
|
||||
);
|
||||
expect(result.processedQuery).toEqual([{ text: query }]);
|
||||
expect(result.shouldProceed).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
getErrorMessage,
|
||||
isNodeError,
|
||||
unescapePath,
|
||||
} from '@gemini-cli/core';
|
||||
} from '@google/gemini-cli-core';
|
||||
import {
|
||||
HistoryItem,
|
||||
IndividualToolCallDisplay,
|
||||
@@ -210,10 +210,10 @@ export async function handleAtCommand({
|
||||
resolvedSuccessfully = true;
|
||||
} catch (error) {
|
||||
if (isNodeError(error) && error.code === 'ENOENT') {
|
||||
onDebugMessage(
|
||||
`Path ${pathName} not found directly, attempting glob search.`,
|
||||
);
|
||||
if (globTool) {
|
||||
if (config.getEnableRecursiveFileSearch() && globTool) {
|
||||
onDebugMessage(
|
||||
`Path ${pathName} not found directly, attempting glob search.`,
|
||||
);
|
||||
try {
|
||||
const globResult = await globTool.execute(
|
||||
{ pattern: `**/*${pathName}*`, path: config.getTargetDir() },
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { vi } from 'vitest';
|
||||
import { useShellCommandProcessor } from './shellCommandProcessor';
|
||||
import { Config, GeminiClient } from '@gemini-cli/core';
|
||||
import { Config, GeminiClient } from '@google/gemini-cli-core';
|
||||
import * as fs from 'fs';
|
||||
import EventEmitter from 'events';
|
||||
|
||||
@@ -22,7 +22,7 @@ vi.mock('os', () => ({
|
||||
platform: () => 'linux',
|
||||
tmpdir: () => '/tmp',
|
||||
}));
|
||||
vi.mock('@gemini-cli/core');
|
||||
vi.mock('@google/gemini-cli-core');
|
||||
vi.mock('../utils/textUtils.js', () => ({
|
||||
isBinary: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -8,7 +8,7 @@ import { spawn } from 'child_process';
|
||||
import { StringDecoder } from 'string_decoder';
|
||||
import type { HistoryItemWithoutId } from '../types.js';
|
||||
import { useCallback } from 'react';
|
||||
import { Config, GeminiClient } from '@gemini-cli/core';
|
||||
import { Config, GeminiClient } from '@google/gemini-cli-core';
|
||||
import { type PartListUnion } from '@google/genai';
|
||||
import { formatMemoryUsage } from '../utils/formatters.js';
|
||||
import { isBinary } from '../utils/textUtils.js';
|
||||
|
||||
@@ -62,23 +62,15 @@ import {
|
||||
} from './slashCommandProcessor.js';
|
||||
import { MessageType } from '../types.js';
|
||||
import {
|
||||
type Config,
|
||||
MCPServerStatus,
|
||||
getMCPServerStatus,
|
||||
Config,
|
||||
MCPDiscoveryState,
|
||||
MCPServerStatus,
|
||||
getMCPDiscoveryState,
|
||||
getMCPServerStatus,
|
||||
GeminiClient,
|
||||
} from '@gemini-cli/core';
|
||||
} from '@google/gemini-cli-core';
|
||||
import { useSessionStats } from '../contexts/SessionContext.js';
|
||||
|
||||
vi.mock('@gemini-code/core', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@gemini-code/core')>();
|
||||
return {
|
||||
...actual,
|
||||
GitService: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
import { LoadedSettings } from '../../config/settings.js';
|
||||
import * as ShowMemoryCommandModule from './useShowMemoryCommand.js';
|
||||
import { GIT_COMMIT_INFO } from '../../generated/git-commit.js';
|
||||
|
||||
@@ -161,10 +153,16 @@ describe('useSlashCommandProcessor', () => {
|
||||
process.env = { ...globalThis.process.env };
|
||||
});
|
||||
|
||||
const getProcessorHook = (showToolDescriptions: boolean = false) =>
|
||||
renderHook(() =>
|
||||
const getProcessorHook = (showToolDescriptions: boolean = false) => {
|
||||
const settings = {
|
||||
merged: {
|
||||
contextFileName: 'GEMINI.md',
|
||||
},
|
||||
} as LoadedSettings;
|
||||
return renderHook(() =>
|
||||
useSlashCommandProcessor(
|
||||
mockConfig,
|
||||
settings,
|
||||
[],
|
||||
mockAddItem,
|
||||
mockClearItems,
|
||||
@@ -181,6 +179,7 @@ describe('useSlashCommandProcessor', () => {
|
||||
mockSetQuittingMessages,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const getProcessor = (showToolDescriptions: boolean = false) =>
|
||||
getProcessorHook(showToolDescriptions).result.current;
|
||||
@@ -253,7 +252,11 @@ describe('useSlashCommandProcessor', () => {
|
||||
});
|
||||
expect(
|
||||
ShowMemoryCommandModule.createShowMemoryAction,
|
||||
).toHaveBeenCalledWith(mockConfig, expect.any(Function));
|
||||
).toHaveBeenCalledWith(
|
||||
mockConfig,
|
||||
expect.any(Object),
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(mockReturnedShowAction).toHaveBeenCalled();
|
||||
expect(commandResult).toBe(true);
|
||||
});
|
||||
@@ -333,6 +336,89 @@ describe('useSlashCommandProcessor', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('/about command', () => {
|
||||
it('should show the about box with all details including auth and project', async () => {
|
||||
// Arrange
|
||||
mockGetCliVersionFn.mockResolvedValue('test-version');
|
||||
process.env.SANDBOX = 'gemini-sandbox';
|
||||
process.env.GOOGLE_CLOUD_PROJECT = 'test-gcp-project';
|
||||
vi.mocked(mockConfig.getModel).mockReturnValue('test-model-from-config');
|
||||
|
||||
const settings = {
|
||||
merged: {
|
||||
selectedAuthType: 'test-auth-type',
|
||||
contextFileName: 'GEMINI.md',
|
||||
},
|
||||
} as LoadedSettings;
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useSlashCommandProcessor(
|
||||
mockConfig,
|
||||
settings,
|
||||
[],
|
||||
mockAddItem,
|
||||
mockClearItems,
|
||||
mockLoadHistory,
|
||||
mockRefreshStatic,
|
||||
mockSetShowHelp,
|
||||
mockOnDebugMessage,
|
||||
mockOpenThemeDialog,
|
||||
mockOpenAuthDialog,
|
||||
mockOpenEditorDialog,
|
||||
mockPerformMemoryRefresh,
|
||||
mockCorgiMode,
|
||||
false,
|
||||
mockSetQuittingMessages,
|
||||
),
|
||||
);
|
||||
|
||||
// Act
|
||||
await act(async () => {
|
||||
await result.current.handleSlashCommand('/about');
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(mockAddItem).toHaveBeenCalledTimes(2); // user message + about message
|
||||
expect(mockAddItem).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
type: 'about',
|
||||
cliVersion: 'test-version',
|
||||
osVersion: 'test-platform',
|
||||
sandboxEnv: 'gemini-sandbox',
|
||||
modelVersion: 'test-model-from-config',
|
||||
selectedAuthType: 'test-auth-type',
|
||||
gcpProject: 'test-gcp-project',
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
|
||||
it('should show sandbox-exec profile when applicable', async () => {
|
||||
// Arrange
|
||||
mockGetCliVersionFn.mockResolvedValue('test-version');
|
||||
process.env.SANDBOX = 'sandbox-exec';
|
||||
process.env.SEATBELT_PROFILE = 'test-profile';
|
||||
vi.mocked(mockConfig.getModel).mockReturnValue('test-model-from-config');
|
||||
|
||||
const { result } = getProcessorHook();
|
||||
|
||||
// Act
|
||||
await act(async () => {
|
||||
await result.current.handleSlashCommand('/about');
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(mockAddItem).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
sandboxEnv: 'sandbox-exec (test-profile)',
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Other commands', () => {
|
||||
it('/help should open help and return true', async () => {
|
||||
const { handleSlashCommand } = getProcessor();
|
||||
@@ -405,14 +491,7 @@ describe('useSlashCommandProcessor', () => {
|
||||
// Use the mocked memoryUsage value
|
||||
const memoryUsage = '11.8 MB';
|
||||
|
||||
const diagnosticInfo = `
|
||||
## Describe the bug
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
## Additional context
|
||||
Add any other context about the problem here.
|
||||
|
||||
## Diagnostic Information
|
||||
const info = `
|
||||
* **CLI Version:** ${cliVersion}
|
||||
* **Git Commit:** ${GIT_COMMIT_INFO}
|
||||
* **Operating System:** ${osVersion}
|
||||
@@ -421,11 +500,11 @@ Add any other context about the problem here.
|
||||
* **Memory Usage:** ${memoryUsage}
|
||||
`;
|
||||
let url =
|
||||
'https://github.com/google-gemini/gemini-cli/issues/new?template=bug_report.md';
|
||||
'https://github.com/google-gemini/gemini-cli/issues/new?template=bug_report.yml';
|
||||
if (description) {
|
||||
url += `&title=${encodeURIComponent(description)}`;
|
||||
}
|
||||
url += `&body=${encodeURIComponent(diagnosticInfo)}`;
|
||||
url += `&info=${encodeURIComponent(info)}`;
|
||||
return url;
|
||||
};
|
||||
|
||||
@@ -457,7 +536,7 @@ Add any other context about the problem here.
|
||||
process.env.SEATBELT_PROFILE = 'permissive-open';
|
||||
const bugCommand = {
|
||||
urlTemplate:
|
||||
'https://custom-bug-tracker.com/new?title={title}&body={body}',
|
||||
'https://custom-bug-tracker.com/new?title={title}&info={info}',
|
||||
};
|
||||
mockConfig = {
|
||||
...mockConfig,
|
||||
@@ -467,14 +546,7 @@ Add any other context about the problem here.
|
||||
|
||||
const { handleSlashCommand } = getProcessor();
|
||||
const bugDescription = 'This is a custom bug';
|
||||
const diagnosticInfo = `
|
||||
## Describe the bug
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
## Additional context
|
||||
Add any other context about the problem here.
|
||||
|
||||
## Diagnostic Information
|
||||
const info = `
|
||||
* **CLI Version:** 0.1.0
|
||||
* **Git Commit:** ${GIT_COMMIT_INFO}
|
||||
* **Operating System:** test-platform test-node-version
|
||||
@@ -484,7 +556,7 @@ Add any other context about the problem here.
|
||||
`;
|
||||
const expectedUrl = bugCommand.urlTemplate
|
||||
.replace('{title}', encodeURIComponent(bugDescription))
|
||||
.replace('{body}', encodeURIComponent(diagnosticInfo));
|
||||
.replace('{info}', encodeURIComponent(info));
|
||||
|
||||
let commandResult: SlashCommandActionReturn | boolean = false;
|
||||
await act(async () => {
|
||||
@@ -700,7 +772,7 @@ Add any other context about the problem here.
|
||||
describe('/mcp command', () => {
|
||||
beforeEach(() => {
|
||||
// Mock the core module with getMCPServerStatus and getMCPDiscoveryState
|
||||
vi.mock('@gemini-cli/core', async (importOriginal) => {
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
...actual,
|
||||
@@ -742,7 +814,8 @@ Add any other context about the problem here.
|
||||
expect(commandResult).toBe(true);
|
||||
});
|
||||
|
||||
it('should display a message when no MCP servers are configured', async () => {
|
||||
it('should display a message with a URL when no MCP servers are configured in a sandbox', async () => {
|
||||
process.env.SANDBOX = 'sandbox';
|
||||
mockConfig = {
|
||||
...mockConfig,
|
||||
getToolRegistry: vi.fn().mockResolvedValue({
|
||||
@@ -761,11 +834,39 @@ Add any other context about the problem here.
|
||||
2,
|
||||
expect.objectContaining({
|
||||
type: MessageType.INFO,
|
||||
text: 'No MCP servers configured.',
|
||||
text: `No MCP servers configured. Please open the following URL in your browser to view documentation:\nhttps://goo.gle/gemini-cli-docs-mcp`,
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
expect(commandResult).toBe(true);
|
||||
delete process.env.SANDBOX;
|
||||
});
|
||||
|
||||
it('should display a message and open a URL when no MCP servers are configured outside a sandbox', async () => {
|
||||
mockConfig = {
|
||||
...mockConfig,
|
||||
getToolRegistry: vi.fn().mockResolvedValue({
|
||||
getToolsByServer: vi.fn().mockReturnValue([]),
|
||||
}),
|
||||
getMcpServers: vi.fn().mockReturnValue({}),
|
||||
} as unknown as Config;
|
||||
|
||||
const { handleSlashCommand } = getProcessor();
|
||||
let commandResult: SlashCommandActionReturn | boolean = false;
|
||||
await act(async () => {
|
||||
commandResult = await handleSlashCommand('/mcp');
|
||||
});
|
||||
|
||||
expect(mockAddItem).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
type: MessageType.INFO,
|
||||
text: 'No MCP servers configured. Opening documentation in your browser: https://goo.gle/gemini-cli-docs-mcp',
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
expect(open).toHaveBeenCalledWith('https://goo.gle/gemini-cli-docs-mcp');
|
||||
expect(commandResult).toBe(true);
|
||||
});
|
||||
|
||||
it('should display configured MCP servers with status indicators and their tools', async () => {
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
MCPServerStatus,
|
||||
getMCPDiscoveryState,
|
||||
getMCPServerStatus,
|
||||
} from '@gemini-cli/core';
|
||||
} from '@google/gemini-cli-core';
|
||||
import { useSessionStats } from '../contexts/SessionContext.js';
|
||||
import {
|
||||
Message,
|
||||
@@ -32,6 +32,7 @@ import { createShowMemoryAction } from './useShowMemoryCommand.js';
|
||||
import { GIT_COMMIT_INFO } from '../../generated/git-commit.js';
|
||||
import { formatDuration, formatMemoryUsage } from '../utils/formatters.js';
|
||||
import { getCliVersion } from '../../utils/version.js';
|
||||
import { LoadedSettings } from '../../config/settings.js';
|
||||
|
||||
export interface SlashCommandActionReturn {
|
||||
shouldScheduleTool?: boolean;
|
||||
@@ -60,6 +61,7 @@ export interface SlashCommand {
|
||||
*/
|
||||
export const useSlashCommandProcessor = (
|
||||
config: Config | null,
|
||||
settings: LoadedSettings,
|
||||
history: HistoryItem[],
|
||||
addItem: UseHistoryManagerReturn['addItem'],
|
||||
clearItems: UseHistoryManagerReturn['clearItems'],
|
||||
@@ -74,6 +76,7 @@ export const useSlashCommandProcessor = (
|
||||
toggleCorgiMode: () => void,
|
||||
showToolDescriptions: boolean = false,
|
||||
setQuittingMessages: (message: HistoryItem[]) => void,
|
||||
openPrivacyNotice: () => void,
|
||||
) => {
|
||||
const session = useSessionStats();
|
||||
const gitService = useMemo(() => {
|
||||
@@ -101,6 +104,8 @@ export const useSlashCommandProcessor = (
|
||||
osVersion: message.osVersion,
|
||||
sandboxEnv: message.sandboxEnv,
|
||||
modelVersion: message.modelVersion,
|
||||
selectedAuthType: message.selectedAuthType,
|
||||
gcpProject: message.gcpProject,
|
||||
};
|
||||
} else if (message.type === MessageType.STATS) {
|
||||
historyItemContent = {
|
||||
@@ -135,9 +140,9 @@ export const useSlashCommandProcessor = (
|
||||
);
|
||||
|
||||
const showMemoryAction = useCallback(async () => {
|
||||
const actionFn = createShowMemoryAction(config, addMessage);
|
||||
const actionFn = createShowMemoryAction(config, settings, addMessage);
|
||||
await actionFn();
|
||||
}, [config, addMessage]);
|
||||
}, [config, settings, addMessage]);
|
||||
|
||||
const addMemoryAction = useCallback(
|
||||
(
|
||||
@@ -169,6 +174,23 @@ export const useSlashCommandProcessor = (
|
||||
[addMessage],
|
||||
);
|
||||
|
||||
const savedChatTags = useCallback(async () => {
|
||||
const geminiDir = config?.getProjectTempDir();
|
||||
if (!geminiDir) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const files = await fs.readdir(geminiDir);
|
||||
return files
|
||||
.filter(
|
||||
(file) => file.startsWith('checkpoint-') && file.endsWith('.json'),
|
||||
)
|
||||
.map((file) => file.replace('checkpoint-', '').replace('.json', ''));
|
||||
} catch (_err) {
|
||||
return [];
|
||||
}
|
||||
}, [config]);
|
||||
|
||||
const slashCommands: SlashCommand[] = useMemo(() => {
|
||||
const commands: SlashCommand[] = [
|
||||
{
|
||||
@@ -180,6 +202,27 @@ export const useSlashCommandProcessor = (
|
||||
setShowHelp(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'docs',
|
||||
description: 'open full Gemini CLI documentation in your browser',
|
||||
action: async (_mainCommand, _subCommand, _args) => {
|
||||
const docsUrl = 'https://goo.gle/gemini-cli-docs';
|
||||
if (process.env.SANDBOX && process.env.SANDBOX !== 'sandbox-exec') {
|
||||
addMessage({
|
||||
type: MessageType.INFO,
|
||||
content: `Please open the following URL in your browser to view the documentation:\n${docsUrl}`,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
} else {
|
||||
addMessage({
|
||||
type: MessageType.INFO,
|
||||
content: `Opening documentation in your browser: ${docsUrl}`,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
await open(docsUrl);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'clear',
|
||||
description: 'clear the screen and conversation history',
|
||||
@@ -212,6 +255,13 @@ export const useSlashCommandProcessor = (
|
||||
openEditorDialog();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'privacy',
|
||||
description: 'display the privacy notice',
|
||||
action: (_mainCommand, _subCommand, _args) => {
|
||||
openPrivacyNotice();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'stats',
|
||||
altName: 'usage',
|
||||
@@ -268,11 +318,21 @@ export const useSlashCommandProcessor = (
|
||||
const serverNames = Object.keys(mcpServers);
|
||||
|
||||
if (serverNames.length === 0) {
|
||||
addMessage({
|
||||
type: MessageType.INFO,
|
||||
content: 'No MCP servers configured.',
|
||||
timestamp: new Date(),
|
||||
});
|
||||
const docsUrl = 'https://goo.gle/gemini-cli-docs-mcp';
|
||||
if (process.env.SANDBOX && process.env.SANDBOX !== 'sandbox-exec') {
|
||||
addMessage({
|
||||
type: MessageType.INFO,
|
||||
content: `No MCP servers configured. Please open the following URL in your browser to view documentation:\n${docsUrl}`,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
} else {
|
||||
addMessage({
|
||||
type: MessageType.INFO,
|
||||
content: `No MCP servers configured. Opening documentation in your browser: ${docsUrl}`,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
await open(docsUrl);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -427,19 +487,27 @@ export const useSlashCommandProcessor = (
|
||||
switch (subCommand) {
|
||||
case 'show':
|
||||
showMemoryAction();
|
||||
return; // Explicitly return void
|
||||
return;
|
||||
case 'refresh':
|
||||
performMemoryRefresh();
|
||||
return; // Explicitly return void
|
||||
return;
|
||||
case 'add':
|
||||
return addMemoryAction(mainCommand, subCommand, args); // Return the object
|
||||
case undefined:
|
||||
addMessage({
|
||||
type: MessageType.ERROR,
|
||||
content:
|
||||
'Missing command\nUsage: /memory <show|refresh|add> [text for add]',
|
||||
timestamp: new Date(),
|
||||
});
|
||||
return;
|
||||
default:
|
||||
addMessage({
|
||||
type: MessageType.ERROR,
|
||||
content: `Unknown /memory command: ${subCommand}. Available: show, refresh, add`,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
return; // Explicitly return void
|
||||
return;
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -538,6 +606,8 @@ export const useSlashCommandProcessor = (
|
||||
}
|
||||
const modelVersion = config?.getModel() || 'Unknown';
|
||||
const cliVersion = await getCliVersion();
|
||||
const selectedAuthType = settings.merged.selectedAuthType || '';
|
||||
const gcpProject = process.env.GOOGLE_CLOUD_PROJECT || '';
|
||||
addMessage({
|
||||
type: MessageType.ABOUT,
|
||||
timestamp: new Date(),
|
||||
@@ -545,6 +615,8 @@ export const useSlashCommandProcessor = (
|
||||
osVersion,
|
||||
sandboxEnv,
|
||||
modelVersion,
|
||||
selectedAuthType,
|
||||
gcpProject,
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -571,14 +643,7 @@ export const useSlashCommandProcessor = (
|
||||
const cliVersion = await getCliVersion();
|
||||
const memoryUsage = formatMemoryUsage(process.memoryUsage().rss);
|
||||
|
||||
const diagnosticInfo = `
|
||||
## Describe the bug
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
## Additional context
|
||||
Add any other context about the problem here.
|
||||
|
||||
## Diagnostic Information
|
||||
const info = `
|
||||
* **CLI Version:** ${cliVersion}
|
||||
* **Git Commit:** ${GIT_COMMIT_INFO}
|
||||
* **Operating System:** ${osVersion}
|
||||
@@ -588,14 +653,14 @@ Add any other context about the problem here.
|
||||
`;
|
||||
|
||||
let bugReportUrl =
|
||||
'https://github.com/google-gemini/gemini-cli/issues/new?template=bug_report.md&title={title}&body={body}';
|
||||
'https://github.com/google-gemini/gemini-cli/issues/new?template=bug_report.yml&title={title}&info={info}';
|
||||
const bugCommand = config?.getBugCommand();
|
||||
if (bugCommand?.urlTemplate) {
|
||||
bugReportUrl = bugCommand.urlTemplate;
|
||||
}
|
||||
bugReportUrl = bugReportUrl
|
||||
.replace('{title}', encodeURIComponent(bugDescription))
|
||||
.replace('{body}', encodeURIComponent(diagnosticInfo));
|
||||
.replace('{info}', encodeURIComponent(info));
|
||||
|
||||
addMessage({
|
||||
type: MessageType.INFO,
|
||||
@@ -618,109 +683,124 @@ Add any other context about the problem here.
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'save',
|
||||
description: 'save conversation checkpoint. Usage: /save [tag]',
|
||||
action: async (_mainCommand, subCommand, _args) => {
|
||||
const tag = (subCommand || '').trim();
|
||||
const logger = new Logger(config?.getSessionId() || '');
|
||||
await logger.initialize();
|
||||
const chat = await config?.getGeminiClient()?.getChat();
|
||||
const history = chat?.getHistory() || [];
|
||||
if (history.length > 0) {
|
||||
await logger.saveCheckpoint(chat?.getHistory() || [], tag);
|
||||
addMessage({
|
||||
type: MessageType.INFO,
|
||||
content: `Conversation checkpoint saved${tag ? ' with tag: ' + tag : ''}.`,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
} else {
|
||||
addMessage({
|
||||
type: MessageType.INFO,
|
||||
content: 'No conversation found to save.',
|
||||
timestamp: new Date(),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'resume',
|
||||
name: 'chat',
|
||||
description:
|
||||
'resume from conversation checkpoint. Usage: /resume [tag]',
|
||||
completion: async () => {
|
||||
const geminiDir = config?.getProjectTempDir();
|
||||
if (!geminiDir) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const files = await fs.readdir(geminiDir);
|
||||
return files
|
||||
.filter(
|
||||
(file) =>
|
||||
file.startsWith('checkpoint-') && file.endsWith('.json'),
|
||||
)
|
||||
.map((file) =>
|
||||
file.replace('checkpoint-', '').replace('.json', ''),
|
||||
);
|
||||
} catch (_err) {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
action: async (_mainCommand, subCommand, _args) => {
|
||||
const tag = (subCommand || '').trim();
|
||||
'Manage conversation history. Usage: /chat <list|save|resume> [tag]',
|
||||
action: async (_mainCommand, subCommand, args) => {
|
||||
const tag = (args || '').trim();
|
||||
const logger = new Logger(config?.getSessionId() || '');
|
||||
await logger.initialize();
|
||||
const conversation = await logger.loadCheckpoint(tag);
|
||||
if (conversation.length === 0) {
|
||||
addMessage({
|
||||
type: MessageType.INFO,
|
||||
content: `No saved checkpoint found${tag ? ' with tag: ' + tag : ''}.`,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const chat = await config?.getGeminiClient()?.getChat();
|
||||
if (!chat) {
|
||||
addMessage({
|
||||
type: MessageType.ERROR,
|
||||
content: 'No chat client available to resume conversation.',
|
||||
content: 'No chat client available for conversation status.',
|
||||
timestamp: new Date(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
clearItems();
|
||||
chat.clearHistory();
|
||||
const rolemap: { [key: string]: MessageType } = {
|
||||
user: MessageType.USER,
|
||||
model: MessageType.GEMINI,
|
||||
};
|
||||
let i = 0;
|
||||
for (const item of conversation) {
|
||||
i += 1;
|
||||
const text =
|
||||
item.parts
|
||||
?.filter((m) => !!m.text)
|
||||
.map((m) => m.text)
|
||||
.join('') || '';
|
||||
if (i <= 2) {
|
||||
// Skip system prompt back and forth.
|
||||
continue;
|
||||
}
|
||||
if (!text) {
|
||||
// Parsing Part[] back to various non-text output not yet implemented.
|
||||
continue;
|
||||
}
|
||||
addItem(
|
||||
{
|
||||
type: (item.role && rolemap[item.role]) || MessageType.GEMINI,
|
||||
text,
|
||||
} as HistoryItemWithoutId,
|
||||
i,
|
||||
);
|
||||
chat.addHistory(item);
|
||||
if (!subCommand) {
|
||||
addMessage({
|
||||
type: MessageType.ERROR,
|
||||
content: 'Missing command\nUsage: /chat <list|save|resume> [tag]',
|
||||
timestamp: new Date(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
switch (subCommand) {
|
||||
case 'save': {
|
||||
const history = chat.getHistory();
|
||||
if (history.length > 0) {
|
||||
await logger.saveCheckpoint(chat?.getHistory() || [], tag);
|
||||
addMessage({
|
||||
type: MessageType.INFO,
|
||||
content: `Conversation checkpoint saved${tag ? ' with tag: ' + tag : ''}.`,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
} else {
|
||||
addMessage({
|
||||
type: MessageType.INFO,
|
||||
content: 'No conversation found to save.',
|
||||
timestamp: new Date(),
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 'resume':
|
||||
case 'restore':
|
||||
case 'load': {
|
||||
const conversation = await logger.loadCheckpoint(tag);
|
||||
if (conversation.length === 0) {
|
||||
addMessage({
|
||||
type: MessageType.INFO,
|
||||
content: `No saved checkpoint found${tag ? ' with tag: ' + tag : ''}.`,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
clearItems();
|
||||
chat.clearHistory();
|
||||
const rolemap: { [key: string]: MessageType } = {
|
||||
user: MessageType.USER,
|
||||
model: MessageType.GEMINI,
|
||||
};
|
||||
let hasSystemPrompt = false;
|
||||
let i = 0;
|
||||
for (const item of conversation) {
|
||||
i += 1;
|
||||
|
||||
// Add each item to history regardless of whether we display
|
||||
// it.
|
||||
chat.addHistory(item);
|
||||
|
||||
const text =
|
||||
item.parts
|
||||
?.filter((m) => !!m.text)
|
||||
.map((m) => m.text)
|
||||
.join('') || '';
|
||||
if (!text) {
|
||||
// Parsing Part[] back to various non-text output not yet implemented.
|
||||
continue;
|
||||
}
|
||||
if (i === 1 && text.match(/context for our chat/)) {
|
||||
hasSystemPrompt = true;
|
||||
}
|
||||
if (i > 2 || !hasSystemPrompt) {
|
||||
addItem(
|
||||
{
|
||||
type:
|
||||
(item.role && rolemap[item.role]) || MessageType.GEMINI,
|
||||
text,
|
||||
} as HistoryItemWithoutId,
|
||||
i,
|
||||
);
|
||||
}
|
||||
}
|
||||
console.clear();
|
||||
refreshStatic();
|
||||
return;
|
||||
}
|
||||
case 'list':
|
||||
addMessage({
|
||||
type: MessageType.INFO,
|
||||
content:
|
||||
'list of saved conversations: ' +
|
||||
(await savedChatTags()).join(', '),
|
||||
timestamp: new Date(),
|
||||
});
|
||||
return;
|
||||
default:
|
||||
addMessage({
|
||||
type: MessageType.ERROR,
|
||||
content: `Unknown /chat command: ${subCommand}. Available: list, save, resume`,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
console.clear();
|
||||
refreshStatic();
|
||||
},
|
||||
completion: async () =>
|
||||
(await savedChatTags()).map((tag) => 'resume ' + tag),
|
||||
},
|
||||
{
|
||||
name: 'quit',
|
||||
@@ -939,7 +1019,9 @@ Add any other context about the problem here.
|
||||
addMemoryAction,
|
||||
addMessage,
|
||||
toggleCorgiMode,
|
||||
savedChatTags,
|
||||
config,
|
||||
settings,
|
||||
showToolDescriptions,
|
||||
session,
|
||||
gitService,
|
||||
@@ -948,6 +1030,7 @@ Add any other context about the problem here.
|
||||
setQuittingMessages,
|
||||
pendingCompressionItemRef,
|
||||
setPendingCompressionItem,
|
||||
openPrivacyNotice,
|
||||
]);
|
||||
|
||||
const handleSlashCommand = useCallback(
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
Config,
|
||||
clearCachedCredentialFile,
|
||||
getErrorMessage,
|
||||
} from '@gemini-cli/core';
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
async function performAuthFlow(authMethod: AuthType, config: Config) {
|
||||
await config.refreshAuth(authMethod);
|
||||
@@ -46,13 +46,7 @@ export const useAuthCommand = (
|
||||
config,
|
||||
);
|
||||
} catch (e) {
|
||||
const errorMessage =
|
||||
settings.merged.selectedAuthType ===
|
||||
AuthType.LOGIN_WITH_GOOGLE_PERSONAL
|
||||
? `Failed to login. Ensure your Google account is not an enterprise account.
|
||||
Message: ${getErrorMessage(e)}`
|
||||
: `Failed to login. Message: ${getErrorMessage(e)}`;
|
||||
setAuthError(errorMessage);
|
||||
setAuthError(`Failed to login. Message: ${getErrorMessage(e)}`);
|
||||
openAuthDialog();
|
||||
} finally {
|
||||
setIsAuthenticating(false);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user