mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-01 00:40:42 -07:00
183 lines
4.6 KiB
TypeScript
183 lines
4.6 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { useCallback, useSyncExternalStore } from 'react';
|
|
import type { ConsoleMessageItem } from '../types.js';
|
|
import {
|
|
coreEvents,
|
|
CoreEvent,
|
|
type ConsoleLogPayload,
|
|
} from '@google/gemini-cli-core';
|
|
|
|
export interface UseErrorCountReturn {
|
|
errorCount: number;
|
|
clearErrorCount: () => void;
|
|
}
|
|
|
|
// --- Global Console Store ---
|
|
|
|
const MAX_CONSOLE_MESSAGES = 1000;
|
|
let globalConsoleMessages: ConsoleMessageItem[] = [];
|
|
let globalErrorCount = 0;
|
|
const listeners = new Set<() => void>();
|
|
|
|
let messageQueue: ConsoleMessageItem[] = [];
|
|
let timeoutId: NodeJS.Timeout | null = null;
|
|
|
|
/**
|
|
* Initializes the global console store and subscribes to coreEvents.
|
|
* Acts as a safe reset function, making it idempotent and useful for test isolation.
|
|
* Must be called during application startup.
|
|
*/
|
|
export function initializeConsoleStore() {
|
|
if (timeoutId) {
|
|
clearTimeout(timeoutId);
|
|
timeoutId = null;
|
|
}
|
|
messageQueue = [];
|
|
globalConsoleMessages = [];
|
|
globalErrorCount = 0;
|
|
notifyListeners();
|
|
|
|
// Safely detach first to ensure idempotency and prevent listener leaks
|
|
coreEvents.off(CoreEvent.ConsoleLog, handleConsoleLog);
|
|
coreEvents.off(CoreEvent.Output, handleOutput);
|
|
|
|
coreEvents.on(CoreEvent.ConsoleLog, handleConsoleLog);
|
|
coreEvents.on(CoreEvent.Output, handleOutput);
|
|
}
|
|
|
|
function notifyListeners() {
|
|
for (const listener of listeners) {
|
|
listener();
|
|
}
|
|
}
|
|
|
|
function processQueue() {
|
|
if (messageQueue.length === 0) return;
|
|
|
|
// Create a new array to trigger React updates
|
|
const newMessages = [...globalConsoleMessages];
|
|
|
|
for (const queuedMessage of messageQueue) {
|
|
if (queuedMessage.type === 'error') {
|
|
globalErrorCount++;
|
|
}
|
|
|
|
// Coalesce consecutive identical messages
|
|
const prev = newMessages[newMessages.length - 1];
|
|
if (
|
|
prev &&
|
|
prev.type === queuedMessage.type &&
|
|
prev.content === queuedMessage.content
|
|
) {
|
|
newMessages[newMessages.length - 1] = {
|
|
...prev,
|
|
count: prev.count + 1,
|
|
};
|
|
} else {
|
|
newMessages.push({ ...queuedMessage, count: 1 });
|
|
}
|
|
}
|
|
|
|
globalConsoleMessages =
|
|
newMessages.length > MAX_CONSOLE_MESSAGES
|
|
? newMessages.slice(-MAX_CONSOLE_MESSAGES)
|
|
: newMessages;
|
|
|
|
messageQueue = [];
|
|
timeoutId = null;
|
|
notifyListeners();
|
|
}
|
|
|
|
function handleNewMessage(message: ConsoleMessageItem) {
|
|
messageQueue.push(message);
|
|
if (!timeoutId) {
|
|
// Batch updates using a timeout. 50ms is a reasonable delay to batch
|
|
// rapid-fire messages without noticeable lag while avoiding React update
|
|
// queue flooding.
|
|
timeoutId = setTimeout(processQueue, 50);
|
|
}
|
|
}
|
|
|
|
// --- Subscription API for useSyncExternalStore ---
|
|
|
|
function subscribe(listener: () => void) {
|
|
listeners.add(listener);
|
|
return () => {
|
|
listeners.delete(listener);
|
|
};
|
|
}
|
|
|
|
function getConsoleMessagesSnapshot() {
|
|
return globalConsoleMessages;
|
|
}
|
|
|
|
function getErrorCountSnapshot() {
|
|
return globalErrorCount;
|
|
}
|
|
|
|
// --- Core Event Listeners (Always active at module level) ---
|
|
|
|
const handleConsoleLog = (payload: ConsoleLogPayload) => {
|
|
let content = payload.content;
|
|
const MAX_CONSOLE_MSG_LENGTH = 10000;
|
|
if (content.length > MAX_CONSOLE_MSG_LENGTH) {
|
|
content =
|
|
content.slice(0, MAX_CONSOLE_MSG_LENGTH) +
|
|
`... [Truncated ${content.length - MAX_CONSOLE_MSG_LENGTH} characters]`;
|
|
}
|
|
|
|
handleNewMessage({
|
|
type: payload.type,
|
|
content,
|
|
count: 1,
|
|
});
|
|
};
|
|
|
|
const handleOutput = (payload: {
|
|
isStderr: boolean;
|
|
chunk: Uint8Array | string;
|
|
}) => {
|
|
let content =
|
|
typeof payload.chunk === 'string'
|
|
? payload.chunk
|
|
: new TextDecoder().decode(payload.chunk);
|
|
|
|
const MAX_OUTPUT_CHUNK_LENGTH = 10000;
|
|
if (content.length > MAX_OUTPUT_CHUNK_LENGTH) {
|
|
content =
|
|
content.slice(0, MAX_OUTPUT_CHUNK_LENGTH) +
|
|
`... [Truncated ${content.length - MAX_OUTPUT_CHUNK_LENGTH} characters]`;
|
|
}
|
|
|
|
handleNewMessage({ type: 'log', content, count: 1 });
|
|
};
|
|
|
|
/**
|
|
* Hook to access the global console message history.
|
|
* Decoupled from any component lifecycle to ensure history is preserved even
|
|
* when the UI is unmounted.
|
|
*/
|
|
export function useConsoleMessages(): ConsoleMessageItem[] {
|
|
return useSyncExternalStore(subscribe, getConsoleMessagesSnapshot);
|
|
}
|
|
|
|
/**
|
|
* Hook to access the global error count.
|
|
* Uses the same external store as useConsoleMessages for consistency.
|
|
*/
|
|
export function useErrorCount(): UseErrorCountReturn {
|
|
const errorCount = useSyncExternalStore(subscribe, getErrorCountSnapshot);
|
|
|
|
const clearErrorCount = useCallback(() => {
|
|
globalErrorCount = 0;
|
|
notifyListeners();
|
|
}, []);
|
|
|
|
return { errorCount, clearErrorCount };
|
|
}
|