@@ -1940,6 +1940,160 @@ describe('AppContainer State Management', () => {
19401940 unmount ( ) ;
19411941 } ) ;
19421942 } ) ;
1943+
1944+ describe ( 'Focus Handling (Tab / Shift+Tab)' , ( ) => {
1945+ beforeEach ( ( ) => {
1946+ // Mock activePtyId to enable focus
1947+ mockedUseGeminiStream . mockReturnValue ( {
1948+ ...DEFAULT_GEMINI_STREAM_MOCK ,
1949+ activePtyId : 1 ,
1950+ } ) ;
1951+ } ) ;
1952+
1953+ it ( 'should focus shell input on Tab' , async ( ) => {
1954+ await setupKeypressTest ( ) ;
1955+
1956+ pressKey ( { name : 'tab' , shift : false } ) ;
1957+
1958+ expect ( capturedUIState . embeddedShellFocused ) . toBe ( true ) ;
1959+ unmount ( ) ;
1960+ } ) ;
1961+
1962+ it ( 'should unfocus shell input on Shift+Tab' , async ( ) => {
1963+ await setupKeypressTest ( ) ;
1964+
1965+ // Focus first
1966+ pressKey ( { name : 'tab' , shift : false } ) ;
1967+ expect ( capturedUIState . embeddedShellFocused ) . toBe ( true ) ;
1968+
1969+ // Unfocus via Shift+Tab
1970+ pressKey ( { name : 'tab' , shift : true } ) ;
1971+ expect ( capturedUIState . embeddedShellFocused ) . toBe ( false ) ;
1972+ unmount ( ) ;
1973+ } ) ;
1974+
1975+ it ( 'should auto-unfocus when activePtyId becomes null' , async ( ) => {
1976+ // Start with active pty and focused
1977+ mockedUseGeminiStream . mockReturnValue ( {
1978+ ...DEFAULT_GEMINI_STREAM_MOCK ,
1979+ activePtyId : 1 ,
1980+ } ) ;
1981+
1982+ const renderResult = render ( getAppContainer ( ) ) ;
1983+ await act ( async ( ) => {
1984+ vi . advanceTimersByTime ( 0 ) ;
1985+ } ) ;
1986+
1987+ // Focus it
1988+ act ( ( ) => {
1989+ handleGlobalKeypress ( {
1990+ name : 'tab' ,
1991+ shift : false ,
1992+ alt : false ,
1993+ ctrl : false ,
1994+ cmd : false ,
1995+ } as Key ) ;
1996+ } ) ;
1997+ expect ( capturedUIState . embeddedShellFocused ) . toBe ( true ) ;
1998+
1999+ // Now mock activePtyId becoming null
2000+ mockedUseGeminiStream . mockReturnValue ( {
2001+ ...DEFAULT_GEMINI_STREAM_MOCK ,
2002+ activePtyId : null ,
2003+ } ) ;
2004+
2005+ // Rerender to trigger useEffect
2006+ await act ( async ( ) => {
2007+ renderResult . rerender ( getAppContainer ( ) ) ;
2008+ } ) ;
2009+
2010+ expect ( capturedUIState . embeddedShellFocused ) . toBe ( false ) ;
2011+ renderResult . unmount ( ) ;
2012+ } ) ;
2013+
2014+ it ( 'should focus background shell on Tab when already visible (not toggle it off)' , async ( ) => {
2015+ const mockToggleBackgroundShell = vi . fn ( ) ;
2016+ mockedUseGeminiStream . mockReturnValue ( {
2017+ ...DEFAULT_GEMINI_STREAM_MOCK ,
2018+ activePtyId : null ,
2019+ isBackgroundShellVisible : true ,
2020+ backgroundShells : new Map ( [ [ 123 , { pid : 123 , status : 'running' } ] ] ) ,
2021+ toggleBackgroundShell : mockToggleBackgroundShell ,
2022+ } ) ;
2023+
2024+ await setupKeypressTest ( ) ;
2025+
2026+ // Initially not focused
2027+ expect ( capturedUIState . embeddedShellFocused ) . toBe ( false ) ;
2028+
2029+ // Press Tab
2030+ pressKey ( { name : 'tab' , shift : false } ) ;
2031+
2032+ // Should be focused
2033+ expect ( capturedUIState . embeddedShellFocused ) . toBe ( true ) ;
2034+ // Should NOT have toggled (closed) the shell
2035+ expect ( mockToggleBackgroundShell ) . not . toHaveBeenCalled ( ) ;
2036+
2037+ unmount ( ) ;
2038+ } ) ;
2039+ } ) ;
2040+
2041+ describe ( 'Background Shell Toggling (CTRL+B)' , ( ) => {
2042+ it ( 'should toggle background shell on Ctrl+B even if visible but not focused' , async ( ) => {
2043+ const mockToggleBackgroundShell = vi . fn ( ) ;
2044+ mockedUseGeminiStream . mockReturnValue ( {
2045+ ...DEFAULT_GEMINI_STREAM_MOCK ,
2046+ activePtyId : null ,
2047+ isBackgroundShellVisible : true ,
2048+ backgroundShells : new Map ( [ [ 123 , { pid : 123 , status : 'running' } ] ] ) ,
2049+ toggleBackgroundShell : mockToggleBackgroundShell ,
2050+ } ) ;
2051+
2052+ await setupKeypressTest ( ) ;
2053+
2054+ // Initially not focused, but visible
2055+ expect ( capturedUIState . embeddedShellFocused ) . toBe ( false ) ;
2056+
2057+ // Press Ctrl+B
2058+ pressKey ( { name : 'b' , ctrl : true } ) ;
2059+
2060+ // Should have toggled (closed) the shell
2061+ expect ( mockToggleBackgroundShell ) . toHaveBeenCalled ( ) ;
2062+ // Should be unfocused
2063+ expect ( capturedUIState . embeddedShellFocused ) . toBe ( false ) ;
2064+
2065+ unmount ( ) ;
2066+ } ) ;
2067+
2068+ it ( 'should show and focus background shell on Ctrl+B if hidden' , async ( ) => {
2069+ const mockToggleBackgroundShell = vi . fn ( ) ;
2070+ const geminiStreamMock = {
2071+ ...DEFAULT_GEMINI_STREAM_MOCK ,
2072+ activePtyId : null ,
2073+ isBackgroundShellVisible : false ,
2074+ backgroundShells : new Map ( [ [ 123 , { pid : 123 , status : 'running' } ] ] ) ,
2075+ toggleBackgroundShell : mockToggleBackgroundShell ,
2076+ } ;
2077+ mockedUseGeminiStream . mockReturnValue ( geminiStreamMock ) ;
2078+
2079+ await setupKeypressTest ( ) ;
2080+
2081+ // Update the mock state when toggled to simulate real behavior
2082+ mockToggleBackgroundShell . mockImplementation ( ( ) => {
2083+ geminiStreamMock . isBackgroundShellVisible = true ;
2084+ } ) ;
2085+
2086+ // Press Ctrl+B
2087+ pressKey ( { name : 'b' , ctrl : true } ) ;
2088+
2089+ // Should have toggled (shown) the shell
2090+ expect ( mockToggleBackgroundShell ) . toHaveBeenCalled ( ) ;
2091+ // Should be focused
2092+ expect ( capturedUIState . embeddedShellFocused ) . toBe ( true ) ;
2093+
2094+ unmount ( ) ;
2095+ } ) ;
2096+ } ) ;
19432097 } ) ;
19442098
19452099 describe ( 'Copy Mode (CTRL+S)' , ( ) => {
0 commit comments