summaryrefslogtreecommitdiff
path: root/packages/cli
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli')
-rw-r--r--packages/cli/src/ui/App.tsx106
-rw-r--r--packages/cli/src/ui/components/InputPrompt.tsx51
2 files changed, 85 insertions, 72 deletions
diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx
index c7ed9a81..c022fb31 100644
--- a/packages/cli/src/ui/App.tsx
+++ b/packages/cli/src/ui/App.tsx
@@ -11,6 +11,7 @@ import {
measureElement,
Static,
Text,
+ useStdin,
useInput,
type Key as InkKeyType,
} from 'ink';
@@ -54,8 +55,10 @@ import { useLogger } from './hooks/useLogger.js';
import { StreamingContext } from './contexts/StreamingContext.js';
import { SessionStatsProvider } from './contexts/SessionContext.js';
import { useGitBranchName } from './hooks/useGitBranchName.js';
+import { useTextBuffer } from './components/shared/text-buffer.js';
+import * as fs from 'fs';
-const CTRL_C_PROMPT_DURATION_MS = 1000;
+const CTRL_EXIT_PROMPT_DURATION_MS = 1000;
interface AppProps {
config: Config;
@@ -98,6 +101,8 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
HistoryItem[] | null
>(null);
const ctrlCTimerRef = useRef<NodeJS.Timeout | null>(null);
+ const [ctrlDPressedOnce, setCtrlDPressedOnce] = useState(false);
+ const ctrlDTimerRef = useRef<NodeJS.Timeout | null>(null);
const errorCount = useMemo(
() => consoleMessages.filter((msg) => msg.type === 'error').length,
@@ -181,54 +186,84 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
setQuittingMessages,
);
+ const { rows: terminalHeight, columns: terminalWidth } = useTerminalSize();
+ const { stdin, setRawMode } = useStdin();
+ const isValidPath = useCallback((filePath: string): boolean => {
+ try {
+ return fs.existsSync(filePath) && fs.statSync(filePath).isFile();
+ } catch (_e) {
+ return false;
+ }
+ }, []);
+
+ const widthFraction = 0.9;
+ const inputWidth = Math.max(
+ 20,
+ Math.round(terminalWidth * widthFraction) - 3,
+ );
+ const suggestionsWidth = Math.max(60, Math.floor(terminalWidth * 0.8));
+
+ const buffer = useTextBuffer({
+ initialText: '',
+ viewport: { height: 10, width: inputWidth },
+ stdin,
+ setRawMode,
+ isValidPath,
+ });
+
+ const handleExit = useCallback(
+ (
+ pressedOnce: boolean,
+ setPressedOnce: (value: boolean) => void,
+ timerRef: React.MutableRefObject<NodeJS.Timeout | null>,
+ ) => {
+ if (pressedOnce) {
+ if (timerRef.current) {
+ clearTimeout(timerRef.current);
+ }
+ const quitCommand = slashCommands.find(
+ (cmd) => cmd.name === 'quit' || cmd.altName === 'exit',
+ );
+ if (quitCommand) {
+ quitCommand.action('quit', '', '');
+ } else {
+ process.exit(0);
+ }
+ } else {
+ setPressedOnce(true);
+ timerRef.current = setTimeout(() => {
+ setPressedOnce(false);
+ timerRef.current = null;
+ }, CTRL_EXIT_PROMPT_DURATION_MS);
+ }
+ },
+ [slashCommands],
+ );
+
useInput((input: string, key: InkKeyType) => {
if (key.ctrl && input === 'o') {
setShowErrorDetails((prev) => !prev);
refreshStatic();
} else if (key.ctrl && input === 't') {
- // Toggle showing tool descriptions
const newValue = !showToolDescriptions;
setShowToolDescriptions(newValue);
refreshStatic();
- // Re-execute the MCP command to show/hide descriptions
const mcpServers = config.getMcpServers();
if (Object.keys(mcpServers || {}).length > 0) {
- // Pass description flag based on the new value
handleSlashCommand(newValue ? '/mcp desc' : '/mcp nodesc');
}
} else if (key.ctrl && (input === 'c' || input === 'C')) {
- if (ctrlCPressedOnce) {
- if (ctrlCTimerRef.current) {
- clearTimeout(ctrlCTimerRef.current);
- }
- const quitCommand = slashCommands.find(
- (cmd) => cmd.name === 'quit' || cmd.altName === 'exit',
- );
- if (quitCommand) {
- quitCommand.action('quit', '', '');
- } else {
- process.exit(0);
- }
- } else {
- setCtrlCPressedOnce(true);
- ctrlCTimerRef.current = setTimeout(() => {
- setCtrlCPressedOnce(false);
- ctrlCTimerRef.current = null;
- }, CTRL_C_PROMPT_DURATION_MS);
+ handleExit(ctrlCPressedOnce, setCtrlCPressedOnce, ctrlCTimerRef);
+ } else if (key.ctrl && (input === 'd' || input === 'D')) {
+ if (buffer.text.length > 0) {
+ // Do nothing if there is text in the input.
+ return;
}
+ handleExit(ctrlDPressedOnce, setCtrlDPressedOnce, ctrlDTimerRef);
}
});
- useEffect(
- () => () => {
- if (ctrlCTimerRef.current) {
- clearTimeout(ctrlCTimerRef.current);
- }
- },
- [],
- );
-
useConsolePatcher({
onNewMessage: handleNewMessage,
debugMode: config.getDebugMode(),
@@ -324,7 +359,6 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
refreshStatic();
}, [clearItems, clearConsoleMessagesState, refreshStatic]);
- const { rows: terminalHeight, columns: terminalWidth } = useTerminalSize(); // Get terminalWidth
const mainControlsRef = useRef<DOMElement>(null);
const pendingHistoryItemRef = useRef<DOMElement>(null);
@@ -514,6 +548,10 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
<Text color={Colors.AccentYellow}>
Press Ctrl+C again to exit.
</Text>
+ ) : ctrlDPressedOnce ? (
+ <Text color={Colors.AccentYellow}>
+ Press Ctrl+D again to exit.
+ </Text>
) : (
<ContextSummaryDisplay
geminiMdFileCount={geminiMdFileCount}
@@ -540,7 +578,9 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
{isInputActive && (
<InputPrompt
- widthFraction={0.9}
+ buffer={buffer}
+ inputWidth={inputWidth}
+ suggestionsWidth={suggestionsWidth}
onSubmit={handleFinalSubmit}
userMessages={userMessages}
onClearScreen={handleClearScreen}
diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx
index 534d7112..c4177c00 100644
--- a/packages/cli/src/ui/components/InputPrompt.tsx
+++ b/packages/cli/src/ui/components/InputPrompt.tsx
@@ -4,15 +4,13 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import * as fs from 'fs';
import React, { useCallback, useEffect, useState } from 'react';
-import { Text, Box, useInput, useStdin } from 'ink';
+import { Text, Box, useInput } from 'ink';
import { Colors } from '../colors.js';
import { SuggestionsDisplay } from './SuggestionsDisplay.js';
import { useInputHistory } from '../hooks/useInputHistory.js';
-import { useTextBuffer, cpSlice, cpLen } from './shared/text-buffer.js';
+import { cpSlice, cpLen, TextBuffer } from './shared/text-buffer.js';
import chalk from 'chalk';
-import { useTerminalSize } from '../hooks/useTerminalSize.js';
import stringWidth from 'string-width';
import process from 'node:process';
import { useCompletion } from '../hooks/useCompletion.js';
@@ -21,60 +19,36 @@ import { SlashCommand } from '../hooks/slashCommandProcessor.js';
import { Config } from '@gemini-cli/core';
export interface InputPromptProps {
+ buffer: TextBuffer;
onSubmit: (value: string) => void;
userMessages: readonly string[];
onClearScreen: () => void;
config: Config; // Added config for useCompletion
slashCommands: SlashCommand[]; // Added slashCommands for useCompletion
placeholder?: string;
- height?: number; // Visible height of the editor area
focus?: boolean;
- widthFraction: number;
+ inputWidth: number;
+ suggestionsWidth: number;
shellModeActive: boolean;
setShellModeActive: (value: boolean) => void;
}
export const InputPrompt: React.FC<InputPromptProps> = ({
+ buffer,
onSubmit,
userMessages,
onClearScreen,
config,
slashCommands,
placeholder = ' Type your message or @path/to/file',
- height = 10,
focus = true,
- widthFraction,
+ inputWidth,
+ suggestionsWidth,
shellModeActive,
setShellModeActive,
}) => {
- const terminalSize = useTerminalSize();
- const padding = 3;
- const effectiveWidth = Math.max(
- 20,
- Math.round(terminalSize.columns * widthFraction) - padding,
- );
- const suggestionsWidth = Math.max(60, Math.floor(terminalSize.columns * 0.8));
-
const [justNavigatedHistory, setJustNavigatedHistory] = useState(false);
- const { stdin, setRawMode } = useStdin();
-
- const isValidPath = useCallback((filePath: string): boolean => {
- try {
- return fs.existsSync(filePath) && fs.statSync(filePath).isFile();
- } catch (_e) {
- return false;
- }
- }, []);
-
- const buffer = useTextBuffer({
- initialText: '',
- viewport: { height, width: effectiveWidth },
- stdin,
- setRawMode,
- isValidPath,
- });
-
const completion = useCompletion(
buffer.text,
config.getTargetDir(),
@@ -370,11 +344,10 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
) : (
linesToRender.map((lineText, visualIdxInRenderedSet) => {
const cursorVisualRow = cursorVisualRowAbsolute - scrollVisualRow;
- let display = cpSlice(lineText, 0, effectiveWidth);
+ let display = cpSlice(lineText, 0, inputWidth);
const currentVisualWidth = stringWidth(display);
- if (currentVisualWidth < effectiveWidth) {
- display =
- display + ' '.repeat(effectiveWidth - currentVisualWidth);
+ if (currentVisualWidth < inputWidth) {
+ display = display + ' '.repeat(inputWidth - currentVisualWidth);
}
if (visualIdxInRenderedSet === cursorVisualRow) {
@@ -394,7 +367,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
cpSlice(display, relativeVisualColForHighlight + 1);
} else if (
relativeVisualColForHighlight === cpLen(display) &&
- cpLen(display) === effectiveWidth
+ cpLen(display) === inputWidth
) {
display = display + chalk.inverse(' ');
}