From 15419434db54a6ff01dbdc5f36d9f0dce98fb760 Mon Sep 17 00:00:00 2001 From: Sehoon Shon Date: Fri, 20 Feb 2026 19:26:46 -0500 Subject: [PATCH] fix(core): deduplicate command names in shell confirmation Fixes #19768 --- packages/core/src/tools/shell.test.ts | 19 +++++++++++++++++-- packages/core/src/tools/shell.ts | 8 ++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/core/src/tools/shell.test.ts b/packages/core/src/tools/shell.test.ts index 5fc3ca7f25..c0fcc36e8c 100644 --- a/packages/core/src/tools/shell.test.ts +++ b/packages/core/src/tools/shell.test.ts @@ -42,7 +42,7 @@ vi.mock('crypto'); vi.mock('../utils/summarizer.js'); import { initializeShellParsers } from '../utils/shell-utils.js'; -import { ShellTool } from './shell.js'; +import { ShellTool, OUTPUT_UPDATE_INTERVAL_MS } from './shell.js'; import { debugLogger } from '../index.js'; import { type Config } from '../config/config.js'; import { @@ -58,7 +58,6 @@ import * as crypto from 'node:crypto'; import * as summarizer from '../utils/summarizer.js'; import { ToolErrorType } from './tool-error.js'; import { ToolConfirmationOutcome } from './tools.js'; -import { OUTPUT_UPDATE_INTERVAL_MS } from './shell.js'; import { SHELL_TOOL_NAME } from './tool-names.js'; import { WorkspaceContext } from '../utils/workspaceContext.js'; import { @@ -775,6 +774,22 @@ describe('ShellTool', () => { }); describe('getConfirmationDetails', () => { + it('should deduplicate root commands in confirmation details', async () => { + const shellTool = new ShellTool(mockConfig, createMockMessageBus()); + const command = 'git status && git diff && git log'; + const invocation = shellTool.build({ command }); + + // @ts-expect-error - getConfirmationDetails is protected + const details = await invocation.getConfirmationDetails( + new AbortController().signal, + ); + + expect(details).not.toBe(false); + if (details && details.type === 'exec') { + expect(details.rootCommand).toBe('git'); + } + }); + it('should annotate sub-commands with redirection correctly', async () => { const shellTool = new ShellTool(mockConfig, createMockMessageBus()); const command = 'mkdir -p baz && echo "hello" > baz/test.md && ls'; diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts index 76db302f42..1cdb22173c 100644 --- a/packages/core/src/tools/shell.ts +++ b/packages/core/src/tools/shell.ts @@ -16,13 +16,13 @@ import type { ToolResult, ToolCallConfirmationDetails, ToolExecuteConfirmationDetails, + PolicyUpdateOptions, } from './tools.js'; import { BaseDeclarativeTool, BaseToolInvocation, ToolConfirmationOutcome, Kind, - type PolicyUpdateOptions, } from './tools.js'; import { getErrorMessage } from '../utils/errors.js'; @@ -124,9 +124,9 @@ export class ShellToolInvocation extends BaseToolInvocation< rootCommandDisplay += ', redirection'; } } else { - rootCommandDisplay = parsed.details - .map((detail) => detail.name) - .join(', '); + rootCommandDisplay = [ + ...new Set(parsed.details.map((detail) => detail.name)), + ].join(', '); } const rootCommands = [...new Set(getCommandRoots(command))];