mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-01 19:03:42 +00:00
feat(cli): support 'notice' format and refine declarative tool rendering
Key enhancements: - Updated `UpdateTopicTool` to provide declarative 'notice' display info, using dynamic descriptions for high-fidelity output. - Refined `ToolGroupDisplay` to 'hoist' notice-format tools to the top of the group. - Implemented conditional boxing in `ToolGroupDisplay`: borders are now suppressed in compact mode, matching the standard CLI view. - Added support for `resultSummary` rendering at the bottom of text results in boxed mode. - Improved `useAgentStream` to wait for turn completion before pushing tools to history, ensuring all notices for a turn are correctly grouped and hoisted together. - Fixed margin and border logic to handle seamless transitions between notices and tool boxes.
This commit is contained in:
@@ -25,6 +25,9 @@ export const ToolGroupDisplay: React.FC<ToolGroupDisplayProps> = ({
|
||||
item,
|
||||
isToolGroupBoundary,
|
||||
}) => {
|
||||
const settings = useSettings();
|
||||
const isCompactModeEnabled = settings.merged.ui?.compactToolOutput === true;
|
||||
|
||||
if (item.type !== 'tool_display_group') {
|
||||
return null;
|
||||
}
|
||||
@@ -32,23 +35,68 @@ export const ToolGroupDisplay: React.FC<ToolGroupDisplayProps> = ({
|
||||
const { tools, borderColor, borderDimColor, borderTop, borderBottom } =
|
||||
item as HistoryItemToolDisplayGroup;
|
||||
|
||||
const noticeTools = tools.filter((t) => t.format === 'notice');
|
||||
const otherTools = tools.filter(
|
||||
(t) => t.format !== 'notice' && t.format !== 'hidden',
|
||||
);
|
||||
|
||||
const hasOtherTools = otherTools.length > 0;
|
||||
const isClosingSlice = tools.length === 0 && borderBottom;
|
||||
|
||||
// Standard view behavior: If compact mode is enabled, non-notice tools
|
||||
// are typically rendered without an outer box.
|
||||
const shouldShowBox =
|
||||
(hasOtherTools || isClosingSlice) && !isCompactModeEnabled;
|
||||
|
||||
const boxBorderTop = borderTop || noticeTools.length > 0;
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor={borderColor}
|
||||
borderDimColor={borderDimColor}
|
||||
borderTop={borderTop}
|
||||
borderBottom={borderBottom}
|
||||
borderLeft={!isToolGroupBoundary}
|
||||
borderRight={!isToolGroupBoundary}
|
||||
marginTop={borderTop ? 1 : 0}
|
||||
marginBottom={borderBottom ? 1 : 0}
|
||||
paddingX={1}
|
||||
>
|
||||
{tools.map((tool, index) => (
|
||||
<ToolDisplayMessage key={index} tool={tool} />
|
||||
))}
|
||||
<Box flexDirection="column">
|
||||
{noticeTools.map((tool, index) => {
|
||||
const isFirstInGroup = index === 0 && borderTop;
|
||||
const isLastElementInGroup =
|
||||
index === noticeTools.length - 1 && !shouldShowBox && borderBottom;
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={`notice-${index}`}
|
||||
marginTop={isFirstInGroup ? 1 : index > 0 ? 1 : 0}
|
||||
marginBottom={isLastElementInGroup ? 1 : 0}
|
||||
>
|
||||
<ToolDisplayMessage tool={tool} />
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
{shouldShowBox ? (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor={borderColor}
|
||||
borderDimColor={borderDimColor}
|
||||
borderTop={boxBorderTop}
|
||||
borderBottom={borderBottom}
|
||||
borderLeft={!isToolGroupBoundary}
|
||||
borderRight={!isToolGroupBoundary}
|
||||
marginTop={boxBorderTop ? 1 : 0}
|
||||
marginBottom={borderBottom ? 1 : 0}
|
||||
paddingX={1}
|
||||
>
|
||||
{otherTools.map((tool, index) => (
|
||||
<ToolDisplayMessage key={`tool-${index}`} tool={tool} />
|
||||
))}
|
||||
</Box>
|
||||
) : otherTools.length > 0 ? (
|
||||
// Compact mode or no tools to box
|
||||
<Box
|
||||
flexDirection="column"
|
||||
marginTop={noticeTools.length > 0 ? 1 : 0}
|
||||
marginBottom={borderBottom ? 1 : 0}
|
||||
>
|
||||
{otherTools.map((tool, index) => (
|
||||
<ToolDisplayMessage key={`tool-${index}`} tool={tool} />
|
||||
))}
|
||||
</Box>
|
||||
) : null}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -90,6 +138,21 @@ const ToolDisplayMessage: React.FC<ToolDisplayMessageProps> = ({ tool }) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (format === 'notice') {
|
||||
return (
|
||||
<Box paddingLeft={2} flexDirection="column">
|
||||
<Text color={theme.text.primary} bold wrap="truncate-end">
|
||||
{name || 'Topic'}:
|
||||
</Text>
|
||||
{description && (
|
||||
<Text color={theme.text.secondary} wrap="wrap">
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const isCompact =
|
||||
format === 'compact' || (format === 'auto' && isCompactModeEnabled);
|
||||
|
||||
@@ -153,7 +216,16 @@ const ToolResultDisplayContent: React.FC<ToolResultDisplayContentProps> = ({
|
||||
|
||||
switch (content.type) {
|
||||
case 'text':
|
||||
return <Text color={theme.text.secondary}>{content.text}</Text>;
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text color={theme.text.secondary}>{content.text}</Text>
|
||||
{summary && (
|
||||
<Box marginTop={1}>
|
||||
<Text color={theme.text.secondary}>{summary}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
case 'diff':
|
||||
// Simplified diff display for now
|
||||
return (
|
||||
|
||||
@@ -77,6 +77,8 @@ export const useAgentStream = ({
|
||||
useStateAndRef<Set<string>>(new Set());
|
||||
const [_isFirstToolInGroup, isFirstToolInGroupRef, setIsFirstToolInGroup] =
|
||||
useStateAndRef<boolean>(true);
|
||||
const [_hasEmittedBoxInTurn, hasEmittedBoxInTurnRef, setHasEmittedBoxInTurn] =
|
||||
useStateAndRef<boolean>(false);
|
||||
|
||||
const { startNewPrompt } = useSessionStats();
|
||||
|
||||
@@ -381,32 +383,27 @@ export const useAgentStream = ({
|
||||
|
||||
// Push completed tools to history
|
||||
useEffect(() => {
|
||||
const toolsToPush: IndividualToolCallDisplay[] = [];
|
||||
for (let i = 0; i < trackedTools.length; i++) {
|
||||
const tc = trackedTools[i];
|
||||
if (pushedToolCallIdsRef.current.has(tc.callId)) continue;
|
||||
if (trackedTools.length === 0) return;
|
||||
|
||||
if (
|
||||
// We only push to history once all currently known tools in the turn are terminal.
|
||||
// This allows ToolGroupDisplay to correctly hoist ALL notices (topics) for the turn.
|
||||
const allTerminal = trackedTools.every(
|
||||
(tc) =>
|
||||
tc.status === 'success' ||
|
||||
tc.status === 'error' ||
|
||||
tc.status === 'cancelled'
|
||||
) {
|
||||
toolsToPush.push(tc);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
tc.status === 'cancelled',
|
||||
);
|
||||
|
||||
if (toolsToPush.length > 0) {
|
||||
const toolsToPush = trackedTools.filter(
|
||||
(tc) => !pushedToolCallIdsRef.current.has(tc.callId),
|
||||
);
|
||||
|
||||
if (allTerminal && toolsToPush.length > 0) {
|
||||
const newPushed = new Set(pushedToolCallIdsRef.current);
|
||||
for (const tc of toolsToPush) {
|
||||
newPushed.add(tc.callId);
|
||||
}
|
||||
|
||||
const isLastInBatch =
|
||||
toolsToPush[toolsToPush.length - 1] ===
|
||||
trackedTools[trackedTools.length - 1];
|
||||
|
||||
const appearance = getToolGroupBorderAppearance(
|
||||
{ type: 'tool_group', tools: trackedTools },
|
||||
activePtyId,
|
||||
@@ -415,6 +412,13 @@ export const useAgentStream = ({
|
||||
backgroundTasks,
|
||||
);
|
||||
|
||||
const hasBoxInBatch = toolsToPush.some(
|
||||
(tc) => tc.display?.format !== 'notice',
|
||||
);
|
||||
const shouldStartNewBlock =
|
||||
isFirstToolInGroupRef.current ||
|
||||
(!hasEmittedBoxInTurnRef.current && hasBoxInBatch);
|
||||
|
||||
const historyItem: HistoryItemToolDisplayGroup = {
|
||||
type: 'tool_display_group',
|
||||
tools: toolsToPush.map((tc) => ({
|
||||
@@ -424,21 +428,27 @@ export const useAgentStream = ({
|
||||
status: tc.status,
|
||||
originalRequestName: tc.originalRequestName,
|
||||
})),
|
||||
borderTop: isFirstToolInGroupRef.current,
|
||||
borderBottom: isLastInBatch,
|
||||
borderTop: shouldStartNewBlock,
|
||||
borderBottom: true,
|
||||
...appearance,
|
||||
};
|
||||
|
||||
addItem(historyItem);
|
||||
setPushedToolCallIds(newPushed);
|
||||
|
||||
if (hasBoxInBatch) {
|
||||
setHasEmittedBoxInTurn(true);
|
||||
}
|
||||
setIsFirstToolInGroup(false);
|
||||
}
|
||||
}, [
|
||||
trackedTools,
|
||||
pushedToolCallIdsRef,
|
||||
isFirstToolInGroupRef,
|
||||
hasEmittedBoxInTurnRef,
|
||||
setPushedToolCallIds,
|
||||
setIsFirstToolInGroup,
|
||||
setHasEmittedBoxInTurn,
|
||||
addItem,
|
||||
activePtyId,
|
||||
isShellFocused,
|
||||
@@ -447,7 +457,7 @@ export const useAgentStream = ({
|
||||
|
||||
const pendingToolGroupItems = useMemo((): HistoryItemWithoutId[] => {
|
||||
const remainingTools = trackedTools.filter(
|
||||
(tc) => !pushedToolCallIds.has(tc.callId),
|
||||
(tc) => !pushedToolCallIdsRef.current.has(tc.callId),
|
||||
);
|
||||
|
||||
const items: HistoryItemWithoutId[] = [];
|
||||
@@ -461,6 +471,13 @@ export const useAgentStream = ({
|
||||
);
|
||||
|
||||
if (remainingTools.length > 0) {
|
||||
const hasBoxInPending = remainingTools.some(
|
||||
(tc) => tc.display?.format !== 'notice',
|
||||
);
|
||||
const shouldStartNewBlock =
|
||||
pushedToolCallIds.size === 0 ||
|
||||
(!hasEmittedBoxInTurnRef.current && hasBoxInPending);
|
||||
|
||||
items.push({
|
||||
type: 'tool_display_group',
|
||||
tools: remainingTools.map((tc) => ({
|
||||
@@ -470,7 +487,7 @@ export const useAgentStream = ({
|
||||
status: tc.status,
|
||||
originalRequestName: tc.originalRequestName,
|
||||
})),
|
||||
borderTop: pushedToolCallIds.size === 0,
|
||||
borderTop: shouldStartNewBlock,
|
||||
borderBottom: false,
|
||||
...appearance,
|
||||
});
|
||||
@@ -510,6 +527,8 @@ export const useAgentStream = ({
|
||||
}, [
|
||||
trackedTools,
|
||||
pushedToolCallIds,
|
||||
pushedToolCallIdsRef,
|
||||
hasEmittedBoxInTurnRef,
|
||||
activePtyId,
|
||||
isShellFocused,
|
||||
backgroundTasks,
|
||||
|
||||
@@ -288,14 +288,6 @@ class LSToolInvocation extends BaseToolInvocation<LSToolParams, ToolResult> {
|
||||
name: LS_DISPLAY_NAME,
|
||||
description: this.getDescription(),
|
||||
resultSummary: displayMessage,
|
||||
result: {
|
||||
type: 'text',
|
||||
text: entries
|
||||
.map(
|
||||
(entry) => `${entry.isDirectory ? '[DIR] ' : ''}${entry.name}`,
|
||||
)
|
||||
.join('\n'),
|
||||
},
|
||||
},
|
||||
returnDisplay: {
|
||||
summary: displayMessage,
|
||||
|
||||
@@ -93,6 +93,11 @@ class UpdateTopicInvocation extends BaseToolInvocation<
|
||||
|
||||
return {
|
||||
llmContent,
|
||||
display: {
|
||||
format: 'notice',
|
||||
name: title || UPDATE_TOPIC_DISPLAY_NAME,
|
||||
description: this.getDescription(),
|
||||
},
|
||||
returnDisplay,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user