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:
Menny Even Danan
2026-02-27 09:36:35 -05:00
committed by GitHub
parent 917c353115
commit a367edd515
44 changed files with 2399 additions and 796 deletions

View File

@@ -0,0 +1,140 @@
// Gemini Response Handler - JSON Stream processing
class GeminiResponseHandler {
constructor(ws, options = {}) {
this.ws = ws;
this.buffer = '';
this.onContentFragment = options.onContentFragment || null;
this.onInit = options.onInit || null;
this.onToolUse = options.onToolUse || null;
this.onToolResult = options.onToolResult || null;
}
// Process incoming raw data from Gemini stream-json
processData(data) {
this.buffer += data;
// Split by newline
const lines = this.buffer.split('\n');
// Keep the last incomplete line in the buffer
this.buffer = lines.pop() || '';
for (const line of lines) {
if (!line.trim()) continue;
try {
const event = JSON.parse(line);
this.handleEvent(event);
} catch (err) {
// Not a JSON line, probably debug output or CLI warnings
// console.error('[Gemini Handler] Non-JSON line ignored:', line);
}
}
}
handleEvent(event) {
const socketSessionId = typeof this.ws.getSessionId === 'function' ? this.ws.getSessionId() : null;
if (event.type === 'init') {
if (this.onInit) {
this.onInit(event);
}
return;
}
if (event.type === 'message' && event.role === 'assistant') {
const content = event.content || '';
// Notify the parent CLI handler of accumulated text
if (this.onContentFragment && content) {
this.onContentFragment(content);
}
let payload = {
type: 'gemini-response',
data: {
type: 'message',
content: content,
isPartial: event.delta === true
}
};
if (socketSessionId) payload.sessionId = socketSessionId;
this.ws.send(payload);
}
else if (event.type === 'tool_use') {
if (this.onToolUse) {
this.onToolUse(event);
}
let payload = {
type: 'gemini-tool-use',
toolName: event.tool_name,
toolId: event.tool_id,
parameters: event.parameters || {}
};
if (socketSessionId) payload.sessionId = socketSessionId;
this.ws.send(payload);
}
else if (event.type === 'tool_result') {
if (this.onToolResult) {
this.onToolResult(event);
}
let payload = {
type: 'gemini-tool-result',
toolId: event.tool_id,
status: event.status,
output: event.output || ''
};
if (socketSessionId) payload.sessionId = socketSessionId;
this.ws.send(payload);
}
else if (event.type === 'result') {
// Send a finalize message string
let payload = {
type: 'gemini-response',
data: {
type: 'message',
content: '',
isPartial: false
}
};
if (socketSessionId) payload.sessionId = socketSessionId;
this.ws.send(payload);
if (event.stats && event.stats.total_tokens) {
let statsPayload = {
type: 'claude-status',
data: {
status: 'Complete',
tokens: event.stats.total_tokens
}
};
if (socketSessionId) statsPayload.sessionId = socketSessionId;
this.ws.send(statsPayload);
}
}
else if (event.type === 'error') {
let payload = {
type: 'gemini-error',
error: event.error || event.message || 'Unknown Gemini streaming error'
};
if (socketSessionId) payload.sessionId = socketSessionId;
this.ws.send(payload);
}
}
forceFlush() {
// If the buffer has content, try to parse it one last time
if (this.buffer.trim()) {
try {
const event = JSON.parse(this.buffer);
this.handleEvent(event);
} catch (err) { }
}
}
destroy() {
this.buffer = '';
}
}
export default GeminiResponseHandler;