mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-02 05:01:04 -07:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bb1ff6eb20 | |||
| ade6dc0cc7 |
@@ -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
|
||||
|
||||
@@ -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
@@ -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');
|
||||
});
|
||||
});
|
||||
Generated
+6
-503
@@ -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"
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'],
|
||||
}),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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' && (
|
||||
|
||||
@@ -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
|
||||
|
||||
+4
@@ -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,
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
@@ -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}',
|
||||
},
|
||||
});
|
||||
@@ -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({
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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' });
|
||||
|
||||
|
||||
@@ -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/');
|
||||
|
||||
Reference in New Issue
Block a user