fast-agent-mcp 0.4.7__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.
Files changed (261) hide show
  1. fast_agent/__init__.py +183 -0
  2. fast_agent/acp/__init__.py +19 -0
  3. fast_agent/acp/acp_aware_mixin.py +304 -0
  4. fast_agent/acp/acp_context.py +437 -0
  5. fast_agent/acp/content_conversion.py +136 -0
  6. fast_agent/acp/filesystem_runtime.py +427 -0
  7. fast_agent/acp/permission_store.py +269 -0
  8. fast_agent/acp/server/__init__.py +5 -0
  9. fast_agent/acp/server/agent_acp_server.py +1472 -0
  10. fast_agent/acp/slash_commands.py +1050 -0
  11. fast_agent/acp/terminal_runtime.py +408 -0
  12. fast_agent/acp/tool_permission_adapter.py +125 -0
  13. fast_agent/acp/tool_permissions.py +474 -0
  14. fast_agent/acp/tool_progress.py +814 -0
  15. fast_agent/agents/__init__.py +85 -0
  16. fast_agent/agents/agent_types.py +64 -0
  17. fast_agent/agents/llm_agent.py +350 -0
  18. fast_agent/agents/llm_decorator.py +1139 -0
  19. fast_agent/agents/mcp_agent.py +1337 -0
  20. fast_agent/agents/tool_agent.py +271 -0
  21. fast_agent/agents/workflow/agents_as_tools_agent.py +849 -0
  22. fast_agent/agents/workflow/chain_agent.py +212 -0
  23. fast_agent/agents/workflow/evaluator_optimizer.py +380 -0
  24. fast_agent/agents/workflow/iterative_planner.py +652 -0
  25. fast_agent/agents/workflow/maker_agent.py +379 -0
  26. fast_agent/agents/workflow/orchestrator_models.py +218 -0
  27. fast_agent/agents/workflow/orchestrator_prompts.py +248 -0
  28. fast_agent/agents/workflow/parallel_agent.py +250 -0
  29. fast_agent/agents/workflow/router_agent.py +353 -0
  30. fast_agent/cli/__init__.py +0 -0
  31. fast_agent/cli/__main__.py +73 -0
  32. fast_agent/cli/commands/acp.py +159 -0
  33. fast_agent/cli/commands/auth.py +404 -0
  34. fast_agent/cli/commands/check_config.py +783 -0
  35. fast_agent/cli/commands/go.py +514 -0
  36. fast_agent/cli/commands/quickstart.py +557 -0
  37. fast_agent/cli/commands/serve.py +143 -0
  38. fast_agent/cli/commands/server_helpers.py +114 -0
  39. fast_agent/cli/commands/setup.py +174 -0
  40. fast_agent/cli/commands/url_parser.py +190 -0
  41. fast_agent/cli/constants.py +40 -0
  42. fast_agent/cli/main.py +115 -0
  43. fast_agent/cli/terminal.py +24 -0
  44. fast_agent/config.py +798 -0
  45. fast_agent/constants.py +41 -0
  46. fast_agent/context.py +279 -0
  47. fast_agent/context_dependent.py +50 -0
  48. fast_agent/core/__init__.py +92 -0
  49. fast_agent/core/agent_app.py +448 -0
  50. fast_agent/core/core_app.py +137 -0
  51. fast_agent/core/direct_decorators.py +784 -0
  52. fast_agent/core/direct_factory.py +620 -0
  53. fast_agent/core/error_handling.py +27 -0
  54. fast_agent/core/exceptions.py +90 -0
  55. fast_agent/core/executor/__init__.py +0 -0
  56. fast_agent/core/executor/executor.py +280 -0
  57. fast_agent/core/executor/task_registry.py +32 -0
  58. fast_agent/core/executor/workflow_signal.py +324 -0
  59. fast_agent/core/fastagent.py +1186 -0
  60. fast_agent/core/logging/__init__.py +5 -0
  61. fast_agent/core/logging/events.py +138 -0
  62. fast_agent/core/logging/json_serializer.py +164 -0
  63. fast_agent/core/logging/listeners.py +309 -0
  64. fast_agent/core/logging/logger.py +278 -0
  65. fast_agent/core/logging/transport.py +481 -0
  66. fast_agent/core/prompt.py +9 -0
  67. fast_agent/core/prompt_templates.py +183 -0
  68. fast_agent/core/validation.py +326 -0
  69. fast_agent/event_progress.py +62 -0
  70. fast_agent/history/history_exporter.py +49 -0
  71. fast_agent/human_input/__init__.py +47 -0
  72. fast_agent/human_input/elicitation_handler.py +123 -0
  73. fast_agent/human_input/elicitation_state.py +33 -0
  74. fast_agent/human_input/form_elements.py +59 -0
  75. fast_agent/human_input/form_fields.py +256 -0
  76. fast_agent/human_input/simple_form.py +113 -0
  77. fast_agent/human_input/types.py +40 -0
  78. fast_agent/interfaces.py +310 -0
  79. fast_agent/llm/__init__.py +9 -0
  80. fast_agent/llm/cancellation.py +22 -0
  81. fast_agent/llm/fastagent_llm.py +931 -0
  82. fast_agent/llm/internal/passthrough.py +161 -0
  83. fast_agent/llm/internal/playback.py +129 -0
  84. fast_agent/llm/internal/silent.py +41 -0
  85. fast_agent/llm/internal/slow.py +38 -0
  86. fast_agent/llm/memory.py +275 -0
  87. fast_agent/llm/model_database.py +490 -0
  88. fast_agent/llm/model_factory.py +388 -0
  89. fast_agent/llm/model_info.py +102 -0
  90. fast_agent/llm/prompt_utils.py +155 -0
  91. fast_agent/llm/provider/anthropic/anthropic_utils.py +84 -0
  92. fast_agent/llm/provider/anthropic/cache_planner.py +56 -0
  93. fast_agent/llm/provider/anthropic/llm_anthropic.py +796 -0
  94. fast_agent/llm/provider/anthropic/multipart_converter_anthropic.py +462 -0
  95. fast_agent/llm/provider/bedrock/bedrock_utils.py +218 -0
  96. fast_agent/llm/provider/bedrock/llm_bedrock.py +2207 -0
  97. fast_agent/llm/provider/bedrock/multipart_converter_bedrock.py +84 -0
  98. fast_agent/llm/provider/google/google_converter.py +466 -0
  99. fast_agent/llm/provider/google/llm_google_native.py +681 -0
  100. fast_agent/llm/provider/openai/llm_aliyun.py +31 -0
  101. fast_agent/llm/provider/openai/llm_azure.py +143 -0
  102. fast_agent/llm/provider/openai/llm_deepseek.py +76 -0
  103. fast_agent/llm/provider/openai/llm_generic.py +35 -0
  104. fast_agent/llm/provider/openai/llm_google_oai.py +32 -0
  105. fast_agent/llm/provider/openai/llm_groq.py +42 -0
  106. fast_agent/llm/provider/openai/llm_huggingface.py +85 -0
  107. fast_agent/llm/provider/openai/llm_openai.py +1195 -0
  108. fast_agent/llm/provider/openai/llm_openai_compatible.py +138 -0
  109. fast_agent/llm/provider/openai/llm_openrouter.py +45 -0
  110. fast_agent/llm/provider/openai/llm_tensorzero_openai.py +128 -0
  111. fast_agent/llm/provider/openai/llm_xai.py +38 -0
  112. fast_agent/llm/provider/openai/multipart_converter_openai.py +561 -0
  113. fast_agent/llm/provider/openai/openai_multipart.py +169 -0
  114. fast_agent/llm/provider/openai/openai_utils.py +67 -0
  115. fast_agent/llm/provider/openai/responses.py +133 -0
  116. fast_agent/llm/provider_key_manager.py +139 -0
  117. fast_agent/llm/provider_types.py +34 -0
  118. fast_agent/llm/request_params.py +61 -0
  119. fast_agent/llm/sampling_converter.py +98 -0
  120. fast_agent/llm/stream_types.py +9 -0
  121. fast_agent/llm/usage_tracking.py +445 -0
  122. fast_agent/mcp/__init__.py +56 -0
  123. fast_agent/mcp/common.py +26 -0
  124. fast_agent/mcp/elicitation_factory.py +84 -0
  125. fast_agent/mcp/elicitation_handlers.py +164 -0
  126. fast_agent/mcp/gen_client.py +83 -0
  127. fast_agent/mcp/helpers/__init__.py +36 -0
  128. fast_agent/mcp/helpers/content_helpers.py +352 -0
  129. fast_agent/mcp/helpers/server_config_helpers.py +25 -0
  130. fast_agent/mcp/hf_auth.py +147 -0
  131. fast_agent/mcp/interfaces.py +92 -0
  132. fast_agent/mcp/logger_textio.py +108 -0
  133. fast_agent/mcp/mcp_agent_client_session.py +411 -0
  134. fast_agent/mcp/mcp_aggregator.py +2175 -0
  135. fast_agent/mcp/mcp_connection_manager.py +723 -0
  136. fast_agent/mcp/mcp_content.py +262 -0
  137. fast_agent/mcp/mime_utils.py +108 -0
  138. fast_agent/mcp/oauth_client.py +509 -0
  139. fast_agent/mcp/prompt.py +159 -0
  140. fast_agent/mcp/prompt_message_extended.py +155 -0
  141. fast_agent/mcp/prompt_render.py +84 -0
  142. fast_agent/mcp/prompt_serialization.py +580 -0
  143. fast_agent/mcp/prompts/__init__.py +0 -0
  144. fast_agent/mcp/prompts/__main__.py +7 -0
  145. fast_agent/mcp/prompts/prompt_constants.py +18 -0
  146. fast_agent/mcp/prompts/prompt_helpers.py +238 -0
  147. fast_agent/mcp/prompts/prompt_load.py +186 -0
  148. fast_agent/mcp/prompts/prompt_server.py +552 -0
  149. fast_agent/mcp/prompts/prompt_template.py +438 -0
  150. fast_agent/mcp/resource_utils.py +215 -0
  151. fast_agent/mcp/sampling.py +200 -0
  152. fast_agent/mcp/server/__init__.py +4 -0
  153. fast_agent/mcp/server/agent_server.py +613 -0
  154. fast_agent/mcp/skybridge.py +44 -0
  155. fast_agent/mcp/sse_tracking.py +287 -0
  156. fast_agent/mcp/stdio_tracking_simple.py +59 -0
  157. fast_agent/mcp/streamable_http_tracking.py +309 -0
  158. fast_agent/mcp/tool_execution_handler.py +137 -0
  159. fast_agent/mcp/tool_permission_handler.py +88 -0
  160. fast_agent/mcp/transport_tracking.py +634 -0
  161. fast_agent/mcp/types.py +24 -0
  162. fast_agent/mcp/ui_agent.py +48 -0
  163. fast_agent/mcp/ui_mixin.py +209 -0
  164. fast_agent/mcp_server_registry.py +89 -0
  165. fast_agent/py.typed +0 -0
  166. fast_agent/resources/examples/data-analysis/analysis-campaign.py +189 -0
  167. fast_agent/resources/examples/data-analysis/analysis.py +68 -0
  168. fast_agent/resources/examples/data-analysis/fastagent.config.yaml +41 -0
  169. fast_agent/resources/examples/data-analysis/mount-point/WA_Fn-UseC_-HR-Employee-Attrition.csv +1471 -0
  170. fast_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
  171. fast_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +297 -0
  172. fast_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
  173. fast_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
  174. fast_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
  175. fast_agent/resources/examples/mcp/elicitations/forms_demo.py +107 -0
  176. fast_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
  177. fast_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
  178. fast_agent/resources/examples/mcp/elicitations/tool_call.py +21 -0
  179. fast_agent/resources/examples/mcp/state-transfer/agent_one.py +18 -0
  180. fast_agent/resources/examples/mcp/state-transfer/agent_two.py +18 -0
  181. fast_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +27 -0
  182. fast_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +15 -0
  183. fast_agent/resources/examples/researcher/fastagent.config.yaml +61 -0
  184. fast_agent/resources/examples/researcher/researcher-eval.py +53 -0
  185. fast_agent/resources/examples/researcher/researcher-imp.py +189 -0
  186. fast_agent/resources/examples/researcher/researcher.py +36 -0
  187. fast_agent/resources/examples/tensorzero/.env.sample +2 -0
  188. fast_agent/resources/examples/tensorzero/Makefile +31 -0
  189. fast_agent/resources/examples/tensorzero/README.md +56 -0
  190. fast_agent/resources/examples/tensorzero/agent.py +35 -0
  191. fast_agent/resources/examples/tensorzero/demo_images/clam.jpg +0 -0
  192. fast_agent/resources/examples/tensorzero/demo_images/crab.png +0 -0
  193. fast_agent/resources/examples/tensorzero/demo_images/shrimp.png +0 -0
  194. fast_agent/resources/examples/tensorzero/docker-compose.yml +105 -0
  195. fast_agent/resources/examples/tensorzero/fastagent.config.yaml +19 -0
  196. fast_agent/resources/examples/tensorzero/image_demo.py +67 -0
  197. fast_agent/resources/examples/tensorzero/mcp_server/Dockerfile +25 -0
  198. fast_agent/resources/examples/tensorzero/mcp_server/entrypoint.sh +35 -0
  199. fast_agent/resources/examples/tensorzero/mcp_server/mcp_server.py +31 -0
  200. fast_agent/resources/examples/tensorzero/mcp_server/pyproject.toml +11 -0
  201. fast_agent/resources/examples/tensorzero/simple_agent.py +25 -0
  202. fast_agent/resources/examples/tensorzero/tensorzero_config/system_schema.json +29 -0
  203. fast_agent/resources/examples/tensorzero/tensorzero_config/system_template.minijinja +11 -0
  204. fast_agent/resources/examples/tensorzero/tensorzero_config/tensorzero.toml +35 -0
  205. fast_agent/resources/examples/workflows/agents_as_tools_extended.py +73 -0
  206. fast_agent/resources/examples/workflows/agents_as_tools_simple.py +50 -0
  207. fast_agent/resources/examples/workflows/chaining.py +37 -0
  208. fast_agent/resources/examples/workflows/evaluator.py +77 -0
  209. fast_agent/resources/examples/workflows/fastagent.config.yaml +26 -0
  210. fast_agent/resources/examples/workflows/graded_report.md +89 -0
  211. fast_agent/resources/examples/workflows/human_input.py +28 -0
  212. fast_agent/resources/examples/workflows/maker.py +156 -0
  213. fast_agent/resources/examples/workflows/orchestrator.py +70 -0
  214. fast_agent/resources/examples/workflows/parallel.py +56 -0
  215. fast_agent/resources/examples/workflows/router.py +69 -0
  216. fast_agent/resources/examples/workflows/short_story.md +13 -0
  217. fast_agent/resources/examples/workflows/short_story.txt +19 -0
  218. fast_agent/resources/setup/.gitignore +30 -0
  219. fast_agent/resources/setup/agent.py +28 -0
  220. fast_agent/resources/setup/fastagent.config.yaml +65 -0
  221. fast_agent/resources/setup/fastagent.secrets.yaml.example +38 -0
  222. fast_agent/resources/setup/pyproject.toml.tmpl +23 -0
  223. fast_agent/skills/__init__.py +9 -0
  224. fast_agent/skills/registry.py +235 -0
  225. fast_agent/tools/elicitation.py +369 -0
  226. fast_agent/tools/shell_runtime.py +402 -0
  227. fast_agent/types/__init__.py +59 -0
  228. fast_agent/types/conversation_summary.py +294 -0
  229. fast_agent/types/llm_stop_reason.py +78 -0
  230. fast_agent/types/message_search.py +249 -0
  231. fast_agent/ui/__init__.py +38 -0
  232. fast_agent/ui/console.py +59 -0
  233. fast_agent/ui/console_display.py +1080 -0
  234. fast_agent/ui/elicitation_form.py +946 -0
  235. fast_agent/ui/elicitation_style.py +59 -0
  236. fast_agent/ui/enhanced_prompt.py +1400 -0
  237. fast_agent/ui/history_display.py +734 -0
  238. fast_agent/ui/interactive_prompt.py +1199 -0
  239. fast_agent/ui/markdown_helpers.py +104 -0
  240. fast_agent/ui/markdown_truncator.py +1004 -0
  241. fast_agent/ui/mcp_display.py +857 -0
  242. fast_agent/ui/mcp_ui_utils.py +235 -0
  243. fast_agent/ui/mermaid_utils.py +169 -0
  244. fast_agent/ui/message_primitives.py +50 -0
  245. fast_agent/ui/notification_tracker.py +205 -0
  246. fast_agent/ui/plain_text_truncator.py +68 -0
  247. fast_agent/ui/progress_display.py +10 -0
  248. fast_agent/ui/rich_progress.py +195 -0
  249. fast_agent/ui/streaming.py +774 -0
  250. fast_agent/ui/streaming_buffer.py +449 -0
  251. fast_agent/ui/tool_display.py +422 -0
  252. fast_agent/ui/usage_display.py +204 -0
  253. fast_agent/utils/__init__.py +5 -0
  254. fast_agent/utils/reasoning_stream_parser.py +77 -0
  255. fast_agent/utils/time.py +22 -0
  256. fast_agent/workflow_telemetry.py +261 -0
  257. fast_agent_mcp-0.4.7.dist-info/METADATA +788 -0
  258. fast_agent_mcp-0.4.7.dist-info/RECORD +261 -0
  259. fast_agent_mcp-0.4.7.dist-info/WHEEL +4 -0
  260. fast_agent_mcp-0.4.7.dist-info/entry_points.txt +7 -0
  261. fast_agent_mcp-0.4.7.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,1080 @@
1
+ from contextlib import contextmanager
2
+ from json import JSONDecodeError
3
+ from typing import TYPE_CHECKING, Any, Iterator, Mapping, Union
4
+
5
+ from mcp.types import CallToolResult
6
+ from rich.markdown import Markdown
7
+ from rich.panel import Panel
8
+ from rich.text import Text
9
+
10
+ from fast_agent.config import LoggerSettings, Settings
11
+ from fast_agent.constants import REASONING
12
+ from fast_agent.core.logging.logger import get_logger
13
+ from fast_agent.ui import console
14
+ from fast_agent.ui.markdown_helpers import prepare_markdown_content
15
+ from fast_agent.ui.mcp_ui_utils import UILink
16
+ from fast_agent.ui.mermaid_utils import (
17
+ MermaidDiagram,
18
+ create_mermaid_live_link,
19
+ detect_diagram_type,
20
+ extract_mermaid_diagrams,
21
+ )
22
+ from fast_agent.ui.message_primitives import MESSAGE_CONFIGS, MessageType
23
+ from fast_agent.ui.streaming import (
24
+ NullStreamingHandle as _NullStreamingHandle,
25
+ )
26
+ from fast_agent.ui.streaming import (
27
+ StreamingHandle,
28
+ )
29
+ from fast_agent.ui.streaming import (
30
+ StreamingMessageHandle as _StreamingMessageHandle,
31
+ )
32
+ from fast_agent.ui.tool_display import ToolDisplay
33
+ from fast_agent.utils.time import format_duration
34
+
35
+ if TYPE_CHECKING:
36
+ from fast_agent.mcp.prompt_message_extended import PromptMessageExtended
37
+ from fast_agent.mcp.skybridge import SkybridgeServerConfig
38
+
39
+ logger = get_logger(__name__)
40
+
41
+ CODE_STYLE = "native"
42
+
43
+
44
+ class ConsoleDisplay:
45
+ """
46
+ Handles displaying formatted messages, tool calls, and results to the console.
47
+ This centralizes the UI display logic used by LLM implementations.
48
+ """
49
+
50
+ CODE_STYLE = CODE_STYLE
51
+
52
+ def __init__(self, config: Settings | None = None) -> None:
53
+ """
54
+ Initialize the console display handler.
55
+
56
+ Args:
57
+ config: Configuration object containing display preferences
58
+ """
59
+ self.config = config
60
+ self._logger_settings = self._resolve_logger_settings(config)
61
+ if self.config and not getattr(self.config, "logger", None):
62
+ # Ensure callers passing in a bare namespace still get sane defaults
63
+ try:
64
+ setattr(self.config, "logger", self._logger_settings)
65
+ except Exception:
66
+ pass
67
+ self._markup = getattr(self._logger_settings, "enable_markup", True)
68
+ self._escape_xml = True
69
+ self._tool_display = ToolDisplay(self)
70
+
71
+ @staticmethod
72
+ def _resolve_logger_settings(config: Settings | None) -> LoggerSettings:
73
+ """Provide a logger settings object even when callers omit it."""
74
+ logger_settings = getattr(config, "logger", None) if config else None
75
+ return logger_settings if logger_settings is not None else LoggerSettings()
76
+
77
+ @property
78
+ def code_style(self) -> str:
79
+ return CODE_STYLE
80
+
81
+ def resolve_streaming_preferences(self) -> tuple[bool, str]:
82
+ """Return whether streaming is enabled plus the active mode."""
83
+ if not self.config:
84
+ return True, "markdown"
85
+
86
+ logger_settings = getattr(self.config, "logger", None)
87
+ if not logger_settings:
88
+ return True, "markdown"
89
+
90
+ streaming_mode = getattr(logger_settings, "streaming", "markdown")
91
+ if streaming_mode not in {"markdown", "plain", "none"}:
92
+ streaming_mode = "markdown"
93
+
94
+ # Legacy compatibility: allow streaming_plain_text override
95
+ if streaming_mode == "markdown" and getattr(logger_settings, "streaming_plain_text", False):
96
+ streaming_mode = "plain"
97
+
98
+ show_chat = bool(getattr(logger_settings, "show_chat", True))
99
+ streaming_display = bool(getattr(logger_settings, "streaming_display", True))
100
+
101
+ enabled = show_chat and streaming_display and streaming_mode != "none"
102
+ return enabled, streaming_mode
103
+
104
+ @staticmethod
105
+ def _looks_like_markdown(text: str) -> bool:
106
+ """
107
+ Heuristic to detect markdown-ish content.
108
+
109
+ We keep this lightweight: focus on common structures that benefit from markdown
110
+ rendering without requiring strict syntax validation.
111
+ """
112
+ import re
113
+
114
+ if not text or len(text) < 3:
115
+ return False
116
+
117
+ if "```" in text:
118
+ return True
119
+
120
+ # Simple markers for common cases that the regex might miss
121
+ # Note: single "*" excluded to avoid false positives
122
+ simple_markers = ["##", "**", "---", "###"]
123
+ if any(marker in text for marker in simple_markers):
124
+ return True
125
+
126
+ markdown_patterns = [
127
+ r"^#{1,6}\s+\S", # headings
128
+ r"^\s*[-*+]\s+\S", # unordered list
129
+ r"^\s*\d+\.\s+\S", # ordered list
130
+ r"`[^`]+`", # inline code
131
+ r"\*\*[^*]+\*\*",
132
+ r"__[^_]+__",
133
+ r"^\s*>\s+\S", # blockquote
134
+ r"\[.+?\]\(.+?\)", # links
135
+ r"!\[.*?\]\(.+?\)", # images
136
+ r"^\s*\|.+\|\s*$", # simple tables
137
+ r"^\s*[-*_]{3,}\s*$", # horizontal rules
138
+ ]
139
+
140
+ return any(re.search(pattern, text, re.MULTILINE) for pattern in markdown_patterns)
141
+
142
+ @staticmethod
143
+ def _format_elapsed(elapsed: float) -> str:
144
+ """Format elapsed seconds for display."""
145
+ if elapsed < 0:
146
+ elapsed = 0.0
147
+ if elapsed < 0.001:
148
+ return "<1ms"
149
+ if elapsed < 1:
150
+ return f"{elapsed * 1000:.0f}ms"
151
+ if elapsed < 10:
152
+ return f"{elapsed:.2f}s"
153
+ if elapsed < 60:
154
+ return f"{elapsed:.1f}s"
155
+ return format_duration(elapsed)
156
+
157
+ def display_message(
158
+ self,
159
+ content: Any,
160
+ message_type: MessageType,
161
+ name: str | None = None,
162
+ right_info: str = "",
163
+ bottom_metadata: list[str] | None = None,
164
+ highlight_index: int | None = None,
165
+ max_item_length: int | None = None,
166
+ is_error: bool = False,
167
+ truncate_content: bool = True,
168
+ additional_message: Text | None = None,
169
+ pre_content: Text | None = None,
170
+ ) -> None:
171
+ """
172
+ Unified method to display formatted messages to the console.
173
+
174
+ Args:
175
+ content: The main content to display (str, Text, JSON, etc.)
176
+ message_type: Type of message (USER, ASSISTANT, TOOL_CALL, TOOL_RESULT)
177
+ name: Optional name to display (agent name, user name, etc.)
178
+ right_info: Information to display on the right side of the header
179
+ bottom_metadata: Optional list of items for bottom separator
180
+ highlight_index: Index of item to highlight in bottom metadata (0-based), or None
181
+ max_item_length: Optional max length for bottom metadata items (with ellipsis)
182
+ is_error: For tool results, whether this is an error (uses red color)
183
+ truncate_content: Whether to truncate long content
184
+ additional_message: Optional Rich Text appended after the main content
185
+ pre_content: Optional Rich Text shown before the main content
186
+ """
187
+ # Get configuration for this message type
188
+ config = MESSAGE_CONFIGS[message_type]
189
+
190
+ # Override colors for error states
191
+ if is_error and message_type == MessageType.TOOL_RESULT:
192
+ block_color = "red"
193
+ else:
194
+ block_color = config["block_color"]
195
+
196
+ # Build the left side of the header
197
+ arrow = config["arrow"]
198
+ arrow_style = config["arrow_style"]
199
+ left = f"[{block_color}]▎[/{block_color}][{arrow_style}]{arrow}[/{arrow_style}]"
200
+ if name:
201
+ left += f" [{block_color if not is_error else 'red'}]{name}[/{block_color if not is_error else 'red'}]"
202
+
203
+ # Create combined separator and status line
204
+ self._create_combined_separator_status(left, right_info)
205
+
206
+ # Display the content
207
+ if pre_content and pre_content.plain:
208
+ console.console.print(pre_content, markup=self._markup)
209
+ self._display_content(
210
+ content, truncate_content, is_error, message_type, check_markdown_markers=False
211
+ )
212
+ if additional_message:
213
+ console.console.print(additional_message, markup=self._markup)
214
+
215
+ # Handle bottom separator with optional metadata
216
+ self._render_bottom_metadata(
217
+ message_type=message_type,
218
+ bottom_metadata=bottom_metadata,
219
+ highlight_index=highlight_index,
220
+ max_item_length=max_item_length,
221
+ )
222
+
223
+ def _display_content(
224
+ self,
225
+ content: Any,
226
+ truncate: bool = True,
227
+ is_error: bool = False,
228
+ message_type: MessageType | None = None,
229
+ check_markdown_markers: bool = False,
230
+ ) -> None:
231
+ """
232
+ Display content in the appropriate format.
233
+
234
+ Args:
235
+ content: Content to display
236
+ truncate: Whether to truncate long content
237
+ is_error: Whether this is error content (affects styling)
238
+ message_type: Type of message to determine appropriate styling
239
+ check_markdown_markers: If True, only use markdown rendering when markers are present
240
+ """
241
+ import json
242
+ import re
243
+
244
+ from rich.pretty import Pretty
245
+ from rich.syntax import Syntax
246
+
247
+ from fast_agent.mcp.helpers.content_helpers import get_text, is_text_content
248
+
249
+ # Determine the style based on message type
250
+ # USER, ASSISTANT, and SYSTEM messages should display in normal style
251
+ # TOOL_CALL and TOOL_RESULT should be dimmed
252
+ if is_error:
253
+ style = "dim red"
254
+ elif message_type in [MessageType.USER, MessageType.ASSISTANT, MessageType.SYSTEM]:
255
+ style = None # No style means default/normal white
256
+ else:
257
+ style = "dim"
258
+
259
+ # Handle different content types
260
+ if isinstance(content, str):
261
+ # Try to detect and handle different string formats
262
+ try:
263
+ # Try as JSON first
264
+ json_obj = json.loads(content)
265
+ if truncate and self.config and self.config.logger.truncate_tools:
266
+ pretty_obj = Pretty(json_obj, max_length=10, max_string=50)
267
+ else:
268
+ pretty_obj = Pretty(json_obj)
269
+ # Apply style only if specified
270
+ if style:
271
+ console.console.print(pretty_obj, style=style, markup=self._markup)
272
+ else:
273
+ console.console.print(pretty_obj, markup=self._markup)
274
+ except (JSONDecodeError, TypeError, ValueError):
275
+ # Check if content appears to be primarily XML
276
+ xml_pattern = r"^<[a-zA-Z_][a-zA-Z0-9_-]*[^>]*>"
277
+ is_xml_content = (
278
+ bool(re.match(xml_pattern, content.strip())) and content.count("<") > 5
279
+ )
280
+
281
+ if is_xml_content:
282
+ # Display XML content with syntax highlighting for better readability
283
+ syntax = Syntax(content, "xml", theme=CODE_STYLE, line_numbers=False)
284
+ console.console.print(syntax, markup=self._markup)
285
+ elif check_markdown_markers:
286
+ # Check for markdown markers before deciding to use markdown rendering
287
+ if self._looks_like_markdown(content):
288
+ # Has markdown markers - render as markdown with escaping
289
+ prepared_content = prepare_markdown_content(content, self._escape_xml)
290
+ md = Markdown(prepared_content, code_theme=CODE_STYLE)
291
+ console.console.print(md, markup=self._markup)
292
+ else:
293
+ # Plain text - display as-is
294
+ if (
295
+ truncate
296
+ and self.config
297
+ and self.config.logger.truncate_tools
298
+ and len(content) > 360
299
+ ):
300
+ content = content[:360] + "..."
301
+ if style:
302
+ console.console.print(content, style=style, markup=self._markup)
303
+ else:
304
+ console.console.print(content, markup=self._markup)
305
+ else:
306
+ # Check if content has substantial XML (mixed content)
307
+ # If so, skip markdown rendering as it turns XML into an unreadable blob
308
+ has_substantial_xml = content.count("<") > 5 and content.count(">") > 5
309
+
310
+ # Check if it looks like markdown
311
+ if self._looks_like_markdown(content) and not has_substantial_xml:
312
+ # Escape HTML/XML tags while preserving code blocks
313
+ prepared_content = prepare_markdown_content(content, self._escape_xml)
314
+ md = Markdown(prepared_content, code_theme=CODE_STYLE)
315
+ # Markdown handles its own styling, don't apply style
316
+ console.console.print(md, markup=self._markup)
317
+ else:
318
+ # Plain text (or mixed markdown+XML content)
319
+ if (
320
+ truncate
321
+ and self.config
322
+ and self.config.logger.truncate_tools
323
+ and len(content) > 360
324
+ ):
325
+ content = content[:360] + "..."
326
+ # Apply style only if specified (None means default white)
327
+ if style:
328
+ console.console.print(content, style=style, markup=self._markup)
329
+ else:
330
+ console.console.print(content, markup=self._markup)
331
+ elif isinstance(content, Text):
332
+ # Rich Text object - check if it contains markdown
333
+ plain_text = content.plain
334
+
335
+ # Check if the plain text contains markdown markers
336
+ if self._looks_like_markdown(plain_text):
337
+ # Split the Text object into segments
338
+ # We need to handle the main content (which may have markdown)
339
+ # and any styled segments that were appended
340
+
341
+ # If the Text object has multiple spans with different styles,
342
+ # we need to be careful about how we render them
343
+ if len(content._spans) > 1:
344
+ # Complex case: Text has multiple styled segments
345
+ # We'll render the first part as markdown if it contains markers
346
+ # and append other styled parts separately
347
+
348
+ # Find where the markdown content ends (usually the first span)
349
+ markdown_end = content._spans[0].end if content._spans else len(plain_text)
350
+ markdown_part = plain_text[:markdown_end]
351
+
352
+ # Check if the first part has markdown
353
+ if self._looks_like_markdown(markdown_part):
354
+ # Render markdown part
355
+ prepared_content = prepare_markdown_content(markdown_part, self._escape_xml)
356
+ md = Markdown(prepared_content, code_theme=CODE_STYLE)
357
+ console.console.print(md, markup=self._markup)
358
+
359
+ # Then render any additional styled segments
360
+ if markdown_end < len(plain_text):
361
+ remaining_text = Text()
362
+ for span in content._spans:
363
+ if span.start >= markdown_end:
364
+ segment_text = plain_text[span.start : span.end]
365
+ remaining_text.append(segment_text, style=span.style)
366
+ if remaining_text.plain:
367
+ console.console.print(remaining_text, markup=self._markup)
368
+ else:
369
+ # No markdown in first part, just print the whole Text object
370
+ console.console.print(content, markup=self._markup)
371
+ else:
372
+ # Simple case: entire text should be rendered as markdown
373
+ prepared_content = prepare_markdown_content(plain_text, self._escape_xml)
374
+ md = Markdown(prepared_content, code_theme=CODE_STYLE)
375
+ console.console.print(md, markup=self._markup)
376
+ else:
377
+ # No markdown markers, print as regular Rich Text
378
+ console.console.print(content, markup=self._markup)
379
+ elif isinstance(content, list):
380
+ # Handle content blocks (for tool results)
381
+ if len(content) == 1 and is_text_content(content[0]):
382
+ # Single text block - display directly
383
+ text_content = get_text(content[0])
384
+ if text_content:
385
+ if (
386
+ truncate
387
+ and self.config
388
+ and self.config.logger.truncate_tools
389
+ and len(text_content) > 360
390
+ ):
391
+ text_content = text_content[:360] + "..."
392
+ # Apply style only if specified
393
+ if style:
394
+ console.console.print(text_content, style=style, markup=self._markup)
395
+ else:
396
+ console.console.print(text_content, markup=self._markup)
397
+ else:
398
+ # Apply style only if specified
399
+ if style:
400
+ console.console.print("(empty text)", style=style, markup=self._markup)
401
+ else:
402
+ console.console.print("(empty text)", markup=self._markup)
403
+ else:
404
+ # Multiple blocks or non-text content
405
+ if truncate and self.config and self.config.logger.truncate_tools:
406
+ pretty_obj = Pretty(content, max_length=10, max_string=50)
407
+ else:
408
+ pretty_obj = Pretty(content)
409
+ # Apply style only if specified
410
+ if style:
411
+ console.console.print(pretty_obj, style=style, markup=self._markup)
412
+ else:
413
+ console.console.print(pretty_obj, markup=self._markup)
414
+ else:
415
+ # Any other type - use Pretty
416
+ if truncate and self.config and self.config.logger.truncate_tools:
417
+ pretty_obj = Pretty(content, max_length=10, max_string=50)
418
+ else:
419
+ pretty_obj = Pretty(content)
420
+ # Apply style only if specified
421
+ if style:
422
+ console.console.print(pretty_obj, style=style, markup=self._markup)
423
+ else:
424
+ console.console.print(pretty_obj, markup=self._markup)
425
+
426
+ def _shorten_items(self, items: list[str], max_length: int) -> list[str]:
427
+ """
428
+ Shorten items to max_length with ellipsis if needed.
429
+
430
+ Args:
431
+ items: List of strings to potentially shorten
432
+ max_length: Maximum length for each item
433
+
434
+ Returns:
435
+ List of shortened strings
436
+ """
437
+ return [item[: max_length - 1] + "…" if len(item) > max_length else item for item in items]
438
+
439
+ def _render_bottom_metadata(
440
+ self,
441
+ *,
442
+ message_type: MessageType,
443
+ bottom_metadata: list[str] | None,
444
+ highlight_index: int | None,
445
+ max_item_length: int | None,
446
+ ) -> None:
447
+ """
448
+ Render the bottom separator line with optional metadata.
449
+
450
+ Args:
451
+ message_type: The type of message being displayed
452
+ bottom_metadata: Optional list of items to show in the separator
453
+ highlight_index: Optional index of the item to highlight
454
+ max_item_length: Optional maximum length for individual items
455
+ """
456
+ console.console.print()
457
+
458
+ if bottom_metadata:
459
+ display_items = bottom_metadata
460
+ if max_item_length:
461
+ display_items = self._shorten_items(bottom_metadata, max_item_length)
462
+
463
+ total_width = console.console.size.width
464
+ prefix = Text("─| ")
465
+ prefix.stylize("dim")
466
+ suffix = Text(" |")
467
+ suffix.stylize("dim")
468
+ available = max(0, total_width - prefix.cell_len - suffix.cell_len)
469
+
470
+ highlight_color = MESSAGE_CONFIGS[message_type]["highlight_color"]
471
+ metadata_text = self._format_bottom_metadata(
472
+ display_items,
473
+ highlight_index,
474
+ highlight_color,
475
+ max_width=available,
476
+ )
477
+
478
+ line = Text()
479
+ line.append_text(prefix)
480
+ line.append_text(metadata_text)
481
+ line.append_text(suffix)
482
+ remaining = total_width - line.cell_len
483
+ if remaining > 0:
484
+ line.append("─" * remaining, style="dim")
485
+ console.console.print(line, markup=self._markup)
486
+ else:
487
+ console.console.print("─" * console.console.size.width, style="dim")
488
+
489
+ console.console.print()
490
+
491
+ def _format_bottom_metadata(
492
+ self,
493
+ items: list[str],
494
+ highlight_index: int | None,
495
+ highlight_color: str,
496
+ max_width: int | None = None,
497
+ ) -> Text:
498
+ """
499
+ Format a list of items with pipe separators and highlighting.
500
+
501
+ Args:
502
+ items: List of items to display
503
+ highlight_index: Index of item to highlight (0-based), or None for no highlighting
504
+ highlight_color: Color to use for highlighting
505
+ max_width: Maximum width for the formatted text
506
+
507
+ Returns:
508
+ Formatted Text object with proper separators and highlighting
509
+ """
510
+ formatted = Text()
511
+
512
+ def will_fit(next_segment: Text) -> bool:
513
+ if max_width is None:
514
+ return True
515
+ # projected length if we append next_segment
516
+ return formatted.cell_len + next_segment.cell_len <= max_width
517
+
518
+ for i, item in enumerate(items):
519
+ sep = Text(" | ", style="dim") if i > 0 else Text("")
520
+
521
+ # Prepare item text with potential highlighting
522
+ should_highlight = highlight_index is not None and i == highlight_index
523
+
524
+ item_text = Text(item, style=(highlight_color if should_highlight else "dim"))
525
+
526
+ # Check if separator + item fits in available width
527
+ if not will_fit(sep + item_text):
528
+ # If nothing has been added yet and the item itself is too long,
529
+ # leave space for an ellipsis and stop.
530
+ if formatted.cell_len == 0 and max_width is not None and max_width > 1:
531
+ # show truncated indicator only
532
+ formatted.append("…", style="dim")
533
+ else:
534
+ # Indicate there are more items but avoid wrapping
535
+ if max_width is None or formatted.cell_len < max_width:
536
+ formatted.append(" …", style="dim")
537
+ break
538
+
539
+ # Append separator and item
540
+ if sep.plain:
541
+ formatted.append_text(sep)
542
+ formatted.append_text(item_text)
543
+
544
+ return formatted
545
+
546
+ def show_tool_result(
547
+ self,
548
+ result: CallToolResult,
549
+ name: str | None = None,
550
+ tool_name: str | None = None,
551
+ skybridge_config: "SkybridgeServerConfig | None" = None,
552
+ timing_ms: float | None = None,
553
+ type_label: str | None = None,
554
+ ) -> None:
555
+ kwargs: dict[str, Any] = {
556
+ "name": name,
557
+ "tool_name": tool_name,
558
+ "skybridge_config": skybridge_config,
559
+ "timing_ms": timing_ms,
560
+ }
561
+ if type_label is not None:
562
+ kwargs["type_label"] = type_label
563
+
564
+ self._tool_display.show_tool_result(result, **kwargs)
565
+
566
+ def show_tool_call(
567
+ self,
568
+ tool_name: str,
569
+ tool_args: dict[str, Any] | None,
570
+ bottom_items: list[str] | None = None,
571
+ highlight_index: int | None = None,
572
+ max_item_length: int | None = None,
573
+ name: str | None = None,
574
+ metadata: dict[str, Any] | None = None,
575
+ type_label: str | None = None,
576
+ ) -> None:
577
+ kwargs: dict[str, Any] = {
578
+ "bottom_items": bottom_items,
579
+ "highlight_index": highlight_index,
580
+ "max_item_length": max_item_length,
581
+ "name": name,
582
+ "metadata": metadata,
583
+ }
584
+ if type_label is not None:
585
+ kwargs["type_label"] = type_label
586
+
587
+ self._tool_display.show_tool_call(tool_name, tool_args, **kwargs)
588
+
589
+ async def show_tool_update(self, updated_server: str, agent_name: str | None = None) -> None:
590
+ await self._tool_display.show_tool_update(updated_server, agent_name=agent_name)
591
+
592
+ def _create_combined_separator_status(self, left_content: str, right_info: str = "") -> None:
593
+ """
594
+ Create a combined separator and status line.
595
+
596
+ Args:
597
+ left_content: The main content (block, arrow, name) - left justified with color
598
+ right_info: Supplementary information to show in brackets - right aligned
599
+ """
600
+ width = console.console.size.width
601
+
602
+ # Create left text
603
+ left_text = Text.from_markup(left_content)
604
+
605
+ # Create right text if we have info
606
+ if right_info and right_info.strip():
607
+ # Add dim brackets around the right info
608
+ right_text = Text()
609
+ right_text.append("[", style="dim")
610
+ right_text.append_text(Text.from_markup(right_info))
611
+ right_text.append("]", style="dim")
612
+ # Calculate separator count
613
+ separator_count = width - left_text.cell_len - right_text.cell_len
614
+ if separator_count < 1:
615
+ separator_count = 1 # Always at least 1 separator
616
+ else:
617
+ right_text = Text("")
618
+ separator_count = width - left_text.cell_len
619
+
620
+ # Build the combined line
621
+ combined = Text()
622
+ combined.append_text(left_text)
623
+ combined.append(" ", style="default")
624
+ combined.append("─" * (separator_count - 1), style="dim")
625
+ combined.append_text(right_text)
626
+
627
+ # Print with empty line before
628
+ console.console.print()
629
+ console.console.print(combined, markup=self._markup)
630
+ console.console.print()
631
+
632
+ @staticmethod
633
+ def summarize_skybridge_configs(
634
+ configs: Mapping[str, "SkybridgeServerConfig"] | None,
635
+ ) -> tuple[list[dict[str, Any]], list[str]]:
636
+ return ToolDisplay.summarize_skybridge_configs(configs)
637
+
638
+ def show_skybridge_summary(
639
+ self,
640
+ agent_name: str,
641
+ configs: Mapping[str, "SkybridgeServerConfig"] | None,
642
+ ) -> None:
643
+ self._tool_display.show_skybridge_summary(agent_name, configs)
644
+
645
+ def _extract_reasoning_content(self, message: "PromptMessageExtended") -> Text | None:
646
+ """Extract reasoning channel content as dim text."""
647
+ channels = message.channels or {}
648
+ reasoning_blocks = channels.get(REASONING) or []
649
+ if not reasoning_blocks:
650
+ return None
651
+
652
+ from fast_agent.mcp.helpers.content_helpers import get_text
653
+
654
+ reasoning_segments = []
655
+ for block in reasoning_blocks:
656
+ text = get_text(block)
657
+ if text:
658
+ reasoning_segments.append(text)
659
+
660
+ if not reasoning_segments:
661
+ return None
662
+
663
+ joined = "\n".join(reasoning_segments)
664
+ if not joined.strip():
665
+ return None
666
+
667
+ # Render reasoning in dim italic and leave a blank line before main content
668
+ text = joined
669
+ if not text.endswith("\n"):
670
+ text += "\n"
671
+ text += "\n"
672
+ return Text(text, style="dim italic")
673
+
674
+ async def show_assistant_message(
675
+ self,
676
+ message_text: Union[str, Text, "PromptMessageExtended"],
677
+ bottom_items: list[str] | None = None,
678
+ highlight_index: int | None = None,
679
+ max_item_length: int | None = None,
680
+ name: str | None = None,
681
+ model: str | None = None,
682
+ additional_message: Text | None = None,
683
+ ) -> None:
684
+ """Display an assistant message in a formatted panel.
685
+
686
+ Args:
687
+ message_text: The message content to display (str, Text, or PromptMessageExtended)
688
+ bottom_items: Optional list of items for bottom separator (e.g., servers, destinations)
689
+ highlight_index: Index of item to highlight in the bottom separator (0-based), or None
690
+ max_item_length: Optional max length for bottom items (with ellipsis)
691
+ title: Title for the message (default "ASSISTANT")
692
+ name: Optional agent name
693
+ model: Optional model name for right info
694
+ additional_message: Optional additional styled message to append
695
+ """
696
+ if self.config and not self.config.logger.show_chat:
697
+ return
698
+
699
+ # Extract text from PromptMessageExtended if needed
700
+ from fast_agent.types import PromptMessageExtended
701
+
702
+ pre_content: Text | None = None
703
+
704
+ if isinstance(message_text, PromptMessageExtended):
705
+ display_text = message_text.last_text() or ""
706
+ pre_content = self._extract_reasoning_content(message_text)
707
+ else:
708
+ display_text = message_text
709
+
710
+ # Build right info
711
+ right_info = f"[dim]{model}[/dim]" if model else ""
712
+
713
+ # Display main message using unified method
714
+ self.display_message(
715
+ content=display_text,
716
+ message_type=MessageType.ASSISTANT,
717
+ name=name,
718
+ right_info=right_info,
719
+ bottom_metadata=bottom_items,
720
+ highlight_index=highlight_index,
721
+ max_item_length=max_item_length,
722
+ truncate_content=False, # Assistant messages shouldn't be truncated
723
+ additional_message=additional_message,
724
+ pre_content=pre_content,
725
+ )
726
+
727
+ # Handle mermaid diagrams separately (after the main message)
728
+ # Extract plain text for mermaid detection
729
+ plain_text = display_text
730
+ if isinstance(display_text, Text):
731
+ plain_text = display_text.plain
732
+
733
+ if isinstance(plain_text, str):
734
+ diagrams = extract_mermaid_diagrams(plain_text)
735
+ if diagrams:
736
+ self._display_mermaid_diagrams(diagrams)
737
+
738
+ @contextmanager
739
+ def streaming_assistant_message(
740
+ self,
741
+ *,
742
+ bottom_items: list[str] | None = None,
743
+ highlight_index: int | None = None,
744
+ max_item_length: int | None = None,
745
+ name: str | None = None,
746
+ model: str | None = None,
747
+ ) -> Iterator[StreamingHandle]:
748
+ """Create a streaming context for assistant messages."""
749
+ streaming_enabled, streaming_mode = self.resolve_streaming_preferences()
750
+
751
+ if not streaming_enabled:
752
+ yield _NullStreamingHandle()
753
+ return
754
+
755
+ from fast_agent.ui.progress_display import progress_display
756
+
757
+ config = MESSAGE_CONFIGS[MessageType.ASSISTANT]
758
+ block_color = config["block_color"]
759
+ arrow = config["arrow"]
760
+ arrow_style = config["arrow_style"]
761
+
762
+ left = f"[{block_color}]▎[/{block_color}][{arrow_style}]{arrow}[/{arrow_style}] "
763
+ if name:
764
+ left += f"[{block_color}]{name}[/{block_color}]"
765
+
766
+ right_info = f"[dim]{model}[/dim]" if model else ""
767
+
768
+ # Determine renderer based on streaming mode
769
+ use_plain_text = streaming_mode == "plain"
770
+
771
+ handle = _StreamingMessageHandle(
772
+ display=self,
773
+ bottom_items=bottom_items,
774
+ highlight_index=highlight_index,
775
+ max_item_length=max_item_length,
776
+ use_plain_text=use_plain_text,
777
+ header_left=left,
778
+ header_right=right_info,
779
+ progress_display=progress_display,
780
+ )
781
+ try:
782
+ yield handle
783
+ finally:
784
+ handle.close()
785
+
786
+ def _display_mermaid_diagrams(self, diagrams: list[MermaidDiagram]) -> None:
787
+ """Display mermaid diagram links."""
788
+ diagram_content = Text()
789
+ # Add bullet at the beginning
790
+ diagram_content.append("● ", style="dim")
791
+
792
+ for i, diagram in enumerate(diagrams, 1):
793
+ if i > 1:
794
+ diagram_content.append(" • ", style="dim")
795
+
796
+ # Generate URL
797
+ url = create_mermaid_live_link(diagram.content)
798
+
799
+ # Format: "1 - Title" or "1 - Flowchart" or "Diagram 1"
800
+ if diagram.title:
801
+ diagram_content.append(f"{i} - {diagram.title}", style=f"bright_blue link {url}")
802
+ else:
803
+ # Try to detect diagram type, fallback to "Diagram N"
804
+ diagram_type = detect_diagram_type(diagram.content)
805
+ if diagram_type != "Diagram":
806
+ diagram_content.append(f"{i} - {diagram_type}", style=f"bright_blue link {url}")
807
+ else:
808
+ diagram_content.append(f"Diagram {i}", style=f"bright_blue link {url}")
809
+
810
+ # Display diagrams on a simple new line (more space efficient)
811
+ console.console.print()
812
+ console.console.print(diagram_content, markup=self._markup)
813
+
814
+ async def show_mcp_ui_links(self, links: list[UILink]) -> None:
815
+ """Display MCP-UI links beneath the chat like mermaid links."""
816
+ if self.config and not self.config.logger.show_chat:
817
+ return
818
+
819
+ if not links:
820
+ return
821
+
822
+ content = Text()
823
+ content.append("● mcp-ui ", style="dim")
824
+ for i, link in enumerate(links, 1):
825
+ if i > 1:
826
+ content.append(" • ", style="dim")
827
+ # Prefer a web-friendly URL (http(s) or data:) if available; fallback to local file
828
+ url = link.web_url if getattr(link, "web_url", None) else f"file://{link.file_path}"
829
+ label = f"{i} - {link.title}"
830
+ content.append(label, style=f"bright_blue link {url}")
831
+
832
+ console.console.print()
833
+ console.console.print(content, markup=self._markup)
834
+
835
+ def show_user_message(
836
+ self,
837
+ message: Union[str, Text],
838
+ model: str | None = None,
839
+ chat_turn: int = 0,
840
+ name: str | None = None,
841
+ attachments: list[str] | None = None,
842
+ ) -> None:
843
+ """Display a user message in the new visual style."""
844
+ if self.config and not self.config.logger.show_chat:
845
+ return
846
+
847
+ # Build right side with model and turn
848
+ right_parts = []
849
+ if model:
850
+ right_parts.append(model)
851
+ if chat_turn > 0:
852
+ right_parts.append(f"turn {chat_turn}")
853
+
854
+ right_info = f"[dim]{' '.join(right_parts)}[/dim]" if right_parts else ""
855
+
856
+ # Build attachment indicator as pre_content
857
+ pre_content: Text | None = None
858
+ if attachments:
859
+ pre_content = Text()
860
+ pre_content.append("🔗 ", style="dim")
861
+ pre_content.append(", ".join(attachments), style="dim blue")
862
+
863
+ self.display_message(
864
+ content=message,
865
+ message_type=MessageType.USER,
866
+ name=name,
867
+ right_info=right_info,
868
+ truncate_content=False, # User messages typically shouldn't be truncated
869
+ pre_content=pre_content,
870
+ )
871
+
872
+ def show_system_message(
873
+ self,
874
+ system_prompt: str,
875
+ agent_name: str | None = None,
876
+ server_count: int = 0,
877
+ ) -> None:
878
+ """Display the system prompt in a formatted panel."""
879
+ if self.config and not self.config.logger.show_chat:
880
+ return
881
+
882
+ # Build right side info
883
+ right_parts = []
884
+ if server_count > 0:
885
+ server_word = "server" if server_count == 1 else "servers"
886
+ right_parts.append(f"{server_count} MCP {server_word}")
887
+
888
+ right_info = f"[dim]{' '.join(right_parts)}[/dim]" if right_parts else ""
889
+
890
+ self.display_message(
891
+ content=system_prompt,
892
+ message_type=MessageType.SYSTEM,
893
+ name=agent_name,
894
+ right_info=right_info,
895
+ truncate_content=False, # Don't truncate system prompts
896
+ )
897
+
898
+ async def show_prompt_loaded(
899
+ self,
900
+ prompt_name: str,
901
+ description: str | None = None,
902
+ message_count: int = 0,
903
+ agent_name: str | None = None,
904
+ server_list: list[str] | None = None,
905
+ highlight_server: str | None = None,
906
+ arguments: dict[str, str] | None = None,
907
+ ) -> None:
908
+ """
909
+ Display information about a loaded prompt template.
910
+
911
+ Args:
912
+ prompt_name: The name of the prompt that was loaded
913
+ description: Optional description of the prompt
914
+ message_count: Number of messages added to the conversation history
915
+ agent_name: Name of the agent using the prompt
916
+ server_list: Optional list of servers to display
917
+ highlight_server: Optional server name to highlight
918
+ arguments: Optional dictionary of arguments passed to the prompt template
919
+ """
920
+ if self.config and not self.config.logger.show_tools:
921
+ return
922
+
923
+ # Build the server list with highlighting
924
+ display_server_list = Text()
925
+ if server_list:
926
+ for server_name in server_list:
927
+ style = "green" if server_name == highlight_server else "dim white"
928
+ display_server_list.append(f"[{server_name}] ", style)
929
+
930
+ # Create content text
931
+ content = Text()
932
+ messages_phrase = f"Loaded {message_count} message{'s' if message_count != 1 else ''}"
933
+ content.append(f"{messages_phrase} from template ", style="cyan italic")
934
+ content.append(f"'{prompt_name}'", style="cyan bold italic")
935
+
936
+ if agent_name:
937
+ content.append(f" for {agent_name}", style="cyan italic")
938
+
939
+ # Add template arguments if provided
940
+ if arguments:
941
+ content.append("\n\nArguments:", style="cyan")
942
+ for key, value in arguments.items():
943
+ content.append(f"\n {key}: ", style="cyan bold")
944
+ content.append(value, style="white")
945
+
946
+ if description:
947
+ content.append("\n\n", style="default")
948
+ content.append(description, style="dim white")
949
+
950
+ # Create panel
951
+ panel = Panel(
952
+ content,
953
+ title="[PROMPT LOADED]",
954
+ title_align="right",
955
+ style="cyan",
956
+ border_style="white",
957
+ padding=(1, 2),
958
+ subtitle=display_server_list,
959
+ subtitle_align="left",
960
+ )
961
+
962
+ console.console.print(panel, markup=self._markup)
963
+ console.console.print("\n")
964
+
965
+ def show_parallel_results(self, parallel_agent) -> None:
966
+ """Display parallel agent results in a clean, organized format.
967
+
968
+ Args:
969
+ parallel_agent: The parallel agent containing fan_out_agents with results
970
+ """
971
+
972
+ from rich.text import Text
973
+
974
+ if self.config and not self.config.logger.show_chat:
975
+ return
976
+
977
+ if not parallel_agent or not hasattr(parallel_agent, "fan_out_agents"):
978
+ return
979
+
980
+ # Collect results and agent information
981
+ agent_results = []
982
+
983
+ for agent in parallel_agent.fan_out_agents:
984
+ # Get the last response text from this agent
985
+ message_history = agent.message_history
986
+ if not message_history:
987
+ continue
988
+
989
+ last_message = message_history[-1]
990
+ content = last_message.last_text()
991
+
992
+ # Get model name
993
+ model = "unknown"
994
+ if agent.llm:
995
+ model = agent.llm.model_name or "unknown"
996
+
997
+ # Get usage information
998
+ tokens = 0
999
+ tool_calls = 0
1000
+ if hasattr(agent, "usage_accumulator") and agent.usage_accumulator:
1001
+ summary = agent.usage_accumulator.get_summary()
1002
+ tokens = summary.get("cumulative_input_tokens", 0) + summary.get(
1003
+ "cumulative_output_tokens", 0
1004
+ )
1005
+ tool_calls = summary.get("cumulative_tool_calls", 0)
1006
+
1007
+ agent_results.append(
1008
+ {
1009
+ "name": agent.name,
1010
+ "model": model,
1011
+ "content": content,
1012
+ "tokens": tokens,
1013
+ "tool_calls": tool_calls,
1014
+ }
1015
+ )
1016
+
1017
+ if not agent_results:
1018
+ return
1019
+
1020
+ # Display header
1021
+ console.console.print()
1022
+ console.console.print("[dim]Parallel execution complete[/dim]")
1023
+ console.console.print()
1024
+
1025
+ # Display results for each agent
1026
+ for i, result in enumerate(agent_results):
1027
+ if i > 0:
1028
+ # Simple full-width separator
1029
+ console.console.print()
1030
+ console.console.print("─" * console.console.size.width, style="dim")
1031
+ console.console.print()
1032
+
1033
+ # Two column header: model name (green) + usage info (dim)
1034
+ left = f"[green]▎[/green] [bold green]{result['model']}[/bold green]"
1035
+
1036
+ # Build right side with tokens and tool calls if available
1037
+ right_parts = []
1038
+ if result["tokens"] > 0:
1039
+ right_parts.append(f"{result['tokens']:,} tokens")
1040
+ if result["tool_calls"] > 0:
1041
+ right_parts.append(f"{result['tool_calls']} tools")
1042
+
1043
+ right = f"[dim]{' • '.join(right_parts) if right_parts else 'no usage data'}[/dim]"
1044
+
1045
+ # Calculate padding to right-align usage info
1046
+ width = console.console.size.width
1047
+ left_text = Text.from_markup(left)
1048
+ right_text = Text.from_markup(right)
1049
+ padding = max(1, width - left_text.cell_len - right_text.cell_len)
1050
+
1051
+ console.console.print(left + " " * padding + right, markup=self._markup)
1052
+ console.console.print()
1053
+
1054
+ # Display content based on its type (check for markdown markers in parallel results)
1055
+ content = result["content"]
1056
+ # Use _display_content with assistant message type so content isn't dimmed
1057
+ self._display_content(
1058
+ content,
1059
+ truncate=False,
1060
+ is_error=False,
1061
+ message_type=MessageType.ASSISTANT,
1062
+ check_markdown_markers=True,
1063
+ )
1064
+
1065
+ # Summary
1066
+ console.console.print()
1067
+ console.console.print("─" * console.console.size.width, style="dim")
1068
+
1069
+ total_tokens = sum(result["tokens"] for result in agent_results)
1070
+ total_tools = sum(result["tool_calls"] for result in agent_results)
1071
+
1072
+ summary_parts = [f"{len(agent_results)} models"]
1073
+ if total_tokens > 0:
1074
+ summary_parts.append(f"{total_tokens:,} tokens")
1075
+ if total_tools > 0:
1076
+ summary_parts.append(f"{total_tools} tools")
1077
+
1078
+ summary_text = " • ".join(summary_parts)
1079
+ console.console.print(f"[dim]{summary_text}[/dim]")
1080
+ console.console.print()