refactor(cli): migrate core tools to native ToolDisplay property and fix UI rendering (#25186)

This commit is contained in:
Michael Bleigh
2026-05-06 14:23:26 -07:00
committed by GitHub
parent 4a10751b49
commit 90304b279c
33 changed files with 1033 additions and 57 deletions
@@ -14,6 +14,7 @@ import { GeminiMessage } from './messages/GeminiMessage.js';
import { InfoMessage } from './messages/InfoMessage.js';
import { ErrorMessage } from './messages/ErrorMessage.js';
import { ToolGroupMessage } from './messages/ToolGroupMessage.js';
import { ToolGroupDisplay } from './messages/ToolGroupDisplay.js';
import { GeminiMessageContent } from './messages/GeminiMessageContent.js';
import { CompressionMessage } from './messages/CompressionMessage.js';
import { WarningMessage } from './messages/WarningMessage.js';
@@ -195,6 +196,12 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
isExpandable={isExpandable}
/>
)}
{itemForDisplay.type === 'tool_display_group' && (
<ToolGroupDisplay
item={itemForDisplay}
isToolGroupBoundary={isToolGroupBoundary}
/>
)}
{itemForDisplay.type === 'subagent' && (
<SubagentHistoryMessage
item={itemForDisplay}
@@ -0,0 +1,304 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, afterEach } from 'vitest';
import { renderWithProviders } from '../../../test-utils/render.js';
import { createMockSettings } from '../../../test-utils/settings.js';
import { ToolGroupDisplay } from './ToolGroupDisplay.js';
import {
CoreToolCallStatus,
UPDATE_TOPIC_DISPLAY_NAME,
} from '@google/gemini-cli-core';
import type {
HistoryItemToolDisplayGroup,
ToolDisplayItem,
} from '../../types.js';
describe('<ToolGroupDisplay />', () => {
afterEach(() => {
vi.restoreAllMocks();
});
const createToolItem = (
overrides: Partial<ToolDisplayItem> = {},
): ToolDisplayItem => ({
status: CoreToolCallStatus.Success,
name: 'test-tool',
description: 'Test description',
...overrides,
});
const createHistoryItem = (
tools: ToolDisplayItem[],
overrides: Partial<HistoryItemToolDisplayGroup> = {},
): HistoryItemToolDisplayGroup => ({
type: 'tool_display_group',
tools,
borderColor: 'gray',
borderDimColor: true,
borderTop: true,
borderBottom: true,
...overrides,
});
const fullVerbositySettings = createMockSettings({
ui: { errorVerbosity: 'full', compactToolOutput: false },
});
const compactSettings = createMockSettings({
ui: { compactToolOutput: true },
});
describe('Golden Snapshots', () => {
it('renders notices at the top (hoisting)', async () => {
const tools = [
createToolItem({ name: 'Tool A', format: 'box' }),
createToolItem({
name: UPDATE_TOPIC_DISPLAY_NAME,
description: 'New Topic',
format: 'notice',
}),
];
const item = createHistoryItem(tools);
const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
{ settings: fullVerbositySettings },
);
const output = lastFrame();
// Notice should be before Tool A
expect(output.indexOf(UPDATE_TOPIC_DISPLAY_NAME)).toBeLessThan(
output.indexOf('Tool A'),
);
expect(output).toMatchSnapshot();
});
it('renders in compact mode (no box borders)', async () => {
const tools = [
createToolItem({ name: 'Tool A' }),
createToolItem({ name: 'Tool B' }),
];
const item = createHistoryItem(tools);
const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
{ settings: compactSettings },
);
const output = lastFrame();
// Should not contain box drawing characters for the outer box
expect(output).not.toContain('╭');
expect(output).not.toContain('╰');
expect(output).toMatchSnapshot();
});
it('renders in boxed mode (full verbosity)', async () => {
const tools = [createToolItem({ name: 'Tool A' })];
const item = createHistoryItem(tools);
const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
{ settings: fullVerbositySettings },
);
const output = lastFrame();
expect(output).toContain('╭');
expect(output).toContain('╰');
expect(output).toMatchSnapshot();
});
it('renders standalone notices without a box', async () => {
const tools = [
createToolItem({
name: 'Notice Only',
format: 'notice',
}),
];
const item = createHistoryItem(tools);
const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
{ settings: fullVerbositySettings },
);
const output = lastFrame();
expect(output).not.toContain('╭');
expect(output).toMatchSnapshot();
});
it('renders error message when display info is missing', async () => {
// Create an item that effectively has no display properties
const tools = [
{
status: CoreToolCallStatus.Executing,
originalRequestName: 'missing-tool',
} as ToolDisplayItem,
];
const item = createHistoryItem(tools);
const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
);
const output = lastFrame();
expect(output).toContain('Error: Tool display missing');
expect(output).toMatchSnapshot();
});
it('hides tools awaiting approval (confirming)', async () => {
const tools = [
createToolItem({
name: 'Confirming Tool',
status: CoreToolCallStatus.AwaitingApproval,
}),
];
const item = createHistoryItem(tools);
const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
);
// Should render nothing (null)
expect(lastFrame({ allowEmpty: true })).toBe('');
});
});
describe('Result Formatting', () => {
it('renders text results with summary below', async () => {
const tools = [
createToolItem({
result: { type: 'text', text: 'Detailed output' },
resultSummary: 'Short summary',
format: 'box',
}),
];
const item = createHistoryItem(tools);
const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
{ settings: fullVerbositySettings },
);
const output = lastFrame();
expect(output).toContain('Detailed output');
expect(output).toContain('Short summary');
// Summary should be below detailed output
expect(output.indexOf('Detailed output')).toBeLessThan(
output.indexOf('Short summary'),
);
expect(output).toMatchSnapshot();
});
it('renders compact tools with summary on same line', async () => {
const tools = [
createToolItem({
resultSummary: 'Success summary',
format: 'compact',
}),
];
const item = createHistoryItem(tools);
const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
);
const output = lastFrame();
expect(output).toContain('→ Success summary');
expect(output).toMatchSnapshot();
});
it('renders placeholder for diff results', async () => {
const tools = [
createToolItem({
result: {
type: 'diff',
beforeText: 'old',
afterText: 'new',
path: 'file.ts',
},
}),
];
const item = createHistoryItem(tools);
const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
{ settings: fullVerbositySettings },
);
const output = lastFrame();
expect(output).toContain('[Diff Display: 3 -> 3 chars]');
expect(output).toMatchSnapshot();
});
it('renders placeholder for terminal results', async () => {
const tools = [
createToolItem({
result: { type: 'terminal' },
}),
];
const item = createHistoryItem(tools);
const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
{ settings: fullVerbositySettings },
);
expect(lastFrame()).toContain('[Terminal Output]');
});
it('renders placeholder for agent results', async () => {
const tools = [
createToolItem({
result: { type: 'agent', threadId: 'thread-123' },
}),
];
const item = createHistoryItem(tools);
const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
{ settings: fullVerbositySettings },
);
expect(lastFrame()).toContain('[Subagent: thread-123]');
});
});
describe('Border & Margin Logic', () => {
it('forces top border on box when it follows a notice', async () => {
const tools = [
createToolItem({ name: 'Notice', format: 'notice' }),
createToolItem({ name: 'Tool in Box', format: 'box' }),
];
// Even if item.borderTop is false (continuing a group),
// the box should have a top border because it follows a notice.
const item = createHistoryItem(tools, { borderTop: false });
const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
{ settings: fullVerbositySettings },
);
const output = lastFrame();
expect(output).toContain('Notice');
expect(output).toContain('╭'); // Top border for the box
expect(output).toMatchSnapshot();
});
it('applies bottom margin in compact mode when group is at boundary', async () => {
const tools = [createToolItem({ name: 'Compact Tool' })];
const item = createHistoryItem(tools, { borderBottom: true });
const { lastFrame } = await renderWithProviders(
<ToolGroupDisplay item={item} />,
{ settings: compactSettings },
);
// This is hard to assert via string check, but ensure match snapshot
// captures the vertical spacing.
expect(lastFrame()).toMatchSnapshot();
});
});
});
@@ -0,0 +1,266 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { Box, Text } from 'ink';
import { CoreToolCallStatus } from '@google/gemini-cli-core';
import type {
HistoryItem,
HistoryItemWithoutId,
HistoryItemToolDisplayGroup,
ToolDisplayItem,
} from '../../types.js';
import { theme } from '../../semantic-colors.js';
import { ToolStatusIndicator } from './ToolShared.js';
import { useSettings } from '../../contexts/SettingsContext.js';
interface ToolGroupDisplayProps {
item: HistoryItem | HistoryItemWithoutId;
isToolGroupBoundary?: boolean;
}
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;
}
const { tools, borderColor, borderDimColor, borderTop, borderBottom } =
item as HistoryItemToolDisplayGroup;
const visibleTools = tools.filter(
(t) => t.status !== CoreToolCallStatus.AwaitingApproval,
);
const noticeTools = visibleTools.filter((t) => t.format === 'notice');
const otherTools = visibleTools.filter(
(t) => t.format !== 'notice' && t.format !== 'hidden',
);
const hasOtherTools = otherTools.length > 0;
const isClosingSlice = tools.length === 0 && borderBottom;
// If no tools are visible and it's not an explicit closing slice, hide the group
if (visibleTools.length === 0 && !isClosingSlice) {
return null;
}
// 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">
{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>
);
};
interface ToolDisplayMessageProps {
tool: ToolDisplayItem;
}
const ToolDisplayMessage: React.FC<ToolDisplayMessageProps> = ({ tool }) => {
const settings = useSettings();
const isCompactModeEnabled = settings.merged.ui?.compactToolOutput === true;
// Since ToolDisplayItem is ToolDisplay & { status, ... }, we check for identifying properties
// of ToolDisplay. If name or description is missing and there's no result, it might be "empty".
if (!tool.name && !tool.description && !tool.result && !tool.resultSummary) {
return (
<Box paddingLeft={2}>
<ToolStatusIndicator
status={tool.status}
name={tool.originalRequestName || 'unknown'}
/>
<Text color={theme.status.error}> Error: Tool display missing</Text>
</Box>
);
}
const {
status,
format: preferredFormat,
name,
description,
resultSummary,
result,
} = tool;
const format = preferredFormat || 'auto';
if (format === 'hidden') {
return null;
}
if (format === 'notice') {
// If the name is part of the description (typical for topic updates),
// suppress the bold name to avoid redundancy and match legacy UI.
const isRedundant = !!(name && description?.includes(`"${name}"`));
return (
<Box paddingLeft={2} flexDirection="column">
{name && !isRedundant && (
<Text color={theme.text.primary} bold wrap="truncate-end">
{name}:
</Text>
)}
{description && (
<Text color={theme.text.secondary} wrap="wrap">
{description}
</Text>
)}
</Box>
);
}
const isCompact =
format === 'compact' || (format === 'auto' && isCompactModeEnabled);
if (isCompact) {
return (
<Box paddingLeft={2} flexDirection="row" flexWrap="wrap">
<ToolStatusIndicator
status={status}
name={name || tool.originalRequestName || ''}
/>
<Text bold color={theme.text.primary}>
{' '}
{name || tool.originalRequestName}{' '}
</Text>
{description && <Text color={theme.text.secondary}>{description}</Text>}
{resultSummary && (
<Text color={theme.text.accent}>
{' '}
{resultSummary.replace(/\n/g, ' ')}
</Text>
)}
</Box>
);
}
// Box format (full)
return (
<Box flexDirection="column" paddingLeft={0}>
<Box flexDirection="row" paddingLeft={1}>
<ToolStatusIndicator
status={status}
name={name || tool.originalRequestName || ''}
/>
<Text bold color={theme.text.primary}>
{' '}
{name || tool.originalRequestName}{' '}
</Text>
{description && <Text color={theme.text.secondary}>{description}</Text>}
</Box>
{resultSummary && !result && (
<Box paddingLeft={1} marginTop={1}>
<Text color={theme.text.primary}>{resultSummary}</Text>
</Box>
)}
{result && (
<Box paddingLeft={1} marginTop={1}>
<ToolResultDisplayContent content={result} summary={resultSummary} />
</Box>
)}
</Box>
);
};
interface ToolResultDisplayContentProps {
content: ToolDisplayItem['result'];
summary?: string | null;
}
const ToolResultDisplayContent: React.FC<ToolResultDisplayContentProps> = ({
content,
summary,
}) => {
if (!content) return null;
switch (content.type) {
case 'text':
return (
<Box flexDirection="column">
<Text color={theme.text.primary}>{content.text}</Text>
{summary && (
<Box marginTop={0}>
<Text color={theme.text.primary}>{summary}</Text>
</Box>
)}
</Box>
);
case 'diff':
// Simplified diff display for now
return (
<Box flexDirection="column">
{summary && <Text color={theme.text.primary}>{summary}</Text>}
<Text color={theme.text.secondary}>
{`[Diff Display: ${content.beforeText.length} -> ${content.afterText.length} chars]`}
</Text>
</Box>
);
case 'terminal':
return <Text color={theme.text.secondary}>[Terminal Output]</Text>;
case 'agent':
return (
<Text color={theme.text.secondary}>[Subagent: {content.threadId}]</Text>
);
default:
return null;
}
};
@@ -0,0 +1,79 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<ToolGroupDisplay /> > Border & Margin Logic > applies bottom margin in compact mode when group is at boundary 1`] = `
" ✓ Compact Tool Test description
"
`;
exports[`<ToolGroupDisplay /> > Border & Margin Logic > forces top border on box when it follows a notice 1`] = `
" Notice:
Test description
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ✓ Tool in Box Test description │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
exports[`<ToolGroupDisplay /> > Golden Snapshots > renders error message when display info is missing 1`] = `
" ⊶ Error: Tool display missing
"
`;
exports[`<ToolGroupDisplay /> > Golden Snapshots > renders in boxed mode (full verbosity) 1`] = `
"
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ✓ Tool A Test description │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
exports[`<ToolGroupDisplay /> > Golden Snapshots > renders in compact mode (no box borders) 1`] = `
" ✓ Tool A Test description
✓ Tool B Test description
"
`;
exports[`<ToolGroupDisplay /> > Golden Snapshots > renders notices at the top (hoisting) 1`] = `
"
Update Topic Context:
New Topic
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ✓ Tool A Test description │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
exports[`<ToolGroupDisplay /> > Golden Snapshots > renders standalone notices without a box 1`] = `
"
Notice Only:
Test description
"
`;
exports[`<ToolGroupDisplay /> > Result Formatting > renders compact tools with summary on same line 1`] = `
" ✓ test-tool Test description → Success summary
"
`;
exports[`<ToolGroupDisplay /> > Result Formatting > renders placeholder for diff results 1`] = `
"
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ✓ test-tool Test description │
│ │
│ [Diff Display: 3 -> 3 chars] │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
exports[`<ToolGroupDisplay /> > Result Formatting > renders text results with summary below 1`] = `
"
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ✓ test-tool Test description │
│ │
│ Detailed output │
│ Short summary │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
+59 -28
View File
@@ -26,7 +26,7 @@ import type {
HistoryItemWithoutId,
LoopDetectionConfirmationRequest,
IndividualToolCallDisplay,
HistoryItemToolGroup,
HistoryItemToolDisplayGroup,
} from '../types.js';
import { StreamingState, MessageType } from '../types.js';
import { findLastSafeSplitPoint } from '../utils/markdownUtilities.js';
@@ -81,6 +81,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();
@@ -408,32 +410,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,
@@ -442,24 +439,43 @@ export const useAgentStream = ({
backgroundTasks,
);
const historyItem: HistoryItemToolGroup = {
type: 'tool_group',
tools: toolsToPush,
borderTop: isFirstToolInGroupRef.current,
borderBottom: isLastInBatch,
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) => ({
name: tc.name,
description: tc.description,
...tc.display,
status: tc.status,
originalRequestName: tc.originalRequestName,
})),
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,
@@ -468,7 +484,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[] = [];
@@ -482,10 +498,23 @@ 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_group',
tools: remainingTools,
borderTop: pushedToolCallIds.size === 0,
type: 'tool_display_group',
tools: remainingTools.map((tc) => ({
name: tc.name,
description: tc.description,
...tc.display,
status: tc.status,
originalRequestName: tc.originalRequestName,
})),
borderTop: shouldStartNewBlock,
borderBottom: false,
...appearance,
});
@@ -513,7 +542,7 @@ export const useAgentStream = ({
(anyVisibleInHistory || anyVisibleInPending)
) {
items.push({
type: 'tool_group' as const,
type: 'tool_display_group',
tools: [],
borderTop: false,
borderBottom: true,
@@ -525,6 +554,8 @@ export const useAgentStream = ({
}, [
trackedTools,
pushedToolCallIds,
pushedToolCallIdsRef,
hasEmittedBoxInTurnRef,
activePtyId,
isShellFocused,
backgroundTasks,
+15
View File
@@ -260,6 +260,20 @@ export type HistoryItemToolGroup = HistoryItemBase & {
borderDimColor?: boolean;
};
export type ToolDisplayItem = ToolDisplay & {
status: CoreToolCallStatus;
originalRequestName?: string;
};
export type HistoryItemToolDisplayGroup = HistoryItemBase & {
type: 'tool_display_group';
tools: ToolDisplayItem[];
borderTop?: boolean;
borderBottom?: boolean;
borderColor?: string;
borderDimColor?: boolean;
};
export type HistoryItemUserShell = HistoryItemBase & {
type: 'user_shell';
text: string;
@@ -406,6 +420,7 @@ export type HistoryItemWithoutId =
| HistoryItemAbout
| HistoryItemHelp
| HistoryItemToolGroup
| HistoryItemToolDisplayGroup
| HistoryItemStats
| HistoryItemModelStats
| HistoryItemToolStats