fast-agent-mcp 0.3.11__py3-none-any.whl → 0.3.12__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of fast-agent-mcp might be problematic. Click here for more details.

@@ -0,0 +1,555 @@
1
+ """Display helpers for agent conversation history."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from shutil import get_terminal_size
6
+ from typing import TYPE_CHECKING, Optional, Sequence
7
+
8
+ from rich import print as rich_print
9
+ from rich.text import Text
10
+
11
+ from fast_agent.mcp.helpers.content_helpers import get_text
12
+
13
+ if TYPE_CHECKING: # pragma: no cover - typing only
14
+ from collections.abc import Mapping
15
+
16
+ from rich.console import Console
17
+
18
+ from fast_agent.llm.usage_tracking import UsageAccumulator
19
+ from fast_agent.types import PromptMessageExtended
20
+
21
+
22
+ NON_TEXT_MARKER = "^"
23
+ TIMELINE_WIDTH = 20
24
+ SUMMARY_COUNT = 8
25
+ ROLE_COLUMN_WIDTH = 17
26
+
27
+
28
+ def _normalize_text(value: Optional[str]) -> str:
29
+ return "" if not value else " ".join(value.split())
30
+
31
+
32
+ class Colours:
33
+ """Central colour palette for history display output."""
34
+
35
+ USER = "blue"
36
+ ASSISTANT = "green"
37
+ TOOL = "magenta"
38
+ HEADER = USER
39
+ TIMELINE_EMPTY = "dim default"
40
+ CONTEXT_SAFE = "green"
41
+ CONTEXT_CAUTION = "yellow"
42
+ CONTEXT_ALERT = "bright_red"
43
+ TOOL_DETAIL = "dim magenta"
44
+
45
+
46
+ def _char_count(value: Optional[str]) -> int:
47
+ return len(_normalize_text(value))
48
+
49
+
50
+ def _format_tool_detail(prefix: str, names: Sequence[str]) -> Text:
51
+ detail = Text(prefix, style=Colours.TOOL_DETAIL)
52
+ if names:
53
+ detail.append(", ".join(names), style=Colours.TOOL_DETAIL)
54
+ return detail
55
+
56
+
57
+ def _ensure_text(value: object | None) -> Text:
58
+ """Coerce various value types into a Rich Text instance."""
59
+
60
+ if isinstance(value, Text):
61
+ return value.copy()
62
+ if value is None:
63
+ return Text("")
64
+ if isinstance(value, str):
65
+ return Text(value)
66
+ if isinstance(value, Sequence) and not isinstance(value, (str, bytes, Text)):
67
+ return Text(", ".join(str(item) for item in value if item))
68
+ return Text(str(value))
69
+
70
+
71
+ def _truncate_text_segment(segment: Text, width: int) -> Text:
72
+ if width <= 0 or segment.cell_len == 0:
73
+ return Text("")
74
+ if segment.cell_len <= width:
75
+ return segment.copy()
76
+ truncated = segment.copy()
77
+ truncated.truncate(width, overflow="ellipsis")
78
+ return truncated
79
+
80
+
81
+ def _compose_summary_text(
82
+ preview: Text,
83
+ detail: Optional[Text],
84
+ *,
85
+ include_non_text: bool,
86
+ max_width: Optional[int],
87
+ ) -> Text:
88
+ marker_component = Text()
89
+ if include_non_text:
90
+ marker_component.append(" ")
91
+ marker_component.append(NON_TEXT_MARKER, style="dim")
92
+
93
+ if max_width is None:
94
+ combined = Text()
95
+ combined.append_text(preview)
96
+ if detail and detail.cell_len > 0:
97
+ if combined.cell_len > 0:
98
+ combined.append(" ")
99
+ combined.append_text(detail)
100
+ combined.append_text(marker_component)
101
+ return combined
102
+
103
+ width_available = max_width
104
+ if width_available <= 0:
105
+ return Text("")
106
+
107
+ if marker_component.cell_len > width_available:
108
+ marker_component = Text("")
109
+ marker_width = marker_component.cell_len
110
+ width_after_marker = max(0, width_available - marker_width)
111
+
112
+ preview_len = preview.cell_len
113
+ detail_component = detail.copy() if detail else Text("")
114
+ detail_len = detail_component.cell_len
115
+ detail_plain = detail_component.plain
116
+
117
+ preview_allow = min(preview_len, width_after_marker)
118
+ detail_allow = 0
119
+ if detail_len > 0 and width_after_marker > 0:
120
+ detail_allow = min(detail_len, max(0, width_after_marker - preview_allow))
121
+
122
+ if width_after_marker > 0:
123
+ min_detail_allow = 1
124
+ for prefix in ("tool→", "result→"):
125
+ if detail_plain.startswith(prefix):
126
+ min_detail_allow = min(detail_len, len(prefix))
127
+ break
128
+ else:
129
+ min_detail_allow = 0
130
+ if detail_allow < min_detail_allow:
131
+ needed = min_detail_allow - detail_allow
132
+ reduction = min(preview_allow, needed)
133
+ preview_allow -= reduction
134
+ detail_allow += reduction
135
+
136
+ preview_allow = max(0, preview_allow)
137
+ detail_allow = max(0, min(detail_allow, detail_len))
138
+
139
+ space = 1 if preview_allow > 0 and detail_allow > 0 else 0
140
+ total = preview_allow + detail_allow + space
141
+ if total > width_after_marker:
142
+ overflow = total - width_after_marker
143
+ reduction = min(preview_allow, overflow)
144
+ preview_allow -= reduction
145
+ overflow -= reduction
146
+ if overflow > 0:
147
+ detail_allow = max(0, detail_allow - overflow)
148
+
149
+ preview_allow = max(0, preview_allow)
150
+ detail_allow = max(0, min(detail_allow, detail_len))
151
+ else:
152
+ preview_allow = min(preview_len, width_after_marker)
153
+ detail_allow = 0
154
+
155
+ preview_segment = _truncate_text_segment(preview, preview_allow)
156
+ detail_segment = (
157
+ _truncate_text_segment(detail_component, detail_allow) if detail_allow > 0 else Text("")
158
+ )
159
+
160
+ combined = Text()
161
+ combined.append_text(preview_segment)
162
+ if preview_segment.cell_len > 0 and detail_segment.cell_len > 0:
163
+ combined.append(" ")
164
+ combined.append_text(detail_segment)
165
+
166
+ if marker_component.cell_len > 0:
167
+ if combined.cell_len + marker_component.cell_len <= max_width:
168
+ combined.append_text(marker_component)
169
+
170
+ return combined
171
+
172
+
173
+ def _preview_text(value: Optional[str], limit: int = 80) -> str:
174
+ normalized = _normalize_text(value)
175
+ if not normalized:
176
+ return "<no text>"
177
+ if len(normalized) <= limit:
178
+ return normalized
179
+ return normalized[: limit - 1] + "…"
180
+
181
+
182
+ def _has_non_text_content(message: PromptMessageExtended) -> bool:
183
+ for block in getattr(message, "content", []) or []:
184
+ block_type = getattr(block, "type", None)
185
+ if block_type and block_type != "text":
186
+ return True
187
+ return False
188
+
189
+
190
+ def _extract_tool_result_summary(result, *, limit: int = 80) -> tuple[str, int, bool]:
191
+ preview: Optional[str] = None
192
+ total_chars = 0
193
+ saw_non_text = False
194
+
195
+ for block in getattr(result, "content", []) or []:
196
+ text = get_text(block)
197
+ if text:
198
+ normalized = _normalize_text(text)
199
+ if preview is None:
200
+ preview = _preview_text(normalized, limit=limit)
201
+ total_chars += len(normalized)
202
+ else:
203
+ saw_non_text = True
204
+
205
+ if preview is not None:
206
+ return preview, total_chars, saw_non_text
207
+ return f"{NON_TEXT_MARKER} non-text tool result", 0, True
208
+
209
+
210
+ def format_chars(value: int) -> str:
211
+ if value <= 0:
212
+ return "—"
213
+ if value >= 1_000_000:
214
+ return f"{value / 1_000_000:.1f}M"
215
+ if value >= 10_000:
216
+ return f"{value / 1_000:.1f}k"
217
+ return str(value)
218
+
219
+
220
+ def _build_history_rows(history: Sequence[PromptMessageExtended]) -> list[dict]:
221
+ rows: list[dict] = []
222
+ call_name_lookup: dict[str, str] = {}
223
+
224
+ for message in history:
225
+ role_raw = getattr(message, "role", "assistant")
226
+ role_value = getattr(role_raw, "value", role_raw)
227
+ role = str(role_value).lower() if role_value else "assistant"
228
+
229
+ text = ""
230
+ if hasattr(message, "first_text"):
231
+ try:
232
+ text = message.first_text() or ""
233
+ except Exception: # pragma: no cover - defensive
234
+ text = ""
235
+ normalized_text = _normalize_text(text)
236
+ chars = len(normalized_text)
237
+ preview = _preview_text(text)
238
+ non_text = _has_non_text_content(message) or chars == 0
239
+
240
+ tool_calls: Optional[Mapping[str, object]] = getattr(message, "tool_calls", None)
241
+ tool_results: Optional[Mapping[str, object]] = getattr(message, "tool_results", None)
242
+
243
+ detail_sections: list[Text] = []
244
+ row_non_text = non_text
245
+ has_tool_request = False
246
+ hide_in_summary = False
247
+ timeline_role = role
248
+ include_in_timeline = True
249
+ result_rows: list[dict] = []
250
+ tool_result_total_chars = 0
251
+ tool_result_has_non_text = False
252
+
253
+ if tool_calls:
254
+ names: list[str] = []
255
+ for call_id, call in tool_calls.items():
256
+ params = getattr(call, "params", None)
257
+ name = getattr(params, "name", None) or getattr(call, "name", None) or call_id
258
+ call_name_lookup[call_id] = name
259
+ names.append(name)
260
+ if names:
261
+ detail_sections.append(_format_tool_detail("tool→", names))
262
+ row_non_text = row_non_text and chars == 0 # treat call as activity
263
+ has_tool_request = True
264
+ if not normalized_text and tool_calls:
265
+ preview = "(issuing tool request)"
266
+
267
+ if tool_results:
268
+ result_names: list[str] = []
269
+ for call_id, result in tool_results.items():
270
+ tool_name = call_name_lookup.get(call_id, call_id)
271
+ result_names.append(tool_name)
272
+ summary, result_chars, result_non_text = _extract_tool_result_summary(result)
273
+ tool_result_total_chars += result_chars
274
+ tool_result_has_non_text = tool_result_has_non_text or result_non_text
275
+ detail = _format_tool_detail("result→", [tool_name])
276
+ result_rows.append(
277
+ {
278
+ "role": "tool",
279
+ "timeline_role": "tool",
280
+ "chars": result_chars,
281
+ "preview": summary,
282
+ "details": detail,
283
+ "non_text": result_non_text,
284
+ "has_tool_request": False,
285
+ "hide_summary": False,
286
+ "include_in_timeline": False,
287
+ }
288
+ )
289
+ if role == "user":
290
+ timeline_role = "tool"
291
+ hide_in_summary = True
292
+ if result_names:
293
+ detail_sections.append(_format_tool_detail("result→", result_names))
294
+
295
+ if detail_sections:
296
+ if len(detail_sections) == 1:
297
+ details: Text | None = detail_sections[0]
298
+ else:
299
+ details = Text()
300
+ for index, section in enumerate(detail_sections):
301
+ if index > 0:
302
+ details.append(" ")
303
+ details.append_text(section)
304
+ else:
305
+ details = None
306
+
307
+ row_chars = chars
308
+ if timeline_role == "tool" and tool_result_total_chars > 0:
309
+ row_chars = tool_result_total_chars
310
+ row_non_text = row_non_text or tool_result_has_non_text
311
+
312
+ rows.append(
313
+ {
314
+ "role": role,
315
+ "timeline_role": timeline_role,
316
+ "chars": row_chars,
317
+ "preview": preview,
318
+ "details": details,
319
+ "non_text": row_non_text,
320
+ "has_tool_request": has_tool_request,
321
+ "hide_summary": hide_in_summary,
322
+ "include_in_timeline": include_in_timeline,
323
+ }
324
+ )
325
+ rows.extend(result_rows)
326
+
327
+ return rows
328
+
329
+
330
+ def _aggregate_timeline_entries(rows: Sequence[dict]) -> list[dict]:
331
+ return [
332
+ {
333
+ "role": row.get("timeline_role", row["role"]),
334
+ "chars": row["chars"],
335
+ "non_text": row["non_text"],
336
+ }
337
+ for row in rows
338
+ if row.get("include_in_timeline", True)
339
+ ]
340
+
341
+
342
+ def _shade_block(chars: int, *, non_text: bool, color: str) -> Text:
343
+ if non_text:
344
+ return Text(NON_TEXT_MARKER, style=f"bold {color}")
345
+ if chars <= 0:
346
+ return Text("·", style="dim")
347
+ if chars < 50:
348
+ return Text("░", style=f"dim {color}")
349
+ if chars < 200:
350
+ return Text("▒", style=f"dim {color}")
351
+ if chars < 500:
352
+ return Text("▒", style=color)
353
+ if chars < 2000:
354
+ return Text("▓", style=color)
355
+ return Text("█", style=f"bold {color}")
356
+
357
+
358
+ def _build_history_bar(entries: Sequence[dict], width: int = TIMELINE_WIDTH) -> tuple[Text, Text]:
359
+ color_map = {"user": Colours.USER, "assistant": Colours.ASSISTANT, "tool": Colours.TOOL}
360
+
361
+ recent = list(entries[-width:])
362
+ bar = Text(" history |", style="dim")
363
+ for entry in recent:
364
+ color = color_map.get(entry["role"], "ansiwhite")
365
+ bar.append_text(
366
+ _shade_block(entry["chars"], non_text=entry.get("non_text", False), color=color)
367
+ )
368
+ remaining = width - len(recent)
369
+ if remaining > 0:
370
+ bar.append("░" * remaining, style=Colours.TIMELINE_EMPTY)
371
+ bar.append("|", style="dim")
372
+
373
+ detail = Text(f"{len(entries)} turns", style="dim")
374
+ return bar, detail
375
+
376
+
377
+ def _build_context_bar_line(
378
+ current: int,
379
+ window: Optional[int],
380
+ width: int = TIMELINE_WIDTH,
381
+ ) -> tuple[Text, Text]:
382
+ bar = Text(" context |", style="dim")
383
+
384
+ if not window or window <= 0:
385
+ bar.append("░" * width, style=Colours.TIMELINE_EMPTY)
386
+ bar.append("|", style="dim")
387
+ detail = Text(f"{format_chars(current)} tokens (unknown window)", style="dim")
388
+ return bar, detail
389
+
390
+ percent = current / window if window else 0.0
391
+ filled = min(width, int(round(min(percent, 1.0) * width)))
392
+
393
+ def color_for(pct: float) -> str:
394
+ if pct >= 0.9:
395
+ return Colours.CONTEXT_ALERT
396
+ if pct >= 0.7:
397
+ return Colours.CONTEXT_CAUTION
398
+ return Colours.CONTEXT_SAFE
399
+
400
+ color = color_for(percent)
401
+ if filled > 0:
402
+ bar.append("█" * filled, style=color)
403
+ if filled < width:
404
+ bar.append("░" * (width - filled), style=Colours.TIMELINE_EMPTY)
405
+ bar.append("|", style="dim")
406
+ bar.append(f" {percent * 100:5.1f}%", style="dim")
407
+ if percent > 1.0:
408
+ bar.append(f" +{(percent - 1) * 100:.0f}%", style="bold bright_red")
409
+
410
+ detail = Text(f"{format_chars(current)} / {format_chars(window)} →", style="dim")
411
+ return bar, detail
412
+
413
+
414
+ def _render_header_line(agent_name: str, *, console: Optional[Console], printer) -> None:
415
+ header = Text()
416
+ header.append("▎", style=Colours.HEADER)
417
+ header.append("●", style=f"dim {Colours.HEADER}")
418
+ header.append(" [ 1] ", style=Colours.HEADER)
419
+ header.append(str(agent_name), style=f"bold {Colours.USER}")
420
+
421
+ line = Text()
422
+ line.append_text(header)
423
+ line.append(" ")
424
+
425
+ try:
426
+ total_width = console.width if console else get_terminal_size().columns
427
+ except Exception:
428
+ total_width = 80
429
+
430
+ separator_width = max(1, total_width - line.cell_len)
431
+ line.append("─" * separator_width, style="dim")
432
+
433
+ printer("")
434
+ printer(line)
435
+ printer("")
436
+
437
+
438
+ def display_history_overview(
439
+ agent_name: str,
440
+ history: Sequence[PromptMessageExtended],
441
+ usage_accumulator: Optional["UsageAccumulator"] = None,
442
+ *,
443
+ console: Optional[Console] = None,
444
+ ) -> None:
445
+ if not history:
446
+ printer = console.print if console else rich_print
447
+ printer("[dim]No conversation history yet[/dim]")
448
+ return
449
+
450
+ printer = console.print if console else rich_print
451
+
452
+ rows = _build_history_rows(history)
453
+ timeline_entries = _aggregate_timeline_entries(rows)
454
+
455
+ history_bar, history_detail = _build_history_bar(timeline_entries)
456
+ if usage_accumulator:
457
+ current_tokens = getattr(usage_accumulator, "current_context_tokens", 0)
458
+ window = getattr(usage_accumulator, "context_window_size", None)
459
+ else:
460
+ current_tokens = 0
461
+ window = None
462
+ context_bar, context_detail = _build_context_bar_line(current_tokens, window)
463
+
464
+ _render_header_line(agent_name, console=console, printer=printer)
465
+
466
+ gap = Text(" ")
467
+ combined_line = Text()
468
+ combined_line.append_text(history_bar)
469
+ combined_line.append_text(gap)
470
+ combined_line.append_text(context_bar)
471
+ printer(combined_line)
472
+
473
+ history_label_len = len(" history |")
474
+ context_label_len = len(" context |")
475
+
476
+ history_available = history_bar.cell_len - history_label_len
477
+ context_available = context_bar.cell_len - context_label_len
478
+
479
+ detail_line = Text()
480
+ detail_line.append(" " * history_label_len, style="dim")
481
+ detail_line.append_text(history_detail)
482
+ if history_available > history_detail.cell_len:
483
+ detail_line.append(" " * (history_available - history_detail.cell_len), style="dim")
484
+ detail_line.append_text(gap)
485
+ detail_line.append(" " * context_label_len, style="dim")
486
+ detail_line.append_text(context_detail)
487
+ if context_available > context_detail.cell_len:
488
+ detail_line.append(" " * (context_available - context_detail.cell_len), style="dim")
489
+ printer(detail_line)
490
+
491
+ printer("")
492
+ printer(
493
+ Text(" " + "─" * (history_bar.cell_len + context_bar.cell_len + gap.cell_len), style="dim")
494
+ )
495
+
496
+ header_line = Text(" ")
497
+ header_line.append(" #", style="dim")
498
+ header_line.append(" ", style="dim")
499
+ header_line.append(f" {'Role':<{ROLE_COLUMN_WIDTH}}", style="dim")
500
+ header_line.append(f" {'Chars':>7}", style="dim")
501
+ header_line.append(" ", style="dim")
502
+ header_line.append("Summary", style="dim")
503
+ printer(header_line)
504
+
505
+ summary_candidates = [row for row in rows if not row.get("hide_summary")]
506
+ summary_rows = summary_candidates[-SUMMARY_COUNT:]
507
+ start_index = len(summary_candidates) - len(summary_rows) + 1
508
+
509
+ role_arrows = {"user": "▶", "assistant": "◀", "tool": "▶"}
510
+ role_styles = {"user": Colours.USER, "assistant": Colours.ASSISTANT, "tool": Colours.TOOL}
511
+ role_labels = {"user": "user", "assistant": "assistant", "tool": "tool result"}
512
+
513
+ try:
514
+ total_width = console.width if console else get_terminal_size().columns
515
+ except Exception:
516
+ total_width = 80
517
+
518
+ for offset, row in enumerate(summary_rows):
519
+ role = row["role"]
520
+ color = role_styles.get(role, "white")
521
+ arrow = role_arrows.get(role, "▶")
522
+ label = role_labels.get(role, role)
523
+ if role == "assistant" and row.get("has_tool_request"):
524
+ label = f"{label}*"
525
+ chars = row["chars"]
526
+ block = _shade_block(chars, non_text=row.get("non_text", False), color=color)
527
+
528
+ details = row.get("details")
529
+ preview_value = row["preview"]
530
+ preview_text = _ensure_text(preview_value)
531
+ detail_text = _ensure_text(details) if details else Text("")
532
+ if detail_text.cell_len == 0:
533
+ detail_text = None
534
+
535
+ line = Text(" ")
536
+ line.append(f"{start_index + offset:>2}", style="dim")
537
+ line.append(" ")
538
+ line.append_text(block)
539
+ line.append(" ")
540
+ line.append(arrow, style=color)
541
+ line.append(" ")
542
+ line.append(f"{label:<{ROLE_COLUMN_WIDTH}}", style=color)
543
+ line.append(f" {format_chars(chars):>7}", style="dim")
544
+ line.append(" ")
545
+ summary_width = max(0, total_width - line.cell_len)
546
+ summary_text = _compose_summary_text(
547
+ preview_text,
548
+ detail_text,
549
+ include_non_text=row.get("non_text", False),
550
+ max_width=summary_width,
551
+ )
552
+ line.append_text(summary_text)
553
+ printer(line)
554
+
555
+ printer("")
@@ -34,6 +34,7 @@ from fast_agent.ui.enhanced_prompt import (
34
34
  handle_special_commands,
35
35
  show_mcp_status,
36
36
  )
37
+ from fast_agent.ui.history_display import display_history_overview
37
38
  from fast_agent.ui.progress_display import progress_display
38
39
  from fast_agent.ui.usage_display import collect_agents_from_provider, display_usage_report
39
40
 
@@ -170,6 +171,39 @@ class InteractivePrompt:
170
171
  # Handle usage display
171
172
  await self._show_usage(prompt_provider, agent)
172
173
  continue
174
+ elif "show_history" in command_result:
175
+ target_agent = command_result.get("show_history", {}).get("agent") or agent
176
+ try:
177
+ agent_obj = prompt_provider._agent(target_agent)
178
+ except Exception:
179
+ rich_print(f"[red]Unable to load agent '{target_agent}'[/red]")
180
+ continue
181
+
182
+ history = getattr(agent_obj, "message_history", [])
183
+ usage = getattr(agent_obj, "usage_accumulator", None)
184
+ display_history_overview(target_agent, history, usage)
185
+ continue
186
+ elif "clear_history" in command_result:
187
+ target_agent = command_result.get("clear_history", {}).get("agent") or agent
188
+ try:
189
+ agent_obj = prompt_provider._agent(target_agent)
190
+ except Exception:
191
+ rich_print(f"[red]Unable to load agent '{target_agent}'[/red]")
192
+ continue
193
+
194
+ if hasattr(agent_obj, "clear"):
195
+ try:
196
+ agent_obj.clear()
197
+ rich_print(f"[green]History cleared for agent '{target_agent}'.[/green]")
198
+ except Exception as exc:
199
+ rich_print(
200
+ f"[red]Failed to clear history for '{target_agent}': {exc}[/red]"
201
+ )
202
+ else:
203
+ rich_print(
204
+ f"[yellow]Agent '{target_agent}' does not support clearing history.[/yellow]"
205
+ )
206
+ continue
173
207
  elif "show_system" in command_result:
174
208
  # Handle system prompt display
175
209
  await self._show_system(prompt_provider, agent)
@@ -266,9 +300,7 @@ class InteractivePrompt:
266
300
  console.print(combined)
267
301
  rich_print()
268
302
 
269
- async def _get_all_prompts(
270
- self, prompt_provider: "AgentApp", agent_name: Optional[str] = None
271
- ):
303
+ async def _get_all_prompts(self, prompt_provider: "AgentApp", agent_name: Optional[str] = None):
272
304
  """
273
305
  Get a list of all available prompts.
274
306
 
@@ -923,7 +955,7 @@ class InteractivePrompt:
923
955
  agent = prompt_provider._agent(agent_name)
924
956
 
925
957
  # Get the system prompt
926
- system_prompt = getattr(agent, 'instruction', None)
958
+ system_prompt = getattr(agent, "instruction", None)
927
959
  if not system_prompt:
928
960
  rich_print("[yellow]No system prompt available[/yellow]")
929
961
  return
@@ -936,24 +968,24 @@ class InteractivePrompt:
936
968
  )
937
969
 
938
970
  # Use the display utility to show the system prompt
939
- if hasattr(agent, 'display') and agent.display:
971
+ if hasattr(agent, "display") and agent.display:
940
972
  agent.display.show_system_message(
941
- system_prompt=system_prompt,
942
- agent_name=agent_name,
943
- server_count=server_count
973
+ system_prompt=system_prompt, agent_name=agent_name, server_count=server_count
944
974
  )
945
975
  else:
946
976
  # Fallback to basic display
947
977
  from fast_agent.ui.console_display import ConsoleDisplay
948
- display = ConsoleDisplay(config=agent.context.config if hasattr(agent, 'context') else None)
978
+
979
+ display = ConsoleDisplay(
980
+ config=agent.context.config if hasattr(agent, "context") else None
981
+ )
949
982
  display.show_system_message(
950
- system_prompt=system_prompt,
951
- agent_name=agent_name,
952
- server_count=server_count
983
+ system_prompt=system_prompt, agent_name=agent_name, server_count=server_count
953
984
  )
954
985
 
955
986
  except Exception as e:
956
987
  import traceback
988
+
957
989
  rich_print(f"[red]Error showing system prompt: {e}[/red]")
958
990
  rich_print(f"[dim]{traceback.format_exc()}[/dim]")
959
991
 
@@ -413,6 +413,9 @@ def _render_channel_summary(status: ServerStatus, indent: str, total_width: int)
413
413
  elif channel.last_status_code == 405 and "GET" in label:
414
414
  # Special case: GET (SSE) with 405 = dim (hollow arrow already handled above)
415
415
  label_style = Colours.TEXT_DIM
416
+ elif arrow_style == Colours.ARROW_ERROR and "GET" in label:
417
+ # Highlight GET stream errors in red to match the arrow indicator
418
+ label_style = Colours.TEXT_ERROR
416
419
  elif channel.request_count == 0 and channel.response_count == 0:
417
420
  # No activity = dim
418
421
  label_style = Colours.TEXT_DIM