mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-28 18:51:01 -07:00
Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d10f28a0c7 | |||
| 15298b28c2 | |||
| 7311e242ec | |||
| 21a3925f99 | |||
| ec35ebbe57 | |||
| 65024d4538 | |||
| 4fb3790051 | |||
| 4a040931ef | |||
| e74efc5c0b | |||
| d5a5995281 | |||
| 893ae4d29a | |||
| 370c45de67 | |||
| a93a1ebd65 | |||
| 3f12c1d7c7 | |||
| 61719a3926 | |||
| beaa2a968b | |||
| ca0e6f9bd9 | |||
| 7a70ab9a5d | |||
| e54eecca51 | |||
| 7de3e4dcf9 | |||
| 8c215c7a88 | |||
| fe4fcf89d9 | |||
| 1f5d7014c6 | |||
| 1ae0499e5d | |||
| 9d741abdba | |||
| f54bccfe03 | |||
| 1db169bc66 | |||
| b9b7bba48d | |||
| 29caa52bb7 | |||
| e5adeaca80 | |||
| 6fb58bd31f | |||
| 7c3469713d | |||
| 64c928fce7 | |||
| 61b21e3d63 | |||
| 77027dff82 | |||
| c0dfa1aec3 | |||
| 06173c0885 | |||
| 08b926796c | |||
| b80234aa3e | |||
| efbddcbf98 | |||
| 63cc363606 | |||
| e0044f2868 | |||
| beff8c91aa | |||
| f510394721 | |||
| 811a383d50 | |||
| 8d171e0200 | |||
| 7b6ab50138 | |||
| 44c8b43328 | |||
| 242afd49a1 | |||
| 66c07d7296 | |||
| 40b73c9447 |
@@ -175,10 +175,10 @@ jobs:
|
||||
NO_COLOR: true
|
||||
run: |
|
||||
if [[ "${{ matrix.shard }}" == "cli" ]]; then
|
||||
npm run test:ci --workspace @google/gemini-cli
|
||||
npm run test:ci --workspace "@google/gemini-cli"
|
||||
else
|
||||
# Explicitly list non-cli packages to ensure they are sharded correctly
|
||||
npm run test:ci --workspace @google/gemini-cli-core --workspace @google/gemini-cli-a2a-server --workspace gemini-cli-vscode-ide-companion --workspace @google/gemini-cli-test-utils --if-present -- --coverage.enabled=false
|
||||
npm run test:ci --workspace "@google/gemini-cli-core" --workspace "@google/gemini-cli-a2a-server" --workspace "gemini-cli-vscode-ide-companion" --workspace "@google/gemini-cli-test-utils" --if-present -- --coverage.enabled=false
|
||||
npm run test:scripts
|
||||
fi
|
||||
|
||||
@@ -263,10 +263,10 @@ jobs:
|
||||
NO_COLOR: true
|
||||
run: |
|
||||
if [[ "${{ matrix.shard }}" == "cli" ]]; then
|
||||
npm run test:ci --workspace @google/gemini-cli -- --coverage.enabled=false
|
||||
npm run test:ci --workspace "@google/gemini-cli" -- --coverage.enabled=false
|
||||
else
|
||||
# Explicitly list non-cli packages to ensure they are sharded correctly
|
||||
npm run test:ci --workspace @google/gemini-cli-core --workspace @google/gemini-cli-a2a-server --workspace gemini-cli-vscode-ide-companion --workspace @google/gemini-cli-test-utils --if-present -- --coverage.enabled=false
|
||||
npm run test:ci --workspace "@google/gemini-cli-core" --workspace "@google/gemini-cli-a2a-server" --workspace "gemini-cli-vscode-ide-companion" --workspace "@google/gemini-cli-test-utils" --if-present -- --coverage.enabled=false
|
||||
npm run test:scripts
|
||||
fi
|
||||
|
||||
@@ -429,11 +429,14 @@ jobs:
|
||||
NODE_ENV: 'test'
|
||||
run: |
|
||||
if ("${{ matrix.shard }}" -eq "cli") {
|
||||
npm run test:ci --workspace @google/gemini-cli -- --coverage.enabled=false
|
||||
npm run test:ci --workspace "@google/gemini-cli" -- --coverage.enabled=false
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
} else {
|
||||
# Explicitly list non-cli packages to ensure they are sharded correctly
|
||||
npm run test:ci --workspace @google/gemini-cli-core --workspace @google/gemini-cli-a2a-server --workspace gemini-cli-vscode-ide-companion --workspace @google/gemini-cli-test-utils --if-present -- --coverage.enabled=false
|
||||
npm run test:ci --workspace "@google/gemini-cli-core" --workspace "@google/gemini-cli-a2a-server" --workspace "gemini-cli-vscode-ide-companion" --workspace "@google/gemini-cli-test-utils" --if-present -- --coverage.enabled=false
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
npm run test:scripts
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
}
|
||||
shell: 'pwsh'
|
||||
|
||||
|
||||
@@ -46,12 +46,6 @@ jobs:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Build project'
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'Detect Steering Changes'
|
||||
id: 'detect'
|
||||
run: |
|
||||
@@ -60,6 +54,14 @@ jobs:
|
||||
echo "SHOULD_RUN=$SHOULD_RUN" >> "$GITHUB_OUTPUT"
|
||||
echo "STEERING_DETECTED=$STEERING_DETECTED" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 'Install dependencies'
|
||||
if: "steps.detect.outputs.SHOULD_RUN == 'true'"
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Build project'
|
||||
if: "steps.detect.outputs.SHOULD_RUN == 'true'"
|
||||
run: 'npm run build'
|
||||
|
||||
- name: 'Analyze PR Content (Guidance)'
|
||||
if: "steps.detect.outputs.STEERING_DETECTED == 'true'"
|
||||
id: 'analysis'
|
||||
@@ -94,7 +96,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: 'Post or Update PR Comment'
|
||||
if: "always() && steps.detect.outputs.STEERING_DETECTED == 'true'"
|
||||
if: "always() && (steps.detect.outputs.STEERING_DETECTED == 'true' || env.REPORT_FILE != '')"
|
||||
env:
|
||||
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
run: |
|
||||
@@ -104,17 +106,20 @@ jobs:
|
||||
cat eval_regression_report.md
|
||||
echo ""
|
||||
fi
|
||||
echo "### 🧠 Model Steering Guidance"
|
||||
echo ""
|
||||
echo "This PR modifies files that affect the model's behavior (prompts, tools, or instructions)."
|
||||
echo ""
|
||||
|
||||
if [[ "${{ steps.analysis.outputs.MISSING_EVALS }}" == "true" ]]; then
|
||||
echo "- ⚠️ **Consider adding Evals:** No behavioral evaluations (\`evals/*.eval.ts\`) were added or updated in this PR. Consider [adding a test case](https://github.com/google-gemini/gemini-cli/blob/main/evals/README.md#creating-an-evaluation) to verify the new behavior and prevent regressions."
|
||||
fi
|
||||
if [[ "${{ steps.detect.outputs.STEERING_DETECTED }}" == "true" ]]; then
|
||||
echo "### 🧠 Model Steering Guidance"
|
||||
echo ""
|
||||
echo "This PR modifies files that affect the model's behavior (prompts, tools, or instructions)."
|
||||
echo ""
|
||||
|
||||
if [[ "${{ steps.analysis.outputs.IS_MAINTAINER }}" == "true" ]]; then
|
||||
echo "- 🚀 **Maintainer Reminder:** Please ensure that these changes do not regress results on benchmark evals before merging."
|
||||
if [[ "${{ steps.analysis.outputs.MISSING_EVALS }}" == "true" ]]; then
|
||||
echo "- ⚠️ **Consider adding Evals:** No behavioral evaluations (\`evals/*.eval.ts\`) were added or updated in this PR. Consider [adding a test case](https://github.com/google-gemini/gemini-cli/blob/main/evals/README.md#creating-an-evaluation) to verify the new behavior and prevent regressions."
|
||||
fi
|
||||
|
||||
if [[ "${{ steps.analysis.outputs.IS_MAINTAINER }}" == "true" ]]; then
|
||||
echo "- 🚀 **Maintainer Reminder:** Please ensure that these changes do not regress results on benchmark evals before merging."
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
@@ -0,0 +1,438 @@
|
||||
# Architectural Design: Gemini CLI to ADK Migration
|
||||
|
||||
| Authors: [Adam Weidman](mailto:adamfweidman@google.com) Contributors: Reviewers: *See section [Status of this document](#status-of-this-document).* | Status: Draft Last revised: Apr 7, 2026 Visibility: Confidential |
|
||||
| :--- | :--- |
|
||||
|
||||
---
|
||||
|
||||
# Goal
|
||||
|
||||
To migrate the Gemini CLI backend execution engine from its legacy fragmented loop structure to the Agent Development Kit (ADK). This migration will unify how agents and subagents are orchestrated, simplify state persistence, and expose a standard `AgentSession` interface for the CLI, future SDK surfaces, and subagent execution.
|
||||
|
||||
---
|
||||
|
||||
# Context
|
||||
|
||||
Over time, Gemini CLI has accumulated complex runtime behaviors: multi-tier tool scheduling, policy-driven approvals, payload masking, dynamic routing, and fine-grained telemetry. Integrating these with ADK requires a clean boundary that preserves Gemini CLI semantics without forking ADK core behavior.
|
||||
|
||||
The key migration boundary is:
|
||||
|
||||
- ADK runtime semantics -> Gemini CLI `AgentProtocol` / `AgentSession`
|
||||
- ADK `Event` stream -> Gemini CLI `AgentEvent` stream
|
||||
|
||||
That boundary, not the model wrapper alone, is the architectural center of this design.
|
||||
|
||||
---
|
||||
|
||||
# Current State and Proposed Mappings
|
||||
|
||||
The following analysis maps existing Gemini CLI components onto ADK capabilities, citing both repositories (`gemini-cli` and `adk-js`).
|
||||
|
||||
## Core Runtime Architecture
|
||||
|
||||
The migration uses one shared ADK-backed runtime core. Every orchestrated agent, including subagents, is exposed through the same external session contract:
|
||||
|
||||
- **Top-level CLI agent** -> `AgentSession`
|
||||
- **Future SDK entry point** -> `AgentSession`
|
||||
- **Subagent execution** -> `AgentSession`
|
||||
|
||||
The runtime owns:
|
||||
|
||||
- ADK runner/session lifecycle
|
||||
- tool execution
|
||||
- policy integration
|
||||
- routing, availability, compaction, and masking hooks
|
||||
- persistence integration
|
||||
|
||||
The adapters own:
|
||||
|
||||
- `streamId` timing guarantees
|
||||
- replay / reattach behavior
|
||||
- translation from ADK `Event` to Gemini CLI `AgentEvent`
|
||||
- top-level versus subagent event projection
|
||||
- projection of a child `AgentSession` into parent-facing tool or thread events when a subagent is embedded inside another agent
|
||||
|
||||
The minimum shape is:
|
||||
|
||||
```typescript
|
||||
interface PipelineServices {
|
||||
run(
|
||||
request: LlmRequest,
|
||||
baseModel: BaseLlm,
|
||||
stream: boolean,
|
||||
): AsyncGenerator<LlmResponse, void>;
|
||||
connect(
|
||||
request: LlmRequest,
|
||||
baseModel: BaseLlm,
|
||||
): Promise<BaseLlmConnection>;
|
||||
}
|
||||
|
||||
class GcliAgentModel extends BaseLlm {
|
||||
constructor(
|
||||
private baseModel: BaseLlm,
|
||||
private pipeline: PipelineServices,
|
||||
) {
|
||||
super({model: 'gcli-consolidated'});
|
||||
}
|
||||
|
||||
async *generateContentAsync(
|
||||
request: LlmRequest,
|
||||
stream = false,
|
||||
): AsyncGenerator<LlmResponse, void> {
|
||||
yield* this.pipeline.run(request, this.baseModel, stream);
|
||||
}
|
||||
|
||||
async connect(request: LlmRequest): Promise<BaseLlmConnection> {
|
||||
return this.pipeline.connect(request, this.baseModel);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Design rule:
|
||||
|
||||
- request mutation stays in the pipeline
|
||||
- all orchestrated agents expose the same `AgentSession` contract
|
||||
- session lifecycle, replay, approvals, and subagent projection stay in the runtime/adapters
|
||||
|
||||
`AdkAgentService` is the composition root for this architecture. It creates and resumes both top-level and subagent `AgentSession`s, builds scoped registries and message-bus instances, wires policy and approval bridges, and embeds child sessions through projection adapters rather than through a separate subagent runtime. See Appendix A for the intended initialization shape.
|
||||
|
||||
Composition rule:
|
||||
|
||||
- stateful tools are cloned or reinstantiated per session when needed; stateless tools may be shared
|
||||
- MCP discovery is shared at the manager layer but registered into session-local tool, prompt, and resource registries
|
||||
- `AgentLoopContext` is decomposed into pipeline config, tool/subagent config, session services, callback bridges, and UI projection rather than passed through as a single runtime object
|
||||
- file persistence remains Gemini CLI-owned through `GcliFileSessionService extends BaseSessionService`
|
||||
|
||||
Persistence rule:
|
||||
|
||||
- persisted history is an append-only event log plus derived app/user/session state
|
||||
- the session service provides atomic append, crash-safe recovery, and single-writer enforcement
|
||||
- rewind truncates the event log, recomputes derived state, and invalidates confirmation/resumption state past the rewind point
|
||||
|
||||
## 3.1 Authentication Flexibility
|
||||
|
||||
The CLI resolves distinct authentication flows (OAuth, ADC, Compute metadata) using standard Google libraries.
|
||||
|
||||
* **Current State:** Resolved in `packages/core/src/code_assist/oauth2.ts` based on `AuthType`.
|
||||
* OAuth (`LOGIN_WITH_GOOGLE`)
|
||||
* Compute Metadata Server (`COMPUTE_ADC`)
|
||||
* **Constraint:** Standard `Gemini` construction in `adk-js/core/src/models/google_llm.ts` still selects backend from constructor-level `apiKey` or Vertex config. It does **not** natively accept Gemini CLI's `AuthClient`-driven auth shape.
|
||||
* **Proposed ADK Mapping:** Phase 1 keeps auth in `GcliAgentModel`. The pipeline resolves refreshed credentials and injects them through `request.config.httpOptions.headers` for unary requests and `request.liveConnectConfig.httpOptions.headers` for live connections.
|
||||
* **Design position:** This is a **bridge**, not native ADK auth parity. Long-term cleanup is either:
|
||||
* a `CodeAssistLlm extends BaseLlm`, or
|
||||
* an upstream ADK auth-provider abstraction.
|
||||
|
||||
```typescript
|
||||
request.config ??= {};
|
||||
request.config.httpOptions ??= {};
|
||||
request.config.httpOptions.headers = {
|
||||
...request.config.httpOptions.headers,
|
||||
Authorization: `Bearer ${await auth.getAccessToken()}`,
|
||||
};
|
||||
```
|
||||
|
||||
## 3.2 Model Steering and Mid-Stream Injection
|
||||
|
||||
User interjections (hints) course-correct the loop mid-turn.
|
||||
|
||||
* **Current State:** Steering today is tied to the legacy loop and injection services.
|
||||
* **Proposed ADK Mapping (Next-Step Steering):** Supported in Phase 1. `beforeModelCallback` can read queued hints and mutate the next outbound request.
|
||||
* **Proposed ADK Mapping (True Real-Time Interrupt):** Still blocked. TypeScript ADK live runtime is not complete yet, and input-stream semantics are not a stable dependency.
|
||||
* **Phase 1 behavior:** user interjections are queued and prepended to the next model request at tool or turn boundaries. True in-place turn interruption remains out of scope.
|
||||
|
||||
## 3.3 State Management and Token Compaction
|
||||
|
||||
The CLI truncates large tool responses and summarizes older history to protect token budgets.
|
||||
|
||||
* **Current State:** `ChatCompressionService` in `packages/core/src/context/chatCompressionService.ts` implements reverse token budgeting and a two-phase verification loop.
|
||||
* **Proposed ADK Mapping:** Compaction remains a Gemini CLI-owned history processor invoked by the request pipeline.
|
||||
* **Design position:** Phase 1 does **not** force this onto ADK `BaseContextCompactor`. Gemini CLI compression is currently an outbound-history projection with truncation plus summary/verification sub-calls, while ADK's native compactor path is event-log-centric and mutates session history.
|
||||
* **Design choice:** In Phase 1, compaction mutates **outgoing request history only**. The persisted session event log remains canonical.
|
||||
* **Utility calls:** Compaction sub-calls use the same routing, auth, and availability pipeline as primary model calls.
|
||||
|
||||
## 3.4 Model Configuration and Hierarchical Overrides
|
||||
|
||||
Dynamic aliasing (for example, temperature scoped to specific sub-commands).
|
||||
|
||||
* **Current State:** Managed by `ModelConfigService`.
|
||||
* **Proposed ADK Mapping:** Resolution stays **request-scoped**, not session-init-scoped.
|
||||
* **Design choice:** The session stores requested model and override state. The runtime resolves the concrete model and temperature on each request, including retries, subagents, and utility calls.
|
||||
|
||||
## 3.5 Universal Policy Enforcement (TOML Rules)
|
||||
|
||||
Tiered workspace restrictions (for example, read-only tools in untrusted folders).
|
||||
|
||||
* **Current State:** Intercepted at tool scheduling time in legacy scheduler loops.
|
||||
* **Proposed ADK Mapping (Container):** Standardize on ADK `SecurityPlugin`.
|
||||
* **Proposed ADK Mapping (Decision Brain):** Implement `GcliPolicyEngineAdapter implements BasePolicyEngine`.
|
||||
* **Richer context:** The adapter is fed session/mode/subagent/MCP metadata from the runtime so current Gemini CLI policy semantics are preserved as closely as possible.
|
||||
* **Phase 1 suspension flow:** current tool approvals use existing Gemini CLI callbacks. This is intentionally **non-native** and **non-resumable across process death**.
|
||||
* **Long-term target:** move the same policy decisions onto native ADK confirmation/resumption semantics once the broader live/elicitation surface is ready.
|
||||
|
||||
```typescript
|
||||
interface GcliPolicyBridge {
|
||||
evaluate(context: ToolCallPolicyContext): Promise<PolicyCheckResult>;
|
||||
}
|
||||
|
||||
export class GcliPolicyEngineAdapter implements BasePolicyEngine {
|
||||
constructor(private readonly policyBridge: GcliPolicyBridge) {}
|
||||
|
||||
async evaluate(context: ToolCallPolicyContext): Promise<PolicyCheckResult> {
|
||||
return this.policyBridge.evaluate(context);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3.6 Telemetry and Observability (Clearcut Tracking)
|
||||
|
||||
Hardware metrics, token counts, and step durations.
|
||||
|
||||
* **Current State:** `ClearcutLogger` reads system metrics and relies on deep scheduler hooks for latency accounting.
|
||||
* **Proposed ADK Mapping:** Use a combination of:
|
||||
* passive event-stream observation, and
|
||||
* explicit runtime instrumentation where passive ADK events are insufficient.
|
||||
* **Correlation rule:** tool timing is keyed by `functionCall.id`, **not** by `event.id`.
|
||||
* **Design position:** Passive stream interception alone is not assumed to provide full parity.
|
||||
|
||||
## 3.7 Dynamic Model Routing and Configurability
|
||||
|
||||
Banning a model mid-turn, auto-routing via classifiers, and falling back dynamically without reinitializing the session.
|
||||
|
||||
* **Current State:** Managed by `ModelRouterService` and a chain of `RoutingStrategy` implementations which require the full `RoutingContext` (`history`, `request`, `AbortSignal`).
|
||||
* **Proposed ADK Mapping:** Mostly implementable now, but not “100% possible today” without bridge logic.
|
||||
* **Design choice:** The runtime constructs a proper `RoutingContext` from:
|
||||
* canonical session history
|
||||
* the pending user request
|
||||
* requested model
|
||||
* abort signal
|
||||
* **Execution point:** routing remains request-scoped and runs before final dispatch.
|
||||
* **Model banning:** treated as routing/fallback selection, not as a synthetic terminal model error.
|
||||
|
||||
## 3.8 Fallbacks and Availability Management
|
||||
|
||||
Ensuring availability by retrying or switching models when rate limits (429s) or terminal faults occur.
|
||||
|
||||
* **Current State:** Managed by `ModelAvailabilityService` and `ModelPool`.
|
||||
* **Proposed ADK Mapping (Preflight):** availability and fallback selection run before dispatch and before routing commits to a concrete model.
|
||||
* **Proposed ADK Mapping (Post-failure):** handled separately by the runtime. Availability state mutation and retry decisions are not treated as pure request preprocessing.
|
||||
* **Global Application:** utility calls use the same availability/fallback services as primary model calls.
|
||||
* **Retry safety rule:** automatic full-turn replay is allowed only before any side-effecting tool has executed. After side effects, the runtime surfaces the failure and requires explicit user action.
|
||||
* **Phase 1 transition path:** approvals and fallback prompts continue to use existing Gemini CLI callbacks. The doc treats this as an internal bridge, not as an existing standard ADK elicitation API.
|
||||
|
||||
## 3.9 State-Driven Mode Switching (Plan Mode)
|
||||
|
||||
Dynamically shifting system prompts and active tools when users switch interaction tiers (for example, Chat Mode to Plan Mode).
|
||||
|
||||
* **Current State:** Toggled via `/plan`, which changes `ApprovalMode` and related legacy scheduler behavior.
|
||||
* **Proposed ADK Mapping (Dynamic Prompt):** Use `InstructionProvider` in `LlmAgentConfig.instruction`.
|
||||
* **Proposed ADK Mapping (Dynamic Tooling):** Use a custom `BaseToolset` whose filter is derived from session mode.
|
||||
* **Single source of truth:** mode is session/runtime state derived from current approval mode; prompt, toolset, policy, routing, and UI all consume the same state.
|
||||
|
||||
```typescript
|
||||
export class GcliModeAwareToolset extends BaseToolset {
|
||||
constructor(
|
||||
private readonly chatTools: BaseTool[],
|
||||
private readonly planTools: BaseTool[],
|
||||
) {
|
||||
super(() => true);
|
||||
}
|
||||
|
||||
async getTools(context?: ReadonlyContext): Promise<BaseTool[]> {
|
||||
const isPlan = context?.state.get('plan_mode') === true;
|
||||
return isPlan ? this.planTools : this.chatTools;
|
||||
}
|
||||
|
||||
async close(): Promise<void> {}
|
||||
}
|
||||
```
|
||||
|
||||
## 3.10 Tool Output Masking
|
||||
|
||||
Managing context window efficiency by offloading bulky tool outputs (for example, shell logs and large file reads) to files.
|
||||
|
||||
* **Current State:** `ToolOutputMaskingService` in `packages/core/src/context/toolOutputMaskingService.ts`.
|
||||
* **Proposed ADK Mapping:** masking runs on **outgoing request history only**, after compaction.
|
||||
* **Design choice:** persisted session history remains the canonical event log. Masking artifacts are session-scoped files referenced from the outbound request projection.
|
||||
|
||||
---
|
||||
|
||||
# 5. Known Gaps in ADK (Gating Blockers)
|
||||
|
||||
This section highlights existing gaps in standard ADK that prevent a seamless cutover without bridge logic or upstream changes.
|
||||
|
||||
## 5.1 Real-Time User Message Injections (Aborted Turns)
|
||||
|
||||
While next-step steering is possible today using `beforeModelCallback`, true real-time interruption requires:
|
||||
|
||||
- stable input-stream support, and
|
||||
- a complete TypeScript live runtime
|
||||
|
||||
Until then, the supported behavior is queued next-step steering at model boundaries, not mid-stream interruption.
|
||||
|
||||
## 5.2 Conversation Rewind and State Reversal
|
||||
|
||||
Translating manual trajectory drops to ADK runtime state is cumbersome. While Python ADK supports rollback, TypeScript ADK does not yet support it natively.
|
||||
|
||||
* **Resolution Strategy:** Gemini CLI implements rewind in `GcliFileSessionService`, but **not** as a shallow JSON edit. Rewind truncates the event log, recomputes derived state, and invalidates any confirmation/resumption state past the rewind point.
|
||||
|
||||
## 5.3 Phase 1 Non-Goals
|
||||
|
||||
To keep the migration surface bounded, Phase 1 intentionally excludes:
|
||||
|
||||
- non-interactive surfacing of `elicitation_request` / `elicitation_response`
|
||||
- true live/bidi interruption semantics
|
||||
- native ADK confirmation/resumption parity for approvals and fallback prompts
|
||||
|
||||
---
|
||||
|
||||
# Long-Term Vision: Unification of Agents and Subagents
|
||||
|
||||
The long-term vision is that subagents and the primary agent share:
|
||||
|
||||
- the same runtime core
|
||||
- the same tool definitions
|
||||
- the same policy constraints
|
||||
- the same configuration schemas
|
||||
- the same orchestration contract: `AgentSession`
|
||||
|
||||
What may differ is the **embedding adapter**:
|
||||
|
||||
- standalone/top-level agent -> consumed directly as `AgentSession`
|
||||
- subagent embedded by a parent agent -> projected from its `AgentSession` into parent-facing tool or child-thread events
|
||||
|
||||
For SDK-first unification, Gemini CLI orchestration targets `AgentSession`, not a specific ADK tool abstraction. When a subagent must be exposed to a parent model as a tool, Gemini CLI wraps that child `AgentSession` with a `FunctionTool` or custom `BaseTool` projection. `AgentTool` remains the native ADK nested-agent option if we later want full ADK-native nested-agent semantics.
|
||||
|
||||
---
|
||||
|
||||
# Migration Sequence and Unification Checklist
|
||||
|
||||
The migration remains non-sequential, but each phase has an explicit success condition:
|
||||
|
||||
- [ ] **Non-interactive session parity** `#22699`
|
||||
output/error parity, basic replay correctness, feature-flagged rollout
|
||||
- [ ] **Interactive session parity** `#22701`
|
||||
projected event parity, approval bridge wiring, no live-interrupt claim
|
||||
- [ ] **Subagent adapter parity** `#22700`
|
||||
tool isolation, activity projection, policy/routing parity for child runs
|
||||
- [ ] **ADK session conformance** `#22974`
|
||||
`AgentSession` timing/replay guarantees preserved by the adapter
|
||||
- [ ] **Skills parity** `#22966`
|
||||
skills work through the shared runtime without special legacy paths
|
||||
- [ ] **Policy and confirmation parity** `#22964`
|
||||
phase-1 callback bridge stable; native confirmation migration scoped separately
|
||||
- [ ] **Compaction quality parity** `#22979`
|
||||
comparable summarization and truncation quality to current behavior
|
||||
|
||||
---
|
||||
|
||||
# Appendix A: Initialization Sketch
|
||||
|
||||
The main agent and subagents share the same composition root. The difference is whether the resulting `AgentSession` is consumed directly or projected into a parent session.
|
||||
|
||||
```typescript
|
||||
shared = {
|
||||
baseModel,
|
||||
modelConfigService,
|
||||
availabilityService,
|
||||
router,
|
||||
authService,
|
||||
policyEngine,
|
||||
mcpClientManager,
|
||||
skillCatalog,
|
||||
baseToolCatalog,
|
||||
sessionService,
|
||||
}
|
||||
|
||||
agentService = new AdkAgentService(shared)
|
||||
|
||||
function createSession(definition, parentSessionId?) {
|
||||
sessionRecord = sessionService.create({ definition, parentSessionId })
|
||||
registries = buildScopedRegistries(definition, parentSessionId)
|
||||
pipeline = createPipeline({
|
||||
sessionId: sessionRecord.id,
|
||||
agentId: definition.name,
|
||||
parentSessionId,
|
||||
})
|
||||
|
||||
model = new GcliAgentModel(baseModel, pipeline)
|
||||
runtime = createAdkRuntime({
|
||||
definition,
|
||||
model,
|
||||
sessionRecord,
|
||||
registries,
|
||||
policyEngine,
|
||||
})
|
||||
|
||||
return new AgentSession(new AdkAgentProtocolAdapter(runtime))
|
||||
}
|
||||
|
||||
function createSubagentTool(definition) {
|
||||
return new FunctionTool(async (input, toolContext) => {
|
||||
child = createSession(definition, toolContext.sessionId)
|
||||
stream = await child.send(userMessage(input))
|
||||
|
||||
for await (event of child.stream(stream)) {
|
||||
projectChildEventToParent(toolContext, event)
|
||||
}
|
||||
|
||||
return collectFinalText()
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Child session projection shape:
|
||||
|
||||
```typescript
|
||||
emit(tool_request({ requestId, name: definition.name, args: input }))
|
||||
|
||||
for await (event of childSession.stream({ streamId })) {
|
||||
if (event.type === 'message' || event.type === 'tool_update') {
|
||||
emit(
|
||||
tool_update({
|
||||
requestId,
|
||||
content: projectDisplayContent(event),
|
||||
}),
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
if (event.type === 'error') {
|
||||
emit(
|
||||
tool_response({
|
||||
requestId,
|
||||
name: definition.name,
|
||||
isError: true,
|
||||
content: projectErrorContent(event),
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === 'agent_end') {
|
||||
emit(
|
||||
tool_response({
|
||||
requestId,
|
||||
name: definition.name,
|
||||
content: collectFinalChildResult(),
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Only the final `tool_response` is returned to the parent model. `tool_update` remains a progress/UI projection surface.
|
||||
|
||||
Service lifetime split:
|
||||
|
||||
- shared across sessions: model config, availability, routing, auth, policy engine, MCP manager, skill catalog
|
||||
- scoped per agent/session: `AgentSession`, pipeline instance, tool/prompt/resource registries, derived message bus
|
||||
- scoped per invocation: shell process, MCP request, confirmation continuation, tool progress updates
|
||||
|
||||
---
|
||||
|
||||
# Status of this document approvals table {#status-of-this-document}
|
||||
|
||||
| #begin-approvals-addon-section See [go/g3a-approvals](http://goto.google.com/g3a-approvals) for instructions on adding reviewers. |
|
||||
| :---: |
|
||||
.
|
||||
@@ -0,0 +1,170 @@
|
||||
# M5 Implementation Details
|
||||
|
||||
Companion to `replatforming_design.md`. Per-flow walkthroughs, the routing
|
||||
and masking integration recipes, and the seam-level acceptance criteria
|
||||
for the early scaffolding PRs. Anything strategic goes back into the
|
||||
design doc; this file is the mechanical reference.
|
||||
|
||||
## Surface Phases
|
||||
|
||||
M5 lands by runnable surface, not by isolated subsystem. Each phase wires
|
||||
one call site end-to-end before the next starts.
|
||||
|
||||
**Phase A — Non-interactive.** Wires the ADK runtime through the
|
||||
non-interactive AgentSession path. Translator, `GcliAgentModel` dispatch
|
||||
through `ContentGenerator`, `GcliRoutingProcessor`, 429 retry +
|
||||
`handleFallback` + `ModelAvailabilityService` (silent-policy branch swaps
|
||||
models without a UI prompt; non-silent intents are unreachable until the
|
||||
TUI handler lands), abort, scheduler-backed tool execution,
|
||||
`ToolOutputMaskingProcessor`, `LoopDetectionAdkPlugin`, native
|
||||
`MCPToolset` (`tools/list` + `tools/call`).
|
||||
|
||||
**Phase B — Local subagents.** Same `AdkAgentProtocol` as Phase A; new
|
||||
work is subagent-only behavior: factory wiring at
|
||||
[`subagent-tool-wrapper.ts:97`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/agents/subagent-tool-wrapper.ts#L97), `complete_task` terminator, grace-period
|
||||
recovery turn, scoped execution wrappers (workspace, memory-inbox,
|
||||
auto-memory-extraction), and the `onWaitingForConfirmation` activity
|
||||
signal. Event propagation to the parent is already an M4 property — no
|
||||
new wiring.
|
||||
|
||||
**Phase C — Interactive.** Wires the TUI on top of the proven runtime.
|
||||
Stream rendering parity, `session_update`, `HookBridgePlugin` + lifecycle
|
||||
hooks + `Notification`, plan mode (`InstructionProvider` + mode-aware
|
||||
tools), user steering injection, the TUI-registered
|
||||
`fallbackModelHandler` callback (unlocks `retry_once`/`stop`/`upgrade`),
|
||||
and `/rewind`. **All `/slash commands` live outside the protocol
|
||||
layer** — `/rewind` is a slash-command/runtime-adapter concern.
|
||||
|
||||
## Walkthroughs
|
||||
|
||||
**Typical message:** user input → `AdkAgentProtocol.send` → emit `agent_start` (`BeforeAgent` hook fires) → emit deterministic `session_update{model}` once `config.getModel()` is resolved (do NOT wait on first translator output — first output may be `error` or `usage`) → `Runner.runAsync` → request processors run in one explicit ordered list (ADK defaults through `CONTENT_REQUEST_PROCESSOR`, then `ToolOutputMaskingProcessor`, then `GcliRoutingProcessor`, then the remaining ADK defaults; supplying `config.requestProcessors` replaces ADK's default list, so we must include the defaults ourselves) → tool preprocessing → `beforeModelCallback` runs guards + `BeforeModel` / `BeforeToolSelection` hooks → `GcliAgentModel.generateContentAsync` dispatches through `config.getContentGenerator().generateContentStream(...)` → translator emits `message` + `tool_request` events → `toAdkTool.execute` calls `scheduler.schedule` (`BeforeTool` hook, approval, exec, per-call truncation at 40k chars via [`tool-executor.ts:196-292`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/scheduler/tool-executor.ts#L196-L292), `AfterTool` hook) → feeds back → loop iterates → final `message` events → `agent_end` (`AfterAgent` hook fires). ADK currently dispatches function calls sequentially within a turn ([`functions.ts:345-491`](https://github.com/google/adk-js/blob/main/core/src/agents/functions.ts#L345-L491)), and `scheduler.schedule` queues concurrent callers — no interleaving risk.
|
||||
|
||||
**429 fallback:** `GcliAgentModel` owns the same retry loop shape that `GeminiChat` owns today: the content-generator call is inside a `retryWithBackoff` `apiCall` closure, `onPersistent429` calls `handleFallback(config, currentModel, authType, error)`, the handler applies the existing `ModelAvailabilityService` transition and may call `config.activateFallbackMode(...)` for `retry_always`, then the retry loop resets attempts and re-runs the same closure. Do not throw to ADK Runner for retry; `LlmAgent.runAndHandleError` converts thrown model errors into error responses/events. Fallback retry must re-resolve the same current model/config path used by `GcliRoutingProcessor`, and the parity test must cover a fallback that changes the concrete model so we do not dispatch with stale model-specific config or tool declarations. The TUI's `fallbackModelHandler` callback ([`handler.ts:89`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/fallback/handler.ts#L89)) is what unlocks the non-silent intents (`retry_once`, `stop`, `upgrade`); non-interactive runs without one and `handleFallback` returns `null` after silent transitions — that's by design, not a Phase A limitation.
|
||||
|
||||
**Loop detection:** `LoopDetectionAdkPlugin.onEventCallback` feeds each `partial:true` delta to `LoopDetectionService.addAndCheck`, and ignores the `partial:false` consolidated event so the accumulated buffer isn't double-counted. On terminate the plugin calls `protocol.abort()` with reason `LOOP_DETECTED`; the protocol emits a synthetic `error{_meta.code:'LOOP_DETECTED'}` event and the abort propagates through `InvocationContext.abortSignal` into `GcliAgentModel.generateContentAsync`. The plugin does NOT mutate the in-flight ADK event and does NOT abort inside `onEventCallback` — that would drop the event before yield ([`runner.ts:332-334`](https://github.com/google/adk-js/blob/main/core/src/runner/runner.ts#L332-L334)).
|
||||
|
||||
**`/rewind`:** slash command → slash-command/runtime-adapter (NOT an `AgentProtocol` method) → `ChatRecordingService.rewindTo(id)` truncates `ConversationRecord.messages` ([`chatRecordingService.ts:743-762`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/services/chatRecordingService.ts#L743-L762)) → `sessionService.deleteSession(...)` drops the ADK session → emits `session_update`. Next `send()` calls `sessionService.createSession(...)` then `appendEvent`s each retained `ConversationRecord` message converted to an ADK `Event` (`InMemorySessionService.createSession` has no seed-events parameter — [`in_memory_session_service.ts:54-67`](https://github.com/google/adk-js/blob/main/core/src/sessions/in_memory_session_service.ts#L54-L67)). Conversion fields: ISO timestamp → epoch ms, `type:'user'|'gemini'` → `author`, synthesized deterministic `invocationId` (ConversationRecord has none), `ToolCallRecord` → `FunctionCall` + `FunctionResponse` Parts, thoughts and `TokensSummary` → `actions.customMetadata`. Rewind-injected events are non-partial by construction. Wired in Phase C alongside the rest of the interactive surface.
|
||||
|
||||
**Subagent invocation:** Parent ADK `LlmAgent` dispatches an M4 subagent tool → [`subagent-tool-wrapper.ts:97`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/agents/subagent-tool-wrapper.ts#L97) calls the session factory (today it constructs `LocalSubagentInvocation` directly — [`local-invocation.ts:48`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/agents/local-invocation.ts#L48)) → factory returns an ADK-backed `AgentProtocol` when the flag is on, legacy `LocalSubagentInvocation` otherwise → child constructs its own independent `Runner` with its own `LlmAgent`, plugins, tools, and `GcliSessionService` → child events stream back through the M4 wrapper to the parent as `tool_update`s and a terminal `tool_response`. Both implementations satisfy `AgentProtocol`; the factory selects between them. Existing parent-side telemetry on [`subagent-tool.ts:224`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/agents/subagent-tool.ts#L224) / [`subagent-tool-wrapper.ts:75`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/agents/subagent-tool-wrapper.ts#L75) is the correlation point — no aggregation changes for M5.
|
||||
|
||||
## Model routing
|
||||
|
||||
ADK ships a `RoutedLlm` that can sit in front of a `BaseLlm` and pick a model
|
||||
per call. M5 does not use it. Gemini CLI already owns the routing stack
|
||||
(`ModelRouterService`, sequence-sticky model, `applyModelSelection`,
|
||||
`ModelConfigService`, `ModelAvailabilityService`, fallback config), and that
|
||||
behavior has to stay byte-for-byte the same through the deprecation window.
|
||||
Bolting a second routing layer on top would not preserve behavior; it would
|
||||
give us two places to keep in sync.
|
||||
|
||||
The integration is a custom ADK **request processor**, not a step inside
|
||||
`BaseLlm`. ADK runs request processors at [`llm_agent.ts:731-743`](https://github.com/google/adk-js/blob/main/core/src/agents/llm_agent.ts#L731-L743) before
|
||||
tool preprocessing ([`llm_agent.ts:746`](https://github.com/google/adk-js/blob/main/core/src/agents/llm_agent.ts#L746)) and before
|
||||
`BaseLlm.generateContentAsync` ([`llm_agent.ts:1024`](https://github.com/google/adk-js/blob/main/core/src/agents/llm_agent.ts#L1024)). Selecting the model
|
||||
inside `generateContentAsync` is too late — tool declarations and
|
||||
generation config have already been materialized from whatever model was
|
||||
on the `LlmRequest` when it arrived.
|
||||
|
||||
Per-call flow inside `GcliRoutingProcessor.runAsync(invocationContext, llmRequest)`:
|
||||
|
||||
1. Build a `RoutingContext` from the masked `llmRequest.contents` + `invocationContext.session` + `Config` + `abortSignal`. Same shape `GeminiClient.processTurn` constructs today.
|
||||
2. If the current sequence model is set, keep using it for this tool-call sequence. Otherwise call `config.getModelRouterService().route(routingContext)`. Sequence-sticky state moves to `Config` (or its successor) in PR #14 with named reset points — today it lives on `GeminiClient` ([`client.ts:101`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/core/client.ts#L101), [`:339`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/core/client.ts#L339)).
|
||||
3. Run the selection through the existing path: `applyModelSelection(...)`, `ModelConfigService`, `ModelAvailabilityService`. Active model, availability state, fallback config, and resolved generation config stay consistent with the legacy run loop. Set the current sequence model.
|
||||
4. Rewrite `llmRequest.model`, `llmRequest.config` (system instructions, generation config), and the tool subset in place. ADK's tool preprocessing then sees the resolved model.
|
||||
|
||||
`GcliAgentModel.generateContentAsync` is the content-generator dispatcher plus the legacy-compatible fallback retry loop:
|
||||
|
||||
5. Check abort signal.
|
||||
6. Map ADK `LlmRequest` → `GenerateContentParameters` and call `config.getContentGenerator().generateContentStream(...)`. Auth (login-with-Google, Gemini API key, Vertex, ADC/compute, gateway, fake responses, logging/recording wrappers) lives inside the content generator implementation — no header injection in this layer.
|
||||
7. Stream the result back to ADK, propagating abort. On 429 / persistent quota errors, stay inside the local `retryWithBackoff` loop: call `handleFallback(...)`, let it apply the existing availability/fallback state transition, reset attempts when it returns a retry intent, and re-enter the content-generator call path. A thrown model error is terminal for this ADK iteration unless an ADK `onModelError` callback returns a replacement response; Runner does not automatically re-run request processors.
|
||||
|
||||
## Availability service integration
|
||||
|
||||
`ModelAvailabilityService` is a self-contained state machine — no Config in its constructor, no Config reads in its methods, just a `Map<ModelId, HealthState>` and the methods that mutate it ([`modelAvailabilityService.ts:41-137`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/availability/modelAvailabilityService.ts#L41-L137)). The class is reusable as-is under ADK. The integration *around* it is Config-driven, and that's what has to be ported.
|
||||
|
||||
What feeds the service: `handleFallback` reads the policy chain from Config (`resolvePolicyChain(config)`), takes the failed model as a parameter, and calls `availability.selectFirstAvailable(candidates)`. The failed model comes from [`client.ts:1080`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/core/client.ts#L1080): `const active = this.config.getActiveModel()`. The retry loop re-polls Config between attempts so any mid-loop fallback mutation is picked up cleanly.
|
||||
|
||||
What resets the service: two triggers, both outside the service. `reset()` fires from `config.setModel()` ([`config.ts:1813`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/config/config.ts#L1813)), itself triggered by `config.activateFallbackMode()` after a `retry_always` decision. `resetTurn()` fires at the turn boundary — call site not yet verified, probably `client.ts` or `GeminiChat`.
|
||||
|
||||
What `GcliAgentModel` has to do: replicate the [`client.ts:1072-1110`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/core/client.ts#L1072-L1110) retry pattern faithfully — pre-call read `config.getActiveModel()`, call `handleFallback(config, currentAttemptModel, authType, error)` on persistent 429, re-poll Config before the next attempt. The `resetTurn` trigger lands at whatever ADK boundary corresponds to "new turn started" — ADK doesn't know about it, so it's an explicit wire.
|
||||
|
||||
Subagent note: `AgentLoopContext.config` is shared by reference between parent and child, so parent and subagent share the same `ModelAvailabilityService.health` map and the same `_activeModel`. A subagent fallback mutates parent state. This is legacy behavior, not an ADK regression — flagging it as a documented property, not a Phase A task.
|
||||
|
||||
Open Questions: locate the `resetTurn` call site in legacy and map it to an ADK trigger.
|
||||
|
||||
## Tool output masking
|
||||
|
||||
Two mechanisms, only one is new under ADK:
|
||||
|
||||
**Per-call truncation (already done by the scheduler).** When a tool returns, [`tool-executor.ts:196-292`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/scheduler/tool-executor.ts#L196-L292) truncates content > 40k chars for shell and single-text-part MCP tools, writes the full output to `<projectTempDir>/.../<toolName>_<callId>_<random>.txt`, and returns a snippet + file path pointer. This rides for free in M5 because `toAdkTool` calls the scheduler.
|
||||
|
||||
**Per-turn batch masking (this is what's new).** Today [`client.ts:637`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/core/client.ts#L637) calls `tryMaskToolOutputs(getHistory())` inside `processTurn`, after compression and before the model call. `ToolOutputMaskingService.mask` walks the whole history, applies a 50k-protection + 30k-prunable-gate sliding window, writes full content to disk, replaces `functionResponse.response` with a `<tool_output_masked>...</tool_output_masked>` preview + pointer ([`toolOutputMaskingService.ts:70-272`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/context/toolOutputMaskingService.ts#L70-L272)). Then `setHistory` syncs to `ChatRecordingService.updateMessagesFromHistory` ([`chatRecordingService.ts:768`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/services/chatRecordingService.ts#L768)).
|
||||
|
||||
Under ADK this becomes a custom `RequestProcessor` (`ToolOutputMaskingProcessor`) registered with `LlmAgent` after ADK has built `llmRequest.contents` from `session.events` and before `GcliRoutingProcessor` reads those contents. The processor:
|
||||
|
||||
1. Reads `llmRequest.contents` (produced by ADK's `ContentRequestProcessor` from `session.events`).
|
||||
2. Calls `ToolOutputMaskingService.mask(llmRequest.contents, config)`. The algorithm is unchanged.
|
||||
3. Applies each masked `functionResponse` back to the matching live ADK `Session.events` part by stable event/part identity. Do not use `session.events[i]` ⇔ `llmRequest.contents[i]`; ADK filters, rearranges, and clone-deeps content while building the request ([`content_processor_utils.ts:72-84`](https://github.com/google/adk-js/blob/main/core/src/agents/processors/content_processor_utils.ts#L72-L84)).
|
||||
4. Writes the masked `llmRequest.contents` back so the model call and downstream routing see the masked view.
|
||||
5. Calls `ChatRecordingService.updateMessagesFromHistory(masked)` so the persisted transcript matches.
|
||||
|
||||
`isAlreadyMasked` keeps it idempotent across turns. The disk file path stays the same across runs once we drop the `Math.random()` suffix in the filename (also part of PR #21).
|
||||
|
||||
## GcliSessionService
|
||||
|
||||
`InMemorySessionService.getSession` does `cloneDeep(session)` ([`in_memory_session_service.ts:88-131`](https://github.com/google/adk-js/blob/main/core/src/sessions/in_memory_session_service.ts#L88-L131)), so the runner operates on a clone of the stored session. Any mutation to `invocationContext.session.events[i]` only persists for the duration of `runAsync`; the next turn fetches a fresh clone. Subclassing it is not enough: its `sessions` store is private ([`in_memory_session_service.ts:40`](https://github.com/google/adk-js/blob/main/core/src/sessions/in_memory_session_service.ts#L40)), and returning the stored reference from `getSession` would make inherited `appendEvent` push the same event twice ([`in_memory_session_service.ts:173-227`](https://github.com/google/adk-js/blob/main/core/src/sessions/in_memory_session_service.ts#L173-L227)).
|
||||
|
||||
`GcliSessionService` extends `BaseSessionService`, not `InMemorySessionService`. It owns an authoritative live-session map keyed by `appName/userId/sessionId`. Plugins and request processors that mutate session events (masking is the primary use case) get persistence across turns.
|
||||
|
||||
```ts
|
||||
class GcliSessionService extends BaseSessionService {
|
||||
private readonly liveSessions = new Map<string, Session>();
|
||||
|
||||
private key(appName: string, userId: string, sessionId: string): string {
|
||||
return `${appName}\0${userId}\0${sessionId}`;
|
||||
}
|
||||
|
||||
override async createSession(req: CreateSessionRequest): Promise<Session> {
|
||||
const session = createSession({
|
||||
id: req.sessionId ?? randomUUID(),
|
||||
appName: req.appName,
|
||||
userId: req.userId,
|
||||
state: req.state ?? {},
|
||||
events: [],
|
||||
lastUpdateTime: Date.now(),
|
||||
});
|
||||
this.liveSessions.set(this.key(req.appName, req.userId, session.id), session);
|
||||
return session;
|
||||
}
|
||||
|
||||
override async getSession(req: GetSessionRequest): Promise<Session | undefined> {
|
||||
const session = this.liveSessions.get(
|
||||
this.key(req.appName, req.userId, req.sessionId),
|
||||
);
|
||||
if (!session || !req.config) return session;
|
||||
// If a caller asks for a filtered view, return a copy so the caller cannot
|
||||
// mutate a truncated live session.
|
||||
return copyWithFilteredEvents(session, req.config);
|
||||
}
|
||||
|
||||
override async appendEvent(req: AppendEventRequest): Promise<Event> {
|
||||
const event = await super.appendEvent(req); // pushes once, skips partials
|
||||
req.session.lastUpdateTime = event.timestamp;
|
||||
return event;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`listSessions` and `deleteSession` operate on `liveSessions`; they do not delegate to an inner `InMemorySessionService` whose state would drift once `appendEvent` stops delegating. The runner calls `getSession` without `GetSessionConfig`, but preserving filtered-copy behavior for `numRecentEvents` / `afterTimestamp` keeps the service compatible with other ADK callers. If any M5 plugin uses ADK app/user state prefixes, PR #20 ports `InMemorySessionService`'s app-state/user-state handling too; otherwise the service intentionally supports only session-local state.
|
||||
|
||||
## PR seam summary
|
||||
|
||||
Seam names and acceptance criteria for the foundation PRs. Aside from the `GcliSessionService` code shape above, pseudo-code stubs are intentionally omitted because they drift from real code; the per-PR description owns the implementation specifics.
|
||||
|
||||
**PR #1 — Scaffold.** Add `packages/core/src/adk-agent/`. Export `AdkAgentProtocol` (skeleton implementing `AgentProtocol`), `GcliSessionService` (`BaseSessionService` implementation with live session storage), and named seam files for `GcliAgentModel`, `GcliRoutingProcessor`, `ToolOutputMaskingProcessor`, `HookBridgePlugin`, `LoopDetectionAdkPlugin`, `toAdkTool`. All bodies are TODO; compile green; no behavior change. Acceptance: module compiles, file structure matches the architectural sketch, all symbols importable.
|
||||
|
||||
**PR #2 — Flag + factory.** Add `experimental.adk.runtimeEnabled` to the settings schema. Implement the session factory: builds an `AdkAgentProtocol` when the runtime flag is on, otherwise the existing legacy implementation. Define precedence with `experimental.adk.agentSessionNoninteractiveEnabled` (the new flag is the runtime selector; the old one continues to gate the non-interactive code path). Acceptance: flag visible in settings, factory unit-tested with both flag states, no production caller wired yet.
|
||||
|
||||
**PR #3 — Event-type cleanup.** Delete unused `elicitation_request`, `elicitation_response`, and `elicitations` references that the protocol surface no longer uses.
|
||||
|
||||
(`/rewind` is intentionally NOT a foundation PR. It stays in the slash-command/runtime-adapter layer and is wired during Phase C — see Surface Phases above.)
|
||||
@@ -0,0 +1,472 @@
|
||||
# Gemini CLI Replatforming Design
|
||||
|
||||
## Goals
|
||||
|
||||
1. **Fast ADK Replatforming:** Transition the default Gemini CLI agent to use
|
||||
the ADK execution engine to reduce maintenance overhead on the legacy run
|
||||
loop.
|
||||
2. **Reuse Existing Frameworks:** Minimize UI and internal API churn by keeping
|
||||
existing callback mechanisms and frameworks. Specifically, we will _not_
|
||||
rewrite the existing tool approval and model fallback mechanisms to use ADK's
|
||||
`elicitation_request`/`elicitation_response` events. We will reuse the
|
||||
existing message bus and scheduler integrations.
|
||||
3. **Unified Core Engine:** Ensure the main interactive chat, non-interactive
|
||||
mode, and subagents execute via the exact same core ADK engine.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
1. **No Public SDK Readiness:** We are no longer refactoring the core codebase
|
||||
to expose a polished, public-facing SDK.
|
||||
2. **No BYOA / Multi-Agent Orchestration:** We are explicitly not building
|
||||
extensibility for third-party agents or external runtime orchestration.
|
||||
3. **No UI Modernization for Elicitations:** We will not refactor the React TUI
|
||||
or underlying message bus to adopt standard ADK elicitation events. Tool
|
||||
approvals and system interrupts will continue to use the legacy callback
|
||||
bridges.
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
Where we want feedback. Each links to the section with more context — please weigh in there.
|
||||
|
||||
**Milestone 3 — TUI behind `AgentSession`:** scope to be fleshed out by @Jacob Richman; tracked in [#22702](https://github.com/google-gemini/gemini-cli/issues/22702). Exemplar areas listed in [Transition Plan](#transition-plan).
|
||||
|
||||
**Milestone 5 — ADK Execution Engine:**
|
||||
|
||||
- Loop detection: which event stream feeds the detector, how does soft-recovery get into the next request, and which terminate mechanism (abort / throw / replacement response)? See [Loop detection](#loop-detection).
|
||||
- MCP: upstream the missing surface into `MCPToolset`, or keep `McpClient` and pass its outputs as functional tools? See [MCP](#mcp).
|
||||
- Slash commands: live outside the protocol layer — scope of the slash-command/runtime-adapter layer TBD. See [Slash commands](#slash-commands).
|
||||
|
||||
---
|
||||
|
||||
## Milestones
|
||||
|
||||
1. AgentSession Interface Create `AgentSession` abstraction flexible enough to
|
||||
support the legacy runtime and ADK.
|
||||
2. Adapting the Non-Interactive Runtime Adapt the existing non-interactive CLI
|
||||
to conform to the new `AgentSession` API.
|
||||
3. Initial TUI Adaptation & Session Creation Move the main agent behind the
|
||||
`AgentSession` interface, adapting the TUI via `useAgentStream` and
|
||||
`LegacyAgentProtocol` while reusing the message bus. **Includes implementing
|
||||
the Agent Creation Factory/Function.**
|
||||
4. Subagent Orchestration Decouple subagents from the legacy runtime by wrapping
|
||||
them in the `AgentSession` interface.
|
||||
5. ADK Execution Engine Build the unified ADK agent conforming to
|
||||
`AgentSession`, handling all modes (Main, Non-Interactive, Subagents).
|
||||
Adopts ADK's loop and request processors; retains gemini-cli's scheduler,
|
||||
routing, fallback, masking, hooks, compression, recording, and loop
|
||||
detection. See [Detailed Design §5](#5-milestone-5-adk-execution-engine).
|
||||
|
||||
## Progress
|
||||
|
||||
| Milestone | Owner | Status | Relevant PRs / Branches | Time Estimate |
|
||||
| :------------------------------------------- | :------------------------------- | :---------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------ |
|
||||
| 1. AgentSession Interface | Adam Weidman, Michael Bleigh | ✅ Complete | [PR #22270](https://github.com/google-gemini/gemini-cli/pull/22270), [PR #23159](https://github.com/google-gemini/gemini-cli/pull/23159), [PR #23548](https://github.com/google-gemini/gemini-cli/pull/23548) | - |
|
||||
| 2. Adapting the Non-Interactive Runtime | Adam Weidman | ✅ Complete | [PR #22984](https://github.com/google-gemini/gemini-cli/pull/22984), [PR #22985](https://github.com/google-gemini/gemini-cli/pull/22985), [PR #22986](https://github.com/google-gemini/gemini-cli/pull/22986), [PR #24439](https://github.com/google-gemini/gemini-cli/pull/24439), [PR #22987](https://github.com/google-gemini/gemini-cli/pull/22987) | - |
|
||||
| 3. Initial TUI Adaptation & Session Creation | Michael Bleigh | 🚧 WIP | [PR #24275](https://github.com/google-gemini/gemini-cli/pull/24275), [PR #24287](https://github.com/google-gemini/gemini-cli/pull/24287), [PR #24292](https://github.com/google-gemini/gemini-cli/pull/24292), [PR #24297](https://github.com/google-gemini/gemini-cli/pull/24297), [Issue #25046](https://github.com/google-gemini/gemini-cli/issues/25046) | 2-3 weeks |
|
||||
| 4. Subagent Orchestration | Adam Weidman | 🚧 WIP | [PR #25302](https://github.com/google-gemini/gemini-cli/pull/25302), [PR #25303](https://github.com/google-gemini/gemini-cli/pull/25303)<br>Branches: `agent-session/local-invocation`, `agent-session/remote-invocation`, `agent-session/agent-tool` | 1 week |
|
||||
| 5. ADK Execution Engine | Adam Weidman, Alexey Kalenkevich | 🚧 WIP | Alexey Prototype: `eeb9301`. See [M5 Execution Plan](#m5-execution-plan) below for the PR breakdown. | 4 weeks |
|
||||
| 6. Testing / Validation / Bug fixing | Adam Weidman, Alexey Kalenkevich | ⏳ Upcoming | Folded into each phase below: non-interactive first, then subagents, then interactive. See [M5 Execution Plan](#m5-execution-plan). | 2 weeks |
|
||||
|
||||
_(Note: Previous milestones related to "Unified Elicitations" and "Adopt ADK
|
||||
Primitives" have been removed as per the updated Non-Goals)._
|
||||
|
||||
---
|
||||
|
||||
## Detailed Design
|
||||
|
||||
### 1. Milestone 1: AgentSession Interface
|
||||
|
||||
The `AgentSession` interface is the core abstraction that decouples the TUI from
|
||||
the specific agent implementation by proposing a purely event-driven loop
|
||||
boundary (`AgentEvent` streams). _For full rationale and design, see the
|
||||
[Gemini CLI Agents design document](https://docs.google.com/document/d/1Zv2_VuVNc-PtsFIU5HYApdmaC3EyK5_fNeivYUtL1fs/edit?tab=t.0#heading=h.ok5bx3z7fmr8)._
|
||||
|
||||
### 2. Milestone 2: Adapting the Non-Interactive Runtime
|
||||
|
||||
The non-interactive runtime conforms to the `AgentSession` API via a legacy
|
||||
protocol adapter. This adapter translates internal message bus and scheduler
|
||||
events into standard agent event streams. Because this mode does not require
|
||||
user interaction, it bypasses complex UI features like tool approvals.
|
||||
|
||||
This capability is enabled via the following experimental setting in
|
||||
`.gemini/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"experimental": {
|
||||
"adk": {
|
||||
"agentSessionNoninteractiveEnabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Milestone 3: Initial TUI Adaptation & Session Creation
|
||||
|
||||
Adapting the interactive TUI to consume the `AgentSession` interface is a
|
||||
complex, multi-layered effort. The core mechanism is an event-streaming hook
|
||||
that subscribes to the session.
|
||||
|
||||
#### Current Event Handling State (Legacy Mechanism)
|
||||
|
||||
The legacy system relies on a central `Scheduler` and a `MessageBus` to
|
||||
orchestrate tool execution and approvals. This is the mechanism we are retaining
|
||||
to avoid a full UI refactor:
|
||||
|
||||
- **Tool Approvals**: When a tool requires user confirmation, the scheduler
|
||||
calls `resolveConfirmation`
|
||||
([`scheduler.ts:L663`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/scheduler/scheduler.ts#L663))
|
||||
and updates the tool's status to `AwaitingApproval` in `confirmation.ts`
|
||||
([`confirmation.ts:L162`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/scheduler/confirmation.ts#L162)).
|
||||
- **UI Notification**: The `SchedulerStateManager` publishes a
|
||||
`TOOL_CALLS_UPDATE` event on the message bus
|
||||
([`state-manager.ts:L254`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/scheduler/state-manager.ts#L254)).
|
||||
The UI hook `useToolScheduler.ts` subscribes to this event to update the React
|
||||
state
|
||||
([`useToolScheduler.ts:L178`](https://github.com/google-gemini/gemini-cli/blob/main/packages/cli/src/ui/hooks/useToolScheduler.ts#L178)).
|
||||
- **User Interaction**: If any tool requires approval, the TUI transitions to a
|
||||
waiting state
|
||||
([`useGeminiStream.ts:L174`](https://github.com/google-gemini/gemini-cli/blob/main/packages/cli/src/ui/hooks/useGeminiStream.ts#L174))
|
||||
and renders the `ToolConfirmationQueue.tsx` component.
|
||||
- **Response Loop**: Once the user makes a decision, `ToolActionsContext.tsx`
|
||||
publishes a `TOOL_CONFIRMATION_RESPONSE`
|
||||
([`ToolActionsContext.tsx:L150`](https://github.com/google-gemini/gemini-cli/blob/main/packages/cli/src/ui/contexts/ToolActionsContext.tsx#L150)).
|
||||
The scheduler, which is blocked in `waitForConfirmation`
|
||||
([`confirmation.ts:L168`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/scheduler/confirmation.ts#L168)),
|
||||
receives the response via the message bus and resumes execution.
|
||||
|
||||
#### Transition Plan
|
||||
|
||||
The interactive `AgentSession` adapter continues to leverage the legacy message bus and scheduler rather than migrating tool approvals to ADK elicitations — minimizing UI churn. The adapter subscribes to the message bus and translates read-only observational events (`tool_update`, `message`, `usage`) into `AgentEvent` streams; interactive/blocking events (tool approvals) stay on the legacy callback path.
|
||||
|
||||
Work areas (scope is settled; implementation within each owned by @Jacob Richman in [#22702](https://github.com/google-gemini/gemini-cli/issues/22702)):
|
||||
|
||||
- **Event Parity:** standard tool calls, MCP tool calls, and subagent display.
|
||||
- **UI Coalescence:** using `agent_start` / `agent_end` for robust UI state management.
|
||||
- **Tool-Controlled Display:** per-tool render variants (`FileDiff`, `TodoList`, `AnsiOutput`, etc.) passing through `AgentEvent`, plus tool-triggered out-of-band display state (e.g., IDE diff overlay, plan-mode toggle).
|
||||
- **Command Routing:** client-initiated commands (slash commands) routed down to the session; ensuring `abort` works properly.
|
||||
- **System Notices:** generic notice events for system notifications.
|
||||
|
||||
### 4. Milestone 4: Subagent Orchestration
|
||||
|
||||
Subagents invoke both local and remote execution via a unified `AgentTool` that
|
||||
wraps them in `AgentSession` instances. This pattern translates low-level
|
||||
execution events into standard `AgentEvent`s for the parent session.
|
||||
|
||||
### 5. Milestone 5: ADK Execution Engine
|
||||
|
||||
Replaces the legacy `GeminiClient` + `Turn` loop with an ADK `Runner` + `LlmAgent`
|
||||
behind the existing `AgentSession` interface. ADK contributes the agent loop;
|
||||
gemini-cli contributes everything else.
|
||||
|
||||
Reference implementation:
|
||||
[commit `eeb9301a`](https://github.com/google-gemini/gemini-cli/commit/eeb9301a489a1609f7d74e6c61569d82c5742821).
|
||||
|
||||
#### Approach: progressive integration
|
||||
|
||||
Start by landing the initial scaffolding: the `adk-agent/` module, the runtime
|
||||
flag, the top-level session factory, and the event-type cleanup. These changes
|
||||
should compile green, keep the flag off by default, and avoid behavior changes.
|
||||
|
||||
Once the initial scaffolding lands, M5 proceeds by **runnable surface**, not
|
||||
by isolated subsystem. Each phase wires one call site end-to-end before
|
||||
moving to the next:
|
||||
|
||||
1. **Non-interactive first.** Run the ADK loop end-to-end without UI. This phase brings up the translator, model dispatch through `GcliAgentModel`, routing, scheduler-backed tool execution, output masking, abort propagation, loop detection, and native MCP. The 429 retry path lands here too: `handleFallback` silently swaps to an available model when the policy allows. The TUI-side prompt that lets a user pick `retry_once` or `stop` is a separate callback registered in Phase C; non-interactive runs without it.
|
||||
|
||||
2. **Local subagents second.** Same ADK runtime — no second implementation. The new work is the subagent-only behavior that doesn't exist on the top-level loop: `complete_task` as a mandatory terminator, grace-period retry, scoped workspace/memory wrappers, and the confirmation-waiting activity signal. Event propagation back to the parent already works through the M4 wrapper.
|
||||
|
||||
3. **Interactive last.** Wire the TUI once the runtime is proven. Stream rendering, `session_update` behavior, hooks and notifications, plan mode, between-turn steering, the TUI fallback prompt callback, and `/rewind`. All slash commands live outside the protocol layer — `/rewind` is owned by the slash-command/runtime-adapter layer, not `AgentProtocol`.
|
||||
|
||||
#### Architectural sketch
|
||||
|
||||
```
|
||||
User input
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ AgentSession │ existing wrapper (agent-session.ts:18)
|
||||
└──────────┬───────────┘ SessionStart / SessionEnd hooks
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ AdkAgentProtocol │ send/subscribe/abort/events
|
||||
│ │ agent_start/end → Before/AfterAgent hooks
|
||||
└──────────┬───────────┘
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ GeminiCliAgent │ extends BaseAgent
|
||||
│ .runAsyncImpl(ctx) │ yields ADK Events
|
||||
└──────────┬───────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ ADK Runner with: │
|
||||
│ • GcliSessionService (live event state) │
|
||||
│ • RequestProcessors (ordered): │
|
||||
│ ADK defaults through content build │
|
||||
│ ToolOutputMaskingProcessor │
|
||||
│ GcliRoutingProcessor │
|
||||
│ remaining ADK defaults │
|
||||
│ • Plugins: │
|
||||
│ MaxTurns / TokenLimit / MaxTime │
|
||||
│ HookBridgePlugin │
|
||||
│ LoopDetectionAdkPlugin │
|
||||
│ Steering injection │
|
||||
│ │
|
||||
│ LlmAgent loop: call → parse → dispatch → │
|
||||
│ feed back → iterate │
|
||||
└────────┬──────────────────────┬──────────────┘
|
||||
│ │
|
||||
┌────────▼─────────┐ ┌─────────▼──────────────┐
|
||||
│ GcliAgentModel │ │ toAdkTool │
|
||||
│ • dispatch │ │ scheduler.schedule({ │
|
||||
│ • fallback retry │ │ callId, name, args │
|
||||
│ • abort │ │ }, signal) │
|
||||
│ │ │ │ → BeforeTool hook │
|
||||
│ ▼ │ │ → MessageBus policy │
|
||||
│ ContentGenerator │ │ → tool exec │
|
||||
│ (auth lives here)│ │ → AfterTool hook │
|
||||
└──────────────────┘ │ → CompletedToolCall[] │
|
||||
└─────────────────────────┘
|
||||
|
||||
Runner yields Events → adk-event-translator → AgentEvent[]
|
||||
→ AdkAgentProtocol._emit → subscribers (TUI, non-interactive, etc.)
|
||||
└─ ChatRecordingService.recordMessage on final (non-partial) events:
|
||||
user messages, consolidated model responses, usage, tool calls
|
||||
```
|
||||
|
||||
Per-flow walkthroughs (typical message, 429 fallback, loop detection,
|
||||
`/rewind`, subagent invocation) and scaffold sketches live in
|
||||
[`implementation_details.md`](./implementation_details.md).
|
||||
|
||||
#### Session entry point
|
||||
|
||||
`AgentProtocol` is the interface ([`types.ts:11`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/agent/types.ts#L11)). `AgentSession` is the
|
||||
existing wrapper class that implements `AgentProtocol`
|
||||
([`agent-session.ts:18`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/agent/agent-session.ts#L18)). M5 adds `AdkAgentProtocol` as a second
|
||||
implementation; the existing `AgentSession` wrapper composes either one.
|
||||
The whole ADK runtime sits inside one constructor:
|
||||
|
||||
```ts
|
||||
class AdkAgentProtocol implements AgentProtocol {
|
||||
constructor(opts: {
|
||||
config: Config; // model + workspace + services
|
||||
// (auth lives inside the ContentGenerator
|
||||
// returned by config.getContentGenerator())
|
||||
instructionProvider: InstructionProvider; // system prompt (mode-aware)
|
||||
tools: BaseTool[]; // gemini-cli tools wrapped via toAdkTool;
|
||||
// MCP tools come from MCPToolset
|
||||
plugins: BasePlugin[]; // HookBridge, LoopDetection, MaxTurns, ...
|
||||
requestProcessors: RequestProcessor[]; // full ordered ADK + GCLI processor list
|
||||
sessionService: BaseSessionService; // GcliSessionService (live in-memory events)
|
||||
parentSessionId?: string; // present iff this is a subagent
|
||||
});
|
||||
send(input: AgentSend): Promise<{ streamId: string | null }>;
|
||||
subscribe(cb: (e: AgentEvent) => void): Unsubscribe;
|
||||
abort(): Promise<void>;
|
||||
get events(): readonly AgentEvent[];
|
||||
}
|
||||
```
|
||||
|
||||
#### Hooks
|
||||
|
||||
Gemini CLI's `HookSystem` (`packages/core/src/hooks/`) ships 11 events
|
||||
that user-defined hooks integrate against. We keep it as the user-facing
|
||||
surface; exposing ADK plugins to users would force every existing hook
|
||||
to rewrite against different signatures.
|
||||
|
||||
Eight of the 11 fire at boundaries our own code controls. Three fire
|
||||
mid-loop where only ADK knows the timing. We fire the eight directly;
|
||||
the three go through a small ADK plugin.
|
||||
|
||||
Owned — fire from our own code:
|
||||
|
||||
| Hook | Fires from |
|
||||
| --- | --- |
|
||||
| `BeforeTool` / `AfterTool` | `scheduler.schedule()` |
|
||||
| `BeforeAgent` / `AfterAgent` | `AdkAgentProtocol` on `agent_start` / `agent_end` |
|
||||
| `SessionStart` / `SessionEnd` | `AdkAgentProtocol` constructor / `.dispose()` |
|
||||
| `PreCompress` | `ChatCompressionService` |
|
||||
| `Notification` | `AdkAgentProtocol._emit` on tool-notification events |
|
||||
|
||||
Bridged — fire from an ADK plugin callback:
|
||||
|
||||
| Hook | Fires from |
|
||||
| --- | --- |
|
||||
| `BeforeToolSelection` | `HookBridgePlugin.beforeModelCallback` |
|
||||
| `BeforeModel` | `HookBridgePlugin.beforeModelCallback` |
|
||||
| `AfterModel` | `HookBridgePlugin.afterModelCallback` |
|
||||
|
||||
`HookBridgePlugin` is a `BasePlugin` registered with the runner. ADK
|
||||
invokes its model callbacks; the body fires the matching gemini-cli
|
||||
hook events. One-way, no state.
|
||||
|
||||
#### MCP
|
||||
|
||||
Today. `mcp-client.ts` is a full server-lifecycle layer: tool discovery, prompt and resource registries, OAuth refresh and 401 recovery, list-change notifications, stdio crash restart, and progress routing.
|
||||
|
||||
What ADK provides. `MCPToolset` is tool-scoped — tools/list and tools/call only, for stdio and streamable HTTP ([`mcp_toolset.ts:58`](https://github.com/google/adk-js/blob/main/core/src/tools/mcp/mcp_toolset.ts#L58), [`mcp_tool.ts:65`](https://github.com/google/adk-js/blob/main/core/src/tools/mcp/mcp_tool.ts#L65)), with HTTP auth material set at connection construction ([`mcp_session_manager.ts:38-50`](https://github.com/google/adk-js/blob/main/core/src/tools/mcp/mcp_session_manager.ts#L38-L50)). It builds its own session manager rather than reusing ours, and exposes no path to prompts or resources even though the underlying `@modelcontextprotocol/sdk` client supports them. Stored-token refresh, 401 recovery, list-change handlers, progress notifications, and stdio crash restart are absent.
|
||||
|
||||
Open Questions:
|
||||
|
||||
- Upstream the missing surface into `MCPToolset`, or keep `McpClient` and pass its outputs (tools, prompts, resources) into the ADK runtime as functional tools?
|
||||
|
||||
#### Subagents
|
||||
|
||||
A subagent is an `AgentProtocol` with a different system prompt, tool subset, and termination policy. Local subagents run through the same `AdkAgentProtocol` constructor as the top-level session; the session factory selects the runtime and picks which plugins ride along per session. The factory omits `LoopDetectionAdkPlugin` for subagents — they have no loop detection today and terminate via `complete_task` plus the executor's turn/time guards. `Config` and `MessageBus` pass through.
|
||||
|
||||
Remote subagents (`RemoteAgentInvocation`, [`remote-invocation.ts:41`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/agents/remote-invocation.ts#L41)) are unaffected — they run on our own A2A integration, and M4's `AgentTool` routes remote vs. local on `definition.kind`. The wiring change is at [`subagent-tool-wrapper.ts:97`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/agents/subagent-tool-wrapper.ts#L97), which today directly constructs `LocalSubagentInvocation` ([`local-invocation.ts:48`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/agents/local-invocation.ts#L48)) and instead delegates to the session factory.
|
||||
|
||||
Legacy reference points for the behaviors Phase B reimplements as plugins / wrappers:
|
||||
|
||||
- `complete_task` mandatory terminator ([`local-executor.ts:355`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/agents/local-executor.ts#L355)) — subagent must call this tool or it errors with `ERROR_NO_COMPLETE_TASK_CALL`.
|
||||
- Grace-period recovery turn ([`local-executor.ts:460`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/agents/local-executor.ts#L460), [`:724`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/agents/local-executor.ts#L724)) — single final retry with an injected "you must call `complete_task` now" message on recoverable terminate reasons.
|
||||
- Scoped execution wrappers ([`local-executor.ts:530-545`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/agents/local-executor.ts#L530-L545), memory injection at [`:624`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/agents/local-executor.ts#L624) and [`:1339`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/agents/local-executor.ts#L1339)) — workspace, memory-inbox, auto-memory-extraction wrap the run.
|
||||
- `onWaitingForConfirmation` UI signal — propagated through the executor's activity callback today; maps to `BeforeTool` under ADK.
|
||||
|
||||
#### Loop detection
|
||||
|
||||
Today. `LoopDetectionService` runs two detectors against one strike counter: a sliding-window hash over text/tool-call chunks as they stream ([`loopDetectionService.ts:339-437`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/services/loopDetectionService.ts#L339-L437)), and a per-N-turn semantic check via a side LLM call ([`loopDetectionService.ts:261-311`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/services/loopDetectionService.ts#L261-L311)). Below threshold, `GeminiClient` prepends a "System: Potential loop detected..." nudge to the next request ([`client.ts:1246-1279`](https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/core/client.ts#L1246-L1279)); at threshold, it throws and the turn unwinds.
|
||||
|
||||
What ADK provides. Plugin callbacks at every event, around every model call, and on model error (the latter can return a replacement response to keep the loop going). Model output is double-emitted — partial deltas per chunk AND a single consolidated event for the same response — so anything reading the stream has to pick one to avoid double-counting. Exit mechanisms ([`llm_agent.js:634-686`](https://github.com/google/adk-js/blob/main/core/src/agents/llm_agent.ts#L634-L686)): a tripped abort signal returns the generator silently; a thrown Error from inside the model call yields a synthetic error event and ends the turn; a replacement response from `onModelErrorCallback` keeps the loop going. ADK has no direct equivalent of "prepend a system message to the next request"; the closest path is mutating the request contents inside `beforeModelCallback`.
|
||||
|
||||
Open Questions:
|
||||
|
||||
- Which event stream feeds the detector — partial deltas vs consolidated events?
|
||||
- How does the soft-recovery nudge get into the next request?
|
||||
- Which terminate mechanism — abort, throw, or replacement response ([`llm_agent.js:634-686`](https://github.com/google/adk-js/blob/main/core/src/agents/llm_agent.ts#L634-L686))?
|
||||
|
||||
#### Slash commands
|
||||
|
||||
Slash commands live outside the core `AgentProtocol` layer, in a slash-command/runtime-adapter layer wired during Phase C. Specific commands (`/clear`, `/help`, `/memory`, `/compress`, `/rewind`, `/agents`, ...) route differently — some are pure TUI commands, some need to talk to the session, some restart the engine. Implementation details for the adapter layer (per-command routing, abort propagation, and the `ConversationRecord → ADK Event` conversion that `/rewind` requires) are still being expanded.
|
||||
|
||||
#### What we reuse from gemini-cli
|
||||
|
||||
These existing services remain the source of truth under the ADK runtime.
|
||||
M5 adapts to them rather than replacing them:
|
||||
|
||||
- `scheduler.schedule()` — tool execution, approval, telemetry, truncation/distillation, `BeforeTool`/`AfterTool` hooks all run inside it (masking is separate — see Tool output masking row)
|
||||
- `MessageBus` + `PolicyEngine` — tool approval correlation; legacy approval UI continues to consume bus events
|
||||
- `handleFallback` + `ModelAvailabilityService` — 429 fallback + model pool
|
||||
- `ModelRouterService` — per-turn model selection (sequence-sticky), authoritative for routing
|
||||
- `ModelConfigService` — model resolution per request
|
||||
- `LoopDetectionService` — strike-tracked detector
|
||||
- `ChatRecordingService` — product-facing conversation persistence + `rewindTo`
|
||||
- `ChatCompressionService` — outgoing-history projection only; ADK `Session.events` stays as in-memory loop state, `ConversationRecord` stays authoritative for persistence and `/rewind`
|
||||
- `InjectionService` — user steering queue
|
||||
- `ToolOutputMaskingService` — masking
|
||||
- `HookSystem` + 11 hook events — see Hooks section
|
||||
- Existing tool catalog and slash commands
|
||||
|
||||
#### What old logic gets dropped under ADK
|
||||
|
||||
These are the legacy bits the ADK runtime actually replaces:
|
||||
|
||||
- `GeminiClient.processTurn` — replaced by ADK `Runner` + request processors.
|
||||
- `GeminiChat` — replaced by ADK `Session` events.
|
||||
- `Turn` loop — replaced by ADK `LlmAgent.runAsyncImpl`.
|
||||
- `ServerGeminiStreamEvent` — replaced by ADK `Event` → translator → `AgentEvent`.
|
||||
|
||||
#### What we don't reuse from ADK
|
||||
|
||||
ADK is a framework; we adopt the loop and a few extension points. The
|
||||
exclusions below would each be plausible mistakes without explicit
|
||||
rejection:
|
||||
|
||||
- ADK's built-in `Gemini` (`BaseLlm`) — we run our own `BaseLlm` subclass (`GcliAgentModel`) that dispatches through `config.getContentGenerator()`. Auth (OAuth / Gemini API key / Vertex), fallback retry, recording/logging wrappers, and telemetry all live in our content generator; using ADK's bare client would bypass all of that.
|
||||
- `BasePolicyEngine` / `SecurityPlugin` — legacy `MessageBus` + `PolicyEngine` stays. ADK's security plugin would force the approval UI to re-bind against ADK elicitations.
|
||||
- `BaseContextCompactor` — `ChatCompressionService` stays. ADK's compactor doesn't know about our token-budget projection rules.
|
||||
- ADK `RoutedLlm` — not the routing source of truth. Gemini CLI routing (`ModelRouterService` + `ModelConfigService` + `ModelAvailabilityService`, sequence-sticky model, model-specific tool/config updates) remains authoritative. Routing lives in a custom ADK `RequestProcessor` — see Model routing in implementation_details.md.
|
||||
- `BaseSessionService` (file-backed) — `ChatRecordingService` stays. M5 uses a small `GcliSessionService` implementation for in-memory ADK event state.
|
||||
- Most ADK plugin callbacks — see Hooks section.
|
||||
|
||||
#### Supported under ADK runtime
|
||||
|
||||
| Feature | Seam |
|
||||
| --- | --- |
|
||||
| Auth (OAuth / Gemini API key / Vertex) | `GcliAgentModel` maps ADK `LlmRequest` to `GenerateContentParameters` and dispatches through `config.getContentGenerator()`; auth lives in the content generator implementation |
|
||||
| Model fallback (429) | `GcliAgentModel` ports the existing `retryWithBackoff` + `handleFallback` + `ModelAvailabilityService` path; retry stays inside the model wrapper, not ADK Runner |
|
||||
| Dynamic routing & availability | `GcliRoutingProcessor` (a custom ADK `RequestProcessor`) consults `ModelRouterService`, `ModelConfigService`, `ModelAvailabilityService`, and the sequence-sticky model, then rewrites `LlmRequest.model` + tool/config before tool preprocessing. No ADK `RoutedLlm` — see Model routing in implementation_details.md |
|
||||
| Tool execution | Existing `scheduler.schedule()` |
|
||||
| Tool approval | Existing `MessageBus` + `ToolConfirmationQueue.tsx` |
|
||||
| Tool output masking | `ToolOutputMaskingProcessor` (a custom ADK `RequestProcessor`) runs `ToolOutputMaskingService.mask`, writes the masked view back to `LlmRequest.contents`, and applies the same masked function responses back to the live ADK session events by event/part identity rather than request-content index; then calls `ChatRecordingService.updateMessagesFromHistory` to sync the transcript. Per-call truncation rides for free inside `scheduler.schedule`. |
|
||||
| Chat compression | Existing `ChatCompressionService` (outgoing-history projection only) |
|
||||
| Loop detection | `LoopDetectionAdkPlugin` + `LoopDetectionService` — see Loop detection section |
|
||||
| Plan mode | `InstructionProvider` + mode-aware tools (mode-change side effects fire from `Config`, runtime-independent) |
|
||||
| User steering (between turns) | `beforeModelCallback` consumes `InjectionService` queue |
|
||||
| MCP `tools/list` + `tools/call` | Native `MCPToolset` — see MCP section |
|
||||
| MCP OAuth | Scoping open — see MCP section |
|
||||
| Subagents | M4 `AgentTool` + session factory — see Subagents section |
|
||||
| `/rewind` and all `/slash commands` | Live outside the protocol layer. `/rewind` is a slash-command/runtime-adapter concern: the adapter truncates `ConversationRecord` via `ChatRecordingService.rewindTo` and drops the ADK session; next `send()` rebuilds via `appendEvent`. Wired in Phase C (interactive). |
|
||||
| User hooks (11 events) | See Hooks section |
|
||||
| Max turns / tokens / time | Custom plugins (`MaxTurnsAdkPlugin`, `TokenLimitAdkPlugin`, `MaxTimeAdkPlugin`) |
|
||||
| Telemetry / Clearcut | Translator + plugin instrumentation; no new telemetry architecture |
|
||||
| Session persistence | `ChatRecordingService` remains the persisted conversation record; ADK `Session.events` (held in `GcliSessionService`) is runtime state only. `streamId` parity only; `eventId` resume is preserved on the protocol interface, see Rewind PR |
|
||||
| Skills, slash commands, ACP | Unchanged surfaces (ACP picks runtime by feature flag) |
|
||||
| VSCode IDE companion | Unaffected — side-channel to CLI process |
|
||||
|
||||
#### Not supported under ADK runtime
|
||||
|
||||
| Feature | Status | Rationale |
|
||||
| --- | --- | --- |
|
||||
| MCP `/mcp prompts` (`prompts/list`, `prompts/get`) | Scoping open — see MCP section | `MCPToolset` exposes no prompts API today. |
|
||||
| MCP `/mcp resources` (`resources/list`, `resources/read`) | Scoping open — see MCP section | Same gap as prompts. |
|
||||
|
||||
---
|
||||
|
||||
## M5 Execution Plan
|
||||
|
||||
**Foundation** — sequenced first:
|
||||
|
||||
| # | Title | Type |
|
||||
| --- | --- | --- |
|
||||
| 1 | `[AdkAgent] Scaffold adk-agent/ module: AdkAgentProtocol skeleton, GcliSessionService, seam names` | Feature |
|
||||
| 2 | `[AdkAgent] Add experimental.adk.runtimeEnabled flag + session factory + define precedence vs existing agentSessionNoninteractiveEnabled` | Feature |
|
||||
| 3 | `[AdkAgent] Delete unused elicitation_request/response from AgentEvent types` | Bug |
|
||||
|
||||
**Phase A — Non-interactive.** Wires the non-interactive AgentSession entry
|
||||
point through the ADK runtime. Proves the core loop without TUI complexity.
|
||||
|
||||
| # | Title | Type |
|
||||
| --- | --- | --- |
|
||||
| A1 | `[AdkAgent] Translator: text + thought + functionCall + functionResponse + usage + error (with _meta.code) + partial/consolidation + agent_start/end; matches event-translator.test.ts shape` | Feature |
|
||||
| A2 | `[AdkAgent] GcliAgentModel: dispatch ADK LlmRequest through config.getContentGenerator() + AbortSignal propagation` | Feature |
|
||||
| A3 | `[AdkAgent] GcliAgentModel: 429 retry via retryWithBackoff + handleFallback + ModelAvailabilityService (retry inside the model wrapper); silent-policy branch covers non-interactive without a UI handler; covers concrete-model changes` | Feature |
|
||||
| A4 | `[AdkAgent] GcliRoutingProcessor (custom ADK RequestProcessor) + move sequence-sticky model owner from GeminiClient to Config with named reset points` | Feature |
|
||||
| A5 | `[AdkAgent] Invalid-stream retry + next-speaker continuation (client.ts:818, :845) — port or explicitly mark Not Supported` | Feature |
|
||||
| A6 | `[AdkAgent] toAdkTool: route execution through existing scheduler.schedule()` | Feature |
|
||||
| A7 | `[AdkAgent] GcliSessionService: BaseSessionService implementation with authoritative live session map and single-append semantics` | Feature |
|
||||
| A8 | `[AdkAgent] ToolOutputMaskingProcessor: ToolOutputMaskingService.mask + apply masks back to live session events by event/part identity + sync ChatRecordingService.updateMessagesFromHistory` | Feature |
|
||||
| A8a | `[Cleanup] Migrate toolDistillationService off GeminiClient onto BaseLlmClient` | Bug |
|
||||
| A9 | `[AdkAgent] Plugins: MaxTurnsAdkPlugin + TokenLimitAdkPlugin + MaxTimeAdkPlugin (beforeModelCallback / before+after)` | Feature |
|
||||
| A10 | `[AdkAgent] LoopDetectionAdkPlugin: feed partial:true deltas to detector; on terminate call protocol.abort() with LOOP_DETECTED; protocol emits synthetic error event` | Feature |
|
||||
| A11 | `[AdkAgent] MCP: adopt native MCPToolset for stdio + HTTP (tools/list + tools/call only)` | Feature |
|
||||
| A11a–d | `[adk-js upstream] MCPToolset capability gaps: prompts/resources, auth lifecycle (token reuse/refresh/401), server lifecycle (list-change, stdio restart, progress, close), and wire upstream prompts/resources into PromptRegistry + ResourceRegistry` | Feature |
|
||||
| A12 | `[AdkAgent] Wire non-interactive entry point through session factory to select ADK runtime` | Feature |
|
||||
|
||||
**Phase B — Local subagents.** Reuses the Phase-A runtime for child sessions
|
||||
once Phase A is wired and runs cleanly.
|
||||
|
||||
| # | Title | Type |
|
||||
| --- | --- | --- |
|
||||
| B1 | `[AdkAgent] Route subagent-tool-wrapper.ts:97 constructor through session factory; return ADK-backed AgentProtocol when the parent runtime is ADK` | Feature |
|
||||
| B2 | `[AdkAgent] Subagent: complete_task mandatory terminator plugin (afterModelCallback)` | Feature |
|
||||
| B3 | `[AdkAgent] Subagent: grace-period recovery turn (inject "you must call complete_task now" + final retry)` | Feature |
|
||||
| B4 | `[AdkAgent] Subagent: scoped execution wrappers (workspace, memory-inbox, auto-memory-extraction)` | Feature |
|
||||
| B5 | `[AdkAgent] Subagent: onWaitingForConfirmation activity signal via BeforeTool` | Feature |
|
||||
|
||||
**Phase C — Interactive.** Wires the TUI last. By this point the runtime
|
||||
loop, model path, tools, fallback, masking, and subagents are already proven
|
||||
outside the TUI. **All `/slash commands` live outside the protocol layer.**
|
||||
|
||||
| # | Title | Type |
|
||||
| --- | --- | --- |
|
||||
| C1 | `[AdkAgent] Wire interactive AgentSession through session factory; TUI stream rendering parity` | Feature |
|
||||
| C2 | `[AdkAgent] User steering injection via beforeModelCallback` | Feature |
|
||||
| C3 | `[AdkAgent] Plan mode: InstructionProvider for system-prompt swapping + mode-aware tool filtering via BaseToolset` | Feature |
|
||||
| C4 | `[AdkAgent] HookBridgePlugin: fire BeforeModel / AfterModel / BeforeToolSelection from beforeModelCallback / afterModelCallback` | Feature |
|
||||
| C5 | `[AdkAgent] Wire SessionStart / SessionEnd / BeforeAgent / AfterAgent / Notification hooks (Notification fires from AdkAgentProtocol._emit on tool-notification events)` | Feature |
|
||||
| C6 | `[AdkAgent] Register TUI fallbackModelHandler callback so non-silent intents (retry_once / stop / upgrade / retry_with_credits) work; handleFallback machinery is already wired in Phase A` | Feature |
|
||||
| C7 | `[AdkAgent] /rewind: slash-command/runtime-adapter truncates ConversationRecord via ChatRecordingService.rewindTo, drops ADK session, next send() rebuilds via appendEvent (NOT an AgentProtocol method)` | Feature |
|
||||
| C8 | `[AdkAgent] Make ADK runtime the default for all wired surfaces` | Feature |
|
||||
@@ -18,6 +18,31 @@ on GitHub.
|
||||
| [Preview](preview.md) | Experimental features ready for early feedback. |
|
||||
| [Stable](latest.md) | Stable, recommended for general use. |
|
||||
|
||||
## Announcements: v0.36.0 - 2026-04-01
|
||||
|
||||
- **Multi-Registry Architecture and Sandboxing:** Introduced a multi-registry
|
||||
architecture and implemented native macOS Seatbelt and Windows sandboxing for
|
||||
enhanced subagent security
|
||||
([#22712](https://github.com/google-gemini/gemini-cli/pull/22712),
|
||||
[#22718](https://github.com/google-gemini/gemini-cli/pull/22718) by @akh64bit,
|
||||
[#22832](https://github.com/google-gemini/gemini-cli/pull/22832) by @ehedlund,
|
||||
[#21807](https://github.com/google-gemini/gemini-cli/pull/21807) by
|
||||
@mattKorwel).
|
||||
- **Refreshed Composer UX:** Implemented a refreshed user experience for the
|
||||
Composer layout and improved terminal interaction robustness
|
||||
([#21212](https://github.com/google-gemini/gemini-cli/pull/21212),
|
||||
[#23286](https://github.com/google-gemini/gemini-cli/pull/23286) by
|
||||
@jwhelangoog).
|
||||
- **Git Worktree Support:** Added native support for Git worktrees, allowing for
|
||||
isolated parallel sessions
|
||||
([#22973](https://github.com/google-gemini/gemini-cli/pull/22973),
|
||||
[#23265](https://github.com/google-gemini/gemini-cli/pull/23265) by @jerop).
|
||||
- **Subagent Context and Feedback:** Enhanced subagents with JIT context
|
||||
injection and resilient tool rejection with contextual feedback
|
||||
([#23032](https://github.com/google-gemini/gemini-cli/pull/23032),
|
||||
[#22951](https://github.com/google-gemini/gemini-cli/pull/22951) by
|
||||
@abhipatel12).
|
||||
|
||||
## Announcements: v0.35.0 - 2026-03-24
|
||||
|
||||
- **Customizable Keyboard Shortcuts:** Users can now customize their keyboard
|
||||
|
||||
+357
-366
@@ -1,6 +1,6 @@
|
||||
# Latest stable release: v0.35.3
|
||||
# Latest stable release: v0.36.0
|
||||
|
||||
Released: March 28, 2026
|
||||
Released: April 1, 2026
|
||||
|
||||
For most users, our latest stable release is the recommended release. Install
|
||||
the latest stable version with:
|
||||
@@ -11,381 +11,372 @@ npm install -g @google/gemini-cli
|
||||
|
||||
## Highlights
|
||||
|
||||
- **Customizable Keyboard Shortcuts:** Significant improvements to input
|
||||
flexibility with support for custom keybindings, literal character bindings,
|
||||
and extended terminal protocol keys.
|
||||
- **Vim Mode Enhancements:** Further refinement of the Vim modal editing
|
||||
experience, adding common motions like \`X\`, \`~\`, \`r\`, and \`f/F/t/T\`,
|
||||
along with yank and paste support.
|
||||
- **Enhanced Security through Sandboxing:** Introduction of a unified
|
||||
\`SandboxManager\` and integration of Linux-native sandboxing (bubblewrap and
|
||||
seccomp) to isolate tool execution and improve system security.
|
||||
- **JIT Context Discovery:** Improved performance and accuracy by enabling
|
||||
Just-In-Time context loading for file system tools, ensuring the model has the
|
||||
most relevant information without overwhelming the context.
|
||||
- **Subagent & Performance Updates:** Subagents are now enabled by default,
|
||||
supported by a model-driven parallel tool scheduler and code splitting for
|
||||
faster startup and more efficient task execution.
|
||||
- **Multi-Registry Architecture and Tool Isolation:** Introduced a
|
||||
multi-registry architecture for subagents and implemented strict sandboxing
|
||||
for macOS (Seatbelt) and Windows to enhance security and isolation.
|
||||
- **Improved Subagent Coordination:** Enhanced subagents with local execution
|
||||
capabilities, JIT context injection (upward traversal capped at git root), and
|
||||
resilient tool rejection with contextual feedback.
|
||||
- **Enhanced UI and UX:** Implemented a refreshed UX for the Composer layout,
|
||||
improved terminal fallback warnings, and resolved various UI flickering and
|
||||
state persistence issues.
|
||||
- **Git Worktree Support:** Added support for Git worktrees to enable isolated
|
||||
parallel sessions within the same repository.
|
||||
- **Plan Mode Improvements:** Plan mode now supports non-interactive execution
|
||||
and includes hardened sandbox path resolution to prevent hallucinations.
|
||||
|
||||
## What's Changed
|
||||
|
||||
- fix(patch): cherry-pick 765fb67 to release/v0.35.2-pr-24055 [CONFLICTS] by
|
||||
@gemini-cli-robot in
|
||||
[#24063](https://github.com/google-gemini/gemini-cli/pull/24063)
|
||||
- fix(core): allow disabling environment variable redaction by @galz10 in
|
||||
[#23927](https://github.com/google-gemini/gemini-cli/pull/23927)
|
||||
- fix(a2a-server): A2A server should execute ask policies in interactive mode by
|
||||
@keith.schaab in
|
||||
[#23831](https://github.com/google-gemini/gemini-cli/pull/23831)
|
||||
- feat(cli): customizable keyboard shortcuts by @scidomino in
|
||||
[#21945](https://github.com/google-gemini/gemini-cli/pull/21945)
|
||||
- feat(core): Thread `AgentLoopContext` through core. by @joshualitt in
|
||||
[#21944](https://github.com/google-gemini/gemini-cli/pull/21944)
|
||||
- chore(release): bump version to 0.35.0-nightly.20260311.657f19c1f by
|
||||
@gemini-cli-robot in
|
||||
[#21966](https://github.com/google-gemini/gemini-cli/pull/21966)
|
||||
- refactor(a2a): remove legacy CoreToolScheduler by @adamfweidman in
|
||||
[#21955](https://github.com/google-gemini/gemini-cli/pull/21955)
|
||||
- feat(ui): add missing vim mode motions (X, ~, r, f/F/t/T, df/dt and friends)
|
||||
by @aanari in [#21932](https://github.com/google-gemini/gemini-cli/pull/21932)
|
||||
- Feat/retry fetch notifications by @aishaneeshah in
|
||||
[#21813](https://github.com/google-gemini/gemini-cli/pull/21813)
|
||||
- fix(core): remove OAuth check from handle fallback and clean up stray file by
|
||||
@sehoon38 in [#21962](https://github.com/google-gemini/gemini-cli/pull/21962)
|
||||
- feat(cli): support literal character keybindings and extended Kitty protocol
|
||||
keys by @scidomino in
|
||||
[#21972](https://github.com/google-gemini/gemini-cli/pull/21972)
|
||||
- fix(ui): clamp cursor to last char after all NORMAL mode deletes by @aanari in
|
||||
[#21973](https://github.com/google-gemini/gemini-cli/pull/21973)
|
||||
- test(core): add missing tests for prompts/utils.ts by @krrishverma1805-web in
|
||||
[#19941](https://github.com/google-gemini/gemini-cli/pull/19941)
|
||||
- fix(cli): allow scrolling keys in copy mode (Ctrl+S selection mode) by
|
||||
@nsalerni in [#19933](https://github.com/google-gemini/gemini-cli/pull/19933)
|
||||
- docs(cli): add custom keybinding documentation by @scidomino in
|
||||
[#21980](https://github.com/google-gemini/gemini-cli/pull/21980)
|
||||
- docs: fix misleading YOLO mode description in defaultApprovalMode by
|
||||
@Gyanranjan-Priyam in
|
||||
[#21878](https://github.com/google-gemini/gemini-cli/pull/21878)
|
||||
- fix: clean up /clear and /resume by @jackwotherspoon in
|
||||
[#22007](https://github.com/google-gemini/gemini-cli/pull/22007)
|
||||
- fix(core)#20941: reap orphaned descendant processes on PTY abort by @manavmax
|
||||
in [#21124](https://github.com/google-gemini/gemini-cli/pull/21124)
|
||||
- fix(core): update language detection to use LSP 3.18 identifiers by @yunaseoul
|
||||
in [#21931](https://github.com/google-gemini/gemini-cli/pull/21931)
|
||||
- feat(cli): support removing keybindings via '-' prefix by @scidomino in
|
||||
[#22042](https://github.com/google-gemini/gemini-cli/pull/22042)
|
||||
- feat(policy): add --admin-policy flag for supplemental admin policies by
|
||||
@galz10 in [#20360](https://github.com/google-gemini/gemini-cli/pull/20360)
|
||||
- merge duplicate imports packages/cli/src subtask1 by @Nixxx19 in
|
||||
[#22040](https://github.com/google-gemini/gemini-cli/pull/22040)
|
||||
- perf(core): parallelize user quota and experiments fetching in refreshAuth by
|
||||
@sehoon38 in [#21648](https://github.com/google-gemini/gemini-cli/pull/21648)
|
||||
- Changelog for v0.34.0-preview.0 by @gemini-cli-robot in
|
||||
[#21965](https://github.com/google-gemini/gemini-cli/pull/21965)
|
||||
- Changelog for v0.33.0 by @gemini-cli-robot in
|
||||
[#21967](https://github.com/google-gemini/gemini-cli/pull/21967)
|
||||
- fix(core): handle EISDIR in robustRealpath on Windows by @sehoon38 in
|
||||
[#21984](https://github.com/google-gemini/gemini-cli/pull/21984)
|
||||
- feat(core): include initiationMethod in conversation interaction telemetry by
|
||||
@yunaseoul in [#22054](https://github.com/google-gemini/gemini-cli/pull/22054)
|
||||
- feat(ui): add vim yank/paste (y/p/P) with unnamed register by @aanari in
|
||||
[#22026](https://github.com/google-gemini/gemini-cli/pull/22026)
|
||||
- fix(core): enable numerical routing for api key users by @sehoon38 in
|
||||
[#21977](https://github.com/google-gemini/gemini-cli/pull/21977)
|
||||
- feat(telemetry): implement retry attempt telemetry for network related retries
|
||||
by @aishaneeshah in
|
||||
[#22027](https://github.com/google-gemini/gemini-cli/pull/22027)
|
||||
- fix(policy): remove unnecessary escapeRegex from pattern builders by
|
||||
@spencer426 in
|
||||
[#21921](https://github.com/google-gemini/gemini-cli/pull/21921)
|
||||
- fix(core): preserve dynamic tool descriptions on session resume by @sehoon38
|
||||
in [#18835](https://github.com/google-gemini/gemini-cli/pull/18835)
|
||||
- chore: allow 'gemini-3.1' in sensitive keyword linter by @scidomino in
|
||||
[#22065](https://github.com/google-gemini/gemini-cli/pull/22065)
|
||||
- feat(core): support custom base URL via env vars by @junaiddshaukat in
|
||||
[#21561](https://github.com/google-gemini/gemini-cli/pull/21561)
|
||||
- merge duplicate imports packages/cli/src subtask2 by @Nixxx19 in
|
||||
[#22051](https://github.com/google-gemini/gemini-cli/pull/22051)
|
||||
- fix(core): silently retry API errors up to 3 times before halting session by
|
||||
@spencer426 in
|
||||
[#21989](https://github.com/google-gemini/gemini-cli/pull/21989)
|
||||
- feat(core): simplify subagent success UI and improve early termination display
|
||||
by @abhipatel12 in
|
||||
[#21917](https://github.com/google-gemini/gemini-cli/pull/21917)
|
||||
- merge duplicate imports packages/cli/src subtask3 by @Nixxx19 in
|
||||
[#22056](https://github.com/google-gemini/gemini-cli/pull/22056)
|
||||
- fix(hooks): fix BeforeAgent/AfterAgent inconsistencies (#18514) by @krishdef7
|
||||
in [#21383](https://github.com/google-gemini/gemini-cli/pull/21383)
|
||||
- feat(core): implement SandboxManager interface and config schema by @galz10 in
|
||||
[#21774](https://github.com/google-gemini/gemini-cli/pull/21774)
|
||||
- docs: document npm deprecation warnings as safe to ignore by @h30s in
|
||||
[#20692](https://github.com/google-gemini/gemini-cli/pull/20692)
|
||||
- fix: remove status/need-triage from maintainer-only issues by @SandyTao520 in
|
||||
[#22044](https://github.com/google-gemini/gemini-cli/pull/22044)
|
||||
- fix(core): propagate subagent context to policy engine by @NTaylorMullen in
|
||||
[#22086](https://github.com/google-gemini/gemini-cli/pull/22086)
|
||||
- fix(cli): resolve skill uninstall failure when skill name is updated by
|
||||
@NTaylorMullen in
|
||||
[#22085](https://github.com/google-gemini/gemini-cli/pull/22085)
|
||||
- docs(plan): clarify interactive plan editing with Ctrl+X by @Adib234 in
|
||||
[#22076](https://github.com/google-gemini/gemini-cli/pull/22076)
|
||||
- fix(policy): ensure user policies are loaded when policyPaths is empty by
|
||||
@NTaylorMullen in
|
||||
[#22090](https://github.com/google-gemini/gemini-cli/pull/22090)
|
||||
- Docs: Add documentation for model steering (experimental). by @jkcinouye in
|
||||
[#21154](https://github.com/google-gemini/gemini-cli/pull/21154)
|
||||
- Add issue for automated changelogs by @g-samroberts in
|
||||
[#21912](https://github.com/google-gemini/gemini-cli/pull/21912)
|
||||
- fix(core): secure argsPattern and revert WEB_FETCH_TOOL_NAME escalation by
|
||||
@spencer426 in
|
||||
[#22104](https://github.com/google-gemini/gemini-cli/pull/22104)
|
||||
- feat(core): differentiate User-Agent for a2a-server and ACP clients by
|
||||
@bdmorgan in [#22059](https://github.com/google-gemini/gemini-cli/pull/22059)
|
||||
- refactor(core): extract ExecutionLifecycleService for tool backgrounding by
|
||||
@adamfweidman in
|
||||
[#21717](https://github.com/google-gemini/gemini-cli/pull/21717)
|
||||
- feat: Display pending and confirming tool calls by @sripasg in
|
||||
[#22106](https://github.com/google-gemini/gemini-cli/pull/22106)
|
||||
- feat(browser): implement input blocker overlay during automation by
|
||||
@kunal-10-cloud in
|
||||
[#21132](https://github.com/google-gemini/gemini-cli/pull/21132)
|
||||
- fix: register themes on extension load not start by @jackwotherspoon in
|
||||
[#22148](https://github.com/google-gemini/gemini-cli/pull/22148)
|
||||
- feat(ui): Do not show Ultra users /upgrade hint (#22154) by @sehoon38 in
|
||||
[#22156](https://github.com/google-gemini/gemini-cli/pull/22156)
|
||||
- chore: remove unnecessary log for themes by @jackwotherspoon in
|
||||
[#22165](https://github.com/google-gemini/gemini-cli/pull/22165)
|
||||
- fix(core): resolve MCP tool FQN validation, schema export, and wildcards in
|
||||
subagents by @abhipatel12 in
|
||||
[#22069](https://github.com/google-gemini/gemini-cli/pull/22069)
|
||||
- fix(cli): validate --model argument at startup by @JaisalJain in
|
||||
[#21393](https://github.com/google-gemini/gemini-cli/pull/21393)
|
||||
- fix(core): handle policy ALLOW for exit_plan_mode by @backnotprop in
|
||||
[#21802](https://github.com/google-gemini/gemini-cli/pull/21802)
|
||||
- feat(telemetry): add Clearcut instrumentation for AI credits billing events by
|
||||
@gsquared94 in
|
||||
[#22153](https://github.com/google-gemini/gemini-cli/pull/22153)
|
||||
- feat(core): add google credentials provider for remote agents by @adamfweidman
|
||||
in [#21024](https://github.com/google-gemini/gemini-cli/pull/21024)
|
||||
- test(cli): add integration test for node deprecation warnings by @Nixxx19 in
|
||||
[#20215](https://github.com/google-gemini/gemini-cli/pull/20215)
|
||||
- feat(cli): allow safe tools to execute concurrently while agent is busy by
|
||||
@spencer426 in
|
||||
[#21988](https://github.com/google-gemini/gemini-cli/pull/21988)
|
||||
- feat(core): implement model-driven parallel tool scheduler by @abhipatel12 in
|
||||
[#21933](https://github.com/google-gemini/gemini-cli/pull/21933)
|
||||
- update vulnerable deps by @scidomino in
|
||||
[#22180](https://github.com/google-gemini/gemini-cli/pull/22180)
|
||||
- fix(core): fix startup stats to use int values for timestamps and durations by
|
||||
@yunaseoul in [#22201](https://github.com/google-gemini/gemini-cli/pull/22201)
|
||||
- fix(core): prevent duplicate tool schemas for instantiated tools by
|
||||
@abhipatel12 in
|
||||
[#22204](https://github.com/google-gemini/gemini-cli/pull/22204)
|
||||
- fix(core): add proxy routing support for remote A2A subagents by @adamfweidman
|
||||
in [#22199](https://github.com/google-gemini/gemini-cli/pull/22199)
|
||||
- fix(core/ide): add Antigravity CLI fallbacks by @apfine in
|
||||
[#22030](https://github.com/google-gemini/gemini-cli/pull/22030)
|
||||
- fix(browser): fix duplicate function declaration error in browser agent by
|
||||
@gsquared94 in
|
||||
[#22207](https://github.com/google-gemini/gemini-cli/pull/22207)
|
||||
- feat(core): implement Stage 1 improvements for webfetch tool by @aishaneeshah
|
||||
in [#21313](https://github.com/google-gemini/gemini-cli/pull/21313)
|
||||
- Changelog for v0.34.0-preview.1 by @gemini-cli-robot in
|
||||
[#22194](https://github.com/google-gemini/gemini-cli/pull/22194)
|
||||
- perf(cli): enable code splitting and deferred UI loading by @sehoon38 in
|
||||
[#22117](https://github.com/google-gemini/gemini-cli/pull/22117)
|
||||
- fix: remove unused img.png from project root by @SandyTao520 in
|
||||
[#22222](https://github.com/google-gemini/gemini-cli/pull/22222)
|
||||
- docs(local model routing): add docs on how to use Gemma for local model
|
||||
routing by @douglas-reid in
|
||||
[#21365](https://github.com/google-gemini/gemini-cli/pull/21365)
|
||||
- feat(a2a): enable native gRPC support and protocol routing by @alisa-alisa in
|
||||
[#21403](https://github.com/google-gemini/gemini-cli/pull/21403)
|
||||
- fix(cli): escape @ symbols on paste to prevent unintended file expansion by
|
||||
@krishdef7 in [#21239](https://github.com/google-gemini/gemini-cli/pull/21239)
|
||||
- feat(core): add trajectoryId to ConversationOffered telemetry by @yunaseoul in
|
||||
[#22214](https://github.com/google-gemini/gemini-cli/pull/22214)
|
||||
- docs: clarify that tools.core is an allowlist for ALL built-in tools by
|
||||
@hobostay in [#18813](https://github.com/google-gemini/gemini-cli/pull/18813)
|
||||
- docs(plan): document hooks with plan mode by @ruomengz in
|
||||
[#22197](https://github.com/google-gemini/gemini-cli/pull/22197)
|
||||
- Changelog for v0.33.1 by @gemini-cli-robot in
|
||||
[#22235](https://github.com/google-gemini/gemini-cli/pull/22235)
|
||||
- build(ci): fix false positive evals trigger on merge commits by @gundermanc in
|
||||
[#22237](https://github.com/google-gemini/gemini-cli/pull/22237)
|
||||
- fix(core): explicitly pass messageBus to policy engine for MCP tool saves by
|
||||
@abhipatel12 in
|
||||
[#22255](https://github.com/google-gemini/gemini-cli/pull/22255)
|
||||
- feat(core): Fully migrate packages/core to AgentLoopContext. by @joshualitt in
|
||||
[#22115](https://github.com/google-gemini/gemini-cli/pull/22115)
|
||||
- feat(core): increase sub-agent turn and time limits by @bdmorgan in
|
||||
[#22196](https://github.com/google-gemini/gemini-cli/pull/22196)
|
||||
- feat(core): instrument file system tools for JIT context discovery by
|
||||
- Changelog for v0.33.2 by @gemini-cli-robot in
|
||||
[#22730](https://github.com/google-gemini/gemini-cli/pull/22730)
|
||||
- feat(core): multi-registry architecture and tool filtering for subagents by
|
||||
@akh64bit in [#22712](https://github.com/google-gemini/gemini-cli/pull/22712)
|
||||
- Changelog for v0.34.0-preview.4 by @gemini-cli-robot in
|
||||
[#22752](https://github.com/google-gemini/gemini-cli/pull/22752)
|
||||
- fix(devtools): use theme-aware text colors for console warnings and errors by
|
||||
@SandyTao520 in
|
||||
[#22082](https://github.com/google-gemini/gemini-cli/pull/22082)
|
||||
- refactor(ui): extract pure session browser utilities by @abhipatel12 in
|
||||
[#22256](https://github.com/google-gemini/gemini-cli/pull/22256)
|
||||
- fix(plan): Fix AskUser evals by @Adib234 in
|
||||
[#22074](https://github.com/google-gemini/gemini-cli/pull/22074)
|
||||
- fix(settings): prevent j/k navigation keys from intercepting edit buffer input
|
||||
by @student-ankitpandit in
|
||||
[#21865](https://github.com/google-gemini/gemini-cli/pull/21865)
|
||||
- feat(skills): improve async-pr-review workflow and logging by @mattKorwel in
|
||||
[#21790](https://github.com/google-gemini/gemini-cli/pull/21790)
|
||||
- refactor(cli): consolidate getErrorMessage utility to core by @scidomino in
|
||||
[#22190](https://github.com/google-gemini/gemini-cli/pull/22190)
|
||||
- fix(core): show descriptive error messages when saving settings fails by
|
||||
@afarber in [#18095](https://github.com/google-gemini/gemini-cli/pull/18095)
|
||||
- docs(core): add authentication guide for remote subagents by @adamfweidman in
|
||||
[#22178](https://github.com/google-gemini/gemini-cli/pull/22178)
|
||||
- docs: overhaul subagents documentation and add /agents command by @abhipatel12
|
||||
in [#22345](https://github.com/google-gemini/gemini-cli/pull/22345)
|
||||
- refactor(ui): extract SessionBrowser static ui components by @abhipatel12 in
|
||||
[#22348](https://github.com/google-gemini/gemini-cli/pull/22348)
|
||||
- test: add Object.create context regression test and tool confirmation
|
||||
integration test by @gsquared94 in
|
||||
[#22356](https://github.com/google-gemini/gemini-cli/pull/22356)
|
||||
- feat(tracker): return TodoList display for tracker tools by @anj-s in
|
||||
[#22060](https://github.com/google-gemini/gemini-cli/pull/22060)
|
||||
- feat(agent): add allowed domain restrictions for browser agent by
|
||||
[#22181](https://github.com/google-gemini/gemini-cli/pull/22181)
|
||||
- Add support for dynamic model Resolution to ModelConfigService by @kevinjwang1
|
||||
in [#22578](https://github.com/google-gemini/gemini-cli/pull/22578)
|
||||
- chore(release): bump version to 0.36.0-nightly.20260317.2f90b4653 by
|
||||
@gemini-cli-robot in
|
||||
[#22858](https://github.com/google-gemini/gemini-cli/pull/22858)
|
||||
- fix(cli): use active sessionId in useLogger and improve resume robustness by
|
||||
@mattKorwel in
|
||||
[#22606](https://github.com/google-gemini/gemini-cli/pull/22606)
|
||||
- fix(cli): expand tilde in policy paths from settings.json by @abhipatel12 in
|
||||
[#22772](https://github.com/google-gemini/gemini-cli/pull/22772)
|
||||
- fix(core): add actionable warnings for terminal fallbacks (#14426) by
|
||||
@spencer426 in
|
||||
[#22211](https://github.com/google-gemini/gemini-cli/pull/22211)
|
||||
- feat(tracker): integrate task tracker protocol into core system prompt by
|
||||
@anj-s in [#22442](https://github.com/google-gemini/gemini-cli/pull/22442)
|
||||
- chore: add posttest build hooks and fix missing dependencies by @NTaylorMullen
|
||||
in [#22865](https://github.com/google-gemini/gemini-cli/pull/22865)
|
||||
- feat(a2a): add agent acknowledgment command and enhance registry discovery by
|
||||
@alisa-alisa in
|
||||
[#22389](https://github.com/google-gemini/gemini-cli/pull/22389)
|
||||
- fix(cli): automatically add all VSCode workspace folders to Gemini context by
|
||||
@sakshisemalti in
|
||||
[#21380](https://github.com/google-gemini/gemini-cli/pull/21380)
|
||||
- feat: add 'blocked' status to tasks and todos by @anj-s in
|
||||
[#22735](https://github.com/google-gemini/gemini-cli/pull/22735)
|
||||
- refactor(cli): remove extra newlines in ShellToolMessage.tsx by @NTaylorMullen
|
||||
in [#22868](https://github.com/google-gemini/gemini-cli/pull/22868)
|
||||
- fix(cli): lazily load settings in onModelChange to prevent stale closure data
|
||||
loss by @KumarADITHYA123 in
|
||||
[#20403](https://github.com/google-gemini/gemini-cli/pull/20403)
|
||||
- feat(core): subagent local execution and tool isolation by @akh64bit in
|
||||
[#22718](https://github.com/google-gemini/gemini-cli/pull/22718)
|
||||
- fix(cli): resolve subagent grouping and UI state persistence by @abhipatel12
|
||||
in [#22252](https://github.com/google-gemini/gemini-cli/pull/22252)
|
||||
- refactor(ui): extract SessionBrowser search and navigation components by
|
||||
@abhipatel12 in
|
||||
[#22377](https://github.com/google-gemini/gemini-cli/pull/22377)
|
||||
- fix: updates Docker image reference for GitHub MCP server by @jhhornn in
|
||||
[#22938](https://github.com/google-gemini/gemini-cli/pull/22938)
|
||||
- refactor(cli): group subagent trajectory deletion and use native filesystem
|
||||
testing by @abhipatel12 in
|
||||
[#22890](https://github.com/google-gemini/gemini-cli/pull/22890)
|
||||
- refactor(cli): simplify keypress and mouse providers and update tests by
|
||||
@scidomino in [#22853](https://github.com/google-gemini/gemini-cli/pull/22853)
|
||||
- Changelog for v0.34.0 by @gemini-cli-robot in
|
||||
[#22860](https://github.com/google-gemini/gemini-cli/pull/22860)
|
||||
- test(cli): simplify createMockSettings calls by @scidomino in
|
||||
[#22952](https://github.com/google-gemini/gemini-cli/pull/22952)
|
||||
- feat(ui): format multi-line banner warnings with a bold title by @keithguerin
|
||||
in [#22955](https://github.com/google-gemini/gemini-cli/pull/22955)
|
||||
- Docs: Remove references to stale Gemini CLI file structure info by
|
||||
@g-samroberts in
|
||||
[#22976](https://github.com/google-gemini/gemini-cli/pull/22976)
|
||||
- feat(ui): remove write todo list tool from UI tips by @aniruddhaadak80 in
|
||||
[#22281](https://github.com/google-gemini/gemini-cli/pull/22281)
|
||||
- Fix issue where subagent thoughts are appended. by @gundermanc in
|
||||
[#22975](https://github.com/google-gemini/gemini-cli/pull/22975)
|
||||
- Feat/browser privacy consent by @kunal-10-cloud in
|
||||
[#21119](https://github.com/google-gemini/gemini-cli/pull/21119)
|
||||
- fix(core): explicitly map execution context in LocalAgentExecutor by @akh64bit
|
||||
in [#22949](https://github.com/google-gemini/gemini-cli/pull/22949)
|
||||
- feat(plan): support plan mode in non-interactive mode by @ruomengz in
|
||||
[#22670](https://github.com/google-gemini/gemini-cli/pull/22670)
|
||||
- feat(core): implement strict macOS sandboxing using Seatbelt allowlist by
|
||||
@ehedlund in [#22832](https://github.com/google-gemini/gemini-cli/pull/22832)
|
||||
- docs: add additional notes by @abhipatel12 in
|
||||
[#23008](https://github.com/google-gemini/gemini-cli/pull/23008)
|
||||
- fix(cli): resolve duplicate footer on tool cancel via ESC (#21743) by
|
||||
@ruomengz in [#21781](https://github.com/google-gemini/gemini-cli/pull/21781)
|
||||
- Changelog for v0.35.0-preview.1 by @gemini-cli-robot in
|
||||
[#23012](https://github.com/google-gemini/gemini-cli/pull/23012)
|
||||
- fix(ui): fix flickering on small terminal heights by @devr0306 in
|
||||
[#21416](https://github.com/google-gemini/gemini-cli/pull/21416)
|
||||
- fix(acp): provide more meta in tool_call_update by @Mervap in
|
||||
[#22663](https://github.com/google-gemini/gemini-cli/pull/22663)
|
||||
- docs: add FAQ entry for checking Gemini CLI version by @surajsahani in
|
||||
[#21271](https://github.com/google-gemini/gemini-cli/pull/21271)
|
||||
- feat(core): resilient subagent tool rejection with contextual feedback by
|
||||
@abhipatel12 in
|
||||
[#22951](https://github.com/google-gemini/gemini-cli/pull/22951)
|
||||
- fix(cli): correctly handle auto-update for standalone binaries by @bdmorgan in
|
||||
[#23038](https://github.com/google-gemini/gemini-cli/pull/23038)
|
||||
- feat(core): add content-utils by @adamfweidman in
|
||||
[#22984](https://github.com/google-gemini/gemini-cli/pull/22984)
|
||||
- fix: circumvent genai sdk requirement for api key when using gateway auth via
|
||||
ACP by @sripasg in
|
||||
[#23042](https://github.com/google-gemini/gemini-cli/pull/23042)
|
||||
- fix(core): don't persist browser consent sentinel in non-interactive mode by
|
||||
@jasonmatthewsuhari in
|
||||
[#23073](https://github.com/google-gemini/gemini-cli/pull/23073)
|
||||
- fix(core): narrow browser agent description to prevent stealing URL tasks from
|
||||
web_fetch by @gsquared94 in
|
||||
[#23086](https://github.com/google-gemini/gemini-cli/pull/23086)
|
||||
- feat(cli): Partial threading of AgentLoopContext. by @joshualitt in
|
||||
[#22978](https://github.com/google-gemini/gemini-cli/pull/22978)
|
||||
- fix(browser-agent): enable "Allow all server tools" session policy by
|
||||
@cynthialong0-0 in
|
||||
[#21775](https://github.com/google-gemini/gemini-cli/pull/21775)
|
||||
- chore/release: bump version to 0.35.0-nightly.20260313.bb060d7a9 by
|
||||
@gemini-cli-robot in
|
||||
[#22251](https://github.com/google-gemini/gemini-cli/pull/22251)
|
||||
- Move keychain fallback to keychain service by @chrstnb in
|
||||
[#22332](https://github.com/google-gemini/gemini-cli/pull/22332)
|
||||
- feat(core): integrate SandboxManager to sandbox all process-spawning tools by
|
||||
@galz10 in [#22231](https://github.com/google-gemini/gemini-cli/pull/22231)
|
||||
- fix(cli): support CJK input and full Unicode scalar values in terminal
|
||||
protocols by @scidomino in
|
||||
[#22353](https://github.com/google-gemini/gemini-cli/pull/22353)
|
||||
- Promote stable tests. by @gundermanc in
|
||||
[#22253](https://github.com/google-gemini/gemini-cli/pull/22253)
|
||||
- feat(tracker): add tracker policy by @anj-s in
|
||||
[#22379](https://github.com/google-gemini/gemini-cli/pull/22379)
|
||||
- feat(security): add disableAlwaysAllow setting to disable auto-approvals by
|
||||
@galz10 in [#21941](https://github.com/google-gemini/gemini-cli/pull/21941)
|
||||
- Revert "fix(cli): validate --model argument at startup" by @sehoon38 in
|
||||
[#22378](https://github.com/google-gemini/gemini-cli/pull/22378)
|
||||
- fix(mcp): handle equivalent root resource URLs in OAuth validation by @galz10
|
||||
in [#20231](https://github.com/google-gemini/gemini-cli/pull/20231)
|
||||
- fix(core): use session-specific temp directory for task tracker by @anj-s in
|
||||
[#22382](https://github.com/google-gemini/gemini-cli/pull/22382)
|
||||
- Fix issue where config was undefined. by @gundermanc in
|
||||
[#22397](https://github.com/google-gemini/gemini-cli/pull/22397)
|
||||
- fix(core): deduplicate project memory when JIT context is enabled by
|
||||
[#22343](https://github.com/google-gemini/gemini-cli/pull/22343)
|
||||
- refactor(cli): integrate real config loading into async test utils by
|
||||
@scidomino in [#23040](https://github.com/google-gemini/gemini-cli/pull/23040)
|
||||
- feat(core): inject memory and JIT context into subagents by @abhipatel12 in
|
||||
[#23032](https://github.com/google-gemini/gemini-cli/pull/23032)
|
||||
- Fix logging and virtual list. by @jacob314 in
|
||||
[#23080](https://github.com/google-gemini/gemini-cli/pull/23080)
|
||||
- feat(core): cap JIT context upward traversal at git root by @SandyTao520 in
|
||||
[#23074](https://github.com/google-gemini/gemini-cli/pull/23074)
|
||||
- Docs: Minor style updates from initial docs audit. by @g-samroberts in
|
||||
[#22872](https://github.com/google-gemini/gemini-cli/pull/22872)
|
||||
- feat(core): add experimental memory manager agent to replace save_memory tool
|
||||
by @SandyTao520 in
|
||||
[#22726](https://github.com/google-gemini/gemini-cli/pull/22726)
|
||||
- Changelog for v0.35.0-preview.2 by @gemini-cli-robot in
|
||||
[#23142](https://github.com/google-gemini/gemini-cli/pull/23142)
|
||||
- Update website issue template for label and title by @g-samroberts in
|
||||
[#23036](https://github.com/google-gemini/gemini-cli/pull/23036)
|
||||
- fix: upgrade ACP SDK from 0.12 to 0.16.1 by @sripasg in
|
||||
[#23132](https://github.com/google-gemini/gemini-cli/pull/23132)
|
||||
- Update callouts to work on github. by @g-samroberts in
|
||||
[#22245](https://github.com/google-gemini/gemini-cli/pull/22245)
|
||||
- feat: ACP: Add token usage metadata to the `send` method's return value by
|
||||
@sripasg in [#23148](https://github.com/google-gemini/gemini-cli/pull/23148)
|
||||
- fix(plan): clarify that plan mode policies are combined with normal mode by
|
||||
@ruomengz in [#23158](https://github.com/google-gemini/gemini-cli/pull/23158)
|
||||
- Add ModelChain support to ModelConfigService and make ModelDialog dynamic by
|
||||
@kevinjwang1 in
|
||||
[#22914](https://github.com/google-gemini/gemini-cli/pull/22914)
|
||||
- Ensure that copied extensions are writable in the user's local directory by
|
||||
@kevinjwang1 in
|
||||
[#23016](https://github.com/google-gemini/gemini-cli/pull/23016)
|
||||
- feat(core): implement native Windows sandboxing by @mattKorwel in
|
||||
[#21807](https://github.com/google-gemini/gemini-cli/pull/21807)
|
||||
- feat(core): add support for admin-forced MCP server installations by
|
||||
@gsquared94 in
|
||||
[#23163](https://github.com/google-gemini/gemini-cli/pull/23163)
|
||||
- chore(lint): ignore .gemini directory and recursive node_modules by
|
||||
@mattKorwel in
|
||||
[#23211](https://github.com/google-gemini/gemini-cli/pull/23211)
|
||||
- feat(cli): conditionally exclude ask_user tool in ACP mode by @nmcnamara-eng
|
||||
in [#23045](https://github.com/google-gemini/gemini-cli/pull/23045)
|
||||
- feat(core): introduce AgentSession and rename stream events to agent events by
|
||||
@mbleigh in [#23159](https://github.com/google-gemini/gemini-cli/pull/23159)
|
||||
- feat(worktree): add Git worktree support for isolated parallel sessions by
|
||||
@jerop in [#22973](https://github.com/google-gemini/gemini-cli/pull/22973)
|
||||
- Add support for linking in the extension registry by @kevinjwang1 in
|
||||
[#23153](https://github.com/google-gemini/gemini-cli/pull/23153)
|
||||
- feat(extensions): add --skip-settings flag to install command by @Ratish1 in
|
||||
[#17212](https://github.com/google-gemini/gemini-cli/pull/17212)
|
||||
- feat(telemetry): track if session is running in a Git worktree by @jerop in
|
||||
[#23265](https://github.com/google-gemini/gemini-cli/pull/23265)
|
||||
- refactor(core): use absolute paths in GEMINI.md context markers by
|
||||
@SandyTao520 in
|
||||
[#22234](https://github.com/google-gemini/gemini-cli/pull/22234)
|
||||
- feat(prompts): implement Topic-Action-Summary model for verbosity reduction by
|
||||
@Abhijit-2592 in
|
||||
[#21503](https://github.com/google-gemini/gemini-cli/pull/21503)
|
||||
- fix(core): fix manual deletion of subagent histories by @abhipatel12 in
|
||||
[#22407](https://github.com/google-gemini/gemini-cli/pull/22407)
|
||||
- Add registry var by @kevinjwang1 in
|
||||
[#22224](https://github.com/google-gemini/gemini-cli/pull/22224)
|
||||
- Add ModelDefinitions to ModelConfigService by @kevinjwang1 in
|
||||
[#22302](https://github.com/google-gemini/gemini-cli/pull/22302)
|
||||
- fix(cli): improve command conflict handling for skills by @NTaylorMullen in
|
||||
[#21942](https://github.com/google-gemini/gemini-cli/pull/21942)
|
||||
- fix(core): merge user settings with extension-provided MCP servers by
|
||||
[#23135](https://github.com/google-gemini/gemini-cli/pull/23135)
|
||||
- fix(core): add sanitization to sub agent thoughts and centralize utilities by
|
||||
@devr0306 in [#22828](https://github.com/google-gemini/gemini-cli/pull/22828)
|
||||
- feat(core): refine User-Agent for VS Code traffic (unified format) by
|
||||
@sehoon38 in [#23256](https://github.com/google-gemini/gemini-cli/pull/23256)
|
||||
- Fix schema for ModelChains by @kevinjwang1 in
|
||||
[#23284](https://github.com/google-gemini/gemini-cli/pull/23284)
|
||||
- test(cli): refactor tests for async render utilities by @scidomino in
|
||||
[#23252](https://github.com/google-gemini/gemini-cli/pull/23252)
|
||||
- feat(core): add security prompt for browser agent by @cynthialong0-0 in
|
||||
[#23241](https://github.com/google-gemini/gemini-cli/pull/23241)
|
||||
- refactor(ide): replace dynamic undici import with static fetch import by
|
||||
@cocosheng-g in
|
||||
[#23268](https://github.com/google-gemini/gemini-cli/pull/23268)
|
||||
- test(cli): address unresolved feedback from PR #23252 by @scidomino in
|
||||
[#23303](https://github.com/google-gemini/gemini-cli/pull/23303)
|
||||
- feat(browser): add sensitive action controls and read-only noise reduction by
|
||||
@cynthialong0-0 in
|
||||
[#22867](https://github.com/google-gemini/gemini-cli/pull/22867)
|
||||
- Disabling failing test while investigating by @alisa-alisa in
|
||||
[#23311](https://github.com/google-gemini/gemini-cli/pull/23311)
|
||||
- fix broken extension link in hooks guide by @Indrapal-70 in
|
||||
[#21728](https://github.com/google-gemini/gemini-cli/pull/21728)
|
||||
- fix(core): fix agent description indentation by @abhipatel12 in
|
||||
[#23315](https://github.com/google-gemini/gemini-cli/pull/23315)
|
||||
- Wrap the text under TOML rule for easier readability in policy-engine.md… by
|
||||
@CogitationOps in
|
||||
[#23076](https://github.com/google-gemini/gemini-cli/pull/23076)
|
||||
- fix(extensions): revert broken extension removal behavior by @ehedlund in
|
||||
[#23317](https://github.com/google-gemini/gemini-cli/pull/23317)
|
||||
- feat(core): set up onboarding telemetry by @yunaseoul in
|
||||
[#23118](https://github.com/google-gemini/gemini-cli/pull/23118)
|
||||
- Retry evals on API error. by @gundermanc in
|
||||
[#23322](https://github.com/google-gemini/gemini-cli/pull/23322)
|
||||
- fix(evals): remove tool restrictions and add compile-time guards by
|
||||
@SandyTao520 in
|
||||
[#23312](https://github.com/google-gemini/gemini-cli/pull/23312)
|
||||
- fix(hooks): support 'ask' decision for BeforeTool hooks by @gundermanc in
|
||||
[#21146](https://github.com/google-gemini/gemini-cli/pull/21146)
|
||||
- feat(browser): add warning message for session mode 'existing' by
|
||||
@cynthialong0-0 in
|
||||
[#23288](https://github.com/google-gemini/gemini-cli/pull/23288)
|
||||
- chore(lint): enforce zero warnings and cleanup syntax restrictions by
|
||||
@alisa-alisa in
|
||||
[#22902](https://github.com/google-gemini/gemini-cli/pull/22902)
|
||||
- fix(cli): add Esc instruction to HooksDialog footer by @abhipatel12 in
|
||||
[#23258](https://github.com/google-gemini/gemini-cli/pull/23258)
|
||||
- Disallow and suppress misused spread operator. by @gundermanc in
|
||||
[#23294](https://github.com/google-gemini/gemini-cli/pull/23294)
|
||||
- fix(core): refine CliHelpAgent description for better delegation by
|
||||
@abhipatel12 in
|
||||
[#22484](https://github.com/google-gemini/gemini-cli/pull/22484)
|
||||
- fix(core): skip discovery for incomplete MCP configs and resolve merge race
|
||||
condition by @abhipatel12 in
|
||||
[#22494](https://github.com/google-gemini/gemini-cli/pull/22494)
|
||||
- fix(automation): harden stale PR closer permissions and maintainer detection
|
||||
by @bdmorgan in
|
||||
[#22558](https://github.com/google-gemini/gemini-cli/pull/22558)
|
||||
- fix(automation): evaluate staleness before checking protected labels by
|
||||
@bdmorgan in [#22561](https://github.com/google-gemini/gemini-cli/pull/22561)
|
||||
- feat(agent): replace the runtime npx for browser agent chrome devtool mcp with
|
||||
pre-built bundle by @cynthialong0-0 in
|
||||
[#22213](https://github.com/google-gemini/gemini-cli/pull/22213)
|
||||
- perf: optimize TrackerService dependency checks by @anj-s in
|
||||
[#22384](https://github.com/google-gemini/gemini-cli/pull/22384)
|
||||
- docs(policy): remove trailing space from commandPrefix examples by @kawasin73
|
||||
in [#22264](https://github.com/google-gemini/gemini-cli/pull/22264)
|
||||
- fix(a2a-server): resolve unsafe assignment lint errors by @ehedlund in
|
||||
[#22661](https://github.com/google-gemini/gemini-cli/pull/22661)
|
||||
- fix: Adjust ToolGroupMessage filtering to hide Confirming and show Canceled
|
||||
tool calls. by @sripasg in
|
||||
[#22230](https://github.com/google-gemini/gemini-cli/pull/22230)
|
||||
- Disallow Object.create() and reflect. by @gundermanc in
|
||||
[#22408](https://github.com/google-gemini/gemini-cli/pull/22408)
|
||||
- Guard pro model usage by @sehoon38 in
|
||||
[#22665](https://github.com/google-gemini/gemini-cli/pull/22665)
|
||||
- refactor(core): Creates AgentSession abstraction for consolidated agent
|
||||
interface. by @mbleigh in
|
||||
[#22270](https://github.com/google-gemini/gemini-cli/pull/22270)
|
||||
- docs(changelog): remove internal commands from release notes by
|
||||
[#23310](https://github.com/google-gemini/gemini-cli/pull/23310)
|
||||
- fix(core): enable global session and persistent approval for web_fetch by
|
||||
@NTaylorMullen in
|
||||
[#23295](https://github.com/google-gemini/gemini-cli/pull/23295)
|
||||
- fix(plan): add state transition override to prevent plan mode freeze by
|
||||
@Adib234 in [#23020](https://github.com/google-gemini/gemini-cli/pull/23020)
|
||||
- fix(cli): record skill activation tool calls in chat history by @NTaylorMullen
|
||||
in [#23203](https://github.com/google-gemini/gemini-cli/pull/23203)
|
||||
- fix(core): ensure subagent tool updates apply configuration overrides
|
||||
immediately by @abhipatel12 in
|
||||
[#23161](https://github.com/google-gemini/gemini-cli/pull/23161)
|
||||
- fix(cli): resolve flicker at boundaries of list in BaseSelectionList by
|
||||
@jackwotherspoon in
|
||||
[#22529](https://github.com/google-gemini/gemini-cli/pull/22529)
|
||||
- feat: enable subagents by @abhipatel12 in
|
||||
[#22386](https://github.com/google-gemini/gemini-cli/pull/22386)
|
||||
- feat(extensions): implement cryptographic integrity verification for extension
|
||||
updates by @ehedlund in
|
||||
[#21772](https://github.com/google-gemini/gemini-cli/pull/21772)
|
||||
- feat(tracker): polish UI sorting and formatting by @anj-s in
|
||||
[#22437](https://github.com/google-gemini/gemini-cli/pull/22437)
|
||||
- Changelog for v0.34.0-preview.2 by @gemini-cli-robot in
|
||||
[#22220](https://github.com/google-gemini/gemini-cli/pull/22220)
|
||||
- fix(core): fix three JIT context bugs in read_file, read_many_files, and
|
||||
memoryDiscovery by @SandyTao520 in
|
||||
[#22679](https://github.com/google-gemini/gemini-cli/pull/22679)
|
||||
- refactor(core): introduce InjectionService with source-aware injection and
|
||||
backend-native background completions by @adamfweidman in
|
||||
[#22544](https://github.com/google-gemini/gemini-cli/pull/22544)
|
||||
- Linux sandbox bubblewrap by @DavidAPierce in
|
||||
[#22680](https://github.com/google-gemini/gemini-cli/pull/22680)
|
||||
- feat(core): increase thought signature retry resilience by @bdmorgan in
|
||||
[#22202](https://github.com/google-gemini/gemini-cli/pull/22202)
|
||||
- feat(core): implement Stage 2 security and consistency improvements for
|
||||
web_fetch by @aishaneeshah in
|
||||
[#22217](https://github.com/google-gemini/gemini-cli/pull/22217)
|
||||
- refactor(core): replace positional execute params with ExecuteOptions bag by
|
||||
[#23298](https://github.com/google-gemini/gemini-cli/pull/23298)
|
||||
- test(cli): force generic terminal in tests to fix snapshot failures by
|
||||
@abhipatel12 in
|
||||
[#23499](https://github.com/google-gemini/gemini-cli/pull/23499)
|
||||
- Evals: PR Guidance adding workflow by @alisa-alisa in
|
||||
[#23164](https://github.com/google-gemini/gemini-cli/pull/23164)
|
||||
- feat(core): refactor SandboxManager to a stateless architecture and introduce
|
||||
explicit Deny interface by @ehedlund in
|
||||
[#23141](https://github.com/google-gemini/gemini-cli/pull/23141)
|
||||
- feat(core): add event-translator and update agent types by @adamfweidman in
|
||||
[#22985](https://github.com/google-gemini/gemini-cli/pull/22985)
|
||||
- perf(cli): parallelize and background startup cleanup tasks by @sehoon38 in
|
||||
[#23545](https://github.com/google-gemini/gemini-cli/pull/23545)
|
||||
- fix: "allow always" for commands with paths by @scidomino in
|
||||
[#23558](https://github.com/google-gemini/gemini-cli/pull/23558)
|
||||
- fix(cli): prevent terminal escape sequences from leaking on exit by
|
||||
@mattKorwel in
|
||||
[#22682](https://github.com/google-gemini/gemini-cli/pull/22682)
|
||||
- feat(cli): implement full "GEMINI CLI" logo for logged-out state by
|
||||
@keithguerin in
|
||||
[#22412](https://github.com/google-gemini/gemini-cli/pull/22412)
|
||||
- fix(plan): reserve minimum height for selection list in AskUserDialog by
|
||||
@ruomengz in [#23280](https://github.com/google-gemini/gemini-cli/pull/23280)
|
||||
- fix(core): harden AgentSession replay semantics by @adamfweidman in
|
||||
[#23548](https://github.com/google-gemini/gemini-cli/pull/23548)
|
||||
- test(core): migrate hook tests to scheduler by @abhipatel12 in
|
||||
[#23496](https://github.com/google-gemini/gemini-cli/pull/23496)
|
||||
- chore(config): disable agents by default by @abhipatel12 in
|
||||
[#23546](https://github.com/google-gemini/gemini-cli/pull/23546)
|
||||
- fix(ui): make tool confirmations take up entire terminal height by @devr0306
|
||||
in [#22366](https://github.com/google-gemini/gemini-cli/pull/22366)
|
||||
- fix(core): prevent redundant remote agent loading on model switch by
|
||||
@adamfweidman in
|
||||
[#22674](https://github.com/google-gemini/gemini-cli/pull/22674)
|
||||
- feat(config): enable JIT context loading by default by @SandyTao520 in
|
||||
[#22736](https://github.com/google-gemini/gemini-cli/pull/22736)
|
||||
- fix(config): ensure discoveryMaxDirs is passed to global config during
|
||||
initialization by @kevin-ramdass in
|
||||
[#22744](https://github.com/google-gemini/gemini-cli/pull/22744)
|
||||
- fix(plan): allowlist get_internal_docs in Plan Mode by @Adib234 in
|
||||
[#22668](https://github.com/google-gemini/gemini-cli/pull/22668)
|
||||
- Changelog for v0.34.0-preview.3 by @gemini-cli-robot in
|
||||
[#22393](https://github.com/google-gemini/gemini-cli/pull/22393)
|
||||
- feat(core): add foundation for subagent tool isolation by @akh64bit in
|
||||
[#22708](https://github.com/google-gemini/gemini-cli/pull/22708)
|
||||
- fix(core): handle surrogate pairs in truncateString by @sehoon38 in
|
||||
[#22754](https://github.com/google-gemini/gemini-cli/pull/22754)
|
||||
- fix(cli): override j/k navigation in settings dialog to fix search input
|
||||
conflict by @sehoon38 in
|
||||
[#22800](https://github.com/google-gemini/gemini-cli/pull/22800)
|
||||
- feat(plan): add 'All the above' option to multi-select AskUser questions by
|
||||
@Adib234 in [#22365](https://github.com/google-gemini/gemini-cli/pull/22365)
|
||||
- docs: distribute package-specific GEMINI.md context to each package by
|
||||
[#23576](https://github.com/google-gemini/gemini-cli/pull/23576)
|
||||
- refactor(core): update production type imports from coreToolScheduler by
|
||||
@abhipatel12 in
|
||||
[#23498](https://github.com/google-gemini/gemini-cli/pull/23498)
|
||||
- feat(cli): always prefix extension skills with colon separator by
|
||||
@NTaylorMullen in
|
||||
[#23566](https://github.com/google-gemini/gemini-cli/pull/23566)
|
||||
- fix(core): properly support allowRedirect in policy engine by @scidomino in
|
||||
[#23579](https://github.com/google-gemini/gemini-cli/pull/23579)
|
||||
- fix(cli): prevent subcommand shadowing and skip auth for commands by
|
||||
@mattKorwel in
|
||||
[#23177](https://github.com/google-gemini/gemini-cli/pull/23177)
|
||||
- fix(test): move flaky tests to non-blocking suite by @mattKorwel in
|
||||
[#23259](https://github.com/google-gemini/gemini-cli/pull/23259)
|
||||
- Changelog for v0.35.0-preview.3 by @gemini-cli-robot in
|
||||
[#23574](https://github.com/google-gemini/gemini-cli/pull/23574)
|
||||
- feat(skills): add behavioral-evals skill with fixing and promoting guides by
|
||||
@abhipatel12 in
|
||||
[#23349](https://github.com/google-gemini/gemini-cli/pull/23349)
|
||||
- refactor(core): delete obsolete coreToolScheduler by @abhipatel12 in
|
||||
[#23502](https://github.com/google-gemini/gemini-cli/pull/23502)
|
||||
- Changelog for v0.35.0-preview.4 by @gemini-cli-robot in
|
||||
[#23581](https://github.com/google-gemini/gemini-cli/pull/23581)
|
||||
- feat(core): add LegacyAgentSession by @adamfweidman in
|
||||
[#22986](https://github.com/google-gemini/gemini-cli/pull/22986)
|
||||
- feat(test-utils): add TestMcpServerBuilder and support in TestRig by
|
||||
@abhipatel12 in
|
||||
[#23491](https://github.com/google-gemini/gemini-cli/pull/23491)
|
||||
- fix(core)!: Force policy config to specify toolName by @kschaab in
|
||||
[#23330](https://github.com/google-gemini/gemini-cli/pull/23330)
|
||||
- eval(save_memory): add multi-turn interactive evals for memoryManager by
|
||||
@SandyTao520 in
|
||||
[#22734](https://github.com/google-gemini/gemini-cli/pull/22734)
|
||||
- fix(cli): clean up stale pasted placeholder metadata after word/line deletions
|
||||
by @Jomak-x in
|
||||
[#20375](https://github.com/google-gemini/gemini-cli/pull/20375)
|
||||
- refactor(core): align JIT memory placement with tiered context model by
|
||||
@SandyTao520 in
|
||||
[#22766](https://github.com/google-gemini/gemini-cli/pull/22766)
|
||||
- Linux sandbox seccomp by @DavidAPierce in
|
||||
[#22815](https://github.com/google-gemini/gemini-cli/pull/22815)
|
||||
- fix(patch): cherry-pick 4e5dfd0 to release/v0.35.0-preview.1-pr-23074 to patch
|
||||
version v0.35.0-preview.1 and create version 0.35.0-preview.2 by
|
||||
[#23572](https://github.com/google-gemini/gemini-cli/pull/23572)
|
||||
- fix(telemetry): patch memory leak and enforce logPrompts privacy by
|
||||
@spencer426 in
|
||||
[#23281](https://github.com/google-gemini/gemini-cli/pull/23281)
|
||||
- perf(cli): background IDE client to speed up initialization by @sehoon38 in
|
||||
[#23603](https://github.com/google-gemini/gemini-cli/pull/23603)
|
||||
- fix(cli): prevent Ctrl+D exit when input buffer is not empty by @wtanaka in
|
||||
[#23306](https://github.com/google-gemini/gemini-cli/pull/23306)
|
||||
- fix: ACP: separate conversational text from execute tool command title by
|
||||
@sripasg in [#23179](https://github.com/google-gemini/gemini-cli/pull/23179)
|
||||
- feat(evals): add behavioral evaluations for subagent routing by @Samee24 in
|
||||
[#23272](https://github.com/google-gemini/gemini-cli/pull/23272)
|
||||
- refactor(cli,core): foundational layout, identity management, and type safety
|
||||
by @jwhelangoog in
|
||||
[#23286](https://github.com/google-gemini/gemini-cli/pull/23286)
|
||||
- fix(core): accurately reflect subagent tool failure in UI by @abhipatel12 in
|
||||
[#23187](https://github.com/google-gemini/gemini-cli/pull/23187)
|
||||
- Changelog for v0.35.0-preview.5 by @gemini-cli-robot in
|
||||
[#23606](https://github.com/google-gemini/gemini-cli/pull/23606)
|
||||
- feat(ui): implement refreshed UX for Composer layout by @jwhelangoog in
|
||||
[#21212](https://github.com/google-gemini/gemini-cli/pull/21212)
|
||||
- fix: API key input dialog user interaction when selected Gemini API Key by
|
||||
@kartikangiras in
|
||||
[#21057](https://github.com/google-gemini/gemini-cli/pull/21057)
|
||||
- docs: update `/mcp refresh` to `/mcp reload` by @adamfweidman in
|
||||
[#23631](https://github.com/google-gemini/gemini-cli/pull/23631)
|
||||
- Implementation of sandbox "Write-Protected" Governance Files by @DavidAPierce
|
||||
in [#23139](https://github.com/google-gemini/gemini-cli/pull/23139)
|
||||
- feat(sandbox): dynamic macOS sandbox expansion and worktree support by @galz10
|
||||
in [#23301](https://github.com/google-gemini/gemini-cli/pull/23301)
|
||||
- fix(acp): Pass the cwd to `AcpFileSystemService` to avoid looping failures in
|
||||
asking for perms to write plan md file by @sripasg in
|
||||
[#23612](https://github.com/google-gemini/gemini-cli/pull/23612)
|
||||
- fix(plan): sandbox path resolution in Plan Mode to prevent hallucinations by
|
||||
@Adib234 in [#22737](https://github.com/google-gemini/gemini-cli/pull/22737)
|
||||
- feat(ui): allow immediate user input during startup by @sehoon38 in
|
||||
[#23661](https://github.com/google-gemini/gemini-cli/pull/23661)
|
||||
- refactor(sandbox): reorganize Windows sandbox files by @galz10 in
|
||||
[#23645](https://github.com/google-gemini/gemini-cli/pull/23645)
|
||||
- fix(core): improve remote agent streaming UI and UX by @adamfweidman in
|
||||
[#23633](https://github.com/google-gemini/gemini-cli/pull/23633)
|
||||
- perf(cli): optimize --version startup time by @sehoon38 in
|
||||
[#23671](https://github.com/google-gemini/gemini-cli/pull/23671)
|
||||
- refactor(core): stop gemini CLI from producing unsafe casts by @gundermanc in
|
||||
[#23611](https://github.com/google-gemini/gemini-cli/pull/23611)
|
||||
- use enableAutoUpdate in test rig by @scidomino in
|
||||
[#23681](https://github.com/google-gemini/gemini-cli/pull/23681)
|
||||
- feat(core): change user-facing auth type from oauth2 to oauth by @adamfweidman
|
||||
in [#23639](https://github.com/google-gemini/gemini-cli/pull/23639)
|
||||
- chore(deps): fix npm audit vulnerabilities by @scidomino in
|
||||
[#23679](https://github.com/google-gemini/gemini-cli/pull/23679)
|
||||
- test(evals): fix overlapping act() deadlock in app-test-helper by @Adib234 in
|
||||
[#23666](https://github.com/google-gemini/gemini-cli/pull/23666)
|
||||
- fix(patch): cherry-pick 055ff92 to release/v0.36.0-preview.0-pr-23672 to patch
|
||||
version v0.36.0-preview.0 and create version 0.36.0-preview.1 by
|
||||
@gemini-cli-robot in
|
||||
[#23134](https://github.com/google-gemini/gemini-cli/pull/23134)
|
||||
- fix(patch): cherry-pick daf3691 to release/v0.35.0-preview.2-pr-23558 to patch
|
||||
version v0.35.0-preview.2 and create version 0.35.0-preview.3 by
|
||||
[#23723](https://github.com/google-gemini/gemini-cli/pull/23723)
|
||||
- fix(patch): cherry-pick 765fb67 to release/v0.36.0-preview.5-pr-24055 to patch
|
||||
version v0.36.0-preview.5 and create version 0.36.0-preview.6 by
|
||||
@gemini-cli-robot in
|
||||
[#23565](https://github.com/google-gemini/gemini-cli/pull/23565)
|
||||
- fix(patch): cherry-pick b2d6dc4 to release/v0.35.0-preview.4-pr-23546
|
||||
[CONFLICTS] by @gemini-cli-robot in
|
||||
[#23585](https://github.com/google-gemini/gemini-cli/pull/23585)
|
||||
[#24061](https://github.com/google-gemini/gemini-cli/pull/24061)
|
||||
|
||||
**Full Changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.34.0...v0.35.3
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.35.3...v0.36.0
|
||||
|
||||
+395
-366
@@ -1,6 +1,6 @@
|
||||
# Preview release: v0.36.0-preview.8
|
||||
# Preview release: v0.37.0-preview.1
|
||||
|
||||
Released: April 01, 2026
|
||||
Released: April 02, 2026
|
||||
|
||||
Our preview release includes the latest, new, and experimental features. This
|
||||
release may not be as stable as our [latest weekly release](latest.md).
|
||||
@@ -13,381 +13,410 @@ npm install -g @google/gemini-cli@preview
|
||||
|
||||
## Highlights
|
||||
|
||||
- **Subagent Architecture Enhancements:** Significant updates to subagents,
|
||||
including local execution, tool isolation, multi-registry discovery, dynamic
|
||||
tool filtering, and JIT context injection.
|
||||
- **Enhanced Security & Sandboxing:** Implemented strict macOS sandboxing using
|
||||
Seatbelt allowlist, native Windows sandboxing, and support for
|
||||
"Write-Protected" governance files.
|
||||
- **Agent Context & State Management:** Introduced task tracker protocol
|
||||
integration, 'blocked' statuses for tasks/todos, and `AgentSession` for
|
||||
improved state management and replay semantics.
|
||||
- **Browser & ACP Capabilities:** Added privacy consent for the browser agent,
|
||||
sensitive action controls, improved API token usage metadata, and gateway auth
|
||||
support via ACP.
|
||||
- **CLI & UX Improvements:** Implemented a refreshed Composer layout, expanded
|
||||
terminal fallback warnings, dynamic model resolution, and Git worktree support
|
||||
for isolated parallel sessions.
|
||||
- **Plan Mode Enhancements**: Plan now includes support for untrusted folders,
|
||||
prioritized pre-approval discussions, and a resolve for sandbox-related
|
||||
deadlocks during file creation.
|
||||
- **Browser Agent Evolved**: Significant updates to the browser agent, including
|
||||
persistent session management, dynamic discovery of read-only tools,
|
||||
sandbox-aware initialization, and automated reclamation of stale snapshots to
|
||||
optimize context window usage.
|
||||
- **Advanced Sandbox Security**: Implementation of dynamic sandbox expansion for
|
||||
both Linux and Windows, alongside secret visibility lockdown for environment
|
||||
files and OS-specific forbidden path support.
|
||||
- **Unified Core Architecture**: Centralized context management and a new
|
||||
`ModelConfigService` for unified model discovery, complemented by the
|
||||
introduction of `AgentHistoryProvider` and tool-based topic grouping
|
||||
(Chapters).
|
||||
- **UI/UX & Performance Improvements**: New Tokyo Night theme, "tab to queue"
|
||||
message support, and compact tool output formatting, plus optimized build
|
||||
scripts and improved layout stability for TUI components.
|
||||
|
||||
## What's Changed
|
||||
|
||||
- fix(patch): cherry-pick 765fb67 to release/v0.36.0-preview.5-pr-24055 to patch
|
||||
version v0.36.0-preview.5 and create version 0.36.0-preview.6 by
|
||||
- fix(patch): cherry-pick 64c928f to release/v0.37.0-preview.0-pr-23257 to patch
|
||||
version v0.37.0-preview.0 and create version 0.37.0-preview.1 by
|
||||
@gemini-cli-robot in
|
||||
[#24061](https://github.com/google-gemini/gemini-cli/pull/24061)
|
||||
- fix(a2a-server): A2A server should execute ask policies in interactive mode by
|
||||
@kschaab in [#23831](https://github.com/google-gemini/gemini-cli/pull/23831)
|
||||
[#24561](https://github.com/google-gemini/gemini-cli/pull/24561)
|
||||
- feat(evals): centralize test agents into test-utils for reuse by @Samee24 in
|
||||
[#23616](https://github.com/google-gemini/gemini-cli/pull/23616)
|
||||
- revert: chore(config): disable agents by default by @abhipatel12 in
|
||||
[#23672](https://github.com/google-gemini/gemini-cli/pull/23672)
|
||||
- fix(plan): update telemetry attribute keys and add timestamp by @Adib234 in
|
||||
[#23685](https://github.com/google-gemini/gemini-cli/pull/23685)
|
||||
- fix(core): prevent premature MCP discovery completion by @jackwotherspoon in
|
||||
[#23637](https://github.com/google-gemini/gemini-cli/pull/23637)
|
||||
- feat(browser): add maxActionsPerTask for browser agent setting by
|
||||
@cynthialong0-0 in
|
||||
[#23216](https://github.com/google-gemini/gemini-cli/pull/23216)
|
||||
- fix(core): improve agent loader error formatting for empty paths by
|
||||
@adamfweidman in
|
||||
[#23690](https://github.com/google-gemini/gemini-cli/pull/23690)
|
||||
- fix(cli): only show updating spinner when auto-update is in progress by
|
||||
@scidomino in [#23709](https://github.com/google-gemini/gemini-cli/pull/23709)
|
||||
- Refine onboarding metrics to log the duration explicitly and use the tier
|
||||
name. by @yunaseoul in
|
||||
[#23678](https://github.com/google-gemini/gemini-cli/pull/23678)
|
||||
- chore(tools): add toJSON to tools and invocations to reduce logging verbosity
|
||||
by @alisa-alisa in
|
||||
[#22899](https://github.com/google-gemini/gemini-cli/pull/22899)
|
||||
- fix(cli): stabilize copy mode to prevent flickering and cursor resets by
|
||||
@mattKorwel in
|
||||
[#22584](https://github.com/google-gemini/gemini-cli/pull/22584)
|
||||
- fix(test): move flaky ctrl-c-exit test to non-blocking suite by @mattKorwel in
|
||||
[#23732](https://github.com/google-gemini/gemini-cli/pull/23732)
|
||||
- feat(skills): add ci skill for automated failure replication by @mattKorwel in
|
||||
[#23720](https://github.com/google-gemini/gemini-cli/pull/23720)
|
||||
- feat(sandbox): implement forbiddenPaths for OS-specific sandbox managers by
|
||||
@ehedlund in [#23282](https://github.com/google-gemini/gemini-cli/pull/23282)
|
||||
- fix(core): conditionally expose additional_permissions in shell tool by
|
||||
@galz10 in [#23729](https://github.com/google-gemini/gemini-cli/pull/23729)
|
||||
- refactor(core): standardize OS-specific sandbox tests and extract linux helper
|
||||
methods by @ehedlund in
|
||||
[#23715](https://github.com/google-gemini/gemini-cli/pull/23715)
|
||||
- format recently added script by @scidomino in
|
||||
[#23739](https://github.com/google-gemini/gemini-cli/pull/23739)
|
||||
- fix(ui): prevent over-eager slash subcommand completion by @keithguerin in
|
||||
[#20136](https://github.com/google-gemini/gemini-cli/pull/20136)
|
||||
- Fix dynamic model routing for gemini 3.1 pro to customtools model by
|
||||
@kevinjwang1 in
|
||||
[#23641](https://github.com/google-gemini/gemini-cli/pull/23641)
|
||||
- feat(core): support inline agentCardJson for remote agents by @adamfweidman in
|
||||
[#23743](https://github.com/google-gemini/gemini-cli/pull/23743)
|
||||
- fix(cli): skip console log/info in headless mode by @cynthialong0-0 in
|
||||
[#22739](https://github.com/google-gemini/gemini-cli/pull/22739)
|
||||
- test(core): install bubblewrap on Linux CI for sandbox integration tests by
|
||||
@ehedlund in [#23583](https://github.com/google-gemini/gemini-cli/pull/23583)
|
||||
- docs(reference): split tools table into category sections by @sheikhlimon in
|
||||
[#21516](https://github.com/google-gemini/gemini-cli/pull/21516)
|
||||
- fix(browser): detect embedded URLs in query params to prevent allowedDomains
|
||||
bypass by @tony-shi in
|
||||
[#23225](https://github.com/google-gemini/gemini-cli/pull/23225)
|
||||
- fix(browser): add proxy bypass constraint to domain restriction system prompt
|
||||
by @tony-shi in
|
||||
[#23229](https://github.com/google-gemini/gemini-cli/pull/23229)
|
||||
- fix(policy): relax write_file argsPattern in plan mode to allow paths without
|
||||
session ID by @Adib234 in
|
||||
[#23695](https://github.com/google-gemini/gemini-cli/pull/23695)
|
||||
- docs: fix grammar in CONTRIBUTING and numbering in sandbox docs by
|
||||
@splint-disk-8i in
|
||||
[#23448](https://github.com/google-gemini/gemini-cli/pull/23448)
|
||||
- fix(acp): allow attachments by adding a permission prompt by @sripasg in
|
||||
[#23680](https://github.com/google-gemini/gemini-cli/pull/23680)
|
||||
- fix(core): thread AbortSignal to chat compression requests (#20405) by
|
||||
@SH20RAJ in [#20778](https://github.com/google-gemini/gemini-cli/pull/20778)
|
||||
- feat(core): implement Windows sandbox dynamic expansion Phase 1 and 2.1 by
|
||||
@scidomino in [#23691](https://github.com/google-gemini/gemini-cli/pull/23691)
|
||||
- Add note about root privileges in sandbox docs by @diodesign in
|
||||
[#23314](https://github.com/google-gemini/gemini-cli/pull/23314)
|
||||
- docs(core): document agent_card_json string literal options for remote agents
|
||||
by @adamfweidman in
|
||||
[#23797](https://github.com/google-gemini/gemini-cli/pull/23797)
|
||||
- feat(core): support inline agentCardJson for remote agents by @adamfweidman in
|
||||
[#23743](https://github.com/google-gemini/gemini-cli/pull/23743)
|
||||
- fix(patch): cherry-pick 055ff92 to release/v0.36.0-preview.0-pr-23672 to patch
|
||||
version v0.36.0-preview.0 and create version 0.36.0-preview.1 by
|
||||
@gemini-cli-robot in
|
||||
[#23723](https://github.com/google-gemini/gemini-cli/pull/23723)
|
||||
- Changelog for v0.33.2 by @gemini-cli-robot in
|
||||
[#22730](https://github.com/google-gemini/gemini-cli/pull/22730)
|
||||
- feat(core): multi-registry architecture and tool filtering for subagents by
|
||||
@akh64bit in [#22712](https://github.com/google-gemini/gemini-cli/pull/22712)
|
||||
- Changelog for v0.34.0-preview.4 by @gemini-cli-robot in
|
||||
[#22752](https://github.com/google-gemini/gemini-cli/pull/22752)
|
||||
- fix(devtools): use theme-aware text colors for console warnings and errors by
|
||||
@SandyTao520 in
|
||||
[#22181](https://github.com/google-gemini/gemini-cli/pull/22181)
|
||||
- Add support for dynamic model Resolution to ModelConfigService by @kevinjwang1
|
||||
in [#22578](https://github.com/google-gemini/gemini-cli/pull/22578)
|
||||
- chore(release): bump version to 0.36.0-nightly.20260317.2f90b4653 by
|
||||
@gemini-cli-robot in
|
||||
[#22858](https://github.com/google-gemini/gemini-cli/pull/22858)
|
||||
- fix(cli): use active sessionId in useLogger and improve resume robustness by
|
||||
@mattKorwel in
|
||||
[#22606](https://github.com/google-gemini/gemini-cli/pull/22606)
|
||||
- fix(cli): expand tilde in policy paths from settings.json by @abhipatel12 in
|
||||
[#22772](https://github.com/google-gemini/gemini-cli/pull/22772)
|
||||
- fix(core): add actionable warnings for terminal fallbacks (#14426) by
|
||||
@spencer426 in
|
||||
[#22211](https://github.com/google-gemini/gemini-cli/pull/22211)
|
||||
- feat(tracker): integrate task tracker protocol into core system prompt by
|
||||
@anj-s in [#22442](https://github.com/google-gemini/gemini-cli/pull/22442)
|
||||
- chore: add posttest build hooks and fix missing dependencies by @NTaylorMullen
|
||||
in [#22865](https://github.com/google-gemini/gemini-cli/pull/22865)
|
||||
- feat(a2a): add agent acknowledgment command and enhance registry discovery by
|
||||
- fix(cli): resolve TTY hang on headless environments by unconditionally
|
||||
resuming process.stdin before React Ink launch by @cocosheng-g in
|
||||
[#23673](https://github.com/google-gemini/gemini-cli/pull/23673)
|
||||
- fix(ui): cleanup estimated string length hacks in composer by @keithguerin in
|
||||
[#23694](https://github.com/google-gemini/gemini-cli/pull/23694)
|
||||
- feat(browser): dynamically discover read-only tools by @cynthialong0-0 in
|
||||
[#23805](https://github.com/google-gemini/gemini-cli/pull/23805)
|
||||
- docs: clarify policy requirement for `general.plan.directory` in settings
|
||||
schema by @jerop in
|
||||
[#23784](https://github.com/google-gemini/gemini-cli/pull/23784)
|
||||
- Revert "perf(cli): optimize --version startup time (#23671)" by @scidomino in
|
||||
[#23812](https://github.com/google-gemini/gemini-cli/pull/23812)
|
||||
- don't silence errors from wombat by @scidomino in
|
||||
[#23822](https://github.com/google-gemini/gemini-cli/pull/23822)
|
||||
- fix(ui): prevent escape key from cancelling requests in shell mode by
|
||||
@PrasannaPal21 in
|
||||
[#21245](https://github.com/google-gemini/gemini-cli/pull/21245)
|
||||
- Changelog for v0.36.0-preview.0 by @gemini-cli-robot in
|
||||
[#23702](https://github.com/google-gemini/gemini-cli/pull/23702)
|
||||
- feat(core,ui): Add experiment-gated support for gemini flash 3.1 lite by
|
||||
@chrstnb in [#23794](https://github.com/google-gemini/gemini-cli/pull/23794)
|
||||
- Changelog for v0.36.0-preview.3 by @gemini-cli-robot in
|
||||
[#23827](https://github.com/google-gemini/gemini-cli/pull/23827)
|
||||
- new linting check: github-actions-pinning by @alisa-alisa in
|
||||
[#23808](https://github.com/google-gemini/gemini-cli/pull/23808)
|
||||
- fix(cli): show helpful guidance when no skills are available by @Niralisj in
|
||||
[#23785](https://github.com/google-gemini/gemini-cli/pull/23785)
|
||||
- fix: Chat logs and errors handle tail tool calls correctly by @googlestrobe in
|
||||
[#22460](https://github.com/google-gemini/gemini-cli/pull/22460)
|
||||
- Don't try removing a tag from a non-existent release. by @scidomino in
|
||||
[#23830](https://github.com/google-gemini/gemini-cli/pull/23830)
|
||||
- fix(cli): allow ask question dialog to take full window height by @jacob314 in
|
||||
[#23693](https://github.com/google-gemini/gemini-cli/pull/23693)
|
||||
- fix(core): strip leading underscores from error types in telemetry by
|
||||
@yunaseoul in [#23824](https://github.com/google-gemini/gemini-cli/pull/23824)
|
||||
- Changelog for v0.35.0 by @gemini-cli-robot in
|
||||
[#23819](https://github.com/google-gemini/gemini-cli/pull/23819)
|
||||
- feat(evals): add reliability harvester and 500/503 retry support by
|
||||
@alisa-alisa in
|
||||
[#22389](https://github.com/google-gemini/gemini-cli/pull/22389)
|
||||
- fix(cli): automatically add all VSCode workspace folders to Gemini context by
|
||||
@sakshisemalti in
|
||||
[#21380](https://github.com/google-gemini/gemini-cli/pull/21380)
|
||||
- feat: add 'blocked' status to tasks and todos by @anj-s in
|
||||
[#22735](https://github.com/google-gemini/gemini-cli/pull/22735)
|
||||
- refactor(cli): remove extra newlines in ShellToolMessage.tsx by @NTaylorMullen
|
||||
in [#22868](https://github.com/google-gemini/gemini-cli/pull/22868)
|
||||
- fix(cli): lazily load settings in onModelChange to prevent stale closure data
|
||||
loss by @KumarADITHYA123 in
|
||||
[#20403](https://github.com/google-gemini/gemini-cli/pull/20403)
|
||||
- feat(core): subagent local execution and tool isolation by @akh64bit in
|
||||
[#22718](https://github.com/google-gemini/gemini-cli/pull/22718)
|
||||
- fix(cli): resolve subagent grouping and UI state persistence by @abhipatel12
|
||||
in [#22252](https://github.com/google-gemini/gemini-cli/pull/22252)
|
||||
- refactor(ui): extract SessionBrowser search and navigation components by
|
||||
@abhipatel12 in
|
||||
[#22377](https://github.com/google-gemini/gemini-cli/pull/22377)
|
||||
- fix: updates Docker image reference for GitHub MCP server by @jhhornn in
|
||||
[#22938](https://github.com/google-gemini/gemini-cli/pull/22938)
|
||||
- refactor(cli): group subagent trajectory deletion and use native filesystem
|
||||
testing by @abhipatel12 in
|
||||
[#22890](https://github.com/google-gemini/gemini-cli/pull/22890)
|
||||
- refactor(cli): simplify keypress and mouse providers and update tests by
|
||||
@scidomino in [#22853](https://github.com/google-gemini/gemini-cli/pull/22853)
|
||||
- Changelog for v0.34.0 by @gemini-cli-robot in
|
||||
[#22860](https://github.com/google-gemini/gemini-cli/pull/22860)
|
||||
- test(cli): simplify createMockSettings calls by @scidomino in
|
||||
[#22952](https://github.com/google-gemini/gemini-cli/pull/22952)
|
||||
- feat(ui): format multi-line banner warnings with a bold title by @keithguerin
|
||||
in [#22955](https://github.com/google-gemini/gemini-cli/pull/22955)
|
||||
- Docs: Remove references to stale Gemini CLI file structure info by
|
||||
@g-samroberts in
|
||||
[#22976](https://github.com/google-gemini/gemini-cli/pull/22976)
|
||||
- feat(ui): remove write todo list tool from UI tips by @aniruddhaadak80 in
|
||||
[#22281](https://github.com/google-gemini/gemini-cli/pull/22281)
|
||||
- Fix issue where subagent thoughts are appended. by @gundermanc in
|
||||
[#22975](https://github.com/google-gemini/gemini-cli/pull/22975)
|
||||
- Feat/browser privacy consent by @kunal-10-cloud in
|
||||
[#21119](https://github.com/google-gemini/gemini-cli/pull/21119)
|
||||
- fix(core): explicitly map execution context in LocalAgentExecutor by @akh64bit
|
||||
in [#22949](https://github.com/google-gemini/gemini-cli/pull/22949)
|
||||
- feat(plan): support plan mode in non-interactive mode by @ruomengz in
|
||||
[#22670](https://github.com/google-gemini/gemini-cli/pull/22670)
|
||||
- feat(core): implement strict macOS sandboxing using Seatbelt allowlist by
|
||||
@ehedlund in [#22832](https://github.com/google-gemini/gemini-cli/pull/22832)
|
||||
- docs: add additional notes by @abhipatel12 in
|
||||
[#23008](https://github.com/google-gemini/gemini-cli/pull/23008)
|
||||
- fix(cli): resolve duplicate footer on tool cancel via ESC (#21743) by
|
||||
@ruomengz in [#21781](https://github.com/google-gemini/gemini-cli/pull/21781)
|
||||
- Changelog for v0.35.0-preview.1 by @gemini-cli-robot in
|
||||
[#23012](https://github.com/google-gemini/gemini-cli/pull/23012)
|
||||
- fix(ui): fix flickering on small terminal heights by @devr0306 in
|
||||
[#21416](https://github.com/google-gemini/gemini-cli/pull/21416)
|
||||
- fix(acp): provide more meta in tool_call_update by @Mervap in
|
||||
[#22663](https://github.com/google-gemini/gemini-cli/pull/22663)
|
||||
- docs: add FAQ entry for checking Gemini CLI version by @surajsahani in
|
||||
[#21271](https://github.com/google-gemini/gemini-cli/pull/21271)
|
||||
- feat(core): resilient subagent tool rejection with contextual feedback by
|
||||
@abhipatel12 in
|
||||
[#22951](https://github.com/google-gemini/gemini-cli/pull/22951)
|
||||
- fix(cli): correctly handle auto-update for standalone binaries by @bdmorgan in
|
||||
[#23038](https://github.com/google-gemini/gemini-cli/pull/23038)
|
||||
- feat(core): add content-utils by @adamfweidman in
|
||||
[#22984](https://github.com/google-gemini/gemini-cli/pull/22984)
|
||||
- fix: circumvent genai sdk requirement for api key when using gateway auth via
|
||||
ACP by @sripasg in
|
||||
[#23042](https://github.com/google-gemini/gemini-cli/pull/23042)
|
||||
- fix(core): don't persist browser consent sentinel in non-interactive mode by
|
||||
@jasonmatthewsuhari in
|
||||
[#23073](https://github.com/google-gemini/gemini-cli/pull/23073)
|
||||
- fix(core): narrow browser agent description to prevent stealing URL tasks from
|
||||
web_fetch by @gsquared94 in
|
||||
[#23086](https://github.com/google-gemini/gemini-cli/pull/23086)
|
||||
- feat(cli): Partial threading of AgentLoopContext. by @joshualitt in
|
||||
[#22978](https://github.com/google-gemini/gemini-cli/pull/22978)
|
||||
- fix(browser-agent): enable "Allow all server tools" session policy by
|
||||
@cynthialong0-0 in
|
||||
[#22343](https://github.com/google-gemini/gemini-cli/pull/22343)
|
||||
- refactor(cli): integrate real config loading into async test utils by
|
||||
@scidomino in [#23040](https://github.com/google-gemini/gemini-cli/pull/23040)
|
||||
- feat(core): inject memory and JIT context into subagents by @abhipatel12 in
|
||||
[#23032](https://github.com/google-gemini/gemini-cli/pull/23032)
|
||||
- Fix logging and virtual list. by @jacob314 in
|
||||
[#23080](https://github.com/google-gemini/gemini-cli/pull/23080)
|
||||
- feat(core): cap JIT context upward traversal at git root by @SandyTao520 in
|
||||
[#23074](https://github.com/google-gemini/gemini-cli/pull/23074)
|
||||
- Docs: Minor style updates from initial docs audit. by @g-samroberts in
|
||||
[#22872](https://github.com/google-gemini/gemini-cli/pull/22872)
|
||||
- feat(core): add experimental memory manager agent to replace save_memory tool
|
||||
by @SandyTao520 in
|
||||
[#22726](https://github.com/google-gemini/gemini-cli/pull/22726)
|
||||
- Changelog for v0.35.0-preview.2 by @gemini-cli-robot in
|
||||
[#23142](https://github.com/google-gemini/gemini-cli/pull/23142)
|
||||
- Update website issue template for label and title by @g-samroberts in
|
||||
[#23036](https://github.com/google-gemini/gemini-cli/pull/23036)
|
||||
- fix: upgrade ACP SDK from 0.12 to 0.16.1 by @sripasg in
|
||||
[#23132](https://github.com/google-gemini/gemini-cli/pull/23132)
|
||||
- Update callouts to work on github. by @g-samroberts in
|
||||
[#22245](https://github.com/google-gemini/gemini-cli/pull/22245)
|
||||
- feat: ACP: Add token usage metadata to the `send` method's return value by
|
||||
@sripasg in [#23148](https://github.com/google-gemini/gemini-cli/pull/23148)
|
||||
- fix(plan): clarify that plan mode policies are combined with normal mode by
|
||||
@ruomengz in [#23158](https://github.com/google-gemini/gemini-cli/pull/23158)
|
||||
- Add ModelChain support to ModelConfigService and make ModelDialog dynamic by
|
||||
@kevinjwang1 in
|
||||
[#22914](https://github.com/google-gemini/gemini-cli/pull/22914)
|
||||
- Ensure that copied extensions are writable in the user's local directory by
|
||||
@kevinjwang1 in
|
||||
[#23016](https://github.com/google-gemini/gemini-cli/pull/23016)
|
||||
- feat(core): implement native Windows sandboxing by @mattKorwel in
|
||||
[#21807](https://github.com/google-gemini/gemini-cli/pull/21807)
|
||||
- feat(core): add support for admin-forced MCP server installations by
|
||||
@gsquared94 in
|
||||
[#23163](https://github.com/google-gemini/gemini-cli/pull/23163)
|
||||
- chore(lint): ignore .gemini directory and recursive node_modules by
|
||||
@mattKorwel in
|
||||
[#23211](https://github.com/google-gemini/gemini-cli/pull/23211)
|
||||
- feat(cli): conditionally exclude ask_user tool in ACP mode by @nmcnamara-eng
|
||||
in [#23045](https://github.com/google-gemini/gemini-cli/pull/23045)
|
||||
- feat(core): introduce AgentSession and rename stream events to agent events by
|
||||
@mbleigh in [#23159](https://github.com/google-gemini/gemini-cli/pull/23159)
|
||||
- feat(worktree): add Git worktree support for isolated parallel sessions by
|
||||
@jerop in [#22973](https://github.com/google-gemini/gemini-cli/pull/22973)
|
||||
- Add support for linking in the extension registry by @kevinjwang1 in
|
||||
[#23153](https://github.com/google-gemini/gemini-cli/pull/23153)
|
||||
- feat(extensions): add --skip-settings flag to install command by @Ratish1 in
|
||||
[#17212](https://github.com/google-gemini/gemini-cli/pull/17212)
|
||||
- feat(telemetry): track if session is running in a Git worktree by @jerop in
|
||||
[#23265](https://github.com/google-gemini/gemini-cli/pull/23265)
|
||||
- refactor(core): use absolute paths in GEMINI.md context markers by
|
||||
@SandyTao520 in
|
||||
[#23135](https://github.com/google-gemini/gemini-cli/pull/23135)
|
||||
- fix(core): add sanitization to sub agent thoughts and centralize utilities by
|
||||
@devr0306 in [#22828](https://github.com/google-gemini/gemini-cli/pull/22828)
|
||||
- feat(core): refine User-Agent for VS Code traffic (unified format) by
|
||||
@sehoon38 in [#23256](https://github.com/google-gemini/gemini-cli/pull/23256)
|
||||
- Fix schema for ModelChains by @kevinjwang1 in
|
||||
[#23284](https://github.com/google-gemini/gemini-cli/pull/23284)
|
||||
- test(cli): refactor tests for async render utilities by @scidomino in
|
||||
[#23252](https://github.com/google-gemini/gemini-cli/pull/23252)
|
||||
- feat(core): add security prompt for browser agent by @cynthialong0-0 in
|
||||
[#23241](https://github.com/google-gemini/gemini-cli/pull/23241)
|
||||
- refactor(ide): replace dynamic undici import with static fetch import by
|
||||
@cocosheng-g in
|
||||
[#23268](https://github.com/google-gemini/gemini-cli/pull/23268)
|
||||
- test(cli): address unresolved feedback from PR #23252 by @scidomino in
|
||||
[#23303](https://github.com/google-gemini/gemini-cli/pull/23303)
|
||||
- feat(browser): add sensitive action controls and read-only noise reduction by
|
||||
@cynthialong0-0 in
|
||||
[#22867](https://github.com/google-gemini/gemini-cli/pull/22867)
|
||||
- Disabling failing test while investigating by @alisa-alisa in
|
||||
[#23311](https://github.com/google-gemini/gemini-cli/pull/23311)
|
||||
- fix broken extension link in hooks guide by @Indrapal-70 in
|
||||
[#21728](https://github.com/google-gemini/gemini-cli/pull/21728)
|
||||
- fix(core): fix agent description indentation by @abhipatel12 in
|
||||
[#23315](https://github.com/google-gemini/gemini-cli/pull/23315)
|
||||
- Wrap the text under TOML rule for easier readability in policy-engine.md… by
|
||||
@CogitationOps in
|
||||
[#23076](https://github.com/google-gemini/gemini-cli/pull/23076)
|
||||
- fix(extensions): revert broken extension removal behavior by @ehedlund in
|
||||
[#23317](https://github.com/google-gemini/gemini-cli/pull/23317)
|
||||
- feat(core): set up onboarding telemetry by @yunaseoul in
|
||||
[#23118](https://github.com/google-gemini/gemini-cli/pull/23118)
|
||||
- Retry evals on API error. by @gundermanc in
|
||||
[#23322](https://github.com/google-gemini/gemini-cli/pull/23322)
|
||||
- fix(evals): remove tool restrictions and add compile-time guards by
|
||||
@SandyTao520 in
|
||||
[#23312](https://github.com/google-gemini/gemini-cli/pull/23312)
|
||||
- fix(hooks): support 'ask' decision for BeforeTool hooks by @gundermanc in
|
||||
[#21146](https://github.com/google-gemini/gemini-cli/pull/21146)
|
||||
- feat(browser): add warning message for session mode 'existing' by
|
||||
@cynthialong0-0 in
|
||||
[#23288](https://github.com/google-gemini/gemini-cli/pull/23288)
|
||||
- chore(lint): enforce zero warnings and cleanup syntax restrictions by
|
||||
@alisa-alisa in
|
||||
[#22902](https://github.com/google-gemini/gemini-cli/pull/22902)
|
||||
- fix(cli): add Esc instruction to HooksDialog footer by @abhipatel12 in
|
||||
[#23258](https://github.com/google-gemini/gemini-cli/pull/23258)
|
||||
- Disallow and suppress misused spread operator. by @gundermanc in
|
||||
[#23294](https://github.com/google-gemini/gemini-cli/pull/23294)
|
||||
- fix(core): refine CliHelpAgent description for better delegation by
|
||||
@abhipatel12 in
|
||||
[#23310](https://github.com/google-gemini/gemini-cli/pull/23310)
|
||||
- fix(core): enable global session and persistent approval for web_fetch by
|
||||
@NTaylorMullen in
|
||||
[#23295](https://github.com/google-gemini/gemini-cli/pull/23295)
|
||||
- fix(plan): add state transition override to prevent plan mode freeze by
|
||||
@Adib234 in [#23020](https://github.com/google-gemini/gemini-cli/pull/23020)
|
||||
- fix(cli): record skill activation tool calls in chat history by @NTaylorMullen
|
||||
in [#23203](https://github.com/google-gemini/gemini-cli/pull/23203)
|
||||
- fix(core): ensure subagent tool updates apply configuration overrides
|
||||
immediately by @abhipatel12 in
|
||||
[#23161](https://github.com/google-gemini/gemini-cli/pull/23161)
|
||||
- fix(cli): resolve flicker at boundaries of list in BaseSelectionList by
|
||||
@jackwotherspoon in
|
||||
[#23298](https://github.com/google-gemini/gemini-cli/pull/23298)
|
||||
- test(cli): force generic terminal in tests to fix snapshot failures by
|
||||
@abhipatel12 in
|
||||
[#23499](https://github.com/google-gemini/gemini-cli/pull/23499)
|
||||
- Evals: PR Guidance adding workflow by @alisa-alisa in
|
||||
[#23164](https://github.com/google-gemini/gemini-cli/pull/23164)
|
||||
- feat(core): refactor SandboxManager to a stateless architecture and introduce
|
||||
explicit Deny interface by @ehedlund in
|
||||
[#23141](https://github.com/google-gemini/gemini-cli/pull/23141)
|
||||
- feat(core): add event-translator and update agent types by @adamfweidman in
|
||||
[#22985](https://github.com/google-gemini/gemini-cli/pull/22985)
|
||||
- perf(cli): parallelize and background startup cleanup tasks by @sehoon38 in
|
||||
[#23545](https://github.com/google-gemini/gemini-cli/pull/23545)
|
||||
- fix: "allow always" for commands with paths by @scidomino in
|
||||
[#23558](https://github.com/google-gemini/gemini-cli/pull/23558)
|
||||
- fix(cli): prevent terminal escape sequences from leaking on exit by
|
||||
@mattKorwel in
|
||||
[#22682](https://github.com/google-gemini/gemini-cli/pull/22682)
|
||||
- feat(cli): implement full "GEMINI CLI" logo for logged-out state by
|
||||
[#23626](https://github.com/google-gemini/gemini-cli/pull/23626)
|
||||
- feat(sandbox): dynamic Linux sandbox expansion and worktree support by @galz10
|
||||
in [#23692](https://github.com/google-gemini/gemini-cli/pull/23692)
|
||||
- Merge examples of use into quickstart documentation by @diodesign in
|
||||
[#23319](https://github.com/google-gemini/gemini-cli/pull/23319)
|
||||
- fix(cli): prioritize primary name matches in slash command search by @sehoon38
|
||||
in [#23850](https://github.com/google-gemini/gemini-cli/pull/23850)
|
||||
- Changelog for v0.35.1 by @gemini-cli-robot in
|
||||
[#23840](https://github.com/google-gemini/gemini-cli/pull/23840)
|
||||
- fix(browser): keep input blocker active across navigations by @kunal-10-cloud
|
||||
in [#22562](https://github.com/google-gemini/gemini-cli/pull/22562)
|
||||
- feat(core): new skill to look for duplicated code while reviewing PRs by
|
||||
@devr0306 in [#23704](https://github.com/google-gemini/gemini-cli/pull/23704)
|
||||
- fix(core): replace hardcoded non-interactive ASK_USER denial with explicit
|
||||
policy rules by @ruomengz in
|
||||
[#23668](https://github.com/google-gemini/gemini-cli/pull/23668)
|
||||
- fix(plan): after exiting plan mode switches model to a flash model by @Adib234
|
||||
in [#23885](https://github.com/google-gemini/gemini-cli/pull/23885)
|
||||
- feat(gcp): add development worker infrastructure by @mattKorwel in
|
||||
[#23814](https://github.com/google-gemini/gemini-cli/pull/23814)
|
||||
- fix(a2a-server): A2A server should execute ask policies in interactive mode by
|
||||
@kschaab in [#23831](https://github.com/google-gemini/gemini-cli/pull/23831)
|
||||
- feat(core): define TrajectoryProvider interface by @sehoon38 in
|
||||
[#23050](https://github.com/google-gemini/gemini-cli/pull/23050)
|
||||
- Docs: Update quotas and pricing by @jkcinouye in
|
||||
[#23835](https://github.com/google-gemini/gemini-cli/pull/23835)
|
||||
- fix(core): allow disabling environment variable redaction by @galz10 in
|
||||
[#23927](https://github.com/google-gemini/gemini-cli/pull/23927)
|
||||
- feat(cli): enable notifications cross-platform via terminal bell fallback by
|
||||
@genneth in [#21618](https://github.com/google-gemini/gemini-cli/pull/21618)
|
||||
- feat(sandbox): implement secret visibility lockdown for env files by
|
||||
@DavidAPierce in
|
||||
[#23712](https://github.com/google-gemini/gemini-cli/pull/23712)
|
||||
- fix(core): remove shell outputChunks buffer caching to prevent memory bloat
|
||||
and sanitize prompt input by @spencer426 in
|
||||
[#23751](https://github.com/google-gemini/gemini-cli/pull/23751)
|
||||
- feat(core): implement persistent browser session management by @kunal-10-cloud
|
||||
in [#21306](https://github.com/google-gemini/gemini-cli/pull/21306)
|
||||
- refactor(core): delegate sandbox denial parsing to SandboxManager by
|
||||
@scidomino in [#23928](https://github.com/google-gemini/gemini-cli/pull/23928)
|
||||
- dep(update) Update Ink version to 6.5.0 by @jacob314 in
|
||||
[#23843](https://github.com/google-gemini/gemini-cli/pull/23843)
|
||||
- Docs: Update 'docs-writer' skill for relative links by @jkcinouye in
|
||||
[#21463](https://github.com/google-gemini/gemini-cli/pull/21463)
|
||||
- Changelog for v0.36.0-preview.4 by @gemini-cli-robot in
|
||||
[#23935](https://github.com/google-gemini/gemini-cli/pull/23935)
|
||||
- fix(acp): Update allow approval policy flow for ACP clients to fix config
|
||||
persistence and compatible with TUI by @sripasg in
|
||||
[#23818](https://github.com/google-gemini/gemini-cli/pull/23818)
|
||||
- Changelog for v0.35.2 by @gemini-cli-robot in
|
||||
[#23960](https://github.com/google-gemini/gemini-cli/pull/23960)
|
||||
- ACP integration documents by @g-samroberts in
|
||||
[#22254](https://github.com/google-gemini/gemini-cli/pull/22254)
|
||||
- fix(core): explicitly set error names to avoid bundling renaming issues by
|
||||
@yunaseoul in [#23913](https://github.com/google-gemini/gemini-cli/pull/23913)
|
||||
- feat(core): subagent isolation and cleanup hardening by @abhipatel12 in
|
||||
[#23903](https://github.com/google-gemini/gemini-cli/pull/23903)
|
||||
- disable extension-reload test by @scidomino in
|
||||
[#24018](https://github.com/google-gemini/gemini-cli/pull/24018)
|
||||
- feat(core): add forbiddenPaths to GlobalSandboxOptions and refactor
|
||||
createSandboxManager by @ehedlund in
|
||||
[#23936](https://github.com/google-gemini/gemini-cli/pull/23936)
|
||||
- refactor(core): improve ignore resolution and fix directory-matching bug by
|
||||
@ehedlund in [#23816](https://github.com/google-gemini/gemini-cli/pull/23816)
|
||||
- revert(core): support custom base URL via env vars by @spencer426 in
|
||||
[#23976](https://github.com/google-gemini/gemini-cli/pull/23976)
|
||||
- Increase memory limited for eslint. by @jacob314 in
|
||||
[#24022](https://github.com/google-gemini/gemini-cli/pull/24022)
|
||||
- fix(acp): prevent crash on empty response in ACP mode by @sripasg in
|
||||
[#23952](https://github.com/google-gemini/gemini-cli/pull/23952)
|
||||
- feat(core): Land `AgentHistoryProvider`. by @joshualitt in
|
||||
[#23978](https://github.com/google-gemini/gemini-cli/pull/23978)
|
||||
- fix(core): switch to subshells for shell tool wrapping to fix heredocs and
|
||||
edge cases by @abhipatel12 in
|
||||
[#24024](https://github.com/google-gemini/gemini-cli/pull/24024)
|
||||
- Debug command. by @jacob314 in
|
||||
[#23851](https://github.com/google-gemini/gemini-cli/pull/23851)
|
||||
- Changelog for v0.36.0-preview.5 by @gemini-cli-robot in
|
||||
[#24046](https://github.com/google-gemini/gemini-cli/pull/24046)
|
||||
- Fix test flakes by globally mocking ink-spinner by @jacob314 in
|
||||
[#24044](https://github.com/google-gemini/gemini-cli/pull/24044)
|
||||
- Enable network access in sandbox configuration by @galz10 in
|
||||
[#24055](https://github.com/google-gemini/gemini-cli/pull/24055)
|
||||
- feat(context): add configurable memoryBoundaryMarkers setting by @SandyTao520
|
||||
in [#24020](https://github.com/google-gemini/gemini-cli/pull/24020)
|
||||
- feat(core): implement windows sandbox expansion and denial detection by
|
||||
@scidomino in [#24027](https://github.com/google-gemini/gemini-cli/pull/24027)
|
||||
- fix(core): resolve ACP Operation Aborted Errors in grep_search by @ivanporty
|
||||
in [#23821](https://github.com/google-gemini/gemini-cli/pull/23821)
|
||||
- fix(hooks): prevent SessionEnd from firing twice in non-interactive mode by
|
||||
@krishdef7 in [#22139](https://github.com/google-gemini/gemini-cli/pull/22139)
|
||||
- Re-word intro to Gemini 3 page. by @g-samroberts in
|
||||
[#24069](https://github.com/google-gemini/gemini-cli/pull/24069)
|
||||
- fix(cli): resolve layout contention and flashing loop in StatusRow by
|
||||
@keithguerin in
|
||||
[#22412](https://github.com/google-gemini/gemini-cli/pull/22412)
|
||||
- fix(plan): reserve minimum height for selection list in AskUserDialog by
|
||||
@ruomengz in [#23280](https://github.com/google-gemini/gemini-cli/pull/23280)
|
||||
- fix(core): harden AgentSession replay semantics by @adamfweidman in
|
||||
[#23548](https://github.com/google-gemini/gemini-cli/pull/23548)
|
||||
- test(core): migrate hook tests to scheduler by @abhipatel12 in
|
||||
[#23496](https://github.com/google-gemini/gemini-cli/pull/23496)
|
||||
- chore(config): disable agents by default by @abhipatel12 in
|
||||
[#23546](https://github.com/google-gemini/gemini-cli/pull/23546)
|
||||
- fix(ui): make tool confirmations take up entire terminal height by @devr0306
|
||||
in [#22366](https://github.com/google-gemini/gemini-cli/pull/22366)
|
||||
- fix(core): prevent redundant remote agent loading on model switch by
|
||||
[#24065](https://github.com/google-gemini/gemini-cli/pull/24065)
|
||||
- fix(sandbox): implement Windows Mandatory Integrity Control for GeminiSandbox
|
||||
by @galz10 in [#24057](https://github.com/google-gemini/gemini-cli/pull/24057)
|
||||
- feat(core): implement tool-based topic grouping (Chapters) by @Abhijit-2592 in
|
||||
[#23150](https://github.com/google-gemini/gemini-cli/pull/23150)
|
||||
- feat(cli): support 'tab to queue' for messages while generating by @gundermanc
|
||||
in [#24052](https://github.com/google-gemini/gemini-cli/pull/24052)
|
||||
- feat(core): agnostic background task UI with CompletionBehavior by
|
||||
@adamfweidman in
|
||||
[#23576](https://github.com/google-gemini/gemini-cli/pull/23576)
|
||||
- refactor(core): update production type imports from coreToolScheduler by
|
||||
@abhipatel12 in
|
||||
[#23498](https://github.com/google-gemini/gemini-cli/pull/23498)
|
||||
- feat(cli): always prefix extension skills with colon separator by
|
||||
@NTaylorMullen in
|
||||
[#23566](https://github.com/google-gemini/gemini-cli/pull/23566)
|
||||
- fix(core): properly support allowRedirect in policy engine by @scidomino in
|
||||
[#23579](https://github.com/google-gemini/gemini-cli/pull/23579)
|
||||
- fix(cli): prevent subcommand shadowing and skip auth for commands by
|
||||
[#22740](https://github.com/google-gemini/gemini-cli/pull/22740)
|
||||
- UX for topic narration tool by @gundermanc in
|
||||
[#24079](https://github.com/google-gemini/gemini-cli/pull/24079)
|
||||
- fix: shellcheck warnings in scripts by @scidomino in
|
||||
[#24035](https://github.com/google-gemini/gemini-cli/pull/24035)
|
||||
- test(evals): add comprehensive subagent delegation evaluations by @abhipatel12
|
||||
in [#24132](https://github.com/google-gemini/gemini-cli/pull/24132)
|
||||
- fix(a2a-server): prioritize ADC before evaluating headless constraints for
|
||||
auth initialization by @spencer426 in
|
||||
[#23614](https://github.com/google-gemini/gemini-cli/pull/23614)
|
||||
- Text can be added after /plan command by @rambleraptor in
|
||||
[#22833](https://github.com/google-gemini/gemini-cli/pull/22833)
|
||||
- fix(cli): resolve missing F12 logs via global console store by @scidomino in
|
||||
[#24235](https://github.com/google-gemini/gemini-cli/pull/24235)
|
||||
- fix broken tests by @scidomino in
|
||||
[#24279](https://github.com/google-gemini/gemini-cli/pull/24279)
|
||||
- fix(evals): add update_topic behavioral eval by @gundermanc in
|
||||
[#24223](https://github.com/google-gemini/gemini-cli/pull/24223)
|
||||
- feat(core): Unified Context Management and Tool Distillation. by @joshualitt
|
||||
in [#24157](https://github.com/google-gemini/gemini-cli/pull/24157)
|
||||
- Default enable narration for the team. by @gundermanc in
|
||||
[#24224](https://github.com/google-gemini/gemini-cli/pull/24224)
|
||||
- fix(core): ensure default agents provide tools and use model-specific schemas
|
||||
by @abhipatel12 in
|
||||
[#24268](https://github.com/google-gemini/gemini-cli/pull/24268)
|
||||
- feat(cli): show Flash Lite Preview model regardless of user tier by @sehoon38
|
||||
in [#23904](https://github.com/google-gemini/gemini-cli/pull/23904)
|
||||
- feat(cli): implement compact tool output by @jwhelangoog in
|
||||
[#20974](https://github.com/google-gemini/gemini-cli/pull/20974)
|
||||
- Add security settings for tool sandboxing by @galz10 in
|
||||
[#23923](https://github.com/google-gemini/gemini-cli/pull/23923)
|
||||
- chore(test-utils): switch integration tests to use PREVIEW_GEMINI_MODEL by
|
||||
@sehoon38 in [#24276](https://github.com/google-gemini/gemini-cli/pull/24276)
|
||||
- feat(core): enable topic update narration for legacy models by @Abhijit-2592
|
||||
in [#24241](https://github.com/google-gemini/gemini-cli/pull/24241)
|
||||
- feat(core): add project-level memory scope to save_memory tool by @SandyTao520
|
||||
in [#24161](https://github.com/google-gemini/gemini-cli/pull/24161)
|
||||
- test(integration): fix plan mode write denial test false positive by @sehoon38
|
||||
in [#24299](https://github.com/google-gemini/gemini-cli/pull/24299)
|
||||
- feat(plan): support `Plan` mode in untrusted folders by @Adib234 in
|
||||
[#17586](https://github.com/google-gemini/gemini-cli/pull/17586)
|
||||
- fix(core): enable mid-stream retries for all models and re-enable compression
|
||||
test by @sehoon38 in
|
||||
[#24302](https://github.com/google-gemini/gemini-cli/pull/24302)
|
||||
- Changelog for v0.36.0-preview.6 by @gemini-cli-robot in
|
||||
[#24082](https://github.com/google-gemini/gemini-cli/pull/24082)
|
||||
- Changelog for v0.35.3 by @gemini-cli-robot in
|
||||
[#24083](https://github.com/google-gemini/gemini-cli/pull/24083)
|
||||
- feat(cli): add auth info to footer by @sehoon38 in
|
||||
[#24042](https://github.com/google-gemini/gemini-cli/pull/24042)
|
||||
- fix(browser): reset action counter for each agent session and let it ignore
|
||||
internal actions by @cynthialong0-0 in
|
||||
[#24228](https://github.com/google-gemini/gemini-cli/pull/24228)
|
||||
- feat(plan): promote planning feature to stable by @ruomengz in
|
||||
[#24282](https://github.com/google-gemini/gemini-cli/pull/24282)
|
||||
- fix(browser): terminate subagent immediately on domain restriction violations
|
||||
by @gsquared94 in
|
||||
[#24313](https://github.com/google-gemini/gemini-cli/pull/24313)
|
||||
- feat(cli): add UI to update extensions by @ruomengz in
|
||||
[#23682](https://github.com/google-gemini/gemini-cli/pull/23682)
|
||||
- Fix(browser): terminate immediately for "browser is already running" error by
|
||||
@cynthialong0-0 in
|
||||
[#24233](https://github.com/google-gemini/gemini-cli/pull/24233)
|
||||
- docs: Add 'plan' option to approval mode in CLI reference by @YifanRuan in
|
||||
[#24134](https://github.com/google-gemini/gemini-cli/pull/24134)
|
||||
- fix(core): batch macOS seatbelt rules into a profile file to prevent ARG_MAX
|
||||
errors by @ehedlund in
|
||||
[#24255](https://github.com/google-gemini/gemini-cli/pull/24255)
|
||||
- fix(core): fix race condition between browser agent and main closing process
|
||||
by @cynthialong0-0 in
|
||||
[#24340](https://github.com/google-gemini/gemini-cli/pull/24340)
|
||||
- perf(build): optimize build scripts for parallel execution and remove
|
||||
redundant checks by @sehoon38 in
|
||||
[#24307](https://github.com/google-gemini/gemini-cli/pull/24307)
|
||||
- ci: install bubblewrap on Linux for release workflows by @ehedlund in
|
||||
[#24347](https://github.com/google-gemini/gemini-cli/pull/24347)
|
||||
- chore(release): allow bundling for all builds, including stable by @sehoon38
|
||||
in [#24305](https://github.com/google-gemini/gemini-cli/pull/24305)
|
||||
- Revert "Add security settings for tool sandboxing" by @jerop in
|
||||
[#24357](https://github.com/google-gemini/gemini-cli/pull/24357)
|
||||
- docs: update subagents docs to not be experimental by @abhipatel12 in
|
||||
[#24343](https://github.com/google-gemini/gemini-cli/pull/24343)
|
||||
- fix(core): implement **read and **write commands in sandbox managers by
|
||||
@galz10 in [#24283](https://github.com/google-gemini/gemini-cli/pull/24283)
|
||||
- don't try to remove tags in dry run by @scidomino in
|
||||
[#24356](https://github.com/google-gemini/gemini-cli/pull/24356)
|
||||
- fix(config): disable JIT context loading by default by @SandyTao520 in
|
||||
[#24364](https://github.com/google-gemini/gemini-cli/pull/24364)
|
||||
- test(sandbox): add integration test for dynamic permission expansion by
|
||||
@galz10 in [#24359](https://github.com/google-gemini/gemini-cli/pull/24359)
|
||||
- docs(policy): remove unsupported mcpName wildcard edge case by @abhipatel12 in
|
||||
[#24133](https://github.com/google-gemini/gemini-cli/pull/24133)
|
||||
- docs: fix broken GEMINI.md link in CONTRIBUTING.md by @Panchal-Tirth in
|
||||
[#24182](https://github.com/google-gemini/gemini-cli/pull/24182)
|
||||
- feat(core): infrastructure for event-driven subagent history by @abhipatel12
|
||||
in [#23914](https://github.com/google-gemini/gemini-cli/pull/23914)
|
||||
- fix(core): resolve Plan Mode deadlock during plan file creation due to sandbox
|
||||
restrictions by @DavidAPierce in
|
||||
[#24047](https://github.com/google-gemini/gemini-cli/pull/24047)
|
||||
- fix(core): fix browser agent UX issues and improve E2E test reliability by
|
||||
@gsquared94 in
|
||||
[#24312](https://github.com/google-gemini/gemini-cli/pull/24312)
|
||||
- fix(ui): wrap topic and intent fields in TopicMessage by @jwhelangoog in
|
||||
[#24386](https://github.com/google-gemini/gemini-cli/pull/24386)
|
||||
- refactor(core): Centralize context management logic into src/context by
|
||||
@joshualitt in
|
||||
[#24380](https://github.com/google-gemini/gemini-cli/pull/24380)
|
||||
- fix(core): pin AuthType.GATEWAY to use Gemini 3.1 Pro/Flash Lite by default by
|
||||
@sripasg in [#24375](https://github.com/google-gemini/gemini-cli/pull/24375)
|
||||
- feat(ui): add Tokyo Night theme by @danrneal in
|
||||
[#24054](https://github.com/google-gemini/gemini-cli/pull/24054)
|
||||
- fix(cli): refactor test config loading and mock debugLogger in test-setup by
|
||||
@mattKorwel in
|
||||
[#23177](https://github.com/google-gemini/gemini-cli/pull/23177)
|
||||
- fix(test): move flaky tests to non-blocking suite by @mattKorwel in
|
||||
[#23259](https://github.com/google-gemini/gemini-cli/pull/23259)
|
||||
- Changelog for v0.35.0-preview.3 by @gemini-cli-robot in
|
||||
[#23574](https://github.com/google-gemini/gemini-cli/pull/23574)
|
||||
- feat(skills): add behavioral-evals skill with fixing and promoting guides by
|
||||
[#24389](https://github.com/google-gemini/gemini-cli/pull/24389)
|
||||
- Set memoryManager to false in settings.json by @mattKorwel in
|
||||
[#24393](https://github.com/google-gemini/gemini-cli/pull/24393)
|
||||
- ink 6.6.3 by @jacob314 in
|
||||
[#24372](https://github.com/google-gemini/gemini-cli/pull/24372)
|
||||
- fix(core): resolve subagent chat recording gaps and directory inheritance by
|
||||
@abhipatel12 in
|
||||
[#23349](https://github.com/google-gemini/gemini-cli/pull/23349)
|
||||
- refactor(core): delete obsolete coreToolScheduler by @abhipatel12 in
|
||||
[#23502](https://github.com/google-gemini/gemini-cli/pull/23502)
|
||||
- Changelog for v0.35.0-preview.4 by @gemini-cli-robot in
|
||||
[#23581](https://github.com/google-gemini/gemini-cli/pull/23581)
|
||||
- feat(core): add LegacyAgentSession by @adamfweidman in
|
||||
[#22986](https://github.com/google-gemini/gemini-cli/pull/22986)
|
||||
- feat(test-utils): add TestMcpServerBuilder and support in TestRig by
|
||||
@abhipatel12 in
|
||||
[#23491](https://github.com/google-gemini/gemini-cli/pull/23491)
|
||||
- fix(core)!: Force policy config to specify toolName by @kschaab in
|
||||
[#23330](https://github.com/google-gemini/gemini-cli/pull/23330)
|
||||
- eval(save_memory): add multi-turn interactive evals for memoryManager by
|
||||
@SandyTao520 in
|
||||
[#23572](https://github.com/google-gemini/gemini-cli/pull/23572)
|
||||
- fix(telemetry): patch memory leak and enforce logPrompts privacy by
|
||||
@spencer426 in
|
||||
[#23281](https://github.com/google-gemini/gemini-cli/pull/23281)
|
||||
- perf(cli): background IDE client to speed up initialization by @sehoon38 in
|
||||
[#23603](https://github.com/google-gemini/gemini-cli/pull/23603)
|
||||
- fix(cli): prevent Ctrl+D exit when input buffer is not empty by @wtanaka in
|
||||
[#23306](https://github.com/google-gemini/gemini-cli/pull/23306)
|
||||
- fix: ACP: separate conversational text from execute tool command title by
|
||||
@sripasg in [#23179](https://github.com/google-gemini/gemini-cli/pull/23179)
|
||||
- feat(evals): add behavioral evaluations for subagent routing by @Samee24 in
|
||||
[#23272](https://github.com/google-gemini/gemini-cli/pull/23272)
|
||||
- refactor(cli,core): foundational layout, identity management, and type safety
|
||||
by @jwhelangoog in
|
||||
[#23286](https://github.com/google-gemini/gemini-cli/pull/23286)
|
||||
- fix(core): accurately reflect subagent tool failure in UI by @abhipatel12 in
|
||||
[#23187](https://github.com/google-gemini/gemini-cli/pull/23187)
|
||||
- Changelog for v0.35.0-preview.5 by @gemini-cli-robot in
|
||||
[#23606](https://github.com/google-gemini/gemini-cli/pull/23606)
|
||||
- feat(ui): implement refreshed UX for Composer layout by @jwhelangoog in
|
||||
[#21212](https://github.com/google-gemini/gemini-cli/pull/21212)
|
||||
- fix: API key input dialog user interaction when selected Gemini API Key by
|
||||
@kartikangiras in
|
||||
[#21057](https://github.com/google-gemini/gemini-cli/pull/21057)
|
||||
- docs: update `/mcp refresh` to `/mcp reload` by @adamfweidman in
|
||||
[#23631](https://github.com/google-gemini/gemini-cli/pull/23631)
|
||||
- Implementation of sandbox "Write-Protected" Governance Files by @DavidAPierce
|
||||
in [#23139](https://github.com/google-gemini/gemini-cli/pull/23139)
|
||||
- feat(sandbox): dynamic macOS sandbox expansion and worktree support by @galz10
|
||||
in [#23301](https://github.com/google-gemini/gemini-cli/pull/23301)
|
||||
- fix(acp): Pass the cwd to `AcpFileSystemService` to avoid looping failures in
|
||||
asking for perms to write plan md file by @sripasg in
|
||||
[#23612](https://github.com/google-gemini/gemini-cli/pull/23612)
|
||||
- fix(plan): sandbox path resolution in Plan Mode to prevent hallucinations by
|
||||
@Adib234 in [#22737](https://github.com/google-gemini/gemini-cli/pull/22737)
|
||||
- feat(ui): allow immediate user input during startup by @sehoon38 in
|
||||
[#23661](https://github.com/google-gemini/gemini-cli/pull/23661)
|
||||
- refactor(sandbox): reorganize Windows sandbox files by @galz10 in
|
||||
[#23645](https://github.com/google-gemini/gemini-cli/pull/23645)
|
||||
- fix(core): improve remote agent streaming UI and UX by @adamfweidman in
|
||||
[#23633](https://github.com/google-gemini/gemini-cli/pull/23633)
|
||||
- perf(cli): optimize --version startup time by @sehoon38 in
|
||||
[#23671](https://github.com/google-gemini/gemini-cli/pull/23671)
|
||||
- refactor(core): stop gemini CLI from producing unsafe casts by @gundermanc in
|
||||
[#23611](https://github.com/google-gemini/gemini-cli/pull/23611)
|
||||
- use enableAutoUpdate in test rig by @scidomino in
|
||||
[#23681](https://github.com/google-gemini/gemini-cli/pull/23681)
|
||||
- feat(core): change user-facing auth type from oauth2 to oauth by @adamfweidman
|
||||
in [#23639](https://github.com/google-gemini/gemini-cli/pull/23639)
|
||||
- chore(deps): fix npm audit vulnerabilities by @scidomino in
|
||||
[#23679](https://github.com/google-gemini/gemini-cli/pull/23679)
|
||||
- test(evals): fix overlapping act() deadlock in app-test-helper by @Adib234 in
|
||||
[#23666](https://github.com/google-gemini/gemini-cli/pull/23666)
|
||||
[#24368](https://github.com/google-gemini/gemini-cli/pull/24368)
|
||||
- fix(cli): cap shell output at 10 MB to prevent RangeError crash by @ProthamD
|
||||
in [#24168](https://github.com/google-gemini/gemini-cli/pull/24168)
|
||||
- feat(plan): conditionally add enter/exit plan mode tools based on current mode
|
||||
by @ruomengz in
|
||||
[#24378](https://github.com/google-gemini/gemini-cli/pull/24378)
|
||||
- feat(core): prioritize discussion before formal plan approval by @jerop in
|
||||
[#24423](https://github.com/google-gemini/gemini-cli/pull/24423)
|
||||
- fix(ui): add accelerated scrolling on alternate buffer mode by @devr0306 in
|
||||
[#23940](https://github.com/google-gemini/gemini-cli/pull/23940)
|
||||
- feat(core): populate sandbox forbidden paths with project ignore file contents
|
||||
by @ehedlund in
|
||||
[#24038](https://github.com/google-gemini/gemini-cli/pull/24038)
|
||||
- fix(core): ensure blue border overlay and input blocker to act correctly
|
||||
depending on browser agent activities by @cynthialong0-0 in
|
||||
[#24385](https://github.com/google-gemini/gemini-cli/pull/24385)
|
||||
- fix(ui): removed additional vertical padding for tables by @devr0306 in
|
||||
[#24381](https://github.com/google-gemini/gemini-cli/pull/24381)
|
||||
- fix(build): upload full bundle directory archive to GitHub releases by
|
||||
@sehoon38 in [#24403](https://github.com/google-gemini/gemini-cli/pull/24403)
|
||||
- fix(build): wire bundle:browser-mcp into bundle pipeline by @gsquared94 in
|
||||
[#24424](https://github.com/google-gemini/gemini-cli/pull/24424)
|
||||
- feat(browser): add sandbox-aware browser agent initialization by @gsquared94
|
||||
in [#24419](https://github.com/google-gemini/gemini-cli/pull/24419)
|
||||
- feat(core): enhance tracker task schemas for detailed titles and descriptions
|
||||
by @anj-s in [#23902](https://github.com/google-gemini/gemini-cli/pull/23902)
|
||||
- refactor(core): Unified context management settings schema by @joshualitt in
|
||||
[#24391](https://github.com/google-gemini/gemini-cli/pull/24391)
|
||||
- feat(core): update browser agent prompt to check open pages first when
|
||||
bringing up by @cynthialong0-0 in
|
||||
[#24431](https://github.com/google-gemini/gemini-cli/pull/24431)
|
||||
- fix(acp) refactor(core,cli): centralize model discovery logic in
|
||||
ModelConfigService by @sripasg in
|
||||
[#24392](https://github.com/google-gemini/gemini-cli/pull/24392)
|
||||
- Changelog for v0.36.0-preview.7 by @gemini-cli-robot in
|
||||
[#24346](https://github.com/google-gemini/gemini-cli/pull/24346)
|
||||
- fix: update task tracker storage location in system prompt by @anj-s in
|
||||
[#24034](https://github.com/google-gemini/gemini-cli/pull/24034)
|
||||
- feat(browser): supersede stale snapshots to reclaim context-window tokens by
|
||||
@gsquared94 in
|
||||
[#24440](https://github.com/google-gemini/gemini-cli/pull/24440)
|
||||
- docs(core): add subagent tool isolation draft doc by @akh64bit in
|
||||
[#23275](https://github.com/google-gemini/gemini-cli/pull/23275)
|
||||
|
||||
**Full Changelog**:
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.35.0-preview.5...v0.36.0-preview.8
|
||||
https://github.com/google-gemini/gemini-cli/compare/v0.36.0-preview.8...v0.37.0-preview.1
|
||||
|
||||
+10
-3
@@ -181,9 +181,16 @@ As described in the
|
||||
rule that does not explicitly specify `modes` is considered "always active" and
|
||||
will apply to Plan Mode as well.
|
||||
|
||||
If you want a rule to apply to other modes but _not_ to Plan Mode, you must
|
||||
explicitly specify the target modes. For example, to allow `npm test` in default
|
||||
and Auto-Edit modes but not in Plan Mode:
|
||||
To maintain the integrity of Plan Mode as a safe research environment,
|
||||
persistent tool approvals are context-aware. Approvals granted in modes like
|
||||
Default or Auto-Edit do not apply to Plan Mode, ensuring that tools trusted for
|
||||
implementation don't automatically execute while you're researching. However,
|
||||
approvals granted while in Plan Mode are treated as intentional choices for
|
||||
global trust and apply to all modes.
|
||||
|
||||
If you want to manually restrict a rule to other modes but _not_ to Plan Mode,
|
||||
you must explicitly specify the target modes. For example, to allow `npm test`
|
||||
in default and Auto-Edit modes but not in Plan Mode:
|
||||
|
||||
```toml
|
||||
[[rule]]
|
||||
|
||||
@@ -136,6 +136,58 @@ gemini -p "build the snap"
|
||||
absolute path — the path must be writable inside the container.
|
||||
- Used with tools like Snapcraft or Rockcraft that require a full system.
|
||||
|
||||
## Tool sandboxing
|
||||
|
||||
Tool-level sandboxing provides granular isolation for individual tool executions
|
||||
(like `shell_exec` and `write_file`) instead of sandboxing the entire Gemini CLI
|
||||
process.
|
||||
|
||||
This approach offers better integration with your local environment for non-tool
|
||||
tasks (like UI rendering and configuration loading) while still providing
|
||||
security for tool-driven operations.
|
||||
|
||||
### How to turn off tool sandboxing
|
||||
|
||||
If you experience issues with tool sandboxing or prefer full-process isolation,
|
||||
you can disable it by setting `security.toolSandboxing` to `false` in your
|
||||
`settings.json` file.
|
||||
|
||||
```json
|
||||
{
|
||||
"security": {
|
||||
"toolSandboxing": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> Changing the `security.toolSandboxing` setting requires a restart of Gemini
|
||||
> CLI to take effect.
|
||||
|
||||
## Sandbox expansion
|
||||
|
||||
Sandbox expansion is a dynamic permission system that lets Gemini CLI request
|
||||
additional permissions for a command when needed.
|
||||
|
||||
When a sandboxed command fails due to permission restrictions (like restricted
|
||||
file paths or network access), or when a command is proactively identified as
|
||||
requiring extra permissions (like `npm install`), Gemini CLI will present you
|
||||
with a "Sandbox Expansion Request."
|
||||
|
||||
### How sandbox expansion works
|
||||
|
||||
1. **Detection**: Gemini CLI detects a sandbox denial or proactively identifies
|
||||
a command that requires extra permissions.
|
||||
2. **Request**: A modal dialog is shown, explaining which additional
|
||||
permissions (e.g., specific directories or network access) are required.
|
||||
3. **Approval**: If you approve the expansion, the command is executed with the
|
||||
extended permissions for that specific run.
|
||||
|
||||
This mechanism ensures you don't have to manually re-run commands with more
|
||||
permissive sandbox settings, while still maintaining control over what the AI
|
||||
can access.
|
||||
|
||||
## Quickstart
|
||||
|
||||
```bash
|
||||
|
||||
+16
-13
@@ -60,7 +60,7 @@ they appear in the UI.
|
||||
| Hide Tips | `ui.hideTips` | Hide helpful tips in the UI | `false` |
|
||||
| Escape Pasted @ Symbols | `ui.escapePastedAtSymbols` | When enabled, @ symbols in pasted text are escaped to prevent unintended @path expansion. | `false` |
|
||||
| Show Shortcuts Hint | `ui.showShortcutsHint` | Show the "? for shortcuts" hint above the input. | `true` |
|
||||
| Compact Tool Output | `ui.compactToolOutput` | Display tool outputs (like directory listings and file reads) in a compact, structured format. | `false` |
|
||||
| Compact Tool Output | `ui.compactToolOutput` | Display tool outputs (like directory listings and file reads) in a compact, structured format. | `true` |
|
||||
| Hide Banner | `ui.hideBanner` | Hide the application banner | `false` |
|
||||
| Hide Context Summary | `ui.hideContextSummary` | Hide the context summary (GEMINI.md, MCP servers) above the input. | `false` |
|
||||
| Hide CWD | `ui.footer.hideCWD` | Hide the current working directory in the footer. | `false` |
|
||||
@@ -74,6 +74,8 @@ they appear in the UI.
|
||||
| Show Model Info In Chat | `ui.showModelInfoInChat` | Show the model name in the chat for each model turn. | `false` |
|
||||
| Show User Identity | `ui.showUserIdentity` | Show the signed-in user's identity (e.g. email) in the UI. | `true` |
|
||||
| Use Alternate Screen Buffer | `ui.useAlternateBuffer` | Use an alternate screen buffer for the UI, preserving shell history. | `false` |
|
||||
| Render Process | `ui.renderProcess` | Enable Ink render process for the UI. | `true` |
|
||||
| Terminal Buffer | `ui.terminalBuffer` | Use the new terminal buffer architecture for rendering. | `true` |
|
||||
| Use Background Color | `ui.useBackgroundColor` | Whether to use background colors in the UI. | `true` |
|
||||
| Incremental Rendering | `ui.incrementalRendering` | Enable incremental rendering for the UI. This option will reduce flickering but may cause rendering artifacts. Only supported when useAlternateBuffer is enabled. | `true` |
|
||||
| Show Spinner | `ui.showSpinner` | Show the spinner during operations. | `true` |
|
||||
@@ -129,7 +131,7 @@ they appear in the UI.
|
||||
| Sandbox Allowed Paths | `tools.sandboxAllowedPaths` | List of additional paths that the sandbox is allowed to access. | `[]` |
|
||||
| Sandbox Network Access | `tools.sandboxNetworkAccess` | Whether the sandbox is allowed to access the network. | `false` |
|
||||
| Enable Interactive Shell | `tools.shell.enableInteractiveShell` | Use node-pty for an interactive shell experience. Fallback to child_process still applies. | `true` |
|
||||
| Show Color | `tools.shell.showColor` | Show color in shell output. | `false` |
|
||||
| Show Color | `tools.shell.showColor` | Show color in shell output. | `true` |
|
||||
| Use Ripgrep | `tools.useRipgrep` | Use ripgrep for file content search instead of the fallback implementation. Provides faster search performance. | `true` |
|
||||
| Tool Output Truncation Threshold | `tools.truncateToolOutputThreshold` | Maximum characters to show when truncating large tool outputs. Set to 0 or negative to disable truncation. | `40000` |
|
||||
| Disable LLM Correction | `tools.disableLLMCorrection` | Disable LLM-based error correction for edit tools. When enabled, tools will fail immediately if exact string matches are not found, instead of attempting to self-correct. | `true` |
|
||||
@@ -138,7 +140,7 @@ they appear in the UI.
|
||||
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- |
|
||||
| Tool Sandboxing | `security.toolSandboxing` | Experimental tool-level sandboxing (implementation in progress). | `false` |
|
||||
| Tool Sandboxing | `security.toolSandboxing` | Tool-level sandboxing. Isolates individual tools instead of the entire CLI process. | `false` |
|
||||
| Disable YOLO Mode | `security.disableYoloMode` | Disable YOLO mode, even if enabled by a flag. | `false` |
|
||||
| Disable Always Allow | `security.disableAlwaysAllow` | Disable "Always allow" options in tool confirmation dialogs. | `false` |
|
||||
| Allow Permanent Tool Approval | `security.enablePermanentToolApproval` | Enable the "Allow for all future sessions" option in tool confirmation dialogs. | `false` |
|
||||
@@ -157,16 +159,17 @@ they appear in the UI.
|
||||
|
||||
### Experimental
|
||||
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||
| Enable Git Worktrees | `experimental.worktrees` | Enable automated Git worktree management for parallel work. | `false` |
|
||||
| Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 for pasting. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
|
||||
| Use OSC 52 Copy | `experimental.useOSC52Copy` | Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
|
||||
| Model Steering | `experimental.modelSteering` | Enable model steering (user hints) to guide the model during tool execution. | `false` |
|
||||
| Direct Web Fetch | `experimental.directWebFetch` | Enable web fetch behavior that bypasses LLM summarization. | `false` |
|
||||
| Memory Manager Agent | `experimental.memoryManager` | Replace the built-in save_memory tool with a memory manager subagent that supports adding, removing, de-duplicating, and organizing memories. | `false` |
|
||||
| Enable Context Management | `experimental.contextManagement` | Enable logic for context management. | `false` |
|
||||
| Topic & Update Narration | `experimental.topicUpdateNarration` | Enable the experimental Topic & Update communication model for reduced chattiness and structured progress reporting. | `false` |
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ---------------------------------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||
| Enable Git Worktrees | `experimental.worktrees` | Enable automated Git worktree management for parallel work. | `false` |
|
||||
| Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 for pasting. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
|
||||
| Use OSC 52 Copy | `experimental.useOSC52Copy` | Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
|
||||
| Model Steering | `experimental.modelSteering` | Enable model steering (user hints) to guide the model during tool execution. | `false` |
|
||||
| Direct Web Fetch | `experimental.directWebFetch` | Enable web fetch behavior that bypasses LLM summarization. | `false` |
|
||||
| Memory Manager Agent | `experimental.memoryManager` | Replace the built-in save_memory tool with a memory manager subagent that supports adding, removing, de-duplicating, and organizing memories. | `false` |
|
||||
| Use the generalist profile to manage agent contexts. | `experimental.generalistProfile` | Suitable for general coding and software development tasks. | `false` |
|
||||
| Enable Context Management | `experimental.contextManagement` | Enable logic for context management. | `false` |
|
||||
| Topic & Update Narration | `experimental.topicUpdateNarration` | Enable the experimental Topic & Update communication model for reduced chattiness and structured progress reporting. | `false` |
|
||||
|
||||
### Skills
|
||||
|
||||
|
||||
+14
-18
@@ -1,4 +1,4 @@
|
||||
# Remote Subagents (experimental)
|
||||
# Remote Subagents
|
||||
|
||||
Gemini CLI supports connecting to remote subagents using the Agent-to-Agent
|
||||
(A2A) protocol. This allows Gemini CLI to interact with other agents, expanding
|
||||
@@ -10,23 +10,6 @@ agents in the following repositories:
|
||||
- [ADK Samples (Python)](https://github.com/google/adk-samples/tree/main/python)
|
||||
- [ADK Python Contributing Samples](https://github.com/google/adk-python/tree/main/contributing/samples)
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> Remote subagents are currently an experimental feature.
|
||||
|
||||
## Configuration
|
||||
|
||||
To use remote subagents, you must explicitly enable them in your
|
||||
`settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"experimental": {
|
||||
"enableAgents": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Proxy support
|
||||
|
||||
Gemini CLI routes traffic to remote agents through an HTTP/HTTPS proxy if one is
|
||||
@@ -459,3 +442,16 @@ Users can manage subagents using the following commands within the Gemini CLI:
|
||||
> [!TIP]
|
||||
> You can use the `@cli_help` agent within Gemini CLI for assistance
|
||||
> with configuring subagents.
|
||||
|
||||
## Disabling remote agents
|
||||
|
||||
Remote subagents are enabled by default. To disable them, set `enableAgents` to
|
||||
`false` in your `settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"experimental": {
|
||||
"enableAgents": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
+61
-25
@@ -5,15 +5,6 @@ session. They are designed to handle specific, complex tasks—like deep codebas
|
||||
analysis, documentation lookup, or domain-specific reasoning—without cluttering
|
||||
the main agent's context or toolset.
|
||||
|
||||
Subagents are enabled by default. To disable them, set `enableAgents` to `false`
|
||||
in your `settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"experimental": { "enableAgents": false }
|
||||
}
|
||||
```
|
||||
|
||||
## What are subagents?
|
||||
|
||||
Subagents are "specialists" that the main Gemini agent can hire for a specific
|
||||
@@ -120,10 +111,12 @@ Gemini CLI comes with the following built-in subagents:
|
||||
|
||||
The browser agent requires:
|
||||
|
||||
- **Chrome** version 144 or later (any recent stable release will work).
|
||||
- **Node.js** with `npx` available (used to launch the
|
||||
[`chrome-devtools-mcp`](https://www.npmjs.com/package/chrome-devtools-mcp)
|
||||
server).
|
||||
- **Chrome** version 144 or later (any recent stable release works).
|
||||
|
||||
The underlying
|
||||
[`chrome-devtools-mcp`](https://www.npmjs.com/package/chrome-devtools-mcp)
|
||||
server is bundled with Gemini CLI and launched automatically — no separate
|
||||
installation is needed.
|
||||
|
||||
#### Enabling the browser agent
|
||||
|
||||
@@ -169,26 +162,58 @@ The available modes are:
|
||||
| `isolated` | Launches Chrome with a temporary profile that is deleted after each session. Use this for clean-state automation. |
|
||||
| `existing` | Attaches to an already-running Chrome instance. You must enable remote debugging first by navigating to `chrome://inspect/#remote-debugging` in Chrome. No new browser process is launched. |
|
||||
|
||||
#### First-run consent
|
||||
|
||||
The first time the browser agent is invoked, Gemini CLI displays a consent
|
||||
dialog. You must accept before the browser session starts. This dialog only
|
||||
appears once.
|
||||
|
||||
#### Configuration reference
|
||||
|
||||
All browser-specific settings go under `agents.browser` in your `settings.json`.
|
||||
For full details, see the
|
||||
[`agents.browser` configuration reference](../reference/configuration.md#agents).
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
| :------------ | :-------- | :------------- | :---------------------------------------------------------------------------------------------- |
|
||||
| `sessionMode` | `string` | `"persistent"` | How Chrome is managed: `"persistent"`, `"isolated"`, or `"existing"`. |
|
||||
| `headless` | `boolean` | `false` | Run Chrome in headless mode (no visible window). |
|
||||
| `profilePath` | `string` | — | Custom path to a browser profile directory. |
|
||||
| `visualModel` | `string` | — | Model override for the visual agent (for example, `"gemini-2.5-computer-use-preview-10-2025"`). |
|
||||
| Setting | Type | Default | Description |
|
||||
| :------------------------ | :--------- | :------------- | :------------------------------------------------------------------------------ |
|
||||
| `sessionMode` | `string` | `"persistent"` | How Chrome is managed: `"persistent"`, `"isolated"`, or `"existing"`. |
|
||||
| `headless` | `boolean` | `false` | Run Chrome in headless mode (no visible window). |
|
||||
| `profilePath` | `string` | — | Custom path to a browser profile directory. |
|
||||
| `visualModel` | `string` | — | Model override for the visual agent. |
|
||||
| `allowedDomains` | `string[]` | — | Restrict navigation to specific domains (for example, `["github.com"]`). |
|
||||
| `disableUserInput` | `boolean` | `true` | Disable user input on the browser window during automation (non-headless only). |
|
||||
| `maxActionsPerTask` | `number` | `100` | Maximum tool calls per task. The agent is terminated when the limit is reached. |
|
||||
| `confirmSensitiveActions` | `boolean` | `false` | Require manual confirmation for `upload_file` and `evaluate_script`. |
|
||||
| `blockFileUploads` | `boolean` | `false` | Hard-block all file upload requests from the agent. |
|
||||
|
||||
#### Automation overlay and input blocking
|
||||
|
||||
In non-headless mode, the browser agent injects a visual overlay into the
|
||||
browser window to indicate that automation is in progress. By default, user
|
||||
input (keyboard and mouse) is also blocked to prevent accidental interference.
|
||||
You can disable this by setting `disableUserInput` to `false`.
|
||||
|
||||
#### Security
|
||||
|
||||
The browser agent enforces the following security restrictions:
|
||||
The browser agent enforces several layers of security:
|
||||
|
||||
- **Blocked URL patterns:** `file://`, `javascript:`, `data:text/html`,
|
||||
`chrome://extensions`, and `chrome://settings/passwords` are always blocked.
|
||||
- **Sensitive action confirmation:** Actions like form filling, file uploads,
|
||||
and form submissions require user confirmation through the standard policy
|
||||
engine.
|
||||
- **Domain restrictions:** When `allowedDomains` is set, the agent can only
|
||||
navigate to the listed domains (and their subdomains when using `*.` prefix).
|
||||
Attempting to visit a disallowed domain throws a fatal error that immediately
|
||||
terminates the agent. The agent also attempts to detect and block the use of
|
||||
allowed domains as proxies (e.g., via query parameters or fragments) to access
|
||||
restricted content.
|
||||
- **Blocked URL patterns:** The underlying MCP server blocks dangerous URL
|
||||
schemes including `file://`, `javascript:`, `data:text/html`,
|
||||
`chrome://extensions`, and `chrome://settings/passwords`.
|
||||
- **Sensitive action confirmation:** Form filling (`fill`, `fill_form`) always
|
||||
requires user confirmation through the policy engine, regardless of approval
|
||||
mode. When `confirmSensitiveActions` is `true`, `upload_file` and
|
||||
`evaluate_script` also require confirmation.
|
||||
- **File upload blocking:** Set `blockFileUploads` to `true` to hard-block all
|
||||
file upload requests, preventing the agent from uploading any files.
|
||||
- **Action rate limiting:** The `maxActionsPerTask` setting (default: 100)
|
||||
limits the total number of tool calls per task to prevent runaway execution.
|
||||
|
||||
#### Visual agent
|
||||
|
||||
@@ -534,3 +559,14 @@ configuration, authentication, and usage instructions.
|
||||
Extensions can bundle and distribute subagents. See the
|
||||
[Extensions documentation](../extensions/index.md#subagents) for details on how
|
||||
to package agents within an extension.
|
||||
|
||||
## Disabling subagents
|
||||
|
||||
Subagents are enabled by default. To disable them, set `enableAgents` to `false`
|
||||
in your `settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"experimental": { "enableAgents": false }
|
||||
}
|
||||
```
|
||||
|
||||
@@ -398,8 +398,8 @@ on this page.
|
||||
|
||||
## Running in headless mode <a id="headless"></a>
|
||||
|
||||
[Headless mode](../cli/headless) will use your existing authentication method,
|
||||
if an existing authentication credential is cached.
|
||||
[Headless mode](../cli/headless.md) will use your existing authentication
|
||||
method, if an existing authentication credential is cached.
|
||||
|
||||
If you have not already signed in with an authentication credential, you must
|
||||
configure authentication using environment variables:
|
||||
|
||||
@@ -122,6 +122,13 @@ code.
|
||||
# From the root of the repository
|
||||
npm run start
|
||||
```
|
||||
- **Production mode (React optimizations):** This method runs the CLI with React
|
||||
production mode enabled, which is useful for testing performance without
|
||||
development overhead.
|
||||
```bash
|
||||
# From the root of the repository
|
||||
npm run start:prod
|
||||
```
|
||||
- **Production-like mode (linked package):** This method simulates a global
|
||||
installation by linking your local package. It's useful for testing a local
|
||||
build in a production workflow.
|
||||
|
||||
+6
-6
@@ -22,11 +22,11 @@ With hooks, you can:
|
||||
|
||||
### Getting started
|
||||
|
||||
- **[Writing hooks guide](../hooks/writing-hooks)**: A tutorial on creating your
|
||||
first hook with comprehensive examples.
|
||||
- **[Best practices](../hooks/best-practices)**: Guidelines on security,
|
||||
- **[Writing hooks guide](../hooks/writing-hooks.md)**: A tutorial on creating
|
||||
your first hook with comprehensive examples.
|
||||
- **[Best practices](../hooks/best-practices.md)**: Guidelines on security,
|
||||
performance, and debugging.
|
||||
- **[Hooks reference](../hooks/reference)**: The definitive technical
|
||||
- **[Hooks reference](../hooks/reference.md)**: The definitive technical
|
||||
specification of I/O schemas and exit codes.
|
||||
|
||||
## Core concepts
|
||||
@@ -154,8 +154,8 @@ Gemini CLI **fingerprints** project hooks. If a hook's name or command changes
|
||||
(e.g., via `git pull`), it is treated as a **new, untrusted hook** and you will
|
||||
be warned before it executes.
|
||||
|
||||
See [Security Considerations](../hooks/best-practices#using-hooks-securely) for
|
||||
a detailed threat model.
|
||||
See [Security Considerations](../hooks/best-practices.md#using-hooks-securely)
|
||||
for a detailed threat model.
|
||||
|
||||
## Managing hooks
|
||||
|
||||
|
||||
@@ -62,11 +62,13 @@ locations for these files:
|
||||
|
||||
**Note on environment variables in settings:** String values within your
|
||||
`settings.json` and `gemini-extension.json` files can reference environment
|
||||
variables using either `$VAR_NAME` or `${VAR_NAME}` syntax. These variables will
|
||||
be automatically resolved when the settings are loaded. For example, if you have
|
||||
an environment variable `MY_API_TOKEN`, you could use it in `settings.json` like
|
||||
this: `"apiKey": "$MY_API_TOKEN"`. Additionally, each extension can have its own
|
||||
`.env` file in its directory, which will be loaded automatically.
|
||||
variables using `$VAR_NAME`, `${VAR_NAME}`, or `${VAR_NAME:-DEFAULT_VALUE}`
|
||||
syntax. These variables will be automatically resolved when the settings are
|
||||
loaded. For example, if you have an environment variable `MY_API_TOKEN`, you
|
||||
could use it in `settings.json` like this: `"apiKey": "$MY_API_TOKEN"`. If you
|
||||
want to provide a fallback value, use `${MY_API_TOKEN:-default-token}`.
|
||||
Additionally, each extension can have its own `.env` file in its directory,
|
||||
which will be loaded automatically.
|
||||
|
||||
**Note for Enterprise Users:** For guidance on deploying and managing Gemini CLI
|
||||
in a corporate environment, please see the
|
||||
@@ -265,7 +267,7 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **`ui.compactToolOutput`** (boolean):
|
||||
- **Description:** Display tool outputs (like directory listings and file
|
||||
reads) in a compact, structured format.
|
||||
- **Default:** `false`
|
||||
- **Default:** `true`
|
||||
|
||||
- **`ui.hideBanner`** (boolean):
|
||||
- **Description:** Hide the application banner
|
||||
@@ -337,6 +339,16 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`ui.renderProcess`** (boolean):
|
||||
- **Description:** Enable Ink render process for the UI.
|
||||
- **Default:** `true`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`ui.terminalBuffer`** (boolean):
|
||||
- **Description:** Use the new terminal buffer architecture for rendering.
|
||||
- **Default:** `true`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`ui.useBackgroundColor`** (boolean):
|
||||
- **Description:** Whether to use background colors in the UI.
|
||||
- **Default:** `true`
|
||||
@@ -1242,7 +1254,8 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`agents.browser.visualModel`** (string):
|
||||
- **Description:** Model override for the visual agent.
|
||||
- **Description:** Model for the visual agent's analyze_screenshot tool. When
|
||||
set, enables the tool.
|
||||
- **Default:** `undefined`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
@@ -1391,7 +1404,7 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
|
||||
- **`tools.shell.showColor`** (boolean):
|
||||
- **Description:** Show color in shell output.
|
||||
- **Default:** `false`
|
||||
- **Default:** `true`
|
||||
|
||||
- **`tools.shell.inactivityTimeout`** (number):
|
||||
- **Description:** The maximum time in seconds allowed without output from the
|
||||
@@ -1479,9 +1492,10 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
#### `security`
|
||||
|
||||
- **`security.toolSandboxing`** (boolean):
|
||||
- **Description:** Experimental tool-level sandboxing (implementation in
|
||||
progress).
|
||||
- **Description:** Tool-level sandboxing. Isolates individual tools instead of
|
||||
the entire CLI process.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`security.disableYoloMode`** (boolean):
|
||||
- **Description:** Disable YOLO mode, even if enabled by a flag.
|
||||
@@ -1690,6 +1704,11 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.generalistProfile`** (boolean):
|
||||
- **Description:** Suitable for general coding and software development tasks.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.contextManagement`** (boolean):
|
||||
- **Description:** Enable logic for context management.
|
||||
- **Default:** `false`
|
||||
|
||||
@@ -102,7 +102,8 @@ available combinations.
|
||||
| `app.showFullTodos` | Toggle the full TODO list. | `Ctrl+T` |
|
||||
| `app.showIdeContextDetail` | Show IDE context details. | `Ctrl+G` |
|
||||
| `app.toggleMarkdown` | Toggle Markdown rendering. | `Alt+M` |
|
||||
| `app.toggleCopyMode` | Toggle copy mode when in alternate buffer mode. | `Ctrl+S` |
|
||||
| `app.toggleCopyMode` | Toggle copy mode when in alternate buffer mode. | `F9` |
|
||||
| `app.toggleMouseMode` | Toggle mouse mode (scrolling and clicking). | `Ctrl+S` |
|
||||
| `app.toggleYolo` | Toggle YOLO (auto-approval) mode for tool calls. | `Ctrl+Y` |
|
||||
| `app.cycleApprovalMode` | Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only). Plan mode is skipped when the agent is busy. | `Shift+Tab` |
|
||||
| `app.showMoreLines` | Expand and collapse blocks of content when not in alternate buffer mode. | `Ctrl+O` |
|
||||
@@ -126,6 +127,9 @@ available combinations.
|
||||
| `background.unfocus` | Move focus from background shell to Gemini. | `Shift+Tab` |
|
||||
| `background.unfocusList` | Move focus from background shell list to Gemini. | `Tab` |
|
||||
| `background.unfocusWarning` | Show warning when trying to move focus away from background shell. | `Tab` |
|
||||
| `app.dumpFrame` | Dump the current frame as a snapshot. | `F8` |
|
||||
| `app.startRecording` | Start recording the session. | `F6` |
|
||||
| `app.stopRecording` | Stop recording the session. | `F7` |
|
||||
|
||||
#### Extension Controls
|
||||
|
||||
|
||||
@@ -171,6 +171,24 @@ modes specified, it is always active.
|
||||
[Customizing Plan Mode Policies](../cli/plan-mode.md#customizing-policies).
|
||||
- `yolo`: A mode where all tools are auto-approved (use with extreme caution).
|
||||
|
||||
To maintain the integrity of Plan Mode as a safe research environment,
|
||||
persistent tool approvals are context-aware. When you select **"Allow for all
|
||||
future sessions"**, the policy engine explicitly includes the current mode and
|
||||
all more permissive modes in the hierarchy (`plan` < `default` < `autoEdit` <
|
||||
`yolo`).
|
||||
|
||||
- **Approvals in `plan` mode**: These represent an intentional choice to trust a
|
||||
tool globally. The resulting rule explicitly includes all modes (`plan`,
|
||||
`default`, `autoEdit`, and `yolo`).
|
||||
- **Approvals in other modes**: These only apply to the current mode and those
|
||||
more permissive. For example:
|
||||
- An approval granted in **`default`** mode applies to `default`, `autoEdit`,
|
||||
and `yolo`.
|
||||
- An approval granted in **`autoEdit`** mode applies to `autoEdit` and `yolo`.
|
||||
- An approval granted in **`yolo`** mode applies only to `yolo`. This ensures
|
||||
that trust flows correctly to more permissive environments while maintaining
|
||||
the safety of more restricted modes like `plan`.
|
||||
|
||||
## Rule matching
|
||||
|
||||
When a tool call is made, the engine checks it against all active rules,
|
||||
@@ -304,7 +322,8 @@ priority = 10
|
||||
denyMessage = "Deletion is permanent"
|
||||
|
||||
# (Optional) An array of approval modes where this rule is active.
|
||||
modes = ["autoEdit"]
|
||||
# If omitted or empty, the rule applies to all modes.
|
||||
modes = ["default", "autoEdit", "yolo"]
|
||||
|
||||
# (Optional) A boolean to restrict the rule to interactive (true) or
|
||||
# non-interactive (false) environments.
|
||||
|
||||
@@ -22,12 +22,6 @@ nightly) or the release branch (for preview/stable).
|
||||
|
||||
- **Platforms:** Tests must pass on **Linux and macOS**.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
> [!NOTE]
|
||||
> Windows tests currently run with `continue-on-error: true`. While a
|
||||
> failure here doesn't block the release technically, it should be
|
||||
> investigated.
|
||||
|
||||
- **Checks:**
|
||||
- **Linting:** No linting errors (ESLint, Prettier, etc.).
|
||||
- **Typechecking:** No TypeScript errors.
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# Gemini CLI releases
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
> [!IMPORTANT]
|
||||
> **Coordinate with the Release Manager:** The release manager is responsible for coordinating patches and releases. Please update them before performing any of the release actions described in this document.
|
||||
|
||||
## `dev` vs `prod` environment
|
||||
|
||||
Our release flows support both `dev` and `prod` environments.
|
||||
|
||||
@@ -94,6 +94,10 @@ const cliConfig = {
|
||||
'process.env.GEMINI_SANDBOX_IMAGE_DEFAULT': JSON.stringify(
|
||||
pkg.config?.sandboxImageUri,
|
||||
),
|
||||
'process.env.NODE_ENV': JSON.stringify(
|
||||
process.env.NODE_ENV || 'production',
|
||||
),
|
||||
'process.env.DEV': JSON.stringify(process.env.DEV || 'false'),
|
||||
},
|
||||
plugins: createWasmPlugins(),
|
||||
alias: {
|
||||
@@ -114,6 +118,10 @@ const a2aServerConfig = {
|
||||
__filename: '__chunk_filename',
|
||||
__dirname: '__chunk_dirname',
|
||||
'process.env.CLI_VERSION': JSON.stringify(pkg.version),
|
||||
'process.env.NODE_ENV': JSON.stringify(
|
||||
process.env.NODE_ENV || 'production',
|
||||
),
|
||||
'process.env.DEV': JSON.stringify(process.env.DEV || 'false'),
|
||||
},
|
||||
plugins: createWasmPlugins(),
|
||||
alias: commonAliases,
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, expect } from 'vitest';
|
||||
import { evalTest } from './test-helper.js';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
describe('Background Process Monitoring', () => {
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: 'should naturally use read output tool to find token',
|
||||
prompt:
|
||||
"Run the script using 'bash generate_token.sh'. It will emit a token after a short delay and continue running. Find the token and tell me what it is.",
|
||||
files: {
|
||||
'generate_token.sh': `#!/bin/bash
|
||||
sleep 2
|
||||
echo "TOKEN=xyz123"
|
||||
sleep 100
|
||||
`,
|
||||
},
|
||||
setup: async (rig) => {
|
||||
// Create .gemini directory to avoid file system error in test rig
|
||||
if (rig.homeDir) {
|
||||
const geminiDir = path.join(rig.homeDir, '.gemini');
|
||||
fs.mkdirSync(geminiDir, { recursive: true });
|
||||
}
|
||||
},
|
||||
assert: async (rig, result) => {
|
||||
const toolCalls = rig.readToolLogs();
|
||||
|
||||
// Check if read_background_output was called
|
||||
const hasReadCall = toolCalls.some(
|
||||
(call) => call.toolRequest.name === 'read_background_output',
|
||||
);
|
||||
|
||||
expect(
|
||||
hasReadCall,
|
||||
'Expected agent to call read_background_output to find the token',
|
||||
).toBe(true);
|
||||
|
||||
// Verify that the agent found the correct token
|
||||
expect(
|
||||
result.includes('xyz123'),
|
||||
`Expected agent to find the token xyz123. Agent output: ${result}`,
|
||||
).toBe(true);
|
||||
},
|
||||
});
|
||||
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: 'should naturally use list tool to verify multiple processes',
|
||||
prompt:
|
||||
"Start three background processes that run 'sleep 100', 'sleep 200', and 'sleep 300' respectively. Verify that all three are currently running.",
|
||||
setup: async (rig) => {
|
||||
// Create .gemini directory to avoid file system error in test rig
|
||||
if (rig.homeDir) {
|
||||
const geminiDir = path.join(rig.homeDir, '.gemini');
|
||||
fs.mkdirSync(geminiDir, { recursive: true });
|
||||
}
|
||||
},
|
||||
assert: async (rig, result) => {
|
||||
const toolCalls = rig.readToolLogs();
|
||||
|
||||
// Check if list_background_processes was called
|
||||
const hasListCall = toolCalls.some(
|
||||
(call) => call.toolRequest.name === 'list_background_processes',
|
||||
);
|
||||
|
||||
expect(
|
||||
hasListCall,
|
||||
'Expected agent to call list_background_processes',
|
||||
).toBe(true);
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,8 @@
|
||||
*/
|
||||
|
||||
import { describe, expect } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { evalTest } from './test-helper.js';
|
||||
|
||||
describe('update_topic_behavior', () => {
|
||||
@@ -113,4 +115,104 @@ describe('update_topic_behavior', () => {
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: 'update_topic should NOT be used for informational coding tasks (Obvious)',
|
||||
approvalMode: 'default',
|
||||
prompt:
|
||||
'Explain the difference between Map and Object in JavaScript and provide a performance-focused code snippet for each.',
|
||||
files: {
|
||||
'.gemini/settings.json': JSON.stringify({
|
||||
experimental: {
|
||||
topicUpdateNarration: true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
assert: async (rig) => {
|
||||
const toolLogs = rig.readToolLogs();
|
||||
const topicCalls = toolLogs.filter(
|
||||
(l) => l.toolRequest.name === UPDATE_TOPIC_TOOL_NAME,
|
||||
);
|
||||
|
||||
expect(
|
||||
topicCalls.length,
|
||||
`Expected 0 update_topic calls for an informational task, but found ${topicCalls.length}`,
|
||||
).toBe(0);
|
||||
},
|
||||
});
|
||||
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: 'update_topic should NOT be used for surgical symbol searches (Grey Area)',
|
||||
approvalMode: 'default',
|
||||
prompt:
|
||||
"Find the file where the 'UPDATE_TOPIC_TOOL_NAME' constant is defined.",
|
||||
files: {
|
||||
'packages/core/src/tools/tool-names.ts':
|
||||
"export const UPDATE_TOPIC_TOOL_NAME = 'update_topic';",
|
||||
'.gemini/settings.json': JSON.stringify({
|
||||
experimental: {
|
||||
topicUpdateNarration: true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
assert: async (rig) => {
|
||||
const toolLogs = rig.readToolLogs();
|
||||
const topicCalls = toolLogs.filter(
|
||||
(l) => l.toolRequest.name === UPDATE_TOPIC_TOOL_NAME,
|
||||
);
|
||||
|
||||
expect(
|
||||
topicCalls.length,
|
||||
`Expected 0 update_topic calls for a surgical symbol search, but found ${topicCalls.length}`,
|
||||
).toBe(0);
|
||||
},
|
||||
});
|
||||
|
||||
evalTest('USUALLY_PASSES', {
|
||||
name: 'update_topic should be used for medium complexity multi-step tasks',
|
||||
prompt:
|
||||
'Refactor the `users-api` project. Move the routing logic from src/app.ts into a new file src/routes.ts, and update app.ts to use the new routes file.',
|
||||
files: {
|
||||
'package.json': JSON.stringify(
|
||||
{
|
||||
name: 'users-api',
|
||||
version: '1.0.0',
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
'src/app.ts': `
|
||||
import express from 'express';
|
||||
const app = express();
|
||||
|
||||
app.get('/users', (req, res) => {
|
||||
res.json([{id: 1, name: 'Alice'}]);
|
||||
});
|
||||
|
||||
app.post('/users', (req, res) => {
|
||||
res.status(201).send();
|
||||
});
|
||||
|
||||
export default app;
|
||||
`,
|
||||
'.gemini/settings.json': JSON.stringify({
|
||||
experimental: {
|
||||
topicUpdateNarration: true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
assert: async (rig) => {
|
||||
const toolLogs = rig.readToolLogs();
|
||||
const topicCalls = toolLogs.filter(
|
||||
(l) => l.toolRequest.name === UPDATE_TOPIC_TOOL_NAME,
|
||||
);
|
||||
|
||||
// This is a multi-step task (read, create new file, edit old file).
|
||||
// It should clear the bar and use update_topic at least at the start and end.
|
||||
expect(topicCalls.length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
// Verify it actually did the refactoring to ensure it didn't just fail immediately
|
||||
expect(fs.existsSync(path.join(rig.testDir, 'src/routes.ts'))).toBe(true);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll check the dynamic content page on the localhost server."},{"functionCall":{"name":"browser_agent","args":{"task":"Navigate to http://127.0.0.1:18923/dynamic.html, wait for the dynamic content to load, then capture the accessibility tree and report what content appeared"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":40,"totalTokenCount":140}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"navigate_page","args":{"url":"http://127.0.0.1:18923/dynamic.html"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":20,"totalTokenCount":120}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"wait_for","args":{"selector":"#dynamic-content","state":"visible","timeout":5000}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":150,"candidatesTokenCount":25,"totalTokenCount":175}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"take_snapshot","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":180,"candidatesTokenCount":15,"totalTokenCount":195}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"complete_task","args":{"result":{"success":true,"summary":"Dynamic content loaded successfully. Found heading 'Content Loaded', message 'This content was loaded dynamically via JavaScript.', and a list with items: Item Alpha, Item Beta, Item Gamma."}}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":40,"totalTokenCount":240}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The dynamic content page loaded successfully:\n\n- **Heading**: Content Loaded\n- **Message**: This content was loaded dynamically via JavaScript\n- **Items**: Item Alpha, Item Beta, Item Gamma\n\nThe JavaScript-rendered content appeared after the initial page load, replacing the 'Loading...' indicator."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":60,"totalTokenCount":260}}]}
|
||||
@@ -0,0 +1,9 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll fill out the contact form on the localhost server."},{"functionCall":{"name":"browser_agent","args":{"task":"Navigate to http://127.0.0.1:18923/form.html, fill in the name field with 'Test User', the email field with 'test@example.com', the message field with 'Hello World', and submit the form"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"navigate_page","args":{"url":"http://127.0.0.1:18923/form.html"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":20,"totalTokenCount":120}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"fill","args":{"selector":"#name","value":"Test User"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":150,"candidatesTokenCount":25,"totalTokenCount":175}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"fill","args":{"selector":"#email","value":"test@example.com"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":180,"candidatesTokenCount":25,"totalTokenCount":205}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"fill","args":{"selector":"#message","value":"Hello World"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":210,"candidatesTokenCount":25,"totalTokenCount":235}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"click","args":{"selector":"#submit-btn"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":240,"candidatesTokenCount":20,"totalTokenCount":260}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"take_snapshot","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":270,"candidatesTokenCount":15,"totalTokenCount":285}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"complete_task","args":{"result":{"success":true,"summary":"Successfully filled and submitted the contact form. Navigated to form-result.html which shows 'Form Submitted Successfully'."}}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":300,"candidatesTokenCount":40,"totalTokenCount":340}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I've successfully filled out and submitted the contact form:\n\n- **Name**: Test User\n- **Email**: test@example.com\n- **Message**: Hello World\n\nAfter submission, the page navigated to the result page showing 'Form Submitted Successfully'."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":300,"candidatesTokenCount":60,"totalTokenCount":360}}]}
|
||||
@@ -0,0 +1,9 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll go through the multi-step flow on the localhost server."},{"functionCall":{"name":"browser_agent","args":{"task":"Navigate to http://127.0.0.1:18923/multi-step/step1.html, fill in 'testuser' as the username, click Next, then on step 2 select 'Option B' and click Finish. Report the final result page content."}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"navigate_page","args":{"url":"http://127.0.0.1:18923/multi-step/step1.html"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":20,"totalTokenCount":120}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"fill","args":{"selector":"#username","value":"testuser"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":150,"candidatesTokenCount":25,"totalTokenCount":175}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"click","args":{"selector":"#next-btn"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":180,"candidatesTokenCount":20,"totalTokenCount":200}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"take_snapshot","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":210,"candidatesTokenCount":15,"totalTokenCount":225}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"click","args":{"selector":"#finish-btn"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":240,"candidatesTokenCount":20,"totalTokenCount":260}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"take_snapshot","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":270,"candidatesTokenCount":15,"totalTokenCount":285}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"complete_task","args":{"result":{"success":true,"summary":"Completed all steps. Step 1: entered username 'testuser'. Step 2: selected default option. Final result page shows 'Multi-Step Complete' with '✓ Complete' status badge."}}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":300,"candidatesTokenCount":40,"totalTokenCount":340}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I've completed the multi-step flow:\n\n1. **Step 1**: Entered 'testuser' as username and clicked Next\n2. **Step 2**: Confirmed selection and clicked Finish\n3. **Result**: Final page shows 'Multi-Step Complete' with a '✓ Complete' status badge\n\nAll steps were successfully navigated."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":300,"candidatesTokenCount":60,"totalTokenCount":360}}]}
|
||||
@@ -0,0 +1,5 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll navigate to the localhost page and read its content using the browser agent."},{"functionCall":{"name":"browser_agent","args":{"task":"Navigate to http://127.0.0.1:18923/index.html and tell me the page title and list all links on the page"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":40,"totalTokenCount":140}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"navigate_page","args":{"url":"http://127.0.0.1:18923/index.html"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":20,"totalTokenCount":120}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"take_snapshot","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":150,"candidatesTokenCount":20,"totalTokenCount":170}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"complete_task","args":{"result":{"success":true,"summary":"Page title is 'Test Fixture - Home'. Found 3 links: Contact Form (/form.html), Multi-Step Flow (/multi-step/step1.html), Dynamic Content (/dynamic.html)."}}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":40,"totalTokenCount":240}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The localhost test fixture page has:\n\n**Title**: Test Fixture - Home\n\n**Links**:\n1. Contact Form (form.html)\n2. Multi-Step Flow (multi-step/step1.html)\n3. Dynamic Content (dynamic.html)\n\nThe page also has a heading 'Test Fixture Home Page' and footer content."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":60,"totalTokenCount":260}}]}
|
||||
@@ -0,0 +1,5 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll take a screenshot of the localhost test page."},{"functionCall":{"name":"browser_agent","args":{"task":"Navigate to http://127.0.0.1:18923/index.html and take a screenshot of the page"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":35,"totalTokenCount":135}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"navigate_page","args":{"url":"http://127.0.0.1:18923/index.html"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":20,"totalTokenCount":120}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"take_screenshot","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":150,"candidatesTokenCount":15,"totalTokenCount":165}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"complete_task","args":{"result":{"success":true,"summary":"Screenshot captured of the localhost test fixture home page showing the heading, navigation links, and footer.","data":{"screenshotTaken":true}}}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":40,"totalTokenCount":240}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I've captured a screenshot of the localhost test fixture page. The screenshot shows the 'Test Fixture Home Page' heading with navigation links to the Contact Form, Multi-Step Flow, and Dynamic Content pages, along with the footer section."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":50,"totalTokenCount":250}}]}
|
||||
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { TestRig, assertModelHasOutput } from './test-helper.js';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
describe('browser-agent-localhost', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
const browserSettings = {
|
||||
agents: {
|
||||
overrides: {
|
||||
browser_agent: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
browser: {
|
||||
headless: true,
|
||||
sessionMode: 'isolated' as const,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rig.cleanup();
|
||||
});
|
||||
|
||||
it('should navigate to localhost fixture and read page content', async () => {
|
||||
rig.setup('localhost-navigate', {
|
||||
fakeResponsesPath: join(
|
||||
__dirname,
|
||||
'browser-agent-localhost.navigate.responses',
|
||||
),
|
||||
settings: browserSettings,
|
||||
});
|
||||
|
||||
const result = await rig.run({
|
||||
args: 'Navigate to http://127.0.0.1:18923/index.html and tell me the page title and list all links.',
|
||||
});
|
||||
|
||||
assertModelHasOutput(result);
|
||||
|
||||
const toolLogs = rig.readToolLogs();
|
||||
const browserAgentCall = toolLogs.find(
|
||||
(t) => t.toolRequest.name === 'browser_agent',
|
||||
);
|
||||
expect(
|
||||
browserAgentCall,
|
||||
'Expected browser_agent to be called',
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it('should fill out and submit a form on localhost', async () => {
|
||||
rig.setup('localhost-form', {
|
||||
fakeResponsesPath: join(
|
||||
__dirname,
|
||||
'browser-agent-localhost.form.responses',
|
||||
),
|
||||
settings: browserSettings,
|
||||
});
|
||||
|
||||
const result = await rig.run({
|
||||
args: "Navigate to http://127.0.0.1:18923/form.html, fill in name='Test User', email='test@example.com', message='Hello World', and submit the form.",
|
||||
});
|
||||
|
||||
assertModelHasOutput(result);
|
||||
|
||||
const toolLogs = rig.readToolLogs();
|
||||
const browserAgentCall = toolLogs.find(
|
||||
(t) => t.toolRequest.name === 'browser_agent',
|
||||
);
|
||||
expect(
|
||||
browserAgentCall,
|
||||
'Expected browser_agent to be called',
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it('should navigate through a multi-step flow', async () => {
|
||||
rig.setup('localhost-multistep', {
|
||||
fakeResponsesPath: join(
|
||||
__dirname,
|
||||
'browser-agent-localhost.multistep.responses',
|
||||
),
|
||||
settings: browserSettings,
|
||||
});
|
||||
|
||||
const result = await rig.run({
|
||||
args: "Go to http://127.0.0.1:18923/multi-step/step1.html, fill in 'testuser' as username, click Next, then click Finish on step 2. Report the result.",
|
||||
});
|
||||
|
||||
assertModelHasOutput(result);
|
||||
|
||||
const toolLogs = rig.readToolLogs();
|
||||
const browserAgentCall = toolLogs.find(
|
||||
(t) => t.toolRequest.name === 'browser_agent',
|
||||
);
|
||||
expect(
|
||||
browserAgentCall,
|
||||
'Expected browser_agent to be called',
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle dynamically loaded content', async () => {
|
||||
rig.setup('localhost-dynamic', {
|
||||
fakeResponsesPath: join(
|
||||
__dirname,
|
||||
'browser-agent-localhost.dynamic.responses',
|
||||
),
|
||||
settings: browserSettings,
|
||||
});
|
||||
|
||||
const result = await rig.run({
|
||||
args: 'Navigate to http://127.0.0.1:18923/dynamic.html, wait for content to load, and tell me what items appear.',
|
||||
});
|
||||
|
||||
assertModelHasOutput(result);
|
||||
|
||||
const toolLogs = rig.readToolLogs();
|
||||
const browserAgentCall = toolLogs.find(
|
||||
(t) => t.toolRequest.name === 'browser_agent',
|
||||
);
|
||||
expect(
|
||||
browserAgentCall,
|
||||
'Expected browser_agent to be called',
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it('should take a screenshot of localhost page', async () => {
|
||||
rig.setup('localhost-screenshot', {
|
||||
fakeResponsesPath: join(
|
||||
__dirname,
|
||||
'browser-agent-localhost.screenshot.responses',
|
||||
),
|
||||
settings: browserSettings,
|
||||
});
|
||||
|
||||
const result = await rig.run({
|
||||
args: 'Navigate to http://127.0.0.1:18923/index.html and take a screenshot.',
|
||||
});
|
||||
|
||||
assertModelHasOutput(result);
|
||||
|
||||
const toolLogs = rig.readToolLogs();
|
||||
const browserCalls = toolLogs.filter(
|
||||
(t) => t.toolRequest.name === 'browser_agent',
|
||||
);
|
||||
expect(browserCalls.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll open https://example.com and check the page title for you."},{"functionCall":{"name":"browser_agent","args":{"task":"Open https://example.com and get the page title"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":35,"totalTokenCount":135}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I have opened the page and the title is 'Example Domain'."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":30,"totalTokenCount":230}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The task is complete. The page title is 'Example Domain'."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":300,"candidatesTokenCount":20,"totalTokenCount":320}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Done."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":400,"candidatesTokenCount":5,"totalTokenCount":405}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"navigate_page","args":{"url":"https://example.com"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":20,"totalTokenCount":120}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"take_snapshot","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":150,"candidatesTokenCount":20,"totalTokenCount":170}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"complete_task","args":{"result":{"success":true,"summary":"The page title is 'Example Domain'."}}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":30,"totalTokenCount":230}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I have opened the page and the title is 'Example Domain'. The browser session has been cleaned up successfully."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":30,"totalTokenCount":230}}]}
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll navigate to https://example.com and analyze the links on the page."},{"functionCall":{"name":"browser_agent","args":{"task":"Go to https://example.com and find all links on the page, then describe them"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":40,"totalTokenCount":140}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"navigate_page","args":{"url":"https://example.com"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":20,"totalTokenCount":120}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"take_snapshot","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":150,"candidatesTokenCount":20,"totalTokenCount":170}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"complete_task","args":{"result":{"success":true,"summary":"Found one link on https://example.com: 'More information...' linking to the IANA website for details about reserved domains."}}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":40,"totalTokenCount":240}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"After analyzing https://example.com, I found the following links:\n\n1. **\"More information...\"** - This is the main link on the page that points to the IANA (Internet Assigned Numbers Authority) website for more details about reserved domains.\n\nThe page is quite minimal with just this single informational link, which is typical for example domains used in documentation."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":70,"totalTokenCount":270}}]}
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll help you open https://example.com and analyze the page. Let me use the browser agent to navigate and capture the page information."},{"functionCall":{"name":"browser_agent","args":{"task":"Navigate to https://example.com and capture the accessibility tree to get the page title and main content"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"navigate_page","args":{"url":"https://example.com"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":30,"totalTokenCount":130}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"take_snapshot","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":150,"candidatesTokenCount":20,"totalTokenCount":170}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"complete_task","args":{"result":{"success":true,"summary":"Successfully navigated to https://example.com. The page title is 'Example Domain' and the main content states: 'This domain is for use in illustrative examples in documents.'"}}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":40,"totalTokenCount":240}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"Based on the browser analysis of https://example.com:\n\n**Page Title**: Example Domain\n\n**Main Content**: The page contains a simple heading \"Example Domain\" and explanatory text stating \"This domain is for use in illustrative examples in documents. You may use this domain in literature without prior coordination or asking for permission.\"\n\nThe page has a clean, minimal layout typical of placeholder domains used for documentation and examples."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":80,"totalTokenCount":280}}]}
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll navigate to https://example.com and take a screenshot for you."},{"functionCall":{"name":"browser_agent","args":{"task":"Navigate to https://example.com and take a screenshot of the page"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":40,"totalTokenCount":140}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"navigate_page","args":{"url":"https://example.com"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":20,"totalTokenCount":120}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"take_screenshot","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":150,"candidatesTokenCount":20,"totalTokenCount":170}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"complete_task","args":{"result":{"success":true,"summary":"Successfully navigated to https://example.com and captured a screenshot. The page shows the Example Domain header with explanatory text."}}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":40,"totalTokenCount":240}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I've successfully navigated to https://example.com and captured a screenshot. The page shows the familiar \"Example Domain\" header with explanatory text below it. The screenshot captures the clean, minimal layout of this demonstration website."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":50,"totalTokenCount":250}}]}
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll help you navigate to https://example.com and perform both operations - capturing the accessibility tree and taking a screenshot."},{"functionCall":{"name":"browser_agent","args":{"task":"Navigate to https://example.com, take a snapshot of the accessibility tree, then take a screenshot"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":45,"totalTokenCount":145}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"navigate_page","args":{"url":"https://example.com"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":20,"totalTokenCount":120}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"take_snapshot","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":150,"candidatesTokenCount":20,"totalTokenCount":170}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"take_screenshot","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":20,"totalTokenCount":220}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"complete_task","args":{"result":{"success":true,"summary":"Successfully navigated to https://example.com, captured accessibility tree snapshot and took a screenshot. Page title is 'Example Domain'."}}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":250,"candidatesTokenCount":40,"totalTokenCount":290}}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I've successfully completed both operations on https://example.com:\n\n**Accessibility Tree Snapshot**: The page has a clear structure with the main heading \"Example Domain\" and descriptive text about the domain's purpose for documentation examples.\n\n**Screenshot**: Captured a visual representation of the page showing the clean, minimal layout with the heading and explanatory text.\n\nBoth the accessibility data and visual screenshot confirm this is the standard example domain page used for documentation purposes."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":80,"totalTokenCount":280}}]}
|
||||
|
||||
@@ -77,7 +77,12 @@ describe.skipIf(!chromeAvailable)('browser-agent', () => {
|
||||
),
|
||||
settings: {
|
||||
agents: {
|
||||
browser_agent: {
|
||||
overrides: {
|
||||
browser_agent: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
browser: {
|
||||
headless: true,
|
||||
sessionMode: 'isolated',
|
||||
},
|
||||
@@ -106,7 +111,12 @@ describe.skipIf(!chromeAvailable)('browser-agent', () => {
|
||||
fakeResponsesPath: join(__dirname, 'browser-agent.screenshot.responses'),
|
||||
settings: {
|
||||
agents: {
|
||||
browser_agent: {
|
||||
overrides: {
|
||||
browser_agent: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
browser: {
|
||||
headless: true,
|
||||
sessionMode: 'isolated',
|
||||
},
|
||||
@@ -132,7 +142,12 @@ describe.skipIf(!chromeAvailable)('browser-agent', () => {
|
||||
fakeResponsesPath: join(__dirname, 'browser-agent.interaction.responses'),
|
||||
settings: {
|
||||
agents: {
|
||||
browser_agent: {
|
||||
overrides: {
|
||||
browser_agent: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
browser: {
|
||||
headless: true,
|
||||
sessionMode: 'isolated',
|
||||
},
|
||||
@@ -161,7 +176,12 @@ describe.skipIf(!chromeAvailable)('browser-agent', () => {
|
||||
fakeResponsesPath: join(__dirname, 'browser-agent.cleanup.responses'),
|
||||
settings: {
|
||||
agents: {
|
||||
browser_agent: {
|
||||
overrides: {
|
||||
browser_agent: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
browser: {
|
||||
headless: true,
|
||||
sessionMode: 'isolated',
|
||||
},
|
||||
@@ -182,7 +202,12 @@ describe.skipIf(!chromeAvailable)('browser-agent', () => {
|
||||
fakeResponsesPath: join(__dirname, 'browser-agent.sequential.responses'),
|
||||
settings: {
|
||||
agents: {
|
||||
browser_agent: {
|
||||
overrides: {
|
||||
browser_agent: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
browser: {
|
||||
headless: true,
|
||||
sessionMode: 'isolated',
|
||||
},
|
||||
@@ -212,7 +237,12 @@ describe.skipIf(!chromeAvailable)('browser-agent', () => {
|
||||
),
|
||||
settings: {
|
||||
agents: {
|
||||
browser_agent: {
|
||||
overrides: {
|
||||
browser_agent: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
browser: {
|
||||
headless: true,
|
||||
sessionMode: 'isolated',
|
||||
},
|
||||
|
||||
@@ -9,16 +9,80 @@ if (process.env['NO_COLOR'] !== undefined) {
|
||||
delete process.env['NO_COLOR'];
|
||||
}
|
||||
|
||||
import { mkdir, readdir, rm } from 'node:fs/promises';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { mkdir, readdir, rm, readFile } from 'node:fs/promises';
|
||||
import { join, dirname, extname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { canUseRipgrep } from '../packages/core/src/tools/ripGrep.js';
|
||||
import { disableMouseTracking } from '@google/gemini-cli-core';
|
||||
import { createServer, type Server } from 'node:http';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = join(__dirname, '..');
|
||||
const integrationTestsDir = join(rootDir, '.integration-tests');
|
||||
let runDir = ''; // Make runDir accessible in teardown
|
||||
let fixtureServer: Server | undefined;
|
||||
|
||||
const FIXTURE_PORT = 18923;
|
||||
const FIXTURE_DIR = join(__dirname, 'test-fixtures');
|
||||
|
||||
const MIME_TYPES: Record<string, string> = {
|
||||
'.html': 'text/html',
|
||||
'.css': 'text/css',
|
||||
'.js': 'application/javascript',
|
||||
'.json': 'application/json',
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.svg': 'image/svg+xml',
|
||||
};
|
||||
|
||||
async function startFixtureServer(): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = createServer(async (req, res) => {
|
||||
const urlPath = req.url?.split('?')[0] || '/';
|
||||
const relativePath = urlPath === '/' ? 'index.html' : urlPath;
|
||||
const filePath = join(FIXTURE_DIR, relativePath);
|
||||
|
||||
if (!filePath.startsWith(FIXTURE_DIR)) {
|
||||
res.writeHead(403, { 'Content-Type': 'text/html' });
|
||||
res.end('<h1>403 Forbidden</h1>');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const content = await readFile(filePath);
|
||||
const ext = extname(filePath);
|
||||
res.writeHead(200, {
|
||||
'Content-Type': MIME_TYPES[ext] || 'application/octet-stream',
|
||||
});
|
||||
res.end(content);
|
||||
} catch {
|
||||
res.writeHead(404, { 'Content-Type': 'text/html' });
|
||||
res.end('<h1>404 Not Found</h1>');
|
||||
}
|
||||
});
|
||||
|
||||
server.on('error', (err: NodeJS.ErrnoException) => {
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
console.warn(
|
||||
`Port ${FIXTURE_PORT} in use, trying ${FIXTURE_PORT + 1}...`,
|
||||
);
|
||||
server.listen(FIXTURE_PORT + 1, '127.0.0.1');
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
|
||||
server.on('listening', () => {
|
||||
const addr = server.address();
|
||||
const port = typeof addr === 'object' && addr ? addr.port : FIXTURE_PORT;
|
||||
fixtureServer = server;
|
||||
console.log(`Test fixture server listening on http://127.0.0.1:${port}`);
|
||||
resolve(port);
|
||||
});
|
||||
|
||||
server.listen(FIXTURE_PORT, '127.0.0.1');
|
||||
});
|
||||
}
|
||||
|
||||
export async function setup() {
|
||||
runDir = join(integrationTestsDir, `${Date.now()}`);
|
||||
@@ -40,6 +104,10 @@ export async function setup() {
|
||||
throw new Error('Failed to download ripgrep binary');
|
||||
}
|
||||
|
||||
// Start the test fixture server
|
||||
const port = await startFixtureServer();
|
||||
process.env['TEST_FIXTURE_PORT'] = String(port);
|
||||
|
||||
// Clean up old test runs, but keep the latest few for debugging
|
||||
try {
|
||||
const testRuns = await readdir(integrationTestsDir);
|
||||
@@ -73,6 +141,14 @@ export async function setup() {
|
||||
}
|
||||
|
||||
export async function teardown() {
|
||||
// Stop the fixture server
|
||||
if (fixtureServer) {
|
||||
await new Promise<void>((resolve) => {
|
||||
fixtureServer!.close(() => resolve());
|
||||
});
|
||||
fixtureServer = undefined;
|
||||
}
|
||||
|
||||
// Disable mouse tracking
|
||||
if (process.stdout.isTTY) {
|
||||
disableMouseTracking();
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I will run the command in the background for you."},{"functionCall":{"name":"run_shell_command","args":{"command":"sleep 10 && echo hello-from-background","is_background":true}}}],"role":"model"},"finishReason":"STOP","index":0}]}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The background process has been started. Now I will list the background processes to verify."},{"functionCall":{"name":"list_background_processes","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}]}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I see the background process 'sleep 10 && echo hello-from-background' is running. Would you like me to read its output?"}],"role":"model"},"finishReason":"STOP","index":0}]}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I will read the output for you."},{"functionCall":{"name":"read_background_output","args":{"pid":12345}}}],"role":"model"},"finishReason":"STOP","index":0}]}]}
|
||||
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The output of the background process is:\nhello-from-background"}],"role":"model"},"finishReason":"STOP","index":0}]}]}
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, beforeEach, afterEach } from 'vitest';
|
||||
import { TestRig } from './test-helper.js';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
describe('shell-background-tools', () => {
|
||||
let rig: TestRig;
|
||||
|
||||
beforeEach(() => {
|
||||
rig = new TestRig();
|
||||
});
|
||||
|
||||
afterEach(async () => await rig.cleanup());
|
||||
|
||||
it('should run a command in the background, list it, and read its output', async () => {
|
||||
// We use a fake responses file to make the test deterministic and run in CI.
|
||||
rig.setup('shell-background-workflow', {
|
||||
fakeResponsesPath: join(__dirname, 'shell-background.responses'),
|
||||
settings: {
|
||||
tools: {
|
||||
core: [
|
||||
'run_shell_command',
|
||||
'list_background_processes',
|
||||
'read_background_output',
|
||||
],
|
||||
},
|
||||
hooksConfig: {
|
||||
enabled: true,
|
||||
},
|
||||
hooks: {
|
||||
BeforeTool: [
|
||||
{
|
||||
matcher: 'run_shell_command',
|
||||
hooks: [
|
||||
{
|
||||
type: 'command',
|
||||
// This hook intercepts run_shell_command.
|
||||
// If is_background is true, it returns a mock result with PID 12345.
|
||||
// It also creates the mock log file that read_background_output expects.
|
||||
command: `node -e "
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const input = JSON.parse(fs.readFileSync(0, 'utf-8'));
|
||||
const args = JSON.parse(input.tool_call.args);
|
||||
|
||||
if (args.is_background) {
|
||||
const logDir = path.join(process.env.GEMINI_CLI_HOME, 'background-processes');
|
||||
if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(logDir, 'background-12345.log'), 'hello-from-background\\n');
|
||||
|
||||
console.log(JSON.stringify({
|
||||
decision: 'replace',
|
||||
hookSpecificOutput: {
|
||||
result: {
|
||||
llmContent: 'Command moved to background (PID: 12345). Output hidden. Press Ctrl+B to view.',
|
||||
data: { pid: 12345, command: args.command }
|
||||
}
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
console.log(JSON.stringify({ decision: 'allow' }));
|
||||
}
|
||||
"`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const run = await rig.runInteractive({ approvalMode: 'yolo' });
|
||||
|
||||
// 1. Start a background process
|
||||
// We use a command that stays alive for a bit to ensure it shows up in lists
|
||||
await run.type(
|
||||
"Run 'sleep 10 && echo hello-from-background' in the background.",
|
||||
);
|
||||
await run.type('\r');
|
||||
|
||||
// Wait for the model's canned response acknowledging the start
|
||||
await run.expectText('background', 30000);
|
||||
|
||||
// 2. List background processes
|
||||
await run.type('List my background processes.');
|
||||
await run.type('\r');
|
||||
// Wait for the model's canned response showing the list
|
||||
await run.expectText('hello-from-background', 30000);
|
||||
|
||||
// 3. Read the output
|
||||
await run.type('Read the output of that process.');
|
||||
await run.type('\r');
|
||||
// Wait for the model's canned response showing the output
|
||||
await run.expectText('hello-from-background', 30000);
|
||||
}, 60000);
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Test Fixture - Dynamic Content</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1 id="dynamic-heading">Dynamic Content Page</h1>
|
||||
<div id="loading-indicator">Loading...</div>
|
||||
<div id="dynamic-content" style="display: none">
|
||||
<h2 id="loaded-heading">Content Loaded</h2>
|
||||
<p id="loaded-message">
|
||||
This content was loaded dynamically via JavaScript.
|
||||
</p>
|
||||
<ul id="item-list">
|
||||
<li>Item Alpha</li>
|
||||
<li>Item Beta</li>
|
||||
<li>Item Gamma</li>
|
||||
</ul>
|
||||
</div>
|
||||
<script>
|
||||
// Simulate async content loading
|
||||
setTimeout(function () {
|
||||
document.getElementById('loading-indicator').style.display = 'none';
|
||||
document.getElementById('dynamic-content').style.display = 'block';
|
||||
}, 500);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Test Fixture - Form Result</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1 id="result-heading">Form Submitted Successfully</h1>
|
||||
<p id="result-message">Thank you for your submission.</p>
|
||||
<div id="submitted-data">
|
||||
<p>Your form data has been received.</p>
|
||||
</div>
|
||||
<a href="/index.html" id="home-link">Back to Home</a>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,37 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Test Fixture - Contact Form</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1 id="form-heading">Contact Form</h1>
|
||||
<form id="contact-form" action="/form-result.html" method="GET">
|
||||
<div>
|
||||
<label for="name">Name:</label>
|
||||
<input type="text" id="name" name="name" required />
|
||||
</div>
|
||||
<div>
|
||||
<label for="email">Email:</label>
|
||||
<input type="email" id="email" name="email" required />
|
||||
</div>
|
||||
<div>
|
||||
<label for="message">Message:</label>
|
||||
<textarea id="message" name="message" rows="4"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label for="category">Category:</label>
|
||||
<select id="category" name="category">
|
||||
<option value="general">General</option>
|
||||
<option value="support">Support</option>
|
||||
<option value="feedback">Feedback</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="subscribe" name="subscribe" value="yes" />
|
||||
<label for="subscribe">Subscribe to newsletter</label>
|
||||
</div>
|
||||
<button type="submit" id="submit-btn">Submit</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,27 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Test Fixture - Home</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1 id="main-heading">Test Fixture Home Page</h1>
|
||||
<p id="description">
|
||||
This is a test fixture page for browser agent integration tests.
|
||||
</p>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/form.html" id="form-link">Contact Form</a></li>
|
||||
<li>
|
||||
<a href="/multi-step/step1.html" id="multistep-link"
|
||||
>Multi-Step Flow</a
|
||||
>
|
||||
</li>
|
||||
<li><a href="/dynamic.html" id="dynamic-link">Dynamic Content</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<footer id="footer">
|
||||
<p>Footer content for testing.</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Test Fixture - Result</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1 id="result-heading">Multi-Step Complete</h1>
|
||||
<p id="result-message">You have completed all steps successfully.</p>
|
||||
<div id="completion-status">
|
||||
<span id="status-badge">✓ Complete</span>
|
||||
</div>
|
||||
<a href="/index.html" id="home-link">Back to Home</a>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Test Fixture - Step 1</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1 id="step-heading">Step 1: Enter Your Details</h1>
|
||||
<p id="step-description">Please provide your name to continue.</p>
|
||||
<form id="step1-form" action="/multi-step/step2.html" method="GET">
|
||||
<label for="username">Username:</label>
|
||||
<input type="text" id="username" name="username" required />
|
||||
<button type="submit" id="next-btn">Next Step</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,22 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Test Fixture - Step 2</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1 id="step-heading">Step 2: Confirm Your Selection</h1>
|
||||
<p id="step-description">Choose your preference below.</p>
|
||||
<form id="step2-form" action="/multi-step/result.html" method="GET">
|
||||
<div>
|
||||
<label for="preference">Preference:</label>
|
||||
<select id="preference" name="preference">
|
||||
<option value="option-a">Option A</option>
|
||||
<option value="option-b">Option B</option>
|
||||
<option value="option-c">Option C</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" id="finish-btn">Finish</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+5
-5
@@ -11,7 +11,7 @@
|
||||
"packages/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"ink": "npm:@jrichman/ink@6.6.3",
|
||||
"ink": "npm:@jrichman/ink@6.6.7",
|
||||
"latest-version": "^9.0.0",
|
||||
"node-fetch-native": "^1.6.7",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
@@ -10049,9 +10049,9 @@
|
||||
},
|
||||
"node_modules/ink": {
|
||||
"name": "@jrichman/ink",
|
||||
"version": "6.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.6.3.tgz",
|
||||
"integrity": "sha512-0v4S7TbbF2tpQrfqH1btwLgTgH+K0vY2BJbokTE5Lk1KBr4TqZ+Pyo+geSD5F+zytX6G2ajGHBQyHk8yGK4C7A==",
|
||||
"version": "6.6.7",
|
||||
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.6.7.tgz",
|
||||
"integrity": "sha512-bDzQLpLzK/dn9Ur/Ku88ZZR9totVcMGrGYAgPHidsAAbe9NKztU1fggj/iu0wRp5g1kBeALb3cfagFGdDxAU1w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-escapes": "^7.0.0",
|
||||
@@ -17526,7 +17526,7 @@
|
||||
"fzf": "^0.5.2",
|
||||
"glob": "^12.0.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"ink": "npm:@jrichman/ink@6.6.3",
|
||||
"ink": "npm:@jrichman/ink@6.6.7",
|
||||
"ink-gradient": "^3.0.0",
|
||||
"ink-spinner": "^5.0.0",
|
||||
"latest-version": "^9.0.0",
|
||||
|
||||
+3
-2
@@ -18,6 +18,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env NODE_ENV=development node scripts/start.js",
|
||||
"start:prod": "cross-env NODE_ENV=production node scripts/start.js",
|
||||
"start:a2a-server": "CODER_AGENT_PORT=41242 npm run start --workspace @google/gemini-cli-a2a-server",
|
||||
"debug": "cross-env DEBUG=1 node --inspect-brk scripts/start.js",
|
||||
"deflake": "node scripts/deflake.js",
|
||||
@@ -68,7 +69,7 @@
|
||||
"pre-commit": "node scripts/pre-commit.js"
|
||||
},
|
||||
"overrides": {
|
||||
"ink": "npm:@jrichman/ink@6.6.3",
|
||||
"ink": "npm:@jrichman/ink@6.6.7",
|
||||
"wrap-ansi": "9.0.2",
|
||||
"cliui": {
|
||||
"wrap-ansi": "7.0.0"
|
||||
@@ -136,7 +137,7 @@
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"ink": "npm:@jrichman/ink@6.6.3",
|
||||
"ink": "npm:@jrichman/ink@6.6.7",
|
||||
"latest-version": "^9.0.0",
|
||||
"node-fetch-native": "^1.6.7",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
|
||||
@@ -109,7 +109,7 @@ export function createMockConfig(
|
||||
enableEnvironmentVariableRedaction: false,
|
||||
},
|
||||
}),
|
||||
isAutoDistillationEnabled: vi.fn().mockReturnValue(false),
|
||||
isContextManagementEnabled: vi.fn().mockReturnValue(false),
|
||||
getContextManagementConfig: vi.fn().mockReturnValue({ enabled: false }),
|
||||
...overrides,
|
||||
} as unknown as Config;
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
"fzf": "^0.5.2",
|
||||
"glob": "^12.0.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"ink": "npm:@jrichman/ink@6.6.3",
|
||||
"ink": "npm:@jrichman/ink@6.6.7",
|
||||
"ink-gradient": "^3.0.0",
|
||||
"ink-spinner": "^5.0.0",
|
||||
"latest-version": "^9.0.0",
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`runNonInteractive > should emit appropriate error event in streaming JSON mode: 'loop detected' 1`] = `
|
||||
"{"type":"init","timestamp":"<TIMESTAMP>","session_id":"test-session-id","model":"test-model"}
|
||||
{"type":"message","timestamp":"<TIMESTAMP>","role":"user","content":"Loop test"}
|
||||
{"type":"error","timestamp":"<TIMESTAMP>","severity":"warning","message":"Loop detected, stopping execution"}
|
||||
{"type":"result","timestamp":"<TIMESTAMP>","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"cached":0,"input":0,"duration_ms":<DURATION>,"tool_calls":0,"models":{}}}
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`runNonInteractive > should emit appropriate error event in streaming JSON mode: 'max session turns' 1`] = `
|
||||
"{"type":"init","timestamp":"<TIMESTAMP>","session_id":"test-session-id","model":"test-model"}
|
||||
{"type":"message","timestamp":"<TIMESTAMP>","role":"user","content":"Max turns test"}
|
||||
{"type":"error","timestamp":"<TIMESTAMP>","severity":"error","message":"Maximum session turns exceeded"}
|
||||
{"type":"result","timestamp":"<TIMESTAMP>","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"cached":0,"input":0,"duration_ms":<DURATION>,"tool_calls":0,"models":{}}}
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`runNonInteractive > should emit appropriate events for streaming JSON output 1`] = `
|
||||
"{"type":"init","timestamp":"<TIMESTAMP>","session_id":"test-session-id","model":"test-model"}
|
||||
{"type":"message","timestamp":"<TIMESTAMP>","role":"user","content":"Stream test"}
|
||||
{"type":"message","timestamp":"<TIMESTAMP>","role":"assistant","content":"Thinking...","delta":true}
|
||||
{"type":"tool_use","timestamp":"<TIMESTAMP>","tool_name":"testTool","tool_id":"tool-1","parameters":{"arg1":"value1"}}
|
||||
{"type":"tool_result","timestamp":"<TIMESTAMP>","tool_id":"tool-1","status":"success","output":"Tool executed successfully"}
|
||||
{"type":"message","timestamp":"<TIMESTAMP>","role":"assistant","content":"Final answer","delta":true}
|
||||
{"type":"result","timestamp":"<TIMESTAMP>","status":"success","stats":{"total_tokens":0,"input_tokens":0,"output_tokens":0,"cached":0,"input":0,"duration_ms":<DURATION>,"tool_calls":0,"models":{}}}
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`runNonInteractive > should write a single newline between sequential text outputs from the model 1`] = `
|
||||
"Use mock tool
|
||||
Use mock tool again
|
||||
Finished.
|
||||
"
|
||||
`;
|
||||
@@ -875,6 +875,32 @@ describe('Session', () => {
|
||||
expect(result).toMatchObject({ stopReason: 'end_turn' });
|
||||
});
|
||||
|
||||
it('should handle prompt with no finish reason (InvalidStreamError)', async () => {
|
||||
mockChat.sendMessageStream.mockRejectedValue(
|
||||
new InvalidStreamError('No finish reason', 'NO_FINISH_REASON'),
|
||||
);
|
||||
|
||||
const result = await session.prompt({
|
||||
sessionId: 'session-1',
|
||||
prompt: [{ type: 'text', text: 'Hi' }],
|
||||
});
|
||||
|
||||
expect(mockChat.sendMessageStream).toHaveBeenCalled();
|
||||
expect(result).toMatchObject({ stopReason: 'end_turn' });
|
||||
});
|
||||
|
||||
it('should handle prompt with no finish reason (NO_FINISH_REASON anomaly)', async () => {
|
||||
mockChat.sendMessageStream.mockRejectedValue({ type: 'NO_FINISH_REASON' });
|
||||
|
||||
const result = await session.prompt({
|
||||
sessionId: 'session-1',
|
||||
prompt: [{ type: 'text', text: 'Hi' }],
|
||||
});
|
||||
|
||||
expect(mockChat.sendMessageStream).toHaveBeenCalled();
|
||||
expect(result).toMatchObject({ stopReason: 'end_turn' });
|
||||
});
|
||||
|
||||
it('should handle /memory command', async () => {
|
||||
const handleCommandSpy = vi
|
||||
.spyOn(
|
||||
|
||||
@@ -863,7 +863,10 @@ export class Session {
|
||||
(error &&
|
||||
typeof error === 'object' &&
|
||||
'type' in error &&
|
||||
error.type === 'NO_RESPONSE_TEXT')
|
||||
(error.type === 'NO_RESPONSE_TEXT' ||
|
||||
error.type === 'NO_FINISH_REASON' ||
|
||||
error.type === 'MALFORMED_FUNCTION_CALL' ||
|
||||
error.type === 'UNEXPECTED_TOOL_CALL'))
|
||||
) {
|
||||
// The stream ended with an empty response or malformed tool call.
|
||||
// Treat this as a graceful end to the model's turn rather than a crash.
|
||||
|
||||
@@ -26,5 +26,8 @@ describe('CommandHandler', () => {
|
||||
|
||||
const init = parse('/init');
|
||||
expect(init.commandToExecute?.name).toBe('init');
|
||||
|
||||
const about = parse('/about');
|
||||
expect(about.commandToExecute?.name).toBe('about');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import { MemoryCommand } from './commands/memory.js';
|
||||
import { ExtensionsCommand } from './commands/extensions.js';
|
||||
import { InitCommand } from './commands/init.js';
|
||||
import { RestoreCommand } from './commands/restore.js';
|
||||
import { AboutCommand } from './commands/about.js';
|
||||
|
||||
export class CommandHandler {
|
||||
private registry: CommandRegistry;
|
||||
@@ -24,6 +25,7 @@ export class CommandHandler {
|
||||
registry.register(new ExtensionsCommand());
|
||||
registry.register(new InitCommand());
|
||||
registry.register(new RestoreCommand());
|
||||
registry.register(new AboutCommand());
|
||||
return registry;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
IdeClient,
|
||||
UserAccountManager,
|
||||
getVersion,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type {
|
||||
Command,
|
||||
CommandContext,
|
||||
CommandExecutionResponse,
|
||||
} from './types.js';
|
||||
import process from 'node:process';
|
||||
|
||||
export class AboutCommand implements Command {
|
||||
readonly name = 'about';
|
||||
readonly description = 'Show version and environment info';
|
||||
|
||||
async execute(
|
||||
context: CommandContext,
|
||||
_args: string[] = [],
|
||||
): Promise<CommandExecutionResponse> {
|
||||
const osVersion = process.platform;
|
||||
let sandboxEnv = 'no sandbox';
|
||||
if (process.env['SANDBOX'] && process.env['SANDBOX'] !== 'sandbox-exec') {
|
||||
sandboxEnv = process.env['SANDBOX'];
|
||||
} else if (process.env['SANDBOX'] === 'sandbox-exec') {
|
||||
sandboxEnv = `sandbox-exec (${
|
||||
process.env['SEATBELT_PROFILE'] || 'unknown'
|
||||
})`;
|
||||
}
|
||||
const modelVersion = context.agentContext.config.getModel() || 'Unknown';
|
||||
const cliVersion = await getVersion();
|
||||
const selectedAuthType =
|
||||
context.settings.merged?.security?.auth?.selectedType ?? '';
|
||||
const gcpProject = process.env['GOOGLE_CLOUD_PROJECT'] || '';
|
||||
const ideClient = await getIdeClientName(context);
|
||||
|
||||
const userAccountManager = new UserAccountManager();
|
||||
const cachedAccount = userAccountManager.getCachedGoogleAccount();
|
||||
const userEmail = cachedAccount ?? 'Unknown';
|
||||
|
||||
const tier = context.agentContext.config.getUserTierName() || 'Unknown';
|
||||
|
||||
const info = [
|
||||
`- Version: ${cliVersion}`,
|
||||
`- OS: ${osVersion}`,
|
||||
`- Sandbox: ${sandboxEnv}`,
|
||||
`- Model: ${modelVersion}`,
|
||||
`- Auth Type: ${selectedAuthType}`,
|
||||
`- GCP Project: ${gcpProject}`,
|
||||
`- IDE Client: ${ideClient}`,
|
||||
`- User Email: ${userEmail}`,
|
||||
`- Tier: ${tier}`,
|
||||
].join('\n');
|
||||
|
||||
return {
|
||||
name: this.name,
|
||||
data: `Gemini CLI Info:\n${info}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function getIdeClientName(context: CommandContext) {
|
||||
if (!context.agentContext.config.getIdeMode()) {
|
||||
return '';
|
||||
}
|
||||
const ideClient = await IdeClient.getInstance();
|
||||
return ideClient?.getDetectedIdeDisplayName() ?? '';
|
||||
}
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
type MCPServerConfig,
|
||||
type GeminiCLIExtension,
|
||||
Storage,
|
||||
generalistProfile,
|
||||
type ContextManagementConfig,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { loadCliConfig, parseArguments, type CliArgs } from './config.js';
|
||||
import {
|
||||
@@ -2174,6 +2176,89 @@ describe('loadCliConfig directWebFetch', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadCliConfig context management', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
|
||||
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
|
||||
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should be false by default when generalistProfile / context management is not set in settings', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
const settings = createTestMergedSettings();
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.getContextManagementConfig()).haveOwnProperty(
|
||||
'enabled',
|
||||
false,
|
||||
);
|
||||
expect(config.isContextManagementEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it('should be true when generalistProfile is set to true in settings', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
const settings = createTestMergedSettings({
|
||||
experimental: {
|
||||
generalistProfile: true,
|
||||
},
|
||||
});
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.getContextManagementConfig()).toStrictEqual(
|
||||
generalistProfile,
|
||||
);
|
||||
expect(config.isContextManagementEnabled()).toBe(true);
|
||||
});
|
||||
|
||||
it('should be true when contextManagement is set to true in settings', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
const contextManagementConfig: Partial<ContextManagementConfig> = {
|
||||
historyWindow: {
|
||||
maxTokens: 100_000,
|
||||
retainedTokens: 50_000,
|
||||
},
|
||||
messageLimits: {
|
||||
normalMaxTokens: 1000,
|
||||
retainedMaxTokens: 10_000,
|
||||
normalizationHeadRatio: 0.25,
|
||||
},
|
||||
tools: {
|
||||
distillation: {
|
||||
maxOutputTokens: 10_000,
|
||||
summarizationThresholdTokens: 15_000,
|
||||
},
|
||||
outputMasking: {
|
||||
protectionThresholdTokens: 30_000,
|
||||
minPrunableThresholdTokens: 10_000,
|
||||
protectLatestTurn: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
const settings = createTestMergedSettings({
|
||||
experimental: {
|
||||
contextManagement: true,
|
||||
},
|
||||
// The type of numbers is being inferred strangely, and so we have to cast
|
||||
// to `any` here.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
contextManagement: contextManagementConfig as any,
|
||||
});
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.getContextManagementConfig()).toStrictEqual({
|
||||
enabled: true,
|
||||
...contextManagementConfig,
|
||||
});
|
||||
expect(config.isContextManagementEnabled()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('screenReader configuration', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
type HookEventName,
|
||||
type OutputFormat,
|
||||
detectIdeFromEnv,
|
||||
generalistProfile,
|
||||
} from '@google/gemini-cli-core';
|
||||
import {
|
||||
type Settings,
|
||||
@@ -883,6 +884,16 @@ export async function loadCliConfig(
|
||||
}
|
||||
}
|
||||
|
||||
const useGeneralistProfile =
|
||||
settings.experimental?.generalistProfile ?? false;
|
||||
const useContextManagement =
|
||||
settings.experimental?.contextManagement ?? false;
|
||||
const contextManagement = {
|
||||
...(useGeneralistProfile ? generalistProfile : {}),
|
||||
...(useContextManagement ? settings?.contextManagement : {}),
|
||||
enabled: useContextManagement || useGeneralistProfile,
|
||||
};
|
||||
|
||||
return new Config({
|
||||
acpMode: isAcpMode,
|
||||
clientName,
|
||||
@@ -977,10 +988,7 @@ export async function loadCliConfig(
|
||||
disabledSkills: settings.skills?.disabled,
|
||||
experimentalJitContext: settings.experimental?.jitContext,
|
||||
experimentalMemoryManager: settings.experimental?.memoryManager,
|
||||
contextManagement: {
|
||||
enabled: settings.experimental?.contextManagement,
|
||||
...settings?.contextManagement,
|
||||
},
|
||||
contextManagement,
|
||||
modelSteering: settings.experimental?.modelSteering,
|
||||
topicUpdateNarration: settings.experimental?.topicUpdateNarration,
|
||||
noBrowser: !!process.env['NO_BROWSER'],
|
||||
@@ -993,6 +1001,8 @@ export async function loadCliConfig(
|
||||
trustedFolder,
|
||||
useBackgroundColor: settings.ui?.useBackgroundColor,
|
||||
useAlternateBuffer: settings.ui?.useAlternateBuffer,
|
||||
useTerminalBuffer: settings.ui?.terminalBuffer,
|
||||
useRenderProcess: settings.ui?.renderProcess,
|
||||
useRipgrep: settings.tools?.useRipgrep,
|
||||
enableInteractiveShell: settings.tools?.shell?.enableInteractiveShell,
|
||||
shellBackgroundCompletionBehavior: settings.tools?.shell
|
||||
|
||||
@@ -576,7 +576,7 @@ const SETTINGS_SCHEMA = {
|
||||
label: 'Compact Tool Output',
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
default: true,
|
||||
description:
|
||||
'Display tool outputs (like directory listings and file reads) in a compact, structured format.',
|
||||
showInDialog: true,
|
||||
@@ -743,6 +743,24 @@ const SETTINGS_SCHEMA = {
|
||||
'Use an alternate screen buffer for the UI, preserving shell history.',
|
||||
showInDialog: true,
|
||||
},
|
||||
renderProcess: {
|
||||
type: 'boolean',
|
||||
label: 'Render Process',
|
||||
category: 'UI',
|
||||
requiresRestart: true,
|
||||
default: true,
|
||||
description: 'Enable Ink render process for the UI.',
|
||||
showInDialog: true,
|
||||
},
|
||||
terminalBuffer: {
|
||||
type: 'boolean',
|
||||
label: 'Terminal Buffer',
|
||||
category: 'UI',
|
||||
requiresRestart: true,
|
||||
default: true,
|
||||
description: 'Use the new terminal buffer architecture for rendering.',
|
||||
showInDialog: true,
|
||||
},
|
||||
useBackgroundColor: {
|
||||
type: 'boolean',
|
||||
label: 'Use Background Color',
|
||||
@@ -1202,7 +1220,8 @@ const SETTINGS_SCHEMA = {
|
||||
category: 'Advanced',
|
||||
requiresRestart: true,
|
||||
default: undefined as string | undefined,
|
||||
description: 'Model override for the visual agent.',
|
||||
description:
|
||||
"Model for the visual agent's analyze_screenshot tool. When set, enables the tool.",
|
||||
showInDialog: false,
|
||||
},
|
||||
allowedDomains: {
|
||||
@@ -1508,7 +1527,7 @@ const SETTINGS_SCHEMA = {
|
||||
label: 'Show Color',
|
||||
category: 'Tools',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
default: true,
|
||||
description: 'Show color in shell output.',
|
||||
showInDialog: true,
|
||||
},
|
||||
@@ -1692,10 +1711,10 @@ const SETTINGS_SCHEMA = {
|
||||
type: 'boolean',
|
||||
label: 'Tool Sandboxing',
|
||||
category: 'Security',
|
||||
requiresRestart: false,
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description:
|
||||
'Experimental tool-level sandboxing (implementation in progress).',
|
||||
'Tool-level sandboxing. Isolates individual tools instead of the entire CLI process.',
|
||||
showInDialog: true,
|
||||
},
|
||||
disableYoloMode: {
|
||||
@@ -2148,6 +2167,16 @@ const SETTINGS_SCHEMA = {
|
||||
'Replace the built-in save_memory tool with a memory manager subagent that supports adding, removing, de-duplicating, and organizing memories.',
|
||||
showInDialog: true,
|
||||
},
|
||||
generalistProfile: {
|
||||
type: 'boolean',
|
||||
label: 'Use the generalist profile to manage agent contexts.',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description:
|
||||
'Suitable for general coding and software development tasks.',
|
||||
showInDialog: true,
|
||||
},
|
||||
contextManagement: {
|
||||
type: 'boolean',
|
||||
label: 'Enable Context Management',
|
||||
|
||||
@@ -327,6 +327,7 @@ describe('gemini.tsx main function cleanup', () => {
|
||||
refreshAuth: vi.fn(),
|
||||
getRemoteAdminSettings: vi.fn(() => undefined),
|
||||
getUseAlternateBuffer: vi.fn(() => false),
|
||||
getUseTerminalBuffer: vi.fn(() => false),
|
||||
...overrides,
|
||||
} as unknown as Config;
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ describe('Model Steering Integration', () => {
|
||||
|
||||
// Then it should proceed with the next action
|
||||
await rig.waitForOutput(
|
||||
/Since you want me to focus on .txt files,[\s\S]*I will read file1.txt/,
|
||||
/Since you want me to focus on \.txt[\s\S]*files,[\s\S]*I will read file1\.txt/,
|
||||
);
|
||||
await rig.waitForOutput('ReadFile');
|
||||
|
||||
|
||||
@@ -43,7 +43,6 @@ import { KeypressProvider } from './ui/contexts/KeypressContext.js';
|
||||
import { useKittyKeyboardProtocol } from './ui/hooks/useKittyKeyboardProtocol.js';
|
||||
import { ScrollProvider } from './ui/contexts/ScrollProvider.js';
|
||||
import { TerminalProvider } from './ui/contexts/TerminalContext.js';
|
||||
import { isAlternateBufferEnabled } from './ui/hooks/useAlternateBuffer.js';
|
||||
import { OverflowProvider } from './ui/contexts/OverflowContext.js';
|
||||
import { profiler } from './ui/components/DebugProfiler.js';
|
||||
import { initializeConsoleStore } from './ui/hooks/useConsoleMessages.js';
|
||||
@@ -64,7 +63,7 @@ export async function startInteractiveUI(
|
||||
// and the Ink alternate buffer mode requires line wrapping harmful to
|
||||
// screen readers.
|
||||
const useAlternateBuffer = shouldEnterAlternateScreen(
|
||||
isAlternateBufferEnabled(config),
|
||||
config.getUseAlternateBuffer(),
|
||||
config.getScreenReader(),
|
||||
);
|
||||
const mouseEventsEnabled = useAlternateBuffer;
|
||||
@@ -133,7 +132,6 @@ export async function startInteractiveUI(
|
||||
// Wait a moment for shpool to stabilize terminal size and state.
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
const instance = render(
|
||||
process.env['DEBUG'] ? (
|
||||
<React.StrictMode>
|
||||
@@ -154,8 +152,12 @@ export async function startInteractiveUI(
|
||||
}
|
||||
profiler.reportFrameRendered();
|
||||
},
|
||||
standardReactLayoutTiming:
|
||||
useAlternateBuffer || config.getUseTerminalBuffer(),
|
||||
patchConsole: false,
|
||||
alternateBuffer: useAlternateBuffer,
|
||||
renderProcess: config.getUseRenderProcess(),
|
||||
terminalBuffer: config.getUseTerminalBuffer(),
|
||||
incrementalRendering:
|
||||
settings.merged.ui.incrementalRendering !== false &&
|
||||
useAlternateBuffer &&
|
||||
|
||||
@@ -166,7 +166,7 @@ describe('runNonInteractive', () => {
|
||||
};
|
||||
|
||||
mockConfig = {
|
||||
initialize: vi.fn().mockResolvedValue(undefined),
|
||||
initialize: vi.fn().mockReturnValue(Promise.resolve(undefined)),
|
||||
getMessageBus: vi.fn().mockReturnValue({
|
||||
subscribe: vi.fn(),
|
||||
unsubscribe: vi.fn(),
|
||||
@@ -190,6 +190,7 @@ describe('runNonInteractive', () => {
|
||||
isTrustedFolder: vi.fn().mockReturnValue(false),
|
||||
getRawOutput: vi.fn().mockReturnValue(false),
|
||||
getAcceptRawOutputRisk: vi.fn().mockReturnValue(false),
|
||||
getAgentSessionNoninteractiveEnabled: vi.fn().mockReturnValue(false),
|
||||
} as unknown as Config;
|
||||
|
||||
mockSettings = {
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
handleMaxTurnsExceededError,
|
||||
} from './utils/errors.js';
|
||||
import { TextOutput } from './ui/utils/textOutput.js';
|
||||
import { runNonInteractive as runNonInteractiveAgentSession } from './nonInteractiveCliAgentSession.js';
|
||||
|
||||
interface RunNonInteractiveParams {
|
||||
config: Config;
|
||||
@@ -55,13 +56,16 @@ interface RunNonInteractiveParams {
|
||||
resumedSessionData?: ResumedSessionData;
|
||||
}
|
||||
|
||||
export async function runNonInteractive({
|
||||
config,
|
||||
settings,
|
||||
input,
|
||||
prompt_id,
|
||||
resumedSessionData,
|
||||
}: RunNonInteractiveParams): Promise<void> {
|
||||
export async function runNonInteractive(
|
||||
params: RunNonInteractiveParams,
|
||||
): Promise<void> {
|
||||
const useAgentSession = params.config.getAgentSessionNoninteractiveEnabled();
|
||||
if (useAgentSession) {
|
||||
return runNonInteractiveAgentSession(params);
|
||||
}
|
||||
|
||||
const { config, settings, input, prompt_id, resumedSessionData } = params;
|
||||
|
||||
return promptIdContext.run(prompt_id, async () => {
|
||||
const consolePatcher = new ConsolePatcher({
|
||||
stderr: true,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,621 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {
|
||||
Config,
|
||||
ResumedSessionData,
|
||||
UserFeedbackPayload,
|
||||
AgentEvent,
|
||||
ContentPart,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { isSlashCommand } from './ui/utils/commandUtils.js';
|
||||
import type { LoadedSettings } from './config/settings.js';
|
||||
import {
|
||||
convertSessionToClientHistory,
|
||||
FatalError,
|
||||
FatalAuthenticationError,
|
||||
FatalInputError,
|
||||
FatalSandboxError,
|
||||
FatalConfigError,
|
||||
FatalTurnLimitedError,
|
||||
FatalToolExecutionError,
|
||||
FatalCancellationError,
|
||||
promptIdContext,
|
||||
OutputFormat,
|
||||
JsonFormatter,
|
||||
StreamJsonFormatter,
|
||||
JsonStreamEventType,
|
||||
uiTelemetryService,
|
||||
coreEvents,
|
||||
CoreEvent,
|
||||
createWorkingStdio,
|
||||
Scheduler,
|
||||
ROOT_SCHEDULER_ID,
|
||||
LegacyAgentSession,
|
||||
ToolErrorType,
|
||||
geminiPartsToContentParts,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
import type { Part } from '@google/genai';
|
||||
import readline from 'node:readline';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
|
||||
import { handleSlashCommand } from './nonInteractiveCliCommands.js';
|
||||
import { ConsolePatcher } from './ui/utils/ConsolePatcher.js';
|
||||
import { handleAtCommand } from './ui/hooks/atCommandProcessor.js';
|
||||
import { handleError, handleToolError } from './utils/errors.js';
|
||||
import { TextOutput } from './ui/utils/textOutput.js';
|
||||
|
||||
interface RunNonInteractiveParams {
|
||||
config: Config;
|
||||
settings: LoadedSettings;
|
||||
input: string;
|
||||
prompt_id: string;
|
||||
resumedSessionData?: ResumedSessionData;
|
||||
}
|
||||
|
||||
export async function runNonInteractive({
|
||||
config,
|
||||
settings,
|
||||
input,
|
||||
prompt_id,
|
||||
resumedSessionData,
|
||||
}: RunNonInteractiveParams): Promise<void> {
|
||||
return promptIdContext.run(prompt_id, async () => {
|
||||
const consolePatcher = new ConsolePatcher({
|
||||
stderr: true,
|
||||
interactive: false,
|
||||
debugMode: config.getDebugMode(),
|
||||
onNewMessage: (msg) => {
|
||||
coreEvents.emitConsoleLog(msg.type, msg.content);
|
||||
},
|
||||
});
|
||||
|
||||
if (process.env['GEMINI_CLI_ACTIVITY_LOG_TARGET']) {
|
||||
const { setupInitialActivityLogger } = await import(
|
||||
'./utils/devtoolsService.js'
|
||||
);
|
||||
await setupInitialActivityLogger(config);
|
||||
}
|
||||
|
||||
const { stdout: workingStdout } = createWorkingStdio();
|
||||
const textOutput = new TextOutput(workingStdout);
|
||||
|
||||
const handleUserFeedback = (payload: UserFeedbackPayload) => {
|
||||
const prefix = payload.severity.toUpperCase();
|
||||
process.stderr.write(`[${prefix}] ${payload.message}\n`);
|
||||
if (payload.error && config.getDebugMode()) {
|
||||
const errorToLog =
|
||||
payload.error instanceof Error
|
||||
? payload.error.stack || payload.error.message
|
||||
: String(payload.error);
|
||||
process.stderr.write(`${errorToLog}\n`);
|
||||
}
|
||||
};
|
||||
|
||||
const startTime = Date.now();
|
||||
const streamFormatter =
|
||||
config.getOutputFormat() === OutputFormat.STREAM_JSON
|
||||
? new StreamJsonFormatter()
|
||||
: null;
|
||||
|
||||
const abortController = new AbortController();
|
||||
|
||||
// Track cancellation state
|
||||
let isAborting = false;
|
||||
let cancelMessageTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
// Setup stdin listener for Ctrl+C detection
|
||||
let stdinWasRaw = false;
|
||||
let rl: readline.Interface | null = null;
|
||||
|
||||
const setupStdinCancellation = () => {
|
||||
// Only setup if stdin is a TTY (user can interact)
|
||||
if (!process.stdin.isTTY) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save original raw mode state
|
||||
stdinWasRaw = process.stdin.isRaw || false;
|
||||
|
||||
// Enable raw mode to capture individual keypresses
|
||||
process.stdin.setRawMode(true);
|
||||
process.stdin.resume();
|
||||
|
||||
// Setup readline to emit keypress events
|
||||
rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
escapeCodeTimeout: 0,
|
||||
});
|
||||
readline.emitKeypressEvents(process.stdin, rl);
|
||||
|
||||
// Listen for Ctrl+C
|
||||
const keypressHandler = (
|
||||
str: string,
|
||||
key: { name?: string; ctrl?: boolean },
|
||||
) => {
|
||||
// Detect Ctrl+C: either ctrl+c key combo or raw character code 3
|
||||
if ((key && key.ctrl && key.name === 'c') || str === '\u0003') {
|
||||
// Only handle once
|
||||
if (isAborting) {
|
||||
return;
|
||||
}
|
||||
|
||||
isAborting = true;
|
||||
|
||||
// Only show message if cancellation takes longer than 200ms
|
||||
// This reduces verbosity for fast cancellations
|
||||
cancelMessageTimer = setTimeout(() => {
|
||||
process.stderr.write('\nCancelling...\n');
|
||||
}, 200);
|
||||
|
||||
abortController.abort();
|
||||
}
|
||||
};
|
||||
|
||||
process.stdin.on('keypress', keypressHandler);
|
||||
};
|
||||
|
||||
const cleanupStdinCancellation = () => {
|
||||
// Clear any pending cancel message timer
|
||||
if (cancelMessageTimer) {
|
||||
clearTimeout(cancelMessageTimer);
|
||||
cancelMessageTimer = null;
|
||||
}
|
||||
|
||||
// Cleanup readline and stdin listeners
|
||||
if (rl) {
|
||||
rl.close();
|
||||
rl = null;
|
||||
}
|
||||
|
||||
// Remove keypress listener
|
||||
process.stdin.removeAllListeners('keypress');
|
||||
|
||||
// Restore stdin to original state
|
||||
if (process.stdin.isTTY) {
|
||||
process.stdin.setRawMode(stdinWasRaw);
|
||||
process.stdin.pause();
|
||||
}
|
||||
};
|
||||
|
||||
let errorToHandle: unknown | undefined;
|
||||
let abortSession = () => {};
|
||||
try {
|
||||
consolePatcher.patch();
|
||||
|
||||
if (
|
||||
config.getRawOutput() &&
|
||||
!config.getAcceptRawOutputRisk() &&
|
||||
config.getOutputFormat() === OutputFormat.TEXT
|
||||
) {
|
||||
process.stderr.write(
|
||||
'[WARNING] --raw-output is enabled. Model output is not sanitized and may contain harmful ANSI sequences (e.g. for phishing or command injection). Use --accept-raw-output-risk to suppress this warning.\n',
|
||||
);
|
||||
}
|
||||
|
||||
// Setup stdin cancellation listener
|
||||
setupStdinCancellation();
|
||||
|
||||
coreEvents.on(CoreEvent.UserFeedback, handleUserFeedback);
|
||||
coreEvents.drainBacklogs();
|
||||
|
||||
// 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.
|
||||
cleanupStdinCancellation();
|
||||
consolePatcher.cleanup();
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
|
||||
const geminiClient = config.getGeminiClient();
|
||||
const scheduler = new Scheduler({
|
||||
context: config,
|
||||
messageBus: config.getMessageBus(),
|
||||
getPreferredEditor: () => undefined,
|
||||
schedulerId: ROOT_SCHEDULER_ID,
|
||||
});
|
||||
|
||||
// Initialize chat. Resume if resume data is passed.
|
||||
if (resumedSessionData) {
|
||||
await geminiClient.resumeChat(
|
||||
convertSessionToClientHistory(
|
||||
resumedSessionData.conversation.messages,
|
||||
),
|
||||
resumedSessionData,
|
||||
);
|
||||
}
|
||||
|
||||
// Emit init event for streaming JSON
|
||||
if (streamFormatter) {
|
||||
streamFormatter.emitEvent({
|
||||
type: JsonStreamEventType.INIT,
|
||||
timestamp: new Date().toISOString(),
|
||||
session_id: config.getSessionId(),
|
||||
model: config.getModel(),
|
||||
});
|
||||
}
|
||||
|
||||
let query: Part[] | undefined;
|
||||
|
||||
if (isSlashCommand(input)) {
|
||||
const slashCommandResult = await handleSlashCommand(
|
||||
input,
|
||||
abortController,
|
||||
config,
|
||||
settings,
|
||||
);
|
||||
if (slashCommandResult) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
query = slashCommandResult as Part[];
|
||||
}
|
||||
}
|
||||
|
||||
if (!query) {
|
||||
const { processedQuery, error } = await handleAtCommand({
|
||||
query: input,
|
||||
config,
|
||||
addItem: (_item, _timestamp) => 0,
|
||||
onDebugMessage: () => {},
|
||||
messageId: Date.now(),
|
||||
signal: abortController.signal,
|
||||
escapePastedAtSymbols: false,
|
||||
});
|
||||
if (error || !processedQuery) {
|
||||
throw new FatalInputError(
|
||||
error || 'Exiting due to an error processing the @ command.',
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
query = processedQuery as Part[];
|
||||
}
|
||||
|
||||
// Emit user message event for streaming JSON
|
||||
if (streamFormatter) {
|
||||
streamFormatter.emitEvent({
|
||||
type: JsonStreamEventType.MESSAGE,
|
||||
timestamp: new Date().toISOString(),
|
||||
role: 'user',
|
||||
content: input,
|
||||
});
|
||||
}
|
||||
|
||||
// Create LegacyAgentSession — owns the agentic loop
|
||||
const session = new LegacyAgentSession({
|
||||
client: geminiClient,
|
||||
scheduler,
|
||||
config,
|
||||
promptId: prompt_id,
|
||||
});
|
||||
|
||||
// Wire Ctrl+C to session abort
|
||||
abortSession = () => {
|
||||
void session.abort();
|
||||
};
|
||||
abortController.signal.addEventListener('abort', abortSession);
|
||||
if (abortController.signal.aborted) {
|
||||
throw new FatalCancellationError('Operation cancelled.');
|
||||
}
|
||||
|
||||
// Start the agentic loop (runs in background)
|
||||
const { streamId } = await session.send({
|
||||
message: {
|
||||
content: geminiPartsToContentParts(query),
|
||||
displayContent: input,
|
||||
},
|
||||
});
|
||||
if (streamId === null) {
|
||||
throw new Error(
|
||||
'LegacyAgentSession.send() unexpectedly returned no stream for a message send.',
|
||||
);
|
||||
}
|
||||
|
||||
const getTextContent = (parts?: ContentPart[]): string | undefined => {
|
||||
const text = parts
|
||||
?.map((part) => (part.type === 'text' ? part.text : ''))
|
||||
.join('');
|
||||
return text ? text : undefined;
|
||||
};
|
||||
|
||||
const emitFinalSuccessResult = (): void => {
|
||||
if (streamFormatter) {
|
||||
const metrics = uiTelemetryService.getMetrics();
|
||||
const durationMs = Date.now() - startTime;
|
||||
streamFormatter.emitEvent({
|
||||
type: JsonStreamEventType.RESULT,
|
||||
timestamp: new Date().toISOString(),
|
||||
status: 'success',
|
||||
stats: streamFormatter.convertToStreamStats(metrics, durationMs),
|
||||
});
|
||||
} else if (config.getOutputFormat() === OutputFormat.JSON) {
|
||||
const formatter = new JsonFormatter();
|
||||
const stats = uiTelemetryService.getMetrics();
|
||||
textOutput.write(
|
||||
formatter.format(config.getSessionId(), responseText, stats),
|
||||
);
|
||||
} else {
|
||||
textOutput.ensureTrailingNewline();
|
||||
}
|
||||
};
|
||||
|
||||
const reconstructFatalError = (event: AgentEvent<'error'>): Error => {
|
||||
const errorMeta = event._meta;
|
||||
const name =
|
||||
typeof errorMeta?.['errorName'] === 'string'
|
||||
? errorMeta['errorName']
|
||||
: undefined;
|
||||
|
||||
let errToThrow: Error;
|
||||
switch (name) {
|
||||
case 'FatalAuthenticationError':
|
||||
errToThrow = new FatalAuthenticationError(event.message);
|
||||
break;
|
||||
case 'FatalInputError':
|
||||
errToThrow = new FatalInputError(event.message);
|
||||
break;
|
||||
case 'FatalSandboxError':
|
||||
errToThrow = new FatalSandboxError(event.message);
|
||||
break;
|
||||
case 'FatalConfigError':
|
||||
errToThrow = new FatalConfigError(event.message);
|
||||
break;
|
||||
case 'FatalTurnLimitedError':
|
||||
errToThrow = new FatalTurnLimitedError(event.message);
|
||||
break;
|
||||
case 'FatalToolExecutionError':
|
||||
errToThrow = new FatalToolExecutionError(event.message);
|
||||
break;
|
||||
case 'FatalCancellationError':
|
||||
errToThrow = new FatalCancellationError(event.message);
|
||||
break;
|
||||
case 'FatalError':
|
||||
errToThrow = new FatalError(
|
||||
event.message,
|
||||
typeof errorMeta?.['exitCode'] === 'number'
|
||||
? errorMeta['exitCode']
|
||||
: 1,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
errToThrow = new Error(event.message);
|
||||
if (name) {
|
||||
Object.defineProperty(errToThrow, 'name', {
|
||||
value: name,
|
||||
enumerable: true,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (errorMeta?.['exitCode'] !== undefined) {
|
||||
Object.defineProperty(errToThrow, 'exitCode', {
|
||||
value: errorMeta['exitCode'],
|
||||
enumerable: true,
|
||||
});
|
||||
}
|
||||
if (errorMeta?.['code'] !== undefined) {
|
||||
Object.defineProperty(errToThrow, 'code', {
|
||||
value: errorMeta['code'],
|
||||
enumerable: true,
|
||||
});
|
||||
}
|
||||
if (errorMeta?.['status'] !== undefined) {
|
||||
Object.defineProperty(errToThrow, 'status', {
|
||||
value: errorMeta['status'],
|
||||
enumerable: true,
|
||||
});
|
||||
}
|
||||
return errToThrow;
|
||||
};
|
||||
|
||||
// Consume AgentEvents for output formatting
|
||||
let responseText = '';
|
||||
let preToolResponseText: string | undefined;
|
||||
let streamEnded = false;
|
||||
for await (const event of session.stream({ streamId })) {
|
||||
if (streamEnded) break;
|
||||
switch (event.type) {
|
||||
case 'message': {
|
||||
if (event.role === 'agent') {
|
||||
for (const part of event.content) {
|
||||
if (part.type === 'text') {
|
||||
const isRaw =
|
||||
config.getRawOutput() || config.getAcceptRawOutputRisk();
|
||||
const output = isRaw ? part.text : stripAnsi(part.text);
|
||||
if (streamFormatter) {
|
||||
streamFormatter.emitEvent({
|
||||
type: JsonStreamEventType.MESSAGE,
|
||||
timestamp: new Date().toISOString(),
|
||||
role: 'assistant',
|
||||
content: output,
|
||||
delta: true,
|
||||
});
|
||||
} else if (config.getOutputFormat() === OutputFormat.JSON) {
|
||||
responseText += output;
|
||||
} else {
|
||||
if (part.text) {
|
||||
textOutput.write(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'tool_request': {
|
||||
if (config.getOutputFormat() === OutputFormat.JSON) {
|
||||
// Final JSON output should reflect the last assistant answer after
|
||||
// any tool orchestration, not intermediate pre-tool text.
|
||||
preToolResponseText = responseText || preToolResponseText;
|
||||
responseText = '';
|
||||
}
|
||||
if (streamFormatter) {
|
||||
streamFormatter.emitEvent({
|
||||
type: JsonStreamEventType.TOOL_USE,
|
||||
timestamp: new Date().toISOString(),
|
||||
tool_name: event.name,
|
||||
tool_id: event.requestId,
|
||||
parameters: event.args,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'tool_response': {
|
||||
textOutput.ensureTrailingNewline();
|
||||
if (streamFormatter) {
|
||||
const displayText = getTextContent(event.displayContent);
|
||||
const errorMsg = getTextContent(event.content) ?? 'Tool error';
|
||||
streamFormatter.emitEvent({
|
||||
type: JsonStreamEventType.TOOL_RESULT,
|
||||
timestamp: new Date().toISOString(),
|
||||
tool_id: event.requestId,
|
||||
status: event.isError ? 'error' : 'success',
|
||||
output: displayText,
|
||||
error: event.isError
|
||||
? {
|
||||
type:
|
||||
typeof event.data?.['errorType'] === 'string'
|
||||
? event.data['errorType']
|
||||
: 'TOOL_EXECUTION_ERROR',
|
||||
message: errorMsg,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
if (event.isError) {
|
||||
const displayText = getTextContent(event.displayContent);
|
||||
const errorMsg = getTextContent(event.content) ?? 'Tool error';
|
||||
|
||||
if (event.data?.['errorType'] === ToolErrorType.STOP_EXECUTION) {
|
||||
if (
|
||||
config.getOutputFormat() === OutputFormat.JSON &&
|
||||
!responseText &&
|
||||
preToolResponseText
|
||||
) {
|
||||
responseText = preToolResponseText;
|
||||
}
|
||||
const stopMessage = `Agent execution stopped: ${errorMsg}`;
|
||||
if (config.getOutputFormat() === OutputFormat.TEXT) {
|
||||
process.stderr.write(`${stopMessage}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
if (event.data?.['errorType'] === ToolErrorType.NO_SPACE_LEFT) {
|
||||
throw new FatalToolExecutionError(
|
||||
'Error executing tool ' +
|
||||
event.name +
|
||||
': ' +
|
||||
(displayText || errorMsg),
|
||||
);
|
||||
}
|
||||
handleToolError(
|
||||
event.name,
|
||||
new Error(errorMsg),
|
||||
config,
|
||||
typeof event.data?.['errorType'] === 'string'
|
||||
? event.data['errorType']
|
||||
: undefined,
|
||||
displayText,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'error': {
|
||||
if (event.fatal) {
|
||||
throw reconstructFatalError(event);
|
||||
}
|
||||
|
||||
const errorCode = event._meta?.['code'];
|
||||
|
||||
if (errorCode === 'AGENT_EXECUTION_BLOCKED') {
|
||||
if (config.getOutputFormat() === OutputFormat.TEXT) {
|
||||
process.stderr.write(`[WARNING] ${event.message}\n`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const severity =
|
||||
event.status === 'RESOURCE_EXHAUSTED' ? 'error' : 'warning';
|
||||
if (config.getOutputFormat() === OutputFormat.TEXT) {
|
||||
process.stderr.write(`[WARNING] ${event.message}\n`);
|
||||
}
|
||||
if (streamFormatter) {
|
||||
streamFormatter.emitEvent({
|
||||
type: JsonStreamEventType.ERROR,
|
||||
timestamp: new Date().toISOString(),
|
||||
severity,
|
||||
message: event.message,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'agent_end': {
|
||||
if (event.reason === 'aborted') {
|
||||
throw new FatalCancellationError('Operation cancelled.');
|
||||
} else if (event.reason === 'max_turns') {
|
||||
const isConfiguredTurnLimit =
|
||||
typeof event.data?.['maxTurns'] === 'number' ||
|
||||
typeof event.data?.['turnCount'] === 'number';
|
||||
|
||||
if (isConfiguredTurnLimit) {
|
||||
throw new FatalTurnLimitedError(
|
||||
'Reached max session turns for this session. Increase the number of turns by specifying maxSessionTurns in settings.json.',
|
||||
);
|
||||
} else if (streamFormatter) {
|
||||
streamFormatter.emitEvent({
|
||||
type: JsonStreamEventType.ERROR,
|
||||
timestamp: new Date().toISOString(),
|
||||
severity: 'error',
|
||||
message: 'Maximum session turns exceeded',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const stopMessage =
|
||||
typeof event.data?.['message'] === 'string'
|
||||
? event.data['message']
|
||||
: '';
|
||||
if (stopMessage && config.getOutputFormat() === OutputFormat.TEXT) {
|
||||
process.stderr.write(`Agent execution stopped: ${stopMessage}\n`);
|
||||
}
|
||||
|
||||
emitFinalSuccessResult();
|
||||
streamEnded = true;
|
||||
break;
|
||||
}
|
||||
case 'initialize':
|
||||
case 'session_update':
|
||||
case 'agent_start':
|
||||
case 'tool_update':
|
||||
case 'elicitation_request':
|
||||
case 'elicitation_response':
|
||||
case 'usage':
|
||||
case 'custom':
|
||||
// Explicitly ignore these non-interactive events
|
||||
break;
|
||||
default:
|
||||
event satisfies never;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
errorToHandle = error;
|
||||
} finally {
|
||||
// Cleanup stdin cancellation before other cleanup
|
||||
cleanupStdinCancellation();
|
||||
abortController.signal.removeEventListener('abort', abortSession);
|
||||
|
||||
consolePatcher.cleanup();
|
||||
coreEvents.off(CoreEvent.UserFeedback, handleUserFeedback);
|
||||
}
|
||||
|
||||
if (errorToHandle) {
|
||||
handleError(errorToHandle, config);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -176,6 +176,8 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
|
||||
getHasAccessToPreviewModel: vi.fn().mockReturnValue(false),
|
||||
validatePathAccess: vi.fn().mockReturnValue(null),
|
||||
getUseAlternateBuffer: vi.fn().mockReturnValue(false),
|
||||
getUseTerminalBuffer: vi.fn().mockReturnValue(false),
|
||||
getUseRenderProcess: vi.fn().mockReturnValue(false),
|
||||
...overrides,
|
||||
}) as unknown as Config;
|
||||
|
||||
|
||||
@@ -223,7 +223,7 @@ class XtermStdout extends EventEmitter {
|
||||
this.once('render', resolve),
|
||||
);
|
||||
const timeoutPromise = new Promise((resolve) =>
|
||||
setTimeout(resolve, 50),
|
||||
setTimeout(resolve, 1000),
|
||||
);
|
||||
await Promise.race([renderPromise, timeoutPromise]);
|
||||
}
|
||||
@@ -254,7 +254,12 @@ class XtermStdout extends EventEmitter {
|
||||
|
||||
const isMatch = () => {
|
||||
if (expectedFrame === '...') {
|
||||
return currentFrame !== '';
|
||||
// '...' is our fallback when output isn't in metrics, meaning Ink rendered *something*
|
||||
// but we don't know what it is. If terminal has content, we consider it a match.
|
||||
// However, if the component rendered null, both would be empty, but our fallback
|
||||
// made expectedFrame '...'. In that case, we can't easily know if it's ready,
|
||||
// but we can assume if there are no pending writes, it's ready.
|
||||
return currentFrame !== '' || this.pendingWrites === 0;
|
||||
}
|
||||
|
||||
// If Ink expects nothing (no new static content and no dynamic output),
|
||||
|
||||
@@ -346,6 +346,7 @@ describe('AppContainer State Management', () => {
|
||||
// Initialize mock stdout for terminal title tests
|
||||
|
||||
mocks.mockStdout.write.mockClear();
|
||||
(disableMouseEvents as import('vitest').Mock).mockClear();
|
||||
|
||||
capturedUIState = null!;
|
||||
|
||||
@@ -470,6 +471,7 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
// Mock Config
|
||||
mockConfig = makeFakeConfig();
|
||||
vi.spyOn(mockConfig, 'getUseRenderProcess').mockReturnValue(false);
|
||||
|
||||
// Mock config's getTargetDir to return consistent workspace directory
|
||||
vi.spyOn(mockConfig, 'getTargetDir').mockReturnValue('/test/workspace');
|
||||
@@ -1356,6 +1358,7 @@ describe('AppContainer State Management', () => {
|
||||
beforeEach(() => {
|
||||
// Reset mock stdout for each test
|
||||
mocks.mockStdout.write.mockClear();
|
||||
(disableMouseEvents as import('vitest').Mock).mockClear();
|
||||
});
|
||||
|
||||
it('verifies useStdout is mocked', async () => {
|
||||
@@ -2459,7 +2462,7 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Copy Mode (CTRL+S)', () => {
|
||||
describe('Copy Mode (F9)', () => {
|
||||
let rerender: () => void;
|
||||
let unmount: () => void;
|
||||
let stdin: Awaited<ReturnType<typeof render>>['stdin'];
|
||||
@@ -2468,6 +2471,8 @@ describe('AppContainer State Management', () => {
|
||||
isAlternateMode = false,
|
||||
childHandler?: Mock,
|
||||
) => {
|
||||
vi.spyOn(mockConfig, 'getUseTerminalBuffer').mockReturnValue(false);
|
||||
|
||||
vi.spyOn(mockConfig, 'getUseAlternateBuffer').mockReturnValue(
|
||||
isAlternateMode,
|
||||
);
|
||||
@@ -2512,6 +2517,8 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.mockStdout.write.mockClear();
|
||||
(disableMouseEvents as import('vitest').Mock).mockClear();
|
||||
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
@@ -2532,12 +2539,13 @@ describe('AppContainer State Management', () => {
|
||||
modeName: 'Alternate Buffer Mode',
|
||||
},
|
||||
])('$modeName', ({ isAlternateMode, shouldEnable }) => {
|
||||
it(`should ${shouldEnable ? 'toggle' : 'NOT toggle'} mouse off when Ctrl+S is pressed`, async () => {
|
||||
it(`should ${shouldEnable ? 'toggle' : 'NOT toggle'} mouse off when F9 is pressed`, async () => {
|
||||
await setupCopyModeTest(isAlternateMode);
|
||||
mocks.mockStdout.write.mockClear(); // Clear initial enable call
|
||||
(disableMouseEvents as import('vitest').Mock).mockClear();
|
||||
|
||||
act(() => {
|
||||
stdin.write('\x13'); // Ctrl+S
|
||||
stdin.write('\x1b[20~'); // F9
|
||||
});
|
||||
rerender();
|
||||
|
||||
@@ -2550,13 +2558,13 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
if (shouldEnable) {
|
||||
it('should toggle mouse back on when Ctrl+S is pressed again', async () => {
|
||||
it('should toggle mouse back on when F9 is pressed again', async () => {
|
||||
await setupCopyModeTest(isAlternateMode);
|
||||
(writeToStdout as Mock).mockClear();
|
||||
|
||||
// Turn it on (disable mouse)
|
||||
act(() => {
|
||||
stdin.write('\x13'); // Ctrl+S
|
||||
stdin.write('\x1b[20~'); // F9
|
||||
});
|
||||
rerender();
|
||||
expect(disableMouseEvents).toHaveBeenCalled();
|
||||
@@ -2576,7 +2584,7 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
// Enter copy mode
|
||||
act(() => {
|
||||
stdin.write('\x13'); // Ctrl+S
|
||||
stdin.write('\x1b[20~'); // F9
|
||||
});
|
||||
rerender();
|
||||
|
||||
@@ -2656,7 +2664,7 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
// 2. Enter copy mode
|
||||
act(() => {
|
||||
stdin.write('\x13'); // Ctrl+S
|
||||
stdin.write('\x1b[20~'); // F9
|
||||
});
|
||||
rerender();
|
||||
|
||||
@@ -3093,6 +3101,7 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
// Clear previous calls
|
||||
mocks.mockStdout.write.mockClear();
|
||||
(disableMouseEvents as import('vitest').Mock).mockClear();
|
||||
|
||||
const { unmount } = await act(async () => renderAppContainer());
|
||||
|
||||
@@ -3135,16 +3144,13 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
// Reset mock stdout to clear any initial writes
|
||||
mocks.mockStdout.write.mockClear();
|
||||
(disableMouseEvents as import('vitest').Mock).mockClear();
|
||||
|
||||
// Submit
|
||||
await act(async () => capturedUIActions.handleFinalSubmit('test prompt'));
|
||||
|
||||
// Should be reset
|
||||
expect(capturedUIState.constrainHeight).toBe(true);
|
||||
// Should refresh static (which clears terminal in non-alternate buffer)
|
||||
expect(mocks.mockStdout.write).toHaveBeenCalledWith(
|
||||
ansiEscapes.clearTerminal,
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -3154,6 +3160,8 @@ describe('AppContainer State Management', () => {
|
||||
);
|
||||
vi.mocked(checkPermissions).mockResolvedValue([]);
|
||||
|
||||
vi.spyOn(mockConfig, 'getUseTerminalBuffer').mockReturnValue(false);
|
||||
|
||||
vi.spyOn(mockConfig, 'getUseAlternateBuffer').mockReturnValue(true);
|
||||
|
||||
const { unmount } = await act(async () =>
|
||||
@@ -3170,6 +3178,7 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
// Reset mock stdout
|
||||
mocks.mockStdout.write.mockClear();
|
||||
(disableMouseEvents as import('vitest').Mock).mockClear();
|
||||
|
||||
// Submit
|
||||
await act(async () => capturedUIActions.handleFinalSubmit('test prompt'));
|
||||
@@ -3403,6 +3412,8 @@ describe('AppContainer State Management', () => {
|
||||
ui: { useAlternateBuffer: true },
|
||||
});
|
||||
|
||||
vi.spyOn(mockConfig, 'getUseTerminalBuffer').mockReturnValue(false);
|
||||
|
||||
vi.spyOn(mockConfig, 'getUseAlternateBuffer').mockReturnValue(true);
|
||||
|
||||
const { unmount } = await act(async () =>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
useEffect,
|
||||
useRef,
|
||||
useLayoutEffect,
|
||||
useContext,
|
||||
} from 'react';
|
||||
import {
|
||||
type DOMElement,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
useStdout,
|
||||
useStdin,
|
||||
type AppProps,
|
||||
AppContext as InkAppContext,
|
||||
} from 'ink';
|
||||
import { App } from './App.js';
|
||||
import { AppContext } from './contexts/AppContext.js';
|
||||
@@ -38,6 +40,8 @@ import {
|
||||
import { checkPermissions } from './hooks/atCommandProcessor.js';
|
||||
import { MessageType, StreamingState } from './types.js';
|
||||
import { ToolActionsProvider } from './contexts/ToolActionsContext.js';
|
||||
import { MouseProvider } from './contexts/MouseContext.js';
|
||||
import { ScrollProvider } from './contexts/ScrollProvider.js';
|
||||
import {
|
||||
type StartupWarning,
|
||||
type EditorType,
|
||||
@@ -210,12 +214,30 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
const { reset } = useOverflowActions()!;
|
||||
const notificationsEnabled = isNotificationsEnabled(settings);
|
||||
|
||||
const { setOptions, dumpCurrentFrame, startRecording, stopRecording } =
|
||||
useContext(InkAppContext);
|
||||
const recordingFilenameRef = useRef<string | null>(null);
|
||||
const historyManager = useHistory({
|
||||
chatRecordingService: config.getGeminiClient()?.getChatRecordingService(),
|
||||
});
|
||||
|
||||
useMemoryMonitor(historyManager);
|
||||
const isAlternateBuffer = config.getUseAlternateBuffer();
|
||||
const [mouseMode, setMouseMode] = useState(() =>
|
||||
config.getUseAlternateBuffer(),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setOptions({
|
||||
stickyHeadersInBackbuffer: mouseMode,
|
||||
});
|
||||
if (mouseMode) {
|
||||
enableMouseEvents();
|
||||
} else {
|
||||
disableMouseEvents();
|
||||
}
|
||||
}, [mouseMode, setOptions]);
|
||||
|
||||
const [corgiMode, setCorgiMode] = useState(false);
|
||||
const [forceRerenderKey, setForceRerenderKey] = useState(0);
|
||||
const [debugMessage, setDebugMessage] = useState<string>('');
|
||||
@@ -621,11 +643,11 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
});
|
||||
|
||||
const refreshStatic = useCallback(() => {
|
||||
if (!isAlternateBuffer) {
|
||||
if (!isAlternateBuffer && !config.getUseTerminalBuffer()) {
|
||||
stdout.write(ansiEscapes.clearTerminal);
|
||||
setHistoryRemountKey((prev) => prev + 1);
|
||||
}
|
||||
setHistoryRemountKey((prev) => prev + 1);
|
||||
}, [setHistoryRemountKey, isAlternateBuffer, stdout]);
|
||||
}, [setHistoryRemountKey, isAlternateBuffer, stdout, config]);
|
||||
|
||||
const shouldUseAlternateScreen = shouldEnterAlternateScreen(
|
||||
isAlternateBuffer,
|
||||
@@ -1034,7 +1056,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
let fileCount: number;
|
||||
|
||||
if (config.isJitContextEnabled()) {
|
||||
await config.getContextManager()?.refresh();
|
||||
await config.getMemoryContextManager()?.refresh();
|
||||
config.updateSystemInstructionIfInitialized();
|
||||
flattenedMemory = flattenMemory(config.getUserMemory());
|
||||
fileCount = config.getGeminiMdFileCount();
|
||||
@@ -1430,10 +1452,17 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
(streamingState === StreamingState.Idle ||
|
||||
streamingState === StreamingState.Responding ||
|
||||
streamingState === StreamingState.WaitingForConfirmation) &&
|
||||
!proQuotaRequest &&
|
||||
!copyModeEnabled;
|
||||
!proQuotaRequest;
|
||||
|
||||
const observerRef = useRef<ResizeObserver | null>(null);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
observerRef.current?.disconnect();
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const [controlsHeight, setControlsHeight] = useState(0);
|
||||
const [lastNonCopyControlsHeight, setLastNonCopyControlsHeight] = useState(0);
|
||||
|
||||
@@ -1732,6 +1761,14 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
setShortcutsHelpVisible(false);
|
||||
}
|
||||
|
||||
if (keyMatchers[Command.TOGGLE_MOUSE_MODE](key)) {
|
||||
setMouseMode((prev) => !prev);
|
||||
if (mouseMode && !isAlternateBuffer) {
|
||||
appEvents.emit(AppEvent.ScrollToBottom);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isAlternateBuffer && keyMatchers[Command.TOGGLE_COPY_MODE](key)) {
|
||||
setCopyModeEnabled(true);
|
||||
disableMouseEvents();
|
||||
@@ -1754,6 +1791,32 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
return true;
|
||||
} else if (keyMatchers[Command.SUSPEND_APP](key)) {
|
||||
handleSuspend();
|
||||
} else if (keyMatchers[Command.DUMP_FRAME](key)) {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const filename = `snapshot-${timestamp}.json`;
|
||||
if (dumpCurrentFrame) {
|
||||
dumpCurrentFrame(filename);
|
||||
debugLogger.log(`Dumped frame to: ${filename}`);
|
||||
}
|
||||
return true;
|
||||
} else if (keyMatchers[Command.START_RECORDING](key)) {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const filename = `recording-${timestamp}.json`;
|
||||
if (startRecording) {
|
||||
startRecording(filename);
|
||||
recordingFilenameRef.current = filename;
|
||||
debugLogger.log(`Started recording to: ${filename}`);
|
||||
}
|
||||
return true;
|
||||
} else if (keyMatchers[Command.STOP_RECORDING](key)) {
|
||||
if (stopRecording) {
|
||||
stopRecording();
|
||||
debugLogger.log(
|
||||
`Stopped recording, saved to: ${recordingFilenameRef.current ?? 'unknown'}`,
|
||||
);
|
||||
recordingFilenameRef.current = null;
|
||||
}
|
||||
return true;
|
||||
} else if (
|
||||
keyMatchers[Command.TOGGLE_COPY_MODE](key) &&
|
||||
!isAlternateBuffer
|
||||
@@ -1940,6 +2003,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
historyManager.history,
|
||||
pendingHistoryItems,
|
||||
toggleAllExpansion,
|
||||
dumpCurrentFrame,
|
||||
startRecording,
|
||||
stopRecording,
|
||||
mouseMode,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1959,7 +2026,9 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
}
|
||||
|
||||
setCopyModeEnabled(false);
|
||||
enableMouseEvents();
|
||||
if (mouseMode) {
|
||||
enableMouseEvents();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
@@ -2276,6 +2345,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
editorError,
|
||||
isEditorDialogOpen,
|
||||
showPrivacyNotice,
|
||||
mouseMode,
|
||||
corgiMode,
|
||||
debugMessage,
|
||||
quittingMessages,
|
||||
@@ -2402,6 +2472,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
editorError,
|
||||
isEditorDialogOpen,
|
||||
showPrivacyNotice,
|
||||
mouseMode,
|
||||
corgiMode,
|
||||
debugMessage,
|
||||
quittingMessages,
|
||||
@@ -2702,7 +2773,11 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
toggleAllExpansion={toggleAllExpansion}
|
||||
>
|
||||
<ShellFocusContext.Provider value={isFocused}>
|
||||
<App key={`app-${forceRerenderKey}`} />
|
||||
<MouseProvider mouseEventsEnabled={mouseMode}>
|
||||
<ScrollProvider>
|
||||
<App key={`app-${forceRerenderKey}`} />
|
||||
</ScrollProvider>
|
||||
</MouseProvider>
|
||||
</ShellFocusContext.Provider>
|
||||
</ToolActionsProvider>
|
||||
</AppContext.Provider>
|
||||
|
||||
@@ -55,12 +55,6 @@ Footer
|
||||
Gemini CLI v1.2.3
|
||||
|
||||
|
||||
|
||||
Tips for getting started:
|
||||
1. Create GEMINI.md files to customize your interactions
|
||||
2. /help for more information
|
||||
3. Ask coding questions, edit code or run commands
|
||||
4. Be specific for the best results
|
||||
Composer
|
||||
"
|
||||
`;
|
||||
@@ -130,13 +124,14 @@ HistoryItemDisplay
|
||||
│ │
|
||||
│ ? ls list directory │
|
||||
│ │
|
||||
│ ls │
|
||||
│ Allow execution of: 'ls'? │
|
||||
│ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │
|
||||
│ │ ls │ │
|
||||
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
|
||||
│ Allow execution of [ls]? │
|
||||
│ │
|
||||
│ ● 1. Allow once │
|
||||
│ 2. Allow for this session │
|
||||
│ 3. No, suggest changes (esc) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
|
||||
|
||||
@@ -144,7 +139,6 @@ HistoryItemDisplay
|
||||
|
||||
|
||||
|
||||
|
||||
Notifications
|
||||
|
||||
Composer
|
||||
|
||||
+282
-254
@@ -4,263 +4,291 @@
|
||||
</style>
|
||||
<rect width="920" height="666" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<rect x="0" y="0" width="900" height="17" fill="#141414" />
|
||||
<text x="0" y="2" fill="#000000" textLength="900" lengthAdjust="spacingAndGlyphs">▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀</text>
|
||||
<rect x="0" y="17" width="9" height="17" fill="#141414" />
|
||||
<rect x="9" y="17" width="18" height="17" fill="#141414" />
|
||||
<text x="9" y="19" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">> </text>
|
||||
<rect x="27" y="17" width="324" height="17" fill="#141414" />
|
||||
<text x="27" y="19" fill="#ffffff" textLength="324" lengthAdjust="spacingAndGlyphs">Can you edit InputPrompt.tsx for me?</text>
|
||||
<rect x="351" y="17" width="549" height="17" fill="#141414" />
|
||||
<rect x="0" y="34" width="900" height="17" fill="#141414" />
|
||||
<text x="0" y="36" fill="#000000" textLength="900" lengthAdjust="spacingAndGlyphs">▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄</text>
|
||||
<text x="0" y="53" fill="#ffffaf" textLength="891" lengthAdjust="spacingAndGlyphs">╭─────────────────────────────────────────────────────────────────────────────────────────────────╮</text>
|
||||
<text x="0" y="70" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="70" fill="#ffffaf" textLength="135" lengthAdjust="spacingAndGlyphs" font-weight="bold">Action Required</text>
|
||||
<text x="882" y="70" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="87" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="882" y="87" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="104" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="104" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">?</text>
|
||||
<text x="45" y="104" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs" font-weight="bold">Edit</text>
|
||||
<text x="90" y="104" fill="#afafaf" textLength="774" lengthAdjust="spacingAndGlyphs">packages/.../InputPrompt.tsx: return kittyProtocolSupporte... => return kittyProto</text>
|
||||
<text x="864" y="104" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">… </text>
|
||||
<text x="882" y="104" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="121" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="882" y="121" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="138" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="138" fill="#afafaf" textLength="414" lengthAdjust="spacingAndGlyphs">... first 44 lines hidden (Ctrl+O to show) ...</text>
|
||||
<text x="882" y="138" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="155" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="155" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">45</text>
|
||||
<text x="63" y="155" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="155" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line45</text>
|
||||
<text x="171" y="155" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="155" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="155" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="155" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="172" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="172" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">46</text>
|
||||
<text x="63" y="172" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="172" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line46</text>
|
||||
<text x="171" y="172" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="172" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="172" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="172" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="189" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="189" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">47</text>
|
||||
<text x="63" y="189" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="189" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line47</text>
|
||||
<text x="171" y="189" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="189" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="189" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="189" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="189" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="206" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="206" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">48</text>
|
||||
<text x="63" y="206" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="206" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line48</text>
|
||||
<text x="171" y="206" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="206" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="206" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="206" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="206" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="223" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="223" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">49</text>
|
||||
<text x="63" y="223" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="223" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line49</text>
|
||||
<text x="171" y="223" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="223" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="223" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="223" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="223" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="240" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="240" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">50</text>
|
||||
<text x="63" y="240" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="240" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line50</text>
|
||||
<text x="171" y="240" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="240" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="240" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="240" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="240" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="257" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="257" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">51</text>
|
||||
<text x="63" y="257" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="257" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line51</text>
|
||||
<text x="171" y="257" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="257" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="257" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="257" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="257" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="274" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="274" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">52</text>
|
||||
<text x="63" y="274" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="274" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line52</text>
|
||||
<text x="171" y="274" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="274" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="274" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="274" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="274" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="291" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="291" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">53</text>
|
||||
<text x="63" y="291" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="291" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line53</text>
|
||||
<text x="171" y="291" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="291" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="291" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="291" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="291" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="308" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="308" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">54</text>
|
||||
<text x="63" y="308" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="308" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line54</text>
|
||||
<text x="171" y="308" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="308" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="308" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="308" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="308" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="325" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="325" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">55</text>
|
||||
<text x="63" y="325" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="325" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line55</text>
|
||||
<text x="171" y="325" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="325" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="325" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="325" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="325" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="342" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="342" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">56</text>
|
||||
<text x="63" y="342" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="342" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line56</text>
|
||||
<text x="171" y="342" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="342" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="342" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="342" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="342" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="359" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="359" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">57</text>
|
||||
<text x="63" y="359" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="359" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line57</text>
|
||||
<text x="171" y="359" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="359" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="359" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="359" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="359" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="376" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="376" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">58</text>
|
||||
<text x="63" y="376" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="376" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line58</text>
|
||||
<text x="171" y="376" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="376" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="376" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="376" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="376" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="393" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="393" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">59</text>
|
||||
<text x="63" y="393" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="393" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line59</text>
|
||||
<text x="171" y="393" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="393" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="393" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="393" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="393" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="410" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="410" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">60</text>
|
||||
<text x="63" y="410" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="117" y="410" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line60</text>
|
||||
<text x="171" y="410" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="198" y="410" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="234" y="410" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="410" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="410" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="427" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="18" y="425" width="18" height="17" fill="#5f0000" />
|
||||
<text x="18" y="427" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">61</text>
|
||||
<rect x="36" y="425" width="9" height="17" fill="#5f0000" />
|
||||
<rect x="45" y="425" width="9" height="17" fill="#5f0000" />
|
||||
<text x="45" y="427" fill="#ff87af" textLength="9" lengthAdjust="spacingAndGlyphs">-</text>
|
||||
<rect x="0" y="0" width="9" height="17" fill="#141414" />
|
||||
<rect x="9" y="0" width="18" height="17" fill="#141414" />
|
||||
<text x="9" y="2" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">> </text>
|
||||
<rect x="27" y="0" width="324" height="17" fill="#141414" />
|
||||
<text x="27" y="2" fill="#ffffff" textLength="324" lengthAdjust="spacingAndGlyphs">Can you edit InputPrompt.tsx for me?</text>
|
||||
<rect x="351" y="0" width="549" height="17" fill="#141414" />
|
||||
<rect x="0" y="17" width="900" height="17" fill="#141414" />
|
||||
<text x="0" y="19" fill="#000000" textLength="900" lengthAdjust="spacingAndGlyphs">▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄</text>
|
||||
<text x="0" y="53" fill="#333333" textLength="891" lengthAdjust="spacingAndGlyphs">╭─────────────────────────────────────────────────────────────────────────────────────────────────╮</text>
|
||||
<text x="0" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="70" fill="#ffffaf" textLength="54" lengthAdjust="spacingAndGlyphs" font-weight="bold">? Edit</text>
|
||||
<text x="882" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="87" fill="#333333" textLength="855" lengthAdjust="spacingAndGlyphs">╭─────────────────────────────────────────────────────────────────────────────────────────────╮</text>
|
||||
<text x="882" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="104" fill="#afafaf" textLength="414" lengthAdjust="spacingAndGlyphs">... first 42 lines hidden (Ctrl+O to show) ...</text>
|
||||
<text x="864" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="882" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="121" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">43</text>
|
||||
<text x="81" y="121" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="135" y="121" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line43</text>
|
||||
<text x="189" y="121" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="216" y="121" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="252" y="121" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="864" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="882" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="138" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">44</text>
|
||||
<text x="81" y="138" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="135" y="138" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line44</text>
|
||||
<text x="189" y="138" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="216" y="138" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="252" y="138" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="864" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="882" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="155" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="155" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="155" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">45</text>
|
||||
<text x="81" y="155" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="135" y="155" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line45</text>
|
||||
<text x="189" y="155" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="216" y="155" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="252" y="155" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="864" y="155" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="882" y="155" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="172" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="172" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="172" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">46</text>
|
||||
<text x="81" y="172" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="135" y="172" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line46</text>
|
||||
<text x="189" y="172" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="216" y="172" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="252" y="172" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="864" y="172" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="882" y="172" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="0" y="189" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="189" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="189" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">47</text>
|
||||
<text x="81" y="189" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="135" y="189" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line47</text>
|
||||
<text x="189" y="189" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="216" y="189" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="252" y="189" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="864" y="189" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="882" y="189" fill="#333333" textLength="18" lengthAdjust="spacingAndGlyphs">│▄</text>
|
||||
<text x="0" y="206" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="206" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="206" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">48</text>
|
||||
<text x="81" y="206" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="135" y="206" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line48</text>
|
||||
<text x="189" y="206" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="216" y="206" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="252" y="206" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="864" y="206" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="882" y="206" fill="#333333" textLength="18" lengthAdjust="spacingAndGlyphs">│█</text>
|
||||
<text x="0" y="223" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="223" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="223" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">49</text>
|
||||
<text x="81" y="223" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="135" y="223" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line49</text>
|
||||
<text x="189" y="223" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="216" y="223" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="252" y="223" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="864" y="223" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="882" y="223" fill="#333333" textLength="18" lengthAdjust="spacingAndGlyphs">│█</text>
|
||||
<text x="0" y="240" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="240" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="240" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">50</text>
|
||||
<text x="81" y="240" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="135" y="240" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line50</text>
|
||||
<text x="189" y="240" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="216" y="240" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="252" y="240" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="864" y="240" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="882" y="240" fill="#333333" textLength="18" lengthAdjust="spacingAndGlyphs">│█</text>
|
||||
<text x="0" y="257" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="257" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="257" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">51</text>
|
||||
<text x="81" y="257" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="135" y="257" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line51</text>
|
||||
<text x="189" y="257" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="216" y="257" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="252" y="257" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="864" y="257" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="882" y="257" fill="#333333" textLength="18" lengthAdjust="spacingAndGlyphs">│█</text>
|
||||
<text x="0" y="274" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="274" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="274" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">52</text>
|
||||
<text x="81" y="274" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="135" y="274" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line52</text>
|
||||
<text x="189" y="274" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="216" y="274" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="252" y="274" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="864" y="274" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="882" y="274" fill="#333333" textLength="18" lengthAdjust="spacingAndGlyphs">│█</text>
|
||||
<text x="0" y="291" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="291" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="291" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">53</text>
|
||||
<text x="81" y="291" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="135" y="291" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line53</text>
|
||||
<text x="189" y="291" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="216" y="291" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="252" y="291" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="864" y="291" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="882" y="291" fill="#333333" textLength="18" lengthAdjust="spacingAndGlyphs">│█</text>
|
||||
<text x="0" y="308" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="308" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="308" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">54</text>
|
||||
<text x="81" y="308" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="135" y="308" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line54</text>
|
||||
<text x="189" y="308" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="216" y="308" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="252" y="308" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="864" y="308" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="882" y="308" fill="#333333" textLength="18" lengthAdjust="spacingAndGlyphs">│█</text>
|
||||
<text x="0" y="325" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="325" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="325" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">55</text>
|
||||
<text x="81" y="325" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="135" y="325" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line55</text>
|
||||
<text x="189" y="325" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="216" y="325" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="252" y="325" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="864" y="325" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="882" y="325" fill="#333333" textLength="18" lengthAdjust="spacingAndGlyphs">│█</text>
|
||||
<text x="0" y="342" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="342" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="342" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">56</text>
|
||||
<text x="81" y="342" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="135" y="342" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line56</text>
|
||||
<text x="189" y="342" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="216" y="342" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="252" y="342" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="864" y="342" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="882" y="342" fill="#333333" textLength="18" lengthAdjust="spacingAndGlyphs">│█</text>
|
||||
<text x="0" y="359" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="359" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="359" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">57</text>
|
||||
<text x="81" y="359" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="135" y="359" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line57</text>
|
||||
<text x="189" y="359" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="216" y="359" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="252" y="359" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="864" y="359" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="882" y="359" fill="#333333" textLength="18" lengthAdjust="spacingAndGlyphs">│█</text>
|
||||
<text x="0" y="376" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="376" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="376" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">58</text>
|
||||
<text x="81" y="376" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="135" y="376" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line58</text>
|
||||
<text x="189" y="376" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="216" y="376" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="252" y="376" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="864" y="376" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="882" y="376" fill="#333333" textLength="18" lengthAdjust="spacingAndGlyphs">│█</text>
|
||||
<text x="0" y="393" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="393" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="393" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">59</text>
|
||||
<text x="81" y="393" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="135" y="393" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line59</text>
|
||||
<text x="189" y="393" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="216" y="393" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="252" y="393" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="864" y="393" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="882" y="393" fill="#333333" textLength="18" lengthAdjust="spacingAndGlyphs">│█</text>
|
||||
<text x="0" y="410" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="410" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="410" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">60</text>
|
||||
<text x="81" y="410" fill="#e5e5e5" textLength="54" lengthAdjust="spacingAndGlyphs">const </text>
|
||||
<text x="135" y="410" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">line60</text>
|
||||
<text x="189" y="410" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs"> = </text>
|
||||
<text x="216" y="410" fill="#0000ee" textLength="36" lengthAdjust="spacingAndGlyphs">true</text>
|
||||
<text x="252" y="410" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="864" y="410" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="882" y="410" fill="#333333" textLength="18" lengthAdjust="spacingAndGlyphs">│█</text>
|
||||
<text x="0" y="427" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="427" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="36" y="425" width="18" height="17" fill="#5f0000" />
|
||||
<text x="36" y="427" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">61</text>
|
||||
<rect x="54" y="425" width="9" height="17" fill="#5f0000" />
|
||||
<rect x="63" y="425" width="9" height="17" fill="#5f0000" />
|
||||
<rect x="72" y="425" width="54" height="17" fill="#5f0000" />
|
||||
<text x="72" y="427" fill="#0000ee" textLength="54" lengthAdjust="spacingAndGlyphs">return</text>
|
||||
<rect x="126" y="425" width="234" height="17" fill="#5f0000" />
|
||||
<text x="126" y="427" fill="#e5e5e5" textLength="234" lengthAdjust="spacingAndGlyphs"> kittyProtocolSupporte...;</text>
|
||||
<text x="882" y="427" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="427" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="444" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="18" y="442" width="18" height="17" fill="#005f00" />
|
||||
<text x="18" y="444" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">61</text>
|
||||
<rect x="36" y="442" width="9" height="17" fill="#005f00" />
|
||||
<rect x="45" y="442" width="9" height="17" fill="#005f00" />
|
||||
<text x="45" y="444" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs">+</text>
|
||||
<text x="63" y="427" fill="#ff87af" textLength="9" lengthAdjust="spacingAndGlyphs">-</text>
|
||||
<rect x="72" y="425" width="9" height="17" fill="#5f0000" />
|
||||
<rect x="81" y="425" width="9" height="17" fill="#5f0000" />
|
||||
<rect x="90" y="425" width="54" height="17" fill="#5f0000" />
|
||||
<text x="90" y="427" fill="#0000ee" textLength="54" lengthAdjust="spacingAndGlyphs">return</text>
|
||||
<rect x="144" y="425" width="234" height="17" fill="#5f0000" />
|
||||
<text x="144" y="427" fill="#e5e5e5" textLength="234" lengthAdjust="spacingAndGlyphs"> kittyProtocolSupporte...;</text>
|
||||
<text x="864" y="427" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="882" y="427" fill="#333333" textLength="18" lengthAdjust="spacingAndGlyphs">│█</text>
|
||||
<text x="0" y="444" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="444" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="36" y="442" width="18" height="17" fill="#005f00" />
|
||||
<text x="36" y="444" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">61</text>
|
||||
<rect x="54" y="442" width="9" height="17" fill="#005f00" />
|
||||
<rect x="63" y="442" width="9" height="17" fill="#005f00" />
|
||||
<rect x="72" y="442" width="54" height="17" fill="#005f00" />
|
||||
<text x="72" y="444" fill="#0000ee" textLength="54" lengthAdjust="spacingAndGlyphs">return</text>
|
||||
<rect x="126" y="442" width="234" height="17" fill="#005f00" />
|
||||
<text x="126" y="444" fill="#e5e5e5" textLength="234" lengthAdjust="spacingAndGlyphs"> kittyProtocolSupporte...;</text>
|
||||
<text x="882" y="444" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="444" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="461" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="461" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">62</text>
|
||||
<text x="63" y="461" fill="#e5e5e5" textLength="180" lengthAdjust="spacingAndGlyphs"> buffer: TextBuffer;</text>
|
||||
<text x="882" y="461" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="461" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="478" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="478" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">63</text>
|
||||
<text x="72" y="478" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs">onSubmit</text>
|
||||
<text x="144" y="478" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs">: (</text>
|
||||
<text x="171" y="478" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs">value</text>
|
||||
<text x="216" y="478" fill="#e5e5e5" textLength="18" lengthAdjust="spacingAndGlyphs">: </text>
|
||||
<text x="234" y="478" fill="#00cdcd" textLength="54" lengthAdjust="spacingAndGlyphs">string</text>
|
||||
<text x="288" y="478" fill="#e5e5e5" textLength="45" lengthAdjust="spacingAndGlyphs">) => </text>
|
||||
<text x="333" y="478" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">void</text>
|
||||
<text x="369" y="478" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="882" y="478" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="478" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="495" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="495" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Apply this change?</text>
|
||||
<text x="882" y="495" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="495" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="512" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="882" y="512" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="512" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="529" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="18" y="527" width="9" height="17" fill="#001a00" />
|
||||
<text x="18" y="529" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">●</text>
|
||||
<rect x="27" y="527" width="9" height="17" fill="#001a00" />
|
||||
<rect x="36" y="527" width="18" height="17" fill="#001a00" />
|
||||
<text x="36" y="529" fill="#00cd00" textLength="18" lengthAdjust="spacingAndGlyphs">1.</text>
|
||||
<rect x="54" y="527" width="9" height="17" fill="#001a00" />
|
||||
<rect x="63" y="527" width="90" height="17" fill="#001a00" />
|
||||
<text x="63" y="529" fill="#00cd00" textLength="90" lengthAdjust="spacingAndGlyphs">Allow once</text>
|
||||
<rect x="153" y="527" width="288" height="17" fill="#001a00" />
|
||||
<text x="882" y="529" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="529" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="546" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="546" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">2.</text>
|
||||
<text x="63" y="546" fill="#ffffff" textLength="198" lengthAdjust="spacingAndGlyphs">Allow for this session</text>
|
||||
<text x="882" y="546" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="546" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="563" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="563" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">3.</text>
|
||||
<text x="63" y="563" fill="#ffffff" textLength="378" lengthAdjust="spacingAndGlyphs">Allow for this file in all future sessions</text>
|
||||
<text x="882" y="563" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="563" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="580" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="580" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">4.</text>
|
||||
<text x="63" y="580" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">Modify with external editor</text>
|
||||
<text x="882" y="580" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="580" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="597" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="597" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">5.</text>
|
||||
<text x="63" y="597" fill="#ffffff" textLength="225" lengthAdjust="spacingAndGlyphs">No, suggest changes (esc)</text>
|
||||
<text x="882" y="597" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="614" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="882" y="614" fill="#ffffaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="891" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="0" y="631" fill="#ffffaf" textLength="891" lengthAdjust="spacingAndGlyphs">╰─────────────────────────────────────────────────────────────────────────────────────────────────╯</text>
|
||||
<text x="891" y="631" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">█</text>
|
||||
<text x="63" y="444" fill="#d7ffd7" textLength="9" lengthAdjust="spacingAndGlyphs">+</text>
|
||||
<rect x="72" y="442" width="9" height="17" fill="#005f00" />
|
||||
<rect x="81" y="442" width="9" height="17" fill="#005f00" />
|
||||
<rect x="90" y="442" width="54" height="17" fill="#005f00" />
|
||||
<text x="90" y="444" fill="#0000ee" textLength="54" lengthAdjust="spacingAndGlyphs">return</text>
|
||||
<rect x="144" y="442" width="234" height="17" fill="#005f00" />
|
||||
<text x="144" y="444" fill="#e5e5e5" textLength="234" lengthAdjust="spacingAndGlyphs"> kittyProtocolSupporte...;</text>
|
||||
<text x="864" y="444" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="882" y="444" fill="#333333" textLength="18" lengthAdjust="spacingAndGlyphs">│█</text>
|
||||
<text x="0" y="461" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="461" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="461" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">62</text>
|
||||
<text x="81" y="461" fill="#e5e5e5" textLength="180" lengthAdjust="spacingAndGlyphs"> buffer: TextBuffer;</text>
|
||||
<text x="864" y="461" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="882" y="461" fill="#333333" textLength="18" lengthAdjust="spacingAndGlyphs">│█</text>
|
||||
<text x="0" y="478" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="478" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="478" fill="#afafaf" textLength="18" lengthAdjust="spacingAndGlyphs">63</text>
|
||||
<text x="90" y="478" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs">onSubmit</text>
|
||||
<text x="162" y="478" fill="#e5e5e5" textLength="27" lengthAdjust="spacingAndGlyphs">: (</text>
|
||||
<text x="189" y="478" fill="#ffffff" textLength="45" lengthAdjust="spacingAndGlyphs">value</text>
|
||||
<text x="234" y="478" fill="#e5e5e5" textLength="18" lengthAdjust="spacingAndGlyphs">: </text>
|
||||
<text x="252" y="478" fill="#00cdcd" textLength="54" lengthAdjust="spacingAndGlyphs">string</text>
|
||||
<text x="306" y="478" fill="#e5e5e5" textLength="45" lengthAdjust="spacingAndGlyphs">) => </text>
|
||||
<text x="351" y="478" fill="#00cdcd" textLength="36" lengthAdjust="spacingAndGlyphs">void</text>
|
||||
<text x="387" y="478" fill="#e5e5e5" textLength="9" lengthAdjust="spacingAndGlyphs">;</text>
|
||||
<text x="864" y="478" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="882" y="478" fill="#333333" textLength="18" lengthAdjust="spacingAndGlyphs">│█</text>
|
||||
<text x="0" y="495" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="495" fill="#333333" textLength="855" lengthAdjust="spacingAndGlyphs">╰─────────────────────────────────────────────────────────────────────────────────────────────╯</text>
|
||||
<text x="882" y="495" fill="#333333" textLength="18" lengthAdjust="spacingAndGlyphs">│█</text>
|
||||
<text x="0" y="512" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="18" y="512" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs">Apply this change?</text>
|
||||
<text x="882" y="512" fill="#333333" textLength="18" lengthAdjust="spacingAndGlyphs">│█</text>
|
||||
<text x="0" y="529" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="882" y="529" fill="#333333" textLength="18" lengthAdjust="spacingAndGlyphs">│█</text>
|
||||
<text x="0" y="546" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<rect x="18" y="544" width="9" height="17" fill="#001a00" />
|
||||
<text x="18" y="546" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">●</text>
|
||||
<rect x="27" y="544" width="9" height="17" fill="#001a00" />
|
||||
<rect x="36" y="544" width="18" height="17" fill="#001a00" />
|
||||
<text x="36" y="546" fill="#00cd00" textLength="18" lengthAdjust="spacingAndGlyphs">1.</text>
|
||||
<rect x="54" y="544" width="9" height="17" fill="#001a00" />
|
||||
<rect x="63" y="544" width="90" height="17" fill="#001a00" />
|
||||
<text x="63" y="546" fill="#00cd00" textLength="90" lengthAdjust="spacingAndGlyphs">Allow once</text>
|
||||
<rect x="153" y="544" width="603" height="17" fill="#001a00" />
|
||||
<text x="882" y="546" fill="#333333" textLength="18" lengthAdjust="spacingAndGlyphs">│█</text>
|
||||
<text x="0" y="563" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="563" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">2.</text>
|
||||
<text x="63" y="563" fill="#ffffff" textLength="198" lengthAdjust="spacingAndGlyphs">Allow for this session</text>
|
||||
<text x="882" y="563" fill="#333333" textLength="18" lengthAdjust="spacingAndGlyphs">│█</text>
|
||||
<text x="0" y="580" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="580" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">3.</text>
|
||||
<text x="63" y="580" fill="#ffffff" textLength="387" lengthAdjust="spacingAndGlyphs">Allow for this file in all future sessions </text>
|
||||
<text x="450" y="580" fill="#afafaf" textLength="306" lengthAdjust="spacingAndGlyphs">~/.gemini/policies/auto-saved.toml</text>
|
||||
<text x="882" y="580" fill="#333333" textLength="18" lengthAdjust="spacingAndGlyphs">│█</text>
|
||||
<text x="0" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="597" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">4.</text>
|
||||
<text x="63" y="597" fill="#ffffff" textLength="243" lengthAdjust="spacingAndGlyphs">Modify with external editor</text>
|
||||
<text x="882" y="597" fill="#333333" textLength="18" lengthAdjust="spacingAndGlyphs">│█</text>
|
||||
<text x="0" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="36" y="614" fill="#ffffff" textLength="18" lengthAdjust="spacingAndGlyphs">5.</text>
|
||||
<text x="63" y="614" fill="#ffffff" textLength="225" lengthAdjust="spacingAndGlyphs">No, suggest changes (esc)</text>
|
||||
<text x="882" y="614" fill="#333333" textLength="18" lengthAdjust="spacingAndGlyphs">│█</text>
|
||||
<text x="0" y="631" fill="#333333" textLength="900" lengthAdjust="spacingAndGlyphs">╰─────────────────────────────────────────────────────────────────────────────────────────────────╯█</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 29 KiB |
@@ -1,43 +1,43 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`Full Terminal Tool Confirmation Snapshot > renders tool confirmation box in the frame of the entire terminal 1`] = `
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> Can you edit InputPrompt.tsx for me?
|
||||
" > Can you edit InputPrompt.tsx for me?
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
|
||||
╭─────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Action Required │
|
||||
│ │
|
||||
│ ? Edit packages/.../InputPrompt.tsx: return kittyProtocolSupporte... => return kittyProto… │
|
||||
│ │
|
||||
│ ... first 44 lines hidden (Ctrl+O to show) ... │
|
||||
│ 45 const line45 = true; │
|
||||
│ 46 const line46 = true; │
|
||||
│ 47 const line47 = true; │█
|
||||
│ 48 const line48 = true; │█
|
||||
│ 49 const line49 = true; │█
|
||||
│ 50 const line50 = true; │█
|
||||
│ 51 const line51 = true; │█
|
||||
│ 52 const line52 = true; │█
|
||||
│ 53 const line53 = true; │█
|
||||
│ 54 const line54 = true; │█
|
||||
│ 55 const line55 = true; │█
|
||||
│ 56 const line56 = true; │█
|
||||
│ 57 const line57 = true; │█
|
||||
│ 58 const line58 = true; │█
|
||||
│ 59 const line59 = true; │█
|
||||
│ 60 const line60 = true; │█
|
||||
│ 61 - return kittyProtocolSupporte...; │█
|
||||
│ 61 + return kittyProtocolSupporte...; │█
|
||||
│ 62 buffer: TextBuffer; │█
|
||||
│ 63 onSubmit: (value: string) => void; │█
|
||||
│ ? Edit │
|
||||
│ ╭─────────────────────────────────────────────────────────────────────────────────────────────╮ │
|
||||
│ │ ... first 42 lines hidden (Ctrl+O to show) ... │ │
|
||||
│ │ 43 const line43 = true; │ │
|
||||
│ │ 44 const line44 = true; │ │
|
||||
│ │ 45 const line45 = true; │ │
|
||||
│ │ 46 const line46 = true; │ │
|
||||
│ │ 47 const line47 = true; │ │▄
|
||||
│ │ 48 const line48 = true; │ │█
|
||||
│ │ 49 const line49 = true; │ │█
|
||||
│ │ 50 const line50 = true; │ │█
|
||||
│ │ 51 const line51 = true; │ │█
|
||||
│ │ 52 const line52 = true; │ │█
|
||||
│ │ 53 const line53 = true; │ │█
|
||||
│ │ 54 const line54 = true; │ │█
|
||||
│ │ 55 const line55 = true; │ │█
|
||||
│ │ 56 const line56 = true; │ │█
|
||||
│ │ 57 const line57 = true; │ │█
|
||||
│ │ 58 const line58 = true; │ │█
|
||||
│ │ 59 const line59 = true; │ │█
|
||||
│ │ 60 const line60 = true; │ │█
|
||||
│ │ 61 - return kittyProtocolSupporte...; │ │█
|
||||
│ │ 61 + return kittyProtocolSupporte...; │ │█
|
||||
│ │ 62 buffer: TextBuffer; │ │█
|
||||
│ │ 63 onSubmit: (value: string) => void; │ │█
|
||||
│ ╰─────────────────────────────────────────────────────────────────────────────────────────────╯ │█
|
||||
│ Apply this change? │█
|
||||
│ │█
|
||||
│ ● 1. Allow once │█
|
||||
│ 2. Allow for this session │█
|
||||
│ 3. Allow for this file in all future sessions │█
|
||||
│ 3. Allow for this file in all future sessions ~/.gemini/policies/auto-saved.toml │█
|
||||
│ 4. Modify with external editor │█
|
||||
│ 5. No, suggest changes (esc) │█
|
||||
│ │█
|
||||
╰─────────────────────────────────────────────────────────────────────────────────────────────────╯█
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -106,7 +106,7 @@ describe('rewindCommand', () => {
|
||||
},
|
||||
config: {
|
||||
getSessionId: () => 'test-session-id',
|
||||
getContextManager: () => ({ refresh: mockResetContext }),
|
||||
getMemoryContextManager: () => ({ refresh: mockResetContext }),
|
||||
getProjectRoot: mockGetProjectRoot,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -61,7 +61,9 @@ async function rewindConversation(
|
||||
client.setHistory(clientHistory as Content[]);
|
||||
|
||||
// Reset context manager as we are rewinding history
|
||||
await context.services.agentContext?.config.getContextManager()?.refresh();
|
||||
await context.services.agentContext?.config
|
||||
.getMemoryContextManager()
|
||||
?.refresh();
|
||||
|
||||
// Update UI History
|
||||
// We generate IDs based on index for the rewind history
|
||||
|
||||
@@ -16,6 +16,7 @@ const createAnsiToken = (overrides: Partial<AnsiToken>): AnsiToken => ({
|
||||
underline: false,
|
||||
dim: false,
|
||||
inverse: false,
|
||||
isUninitialized: false,
|
||||
fg: '#ffffff',
|
||||
bg: '#000000',
|
||||
...overrides,
|
||||
@@ -156,4 +157,30 @@ describe('<AnsiOutputText />', () => {
|
||||
expect(lastFrame()).toBeDefined();
|
||||
unmount();
|
||||
});
|
||||
|
||||
describe('robustness', () => {
|
||||
it('does NOT crash when data is undefined', async () => {
|
||||
const { lastFrame, unmount } = await render(
|
||||
<AnsiOutputText
|
||||
data={undefined as unknown as AnsiOutput}
|
||||
width={80}
|
||||
disableTruncation={true}
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame({ allowEmpty: true }).trim()).toBe('');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('does NOT crash when data is an object but not an array', async () => {
|
||||
const { lastFrame, unmount } = await render(
|
||||
<AnsiOutputText
|
||||
data={{ summary: 'test' } as unknown as AnsiOutput}
|
||||
width={80}
|
||||
disableTruncation={true}
|
||||
/>,
|
||||
);
|
||||
expect(lastFrame({ allowEmpty: true }).trim()).toBe('');
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,14 +35,16 @@ export const AnsiOutputText: React.FC<AnsiOutputProps> = ({
|
||||
? Math.min(availableHeightLimit, maxLines)
|
||||
: (availableHeightLimit ?? maxLines ?? DEFAULT_HEIGHT);
|
||||
|
||||
const lastLines = disableTruncation
|
||||
? data
|
||||
: numLinesRetained === 0
|
||||
? []
|
||||
: data.slice(-numLinesRetained);
|
||||
const lastLines = Array.isArray(data)
|
||||
? disableTruncation
|
||||
? data
|
||||
: numLinesRetained === 0
|
||||
? []
|
||||
: data.slice(-numLinesRetained)
|
||||
: [];
|
||||
return (
|
||||
<Box flexDirection="column" width={width} flexShrink={0} overflow="hidden">
|
||||
{lastLines.map((line: AnsiLine, lineIndex: number) => (
|
||||
{(lastLines as AnsiLine[]).map((line: AnsiLine, lineIndex: number) => (
|
||||
<Box key={lineIndex} height={1} overflow="hidden">
|
||||
<AnsiLineText line={line} />
|
||||
</Box>
|
||||
|
||||
@@ -1409,6 +1409,53 @@ describe('AskUserDialog', () => {
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it('supports "Other" option for yesno questions', async () => {
|
||||
const questions: Question[] = [
|
||||
{
|
||||
question: 'Is this correct?',
|
||||
header: 'Confirm',
|
||||
type: QuestionType.YESNO,
|
||||
},
|
||||
];
|
||||
|
||||
const onSubmit = vi.fn();
|
||||
const { stdin, lastFrame, waitUntilReady } = await renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={questions}
|
||||
onSubmit={onSubmit}
|
||||
onCancel={vi.fn()}
|
||||
width={80}
|
||||
/>,
|
||||
{ width: 80 },
|
||||
);
|
||||
|
||||
// Navigate to "Other" (3rd option: 1. Yes, 2. No, 3. Other)
|
||||
writeKey(stdin, '\x1b[B'); // Down to No
|
||||
writeKey(stdin, '\x1b[B'); // Down to Other
|
||||
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Enter a custom value');
|
||||
});
|
||||
|
||||
// Type feedback
|
||||
for (const char of 'Yes, but with caveats') {
|
||||
writeKey(stdin, char);
|
||||
}
|
||||
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Yes, but with caveats');
|
||||
});
|
||||
|
||||
// Submit
|
||||
writeKey(stdin, '\r');
|
||||
|
||||
await waitFor(async () => {
|
||||
expect(onSubmit).toHaveBeenCalledWith({ '0': 'Yes, but with caveats' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('expands paste placeholders in multi-select custom option via Done', async () => {
|
||||
|
||||
@@ -511,8 +511,9 @@ const ChoiceQuestionView: React.FC<ChoiceQuestionViewProps> = ({
|
||||
}) => {
|
||||
const keyMatchers = useKeyMatchers();
|
||||
const isAlternateBuffer = useAlternateBuffer();
|
||||
const numOptions =
|
||||
(question.options?.length ?? 0) + (question.type !== 'yesno' ? 1 : 0);
|
||||
const hasAll = question.multiSelect && (question.options?.length ?? 0) > 1;
|
||||
// Calculate total options including 'All' and 'Other' to ensure consistent numbering column width
|
||||
const numOptions = (question.options?.length ?? 0) + (hasAll ? 1 : 0) + 1;
|
||||
const numLen = String(numOptions).length;
|
||||
const radioWidth = 2; // "● "
|
||||
const numberWidth = numLen + 2; // e.g., "1. "
|
||||
@@ -735,17 +736,15 @@ const ChoiceQuestionView: React.FC<ChoiceQuestionViewProps> = ({
|
||||
list.push({ key: 'all', value: allItem });
|
||||
}
|
||||
|
||||
// Only add custom option for choice type, not yesno
|
||||
if (question.type !== 'yesno') {
|
||||
const otherItem: OptionItem = {
|
||||
key: 'other',
|
||||
label: customOptionText || '',
|
||||
description: '',
|
||||
type: 'other',
|
||||
index: list.length,
|
||||
};
|
||||
list.push({ key: 'other', value: otherItem });
|
||||
}
|
||||
// Add custom option for choice and yesno types
|
||||
const otherItem: OptionItem = {
|
||||
key: 'other',
|
||||
label: customOptionText || '',
|
||||
description: '',
|
||||
type: 'other',
|
||||
index: list.length,
|
||||
};
|
||||
list.push({ key: 'other', value: otherItem });
|
||||
|
||||
if (question.multiSelect) {
|
||||
const doneItem: OptionItem = {
|
||||
@@ -759,7 +758,7 @@ const ChoiceQuestionView: React.FC<ChoiceQuestionViewProps> = ({
|
||||
}
|
||||
|
||||
return list;
|
||||
}, [questionOptions, question.multiSelect, question.type, customOptionText]);
|
||||
}, [questionOptions, question.multiSelect, customOptionText]);
|
||||
|
||||
const handleHighlight = useCallback(
|
||||
(itemValue: OptionItem) => {
|
||||
|
||||
@@ -26,6 +26,7 @@ import { OverflowProvider } from '../contexts/OverflowContext.js';
|
||||
import { ConfigInitDisplay } from './ConfigInitDisplay.js';
|
||||
import { TodoTray } from './messages/Todo.js';
|
||||
import { useComposerStatus } from '../hooks/useComposerStatus.js';
|
||||
import { appEvents, AppEvent } from '../../utils/events.js';
|
||||
|
||||
export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
const uiState = useUIState();
|
||||
@@ -55,6 +56,12 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
|
||||
const { setShortcutsHelpVisible } = uiActions;
|
||||
|
||||
useEffect(() => {
|
||||
if (hasPendingActionRequired) {
|
||||
appEvents.emit(AppEvent.ScrollToBottom);
|
||||
}
|
||||
}, [hasPendingActionRequired]);
|
||||
|
||||
useEffect(() => {
|
||||
if (uiState.shortcutsHelpVisible && !isPassiveShortcutsHelpState) {
|
||||
setShortcutsHelpVisible(false);
|
||||
@@ -172,9 +179,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
|
||||
{showUiDetails &&
|
||||
!settings.merged.ui.hideFooter &&
|
||||
!isScreenReaderEnabled && (
|
||||
<Footer copyModeEnabled={uiState.copyModeEnabled} />
|
||||
)}
|
||||
!isScreenReaderEnabled && <Footer />}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -166,6 +166,7 @@ Implement a comprehensive authentication system with multiple providers.
|
||||
writeTextFile: vi.fn(),
|
||||
}),
|
||||
getUseAlternateBuffer: () => useAlternateBuffer,
|
||||
getUseTerminalBuffer: () => false,
|
||||
} as unknown as import('@google/gemini-cli-core').Config,
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer } }),
|
||||
},
|
||||
@@ -466,6 +467,7 @@ Implement a comprehensive authentication system with multiple providers.
|
||||
writeTextFile: vi.fn(),
|
||||
}),
|
||||
getUseAlternateBuffer: () => useAlternateBuffer ?? true,
|
||||
getUseTerminalBuffer: () => false,
|
||||
} as unknown as import('@google/gemini-cli-core').Config,
|
||||
settings: createMockSettings({
|
||||
ui: { useAlternateBuffer: useAlternateBuffer ?? true },
|
||||
|
||||
@@ -18,7 +18,7 @@ vi.mock('../../utils/processUtils.js', () => ({
|
||||
}));
|
||||
|
||||
const mockedExit = vi.hoisted(() => vi.fn());
|
||||
const mockedCwd = vi.hoisted(() => vi.fn());
|
||||
const mockedCwd = vi.hoisted(() => vi.fn().mockReturnValue('/mock/cwd'));
|
||||
const mockedRows = vi.hoisted(() => ({ current: 24 }));
|
||||
|
||||
vi.mock('node:process', async () => {
|
||||
@@ -85,7 +85,7 @@ describe('FolderTrustDialog', () => {
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('This folder contains:');
|
||||
expect(lastFrame()).toContain('hidden');
|
||||
expect(lastFrame()).not.toContain('cmd9');
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -116,7 +116,7 @@ describe('FolderTrustDialog', () => {
|
||||
|
||||
// With maxHeight=4, the intro text (4 lines) will take most of the space.
|
||||
// The discovery results will likely be hidden.
|
||||
expect(lastFrame()).toContain('hidden');
|
||||
expect(lastFrame()).not.toContain('cmd1');
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -145,7 +145,7 @@ describe('FolderTrustDialog', () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('hidden');
|
||||
expect(lastFrame()).not.toContain('cmd1');
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -178,10 +178,11 @@ describe('FolderTrustDialog', () => {
|
||||
// Initial state: truncated
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('Do you trust the files in this folder?');
|
||||
expect(lastFrame()).toContain('Press Ctrl+O');
|
||||
expect(lastFrame()).toContain('hidden');
|
||||
expect(lastFrame()).not.toContain('cmd9');
|
||||
});
|
||||
|
||||
unmount();
|
||||
|
||||
// We can't easily simulate global Ctrl+O toggle in this unit test
|
||||
// because it's handled in AppContainer.
|
||||
// But we can re-render with constrainHeight: false.
|
||||
@@ -195,7 +196,7 @@ describe('FolderTrustDialog', () => {
|
||||
width: 80,
|
||||
config: makeFakeConfig({ useAlternateBuffer: false }),
|
||||
settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
|
||||
uiState: { constrainHeight: false, terminalHeight: 24 },
|
||||
uiState: { constrainHeight: false, terminalHeight: 50 },
|
||||
},
|
||||
);
|
||||
|
||||
@@ -205,7 +206,6 @@ describe('FolderTrustDialog', () => {
|
||||
expect(lastFrameExpanded()).toContain('- cmd4');
|
||||
});
|
||||
|
||||
unmount();
|
||||
unmountExpanded();
|
||||
});
|
||||
|
||||
|
||||
@@ -81,6 +81,7 @@ const mockConfigPlain = {
|
||||
isTrustedFolder: () => true,
|
||||
getExtensionRegistryURI: () => undefined,
|
||||
getContentGeneratorConfig: () => ({ authType: undefined }),
|
||||
getSandboxEnabled: () => false,
|
||||
};
|
||||
|
||||
const mockConfig = mockConfigPlain as unknown as Config;
|
||||
@@ -364,7 +365,7 @@ describe('<Footer />', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should display custom sandbox info when SANDBOX env is set', async () => {
|
||||
it('should display "current process" for custom sandbox when SANDBOX env is set', async () => {
|
||||
vi.stubEnv('SANDBOX', 'gemini-cli-test-sandbox');
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: mockConfig,
|
||||
@@ -374,12 +375,12 @@ describe('<Footer />', () => {
|
||||
sessionStats: mockSessionStats,
|
||||
},
|
||||
});
|
||||
expect(lastFrame()).toContain('test');
|
||||
expect(lastFrame()).toContain('current process');
|
||||
vi.unstubAllEnvs();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should display macOS Seatbelt info when SANDBOX is sandbox-exec', async () => {
|
||||
it('should display "current process" for macOS Seatbelt when SANDBOX is sandbox-exec', async () => {
|
||||
vi.stubEnv('SANDBOX', 'sandbox-exec');
|
||||
vi.stubEnv('SEATBELT_PROFILE', 'test-profile');
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
@@ -387,7 +388,7 @@ describe('<Footer />', () => {
|
||||
width: 120,
|
||||
uiState: { isTrustedFolder: true, sessionStats: mockSessionStats },
|
||||
});
|
||||
expect(lastFrame()).toMatch(/macOS Seatbelt.*\(test-profile\)/s);
|
||||
expect(lastFrame()).toContain('current process');
|
||||
vi.unstubAllEnvs();
|
||||
unmount();
|
||||
});
|
||||
@@ -405,6 +406,24 @@ describe('<Footer />', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should display "all tools" when tool sandboxing is enabled and agent is local', async () => {
|
||||
vi.stubEnv('SANDBOX', '');
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
config: Object.assign(
|
||||
Object.create(Object.getPrototypeOf(mockConfig)),
|
||||
mockConfig,
|
||||
{
|
||||
getSandboxEnabled: () => true,
|
||||
},
|
||||
),
|
||||
width: 120,
|
||||
uiState: { isTrustedFolder: true, sessionStats: mockSessionStats },
|
||||
});
|
||||
expect(lastFrame()).toContain('all tools');
|
||||
vi.unstubAllEnvs();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should prioritize untrusted message over sandbox info', async () => {
|
||||
vi.stubEnv('SANDBOX', 'gemini-cli-test-sandbox');
|
||||
const { lastFrame, unmount } = await renderWithProviders(<Footer />, {
|
||||
|
||||
@@ -67,26 +67,19 @@ interface SandboxIndicatorProps {
|
||||
const SandboxIndicator: React.FC<SandboxIndicatorProps> = ({
|
||||
isTrustedFolder,
|
||||
}) => {
|
||||
const config = useConfig();
|
||||
const sandboxEnabled = config.getSandboxEnabled();
|
||||
if (isTrustedFolder === false) {
|
||||
return <Text color={theme.status.warning}>untrusted</Text>;
|
||||
}
|
||||
|
||||
const sandbox = process.env['SANDBOX'];
|
||||
if (sandbox && sandbox !== 'sandbox-exec') {
|
||||
return (
|
||||
<Text color="green">{sandbox.replace(/^gemini-(?:cli-)?/, '')}</Text>
|
||||
);
|
||||
if (sandbox) {
|
||||
return <Text color={theme.status.warning}>current process</Text>;
|
||||
}
|
||||
|
||||
if (sandbox === 'sandbox-exec') {
|
||||
return (
|
||||
<Text color={theme.status.warning}>
|
||||
macOS Seatbelt{' '}
|
||||
<Text color={theme.ui.comment}>
|
||||
({process.env['SEATBELT_PROFILE']})
|
||||
</Text>
|
||||
</Text>
|
||||
);
|
||||
if (sandboxEnabled) {
|
||||
return <Text color={theme.status.warning}>all tools</Text>;
|
||||
}
|
||||
|
||||
return <Text color={theme.status.error}>no sandbox</Text>;
|
||||
@@ -178,9 +171,7 @@ interface FooterColumn {
|
||||
isHighPriority: boolean;
|
||||
}
|
||||
|
||||
export const Footer: React.FC<{ copyModeEnabled?: boolean }> = ({
|
||||
copyModeEnabled = false,
|
||||
}) => {
|
||||
export const Footer: React.FC = () => {
|
||||
const uiState = useUIState();
|
||||
const config = useConfig();
|
||||
const settings = useSettings();
|
||||
@@ -198,10 +189,6 @@ export const Footer: React.FC<{ copyModeEnabled?: boolean }> = ({
|
||||
}
|
||||
}, [authType]);
|
||||
|
||||
if (copyModeEnabled) {
|
||||
return <Box height={1} />;
|
||||
}
|
||||
|
||||
const {
|
||||
model,
|
||||
targetDir,
|
||||
@@ -317,9 +304,8 @@ export const Footer: React.FC<{ copyModeEnabled?: boolean }> = ({
|
||||
let str = 'no sandbox';
|
||||
const sandbox = process.env['SANDBOX'];
|
||||
if (isTrustedFolder === false) str = 'untrusted';
|
||||
else if (sandbox === 'sandbox-exec')
|
||||
str = `macOS Seatbelt (${process.env['SEATBELT_PROFILE']})`;
|
||||
else if (sandbox) str = sandbox.replace(/^gemini-(?:cli-)?/, '');
|
||||
else if (sandbox) str = 'current process';
|
||||
else if (config.getSandboxEnabled()) str = 'all tools';
|
||||
|
||||
addCol(
|
||||
id,
|
||||
|
||||
@@ -72,7 +72,7 @@ describe('Help Component', () => {
|
||||
|
||||
expect(output).toContain('Keyboard Shortcuts:');
|
||||
expect(output).toContain('Ctrl+C');
|
||||
expect(output).toContain('Ctrl+S');
|
||||
expect(output).toContain('Shift+Tab');
|
||||
expect(output).toContain('Page Up/Page Down');
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -49,6 +49,7 @@ interface HistoryItemDisplayProps {
|
||||
isExpandable?: boolean;
|
||||
isFirstThinking?: boolean;
|
||||
isFirstAfterThinking?: boolean;
|
||||
isToolGroupBoundary?: boolean;
|
||||
suppressNarration?: boolean;
|
||||
}
|
||||
|
||||
@@ -62,14 +63,17 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
isExpandable,
|
||||
isFirstThinking = false,
|
||||
isFirstAfterThinking = false,
|
||||
isToolGroupBoundary = false,
|
||||
suppressNarration = false,
|
||||
}) => {
|
||||
const settings = useSettings();
|
||||
const inlineThinkingMode = getInlineThinkingMode(settings);
|
||||
const itemForDisplay = useMemo(() => escapeAnsiCtrlCodes(item), [item]);
|
||||
|
||||
const needsTopMarginAfterThinking =
|
||||
isFirstAfterThinking && inlineThinkingMode !== 'off';
|
||||
const needTopMargin = !!(
|
||||
(isFirstAfterThinking && inlineThinkingMode !== 'off') ||
|
||||
isToolGroupBoundary
|
||||
);
|
||||
|
||||
// If there's a topic update in this turn, we suppress the regular narration
|
||||
// and thoughts as they are being "replaced" by the update_topic tool.
|
||||
@@ -87,7 +91,7 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
flexDirection="column"
|
||||
key={itemForDisplay.id}
|
||||
width={terminalWidth}
|
||||
marginTop={needsTopMarginAfterThinking ? 1 : 0}
|
||||
marginTop={needTopMargin ? 1 : 0}
|
||||
>
|
||||
{/* Render standard message types */}
|
||||
{itemForDisplay.type === 'thinking' && inlineThinkingMode !== 'off' && (
|
||||
@@ -160,23 +164,9 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
{itemForDisplay.type === 'stats' && (
|
||||
<StatsDisplay
|
||||
duration={itemForDisplay.duration}
|
||||
quotas={itemForDisplay.quotas}
|
||||
selectedAuthType={itemForDisplay.selectedAuthType}
|
||||
userEmail={itemForDisplay.userEmail}
|
||||
tier={itemForDisplay.tier}
|
||||
currentModel={itemForDisplay.currentModel}
|
||||
quotaStats={
|
||||
itemForDisplay.pooledRemaining !== undefined ||
|
||||
itemForDisplay.pooledLimit !== undefined ||
|
||||
itemForDisplay.pooledResetTime !== undefined
|
||||
? {
|
||||
remaining: itemForDisplay.pooledRemaining,
|
||||
limit: itemForDisplay.pooledLimit,
|
||||
resetTime: itemForDisplay.pooledResetTime,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
creditBalance={itemForDisplay.creditBalance}
|
||||
/>
|
||||
)}
|
||||
{itemForDisplay.type === 'model_stats' && (
|
||||
@@ -214,6 +204,7 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
borderTop={itemForDisplay.borderTop}
|
||||
borderBottom={itemForDisplay.borderBottom}
|
||||
isExpandable={isExpandable}
|
||||
isToolGroupBoundary={isToolGroupBoundary}
|
||||
/>
|
||||
)}
|
||||
{itemForDisplay.type === 'subagent' && (
|
||||
|
||||
@@ -220,6 +220,7 @@ describe('InputPrompt', () => {
|
||||
col = newText.length;
|
||||
}
|
||||
mockBuffer.cursor = [0, col];
|
||||
mockBuffer.allVisualLines = [newText];
|
||||
mockBuffer.viewportVisualLines = [newText];
|
||||
mockBuffer.allVisualLines = [newText];
|
||||
mockBuffer.visualToLogicalMap = [[0, 0]];
|
||||
@@ -2273,6 +2274,7 @@ describe('InputPrompt', () => {
|
||||
async ({ text, visualCursor }) => {
|
||||
mockBuffer.text = text;
|
||||
mockBuffer.lines = [text];
|
||||
mockBuffer.allVisualLines = [text];
|
||||
mockBuffer.viewportVisualLines = [text];
|
||||
mockBuffer.visualCursor = visualCursor as [number, number];
|
||||
props.config.getUseBackgroundColor = () => false;
|
||||
@@ -2322,6 +2324,7 @@ describe('InputPrompt', () => {
|
||||
async ({ text, visualCursor, visualToLogicalMap }) => {
|
||||
mockBuffer.text = text;
|
||||
mockBuffer.lines = text.split('\n');
|
||||
mockBuffer.allVisualLines = text.split('\n');
|
||||
mockBuffer.viewportVisualLines = text.split('\n');
|
||||
mockBuffer.visualCursor = visualCursor as [number, number];
|
||||
mockBuffer.visualToLogicalMap = visualToLogicalMap as Array<
|
||||
@@ -2342,6 +2345,7 @@ describe('InputPrompt', () => {
|
||||
const text = 'first line\n\nthird line';
|
||||
mockBuffer.text = text;
|
||||
mockBuffer.lines = text.split('\n');
|
||||
mockBuffer.allVisualLines = text.split('\n');
|
||||
mockBuffer.viewportVisualLines = text.split('\n');
|
||||
mockBuffer.visualCursor = [1, 0]; // cursor on the blank line
|
||||
mockBuffer.visualToLogicalMap = [
|
||||
@@ -2361,11 +2365,98 @@ describe('InputPrompt', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('scrolling large inputs', () => {
|
||||
it('should correctly render scrolling down and up for large inputs', async () => {
|
||||
const lines = Array.from({ length: 50 }).map((_, i) => `testline ${i}`);
|
||||
|
||||
// Since we need to test how the React component tree responds to TextBuffer state changes,
|
||||
// we must provide a fake TextBuffer implementation that triggers re-renders like the real one.
|
||||
|
||||
const TestWrapper = () => {
|
||||
const [bufferState, setBufferState] = useState({
|
||||
text: lines.join('\n'),
|
||||
lines,
|
||||
allVisualLines: lines,
|
||||
viewportVisualLines: lines.slice(0, 10),
|
||||
visualToLogicalMap: lines.map((_, i) => [i, 0]),
|
||||
visualCursor: [0, 0] as [number, number],
|
||||
visualScrollRow: 0,
|
||||
viewportHeight: 10,
|
||||
});
|
||||
|
||||
const fakeBuffer = {
|
||||
...mockBuffer,
|
||||
...bufferState,
|
||||
handleInput: vi.fn().mockImplementation((key) => {
|
||||
let newRow = bufferState.visualCursor[0];
|
||||
let newScroll = bufferState.visualScrollRow;
|
||||
if (key.name === 'down') {
|
||||
newRow = Math.min(49, newRow + 1);
|
||||
if (newRow >= newScroll + 10) newScroll++;
|
||||
} else if (key.name === 'up') {
|
||||
newRow = Math.max(0, newRow - 1);
|
||||
if (newRow < newScroll) newScroll--;
|
||||
}
|
||||
setBufferState({
|
||||
...bufferState,
|
||||
visualCursor: [newRow, 0],
|
||||
visualScrollRow: newScroll,
|
||||
viewportVisualLines: lines.slice(newScroll, newScroll + 10),
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
} as unknown as TextBuffer;
|
||||
|
||||
return <InputPrompt {...props} buffer={fakeBuffer} />;
|
||||
};
|
||||
|
||||
const { stdout, unmount, stdin } = await renderWithProviders(
|
||||
<TestWrapper />,
|
||||
{
|
||||
uiActions,
|
||||
},
|
||||
);
|
||||
|
||||
// Verify initial render
|
||||
await waitFor(() => {
|
||||
expect(stdout.lastFrame()).toContain('testline 0');
|
||||
expect(stdout.lastFrame()).not.toContain('testline 49');
|
||||
});
|
||||
|
||||
// Move cursor to bottom
|
||||
for (let i = 0; i < 49; i++) {
|
||||
act(() => {
|
||||
stdin.write('\x1b[B'); // Arrow Down
|
||||
});
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
expect(stdout.lastFrame()).toContain('testline 49');
|
||||
expect(stdout.lastFrame()).not.toContain('testline 0');
|
||||
});
|
||||
|
||||
// Move cursor back to top
|
||||
for (let i = 0; i < 49; i++) {
|
||||
act(() => {
|
||||
stdin.write('\x1b[A'); // Arrow Up
|
||||
});
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
expect(stdout.lastFrame()).toContain('testline 0');
|
||||
expect(stdout.lastFrame()).not.toContain('testline 49');
|
||||
});
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiline rendering', () => {
|
||||
it('should correctly render multiline input including blank lines', async () => {
|
||||
const text = 'hello\n\nworld';
|
||||
mockBuffer.text = text;
|
||||
mockBuffer.lines = text.split('\n');
|
||||
mockBuffer.allVisualLines = text.split('\n');
|
||||
mockBuffer.viewportVisualLines = text.split('\n');
|
||||
mockBuffer.allVisualLines = text.split('\n');
|
||||
mockBuffer.visualCursor = [2, 5]; // cursor at the end of "world"
|
||||
@@ -3592,7 +3683,9 @@ describe('InputPrompt', () => {
|
||||
async ({ relX, relY, mouseCol, mouseRow }) => {
|
||||
props.buffer.text = 'hello world\nsecond line';
|
||||
props.buffer.lines = ['hello world', 'second line'];
|
||||
props.buffer.allVisualLines = ['hello world', 'second line'];
|
||||
props.buffer.viewportVisualLines = ['hello world', 'second line'];
|
||||
props.buffer.viewportHeight = 10;
|
||||
props.buffer.visualToLogicalMap = [
|
||||
[0, 0],
|
||||
[1, 0],
|
||||
@@ -3630,6 +3723,7 @@ describe('InputPrompt', () => {
|
||||
it('should unfocus embedded shell on click', async () => {
|
||||
props.buffer.text = 'hello';
|
||||
props.buffer.lines = ['hello'];
|
||||
props.buffer.allVisualLines = ['hello'];
|
||||
props.buffer.viewportVisualLines = ['hello'];
|
||||
props.buffer.visualToLogicalMap = [[0, 0]];
|
||||
props.isEmbeddedShellFocused = true;
|
||||
@@ -3671,6 +3765,7 @@ describe('InputPrompt', () => {
|
||||
lines: currentLines,
|
||||
viewportVisualLines: currentLines,
|
||||
allVisualLines: currentLines,
|
||||
viewportHeight: 10,
|
||||
pastedContent: { [id]: largeText },
|
||||
transformationsByLine: isExpanded
|
||||
? currentLines.map(() => [])
|
||||
@@ -3759,6 +3854,7 @@ describe('InputPrompt', () => {
|
||||
lines: currentLines,
|
||||
viewportVisualLines: currentLines,
|
||||
allVisualLines: currentLines,
|
||||
viewportHeight: 10,
|
||||
pastedContent: { [id]: largeText },
|
||||
transformationsByLine: isExpanded
|
||||
? currentLines.map(() => [])
|
||||
@@ -3830,7 +3926,9 @@ describe('InputPrompt', () => {
|
||||
props.config.getUseBackgroundColor = () => false;
|
||||
props.buffer.text = 'hello world';
|
||||
props.buffer.lines = ['hello world'];
|
||||
props.buffer.allVisualLines = ['hello world'];
|
||||
props.buffer.viewportVisualLines = ['hello world'];
|
||||
props.buffer.viewportHeight = 10;
|
||||
props.buffer.visualToLogicalMap = [[0, 0]];
|
||||
props.buffer.visualCursor = [0, 11];
|
||||
props.buffer.visualScrollRow = 0;
|
||||
@@ -4137,6 +4235,7 @@ describe('InputPrompt', () => {
|
||||
const text = 'hello';
|
||||
mockBuffer.text = text;
|
||||
mockBuffer.lines = [text];
|
||||
mockBuffer.allVisualLines = [text];
|
||||
mockBuffer.viewportVisualLines = [text];
|
||||
mockBuffer.visualToLogicalMap = [[0, 0]];
|
||||
mockBuffer.visualCursor = [0, 3]; // Cursor after 'hel'
|
||||
@@ -4167,6 +4266,7 @@ describe('InputPrompt', () => {
|
||||
const text = '👍hello';
|
||||
mockBuffer.text = text;
|
||||
mockBuffer.lines = [text];
|
||||
mockBuffer.allVisualLines = [text];
|
||||
mockBuffer.viewportVisualLines = [text];
|
||||
mockBuffer.visualToLogicalMap = [[0, 0]];
|
||||
mockBuffer.visualCursor = [0, 2]; // Cursor after '👍h' (Note: '👍' is one code point but width 2)
|
||||
@@ -4196,6 +4296,7 @@ describe('InputPrompt', () => {
|
||||
const text = '😀😀😀';
|
||||
mockBuffer.text = text;
|
||||
mockBuffer.lines = [text];
|
||||
mockBuffer.allVisualLines = [text];
|
||||
mockBuffer.viewportVisualLines = [text];
|
||||
mockBuffer.visualToLogicalMap = [[0, 0]];
|
||||
mockBuffer.visualCursor = [0, 2]; // Cursor after 2 emojis (each 1 code point, width 2)
|
||||
@@ -4225,7 +4326,9 @@ describe('InputPrompt', () => {
|
||||
const lines = ['😀😀', 'hello 😀', 'world'];
|
||||
mockBuffer.text = lines.join('\n');
|
||||
mockBuffer.lines = lines;
|
||||
mockBuffer.allVisualLines = lines;
|
||||
mockBuffer.viewportVisualLines = lines;
|
||||
mockBuffer.viewportHeight = 10;
|
||||
mockBuffer.visualToLogicalMap = [
|
||||
[0, 0],
|
||||
[1, 0],
|
||||
@@ -4262,7 +4365,9 @@ describe('InputPrompt', () => {
|
||||
const lines = ['first line', 'second line', 'third line'];
|
||||
mockBuffer.text = lines.join('\n');
|
||||
mockBuffer.lines = lines;
|
||||
mockBuffer.allVisualLines = lines;
|
||||
mockBuffer.viewportVisualLines = lines;
|
||||
mockBuffer.viewportHeight = 10;
|
||||
mockBuffer.visualToLogicalMap = [
|
||||
[0, 0],
|
||||
[1, 0],
|
||||
@@ -4303,6 +4408,7 @@ describe('InputPrompt', () => {
|
||||
it('should report cursor position 0 when input is empty and placeholder is shown', async () => {
|
||||
mockBuffer.text = '';
|
||||
mockBuffer.lines = [''];
|
||||
mockBuffer.allVisualLines = [''];
|
||||
mockBuffer.viewportVisualLines = [''];
|
||||
mockBuffer.visualToLogicalMap = [[0, 0]];
|
||||
mockBuffer.visualCursor = [0, 0];
|
||||
@@ -4335,6 +4441,7 @@ describe('InputPrompt', () => {
|
||||
const applyVisualState = (visualLine: string, cursorCol: number): void => {
|
||||
mockBuffer.text = logicalLine;
|
||||
mockBuffer.lines = [logicalLine];
|
||||
mockBuffer.allVisualLines = [visualLine];
|
||||
mockBuffer.viewportVisualLines = [visualLine];
|
||||
mockBuffer.allVisualLines = [visualLine];
|
||||
mockBuffer.visualToLogicalMap = [[0, 0]];
|
||||
|
||||
@@ -12,6 +12,10 @@ import { SuggestionsDisplay, MAX_WIDTH } from './SuggestionsDisplay.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { useInputHistory } from '../hooks/useInputHistory.js';
|
||||
import { escapeAtSymbols } from '../hooks/atCommandProcessor.js';
|
||||
import {
|
||||
ScrollableList,
|
||||
type ScrollableListRef,
|
||||
} from './shared/ScrollableList.js';
|
||||
import { HalfLinePaddedBox } from './shared/HalfLinePaddedBox.js';
|
||||
import {
|
||||
type TextBuffer,
|
||||
@@ -95,6 +99,10 @@ export function isTerminalPasteTrusted(
|
||||
return kittyProtocolSupported;
|
||||
}
|
||||
|
||||
export type ScrollableItem =
|
||||
| { type: 'visualLine'; lineText: string; absoluteVisualIdx: number }
|
||||
| { type: 'ghostLine'; ghostLine: string; index: number };
|
||||
|
||||
export interface InputPromptProps {
|
||||
buffer: TextBuffer;
|
||||
onSubmit: (value: string) => void;
|
||||
@@ -268,6 +276,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
const pasteTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const innerBoxRef = useRef<DOMElement>(null);
|
||||
const hasUserNavigatedSuggestions = useRef(false);
|
||||
const listRef = useRef<ScrollableListRef<ScrollableItem>>(null);
|
||||
|
||||
const [reverseSearchActive, setReverseSearchActive] = useState(false);
|
||||
const [commandSearchActive, setCommandSearchActive] = useState(false);
|
||||
@@ -338,6 +347,10 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
const showCursor =
|
||||
focus && isShellFocused && !isEmbeddedShellFocused && !copyModeEnabled;
|
||||
|
||||
useEffect(() => {
|
||||
appEvents.emit(AppEvent.ScrollToBottom);
|
||||
}, [buffer.text, buffer.cursor]);
|
||||
|
||||
// Notify parent component about escape prompt state changes
|
||||
useEffect(() => {
|
||||
if (onEscapePromptChange) {
|
||||
@@ -552,7 +565,10 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
if (isEmbeddedShellFocused) {
|
||||
setEmbeddedShellFocused(false);
|
||||
}
|
||||
const visualRow = buffer.visualScrollRow + relY;
|
||||
const currentScrollTop = Math.round(
|
||||
listRef.current?.getScrollState().scrollTop ?? buffer.visualScrollRow,
|
||||
);
|
||||
const visualRow = currentScrollTop + relY;
|
||||
buffer.moveToVisualPosition(visualRow, relX);
|
||||
},
|
||||
{ isActive: focus },
|
||||
@@ -566,7 +582,10 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
(_event, relX, relY) => {
|
||||
if (!isAlternateBuffer) return;
|
||||
|
||||
const visualLine = buffer.viewportVisualLines[relY];
|
||||
const currentScrollTop = Math.round(
|
||||
listRef.current?.getScrollState().scrollTop ?? buffer.visualScrollRow,
|
||||
);
|
||||
const visualLine = buffer.allVisualLines[currentScrollTop + relY];
|
||||
if (!visualLine) return;
|
||||
|
||||
// Even if we click past the end of the line, we might want to collapse an expanded paste
|
||||
@@ -574,10 +593,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
|
||||
const logicalPos = isPastEndOfLine
|
||||
? null
|
||||
: buffer.getLogicalPositionFromVisual(
|
||||
buffer.visualScrollRow + relY,
|
||||
relX,
|
||||
);
|
||||
: buffer.getLogicalPositionFromVisual(currentScrollTop + relY, relX);
|
||||
|
||||
// Check for paste placeholder (collapsed state)
|
||||
if (logicalPos) {
|
||||
@@ -599,7 +615,9 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
|
||||
// If we didn't click a placeholder to expand, check if we are inside or after
|
||||
// an expanded paste region and collapse it.
|
||||
const row = buffer.visualScrollRow + relY;
|
||||
const visualRow = currentScrollTop + relY;
|
||||
const mapEntry = buffer.visualToLogicalMap[visualRow];
|
||||
const row = mapEntry ? mapEntry[0] : visualRow;
|
||||
const expandedId = buffer.getExpandedPasteAtLine(row);
|
||||
if (expandedId) {
|
||||
buffer.togglePasteExpansion(
|
||||
@@ -1342,14 +1360,12 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
);
|
||||
|
||||
useKeypress(handleInput, {
|
||||
isActive: !isEmbeddedShellFocused,
|
||||
isActive: !isEmbeddedShellFocused && !copyModeEnabled,
|
||||
priority: true,
|
||||
});
|
||||
|
||||
const linesToRender = buffer.viewportVisualLines;
|
||||
const [cursorVisualRowAbsolute, cursorVisualColAbsolute] =
|
||||
buffer.visualCursor;
|
||||
const scrollVisualRow = buffer.visualScrollRow;
|
||||
|
||||
const getGhostTextLines = useCallback(() => {
|
||||
if (
|
||||
@@ -1464,6 +1480,155 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
|
||||
const { inlineGhost, additionalLines } = getGhostTextLines();
|
||||
|
||||
const scrollableData = useMemo(() => {
|
||||
const items: ScrollableItem[] = buffer.allVisualLines.map(
|
||||
(lineText, index) => ({
|
||||
type: 'visualLine',
|
||||
lineText,
|
||||
absoluteVisualIdx: index,
|
||||
}),
|
||||
);
|
||||
|
||||
additionalLines.forEach((ghostLine, index) => {
|
||||
items.push({
|
||||
type: 'ghostLine',
|
||||
ghostLine,
|
||||
index,
|
||||
});
|
||||
});
|
||||
|
||||
return items;
|
||||
}, [buffer.allVisualLines, additionalLines]);
|
||||
|
||||
const renderItem = useCallback(
|
||||
({ item }: { item: ScrollableItem; index: number }) => {
|
||||
if (item.type === 'ghostLine') {
|
||||
const padding = Math.max(0, inputWidth - stringWidth(item.ghostLine));
|
||||
return (
|
||||
<Box height={1}>
|
||||
<Text color={theme.text.secondary}>
|
||||
{item.ghostLine}
|
||||
{' '.repeat(padding)}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const { lineText, absoluteVisualIdx } = item;
|
||||
// console.log('renderItem called with:', lineText);
|
||||
const mapEntry = buffer.visualToLogicalMap[absoluteVisualIdx];
|
||||
if (!mapEntry) return <Text> </Text>;
|
||||
|
||||
const isOnCursorLine =
|
||||
focus && absoluteVisualIdx === cursorVisualRowAbsolute;
|
||||
const renderedLine: React.ReactNode[] = [];
|
||||
const [logicalLineIdx] = mapEntry;
|
||||
const logicalLine = buffer.lines[logicalLineIdx] || '';
|
||||
const transformations =
|
||||
buffer.transformationsByLine[logicalLineIdx] ?? [];
|
||||
const tokens = parseInputForHighlighting(
|
||||
logicalLine,
|
||||
logicalLineIdx,
|
||||
transformations,
|
||||
...(focus && buffer.cursor[0] === logicalLineIdx
|
||||
? [buffer.cursor[1]]
|
||||
: []),
|
||||
);
|
||||
const visualStartCol =
|
||||
buffer.visualToTransformedMap[absoluteVisualIdx] ?? 0;
|
||||
const visualEndCol = visualStartCol + cpLen(lineText);
|
||||
const segments = parseSegmentsFromTokens(
|
||||
tokens,
|
||||
visualStartCol,
|
||||
visualEndCol,
|
||||
);
|
||||
let charCount = 0;
|
||||
segments.forEach((seg, segIdx) => {
|
||||
const segLen = cpLen(seg.text);
|
||||
let display = seg.text;
|
||||
if (isOnCursorLine) {
|
||||
const relCol = cursorVisualColAbsolute;
|
||||
const segStart = charCount;
|
||||
const segEnd = segStart + segLen;
|
||||
if (relCol >= segStart && relCol < segEnd) {
|
||||
const charToHighlight = cpSlice(
|
||||
display,
|
||||
relCol - segStart,
|
||||
relCol - segStart + 1,
|
||||
);
|
||||
const highlighted = showCursor
|
||||
? chalk.inverse(charToHighlight)
|
||||
: charToHighlight;
|
||||
display =
|
||||
cpSlice(display, 0, relCol - segStart) +
|
||||
highlighted +
|
||||
cpSlice(display, relCol - segStart + 1);
|
||||
}
|
||||
charCount = segEnd;
|
||||
} else {
|
||||
charCount += segLen;
|
||||
}
|
||||
const color =
|
||||
seg.type === 'command' || seg.type === 'file' || seg.type === 'paste'
|
||||
? theme.text.accent
|
||||
: theme.text.primary;
|
||||
renderedLine.push(
|
||||
<Text key={`token-${segIdx}`} color={color}>
|
||||
{display}
|
||||
</Text>,
|
||||
);
|
||||
});
|
||||
|
||||
const currentLineGhost = isOnCursorLine ? inlineGhost : '';
|
||||
if (
|
||||
isOnCursorLine &&
|
||||
cursorVisualColAbsolute === cpLen(lineText) &&
|
||||
!currentLineGhost
|
||||
) {
|
||||
renderedLine.push(
|
||||
<Text key={`cursor-end-${cursorVisualColAbsolute}`}>
|
||||
{showCursor ? chalk.inverse(' ') : ' '}
|
||||
</Text>,
|
||||
);
|
||||
}
|
||||
const showCursorBeforeGhost =
|
||||
focus &&
|
||||
isOnCursorLine &&
|
||||
cursorVisualColAbsolute === cpLen(lineText) &&
|
||||
currentLineGhost;
|
||||
return (
|
||||
<Box height={1}>
|
||||
<Text
|
||||
terminalCursorFocus={showCursor && isOnCursorLine}
|
||||
terminalCursorPosition={cpIndexToOffset(
|
||||
lineText,
|
||||
cursorVisualColAbsolute,
|
||||
)}
|
||||
>
|
||||
{renderedLine}
|
||||
{showCursorBeforeGhost && (showCursor ? chalk.inverse(' ') : ' ')}
|
||||
{currentLineGhost && (
|
||||
<Text color={theme.text.secondary}>{currentLineGhost}</Text>
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
[
|
||||
buffer.visualToLogicalMap,
|
||||
buffer.lines,
|
||||
buffer.transformationsByLine,
|
||||
buffer.cursor,
|
||||
buffer.visualToTransformedMap,
|
||||
focus,
|
||||
cursorVisualRowAbsolute,
|
||||
cursorVisualColAbsolute,
|
||||
showCursor,
|
||||
inlineGhost,
|
||||
inputWidth,
|
||||
],
|
||||
);
|
||||
|
||||
const useBackgroundColor = config.getUseBackgroundColor();
|
||||
const isLowColor = isLowColorDepth();
|
||||
const terminalBg = theme.background.primary || 'black';
|
||||
@@ -1481,6 +1646,46 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
return false;
|
||||
}, [useBackgroundColor, isLowColor, terminalBg]);
|
||||
|
||||
const prevCursorRef = useRef(buffer.visualCursor);
|
||||
const prevTextRef = useRef(buffer.text);
|
||||
|
||||
// Effect to ensure cursor remains visible after interactions
|
||||
useEffect(() => {
|
||||
const cursorChanged = prevCursorRef.current !== buffer.visualCursor;
|
||||
const textChanged = prevTextRef.current !== buffer.text;
|
||||
|
||||
prevCursorRef.current = buffer.visualCursor;
|
||||
prevTextRef.current = buffer.text;
|
||||
|
||||
if (!cursorChanged && !textChanged) return;
|
||||
|
||||
if (!listRef.current || !focus) return;
|
||||
const { scrollTop, innerHeight } = listRef.current.getScrollState();
|
||||
if (innerHeight === 0) return;
|
||||
|
||||
const cursorVisualRow = buffer.visualCursor[0];
|
||||
const actualScrollTop = Math.round(scrollTop);
|
||||
|
||||
// If cursor is out of the currently visible viewport...
|
||||
if (
|
||||
cursorVisualRow < actualScrollTop ||
|
||||
cursorVisualRow >= actualScrollTop + innerHeight
|
||||
) {
|
||||
// Calculate minimal scroll to make it visible
|
||||
let newScrollTop = actualScrollTop;
|
||||
if (cursorVisualRow < actualScrollTop) {
|
||||
newScrollTop = cursorVisualRow;
|
||||
} else if (cursorVisualRow >= actualScrollTop + innerHeight) {
|
||||
newScrollTop = cursorVisualRow - innerHeight + 1;
|
||||
}
|
||||
|
||||
listRef.current.scrollToIndex({ index: newScrollTop });
|
||||
}
|
||||
}, [buffer.visualCursor, buffer.text, focus]);
|
||||
|
||||
const listBackgroundColor =
|
||||
useLineFallback || !useBackgroundColor ? undefined : theme.background.input;
|
||||
|
||||
useEffect(() => {
|
||||
if (onSuggestionsVisibilityChange) {
|
||||
onSuggestionsVisibilityChange(shouldShowSuggestions);
|
||||
@@ -1611,153 +1816,30 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
<Text color={theme.text.secondary}>{placeholder}</Text>
|
||||
)
|
||||
) : (
|
||||
linesToRender
|
||||
.map((lineText: string, visualIdxInRenderedSet: number) => {
|
||||
const absoluteVisualIdx =
|
||||
scrollVisualRow + visualIdxInRenderedSet;
|
||||
const mapEntry = buffer.visualToLogicalMap[absoluteVisualIdx];
|
||||
if (!mapEntry) return null;
|
||||
|
||||
const cursorVisualRow =
|
||||
cursorVisualRowAbsolute - scrollVisualRow;
|
||||
const isOnCursorLine =
|
||||
focus && visualIdxInRenderedSet === cursorVisualRow;
|
||||
|
||||
const renderedLine: React.ReactNode[] = [];
|
||||
|
||||
const [logicalLineIdx] = mapEntry;
|
||||
const logicalLine = buffer.lines[logicalLineIdx] || '';
|
||||
const transformations =
|
||||
buffer.transformationsByLine[logicalLineIdx] ?? [];
|
||||
const tokens = parseInputForHighlighting(
|
||||
logicalLine,
|
||||
logicalLineIdx,
|
||||
transformations,
|
||||
...(focus && buffer.cursor[0] === logicalLineIdx
|
||||
? [buffer.cursor[1]]
|
||||
: []),
|
||||
);
|
||||
const startColInTransformed =
|
||||
buffer.visualToTransformedMap[absoluteVisualIdx] ?? 0;
|
||||
const visualStartCol = startColInTransformed;
|
||||
const visualEndCol = visualStartCol + cpLen(lineText);
|
||||
const segments = parseSegmentsFromTokens(
|
||||
tokens,
|
||||
visualStartCol,
|
||||
visualEndCol,
|
||||
);
|
||||
let charCount = 0;
|
||||
segments.forEach((seg, segIdx) => {
|
||||
const segLen = cpLen(seg.text);
|
||||
let display = seg.text;
|
||||
|
||||
if (isOnCursorLine) {
|
||||
const relativeVisualColForHighlight =
|
||||
cursorVisualColAbsolute;
|
||||
const segStart = charCount;
|
||||
const segEnd = segStart + segLen;
|
||||
if (
|
||||
relativeVisualColForHighlight >= segStart &&
|
||||
relativeVisualColForHighlight < segEnd
|
||||
) {
|
||||
const charToHighlight = cpSlice(
|
||||
display,
|
||||
relativeVisualColForHighlight - segStart,
|
||||
relativeVisualColForHighlight - segStart + 1,
|
||||
);
|
||||
const highlighted = showCursor
|
||||
? chalk.inverse(charToHighlight)
|
||||
: charToHighlight;
|
||||
display =
|
||||
cpSlice(
|
||||
display,
|
||||
0,
|
||||
relativeVisualColForHighlight - segStart,
|
||||
) +
|
||||
highlighted +
|
||||
cpSlice(
|
||||
display,
|
||||
relativeVisualColForHighlight - segStart + 1,
|
||||
);
|
||||
}
|
||||
charCount = segEnd;
|
||||
} else {
|
||||
// Advance the running counter even when not on cursor line
|
||||
charCount += segLen;
|
||||
}
|
||||
|
||||
const color =
|
||||
seg.type === 'command' ||
|
||||
seg.type === 'file' ||
|
||||
seg.type === 'paste'
|
||||
? theme.text.accent
|
||||
: theme.text.primary;
|
||||
|
||||
renderedLine.push(
|
||||
<Text key={`token-${segIdx}`} color={color}>
|
||||
{display}
|
||||
</Text>,
|
||||
);
|
||||
});
|
||||
|
||||
const currentLineGhost = isOnCursorLine ? inlineGhost : '';
|
||||
if (
|
||||
isOnCursorLine &&
|
||||
cursorVisualColAbsolute === cpLen(lineText)
|
||||
) {
|
||||
if (!currentLineGhost) {
|
||||
renderedLine.push(
|
||||
<Text key={`cursor-end-${cursorVisualColAbsolute}`}>
|
||||
{showCursor ? chalk.inverse(' ') : ' '}
|
||||
</Text>,
|
||||
);
|
||||
}
|
||||
<Box
|
||||
flexDirection="column"
|
||||
height={Math.min(buffer.viewportHeight, scrollableData.length)}
|
||||
width="100%"
|
||||
>
|
||||
<ScrollableList
|
||||
ref={listRef}
|
||||
hasFocus={focus}
|
||||
data={scrollableData}
|
||||
renderItem={renderItem}
|
||||
estimatedItemHeight={() => 1}
|
||||
keyExtractor={(item) =>
|
||||
item.type === 'visualLine'
|
||||
? `line-${item.absoluteVisualIdx}`
|
||||
: `ghost-${item.index}`
|
||||
}
|
||||
|
||||
const showCursorBeforeGhost =
|
||||
focus &&
|
||||
isOnCursorLine &&
|
||||
cursorVisualColAbsolute === cpLen(lineText) &&
|
||||
currentLineGhost;
|
||||
|
||||
return (
|
||||
<Box key={`line-${visualIdxInRenderedSet}`} height={1}>
|
||||
<Text
|
||||
terminalCursorFocus={showCursor && isOnCursorLine}
|
||||
terminalCursorPosition={cpIndexToOffset(
|
||||
lineText,
|
||||
cursorVisualColAbsolute,
|
||||
)}
|
||||
>
|
||||
{renderedLine}
|
||||
{showCursorBeforeGhost &&
|
||||
(showCursor ? chalk.inverse(' ') : ' ')}
|
||||
{currentLineGhost && (
|
||||
<Text color={theme.text.secondary}>
|
||||
{currentLineGhost}
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
})
|
||||
.concat(
|
||||
additionalLines.map((ghostLine, index) => {
|
||||
const padding = Math.max(
|
||||
0,
|
||||
inputWidth - stringWidth(ghostLine),
|
||||
);
|
||||
return (
|
||||
<Text
|
||||
key={`ghost-line-${index}`}
|
||||
color={theme.text.secondary}
|
||||
>
|
||||
{ghostLine}
|
||||
{' '.repeat(padding)}
|
||||
</Text>
|
||||
);
|
||||
}),
|
||||
)
|
||||
width="100%"
|
||||
backgroundColor={listBackgroundColor}
|
||||
containerHeight={Math.min(
|
||||
buffer.viewportHeight,
|
||||
scrollableData.length,
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -63,6 +63,17 @@ describe('<LoadingIndicator />', () => {
|
||||
expect(lastFrame({ allowEmpty: true })?.trim()).toBe('');
|
||||
});
|
||||
|
||||
it('should not show cancel and timer when idle even if a phrase exists', async () => {
|
||||
const { lastFrame, waitUntilReady } = await renderWithContext(
|
||||
<LoadingIndicator currentLoadingPhrase="Retrying..." elapsedTime={5} />,
|
||||
StreamingState.Idle,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Retrying...');
|
||||
expect(output).not.toContain('(esc to cancel');
|
||||
});
|
||||
|
||||
it('should render spinner, phrase, and time when streamingState is Responding', async () => {
|
||||
const { lastFrame, waitUntilReady } = await renderWithContext(
|
||||
<LoadingIndicator {...defaultProps} />,
|
||||
|
||||
@@ -72,8 +72,7 @@ export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
|
||||
: undefined);
|
||||
|
||||
const cancelAndTimerContent =
|
||||
showCancelAndTimer &&
|
||||
streamingState !== StreamingState.WaitingForConfirmation
|
||||
showCancelAndTimer && streamingState === StreamingState.Responding
|
||||
? `(esc to cancel, ${elapsedTime < 60 ? `${elapsedTime}s` : formatDuration(elapsedTime * 1000)})`
|
||||
: null;
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useAppContext } from '../contexts/AppContext.js';
|
||||
import { AppHeader } from './AppHeader.js';
|
||||
|
||||
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
|
||||
import { useConfig } from '../contexts/ConfigContext.js';
|
||||
import {
|
||||
SCROLL_TO_ITEM_END,
|
||||
type VirtualizedListRef,
|
||||
@@ -22,6 +23,7 @@ import { MAX_GEMINI_MESSAGE_LINES } from '../constants.js';
|
||||
import { useConfirmingTool } from '../hooks/useConfirmingTool.js';
|
||||
import { ToolConfirmationQueue } from './ToolConfirmationQueue.js';
|
||||
import { isTopicTool } from './messages/TopicMessage.js';
|
||||
import { appEvents, AppEvent } from '../../utils/events.js';
|
||||
|
||||
const MemoizedHistoryItemDisplay = memo(HistoryItemDisplay);
|
||||
const MemoizedAppHeader = memo(AppHeader);
|
||||
@@ -33,7 +35,10 @@ const MemoizedAppHeader = memo(AppHeader);
|
||||
export const MainContent = () => {
|
||||
const { version } = useAppContext();
|
||||
const uiState = useUIState();
|
||||
const isAlternateBuffer = useAlternateBuffer();
|
||||
const isAlternateBufferOrTerminalBuffer = useAlternateBuffer();
|
||||
const config = useConfig();
|
||||
const useTerminalBuffer = config.getUseTerminalBuffer();
|
||||
const isAlternateBuffer = config.getUseAlternateBuffer();
|
||||
|
||||
const confirmingTool = useConfirmingTool();
|
||||
const showConfirmationQueue = confirmingTool !== null;
|
||||
@@ -47,12 +52,23 @@ export const MainContent = () => {
|
||||
}
|
||||
}, [showConfirmationQueue, confirmingToolCallId]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
scrollableListRef.current?.scrollToEnd();
|
||||
};
|
||||
appEvents.on(AppEvent.ScrollToBottom, handleScroll);
|
||||
return () => {
|
||||
appEvents.off(AppEvent.ScrollToBottom, handleScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const {
|
||||
pendingHistoryItems,
|
||||
mainAreaWidth,
|
||||
staticAreaMaxItemHeight,
|
||||
availableTerminalHeight,
|
||||
cleanUiDetailsVisible,
|
||||
mouseMode,
|
||||
} = uiState;
|
||||
const showHeaderDetails = cleanUiDetailsVisible;
|
||||
|
||||
@@ -103,12 +119,16 @@ export const MainContent = () => {
|
||||
item.type === 'thinking' && prevType !== 'thinking';
|
||||
const isFirstAfterThinking =
|
||||
item.type !== 'thinking' && prevType === 'thinking';
|
||||
const isToolGroupBoundary =
|
||||
(item.type !== 'tool_group' && prevType === 'tool_group') ||
|
||||
(item.type === 'tool_group' && prevType !== 'tool_group');
|
||||
|
||||
return {
|
||||
item,
|
||||
isExpandable: i > lastUserPromptIndex,
|
||||
isFirstThinking,
|
||||
isFirstAfterThinking,
|
||||
isToolGroupBoundary,
|
||||
suppressNarration: suppressNarrationFlags[i] ?? false,
|
||||
};
|
||||
}),
|
||||
@@ -123,6 +143,7 @@ export const MainContent = () => {
|
||||
isExpandable,
|
||||
isFirstThinking,
|
||||
isFirstAfterThinking,
|
||||
isToolGroupBoundary,
|
||||
suppressNarration,
|
||||
}) => (
|
||||
<MemoizedHistoryItemDisplay
|
||||
@@ -140,6 +161,7 @@ export const MainContent = () => {
|
||||
isExpandable={isExpandable}
|
||||
isFirstThinking={isFirstThinking}
|
||||
isFirstAfterThinking={isFirstAfterThinking}
|
||||
isToolGroupBoundary={isToolGroupBoundary}
|
||||
suppressNarration={suppressNarration}
|
||||
/>
|
||||
),
|
||||
@@ -175,6 +197,9 @@ export const MainContent = () => {
|
||||
item.type === 'thinking' && prevType !== 'thinking';
|
||||
const isFirstAfterThinking =
|
||||
item.type !== 'thinking' && prevType === 'thinking';
|
||||
const isToolGroupBoundary =
|
||||
(item.type !== 'tool_group' && prevType === 'tool_group') ||
|
||||
(item.type === 'tool_group' && prevType !== 'tool_group');
|
||||
|
||||
const suppressNarration =
|
||||
suppressNarrationFlags[uiState.history.length + i] ?? false;
|
||||
@@ -191,6 +216,7 @@ export const MainContent = () => {
|
||||
isExpandable={true}
|
||||
isFirstThinking={isFirstThinking}
|
||||
isFirstAfterThinking={isFirstAfterThinking}
|
||||
isToolGroupBoundary={isToolGroupBoundary}
|
||||
suppressNarration={suppressNarration}
|
||||
/>
|
||||
);
|
||||
@@ -218,25 +244,14 @@ export const MainContent = () => {
|
||||
const virtualizedData = useMemo(
|
||||
() => [
|
||||
{ type: 'header' as const },
|
||||
...augmentedHistory.map(
|
||||
({
|
||||
item,
|
||||
isExpandable,
|
||||
isFirstThinking,
|
||||
isFirstAfterThinking,
|
||||
suppressNarration,
|
||||
}) => ({
|
||||
type: 'history' as const,
|
||||
item,
|
||||
isExpandable,
|
||||
isFirstThinking,
|
||||
isFirstAfterThinking,
|
||||
suppressNarration,
|
||||
}),
|
||||
),
|
||||
...augmentedHistory.map((data, index) => ({
|
||||
type: 'history' as const,
|
||||
item: data.item,
|
||||
element: historyItems[index],
|
||||
})),
|
||||
{ type: 'pending' as const },
|
||||
],
|
||||
[augmentedHistory],
|
||||
[augmentedHistory, historyItems],
|
||||
);
|
||||
|
||||
const renderItem = useCallback(
|
||||
@@ -250,58 +265,79 @@ export const MainContent = () => {
|
||||
/>
|
||||
);
|
||||
} else if (item.type === 'history') {
|
||||
return (
|
||||
<MemoizedHistoryItemDisplay
|
||||
terminalWidth={mainAreaWidth}
|
||||
availableTerminalHeight={
|
||||
uiState.constrainHeight || !item.isExpandable
|
||||
? staticAreaMaxItemHeight
|
||||
: undefined
|
||||
}
|
||||
availableTerminalHeightGemini={MAX_GEMINI_MESSAGE_LINES}
|
||||
key={item.item.id}
|
||||
item={item.item}
|
||||
isPending={false}
|
||||
commands={uiState.slashCommands}
|
||||
isExpandable={item.isExpandable}
|
||||
isFirstThinking={item.isFirstThinking}
|
||||
isFirstAfterThinking={item.isFirstAfterThinking}
|
||||
suppressNarration={item.suppressNarration}
|
||||
/>
|
||||
);
|
||||
return item.element;
|
||||
} else {
|
||||
return pendingItems;
|
||||
}
|
||||
},
|
||||
[
|
||||
showHeaderDetails,
|
||||
version,
|
||||
mainAreaWidth,
|
||||
uiState.slashCommands,
|
||||
pendingItems,
|
||||
uiState.constrainHeight,
|
||||
staticAreaMaxItemHeight,
|
||||
],
|
||||
[showHeaderDetails, version, pendingItems],
|
||||
);
|
||||
|
||||
if (isAlternateBuffer) {
|
||||
return (
|
||||
<ScrollableList
|
||||
ref={scrollableListRef}
|
||||
hasFocus={!uiState.isEditorDialogOpen && !uiState.embeddedShellFocused}
|
||||
width={uiState.terminalWidth}
|
||||
data={virtualizedData}
|
||||
renderItem={renderItem}
|
||||
estimatedItemHeight={() => 100}
|
||||
keyExtractor={(item, _index) => {
|
||||
if (item.type === 'header') return 'header';
|
||||
if (item.type === 'history') return item.item.id.toString();
|
||||
return 'pending';
|
||||
}}
|
||||
initialScrollIndex={SCROLL_TO_ITEM_END}
|
||||
initialScrollOffsetInIndex={SCROLL_TO_ITEM_END}
|
||||
/>
|
||||
);
|
||||
const estimatedItemHeight = useCallback(() => 100, []);
|
||||
|
||||
const keyExtractor = useCallback(
|
||||
(item: (typeof virtualizedData)[number], _index: number) => {
|
||||
if (item.type === 'header') return 'header';
|
||||
if (item.type === 'history') return item.item.id.toString();
|
||||
return 'pending';
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// TODO(jacobr): we should return true for all messages that are not
|
||||
// interactive. Gemini messages and Tool results that are not scrollable,
|
||||
// collapsible, or clickable should also be tagged as static in the future.
|
||||
const isStaticItem = useCallback(
|
||||
(item: (typeof virtualizedData)[number]) => item.type === 'header',
|
||||
[],
|
||||
);
|
||||
|
||||
const scrollableList = useMemo(() => {
|
||||
if (isAlternateBufferOrTerminalBuffer) {
|
||||
return (
|
||||
<ScrollableList
|
||||
ref={scrollableListRef}
|
||||
hasFocus={
|
||||
!uiState.isEditorDialogOpen && !uiState.embeddedShellFocused
|
||||
}
|
||||
width={uiState.terminalWidth}
|
||||
data={virtualizedData}
|
||||
renderItem={renderItem}
|
||||
estimatedItemHeight={estimatedItemHeight}
|
||||
keyExtractor={keyExtractor}
|
||||
initialScrollIndex={SCROLL_TO_ITEM_END}
|
||||
initialScrollOffsetInIndex={SCROLL_TO_ITEM_END}
|
||||
renderStatic={useTerminalBuffer}
|
||||
isStaticItem={useTerminalBuffer ? isStaticItem : undefined}
|
||||
overflowToBackbuffer={useTerminalBuffer && !isAlternateBuffer}
|
||||
scrollbar={mouseMode}
|
||||
/>
|
||||
// TODO(jacobr): consider adding stableScrollback={!config.getUseAlternateBuffer()}
|
||||
// as that will reduce the # of cases where we will have to clear the
|
||||
// scrollback buffer due to the scrollback size changing but we need to
|
||||
// work out ensuring we only attempt it within a smaller range of
|
||||
// scrollback vals. Right now it sometimes triggers adding more white
|
||||
// space than it should.
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}, [
|
||||
isAlternateBufferOrTerminalBuffer,
|
||||
uiState.isEditorDialogOpen,
|
||||
uiState.embeddedShellFocused,
|
||||
uiState.terminalWidth,
|
||||
virtualizedData,
|
||||
renderItem,
|
||||
estimatedItemHeight,
|
||||
keyExtractor,
|
||||
useTerminalBuffer,
|
||||
isStaticItem,
|
||||
mouseMode,
|
||||
isAlternateBuffer,
|
||||
]);
|
||||
|
||||
if (isAlternateBufferOrTerminalBuffer) {
|
||||
return scrollableList;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -65,6 +65,15 @@ describe('<ModelDialog />', () => {
|
||||
getGemini31FlashLiteLaunchedSync: () => boolean;
|
||||
getProModelNoAccess: () => Promise<boolean>;
|
||||
getProModelNoAccessSync: () => boolean;
|
||||
getLastRetrievedQuota: () =>
|
||||
| {
|
||||
buckets: Array<{
|
||||
modelId?: string;
|
||||
remainingFraction?: number;
|
||||
resetTime?: string;
|
||||
}>;
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
|
||||
const mockConfig: MockConfig = {
|
||||
@@ -76,6 +85,7 @@ describe('<ModelDialog />', () => {
|
||||
getGemini31FlashLiteLaunchedSync: mockGetGemini31FlashLiteLaunchedSync,
|
||||
getProModelNoAccess: mockGetProModelNoAccess,
|
||||
getProModelNoAccessSync: mockGetProModelNoAccessSync,
|
||||
getLastRetrievedQuota: () => ({ buckets: [] }),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
import type React from 'react';
|
||||
import { useCallback, useContext, useMemo, useState, useEffect } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { ModelQuotaDisplay } from './ModelQuotaDisplay.js';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
import {
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
PREVIEW_GEMINI_3_1_MODEL,
|
||||
@@ -37,6 +39,7 @@ interface ModelDialogProps {
|
||||
export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
const config = useContext(ConfigContext);
|
||||
const settings = useSettings();
|
||||
const { terminalWidth } = useUIState();
|
||||
const [hasAccessToProModel, setHasAccessToProModel] = useState<boolean>(
|
||||
() => !(config?.getProModelNoAccessSync() ?? false),
|
||||
);
|
||||
@@ -338,20 +341,24 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
</Box>
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Box>
|
||||
<Text color={theme.text.primary}>
|
||||
<Text bold color={theme.text.primary}>
|
||||
Remember model for future sessions:{' '}
|
||||
</Text>
|
||||
<Text color={theme.status.success}>
|
||||
{persistMode ? 'true' : 'false'}
|
||||
</Text>
|
||||
<Text color={theme.text.secondary}> (Press Tab to toggle)</Text>
|
||||
</Box>
|
||||
<Text color={theme.text.secondary}>(Press Tab to toggle)</Text>
|
||||
</Box>
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Box flexDirection="column">
|
||||
<Text color={theme.text.secondary}>
|
||||
{'> To use a specific Gemini model on startup, use the --model flag.'}
|
||||
</Text>
|
||||
</Box>
|
||||
<ModelQuotaDisplay
|
||||
buckets={config?.getLastRetrievedQuota()?.buckets}
|
||||
availableWidth={terminalWidth - 2}
|
||||
/>
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text color={theme.text.secondary}>(Press Esc to close)</Text>
|
||||
</Box>
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { ModelQuotaDisplay } from './ModelQuotaDisplay.js';
|
||||
|
||||
describe('<ModelQuotaDisplay />', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubEnv('TZ', 'UTC');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it('renders quota information when buckets are provided', async () => {
|
||||
const now = new Date('2025-01-01T12:00:00Z');
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(now);
|
||||
|
||||
const resetTime = new Date(now.getTime() + 1000 * 60 * 90).toISOString(); // 1 hour 30 minutes from now
|
||||
|
||||
const buckets = [
|
||||
{
|
||||
modelId: 'gemini-2.5-pro',
|
||||
remainingFraction: 0.75,
|
||||
resetTime,
|
||||
},
|
||||
];
|
||||
|
||||
const { lastFrame } = await renderWithProviders(
|
||||
<ModelQuotaDisplay buckets={buckets} availableWidth={100} />,
|
||||
{ width: 100 },
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('Model usage');
|
||||
expect(output).toContain('Pro');
|
||||
expect(output).toContain('25%');
|
||||
expect(output).toContain('Resets:');
|
||||
expect(output).toMatchSnapshot();
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('renders nothing when no buckets are provided', async () => {
|
||||
const { lastFrame } = await renderWithProviders(
|
||||
<ModelQuotaDisplay buckets={[]} availableWidth={100} />,
|
||||
{ width: 100 },
|
||||
);
|
||||
const output = lastFrame({ allowEmpty: true });
|
||||
expect(output).toBe('');
|
||||
});
|
||||
|
||||
it('filters models based on modelsToShow prop', async () => {
|
||||
const buckets = [
|
||||
{
|
||||
modelId: 'gemini-2.5-pro',
|
||||
remainingFraction: 0.5,
|
||||
resetTime: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
modelId: 'gemini-2.5-flash',
|
||||
remainingFraction: 0.8,
|
||||
resetTime: new Date().toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
const { lastFrame } = await renderWithProviders(
|
||||
<ModelQuotaDisplay
|
||||
buckets={buckets}
|
||||
modelsToShow={['gemini-2.5-pro']}
|
||||
availableWidth={100}
|
||||
/>,
|
||||
{ width: 100 },
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('Pro');
|
||||
expect(output).not.toContain('Flash');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { ProgressBar } from './ProgressBar.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { formatResetTime } from '../utils/formatters.js';
|
||||
import { getDisplayString } from '@google/gemini-cli-core';
|
||||
import { useConfig } from '../contexts/ConfigContext.js';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
|
||||
interface LocalBucket {
|
||||
modelId?: string;
|
||||
remainingFraction?: number;
|
||||
resetTime?: string;
|
||||
}
|
||||
|
||||
interface ModelQuotaDisplayProps {
|
||||
buckets?: LocalBucket[];
|
||||
availableWidth?: number;
|
||||
modelsToShow?: string[];
|
||||
title?: string;
|
||||
}
|
||||
|
||||
interface ModelUsageRowProps {
|
||||
row: {
|
||||
modelId: string;
|
||||
name: string;
|
||||
usedFraction: number;
|
||||
usedPercentage: number;
|
||||
resetTime?: string;
|
||||
};
|
||||
availableWidth?: number;
|
||||
}
|
||||
|
||||
const ModelUsageRow = ({ row, availableWidth }: ModelUsageRowProps) => {
|
||||
const { terminalWidth } = useUIState();
|
||||
|
||||
const nameColumnWidth = 12;
|
||||
const percentageWidth = 4;
|
||||
const resetColumnWidth = 26;
|
||||
const usedPercentage = row.usedPercentage;
|
||||
|
||||
const nameLabel = row.name;
|
||||
const percentageLabel = `${usedPercentage.toFixed(0)}%`.padEnd(
|
||||
percentageWidth,
|
||||
);
|
||||
const resetLabel = row.resetTime
|
||||
? formatResetTime(row.resetTime, 'column')
|
||||
.slice(0, resetColumnWidth)
|
||||
.padEnd(resetColumnWidth)
|
||||
: ''.padEnd(resetColumnWidth);
|
||||
|
||||
// Calculate the exact width of all fixed adjacent siblings
|
||||
const nameColWidth = nameColumnWidth;
|
||||
const percentColWidth = percentageWidth + 1; // width + marginLeft
|
||||
const resetColWidth = resetColumnWidth + 1; // width + marginLeft
|
||||
|
||||
const fixedSiblingWidth = nameColWidth + percentColWidth + resetColWidth;
|
||||
|
||||
const calcWidth = availableWidth ?? terminalWidth;
|
||||
const defaultPadding = availableWidth != null ? 0 : 4;
|
||||
|
||||
// Subtract fixed sibling widths from total width.
|
||||
// We keep a small buffer (e.g., 3) to prevent edge-case wrapping.
|
||||
const buffer = 3;
|
||||
const barWidth = Math.max(
|
||||
0,
|
||||
calcWidth - defaultPadding - fixedSiblingWidth - buffer,
|
||||
);
|
||||
|
||||
let percentageColor = theme.text.primary;
|
||||
if (usedPercentage >= 100) {
|
||||
percentageColor = theme.status.error;
|
||||
} else if (usedPercentage >= 80) {
|
||||
percentageColor = theme.status.warning;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="row" width="100%">
|
||||
<Box width={nameColumnWidth}>
|
||||
<Text color={theme.text.primary} wrap="truncate-end">
|
||||
{nameLabel}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box flexGrow={1}>
|
||||
<ProgressBar value={usedPercentage} width={barWidth} />
|
||||
</Box>
|
||||
|
||||
<Box width={4} marginLeft={1}>
|
||||
<Text color={percentageColor}>{percentageLabel}</Text>
|
||||
</Box>
|
||||
|
||||
<Box width={resetColumnWidth} marginLeft={1}>
|
||||
<Text color={theme.text.secondary}>
|
||||
{resetLabel.trim() ? `Resets: ${resetLabel}` : ''}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const ModelQuotaDisplay = ({
|
||||
buckets,
|
||||
availableWidth,
|
||||
modelsToShow = ['all'],
|
||||
title = 'Model usage',
|
||||
}: ModelQuotaDisplayProps) => {
|
||||
const config = useConfig();
|
||||
|
||||
const modelsWithQuotas = useMemo(() => {
|
||||
if (!buckets) return [];
|
||||
|
||||
let filteredBuckets = buckets.filter(
|
||||
(b) => b.modelId && b.remainingFraction != null,
|
||||
);
|
||||
|
||||
if (modelsToShow.includes('current')) {
|
||||
const currentModel = config.getActiveModel?.() ?? config.getModel?.();
|
||||
filteredBuckets = filteredBuckets.filter(
|
||||
(b) => b.modelId === currentModel,
|
||||
);
|
||||
} else if (!modelsToShow.includes('all')) {
|
||||
filteredBuckets = filteredBuckets.filter(
|
||||
(b) => b.modelId && modelsToShow.includes(b.modelId),
|
||||
);
|
||||
}
|
||||
|
||||
const groupedByTier = new Map<
|
||||
string,
|
||||
{
|
||||
modelId: string;
|
||||
remainingFraction: number;
|
||||
resetTime?: string;
|
||||
name: string;
|
||||
}
|
||||
>();
|
||||
|
||||
filteredBuckets.forEach((b) => {
|
||||
const modelId = b.modelId;
|
||||
const remainingFraction = b.remainingFraction;
|
||||
if (!modelId || remainingFraction == null) return;
|
||||
|
||||
const tier =
|
||||
config?.modelConfigService?.getModelDefinition(modelId)?.tier;
|
||||
const groupKey = tier ?? modelId;
|
||||
const existing = groupedByTier.get(groupKey);
|
||||
|
||||
if (!existing || remainingFraction < existing.remainingFraction) {
|
||||
const tierDisplayNames: Record<string, string> = {
|
||||
pro: 'Pro',
|
||||
flash: 'Flash',
|
||||
'flash-lite': 'Flash Lite',
|
||||
};
|
||||
const name = tier
|
||||
? (tierDisplayNames[tier] ?? tier)
|
||||
: getDisplayString(modelId, config);
|
||||
|
||||
groupedByTier.set(groupKey, {
|
||||
modelId,
|
||||
remainingFraction,
|
||||
resetTime: b.resetTime,
|
||||
name,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(groupedByTier.entries()).map(([key, data]) => {
|
||||
const usedFraction = 1 - data.remainingFraction;
|
||||
const usedPercentage = usedFraction * 100;
|
||||
return {
|
||||
modelId: key,
|
||||
name: data.name,
|
||||
usedFraction,
|
||||
usedPercentage,
|
||||
resetTime: data.resetTime,
|
||||
};
|
||||
});
|
||||
}, [buckets, config, modelsToShow]);
|
||||
|
||||
if (modelsWithQuotas.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
{/* Rule Line */}
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderTop={true}
|
||||
borderBottom={false}
|
||||
borderLeft={false}
|
||||
borderRight={false}
|
||||
borderColor={theme.border.default}
|
||||
/>
|
||||
|
||||
<Box flexDirection="column">
|
||||
<Box marginBottom={1}>
|
||||
<Text bold color={theme.text.primary}>
|
||||
{title}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{modelsWithQuotas.map((row) => (
|
||||
<ModelUsageRow
|
||||
key={row.modelId}
|
||||
row={row}
|
||||
availableWidth={availableWidth}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -22,7 +22,7 @@ import * as processUtils from '../../utils/processUtils.js';
|
||||
import { usePermissionsModifyTrust } from '../hooks/usePermissionsModifyTrust.js';
|
||||
|
||||
// Hoist mocks for dependencies of the usePermissionsModifyTrust hook
|
||||
const mockedCwd = vi.hoisted(() => vi.fn());
|
||||
const mockedCwd = vi.hoisted(() => vi.fn().mockReturnValue('/mock/cwd'));
|
||||
const mockedLoadTrustedFolders = vi.hoisted(() => vi.fn());
|
||||
const mockedIsWorkspaceTrusted = vi.hoisted(() => vi.fn());
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ProgressBar } from './ProgressBar.js';
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
|
||||
describe('<ProgressBar />', () => {
|
||||
it('renders 0% correctly', async () => {
|
||||
const { lastFrame } = await renderWithProviders(
|
||||
<ProgressBar value={0} width={10} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders 50% correctly', async () => {
|
||||
const { lastFrame } = await renderWithProviders(
|
||||
<ProgressBar value={50} width={10} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders warning threshold correctly', async () => {
|
||||
const { lastFrame } = await renderWithProviders(
|
||||
<ProgressBar value={85} width={10} warningThreshold={80} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders error threshold correctly at 100%', async () => {
|
||||
const { lastFrame } = await renderWithProviders(
|
||||
<ProgressBar value={100} width={10} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
|
||||
interface ProgressBarProps {
|
||||
value: number; // 0 to 100
|
||||
width: number;
|
||||
warningThreshold?: number;
|
||||
}
|
||||
|
||||
export const ProgressBar: React.FC<ProgressBarProps> = ({
|
||||
value,
|
||||
width,
|
||||
warningThreshold = 80,
|
||||
}) => {
|
||||
const safeValue = Math.min(Math.max(value, 0), 100);
|
||||
const activeChars = Math.ceil((safeValue / 100) * width);
|
||||
const inactiveChars = width - activeChars;
|
||||
|
||||
let color = theme.status.success;
|
||||
if (safeValue >= 100) {
|
||||
color = theme.status.error;
|
||||
} else if (safeValue >= warningThreshold) {
|
||||
color = theme.status.warning;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="row">
|
||||
<Text color={color}>{'▬'.repeat(activeChars)}</Text>
|
||||
<Text color={theme.border.default}>{'▬'.repeat(inactiveChars)}</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -9,10 +9,7 @@ import { describe, it, expect, vi } from 'vitest';
|
||||
import { StatsDisplay } from './StatsDisplay.js';
|
||||
import * as SessionContext from '../contexts/SessionContext.js';
|
||||
import { type SessionMetrics } from '../contexts/SessionContext.js';
|
||||
import {
|
||||
ToolCallDecision,
|
||||
type RetrieveUserQuotaResponse,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { ToolCallDecision } from '@google/gemini-cli-core';
|
||||
|
||||
// Mock the context to provide controlled data for testing
|
||||
vi.mock('../contexts/SessionContext.js', async (importOriginal) => {
|
||||
@@ -124,10 +121,13 @@ describe('<StatsDisplay />', () => {
|
||||
const { lastFrame } = await renderWithMockedStats(metrics);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('gemini-2.5-pro');
|
||||
expect(output).toContain('gemini-2.5-flash');
|
||||
expect(output).toContain('15,000');
|
||||
expect(output).toContain('10,000');
|
||||
expect(output).toContain('Performance');
|
||||
expect(output).toContain('Interaction Summary');
|
||||
expect(output).toContain('Model Usage');
|
||||
expect(output).toContain('Reqs');
|
||||
expect(output).toContain('Input Tokens');
|
||||
expect(output).toContain('Cache Reads');
|
||||
expect(output).toContain('Output Tokens');
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -182,7 +182,7 @@ describe('<StatsDisplay />', () => {
|
||||
expect(output).toContain('Performance');
|
||||
expect(output).toContain('Interaction Summary');
|
||||
expect(output).toContain('User Agreement');
|
||||
expect(output).toContain('gemini-2.5-pro');
|
||||
expect(output).toContain('Model Usage');
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -406,174 +406,6 @@ describe('<StatsDisplay />', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Quota Display', () => {
|
||||
it('renders quota information when quotas are provided', async () => {
|
||||
const now = new Date('2025-01-01T12:00:00Z');
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(now);
|
||||
|
||||
const metrics = createTestMetrics({
|
||||
models: {
|
||||
'gemini-2.5-pro': {
|
||||
api: { totalRequests: 1, totalErrors: 0, totalLatencyMs: 100 },
|
||||
tokens: {
|
||||
input: 50,
|
||||
prompt: 100,
|
||||
candidates: 100,
|
||||
total: 250,
|
||||
cached: 50,
|
||||
thoughts: 0,
|
||||
tool: 0,
|
||||
},
|
||||
roles: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const resetTime = new Date(now.getTime() + 1000 * 60 * 90).toISOString(); // 1 hour 30 minutes from now
|
||||
|
||||
const quotas: RetrieveUserQuotaResponse = {
|
||||
buckets: [
|
||||
{
|
||||
modelId: 'gemini-2.5-pro',
|
||||
remainingAmount: '75',
|
||||
remainingFraction: 0.75,
|
||||
resetTime,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
useSessionStatsMock.mockReturnValue({
|
||||
stats: {
|
||||
sessionId: 'test-session-id',
|
||||
sessionStartTime: new Date(),
|
||||
metrics,
|
||||
lastPromptTokenCount: 0,
|
||||
promptCount: 5,
|
||||
},
|
||||
|
||||
getPromptCount: () => 5,
|
||||
startNewPrompt: vi.fn(),
|
||||
});
|
||||
|
||||
const { lastFrame } = await renderWithProviders(
|
||||
<StatsDisplay duration="1s" quotas={quotas} />,
|
||||
{ width: 100 },
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('Model usage');
|
||||
expect(output).toContain('25%');
|
||||
expect(output).toContain('Usage resets');
|
||||
expect(output).toMatchSnapshot();
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('renders pooled quota information for auto mode', async () => {
|
||||
const now = new Date('2025-01-01T12:00:00Z');
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(now);
|
||||
|
||||
const metrics = createTestMetrics();
|
||||
const quotas: RetrieveUserQuotaResponse = {
|
||||
buckets: [
|
||||
{
|
||||
modelId: 'gemini-2.5-pro',
|
||||
remainingAmount: '10',
|
||||
remainingFraction: 0.1, // limit = 100
|
||||
},
|
||||
{
|
||||
modelId: 'gemini-2.5-flash',
|
||||
remainingAmount: '700',
|
||||
remainingFraction: 0.7, // limit = 1000
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
useSessionStatsMock.mockReturnValue({
|
||||
stats: {
|
||||
sessionId: 'test-session-id',
|
||||
sessionStartTime: new Date(),
|
||||
metrics,
|
||||
lastPromptTokenCount: 0,
|
||||
promptCount: 5,
|
||||
},
|
||||
getPromptCount: () => 5,
|
||||
startNewPrompt: vi.fn(),
|
||||
});
|
||||
|
||||
const { lastFrame } = await renderWithProviders(
|
||||
<StatsDisplay
|
||||
duration="1s"
|
||||
quotas={quotas}
|
||||
currentModel="auto"
|
||||
quotaStats={{
|
||||
remaining: 710,
|
||||
limit: 1100,
|
||||
}}
|
||||
/>,
|
||||
{ width: 100 },
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
// (1 - 710/1100) * 100 = 35.5%
|
||||
expect(output).toContain('35%');
|
||||
expect(output).toContain('Usage limit: 1,100');
|
||||
expect(output).toMatchSnapshot();
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('renders quota information for unused models', async () => {
|
||||
const now = new Date('2025-01-01T12:00:00Z');
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(now);
|
||||
|
||||
// No models in metrics, but a quota for gemini-2.5-flash
|
||||
const metrics = createTestMetrics();
|
||||
|
||||
const resetTime = new Date(now.getTime() + 1000 * 60 * 120).toISOString(); // 2 hours from now
|
||||
|
||||
const quotas: RetrieveUserQuotaResponse = {
|
||||
buckets: [
|
||||
{
|
||||
modelId: 'gemini-2.5-flash',
|
||||
remainingAmount: '50',
|
||||
remainingFraction: 0.5,
|
||||
resetTime,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
useSessionStatsMock.mockReturnValue({
|
||||
stats: {
|
||||
sessionId: 'test-session-id',
|
||||
sessionStartTime: new Date(),
|
||||
metrics,
|
||||
lastPromptTokenCount: 0,
|
||||
promptCount: 5,
|
||||
},
|
||||
getPromptCount: () => 5,
|
||||
startNewPrompt: vi.fn(),
|
||||
});
|
||||
|
||||
const { lastFrame } = await renderWithProviders(
|
||||
<StatsDisplay duration="1s" quotas={quotas} />,
|
||||
{ width: 100 },
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('gemini-2.5-flash');
|
||||
expect(output).toContain('-'); // for requests
|
||||
expect(output).toContain('50%');
|
||||
expect(output).toContain('Usage resets');
|
||||
expect(output).toMatchSnapshot();
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
describe('User Identity Display', () => {
|
||||
it('renders User row with Auth Method and Tier', async () => {
|
||||
const metrics = createTestMetrics();
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text, useStdout } from 'ink';
|
||||
import { Box, Text } from 'ink';
|
||||
import { ThemedGradient } from './ThemedGradient.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { formatDuration, formatResetTime } from '../utils/formatters.js';
|
||||
import { formatDuration } from '../utils/formatters.js';
|
||||
import {
|
||||
useSessionStats,
|
||||
type ModelMetrics,
|
||||
@@ -19,25 +19,10 @@ import {
|
||||
TOOL_SUCCESS_RATE_MEDIUM,
|
||||
USER_AGREEMENT_RATE_HIGH,
|
||||
USER_AGREEMENT_RATE_MEDIUM,
|
||||
CACHE_EFFICIENCY_HIGH,
|
||||
CACHE_EFFICIENCY_MEDIUM,
|
||||
getUsedStatusColor,
|
||||
QUOTA_USED_WARNING_THRESHOLD,
|
||||
QUOTA_USED_CRITICAL_THRESHOLD,
|
||||
} from '../utils/displayUtils.js';
|
||||
import { computeSessionStats } from '../utils/computeStats.js';
|
||||
import {
|
||||
type Config,
|
||||
type RetrieveUserQuotaResponse,
|
||||
isActiveModel,
|
||||
getDisplayString,
|
||||
isAutoModel,
|
||||
AuthType,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
import { useConfig } from '../contexts/ConfigContext.js';
|
||||
import type { QuotaStats } from '../types.js';
|
||||
import { QuotaStatsInfo } from './QuotaStatsInfo.js';
|
||||
|
||||
// A more flexible and powerful StatRow component
|
||||
interface StatRowProps {
|
||||
@@ -87,433 +72,94 @@ const Section: React.FC<SectionProps> = ({ title, children }) => (
|
||||
);
|
||||
|
||||
// Logic for building the unified list of table rows
|
||||
const buildModelRows = (
|
||||
models: Record<string, ModelMetrics>,
|
||||
config: Config,
|
||||
quotas?: RetrieveUserQuotaResponse,
|
||||
useGemini3_1 = false,
|
||||
useGemini3_1FlashLite = false,
|
||||
useCustomToolModel = false,
|
||||
) => {
|
||||
const getBaseModelName = (name: string) => name.replace('-001', '');
|
||||
const usedModelNames = new Set(
|
||||
Object.keys(models)
|
||||
.map(getBaseModelName)
|
||||
.map((name) => getDisplayString(name, config)),
|
||||
);
|
||||
|
||||
// 1. Models with active usage
|
||||
const activeRows = Object.entries(models).map(([name, metrics]) => {
|
||||
const modelName = getBaseModelName(name);
|
||||
const cachedTokens = metrics.tokens.cached;
|
||||
const inputTokens = metrics.tokens.input;
|
||||
return {
|
||||
key: name,
|
||||
modelName: getDisplayString(modelName, config),
|
||||
requests: metrics.api.totalRequests,
|
||||
cachedTokens: cachedTokens.toLocaleString(),
|
||||
inputTokens: inputTokens.toLocaleString(),
|
||||
outputTokens: metrics.tokens.candidates.toLocaleString(),
|
||||
bucket: quotas?.buckets?.find((b) => b.modelId === modelName),
|
||||
isActive: true,
|
||||
};
|
||||
});
|
||||
|
||||
// 2. Models with quota only
|
||||
const quotaRows =
|
||||
quotas?.buckets
|
||||
?.filter(
|
||||
(b) =>
|
||||
b.modelId &&
|
||||
isActiveModel(
|
||||
b.modelId,
|
||||
useGemini3_1,
|
||||
useGemini3_1FlashLite,
|
||||
useCustomToolModel,
|
||||
) &&
|
||||
!usedModelNames.has(getDisplayString(b.modelId, config)),
|
||||
)
|
||||
.map((bucket) => ({
|
||||
key: bucket.modelId!,
|
||||
modelName: getDisplayString(bucket.modelId!, config),
|
||||
requests: '-',
|
||||
cachedTokens: '-',
|
||||
inputTokens: '-',
|
||||
outputTokens: '-',
|
||||
bucket,
|
||||
isActive: false,
|
||||
})) || [];
|
||||
|
||||
return [...activeRows, ...quotaRows];
|
||||
};
|
||||
|
||||
const ModelUsageTable: React.FC<{
|
||||
interface ModelUsageTableProps {
|
||||
models: Record<string, ModelMetrics>;
|
||||
config: Config;
|
||||
quotas?: RetrieveUserQuotaResponse;
|
||||
cacheEfficiency: number;
|
||||
totalCachedTokens: number;
|
||||
currentModel?: string;
|
||||
pooledRemaining?: number;
|
||||
pooledLimit?: number;
|
||||
pooledResetTime?: string;
|
||||
useGemini3_1?: boolean;
|
||||
useGemini3_1FlashLite?: boolean;
|
||||
useCustomToolModel?: boolean;
|
||||
}> = ({
|
||||
models,
|
||||
config,
|
||||
quotas,
|
||||
cacheEfficiency,
|
||||
totalCachedTokens,
|
||||
currentModel,
|
||||
pooledRemaining,
|
||||
pooledLimit,
|
||||
pooledResetTime,
|
||||
useGemini3_1,
|
||||
useGemini3_1FlashLite,
|
||||
useCustomToolModel,
|
||||
}) => {
|
||||
const { stdout } = useStdout();
|
||||
const terminalWidth = stdout?.columns ?? 84;
|
||||
const rows = buildModelRows(
|
||||
models,
|
||||
config,
|
||||
quotas,
|
||||
useGemini3_1,
|
||||
useGemini3_1FlashLite,
|
||||
useCustomToolModel,
|
||||
);
|
||||
}
|
||||
|
||||
if (rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const showQuotaColumn = !!quotas && rows.some((row) => !!row.bucket);
|
||||
|
||||
const nameWidth = 23;
|
||||
const requestsWidth = 5;
|
||||
const uncachedWidth = 15;
|
||||
const cachedWidth = 14;
|
||||
const outputTokensWidth = 15;
|
||||
const percentageWidth = showQuotaColumn ? 6 : 0;
|
||||
const resetWidth = 22;
|
||||
|
||||
// Total width of other columns (including parent box paddingX={2})
|
||||
const fixedWidth = nameWidth + requestsWidth + percentageWidth + resetWidth;
|
||||
const outerPadding = 4;
|
||||
const availableForUsage = terminalWidth - outerPadding - fixedWidth;
|
||||
|
||||
const usageLimitWidth = showQuotaColumn
|
||||
? Math.max(10, Math.min(24, availableForUsage))
|
||||
: 0;
|
||||
const progressBarWidth = Math.max(2, usageLimitWidth - 4);
|
||||
|
||||
const renderProgressBar = (
|
||||
usedFraction: number,
|
||||
color: string,
|
||||
totalSteps = 20,
|
||||
) => {
|
||||
let filledSteps = Math.round(usedFraction * totalSteps);
|
||||
|
||||
// If something is used (fraction > 0) but rounds to 0, show 1 tick.
|
||||
// If < 100% (fraction < 1) but rounds to 20, show 19 ticks.
|
||||
if (usedFraction > 0 && usedFraction < 1) {
|
||||
filledSteps = Math.min(Math.max(filledSteps, 1), totalSteps - 1);
|
||||
}
|
||||
|
||||
const emptySteps = Math.max(0, totalSteps - filledSteps);
|
||||
return (
|
||||
<Box flexDirection="row" flexShrink={0}>
|
||||
<Text wrap="truncate-end">
|
||||
<Text color={color}>{'▬'.repeat(filledSteps)}</Text>
|
||||
<Text color={theme.border.default}>{'▬'.repeat(emptySteps)}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const cacheEfficiencyColor = getStatusColor(cacheEfficiency, {
|
||||
green: CACHE_EFFICIENCY_HIGH,
|
||||
yellow: CACHE_EFFICIENCY_MEDIUM,
|
||||
});
|
||||
|
||||
const totalWidth =
|
||||
nameWidth +
|
||||
requestsWidth +
|
||||
(showQuotaColumn
|
||||
? usageLimitWidth + percentageWidth + resetWidth
|
||||
: uncachedWidth + cachedWidth + outputTokensWidth);
|
||||
|
||||
const isAuto = currentModel && isAutoModel(currentModel);
|
||||
const ModelUsageTable: React.FC<ModelUsageTableProps> = ({ models }) => {
|
||||
const nameWidth = 28;
|
||||
const requestsWidth = 8;
|
||||
const inputTokensWidth = 14;
|
||||
const cacheReadsWidth = 14;
|
||||
const outputTokensWidth = 14;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
{isAuto &&
|
||||
showQuotaColumn &&
|
||||
pooledRemaining !== undefined &&
|
||||
pooledLimit !== undefined &&
|
||||
pooledLimit > 0 && (
|
||||
<Box flexDirection="column" marginTop={0} marginBottom={1}>
|
||||
<QuotaStatsInfo
|
||||
remaining={pooledRemaining}
|
||||
limit={pooledLimit}
|
||||
resetTime={pooledResetTime}
|
||||
/>
|
||||
<Text color={theme.text.primary}>
|
||||
For a full token breakdown, run `/stats model`.
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Text bold color={theme.text.primary}>
|
||||
Model Usage
|
||||
</Text>
|
||||
<Text color={theme.text.secondary}>
|
||||
Use /model to view model quota information
|
||||
</Text>
|
||||
<Box height={1} />
|
||||
|
||||
<Box alignItems="flex-end">
|
||||
<Box width={nameWidth} flexShrink={0}>
|
||||
<Text bold color={theme.text.primary}>
|
||||
Model
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
width={requestsWidth}
|
||||
flexDirection="column"
|
||||
alignItems="flex-end"
|
||||
flexShrink={0}
|
||||
>
|
||||
<Text bold color={theme.text.primary}>
|
||||
Reqs
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{!showQuotaColumn && (
|
||||
<>
|
||||
<Box
|
||||
width={uncachedWidth}
|
||||
flexDirection="column"
|
||||
alignItems="flex-end"
|
||||
flexShrink={0}
|
||||
>
|
||||
<Text bold color={theme.text.primary}>
|
||||
Input Tokens
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
width={cachedWidth}
|
||||
flexDirection="column"
|
||||
alignItems="flex-end"
|
||||
flexShrink={0}
|
||||
>
|
||||
<Text bold color={theme.text.primary}>
|
||||
Cache Reads
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
width={outputTokensWidth}
|
||||
flexDirection="column"
|
||||
alignItems="flex-end"
|
||||
flexShrink={0}
|
||||
>
|
||||
<Text bold color={theme.text.primary}>
|
||||
Output Tokens
|
||||
</Text>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
{showQuotaColumn && (
|
||||
<>
|
||||
<Box
|
||||
width={usageLimitWidth}
|
||||
flexDirection="column"
|
||||
alignItems="flex-start"
|
||||
paddingLeft={4}
|
||||
flexShrink={0}
|
||||
>
|
||||
<Text bold color={theme.text.primary}>
|
||||
Model usage
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={percentageWidth} flexShrink={0} />
|
||||
<Box
|
||||
width={resetWidth}
|
||||
flexDirection="column"
|
||||
alignItems="flex-start"
|
||||
paddingLeft={2}
|
||||
flexShrink={0}
|
||||
>
|
||||
<Text bold color={theme.text.primary} wrap="truncate-end">
|
||||
Usage resets
|
||||
</Text>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Divider */}
|
||||
{/* Header */}
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderBottom={true}
|
||||
borderStyle="single"
|
||||
borderColor={theme.border.default}
|
||||
borderTop={false}
|
||||
borderLeft={false}
|
||||
borderRight={false}
|
||||
borderColor={theme.border.default}
|
||||
width={totalWidth}
|
||||
></Box>
|
||||
|
||||
{rows.map((row) => {
|
||||
let effectiveUsedFraction = 0;
|
||||
let usedPercentage = 0;
|
||||
let statusColor = theme.ui.comment;
|
||||
let percentageText = '';
|
||||
|
||||
if (row.bucket && row.bucket.remainingFraction != null) {
|
||||
const actualUsedFraction = 1 - row.bucket.remainingFraction;
|
||||
effectiveUsedFraction =
|
||||
actualUsedFraction === 0 && row.isActive
|
||||
? 0.001
|
||||
: actualUsedFraction;
|
||||
usedPercentage = effectiveUsedFraction * 100;
|
||||
statusColor =
|
||||
getUsedStatusColor(usedPercentage, {
|
||||
warning: QUOTA_USED_WARNING_THRESHOLD,
|
||||
critical: QUOTA_USED_CRITICAL_THRESHOLD,
|
||||
}) ?? (row.isActive ? theme.text.primary : theme.ui.comment);
|
||||
percentageText =
|
||||
usedPercentage > 0 && usedPercentage < 1
|
||||
? `${usedPercentage.toFixed(1)}%`
|
||||
: `${usedPercentage.toFixed(0)}%`;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box key={row.key}>
|
||||
<Box width={nameWidth} flexShrink={0}>
|
||||
<Text
|
||||
color={row.isActive ? theme.text.primary : theme.text.secondary}
|
||||
wrap="truncate-end"
|
||||
>
|
||||
{row.modelName}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
width={requestsWidth}
|
||||
flexDirection="column"
|
||||
alignItems="flex-end"
|
||||
flexShrink={0}
|
||||
>
|
||||
<Text
|
||||
color={row.isActive ? theme.text.primary : theme.text.secondary}
|
||||
>
|
||||
{row.requests}
|
||||
</Text>
|
||||
</Box>
|
||||
{!showQuotaColumn && (
|
||||
<>
|
||||
<Box
|
||||
width={uncachedWidth}
|
||||
flexDirection="column"
|
||||
alignItems="flex-end"
|
||||
flexShrink={0}
|
||||
>
|
||||
<Text
|
||||
color={
|
||||
row.isActive ? theme.text.primary : theme.text.secondary
|
||||
}
|
||||
>
|
||||
{row.inputTokens}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
width={cachedWidth}
|
||||
flexDirection="column"
|
||||
alignItems="flex-end"
|
||||
flexShrink={0}
|
||||
>
|
||||
<Text color={theme.text.secondary}>{row.cachedTokens}</Text>
|
||||
</Box>
|
||||
<Box
|
||||
width={outputTokensWidth}
|
||||
flexDirection="column"
|
||||
alignItems="flex-end"
|
||||
flexShrink={0}
|
||||
>
|
||||
<Text
|
||||
color={
|
||||
row.isActive ? theme.text.primary : theme.text.secondary
|
||||
}
|
||||
>
|
||||
{row.outputTokens}
|
||||
</Text>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
{showQuotaColumn && (
|
||||
<>
|
||||
<Box
|
||||
width={usageLimitWidth}
|
||||
flexDirection="column"
|
||||
alignItems="flex-start"
|
||||
paddingLeft={4}
|
||||
flexShrink={0}
|
||||
>
|
||||
{row.bucket && row.bucket.remainingFraction != null && (
|
||||
<Box flexDirection="row" flexShrink={0}>
|
||||
{renderProgressBar(
|
||||
effectiveUsedFraction,
|
||||
statusColor,
|
||||
progressBarWidth,
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Box
|
||||
width={percentageWidth}
|
||||
flexDirection="column"
|
||||
alignItems="flex-end"
|
||||
flexShrink={0}
|
||||
>
|
||||
{row.bucket && row.bucket.remainingFraction != null && (
|
||||
<Box>
|
||||
{row.bucket.remainingFraction === 0 ? (
|
||||
<Text color={theme.status.error} wrap="truncate-end">
|
||||
Limit
|
||||
</Text>
|
||||
) : (
|
||||
<Text color={statusColor} wrap="truncate-end">
|
||||
{percentageText}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Box
|
||||
width={resetWidth}
|
||||
flexDirection="column"
|
||||
alignItems="flex-start"
|
||||
paddingLeft={2}
|
||||
flexShrink={0}
|
||||
>
|
||||
<Text color={theme.text.secondary} wrap="truncate-end">
|
||||
{row.bucket?.resetTime &&
|
||||
formatResetTime(row.bucket.resetTime, 'column')
|
||||
? formatResetTime(row.bucket.resetTime, 'column')
|
||||
: ''}
|
||||
</Text>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
|
||||
{cacheEfficiency > 0 && !showQuotaColumn && (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Text color={theme.text.primary}>
|
||||
<Text color={theme.status.success}>Savings Highlight:</Text>{' '}
|
||||
{totalCachedTokens.toLocaleString()} (
|
||||
<Text color={cacheEfficiencyColor}>
|
||||
{cacheEfficiency.toFixed(1)}%
|
||||
</Text>
|
||||
) of input tokens were served from the cache, reducing costs.
|
||||
>
|
||||
<Box width={nameWidth}>
|
||||
<Text bold color={theme.text.secondary}>
|
||||
Model
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box width={requestsWidth} justifyContent="flex-end">
|
||||
<Text bold color={theme.text.secondary}>
|
||||
Reqs
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={inputTokensWidth} justifyContent="flex-end">
|
||||
<Text bold color={theme.text.secondary}>
|
||||
Input Tokens
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={cacheReadsWidth} justifyContent="flex-end">
|
||||
<Text bold color={theme.text.secondary}>
|
||||
Cache Reads
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={outputTokensWidth} justifyContent="flex-end">
|
||||
<Text bold color={theme.text.secondary}>
|
||||
Output Tokens
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Rows */}
|
||||
{Object.entries(models).map(([name, modelMetrics]) => (
|
||||
<Box key={name}>
|
||||
<Box width={nameWidth}>
|
||||
<Text color={theme.text.primary} wrap="truncate-end">
|
||||
{name}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={requestsWidth} justifyContent="flex-end">
|
||||
<Text color={theme.text.primary}>
|
||||
{modelMetrics.api.totalRequests}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={inputTokensWidth} justifyContent="flex-end">
|
||||
<Text color={theme.text.primary}>
|
||||
{modelMetrics.tokens.prompt.toLocaleString()}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={cacheReadsWidth} justifyContent="flex-end">
|
||||
<Text color={theme.text.primary}>
|
||||
{modelMetrics.tokens.cached.toLocaleString()}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={outputTokensWidth} justifyContent="flex-end">
|
||||
<Text color={theme.text.primary}>
|
||||
{modelMetrics.tokens.candidates.toLocaleString()}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -521,7 +167,6 @@ const ModelUsageTable: React.FC<{
|
||||
interface StatsDisplayProps {
|
||||
duration: string;
|
||||
title?: string;
|
||||
quotas?: RetrieveUserQuotaResponse;
|
||||
footer?: string;
|
||||
selectedAuthType?: string;
|
||||
userEmail?: string;
|
||||
@@ -534,30 +179,17 @@ interface StatsDisplayProps {
|
||||
export const StatsDisplay: React.FC<StatsDisplayProps> = ({
|
||||
duration,
|
||||
title,
|
||||
quotas,
|
||||
footer,
|
||||
selectedAuthType,
|
||||
userEmail,
|
||||
tier,
|
||||
currentModel,
|
||||
quotaStats,
|
||||
creditBalance,
|
||||
}) => {
|
||||
const { stats } = useSessionStats();
|
||||
const { metrics } = stats;
|
||||
const { models, tools, files } = metrics;
|
||||
const { tools, files, models } = metrics;
|
||||
const computed = computeSessionStats(metrics);
|
||||
const settings = useSettings();
|
||||
const config = useConfig();
|
||||
const useGemini3_1 = config.getGemini31LaunchedSync?.() ?? false;
|
||||
const useGemini3_1FlashLite =
|
||||
config.getGemini31FlashLiteLaunchedSync?.() ?? false;
|
||||
const useCustomToolModel =
|
||||
useGemini3_1 &&
|
||||
config.getContentGeneratorConfig().authType === AuthType.USE_GEMINI;
|
||||
const pooledRemaining = quotaStats?.remaining;
|
||||
const pooledLimit = quotaStats?.limit;
|
||||
const pooledResetTime = quotaStats?.resetTime;
|
||||
|
||||
const showUserIdentity = settings.merged.ui.showUserIdentity;
|
||||
|
||||
@@ -697,20 +329,9 @@ export const StatsDisplay: React.FC<StatsDisplayProps> = ({
|
||||
</Text>
|
||||
</SubStatRow>
|
||||
</Section>
|
||||
<ModelUsageTable
|
||||
models={models}
|
||||
config={config}
|
||||
quotas={quotas}
|
||||
cacheEfficiency={computed.cacheEfficiency}
|
||||
totalCachedTokens={computed.totalCachedTokens}
|
||||
currentModel={currentModel}
|
||||
pooledRemaining={pooledRemaining}
|
||||
pooledLimit={pooledLimit}
|
||||
pooledResetTime={pooledResetTime}
|
||||
useGemini3_1={useGemini3_1}
|
||||
useGemini3_1FlashLite={useGemini3_1FlashLite}
|
||||
useCustomToolModel={useCustomToolModel}
|
||||
/>
|
||||
|
||||
{Object.keys(models).length > 0 && <ModelUsageTable models={models} />}
|
||||
|
||||
{renderFooter()}
|
||||
</Box>
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user