mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-12 12:54:07 -07:00
UX for topic narration tool (#24079)
This commit is contained in:
committed by
GitHub
parent
3eebb75b7a
commit
b7c86b5497
@@ -48,6 +48,7 @@ interface HistoryItemDisplayProps {
|
|||||||
isExpandable?: boolean;
|
isExpandable?: boolean;
|
||||||
isFirstThinking?: boolean;
|
isFirstThinking?: boolean;
|
||||||
isFirstAfterThinking?: boolean;
|
isFirstAfterThinking?: boolean;
|
||||||
|
suppressNarration?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||||
@@ -60,6 +61,7 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
|||||||
isExpandable,
|
isExpandable,
|
||||||
isFirstThinking = false,
|
isFirstThinking = false,
|
||||||
isFirstAfterThinking = false,
|
isFirstAfterThinking = false,
|
||||||
|
suppressNarration = false,
|
||||||
}) => {
|
}) => {
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
const inlineThinkingMode = getInlineThinkingMode(settings);
|
const inlineThinkingMode = getInlineThinkingMode(settings);
|
||||||
@@ -68,6 +70,17 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
|||||||
const needsTopMarginAfterThinking =
|
const needsTopMarginAfterThinking =
|
||||||
isFirstAfterThinking && inlineThinkingMode !== 'off';
|
isFirstAfterThinking && inlineThinkingMode !== 'off';
|
||||||
|
|
||||||
|
// If there's a topic update in this turn, we suppress the regular narration
|
||||||
|
// and thoughts as they are being "replaced" by the update_topic tool.
|
||||||
|
if (
|
||||||
|
suppressNarration &&
|
||||||
|
(itemForDisplay.type === 'thinking' ||
|
||||||
|
itemForDisplay.type === 'gemini' ||
|
||||||
|
itemForDisplay.type === 'gemini_content')
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
|
|||||||
@@ -7,8 +7,10 @@
|
|||||||
import { Box, Static } from 'ink';
|
import { Box, Static } from 'ink';
|
||||||
import { HistoryItemDisplay } from './HistoryItemDisplay.js';
|
import { HistoryItemDisplay } from './HistoryItemDisplay.js';
|
||||||
import { useUIState } from '../contexts/UIStateContext.js';
|
import { useUIState } from '../contexts/UIStateContext.js';
|
||||||
|
import { useSettings } from '../contexts/SettingsContext.js';
|
||||||
import { useAppContext } from '../contexts/AppContext.js';
|
import { useAppContext } from '../contexts/AppContext.js';
|
||||||
import { AppHeader } from './AppHeader.js';
|
import { AppHeader } from './AppHeader.js';
|
||||||
|
|
||||||
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
|
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
|
||||||
import {
|
import {
|
||||||
SCROLL_TO_ITEM_END,
|
SCROLL_TO_ITEM_END,
|
||||||
@@ -19,6 +21,7 @@ import { useMemo, memo, useCallback, useEffect, useRef } from 'react';
|
|||||||
import { MAX_GEMINI_MESSAGE_LINES } from '../constants.js';
|
import { MAX_GEMINI_MESSAGE_LINES } from '../constants.js';
|
||||||
import { useConfirmingTool } from '../hooks/useConfirmingTool.js';
|
import { useConfirmingTool } from '../hooks/useConfirmingTool.js';
|
||||||
import { ToolConfirmationQueue } from './ToolConfirmationQueue.js';
|
import { ToolConfirmationQueue } from './ToolConfirmationQueue.js';
|
||||||
|
import { isTopicTool } from './messages/TopicMessage.js';
|
||||||
|
|
||||||
const MemoizedHistoryItemDisplay = memo(HistoryItemDisplay);
|
const MemoizedHistoryItemDisplay = memo(HistoryItemDisplay);
|
||||||
const MemoizedAppHeader = memo(AppHeader);
|
const MemoizedAppHeader = memo(AppHeader);
|
||||||
@@ -63,12 +66,39 @@ export const MainContent = () => {
|
|||||||
return -1;
|
return -1;
|
||||||
}, [uiState.history]);
|
}, [uiState.history]);
|
||||||
|
|
||||||
|
const settings = useSettings();
|
||||||
|
const topicUpdateNarrationEnabled =
|
||||||
|
settings.merged.experimental?.topicUpdateNarration === true;
|
||||||
|
|
||||||
|
const suppressNarrationFlags = useMemo(() => {
|
||||||
|
const combinedHistory = [...uiState.history, ...pendingHistoryItems];
|
||||||
|
const flags = new Array<boolean>(combinedHistory.length).fill(false);
|
||||||
|
|
||||||
|
if (topicUpdateNarrationEnabled) {
|
||||||
|
let toolGroupInTurn = false;
|
||||||
|
for (let i = combinedHistory.length - 1; i >= 0; i--) {
|
||||||
|
const item = combinedHistory[i];
|
||||||
|
if (item.type === 'user' || item.type === 'user_shell') {
|
||||||
|
toolGroupInTurn = false;
|
||||||
|
} else if (item.type === 'tool_group') {
|
||||||
|
toolGroupInTurn = item.tools.some((t) => isTopicTool(t.name));
|
||||||
|
} else if (
|
||||||
|
(item.type === 'thinking' ||
|
||||||
|
item.type === 'gemini' ||
|
||||||
|
item.type === 'gemini_content') &&
|
||||||
|
toolGroupInTurn
|
||||||
|
) {
|
||||||
|
flags[i] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return flags;
|
||||||
|
}, [uiState.history, pendingHistoryItems, topicUpdateNarrationEnabled]);
|
||||||
|
|
||||||
const augmentedHistory = useMemo(
|
const augmentedHistory = useMemo(
|
||||||
() =>
|
() =>
|
||||||
uiState.history.map((item, index) => {
|
uiState.history.map((item, i) => {
|
||||||
const isExpandable = index > lastUserPromptIndex;
|
const prevType = i > 0 ? uiState.history[i - 1]?.type : undefined;
|
||||||
const prevType =
|
|
||||||
index > 0 ? uiState.history[index - 1]?.type : undefined;
|
|
||||||
const isFirstThinking =
|
const isFirstThinking =
|
||||||
item.type === 'thinking' && prevType !== 'thinking';
|
item.type === 'thinking' && prevType !== 'thinking';
|
||||||
const isFirstAfterThinking =
|
const isFirstAfterThinking =
|
||||||
@@ -76,18 +106,25 @@ export const MainContent = () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
item,
|
item,
|
||||||
isExpandable,
|
isExpandable: i > lastUserPromptIndex,
|
||||||
isFirstThinking,
|
isFirstThinking,
|
||||||
isFirstAfterThinking,
|
isFirstAfterThinking,
|
||||||
|
suppressNarration: suppressNarrationFlags[i] ?? false,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
[uiState.history, lastUserPromptIndex],
|
[uiState.history, lastUserPromptIndex, suppressNarrationFlags],
|
||||||
);
|
);
|
||||||
|
|
||||||
const historyItems = useMemo(
|
const historyItems = useMemo(
|
||||||
() =>
|
() =>
|
||||||
augmentedHistory.map(
|
augmentedHistory.map(
|
||||||
({ item, isExpandable, isFirstThinking, isFirstAfterThinking }) => (
|
({
|
||||||
|
item,
|
||||||
|
isExpandable,
|
||||||
|
isFirstThinking,
|
||||||
|
isFirstAfterThinking,
|
||||||
|
suppressNarration,
|
||||||
|
}) => (
|
||||||
<MemoizedHistoryItemDisplay
|
<MemoizedHistoryItemDisplay
|
||||||
terminalWidth={mainAreaWidth}
|
terminalWidth={mainAreaWidth}
|
||||||
availableTerminalHeight={
|
availableTerminalHeight={
|
||||||
@@ -103,6 +140,7 @@ export const MainContent = () => {
|
|||||||
isExpandable={isExpandable}
|
isExpandable={isExpandable}
|
||||||
isFirstThinking={isFirstThinking}
|
isFirstThinking={isFirstThinking}
|
||||||
isFirstAfterThinking={isFirstAfterThinking}
|
isFirstAfterThinking={isFirstAfterThinking}
|
||||||
|
suppressNarration={suppressNarration}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -138,6 +176,9 @@ export const MainContent = () => {
|
|||||||
const isFirstAfterThinking =
|
const isFirstAfterThinking =
|
||||||
item.type !== 'thinking' && prevType === 'thinking';
|
item.type !== 'thinking' && prevType === 'thinking';
|
||||||
|
|
||||||
|
const suppressNarration =
|
||||||
|
suppressNarrationFlags[uiState.history.length + i] ?? false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HistoryItemDisplay
|
<HistoryItemDisplay
|
||||||
key={`pending-${i}`}
|
key={`pending-${i}`}
|
||||||
@@ -150,6 +191,7 @@ export const MainContent = () => {
|
|||||||
isExpandable={true}
|
isExpandable={true}
|
||||||
isFirstThinking={isFirstThinking}
|
isFirstThinking={isFirstThinking}
|
||||||
isFirstAfterThinking={isFirstAfterThinking}
|
isFirstAfterThinking={isFirstAfterThinking}
|
||||||
|
suppressNarration={suppressNarration}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -169,6 +211,7 @@ export const MainContent = () => {
|
|||||||
showConfirmationQueue,
|
showConfirmationQueue,
|
||||||
confirmingTool,
|
confirmingTool,
|
||||||
uiState.history,
|
uiState.history,
|
||||||
|
suppressNarrationFlags,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -176,12 +219,19 @@ export const MainContent = () => {
|
|||||||
() => [
|
() => [
|
||||||
{ type: 'header' as const },
|
{ type: 'header' as const },
|
||||||
...augmentedHistory.map(
|
...augmentedHistory.map(
|
||||||
({ item, isExpandable, isFirstThinking, isFirstAfterThinking }) => ({
|
({
|
||||||
|
item,
|
||||||
|
isExpandable,
|
||||||
|
isFirstThinking,
|
||||||
|
isFirstAfterThinking,
|
||||||
|
suppressNarration,
|
||||||
|
}) => ({
|
||||||
type: 'history' as const,
|
type: 'history' as const,
|
||||||
item,
|
item,
|
||||||
isExpandable,
|
isExpandable,
|
||||||
isFirstThinking,
|
isFirstThinking,
|
||||||
isFirstAfterThinking,
|
isFirstAfterThinking,
|
||||||
|
suppressNarration,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
{ type: 'pending' as const },
|
{ type: 'pending' as const },
|
||||||
@@ -216,6 +266,7 @@ export const MainContent = () => {
|
|||||||
isExpandable={item.isExpandable}
|
isExpandable={item.isExpandable}
|
||||||
isFirstThinking={item.isFirstThinking}
|
isFirstThinking={item.isFirstThinking}
|
||||||
isFirstAfterThinking={item.isFirstAfterThinking}
|
isFirstAfterThinking={item.isFirstAfterThinking}
|
||||||
|
suppressNarration={item.suppressNarration}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -7,13 +7,10 @@
|
|||||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||||
import { ToolGroupMessage } from './ToolGroupMessage.js';
|
import { ToolGroupMessage } from './ToolGroupMessage.js';
|
||||||
import type {
|
|
||||||
HistoryItem,
|
|
||||||
HistoryItemWithoutId,
|
|
||||||
IndividualToolCallDisplay,
|
|
||||||
} from '../../types.js';
|
|
||||||
import { Scrollable } from '../shared/Scrollable.js';
|
|
||||||
import {
|
import {
|
||||||
|
UPDATE_TOPIC_TOOL_NAME,
|
||||||
|
TOPIC_PARAM_TITLE,
|
||||||
|
TOPIC_PARAM_STRATEGIC_INTENT,
|
||||||
makeFakeConfig,
|
makeFakeConfig,
|
||||||
CoreToolCallStatus,
|
CoreToolCallStatus,
|
||||||
ApprovalMode,
|
ApprovalMode,
|
||||||
@@ -23,6 +20,12 @@ import {
|
|||||||
READ_FILE_DISPLAY_NAME,
|
READ_FILE_DISPLAY_NAME,
|
||||||
GLOB_DISPLAY_NAME,
|
GLOB_DISPLAY_NAME,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
|
import type {
|
||||||
|
HistoryItem,
|
||||||
|
HistoryItemWithoutId,
|
||||||
|
IndividualToolCallDisplay,
|
||||||
|
} from '../../types.js';
|
||||||
|
import { Scrollable } from '../shared/Scrollable.js';
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import { createMockSettings } from '../../../test-utils/settings.js';
|
import { createMockSettings } from '../../../test-utils/settings.js';
|
||||||
|
|
||||||
@@ -36,6 +39,7 @@ describe('<ToolGroupMessage />', () => {
|
|||||||
): IndividualToolCallDisplay => ({
|
): IndividualToolCallDisplay => ({
|
||||||
callId: 'tool-123',
|
callId: 'tool-123',
|
||||||
name: 'test-tool',
|
name: 'test-tool',
|
||||||
|
args: {},
|
||||||
description: 'A tool for testing',
|
description: 'A tool for testing',
|
||||||
resultDisplay: 'Test result',
|
resultDisplay: 'Test result',
|
||||||
status: CoreToolCallStatus.Success,
|
status: CoreToolCallStatus.Success,
|
||||||
@@ -253,8 +257,71 @@ describe('<ToolGroupMessage />', () => {
|
|||||||
unmount();
|
unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders mixed tool calls including shell command', async () => {
|
it('renders update_topic tool call using TopicMessage', async () => {
|
||||||
const toolCalls = [
|
const toolCalls = [
|
||||||
|
createToolCall({
|
||||||
|
callId: 'topic-tool',
|
||||||
|
name: UPDATE_TOPIC_TOOL_NAME,
|
||||||
|
args: {
|
||||||
|
[TOPIC_PARAM_TITLE]: 'Testing Topic',
|
||||||
|
[TOPIC_PARAM_STRATEGIC_INTENT]: 'This is the description',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
const item = createItem(toolCalls);
|
||||||
|
|
||||||
|
const { lastFrame, unmount } = await renderWithProviders(
|
||||||
|
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||||
|
{
|
||||||
|
config: baseMockConfig,
|
||||||
|
settings: fullVerbositySettings,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const output = lastFrame();
|
||||||
|
expect(output).toContain('Testing Topic');
|
||||||
|
expect(output).toContain('— This is the description');
|
||||||
|
expect(output).toMatchSnapshot('update_topic_tool');
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders update_topic tool call with summary instead of strategic_intent', async () => {
|
||||||
|
const toolCalls = [
|
||||||
|
createToolCall({
|
||||||
|
callId: 'topic-tool-summary',
|
||||||
|
name: UPDATE_TOPIC_TOOL_NAME,
|
||||||
|
args: {
|
||||||
|
[TOPIC_PARAM_TITLE]: 'Testing Topic',
|
||||||
|
summary: 'This is the summary',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
const item = createItem(toolCalls);
|
||||||
|
|
||||||
|
const { lastFrame, unmount } = await renderWithProviders(
|
||||||
|
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||||
|
{
|
||||||
|
config: baseMockConfig,
|
||||||
|
settings: fullVerbositySettings,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const output = lastFrame();
|
||||||
|
expect(output).toContain('Testing Topic');
|
||||||
|
expect(output).toContain('— This is the summary');
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders mixed tool calls including update_topic', async () => {
|
||||||
|
const toolCalls = [
|
||||||
|
createToolCall({
|
||||||
|
callId: 'topic-tool-mixed',
|
||||||
|
name: UPDATE_TOPIC_TOOL_NAME,
|
||||||
|
args: {
|
||||||
|
[TOPIC_PARAM_TITLE]: 'Testing Topic',
|
||||||
|
[TOPIC_PARAM_STRATEGIC_INTENT]: 'This is the description',
|
||||||
|
},
|
||||||
|
}),
|
||||||
createToolCall({
|
createToolCall({
|
||||||
callId: 'tool-1',
|
callId: 'tool-1',
|
||||||
name: 'read_file',
|
name: 'read_file',
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import type {
|
|||||||
import { ToolCallStatus, mapCoreStatusToDisplayStatus } from '../../types.js';
|
import { ToolCallStatus, mapCoreStatusToDisplayStatus } from '../../types.js';
|
||||||
import { ToolMessage } from './ToolMessage.js';
|
import { ToolMessage } from './ToolMessage.js';
|
||||||
import { ShellToolMessage } from './ShellToolMessage.js';
|
import { ShellToolMessage } from './ShellToolMessage.js';
|
||||||
|
import { TopicMessage, isTopicTool } from './TopicMessage.js';
|
||||||
import { SubagentGroupDisplay } from './SubagentGroupDisplay.js';
|
import { SubagentGroupDisplay } from './SubagentGroupDisplay.js';
|
||||||
import { theme } from '../../semantic-colors.js';
|
import { theme } from '../../semantic-colors.js';
|
||||||
import { useConfig } from '../../contexts/ConfigContext.js';
|
import { useConfig } from '../../contexts/ConfigContext.js';
|
||||||
@@ -192,7 +193,20 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
|||||||
paddingRight={TOOL_MESSAGE_HORIZONTAL_MARGIN}
|
paddingRight={TOOL_MESSAGE_HORIZONTAL_MARGIN}
|
||||||
>
|
>
|
||||||
{groupedTools.map((group, index) => {
|
{groupedTools.map((group, index) => {
|
||||||
const isFirst = index === 0;
|
let isFirst = index === 0;
|
||||||
|
if (!isFirst) {
|
||||||
|
// Check if all previous tools were topics
|
||||||
|
let allPreviousWereTopics = true;
|
||||||
|
for (let i = 0; i < index; i++) {
|
||||||
|
const prevGroup = groupedTools[i];
|
||||||
|
if (Array.isArray(prevGroup) || !isTopicTool(prevGroup.name)) {
|
||||||
|
allPreviousWereTopics = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isFirst = allPreviousWereTopics;
|
||||||
|
}
|
||||||
|
|
||||||
const resolvedIsFirst =
|
const resolvedIsFirst =
|
||||||
borderTopOverride !== undefined
|
borderTopOverride !== undefined
|
||||||
? borderTopOverride && isFirst
|
? borderTopOverride && isFirst
|
||||||
@@ -215,6 +229,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
|||||||
|
|
||||||
const tool = group;
|
const tool = group;
|
||||||
const isShellToolCall = isShellTool(tool.name);
|
const isShellToolCall = isShellTool(tool.name);
|
||||||
|
const isTopicToolCall = isTopicTool(tool.name);
|
||||||
|
|
||||||
const commonProps = {
|
const commonProps = {
|
||||||
...tool,
|
...tool,
|
||||||
@@ -234,7 +249,9 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
|||||||
minHeight={1}
|
minHeight={1}
|
||||||
width={contentWidth}
|
width={contentWidth}
|
||||||
>
|
>
|
||||||
{isShellToolCall ? (
|
{isTopicToolCall ? (
|
||||||
|
<TopicMessage {...commonProps} />
|
||||||
|
) : isShellToolCall ? (
|
||||||
<ShellToolMessage {...commonProps} config={config} />
|
<ShellToolMessage {...commonProps} config={config} />
|
||||||
) : (
|
) : (
|
||||||
<ToolMessage {...commonProps} />
|
<ToolMessage {...commonProps} />
|
||||||
@@ -262,26 +279,26 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{
|
{/*
|
||||||
/*
|
We have to keep the bottom border separate so it doesn't get
|
||||||
We have to keep the bottom border separate so it doesn't get
|
drawn over by the sticky header directly inside it.
|
||||||
drawn over by the sticky header directly inside it.
|
*/}
|
||||||
*/
|
{(visibleToolCalls.length > 0 || borderBottomOverride !== undefined) &&
|
||||||
(visibleToolCalls.length > 0 || borderBottomOverride !== undefined) &&
|
borderBottomOverride !== false &&
|
||||||
borderBottomOverride !== false && (
|
(visibleToolCalls.length === 0 ||
|
||||||
<Box
|
!visibleToolCalls.every((tool) => isTopicTool(tool.name))) && (
|
||||||
height={0}
|
<Box
|
||||||
width={contentWidth}
|
height={0}
|
||||||
borderLeft={true}
|
width={contentWidth}
|
||||||
borderRight={true}
|
borderLeft={true}
|
||||||
borderTop={false}
|
borderRight={true}
|
||||||
borderBottom={borderBottomOverride ?? true}
|
borderTop={false}
|
||||||
borderColor={borderColor}
|
borderBottom={borderBottomOverride ?? true}
|
||||||
borderDimColor={borderDimColor}
|
borderColor={borderColor}
|
||||||
borderStyle="round"
|
borderDimColor={borderDimColor}
|
||||||
/>
|
borderStyle="round"
|
||||||
)
|
/>
|
||||||
}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type React from 'react';
|
||||||
|
import { Box, Text } from 'ink';
|
||||||
|
import {
|
||||||
|
UPDATE_TOPIC_TOOL_NAME,
|
||||||
|
UPDATE_TOPIC_DISPLAY_NAME,
|
||||||
|
TOPIC_PARAM_TITLE,
|
||||||
|
TOPIC_PARAM_SUMMARY,
|
||||||
|
TOPIC_PARAM_STRATEGIC_INTENT,
|
||||||
|
} from '@google/gemini-cli-core';
|
||||||
|
import type { IndividualToolCallDisplay } from '../../types.js';
|
||||||
|
import { theme } from '../../semantic-colors.js';
|
||||||
|
|
||||||
|
interface TopicMessageProps extends IndividualToolCallDisplay {
|
||||||
|
terminalWidth: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isTopicTool = (name: string): boolean =>
|
||||||
|
name === UPDATE_TOPIC_TOOL_NAME || name === UPDATE_TOPIC_DISPLAY_NAME;
|
||||||
|
|
||||||
|
export const TopicMessage: React.FC<TopicMessageProps> = ({ args }) => {
|
||||||
|
const rawTitle = args?.[TOPIC_PARAM_TITLE];
|
||||||
|
const title = typeof rawTitle === 'string' ? rawTitle : undefined;
|
||||||
|
const rawIntent =
|
||||||
|
args?.[TOPIC_PARAM_STRATEGIC_INTENT] || args?.[TOPIC_PARAM_SUMMARY];
|
||||||
|
const intent = typeof rawIntent === 'string' ? rawIntent : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flexDirection="row" marginLeft={2}>
|
||||||
|
<Text color={theme.text.primary} bold>
|
||||||
|
{title || 'Topic'}
|
||||||
|
</Text>
|
||||||
|
{intent && <Text color={theme.text.secondary}> — {intent}</Text>}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
+8
-2
@@ -74,8 +74,9 @@ exports[`<ToolGroupMessage /> > Golden Snapshots > renders header when scrolled
|
|||||||
"
|
"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders mixed tool calls including shell command 1`] = `
|
exports[`<ToolGroupMessage /> > Golden Snapshots > renders mixed tool calls including update_topic 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────╮
|
" Testing Topic — This is the description
|
||||||
|
╭──────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ✓ read_file Read a file │
|
│ ✓ read_file Read a file │
|
||||||
│ │
|
│ │
|
||||||
│ Test result │
|
│ Test result │
|
||||||
@@ -137,6 +138,11 @@ exports[`<ToolGroupMessage /> > Golden Snapshots > renders two tool groups where
|
|||||||
"
|
"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`<ToolGroupMessage /> > Golden Snapshots > renders update_topic tool call using TopicMessage > update_topic_tool 1`] = `
|
||||||
|
" Testing Topic — This is the description
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders with limited terminal height 1`] = `
|
exports[`<ToolGroupMessage /> > Golden Snapshots > renders with limited terminal height 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ✓ tool-with-result Tool with output │
|
│ ✓ tool-with-result Tool with output │
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export function mapToDisplay(
|
|||||||
callId: call.request.callId,
|
callId: call.request.callId,
|
||||||
parentCallId: call.request.parentCallId,
|
parentCallId: call.request.parentCallId,
|
||||||
name: displayName,
|
name: displayName,
|
||||||
|
args: call.request.args,
|
||||||
description,
|
description,
|
||||||
renderOutputAsMarkdown,
|
renderOutputAsMarkdown,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ import {
|
|||||||
Kind,
|
Kind,
|
||||||
ACTIVATE_SKILL_TOOL_NAME,
|
ACTIVATE_SKILL_TOOL_NAME,
|
||||||
shouldHideToolCall,
|
shouldHideToolCall,
|
||||||
|
UPDATE_TOPIC_TOOL_NAME,
|
||||||
|
UPDATE_TOPIC_DISPLAY_NAME,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import type {
|
import type {
|
||||||
Config,
|
Config,
|
||||||
@@ -108,6 +110,9 @@ interface BackgroundedToolInfo {
|
|||||||
initialOutput: string;
|
initialOutput: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isTopicTool = (name: string): boolean =>
|
||||||
|
name === UPDATE_TOPIC_TOOL_NAME || name === UPDATE_TOPIC_DISPLAY_NAME;
|
||||||
|
|
||||||
enum StreamProcessingStatus {
|
enum StreamProcessingStatus {
|
||||||
Completed,
|
Completed,
|
||||||
UserCancelled,
|
UserCancelled,
|
||||||
@@ -489,7 +494,17 @@ export const useGeminiStream = (
|
|||||||
addItem(historyItem);
|
addItem(historyItem);
|
||||||
|
|
||||||
setPushedToolCallIds(newPushed);
|
setPushedToolCallIds(newPushed);
|
||||||
setIsFirstToolInGroup(false);
|
|
||||||
|
// If this batch ONLY contains topics, and we were the first in the group,
|
||||||
|
// the NEXT batch is still effectively the first VISIBLE bordered tool in the group.
|
||||||
|
if (
|
||||||
|
isFirstToolInGroupRef.current &&
|
||||||
|
toolsToPush.every((tc) => isTopicTool(tc.request.name))
|
||||||
|
) {
|
||||||
|
// Keep it true!
|
||||||
|
} else {
|
||||||
|
setIsFirstToolInGroup(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
toolCalls,
|
toolCalls,
|
||||||
@@ -502,7 +517,6 @@ export const useGeminiStream = (
|
|||||||
isShellFocused,
|
isShellFocused,
|
||||||
backgroundTasks,
|
backgroundTasks,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const pendingToolGroupItems = useMemo((): HistoryItemWithoutId[] => {
|
const pendingToolGroupItems = useMemo((): HistoryItemWithoutId[] => {
|
||||||
const remainingTools = toolCalls.filter(
|
const remainingTools = toolCalls.filter(
|
||||||
(tc) => !pushedToolCallIds.has(tc.request.callId),
|
(tc) => !pushedToolCallIds.has(tc.request.callId),
|
||||||
@@ -519,15 +533,26 @@ export const useGeminiStream = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (remainingTools.length > 0) {
|
if (remainingTools.length > 0) {
|
||||||
|
// Should we draw a top border? Yes if NO previous tools were drawn,
|
||||||
|
// OR if ALL previously drawn tools were topics (which don't draw top borders).
|
||||||
|
let needsTopBorder = pushedToolCallIds.size === 0;
|
||||||
|
if (!needsTopBorder) {
|
||||||
|
const allPushedWereTopics = toolCalls
|
||||||
|
.filter((tc) => pushedToolCallIds.has(tc.request.callId))
|
||||||
|
.every((tc) => isTopicTool(tc.request.name));
|
||||||
|
if (allPushedWereTopics) {
|
||||||
|
needsTopBorder = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
items.push(
|
items.push(
|
||||||
mapTrackedToolCallsToDisplay(remainingTools, {
|
mapTrackedToolCallsToDisplay(remainingTools, {
|
||||||
borderTop: pushedToolCallIds.size === 0,
|
borderTop: needsTopBorder,
|
||||||
borderBottom: false, // Stay open to connect with the slice below
|
borderBottom: false, // Stay open to connect with the slice below
|
||||||
...appearance,
|
...appearance,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always show a bottom border slice if we have ANY tools in the batch
|
// Always show a bottom border slice if we have ANY tools in the batch
|
||||||
// and we haven't finished pushing the whole batch to history yet.
|
// and we haven't finished pushing the whole batch to history yet.
|
||||||
// Once all tools are terminal and pushed, the last history item handles the closing border.
|
// Once all tools are terminal and pushed, the last history item handles the closing border.
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ export interface IndividualToolCallDisplay {
|
|||||||
callId: string;
|
callId: string;
|
||||||
parentCallId?: string;
|
parentCallId?: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
args?: Record<string, unknown>;
|
||||||
description: string;
|
description: string;
|
||||||
resultDisplay: ToolResultDisplay | undefined;
|
resultDisplay: ToolResultDisplay | undefined;
|
||||||
status: CoreToolCallStatus;
|
status: CoreToolCallStatus;
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ export const PARAM_ADDITIONAL_PERMISSIONS = 'additional_permissions';
|
|||||||
|
|
||||||
// -- update_topic --
|
// -- update_topic --
|
||||||
export const UPDATE_TOPIC_TOOL_NAME = 'update_topic';
|
export const UPDATE_TOPIC_TOOL_NAME = 'update_topic';
|
||||||
|
export const UPDATE_TOPIC_DISPLAY_NAME = 'Update Topic Context';
|
||||||
export const TOPIC_PARAM_TITLE = 'title';
|
export const TOPIC_PARAM_TITLE = 'title';
|
||||||
export const TOPIC_PARAM_SUMMARY = 'summary';
|
export const TOPIC_PARAM_SUMMARY = 'summary';
|
||||||
export const TOPIC_PARAM_STRATEGIC_INTENT = 'strategic_intent';
|
export const TOPIC_PARAM_STRATEGIC_INTENT = 'strategic_intent';
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export {
|
|||||||
EXIT_PLAN_MODE_TOOL_NAME,
|
EXIT_PLAN_MODE_TOOL_NAME,
|
||||||
ENTER_PLAN_MODE_TOOL_NAME,
|
ENTER_PLAN_MODE_TOOL_NAME,
|
||||||
UPDATE_TOPIC_TOOL_NAME,
|
UPDATE_TOPIC_TOOL_NAME,
|
||||||
|
UPDATE_TOPIC_DISPLAY_NAME,
|
||||||
// Shared parameter names
|
// Shared parameter names
|
||||||
PARAM_FILE_PATH,
|
PARAM_FILE_PATH,
|
||||||
PARAM_DIR_PATH,
|
PARAM_DIR_PATH,
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ import {
|
|||||||
EXIT_PLAN_PARAM_PLAN_FILENAME,
|
EXIT_PLAN_PARAM_PLAN_FILENAME,
|
||||||
SKILL_PARAM_NAME,
|
SKILL_PARAM_NAME,
|
||||||
UPDATE_TOPIC_TOOL_NAME,
|
UPDATE_TOPIC_TOOL_NAME,
|
||||||
|
UPDATE_TOPIC_DISPLAY_NAME,
|
||||||
TOPIC_PARAM_TITLE,
|
TOPIC_PARAM_TITLE,
|
||||||
TOPIC_PARAM_SUMMARY,
|
TOPIC_PARAM_SUMMARY,
|
||||||
TOPIC_PARAM_STRATEGIC_INTENT,
|
TOPIC_PARAM_STRATEGIC_INTENT,
|
||||||
@@ -100,6 +101,7 @@ export {
|
|||||||
EXIT_PLAN_MODE_TOOL_NAME,
|
EXIT_PLAN_MODE_TOOL_NAME,
|
||||||
ENTER_PLAN_MODE_TOOL_NAME,
|
ENTER_PLAN_MODE_TOOL_NAME,
|
||||||
UPDATE_TOPIC_TOOL_NAME,
|
UPDATE_TOPIC_TOOL_NAME,
|
||||||
|
UPDATE_TOPIC_DISPLAY_NAME,
|
||||||
// Shared parameter names
|
// Shared parameter names
|
||||||
PARAM_FILE_PATH,
|
PARAM_FILE_PATH,
|
||||||
PARAM_DIR_PATH,
|
PARAM_DIR_PATH,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
UPDATE_TOPIC_TOOL_NAME,
|
UPDATE_TOPIC_TOOL_NAME,
|
||||||
|
UPDATE_TOPIC_DISPLAY_NAME,
|
||||||
TOPIC_PARAM_TITLE,
|
TOPIC_PARAM_TITLE,
|
||||||
TOPIC_PARAM_SUMMARY,
|
TOPIC_PARAM_SUMMARY,
|
||||||
TOPIC_PARAM_STRATEGIC_INTENT,
|
TOPIC_PARAM_STRATEGIC_INTENT,
|
||||||
@@ -110,7 +111,7 @@ export class UpdateTopicTool extends BaseDeclarativeTool<
|
|||||||
const declaration = getUpdateTopicDeclaration();
|
const declaration = getUpdateTopicDeclaration();
|
||||||
super(
|
super(
|
||||||
UPDATE_TOPIC_TOOL_NAME,
|
UPDATE_TOPIC_TOOL_NAME,
|
||||||
'Update Topic Context',
|
UPDATE_TOPIC_DISPLAY_NAME,
|
||||||
declaration.description ?? '',
|
declaration.description ?? '',
|
||||||
Kind.Think,
|
Kind.Think,
|
||||||
declaration.parametersJsonSchema,
|
declaration.parametersJsonSchema,
|
||||||
|
|||||||
Reference in New Issue
Block a user