mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-29 07:25:33 +08:00
feat: Google's gemini-cli integration (#422)
* feat: integrate Gemini AI agent provider - Core Backend: Ported gemini-cli.js and gemini-response-handler.js to establish the CLI bridge. Registered 'gemini' as an active provider within index.js. - Core Frontend: Extended QuickSettingsPanel.jsx, Settings.jsx, and AgentListItem.jsx to render the Gemini provider option, models (gemini-pro, gemini-flash, etc.), and handle OAuth states. - WebSocket Pipeline: Added support for gemini-command executions in backend and payload processing of gemini-response and gemini-error streams in useChatRealtimeHandlers.ts. Resolved JSON double-stringification and sessionId stripping issues in the transmission handler. - Platform Compatibility: Added scripts/fix-node-pty.js postinstall script and modified posix_spawnp calls with sh -c wrapper to prevent ENOEXEC and MacOS permission errors when spawning the gemini headless binary. - UX & Design: Imported official Google Gemini branding via GeminiLogo.jsx and gemini-ai-icon.svg. Updated translations (chat.json) for en, zh-CN, and ko locales. * fix: propagate gemini permission mode from settings to cli - Added Gemini Permissions UI in Settings to toggle Auto Edit and YOLO modes - Synced gemini permission mode to localStorage - Passed permissionMode in useChatComposerState for Gemini commands - Mapped frontend permission modes to --yolo and --approval-mode options in gemini-cli.js * feat(gemini): Refactor Gemini CLI integration to use stream-json - Replaced regex buffering text-system with NDJSON stream parsing - Added fallback for restricted models like gemini-3.1-pro-preview * feat(gemini): Render tool_use and tool_result UI bubbles - Forwarded gemini tool NDJSON objects to the websocket - Added React state handlers in useChatRealtimeHandlers to match Claude's tool UI behavior * feat(gemini): Add native session resumption and UI token tracking - Captured cliSessionId from init events to map ClaudeCodeUI's chat sessionId directly into Gemini's internal session manager. - Updated gemini-cli.js spawn arguments to append the --resume proxy flag instead of naively dumping the accumulated chat history into the command prompt. - Handled result stream objects by proxying total_tokens back into the frontend's claude-status tracker to natively populate the UI label. - Eliminated gemini-3 model proxy filter entirely. * fix(gemini): Fix static 'Claude' name rendering in chat UI header - Added "gemini": "Gemini" translation strings to messageTypes across English, Korean, and Chinese loc dictionaries. - Updated AssistantThinkingIndicator and MessageComponent ternary checks to identify provider === 'gemini' and render the appropriate brand label instead of statically defaulting to Claude. * feat: Add Gemini session persistence API mapping and Sidebar UI * fix(gemini): Watch ~/.gemini/sessions for live UI updates Added the .gemini/sessions directory to PROVIDER_WATCH_PATHS so that Chokidar emits projects_updated websocket events when new Gemini sessions are created or modified, fixing live sidebar updates. * fix(gemini): Fix Gemini authentication status display in Settings UI - Injected 'checkGeminiAuthStatus' into the Settings.jsx React effect hook so that the UI can poll and render the 'geminiAuthStatus' state. - Updated 'checkGeminiCredentials()' inside server/routes/cli-auth.js to read from '~/.gemini/oauth_creds.json' and '~/.gemini/google_accounts.json', resolving the email address correctly. * Use logo-only icon for gemini * feat(gemini): Add Gemini 3 preview models to UI selection list * Fix Gemini CLI session resume bug and PR #422 review nitpicks * Fix Gemini tool calls disappearing from UI after completion * fix(gemini): resolve outstanding PR #422 feedback and stabilize gemini CLI timeouts * fix(gemini): resolve resume flag and shell session initialization issues This commit addresses the remaining PR comments for the Gemini CLI integration: - Moves the `--resume` flag logic outside the prompt command block, ensuring Gemini sessions correctly resume even when a new prompt isn't passed. - Updates `handleShellConnection` to correctly lookup the native `cliSessionId` from the internal `sessionId` when spawning Gemini sessions in a plain shell. - Refactors dynamic import of `sessionManager.js` back to a native static import for code consistency. * chore: fix TypeScript errors and remove gemini CLI dependency * fix: use cross-spawn on Windows to resolve gemini.cmd correctly --------- Co-authored-by: Haileyesus <118998054+blackmammoth@users.noreply.github.com>
This commit is contained in:
@@ -45,10 +45,10 @@ type PermissionGrantState = 'idle' | 'granted' | 'error';
|
||||
const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFileOpen, onShowSettings, onGrantToolPermission, autoExpandTools, showRawParameters, showThinking, selectedProject, provider }: MessageComponentProps) => {
|
||||
const { t } = useTranslation('chat');
|
||||
const isGrouped = prevMessage && prevMessage.type === message.type &&
|
||||
((prevMessage.type === 'assistant') ||
|
||||
(prevMessage.type === 'user') ||
|
||||
(prevMessage.type === 'tool') ||
|
||||
(prevMessage.type === 'error'));
|
||||
((prevMessage.type === 'assistant') ||
|
||||
(prevMessage.type === 'user') ||
|
||||
(prevMessage.type === 'tool') ||
|
||||
(prevMessage.type === 'error'));
|
||||
const messageRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const [isExpanded, setIsExpanded] = React.useState(false);
|
||||
const permissionSuggestion = getClaudePermissionSuggestion(message, provider);
|
||||
@@ -154,11 +154,11 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
</div>
|
||||
)}
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{message.type === 'error' ? t('messageTypes.error') : message.type === 'tool' ? t('messageTypes.tool') : (provider === 'cursor' ? t('messageTypes.cursor') : provider === 'codex' ? t('messageTypes.codex') : t('messageTypes.claude'))}
|
||||
{message.type === 'error' ? t('messageTypes.error') : message.type === 'tool' ? t('messageTypes.tool') : (provider === 'cursor' ? t('messageTypes.cursor') : provider === 'codex' ? t('messageTypes.codex') : provider === 'gemini' ? t('messageTypes.gemini') : t('messageTypes.claude'))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
<div className="w-full">
|
||||
|
||||
{message.isToolUse ? (
|
||||
@@ -188,7 +188,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
subagentState={message.subagentState}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
{/* Tool Result Section */}
|
||||
{message.toolResult && !shouldHideToolResult(message.toolName || 'UnknownTool', message.toolResult) && (
|
||||
message.toolResult.isError ? (
|
||||
@@ -222,11 +222,10 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
}
|
||||
}}
|
||||
disabled={permissionSuggestion.isAllowed || permissionGrantState === 'granted'}
|
||||
className={`inline-flex items-center gap-2 px-3 py-1.5 rounded-md text-xs font-medium border transition-colors ${
|
||||
permissionSuggestion.isAllowed || permissionGrantState === 'granted'
|
||||
? 'bg-green-100 dark:bg-green-900/30 border-green-300/70 dark:border-green-800/60 text-green-800 dark:text-green-200 cursor-default'
|
||||
: 'bg-white/80 dark:bg-gray-900/40 border-red-300/70 dark:border-red-800/60 text-red-700 dark:text-red-200 hover:bg-white dark:hover:bg-gray-900/70'
|
||||
}`}
|
||||
className={`inline-flex items-center gap-2 px-3 py-1.5 rounded-md text-xs font-medium border transition-colors ${permissionSuggestion.isAllowed || permissionGrantState === 'granted'
|
||||
? 'bg-green-100 dark:bg-green-900/30 border-green-300/70 dark:border-green-800/60 text-green-800 dark:text-green-200 cursor-default'
|
||||
: 'bg-white/80 dark:bg-gray-900/40 border-red-300/70 dark:border-red-800/60 text-red-700 dark:text-red-200 hover:bg-white dark:hover:bg-gray-900/70'
|
||||
}`}
|
||||
>
|
||||
{permissionSuggestion.isAllowed || permissionGrantState === 'granted'
|
||||
? t('permissions.added')
|
||||
@@ -294,7 +293,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
const lines = (message.content || '').split('\n').filter((line) => line.trim());
|
||||
const questionLine = lines.find((line) => line.includes('?')) || lines[0] || '';
|
||||
const options: InteractiveOption[] = [];
|
||||
|
||||
|
||||
// Parse the menu options
|
||||
lines.forEach((line) => {
|
||||
// Match lines like "❯ 1. Yes" or " 2. No"
|
||||
@@ -308,31 +307,29 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="text-sm text-amber-800 dark:text-amber-200 mb-4">
|
||||
{questionLine}
|
||||
</p>
|
||||
|
||||
|
||||
{/* Option buttons */}
|
||||
<div className="space-y-2 mb-4">
|
||||
{options.map((option) => (
|
||||
<button
|
||||
key={option.number}
|
||||
className={`w-full text-left px-4 py-3 rounded-lg border-2 transition-all ${
|
||||
option.isSelected
|
||||
? 'bg-amber-600 dark:bg-amber-700 text-white border-amber-600 dark:border-amber-700 shadow-md'
|
||||
: 'bg-white dark:bg-gray-800 text-amber-900 dark:text-amber-100 border-amber-300 dark:border-amber-700'
|
||||
} cursor-not-allowed opacity-75`}
|
||||
className={`w-full text-left px-4 py-3 rounded-lg border-2 transition-all ${option.isSelected
|
||||
? 'bg-amber-600 dark:bg-amber-700 text-white border-amber-600 dark:border-amber-700 shadow-md'
|
||||
: 'bg-white dark:bg-gray-800 text-amber-900 dark:text-amber-100 border-amber-300 dark:border-amber-700'
|
||||
} cursor-not-allowed opacity-75`}
|
||||
disabled
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold ${
|
||||
option.isSelected
|
||||
? 'bg-white/20'
|
||||
: 'bg-amber-100 dark:bg-amber-800/50'
|
||||
}`}>
|
||||
<span className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold ${option.isSelected
|
||||
? 'bg-white/20'
|
||||
: 'bg-amber-100 dark:bg-amber-800/50'
|
||||
}`}>
|
||||
{option.number}
|
||||
</span>
|
||||
<span className="text-sm sm:text-base font-medium flex-1">
|
||||
@@ -345,7 +342,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
<div className="bg-amber-100 dark:bg-amber-800/30 rounded-lg p-3">
|
||||
<p className="text-amber-900 dark:text-amber-100 text-sm font-medium mb-1">
|
||||
{t('interactive.waiting')}
|
||||
@@ -399,7 +396,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
// Detect if content is pure JSON (starts with { or [)
|
||||
const trimmedContent = content.trim();
|
||||
if ((trimmedContent.startsWith('{') || trimmedContent.startsWith('[')) &&
|
||||
(trimmedContent.endsWith('}') || trimmedContent.endsWith(']'))) {
|
||||
(trimmedContent.endsWith('}') || trimmedContent.endsWith(']'))) {
|
||||
try {
|
||||
const parsed = JSON.parse(trimmedContent);
|
||||
const formatted = JSON.stringify(parsed, null, 2);
|
||||
@@ -439,7 +436,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{!isGrouped && (
|
||||
<div className="text-[11px] text-gray-400 dark:text-gray-500 mt-1">
|
||||
{formattedTime}
|
||||
|
||||
Reference in New Issue
Block a user