diff --git a/packages/cli/src/ui/components/messages/DenseToolMessage.test.tsx b/packages/cli/src/ui/components/messages/DenseToolMessage.test.tsx
index 1feadb8ba4..7bb943791b 100644
--- a/packages/cli/src/ui/components/messages/DenseToolMessage.test.tsx
+++ b/packages/cli/src/ui/components/messages/DenseToolMessage.test.tsx
@@ -565,6 +565,45 @@ describe('DenseToolMessage', () => {
expect(lastFrame()).toContain('new line');
});
+ it('shows diff content when globally expanded inside a VirtualizedList context', async () => {
+ const mockListContext = {
+ registerInteractivity: vi.fn(),
+ setItemState: vi.fn(),
+ getItemState: vi.fn(),
+ isItemToggled: vi.fn().mockReturnValue(false),
+ toggleItem: vi.fn(),
+ registerClickCallback: vi.fn(),
+ unregisterClickCallback: vi.fn(),
+ };
+
+ const { lastFrame, waitUntilReady } = await renderWithProviders(
+
+ }
+ >
+
+ ,
+ {
+ config: makeFakeConfig({ useAlternateBuffer: true }),
+ settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
+ toolActions: {
+ isExpanded: () => true,
+ },
+ },
+ );
+ await waitUntilReady();
+
+ expect(lastFrame()).toContain('new line');
+ });
+
it('toggles expansion when header is clicked', async () => {
const toggleExpansion = vi.fn();
const toggleItem = vi.fn();
diff --git a/packages/cli/src/ui/components/messages/DenseToolMessage.tsx b/packages/cli/src/ui/components/messages/DenseToolMessage.tsx
index d449703cf2..5d762666c4 100644
--- a/packages/cli/src/ui/components/messages/DenseToolMessage.tsx
+++ b/packages/cli/src/ui/components/messages/DenseToolMessage.tsx
@@ -283,10 +283,15 @@ export const DenseToolMessage: React.FC = (props) => {
// Determine expansion state based on list context or fallback to tool actions
const isExpanded = useMemo(() => {
+ const isExpandedGlobally = isExpandedInContext
+ ? isExpandedInContext(callId)
+ : false;
if (itemKey && virtualizedListContext) {
- return virtualizedListContext.isItemToggled(itemKey);
+ return (
+ virtualizedListContext.isItemToggled(itemKey) || isExpandedGlobally
+ );
}
- return isExpandedInContext ? isExpandedInContext(callId) : false;
+ return isExpandedGlobally;
}, [itemKey, virtualizedListContext, isExpandedInContext, callId]);
const handleToggle = useCallback(() => {
diff --git a/packages/cli/src/ui/components/messages/TopicMessage.tsx b/packages/cli/src/ui/components/messages/TopicMessage.tsx
index 59be80602d..e51f421d47 100644
--- a/packages/cli/src/ui/components/messages/TopicMessage.tsx
+++ b/packages/cli/src/ui/components/messages/TopicMessage.tsx
@@ -24,6 +24,7 @@ interface TopicMessageProps extends IndividualToolCallDisplay {
terminalWidth: number;
availableTerminalHeight?: number;
isExpandable?: boolean;
+ // TopicMessage is only interactive when rendered inside VirtualizedList.
itemKey?: string;
}
diff --git a/packages/cli/src/ui/components/shared/FixedScrollableList.tsx b/packages/cli/src/ui/components/shared/FixedScrollableList.tsx
index b382579d94..1d21ff4ed8 100644
--- a/packages/cli/src/ui/components/shared/FixedScrollableList.tsx
+++ b/packages/cli/src/ui/components/shared/FixedScrollableList.tsx
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright 2025 Google LLC
+ * Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
diff --git a/packages/cli/src/ui/components/shared/FixedVirtualizedList.test.tsx b/packages/cli/src/ui/components/shared/FixedVirtualizedList.test.tsx
new file mode 100644
index 0000000000..363eec1501
--- /dev/null
+++ b/packages/cli/src/ui/components/shared/FixedVirtualizedList.test.tsx
@@ -0,0 +1,55 @@
+/**
+ * @license
+ * Copyright 2026 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { act } from 'react';
+import { Box, Text } from 'ink';
+import { describe, expect, it } from 'vitest';
+import { renderWithProviders as render } from '../../../test-utils/render.js';
+import {
+ FixedVirtualizedList,
+ SCROLL_TO_ITEM_END,
+} from './FixedVirtualizedList.js';
+
+describe('', () => {
+ const renderList = (data: string[]) => (
+
+ (
+
+ {item}
+
+ )}
+ itemHeight={1}
+ keyExtractor={(item) => item}
+ initialScrollIndex={SCROLL_TO_ITEM_END}
+ initialScrollOffsetInIndex={SCROLL_TO_ITEM_END}
+ width={80}
+ maxHeight={5}
+ />
+
+ );
+
+ it('sticks to the bottom when data grows', async () => {
+ const initialData = Array.from({ length: 10 }, (_, i) => `Item ${i}`);
+ const { lastFrame, rerender, waitUntilReady, unmount } = await render(
+ renderList(initialData),
+ );
+ await waitUntilReady();
+
+ expect(lastFrame()).toContain('Item 9');
+
+ const newData = [...initialData, 'Item 10', 'Item 11'];
+ await act(async () => {
+ rerender(renderList(newData));
+ });
+ await waitUntilReady();
+
+ expect(lastFrame()).toContain('Item 11');
+ expect(lastFrame()).not.toContain('Item 0');
+ unmount();
+ });
+});
diff --git a/packages/cli/src/ui/components/shared/FixedVirtualizedList.tsx b/packages/cli/src/ui/components/shared/FixedVirtualizedList.tsx
index 4f98175462..c6a21735e6 100644
--- a/packages/cli/src/ui/components/shared/FixedVirtualizedList.tsx
+++ b/packages/cli/src/ui/components/shared/FixedVirtualizedList.tsx
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright 2025 Google LLC
+ * Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
@@ -172,23 +172,21 @@ function FixedVirtualizedList(
props.targetScrollIndex,
);
const prevDataLength = useRef(data.length);
+ const previousDataLength = prevDataLength.current;
if (
(props.targetScrollIndex !== undefined &&
props.targetScrollIndex !== prevTargetScrollIndex &&
data.length > 0) ||
(props.targetScrollIndex !== undefined &&
- prevDataLength.current === 0 &&
+ previousDataLength === 0 &&
data.length > 0)
) {
if (props.targetScrollIndex !== prevTargetScrollIndex) {
setPrevTargetScrollIndex(props.targetScrollIndex);
}
- prevDataLength.current = data.length;
setIsStickingToBottom(false);
setScrollAnchor({ index: props.targetScrollIndex, offset: 0 });
- } else {
- prevDataLength.current = data.length;
}
const rawStateActualScrollTop = (() => {
@@ -236,7 +234,7 @@ function FixedVirtualizedList(
}
}
- const listGrew = data.length > prevDataLength.current;
+ const listGrew = data.length > previousDataLength;
const containerChanged =
prevContainerHeight.current !== scrollableContainerHeight;
const shouldAutoScroll = props.targetScrollIndex === undefined;
diff --git a/packages/cli/src/ui/components/shared/VirtualizedList.test.tsx b/packages/cli/src/ui/components/shared/VirtualizedList.test.tsx
index eb7a77848a..37ea63cafe 100644
--- a/packages/cli/src/ui/components/shared/VirtualizedList.test.tsx
+++ b/packages/cli/src/ui/components/shared/VirtualizedList.test.tsx
@@ -119,6 +119,41 @@ describe('', () => {
unmount();
});
+ it('rerenders cached items when renderItem changes', async () => {
+ const data = ['Item 0'];
+ const renderWithLabel = (label: string) => (
+
+ (
+
+
+ {label} {item}
+
+
+ )}
+ keyExtractor={keyExtractor}
+ estimatedItemHeight={() => itemHeight}
+ />
+
+ );
+
+ const { lastFrame, rerender, waitUntilReady, unmount } = await render(
+ renderWithLabel('Initial'),
+ );
+ await waitUntilReady();
+ expect(lastFrame()).toContain('Initial Item 0');
+
+ await act(async () => {
+ rerender(renderWithLabel('Updated'));
+ });
+ await waitUntilReady();
+
+ expect(lastFrame()).toContain('Updated Item 0');
+ expect(lastFrame()).not.toContain('Initial Item 0');
+ unmount();
+ });
+
it('scrolls down to show new items when requested via ref', async () => {
const ref = createRef>();
const { lastFrame, waitUntilReady, unmount } = await render(
diff --git a/packages/cli/src/ui/components/shared/VirtualizedList.tsx b/packages/cli/src/ui/components/shared/VirtualizedList.tsx
index 0d9e2ecdce..fa22387b39 100644
--- a/packages/cli/src/ui/components/shared/VirtualizedList.tsx
+++ b/packages/cli/src/ui/components/shared/VirtualizedList.tsx
@@ -926,6 +926,7 @@ function VirtualizedList(
containerWidth: number;
index: number;
isToggled: boolean;
+ renderItem: typeof renderItem;
}
>(),
);
@@ -965,7 +966,8 @@ function VirtualizedList(
cached.width === width &&
cached.containerWidth === containerWidth &&
cached.index === i &&
- cached.isToggled === isToggled
+ cached.isToggled === isToggled &&
+ cached.renderItem === renderItem
) {
contentElement = cached.element;
} else {
@@ -998,6 +1000,7 @@ function VirtualizedList(
containerWidth,
index: i,
isToggled,
+ renderItem,
});
}
diff --git a/packages/cli/src/ui/hooks/useMouseClick.ts b/packages/cli/src/ui/hooks/useMouseClick.ts
index 0b0d5caecc..1509eee40f 100644
--- a/packages/cli/src/ui/hooks/useMouseClick.ts
+++ b/packages/cli/src/ui/hooks/useMouseClick.ts
@@ -12,7 +12,6 @@ import {
type MouseEvent,
type MouseEventName,
} from '../contexts/MouseContext.js';
-import { debugLogger } from '@google/gemini-cli-core';
export const useMouseClick = (
containerRef: React.RefObject,
@@ -32,9 +31,6 @@ export const useMouseClick = (
const eventName =
name ?? (button === 'left' ? 'left-press' : 'right-release');
- debugLogger.log(
- `[useMouseClick] received event=${event.name} expected=${eventName} hasContainer=${!!containerRef.current}`,
- );
if (event.name === eventName && containerRef.current) {
const { x, y, width, height } = getBoundingBox(containerRef.current);
// Terminal mouse events are 1-based, Ink layout is 0-based.
@@ -44,17 +40,12 @@ export const useMouseClick = (
const relativeX = mouseX - x;
const relativeY = mouseY - y;
- debugLogger.log(
- `[useMouseClick] bounds x=${x} y=${y} w=${width} h=${height} mouseX=${mouseX} mouseY=${mouseY} relX=${relativeX} relY=${relativeY}`,
- );
-
if (
relativeX >= 0 &&
relativeX < width &&
relativeY >= 0 &&
relativeY < height
) {
- debugLogger.log(`[useMouseClick] Triggering handler!`);
handlerRef.current(event, relativeX, relativeY);
}
}