mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-07 09:32:59 -07:00
feat(cli): add UI integration for /btw command
This commit is contained in:
committed by
Mahima Shanware
parent
4bc7e2554f
commit
17b40b31b8
@@ -523,6 +523,13 @@ const baseMockUiState = {
|
||||
},
|
||||
hintMode: false,
|
||||
hintBuffer: '',
|
||||
btwState: {
|
||||
isActive: false,
|
||||
query: '',
|
||||
response: '',
|
||||
isStreaming: false,
|
||||
error: null,
|
||||
},
|
||||
bannerData: {
|
||||
defaultText: '',
|
||||
warningText: '',
|
||||
@@ -589,6 +596,7 @@ const mockUIActions: UIActions = {
|
||||
dismissBackgroundTask: vi.fn(),
|
||||
setActiveBackgroundTaskPid: vi.fn(),
|
||||
setIsBackgroundTaskListOpen: vi.fn(),
|
||||
dismissBtw: vi.fn(),
|
||||
setAuthContext: vi.fn(),
|
||||
onHintInput: vi.fn(),
|
||||
onHintBackspace: vi.fn(),
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
useLayoutEffect,
|
||||
useContext,
|
||||
} from 'react';
|
||||
import { type PartListUnion } from '@google/genai';
|
||||
import {
|
||||
type DOMElement,
|
||||
ResizeObserver,
|
||||
@@ -91,6 +92,7 @@ import {
|
||||
ApiKeyUpdatedEvent,
|
||||
type InjectionSource,
|
||||
startMemoryService,
|
||||
partListUnionToString,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { validateAuthMethod } from '../config/auth.js';
|
||||
import process from 'node:process';
|
||||
@@ -156,6 +158,7 @@ import { isWorkspaceTrusted } from '../config/trustedFolders.js';
|
||||
import { useSettings } from './contexts/SettingsContext.js';
|
||||
import { terminalCapabilityManager } from './utils/terminalCapabilityManager.js';
|
||||
import { useInputHistoryStore } from './hooks/useInputHistoryStore.js';
|
||||
import { useBtw } from './hooks/useBtw.js';
|
||||
import { useBanner } from './hooks/useBanner.js';
|
||||
import { useTerminalSetupPrompt } from './utils/terminalSetup.js';
|
||||
import { useHookDisplayState } from './hooks/useHookDisplayState.js';
|
||||
@@ -225,6 +228,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
const historyManager = useHistory({
|
||||
chatRecordingService: config.getGeminiClient()?.getChatRecordingService(),
|
||||
});
|
||||
const btw = useBtw(config.getGeminiClient());
|
||||
|
||||
useMemoryMonitor(historyManager);
|
||||
const isAlternateBuffer = config.getUseAlternateBuffer();
|
||||
@@ -1026,6 +1030,21 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
setCustomDialog,
|
||||
);
|
||||
|
||||
const handleSlashCommandWrapper = useCallback(
|
||||
async (cmd: PartListUnion) => {
|
||||
const submittedValue = partListUnionToString(cmd);
|
||||
const result = await handleSlashCommand(submittedValue);
|
||||
if (result && result.type === 'btw') {
|
||||
void btw.submitBtw(result.query);
|
||||
return {
|
||||
type: 'handled' as const,
|
||||
};
|
||||
}
|
||||
return result;
|
||||
},
|
||||
[handleSlashCommand, btw],
|
||||
);
|
||||
|
||||
const [authConsentRequest, setAuthConsentRequest] =
|
||||
useState<ConfirmationRequest | null>(null);
|
||||
const [permissionConfirmationRequest, setPermissionConfirmationRequest] =
|
||||
@@ -1187,7 +1206,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
config,
|
||||
settings,
|
||||
setDebugMessage,
|
||||
handleSlashCommand,
|
||||
handleSlashCommandWrapper,
|
||||
shellModeActive,
|
||||
getPreferredEditor,
|
||||
onAuthError,
|
||||
@@ -1355,7 +1374,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
slashCommands ?? [],
|
||||
);
|
||||
if (commandToExecute?.isSafeConcurrent) {
|
||||
void handleSlashCommand(submittedValue);
|
||||
void handleSlashCommandWrapper(submittedValue);
|
||||
addInput(submittedValue);
|
||||
return;
|
||||
}
|
||||
@@ -1407,7 +1426,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
addMessage,
|
||||
addInput,
|
||||
submitQuery,
|
||||
handleSlashCommand,
|
||||
handleSlashCommandWrapper,
|
||||
slashCommands,
|
||||
isMcpReady,
|
||||
streamingState,
|
||||
@@ -2491,6 +2510,13 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
hintMode:
|
||||
config.isModelSteeringEnabled() && isToolExecuting(pendingHistoryItems),
|
||||
hintBuffer: '',
|
||||
btwState: {
|
||||
isActive: btw.isActive,
|
||||
query: btw.query,
|
||||
response: btw.response,
|
||||
isStreaming: btw.isStreaming,
|
||||
error: btw.error,
|
||||
},
|
||||
}),
|
||||
[
|
||||
isThemeDialogOpen,
|
||||
@@ -2608,6 +2634,11 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
adminSettingsChanged,
|
||||
newAgents,
|
||||
showIsExpandableHint,
|
||||
btw.isActive,
|
||||
btw.query,
|
||||
btw.response,
|
||||
btw.isStreaming,
|
||||
btw.error,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -2667,6 +2698,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
dismissBackgroundTask,
|
||||
setActiveBackgroundTaskPid,
|
||||
setIsBackgroundTaskListOpen,
|
||||
dismissBtw: btw.dismissBtw,
|
||||
setAuthContext,
|
||||
onHintInput: () => {},
|
||||
onHintBackspace: () => {},
|
||||
@@ -2761,6 +2793,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
setIsBackgroundTaskListOpen,
|
||||
setAuthContext,
|
||||
setAccountSuspensionInfo,
|
||||
btw.dismissBtw,
|
||||
newAgents,
|
||||
config,
|
||||
historyManager,
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { MarkdownDisplay } from '../utils/MarkdownDisplay.js';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
import { GeminiRespondingSpinner } from './GeminiRespondingSpinner.js';
|
||||
|
||||
interface BtwDisplayProps {
|
||||
query: string;
|
||||
response: string;
|
||||
isStreaming: boolean;
|
||||
error: string | null;
|
||||
terminalWidth: number;
|
||||
}
|
||||
|
||||
export const BtwDisplay: React.FC<BtwDisplayProps> = ({
|
||||
query,
|
||||
response,
|
||||
isStreaming,
|
||||
error,
|
||||
terminalWidth,
|
||||
}) => {
|
||||
const { renderMarkdown } = useUIState();
|
||||
|
||||
if (!query) return null;
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor={theme.text.accent}
|
||||
paddingX={1}
|
||||
width={terminalWidth}
|
||||
marginBottom={1}
|
||||
>
|
||||
<Box flexDirection="row" justifyContent="space-between" marginBottom={1}>
|
||||
<Box>
|
||||
<Text color={theme.text.accent} bold>
|
||||
BY THE WAY
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={theme.text.secondary} italic>
|
||||
Press Esc, Enter or Space to dismiss
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box marginBottom={1}>
|
||||
<Text color={theme.text.secondary}>Q: </Text>
|
||||
<Text color={theme.text.secondary} italic>
|
||||
{query}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column">
|
||||
{error ? (
|
||||
<Text color={theme.status.error}>{error}</Text>
|
||||
) : (
|
||||
<Box flexDirection="row">
|
||||
<Box flexGrow={1}>
|
||||
<MarkdownDisplay
|
||||
text={response}
|
||||
isPending={isStreaming}
|
||||
terminalWidth={terminalWidth - 6}
|
||||
renderMarkdown={renderMarkdown}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{isStreaming && (
|
||||
<Box marginTop={1}>
|
||||
<GeminiRespondingSpinner />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -240,6 +240,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
setEmbeddedShellFocused,
|
||||
setShortcutsHelpVisible,
|
||||
toggleCleanUiDetailsVisible,
|
||||
dismissBtw,
|
||||
} = useUIActions();
|
||||
const {
|
||||
terminalWidth,
|
||||
@@ -248,6 +249,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
backgroundTasks,
|
||||
backgroundTaskHeight,
|
||||
shortcutsHelpVisible,
|
||||
btwState,
|
||||
} = useUIState();
|
||||
const [suppressCompletion, setSuppressCompletion] = useState(false);
|
||||
const { handlePress: registerPlainTabPress, resetCount: resetPlainTabPress } =
|
||||
@@ -682,6 +684,18 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
keyMatchers[Command.COLLAPSE_SUGGESTION](key) ||
|
||||
keyMatchers[Command.ACCEPT_SUGGESTION](key));
|
||||
|
||||
// Handle BTW dismissal
|
||||
if (btwState.isActive) {
|
||||
if (
|
||||
keyMatchers[Command.ESCAPE](key) ||
|
||||
keyMatchers[Command.SUBMIT](key) ||
|
||||
(key.sequence === ' ' && buffer.text.length === 0)
|
||||
) {
|
||||
dismissBtw();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset completion suppression if the user performs any action other than
|
||||
// history navigation or cursor movement.
|
||||
// We explicitly skip this if we are currently navigating suggestions.
|
||||
@@ -1371,6 +1385,8 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
keyMatchers,
|
||||
isHelpDismissKey,
|
||||
settings,
|
||||
btwState.isActive,
|
||||
dismissBtw,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ export interface UIActions {
|
||||
dismissBackgroundTask: (pid: number) => Promise<void>;
|
||||
setActiveBackgroundTaskPid: (pid: number) => void;
|
||||
setIsBackgroundTaskListOpen: (isOpen: boolean) => void;
|
||||
dismissBtw: () => void;
|
||||
setAuthContext: (context: { requiresRestart?: boolean }) => void;
|
||||
onHintInput: (char: string) => void;
|
||||
onHintBackspace: () => void;
|
||||
|
||||
@@ -218,6 +218,13 @@ export interface UIState {
|
||||
showIsExpandableHint: boolean;
|
||||
hintMode: boolean;
|
||||
hintBuffer: string;
|
||||
btwState: {
|
||||
isActive: boolean;
|
||||
query: string;
|
||||
response: string;
|
||||
isStreaming: boolean;
|
||||
error: string | null;
|
||||
};
|
||||
transientMessage: {
|
||||
text: string;
|
||||
type: TransientMessageType;
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useFlickerDetector } from '../hooks/useFlickerDetector.js';
|
||||
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
|
||||
import { CopyModeWarning } from '../components/CopyModeWarning.js';
|
||||
import { BackgroundTaskDisplay } from '../components/BackgroundTaskDisplay.js';
|
||||
import { BtwDisplay } from '../components/BtwDisplay.js';
|
||||
import { StreamingState } from '../types.js';
|
||||
import { useInputState } from '../contexts/InputContext.js';
|
||||
|
||||
@@ -66,6 +67,15 @@ export const DefaultAppLayout: React.FC = () => {
|
||||
width={uiState.terminalWidth}
|
||||
height={copyModeEnabled ? uiState.stableControlsHeight : undefined}
|
||||
>
|
||||
{uiState.btwState.isActive && (
|
||||
<BtwDisplay
|
||||
query={uiState.btwState.query}
|
||||
response={uiState.btwState.response}
|
||||
isStreaming={uiState.btwState.isStreaming}
|
||||
error={uiState.btwState.error}
|
||||
terminalWidth={uiState.terminalWidth}
|
||||
/>
|
||||
)}
|
||||
<Notifications />
|
||||
<CopyModeWarning />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user