55 */
66
77import type React from 'react' ;
8- import { Box } from 'ink' ;
8+ import { useState , useEffect } from 'react' ;
9+ import { Box , Text } from 'ink' ;
910import type { IndividualToolCallDisplay } from '../../types.js' ;
1011import { StickyHeader } from '../StickyHeader.js' ;
1112import { ToolResultDisplay } from './ToolResultDisplay.js' ;
@@ -14,7 +15,17 @@ import {
1415 ToolInfo ,
1516 TrailingIndicator ,
1617 type TextEmphasis ,
18+ STATUS_INDICATOR_WIDTH ,
1719} from './ToolShared.js' ;
20+ import {
21+ SHELL_COMMAND_NAME ,
22+ SHELL_FOCUS_HINT_DELAY_MS ,
23+ } from '../../constants.js' ;
24+ import { theme } from '../../semantic-colors.js' ;
25+ import type { Config } from '@google/gemini-cli-core' ;
26+ import { useInactivityTimer } from '../../hooks/useInactivityTimer.js' ;
27+ import { ToolCallStatus } from '../../types.js' ;
28+ import { ShellInputPrompt } from '../ShellInputPrompt.js' ;
1829
1930export type { TextEmphasis } ;
2031
@@ -26,6 +37,10 @@ export interface ToolMessageProps extends IndividualToolCallDisplay {
2637 isFirst : boolean ;
2738 borderColor : string ;
2839 borderDimColor : boolean ;
40+ activeShellPtyId ?: number | null ;
41+ embeddedShellFocused ?: boolean ;
42+ ptyId ?: number ;
43+ config ?: Config ;
2944}
3045
3146export const ToolMessage : React . FC < ToolMessageProps > = ( {
@@ -40,41 +55,96 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
4055 isFirst,
4156 borderColor,
4257 borderDimColor,
43- } ) => (
44- < Box flexDirection = "column" width = { terminalWidth } >
45- < StickyHeader
46- width = { terminalWidth }
47- isFirst = { isFirst }
48- borderColor = { borderColor }
49- borderDimColor = { borderDimColor }
50- >
51- < ToolStatusIndicator status = { status } name = { name } />
52- < ToolInfo
53- name = { name }
54- status = { status }
55- description = { description }
56- emphasis = { emphasis }
57- />
58- { emphasis === 'high' && < TrailingIndicator /> }
59- </ StickyHeader >
60- < Box
61- width = { terminalWidth }
62- borderStyle = "round"
63- borderColor = { borderColor }
64- borderDimColor = { borderDimColor }
65- borderTop = { false }
66- borderBottom = { false }
67- borderLeft = { true }
68- borderRight = { true }
69- paddingX = { 1 }
70- flexDirection = "column"
71- >
72- < ToolResultDisplay
73- resultDisplay = { resultDisplay }
74- availableTerminalHeight = { availableTerminalHeight }
75- terminalWidth = { terminalWidth }
76- renderOutputAsMarkdown = { renderOutputAsMarkdown }
77- />
58+ activeShellPtyId,
59+ embeddedShellFocused,
60+ ptyId,
61+ config,
62+ } ) => {
63+ const isThisShellFocused =
64+ ( name === SHELL_COMMAND_NAME || name === 'Shell' ) &&
65+ status === ToolCallStatus . Executing &&
66+ ptyId === activeShellPtyId &&
67+ embeddedShellFocused ;
68+
69+ const [ lastUpdateTime , setLastUpdateTime ] = useState < Date | null > ( null ) ;
70+ const [ userHasFocused , setUserHasFocused ] = useState ( false ) ;
71+ const showFocusHint = useInactivityTimer (
72+ ! ! lastUpdateTime ,
73+ lastUpdateTime ? lastUpdateTime . getTime ( ) : 0 ,
74+ SHELL_FOCUS_HINT_DELAY_MS ,
75+ ) ;
76+
77+ useEffect ( ( ) => {
78+ if ( resultDisplay ) {
79+ setLastUpdateTime ( new Date ( ) ) ;
80+ }
81+ } , [ resultDisplay ] ) ;
82+
83+ useEffect ( ( ) => {
84+ if ( isThisShellFocused ) {
85+ setUserHasFocused ( true ) ;
86+ }
87+ } , [ isThisShellFocused ] ) ;
88+
89+ const isThisShellFocusable =
90+ ( name === SHELL_COMMAND_NAME || name === 'Shell' ) &&
91+ status === ToolCallStatus . Executing &&
92+ config ?. getEnableInteractiveShell ( ) ;
93+
94+ const shouldShowFocusHint =
95+ isThisShellFocusable && ( showFocusHint || userHasFocused ) ;
96+
97+ return (
98+ < Box flexDirection = "column" width = { terminalWidth } >
99+ < StickyHeader
100+ width = { terminalWidth }
101+ isFirst = { isFirst }
102+ borderColor = { borderColor }
103+ borderDimColor = { borderDimColor }
104+ >
105+ < ToolStatusIndicator status = { status } name = { name } />
106+ < ToolInfo
107+ name = { name }
108+ status = { status }
109+ description = { description }
110+ emphasis = { emphasis }
111+ />
112+ { shouldShowFocusHint && (
113+ < Box marginLeft = { 1 } flexShrink = { 0 } >
114+ < Text color = { theme . text . accent } >
115+ { isThisShellFocused ? '(Focused)' : '(ctrl+f to focus)' }
116+ </ Text >
117+ </ Box >
118+ ) }
119+ { emphasis === 'high' && < TrailingIndicator /> }
120+ </ StickyHeader >
121+ < Box
122+ width = { terminalWidth }
123+ borderStyle = "round"
124+ borderColor = { borderColor }
125+ borderDimColor = { borderDimColor }
126+ borderTop = { false }
127+ borderBottom = { false }
128+ borderLeft = { true }
129+ borderRight = { true }
130+ paddingX = { 1 }
131+ flexDirection = "column"
132+ >
133+ < ToolResultDisplay
134+ resultDisplay = { resultDisplay }
135+ availableTerminalHeight = { availableTerminalHeight }
136+ terminalWidth = { terminalWidth }
137+ renderOutputAsMarkdown = { renderOutputAsMarkdown }
138+ />
139+ { isThisShellFocused && config && (
140+ < Box paddingLeft = { STATUS_INDICATOR_WIDTH } marginTop = { 1 } >
141+ < ShellInputPrompt
142+ activeShellPtyId = { activeShellPtyId ?? null }
143+ focus = { embeddedShellFocused }
144+ />
145+ </ Box >
146+ ) }
147+ </ Box >
78148 </ Box >
79- </ Box >
80- ) ;
149+ ) ;
150+ } ;
0 commit comments