Compare commits

...

5 Commits

9 changed files with 89 additions and 29 deletions
+1
View File
@@ -604,6 +604,7 @@ const mockUIActions: UIActions = {
revealCleanUiDetailsTemporarily: vi.fn(),
handleWarning: vi.fn(),
setEmbeddedShellFocused: vi.fn(),
setActivePtyId: vi.fn(),
dismissBackgroundShell: vi.fn(),
setActiveBackgroundShellPid: vi.fn(),
setIsBackgroundShellListOpen: vi.fn(),
+3
View File
@@ -1109,6 +1109,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
backgroundShells,
dismissBackgroundShell,
retryStatus,
setActivePtyId,
} = useGeminiStream(
config.getGeminiClient(),
historyManager.history,
@@ -2509,6 +2510,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
revealCleanUiDetailsTemporarily,
handleWarning,
setEmbeddedShellFocused,
setActivePtyId,
dismissBackgroundShell,
setActiveBackgroundShellPid,
setIsBackgroundShellListOpen,
@@ -2601,6 +2603,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
revealCleanUiDetailsTemporarily,
handleWarning,
setEmbeddedShellFocused,
setActivePtyId,
dismissBackgroundShell,
setActiveBackgroundShellPid,
setIsBackgroundShellListOpen,
@@ -26,7 +26,6 @@ export const StickyHeader: React.FC<StickyHeaderProps> = ({
containerRef,
}) => (
<Box
ref={containerRef}
sticky
minHeight={1}
flexShrink={0}
@@ -58,6 +57,7 @@ export const StickyHeader: React.FC<StickyHeaderProps> = ({
}
>
<Box
ref={containerRef}
borderStyle="round"
width={width}
borderColor={borderColor}
@@ -78,7 +78,7 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
embeddedShellFocused,
);
const { setEmbeddedShellFocused } = useUIActions();
const { setEmbeddedShellFocused, setActivePtyId } = useUIActions();
const wasFocusedRef = React.useRef(false);
React.useEffect(() => {
@@ -102,13 +102,14 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
const handleFocus = () => {
if (isThisShellFocusable) {
setActivePtyId(ptyId ?? null);
setEmbeddedShellFocused(true);
}
};
useMouseClick(headerRef, handleFocus, { isActive: !!isThisShellFocusable });
useMouseClick(headerRef, handleFocus, { isActive: true });
useMouseClick(contentRef, handleFocus, { isActive: !!isThisShellFocusable });
useMouseClick(contentRef, handleFocus, { isActive: true });
const { shouldShowFocusHint } = useFocusHint(
isThisShellFocusable,
@@ -16,12 +16,11 @@ import {
} from '../../constants.js';
import { theme } from '../../semantic-colors.js';
import {
type Config,
SHELL_TOOL_NAME,
isCompletedAskUserTool,
type ToolResultDisplay,
CoreToolCallStatus,
isCompletedAskUserTool
} from '@google/gemini-cli-core';
import type { Config, ToolResultDisplay ,
CoreToolCallStatus} from '@google/gemini-cli-core';
import { useInactivityTimer } from '../../hooks/useInactivityTimer.js';
import { formatCommand } from '../../utils/keybindingUtils.js';
import { Command } from '../../../config/keyBindings.js';
@@ -44,14 +43,10 @@ export function isShellTool(name: string): boolean {
*/
export function isThisShellFocusable(
name: string,
status: CoreToolCallStatus,
config?: Config,
_status: CoreToolCallStatus,
_config?: Config,
): boolean {
return !!(
isShellTool(name) &&
status === CoreToolCallStatus.Executing &&
config?.getEnableInteractiveShell()
);
return isShellTool(name);
}
/**
@@ -59,14 +54,13 @@ export function isThisShellFocusable(
*/
export function isThisShellFocused(
name: string,
status: CoreToolCallStatus,
_status: CoreToolCallStatus,
ptyId?: number,
activeShellPtyId?: number | null,
embeddedShellFocused?: boolean,
): boolean {
return !!(
isShellTool(name) &&
status === CoreToolCallStatus.Executing &&
ptyId === activeShellPtyId &&
embeddedShellFocused
);
@@ -5,12 +5,20 @@
*/
import type React from 'react';
import { useState, useRef, useCallback, useMemo, useLayoutEffect } from 'react';
import { Box, ResizeObserver, type DOMElement } from 'ink';
import {
useState,
useRef,
useCallback,
useMemo,
useLayoutEffect,
useEffect,
} from 'react';
import { Box, ResizeObserver, getBoundingBox, type DOMElement } from 'ink';
import { useKeypress, type Key } from '../../hooks/useKeypress.js';
import { useScrollable } from '../../contexts/ScrollProvider.js';
import { useAnimatedScrollbar } from '../../hooks/useAnimatedScrollbar.js';
import { useBatchedScroll } from '../../hooks/useBatchedScroll.js';
import { useMouse, type MouseEvent } from '../../hooks/useMouse.js';
import { keyMatchers, Command } from '../../keyMatchers.js';
interface ScrollableProps {
@@ -115,6 +123,30 @@ export const Scrollable: React.FC<ScrollableProps> = ({
[scrollToBottom],
);
const [isHovered, setIsHovered] = useState(false);
useMouse(
(event: MouseEvent) => {
if (event.name === 'move' && viewportRef.current) {
const boundingBox = getBoundingBox(viewportRef.current);
if (boundingBox) {
const { x, y, width, height } = boundingBox;
const inside =
event.col >= x &&
event.col < x + width + 1 &&
event.row >= y &&
event.row < y + height;
if (inside !== isHovered) {
setIsHovered(inside);
}
}
}
return false;
},
{ isActive: true },
);
const { getScrollTop, setPendingScrollTop } = useBatchedScroll(scrollTop);
const scrollBy = useCallback(
@@ -135,8 +167,20 @@ export const Scrollable: React.FC<ScrollableProps> = ({
const { scrollbarColor, flashScrollbar, scrollByWithAnimation } =
useAnimatedScrollbar(hasFocus, scrollBy);
// Flash scrollbar on hover for discoverability.
const wasHovered = useRef(isHovered);
useEffect(() => {
if (isHovered && !wasHovered.current && !hasFocus) {
flashScrollbar();
}
wasHovered.current = isHovered;
}, [isHovered, hasFocus, flashScrollbar]);
useKeypress(
(key: Key) => {
if (!hasFocus) {
return false;
}
const { scrollHeight, innerHeight } = sizeRef.current;
const scrollTop = getScrollTop();
const maxScroll = Math.max(0, scrollHeight - innerHeight);
@@ -175,13 +219,20 @@ export const Scrollable: React.FC<ScrollableProps> = ({
);
const getScrollState = useCallback(() => {
if (!hasFocus && !isHovered) {
return {
scrollTop: 0,
scrollHeight: 0,
innerHeight: 0,
};
}
const maxScroll = Math.max(0, size.scrollHeight - size.innerHeight);
return {
scrollTop: Math.min(getScrollTop(), maxScroll),
scrollHeight: size.scrollHeight,
innerHeight: size.innerHeight,
};
}, [getScrollTop, size.scrollHeight, size.innerHeight]);
}, [hasFocus, isHovered, getScrollTop, size.scrollHeight, size.innerHeight]);
const hasFocusCallback = useCallback(() => hasFocus, [hasFocus]);
@@ -206,11 +257,11 @@ export const Scrollable: React.FC<ScrollableProps> = ({
width={width ?? maxWidth}
height={height}
flexDirection="column"
overflowY="scroll"
overflowY={hasFocus || isHovered ? 'scroll' : 'hidden'}
overflowX="hidden"
scrollTop={scrollTop}
flexGrow={flexGrow}
scrollbarThumbColor={scrollbarColor}
scrollbarThumbColor={hasFocus || isHovered ? scrollbarColor : undefined}
>
{/*
This inner box is necessary to prevent the parent from shrinking
@@ -80,6 +80,7 @@ export interface UIActions {
revealCleanUiDetailsTemporarily: (durationMs?: number) => void;
handleWarning: (message: string) => void;
setEmbeddedShellFocused: (value: boolean) => void;
setActivePtyId: (pid: number | null) => void;
dismissBackgroundShell: (pid: number) => void;
setActiveBackgroundShellPid: (pid: number) => void;
setIsBackgroundShellListOpen: (isOpen: boolean) => void;
@@ -152,6 +152,13 @@ export const useShellCommandProcessor = (
[m],
);
const setActivePtyId = useCallback(
(pid: number | null) => {
dispatch({ type: 'SET_ACTIVE_PTY', pid });
},
[dispatch],
);
const toggleBackgroundShell = useCallback(() => {
if (state.backgroundShells.size > 0) {
const willBeVisible = !state.isBackgroundShellVisible;
@@ -550,5 +557,6 @@ export const useShellCommandProcessor = (
registerBackgroundShell,
dismissBackgroundShell,
backgroundShells: state.backgroundShells,
setActivePtyId,
};
};
+8 -7
View File
@@ -204,9 +204,8 @@ export const useGeminiStream = (
consumeUserHint?: () => string | null,
) => {
const [initError, setInitError] = useState<string | null>(null);
const [retryStatus, setRetryStatus] = useState<RetryAttemptPayload | null>(
null,
);
const [modelRetryStatus, setModelRetryStatus] =
useState<RetryAttemptPayload | null>(null);
const isLowErrorVerbosity = settings.merged.ui?.errorVerbosity !== 'full';
const suppressedToolErrorCountRef = useRef(0);
const suppressedToolErrorNoteShownRef = useRef(false);
@@ -242,7 +241,7 @@ export const useGeminiStream = (
useEffect(() => {
const handleRetryAttempt = (payload: RetryAttemptPayload) => {
setRetryStatus(payload);
setModelRetryStatus(payload);
};
coreEvents.on(CoreEvent.RetryAttempt, handleRetryAttempt);
return () => {
@@ -338,6 +337,7 @@ export const useGeminiStream = (
registerBackgroundShell,
dismissBackgroundShell,
backgroundShells,
setActivePtyId,
} = useShellCommandProcessor(
addItem,
setPendingHistoryItem,
@@ -564,7 +564,7 @@ export const useGeminiStream = (
useEffect(() => {
if (!isResponding) {
setRetryStatus(null);
setModelRetryStatus(null);
}
}, [isResponding]);
@@ -844,7 +844,7 @@ export const useGeminiStream = (
currentGeminiMessageBuffer: string,
userMessageTimestamp: number,
): string => {
setRetryStatus(null);
setModelRetryStatus(null);
if (turnCancelledRef.current) {
// Prevents additional output after a user initiated cancel.
return '';
@@ -1897,6 +1897,7 @@ export const useGeminiStream = (
backgroundCurrentShell,
backgroundShells,
dismissBackgroundShell,
retryStatus,
retryStatus: modelRetryStatus,
setActivePtyId,
};
};