mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-12 21:03:05 -07:00
Co-authored-by: Gal Zahavi <38544478+galz10@users.noreply.github.com>
This commit is contained in:
@@ -5,17 +5,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box, Text, type DOMElement } from 'ink';
|
import { Box, type DOMElement } from 'ink';
|
||||||
import { ToolCallStatus } from '../../types.js';
|
|
||||||
import { ShellInputPrompt } from '../ShellInputPrompt.js';
|
import { ShellInputPrompt } from '../ShellInputPrompt.js';
|
||||||
import { StickyHeader } from '../StickyHeader.js';
|
import { StickyHeader } from '../StickyHeader.js';
|
||||||
import {
|
|
||||||
SHELL_COMMAND_NAME,
|
|
||||||
SHELL_NAME,
|
|
||||||
SHELL_FOCUS_HINT_DELAY_MS,
|
|
||||||
} from '../../constants.js';
|
|
||||||
import { theme } from '../../semantic-colors.js';
|
|
||||||
import { SHELL_TOOL_NAME } from '@google/gemini-cli-core';
|
|
||||||
import { useUIActions } from '../../contexts/UIActionsContext.js';
|
import { useUIActions } from '../../contexts/UIActionsContext.js';
|
||||||
import { useMouseClick } from '../../hooks/useMouseClick.js';
|
import { useMouseClick } from '../../hooks/useMouseClick.js';
|
||||||
import { ToolResultDisplay } from './ToolResultDisplay.js';
|
import { ToolResultDisplay } from './ToolResultDisplay.js';
|
||||||
@@ -24,6 +16,10 @@ import {
|
|||||||
ToolInfo,
|
ToolInfo,
|
||||||
TrailingIndicator,
|
TrailingIndicator,
|
||||||
STATUS_INDICATOR_WIDTH,
|
STATUS_INDICATOR_WIDTH,
|
||||||
|
isThisShellFocusable as checkIsShellFocusable,
|
||||||
|
isThisShellFocused as checkIsShellFocused,
|
||||||
|
useFocusHint,
|
||||||
|
FocusHint,
|
||||||
} from './ToolShared.js';
|
} from './ToolShared.js';
|
||||||
import type { ToolMessageProps } from './ToolMessage.js';
|
import type { ToolMessageProps } from './ToolMessage.js';
|
||||||
import type { Config } from '@google/gemini-cli-core';
|
import type { Config } from '@google/gemini-cli-core';
|
||||||
@@ -65,13 +61,13 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
|
|||||||
|
|
||||||
borderDimColor,
|
borderDimColor,
|
||||||
}) => {
|
}) => {
|
||||||
const isThisShellFocused =
|
const isThisShellFocused = checkIsShellFocused(
|
||||||
(name === SHELL_COMMAND_NAME ||
|
name,
|
||||||
name === SHELL_NAME ||
|
status,
|
||||||
name === SHELL_TOOL_NAME) &&
|
ptyId,
|
||||||
status === ToolCallStatus.Executing &&
|
activeShellPtyId,
|
||||||
ptyId === activeShellPtyId &&
|
embeddedShellFocused,
|
||||||
embeddedShellFocused;
|
);
|
||||||
|
|
||||||
const { setEmbeddedShellFocused } = useUIActions();
|
const { setEmbeddedShellFocused } = useUIActions();
|
||||||
|
|
||||||
@@ -81,12 +77,7 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
|
|||||||
|
|
||||||
// The shell is focusable if it's the shell command, it's executing, and the interactive shell is enabled.
|
// The shell is focusable if it's the shell command, it's executing, and the interactive shell is enabled.
|
||||||
|
|
||||||
const isThisShellFocusable =
|
const isThisShellFocusable = checkIsShellFocusable(name, status, config);
|
||||||
(name === SHELL_COMMAND_NAME ||
|
|
||||||
name === SHELL_NAME ||
|
|
||||||
name === SHELL_TOOL_NAME) &&
|
|
||||||
status === ToolCallStatus.Executing &&
|
|
||||||
config?.getEnableInteractiveShell();
|
|
||||||
|
|
||||||
const handleFocus = () => {
|
const handleFocus = () => {
|
||||||
if (isThisShellFocusable) {
|
if (isThisShellFocusable) {
|
||||||
@@ -112,38 +103,11 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
|
|||||||
}
|
}
|
||||||
}, [isThisShellFocused, embeddedShellFocused, setEmbeddedShellFocused]);
|
}, [isThisShellFocused, embeddedShellFocused, setEmbeddedShellFocused]);
|
||||||
|
|
||||||
const [lastUpdateTime, setLastUpdateTime] = React.useState<Date | null>(null);
|
const { shouldShowFocusHint } = useFocusHint(
|
||||||
|
isThisShellFocusable,
|
||||||
const [userHasFocused, setUserHasFocused] = React.useState(false);
|
isThisShellFocused,
|
||||||
|
resultDisplay,
|
||||||
const [showFocusHint, setShowFocusHint] = React.useState(false);
|
);
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (resultDisplay) {
|
|
||||||
setLastUpdateTime(new Date());
|
|
||||||
}
|
|
||||||
}, [resultDisplay]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!lastUpdateTime) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
setShowFocusHint(true);
|
|
||||||
}, SHELL_FOCUS_HINT_DELAY_MS);
|
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [lastUpdateTime]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (isThisShellFocused) {
|
|
||||||
setUserHasFocused(true);
|
|
||||||
}
|
|
||||||
}, [isThisShellFocused]);
|
|
||||||
|
|
||||||
const shouldShowFocusHint =
|
|
||||||
isThisShellFocusable && (showFocusHint || userHasFocused);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -163,13 +127,10 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
|
|||||||
emphasis={emphasis}
|
emphasis={emphasis}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{shouldShowFocusHint && (
|
<FocusHint
|
||||||
<Box marginLeft={1} flexShrink={0}>
|
shouldShowFocusHint={shouldShowFocusHint}
|
||||||
<Text color={theme.text.accent}>
|
isThisShellFocused={isThisShellFocused}
|
||||||
{isThisShellFocused ? '(Focused)' : '(tab to focus)'}
|
/>
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{emphasis === 'high' && <TrailingIndicator />}
|
{emphasis === 'high' && <TrailingIndicator />}
|
||||||
</StickyHeader>
|
</StickyHeader>
|
||||||
|
|||||||
@@ -13,9 +13,8 @@ import { ToolMessage } from './ToolMessage.js';
|
|||||||
import { ShellToolMessage } from './ShellToolMessage.js';
|
import { ShellToolMessage } from './ShellToolMessage.js';
|
||||||
import { ToolConfirmationMessage } from './ToolConfirmationMessage.js';
|
import { ToolConfirmationMessage } from './ToolConfirmationMessage.js';
|
||||||
import { theme } from '../../semantic-colors.js';
|
import { theme } from '../../semantic-colors.js';
|
||||||
import { SHELL_COMMAND_NAME, SHELL_NAME } from '../../constants.js';
|
|
||||||
import { SHELL_TOOL_NAME } from '@google/gemini-cli-core';
|
|
||||||
import { useConfig } from '../../contexts/ConfigContext.js';
|
import { useConfig } from '../../contexts/ConfigContext.js';
|
||||||
|
import { isShellTool, isThisShellFocused } from './ToolShared.js';
|
||||||
|
|
||||||
interface ToolGroupMessageProps {
|
interface ToolGroupMessageProps {
|
||||||
groupId: number;
|
groupId: number;
|
||||||
@@ -37,21 +36,22 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
|||||||
activeShellPtyId,
|
activeShellPtyId,
|
||||||
embeddedShellFocused,
|
embeddedShellFocused,
|
||||||
}) => {
|
}) => {
|
||||||
const isEmbeddedShellFocused =
|
const isEmbeddedShellFocused = toolCalls.some((t) =>
|
||||||
embeddedShellFocused &&
|
isThisShellFocused(
|
||||||
toolCalls.some(
|
t.name,
|
||||||
(t) =>
|
t.status,
|
||||||
t.ptyId === activeShellPtyId && t.status === ToolCallStatus.Executing,
|
t.ptyId,
|
||||||
);
|
activeShellPtyId,
|
||||||
|
embeddedShellFocused,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const hasPending = !toolCalls.every(
|
const hasPending = !toolCalls.every(
|
||||||
(t) => t.status === ToolCallStatus.Success,
|
(t) => t.status === ToolCallStatus.Success,
|
||||||
);
|
);
|
||||||
|
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
const isShellCommand = toolCalls.some(
|
const isShellCommand = toolCalls.some((t) => isShellTool(t.name));
|
||||||
(t) => t.name === SHELL_COMMAND_NAME || t.name === SHELL_NAME,
|
|
||||||
);
|
|
||||||
const borderColor =
|
const borderColor =
|
||||||
(isShellCommand && hasPending) || isEmbeddedShellFocused
|
(isShellCommand && hasPending) || isEmbeddedShellFocused
|
||||||
? theme.ui.symbol
|
? theme.ui.symbol
|
||||||
@@ -105,10 +105,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
|||||||
{toolCalls.map((tool, index) => {
|
{toolCalls.map((tool, index) => {
|
||||||
const isConfirming = toolAwaitingApproval?.callId === tool.callId;
|
const isConfirming = toolAwaitingApproval?.callId === tool.callId;
|
||||||
const isFirst = index === 0;
|
const isFirst = index === 0;
|
||||||
const isShellTool =
|
const isShellToolCall = isShellTool(tool.name);
|
||||||
tool.name === SHELL_COMMAND_NAME ||
|
|
||||||
tool.name === SHELL_NAME ||
|
|
||||||
tool.name === SHELL_TOOL_NAME;
|
|
||||||
|
|
||||||
const commonProps = {
|
const commonProps = {
|
||||||
...tool,
|
...tool,
|
||||||
@@ -131,7 +128,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
|||||||
minHeight={1}
|
minHeight={1}
|
||||||
width={terminalWidth}
|
width={terminalWidth}
|
||||||
>
|
>
|
||||||
{isShellTool ? (
|
{isShellToolCall ? (
|
||||||
<ShellToolMessage
|
<ShellToolMessage
|
||||||
{...commonProps}
|
{...commonProps}
|
||||||
activeShellPtyId={activeShellPtyId}
|
activeShellPtyId={activeShellPtyId}
|
||||||
|
|||||||
@@ -5,8 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { useState, useEffect } from 'react';
|
import { Box } from 'ink';
|
||||||
import { Box, Text } from 'ink';
|
|
||||||
import type { IndividualToolCallDisplay } from '../../types.js';
|
import type { IndividualToolCallDisplay } from '../../types.js';
|
||||||
import { StickyHeader } from '../StickyHeader.js';
|
import { StickyHeader } from '../StickyHeader.js';
|
||||||
import { ToolResultDisplay } from './ToolResultDisplay.js';
|
import { ToolResultDisplay } from './ToolResultDisplay.js';
|
||||||
@@ -16,15 +15,12 @@ import {
|
|||||||
TrailingIndicator,
|
TrailingIndicator,
|
||||||
type TextEmphasis,
|
type TextEmphasis,
|
||||||
STATUS_INDICATOR_WIDTH,
|
STATUS_INDICATOR_WIDTH,
|
||||||
|
isThisShellFocusable as checkIsShellFocusable,
|
||||||
|
isThisShellFocused as checkIsShellFocused,
|
||||||
|
useFocusHint,
|
||||||
|
FocusHint,
|
||||||
} from './ToolShared.js';
|
} from './ToolShared.js';
|
||||||
import {
|
import { type Config } from '@google/gemini-cli-core';
|
||||||
SHELL_COMMAND_NAME,
|
|
||||||
SHELL_FOCUS_HINT_DELAY_MS,
|
|
||||||
} from '../../constants.js';
|
|
||||||
import { theme } from '../../semantic-colors.js';
|
|
||||||
import type { Config } from '@google/gemini-cli-core';
|
|
||||||
import { useInactivityTimer } from '../../hooks/useInactivityTimer.js';
|
|
||||||
import { ToolCallStatus } from '../../types.js';
|
|
||||||
import { ShellInputPrompt } from '../ShellInputPrompt.js';
|
import { ShellInputPrompt } from '../ShellInputPrompt.js';
|
||||||
|
|
||||||
export type { TextEmphasis };
|
export type { TextEmphasis };
|
||||||
@@ -60,39 +56,21 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
|||||||
ptyId,
|
ptyId,
|
||||||
config,
|
config,
|
||||||
}) => {
|
}) => {
|
||||||
const isThisShellFocused =
|
const isThisShellFocused = checkIsShellFocused(
|
||||||
(name === SHELL_COMMAND_NAME || name === 'Shell') &&
|
name,
|
||||||
status === ToolCallStatus.Executing &&
|
status,
|
||||||
ptyId === activeShellPtyId &&
|
ptyId,
|
||||||
embeddedShellFocused;
|
activeShellPtyId,
|
||||||
|
embeddedShellFocused,
|
||||||
const [lastUpdateTime, setLastUpdateTime] = useState<Date | null>(null);
|
|
||||||
const [userHasFocused, setUserHasFocused] = useState(false);
|
|
||||||
const showFocusHint = useInactivityTimer(
|
|
||||||
!!lastUpdateTime,
|
|
||||||
lastUpdateTime ? lastUpdateTime.getTime() : 0,
|
|
||||||
SHELL_FOCUS_HINT_DELAY_MS,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
const isThisShellFocusable = checkIsShellFocusable(name, status, config);
|
||||||
if (resultDisplay) {
|
|
||||||
setLastUpdateTime(new Date());
|
|
||||||
}
|
|
||||||
}, [resultDisplay]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const { shouldShowFocusHint } = useFocusHint(
|
||||||
if (isThisShellFocused) {
|
isThisShellFocusable,
|
||||||
setUserHasFocused(true);
|
isThisShellFocused,
|
||||||
}
|
resultDisplay,
|
||||||
}, [isThisShellFocused]);
|
);
|
||||||
|
|
||||||
const isThisShellFocusable =
|
|
||||||
(name === SHELL_COMMAND_NAME || name === 'Shell') &&
|
|
||||||
status === ToolCallStatus.Executing &&
|
|
||||||
config?.getEnableInteractiveShell();
|
|
||||||
|
|
||||||
const shouldShowFocusHint =
|
|
||||||
isThisShellFocusable && (showFocusHint || userHasFocused);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// It is crucial we don't replace this <> with a Box because otherwise the
|
// It is crucial we don't replace this <> with a Box because otherwise the
|
||||||
@@ -112,13 +90,10 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
|||||||
description={description}
|
description={description}
|
||||||
emphasis={emphasis}
|
emphasis={emphasis}
|
||||||
/>
|
/>
|
||||||
{shouldShowFocusHint && (
|
<FocusHint
|
||||||
<Box marginLeft={1} flexShrink={0}>
|
shouldShowFocusHint={shouldShowFocusHint}
|
||||||
<Text color={theme.text.accent}>
|
isThisShellFocused={isThisShellFocused}
|
||||||
{isThisShellFocused ? '(Focused)' : '(tab to focus)'}
|
/>
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
{emphasis === 'high' && <TrailingIndicator />}
|
{emphasis === 'high' && <TrailingIndicator />}
|
||||||
</StickyHeader>
|
</StickyHeader>
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { act } from 'react';
|
||||||
|
import { ToolMessage } from './ToolMessage.js';
|
||||||
|
import { ShellToolMessage } from './ShellToolMessage.js';
|
||||||
|
import { ToolCallStatus, StreamingState } from '../../types.js';
|
||||||
|
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
import {
|
||||||
|
SHELL_COMMAND_NAME,
|
||||||
|
SHELL_FOCUS_HINT_DELAY_MS,
|
||||||
|
} from '../../constants.js';
|
||||||
|
import type { Config, ToolResultDisplay } from '@google/gemini-cli-core';
|
||||||
|
|
||||||
|
vi.mock('../GeminiRespondingSpinner.js', () => ({
|
||||||
|
GeminiRespondingSpinner: () => null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('./ToolResultDisplay.js', () => ({
|
||||||
|
ToolResultDisplay: () => null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Focus Hint', () => {
|
||||||
|
const mockConfig = {
|
||||||
|
getEnableInteractiveShell: () => true,
|
||||||
|
} as Config;
|
||||||
|
|
||||||
|
const baseProps = {
|
||||||
|
callId: 'tool-123',
|
||||||
|
name: SHELL_COMMAND_NAME,
|
||||||
|
description: 'A tool for testing',
|
||||||
|
resultDisplay: undefined as ToolResultDisplay | undefined,
|
||||||
|
status: ToolCallStatus.Executing,
|
||||||
|
terminalWidth: 80,
|
||||||
|
confirmationDetails: undefined,
|
||||||
|
emphasis: 'medium' as const,
|
||||||
|
isFirst: true,
|
||||||
|
borderColor: 'green',
|
||||||
|
borderDimColor: false,
|
||||||
|
config: mockConfig,
|
||||||
|
ptyId: 1,
|
||||||
|
activeShellPtyId: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
const testCases = [
|
||||||
|
{ Component: ToolMessage, componentName: 'ToolMessage' },
|
||||||
|
{ Component: ShellToolMessage, componentName: 'ShellToolMessage' },
|
||||||
|
];
|
||||||
|
|
||||||
|
describe.each(testCases)('$componentName', ({ Component }) => {
|
||||||
|
it('shows focus hint after delay even with NO output', async () => {
|
||||||
|
const { lastFrame } = renderWithProviders(
|
||||||
|
<Component {...baseProps} resultDisplay={undefined} />,
|
||||||
|
{ uiState: { streamingState: StreamingState.Idle } },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initially, no focus hint
|
||||||
|
expect(lastFrame()).toMatchSnapshot('initial-no-output');
|
||||||
|
|
||||||
|
// Advance timers by the delay
|
||||||
|
act(() => {
|
||||||
|
vi.advanceTimersByTime(SHELL_FOCUS_HINT_DELAY_MS + 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Now it SHOULD contain the focus hint
|
||||||
|
expect(lastFrame()).toMatchSnapshot('after-delay-no-output');
|
||||||
|
expect(lastFrame()).toContain('(tab to focus)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows focus hint after delay with output', async () => {
|
||||||
|
const { lastFrame } = renderWithProviders(
|
||||||
|
<Component {...baseProps} resultDisplay="Some output" />,
|
||||||
|
{ uiState: { streamingState: StreamingState.Idle } },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initially, no focus hint
|
||||||
|
expect(lastFrame()).toMatchSnapshot('initial-with-output');
|
||||||
|
|
||||||
|
// Advance timers
|
||||||
|
act(() => {
|
||||||
|
vi.advanceTimersByTime(SHELL_FOCUS_HINT_DELAY_MS + 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(lastFrame()).toMatchSnapshot('after-delay-with-output');
|
||||||
|
expect(lastFrame()).toContain('(tab to focus)');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles long descriptions by shrinking them to show the focus hint', async () => {
|
||||||
|
const longDescription = 'A'.repeat(100);
|
||||||
|
const { lastFrame } = renderWithProviders(
|
||||||
|
<ToolMessage
|
||||||
|
{...baseProps}
|
||||||
|
description={longDescription}
|
||||||
|
resultDisplay="output"
|
||||||
|
/>,
|
||||||
|
{ uiState: { streamingState: StreamingState.Idle } },
|
||||||
|
);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
vi.advanceTimersByTime(SHELL_FOCUS_HINT_DELAY_MS + 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
// The focus hint should be visible
|
||||||
|
expect(lastFrame()).toMatchSnapshot('long-description');
|
||||||
|
expect(lastFrame()).toContain('(tab to focus)');
|
||||||
|
// The name should still be visible
|
||||||
|
expect(lastFrame()).toContain(SHELL_COMMAND_NAME);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Box, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
import { ToolCallStatus } from '../../types.js';
|
import { ToolCallStatus } from '../../types.js';
|
||||||
import { GeminiRespondingSpinner } from '../GeminiRespondingSpinner.js';
|
import { GeminiRespondingSpinner } from '../GeminiRespondingSpinner.js';
|
||||||
@@ -12,12 +12,116 @@ import {
|
|||||||
SHELL_COMMAND_NAME,
|
SHELL_COMMAND_NAME,
|
||||||
SHELL_NAME,
|
SHELL_NAME,
|
||||||
TOOL_STATUS,
|
TOOL_STATUS,
|
||||||
|
SHELL_FOCUS_HINT_DELAY_MS,
|
||||||
} from '../../constants.js';
|
} from '../../constants.js';
|
||||||
import { theme } from '../../semantic-colors.js';
|
import { theme } from '../../semantic-colors.js';
|
||||||
import { SHELL_TOOL_NAME } from '@google/gemini-cli-core';
|
import {
|
||||||
|
type Config,
|
||||||
|
SHELL_TOOL_NAME,
|
||||||
|
type ToolResultDisplay,
|
||||||
|
} from '@google/gemini-cli-core';
|
||||||
|
import { useInactivityTimer } from '../../hooks/useInactivityTimer.js';
|
||||||
|
|
||||||
export const STATUS_INDICATOR_WIDTH = 3;
|
export const STATUS_INDICATOR_WIDTH = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the tool name corresponds to a shell tool.
|
||||||
|
*/
|
||||||
|
export function isShellTool(name: string): boolean {
|
||||||
|
return (
|
||||||
|
name === SHELL_COMMAND_NAME ||
|
||||||
|
name === SHELL_NAME ||
|
||||||
|
name === SHELL_TOOL_NAME
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the shell tool call is currently focusable.
|
||||||
|
*/
|
||||||
|
export function isThisShellFocusable(
|
||||||
|
name: string,
|
||||||
|
status: ToolCallStatus,
|
||||||
|
config?: Config,
|
||||||
|
): boolean {
|
||||||
|
return !!(
|
||||||
|
isShellTool(name) &&
|
||||||
|
status === ToolCallStatus.Executing &&
|
||||||
|
config?.getEnableInteractiveShell()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if this specific shell tool call is currently focused.
|
||||||
|
*/
|
||||||
|
export function isThisShellFocused(
|
||||||
|
name: string,
|
||||||
|
status: ToolCallStatus,
|
||||||
|
ptyId?: number,
|
||||||
|
activeShellPtyId?: number | null,
|
||||||
|
embeddedShellFocused?: boolean,
|
||||||
|
): boolean {
|
||||||
|
return !!(
|
||||||
|
isShellTool(name) &&
|
||||||
|
status === ToolCallStatus.Executing &&
|
||||||
|
ptyId === activeShellPtyId &&
|
||||||
|
embeddedShellFocused
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to manage focus hint state.
|
||||||
|
*/
|
||||||
|
export function useFocusHint(
|
||||||
|
isThisShellFocusable: boolean,
|
||||||
|
isThisShellFocused: boolean,
|
||||||
|
resultDisplay: ToolResultDisplay | undefined,
|
||||||
|
) {
|
||||||
|
const [lastUpdateTime, setLastUpdateTime] = useState<Date | null>(null);
|
||||||
|
const [userHasFocused, setUserHasFocused] = useState(false);
|
||||||
|
const showFocusHint = useInactivityTimer(
|
||||||
|
isThisShellFocusable,
|
||||||
|
lastUpdateTime ? lastUpdateTime.getTime() : 0,
|
||||||
|
SHELL_FOCUS_HINT_DELAY_MS,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (resultDisplay) {
|
||||||
|
setLastUpdateTime(new Date());
|
||||||
|
}
|
||||||
|
}, [resultDisplay]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isThisShellFocused) {
|
||||||
|
setUserHasFocused(true);
|
||||||
|
}
|
||||||
|
}, [isThisShellFocused]);
|
||||||
|
|
||||||
|
const shouldShowFocusHint =
|
||||||
|
isThisShellFocusable && (showFocusHint || userHasFocused);
|
||||||
|
|
||||||
|
return { shouldShowFocusHint };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to render the focus hint.
|
||||||
|
*/
|
||||||
|
export const FocusHint: React.FC<{
|
||||||
|
shouldShowFocusHint: boolean;
|
||||||
|
isThisShellFocused: boolean;
|
||||||
|
}> = ({ shouldShowFocusHint, isThisShellFocused }) => {
|
||||||
|
if (!shouldShowFocusHint) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box marginLeft={1} flexShrink={0}>
|
||||||
|
<Text color={theme.text.accent}>
|
||||||
|
{isThisShellFocused ? '(Focused)' : '(tab to focus)'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export type TextEmphasis = 'high' | 'medium' | 'low';
|
export type TextEmphasis = 'high' | 'medium' | 'low';
|
||||||
|
|
||||||
type ToolStatusIndicatorProps = {
|
type ToolStatusIndicatorProps = {
|
||||||
@@ -29,10 +133,7 @@ export const ToolStatusIndicator: React.FC<ToolStatusIndicatorProps> = ({
|
|||||||
status,
|
status,
|
||||||
name,
|
name,
|
||||||
}) => {
|
}) => {
|
||||||
const isShell =
|
const isShell = isShellTool(name);
|
||||||
name === SHELL_COMMAND_NAME ||
|
|
||||||
name === SHELL_NAME ||
|
|
||||||
name === SHELL_TOOL_NAME;
|
|
||||||
const statusColor = isShell ? theme.ui.symbol : theme.status.warning;
|
const statusColor = isShell ? theme.ui.symbol : theme.status.warning;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`Focus Hint > 'ShellToolMessage' > shows focus hint after delay even with NO output > after-delay-no-output 1`] = `
|
||||||
|
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
│ Shell Command A tool for testing (tab to focus) │
|
||||||
|
│ │"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Focus Hint > 'ShellToolMessage' > shows focus hint after delay even with NO output > initial-no-output 1`] = `
|
||||||
|
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
│ Shell Command A tool for testing │
|
||||||
|
│ │"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Focus Hint > 'ShellToolMessage' > shows focus hint after delay with output > after-delay-with-output 1`] = `
|
||||||
|
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
│ Shell Command A tool for testing (tab to focus) │
|
||||||
|
│ │"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Focus Hint > 'ShellToolMessage' > shows focus hint after delay with output > initial-with-output 1`] = `
|
||||||
|
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
│ Shell Command A tool for testing │
|
||||||
|
│ │"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Focus Hint > 'ToolMessage' > shows focus hint after delay even with NO output > after-delay-no-output 1`] = `
|
||||||
|
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
│ Shell Command A tool for testing (tab to focus) │
|
||||||
|
│ │"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Focus Hint > 'ToolMessage' > shows focus hint after delay even with NO output > initial-no-output 1`] = `
|
||||||
|
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
│ Shell Command A tool for testing │
|
||||||
|
│ │"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Focus Hint > 'ToolMessage' > shows focus hint after delay with output > after-delay-with-output 1`] = `
|
||||||
|
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
│ Shell Command A tool for testing (tab to focus) │
|
||||||
|
│ │"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Focus Hint > 'ToolMessage' > shows focus hint after delay with output > initial-with-output 1`] = `
|
||||||
|
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
│ Shell Command A tool for testing │
|
||||||
|
│ │"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Focus Hint > handles long descriptions by shrinking them to show the focus hint > long-description 1`] = `
|
||||||
|
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
│ Shell Command AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA… (tab to focus) │
|
||||||
|
│ │"
|
||||||
|
`;
|
||||||
Reference in New Issue
Block a user