diff --git a/.gemini/settings.json b/.gemini/settings.json
index 9051dc78de..6e51b96ed1 100644
--- a/.gemini/settings.json
+++ b/.gemini/settings.json
@@ -1,11 +1,16 @@
{
"experimental": {
- "plan": true,
"extensionReloading": true,
"modelSteering": true,
"memoryManager": true
},
"general": {
- "devtools": true
+ "devtools": true,
+ "plan": {
+ "enabled": true
+ }
+ },
+ "agents": {
+ "overrides": {}
}
}
diff --git a/integration-tests/shell-parity.test.ts b/integration-tests/shell-parity.test.ts
index 7bc16d68df..12352300a3 100644
--- a/integration-tests/shell-parity.test.ts
+++ b/integration-tests/shell-parity.test.ts
@@ -5,9 +5,7 @@
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
-import {
- TestRig,
-} from './test-helper.js';
+import { TestRig } from './test-helper.js';
describe('shell-parity', () => {
let rig: TestRig;
@@ -19,20 +17,29 @@ describe('shell-parity', () => {
afterEach(async () => await rig.cleanup());
it('should use run_shell_command for replace when sandboxing is enabled', async () => {
- await rig.setup('should use run_shell_command for replace when sandboxing is enabled', {
- settings: {
- security: { toolSandboxing: true },
+ await rig.setup(
+ 'should use run_shell_command for replace when sandboxing is enabled',
+ {
+ settings: {
+ security: { toolSandboxing: true },
+ },
},
- });
+ );
rig.createFile('test.ts', 'const foo = "bar";');
// We expect the model to use run_shell_command because edit/replace/write_file are filtered out.
- const result = await rig.run({
+ await rig.run({
args: `Replace "bar" with "baz" in test.ts`,
});
// Verify forbidden tools were NOT used
- const forbiddenTools = ['grep_search', 'replace', 'write_file', 'edit', 'read_file'];
+ const forbiddenTools = [
+ 'grep_search',
+ 'replace',
+ 'write_file',
+ 'edit',
+ 'read_file',
+ ];
const toolLogs = rig.readToolLogs();
const usedForbidden = toolLogs.filter((t) =>
forbiddenTools.includes(t.toolRequest.name),
@@ -55,11 +62,14 @@ describe('shell-parity', () => {
});
it('should use run_shell_command for search when sandboxing is enabled', async () => {
- await rig.setup('should use run_shell_command for search when sandboxing is enabled', {
- settings: {
- security: { toolSandboxing: true },
+ await rig.setup(
+ 'should use run_shell_command for search when sandboxing is enabled',
+ {
+ settings: {
+ security: { toolSandboxing: true },
+ },
},
- });
+ );
rig.createFile('search-me.txt', 'target-string');
await rig.run({
@@ -68,7 +78,9 @@ describe('shell-parity', () => {
// Verify grep_search was NOT used
const toolLogs = rig.readToolLogs();
- const usedGrep = toolLogs.filter((t) => t.toolRequest.name === 'grep_search');
+ const usedGrep = toolLogs.filter(
+ (t) => t.toolRequest.name === 'grep_search',
+ );
expect(usedGrep).toHaveLength(0);
// Verify run_shell_command was used
@@ -80,11 +92,14 @@ describe('shell-parity', () => {
});
it('should use run_shell_command for read when sandboxing is enabled', async () => {
- await rig.setup('should use run_shell_command for read when sandboxing is enabled', {
- settings: {
- security: { toolSandboxing: true },
+ await rig.setup(
+ 'should use run_shell_command for read when sandboxing is enabled',
+ {
+ settings: {
+ security: { toolSandboxing: true },
+ },
},
- });
+ );
rig.createFile('read-me.txt', 'hello world');
const result = await rig.run({
diff --git a/packages/cli/src/ui/components/shared/__snapshots__/SectionHeader.test.tsx.snap b/packages/cli/src/ui/components/shared/__snapshots__/SectionHeader.test.tsx.snap
index fb18e546a8..bdf5564124 100644
--- a/packages/cli/src/ui/components/shared/__snapshots__/SectionHeader.test.tsx.snap
+++ b/packages/cli/src/ui/components/shared/__snapshots__/SectionHeader.test.tsx.snap
@@ -6,6 +6,12 @@ Narrow Container
"
`;
+exports[` > 'renders correctly in a narrow contain…' 2`] = `
+"─────────────────────────
+Narrow Container
+"
+`;
+
exports[` > 'renders correctly when title is trunc…' 1`] = `
"────────────────────
Very Long Header Ti…
diff --git a/packages/cli/src/ui/hooks/toolMapping.ts b/packages/cli/src/ui/hooks/toolMapping.ts
index 901b89e44a..afb8b34755 100644
--- a/packages/cli/src/ui/hooks/toolMapping.ts
+++ b/packages/cli/src/ui/hooks/toolMapping.ts
@@ -42,7 +42,10 @@ export function mapToDisplay(
if (call.status === CoreToolCallStatus.Error) {
description = JSON.stringify(call.request.args);
} else {
- description = typeof call.invocation.getDisplayTitle === 'function' ? call.invocation.getDisplayTitle() : call.invocation.getDescription();
+ description =
+ typeof call.invocation.getDisplayTitle === 'function'
+ ? call.invocation.getDisplayTitle()
+ : call.invocation.getDescription();
renderOutputAsMarkdown = call.tool.isOutputMarkdown;
}
diff --git a/packages/core/src/agents/local-executor.ts b/packages/core/src/agents/local-executor.ts
index bfd8973cdf..b62df0d11b 100644
--- a/packages/core/src/agents/local-executor.ts
+++ b/packages/core/src/agents/local-executor.ts
@@ -1026,7 +1026,10 @@ export class LocalAgentExecutor {
const invocation = tool.build(args);
// Prefer getDisplayTitle if it differs, otherwise fallback to getDescription.
// This ensures the timeline ("Shell (Replace)") matches the confirmation dialog.
- description = typeof invocation.getDisplayTitle === 'function' ? invocation.getDisplayTitle() : invocation.getDescription();
+ description =
+ typeof invocation.getDisplayTitle === 'function'
+ ? invocation.getDisplayTitle()
+ : invocation.getDescription();
}
} catch {
// Ignore errors during formatting for activity emission
diff --git a/packages/core/src/core/geminiChat.ts b/packages/core/src/core/geminiChat.ts
index eb95b61ea5..3fb966ecf0 100644
--- a/packages/core/src/core/geminiChat.ts
+++ b/packages/core/src/core/geminiChat.ts
@@ -1032,7 +1032,10 @@ export class GeminiChat {
let description: string | undefined = undefined;
if ('invocation' in call && call.invocation) {
- description = typeof call.invocation.getDisplayTitle === 'function' ? call.invocation.getDisplayTitle() : call.invocation.getDescription();
+ description =
+ typeof call.invocation.getDisplayTitle === 'function'
+ ? call.invocation.getDisplayTitle()
+ : call.invocation.getDescription();
}
return {
diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts
index 96d6114066..29ec9fb9ba 100644
--- a/packages/core/src/tools/shell.ts
+++ b/packages/core/src/tools/shell.ts
@@ -151,6 +151,8 @@ export class ShellToolInvocation extends BaseToolInvocation<
return `Shell (Write File)`;
case FileOperationType.READ:
return `Shell (Read File)`;
+ default:
+ break;
}
}
return this.params.command;
@@ -218,7 +220,7 @@ export class ShellToolInvocation extends BaseToolInvocation<
}
protected override async getConfirmationDetails(
- abortSignal: AbortSignal,
+ _abortSignal: AbortSignal,
): Promise {
const command = stripShellWrapper(this.params.command);
const inferred = inferFileOperation(this.params.command);
@@ -244,25 +246,29 @@ export class ShellToolInvocation extends BaseToolInvocation<
inferred.type === FileOperationType.EDIT &&
originalContent !== null
) {
- if (inferred.metadata?.['sedExpression']) {
+ if (typeof inferred.metadata?.['sedExpression'] === 'string') {
newContent = this.simulateSed(
originalContent,
- inferred.metadata['sedExpression'] as string,
+ inferred.metadata['sedExpression'],
);
} else if (
- inferred.metadata?.['oldString'] &&
- inferred.metadata?.['newString']
+ typeof inferred.metadata?.['oldString'] === 'string' &&
+ typeof inferred.metadata?.['newString'] === 'string'
) {
newContent = this.simulatePsReplace(
originalContent,
- inferred.metadata['oldString'] as string,
- inferred.metadata['newString'] as string,
+ inferred.metadata['oldString'],
+ inferred.metadata['newString'],
);
}
}
if (newContent !== undefined && originalContent !== null) {
- const fileDiff = Diff.createPatch(filePath, originalContent, newContent);
+ const fileDiff = Diff.createPatch(
+ filePath,
+ originalContent,
+ newContent,
+ );
const editDetails: ToolEditConfirmationDetails = {
type: 'edit',
title: this.getDisplayTitle(),
@@ -511,6 +517,8 @@ export class ShellToolInvocation extends BaseToolInvocation<
operation = FileOperation.UPDATE;
canonicalToolName = WRITE_FILE_TOOL_NAME;
break;
+ default:
+ break;
}
if (operation) {
diff --git a/packages/core/src/utils/shell-utils.test.ts b/packages/core/src/utils/shell-utils.test.ts
index ff79f60c3a..6c4c6c381d 100644
--- a/packages/core/src/utils/shell-utils.test.ts
+++ b/packages/core/src/utils/shell-utils.test.ts
@@ -606,40 +606,42 @@ describe('resolveExecutable', () => {
});
});
- describe('inferFileOperation', () => {
- it('should infer sed -i as an EDIT operation', () => {
- mockPlatform.mockReturnValue('linux');
- const result = inferFileOperation("sed -i 's/foo/bar/g' test.ts");
- expect(result).toBeDefined();
- expect(result?.type).toBe(FileOperationType.EDIT);
- expect(result?.filePath).toBe('test.ts');
- expect(result?.metadata?.['sedExpression']).toBe('s/foo/bar/g');
- });
-
- it('should infer echo redirection as a WRITE operation', () => {
- mockPlatform.mockReturnValue('linux');
- const result = inferFileOperation("echo 'hello' > hello.txt");
- expect(result).toBeDefined();
- expect(result?.type).toBe(FileOperationType.WRITE);
- expect(result?.filePath).toBe('hello.txt');
- });
-
- it('should infer grep as a SEARCH operation', () => {
- mockPlatform.mockReturnValue('linux');
- const result = inferFileOperation("grep 'pattern' file.txt");
- expect(result).toBeDefined();
- expect(result?.type).toBe(FileOperationType.SEARCH);
- expect(result?.filePath).toBe('file.txt');
- expect(result?.metadata?.['pattern']).toBe('pattern');
- });
-
- it('should infer PowerShell -replace as an EDIT operation', () => {
- mockPlatform.mockReturnValue('win32');
- const result = inferFileOperation("(Get-Content file.txt) -replace 'a', 'b' | Set-Content file.txt");
- expect(result).toBeDefined();
- expect(result?.type).toBe(FileOperationType.EDIT);
- expect(result?.filePath).toBe('file.txt');
- expect(result?.metadata?.['oldString']).toBe('a');
- expect(result?.metadata?.['newString']).toBe('b');
- });
+describe('inferFileOperation', () => {
+ it('should infer sed -i as an EDIT operation', () => {
+ mockPlatform.mockReturnValue('linux');
+ const result = inferFileOperation("sed -i 's/foo/bar/g' test.ts");
+ expect(result).toBeDefined();
+ expect(result?.type).toBe(FileOperationType.EDIT);
+ expect(result?.filePath).toBe('test.ts');
+ expect(result?.metadata?.['sedExpression']).toBe('s/foo/bar/g');
});
+
+ it('should infer echo redirection as a WRITE operation', () => {
+ mockPlatform.mockReturnValue('linux');
+ const result = inferFileOperation("echo 'hello' > hello.txt");
+ expect(result).toBeDefined();
+ expect(result?.type).toBe(FileOperationType.WRITE);
+ expect(result?.filePath).toBe('hello.txt');
+ });
+
+ it('should infer grep as a SEARCH operation', () => {
+ mockPlatform.mockReturnValue('linux');
+ const result = inferFileOperation("grep 'pattern' file.txt");
+ expect(result).toBeDefined();
+ expect(result?.type).toBe(FileOperationType.SEARCH);
+ expect(result?.filePath).toBe('file.txt');
+ expect(result?.metadata?.['pattern']).toBe('pattern');
+ });
+
+ it('should infer PowerShell -replace as an EDIT operation', () => {
+ mockPlatform.mockReturnValue('win32');
+ const result = inferFileOperation(
+ "(Get-Content file.txt) -replace 'a', 'b' | Set-Content file.txt",
+ );
+ expect(result).toBeDefined();
+ expect(result?.type).toBe(FileOperationType.EDIT);
+ expect(result?.filePath).toBe('file.txt');
+ expect(result?.metadata?.['oldString']).toBe('a');
+ expect(result?.metadata?.['newString']).toBe('b');
+ });
+});
diff --git a/packages/core/src/utils/shell-utils.ts b/packages/core/src/utils/shell-utils.ts
index 56ef2bd9c2..466a98b7e0 100644
--- a/packages/core/src/utils/shell-utils.ts
+++ b/packages/core/src/utils/shell-utils.ts
@@ -835,7 +835,9 @@ export function inferFileOperation(
}
// ... | tee file
- const teeMatch = stripped.match(/\|\s*tee\s+(?:-a\s+)?['"]?([^'"]+)['"]?\s*$/);
+ const teeMatch = stripped.match(
+ /\|\s*tee\s+(?:-a\s+)?['"]?([^'"]+)['"]?\s*$/,
+ );
if (teeMatch) {
return {
type: FileOperationType.WRITE,
@@ -874,7 +876,10 @@ export function inferFileOperation(
return {
type: FileOperationType.EDIT,
filePath: psReplaceMatch[1].trim(),
- metadata: { oldString: psReplaceMatch[2], newString: psReplaceMatch[3] },
+ metadata: {
+ oldString: psReplaceMatch[2],
+ newString: psReplaceMatch[3],
+ },
};
}