Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 24 additions & 29 deletions packages/cli/src/ui/components/Composer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ vi.mock('./LoadingIndicator.js', () => ({
),
}));

vi.mock('./StatusDisplay.js', () => ({
StatusDisplay: () => <Text>StatusDisplay</Text>,
}));

vi.mock('./ToastDisplay.js', () => ({
ToastDisplay: () => <Text>ToastDisplay</Text>,
}));

vi.mock('./ContextSummaryDisplay.js', () => ({
ContextSummaryDisplay: () => <Text>ContextSummaryDisplay</Text>,
}));
Expand Down Expand Up @@ -410,7 +418,7 @@ describe('Composer', () => {
});

describe('Context and Status Display', () => {
it('shows ContextSummaryDisplay in normal state', () => {
it('shows StatusDisplay and ApprovalModeIndicator in normal state', () => {
const uiState = createMockUIState({
ctrlCPressedOnce: false,
ctrlDPressedOnce: false,
Expand All @@ -419,49 +427,36 @@ describe('Composer', () => {

const { lastFrame } = renderComposer(uiState);

expect(lastFrame()).toContain('ContextSummaryDisplay');
});

it('renders HookStatusDisplay instead of ContextSummaryDisplay with active hooks', () => {
const uiState = createMockUIState({
activeHooks: [{ name: 'test-hook', eventName: 'before-agent' }],
});

const { lastFrame } = renderComposer(uiState);

expect(lastFrame()).toContain('HookStatusDisplay');
expect(lastFrame()).not.toContain('ContextSummaryDisplay');
const output = lastFrame();
expect(output).toContain('StatusDisplay');
expect(output).toContain('ApprovalModeIndicator');
expect(output).not.toContain('ToastDisplay');
});

it('shows Ctrl+C exit prompt when ctrlCPressedOnce is true', () => {
it('shows ToastDisplay and hides ApprovalModeIndicator when a toast is present', () => {
const uiState = createMockUIState({
ctrlCPressedOnce: true,
});

const { lastFrame } = renderComposer(uiState);

expect(lastFrame()).toContain('Press Ctrl+C again to exit');
});

it('shows Ctrl+D exit prompt when ctrlDPressedOnce is true', () => {
const uiState = createMockUIState({
ctrlDPressedOnce: true,
});

const { lastFrame } = renderComposer(uiState);

expect(lastFrame()).toContain('Press Ctrl+D again to exit');
const output = lastFrame();
expect(output).toContain('ToastDisplay');
expect(output).not.toContain('ApprovalModeIndicator');
// StatusDisplay should still be present now!
expect(output).toContain('StatusDisplay');
});

it('shows escape prompt when showEscapePrompt is true', () => {
it('shows ToastDisplay for other toast types', () => {
const uiState = createMockUIState({
showEscapePrompt: true,
history: [{ id: 1, type: 'user', text: 'test' }],
warningMessage: 'Warning',
});

const { lastFrame } = renderComposer(uiState);

expect(lastFrame()).toContain('Press Esc again to rewind');
const output = lastFrame();
expect(output).toContain('ToastDisplay');
expect(output).not.toContain('ApprovalModeIndicator');
});
});

Expand Down
88 changes: 50 additions & 38 deletions packages/cli/src/ui/components/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useState } from 'react';
import { Box, useIsScreenReaderEnabled } from 'ink';
import { LoadingIndicator } from './LoadingIndicator.js';
import { StatusDisplay } from './StatusDisplay.js';
import { ToastDisplay } from './ToastDisplay.js';
import { ApprovalModeIndicator } from './ApprovalModeIndicator.js';
import { ShellModeIndicator } from './ShellModeIndicator.js';
import { DetailedMessagesDisplay } from './DetailedMessagesDisplay.js';
Expand Down Expand Up @@ -62,6 +63,13 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
Boolean(uiState.proQuotaRequest) ||
Boolean(uiState.validationRequest) ||
Boolean(uiState.customDialog);
const hasToast =
uiState.ctrlCPressedOnce ||
Boolean(uiState.warningMessage) ||
uiState.ctrlDPressedOnce ||
(uiState.showEscapePrompt &&
(uiState.buffer.text.length > 0 || uiState.history.length > 0)) ||
Boolean(uiState.queueErrorMessage);
const showLoadingIndicator =
(!uiState.embeddedShellFocused || uiState.isBackgroundShellVisible) &&
uiState.streamingState === StreamingState.Responding &&
Expand Down Expand Up @@ -148,44 +156,48 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
alignItems="center"
flexGrow={1}
>
{!showLoadingIndicator && (
<Box
flexDirection={isNarrow ? 'column' : 'row'}
alignItems={isNarrow ? 'flex-start' : 'center'}
>
{showApprovalIndicator && (
<ApprovalModeIndicator
approvalMode={showApprovalModeIndicator}
isPlanEnabled={config.isPlanEnabled()}
/>
)}
{uiState.shellModeActive && (
<Box
marginLeft={showApprovalIndicator && !isNarrow ? 1 : 0}
marginTop={showApprovalIndicator && isNarrow ? 1 : 0}
>
<ShellModeIndicator />
</Box>
)}
{showRawMarkdownIndicator && (
<Box
marginLeft={
(showApprovalIndicator || uiState.shellModeActive) &&
!isNarrow
? 1
: 0
}
marginTop={
(showApprovalIndicator || uiState.shellModeActive) &&
isNarrow
? 1
: 0
}
>
<RawMarkdownIndicator />
</Box>
)}
</Box>
{hasToast ? (
<ToastDisplay />
) : (
!showLoadingIndicator && (
<Box
flexDirection={isNarrow ? 'column' : 'row'}
alignItems={isNarrow ? 'flex-start' : 'center'}
>
{showApprovalIndicator && (
<ApprovalModeIndicator
approvalMode={showApprovalModeIndicator}
isPlanEnabled={config.isPlanEnabled()}
/>
)}
{uiState.shellModeActive && (
<Box
marginLeft={showApprovalIndicator && !isNarrow ? 1 : 0}
marginTop={showApprovalIndicator && isNarrow ? 1 : 0}
>
<ShellModeIndicator />
</Box>
)}
{showRawMarkdownIndicator && (
<Box
marginLeft={
(showApprovalIndicator || uiState.shellModeActive) &&
!isNarrow
? 1
: 0
}
marginTop={
(showApprovalIndicator || uiState.shellModeActive) &&
isNarrow
? 1
: 0
}
>
<RawMarkdownIndicator />
</Box>
)}
</Box>
)
)}
</Box>

Expand Down
106 changes: 0 additions & 106 deletions packages/cli/src/ui/components/StatusDisplay.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { render } from '../../test-utils/render.js';
import { Text } from 'ink';
import { StatusDisplay } from './StatusDisplay.js';
import { UIStateContext, type UIState } from '../contexts/UIStateContext.js';
import { TransientMessageType } from '../../utils/events.js';
import { ConfigContext } from '../contexts/ConfigContext.js';
import { SettingsContext } from '../contexts/SettingsContext.js';
import { createMockSettings } from '../../test-utils/settings.js';
Expand Down Expand Up @@ -110,111 +109,6 @@ describe('StatusDisplay', () => {
expect(lastFrame()).toMatchSnapshot();
});

it('prioritizes Ctrl+C prompt over everything else (except system md)', () => {
const uiState = createMockUIState({
ctrlCPressedOnce: true,
transientMessage: {
text: 'Warning',
type: TransientMessageType.Warning,
},
activeHooks: [{ name: 'hook', eventName: 'event' }],
});
const { lastFrame } = renderStatusDisplay(
{ hideContextSummary: false },
uiState,
);
expect(lastFrame()).toMatchSnapshot();
});

it('renders warning message', () => {
const uiState = createMockUIState({
transientMessage: {
text: 'This is a warning',
type: TransientMessageType.Warning,
},
});
const { lastFrame } = renderStatusDisplay(
{ hideContextSummary: false },
uiState,
);
expect(lastFrame()).toMatchSnapshot();
});

it('renders hint message', () => {
const uiState = createMockUIState({
transientMessage: {
text: 'This is a hint',
type: TransientMessageType.Hint,
},
});
const { lastFrame } = renderStatusDisplay(
{ hideContextSummary: false },
uiState,
);
expect(lastFrame()).toMatchSnapshot();
});

it('prioritizes warning over Ctrl+D', () => {
const uiState = createMockUIState({
transientMessage: {
text: 'Warning',
type: TransientMessageType.Warning,
},
ctrlDPressedOnce: true,
});
const { lastFrame } = renderStatusDisplay(
{ hideContextSummary: false },
uiState,
);
expect(lastFrame()).toMatchSnapshot();
});

it('renders Ctrl+D prompt', () => {
const uiState = createMockUIState({
ctrlDPressedOnce: true,
});
const { lastFrame } = renderStatusDisplay(
{ hideContextSummary: false },
uiState,
);
expect(lastFrame()).toMatchSnapshot();
});

it('renders Escape prompt when buffer is empty', () => {
const uiState = createMockUIState({
showEscapePrompt: true,
buffer: { text: '' },
});
const { lastFrame } = renderStatusDisplay(
{ hideContextSummary: false },
uiState,
);
expect(lastFrame()).toMatchSnapshot();
});

it('renders Escape prompt when buffer is NOT empty', () => {
const uiState = createMockUIState({
showEscapePrompt: true,
buffer: { text: 'some text' },
});
const { lastFrame } = renderStatusDisplay(
{ hideContextSummary: false },
uiState,
);
expect(lastFrame()).toMatchSnapshot();
});

it('renders Queue Error Message', () => {
const uiState = createMockUIState({
queueErrorMessage: 'Queue Error',
});
const { lastFrame } = renderStatusDisplay(
{ hideContextSummary: false },
uiState,
);
expect(lastFrame()).toMatchSnapshot();
});

it('renders HookStatusDisplay when hooks are active', () => {
const uiState = createMockUIState({
activeHooks: [{ name: 'hook', eventName: 'event' }],
Expand Down
50 changes: 0 additions & 50 deletions packages/cli/src/ui/components/StatusDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import type React from 'react';
import { Text } from 'ink';
import { theme } from '../semantic-colors.js';
import { useUIState } from '../contexts/UIStateContext.js';
import { TransientMessageType } from '../../utils/events.js';
import { useSettings } from '../contexts/SettingsContext.js';
import { useConfig } from '../contexts/ConfigContext.js';
import { ContextSummaryDisplay } from './ContextSummaryDisplay.js';
Expand All @@ -29,55 +28,6 @@ export const StatusDisplay: React.FC<StatusDisplayProps> = ({
return <Text color={theme.status.error}>|⌐■_■|</Text>;
}

if (uiState.ctrlCPressedOnce) {
return (
<Text color={theme.status.warning}>Press Ctrl+C again to exit.</Text>
);
}

if (
uiState.transientMessage?.type === TransientMessageType.Warning &&
uiState.transientMessage.text
) {
return (
<Text color={theme.status.warning}>{uiState.transientMessage.text}</Text>
);
}

if (uiState.ctrlDPressedOnce) {
return (
<Text color={theme.status.warning}>Press Ctrl+D again to exit.</Text>
);
}

if (uiState.showEscapePrompt) {
const isPromptEmpty = uiState.buffer.text.length === 0;
const hasHistory = uiState.history.length > 0;

if (isPromptEmpty && !hasHistory) {
return null;
}

return (
<Text color={theme.text.secondary}>
Press Esc again to {isPromptEmpty ? 'rewind' : 'clear prompt'}.
</Text>
);
}

if (
uiState.transientMessage?.type === TransientMessageType.Hint &&
uiState.transientMessage.text
) {
return (
<Text color={theme.text.secondary}>{uiState.transientMessage.text}</Text>
);
}

if (uiState.queueErrorMessage) {
return <Text color={theme.status.error}>{uiState.queueErrorMessage}</Text>;
}

if (
uiState.activeHooks.length > 0 &&
settings.merged.hooksConfig.notifications
Expand Down
Loading
Loading