feat(ui): Introduce useUI Hook and UIContext (#5488)

Co-authored-by: Jacob Richman <jacob314@gmail.com>
This commit is contained in:
Keith Lyons
2025-09-06 01:39:02 -04:00
committed by GitHub
parent fe15b04f33
commit 885af07ddb
40 changed files with 3443 additions and 3388 deletions
@@ -7,13 +7,16 @@
import { render } from 'ink-testing-library';
import { describe, it, expect, vi } from 'vitest';
import { Text } from 'ink';
import type React from 'react';
import { ToolGroupMessage } from './ToolGroupMessage.js';
import { type IndividualToolCallDisplay, ToolCallStatus } from '../../types.js';
import type { IndividualToolCallDisplay } from '../../types.js';
import { ToolCallStatus } from '../../types.js';
import type {
Config,
ToolCallConfirmationDetails,
} from '@google/gemini-cli-core';
import { TOOL_STATUS } from '../../constants.js';
import { ConfigContext } from '../../contexts/ConfigContext.js';
// Mock child components to isolate ToolGroupMessage behavior
vi.mock('./ToolMessage.js', () => ({
@@ -81,14 +84,21 @@ describe('<ToolGroupMessage />', () => {
const baseProps = {
groupId: 1,
terminalWidth: 80,
config: mockConfig,
isFocused: true,
};
// Helper to wrap component with required providers
const renderWithProviders = (component: React.ReactElement) =>
render(
<ConfigContext.Provider value={mockConfig}>
{component}
</ConfigContext.Provider>,
);
describe('Golden Snapshots', () => {
it('renders single successful tool call', () => {
const toolCalls = [createToolCall()];
const { lastFrame } = render(
const { lastFrame } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
);
expect(lastFrame()).toMatchSnapshot();
@@ -115,7 +125,7 @@ describe('<ToolGroupMessage />', () => {
status: ToolCallStatus.Error,
}),
];
const { lastFrame } = render(
const { lastFrame } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
);
expect(lastFrame()).toMatchSnapshot();
@@ -136,7 +146,7 @@ describe('<ToolGroupMessage />', () => {
},
}),
];
const { lastFrame } = render(
const { lastFrame } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
);
expect(lastFrame()).toMatchSnapshot();
@@ -151,7 +161,7 @@ describe('<ToolGroupMessage />', () => {
status: ToolCallStatus.Success,
}),
];
const { lastFrame } = render(
const { lastFrame } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
);
expect(lastFrame()).toMatchSnapshot();
@@ -178,7 +188,7 @@ describe('<ToolGroupMessage />', () => {
status: ToolCallStatus.Pending,
}),
];
const { lastFrame } = render(
const { lastFrame } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
);
expect(lastFrame()).toMatchSnapshot();
@@ -200,7 +210,7 @@ describe('<ToolGroupMessage />', () => {
resultDisplay: 'More output here',
}),
];
const { lastFrame } = render(
const { lastFrame } = renderWithProviders(
<ToolGroupMessage
{...baseProps}
toolCalls={toolCalls}
@@ -212,7 +222,7 @@ describe('<ToolGroupMessage />', () => {
it('renders when not focused', () => {
const toolCalls = [createToolCall()];
const { lastFrame } = render(
const { lastFrame } = renderWithProviders(
<ToolGroupMessage
{...baseProps}
toolCalls={toolCalls}
@@ -230,7 +240,7 @@ describe('<ToolGroupMessage />', () => {
'This is a very long description that might cause wrapping issues',
}),
];
const { lastFrame } = render(
const { lastFrame } = renderWithProviders(
<ToolGroupMessage
{...baseProps}
toolCalls={toolCalls}
@@ -241,7 +251,7 @@ describe('<ToolGroupMessage />', () => {
});
it('renders empty tool calls array', () => {
const { lastFrame } = render(
const { lastFrame } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={[]} />,
);
expect(lastFrame()).toMatchSnapshot();
@@ -251,7 +261,7 @@ describe('<ToolGroupMessage />', () => {
describe('Border Color Logic', () => {
it('uses yellow border when tools are pending', () => {
const toolCalls = [createToolCall({ status: ToolCallStatus.Pending })];
const { lastFrame } = render(
const { lastFrame } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
);
// The snapshot will capture the visual appearance including border color
@@ -265,7 +275,7 @@ describe('<ToolGroupMessage />', () => {
status: ToolCallStatus.Success,
}),
];
const { lastFrame } = render(
const { lastFrame } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
);
expect(lastFrame()).toMatchSnapshot();
@@ -280,7 +290,7 @@ describe('<ToolGroupMessage />', () => {
status: ToolCallStatus.Success,
}),
];
const { lastFrame } = render(
const { lastFrame } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
);
expect(lastFrame()).toMatchSnapshot();
@@ -303,7 +313,7 @@ describe('<ToolGroupMessage />', () => {
resultDisplay: '', // No result
}),
];
const { lastFrame } = render(
const { lastFrame } = renderWithProviders(
<ToolGroupMessage
{...baseProps}
toolCalls={toolCalls}
@@ -340,7 +350,7 @@ describe('<ToolGroupMessage />', () => {
},
}),
];
const { lastFrame } = render(
const { lastFrame } = renderWithProviders(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
);
// Should only show confirmation for the first tool
@@ -11,16 +11,15 @@ import type { IndividualToolCallDisplay } from '../../types.js';
import { ToolCallStatus } from '../../types.js';
import { ToolMessage } from './ToolMessage.js';
import { ToolConfirmationMessage } from './ToolConfirmationMessage.js';
import { Colors } from '../../colors.js';
import type { Config } from '@google/gemini-cli-core';
import { theme } from '../../semantic-colors.js';
import { SHELL_COMMAND_NAME } from '../../constants.js';
import { useConfig } from '../../contexts/ConfigContext.js';
interface ToolGroupMessageProps {
groupId: number;
toolCalls: IndividualToolCallDisplay[];
availableTerminalHeight?: number;
terminalWidth: number;
config: Config;
isFocused?: boolean;
}
@@ -29,15 +28,15 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
toolCalls,
availableTerminalHeight,
terminalWidth,
config,
isFocused = true,
}) => {
const config = useConfig();
const hasPending = !toolCalls.every(
(t) => t.status === ToolCallStatus.Success,
);
const isShellCommand = toolCalls.some((t) => t.name === SHELL_COMMAND_NAME);
const borderColor =
hasPending || isShellCommand ? Colors.AccentYellow : Colors.Gray;
hasPending || isShellCommand ? theme.status.warning : theme.border.default;
const staticHeight = /* border */ 2 + /* marginBottom */ 1;
// This is a bit of a magic number, but it accounts for the border and