Sticky headers where the top rounded border is sticky. (#12971)

This commit is contained in:
Jacob Richman
2025-11-12 17:01:16 -08:00
committed by jacob314
parent 0c4d3b2666
commit 046b3011c2
13 changed files with 318 additions and 160 deletions
+20 -14
View File
@@ -19,6 +19,7 @@ import { calculateMainAreaWidth } from '../ui/utils/ui-sizing.js';
import { VimModeProvider } from '../ui/contexts/VimModeContext.js'; import { VimModeProvider } from '../ui/contexts/VimModeContext.js';
import { MouseProvider } from '../ui/contexts/MouseContext.js'; import { MouseProvider } from '../ui/contexts/MouseContext.js';
import { ScrollProvider } from '../ui/contexts/ScrollProvider.js'; import { ScrollProvider } from '../ui/contexts/ScrollProvider.js';
import { StreamingContext } from '../ui/contexts/StreamingContext.js';
import { type Config } from '@google/gemini-cli-core'; import { type Config } from '@google/gemini-cli-core';
@@ -69,6 +70,9 @@ const mockConfig = {
getTargetDir: () => getTargetDir: () =>
'/Users/test/project/foo/bar/and/some/more/directories/to/make/it/long', '/Users/test/project/foo/bar/and/some/more/directories/to/make/it/long',
getDebugMode: () => false, getDebugMode: () => false,
isTrustedFolder: () => true,
getIdeMode: () => false,
getEnableInteractiveShell: () => true,
}; };
const configProxy = new Proxy(mockConfig, { const configProxy = new Proxy(mockConfig, {
@@ -177,20 +181,22 @@ export const renderWithProviders = (
<UIStateContext.Provider value={finalUiState}> <UIStateContext.Provider value={finalUiState}>
<VimModeProvider settings={finalSettings}> <VimModeProvider settings={finalSettings}>
<ShellFocusContext.Provider value={shellFocus}> <ShellFocusContext.Provider value={shellFocus}>
<KeypressProvider> <StreamingContext.Provider value={finalUiState.streamingState}>
<MouseProvider mouseEventsEnabled={mouseEventsEnabled}> <KeypressProvider>
<ScrollProvider> <MouseProvider mouseEventsEnabled={mouseEventsEnabled}>
<Box <ScrollProvider>
width={terminalWidth} <Box
flexShrink={0} width={terminalWidth}
flexGrow={0} flexShrink={0}
flexDirection="column" flexGrow={0}
> flexDirection="column"
{component} >
</Box> {component}
</ScrollProvider> </Box>
</MouseProvider> </ScrollProvider>
</KeypressProvider> </MouseProvider>
</KeypressProvider>
</StreamingContext.Provider>
</ShellFocusContext.Provider> </ShellFocusContext.Provider>
</VimModeProvider> </VimModeProvider>
</UIStateContext.Provider> </UIStateContext.Provider>
@@ -11,7 +11,6 @@ import type { HistoryItem, HistoryItemWithoutId } from '../types.js';
import { Text } from 'ink'; import { Text } from 'ink';
import { renderWithProviders } from '../../test-utils/render.js'; import { renderWithProviders } from '../../test-utils/render.js';
import type { Config } from '@google/gemini-cli-core'; import type { Config } from '@google/gemini-cli-core';
import type { ToolMessageProps } from './messages/ToolMessage.js';
vi.mock('../contexts/AppContext.js', () => ({ vi.mock('../contexts/AppContext.js', () => ({
useAppContext: () => ({ useAppContext: () => ({
@@ -32,14 +31,6 @@ vi.mock('../GeminiRespondingSpinner.js', () => ({
GeminiRespondingSpinner: () => <Text>Spinner</Text>, GeminiRespondingSpinner: () => <Text>Spinner</Text>,
})); }));
vi.mock('./messages/ToolMessage.js', () => ({
ToolMessage: (props: ToolMessageProps) => (
<Text>
ToolMessage: {props.name} - {props.status}
</Text>
),
}));
const mockHistory: HistoryItem[] = [ const mockHistory: HistoryItem[] = [
{ {
id: 1, id: 1,
@@ -10,9 +10,14 @@ import { StickyHeader } from './StickyHeader.js';
import { renderWithProviders } from '../../test-utils/render.js'; import { renderWithProviders } from '../../test-utils/render.js';
describe('StickyHeader', () => { describe('StickyHeader', () => {
it('renders children', () => { it.each([true, false])('renders children with isFirst=%s', (isFirst) => {
const { lastFrame } = renderWithProviders( const { lastFrame } = renderWithProviders(
<StickyHeader width={80}> <StickyHeader
isFirst={isFirst}
width={80}
borderColor="green"
borderDimColor={false}
>
<Text>Hello Sticky</Text> <Text>Hello Sticky</Text>
</StickyHeader>, </StickyHeader>,
); );
@@ -11,11 +11,17 @@ import { theme } from '../semantic-colors.js';
export interface StickyHeaderProps { export interface StickyHeaderProps {
children: React.ReactNode; children: React.ReactNode;
width: number; width: number;
isFirst: boolean;
borderColor: string;
borderDimColor: boolean;
} }
export const StickyHeader: React.FC<StickyHeaderProps> = ({ export const StickyHeader: React.FC<StickyHeaderProps> = ({
children, children,
width, width,
isFirst,
borderColor,
borderDimColor,
}) => ( }) => (
<Box <Box
sticky sticky
@@ -24,20 +30,43 @@ export const StickyHeader: React.FC<StickyHeaderProps> = ({
width={width} width={width}
stickyChildren={ stickyChildren={
<Box <Box
borderStyle="single" borderStyle="round"
flexDirection="column"
width={width} width={width}
opaque opaque
borderColor={theme.ui.dark} borderColor={borderColor}
borderTop={false} borderDimColor={borderDimColor}
borderLeft={false} borderBottom={false}
borderRight={false} borderTop={isFirst}
paddingX={1} paddingTop={isFirst ? 0 : 1}
> >
{children} <Box paddingX={1}>{children}</Box>
{/* Dark border to separate header from content. */}
<Box
width={width - 2}
borderColor={theme.ui.dark}
borderStyle="single"
borderTop={false}
borderBottom={true}
borderLeft={false}
borderRight={false}
></Box>
</Box> </Box>
} }
> >
<Box paddingX={1} width={width}> <Box
borderStyle="round"
width={width}
borderColor={borderColor}
borderDimColor={borderDimColor}
borderBottom={false}
borderTop={isFirst}
borderLeft={true}
borderRight={true}
paddingX={1}
paddingBottom={1}
paddingTop={isFirst ? 0 : 1}
>
{children} {children}
</Box> </Box>
</Box> </Box>
@@ -17,12 +17,15 @@ Tips for getting started:
3. Create GEMINI.md files to customize your interactions with Gemini. 3. Create GEMINI.md files to customize your interactions with Gemini.
4. /help for more information. 4. /help for more information.
╭──────────────────────────────────────────────────────────────────────────────╮ ╭──────────────────────────────────────────────────────────────────────────────╮
ToolMessage: tool1 - Success ✓ tool1 Description for tool 1
│ │
╰──────────────────────────────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────────────────╯
╭──────────────────────────────────────────────────────────────────────────────╮ ╭──────────────────────────────────────────────────────────────────────────────╮
ToolMessage: tool2 - Success ✓ tool2 Description for tool 2
│ │
╰──────────────────────────────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────────────────╯
╭──────────────────────────────────────────────────────────────────────────────╮ ╭──────────────────────────────────────────────────────────────────────────────╮
ToolMessage: tool3 - Pending o tool3 Description for tool 3
│ │
╰──────────────────────────────────────────────────────────────────────────────╯" ╰──────────────────────────────────────────────────────────────────────────────╯"
`; `;
@@ -41,7 +41,6 @@ export const ToolConfirmationMessage: React.FC<
terminalWidth, terminalWidth,
}) => { }) => {
const { onConfirm } = confirmationDetails; const { onConfirm } = confirmationDetails;
const childWidth = terminalWidth - 2; // 2 for padding
const isAlternateBuffer = useAlternateBuffer(); const isAlternateBuffer = useAlternateBuffer();
@@ -249,21 +248,15 @@ export const ToolConfirmationMessage: React.FC<
</Box> </Box>
); );
bodyContent = ( bodyContent = isAlternateBuffer ? (
<Box flexDirection="column"> commandBox
<Box paddingX={1}> ) : (
{isAlternateBuffer ? ( <MaxSizedBox
commandBox maxHeight={bodyContentHeight}
) : ( maxWidth={Math.max(terminalWidth, 1)}
<MaxSizedBox >
maxHeight={bodyContentHeight} {commandBox}
maxWidth={Math.max(childWidth, 1)} </MaxSizedBox>
>
{commandBox}
</MaxSizedBox>
)}
</Box>
</Box>
); );
} else if (confirmationDetails.type === 'info') { } else if (confirmationDetails.type === 'info') {
const infoProps = confirmationDetails; const infoProps = confirmationDetails;
@@ -274,7 +267,7 @@ export const ToolConfirmationMessage: React.FC<
); );
bodyContent = ( bodyContent = (
<Box flexDirection="column" paddingX={1}> <Box flexDirection="column">
<Text color={theme.text.link}> <Text color={theme.text.link}>
<RenderInline <RenderInline
text={infoProps.prompt} text={infoProps.prompt}
@@ -299,7 +292,7 @@ export const ToolConfirmationMessage: React.FC<
const mcpProps = confirmationDetails as ToolMcpConfirmationDetails; const mcpProps = confirmationDetails as ToolMcpConfirmationDetails;
bodyContent = ( bodyContent = (
<Box flexDirection="column" paddingX={1}> <Box flexDirection="column">
<Text color={theme.text.link}>MCP Server: {mcpProps.serverName}</Text> <Text color={theme.text.link}>MCP Server: {mcpProps.serverName}</Text>
<Text color={theme.text.link}>Tool: {mcpProps.toolName}</Text> <Text color={theme.text.link}>Tool: {mcpProps.toolName}</Text>
</Box> </Box>
@@ -315,7 +308,6 @@ export const ToolConfirmationMessage: React.FC<
availableTerminalHeight, availableTerminalHeight,
terminalWidth, terminalWidth,
isAlternateBuffer, isAlternateBuffer,
childWidth,
]); ]);
if (confirmationDetails.type === 'edit') { if (confirmationDetails.type === 'edit') {
@@ -326,7 +318,8 @@ export const ToolConfirmationMessage: React.FC<
borderStyle="round" borderStyle="round"
borderColor={theme.border.default} borderColor={theme.border.default}
justifyContent="space-around" justifyContent="space-around"
padding={1} paddingTop={1}
paddingBottom={1}
overflow="hidden" overflow="hidden"
> >
<Text color={theme.text.primary}>Modify in progress: </Text> <Text color={theme.text.primary}>Modify in progress: </Text>
@@ -342,23 +335,17 @@ export const ToolConfirmationMessage: React.FC<
<Box flexDirection="column" paddingTop={0} paddingBottom={1}> <Box flexDirection="column" paddingTop={0} paddingBottom={1}>
{/* Body Content (Diff Renderer or Command Info) */} {/* Body Content (Diff Renderer or Command Info) */}
{/* No separate context display here anymore for edits */} {/* No separate context display here anymore for edits */}
<Box <Box flexGrow={1} flexShrink={1} overflow="hidden" marginBottom={1}>
flexGrow={1}
flexShrink={1}
overflow="hidden"
marginBottom={1}
paddingLeft={1}
>
{bodyContent} {bodyContent}
</Box> </Box>
{/* Confirmation Question */} {/* Confirmation Question */}
<Box marginBottom={1} flexShrink={0} paddingX={1}> <Box marginBottom={1} flexShrink={0}>
<Text color={theme.text.primary}>{question}</Text> <Text color={theme.text.primary}>{question}</Text>
</Box> </Box>
{/* Select Input for Options */} {/* Select Input for Options */}
<Box flexShrink={0} paddingX={1}> <Box flexShrink={0}>
<RadioButtonSelect <RadioButtonSelect
items={options} items={options}
onSelect={handleSelect} onSelect={handleSelect}
@@ -6,7 +6,6 @@
import { renderWithProviders } from '../../../test-utils/render.js'; import { renderWithProviders } from '../../../test-utils/render.js';
import { describe, it, expect, vi } from 'vitest'; import { describe, it, expect, vi } from 'vitest';
import { Text } from 'ink';
import { ToolGroupMessage } from './ToolGroupMessage.js'; import { ToolGroupMessage } from './ToolGroupMessage.js';
import type { IndividualToolCallDisplay } from '../../types.js'; import type { IndividualToolCallDisplay } from '../../types.js';
import { ToolCallStatus } from '../../types.js'; import { ToolCallStatus } from '../../types.js';
@@ -252,20 +251,32 @@ describe('<ToolGroupMessage />', () => {
unmount(); unmount();
}); });
<<<<<<< HEAD
it('renders header when scrolled', () => { it('renders header when scrolled', () => {
=======
it('renders sticky header when scrolled', () => {
>>>>>>> ee7065f6 (Sticky headers where the top rounded border is sticky. (#12971))
const toolCalls = [ const toolCalls = [
createToolCall({ createToolCall({
callId: '1', callId: '1',
name: 'tool-1', name: 'tool-1',
<<<<<<< HEAD
description: description:
'Description 1. This is a long description that will need to be truncated if the terminal width is small.', 'Description 1. This is a long description that will need to be truncated if the terminal width is small.',
resultDisplay: 'line1\nline2\nline3\nline4\nline5', resultDisplay: 'line1\nline2\nline3\nline4\nline5',
=======
description: 'Description 1\n'.repeat(5),
>>>>>>> ee7065f6 (Sticky headers where the top rounded border is sticky. (#12971))
}), }),
createToolCall({ createToolCall({
callId: '2', callId: '2',
name: 'tool-2', name: 'tool-2',
<<<<<<< HEAD
description: 'Description 2', description: 'Description 2',
resultDisplay: 'line1\nline2', resultDisplay: 'line1\nline2',
=======
description: 'Description 2\n'.repeat(5),
>>>>>>> ee7065f6 (Sticky headers where the top rounded border is sticky. (#12971))
}), }),
]; ];
const { lastFrame, unmount } = renderWithProviders( const { lastFrame, unmount } = renderWithProviders(
@@ -14,7 +14,6 @@ import { ToolConfirmationMessage } from './ToolConfirmationMessage.js';
import { theme } from '../../semantic-colors.js'; import { theme } from '../../semantic-colors.js';
import { SHELL_COMMAND_NAME, SHELL_NAME } from '../../constants.js'; import { SHELL_COMMAND_NAME, SHELL_NAME } from '../../constants.js';
import { useConfig } from '../../contexts/ConfigContext.js'; import { useConfig } from '../../contexts/ConfigContext.js';
import { useAlternateBuffer } from '../../hooks/useAlternateBuffer.js';
interface ToolGroupMessageProps { interface ToolGroupMessageProps {
groupId: number; groupId: number;
@@ -48,7 +47,6 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
); );
const config = useConfig(); const config = useConfig();
const isAlternateBuffer = useAlternateBuffer();
const isShellCommand = toolCalls.some( const isShellCommand = toolCalls.some(
(t) => t.name === SHELL_COMMAND_NAME || t.name === SHELL_NAME, (t) => t.name === SHELL_COMMAND_NAME || t.name === SHELL_NAME,
); );
@@ -59,10 +57,10 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
? theme.status.warning ? theme.status.warning
: theme.border.default; : theme.border.default;
const borderDimColor =
hasPending && (!isShellCommand || !isEmbeddedShellFocused);
const staticHeight = /* border */ 2 + /* marginBottom */ 1; const staticHeight = /* border */ 2 + /* marginBottom */ 1;
// This is a bit of a magic number, but it accounts for the border and
// marginLeft in regular mode and just the border in alternate buffer mode.
const innerWidth = isAlternateBuffer ? terminalWidth - 3 : terminalWidth - 4;
// only prompt for tool approval on the first 'confirming' tool in the list // only prompt for tool approval on the first 'confirming' tool in the list
// note, after the CTA, this automatically moves over to the next 'confirming' tool // note, after the CTA, this automatically moves over to the next 'confirming' tool
@@ -89,9 +87,11 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
: undefined; : undefined;
return ( return (
// This box doesn't have a border even though it conceptually does because
// we need to allow the sticky headers to render the borders themselves so
// that the top border can be sticky.
<Box <Box
flexDirection="column" flexDirection="column"
borderStyle="round"
/* /*
This width constraint is highly important and protects us from an Ink rendering bug. This width constraint is highly important and protects us from an Ink rendering bug.
Since the ToolGroup can typically change rendering states frequently, it can cause Since the ToolGroup can typically change rendering states frequently, it can cause
@@ -99,52 +99,65 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
cause tearing. cause tearing.
*/ */
width={terminalWidth} width={terminalWidth}
borderDimColor={
hasPending && (!isShellCommand || !isEmbeddedShellFocused)
}
borderColor={borderColor}
gap={1}
> >
{toolCalls.map((tool) => { {toolCalls.map((tool, index) => {
const isConfirming = toolAwaitingApproval?.callId === tool.callId; const isConfirming = toolAwaitingApproval?.callId === tool.callId;
const isFirst = index === 0;
const isLast = index === toolCalls.length - 1;
return ( return (
<Box <Box
key={tool.callId} key={tool.callId}
flexDirection="column" flexDirection="column"
minHeight={1} minHeight={1}
width={innerWidth} width={terminalWidth}
> >
<ToolMessage <ToolMessage
{...tool} {...tool}
availableTerminalHeight={availableTerminalHeightPerToolMessage} availableTerminalHeight={availableTerminalHeightPerToolMessage}
terminalWidth={innerWidth} terminalWidth={terminalWidth}
emphasis={ emphasis={
isConfirming ? 'high' : toolAwaitingApproval ? 'low' : 'medium' isConfirming ? 'high' : toolAwaitingApproval ? 'low' : 'medium'
} }
activeShellPtyId={activeShellPtyId} activeShellPtyId={activeShellPtyId}
embeddedShellFocused={embeddedShellFocused} embeddedShellFocused={embeddedShellFocused}
config={config} config={config}
isFirst={isFirst}
borderColor={borderColor}
borderDimColor={borderDimColor}
/> />
{tool.status === ToolCallStatus.Confirming && <Box
isConfirming && borderLeft={true}
tool.confirmationDetails && ( borderRight={true}
<ToolConfirmationMessage borderTop={false}
confirmationDetails={tool.confirmationDetails} borderBottom={isLast}
config={config} borderColor={borderColor}
isFocused={isFocused} borderDimColor={borderDimColor}
availableTerminalHeight={ flexDirection="column"
availableTerminalHeightPerToolMessage borderStyle="round"
} paddingLeft={1}
terminalWidth={innerWidth} paddingRight={1}
/> >
{tool.status === ToolCallStatus.Confirming &&
isConfirming &&
tool.confirmationDetails && (
<ToolConfirmationMessage
confirmationDetails={tool.confirmationDetails}
config={config}
isFocused={isFocused}
availableTerminalHeight={
availableTerminalHeightPerToolMessage
}
terminalWidth={terminalWidth - 4}
/>
)}
{tool.outputFile && (
<Box>
<Text color={theme.text.primary}>
Output too long and was saved to: {tool.outputFile}
</Text>
</Box>
)} )}
{tool.outputFile && ( </Box>
<Box marginX={1}>
<Text color={theme.text.primary}>
Output too long and was saved to: {tool.outputFile}
</Text>
</Box>
)}
</Box> </Box>
); );
})} })}
@@ -89,6 +89,9 @@ describe('<ToolMessage />', () => {
terminalWidth: 80, terminalWidth: 80,
confirmationDetails: undefined, confirmationDetails: undefined,
emphasis: 'medium', emphasis: 'medium',
isFirst: true,
borderColor: 'green',
borderDimColor: false,
}; };
it('renders basic tool information', () => { it('renders basic tool information', () => {
@@ -42,6 +42,9 @@ export interface ToolMessageProps extends IndividualToolCallDisplay {
renderOutputAsMarkdown?: boolean; renderOutputAsMarkdown?: boolean;
activeShellPtyId?: number | null; activeShellPtyId?: number | null;
embeddedShellFocused?: boolean; embeddedShellFocused?: boolean;
isFirst: boolean;
borderColor: string;
borderDimColor: boolean;
config?: Config; config?: Config;
} }
@@ -58,6 +61,9 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
embeddedShellFocused, embeddedShellFocused,
ptyId, ptyId,
config, config,
isFirst,
borderColor,
borderDimColor,
}) => { }) => {
const { renderMarkdown } = useUIState(); const { renderMarkdown } = useUIState();
const isAlternateBuffer = useAlternateBuffer(); const isAlternateBuffer = useAlternateBuffer();
@@ -116,7 +122,8 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
if (availableHeight && !isAlternateBuffer) { if (availableHeight && !isAlternateBuffer) {
renderOutputAsMarkdown = false; renderOutputAsMarkdown = false;
} }
const childWidth = terminalWidth; const combinedPaddingAndBorderWidth = 4;
const childWidth = terminalWidth - combinedPaddingAndBorderWidth;
const truncatedResultDisplay = React.useMemo(() => { const truncatedResultDisplay = React.useMemo(() => {
if (typeof resultDisplay === 'string') { if (typeof resultDisplay === 'string') {
@@ -131,7 +138,7 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
if (!truncatedResultDisplay) return null; if (!truncatedResultDisplay) return null;
return ( return (
<Box width={terminalWidth} flexDirection="column" paddingLeft={1}> <Box width={childWidth} flexDirection="column">
<Box flexDirection="column"> <Box flexDirection="column">
{typeof truncatedResultDisplay === 'string' && {typeof truncatedResultDisplay === 'string' &&
renderOutputAsMarkdown ? ( renderOutputAsMarkdown ? (
@@ -189,15 +196,16 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
renderMarkdown, renderMarkdown,
isAlternateBuffer, isAlternateBuffer,
availableHeight, availableHeight,
terminalWidth,
]); ]);
return ( return (
// We have the StickyHeader intentionally exceedsthe allowed width for this
// component by 1 so tne horizontal line it renders can extend into the 1
// pixel of padding of the box drawn by the parent of the ToolMessage.
<> <>
<StickyHeader width={terminalWidth + 1}> <StickyHeader
width={terminalWidth}
isFirst={isFirst}
borderColor={borderColor}
borderDimColor={borderDimColor}
>
<ToolStatusIndicator status={status} name={name} /> <ToolStatusIndicator status={status} name={name} />
<ToolInfo <ToolInfo
name={name} name={name}
@@ -214,15 +222,28 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
)} )}
{emphasis === 'high' && <TrailingIndicator />} {emphasis === 'high' && <TrailingIndicator />}
</StickyHeader> </StickyHeader>
{renderedResult} <Box
{isThisShellFocused && config && ( width={terminalWidth}
<Box paddingLeft={STATUS_INDICATOR_WIDTH} marginTop={1}> borderStyle="round"
<ShellInputPrompt borderColor={borderColor}
activeShellPtyId={activeShellPtyId ?? null} borderDimColor={borderDimColor}
focus={embeddedShellFocused} borderTop={false}
/> borderBottom={false}
</Box> borderLeft={true}
)} borderRight={true}
paddingX={1}
flexDirection="column"
>
{renderedResult}
{isThisShellFocused && config && (
<Box paddingLeft={STATUS_INDICATOR_WIDTH} marginTop={1}>
<ShellInputPrompt
activeShellPtyId={activeShellPtyId ?? null}
focus={embeddedShellFocused}
/>
</Box>
)}
</Box>
</> </>
); );
}; };
@@ -20,6 +20,9 @@ describe('<ToolMessage /> - Raw Markdown Display Snapshots', () => {
terminalWidth: 80, terminalWidth: 80,
confirmationDetails: undefined, confirmationDetails: undefined,
emphasis: 'medium', emphasis: 'medium',
isFirst: true,
borderColor: 'green',
borderDimColor: false,
}; };
it.each([ it.each([
@@ -2,37 +2,53 @@
exports[`<ToolGroupMessage /> > Border Color Logic > uses gray border when all tools are successful and no shell commands 1`] = ` exports[`<ToolGroupMessage /> > Border Color Logic > uses gray border when all tools are successful and no shell commands 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮ "╭──────────────────────────────────────────────────────────────────────────────╮
MockTool[tool-123]: ✓ test-tool - A tool for testing (medium) │ ✓ test-tool A tool for testing
│ │ │ │
MockTool[tool-2]: ✓ another-tool - A tool for testing (medium) Test result
│ │
│ ✓ another-tool A tool for testing │
│ │
│ Test result │
╰──────────────────────────────────────────────────────────────────────────────╯" ╰──────────────────────────────────────────────────────────────────────────────╯"
`; `;
exports[`<ToolGroupMessage /> > Border Color Logic > uses yellow border for shell commands even when successful 1`] = ` exports[`<ToolGroupMessage /> > Border Color Logic > uses yellow border for shell commands even when successful 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮ "╭──────────────────────────────────────────────────────────────────────────────╮
MockTool[tool-123]: ✓ run_shell_command - A tool for testing (medium) │ ✓ run_shell_command A tool for testing
│ │
│ Test result │
╰──────────────────────────────────────────────────────────────────────────────╯" ╰──────────────────────────────────────────────────────────────────────────────╯"
`; `;
exports[`<ToolGroupMessage /> > Border Color Logic > uses yellow border when tools are pending 1`] = ` exports[`<ToolGroupMessage /> > Border Color Logic > uses yellow border when tools are pending 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮ "╭──────────────────────────────────────────────────────────────────────────────╮
MockTool[tool-123]: o test-tool - A tool for testing (medium) │ o test-tool A tool for testing
│ │
│ Test result │
╰──────────────────────────────────────────────────────────────────────────────╯" ╰──────────────────────────────────────────────────────────────────────────────╯"
`; `;
exports[`<ToolGroupMessage /> > Confirmation Handling > shows confirmation dialog for first confirming tool only 1`] = ` exports[`<ToolGroupMessage /> > Confirmation Handling > shows confirmation dialog for first confirming tool only 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮ "╭──────────────────────────────────────────────────────────────────────────────╮
MockTool[tool-1]: ? first-confirm - A tool for testing (high) │ ? first-confirm A tool for testing
│MockConfirmation: Confirm first tool │
│ │ │ │
MockTool[tool-2]: ? second-confirm - A tool for testing (low) Test result
│ Confirm first tool │
│ │
│ Do you want to proceed? │
│ │
│ ● 1. Yes, allow once │
│ 2. Yes, allow always │
│ 3. No, suggest changes (esc) │
│ │
│ │
│ ? second-confirm A tool for testing │
│ │
│ Test result │
╰──────────────────────────────────────────────────────────────────────────────╯" ╰──────────────────────────────────────────────────────────────────────────────╯"
`; `;
exports[`<ToolGroupMessage /> > Golden Snapshots > renders empty tool calls array 1`] = ` exports[`<ToolGroupMessage /> > Golden Snapshots > renders empty tool calls array 1`] = `""`;
"╭──────────────────────────────────────────────────────────────────────────────╮
╰──────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`<ToolGroupMessage /> > Golden Snapshots > renders header when scrolled 1`] = ` exports[`<ToolGroupMessage /> > Golden Snapshots > renders header when scrolled 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮ "╭──────────────────────────────────────────────────────────────────────────────╮
@@ -49,41 +65,87 @@ 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 shell command 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮ "╭──────────────────────────────────────────────────────────────────────────────╮
MockTool[tool-1]: ✓ read_file - Read a file (medium) │ ✓ read_file Read a file
│ │ │ │
MockTool[tool-2]: ⊷ run_shell_command - Run command (medium) Test result
│ │ │ │
MockTool[tool-3]: o write_file - Write to file (medium) ⊷ run_shell_command Run command
│ │
│ Test result │
│ │
│ o write_file Write to file │
│ │
│ Test result │
╰──────────────────────────────────────────────────────────────────────────────╯" ╰──────────────────────────────────────────────────────────────────────────────╯"
`; `;
exports[`<ToolGroupMessage /> > Golden Snapshots > renders multiple tool calls with different statuses 1`] = ` exports[`<ToolGroupMessage /> > Golden Snapshots > renders multiple tool calls with different statuses 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮ "╭──────────────────────────────────────────────────────────────────────────────╮
MockTool[tool-1]: ✓ successful-tool - This tool succeeded (medium) │ ✓ successful-tool This tool succeeded
│ │ │ │
MockTool[tool-2]: o pending-tool - This tool is pending (medium) Test result
│ │ │ │
MockTool[tool-3]: x error-tool - This tool failed (medium) o pending-tool This tool is pending
│ │
│ Test result │
│ │
│ x error-tool This tool failed │
│ │
│ Test result │
╰──────────────────────────────────────────────────────────────────────────────╯" ╰──────────────────────────────────────────────────────────────────────────────╯"
`; `;
exports[`<ToolGroupMessage /> > Golden Snapshots > renders shell command with yellow border 1`] = ` exports[`<ToolGroupMessage /> > Golden Snapshots > renders shell command with yellow border 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮ "╭──────────────────────────────────────────────────────────────────────────────╮
MockTool[shell-1]: ✓ run_shell_command - Execute shell command (medium) │ ✓ run_shell_command Execute shell command
│ │
│ Test result │
╰──────────────────────────────────────────────────────────────────────────────╯" ╰──────────────────────────────────────────────────────────────────────────────╯"
`; `;
exports[`<ToolGroupMessage /> > Golden Snapshots > renders single successful tool call 1`] = ` exports[`<ToolGroupMessage /> > Golden Snapshots > renders single successful tool call 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮ "╭──────────────────────────────────────────────────────────────────────────────╮
MockTool[tool-123]: ✓ test-tool - A tool for testing (medium) │ ✓ test-tool A tool for testing
│ │
│ Test result │
╰──────────────────────────────────────────────────────────────────────────────╯" ╰──────────────────────────────────────────────────────────────────────────────╯"
`; `;
exports[`<ToolGroupMessage /> > Golden Snapshots > renders sticky header when scrolled 1`] = `
"│ │
│ ✓ tool-2 Description 2 │
│ Description 2 │
│ Description 2 │
│ Description 2 │ ▄
│ Description 2 │ █
│ │ █
│ │ █
│ Test result │ █
╰──────────────────────────────────────────────────────────────────────────────╯ █"
`;
exports[`<ToolGroupMessage /> > Golden Snapshots > renders tool call awaiting confirmation 1`] = ` exports[`<ToolGroupMessage /> > Golden Snapshots > renders tool call awaiting confirmation 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮ "╭──────────────────────────────────────────────────────────────────────────────╮
MockTool[tool-confirm]: ? confirmation-tool - This tool needs confirmation │ │ ? confirmation-tool This tool needs confirmation
(high)
MockConfirmation: Are you sure you want to proceed? Test result
│ Are you sure you want to proceed? │
│ │
│ Do you want to proceed? │
│ │
│ ● 1. Yes, allow once │
│ 2. Yes, allow always │
│ 3. No, suggest changes (esc) │
│ │
╰──────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`<ToolGroupMessage /> > Golden Snapshots > renders tool call with outputFile 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ ✓ tool-with-file Tool that saved output to file │
│ │
│ Test result │
│ Output too long and was saved to: /path/to/output.txt │
╰──────────────────────────────────────────────────────────────────────────────╯" ╰──────────────────────────────────────────────────────────────────────────────╯"
`; `;
@@ -97,34 +159,46 @@ exports[`<ToolGroupMessage /> > Golden Snapshots > renders tool call with output
exports[`<ToolGroupMessage /> > Golden Snapshots > renders when not focused 1`] = ` exports[`<ToolGroupMessage /> > Golden Snapshots > renders when not focused 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮ "╭──────────────────────────────────────────────────────────────────────────────╮
MockTool[tool-123]: ✓ test-tool - A tool for testing (medium) │ ✓ test-tool A tool for testing
│ │
│ Test result │
╰──────────────────────────────────────────────────────────────────────────────╯" ╰──────────────────────────────────────────────────────────────────────────────╯"
`; `;
exports[`<ToolGroupMessage /> > Golden Snapshots > renders with limited terminal height 1`] = ` exports[`<ToolGroupMessage /> > Golden Snapshots > renders with limited terminal height 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮ "╭──────────────────────────────────────────────────────────────────────────────╮
MockTool[tool-1]: ✓ tool-with-result - Tool with output (medium) │ ✓ tool-with-result Tool with output
│ │ │ │
MockTool[tool-2]: ✓ another-tool - Another tool (medium) This is a long result that might need height constraints
│ │
│ ✓ another-tool Another tool │
│ │
│ More output here │
╰──────────────────────────────────────────────────────────────────────────────╯" ╰──────────────────────────────────────────────────────────────────────────────╯"
`; `;
exports[`<ToolGroupMessage /> > Golden Snapshots > renders with narrow terminal width 1`] = ` exports[`<ToolGroupMessage /> > Golden Snapshots > renders with narrow terminal width 1`] = `
"╭──────────────────────────────────────╮ "╭──────────────────────────────────────╮
MockTool[tool-123]: ✓ ✓ very-long-tool-name-that-might-wr
│very-long-tool-name-that-might-wrap ap This is a very long
- This is a very long description description that might cause
that might cause wrapping issues │ wrapping issues
(medium)
│ Test result │
╰──────────────────────────────────────╯" ╰──────────────────────────────────────╯"
`; `;
exports[`<ToolGroupMessage /> > Height Calculation > calculates available height correctly with multiple tools with results 1`] = ` exports[`<ToolGroupMessage /> > Height Calculation > calculates available height correctly with multiple tools with results 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮ "╭──────────────────────────────────────────────────────────────────────────────╮
MockTool[tool-1]: ✓ test-tool - A tool for testing (medium) │ ✓ test-tool A tool for testing
│ │ │ │
MockTool[tool-2]: ✓ test-tool - A tool for testing (medium) Result 1
│ │
│ ✓ test-tool A tool for testing │
│ │
│ Result 2 │
│ │
│ ✓ test-tool A tool for testing │
│ │ │ │
│MockTool[tool-3]: ✓ test-tool - A tool for testing (medium) │
╰──────────────────────────────────────────────────────────────────────────────╯" ╰──────────────────────────────────────────────────────────────────────────────╯"
`; `;
@@ -1,31 +1,43 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<ToolMessage /> - Raw Markdown Display Snapshots > renders with renderMarkdown=false, useAlternateBuffer=false '(raw markdown, regular buffer)' 1`] = ` exports[`<ToolMessage /> - Raw Markdown Display Snapshots > renders with renderMarkdown=false, useAlternateBuffer=false '(raw markdown, regular buffer)' 1`] = `
" ✓ test-tool A tool for testing "╭──────────────────────────────────────────────────────────────────────────────╮
Test **bold** and \`code\` markdown" │ ✓ test-tool A tool for testing │
│ │
│ Test **bold** and \`code\` markdown │"
`; `;
exports[`<ToolMessage /> - Raw Markdown Display Snapshots > renders with renderMarkdown=false, useAlternateBuffer=true '(raw markdown, alternate buffer)' 1`] = ` exports[`<ToolMessage /> - Raw Markdown Display Snapshots > renders with renderMarkdown=false, useAlternateBuffer=true '(raw markdown, alternate buffer)' 1`] = `
" ✓ test-tool A tool for testing "╭──────────────────────────────────────────────────────────────────────────────╮
Test **bold** and \`code\` markdown" │ ✓ test-tool A tool for testing │
│ │
│ Test **bold** and \`code\` markdown │"
`; `;
exports[`<ToolMessage /> - Raw Markdown Display Snapshots > renders with renderMarkdown=true, useAlternateBuffer=false '(constrained height, regular buffer -…' 1`] = ` exports[`<ToolMessage /> - Raw Markdown Display Snapshots > renders with renderMarkdown=true, useAlternateBuffer=false '(constrained height, regular buffer -…' 1`] = `
" ✓ test-tool A tool for testing "╭──────────────────────────────────────────────────────────────────────────────╮
Test **bold** and \`code\` markdown" │ ✓ test-tool A tool for testing │
│ │
│ Test **bold** and \`code\` markdown │"
`; `;
exports[`<ToolMessage /> - Raw Markdown Display Snapshots > renders with renderMarkdown=true, useAlternateBuffer=false '(default, regular buffer)' 1`] = ` exports[`<ToolMessage /> - Raw Markdown Display Snapshots > renders with renderMarkdown=true, useAlternateBuffer=false '(default, regular buffer)' 1`] = `
" ✓ test-tool A tool for testing "╭──────────────────────────────────────────────────────────────────────────────╮
Test bold and code markdown" │ ✓ test-tool A tool for testing │
│ │
│ Test bold and code markdown │"
`; `;
exports[`<ToolMessage /> - Raw Markdown Display Snapshots > renders with renderMarkdown=true, useAlternateBuffer=true '(constrained height, alternate buffer…' 1`] = ` exports[`<ToolMessage /> - Raw Markdown Display Snapshots > renders with renderMarkdown=true, useAlternateBuffer=true '(constrained height, alternate buffer…' 1`] = `
" ✓ test-tool A tool for testing "╭──────────────────────────────────────────────────────────────────────────────╮
Test bold and code markdown" │ ✓ test-tool A tool for testing │
│ │
│ Test bold and code markdown │"
`; `;
exports[`<ToolMessage /> - Raw Markdown Display Snapshots > renders with renderMarkdown=true, useAlternateBuffer=true '(default, alternate buffer)' 1`] = ` exports[`<ToolMessage /> - Raw Markdown Display Snapshots > renders with renderMarkdown=true, useAlternateBuffer=true '(default, alternate buffer)' 1`] = `
" ✓ test-tool A tool for testing "╭──────────────────────────────────────────────────────────────────────────────╮
Test bold and code markdown" │ ✓ test-tool A tool for testing │
│ │
│ Test bold and code markdown │"
`; `;