mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-29 15:30:40 -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;
|
||||
isFirstThinking?: boolean;
|
||||
isFirstAfterThinking?: boolean;
|
||||
suppressNarration?: boolean;
|
||||
}
|
||||
|
||||
export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
@@ -60,6 +61,7 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
isExpandable,
|
||||
isFirstThinking = false,
|
||||
isFirstAfterThinking = false,
|
||||
suppressNarration = false,
|
||||
}) => {
|
||||
const settings = useSettings();
|
||||
const inlineThinkingMode = getInlineThinkingMode(settings);
|
||||
@@ -68,6 +70,17 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
const needsTopMarginAfterThinking =
|
||||
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 (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
import { Box, Static } from 'ink';
|
||||
import { HistoryItemDisplay } from './HistoryItemDisplay.js';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
import { useAppContext } from '../contexts/AppContext.js';
|
||||
import { AppHeader } from './AppHeader.js';
|
||||
|
||||
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
|
||||
import {
|
||||
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 { useConfirmingTool } from '../hooks/useConfirmingTool.js';
|
||||
import { ToolConfirmationQueue } from './ToolConfirmationQueue.js';
|
||||
import { isTopicTool } from './messages/TopicMessage.js';
|
||||
|
||||
const MemoizedHistoryItemDisplay = memo(HistoryItemDisplay);
|
||||
const MemoizedAppHeader = memo(AppHeader);
|
||||
@@ -63,12 +66,39 @@ export const MainContent = () => {
|
||||
return -1;
|
||||
}, [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(
|
||||
() =>
|
||||
uiState.history.map((item, index) => {
|
||||
const isExpandable = index > lastUserPromptIndex;
|
||||
const prevType =
|
||||
index > 0 ? uiState.history[index - 1]?.type : undefined;
|
||||
uiState.history.map((item, i) => {
|
||||
const prevType = i > 0 ? uiState.history[i - 1]?.type : undefined;
|
||||
const isFirstThinking =
|
||||
item.type === 'thinking' && prevType !== 'thinking';
|
||||
const isFirstAfterThinking =
|
||||
@@ -76,18 +106,25 @@ export const MainContent = () => {
|
||||
|
||||
return {
|
||||
item,
|
||||
isExpandable,
|
||||
isExpandable: i > lastUserPromptIndex,
|
||||
isFirstThinking,
|
||||
isFirstAfterThinking,
|
||||
suppressNarration: suppressNarrationFlags[i] ?? false,
|
||||
};
|
||||
}),
|
||||
[uiState.history, lastUserPromptIndex],
|
||||
[uiState.history, lastUserPromptIndex, suppressNarrationFlags],
|
||||
);
|
||||
|
||||
const historyItems = useMemo(
|
||||
() =>
|
||||
augmentedHistory.map(
|
||||
({ item, isExpandable, isFirstThinking, isFirstAfterThinking }) => (
|
||||
({
|
||||
item,
|
||||
isExpandable,
|
||||
isFirstThinking,
|
||||
isFirstAfterThinking,
|
||||
suppressNarration,
|
||||
}) => (
|
||||
<MemoizedHistoryItemDisplay
|
||||
terminalWidth={mainAreaWidth}
|
||||
availableTerminalHeight={
|
||||
@@ -103,6 +140,7 @@ export const MainContent = () => {
|
||||
isExpandable={isExpandable}
|
||||
isFirstThinking={isFirstThinking}
|
||||
isFirstAfterThinking={isFirstAfterThinking}
|
||||
suppressNarration={suppressNarration}
|
||||
/>
|
||||
),
|
||||
),
|
||||
@@ -138,6 +176,9 @@ export const MainContent = () => {
|
||||
const isFirstAfterThinking =
|
||||
item.type !== 'thinking' && prevType === 'thinking';
|
||||
|
||||
const suppressNarration =
|
||||
suppressNarrationFlags[uiState.history.length + i] ?? false;
|
||||
|
||||
return (
|
||||
<HistoryItemDisplay
|
||||
key={`pending-${i}`}
|
||||
@@ -150,6 +191,7 @@ export const MainContent = () => {
|
||||
isExpandable={true}
|
||||
isFirstThinking={isFirstThinking}
|
||||
isFirstAfterThinking={isFirstAfterThinking}
|
||||
suppressNarration={suppressNarration}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -169,6 +211,7 @@ export const MainContent = () => {
|
||||
showConfirmationQueue,
|
||||
confirmingTool,
|
||||
uiState.history,
|
||||
suppressNarrationFlags,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -176,12 +219,19 @@ export const MainContent = () => {
|
||||
() => [
|
||||
{ type: 'header' as const },
|
||||
...augmentedHistory.map(
|
||||
({ item, isExpandable, isFirstThinking, isFirstAfterThinking }) => ({
|
||||
({
|
||||
item,
|
||||
isExpandable,
|
||||
isFirstThinking,
|
||||
isFirstAfterThinking,
|
||||
suppressNarration,
|
||||
}) => ({
|
||||
type: 'history' as const,
|
||||
item,
|
||||
isExpandable,
|
||||
isFirstThinking,
|
||||
isFirstAfterThinking,
|
||||
suppressNarration,
|
||||
}),
|
||||
),
|
||||
{ type: 'pending' as const },
|
||||
@@ -216,6 +266,7 @@ export const MainContent = () => {
|
||||
isExpandable={item.isExpandable}
|
||||
isFirstThinking={item.isFirstThinking}
|
||||
isFirstAfterThinking={item.isFirstAfterThinking}
|
||||
suppressNarration={item.suppressNarration}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -7,13 +7,10 @@
|
||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { ToolGroupMessage } from './ToolGroupMessage.js';
|
||||
import type {
|
||||
HistoryItem,
|
||||
HistoryItemWithoutId,
|
||||
IndividualToolCallDisplay,
|
||||
} from '../../types.js';
|
||||
import { Scrollable } from '../shared/Scrollable.js';
|
||||
import {
|
||||
UPDATE_TOPIC_TOOL_NAME,
|
||||
TOPIC_PARAM_TITLE,
|
||||
TOPIC_PARAM_STRATEGIC_INTENT,
|
||||
makeFakeConfig,
|
||||
CoreToolCallStatus,
|
||||
ApprovalMode,
|
||||
@@ -23,6 +20,12 @@ import {
|
||||
READ_FILE_DISPLAY_NAME,
|
||||
GLOB_DISPLAY_NAME,
|
||||
} 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 { createMockSettings } from '../../../test-utils/settings.js';
|
||||
|
||||
@@ -36,6 +39,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
): IndividualToolCallDisplay => ({
|
||||
callId: 'tool-123',
|
||||
name: 'test-tool',
|
||||
args: {},
|
||||
description: 'A tool for testing',
|
||||
resultDisplay: 'Test result',
|
||||
status: CoreToolCallStatus.Success,
|
||||
@@ -253,8 +257,71 @@ describe('<ToolGroupMessage />', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders mixed tool calls including shell command', async () => {
|
||||
it('renders update_topic tool call using TopicMessage', async () => {
|
||||
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({
|
||||
callId: 'tool-1',
|
||||
name: 'read_file',
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
import { ToolCallStatus, mapCoreStatusToDisplayStatus } from '../../types.js';
|
||||
import { ToolMessage } from './ToolMessage.js';
|
||||
import { ShellToolMessage } from './ShellToolMessage.js';
|
||||
import { TopicMessage, isTopicTool } from './TopicMessage.js';
|
||||
import { SubagentGroupDisplay } from './SubagentGroupDisplay.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { useConfig } from '../../contexts/ConfigContext.js';
|
||||
@@ -192,7 +193,20 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
paddingRight={TOOL_MESSAGE_HORIZONTAL_MARGIN}
|
||||
>
|
||||
{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 =
|
||||
borderTopOverride !== undefined
|
||||
? borderTopOverride && isFirst
|
||||
@@ -215,6 +229,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
|
||||
const tool = group;
|
||||
const isShellToolCall = isShellTool(tool.name);
|
||||
const isTopicToolCall = isTopicTool(tool.name);
|
||||
|
||||
const commonProps = {
|
||||
...tool,
|
||||
@@ -234,7 +249,9 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
minHeight={1}
|
||||
width={contentWidth}
|
||||
>
|
||||
{isShellToolCall ? (
|
||||
{isTopicToolCall ? (
|
||||
<TopicMessage {...commonProps} />
|
||||
) : isShellToolCall ? (
|
||||
<ShellToolMessage {...commonProps} config={config} />
|
||||
) : (
|
||||
<ToolMessage {...commonProps} />
|
||||
@@ -262,26 +279,26 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
{
|
||||
/*
|
||||
We have to keep the bottom border separate so it doesn't get
|
||||
drawn over by the sticky header directly inside it.
|
||||
*/
|
||||
(visibleToolCalls.length > 0 || borderBottomOverride !== undefined) &&
|
||||
borderBottomOverride !== false && (
|
||||
<Box
|
||||
height={0}
|
||||
width={contentWidth}
|
||||
borderLeft={true}
|
||||
borderRight={true}
|
||||
borderTop={false}
|
||||
borderBottom={borderBottomOverride ?? true}
|
||||
borderColor={borderColor}
|
||||
borderDimColor={borderDimColor}
|
||||
borderStyle="round"
|
||||
/>
|
||||
)
|
||||
}
|
||||
{/*
|
||||
We have to keep the bottom border separate so it doesn't get
|
||||
drawn over by the sticky header directly inside it.
|
||||
*/}
|
||||
{(visibleToolCalls.length > 0 || borderBottomOverride !== undefined) &&
|
||||
borderBottomOverride !== false &&
|
||||
(visibleToolCalls.length === 0 ||
|
||||
!visibleToolCalls.every((tool) => isTopicTool(tool.name))) && (
|
||||
<Box
|
||||
height={0}
|
||||
width={contentWidth}
|
||||
borderLeft={true}
|
||||
borderRight={true}
|
||||
borderTop={false}
|
||||
borderBottom={borderBottomOverride ?? true}
|
||||
borderColor={borderColor}
|
||||
borderDimColor={borderDimColor}
|
||||
borderStyle="round"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
|
||||
41
packages/cli/src/ui/components/messages/TopicMessage.tsx
Normal file
41
packages/cli/src/ui/components/messages/TopicMessage.tsx
Normal file
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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 │
|
||||
│ │
|
||||
│ 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`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ tool-with-result Tool with output │
|
||||
|
||||
@@ -50,6 +50,7 @@ export function mapToDisplay(
|
||||
callId: call.request.callId,
|
||||
parentCallId: call.request.parentCallId,
|
||||
name: displayName,
|
||||
args: call.request.args,
|
||||
description,
|
||||
renderOutputAsMarkdown,
|
||||
};
|
||||
|
||||
@@ -40,6 +40,8 @@ import {
|
||||
Kind,
|
||||
ACTIVATE_SKILL_TOOL_NAME,
|
||||
shouldHideToolCall,
|
||||
UPDATE_TOPIC_TOOL_NAME,
|
||||
UPDATE_TOPIC_DISPLAY_NAME,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type {
|
||||
Config,
|
||||
@@ -108,6 +110,9 @@ interface BackgroundedToolInfo {
|
||||
initialOutput: string;
|
||||
}
|
||||
|
||||
const isTopicTool = (name: string): boolean =>
|
||||
name === UPDATE_TOPIC_TOOL_NAME || name === UPDATE_TOPIC_DISPLAY_NAME;
|
||||
|
||||
enum StreamProcessingStatus {
|
||||
Completed,
|
||||
UserCancelled,
|
||||
@@ -489,7 +494,17 @@ export const useGeminiStream = (
|
||||
addItem(historyItem);
|
||||
|
||||
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,
|
||||
@@ -502,7 +517,6 @@ export const useGeminiStream = (
|
||||
isShellFocused,
|
||||
backgroundTasks,
|
||||
]);
|
||||
|
||||
const pendingToolGroupItems = useMemo((): HistoryItemWithoutId[] => {
|
||||
const remainingTools = toolCalls.filter(
|
||||
(tc) => !pushedToolCallIds.has(tc.request.callId),
|
||||
@@ -519,15 +533,26 @@ export const useGeminiStream = (
|
||||
);
|
||||
|
||||
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(
|
||||
mapTrackedToolCallsToDisplay(remainingTools, {
|
||||
borderTop: pushedToolCallIds.size === 0,
|
||||
borderTop: needsTopBorder,
|
||||
borderBottom: false, // Stay open to connect with the slice below
|
||||
...appearance,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Once all tools are terminal and pushed, the last history item handles the closing border.
|
||||
|
||||
@@ -118,6 +118,7 @@ export interface IndividualToolCallDisplay {
|
||||
callId: string;
|
||||
parentCallId?: string;
|
||||
name: string;
|
||||
args?: Record<string, unknown>;
|
||||
description: string;
|
||||
resultDisplay: ToolResultDisplay | undefined;
|
||||
status: CoreToolCallStatus;
|
||||
|
||||
@@ -128,6 +128,7 @@ export const PARAM_ADDITIONAL_PERMISSIONS = 'additional_permissions';
|
||||
|
||||
// -- 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_SUMMARY = 'summary';
|
||||
export const TOPIC_PARAM_STRATEGIC_INTENT = 'strategic_intent';
|
||||
|
||||
@@ -40,6 +40,7 @@ export {
|
||||
EXIT_PLAN_MODE_TOOL_NAME,
|
||||
ENTER_PLAN_MODE_TOOL_NAME,
|
||||
UPDATE_TOPIC_TOOL_NAME,
|
||||
UPDATE_TOPIC_DISPLAY_NAME,
|
||||
// Shared parameter names
|
||||
PARAM_FILE_PATH,
|
||||
PARAM_DIR_PATH,
|
||||
|
||||
@@ -76,6 +76,7 @@ import {
|
||||
EXIT_PLAN_PARAM_PLAN_FILENAME,
|
||||
SKILL_PARAM_NAME,
|
||||
UPDATE_TOPIC_TOOL_NAME,
|
||||
UPDATE_TOPIC_DISPLAY_NAME,
|
||||
TOPIC_PARAM_TITLE,
|
||||
TOPIC_PARAM_SUMMARY,
|
||||
TOPIC_PARAM_STRATEGIC_INTENT,
|
||||
@@ -100,6 +101,7 @@ export {
|
||||
EXIT_PLAN_MODE_TOOL_NAME,
|
||||
ENTER_PLAN_MODE_TOOL_NAME,
|
||||
UPDATE_TOPIC_TOOL_NAME,
|
||||
UPDATE_TOPIC_DISPLAY_NAME,
|
||||
// Shared parameter names
|
||||
PARAM_FILE_PATH,
|
||||
PARAM_DIR_PATH,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import {
|
||||
UPDATE_TOPIC_TOOL_NAME,
|
||||
UPDATE_TOPIC_DISPLAY_NAME,
|
||||
TOPIC_PARAM_TITLE,
|
||||
TOPIC_PARAM_SUMMARY,
|
||||
TOPIC_PARAM_STRATEGIC_INTENT,
|
||||
@@ -110,7 +111,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,
|
||||
|
||||
Reference in New Issue
Block a user