qwen-code/packages/cli/src/core/geminiStreamProcessor.ts

142 lines
4.9 KiB
TypeScript
Raw Normal View History

Initial commit of Gemini Code CLI This commit introduces the initial codebase for the Gemini Code CLI, a command-line interface designed to facilitate interaction with the Gemini API for software engineering tasks. The code was migrated from a previous git repository as a single squashed commit. Core Features & Components: * **Gemini Integration:** Leverages the `@google/genai` SDK to interact with the Gemini models, supporting chat history, streaming responses, and function calling (tools). * **Terminal UI:** Built with Ink (React for CLIs) providing an interactive chat interface within the terminal, including input prompts, message display, loading indicators, and tool interaction elements. * **Tooling Framework:** Implements a robust tool system allowing Gemini to interact with the local environment. Includes tools for: * File system listing (`ls`) * File reading (`read-file`) * Content searching (`grep`) * File globbing (`glob`) * File editing (`edit`) * File writing (`write-file`) * Executing bash commands (`terminal`) * **State Management:** Handles the streaming state of Gemini responses and manages the conversation history. * **Configuration:** Parses command-line arguments (`yargs`) and loads environment variables (`dotenv`) for setup. * **Project Structure:** Organized into `core`, `ui`, `tools`, `config`, and `utils` directories using TypeScript. Includes basic build (`tsc`) and start scripts. This initial version establishes the foundation for a powerful CLI tool enabling developers to use Gemini for coding assistance directly in their terminal environment. --- Created by yours truly: __Gemini Code__
2025-04-15 21:41:08 -07:00
import { Part } from '@google/genai';
import { HistoryItem } from '../ui/types.js';
import { GeminiEventType, GeminiStream } from './GeminiStream.js';
import { handleToolCallChunk, addErrorMessageToHistory } from './historyUpdater.js';
interface StreamProcessorParams {
stream: GeminiStream;
signal: AbortSignal;
setHistory: React.Dispatch<React.SetStateAction<HistoryItem[]>>;
submitQuery: (query: Part) => Promise<void>,
getNextMessageId: () => number;
addHistoryItem: (itemData: Omit<HistoryItem, 'id'>, id: number) => void;
currentToolGroupIdRef: React.MutableRefObject<number | null>;
}
/**
* Processes the Gemini stream, managing text buffering, adaptive rendering,
* and delegating history updates for tool calls and errors.
*/
export const processGeminiStream = async ({ // Renamed function for clarity
stream,
signal,
setHistory,
submitQuery,
getNextMessageId,
addHistoryItem,
currentToolGroupIdRef,
}: StreamProcessorParams): Promise<void> => {
// --- State specific to this stream processing invocation ---
let textBuffer = '';
let renderTimeoutId: NodeJS.Timeout | null = null;
let isStreamComplete = false;
let currentGeminiMessageId: number | null = null;
const render = (content: string) => {
if (currentGeminiMessageId === null) {
return;
}
setHistory(prev => prev.map(item =>
item.id === currentGeminiMessageId && item.type === 'gemini'
? { ...item, text: (item.text ?? '') + content }
: item
));
}
// --- Adaptive Rendering Logic (nested) ---
const renderBufferedText = () => {
if (signal.aborted) {
if (renderTimeoutId) clearTimeout(renderTimeoutId);
renderTimeoutId = null;
return;
}
const bufferLength = textBuffer.length;
let chunkSize = 0;
let delay = 50;
if (bufferLength > 150) {
chunkSize = Math.min(bufferLength, 30); delay = 5;
} else if (bufferLength > 30) {
chunkSize = Math.min(bufferLength, 10); delay = 10;
} else if (bufferLength > 0) {
chunkSize = 2; delay = 20;
}
if (chunkSize > 0) {
const chunkToRender = textBuffer.substring(0, chunkSize);
textBuffer = textBuffer.substring(chunkSize);
render(chunkToRender);
renderTimeoutId = setTimeout(renderBufferedText, delay);
} else {
renderTimeoutId = null; // Clear timeout ID if nothing to render
if (!isStreamComplete) {
// Buffer empty, but stream might still send data, check again later
renderTimeoutId = setTimeout(renderBufferedText, 50);
}
}
};
const scheduleRender = () => {
if (renderTimeoutId === null) {
renderTimeoutId = setTimeout(renderBufferedText, 0);
}
};
// --- Stream Processing Loop ---
try {
for await (const chunk of stream) {
if (signal.aborted) break;
if (chunk.type === GeminiEventType.Content) {
currentToolGroupIdRef.current = null; // Reset tool group on text
if (currentGeminiMessageId === null) {
currentGeminiMessageId = getNextMessageId();
addHistoryItem({ type: 'gemini', text: '' }, currentGeminiMessageId);
textBuffer = '';
}
textBuffer += chunk.value;
scheduleRender();
} else if (chunk.type === GeminiEventType.ToolCallInfo) {
if (renderTimeoutId) { // Stop rendering loop
clearTimeout(renderTimeoutId);
renderTimeoutId = null;
}
// Flush any text buffer content.
render(textBuffer);
currentGeminiMessageId = null; // End text message context
textBuffer = ''; // Clear buffer
// Delegate history update for tool call
handleToolCallChunk(
chunk.value,
setHistory,
submitQuery,
getNextMessageId,
currentToolGroupIdRef
);
}
}
if (signal.aborted) {
throw new Error("Request cancelled by user");
}
} catch (error: any) {
if (renderTimeoutId) { // Ensure render loop stops on error
clearTimeout(renderTimeoutId);
renderTimeoutId = null;
}
// Delegate history update for error message
addErrorMessageToHistory(error, setHistory, getNextMessageId);
} finally {
isStreamComplete = true; // Signal stream end for render loop completion
if (renderTimeoutId) {
clearTimeout(renderTimeoutId);
renderTimeoutId = null;
}
renderBufferedText(); // Force final render
}
};