mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-03-15 10:57:25 +00: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:
@@ -12,7 +12,7 @@ type AgentListItemProps = {
|
||||
|
||||
type AgentConfig = {
|
||||
name: string;
|
||||
color: 'blue' | 'purple' | 'gray';
|
||||
color: 'blue' | 'purple' | 'gray' | 'indigo';
|
||||
};
|
||||
|
||||
const agentConfig: Record<AgentProvider, AgentConfig> = {
|
||||
@@ -28,6 +28,10 @@ const agentConfig: Record<AgentProvider, AgentConfig> = {
|
||||
name: 'Codex',
|
||||
color: 'gray',
|
||||
},
|
||||
gemini: {
|
||||
name: 'Gemini',
|
||||
color: 'indigo',
|
||||
}
|
||||
};
|
||||
|
||||
const colorClasses = {
|
||||
@@ -49,6 +53,12 @@ const colorClasses = {
|
||||
bg: 'bg-gray-100 dark:bg-gray-800/50',
|
||||
dot: 'bg-gray-700 dark:bg-gray-300',
|
||||
},
|
||||
indigo: {
|
||||
border: 'border-l-indigo-500 md:border-l-indigo-500',
|
||||
borderBottom: 'border-b-indigo-500',
|
||||
bg: 'bg-indigo-50 dark:bg-indigo-900/20',
|
||||
dot: 'bg-indigo-500',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export default function AgentListItem({
|
||||
@@ -66,11 +76,10 @@ export default function AgentListItem({
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`flex-1 text-center py-3 px-2 border-b-2 transition-colors ${
|
||||
isSelected
|
||||
? `${colors.borderBottom} ${colors.bg}`
|
||||
: 'border-transparent hover:bg-gray-50 dark:hover:bg-gray-800'
|
||||
}`}
|
||||
className={`flex-1 text-center py-3 px-2 border-b-2 transition-colors ${isSelected
|
||||
? `${colors.borderBottom} ${colors.bg}`
|
||||
: 'border-transparent hover:bg-gray-50 dark:hover:bg-gray-800'
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<SessionProviderLogo provider={agentId} className="w-5 h-5" />
|
||||
@@ -86,11 +95,10 @@ export default function AgentListItem({
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`w-full text-left p-3 border-l-4 transition-colors ${
|
||||
isSelected
|
||||
? `${colors.border} ${colors.bg}`
|
||||
: 'border-transparent hover:bg-gray-50 dark:hover:bg-gray-800'
|
||||
}`}
|
||||
className={`w-full text-left p-3 border-l-4 transition-colors ${isSelected
|
||||
? `${colors.border} ${colors.bg}`
|
||||
: 'border-transparent hover:bg-gray-50 dark:hover:bg-gray-800'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<SessionProviderLogo provider={agentId} className="w-4 h-4" />
|
||||
|
||||
@@ -9,15 +9,19 @@ export default function AgentsSettingsTab({
|
||||
claudeAuthStatus,
|
||||
cursorAuthStatus,
|
||||
codexAuthStatus,
|
||||
geminiAuthStatus,
|
||||
onClaudeLogin,
|
||||
onCursorLogin,
|
||||
onCodexLogin,
|
||||
onGeminiLogin,
|
||||
claudePermissions,
|
||||
onClaudePermissionsChange,
|
||||
cursorPermissions,
|
||||
onCursorPermissionsChange,
|
||||
codexPermissionMode,
|
||||
onCodexPermissionModeChange,
|
||||
geminiPermissionMode,
|
||||
onGeminiPermissionModeChange,
|
||||
mcpServers,
|
||||
cursorMcpServers,
|
||||
codexMcpServers,
|
||||
@@ -48,13 +52,19 @@ export default function AgentsSettingsTab({
|
||||
authStatus: codexAuthStatus,
|
||||
onLogin: onCodexLogin,
|
||||
},
|
||||
gemini: {
|
||||
authStatus: geminiAuthStatus,
|
||||
onLogin: onGeminiLogin,
|
||||
},
|
||||
}), [
|
||||
claudeAuthStatus,
|
||||
codexAuthStatus,
|
||||
cursorAuthStatus,
|
||||
geminiAuthStatus,
|
||||
onClaudeLogin,
|
||||
onCodexLogin,
|
||||
onCursorLogin,
|
||||
onGeminiLogin,
|
||||
]);
|
||||
|
||||
return (
|
||||
@@ -81,6 +91,8 @@ export default function AgentsSettingsTab({
|
||||
onCursorPermissionsChange={onCursorPermissionsChange}
|
||||
codexPermissionMode={codexPermissionMode}
|
||||
onCodexPermissionModeChange={onCodexPermissionModeChange}
|
||||
geminiPermissionMode={geminiPermissionMode}
|
||||
onGeminiPermissionModeChange={onGeminiPermissionModeChange}
|
||||
mcpServers={mcpServers}
|
||||
cursorMcpServers={cursorMcpServers}
|
||||
codexMcpServers={codexMcpServers}
|
||||
|
||||
@@ -18,6 +18,7 @@ type AgentVisualConfig = {
|
||||
textClass: string;
|
||||
subtextClass: string;
|
||||
buttonClass: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
const agentConfig: Record<AgentProvider, AgentVisualConfig> = {
|
||||
@@ -45,6 +46,15 @@ const agentConfig: Record<AgentProvider, AgentVisualConfig> = {
|
||||
subtextClass: 'text-gray-700 dark:text-gray-300',
|
||||
buttonClass: 'bg-gray-800 hover:bg-gray-900 dark:bg-gray-700 dark:hover:bg-gray-600',
|
||||
},
|
||||
gemini: {
|
||||
name: 'Gemini',
|
||||
description: 'Google Gemini AI assistant',
|
||||
bgClass: 'bg-indigo-50 dark:bg-indigo-900/20',
|
||||
borderClass: 'border-indigo-200 dark:border-indigo-800',
|
||||
textClass: 'text-indigo-900 dark:text-indigo-100',
|
||||
subtextClass: 'text-indigo-700 dark:text-indigo-300',
|
||||
buttonClass: 'bg-indigo-600 hover:bg-indigo-700',
|
||||
},
|
||||
};
|
||||
|
||||
export default function AccountContent({ agent, authStatus, onLogin }: AccountContentProps) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { AlertTriangle, Plus, Shield, X } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '../../../../../../ui/button';
|
||||
import { Input } from '../../../../../../ui/input';
|
||||
import type { CodexPermissionMode } from '../../../../../types/types';
|
||||
import type { CodexPermissionMode, GeminiPermissionMode } from '../../../../../types/types';
|
||||
|
||||
const COMMON_CLAUDE_TOOLS = [
|
||||
'Bash(git log:*)',
|
||||
@@ -489,11 +489,10 @@ function CodexPermissions({ permissionMode, onPermissionModeChange }: Omit<Codex
|
||||
<p className="text-sm text-muted-foreground">{t('permissions.codex.description')}</p>
|
||||
|
||||
<div
|
||||
className={`border rounded-lg p-4 cursor-pointer transition-all ${
|
||||
permissionMode === 'default'
|
||||
? 'bg-gray-100 dark:bg-gray-800 border-gray-400 dark:border-gray-500'
|
||||
: 'bg-gray-50 dark:bg-gray-900/50 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
||||
}`}
|
||||
className={`border rounded-lg p-4 cursor-pointer transition-all ${permissionMode === 'default'
|
||||
? 'bg-gray-100 dark:bg-gray-800 border-gray-400 dark:border-gray-500'
|
||||
: 'bg-gray-50 dark:bg-gray-900/50 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
||||
}`}
|
||||
onClick={() => onPermissionModeChange('default')}
|
||||
>
|
||||
<label className="flex items-start gap-3 cursor-pointer">
|
||||
@@ -514,11 +513,10 @@ function CodexPermissions({ permissionMode, onPermissionModeChange }: Omit<Codex
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`border rounded-lg p-4 cursor-pointer transition-all ${
|
||||
permissionMode === 'acceptEdits'
|
||||
? 'bg-green-50 dark:bg-green-900/20 border-green-400 dark:border-green-600'
|
||||
: 'bg-gray-50 dark:bg-gray-900/50 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
||||
}`}
|
||||
className={`border rounded-lg p-4 cursor-pointer transition-all ${permissionMode === 'acceptEdits'
|
||||
? 'bg-green-50 dark:bg-green-900/20 border-green-400 dark:border-green-600'
|
||||
: 'bg-gray-50 dark:bg-gray-900/50 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
||||
}`}
|
||||
onClick={() => onPermissionModeChange('acceptEdits')}
|
||||
>
|
||||
<label className="flex items-start gap-3 cursor-pointer">
|
||||
@@ -539,11 +537,10 @@ function CodexPermissions({ permissionMode, onPermissionModeChange }: Omit<Codex
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`border rounded-lg p-4 cursor-pointer transition-all ${
|
||||
permissionMode === 'bypassPermissions'
|
||||
? 'bg-orange-50 dark:bg-orange-900/20 border-orange-400 dark:border-orange-600'
|
||||
: 'bg-gray-50 dark:bg-gray-900/50 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
||||
}`}
|
||||
className={`border rounded-lg p-4 cursor-pointer transition-all ${permissionMode === 'bypassPermissions'
|
||||
? 'bg-orange-50 dark:bg-orange-900/20 border-orange-400 dark:border-orange-600'
|
||||
: 'bg-gray-50 dark:bg-gray-900/50 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
||||
}`}
|
||||
onClick={() => onPermissionModeChange('bypassPermissions')}
|
||||
>
|
||||
<label className="flex items-start gap-3 cursor-pointer">
|
||||
@@ -582,7 +579,111 @@ function CodexPermissions({ permissionMode, onPermissionModeChange }: Omit<Codex
|
||||
);
|
||||
}
|
||||
|
||||
type PermissionsContentProps = ClaudePermissionsProps | CursorPermissionsProps | CodexPermissionsProps;
|
||||
type GeminiPermissionsProps = {
|
||||
agent: 'gemini';
|
||||
permissionMode: GeminiPermissionMode;
|
||||
onPermissionModeChange: (value: GeminiPermissionMode) => void;
|
||||
};
|
||||
|
||||
// Gemini Permissions
|
||||
function GeminiPermissions({ permissionMode, onPermissionModeChange }: Omit<GeminiPermissionsProps, 'agent'>) {
|
||||
const { t } = useTranslation(['settings', 'chat']);
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Shield className="w-5 h-5 text-green-500" />
|
||||
<h3 className="text-lg font-medium text-foreground">
|
||||
{t('gemini.permissionMode')}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('gemini.description')}
|
||||
</p>
|
||||
|
||||
{/* Default Mode */}
|
||||
<div
|
||||
className={`border rounded-lg p-4 cursor-pointer transition-all ${permissionMode === 'default'
|
||||
? 'bg-gray-100 dark:bg-gray-800 border-gray-400 dark:border-gray-500'
|
||||
: 'bg-gray-50 dark:bg-gray-900/50 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
||||
}`}
|
||||
onClick={() => onPermissionModeChange('default')}
|
||||
>
|
||||
<label className="flex items-start gap-3 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="geminiPermissionMode"
|
||||
checked={permissionMode === 'default'}
|
||||
onChange={() => onPermissionModeChange('default')}
|
||||
className="mt-1 w-4 h-4 text-green-600"
|
||||
/>
|
||||
<div>
|
||||
<div className="font-medium text-foreground">{t('gemini.modes.default.title')}</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t('gemini.modes.default.description')}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Auto Edit Mode */}
|
||||
<div
|
||||
className={`border rounded-lg p-4 cursor-pointer transition-all ${permissionMode === 'auto_edit'
|
||||
? 'bg-green-50 dark:bg-green-900/20 border-green-400 dark:border-green-600'
|
||||
: 'bg-gray-50 dark:bg-gray-900/50 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
||||
}`}
|
||||
onClick={() => onPermissionModeChange('auto_edit')}
|
||||
>
|
||||
<label className="flex items-start gap-3 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="geminiPermissionMode"
|
||||
checked={permissionMode === 'auto_edit'}
|
||||
onChange={() => onPermissionModeChange('auto_edit')}
|
||||
className="mt-1 w-4 h-4 text-green-600"
|
||||
/>
|
||||
<div>
|
||||
<div className="font-medium text-green-900 dark:text-green-100">{t('gemini.modes.autoEdit.title')}</div>
|
||||
<div className="text-sm text-green-700 dark:text-green-300">
|
||||
{t('gemini.modes.autoEdit.description')}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* YOLO Mode */}
|
||||
<div
|
||||
className={`border rounded-lg p-4 cursor-pointer transition-all ${permissionMode === 'yolo'
|
||||
? 'bg-orange-50 dark:bg-orange-900/20 border-orange-400 dark:border-orange-600'
|
||||
: 'bg-gray-50 dark:bg-gray-900/50 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
|
||||
}`}
|
||||
onClick={() => onPermissionModeChange('yolo')}
|
||||
>
|
||||
<label className="flex items-start gap-3 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="geminiPermissionMode"
|
||||
checked={permissionMode === 'yolo'}
|
||||
onChange={() => onPermissionModeChange('yolo')}
|
||||
className="mt-1 w-4 h-4 text-orange-600"
|
||||
/>
|
||||
<div>
|
||||
<div className="font-medium text-orange-900 dark:text-orange-100 flex items-center gap-2">
|
||||
{t('gemini.modes.yolo.title')}
|
||||
<AlertTriangle className="w-4 h-4" />
|
||||
</div>
|
||||
<div className="text-sm text-orange-700 dark:text-orange-300">
|
||||
{t('gemini.modes.yolo.description')}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type PermissionsContentProps = ClaudePermissionsProps | CursorPermissionsProps | CodexPermissionsProps | GeminiPermissionsProps;
|
||||
|
||||
export default function PermissionsContent(props: PermissionsContentProps) {
|
||||
if (props.agent === 'claude') {
|
||||
@@ -593,5 +694,9 @@ export default function PermissionsContent(props: PermissionsContentProps) {
|
||||
return <CursorPermissions {...props} />;
|
||||
}
|
||||
|
||||
if (props.agent === 'gemini') {
|
||||
return <GeminiPermissions {...props} />;
|
||||
}
|
||||
|
||||
return <CodexPermissions {...props} />;
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@ import type {
|
||||
AuthStatus,
|
||||
AgentCategory,
|
||||
ClaudePermissionsState,
|
||||
CodexPermissionMode,
|
||||
CursorPermissionsState,
|
||||
CodexPermissionMode,
|
||||
GeminiPermissionMode,
|
||||
McpServer,
|
||||
McpToolsResult,
|
||||
McpTestResult,
|
||||
@@ -21,15 +22,19 @@ export type AgentsSettingsTabProps = {
|
||||
claudeAuthStatus: AuthStatus;
|
||||
cursorAuthStatus: AuthStatus;
|
||||
codexAuthStatus: AuthStatus;
|
||||
geminiAuthStatus: AuthStatus;
|
||||
onClaudeLogin: () => void;
|
||||
onCursorLogin: () => void;
|
||||
onCodexLogin: () => void;
|
||||
onGeminiLogin: () => void;
|
||||
claudePermissions: ClaudePermissionsState;
|
||||
onClaudePermissionsChange: (value: ClaudePermissionsState) => void;
|
||||
cursorPermissions: CursorPermissionsState;
|
||||
onCursorPermissionsChange: (value: CursorPermissionsState) => void;
|
||||
codexPermissionMode: CodexPermissionMode;
|
||||
onCodexPermissionModeChange: (value: CodexPermissionMode) => void;
|
||||
geminiPermissionMode: GeminiPermissionMode;
|
||||
onGeminiPermissionModeChange: (value: GeminiPermissionMode) => void;
|
||||
mcpServers: McpServer[];
|
||||
cursorMcpServers: McpServer[];
|
||||
codexMcpServers: McpServer[];
|
||||
@@ -66,6 +71,8 @@ export type AgentCategoryContentSectionProps = {
|
||||
onCursorPermissionsChange: (value: CursorPermissionsState) => void;
|
||||
codexPermissionMode: CodexPermissionMode;
|
||||
onCodexPermissionModeChange: (value: CodexPermissionMode) => void;
|
||||
geminiPermissionMode: GeminiPermissionMode;
|
||||
onGeminiPermissionModeChange: (value: GeminiPermissionMode) => void;
|
||||
mcpServers: McpServer[];
|
||||
cursorMcpServers: McpServer[];
|
||||
codexMcpServers: McpServer[];
|
||||
|
||||
Reference in New Issue
Block a user