22
33from __future__ import annotations
44
5+ import json
6+ from collections .abc import Mapping , Sequence
57from shutil import get_terminal_size
6- from typing import TYPE_CHECKING , Optional , Sequence
8+ from typing import TYPE_CHECKING
79
810from rich import print as rich_print
911from rich .text import Text
1012
13+ from fast_agent .constants import FAST_AGENT_TIMING
1114from fast_agent .mcp .helpers .content_helpers import get_text
1215
1316if TYPE_CHECKING : # pragma: no cover - typing only
14- from collections .abc import Mapping
15-
1617 from rich .console import Console
1718
1819 from fast_agent .llm .usage_tracking import UsageAccumulator
2526ROLE_COLUMN_WIDTH = 17
2627
2728
28- def _normalize_text (value : Optional [ str ] ) -> str :
29+ def _normalize_text (value : str | None ) -> str :
2930 return "" if not value else " " .join (value .split ())
3031
3132
@@ -44,7 +45,7 @@ class Colours:
4445 TOOL_DETAIL = "dim magenta"
4546
4647
47- def _char_count (value : Optional [ str ] ) -> int :
48+ def _char_count (value : str | None ) -> int :
4849 return len (_normalize_text (value ))
4950
5051
@@ -81,10 +82,10 @@ def _truncate_text_segment(segment: Text, width: int) -> Text:
8182
8283def _compose_summary_text (
8384 preview : Text ,
84- detail : Optional [ Text ] ,
85+ detail : Text | None ,
8586 * ,
8687 include_non_text : bool ,
87- max_width : Optional [ int ] ,
88+ max_width : int | None ,
8889) -> Text :
8990 marker_component = Text ()
9091 if include_non_text :
@@ -171,7 +172,7 @@ def _compose_summary_text(
171172 return combined
172173
173174
174- def _preview_text (value : Optional [ str ] , limit : int = 80 ) -> str :
175+ def _preview_text (value : str | None , limit : int = 80 ) -> str :
175176 normalized = _normalize_text (value )
176177 if not normalized :
177178 return "<no text>"
@@ -189,7 +190,7 @@ def _has_non_text_content(message: PromptMessageExtended) -> bool:
189190
190191
191192def _extract_tool_result_summary (result , * , limit : int = 80 ) -> tuple [str , int , bool ]:
192- preview : Optional [ str ] = None
193+ preview : str | None = None
193194 total_chars = 0
194195 saw_non_text = False
195196
@@ -218,6 +219,36 @@ def format_chars(value: int) -> str:
218219 return str (value )
219220
220221
222+ def _extract_timing_ms (message : PromptMessageExtended ) -> float | None :
223+ """Extract timing duration in milliseconds from message channels."""
224+ channels = getattr (message , "channels" , None )
225+ if not channels :
226+ return None
227+
228+ timing_blocks = channels .get (FAST_AGENT_TIMING , [])
229+ if not timing_blocks :
230+ return None
231+
232+ timing_text = get_text (timing_blocks [0 ])
233+ if not timing_text :
234+ return None
235+
236+ try :
237+ timing_data = json .loads (timing_text )
238+ return timing_data .get ("duration_ms" )
239+ except (json .JSONDecodeError , AttributeError , KeyError ):
240+ return None
241+
242+
243+ def format_time (value : float | None ) -> str :
244+ """Format timing value for display."""
245+ if value is None :
246+ return "-"
247+ if value < 1000 :
248+ return f"{ value :.0f} ms"
249+ return f"{ value / 1000 :.1f} s"
250+
251+
221252def _build_history_rows (history : Sequence [PromptMessageExtended ]) -> list [dict ]:
222253 rows : list [dict ] = []
223254 call_name_lookup : dict [str , str ] = {}
@@ -238,8 +269,11 @@ def _build_history_rows(history: Sequence[PromptMessageExtended]) -> list[dict]:
238269 preview = _preview_text (text )
239270 non_text = _has_non_text_content (message ) or chars == 0
240271
241- tool_calls : Optional [Mapping [str , object ]] = getattr (message , "tool_calls" , None )
242- tool_results : Optional [Mapping [str , object ]] = getattr (message , "tool_results" , None )
272+ # Extract timing data
273+ timing_ms = _extract_timing_ms (message )
274+
275+ tool_calls : Mapping [str , object ] | None = getattr (message , "tool_calls" , None )
276+ tool_results : Mapping [str , object ] | None = getattr (message , "tool_results" , None )
243277
244278 detail_sections : list [Text ] = []
245279 row_non_text = non_text
@@ -289,6 +323,7 @@ def _build_history_rows(history: Sequence[PromptMessageExtended]) -> list[dict]:
289323 "hide_summary" : False ,
290324 "include_in_timeline" : False ,
291325 "is_error" : is_error ,
326+ "timing_ms" : None ,
292327 }
293328 )
294329 if role == "user" :
@@ -327,6 +362,7 @@ def _build_history_rows(history: Sequence[PromptMessageExtended]) -> list[dict]:
327362 "hide_summary" : hide_in_summary ,
328363 "include_in_timeline" : include_in_timeline ,
329364 "is_error" : row_is_error ,
365+ "timing_ms" : timing_ms ,
330366 }
331367 )
332368 rows .extend (result_rows )
@@ -392,7 +428,7 @@ def _build_history_bar(entries: Sequence[dict], width: int = TIMELINE_WIDTH) ->
392428
393429def _build_context_bar_line (
394430 current : int ,
395- window : Optional [ int ] ,
431+ window : int | None ,
396432 width : int = TIMELINE_WIDTH ,
397433) -> tuple [Text , Text ]:
398434 bar = Text (" context |" , style = "dim" )
@@ -427,7 +463,7 @@ def color_for(pct: float) -> str:
427463 return bar , detail
428464
429465
430- def _render_header_line (agent_name : str , * , console : Optional [ Console ] , printer ) -> None :
466+ def _render_header_line (agent_name : str , * , console : Console | None , printer ) -> None :
431467 header = Text ()
432468 header .append ("▎" , style = Colours .HEADER )
433469 header .append ("●" , style = f"dim { Colours .HEADER } " )
@@ -454,9 +490,9 @@ def _render_header_line(agent_name: str, *, console: Optional[Console], printer)
454490def display_history_overview (
455491 agent_name : str ,
456492 history : Sequence [PromptMessageExtended ],
457- usage_accumulator : Optional [ "UsageAccumulator" ] = None ,
493+ usage_accumulator : "UsageAccumulator" | None = None ,
458494 * ,
459- console : Optional [ Console ] = None ,
495+ console : Console | None = None ,
460496) -> None :
461497 if not history :
462498 printer = console .print if console else rich_print
@@ -509,15 +545,6 @@ def display_history_overview(
509545 Text (" " + "─" * (history_bar .cell_len + context_bar .cell_len + gap .cell_len ), style = "dim" )
510546 )
511547
512- header_line = Text (" " )
513- header_line .append (" #" , style = "dim" )
514- header_line .append (" " , style = "dim" )
515- header_line .append (f" { 'Role' :<{ROLE_COLUMN_WIDTH }} " , style = "dim" )
516- header_line .append (f" { 'Chars' :>7} " , style = "dim" )
517- header_line .append (" " , style = "dim" )
518- header_line .append ("Summary" , style = "dim" )
519- printer (header_line )
520-
521548 summary_candidates = [row for row in rows if not row .get ("hide_summary" )]
522549 summary_rows = summary_candidates [- SUMMARY_COUNT :]
523550 start_index = len (summary_candidates ) - len (summary_rows ) + 1
@@ -530,6 +557,22 @@ def display_history_overview(
530557 except Exception :
531558 total_width = 80
532559
560+ # Responsive column layout based on terminal width
561+ show_time = total_width >= 60
562+ show_chars = total_width >= 50
563+
564+ header_line = Text (" " )
565+ header_line .append (" #" , style = "dim" )
566+ header_line .append (" " , style = "dim" )
567+ header_line .append (f" { 'Role' :<{ROLE_COLUMN_WIDTH }} " , style = "dim" )
568+ if show_time :
569+ header_line .append (f" { 'Time' :>7} " , style = "dim" )
570+ if show_chars :
571+ header_line .append (f" { 'Chars' :>7} " , style = "dim" )
572+ header_line .append (" " , style = "dim" )
573+ header_line .append ("Summary" , style = "dim" )
574+ printer (header_line )
575+
533576 for offset , row in enumerate (summary_rows ):
534577 role = row ["role" ]
535578 color = _get_role_color (role , is_error = row .get ("is_error" , False ))
@@ -547,6 +590,9 @@ def display_history_overview(
547590 if detail_text .cell_len == 0 :
548591 detail_text = None
549592
593+ timing_ms = row .get ("timing_ms" )
594+ timing_str = format_time (timing_ms )
595+
550596 line = Text (" " )
551597 line .append (f"{ start_index + offset :>2} " , style = "dim" )
552598 line .append (" " )
@@ -555,7 +601,10 @@ def display_history_overview(
555601 line .append (arrow , style = color )
556602 line .append (" " )
557603 line .append (f"{ label :<{ROLE_COLUMN_WIDTH }} " , style = color )
558- line .append (f" { format_chars (chars ):>7} " , style = "dim" )
604+ if show_time :
605+ line .append (f" { timing_str :>7} " , style = "dim" )
606+ if show_chars :
607+ line .append (f" { format_chars (chars ):>7} " , style = "dim" )
559608 line .append (" " )
560609 summary_width = max (0 , total_width - line .cell_len )
561610 summary_text = _compose_summary_text (
0 commit comments