Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 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
4 changes: 4 additions & 0 deletions packages/cli/src/config/keyBindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export enum Command {
UNFOCUS_SHELL_INPUT = 'app.unfocusShellInput',
CLEAR_SCREEN = 'app.clearScreen',
RESTART_APP = 'app.restart',
TOGGLE_STEERING_MODE = 'app.toggleSteeringMode',
}

/**
Expand Down Expand Up @@ -258,6 +259,7 @@ export const defaultKeyBindings: KeyBindingConfig = {
[Command.UNFOCUS_SHELL_INPUT]: [{ key: 'tab' }],
[Command.CLEAR_SCREEN]: [{ key: 'l', ctrl: true }],
[Command.RESTART_APP]: [{ key: 'r' }],
[Command.TOGGLE_STEERING_MODE]: [{ key: 'o', ctrl: true }],
};

interface CommandCategory {
Expand Down Expand Up @@ -365,6 +367,7 @@ export const commandCategories: readonly CommandCategory[] = [
Command.UNFOCUS_SHELL_INPUT,
Command.CLEAR_SCREEN,
Command.RESTART_APP,
Command.TOGGLE_STEERING_MODE,
],
},
];
Expand Down Expand Up @@ -453,4 +456,5 @@ export const commandDescriptions: Readonly<Record<Command, string>> = {
[Command.UNFOCUS_SHELL_INPUT]: 'Focus the Gemini input from the shell input.',
[Command.CLEAR_SCREEN]: 'Clear the terminal screen and redraw the UI.',
[Command.RESTART_APP]: 'Restart the application.',
[Command.TOGGLE_STEERING_MODE]: 'Toggle steering mode to correct the agent.',
};
23 changes: 22 additions & 1 deletion packages/cli/src/ui/AppContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ import { useHookDisplayState } from './hooks/useHookDisplayState.js';
import {
WARNING_PROMPT_DURATION_MS,
QUEUE_ERROR_DISPLAY_DURATION_MS,
STEERING_TEMPLATE,
} from './constants.js';
import { LoginWithGoogleRestartDialog } from './auth/LoginWithGoogleRestartDialog.js';
import { isSlashCommand } from './utils/commandUtils.js';
Expand Down Expand Up @@ -201,6 +202,7 @@ export const AppContainer = (props: AppContainerProps) => {
const [copyModeEnabled, setCopyModeEnabled] = useState(false);
const [pendingRestorePrompt, setPendingRestorePrompt] = useState(false);
const [adminSettingsChanged, setAdminSettingsChanged] = useState(false);
const [isSteeringMode, setIsSteeringMode] = useState(false);

const [shellModeActive, setShellModeActive] = useState(false);
const [modelSwitchedFromQuotaError, setModelSwitchedFromQuotaError] =
Expand Down Expand Up @@ -866,6 +868,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
loopDetectionConfirmationRequest,
lastOutputTime,
retryStatus,
injectSteeringMessage,
} = useGeminiStream(
config.getGeminiClient(),
historyManager.history,
Expand Down Expand Up @@ -968,9 +971,21 @@ Logging in with Google... Restarting Gemini CLI to continue.
(submittedValue: string) => {
const isSlash = isSlashCommand(submittedValue.trim());
const isIdle = streamingState === StreamingState.Idle;
let displayText: string | undefined;

if (isSteeringMode) {
setIsSteeringMode(false);
if (!isIdle) {
injectSteeringMessage(submittedValue);
cancelHandlerRef.current(false);
return;
}
displayText = submittedValue;
submittedValue = STEERING_TEMPLATE(submittedValue);
}

if (isSlash || (isIdle && isMcpReady)) {
void submitQuery(submittedValue);
void submitQuery(submittedValue, undefined, undefined, displayText);
} else {
// Check messageQueue.length === 0 to only notify on the first queued item
if (isIdle && !isMcpReady && messageQueue.length === 0) {
Expand All @@ -990,6 +1005,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
isMcpReady,
streamingState,
messageQueue.length,
isSteeringMode,
injectSteeringMessage,
],
);

Expand Down Expand Up @@ -1361,6 +1378,8 @@ Logging in with Google... Restarting Gemini CLI to continue.

if (keyMatchers[Command.SHOW_ERROR_DETAILS](key)) {
setShowErrorDetails((prev) => !prev);
} else if (keyMatchers[Command.TOGGLE_STEERING_MODE](key)) {
setIsSteeringMode((prev) => !prev);
} else if (keyMatchers[Command.SHOW_FULL_TODOS](key)) {
setShowFullTodos((prev) => !prev);
} else if (keyMatchers[Command.TOGGLE_MARKDOWN](key)) {
Expand Down Expand Up @@ -1721,6 +1740,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
terminalBackgroundColor: config.getTerminalBackground(),
settingsNonce,
adminSettingsChanged,
isSteeringMode,
}),
[
isThemeDialogOpen,
Expand Down Expand Up @@ -1820,6 +1840,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
config,
settingsNonce,
adminSettingsChanged,
isSteeringMode,
],
);

Expand Down
12 changes: 7 additions & 5 deletions packages/cli/src/ui/components/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,13 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
isEmbeddedShellFocused={uiState.embeddedShellFocused}
popAllMessages={uiActions.popAllMessages}
placeholder={
vimEnabled
? " Press 'i' for INSERT mode and 'Esc' for NORMAL mode."
: uiState.shellModeActive
? ' Type your shell command'
: ' Type your message or @path/to/file'
uiState.isSteeringMode
? ' STEERING MODE: Inject correction...'
: vimEnabled
? " Press 'i' for INSERT mode and 'Esc' for NORMAL mode."
: uiState.shellModeActive
? ' Type your shell command'
: ' Type your message or @path/to/file'
}
setQueueErrorMessage={uiActions.setQueueErrorMessage}
streamingState={uiState.streamingState}
Expand Down
6 changes: 5 additions & 1 deletion packages/cli/src/ui/components/HistoryItemDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,11 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
<Box flexDirection="column" key={itemForDisplay.id} width={terminalWidth}>
{/* Render standard message types */}
{itemForDisplay.type === 'user' && (
<UserMessage text={itemForDisplay.text} width={terminalWidth} />
<UserMessage
text={itemForDisplay.text}
displayText={itemForDisplay.displayText}
width={terminalWidth}
/>
)}
{itemForDisplay.type === 'user_shell' && (
<UserShellMessage text={itemForDisplay.text} />
Expand Down
9 changes: 7 additions & 2 deletions packages/cli/src/ui/components/messages/UserMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,15 @@ import { isSlashCommand as checkIsSlashCommand } from '../../utils/commandUtils.

interface UserMessageProps {
text: string;
displayText?: string;
width: number;
}

export const UserMessage: React.FC<UserMessageProps> = ({ text, width }) => {
export const UserMessage: React.FC<UserMessageProps> = ({
text,
displayText,
width,
}) => {
const prefix = '> ';
const prefixWidth = prefix.length;
const isSlashCommand = checkIsSlashCommand(text);
Expand All @@ -37,7 +42,7 @@ export const UserMessage: React.FC<UserMessageProps> = ({ text, width }) => {
</Box>
<Box flexGrow={1}>
<Text wrap="wrap" color={textColor}>
{text}
{displayText ?? text}
</Text>
</Box>
</Box>
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/ui/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,6 @@ export const SHELL_SILENT_WORKING_TITLE_DELAY_MS = 120000;
export const KEYBOARD_SHORTCUTS_URL =
'https://geminicli.com/docs/cli/keyboard-shortcuts/';
export const LRU_BUFFER_PERF_CACHE_LIMIT = 20000;

export const STEERING_TEMPLATE = (message: string) =>
`**STEERING CORRECTION:** ${message}\n\n**INSTRUCTIONS:**\n1. Recalibrate your internal state, goal, and plan based on this direction.\n2. If the direction is clear, proceed immediately to the next step of your **REVISED** plan.\n3. If your revised plan is now complete, stop and conclude the task. Only stop for clarification if absolutely necessary to avoid a critical error.`;
1 change: 1 addition & 0 deletions packages/cli/src/ui/contexts/UIStateContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ export interface UIState {
terminalBackgroundColor: TerminalBackgroundColor;
settingsNonce: number;
adminSettingsChanged: boolean;
isSteeringMode: boolean;
}

export const UIStateContext = createContext<UIState | null>(null);
Expand Down
59 changes: 55 additions & 4 deletions packages/cli/src/ui/hooks/useGeminiStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ import { findLastSafeSplitPoint } from '../utils/markdownUtilities.js';
import { useStateAndRef } from './useStateAndRef.js';
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
import { useLogger } from './useLogger.js';
import { SHELL_COMMAND_NAME } from '../constants.js';
import { SHELL_COMMAND_NAME, STEERING_TEMPLATE } from '../constants.js';
import { mapToDisplay as mapTrackedToolCallsToDisplay } from './toolMapping.js';
import {
useToolScheduler,
Expand Down Expand Up @@ -225,6 +225,23 @@ export const useGeminiStream = (
const lastQueryRef = useRef<PartListUnion | null>(null);
const lastPromptIdRef = useRef<string | null>(null);
const loopDetectedRef = useRef(false);
const pendingSteeringMessageRef = useRef<string | null>(null);
const steeringInjectedInCurrentTurnRef = useRef(false);

const injectSteeringMessage = useCallback(
(message: string) => {
const formattedMessage = STEERING_TEMPLATE(message);
pendingSteeringMessageRef.current = message;
steeringInjectedInCurrentTurnRef.current = false;
addItem({
type: MessageType.USER,
text: formattedMessage,
displayText: message,
});
},
[addItem],
);

const [
loopDetectionConfirmationRequest,
setLoopDetectionConfirmationRequest,
Expand Down Expand Up @@ -426,6 +443,7 @@ export const useGeminiStream = (
userMessageTimestamp: number,
abortSignal: AbortSignal,
prompt_id: string,
displayText?: string,
): Promise<{
queryToSend: PartListUnion | null;
shouldProceed: boolean;
Expand Down Expand Up @@ -492,7 +510,7 @@ export const useGeminiStream = (
if (isAtCommand(trimmedQuery)) {
// Add user's turn before @ command processing for correct UI ordering.
addItem(
{ type: MessageType.USER, text: trimmedQuery },
{ type: MessageType.USER, text: trimmedQuery, displayText },
userMessageTimestamp,
);

Expand All @@ -513,7 +531,7 @@ export const useGeminiStream = (
} else {
// Normal query for Gemini
addItem(
{ type: MessageType.USER, text: trimmedQuery },
{ type: MessageType.USER, text: trimmedQuery, displayText },
userMessageTimestamp,
);
localQueryToSendToGemini = trimmedQuery;
Expand Down Expand Up @@ -984,6 +1002,7 @@ export const useGeminiStream = (
query: PartListUnion,
options?: { isContinuation: boolean },
prompt_id?: string,
displayText?: string,
) =>
runInDevTraceSpan(
{ name: 'submitQuery' },
Expand Down Expand Up @@ -1015,11 +1034,25 @@ export const useGeminiStream = (
prompt_id = config.getSessionId() + '########' + getPromptCount();
}
return promptIdContext.run(prompt_id, async () => {
let currentQuery = query;
const steeringMsg = pendingSteeringMessageRef.current;

if (steeringMsg && options?.isContinuation) {
const injectionText = `\n\n${STEERING_TEMPLATE(steeringMsg)}`;
if (Array.isArray(currentQuery)) {
currentQuery = [...currentQuery, { text: injectionText }];
} else if (typeof currentQuery === 'string') {
currentQuery = currentQuery + injectionText;
}
steeringInjectedInCurrentTurnRef.current = true;
}

const { queryToSend, shouldProceed } = await prepareQueryForGemini(
query,
currentQuery,
userMessageTimestamp,
abortSignal,
prompt_id!,
displayText,
);

if (!shouldProceed || queryToSend === null) {
Expand Down Expand Up @@ -1157,6 +1190,22 @@ export const useGeminiStream = (
],
);

useEffect(() => {
if (
streamingState === StreamingState.Idle &&
pendingSteeringMessageRef.current
) {
if (steeringInjectedInCurrentTurnRef.current) {
pendingSteeringMessageRef.current = null;
steeringInjectedInCurrentTurnRef.current = false;
} else {
const msg = pendingSteeringMessageRef.current;
pendingSteeringMessageRef.current = null;
void submitQuery(STEERING_TEMPLATE(msg), undefined, undefined, msg);
}
}
}, [streamingState, submitQuery]);

const handleApprovalModeChange = useCallback(
async (newApprovalMode: ApprovalMode) => {
// Auto-approve pending tool calls when switching to auto-approval modes
Expand Down Expand Up @@ -1335,6 +1384,7 @@ export const useGeminiStream = (
isContinuation: true,
},
prompt_ids[0],
pendingSteeringMessageRef.current ?? undefined,
);
},
[
Expand Down Expand Up @@ -1434,5 +1484,6 @@ export const useGeminiStream = (
loopDetectionConfirmationRequest,
lastOutputTime,
retryStatus,
injectSteeringMessage,
};
};
1 change: 1 addition & 0 deletions packages/cli/src/ui/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export const emptyIcon = ' ';

export interface HistoryItemBase {
text?: string; // Text content for user/gemini/info/error messages
displayText?: string;
}

export type HistoryItemUser = HistoryItemBase & {
Expand Down
Loading