From 3b601b3d90a57d113c666c3922f6064a8f1ff2a8 Mon Sep 17 00:00:00 2001
From: Abhi <43648792+abhipatel12@users.noreply.github.com>
Date: Fri, 13 Mar 2026 13:25:13 -0400
Subject: [PATCH 1/3] refactor(ui): extract SessionBrowser static ui components
(#22348)
---
.../cli/src/ui/components/SessionBrowser.tsx | 35 ++-----------------
.../SessionBrowser/SessionBrowserEmpty.tsx | 19 ++++++++++
.../SessionBrowser/SessionBrowserError.tsx | 24 +++++++++++++
.../SessionBrowser/SessionBrowserLoading.tsx | 18 ++++++++++
.../SessionBrowserStates.test.tsx | 35 +++++++++++++++++++
.../SessionBrowserStates.test.tsx.snap | 18 ++++++++++
6 files changed, 117 insertions(+), 32 deletions(-)
create mode 100644 packages/cli/src/ui/components/SessionBrowser/SessionBrowserEmpty.tsx
create mode 100644 packages/cli/src/ui/components/SessionBrowser/SessionBrowserError.tsx
create mode 100644 packages/cli/src/ui/components/SessionBrowser/SessionBrowserLoading.tsx
create mode 100644 packages/cli/src/ui/components/SessionBrowser/SessionBrowserStates.test.tsx
create mode 100644 packages/cli/src/ui/components/SessionBrowser/__snapshots__/SessionBrowserStates.test.tsx.snap
diff --git a/packages/cli/src/ui/components/SessionBrowser.tsx b/packages/cli/src/ui/components/SessionBrowser.tsx
index 9e2843c570..0fc80a1d4e 100644
--- a/packages/cli/src/ui/components/SessionBrowser.tsx
+++ b/packages/cli/src/ui/components/SessionBrowser.tsx
@@ -116,38 +116,9 @@ const Kbd = ({ name, shortcut }: { name: string; shortcut: string }) => (
>
);
-/**
- * Loading state component displayed while sessions are being loaded.
- */
-const SessionBrowserLoading = (): React.JSX.Element => (
-
- Loading sessions…
-
-);
-
-/**
- * Error state component displayed when session loading fails.
- */
-const SessionBrowserError = ({
- state,
-}: {
- state: SessionBrowserState;
-}): React.JSX.Element => (
-
- Error: {state.error}
- Press q to exit
-
-);
-
-/**
- * Empty state component displayed when no sessions are found.
- */
-const SessionBrowserEmpty = (): React.JSX.Element => (
-
- No auto-saved conversations found.
- Press q to exit
-
-);
+import { SessionBrowserLoading } from './SessionBrowser/SessionBrowserLoading.js';
+import { SessionBrowserError } from './SessionBrowser/SessionBrowserError.js';
+import { SessionBrowserEmpty } from './SessionBrowser/SessionBrowserEmpty.js';
import { sortSessions, filterSessions } from './SessionBrowser/utils.js';
diff --git a/packages/cli/src/ui/components/SessionBrowser/SessionBrowserEmpty.tsx b/packages/cli/src/ui/components/SessionBrowser/SessionBrowserEmpty.tsx
new file mode 100644
index 0000000000..31c9544cd8
--- /dev/null
+++ b/packages/cli/src/ui/components/SessionBrowser/SessionBrowserEmpty.tsx
@@ -0,0 +1,19 @@
+/**
+ * @license
+ * Copyright 2026 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type React from 'react';
+import { Box, Text } from 'ink';
+import { Colors } from '../../colors.js';
+
+/**
+ * Empty state component displayed when no sessions are found.
+ */
+export const SessionBrowserEmpty = (): React.JSX.Element => (
+
+ No auto-saved conversations found.
+ Press q to exit
+
+);
diff --git a/packages/cli/src/ui/components/SessionBrowser/SessionBrowserError.tsx b/packages/cli/src/ui/components/SessionBrowser/SessionBrowserError.tsx
new file mode 100644
index 0000000000..cf46fb8954
--- /dev/null
+++ b/packages/cli/src/ui/components/SessionBrowser/SessionBrowserError.tsx
@@ -0,0 +1,24 @@
+/**
+ * @license
+ * Copyright 2026 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type React from 'react';
+import { Box, Text } from 'ink';
+import { Colors } from '../../colors.js';
+import type { SessionBrowserState } from '../SessionBrowser.js';
+
+/**
+ * Error state component displayed when session loading fails.
+ */
+export const SessionBrowserError = ({
+ state,
+}: {
+ state: SessionBrowserState;
+}): React.JSX.Element => (
+
+ Error: {state.error}
+ Press q to exit
+
+);
diff --git a/packages/cli/src/ui/components/SessionBrowser/SessionBrowserLoading.tsx b/packages/cli/src/ui/components/SessionBrowser/SessionBrowserLoading.tsx
new file mode 100644
index 0000000000..e0c372eca2
--- /dev/null
+++ b/packages/cli/src/ui/components/SessionBrowser/SessionBrowserLoading.tsx
@@ -0,0 +1,18 @@
+/**
+ * @license
+ * Copyright 2026 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type React from 'react';
+import { Box, Text } from 'ink';
+import { Colors } from '../../colors.js';
+
+/**
+ * Loading state component displayed while sessions are being loaded.
+ */
+export const SessionBrowserLoading = (): React.JSX.Element => (
+
+ Loading sessions…
+
+);
diff --git a/packages/cli/src/ui/components/SessionBrowser/SessionBrowserStates.test.tsx b/packages/cli/src/ui/components/SessionBrowser/SessionBrowserStates.test.tsx
new file mode 100644
index 0000000000..2b816a8211
--- /dev/null
+++ b/packages/cli/src/ui/components/SessionBrowser/SessionBrowserStates.test.tsx
@@ -0,0 +1,35 @@
+/**
+ * @license
+ * Copyright 2026 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { render } from '../../../test-utils/render.js';
+import { describe, it, expect } from 'vitest';
+import { SessionBrowserLoading } from './SessionBrowserLoading.js';
+import { SessionBrowserError } from './SessionBrowserError.js';
+import { SessionBrowserEmpty } from './SessionBrowserEmpty.js';
+import type { SessionBrowserState } from '../SessionBrowser.js';
+
+describe('SessionBrowser UI States', () => {
+ it('SessionBrowserLoading renders correctly', async () => {
+ const { lastFrame, waitUntilReady } = render();
+ await waitUntilReady();
+ expect(lastFrame()).toMatchSnapshot();
+ });
+
+ it('SessionBrowserError renders correctly', async () => {
+ const mockState = { error: 'Test error message' } as SessionBrowserState;
+ const { lastFrame, waitUntilReady } = render(
+ ,
+ );
+ await waitUntilReady();
+ expect(lastFrame()).toMatchSnapshot();
+ });
+
+ it('SessionBrowserEmpty renders correctly', async () => {
+ const { lastFrame, waitUntilReady } = render();
+ await waitUntilReady();
+ expect(lastFrame()).toMatchSnapshot();
+ });
+});
diff --git a/packages/cli/src/ui/components/SessionBrowser/__snapshots__/SessionBrowserStates.test.tsx.snap b/packages/cli/src/ui/components/SessionBrowser/__snapshots__/SessionBrowserStates.test.tsx.snap
new file mode 100644
index 0000000000..e5939219cb
--- /dev/null
+++ b/packages/cli/src/ui/components/SessionBrowser/__snapshots__/SessionBrowserStates.test.tsx.snap
@@ -0,0 +1,18 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`SessionBrowser UI States > SessionBrowserEmpty renders correctly 1`] = `
+" No auto-saved conversations found.
+ Press q to exit
+"
+`;
+
+exports[`SessionBrowser UI States > SessionBrowserError renders correctly 1`] = `
+" Error: Test error message
+ Press q to exit
+"
+`;
+
+exports[`SessionBrowser UI States > SessionBrowserLoading renders correctly 1`] = `
+" Loading sessions…
+"
+`;
From bbd80c9393e11f4fb09e62761d153706793877a1 Mon Sep 17 00:00:00 2001
From: Abhi <43648792+abhipatel12@users.noreply.github.com>
Date: Fri, 13 Mar 2026 13:26:13 -0400
Subject: [PATCH 2/3] docs: overhaul subagents documentation and add /agents
command (#22345)
---
docs/core/subagents.md | 152 ++++++++++++++++++++++++++++++++-----
docs/reference/commands.md | 25 ++++++
2 files changed, 159 insertions(+), 18 deletions(-)
diff --git a/docs/core/subagents.md b/docs/core/subagents.md
index e937f28e77..659ed6d640 100644
--- a/docs/core/subagents.md
+++ b/docs/core/subagents.md
@@ -38,6 +38,34 @@ main agent calls the tool, it delegates the task to the subagent. Once the
subagent completes its task, it reports back to the main agent with its
findings.
+## How to use subagents
+
+You can use subagents through automatic delegation or by explicitly forcing them
+in your prompt.
+
+### Automatic delegation
+
+Gemini CLI's main agent is instructed to use specialized subagents when a task
+matches their expertise. For example, if you ask "How does the auth system
+work?", the main agent may decide to call the `codebase_investigator` subagent
+to perform the research.
+
+### Forcing a subagent (@ syntax)
+
+You can explicitly direct a task to a specific subagent by using the `@` symbol
+followed by the subagent's name at the beginning of your prompt. This is useful
+when you want to bypass the main agent's decision-making and go straight to a
+specialist.
+
+**Example:**
+
+```bash
+@codebase_investigator Map out the relationship between the AgentRegistry and the LocalAgentExecutor.
+```
+
+When you use the `@` syntax, the CLI injects a system note that nudges the
+primary model to use that specific subagent tool immediately.
+
## Built-in subagents
Gemini CLI comes with the following built-in subagents:
@@ -49,15 +77,17 @@ Gemini CLI comes with the following built-in subagents:
dependencies.
- **When to use:** "How does the authentication system work?", "Map out the
dependencies of the `AgentRegistry` class."
-- **Configuration:** Enabled by default. You can configure it in
- `settings.json`. Example (forcing a specific model):
+- **Configuration:** Enabled by default. You can override its settings in
+ `settings.json` under `agents.overrides`. Example (forcing a specific model
+ and increasing turns):
```json
{
- "experimental": {
- "codebaseInvestigatorSettings": {
- "enabled": true,
- "maxNumTurns": 20,
- "model": "gemini-2.5-pro"
+ "agents": {
+ "overrides": {
+ "codebase_investigator": {
+ "modelConfig": { "model": "gemini-3-flash-preview" },
+ "runConfig": { "maxTurns": 50 }
+ }
}
}
}
@@ -233,7 +263,7 @@ kind: local
tools:
- read_file
- grep_search
-model: gemini-2.5-pro
+model: gemini-3-flash-preview
temperature: 0.2
max_turns: 10
---
@@ -254,16 +284,102 @@ it yourself; just report it.
### Configuration schema
-| Field | Type | Required | Description |
-| :------------- | :----- | :------- | :------------------------------------------------------------------------------------------------------------------------ |
-| `name` | string | Yes | Unique identifier (slug) used as the tool name for the agent. Only lowercase letters, numbers, hyphens, and underscores. |
-| `description` | string | Yes | Short description of what the agent does. This is visible to the main agent to help it decide when to call this subagent. |
-| `kind` | string | No | `local` (default) or `remote`. |
-| `tools` | array | No | List of tool names this agent can use. If omitted, it may have access to a default set. |
-| `model` | string | No | Specific model to use (e.g., `gemini-2.5-pro`). Defaults to `inherit` (uses the main session model). |
-| `temperature` | number | No | Model temperature (0.0 - 2.0). |
-| `max_turns` | number | No | Maximum number of conversation turns allowed for this agent before it must return. Defaults to `15`. |
-| `timeout_mins` | number | No | Maximum execution time in minutes. Defaults to `5`. |
+| Field | Type | Required | Description |
+| :------------- | :----- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `name` | string | Yes | Unique identifier (slug) used as the tool name for the agent. Only lowercase letters, numbers, hyphens, and underscores. |
+| `description` | string | Yes | Short description of what the agent does. This is visible to the main agent to help it decide when to call this subagent. |
+| `kind` | string | No | `local` (default) or `remote`. |
+| `tools` | array | No | List of tool names this agent can use. Supports wildcards: `*` (all tools), `mcp_*` (all MCP tools), `mcp_server_*` (all tools from a server). **If omitted, it inherits all tools from the parent session.** |
+| `model` | string | No | Specific model to use (e.g., `gemini-3-preview`). Defaults to `inherit` (uses the main session model). |
+| `temperature` | number | No | Model temperature (0.0 - 2.0). Defaults to `1`. |
+| `max_turns` | number | No | Maximum number of conversation turns allowed for this agent before it must return. Defaults to `30`. |
+| `timeout_mins` | number | No | Maximum execution time in minutes. Defaults to `10`. |
+
+### Tool wildcards
+
+When defining `tools` for a subagent, you can use wildcards to quickly grant
+access to groups of tools:
+
+- `*`: Grant access to all available built-in and discovered tools.
+- `mcp_*`: Grant access to all tools from all connected MCP servers.
+- `mcp_my-server_*`: Grant access to all tools from a specific MCP server named
+ `my-server`.
+
+### Isolation and recursion protection
+
+Each subagent runs in its own isolated context loop. This means:
+
+- **Independent history:** The subagent's conversation history does not bloat
+ the main agent's context.
+- **Isolated tools:** The subagent only has access to the tools you explicitly
+ grant it.
+- **Recursion protection:** To prevent infinite loops and excessive token usage,
+ subagents **cannot** call other subagents. If a subagent is granted the `*`
+ tool wildcard, it will still be unable to see or invoke other agents.
+
+## Managing subagents
+
+You can manage subagents interactively using the `/agents` command or
+persistently via `settings.json`.
+
+### Interactive management (/agents)
+
+If you are in an interactive CLI session, you can use the `/agents` command to
+manage subagents without editing configuration files manually. This is the
+recommended way to quickly enable, disable, or re-configure agents on the fly.
+
+For a full list of sub-commands and usage, see the
+[`/agents` command reference](../reference/commands.md#agents).
+
+### Persistent configuration (settings.json)
+
+While the `/agents` command and agent definition files provide a starting point,
+you can use `settings.json` for global, persistent overrides. This is useful for
+enforcing specific models or execution limits across all sessions.
+
+#### `agents.overrides`
+
+Use this to enable or disable specific agents or override their run
+configurations.
+
+```json
+{
+ "agents": {
+ "overrides": {
+ "security-auditor": {
+ "enabled": false,
+ "runConfig": {
+ "maxTurns": 20,
+ "maxTimeMinutes": 10
+ }
+ }
+ }
+ }
+}
+```
+
+#### `modelConfigs.overrides`
+
+You can target specific subagents with custom model settings (like system
+instruction prefixes or specific safety settings) using the `overrideScope`
+field.
+
+```json
+{
+ "modelConfigs": {
+ "overrides": [
+ {
+ "match": { "overrideScope": "security-auditor" },
+ "modelConfig": {
+ "generateContentConfig": {
+ "temperature": 0.1
+ }
+ }
+ }
+ ]
+ }
+}
+```
### Optimizing your subagent
diff --git a/docs/reference/commands.md b/docs/reference/commands.md
index c7c25cba1e..e9383152d2 100644
--- a/docs/reference/commands.md
+++ b/docs/reference/commands.md
@@ -14,6 +14,31 @@ Slash commands provide meta-level control over the CLI itself.
- **Description:** Show version info. Share this information when filing issues.
+### `/agents`
+
+- **Description:** Manage local and remote subagents.
+- **Note:** This command is experimental and requires
+ `experimental.enableAgents: true` in your `settings.json`.
+- **Sub-commands:**
+ - **`list`**:
+ - **Description:** Lists all discovered agents, including built-in, local,
+ and remote agents.
+ - **Usage:** `/agents list`
+ - **`reload`** (alias: `refresh`):
+ - **Description:** Rescans agent directories (`~/.gemini/agents` and
+ `.gemini/agents`) and reloads the registry.
+ - **Usage:** `/agents reload`
+ - **`enable`**:
+ - **Description:** Enables a specific subagent.
+ - **Usage:** `/agents enable `
+ - **`disable`**:
+ - **Description:** Disables a specific subagent.
+ - **Usage:** `/agents disable `
+ - **`config`**:
+ - **Description:** Opens a configuration dialog for the specified agent to
+ adjust its model, temperature, or execution limits.
+ - **Usage:** `/agents config `
+
### `/auth`
- **Description:** Open a dialog that lets you change the authentication method.
From d368997ca3bdf9cc3e99b79cf73c894e278a6d2b Mon Sep 17 00:00:00 2001
From: Gaurav <39389231+gsquared94@users.noreply.github.com>
Date: Fri, 13 Mar 2026 10:49:33 -0700
Subject: [PATCH 3/3] test: add Object.create context regression test and tool
confirmation integration test (#22356)
---
.../browser-agent.confirmation.responses | 1 +
integration-tests/browser-agent.test.ts | 29 +++++++++++++++
.../core/src/agents/subagent-tool-wrapper.ts | 8 +---
packages/core/src/scheduler/policy.test.ts | 37 +++++++++++++++++++
4 files changed, 69 insertions(+), 6 deletions(-)
create mode 100644 integration-tests/browser-agent.confirmation.responses
diff --git a/integration-tests/browser-agent.confirmation.responses b/integration-tests/browser-agent.confirmation.responses
new file mode 100644
index 0000000000..4f645c6531
--- /dev/null
+++ b/integration-tests/browser-agent.confirmation.responses
@@ -0,0 +1 @@
+{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"write_file","args":{"file_path":"test.txt","content":"hello"}}},{"text":"I've successfully written \"hello\" to test.txt. The file has been created with the specified content."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]}
diff --git a/integration-tests/browser-agent.test.ts b/integration-tests/browser-agent.test.ts
index 0fdb3e717b..f9f07d4c9e 100644
--- a/integration-tests/browser-agent.test.ts
+++ b/integration-tests/browser-agent.test.ts
@@ -203,4 +203,33 @@ describe.skipIf(!chromeAvailable)('browser-agent', () => {
// Should successfully complete all operations
assertModelHasOutput(result);
});
+
+ it('should handle tool confirmation for write_file without crashing', async () => {
+ rig.setup('tool-confirmation', {
+ fakeResponsesPath: join(
+ __dirname,
+ 'browser-agent.confirmation.responses',
+ ),
+ settings: {
+ agents: {
+ browser_agent: {
+ headless: true,
+ sessionMode: 'isolated',
+ },
+ },
+ },
+ });
+
+ const run = await rig.runInteractive({ approvalMode: 'default' });
+
+ await run.type('Write hello to test.txt');
+ await run.type('\r');
+
+ await run.expectText('Allow', 15000);
+
+ await run.type('y');
+ await run.type('\r');
+
+ await run.expectText('successfully written', 15000);
+ });
});
diff --git a/packages/core/src/agents/subagent-tool-wrapper.ts b/packages/core/src/agents/subagent-tool-wrapper.ts
index ff64d4a03f..cf6d1e7112 100644
--- a/packages/core/src/agents/subagent-tool-wrapper.ts
+++ b/packages/core/src/agents/subagent-tool-wrapper.ts
@@ -10,7 +10,7 @@ import {
type ToolInvocation,
type ToolResult,
} from '../tools/tools.js';
-import type { Config } from '../config/config.js';
+
import { type AgentLoopContext } from '../config/agent-loop-context.js';
import type { AgentDefinition, AgentInputs } from './types.js';
import { LocalSubagentInvocation } from './local-invocation.js';
@@ -54,10 +54,6 @@ export class SubagentToolWrapper extends BaseDeclarativeTool<
);
}
- private get config(): Config {
- return this.context.config;
- }
-
/**
* Creates an invocation instance for executing the subagent.
*
@@ -89,7 +85,7 @@ export class SubagentToolWrapper extends BaseDeclarativeTool<
// Special handling for browser agent - needs async MCP setup
if (definition.name === BROWSER_AGENT_NAME) {
return new BrowserAgentInvocation(
- this.config,
+ this.context,
params,
effectiveMessageBus,
_toolName,
diff --git a/packages/core/src/scheduler/policy.test.ts b/packages/core/src/scheduler/policy.test.ts
index 750b14c2ed..e802a4b220 100644
--- a/packages/core/src/scheduler/policy.test.ts
+++ b/packages/core/src/scheduler/policy.test.ts
@@ -676,6 +676,43 @@ describe('policy.ts', () => {
}),
);
});
+
+ it('should work when context is created via Object.create (prototype chain)', async () => {
+ const mockConfig = {
+ setApprovalMode: vi.fn(),
+ } as unknown as Mocked;
+ const mockMessageBus = {
+ publish: vi.fn(),
+ } as unknown as Mocked;
+
+ const baseContext = {
+ config: mockConfig,
+ messageBus: mockMessageBus,
+ };
+ const protoContext: AgentLoopContext = Object.create(baseContext);
+
+ expect(Object.keys(protoContext)).toHaveLength(0);
+ expect(protoContext.config).toBe(mockConfig);
+ expect(protoContext.messageBus).toBe(mockMessageBus);
+
+ const tool = { name: 'test-tool' } as AnyDeclarativeTool;
+
+ await updatePolicy(
+ tool,
+ ToolConfirmationOutcome.ProceedAlways,
+ undefined,
+ protoContext,
+ mockMessageBus,
+ );
+
+ expect(mockMessageBus.publish).toHaveBeenCalledWith(
+ expect.objectContaining({
+ type: MessageBusType.UPDATE_POLICY,
+ toolName: 'test-tool',
+ persist: false,
+ }),
+ );
+ });
});
describe('getPolicyDenialError', () => {