@@ -188,6 +188,160 @@ describe("palette filters as you type", () => {
188188 } )
189189} )
190190
191+ const DESCRIBED_CATALOG : readonly PaletteCommand [ ] = [
192+ {
193+ id : "help" ,
194+ label : "/help" ,
195+ description : "Show the keyboard shortcut and command overlay" ,
196+ } ,
197+ {
198+ id : "model" ,
199+ label : "/model" ,
200+ description : "Switch the active model or provider" ,
201+ } ,
202+ {
203+ id : "mcp" ,
204+ label : "/mcp" ,
205+ description : "Manage MCP servers" ,
206+ } ,
207+ ]
208+
209+ const HELP_DESC = "Show the keyboard shortcut and command overlay"
210+ const MODEL_DESC = "Switch the active model or provider"
211+
212+ function stripFrameLines ( frame : string ) : string [ ] {
213+ return frame
214+ . split ( "\n" )
215+ . map ( ( line ) => line . replace ( / ^ \s * │ / , "" ) . replace ( / │ \s * $ / , "" ) . trimEnd ( ) )
216+ }
217+
218+ /** Interior zone rows under the list rule, before the overlay's bottom border. */
219+ function zoneAfterList (
220+ lines : readonly string [ ] ,
221+ labels : readonly string [ ] ,
222+ ) : readonly string [ ] | undefined {
223+ let last = - 1
224+ for ( const [ i , line ] of lines . entries ( ) ) {
225+ if ( labels . some ( ( label ) => line . includes ( label ) ) ) last = i
226+ }
227+ if ( last < 0 ) return undefined
228+ const below = lines . slice ( last + 1 )
229+ const ruleAt = below . findIndex (
230+ ( r ) => r . includes ( "─" ) && ! / [ ┌ ┐ └ ┘ ╭ ╮ ╰ ╯ ] / . test ( r ) ,
231+ )
232+ if ( ruleAt < 0 ) return undefined
233+ const afterRule = below . slice ( ruleAt + 1 )
234+ const boxBottom = afterRule . findIndex ( ( r ) => / [ └ ┘ ] / . test ( r ) )
235+ return boxBottom >= 0 ? afterRule . slice ( 0 , boxBottom ) : afterRule
236+ }
237+
238+ function expectNameOnlyRows (
239+ lines : readonly string [ ] ,
240+ labels : readonly string [ ] ,
241+ ) : void {
242+ for ( const label of labels ) {
243+ const row = lines . find ( ( r ) => r . includes ( label ) )
244+ expect ( row ) . toBeDefined ( )
245+ expect ( row ! . trim ( ) ) . toBe ( label )
246+ }
247+ }
248+
249+ function expectDescriptionUnderListRule (
250+ lines : readonly string [ ] ,
251+ description : string ,
252+ labels : readonly string [ ] ,
253+ ) : void {
254+ for ( const label of labels ) {
255+ const row = lines . find ( ( r ) => r . includes ( label ) )
256+ expect ( row ) . toBeDefined ( )
257+ expect ( row ) . not . toContain ( description )
258+ }
259+ const zone = zoneAfterList ( lines , labels )
260+ expect ( zone ) . toBeDefined ( )
261+ expect ( zone ! . some ( ( r ) => r . includes ( description ) ) ) . toBe ( true )
262+ }
263+
264+ describe ( "command list description zone" , ( ) => {
265+ test ( "paints the focused command's registry description, not on the row" , async ( ) => {
266+ await withTestRenderer (
267+ async ( h ) => {
268+ const shell = createAppShell ( h . renderer , {
269+ terminal : { columns : 100 , rows : 32 } ,
270+ wireKeys : false ,
271+ run : "idle" ,
272+ } )
273+ openPalette ( shell , { catalog : DESCRIBED_CATALOG } )
274+ await h . renderOnce ( )
275+ const labels = DESCRIBED_CATALOG . map ( ( c ) => c . label )
276+ const lines = stripFrameLines ( h . captureCharFrame ( ) )
277+ expectNameOnlyRows ( lines , labels )
278+ expectDescriptionUnderListRule ( lines , HELP_DESC , labels )
279+ } ,
280+ { width : 100 , height : 32 } ,
281+ )
282+ } )
283+
284+ test ( "moving the overlay selection updates the zone to the newly focused command" , async ( ) => {
285+ await withTestRenderer (
286+ async ( h ) => {
287+ const shell = createAppShell ( h . renderer , {
288+ terminal : { columns : 100 , rows : 32 } ,
289+ wireKeys : false ,
290+ run : "idle" ,
291+ } )
292+ openPalette ( shell , { catalog : DESCRIBED_CATALOG } )
293+ await h . renderOnce ( )
294+ const labels = DESCRIBED_CATALOG . map ( ( c ) => c . label )
295+ const before = stripFrameLines ( h . captureCharFrame ( ) )
296+ expectDescriptionUnderListRule ( before , HELP_DESC , labels )
297+ expect ( before . join ( "\n" ) ) . not . toContain ( MODEL_DESC )
298+
299+ moveOverlaySelection ( shell , 1 )
300+ await h . renderOnce ( )
301+ const after = stripFrameLines ( h . captureCharFrame ( ) )
302+ expectNameOnlyRows ( after , labels )
303+ expectDescriptionUnderListRule ( after , MODEL_DESC , labels )
304+ expect ( after . join ( "\n" ) ) . not . toContain ( HELP_DESC )
305+ } ,
306+ { width : 100 , height : 32 } ,
307+ )
308+ } )
309+
310+ test ( "an undescribed row leaves the zone blank without leftover neighbor copy" , async ( ) => {
311+ const mixed : readonly PaletteCommand [ ] = [
312+ { id : "help" , label : "/help" , description : HELP_DESC } ,
313+ { id : "model" , label : "/model" } ,
314+ ]
315+ await withTestRenderer (
316+ async ( h ) => {
317+ const shell = createAppShell ( h . renderer , {
318+ terminal : { columns : 100 , rows : 32 } ,
319+ wireKeys : false ,
320+ run : "idle" ,
321+ } )
322+ openPalette ( shell , { catalog : mixed } )
323+ await h . renderOnce ( )
324+ const labels = mixed . map ( ( c ) => c . label )
325+ const described = stripFrameLines ( h . captureCharFrame ( ) )
326+ expectNameOnlyRows ( described , labels )
327+ expectDescriptionUnderListRule ( described , HELP_DESC , labels )
328+ const reserved = shell . layout . heights . overlay_host
329+
330+ moveOverlaySelection ( shell , 1 )
331+ await h . renderOnce ( )
332+ const blank = stripFrameLines ( h . captureCharFrame ( ) )
333+ expectNameOnlyRows ( blank , labels )
334+ const zone = zoneAfterList ( blank , labels )
335+ expect ( zone ) . toBeDefined ( )
336+ expect ( zone ! . every ( ( r ) => r . trim ( ) === "" ) ) . toBe ( true )
337+ expect ( blank . join ( "\n" ) ) . not . toContain ( HELP_DESC )
338+ expect ( shell . layout . heights . overlay_host ) . toBe ( reserved )
339+ } ,
340+ { width : 100 , height : 32 } ,
341+ )
342+ } )
343+ } )
344+
191345describe ( "command list width" , ( ) => {
192346 // Both boxes are children of the same padded root; a width computed a
193347 // second way for the floating list drifts from the prompt box's "100%".
0 commit comments