feat(cli): Introduce toggleable shell mode with enhanced UI
- Implements a toggleable shell mode, removing the need to prefix every command with `!`. - Users can now enter and exit shell mode by typing `!` as the first character in an empty input prompt. - The input prompt visually indicates active shell mode with a distinct color and `! ` prefix. - Shell command history items (`user_shell`) are now visually differentiated from regular user messages. - This provides a cleaner and more streamlined user experience for frequent shell interactions. Fixes https://b.corp.google.com/issues/418509745
This commit is contained in:
parent
0d4e0fe647
commit
e4d978da7c
|
@ -61,6 +61,7 @@ export const App = ({
|
|||
const [themeError, setThemeError] = useState<string | null>(null);
|
||||
const [footerHeight, setFooterHeight] = useState<number>(0);
|
||||
const [corgiMode, setCorgiMode] = useState(false);
|
||||
const [shellModeActive, setShellModeActive] = useState(false);
|
||||
|
||||
const toggleCorgiMode = useCallback(() => {
|
||||
setCorgiMode((prev) => !prev);
|
||||
|
@ -152,10 +153,16 @@ export const App = ({
|
|||
(submittedValue: string) => {
|
||||
const trimmedValue = submittedValue.trim();
|
||||
if (trimmedValue.length > 0) {
|
||||
submitQuery(submittedValue);
|
||||
if (shellModeActive && !trimmedValue.startsWith('!')) {
|
||||
// TODO: Don't prefix (hack) and properly submit pass throughs to a dedicated hook:
|
||||
// https://b.corp.google.com/issues/418509745
|
||||
submitQuery(`!${trimmedValue}`);
|
||||
} else {
|
||||
submitQuery(trimmedValue);
|
||||
}
|
||||
}
|
||||
},
|
||||
[submitQuery],
|
||||
[submitQuery, shellModeActive],
|
||||
);
|
||||
|
||||
const userMessages = useMemo(
|
||||
|
@ -364,6 +371,8 @@ export const App = ({
|
|||
resetCompletion={completion.resetCompletionState}
|
||||
setEditorState={setEditorState}
|
||||
onClearScreen={handleClearScreen} // Added onClearScreen prop
|
||||
shellModeActive={shellModeActive}
|
||||
setShellModeActive={setShellModeActive}
|
||||
/>
|
||||
{completion.showSuggestions && (
|
||||
<Box>
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import React from 'react';
|
||||
import type { HistoryItem } from '../types.js';
|
||||
import { UserMessage } from './messages/UserMessage.js';
|
||||
import { UserShellMessage } from './messages/UserShellMessage.js';
|
||||
import { GeminiMessage } from './messages/GeminiMessage.js';
|
||||
import { InfoMessage } from './messages/InfoMessage.js';
|
||||
import { ErrorMessage } from './messages/ErrorMessage.js';
|
||||
|
@ -28,6 +29,7 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
|||
<Box flexDirection="column" key={item.id}>
|
||||
{/* Render standard message types */}
|
||||
{item.type === 'user' && <UserMessage text={item.text} />}
|
||||
{item.type === 'user_shell' && <UserShellMessage text={item.text} />}
|
||||
{item.type === 'gemini' && (
|
||||
<GeminiMessage
|
||||
text={item.text}
|
||||
|
|
|
@ -26,6 +26,8 @@ interface InputPromptProps {
|
|||
navigateSuggestionDown: () => void;
|
||||
setEditorState: (updater: (prevState: EditorState) => EditorState) => void;
|
||||
onClearScreen: () => void;
|
||||
shellModeActive: boolean;
|
||||
setShellModeActive: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export interface EditorState {
|
||||
|
@ -48,6 +50,8 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
|||
resetCompletion,
|
||||
setEditorState,
|
||||
onClearScreen,
|
||||
shellModeActive,
|
||||
setShellModeActive,
|
||||
}) => {
|
||||
const handleSubmit = useCallback(
|
||||
(submittedValue: string) => {
|
||||
|
@ -116,6 +120,11 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
|||
_currentText?: string,
|
||||
_cursorOffset?: number,
|
||||
) => {
|
||||
if (input === '!' && query === '' && !showSuggestions) {
|
||||
setShellModeActive(!shellModeActive);
|
||||
onChangeAndMoveCursor(''); // Clear the '!' from input
|
||||
return true;
|
||||
}
|
||||
if (showSuggestions) {
|
||||
if (key.upArrow) {
|
||||
navigateSuggestionUp();
|
||||
|
@ -186,12 +195,21 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
|||
inputHistory,
|
||||
setEditorState,
|
||||
onClearScreen,
|
||||
shellModeActive,
|
||||
setShellModeActive,
|
||||
onChangeAndMoveCursor,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<Box borderStyle="round" borderColor={Colors.AccentBlue} paddingX={1}>
|
||||
<Text color={Colors.AccentPurple}>> </Text>
|
||||
<Box
|
||||
borderStyle="round"
|
||||
borderColor={shellModeActive ? Colors.AccentYellow : Colors.AccentBlue}
|
||||
paddingX={1}
|
||||
>
|
||||
<Text color={shellModeActive ? Colors.AccentYellow : Colors.AccentPurple}>
|
||||
{shellModeActive ? '! ' : '> '}
|
||||
</Text>
|
||||
<Box flexGrow={1}>
|
||||
<MultilineTextEditor
|
||||
key={editorState.key.toString()}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { Colors } from '../../colors.js';
|
||||
|
||||
interface UserShellMessageProps {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export const UserShellMessage: React.FC<UserShellMessageProps> = ({ text }) => {
|
||||
// Remove leading '!' if present, as App.tsx adds it for the processor.
|
||||
const commandToDisplay = text.startsWith('!') ? text.substring(1) : text;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Text color={Colors.AccentCyan}>$ </Text>
|
||||
<Text>{commandToDisplay}</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
|
@ -38,7 +38,10 @@ export const useShellCommandProcessor = (
|
|||
const commandToExecute = rawQuery.trim().slice(1).trimStart();
|
||||
|
||||
const userMessageTimestamp = Date.now();
|
||||
addItemToHistory({ type: 'user', text: rawQuery }, userMessageTimestamp);
|
||||
addItemToHistory(
|
||||
{ type: 'user_shell', text: rawQuery },
|
||||
userMessageTimestamp,
|
||||
);
|
||||
|
||||
if (!commandToExecute) {
|
||||
addItemToHistory(
|
||||
|
|
|
@ -85,12 +85,18 @@ export type HistoryItemToolGroup = HistoryItemBase & {
|
|||
tools: IndividualToolCallDisplay[];
|
||||
};
|
||||
|
||||
export type HistoryItemUserShell = HistoryItemBase & {
|
||||
type: 'user_shell';
|
||||
text: string;
|
||||
};
|
||||
|
||||
// Using Omit<HistoryItem, 'id'> seems to have some issues with typescript's
|
||||
// type inference e.g. historyItem.type === 'tool_group' isn't auto-inferring that
|
||||
// 'tools' in historyItem.
|
||||
// Individually exported types extending HistoryItemBase
|
||||
export type HistoryItemWithoutId =
|
||||
| HistoryItemUser
|
||||
| HistoryItemUserShell
|
||||
| HistoryItemGemini
|
||||
| HistoryItemGeminiContent
|
||||
| HistoryItemInfo
|
||||
|
|
Loading…
Reference in New Issue