Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions packages/cli/src/test-utils/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ const baseMockUiState = {
proQuotaRequest: null,
validationRequest: null,
},
hintMode: false,
hintBuffer: '',
};

export const mockAppState: AppState = {
Expand Down Expand Up @@ -209,6 +211,10 @@ const mockUIActions: UIActions = {
setActiveBackgroundShellPid: vi.fn(),
setIsBackgroundShellListOpen: vi.fn(),
setAuthContext: vi.fn(),
onHintInput: vi.fn(),
onHintBackspace: vi.fn(),
onHintClear: vi.fn(),
onHintSubmit: vi.fn(),
handleRestart: vi.fn(),
handleNewAgentsSelect: vi.fn(),
};
Expand Down
93 changes: 92 additions & 1 deletion packages/cli/src/ui/AppContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,11 @@ import { basename } from 'node:path';
import { computeTerminalTitle } from '../utils/windowTitle.js';
import { useTextBuffer } from './components/shared/text-buffer.js';
import { useLogger } from './hooks/useLogger.js';
import { useGeminiStream } from './hooks/useGeminiStream.js';
import {
buildUserSteeringHintPrompt,
generateSteeringAckMessage,
useGeminiStream,
} from './hooks/useGeminiStream.js';
import { type BackgroundShell } from './hooks/shellCommandProcessor.js';
import { useVim } from './hooks/vim.js';
import { type LoadableSettingScope, SettingScope } from '../config/settings.js';
Expand Down Expand Up @@ -963,6 +967,18 @@ Logging in with Google... Restarting Gemini CLI to continue.
}
}, [pendingRestorePrompt, inputHistory, historyManager.history]);

const consumePendingHints = useCallback(() => {
const userHints = config.consumeUserHints();
if (userHints.length === 0) {
return null;
}
return userHints.join('\n');
}, [config]);
const getUserHint = useCallback(
() => consumePendingHints(),
[consumePendingHints],
);

const {
streamingState,
submitQuery,
Expand Down Expand Up @@ -1001,6 +1017,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
terminalWidth,
terminalHeight,
embeddedShellFocused,
getUserHint,
);

toggleBackgroundShellRef.current = toggleBackgroundShell;
Expand Down Expand Up @@ -1103,10 +1120,38 @@ Logging in with Google... Restarting Gemini CLI to continue.
],
);

const handleHintSubmit = useCallback(
(hint: string) => {
const trimmed = hint.trim();
if (!trimmed) {
return;
}
config.addUserHint(trimmed);
// Render hints as regular user input so they look like normal commands.
historyManager.addItem({
type: MessageType.USER,
text: trimmed,
});
},
[config, historyManager],
);

const handleFinalSubmit = useCallback(
async (submittedValue: string) => {
const isSlash = isSlashCommand(submittedValue.trim());
const isIdle = streamingState === StreamingState.Idle;
const isAgentRunning =
streamingState === StreamingState.Responding ||
isToolExecuting([
...pendingSlashCommandHistoryItems,
...pendingGeminiHistoryItems,
]);

if (isAgentRunning && !isSlash) {
handleHintSubmit(submittedValue);
addInput(submittedValue);
return;
}

if (isSlash || (isIdle && isMcpReady)) {
if (!isSlash) {
Expand Down Expand Up @@ -1148,7 +1193,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
isMcpReady,
streamingState,
messageQueue.length,
pendingSlashCommandHistoryItems,
pendingGeminiHistoryItems,
config,
handleHintSubmit,
],
);

Expand Down Expand Up @@ -1814,6 +1862,43 @@ Logging in with Google... Restarting Gemini CLI to continue.
[pendingSlashCommandHistoryItems, pendingGeminiHistoryItems],
);

useEffect(() => {
if (
!isConfigInitialized ||
streamingState !== StreamingState.Idle ||
!isMcpReady
) {
return;
}

const pendingHint = consumePendingHints();
if (!pendingHint) {
return;
}

const geminiClient = config.getGeminiClient();
void generateSteeringAckMessage(geminiClient, pendingHint).then(
(ackText) => {
historyManager.addItem({
type: 'info',
icon: '· ',
color: 'gray',
marginBottom: 1,
text: ackText,
} as Omit<HistoryItem, 'id'>);
},
);
void submitQuery([{ text: buildUserSteeringHintPrompt(pendingHint) }]);
}, [
config,
consumePendingHints,
historyManager,
isConfigInitialized,
isMcpReady,
streamingState,
submitQuery,
]);

const allToolCalls = useMemo(
() =>
pendingHistoryItems
Expand Down Expand Up @@ -1975,6 +2060,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
isBackgroundShellListOpen,
adminSettingsChanged,
newAgents,
hintMode: false,
hintBuffer: '',
}),
[
isThemeDialogOpen,
Expand Down Expand Up @@ -2137,6 +2224,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
setActiveBackgroundShellPid,
setIsBackgroundShellListOpen,
setAuthContext,
onHintInput: () => {},
onHintBackspace: () => {},
onHintClear: () => {},
onHintSubmit: () => {},
handleRestart: async () => {
if (process.send) {
const remoteSettings = config.getRemoteAdminSettings();
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/ui/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ export const Footer: React.FC = () => {
const justifyContent = hideCWD && hideModelInfo ? 'center' : 'space-between';
const displayVimMode = vimEnabled ? vimMode : undefined;

const showDebugProfiler = debugMode || isDevelopment;
const showDebugProfiler =
debugMode || (isDevelopment && settings.merged.general.devtools);

return (
<Box
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/ui/components/Header.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ describe('<Header />', () => {
},
background: {
primary: '',
hintMode: '',
diff: { added: '', removed: '' },
},
border: {
Expand Down
12 changes: 12 additions & 0 deletions packages/cli/src/ui/components/HistoryItemDisplay.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@ describe('<HistoryItemDisplay />', () => {
expect(lastFrame()).toContain('Hello');
});

it('renders HintMessage for "hint" type', () => {
const item: HistoryItem = {
...baseItem,
type: 'hint',
text: 'Try using ripgrep first',
};
const { lastFrame } = renderWithProviders(
<HistoryItemDisplay {...baseItem} item={item} />,
);
expect(lastFrame()).toContain('Try using ripgrep first');
});

it('renders UserMessage for "user" type with slash command', () => {
const item: HistoryItem = {
...baseItem,
Expand Down
5 changes: 5 additions & 0 deletions packages/cli/src/ui/components/HistoryItemDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { ChatList } from './views/ChatList.js';
import { HooksList } from './views/HooksList.js';
import { ModelMessage } from './messages/ModelMessage.js';
import { ThinkingMessage } from './messages/ThinkingMessage.js';
import { HintMessage } from './messages/HintMessage.js';
import { getInlineThinkingMode } from '../utils/inlineThinkingMode.js';
import { useSettings } from '../contexts/SettingsContext.js';

Expand Down Expand Up @@ -71,6 +72,9 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
{itemForDisplay.type === 'thinking' && inlineThinkingMode !== 'off' && (
<ThinkingMessage thought={itemForDisplay.thought} />
)}
{itemForDisplay.type === 'hint' && (
<HintMessage text={itemForDisplay.text} />
)}
{itemForDisplay.type === 'user' && (
<UserMessage text={itemForDisplay.text} width={terminalWidth} />
)}
Expand Down Expand Up @@ -102,6 +106,7 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
text={itemForDisplay.text}
icon={itemForDisplay.icon}
color={itemForDisplay.color}
marginBottom={itemForDisplay.marginBottom}
/>
)}
{itemForDisplay.type === 'warning' && (
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/ui/components/messages/InfoMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,21 @@ interface InfoMessageProps {
text: string;
icon?: string;
color?: string;
marginBottom?: number;
}

export const InfoMessage: React.FC<InfoMessageProps> = ({
text,
icon,
color,
marginBottom,
}) => {
color ??= theme.status.warning;
const prefix = icon ?? 'ℹ ';
const prefixWidth = prefix.length;

return (
<Box flexDirection="row" marginTop={1}>
<Box flexDirection="row" marginTop={1} marginBottom={marginBottom ?? 0}>
<Box width={prefixWidth}>
<Text color={color}>{prefix}</Text>
</Box>
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/ui/contexts/UIActionsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ export interface UIActions {
setActiveBackgroundShellPid: (pid: number) => void;
setIsBackgroundShellListOpen: (isOpen: boolean) => void;
setAuthContext: (context: { requiresRestart?: boolean }) => void;
onHintInput: (char: string) => void;
onHintBackspace: () => void;
onHintClear: () => void;
onHintSubmit: (hint: string) => void;
handleRestart: () => void;
handleNewAgentsSelect: (choice: NewAgentsChoice) => Promise<void>;
}
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/ui/contexts/UIStateContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ export interface UIState {
isBackgroundShellListOpen: boolean;
adminSettingsChanged: boolean;
newAgents: AgentDefinition[] | null;
hintMode: boolean;
hintBuffer: string;
transientMessage: {
text: string;
type: TransientMessageType;
Expand Down
Loading