mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-28 05:55:17 -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' },
|
ui: { errorVerbosity: 'full' },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const lowVerbositySettings = createMockSettings({
|
||||||
|
merged: {
|
||||||
|
ui: { errorVerbosity: 'low' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
describe('Golden Snapshots', () => {
|
describe('Golden Snapshots', () => {
|
||||||
it('renders single successful tool call', async () => {
|
it('renders single successful tool call', async () => {
|
||||||
@@ -721,6 +726,245 @@ describe('<ToolGroupMessage />', () => {
|
|||||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||||
unmount();
|
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', () => {
|
describe('Plan Mode Filtering', () => {
|
||||||
|
|||||||
@@ -141,11 +141,15 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
|||||||
|
|
||||||
const contentWidth = terminalWidth - TOOL_MESSAGE_HORIZONTAL_MARGIN;
|
const contentWidth = terminalWidth - TOOL_MESSAGE_HORIZONTAL_MARGIN;
|
||||||
|
|
||||||
// If all tools are filtered out (e.g., in-progress AskUser tools, confirming tools),
|
// If all tools are filtered out (e.g., in-progress AskUser tools, low-verbosity
|
||||||
// only render if we need to close a border from previous
|
// internal errors, plan-mode hidden write/edit), we should not emit standalone
|
||||||
// tool groups. borderBottomOverride=true means we must render the closing border;
|
// border fragments. The only case where an empty group should render is the
|
||||||
// undefined or false means there's nothing to display.
|
// explicit "closing slice" (tools: []) used to bridge static/pending sections.
|
||||||
if (visibleToolCalls.length === 0 && borderBottomOverride !== true) {
|
const isExplicitClosingSlice = allToolCalls.length === 0;
|
||||||
|
if (
|
||||||
|
visibleToolCalls.length === 0 &&
|
||||||
|
(!isExplicitClosingSlice || borderBottomOverride !== true)
|
||||||
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user