Merge tag 'v0.1.15' into feature/yiheng/sync-gemini-cli-0.1.15

This commit is contained in:
奕桁
2025-08-01 23:06:11 +08:00
340 changed files with 36528 additions and 22931 deletions

View File

@@ -36,6 +36,7 @@ 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 { ShellConfirmationDialog } from './components/ShellConfirmationDialog.js';
import { Colors } from './colors.js';
import { Help } from './components/Help.js';
import { loadHierarchicalGeminiMemory } from '../config/config.js';
@@ -46,6 +47,7 @@ import { registerCleanup } from '../utils/cleanup.js';
import { DetailedMessagesDisplay } from './components/DetailedMessagesDisplay.js';
import { HistoryItemDisplay } from './components/HistoryItemDisplay.js';
import { ContextSummaryDisplay } from './components/ContextSummaryDisplay.js';
import { IDEContextDetailDisplay } from './components/IDEContextDetailDisplay.js';
import { useHistory } from './hooks/useHistoryManager.js';
import process from 'node:process';
import {
@@ -57,6 +59,9 @@ import {
EditorType,
FlashFallbackEvent,
logFlashFallback,
AuthType,
type OpenFiles,
ideContext,
} from '@qwen-code/qwen-code-core';
import { validateAuthMethod } from '../config/auth.js';
import { useLogger } from './hooks/useLogger.js';
@@ -66,8 +71,11 @@ import {
useSessionStats,
} from './contexts/SessionContext.js';
import { useGitBranchName } from './hooks/useGitBranchName.js';
import { useFocus } from './hooks/useFocus.js';
import { useBracketedPaste } from './hooks/useBracketedPaste.js';
import { useTextBuffer } from './components/shared/text-buffer.js';
import { useVimMode, VimModeProvider } from './contexts/VimModeContext.js';
import { useVim } from './hooks/vim.js';
import * as fs from 'fs';
import { UpdateNotification } from './components/UpdateNotification.js';
import {
@@ -80,6 +88,7 @@ import ansiEscapes from 'ansi-escapes';
import { OverflowProvider } from './contexts/OverflowContext.js';
import { ShowMoreLines } from './components/ShowMoreLines.js';
import { PrivacyNotice } from './privacy/PrivacyNotice.js';
import { appEvents, AppEvent } from '../utils/events.js';
const CTRL_EXIT_PROMPT_DURATION_MS = 1000;
@@ -92,11 +101,14 @@ interface AppProps {
export const AppWrapper = (props: AppProps) => (
<SessionStatsProvider>
<App {...props} />
<VimModeProvider settings={props.settings}>
<App {...props} />
</VimModeProvider>
</SessionStatsProvider>
);
const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
const isFocused = useFocus();
useBracketedPaste();
const [updateMessage, setUpdateMessage] = useState<string | null>(null);
const { stdout } = useStdout();
@@ -143,6 +155,8 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
const [showErrorDetails, setShowErrorDetails] = useState<boolean>(false);
const [showToolDescriptions, setShowToolDescriptions] =
useState<boolean>(false);
const [showIDEContextDetail, setShowIDEContextDetail] =
useState<boolean>(false);
const [ctrlCPressedOnce, setCtrlCPressedOnce] = useState(false);
const [quittingMessages, setQuittingMessages] = useState<
HistoryItem[] | null
@@ -155,6 +169,37 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
const [modelSwitchedFromQuotaError, setModelSwitchedFromQuotaError] =
useState<boolean>(false);
const [userTier, setUserTier] = useState<UserTierId | undefined>(undefined);
const [openFiles, setOpenFiles] = useState<OpenFiles | undefined>();
const [isProcessing, setIsProcessing] = useState<boolean>(false);
useEffect(() => {
const unsubscribe = ideContext.subscribeToOpenFiles(setOpenFiles);
// Set the initial value
setOpenFiles(ideContext.getOpenFilesContext());
return unsubscribe;
}, []);
useEffect(() => {
const openDebugConsole = () => {
setShowErrorDetails(true);
setConstrainHeight(false); // Make sure the user sees the full message.
};
appEvents.on(AppEvent.OpenDebugConsole, openDebugConsole);
const logErrorHandler = (errorMessage: unknown) => {
handleNewMessage({
type: 'error',
content: String(errorMessage),
count: 1,
});
};
appEvents.on(AppEvent.LogError, logErrorHandler);
return () => {
appEvents.off(AppEvent.OpenDebugConsole, openDebugConsole);
appEvents.off(AppEvent.LogError, logErrorHandler);
};
}, [handleNewMessage]);
const openPrivacyNotice = useCallback(() => {
setShowPrivacyNotice(true);
@@ -162,7 +207,10 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
const initialPromptSubmitted = useRef(false);
const errorCount = useMemo(
() => consoleMessages.filter((msg) => msg.type === 'error').length,
() =>
consoleMessages
.filter((msg) => msg.type === 'error')
.reduce((total, msg) => total + msg.count, 0),
[consoleMessages],
);
@@ -193,26 +241,11 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
// Sync user tier from config when authentication changes
useEffect(() => {
const syncUserTier = async () => {
try {
const configUserTier = await config.getUserTier();
if (configUserTier !== userTier) {
setUserTier(configUserTier);
}
} catch (error) {
// Silently fail - this is not critical functionality
// Only log in debug mode to avoid cluttering the console
if (config.getDebugMode()) {
console.debug('Failed to sync user tier:', error);
}
}
};
// Only sync when not currently authenticating
if (!isAuthenticating) {
syncUserTier();
setUserTier(config.getGeminiClient()?.getUserTier());
}
}, [config, userTier, isAuthenticating]);
}, [config, isAuthenticating]);
const {
isEditorDialogOpen,
@@ -238,8 +271,11 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
process.cwd(),
config.getDebugMode(),
config.getFileService(),
settings.merged,
config.getExtensionContextFilePaths(),
config.getFileFilteringOptions(),
);
config.setUserMemory(memoryContent);
config.setGeminiMdFileCount(fileCount);
setGeminiMdFileCount(fileCount);
@@ -267,7 +303,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
);
console.error('Error refreshing memory:', error);
}
}, [config, addItem]);
}, [config, addItem, settings.merged]);
// Watch for model changes (e.g., from Flash fallback)
useEffect(() => {
@@ -294,64 +330,70 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
): Promise<boolean> => {
let message: string;
// Use actual user tier if available, otherwise default to FREE tier behavior (safe default)
const isPaidTier =
userTier === UserTierId.LEGACY || userTier === UserTierId.STANDARD;
if (
config.getContentGeneratorConfig().authType ===
AuthType.LOGIN_WITH_GOOGLE
) {
// Use actual user tier if available; otherwise, default to FREE tier behavior (safe default)
const isPaidTier =
userTier === UserTierId.LEGACY || userTier === UserTierId.STANDARD;
// Check if this is a Pro quota exceeded error
if (error && isProQuotaExceededError(error)) {
if (isPaidTier) {
message = `⚡ You have reached your daily ${currentModel} quota limit.
// Check if this is a Pro quota exceeded error
if (error && isProQuotaExceededError(error)) {
if (isPaidTier) {
message = `⚡ You have reached your daily ${currentModel} quota limit.
⚡ Automatically switching from ${currentModel} to ${fallbackModel} for the remainder of this session.
⚡ To continue accessing the ${currentModel} model today, consider using /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey`;
} else {
message = `⚡ You have reached your daily ${currentModel} quota limit.
} else {
message = `⚡ You have reached your daily ${currentModel} quota limit.
⚡ Automatically switching from ${currentModel} to ${fallbackModel} for the remainder of this session.
⚡ To increase your limits, upgrade to a Gemini Code Assist Standard or Enterprise plan with higher limits at 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`;
}
} else if (error && isGenericQuotaExceededError(error)) {
if (isPaidTier) {
message = `⚡ You have reached your daily quota limit.
}
} else if (error && isGenericQuotaExceededError(error)) {
if (isPaidTier) {
message = `⚡ You have reached your daily quota limit.
⚡ Automatically switching from ${currentModel} to ${fallbackModel} for the remainder of this session.
⚡ To continue accessing the ${currentModel} model today, consider using /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey`;
} else {
message = `⚡ You have reached your daily quota limit.
} else {
message = `⚡ You have reached your daily quota limit.
⚡ Automatically switching from ${currentModel} to ${fallbackModel} for the remainder of this session.
⚡ To increase your limits, upgrade to a Gemini Code Assist Standard or Enterprise plan with higher limits at 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`;
}
} else {
if (isPaidTier) {
// Default fallback message for other cases (like consecutive 429s)
message = `⚡ Automatically switching from ${currentModel} to ${fallbackModel} for faster responses for the remainder of this session.
}
} else {
if (isPaidTier) {
// Default fallback message for other cases (like consecutive 429s)
message = `⚡ Automatically switching from ${currentModel} to ${fallbackModel} for faster responses for the remainder of this session.
⚡ Possible reasons for this are that you have received multiple consecutive capacity errors or you have reached your daily ${currentModel} quota limit
⚡ To continue accessing the ${currentModel} model today, consider using /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey`;
} else {
// Default fallback message for other cases (like consecutive 429s)
message = `⚡ Automatically switching from ${currentModel} to ${fallbackModel} for faster responses for the remainder of this session.
} else {
// Default fallback message for other cases (like consecutive 429s)
message = `⚡ Automatically switching from ${currentModel} to ${fallbackModel} for faster responses for the remainder of this session.
⚡ Possible reasons for this are that you have received multiple consecutive capacity errors or you have reached your daily ${currentModel} quota limit
⚡ To increase your limits, upgrade to a Gemini Code Assist Standard or Enterprise plan with higher limits at 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`;
}
}
// Add message to UI history
addItem(
{
type: MessageType.INFO,
text: message,
},
Date.now(),
);
// Set the flag to prevent tool continuation
setModelSwitchedFromQuotaError(true);
// Set global quota error flag to prevent Flash model calls
config.setQuotaErrorOccurred(true);
}
// Add message to UI history
addItem(
{
type: MessageType.INFO,
text: message,
},
Date.now(),
);
// Set the flag to prevent tool continuation
setModelSwitchedFromQuotaError(true);
// Set global quota error flag to prevent Flash model calls
config.setQuotaErrorOccurred(true);
// Switch model for future use but return false to stop current retry
config.setModel(fallbackModel);
logFlashFallback(
@@ -364,15 +406,58 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
config.setFlashFallbackHandler(flashFallbackHandler);
}, [config, addItem, userTier]);
// Terminal and UI setup
const { rows: terminalHeight, columns: terminalWidth } = useTerminalSize();
const { stdin, setRawMode } = useStdin();
const isInitialMount = useRef(true);
const widthFraction = 0.9;
const inputWidth = Math.max(
20,
Math.floor(terminalWidth * widthFraction) - 3,
);
const suggestionsWidth = Math.max(60, Math.floor(terminalWidth * 0.8));
// Utility callbacks
const isValidPath = useCallback((filePath: string): boolean => {
try {
return fs.existsSync(filePath) && fs.statSync(filePath).isFile();
} catch (_e) {
return false;
}
}, []);
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]);
// Core hooks and processors
const {
vimEnabled: vimModeEnabled,
vimMode,
toggleVimEnabled,
} = useVimMode();
const {
handleSlashCommand,
slashCommands,
pendingHistoryItems: pendingSlashCommandHistoryItems,
commandContext,
shellConfirmationRequest,
} = useSlashCommandProcessor(
config,
settings,
history,
addItem,
clearItems,
loadHistory,
@@ -383,29 +468,44 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
openAuthDialog,
openEditorDialog,
toggleCorgiMode,
showToolDescriptions,
setQuittingMessages,
openPrivacyNotice,
toggleVimEnabled,
setIsProcessing,
);
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 {
streamingState,
submitQuery,
initError,
pendingHistoryItems: pendingGeminiHistoryItems,
thought,
} = useGeminiStream(
config.getGeminiClient(),
history,
addItem,
setShowHelp,
config,
setDebugMessage,
handleSlashCommand,
shellModeActive,
getPreferredEditor,
onAuthError,
performMemoryRefresh,
modelSwitchedFromQuotaError,
setModelSwitchedFromQuotaError,
);
// Input handling
const handleFinalSubmit = useCallback(
(submittedValue: string) => {
const trimmedValue = submittedValue.trim();
if (trimmedValue.length > 0) {
submitQuery(trimmedValue);
}
},
[submitQuery],
);
const suggestionsWidth = Math.max(60, Math.floor(terminalWidth * 0.8));
const buffer = useTextBuffer({
initialText: '',
@@ -416,6 +516,14 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
shellModeActive,
});
const { handleInput: vimHandleInput } = useVim(buffer, handleFinalSubmit);
const pendingHistoryItems = [...pendingSlashCommandHistoryItems];
pendingHistoryItems.push(...pendingGeminiHistoryItems);
const { elapsedTime, currentLoadingPhrase } =
useLoadingIndicator(streamingState);
const showAutoAcceptIndicator = useAutoAcceptIndicator({ config });
const handleExit = useCallback(
(
pressedOnce: boolean,
@@ -426,15 +534,8 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
const quitCommand = slashCommands.find(
(cmd) => cmd.name === 'quit' || cmd.altName === 'exit',
);
if (quitCommand && quitCommand.action) {
quitCommand.action(commandContext, '');
} else {
// This is unlikely to be needed but added for an additional fallback.
process.exit(0);
}
// Directly invoke the central command handler.
handleSlashCommand('/quit');
} else {
setPressedOnce(true);
timerRef.current = setTimeout(() => {
@@ -443,8 +544,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
}, CTRL_EXIT_PROMPT_DURATION_MS);
}
},
// Add commandContext to the dependency array here!
[slashCommands, commandContext],
[handleSlashCommand],
);
useInput((input: string, key: InkKeyType) => {
@@ -468,6 +568,8 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
if (Object.keys(mcpServers || {}).length > 0) {
handleSlashCommand(newValue ? '/mcp desc' : '/mcp nodesc');
}
} else if (key.ctrl && input === 'e' && ideContext) {
setShowIDEContextDetail((prev) => !prev);
} else if (key.ctrl && (input === 'c' || input === 'C')) {
handleExit(ctrlCPressedOnce, setCtrlCPressedOnce, ctrlCTimerRef);
} else if (key.ctrl && (input === 'd' || input === 'D')) {
@@ -487,57 +589,6 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
}
}, [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,
modelSwitchedFromQuotaError,
setModelSwitchedFromQuotaError,
);
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[]>([]);
@@ -577,7 +628,8 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
fetchUserMessages();
}, [history, logger]);
const isInputActive = streamingState === StreamingState.Idle && !initError;
const isInputActive =
streamingState === StreamingState.Idle && !initError && !isProcessing;
const handleClearScreen = useCallback(() => {
clearItems();
@@ -695,9 +747,13 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
// Arbitrary threshold to ensure that items in the static area are large
// enough but not too large to make the terminal hard to use.
const staticAreaMaxItemHeight = Math.max(terminalHeight * 4, 100);
const placeholder = vimModeEnabled
? " Press 'i' for INSERT mode and 'Esc' for NORMAL mode."
: ' Type your message or @path/to/file';
return (
<StreamingContext.Provider value={streamingState}>
<Box flexDirection="column" marginBottom={1} width="90%">
<Box flexDirection="column" width="90%">
{/* Move UpdateNotification outside Static so it can re-render when updateMessage changes */}
{updateMessage && <UpdateNotification message={updateMessage} />}
@@ -779,7 +835,9 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
</Box>
)}
{isThemeDialogOpen ? (
{shellConfirmationRequest ? (
<ShellConfirmationDialog request={shellConfirmationRequest} />
) : isThemeDialogOpen ? (
<Box flexDirection="column">
{themeError && (
<Box marginBottom={1}>
@@ -864,6 +922,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
}
elapsedTime={elapsedTime}
/>
<Box
marginTop={1}
display="flex"
@@ -884,9 +943,11 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
</Text>
) : (
<ContextSummaryDisplay
openFiles={openFiles}
geminiMdFileCount={geminiMdFileCount}
contextFileNames={contextFileNames}
mcpServers={config.getMcpServers()}
blockedMcpServers={config.getBlockedMcpServers()}
showToolDescriptions={showToolDescriptions}
/>
)}
@@ -901,7 +962,9 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
{shellModeActive && <ShellModeIndicator />}
</Box>
</Box>
{showIDEContextDetail && (
<IDEContextDetailDisplay openFiles={openFiles} />
)}
{showErrorDetails && (
<OverflowProvider>
<Box flexDirection="column">
@@ -930,6 +993,9 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
commandContext={commandContext}
shellModeActive={shellModeActive}
setShellModeActive={setShellModeActive}
focus={isFocused}
vimHandleInput={vimHandleInput}
placeholder={placeholder}
/>
)}
</>
@@ -981,6 +1047,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
}
promptTokenCount={sessionStats.lastPromptTokenCount}
nightly={nightly}
vimMode={vimModeEnabled ? vimMode : undefined}
/>
</Box>
</Box>