@@ -144,18 +144,30 @@ def _shorten_path(path: str, max_len: int = 48) -> str:
144144 return "…/" + tail [- (max_len - 2 ) :]
145145
146146
147+ def _format_params (tool_input : dict [str , Any ], keys : list [str ]) -> str :
148+ """Format specified tool parameters as ``key: value`` pairs."""
149+ parts = []
150+ for key in keys :
151+ val = tool_input .get (key )
152+ if val is not None :
153+ parts .append (f"{ key } : { val } " )
154+ return " · " .join (parts ) if parts else ""
155+
156+
147157_TOOL_ARG_EXTRACTORS : dict [str , Callable [[dict [str , Any ]], str ]] = {
148158 "Read" : lambda i : _shorten_path (i .get ("file_path" , "" )),
149159 "Write" : lambda i : _shorten_path (i .get ("file_path" , "" )),
150160 "Edit" : lambda i : _shorten_path (i .get ("file_path" , "" )),
151161 "MultiEdit" : lambda i : _shorten_path (i .get ("file_path" , "" )),
152162 "Glob" : lambda i : i .get ("pattern" , "" ),
153163 "Grep" : lambda i : i .get ("pattern" , "" ),
154- "Bash" : lambda i : _truncate ( i .get ("command" , "" ), 80 ),
155- "Task" : lambda i : _truncate ( i . get ( "description" , i . get ( "prompt" , "" )), 80 ),
164+ "Bash" : lambda i : i .get ("command" , "" ),
165+ "Task" : lambda i : _format_params ( i , [ "description" , "prompt" ] ),
156166 "WebFetch" : lambda i : i .get ("url" , "" ),
157167 "WebSearch" : lambda i : i .get ("query" , "" ),
158168 "TodoWrite" : lambda i : f"{ len (i .get ('todos' , []))} todos" ,
169+ "Agent" : lambda i : _format_params (i , ["description" , "prompt" ]),
170+ "ToolSearch" : lambda i : _format_params (i , ["query" , "max_results" ]),
159171}
160172
161173
@@ -300,41 +312,31 @@ def set_peek_visible(self, visible: bool) -> None:
300312
301313 # ── Stream-json processing ───────────────────────────────────────
302314
303- def apply (self , raw : dict [str , Any ]) -> str | None :
315+ def apply (self , raw : dict [str , Any ]) -> None :
304316 """Update panel state from a parsed stream-json dict.
305317
306- Returns the scroll-line markup string (or ``None``). The line is
307- also appended to the internal scroll buffer so it renders inside
308- the Live region.
318+ Each handler appends its own scroll lines to the buffer so that
319+ multi-line blocks (thinking, long text) can produce several rows.
309320 """
310321 event_type = raw .get ("type" )
311322
312323 if event_type == "system" and raw .get ("subtype" ) == "init" :
313324 self ._model = raw .get ("model" , "" )
314- return None
315-
316- scroll_line : str | None = None
317-
318- if event_type == "assistant" :
319- scroll_line = self ._apply_assistant (raw )
325+ elif event_type == "assistant" :
326+ self ._apply_assistant (raw )
320327 elif event_type == "user" :
321- scroll_line = self ._apply_user (raw )
328+ self ._apply_user (raw )
322329 elif event_type == "rate_limit_event" :
323330 info = raw .get ("rate_limit_info" , {})
324331 status = info .get ("status" , "" )
325332 resets = info .get ("resetsAt" , "" )
326- scroll_line = (
333+ self . add_scroll_line (
327334 f"[bold { _brand .PEACH } ]⚠ rate limit:[/]"
328335 f" [dim]{ escape_markup (str (status ))} "
329336 f", resets { escape_markup (str (resets ))} [/]"
330337 )
331338
332- if scroll_line is not None :
333- self .add_scroll_line (scroll_line )
334-
335- return scroll_line
336-
337- def _apply_assistant (self , raw : dict [str , Any ]) -> str | None :
339+ def _apply_assistant (self , raw : dict [str , Any ]) -> None :
338340 msg = raw .get ("message" , {})
339341
340342 # Update token counts from usage
@@ -348,21 +350,30 @@ def _apply_assistant(self, raw: dict[str, Any]) -> str | None:
348350
349351 content = msg .get ("content" , [])
350352 if not isinstance (content , list ):
351- return None
353+ return
352354
353- scroll_line : str | None = None
354355 for block in content :
355356 if not isinstance (block , dict ):
356357 continue
357358 block_type = block .get ("type" )
358359
359- if block_type == "text" :
360+ if block_type == "thinking" :
361+ text = block .get ("thinking" , "" )
362+ if text :
363+ for tline in text .split ("\n " ):
364+ self .add_scroll_line (
365+ f"[dim italic]{ escape_markup (tline )} [/]"
366+ )
367+
368+ elif block_type == "text" :
360369 text = block .get ("text" , "" )
361- preview = _truncate (text .replace ("\n " , " " ), 100 )
362- if preview :
363- scroll_line = (
364- f"[italic { _brand .LAVENDER } ]“{ escape_markup (preview )} ”[/]"
365- )
370+ if text :
371+ for tline in text .split ("\n " ):
372+ if tline .strip ():
373+ self .add_scroll_line (
374+ f"[italic { _brand .LAVENDER } ]"
375+ f"\" { escape_markup (tline )} \" [/]"
376+ )
366377
367378 elif block_type == "tool_use" :
368379 name = block .get ("name" , "?" )
@@ -387,40 +398,40 @@ def _apply_assistant(self, raw: dict[str, Any]) -> str | None:
387398 else :
388399 name_col = f"{ name } "
389400 if arg :
390- scroll_line = (
401+ self . add_scroll_line (
391402 f"[bold { color } ]{ escape_markup (name_col )} [/]"
392403 f"[dim]{ escape_markup (arg )} [/]"
393404 )
394405 else :
395- scroll_line = f"[bold { color } ] { escape_markup ( name ) } [/]"
396-
397- return scroll_line
406+ self . add_scroll_line (
407+ f"[bold { color } ] { escape_markup ( name ) } [/]"
408+ )
398409
399- def _apply_user (self , raw : dict [str , Any ]) -> str | None :
410+ def _apply_user (self , raw : dict [str , Any ]) -> None :
400411 msg = raw .get ("message" , {})
401412 content = msg .get ("content" , [])
402413 if not isinstance (content , list ):
403- return None
414+ return
404415 for block in content :
405416 if not isinstance (block , dict ):
406417 continue
407418 if block .get ("type" ) == "tool_result" and block .get ("is_error" ):
408419 snippet = _truncate (str (block .get ("content" , "" )), 100 )
409- return (
420+ self . add_scroll_line (
410421 f"[bold { _brand .DEEP_ORANGE } ]{ _ICON_FAILURE } tool error:[/]"
411422 f" [dim]{ escape_markup (snippet )} [/]"
412423 )
413- return None
424+ return
414425
415426 def _format_tokens (self ) -> str :
416- """Format token counts as compact ↑in ↓ out string."""
427+ """Format token counts as compact ctx/ out string."""
417428 parts : list [str ] = []
418429 total_in = self ._input_tokens
419430 if total_in > 0 :
420- parts .append (f"↑ { self ._format_count (total_in )} " )
431+ parts .append (f"ctx { self ._format_count (total_in )} " )
421432 if self ._output_tokens > 0 :
422- parts .append (f"↓ { self ._format_count (self ._output_tokens )} " )
423- return " " .join (parts )
433+ parts .append (f"out { self ._format_count (self ._output_tokens )} " )
434+ return " · " .join (parts )
424435
425436 @staticmethod
426437 def _format_count (n : int ) -> str :
@@ -466,7 +477,7 @@ def _build_subtitle(self) -> Text | None:
466477 return sub
467478
468479 def _build_footer (self ) -> Table :
469- """Bottom row of the panel: spinner + tool counts."""
480+ """Bottom row of the panel: spinner + tool counts + peek hint ."""
470481 summary = Text (no_wrap = True , overflow = "ellipsis" )
471482 if self ._tool_count > 0 :
472483 summary .append (
@@ -480,10 +491,13 @@ def _build_footer(self) -> Table:
480491 else :
481492 summary .append ("waiting for first tool call…" , style = "dim italic" )
482493
494+ hint = Text ("Shift+P full screen" , style = "dim" , no_wrap = True )
495+
483496 grid = Table .grid (expand = True )
484497 grid .add_column (width = 2 , no_wrap = True )
485498 grid .add_column (ratio = 1 , no_wrap = True , overflow = "ellipsis" )
486- grid .add_row (self ._spinner , summary )
499+ grid .add_column (no_wrap = True , justify = "right" )
500+ grid .add_row (self ._spinner , summary , hint )
487501 return grid
488502
489503 def _build_body (self ) -> Group :
@@ -601,7 +615,8 @@ def _build_header(self, total: int, visible: int) -> Text:
601615 def _build_footer (self ) -> Text :
602616 hint = Text (no_wrap = True , overflow = "ellipsis" )
603617 hint .append (
604- " ↑/k up · ↓/j down · b/space page · g/G top/bottom · q/" , style = "dim"
618+ " ↑/k up · ↓/j down · b page up · space page down · g/G top/bottom · q/" ,
619+ style = "dim" ,
605620 )
606621 hint .append (FULLSCREEN_PEEK_KEY , style = f"bold { _brand .PURPLE } " )
607622 hint .append (" exit " , style = "dim" )
@@ -627,21 +642,52 @@ def __rich_console__(
627642 start = max (0 , end - visible )
628643 window = lines [start :end ]
629644
645+ # Scrollbar metrics
646+ show_scrollbar = total > visible
647+ thumb_start = 0
648+ thumb_size = visible
649+ if show_scrollbar :
650+ thumb_size = max (1 , visible * visible // total )
651+ max_off_val = max (total - visible , 1 )
652+ frac = 1.0 - (self ._offset / max_off_val )
653+ track_space = visible - thumb_size
654+ thumb_start = int (frac * track_space )
655+
630656 rows : list [Any ] = []
631657 rows .append (self ._build_header (total , visible ))
632658 rows .append (Text ("" ))
633- if window :
634- for line in window :
659+
660+ # Content area with optional scrollbar column
661+ content = Table .grid (expand = True )
662+ content .add_column (ratio = 1 , no_wrap = True , overflow = "ellipsis" )
663+ if show_scrollbar :
664+ content .add_column (width = 1 , no_wrap = True )
665+
666+ for i in range (visible ):
667+ if i < len (window ):
668+ line = window [i ]
635669 line .no_wrap = True
636670 line .overflow = "ellipsis"
637- rows .append (line )
638- else :
671+ else :
672+ line = Text ("" )
673+ if show_scrollbar :
674+ in_thumb = thumb_start <= i < thumb_start + thumb_size
675+ bar = Text (
676+ "█" if in_thumb else "│" ,
677+ style = _brand .PURPLE if in_thumb else "dim" ,
678+ )
679+ content .add_row (line , bar )
680+ else :
681+ content .add_row (line )
682+
683+ if not window and not show_scrollbar :
684+ # Replace the empty grid with a waiting message
639685 rows .append (Text (" (waiting for activity…)" , style = "dim italic" ))
640- # Pad the body so the footer hugs the bottom border even when
641- # the buffer is shorter than the viewport.
642- padding = visible - len ( window )
643- for _ in range ( max ( 0 , padding )):
644- rows . append ( Text ( "" ))
686+ for _ in range ( max ( 0 , visible - 1 )):
687+ rows . append ( Text ( "" ))
688+ else :
689+ rows . append ( content )
690+
645691 rows .append (Text ("" ))
646692 rows .append (self ._build_footer ())
647693
@@ -1145,10 +1191,14 @@ def _build_footer(self) -> Table:
11451191 summary .append (" of agent output" , style = "dim" )
11461192 else :
11471193 summary .append ("waiting for agent output…" , style = "dim italic" )
1194+
1195+ hint = Text ("Shift+P full screen" , style = "dim" , no_wrap = True )
1196+
11481197 grid = Table .grid (expand = True )
11491198 grid .add_column (width = 2 , no_wrap = True )
11501199 grid .add_column (ratio = 1 , no_wrap = True , overflow = "ellipsis" )
1151- grid .add_row (self ._spinner , summary )
1200+ grid .add_column (no_wrap = True , justify = "right" )
1201+ grid .add_row (self ._spinner , summary , hint )
11521202 return grid
11531203
11541204 def _build_body (self ) -> Group :
0 commit comments