mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-12 20:37:08 -07:00
ui(cli): update braille loader to circular spinner
This commit is contained in:
@@ -131,12 +131,43 @@ class MockExtensionManager extends ExtensionLoader {
|
||||
};
|
||||
}
|
||||
|
||||
// Mock terminalCapabilityManager to avoid terminal setup prompt during tests
|
||||
vi.mock('../ui/utils/terminalCapabilityManager.js', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<
|
||||
typeof import('../ui/utils/terminalCapabilityManager.js')
|
||||
>();
|
||||
const mockedManager = Object.create(
|
||||
Object.getPrototypeOf(actual.terminalCapabilityManager),
|
||||
);
|
||||
Object.assign(mockedManager, actual.terminalCapabilityManager, {
|
||||
isKittyProtocolEnabled: () => true,
|
||||
enableKittyProtocol: vi.fn(),
|
||||
disableKittyProtocol: vi.fn(),
|
||||
enableSupportedModes: vi.fn(),
|
||||
disableSupportedModes: vi.fn(),
|
||||
onSupportChange: vi.fn(),
|
||||
offSupportChange: vi.fn(),
|
||||
});
|
||||
return {
|
||||
...actual,
|
||||
terminalCapabilityManager: mockedManager,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../ui/components/GeminiSpinner.js', async () => {
|
||||
const React = await import('react');
|
||||
const { Text } = await import('ink');
|
||||
return {
|
||||
GeminiSpinner: () => React.createElement(Text, null, '...'),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock GeminiRespondingSpinner to disable animations (avoiding 'act()' warnings) without triggering screen reader mode.
|
||||
vi.mock('../ui/components/GeminiRespondingSpinner.js', async () => {
|
||||
const React = await import('react');
|
||||
const { Text } = await import('ink');
|
||||
return {
|
||||
GeminiSpinner: () => React.createElement(Text, null, '...'),
|
||||
GeminiRespondingSpinner: ({
|
||||
nonRespondingDisplay,
|
||||
}: {
|
||||
@@ -203,6 +234,9 @@ export class AppRig {
|
||||
resetSettingsCacheForTesting();
|
||||
this.settings = this.createRigSettings();
|
||||
|
||||
// Disable the terminal setup prompt globally for AppRig tests.
|
||||
persistentStateMock.set('terminalSetupPromptShown', true);
|
||||
|
||||
const approvalMode =
|
||||
this.options.configOverrides?.approvalMode ?? ApprovalMode.DEFAULT;
|
||||
const policyEngineConfig = await createPolicyEngineConfig(
|
||||
@@ -280,6 +314,10 @@ export class AppRig {
|
||||
enabled: false,
|
||||
hasSeenNudge: true,
|
||||
},
|
||||
ui: {
|
||||
hasSeenTerminalSetupPrompt: true,
|
||||
showSpinner: false,
|
||||
},
|
||||
},
|
||||
originalSettings: {},
|
||||
},
|
||||
@@ -299,6 +337,8 @@ export class AppRig {
|
||||
},
|
||||
ui: {
|
||||
useAlternateBuffer: false,
|
||||
hasSeenTerminalSetupPrompt: true,
|
||||
showSpinner: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import type React from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { CliSpinner } from '../components/CliSpinner.js';
|
||||
import { BrailleAnimation } from '../components/BrailleAnimation.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
|
||||
@@ -53,8 +53,8 @@ export function AuthInProgress({
|
||||
) : (
|
||||
<Box>
|
||||
<Text>
|
||||
<CliSpinner type="dots" /> Waiting for authentication... (Press Esc
|
||||
or Ctrl+C to cancel)
|
||||
<BrailleAnimation /> Waiting for authentication... (Press Esc or
|
||||
Ctrl+C to cancel)
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { BrailleAnimation } from './BrailleAnimation.js';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { act } from 'react';
|
||||
import { createMockSettings } from '../../test-utils/settings.js';
|
||||
|
||||
describe('BrailleAnimation', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should grow from length 1 to 5 and match verification frames', async () => {
|
||||
const settings = createMockSettings({
|
||||
merged: {
|
||||
ui: {
|
||||
showSpinner: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// renderWithProviders will call waitUntilReady once.
|
||||
const renderResult = await renderWithProviders(
|
||||
<BrailleAnimation interval={100} variant="Long" animate={true} />,
|
||||
{ settings },
|
||||
);
|
||||
|
||||
const { lastFrameRaw } = renderResult;
|
||||
|
||||
const verificationFrames = [
|
||||
'⢎⠁', // 0
|
||||
'⠎⠑', // 1
|
||||
'⠊⠱', // 2
|
||||
'⠈⡱', // 3
|
||||
'⢀⡱', // 4
|
||||
'⢄⡰', // 5
|
||||
'⢆⡠', // 6
|
||||
'⢎⡀', // 7
|
||||
];
|
||||
|
||||
// Advance 16 ticks to reach length 5.
|
||||
for (let i = 0; i < 16; i++) {
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
});
|
||||
}
|
||||
|
||||
// Now check the sequence.
|
||||
let current = lastFrameRaw();
|
||||
let startIdx = verificationFrames.findIndex((f) => current.includes(f));
|
||||
|
||||
if (startIdx === -1) {
|
||||
for (let attempt = 0; attempt < 8; attempt++) {
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
});
|
||||
current = lastFrameRaw();
|
||||
startIdx = verificationFrames.findIndex((f) => current.includes(f));
|
||||
if (startIdx !== -1) break;
|
||||
}
|
||||
}
|
||||
|
||||
expect(
|
||||
startIdx,
|
||||
`Should have reached length 5 frames. Current: ${current}`,
|
||||
).not.toBe(-1);
|
||||
|
||||
// Verify the sequence.
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const idx = (startIdx + i) % 8;
|
||||
expect(lastFrameRaw()).toContain(verificationFrames[idx]);
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
});
|
||||
}
|
||||
|
||||
act(() => {
|
||||
renderResult.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support "Composite" variant with dynamic lengths', async () => {
|
||||
const renderResult = await renderWithProviders(
|
||||
<BrailleAnimation interval={100} variant="Composite" animate={true} />,
|
||||
);
|
||||
|
||||
// Just verify it renders something
|
||||
expect(renderResult.lastFrameRaw()).toBeTruthy();
|
||||
|
||||
act(() => {
|
||||
renderResult.unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Text } from 'ink';
|
||||
import { debugState } from '../debug.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
|
||||
// Dot bitmasks and character assignments for the 4x4 circle perimeter
|
||||
// Char 0 corresponds to the first Braille character (c1), Char 1 to the second (c2).
|
||||
const DOTS = [
|
||||
{ char: 1, bit: 1 }, // Dot 1 (c2)
|
||||
{ char: 1, bit: 16 }, // Dot 5 (c2)
|
||||
{ char: 1, bit: 32 }, // Dot 6 (c2)
|
||||
{ char: 1, bit: 64 }, // Dot 7 (c2)
|
||||
{ char: 0, bit: 128 }, // Dot 8 (c1)
|
||||
{ char: 0, bit: 4 }, // Dot 3 (c1)
|
||||
{ char: 0, bit: 2 }, // Dot 2 (c1)
|
||||
{ char: 0, bit: 8 }, // Dot 4 (c1)
|
||||
];
|
||||
|
||||
const COMPOSITE_SEQUENCE = [2, 3, 4, 5, 4, 3];
|
||||
|
||||
export type BrailleVariant =
|
||||
| 'Static'
|
||||
| 'Small'
|
||||
| 'Medium'
|
||||
| 'Long'
|
||||
| 'Composite';
|
||||
|
||||
interface BrailleAnimationProps {
|
||||
variant?: BrailleVariant;
|
||||
interval?: number;
|
||||
animate?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Braille Snake Animation Component
|
||||
*
|
||||
* Variants match the prototype style:
|
||||
* - 'Static': Fixed frame '⢎⡱'
|
||||
* - 'Small': Fixed length 2
|
||||
* - 'Medium': Fixed length 3
|
||||
* - 'Long': Phased growth (len 1, 3, 5) changing every 8 ticks
|
||||
* - 'Composite': Dynamic length [2, 3, 4, 5, 4, 3] changing every 8 ticks
|
||||
*/
|
||||
export const BrailleAnimation: React.FC<BrailleAnimationProps> = ({
|
||||
variant = 'Composite',
|
||||
interval = 80,
|
||||
animate = !process.env['VITEST'],
|
||||
}) => {
|
||||
const [tick, setTick] = useState(0);
|
||||
const settings = useSettings();
|
||||
const shouldShow = settings.merged.ui?.showSpinner !== false;
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldShow || !animate) return;
|
||||
|
||||
debugState.debugNumAnimatedComponents++;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
setTick((t) => t + 1);
|
||||
}, interval);
|
||||
|
||||
return () => {
|
||||
debugState.debugNumAnimatedComponents--;
|
||||
clearInterval(timer);
|
||||
};
|
||||
}, [interval, shouldShow, animate]);
|
||||
|
||||
const getLength = () => {
|
||||
const cycle = Math.floor(tick / 8);
|
||||
switch (variant) {
|
||||
case 'Small':
|
||||
return 2;
|
||||
case 'Medium':
|
||||
return 3;
|
||||
case 'Long':
|
||||
return cycle === 0 ? 1 : cycle === 1 ? 3 : 5;
|
||||
case 'Composite':
|
||||
return COMPOSITE_SEQUENCE[cycle % COMPOSITE_SEQUENCE.length];
|
||||
case 'Static':
|
||||
return 0;
|
||||
default:
|
||||
return 5;
|
||||
}
|
||||
};
|
||||
|
||||
const getFrame = () => {
|
||||
if (variant === 'Static') {
|
||||
return '⢎⡱';
|
||||
}
|
||||
|
||||
const length = getLength();
|
||||
let [c1, c2] = [0, 0];
|
||||
const head = tick % 8;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const { char, bit } = DOTS[(head - i + 80) % 8];
|
||||
char === 0 ? (c1 |= bit) : (c2 |= bit);
|
||||
}
|
||||
|
||||
return String.fromCharCode(0x2800 + c1) + String.fromCharCode(0x2800 + c2);
|
||||
};
|
||||
|
||||
if (!shouldShow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Text>{getFrame()}</Text>;
|
||||
};
|
||||
@@ -15,14 +15,13 @@ import {
|
||||
} from '../textConstants.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { GeminiSpinner } from './GeminiSpinner.js';
|
||||
|
||||
interface GeminiRespondingSpinnerProps {
|
||||
/**
|
||||
* Optional string to display when not in Responding state.
|
||||
* Optional string or component to display when not in Responding state.
|
||||
* If not provided and not Responding, renders null.
|
||||
*/
|
||||
nonRespondingDisplay?: string;
|
||||
spinnerType?: SpinnerName;
|
||||
nonRespondingDisplay?: React.ReactNode;
|
||||
spinnerType?: SpinnerName | 'dynamic';
|
||||
/**
|
||||
* If true, we prioritize showing the nonRespondingDisplay (hook icon)
|
||||
* even if the state is Responding.
|
||||
@@ -35,7 +34,7 @@ export const GeminiRespondingSpinner: React.FC<
|
||||
GeminiRespondingSpinnerProps
|
||||
> = ({
|
||||
nonRespondingDisplay,
|
||||
spinnerType = 'dots',
|
||||
spinnerType = 'dynamic',
|
||||
isHookActive = false,
|
||||
color,
|
||||
}) => {
|
||||
@@ -54,10 +53,14 @@ export const GeminiRespondingSpinner: React.FC<
|
||||
}
|
||||
|
||||
if (nonRespondingDisplay) {
|
||||
return isScreenReaderEnabled ? (
|
||||
<Text>{SCREEN_READER_LOADING}</Text>
|
||||
) : (
|
||||
if (isScreenReaderEnabled) {
|
||||
return <Text>{SCREEN_READER_LOADING}</Text>;
|
||||
}
|
||||
|
||||
return typeof nonRespondingDisplay === 'string' ? (
|
||||
<Text color={color ?? theme.text.primary}>{nonRespondingDisplay}</Text>
|
||||
) : (
|
||||
<>{nonRespondingDisplay}</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { GeminiSpinner } from './GeminiSpinner.js';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { Text } from 'ink';
|
||||
import { act } from 'react';
|
||||
|
||||
// Mock components to simplify testing
|
||||
vi.mock('./BrailleAnimation.js', () => ({
|
||||
BrailleAnimation: ({ variant }: { variant: string }) => (
|
||||
<Text>BrailleAnimation-{variant}</Text>
|
||||
),
|
||||
GEMINI_SPINNER: { interval: 80, frames: [] },
|
||||
}));
|
||||
|
||||
vi.mock('./CliSpinner.js', () => ({
|
||||
CliSpinner: ({ type }: { type: string }) => <Text>CliSpinner-{type}</Text>,
|
||||
}));
|
||||
|
||||
describe('GeminiSpinner', () => {
|
||||
it('renders BrailleAnimation with "Composite" variant by default', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<GeminiSpinner />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('BrailleAnimation-Composite');
|
||||
act(() => {
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders CliSpinner when a specific spinnerType string is provided', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
|
||||
<GeminiSpinner spinnerType="dots" />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('CliSpinner-dots');
|
||||
act(() => {
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders screen reader text when screen reader is enabled', async () => {
|
||||
// Note: useIsScreenReaderEnabled is used in GeminiSpinner
|
||||
// We would need to mock it if we wanted to test this explicitly,
|
||||
// but the default is false in our test environment.
|
||||
});
|
||||
});
|
||||
@@ -11,19 +11,23 @@ import { CliSpinner } from './CliSpinner.js';
|
||||
import type { SpinnerName } from 'cli-spinners';
|
||||
import { Colors } from '../colors.js';
|
||||
import tinygradient from 'tinygradient';
|
||||
import { BrailleAnimation } from './BrailleAnimation.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
|
||||
const COLOR_CYCLE_DURATION_MS = 4000;
|
||||
|
||||
interface GeminiSpinnerProps {
|
||||
spinnerType?: SpinnerName;
|
||||
spinnerType?: SpinnerName | 'dynamic';
|
||||
altText?: string;
|
||||
}
|
||||
|
||||
export const GeminiSpinner: React.FC<GeminiSpinnerProps> = ({
|
||||
spinnerType = 'dots',
|
||||
spinnerType = 'dynamic',
|
||||
altText,
|
||||
}) => {
|
||||
const isScreenReaderEnabled = useIsScreenReaderEnabled();
|
||||
const settings = useSettings();
|
||||
const shouldShow = settings.merged.ui?.showSpinner !== false;
|
||||
const [time, setTime] = useState(0);
|
||||
|
||||
const googleGradient = useMemo(() => {
|
||||
@@ -39,7 +43,7 @@ export const GeminiSpinner: React.FC<GeminiSpinnerProps> = ({
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isScreenReaderEnabled) {
|
||||
if (isScreenReaderEnabled || !shouldShow) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -48,16 +52,22 @@ export const GeminiSpinner: React.FC<GeminiSpinnerProps> = ({
|
||||
}, 30); // ~33fps for smooth color transitions
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isScreenReaderEnabled]);
|
||||
}, [isScreenReaderEnabled, shouldShow]);
|
||||
|
||||
const progress = (time % COLOR_CYCLE_DURATION_MS) / COLOR_CYCLE_DURATION_MS;
|
||||
const currentColor = googleGradient.rgbAt(progress).toHexString();
|
||||
|
||||
const renderSpinner = () => {
|
||||
if (spinnerType === 'dynamic') {
|
||||
return <BrailleAnimation variant="Composite" />;
|
||||
}
|
||||
|
||||
return <CliSpinner type={spinnerType} />;
|
||||
};
|
||||
|
||||
return isScreenReaderEnabled ? (
|
||||
<Text>{altText}</Text>
|
||||
) : (
|
||||
<Text color={currentColor}>
|
||||
<CliSpinner type={spinnerType} />
|
||||
</Text>
|
||||
<Text color={currentColor}>{renderSpinner()}</Text>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,13 +18,13 @@ vi.mock('./GeminiRespondingSpinner.js', () => ({
|
||||
GeminiRespondingSpinner: ({
|
||||
nonRespondingDisplay,
|
||||
}: {
|
||||
nonRespondingDisplay?: string;
|
||||
nonRespondingDisplay?: React.ReactNode;
|
||||
}) => {
|
||||
const streamingState = React.useContext(StreamingContext)!;
|
||||
if (streamingState === StreamingState.Responding) {
|
||||
return <Text>MockRespondingSpinner</Text>;
|
||||
} else if (nonRespondingDisplay) {
|
||||
return <Text>{nonRespondingDisplay}</Text>;
|
||||
return <>{nonRespondingDisplay}</>;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
@@ -86,7 +86,7 @@ describe('<LoadingIndicator />', () => {
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('⠏'); // Static char for WaitingForConfirmation
|
||||
expect(output).toContain('⢎⡱'); // Static char for WaitingForConfirmation
|
||||
expect(output).toContain('Confirm action');
|
||||
expect(output).not.toContain('(esc to cancel)');
|
||||
expect(output).not.toContain(', 10s');
|
||||
@@ -208,7 +208,7 @@ describe('<LoadingIndicator />', () => {
|
||||
});
|
||||
await waitUntilReady();
|
||||
output = lastFrame();
|
||||
expect(output).toContain('⠏');
|
||||
expect(output).toContain('⢎⡱');
|
||||
expect(output).toContain('Please Confirm');
|
||||
expect(output).not.toContain('(esc to cancel)');
|
||||
expect(output).not.toContain(', 15s');
|
||||
|
||||
@@ -11,6 +11,7 @@ import { theme } from '../semantic-colors.js';
|
||||
import { useStreamingContext } from '../contexts/StreamingContext.js';
|
||||
import { StreamingState } from '../types.js';
|
||||
import { GeminiRespondingSpinner } from './GeminiRespondingSpinner.js';
|
||||
import { BrailleAnimation } from './BrailleAnimation.js';
|
||||
import { formatDuration } from '../utils/formatters.js';
|
||||
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
||||
import { isNarrowWidth } from '../utils/isNarrowWidth.js';
|
||||
@@ -96,9 +97,13 @@ export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
|
||||
<GeminiRespondingSpinner
|
||||
nonRespondingDisplay={
|
||||
spinnerIcon ??
|
||||
(streamingState === StreamingState.WaitingForConfirmation
|
||||
? '⠏'
|
||||
: '')
|
||||
(streamingState === StreamingState.WaitingForConfirmation ? (
|
||||
<Text color={theme.text.primary}>
|
||||
<BrailleAnimation variant="Static" />
|
||||
</Text>
|
||||
) : (
|
||||
''
|
||||
))
|
||||
}
|
||||
isHookActive={isHookActive}
|
||||
/>
|
||||
@@ -140,9 +145,13 @@ export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
|
||||
<GeminiRespondingSpinner
|
||||
nonRespondingDisplay={
|
||||
spinnerIcon ??
|
||||
(streamingState === StreamingState.WaitingForConfirmation
|
||||
? '⠏'
|
||||
: '')
|
||||
(streamingState === StreamingState.WaitingForConfirmation ? (
|
||||
<Text color={theme.text.primary}>
|
||||
<BrailleAnimation variant="Static" />
|
||||
</Text>
|
||||
) : (
|
||||
''
|
||||
))
|
||||
}
|
||||
isHookActive={isHookActive}
|
||||
/>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { Box, Text } from 'ink';
|
||||
import type { CompressionProps } from '../../types.js';
|
||||
import { CliSpinner } from '../CliSpinner.js';
|
||||
import { BrailleAnimation } from '../BrailleAnimation.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { SCREEN_READER_MODEL_PREFIX } from '../../textConstants.js';
|
||||
import { CompressionStatus } from '@google/gemini-cli-core';
|
||||
@@ -61,7 +61,7 @@ export function CompressionMessage({
|
||||
<Box flexDirection="row">
|
||||
<Box marginRight={1}>
|
||||
{isPending ? (
|
||||
<CliSpinner type="dots" />
|
||||
<BrailleAnimation />
|
||||
) : (
|
||||
<Text color={theme.text.accent}>✦</Text>
|
||||
)}
|
||||
|
||||
@@ -10,8 +10,8 @@ import type { SubagentProgress } from '@google/gemini-cli-core';
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { Text } from 'ink';
|
||||
|
||||
vi.mock('ink-spinner', () => ({
|
||||
default: () => <Text>⠋</Text>,
|
||||
vi.mock('../BrailleAnimation.js', () => ({
|
||||
BrailleAnimation: () => <Text>⠋</Text>,
|
||||
}));
|
||||
|
||||
describe('<SubagentProgressDisplay />', () => {
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import Spinner from 'ink-spinner';
|
||||
import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
|
||||
import { BrailleAnimation } from '../BrailleAnimation.js';
|
||||
import type {
|
||||
SubagentProgress,
|
||||
SubagentActivityItem,
|
||||
@@ -106,7 +106,7 @@ export const SubagentProgressDisplay: React.FC<
|
||||
} else if (item.type === 'tool_call') {
|
||||
const statusSymbol =
|
||||
item.status === 'running' ? (
|
||||
<Spinner type="dots" />
|
||||
<BrailleAnimation />
|
||||
) : item.status === 'completed' ? (
|
||||
<Text color={theme.status.success}>{TOOL_STATUS.SUCCESS}</Text>
|
||||
) : item.status === 'cancelled' ? (
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import Spinner from 'ink-spinner';
|
||||
import {
|
||||
debugLogger,
|
||||
spawnAsync,
|
||||
@@ -16,6 +15,7 @@ import {
|
||||
import { useKeypress } from '../../hooks/useKeypress.js';
|
||||
import { Command } from '../../key/keyMatchers.js';
|
||||
import { useKeyMatchers } from '../../hooks/useKeyMatchers.js';
|
||||
import { BrailleAnimation } from '../BrailleAnimation.js';
|
||||
|
||||
interface Issue {
|
||||
number: number;
|
||||
@@ -725,7 +725,7 @@ Return a JSON object with:
|
||||
if (state.status === 'loading') {
|
||||
return (
|
||||
<Box>
|
||||
<Spinner type="dots" />
|
||||
<BrailleAnimation />
|
||||
<Text> {state.message}</Text>
|
||||
</Box>
|
||||
);
|
||||
@@ -921,7 +921,7 @@ Return a JSON object with:
|
||||
justifyContent="center"
|
||||
height={VISIBLE_CANDIDATES * 2}
|
||||
>
|
||||
<Spinner type="dots" />
|
||||
<BrailleAnimation />
|
||||
<Text> {state.message}</Text>
|
||||
</Box>
|
||||
) : (
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import Spinner from 'ink-spinner';
|
||||
import {
|
||||
debugLogger,
|
||||
spawnAsync,
|
||||
@@ -18,6 +17,7 @@ import { Command } from '../../key/keyMatchers.js';
|
||||
import { TextInput } from '../shared/TextInput.js';
|
||||
import { useTextBuffer } from '../shared/text-buffer.js';
|
||||
import { useKeyMatchers } from '../../hooks/useKeyMatchers.js';
|
||||
import { BrailleAnimation } from '../BrailleAnimation.js';
|
||||
|
||||
interface Issue {
|
||||
number: number;
|
||||
@@ -448,7 +448,7 @@ Return a JSON object with:
|
||||
if (state.status === 'loading') {
|
||||
return (
|
||||
<Box>
|
||||
<Spinner type="dots" />
|
||||
<BrailleAnimation />
|
||||
<Text> {state.message}</Text>
|
||||
</Box>
|
||||
);
|
||||
@@ -521,7 +521,7 @@ Return a JSON object with:
|
||||
if (state.status === 'analyzing') {
|
||||
return (
|
||||
<Box>
|
||||
<Spinner type="dots" />
|
||||
<BrailleAnimation />
|
||||
<Text> {state.message}</Text>
|
||||
</Box>
|
||||
);
|
||||
@@ -610,7 +610,7 @@ Return a JSON object with:
|
||||
>
|
||||
{state.status === 'analyzing' ? (
|
||||
<Box>
|
||||
<Spinner type="dots" />
|
||||
<BrailleAnimation />
|
||||
<Text> Analyzing issue with Gemini...</Text>
|
||||
</Box>
|
||||
) : analysis ? (
|
||||
|
||||
Reference in New Issue
Block a user