mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 14:10:37 -07:00
fix(ui): prevent empty tool-group border stubs after filtering (#21852)
Co-authored-by: jacob314 <jacob314@gmail.com>
This commit is contained in:
@@ -69,6 +69,11 @@ describe('<ToolGroupMessage />', () => {
|
||||
ui: { errorVerbosity: 'full' },
|
||||
},
|
||||
});
|
||||
const lowVerbositySettings = createMockSettings({
|
||||
merged: {
|
||||
ui: { errorVerbosity: 'low' },
|
||||
},
|
||||
});
|
||||
|
||||
describe('Golden Snapshots', () => {
|
||||
it('renders single successful tool call', async () => {
|
||||
@@ -721,6 +726,245 @@ describe('<ToolGroupMessage />', () => {
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('does not render a bottom-border fragment when all tools are filtered out', async () => {
|
||||
const toolCalls = [
|
||||
createToolCall({
|
||||
callId: 'hidden-error-tool',
|
||||
name: 'error-tool',
|
||||
status: CoreToolCallStatus.Error,
|
||||
resultDisplay: 'Hidden in low verbosity',
|
||||
isClientInitiated: false,
|
||||
}),
|
||||
];
|
||||
const item = createItem(toolCalls);
|
||||
|
||||
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
|
||||
<ToolGroupMessage
|
||||
{...baseProps}
|
||||
item={item}
|
||||
toolCalls={toolCalls}
|
||||
borderTop={false}
|
||||
borderBottom={true}
|
||||
/>,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
settings: lowVerbositySettings,
|
||||
},
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('still renders explicit closing slices for split static/pending groups', async () => {
|
||||
const toolCalls: IndividualToolCallDisplay[] = [];
|
||||
const item = createItem(toolCalls);
|
||||
|
||||
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
|
||||
<ToolGroupMessage
|
||||
{...baseProps}
|
||||
item={item}
|
||||
toolCalls={toolCalls}
|
||||
borderTop={false}
|
||||
borderBottom={true}
|
||||
/>,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
settings: fullVerbositySettings,
|
||||
},
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })).not.toBe('');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('does not render a border fragment when plan-mode tools are filtered out', async () => {
|
||||
const toolCalls = [
|
||||
createToolCall({
|
||||
callId: 'plan-write',
|
||||
name: WRITE_FILE_DISPLAY_NAME,
|
||||
approvalMode: ApprovalMode.PLAN,
|
||||
status: CoreToolCallStatus.Success,
|
||||
resultDisplay: 'Plan file written',
|
||||
}),
|
||||
];
|
||||
const item = createItem(toolCalls);
|
||||
|
||||
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
|
||||
<ToolGroupMessage
|
||||
{...baseProps}
|
||||
item={item}
|
||||
toolCalls={toolCalls}
|
||||
borderTop={false}
|
||||
borderBottom={true}
|
||||
/>,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
settings: fullVerbositySettings,
|
||||
},
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('does not render a border fragment when only confirming tools are present', async () => {
|
||||
const toolCalls = [
|
||||
createToolCall({
|
||||
callId: 'confirm-only',
|
||||
status: CoreToolCallStatus.AwaitingApproval,
|
||||
confirmationDetails: {
|
||||
type: 'info',
|
||||
title: 'Confirm',
|
||||
prompt: 'Proceed?',
|
||||
},
|
||||
}),
|
||||
];
|
||||
const item = createItem(toolCalls);
|
||||
|
||||
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
|
||||
<ToolGroupMessage
|
||||
{...baseProps}
|
||||
item={item}
|
||||
toolCalls={toolCalls}
|
||||
borderTop={false}
|
||||
borderBottom={true}
|
||||
/>,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
settings: fullVerbositySettings,
|
||||
},
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('does not leave a border stub when transitioning from visible to fully filtered tools', async () => {
|
||||
const visibleTools = [
|
||||
createToolCall({
|
||||
callId: 'visible-success',
|
||||
name: 'visible-tool',
|
||||
status: CoreToolCallStatus.Success,
|
||||
resultDisplay: 'visible output',
|
||||
}),
|
||||
];
|
||||
const hiddenTools = [
|
||||
createToolCall({
|
||||
callId: 'hidden-error',
|
||||
name: 'hidden-error-tool',
|
||||
status: CoreToolCallStatus.Error,
|
||||
resultDisplay: 'hidden output',
|
||||
isClientInitiated: false,
|
||||
}),
|
||||
];
|
||||
|
||||
const initialItem = createItem(visibleTools);
|
||||
const hiddenItem = createItem(hiddenTools);
|
||||
|
||||
const firstRender = renderWithProviders(
|
||||
<ToolGroupMessage
|
||||
{...baseProps}
|
||||
item={initialItem}
|
||||
toolCalls={visibleTools}
|
||||
borderTop={false}
|
||||
borderBottom={true}
|
||||
/>,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
settings: lowVerbositySettings,
|
||||
},
|
||||
);
|
||||
await firstRender.waitUntilReady();
|
||||
expect(firstRender.lastFrame()).toContain('visible-tool');
|
||||
firstRender.unmount();
|
||||
|
||||
const secondRender = renderWithProviders(
|
||||
<ToolGroupMessage
|
||||
{...baseProps}
|
||||
item={hiddenItem}
|
||||
toolCalls={hiddenTools}
|
||||
borderTop={false}
|
||||
borderBottom={true}
|
||||
/>,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
settings: lowVerbositySettings,
|
||||
},
|
||||
);
|
||||
await secondRender.waitUntilReady();
|
||||
expect(secondRender.lastFrame({ allowEmpty: true })).toBe('');
|
||||
secondRender.unmount();
|
||||
});
|
||||
|
||||
it('keeps visible tools rendered with many filtered tools (stress case)', async () => {
|
||||
const visibleTool = createToolCall({
|
||||
callId: 'visible-tool',
|
||||
name: 'visible-tool',
|
||||
status: CoreToolCallStatus.Success,
|
||||
resultDisplay: 'visible output',
|
||||
});
|
||||
const hiddenTools = Array.from({ length: 50 }, (_, index) =>
|
||||
createToolCall({
|
||||
callId: `hidden-${index}`,
|
||||
name: `hidden-error-${index}`,
|
||||
status: CoreToolCallStatus.Error,
|
||||
resultDisplay: `hidden output ${index}`,
|
||||
isClientInitiated: false,
|
||||
}),
|
||||
);
|
||||
const toolCalls = [visibleTool, ...hiddenTools];
|
||||
const item = createItem(toolCalls);
|
||||
|
||||
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
|
||||
<ToolGroupMessage
|
||||
{...baseProps}
|
||||
item={item}
|
||||
toolCalls={toolCalls}
|
||||
borderTop={false}
|
||||
borderBottom={true}
|
||||
/>,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
settings: lowVerbositySettings,
|
||||
},
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('visible-tool');
|
||||
expect(output).not.toContain('hidden-error-0');
|
||||
expect(output).not.toContain('hidden-error-49');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders explicit closing slice even at very narrow terminal width', async () => {
|
||||
const toolCalls: IndividualToolCallDisplay[] = [];
|
||||
const item = createItem(toolCalls);
|
||||
|
||||
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
|
||||
<ToolGroupMessage
|
||||
item={item}
|
||||
toolCalls={toolCalls}
|
||||
terminalWidth={8}
|
||||
borderTop={false}
|
||||
borderBottom={true}
|
||||
/>,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
settings: fullVerbositySettings,
|
||||
},
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
expect(lastFrame({ allowEmpty: true })).not.toBe('');
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Plan Mode Filtering', () => {
|
||||
|
||||
@@ -141,11 +141,15 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
|
||||
const contentWidth = terminalWidth - TOOL_MESSAGE_HORIZONTAL_MARGIN;
|
||||
|
||||
// If all tools are filtered out (e.g., in-progress AskUser tools, confirming tools),
|
||||
// only render if we need to close a border from previous
|
||||
// tool groups. borderBottomOverride=true means we must render the closing border;
|
||||
// undefined or false means there's nothing to display.
|
||||
if (visibleToolCalls.length === 0 && borderBottomOverride !== true) {
|
||||
// If all tools are filtered out (e.g., in-progress AskUser tools, low-verbosity
|
||||
// internal errors, plan-mode hidden write/edit), we should not emit standalone
|
||||
// border fragments. The only case where an empty group should render is the
|
||||
// explicit "closing slice" (tools: []) used to bridge static/pending sections.
|
||||
const isExplicitClosingSlice = allToolCalls.length === 0;
|
||||
if (
|
||||
visibleToolCalls.length === 0 &&
|
||||
(!isExplicitClosingSlice || borderBottomOverride !== true)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user