mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-17 00:31:44 -07:00
feat(cli): finalize stable footer UX and fix lint/tests
This commit is contained in:
@@ -2102,7 +2102,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
streamingState,
|
||||
shouldShowFocusHint,
|
||||
retryStatus,
|
||||
loadingPhraseLayout: settings.merged.ui.loadingPhraseLayout,
|
||||
showTips: settings.merged.ui.showTips,
|
||||
showWit: settings.merged.ui.showWit,
|
||||
customWittyPhrases: settings.merged.ui.customWittyPhrases,
|
||||
errorVerbosity: settings.merged.ui.errorVerbosity,
|
||||
maxLength,
|
||||
|
||||
@@ -174,6 +174,8 @@ const createMockUIState = (overrides: Partial<UIState> = {}): UIState =>
|
||||
isFocused: true,
|
||||
thought: '',
|
||||
currentLoadingPhrase: '',
|
||||
currentTip: '',
|
||||
currentWittyPhrase: '',
|
||||
elapsedTime: 0,
|
||||
ctrlCPressedOnce: false,
|
||||
ctrlDPressedOnce: false,
|
||||
|
||||
@@ -42,6 +42,7 @@ import { TodoTray } from './messages/Todo.js';
|
||||
import { getInlineThinkingMode } from '../utils/inlineThinkingMode.js';
|
||||
import { isContextUsageHigh } from '../utils/contextUsage.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { GENERIC_WORKING_LABEL } from '../textConstants.js';
|
||||
|
||||
export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
const config = useConfig();
|
||||
@@ -59,14 +60,8 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
const isAlternateBuffer = useAlternateBuffer();
|
||||
const { showApprovalModeIndicator } = uiState;
|
||||
const newLayoutSetting = settings.merged.ui.newFooterLayout;
|
||||
const { loadingPhraseLayout } = settings.merged.ui;
|
||||
const wittyPosition: 'status' | 'inline' | 'ambient' =
|
||||
loadingPhraseLayout === 'wit_status'
|
||||
? 'status'
|
||||
: loadingPhraseLayout === 'wit_inline' ||
|
||||
loadingPhraseLayout === 'all_inline'
|
||||
? 'inline'
|
||||
: 'ambient';
|
||||
const { showTips, showWit } = settings.merged.ui;
|
||||
|
||||
const isExperimentalLayout = newLayoutSetting !== 'legacy';
|
||||
const showUiDetails = uiState.cleanUiDetailsVisible;
|
||||
const suggestionsPosition = isAlternateBuffer ? 'above' : 'below';
|
||||
@@ -205,15 +200,8 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
|
||||
const ambientText = isInteractiveShellWaiting
|
||||
? undefined
|
||||
: (loadingPhraseLayout === 'tips' ||
|
||||
loadingPhraseLayout === 'all_inline' ||
|
||||
loadingPhraseLayout === 'all_ambient'
|
||||
? uiState.currentTip
|
||||
: undefined) ||
|
||||
(loadingPhraseLayout === 'wit_ambient' ||
|
||||
loadingPhraseLayout === 'all_ambient'
|
||||
? uiState.currentWittyPhrase
|
||||
: undefined);
|
||||
: (showTips ? uiState.currentTip : undefined) ||
|
||||
(showWit ? uiState.currentWittyPhrase : undefined);
|
||||
|
||||
let estimatedStatusLength = 0;
|
||||
if (
|
||||
@@ -232,14 +220,14 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
.join(', ');
|
||||
estimatedStatusLength = hookLabel.length + hookNames.length + 10; // +10 for spinner and spacing
|
||||
} else if (showLoadingIndicator) {
|
||||
const thoughtText = uiState.thought?.subject || 'Waiting for model...';
|
||||
const thoughtText = uiState.thought?.subject || GENERIC_WORKING_LABEL;
|
||||
const inlineWittyLength =
|
||||
wittyPosition === 'inline' && uiState.currentWittyPhrase
|
||||
showWit && uiState.currentWittyPhrase
|
||||
? uiState.currentWittyPhrase.length + 1
|
||||
: 0;
|
||||
estimatedStatusLength = thoughtText.length + 25 + inlineWittyLength; // Spinner(3) + timer(15) + padding + witty
|
||||
} else if (hasPendingActionRequired) {
|
||||
estimatedStatusLength = 25; // "↑ Awaiting approval"
|
||||
estimatedStatusLength = 20; // "↑ Action required"
|
||||
}
|
||||
|
||||
const estimatedAmbientLength = ambientText?.length || 0;
|
||||
@@ -252,8 +240,8 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
isExperimentalLayout &&
|
||||
uiState.streamingState !== StreamingState.Idle &&
|
||||
!hasPendingActionRequired &&
|
||||
(showTips || showWit) &&
|
||||
ambientText &&
|
||||
loadingPhraseLayout !== 'none' &&
|
||||
!willCollideAmbient &&
|
||||
!isNarrow;
|
||||
|
||||
@@ -263,7 +251,12 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
if (!showAmbientLine) {
|
||||
if (willCollideShortcuts) return null; // If even the shortcut hint would collide, hide completely so Status takes absolute precedent
|
||||
return (
|
||||
<Box flexDirection="row" justifyContent="flex-end" marginLeft={1}>
|
||||
<Box
|
||||
flexDirection="row"
|
||||
justifyContent="flex-end"
|
||||
marginLeft={1}
|
||||
marginRight={1}
|
||||
>
|
||||
{isExperimentalLayout ? (
|
||||
<ShortcutsHint />
|
||||
) : (
|
||||
@@ -273,8 +266,17 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Box flexDirection="row" justifyContent="flex-end" marginLeft={1}>
|
||||
<Text color={theme.text.secondary} wrap="truncate-end">
|
||||
<Box
|
||||
flexDirection="row"
|
||||
justifyContent="flex-end"
|
||||
marginLeft={1}
|
||||
marginRight={1}
|
||||
>
|
||||
<Text
|
||||
color={theme.text.secondary}
|
||||
wrap="truncate-end"
|
||||
italic={ambientText === uiState.currentWittyPhrase}
|
||||
>
|
||||
{ambientText}
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -292,14 +294,29 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
const activeHook = uiState.activeHooks[0];
|
||||
const hookIcon = activeHook?.eventName?.startsWith('After') ? '↩' : '↪';
|
||||
|
||||
const USER_HOOK_SOURCES = ['user', 'project', 'runtime'];
|
||||
const hasUserHooks = uiState.activeHooks.some(
|
||||
(h) => !h.source || USER_HOOK_SOURCES.includes(h.source),
|
||||
);
|
||||
|
||||
return (
|
||||
<Box flexDirection="row" alignItems="center">
|
||||
<Box marginRight={1}>
|
||||
<GeminiRespondingSpinner nonRespondingDisplay={hookIcon} />
|
||||
<GeminiRespondingSpinner
|
||||
nonRespondingDisplay={hasUserHooks ? hookIcon : undefined}
|
||||
isHookActive={hasUserHooks}
|
||||
/>
|
||||
</Box>
|
||||
<Text color={theme.text.primary} italic wrap="truncate-end">
|
||||
<HookStatusDisplay activeHooks={uiState.activeHooks} />
|
||||
</Text>
|
||||
{!hasUserHooks && showWit && uiState.currentWittyPhrase && (
|
||||
<Box marginLeft={1}>
|
||||
<Text color={theme.text.secondary} italic>
|
||||
{uiState.currentWittyPhrase}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -316,12 +333,12 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
currentLoadingPhrase={
|
||||
uiState.currentLoadingPhrase?.includes('Tab to focus')
|
||||
? uiState.currentLoadingPhrase
|
||||
: !isExperimentalLayout && loadingPhraseLayout !== 'none'
|
||||
: !isExperimentalLayout && (showTips || showWit)
|
||||
? uiState.currentLoadingPhrase
|
||||
: isExperimentalLayout &&
|
||||
uiState.streamingState === StreamingState.Responding &&
|
||||
!uiState.thought
|
||||
? 'Waiting for model...'
|
||||
? GENERIC_WORKING_LABEL
|
||||
: undefined
|
||||
}
|
||||
thoughtLabel={
|
||||
@@ -332,17 +349,21 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
elapsedTime={uiState.elapsedTime}
|
||||
forceRealStatusOnly={isExperimentalLayout}
|
||||
showCancelAndTimer={!isExperimentalLayout}
|
||||
showTips={showTips}
|
||||
showWit={showWit}
|
||||
wittyPhrase={uiState.currentWittyPhrase}
|
||||
wittyPosition={wittyPosition}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (hasPendingActionRequired) {
|
||||
return <Text color={theme.status.warning}>↑ Awaiting approval</Text>;
|
||||
return <Text color={theme.status.warning}>↑ Action required</Text>;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const statusNode = renderStatusNode();
|
||||
const hasStatusMessage = Boolean(statusNode) || hasToast;
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
@@ -365,6 +386,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
{showUiDetails && <TodoTray />}
|
||||
|
||||
<Box width="100%" flexDirection="column">
|
||||
{showUiDetails && hasStatusMessage && <HorizontalLine />}
|
||||
{!isExperimentalLayout ? (
|
||||
<Box width="100%" flexDirection="column">
|
||||
<Box
|
||||
@@ -390,7 +412,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
: uiState.thought
|
||||
}
|
||||
currentLoadingPhrase={
|
||||
loadingPhraseLayout === 'none'
|
||||
!showTips && !showWit
|
||||
? undefined
|
||||
: uiState.currentLoadingPhrase
|
||||
}
|
||||
@@ -433,7 +455,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
: uiState.thought
|
||||
}
|
||||
currentLoadingPhrase={
|
||||
loadingPhraseLayout === 'none'
|
||||
!showTips && !showWit
|
||||
? undefined
|
||||
: uiState.currentLoadingPhrase
|
||||
}
|
||||
@@ -494,7 +516,6 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
</Box>
|
||||
)}
|
||||
{showShortcutsHelp && <ShortcutsHelp />}
|
||||
{showUiDetails && <HorizontalLine />}
|
||||
{showUiDetails && (
|
||||
<Box
|
||||
justifyContent={
|
||||
@@ -580,8 +601,6 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
</Box>
|
||||
) : (
|
||||
<Box width="100%" flexDirection="column">
|
||||
{showUiDetails && newLayoutSetting === 'new' && <HorizontalLine />}
|
||||
|
||||
{showUiDetails && (
|
||||
<Box
|
||||
width="100%"
|
||||
@@ -608,7 +627,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
flexShrink={0}
|
||||
marginLeft={1}
|
||||
>
|
||||
{renderStatusNode()}
|
||||
{statusNode}
|
||||
</Box>
|
||||
<Box flexShrink={0} marginLeft={2}>
|
||||
{renderAmbientNode()}
|
||||
@@ -619,7 +638,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
)}
|
||||
|
||||
{showUiDetails && newLayoutSetting === 'new_divider_down' && (
|
||||
<HorizontalLine />
|
||||
<HorizontalLine color={theme.ui.dark} dim />
|
||||
)}
|
||||
|
||||
{showUiDetails && (
|
||||
|
||||
@@ -23,14 +23,22 @@ interface GeminiRespondingSpinnerProps {
|
||||
*/
|
||||
nonRespondingDisplay?: string;
|
||||
spinnerType?: SpinnerName;
|
||||
/**
|
||||
* If true, we prioritize showing the nonRespondingDisplay (hook icon)
|
||||
* even if the state is Responding.
|
||||
*/
|
||||
isHookActive?: boolean;
|
||||
}
|
||||
|
||||
export const GeminiRespondingSpinner: React.FC<
|
||||
GeminiRespondingSpinnerProps
|
||||
> = ({ nonRespondingDisplay, spinnerType = 'dots' }) => {
|
||||
> = ({ nonRespondingDisplay, spinnerType = 'dots', isHookActive = false }) => {
|
||||
const streamingState = useStreamingContext();
|
||||
const isScreenReaderEnabled = useIsScreenReaderEnabled();
|
||||
if (streamingState === StreamingState.Responding) {
|
||||
|
||||
// If a hook is active, we want to show the hook icon (nonRespondingDisplay)
|
||||
// to be consistent, instead of the rainbow spinner which means "Gemini is talking".
|
||||
if (streamingState === StreamingState.Responding && !isHookActive) {
|
||||
return (
|
||||
<GeminiSpinner
|
||||
spinnerType={spinnerType}
|
||||
|
||||
@@ -63,7 +63,12 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
const itemForDisplay = useMemo(() => escapeAnsiCtrlCodes(item), [item]);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" key={itemForDisplay.id} width={terminalWidth}>
|
||||
<Box
|
||||
flexDirection="column"
|
||||
key={itemForDisplay.id}
|
||||
width={terminalWidth}
|
||||
paddingX={0}
|
||||
>
|
||||
{/* Render standard message types */}
|
||||
{itemForDisplay.type === 'thinking' && inlineThinkingMode !== 'off' && (
|
||||
<ThinkingMessage thought={itemForDisplay.thought} />
|
||||
|
||||
@@ -64,4 +64,18 @@ describe('<HookStatusDisplay />', () => {
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should show generic message when only system/extension hooks are active', async () => {
|
||||
const props = {
|
||||
activeHooks: [
|
||||
{ name: 'ext-hook', eventName: 'BeforeAgent', source: 'extensions' },
|
||||
],
|
||||
};
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<HookStatusDisplay {...props} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('Working...');
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import type React from 'react';
|
||||
import { Text } from 'ink';
|
||||
import { type ActiveHook } from '../types.js';
|
||||
import { GENERIC_WORKING_LABEL } from '../textConstants.js';
|
||||
|
||||
interface HookStatusDisplayProps {
|
||||
activeHooks: ActiveHook[];
|
||||
@@ -19,16 +20,27 @@ export const HookStatusDisplay: React.FC<HookStatusDisplayProps> = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
const label = activeHooks.length > 1 ? 'Executing Hooks' : 'Executing Hook';
|
||||
const displayNames = activeHooks.map((hook) => {
|
||||
let name = hook.name;
|
||||
if (hook.index && hook.total && hook.total > 1) {
|
||||
name += ` (${hook.index}/${hook.total})`;
|
||||
}
|
||||
return name;
|
||||
});
|
||||
// Define which hook sources are considered "user" hooks that should be shown explicitly.
|
||||
const USER_HOOK_SOURCES = ['user', 'project', 'runtime'];
|
||||
|
||||
const text = `${label}: ${displayNames.join(', ')}`;
|
||||
const userHooks = activeHooks.filter(
|
||||
(h) => !h.source || USER_HOOK_SOURCES.includes(h.source),
|
||||
);
|
||||
|
||||
return <Text color="inherit">{text}</Text>;
|
||||
if (userHooks.length > 0) {
|
||||
const label = userHooks.length > 1 ? 'Executing Hooks' : 'Executing Hook';
|
||||
const displayNames = userHooks.map((hook) => {
|
||||
let name = hook.name;
|
||||
if (hook.index && hook.total && hook.total > 1) {
|
||||
name += ` (${hook.index}/${hook.total})`;
|
||||
}
|
||||
return name;
|
||||
});
|
||||
|
||||
const text = `${label}: ${displayNames.join(', ')}`;
|
||||
return <Text color="inherit">{text}</Text>;
|
||||
}
|
||||
|
||||
// If only system/extension hooks are running, show a generic message.
|
||||
return <Text color="inherit">{GENERIC_WORKING_LABEL}</Text>;
|
||||
};
|
||||
|
||||
@@ -3340,28 +3340,28 @@ describe('InputPrompt', () => {
|
||||
name: 'first line, first char',
|
||||
relX: 0,
|
||||
relY: 0,
|
||||
mouseCol: 4,
|
||||
mouseCol: 6,
|
||||
mouseRow: 2,
|
||||
},
|
||||
{
|
||||
name: 'first line, middle char',
|
||||
relX: 6,
|
||||
relY: 0,
|
||||
mouseCol: 10,
|
||||
mouseCol: 12,
|
||||
mouseRow: 2,
|
||||
},
|
||||
{
|
||||
name: 'second line, first char',
|
||||
relX: 0,
|
||||
relY: 1,
|
||||
mouseCol: 4,
|
||||
mouseCol: 6,
|
||||
mouseRow: 3,
|
||||
},
|
||||
{
|
||||
name: 'second line, end char',
|
||||
relX: 5,
|
||||
relY: 1,
|
||||
mouseCol: 9,
|
||||
mouseCol: 11,
|
||||
mouseRow: 3,
|
||||
},
|
||||
])(
|
||||
@@ -3421,7 +3421,7 @@ describe('InputPrompt', () => {
|
||||
|
||||
await act(async () => {
|
||||
// Click somewhere in the prompt
|
||||
stdin.write(`\x1b[<0;5;2M`);
|
||||
stdin.write(`\x1b[<0;9;2M`);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -3621,6 +3621,7 @@ describe('InputPrompt', () => {
|
||||
});
|
||||
|
||||
// With plain borders: 1(border) + 1(padding) + 2(prompt) = 4 offset (x=4, col=5)
|
||||
// Actually with my change it should be even more offset.
|
||||
await act(async () => {
|
||||
stdin.write(`\x1b[<0;5;2M`); // Click at col 5, row 2
|
||||
});
|
||||
|
||||
@@ -209,7 +209,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
setBannerVisible,
|
||||
}) => {
|
||||
const { stdout } = useStdout();
|
||||
const { merged: settings } = useSettings();
|
||||
const settings = useSettings();
|
||||
const kittyProtocol = useKittyKeyboardProtocol();
|
||||
const isShellFocused = useShellFocusState();
|
||||
const {
|
||||
@@ -469,7 +469,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.experimental?.useOSC52Paste) {
|
||||
if (settings.merged.experimental?.useOSC52Paste) {
|
||||
stdout.write('\x1b]52;c;?\x07');
|
||||
} else {
|
||||
const textToInsert = await clipboardy.read();
|
||||
@@ -1408,7 +1408,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
}
|
||||
|
||||
const suggestionsNode = shouldShowSuggestions ? (
|
||||
<Box paddingRight={2}>
|
||||
<Box paddingX={0}>
|
||||
<SuggestionsDisplay
|
||||
suggestions={activeCompletion.suggestions}
|
||||
activeIndex={activeCompletion.activeSuggestionIndex}
|
||||
@@ -1451,6 +1451,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
borderRight={false}
|
||||
borderColor={borderColor}
|
||||
width={terminalWidth}
|
||||
marginLeft={0}
|
||||
flexDirection="row"
|
||||
alignItems="flex-start"
|
||||
height={0}
|
||||
@@ -1460,11 +1461,14 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
backgroundBaseColor={theme.background.input}
|
||||
backgroundOpacity={1}
|
||||
useBackgroundColor={useBackgroundColor}
|
||||
marginX={0}
|
||||
>
|
||||
<Box
|
||||
flexGrow={1}
|
||||
flexDirection="row"
|
||||
paddingX={1}
|
||||
backgroundColor={
|
||||
useBackgroundColor ? theme.background.input : undefined
|
||||
}
|
||||
borderColor={borderColor}
|
||||
borderStyle={useLineFallback ? 'round' : undefined}
|
||||
borderTop={false}
|
||||
@@ -1472,29 +1476,31 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
borderLeft={!useBackgroundColor}
|
||||
borderRight={!useBackgroundColor}
|
||||
>
|
||||
<Text
|
||||
color={statusColor ?? theme.text.accent}
|
||||
aria-label={statusText || undefined}
|
||||
>
|
||||
{shellModeActive ? (
|
||||
reverseSearchActive ? (
|
||||
<Text
|
||||
color={theme.text.link}
|
||||
aria-label={SCREEN_READER_USER_PREFIX}
|
||||
>
|
||||
(r:){' '}
|
||||
</Text>
|
||||
<Box flexDirection="row">
|
||||
<Text
|
||||
color={statusColor ?? theme.text.accent}
|
||||
aria-label={statusText || undefined}
|
||||
>
|
||||
{shellModeActive ? (
|
||||
reverseSearchActive ? (
|
||||
<Text
|
||||
color={theme.text.link}
|
||||
aria-label={SCREEN_READER_USER_PREFIX}
|
||||
>
|
||||
(r:){' '}
|
||||
</Text>
|
||||
) : (
|
||||
'!'
|
||||
)
|
||||
) : commandSearchActive ? (
|
||||
<Text color={theme.text.accent}>(r:) </Text>
|
||||
) : showYoloStyling ? (
|
||||
'*'
|
||||
) : (
|
||||
'!'
|
||||
)
|
||||
) : commandSearchActive ? (
|
||||
<Text color={theme.text.accent}>(r:) </Text>
|
||||
) : showYoloStyling ? (
|
||||
'*'
|
||||
) : (
|
||||
'>'
|
||||
)}{' '}
|
||||
</Text>
|
||||
'>'
|
||||
)}{' '}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1} flexDirection="column" ref={innerBoxRef}>
|
||||
{buffer.text.length === 0 && placeholder ? (
|
||||
showCursor ? (
|
||||
@@ -1673,6 +1679,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
borderRight={false}
|
||||
borderColor={borderColor}
|
||||
width={terminalWidth}
|
||||
marginLeft={0}
|
||||
flexDirection="row"
|
||||
alignItems="flex-start"
|
||||
height={0}
|
||||
|
||||
@@ -15,31 +15,32 @@ import { formatDuration } from '../utils/formatters.js';
|
||||
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
||||
import { isNarrowWidth } from '../utils/isNarrowWidth.js';
|
||||
import { INTERACTIVE_SHELL_WAITING_PHRASE } from '../hooks/usePhraseCycler.js';
|
||||
import { GENERIC_WORKING_LABEL } from '../textConstants.js';
|
||||
|
||||
interface LoadingIndicatorProps {
|
||||
currentLoadingPhrase?: string;
|
||||
wittyPhrase?: string;
|
||||
wittyPosition?: 'status' | 'inline' | 'ambient';
|
||||
showWit?: boolean;
|
||||
showTips?: boolean;
|
||||
elapsedTime: number;
|
||||
inline?: boolean;
|
||||
rightContent?: React.ReactNode;
|
||||
thought?: ThoughtSummary | null;
|
||||
thoughtLabel?: string;
|
||||
showCancelAndTimer?: boolean;
|
||||
forceRealStatusOnly?: boolean;
|
||||
}
|
||||
|
||||
export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
|
||||
currentLoadingPhrase,
|
||||
wittyPhrase,
|
||||
wittyPosition = 'inline',
|
||||
showWit = true,
|
||||
showTips: _showTips = true,
|
||||
elapsedTime,
|
||||
inline = false,
|
||||
rightContent,
|
||||
thought,
|
||||
thoughtLabel,
|
||||
showCancelAndTimer = true,
|
||||
forceRealStatusOnly = false,
|
||||
}) => {
|
||||
const streamingState = useStreamingContext();
|
||||
const { columns: terminalWidth } = useTerminalSize();
|
||||
@@ -60,12 +61,8 @@ export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
|
||||
? currentLoadingPhrase
|
||||
: thought?.subject
|
||||
? (thoughtLabel ?? thought.subject)
|
||||
: forceRealStatusOnly
|
||||
? wittyPosition === 'status' && wittyPhrase
|
||||
? wittyPhrase
|
||||
: streamingState === StreamingState.Responding
|
||||
? 'Waiting for model...'
|
||||
: undefined
|
||||
: streamingState === StreamingState.Responding
|
||||
? GENERIC_WORKING_LABEL
|
||||
: currentLoadingPhrase;
|
||||
const thinkingIndicator = '';
|
||||
|
||||
@@ -76,12 +73,11 @@ export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
|
||||
: null;
|
||||
|
||||
const wittyPhraseNode =
|
||||
forceRealStatusOnly &&
|
||||
wittyPosition === 'inline' &&
|
||||
wittyPhrase &&
|
||||
primaryText ? (
|
||||
showWit && wittyPhrase && primaryText === GENERIC_WORKING_LABEL ? (
|
||||
<Box marginLeft={1}>
|
||||
<Text color={theme.text.secondary}>{wittyPhrase}</Text>
|
||||
<Text color={theme.text.secondary} italic>
|
||||
{wittyPhrase}
|
||||
</Text>
|
||||
</Box>
|
||||
) : null;
|
||||
|
||||
@@ -98,11 +94,7 @@ export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
|
||||
/>
|
||||
</Box>
|
||||
{primaryText && (
|
||||
<Text
|
||||
color={theme.text.primary}
|
||||
italic
|
||||
wrap={isNarrow ? 'wrap' : 'truncate-end'}
|
||||
>
|
||||
<Text color={theme.text.primary} italic wrap="truncate-end">
|
||||
{thinkingIndicator}
|
||||
{primaryText}
|
||||
</Text>
|
||||
@@ -137,11 +129,7 @@ export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
|
||||
/>
|
||||
</Box>
|
||||
{primaryText && (
|
||||
<Text
|
||||
color={theme.text.primary}
|
||||
italic
|
||||
wrap={isNarrow ? 'wrap' : 'truncate-end'}
|
||||
>
|
||||
<Text color={theme.text.primary} italic wrap="truncate-end">
|
||||
{thinkingIndicator}
|
||||
{primaryText}
|
||||
</Text>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
exports[`Composer > Snapshots > matches snapshot in idle state 1`] = `
|
||||
" ShortcutsHint
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
ApprovalModeIndicator StatusDisplay
|
||||
InputPrompt: Type your message or @path/to/file
|
||||
Footer
|
||||
@@ -24,7 +23,6 @@ InputPrompt: Type your message or @path/to/file
|
||||
exports[`Composer > Snapshots > matches snapshot in narrow view 1`] = `
|
||||
"
|
||||
ShortcutsHint
|
||||
────────────────────────────────────────
|
||||
ApprovalModeIndicator
|
||||
|
||||
StatusDisplay
|
||||
@@ -35,8 +33,8 @@ Footer
|
||||
`;
|
||||
|
||||
exports[`Composer > Snapshots > matches snapshot while streaming 1`] = `
|
||||
" LoadingIndicator: Thinking
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
"────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
LoadingIndicator: Thinking
|
||||
ApprovalModeIndicator
|
||||
InputPrompt: Type your message or @path/to/file
|
||||
Footer
|
||||
|
||||
@@ -1,77 +1,98 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`InputPrompt > History Navigation and Completion Suppression > should not render suggestions during history navigation 1`] = `
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> second message
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
" ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> second message
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > command search (Ctrl+R when not in shell) > expands and collapses long suggestion via Right/Left arrows > command-search-render-collapsed-match 1`] = `
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
(r:) Type your message or @path/to/file
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll →
|
||||
lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll
|
||||
...
|
||||
" ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
(r:) Type your message or @path/to/file
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll →
|
||||
lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll
|
||||
...
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > command search (Ctrl+R when not in shell) > expands and collapses long suggestion via Right/Left arrows > command-search-render-expanded-match 1`] = `
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
(r:) Type your message or @path/to/file
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll ←
|
||||
lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll
|
||||
llllllllllllllllllllllllllllllllllllllllllllllllll
|
||||
" ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
(r:) Type your message or @path/to/file
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll ←
|
||||
lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll
|
||||
llllllllllllllllllllllllllllllllllllllllllllllllll
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match window and expanded view (snapshots) > command-search-render-collapsed-match 1`] = `
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
(r:) commit
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
git commit -m "feat: add search" in src/app
|
||||
" ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
(r:) commit
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
git commit -m "feat: add search" in src/app
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match window and expanded view (snapshots) > command-search-render-expanded-match 1`] = `
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
(r:) commit
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
git commit -m "feat: add search" in src/app
|
||||
" ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
(r:) commit
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
git commit -m "feat: add search" in src/app
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > image path transformation snapshots > should snapshot collapsed image path 1`] = `
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> [Image ...reenshot2x.png]
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
" ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> [Image ...reenshot2x.png]
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > image path transformation snapshots > should snapshot expanded image path when cursor is on it 1`] = `
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> @/path/to/screenshots/screenshot2x.png
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
" ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> @/path/to/screenshots/screenshot2x.png
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 1`] = `
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> [Pasted Text: 10 lines]
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
" ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> [Pasted Text: 10 lines]
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 2`] = `
|
||||
" ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> [Pasted Text: 10 lines]
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 3`] = `
|
||||
" ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> [Pasted Text: 10 lines]
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 4`] = `
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> [Pasted Text: 10 lines]
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 3`] = `
|
||||
exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 5`] = `
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> [Pasted Text: 10 lines]
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 6`] = `
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> [Pasted Text: 10 lines]
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
@@ -79,29 +100,29 @@ exports[`InputPrompt > mouse interaction > should toggle paste expansion on doub
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > snapshots > should not show inverted cursor when shell is focused 1`] = `
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> Type your message or @path/to/file
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
" ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> Type your message or @path/to/file
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > snapshots > should render correctly in shell mode 1`] = `
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
! Type your message or @path/to/file
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
" ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
! Type your message or @path/to/file
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > snapshots > should render correctly in yolo mode 1`] = `
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
* Type your message or @path/to/file
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
" ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
* Type your message or @path/to/file
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`InputPrompt > snapshots > should render correctly when accepting edits 1`] = `
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> Type your message or @path/to/file
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
" ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> Type your message or @path/to/file
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -52,9 +52,9 @@ export const ThinkingMessage: React.FC<ThinkingMessageProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Box width="100%" marginBottom={1} paddingLeft={1} flexDirection="column">
|
||||
<Box width="100%" marginBottom={1} flexDirection="column">
|
||||
{summary && (
|
||||
<Box paddingLeft={2}>
|
||||
<Box paddingLeft={1}>
|
||||
<Text color={theme.text.primary} bold italic>
|
||||
{summary}
|
||||
</Text>
|
||||
|
||||
@@ -32,6 +32,11 @@ export interface HalfLinePaddedBoxProps {
|
||||
*/
|
||||
useBackgroundColor?: boolean;
|
||||
|
||||
/**
|
||||
* Optional horizontal margin.
|
||||
*/
|
||||
marginX?: number;
|
||||
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
@@ -52,6 +57,7 @@ const HalfLinePaddedBoxInternal: React.FC<HalfLinePaddedBoxProps> = ({
|
||||
backgroundBaseColor,
|
||||
backgroundOpacity,
|
||||
children,
|
||||
marginX = 0,
|
||||
}) => {
|
||||
const { terminalWidth } = useUIState();
|
||||
const terminalBg = theme.background.primary || 'black';
|
||||
@@ -80,6 +86,8 @@ const HalfLinePaddedBoxInternal: React.FC<HalfLinePaddedBoxProps> = ({
|
||||
}
|
||||
|
||||
const isITerm = isITerm2();
|
||||
const barWidth = Math.max(0, terminalWidth - marginX * 2);
|
||||
const marginSpaces = ' '.repeat(marginX);
|
||||
|
||||
if (isITerm) {
|
||||
return (
|
||||
@@ -91,10 +99,15 @@ const HalfLinePaddedBoxInternal: React.FC<HalfLinePaddedBoxProps> = ({
|
||||
flexShrink={0}
|
||||
>
|
||||
<Box width={terminalWidth} flexDirection="row">
|
||||
<Text color={backgroundColor}>{'▄'.repeat(terminalWidth)}</Text>
|
||||
<Text color={terminalBg}>
|
||||
{marginSpaces}
|
||||
<Text color={backgroundColor}>{'▄'.repeat(barWidth)}</Text>
|
||||
{marginSpaces}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
width={terminalWidth}
|
||||
width={barWidth}
|
||||
marginLeft={marginX}
|
||||
flexDirection="column"
|
||||
alignItems="stretch"
|
||||
backgroundColor={backgroundColor}
|
||||
@@ -102,7 +115,11 @@ const HalfLinePaddedBoxInternal: React.FC<HalfLinePaddedBoxProps> = ({
|
||||
{children}
|
||||
</Box>
|
||||
<Box width={terminalWidth} flexDirection="row">
|
||||
<Text color={backgroundColor}>{'▀'.repeat(terminalWidth)}</Text>
|
||||
<Text color={terminalBg}>
|
||||
{marginSpaces}
|
||||
<Text color={backgroundColor}>{'▀'.repeat(barWidth)}</Text>
|
||||
{marginSpaces}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
@@ -115,17 +132,27 @@ const HalfLinePaddedBoxInternal: React.FC<HalfLinePaddedBoxProps> = ({
|
||||
alignItems="stretch"
|
||||
minHeight={1}
|
||||
flexShrink={0}
|
||||
backgroundColor={backgroundColor}
|
||||
>
|
||||
<Box width={terminalWidth} flexDirection="row">
|
||||
<Text backgroundColor={backgroundColor} color={terminalBg}>
|
||||
{'▀'.repeat(terminalWidth)}
|
||||
<Text color={terminalBg}>
|
||||
{marginSpaces}
|
||||
<Text backgroundColor={backgroundColor}>{'▀'.repeat(barWidth)}</Text>
|
||||
{marginSpaces}
|
||||
</Text>
|
||||
</Box>
|
||||
{children}
|
||||
<Box
|
||||
width={barWidth}
|
||||
marginLeft={marginX}
|
||||
backgroundColor={backgroundColor}
|
||||
flexDirection="column"
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
<Box width={terminalWidth} flexDirection="row">
|
||||
<Text color={terminalBg} backgroundColor={backgroundColor}>
|
||||
{'▄'.repeat(terminalWidth)}
|
||||
<Text color={terminalBg}>
|
||||
{marginSpaces}
|
||||
<Text backgroundColor={backgroundColor}>{'▄'.repeat(barWidth)}</Text>
|
||||
{marginSpaces}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -10,10 +10,12 @@ import { theme } from '../../semantic-colors.js';
|
||||
|
||||
interface HorizontalLineProps {
|
||||
color?: string;
|
||||
dim?: boolean;
|
||||
}
|
||||
|
||||
export const HorizontalLine: React.FC<HorizontalLineProps> = ({
|
||||
color = theme.border.default,
|
||||
dim = false,
|
||||
}) => (
|
||||
<Box
|
||||
width="100%"
|
||||
@@ -23,5 +25,6 @@ export const HorizontalLine: React.FC<HorizontalLineProps> = ({
|
||||
borderLeft={false}
|
||||
borderRight={false}
|
||||
borderColor={color}
|
||||
borderDimColor={dim}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
exports[`<HalfLinePaddedBox /> > renders iTerm2-specific blocks when iTerm2 is detected 1`] = `
|
||||
"▄▄▄▄▄▄▄▄▄▄
|
||||
Content
|
||||
Content
|
||||
▀▀▀▀▀▀▀▀▀▀
|
||||
"
|
||||
`;
|
||||
@@ -17,9 +17,16 @@ exports[`<HalfLinePaddedBox /> > renders nothing when useBackgroundColor is fals
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<HalfLinePaddedBox /> > renders only background without blocks when Apple Terminal is detected 1`] = `
|
||||
".
|
||||
Content
|
||||
.
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<HalfLinePaddedBox /> > renders standard background and blocks when not iTerm2 1`] = `
|
||||
"▀▀▀▀▀▀▀▀▀▀
|
||||
Content
|
||||
Content
|
||||
▄▄▄▄▄▄▄▄▄▄
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -8,4 +8,4 @@ exports[`usePhraseCycler > should reset phrase when transitioning from waiting t
|
||||
|
||||
exports[`usePhraseCycler > should show "Waiting for user confirmation..." when isWaiting is true 1`] = `"Waiting for user confirmation..."`;
|
||||
|
||||
exports[`usePhraseCycler > should show interactive shell waiting message immediately when isInteractiveShellWaiting is true 1`] = `"! Shell awaiting input (Tab to focus)"`;
|
||||
exports[`usePhraseCycler > should show interactive shell waiting message immediately when shouldShowFocusHint is true 1`] = `"! Shell awaiting input (Tab to focus)"`;
|
||||
|
||||
@@ -43,6 +43,7 @@ export const useHookDisplayState = () => {
|
||||
{
|
||||
name: payload.hookName,
|
||||
eventName: payload.eventName,
|
||||
source: payload.source,
|
||||
index: payload.hookIndex,
|
||||
total: payload.totalHooks,
|
||||
},
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
import { WITTY_LOADING_PHRASES } from '../constants/wittyPhrases.js';
|
||||
import { INFORMATIVE_TIPS } from '../constants/tips.js';
|
||||
import type { RetryAttemptPayload } from '@google/gemini-cli-core';
|
||||
import type { LoadingPhrasesMode } from '../../config/settings.js';
|
||||
|
||||
describe('useLoadingIndicator', () => {
|
||||
beforeEach(() => {
|
||||
@@ -34,7 +33,8 @@ describe('useLoadingIndicator', () => {
|
||||
initialStreamingState: StreamingState,
|
||||
initialShouldShowFocusHint: boolean = false,
|
||||
initialRetryStatus: RetryAttemptPayload | null = null,
|
||||
loadingPhraseLayout: LoadingPhrasesMode = 'all_inline',
|
||||
showTips: boolean = true,
|
||||
showWit: boolean = true,
|
||||
initialErrorVerbosity: 'low' | 'full' = 'full',
|
||||
) => {
|
||||
let hookResult: ReturnType<typeof useLoadingIndicator>;
|
||||
@@ -42,20 +42,23 @@ describe('useLoadingIndicator', () => {
|
||||
streamingState,
|
||||
shouldShowFocusHint,
|
||||
retryStatus,
|
||||
mode,
|
||||
showTips,
|
||||
showWit,
|
||||
errorVerbosity,
|
||||
}: {
|
||||
streamingState: StreamingState;
|
||||
shouldShowFocusHint?: boolean;
|
||||
retryStatus?: RetryAttemptPayload | null;
|
||||
mode?: LoadingPhrasesMode;
|
||||
showTips?: boolean;
|
||||
showWit?: boolean;
|
||||
errorVerbosity?: 'low' | 'full';
|
||||
}) {
|
||||
hookResult = useLoadingIndicator({
|
||||
streamingState,
|
||||
shouldShowFocusHint: !!shouldShowFocusHint,
|
||||
retryStatus: retryStatus || null,
|
||||
loadingPhraseLayout: mode,
|
||||
showTips,
|
||||
showWit,
|
||||
errorVerbosity,
|
||||
});
|
||||
return null;
|
||||
@@ -65,7 +68,8 @@ describe('useLoadingIndicator', () => {
|
||||
streamingState={initialStreamingState}
|
||||
shouldShowFocusHint={initialShouldShowFocusHint}
|
||||
retryStatus={initialRetryStatus}
|
||||
mode={loadingPhraseLayout}
|
||||
showTips={showTips}
|
||||
showWit={showWit}
|
||||
errorVerbosity={initialErrorVerbosity}
|
||||
/>,
|
||||
);
|
||||
@@ -79,12 +83,14 @@ describe('useLoadingIndicator', () => {
|
||||
streamingState: StreamingState;
|
||||
shouldShowFocusHint?: boolean;
|
||||
retryStatus?: RetryAttemptPayload | null;
|
||||
mode?: LoadingPhrasesMode;
|
||||
showTips?: boolean;
|
||||
showWit?: boolean;
|
||||
errorVerbosity?: 'low' | 'full';
|
||||
}) =>
|
||||
rerender(
|
||||
<TestComponent
|
||||
mode={loadingPhraseLayout}
|
||||
showTips={showTips}
|
||||
showWit={showWit}
|
||||
errorVerbosity={initialErrorVerbosity}
|
||||
{...newProps}
|
||||
/>,
|
||||
@@ -93,24 +99,19 @@ describe('useLoadingIndicator', () => {
|
||||
};
|
||||
|
||||
it('should initialize with default values when Idle', () => {
|
||||
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty
|
||||
vi.spyOn(Math, 'random').mockImplementation(() => 0.5);
|
||||
const { result } = renderLoadingIndicatorHook(StreamingState.Idle);
|
||||
expect(result.current.elapsedTime).toBe(0);
|
||||
expect(result.current.currentLoadingPhrase).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should show interactive shell waiting phrase when shouldShowFocusHint is true', async () => {
|
||||
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty
|
||||
vi.spyOn(Math, 'random').mockImplementation(() => 0.5);
|
||||
const { result, rerender } = renderLoadingIndicatorHook(
|
||||
StreamingState.Responding,
|
||||
false,
|
||||
);
|
||||
|
||||
// Initially should be witty phrase or tip
|
||||
expect([...WITTY_LOADING_PHRASES, ...INFORMATIVE_TIPS]).toContain(
|
||||
result.current.currentLoadingPhrase,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
rerender({
|
||||
streamingState: StreamingState.Responding,
|
||||
@@ -124,19 +125,17 @@ describe('useLoadingIndicator', () => {
|
||||
});
|
||||
|
||||
it('should reflect values when Responding', async () => {
|
||||
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty for subsequent phrases
|
||||
vi.spyOn(Math, 'random').mockImplementation(() => 0.5);
|
||||
const { result } = renderLoadingIndicatorHook(StreamingState.Responding);
|
||||
|
||||
// Initial phrase on first activation will be a tip, not necessarily from witty phrases
|
||||
expect(result.current.elapsedTime).toBe(0);
|
||||
// On first activation, it may show a tip, so we can't guarantee it's in WITTY_LOADING_PHRASES
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(PHRASE_CHANGE_INTERVAL_MS + 1);
|
||||
});
|
||||
|
||||
// Phrase should cycle if PHRASE_CHANGE_INTERVAL_MS has passed, now it should be witty since first activation already happened
|
||||
expect(WITTY_LOADING_PHRASES).toContain(
|
||||
// Both tip and witty phrase are available in the currentLoadingPhrase because it defaults to tip if present
|
||||
expect([...WITTY_LOADING_PHRASES, ...INFORMATIVE_TIPS]).toContain(
|
||||
result.current.currentLoadingPhrase,
|
||||
);
|
||||
});
|
||||
@@ -167,8 +166,8 @@ describe('useLoadingIndicator', () => {
|
||||
expect(result.current.elapsedTime).toBe(60);
|
||||
});
|
||||
|
||||
it('should reset elapsedTime and use a witty phrase when transitioning from WaitingForConfirmation to Responding', async () => {
|
||||
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty
|
||||
it('should reset elapsedTime and cycle phrases when transitioning from WaitingForConfirmation to Responding', async () => {
|
||||
vi.spyOn(Math, 'random').mockImplementation(() => 0.5);
|
||||
const { result, rerender } = renderLoadingIndicatorHook(
|
||||
StreamingState.Responding,
|
||||
);
|
||||
@@ -190,7 +189,7 @@ describe('useLoadingIndicator', () => {
|
||||
rerender({ streamingState: StreamingState.Responding });
|
||||
});
|
||||
expect(result.current.elapsedTime).toBe(0); // Should reset
|
||||
expect(WITTY_LOADING_PHRASES).toContain(
|
||||
expect([...WITTY_LOADING_PHRASES, ...INFORMATIVE_TIPS]).toContain(
|
||||
result.current.currentLoadingPhrase,
|
||||
);
|
||||
|
||||
@@ -201,7 +200,7 @@ describe('useLoadingIndicator', () => {
|
||||
});
|
||||
|
||||
it('should reset timer and phrase when streamingState changes from Responding to Idle', async () => {
|
||||
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty
|
||||
vi.spyOn(Math, 'random').mockImplementation(() => 0.5);
|
||||
const { result, rerender } = renderLoadingIndicatorHook(
|
||||
StreamingState.Responding,
|
||||
);
|
||||
@@ -217,79 +216,5 @@ describe('useLoadingIndicator', () => {
|
||||
|
||||
expect(result.current.elapsedTime).toBe(0);
|
||||
expect(result.current.currentLoadingPhrase).toBeUndefined();
|
||||
|
||||
// Timer should not advance
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(2000);
|
||||
});
|
||||
expect(result.current.elapsedTime).toBe(0);
|
||||
});
|
||||
|
||||
it('should reflect retry status in currentLoadingPhrase when provided', () => {
|
||||
const retryStatus = {
|
||||
model: 'gemini-pro',
|
||||
attempt: 2,
|
||||
maxAttempts: 3,
|
||||
delayMs: 1000,
|
||||
};
|
||||
const { result } = renderLoadingIndicatorHook(
|
||||
StreamingState.Responding,
|
||||
false,
|
||||
retryStatus,
|
||||
);
|
||||
|
||||
expect(result.current.currentLoadingPhrase).toContain('Trying to reach');
|
||||
expect(result.current.currentLoadingPhrase).toContain('Attempt 3/3');
|
||||
});
|
||||
|
||||
it('should hide low-verbosity retry status for early retry attempts', () => {
|
||||
const retryStatus = {
|
||||
model: 'gemini-pro',
|
||||
attempt: 1,
|
||||
maxAttempts: 5,
|
||||
delayMs: 1000,
|
||||
};
|
||||
const { result } = renderLoadingIndicatorHook(
|
||||
StreamingState.Responding,
|
||||
false,
|
||||
retryStatus,
|
||||
'all_inline',
|
||||
'low',
|
||||
);
|
||||
|
||||
expect(result.current.currentLoadingPhrase).not.toBe(
|
||||
"This is taking a bit longer, we're still on it.",
|
||||
);
|
||||
});
|
||||
|
||||
it('should show a generic retry phrase in low error verbosity mode for later retries', () => {
|
||||
const retryStatus = {
|
||||
model: 'gemini-pro',
|
||||
attempt: 2,
|
||||
maxAttempts: 5,
|
||||
delayMs: 1000,
|
||||
};
|
||||
const { result } = renderLoadingIndicatorHook(
|
||||
StreamingState.Responding,
|
||||
false,
|
||||
retryStatus,
|
||||
'all_inline',
|
||||
'low',
|
||||
);
|
||||
|
||||
expect(result.current.currentLoadingPhrase).toBe(
|
||||
"This is taking a bit longer, we're still on it.",
|
||||
);
|
||||
});
|
||||
|
||||
it('should show no phrases when loadingPhraseLayout is "none"', () => {
|
||||
const { result } = renderLoadingIndicatorHook(
|
||||
StreamingState.Responding,
|
||||
false,
|
||||
null,
|
||||
'none',
|
||||
);
|
||||
|
||||
expect(result.current.currentLoadingPhrase).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
getDisplayString,
|
||||
type RetryAttemptPayload,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { LoadingPhrasesMode } from '../../config/settings.js';
|
||||
|
||||
const LOW_VERBOSITY_RETRY_HINT_ATTEMPT_THRESHOLD = 2;
|
||||
|
||||
@@ -20,7 +19,8 @@ export interface UseLoadingIndicatorProps {
|
||||
streamingState: StreamingState;
|
||||
shouldShowFocusHint: boolean;
|
||||
retryStatus: RetryAttemptPayload | null;
|
||||
loadingPhraseLayout?: LoadingPhrasesMode;
|
||||
showTips?: boolean;
|
||||
showWit?: boolean;
|
||||
customWittyPhrases?: string[];
|
||||
errorVerbosity?: 'low' | 'full';
|
||||
maxLength?: number;
|
||||
@@ -30,7 +30,8 @@ export const useLoadingIndicator = ({
|
||||
streamingState,
|
||||
shouldShowFocusHint,
|
||||
retryStatus,
|
||||
loadingPhraseLayout,
|
||||
showTips = true,
|
||||
showWit = true,
|
||||
customWittyPhrases,
|
||||
errorVerbosity = 'full',
|
||||
maxLength,
|
||||
@@ -46,7 +47,8 @@ export const useLoadingIndicator = ({
|
||||
isPhraseCyclingActive,
|
||||
isWaiting,
|
||||
shouldShowFocusHint,
|
||||
loadingPhraseLayout,
|
||||
showTips,
|
||||
showWit,
|
||||
customWittyPhrases,
|
||||
maxLength,
|
||||
);
|
||||
|
||||
@@ -14,30 +14,35 @@ import {
|
||||
} from './usePhraseCycler.js';
|
||||
import { INFORMATIVE_TIPS } from '../constants/tips.js';
|
||||
import { WITTY_LOADING_PHRASES } from '../constants/wittyPhrases.js';
|
||||
import type { LoadingPhrasesMode } from '../../config/settings.js';
|
||||
|
||||
// Test component to consume the hook
|
||||
const TestComponent = ({
|
||||
isActive,
|
||||
isWaiting,
|
||||
isInteractiveShellWaiting = false,
|
||||
loadingPhraseLayout = 'all_inline',
|
||||
shouldShowFocusHint = false,
|
||||
showTips = true,
|
||||
showWit = true,
|
||||
customPhrases,
|
||||
}: {
|
||||
isActive: boolean;
|
||||
isWaiting: boolean;
|
||||
isInteractiveShellWaiting?: boolean;
|
||||
loadingPhraseLayout?: LoadingPhrasesMode;
|
||||
shouldShowFocusHint?: boolean;
|
||||
showTips?: boolean;
|
||||
showWit?: boolean;
|
||||
customPhrases?: string[];
|
||||
}) => {
|
||||
const { currentTip, currentWittyPhrase } = usePhraseCycler(
|
||||
isActive,
|
||||
isWaiting,
|
||||
isInteractiveShellWaiting,
|
||||
loadingPhraseLayout,
|
||||
shouldShowFocusHint,
|
||||
showTips,
|
||||
showWit,
|
||||
customPhrases,
|
||||
);
|
||||
return <Text>{currentTip || currentWittyPhrase}</Text>;
|
||||
// For tests, we'll combine them to verify existence
|
||||
return (
|
||||
<Text>{[currentTip, currentWittyPhrase].filter(Boolean).join(' | ')}</Text>
|
||||
);
|
||||
};
|
||||
|
||||
describe('usePhraseCycler', () => {
|
||||
@@ -75,7 +80,7 @@ describe('usePhraseCycler', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should show interactive shell waiting message immediately when isInteractiveShellWaiting is true', async () => {
|
||||
it('should show interactive shell waiting message immediately when shouldShowFocusHint is true', async () => {
|
||||
const { lastFrame, rerender, waitUntilReady, unmount } = render(
|
||||
<TestComponent isActive={true} isWaiting={false} />,
|
||||
);
|
||||
@@ -86,7 +91,7 @@ describe('usePhraseCycler', () => {
|
||||
<TestComponent
|
||||
isActive={true}
|
||||
isWaiting={false}
|
||||
isInteractiveShellWaiting={true}
|
||||
shouldShowFocusHint={true}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
@@ -108,7 +113,7 @@ describe('usePhraseCycler', () => {
|
||||
<TestComponent
|
||||
isActive={true}
|
||||
isWaiting={true}
|
||||
isInteractiveShellWaiting={true}
|
||||
shouldShowFocusHint={true}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
@@ -133,55 +138,56 @@ describe('usePhraseCycler', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should show a tip on first activation, then a witty phrase', async () => {
|
||||
vi.spyOn(Math, 'random').mockImplementation(() => 0.99); // Subsequent phrases are witty
|
||||
it('should show both a tip and a witty phrase when both are enabled', async () => {
|
||||
vi.spyOn(Math, 'random').mockImplementation(() => 0.5);
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<TestComponent isActive={true} isWaiting={false} />,
|
||||
<TestComponent
|
||||
isActive={true}
|
||||
isWaiting={false}
|
||||
showTips={true}
|
||||
showWit={true}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
// Initial phrase on first activation should be a tip
|
||||
expect(INFORMATIVE_TIPS).toContain(lastFrame().trim());
|
||||
|
||||
// After the first interval, it should be a witty phrase
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(PHRASE_CHANGE_INTERVAL_MS + 100);
|
||||
});
|
||||
await waitUntilReady();
|
||||
expect(WITTY_LOADING_PHRASES).toContain(lastFrame().trim());
|
||||
// In the new logic, both are selected independently if enabled.
|
||||
const frame = lastFrame().trim();
|
||||
const parts = frame.split(' | ');
|
||||
expect(parts).toHaveLength(2);
|
||||
expect(INFORMATIVE_TIPS).toContain(parts[0]);
|
||||
expect(WITTY_LOADING_PHRASES).toContain(parts[1]);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should cycle through phrases when isActive is true and not waiting', async () => {
|
||||
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty for subsequent phrases
|
||||
vi.spyOn(Math, 'random').mockImplementation(() => 0.5);
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<TestComponent isActive={true} isWaiting={false} />,
|
||||
<TestComponent
|
||||
isActive={true}
|
||||
isWaiting={false}
|
||||
showTips={true}
|
||||
showWit={true}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
// Initial phrase on first activation will be a tip
|
||||
|
||||
// After the first interval, it should follow the random pattern (witty phrases due to mock)
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(PHRASE_CHANGE_INTERVAL_MS + 100);
|
||||
});
|
||||
await waitUntilReady();
|
||||
expect(WITTY_LOADING_PHRASES).toContain(lastFrame().trim());
|
||||
const frame = lastFrame().trim();
|
||||
const parts = frame.split(' | ');
|
||||
expect(parts).toHaveLength(2);
|
||||
expect(INFORMATIVE_TIPS).toContain(parts[0]);
|
||||
expect(WITTY_LOADING_PHRASES).toContain(parts[1]);
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(PHRASE_CHANGE_INTERVAL_MS);
|
||||
});
|
||||
await waitUntilReady();
|
||||
expect(WITTY_LOADING_PHRASES).toContain(lastFrame().trim());
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should reset to a phrase when isActive becomes true after being false', async () => {
|
||||
it('should reset to phrases when isActive becomes true after being false', async () => {
|
||||
const customPhrases = ['Phrase A', 'Phrase B'];
|
||||
let callCount = 0;
|
||||
vi.spyOn(Math, 'random').mockImplementation(() => {
|
||||
// For custom phrases, only 1 Math.random call is made per update.
|
||||
// 0 -> index 0 ('Phrase A')
|
||||
// 0.99 -> index 1 ('Phrase B')
|
||||
const val = callCount % 2 === 0 ? 0 : 0.99;
|
||||
callCount++;
|
||||
return val;
|
||||
@@ -192,34 +198,31 @@ describe('usePhraseCycler', () => {
|
||||
isActive={false}
|
||||
isWaiting={false}
|
||||
customPhrases={customPhrases}
|
||||
showWit={true}
|
||||
showTips={false}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
// Activate -> On first activation will show tip on initial call, then first interval will use first mock value for 'Phrase A'
|
||||
// Activate
|
||||
await act(async () => {
|
||||
rerender(
|
||||
<TestComponent
|
||||
isActive={true}
|
||||
isWaiting={false}
|
||||
customPhrases={customPhrases}
|
||||
showWit={true}
|
||||
showTips={false}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(PHRASE_CHANGE_INTERVAL_MS); // First interval after initial state -> callCount 0 -> 'Phrase A'
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
});
|
||||
await waitUntilReady();
|
||||
expect(customPhrases).toContain(lastFrame().trim()); // Should be one of the custom phrases
|
||||
|
||||
// Second interval -> callCount 1 -> returns 0.99 -> 'Phrase B'
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(PHRASE_CHANGE_INTERVAL_MS);
|
||||
});
|
||||
await waitUntilReady();
|
||||
expect(customPhrases).toContain(lastFrame().trim()); // Should be one of the custom phrases
|
||||
expect(customPhrases).toContain(lastFrame().trim());
|
||||
|
||||
// Deactivate -> resets to undefined (empty string in output)
|
||||
await act(async () => {
|
||||
@@ -228,6 +231,8 @@ describe('usePhraseCycler', () => {
|
||||
isActive={false}
|
||||
isWaiting={false}
|
||||
customPhrases={customPhrases}
|
||||
showWit={true}
|
||||
showTips={false}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
@@ -235,24 +240,6 @@ describe('usePhraseCycler', () => {
|
||||
|
||||
// The phrase should be empty after reset
|
||||
expect(lastFrame({ allowEmpty: true }).trim()).toBe('');
|
||||
|
||||
// Activate again -> this will show a tip on first activation, then cycle from where mock is
|
||||
await act(async () => {
|
||||
rerender(
|
||||
<TestComponent
|
||||
isActive={true}
|
||||
isWaiting={false}
|
||||
customPhrases={customPhrases}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(PHRASE_CHANGE_INTERVAL_MS); // First interval after re-activation -> should contain phrase
|
||||
});
|
||||
await waitUntilReady();
|
||||
expect(customPhrases).toContain(lastFrame().trim()); // Should be one of the custom phrases
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -293,7 +280,8 @@ describe('usePhraseCycler', () => {
|
||||
<TestComponent
|
||||
isActive={config.isActive}
|
||||
isWaiting={false}
|
||||
loadingPhraseLayout="wit_inline"
|
||||
showTips={false}
|
||||
showWit={true}
|
||||
customPhrases={config.customPhrases}
|
||||
/>
|
||||
);
|
||||
@@ -304,7 +292,7 @@ describe('usePhraseCycler', () => {
|
||||
|
||||
// After first interval, it should use custom phrases
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(PHRASE_CHANGE_INTERVAL_MS + 100);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
@@ -323,78 +311,24 @@ describe('usePhraseCycler', () => {
|
||||
await waitUntilReady();
|
||||
expect(customPhrases).toContain(lastFrame({ allowEmpty: true }).trim());
|
||||
|
||||
randomMock.mockReturnValue(0.99);
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(PHRASE_CHANGE_INTERVAL_MS);
|
||||
});
|
||||
await waitUntilReady();
|
||||
expect(customPhrases).toContain(lastFrame({ allowEmpty: true }).trim());
|
||||
|
||||
// Test fallback to default phrases.
|
||||
randomMock.mockRestore();
|
||||
vi.spyOn(Math, 'random').mockReturnValue(0.5); // Always witty
|
||||
|
||||
await act(async () => {
|
||||
setStateExternally?.({
|
||||
isActive: true,
|
||||
customPhrases: [] as string[],
|
||||
});
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(PHRASE_CHANGE_INTERVAL_MS); // Wait for first cycle
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
expect(WITTY_LOADING_PHRASES).toContain(lastFrame().trim());
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should fall back to witty phrases if custom phrases are an empty array', async () => {
|
||||
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty for subsequent phrases
|
||||
vi.spyOn(Math, 'random').mockImplementation(() => 0.5);
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<TestComponent isActive={true} isWaiting={false} customPhrases={[]} />,
|
||||
<TestComponent
|
||||
isActive={true}
|
||||
isWaiting={false}
|
||||
showTips={false}
|
||||
showWit={true}
|
||||
customPhrases={[]}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(PHRASE_CHANGE_INTERVAL_MS); // Next phrase after tip
|
||||
});
|
||||
await waitUntilReady();
|
||||
expect(WITTY_LOADING_PHRASES).toContain(lastFrame().trim());
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should reset phrase when transitioning from waiting to active', async () => {
|
||||
vi.spyOn(Math, 'random').mockImplementation(() => 0.5); // Always witty for subsequent phrases
|
||||
const { lastFrame, rerender, waitUntilReady, unmount } = render(
|
||||
<TestComponent isActive={true} isWaiting={false} />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
// Cycle to a different phrase (should be witty due to mock)
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(PHRASE_CHANGE_INTERVAL_MS);
|
||||
});
|
||||
await waitUntilReady();
|
||||
expect(WITTY_LOADING_PHRASES).toContain(lastFrame().trim());
|
||||
|
||||
// Go to waiting state
|
||||
await act(async () => {
|
||||
rerender(<TestComponent isActive={false} isWaiting={true} />);
|
||||
});
|
||||
await waitUntilReady();
|
||||
expect(lastFrame().trim()).toMatchSnapshot();
|
||||
|
||||
// Go back to active cycling - should pick a phrase based on the logic (witty due to mock)
|
||||
await act(async () => {
|
||||
rerender(<TestComponent isActive={true} isWaiting={false} />);
|
||||
});
|
||||
await waitUntilReady();
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(PHRASE_CHANGE_INTERVAL_MS); // Skip the tip and get next phrase
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
});
|
||||
await waitUntilReady();
|
||||
expect(WITTY_LOADING_PHRASES).toContain(lastFrame().trim());
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { INFORMATIVE_TIPS } from '../constants/tips.js';
|
||||
import { WITTY_LOADING_PHRASES } from '../constants/wittyPhrases.js';
|
||||
import type { LoadingPhrasesMode } from '../../config/settings.js';
|
||||
|
||||
export const PHRASE_CHANGE_INTERVAL_MS = 15000;
|
||||
export const INTERACTIVE_SHELL_WAITING_PHRASE =
|
||||
@@ -18,26 +17,33 @@ export const INTERACTIVE_SHELL_WAITING_PHRASE =
|
||||
* @param isActive Whether the phrase cycling should be active.
|
||||
* @param isWaiting Whether to show a specific waiting phrase.
|
||||
* @param shouldShowFocusHint Whether to show the shell focus hint.
|
||||
* @param loadingPhraseLayout Which phrases to show and where.
|
||||
* @param showTips Whether to show informative tips.
|
||||
* @param showWit Whether to show witty phrases.
|
||||
* @param customPhrases Optional list of custom phrases to use instead of built-in witty phrases.
|
||||
* @param maxLength Optional maximum length for the selected phrase.
|
||||
* @returns The current tip and witty phrase.
|
||||
* @returns The current loading phrase.
|
||||
*/
|
||||
export const usePhraseCycler = (
|
||||
isActive: boolean,
|
||||
isWaiting: boolean,
|
||||
shouldShowFocusHint: boolean,
|
||||
loadingPhraseLayout: LoadingPhrasesMode = 'all_inline',
|
||||
showTips: boolean = true,
|
||||
showWit: boolean = true,
|
||||
customPhrases?: string[],
|
||||
maxLength?: number,
|
||||
) => {
|
||||
const [currentTip, setCurrentTip] = useState<string | undefined>(undefined);
|
||||
const [currentWittyPhrase, setCurrentWittyPhrase] = useState<
|
||||
const [currentTipState, setCurrentTipState] = useState<string | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [currentWittyPhraseState, setCurrentWittyPhraseState] = useState<
|
||||
string | undefined
|
||||
>(undefined);
|
||||
|
||||
const phraseIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const hasShownFirstRequestTipRef = useRef(false);
|
||||
const lastChangeTimeRef = useRef<number>(0);
|
||||
const lastSelectedTipRef = useRef<string | undefined>(undefined);
|
||||
const lastSelectedWittyPhraseRef = useRef<string | undefined>(undefined);
|
||||
const MIN_TIP_DISPLAY_TIME_MS = 10000;
|
||||
|
||||
useEffect(() => {
|
||||
// Always clear on re-run
|
||||
@@ -46,86 +52,75 @@ export const usePhraseCycler = (
|
||||
phraseIntervalRef.current = null;
|
||||
}
|
||||
|
||||
if (shouldShowFocusHint) {
|
||||
setCurrentTip(INTERACTIVE_SHELL_WAITING_PHRASE);
|
||||
setCurrentWittyPhrase(undefined);
|
||||
if (shouldShowFocusHint || isWaiting) {
|
||||
// These are handled by the return value directly for immediate feedback
|
||||
return;
|
||||
}
|
||||
|
||||
if (isWaiting) {
|
||||
setCurrentTip('Waiting for user confirmation...');
|
||||
setCurrentWittyPhrase(undefined);
|
||||
if (!isActive || (!showTips && !showWit)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isActive || loadingPhraseLayout === 'none') {
|
||||
setCurrentTip(undefined);
|
||||
setCurrentWittyPhrase(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const wittyPhrases =
|
||||
const wittyPhrasesList =
|
||||
customPhrases && customPhrases.length > 0
|
||||
? customPhrases
|
||||
: WITTY_LOADING_PHRASES;
|
||||
|
||||
const setRandomPhrase = () => {
|
||||
let currentMode: 'tips' | 'witty' | 'all' = 'all';
|
||||
|
||||
if (loadingPhraseLayout === 'tips') {
|
||||
currentMode = 'tips';
|
||||
} else if (
|
||||
loadingPhraseLayout === 'wit_status' ||
|
||||
loadingPhraseLayout === 'wit_inline' ||
|
||||
loadingPhraseLayout === 'wit_ambient'
|
||||
) {
|
||||
currentMode = 'witty';
|
||||
}
|
||||
|
||||
// In 'all' modes, we decide once per phrase cycle what to show
|
||||
const setRandomPhrases = (force: boolean = false) => {
|
||||
const now = Date.now();
|
||||
if (
|
||||
loadingPhraseLayout === 'all_inline' ||
|
||||
loadingPhraseLayout === 'all_ambient'
|
||||
!force &&
|
||||
now - lastChangeTimeRef.current < MIN_TIP_DISPLAY_TIME_MS &&
|
||||
(lastSelectedTipRef.current || lastSelectedWittyPhraseRef.current)
|
||||
) {
|
||||
if (!hasShownFirstRequestTipRef.current) {
|
||||
currentMode = 'tips';
|
||||
hasShownFirstRequestTipRef.current = true;
|
||||
} else {
|
||||
currentMode = Math.random() < 1 / 2 ? 'tips' : 'witty';
|
||||
}
|
||||
// Sync state if it was cleared by inactivation.
|
||||
setCurrentTipState(lastSelectedTipRef.current);
|
||||
setCurrentWittyPhraseState(lastSelectedWittyPhraseRef.current);
|
||||
return;
|
||||
}
|
||||
|
||||
const phraseList =
|
||||
currentMode === 'witty' ? wittyPhrases : INFORMATIVE_TIPS;
|
||||
const adjustedMaxLength = maxLength;
|
||||
|
||||
const filteredList =
|
||||
maxLength !== undefined
|
||||
? phraseList.filter((p) => p.length <= maxLength)
|
||||
: phraseList;
|
||||
|
||||
if (filteredList.length > 0) {
|
||||
const randomIndex = Math.floor(Math.random() * filteredList.length);
|
||||
const selected = filteredList[randomIndex];
|
||||
if (currentMode === 'witty') {
|
||||
setCurrentWittyPhrase(selected);
|
||||
setCurrentTip(undefined);
|
||||
} else {
|
||||
setCurrentTip(selected);
|
||||
setCurrentWittyPhrase(undefined);
|
||||
if (showTips) {
|
||||
const filteredTips =
|
||||
adjustedMaxLength !== undefined
|
||||
? INFORMATIVE_TIPS.filter((p) => p.length <= adjustedMaxLength)
|
||||
: INFORMATIVE_TIPS;
|
||||
if (filteredTips.length > 0) {
|
||||
const selected =
|
||||
filteredTips[Math.floor(Math.random() * filteredTips.length)];
|
||||
setCurrentTipState(selected);
|
||||
lastSelectedTipRef.current = selected;
|
||||
}
|
||||
} else {
|
||||
// If no phrases fit, try to fallback
|
||||
setCurrentTip(undefined);
|
||||
setCurrentWittyPhrase(undefined);
|
||||
setCurrentTipState(undefined);
|
||||
lastSelectedTipRef.current = undefined;
|
||||
}
|
||||
|
||||
if (showWit) {
|
||||
const filteredWitty =
|
||||
adjustedMaxLength !== undefined
|
||||
? wittyPhrasesList.filter((p) => p.length <= adjustedMaxLength)
|
||||
: wittyPhrasesList;
|
||||
if (filteredWitty.length > 0) {
|
||||
const selected =
|
||||
filteredWitty[Math.floor(Math.random() * filteredWitty.length)];
|
||||
setCurrentWittyPhraseState(selected);
|
||||
lastSelectedWittyPhraseRef.current = selected;
|
||||
}
|
||||
} else {
|
||||
setCurrentWittyPhraseState(undefined);
|
||||
lastSelectedWittyPhraseRef.current = undefined;
|
||||
}
|
||||
|
||||
lastChangeTimeRef.current = now;
|
||||
};
|
||||
|
||||
// Select an initial random phrase
|
||||
setRandomPhrase();
|
||||
// Select initial random phrases or resume previous ones
|
||||
setRandomPhrases(false);
|
||||
|
||||
phraseIntervalRef.current = setInterval(() => {
|
||||
// Select a new random phrase
|
||||
setRandomPhrase();
|
||||
setRandomPhrases(true); // Force change on interval
|
||||
}, PHRASE_CHANGE_INTERVAL_MS);
|
||||
|
||||
return () => {
|
||||
@@ -138,10 +133,23 @@ export const usePhraseCycler = (
|
||||
isActive,
|
||||
isWaiting,
|
||||
shouldShowFocusHint,
|
||||
loadingPhraseLayout,
|
||||
showTips,
|
||||
showWit,
|
||||
customPhrases,
|
||||
maxLength,
|
||||
]);
|
||||
|
||||
let currentTip = undefined;
|
||||
let currentWittyPhrase = undefined;
|
||||
|
||||
if (shouldShowFocusHint) {
|
||||
currentTip = INTERACTIVE_SHELL_WAITING_PHRASE;
|
||||
} else if (isWaiting) {
|
||||
currentTip = 'Waiting for user confirmation...';
|
||||
} else if (isActive) {
|
||||
currentTip = currentTipState;
|
||||
currentWittyPhrase = currentWittyPhraseState;
|
||||
}
|
||||
|
||||
return { currentTip, currentWittyPhrase };
|
||||
};
|
||||
|
||||
@@ -18,3 +18,5 @@ export const REDIRECTION_WARNING_NOTE_TEXT =
|
||||
export const REDIRECTION_WARNING_TIP_LABEL = 'Tip: '; // Padded to align with "Note: "
|
||||
export const REDIRECTION_WARNING_TIP_TEXT =
|
||||
'Toggle auto-edit (Shift+Tab) to allow redirection in the future.';
|
||||
|
||||
export const GENERIC_WORKING_LABEL = 'Working...';
|
||||
|
||||
@@ -516,6 +516,7 @@ export interface PermissionConfirmationRequest {
|
||||
export interface ActiveHook {
|
||||
name: string;
|
||||
eventName: string;
|
||||
source?: string;
|
||||
index?: number;
|
||||
total?: number;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user