Upgrade to Ink 6 and React 19 (#2096)

Co-authored-by: jacob314 <jacob314@gmail.com>
This commit is contained in:
Sandy Tao 2025-06-27 16:39:54 -07:00 committed by GitHub
parent 19d2a0fb35
commit 150df382f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 1129 additions and 1571 deletions

View File

@ -91,7 +91,6 @@ Rather than relying on Java-esque private or public class members, which can be
TypeScript's power lies in its ability to provide static type checking, catching potential errors before your code runs. To fully leverage this, it's crucial to avoid the `any` type and be judicious with type assertions.
- **The Dangers of `any`**: Using any effectively opts out of TypeScript's type checking for that particular variable or expression. While it might seem convenient in the short term, it introduces significant risks:
- **Loss of Type Safety**: You lose all the benefits of type checking, making it easy to introduce runtime errors that TypeScript would otherwise have caught.
- **Reduced Readability and Maintainability**: Code with `any` types is harder to understand and maintain, as the expected type of data is no longer explicitly defined.
- **Masking Underlying Issues**: Often, the need for any indicates a deeper problem in the design of your code or the way you're interacting with external libraries. It's a sign that you might need to refine your types or refactor your code.
@ -163,7 +162,6 @@ Design for a good user experience - Provide clear, minimal, and non-blocking UI
### Process
1. Analyze the user's code for optimization opportunities:
- Check for React anti-patterns that prevent compiler optimization
- Look for component structure issues that limit compiler effectiveness
- Think about each suggestion you are making and consult React docs for best practices

View File

@ -7,7 +7,6 @@ This document provides a high-level overview of the Gemini CLI's architecture.
The Gemini CLI is primarily composed of two main packages, along with a suite of tools that can be used by the system in the course of handling command-line input:
1. **CLI package (`packages/cli`):**
- **Purpose:** This contains the user-facing portion of the Gemini CLI, such as handling the initial user input, presenting the final output, and managing the overall user experience.
- **Key functions contained in the package:**
- [Input processing](./cli/commands.md)
@ -17,7 +16,6 @@ The Gemini CLI is primarily composed of two main packages, along with a suite of
- [CLI configuration settings](./cli/configuration.md)
2. **Core package (`packages/core`):**
- **Purpose:** This acts as the backend for the Gemini CLI. It receives requests sent from `packages/cli`, orchestrates interactions with the Gemini API, and manages the execution of available tools.
- **Key functions contained in the package:**
- API client for communicating with the Google Gemini API

View File

@ -3,18 +3,15 @@
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:
@ -22,7 +19,6 @@ The Gemini CLI requires you to authenticate with Google's AI services. On initia
```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
@ -31,7 +27,6 @@ The Gemini CLI requires you to authenticate with Google's AI services. On initia
```
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:

View File

@ -7,11 +7,9 @@ Gemini CLI supports several built-in commands to help you manage your session, c
Slash commands provide meta-level control over the CLI itself.
- **`/bug`**
- **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`**
@ -24,24 +22,19 @@ Slash commands provide meta-level control over the CLI itself.
- **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.
- **Keyboard shortcut:** Press **Ctrl+L** at any time to perform a clear action.
- **`/compress`**
- **Description:** Replace the entire chat context with a summary. This saves on tokens used for future tasks while retaining a high level summary of what has happened.
- **`/editor`**
- **Description:** Open a dialog for selecting supported editors.
- **`/help`** (or **`/?`**)
- **Description:** Display help information about the Gemini CLI, including available commands and their usage.
- **`/mcp`**
- **Description:** List configured Model Context Protocol (MCP) servers, their connection status, server details, and available tools.
- **Sub-commands:**
- **`desc`** or **`descriptions`**:
@ -53,7 +46,6 @@ Slash commands provide meta-level control over the CLI itself.
- **Keyboard Shortcut:** Press **Ctrl+T** at any time to toggle between showing and hiding tool descriptions.
- **`/memory`**
- **Description:** Manage the AI's instructional context (hierarchical memory loaded from `GEMINI.md` files).
- **Sub-commands:**
- **`add`**:
@ -65,29 +57,23 @@ Slash commands provide meta-level control over the CLI itself.
- **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, 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.
- **Sub-commands:**
- **`desc`** or **`descriptions`**:
@ -96,7 +82,6 @@ Slash commands provide meta-level control over the CLI itself.
- **Description:** Hide tool descriptions, showing only the tool names.
- **`/quit`** (or **`/exit`**)
- **Description:** Exit Gemini CLI.
## At commands (`@`)
@ -104,7 +89,6 @@ Slash commands provide meta-level control over the CLI itself.
At commands are used to include the content of files or directories as part of your prompt to Gemini. These commands include git-aware filtering.
- **`@<path_to_file_or_directory>`**
- **Description:** Inject the content of the specified file or files into your current prompt. This is useful for asking questions about specific code, text, or collections of files.
- **Examples:**
- `@path/to/your/file.txt Explain this text.`
@ -132,14 +116,12 @@ At commands are used to include the content of files or directories as part of y
The `!` prefix lets you interact with your system's shell directly from within Gemini CLI.
- **`!<shell_command>`**
- **Description:** Execute the given `<shell_command>` in your system's default shell. Any output or errors from the command are displayed in the terminal.
- **Examples:**
- `!ls -la` (executes `ls -la` and returns to Gemini CLI)
- `!git status` (executes `git status` and returns to Gemini CLI)
- **`!` (Toggle shell mode)**
- **Description:** Typing `!` on its own toggles shell mode.
- **Entering shell mode:**
- When active, shell mode uses a different coloring and a "Shell Mode Indicator".

View File

@ -34,13 +34,11 @@ In addition to a project settings file, a project's `.gemini` directory can cont
### Available settings in `settings.json`:
- **`contextFileName`** (string or array of strings):
- **Description:** Specifies the filename for context files (e.g., `GEMINI.md`, `AGENTS.md`). Can be a single filename or a list of accepted filenames.
- **Default:** `GEMINI.md`
- **Example:** `"contextFileName": "AGENTS.md"`
- **`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.yml&title={title}&info={info}"`
- **Properties:**
@ -53,7 +51,6 @@ In addition to a project settings file, a project's `.gemini` directory can cont
```
- **`fileFiltering`** (object):
- **Description:** Controls git-aware file filtering behavior for @ commands and file discovery tools.
- **Default:** `"respectGitIgnore": true, "enableRecursiveFileSearch": true`
- **Properties:**
@ -68,43 +65,36 @@ In addition to a project settings file, a project's `.gemini` directory can cont
```
- **`coreTools`** (array of strings):
- **Description:** Allows you to specify a list of core tool names that should be made available to the model. This can be used to restrict the set of built-in tools. See [Built-in Tools](../core/tools-api.md#built-in-tools) for a list of core tools.
- **Default:** All tools available for use by the Gemini model.
- **Example:** `"coreTools": ["ReadFileTool", "GlobTool", "SearchText"]`.
- **`excludeTools`** (array of strings):
- **Description:** Allows you to specify a list of core tool names that should be excluded from the model. A tool listed in both `excludeTools` and `coreTools` is excluded.
- **Default**: No tools excluded.
- **Example:** `"excludeTools": ["run_shell_command", "findFiles"]`.
- **`autoAccept`** (boolean):
- **Description:** Controls whether the CLI automatically accepts and executes tool calls that are considered safe (e.g., read-only operations) without explicit user confirmation. If set to `true`, the CLI will bypass the confirmation prompt for tools deemed safe.
- **Default:** `false`
- **Example:** `"autoAccept": true`
- **`theme`** (string):
- **Description:** Sets the visual [theme](./themes.md) for Gemini CLI.
- **Default:** `"Default"`
- **Example:** `"theme": "GitHub"`
- **`sandbox`** (boolean or string):
- **Description:** Controls whether and how to use sandboxing for tool execution. If set to `true`, Gemini CLI uses a pre-built `gemini-cli-sandbox` Docker image. For more information, see [Sandboxing](#sandboxing).
- **Default:** `false`
- **Example:** `"sandbox": "docker"`
- **`toolDiscoveryCommand`** (string):
- **Description:** Defines a custom shell command for discovering tools from your project. The shell command must return on `stdout` a JSON array of [function declarations](https://ai.google.dev/gemini-api/docs/function-calling#function-declarations). Tool wrappers are optional.
- **Default:** Empty
- **Example:** `"toolDiscoveryCommand": "bin/get_tools"`
- **`toolCallCommand`** (string):
- **Description:** Defines a custom shell command for calling a specific tool that was discovered using `toolDiscoveryCommand`. The shell command must meet the following criteria:
- It must take function `name` (exactly as in [function declaration](https://ai.google.dev/gemini-api/docs/function-calling#function-declarations)) as first command line argument.
- It must read function arguments as JSON on `stdin`, analogous to [`functionCall.args`](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#functioncall).
@ -113,7 +103,6 @@ In addition to a project settings file, a project's `.gemini` directory can cont
- **Example:** `"toolCallCommand": "bin/call_tool"`
- **`mcpServers`** (object):
- **Description:** Configures connections to one or more Model-Context Protocol (MCP) servers for discovering and using custom tools. Gemini CLI attempts to connect to each configured MCP server to discover available tools. If multiple MCP servers expose a tool with the same name, the tool names will be prefixed with the server alias you defined in the configuration (e.g., `serverAlias__actualToolName`) to avoid conflicts. Note that the system might strip certain schema properties from MCP tool definitions for compatibility.
- **Default:** Empty
- **Properties:**
@ -149,14 +138,12 @@ In addition to a project settings file, a project's `.gemini` directory can cont
```
- **`checkpointing`** (object):
- **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"`

View File

@ -5,7 +5,6 @@ The Gemini CLI core (`packages/core`) features a robust system for defining, reg
## Core Concepts
- **Tool (`tools.ts`):** An interface and base class (`BaseTool`) that defines the contract for all tools. Each tool must have:
- `name`: A unique internal name (used in API calls to Gemini).
- `displayName`: A user-friendly name.
- `description`: A clear explanation of what the tool does, which is provided to the Gemini model.
@ -16,7 +15,6 @@ The Gemini CLI core (`packages/core`) features a robust system for defining, reg
- `execute()`: The core method that performs the tool's action and returns a `ToolResult`.
- **`ToolResult` (`tools.ts`):** An interface defining the structure of a tool's execution outcome:
- `llmContent`: The factual string content to be included in the history sent back to the LLM for context.
- `returnDisplay`: A user-friendly string (often Markdown) or a special object (like `FileDiff`) for display in the CLI.

View File

@ -15,14 +15,12 @@ You can enable telemetry in multiple ways. Configuration is primarily managed vi
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.
@ -73,7 +71,6 @@ Use the `npm run telemetry -- --target=local` command to automate the process of
```
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.
@ -94,7 +91,6 @@ Use the `npm run telemetry -- --target=local` command to automate the process of
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
@ -111,7 +107,6 @@ Use the `npm run telemetry -- --target=gcp` command to automate setting up a loc
```
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`).
@ -141,7 +136,6 @@ The following section describes the structure of logs and metrics generated for
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)
@ -157,13 +151,11 @@ Logs are timestamped records of specific events. The following events are logged
- `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`
@ -174,13 +166,11 @@ Logs are timestamped records of specific events. The following events are logged
- `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`
@ -189,7 +179,6 @@ Logs are timestamped records of specific events. The following events are logged
- `duration_ms`
- `gemini_cli.api_response`: This event occurs upon receiving a response from Gemini API.
- **Attributes**:
- `model`
- `status_code`
@ -209,38 +198,32 @@ Metrics are numerical measurements of behavior over time. The following metrics
- `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.

View File

@ -180,7 +180,6 @@ The Gemini CLI provides a comprehensive suite of tools for interacting with the
- **Display name:** Edit
- **File:** `edit.ts`
- **Parameters:**
- `file_path` (string, required): The absolute path to the file to modify.
- `old_string` (string, required): The exact literal text to replace.
@ -206,7 +205,6 @@ The Gemini CLI provides a comprehensive suite of tools for interacting with the
- 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`.

View File

@ -5,7 +5,6 @@ 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.
@ -18,27 +17,22 @@ This guide provides solutions to common issues and debugging tips.
## 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 @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.**
- **Cause:** Another process is already using the port the MCP server is trying to bind to.
- **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 to run Gemini CLI).**
- **Cause:** Gemini CLI is not correctly installed or not in your system's PATH.
- **Solution:**
1. Ensure Gemini CLI installation was successful.
@ -46,32 +40,27 @@ This guide provides solutions to common issues and debugging tips.
3. If running from source, ensure you are using the correct command to invoke it (e.g., `node packages/cli/dist/index.js ...`).
- **Error: `MODULE_NOT_FOUND` or import errors.**
- **Cause:** Dependencies are not installed correctly, or the project hasn't been built.
- **Solution:**
1. Run `npm install` to ensure all dependencies are present.
2. Run `npm run build` to compile the project.
- **Error: "Operation not permitted", "Permission denied", or similar.**
- **Cause:** If sandboxing is enabled, then the application is likely attempting an operation restricted by your sandbox, such as writing outside the project directory or system temp directory.
- **Solution:** See [Sandboxing](./cli/configuration.md#sandboxing) for more information, including how to customize your sandbox configuration.
## Debugging Tips
- **CLI debugging:**
- Use the `--verbose` flag (if available) with CLI commands for more detailed output.
- Check the CLI logs, often found in a user-specific configuration or cache directory.
- **Core debugging:**
- Check the server console output for error messages or stack traces.
- Increase log verbosity if configurable.
- Use Node.js debugging tools (e.g., `node --inspect`) if you need to step through server-side code.
- **Tool issues:**
- If a specific tool is failing, try to isolate the issue by running the simplest possible version of the command or operation the tool performs.
- For `run_shell_command`, check that the command works directly in your shell first.
- For file system tools, double-check paths and permissions.

2235
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -38,17 +38,17 @@
"gaxios": "^6.1.1",
"glob": "^10.4.1",
"highlight.js": "^11.11.1",
"ink": "^5.2.0",
"ink": "^6.0.1",
"ink-big-text": "^2.0.0",
"ink-gradient": "^3.0.0",
"ink-select-input": "^6.0.0",
"ink-select-input": "^6.2.0",
"ink-spinner": "^5.0.0",
"ink-link": "^4.0.0",
"ink-link": "^4.1.0",
"ink-text-input": "^6.0.0",
"lowlight": "^3.3.0",
"mime-types": "^2.1.4",
"open": "^10.1.2",
"react": "^18.3.1",
"react": "^19.1.0",
"read-package-up": "^11.0.0",
"shell-quote": "^1.8.2",
"string-width": "^7.1.0",
@ -58,16 +58,20 @@
"yargs": "^17.7.2"
},
"devDependencies": {
"@testing-library/react": "^14.0.0",
"@testing-library/react": "^16.3.0",
"@babel/runtime": "^7.27.6",
"@types/command-exists": "^1.2.3",
"@types/diff": "^7.0.2",
"@types/dotenv": "^6.1.1",
"@types/node": "^20.11.24",
"@types/react": "^18.3.1",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@types/shell-quote": "^1.7.5",
"@types/yargs": "^17.0.32",
"ink-testing-library": "^4.0.0",
"jsdom": "^26.1.0",
"react-dom": "^19.1.0",
"pretty-format": "^30.0.2",
"typescript": "^5.3.3",
"vitest": "^3.1.1"
},

View File

@ -351,18 +351,6 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
// 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') {
@ -530,23 +518,6 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
};
}, [terminalWidth, terminalHeight, refreshStatic]);
useEffect(() => {
if (!pendingHistoryItems.length) {
return;
}
const pendingItemDimensions = measureElement(
pendingHistoryItemRef.current!,
);
// 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 (pendingItemDimensions.height > availableTerminalHeight) {
setStaticNeedsRefresh(true);
}
}, [pendingHistoryItems.length, availableTerminalHeight, streamingState]);
useEffect(() => {
if (streamingState === StreamingState.Idle && staticNeedsRefresh) {
setStaticNeedsRefresh(false);

View File

@ -116,13 +116,15 @@ export const MaxSizedBox: React.FC<MaxSizedBoxProps> = ({
throw new Error('maxWidth must be defined when maxHeight is set.');
}
function visitRows(element: React.ReactNode) {
if (!React.isValidElement(element)) {
if (!React.isValidElement<{ children?: React.ReactNode }>(element)) {
return;
}
if (element.type === Fragment) {
React.Children.forEach(element.props.children, visitRows);
return;
}
if (element.type === Box) {
layoutInkElementAsStyledText(element, maxWidth!, laidOutStyledText);
return;
@ -246,7 +248,10 @@ interface Row {
* @returns An array of `Row` objects.
*/
function visitBoxRow(element: React.ReactNode): Row {
if (!React.isValidElement(element) || element.type !== Box) {
if (
!React.isValidElement<{ children?: React.ReactNode }>(element) ||
element.type !== Box
) {
debugReportError(
`All children of MaxSizedBox must be <Box> elements`,
element,
@ -258,7 +263,15 @@ function visitBoxRow(element: React.ReactNode): Row {
}
if (enableDebugLog) {
const boxProps = element.props;
const boxProps = element.props as {
children?: React.ReactNode | undefined;
readonly flexDirection?:
| 'row'
| 'column'
| 'row-reverse'
| 'column-reverse'
| undefined;
};
// Ensure the Box has no props other than the default ones and key.
let maxExpectedProps = 4;
if (boxProps.children !== undefined) {
@ -323,14 +336,13 @@ function visitBoxRow(element: React.ReactNode): Row {
return;
}
if (!React.isValidElement(element)) {
if (!React.isValidElement<{ children?: React.ReactNode }>(element)) {
debugReportError('Invalid element.', element);
return;
}
if (element.type === Fragment) {
const fragmentChildren = element.props.children;
React.Children.forEach(fragmentChildren, (child) =>
React.Children.forEach(element.props.children, (child) =>
visitRowChild(child, parentProps),
);
return;

View File

@ -6,6 +6,7 @@
import { type MutableRefObject } from 'react';
import { render } from 'ink-testing-library';
import { renderHook } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import { SessionStatsProvider, useSessionStats } from './SessionContext.js';
import { describe, it, expect, vi } from 'vitest';
@ -223,21 +224,16 @@ describe('SessionStatsContext', () => {
});
it('should throw an error when useSessionStats is used outside of a provider', () => {
// Suppress the expected console error during this test.
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
// Suppress console.error for this test since we expect an error
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const contextRef = { current: undefined };
// We expect rendering to fail, which React will catch and log as an error.
render(<TestHarness contextRef={contextRef} />);
// Assert that the first argument of the first call to console.error
// contains the expected message. This is more robust than checking
// the exact arguments, which can be affected by React/JSDOM internals.
expect(errorSpy.mock.calls[0][0]).toContain(
'useSessionStats must be used within a SessionStatsProvider',
);
errorSpy.mockRestore();
try {
// Expect renderHook itself to throw when the hook is used outside a provider
expect(() => {
renderHook(() => useSessionStats());
}).toThrow('useSessionStats must be used within a SessionStatsProvider');
} finally {
consoleSpy.mockRestore();
}
});
});

View File

@ -25,9 +25,12 @@ export function useConsoleMessages(): UseConsoleMessagesReturn {
return;
}
const newMessagesToAdd = messageQueueRef.current;
messageQueueRef.current = [];
setConsoleMessages((prevMessages) => {
const newMessages = [...prevMessages];
messageQueueRef.current.forEach((queuedMessage) => {
newMessagesToAdd.forEach((queuedMessage) => {
if (
newMessages.length > 0 &&
newMessages[newMessages.length - 1].type === queuedMessage.type &&
@ -42,7 +45,6 @@ export function useConsoleMessages(): UseConsoleMessagesReturn {
return newMessages;
});
messageQueueRef.current = [];
messageQueueTimeoutRef.current = null; // Allow next scheduling
}, []);

View File

@ -496,13 +496,17 @@ describe('useGeminiStream', () => {
} as TrackedCompletedToolCall, // Treat error as a form of completion for submission
];
// 1. On the first render, there are no tool calls.
mockUseReactToolScheduler.mockReturnValue([
[],
mockScheduleToolCalls,
mockMarkToolsAsSubmitted,
]);
const { rerender } = renderHook(() =>
// Capture the onComplete callback
let capturedOnComplete:
| ((completedTools: TrackedToolCall[]) => Promise<void>)
| null = null;
mockUseReactToolScheduler.mockImplementation((onComplete) => {
capturedOnComplete = onComplete;
return [[], mockScheduleToolCalls, mockMarkToolsAsSubmitted];
});
renderHook(() =>
useGeminiStream(
new MockedGeminiClientClass(mockConfig),
[],
@ -518,16 +522,11 @@ describe('useGeminiStream', () => {
),
);
// 2. Before the second render, change the mock to return the completed tools.
mockUseReactToolScheduler.mockReturnValue([
completedToolCalls,
mockScheduleToolCalls,
mockMarkToolsAsSubmitted,
]);
// 3. Trigger a re-render. The hook will now receive the completed tools, causing the effect to run.
act(() => {
rerender();
// Trigger the onComplete callback with completed tools
await act(async () => {
if (capturedOnComplete) {
await capturedOnComplete(completedToolCalls);
}
});
await waitFor(() => {
@ -561,13 +560,17 @@ describe('useGeminiStream', () => {
];
const client = new MockedGeminiClientClass(mockConfig);
// 1. First render: no tool calls.
mockUseReactToolScheduler.mockReturnValue([
[],
mockScheduleToolCalls,
mockMarkToolsAsSubmitted,
]);
const { rerender } = renderHook(() =>
// Capture the onComplete callback
let capturedOnComplete:
| ((completedTools: TrackedToolCall[]) => Promise<void>)
| null = null;
mockUseReactToolScheduler.mockImplementation((onComplete) => {
capturedOnComplete = onComplete;
return [[], mockScheduleToolCalls, mockMarkToolsAsSubmitted];
});
renderHook(() =>
useGeminiStream(
client,
[],
@ -583,16 +586,11 @@ describe('useGeminiStream', () => {
),
);
// 2. Second render: tool calls are now cancelled.
mockUseReactToolScheduler.mockReturnValue([
cancelledToolCalls,
mockScheduleToolCalls,
mockMarkToolsAsSubmitted,
]);
// 3. Trigger the re-render.
act(() => {
rerender();
// Trigger the onComplete callback with cancelled tools
await act(async () => {
if (capturedOnComplete) {
await capturedOnComplete(cancelledToolCalls);
}
});
await waitFor(() => {
@ -685,7 +683,12 @@ describe('useGeminiStream', () => {
const initialToolCalls: TrackedToolCall[] = [
{
request: { callId: 'call1', name: 'tool1', args: {} },
request: {
callId: 'call1',
name: 'tool1',
args: {},
isClientInitiated: false,
},
status: 'executing',
responseSubmittedToGemini: false,
tool: {
@ -711,36 +714,67 @@ describe('useGeminiStream', () => {
} as TrackedCompletedToolCall,
];
const { result, rerender, client } = renderTestHook(initialToolCalls);
// Capture the onComplete callback
let capturedOnComplete:
| ((completedTools: TrackedToolCall[]) => Promise<void>)
| null = null;
let currentToolCalls = initialToolCalls;
mockUseReactToolScheduler.mockImplementation((onComplete) => {
capturedOnComplete = onComplete;
return [
currentToolCalls,
mockScheduleToolCalls,
mockMarkToolsAsSubmitted,
];
});
const { result, rerender } = renderHook(() =>
useGeminiStream(
new MockedGeminiClientClass(mockConfig),
[],
mockAddItem,
mockSetShowHelp,
mockConfig,
mockOnDebugMessage,
mockHandleSlashCommand,
false,
() => 'vscode' as EditorType,
() => {},
() => Promise.resolve(),
),
);
// 1. Initial state should be Responding because a tool is executing.
expect(result.current.streamingState).toBe(StreamingState.Responding);
// 2. Rerender with the completed tool call.
// The useEffect should pick this up but hasn't called submitQuery yet.
// 2. Update the tool calls to completed state and rerender
currentToolCalls = completedToolCalls;
mockUseReactToolScheduler.mockImplementation((onComplete) => {
capturedOnComplete = onComplete;
return [
completedToolCalls,
mockScheduleToolCalls,
mockMarkToolsAsSubmitted,
];
});
act(() => {
rerender({
client,
history: [],
addItem: mockAddItem,
setShowHelp: mockSetShowHelp,
config: mockConfig,
onDebugMessage: mockOnDebugMessage,
handleSlashCommand:
mockHandleSlashCommand as unknown as typeof mockHandleSlashCommand,
shellModeActive: false,
loadedSettings: mockLoadedSettings,
// This is the key part of the test: update the toolCalls array
// to simulate the tool finishing.
toolCalls: completedToolCalls,
});
rerender();
});
// 3. The state should *still* be Responding, not Idle.
// This is because the completed tool's response has not been submitted yet.
expect(result.current.streamingState).toBe(StreamingState.Responding);
// 4. Wait for the useEffect to call submitQuery.
// 4. Trigger the onComplete callback to simulate tool completion
await act(async () => {
if (capturedOnComplete) {
await capturedOnComplete(completedToolCalls);
}
});
// 5. Wait for submitQuery to be called
await waitFor(() => {
expect(mockSendMessageStream).toHaveBeenCalledWith(
toolCallResponseParts,
@ -748,7 +782,7 @@ describe('useGeminiStream', () => {
);
});
// 5. After submission, the state should remain Responding.
// 6. After submission, the state should remain Responding until the stream completes.
expect(result.current.streamingState).toBe(StreamingState.Responding);
});
@ -929,14 +963,17 @@ describe('useGeminiStream', () => {
} as any,
};
// 1. Initial render state: no tool calls
mockUseReactToolScheduler.mockReturnValue([
[],
mockScheduleToolCalls,
mockMarkToolsAsSubmitted,
]);
// Capture the onComplete callback
let capturedOnComplete:
| ((completedTools: TrackedToolCall[]) => Promise<void>)
| null = null;
const { result, rerender } = renderHook(() =>
mockUseReactToolScheduler.mockImplementation((onComplete) => {
capturedOnComplete = onComplete;
return [[], mockScheduleToolCalls, mockMarkToolsAsSubmitted];
});
const { result } = renderHook(() =>
useGeminiStream(
new MockedGeminiClientClass(mockConfig),
[],
@ -957,17 +994,11 @@ describe('useGeminiStream', () => {
await result.current.submitQuery('/memory add "test fact"');
});
// The command handler schedules the tool. Now we simulate the tool completing.
// 2. Before the next render, set the mock to return the completed tool.
mockUseReactToolScheduler.mockReturnValue([
[completedToolCall],
mockScheduleToolCalls,
mockMarkToolsAsSubmitted,
]);
// 3. Trigger a re-render to process the completed tool.
act(() => {
rerender();
// Trigger the onComplete callback with the completed client-initiated tool
await act(async () => {
if (capturedOnComplete) {
await capturedOnComplete([completedToolCall]);
}
});
// --- Assert the outcome ---
@ -1007,13 +1038,17 @@ describe('useGeminiStream', () => {
} as any,
};
mockUseReactToolScheduler.mockReturnValue([
[completedToolCall],
mockScheduleToolCalls,
mockMarkToolsAsSubmitted,
]);
// Capture the onComplete callback
let capturedOnComplete:
| ((completedTools: TrackedToolCall[]) => Promise<void>)
| null = null;
const { rerender } = renderHook(() =>
mockUseReactToolScheduler.mockImplementation((onComplete) => {
capturedOnComplete = onComplete;
return [[], mockScheduleToolCalls, mockMarkToolsAsSubmitted];
});
renderHook(() =>
useGeminiStream(
new MockedGeminiClientClass(mockConfig),
[],
@ -1029,8 +1064,11 @@ describe('useGeminiStream', () => {
),
);
act(() => {
rerender();
// Trigger the onComplete callback with the completed save_memory tool
await act(async () => {
if (capturedOnComplete) {
await capturedOnComplete([completedToolCall]);
}
});
await waitFor(() => {

View File

@ -111,17 +111,21 @@ export const useGeminiStream = (
const [toolCalls, scheduleToolCalls, markToolsAsSubmitted] =
useReactToolScheduler(
(completedToolCallsFromScheduler) => {
async (completedToolCallsFromScheduler) => {
// This onComplete is called when ALL scheduled tools for a given batch are done.
if (completedToolCallsFromScheduler.length > 0) {
// Add the final state of these tools to the history for display.
// The new useEffect will handle submitting their responses.
addItem(
mapTrackedToolCallsToDisplay(
completedToolCallsFromScheduler as TrackedToolCall[],
),
Date.now(),
);
// Handle tool response submission immediately when tools complete
await handleCompletedTools(
completedToolCallsFromScheduler as TrackedToolCall[],
);
}
},
config,
@ -570,40 +574,33 @@ export const useGeminiStream = (
],
);
/**
* Automatically submits responses for completed tool calls.
* This effect runs when `toolCalls` or `isResponding` changes.
* It ensures that tool responses are sent back to Gemini only when
* all processing for a given set of tools is finished and Gemini
* is not already generating a response.
*/
useEffect(() => {
const run = async () => {
const handleCompletedTools = useCallback(
async (completedToolCallsFromScheduler: TrackedToolCall[]) => {
if (isResponding) {
return;
}
const completedAndReadyToSubmitTools = toolCalls.filter(
(
tc: TrackedToolCall,
): tc is TrackedCompletedToolCall | TrackedCancelledToolCall => {
const isTerminalState =
tc.status === 'success' ||
tc.status === 'error' ||
tc.status === 'cancelled';
const completedAndReadyToSubmitTools =
completedToolCallsFromScheduler.filter(
(
tc: TrackedToolCall,
): tc is TrackedCompletedToolCall | TrackedCancelledToolCall => {
const isTerminalState =
tc.status === 'success' ||
tc.status === 'error' ||
tc.status === 'cancelled';
if (isTerminalState) {
const completedOrCancelledCall = tc as
| TrackedCompletedToolCall
| TrackedCancelledToolCall;
return (
!completedOrCancelledCall.responseSubmittedToGemini &&
completedOrCancelledCall.response?.responseParts !== undefined
);
}
return false;
},
);
if (isTerminalState) {
const completedOrCancelledCall = tc as
| TrackedCompletedToolCall
| TrackedCancelledToolCall;
return (
completedOrCancelledCall.response?.responseParts !== undefined
);
}
return false;
},
);
// Finalize any client-initiated tools as soon as they are done.
const clientTools = completedAndReadyToSubmitTools.filter(
@ -630,15 +627,6 @@ export const useGeminiStream = (
);
}
// Only proceed with submitting to Gemini if ALL tools are complete.
const allToolsAreComplete =
toolCalls.length > 0 &&
toolCalls.length === completedAndReadyToSubmitTools.length;
if (!allToolsAreComplete) {
return;
}
const geminiTools = completedAndReadyToSubmitTools.filter(
(t) => !t.request.isClientInitiated,
);
@ -693,17 +681,15 @@ export const useGeminiStream = (
submitQuery(mergePartListUnions(responsesToSend), {
isContinuation: true,
});
};
void run();
}, [
toolCalls,
isResponding,
submitQuery,
markToolsAsSubmitted,
addItem,
geminiClient,
performMemoryRefresh,
]);
},
[
isResponding,
submitQuery,
markToolsAsSubmitted,
geminiClient,
performMemoryRefresh,
],
);
const pendingHistoryItems = [
pendingHistoryItemRef.current,

View File

@ -128,7 +128,7 @@ export function useReactToolScheduler(
}),
);
},
[],
[setToolCallsForDisplay],
);
const scheduler = useMemo(
@ -152,7 +152,7 @@ export function useReactToolScheduler(
);
const schedule: ScheduleFn = useCallback(
async (
(
request: ToolCallRequestInfo | ToolCallRequestInfo[],
signal: AbortSignal,
) => {