| |
| |
| |
| |
| |
|
|
| import React from 'react'; |
| import { Box, type DOMElement } from 'ink'; |
| import { ShellInputPrompt } from '../ShellInputPrompt.js'; |
| import { StickyHeader } from '../StickyHeader.js'; |
| import { useUIActions } from '../../contexts/UIActionsContext.js'; |
| import { useMouseClick } from '../../hooks/useMouseClick.js'; |
| import { ToolResultDisplay } from './ToolResultDisplay.js'; |
| import { |
| ToolStatusIndicator, |
| ToolInfo, |
| TrailingIndicator, |
| isThisShellFocusable as checkIsShellFocusable, |
| isThisShellFocused as checkIsShellFocused, |
| useFocusHint, |
| FocusHint, |
| } from './ToolShared.js'; |
| import type { ToolMessageProps } from './ToolMessage.js'; |
| import { ACTIVE_SHELL_MAX_LINES } from '../../constants.js'; |
| import { useAlternateBuffer } from '../../hooks/useAlternateBuffer.js'; |
| import { useUIState } from '../../contexts/UIStateContext.js'; |
| import { useToolActions } from '../../contexts/ToolActionsContext.js'; |
| import { |
| type Config, |
| ShellExecutionService, |
| CoreToolCallStatus, |
| } from '@google/gemini-cli-core'; |
| import { |
| calculateShellMaxLines, |
| calculateToolContentMaxLines, |
| SHELL_CONTENT_OVERHEAD, |
| } from '../../utils/toolLayoutUtils.js'; |
|
|
| export interface ShellToolMessageProps extends ToolMessageProps { |
| config?: Config; |
| isExpandable?: boolean; |
| } |
|
|
| export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({ |
| callId, |
| name, |
| description, |
| resultDisplay, |
| status, |
| availableTerminalHeight, |
| terminalWidth, |
| emphasis = 'medium', |
| renderOutputAsMarkdown = true, |
| ptyId, |
| config, |
| isFirst, |
| borderColor, |
| borderDimColor, |
| isExpandable, |
| originalRequestName, |
| }) => { |
| const { isExpanded: isExpandedInContext } = useToolActions(); |
|
|
| const isExpanded = |
| (isExpandedInContext ? isExpandedInContext(callId) : false) || |
| availableTerminalHeight === undefined; |
|
|
| const { |
| activePtyId: activeShellPtyId, |
| embeddedShellFocused, |
| constrainHeight, |
| } = useUIState(); |
| const isAlternateBuffer = useAlternateBuffer(); |
|
|
| const isThisShellFocused = checkIsShellFocused( |
| name, |
| status, |
| ptyId, |
| activeShellPtyId, |
| embeddedShellFocused, |
| ); |
|
|
| const maxLines = calculateShellMaxLines({ |
| status, |
| isAlternateBuffer, |
| isThisShellFocused, |
| availableTerminalHeight, |
| constrainHeight, |
| isExpandable, |
| }); |
|
|
| const availableHeight = calculateToolContentMaxLines({ |
| availableTerminalHeight, |
| isAlternateBuffer, |
| maxLinesLimit: maxLines, |
| }); |
|
|
| React.useEffect(() => { |
| const isExecuting = status === CoreToolCallStatus.Executing; |
| if (isExecuting && ptyId) { |
| try { |
| const childWidth = terminalWidth - 4; |
| const finalHeight = |
| availableHeight ?? ACTIVE_SHELL_MAX_LINES - SHELL_CONTENT_OVERHEAD; |
|
|
| ShellExecutionService.resizePty( |
| ptyId, |
| Math.max(1, childWidth), |
| Math.max(1, finalHeight), |
| ); |
| } catch (e) { |
| if ( |
| !( |
| e instanceof Error && |
| e.message.includes('Cannot resize a pty that has already exited') |
| ) |
| ) { |
| throw e; |
| } |
| } |
| } |
| }, [ptyId, status, terminalWidth, availableHeight]); |
|
|
| const { setEmbeddedShellFocused } = useUIActions(); |
| const wasFocusedRef = React.useRef(false); |
|
|
| React.useEffect(() => { |
| if (isThisShellFocused) { |
| wasFocusedRef.current = true; |
| } else if (wasFocusedRef.current) { |
| if (embeddedShellFocused) { |
| setEmbeddedShellFocused(false); |
| } |
| wasFocusedRef.current = false; |
| } |
| }, [isThisShellFocused, embeddedShellFocused, setEmbeddedShellFocused]); |
|
|
| const headerRef = React.useRef<DOMElement>(null); |
| const contentRef = React.useRef<DOMElement>(null); |
|
|
| |
| const isThisShellFocusable = checkIsShellFocusable(name, status, config); |
|
|
| const handleFocus = () => { |
| if (isThisShellFocusable) { |
| setEmbeddedShellFocused(true); |
| } |
| }; |
|
|
| useMouseClick(headerRef, handleFocus, { isActive: !!isThisShellFocusable }); |
| useMouseClick(contentRef, handleFocus, { isActive: !!isThisShellFocusable }); |
|
|
| const { shouldShowFocusHint } = useFocusHint( |
| isThisShellFocusable, |
| isThisShellFocused, |
| resultDisplay, |
| ); |
|
|
| return ( |
| <> |
| <StickyHeader |
| width={terminalWidth} |
| isFirst={isFirst} |
| borderColor={borderColor} |
| borderDimColor={borderDimColor} |
| containerRef={headerRef} |
| > |
| <ToolStatusIndicator |
| status={status} |
| name={name} |
| isFocused={isThisShellFocused} |
| /> |
| |
| <ToolInfo |
| name={name} |
| status={status} |
| description={description} |
| emphasis={emphasis} |
| originalRequestName={originalRequestName} |
| isExpanded={isExpanded} |
| /> |
| |
| <FocusHint |
| shouldShowFocusHint={shouldShowFocusHint} |
| isThisShellFocused={isThisShellFocused} |
| /> |
| |
| {emphasis === 'high' && <TrailingIndicator />} |
| </StickyHeader> |
| |
| <Box |
| ref={contentRef} |
| width={terminalWidth} |
| borderStyle="round" |
| borderColor={borderColor} |
| borderDimColor={borderDimColor} |
| borderTop={false} |
| borderBottom={false} |
| borderLeft={true} |
| borderRight={true} |
| paddingX={1} |
| flexDirection="column" |
| > |
| <ToolResultDisplay |
| resultDisplay={resultDisplay} |
| availableTerminalHeight={availableTerminalHeight} |
| terminalWidth={terminalWidth} |
| renderOutputAsMarkdown={renderOutputAsMarkdown} |
| hasFocus={isThisShellFocused} |
| maxLines={maxLines} |
| /> |
| {isThisShellFocused && config && ( |
| <ShellInputPrompt |
| activeShellPtyId={activeShellPtyId ?? null} |
| focus={embeddedShellFocused} |
| scrollPageSize={availableTerminalHeight ?? ACTIVE_SHELL_MAX_LINES} |
| /> |
| )} |
| </Box> |
| </> |
| ); |
| }; |
|
|