feat(cli): add UI integration for /btw command

This commit is contained in:
Mahima Shanware
2026-03-27 16:43:21 +00:00
committed by Mahima Shanware
parent 4bc7e2554f
commit 17b40b31b8
7 changed files with 164 additions and 3 deletions
+8
View File
@@ -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(),
+36 -3
View File
@@ -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 />