66
77import { renderWithProviders } from '../../test-utils/render.js' ;
88import { waitFor } from '../../test-utils/async.js' ;
9- import { MainContent } from './MainContent.js' ;
9+ import { MainContent , getToolGroupBorderAppearance } from './MainContent.js' ;
1010import { describe , it , expect , vi , beforeEach } from 'vitest' ;
1111import { Box , Text } from 'ink' ;
1212import { act , useState , type JSX } from 'react' ;
@@ -18,6 +18,7 @@ import {
1818 type UIState ,
1919} from '../contexts/UIStateContext.js' ;
2020import { CoreToolCallStatus } from '@google/gemini-cli-core' ;
21+ import { type IndividualToolCallDisplay } from '../types.js' ;
2122
2223// Mock dependencies
2324vi . mock ( '../contexts/SettingsContext.js' , async ( ) => {
@@ -76,6 +77,208 @@ vi.mock('./shared/ScrollableList.js', () => ({
7677 SCROLL_TO_ITEM_END : 0 ,
7778} ) ) ;
7879
80+ import { theme } from '../semantic-colors.js' ;
81+ import { type BackgroundShell } from '../hooks/shellReducer.js' ;
82+
83+ describe ( 'getToolGroupBorderAppearance' , ( ) => {
84+ const mockBackgroundShells = new Map < number , BackgroundShell > ( ) ;
85+ const activeShellPtyId = 123 ;
86+
87+ it ( 'returns default empty values for non-tool_group items' , ( ) => {
88+ const item = { type : 'user' as const , text : 'Hello' , id : 1 } ;
89+ const result = getToolGroupBorderAppearance (
90+ item ,
91+ null ,
92+ false ,
93+ [ ] ,
94+ mockBackgroundShells ,
95+ ) ;
96+ expect ( result ) . toEqual ( { borderColor : '' , borderDimColor : false } ) ;
97+ } ) ;
98+
99+ it ( 'inspects only the last pending tool_group item if current has no tools' , ( ) => {
100+ const item = { type : 'tool_group' as const , tools : [ ] , id : 1 } ;
101+ const pendingItems = [
102+ {
103+ type : 'tool_group' as const ,
104+ tools : [
105+ {
106+ callId : '1' ,
107+ name : 'some_tool' ,
108+ description : '' ,
109+ status : CoreToolCallStatus . Executing ,
110+ ptyId : undefined ,
111+ resultDisplay : undefined ,
112+ confirmationDetails : undefined ,
113+ } as IndividualToolCallDisplay ,
114+ ] ,
115+ } ,
116+ {
117+ type : 'tool_group' as const ,
118+ tools : [
119+ {
120+ callId : '2' ,
121+ name : 'other_tool' ,
122+ description : '' ,
123+ status : CoreToolCallStatus . Success ,
124+ ptyId : undefined ,
125+ resultDisplay : undefined ,
126+ confirmationDetails : undefined ,
127+ } as IndividualToolCallDisplay ,
128+ ] ,
129+ } ,
130+ ] ;
131+
132+ // Only the last item (Success) should be inspected, so hasPending = false.
133+ // The previous item was Executing (pending) but it shouldn't be counted.
134+ const result = getToolGroupBorderAppearance (
135+ item ,
136+ null ,
137+ false ,
138+ pendingItems ,
139+ mockBackgroundShells ,
140+ ) ;
141+ expect ( result ) . toEqual ( {
142+ borderColor : theme . border . default ,
143+ borderDimColor : false ,
144+ } ) ;
145+ } ) ;
146+
147+ it ( 'returns default border for completed normal tools' , ( ) => {
148+ const item = {
149+ type : 'tool_group' as const ,
150+ tools : [
151+ {
152+ callId : '1' ,
153+ name : 'some_tool' ,
154+ description : '' ,
155+ status : CoreToolCallStatus . Success ,
156+ ptyId : undefined ,
157+ resultDisplay : undefined ,
158+ confirmationDetails : undefined ,
159+ } as IndividualToolCallDisplay ,
160+ ] ,
161+ id : 1 ,
162+ } ;
163+ const result = getToolGroupBorderAppearance (
164+ item ,
165+ null ,
166+ false ,
167+ [ ] ,
168+ mockBackgroundShells ,
169+ ) ;
170+ expect ( result ) . toEqual ( {
171+ borderColor : theme . border . default ,
172+ borderDimColor : false ,
173+ } ) ;
174+ } ) ;
175+
176+ it ( 'returns warning border for pending normal tools' , ( ) => {
177+ const item = {
178+ type : 'tool_group' as const ,
179+ tools : [
180+ {
181+ callId : '1' ,
182+ name : 'some_tool' ,
183+ description : '' ,
184+ status : CoreToolCallStatus . Executing ,
185+ ptyId : undefined ,
186+ resultDisplay : undefined ,
187+ confirmationDetails : undefined ,
188+ } as IndividualToolCallDisplay ,
189+ ] ,
190+ id : 1 ,
191+ } ;
192+ const result = getToolGroupBorderAppearance (
193+ item ,
194+ null ,
195+ false ,
196+ [ ] ,
197+ mockBackgroundShells ,
198+ ) ;
199+ expect ( result ) . toEqual ( {
200+ borderColor : theme . status . warning ,
201+ borderDimColor : true ,
202+ } ) ;
203+ } ) ;
204+
205+ it ( 'returns symbol border for executing shell commands' , ( ) => {
206+ const item = {
207+ type : 'tool_group' as const ,
208+ tools : [
209+ {
210+ callId : '1' ,
211+ name : SHELL_COMMAND_NAME ,
212+ description : '' ,
213+ status : CoreToolCallStatus . Executing ,
214+ ptyId : activeShellPtyId ,
215+ resultDisplay : undefined ,
216+ confirmationDetails : undefined ,
217+ } as IndividualToolCallDisplay ,
218+ ] ,
219+ id : 1 ,
220+ } ;
221+ // While executing shell commands, it's dim false, border symbol
222+ const result = getToolGroupBorderAppearance (
223+ item ,
224+ activeShellPtyId ,
225+ true ,
226+ [ ] ,
227+ mockBackgroundShells ,
228+ ) ;
229+ expect ( result ) . toEqual ( {
230+ borderColor : theme . ui . symbol ,
231+ borderDimColor : false ,
232+ } ) ;
233+ } ) ;
234+
235+ it ( 'returns symbol border and dims color for background executing shell command when another shell is active' , ( ) => {
236+ const item = {
237+ type : 'tool_group' as const ,
238+ tools : [
239+ {
240+ callId : '1' ,
241+ name : SHELL_COMMAND_NAME ,
242+ description : '' ,
243+ status : CoreToolCallStatus . Executing ,
244+ ptyId : 456 , // Different ptyId, not active
245+ resultDisplay : undefined ,
246+ confirmationDetails : undefined ,
247+ } as IndividualToolCallDisplay ,
248+ ] ,
249+ id : 1 ,
250+ } ;
251+ const result = getToolGroupBorderAppearance (
252+ item ,
253+ activeShellPtyId ,
254+ false ,
255+ [ ] ,
256+ mockBackgroundShells ,
257+ ) ;
258+ expect ( result ) . toEqual ( {
259+ borderColor : theme . ui . symbol ,
260+ borderDimColor : true ,
261+ } ) ;
262+ } ) ;
263+
264+ it ( 'handles empty tools with active shell turn (isCurrentlyInShellTurn)' , ( ) => {
265+ const item = { type : 'tool_group' as const , tools : [ ] , id : 1 } ;
266+
267+ // active shell turn
268+ const result = getToolGroupBorderAppearance (
269+ item ,
270+ activeShellPtyId ,
271+ true ,
272+ [ ] ,
273+ mockBackgroundShells ,
274+ ) ;
275+ // Since there are no tools to inspect, it falls back to empty pending, but isCurrentlyInShellTurn=true
276+ // so it counts as pending shell.
277+ expect ( result . borderColor ) . toEqual ( theme . ui . symbol ) ;
278+ // It shouldn't be dim because there are no tools to say it isEmbeddedShellFocused = false
279+ } ) ;
280+ } ) ;
281+
79282describe ( 'MainContent' , ( ) => {
80283 const defaultMockUiState = {
81284 history : [
@@ -258,7 +461,7 @@ describe('MainContent', () => {
258461 history : [ ] ,
259462 pendingHistoryItems : [
260463 {
261- type : 'tool_group' as const ,
464+ type : 'tool_group' ,
262465 id : 1 ,
263466 tools : [
264467 {
0 commit comments