|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import { useCallback, useEffect, useMemo, useState, useRef } from 'react'; |
|
|
import { |
|
|
Box, |
|
|
DOMElement, |
|
|
measureElement, |
|
|
Static, |
|
|
Text, |
|
|
useStdin, |
|
|
useStdout, |
|
|
useInput, |
|
|
type Key as InkKeyType, |
|
|
} from 'ink'; |
|
|
import { StreamingState, type HistoryItem, MessageType } from './types.js'; |
|
|
import { useTerminalSize } from './hooks/useTerminalSize.js'; |
|
|
import { useGeminiStream } from './hooks/useGeminiStream.js'; |
|
|
import { useLoadingIndicator } from './hooks/useLoadingIndicator.js'; |
|
|
import { useThemeCommand } from './hooks/useThemeCommand.js'; |
|
|
import { useAuthCommand } from './hooks/useAuthCommand.js'; |
|
|
import { useEditorSettings } from './hooks/useEditorSettings.js'; |
|
|
import { useSlashCommandProcessor } from './hooks/slashCommandProcessor.js'; |
|
|
import { useAutoAcceptIndicator } from './hooks/useAutoAcceptIndicator.js'; |
|
|
import { useConsoleMessages } from './hooks/useConsoleMessages.js'; |
|
|
import { Header } from './components/Header.js'; |
|
|
import { LoadingIndicator } from './components/LoadingIndicator.js'; |
|
|
import { AutoAcceptIndicator } from './components/AutoAcceptIndicator.js'; |
|
|
import { ShellModeIndicator } from './components/ShellModeIndicator.js'; |
|
|
import { InputPrompt } from './components/InputPrompt.js'; |
|
|
import { Footer } from './components/Footer.js'; |
|
|
import { ThemeDialog } from './components/ThemeDialog.js'; |
|
|
import { AuthDialog } from './components/AuthDialog.js'; |
|
|
import { AuthInProgress } from './components/AuthInProgress.js'; |
|
|
import { EditorSettingsDialog } from './components/EditorSettingsDialog.js'; |
|
|
import { Colors } from './colors.js'; |
|
|
import { Help } from './components/Help.js'; |
|
|
import { loadHierarchicalGeminiMemory } from '../config/config.js'; |
|
|
import { LoadedSettings } from '../config/settings.js'; |
|
|
import { Tips } from './components/Tips.js'; |
|
|
import { useConsolePatcher } from './components/ConsolePatcher.js'; |
|
|
import { DetailedMessagesDisplay } from './components/DetailedMessagesDisplay.js'; |
|
|
import { HistoryItemDisplay } from './components/HistoryItemDisplay.js'; |
|
|
import { ContextSummaryDisplay } from './components/ContextSummaryDisplay.js'; |
|
|
import { useHistory } from './hooks/useHistoryManager.js'; |
|
|
import process from 'node:process'; |
|
|
import { |
|
|
getErrorMessage, |
|
|
type Config, |
|
|
getAllGeminiMdFilenames, |
|
|
ApprovalMode, |
|
|
isEditorAvailable, |
|
|
EditorType, |
|
|
} from '@google/gemini-cli-core'; |
|
|
import { validateAuthMethod } from '../config/auth.js'; |
|
|
import { useLogger } from './hooks/useLogger.js'; |
|
|
import { StreamingContext } from './contexts/StreamingContext.js'; |
|
|
import { |
|
|
SessionStatsProvider, |
|
|
useSessionStats, |
|
|
} from './contexts/SessionContext.js'; |
|
|
import { useGitBranchName } from './hooks/useGitBranchName.js'; |
|
|
import { useBracketedPaste } from './hooks/useBracketedPaste.js'; |
|
|
import { useTextBuffer } from './components/shared/text-buffer.js'; |
|
|
import * as fs from 'fs'; |
|
|
import { UpdateNotification } from './components/UpdateNotification.js'; |
|
|
import { checkForUpdates } from './utils/updateCheck.js'; |
|
|
import ansiEscapes from 'ansi-escapes'; |
|
|
import { OverflowProvider } from './contexts/OverflowContext.js'; |
|
|
import { ShowMoreLines } from './components/ShowMoreLines.js'; |
|
|
import { PrivacyNotice } from './privacy/PrivacyNotice.js'; |
|
|
|
|
|
const CTRL_EXIT_PROMPT_DURATION_MS = 1000; |
|
|
|
|
|
interface AppProps { |
|
|
config: Config; |
|
|
settings: LoadedSettings; |
|
|
startupWarnings?: string[]; |
|
|
} |
|
|
|
|
|
export const AppWrapper = (props: AppProps) => ( |
|
|
<SessionStatsProvider> |
|
|
<App {...props} /> |
|
|
</SessionStatsProvider> |
|
|
); |
|
|
|
|
|
const App = ({ config, settings, startupWarnings = [] }: AppProps) => { |
|
|
useBracketedPaste(); |
|
|
const [updateMessage, setUpdateMessage] = useState<string | null>(null); |
|
|
const { stdout } = useStdout(); |
|
|
|
|
|
useEffect(() => { |
|
|
checkForUpdates().then(setUpdateMessage); |
|
|
}, []); |
|
|
|
|
|
const { history, addItem, clearItems, loadHistory } = useHistory(); |
|
|
const { |
|
|
consoleMessages, |
|
|
handleNewMessage, |
|
|
clearConsoleMessages: clearConsoleMessagesState, |
|
|
} = useConsoleMessages(); |
|
|
const { stats: sessionStats } = useSessionStats(); |
|
|
const [staticNeedsRefresh, setStaticNeedsRefresh] = useState(false); |
|
|
const [staticKey, setStaticKey] = useState(0); |
|
|
const refreshStatic = useCallback(() => { |
|
|
stdout.write(ansiEscapes.clearTerminal); |
|
|
setStaticKey((prev) => prev + 1); |
|
|
}, [setStaticKey, stdout]); |
|
|
|
|
|
const [geminiMdFileCount, setGeminiMdFileCount] = useState<number>(0); |
|
|
const [debugMessage, setDebugMessage] = useState<string>(''); |
|
|
const [showHelp, setShowHelp] = useState<boolean>(false); |
|
|
const [themeError, setThemeError] = useState<string | null>(null); |
|
|
const [authError, setAuthError] = useState<string | null>(null); |
|
|
const [editorError, setEditorError] = useState<string | null>(null); |
|
|
const [footerHeight, setFooterHeight] = useState<number>(0); |
|
|
const [corgiMode, setCorgiMode] = useState(false); |
|
|
const [currentModel, setCurrentModel] = useState(config.getModel()); |
|
|
const [shellModeActive, setShellModeActive] = useState(false); |
|
|
const [showErrorDetails, setShowErrorDetails] = useState<boolean>(false); |
|
|
const [showToolDescriptions, setShowToolDescriptions] = |
|
|
useState<boolean>(false); |
|
|
const [ctrlCPressedOnce, setCtrlCPressedOnce] = useState(false); |
|
|
const [quittingMessages, setQuittingMessages] = useState< |
|
|
HistoryItem[] | null |
|
|
>(null); |
|
|
const ctrlCTimerRef = useRef<NodeJS.Timeout | null>(null); |
|
|
const [ctrlDPressedOnce, setCtrlDPressedOnce] = useState(false); |
|
|
const ctrlDTimerRef = useRef<NodeJS.Timeout | null>(null); |
|
|
const [constrainHeight, setConstrainHeight] = useState<boolean>(true); |
|
|
const [showPrivacyNotice, setShowPrivacyNotice] = useState<boolean>(false); |
|
|
|
|
|
const openPrivacyNotice = useCallback(() => { |
|
|
setShowPrivacyNotice(true); |
|
|
}, []); |
|
|
|
|
|
const errorCount = useMemo( |
|
|
() => consoleMessages.filter((msg) => msg.type === 'error').length, |
|
|
[consoleMessages], |
|
|
); |
|
|
|
|
|
const { |
|
|
isThemeDialogOpen, |
|
|
openThemeDialog, |
|
|
handleThemeSelect, |
|
|
handleThemeHighlight, |
|
|
} = useThemeCommand(settings, setThemeError, addItem); |
|
|
|
|
|
const { |
|
|
isAuthDialogOpen, |
|
|
openAuthDialog, |
|
|
handleAuthSelect, |
|
|
handleAuthHighlight, |
|
|
isAuthenticating, |
|
|
cancelAuthentication, |
|
|
} = useAuthCommand(settings, setAuthError, config); |
|
|
|
|
|
useEffect(() => { |
|
|
if (settings.merged.selectedAuthType) { |
|
|
const error = validateAuthMethod(settings.merged.selectedAuthType); |
|
|
if (error) { |
|
|
setAuthError(error); |
|
|
openAuthDialog(); |
|
|
} |
|
|
} |
|
|
}, [settings.merged.selectedAuthType, openAuthDialog, setAuthError]); |
|
|
|
|
|
const { |
|
|
isEditorDialogOpen, |
|
|
openEditorDialog, |
|
|
handleEditorSelect, |
|
|
exitEditorDialog, |
|
|
} = useEditorSettings(settings, setEditorError, addItem); |
|
|
|
|
|
const toggleCorgiMode = useCallback(() => { |
|
|
setCorgiMode((prev) => !prev); |
|
|
}, []); |
|
|
|
|
|
const performMemoryRefresh = useCallback(async () => { |
|
|
addItem( |
|
|
{ |
|
|
type: MessageType.INFO, |
|
|
text: 'Refreshing hierarchical memory (GEMINI.md or other context files)...', |
|
|
}, |
|
|
Date.now(), |
|
|
); |
|
|
try { |
|
|
const { memoryContent, fileCount } = await loadHierarchicalGeminiMemory( |
|
|
process.cwd(), |
|
|
config.getDebugMode(), |
|
|
config.getFileService(), |
|
|
config.getExtensionContextFilePaths(), |
|
|
); |
|
|
config.setUserMemory(memoryContent); |
|
|
config.setGeminiMdFileCount(fileCount); |
|
|
setGeminiMdFileCount(fileCount); |
|
|
|
|
|
addItem( |
|
|
{ |
|
|
type: MessageType.INFO, |
|
|
text: `Memory refreshed successfully. ${memoryContent.length > 0 ? `Loaded ${memoryContent.length} characters from ${fileCount} file(s).` : 'No memory content found.'}`, |
|
|
}, |
|
|
Date.now(), |
|
|
); |
|
|
if (config.getDebugMode()) { |
|
|
console.log( |
|
|
`[DEBUG] Refreshed memory content in config: ${memoryContent.substring(0, 200)}...`, |
|
|
); |
|
|
} |
|
|
} catch (error) { |
|
|
const errorMessage = getErrorMessage(error); |
|
|
addItem( |
|
|
{ |
|
|
type: MessageType.ERROR, |
|
|
text: `Error refreshing memory: ${errorMessage}`, |
|
|
}, |
|
|
Date.now(), |
|
|
); |
|
|
console.error('Error refreshing memory:', error); |
|
|
} |
|
|
}, [config, addItem]); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
const checkModelChange = () => { |
|
|
const configModel = config.getModel(); |
|
|
if (configModel !== currentModel) { |
|
|
setCurrentModel(configModel); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
checkModelChange(); |
|
|
const interval = setInterval(checkModelChange, 1000); |
|
|
|
|
|
return () => clearInterval(interval); |
|
|
}, [config, currentModel]); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
const flashFallbackHandler = async ( |
|
|
currentModel: string, |
|
|
fallbackModel: string, |
|
|
): Promise<boolean> => { |
|
|
|
|
|
addItem( |
|
|
{ |
|
|
type: MessageType.INFO, |
|
|
text: `⚡ Slow response times detected. Automatically switching from ${currentModel} to ${fallbackModel} for faster responses for the remainder of this session. |
|
|
⚡ To avoid this you can either upgrade to Standard tier. See: https://goo.gle/set-up-gemini-code-assist |
|
|
⚡ Or you can utilize a Gemini API Key. See: https://goo.gle/gemini-cli-docs-auth#gemini-api-key |
|
|
⚡ You can switch authentication methods by typing /auth`, |
|
|
}, |
|
|
Date.now(), |
|
|
); |
|
|
return true; |
|
|
}; |
|
|
|
|
|
config.setFlashFallbackHandler(flashFallbackHandler); |
|
|
}, [config, addItem]); |
|
|
|
|
|
const { |
|
|
handleSlashCommand, |
|
|
slashCommands, |
|
|
pendingHistoryItems: pendingSlashCommandHistoryItems, |
|
|
} = useSlashCommandProcessor( |
|
|
config, |
|
|
settings, |
|
|
history, |
|
|
addItem, |
|
|
clearItems, |
|
|
loadHistory, |
|
|
refreshStatic, |
|
|
setShowHelp, |
|
|
setDebugMessage, |
|
|
openThemeDialog, |
|
|
openAuthDialog, |
|
|
openEditorDialog, |
|
|
performMemoryRefresh, |
|
|
toggleCorgiMode, |
|
|
showToolDescriptions, |
|
|
setQuittingMessages, |
|
|
openPrivacyNotice, |
|
|
); |
|
|
const pendingHistoryItems = [...pendingSlashCommandHistoryItems]; |
|
|
|
|
|
const { rows: terminalHeight, columns: terminalWidth } = useTerminalSize(); |
|
|
const isInitialMount = useRef(true); |
|
|
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.floor(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) => { |
|
|
let enteringConstrainHeightMode = false; |
|
|
if (!constrainHeight) { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
enteringConstrainHeightMode = true; |
|
|
setConstrainHeight(true); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (pendingHistoryItemRef.current && pendingHistoryItems.length > 0) { |
|
|
const pendingItemDimensions = measureElement( |
|
|
pendingHistoryItemRef.current, |
|
|
); |
|
|
if (pendingItemDimensions.height > availableTerminalHeight) { |
|
|
refreshStatic(); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
if (key.ctrl && input === 'o') { |
|
|
setShowErrorDetails((prev) => !prev); |
|
|
} else if (key.ctrl && input === 't') { |
|
|
const newValue = !showToolDescriptions; |
|
|
setShowToolDescriptions(newValue); |
|
|
|
|
|
const mcpServers = config.getMcpServers(); |
|
|
if (Object.keys(mcpServers || {}).length > 0) { |
|
|
handleSlashCommand(newValue ? '/mcp desc' : '/mcp nodesc'); |
|
|
} |
|
|
} else if (key.ctrl && (input === 'c' || input === 'C')) { |
|
|
handleExit(ctrlCPressedOnce, setCtrlCPressedOnce, ctrlCTimerRef); |
|
|
} else if (key.ctrl && (input === 'd' || input === 'D')) { |
|
|
if (buffer.text.length > 0) { |
|
|
|
|
|
return; |
|
|
} |
|
|
handleExit(ctrlDPressedOnce, setCtrlDPressedOnce, ctrlDTimerRef); |
|
|
} else if (key.ctrl && input === 's' && !enteringConstrainHeightMode) { |
|
|
setConstrainHeight(false); |
|
|
} |
|
|
}); |
|
|
|
|
|
useConsolePatcher({ |
|
|
onNewMessage: handleNewMessage, |
|
|
debugMode: config.getDebugMode(), |
|
|
}); |
|
|
|
|
|
useEffect(() => { |
|
|
if (config) { |
|
|
setGeminiMdFileCount(config.getGeminiMdFileCount()); |
|
|
} |
|
|
}, [config]); |
|
|
|
|
|
const getPreferredEditor = useCallback(() => { |
|
|
const editorType = settings.merged.preferredEditor; |
|
|
const isValidEditor = isEditorAvailable(editorType); |
|
|
if (!isValidEditor) { |
|
|
openEditorDialog(); |
|
|
return; |
|
|
} |
|
|
return editorType as EditorType; |
|
|
}, [settings, openEditorDialog]); |
|
|
|
|
|
const onAuthError = useCallback(() => { |
|
|
setAuthError('reauth required'); |
|
|
openAuthDialog(); |
|
|
}, [openAuthDialog, setAuthError]); |
|
|
|
|
|
const { |
|
|
streamingState, |
|
|
submitQuery, |
|
|
initError, |
|
|
pendingHistoryItems: pendingGeminiHistoryItems, |
|
|
thought, |
|
|
} = useGeminiStream( |
|
|
config.getGeminiClient(), |
|
|
history, |
|
|
addItem, |
|
|
setShowHelp, |
|
|
config, |
|
|
setDebugMessage, |
|
|
handleSlashCommand, |
|
|
shellModeActive, |
|
|
getPreferredEditor, |
|
|
onAuthError, |
|
|
performMemoryRefresh, |
|
|
); |
|
|
pendingHistoryItems.push(...pendingGeminiHistoryItems); |
|
|
const { elapsedTime, currentLoadingPhrase } = |
|
|
useLoadingIndicator(streamingState); |
|
|
const showAutoAcceptIndicator = useAutoAcceptIndicator({ config }); |
|
|
|
|
|
const handleFinalSubmit = useCallback( |
|
|
(submittedValue: string) => { |
|
|
const trimmedValue = submittedValue.trim(); |
|
|
if (trimmedValue.length > 0) { |
|
|
submitQuery(trimmedValue); |
|
|
} |
|
|
}, |
|
|
[submitQuery], |
|
|
); |
|
|
|
|
|
const logger = useLogger(); |
|
|
const [userMessages, setUserMessages] = useState<string[]>([]); |
|
|
|
|
|
useEffect(() => { |
|
|
const fetchUserMessages = async () => { |
|
|
const pastMessagesRaw = (await logger?.getPreviousUserMessages()) || []; |
|
|
|
|
|
const currentSessionUserMessages = history |
|
|
.filter( |
|
|
(item): item is HistoryItem & { type: 'user'; text: string } => |
|
|
item.type === 'user' && |
|
|
typeof item.text === 'string' && |
|
|
item.text.trim() !== '', |
|
|
) |
|
|
.map((item) => item.text) |
|
|
.reverse(); |
|
|
|
|
|
|
|
|
const combinedMessages = [ |
|
|
...currentSessionUserMessages, |
|
|
...pastMessagesRaw, |
|
|
]; |
|
|
|
|
|
|
|
|
const deduplicatedMessages: string[] = []; |
|
|
if (combinedMessages.length > 0) { |
|
|
deduplicatedMessages.push(combinedMessages[0]); |
|
|
for (let i = 1; i < combinedMessages.length; i++) { |
|
|
if (combinedMessages[i] !== combinedMessages[i - 1]) { |
|
|
deduplicatedMessages.push(combinedMessages[i]); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
setUserMessages(deduplicatedMessages.reverse()); |
|
|
}; |
|
|
fetchUserMessages(); |
|
|
}, [history, logger]); |
|
|
|
|
|
const isInputActive = streamingState === StreamingState.Idle && !initError; |
|
|
|
|
|
const handleClearScreen = useCallback(() => { |
|
|
clearItems(); |
|
|
clearConsoleMessagesState(); |
|
|
console.clear(); |
|
|
refreshStatic(); |
|
|
}, [clearItems, clearConsoleMessagesState, refreshStatic]); |
|
|
|
|
|
const mainControlsRef = useRef<DOMElement>(null); |
|
|
const pendingHistoryItemRef = useRef<DOMElement>(null); |
|
|
|
|
|
useEffect(() => { |
|
|
if (mainControlsRef.current) { |
|
|
const fullFooterMeasurement = measureElement(mainControlsRef.current); |
|
|
setFooterHeight(fullFooterMeasurement.height); |
|
|
} |
|
|
}, [terminalHeight, consoleMessages, showErrorDetails]); |
|
|
|
|
|
const staticExtraHeight = 3; |
|
|
const availableTerminalHeight = useMemo( |
|
|
() => terminalHeight - footerHeight - staticExtraHeight, |
|
|
[terminalHeight, footerHeight], |
|
|
); |
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
if (isInitialMount.current) { |
|
|
isInitialMount.current = false; |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
const handler = setTimeout(() => { |
|
|
setStaticNeedsRefresh(false); |
|
|
refreshStatic(); |
|
|
}, 300); |
|
|
|
|
|
return () => { |
|
|
clearTimeout(handler); |
|
|
}; |
|
|
}, [terminalWidth, terminalHeight, refreshStatic]); |
|
|
|
|
|
useEffect(() => { |
|
|
if (!pendingHistoryItems.length) { |
|
|
return; |
|
|
} |
|
|
|
|
|
const pendingItemDimensions = measureElement( |
|
|
pendingHistoryItemRef.current!, |
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (pendingItemDimensions.height > availableTerminalHeight) { |
|
|
setStaticNeedsRefresh(true); |
|
|
} |
|
|
}, [pendingHistoryItems.length, availableTerminalHeight, streamingState]); |
|
|
|
|
|
useEffect(() => { |
|
|
if (streamingState === StreamingState.Idle && staticNeedsRefresh) { |
|
|
setStaticNeedsRefresh(false); |
|
|
refreshStatic(); |
|
|
} |
|
|
}, [streamingState, refreshStatic, staticNeedsRefresh]); |
|
|
|
|
|
const filteredConsoleMessages = useMemo(() => { |
|
|
if (config.getDebugMode()) { |
|
|
return consoleMessages; |
|
|
} |
|
|
return consoleMessages.filter((msg) => msg.type !== 'debug'); |
|
|
}, [consoleMessages, config]); |
|
|
|
|
|
const branchName = useGitBranchName(config.getTargetDir()); |
|
|
|
|
|
const contextFileNames = useMemo(() => { |
|
|
const fromSettings = settings.merged.contextFileName; |
|
|
if (fromSettings) { |
|
|
return Array.isArray(fromSettings) ? fromSettings : [fromSettings]; |
|
|
} |
|
|
return getAllGeminiMdFilenames(); |
|
|
}, [settings.merged.contextFileName]); |
|
|
|
|
|
if (quittingMessages) { |
|
|
return ( |
|
|
<Box flexDirection="column" marginBottom={1}> |
|
|
{quittingMessages.map((item) => ( |
|
|
<HistoryItemDisplay |
|
|
key={item.id} |
|
|
availableTerminalHeight={ |
|
|
constrainHeight ? availableTerminalHeight : undefined |
|
|
} |
|
|
terminalWidth={terminalWidth} |
|
|
item={item} |
|
|
isPending={false} |
|
|
config={config} |
|
|
/> |
|
|
))} |
|
|
</Box> |
|
|
); |
|
|
} |
|
|
const mainAreaWidth = Math.floor(terminalWidth * 0.9); |
|
|
const debugConsoleMaxHeight = Math.floor(Math.max(terminalHeight * 0.2, 5)); |
|
|
|
|
|
|
|
|
const staticAreaMaxItemHeight = Math.max(terminalHeight * 4, 100); |
|
|
return ( |
|
|
<StreamingContext.Provider value={streamingState}> |
|
|
<Box flexDirection="column" marginBottom={1} width="90%"> |
|
|
{/* |
|
|
* The Static component is an Ink intrinsic in which there can only be 1 per application. |
|
|
* Because of this restriction we're hacking it slightly by having a 'header' item here to |
|
|
* ensure that it's statically rendered. |
|
|
* |
|
|
* Background on the Static Item: Anything in the Static component is written a single time |
|
|
* to the console. Think of it like doing a console.log and then never using ANSI codes to |
|
|
* clear that content ever again. Effectively it has a moving frame that every time new static |
|
|
* content is set it'll flush content to the terminal and move the area which it's "clearing" |
|
|
* down a notch. Without Static the area which gets erased and redrawn continuously grows. |
|
|
*/} |
|
|
<Static |
|
|
key={staticKey} |
|
|
items={[ |
|
|
<Box flexDirection="column" key="header"> |
|
|
<Header terminalWidth={terminalWidth} /> |
|
|
<Tips config={config} /> |
|
|
{updateMessage && <UpdateNotification message={updateMessage} />} |
|
|
</Box>, |
|
|
...history.map((h) => ( |
|
|
<HistoryItemDisplay |
|
|
terminalWidth={mainAreaWidth} |
|
|
availableTerminalHeight={staticAreaMaxItemHeight} |
|
|
key={h.id} |
|
|
item={h} |
|
|
isPending={false} |
|
|
config={config} |
|
|
/> |
|
|
)), |
|
|
]} |
|
|
> |
|
|
{(item) => item} |
|
|
</Static> |
|
|
<OverflowProvider> |
|
|
<Box ref={pendingHistoryItemRef} flexDirection="column"> |
|
|
{pendingHistoryItems.map((item, i) => ( |
|
|
<HistoryItemDisplay |
|
|
key={i} |
|
|
availableTerminalHeight={ |
|
|
constrainHeight ? availableTerminalHeight : undefined |
|
|
} |
|
|
terminalWidth={mainAreaWidth} |
|
|
// TODO(taehykim): It seems like references to ids aren't necessary in |
|
|
// HistoryItemDisplay. Refactor later. Use a fake id for now. |
|
|
item={{ ...item, id: 0 }} |
|
|
isPending={true} |
|
|
config={config} |
|
|
isFocused={!isEditorDialogOpen} |
|
|
/> |
|
|
))} |
|
|
<ShowMoreLines constrainHeight={constrainHeight} /> |
|
|
</Box> |
|
|
</OverflowProvider> |
|
|
|
|
|
{showHelp && <Help commands={slashCommands} />} |
|
|
|
|
|
<Box flexDirection="column" ref={mainControlsRef}> |
|
|
{startupWarnings.length > 0 && ( |
|
|
<Box |
|
|
borderStyle="round" |
|
|
borderColor={Colors.AccentYellow} |
|
|
paddingX={1} |
|
|
marginY={1} |
|
|
flexDirection="column" |
|
|
> |
|
|
{startupWarnings.map((warning, index) => ( |
|
|
<Text key={index} color={Colors.AccentYellow}> |
|
|
{warning} |
|
|
</Text> |
|
|
))} |
|
|
</Box> |
|
|
)} |
|
|
|
|
|
{isThemeDialogOpen ? ( |
|
|
<Box flexDirection="column"> |
|
|
{themeError && ( |
|
|
<Box marginBottom={1}> |
|
|
<Text color={Colors.AccentRed}>{themeError}</Text> |
|
|
</Box> |
|
|
)} |
|
|
<ThemeDialog |
|
|
onSelect={handleThemeSelect} |
|
|
onHighlight={handleThemeHighlight} |
|
|
settings={settings} |
|
|
availableTerminalHeight={ |
|
|
constrainHeight |
|
|
? terminalHeight - staticExtraHeight |
|
|
: undefined |
|
|
} |
|
|
terminalWidth={mainAreaWidth} |
|
|
/> |
|
|
</Box> |
|
|
) : isAuthenticating ? ( |
|
|
<AuthInProgress |
|
|
onTimeout={() => { |
|
|
setAuthError('Authentication timed out. Please try again.'); |
|
|
cancelAuthentication(); |
|
|
openAuthDialog(); |
|
|
}} |
|
|
/> |
|
|
) : isAuthDialogOpen ? ( |
|
|
<Box flexDirection="column"> |
|
|
<AuthDialog |
|
|
onSelect={handleAuthSelect} |
|
|
onHighlight={handleAuthHighlight} |
|
|
settings={settings} |
|
|
initialErrorMessage={authError} |
|
|
/> |
|
|
</Box> |
|
|
) : isEditorDialogOpen ? ( |
|
|
<Box flexDirection="column"> |
|
|
{editorError && ( |
|
|
<Box marginBottom={1}> |
|
|
<Text color={Colors.AccentRed}>{editorError}</Text> |
|
|
</Box> |
|
|
)} |
|
|
<EditorSettingsDialog |
|
|
onSelect={handleEditorSelect} |
|
|
settings={settings} |
|
|
onExit={exitEditorDialog} |
|
|
/> |
|
|
</Box> |
|
|
) : showPrivacyNotice ? ( |
|
|
<PrivacyNotice |
|
|
onExit={() => setShowPrivacyNotice(false)} |
|
|
config={config} |
|
|
/> |
|
|
) : ( |
|
|
<> |
|
|
<LoadingIndicator |
|
|
thought={ |
|
|
streamingState === StreamingState.WaitingForConfirmation || |
|
|
config.getAccessibility()?.disableLoadingPhrases |
|
|
? undefined |
|
|
: thought |
|
|
} |
|
|
currentLoadingPhrase={ |
|
|
config.getAccessibility()?.disableLoadingPhrases |
|
|
? undefined |
|
|
: currentLoadingPhrase |
|
|
} |
|
|
elapsedTime={elapsedTime} |
|
|
/> |
|
|
<Box |
|
|
marginTop={1} |
|
|
display="flex" |
|
|
justifyContent="space-between" |
|
|
width="100%" |
|
|
> |
|
|
<Box> |
|
|
{process.env.GEMINI_SYSTEM_MD && ( |
|
|
<Text color={Colors.AccentRed}>|⌐■_■| </Text> |
|
|
)} |
|
|
{ctrlCPressedOnce ? ( |
|
|
<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} |
|
|
contextFileNames={contextFileNames} |
|
|
mcpServers={config.getMcpServers()} |
|
|
showToolDescriptions={showToolDescriptions} |
|
|
/> |
|
|
)} |
|
|
</Box> |
|
|
<Box> |
|
|
{showAutoAcceptIndicator !== ApprovalMode.DEFAULT && |
|
|
!shellModeActive && ( |
|
|
<AutoAcceptIndicator |
|
|
approvalMode={showAutoAcceptIndicator} |
|
|
/> |
|
|
)} |
|
|
{shellModeActive && <ShellModeIndicator />} |
|
|
</Box> |
|
|
</Box> |
|
|
|
|
|
{showErrorDetails && ( |
|
|
<OverflowProvider> |
|
|
<DetailedMessagesDisplay |
|
|
messages={filteredConsoleMessages} |
|
|
maxHeight={ |
|
|
constrainHeight ? debugConsoleMaxHeight : undefined |
|
|
} |
|
|
width={inputWidth} |
|
|
/> |
|
|
<ShowMoreLines constrainHeight={constrainHeight} /> |
|
|
</OverflowProvider> |
|
|
)} |
|
|
|
|
|
{isInputActive && ( |
|
|
<InputPrompt |
|
|
buffer={buffer} |
|
|
inputWidth={inputWidth} |
|
|
suggestionsWidth={suggestionsWidth} |
|
|
onSubmit={handleFinalSubmit} |
|
|
userMessages={userMessages} |
|
|
onClearScreen={handleClearScreen} |
|
|
config={config} |
|
|
slashCommands={slashCommands} |
|
|
shellModeActive={shellModeActive} |
|
|
setShellModeActive={setShellModeActive} |
|
|
/> |
|
|
)} |
|
|
</> |
|
|
)} |
|
|
|
|
|
{initError && streamingState !== StreamingState.Responding && ( |
|
|
<Box |
|
|
borderStyle="round" |
|
|
borderColor={Colors.AccentRed} |
|
|
paddingX={1} |
|
|
marginBottom={1} |
|
|
> |
|
|
{history.find( |
|
|
(item) => |
|
|
item.type === 'error' && item.text?.includes(initError), |
|
|
)?.text ? ( |
|
|
<Text color={Colors.AccentRed}> |
|
|
{ |
|
|
history.find( |
|
|
(item) => |
|
|
item.type === 'error' && item.text?.includes(initError), |
|
|
)?.text |
|
|
} |
|
|
</Text> |
|
|
) : ( |
|
|
<> |
|
|
<Text color={Colors.AccentRed}> |
|
|
Initialization Error: {initError} |
|
|
</Text> |
|
|
<Text color={Colors.AccentRed}> |
|
|
{' '} |
|
|
Please check API key and configuration. |
|
|
</Text> |
|
|
</> |
|
|
)} |
|
|
</Box> |
|
|
)} |
|
|
<Footer |
|
|
model={currentModel} |
|
|
targetDir={config.getTargetDir()} |
|
|
debugMode={config.getDebugMode()} |
|
|
branchName={branchName} |
|
|
debugMessage={debugMessage} |
|
|
corgiMode={corgiMode} |
|
|
errorCount={errorCount} |
|
|
showErrorDetails={showErrorDetails} |
|
|
showMemoryUsage={ |
|
|
config.getDebugMode() || config.getShowMemoryUsage() |
|
|
} |
|
|
promptTokenCount={sessionStats.currentResponse.promptTokenCount} |
|
|
candidatesTokenCount={ |
|
|
sessionStats.currentResponse.candidatesTokenCount |
|
|
} |
|
|
totalTokenCount={sessionStats.currentResponse.totalTokenCount} |
|
|
/> |
|
|
</Box> |
|
|
</Box> |
|
|
</StreamingContext.Provider> |
|
|
); |
|
|
}; |
|
|
|