mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-14 16:10:59 -07:00
test
This commit is contained in:
31
packages/chrome-extension/background.js
Normal file
31
packages/chrome-extension/background.js
Normal file
@@ -0,0 +1,31 @@
|
||||
// Wrap sidePanel API call to handle potential unavailability or errors
|
||||
if (chrome.sidePanel && chrome.sidePanel.setPanelBehavior) {
|
||||
chrome.sidePanel
|
||||
.setPanelBehavior({ openPanelOnActionClick: true })
|
||||
.catch((error) => console.error("setPanelBehavior error:", error));
|
||||
}
|
||||
|
||||
chrome.runtime.onInstalled.addListener(() => {
|
||||
// Create context menu item, suppressing error if it already exists
|
||||
chrome.contextMenus.create({
|
||||
id: 'openSidePanel',
|
||||
title: 'Open Gemini CLI',
|
||||
contexts: ['all']
|
||||
}, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.log("Context menu create warning:", chrome.runtime.lastError.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
chrome.contextMenus.onClicked.addListener((info, tab) => {
|
||||
if (info.menuItemId === 'openSidePanel') {
|
||||
// Check if sidePanel API is available before calling
|
||||
if (chrome.sidePanel && chrome.sidePanel.open) {
|
||||
chrome.sidePanel.open({ windowId: tab.windowId })
|
||||
.catch(err => console.error("sidePanel.open error:", err));
|
||||
} else {
|
||||
console.error("chrome.sidePanel.open is not available.");
|
||||
}
|
||||
}
|
||||
});
|
||||
17
packages/chrome-extension/manifest.json
Normal file
17
packages/chrome-extension/manifest.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Gemini CLI Companion",
|
||||
"version": "1.0",
|
||||
"description": "Connects to Gemini CLI A2A Server",
|
||||
"permissions": ["sidePanel", "activeTab", "scripting", "storage", "contextMenus"],
|
||||
"host_permissions": ["http://localhost/*", "<all_urls>"],
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"action": {
|
||||
"default_title": "Open Gemini CLI"
|
||||
},
|
||||
"side_panel": {
|
||||
"default_path": "sidepanel.html"
|
||||
}
|
||||
}
|
||||
250
packages/chrome-extension/sidepanel.js
Normal file
250
packages/chrome-extension/sidepanel.js
Normal file
@@ -0,0 +1,250 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const serverUrlInput = document.getElementById('server-url');
|
||||
const chatHistory = document.getElementById('chat-history');
|
||||
const promptInput = document.getElementById('prompt-input');
|
||||
const sendBtn = document.getElementById('send-btn');
|
||||
const includeContextCheckbox = document.getElementById('include-page-context');
|
||||
|
||||
// Helper for UUID generation
|
||||
function generateUUID() {
|
||||
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, c =>
|
||||
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
|
||||
);
|
||||
}
|
||||
|
||||
// Load saved settings
|
||||
chrome.storage.local.get(['serverUrl'], (result) => {
|
||||
if (result.serverUrl) {
|
||||
serverUrlInput.value = result.serverUrl;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('save-config').addEventListener('click', () => {
|
||||
chrome.storage.local.set({ serverUrl: serverUrlInput.value });
|
||||
alert('Settings saved');
|
||||
});
|
||||
|
||||
sendBtn.addEventListener('click', async () => {
|
||||
console.log('Send button clicked');
|
||||
const prompt = promptInput.value.trim();
|
||||
if (!prompt) return;
|
||||
|
||||
// Remove trailing slash if present
|
||||
const serverUrl = serverUrlInput.value.replace(/\/$/, '');
|
||||
if (!serverUrl) {
|
||||
alert('Please set the Server URL');
|
||||
return;
|
||||
}
|
||||
|
||||
appendMessage('user', prompt);
|
||||
promptInput.value = '';
|
||||
sendBtn.disabled = true;
|
||||
|
||||
let context = '';
|
||||
if (includeContextCheckbox.checked) {
|
||||
try {
|
||||
console.log('Fetching page context...');
|
||||
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
if (tab && tab.id) {
|
||||
const [{ result }] = await chrome.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
func: () => document.body.innerText,
|
||||
});
|
||||
context = `
|
||||
|
||||
[Context from ${tab.url}]:
|
||||
${result}
|
||||
`;
|
||||
console.log('Page context retrieved successfully');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to get page context:', e);
|
||||
appendMessage('agent', 'Error: Could not retrieve page context. ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`Sending message to ${serverUrl}...`);
|
||||
await sendMessage(serverUrl, prompt + context);
|
||||
} catch (error) {
|
||||
console.error('sendMessage failed:', error);
|
||||
appendMessage('agent', `Error: ${error.message}`);
|
||||
} finally {
|
||||
sendBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
function appendMessage(role, text) {
|
||||
const div = document.createElement('div');
|
||||
div.className = `message ${role}-message`;
|
||||
div.innerText = text;
|
||||
chatHistory.appendChild(div);
|
||||
chatHistory.scrollTop = chatHistory.scrollHeight;
|
||||
}
|
||||
|
||||
async function sendMessage(baseUrl, text) {
|
||||
let messageId;
|
||||
try {
|
||||
messageId = generateUUID();
|
||||
} catch (e) {
|
||||
console.error('UUID generation failed:', e);
|
||||
messageId = 'fallback-id-' + Date.now();
|
||||
}
|
||||
|
||||
// Construct JSON-RPC payload based on testing_utils.ts
|
||||
const rpcPayload = {
|
||||
jsonrpc: '2.0',
|
||||
id: generateUUID(),
|
||||
method: 'message/stream',
|
||||
params: {
|
||||
message: {
|
||||
kind: 'message',
|
||||
role: 'user',
|
||||
parts: [{ kind: 'text', text: text }],
|
||||
messageId: messageId
|
||||
},
|
||||
metadata: {
|
||||
coderAgent: {
|
||||
kind: 'agent-settings',
|
||||
// Optional: Try to infer or leave empty if server has defaults
|
||||
// workspacePath: '/tmp'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
console.log('Sending payload:', rpcPayload);
|
||||
|
||||
let response;
|
||||
|
||||
// Try root endpoint first (JSON-RPC style)
|
||||
try {
|
||||
console.log(`Attempting POST ${baseUrl}/`);
|
||||
response = await fetch(`${baseUrl}/`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(rpcPayload)
|
||||
});
|
||||
console.log(`Root endpoint response status: ${response.status}`);
|
||||
} catch (e) {
|
||||
console.error(`Fetch to ${baseUrl}/ failed:`, e);
|
||||
}
|
||||
|
||||
if (!response || response.status === 404) {
|
||||
console.log('Root endpoint 404 or failed, trying /message/stream (REST style)...');
|
||||
// Fallback to /message/stream
|
||||
try {
|
||||
response = await fetch(`${baseUrl}/message/stream`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(rpcPayload.params) // Send params as body
|
||||
});
|
||||
console.log(`/message/stream response status: ${response ? response.status : 'undefined'}`);
|
||||
} catch (e) {
|
||||
console.error(`Fetch to ${baseUrl}/message/stream failed:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!response || response.status === 404) {
|
||||
console.log('Trying /v1/message/stream...');
|
||||
try {
|
||||
response = await fetch(`${baseUrl}/v1/message/stream`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(rpcPayload.params)
|
||||
});
|
||||
console.log(`/v1/message/stream response status: ${response ? response.status : 'undefined'}`);
|
||||
} catch (e) {
|
||||
console.error(`Fetch to ${baseUrl}/v1/message/stream failed:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
throw new Error('All fetch attempts failed. Check console for details.');
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server returned ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
await handleStream(response);
|
||||
}
|
||||
|
||||
async function handleStream(response) {
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = 'message agent-message';
|
||||
chatHistory.appendChild(messageDiv);
|
||||
|
||||
let buffer = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
buffer += chunk;
|
||||
|
||||
const lines = buffer.split('\n');
|
||||
// Process all complete lines
|
||||
for (let i = 0; i < lines.length - 1; i++) {
|
||||
const line = lines[i];
|
||||
if (line.startsWith('data: ')) {
|
||||
try {
|
||||
const jsonStr = line.substring(6);
|
||||
if (jsonStr.trim() === '[DONE]') continue;
|
||||
|
||||
const jsonResponse = JSON.parse(jsonStr);
|
||||
console.log('Event:', jsonResponse);
|
||||
|
||||
// The server response wrapper: { jsonrpc: '2.0', result: EVENT, id: ... }
|
||||
const event = jsonResponse.result || jsonResponse;
|
||||
|
||||
if (event.kind === 'status-update' && event.status && event.status.message) {
|
||||
const message = event.status.message;
|
||||
if (message.parts) {
|
||||
const textParts = message.parts.filter(p => p.kind === 'text');
|
||||
if (textParts.length > 0) {
|
||||
// Accumulate text parts.
|
||||
// Note: In a real streaming scenario, we might get partial text or full updates.
|
||||
// If _sendTextContent sends a new message each time, we should append.
|
||||
// If it sends the same message with more text, we should replace?
|
||||
// Based on task.ts, _sendTextContent creates a NEW messageId each time.
|
||||
// So we should APPEND.
|
||||
|
||||
const newText = textParts.map(p => p.text).join('');
|
||||
messageDiv.innerText += newText;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Tool Calls (if any info is sent via different event kind)
|
||||
// task.ts sends 'status-update' with 'ToolCallConfirmationEvent' or 'ToolCallUpdateEvent'
|
||||
// The message contains 'data' part with ToolCall info.
|
||||
|
||||
if (event.kind === 'status-update' && event.status.message?.parts) {
|
||||
const dataParts = event.status.message.parts.filter(p => p.kind === 'data');
|
||||
for (const part of dataParts) {
|
||||
if (part.data && part.data.request) { // It's a tool call
|
||||
const toolName = part.data.tool?.name || part.data.request.name || 'Unknown Tool';
|
||||
const status = part.data.status;
|
||||
messageDiv.innerText += `\n[Tool: ${toolName} (${status})]\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error('Parse error', e, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Keep the last incomplete line in buffer
|
||||
buffer = lines[lines.length - 1];
|
||||
chatHistory.scrollTop = chatHistory.scrollHeight;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -97,6 +97,7 @@ import { isAlternateBufferEnabled } from './ui/hooks/useAlternateBuffer.js';
|
||||
import { setupTerminalAndTheme } from './utils/terminalTheme.js';
|
||||
import { profiler } from './ui/components/DebugProfiler.js';
|
||||
import { runDeferredCommand } from './deferred.js';
|
||||
import { remoteInputServer } from './utils/remoteInputServer.js';
|
||||
|
||||
const SLOW_RENDER_MS = 200;
|
||||
|
||||
@@ -206,6 +207,9 @@ export async function startInteractiveUI(
|
||||
consolePatcher.patch();
|
||||
registerCleanup(consolePatcher.cleanup);
|
||||
|
||||
remoteInputServer.start();
|
||||
registerCleanup(() => remoteInputServer.stop());
|
||||
|
||||
const { stdout: inkStdout, stderr: inkStderr } = createWorkingStdio();
|
||||
|
||||
// Create wrapper component to use hooks inside render
|
||||
|
||||
@@ -993,6 +993,16 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleRemoteInput = (text: string) => {
|
||||
handleFinalSubmit(text);
|
||||
};
|
||||
appEvents.on(AppEvent.RemoteInput, handleRemoteInput);
|
||||
return () => {
|
||||
appEvents.off(AppEvent.RemoteInput, handleRemoteInput);
|
||||
};
|
||||
}, [handleFinalSubmit]);
|
||||
|
||||
const handleClearScreen = useCallback(() => {
|
||||
historyManager.clearItems();
|
||||
clearConsoleMessagesState();
|
||||
|
||||
@@ -11,6 +11,7 @@ export enum AppEvent {
|
||||
Flicker = 'flicker',
|
||||
SelectionWarning = 'selection-warning',
|
||||
PasteTimeout = 'paste-timeout',
|
||||
RemoteInput = 'remote-input',
|
||||
}
|
||||
|
||||
export interface AppEvents {
|
||||
@@ -18,6 +19,7 @@ export interface AppEvents {
|
||||
[AppEvent.Flicker]: never[];
|
||||
[AppEvent.SelectionWarning]: never[];
|
||||
[AppEvent.PasteTimeout]: never[];
|
||||
[AppEvent.RemoteInput]: string[];
|
||||
}
|
||||
|
||||
export const appEvents = new EventEmitter<AppEvents>();
|
||||
|
||||
112
packages/cli/src/utils/remoteInputServer.ts
Normal file
112
packages/cli/src/utils/remoteInputServer.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import http from 'node:http';
|
||||
import { appEvents, AppEvent } from './events.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
|
||||
const REMOTE_INPUT_PORT = 41243;
|
||||
|
||||
export class RemoteInputServer {
|
||||
private server: http.Server | undefined;
|
||||
|
||||
start() {
|
||||
this.server = http.createServer((req, res) => {
|
||||
// Enable CORS
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(204);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === 'POST' && (req.url === '/message' || req.url === '/message/stream')) {
|
||||
let body = '';
|
||||
req.on('data', (chunk) => {
|
||||
body += chunk.toString();
|
||||
});
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const data = JSON.parse(body);
|
||||
// Handle both raw format { text: "..." } and A2A format
|
||||
let text = '';
|
||||
|
||||
// 1. Check for A2A JSON-RPC format
|
||||
if (data.params?.message?.parts?.[0]?.text) {
|
||||
text = data.params.message.parts[0].text;
|
||||
}
|
||||
// 2. Check for simple format
|
||||
else if (data.text) {
|
||||
text = data.text;
|
||||
}
|
||||
// 3. Check for message object (A2A style params body)
|
||||
else if (data.message?.parts?.[0]?.text) {
|
||||
text = data.message.parts[0].text;
|
||||
}
|
||||
// 4. Check for message object (standard content style)
|
||||
else if (data.message?.content?.[0]?.text) {
|
||||
text = data.message.content[0].text;
|
||||
}
|
||||
|
||||
if (text) {
|
||||
debugLogger.log('[RemoteInputServer] Received input:', text);
|
||||
appEvents.emit(AppEvent.RemoteInput, text);
|
||||
|
||||
// Respond with SSE-like structure or just OK
|
||||
// Chrome Extension expects SSE if it hits /message/stream
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive'
|
||||
});
|
||||
res.write(`data: ${JSON.stringify({
|
||||
result: {
|
||||
kind: 'status-update',
|
||||
status: {
|
||||
message: {
|
||||
parts: [{ kind: 'text', text: 'Input received by interactive CLI.' }]
|
||||
}
|
||||
}
|
||||
}
|
||||
})}\n\n`);
|
||||
res.end();
|
||||
} else {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'No text found in body' }));
|
||||
}
|
||||
} catch (e) {
|
||||
debugLogger.error('[RemoteInputServer] Error parsing body:', e);
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
|
||||
this.server.listen(REMOTE_INPUT_PORT, '127.0.0.1', () => {
|
||||
debugLogger.log(`[RemoteInputServer] Listening on http://127.0.0.1:${REMOTE_INPUT_PORT}`);
|
||||
});
|
||||
|
||||
this.server.on('error', (err) => {
|
||||
debugLogger.warn(`[RemoteInputServer] Failed to start: ${err.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.server) {
|
||||
this.server.close();
|
||||
this.server = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const remoteInputServer = new RemoteInputServer();
|
||||
Reference in New Issue
Block a user