Compare commits

..

2 Commits

Author SHA1 Message Date
matt korwel bb1ff6eb20 Update packages/cli/src/ui/components/MainContent.tsx
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-03-17 10:28:01 -07:00
mkorwel ade6dc0cc7 feat(ui): add fullscreen toggle for integrated and background shells 2026-03-16 01:04:36 -07:00
54 changed files with 807 additions and 1454 deletions
-5
View File
@@ -1157,11 +1157,6 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `true`
- **Requires restart:** Yes
- **`experimental.plannerSubagent`** (boolean):
- **Description:** Use the new planner subagent for plan mode.
- **Default:** `false`
- **Requires restart:** Yes
- **`experimental.enableAgents`** (boolean):
- **Description:** Enable local and remote subagents. Warning: Experimental
feature, uses YOLO mode for subagents
+3 -3
View File
@@ -60,7 +60,7 @@ command.
```toml
[[rule]]
toolName = "run_shell_command"
commandPrefix = "git"
commandPrefix = "git "
decision = "ask_user"
priority = 100
```
@@ -264,7 +264,7 @@ argsPattern = '"command":"(git|npm)'
# (Optional) A string or array of strings that a shell command must start with.
# This is syntactic sugar for `toolName = "run_shell_command"` and an `argsPattern`.
commandPrefix = "git"
commandPrefix = "git "
# (Optional) A regex to match against the entire shell command.
# This is also syntactic sugar for `toolName = "run_shell_command"`.
@@ -321,7 +321,7 @@ This rule will ask for user confirmation before executing any `git` command.
```toml
[[rule]]
toolName = "run_shell_command"
commandPrefix = "git"
commandPrefix = "git "
decision = "ask_user"
priority = 100
```
+1 -1
View File
@@ -303,7 +303,7 @@ export default tseslint.config(
},
},
{
files: ['./scripts/**/*.js', 'esbuild.config.js', 'packages/core/scripts/**/*.{js,mjs}'],
files: ['./scripts/**/*.js', 'esbuild.config.js'],
languageOptions: {
globals: {
...globals.node,
@@ -1,96 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestRig } from './test-helper.js';
import fs from 'node:fs';
import path from 'node:path';
describe('Planner Subagent E2E', () => {
let rig: TestRig;
beforeEach(() => {
rig = new TestRig();
});
afterEach(async () => await rig.cleanup());
// TODO(joshualitt): Re-enable after planner subagent is working end-to-end.
it.skip('should enter plan mode, create a plan, and exit plan mode', async () => {
// We'll use a real run to see if it works.
// We need to enable the planner agent and the required tools.
rig.setup('planner-subagent-e2e', {
settings: {
experimental: {
plan: true,
agents: true,
},
tools: {
core: [
'enter_plan_mode',
'exit_plan_mode',
'write_file',
'read_file',
'list_directory',
],
},
// Add policy to allow enter_plan_mode without confirmation in yolo mode
// Actually TestRig.run uses --approval-mode=yolo by default if not specified.
// But enter_plan_mode and exit_plan_mode might still ask if not explicitly allowed.
policy: {
rules: [
{ toolName: 'enter_plan_mode', decision: 'ALLOW' },
{ toolName: 'exit_plan_mode', decision: 'ALLOW' },
{ toolName: 'write_file', decision: 'ALLOW' },
],
},
},
});
rig.mkdir('plans');
rig.createFile('hello.ts', 'console.log("hello");');
// Prompt the agent to create a plan for refactoring hello.ts
// We expect:
// 1. Main agent calls enter_plan_mode
// 2. Planner subagent is launched
// 3. Planner subagent reads hello.ts
// 4. Planner subagent writes plans/refactor.md
// 5. Planner subagent calls exit_plan_mode
// 6. Main agent resumes
await rig.run({
stdin:
'Please create a refactoring plan for hello.ts to use a function. Put the plan in plans/refactor.md and then approve it.',
approvalMode: 'yolo',
timeout: 60000, // Planning can take a while
});
// 1. Verify enter_plan_mode was called
const enterPlanCalled = await rig.waitForToolCall('enter_plan_mode');
expect(enterPlanCalled).toBe(true);
// 2. Verify write_file was called for the plan
const writeFileCalled = await rig.waitForToolCall(
'write_file',
20000,
(args) => {
return args.includes('plans/refactor.md');
},
);
expect(writeFileCalled).toBe(true);
// 3. Verify exit_plan_mode was called
const exitPlanCalled = await rig.waitForToolCall('exit_plan_mode');
expect(exitPlanCalled).toBe(true);
// 4. Verify the plan file exists on disk
const planPath = path.join(rig.testDir!, 'plans/refactor.md');
expect(fs.existsSync(planPath)).toBe(true);
const planContent = fs.readFileSync(planPath, 'utf-8');
expect(planContent).toContain('hello.ts');
});
});
+6 -503
View File
@@ -3044,27 +3044,6 @@
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
"license": "BSD-3-Clause"
},
"node_modules/@puppeteer/browsers": {
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz",
"integrity": "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==",
"license": "Apache-2.0",
"dependencies": {
"debug": "^4.4.3",
"extract-zip": "^2.0.1",
"progress": "^2.0.3",
"proxy-agent": "^6.5.0",
"semver": "^7.7.4",
"tar-fs": "^3.1.1",
"yargs": "^17.7.2"
},
"bin": {
"browsers": "lib/cjs/main-cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
@@ -3789,12 +3768,6 @@
"node": ">= 10"
}
},
"node_modules/@tootallnate/quickjs-emscripten": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
"integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==",
"license": "MIT"
},
"node_modules/@ts-morph/common": {
"version": "0.12.3",
"resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.12.3.tgz",
@@ -5620,18 +5593,6 @@
"node": ">=12"
}
},
"node_modules/ast-types": {
"version": "0.13.4",
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
"integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.1"
},
"engines": {
"node": ">=4"
}
},
"node_modules/ast-v8-to-istanbul": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.8.tgz",
@@ -5724,20 +5685,6 @@
"typed-rest-client": "^1.8.4"
}
},
"node_modules/b4a": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz",
"integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==",
"license": "Apache-2.0",
"peerDependencies": {
"react-native-b4a": "*"
},
"peerDependenciesMeta": {
"react-native-b4a": {
"optional": true
}
}
},
"node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
@@ -5747,93 +5694,6 @@
"node": "18 || 20 || >=22"
}
},
"node_modules/bare-events": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
"license": "Apache-2.0",
"peerDependencies": {
"bare-abort-controller": "*"
},
"peerDependenciesMeta": {
"bare-abort-controller": {
"optional": true
}
}
},
"node_modules/bare-fs": {
"version": "4.5.5",
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.5.tgz",
"integrity": "sha512-XvwYM6VZqKoqDll8BmSww5luA5eflDzY0uEFfBJtFKe4PAAtxBjU3YIxzIBzhyaEQBy1VXEQBto4cpN5RZJw+w==",
"license": "Apache-2.0",
"dependencies": {
"bare-events": "^2.5.4",
"bare-path": "^3.0.0",
"bare-stream": "^2.6.4",
"bare-url": "^2.2.2",
"fast-fifo": "^1.3.2"
},
"engines": {
"bare": ">=1.16.0"
},
"peerDependencies": {
"bare-buffer": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
}
}
},
"node_modules/bare-os": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.7.1.tgz",
"integrity": "sha512-ebvMaS5BgZKmJlvuWh14dg9rbUI84QeV3WlWn6Ph6lFI8jJoh7ADtVTyD2c93euwbe+zgi0DVrl4YmqXeM9aIA==",
"license": "Apache-2.0",
"engines": {
"bare": ">=1.14.0"
}
},
"node_modules/bare-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
"integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
"license": "Apache-2.0",
"dependencies": {
"bare-os": "^3.0.1"
}
},
"node_modules/bare-stream": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.8.1.tgz",
"integrity": "sha512-bSeR8RfvbRwDpD7HWZvn8M3uYNDrk7m9DQjYOFkENZlXW8Ju/MPaqUPQq5LqJ3kyjEm07siTaAQ7wBKCU59oHg==",
"license": "Apache-2.0",
"dependencies": {
"streamx": "^2.21.0",
"teex": "^1.0.1"
},
"peerDependencies": {
"bare-buffer": "*",
"bare-events": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
},
"bare-events": {
"optional": true
}
}
},
"node_modules/bare-url": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz",
"integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==",
"license": "Apache-2.0",
"dependencies": {
"bare-path": "^3.0.0"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -5854,15 +5714,6 @@
],
"license": "MIT"
},
"node_modules/basic-ftp": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz",
"integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/before-after-hook": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz",
@@ -6261,32 +6112,6 @@
"node": ">=18"
}
},
"node_modules/chrome-devtools-mcp": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/chrome-devtools-mcp/-/chrome-devtools-mcp-0.19.0.tgz",
"integrity": "sha512-LfqjOxdUjWvCQrfeI5V3ZBJCUIDKGNmexSbSAgsrjVggN4X1OSObLxleSlX2zwcXRZYxqy209cww0MXcXuN1zw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"chrome-devtools-mcp": "build/src/index.js"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23"
}
},
"node_modules/chromium-bidi": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz",
"integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==",
"license": "Apache-2.0",
"dependencies": {
"mitt": "^3.0.1",
"zod": "^3.24.1"
},
"peerDependencies": {
"devtools-protocol": "*"
}
},
"node_modules/cjs-module-lexer": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz",
@@ -7129,20 +6954,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/degenerator": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz",
"integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==",
"license": "MIT",
"dependencies": {
"ast-types": "^0.13.4",
"escodegen": "^2.1.0",
"esprima": "^4.0.1"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -7402,12 +7213,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/devtools-protocol": {
"version": "0.0.1581282",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz",
"integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==",
"license": "BSD-3-Clause"
},
"node_modules/dezalgo": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
@@ -7963,27 +7768,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/escodegen": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
"integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
"license": "BSD-2-Clause",
"dependencies": {
"esprima": "^4.0.1",
"estraverse": "^5.2.0",
"esutils": "^2.0.2"
},
"bin": {
"escodegen": "bin/escodegen.js",
"esgenerate": "bin/esgenerate.js"
},
"engines": {
"node": ">=6.0"
},
"optionalDependencies": {
"source-map": "~0.6.1"
}
},
"node_modules/eslint": {
"version": "9.29.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.29.0.tgz",
@@ -8344,6 +8128,7 @@
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
@@ -8362,6 +8147,7 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.10.0"
@@ -8413,15 +8199,6 @@
"uuid": "dist/bin/uuid"
}
},
"node_modules/events-universal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
"integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
"license": "Apache-2.0",
"dependencies": {
"bare-events": "^2.7.0"
}
},
"node_modules/eventsource": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
@@ -8629,12 +8406,6 @@
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"license": "MIT"
},
"node_modules/fast-fifo": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
"license": "MIT"
},
"node_modules/fast-glob": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
@@ -9277,29 +9048,6 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/get-uri": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz",
"integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==",
"license": "MIT",
"dependencies": {
"basic-ftp": "^5.0.2",
"data-uri-to-buffer": "^6.0.2",
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/get-uri/node_modules/data-uri-to-buffer": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
"integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/glob": {
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-12.0.0.tgz",
@@ -9927,6 +9675,7 @@
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
"dev": true,
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.0",
@@ -12023,12 +11772,6 @@
"node": ">= 18"
}
},
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"license": "MIT"
},
"node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
@@ -12229,15 +11972,6 @@
"node": ">= 0.6"
}
},
"node_modules/netmask": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz",
"integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==",
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/node-addon-api": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz",
@@ -12941,38 +12675,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pac-proxy-agent": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
"integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==",
"license": "MIT",
"dependencies": {
"@tootallnate/quickjs-emscripten": "^0.23.0",
"agent-base": "^7.1.2",
"debug": "^4.3.4",
"get-uri": "^6.0.1",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.6",
"pac-resolver": "^7.0.1",
"socks-proxy-agent": "^8.0.5"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/pac-resolver": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz",
"integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==",
"license": "MIT",
"dependencies": {
"degenerator": "^5.0.0",
"netmask": "^2.0.2"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/package-json": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/package-json/-/package-json-10.0.1.tgz",
@@ -13443,15 +13145,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/prompts": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
@@ -13557,40 +13250,6 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-agent": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz",
"integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "^4.3.4",
"http-proxy-agent": "^7.0.1",
"https-proxy-agent": "^7.0.6",
"lru-cache": "^7.14.1",
"pac-proxy-agent": "^7.1.0",
"proxy-from-env": "^1.1.0",
"socks-proxy-agent": "^8.0.5"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/proxy-agent/node_modules/lru-cache": {
"version": "7.18.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/psl": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
@@ -13644,45 +13303,6 @@
"node": ">=6"
}
},
"node_modules/puppeteer-core": {
"version": "24.39.0",
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.39.0.tgz",
"integrity": "sha512-SzIxz76Kgu17HUIi57HOejPiN0JKa9VCd2GcPY1sAh6RA4BzGZarFQdOYIYrBdUVbtyH7CrDb9uhGEwVXK/YNA==",
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "2.13.0",
"chromium-bidi": "14.0.0",
"debug": "^4.4.3",
"devtools-protocol": "0.0.1581282",
"typed-query-selector": "^2.12.1",
"webdriver-bidi-protocol": "0.4.1",
"ws": "^8.19.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/puppeteer-core/node_modules/ws": {
"version": "8.19.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/qs": {
"version": "6.14.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
@@ -14645,9 +14265,9 @@
}
},
"node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -14978,54 +14598,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
"license": "MIT",
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/socks": {
"version": "2.8.7",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
"integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
"license": "MIT",
"dependencies": {
"ip-address": "^10.0.1",
"smart-buffer": "^4.2.0"
},
"engines": {
"node": ">= 10.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/socks-proxy-agent": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
"integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "^4.3.4",
"socks": "^2.8.3"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"license": "BSD-3-Clause",
"optional": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -15154,17 +14726,6 @@
"integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==",
"license": "MIT"
},
"node_modules/streamx": {
"version": "2.23.0",
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
"integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
"license": "MIT",
"dependencies": {
"events-universal": "^1.0.0",
"fast-fifo": "^1.3.2",
"text-decoder": "^1.1.0"
}
},
"node_modules/strict-event-emitter": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz",
@@ -15762,32 +15323,6 @@
"node": ">=8"
}
},
"node_modules/tar-fs": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz",
"integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==",
"license": "MIT",
"dependencies": {
"pump": "^3.0.0",
"tar-stream": "^3.1.5"
},
"optionalDependencies": {
"bare-fs": "^4.0.1",
"bare-path": "^3.0.0"
}
},
"node_modules/tar-stream": {
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz",
"integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==",
"license": "MIT",
"dependencies": {
"b4a": "^1.6.4",
"bare-fs": "^4.5.5",
"fast-fifo": "^1.2.0",
"streamx": "^2.15.0"
}
},
"node_modules/teeny-request": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz",
@@ -15843,15 +15378,6 @@
"node": ">= 6"
}
},
"node_modules/teex": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
"integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
"license": "MIT",
"dependencies": {
"streamx": "^2.12.5"
}
},
"node_modules/terminal-link": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-4.0.0.tgz",
@@ -15884,15 +15410,6 @@
"node": ">=18"
}
},
"node_modules/text-decoder": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
"integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==",
"license": "Apache-2.0",
"dependencies": {
"b4a": "^1.6.4"
}
},
"node_modules/text-hex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
@@ -16370,12 +15887,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/typed-query-selector": {
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.1.tgz",
"integrity": "sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA==",
"license": "MIT"
},
"node_modules/typed-rest-client": {
"version": "1.8.11",
"resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz",
@@ -16847,12 +16358,6 @@
}
}
},
"node_modules/webdriver-bidi-protocol": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz",
"integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==",
"license": "Apache-2.0"
},
"node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
@@ -17750,7 +17255,6 @@
"open": "^10.1.2",
"picomatch": "^4.0.1",
"proper-lockfile": "^4.1.2",
"puppeteer-core": "^24.0.0",
"read-package-up": "^11.0.0",
"shell-quote": "^1.8.3",
"simple-git": "^3.28.0",
@@ -17769,7 +17273,6 @@
"@types/fast-levenshtein": "^0.0.4",
"@types/js-yaml": "^4.0.9",
"@types/picomatch": "^4.0.1",
"chrome-devtools-mcp": "^0.19.0",
"msw": "^2.3.4",
"typescript": "^5.3.3",
"vitest": "^3.1.1"
+1 -1
View File
@@ -104,7 +104,7 @@ export class AddMemoryCommand implements Command {
const signal = abortController.signal;
await tool.buildAndExecute(result.toolArgs, signal, undefined, {
sanitizationConfig: DEFAULT_SANITIZATION_CONFIG,
sandboxManager: loopContext.sandboxManager,
sandboxManager: context.config.sandboxManager,
});
await refreshMemory(context.config);
return {
@@ -23,7 +23,6 @@ import {
type Storage,
NoopSandboxManager,
type ToolRegistry,
type SandboxManager,
} from '@google/gemini-cli-core';
import { createMockMessageBus } from '@google/gemini-cli-core/src/test-utils/mock-message-bus.js';
import { expect, vi } from 'vitest';
@@ -100,8 +99,7 @@ export function createMockConfig(
getGitService: vi.fn(),
validatePathAccess: vi.fn().mockReturnValue(undefined),
getShellExecutionConfig: vi.fn().mockReturnValue({
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
sandboxManager: new NoopSandboxManager() as unknown as SandboxManager,
sandboxManager: new NoopSandboxManager(),
sanitizationConfig: {
allowedEnvironmentVariables: [],
blockedEnvironmentVariables: [],
@@ -16,7 +16,11 @@ import {
} from 'vitest';
import { handleInstall, installCommand } from './install.js';
import yargs from 'yargs';
import * as core from '@google/gemini-cli-core';
import {
debugLogger,
type FolderTrustDiscoveryService,
type GeminiCLIExtension,
} from '@google/gemini-cli-core';
import {
ExtensionManager,
type inferInstallMetadata,
@@ -52,7 +56,7 @@ const mockIsWorkspaceTrusted: Mock<typeof isWorkspaceTrusted> = vi.hoisted(() =>
const mockLoadTrustedFolders: Mock<typeof loadTrustedFolders> = vi.hoisted(() =>
vi.fn(),
);
const mockDiscover: Mock<typeof core.FolderTrustDiscoveryService.discover> =
const mockDiscover: Mock<typeof FolderTrustDiscoveryService.discover> =
vi.hoisted(() => vi.fn());
vi.mock('../../config/extensions/consent.js', () => ({
@@ -117,8 +121,8 @@ describe('handleInstall', () => {
let processSpy: MockInstance;
beforeEach(() => {
debugLogSpy = vi.spyOn(core.debugLogger, 'log');
debugErrorSpy = vi.spyOn(core.debugLogger, 'error');
debugLogSpy = vi.spyOn(debugLogger, 'log');
debugErrorSpy = vi.spyOn(debugLogger, 'error');
processSpy = vi
.spyOn(process, 'exit')
.mockImplementation(() => undefined as never);
@@ -171,8 +175,8 @@ describe('handleInstall', () => {
});
function createMockExtension(
overrides: Partial<core.GeminiCLIExtension> = {},
): core.GeminiCLIExtension {
overrides: Partial<GeminiCLIExtension> = {},
): GeminiCLIExtension {
return {
name: 'mock-extension',
version: '1.0.0',
+38 -38
View File
@@ -21,6 +21,11 @@ import {
type MCPServerConfig,
type GeminiCLIExtension,
Storage,
PolicyDecision,
TelemetryTarget,
loadServerHierarchicalMemory,
createPolicyEngineConfig,
type Config as CoreConfig,
} from '@google/gemini-cli-core';
import { loadCliConfig, parseArguments, type CliArgs } from './config.js';
import {
@@ -28,7 +33,6 @@ import {
type MergedSettings,
createTestMergedSettings,
} from './settings.js';
import * as ServerConfig from '@google/gemini-cli-core';
import { isWorkspaceTrusted } from './trustedFolders.js';
import { ExtensionManager } from './extension-manager.js';
@@ -99,9 +103,9 @@ vi.mock('read-package-up', () => ({
}));
vi.mock('@google/gemini-cli-core', async () => {
const actualServer = await vi.importActual<typeof ServerConfig>(
'@google/gemini-cli-core',
);
const actualServer = await vi.importActual<
typeof import('@google/gemini-cli-core')
>('@google/gemini-cli-core');
return {
...actualServer,
IdeClient: {
@@ -146,8 +150,8 @@ vi.mock('@google/gemini-cli-core', async () => {
createPolicyEngineConfig: vi.fn(async () => ({
rules: [],
checkers: [],
defaultDecision: ServerConfig.PolicyDecision.ASK_USER,
approvalMode: ServerConfig.ApprovalMode.DEFAULT,
defaultDecision: PolicyDecision.ASK_USER,
approvalMode: ApprovalMode.DEFAULT,
})),
getAdminErrorMessage: vi.fn(
(_feature) =>
@@ -846,7 +850,7 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
]);
const argv = await parseArguments(createTestMergedSettings());
await loadCliConfig(settings, 'session-id', argv);
expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith(
expect(loadServerHierarchicalMemory).toHaveBeenCalledWith(
expect.any(String),
[],
expect.any(Object),
@@ -874,7 +878,7 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
const argv = await parseArguments(settings);
await loadCliConfig(settings, 'session-id', argv);
expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith(
expect(loadServerHierarchicalMemory).toHaveBeenCalledWith(
expect.any(String),
[includeDir],
expect.any(Object),
@@ -901,7 +905,7 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
const argv = await parseArguments(settings);
await loadCliConfig(settings, 'session-id', argv);
expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith(
expect(loadServerHierarchicalMemory).toHaveBeenCalledWith(
expect.any(String),
[],
expect.any(Object),
@@ -2500,7 +2504,7 @@ describe('loadCliConfig approval mode', () => {
'test-session',
argv,
);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
});
it('should set YOLO approval mode when --yolo flag is used', async () => {
@@ -2511,7 +2515,7 @@ describe('loadCliConfig approval mode', () => {
'test-session',
argv,
);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
expect(config.getApprovalMode()).toBe(ApprovalMode.YOLO);
});
it('should set YOLO approval mode when -y flag is used', async () => {
@@ -2522,7 +2526,7 @@ describe('loadCliConfig approval mode', () => {
'test-session',
argv,
);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
expect(config.getApprovalMode()).toBe(ApprovalMode.YOLO);
});
it('should set DEFAULT approval mode when --approval-mode=default', async () => {
@@ -2533,7 +2537,7 @@ describe('loadCliConfig approval mode', () => {
'test-session',
argv,
);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
});
it('should set AUTO_EDIT approval mode when --approval-mode=auto_edit', async () => {
@@ -2544,7 +2548,7 @@ describe('loadCliConfig approval mode', () => {
'test-session',
argv,
);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.AUTO_EDIT);
expect(config.getApprovalMode()).toBe(ApprovalMode.AUTO_EDIT);
});
it('should set YOLO approval mode when --approval-mode=yolo', async () => {
@@ -2555,7 +2559,7 @@ describe('loadCliConfig approval mode', () => {
'test-session',
argv,
);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
expect(config.getApprovalMode()).toBe(ApprovalMode.YOLO);
});
it('should prioritize --approval-mode over --yolo when both would be valid (but validation prevents this)', async () => {
@@ -2570,7 +2574,7 @@ describe('loadCliConfig approval mode', () => {
'test-session',
argv,
);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
});
it('should fall back to --yolo behavior when --approval-mode is not set', async () => {
@@ -2581,7 +2585,7 @@ describe('loadCliConfig approval mode', () => {
'test-session',
argv,
);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
expect(config.getApprovalMode()).toBe(ApprovalMode.YOLO);
});
it('should set Plan approval mode when --approval-mode=plan is used and experimental.plan is enabled', async () => {
@@ -2593,7 +2597,7 @@ describe('loadCliConfig approval mode', () => {
},
});
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.PLAN);
expect(config.getApprovalMode()).toBe(ApprovalMode.PLAN);
});
it('should ignore "yolo" in settings.tools.approvalMode and fall back to DEFAULT', async () => {
@@ -2606,7 +2610,7 @@ describe('loadCliConfig approval mode', () => {
});
const argv = await parseArguments(settings);
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
});
it('should throw error when --approval-mode=plan is used but experimental.plan is disabled', async () => {
@@ -2663,7 +2667,7 @@ describe('loadCliConfig approval mode', () => {
'test-session',
argv,
);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
});
it('should override --approval-mode=auto_edit to DEFAULT', async () => {
@@ -2674,7 +2678,7 @@ describe('loadCliConfig approval mode', () => {
'test-session',
argv,
);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
});
it('should override --yolo flag to DEFAULT', async () => {
@@ -2685,7 +2689,7 @@ describe('loadCliConfig approval mode', () => {
'test-session',
argv,
);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
});
it('should remain DEFAULT when --approval-mode=default', async () => {
@@ -2696,7 +2700,7 @@ describe('loadCliConfig approval mode', () => {
'test-session',
argv,
);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEFAULT);
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
});
});
@@ -2708,9 +2712,7 @@ describe('loadCliConfig approval mode', () => {
});
const argv = await parseArguments(settings);
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getApprovalMode()).toBe(
ServerConfig.ApprovalMode.AUTO_EDIT,
);
expect(config.getApprovalMode()).toBe(ApprovalMode.AUTO_EDIT);
});
it('should prioritize --approval-mode flag over settings', async () => {
@@ -2720,9 +2722,7 @@ describe('loadCliConfig approval mode', () => {
});
const argv = await parseArguments(settings);
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getApprovalMode()).toBe(
ServerConfig.ApprovalMode.AUTO_EDIT,
);
expect(config.getApprovalMode()).toBe(ApprovalMode.AUTO_EDIT);
});
it('should prioritize --yolo flag over settings', async () => {
@@ -2732,7 +2732,7 @@ describe('loadCliConfig approval mode', () => {
});
const argv = await parseArguments(settings);
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
expect(config.getApprovalMode()).toBe(ApprovalMode.YOLO);
});
it('should respect plan mode from settings when experimental.plan is enabled', async () => {
@@ -2743,7 +2743,7 @@ describe('loadCliConfig approval mode', () => {
});
const argv = await parseArguments(settings);
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.PLAN);
expect(config.getApprovalMode()).toBe(ApprovalMode.PLAN);
});
it('should throw error if plan mode is in settings but experimental.plan is disabled', async () => {
@@ -2841,7 +2841,7 @@ describe('loadCliConfig fileFiltering', () => {
>;
const testCases: Array<{
property: keyof FileFilteringSettings;
getter: (config: ServerConfig.Config) => boolean;
getter: (config: CoreConfig) => boolean;
value: boolean;
}> = [
{
@@ -3085,7 +3085,7 @@ describe('Telemetry configuration via environment variables', () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments(createTestMergedSettings());
const settings = createTestMergedSettings({
telemetry: { target: ServerConfig.TelemetryTarget.LOCAL },
telemetry: { target: TelemetryTarget.LOCAL },
});
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getTelemetryTarget()).toBe('gcp');
@@ -3096,7 +3096,7 @@ describe('Telemetry configuration via environment variables', () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments(createTestMergedSettings());
const settings = createTestMergedSettings({
telemetry: { target: ServerConfig.TelemetryTarget.GCP },
telemetry: { target: TelemetryTarget.GCP },
});
await expect(loadCliConfig(settings, 'test-session', argv)).rejects.toThrow(
/Invalid telemetry configuration: .*Invalid telemetry target/i,
@@ -3174,7 +3174,7 @@ describe('Telemetry configuration via environment variables', () => {
process.argv = ['node', 'script.js'];
const argv = await parseArguments(createTestMergedSettings());
const settings = createTestMergedSettings({
telemetry: { target: ServerConfig.TelemetryTarget.LOCAL },
telemetry: { target: TelemetryTarget.LOCAL },
});
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getTelemetryTarget()).toBe('local');
@@ -3298,7 +3298,7 @@ describe('Policy Engine Integration in loadCliConfig', () => {
await loadCliConfig(settings, 'test-session', argv);
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect(createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
tools: expect.objectContaining({
allowed: expect.arrayContaining(['cli-tool']),
@@ -3319,7 +3319,7 @@ describe('Policy Engine Integration in loadCliConfig', () => {
await loadCliConfig(settings, 'test-session', argv);
// In non-interactive mode, only ask_user is excluded by default
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect(createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
tools: expect.objectContaining({
exclude: expect.arrayContaining([ASK_USER_TOOL_NAME]),
@@ -3341,7 +3341,7 @@ describe('Policy Engine Integration in loadCliConfig', () => {
await loadCliConfig(settings, 'test-session', argv);
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect(createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
policyPaths: ['/path/to/policy1.toml', '/path/to/policy2.toml'],
}),
+9 -1
View File
@@ -97,6 +97,7 @@ export interface CliArgs {
rawOutput: boolean | undefined;
acceptRawOutputRisk: boolean | undefined;
isCommand: boolean | undefined;
fullscreen?: boolean;
}
/**
@@ -297,6 +298,10 @@ export async function parseArguments(
.option('accept-raw-output-risk', {
type: 'boolean',
description: 'Suppress the security warning when using --raw-output.',
})
.option('fullscreen', {
type: 'boolean',
description: 'Enable experimental fullscreen mode.',
}),
)
// Register MCP subcommands
@@ -435,6 +440,10 @@ export async function loadCliConfig(
process.env['GEMINI_SANDBOX'] = 'true';
}
if (argv.fullscreen !== undefined) {
settings.experimental.fullscreen = argv.fullscreen;
}
const memoryImportFormat = settings.context?.importFormat || 'tree';
const includeDirectoryTree = settings.context?.includeDirectoryTree ?? true;
@@ -802,7 +811,6 @@ export async function loadCliConfig(
extensionLoader: extensionManager,
extensionRegistryURI,
enableExtensionReloading: settings.experimental?.extensionReloading,
plannerSubagent: settings.experimental?.plannerSubagent,
enableAgents: settings.experimental?.enableAgents,
plan: settings.experimental?.plan,
tracker: settings.experimental?.taskTracker,
+10 -9
View File
@@ -1833,15 +1833,6 @@ const SETTINGS_SCHEMA = {
},
},
},
plannerSubagent: {
type: 'boolean',
label: 'Planner Subagent',
category: 'Experimental',
requiresRestart: true,
default: false,
description: 'Use the new planner subagent for plan mode.',
showInDialog: false,
},
enableAgents: {
type: 'boolean',
label: 'Enable Agents',
@@ -1937,6 +1928,16 @@ const SETTINGS_SCHEMA = {
description: 'Enable Plan Mode.',
showInDialog: true,
},
fullscreen: {
type: 'boolean',
label: 'Fullscreen Mode',
category: 'Experimental',
requiresRestart: false,
default: false,
description:
'Enable experimental fullscreen mode for integrated and background shells (toggle with ctrl+7).',
showInDialog: true,
},
taskTracker: {
type: 'boolean',
label: 'Task Tracker',
@@ -8,7 +8,11 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
import * as path from 'node:path';
import { loadCliConfig, type CliArgs } from './config.js';
import { createTestMergedSettings } from './settings.js';
import * as ServerConfig from '@google/gemini-cli-core';
import {
isHeadlessMode,
Storage,
createPolicyEngineConfig,
} from '@google/gemini-cli-core';
import { isWorkspaceTrusted } from './trustedFolders.js';
import * as Policy from './policy.js';
@@ -21,9 +25,9 @@ const mockCheckIntegrity = vi.fn();
const mockAcceptIntegrity = vi.fn();
vi.mock('@google/gemini-cli-core', async () => {
const actual = await vi.importActual<typeof ServerConfig>(
'@google/gemini-cli-core',
);
const actual = await vi.importActual<
typeof import('@google/gemini-cli-core')
>('@google/gemini-cli-core');
return {
...actual,
loadServerHierarchicalMemory: vi.fn().mockResolvedValue({
@@ -61,11 +65,11 @@ describe('Workspace-Level Policy CLI Integration', () => {
hash: 'test-hash',
fileCount: 1,
});
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(false);
vi.mocked(isHeadlessMode).mockReturnValue(false);
});
it('should have getWorkspacePoliciesDir on Storage class', () => {
const storage = new ServerConfig.Storage(MOCK_CWD);
const storage = new Storage(MOCK_CWD);
expect(storage.getWorkspacePoliciesDir).toBeDefined();
expect(typeof storage.getWorkspacePoliciesDir).toBe('function');
});
@@ -81,7 +85,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
await loadCliConfig(settings, 'test-session', argv, { cwd: MOCK_CWD });
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect(createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
workspacePoliciesDir: expect.stringContaining(
path.join('.gemini', 'policies'),
@@ -102,7 +106,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
await loadCliConfig(settings, 'test-session', argv, { cwd: MOCK_CWD });
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect(createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
workspacePoliciesDir: undefined,
}),
@@ -126,7 +130,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
await loadCliConfig(settings, 'test-session', argv, { cwd: MOCK_CWD });
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect(createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
workspacePoliciesDir: undefined,
}),
@@ -144,7 +148,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
hash: 'new-hash',
fileCount: 1,
});
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(true); // Non-interactive
vi.mocked(isHeadlessMode).mockReturnValue(true); // Non-interactive
const settings = createTestMergedSettings();
const argv = { prompt: 'do something' } as unknown as CliArgs;
@@ -156,7 +160,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
MOCK_CWD,
'new-hash',
);
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect(createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
workspacePoliciesDir: expect.stringContaining(
path.join('.gemini', 'policies'),
@@ -176,7 +180,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
hash: 'new-hash',
fileCount: 1,
});
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(false); // Interactive
vi.mocked(isHeadlessMode).mockReturnValue(false); // Interactive
const settings = createTestMergedSettings();
const argv = {
@@ -194,7 +198,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
MOCK_CWD,
'new-hash',
);
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect(createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
workspacePoliciesDir: expect.stringContaining(
path.join('.gemini', 'policies'),
@@ -214,7 +218,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
hash: 'new-hash',
fileCount: 5,
});
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(false); // Interactive
vi.mocked(isHeadlessMode).mockReturnValue(false); // Interactive
const settings = createTestMergedSettings();
const argv = { query: 'test' } as unknown as CliArgs;
@@ -230,7 +234,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
'new-hash',
);
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect(createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
workspacePoliciesDir: expect.stringContaining(
path.join('.gemini', 'policies'),
@@ -255,7 +259,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
hash: 'new-hash',
fileCount: 1,
});
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(false); // Interactive
vi.mocked(isHeadlessMode).mockReturnValue(false); // Interactive
const settings = createTestMergedSettings();
const argv = {
@@ -273,7 +277,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
policyDir: expect.stringContaining(path.join('.gemini', 'policies')),
newHash: 'new-hash',
});
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect(createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
workspacePoliciesDir: undefined,
}),
+35
View File
@@ -1148,6 +1148,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
activeBackgroundShellPid,
setIsBackgroundShellListOpen,
isBackgroundShellListOpen,
isBackgroundShellFullscreen,
setIsBackgroundShellFullscreen,
setActiveBackgroundShellPid,
backgroundShellHeight,
} = useBackgroundShellManager({
@@ -1160,6 +1162,9 @@ Logging in with Google... Restarting Gemini CLI to continue.
terminalHeight,
});
const [isForegroundShellFullscreen, setIsForegroundShellFullscreen] =
useState(false);
setIsBackgroundShellListOpenRef.current = setIsBackgroundShellListOpen;
const lastOutputTimeRef = useRef(0);
@@ -1840,6 +1845,29 @@ Logging in with Google... Restarting Gemini CLI to continue.
setIsBackgroundShellListOpen(true);
}
return true;
} else if (keyMatchers[Command.TOGGLE_SHELL_FULLSCREEN](key)) {
if (
settings.merged.experimental.fullscreen &&
backgroundShells.size > 0 &&
isBackgroundShellVisible
) {
setIsBackgroundShellFullscreen((prev) => {
const newValue = !prev;
if (newValue) {
setEmbeddedShellFocused(true);
}
return newValue;
});
} else if (settings.merged.experimental.fullscreen && activePtyId) {
setIsForegroundShellFullscreen((prev) => {
const newValue = !prev;
if (newValue) {
setEmbeddedShellFocused(true);
}
return newValue;
});
}
return true;
}
return false;
},
@@ -1870,7 +1898,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
lastOutputTimeRef,
showTransientMessage,
settings.merged.general.devtools,
settings.merged.experimental.fullscreen,
showErrorDetails,
setIsBackgroundShellFullscreen,
setIsForegroundShellFullscreen,
triggerExpandHint,
keyMatchers,
isHelpDismissKey,
@@ -2298,6 +2329,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
backgroundShells,
activeBackgroundShellPid,
backgroundShellHeight,
isBackgroundShellFullscreen,
isForegroundShellFullscreen,
isBackgroundShellListOpen,
adminSettingsChanged,
newAgents,
@@ -2424,6 +2457,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
config,
settingsNonce,
backgroundShellHeight,
isBackgroundShellFullscreen,
isForegroundShellFullscreen,
isBackgroundShellListOpen,
activeBackgroundShellPid,
backgroundShells,
@@ -33,6 +33,7 @@ import {
type RadioSelectItem,
} from './shared/RadioButtonSelect.js';
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
import { useSettings } from '../contexts/SettingsContext.js';
interface BackgroundShellDisplayProps {
shells: Map<number, BackgroundShell>;
@@ -70,6 +71,7 @@ export const BackgroundShellDisplay = ({
isListOpenProp,
}: BackgroundShellDisplayProps) => {
const keyMatchers = useKeyMatchers();
const settings = useSettings();
const {
dismissBackgroundShell,
setActiveBackgroundShellPid,
@@ -178,6 +180,10 @@ export const BackgroundShellDisplay = ({
return false;
}
if (keyMatchers[Command.TOGGLE_SHELL_FULLSCREEN](key)) {
return false;
}
if (keyMatchers[Command.KILL_BACKGROUND_SHELL](key)) {
void dismissBackgroundShell(activeShell.pid);
return true;
@@ -207,6 +213,9 @@ export const BackgroundShellDisplay = ({
{ label: 'Close', command: Command.TOGGLE_BACKGROUND_SHELL },
{ label: 'Kill', command: Command.KILL_BACKGROUND_SHELL },
{ label: 'List', command: Command.TOGGLE_BACKGROUND_SHELL_LIST },
...(settings.merged.experimental.fullscreen
? [{ label: 'Fullscreen', command: Command.TOGGLE_SHELL_FULLSCREEN }]
: []),
];
const helpTextStr = helpTextParts
@@ -48,6 +48,7 @@ interface HistoryItemDisplayProps {
isExpandable?: boolean;
isFirstThinking?: boolean;
isFirstAfterThinking?: boolean;
isFullscreen?: boolean;
}
export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
@@ -60,6 +61,7 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
isExpandable,
isFirstThinking = false,
isFirstAfterThinking = false,
isFullscreen = false,
}) => {
const settings = useSettings();
const inlineThinkingMode = getInlineThinkingMode(settings);
@@ -73,7 +75,8 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
flexDirection="column"
key={itemForDisplay.id}
width={terminalWidth}
marginTop={needsTopMarginAfterThinking ? 1 : 0}
marginTop={isFullscreen ? 0 : needsTopMarginAfterThinking ? 1 : 0}
paddingTop={isFullscreen ? 1 : 0}
>
{/* Render standard message types */}
{itemForDisplay.type === 'thinking' && inlineThinkingMode !== 'off' && (
@@ -197,9 +200,10 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
toolCalls={itemForDisplay.tools}
availableTerminalHeight={availableTerminalHeight}
terminalWidth={terminalWidth}
borderTop={itemForDisplay.borderTop}
borderBottom={itemForDisplay.borderBottom}
borderTop={isFullscreen ? true : itemForDisplay.borderTop}
borderBottom={isFullscreen ? true : itemForDisplay.borderBottom}
isExpandable={isExpandable}
isFullscreen={isFullscreen}
/>
)}
{itemForDisplay.type === 'compression' && (
+135 -12
View File
@@ -49,9 +49,14 @@ export const MainContent = () => {
mainAreaWidth,
staticAreaMaxItemHeight,
cleanUiDetailsVisible,
isForegroundShellFullscreen,
terminalHeight,
activePtyId,
} = uiState;
const showHeaderDetails = cleanUiDetailsVisible;
const fullscreenHeight = Math.max(terminalHeight - FOOTER_RESERVED_HEIGHT, 5);
const lastUserPromptIndex = useMemo(() => {
for (let i = uiState.history.length - 1; i >= 0; i--) {
const type = uiState.history[i].type;
@@ -90,9 +95,11 @@ export const MainContent = () => {
<MemoizedHistoryItemDisplay
terminalWidth={mainAreaWidth}
availableTerminalHeight={
uiState.constrainHeight || !isExpandable
? staticAreaMaxItemHeight
: undefined
isForegroundShellFullscreen
? fullscreenHeight
: uiState.constrainHeight || !isExpandable
? staticAreaMaxItemHeight
: undefined
}
availableTerminalHeightGemini={MAX_GEMINI_MESSAGE_LINES}
key={item.id}
@@ -102,6 +109,7 @@ export const MainContent = () => {
isExpandable={isExpandable}
isFirstThinking={isFirstThinking}
isFirstAfterThinking={isFirstAfterThinking}
isFullscreen={isForegroundShellFullscreen}
/>
),
),
@@ -111,6 +119,8 @@ export const MainContent = () => {
staticAreaMaxItemHeight,
uiState.slashCommands,
uiState.constrainHeight,
isForegroundShellFullscreen,
fullscreenHeight,
],
);
@@ -141,7 +151,11 @@ export const MainContent = () => {
<HistoryItemDisplay
key={i}
availableTerminalHeight={
uiState.constrainHeight ? staticAreaMaxItemHeight : undefined
isForegroundShellFullscreen
? fullscreenHeight
: uiState.constrainHeight
? staticAreaMaxItemHeight
: undefined
}
terminalWidth={mainAreaWidth}
item={{ ...item, id: 0 }}
@@ -149,6 +163,7 @@ export const MainContent = () => {
isExpandable={true}
isFirstThinking={isFirstThinking}
isFirstAfterThinking={isFirstAfterThinking}
isFullscreen={isForegroundShellFullscreen}
/>
);
})}
@@ -165,11 +180,41 @@ export const MainContent = () => {
showConfirmationQueue,
confirmingTool,
uiState.history,
isForegroundShellFullscreen,
fullscreenHeight,
],
);
const virtualizedData = useMemo(
() => [
const virtualizedData = useMemo(() => {
if (isForegroundShellFullscreen && activePtyId) {
// Find the item that contains the active PTY
const historyItem = uiState.history.find(
(h) =>
h.type === 'tool_group' &&
h.tools.some((t) => t.ptyId === activePtyId),
);
if (historyItem) {
return [
{
type: 'history' as const,
item: historyItem,
isExpandable: true,
isFirstThinking: false,
isFirstAfterThinking: false,
},
];
}
const pendingItem = pendingHistoryItems.find(
(h) =>
h.type === 'tool_group' &&
h.tools.some((t) => t.ptyId === activePtyId),
);
if (pendingItem) {
return [{ type: 'pending' as const }];
}
}
return [
{ type: 'header' as const },
...augmentedHistory.map(
({ item, isExpandable, isFirstThinking, isFirstAfterThinking }) => ({
@@ -181,9 +226,14 @@ export const MainContent = () => {
}),
),
{ type: 'pending' as const },
],
[augmentedHistory],
);
];
}, [
augmentedHistory,
isForegroundShellFullscreen,
activePtyId,
uiState.history,
pendingHistoryItems,
]);
const renderItem = useCallback(
({ item }: { item: (typeof virtualizedData)[number] }) => {
@@ -200,9 +250,11 @@ export const MainContent = () => {
<MemoizedHistoryItemDisplay
terminalWidth={mainAreaWidth}
availableTerminalHeight={
uiState.constrainHeight || !item.isExpandable
? staticAreaMaxItemHeight
: undefined
isForegroundShellFullscreen
? fullscreenHeight
: uiState.constrainHeight || !item.isExpandable
? staticAreaMaxItemHeight
: undefined
}
availableTerminalHeightGemini={MAX_GEMINI_MESSAGE_LINES}
key={item.item.id}
@@ -212,9 +264,34 @@ export const MainContent = () => {
isExpandable={item.isExpandable}
isFirstThinking={item.isFirstThinking}
isFirstAfterThinking={item.isFirstAfterThinking}
isFullscreen={isForegroundShellFullscreen}
/>
);
} else {
if (isForegroundShellFullscreen && activePtyId) {
const pendingItem = pendingHistoryItems.find(
(h) =>
h.type === 'tool_group' &&
h.tools.some((t) => t.ptyId === activePtyId),
);
if (pendingItem) {
return (
<Box flexDirection="column">
<HistoryItemDisplay
key={0}
availableTerminalHeight={fullscreenHeight}
terminalWidth={mainAreaWidth}
item={{ ...pendingItem, id: 0 }}
isPending={true}
isExpandable={true}
isFirstThinking={false}
isFirstAfterThinking={false}
isFullscreen={true}
/>
</Box>
);
}
}
return pendingItems;
}
},
@@ -226,9 +303,55 @@ export const MainContent = () => {
pendingItems,
uiState.constrainHeight,
staticAreaMaxItemHeight,
isForegroundShellFullscreen,
fullscreenHeight,
activePtyId,
pendingHistoryItems,
],
);
if (isForegroundShellFullscreen && activePtyId) {
const historyItem = uiState.history.find(
(h) =>
h.type === 'tool_group' && h.tools.some((t) => t.ptyId === activePtyId),
);
if (historyItem) {
return (
<Box flexDirection="column" flexGrow={1} display="flex">
<HistoryItemDisplay
terminalWidth={mainAreaWidth}
availableTerminalHeight={fullscreenHeight}
key={historyItem.id}
item={historyItem}
isPending={false}
commands={uiState.slashCommands}
isExpandable={true}
isFullscreen={true}
/>
</Box>
);
}
const pendingItem = pendingHistoryItems.find(
(h) =>
h.type === 'tool_group' && h.tools.some((t) => t.ptyId === activePtyId),
);
if (pendingItem) {
return (
<Box flexDirection="column" flexGrow={1} display="flex">
<HistoryItemDisplay
key={0}
availableTerminalHeight={fullscreenHeight}
terminalWidth={mainAreaWidth}
item={{ ...pendingItem, id: 0 }}
isPending={true}
isExpandable={true}
isFullscreen={true}
/>
</Box>
);
}
}
if (isAlternateBuffer) {
return (
<ScrollableList
@@ -13,6 +13,10 @@ Tips for getting started:
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
╭──────────────────────────────────────────────────────────────────────────╮
│ ? confirming_tool Confirming tool description │
│ │
╰──────────────────────────────────────────────────────────────────────────╯
Action Required (was prompted):
@@ -38,37 +38,25 @@ import {
export interface ShellToolMessageProps extends ToolMessageProps {
config?: Config;
isExpandable?: boolean;
isFullscreen?: boolean;
}
export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
name,
description,
resultDisplay,
status,
availableTerminalHeight,
terminalWidth,
emphasis = 'medium',
renderOutputAsMarkdown = true,
ptyId,
config,
isFirst,
borderColor,
borderDimColor,
isExpandable,
isFullscreen,
originalRequestName,
}) => {
const {
@@ -93,14 +81,18 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
availableTerminalHeight,
constrainHeight,
isExpandable,
isFullscreen,
});
const availableHeight = calculateToolContentMaxLines({
availableTerminalHeight,
isAlternateBuffer,
maxLinesLimit: maxLines,
isFullscreen,
});
const lastDimensionsRef = React.useRef({ width: 0, height: 0 });
React.useEffect(() => {
const isExecuting = status === CoreToolCallStatus.Executing;
if (isExecuting && ptyId) {
@@ -109,11 +101,20 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
const finalHeight =
availableHeight ?? ACTIVE_SHELL_MAX_LINES - SHELL_CONTENT_OVERHEAD;
ShellExecutionService.resizePty(
ptyId,
Math.max(1, childWidth),
Math.max(1, finalHeight),
);
if (
lastDimensionsRef.current.width !== childWidth ||
lastDimensionsRef.current.height !== finalHeight
) {
ShellExecutionService.resizePty(
ptyId,
Math.max(1, childWidth),
Math.max(1, finalHeight),
);
lastDimensionsRef.current = {
width: childWidth,
height: finalHeight,
};
}
} catch (e) {
if (
!(
@@ -142,11 +143,8 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
}, [isThisShellFocused, embeddedShellFocused, setEmbeddedShellFocused]);
const headerRef = React.useRef<DOMElement>(null);
const contentRef = React.useRef<DOMElement>(null);
// The shell is focusable if it's the shell command, it's executing, and the interactive shell is enabled.
const isThisShellFocusable = checkIsShellFocusable(name, status, config);
const handleFocus = () => {
@@ -156,7 +154,6 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
};
useMouseClick(headerRef, handleFocus, { isActive: !!isThisShellFocusable });
useMouseClick(contentRef, handleFocus, { isActive: !!isThisShellFocusable });
const { shouldShowFocusHint } = useFocusHint(
@@ -169,7 +166,7 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
<>
<StickyHeader
width={terminalWidth}
isFirst={isFirst}
isFirst={isFullscreen ? true : isFirst}
borderColor={borderColor}
borderDimColor={borderDimColor}
containerRef={headerRef}
@@ -216,6 +213,7 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
renderOutputAsMarkdown={renderOutputAsMarkdown}
hasFocus={isThisShellFocused}
maxLines={maxLines}
isFullscreen={isFullscreen}
/>
{isThisShellFocused && config && (
<ShellInputPrompt
@@ -118,30 +118,10 @@ describe('<ToolGroupMessage />', () => {
{ config: baseMockConfig, settings: fullVerbositySettings },
);
// Should now hide confirming tools (to avoid duplication with Global Queue)
await waitUntilReady();
expect(lastFrame({ allowEmpty: true })).toBe('');
unmount();
});
it('renders canceled tool calls', async () => {
const toolCalls = [
createToolCall({
callId: 'canceled-tool',
name: 'canceled-tool',
status: CoreToolCallStatus.Cancelled,
}),
];
const item = createItem(toolCalls);
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
{ config: baseMockConfig, settings: fullVerbositySettings },
);
// Should now render confirming tools
await waitUntilReady();
const output = lastFrame();
expect(output).toMatchSnapshot('canceled_tool');
expect(output).toContain('test-tool');
unmount();
});
@@ -862,7 +842,7 @@ describe('<ToolGroupMessage />', () => {
);
await waitUntilReady();
expect(lastFrame({ allowEmpty: true })).toBe('');
expect(lastFrame({ allowEmpty: true })).not.toBe('');
unmount();
});
@@ -35,6 +35,7 @@ interface ToolGroupMessageProps {
borderTop?: boolean;
borderBottom?: boolean;
isExpandable?: boolean;
isFullscreen?: boolean;
}
// Main component renders the border and maps the tools using ToolMessage
@@ -48,6 +49,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
borderTop: borderTopOverride,
borderBottom: borderBottomOverride,
isExpandable,
isFullscreen,
}) => {
const settings = useSettings();
const isLowErrorVerbosity = settings.merged.ui?.errorVerbosity !== 'full';
@@ -110,12 +112,11 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
() =>
toolCalls.filter((t) => {
const displayStatus = mapCoreStatusToDisplayStatus(t.status);
// We hide Confirming tools from the history log because they are
// currently being rendered in the interactive ToolConfirmationQueue.
// We show everything else, including Pending (waiting to run) and
// Canceled (rejected by user), to ensure the history is complete
// and to avoid tools "vanishing" after approval.
return displayStatus !== ToolCallStatus.Confirming;
// We used to filter out Pending and Confirming statuses here to avoid
// duplication with the Global Queue, but this causes tools to appear to
// "vanish" from the context after approval.
// We now allow them to be visible here as well.
return displayStatus !== ToolCallStatus.Canceled;
}),
[toolCalls],
@@ -141,7 +142,8 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
)
: undefined;
const contentWidth = terminalWidth - TOOL_MESSAGE_HORIZONTAL_MARGIN;
const horizontalMargin = TOOL_MESSAGE_HORIZONTAL_MARGIN;
const contentWidth = terminalWidth - horizontalMargin;
// If all tools are filtered out (e.g., in-progress AskUser tools, low-verbosity
// internal errors, plan-mode hidden write/edit), we should not emit standalone
@@ -165,7 +167,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
cause tearing.
*/
width={terminalWidth}
paddingRight={TOOL_MESSAGE_HORIZONTAL_MARGIN}
paddingRight={horizontalMargin}
>
{visibleToolCalls.map((tool, index) => {
const isFirst = index === 0;
@@ -183,6 +185,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
borderColor,
borderDimColor,
isExpandable,
isFullscreen,
};
return (
@@ -227,7 +230,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
*/
(visibleToolCalls.length > 0 || borderBottomOverride !== undefined) && (
<Box
height={0}
height={isFullscreen ? 1 : 0}
width={contentWidth}
borderLeft={true}
borderRight={true}
@@ -39,6 +39,7 @@ export interface ToolMessageProps extends IndividualToolCallDisplay {
embeddedShellFocused?: boolean;
ptyId?: number;
config?: Config;
isFullscreen?: boolean;
}
export const ToolMessage: React.FC<ToolMessageProps> = ({
@@ -34,6 +34,7 @@ export interface ToolResultDisplayProps {
maxLines?: number;
hasFocus?: boolean;
overflowDirection?: 'top' | 'bottom';
isFullscreen?: boolean;
}
interface FileDiffResult {
@@ -49,6 +50,7 @@ export const ToolResultDisplay: React.FC<ToolResultDisplayProps> = ({
maxLines,
hasFocus = false,
overflowDirection = 'top',
isFullscreen = false,
}) => {
const { renderMarkdown } = useUIState();
const isAlternateBuffer = useAlternateBuffer();
@@ -57,6 +59,7 @@ export const ToolResultDisplay: React.FC<ToolResultDisplayProps> = ({
availableTerminalHeight,
isAlternateBuffer,
maxLinesLimit: maxLines,
isFullscreen,
});
const combinedPaddingAndBorderWidth = 4;
@@ -173,11 +176,13 @@ export const ToolResultDisplay: React.FC<ToolResultDisplayProps> = ({
// Virtualized path for large ANSI arrays
if (Array.isArray(resultDisplay)) {
const limit = maxLines ?? availableHeight ?? ACTIVE_SHELL_MAX_LINES;
const listHeight = Math.min(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(resultDisplay as AnsiOutput).length,
limit,
);
const listHeight = isFullscreen
? limit
: Math.min(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(resultDisplay as AnsiOutput).length,
limit,
);
return (
<Box width={childWidth} flexDirection="column" maxHeight={listHeight}>
@@ -49,15 +49,6 @@ exports[`<ToolGroupMessage /> > Border Color Logic > uses yellow border for shel
"
`;
exports[`<ToolGroupMessage /> > Golden Snapshots > renders canceled tool calls > canceled_tool 1`] = `
"╭──────────────────────────────────────────────────────────────────────────╮
│ - canceled-tool A tool for testing │
│ │
│ Test result │
╰──────────────────────────────────────────────────────────────────────────╯
"
`;
exports[`<ToolGroupMessage /> > Golden Snapshots > renders empty tool calls array 1`] = `""`;
exports[`<ToolGroupMessage /> > Golden Snapshots > renders header when scrolled 1`] = `
@@ -217,6 +217,8 @@ export interface UIState {
backgroundShells: Map<number, BackgroundShell>;
activeBackgroundShellPid: number | null;
backgroundShellHeight: number;
isBackgroundShellFullscreen: boolean;
isForegroundShellFullscreen: boolean;
isBackgroundShellListOpen: boolean;
adminSettingsChanged: boolean;
newAgents: AgentDefinition[] | null;
@@ -28,6 +28,8 @@ export function useBackgroundShellManager({
}: BackgroundShellManagerProps) {
const [isBackgroundShellListOpen, setIsBackgroundShellListOpen] =
useState(false);
const [isBackgroundShellFullscreen, setIsBackgroundShellFullscreen] =
useState(false);
const [activeBackgroundShellPid, setActiveBackgroundShellPid] = useState<
number | null
>(null);
@@ -73,17 +75,27 @@ export function useBackgroundShellManager({
setEmbeddedShellFocused,
]);
const backgroundShellHeight = useMemo(
() =>
isBackgroundShellVisible && backgroundShells.size > 0
? Math.max(Math.floor(terminalHeight * 0.3), 5)
: 0,
[isBackgroundShellVisible, backgroundShells.size, terminalHeight],
);
const backgroundShellHeight = useMemo(() => {
if (!isBackgroundShellVisible || backgroundShells.size === 0) {
return 0;
}
if (isBackgroundShellFullscreen) {
// Leave enough room for the footer/composer (approx 7 lines)
return Math.max(terminalHeight - 7, 5);
}
return Math.max(Math.floor(terminalHeight * 0.3), 5);
}, [
isBackgroundShellVisible,
backgroundShells.size,
terminalHeight,
isBackgroundShellFullscreen,
]);
return {
isBackgroundShellListOpen,
setIsBackgroundShellListOpen,
isBackgroundShellFullscreen,
setIsBackgroundShellFullscreen,
activeBackgroundShellPid,
setActiveBackgroundShellPid,
backgroundShellHeight,
+5
View File
@@ -100,6 +100,7 @@ export enum Command {
BACKGROUND_SHELL_SELECT = 'background.select',
TOGGLE_BACKGROUND_SHELL = 'background.toggle',
TOGGLE_BACKGROUND_SHELL_LIST = 'background.toggleList',
TOGGLE_SHELL_FULLSCREEN = 'shell.toggleFullscreen',
KILL_BACKGROUND_SHELL = 'background.kill',
UNFOCUS_BACKGROUND_SHELL = 'background.unfocus',
UNFOCUS_BACKGROUND_SHELL_LIST = 'background.unfocusList',
@@ -395,6 +396,7 @@ export const defaultKeyBindingConfig: KeyBindingConfig = new Map([
[Command.BACKGROUND_SHELL_SELECT, [new KeyBinding('enter')]],
[Command.TOGGLE_BACKGROUND_SHELL, [new KeyBinding('ctrl+b')]],
[Command.TOGGLE_BACKGROUND_SHELL_LIST, [new KeyBinding('ctrl+l')]],
[Command.TOGGLE_SHELL_FULLSCREEN, [new KeyBinding('ctrl+7')]],
[Command.KILL_BACKGROUND_SHELL, [new KeyBinding('ctrl+k')]],
[Command.UNFOCUS_BACKGROUND_SHELL, [new KeyBinding('shift+tab')]],
[Command.UNFOCUS_BACKGROUND_SHELL_LIST, [new KeyBinding('tab')]],
@@ -519,6 +521,7 @@ export const commandCategories: readonly CommandCategory[] = [
Command.BACKGROUND_SHELL_SELECT,
Command.TOGGLE_BACKGROUND_SHELL,
Command.TOGGLE_BACKGROUND_SHELL_LIST,
Command.TOGGLE_SHELL_FULLSCREEN,
Command.KILL_BACKGROUND_SHELL,
Command.UNFOCUS_BACKGROUND_SHELL,
Command.UNFOCUS_BACKGROUND_SHELL_LIST,
@@ -625,6 +628,8 @@ export const commandDescriptions: Readonly<Record<Command, string>> = {
[Command.TOGGLE_BACKGROUND_SHELL]:
'Toggle current background shell visibility.',
[Command.TOGGLE_BACKGROUND_SHELL_LIST]: 'Toggle background shell list.',
[Command.TOGGLE_SHELL_FULLSCREEN]:
'Toggle fullscreen mode for the active integrated or background shell.',
[Command.KILL_BACKGROUND_SHELL]: 'Kill the active background shell.',
[Command.UNFOCUS_BACKGROUND_SHELL]:
'Move focus from background shell to Gemini.',
@@ -39,9 +39,12 @@ export const DefaultAppLayout: React.FC = () => {
overflow="hidden"
ref={uiState.rootUiRef}
>
<MainContent />
<Box flexGrow={1} flexDirection="column">
<MainContent />
</Box>
{uiState.isBackgroundShellVisible &&
!uiState.isForegroundShellFullscreen &&
uiState.backgroundShells.size > 0 &&
uiState.activeBackgroundShellPid &&
uiState.backgroundShellHeight > 0 &&
+17 -3
View File
@@ -39,8 +39,14 @@ export function calculateToolContentMaxLines(options: {
availableTerminalHeight: number | undefined;
isAlternateBuffer: boolean;
maxLinesLimit?: number;
isFullscreen?: boolean;
}): number | undefined {
const { availableTerminalHeight, isAlternateBuffer, maxLinesLimit } = options;
const {
availableTerminalHeight,
isAlternateBuffer,
maxLinesLimit,
isFullscreen,
} = options;
const reservedLines = isAlternateBuffer
? TOOL_RESULT_ASB_RESERVED_LINE_COUNT
@@ -48,7 +54,8 @@ export function calculateToolContentMaxLines(options: {
let contentHeight = availableTerminalHeight
? Math.max(
availableTerminalHeight - TOOL_RESULT_STATIC_HEIGHT - reservedLines,
availableTerminalHeight -
(isFullscreen ? 3 : TOOL_RESULT_STATIC_HEIGHT + reservedLines),
TOOL_RESULT_MIN_LINES_SHOWN + 1,
)
: undefined;
@@ -78,6 +85,7 @@ export function calculateShellMaxLines(options: {
availableTerminalHeight: number | undefined;
constrainHeight: boolean;
isExpandable: boolean | undefined;
isFullscreen?: boolean;
}): number | undefined {
const {
status,
@@ -86,6 +94,7 @@ export function calculateShellMaxLines(options: {
availableTerminalHeight,
constrainHeight,
isExpandable,
isFullscreen,
} = options;
// 1. If the user explicitly requested expansion (unconstrained), remove all caps.
@@ -102,7 +111,12 @@ export function calculateShellMaxLines(options: {
const maxLinesBasedOnHeight = Math.max(1, availableTerminalHeight - 2);
// 3. Handle ASB mode focus expansion.
// 3. Handle Fullscreen or ASB mode focus expansion.
// Fullscreen mode always takes the full available height.
if (isFullscreen && isThisShellFocused) {
return maxLinesBasedOnHeight;
}
// We allow a focused shell in ASB mode to take up the full available height,
// BUT only if we aren't trying to maintain a constrained view (e.g., history items).
if (isAlternateBuffer && isThisShellFocused && !constrainHeight) {
-3
View File
@@ -10,7 +10,6 @@
"type": "module",
"main": "dist/index.js",
"scripts": {
"bundle:browser-mcp": "node scripts/bundle-browser-mcp.mjs",
"build": "node ../../scripts/build_package.js",
"lint": "eslint . --ext .ts,.tsx",
"format": "prettier --write .",
@@ -74,7 +73,6 @@
"open": "^10.1.2",
"picomatch": "^4.0.1",
"proper-lockfile": "^4.1.2",
"puppeteer-core": "^24.0.0",
"read-package-up": "^11.0.0",
"shell-quote": "^1.8.3",
"simple-git": "^3.28.0",
@@ -103,7 +101,6 @@
"@types/fast-levenshtein": "^0.0.4",
"@types/js-yaml": "^4.0.9",
"@types/picomatch": "^4.0.1",
"chrome-devtools-mcp": "^0.19.0",
"msw": "^2.3.4",
"typescript": "^5.3.3",
"vitest": "^3.1.1"
@@ -1,104 +0,0 @@
import esbuild from 'esbuild';
import fs from 'node:fs'; // Import the full fs module
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const manifestPath = path.resolve(
__dirname,
'../src/agents/browser/browser-tools-manifest.json',
);
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
// Only exclude tools explicitly mentioned in the manifest's exclude list
const excludedToolsFiles = (manifest.exclude || []).map((t) => t.name);
// Basic esbuild plugin to empty out excluded modules
const emptyModulePlugin = {
name: 'empty-modules',
setup(build) {
if (excludedToolsFiles.length === 0) return;
// Create a filter that matches any of the excluded tools
const excludeFilter = new RegExp(`(${excludedToolsFiles.join('|')})\\.js$`);
build.onResolve({ filter: excludeFilter }, (args) => {
// Check if we are inside a tools directory to avoid accidental matches
if (
args.importer.includes('chrome-devtools-mcp') &&
/[\\/]tools[\\/]/.test(args.importer)
) {
return { path: args.path, namespace: 'empty' };
}
return null;
});
build.onLoad({ filter: /.*/, namespace: 'empty' }, (_args) => ({
contents: 'export {};', // Empty module (ESM)
loader: 'js',
}));
},
};
async function bundle() {
try {
const entryPoint = path.resolve(
__dirname,
'../../../node_modules/chrome-devtools-mcp/build/src/index.js',
);
await esbuild.build({
entryPoints: [entryPoint],
bundle: true,
outfile: path.resolve(
__dirname,
'../dist/bundled/chrome-devtools-mcp.mjs',
),
format: 'esm',
platform: 'node',
plugins: [emptyModulePlugin],
external: [
'puppeteer-core',
'/bundled/*',
'../../../node_modules/puppeteer-core/*',
],
banner: {
js: 'import { createRequire as __createRequire } from "module"; const require = __createRequire(import.meta.url);',
},
});
// Copy third_party assets
const srcThirdParty = path.resolve(
__dirname,
'../../../node_modules/chrome-devtools-mcp/build/src/third_party',
);
const destThirdParty = path.resolve(
__dirname,
'../dist/bundled/third_party',
);
if (fs.existsSync(srcThirdParty)) {
if (fs.existsSync(destThirdParty)) {
fs.rmSync(destThirdParty, { recursive: true, force: true });
}
fs.cpSync(srcThirdParty, destThirdParty, {
recursive: true,
filter: (src) => {
// Skip large/unnecessary bundles that are either explicitly excluded
// or not required for the browser agent functionality.
return (
!src.includes('lighthouse-devtools-mcp-bundle.js') &&
!src.includes('devtools-formatter-worker.js')
);
},
});
} else {
console.warn(`Warning: third_party assets not found at ${srcThirdParty}`);
}
} catch (error) {
console.error('Error bundling chrome-devtools-mcp:', error);
process.exit(1);
}
}
bundle();
@@ -1,22 +0,0 @@
{
"description": "Explicitly promoted tools from chrome-devtools-mcp for the gemini-cli browser agent.",
"targetVersion": "0.19.0",
"exclude": [
{
"name": "lighthouse",
"reason": "3.5 MB pre-built bundle — not needed for gemini-cli browser agent's core tasks."
},
{
"name": "performance",
"reason": "Depends on chrome-devtools-frontend TraceEngine (~800 KB) — not needed for core tasks."
},
{
"name": "screencast",
"reason": "Requires ffmpeg at runtime — not a common browser agent use case and adds external dependency."
},
{
"name": "extensions",
"reason": "Extension management not relevant for the gemini-cli browser agent's current scope."
}
]
}
@@ -24,7 +24,6 @@ const mockBrowserManager = {
{ name: 'click', description: 'Click element' },
{ name: 'fill', description: 'Fill form field' },
{ name: 'navigate_page', description: 'Navigate to URL' },
{ name: 'type_text', description: 'Type text into an element' },
// Visual tools (from --experimental-vision)
{ name: 'click_at', description: 'Click at coordinates' },
]),
@@ -71,7 +70,6 @@ describe('browserAgentFactory', () => {
{ name: 'click', description: 'Click element' },
{ name: 'fill', description: 'Fill form field' },
{ name: 'navigate_page', description: 'Navigate to URL' },
{ name: 'type_text', description: 'Type text into an element' },
// Visual tools (from --experimental-vision)
{ name: 'click_at', description: 'Click at coordinates' },
]);
@@ -137,7 +135,7 @@ describe('browserAgentFactory', () => {
);
expect(definition.name).toBe(BROWSER_AGENT_NAME);
// 6 MCP tools (no analyze_screenshot without visualModel)
// 5 MCP tools + 1 type_text composite tool (no analyze_screenshot without visualModel)
expect(definition.toolConfig?.tools).toHaveLength(6);
});
@@ -230,7 +228,7 @@ describe('browserAgentFactory', () => {
mockMessageBus,
);
// 6 MCP tools + 1 analyze_screenshot
// 5 MCP tools + 1 type_text + 1 analyze_screenshot
expect(definition.toolConfig?.tools).toHaveLength(7);
const toolNames =
definition.toolConfig?.tools
@@ -270,7 +268,6 @@ describe('browserAgentFactory', () => {
{ name: 'close_page', description: 'Close page' },
{ name: 'select_page', description: 'Select page' },
{ name: 'press_key', description: 'Press key' },
{ name: 'type_text', description: 'Type text into an element' },
{ name: 'hover', description: 'Hover element' },
]);
@@ -294,6 +291,7 @@ describe('browserAgentFactory', () => {
expect(toolNames).toContain('click');
expect(toolNames).toContain('take_snapshot');
expect(toolNames).toContain('press_key');
// Custom composite tool must also be present
expect(toolNames).toContain('type_text');
// Total: 9 MCP + 1 type_text (no analyze_screenshot without visualModel)
expect(definition.toolConfig?.tools).toHaveLength(10);
@@ -39,7 +39,6 @@ vi.mock('@modelcontextprotocol/sdk/client/stdio.js', () => ({
vi.mock('../../utils/debugLogger.js', () => ({
debugLogger: {
log: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
},
}));
@@ -48,20 +47,6 @@ vi.mock('./automationOverlay.js', () => ({
injectAutomationOverlay: vi.fn().mockResolvedValue(undefined),
}));
vi.mock('node:fs', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:fs')>();
return {
...actual,
existsSync: vi.fn((p: string) => {
if (p.endsWith('bundled/chrome-devtools-mcp.mjs')) {
return false; // Default
}
return actual.existsSync(p);
}),
};
});
import * as fs from 'node:fs';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
@@ -111,40 +96,6 @@ describe('BrowserManager', () => {
vi.restoreAllMocks();
});
describe('MCP bundled path resolution', () => {
it('should use bundled path if it exists (handles bundled CLI)', async () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
const manager = new BrowserManager(mockConfig);
await manager.ensureConnection();
expect(StdioClientTransport).toHaveBeenCalledWith(
expect.objectContaining({
command: 'node',
args: expect.arrayContaining([
expect.stringMatching(/bundled\/chrome-devtools-mcp\.mjs$/),
]),
}),
);
});
it('should fall back to development path if bundled path does not exist', async () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
const manager = new BrowserManager(mockConfig);
await manager.ensureConnection();
expect(StdioClientTransport).toHaveBeenCalledWith(
expect.objectContaining({
command: 'node',
args: expect.arrayContaining([
expect.stringMatching(
/(dist\/)?bundled\/chrome-devtools-mcp\.mjs$/,
),
]),
}),
);
});
});
describe('getRawMcpClient', () => {
it('should ensure connection and return raw MCP client', async () => {
const manager = new BrowserManager(mockConfig);
@@ -271,9 +222,10 @@ describe('BrowserManager', () => {
// Verify StdioClientTransport was created with correct args
expect(StdioClientTransport).toHaveBeenCalledWith(
expect.objectContaining({
command: 'node',
command: process.platform === 'win32' ? 'npx.cmd' : 'npx',
args: expect.arrayContaining([
expect.stringMatching(/chrome-devtools-mcp\.mjs$/),
'-y',
expect.stringMatching(/chrome-devtools-mcp@/),
'--experimental-vision',
]),
}),
@@ -283,7 +235,6 @@ describe('BrowserManager', () => {
?.args as string[];
expect(args).not.toContain('--isolated');
expect(args).not.toContain('--autoConnect');
expect(args).not.toContain('-y');
// Persistent mode should set the default --userDataDir under ~/.gemini
expect(args).toContain('--userDataDir');
const userDataDirIndex = args.indexOf('--userDataDir');
@@ -343,7 +294,7 @@ describe('BrowserManager', () => {
expect(StdioClientTransport).toHaveBeenCalledWith(
expect.objectContaining({
command: 'node',
command: process.platform === 'win32' ? 'npx.cmd' : 'npx',
args: expect.arrayContaining(['--headless']),
}),
);
@@ -368,7 +319,7 @@ describe('BrowserManager', () => {
expect(StdioClientTransport).toHaveBeenCalledWith(
expect.objectContaining({
command: 'node',
command: process.platform === 'win32' ? 'npx.cmd' : 'npx',
args: expect.arrayContaining(['--userDataDir', '/path/to/profile']),
}),
);
@@ -25,12 +25,10 @@ import type { Config } from '../../config/config.js';
import { Storage } from '../../config/storage.js';
import { injectInputBlocker } from './inputBlocker.js';
import * as path from 'node:path';
import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { injectAutomationOverlay } from './automationOverlay.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Pin chrome-devtools-mcp version for reproducibility.
const CHROME_DEVTOOLS_MCP_VERSION = '0.17.1';
// Default browser profile directory name within ~/.gemini/
const BROWSER_PROFILE_DIR = 'cli-browser-profile';
@@ -281,7 +279,7 @@ export class BrowserManager {
this.rawMcpClient = undefined;
}
// Close transport (this terminates the browser)
// Close transport (this terminates the npx process and browser)
if (this.mcpTransport) {
try {
await this.mcpTransport.close();
@@ -299,7 +297,8 @@ export class BrowserManager {
/**
* Connects to chrome-devtools-mcp which manages the browser process.
*
* Spawns node with the bundled chrome-devtools-mcp.mjs.
* Spawns npx chrome-devtools-mcp with:
* - --isolated: Manages its own browser instance
* - --experimental-vision: Enables visual tools (click_at, etc.)
*
* IMPORTANT: This does NOT use McpClientManager and does NOT register
@@ -324,7 +323,11 @@ export class BrowserManager {
const browserConfig = this.config.getBrowserAgentConfig();
const sessionMode = browserConfig.customConfig.sessionMode ?? 'persistent';
const mcpArgs = ['--experimental-vision'];
const mcpArgs = [
'-y',
`chrome-devtools-mcp@${CHROME_DEVTOOLS_MCP_VERSION}`,
'--experimental-vision',
];
// Session mode determines how the browser is managed:
// - "isolated": Temp profile, cleaned up after session (--isolated)
@@ -370,28 +373,15 @@ export class BrowserManager {
}
debugLogger.log(
`Launching bundled chrome-devtools-mcp (${sessionMode} mode) with args: ${mcpArgs.join(' ')}`,
`Launching chrome-devtools-mcp (${sessionMode} mode) with args: ${mcpArgs.join(' ')}`,
);
// Create stdio transport to the bundled chrome-devtools-mcp.
// Create stdio transport to npx chrome-devtools-mcp.
// stderr is piped (not inherited) to prevent MCP server banners and
// warnings from corrupting the UI in alternate buffer mode.
let bundleMcpPath = path.resolve(
__dirname,
'bundled/chrome-devtools-mcp.mjs',
);
if (!fs.existsSync(bundleMcpPath)) {
bundleMcpPath = path.resolve(
__dirname,
__dirname.includes(`${path.sep}dist${path.sep}`)
? '../../../bundled/chrome-devtools-mcp.mjs'
: '../../../dist/bundled/chrome-devtools-mcp.mjs',
);
}
this.mcpTransport = new StdioClientTransport({
command: 'node',
args: [bundleMcpPath, ...mcpArgs],
command: process.platform === 'win32' ? 'npx.cmd' : 'npx',
args: mcpArgs,
stderr: 'pipe',
});
@@ -502,7 +492,8 @@ export class BrowserManager {
`Timed out connecting to Chrome: ${message}\n\n` +
`Possible causes:\n` +
` 1. Chrome is not installed or not in PATH\n` +
` 2. Chrome failed to start (try setting headless: true in settings.json)`,
` 2. npx cannot download chrome-devtools-mcp (check network/proxy)\n` +
` 3. Chrome failed to start (try setting headless: true in settings.json)`,
);
}
@@ -68,19 +68,18 @@ describe('mcpToolWrapper', () => {
const tools = await createMcpDeclarativeTools(
mockBrowserManager,
mockMessageBus,
false,
);
expect(tools).toHaveLength(2);
expect(tools).toHaveLength(3);
expect(tools[0].name).toBe('take_snapshot');
expect(tools[1].name).toBe('click');
expect(tools[2].name).toBe('type_text');
});
it('should return tools with correct description', async () => {
const tools = await createMcpDeclarativeTools(
mockBrowserManager,
mockMessageBus,
false,
);
// Descriptions include augmented hints, so we check they contain the original
@@ -94,7 +93,6 @@ describe('mcpToolWrapper', () => {
const tools = await createMcpDeclarativeTools(
mockBrowserManager,
mockMessageBus,
false,
);
const schema = tools[0].schema;
@@ -108,7 +106,6 @@ describe('mcpToolWrapper', () => {
const tools = await createMcpDeclarativeTools(
mockBrowserManager,
mockMessageBus,
false,
);
const invocation = tools[0].build({ verbose: true });
@@ -121,7 +118,6 @@ describe('mcpToolWrapper', () => {
const tools = await createMcpDeclarativeTools(
mockBrowserManager,
mockMessageBus,
false,
);
const invocation = tools[0].build({});
@@ -135,7 +131,6 @@ describe('mcpToolWrapper', () => {
const tools = await createMcpDeclarativeTools(
mockBrowserManager,
mockMessageBus,
false,
);
const invocation = tools[1].build({ uid: 'elem-123' });
@@ -154,7 +149,6 @@ describe('mcpToolWrapper', () => {
const tools = await createMcpDeclarativeTools(
mockBrowserManager,
mockMessageBus,
false,
);
const invocation = tools[0].build({ verbose: true });
@@ -173,7 +167,6 @@ describe('mcpToolWrapper', () => {
const tools = await createMcpDeclarativeTools(
mockBrowserManager,
mockMessageBus,
false,
);
const invocation = tools[1].build({ uid: 'invalid' });
@@ -191,7 +184,6 @@ describe('mcpToolWrapper', () => {
const tools = await createMcpDeclarativeTools(
mockBrowserManager,
mockMessageBus,
false,
);
const invocation = tools[0].build({});
@@ -175,6 +175,144 @@ class McpToolInvocation extends BaseToolInvocation<
}
}
/**
* Composite tool invocation that types a full string by calling press_key
* for each character internally, avoiding N model round-trips.
*/
class TypeTextInvocation extends BaseToolInvocation<
Record<string, unknown>,
ToolResult
> {
constructor(
private readonly browserManager: BrowserManager,
private readonly text: string,
private readonly submitKey: string | undefined,
messageBus: MessageBus,
) {
super({ text, submitKey }, messageBus, 'type_text', 'type_text');
}
getDescription(): string {
const preview = `"${this.text.substring(0, 50)}${this.text.length > 50 ? '...' : ''}"`;
return this.submitKey
? `type_text: ${preview} + ${this.submitKey}`
: `type_text: ${preview}`;
}
protected override async getConfirmationDetails(
_abortSignal: AbortSignal,
): Promise<ToolCallConfirmationDetails | false> {
if (!this.messageBus) {
return false;
}
return {
type: 'mcp',
title: `Confirm Tool: type_text`,
serverName: 'browser-agent',
toolName: 'type_text',
toolDisplayName: 'type_text',
onConfirm: async (outcome: ToolConfirmationOutcome) => {
await this.publishPolicyUpdate(outcome);
},
};
}
override getPolicyUpdateOptions(
_outcome: ToolConfirmationOutcome,
): PolicyUpdateOptions | undefined {
return {
mcpName: 'browser-agent',
};
}
override async execute(signal: AbortSignal): Promise<ToolResult> {
try {
if (signal.aborted) {
return {
llmContent: 'Error: Operation cancelled before typing started.',
returnDisplay: 'Operation cancelled before typing started.',
error: { message: 'Operation cancelled' },
};
}
await this.typeCharByChar(signal);
// Optionally press a submit key (Enter, Tab, etc.) after typing
if (this.submitKey && !signal.aborted) {
const keyResult = await this.browserManager.callTool(
'press_key',
{ key: this.submitKey },
signal,
);
if (keyResult.isError) {
const errText = this.extractErrorText(keyResult);
debugLogger.warn(
`type_text: submitKey("${this.submitKey}") failed: ${errText}`,
);
}
}
const summary = this.submitKey
? `Successfully typed "${this.text}" and pressed ${this.submitKey}`
: `Successfully typed "${this.text}"`;
return {
llmContent: summary,
returnDisplay: summary,
};
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
// Chrome connection errors are fatal
if (errorMsg.includes('Could not connect to Chrome')) {
throw error;
}
debugLogger.error(`type_text failed: ${errorMsg}`);
return {
llmContent: `Error: ${errorMsg}`,
returnDisplay: `Error: ${errorMsg}`,
error: { message: errorMsg },
};
}
}
/** Types each character via individual press_key MCP calls. */
private async typeCharByChar(signal: AbortSignal): Promise<void> {
const chars = [...this.text]; // Handle Unicode correctly
for (const char of chars) {
if (signal.aborted) return;
// Map special characters to key names
const key = char === ' ' ? 'Space' : char;
const result = await this.browserManager.callTool(
'press_key',
{ key },
signal,
);
if (result.isError) {
debugLogger.warn(
`type_text: press_key("${key}") failed: ${this.extractErrorText(result)}`,
);
}
}
}
/** Extract error text from an MCP tool result. */
private extractErrorText(result: McpToolCallResult): string {
return (
result.content
?.filter(
(c: { type: string; text?: string }) => c.type === 'text' && c.text,
)
.map((c: { type: string; text?: string }) => c.text)
.join('\n') || 'Unknown error'
);
}
}
/**
* DeclarativeTool wrapper for an MCP tool.
*/
@@ -215,6 +353,65 @@ class McpDeclarativeTool extends DeclarativeTool<
}
}
/**
* DeclarativeTool for the custom type_text composite tool.
*/
class TypeTextDeclarativeTool extends DeclarativeTool<
Record<string, unknown>,
ToolResult
> {
constructor(
private readonly browserManager: BrowserManager,
messageBus: MessageBus,
) {
super(
'type_text',
'type_text',
'Types a full text string into the currently focused element. ' +
'Much faster than calling press_key for each character individually. ' +
'Use this to enter text into form fields, search boxes, spreadsheet cells, or any focused input. ' +
'The element must already be focused (e.g., after a click). ' +
'Use submitKey to press a key after typing (e.g., submitKey="Enter" to submit a form or confirm a value, submitKey="Tab" to move to the next field).',
Kind.Other,
{
type: 'object',
properties: {
text: {
type: 'string',
description: 'The text to type into the focused element.',
},
submitKey: {
type: 'string',
description:
'Optional key to press after typing (e.g., "Enter", "Tab", "Escape"). ' +
'Useful for submitting form fields or moving to the next cell in a spreadsheet.',
},
},
required: ['text'],
},
messageBus,
/* isOutputMarkdown */ true,
/* canUpdateOutput */ false,
);
}
build(
params: Record<string, unknown>,
): ToolInvocation<Record<string, unknown>, ToolResult> {
const submitKey =
// eslint-disable-next-line no-restricted-syntax
typeof params['submitKey'] === 'string' && params['submitKey']
? params['submitKey']
: undefined;
return new TypeTextInvocation(
this.browserManager,
String(params['text'] ?? ''),
submitKey,
this.messageBus,
);
}
}
/**
* Creates DeclarativeTool instances from dynamically discovered MCP tools,
* plus custom composite tools (like type_text).
@@ -226,14 +423,13 @@ class McpDeclarativeTool extends DeclarativeTool<
*
* @param browserManager The browser manager with isolated MCP client
* @param messageBus Message bus for tool invocations
* @param shouldDisableInput Whether input should be disabled for this agent
* @returns Array of DeclarativeTools that dispatch to the isolated MCP client
*/
export async function createMcpDeclarativeTools(
browserManager: BrowserManager,
messageBus: MessageBus,
shouldDisableInput: boolean = false,
): Promise<McpDeclarativeTool[]> {
): Promise<Array<McpDeclarativeTool | TypeTextDeclarativeTool>> {
// Get dynamically discovered tools from the MCP server
const mcpTools = await browserManager.getDiscoveredTools();
@@ -242,25 +438,29 @@ export async function createMcpDeclarativeTools(
(shouldDisableInput ? ' (input blocker enabled)' : ''),
);
const tools: McpDeclarativeTool[] = mcpTools.map((mcpTool) => {
const schema = convertMcpToolToFunctionDeclaration(mcpTool);
// Augment description with uid-context hints
const augmentedDescription = augmentToolDescription(
mcpTool.name,
mcpTool.description ?? '',
);
return new McpDeclarativeTool(
browserManager,
mcpTool.name,
augmentedDescription,
schema.parametersJsonSchema,
messageBus,
shouldDisableInput,
);
});
const tools: Array<McpDeclarativeTool | TypeTextDeclarativeTool> =
mcpTools.map((mcpTool) => {
const schema = convertMcpToolToFunctionDeclaration(mcpTool);
// Augment description with uid-context hints
const augmentedDescription = augmentToolDescription(
mcpTool.name,
mcpTool.description ?? '',
);
return new McpDeclarativeTool(
browserManager,
mcpTool.name,
augmentedDescription,
schema.parametersJsonSchema,
messageBus,
shouldDisableInput,
);
});
// Add custom composite tools
tools.push(new TypeTextDeclarativeTool(browserManager, messageBus));
debugLogger.log(
`Total tools registered: ${tools.length} (${mcpTools.length} MCP)`,
`Total tools registered: ${tools.length} (${mcpTools.length} MCP + 1 custom)`,
);
return tools;
+55 -142
View File
@@ -103,13 +103,8 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
private readonly onActivity?: ActivityCallback;
private readonly compressionService: ChatCompressionService;
private readonly parentCallId?: string;
private readonly completionToolName: string;
private hasFailedCompressionAttempt = false;
private usesCustomCompletionTool(): boolean {
return this.completionToolName !== TASK_COMPLETE_TOOL_NAME;
}
private get config(): Config {
return this.context.config;
}
@@ -269,8 +264,6 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
this.onActivity = onActivity;
this.compressionService = new ChatCompressionService();
this.parentCallId = parentCallId;
this.completionToolName =
definition.runConfig.completionToolName || TASK_COMPLETE_TOOL_NAME;
const randomIdPart = Math.random().toString(36).slice(2, 8);
// parentPromptId will be undefined if this agent is invoked directly
@@ -316,7 +309,7 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
// If the model stops calling tools without calling complete_task, it's an error.
if (functionCalls.length === 0) {
this.emitActivity('ERROR', {
error: `Agent stopped calling tools but did not call '${this.completionToolName}' to finalize the session.`,
error: `Agent stopped calling tools but did not call '${TASK_COMPLETE_TOOL_NAME}' to finalize the session.`,
context: 'protocol_violation',
});
return {
@@ -381,10 +374,7 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
default:
throw new Error(`Unknown terminate reason: ${reason}`);
}
const explainInterrupted = this.usesCustomCompletionTool()
? ''
: ' and explain that your investigation was interrupted';
return `${explanation} You have one final chance to complete the task with a short grace period. You MUST call \`${this.completionToolName}\` immediately with your best answer${explainInterrupted}. Do not call any other tools.`;
return `${explanation} You have one final chance to complete the task with a short grace period. You MUST call \`${TASK_COMPLETE_TOOL_NAME}\` immediately with your best answer and explain that your investigation was interrupted. Do not call any other tools.`;
}
/**
@@ -651,7 +641,7 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
// The finalResult was already set by executeTurn, but we re-emit just in case.
finalResult =
finalResult ||
`Agent stopped calling tools but did not call '${this.completionToolName}'.`;
`Agent stopped calling tools but did not call '${TASK_COMPLETE_TOOL_NAME}'.`;
this.emitActivity('ERROR', {
error: finalResult,
context: 'protocol_violation',
@@ -910,54 +900,6 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
}
}
/**
* Validates and processes the final output of the agent.
*/
private validateAndProcessOutput(outputValue: unknown): {
submittedOutput: string;
success: boolean;
error?: string;
} {
const { outputConfig, processOutput } = this.definition;
if (outputConfig) {
const validationResult = outputConfig.schema.safeParse(outputValue);
if (!validationResult.success) {
return {
submittedOutput: '',
success: false,
error: `Output validation failed: ${JSON.stringify(validationResult.error.flatten())}`,
};
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const validatedOutput = validationResult.data;
if (processOutput) {
return {
submittedOutput: processOutput(validatedOutput),
success: true,
};
} else {
return {
submittedOutput:
typeof validatedOutput === 'string'
? validatedOutput
: JSON.stringify(validatedOutput, null, 2),
success: true,
};
}
}
return {
submittedOutput:
typeof outputValue === 'string'
? outputValue
: JSON.stringify(outputValue, null, 2),
success: true,
};
}
/**
* Executes function calls requested by the model and returns the results.
*
@@ -976,7 +918,7 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
}> {
const allowedToolNames = new Set(this.toolRegistry.getAllToolNames());
// Always allow the completion tool
allowedToolNames.add(this.completionToolName);
allowedToolNames.add(TASK_COMPLETE_TOOL_NAME);
let submittedOutput: string | null = null;
let taskCompleted = false;
@@ -1017,10 +959,7 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
callId,
});
if (
toolName === TASK_COMPLETE_TOOL_NAME &&
!this.usesCustomCompletionTool()
) {
if (toolName === TASK_COMPLETE_TOOL_NAME) {
if (taskCompleted) {
const error =
'Task already marked complete in this turn. Ignoring duplicate call.';
@@ -1044,13 +983,13 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
if (outputConfig) {
const outputName = outputConfig.outputName;
const outputValue = args[outputName];
if (outputValue !== undefined) {
const result = this.validateAndProcessOutput(outputValue);
if (args[outputName] !== undefined) {
const outputValue = args[outputName];
const validationResult = outputConfig.schema.safeParse(outputValue);
if (!result.success) {
if (!validationResult.success) {
taskCompleted = false; // Validation failed, revoke completion
const error = result.error!;
const error = `Output validation failed: ${JSON.stringify(validationResult.error.flatten())}`;
syncResults.set(callId, {
functionResponse: {
name: TASK_COMPLETE_TOOL_NAME,
@@ -1066,7 +1005,16 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
continue;
}
submittedOutput = result.submittedOutput;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const validatedOutput = validationResult.data;
if (this.definition.processOutput) {
submittedOutput = this.definition.processOutput(validatedOutput);
} else {
submittedOutput =
typeof outputValue === 'string'
? outputValue
: JSON.stringify(outputValue, null, 2);
}
syncResults.set(callId, {
functionResponse: {
name: TASK_COMPLETE_TOOL_NAME,
@@ -1201,34 +1149,6 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
id: call.request.callId,
output: call.response.resultDisplay,
});
// Check if this was a custom completion tool and it signaled task completion
if (
toolName === this.completionToolName &&
call.response.isTaskCompletion
) {
debugLogger.log(
`[LocalAgentExecutor] Custom completion tool '${toolName}' signaled task completion.`,
);
const outputValue = call.response.data;
const result = this.validateAndProcessOutput(outputValue);
if (result.success) {
taskCompleted = true;
submittedOutput = result.submittedOutput;
} else {
// If validation fails, we still mark it as completed but use the raw output
// and maybe log a warning.
debugLogger.warn(
`[LocalAgentExecutor] Custom completion tool '${toolName}' returned data that failed output validation: ${result.error}`,
);
taskCompleted = true;
submittedOutput =
typeof outputValue === 'string'
? outputValue
: JSON.stringify(outputValue, null, 2);
}
}
} else if (call.status === 'error') {
this.emitActivity('ERROR', {
context: 'tool_call',
@@ -1298,45 +1218,43 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
toolsList.push(...this.toolRegistry.getFunctionDeclarations());
}
// Always inject completion tool if it's complete_task.
// If it's a custom tool, it should be in toolConfig.
if (!this.usesCustomCompletionTool()) {
const completeTool: FunctionDeclaration = {
name: TASK_COMPLETE_TOOL_NAME,
description: outputConfig
? 'Call this tool to submit your final answer and complete the task. This is the ONLY way to finish.'
: 'Call this tool to submit your final findings and complete the task. This is the ONLY way to finish.',
parameters: {
type: Type.OBJECT,
properties: {},
required: [],
},
// Always inject complete_task.
// Configure its schema based on whether output is expected.
const completeTool: FunctionDeclaration = {
name: TASK_COMPLETE_TOOL_NAME,
description: outputConfig
? 'Call this tool to submit your final answer and complete the task. This is the ONLY way to finish.'
: 'Call this tool to submit your final findings and complete the task. This is the ONLY way to finish.',
parameters: {
type: Type.OBJECT,
properties: {},
required: [],
},
};
if (outputConfig) {
const jsonSchema = zodToJsonSchema(outputConfig.schema);
const {
$schema: _$schema,
definitions: _definitions,
...schema
} = jsonSchema;
completeTool.parameters!.properties![outputConfig.outputName] =
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
schema as Schema;
completeTool.parameters!.required!.push(outputConfig.outputName);
} else {
completeTool.parameters!.properties!['result'] = {
type: Type.STRING,
description:
'Your final results or findings to return to the orchestrator. ' +
'Ensure this is comprehensive and follows any formatting requested in your instructions.',
};
if (outputConfig) {
const jsonSchema = zodToJsonSchema(outputConfig.schema);
const {
$schema: _$schema,
definitions: _definitions,
...schema
} = jsonSchema;
completeTool.parameters!.properties![outputConfig.outputName] =
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
schema as Schema;
completeTool.parameters!.required!.push(outputConfig.outputName);
} else {
completeTool.parameters!.properties!['result'] = {
type: Type.STRING,
description:
'Your final results or findings to return to the orchestrator. ' +
'Ensure this is comprehensive and follows any formatting requested in your instructions.',
};
completeTool.parameters!.required!.push('result');
}
toolsList.push(completeTool);
completeTool.parameters!.required!.push('result');
}
toolsList.push(completeTool);
return toolsList;
}
@@ -1361,12 +1279,7 @@ Important Rules:
* Work systematically using available tools to complete your task.
* Always use absolute paths for file operations. Construct them using the provided "Environment Context".`;
if (this.usesCustomCompletionTool()) {
finalPrompt += `
* When you have completed your task, you MUST call the \`${this.completionToolName}\` tool to signal completion.
* Do not call any other tools in the same turn as \`${this.completionToolName}\`.
* This is the ONLY way to complete your mission. If you stop calling tools without calling this, you have failed.`;
} else if (this.definition.outputConfig) {
if (this.definition.outputConfig) {
finalPrompt += `
* When you have completed your task, you MUST call the \`${TASK_COMPLETE_TOOL_NAME}\` tool with your structured output.
* Do not call any other tools in the same turn as \`${TASK_COMPLETE_TOOL_NAME}\`.
@@ -1,15 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { z } from 'zod';
export const PlannerAgentSchema = z.object({
plan_path: z
.string()
.describe('The path to the finalized and approved plan.'),
});
export type PlannerAgentOutput = z.infer<typeof PlannerAgentSchema>;
-86
View File
@@ -1,86 +0,0 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { Config } from '../config/config.js';
import { getCoreSystemPrompt } from '../core/prompts.js';
import type { LocalAgentDefinition } from './types.js';
import { PlannerAgentSchema } from './planner-schema.js';
import {
GLOB_TOOL_NAME,
GREP_TOOL_NAME,
LS_TOOL_NAME,
READ_FILE_TOOL_NAME,
ASK_USER_TOOL_NAME,
EXIT_PLAN_MODE_TOOL_NAME,
WRITE_FILE_TOOL_NAME,
WEB_SEARCH_TOOL_NAME,
WEB_FETCH_TOOL_NAME,
} from '../tools/tool-names.js';
/**
* A specialized subagent for research and planning.
* It operates with read-only tools (mostly) and is restricted to writing plans
* until user approval is received.
*/
export const PlannerAgent = (
config: Config,
): LocalAgentDefinition<typeof PlannerAgentSchema> => ({
kind: 'local',
name: 'planner',
displayName: 'Planner Agent',
description:
'A specialized subagent for research and planning. It explores the codebase, designs solutions, and drafts detailed implementation plans for user approval.',
inputConfig: {
inputSchema: {
type: 'object',
properties: {
reason: {
type: 'string',
description: 'The reason for entering plan mode or the task to plan.',
},
},
required: ['reason'],
},
},
outputConfig: {
outputName: 'plan_path',
description: 'The path to the finalized and approved plan.',
schema: PlannerAgentSchema,
},
processOutput: (output) => output.plan_path,
modelConfig: {
model: 'inherit',
},
runConfig: {
maxTimeMinutes: 10,
maxTurns: 20,
completionToolName: EXIT_PLAN_MODE_TOOL_NAME,
hasCustomEntryPoint: true,
},
toolConfig: {
tools: [
LS_TOOL_NAME,
READ_FILE_TOOL_NAME,
GLOB_TOOL_NAME,
GREP_TOOL_NAME,
ASK_USER_TOOL_NAME,
EXIT_PLAN_MODE_TOOL_NAME,
WRITE_FILE_TOOL_NAME,
WEB_SEARCH_TOOL_NAME,
WEB_FETCH_TOOL_NAME,
],
},
promptConfig: {
get systemPrompt() {
return getCoreSystemPrompt(
config,
undefined, // userMemory
false, // interactiveOverride
);
},
query: 'Start planning for: ${reason}',
},
});
-10
View File
@@ -274,7 +274,6 @@ describe('AgentRegistry', () => {
codebase_investigator: { enabled: false },
cli_help: { enabled: false },
generalist: { enabled: false },
planner: { enabled: false },
},
},
});
@@ -336,15 +335,6 @@ describe('AgentRegistry', () => {
expect(registry.getDefinition('generalist')).toBeDefined();
});
it('should NOT register planner agent if plannerSubagent is false', async () => {
const config = makeMockedConfig({ plannerSubagent: false });
const registry = new TestableAgentRegistry(config);
await registry.initialize();
expect(registry.getDefinition('planner')).toBeUndefined();
});
it('should NOT register a non-experimental agent if enabled is false', async () => {
// CLI help is NOT experimental, but we explicitly disable it via enabled: false
const config = makeMockedConfig({
-4
View File
@@ -12,7 +12,6 @@ import { loadAgentsFromDirectory } from './agentLoader.js';
import { CodebaseInvestigatorAgent } from './codebase-investigator.js';
import { CliHelpAgent } from './cli-help-agent.js';
import { GeneralistAgent } from './generalist-agent.js';
import { PlannerAgent } from './planner.js';
import { BrowserAgentDefinition } from './browser/browserAgentDefinition.js';
import { A2AClientManager } from './a2a-client-manager.js';
import { A2AAuthProviderFactory } from './auth-provider/factory.js';
@@ -244,9 +243,6 @@ export class AgentRegistry {
this.registerLocalAgent(CodebaseInvestigatorAgent(this.config));
this.registerLocalAgent(CliHelpAgent(this.config));
this.registerLocalAgent(GeneralistAgent(this.config));
if (this.config.isPlannerSubagentEnabled()) {
this.registerLocalAgent(PlannerAgent(this.config));
}
// Register the browser agent if enabled in settings.
// Tools are configured dynamically at invocation time via browserAgentFactory.
-10
View File
@@ -231,14 +231,4 @@ export interface RunConfig {
* If not specified, defaults to DEFAULT_MAX_TURNS (30).
*/
maxTurns?: number;
/**
* The name of the tool that signals task completion.
* Defaults to 'complete_task'.
*/
completionToolName?: string;
/**
* Whether or not this subagent has a custom entry point.
* Defaults to `false`, and wraps subagents in a type-safe entry-point.
*/
hasCustomEntryPoint?: boolean;
}
+1 -14
View File
@@ -618,7 +618,6 @@ export interface ConfigParameters {
disabledHooks?: string[];
projectHooks?: { [K in HookEventName]?: HookDefinition[] };
enableAgents?: boolean;
plannerSubagent?: boolean;
enableEventDrivenScheduler?: boolean;
skillsSupport?: boolean;
disabledSkills?: string[];
@@ -838,7 +837,6 @@ export class Config implements McpContext, AgentLoopContext {
overageStrategy: OverageStrategy;
};
private readonly plannerSubagent: boolean;
private readonly enableAgents: boolean;
private agents: AgentSettings;
private readonly enableEventDrivenScheduler: boolean;
@@ -950,7 +948,6 @@ export class Config implements McpContext, AgentLoopContext {
this.model = params.model;
this.disableLoopDetection = params.disableLoopDetection ?? false;
this._activeModel = params.model;
this.plannerSubagent = params.plannerSubagent ?? false;
this.enableAgents = params.enableAgents ?? false;
this.agents = params.agents ?? {};
this.disableLLMCorrection = params.disableLLMCorrection ?? true;
@@ -2484,10 +2481,6 @@ export class Config implements McpContext, AgentLoopContext {
this.approvedPlanPath = path;
}
isPlannerSubagentEnabled(): boolean {
return this.plannerSubagent && this.isPlanEnabled();
}
isAgentsEnabled(): boolean {
return this.enableAgents;
}
@@ -3135,13 +3128,7 @@ export class Config implements McpContext, AgentLoopContext {
for (const definition of definitions) {
try {
if (
definition.kind === 'local' &&
definition.runConfig.hasCustomEntryPoint === true
) {
continue;
}
const tool = new SubagentTool(definition, this, this._messageBus);
const tool = new SubagentTool(definition, this, this.messageBus);
registry.registerTool(tool);
} catch (e: unknown) {
debugLogger.warn(
@@ -364,7 +364,6 @@ export class ToolExecutor {
errorType: undefined,
outputFile,
contentLength: typeof content === 'string' ? content.length : undefined,
isTaskCompletion: toolResult.isTaskCompletion,
data: toolResult.data,
};
-1
View File
@@ -57,7 +57,6 @@ export interface ToolCallResponseInfo {
errorType: ToolErrorType | undefined;
outputFile?: string | undefined;
contentLength?: number;
isTaskCompletion?: boolean;
/**
* Optional data payload for passing structured information back to the caller.
*/
+34 -26
View File
@@ -51,8 +51,8 @@ export class TrackerService {
};
if (task.parentId) {
const parent = await this.getTask(task.parentId);
if (!parent) {
const parentList = await this.listTasks();
if (!parentList.find((t) => t.id === task.parentId)) {
throw new Error(`Parent task with ID ${task.parentId} not found.`);
}
}
@@ -143,7 +143,14 @@ export class TrackerService {
const isClosing = updates.status === TaskStatus.CLOSED;
const changingDependencies = updates.dependencies !== undefined;
const task = await this.getTask(id);
let taskMap: Map<string, TrackerTask> | undefined;
if (isClosing || changingDependencies) {
const allTasks = await this.listTasks();
taskMap = new Map<string, TrackerTask>(allTasks.map((t) => [t.id, t]));
}
const task = taskMap ? taskMap.get(id) : await this.getTask(id);
if (!task) {
throw new Error(`Task with ID ${id} not found.`);
@@ -152,7 +159,9 @@ export class TrackerService {
const updatedTask = { ...task, ...updates, id: task.id };
if (updatedTask.parentId) {
const parentExists = !!(await this.getTask(updatedTask.parentId));
const parentExists = taskMap
? taskMap.has(updatedTask.parentId)
: !!(await this.getTask(updatedTask.parentId));
if (!parentExists) {
throw new Error(
`Parent task with ID ${updatedTask.parentId} not found.`,
@@ -160,12 +169,15 @@ export class TrackerService {
}
}
if (isClosing && task.status !== TaskStatus.CLOSED) {
await this.validateCanClose(updatedTask);
}
if (taskMap) {
if (isClosing && task.status !== TaskStatus.CLOSED) {
this.validateCanClose(updatedTask, taskMap);
}
if (changingDependencies) {
await this.validateNoCircularDependencies(updatedTask);
if (changingDependencies) {
taskMap.set(updatedTask.id, updatedTask);
this.validateNoCircularDependencies(updatedTask, taskMap);
}
}
TrackerTaskSchema.parse(updatedTask);
@@ -185,9 +197,12 @@ export class TrackerService {
/**
* Validates that a task can be closed (all dependencies must be closed).
*/
private async validateCanClose(task: TrackerTask): Promise<void> {
private validateCanClose(
task: TrackerTask,
taskMap: Map<string, TrackerTask>,
): void {
for (const depId of task.dependencies) {
const dep = await this.getTask(depId);
const dep = taskMap.get(depId);
if (!dep) {
throw new Error(`Dependency ${depId} not found for task ${task.id}.`);
}
@@ -202,15 +217,14 @@ export class TrackerService {
/**
* Validates that there are no circular dependencies.
*/
private async validateNoCircularDependencies(
private validateNoCircularDependencies(
task: TrackerTask,
): Promise<void> {
taskMap: Map<string, TrackerTask>,
): void {
const visited = new Set<string>();
const stack = new Set<string>();
const cache = new Map<string, TrackerTask>();
cache.set(task.id, task);
const check = async (currentId: string) => {
const check = (currentId: string) => {
if (stack.has(currentId)) {
throw new Error(
`Circular dependency detected involving task ${currentId}.`,
@@ -223,23 +237,17 @@ export class TrackerService {
visited.add(currentId);
stack.add(currentId);
let currentTask = cache.get(currentId);
const currentTask = taskMap.get(currentId);
if (!currentTask) {
const fetched = await this.getTask(currentId);
if (!fetched) {
throw new Error(`Dependency ${currentId} not found.`);
}
currentTask = fetched;
cache.set(currentId, currentTask);
throw new Error(`Dependency ${currentId} not found.`);
}
for (const depId of currentTask.dependencies) {
await check(depId);
check(depId);
}
stack.delete(currentId);
};
await check(task.id);
check(task.id);
}
}
+75 -41
View File
@@ -10,40 +10,25 @@ import { createMockMessageBus } from '../test-utils/mock-message-bus.js';
import type { Config } from '../config/config.js';
import type { MessageBus } from '../confirmation-bus/message-bus.js';
import { ToolConfirmationOutcome } from './tools.js';
import { SubagentToolWrapper } from '../agents/subagent-tool-wrapper.js';
import type { LocalAgentDefinition } from '../agents/types.js';
vi.mock('../agents/subagent-tool-wrapper.js');
import { ApprovalMode } from '../policy/types.js';
describe('EnterPlanModeTool', () => {
let tool: EnterPlanModeTool;
let mockMessageBus: ReturnType<typeof createMockMessageBus>;
let mockConfig: Config;
let mockPlannerDefinition: LocalAgentDefinition;
let mockConfig: Partial<Config>;
beforeEach(() => {
mockMessageBus = createMockMessageBus();
vi.mocked(mockMessageBus.publish).mockResolvedValue(undefined);
mockPlannerDefinition = {
kind: 'local',
name: 'planner',
description: 'Mock Planner',
inputConfig: { inputSchema: {} },
} as LocalAgentDefinition;
mockConfig = {
setApprovalMode: vi.fn(),
isPlannerSubagentEnabled: vi.fn().mockReturnValue(true),
getAgentRegistry: vi.fn().mockReturnValue({
getDefinition: vi.fn().mockReturnValue(mockPlannerDefinition),
}),
storage: {
getPlansDir: vi.fn().mockReturnValue('/mock/plans/dir'),
} as unknown as Config['storage'],
} as unknown as Config;
};
tool = new EnterPlanModeTool(
mockConfig,
mockConfig as Config,
mockMessageBus as unknown as MessageBus,
);
});
@@ -56,6 +41,7 @@ describe('EnterPlanModeTool', () => {
it('should return info confirmation details when policy says ASK_USER', async () => {
const invocation = tool.build({});
// Mock getMessageBusDecision to return ASK_USER
vi.spyOn(
invocation as unknown as {
getMessageBusDecision: () => Promise<string>;
@@ -78,41 +64,73 @@ describe('EnterPlanModeTool', () => {
);
}
});
it('should return false when policy decision is ALLOW', async () => {
const invocation = tool.build({});
// Mock getMessageBusDecision to return ALLOW
vi.spyOn(
invocation as unknown as {
getMessageBusDecision: () => Promise<string>;
},
'getMessageBusDecision',
).mockResolvedValue('ALLOW');
const result = await invocation.shouldConfirmExecute(
new AbortController().signal,
);
expect(result).toBe(false);
});
it('should throw error when policy decision is DENY', async () => {
const invocation = tool.build({});
// Mock getMessageBusDecision to return DENY
vi.spyOn(
invocation as unknown as {
getMessageBusDecision: () => Promise<string>;
},
'getMessageBusDecision',
).mockResolvedValue('DENY');
await expect(
invocation.shouldConfirmExecute(new AbortController().signal),
).rejects.toThrow(/denied by policy/);
});
});
describe('execute', () => {
it('should delegate to planner subagent', async () => {
const invocation = tool.build({ reason: 'test reason' });
const mockSubInvocation = {
execute: vi.fn().mockResolvedValue({
llmContent: 'Plan created.',
returnDisplay: 'Plan Display',
}),
};
vi.mocked(SubagentToolWrapper).prototype.build = vi
.fn()
.mockReturnValue(mockSubInvocation);
it('should set approval mode to PLAN and return message', async () => {
const invocation = tool.build({});
const result = await invocation.execute(new AbortController().signal);
expect(mockConfig.getAgentRegistry().getDefinition).toHaveBeenCalledWith(
'planner',
expect(mockConfig.setApprovalMode).toHaveBeenCalledWith(
ApprovalMode.PLAN,
);
expect(SubagentToolWrapper).toHaveBeenCalledWith(
mockPlannerDefinition,
mockConfig,
mockMessageBus,
expect(result.llmContent).toContain('Switching to Plan mode');
expect(result.returnDisplay).toBe('Switching to Plan mode');
});
it('should include optional reason in output display but not in llmContent', async () => {
const reason = 'Design new database schema';
const invocation = tool.build({ reason });
const result = await invocation.execute(new AbortController().signal);
expect(mockConfig.setApprovalMode).toHaveBeenCalledWith(
ApprovalMode.PLAN,
);
expect(mockSubInvocation.execute).toHaveBeenCalled();
expect(result.llmContent).toBe('Plan created.');
expect(result.returnDisplay).toBe('Plan Display');
expect(result.llmContent).toBe('Switching to Plan mode.');
expect(result.llmContent).not.toContain(reason);
expect(result.returnDisplay).toContain(reason);
});
it('should not enter plan mode if cancelled', async () => {
const invocation = tool.build({});
// Simulate getting confirmation details
vi.spyOn(
invocation as unknown as {
getMessageBusDecision: () => Promise<string>;
@@ -123,14 +141,30 @@ describe('EnterPlanModeTool', () => {
const details = await invocation.shouldConfirmExecute(
new AbortController().signal,
);
expect(details).not.toBe(false);
if (details) {
// Simulate user cancelling
await details.onConfirm(ToolConfirmationOutcome.Cancel);
}
const result = await invocation.execute(new AbortController().signal);
expect(mockConfig.setApprovalMode).not.toHaveBeenCalled();
expect(result.returnDisplay).toBe('Cancelled');
expect(result.llmContent).toContain('User cancelled');
});
});
describe('validateToolParams', () => {
it('should allow empty params', () => {
const result = tool.validateToolParams({});
expect(result).toBeNull();
});
it('should allow reason param', () => {
const result = tool.validateToolParams({ reason: 'test' });
expect(result).toBeNull();
});
});
});
+7 -34
View File
@@ -11,7 +11,6 @@ import {
Kind,
type ToolInfoConfirmationDetails,
ToolConfirmationOutcome,
type ToolLiveOutput,
} from './tools.js';
import type { MessageBus } from '../confirmation-bus/message-bus.js';
import type { Config } from '../config/config.js';
@@ -19,7 +18,6 @@ import { ENTER_PLAN_MODE_TOOL_NAME } from './tool-names.js';
import { ApprovalMode } from '../policy/types.js';
import { ENTER_PLAN_MODE_DEFINITION } from './definitions/coreTools.js';
import { resolveToolDeclaration } from './definitions/resolver.js';
import { SubagentToolWrapper } from '../agents/subagent-tool-wrapper.js';
export interface EnterPlanModeParams {
reason?: string;
@@ -42,8 +40,6 @@ export class EnterPlanModeTool extends BaseDeclarativeTool<
Kind.Plan,
ENTER_PLAN_MODE_DEFINITION.base.parametersJsonSchema,
messageBus,
/* isOutputMarkdown */ true,
/* canUpdateOutput */ true,
);
}
@@ -116,10 +112,7 @@ export class EnterPlanModeInvocation extends BaseToolInvocation<
};
}
async execute(
signal: AbortSignal,
updateOutput?: (output: ToolLiveOutput) => void,
): Promise<ToolResult> {
async execute(_signal: AbortSignal): Promise<ToolResult> {
if (this.confirmationOutcome === ToolConfirmationOutcome.Cancel) {
return {
llmContent: 'User cancelled entering Plan Mode.',
@@ -129,31 +122,11 @@ export class EnterPlanModeInvocation extends BaseToolInvocation<
this.config.setApprovalMode(ApprovalMode.PLAN);
if (!this.config.isPlannerSubagentEnabled()) {
return {
llmContent: 'Switching to Plan mode.',
returnDisplay: this.params.reason
? `Switching to Plan mode: ${this.params.reason}`
: 'Switching to Plan mode',
};
}
const plannerDefinition = this.config
.getAgentRegistry()
.getDefinition('planner');
if (!plannerDefinition) {
throw new Error('Planner agent not found.');
}
const wrapper = new SubagentToolWrapper(
plannerDefinition,
this.config,
this.messageBus,
);
const subInvocation = wrapper.build({
reason: this.params.reason || 'Requested by main agent',
});
return subInvocation.execute(signal, updateOutput);
return {
llmContent: 'Switching to Plan mode.',
returnDisplay: this.params.reason
? `Switching to Plan mode: ${this.params.reason}`
: 'Switching to Plan mode',
};
}
}
@@ -44,7 +44,6 @@ describe('ExitPlanModeTool', () => {
getTargetDir: vi.fn().mockReturnValue(tempRootDir),
setApprovalMode: vi.fn(),
setApprovedPlanPath: vi.fn(),
isPlannerSubagentEnabled: vi.fn().mockReturnValue(true),
storage: {
getPlansDir: vi.fn().mockReturnValue(mockPlansDir),
} as unknown as Config['storage'],
@@ -201,10 +200,6 @@ describe('ExitPlanModeTool', () => {
const expectedPath = path.join(mockPlansDir, 'test.md');
expect(result).toEqual({
data: {
plan_path: expectedPath,
},
isTaskCompletion: true,
llmContent: `Plan approved. Switching to Default mode (edits will require confirmation).
The approved implementation plan is stored at: ${expectedPath}
@@ -233,10 +228,6 @@ Read and follow the plan strictly during implementation.`,
const expectedPath = path.join(mockPlansDir, 'test.md');
expect(result).toEqual({
data: {
plan_path: expectedPath,
},
isTaskCompletion: true,
llmContent: `Plan approved. Switching to Auto-Edit mode (edits will be applied automatically).
The approved implementation plan is stored at: ${expectedPath}
+5 -10
View File
@@ -26,9 +26,10 @@ import { PlanExecutionEvent } from '../telemetry/types.js';
import { getExitPlanModeDefinition } from './definitions/coreTools.js';
import { resolveToolDeclaration } from './definitions/resolver.js';
import { getPlanModeExitMessage } from '../utils/approvalModeUtils.js';
import { type PlannerAgentOutput } from '../agents/planner-schema.js';
export type ExitPlanModeParams = PlannerAgentOutput;
export interface ExitPlanModeParams {
plan_path: string;
}
export class ExitPlanModeTool extends BaseDeclarativeTool<
ExitPlanModeParams,
@@ -224,20 +225,14 @@ export class ExitPlanModeInvocation extends BaseToolInvocation<
logPlanExecution(this.config, new PlanExecutionEvent(newMode));
const exitMessage = getPlanModeExitMessage(newMode);
const result: ToolResult = {
return {
llmContent: `${exitMessage}
The approved implementation plan is stored at: ${resolvedPlanPath}
Read and follow the plan strictly during implementation.`,
returnDisplay: `Plan approved: ${resolvedPlanPath}`,
};
if (this.config.isPlannerSubagentEnabled()) {
result.isTaskCompletion = true;
result.data = { plan_path: resolvedPlanPath };
}
return result;
} else {
const feedback = payload?.feedback?.trim();
if (feedback) {
-6
View File
@@ -680,12 +680,6 @@ export interface ToolResult {
type?: ToolErrorType; // An optional machine-readable error type (e.g., 'FILE_NOT_FOUND').
};
/**
* If true, this tool result signals an agent indicating they have completed a
* task.
*/
isTaskCompletion?: boolean;
/**
* Optional data payload for passing structured information back to the caller.
*/
-7
View File
@@ -1968,13 +1968,6 @@
},
"additionalProperties": false
},
"plannerSubagent": {
"title": "Planner Subagent",
"description": "Use the new planner subagent for plan mode.",
"markdownDescription": "Use the new planner subagent for plan mode.\n\n- Category: `Experimental`\n- Requires restart: `yes`\n- Default: `false`",
"default": false,
"type": "boolean"
},
"enableAgents": {
"title": "Enable Agents",
"description": "Enable local and remote subagents. Warning: Experimental feature, uses YOLO mode for subagents",
-9
View File
@@ -31,15 +31,6 @@ const packageName = basename(process.cwd());
// build typescript files
execSync('tsc --build', { stdio: 'inherit' });
// Run package-specific bundling if the script exists
const bundleScript = join(process.cwd(), 'scripts', 'bundle-browser-mcp.mjs');
if (packageName === 'core' && existsSync(bundleScript)) {
console.log('Running chrome devtools MCP bundling...');
execSync('npm run bundle:browser-mcp', {
stdio: 'inherit',
});
}
// copy .{md,json} files
execSync('node ../../scripts/copy_files.js', { stdio: 'inherit' });
-8
View File
@@ -95,12 +95,4 @@ if (existsSync(devtoolsDistSrc)) {
console.log('Copied devtools package to bundle/node_modules/');
}
// 6. Copy bundled chrome-devtools-mcp
const bundleMcpSrc = join(root, 'packages/core/dist/bundled');
const bundleMcpDest = join(bundleDir, 'bundled');
if (existsSync(bundleMcpSrc)) {
cpSync(bundleMcpSrc, bundleMcpDest, { recursive: true, dereference: true });
console.log('Copied bundled chrome-devtools-mcp to bundle/bundled/');
}
console.log('Assets copied to bundle/');