fix(ui): unify shortcuts hint into ambient tips system and remove margins

This commit is contained in:
Keith Guerin
2026-03-12 16:03:34 -07:00
parent d92ce45af2
commit ad2f426abb
4 changed files with 39 additions and 85 deletions

View File

@@ -89,10 +89,6 @@ vi.mock('./ShellModeIndicator.js', () => ({
ShellModeIndicator: () => <Text>ShellModeIndicator</Text>,
}));
vi.mock('./ShortcutsHint.js', () => ({
ShortcutsHint: () => <Text>ShortcutsHint</Text>,
}));
vi.mock('./ShortcutsHelp.js', () => ({
ShortcutsHelp: () => <Text>ShortcutsHelp</Text>,
}));
@@ -847,7 +843,9 @@ describe('Composer', () => {
await vi.advanceTimersByTimeAsync(250);
});
expect(lastFrame({ allowEmpty: true })).toContain('ShortcutsHint');
expect(lastFrame({ allowEmpty: true })).toContain(
'press tab twice for more',
);
});
it('hides shortcuts hint when text is typed in buffer', async () => {
@@ -901,7 +899,7 @@ describe('Composer', () => {
await vi.advanceTimersByTimeAsync(250);
});
expect(lastFrame()).toContain('ShortcutsHint');
expect(lastFrame()).toContain('press tab twice for more');
});
it('shows shortcuts hint when full UI details are visible', async () => {
@@ -916,7 +914,7 @@ describe('Composer', () => {
});
// In Refreshed UX, shortcuts hint is in the top multipurpose status row
expect(lastFrame()).toContain('ShortcutsHint');
expect(lastFrame()).toContain('? for shortcuts');
});
it('hides shortcuts hint while loading when full UI details are visible', async () => {
@@ -1009,7 +1007,7 @@ describe('Composer', () => {
});
// In Refreshed UX, shortcuts hint is in the top status row and doesn't collide with suggestions below
expect(lastFrame()).toContain('ShortcutsHint');
expect(lastFrame()).toContain('press tab twice for more');
});
});

View File

@@ -31,7 +31,6 @@ import { ApprovalModeIndicator } from './ApprovalModeIndicator.js';
import { ShellModeIndicator } from './ShellModeIndicator.js';
import { DetailedMessagesDisplay } from './DetailedMessagesDisplay.js';
import { RawMarkdownIndicator } from './RawMarkdownIndicator.js';
import { ShortcutsHint } from './ShortcutsHint.js';
import { ShortcutsHelp } from './ShortcutsHelp.js';
import { InputPrompt } from './InputPrompt.js';
import { Footer } from './Footer.js';
@@ -112,27 +111,6 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
uiState.streamingState === StreamingState.Idle &&
!hasPendingActionRequired;
const [showShortcutsHintDebounced, setShowShortcutsHintDebounced] =
useState(false);
const canShowShortcutsHint =
uiState.isInputActive &&
uiState.streamingState === StreamingState.Idle &&
!hasPendingActionRequired &&
uiState.buffer.text.length === 0;
useEffect(() => {
if (!canShowShortcutsHint) {
setShowShortcutsHintDebounced(false);
return;
}
const timeout = setTimeout(() => {
setShowShortcutsHintDebounced(true);
}, 200);
return () => clearTimeout(timeout);
}, [canShowShortcutsHint]);
/**
* Use the setting if provided, otherwise default to true for the new UX.
* This allows tests to override the collapse behavior.
@@ -194,9 +172,6 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
const shouldReserveSpaceForShortcutsHint =
settings.merged.ui.showShortcutsHint && !hideUiDetailsForSuggestions;
const showShortcutsHint =
shouldReserveSpaceForShortcutsHint && showShortcutsHintDebounced;
const showMinimalContextBleedThrough =
!settings.merged.ui.footer.hideContextPercentage &&
isContextUsageHigh(
@@ -239,7 +214,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
* Determine the ambient text (tip) to display.
*/
const ambientContentStr = (() => {
// Only show Tips on the right
// 1. Proactive Tip (Priority)
if (showTips && uiState.currentTip) {
if (
estimatedStatusLength + uiState.currentTip.length + 10 <=
@@ -249,6 +224,15 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
}
}
// 2. Shortcut Hint (Fallback)
if (
settings.merged.ui.showShortcutsHint &&
!hideUiDetailsForSuggestions &&
!hasPendingActionRequired
) {
return showUiDetails ? '? for shortcuts' : 'press tab twice for more';
}
return undefined;
})();
@@ -257,9 +241,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
estimatedStatusLength + estimatedAmbientLength + 5 > terminalWidth;
const showAmbientLine =
uiState.streamingState !== StreamingState.Idle &&
!hasPendingActionRequired &&
(showTips || showWit) &&
ambientContentStr &&
!willCollideAmbient &&
!isNarrow;
@@ -296,17 +278,22 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
const renderAmbientNode = () => {
if (!ambientContentStr) return null;
const isShortcutHint =
ambientContentStr === '? for shortcuts' ||
ambientContentStr === 'press tab twice for more';
const color =
isShortcutHint && uiState.shortcutsHelpVisible
? theme.text.accent
: theme.text.secondary;
return (
<Box
flexDirection="row"
justifyContent="flex-end"
marginLeft={1}
marginRight={1}
>
<Box flexDirection="row" justifyContent="flex-end">
<Text
color={theme.text.secondary}
color={color}
wrap="truncate-end"
italic={ambientContentStr === uiState.currentWittyPhrase}
italic={
!isShortcutHint && ambientContentStr === uiState.currentWittyPhrase
}
>
{ambientContentStr === uiState.currentTip
? `Tip: ${ambientContentStr}`
@@ -352,7 +339,6 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
thought={uiState.thought}
elapsedTime={uiState.elapsedTime}
forceRealStatusOnly={false}
showCancelAndTimer={false}
wittyPhrase={uiState.currentWittyPhrase}
/>
);
@@ -374,7 +360,6 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
errorVerbosity={settings.merged.ui.errorVerbosity}
elapsedTime={uiState.elapsedTime}
forceRealStatusOnly={true}
showCancelAndTimer={false}
/>
)}
{hasUserHooks && (
@@ -450,12 +435,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
</Box>
<Box flexShrink={0} marginLeft={2} marginRight={isNarrow ? 0 : 1}>
{!isNarrow && (
<>
{showShortcutsHint && <ShortcutsHint />}
{!showShortcutsHint && showAmbientLine && renderAmbientNode()}
</>
)}
{!isNarrow && showAmbientLine && renderAmbientNode()}
</Box>
</Box>
)}
@@ -464,7 +444,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
{showRow1 &&
showRow2 &&
(showUiDetails || (showRow1_MiniMode && showRow2_MiniMode)) && (
<Box width="100%" marginLeft={1} marginRight={1}>
<Box width="100%">
<Box
borderStyle="single"
borderTop

View File

@@ -1,24 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { Text } from 'ink';
import { theme } from '../semantic-colors.js';
import { useUIState } from '../contexts/UIStateContext.js';
export const ShortcutsHint: React.FC = () => {
const { cleanUiDetailsVisible, shortcutsHelpVisible } = useUIState();
if (!cleanUiDetailsVisible) {
return <Text color={theme.text.secondary}> press tab twice for more </Text>;
}
const highlightColor = shortcutsHelpVisible
? theme.text.accent
: theme.text.secondary;
return <Text color={highlightColor}>? for shortcuts</Text>;
};

View File

@@ -2,8 +2,8 @@
exports[`Composer > Snapshots > matches snapshot in idle state 1`] = `
"
───────────────────────────────────────────────────────────────────────────────────────────────────
? for shortcuts
───────────────────────────────────────────────────────────────────────────────────────────────────
ApprovalModeIndicator: default StatusDisplay
InputPrompt: Type your message or @path/to/file
Footer
@@ -11,21 +11,21 @@ Footer
`;
exports[`Composer > Snapshots > matches snapshot in minimal UI mode 1`] = `
"
" press tab twice for more
InputPrompt: Type your message or @path/to/file
"
`;
exports[`Composer > Snapshots > matches snapshot in minimal UI mode while loading 1`] = `
"LoadingIndicator
"LoadingIndicator press tab twice for more
InputPrompt: Type your message or @path/to/file
"
`;
exports[`Composer > Snapshots > matches snapshot in narrow view 1`] = `
"
────────────────────────────────────────
? for shortcuts
────────────────────────────────────────
ApprovalModeIndicator: StatusDispl
default ay
InputPrompt: Type your message or
@@ -36,8 +36,8 @@ Footer
exports[`Composer > Snapshots > matches snapshot while streaming 1`] = `
"
LoadingIndicator: Thinking
───────────────────────────────────────────────────────────────────────────────────────────────────
LoadingIndicator: Thinking ? for shortcuts
───────────────────────────────────────────────────────────────────────────────────────────────────
ApprovalModeIndicator: default StatusDisplay
InputPrompt: Type your message or @path/to/file
Footer