fast-agent-mcp 0.3.11__py3-none-any.whl → 0.3.13__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.

@@ -129,6 +129,7 @@ class FastAgentLLM(ContextDependent, FastAgentLLMProtocol, Generic[MessageParamT
129
129
  self.history: Memory[MessageParamT] = SimpleMemory[MessageParamT]()
130
130
 
131
131
  self._message_history: List[PromptMessageExtended] = []
132
+ self._template_messages: List[PromptMessageExtended] = []
132
133
 
133
134
  # Initialize the display component
134
135
  from fast_agent.ui.console_display import ConsoleDisplay
@@ -575,11 +576,15 @@ class FastAgentLLM(ContextDependent, FastAgentLLMProtocol, Generic[MessageParamT
575
576
 
576
577
  # Convert to PromptMessageExtended objects
577
578
  multipart_messages = PromptMessageExtended.parse_get_prompt_result(prompt_result)
579
+ # Store a local copy of template messages so we can retain them across clears
580
+ self._template_messages = [msg.model_copy(deep=True) for msg in multipart_messages]
578
581
 
579
582
  # Delegate to the provider-specific implementation
580
583
  result = await self._apply_prompt_provider_specific(
581
584
  multipart_messages, None, is_template=True
582
585
  )
586
+ # Ensure message history always includes the stored template when applied
587
+ self._message_history = [msg.model_copy(deep=True) for msg in self._template_messages]
583
588
  return result.first_text()
584
589
 
585
590
  async def _save_history(self, filename: str) -> None:
@@ -607,6 +612,18 @@ class FastAgentLLM(ContextDependent, FastAgentLLMProtocol, Generic[MessageParamT
607
612
  """
608
613
  return self._message_history
609
614
 
615
+ def clear(self, *, clear_prompts: bool = False) -> None:
616
+ """Reset stored message history while optionally retaining prompt templates."""
617
+
618
+ self.history.clear(clear_prompts=clear_prompts)
619
+ if clear_prompts:
620
+ self._template_messages = []
621
+ self._message_history = []
622
+ return
623
+
624
+ # Restore message history to template messages only; new turns will append as normal
625
+ self._message_history = [msg.model_copy(deep=True) for msg in self._template_messages]
626
+
610
627
  def _api_key(self):
611
628
  if self._init_api_key:
612
629
  return self._init_api_key
@@ -232,6 +232,8 @@ class ModelDatabase:
232
232
  "claude-3-7-sonnet-latest": ANTHROPIC_37_SERIES,
233
233
  "claude-sonnet-4-0": ANTHROPIC_SONNET_4_VERSIONED,
234
234
  "claude-sonnet-4-20250514": ANTHROPIC_SONNET_4_VERSIONED,
235
+ "claude-sonnet-4-5": ANTHROPIC_SONNET_4_VERSIONED,
236
+ "claude-sonnet-4-5-20250929": ANTHROPIC_SONNET_4_VERSIONED,
235
237
  "claude-opus-4-0": ANTHROPIC_OPUS_4_VERSIONED,
236
238
  "claude-opus-4-1": ANTHROPIC_OPUS_4_VERSIONED,
237
239
  "claude-opus-4-20250514": ANTHROPIC_OPUS_4_VERSIONED,
@@ -84,6 +84,8 @@ class ModelFactory:
84
84
  "claude-opus-4-20250514": Provider.ANTHROPIC,
85
85
  "claude-sonnet-4-20250514": Provider.ANTHROPIC,
86
86
  "claude-sonnet-4-0": Provider.ANTHROPIC,
87
+ "claude-sonnet-4-5-20250929": Provider.ANTHROPIC,
88
+ "claude-sonnet-4-5": Provider.ANTHROPIC,
87
89
  "deepseek-chat": Provider.DEEPSEEK,
88
90
  "gemini-2.0-flash": Provider.GOOGLE,
89
91
  "gemini-2.5-flash-preview-05-20": Provider.GOOGLE,
@@ -101,8 +103,9 @@ class ModelFactory:
101
103
  }
102
104
 
103
105
  MODEL_ALIASES = {
104
- "sonnet": "claude-sonnet-4-0",
106
+ "sonnet": "claude-sonnet-4-5",
105
107
  "sonnet4": "claude-sonnet-4-0",
108
+ "sonnet45": "claude-sonnet-4-5",
106
109
  "sonnet35": "claude-3-5-sonnet-latest",
107
110
  "sonnet37": "claude-3-7-sonnet-latest",
108
111
  "claude": "claude-sonnet-4-0",
@@ -53,6 +53,14 @@ class GoogleConverter:
53
53
  if key in unsupported_keys:
54
54
  continue # Skip this key
55
55
 
56
+ # Rewrite unsupported 'const' to a safe form for Gemini tools
57
+ # - For string const, convert to enum [value]
58
+ # - For non-string const (booleans/numbers), drop the constraint
59
+ if key == "const":
60
+ if isinstance(value, str):
61
+ cleaned_schema["enum"] = [value]
62
+ continue
63
+
56
64
  if (
57
65
  key == "format"
58
66
  and schema.get("type") == "string"
@@ -140,9 +148,8 @@ class GoogleConverter:
140
148
  )
141
149
  elif is_resource_content(part_content):
142
150
  assert isinstance(part_content, EmbeddedResource)
143
- if (
144
- "application/pdf" == part_content.resource.mimeType
145
- and isinstance(part_content.resource, BlobResourceContents)
151
+ if "application/pdf" == part_content.resource.mimeType and isinstance(
152
+ part_content.resource, BlobResourceContents
146
153
  ):
147
154
  pdf_bytes = base64.b64decode(part_content.resource.blob)
148
155
  parts.append(
@@ -4,6 +4,7 @@ It adds logging and supports sampling requests.
4
4
  """
5
5
 
6
6
  from datetime import timedelta
7
+ from time import perf_counter
7
8
  from typing import TYPE_CHECKING
8
9
 
9
10
  from mcp import ClientSession, ServerNotification
@@ -207,6 +208,7 @@ class MCPAgentClientSession(ClientSession, ContextDependent):
207
208
  ) -> ReceiveResultT:
208
209
  logger.debug("send_request: request=", data=request.model_dump())
209
210
  request_id = getattr(self, "_request_id", None)
211
+ start_time = perf_counter()
210
212
  try:
211
213
  result = await super().send_request(
212
214
  request=request,
@@ -220,6 +222,7 @@ class MCPAgentClientSession(ClientSession, ContextDependent):
220
222
  data=result.model_dump() if result is not None else "no response returned",
221
223
  )
222
224
  self._attach_transport_channel(request_id, result)
225
+ self._attach_transport_elapsed(result, perf_counter() - start_time)
223
226
  return result
224
227
  except Exception as e:
225
228
  # Handle connection errors cleanly
@@ -250,6 +253,16 @@ class MCPAgentClientSession(ClientSession, ContextDependent):
250
253
  # If result cannot be mutated, ignore silently
251
254
  pass
252
255
 
256
+ @staticmethod
257
+ def _attach_transport_elapsed(result, elapsed: float | None) -> None:
258
+ if result is None or elapsed is None:
259
+ return
260
+ try:
261
+ setattr(result, "transport_elapsed", max(elapsed, 0.0))
262
+ except Exception:
263
+ # Ignore if result is immutable
264
+ pass
265
+
253
266
  async def _received_notification(self, notification: ServerNotification) -> None:
254
267
  """
255
268
  Can be overridden by subclasses to handle a notification without needing
@@ -38,6 +38,8 @@ from fast_agent.mcp.streamable_http_tracking import tracking_streamablehttp_clie
38
38
  from fast_agent.mcp.transport_tracking import TransportChannelMetrics
39
39
 
40
40
  if TYPE_CHECKING:
41
+ from mcp.client.auth import OAuthClientProvider
42
+
41
43
  from fast_agent.context import Context
42
44
  from fast_agent.mcp_server_registry import ServerRegistry
43
45
 
@@ -65,6 +67,38 @@ def _add_none_to_context(context_manager):
65
67
  return StreamingContextAdapter(context_manager)
66
68
 
67
69
 
70
+ def _prepare_headers_and_auth(
71
+ server_config: MCPServerSettings,
72
+ ) -> tuple[dict[str, str], Optional["OAuthClientProvider"], set[str]]:
73
+ """
74
+ Prepare request headers and determine if OAuth authentication should be used.
75
+
76
+ Returns a copy of the headers, an OAuth auth provider when applicable, and the set
77
+ of user-supplied authorization header keys.
78
+ """
79
+ headers: dict[str, str] = dict(server_config.headers or {})
80
+ auth_header_keys = {"authorization", "x-hf-authorization"}
81
+ user_provided_auth_keys = {key for key in headers if key.lower() in auth_header_keys}
82
+
83
+ # OAuth is only relevant for SSE/HTTP transports and should be skipped when the
84
+ # user has already supplied explicit Authorization headers.
85
+ if server_config.transport not in ("sse", "http") or user_provided_auth_keys:
86
+ return headers, None, user_provided_auth_keys
87
+
88
+ oauth_auth = build_oauth_provider(server_config)
89
+ if oauth_auth is not None:
90
+ # Scrub Authorization headers so OAuth-managed credentials are the only ones sent.
91
+ for header_name in (
92
+ "Authorization",
93
+ "authorization",
94
+ "X-HF-Authorization",
95
+ "x-hf-authorization",
96
+ ):
97
+ headers.pop(header_name, None)
98
+
99
+ return headers, oauth_auth, user_provided_auth_keys
100
+
101
+
68
102
  class ServerConnection:
69
103
  """
70
104
  Represents a long-lived MCP server connection, including:
@@ -113,7 +147,9 @@ class ServerConnection:
113
147
  self.server_implementation: Implementation | None = None
114
148
  self.client_capabilities: dict | None = None
115
149
  self.server_instructions_available: bool = False
116
- self.server_instructions_enabled: bool = server_config.include_instructions if server_config else True
150
+ self.server_instructions_enabled: bool = (
151
+ server_config.include_instructions if server_config else True
152
+ )
117
153
  self.session_id: str | None = None
118
154
  self._get_session_id_cb: GetSessionIdCallback | None = None
119
155
  self.transport_metrics: TransportChannelMetrics | None = None
@@ -404,7 +440,9 @@ class MCPConnectionManager(ContextDependent):
404
440
 
405
441
  logger.debug(f"{server_name}: Found server configuration=", data=config.model_dump())
406
442
 
407
- transport_metrics = TransportChannelMetrics() if config.transport in ("http", "stdio") else None
443
+ transport_metrics = (
444
+ TransportChannelMetrics() if config.transport in ("http", "stdio") else None
445
+ )
408
446
 
409
447
  def transport_context_factory():
410
448
  if config.transport == "stdio":
@@ -425,7 +463,9 @@ class MCPConnectionManager(ContextDependent):
425
463
 
426
464
  channel_hook = transport_metrics.record_event if transport_metrics else None
427
465
  return _add_none_to_context(
428
- tracking_stdio_client(server_params, channel_hook=channel_hook, errlog=error_handler)
466
+ tracking_stdio_client(
467
+ server_params, channel_hook=channel_hook, errlog=error_handler
468
+ )
429
469
  )
430
470
  elif config.transport == "sse":
431
471
  if not config.url:
@@ -434,12 +474,12 @@ class MCPConnectionManager(ContextDependent):
434
474
  )
435
475
  # Suppress MCP library error spam
436
476
  self._suppress_mcp_sse_errors()
437
- oauth_auth = build_oauth_provider(config)
438
- # If using OAuth, strip any pre-existing Authorization headers to avoid conflicts
439
- headers = dict(config.headers or {})
440
- if oauth_auth is not None:
441
- headers.pop("Authorization", None)
442
- headers.pop("X-HF-Authorization", None)
477
+ headers, oauth_auth, user_auth_keys = _prepare_headers_and_auth(config)
478
+ if user_auth_keys:
479
+ logger.debug(
480
+ f"{server_name}: Using user-specified auth header(s); skipping OAuth provider.",
481
+ user_auth_headers=sorted(user_auth_keys),
482
+ )
443
483
  return _add_none_to_context(
444
484
  sse_client(
445
485
  config.url,
@@ -453,19 +493,22 @@ class MCPConnectionManager(ContextDependent):
453
493
  raise ValueError(
454
494
  f"Server '{server_name}' uses http transport but no url is specified"
455
495
  )
456
- oauth_auth = build_oauth_provider(config)
457
- headers = dict(config.headers or {})
458
- if oauth_auth is not None:
459
- headers.pop("Authorization", None)
460
- headers.pop("X-HF-Authorization", None)
496
+ headers, oauth_auth, user_auth_keys = _prepare_headers_and_auth(config)
497
+ if user_auth_keys:
498
+ logger.debug(
499
+ f"{server_name}: Using user-specified auth header(s); skipping OAuth provider.",
500
+ user_auth_headers=sorted(user_auth_keys),
501
+ )
461
502
  channel_hook = None
462
503
  if transport_metrics is not None:
504
+
463
505
  def channel_hook(event):
464
506
  try:
465
507
  transport_metrics.record_event(event)
466
508
  except Exception: # pragma: no cover - defensive guard
467
509
  logger.debug(
468
- "%s: transport metrics hook failed", server_name,
510
+ "%s: transport metrics hook failed",
511
+ server_name,
469
512
  exc_info=True,
470
513
  )
471
514
 
@@ -117,6 +117,7 @@ def load_prompt(file: Path) -> List[PromptMessageExtended]:
117
117
  if path_str.endswith(".json"):
118
118
  # JSON files use the serialization module directly
119
119
  from fast_agent.mcp.prompt_serialization import load_messages
120
+
120
121
  return load_messages(str(file))
121
122
  else:
122
123
  # Non-JSON files need template processing for resource loading
@@ -128,15 +129,13 @@ def load_prompt(file: Path) -> List[PromptMessageExtended]:
128
129
  # Render the template without arguments to get the messages
129
130
  messages = create_messages_with_resources(
130
131
  template.content_sections,
131
- [file] # Pass the file path for resource resolution
132
+ [file], # Pass the file path for resource resolution
132
133
  )
133
134
 
134
135
  # Convert to PromptMessageExtended
135
136
  return PromptMessageExtended.to_extended(messages)
136
137
 
137
138
 
138
-
139
-
140
139
  def load_prompt_as_get_prompt_result(file: Path):
141
140
  """
142
141
  Load a prompt from a file and convert to GetPromptResult format for MCP compatibility.
@@ -6,6 +6,7 @@ from mcp.types import CallToolResult
6
6
  from rich.panel import Panel
7
7
  from rich.text import Text
8
8
 
9
+ from fast_agent.constants import REASONING
9
10
  from fast_agent.ui import console
10
11
  from fast_agent.ui.mcp_ui_utils import UILink
11
12
  from fast_agent.ui.mermaid_utils import (
@@ -144,6 +145,25 @@ class ConsoleDisplay:
144
145
  self._markup = config.logger.enable_markup if config else True
145
146
  self._escape_xml = True
146
147
 
148
+ @staticmethod
149
+ def _format_elapsed(elapsed: float) -> str:
150
+ """Format elapsed seconds for display."""
151
+ if elapsed < 0:
152
+ elapsed = 0.0
153
+ if elapsed < 0.001:
154
+ return "<1ms"
155
+ if elapsed < 1:
156
+ return f"{elapsed * 1000:.0f}ms"
157
+ if elapsed < 10:
158
+ return f"{elapsed:.2f}s"
159
+ if elapsed < 60:
160
+ return f"{elapsed:.1f}s"
161
+ minutes, seconds = divmod(elapsed, 60)
162
+ if minutes < 60:
163
+ return f"{int(minutes)}m {seconds:02.0f}s"
164
+ hours, minutes = divmod(int(minutes), 60)
165
+ return f"{hours}h {minutes:02d}m"
166
+
147
167
  def display_message(
148
168
  self,
149
169
  content: Any,
@@ -156,6 +176,7 @@ class ConsoleDisplay:
156
176
  is_error: bool = False,
157
177
  truncate_content: bool = True,
158
178
  additional_message: Text | None = None,
179
+ pre_content: Text | None = None,
159
180
  ) -> None:
160
181
  """
161
182
  Unified method to display formatted messages to the console.
@@ -170,6 +191,8 @@ class ConsoleDisplay:
170
191
  max_item_length: Optional max length for bottom metadata items (with ellipsis)
171
192
  is_error: For tool results, whether this is an error (uses red color)
172
193
  truncate_content: Whether to truncate long content
194
+ additional_message: Optional Rich Text appended after the main content
195
+ pre_content: Optional Rich Text shown before the main content
173
196
  """
174
197
  # Get configuration for this message type
175
198
  config = MESSAGE_CONFIGS[message_type]
@@ -191,6 +214,8 @@ class ConsoleDisplay:
191
214
  self._create_combined_separator_status(left, right_info)
192
215
 
193
216
  # Display the content
217
+ if pre_content and pre_content.plain:
218
+ console.console.print(pre_content, markup=self._markup)
194
219
  self._display_content(
195
220
  content, truncate_content, is_error, message_type, check_markdown_markers=False
196
221
  )
@@ -544,7 +569,7 @@ class ConsoleDisplay:
544
569
 
545
570
  # Build transport channel info for bottom bar
546
571
  channel = getattr(result, "transport_channel", None)
547
- bottom_metadata = None
572
+ bottom_metadata_items: List[str] = []
548
573
  if channel:
549
574
  # Format channel info for bottom bar
550
575
  if channel == "post-json":
@@ -560,7 +585,13 @@ class ConsoleDisplay:
560
585
  else:
561
586
  transport_info = channel.upper()
562
587
 
563
- bottom_metadata = [transport_info]
588
+ bottom_metadata_items.append(transport_info)
589
+
590
+ elapsed = getattr(result, "transport_elapsed", None)
591
+ if isinstance(elapsed, (int, float)):
592
+ bottom_metadata_items.append(self._format_elapsed(float(elapsed)))
593
+
594
+ bottom_metadata = bottom_metadata_items or None
564
595
 
565
596
  # Build right info (without channel info)
566
597
  right_info = f"[dim]tool result - {status}[/dim]"
@@ -724,8 +755,26 @@ class ConsoleDisplay:
724
755
  # Extract text from PromptMessageExtended if needed
725
756
  from fast_agent.types import PromptMessageExtended
726
757
 
758
+ pre_content: Text | None = None
759
+
727
760
  if isinstance(message_text, PromptMessageExtended):
728
761
  display_text = message_text.last_text() or ""
762
+
763
+ channels = message_text.channels or {}
764
+ reasoning_blocks = channels.get(REASONING) or []
765
+ if reasoning_blocks:
766
+ from fast_agent.mcp.helpers.content_helpers import get_text
767
+
768
+ reasoning_segments = []
769
+ for block in reasoning_blocks:
770
+ text = get_text(block)
771
+ if text:
772
+ reasoning_segments.append(text)
773
+
774
+ if reasoning_segments:
775
+ joined = "\n".join(reasoning_segments)
776
+ if joined.strip():
777
+ pre_content = Text(joined, style="dim default")
729
778
  else:
730
779
  display_text = message_text
731
780
 
@@ -743,6 +792,7 @@ class ConsoleDisplay:
743
792
  max_item_length=max_item_length,
744
793
  truncate_content=False, # Assistant messages shouldn't be truncated
745
794
  additional_message=additional_message,
795
+ pre_content=pre_content,
746
796
  )
747
797
 
748
798
  # Handle mermaid diagrams separately (after the main message)
@@ -339,15 +339,16 @@ class AgentCompleter(Completer):
339
339
  # Map commands to their descriptions for better completion hints
340
340
  self.commands = {
341
341
  "mcp": "Show MCP server status",
342
+ "history": "Show conversation history overview (optionally another agent)",
342
343
  "tools": "List available MCP tools",
343
344
  "prompt": "List and choose MCP prompts, or apply specific prompt (/prompt <name>)",
345
+ "clear": "Clear history",
344
346
  "agents": "List available agents",
345
347
  "system": "Show the current system prompt",
346
348
  "usage": "Show current usage statistics",
347
349
  "markdown": "Show last assistant message without markdown formatting",
348
350
  "save_history": "Save history; .json = MCP JSON, others = Markdown",
349
351
  "help": "Show commands and shortcuts",
350
- "clear": "Clear the screen",
351
352
  "EXIT": "Exit fast-agent, terminating any running workflows",
352
353
  "STOP": "Stop this prompting session and move to next workflow step",
353
354
  **(commands or {}), # Allow custom commands to be passed in
@@ -518,7 +519,15 @@ def create_keybindings(
518
519
 
519
520
  @kb.add("c-l")
520
521
  def _(event) -> None:
521
- """Ctrl+L: Clear the input buffer."""
522
+ """Ctrl+L: Clear and redraw the terminal screen."""
523
+ app_ref = event.app or app
524
+ if app_ref and getattr(app_ref, "renderer", None):
525
+ app_ref.renderer.clear()
526
+ app_ref.invalidate()
527
+
528
+ @kb.add("c-u")
529
+ def _(event) -> None:
530
+ """Ctrl+U: Clear the input buffer."""
522
531
  event.current_buffer.text = ""
523
532
 
524
533
  @kb.add("c-e")
@@ -725,13 +734,24 @@ async def get_enhanced_input(
725
734
  # Check for active events first (highest priority)
726
735
  active_status = notification_tracker.get_active_status()
727
736
  if active_status:
728
- event_type = active_status['type'].upper()
729
- server = active_status['server']
730
- notification_segment = f" | <style fg='ansired' bg='ansiblack'>◀ {event_type} ({server})</style>"
737
+ event_type = active_status["type"].upper()
738
+ server = active_status["server"]
739
+ notification_segment = (
740
+ f" | <style fg='ansired' bg='ansiblack'>◀ {event_type} ({server})</style>"
741
+ )
731
742
  elif notification_tracker.get_count() > 0:
732
743
  # Show completed events summary when no active events
733
- summary = notification_tracker.get_summary()
734
- notification_segment = f" | ◀ {notification_tracker.get_count()} updates ({summary})"
744
+ counts_by_type = notification_tracker.get_counts_by_type()
745
+ total_events = sum(counts_by_type.values()) if counts_by_type else 0
746
+
747
+ if len(counts_by_type) == 1:
748
+ event_type, count = next(iter(counts_by_type.items()))
749
+ label_text = notification_tracker.format_event_label(event_type, count)
750
+ notification_segment = f" | ◀ {label_text}"
751
+ else:
752
+ summary = notification_tracker.get_summary(compact=True)
753
+ heading = "event" if total_events == 1 else "events"
754
+ notification_segment = f" | ◀ {total_events} {heading} ({summary})"
735
755
 
736
756
  if middle:
737
757
  return HTML(
@@ -824,14 +844,26 @@ async def get_enhanced_input(
824
844
 
825
845
  if cmd == "help":
826
846
  return "HELP"
827
- elif cmd == "clear":
828
- return "CLEAR"
829
847
  elif cmd == "agents":
830
848
  return "LIST_AGENTS"
831
849
  elif cmd == "system":
832
850
  return "SHOW_SYSTEM"
833
851
  elif cmd == "usage":
834
852
  return "SHOW_USAGE"
853
+ elif cmd == "history":
854
+ target_agent = None
855
+ if len(cmd_parts) > 1:
856
+ candidate = cmd_parts[1].strip()
857
+ if candidate:
858
+ target_agent = candidate
859
+ return {"show_history": {"agent": target_agent}}
860
+ elif cmd == "clear":
861
+ target_agent = None
862
+ if len(cmd_parts) > 1:
863
+ candidate = cmd_parts[1].strip()
864
+ if candidate:
865
+ target_agent = candidate
866
+ return {"clear_history": {"agent": target_agent}}
835
867
  elif cmd == "markdown":
836
868
  return "MARKDOWN"
837
869
  elif cmd in ("save_history", "save"):
@@ -1010,15 +1042,18 @@ async def handle_special_commands(command, agent_app=None):
1010
1042
  if isinstance(command, dict):
1011
1043
  return command
1012
1044
 
1045
+ global agent_histories
1046
+
1013
1047
  # Check for special string commands
1014
1048
  if command == "HELP":
1015
1049
  rich_print("\n[bold]Available Commands:[/bold]")
1016
1050
  rich_print(" /help - Show this help")
1017
- rich_print(" /clear - Clear screen")
1018
1051
  rich_print(" /agents - List available agents")
1019
1052
  rich_print(" /system - Show the current system prompt")
1020
1053
  rich_print(" /prompt <name> - Apply a specific prompt by name")
1021
1054
  rich_print(" /usage - Show current usage statistics")
1055
+ rich_print(" /history [agent_name] - Show chat history overview")
1056
+ rich_print(" /clear [agent_name] - Clear conversation history (keeps templates)")
1022
1057
  rich_print(" /markdown - Show last assistant message without markdown formatting")
1023
1058
  rich_print(" /mcpstatus - Show MCP server status summary for the active agent")
1024
1059
  rich_print(" /save_history <filename> - Save current chat history to a file")
@@ -1034,15 +1069,11 @@ async def handle_special_commands(command, agent_app=None):
1034
1069
  rich_print(" Ctrl+T - Toggle multiline mode")
1035
1070
  rich_print(" Ctrl+E - Edit in external editor")
1036
1071
  rich_print(" Ctrl+Y - Copy last assistant response to clipboard")
1037
- rich_print(" Ctrl+L - Clear input")
1072
+ rich_print(" Ctrl+L - Redraw the screen")
1073
+ rich_print(" Ctrl+U - Clear input")
1038
1074
  rich_print(" Up/Down - Navigate history")
1039
1075
  return True
1040
1076
 
1041
- elif command == "CLEAR":
1042
- # Clear screen (ANSI escape sequence)
1043
- print("\033c", end="")
1044
- return True
1045
-
1046
1077
  elif isinstance(command, str) and command.upper() == "EXIT":
1047
1078
  raise PromptExitError("User requested to exit fast-agent session")
1048
1079