Compare commits

...

39 Commits

Author SHA1 Message Date
Mahima Shanware 9ff03ddd20 fix(core): remove redundant plansDirCache to avoid stale configs and satisfy bot 2026-04-07 16:53:10 +00:00
Mahima Shanware 50d8880e9c fix(core): remove lint disablers in plan directory validation
Replaced eslint-disable-next-line comments with explicit type guarding (checking instanceof Error and 'code') and used process.stderr.write instead of console.warn to comply with project linting rules natively.
2026-04-07 16:40:32 +00:00
Mahima Shanware 495931be79 fix(core): remove duplicate isSubpath import
Moved the isSubpath and resolveToRealPath imports to the top of the file to satisfy the static analysis bot, which failed to detect the existing import statement further down in the file.
2026-04-07 16:40:31 +00:00
Mahima Shanware df8f1a8f03 fix: address review bot feedback on concurrency and error handling
- Reverted the 'isIdle' guard in AppContainer.tsx to ensure slash commands entered while the agent is busy are correctly processed or queued, preventing them from falling through as regular chat text.
- Enhanced the physical path validation in config.ts to gracefully handle 'mkdirSync' failures (e.g. EACCES). The CLI will now log a warning and return the lexically-validated path instead of throwing a misleading 'Security violation' via 'resolveToRealPath'.
2026-04-07 16:40:31 +00:00
Mahima Shanware 7c0cfd53df fix(core): gracefully degrade global config dir path validation on EACCES
If the user's home directory or global `.gemini` config directory has
restrictive permissions that prevent `realpathSync` from succeeding, the
CLI should not crash. Instead, it now gracefully degrades by omitting
the global config directory from the valid security boundaries for plan
directories, allowing the CLI to continue operating securely within the
project root.
2026-04-07 16:40:31 +00:00
Mahima Shanware 432df7982c fix: address final review feedback on context resets and pre-creation path validation 2026-04-07 16:40:31 +00:00
Mahima Shanware 35fd166bce fix(core): ensure global gemini dir resolution is crash-safe during plan validation 2026-04-07 16:40:31 +00:00
Mahima Shanware e42b762553 fix(core): ensure path validation always executes even if mkdirSync fails 2026-04-07 16:40:31 +00:00
Mahima Shanware 76c28142eb fix(cli): remove /plan extension context hacks and fix dropped queue messages 2026-04-07 16:40:31 +00:00
Mahima Shanware 7ab4c3d61f fix(core,cli): resolve TOCTOU, concurrency, and performance regressions in plan resolution 2026-04-07 16:40:31 +00:00
Mahima Shanware ded474c2d0 fix(core): fail-closed security for plan directory TOCTOU
Resolves security review findings:
- Reordered resolveToRealPath before mkdirSync to fully eliminate TOCTOU risks with symlink injection.
- Fail closed by re-throwing 'Security violation' errors instead of swallowing them.
- Replaced lint-disabler with process.stderr.write for legitimate fallback warnings.
- Used direct context string as LRUCache key to avoid collision with an extension potentially named 'default'.
2026-04-07 16:35:24 +00:00
Mahima Shanware 3256b16039 fix(core): mitigate TOCTOU vulnerability in plan directory creation
This change addresses a critical security review finding regarding a Time-of-Check to Time-of-Use (TOCTOU) vulnerability.

Previously, plan directory paths were validated using `isSubpath` before creation. However, an attacker could potentially replace a path component with a symlink pointing outside the project root exactly between validation and creation.

By resolving the physical path *after* `fs.mkdirSync` using `resolveToRealPath` and then verifying it with `isSubpath`, we ensure that the actual directory created on disk resides safely within the workspace. Any violation results in a warning, and the malicious path is prevented from being added to the agent's `workspaceContext`.
2026-04-07 16:35:24 +00:00
Mahima Shanware 5e89760856 perf(core): optimize plan directory resolution with LRUCache and cached project root
This commit addresses the final performance and usability review comments:

- **Performance:** Introduced `LRUCache` for `plansDirCache` and `initializedPlanDirs` to prevent redundant, synchronous filesystem calls to `Storage.getPlansDir` on every turn.
- **Performance:** Cached the resolved `realProjectRoot` in the `Storage` constructor, eliminating expensive synchronous symlink resolution calls during active command routing.
- **Usability:** Replaced hard `throw` with `console.warn` when `fs.mkdirSync` fails (e.g., `EACCES`, `EEXIST`), allowing the CLI to gracefully degrade and continue functioning rather than crashing the entire process.
- **Validation:** Updated `config.test.ts` to verify the exact warning messages emitted during filesystem failures.
2026-04-07 16:35:24 +00:00
Mahima Shanware 6559fdbc31 fix(core,cli): address review findings for plan dir resolution and security
This commit addresses several critical findings from the review bot:

- **Security:** Implemented defense-in-depth symlink resolution. Removed insecure string-based fallbacks in `Storage.getPlansDir` and added a mandatory `isSubpath` validation AFTER directory creation in `Config.getPlansDir` to prevent TOCTOU traversal attacks.
- **Architecture:** Fixed a race condition where active extension context was mutated synchronously in `AppContainer`, potentially corrupting concurrent background tasks. Mutation now occurs within the command execution pipeline.
- **Robustness:** Switched to canonical path checking for `plan` command detection to support aliases and subcommands.
- **Regressions:** Added a `planEnabled` guard to prevent unwanted directory creation when the planning feature is disabled.
- **Validation:** Added exhaustive unit tests covering sequential context switching, shared directory deduplication, and symlink security edge cases.
2026-04-07 16:35:24 +00:00
Mahima Shanware a5c2bf81f4 fix(core): remove redundant ENOENT fallback in getPlansDir to fix traversal vulnerability
This removes the insecure ENOENT fallback in `Storage.getPlansDir` that could be exploited to bypass the `isSubpath` check via symlinks. The fallback was unnecessary because the underlying `resolveToRealPath` function (via `robustRealpath`) was recently updated to gracefully handle and resolve symlinks for non-existent target paths.
2026-04-07 16:35:24 +00:00
Mahima Shanware b5d92caf89 fix(core): handle plan dir EEXIST safely and rely on mkdir idempotency
This addresses a potential TOCTOU vulnerability and edge case identified during review. The redundant `fs.existsSync` check in `getPlansDir` has been removed, allowing `fs.mkdirSync(..., { recursive: true })` to safely handle directory idempotency.

By relying directly on `mkdirSync`, we ensure that if a non-directory file already exists at the target path, the system will correctly throw an `EEXIST` error rather than silently treating the file as a directory and crashing later during workspace registration.
2026-04-07 16:35:23 +00:00
Mahima Shanware 81c74e1483 perf(core): cache initialized plan directories
Adds caching to getPlansDir to avoid redundant synchronous disk I/O and repeated workspace context registrations.
2026-04-07 16:35:23 +00:00
Mahima Shanware b2f7c157ce fix(cli): consistently clear sticky extension context
This fixes a bug where the active extension context would remain sticky when a user switched from an extension command to a standard non-plan command, or to an extension without a plan directory.

The context is now correctly reset to undefined when an extension command without a plan directory is executed, preventing subsequent plan mode invocations from incorrectly targeting the previous extension's folder.
2026-04-07 16:35:23 +00:00
Mahima Shanware 0a8195fb3a fix(core): address extension context stickiness and symlink path resolution
This commit addresses two bugs identified during review:

1. Cleared the sticky `activeExtensionContext` when the standard `/plan` command is executed, ensuring subsequent prompts correctly target the default global plan directory.
2. Fixed a path resolution regression in `Storage.getPlansDir()` by constructing the fallback ENOENT path directly against the real project root. This prevents `isSubpath` validation failures and potential traversal vulnerabilities when the project root is a symlink.
2026-04-07 16:35:23 +00:00
Mahima Shanware 058b5e31b4 feat(cli): wire active extension context into slash command routing
Extracts the extension context from slash commands based on their registered metadata and sets it as the active context in the Config before execution. This enables the backend to dynamically route plan directories based on the extension that owns the invoked command.
2026-04-07 16:35:23 +00:00
Mahima Shanware 402a96a519 fix(core): migrate consumers to lazily-evaluated getPlansDir
Updates prompts and tool implementations (edit, write-file, enter/exit plan mode) to route through Config.getPlansDir() instead of Storage.getPlansDir(). This ensures the plan directory is lazily created exactly when these features attempt to use it, preventing ENOENT failures.
2026-04-07 16:35:23 +00:00
Mahima Shanware bdf90e9985 feat(core): dynamic MRU plan directory resolution and lazy initialization
Introduces active extension context tracking in config to support dynamic switching of plan directories. Resolves circular dependency in storage by deferring plan directory creation until on-demand use, preventing ENOENT errors on non-existent paths.
2026-04-07 16:34:08 +00:00
Emily Hedlund 1762c9c509 temporarily disable sandbox integration test on windows (#24786) 2026-04-07 15:33:40 +00:00
Abhijit Balaji 0025978d76 feat(cli): support selective topic expansion and click-to-expand (#24793) 2026-04-07 15:00:40 +00:00
Gaurav 4c5e887732 feat(telemetry): add browser agent clearcut metrics (#24688) 2026-04-07 07:48:38 +00:00
Abhi 83096c68b0 fix(policy): allow complete_task in plan mode (#24771) 2026-04-07 03:43:42 +00:00
Christian Gunderman d2b775f9a7 Add an eval for and fix unsafe cloning behavior. (#24457) 2026-04-07 03:17:44 +00:00
Spencer 0a8da988ed fix(cli): ensure skills list outputs to stdout in non-interactive environments (#24566) 2026-04-07 02:40:23 +00:00
David Pierce 984f02c180 relax tool sandboxing overrides for plan mode to match defaults. (#24762) 2026-04-06 22:18:10 +00:00
Tommaso Sciortino df67f973ed fix(cli): respect global environment variable allowlist (#24767) 2026-04-06 22:17:55 +00:00
Christian Gunderman 7872d6d7fe fix(ui): improve narration suppression and reduce flicker (#24635) 2026-04-06 21:18:59 +00:00
Gaurav e116aa34f4 fix(browser): remove premature browser cleanup after subagent invocation (#24753) 2026-04-06 21:17:31 +00:00
Abhijit Balaji ad98294352 Revert "feat(core,cli): prioritize summary for topics (#24608)" (#24777) 2026-04-06 20:33:18 +00:00
Dev Randalpura 2353a6d253 fix(ui): fixed auth race condition causing logo to flicker (#24652) 2026-04-06 20:17:05 +00:00
krishdef7 8ac560d2c9 fix(core): handle partial llm_request in BeforeModel hook override (#22326) 2026-04-06 20:11:38 +00:00
Tommaso Sciortino c6a9d3de13 fix(cli): remove -S from shebang to fix Windows and BSD execution (#24756) 2026-04-06 19:06:56 +00:00
Christian Gunderman 8f131ffef7 Fix issue where topic headers can be posted back to back (#24759) 2026-04-06 18:36:22 +00:00
Jacob Richman 70f6d6a992 split context (#24623) 2026-04-06 17:20:38 +00:00
cynthialong0-0 c96cb09e09 feat(cli): add role specific metrics to /stats (#24659) 2026-04-06 16:20:48 +00:00
92 changed files with 2619 additions and 727 deletions
@@ -33,6 +33,16 @@ evaluation.
- **Warning**: Do not lose test fidelity by making prompts too direct/easy.
- **Primary Fix Trigger**: Adjust tool descriptions, system prompts
(`snippets.ts`), or **modules that contribute to the prompt template**.
- Fixes should generally try to improve the prompt `@packages/core/src/prompts/snippets.ts` first.
- **Instructional Generality**: Changes to the system prompt should aim to be as general as possible while still accomplishing the goal. Specificity should be added only as needed.
- **Principle**: Instead of creating "forbidden lists" for specific syntax (e.g., "Don't use `Object.create()`"), formulate a broader engineering principle that covers the underlying issue (e.g., "Prioritize explicit composition over hidden prototype manipulation"). This improves steerability across a wider range of similar scenarios.
- *Low Specificity*: "Follow ecosystem best practices"
- *Medium Specificity*: "Utilize OOP and functional best practices, as applicable"
- *High Specificity*: Provide ecosystem-specific hints as examples of a broader principle rather than direct instructions. e.g., "NEVER use hacks like bypassing the type system or employing 'hidden' logic (e.g.: reflection, prototype manipulation). Instead, use explicit and idiomatic language features (e.g.: type guards, explicit class instantiation, or object spread) that maintain structural integrity."
- **Prompt Simplification**: Once the test is passing, use `ask_user` to determine if prompt simplification is desired.
- **Criteria**: Simplification should be attempted only if there are related clauses that can be de-duplicated or reparented under a single heading.
- **Verification**: As part of simplification, you MUST identify and run any behavioral eval tests that might be affected by the changes to ensure no regressions are introduced.
- Test fixes should not "cheat" by changing a test's `GEMINI.md` file or by updating the test's prompt to instruct it to not repro the bug.
- **Warning**: Prompts have multiple configurations; ensure your fix targets
the correct config for the model in question.
4. **Architecture Options**: If prompt or instruction tuning triggers no
+64
View File
@@ -0,0 +1,64 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { evalTest, TestRig } from './test-helper.js';
evalTest('USUALLY_PASSES', {
name: 'Reproduction: Agent uses Object.create() for cloning/delegation',
prompt:
'Create a utility function `createScopedConfig(config: Config, additionalDirectories: string[]): Config` in `packages/core/src/config/scoped-config.ts` that returns a new Config instance. This instance should override `getWorkspaceContext()` to include the additional directories, but delegate all other method calls (like `isPathAllowed` or `validatePathAccess`) to the original config. Note that `Config` is a complex class with private state and cannot be easily shallow-copied or reconstructed.',
files: {
'packages/core/src/config/config.ts': `
export class Config {
private _internalState = 'secret';
constructor(private workspaceContext: any) {}
getWorkspaceContext() { return this.workspaceContext; }
isPathAllowed(path: string) {
return this.getWorkspaceContext().isPathWithinWorkspace(path);
}
validatePathAccess(path: string) {
if (!this.isPathAllowed(path)) return 'Denied';
return null;
}
}`,
'packages/core/src/utils/workspaceContext.ts': `
export class WorkspaceContext {
constructor(private root: string, private additional: string[] = []) {}
getDirectories() { return [this.root, ...this.additional]; }
isPathWithinWorkspace(path: string) {
return this.getDirectories().some(d => path.startsWith(d));
}
}`,
'package.json': JSON.stringify({
name: 'test-project',
version: '1.0.0',
type: 'module',
}),
},
assert: async (rig: TestRig) => {
const filePath = 'packages/core/src/config/scoped-config.ts';
const content = rig.readFile(filePath);
if (!content) {
throw new Error(`File ${filePath} was not created.`);
}
// Strip comments to avoid false positives.
const codeWithoutComments = content.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '');
// Ensure that the agent did not use Object.create() in the implementation.
// We check for the call pattern specifically using a regex to avoid false positives in comments.
const hasObjectCreate = /\bObject\.create\s*\(/.test(codeWithoutComments);
if (hasObjectCreate) {
throw new Error(
'Evaluation Failed: Agent used Object.create() for cloning. ' +
'This behavior is forbidden by the project lint rules (no-restricted-syntax). ' +
'Implementation found:\n\n' +
content,
);
}
},
});
+43
View File
@@ -215,4 +215,47 @@ export default app;
expect(fs.existsSync(path.join(rig.testDir, 'src/routes.ts'))).toBe(true);
},
});
/**
* Regression test for a bug where update_topic was called multiple times in a
* row. We have seen cases of this occurring in earlier versions of the update_topic
* system instruction, prior to https://github.com/google-gemini/gemini-cli/pull/24640.
* This test demonstrated that there are cases where it can still occur and validates
* the prompt change that improves the behavior.
*/
evalTest('USUALLY_PASSES', {
name: 'update_topic should not be called twice in a row',
prompt: `
We need to build a C compiler.
Before you write any code, you must formally declare your strategy.
First, declare that you will build a Lexer.
Then, immediately realize that is wrong and declare that you will actually build a Parser instead.
Finally, create 'parser.c'.
`,
files: {
'package.json': JSON.stringify({ name: 'test-project' }),
'.gemini/settings.json': JSON.stringify({
experimental: {
topicUpdateNarration: true,
},
}),
},
assert: async (rig) => {
const toolLogs = rig.readToolLogs();
// Check for back-to-back update_topic calls
for (let i = 1; i < toolLogs.length; i++) {
if (
toolLogs[i - 1].toolRequest.name === UPDATE_TOPIC_TOOL_NAME &&
toolLogs[i].toolRequest.name === UPDATE_TOPIC_TOOL_NAME
) {
throw new Error(
`Detected back-to-back ${UPDATE_TOPIC_TOOL_NAME} calls at index ${i - 1} and ${i}`,
);
}
}
},
});
});
@@ -0,0 +1,8 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I'll browse to example.com twice to verify the content. Let me first check the page title, then check the links on the page."},{"functionCall":{"name":"browser_agent","args":{"task":"Navigate to https://example.com and tell me the page title using the accessibility tree"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":50,"totalTokenCount":150}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"navigate_page","args":{"url":"https://example.com"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":100,"candidatesTokenCount":30,"totalTokenCount":130}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"take_snapshot","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":150,"candidatesTokenCount":20,"totalTokenCount":170}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"complete_task","args":{"result":{"success":true,"summary":"Page title is 'Example Domain'."}}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":40,"totalTokenCount":240}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"The page title is 'Example Domain'. Now let me check the links on the page."},{"functionCall":{"name":"browser_agent","args":{"task":"Take a snapshot of the accessibility tree on the currently open page and tell me about any links"}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":50,"totalTokenCount":250}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"take_snapshot","args":{}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":150,"candidatesTokenCount":20,"totalTokenCount":170}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"functionCall":{"name":"complete_task","args":{"result":{"success":true,"summary":"Found a link 'More information...' pointing to iana.org."}}}}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":200,"candidatesTokenCount":40,"totalTokenCount":240}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"I browsed example.com twice using persistent browser sessions:\n\n1. **First visit**: Page title is 'Example Domain'\n2. **Second visit**: Found a link 'More information...' pointing to iana.org\n\nThe browser stayed open between both visits, confirming persistent session management works correctly."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":300,"candidatesTokenCount":60,"totalTokenCount":360}}]}
+45
View File
@@ -229,6 +229,51 @@ describe.skipIf(!chromeAvailable)('browser-agent', () => {
assertModelHasOutput(result);
});
it('should keep browser open across multiple browser_agent invocations', async () => {
rig.setup('browser-persistent-session', {
fakeResponsesPath: join(
__dirname,
'browser-agent.persistent-session.responses',
),
settings: {
agents: {
overrides: {
browser_agent: {
enabled: true,
},
},
browser: {
headless: true,
sessionMode: 'isolated',
},
},
},
});
const result = await rig.run({
args: 'Browse to example.com twice: first get the page title, then check for links.',
});
const toolLogs = rig.readToolLogs();
const browserCalls = toolLogs.filter(
(t) => t.toolRequest.name === 'browser_agent',
);
// Both browser_agent invocations must succeed — if the browser was
// incorrectly closed after the first call (regression #24210),
// the second call would fail.
expect(
browserCalls.length,
'Expected browser_agent to be called twice',
).toBe(2);
expect(
browserCalls.every((c) => c.toolRequest.success),
'Both browser_agent calls should succeed',
).toBe(true);
assertModelHasOutput(result);
});
it('should handle tool confirmation for write_file without crashing', async () => {
rig.setup('tool-confirmation', {
fakeResponsesPath: join(
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env -S node --no-warnings=DEP0040
#!/usr/bin/env node
/**
* @license
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env -S node --no-warnings=DEP0040
#!/usr/bin/env node
/**
* @license
+26 -27
View File
@@ -4,8 +4,16 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { coreEvents, type Config } from '@google/gemini-cli-core';
import {
vi,
describe,
it,
expect,
beforeEach,
afterEach,
type MockInstance,
} from 'vitest';
import { type Config } from '@google/gemini-cli-core';
import { handleList, listCommand } from './list.js';
import { loadSettings, type LoadedSettings } from '../../config/settings.js';
import { loadCliConfig } from '../../config/config.js';
@@ -32,12 +40,16 @@ vi.mock('../utils.js', () => ({
describe('skills list command', () => {
const mockLoadSettings = vi.mocked(loadSettings);
const mockLoadCliConfig = vi.mocked(loadCliConfig);
let stdoutWriteSpy: MockInstance<typeof process.stdout.write>;
beforeEach(async () => {
vi.clearAllMocks();
mockLoadSettings.mockReturnValue({
merged: {},
} as unknown as LoadedSettings);
stdoutWriteSpy = vi
.spyOn(process.stdout, 'write')
.mockImplementation(() => true);
});
afterEach(() => {
@@ -56,10 +68,7 @@ describe('skills list command', () => {
await handleList({});
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
'log',
'No skills discovered.',
);
expect(stdoutWriteSpy).toHaveBeenCalledWith('No skills discovered.\n');
});
it('should list all discovered skills', async () => {
@@ -87,24 +96,19 @@ describe('skills list command', () => {
await handleList({});
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
'log',
chalk.bold('Discovered Agent Skills:'),
expect(stdoutWriteSpy).toHaveBeenCalledWith(
chalk.bold('Discovered Agent Skills:') + '\n\n',
);
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
'log',
expect(stdoutWriteSpy).toHaveBeenCalledWith(
expect.stringContaining('skill1'),
);
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
'log',
expect(stdoutWriteSpy).toHaveBeenCalledWith(
expect.stringContaining(chalk.green('[Enabled]')),
);
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
'log',
expect(stdoutWriteSpy).toHaveBeenCalledWith(
expect.stringContaining('skill2'),
);
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
'log',
expect(stdoutWriteSpy).toHaveBeenCalledWith(
expect.stringContaining(chalk.red('[Disabled]')),
);
});
@@ -135,12 +139,10 @@ describe('skills list command', () => {
// Default
await handleList({ all: false });
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
'log',
expect(stdoutWriteSpy).toHaveBeenCalledWith(
expect.stringContaining('regular'),
);
expect(coreEvents.emitConsoleLog).not.toHaveBeenCalledWith(
'log',
expect(stdoutWriteSpy).not.toHaveBeenCalledWith(
expect.stringContaining('builtin'),
);
@@ -148,16 +150,13 @@ describe('skills list command', () => {
// With all: true
await handleList({ all: true });
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
'log',
expect(stdoutWriteSpy).toHaveBeenCalledWith(
expect.stringContaining('regular'),
);
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
'log',
expect(stdoutWriteSpy).toHaveBeenCalledWith(
expect.stringContaining('builtin'),
);
expect(coreEvents.emitConsoleLog).toHaveBeenCalledWith(
'log',
expect(stdoutWriteSpy).toHaveBeenCalledWith(
expect.stringContaining(chalk.gray(' [Built-in]')),
);
});
+7 -8
View File
@@ -5,7 +5,6 @@
*/
import type { CommandModule } from 'yargs';
import { debugLogger } from '@google/gemini-cli-core';
import { loadSettings } from '../../config/settings.js';
import { loadCliConfig, type CliArgs } from '../../config/config.js';
import { exitCli } from '../utils.js';
@@ -42,12 +41,11 @@ export async function handleList(args: { all?: boolean }) {
});
if (skills.length === 0) {
debugLogger.log('No skills discovered.');
process.stdout.write('No skills discovered.\n');
return;
}
debugLogger.log(chalk.bold('Discovered Agent Skills:'));
debugLogger.log('');
process.stdout.write(chalk.bold('Discovered Agent Skills:') + '\n\n');
for (const skill of skills) {
const status = skill.disabled
@@ -56,10 +54,11 @@ export async function handleList(args: { all?: boolean }) {
const builtinSuffix = skill.isBuiltin ? chalk.gray(' [Built-in]') : '';
debugLogger.log(`${chalk.bold(skill.name)} ${status}${builtinSuffix}`);
debugLogger.log(` Description: ${skill.description}`);
debugLogger.log(` Location: ${skill.location}`);
debugLogger.log('');
process.stdout.write(
`${chalk.bold(skill.name)} ${status}${builtinSuffix}\n`,
);
process.stdout.write(` Description: ${skill.description}\n`);
process.stdout.write(` Location: ${skill.location}\n\n`);
}
}
+2 -1
View File
@@ -3796,7 +3796,8 @@ describe('loadCliConfig mcpEnabled', () => {
]);
const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.storage.getPlansDir()).toContain('ext-plans-dir');
config.setActiveExtensionContext('ext-plan');
expect(config.getPlansDir()).toContain('ext-plans-dir');
});
it('should NOT use plan directory from active extension when user has specified one', async () => {
+10 -6
View File
@@ -610,9 +610,12 @@ export async function loadCliConfig(
});
await extensionManager.loadExtensions();
const extensionPlanSettings = extensionManager
.getExtensions()
.find((ext) => ext.isActive && ext.plan?.directory)?.plan;
const extensionPlanDirs: Record<string, string> = {};
for (const ext of extensionManager.getExtensions()) {
if (ext.isActive && ext.plan?.directory) {
extensionPlanDirs[ext.name] = ext.plan.directory;
}
}
const experimentalJitContext = settings.experimental.jitContext;
@@ -938,6 +941,8 @@ export async function loadCliConfig(
: undefined,
blockedEnvironmentVariables:
settings.security?.environmentVariableRedaction?.blocked,
allowedEnvironmentVariables:
settings.security?.environmentVariableRedaction?.allowed,
enableEnvironmentVariableRedaction:
settings.security?.environmentVariableRedaction?.enabled,
userMemory: memoryContent,
@@ -980,9 +985,8 @@ export async function loadCliConfig(
plan: settings.general?.plan?.enabled ?? true,
tracker: settings.experimental?.taskTracker,
directWebFetch: settings.experimental?.directWebFetch,
planSettings: settings.general?.plan?.directory
? settings.general.plan
: (extensionPlanSettings ?? settings.general?.plan),
planSettings: settings.general?.plan,
extensionPlanDirs,
enableEventDrivenScheduler: true,
skillsSupport: settings.skills?.enabled ?? true,
disabledSkills: settings.skills?.disabled,
@@ -23,6 +23,8 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
isInteractive: vi.fn(() => false),
isInitialized: vi.fn(() => true),
setTerminalBackground: vi.fn(),
setActiveExtensionContext: vi.fn(),
hasExtensionPlanDir: vi.fn(() => true),
storage: {
getProjectTempDir: vi.fn().mockReturnValue('/tmp/gemini-test'),
initialize: vi.fn().mockResolvedValue(undefined),
+77 -56
View File
@@ -504,6 +504,8 @@ const baseMockUiState = {
history: [],
renderMarkdown: true,
streamingState: StreamingState.Idle,
isConfigInitialized: true,
isAuthenticating: false,
terminalWidth: 100,
terminalHeight: 40,
currentModel: 'gemini-pro',
@@ -598,6 +600,9 @@ const mockUIActions: UIActions = {
clearAccountSuspension: vi.fn(),
};
import { type TextBuffer } from '../ui/components/shared/text-buffer.js';
import { InputContext, type InputState } from '../ui/contexts/InputContext.js';
let capturedOverflowState: OverflowState | undefined;
let capturedOverflowActions: OverflowActions | undefined;
const ContextCapture: React.FC<{ children: React.ReactNode }> = ({
@@ -614,6 +619,7 @@ export const renderWithProviders = async (
shellFocus = true,
settings = mockSettings,
uiState: providedUiState,
inputState: providedInputState,
width,
mouseEventsEnabled = false,
config,
@@ -625,6 +631,7 @@ export const renderWithProviders = async (
shellFocus?: boolean;
settings?: LoadedSettings;
uiState?: Partial<UIState>;
inputState?: Partial<InputState>;
width?: number;
mouseEventsEnabled?: boolean;
config?: Config;
@@ -659,6 +666,18 @@ export const renderWithProviders = async (
},
) as UIState;
const inputState = {
buffer: { text: '' } as unknown as TextBuffer,
userMessages: [],
shellModeActive: false,
showEscapePrompt: false,
copyModeEnabled: false,
inputWidth: 80,
suggestionsWidth: 40,
...(providedUiState as unknown as Partial<InputState>),
...providedInputState,
};
if (persistentState?.get) {
persistentStateMock.get.mockImplementation(persistentState.get);
}
@@ -708,63 +727,65 @@ export const renderWithProviders = async (
<AppContext.Provider value={appState}>
<ConfigContext.Provider value={config}>
<SettingsContext.Provider value={settings}>
<UIStateContext.Provider value={finalUiState}>
<VimModeProvider>
<ShellFocusContext.Provider value={shellFocus}>
<SessionStatsProvider>
<StreamingContext.Provider
value={finalUiState.streamingState}
>
<UIActionsContext.Provider value={finalUIActions}>
<OverflowProvider>
<ToolActionsProvider
config={config}
toolCalls={allToolCalls}
isExpanded={
toolActions?.isExpanded ??
vi.fn().mockReturnValue(false)
}
toggleExpansion={
toolActions?.toggleExpansion ?? vi.fn()
}
toggleAllExpansion={
toolActions?.toggleAllExpansion ?? vi.fn()
}
>
<AskUserActionsProvider
request={null}
onSubmit={vi.fn()}
onCancel={vi.fn()}
<InputContext.Provider value={inputState}>
<UIStateContext.Provider value={finalUiState}>
<VimModeProvider>
<ShellFocusContext.Provider value={shellFocus}>
<SessionStatsProvider>
<StreamingContext.Provider
value={finalUiState.streamingState}
>
<UIActionsContext.Provider value={finalUIActions}>
<OverflowProvider>
<ToolActionsProvider
config={config}
toolCalls={allToolCalls}
isExpanded={
toolActions?.isExpanded ??
vi.fn().mockReturnValue(false)
}
toggleExpansion={
toolActions?.toggleExpansion ?? vi.fn()
}
toggleAllExpansion={
toolActions?.toggleAllExpansion ?? vi.fn()
}
>
<KeypressProvider>
<MouseProvider
mouseEventsEnabled={mouseEventsEnabled}
>
<TerminalProvider>
<ScrollProvider>
<ContextCapture>
<Box
width={terminalWidth}
flexShrink={0}
flexGrow={0}
flexDirection="column"
>
{comp}
</Box>
</ContextCapture>
</ScrollProvider>
</TerminalProvider>
</MouseProvider>
</KeypressProvider>
</AskUserActionsProvider>
</ToolActionsProvider>
</OverflowProvider>
</UIActionsContext.Provider>
</StreamingContext.Provider>
</SessionStatsProvider>
</ShellFocusContext.Provider>
</VimModeProvider>
</UIStateContext.Provider>
<AskUserActionsProvider
request={null}
onSubmit={vi.fn()}
onCancel={vi.fn()}
>
<KeypressProvider>
<MouseProvider
mouseEventsEnabled={mouseEventsEnabled}
>
<TerminalProvider>
<ScrollProvider>
<ContextCapture>
<Box
width={terminalWidth}
flexShrink={0}
flexGrow={0}
flexDirection="column"
>
{comp}
</Box>
</ContextCapture>
</ScrollProvider>
</TerminalProvider>
</MouseProvider>
</KeypressProvider>
</AskUserActionsProvider>
</ToolActionsProvider>
</OverflowProvider>
</UIActionsContext.Provider>
</StreamingContext.Provider>
</SessionStatsProvider>
</ShellFocusContext.Provider>
</VimModeProvider>
</UIStateContext.Provider>
</InputContext.Provider>
</SettingsContext.Provider>
</ConfigContext.Provider>
</AppContext.Provider>
+9 -7
View File
@@ -122,13 +122,17 @@ vi.mock('ink', async (importOriginal) => {
};
});
import { InputContext, type InputState } from './contexts/InputContext.js';
// Helper component will read the context values provided by AppContainer
// so we can assert against them in our tests.
let capturedUIState: UIState;
let capturedInputState: InputState;
let capturedUIActions: UIActions;
let capturedOverflowActions: OverflowActions;
function TestContextConsumer() {
capturedUIState = useContext(UIStateContext)!;
capturedInputState = useContext(InputContext)!;
capturedUIActions = useContext(UIActionsContext)!;
capturedOverflowActions = useOverflowActions()!;
return null;
@@ -3036,7 +3040,7 @@ describe('AppContainer State Management', () => {
});
const { unmount } = await act(async () => renderAppContainer());
expect(capturedUIState.userMessages).toContain('previous message');
expect(capturedInputState.userMessages).toContain('previous message');
const { onCancelSubmit } = extractUseGeminiStreamArgs(
mockedUseGeminiStream.mock.lastCall!,
@@ -3064,8 +3068,8 @@ describe('AppContainer State Management', () => {
const { rerender, unmount } = await act(async () => renderAppContainer());
// Verify userMessages is populated from inputHistory
expect(capturedUIState.userMessages).toContain('first prompt');
expect(capturedUIState.userMessages).toContain('second prompt');
expect(capturedInputState.userMessages).toContain('first prompt');
expect(capturedInputState.userMessages).toContain('second prompt');
// Clear the conversation history (simulating /clear command)
const mockClearItems = vi.fn();
@@ -3084,8 +3088,8 @@ describe('AppContainer State Management', () => {
// Verify that userMessages still contains the input history
// (it should not be affected by clearing conversation history)
expect(capturedUIState.userMessages).toContain('first prompt');
expect(capturedUIState.userMessages).toContain('second prompt');
expect(capturedInputState.userMessages).toContain('first prompt');
expect(capturedInputState.userMessages).toContain('second prompt');
unmount();
});
@@ -3191,9 +3195,7 @@ describe('AppContainer State Management', () => {
);
unmount();
});
});
describe('Overflow Hint Handling', () => {
beforeEach(() => {
vi.useFakeTimers();
});
+52 -40
View File
@@ -194,6 +194,8 @@ import {
} from './hooks/useVisibilityToggle.js';
import { useKeyMatchers } from './hooks/useKeyMatchers.js';
import { InputContext } from './contexts/InputContext.js';
/**
* The fraction of the terminal width to allocate to the shell.
* This provides horizontal padding.
@@ -1345,10 +1347,11 @@ Logging in with Google... Restarting Gemini CLI to continue.
isToolExecuting(pendingHistoryItems);
if (isSlash && isAgentRunning) {
const { commandToExecute } = parseSlashCommand(
const parsedCommand = parseSlashCommand(
submittedValue,
slashCommands ?? [],
);
const commandToExecute = parsedCommand.commandToExecute;
if (commandToExecute?.isSafeConcurrent) {
void handleSlashCommand(submittedValue);
addInput(submittedValue);
@@ -2328,6 +2331,27 @@ Logging in with Google... Restarting Gemini CLI to continue.
};
}, [config, refreshStatic]);
const inputState = useMemo(
() => ({
buffer,
userMessages: inputHistory,
shellModeActive,
showEscapePrompt,
copyModeEnabled,
inputWidth,
suggestionsWidth,
}),
[
buffer,
inputHistory,
shellModeActive,
showEscapePrompt,
copyModeEnabled,
inputWidth,
suggestionsWidth,
],
);
const uiState: UIState = useMemo(
() => ({
history: historyManager.history,
@@ -2371,11 +2395,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
initError,
pendingGeminiHistoryItems,
thought,
shellModeActive,
userMessages: inputHistory,
buffer,
inputWidth,
suggestionsWidth,
isInputActive,
isResuming,
shouldShowIdePrompt,
@@ -2391,7 +2410,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
renderMarkdown,
ctrlCPressedOnce: ctrlCPressCount >= 1,
ctrlDPressedOnce: ctrlDPressCount >= 1,
showEscapePrompt,
shortcutsHelpVisible,
cleanUiDetailsVisible,
isFocused,
@@ -2443,7 +2461,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
embeddedShellFocused,
showDebugProfiler,
customDialog,
copyModeEnabled,
transientMessage,
bannerData,
bannerVisible,
@@ -2498,11 +2515,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
initError,
pendingGeminiHistoryItems,
thought,
shellModeActive,
inputHistory,
buffer,
inputWidth,
suggestionsWidth,
isInputActive,
isResuming,
shouldShowIdePrompt,
@@ -2518,7 +2530,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
renderMarkdown,
ctrlCPressCount,
ctrlDPressCount,
showEscapePrompt,
shortcutsHelpVisible,
cleanUiDetailsVisible,
isFocused,
@@ -2570,7 +2581,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
customDialog,
apiKeyDefaultValue,
authState,
copyModeEnabled,
transientMessage,
bannerData,
bannerVisible,
@@ -2757,32 +2767,34 @@ Logging in with Google... Restarting Gemini CLI to continue.
return (
<UIStateContext.Provider value={uiState}>
<UIActionsContext.Provider value={uiActions}>
<ConfigContext.Provider value={config}>
<AppContext.Provider
value={{
version: props.version,
startupWarnings: props.startupWarnings || [],
}}
>
<ToolActionsProvider
config={config}
toolCalls={allToolCalls}
isExpanded={isExpanded}
toggleExpansion={toggleExpansion}
toggleAllExpansion={toggleAllExpansion}
<InputContext.Provider value={inputState}>
<UIActionsContext.Provider value={uiActions}>
<ConfigContext.Provider value={config}>
<AppContext.Provider
value={{
version: props.version,
startupWarnings: props.startupWarnings || [],
}}
>
<ShellFocusContext.Provider value={isFocused}>
<MouseProvider mouseEventsEnabled={mouseMode}>
<ScrollProvider>
<App key={`app-${forceRerenderKey}`} />
</ScrollProvider>
</MouseProvider>
</ShellFocusContext.Provider>
</ToolActionsProvider>
</AppContext.Provider>
</ConfigContext.Provider>
</UIActionsContext.Provider>
<ToolActionsProvider
config={config}
toolCalls={allToolCalls}
isExpanded={isExpanded}
toggleExpansion={toggleExpansion}
toggleAllExpansion={toggleAllExpansion}
>
<ShellFocusContext.Provider value={isFocused}>
<MouseProvider mouseEventsEnabled={mouseMode}>
<ScrollProvider>
<App key={`app-${forceRerenderKey}`} />
</ScrollProvider>
</MouseProvider>
</ShellFocusContext.Provider>
</ToolActionsProvider>
</AppContext.Provider>
</ConfigContext.Provider>
</UIActionsContext.Provider>
</InputContext.Provider>
</UIStateContext.Provider>
);
};
@@ -45,6 +45,7 @@ describe('clearCommand', () => {
fireSessionEndEvent: vi.fn().mockResolvedValue(undefined),
fireSessionStartEvent: vi.fn().mockResolvedValue(undefined),
}),
setActiveExtensionContext: vi.fn(),
injectionService: {
clear: mockHintClear,
},
+2 -1
View File
@@ -30,8 +30,9 @@ export const clearCommand: SlashCommand = {
await hookSystem.fireSessionEndEvent(SessionEndReason.Clear);
}
// Reset user steering hints
// Reset user steering hints and extension context
config?.injectionService.clear();
config?.setActiveExtensionContext(undefined);
// Start a new conversation recording with a new session ID
// We MUST do this before calling resetChat() so the new ChatRecordingService
@@ -56,8 +56,9 @@ describe('planCommand', () => {
config: {
isPlanEnabled: vi.fn(),
setApprovalMode: vi.fn(),
getApprovedPlanPath: vi.fn(),
getApprovalMode: vi.fn(),
getApprovedPlanPath: vi.fn(),
getPlansDir: vi.fn(),
getFileSystemService: vi.fn(),
storage: {
getPlansDir: vi.fn().mockReturnValue('/mock/plans/dir'),
+1 -1
View File
@@ -82,7 +82,7 @@ export const planCommand: SlashCommand = {
try {
const content = await processSingleFileContent(
approvedPlanPath,
config.storage.getPlansDir(),
config.getPlansDir(),
config.getFileSystemService(),
);
const fileName = path.basename(approvedPlanPath);
+9 -2
View File
@@ -59,13 +59,20 @@ const NARROW_TERMINAL_BREAKPOINT = 60;
export const AppHeader = ({ version, showDetails = true }: AppHeaderProps) => {
const settings = useSettings();
const config = useConfig();
const { terminalWidth, bannerData, bannerVisible, updateInfo } = useUIState();
const {
terminalWidth,
bannerData,
bannerVisible,
updateInfo,
isConfigInitialized,
isAuthenticating,
} = useUIState();
const { bannerText } = useBanner(bannerData);
const { showTips } = useTips();
const authType = config.getContentGeneratorConfig()?.authType;
const loggedOut = !authType;
const loggedOut = isConfigInitialized && !isAuthenticating && !authType;
const showHeader = !(
settings.merged.ui.hideBanner || config.getScreenReader()
@@ -245,20 +245,37 @@ const createMockConfig = (overrides = {}): Config =>
...overrides,
}) as unknown as Config;
import { InputContext, type InputState } from '../contexts/InputContext.js';
const renderComposer = async (
uiState: UIState,
settings = createMockSettings({ ui: {} }),
config = createMockConfig(),
uiActions = createMockUIActions(),
inputStateOverrides: Partial<InputState> = {},
) => {
const inputState = {
buffer: { text: '' } as unknown as TextBuffer,
userMessages: [],
shellModeActive: false,
showEscapePrompt: false,
copyModeEnabled: false,
inputWidth: 80,
suggestionsWidth: 40,
...(uiState as unknown as Partial<InputState>),
...inputStateOverrides,
};
const result = await render(
<ConfigContext.Provider value={config as unknown as Config}>
<SettingsContext.Provider value={settings as unknown as LoadedSettings}>
<UIStateContext.Provider value={uiState}>
<UIActionsContext.Provider value={uiActions}>
<Composer isFocused={true} />
</UIActionsContext.Provider>
</UIStateContext.Provider>
<InputContext.Provider value={inputState}>
<UIStateContext.Provider value={uiState}>
<UIActionsContext.Provider value={uiActions}>
<Composer isFocused={true} />
</UIActionsContext.Provider>
</UIStateContext.Provider>
</InputContext.Provider>
</SettingsContext.Provider>
</ConfigContext.Provider>,
);
@@ -541,7 +558,6 @@ describe('Composer', () => {
const uiState = createMockUIState({
ctrlCPressedOnce: false,
ctrlDPressedOnce: false,
showEscapePrompt: false,
});
const { lastFrame } = await renderComposer(uiState);
@@ -631,7 +647,6 @@ describe('Composer', () => {
async (mode) => {
const uiState = createMockUIState({
showApprovalModeIndicator: mode,
shellModeActive: false,
});
const { lastFrame } = await renderComposer(uiState);
@@ -641,11 +656,15 @@ describe('Composer', () => {
);
it('shows ShellModeIndicator when shell mode is active', async () => {
const uiState = createMockUIState({
shellModeActive: true,
});
const uiState = createMockUIState();
const { lastFrame } = await renderComposer(uiState);
const { lastFrame } = await renderComposer(
uiState,
undefined,
undefined,
undefined,
{ shellModeActive: true },
);
expect(lastFrame()).toMatch(/ShellModeIndic[\s\S]*tor/);
});
@@ -724,11 +743,16 @@ describe('Composer', () => {
it('shows Esc rewind prompt in minimal mode without showing full UI', async () => {
const uiState = createMockUIState({
cleanUiDetailsVisible: false,
showEscapePrompt: true,
history: [{ id: 1, type: 'user', text: 'msg' }],
});
const { lastFrame } = await renderComposer(uiState);
const { lastFrame } = await renderComposer(
uiState,
undefined,
undefined,
undefined,
{ showEscapePrompt: true },
);
const output = lastFrame();
expect(output).toContain('Press Esc again to rewind.');
expect(output).not.toContain('ContextSummaryDisplay');
@@ -828,11 +852,16 @@ describe('Composer', () => {
describe('Shortcuts Hint', () => {
it('restores shortcuts hint after 200ms debounce when buffer is empty', async () => {
const uiState = createMockUIState({
buffer: { text: '' } as unknown as TextBuffer,
cleanUiDetailsVisible: false,
});
const { lastFrame } = await renderComposer(uiState);
const { lastFrame } = await renderComposer(
uiState,
undefined,
undefined,
undefined,
{ buffer: { text: '' } as unknown as TextBuffer },
);
await act(async () => {
await vi.advanceTimersByTimeAsync(250);
@@ -845,11 +874,16 @@ describe('Composer', () => {
it('hides shortcuts hint when text is typed in buffer', async () => {
const uiState = createMockUIState({
buffer: { text: 'hello' } as unknown as TextBuffer,
cleanUiDetailsVisible: false,
});
const { lastFrame } = await renderComposer(uiState);
const { lastFrame } = await renderComposer(
uiState,
undefined,
undefined,
undefined,
{ buffer: { text: 'hello' } as unknown as TextBuffer },
);
expect(lastFrame()).not.toContain('press tab twice for more');
expect(lastFrame()).not.toContain('? for shortcuts');
+5 -9
View File
@@ -9,6 +9,7 @@ import { useState, useEffect } from 'react';
import { useConfig } from '../contexts/ConfigContext.js';
import { useSettings } from '../contexts/SettingsContext.js';
import { useUIState } from '../contexts/UIStateContext.js';
import { useInputState } from '../contexts/InputContext.js';
import { useUIActions } from '../contexts/UIActionsContext.js';
import { useVimMode } from '../contexts/VimModeContext.js';
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
@@ -30,6 +31,7 @@ import { appEvents, AppEvent } from '../../utils/events.js';
export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
const uiState = useUIState();
const inputState = useInputState();
const uiActions = useUIActions();
const settings = useSettings();
const config = useConfig();
@@ -81,12 +83,12 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
return null;
}
const hasToast = shouldShowToast(uiState);
const showToast = shouldShowToast(uiState, inputState);
const hideUiDetailsForSuggestions =
suggestionsVisible && suggestionsPosition === 'above';
// Mini Mode VIP Flags (Pure Content Triggers)
const showMinimalToast = hasToast;
const showMinimalToast = showToast;
return (
<Box
@@ -141,17 +143,12 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
{uiState.isInputActive && (
<InputPrompt
buffer={uiState.buffer}
inputWidth={uiState.inputWidth}
suggestionsWidth={uiState.suggestionsWidth}
onSubmit={uiActions.handleFinalSubmit}
userMessages={uiState.userMessages}
setBannerVisible={uiActions.setBannerVisible}
onClearScreen={uiActions.handleClearScreen}
config={config}
slashCommands={uiState.slashCommands || []}
commandContext={uiState.commandContext}
shellModeActive={uiState.shellModeActive}
setShellModeActive={uiActions.setShellModeActive}
approvalMode={uiState.showApprovalModeIndicator}
onEscapePromptChange={uiActions.onEscapePromptChange}
@@ -165,7 +162,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
? vimMode === 'INSERT'
? " Press 'Esc' for NORMAL mode."
: " Press 'i' for INSERT mode."
: uiState.shellModeActive
: inputState.shellModeActive
? ' Type your shell command'
: ' Type your message or @path/to/file'
}
@@ -173,7 +170,6 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
streamingState={uiState.streamingState}
suggestionsPosition={suggestionsPosition}
onSuggestionsVisibilityChange={setSuggestionsVisible}
copyModeEnabled={uiState.copyModeEnabled}
/>
)}
@@ -4,34 +4,36 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from '../../test-utils/render.js';
import { CopyModeWarning } from './CopyModeWarning.js';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { useUIState, type UIState } from '../contexts/UIStateContext.js';
import { renderWithProviders } from '../../test-utils/render.js';
import { useInputState } from '../contexts/InputContext.js';
vi.mock('../contexts/UIStateContext.js');
vi.mock('../contexts/InputContext.js');
describe('CopyModeWarning', () => {
const mockUseUIState = vi.mocked(useUIState);
beforeEach(() => {
vi.clearAllMocks();
});
it('renders nothing when copy mode is disabled', async () => {
mockUseUIState.mockReturnValue({
vi.mocked(useInputState).mockReturnValue({
copyModeEnabled: false,
} as unknown as UIState);
const { lastFrame, unmount } = await render(<CopyModeWarning />);
} as unknown as ReturnType<typeof useInputState>);
const { lastFrame, unmount } = await renderWithProviders(
<CopyModeWarning />,
);
expect(lastFrame({ allowEmpty: true })).toBe('');
unmount();
});
it('renders warning when copy mode is enabled', async () => {
mockUseUIState.mockReturnValue({
vi.mocked(useInputState).mockReturnValue({
copyModeEnabled: true,
} as unknown as UIState);
const { lastFrame, unmount } = await render(<CopyModeWarning />);
} as unknown as ReturnType<typeof useInputState>);
const { lastFrame, unmount } = await renderWithProviders(
<CopyModeWarning />,
);
expect(lastFrame()).toContain('In Copy Mode');
expect(lastFrame()).toContain('Use Page Up/Down to scroll');
expect(lastFrame()).toContain('Press Ctrl+S or any other key to exit');
@@ -6,11 +6,11 @@
import type React from 'react';
import { Box, Text } from 'ink';
import { useUIState } from '../contexts/UIStateContext.js';
import { useInputState } from '../contexts/InputContext.js';
import { theme } from '../semantic-colors.js';
export const CopyModeWarning: React.FC = () => {
const { copyModeEnabled } = useUIState();
const { copyModeEnabled } = useInputState();
return (
<Box height={1}>
@@ -169,6 +169,11 @@ Implement a comprehensive authentication system with multiple providers.
getUseTerminalBuffer: () => false,
} as unknown as import('@google/gemini-cli-core').Config,
settings: createMockSettings({ ui: { useAlternateBuffer } }),
inputState: {
buffer: { text: '' } as never,
showEscapePrompt: false,
shellModeActive: false,
},
},
);
};
@@ -472,6 +477,11 @@ Implement a comprehensive authentication system with multiple providers.
settings: createMockSettings({
ui: { useAlternateBuffer: useAlternateBuffer ?? true },
}),
inputState: {
buffer: { text: '' } as never,
showEscapePrompt: false,
shellModeActive: false,
},
},
),
);
+3 -4
View File
@@ -26,6 +26,7 @@ import { useUIState } from '../contexts/UIStateContext.js';
import { useConfig } from '../contexts/ConfigContext.js';
import { useSettings } from '../contexts/SettingsContext.js';
import { useVimMode } from '../contexts/VimModeContext.js';
import { useInputState } from '../contexts/InputContext.js';
import {
ALL_ITEMS,
type FooterItemId,
@@ -173,6 +174,7 @@ interface FooterColumn {
export const Footer: React.FC = () => {
const uiState = useUIState();
const { copyModeEnabled } = useInputState();
const config = useConfig();
const settings = useSettings();
const { vimEnabled, vimMode } = useVimMode();
@@ -365,10 +367,7 @@ export const Footer: React.FC = () => {
id,
header,
() => (
<MemoryUsageDisplay
color={itemColor}
isActive={!uiState.copyModeEnabled}
/>
<MemoryUsageDisplay color={itemColor} isActive={!copyModeEnabled} />
),
10,
);
File diff suppressed because it is too large Load Diff
+12 -13
View File
@@ -70,6 +70,7 @@ import { getSafeLowColorBackground } from '../themes/color-utils.js';
import { isLowColorDepth } from '../utils/terminalUtils.js';
import { useShellFocusState } from '../contexts/ShellFocusContext.js';
import { useUIState } from '../contexts/UIStateContext.js';
import { useInputState } from '../contexts/InputContext.js';
import {
appEvents,
AppEvent,
@@ -104,18 +105,13 @@ export type ScrollableItem =
| { type: 'ghostLine'; ghostLine: string; index: number };
export interface InputPromptProps {
buffer: TextBuffer;
onSubmit: (value: string) => void;
userMessages: readonly string[];
onClearScreen: () => void;
config: Config;
slashCommands: readonly SlashCommand[];
commandContext: CommandContext;
placeholder?: string;
focus?: boolean;
inputWidth: number;
suggestionsWidth: number;
shellModeActive: boolean;
setShellModeActive: (value: boolean) => void;
approvalMode: ApprovalMode;
onEscapePromptChange?: (showPrompt: boolean) => void;
@@ -128,7 +124,6 @@ export interface InputPromptProps {
onQueueMessage?: (message: string) => void;
suggestionsPosition?: 'above' | 'below';
setBannerVisible: (visible: boolean) => void;
copyModeEnabled?: boolean;
}
// The input content, input container, and input suggestions list may have different widths
@@ -199,18 +194,13 @@ export function tryTogglePasteExpansion(buffer: TextBuffer): boolean {
}
export const InputPrompt: React.FC<InputPromptProps> = ({
buffer,
onSubmit,
userMessages,
onClearScreen,
config,
slashCommands,
commandContext,
placeholder = ' Type your message or @path/to/file',
focus = true,
inputWidth,
suggestionsWidth,
shellModeActive,
setShellModeActive,
approvalMode,
onEscapePromptChange,
@@ -223,8 +213,16 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
onQueueMessage,
suggestionsPosition = 'below',
setBannerVisible,
copyModeEnabled = false,
}) => {
const inputState = useInputState();
const {
buffer,
userMessages,
shellModeActive,
copyModeEnabled,
inputWidth,
suggestionsWidth,
} = inputState;
const isHelpDismissKey = useIsHelpDismissKey();
const keyMatchers = useKeyMatchers();
const { stdout } = useStdout();
@@ -434,7 +432,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
slashCommands,
);
if (commandToExecute?.isSafeConcurrent) {
inputHistory.handleSubmit(trimmedMessage);
handleSubmitAndClear(trimmedMessage);
return;
}
}
@@ -452,6 +450,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
streamingState,
setQueueErrorMessage,
slashCommands,
handleSubmitAndClear,
],
);
@@ -6,7 +6,11 @@
import { renderWithProviders } from '../../test-utils/render.js';
import { createMockSettings } from '../../test-utils/settings.js';
import { makeFakeConfig, CoreToolCallStatus } from '@google/gemini-cli-core';
import {
makeFakeConfig,
CoreToolCallStatus,
UPDATE_TOPIC_TOOL_NAME,
} from '@google/gemini-cli-core';
import { waitFor } from '../../test-utils/async.js';
import { MainContent } from './MainContent.js';
import { getToolGroupBorderAppearance } from '../utils/borderStyles.js';
@@ -728,6 +732,158 @@ describe('MainContent', () => {
unmount();
});
describe('Narration Suppression', () => {
const settingsWithNarration = createMockSettings({
merged: {
ui: { inlineThinkingMode: 'expanded' },
experimental: { topicUpdateNarration: true },
},
});
it('suppresses thinking ALWAYS when narration is enabled', async () => {
mockUseSettings.mockReturnValue(settingsWithNarration);
const uiState = {
...defaultMockUiState,
history: [
{ id: 1, type: 'user' as const, text: 'Hello' },
{
id: 2,
type: 'thinking' as const,
thought: {
subject: 'Thinking...',
description: 'Thinking about hello',
},
},
{ id: 3, type: 'gemini' as const, text: 'I am helping.' },
],
};
const { lastFrame, unmount } = await renderWithProviders(
<MainContent />,
{
uiState: uiState as Partial<UIState>,
settings: settingsWithNarration,
},
);
const output = lastFrame();
expect(output).not.toContain('Thinking...');
expect(output).toContain('I am helping.');
unmount();
});
it('suppresses text in intermediate turns (contains non-topic tools)', async () => {
mockUseSettings.mockReturnValue(settingsWithNarration);
const uiState = {
...defaultMockUiState,
history: [
{ id: 100, type: 'user' as const, text: 'Search' },
{
id: 101,
type: 'gemini' as const,
text: 'I will now search the files.',
},
{
id: 102,
type: 'tool_group' as const,
tools: [
{
callId: '1',
name: 'ls',
args: { path: '.' },
status: CoreToolCallStatus.Success,
},
],
},
],
};
const { lastFrame, unmount } = await renderWithProviders(
<MainContent />,
{
uiState: uiState as Partial<UIState>,
settings: settingsWithNarration,
},
);
const output = lastFrame();
expect(output).not.toContain('I will now search the files.');
unmount();
});
it('suppresses text that precedes a topic tool in the same turn', async () => {
mockUseSettings.mockReturnValue(settingsWithNarration);
const uiState = {
...defaultMockUiState,
history: [
{ id: 200, type: 'user' as const, text: 'Hello' },
{ id: 201, type: 'gemini' as const, text: 'I will now help you.' },
{
id: 202,
type: 'tool_group' as const,
tools: [
{
callId: '1',
name: UPDATE_TOPIC_TOOL_NAME,
args: { title: 'Helping', summary: 'Helping the user' },
status: CoreToolCallStatus.Success,
},
],
},
],
};
const { lastFrame, unmount } = await renderWithProviders(
<MainContent />,
{
uiState: uiState as Partial<UIState>,
settings: settingsWithNarration,
},
);
const output = lastFrame();
expect(output).not.toContain('I will now help you.');
expect(output).toContain('Helping');
expect(output).toContain('Helping the user');
unmount();
});
it('shows text in the final turn if it comes AFTER the topic tool', async () => {
mockUseSettings.mockReturnValue(settingsWithNarration);
const uiState = {
...defaultMockUiState,
history: [
{ id: 300, type: 'user' as const, text: 'Hello' },
{
id: 301,
type: 'tool_group' as const,
tools: [
{
callId: '1',
name: UPDATE_TOPIC_TOOL_NAME,
args: { title: 'Final Answer', summary: 'I have finished' },
status: CoreToolCallStatus.Success,
},
],
},
{ id: 302, type: 'gemini' as const, text: 'Here is your answer.' },
],
};
const { lastFrame, unmount } = await renderWithProviders(
<MainContent />,
{
uiState: uiState as Partial<UIState>,
settings: settingsWithNarration,
},
);
const output = lastFrame();
expect(output).toContain('Here is your answer.');
unmount();
});
});
it('renders multiple thinking messages sequentially correctly', async () => {
mockUseSettings.mockReturnValue({
merged: {
+39 -8
View File
@@ -91,20 +91,47 @@ export const MainContent = () => {
const flags = new Array<boolean>(combinedHistory.length).fill(false);
if (topicUpdateNarrationEnabled) {
let toolGroupInTurn = false;
let turnIsIntermediate = false;
let hasTopicToolInTurn = false;
for (let i = combinedHistory.length - 1; i >= 0; i--) {
const item = combinedHistory[i];
if (item.type === 'user' || item.type === 'user_shell') {
toolGroupInTurn = false;
turnIsIntermediate = false;
hasTopicToolInTurn = false;
} else if (item.type === 'tool_group') {
toolGroupInTurn = item.tools.some((t) => isTopicTool(t.name));
const hasTopic = item.tools.some((t) => isTopicTool(t.name));
const hasNonTopic = item.tools.some((t) => !isTopicTool(t.name));
if (hasTopic) {
hasTopicToolInTurn = true;
}
if (hasNonTopic) {
turnIsIntermediate = true;
}
} else if (
(item.type === 'thinking' ||
item.type === 'gemini' ||
item.type === 'gemini_content') &&
toolGroupInTurn
item.type === 'thinking' ||
item.type === 'gemini' ||
item.type === 'gemini_content'
) {
flags[i] = true;
// Rule 1: Always suppress thinking when narration is enabled to avoid
// "flashing" as the model starts its response, and because the Topic
// UI provides the necessary high-level intent.
if (item.type === 'thinking') {
flags[i] = true;
continue;
}
// Rule 2: Suppress text in intermediate turns (turns containing non-topic
// tools) to hide mechanical narration.
if (turnIsIntermediate) {
flags[i] = true;
}
// Rule 3: Suppress text that precedes a topic tool in the same turn,
// as the topic tool "replaces" it.
if (hasTopicToolInTurn) {
flags[i] = true;
}
}
}
}
@@ -336,6 +363,10 @@ export const MainContent = () => {
isAlternateBuffer,
]);
if (!uiState.isConfigInitialized) {
return null;
}
if (isAlternateBufferOrTerminalBuffer) {
return scrollableList;
}
@@ -9,7 +9,7 @@ import { describe, it, expect, vi } from 'vitest';
import { StatsDisplay } from './StatsDisplay.js';
import * as SessionContext from '../contexts/SessionContext.js';
import { type SessionMetrics } from '../contexts/SessionContext.js';
import { ToolCallDecision } from '@google/gemini-cli-core';
import { ToolCallDecision, LlmRole } from '@google/gemini-cli-core';
// Mock the context to provide controlled data for testing
vi.mock('../contexts/SessionContext.js', async (importOriginal) => {
@@ -131,6 +131,66 @@ describe('<StatsDisplay />', () => {
expect(output).toMatchSnapshot();
});
it('renders role breakdown correctly under models', async () => {
const metrics = createTestMetrics({
models: {
'gemini-2.5-flash': {
api: { totalRequests: 10, totalErrors: 0, totalLatencyMs: 10000 },
tokens: {
input: 1000,
prompt: 1200,
candidates: 2000,
total: 3200,
cached: 200,
thoughts: 0,
tool: 0,
},
roles: {
[LlmRole.MAIN]: {
totalRequests: 7,
totalErrors: 0,
totalLatencyMs: 7000,
tokens: {
input: 800,
prompt: 900,
candidates: 1500,
total: 2400,
cached: 100,
thoughts: 0,
tool: 0,
},
},
[LlmRole.UTILITY_TOOL]: {
totalRequests: 3,
totalErrors: 0,
totalLatencyMs: 3000,
tokens: {
input: 200,
prompt: 300,
candidates: 500,
total: 800,
cached: 100,
thoughts: 0,
tool: 0,
},
},
},
},
},
});
const { lastFrame } = await renderWithMockedStats(metrics);
const output = lastFrame();
expect(output).toContain('gemini-2.5-flash');
expect(output).toContain('10'); // Total requests
expect(output).toContain('↳ main');
expect(output).toContain('7'); // main requests
expect(output).toContain('↳ utility_tool');
expect(output).toContain('3'); // tool requests
expect(output).toMatchSnapshot();
});
it('renders all sections when all data is present', async () => {
const metrics = createTestMetrics({
models: {
+75 -12
View File
@@ -12,6 +12,7 @@ import { formatDuration } from '../utils/formatters.js';
import {
useSessionStats,
type ModelMetrics,
type RoleMetrics,
} from '../contexts/SessionContext.js';
import {
getStatusColor,
@@ -23,6 +24,7 @@ import {
import { computeSessionStats } from '../utils/computeStats.js';
import { useSettings } from '../contexts/SettingsContext.js';
import type { QuotaStats } from '../types.js';
import { LlmRole } from '@google/gemini-cli-core';
// A more flexible and powerful StatRow component
interface StatRowProps {
@@ -77,6 +79,16 @@ interface ModelUsageTableProps {
models: Record<string, ModelMetrics>;
}
interface ModelRow {
name: string;
displayName: string;
requests: number | string;
cachedTokens: string;
inputTokens: string;
outputTokens: string;
isSubRow: boolean;
}
const ModelUsageTable: React.FC<ModelUsageTableProps> = ({ models }) => {
const nameWidth = 28;
const requestsWidth = 8;
@@ -84,6 +96,46 @@ const ModelUsageTable: React.FC<ModelUsageTableProps> = ({ models }) => {
const cacheReadsWidth = 14;
const outputTokensWidth = 14;
const rows: ModelRow[] = [];
Object.entries(models).forEach(([name, metrics]) => {
rows.push({
name,
displayName: name,
requests: metrics.api.totalRequests,
cachedTokens: metrics.tokens.cached.toLocaleString(),
inputTokens: metrics.tokens.prompt.toLocaleString(),
outputTokens: metrics.tokens.candidates.toLocaleString(),
isSubRow: false,
});
if (metrics.roles) {
const roleEntries = Object.entries(metrics.roles).filter(
(entry): entry is [string, RoleMetrics] =>
entry[1] !== undefined && entry[1].totalRequests > 0,
);
roleEntries.sort(([a], [b]) => {
if (a === b) return 0;
if (a === LlmRole.MAIN) return -1;
if (b === LlmRole.MAIN) return 1;
return a.localeCompare(b);
});
roleEntries.forEach(([role, roleMetrics]) => {
rows.push({
name: `${name}-${role}`,
displayName: `${role}`,
requests: roleMetrics.totalRequests,
cachedTokens: roleMetrics.tokens.cached.toLocaleString(),
inputTokens: roleMetrics.tokens.prompt.toLocaleString(),
outputTokens: roleMetrics.tokens.candidates.toLocaleString(),
isSubRow: true,
});
});
}
});
return (
<Box flexDirection="column" marginTop={1}>
<Text bold color={theme.text.primary}>
@@ -131,31 +183,42 @@ const ModelUsageTable: React.FC<ModelUsageTableProps> = ({ models }) => {
</Box>
{/* Rows */}
{Object.entries(models).map(([name, modelMetrics]) => (
<Box key={name}>
{rows.map((row) => (
<Box key={row.name}>
<Box width={nameWidth}>
<Text color={theme.text.primary} wrap="truncate-end">
{name}
<Text
color={row.isSubRow ? theme.text.secondary : theme.text.primary}
wrap="truncate-end"
>
{row.displayName}
</Text>
</Box>
<Box width={requestsWidth} justifyContent="flex-end">
<Text color={theme.text.primary}>
{modelMetrics.api.totalRequests}
<Text
color={row.isSubRow ? theme.text.secondary : theme.text.primary}
>
{row.requests}
</Text>
</Box>
<Box width={inputTokensWidth} justifyContent="flex-end">
<Text color={theme.text.primary}>
{modelMetrics.tokens.prompt.toLocaleString()}
<Text
color={row.isSubRow ? theme.text.secondary : theme.text.primary}
>
{row.inputTokens}
</Text>
</Box>
<Box width={cacheReadsWidth} justifyContent="flex-end">
<Text color={theme.text.primary}>
{modelMetrics.tokens.cached.toLocaleString()}
<Text
color={row.isSubRow ? theme.text.secondary : theme.text.primary}
>
{row.cachedTokens}
</Text>
</Box>
<Box width={outputTokensWidth} justifyContent="flex-end">
<Text color={theme.text.primary}>
{modelMetrics.tokens.candidates.toLocaleString()}
<Text
color={row.isSubRow ? theme.text.secondary : theme.text.primary}
>
{row.outputTokens}
</Text>
</Box>
</Box>
@@ -9,7 +9,7 @@ import { StatusRow } from './StatusRow.js';
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
import { useComposerStatus } from '../hooks/useComposerStatus.js';
import { type UIState } from '../contexts/UIStateContext.js';
import { type TextBuffer } from '../components/shared/text-buffer.js';
import { type SessionStatsState } from '../contexts/SessionContext.js';
import { type ThoughtSummary } from '../types.js';
import { ApprovalMode } from '@google/gemini-cli-core';
@@ -29,13 +29,11 @@ describe('<StatusRow />', () => {
elapsedTime: 0,
currentWittyPhrase: undefined,
activeHooks: [],
buffer: { text: '' } as unknown as TextBuffer,
sessionStats: { lastPromptTokenCount: 0 } as unknown as SessionStatsState,
shortcutsHelpVisible: false,
contextFileNames: [],
showApprovalModeIndicator: ApprovalMode.DEFAULT,
allowPlanMode: false,
shellModeActive: false,
renderMarkdown: true,
currentModel: 'gemini-3',
};
+12 -8
View File
@@ -153,6 +153,8 @@ export const StatusNode: React.FC<{
);
};
import { useInputState } from '../contexts/InputContext.js';
export const StatusRow: React.FC<StatusRowProps> = ({
showUiDetails,
isNarrow,
@@ -162,6 +164,7 @@ export const StatusRow: React.FC<StatusRowProps> = ({
hasPendingActionRequired,
}) => {
const uiState = useUIState();
const inputState = useInputState();
const settings = useSettings();
const {
isInteractiveShellWaiting,
@@ -225,7 +228,7 @@ export const StatusRow: React.FC<StatusRowProps> = ({
settings.merged.ui.showShortcutsHint &&
!hideUiDetailsForSuggestions &&
!hasPendingActionRequired &&
uiState.buffer.text.length === 0
inputState.buffer.text.length === 0
) {
return showUiDetails ? '? for shortcuts' : 'press tab twice for more';
}
@@ -391,13 +394,14 @@ export const StatusRow: React.FC<StatusRowProps> = ({
>
{showUiDetails ? (
<>
{!hideUiDetailsForSuggestions && !uiState.shellModeActive && (
<ApprovalModeIndicator
approvalMode={uiState.showApprovalModeIndicator}
allowPlanMode={uiState.allowPlanMode}
/>
)}
{uiState.shellModeActive && (
{!hideUiDetailsForSuggestions &&
!inputState.shellModeActive && (
<ApprovalModeIndicator
approvalMode={uiState.showApprovalModeIndicator}
allowPlanMode={uiState.allowPlanMode}
/>
)}
{inputState.shellModeActive && (
<Box marginLeft={LAYOUT.INDICATOR_LEFT_MARGIN}>
<ShellModeIndicator />
</Box>
@@ -9,16 +9,24 @@ import { renderWithProviders } from '../../test-utils/render.js';
import { ToastDisplay, shouldShowToast } from './ToastDisplay.js';
import { TransientMessageType } from '../../utils/events.js';
import { type UIState } from '../contexts/UIStateContext.js';
import { type InputState } from '../contexts/InputContext.js';
import { type TextBuffer } from './shared/text-buffer.js';
import { type HistoryItem } from '../types.js';
const renderToastDisplay = async (uiState: Partial<UIState> = {}) =>
const renderToastDisplay = async (
uiState: Partial<UIState> = {},
inputState: Partial<InputState> = {},
) =>
renderWithProviders(<ToastDisplay />, {
uiState: {
buffer: { text: '' } as TextBuffer,
history: [] as HistoryItem[],
...uiState,
},
inputState: {
buffer: { text: '' } as TextBuffer,
showEscapePrompt: false,
...inputState,
},
});
describe('ToastDisplay', () => {
@@ -27,86 +35,121 @@ describe('ToastDisplay', () => {
});
describe('shouldShowToast', () => {
const baseState: Partial<UIState> = {
const baseUIState: Partial<UIState> = {
ctrlCPressedOnce: false,
transientMessage: null,
ctrlDPressedOnce: false,
showEscapePrompt: false,
buffer: { text: '' } as TextBuffer,
history: [] as HistoryItem[],
queueErrorMessage: null,
showIsExpandableHint: false,
};
const baseInputState: Partial<InputState> = {
showEscapePrompt: false,
buffer: { text: '' } as TextBuffer,
};
it('returns false for default state', () => {
expect(shouldShowToast(baseState as UIState)).toBe(false);
expect(
shouldShowToast(baseUIState as UIState, baseInputState as InputState),
).toBe(false);
});
it('returns true when showIsExpandableHint is true', () => {
expect(
shouldShowToast({
...baseState,
showIsExpandableHint: true,
} as UIState),
shouldShowToast(
{
...baseUIState,
showIsExpandableHint: true,
} as UIState,
baseInputState as InputState,
),
).toBe(true);
});
it('returns true when ctrlCPressedOnce is true', () => {
expect(
shouldShowToast({ ...baseState, ctrlCPressedOnce: true } as UIState),
shouldShowToast(
{ ...baseUIState, ctrlCPressedOnce: true } as UIState,
baseInputState as InputState,
),
).toBe(true);
});
it('returns true when transientMessage is present', () => {
expect(
shouldShowToast({
...baseState,
transientMessage: { text: 'test', type: TransientMessageType.Hint },
} as UIState),
shouldShowToast(
{
...baseUIState,
transientMessage: { text: 'test', type: TransientMessageType.Hint },
} as UIState,
baseInputState as InputState,
),
).toBe(true);
});
it('returns true when ctrlDPressedOnce is true', () => {
expect(
shouldShowToast({ ...baseState, ctrlDPressedOnce: true } as UIState),
shouldShowToast(
{ ...baseUIState, ctrlDPressedOnce: true } as UIState,
baseInputState as InputState,
),
).toBe(true);
});
it('returns true when showEscapePrompt is true and buffer is NOT empty', () => {
expect(
shouldShowToast({
...baseState,
showEscapePrompt: true,
buffer: { text: 'some text' } as TextBuffer,
} as UIState),
shouldShowToast(
{
...baseUIState,
} as UIState,
{
...baseInputState,
showEscapePrompt: true,
buffer: { text: 'some text' } as TextBuffer,
} as InputState,
),
).toBe(true);
});
it('returns true when showEscapePrompt is true and history is NOT empty', () => {
expect(
shouldShowToast({
...baseState,
showEscapePrompt: true,
history: [{ id: '1' } as unknown as HistoryItem],
} as UIState),
shouldShowToast(
{
...baseUIState,
history: [{ id: '1' } as unknown as HistoryItem],
} as UIState,
{
...baseInputState,
showEscapePrompt: true,
} as InputState,
),
).toBe(true);
});
it('returns false when showEscapePrompt is true but buffer and history are empty', () => {
expect(
shouldShowToast({
...baseState,
showEscapePrompt: true,
} as UIState),
shouldShowToast(
{
...baseUIState,
} as UIState,
{
...baseInputState,
showEscapePrompt: true,
} as InputState,
),
).toBe(false);
});
it('returns true when queueErrorMessage is present', () => {
expect(
shouldShowToast({
...baseState,
queueErrorMessage: 'error',
} as UIState),
shouldShowToast(
{
...baseUIState,
queueErrorMessage: 'error',
} as UIState,
baseInputState as InputState,
),
).toBe(true);
});
});
@@ -151,18 +194,25 @@ describe('ToastDisplay', () => {
});
it('renders Escape prompt when buffer is empty', async () => {
const { lastFrame } = await renderToastDisplay({
showEscapePrompt: true,
history: [{ id: 1, type: 'user', text: 'test' }] as HistoryItem[],
});
const { lastFrame } = await renderToastDisplay(
{
history: [{ id: 1, type: 'user', text: 'test' }] as HistoryItem[],
},
{
showEscapePrompt: true,
},
);
expect(lastFrame()).toMatchSnapshot();
});
it('renders Escape prompt when buffer is NOT empty', async () => {
const { lastFrame } = await renderToastDisplay({
showEscapePrompt: true,
buffer: { text: 'some text' } as TextBuffer,
});
const { lastFrame } = await renderToastDisplay(
{},
{
showEscapePrompt: true,
buffer: { text: 'some text' } as TextBuffer,
},
);
expect(lastFrame()).toMatchSnapshot();
});
@@ -8,15 +8,19 @@ import type React from 'react';
import { Text } from 'ink';
import { theme } from '../semantic-colors.js';
import { useUIState, type UIState } from '../contexts/UIStateContext.js';
import { useInputState, type InputState } from '../contexts/InputContext.js';
import { TransientMessageType } from '../../utils/events.js';
export function shouldShowToast(uiState: UIState): boolean {
export function shouldShowToast(
uiState: UIState,
inputState: InputState,
): boolean {
return (
uiState.ctrlCPressedOnce ||
Boolean(uiState.transientMessage) ||
uiState.ctrlDPressedOnce ||
(uiState.showEscapePrompt &&
(uiState.buffer.text.length > 0 || uiState.history.length > 0)) ||
(inputState.showEscapePrompt &&
(inputState.buffer.text.length > 0 || uiState.history.length > 0)) ||
Boolean(uiState.queueErrorMessage) ||
uiState.showIsExpandableHint
);
@@ -24,6 +28,7 @@ export function shouldShowToast(uiState: UIState): boolean {
export const ToastDisplay: React.FC = () => {
const uiState = useUIState();
const inputState = useInputState();
if (uiState.ctrlCPressedOnce) {
return (
@@ -46,8 +51,8 @@ export const ToastDisplay: React.FC = () => {
);
}
if (uiState.showEscapePrompt) {
const isPromptEmpty = uiState.buffer.text.length === 0;
if (inputState.showEscapePrompt) {
const isPromptEmpty = inputState.buffer.text.length === 0;
const hasHistory = uiState.history.length > 0;
if (isPromptEmpty && !hasHistory) {
@@ -168,6 +168,13 @@ exports[`InputPrompt > mouse interaction > should toggle paste expansion on doub
"
`;
exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 4`] = `
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
> [Pasted Text: 10 lines]
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
"
`;
exports[`InputPrompt > multiline rendering > should correctly render multiline input including blank lines 1`] = `
"────────────────────────────────────────────────────────────────────────────────────────────────────
│ > hello │
@@ -263,3 +263,32 @@ exports[`<StatsDisplay /> > renders only the Performance section in its zero sta
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
exports[`<StatsDisplay /> > renders role breakdown correctly under models 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ │
│ Session Stats │
│ │
│ Interaction Summary │
│ Session ID: test-session-id │
│ Tool Calls: 0 ( ✓ 0 x 0 ) │
│ Success Rate: 0.0% │
│ │
│ Performance │
│ Wall Time: 1s │
│ Agent Active: 10.0s │
│ » API Time: 10.0s (100.0%) │
│ » Tool Time: 0s (0.0%) │
│ │
│ │
│ Model Usage │
│ Use /model to view model quota information │
│ │
│ Model Reqs Input Tokens Cache Reads Output Tokens │
│ ────────────────────────────────────────────────────────────────────────────────────────────── │
│ gemini-2.5-flash 10 1,200 200 2,000 │
│ ↳ main 7 900 100 1,500 │
│ ↳ utility_tool 3 300 100 500 │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
@@ -258,15 +258,42 @@ describe('<ToolGroupMessage />', () => {
unmount();
});
it('renders update_topic tool call prioritizing summary over strategic_intent', async () => {
it('renders update_topic tool call using TopicMessage', async () => {
const toolCalls = [
createToolCall({
callId: 'topic-tool-priority',
callId: 'topic-tool',
name: UPDATE_TOPIC_TOOL_NAME,
args: {
[TOPIC_PARAM_TITLE]: 'Testing Topic',
[TOPIC_PARAM_STRATEGIC_INTENT]: 'This is the description',
},
}),
];
const item = createItem(toolCalls);
const { lastFrame, unmount } = await renderWithProviders(
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
{
config: baseMockConfig,
settings: fullVerbositySettings,
},
);
const output = lastFrame();
expect(output).toContain('Testing Topic: ');
expect(output).toContain('This is the description');
expect(output).toMatchSnapshot('update_topic_tool');
unmount();
});
it('renders update_topic tool call with summary instead of strategic_intent', async () => {
const toolCalls = [
createToolCall({
callId: 'topic-tool-summary',
name: UPDATE_TOPIC_TOOL_NAME,
args: {
[TOPIC_PARAM_TITLE]: 'Testing Topic',
[TOPIC_PARAM_SUMMARY]: 'This is the summary',
[TOPIC_PARAM_STRATEGIC_INTENT]: 'This should be ignored',
},
}),
];
@@ -283,34 +310,6 @@ describe('<ToolGroupMessage />', () => {
const output = lastFrame();
expect(output).toContain('Testing Topic: ');
expect(output).toContain('This is the summary');
expect(output).not.toContain('This should be ignored');
unmount();
});
it('renders update_topic tool call falling back to strategic_intent', async () => {
const toolCalls = [
createToolCall({
callId: 'topic-tool-fallback',
name: UPDATE_TOPIC_TOOL_NAME,
args: {
[TOPIC_PARAM_TITLE]: 'Testing Topic',
[TOPIC_PARAM_STRATEGIC_INTENT]: 'Fallback intent',
},
}),
];
const item = createItem(toolCalls);
const { lastFrame, unmount } = await renderWithProviders(
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
{
config: baseMockConfig,
settings: fullVerbositySettings,
},
);
const output = lastFrame();
expect(output).toContain('Testing Topic: ');
expect(output).toContain('Fallback intent');
unmount();
});
@@ -321,7 +320,7 @@ describe('<ToolGroupMessage />', () => {
name: UPDATE_TOPIC_TOOL_NAME,
args: {
[TOPIC_PARAM_TITLE]: 'Testing Topic',
[TOPIC_PARAM_SUMMARY]: 'This is the summary',
[TOPIC_PARAM_STRATEGIC_INTENT]: 'This is the description',
},
}),
createToolCall({
@@ -0,0 +1,114 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi } from 'vitest';
import { TopicMessage } from './TopicMessage.js';
import { renderWithProviders } from '../../../test-utils/render.js';
import {
TOPIC_PARAM_TITLE,
TOPIC_PARAM_SUMMARY,
TOPIC_PARAM_STRATEGIC_INTENT,
CoreToolCallStatus,
UPDATE_TOPIC_TOOL_NAME,
} from '@google/gemini-cli-core';
describe('<TopicMessage />', () => {
const baseArgs = {
[TOPIC_PARAM_TITLE]: 'Test Topic',
[TOPIC_PARAM_STRATEGIC_INTENT]: 'This is the strategic intent.',
[TOPIC_PARAM_SUMMARY]:
'This is the detailed summary that should be expandable.',
};
const renderTopic = async (
args: Record<string, unknown>,
height?: number,
toolActions?: {
isExpanded?: (callId: string) => boolean;
toggleExpansion?: (callId: string) => void;
},
) =>
renderWithProviders(
<TopicMessage
args={args}
terminalWidth={80}
availableTerminalHeight={height}
callId="test-topic"
name={UPDATE_TOPIC_TOOL_NAME}
description="Updating topic"
status={CoreToolCallStatus.Success}
confirmationDetails={undefined}
resultDisplay={undefined}
/>,
{ toolActions, mouseEventsEnabled: true },
);
it('renders title and intent by default (collapsed)', async () => {
const { lastFrame } = await renderTopic(baseArgs, 40);
const frame = lastFrame();
expect(frame).toContain('Test Topic:');
expect(frame).toContain('This is the strategic intent.');
expect(frame).not.toContain('This is the detailed summary');
expect(frame).not.toContain('(ctrl+o to expand)');
});
it('renders summary when globally expanded (Ctrl+O)', async () => {
const { lastFrame } = await renderTopic(baseArgs, undefined);
const frame = lastFrame();
expect(frame).toContain('Test Topic:');
expect(frame).toContain('This is the strategic intent.');
expect(frame).toContain('This is the detailed summary');
expect(frame).not.toContain('(ctrl+o to collapse)');
});
it('renders summary when selectively expanded via context', async () => {
const isExpanded = vi.fn((id) => id === 'test-topic');
const { lastFrame } = await renderTopic(baseArgs, 40, { isExpanded });
const frame = lastFrame();
expect(frame).toContain('Test Topic:');
expect(frame).toContain('This is the detailed summary');
expect(frame).not.toContain('(ctrl+o to collapse)');
});
it('calls toggleExpansion when clicked', async () => {
const toggleExpansion = vi.fn();
const { simulateClick } = await renderTopic(baseArgs, 40, {
toggleExpansion,
});
// In renderWithProviders, the component is wrapped in a Box with terminalWidth.
// The TopicMessage has marginLeft={2}.
// So col 5 should definitely hit the text content.
// row 1 is the first line of the TopicMessage.
await simulateClick(5, 1);
expect(toggleExpansion).toHaveBeenCalledWith('test-topic');
});
it('falls back to summary if strategic_intent is missing', async () => {
const args = {
[TOPIC_PARAM_TITLE]: 'Test Topic',
[TOPIC_PARAM_SUMMARY]: 'Only summary is present.',
};
const { lastFrame } = await renderTopic(args, 40);
const frame = lastFrame();
expect(frame).toContain('Test Topic:');
expect(frame).toContain('Only summary is present.');
expect(frame).not.toContain('(ctrl+o to expand)');
});
it('renders only strategic_intent if summary is missing', async () => {
const args = {
[TOPIC_PARAM_TITLE]: 'Test Topic',
[TOPIC_PARAM_STRATEGIC_INTENT]: 'Only intent is present.',
};
const { lastFrame } = await renderTopic(args, 40);
const frame = lastFrame();
expect(frame).toContain('Test Topic:');
expect(frame).toContain('Only intent is present.');
expect(frame).not.toContain('(ctrl+o to expand)');
});
});
@@ -5,7 +5,8 @@
*/
import type React from 'react';
import { Box, Text } from 'ink';
import { useEffect, useId, useRef, useCallback } from 'react';
import { Box, Text, type DOMElement } from 'ink';
import {
UPDATE_TOPIC_TOOL_NAME,
UPDATE_TOPIC_DISPLAY_NAME,
@@ -15,32 +16,103 @@ import {
} from '@google/gemini-cli-core';
import type { IndividualToolCallDisplay } from '../../types.js';
import { theme } from '../../semantic-colors.js';
import { useOverflowActions } from '../../contexts/OverflowContext.js';
import { useToolActions } from '../../contexts/ToolActionsContext.js';
import { useMouseClick } from '../../hooks/useMouseClick.js';
interface TopicMessageProps extends IndividualToolCallDisplay {
terminalWidth: number;
availableTerminalHeight?: number;
isExpandable?: boolean;
}
export const isTopicTool = (name: string): boolean =>
name === UPDATE_TOPIC_TOOL_NAME || name === UPDATE_TOPIC_DISPLAY_NAME;
export const TopicMessage: React.FC<TopicMessageProps> = ({ args }) => {
export const TopicMessage: React.FC<TopicMessageProps> = ({
callId,
args,
availableTerminalHeight,
isExpandable = true,
}) => {
const { isExpanded: isExpandedInContext, toggleExpansion } = useToolActions();
// Expansion is active if either:
// 1. The individual callId is expanded in the ToolActionsContext
// 2. The entire turn is expanded (Ctrl+O) which sets availableTerminalHeight to undefined
const isExpanded =
(isExpandedInContext ? isExpandedInContext(callId) : false) ||
availableTerminalHeight === undefined;
const overflowActions = useOverflowActions();
const uniqueId = useId();
const overflowId = `topic-${uniqueId}`;
const containerRef = useRef<DOMElement>(null);
const rawTitle = args?.[TOPIC_PARAM_TITLE];
const title = typeof rawTitle === 'string' ? rawTitle : undefined;
const rawDescription =
args?.[TOPIC_PARAM_SUMMARY] || args?.[TOPIC_PARAM_STRATEGIC_INTENT];
const description =
typeof rawDescription === 'string' ? rawDescription : undefined;
const rawStrategicIntent = args?.[TOPIC_PARAM_STRATEGIC_INTENT];
const strategicIntent =
typeof rawStrategicIntent === 'string' ? rawStrategicIntent : undefined;
const rawSummary = args?.[TOPIC_PARAM_SUMMARY];
const summary = typeof rawSummary === 'string' ? rawSummary : undefined;
// Top line intent: prefer strategic_intent, fallback to summary
const intent = strategicIntent || summary;
// Extra summary: only if both exist and are different (or just summary if we want to show it below)
const hasExtraSummary = !!(
strategicIntent &&
summary &&
strategicIntent !== summary
);
const handleToggle = useCallback(() => {
if (toggleExpansion && hasExtraSummary) {
toggleExpansion(callId);
}
}, [toggleExpansion, hasExtraSummary, callId]);
useMouseClick(containerRef, handleToggle, {
isActive: isExpandable && hasExtraSummary,
});
useEffect(() => {
// Only register if there is more content (summary) and it's currently hidden
const hasHiddenContent = isExpandable && hasExtraSummary && !isExpanded;
if (hasHiddenContent && overflowActions) {
overflowActions.addOverflowingId(overflowId);
} else if (overflowActions) {
overflowActions.removeOverflowingId(overflowId);
}
return () => {
overflowActions?.removeOverflowingId(overflowId);
};
}, [isExpandable, hasExtraSummary, isExpanded, overflowActions, overflowId]);
return (
<Box flexDirection="row" marginLeft={2} flexWrap="wrap">
<Text color={theme.text.primary} bold wrap="truncate-end">
{title || 'Topic'}
{description && <Text>: </Text>}
</Text>
{description && (
<Text color={theme.text.secondary} wrap="wrap">
{description}
<Box ref={containerRef} flexDirection="column" marginLeft={2}>
<Box flexDirection="row" flexWrap="wrap">
<Text color={theme.text.primary} bold wrap="truncate-end">
{title || 'Topic'}
{intent && <Text>: </Text>}
</Text>
{intent && (
<Text color={theme.text.secondary} wrap="wrap">
{intent}
</Text>
)}
</Box>
{isExpanded && hasExtraSummary && summary && (
<Box marginTop={1} marginLeft={0}>
<Text color={theme.text.secondary} wrap="wrap">
{summary}
</Text>
</Box>
)}
</Box>
);
@@ -78,7 +78,7 @@ exports[`<ToolGroupMessage /> > Golden Snapshots > renders header when scrolled
exports[`<ToolGroupMessage /> > Golden Snapshots > renders mixed tool calls including update_topic 1`] = `
"
Testing Topic: This is the summary
Testing Topic: This is the description
╭──────────────────────────────────────────────────────────────────────────╮
│ ✓ read_file Read a file │
@@ -142,6 +142,12 @@ exports[`<ToolGroupMessage /> > Golden Snapshots > renders two tool groups where
"
`;
exports[`<ToolGroupMessage /> > Golden Snapshots > renders update_topic tool call using TopicMessage > update_topic_tool 1`] = `
"
Testing Topic: This is the description
"
`;
exports[`<ToolGroupMessage /> > Golden Snapshots > renders with limited terminal height 1`] = `
"╭──────────────────────────────────────────────────────────────────────────╮
│ ✓ tool-with-result Tool with output │
@@ -85,32 +85,43 @@ const VirtualizedListItem = memo(
width,
containerWidth,
itemKey,
itemRef,
index,
onSetRef,
}: {
content: React.ReactElement;
shouldBeStatic: boolean;
width: number | string | undefined;
containerWidth: number;
itemKey: string;
itemRef: (el: DOMElement | null) => void;
}) => (
<Box width="100%" flexDirection="column" flexShrink={0} ref={itemRef}>
{shouldBeStatic ? (
<StaticRender
width={typeof width === 'number' ? width : containerWidth}
key={
itemKey +
'-static-' +
(typeof width === 'number' ? width : containerWidth)
}
>
{content}
</StaticRender>
) : (
content
)}
</Box>
),
index: number;
onSetRef: (index: number, el: DOMElement | null) => void;
}) => {
const itemRef = useCallback(
(el: DOMElement | null) => {
onSetRef(index, el);
},
[index, onSetRef],
);
return (
<Box width="100%" flexDirection="column" flexShrink={0} ref={itemRef}>
{shouldBeStatic ? (
<StaticRender
width={typeof width === 'number' ? width : containerWidth}
key={
itemKey +
'-static-' +
(typeof width === 'number' ? width : containerWidth)
}
>
{content}
</StaticRender>
) : (
content
)}
</Box>
);
},
);
VirtualizedListItem.displayName = 'VirtualizedListItem';
@@ -195,6 +206,10 @@ function VirtualizedList<T>(
const containerObserverRef = useRef<ResizeObserver | null>(null);
const nodeToKeyRef = useRef(new WeakMap<DOMElement, string>());
const onSetRef = useCallback((index: number, el: DOMElement | null) => {
itemRefs.current[index] = el;
}, []);
const containerRefCallback = useCallback((node: DOMElement | null) => {
containerObserverRef.current?.disconnect();
containerRef.current = node;
@@ -517,7 +532,6 @@ function VirtualizedList<T>(
observedNodes.current = currentNodes;
});
const renderedItems = [];
const renderRangeStart =
renderStatic || overflowToBackbuffer ? 0 : startIndex;
const renderRangeEnd = renderStatic ? data.length - 1 : endIndex;
@@ -533,7 +547,12 @@ function VirtualizedList<T>(
process.env['NODE_ENV'] === 'test' ||
(width !== undefined && typeof width === 'number');
if (isReady) {
const renderedItems = useMemo(() => {
if (!isReady) {
return [];
}
const items = [];
for (let i = renderRangeStart; i <= renderRangeEnd; i++) {
const item = data[i];
if (item) {
@@ -545,7 +564,7 @@ function VirtualizedList<T>(
const content = renderItem({ item, index: i });
const key = keyExtractor(item, i);
renderedItems.push(
items.push(
<VirtualizedListItem
key={key}
itemKey={key}
@@ -553,16 +572,28 @@ function VirtualizedList<T>(
shouldBeStatic={shouldBeStatic}
width={width}
containerWidth={containerWidth}
itemRef={(el: DOMElement | null) => {
if (i >= renderRangeStart && i <= renderRangeEnd) {
itemRefs.current[i] = el;
}
}}
index={i}
onSetRef={onSetRef}
/>,
);
}
}
}
return items;
}, [
isReady,
renderRangeStart,
renderRangeEnd,
data,
startIndex,
endIndex,
renderStatic,
isStaticItem,
renderItem,
keyExtractor,
width,
containerWidth,
onSetRef,
]);
const { getScrollTop, setPendingScrollTop } = useBatchedScroll(scrollTop);
@@ -0,0 +1,28 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { createContext, useContext } from 'react';
import type { TextBuffer } from '../components/shared/text-buffer.js';
export interface InputState {
buffer: TextBuffer;
userMessages: string[];
shellModeActive: boolean;
showEscapePrompt: boolean;
copyModeEnabled: boolean | undefined;
inputWidth: number;
suggestionsWidth: number;
}
export const InputContext = createContext<InputState | null>(null);
export const useInputState = () => {
const context = useContext(InputContext);
if (!context) {
throw new Error('useInputState must be used within an InputProvider');
}
return context;
};
@@ -17,6 +17,7 @@ import {
import type {
SessionMetrics,
ModelMetrics,
RoleMetrics,
ToolCallStats,
} from '@google/gemini-cli-core';
import { uiTelemetryService, sessionId } from '@google/gemini-cli-core';
@@ -139,7 +140,7 @@ function areMetricsEqual(a: SessionMetrics, b: SessionMetrics): boolean {
return true;
}
export type { SessionMetrics, ModelMetrics };
export type { SessionMetrics, ModelMetrics, RoleMetrics };
export interface SessionStatsState {
sessionId: string;
@@ -17,7 +17,7 @@ import type {
PermissionConfirmationRequest,
} from '../types.js';
import type { CommandContext, SlashCommand } from '../commands/types.js';
import type { TextBuffer } from '../components/shared/text-buffer.js';
import type {
IdeContext,
ApprovalMode,
@@ -143,11 +143,6 @@ export interface UIState {
initError: string | null;
pendingGeminiHistoryItems: HistoryItemWithoutId[];
thought: ThoughtSummary | null;
shellModeActive: boolean;
userMessages: string[];
buffer: TextBuffer;
inputWidth: number;
suggestionsWidth: number;
isInputActive: boolean;
isResuming: boolean;
shouldShowIdePrompt: boolean;
@@ -162,7 +157,6 @@ export interface UIState {
renderMarkdown: boolean;
ctrlCPressedOnce: boolean;
ctrlDPressedOnce: boolean;
showEscapePrompt: boolean;
shortcutsHelpVisible: boolean;
cleanUiDetailsVisible: boolean;
elapsedTime: number;
@@ -207,7 +201,6 @@ export interface UIState {
embeddedShellFocused: boolean;
showDebugProfiler: boolean;
showFullTodos: boolean;
copyModeEnabled: boolean;
bannerData: {
defaultText: string;
warningText: string;
@@ -993,4 +993,103 @@ describe('useSlashCommandProcessor', () => {
expect(result.current.slashCommands).toEqual([newCommand]),
);
});
describe('Active Extension Context Switching', () => {
it('sets active extension context when a command with a plan dir is executed', async () => {
const extensionCommand = createTestCommand({
name: 'conductor:setup',
extensionName: 'conductor',
action: vi.fn(),
});
const spyHasPlanDir = vi
.spyOn(mockConfig, 'hasExtensionPlanDir')
.mockReturnValue(true);
const spySetContext = vi.spyOn(mockConfig, 'setActiveExtensionContext');
const hook = await setupProcessorHook({
builtinCommands: [extensionCommand],
});
await waitFor(() => expect(hook.current.slashCommands!.length).toBe(1));
await act(async () => {
await hook.current.handleSlashCommand('/conductor:setup');
});
expect(spyHasPlanDir).toHaveBeenCalledWith('conductor');
expect(spySetContext).toHaveBeenCalledWith('conductor');
});
it('clears active extension context when a command WITHOUT a plan dir is executed', async () => {
const extensionCommand = createTestCommand({
name: 'other:cmd',
extensionName: 'other',
action: vi.fn(),
});
const spyHasPlanDir = vi
.spyOn(mockConfig, 'hasExtensionPlanDir')
.mockReturnValue(false);
const spySetContext = vi.spyOn(mockConfig, 'setActiveExtensionContext');
const hook = await setupProcessorHook({
builtinCommands: [extensionCommand],
});
await waitFor(() => expect(hook.current.slashCommands!.length).toBe(1));
await act(async () => {
await hook.current.handleSlashCommand('/other:cmd');
});
expect(spyHasPlanDir).toHaveBeenCalledWith('other');
expect(spySetContext).toHaveBeenCalledWith(undefined);
});
it('handles a sequence of context switches between extensions and default plan mode', async () => {
const extA = createTestCommand({
name: 'extA',
extensionName: 'extA',
action: vi.fn(),
});
const extB = createTestCommand({
name: 'extB',
extensionName: 'extB',
action: vi.fn(),
});
const planCmd = createTestCommand({
name: 'plan',
action: vi.fn(),
});
const spySetContext = vi.spyOn(mockConfig, 'setActiveExtensionContext');
vi.spyOn(mockConfig, 'hasExtensionPlanDir').mockReturnValue(true);
const hook = await setupProcessorHook({
builtinCommands: [extA, extB, planCmd],
});
await waitFor(() => expect(hook.current.slashCommands!.length).toBe(3));
// 1. Run Ext A
await act(async () => {
await hook.current.handleSlashCommand('/extA');
});
expect(spySetContext).toHaveBeenLastCalledWith('extA');
// 2. Run Ext B
await act(async () => {
await hook.current.handleSlashCommand('/extB');
});
expect(spySetContext).toHaveBeenLastCalledWith('extB');
// 3. Run /help (Concurrent global command)
spySetContext.mockClear();
await act(async () => {
await hook.current.handleSlashCommand('/help');
});
// A concurrent command should NOT modify the active extension context
expect(spySetContext).not.toHaveBeenCalled();
});
});
});
@@ -368,8 +368,17 @@ export const useSlashCommandProcessor = (
commandToExecute,
args,
canonicalPath: resolvedCommandPath,
extensionContext,
} = parseSlashCommand(trimmed, commands);
if (config && commandToExecute && !commandToExecute.isSafeConcurrent) {
config.setActiveExtensionContext(
extensionContext && config.hasExtensionPlanDir(extensionContext)
? extensionContext
: undefined,
);
}
// If the input doesn't match any known command, check if MCP servers
// are still loading (the command might come from an MCP server).
// Otherwise, treat it as regular text input (e.g. file paths like
@@ -5,8 +5,11 @@
*/
import { render } from '../../test-utils/render.js';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { DefaultAppLayout } from './DefaultAppLayout.js';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { useInputState } from '../contexts/InputContext.js';
vi.mock('../contexts/InputContext.js');
import { StreamingState } from '../types.js';
import { Text } from 'ink';
import type { UIState } from '../contexts/UIStateContext.js';
@@ -95,6 +98,9 @@ const createMockShell = (pid: number): BackgroundTask => ({
describe('<DefaultAppLayout />', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(useInputState).mockReturnValue({
copyModeEnabled: false,
} as unknown as ReturnType<typeof useInputState>);
// Reset mock state defaults
mockUIState.backgroundTasks = new Map();
mockUIState.activeBackgroundTaskPid = null;
@@ -17,9 +17,11 @@ import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
import { CopyModeWarning } from '../components/CopyModeWarning.js';
import { BackgroundTaskDisplay } from '../components/BackgroundTaskDisplay.js';
import { StreamingState } from '../types.js';
import { useInputState } from '../contexts/InputContext.js';
export const DefaultAppLayout: React.FC = () => {
const uiState = useUIState();
const { copyModeEnabled } = useInputState();
const isAlternateBuffer = useAlternateBuffer();
const { rootUiRef, terminalHeight } = uiState;
@@ -62,9 +64,7 @@ export const DefaultAppLayout: React.FC = () => {
flexShrink={0}
flexGrow={0}
width={uiState.terminalWidth}
height={
uiState.copyModeEnabled ? uiState.stableControlsHeight : undefined
}
height={copyModeEnabled ? uiState.stableControlsHeight : undefined}
>
<Notifications />
<CopyModeWarning />
+5 -1
View File
@@ -10,6 +10,7 @@ export type ParsedSlashCommand = {
commandToExecute: SlashCommand | undefined;
args: string;
canonicalPath: string[];
extensionContext?: string;
};
/**
@@ -69,6 +70,8 @@ export const parseSlashCommand = (
const args = parts.slice(pathIndex).join(' ');
const extensionContext = commandToExecute?.extensionName;
// Backtrack if the matched (sub)command doesn't take arguments but some were provided,
// AND the parent command is capable of handling them.
if (
@@ -82,8 +85,9 @@ export const parseSlashCommand = (
commandToExecute: parentCommand,
args: parts.slice(pathIndex - 1).join(' '),
canonicalPath: canonicalPath.slice(0, -1),
extensionContext: parentCommand.extensionName,
};
}
return { commandToExecute, args, canonicalPath };
return { commandToExecute, args, canonicalPath, extensionContext };
};
View File
View File
@@ -32,11 +32,11 @@ import { createAnalyzeScreenshotTool } from './analyzeScreenshot.js';
import { injectAutomationOverlay } from './automationOverlay.js';
import { injectInputBlocker } from './inputBlocker.js';
import { debugLogger } from '../../utils/debugLogger.js';
import { recordBrowserAgentToolDiscovery } from '../../telemetry/metrics.js';
import {
recordBrowserAgentToolDiscovery,
recordBrowserAgentVisionStatus,
recordBrowserAgentCleanup,
} from '../../telemetry/metrics.js';
logBrowserAgentVisionStatus,
logBrowserAgentCleanup,
} from '../../telemetry/loggers.js';
import {
PolicyDecision,
PRIORITY_SUBAGENT_TOOL,
@@ -248,7 +248,7 @@ export async function createBrowserAgentDefinition(
const allTools: AnyDeclarativeTool[] = [...mcpTools];
const visionDisabledReason = getVisionDisabledReason();
recordBrowserAgentVisionStatus(config, {
logBrowserAgentVisionStatus(config, {
enabled: !visionDisabledReason,
disabled_reason: visionDisabledReason?.code,
});
@@ -299,13 +299,13 @@ export async function cleanupBrowserAgent(
const startMs = Date.now();
try {
await browserManager.close();
recordBrowserAgentCleanup(config, Date.now() - startMs, {
logBrowserAgentCleanup(config, Date.now() - startMs, {
session_mode: sessionMode,
success: true,
});
debugLogger.log('Browser agent cleanup complete');
} catch (error) {
recordBrowserAgentCleanup(config, Date.now() - startMs, {
logBrowserAgentCleanup(config, Date.now() - startMs, {
session_mode: sessionMode,
success: false,
});
@@ -742,7 +742,7 @@ describe('BrowserAgentInvocation', () => {
);
});
it('should call cleanupBrowserAgent with correct params', async () => {
it('should not call cleanupBrowserAgent (cleanup is handled by BrowserManager.resetAll)', async () => {
const invocation = new BrowserAgentInvocation(
mockConfig,
mockParams,
@@ -750,11 +750,7 @@ describe('BrowserAgentInvocation', () => {
);
await invocation.execute(new AbortController().signal, vi.fn());
expect(cleanupBrowserAgent).toHaveBeenCalledWith(
expect.anything(),
mockConfig,
'persistent',
);
expect(cleanupBrowserAgent).not.toHaveBeenCalled();
});
});
@@ -34,12 +34,9 @@ import {
isToolActivityError,
} from '../types.js';
import type { MessageBus } from '../../confirmation-bus/message-bus.js';
import {
createBrowserAgentDefinition,
cleanupBrowserAgent,
} from './browserAgentFactory.js';
import { createBrowserAgentDefinition } from './browserAgentFactory.js';
import { removeInputBlocker } from './inputBlocker.js';
import { recordBrowserAgentTaskOutcome } from '../../telemetry/metrics.js';
import { logBrowserAgentTaskOutcome } from '../../telemetry/loggers.js';
import {
sanitizeThoughtContent,
sanitizeToolArgs,
@@ -400,7 +397,7 @@ ${output.result}`;
},
};
} finally {
recordBrowserAgentTaskOutcome(this.config, {
logBrowserAgentTaskOutcome(this.config, {
success: taskSuccess,
session_mode: sessionMode,
vision_enabled: visionEnabled,
@@ -444,7 +441,6 @@ ${output.result}`;
} catch {
// Ignore errors for removing the overlays.
}
await cleanupBrowserAgent(browserManager, this.config, sessionMode);
}
}
}
@@ -373,6 +373,7 @@ describe('BrowserManager', () => {
session_mode: 'persistent',
headless: false,
success: true,
tool_count: 4,
},
);
});
@@ -30,7 +30,7 @@ import * as path from 'node:path';
import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { injectAutomationOverlay } from './automationOverlay.js';
import { recordBrowserAgentConnection } from '../../telemetry/metrics.js';
import { logBrowserAgentConnection } from '../../telemetry/loggers.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -563,7 +563,9 @@ export class BrowserManager {
// Add optional settings from config.
// Force headless in seatbelt sandbox since Chrome profile/display access
// may be restricted, and the user is running in a sandboxed environment.
if (browserConfig.customConfig.headless || isSeatbeltSandbox) {
const effectiveHeadless =
!!browserConfig.customConfig.headless || isSeatbeltSandbox;
if (effectiveHeadless) {
mcpArgs.push('--headless');
}
if (browserConfig.customConfig.profilePath) {
@@ -667,15 +669,12 @@ export class BrowserManager {
// clear the action counter for each connection
this.actionCounter = 0;
recordBrowserAgentConnection(
this.config,
Date.now() - connectStartMs,
{
session_mode: sessionMode,
headless: !!browserConfig.customConfig.headless,
success: true,
},
);
logBrowserAgentConnection(this.config, Date.now() - connectStartMs, {
session_mode: sessionMode,
headless: effectiveHeadless,
success: true,
tool_count: this.discoveredTools.length,
});
})(),
new Promise<never>((_, reject) => {
timeoutId = setTimeout(
@@ -696,9 +695,9 @@ export class BrowserManager {
error instanceof Error ? error.message : String(error);
const errorType = BrowserManager.classifyConnectionError(rawErrorMessage);
recordBrowserAgentConnection(this.config, Date.now() - connectStartMs, {
logBrowserAgentConnection(this.config, Date.now() - connectStartMs, {
session_mode: sessionMode,
headless: !!browserConfig.customConfig.headless,
headless: effectiveHeadless,
success: false,
error_type: errorType,
});
+165 -18
View File
@@ -3275,6 +3275,9 @@ describe('Plans Directory Initialization', () => {
debugMode: false,
model: 'test-model',
cwd: '/tmp/test',
planSettings: {
directory: 'plans',
},
};
beforeEach(() => {
@@ -3283,11 +3286,12 @@ describe('Plans Directory Initialization', () => {
afterEach(() => {
vi.mocked(fs.promises.mkdir).mockRestore();
vi.mocked(fs.promises.access).mockRestore?.();
vi.mocked(fs.mkdirSync).mockRestore?.();
vi.mocked(fs.existsSync).mockReturnValue(true); // Reset to default mock behavior
});
it('should add plans directory to workspace context if it exists', async () => {
vi.spyOn(fs.promises, 'access').mockResolvedValue(undefined);
it('should not eagerly create plans directory during initialization', async () => {
vi.spyOn(fs, 'mkdirSync').mockReturnValue(undefined);
const config = new Config({
...baseParams,
plan: true,
@@ -3295,18 +3299,84 @@ describe('Plans Directory Initialization', () => {
await config.initialize();
const plansDir = config.storage.getPlansDir();
// Should NOT create the directory eagerly
expect(fs.promises.mkdir).not.toHaveBeenCalled();
// Should check if it exists
expect(fs.promises.access).toHaveBeenCalledWith(plansDir);
expect(fs.mkdirSync).not.toHaveBeenCalled();
// Using storage directly to avoid triggering creation
const plansDir = config.storage.getPlansDir();
const context = config.getWorkspaceContext();
expect(context.getDirectories()).not.toContain(plansDir);
});
it('should create plans directory and add it to workspace context when getPlansDir is called', async () => {
vi.spyOn(fs, 'mkdirSync').mockReturnValue(undefined);
const config = new Config({
...baseParams,
plan: true,
});
await config.initialize();
const plansDir = config.getPlansDir();
expect(fs.mkdirSync).toHaveBeenCalledWith(plansDir, {
recursive: true,
});
const context = config.getWorkspaceContext();
expect(context.getDirectories()).toContain(plansDir);
});
it('should NOT add plans directory to workspace context if it does not exist', async () => {
vi.spyOn(fs.promises, 'access').mockRejectedValue({ code: 'ENOENT' });
it('should gracefully handle existing directories by relying on mkdirSync recursive: true', async () => {
vi.spyOn(fs, 'mkdirSync').mockReturnValue(undefined);
const config = new Config({
...baseParams,
plan: true,
});
await config.initialize();
const plansDir = config.getPlansDir();
// mkdirSync should be called unconditionally
expect(fs.mkdirSync).toHaveBeenCalledWith(plansDir, { recursive: true });
// It MUST still register the directory
const context = config.getWorkspaceContext();
expect(context.getDirectories()).toContain(plansDir);
});
it('should log a warning if the plan directory path is blocked by an existing file (EEXIST)', async () => {
const writeSpy = vi
.spyOn(process.stderr, 'write')
.mockImplementation(() => true);
vi.spyOn(fs, 'mkdirSync').mockImplementation(() => {
const err = new Error('File exists') as NodeJS.ErrnoException;
err.code = 'EEXIST';
throw err;
});
const config = new Config({
...baseParams,
plan: true,
});
await config.initialize();
config.getPlansDir();
expect(writeSpy).toHaveBeenCalledWith(
expect.stringMatching(
/Failed to initialize active plan directory.*File exists/,
),
);
writeSpy.mockRestore();
});
it('should throw a security violation and verify mkdirSync runs before realpathSync (TOCTOU mitigation)', async () => {
const callOrder: string[] = [];
vi.spyOn(fs, 'mkdirSync').mockImplementation(() => {
callOrder.push('mkdirSync');
return undefined;
});
const config = new Config({
...baseParams,
plan: true,
@@ -3314,15 +3384,90 @@ describe('Plans Directory Initialization', () => {
await config.initialize();
const plansDir = config.storage.getPlansDir();
expect(fs.promises.mkdir).not.toHaveBeenCalled();
expect(fs.promises.access).toHaveBeenCalledWith(plansDir);
// Bypass Storage check so we can specifically test Config's check
vi.spyOn(config.storage, 'getPlansDir').mockReturnValue('/tmp/test/plans');
const context = config.getWorkspaceContext();
expect(context.getDirectories()).not.toContain(plansDir);
const realpathSpy = vi
.spyOn(fs, 'realpathSync')
.mockImplementation((p: fs.PathLike) => {
const pStr = p.toString();
// Ignore the calls from storage/initialization
if (pStr.includes('plans')) {
callOrder.push('realpathSync');
return '/outside/the/project/root/plans';
}
return pStr;
});
try {
expect(() => config.getPlansDir()).toThrow(
/Security violation: Resolved plan directory.*is outside both the project root.*and the global configuration directory/,
);
expect(callOrder).toEqual(['mkdirSync', 'realpathSync']);
} finally {
realpathSpy.mockRestore();
}
});
it('should log a warning if mkdirSync fails during getPlansDir (e.g. EACCES)', async () => {
const writeSpy = vi
.spyOn(process.stderr, 'write')
.mockImplementation(() => true);
vi.spyOn(fs, 'mkdirSync').mockImplementation(() => {
const err = new Error('Permission denied') as NodeJS.ErrnoException;
err.code = 'EACCES';
throw err;
});
const config = new Config({
...baseParams,
plan: true,
});
await config.initialize();
config.getPlansDir();
expect(writeSpy).toHaveBeenCalledWith(
expect.stringMatching(
/Failed to initialize active plan directory.*Permission denied/,
),
);
writeSpy.mockRestore();
});
it('should deduplicate and cache when multiple extensions (or default) use the same directory', async () => {
vi.spyOn(fs, 'mkdirSync').mockReturnValue(undefined);
const config = new Config({
...baseParams,
plan: true,
});
await config.initialize();
// 1. Call for Default Plan Dir
const defaultDir = config.getPlansDir();
expect(fs.mkdirSync).toHaveBeenCalledTimes(1);
// 2. Mock an extension that happens to use the SAME directory string
vi.spyOn(
config as unknown as {
getActiveExtensionPlanDir: () => string | undefined;
},
'getActiveExtensionPlanDir',
).mockReturnValue(
'plans', // This will resolve to the same path as the default in our mock setup
);
const extDir = config.getPlansDir();
// It should be the same path
expect(extDir).toBe(defaultDir);
// It should NOT have called mkdirSync a second time
expect(fs.mkdirSync).toHaveBeenCalledTimes(1);
});
it('should NOT create plans directory or add it to workspace context when plan is disabled', async () => {
vi.spyOn(fs, 'mkdirSync').mockReturnValue(undefined);
const config = new Config({
...baseParams,
plan: false,
@@ -3330,10 +3475,12 @@ describe('Plans Directory Initialization', () => {
await config.initialize();
const plansDir = config.storage.getPlansDir();
expect(fs.promises.mkdir).not.toHaveBeenCalledWith(plansDir, {
recursive: true,
});
// Even if getPlansDir is called manually, it should NOT create the directory
const plansDir = config.getPlansDir();
expect(fs.mkdirSync).not.toHaveBeenCalled();
expect(config.getWorkspaceContext().getDirectories()).not.toContain(
plansDir,
);
});
});
+130 -15
View File
@@ -6,6 +6,9 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { LRUCache } from 'mnemonist';
import { SecurityError } from '../core/errors.js';
import { isSubpath, resolveToRealPath } from '../utils/paths.js';
import { SandboxPolicyManager } from '../policy/sandboxPolicyManager.js';
import { inspect } from 'node:util';
import process from 'node:process';
@@ -168,7 +171,6 @@ import { SkillManager, type SkillDefinition } from '../skills/skillManager.js';
import { startupProfiler } from '../telemetry/startupProfiler.js';
import type { AgentDefinition } from '../agents/types.js';
import { fetchAdminControls } from '../code_assist/admin/admin_controls.js';
import { isSubpath, resolveToRealPath } from '../utils/paths.js';
import { InjectionService } from './injectionService.js';
import { ExecutionLifecycleService } from '../services/executionLifecycleService.js';
import { WORKSPACE_POLICY_TIER } from '../policy/config.js';
@@ -710,6 +712,7 @@ export interface ConfigParameters {
plan?: boolean;
tracker?: boolean;
planSettings?: PlanSettings;
extensionPlanDirs?: Record<string, string>;
worktreeSettings?: WorktreeSettings;
modelSteering?: boolean;
onModelChange?: (model: string) => void;
@@ -773,6 +776,16 @@ export class Config implements McpContext, AgentLoopContext {
private readonly extensionsEnabled: boolean;
private mcpServers: Record<string, MCPServerConfig> | undefined;
private readonly mcpEnablementCallbacks?: McpEnablementCallbacks;
/**
* The persistent context used to route tools (e.g. plans, tasks) to the correct
* extension directory.
* This is a persistent session state (similar to `approvalMode`). It remains active
* across multiple LLM turns until explicitly changed by another command.
*/
private activeExtensionContext?: string;
private initializedPlanDirs = new Set<string>();
private globalGeminiDirCache = new LRUCache<string, string | undefined>(1);
private readonly extensionPlanDirs: Record<string, string>;
private userMemory: string | HierarchicalMemory;
private geminiMdFileCount: number;
private geminiMdFilePaths: string[];
@@ -1036,6 +1049,7 @@ export class Config implements McpContext, AgentLoopContext {
this.mcpServerCommand = params.mcpServerCommand;
this.mcpServers = params.mcpServers;
this.mcpEnablementCallbacks = params.mcpEnablementCallbacks;
this.extensionPlanDirs = params.extensionPlanDirs ?? {};
this.mcpEnabled = params.mcpEnabled ?? true;
this.extensionsEnabled = params.extensionsEnabled ?? true;
this.allowedMcpServers = params.allowedMcpServers ?? [];
@@ -1405,20 +1419,6 @@ export class Config implements McpContext, AgentLoopContext {
this.workspaceContext.addDirectory(dir);
}
// Add plans directory to workspace context for plan file storage
if (this.planEnabled) {
const plansDir = this.storage.getPlansDir();
try {
await fs.promises.access(plansDir);
this.workspaceContext.addDirectory(plansDir);
} catch {
// Directory does not exist yet, so we don't add it to the workspace context.
// It will be created when the first plan is written. Since custom plan
// directories must be within the project root, they are automatically
// covered by the project-wide file discovery once created.
}
}
// Initialize centralized FileDiscoveryService
const discoverToolsHandle = startupProfiler.start('discover_tools');
this.getFileService();
@@ -2238,6 +2238,121 @@ export class Config implements McpContext, AgentLoopContext {
return this.mcpEnabled;
}
getActiveExtensionContext(): string | undefined {
return this.activeExtensionContext;
}
setActiveExtensionContext(context: string | undefined): void {
this.activeExtensionContext = context;
}
hasExtensionPlanDir(name: string): boolean {
return !!this.extensionPlanDirs[name];
}
getActiveExtensionPlanDir(): string | undefined {
const context = this.getActiveExtensionContext();
if (context) {
return this.extensionPlanDirs[context];
}
return undefined;
}
getPlansDir(): string {
const plansDir = this.storage.getPlansDir(this.getActiveExtensionPlanDir());
if (this.initializedPlanDirs.has(plansDir)) {
return plansDir;
}
const realProjectRoot = this.storage.getRealProjectRoot();
let realGlobalGeminiDir = this.globalGeminiDirCache.get('default');
if (!this.globalGeminiDirCache.has('default')) {
try {
realGlobalGeminiDir = resolveToRealPath(Storage.getGlobalGeminiDir());
} catch {
// If we can't securely resolve the global config dir (e.g. strict EACCES permissions on ~/),
// we gracefully degrade by not allowing it as a valid security boundary for plans.
realGlobalGeminiDir = undefined;
}
this.globalGeminiDirCache.set('default', realGlobalGeminiDir);
}
// 1. Lexical security check (before any filesystem mutation or return)
// We compare purely unresolved paths here to avoid static analyzer warnings about mixing resolved and unresolved paths.
// The physical security check happens AFTER mkdirSync.
const unresolvedProjectRoot = path.resolve(this.storage.getProjectRoot());
const unresolvedGlobalDir = path.resolve(Storage.getGlobalGeminiDir());
const unresolvedPlansDir = path.resolve(plansDir);
if (
!isSubpath(unresolvedProjectRoot, unresolvedPlansDir) &&
!isSubpath(unresolvedGlobalDir, unresolvedPlansDir)
) {
throw new SecurityError(
`Security violation: Plan directory '${unresolvedPlansDir}' is outside both the project root '${unresolvedProjectRoot}' and the global configuration directory.`,
);
}
// 2. We only attempt to physically create the directory if plan mode is enabled
if (this.planEnabled) {
let mkdirError: unknown;
try {
fs.mkdirSync(plansDir, { recursive: true });
} catch (e: unknown) {
mkdirError = e;
}
// 3. Physical security check (after creation, to mitigate TOCTOU symlink attacks)
let realPlansDir: string;
try {
realPlansDir = resolveToRealPath(plansDir);
} catch (e: unknown) {
if (
mkdirError &&
e instanceof Error &&
'code' in e &&
e.code === 'ENOENT'
) {
const errorMessage =
mkdirError instanceof Error
? mkdirError.message
: String(mkdirError);
process.stderr.write(
`Failed to initialize active plan directory at '${plansDir}': ${errorMessage}\n`,
);
this.initializedPlanDirs.add(plansDir);
return plansDir;
}
throw new SecurityError(
`Security violation: Could not securely resolve plan directory '${plansDir}'. System error: ${e instanceof Error ? e.message : String(e)}`,
);
}
if (
!isSubpath(realProjectRoot, realPlansDir) &&
(!realGlobalGeminiDir || !isSubpath(realGlobalGeminiDir, realPlansDir))
) {
throw new SecurityError(
`Security violation: Resolved plan directory '${realPlansDir}' is outside both the project root '${realProjectRoot}' and the global configuration directory.`,
);
}
if (mkdirError) {
const errorMessage =
mkdirError instanceof Error ? mkdirError.message : String(mkdirError);
process.stderr.write(
`Failed to initialize active plan directory at '${plansDir}': ${errorMessage}\n`,
);
} else {
this.workspaceContext.addDirectory(realPlansDir);
}
}
this.initializedPlanDirs.add(plansDir);
return plansDir;
}
getMcpEnablementCallbacks(): McpEnablementCallbacks | undefined {
return this.mcpEnablementCallbacks;
}
+18 -22
View File
@@ -24,7 +24,7 @@ vi.mock('fs', async (importOriginal) => {
});
import { Storage } from './storage.js';
import { GEMINI_DIR, homedir, resolveToRealPath } from '../utils/paths.js';
import { GEMINI_DIR, homedir } from '../utils/paths.js';
import { ProjectRegistry } from './projectRegistry.js';
import { StorageMigration } from './storageMigration.js';
@@ -103,8 +103,8 @@ describe('Storage - Security', () => {
});
describe('Storage additional helpers', () => {
const projectRoot = resolveToRealPath(path.resolve('/tmp/project'));
const storage = new Storage(projectRoot);
const projectRoot = path.resolve('/tmp/project');
let storage = new Storage(projectRoot);
beforeEach(() => {
ProjectRegistry.prototype.getShortId = vi
@@ -306,12 +306,6 @@ describe('Storage additional helpers', () => {
customDir: '.my-plans',
expected: path.resolve(projectRoot, '.my-plans'),
},
{
name: 'custom absolute path outside throws',
customDir: path.resolve('/absolute/path/to/plans'),
expected: '',
expectedError: `Custom plans directory '${path.resolve('/absolute/path/to/plans')}' resolves to '${path.resolve('/absolute/path/to/plans')}', which is outside the project root '${resolveToRealPath(projectRoot)}'.`,
},
{
name: 'absolute path that happens to be inside project root',
customDir: path.join(projectRoot, 'internal-plans'),
@@ -332,37 +326,39 @@ describe('Storage additional helpers', () => {
customDir: undefined,
expected: () => storage.getProjectTempPlansDir(),
},
{
name: 'escaping relative path throws',
customDir: '../escaped-plans',
expected: '',
expectedError: `Custom plans directory '../escaped-plans' resolves to '${resolveToRealPath(path.resolve(projectRoot, '../escaped-plans'))}', which is outside the project root '${resolveToRealPath(projectRoot)}'.`,
},
{
name: 'hidden directory starting with ..',
customDir: '..plans',
expected: path.resolve(projectRoot, '..plans'),
},
{
name: 'security escape via symbolic link throws',
customDir: 'symlink-to-outside',
name: 'non-existent plan dir in a symlinked project root',
customDir: 'new-plans',
setup: () => {
vi.mocked(fs.realpathSync).mockImplementation((p: fs.PathLike) => {
if (p.toString().includes('symlink-to-outside')) {
return path.resolve('/outside/project/root');
const pStr = p.toString();
if (pStr === projectRoot) {
return '/private/tmp/project';
}
return p.toString();
if (pStr.includes('new-plans')) {
const err = new Error('ENOENT') as NodeJS.ErrnoException;
err.code = 'ENOENT';
throw err;
}
return pStr;
});
return () => vi.mocked(fs.realpathSync).mockRestore();
},
expected: '',
expectedError: `Custom plans directory 'symlink-to-outside' resolves to '${path.resolve('/outside/project/root')}', which is outside the project root '${resolveToRealPath(projectRoot)}'.`,
expected: path.resolve(projectRoot, 'new-plans'),
},
];
testCases.forEach(({ name, customDir, expected, expectedError, setup }) => {
it(`should handle ${name}`, async () => {
const cleanup = setup?.();
if (setup) {
storage = new Storage(projectRoot, 'test-session');
}
try {
if (name.includes('default behavior')) {
await storage.initialize();
+10 -18
View File
@@ -12,13 +12,11 @@ import {
GEMINI_DIR,
homedir,
GOOGLE_ACCOUNTS_FILENAME,
isSubpath,
resolveToRealPath,
normalizePath,
} from '../utils/paths.js';
import { ProjectRegistry } from './projectRegistry.js';
import { StorageMigration } from './storageMigration.js';
export const OAUTH_FILE = 'oauth_creds.json';
const TMP_DIR_NAME = 'tmp';
const BIN_DIR_NAME = 'bin';
@@ -32,10 +30,16 @@ export class Storage {
private projectIdentifier: string | undefined;
private initPromise: Promise<void> | undefined;
private customPlansDir: string | undefined;
private readonly realProjectRoot: string;
constructor(targetDir: string, sessionId?: string) {
this.targetDir = targetDir;
this.sessionId = sessionId;
this.realProjectRoot = resolveToRealPath(targetDir);
}
getRealProjectRoot(): string {
return this.realProjectRoot;
}
setCustomPlansDir(dir: string | undefined): void {
@@ -320,22 +324,10 @@ export class Storage {
return path.join(this.getProjectTempDir(), 'tracker');
}
getPlansDir(): string {
if (this.customPlansDir) {
const resolvedPath = path.resolve(
this.getProjectRoot(),
this.customPlansDir,
);
const realProjectRoot = resolveToRealPath(this.getProjectRoot());
const realResolvedPath = resolveToRealPath(resolvedPath);
if (!isSubpath(realProjectRoot, realResolvedPath)) {
throw new Error(
`Custom plans directory '${this.customPlansDir}' resolves to '${realResolvedPath}', which is outside the project root '${realProjectRoot}'.`,
);
}
return resolvedPath;
getPlansDir(extensionPlanDir?: string): string {
const customDir = extensionPlanDir || this.customPlansDir;
if (customDir) {
return path.resolve(this.getProjectRoot(), customDir);
}
return this.getProjectTempPlansDir();
}
@@ -42,7 +42,8 @@ Use the following guidelines to optimize your search and read patterns.
## Engineering Standards
- **Contextual Precedence:** Instructions found in \`GEMINI.md\` files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Conventions & Style:** Rigorously adhere to existing workspace conventions, architectural patterns, and style (naming, formatting, typing, commenting). During the research phase, analyze surrounding files, tests, and configuration to ensure your changes are seamless, idiomatic, and consistent with the local context. Never compromise idiomatic quality or completeness (e.g., proper declarations, type safety, documentation) to minimize tool calls; all supporting changes required by local conventions are part of a surgical update.
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings or bypassing the type system (i.e.: casts in TypeScript) unless explicitly instructed to by the user. Instead, use idiomatic language features (e.g.: type guard functions).
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings, bypassing the type system (e.g.: casts in TypeScript), or employing "hidden" logic (e.g.: reflection, prototype manipulation) unless explicitly instructed to by the user. Instead, use explicit and idiomatic language features (e.g.: type guards, explicit class instantiation, or object spread) that maintain structural integrity and type safety.
- **Design Patterns:** Prioritize explicit composition and delegation (e.g.: wrapper classes, proxies, or factory functions) over complex inheritance or prototype-based cloning. When extending or modifying existing classes, prefer patterns that are easily traceable and type-safe.
- **Libraries/Frameworks:** NEVER assume a library/framework is available. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', etc.) before employing it.
- **Technical Integrity:** You are responsible for the entire lifecycle: implementation, testing, and validation. Within the scope of your changes, prioritize readability and long-term maintainability by consolidating logic into clean abstractions rather than threading state across unrelated layers. Align strictly with the requested architectural direction, ensuring the final implementation is focused and free of redundant "just-in-case" alternatives. Validation is not merely running tests; it is the exhaustive process of ensuring that every aspect of your change—behavioral, structural, and stylistic—is correct and fully compatible with the broader project. For bug fixes, you must empirically reproduce the failure with a new test case or reproduction script before applying the fix.
- **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a corresponding Directive is issued. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. For Directives, only clarify if critically underspecified; otherwise, work autonomously. You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction.
@@ -219,7 +220,8 @@ Use the following guidelines to optimize your search and read patterns.
## Engineering Standards
- **Contextual Precedence:** Instructions found in \`GEMINI.md\` files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Conventions & Style:** Rigorously adhere to existing workspace conventions, architectural patterns, and style (naming, formatting, typing, commenting). During the research phase, analyze surrounding files, tests, and configuration to ensure your changes are seamless, idiomatic, and consistent with the local context. Never compromise idiomatic quality or completeness (e.g., proper declarations, type safety, documentation) to minimize tool calls; all supporting changes required by local conventions are part of a surgical update.
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings or bypassing the type system (i.e.: casts in TypeScript) unless explicitly instructed to by the user. Instead, use idiomatic language features (e.g.: type guard functions).
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings, bypassing the type system (e.g.: casts in TypeScript), or employing "hidden" logic (e.g.: reflection, prototype manipulation) unless explicitly instructed to by the user. Instead, use explicit and idiomatic language features (e.g.: type guards, explicit class instantiation, or object spread) that maintain structural integrity and type safety.
- **Design Patterns:** Prioritize explicit composition and delegation (e.g.: wrapper classes, proxies, or factory functions) over complex inheritance or prototype-based cloning. When extending or modifying existing classes, prefer patterns that are easily traceable and type-safe.
- **Libraries/Frameworks:** NEVER assume a library/framework is available. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', etc.) before employing it.
- **Technical Integrity:** You are responsible for the entire lifecycle: implementation, testing, and validation. Within the scope of your changes, prioritize readability and long-term maintainability by consolidating logic into clean abstractions rather than threading state across unrelated layers. Align strictly with the requested architectural direction, ensuring the final implementation is focused and free of redundant "just-in-case" alternatives. Validation is not merely running tests; it is the exhaustive process of ensuring that every aspect of your change—behavioral, structural, and stylistic—is correct and fully compatible with the broader project. For bug fixes, you must empirically reproduce the failure with a new test case or reproduction script before applying the fix.
- **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a corresponding Directive is issued. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. For Directives, only clarify if critically underspecified; otherwise, work autonomously. You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction.
@@ -369,6 +371,8 @@ exports[`Core System Prompt (prompts.ts) > ApprovalMode in System Prompt > shoul
- **Libraries/Frameworks:** NEVER assume a library/framework is available or appropriate. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', 'build.gradle', etc., or observe neighboring files) before employing it.
- **Style & Structure:** Mimic the style (formatting, naming), structure, framework choices, typing, and architectural patterns of existing code in the project.
- **Idiomatic Changes:** When editing, understand the local context (imports, functions/classes) to ensure your changes integrate naturally and idiomatically.
- **Types, Warnings & Linters:** NEVER use hacks like disabling or suppressing warnings, bypassing the type system (e.g.: casts in TypeScript), or employing "hidden" logic (e.g.: reflection, prototype manipulation) unless explicitly instructed to by the user. Instead, use explicit and idiomatic language features (e.g.: type guards, explicit class instantiation, or object spread) that maintain structural integrity and type safety.
- **Design Patterns:** Prioritize explicit composition and delegation (e.g.: wrapper classes, proxies, or factory functions) over complex inheritance or prototype-based cloning. When extending or modifying existing classes, prefer patterns that are easily traceable and type-safe.
- **Comments:** Add code comments sparingly. Focus on *why* something is done, especially for complex logic, rather than *what* is done. Only add high-value comments if necessary for clarity or if requested by the user. Do not edit comments that are separate from the code you are changing. *NEVER* talk to the user or describe your changes through comments.
- **Proactiveness:** Fulfill the user's request thoroughly. When adding features or fixing bugs, this includes adding tests to ensure quality. Consider all created files, especially tests, to be permanent artifacts unless the user says otherwise.
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
@@ -515,7 +519,8 @@ Use the following guidelines to optimize your search and read patterns.
## Engineering Standards
- **Contextual Precedence:** Instructions found in \`GEMINI.md\` files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Conventions & Style:** Rigorously adhere to existing workspace conventions, architectural patterns, and style (naming, formatting, typing, commenting). During the research phase, analyze surrounding files, tests, and configuration to ensure your changes are seamless, idiomatic, and consistent with the local context. Never compromise idiomatic quality or completeness (e.g., proper declarations, type safety, documentation) to minimize tool calls; all supporting changes required by local conventions are part of a surgical update.
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings or bypassing the type system (i.e.: casts in TypeScript) unless explicitly instructed to by the user. Instead, use idiomatic language features (e.g.: type guard functions).
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings, bypassing the type system (e.g.: casts in TypeScript), or employing "hidden" logic (e.g.: reflection, prototype manipulation) unless explicitly instructed to by the user. Instead, use explicit and idiomatic language features (e.g.: type guards, explicit class instantiation, or object spread) that maintain structural integrity and type safety.
- **Design Patterns:** Prioritize explicit composition and delegation (e.g.: wrapper classes, proxies, or factory functions) over complex inheritance or prototype-based cloning. When extending or modifying existing classes, prefer patterns that are easily traceable and type-safe.
- **Libraries/Frameworks:** NEVER assume a library/framework is available. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', etc.) before employing it.
- **Technical Integrity:** You are responsible for the entire lifecycle: implementation, testing, and validation. Within the scope of your changes, prioritize readability and long-term maintainability by consolidating logic into clean abstractions rather than threading state across unrelated layers. Align strictly with the requested architectural direction, ensuring the final implementation is focused and free of redundant "just-in-case" alternatives. Validation is not merely running tests; it is the exhaustive process of ensuring that every aspect of your change—behavioral, structural, and stylistic—is correct and fully compatible with the broader project. For bug fixes, you must empirically reproduce the failure with a new test case or reproduction script before applying the fix.
- **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a corresponding Directive is issued. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. For Directives, only clarify if critically underspecified; otherwise, work autonomously. You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction.
@@ -567,7 +572,7 @@ For example:
# Active Approval Mode: Plan
You are operating in **Plan Mode**. Your goal is to produce an implementation plan in \`/tmp/project-temp/plans/\` and get user approval before editing source code.
You are operating in **Plan Mode**. Your goal is to produce an implementation plan in \`/tmp/plans/\` and get user approval before editing source code.
## Available Tools
The following tools are available in Plan Mode:
@@ -583,8 +588,8 @@ The following tools are available in Plan Mode:
</available_tools>
## Rules
1. **Read-Only:** You cannot modify source code. You may ONLY use read-only tools to explore, and you can only write to \`/tmp/project-temp/plans/\`. If the user asks you to modify source code directly, you MUST explain that you are in Plan Mode and must first create a plan and get approval.
2. **Write Constraint:** \`write_file\` and \`replace\` may ONLY be used to write .md plan files to \`/tmp/project-temp/plans/\`. They cannot modify source code.
1. **Read-Only:** You cannot modify source code. You may ONLY use read-only tools to explore, and you can only write to \`/tmp/plans/\`. If the user asks you to modify source code directly, you MUST explain that you are in Plan Mode and must first create a plan and get approval.
2. **Write Constraint:** \`write_file\` and \`replace\` may ONLY be used to write .md plan files to \`/tmp/plans/\`. They cannot modify source code.
3. **Efficiency:** Autonomously combine discovery and drafting phases to minimize conversational turns. If the request is ambiguous, use \`ask_user\` to clarify. Use multi-select to offer flexibility and include detailed descriptions for each option to help the user understand the implications of their choice.
4. **Inquiries and Directives:** Distinguish between Inquiries and Directives to minimize unnecessary planning.
- **Inquiries:** If the request is an **Inquiry** (e.g., "How does X work?"), answer directly. DO NOT create a plan.
@@ -607,7 +612,7 @@ The depth of your consultation should be proportional to the task's complexity.
**CRITICAL:** You MUST NOT proceed to Step 3 (Draft) or Step 4 (Review & Approval) in the same turn as your initial strategy proposal. You MUST wait for user feedback and reach a clear agreement before drafting or submitting the plan.
### 3. Draft
Write the implementation plan to \`/tmp/project-temp/plans/\`. The plan's structure adapts to the task:
Write the implementation plan to \`/tmp/plans/\`. The plan's structure adapts to the task:
- **Simple Tasks:** Include a bulleted list of specific **Changes** and **Verification** steps.
- **Standard Tasks:** Include an **Objective**, **Key Files & Context**, **Implementation Steps**, and **Verification & Testing**.
- **Complex Tasks:** Include **Background & Motivation**, **Scope & Impact**, **Proposed Solution**, **Alternatives Considered**, a phased **Implementation Plan**, **Verification**, and **Migration & Rollback** strategies.
@@ -692,7 +697,8 @@ Use the following guidelines to optimize your search and read patterns.
## Engineering Standards
- **Contextual Precedence:** Instructions found in \`GEMINI.md\` files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Conventions & Style:** Rigorously adhere to existing workspace conventions, architectural patterns, and style (naming, formatting, typing, commenting). During the research phase, analyze surrounding files, tests, and configuration to ensure your changes are seamless, idiomatic, and consistent with the local context. Never compromise idiomatic quality or completeness (e.g., proper declarations, type safety, documentation) to minimize tool calls; all supporting changes required by local conventions are part of a surgical update.
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings or bypassing the type system (i.e.: casts in TypeScript) unless explicitly instructed to by the user. Instead, use idiomatic language features (e.g.: type guard functions).
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings, bypassing the type system (e.g.: casts in TypeScript), or employing "hidden" logic (e.g.: reflection, prototype manipulation) unless explicitly instructed to by the user. Instead, use explicit and idiomatic language features (e.g.: type guards, explicit class instantiation, or object spread) that maintain structural integrity and type safety.
- **Design Patterns:** Prioritize explicit composition and delegation (e.g.: wrapper classes, proxies, or factory functions) over complex inheritance or prototype-based cloning. When extending or modifying existing classes, prefer patterns that are easily traceable and type-safe.
- **Libraries/Frameworks:** NEVER assume a library/framework is available. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', etc.) before employing it.
- **Technical Integrity:** You are responsible for the entire lifecycle: implementation, testing, and validation. Within the scope of your changes, prioritize readability and long-term maintainability by consolidating logic into clean abstractions rather than threading state across unrelated layers. Align strictly with the requested architectural direction, ensuring the final implementation is focused and free of redundant "just-in-case" alternatives. Validation is not merely running tests; it is the exhaustive process of ensuring that every aspect of your change—behavioral, structural, and stylistic—is correct and fully compatible with the broader project. For bug fixes, you must empirically reproduce the failure with a new test case or reproduction script before applying the fix.
- **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a corresponding Directive is issued. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. For Directives, only clarify if critically underspecified; otherwise, work autonomously. You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction.
@@ -867,7 +873,8 @@ Use the following guidelines to optimize your search and read patterns.
## Engineering Standards
- **Contextual Precedence:** Instructions found in \`GEMINI.md\` files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Conventions & Style:** Rigorously adhere to existing workspace conventions, architectural patterns, and style (naming, formatting, typing, commenting). During the research phase, analyze surrounding files, tests, and configuration to ensure your changes are seamless, idiomatic, and consistent with the local context. Never compromise idiomatic quality or completeness (e.g., proper declarations, type safety, documentation) to minimize tool calls; all supporting changes required by local conventions are part of a surgical update.
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings or bypassing the type system (i.e.: casts in TypeScript) unless explicitly instructed to by the user. Instead, use idiomatic language features (e.g.: type guard functions).
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings, bypassing the type system (e.g.: casts in TypeScript), or employing "hidden" logic (e.g.: reflection, prototype manipulation) unless explicitly instructed to by the user. Instead, use explicit and idiomatic language features (e.g.: type guards, explicit class instantiation, or object spread) that maintain structural integrity and type safety.
- **Design Patterns:** Prioritize explicit composition and delegation (e.g.: wrapper classes, proxies, or factory functions) over complex inheritance or prototype-based cloning. When extending or modifying existing classes, prefer patterns that are easily traceable and type-safe.
- **Libraries/Frameworks:** NEVER assume a library/framework is available. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', etc.) before employing it.
- **Technical Integrity:** You are responsible for the entire lifecycle: implementation, testing, and validation. Within the scope of your changes, prioritize readability and long-term maintainability by consolidating logic into clean abstractions rather than threading state across unrelated layers. Align strictly with the requested architectural direction, ensuring the final implementation is focused and free of redundant "just-in-case" alternatives. Validation is not merely running tests; it is the exhaustive process of ensuring that every aspect of your change—behavioral, structural, and stylistic—is correct and fully compatible with the broader project. For bug fixes, you must empirically reproduce the failure with a new test case or reproduction script before applying the fix.
- **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a corresponding Directive is issued. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. For Directives, you must work autonomously as no further user input is available. You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction.
@@ -994,7 +1001,8 @@ Use the following guidelines to optimize your search and read patterns.
## Engineering Standards
- **Contextual Precedence:** Instructions found in \`GEMINI.md\` files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Conventions & Style:** Rigorously adhere to existing workspace conventions, architectural patterns, and style (naming, formatting, typing, commenting). During the research phase, analyze surrounding files, tests, and configuration to ensure your changes are seamless, idiomatic, and consistent with the local context. Never compromise idiomatic quality or completeness (e.g., proper declarations, type safety, documentation) to minimize tool calls; all supporting changes required by local conventions are part of a surgical update.
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings or bypassing the type system (i.e.: casts in TypeScript) unless explicitly instructed to by the user. Instead, use idiomatic language features (e.g.: type guard functions).
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings, bypassing the type system (e.g.: casts in TypeScript), or employing "hidden" logic (e.g.: reflection, prototype manipulation) unless explicitly instructed to by the user. Instead, use explicit and idiomatic language features (e.g.: type guards, explicit class instantiation, or object spread) that maintain structural integrity and type safety.
- **Design Patterns:** Prioritize explicit composition and delegation (e.g.: wrapper classes, proxies, or factory functions) over complex inheritance or prototype-based cloning. When extending or modifying existing classes, prefer patterns that are easily traceable and type-safe.
- **Libraries/Frameworks:** NEVER assume a library/framework is available. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', etc.) before employing it.
- **Technical Integrity:** You are responsible for the entire lifecycle: implementation, testing, and validation. Within the scope of your changes, prioritize readability and long-term maintainability by consolidating logic into clean abstractions rather than threading state across unrelated layers. Align strictly with the requested architectural direction, ensuring the final implementation is focused and free of redundant "just-in-case" alternatives. Validation is not merely running tests; it is the exhaustive process of ensuring that every aspect of your change—behavioral, structural, and stylistic—is correct and fully compatible with the broader project. For bug fixes, you must empirically reproduce the failure with a new test case or reproduction script before applying the fix.
- **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a corresponding Directive is issued. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. For Directives, you must work autonomously as no further user input is available. You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction.
@@ -1088,6 +1096,8 @@ exports[`Core System Prompt (prompts.ts) > should handle git instructions when i
- **Libraries/Frameworks:** NEVER assume a library/framework is available or appropriate. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', 'build.gradle', etc., or observe neighboring files) before employing it.
- **Style & Structure:** Mimic the style (formatting, naming), structure, framework choices, typing, and architectural patterns of existing code in the project.
- **Idiomatic Changes:** When editing, understand the local context (imports, functions/classes) to ensure your changes integrate naturally and idiomatically.
- **Types, Warnings & Linters:** NEVER use hacks like disabling or suppressing warnings, bypassing the type system (e.g.: casts in TypeScript), or employing "hidden" logic (e.g.: reflection, prototype manipulation) unless explicitly instructed to by the user. Instead, use explicit and idiomatic language features (e.g.: type guards, explicit class instantiation, or object spread) that maintain structural integrity and type safety.
- **Design Patterns:** Prioritize explicit composition and delegation (e.g.: wrapper classes, proxies, or factory functions) over complex inheritance or prototype-based cloning. When extending or modifying existing classes, prefer patterns that are easily traceable and type-safe.
- **Comments:** Add code comments sparingly. Focus on *why* something is done, especially for complex logic, rather than *what* is done. Only add high-value comments if necessary for clarity or if requested by the user. Do not edit comments that are separate from the code you are changing. *NEVER* talk to the user or describe your changes through comments.
- **Proactiveness:** Fulfill the user's request thoroughly. When adding features or fixing bugs, this includes adding tests to ensure quality. Consider all created files, especially tests, to be permanent artifacts unless the user says otherwise.
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
@@ -1201,6 +1211,8 @@ exports[`Core System Prompt (prompts.ts) > should handle git instructions when i
- **Libraries/Frameworks:** NEVER assume a library/framework is available or appropriate. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', 'build.gradle', etc., or observe neighboring files) before employing it.
- **Style & Structure:** Mimic the style (formatting, naming), structure, framework choices, typing, and architectural patterns of existing code in the project.
- **Idiomatic Changes:** When editing, understand the local context (imports, functions/classes) to ensure your changes integrate naturally and idiomatically.
- **Types, Warnings & Linters:** NEVER use hacks like disabling or suppressing warnings, bypassing the type system (e.g.: casts in TypeScript), or employing "hidden" logic (e.g.: reflection, prototype manipulation) unless explicitly instructed to by the user. Instead, use explicit and idiomatic language features (e.g.: type guards, explicit class instantiation, or object spread) that maintain structural integrity and type safety.
- **Design Patterns:** Prioritize explicit composition and delegation (e.g.: wrapper classes, proxies, or factory functions) over complex inheritance or prototype-based cloning. When extending or modifying existing classes, prefer patterns that are easily traceable and type-safe.
- **Comments:** Add code comments sparingly. Focus on *why* something is done, especially for complex logic, rather than *what* is done. Only add high-value comments if necessary for clarity or if requested by the user. Do not edit comments that are separate from the code you are changing. *NEVER* talk to the user or describe your changes through comments.
- **Proactiveness:** Fulfill the user's request thoroughly. When adding features or fixing bugs, this includes adding tests to ensure quality. Consider all created files, especially tests, to be permanent artifacts unless the user says otherwise.
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
@@ -1332,6 +1344,8 @@ exports[`Core System Prompt (prompts.ts) > should include approved plan instruct
- **Libraries/Frameworks:** NEVER assume a library/framework is available or appropriate. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', 'build.gradle', etc., or observe neighboring files) before employing it.
- **Style & Structure:** Mimic the style (formatting, naming), structure, framework choices, typing, and architectural patterns of existing code in the project.
- **Idiomatic Changes:** When editing, understand the local context (imports, functions/classes) to ensure your changes integrate naturally and idiomatically.
- **Types, Warnings & Linters:** NEVER use hacks like disabling or suppressing warnings, bypassing the type system (e.g.: casts in TypeScript), or employing "hidden" logic (e.g.: reflection, prototype manipulation) unless explicitly instructed to by the user. Instead, use explicit and idiomatic language features (e.g.: type guards, explicit class instantiation, or object spread) that maintain structural integrity and type safety.
- **Design Patterns:** Prioritize explicit composition and delegation (e.g.: wrapper classes, proxies, or factory functions) over complex inheritance or prototype-based cloning. When extending or modifying existing classes, prefer patterns that are easily traceable and type-safe.
- **Comments:** Add code comments sparingly. Focus on *why* something is done, especially for complex logic, rather than *what* is done. Only add high-value comments if necessary for clarity or if requested by the user. Do not edit comments that are separate from the code you are changing. *NEVER* talk to the user or describe your changes through comments.
- **Proactiveness:** Fulfill the user's request thoroughly. When adding features or fixing bugs, this includes adding tests to ensure quality. Consider all created files, especially tests, to be permanent artifacts unless the user says otherwise.
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
@@ -1435,6 +1449,8 @@ exports[`Core System Prompt (prompts.ts) > should include available_skills when
- **Libraries/Frameworks:** NEVER assume a library/framework is available or appropriate. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', 'build.gradle', etc., or observe neighboring files) before employing it.
- **Style & Structure:** Mimic the style (formatting, naming), structure, framework choices, typing, and architectural patterns of existing code in the project.
- **Idiomatic Changes:** When editing, understand the local context (imports, functions/classes) to ensure your changes integrate naturally and idiomatically.
- **Types, Warnings & Linters:** NEVER use hacks like disabling or suppressing warnings, bypassing the type system (e.g.: casts in TypeScript), or employing "hidden" logic (e.g.: reflection, prototype manipulation) unless explicitly instructed to by the user. Instead, use explicit and idiomatic language features (e.g.: type guards, explicit class instantiation, or object spread) that maintain structural integrity and type safety.
- **Design Patterns:** Prioritize explicit composition and delegation (e.g.: wrapper classes, proxies, or factory functions) over complex inheritance or prototype-based cloning. When extending or modifying existing classes, prefer patterns that are easily traceable and type-safe.
- **Comments:** Add code comments sparingly. Focus on *why* something is done, especially for complex logic, rather than *what* is done. Only add high-value comments if necessary for clarity or if requested by the user. Do not edit comments that are separate from the code you are changing. *NEVER* talk to the user or describe your changes through comments.
- **Proactiveness:** Fulfill the user's request thoroughly. When adding features or fixing bugs, this includes adding tests to ensure quality. Consider all created files, especially tests, to be permanent artifacts unless the user says otherwise.
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
@@ -1594,7 +1610,8 @@ Use the following guidelines to optimize your search and read patterns.
## Engineering Standards
- **Contextual Precedence:** Instructions found in \`GEMINI.md\` files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Conventions & Style:** Rigorously adhere to existing workspace conventions, architectural patterns, and style (naming, formatting, typing, commenting). During the research phase, analyze surrounding files, tests, and configuration to ensure your changes are seamless, idiomatic, and consistent with the local context. Never compromise idiomatic quality or completeness (e.g., proper declarations, type safety, documentation) to minimize tool calls; all supporting changes required by local conventions are part of a surgical update.
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings or bypassing the type system (i.e.: casts in TypeScript) unless explicitly instructed to by the user. Instead, use idiomatic language features (e.g.: type guard functions).
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings, bypassing the type system (e.g.: casts in TypeScript), or employing "hidden" logic (e.g.: reflection, prototype manipulation) unless explicitly instructed to by the user. Instead, use explicit and idiomatic language features (e.g.: type guards, explicit class instantiation, or object spread) that maintain structural integrity and type safety.
- **Design Patterns:** Prioritize explicit composition and delegation (e.g.: wrapper classes, proxies, or factory functions) over complex inheritance or prototype-based cloning. When extending or modifying existing classes, prefer patterns that are easily traceable and type-safe.
- **Libraries/Frameworks:** NEVER assume a library/framework is available. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', etc.) before employing it.
- **Technical Integrity:** You are responsible for the entire lifecycle: implementation, testing, and validation. Within the scope of your changes, prioritize readability and long-term maintainability by consolidating logic into clean abstractions rather than threading state across unrelated layers. Align strictly with the requested architectural direction, ensuring the final implementation is focused and free of redundant "just-in-case" alternatives. Validation is not merely running tests; it is the exhaustive process of ensuring that every aspect of your change—behavioral, structural, and stylistic—is correct and fully compatible with the broader project. For bug fixes, you must empirically reproduce the failure with a new test case or reproduction script before applying the fix.
- **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a corresponding Directive is issued. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. For Directives, only clarify if critically underspecified; otherwise, work autonomously. You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction.
@@ -1765,7 +1782,8 @@ Use the following guidelines to optimize your search and read patterns.
## Engineering Standards
- **Contextual Precedence:** Instructions found in \`GEMINI.md\` files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Conventions & Style:** Rigorously adhere to existing workspace conventions, architectural patterns, and style (naming, formatting, typing, commenting). During the research phase, analyze surrounding files, tests, and configuration to ensure your changes are seamless, idiomatic, and consistent with the local context. Never compromise idiomatic quality or completeness (e.g., proper declarations, type safety, documentation) to minimize tool calls; all supporting changes required by local conventions are part of a surgical update.
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings or bypassing the type system (i.e.: casts in TypeScript) unless explicitly instructed to by the user. Instead, use idiomatic language features (e.g.: type guard functions).
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings, bypassing the type system (e.g.: casts in TypeScript), or employing "hidden" logic (e.g.: reflection, prototype manipulation) unless explicitly instructed to by the user. Instead, use explicit and idiomatic language features (e.g.: type guards, explicit class instantiation, or object spread) that maintain structural integrity and type safety.
- **Design Patterns:** Prioritize explicit composition and delegation (e.g.: wrapper classes, proxies, or factory functions) over complex inheritance or prototype-based cloning. When extending or modifying existing classes, prefer patterns that are easily traceable and type-safe.
- **Libraries/Frameworks:** NEVER assume a library/framework is available. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', etc.) before employing it.
- **Technical Integrity:** You are responsible for the entire lifecycle: implementation, testing, and validation. Within the scope of your changes, prioritize readability and long-term maintainability by consolidating logic into clean abstractions rather than threading state across unrelated layers. Align strictly with the requested architectural direction, ensuring the final implementation is focused and free of redundant "just-in-case" alternatives. Validation is not merely running tests; it is the exhaustive process of ensuring that every aspect of your change—behavioral, structural, and stylistic—is correct and fully compatible with the broader project. For bug fixes, you must empirically reproduce the failure with a new test case or reproduction script before applying the fix.
- **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a corresponding Directive is issued. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. For Directives, only clarify if critically underspecified; otherwise, work autonomously. You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction.
@@ -1927,7 +1945,8 @@ Use the following guidelines to optimize your search and read patterns.
## Engineering Standards
- **Contextual Precedence:** Instructions found in \`GEMINI.md\` files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Conventions & Style:** Rigorously adhere to existing workspace conventions, architectural patterns, and style (naming, formatting, typing, commenting). During the research phase, analyze surrounding files, tests, and configuration to ensure your changes are seamless, idiomatic, and consistent with the local context. Never compromise idiomatic quality or completeness (e.g., proper declarations, type safety, documentation) to minimize tool calls; all supporting changes required by local conventions are part of a surgical update.
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings or bypassing the type system (i.e.: casts in TypeScript) unless explicitly instructed to by the user. Instead, use idiomatic language features (e.g.: type guard functions).
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings, bypassing the type system (e.g.: casts in TypeScript), or employing "hidden" logic (e.g.: reflection, prototype manipulation) unless explicitly instructed to by the user. Instead, use explicit and idiomatic language features (e.g.: type guards, explicit class instantiation, or object spread) that maintain structural integrity and type safety.
- **Design Patterns:** Prioritize explicit composition and delegation (e.g.: wrapper classes, proxies, or factory functions) over complex inheritance or prototype-based cloning. When extending or modifying existing classes, prefer patterns that are easily traceable and type-safe.
- **Libraries/Frameworks:** NEVER assume a library/framework is available. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', etc.) before employing it.
- **Technical Integrity:** You are responsible for the entire lifecycle: implementation, testing, and validation. Within the scope of your changes, prioritize readability and long-term maintainability by consolidating logic into clean abstractions rather than threading state across unrelated layers. Align strictly with the requested architectural direction, ensuring the final implementation is focused and free of redundant "just-in-case" alternatives. Validation is not merely running tests; it is the exhaustive process of ensuring that every aspect of your change—behavioral, structural, and stylistic—is correct and fully compatible with the broader project. For bug fixes, you must empirically reproduce the failure with a new test case or reproduction script before applying the fix.
- **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a corresponding Directive is issued. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. For Directives, only clarify if critically underspecified; otherwise, work autonomously. You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction.
@@ -2089,7 +2108,8 @@ Use the following guidelines to optimize your search and read patterns.
## Engineering Standards
- **Contextual Precedence:** Instructions found in \`GEMINI.md\` files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Conventions & Style:** Rigorously adhere to existing workspace conventions, architectural patterns, and style (naming, formatting, typing, commenting). During the research phase, analyze surrounding files, tests, and configuration to ensure your changes are seamless, idiomatic, and consistent with the local context. Never compromise idiomatic quality or completeness (e.g., proper declarations, type safety, documentation) to minimize tool calls; all supporting changes required by local conventions are part of a surgical update.
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings or bypassing the type system (i.e.: casts in TypeScript) unless explicitly instructed to by the user. Instead, use idiomatic language features (e.g.: type guard functions).
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings, bypassing the type system (e.g.: casts in TypeScript), or employing "hidden" logic (e.g.: reflection, prototype manipulation) unless explicitly instructed to by the user. Instead, use explicit and idiomatic language features (e.g.: type guards, explicit class instantiation, or object spread) that maintain structural integrity and type safety.
- **Design Patterns:** Prioritize explicit composition and delegation (e.g.: wrapper classes, proxies, or factory functions) over complex inheritance or prototype-based cloning. When extending or modifying existing classes, prefer patterns that are easily traceable and type-safe.
- **Libraries/Frameworks:** NEVER assume a library/framework is available. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', etc.) before employing it.
- **Technical Integrity:** You are responsible for the entire lifecycle: implementation, testing, and validation. Within the scope of your changes, prioritize readability and long-term maintainability by consolidating logic into clean abstractions rather than threading state across unrelated layers. Align strictly with the requested architectural direction, ensuring the final implementation is focused and free of redundant "just-in-case" alternatives. Validation is not merely running tests; it is the exhaustive process of ensuring that every aspect of your change—behavioral, structural, and stylistic—is correct and fully compatible with the broader project. For bug fixes, you must empirically reproduce the failure with a new test case or reproduction script before applying the fix.
- **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a corresponding Directive is issued. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. For Directives, only clarify if critically underspecified; otherwise, work autonomously. You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction.
@@ -2247,7 +2267,8 @@ Use the following guidelines to optimize your search and read patterns.
## Engineering Standards
- **Contextual Precedence:** Instructions found in \`GEMINI.md\` files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Conventions & Style:** Rigorously adhere to existing workspace conventions, architectural patterns, and style (naming, formatting, typing, commenting). During the research phase, analyze surrounding files, tests, and configuration to ensure your changes are seamless, idiomatic, and consistent with the local context. Never compromise idiomatic quality or completeness (e.g., proper declarations, type safety, documentation) to minimize tool calls; all supporting changes required by local conventions are part of a surgical update.
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings or bypassing the type system (i.e.: casts in TypeScript) unless explicitly instructed to by the user. Instead, use idiomatic language features (e.g.: type guard functions).
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings, bypassing the type system (e.g.: casts in TypeScript), or employing "hidden" logic (e.g.: reflection, prototype manipulation) unless explicitly instructed to by the user. Instead, use explicit and idiomatic language features (e.g.: type guards, explicit class instantiation, or object spread) that maintain structural integrity and type safety.
- **Design Patterns:** Prioritize explicit composition and delegation (e.g.: wrapper classes, proxies, or factory functions) over complex inheritance or prototype-based cloning. When extending or modifying existing classes, prefer patterns that are easily traceable and type-safe.
- **Libraries/Frameworks:** NEVER assume a library/framework is available. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', etc.) before employing it.
- **Technical Integrity:** You are responsible for the entire lifecycle: implementation, testing, and validation. Within the scope of your changes, prioritize readability and long-term maintainability by consolidating logic into clean abstractions rather than threading state across unrelated layers. Align strictly with the requested architectural direction, ensuring the final implementation is focused and free of redundant "just-in-case" alternatives. Validation is not merely running tests; it is the exhaustive process of ensuring that every aspect of your change—behavioral, structural, and stylistic—is correct and fully compatible with the broader project. For bug fixes, you must empirically reproduce the failure with a new test case or reproduction script before applying the fix.
- **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a corresponding Directive is issued. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. For Directives, only clarify if critically underspecified; otherwise, work autonomously. You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction.
@@ -2405,7 +2426,8 @@ Use the following guidelines to optimize your search and read patterns.
## Engineering Standards
- **Contextual Precedence:** Instructions found in \`GEMINI.md\` files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Conventions & Style:** Rigorously adhere to existing workspace conventions, architectural patterns, and style (naming, formatting, typing, commenting). During the research phase, analyze surrounding files, tests, and configuration to ensure your changes are seamless, idiomatic, and consistent with the local context. Never compromise idiomatic quality or completeness (e.g., proper declarations, type safety, documentation) to minimize tool calls; all supporting changes required by local conventions are part of a surgical update.
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings or bypassing the type system (i.e.: casts in TypeScript) unless explicitly instructed to by the user. Instead, use idiomatic language features (e.g.: type guard functions).
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings, bypassing the type system (e.g.: casts in TypeScript), or employing "hidden" logic (e.g.: reflection, prototype manipulation) unless explicitly instructed to by the user. Instead, use explicit and idiomatic language features (e.g.: type guards, explicit class instantiation, or object spread) that maintain structural integrity and type safety.
- **Design Patterns:** Prioritize explicit composition and delegation (e.g.: wrapper classes, proxies, or factory functions) over complex inheritance or prototype-based cloning. When extending or modifying existing classes, prefer patterns that are easily traceable and type-safe.
- **Libraries/Frameworks:** NEVER assume a library/framework is available. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', etc.) before employing it.
- **Technical Integrity:** You are responsible for the entire lifecycle: implementation, testing, and validation. Within the scope of your changes, prioritize readability and long-term maintainability by consolidating logic into clean abstractions rather than threading state across unrelated layers. Align strictly with the requested architectural direction, ensuring the final implementation is focused and free of redundant "just-in-case" alternatives. Validation is not merely running tests; it is the exhaustive process of ensuring that every aspect of your change—behavioral, structural, and stylistic—is correct and fully compatible with the broader project. For bug fixes, you must empirically reproduce the failure with a new test case or reproduction script before applying the fix.
- **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a corresponding Directive is issued. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. For Directives, only clarify if critically underspecified; otherwise, work autonomously. You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction.
@@ -2557,7 +2579,8 @@ Use the following guidelines to optimize your search and read patterns.
## Engineering Standards
- **Contextual Precedence:** Instructions found in \`GEMINI.md\` files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Conventions & Style:** Rigorously adhere to existing workspace conventions, architectural patterns, and style (naming, formatting, typing, commenting). During the research phase, analyze surrounding files, tests, and configuration to ensure your changes are seamless, idiomatic, and consistent with the local context. Never compromise idiomatic quality or completeness (e.g., proper declarations, type safety, documentation) to minimize tool calls; all supporting changes required by local conventions are part of a surgical update.
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings or bypassing the type system (i.e.: casts in TypeScript) unless explicitly instructed to by the user. Instead, use idiomatic language features (e.g.: type guard functions).
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings, bypassing the type system (e.g.: casts in TypeScript), or employing "hidden" logic (e.g.: reflection, prototype manipulation) unless explicitly instructed to by the user. Instead, use explicit and idiomatic language features (e.g.: type guards, explicit class instantiation, or object spread) that maintain structural integrity and type safety.
- **Design Patterns:** Prioritize explicit composition and delegation (e.g.: wrapper classes, proxies, or factory functions) over complex inheritance or prototype-based cloning. When extending or modifying existing classes, prefer patterns that are easily traceable and type-safe.
- **Libraries/Frameworks:** NEVER assume a library/framework is available. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', etc.) before employing it.
- **Technical Integrity:** You are responsible for the entire lifecycle: implementation, testing, and validation. Within the scope of your changes, prioritize readability and long-term maintainability by consolidating logic into clean abstractions rather than threading state across unrelated layers. Align strictly with the requested architectural direction, ensuring the final implementation is focused and free of redundant "just-in-case" alternatives. Validation is not merely running tests; it is the exhaustive process of ensuring that every aspect of your change—behavioral, structural, and stylistic—is correct and fully compatible with the broader project. For bug fixes, you must empirically reproduce the failure with a new test case or reproduction script before applying the fix.
- **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a corresponding Directive is issued. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. For Directives, only clarify if critically underspecified; otherwise, work autonomously. You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction.
@@ -2714,7 +2737,8 @@ Use the following guidelines to optimize your search and read patterns.
## Engineering Standards
- **Contextual Precedence:** Instructions found in \`GEMINI.md\` files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Conventions & Style:** Rigorously adhere to existing workspace conventions, architectural patterns, and style (naming, formatting, typing, commenting). During the research phase, analyze surrounding files, tests, and configuration to ensure your changes are seamless, idiomatic, and consistent with the local context. Never compromise idiomatic quality or completeness (e.g., proper declarations, type safety, documentation) to minimize tool calls; all supporting changes required by local conventions are part of a surgical update.
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings or bypassing the type system (i.e.: casts in TypeScript) unless explicitly instructed to by the user. Instead, use idiomatic language features (e.g.: type guard functions).
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings, bypassing the type system (e.g.: casts in TypeScript), or employing "hidden" logic (e.g.: reflection, prototype manipulation) unless explicitly instructed to by the user. Instead, use explicit and idiomatic language features (e.g.: type guards, explicit class instantiation, or object spread) that maintain structural integrity and type safety.
- **Design Patterns:** Prioritize explicit composition and delegation (e.g.: wrapper classes, proxies, or factory functions) over complex inheritance or prototype-based cloning. When extending or modifying existing classes, prefer patterns that are easily traceable and type-safe.
- **Libraries/Frameworks:** NEVER assume a library/framework is available. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', etc.) before employing it.
- **Technical Integrity:** You are responsible for the entire lifecycle: implementation, testing, and validation. Within the scope of your changes, prioritize readability and long-term maintainability by consolidating logic into clean abstractions rather than threading state across unrelated layers. Align strictly with the requested architectural direction, ensuring the final implementation is focused and free of redundant "just-in-case" alternatives. Validation is not merely running tests; it is the exhaustive process of ensuring that every aspect of your change—behavioral, structural, and stylistic—is correct and fully compatible with the broader project. For bug fixes, you must empirically reproduce the failure with a new test case or reproduction script before applying the fix.
- **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a corresponding Directive is issued. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. For Directives, only clarify if critically underspecified; otherwise, work autonomously. You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction.
@@ -2839,6 +2863,8 @@ exports[`Core System Prompt (prompts.ts) > should include the TASK MANAGEMENT PR
- **Libraries/Frameworks:** NEVER assume a library/framework is available or appropriate. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', 'build.gradle', etc., or observe neighboring files) before employing it.
- **Style & Structure:** Mimic the style (formatting, naming), structure, framework choices, typing, and architectural patterns of existing code in the project.
- **Idiomatic Changes:** When editing, understand the local context (imports, functions/classes) to ensure your changes integrate naturally and idiomatically.
- **Types, Warnings & Linters:** NEVER use hacks like disabling or suppressing warnings, bypassing the type system (e.g.: casts in TypeScript), or employing "hidden" logic (e.g.: reflection, prototype manipulation) unless explicitly instructed to by the user. Instead, use explicit and idiomatic language features (e.g.: type guards, explicit class instantiation, or object spread) that maintain structural integrity and type safety.
- **Design Patterns:** Prioritize explicit composition and delegation (e.g.: wrapper classes, proxies, or factory functions) over complex inheritance or prototype-based cloning. When extending or modifying existing classes, prefer patterns that are easily traceable and type-safe.
- **Comments:** Add code comments sparingly. Focus on *why* something is done, especially for complex logic, rather than *what* is done. Only add high-value comments if necessary for clarity or if requested by the user. Do not edit comments that are separate from the code you are changing. *NEVER* talk to the user or describe your changes through comments.
- **Proactiveness:** Fulfill the user's request thoroughly. When adding features or fixing bugs, this includes adding tests to ensure quality. Consider all created files, especially tests, to be permanent artifacts unless the user says otherwise.
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
@@ -2997,7 +3023,8 @@ Use the following guidelines to optimize your search and read patterns.
## Engineering Standards
- **Contextual Precedence:** Instructions found in \`GEMINI.md\` files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Conventions & Style:** Rigorously adhere to existing workspace conventions, architectural patterns, and style (naming, formatting, typing, commenting). During the research phase, analyze surrounding files, tests, and configuration to ensure your changes are seamless, idiomatic, and consistent with the local context. Never compromise idiomatic quality or completeness (e.g., proper declarations, type safety, documentation) to minimize tool calls; all supporting changes required by local conventions are part of a surgical update.
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings or bypassing the type system (i.e.: casts in TypeScript) unless explicitly instructed to by the user. Instead, use idiomatic language features (e.g.: type guard functions).
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings, bypassing the type system (e.g.: casts in TypeScript), or employing "hidden" logic (e.g.: reflection, prototype manipulation) unless explicitly instructed to by the user. Instead, use explicit and idiomatic language features (e.g.: type guards, explicit class instantiation, or object spread) that maintain structural integrity and type safety.
- **Design Patterns:** Prioritize explicit composition and delegation (e.g.: wrapper classes, proxies, or factory functions) over complex inheritance or prototype-based cloning. When extending or modifying existing classes, prefer patterns that are easily traceable and type-safe.
- **Libraries/Frameworks:** NEVER assume a library/framework is available. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', etc.) before employing it.
- **Technical Integrity:** You are responsible for the entire lifecycle: implementation, testing, and validation. Within the scope of your changes, prioritize readability and long-term maintainability by consolidating logic into clean abstractions rather than threading state across unrelated layers. Align strictly with the requested architectural direction, ensuring the final implementation is focused and free of redundant "just-in-case" alternatives. Validation is not merely running tests; it is the exhaustive process of ensuring that every aspect of your change—behavioral, structural, and stylistic—is correct and fully compatible with the broader project. For bug fixes, you must empirically reproduce the failure with a new test case or reproduction script before applying the fix.
- **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a corresponding Directive is issued. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. For Directives, only clarify if critically underspecified; otherwise, work autonomously. You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction.
@@ -3134,6 +3161,8 @@ exports[`Core System Prompt (prompts.ts) > should match snapshot on Windows 1`]
- **Libraries/Frameworks:** NEVER assume a library/framework is available or appropriate. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', 'build.gradle', etc., or observe neighboring files) before employing it.
- **Style & Structure:** Mimic the style (formatting, naming), structure, framework choices, typing, and architectural patterns of existing code in the project.
- **Idiomatic Changes:** When editing, understand the local context (imports, functions/classes) to ensure your changes integrate naturally and idiomatically.
- **Types, Warnings & Linters:** NEVER use hacks like disabling or suppressing warnings, bypassing the type system (e.g.: casts in TypeScript), or employing "hidden" logic (e.g.: reflection, prototype manipulation) unless explicitly instructed to by the user. Instead, use explicit and idiomatic language features (e.g.: type guards, explicit class instantiation, or object spread) that maintain structural integrity and type safety.
- **Design Patterns:** Prioritize explicit composition and delegation (e.g.: wrapper classes, proxies, or factory functions) over complex inheritance or prototype-based cloning. When extending or modifying existing classes, prefer patterns that are easily traceable and type-safe.
- **Comments:** Add code comments sparingly. Focus on *why* something is done, especially for complex logic, rather than *what* is done. Only add high-value comments if necessary for clarity or if requested by the user. Do not edit comments that are separate from the code you are changing. *NEVER* talk to the user or describe your changes through comments.
- **Proactiveness:** Fulfill the user's request thoroughly. When adding features or fixing bugs, this includes adding tests to ensure quality. Consider all created files, especially tests, to be permanent artifacts unless the user says otherwise.
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
@@ -3247,6 +3276,8 @@ exports[`Core System Prompt (prompts.ts) > should render hierarchical memory wit
- **Libraries/Frameworks:** NEVER assume a library/framework is available or appropriate. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', 'build.gradle', etc., or observe neighboring files) before employing it.
- **Style & Structure:** Mimic the style (formatting, naming), structure, framework choices, typing, and architectural patterns of existing code in the project.
- **Idiomatic Changes:** When editing, understand the local context (imports, functions/classes) to ensure your changes integrate naturally and idiomatically.
- **Types, Warnings & Linters:** NEVER use hacks like disabling or suppressing warnings, bypassing the type system (e.g.: casts in TypeScript), or employing "hidden" logic (e.g.: reflection, prototype manipulation) unless explicitly instructed to by the user. Instead, use explicit and idiomatic language features (e.g.: type guards, explicit class instantiation, or object spread) that maintain structural integrity and type safety.
- **Design Patterns:** Prioritize explicit composition and delegation (e.g.: wrapper classes, proxies, or factory functions) over complex inheritance or prototype-based cloning. When extending or modifying existing classes, prefer patterns that are easily traceable and type-safe.
- **Comments:** Add code comments sparingly. Focus on *why* something is done, especially for complex logic, rather than *what* is done. Only add high-value comments if necessary for clarity or if requested by the user. Do not edit comments that are separate from the code you are changing. *NEVER* talk to the user or describe your changes through comments.
- **Proactiveness:** Fulfill the user's request thoroughly. When adding features or fixing bugs, this includes adding tests to ensure quality. Consider all created files, especially tests, to be permanent artifacts unless the user says otherwise.
- **Conflict Resolution:** Instructions are provided in hierarchical context tags: \`<global_context>\`, \`<extension_context>\`, and \`<project_context>\`. In case of contradictory instructions, follow this priority: \`<project_context>\` (highest) > \`<extension_context>\` > \`<global_context>\` (lowest).
@@ -3408,7 +3439,8 @@ Use the following guidelines to optimize your search and read patterns.
## Engineering Standards
- **Contextual Precedence:** Instructions found in \`GEMINI.md\` files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Conventions & Style:** Rigorously adhere to existing workspace conventions, architectural patterns, and style (naming, formatting, typing, commenting). During the research phase, analyze surrounding files, tests, and configuration to ensure your changes are seamless, idiomatic, and consistent with the local context. Never compromise idiomatic quality or completeness (e.g., proper declarations, type safety, documentation) to minimize tool calls; all supporting changes required by local conventions are part of a surgical update.
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings or bypassing the type system (i.e.: casts in TypeScript) unless explicitly instructed to by the user. Instead, use idiomatic language features (e.g.: type guard functions).
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings, bypassing the type system (e.g.: casts in TypeScript), or employing "hidden" logic (e.g.: reflection, prototype manipulation) unless explicitly instructed to by the user. Instead, use explicit and idiomatic language features (e.g.: type guards, explicit class instantiation, or object spread) that maintain structural integrity and type safety.
- **Design Patterns:** Prioritize explicit composition and delegation (e.g.: wrapper classes, proxies, or factory functions) over complex inheritance or prototype-based cloning. When extending or modifying existing classes, prefer patterns that are easily traceable and type-safe.
- **Libraries/Frameworks:** NEVER assume a library/framework is available. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', etc.) before employing it.
- **Technical Integrity:** You are responsible for the entire lifecycle: implementation, testing, and validation. Within the scope of your changes, prioritize readability and long-term maintainability by consolidating logic into clean abstractions rather than threading state across unrelated layers. Align strictly with the requested architectural direction, ensuring the final implementation is focused and free of redundant "just-in-case" alternatives. Validation is not merely running tests; it is the exhaustive process of ensuring that every aspect of your change—behavioral, structural, and stylistic—is correct and fully compatible with the broader project. For bug fixes, you must empirically reproduce the failure with a new test case or reproduction script before applying the fix.
- **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a corresponding Directive is issued. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. For Directives, only clarify if critically underspecified; otherwise, work autonomously. You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction.
@@ -3566,7 +3598,8 @@ Use the following guidelines to optimize your search and read patterns.
## Engineering Standards
- **Contextual Precedence:** Instructions found in \`GEMINI.md\` files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Conventions & Style:** Rigorously adhere to existing workspace conventions, architectural patterns, and style (naming, formatting, typing, commenting). During the research phase, analyze surrounding files, tests, and configuration to ensure your changes are seamless, idiomatic, and consistent with the local context. Never compromise idiomatic quality or completeness (e.g., proper declarations, type safety, documentation) to minimize tool calls; all supporting changes required by local conventions are part of a surgical update.
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings or bypassing the type system (i.e.: casts in TypeScript) unless explicitly instructed to by the user. Instead, use idiomatic language features (e.g.: type guard functions).
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings, bypassing the type system (e.g.: casts in TypeScript), or employing "hidden" logic (e.g.: reflection, prototype manipulation) unless explicitly instructed to by the user. Instead, use explicit and idiomatic language features (e.g.: type guards, explicit class instantiation, or object spread) that maintain structural integrity and type safety.
- **Design Patterns:** Prioritize explicit composition and delegation (e.g.: wrapper classes, proxies, or factory functions) over complex inheritance or prototype-based cloning. When extending or modifying existing classes, prefer patterns that are easily traceable and type-safe.
- **Libraries/Frameworks:** NEVER assume a library/framework is available. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', etc.) before employing it.
- **Technical Integrity:** You are responsible for the entire lifecycle: implementation, testing, and validation. Within the scope of your changes, prioritize readability and long-term maintainability by consolidating logic into clean abstractions rather than threading state across unrelated layers. Align strictly with the requested architectural direction, ensuring the final implementation is focused and free of redundant "just-in-case" alternatives. Validation is not merely running tests; it is the exhaustive process of ensuring that every aspect of your change—behavioral, structural, and stylistic—is correct and fully compatible with the broader project. For bug fixes, you must empirically reproduce the failure with a new test case or reproduction script before applying the fix.
- **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a corresponding Directive is issued. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. For Directives, only clarify if critically underspecified; otherwise, work autonomously. You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction.
@@ -3691,6 +3724,8 @@ exports[`Core System Prompt (prompts.ts) > should return the interactive avoidan
- **Libraries/Frameworks:** NEVER assume a library/framework is available or appropriate. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', 'build.gradle', etc., or observe neighboring files) before employing it.
- **Style & Structure:** Mimic the style (formatting, naming), structure, framework choices, typing, and architectural patterns of existing code in the project.
- **Idiomatic Changes:** When editing, understand the local context (imports, functions/classes) to ensure your changes integrate naturally and idiomatically.
- **Types, Warnings & Linters:** NEVER use hacks like disabling or suppressing warnings, bypassing the type system (e.g.: casts in TypeScript), or employing "hidden" logic (e.g.: reflection, prototype manipulation) unless explicitly instructed to by the user. Instead, use explicit and idiomatic language features (e.g.: type guards, explicit class instantiation, or object spread) that maintain structural integrity and type safety.
- **Design Patterns:** Prioritize explicit composition and delegation (e.g.: wrapper classes, proxies, or factory functions) over complex inheritance or prototype-based cloning. When extending or modifying existing classes, prefer patterns that are easily traceable and type-safe.
- **Comments:** Add code comments sparingly. Focus on *why* something is done, especially for complex logic, rather than *what* is done. Only add high-value comments if necessary for clarity or if requested by the user. Do not edit comments that are separate from the code you are changing. *NEVER* talk to the user or describe your changes through comments.
- **Proactiveness:** Fulfill the user's request thoroughly. When adding features or fixing bugs, this includes adding tests to ensure quality. Consider all created files, especially tests, to be permanent artifacts unless the user says otherwise.
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
@@ -3836,7 +3871,8 @@ Use the following guidelines to optimize your search and read patterns.
## Engineering Standards
- **Contextual Precedence:** Instructions found in \`GEMINI.md\` files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Conventions & Style:** Rigorously adhere to existing workspace conventions, architectural patterns, and style (naming, formatting, typing, commenting). During the research phase, analyze surrounding files, tests, and configuration to ensure your changes are seamless, idiomatic, and consistent with the local context. Never compromise idiomatic quality or completeness (e.g., proper declarations, type safety, documentation) to minimize tool calls; all supporting changes required by local conventions are part of a surgical update.
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings or bypassing the type system (i.e.: casts in TypeScript) unless explicitly instructed to by the user. Instead, use idiomatic language features (e.g.: type guard functions).
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings, bypassing the type system (e.g.: casts in TypeScript), or employing "hidden" logic (e.g.: reflection, prototype manipulation) unless explicitly instructed to by the user. Instead, use explicit and idiomatic language features (e.g.: type guards, explicit class instantiation, or object spread) that maintain structural integrity and type safety.
- **Design Patterns:** Prioritize explicit composition and delegation (e.g.: wrapper classes, proxies, or factory functions) over complex inheritance or prototype-based cloning. When extending or modifying existing classes, prefer patterns that are easily traceable and type-safe.
- **Libraries/Frameworks:** NEVER assume a library/framework is available. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', etc.) before employing it.
- **Technical Integrity:** You are responsible for the entire lifecycle: implementation, testing, and validation. Within the scope of your changes, prioritize readability and long-term maintainability by consolidating logic into clean abstractions rather than threading state across unrelated layers. Align strictly with the requested architectural direction, ensuring the final implementation is focused and free of redundant "just-in-case" alternatives. Validation is not merely running tests; it is the exhaustive process of ensuring that every aspect of your change—behavioral, structural, and stylistic—is correct and fully compatible with the broader project. For bug fixes, you must empirically reproduce the failure with a new test case or reproduction script before applying the fix.
- **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a corresponding Directive is issued. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. For Directives, only clarify if critically underspecified; otherwise, work autonomously. You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction.
@@ -3994,7 +4030,8 @@ Use the following guidelines to optimize your search and read patterns.
## Engineering Standards
- **Contextual Precedence:** Instructions found in \`GEMINI.md\` files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Conventions & Style:** Rigorously adhere to existing workspace conventions, architectural patterns, and style (naming, formatting, typing, commenting). During the research phase, analyze surrounding files, tests, and configuration to ensure your changes are seamless, idiomatic, and consistent with the local context. Never compromise idiomatic quality or completeness (e.g., proper declarations, type safety, documentation) to minimize tool calls; all supporting changes required by local conventions are part of a surgical update.
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings or bypassing the type system (i.e.: casts in TypeScript) unless explicitly instructed to by the user. Instead, use idiomatic language features (e.g.: type guard functions).
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings, bypassing the type system (e.g.: casts in TypeScript), or employing "hidden" logic (e.g.: reflection, prototype manipulation) unless explicitly instructed to by the user. Instead, use explicit and idiomatic language features (e.g.: type guards, explicit class instantiation, or object spread) that maintain structural integrity and type safety.
- **Design Patterns:** Prioritize explicit composition and delegation (e.g.: wrapper classes, proxies, or factory functions) over complex inheritance or prototype-based cloning. When extending or modifying existing classes, prefer patterns that are easily traceable and type-safe.
- **Libraries/Frameworks:** NEVER assume a library/framework is available. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', etc.) before employing it.
- **Technical Integrity:** You are responsible for the entire lifecycle: implementation, testing, and validation. Within the scope of your changes, prioritize readability and long-term maintainability by consolidating logic into clean abstractions rather than threading state across unrelated layers. Align strictly with the requested architectural direction, ensuring the final implementation is focused and free of redundant "just-in-case" alternatives. Validation is not merely running tests; it is the exhaustive process of ensuring that every aspect of your change—behavioral, structural, and stylistic—is correct and fully compatible with the broader project. For bug fixes, you must empirically reproduce the failure with a new test case or reproduction script before applying the fix.
- **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a corresponding Directive is issued. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. For Directives, only clarify if critically underspecified; otherwise, work autonomously. You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction.
@@ -4119,6 +4156,8 @@ exports[`Core System Prompt (prompts.ts) > should use legacy system prompt for n
- **Libraries/Frameworks:** NEVER assume a library/framework is available or appropriate. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', 'build.gradle', etc., or observe neighboring files) before employing it.
- **Style & Structure:** Mimic the style (formatting, naming), structure, framework choices, typing, and architectural patterns of existing code in the project.
- **Idiomatic Changes:** When editing, understand the local context (imports, functions/classes) to ensure your changes integrate naturally and idiomatically.
- **Types, Warnings & Linters:** NEVER use hacks like disabling or suppressing warnings, bypassing the type system (e.g.: casts in TypeScript), or employing "hidden" logic (e.g.: reflection, prototype manipulation) unless explicitly instructed to by the user. Instead, use explicit and idiomatic language features (e.g.: type guards, explicit class instantiation, or object spread) that maintain structural integrity and type safety.
- **Design Patterns:** Prioritize explicit composition and delegation (e.g.: wrapper classes, proxies, or factory functions) over complex inheritance or prototype-based cloning. When extending or modifying existing classes, prefer patterns that are easily traceable and type-safe.
- **Comments:** Add code comments sparingly. Focus on *why* something is done, especially for complex logic, rather than *what* is done. Only add high-value comments if necessary for clarity or if requested by the user. Do not edit comments that are separate from the code you are changing. *NEVER* talk to the user or describe your changes through comments.
- **Proactiveness:** Fulfill the user's request thoroughly. When adding features or fixing bugs, this includes adding tests to ensure quality. Consider all created files, especially tests, to be permanent artifacts unless the user says otherwise.
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
+11
View File
@@ -0,0 +1,11 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
export class SecurityError extends Error {
constructor(message: string) {
super(message);
this.name = 'SecurityError';
}
}
+3 -1
View File
@@ -90,9 +90,10 @@ describe('Core System Prompt (prompts.ts)', () => {
getToolRegistry: vi.fn().mockReturnValue(mockRegistry),
getEnableShellOutputEfficiency: vi.fn().mockReturnValue(true),
getSandboxEnabled: vi.fn().mockReturnValue(false),
getPlansDir: vi.fn().mockReturnValue('/tmp/plans'),
storage: {
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project-temp'),
getPlansDir: vi.fn().mockReturnValue('/tmp/project-temp/plans'),
getPlansDir: vi.fn().mockReturnValue('/tmp/plans'),
getProjectTempTrackerDir: vi
.fn()
.mockReturnValue('/mock/.gemini/tmp/session/tracker'),
@@ -423,6 +424,7 @@ describe('Core System Prompt (prompts.ts)', () => {
getToolRegistry: vi.fn().mockReturnValue(mockToolRegistry),
getEnableShellOutputEfficiency: vi.fn().mockReturnValue(true),
getSandboxEnabled: vi.fn().mockReturnValue(false),
getPlansDir: vi.fn().mockReturnValue('/tmp/plans'),
storage: {
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project-temp'),
},
@@ -121,6 +121,56 @@ describe('HookTranslator', () => {
},
]);
});
it('should apply model override when hook returns only model field', () => {
const baseRequest: GenerateContentParameters = {
model: 'gemini-2.5-flash-lite',
contents: [
{
role: 'user',
parts: [{ text: 'Hello' }],
},
],
} as unknown as GenerateContentParameters;
// Simulate a hook that only overrides the model — no messages field
const hookRequest = {
model: 'gemini-2.5-flash',
} as unknown as LLMRequest;
const sdkRequest = translator.fromHookLLMRequest(
hookRequest,
baseRequest,
);
// Model should be overridden
expect(sdkRequest.model).toBe('gemini-2.5-flash');
// Original conversation contents should be preserved
expect(sdkRequest.contents).toEqual(baseRequest.contents);
});
it('should preserve base request contents when hook messages is undefined', () => {
const baseRequest: GenerateContentParameters = {
model: 'gemini-1.5-flash',
contents: [
{ role: 'user', parts: [{ text: 'original message' }] },
{ role: 'model', parts: [{ text: 'original reply' }] },
],
} as unknown as GenerateContentParameters;
const hookRequest = {
model: 'gemini-1.5-pro',
// messages intentionally omitted
} as unknown as LLMRequest;
const sdkRequest = translator.fromHookLLMRequest(
hookRequest,
baseRequest,
);
expect(sdkRequest.model).toBe('gemini-1.5-pro');
expect(sdkRequest.contents).toEqual(baseRequest.contents);
});
});
describe('LLM Response Translation', () => {
+21 -14
View File
@@ -225,23 +225,30 @@ export class HookTranslatorGenAIv1 extends HookTranslator {
hookRequest: LLMRequest,
baseRequest?: GenerateContentParameters,
): GenerateContentParameters {
// Convert hook messages back to SDK Content format
const contents = hookRequest.messages.map((message) => ({
role: message.role === 'model' ? 'model' : message.role,
parts: [
{
text:
typeof message.content === 'string'
? message.content
: String(message.content),
},
],
}));
// Convert hook messages back to SDK Content format.
// If the hook returned a partial request without messages (e.g. only
// overriding `model`), fall back to the base request's contents so the
// conversation is preserved.
const contents = hookRequest.messages
? hookRequest.messages.map((message) => ({
role: message.role === 'model' ? 'model' : message.role,
parts: [
{
text:
typeof message.content === 'string'
? message.content
: String(message.content),
},
],
}))
: (baseRequest?.contents ?? []);
// Build the result with proper typing
// Build the result with proper typing.
// Use nullish coalescing so a hook that only sets `model` still works --
// fall back to the base request's model rather than overwriting with undefined.
const result: GenerateContentParameters = {
...baseRequest,
model: hookRequest.model,
model: hookRequest.model ?? baseRequest?.model ?? '',
contents,
};
+2 -1
View File
@@ -276,7 +276,8 @@ export * from './voice/responseFormatter.js';
// Export types from @google/genai
export type { Content, Part, FunctionCall } from '@google/genai';
// Export context types and profiles
export * from './context/types.js';
export * from './context/profiles.js';
export * from './core/errors.js';
+2 -1
View File
@@ -107,7 +107,8 @@ toolName = [
"activate_skill",
"codebase_investigator",
"cli_help",
"get_internal_docs"
"get_internal_docs",
"complete_task"
]
decision = "allow"
priority = 70
@@ -2,7 +2,7 @@
network = false
readonly = true
approvedTools = []
allowOverrides = false
allowOverrides = true
[modes.default]
network = false
@@ -17,4 +17,3 @@ approvedTools = ['sed', 'grep', 'awk', 'perl', 'cat', 'echo', 'Add-Content', 'Se
allowOverrides = true
[commands]
@@ -64,7 +64,7 @@ export class SandboxPolicyManager {
network: false,
readonly: true,
approvedTools: [],
allowOverrides: false,
allowOverrides: true,
},
default: {
network: false,
@@ -61,6 +61,7 @@ describe('PromptProvider', () => {
topicState: new TopicState(),
getEnableShellOutputEfficiency: vi.fn().mockReturnValue(true),
getSandboxEnabled: vi.fn().mockReturnValue(false),
getPlansDir: vi.fn().mockReturnValue('/tmp/project-temp/plans'),
storage: {
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project-temp'),
getPlansDir: vi.fn().mockReturnValue('/tmp/project-temp/plans'),
+1 -1
View File
@@ -188,7 +188,7 @@ export class PromptProvider {
() => ({
interactive: interactiveMode,
planModeToolsList,
plansDir: context.config.storage.getPlansDir(),
plansDir: context.config.getPlansDir(),
approvedPlanPath: context.config.getApprovedPlanPath(),
}),
isPlanMode,
+5 -1
View File
@@ -177,6 +177,8 @@ export function renderCoreMandates(options?: CoreMandatesOptions): string {
- **Libraries/Frameworks:** NEVER assume a library/framework is available or appropriate. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', 'build.gradle', etc., or observe neighboring files) before employing it.
- **Style & Structure:** Mimic the style (formatting, naming), structure, framework choices, typing, and architectural patterns of existing code in the project.
- **Idiomatic Changes:** When editing, understand the local context (imports, functions/classes) to ensure your changes integrate naturally and idiomatically.
- **Types, Warnings & Linters:** NEVER use hacks like disabling or suppressing warnings, bypassing the type system (e.g.: casts in TypeScript), or employing "hidden" logic (e.g.: reflection, prototype manipulation) unless explicitly instructed to by the user. Instead, use explicit and idiomatic language features (e.g.: type guards, explicit class instantiation, or object spread) that maintain structural integrity and type safety.
- **Design Patterns:** Prioritize explicit composition and delegation (e.g.: wrapper classes, proxies, or factory functions) over complex inheritance or prototype-based cloning. When extending or modifying existing classes, prefer patterns that are easily traceable and type-safe.
- **Comments:** Add code comments sparingly. Focus on *why* something is done, especially for complex logic, rather than *what* is done. Only add high-value comments if necessary for clarity or if requested by the user. Do not edit comments that are separate from the code you are changing. *NEVER* talk to the user or describe your changes through comments.
- **Proactiveness:** Fulfill the user's request thoroughly. When adding features or fixing bugs, this includes adding tests to ensure quality. Consider all created files, especially tests, to be permanent artifacts unless the user says otherwise.${mandateConflictResolution(options.hasHierarchicalMemory)}
- **User Hints:** During execution, the user may provide real-time hints (marked as "User hint:" or "User hints:"). Treat these as high-priority but scope-preserving course corrections: apply the minimal plan change needed, keep unaffected user tasks active, and never cancel/skip tasks unless cancellation is explicit for those tasks. Hints may add new tasks, modify one or more tasks, cancel specific tasks, or provide extra context only. If scope is ambiguous, ask for clarification before dropping work.
@@ -518,7 +520,9 @@ function mandateTopicUpdateModel(): string {
## Topic Updates
As you work, the user follows along by reading topic updates that you publish with ${UPDATE_TOPIC_TOOL_NAME}. Keep them informed by doing the following:
- Usage Exception: NEVER use ${UPDATE_TOPIC_TOOL_NAME} for answering questions, providing explanations, or performing isolated lookup tasks (e.g. reading a single file, running a quick search, or checking a version). It is STRICTLY for orchestrating multi-step codebase modifications or complex investigations involving 3 or more tool calls.\n- Always call ${UPDATE_TOPIC_TOOL_NAME} in your first and last turn for tasks that require 3 or more tool calls. The final turn should always recap what was done.
- Usage Exception: NEVER use ${UPDATE_TOPIC_TOOL_NAME} for answering questions, providing explanations, or performing isolated lookup tasks (e.g. reading a single file, running a quick search, or checking a version). It is STRICTLY for orchestrating multi-step codebase modifications or complex investigations involving 3 or more tool calls.
- Always call ${UPDATE_TOPIC_TOOL_NAME} in your first turn.
- For tasks taking multiple turns, also call ${UPDATE_TOPIC_TOOL_NAME} in your last turn to recap what was done.
- Each topic update should give a concise description of what you are doing for the next few turns in the \`${TOPIC_PARAM_SUMMARY}\` parameter.
- Provide topic updates whenever you change "topics". A topic is typically a discrete subgoal and will be every 3 to 10 turns. Do not use ${UPDATE_TOPIC_TOOL_NAME} on every turn.
- The typical complex user message should call ${UPDATE_TOPIC_TOOL_NAME} 3 or more times. Each corresponds to a distinct phase of the task, such as "Researching X", "Researching Y", "Implementing Z with X", and "Testing Z".
+5 -2
View File
@@ -229,7 +229,8 @@ Use the following guidelines to optimize your search and read patterns.
## Engineering Standards
- **Contextual Precedence:** Instructions found in ${formattedFilenames} files are foundational mandates. They take absolute precedence over the general workflows and tool defaults described in this system prompt.
- **Conventions & Style:** Rigorously adhere to existing workspace conventions, architectural patterns, and style (naming, formatting, typing, commenting). During the research phase, analyze surrounding files, tests, and configuration to ensure your changes are seamless, idiomatic, and consistent with the local context. Never compromise idiomatic quality or completeness (e.g., proper declarations, type safety, documentation) to minimize tool calls; all supporting changes required by local conventions are part of a surgical update.
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings or bypassing the type system (i.e.: casts in TypeScript) unless explicitly instructed to by the user. Instead, use idiomatic language features (e.g.: type guard functions).
- **Types, warnings and linters:** NEVER use hacks like disabling or suppressing warnings, bypassing the type system (e.g.: casts in TypeScript), or employing "hidden" logic (e.g.: reflection, prototype manipulation) unless explicitly instructed to by the user. Instead, use explicit and idiomatic language features (e.g.: type guards, explicit class instantiation, or object spread) that maintain structural integrity and type safety.
- **Design Patterns:** Prioritize explicit composition and delegation (e.g.: wrapper classes, proxies, or factory functions) over complex inheritance or prototype-based cloning. When extending or modifying existing classes, prefer patterns that are easily traceable and type-safe.
- **Libraries/Frameworks:** NEVER assume a library/framework is available. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', etc.) before employing it.
- **Technical Integrity:** You are responsible for the entire lifecycle: implementation, testing, and validation. Within the scope of your changes, prioritize readability and long-term maintainability by consolidating logic into clean abstractions rather than threading state across unrelated layers. Align strictly with the requested architectural direction, ensuring the final implementation is focused and free of redundant "just-in-case" alternatives. Validation is not merely running tests; it is the exhaustive process of ensuring that every aspect of your changebehavioral, structural, and stylisticis correct and fully compatible with the broader project. For bug fixes, you must empirically reproduce the failure with a new test case or reproduction script before applying the fix.
- **Expertise & Intent Alignment:** Provide proactive technical opinions grounded in research while strictly adhering to the user's intended workflow. Distinguish between **Directives** (unambiguous requests for action or implementation) and **Inquiries** (requests for analysis, advice, or observations). Assume all requests are Inquiries unless they contain an explicit instruction to perform a task. For Inquiries, your scope is strictly limited to research and analysis; you may propose a solution or strategy, but you MUST NOT modify files until a corresponding Directive is issued. Do not initiate implementation based on observations of bugs or statements of fact. Once an Inquiry is resolved, or while waiting for a Directive, stop and wait for the next user instruction. ${options.interactive ? 'For Directives, only clarify if critically underspecified; otherwise, work autonomously.' : 'For Directives, you must work autonomously as no further user input is available.'} You should only seek user intervention if you have exhausted all possible routes or if a proposed solution would take the workspace in a significantly different architectural direction.
@@ -629,7 +630,9 @@ function mandateTopicUpdateModel(): string {
## Topic Updates
As you work, the user follows along by reading topic updates that you publish with ${UPDATE_TOPIC_TOOL_NAME}. Keep them informed by doing the following:
- Usage Exception: NEVER use ${UPDATE_TOPIC_TOOL_NAME} for answering questions, providing explanations, or performing isolated lookup tasks (e.g. reading a single file, running a quick search, or checking a version). It is STRICTLY for orchestrating multi-step codebase modifications or complex investigations involving 3 or more tool calls.\n- Always call ${UPDATE_TOPIC_TOOL_NAME} in your first and last turn for tasks that require 3 or more tool calls. The final turn should always recap what was done.
- Usage Exception: NEVER use ${UPDATE_TOPIC_TOOL_NAME} for answering questions, providing explanations, or performing isolated lookup tasks (e.g. reading a single file, running a quick search, or checking a version). It is STRICTLY for orchestrating multi-step codebase modifications or complex investigations involving 3 or more tool calls.
- Always call ${UPDATE_TOPIC_TOOL_NAME} in your first turn.
- For tasks taking multiple turns, also call ${UPDATE_TOPIC_TOOL_NAME} in your last turn to recap what was done.
- Each topic update should give a concise description of what you are doing for the next few turns in the \`${TOPIC_PARAM_SUMMARY}\` parameter.
- Provide topic updates whenever you change "topics". A topic is typically a discrete subgoal and will be every 3 to 10 turns. Do not use ${UPDATE_TOPIC_TOOL_NAME} on every turn.
- The typical complex user message should call ${UPDATE_TOPIC_TOOL_NAME} 3 or more times. Each corresponds to a distinct phase of the task, such as "Researching X", "Researching Y", "Implementing Z with X", and "Testing Z".
@@ -105,7 +105,8 @@ function ensureSandboxAvailable(): boolean {
if (platform === 'win32') {
// Windows sandboxing relies on icacls, which is a core system utility and
// always available.
return true;
// TODO: reenable once flakiness is addressed
return false;
}
if (platform === 'darwin') {
@@ -1692,4 +1692,187 @@ describe('ClearcutLogger', () => {
]);
});
});
describe('logBrowserAgentConnectionEvent', () => {
it('logs a successful connection event', () => {
const { logger } = setup();
logger?.logBrowserAgentConnectionEvent({
session_mode: 'isolated',
headless: true,
success: true,
duration_ms: 1500,
});
const events = getEvents(logger!);
expect(events.length).toBe(1);
expect(events[0]).toHaveEventName(EventNames.BROWSER_AGENT_CONNECTION);
expect(events[0]).toHaveMetadataValue([
EventMetadataKey.GEMINI_CLI_BROWSER_AGENT_SESSION_MODE,
'isolated',
]);
expect(events[0]).toHaveMetadataValue([
EventMetadataKey.GEMINI_CLI_BROWSER_AGENT_HEADLESS,
'true',
]);
expect(events[0]).toHaveMetadataValue([
EventMetadataKey.GEMINI_CLI_BROWSER_AGENT_SUCCESS,
'true',
]);
expect(events[0]).toHaveMetadataValue([
EventMetadataKey.GEMINI_CLI_BROWSER_AGENT_DURATION_MS,
'1500',
]);
});
it('logs a failed connection event with error_type', () => {
const { logger } = setup();
logger?.logBrowserAgentConnectionEvent({
session_mode: 'persistent',
headless: false,
success: false,
duration_ms: 30000,
error_type: 'timeout',
});
const events = getEvents(logger!);
expect(events.length).toBe(1);
expect(events[0]).toHaveMetadataValue([
EventMetadataKey.GEMINI_CLI_BROWSER_AGENT_SUCCESS,
'false',
]);
expect(events[0]).toHaveMetadataValue([
EventMetadataKey.GEMINI_CLI_BROWSER_AGENT_ERROR_TYPE,
'timeout',
]);
});
it('logs tool_count when provided', () => {
const { logger } = setup();
logger?.logBrowserAgentConnectionEvent({
session_mode: 'existing',
headless: true,
success: true,
duration_ms: 800,
tool_count: 12,
});
const events = getEvents(logger!);
expect(events[0]).toHaveMetadataValue([
EventMetadataKey.GEMINI_CLI_BROWSER_AGENT_TOOL_COUNT,
'12',
]);
});
});
describe('logBrowserAgentVisionStatusEvent', () => {
it('logs vision enabled', () => {
const { logger } = setup();
logger?.logBrowserAgentVisionStatusEvent({ enabled: true });
const events = getEvents(logger!);
expect(events.length).toBe(1);
expect(events[0]).toHaveEventName(EventNames.BROWSER_AGENT_VISION_STATUS);
expect(events[0]).toHaveMetadataValue([
EventMetadataKey.GEMINI_CLI_BROWSER_AGENT_VISION_ENABLED,
'true',
]);
});
it('logs vision disabled with reason', () => {
const { logger } = setup();
logger?.logBrowserAgentVisionStatusEvent({
enabled: false,
disabled_reason: 'no_visual_model',
});
const events = getEvents(logger!);
expect(events[0]).toHaveMetadataValue([
EventMetadataKey.GEMINI_CLI_BROWSER_AGENT_VISION_ENABLED,
'false',
]);
expect(events[0]).toHaveMetadataValue([
EventMetadataKey.GEMINI_CLI_BROWSER_AGENT_VISION_DISABLED_REASON,
'no_visual_model',
]);
});
});
describe('logBrowserAgentTaskOutcomeEvent', () => {
it('logs a task outcome event with all attributes', () => {
const { logger } = setup();
logger?.logBrowserAgentTaskOutcomeEvent({
success: true,
session_mode: 'isolated',
vision_enabled: true,
headless: true,
duration_ms: 5000,
});
const events = getEvents(logger!);
expect(events.length).toBe(1);
expect(events[0]).toHaveEventName(EventNames.BROWSER_AGENT_TASK_OUTCOME);
expect(events[0]).toHaveMetadataValue([
EventMetadataKey.GEMINI_CLI_BROWSER_AGENT_SUCCESS,
'true',
]);
expect(events[0]).toHaveMetadataValue([
EventMetadataKey.GEMINI_CLI_BROWSER_AGENT_SESSION_MODE,
'isolated',
]);
expect(events[0]).toHaveMetadataValue([
EventMetadataKey.GEMINI_CLI_BROWSER_AGENT_VISION_ENABLED,
'true',
]);
expect(events[0]).toHaveMetadataValue([
EventMetadataKey.GEMINI_CLI_BROWSER_AGENT_HEADLESS,
'true',
]);
expect(events[0]).toHaveMetadataValue([
EventMetadataKey.GEMINI_CLI_BROWSER_AGENT_DURATION_MS,
'5000',
]);
});
});
describe('logBrowserAgentCleanupEvent', () => {
it('logs a cleanup event with all attributes', () => {
const { logger } = setup();
logger?.logBrowserAgentCleanupEvent({
session_mode: 'isolated',
success: true,
duration_ms: 200,
});
const events = getEvents(logger!);
expect(events.length).toBe(1);
expect(events[0]).toHaveEventName(EventNames.BROWSER_AGENT_CLEANUP);
expect(events[0]).toHaveMetadataValue([
EventMetadataKey.GEMINI_CLI_BROWSER_AGENT_SESSION_MODE,
'isolated',
]);
expect(events[0]).toHaveMetadataValue([
EventMetadataKey.GEMINI_CLI_BROWSER_AGENT_SUCCESS,
'true',
]);
expect(events[0]).toHaveMetadataValue([
EventMetadataKey.GEMINI_CLI_BROWSER_AGENT_DURATION_MS,
'200',
]);
});
it('logs a failed cleanup event', () => {
const { logger } = setup();
logger?.logBrowserAgentCleanupEvent({
session_mode: 'persistent',
success: false,
duration_ms: 5000,
});
const events = getEvents(logger!);
expect(events[0]).toHaveMetadataValue([
EventMetadataKey.GEMINI_CLI_BROWSER_AGENT_SUCCESS,
'false',
]);
});
});
});
@@ -135,6 +135,10 @@ export enum EventNames {
OVERAGE_OPTION_SELECTED = 'overage_option_selected',
EMPTY_WALLET_MENU_SHOWN = 'empty_wallet_menu_shown',
CREDIT_PURCHASE_CLICK = 'credit_purchase_click',
BROWSER_AGENT_CONNECTION = 'browser_agent_connection',
BROWSER_AGENT_VISION_STATUS = 'browser_agent_vision_status',
BROWSER_AGENT_TASK_OUTCOME = 'browser_agent_task_outcome',
BROWSER_AGENT_CLEANUP = 'browser_agent_cleanup',
}
export interface LogResponse {
@@ -1935,6 +1939,146 @@ export class ClearcutLogger {
this.flushIfNeeded();
}
// ==========================================================================
// Browser Agent Events
// ==========================================================================
logBrowserAgentConnectionEvent(attrs: {
session_mode: string;
headless: boolean;
success: boolean;
duration_ms: number;
error_type?: string;
tool_count?: number;
}): void {
const data: EventValue[] = [
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_BROWSER_AGENT_SESSION_MODE,
value: attrs.session_mode,
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_BROWSER_AGENT_HEADLESS,
value: attrs.headless.toString(),
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_BROWSER_AGENT_SUCCESS,
value: attrs.success.toString(),
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_BROWSER_AGENT_DURATION_MS,
value: attrs.duration_ms.toString(),
},
];
if (attrs.error_type) {
data.push({
gemini_cli_key: EventMetadataKey.GEMINI_CLI_BROWSER_AGENT_ERROR_TYPE,
value: attrs.error_type,
});
}
if (attrs.tool_count !== undefined) {
data.push({
gemini_cli_key: EventMetadataKey.GEMINI_CLI_BROWSER_AGENT_TOOL_COUNT,
value: attrs.tool_count.toString(),
});
}
this.enqueueLogEvent(
this.createLogEvent(EventNames.BROWSER_AGENT_CONNECTION, data),
);
this.flushIfNeeded();
}
logBrowserAgentVisionStatusEvent(attrs: {
enabled: boolean;
disabled_reason?: string;
}): void {
const data: EventValue[] = [
{
gemini_cli_key:
EventMetadataKey.GEMINI_CLI_BROWSER_AGENT_VISION_ENABLED,
value: attrs.enabled.toString(),
},
];
if (attrs.disabled_reason) {
data.push({
gemini_cli_key:
EventMetadataKey.GEMINI_CLI_BROWSER_AGENT_VISION_DISABLED_REASON,
value: attrs.disabled_reason,
});
}
this.enqueueLogEvent(
this.createLogEvent(EventNames.BROWSER_AGENT_VISION_STATUS, data),
);
this.flushIfNeeded();
}
logBrowserAgentTaskOutcomeEvent(attrs: {
success: boolean;
session_mode: string;
vision_enabled: boolean;
headless: boolean;
duration_ms: number;
}): void {
const data: EventValue[] = [
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_BROWSER_AGENT_SUCCESS,
value: attrs.success.toString(),
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_BROWSER_AGENT_SESSION_MODE,
value: attrs.session_mode,
},
{
gemini_cli_key:
EventMetadataKey.GEMINI_CLI_BROWSER_AGENT_VISION_ENABLED,
value: attrs.vision_enabled.toString(),
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_BROWSER_AGENT_HEADLESS,
value: attrs.headless.toString(),
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_BROWSER_AGENT_DURATION_MS,
value: attrs.duration_ms.toString(),
},
];
this.enqueueLogEvent(
this.createLogEvent(EventNames.BROWSER_AGENT_TASK_OUTCOME, data),
);
this.flushIfNeeded();
}
logBrowserAgentCleanupEvent(attrs: {
session_mode: string;
success: boolean;
duration_ms: number;
}): void {
const data: EventValue[] = [
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_BROWSER_AGENT_SESSION_MODE,
value: attrs.session_mode,
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_BROWSER_AGENT_SUCCESS,
value: attrs.success.toString(),
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_BROWSER_AGENT_DURATION_MS,
value: attrs.duration_ms.toString(),
},
];
this.enqueueLogEvent(
this.createLogEvent(EventNames.BROWSER_AGENT_CLEANUP, data),
);
this.flushIfNeeded();
}
/**
* Adds default fields to data, and returns a new data array. This fields
* should exist on all log events.
@@ -7,7 +7,7 @@
// Defines valid event metadata keys for Clearcut logging.
export enum EventMetadataKey {
// Deleted enums: 24
// Next ID: 195
// Next ID: 203
GEMINI_CLI_KEY_UNKNOWN = 0,
@@ -725,4 +725,32 @@ export enum EventMetadataKey {
// Logs the duration of the onboarding process in milliseconds.
GEMINI_CLI_ONBOARDING_DURATION_MS = 194,
// ==========================================================================
// Browser Agent Event Keys
// ==========================================================================
// Logs the browser agent session mode (persistent, isolated, existing).
GEMINI_CLI_BROWSER_AGENT_SESSION_MODE = 195,
// Logs whether the browser agent ran in headless mode.
GEMINI_CLI_BROWSER_AGENT_HEADLESS = 196,
// Logs whether the browser agent operation was successful.
GEMINI_CLI_BROWSER_AGENT_SUCCESS = 197,
// Logs the error type for a browser agent connection failure.
GEMINI_CLI_BROWSER_AGENT_ERROR_TYPE = 198,
// Logs the duration in milliseconds for a browser agent operation.
GEMINI_CLI_BROWSER_AGENT_DURATION_MS = 199,
// Logs whether vision mode was enabled for the browser agent.
GEMINI_CLI_BROWSER_AGENT_VISION_ENABLED = 200,
// Logs the reason vision mode was disabled for the browser agent.
GEMINI_CLI_BROWSER_AGENT_VISION_DISABLED_REASON = 201,
// Logs the number of tools discovered from the MCP server.
GEMINI_CLI_BROWSER_AGENT_TOOL_COUNT = 202,
}
+91
View File
@@ -83,6 +83,10 @@ import {
recordInvalidChunk,
recordOnboardingStart,
recordOnboardingSuccess,
recordBrowserAgentConnection,
recordBrowserAgentVisionStatus,
recordBrowserAgentTaskOutcome,
recordBrowserAgentCleanup,
} from './metrics.js';
import { bufferTelemetryEvent } from './sdk.js';
import { uiTelemetryService, type UiEvent } from './uiTelemetry.js';
@@ -939,3 +943,90 @@ export function logBillingEvent(
}
}
}
// ==========================================================================
// Browser Agent Events
// ==========================================================================
export function logBrowserAgentConnection(
config: Config,
durationMs: number,
attributes: {
session_mode: 'persistent' | 'isolated' | 'existing';
headless: boolean;
success: boolean;
error_type?:
| 'profile_locked'
| 'timeout'
| 'connection_refused'
| 'unknown';
tool_count?: number;
},
): void {
ClearcutLogger.getInstance(config)?.logBrowserAgentConnectionEvent({
session_mode: attributes.session_mode,
headless: attributes.headless,
success: attributes.success,
duration_ms: durationMs,
error_type: attributes.error_type,
tool_count: attributes.tool_count,
});
recordBrowserAgentConnection(config, durationMs, attributes);
}
export function logBrowserAgentVisionStatus(
config: Config,
attributes: {
enabled: boolean;
disabled_reason?:
| 'no_visual_model'
| 'missing_visual_tools'
| 'blocked_auth_type';
},
): void {
ClearcutLogger.getInstance(config)?.logBrowserAgentVisionStatusEvent({
enabled: attributes.enabled,
disabled_reason: attributes.disabled_reason,
});
recordBrowserAgentVisionStatus(config, attributes);
}
export function logBrowserAgentTaskOutcome(
config: Config,
attributes: {
success: boolean;
session_mode: 'persistent' | 'isolated' | 'existing';
vision_enabled: boolean;
headless: boolean;
duration_ms: number;
},
): void {
ClearcutLogger.getInstance(config)?.logBrowserAgentTaskOutcomeEvent({
success: attributes.success,
session_mode: attributes.session_mode,
vision_enabled: attributes.vision_enabled,
headless: attributes.headless,
duration_ms: attributes.duration_ms,
});
recordBrowserAgentTaskOutcome(config, attributes);
}
export function logBrowserAgentCleanup(
config: Config,
durationMs: number,
attributes: {
session_mode: 'persistent' | 'isolated' | 'existing';
success: boolean;
},
): void {
ClearcutLogger.getInstance(config)?.logBrowserAgentCleanupEvent({
session_mode: attributes.session_mode,
success: attributes.success,
duration_ms: durationMs,
});
recordBrowserAgentCleanup(config, durationMs, attributes);
}
@@ -1687,6 +1687,29 @@ describe('Telemetry Metrics', () => {
expect(mockCounterAddFn).not.toHaveBeenCalled();
});
it('records tool_count on success when provided', () => {
initializeMetricsModule(mockConfig);
mockCounterAddFn.mockClear();
mockHistogramRecordFn.mockClear();
recordBrowserAgentConnectionModule(mockConfig, 1200, {
session_mode: 'isolated',
headless: false,
success: true,
tool_count: 5,
});
expect(mockHistogramRecordFn).toHaveBeenCalledWith(1200, {
'session.id': 'test-session-id',
'installation.id': 'test-installation-id',
'user.email': 'test@example.com',
session_mode: 'isolated',
headless: false,
success: true,
tool_count: 5,
});
});
it('records connection duration and failure counter on error', () => {
initializeMetricsModule(mockConfig);
mockCounterAddFn.mockClear();
+2
View File
@@ -1624,6 +1624,7 @@ export function recordBrowserAgentConnection(
| 'timeout'
| 'connection_refused'
| 'unknown';
tool_count?: number;
},
): void {
if (!isMetricsInitialized) return;
@@ -1635,6 +1636,7 @@ export function recordBrowserAgentConnection(
session_mode: attributes.session_mode,
headless: attributes.headless,
success: attributes.success,
tool_count: attributes.tool_count,
});
if (!attributes.success && browserAgentConnectionFailureCounter) {
+3 -1
View File
@@ -132,9 +132,10 @@ describe('EditTool', () => {
getDisableLLMCorrection: vi.fn(() => true),
getExperiments: () => {},
isPlanMode: vi.fn(() => false),
getPlansDir: vi.fn().mockReturnValue('/tmp/project/plans'),
storage: {
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'),
getPlansDir: vi.fn().mockReturnValue('/tmp/plans'),
getPlansDir: vi.fn().mockReturnValue('/tmp/project/plans'),
},
isPathAllowed(this: Config, absolutePath: string): boolean {
const workspaceContext = this.getWorkspaceContext();
@@ -1314,6 +1315,7 @@ function doIt() {
fs.mkdirSync(plansDir);
vi.mocked(mockConfig.isPlanMode).mockReturnValue(true);
vi.mocked(mockConfig.getPlansDir).mockReturnValue(plansDir);
vi.mocked(mockConfig.storage.getPlansDir).mockReturnValue(plansDir);
const filePath = path.join(rootDir, 'test-file.txt');
+1 -4
View File
@@ -465,10 +465,7 @@ class EditToolInvocation
);
if (this.config.isPlanMode()) {
const safeFilename = path.basename(this.params.file_path);
this.resolvedPath = path.join(
this.config.storage.getPlansDir(),
safeFilename,
);
this.resolvedPath = path.join(this.config.getPlansDir(), safeFilename);
} else if (!path.isAbsolute(this.params.file_path)) {
const result = correctPath(this.params.file_path, this.config);
if (result.success) {
@@ -11,7 +11,6 @@ import type { Config } from '../config/config.js';
import type { MessageBus } from '../confirmation-bus/message-bus.js';
import { ToolConfirmationOutcome } from './tools.js';
import { ApprovalMode } from '../policy/types.js';
import fs from 'node:fs';
vi.mock('node:fs', async () => {
const actual = await vi.importActual<typeof import('node:fs')>('node:fs');
@@ -39,6 +38,7 @@ describe('EnterPlanModeTool', () => {
mockConfig = {
setApprovalMode: vi.fn(),
getPlansDir: vi.fn().mockReturnValue('/mock/plans/dir'),
storage: {
getPlansDir: vi.fn().mockReturnValue('/mock/plans/dir'),
} as unknown as Config['storage'],
@@ -119,7 +119,6 @@ describe('EnterPlanModeTool', () => {
describe('execute', () => {
it('should set approval mode to PLAN and return message', async () => {
const invocation = tool.build({});
vi.mocked(fs.existsSync).mockReturnValue(true);
const result = await invocation.execute(new AbortController().signal);
@@ -130,21 +129,9 @@ describe('EnterPlanModeTool', () => {
expect(result.returnDisplay).toBe('Switching to Plan mode');
});
it('should create plans directory if it does not exist', async () => {
const invocation = tool.build({});
vi.mocked(fs.existsSync).mockReturnValue(false);
await invocation.execute(new AbortController().signal);
expect(fs.mkdirSync).toHaveBeenCalledWith('/mock/plans/dir', {
recursive: true,
});
});
it('should include optional reason in output display but not in llmContent', async () => {
const reason = 'Design new database schema';
const invocation = tool.build({ reason });
vi.mocked(fs.existsSync).mockReturnValue(true);
const result = await invocation.execute(new AbortController().signal);
@@ -4,7 +4,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
import fs from 'node:fs';
import {
BaseDeclarativeTool,
BaseToolInvocation,
@@ -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 { debugLogger } from '../utils/debugLogger.js';
export interface EnterPlanModeParams {
reason?: string;
@@ -124,19 +122,6 @@ export class EnterPlanModeInvocation extends BaseToolInvocation<
this.config.setApprovalMode(ApprovalMode.PLAN);
// Ensure plans directory exists so that the agent can write the plan file.
// In sandboxed environments, the plans directory must exist on the host
// before it can be bound/allowed in the sandbox.
const plansDir = this.config.storage.getPlansDir();
if (!fs.existsSync(plansDir)) {
try {
fs.mkdirSync(plansDir, { recursive: true });
} catch (e) {
// Log error but don't fail; write_file will try again later
debugLogger.error(`Failed to create plans directory: ${plansDir}`, e);
}
}
return {
llmContent: 'Switching to Plan mode.',
returnDisplay: this.params.reason
@@ -44,6 +44,7 @@ describe('ExitPlanModeTool', () => {
getTargetDir: vi.fn().mockReturnValue(tempRootDir),
setApprovalMode: vi.fn(),
setApprovedPlanPath: vi.fn(),
getPlansDir: vi.fn().mockReturnValue(mockPlansDir),
storage: {
getPlansDir: vi.fn().mockReturnValue(mockPlansDir),
} as unknown as Config['storage'],
+5 -8
View File
@@ -60,11 +60,8 @@ export class ExitPlanModeTool extends BaseDeclarativeTool<
}
const safeFilename = path.basename(params.plan_filename);
const plansDir = resolveToRealPath(this.config.storage.getPlansDir());
const resolvedPath = path.join(
this.config.storage.getPlansDir(),
safeFilename,
);
const plansDir = resolveToRealPath(this.config.getPlansDir());
const resolvedPath = path.join(this.config.getPlansDir(), safeFilename);
const realPath = resolveToRealPath(resolvedPath);
@@ -120,7 +117,7 @@ export class ExitPlanModeInvocation extends BaseToolInvocation<
const pathError = await validatePlanPath(
this.params.plan_filename,
this.config.storage.getPlansDir(),
this.config.getPlansDir(),
);
if (pathError) {
this.planValidationError = pathError;
@@ -170,7 +167,7 @@ export class ExitPlanModeInvocation extends BaseToolInvocation<
}
getDescription(): string {
return `Requesting plan approval for: ${path.join(this.config.storage.getPlansDir(), this.params.plan_filename)}`;
return `Requesting plan approval for: ${path.join(this.config.getPlansDir(), this.params.plan_filename)}`;
}
/**
@@ -179,7 +176,7 @@ export class ExitPlanModeInvocation extends BaseToolInvocation<
*/
private getResolvedPlanPath(): string {
const safeFilename = path.basename(this.params.plan_filename);
return path.join(this.config.storage.getPlansDir(), safeFilename);
return path.join(this.config.getPlansDir(), safeFilename);
}
async execute(_signal: AbortSignal): Promise<ToolResult> {
@@ -237,6 +237,7 @@ describe('ToolRegistry', () => {
beforeEach(() => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.realpathSync).mockImplementation((p) => p.toString());
vi.mocked(fs.statSync).mockReturnValue({
isDirectory: () => true,
} as fs.Stats);
+1 -1
View File
@@ -617,7 +617,7 @@ export class ToolRegistry {
*/
getFunctionDeclarations(modelId?: string): FunctionDeclaration[] {
const isPlanMode = this.config.getApprovalMode() === ApprovalMode.PLAN;
const plansDir = this.config.storage.getPlansDir();
const plansDir = this.config.getPlansDir();
const declarations: FunctionDeclaration[] = [];
const seenNames = new Set<string>();
@@ -107,6 +107,7 @@ const mockConfigInternal = {
getDisableLLMCorrection: vi.fn(() => true),
isPlanMode: vi.fn(() => false),
getActiveModel: () => 'test-model',
getPlansDir: vi.fn().mockReturnValue('/tmp/plans'),
storage: {
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'),
},
+1 -4
View File
@@ -168,10 +168,7 @@ class WriteFileToolInvocation extends BaseToolInvocation<
if (this.config.isPlanMode()) {
const safeFilename = path.basename(this.params.file_path);
this.resolvedPath = path.join(
this.config.storage.getPlansDir(),
safeFilename,
);
this.resolvedPath = path.join(this.config.getPlansDir(), safeFilename);
} else {
this.resolvedPath = path.resolve(
this.config.getTargetDir(),