mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -07:00
Sticky headers where the top rounded border is sticky. (#12971)
This commit is contained in:
@@ -19,6 +19,7 @@ import { calculateMainAreaWidth } from '../ui/utils/ui-sizing.js';
|
||||
import { VimModeProvider } from '../ui/contexts/VimModeContext.js';
|
||||
import { MouseProvider } from '../ui/contexts/MouseContext.js';
|
||||
import { ScrollProvider } from '../ui/contexts/ScrollProvider.js';
|
||||
import { StreamingContext } from '../ui/contexts/StreamingContext.js';
|
||||
|
||||
import { type Config } from '@google/gemini-cli-core';
|
||||
|
||||
@@ -69,6 +70,9 @@ const mockConfig = {
|
||||
getTargetDir: () =>
|
||||
'/Users/test/project/foo/bar/and/some/more/directories/to/make/it/long',
|
||||
getDebugMode: () => false,
|
||||
isTrustedFolder: () => true,
|
||||
getIdeMode: () => false,
|
||||
getEnableInteractiveShell: () => true,
|
||||
};
|
||||
|
||||
const configProxy = new Proxy(mockConfig, {
|
||||
@@ -177,20 +181,22 @@ export const renderWithProviders = (
|
||||
<UIStateContext.Provider value={finalUiState}>
|
||||
<VimModeProvider settings={finalSettings}>
|
||||
<ShellFocusContext.Provider value={shellFocus}>
|
||||
<KeypressProvider>
|
||||
<MouseProvider mouseEventsEnabled={mouseEventsEnabled}>
|
||||
<ScrollProvider>
|
||||
<Box
|
||||
width={terminalWidth}
|
||||
flexShrink={0}
|
||||
flexGrow={0}
|
||||
flexDirection="column"
|
||||
>
|
||||
{component}
|
||||
</Box>
|
||||
</ScrollProvider>
|
||||
</MouseProvider>
|
||||
</KeypressProvider>
|
||||
<StreamingContext.Provider value={finalUiState.streamingState}>
|
||||
<KeypressProvider>
|
||||
<MouseProvider mouseEventsEnabled={mouseEventsEnabled}>
|
||||
<ScrollProvider>
|
||||
<Box
|
||||
width={terminalWidth}
|
||||
flexShrink={0}
|
||||
flexGrow={0}
|
||||
flexDirection="column"
|
||||
>
|
||||
{component}
|
||||
</Box>
|
||||
</ScrollProvider>
|
||||
</MouseProvider>
|
||||
</KeypressProvider>
|
||||
</StreamingContext.Provider>
|
||||
</ShellFocusContext.Provider>
|
||||
</VimModeProvider>
|
||||
</UIStateContext.Provider>
|
||||
|
||||
@@ -11,7 +11,6 @@ import type { HistoryItem, HistoryItemWithoutId } from '../types.js';
|
||||
import { Text } from 'ink';
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import type { ToolMessageProps } from './messages/ToolMessage.js';
|
||||
|
||||
vi.mock('../contexts/AppContext.js', () => ({
|
||||
useAppContext: () => ({
|
||||
@@ -32,14 +31,6 @@ vi.mock('../GeminiRespondingSpinner.js', () => ({
|
||||
GeminiRespondingSpinner: () => <Text>Spinner</Text>,
|
||||
}));
|
||||
|
||||
vi.mock('./messages/ToolMessage.js', () => ({
|
||||
ToolMessage: (props: ToolMessageProps) => (
|
||||
<Text>
|
||||
ToolMessage: {props.name} - {props.status}
|
||||
</Text>
|
||||
),
|
||||
}));
|
||||
|
||||
const mockHistory: HistoryItem[] = [
|
||||
{
|
||||
id: 1,
|
||||
|
||||
@@ -10,9 +10,14 @@ import { StickyHeader } from './StickyHeader.js';
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
|
||||
describe('StickyHeader', () => {
|
||||
it('renders children', () => {
|
||||
it.each([true, false])('renders children with isFirst=%s', (isFirst) => {
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<StickyHeader width={80}>
|
||||
<StickyHeader
|
||||
isFirst={isFirst}
|
||||
width={80}
|
||||
borderColor="green"
|
||||
borderDimColor={false}
|
||||
>
|
||||
<Text>Hello Sticky</Text>
|
||||
</StickyHeader>,
|
||||
);
|
||||
|
||||
@@ -11,11 +11,17 @@ import { theme } from '../semantic-colors.js';
|
||||
export interface StickyHeaderProps {
|
||||
children: React.ReactNode;
|
||||
width: number;
|
||||
isFirst: boolean;
|
||||
borderColor: string;
|
||||
borderDimColor: boolean;
|
||||
}
|
||||
|
||||
export const StickyHeader: React.FC<StickyHeaderProps> = ({
|
||||
children,
|
||||
width,
|
||||
isFirst,
|
||||
borderColor,
|
||||
borderDimColor,
|
||||
}) => (
|
||||
<Box
|
||||
sticky
|
||||
@@ -24,20 +30,43 @@ export const StickyHeader: React.FC<StickyHeaderProps> = ({
|
||||
width={width}
|
||||
stickyChildren={
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderStyle="round"
|
||||
flexDirection="column"
|
||||
width={width}
|
||||
opaque
|
||||
borderColor={theme.ui.dark}
|
||||
borderTop={false}
|
||||
borderLeft={false}
|
||||
borderRight={false}
|
||||
paddingX={1}
|
||||
borderColor={borderColor}
|
||||
borderDimColor={borderDimColor}
|
||||
borderBottom={false}
|
||||
borderTop={isFirst}
|
||||
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 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}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -17,12 +17,15 @@ Tips for getting started:
|
||||
3. Create GEMINI.md files to customize your interactions with Gemini.
|
||||
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,
|
||||
}) => {
|
||||
const { onConfirm } = confirmationDetails;
|
||||
const childWidth = terminalWidth - 2; // 2 for padding
|
||||
|
||||
const isAlternateBuffer = useAlternateBuffer();
|
||||
|
||||
@@ -249,21 +248,15 @@ export const ToolConfirmationMessage: React.FC<
|
||||
</Box>
|
||||
);
|
||||
|
||||
bodyContent = (
|
||||
<Box flexDirection="column">
|
||||
<Box paddingX={1}>
|
||||
{isAlternateBuffer ? (
|
||||
commandBox
|
||||
) : (
|
||||
<MaxSizedBox
|
||||
maxHeight={bodyContentHeight}
|
||||
maxWidth={Math.max(childWidth, 1)}
|
||||
>
|
||||
{commandBox}
|
||||
</MaxSizedBox>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
bodyContent = isAlternateBuffer ? (
|
||||
commandBox
|
||||
) : (
|
||||
<MaxSizedBox
|
||||
maxHeight={bodyContentHeight}
|
||||
maxWidth={Math.max(terminalWidth, 1)}
|
||||
>
|
||||
{commandBox}
|
||||
</MaxSizedBox>
|
||||
);
|
||||
} else if (confirmationDetails.type === 'info') {
|
||||
const infoProps = confirmationDetails;
|
||||
@@ -274,7 +267,7 @@ export const ToolConfirmationMessage: React.FC<
|
||||
);
|
||||
|
||||
bodyContent = (
|
||||
<Box flexDirection="column" paddingX={1}>
|
||||
<Box flexDirection="column">
|
||||
<Text color={theme.text.link}>
|
||||
<RenderInline
|
||||
text={infoProps.prompt}
|
||||
@@ -299,7 +292,7 @@ export const ToolConfirmationMessage: React.FC<
|
||||
const mcpProps = confirmationDetails as ToolMcpConfirmationDetails;
|
||||
|
||||
bodyContent = (
|
||||
<Box flexDirection="column" paddingX={1}>
|
||||
<Box flexDirection="column">
|
||||
<Text color={theme.text.link}>MCP Server: {mcpProps.serverName}</Text>
|
||||
<Text color={theme.text.link}>Tool: {mcpProps.toolName}</Text>
|
||||
</Box>
|
||||
@@ -315,7 +308,6 @@ export const ToolConfirmationMessage: React.FC<
|
||||
availableTerminalHeight,
|
||||
terminalWidth,
|
||||
isAlternateBuffer,
|
||||
childWidth,
|
||||
]);
|
||||
|
||||
if (confirmationDetails.type === 'edit') {
|
||||
@@ -326,7 +318,8 @@ export const ToolConfirmationMessage: React.FC<
|
||||
borderStyle="round"
|
||||
borderColor={theme.border.default}
|
||||
justifyContent="space-around"
|
||||
padding={1}
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
overflow="hidden"
|
||||
>
|
||||
<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}>
|
||||
{/* Body Content (Diff Renderer or Command Info) */}
|
||||
{/* No separate context display here anymore for edits */}
|
||||
<Box
|
||||
flexGrow={1}
|
||||
flexShrink={1}
|
||||
overflow="hidden"
|
||||
marginBottom={1}
|
||||
paddingLeft={1}
|
||||
>
|
||||
<Box flexGrow={1} flexShrink={1} overflow="hidden" marginBottom={1}>
|
||||
{bodyContent}
|
||||
</Box>
|
||||
|
||||
{/* Confirmation Question */}
|
||||
<Box marginBottom={1} flexShrink={0} paddingX={1}>
|
||||
<Box marginBottom={1} flexShrink={0}>
|
||||
<Text color={theme.text.primary}>{question}</Text>
|
||||
</Box>
|
||||
|
||||
{/* Select Input for Options */}
|
||||
<Box flexShrink={0} paddingX={1}>
|
||||
<Box flexShrink={0}>
|
||||
<RadioButtonSelect
|
||||
items={options}
|
||||
onSelect={handleSelect}
|
||||
|
||||
@@ -6,59 +6,10 @@
|
||||
|
||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { Text } from 'ink';
|
||||
import { ToolGroupMessage } from './ToolGroupMessage.js';
|
||||
import type { IndividualToolCallDisplay } from '../../types.js';
|
||||
import { ToolCallStatus } from '../../types.js';
|
||||
import type { ToolCallConfirmationDetails } from '@google/gemini-cli-core';
|
||||
import { TOOL_STATUS } from '../../constants.js';
|
||||
|
||||
// Mock child components to isolate ToolGroupMessage behavior
|
||||
vi.mock('./ToolMessage.js', () => ({
|
||||
ToolMessage: function MockToolMessage({
|
||||
callId,
|
||||
name,
|
||||
description,
|
||||
status,
|
||||
emphasis,
|
||||
}: {
|
||||
callId: string;
|
||||
name: string;
|
||||
description: string;
|
||||
status: ToolCallStatus;
|
||||
emphasis: string;
|
||||
}) {
|
||||
// Use the same constants as the real component
|
||||
const statusSymbolMap: Record<ToolCallStatus, string> = {
|
||||
[ToolCallStatus.Success]: TOOL_STATUS.SUCCESS,
|
||||
[ToolCallStatus.Pending]: TOOL_STATUS.PENDING,
|
||||
[ToolCallStatus.Executing]: TOOL_STATUS.EXECUTING,
|
||||
[ToolCallStatus.Confirming]: TOOL_STATUS.CONFIRMING,
|
||||
[ToolCallStatus.Canceled]: TOOL_STATUS.CANCELED,
|
||||
[ToolCallStatus.Error]: TOOL_STATUS.ERROR,
|
||||
};
|
||||
const statusSymbol = statusSymbolMap[status] || '?';
|
||||
return (
|
||||
<Text>
|
||||
MockTool[{callId}]: {statusSymbol} {name} - {description} ({emphasis})
|
||||
</Text>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('./ToolConfirmationMessage.js', () => ({
|
||||
ToolConfirmationMessage: function MockToolConfirmationMessage({
|
||||
confirmationDetails,
|
||||
}: {
|
||||
confirmationDetails: ToolCallConfirmationDetails;
|
||||
}) {
|
||||
const displayText =
|
||||
confirmationDetails?.type === 'info'
|
||||
? (confirmationDetails as { prompt: string }).prompt
|
||||
: confirmationDetails?.title || 'confirm';
|
||||
return <Text>MockConfirmation: {displayText}</Text>;
|
||||
},
|
||||
}));
|
||||
import { Scrollable } from '../shared/Scrollable.js';
|
||||
|
||||
describe('<ToolGroupMessage />', () => {
|
||||
const createToolCall = (
|
||||
@@ -250,6 +201,45 @@ describe('<ToolGroupMessage />', () => {
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders sticky header when scrolled', () => {
|
||||
const toolCalls = [
|
||||
createToolCall({
|
||||
callId: '1',
|
||||
name: 'tool-1',
|
||||
description: 'Description 1\n'.repeat(5),
|
||||
}),
|
||||
createToolCall({
|
||||
callId: '2',
|
||||
name: 'tool-2',
|
||||
description: 'Description 2\n'.repeat(5),
|
||||
}),
|
||||
];
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<Scrollable height={10} hasFocus={true} scrollToBottom={true}>
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />
|
||||
</Scrollable>,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders tool call with outputFile', () => {
|
||||
const toolCalls = [
|
||||
createToolCall({
|
||||
callId: 'tool-output-file',
|
||||
name: 'tool-with-file',
|
||||
description: 'Tool that saved output to file',
|
||||
status: ToolCallStatus.Success,
|
||||
outputFile: '/path/to/output.txt',
|
||||
}),
|
||||
];
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Border Color Logic', () => {
|
||||
|
||||
@@ -14,7 +14,6 @@ import { ToolConfirmationMessage } from './ToolConfirmationMessage.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { SHELL_COMMAND_NAME, SHELL_NAME } from '../../constants.js';
|
||||
import { useConfig } from '../../contexts/ConfigContext.js';
|
||||
import { useAlternateBuffer } from '../../hooks/useAlternateBuffer.js';
|
||||
|
||||
interface ToolGroupMessageProps {
|
||||
groupId: number;
|
||||
@@ -48,7 +47,6 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
);
|
||||
|
||||
const config = useConfig();
|
||||
const isAlternateBuffer = useAlternateBuffer();
|
||||
const isShellCommand = toolCalls.some(
|
||||
(t) => t.name === SHELL_COMMAND_NAME || t.name === SHELL_NAME,
|
||||
);
|
||||
@@ -59,10 +57,10 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
? theme.status.warning
|
||||
: theme.border.default;
|
||||
|
||||
const borderDimColor =
|
||||
hasPending && (!isShellCommand || !isEmbeddedShellFocused);
|
||||
|
||||
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
|
||||
// note, after the CTA, this automatically moves over to the next 'confirming' tool
|
||||
@@ -89,9 +87,11 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
: undefined;
|
||||
|
||||
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
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
/*
|
||||
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
|
||||
@@ -99,52 +99,65 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
cause tearing.
|
||||
*/
|
||||
width={terminalWidth}
|
||||
borderDimColor={
|
||||
hasPending && (!isShellCommand || !isEmbeddedShellFocused)
|
||||
}
|
||||
borderColor={borderColor}
|
||||
gap={1}
|
||||
>
|
||||
{toolCalls.map((tool) => {
|
||||
{toolCalls.map((tool, index) => {
|
||||
const isConfirming = toolAwaitingApproval?.callId === tool.callId;
|
||||
const isFirst = index === 0;
|
||||
const isLast = index === toolCalls.length - 1;
|
||||
return (
|
||||
<Box
|
||||
key={tool.callId}
|
||||
flexDirection="column"
|
||||
minHeight={1}
|
||||
width={innerWidth}
|
||||
width={terminalWidth}
|
||||
>
|
||||
<ToolMessage
|
||||
{...tool}
|
||||
availableTerminalHeight={availableTerminalHeightPerToolMessage}
|
||||
terminalWidth={innerWidth}
|
||||
terminalWidth={terminalWidth}
|
||||
emphasis={
|
||||
isConfirming ? 'high' : toolAwaitingApproval ? 'low' : 'medium'
|
||||
}
|
||||
activeShellPtyId={activeShellPtyId}
|
||||
embeddedShellFocused={embeddedShellFocused}
|
||||
config={config}
|
||||
isFirst={isFirst}
|
||||
borderColor={borderColor}
|
||||
borderDimColor={borderDimColor}
|
||||
/>
|
||||
{tool.status === ToolCallStatus.Confirming &&
|
||||
isConfirming &&
|
||||
tool.confirmationDetails && (
|
||||
<ToolConfirmationMessage
|
||||
confirmationDetails={tool.confirmationDetails}
|
||||
config={config}
|
||||
isFocused={isFocused}
|
||||
availableTerminalHeight={
|
||||
availableTerminalHeightPerToolMessage
|
||||
}
|
||||
terminalWidth={innerWidth}
|
||||
/>
|
||||
<Box
|
||||
borderLeft={true}
|
||||
borderRight={true}
|
||||
borderTop={false}
|
||||
borderBottom={isLast}
|
||||
borderColor={borderColor}
|
||||
borderDimColor={borderDimColor}
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
paddingLeft={1}
|
||||
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 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,
|
||||
confirmationDetails: undefined,
|
||||
emphasis: 'medium',
|
||||
isFirst: true,
|
||||
borderColor: 'green',
|
||||
borderDimColor: false,
|
||||
};
|
||||
|
||||
it('renders basic tool information', () => {
|
||||
|
||||
@@ -42,6 +42,9 @@ export interface ToolMessageProps extends IndividualToolCallDisplay {
|
||||
renderOutputAsMarkdown?: boolean;
|
||||
activeShellPtyId?: number | null;
|
||||
embeddedShellFocused?: boolean;
|
||||
isFirst: boolean;
|
||||
borderColor: string;
|
||||
borderDimColor: boolean;
|
||||
config?: Config;
|
||||
}
|
||||
|
||||
@@ -58,6 +61,9 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
||||
embeddedShellFocused,
|
||||
ptyId,
|
||||
config,
|
||||
isFirst,
|
||||
borderColor,
|
||||
borderDimColor,
|
||||
}) => {
|
||||
const { renderMarkdown } = useUIState();
|
||||
const isAlternateBuffer = useAlternateBuffer();
|
||||
@@ -116,7 +122,8 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
||||
if (availableHeight && !isAlternateBuffer) {
|
||||
renderOutputAsMarkdown = false;
|
||||
}
|
||||
const childWidth = terminalWidth;
|
||||
const combinedPaddingAndBorderWidth = 4;
|
||||
const childWidth = terminalWidth - combinedPaddingAndBorderWidth;
|
||||
|
||||
const truncatedResultDisplay = React.useMemo(() => {
|
||||
if (typeof resultDisplay === 'string') {
|
||||
@@ -131,7 +138,7 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
||||
if (!truncatedResultDisplay) return null;
|
||||
|
||||
return (
|
||||
<Box width={terminalWidth} flexDirection="column" paddingLeft={1}>
|
||||
<Box width={childWidth} flexDirection="column">
|
||||
<Box flexDirection="column">
|
||||
{typeof truncatedResultDisplay === 'string' &&
|
||||
renderOutputAsMarkdown ? (
|
||||
@@ -189,15 +196,16 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
||||
renderMarkdown,
|
||||
isAlternateBuffer,
|
||||
availableHeight,
|
||||
terminalWidth,
|
||||
]);
|
||||
|
||||
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} />
|
||||
<ToolInfo
|
||||
name={name}
|
||||
@@ -214,15 +222,28 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
||||
)}
|
||||
{emphasis === 'high' && <TrailingIndicator />}
|
||||
</StickyHeader>
|
||||
{renderedResult}
|
||||
{isThisShellFocused && config && (
|
||||
<Box paddingLeft={STATUS_INDICATOR_WIDTH} marginTop={1}>
|
||||
<ShellInputPrompt
|
||||
activeShellPtyId={activeShellPtyId ?? null}
|
||||
focus={embeddedShellFocused}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<Box
|
||||
width={terminalWidth}
|
||||
borderStyle="round"
|
||||
borderColor={borderColor}
|
||||
borderDimColor={borderDimColor}
|
||||
borderTop={false}
|
||||
borderBottom={false}
|
||||
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,
|
||||
confirmationDetails: undefined,
|
||||
emphasis: 'medium',
|
||||
isFirst: true,
|
||||
borderColor: 'green',
|
||||
borderDimColor: false,
|
||||
};
|
||||
|
||||
it.each([
|
||||
|
||||
@@ -2,108 +2,182 @@
|
||||
|
||||
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`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│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`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│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`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│MockTool[tool-1]: ? first-confirm - A tool for testing (high) │
|
||||
│MockConfirmation: Confirm first tool │
|
||||
│ ? first-confirm A tool for testing ← │
|
||||
│ │
|
||||
│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 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`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│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`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│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`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│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`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│MockTool[tool-confirm]: ? confirmation-tool - This tool needs confirmation │
|
||||
│(high) │
|
||||
│MockConfirmation: Are you sure you want to proceed? │
|
||||
│ ? confirmation-tool This tool needs confirmation ← │
|
||||
│ │
|
||||
│ 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 │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
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`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│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`] = `
|
||||
"╭──────────────────────────────────────╮
|
||||
│MockTool[tool-123]: ✓ │
|
||||
│very-long-tool-name-that-might-wrap │
|
||||
│- This is a very long description │
|
||||
│that might cause wrapping issues │
|
||||
│(medium) │
|
||||
│ ✓ very-long-tool-name-that-might-wr │
|
||||
│ ap This is a very long │
|
||||
│ description that might cause │
|
||||
│ wrapping issues │
|
||||
│ │
|
||||
│ Test result │
|
||||
╰──────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
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
|
||||
|
||||
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`] = `
|
||||
" ✓ 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`] = `
|
||||
" ✓ 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`] = `
|
||||
" ✓ 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`] = `
|
||||
" ✓ 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`] = `
|
||||
" ✓ test-tool A tool for testing
|
||||
Test bold and code markdown"
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ Test bold and code markdown │"
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user