11import { afterEach , beforeEach , expect , it , vi } from "vitest" ;
22import { effectScope , nextTick , ref } from "vue" ;
3+ import type { TerminalCursorAnchor } from "#koko" ;
34import { useTerminalAiHudLayout } from "./useTerminalAiHudLayout" ;
5+ import { TERMINAL_AI_HINT_IDLE_MS } from "~/utils/terminalAiCommand" ;
46
57const terminal = vi . hoisted ( ( ) => ( {
6- anchor : { left : 140 , top : 100 , width : 8 , height : 18 } ,
7- element : null as HTMLElement | null
8+ anchor : { left : 140 , top : 100 , width : 8 , height : 18 , bufferType : "normal" } as TerminalCursorAnchor ,
9+ element : null as HTMLElement | null ,
10+ cursorListener : ( _anchor : TerminalCursorAnchor | null ) => { } ,
11+ inputListener : ( ) => { }
812} ) ) ;
913vi . mock ( "#koko" , ( ) => ( {
1014 getKokoTerminalCursorAnchor : ( ) => terminal . anchor ,
1115 getKokoTerminalElement : ( ) => terminal . element ,
12- subscribeKokoTerminalCursorAnchor : ( ) => ( ) => { } ,
13- subscribeKokoTerminalUserInput : ( ) => ( ) => { }
16+ subscribeKokoTerminalCursorAnchor : ( _paneId : string , listener : typeof terminal . cursorListener ) => {
17+ terminal . cursorListener = listener ;
18+ return ( ) => {
19+ terminal . cursorListener = ( ) => { } ;
20+ } ;
21+ } ,
22+ subscribeKokoTerminalUserInput : ( _paneId : string , listener : ( ) => void ) => {
23+ terminal . inputListener = listener ;
24+ return ( ) => {
25+ terminal . inputListener = ( ) => { } ;
26+ } ;
27+ }
1428} ) ) ;
1529
1630const cleanups : Array < ( ) => void > = [ ] ;
@@ -20,14 +34,15 @@ beforeEach(() => {
2034 vi . stubGlobal ( "innerWidth" , 1000 ) ;
2135 vi . stubGlobal ( "innerHeight" , 800 ) ;
2236 }
23- terminal . anchor = { left : 140 , top : 100 , width : 8 , height : 18 } ;
37+ terminal . anchor = { left : 140 , top : 100 , width : 8 , height : 18 , bufferType : "normal" } ;
2438} ) ;
2539afterEach ( ( ) => {
2640 cleanups . splice ( 0 ) . forEach ( ( cleanup ) => cleanup ( ) ) ;
41+ vi . useRealTimers ( ) ;
2742 vi . unstubAllGlobals ( ) ;
2843} ) ;
2944
30- async function setup ( ) {
45+ async function setup ( sessionInfoReady = false ) {
3146 const area = { left : 100 , top : 50 , right : 900 , bottom : 750 , width : 800 , height : 700 } ;
3247 terminal . element = { getBoundingClientRect : ( ) => area } as HTMLElement ;
3348 const measured = { height : 110 , scrollHeight : 110 } ;
@@ -42,9 +57,12 @@ async function setup() {
4257 const open = ref ( false ) ;
4358 const scope = effectScope ( ) ;
4459 const layout = scope . run ( ( ) =>
45- useTerminalAiHudLayout ( { paneId : ( ) => "pane" , open, sessionInfoReady : ( ) => false } )
60+ useTerminalAiHudLayout ( { paneId : ( ) => "pane" , open, sessionInfoReady : ( ) => sessionInfoReady } )
4661 ) ! ;
47- layout . hostRef . value = { getBoundingClientRect : ( ) => area } as HTMLElement ;
62+ layout . hostRef . value = {
63+ getBoundingClientRect : ( ) => area ,
64+ style : { setProperty : vi . fn ( ) }
65+ } as unknown as HTMLElement ;
4866 layout . panelRef . value = Object . defineProperties ( new EventTarget ( ) , {
4967 getBoundingClientRect : { value : ( ) => measured } ,
5068 scrollHeight : { get : ( ) => measured . scrollHeight }
@@ -102,6 +120,41 @@ async function setup() {
102120 return { area, measured, handle, open, layout, dispatch, dispatchResize, position, size } ;
103121}
104122
123+ it ( "hides the caret hint in alternate screens, keeps it hidden after idle, and restores it on exit" , async ( ) => {
124+ vi . useFakeTimers ( ) ;
125+ if ( ! window . setTimeout ) {
126+ window . setTimeout = globalThis . setTimeout as unknown as typeof window . setTimeout ;
127+ window . clearTimeout = globalThis . clearTimeout as typeof window . clearTimeout ;
128+ }
129+ vi . stubGlobal ( "getComputedStyle" , ( ) => ( { backgroundColor : "rgb(0, 0, 0)" } ) ) ;
130+ const { layout, open } = await setup ( true ) ;
131+ open . value = false ;
132+ layout . startCursorTracking ( ) ;
133+ await nextTick ( ) ;
134+ expect ( layout . hintVisible . value ) . toBe ( true ) ;
135+
136+ terminal . anchor = { ...terminal . anchor , bufferType : "alternate" } ;
137+ terminal . cursorListener ( terminal . anchor ) ;
138+ await nextTick ( ) ;
139+ expect ( layout . hintVisible . value ) . toBe ( false ) ;
140+
141+ terminal . inputListener ( ) ;
142+ await vi . advanceTimersByTimeAsync ( TERMINAL_AI_HINT_IDLE_MS ) ;
143+ expect ( layout . hintVisible . value ) . toBe ( false ) ;
144+
145+ terminal . anchor = { ...terminal . anchor , bufferType : "normal" } ;
146+ terminal . cursorListener ( terminal . anchor ) ;
147+ await nextTick ( ) ;
148+ expect ( layout . hintVisible . value ) . toBe ( true ) ;
149+
150+ terminal . anchor = { ...terminal . anchor , bufferType : "alternate" } ;
151+ terminal . cursorListener ( terminal . anchor ) ;
152+ await nextTick ( ) ;
153+ await layout . reveal ( terminal . element ! ) ;
154+ expect ( open . value ) . toBe ( true ) ;
155+ expect ( layout . hintVisible . value ) . toBe ( false ) ;
156+ } ) ;
157+
105158it ( "moves from the original pointer position without drift and stays moved when the cursor or content changes" , async ( ) => {
106159 const { layout, measured, dispatch, position } = await setup ( ) ;
107160 expect ( position ( ) ) . toEqual ( { left : "140px" , top : "126px" } ) ;
0 commit comments