mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-24 00:30:59 -07:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2bf1f61b17 | |||
| c20a5267ec | |||
| a47f4db5e7 | |||
| f45334f9bc | |||
| 76638be07f | |||
| c7a2250e1e | |||
| d937221d2f | |||
| b101a83414 |
@@ -86,12 +86,13 @@ available combinations.
|
||||
|
||||
#### Text Input
|
||||
|
||||
| Command | Action | Keys |
|
||||
| -------------------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| `input.submit` | Submit the current prompt. | `Enter` |
|
||||
| `input.newline` | Insert a newline without submitting. | `Ctrl+Enter`<br />`Cmd/Win+Enter`<br />`Alt+Enter`<br />`Shift+Enter`<br />`Ctrl+J` |
|
||||
| `input.openExternalEditor` | Open the current prompt or the plan in an external editor. | `Ctrl+X` |
|
||||
| `input.paste` | Paste from the clipboard. | `Ctrl+V`<br />`Cmd/Win+V`<br />`Alt+V` |
|
||||
| Command | Action | Keys |
|
||||
| -------------------------- | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| `input.submit` | Submit the current prompt. | `Enter` |
|
||||
| `input.newline` | Insert a newline without submitting. | `Ctrl+Enter`<br />`Cmd/Win+Enter`<br />`Alt+Enter`<br />`Shift+Enter`<br />`Ctrl+J` |
|
||||
| `input.openExternalEditor` | Open the current prompt or the plan in an external editor. | `Ctrl+X` |
|
||||
| `input.paste` | Paste from the clipboard. | `Ctrl+V`<br />`Cmd/Win+V`<br />`Alt+V` |
|
||||
| `input.stash` | Stash the current input to temporarily set it aside. Restores on next submit. | `Alt+S` |
|
||||
|
||||
#### App Controls
|
||||
|
||||
@@ -232,6 +233,8 @@ a `key` combination.
|
||||
- `Ctrl + X` (while a plan is presented): Open the plan in an external editor to
|
||||
[collaboratively edit or comment](../cli/plan-mode.md#collaborative-plan-editing)
|
||||
on the implementation strategy.
|
||||
- `Alt+S`: Stash the current prompt to temporarily set it aside. The stashed
|
||||
prompt is restored to the input box when you submit your next prompt.
|
||||
- `Double-click` on a paste placeholder (alternate buffer mode only): Expand to
|
||||
view full content inline. Double-click again to collapse.
|
||||
|
||||
|
||||
@@ -567,6 +567,8 @@ const mockUIActions: UIActions = {
|
||||
handleEmptyWalletChoice: vi.fn(),
|
||||
setQueueErrorMessage: vi.fn(),
|
||||
popAllMessages: vi.fn(),
|
||||
stashPrompt: vi.fn(),
|
||||
popStashedPrompt: vi.fn(),
|
||||
handleApiKeySubmit: vi.fn(),
|
||||
handleApiKeyCancel: vi.fn(),
|
||||
setBannerVisible: vi.fn(),
|
||||
|
||||
@@ -130,6 +130,7 @@ import { registerCleanup, runExitCleanup } from '../utils/cleanup.js';
|
||||
import { relaunchApp } from '../utils/processUtils.js';
|
||||
import type { SessionInfo } from '../utils/sessionUtils.js';
|
||||
import { useMessageQueue } from './hooks/useMessageQueue.js';
|
||||
import { usePromptStash } from './hooks/usePromptStash.js';
|
||||
import { useMcpStatus } from './hooks/useMcpStatus.js';
|
||||
import { useApprovalModeIndicator } from './hooks/useApprovalModeIndicator.js';
|
||||
import { useSessionStats } from './contexts/SessionContext.js';
|
||||
@@ -253,6 +254,8 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
QUEUE_ERROR_DISPLAY_DURATION_MS,
|
||||
);
|
||||
|
||||
const { stashedPrompt, stashPrompt, popStashedPrompt } = usePromptStash();
|
||||
|
||||
const [newAgents, setNewAgents] = useState<AgentDefinition[] | null>(null);
|
||||
const [constrainHeight, setConstrainHeight] = useState<boolean>(true);
|
||||
const [expandHintTrigger, triggerExpandHint] = useTimedMessage<boolean>(
|
||||
@@ -2267,6 +2270,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
activeHooks,
|
||||
messageQueue,
|
||||
queueErrorMessage,
|
||||
stashedPrompt,
|
||||
showApprovalModeIndicator,
|
||||
allowPlanMode,
|
||||
currentModel,
|
||||
@@ -2393,6 +2397,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
activeHooks,
|
||||
messageQueue,
|
||||
queueErrorMessage,
|
||||
stashedPrompt,
|
||||
showApprovalModeIndicator,
|
||||
allowPlanMode,
|
||||
userTier,
|
||||
@@ -2492,6 +2497,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
handleDeleteSession,
|
||||
setQueueErrorMessage,
|
||||
popAllMessages,
|
||||
stashPrompt,
|
||||
popStashedPrompt,
|
||||
handleApiKeySubmit,
|
||||
handleApiKeyCancel,
|
||||
setBannerVisible,
|
||||
@@ -2583,6 +2590,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
handleDeleteSession,
|
||||
setQueueErrorMessage,
|
||||
popAllMessages,
|
||||
stashPrompt,
|
||||
popStashedPrompt,
|
||||
handleApiKeySubmit,
|
||||
handleApiKeyCancel,
|
||||
setBannerVisible,
|
||||
|
||||
@@ -136,11 +136,16 @@ vi.mock('./QueuedMessageDisplay.js', () => ({
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Box flexDirection="column">
|
||||
<Box paddingLeft={2}>
|
||||
<Text>Queued (press ↑ to edit):</Text>
|
||||
</Box>
|
||||
{messageQueue.map((message, index) => (
|
||||
<Text key={index}>{message}</Text>
|
||||
<Box key={index} paddingLeft={4}>
|
||||
<Text>{message}</Text>
|
||||
</Box>
|
||||
))}
|
||||
</>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
}));
|
||||
@@ -158,6 +163,7 @@ const createMockUIState = (overrides: Partial<UIState> = {}): UIState =>
|
||||
contextFileNames: [],
|
||||
showApprovalModeIndicator: ApprovalMode.DEFAULT,
|
||||
messageQueue: [],
|
||||
stashedPrompt: null,
|
||||
showErrorDetails: false,
|
||||
constrainHeight: false,
|
||||
isInputActive: true,
|
||||
@@ -1094,5 +1100,14 @@ describe('Composer', () => {
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot with stashed prompt and queued messages', async () => {
|
||||
const uiState = createMockUIState({
|
||||
stashedPrompt: 'This is a stashed prompt',
|
||||
messageQueue: ['First queued message', 'Second queued message'],
|
||||
});
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,6 +39,7 @@ import { InputPrompt } from './InputPrompt.js';
|
||||
import { Footer } from './Footer.js';
|
||||
import { ShowMoreLines } from './ShowMoreLines.js';
|
||||
import { QueuedMessageDisplay } from './QueuedMessageDisplay.js';
|
||||
import { StashedPromptDisplay } from './StashedPromptDisplay.js';
|
||||
import { OverflowProvider } from '../contexts/OverflowContext.js';
|
||||
import { ConfigInitDisplay } from './ConfigInitDisplay.js';
|
||||
import { TodoTray } from './messages/Todo.js';
|
||||
@@ -522,9 +523,13 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
<ConfigInitDisplay message="Resuming session..." />
|
||||
)}
|
||||
|
||||
{showUiDetails && (
|
||||
<QueuedMessageDisplay messageQueue={uiState.messageQueue} />
|
||||
)}
|
||||
{showUiDetails &&
|
||||
(uiState.stashedPrompt || uiState.messageQueue.length > 0) && (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<StashedPromptDisplay stashedPrompt={uiState.stashedPrompt} />
|
||||
<QueuedMessageDisplay messageQueue={uiState.messageQueue} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{showUiDetails && <TodoTray />}
|
||||
|
||||
|
||||
@@ -225,6 +225,8 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
setEmbeddedShellFocused,
|
||||
setShortcutsHelpVisible,
|
||||
toggleCleanUiDetailsVisible,
|
||||
stashPrompt,
|
||||
popStashedPrompt,
|
||||
} = useUIActions();
|
||||
const {
|
||||
terminalWidth,
|
||||
@@ -372,6 +374,12 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
onSubmit(processedValue);
|
||||
resetCompletionState();
|
||||
resetReverseSearchCompletionState();
|
||||
|
||||
// Restore stashed prompt after submit
|
||||
const stashed = popStashedPrompt();
|
||||
if (stashed) {
|
||||
buffer.setText(stashed, 'end');
|
||||
}
|
||||
},
|
||||
[
|
||||
buffer,
|
||||
@@ -380,6 +388,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
shellModeActive,
|
||||
shellHistory,
|
||||
resetReverseSearchCompletionState,
|
||||
popStashedPrompt,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1213,6 +1222,16 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
return true;
|
||||
}
|
||||
|
||||
// Stash current input
|
||||
if (keyMatchers[Command.STASH_INPUT](key)) {
|
||||
if (buffer.text.length > 0) {
|
||||
stashPrompt(buffer.text);
|
||||
buffer.setText('');
|
||||
resetCompletionState();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// External editor
|
||||
if (keyMatchers[Command.OPEN_EXTERNAL_EDITOR](key)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
@@ -1304,6 +1323,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
shouldShowSuggestions,
|
||||
isShellSuggestionsVisible,
|
||||
forceShowShellSuggestions,
|
||||
stashPrompt,
|
||||
keyMatchers,
|
||||
isHelpDismissKey,
|
||||
settings,
|
||||
|
||||
@@ -20,7 +20,7 @@ export const QueuedMessageDisplay = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Box flexDirection="column">
|
||||
<Box paddingLeft={2}>
|
||||
<Text dimColor>Queued (press ↑ to edit):</Text>
|
||||
</Box>
|
||||
|
||||
@@ -44,6 +44,10 @@ const buildShortcutItems = (): ShortcutItem[] => [
|
||||
key: formatCommand(Command.OPEN_EXTERNAL_EDITOR),
|
||||
description: 'open external editor',
|
||||
},
|
||||
{
|
||||
key: formatCommand(Command.STASH_INPUT),
|
||||
description: 'stash prompt',
|
||||
},
|
||||
];
|
||||
|
||||
const Shortcut: React.FC<{ item: ShortcutItem }> = ({ item }) => (
|
||||
@@ -73,8 +77,9 @@ export const ShortcutsHelp: React.FC = () => {
|
||||
items[7],
|
||||
items[2],
|
||||
items[8],
|
||||
items[9],
|
||||
items[10],
|
||||
items[3],
|
||||
items[9],
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { StashedPromptDisplay } from './StashedPromptDisplay.js';
|
||||
|
||||
describe('StashedPromptDisplay', () => {
|
||||
it('renders nothing when no stash exists', async () => {
|
||||
const { lastFrame, unmount } = await render(
|
||||
<StashedPromptDisplay stashedPrompt={null} />,
|
||||
);
|
||||
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('displays stash indicator when stash exists', async () => {
|
||||
const { lastFrame, unmount } = await render(
|
||||
<StashedPromptDisplay stashedPrompt="some stashed text" />,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Stashed (restores after submit)');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('does not display the stashed text content', async () => {
|
||||
const { lastFrame, unmount } = await render(
|
||||
<StashedPromptDisplay stashedPrompt="secret stashed content" />,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Stashed');
|
||||
expect(output).not.toContain('secret stashed content');
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Box, Text } from 'ink';
|
||||
|
||||
export interface StashedPromptDisplayProps {
|
||||
stashedPrompt: string | null;
|
||||
}
|
||||
|
||||
export const StashedPromptDisplay = ({
|
||||
stashedPrompt,
|
||||
}: StashedPromptDisplayProps) => {
|
||||
if (!stashedPrompt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box paddingLeft={2}>
|
||||
<Text dimColor>Stashed (restores after submit)</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -43,3 +43,18 @@ InputPrompt: Type your message or @path/to/file
|
||||
Footer
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Composer > Snapshots > matches snapshot with stashed prompt and queued messages 1`] = `
|
||||
"
|
||||
Stashed (restores after submit)
|
||||
Queued (press ↑ to edit):
|
||||
First queued message
|
||||
Second queued message
|
||||
|
||||
? for shortcuts
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
ApprovalModeIndicator: default StatusDisplay
|
||||
InputPrompt: Type your message or @path/to/file
|
||||
Footer
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -13,6 +13,7 @@ exports[`ShortcutsHelp > renders correctly in 'narrow' mode on 'linux' 1`] = `
|
||||
Alt+M raw markdown mode
|
||||
Ctrl+R reverse-search history
|
||||
Ctrl+X open external editor
|
||||
Alt+S stash prompt
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -29,6 +30,7 @@ exports[`ShortcutsHelp > renders correctly in 'narrow' mode on 'mac' 1`] = `
|
||||
Option+M raw markdown mode
|
||||
Ctrl+R reverse-search history
|
||||
Ctrl+X open external editor
|
||||
Option+S stash prompt
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -37,8 +39,8 @@ exports[`ShortcutsHelp > renders correctly in 'wide' mode on 'linux' 1`] = `
|
||||
Shortcuts See /help for more
|
||||
! shell mode Shift+Tab cycle mode Ctrl+V paste images
|
||||
@ select file or folder Ctrl+Y YOLO mode Alt+M raw markdown mode
|
||||
Double Esc clear & rewind Ctrl+R reverse-search history Ctrl+X open external editor
|
||||
Tab focus UI
|
||||
Double Esc clear & rewind Ctrl+R reverse-search history Alt+S stash prompt
|
||||
Tab focus UI Ctrl+X open external editor
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -47,7 +49,7 @@ exports[`ShortcutsHelp > renders correctly in 'wide' mode on 'mac' 1`] = `
|
||||
Shortcuts See /help for more
|
||||
! shell mode Shift+Tab cycle mode Ctrl+V paste images
|
||||
@ select file or folder Ctrl+Y YOLO mode Option+M raw markdown mode
|
||||
Double Esc clear & rewind Ctrl+R reverse-search history Ctrl+X open external editor
|
||||
Tab focus UI
|
||||
Double Esc clear & rewind Ctrl+R reverse-search history Option+S stash prompt
|
||||
Tab focus UI Ctrl+X open external editor
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -202,6 +202,7 @@ const MAC_ALT_KEY_CHARACTER_MAP: Record<string, string> = {
|
||||
'\u222B': 'b', // "∫" back one word
|
||||
'\u0192': 'f', // "ƒ" forward one word
|
||||
'\u00B5': 'm', // "µ" toggle markup view
|
||||
'\u00DF': 's', // "ß" stash prompt
|
||||
'\u03A9': 'z', // "Ω" Option+z
|
||||
'\u00B8': 'Z', // "¸" Option+Shift+z
|
||||
'\u2202': 'd', // "∂" delete word forward
|
||||
|
||||
@@ -71,6 +71,8 @@ export interface UIActions {
|
||||
handleDeleteSession: (session: SessionInfo) => Promise<void>;
|
||||
setQueueErrorMessage: (message: string | null) => void;
|
||||
popAllMessages: () => string | undefined;
|
||||
stashPrompt: (text: string) => void;
|
||||
popStashedPrompt: () => string | null;
|
||||
handleApiKeySubmit: (apiKey: string) => Promise<void>;
|
||||
handleApiKeyCancel: () => void;
|
||||
setBannerVisible: (visible: boolean) => void;
|
||||
|
||||
@@ -172,6 +172,7 @@ export interface UIState {
|
||||
activeHooks: ActiveHook[];
|
||||
messageQueue: string[];
|
||||
queueErrorMessage: string | null;
|
||||
stashedPrompt: string | null;
|
||||
showApprovalModeIndicator: ApprovalMode;
|
||||
allowPlanMode: boolean;
|
||||
// Quota-related state
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
export interface UsePromptStashReturn {
|
||||
/** The currently stashed prompt text, or null if nothing is stashed. */
|
||||
stashedPrompt: string | null;
|
||||
/** Save text to the stash, replacing any existing stash. No-op for empty strings. */
|
||||
stashPrompt: (text: string) => void;
|
||||
/** Pop and return the stashed prompt, clearing the stash. */
|
||||
popStashedPrompt: () => string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for managing a single stashed prompt.
|
||||
*
|
||||
* Allows users to temporarily set aside their current input and restore it
|
||||
* after the next submit.
|
||||
*/
|
||||
export function usePromptStash(): UsePromptStashReturn {
|
||||
const [stashedPrompt, setStashedPrompt] = useState<string | null>(null);
|
||||
|
||||
const stashPrompt = useCallback((text: string) => {
|
||||
if (text.length > 0) {
|
||||
setStashedPrompt(text);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const popStashedPrompt = useCallback(() => {
|
||||
const prompt = stashedPrompt;
|
||||
setStashedPrompt(null);
|
||||
return prompt;
|
||||
}, [stashedPrompt]);
|
||||
|
||||
return { stashedPrompt, stashPrompt, popStashedPrompt };
|
||||
}
|
||||
@@ -77,6 +77,7 @@ export enum Command {
|
||||
NEWLINE = 'input.newline',
|
||||
OPEN_EXTERNAL_EDITOR = 'input.openExternalEditor',
|
||||
PASTE_CLIPBOARD = 'input.paste',
|
||||
STASH_INPUT = 'input.stash',
|
||||
|
||||
// App Controls
|
||||
SHOW_ERROR_DETAILS = 'app.showErrorDetails',
|
||||
@@ -374,6 +375,8 @@ export const defaultKeyBindingConfig: KeyBindingConfig = new Map([
|
||||
],
|
||||
],
|
||||
|
||||
[Command.STASH_INPUT, [new KeyBinding('alt+s')]],
|
||||
|
||||
// App Controls
|
||||
[Command.SHOW_ERROR_DETAILS, [new KeyBinding('f12')]],
|
||||
[Command.SHOW_FULL_TODOS, [new KeyBinding('ctrl+t')]],
|
||||
@@ -491,6 +494,7 @@ export const commandCategories: readonly CommandCategory[] = [
|
||||
Command.NEWLINE,
|
||||
Command.OPEN_EXTERNAL_EDITOR,
|
||||
Command.PASTE_CLIPBOARD,
|
||||
Command.STASH_INPUT,
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -597,6 +601,8 @@ export const commandDescriptions: Readonly<Record<Command, string>> = {
|
||||
[Command.OPEN_EXTERNAL_EDITOR]:
|
||||
'Open the current prompt or the plan in an external editor.',
|
||||
[Command.PASTE_CLIPBOARD]: 'Paste from the clipboard.',
|
||||
[Command.STASH_INPUT]:
|
||||
'Stash the current input to temporarily set it aside. Restores on next submit.',
|
||||
|
||||
// App Controls
|
||||
[Command.SHOW_ERROR_DETAILS]: 'Toggle detailed error information.',
|
||||
|
||||
Reference in New Issue
Block a user