mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-03-13 01:47:24 +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:
@@ -145,6 +145,7 @@ export function useChatRealtimeHandlers({
|
||||
'claude-error',
|
||||
'cursor-error',
|
||||
'codex-error',
|
||||
'gemini-error',
|
||||
]);
|
||||
|
||||
const isClaudeSystemInit =
|
||||
@@ -162,8 +163,8 @@ export function useChatRealtimeHandlers({
|
||||
const systemInitSessionId = isClaudeSystemInit
|
||||
? structuredMessageData?.session_id
|
||||
: isCursorSystemInit
|
||||
? rawStructuredData?.session_id
|
||||
: null;
|
||||
? rawStructuredData?.session_id
|
||||
: null;
|
||||
|
||||
const activeViewSessionId =
|
||||
selectedSession?.id || currentSessionId || pendingViewSessionRef.current?.sessionId || null;
|
||||
@@ -176,7 +177,8 @@ export function useChatRealtimeHandlers({
|
||||
!pendingViewSessionRef.current.sessionId &&
|
||||
(latestMessage.type === 'claude-error' ||
|
||||
latestMessage.type === 'cursor-error' ||
|
||||
latestMessage.type === 'codex-error');
|
||||
latestMessage.type === 'codex-error' ||
|
||||
latestMessage.type === 'gemini-error');
|
||||
|
||||
const handleBackgroundLifecycle = (sessionId?: string) => {
|
||||
if (!sessionId) {
|
||||
@@ -225,12 +227,6 @@ export function useChatRealtimeHandlers({
|
||||
if (latestMessage.sessionId && lifecycleMessageTypes.has(String(latestMessage.type))) {
|
||||
handleBackgroundLifecycle(latestMessage.sessionId);
|
||||
}
|
||||
console.log(
|
||||
'Skipping message for different session:',
|
||||
latestMessage.sessionId,
|
||||
'current:',
|
||||
activeViewSessionId,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -297,11 +293,6 @@ export function useChatRealtimeHandlers({
|
||||
structuredMessageData.session_id !== currentSessionId &&
|
||||
isSystemInitForView
|
||||
) {
|
||||
console.log('Claude CLI session duplication detected:', {
|
||||
originalSession: currentSessionId,
|
||||
newSession: structuredMessageData.session_id,
|
||||
});
|
||||
|
||||
setIsSystemSessionChange(true);
|
||||
onNavigateToSession?.(structuredMessageData.session_id);
|
||||
return;
|
||||
@@ -314,10 +305,6 @@ export function useChatRealtimeHandlers({
|
||||
!currentSessionId &&
|
||||
isSystemInitForView
|
||||
) {
|
||||
console.log('New session init detected:', {
|
||||
newSession: structuredMessageData.session_id,
|
||||
});
|
||||
|
||||
setIsSystemSessionChange(true);
|
||||
onNavigateToSession?.(structuredMessageData.session_id);
|
||||
return;
|
||||
@@ -331,7 +318,6 @@ export function useChatRealtimeHandlers({
|
||||
structuredMessageData.session_id === currentSessionId &&
|
||||
isSystemInitForView
|
||||
) {
|
||||
console.log('System init message for current session, ignoring');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -583,17 +569,12 @@ export function useChatRealtimeHandlers({
|
||||
}
|
||||
|
||||
if (currentSessionId && cursorData.session_id !== currentSessionId) {
|
||||
console.log('Cursor session switch detected:', {
|
||||
originalSession: currentSessionId,
|
||||
newSession: cursorData.session_id,
|
||||
});
|
||||
setIsSystemSessionChange(true);
|
||||
onNavigateToSession?.(cursorData.session_id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentSessionId) {
|
||||
console.log('Cursor new session init detected:', { newSession: cursorData.session_id });
|
||||
setIsSystemSessionChange(true);
|
||||
onNavigateToSession?.(cursorData.session_id);
|
||||
return;
|
||||
@@ -612,9 +593,8 @@ export function useChatRealtimeHandlers({
|
||||
...previous,
|
||||
{
|
||||
type: 'assistant',
|
||||
content: `Using tool: ${latestMessage.tool} ${
|
||||
latestMessage.input ? `with ${latestMessage.input}` : ''
|
||||
}`,
|
||||
content: `Using tool: ${latestMessage.tool} ${latestMessage.input ? `with ${latestMessage.input}` : ''
|
||||
}`,
|
||||
timestamp: new Date(),
|
||||
isToolUse: true,
|
||||
toolName: latestMessage.tool,
|
||||
@@ -897,7 +877,6 @@ export function useChatRealtimeHandlers({
|
||||
onNavigateToSession?.(codexActualSessionId);
|
||||
}
|
||||
sessionStorage.removeItem('pendingSessionId');
|
||||
console.log('Codex session complete, ID set to:', codexPendingSessionId);
|
||||
}
|
||||
|
||||
if (selectedProject) {
|
||||
@@ -919,6 +898,91 @@ export function useChatRealtimeHandlers({
|
||||
]);
|
||||
break;
|
||||
|
||||
case 'gemini-response': {
|
||||
const geminiData = latestMessage.data;
|
||||
|
||||
if (geminiData && geminiData.type === 'message' && typeof geminiData.content === 'string') {
|
||||
const content = decodeHtmlEntities(geminiData.content);
|
||||
|
||||
if (content) {
|
||||
streamBufferRef.current += streamBufferRef.current ? `\n${content}` : content;
|
||||
}
|
||||
|
||||
if (!geminiData.isPartial) {
|
||||
// Immediate flush and finalization for the last chunk
|
||||
if (streamTimerRef.current) {
|
||||
clearTimeout(streamTimerRef.current);
|
||||
streamTimerRef.current = null;
|
||||
}
|
||||
const chunk = streamBufferRef.current;
|
||||
streamBufferRef.current = '';
|
||||
|
||||
if (chunk) {
|
||||
appendStreamingChunk(setChatMessages, chunk, true);
|
||||
}
|
||||
finalizeStreamingMessage(setChatMessages);
|
||||
} else if (!streamTimerRef.current && streamBufferRef.current) {
|
||||
streamTimerRef.current = window.setTimeout(() => {
|
||||
const chunk = streamBufferRef.current;
|
||||
streamBufferRef.current = '';
|
||||
streamTimerRef.current = null;
|
||||
|
||||
if (chunk) {
|
||||
appendStreamingChunk(setChatMessages, chunk, true);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'gemini-error':
|
||||
setIsLoading(false);
|
||||
setCanAbortSession(false);
|
||||
setChatMessages((previous) => [
|
||||
...previous,
|
||||
{
|
||||
type: 'error',
|
||||
content: latestMessage.error || 'An error occurred with Gemini',
|
||||
timestamp: new Date(),
|
||||
},
|
||||
]);
|
||||
break;
|
||||
|
||||
case 'gemini-tool-use':
|
||||
setChatMessages((previous) => [
|
||||
...previous,
|
||||
{
|
||||
type: 'assistant',
|
||||
content: '',
|
||||
timestamp: new Date(),
|
||||
isToolUse: true,
|
||||
toolName: latestMessage.toolName,
|
||||
toolInput: latestMessage.parameters ? JSON.stringify(latestMessage.parameters, null, 2) : '',
|
||||
toolId: latestMessage.toolId,
|
||||
toolResult: null,
|
||||
}
|
||||
]);
|
||||
break;
|
||||
|
||||
case 'gemini-tool-result':
|
||||
setChatMessages((previous) =>
|
||||
previous.map((message) => {
|
||||
if (message.isToolUse && message.toolId === latestMessage.toolId) {
|
||||
return {
|
||||
...message,
|
||||
toolResult: {
|
||||
content: latestMessage.output || `Status: ${latestMessage.status}`,
|
||||
isError: latestMessage.status === 'error',
|
||||
timestamp: new Date(),
|
||||
},
|
||||
};
|
||||
}
|
||||
return message;
|
||||
}),
|
||||
);
|
||||
break;
|
||||
|
||||
case 'session-aborted': {
|
||||
const pendingSessionId =
|
||||
typeof window !== 'undefined' ? sessionStorage.getItem('pendingSessionId') : null;
|
||||
|
||||
Reference in New Issue
Block a user