mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-12 12:54:07 -07:00
feat(cli): prototype clean UI toggle and minimal-mode bleed-through (#18683)
This commit is contained in:
@@ -120,6 +120,8 @@ Slash commands provide meta-level control over the CLI itself.
|
|||||||
- **`/shortcuts`**
|
- **`/shortcuts`**
|
||||||
- **Description:** Toggle the shortcuts panel above the input.
|
- **Description:** Toggle the shortcuts panel above the input.
|
||||||
- **Shortcut:** Press `?` when the prompt is empty.
|
- **Shortcut:** Press `?` when the prompt is empty.
|
||||||
|
- **Note:** This is separate from the clean UI detail toggle on double-`Tab`,
|
||||||
|
which switches between minimal and full UI chrome.
|
||||||
|
|
||||||
- **`/hooks`**
|
- **`/hooks`**
|
||||||
- **Description:** Manage hooks, which allow you to intercept and customize
|
- **Description:** Manage hooks, which allow you to intercept and customize
|
||||||
|
|||||||
@@ -114,8 +114,8 @@ available combinations.
|
|||||||
| Dismiss background shell list. | `Esc` |
|
| Dismiss background shell list. | `Esc` |
|
||||||
| Move focus from background shell to Gemini. | `Shift + Tab` |
|
| Move focus from background shell to Gemini. | `Shift + Tab` |
|
||||||
| Move focus from background shell list to Gemini. | `Tab (no Shift)` |
|
| Move focus from background shell list to Gemini. | `Tab (no Shift)` |
|
||||||
| Show warning when trying to unfocus background shell via Tab. | `Tab (no Shift)` |
|
| Show warning when trying to move focus away from background shell. | `Tab (no Shift)` |
|
||||||
| Show warning when trying to unfocus shell input via Tab. | `Tab (no Shift)` |
|
| Show warning when trying to move focus away from shell input. | `Tab (no Shift)` |
|
||||||
| Move focus from Gemini to the active shell. | `Tab (no Shift)` |
|
| Move focus from Gemini to the active shell. | `Tab (no Shift)` |
|
||||||
| Move focus from the shell back to Gemini. | `Shift + Tab` |
|
| Move focus from the shell back to Gemini. | `Shift + Tab` |
|
||||||
| Clear the terminal screen and redraw the UI. | `Ctrl + L` |
|
| Clear the terminal screen and redraw the UI. | `Ctrl + L` |
|
||||||
@@ -134,6 +134,11 @@ available combinations.
|
|||||||
The panel also auto-hides while the agent is running/streaming or when
|
The panel also auto-hides while the agent is running/streaming or when
|
||||||
action-required dialogs are shown. Press `?` again to close the panel and
|
action-required dialogs are shown. Press `?` again to close the panel and
|
||||||
insert a `?` into the prompt.
|
insert a `?` into the prompt.
|
||||||
|
- `Tab` + `Tab` (while typing in the prompt): Toggle between minimal and full UI
|
||||||
|
details when no completion/search interaction is active. The selected mode is
|
||||||
|
remembered for future sessions. Full UI remains the default on first run, and
|
||||||
|
single `Tab` keeps its existing completion/focus behavior.
|
||||||
|
- `Shift + Tab` (while typing in the prompt): Cycle approval modes.
|
||||||
- `\` (at end of a line) + `Enter`: Insert a newline without leaving single-line
|
- `\` (at end of a line) + `Enter`: Insert a newline without leaving single-line
|
||||||
mode.
|
mode.
|
||||||
- `Esc` pressed twice quickly: Clear the input prompt if it is not empty,
|
- `Esc` pressed twice quickly: Clear the input prompt if it is not empty,
|
||||||
|
|||||||
@@ -516,9 +516,9 @@ export const commandDescriptions: Readonly<Record<Command, string>> = {
|
|||||||
[Command.UNFOCUS_BACKGROUND_SHELL_LIST]:
|
[Command.UNFOCUS_BACKGROUND_SHELL_LIST]:
|
||||||
'Move focus from background shell list to Gemini.',
|
'Move focus from background shell list to Gemini.',
|
||||||
[Command.SHOW_BACKGROUND_SHELL_UNFOCUS_WARNING]:
|
[Command.SHOW_BACKGROUND_SHELL_UNFOCUS_WARNING]:
|
||||||
'Show warning when trying to unfocus background shell via Tab.',
|
'Show warning when trying to move focus away from background shell.',
|
||||||
[Command.SHOW_SHELL_INPUT_UNFOCUS_WARNING]:
|
[Command.SHOW_SHELL_INPUT_UNFOCUS_WARNING]:
|
||||||
'Show warning when trying to unfocus shell input via Tab.',
|
'Show warning when trying to move focus away from shell input.',
|
||||||
[Command.FOCUS_SHELL_INPUT]: 'Move focus from Gemini to the active shell.',
|
[Command.FOCUS_SHELL_INPUT]: 'Move focus from Gemini to the active shell.',
|
||||||
[Command.UNFOCUS_SHELL_INPUT]: 'Move focus from the shell back to Gemini.',
|
[Command.UNFOCUS_SHELL_INPUT]: 'Move focus from the shell back to Gemini.',
|
||||||
[Command.CLEAR_SCREEN]: 'Clear the terminal screen and redraw the UI.',
|
[Command.CLEAR_SCREEN]: 'Clear the terminal screen and redraw the UI.',
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ const baseMockUiState = {
|
|||||||
terminalWidth: 120,
|
terminalWidth: 120,
|
||||||
terminalHeight: 40,
|
terminalHeight: 40,
|
||||||
currentModel: 'gemini-pro',
|
currentModel: 'gemini-pro',
|
||||||
|
cleanUiDetailsVisible: false,
|
||||||
terminalBackgroundColor: undefined,
|
terminalBackgroundColor: undefined,
|
||||||
activePtyId: undefined,
|
activePtyId: undefined,
|
||||||
backgroundShells: new Map(),
|
backgroundShells: new Map(),
|
||||||
@@ -204,6 +205,10 @@ const mockUIActions: UIActions = {
|
|||||||
handleApiKeyCancel: vi.fn(),
|
handleApiKeyCancel: vi.fn(),
|
||||||
setBannerVisible: vi.fn(),
|
setBannerVisible: vi.fn(),
|
||||||
setShortcutsHelpVisible: vi.fn(),
|
setShortcutsHelpVisible: vi.fn(),
|
||||||
|
setCleanUiDetailsVisible: vi.fn(),
|
||||||
|
toggleCleanUiDetailsVisible: vi.fn(),
|
||||||
|
revealCleanUiDetailsTemporarily: vi.fn(),
|
||||||
|
handleWarning: vi.fn(),
|
||||||
setEmbeddedShellFocused: vi.fn(),
|
setEmbeddedShellFocused: vi.fn(),
|
||||||
dismissBackgroundShell: vi.fn(),
|
dismissBackgroundShell: vi.fn(),
|
||||||
setActiveBackgroundShellPid: vi.fn(),
|
setActiveBackgroundShellPid: vi.fn(),
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ describe('App', () => {
|
|||||||
|
|
||||||
const mockUIState: Partial<UIState> = {
|
const mockUIState: Partial<UIState> = {
|
||||||
streamingState: StreamingState.Idle,
|
streamingState: StreamingState.Idle,
|
||||||
|
cleanUiDetailsVisible: true,
|
||||||
quittingMessages: null,
|
quittingMessages: null,
|
||||||
dialogsVisible: false,
|
dialogsVisible: false,
|
||||||
mainControlsRef: {
|
mainControlsRef: {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
type Mock,
|
type Mock,
|
||||||
type MockedObject,
|
type MockedObject,
|
||||||
} from 'vitest';
|
} from 'vitest';
|
||||||
import { render } from '../test-utils/render.js';
|
import { render, persistentStateMock } from '../test-utils/render.js';
|
||||||
import { waitFor } from '../test-utils/async.js';
|
import { waitFor } from '../test-utils/async.js';
|
||||||
import { cleanup } from 'ink-testing-library';
|
import { cleanup } from 'ink-testing-library';
|
||||||
import { act, useContext, type ReactElement } from 'react';
|
import { act, useContext, type ReactElement } from 'react';
|
||||||
@@ -299,6 +299,7 @@ describe('AppContainer State Management', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
persistentStateMock.reset();
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
|
||||||
mockIdeClient.getInstance.mockReturnValue(new Promise(() => {}));
|
mockIdeClient.getInstance.mockReturnValue(new Promise(() => {}));
|
||||||
@@ -488,6 +489,37 @@ describe('AppContainer State Management', () => {
|
|||||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||||
unmount!();
|
unmount!();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows full UI details by default', async () => {
|
||||||
|
let unmount: () => void;
|
||||||
|
await act(async () => {
|
||||||
|
const result = renderAppContainer();
|
||||||
|
unmount = result.unmount;
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(capturedUIState.cleanUiDetailsVisible).toBe(true);
|
||||||
|
});
|
||||||
|
unmount!();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('starts in minimal UI mode when Focus UI preference is persisted', async () => {
|
||||||
|
persistentStateMock.get.mockReturnValueOnce(true);
|
||||||
|
|
||||||
|
let unmount: () => void;
|
||||||
|
await act(async () => {
|
||||||
|
const result = renderAppContainer({
|
||||||
|
settings: mockSettings,
|
||||||
|
});
|
||||||
|
unmount = result.unmount;
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(capturedUIState.cleanUiDetailsVisible).toBe(false);
|
||||||
|
});
|
||||||
|
expect(persistentStateMock.get).toHaveBeenCalledWith('focusUiEnabled');
|
||||||
|
unmount!();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('State Initialization', () => {
|
describe('State Initialization', () => {
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import {
|
|||||||
type UserTierId,
|
type UserTierId,
|
||||||
type UserFeedbackPayload,
|
type UserFeedbackPayload,
|
||||||
type AgentDefinition,
|
type AgentDefinition,
|
||||||
|
type ApprovalMode,
|
||||||
IdeClient,
|
IdeClient,
|
||||||
ideContextStore,
|
ideContextStore,
|
||||||
getErrorMessage,
|
getErrorMessage,
|
||||||
@@ -133,6 +134,7 @@ import { ShellFocusContext } from './contexts/ShellFocusContext.js';
|
|||||||
import { type ExtensionManager } from '../config/extension-manager.js';
|
import { type ExtensionManager } from '../config/extension-manager.js';
|
||||||
import { requestConsentInteractive } from '../config/extensions/consent.js';
|
import { requestConsentInteractive } from '../config/extensions/consent.js';
|
||||||
import { useSessionBrowser } from './hooks/useSessionBrowser.js';
|
import { useSessionBrowser } from './hooks/useSessionBrowser.js';
|
||||||
|
import { persistentState } from '../utils/persistentState.js';
|
||||||
import { useSessionResume } from './hooks/useSessionResume.js';
|
import { useSessionResume } from './hooks/useSessionResume.js';
|
||||||
import { useIncludeDirsTrust } from './hooks/useIncludeDirsTrust.js';
|
import { useIncludeDirsTrust } from './hooks/useIncludeDirsTrust.js';
|
||||||
import { isWorkspaceTrusted } from '../config/trustedFolders.js';
|
import { isWorkspaceTrusted } from '../config/trustedFolders.js';
|
||||||
@@ -184,6 +186,9 @@ interface AppContainerProps {
|
|||||||
resumedSessionData?: ResumedSessionData;
|
resumedSessionData?: ResumedSessionData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const APPROVAL_MODE_REVEAL_DURATION_MS = 1200;
|
||||||
|
const FOCUS_UI_ENABLED_STATE_KEY = 'focusUiEnabled';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The fraction of the terminal width to allocate to the shell.
|
* The fraction of the terminal width to allocate to the shell.
|
||||||
* This provides horizontal padding.
|
* This provides horizontal padding.
|
||||||
@@ -796,7 +801,65 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
const setIsBackgroundShellListOpenRef = useRef<(open: boolean) => void>(
|
const setIsBackgroundShellListOpenRef = useRef<(open: boolean) => void>(
|
||||||
() => {},
|
() => {},
|
||||||
);
|
);
|
||||||
|
const [focusUiEnabledByDefault] = useState(
|
||||||
|
() => persistentState.get(FOCUS_UI_ENABLED_STATE_KEY) === true,
|
||||||
|
);
|
||||||
const [shortcutsHelpVisible, setShortcutsHelpVisible] = useState(false);
|
const [shortcutsHelpVisible, setShortcutsHelpVisible] = useState(false);
|
||||||
|
const [cleanUiDetailsVisible, setCleanUiDetailsVisibleState] = useState(
|
||||||
|
!focusUiEnabledByDefault,
|
||||||
|
);
|
||||||
|
const modeRevealTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const cleanUiDetailsPinnedRef = useRef(!focusUiEnabledByDefault);
|
||||||
|
|
||||||
|
const clearModeRevealTimeout = useCallback(() => {
|
||||||
|
if (modeRevealTimeoutRef.current) {
|
||||||
|
clearTimeout(modeRevealTimeoutRef.current);
|
||||||
|
modeRevealTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const persistFocusUiPreference = useCallback((isFullUiVisible: boolean) => {
|
||||||
|
persistentState.set(FOCUS_UI_ENABLED_STATE_KEY, !isFullUiVisible);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setCleanUiDetailsVisible = useCallback(
|
||||||
|
(visible: boolean) => {
|
||||||
|
clearModeRevealTimeout();
|
||||||
|
cleanUiDetailsPinnedRef.current = visible;
|
||||||
|
setCleanUiDetailsVisibleState(visible);
|
||||||
|
persistFocusUiPreference(visible);
|
||||||
|
},
|
||||||
|
[clearModeRevealTimeout, persistFocusUiPreference],
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggleCleanUiDetailsVisible = useCallback(() => {
|
||||||
|
clearModeRevealTimeout();
|
||||||
|
setCleanUiDetailsVisibleState((visible) => {
|
||||||
|
const nextVisible = !visible;
|
||||||
|
cleanUiDetailsPinnedRef.current = nextVisible;
|
||||||
|
persistFocusUiPreference(nextVisible);
|
||||||
|
return nextVisible;
|
||||||
|
});
|
||||||
|
}, [clearModeRevealTimeout, persistFocusUiPreference]);
|
||||||
|
|
||||||
|
const revealCleanUiDetailsTemporarily = useCallback(
|
||||||
|
(durationMs: number = APPROVAL_MODE_REVEAL_DURATION_MS) => {
|
||||||
|
if (cleanUiDetailsPinnedRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearModeRevealTimeout();
|
||||||
|
setCleanUiDetailsVisibleState(true);
|
||||||
|
modeRevealTimeoutRef.current = setTimeout(() => {
|
||||||
|
if (!cleanUiDetailsPinnedRef.current) {
|
||||||
|
setCleanUiDetailsVisibleState(false);
|
||||||
|
}
|
||||||
|
modeRevealTimeoutRef.current = null;
|
||||||
|
}, durationMs);
|
||||||
|
},
|
||||||
|
[clearModeRevealTimeout],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => () => clearModeRevealTimeout(), [clearModeRevealTimeout]);
|
||||||
|
|
||||||
const slashCommandActions = useMemo(
|
const slashCommandActions = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -1057,11 +1120,25 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
const shouldShowActionRequiredTitle = inactivityStatus === 'action_required';
|
const shouldShowActionRequiredTitle = inactivityStatus === 'action_required';
|
||||||
const shouldShowSilentWorkingTitle = inactivityStatus === 'silent_working';
|
const shouldShowSilentWorkingTitle = inactivityStatus === 'silent_working';
|
||||||
|
|
||||||
|
const handleApprovalModeChangeWithUiReveal = useCallback(
|
||||||
|
(mode: ApprovalMode) => {
|
||||||
|
void handleApprovalModeChange(mode);
|
||||||
|
if (!cleanUiDetailsVisible) {
|
||||||
|
revealCleanUiDetailsTemporarily(APPROVAL_MODE_REVEAL_DURATION_MS);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
handleApprovalModeChange,
|
||||||
|
cleanUiDetailsVisible,
|
||||||
|
revealCleanUiDetailsTemporarily,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
// Auto-accept indicator
|
// Auto-accept indicator
|
||||||
const showApprovalModeIndicator = useApprovalModeIndicator({
|
const showApprovalModeIndicator = useApprovalModeIndicator({
|
||||||
config,
|
config,
|
||||||
addItem: historyManager.addItem,
|
addItem: historyManager.addItem,
|
||||||
onApprovalModeChange: handleApprovalModeChange,
|
onApprovalModeChange: handleApprovalModeChangeWithUiReveal,
|
||||||
isActive: !embeddedShellFocused,
|
isActive: !embeddedShellFocused,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1377,6 +1454,9 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
if (tabFocusTimeoutRef.current) {
|
if (tabFocusTimeoutRef.current) {
|
||||||
clearTimeout(tabFocusTimeoutRef.current);
|
clearTimeout(tabFocusTimeoutRef.current);
|
||||||
}
|
}
|
||||||
|
if (modeRevealTimeoutRef.current) {
|
||||||
|
clearTimeout(modeRevealTimeoutRef.current);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [showTransientMessage]);
|
}, [showTransientMessage]);
|
||||||
|
|
||||||
@@ -1977,6 +2057,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
ctrlDPressedOnce: ctrlDPressCount >= 1,
|
ctrlDPressedOnce: ctrlDPressCount >= 1,
|
||||||
showEscapePrompt,
|
showEscapePrompt,
|
||||||
shortcutsHelpVisible,
|
shortcutsHelpVisible,
|
||||||
|
cleanUiDetailsVisible,
|
||||||
isFocused,
|
isFocused,
|
||||||
elapsedTime,
|
elapsedTime,
|
||||||
currentLoadingPhrase,
|
currentLoadingPhrase,
|
||||||
@@ -2087,6 +2168,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
ctrlDPressCount,
|
ctrlDPressCount,
|
||||||
showEscapePrompt,
|
showEscapePrompt,
|
||||||
shortcutsHelpVisible,
|
shortcutsHelpVisible,
|
||||||
|
cleanUiDetailsVisible,
|
||||||
isFocused,
|
isFocused,
|
||||||
elapsedTime,
|
elapsedTime,
|
||||||
currentLoadingPhrase,
|
currentLoadingPhrase,
|
||||||
@@ -2188,6 +2270,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
handleApiKeyCancel,
|
handleApiKeyCancel,
|
||||||
setBannerVisible,
|
setBannerVisible,
|
||||||
setShortcutsHelpVisible,
|
setShortcutsHelpVisible,
|
||||||
|
setCleanUiDetailsVisible,
|
||||||
|
toggleCleanUiDetailsVisible,
|
||||||
|
revealCleanUiDetailsTemporarily,
|
||||||
|
handleWarning,
|
||||||
setEmbeddedShellFocused,
|
setEmbeddedShellFocused,
|
||||||
dismissBackgroundShell,
|
dismissBackgroundShell,
|
||||||
setActiveBackgroundShellPid,
|
setActiveBackgroundShellPid,
|
||||||
@@ -2264,6 +2350,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
handleApiKeyCancel,
|
handleApiKeyCancel,
|
||||||
setBannerVisible,
|
setBannerVisible,
|
||||||
setShortcutsHelpVisible,
|
setShortcutsHelpVisible,
|
||||||
|
setCleanUiDetailsVisible,
|
||||||
|
toggleCleanUiDetailsVisible,
|
||||||
|
revealCleanUiDetailsTemporarily,
|
||||||
|
handleWarning,
|
||||||
setEmbeddedShellFocused,
|
setEmbeddedShellFocused,
|
||||||
dismissBackgroundShell,
|
dismissBackgroundShell,
|
||||||
setActiveBackgroundShellPid,
|
setActiveBackgroundShellPid,
|
||||||
|
|||||||
@@ -17,9 +17,10 @@ import { useTips } from '../hooks/useTips.js';
|
|||||||
|
|
||||||
interface AppHeaderProps {
|
interface AppHeaderProps {
|
||||||
version: string;
|
version: string;
|
||||||
|
showDetails?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AppHeader = ({ version }: AppHeaderProps) => {
|
export const AppHeader = ({ version, showDetails = true }: AppHeaderProps) => {
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
const { nightly, terminalWidth, bannerData, bannerVisible } = useUIState();
|
const { nightly, terminalWidth, bannerData, bannerVisible } = useUIState();
|
||||||
@@ -27,6 +28,14 @@ export const AppHeader = ({ version }: AppHeaderProps) => {
|
|||||||
const { bannerText } = useBanner(bannerData);
|
const { bannerText } = useBanner(bannerData);
|
||||||
const { showTips } = useTips();
|
const { showTips } = useTips();
|
||||||
|
|
||||||
|
if (!showDetails) {
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column">
|
||||||
|
<Header version={version} nightly={false} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
{!(settings.merged.ui.hideBanner || config.getScreenReader()) && (
|
{!(settings.merged.ui.hideBanner || config.getScreenReader()) && (
|
||||||
|
|||||||
@@ -4,9 +4,10 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
import { beforeEach, afterEach, describe, it, expect, vi } from 'vitest';
|
||||||
import { render } from '../../test-utils/render.js';
|
import { render } from '../../test-utils/render.js';
|
||||||
import { Box, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
|
import { useEffect } from 'react';
|
||||||
import { Composer } from './Composer.js';
|
import { Composer } from './Composer.js';
|
||||||
import { UIStateContext, type UIState } from '../contexts/UIStateContext.js';
|
import { UIStateContext, type UIState } from '../contexts/UIStateContext.js';
|
||||||
import {
|
import {
|
||||||
@@ -23,13 +24,18 @@ vi.mock('../contexts/VimModeContext.js', () => ({
|
|||||||
vimMode: 'INSERT',
|
vimMode: 'INSERT',
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
import { ApprovalMode } from '@google/gemini-cli-core';
|
import { ApprovalMode, tokenLimit } from '@google/gemini-cli-core';
|
||||||
import type { Config } from '@google/gemini-cli-core';
|
import type { Config } from '@google/gemini-cli-core';
|
||||||
import { StreamingState, ToolCallStatus } from '../types.js';
|
import { StreamingState, ToolCallStatus } from '../types.js';
|
||||||
import { TransientMessageType } from '../../utils/events.js';
|
import { TransientMessageType } from '../../utils/events.js';
|
||||||
import type { LoadedSettings } from '../../config/settings.js';
|
import type { LoadedSettings } from '../../config/settings.js';
|
||||||
import type { SessionMetrics } from '../contexts/SessionContext.js';
|
import type { SessionMetrics } from '../contexts/SessionContext.js';
|
||||||
|
|
||||||
|
const composerTestControls = vi.hoisted(() => ({
|
||||||
|
suggestionsVisible: false,
|
||||||
|
isAlternateBuffer: false,
|
||||||
|
}));
|
||||||
|
|
||||||
// Mock child components
|
// Mock child components
|
||||||
vi.mock('./LoadingIndicator.js', () => ({
|
vi.mock('./LoadingIndicator.js', () => ({
|
||||||
LoadingIndicator: ({
|
LoadingIndicator: ({
|
||||||
@@ -90,9 +96,19 @@ vi.mock('./DetailedMessagesDisplay.js', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./InputPrompt.js', () => ({
|
vi.mock('./InputPrompt.js', () => ({
|
||||||
InputPrompt: ({ placeholder }: { placeholder?: string }) => (
|
InputPrompt: ({
|
||||||
<Text>InputPrompt: {placeholder}</Text>
|
placeholder,
|
||||||
),
|
onSuggestionsVisibilityChange,
|
||||||
|
}: {
|
||||||
|
placeholder?: string;
|
||||||
|
onSuggestionsVisibilityChange?: (visible: boolean) => void;
|
||||||
|
}) => {
|
||||||
|
useEffect(() => {
|
||||||
|
onSuggestionsVisibilityChange?.(composerTestControls.suggestionsVisible);
|
||||||
|
}, [onSuggestionsVisibilityChange]);
|
||||||
|
|
||||||
|
return <Text>InputPrompt: {placeholder}</Text>;
|
||||||
|
},
|
||||||
calculatePromptWidths: vi.fn(() => ({
|
calculatePromptWidths: vi.fn(() => ({
|
||||||
inputWidth: 80,
|
inputWidth: 80,
|
||||||
suggestionsWidth: 40,
|
suggestionsWidth: 40,
|
||||||
@@ -100,6 +116,10 @@ vi.mock('./InputPrompt.js', () => ({
|
|||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('../hooks/useAlternateBuffer.js', () => ({
|
||||||
|
useAlternateBuffer: () => composerTestControls.isAlternateBuffer,
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('./Footer.js', () => ({
|
vi.mock('./Footer.js', () => ({
|
||||||
Footer: () => <Text>Footer</Text>,
|
Footer: () => <Text>Footer</Text>,
|
||||||
}));
|
}));
|
||||||
@@ -154,15 +174,19 @@ const createMockUIState = (overrides: Partial<UIState> = {}): UIState =>
|
|||||||
ctrlDPressedOnce: false,
|
ctrlDPressedOnce: false,
|
||||||
showEscapePrompt: false,
|
showEscapePrompt: false,
|
||||||
shortcutsHelpVisible: false,
|
shortcutsHelpVisible: false,
|
||||||
|
cleanUiDetailsVisible: true,
|
||||||
ideContextState: null,
|
ideContextState: null,
|
||||||
geminiMdFileCount: 0,
|
geminiMdFileCount: 0,
|
||||||
renderMarkdown: true,
|
renderMarkdown: true,
|
||||||
filteredConsoleMessages: [],
|
filteredConsoleMessages: [],
|
||||||
history: [],
|
history: [],
|
||||||
sessionStats: {
|
sessionStats: {
|
||||||
|
sessionId: 'test-session',
|
||||||
|
sessionStartTime: new Date(),
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
metrics: {} as any,
|
||||||
lastPromptTokenCount: 0,
|
lastPromptTokenCount: 0,
|
||||||
sessionTokenCount: 0,
|
promptCount: 0,
|
||||||
totalPrompts: 0,
|
|
||||||
},
|
},
|
||||||
branchName: 'main',
|
branchName: 'main',
|
||||||
debugMessage: '',
|
debugMessage: '',
|
||||||
@@ -187,6 +211,9 @@ const createMockUIActions = (): UIActions =>
|
|||||||
handleFinalSubmit: vi.fn(),
|
handleFinalSubmit: vi.fn(),
|
||||||
handleClearScreen: vi.fn(),
|
handleClearScreen: vi.fn(),
|
||||||
setShellModeActive: vi.fn(),
|
setShellModeActive: vi.fn(),
|
||||||
|
setCleanUiDetailsVisible: vi.fn(),
|
||||||
|
toggleCleanUiDetailsVisible: vi.fn(),
|
||||||
|
revealCleanUiDetailsTemporarily: vi.fn(),
|
||||||
onEscapePromptChange: vi.fn(),
|
onEscapePromptChange: vi.fn(),
|
||||||
vimHandleInput: vi.fn(),
|
vimHandleInput: vi.fn(),
|
||||||
setShortcutsHelpVisible: vi.fn(),
|
setShortcutsHelpVisible: vi.fn(),
|
||||||
@@ -233,6 +260,11 @@ const renderComposer = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
describe('Composer', () => {
|
describe('Composer', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
composerTestControls.suggestionsVisible = false;
|
||||||
|
composerTestControls.isAlternateBuffer = false;
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
@@ -342,6 +374,7 @@ describe('Composer', () => {
|
|||||||
const uiState = createMockUIState({
|
const uiState = createMockUIState({
|
||||||
streamingState: StreamingState.Responding,
|
streamingState: StreamingState.Responding,
|
||||||
elapsedTime: 1,
|
elapsedTime: 1,
|
||||||
|
cleanUiDetailsVisible: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { lastFrame } = renderComposer(uiState);
|
const { lastFrame } = renderComposer(uiState);
|
||||||
@@ -514,6 +547,21 @@ describe('Composer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Input and Indicators', () => {
|
describe('Input and Indicators', () => {
|
||||||
|
it('hides non-essential UI details in clean mode', () => {
|
||||||
|
const uiState = createMockUIState({
|
||||||
|
cleanUiDetailsVisible: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { lastFrame } = renderComposer(uiState);
|
||||||
|
|
||||||
|
const output = lastFrame();
|
||||||
|
expect(output).toContain('ShortcutsHint');
|
||||||
|
expect(output).toContain('InputPrompt');
|
||||||
|
expect(output).not.toContain('Footer');
|
||||||
|
expect(output).not.toContain('ApprovalModeIndicator');
|
||||||
|
expect(output).not.toContain('ContextSummaryDisplay');
|
||||||
|
});
|
||||||
|
|
||||||
it('renders InputPrompt when input is active', () => {
|
it('renders InputPrompt when input is active', () => {
|
||||||
const uiState = createMockUIState({
|
const uiState = createMockUIState({
|
||||||
isInputActive: true,
|
isInputActive: true,
|
||||||
@@ -582,6 +630,92 @@ describe('Composer', () => {
|
|||||||
|
|
||||||
expect(lastFrame()).not.toContain('raw markdown mode');
|
expect(lastFrame()).not.toContain('raw markdown mode');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[ApprovalMode.YOLO, 'YOLO'],
|
||||||
|
[ApprovalMode.PLAN, 'plan'],
|
||||||
|
[ApprovalMode.AUTO_EDIT, 'auto edit'],
|
||||||
|
])(
|
||||||
|
'shows minimal mode badge "%s" when clean UI details are hidden',
|
||||||
|
(mode, label) => {
|
||||||
|
const uiState = createMockUIState({
|
||||||
|
cleanUiDetailsVisible: false,
|
||||||
|
showApprovalModeIndicator: mode,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { lastFrame } = renderComposer(uiState);
|
||||||
|
expect(lastFrame()).toContain(label);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it('hides minimal mode badge while loading in clean mode', () => {
|
||||||
|
const uiState = createMockUIState({
|
||||||
|
cleanUiDetailsVisible: false,
|
||||||
|
streamingState: StreamingState.Responding,
|
||||||
|
elapsedTime: 1,
|
||||||
|
showApprovalModeIndicator: ApprovalMode.PLAN,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { lastFrame } = renderComposer(uiState);
|
||||||
|
const output = lastFrame();
|
||||||
|
expect(output).toContain('LoadingIndicator');
|
||||||
|
expect(output).not.toContain('plan');
|
||||||
|
expect(output).not.toContain('ShortcutsHint');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hides minimal mode badge while action-required state is active', () => {
|
||||||
|
const uiState = createMockUIState({
|
||||||
|
cleanUiDetailsVisible: false,
|
||||||
|
showApprovalModeIndicator: ApprovalMode.PLAN,
|
||||||
|
customDialog: (
|
||||||
|
<Box>
|
||||||
|
<Text>Prompt</Text>
|
||||||
|
</Box>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { lastFrame } = renderComposer(uiState);
|
||||||
|
const output = lastFrame();
|
||||||
|
expect(output).not.toContain('plan');
|
||||||
|
expect(output).not.toContain('ShortcutsHint');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows Esc rewind prompt in minimal mode without showing full UI', () => {
|
||||||
|
const uiState = createMockUIState({
|
||||||
|
cleanUiDetailsVisible: false,
|
||||||
|
showEscapePrompt: true,
|
||||||
|
history: [{ id: 1, type: 'user', text: 'msg' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { lastFrame } = renderComposer(uiState);
|
||||||
|
const output = lastFrame();
|
||||||
|
expect(output).toContain('ToastDisplay');
|
||||||
|
expect(output).not.toContain('ContextSummaryDisplay');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows context usage bleed-through when over 60%', () => {
|
||||||
|
const model = 'gemini-2.5-pro';
|
||||||
|
const uiState = createMockUIState({
|
||||||
|
cleanUiDetailsVisible: false,
|
||||||
|
currentModel: model,
|
||||||
|
sessionStats: {
|
||||||
|
sessionId: 'test-session',
|
||||||
|
sessionStartTime: new Date(),
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
metrics: {} as any,
|
||||||
|
lastPromptTokenCount: Math.floor(tokenLimit(model) * 0.7),
|
||||||
|
promptCount: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const settings = createMockSettings({
|
||||||
|
ui: {
|
||||||
|
footer: { hideContextPercentage: false },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { lastFrame } = renderComposer(uiState, settings);
|
||||||
|
expect(lastFrame()).toContain('%');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Error Details Display', () => {
|
describe('Error Details Display', () => {
|
||||||
@@ -680,7 +814,84 @@ describe('Composer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('keeps shortcuts hint visible when no action is required', () => {
|
it('keeps shortcuts hint visible when no action is required', () => {
|
||||||
const uiState = createMockUIState();
|
const uiState = createMockUIState({
|
||||||
|
cleanUiDetailsVisible: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { lastFrame } = renderComposer(uiState);
|
||||||
|
|
||||||
|
expect(lastFrame()).toContain('ShortcutsHint');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows shortcuts hint when full UI details are visible', () => {
|
||||||
|
const uiState = createMockUIState({
|
||||||
|
cleanUiDetailsVisible: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { lastFrame } = renderComposer(uiState);
|
||||||
|
|
||||||
|
expect(lastFrame()).toContain('ShortcutsHint');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hides shortcuts hint while loading in minimal mode', () => {
|
||||||
|
const uiState = createMockUIState({
|
||||||
|
cleanUiDetailsVisible: false,
|
||||||
|
streamingState: StreamingState.Responding,
|
||||||
|
elapsedTime: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { lastFrame } = renderComposer(uiState);
|
||||||
|
|
||||||
|
expect(lastFrame()).not.toContain('ShortcutsHint');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows shortcuts help in minimal mode when toggled on', () => {
|
||||||
|
const uiState = createMockUIState({
|
||||||
|
cleanUiDetailsVisible: false,
|
||||||
|
shortcutsHelpVisible: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { lastFrame } = renderComposer(uiState);
|
||||||
|
|
||||||
|
expect(lastFrame()).toContain('ShortcutsHelp');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hides shortcuts hint when suggestions are visible above input in alternate buffer', () => {
|
||||||
|
composerTestControls.isAlternateBuffer = true;
|
||||||
|
composerTestControls.suggestionsVisible = true;
|
||||||
|
|
||||||
|
const uiState = createMockUIState({
|
||||||
|
cleanUiDetailsVisible: false,
|
||||||
|
showApprovalModeIndicator: ApprovalMode.PLAN,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { lastFrame } = renderComposer(uiState);
|
||||||
|
|
||||||
|
expect(lastFrame()).not.toContain('ShortcutsHint');
|
||||||
|
expect(lastFrame()).not.toContain('plan');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hides approval mode indicator when suggestions are visible above input in alternate buffer', () => {
|
||||||
|
composerTestControls.isAlternateBuffer = true;
|
||||||
|
composerTestControls.suggestionsVisible = true;
|
||||||
|
|
||||||
|
const uiState = createMockUIState({
|
||||||
|
cleanUiDetailsVisible: true,
|
||||||
|
showApprovalModeIndicator: ApprovalMode.YOLO,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { lastFrame } = renderComposer(uiState);
|
||||||
|
|
||||||
|
expect(lastFrame()).not.toContain('ApprovalModeIndicator');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps shortcuts hint when suggestions are visible below input in regular buffer', () => {
|
||||||
|
composerTestControls.isAlternateBuffer = false;
|
||||||
|
composerTestControls.suggestionsVisible = true;
|
||||||
|
|
||||||
|
const uiState = createMockUIState({
|
||||||
|
cleanUiDetailsVisible: false,
|
||||||
|
});
|
||||||
|
|
||||||
const { lastFrame } = renderComposer(uiState);
|
const { lastFrame } = renderComposer(uiState);
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import { Box, useIsScreenReaderEnabled } from 'ink';
|
import { Box, Text, useIsScreenReaderEnabled } from 'ink';
|
||||||
|
import { ApprovalMode, tokenLimit } from '@google/gemini-cli-core';
|
||||||
import { LoadingIndicator } from './LoadingIndicator.js';
|
import { LoadingIndicator } from './LoadingIndicator.js';
|
||||||
import { StatusDisplay } from './StatusDisplay.js';
|
import { StatusDisplay } from './StatusDisplay.js';
|
||||||
import { ToastDisplay, shouldShowToast } from './ToastDisplay.js';
|
import { ToastDisplay, shouldShowToast } from './ToastDisplay.js';
|
||||||
@@ -19,6 +20,7 @@ import { InputPrompt } from './InputPrompt.js';
|
|||||||
import { Footer } from './Footer.js';
|
import { Footer } from './Footer.js';
|
||||||
import { ShowMoreLines } from './ShowMoreLines.js';
|
import { ShowMoreLines } from './ShowMoreLines.js';
|
||||||
import { QueuedMessageDisplay } from './QueuedMessageDisplay.js';
|
import { QueuedMessageDisplay } from './QueuedMessageDisplay.js';
|
||||||
|
import { ContextUsageDisplay } from './ContextUsageDisplay.js';
|
||||||
import { HorizontalLine } from './shared/HorizontalLine.js';
|
import { HorizontalLine } from './shared/HorizontalLine.js';
|
||||||
import { OverflowProvider } from '../contexts/OverflowContext.js';
|
import { OverflowProvider } from '../contexts/OverflowContext.js';
|
||||||
import { isNarrowWidth } from '../utils/isNarrowWidth.js';
|
import { isNarrowWidth } from '../utils/isNarrowWidth.js';
|
||||||
@@ -36,6 +38,7 @@ import {
|
|||||||
import { ConfigInitDisplay } from '../components/ConfigInitDisplay.js';
|
import { ConfigInitDisplay } from '../components/ConfigInitDisplay.js';
|
||||||
import { TodoTray } from './messages/Todo.js';
|
import { TodoTray } from './messages/Todo.js';
|
||||||
import { getInlineThinkingMode } from '../utils/inlineThinkingMode.js';
|
import { getInlineThinkingMode } from '../utils/inlineThinkingMode.js';
|
||||||
|
import { theme } from '../semantic-colors.js';
|
||||||
|
|
||||||
export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
@@ -52,6 +55,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
|||||||
|
|
||||||
const isAlternateBuffer = useAlternateBuffer();
|
const isAlternateBuffer = useAlternateBuffer();
|
||||||
const { showApprovalModeIndicator } = uiState;
|
const { showApprovalModeIndicator } = uiState;
|
||||||
|
const showUiDetails = uiState.cleanUiDetailsVisible;
|
||||||
const suggestionsPosition = isAlternateBuffer ? 'above' : 'below';
|
const suggestionsPosition = isAlternateBuffer ? 'above' : 'below';
|
||||||
const hideContextSummary =
|
const hideContextSummary =
|
||||||
suggestionsVisible && suggestionsPosition === 'above';
|
suggestionsVisible && suggestionsPosition === 'above';
|
||||||
@@ -98,17 +102,60 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
|||||||
uiState.shortcutsHelpVisible &&
|
uiState.shortcutsHelpVisible &&
|
||||||
uiState.streamingState === StreamingState.Idle &&
|
uiState.streamingState === StreamingState.Idle &&
|
||||||
!hasPendingActionRequired;
|
!hasPendingActionRequired;
|
||||||
const showShortcutsHint =
|
|
||||||
settings.merged.ui.showShortcutsHint &&
|
|
||||||
uiState.streamingState === StreamingState.Idle &&
|
|
||||||
!hasPendingActionRequired;
|
|
||||||
const hasToast = shouldShowToast(uiState);
|
const hasToast = shouldShowToast(uiState);
|
||||||
const showLoadingIndicator =
|
const showLoadingIndicator =
|
||||||
(!uiState.embeddedShellFocused || uiState.isBackgroundShellVisible) &&
|
(!uiState.embeddedShellFocused || uiState.isBackgroundShellVisible) &&
|
||||||
uiState.streamingState === StreamingState.Responding &&
|
uiState.streamingState === StreamingState.Responding &&
|
||||||
!hasPendingActionRequired;
|
!hasPendingActionRequired;
|
||||||
const showApprovalIndicator = !uiState.shellModeActive;
|
const hideUiDetailsForSuggestions =
|
||||||
|
suggestionsVisible && suggestionsPosition === 'above';
|
||||||
|
const showApprovalIndicator =
|
||||||
|
!uiState.shellModeActive && !hideUiDetailsForSuggestions;
|
||||||
const showRawMarkdownIndicator = !uiState.renderMarkdown;
|
const showRawMarkdownIndicator = !uiState.renderMarkdown;
|
||||||
|
const modeBleedThrough =
|
||||||
|
showApprovalModeIndicator === ApprovalMode.YOLO
|
||||||
|
? { text: 'YOLO', color: theme.status.error }
|
||||||
|
: showApprovalModeIndicator === ApprovalMode.PLAN
|
||||||
|
? { text: 'plan', color: theme.status.success }
|
||||||
|
: showApprovalModeIndicator === ApprovalMode.AUTO_EDIT
|
||||||
|
? { text: 'auto edit', color: theme.status.warning }
|
||||||
|
: null;
|
||||||
|
const hideMinimalModeHintWhileBusy =
|
||||||
|
!showUiDetails && (showLoadingIndicator || hasPendingActionRequired);
|
||||||
|
const minimalModeBleedThrough = hideMinimalModeHintWhileBusy
|
||||||
|
? null
|
||||||
|
: modeBleedThrough;
|
||||||
|
const hasMinimalStatusBleedThrough = shouldShowToast(uiState);
|
||||||
|
const contextTokenLimit =
|
||||||
|
typeof uiState.currentModel === 'string' && uiState.currentModel.length > 0
|
||||||
|
? tokenLimit(uiState.currentModel)
|
||||||
|
: 0;
|
||||||
|
const showMinimalContextBleedThrough =
|
||||||
|
!settings.merged.ui.footer.hideContextPercentage &&
|
||||||
|
typeof uiState.currentModel === 'string' &&
|
||||||
|
uiState.currentModel.length > 0 &&
|
||||||
|
contextTokenLimit > 0 &&
|
||||||
|
uiState.sessionStats.lastPromptTokenCount / contextTokenLimit > 0.6;
|
||||||
|
const hideShortcutsHintForSuggestions = hideUiDetailsForSuggestions;
|
||||||
|
const showShortcutsHint =
|
||||||
|
settings.merged.ui.showShortcutsHint &&
|
||||||
|
!hideShortcutsHintForSuggestions &&
|
||||||
|
!hideMinimalModeHintWhileBusy &&
|
||||||
|
!hasPendingActionRequired &&
|
||||||
|
(!showUiDetails || !showLoadingIndicator);
|
||||||
|
const showMinimalModeBleedThrough =
|
||||||
|
!hideUiDetailsForSuggestions && Boolean(minimalModeBleedThrough);
|
||||||
|
const showMinimalInlineLoading = !showUiDetails && showLoadingIndicator;
|
||||||
|
const showMinimalBleedThroughRow =
|
||||||
|
!showUiDetails &&
|
||||||
|
(showMinimalModeBleedThrough ||
|
||||||
|
hasMinimalStatusBleedThrough ||
|
||||||
|
showMinimalContextBleedThrough);
|
||||||
|
const showMinimalMetaRow =
|
||||||
|
!showUiDetails &&
|
||||||
|
(showMinimalInlineLoading ||
|
||||||
|
showMinimalBleedThroughRow ||
|
||||||
|
showShortcutsHint);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -125,9 +172,11 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<QueuedMessageDisplay messageQueue={uiState.messageQueue} />
|
{showUiDetails && (
|
||||||
|
<QueuedMessageDisplay messageQueue={uiState.messageQueue} />
|
||||||
|
)}
|
||||||
|
|
||||||
<TodoTray />
|
{showUiDetails && <TodoTray />}
|
||||||
|
|
||||||
<Box marginTop={1} width="100%" flexDirection="column">
|
<Box marginTop={1} width="100%" flexDirection="column">
|
||||||
<Box
|
<Box
|
||||||
@@ -143,7 +192,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
|||||||
alignItems="center"
|
alignItems="center"
|
||||||
flexGrow={1}
|
flexGrow={1}
|
||||||
>
|
>
|
||||||
{showLoadingIndicator && (
|
{showUiDetails && showLoadingIndicator && (
|
||||||
<LoadingIndicator
|
<LoadingIndicator
|
||||||
inline
|
inline
|
||||||
thought={
|
thought={
|
||||||
@@ -170,86 +219,169 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
|||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
alignItems={isNarrow ? 'flex-start' : 'flex-end'}
|
alignItems={isNarrow ? 'flex-start' : 'flex-end'}
|
||||||
>
|
>
|
||||||
{showShortcutsHint && <ShortcutsHint />}
|
{showUiDetails && showShortcutsHint && <ShortcutsHint />}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
{showShortcutsHelp && <ShortcutsHelp />}
|
{showMinimalMetaRow && (
|
||||||
<HorizontalLine />
|
|
||||||
<Box
|
|
||||||
justifyContent={
|
|
||||||
settings.merged.ui.hideContextSummary
|
|
||||||
? 'flex-start'
|
|
||||||
: 'space-between'
|
|
||||||
}
|
|
||||||
width="100%"
|
|
||||||
flexDirection={isNarrow ? 'column' : 'row'}
|
|
||||||
alignItems={isNarrow ? 'flex-start' : 'center'}
|
|
||||||
>
|
|
||||||
<Box
|
<Box
|
||||||
marginLeft={1}
|
justifyContent="space-between"
|
||||||
marginRight={isNarrow ? 0 : 1}
|
width="100%"
|
||||||
flexDirection="row"
|
flexDirection={isNarrow ? 'column' : 'row'}
|
||||||
alignItems="center"
|
alignItems={isNarrow ? 'flex-start' : 'center'}
|
||||||
flexGrow={1}
|
|
||||||
>
|
>
|
||||||
{hasToast ? (
|
<Box
|
||||||
<ToastDisplay />
|
marginLeft={1}
|
||||||
) : (
|
marginRight={isNarrow ? 0 : 1}
|
||||||
!showLoadingIndicator && (
|
flexDirection="row"
|
||||||
|
alignItems={isNarrow ? 'flex-start' : 'center'}
|
||||||
|
flexGrow={1}
|
||||||
|
>
|
||||||
|
{showMinimalInlineLoading && (
|
||||||
|
<LoadingIndicator
|
||||||
|
inline
|
||||||
|
thought={
|
||||||
|
uiState.streamingState ===
|
||||||
|
StreamingState.WaitingForConfirmation ||
|
||||||
|
config.getAccessibility()?.enableLoadingPhrases === false
|
||||||
|
? undefined
|
||||||
|
: uiState.thought
|
||||||
|
}
|
||||||
|
currentLoadingPhrase={
|
||||||
|
config.getAccessibility()?.enableLoadingPhrases === false
|
||||||
|
? undefined
|
||||||
|
: uiState.currentLoadingPhrase
|
||||||
|
}
|
||||||
|
thoughtLabel={
|
||||||
|
inlineThinkingMode === 'full' ? 'Thinking ...' : undefined
|
||||||
|
}
|
||||||
|
elapsedTime={uiState.elapsedTime}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showMinimalModeBleedThrough && minimalModeBleedThrough && (
|
||||||
|
<Text color={minimalModeBleedThrough.color}>
|
||||||
|
● {minimalModeBleedThrough.text}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{hasMinimalStatusBleedThrough && (
|
||||||
<Box
|
<Box
|
||||||
flexDirection={isNarrow ? 'column' : 'row'}
|
marginLeft={
|
||||||
alignItems={isNarrow ? 'flex-start' : 'center'}
|
showMinimalInlineLoading || showMinimalModeBleedThrough
|
||||||
|
? 1
|
||||||
|
: 0
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{showApprovalIndicator && (
|
<ToastDisplay />
|
||||||
<ApprovalModeIndicator
|
|
||||||
approvalMode={showApprovalModeIndicator}
|
|
||||||
isPlanEnabled={config.isPlanEnabled()}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{uiState.shellModeActive && (
|
|
||||||
<Box
|
|
||||||
marginLeft={showApprovalIndicator && !isNarrow ? 1 : 0}
|
|
||||||
marginTop={showApprovalIndicator && isNarrow ? 1 : 0}
|
|
||||||
>
|
|
||||||
<ShellModeIndicator />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
{showRawMarkdownIndicator && (
|
|
||||||
<Box
|
|
||||||
marginLeft={
|
|
||||||
(showApprovalIndicator || uiState.shellModeActive) &&
|
|
||||||
!isNarrow
|
|
||||||
? 1
|
|
||||||
: 0
|
|
||||||
}
|
|
||||||
marginTop={
|
|
||||||
(showApprovalIndicator || uiState.shellModeActive) &&
|
|
||||||
isNarrow
|
|
||||||
? 1
|
|
||||||
: 0
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<RawMarkdownIndicator />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)}
|
||||||
|
</Box>
|
||||||
|
{(showMinimalContextBleedThrough || showShortcutsHint) && (
|
||||||
|
<Box
|
||||||
|
marginTop={isNarrow && showMinimalBleedThroughRow ? 1 : 0}
|
||||||
|
flexDirection={isNarrow ? 'column' : 'row'}
|
||||||
|
alignItems={isNarrow ? 'flex-start' : 'flex-end'}
|
||||||
|
>
|
||||||
|
{showMinimalContextBleedThrough && (
|
||||||
|
<ContextUsageDisplay
|
||||||
|
promptTokenCount={uiState.sessionStats.lastPromptTokenCount}
|
||||||
|
model={uiState.currentModel}
|
||||||
|
terminalWidth={uiState.terminalWidth}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showShortcutsHint && (
|
||||||
|
<Box
|
||||||
|
marginLeft={
|
||||||
|
showMinimalContextBleedThrough && !isNarrow ? 1 : 0
|
||||||
|
}
|
||||||
|
marginTop={
|
||||||
|
showMinimalContextBleedThrough && isNarrow ? 1 : 0
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ShortcutsHint />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
)}
|
||||||
|
{showShortcutsHelp && <ShortcutsHelp />}
|
||||||
|
{showUiDetails && <HorizontalLine />}
|
||||||
|
{showUiDetails && (
|
||||||
<Box
|
<Box
|
||||||
marginTop={isNarrow ? 1 : 0}
|
justifyContent={
|
||||||
flexDirection="column"
|
settings.merged.ui.hideContextSummary
|
||||||
alignItems={isNarrow ? 'flex-start' : 'flex-end'}
|
? 'flex-start'
|
||||||
|
: 'space-between'
|
||||||
|
}
|
||||||
|
width="100%"
|
||||||
|
flexDirection={isNarrow ? 'column' : 'row'}
|
||||||
|
alignItems={isNarrow ? 'flex-start' : 'center'}
|
||||||
>
|
>
|
||||||
{!showLoadingIndicator && (
|
<Box
|
||||||
<StatusDisplay hideContextSummary={hideContextSummary} />
|
marginLeft={1}
|
||||||
)}
|
marginRight={isNarrow ? 0 : 1}
|
||||||
|
flexDirection="row"
|
||||||
|
alignItems="center"
|
||||||
|
flexGrow={1}
|
||||||
|
>
|
||||||
|
{hasToast ? (
|
||||||
|
<ToastDisplay />
|
||||||
|
) : (
|
||||||
|
!showLoadingIndicator && (
|
||||||
|
<Box
|
||||||
|
flexDirection={isNarrow ? 'column' : 'row'}
|
||||||
|
alignItems={isNarrow ? 'flex-start' : 'center'}
|
||||||
|
>
|
||||||
|
{showApprovalIndicator && (
|
||||||
|
<ApprovalModeIndicator
|
||||||
|
approvalMode={showApprovalModeIndicator}
|
||||||
|
isPlanEnabled={config.isPlanEnabled()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{uiState.shellModeActive && (
|
||||||
|
<Box
|
||||||
|
marginLeft={showApprovalIndicator && !isNarrow ? 1 : 0}
|
||||||
|
marginTop={showApprovalIndicator && isNarrow ? 1 : 0}
|
||||||
|
>
|
||||||
|
<ShellModeIndicator />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{showRawMarkdownIndicator && (
|
||||||
|
<Box
|
||||||
|
marginLeft={
|
||||||
|
(showApprovalIndicator || uiState.shellModeActive) &&
|
||||||
|
!isNarrow
|
||||||
|
? 1
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
marginTop={
|
||||||
|
(showApprovalIndicator || uiState.shellModeActive) &&
|
||||||
|
isNarrow
|
||||||
|
? 1
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<RawMarkdownIndicator />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
marginTop={isNarrow ? 1 : 0}
|
||||||
|
flexDirection="column"
|
||||||
|
alignItems={isNarrow ? 'flex-start' : 'flex-end'}
|
||||||
|
>
|
||||||
|
{!showLoadingIndicator && (
|
||||||
|
<StatusDisplay hideContextSummary={hideContextSummary} />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{uiState.showErrorDetails && (
|
{showUiDetails && uiState.showErrorDetails && (
|
||||||
<OverflowProvider>
|
<OverflowProvider>
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
<DetailedMessagesDisplay
|
<DetailedMessagesDisplay
|
||||||
@@ -301,7 +433,9 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!settings.merged.ui.hideFooter && !isScreenReaderEnabled && <Footer />}
|
{showUiDetails &&
|
||||||
|
!settings.merged.ui.hideFooter &&
|
||||||
|
!isScreenReaderEnabled && <Footer />}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -149,8 +149,14 @@ describe('InputPrompt', () => {
|
|||||||
);
|
);
|
||||||
const mockedUseKittyKeyboardProtocol = vi.mocked(useKittyKeyboardProtocol);
|
const mockedUseKittyKeyboardProtocol = vi.mocked(useKittyKeyboardProtocol);
|
||||||
const mockSetEmbeddedShellFocused = vi.fn();
|
const mockSetEmbeddedShellFocused = vi.fn();
|
||||||
|
const mockSetCleanUiDetailsVisible = vi.fn();
|
||||||
|
const mockToggleCleanUiDetailsVisible = vi.fn();
|
||||||
|
const mockRevealCleanUiDetailsTemporarily = vi.fn();
|
||||||
const uiActions = {
|
const uiActions = {
|
||||||
setEmbeddedShellFocused: mockSetEmbeddedShellFocused,
|
setEmbeddedShellFocused: mockSetEmbeddedShellFocused,
|
||||||
|
setCleanUiDetailsVisible: mockSetCleanUiDetailsVisible,
|
||||||
|
toggleCleanUiDetailsVisible: mockToggleCleanUiDetailsVisible,
|
||||||
|
revealCleanUiDetailsTemporarily: mockRevealCleanUiDetailsTemporarily,
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -2945,29 +2951,29 @@ describe('InputPrompt', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Tab focus toggle', () => {
|
describe('Tab clean UI toggle', () => {
|
||||||
it.each([
|
it.each([
|
||||||
{
|
{
|
||||||
name: 'should toggle focus in on Tab when no suggestions or ghost text',
|
name: 'should toggle clean UI details on double-Tab when no suggestions or ghost text',
|
||||||
showSuggestions: false,
|
showSuggestions: false,
|
||||||
ghostText: '',
|
ghostText: '',
|
||||||
suggestions: [],
|
suggestions: [],
|
||||||
expectedFocusToggle: true,
|
expectedUiToggle: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'should accept ghost text and NOT toggle focus on Tab',
|
name: 'should accept ghost text and NOT toggle clean UI details on Tab',
|
||||||
showSuggestions: false,
|
showSuggestions: false,
|
||||||
ghostText: 'ghost text',
|
ghostText: 'ghost text',
|
||||||
suggestions: [],
|
suggestions: [],
|
||||||
expectedFocusToggle: false,
|
expectedUiToggle: false,
|
||||||
expectedAcceptCall: true,
|
expectedAcceptCall: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'should NOT toggle focus on Tab when suggestions are present',
|
name: 'should NOT toggle clean UI details on Tab when suggestions are present',
|
||||||
showSuggestions: true,
|
showSuggestions: true,
|
||||||
ghostText: '',
|
ghostText: '',
|
||||||
suggestions: [{ label: 'test', value: 'test' }],
|
suggestions: [{ label: 'test', value: 'test' }],
|
||||||
expectedFocusToggle: false,
|
expectedUiToggle: false,
|
||||||
},
|
},
|
||||||
])(
|
])(
|
||||||
'$name',
|
'$name',
|
||||||
@@ -2975,7 +2981,7 @@ describe('InputPrompt', () => {
|
|||||||
showSuggestions,
|
showSuggestions,
|
||||||
ghostText,
|
ghostText,
|
||||||
suggestions,
|
suggestions,
|
||||||
expectedFocusToggle,
|
expectedUiToggle,
|
||||||
expectedAcceptCall,
|
expectedAcceptCall,
|
||||||
}) => {
|
}) => {
|
||||||
const mockAccept = vi.fn();
|
const mockAccept = vi.fn();
|
||||||
@@ -2997,21 +3003,24 @@ describe('InputPrompt', () => {
|
|||||||
<InputPrompt {...props} />,
|
<InputPrompt {...props} />,
|
||||||
{
|
{
|
||||||
uiActions,
|
uiActions,
|
||||||
uiState: { activePtyId: 1 },
|
uiState: {},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
stdin.write('\t');
|
stdin.write('\t');
|
||||||
|
if (expectedUiToggle) {
|
||||||
|
stdin.write('\t');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
if (expectedFocusToggle) {
|
if (expectedUiToggle) {
|
||||||
expect(uiActions.setEmbeddedShellFocused).toHaveBeenCalledWith(
|
expect(uiActions.toggleCleanUiDetailsVisible).toHaveBeenCalled();
|
||||||
true,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
expect(uiActions.setEmbeddedShellFocused).not.toHaveBeenCalled();
|
expect(
|
||||||
|
uiActions.toggleCleanUiDetailsVisible,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expectedAcceptCall) {
|
if (expectedAcceptCall) {
|
||||||
@@ -3021,6 +3030,75 @@ describe('InputPrompt', () => {
|
|||||||
unmount();
|
unmount();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
it('should not reveal clean UI details on Shift+Tab when hidden', async () => {
|
||||||
|
mockedUseCommandCompletion.mockReturnValue({
|
||||||
|
...mockCommandCompletion,
|
||||||
|
showSuggestions: false,
|
||||||
|
suggestions: [],
|
||||||
|
promptCompletion: {
|
||||||
|
text: '',
|
||||||
|
accept: vi.fn(),
|
||||||
|
clear: vi.fn(),
|
||||||
|
isLoading: false,
|
||||||
|
isActive: false,
|
||||||
|
markSelected: vi.fn(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { stdin, unmount } = renderWithProviders(
|
||||||
|
<InputPrompt {...props} />,
|
||||||
|
{
|
||||||
|
uiActions,
|
||||||
|
uiState: { activePtyId: 1, cleanUiDetailsVisible: false },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
stdin.write('\x1b[Z');
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
uiActions.revealCleanUiDetailsTemporarily,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should toggle clean UI details on double-Tab by default', async () => {
|
||||||
|
mockedUseCommandCompletion.mockReturnValue({
|
||||||
|
...mockCommandCompletion,
|
||||||
|
showSuggestions: false,
|
||||||
|
suggestions: [],
|
||||||
|
promptCompletion: {
|
||||||
|
text: '',
|
||||||
|
accept: vi.fn(),
|
||||||
|
clear: vi.fn(),
|
||||||
|
isLoading: false,
|
||||||
|
isActive: false,
|
||||||
|
markSelected: vi.fn(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { stdin, unmount } = renderWithProviders(
|
||||||
|
<InputPrompt {...props} />,
|
||||||
|
{
|
||||||
|
uiActions,
|
||||||
|
uiState: {},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
stdin.write('\t');
|
||||||
|
stdin.write('\t');
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(uiActions.toggleCleanUiDetailsVisible).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('mouse interaction', () => {
|
describe('mouse interaction', () => {
|
||||||
|
|||||||
@@ -144,6 +144,8 @@ export function isLargePaste(text: string): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DOUBLE_TAB_CLEAN_UI_TOGGLE_WINDOW_MS = 350;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt to toggle expansion of a paste placeholder in the buffer.
|
* Attempt to toggle expansion of a paste placeholder in the buffer.
|
||||||
* Returns true if a toggle action was performed or hint was shown, false otherwise.
|
* Returns true if a toggle action was performed or hint was shown, false otherwise.
|
||||||
@@ -211,7 +213,11 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
|||||||
const { merged: settings } = useSettings();
|
const { merged: settings } = useSettings();
|
||||||
const kittyProtocol = useKittyKeyboardProtocol();
|
const kittyProtocol = useKittyKeyboardProtocol();
|
||||||
const isShellFocused = useShellFocusState();
|
const isShellFocused = useShellFocusState();
|
||||||
const { setEmbeddedShellFocused, setShortcutsHelpVisible } = useUIActions();
|
const {
|
||||||
|
setEmbeddedShellFocused,
|
||||||
|
setShortcutsHelpVisible,
|
||||||
|
toggleCleanUiDetailsVisible,
|
||||||
|
} = useUIActions();
|
||||||
const {
|
const {
|
||||||
terminalWidth,
|
terminalWidth,
|
||||||
activePtyId,
|
activePtyId,
|
||||||
@@ -223,6 +229,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
|||||||
} = useUIState();
|
} = useUIState();
|
||||||
const [suppressCompletion, setSuppressCompletion] = useState(false);
|
const [suppressCompletion, setSuppressCompletion] = useState(false);
|
||||||
const escPressCount = useRef(0);
|
const escPressCount = useRef(0);
|
||||||
|
const lastPlainTabPressTimeRef = useRef<number | null>(null);
|
||||||
const [showEscapePrompt, setShowEscapePrompt] = useState(false);
|
const [showEscapePrompt, setShowEscapePrompt] = useState(false);
|
||||||
const escapeTimerRef = useRef<NodeJS.Timeout | null>(null);
|
const escapeTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const [recentUnsafePasteTime, setRecentUnsafePasteTime] = useState<
|
const [recentUnsafePasteTime, setRecentUnsafePasteTime] = useState<
|
||||||
@@ -624,6 +631,33 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isPlainTab =
|
||||||
|
key.name === 'tab' && !key.shift && !key.alt && !key.ctrl && !key.cmd;
|
||||||
|
const hasTabCompletionInteraction =
|
||||||
|
completion.showSuggestions ||
|
||||||
|
Boolean(completion.promptCompletion.text) ||
|
||||||
|
reverseSearchActive ||
|
||||||
|
commandSearchActive;
|
||||||
|
if (isPlainTab) {
|
||||||
|
if (!hasTabCompletionInteraction) {
|
||||||
|
const now = Date.now();
|
||||||
|
const isDoubleTabPress =
|
||||||
|
lastPlainTabPressTimeRef.current !== null &&
|
||||||
|
now - lastPlainTabPressTimeRef.current <=
|
||||||
|
DOUBLE_TAB_CLEAN_UI_TOGGLE_WINDOW_MS;
|
||||||
|
if (isDoubleTabPress) {
|
||||||
|
lastPlainTabPressTimeRef.current = null;
|
||||||
|
toggleCleanUiDetailsVisible();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
lastPlainTabPressTimeRef.current = now;
|
||||||
|
} else {
|
||||||
|
lastPlainTabPressTimeRef.current = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lastPlainTabPressTimeRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (key.name === 'paste') {
|
if (key.name === 'paste') {
|
||||||
if (shortcutsHelpVisible) {
|
if (shortcutsHelpVisible) {
|
||||||
setShortcutsHelpVisible(false);
|
setShortcutsHelpVisible(false);
|
||||||
@@ -1172,6 +1206,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
|||||||
kittyProtocol.enabled,
|
kittyProtocol.enabled,
|
||||||
shortcutsHelpVisible,
|
shortcutsHelpVisible,
|
||||||
setShortcutsHelpVisible,
|
setShortcutsHelpVisible,
|
||||||
|
toggleCleanUiDetailsVisible,
|
||||||
tryLoadQueuedMessages,
|
tryLoadQueuedMessages,
|
||||||
setBannerVisible,
|
setBannerVisible,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
|||||||
@@ -9,11 +9,15 @@ import { waitFor } from '../../test-utils/async.js';
|
|||||||
import { MainContent } from './MainContent.js';
|
import { MainContent } from './MainContent.js';
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { Box, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
import type React from 'react';
|
import { act, useState, type JSX } from 'react';
|
||||||
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
|
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
|
||||||
import { ToolCallStatus } from '../types.js';
|
import { ToolCallStatus } from '../types.js';
|
||||||
import { SHELL_COMMAND_NAME } from '../constants.js';
|
import { SHELL_COMMAND_NAME } from '../constants.js';
|
||||||
import type { UIState } from '../contexts/UIStateContext.js';
|
import {
|
||||||
|
UIStateContext,
|
||||||
|
useUIState,
|
||||||
|
type UIState,
|
||||||
|
} from '../contexts/UIStateContext.js';
|
||||||
|
|
||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
vi.mock('../contexts/SettingsContext.js', async () => {
|
vi.mock('../contexts/SettingsContext.js', async () => {
|
||||||
@@ -45,7 +49,9 @@ vi.mock('../hooks/useAlternateBuffer.js', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./AppHeader.js', () => ({
|
vi.mock('./AppHeader.js', () => ({
|
||||||
AppHeader: () => <Text>AppHeader</Text>,
|
AppHeader: ({ showDetails = true }: { showDetails?: boolean }) => (
|
||||||
|
<Text>{showDetails ? 'AppHeader(full)' : 'AppHeader(minimal)'}</Text>
|
||||||
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./ShowMoreLines.js', () => ({
|
vi.mock('./ShowMoreLines.js', () => ({
|
||||||
@@ -58,7 +64,7 @@ vi.mock('./shared/ScrollableList.js', () => ({
|
|||||||
renderItem,
|
renderItem,
|
||||||
}: {
|
}: {
|
||||||
data: unknown[];
|
data: unknown[];
|
||||||
renderItem: (props: { item: unknown }) => React.JSX.Element;
|
renderItem: (props: { item: unknown }) => JSX.Element;
|
||||||
}) => (
|
}) => (
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
<Text>ScrollableList</Text>
|
<Text>ScrollableList</Text>
|
||||||
@@ -87,6 +93,7 @@ describe('MainContent', () => {
|
|||||||
activePtyId: undefined,
|
activePtyId: undefined,
|
||||||
embeddedShellFocused: false,
|
embeddedShellFocused: false,
|
||||||
historyRemountKey: 0,
|
historyRemountKey: 0,
|
||||||
|
cleanUiDetailsVisible: true,
|
||||||
bannerData: { defaultText: '', warningText: '' },
|
bannerData: { defaultText: '', warningText: '' },
|
||||||
bannerVisible: false,
|
bannerVisible: false,
|
||||||
copyModeEnabled: false,
|
copyModeEnabled: false,
|
||||||
@@ -101,7 +108,7 @@ describe('MainContent', () => {
|
|||||||
const { lastFrame } = renderWithProviders(<MainContent />, {
|
const { lastFrame } = renderWithProviders(<MainContent />, {
|
||||||
uiState: defaultMockUiState as Partial<UIState>,
|
uiState: defaultMockUiState as Partial<UIState>,
|
||||||
});
|
});
|
||||||
await waitFor(() => expect(lastFrame()).toContain('AppHeader'));
|
await waitFor(() => expect(lastFrame()).toContain('AppHeader(full)'));
|
||||||
const output = lastFrame();
|
const output = lastFrame();
|
||||||
|
|
||||||
expect(output).toContain('Hello');
|
expect(output).toContain('Hello');
|
||||||
@@ -116,11 +123,81 @@ describe('MainContent', () => {
|
|||||||
await waitFor(() => expect(lastFrame()).toContain('ScrollableList'));
|
await waitFor(() => expect(lastFrame()).toContain('ScrollableList'));
|
||||||
const output = lastFrame();
|
const output = lastFrame();
|
||||||
|
|
||||||
expect(output).toContain('AppHeader');
|
expect(output).toContain('AppHeader(full)');
|
||||||
expect(output).toContain('Hello');
|
expect(output).toContain('Hello');
|
||||||
expect(output).toContain('Hi there');
|
expect(output).toContain('Hi there');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders minimal header in minimal mode (alternate buffer)', async () => {
|
||||||
|
vi.mocked(useAlternateBuffer).mockReturnValue(true);
|
||||||
|
|
||||||
|
const { lastFrame } = renderWithProviders(<MainContent />, {
|
||||||
|
uiState: {
|
||||||
|
...defaultMockUiState,
|
||||||
|
cleanUiDetailsVisible: false,
|
||||||
|
} as Partial<UIState>,
|
||||||
|
});
|
||||||
|
await waitFor(() => expect(lastFrame()).toContain('Hello'));
|
||||||
|
const output = lastFrame();
|
||||||
|
|
||||||
|
expect(output).toContain('AppHeader(minimal)');
|
||||||
|
expect(output).not.toContain('AppHeader(full)');
|
||||||
|
expect(output).toContain('Hello');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('restores full header details after toggle in alternate buffer mode', async () => {
|
||||||
|
vi.mocked(useAlternateBuffer).mockReturnValue(true);
|
||||||
|
|
||||||
|
let setShowDetails: ((visible: boolean) => void) | undefined;
|
||||||
|
const ToggleHarness = () => {
|
||||||
|
const outerState = useUIState();
|
||||||
|
const [showDetails, setShowDetailsState] = useState(
|
||||||
|
outerState.cleanUiDetailsVisible,
|
||||||
|
);
|
||||||
|
setShowDetails = setShowDetailsState;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UIStateContext.Provider
|
||||||
|
value={{ ...outerState, cleanUiDetailsVisible: showDetails }}
|
||||||
|
>
|
||||||
|
<MainContent />
|
||||||
|
</UIStateContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { lastFrame } = renderWithProviders(<ToggleHarness />, {
|
||||||
|
uiState: {
|
||||||
|
...defaultMockUiState,
|
||||||
|
cleanUiDetailsVisible: false,
|
||||||
|
} as Partial<UIState>,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => expect(lastFrame()).toContain('AppHeader(minimal)'));
|
||||||
|
if (!setShowDetails) {
|
||||||
|
throw new Error('setShowDetails was not initialized');
|
||||||
|
}
|
||||||
|
const setShowDetailsSafe = setShowDetails;
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
setShowDetailsSafe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => expect(lastFrame()).toContain('AppHeader(full)'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('always renders full header details in normal buffer mode', async () => {
|
||||||
|
vi.mocked(useAlternateBuffer).mockReturnValue(false);
|
||||||
|
const { lastFrame } = renderWithProviders(<MainContent />, {
|
||||||
|
uiState: {
|
||||||
|
...defaultMockUiState,
|
||||||
|
cleanUiDetailsVisible: false,
|
||||||
|
} as Partial<UIState>,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => expect(lastFrame()).toContain('AppHeader(full)'));
|
||||||
|
expect(lastFrame()).not.toContain('AppHeader(minimal)');
|
||||||
|
});
|
||||||
|
|
||||||
it('does not constrain height in alternate buffer mode', async () => {
|
it('does not constrain height in alternate buffer mode', async () => {
|
||||||
vi.mocked(useAlternateBuffer).mockReturnValue(true);
|
vi.mocked(useAlternateBuffer).mockReturnValue(true);
|
||||||
const { lastFrame } = renderWithProviders(<MainContent />, {
|
const { lastFrame } = renderWithProviders(<MainContent />, {
|
||||||
@@ -129,7 +206,9 @@ describe('MainContent', () => {
|
|||||||
await waitFor(() => expect(lastFrame()).toContain('Hello'));
|
await waitFor(() => expect(lastFrame()).toContain('Hello'));
|
||||||
const output = lastFrame();
|
const output = lastFrame();
|
||||||
|
|
||||||
expect(output).toMatchSnapshot();
|
expect(output).toContain('AppHeader(full)');
|
||||||
|
expect(output).toContain('Hello');
|
||||||
|
expect(output).toContain('Hi there');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('MainContent Tool Output Height Logic', () => {
|
describe('MainContent Tool Output Height Logic', () => {
|
||||||
@@ -210,6 +289,7 @@ describe('MainContent', () => {
|
|||||||
isEditorDialogOpen: false,
|
isEditorDialogOpen: false,
|
||||||
slashCommands: [],
|
slashCommands: [],
|
||||||
historyRemountKey: 0,
|
historyRemountKey: 0,
|
||||||
|
cleanUiDetailsVisible: true,
|
||||||
bannerData: {
|
bannerData: {
|
||||||
defaultText: '',
|
defaultText: '',
|
||||||
warningText: '',
|
warningText: '',
|
||||||
|
|||||||
@@ -48,7 +48,9 @@ export const MainContent = () => {
|
|||||||
mainAreaWidth,
|
mainAreaWidth,
|
||||||
staticAreaMaxItemHeight,
|
staticAreaMaxItemHeight,
|
||||||
availableTerminalHeight,
|
availableTerminalHeight,
|
||||||
|
cleanUiDetailsVisible,
|
||||||
} = uiState;
|
} = uiState;
|
||||||
|
const showHeaderDetails = cleanUiDetailsVisible;
|
||||||
|
|
||||||
const historyItems = useMemo(
|
const historyItems = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -120,7 +122,13 @@ export const MainContent = () => {
|
|||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
({ item }: { item: (typeof virtualizedData)[number] }) => {
|
({ item }: { item: (typeof virtualizedData)[number] }) => {
|
||||||
if (item.type === 'header') {
|
if (item.type === 'header') {
|
||||||
return <MemoizedAppHeader key="app-header" version={version} />;
|
return (
|
||||||
|
<MemoizedAppHeader
|
||||||
|
key="app-header"
|
||||||
|
version={version}
|
||||||
|
showDetails={showHeaderDetails}
|
||||||
|
/>
|
||||||
|
);
|
||||||
} else if (item.type === 'history') {
|
} else if (item.type === 'history') {
|
||||||
return (
|
return (
|
||||||
<MemoizedHistoryItemDisplay
|
<MemoizedHistoryItemDisplay
|
||||||
@@ -137,7 +145,13 @@ export const MainContent = () => {
|
|||||||
return pendingItems;
|
return pendingItems;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[version, mainAreaWidth, uiState.slashCommands, pendingItems],
|
[
|
||||||
|
showHeaderDetails,
|
||||||
|
version,
|
||||||
|
mainAreaWidth,
|
||||||
|
uiState.slashCommands,
|
||||||
|
pendingItems,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isAlternateBuffer) {
|
if (isAlternateBuffer) {
|
||||||
|
|||||||
@@ -46,4 +46,10 @@ describe('ShortcutsHelp', () => {
|
|||||||
expect(lastFrame()).toMatchSnapshot();
|
expect(lastFrame()).toMatchSnapshot();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
it('always shows Tab Tab focus UI shortcut', () => {
|
||||||
|
const rendered = renderWithProviders(<ShortcutsHelp />);
|
||||||
|
expect(rendered.lastFrame()).toContain('Tab Tab');
|
||||||
|
rendered.unmount();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,13 +22,14 @@ const buildShortcutItems = (): ShortcutItem[] => {
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
{ key: '!', description: 'shell mode' },
|
{ key: '!', description: 'shell mode' },
|
||||||
|
{ key: '@', description: 'select file or folder' },
|
||||||
|
{ key: 'Esc Esc', description: 'clear & rewind' },
|
||||||
|
{ key: 'Tab Tab', description: 'focus UI' },
|
||||||
|
{ key: 'Ctrl+Y', description: 'YOLO mode' },
|
||||||
{ key: 'Shift+Tab', description: 'cycle mode' },
|
{ key: 'Shift+Tab', description: 'cycle mode' },
|
||||||
{ key: 'Ctrl+V', description: 'paste images' },
|
{ key: 'Ctrl+V', description: 'paste images' },
|
||||||
{ key: '@', description: 'select file or folder' },
|
|
||||||
{ key: 'Ctrl+Y', description: 'YOLO mode' },
|
|
||||||
{ key: 'Ctrl+R', description: 'reverse-search history' },
|
|
||||||
{ key: 'Esc Esc', description: 'clear prompt / rewind' },
|
|
||||||
{ key: `${altLabel}+M`, description: 'raw markdown mode' },
|
{ key: `${altLabel}+M`, description: 'raw markdown mode' },
|
||||||
|
{ key: 'Ctrl+R', description: 'reverse-search history' },
|
||||||
{ key: 'Ctrl+X', description: 'open external editor' },
|
{ key: 'Ctrl+X', description: 'open external editor' },
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
@@ -46,15 +47,29 @@ const Shortcut: React.FC<{ item: ShortcutItem }> = ({ item }) => (
|
|||||||
|
|
||||||
export const ShortcutsHelp: React.FC = () => {
|
export const ShortcutsHelp: React.FC = () => {
|
||||||
const { terminalWidth } = useUIState();
|
const { terminalWidth } = useUIState();
|
||||||
const items = buildShortcutItems();
|
|
||||||
|
|
||||||
const isNarrow = isNarrowWidth(terminalWidth);
|
const isNarrow = isNarrowWidth(terminalWidth);
|
||||||
|
const items = buildShortcutItems();
|
||||||
|
const itemsForDisplay = isNarrow
|
||||||
|
? items
|
||||||
|
: [
|
||||||
|
// Keep first column stable: !, @, Esc Esc, Tab Tab.
|
||||||
|
items[0],
|
||||||
|
items[5],
|
||||||
|
items[6],
|
||||||
|
items[1],
|
||||||
|
items[4],
|
||||||
|
items[7],
|
||||||
|
items[2],
|
||||||
|
items[8],
|
||||||
|
items[9],
|
||||||
|
items[3],
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" width="100%">
|
<Box flexDirection="column" width="100%">
|
||||||
<SectionHeader title="Shortcuts (for more, see /help)" />
|
<SectionHeader title="Shortcuts (for more, see /help)" />
|
||||||
<Box flexDirection="row" flexWrap="wrap" paddingLeft={1} paddingRight={2}>
|
<Box flexDirection="row" flexWrap="wrap" paddingLeft={1} paddingRight={2}>
|
||||||
{items.map((item, index) => (
|
{itemsForDisplay.map((item, index) => (
|
||||||
<Box
|
<Box
|
||||||
key={`${item.key}-${index}`}
|
key={`${item.key}-${index}`}
|
||||||
width={isNarrow ? '100%' : '33%'}
|
width={isNarrow ? '100%' : '33%'}
|
||||||
|
|||||||
@@ -10,7 +10,12 @@ import { theme } from '../semantic-colors.js';
|
|||||||
import { useUIState } from '../contexts/UIStateContext.js';
|
import { useUIState } from '../contexts/UIStateContext.js';
|
||||||
|
|
||||||
export const ShortcutsHint: React.FC = () => {
|
export const ShortcutsHint: React.FC = () => {
|
||||||
const { shortcutsHelpVisible } = useUIState();
|
const { cleanUiDetailsVisible, shortcutsHelpVisible } = useUIState();
|
||||||
|
|
||||||
|
if (!cleanUiDetailsVisible) {
|
||||||
|
return <Text color={theme.text.secondary}> press tab twice for more </Text>;
|
||||||
|
}
|
||||||
|
|
||||||
const highlightColor = shortcutsHelpVisible
|
const highlightColor = shortcutsHelpVisible
|
||||||
? theme.text.accent
|
? theme.text.accent
|
||||||
: theme.text.secondary;
|
: theme.text.secondary;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
exports[`MainContent > MainContent Tool Output Height Logic > 'ASB mode - Focused shell should expand' 1`] = `
|
exports[`MainContent > MainContent Tool Output Height Logic > 'ASB mode - Focused shell should expand' 1`] = `
|
||||||
"ScrollableList
|
"ScrollableList
|
||||||
AppHeader
|
AppHeader(full)
|
||||||
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
|
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ⊷ Shell Command Running a long command... │
|
│ ⊷ Shell Command Running a long command... │
|
||||||
│ │
|
│ │
|
||||||
@@ -33,7 +33,7 @@ ShowMoreLines"
|
|||||||
|
|
||||||
exports[`MainContent > MainContent Tool Output Height Logic > 'ASB mode - Unfocused shell' 1`] = `
|
exports[`MainContent > MainContent Tool Output Height Logic > 'ASB mode - Unfocused shell' 1`] = `
|
||||||
"ScrollableList
|
"ScrollableList
|
||||||
AppHeader
|
AppHeader(full)
|
||||||
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
|
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ⊷ Shell Command Running a long command... │
|
│ ⊷ Shell Command Running a long command... │
|
||||||
│ │
|
│ │
|
||||||
@@ -57,7 +57,7 @@ ShowMoreLines"
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`MainContent > MainContent Tool Output Height Logic > 'Normal mode - Constrained height' 1`] = `
|
exports[`MainContent > MainContent Tool Output Height Logic > 'Normal mode - Constrained height' 1`] = `
|
||||||
"AppHeader
|
"AppHeader(full)
|
||||||
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
|
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ⊷ Shell Command Running a long command... │
|
│ ⊷ Shell Command Running a long command... │
|
||||||
│ │
|
│ │
|
||||||
@@ -81,7 +81,7 @@ ShowMoreLines"
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`MainContent > MainContent Tool Output Height Logic > 'Normal mode - Unconstrained height' 1`] = `
|
exports[`MainContent > MainContent Tool Output Height Logic > 'Normal mode - Unconstrained height' 1`] = `
|
||||||
"AppHeader
|
"AppHeader(full)
|
||||||
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
|
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ⊷ Shell Command Running a long command... │
|
│ ⊷ Shell Command Running a long command... │
|
||||||
│ │
|
│ │
|
||||||
@@ -103,14 +103,3 @@ exports[`MainContent > MainContent Tool Output Height Logic > 'Normal mode - Unc
|
|||||||
╰──────────────────────────────────────────────────────────────────────────────────────────────╯
|
╰──────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||||
ShowMoreLines"
|
ShowMoreLines"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`MainContent > does not constrain height in alternate buffer mode 1`] = `
|
|
||||||
"ScrollableList
|
|
||||||
AppHeader
|
|
||||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
|
||||||
> Hello
|
|
||||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
|
||||||
✦ Hi there
|
|
||||||
ShowMoreLines
|
|
||||||
"
|
|
||||||
`;
|
|
||||||
|
|||||||
@@ -3,39 +3,43 @@
|
|||||||
exports[`ShortcutsHelp > renders correctly in 'narrow' mode on 'linux' 1`] = `
|
exports[`ShortcutsHelp > renders correctly in 'narrow' mode on 'linux' 1`] = `
|
||||||
"── Shortcuts (for more, see /help) ─────
|
"── Shortcuts (for more, see /help) ─────
|
||||||
! shell mode
|
! shell mode
|
||||||
|
@ select file or folder
|
||||||
|
Esc Esc clear & rewind
|
||||||
|
Tab Tab focus UI
|
||||||
|
Ctrl+Y YOLO mode
|
||||||
Shift+Tab cycle mode
|
Shift+Tab cycle mode
|
||||||
Ctrl+V paste images
|
Ctrl+V paste images
|
||||||
@ select file or folder
|
|
||||||
Ctrl+Y YOLO mode
|
|
||||||
Ctrl+R reverse-search history
|
|
||||||
Esc Esc clear prompt / rewind
|
|
||||||
Alt+M raw markdown mode
|
Alt+M raw markdown mode
|
||||||
|
Ctrl+R reverse-search history
|
||||||
Ctrl+X open external editor"
|
Ctrl+X open external editor"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`ShortcutsHelp > renders correctly in 'narrow' mode on 'mac' 1`] = `
|
exports[`ShortcutsHelp > renders correctly in 'narrow' mode on 'mac' 1`] = `
|
||||||
"── Shortcuts (for more, see /help) ─────
|
"── Shortcuts (for more, see /help) ─────
|
||||||
! shell mode
|
! shell mode
|
||||||
|
@ select file or folder
|
||||||
|
Esc Esc clear & rewind
|
||||||
|
Tab Tab focus UI
|
||||||
|
Ctrl+Y YOLO mode
|
||||||
Shift+Tab cycle mode
|
Shift+Tab cycle mode
|
||||||
Ctrl+V paste images
|
Ctrl+V paste images
|
||||||
@ select file or folder
|
|
||||||
Ctrl+Y YOLO mode
|
|
||||||
Ctrl+R reverse-search history
|
|
||||||
Esc Esc clear prompt / rewind
|
|
||||||
Option+M raw markdown mode
|
Option+M raw markdown mode
|
||||||
|
Ctrl+R reverse-search history
|
||||||
Ctrl+X open external editor"
|
Ctrl+X open external editor"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`ShortcutsHelp > renders correctly in 'wide' mode on 'linux' 1`] = `
|
exports[`ShortcutsHelp > renders correctly in 'wide' mode on 'linux' 1`] = `
|
||||||
"── Shortcuts (for more, see /help) ─────────────────────────────────────────────────────────────────
|
"── Shortcuts (for more, see /help) ─────────────────────────────────────────────────────────────────
|
||||||
! shell mode Shift+Tab cycle mode Ctrl+V paste images
|
! shell mode Shift+Tab cycle mode Ctrl+V paste images
|
||||||
@ select file or folder Ctrl+Y YOLO mode Ctrl+R reverse-search history
|
@ select file or folder Ctrl+Y YOLO mode Alt+M raw markdown mode
|
||||||
Esc Esc clear prompt / rewind Alt+M raw markdown mode Ctrl+X open external editor"
|
Esc Esc clear & rewind Ctrl+R reverse-search history Ctrl+X open external editor
|
||||||
|
Tab Tab focus UI"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`ShortcutsHelp > renders correctly in 'wide' mode on 'mac' 1`] = `
|
exports[`ShortcutsHelp > renders correctly in 'wide' mode on 'mac' 1`] = `
|
||||||
"── Shortcuts (for more, see /help) ─────────────────────────────────────────────────────────────────
|
"── Shortcuts (for more, see /help) ─────────────────────────────────────────────────────────────────
|
||||||
! shell mode Shift+Tab cycle mode Ctrl+V paste images
|
! shell mode Shift+Tab cycle mode Ctrl+V paste images
|
||||||
@ select file or folder Ctrl+Y YOLO mode Ctrl+R reverse-search history
|
@ select file or folder Ctrl+Y YOLO mode Option+M raw markdown mode
|
||||||
Esc Esc clear prompt / rewind Option+M raw markdown mode Ctrl+X open external editor"
|
Esc Esc clear & rewind Ctrl+R reverse-search history Ctrl+X open external editor
|
||||||
|
Tab Tab focus UI"
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -68,6 +68,10 @@ export interface UIActions {
|
|||||||
handleApiKeyCancel: () => void;
|
handleApiKeyCancel: () => void;
|
||||||
setBannerVisible: (visible: boolean) => void;
|
setBannerVisible: (visible: boolean) => void;
|
||||||
setShortcutsHelpVisible: (visible: boolean) => void;
|
setShortcutsHelpVisible: (visible: boolean) => void;
|
||||||
|
setCleanUiDetailsVisible: (visible: boolean) => void;
|
||||||
|
toggleCleanUiDetailsVisible: () => void;
|
||||||
|
revealCleanUiDetailsTemporarily: (durationMs?: number) => void;
|
||||||
|
handleWarning: (message: string) => void;
|
||||||
setEmbeddedShellFocused: (value: boolean) => void;
|
setEmbeddedShellFocused: (value: boolean) => void;
|
||||||
dismissBackgroundShell: (pid: number) => void;
|
dismissBackgroundShell: (pid: number) => void;
|
||||||
setActiveBackgroundShellPid: (pid: number) => void;
|
setActiveBackgroundShellPid: (pid: number) => void;
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ export interface UIState {
|
|||||||
ctrlDPressedOnce: boolean;
|
ctrlDPressedOnce: boolean;
|
||||||
showEscapePrompt: boolean;
|
showEscapePrompt: boolean;
|
||||||
shortcutsHelpVisible: boolean;
|
shortcutsHelpVisible: boolean;
|
||||||
|
cleanUiDetailsVisible: boolean;
|
||||||
elapsedTime: number;
|
elapsedTime: number;
|
||||||
currentLoadingPhrase: string | undefined;
|
currentLoadingPhrase: string | undefined;
|
||||||
historyRemountKey: number;
|
historyRemountKey: number;
|
||||||
|
|||||||
@@ -370,7 +370,7 @@ describe('keyMatchers', () => {
|
|||||||
{
|
{
|
||||||
command: Command.FOCUS_SHELL_INPUT,
|
command: Command.FOCUS_SHELL_INPUT,
|
||||||
positive: [createKey('tab')],
|
positive: [createKey('tab')],
|
||||||
negative: [createKey('f', { ctrl: true }), createKey('f')],
|
negative: [createKey('f6'), createKey('f', { ctrl: true })],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: Command.TOGGLE_YOLO,
|
command: Command.TOGGLE_YOLO,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ interface PersistentStateData {
|
|||||||
defaultBannerShownCount?: Record<string, number>;
|
defaultBannerShownCount?: Record<string, number>;
|
||||||
tipsShown?: number;
|
tipsShown?: number;
|
||||||
hasSeenScreenReaderNudge?: boolean;
|
hasSeenScreenReaderNudge?: boolean;
|
||||||
|
focusUiEnabled?: boolean;
|
||||||
// Add other persistent state keys here as needed
|
// Add other persistent state keys here as needed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user