Implement compact UX for topic.

This commit is contained in:
Christian Gunderman
2026-03-26 22:27:03 -07:00
parent c69969a8d3
commit f4a707be18
7 changed files with 145 additions and 20 deletions
@@ -17,6 +17,10 @@ import {
isGrepResult,
isListResult,
isReadManyFilesResult,
UPDATE_TOPIC_DISPLAY_NAME,
UPDATE_TOPIC_TOOL_NAME,
TOPIC_PARAM_TITLE,
TOPIC_PARAM_STRATEGIC_INTENT,
} from '@google/gemini-cli-core';
import { type IndividualToolCallDisplay, isTodoList } from '../../types.js';
import { useAlternateBuffer } from '../../hooks/useAlternateBuffer.js';
@@ -306,10 +310,33 @@ function getGenericSuccessData(
return { description, summary, payload };
}
function getUpdateTopicData(args?: Record<string, unknown>): ViewParts {
const rawTitle = args?.[TOPIC_PARAM_TITLE];
const title = typeof rawTitle === 'string' ? rawTitle : undefined;
const rawIntent = args?.[TOPIC_PARAM_STRATEGIC_INTENT];
const intent = typeof rawIntent === 'string' ? rawIntent : undefined;
const description = (
<Text color={theme.text.primary} bold wrap="truncate-end">
{title || 'Topic'}
</Text>
);
const summary = intent ? (
<Text color={theme.text.secondary} wrap="truncate-end">
{' '}
-- {intent}
</Text>
) : undefined;
return { description, summary };
}
export const DenseToolMessage: React.FC<DenseToolMessageProps> = (props) => {
const {
callId,
name,
args,
status,
resultDisplay,
confirmationDetails,
@@ -323,6 +350,8 @@ export const DenseToolMessage: React.FC<DenseToolMessageProps> = (props) => {
const isAlternateBuffer = useAlternateBuffer();
const { isExpanded: isExpandedInContext, toggleExpansion } = useToolActions();
// ... (rest of component)
// Handle optional context members
const [localIsExpanded, setLocalIsExpanded] = useState(false);
const isExpanded = isExpandedInContext
@@ -370,6 +399,9 @@ export const DenseToolMessage: React.FC<DenseToolMessageProps> = (props) => {
// State-to-View Coordination
const viewParts = useMemo((): ViewParts => {
if (name === UPDATE_TOPIC_DISPLAY_NAME || name === UPDATE_TOPIC_TOOL_NAME) {
return getUpdateTopicData(args);
}
if (diff) {
return getFileOpData(
diff,
@@ -431,6 +463,8 @@ export const DenseToolMessage: React.FC<DenseToolMessageProps> = (props) => {
availableTerminalHeight,
originalDescription,
isAlternateBuffer,
name,
args,
]);
const { description, summary } = viewParts;
@@ -497,25 +531,42 @@ export const DenseToolMessage: React.FC<DenseToolMessageProps> = (props) => {
return (
<Box flexDirection="column">
<Box marginLeft={2} flexDirection="row" flexWrap="wrap">
<ToolStatusIndicator status={status} name={name} />
<Box maxWidth={25} flexShrink={1} flexGrow={0}>
<Text color={theme.text.primary} bold wrap="truncate-end">
{name}{' '}
</Text>
</Box>
<Box marginLeft={1} flexShrink={1} flexGrow={0}>
{description}
</Box>
{summary && (
<Box
key="tool-summary"
ref={isAlternateBuffer && diff ? toggleRef : undefined}
marginLeft={1}
flexGrow={0}
>
{summary}
</Box>
<Box marginLeft={2} flexDirection="row">
{name === UPDATE_TOPIC_DISPLAY_NAME ||
name === UPDATE_TOPIC_TOOL_NAME ? (
<>
<Box flexShrink={1} flexGrow={0}>
{description}
</Box>
{summary && (
<Box marginLeft={1} flexGrow={0} flexShrink={1}>
{summary}
</Box>
)}
</>
) : (
<>
<ToolStatusIndicator status={status} name={name} />
<Box maxWidth={25} flexShrink={1} flexGrow={0}>
<Text color={theme.text.primary} bold wrap="truncate-end">
{name}{' '}
</Text>
</Box>
<Box marginLeft={1} flexShrink={1} flexGrow={0}>
{description}
</Box>
{summary && (
<Box
key="tool-summary"
ref={isAlternateBuffer && diff ? toggleRef : undefined}
marginLeft={1}
flexGrow={0}
flexShrink={1}
>
{summary}
</Box>
)}
</>
)}
</Box>
@@ -22,6 +22,9 @@ import {
EDIT_DISPLAY_NAME,
READ_FILE_DISPLAY_NAME,
GLOB_DISPLAY_NAME,
UPDATE_TOPIC_DISPLAY_NAME,
TOPIC_PARAM_TITLE,
TOPIC_PARAM_STRATEGIC_INTENT,
} from '@google/gemini-cli-core';
import os from 'node:os';
import { createMockSettings } from '../../../test-utils/settings.js';
@@ -464,6 +467,60 @@ describe('<ToolGroupMessage />', () => {
unmount();
});
describe('UpdateTopicTool', () => {
it('renders update_topic with Title -- Description format even when compact mode is disabled', async () => {
const toolCalls = [
createToolCall({
name: UPDATE_TOPIC_DISPLAY_NAME,
args: {
[TOPIC_PARAM_TITLE]: 'Research',
[TOPIC_PARAM_STRATEGIC_INTENT]: 'Researching Agent Skills',
},
}),
];
const item = createItem(toolCalls);
const { lastFrame, unmount } = await renderWithProviders(
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
{
config: baseMockConfig,
settings: createMockSettings({
ui: { compactToolOutput: false },
}),
},
);
const output = lastFrame();
expect(output).toContain('Research');
expect(output).toContain('-- Researching Agent Skills');
expect(output).not.toContain('update_topic');
unmount();
});
it('renders update_topic with Topic -- Description format when title is missing', async () => {
const toolCalls = [
createToolCall({
name: UPDATE_TOPIC_DISPLAY_NAME,
args: {
[TOPIC_PARAM_STRATEGIC_INTENT]: 'Tactical update',
},
}),
];
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('Topic');
expect(output).toContain('-- Tactical update');
unmount();
});
});
it('renders two tool groups where only the last line of the previous group is visible', async () => {
const toolCalls1 = [
createToolCall({
@@ -33,6 +33,8 @@ import {
WEB_FETCH_DISPLAY_NAME,
WRITE_FILE_DISPLAY_NAME,
READ_MANY_FILES_DISPLAY_NAME,
UPDATE_TOPIC_DISPLAY_NAME,
UPDATE_TOPIC_TOOL_NAME,
isFileDiff,
isGrepResult,
isListResult,
@@ -51,6 +53,8 @@ const COMPACT_OUTPUT_ALLOWLIST = new Set([
WEB_FETCH_DISPLAY_NAME,
WRITE_FILE_DISPLAY_NAME,
READ_MANY_FILES_DISPLAY_NAME,
UPDATE_TOPIC_DISPLAY_NAME,
UPDATE_TOPIC_TOOL_NAME,
]);
// Helper to identify if a tool should use the compact view
@@ -60,6 +64,15 @@ export const isCompactTool = (
): boolean => {
const hasCompactOutputSupport = COMPACT_OUTPUT_ALLOWLIST.has(tool.name);
const displayStatus = mapCoreStatusToDisplayStatus(tool.status);
// update_topic always uses the dense/compact representation
if (
tool.name === UPDATE_TOPIC_DISPLAY_NAME ||
tool.name === UPDATE_TOPIC_TOOL_NAME
) {
return displayStatus !== ToolCallStatus.Confirming;
}
return (
isCompactModeEnabled &&
hasCompactOutputSupport &&
+1
View File
@@ -51,6 +51,7 @@ export function mapToDisplay(
parentCallId: call.request.parentCallId,
name: displayName,
description,
args: call.request.args,
renderOutputAsMarkdown,
};
+1
View File
@@ -119,6 +119,7 @@ export interface IndividualToolCallDisplay {
parentCallId?: string;
name: string;
description: string;
args?: Record<string, unknown>;
resultDisplay: ToolResultDisplay | undefined;
status: CoreToolCallStatus;
// True when the tool was initiated directly by the user (slash/@/shell flows).
+1
View File
@@ -193,6 +193,7 @@ export const GREP_DISPLAY_NAME = 'SearchText';
export const WEB_SEARCH_DISPLAY_NAME = 'GoogleSearch';
export const WEB_FETCH_DISPLAY_NAME = 'WebFetch';
export const READ_MANY_FILES_DISPLAY_NAME = 'ReadManyFiles';
export const UPDATE_TOPIC_DISPLAY_NAME = 'Update Topic Context';
/**
* Mapping of legacy tool names to their current names.
+2 -1
View File
@@ -10,6 +10,7 @@ import {
TOPIC_PARAM_SUMMARY,
TOPIC_PARAM_STRATEGIC_INTENT,
} from './definitions/coreTools.js';
import { UPDATE_TOPIC_DISPLAY_NAME } from './tool-names.js';
import {
BaseDeclarativeTool,
BaseToolInvocation,
@@ -153,7 +154,7 @@ export class UpdateTopicTool extends BaseDeclarativeTool<
const declaration = getUpdateTopicDeclaration();
super(
UPDATE_TOPIC_TOOL_NAME,
'Update Topic Context',
UPDATE_TOPIC_DISPLAY_NAME,
declaration.description ?? '',
Kind.Think,
declaration.parametersJsonSchema,