Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
eda9eab
feat(cli): add clean UI minimal mode toggle prototype
LyalinDotCom Feb 9, 2026
866b291
fix(cli): resolve clean-ui rebase fallout on latest main
LyalinDotCom Feb 10, 2026
86d9a72
feat(cli): add double-tab clean UI toggle and restore tab focus
LyalinDotCom Feb 10, 2026
6df688f
Merge branch 'main' into explore-cleanui
LyalinDotCom Feb 10, 2026
915d67f
fix(cli): keep header at top in regular buffer mode
LyalinDotCom Feb 10, 2026
bd5b0a2
Merge branch 'main' into explore-cleanui
LyalinDotCom Feb 10, 2026
b1374dd
fix(cli): hide minimal hints while busy and show inline loading
LyalinDotCom Feb 10, 2026
8250933
Merge branch 'main' into explore-cleanui
LyalinDotCom Feb 10, 2026
afef6b0
Merge branch 'main' into explore-cleanui
LyalinDotCom Feb 10, 2026
920db77
fix(cli): hide tab hint when suggestions render above input
LyalinDotCom Feb 10, 2026
c223541
Merge branch 'main' into explore-cleanui
LyalinDotCom Feb 10, 2026
21691f4
fix(cli): hide mode labels when slash menu is above input
LyalinDotCom Feb 10, 2026
9135e36
fix(cli): prioritize key shortcuts in help layout
LyalinDotCom Feb 10, 2026
4024c37
fix(cli): force regular buffer remount after static refresh
LyalinDotCom Feb 10, 2026
dacd4ea
fix(cli): stabilize clear redraw and polish shortcuts wording
LyalinDotCom Feb 10, 2026
b200fde
Merge remote-tracking branch 'origin/main' into explore-cleanui-rebase
LyalinDotCom Feb 10, 2026
707ad75
Merge branch 'main' into explore-cleanui
LyalinDotCom Feb 10, 2026
176f362
feat(cli): add colored mode dot in minimal badges
LyalinDotCom Feb 10, 2026
2f640de
Fix MainContent snapshot for alternate buffer separators
LyalinDotCom Feb 10, 2026
f567586
Merge branch 'main' into explore-cleanui
LyalinDotCom Feb 10, 2026
eccdb60
Merge branch 'main' into explore-cleanui
LyalinDotCom Feb 10, 2026
cb53692
Add Focus UI preview setting and gate clean UI behavior
LyalinDotCom Feb 10, 2026
94074cf
Merge main into explore-cleanui and resolve clean UI conflicts
LyalinDotCom Feb 12, 2026
c72fc61
Merge branch 'main' into explore-cleanui
LyalinDotCom Feb 12, 2026
837e8fa
Merge branch 'main' into explore-cleanui
LyalinDotCom Feb 12, 2026
c4fb4a6
Fix regular-buffer header redraw behavior
LyalinDotCom Feb 12, 2026
c0c0aa8
Merge branch 'main' into explore-cleanui
LyalinDotCom Feb 12, 2026
5189948
Fix CI settings sync checks and MainContent tests
LyalinDotCom Feb 12, 2026
16b47c2
Merge branch 'main' into explore-cleanui
LyalinDotCom Feb 12, 2026
9d31595
Merge branch 'main' into explore-cleanui
LyalinDotCom Feb 12, 2026
fe5c6aa
feat(cli): remove focus UI setting and persist tab-tab mode
LyalinDotCom Feb 12, 2026
7358e3f
Merge branch 'main' into explore-cleanui
LyalinDotCom Feb 12, 2026
2764d4d
docs(cli): resolve keyboard shortcuts merge text
LyalinDotCom Feb 12, 2026
3012e7f
Merge origin/main into explore-cleanui
LyalinDotCom Feb 12, 2026
8ff00b3
Merge branch 'main' into explore-cleanui
LyalinDotCom Feb 12, 2026
0f80fe6
fix(cli): remove duplicate handleWarning declaration
LyalinDotCom Feb 12, 2026
a4c0159
Merge remote-tracking branch 'origin/main' into explore-cleanui-rebase
LyalinDotCom Feb 12, 2026
de3ffc0
docs(cli): clarify Focus UI default and single-tab behavior
LyalinDotCom Feb 12, 2026
49c09a3
Merge branch 'main' into explore-cleanui
LyalinDotCom Feb 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/cli/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ Slash commands provide meta-level control over the CLI itself.
- **`/shortcuts`**
- **Description:** Toggle the shortcuts panel above the input.
- **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`**
- **Description:** Manage hooks, which allow you to intercept and customize
Expand Down
9 changes: 7 additions & 2 deletions docs/cli/keyboard-shortcuts.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ available combinations.
| Dismiss background shell list. | `Esc` |
| Move focus from background shell to Gemini. | `Shift + Tab` |
| 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 unfocus shell input via Tab. | `Tab (no Shift)` |
| Show warning when trying to move focus away from background shell. | `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 the shell back to Gemini. | `Shift + Tab` |
| Clear the terminal screen and redraw the UI. | `Ctrl + L` |
Expand All @@ -134,6 +134,11 @@ available combinations.
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
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
mode.
- `Esc` pressed twice quickly: Clear the input prompt if it is not empty,
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/config/keyBindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,9 +516,9 @@ export const commandDescriptions: Readonly<Record<Command, string>> = {
[Command.UNFOCUS_BACKGROUND_SHELL_LIST]:
'Move focus from background shell list to Gemini.',
[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]:
'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.UNFOCUS_SHELL_INPUT]: 'Move focus from the shell back to Gemini.',
[Command.CLEAR_SCREEN]: 'Clear the terminal screen and redraw the UI.',
Expand Down
5 changes: 5 additions & 0 deletions packages/cli/src/test-utils/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ const baseMockUiState = {
terminalWidth: 120,
terminalHeight: 40,
currentModel: 'gemini-pro',
cleanUiDetailsVisible: false,
terminalBackgroundColor: undefined,
activePtyId: undefined,
backgroundShells: new Map(),
Expand Down Expand Up @@ -204,6 +205,10 @@ const mockUIActions: UIActions = {
handleApiKeyCancel: vi.fn(),
setBannerVisible: vi.fn(),
setShortcutsHelpVisible: vi.fn(),
setCleanUiDetailsVisible: vi.fn(),
toggleCleanUiDetailsVisible: vi.fn(),
revealCleanUiDetailsTemporarily: vi.fn(),
handleWarning: vi.fn(),
setEmbeddedShellFocused: vi.fn(),
dismissBackgroundShell: vi.fn(),
setActiveBackgroundShellPid: vi.fn(),
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/ui/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ describe('App', () => {

const mockUIState: Partial<UIState> = {
streamingState: StreamingState.Idle,
cleanUiDetailsVisible: true,
quittingMessages: null,
dialogsVisible: false,
mainControlsRef: {
Expand Down
34 changes: 33 additions & 1 deletion packages/cli/src/ui/AppContainer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
type Mock,
type MockedObject,
} 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 { cleanup } from 'ink-testing-library';
import { act, useContext, type ReactElement } from 'react';
Expand Down Expand Up @@ -299,6 +299,7 @@ describe('AppContainer State Management', () => {
};

beforeEach(() => {
persistentStateMock.reset();
vi.clearAllMocks();

mockIdeClient.getInstance.mockReturnValue(new Promise(() => {}));
Expand Down Expand Up @@ -488,6 +489,37 @@ describe('AppContainer State Management', () => {
await waitFor(() => expect(capturedUIState).toBeTruthy());
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', () => {
Expand Down
92 changes: 91 additions & 1 deletion packages/cli/src/ui/AppContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
type UserTierId,
type UserFeedbackPayload,
type AgentDefinition,
type ApprovalMode,
IdeClient,
ideContextStore,
getErrorMessage,
Expand Down Expand Up @@ -133,6 +134,7 @@ import { ShellFocusContext } from './contexts/ShellFocusContext.js';
import { type ExtensionManager } from '../config/extension-manager.js';
import { requestConsentInteractive } from '../config/extensions/consent.js';
import { useSessionBrowser } from './hooks/useSessionBrowser.js';
import { persistentState } from '../utils/persistentState.js';
import { useSessionResume } from './hooks/useSessionResume.js';
import { useIncludeDirsTrust } from './hooks/useIncludeDirsTrust.js';
import { isWorkspaceTrusted } from '../config/trustedFolders.js';
Expand Down Expand Up @@ -184,6 +186,9 @@ interface AppContainerProps {
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.
* This provides horizontal padding.
Expand Down Expand Up @@ -796,7 +801,65 @@ Logging in with Google... Restarting Gemini CLI to continue.
const setIsBackgroundShellListOpenRef = useRef<(open: boolean) => void>(
() => {},
);
const [focusUiEnabledByDefault] = useState(
() => persistentState.get(FOCUS_UI_ENABLED_STATE_KEY) === true,
);
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(
() => ({
Expand Down Expand Up @@ -1057,11 +1120,25 @@ Logging in with Google... Restarting Gemini CLI to continue.
const shouldShowActionRequiredTitle = inactivityStatus === 'action_required';
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
const showApprovalModeIndicator = useApprovalModeIndicator({
config,
addItem: historyManager.addItem,
onApprovalModeChange: handleApprovalModeChange,
onApprovalModeChange: handleApprovalModeChangeWithUiReveal,
isActive: !embeddedShellFocused,
});

Expand Down Expand Up @@ -1377,6 +1454,9 @@ Logging in with Google... Restarting Gemini CLI to continue.
if (tabFocusTimeoutRef.current) {
clearTimeout(tabFocusTimeoutRef.current);
}
if (modeRevealTimeoutRef.current) {
clearTimeout(modeRevealTimeoutRef.current);
}
};
}, [showTransientMessage]);

Expand Down Expand Up @@ -1977,6 +2057,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
ctrlDPressedOnce: ctrlDPressCount >= 1,
showEscapePrompt,
shortcutsHelpVisible,
cleanUiDetailsVisible,
isFocused,
elapsedTime,
currentLoadingPhrase,
Expand Down Expand Up @@ -2087,6 +2168,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
ctrlDPressCount,
showEscapePrompt,
shortcutsHelpVisible,
cleanUiDetailsVisible,
isFocused,
elapsedTime,
currentLoadingPhrase,
Expand Down Expand Up @@ -2188,6 +2270,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
handleApiKeyCancel,
setBannerVisible,
setShortcutsHelpVisible,
setCleanUiDetailsVisible,
toggleCleanUiDetailsVisible,
revealCleanUiDetailsTemporarily,
handleWarning,
setEmbeddedShellFocused,
dismissBackgroundShell,
setActiveBackgroundShellPid,
Expand Down Expand Up @@ -2264,6 +2350,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
handleApiKeyCancel,
setBannerVisible,
setShortcutsHelpVisible,
setCleanUiDetailsVisible,
toggleCleanUiDetailsVisible,
revealCleanUiDetailsTemporarily,
handleWarning,
setEmbeddedShellFocused,
dismissBackgroundShell,
setActiveBackgroundShellPid,
Expand Down
11 changes: 10 additions & 1 deletion packages/cli/src/ui/components/AppHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,25 @@ import { useTips } from '../hooks/useTips.js';

interface AppHeaderProps {
version: string;
showDetails?: boolean;
}

export const AppHeader = ({ version }: AppHeaderProps) => {
export const AppHeader = ({ version, showDetails = true }: AppHeaderProps) => {
const settings = useSettings();
const config = useConfig();
const { nightly, terminalWidth, bannerData, bannerVisible } = useUIState();

const { bannerText } = useBanner(bannerData);
const { showTips } = useTips();

if (!showDetails) {
return (
<Box flexDirection="column">
<Header version={version} nightly={false} />
</Box>
);
}

return (
<Box flexDirection="column">
{!(settings.merged.ui.hideBanner || config.getScreenReader()) && (
Expand Down
Loading
Loading