mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-05 06:31:44 -07:00
44259a00e2
Checkpoint optimizing virtualized list Fixes for fallback rendering where terminalBuffer=false Change terminalBuffer false back to the default while we fix performance with very large chats. Checkpoint changes to virtualized list. Fix virtualized list NO commit Update ink version. Fix UI snapshot mismatch in MainContent tests and VirtualizedList computation Checkpoint. Optimize scrolling checkpoint Fix tests and resolve remaining issues after rebase - Fixed ToolStickyHeaderRegression scrolling logic. - Added MouseProvider to VirtualizedList test. - Updated snapshots to reflect layout changes in optimized virtual list. Fix flaky plan-mode test by removing model request assertions Fix unit tests by updating expected decisions for tool permissions after rebase use onStaticRender in VirtualizedList Checkpoint VirtualizedListClick Update version Bug fixes. Code review comment fixes. settings.json hack Update version and local tweaks.
211 lines
5.5 KiB
TypeScript
211 lines
5.5 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { useStdin } from 'ink';
|
|
import type React from 'react';
|
|
import {
|
|
createContext,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useLayoutEffect,
|
|
useMemo,
|
|
useRef,
|
|
} from 'react';
|
|
import { ESC } from '../utils/input.js';
|
|
import { debugLogger } from '@google/gemini-cli-core';
|
|
import { appEvents, AppEvent } from '../../utils/events.js';
|
|
import {
|
|
isIncompleteMouseSequence,
|
|
parseMouseEvent,
|
|
type MouseEvent,
|
|
type MouseEventName,
|
|
type MouseHandler,
|
|
DOUBLE_CLICK_THRESHOLD_MS,
|
|
DOUBLE_CLICK_DISTANCE_TOLERANCE,
|
|
} from '../utils/mouse.js';
|
|
import { useSettingsStore } from './SettingsContext.js';
|
|
|
|
export type { MouseEvent, MouseEventName, MouseHandler };
|
|
|
|
const MAX_MOUSE_BUFFER_SIZE = 4096;
|
|
|
|
interface MouseContextValue {
|
|
subscribe: (handler: MouseHandler) => void;
|
|
unsubscribe: (handler: MouseHandler) => void;
|
|
broadcast: (event: MouseEvent) => void;
|
|
}
|
|
|
|
const MouseContext = createContext<MouseContextValue | undefined>(undefined);
|
|
|
|
export function useMouseContext() {
|
|
const context = useContext(MouseContext);
|
|
if (!context) {
|
|
throw new Error('useMouseContext must be used within a MouseProvider');
|
|
}
|
|
return context;
|
|
}
|
|
|
|
export function useMouse(handler: MouseHandler, { isActive = true } = {}) {
|
|
const { subscribe, unsubscribe } = useMouseContext();
|
|
|
|
useLayoutEffect(() => {
|
|
if (!isActive) {
|
|
return;
|
|
}
|
|
|
|
subscribe(handler);
|
|
return () => unsubscribe(handler);
|
|
}, [isActive, handler, subscribe, unsubscribe]);
|
|
}
|
|
|
|
export function MouseProvider({
|
|
children,
|
|
mouseEventsEnabled,
|
|
}: {
|
|
children: React.ReactNode;
|
|
mouseEventsEnabled?: boolean;
|
|
}) {
|
|
const { settings } = useSettingsStore();
|
|
const debugKeystrokeLogging = settings.merged.general.debugKeystrokeLogging;
|
|
|
|
const { stdin } = useStdin();
|
|
const subscribers = useRef<Set<MouseHandler>>(new Set()).current;
|
|
const lastClickRef = useRef<{
|
|
time: number;
|
|
col: number;
|
|
row: number;
|
|
} | null>(null);
|
|
|
|
const subscribe = useCallback(
|
|
(handler: MouseHandler) => {
|
|
subscribers.add(handler);
|
|
},
|
|
[subscribers],
|
|
);
|
|
|
|
const unsubscribe = useCallback(
|
|
(handler: MouseHandler) => {
|
|
subscribers.delete(handler);
|
|
},
|
|
[subscribers],
|
|
);
|
|
|
|
const broadcast = useCallback(
|
|
(event: MouseEvent) => {
|
|
let handled = false;
|
|
for (const handler of subscribers) {
|
|
if (handler(event) === true) {
|
|
handled = true;
|
|
}
|
|
}
|
|
|
|
if (event.name === 'left-press') {
|
|
const now = Date.now();
|
|
const lastClick = lastClickRef.current;
|
|
if (
|
|
lastClick &&
|
|
now - lastClick.time < DOUBLE_CLICK_THRESHOLD_MS &&
|
|
Math.abs(event.col - lastClick.col) <=
|
|
DOUBLE_CLICK_DISTANCE_TOLERANCE &&
|
|
Math.abs(event.row - lastClick.row) <= DOUBLE_CLICK_DISTANCE_TOLERANCE
|
|
) {
|
|
const doubleClickEvent: MouseEvent = {
|
|
...event,
|
|
name: 'double-click',
|
|
};
|
|
for (const handler of subscribers) {
|
|
handler(doubleClickEvent);
|
|
}
|
|
lastClickRef.current = null;
|
|
} else {
|
|
lastClickRef.current = { time: now, col: event.col, row: event.row };
|
|
}
|
|
}
|
|
|
|
if (
|
|
!handled &&
|
|
event.name === 'move' &&
|
|
event.col >= 0 &&
|
|
event.row >= 0 &&
|
|
event.button === 'left'
|
|
) {
|
|
// Terminal apps only receive mouse move events when the mouse is down
|
|
// so this always indicates a mouse drag that the user was expecting
|
|
// would trigger text selection but does not as we are handling mouse
|
|
// events not the terminal.
|
|
appEvents.emit(AppEvent.SelectionWarning);
|
|
}
|
|
},
|
|
[subscribers],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (!mouseEventsEnabled) {
|
|
return;
|
|
}
|
|
|
|
let mouseBuffer = '';
|
|
|
|
const handleData = (data: Buffer | string) => {
|
|
mouseBuffer += typeof data === 'string' ? data : data.toString('utf-8');
|
|
|
|
// Safety cap to prevent infinite buffer growth on garbage
|
|
if (mouseBuffer.length > MAX_MOUSE_BUFFER_SIZE) {
|
|
mouseBuffer = mouseBuffer.slice(-MAX_MOUSE_BUFFER_SIZE);
|
|
}
|
|
|
|
while (mouseBuffer.length > 0) {
|
|
const parsed = parseMouseEvent(mouseBuffer);
|
|
|
|
if (parsed) {
|
|
if (debugKeystrokeLogging) {
|
|
debugLogger.log(
|
|
'[DEBUG] Mouse event parsed:',
|
|
JSON.stringify(parsed.event),
|
|
);
|
|
}
|
|
broadcast(parsed.event);
|
|
mouseBuffer = mouseBuffer.slice(parsed.length);
|
|
continue;
|
|
}
|
|
|
|
if (isIncompleteMouseSequence(mouseBuffer)) {
|
|
break; // Wait for more data
|
|
}
|
|
|
|
// Not a valid sequence at start, and not waiting for more data.
|
|
// Discard garbage until next possible sequence start.
|
|
const nextEsc = mouseBuffer.indexOf(ESC, 1);
|
|
if (nextEsc !== -1) {
|
|
mouseBuffer = mouseBuffer.slice(nextEsc);
|
|
// Loop continues to try parsing at new location
|
|
} else {
|
|
mouseBuffer = '';
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
stdin.on('data', handleData);
|
|
|
|
return () => {
|
|
stdin.removeListener('data', handleData);
|
|
};
|
|
}, [stdin, mouseEventsEnabled, broadcast, debugKeystrokeLogging]);
|
|
|
|
const contextValue = useMemo(
|
|
() => ({ subscribe, unsubscribe, broadcast }),
|
|
[subscribe, unsubscribe, broadcast],
|
|
);
|
|
|
|
return (
|
|
<MouseContext.Provider value={contextValue}>
|
|
{children}
|
|
</MouseContext.Provider>
|
|
);
|
|
}
|