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,946 @@
1
+ """Simplified, robust elicitation form dialog."""
2
+
3
+ import re
4
+ from datetime import date, datetime
5
+ from typing import Any, Tuple
6
+
7
+ from mcp.types import ElicitRequestedSchema
8
+ from prompt_toolkit import Application
9
+ from prompt_toolkit.buffer import Buffer
10
+ from prompt_toolkit.filters import Condition
11
+ from prompt_toolkit.formatted_text import FormattedText
12
+ from prompt_toolkit.key_binding import KeyBindings
13
+ from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous
14
+ from prompt_toolkit.layout import HSplit, Layout, ScrollablePane, VSplit, Window
15
+ from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl
16
+ from prompt_toolkit.validation import ValidationError, Validator
17
+ from prompt_toolkit.widgets import (
18
+ Button,
19
+ Checkbox,
20
+ Frame,
21
+ Label,
22
+ RadioList,
23
+ )
24
+ from pydantic import AnyUrl, EmailStr
25
+ from pydantic import ValidationError as PydanticValidationError
26
+
27
+ from fast_agent.human_input.form_elements import ValidatedCheckboxList
28
+ from fast_agent.ui.elicitation_style import ELICITATION_STYLE
29
+
30
+ text_navigation_mode = False
31
+
32
+
33
+ class SimpleNumberValidator(Validator):
34
+ """Simple number validator with real-time feedback."""
35
+
36
+ def __init__(self, field_type: str, minimum: float | None = None, maximum: float | None = None):
37
+ self.field_type = field_type
38
+ self.minimum = minimum
39
+ self.maximum = maximum
40
+
41
+ def validate(self, document):
42
+ text = document.text.strip()
43
+ if not text:
44
+ return # Empty is OK for optional fields
45
+
46
+ try:
47
+ if self.field_type == "integer":
48
+ value = int(text)
49
+ else:
50
+ value = float(text)
51
+
52
+ if self.minimum is not None and value < self.minimum:
53
+ raise ValidationError(
54
+ message=f"Must be ≥ {self.minimum}", cursor_position=len(text)
55
+ )
56
+
57
+ if self.maximum is not None and value > self.maximum:
58
+ raise ValidationError(
59
+ message=f"Must be ≤ {self.maximum}", cursor_position=len(text)
60
+ )
61
+
62
+ except ValueError:
63
+ raise ValidationError(message=f"Invalid {self.field_type}", cursor_position=len(text))
64
+
65
+
66
+ class SimpleStringValidator(Validator):
67
+ """Simple string validator with real-time feedback."""
68
+
69
+ def __init__(
70
+ self,
71
+ min_length: int | None = None,
72
+ max_length: int | None = None,
73
+ pattern: str | None = None,
74
+ ):
75
+ self.min_length = min_length
76
+ self.max_length = max_length
77
+ self.pattern = re.compile(pattern, re.DOTALL) if pattern else None
78
+
79
+ def validate(self, document):
80
+ text = document.text
81
+ if not text:
82
+ return # Empty is OK for optional fields
83
+
84
+ if self.min_length is not None and len(text) < self.min_length:
85
+ raise ValidationError(
86
+ message=f"Need {self.min_length - len(text)} more chars", cursor_position=len(text)
87
+ )
88
+
89
+ if self.max_length is not None and len(text) > self.max_length:
90
+ raise ValidationError(
91
+ message=f"Too long by {len(text) - self.max_length} chars",
92
+ cursor_position=self.max_length,
93
+ )
94
+
95
+ if self.pattern is not None and self.pattern.fullmatch(text) is None:
96
+ # TODO: Wrap or truncate line if too long
97
+ raise ValidationError(
98
+ message=f"Must match pattern '{self.pattern.pattern}'", cursor_position=len(text)
99
+ )
100
+
101
+
102
+ class FormatValidator(Validator):
103
+ """Format-specific validator using Pydantic validators."""
104
+
105
+ def __init__(self, format_type: str):
106
+ self.format_type = format_type
107
+
108
+ def validate(self, document):
109
+ text = document.text.strip()
110
+ if not text:
111
+ return # Empty is OK for optional fields
112
+
113
+ try:
114
+ if self.format_type == "email":
115
+ # Use Pydantic model validation for email
116
+ from pydantic import BaseModel
117
+
118
+ class EmailModel(BaseModel):
119
+ email: EmailStr
120
+
121
+ EmailModel(email=text)
122
+ elif self.format_type == "uri":
123
+ # Use Pydantic model validation for URI
124
+ from pydantic import BaseModel
125
+
126
+ class UriModel(BaseModel):
127
+ uri: AnyUrl
128
+
129
+ UriModel(uri=text)
130
+ elif self.format_type == "date":
131
+ # Validate ISO date format (YYYY-MM-DD)
132
+ date.fromisoformat(text)
133
+ elif self.format_type == "date-time":
134
+ # Validate ISO datetime format
135
+ datetime.fromisoformat(text.replace("Z", "+00:00"))
136
+ except (PydanticValidationError, ValueError):
137
+ # Extract readable error message
138
+ if self.format_type == "email":
139
+ message = "Invalid email format"
140
+ elif self.format_type == "uri":
141
+ message = "Invalid URI format"
142
+ elif self.format_type == "date":
143
+ message = "Invalid date (use YYYY-MM-DD)"
144
+ elif self.format_type == "date-time":
145
+ message = "Invalid datetime (use ISO 8601)"
146
+ else:
147
+ message = f"Invalid {self.format_type} format"
148
+
149
+ raise ValidationError(message=message, cursor_position=len(text))
150
+
151
+
152
+ class ElicitationForm:
153
+ """Simplified elicitation form with all fields visible."""
154
+
155
+ def __init__(
156
+ self, schema: ElicitRequestedSchema, message: str, agent_name: str, server_name: str
157
+ ):
158
+ self.schema = schema
159
+ self.message = message
160
+ self.agent_name = agent_name
161
+ self.server_name = server_name
162
+
163
+ # Parse schema
164
+ self.properties = schema.get("properties", {})
165
+ self.required_fields = schema.get("required", [])
166
+
167
+ # Field storage
168
+ self.field_widgets: dict[str, Any] = {}
169
+ self.multiline_fields: set[str] = set() # Track which fields are multiline
170
+
171
+ # Result
172
+ self.result = None
173
+ self.action = "cancel"
174
+
175
+ # Build form
176
+ self._build_form()
177
+
178
+ def _build_form(self):
179
+ """Build the form layout."""
180
+
181
+ # Fast-agent provided data (Agent and MCP Server) - aligned labels
182
+ fastagent_info = FormattedText(
183
+ [
184
+ ("class:label", "Agent: "),
185
+ ("class:agent-name", self.agent_name),
186
+ ("class:label", "\nMCP Server: "),
187
+ ("class:server-name", self.server_name),
188
+ ]
189
+ )
190
+ fastagent_header = Window(
191
+ FormattedTextControl(fastagent_info),
192
+ height=2, # Just agent and server lines
193
+ )
194
+
195
+ # MCP Server provided message
196
+ mcp_message = FormattedText([("class:message", self.message)])
197
+ mcp_header = Window(
198
+ FormattedTextControl(mcp_message),
199
+ height=len(self.message.split("\n")),
200
+ )
201
+
202
+ # Create sticky headers (outside scrollable area)
203
+ sticky_headers = HSplit(
204
+ [
205
+ Window(height=1), # Top padding
206
+ VSplit(
207
+ [
208
+ Window(width=2), # Left padding
209
+ fastagent_header, # Fast-agent info
210
+ Window(width=2), # Right padding
211
+ ]
212
+ ),
213
+ Window(height=1), # Spacing
214
+ VSplit(
215
+ [
216
+ Window(width=2), # Left padding
217
+ mcp_header, # MCP server message
218
+ Window(width=2), # Right padding
219
+ ]
220
+ ),
221
+ Window(height=1), # Spacing
222
+ ]
223
+ )
224
+
225
+ # Create scrollable form fields (without headers)
226
+ form_fields = []
227
+
228
+ for field_name, field_def in self.properties.items():
229
+ field_widget = self._create_field(field_name, field_def)
230
+ if field_widget:
231
+ form_fields.append(field_widget)
232
+ form_fields.append(Window(height=1)) # Spacing
233
+
234
+ # Status line for error display (disabled ValidationToolbar to avoid confusion)
235
+ self.status_control = FormattedTextControl(text="")
236
+ self.status_line = Window(
237
+ self.status_control, height=1
238
+ ) # Store reference for later clearing
239
+
240
+ # Buttons - ensure they accept focus
241
+ submit_btn = Button("Accept", handler=self._accept)
242
+ cancel_btn = Button("Cancel", handler=self._cancel)
243
+ decline_btn = Button("Decline", handler=self._decline)
244
+ cancel_all_btn = Button("Cancel All", handler=self._cancel_all)
245
+
246
+ # Store button references for focus debugging
247
+ self.buttons = [submit_btn, decline_btn, cancel_btn, cancel_all_btn]
248
+
249
+ buttons = VSplit(
250
+ [
251
+ submit_btn,
252
+ Window(width=2),
253
+ decline_btn,
254
+ Window(width=2),
255
+ cancel_btn,
256
+ Window(width=2),
257
+ cancel_all_btn,
258
+ ]
259
+ )
260
+
261
+ # Main scrollable content (form fields and buttons only)
262
+ form_fields.extend([self.status_line, buttons])
263
+ scrollable_form_content = HSplit(form_fields)
264
+
265
+ # Add padding around scrollable content
266
+ padded_scrollable_content = HSplit(
267
+ [
268
+ VSplit(
269
+ [
270
+ Window(width=2), # Left padding
271
+ scrollable_form_content,
272
+ Window(width=2), # Right padding
273
+ ]
274
+ ),
275
+ Window(height=1), # Bottom padding
276
+ ]
277
+ )
278
+
279
+ # Wrap only form fields in ScrollablePane (headers stay fixed)
280
+ scrollable_content = ScrollablePane(
281
+ content=padded_scrollable_content,
282
+ show_scrollbar=False, # Only show when content exceeds available space
283
+ display_arrows=False, # Only show when content exceeds available space
284
+ keep_cursor_visible=True,
285
+ keep_focused_window_visible=True,
286
+ )
287
+
288
+ # Combine sticky headers and scrollable content (no separate title bar needed)
289
+ full_content = HSplit(
290
+ [
291
+ Window(height=1), # Top spacing
292
+ sticky_headers, # Headers stay fixed at top
293
+ scrollable_content, # Form fields can scroll
294
+ ]
295
+ )
296
+
297
+ # Choose dialog title: prefer schema.title if provided
298
+ dialog_title = self.schema.get("title") if isinstance(self.schema, dict) else None
299
+ if not dialog_title or not isinstance(dialog_title, str):
300
+ dialog_title = "Elicitation Request"
301
+
302
+ # Create dialog frame with dynamic title
303
+ dialog = Frame(
304
+ body=full_content,
305
+ title=dialog_title,
306
+ style="class:dialog",
307
+ )
308
+
309
+ # Apply width constraints by putting Frame in VSplit with flexible spacers
310
+ # This prevents console display interference and constrains the Frame border
311
+ constrained_dialog = VSplit(
312
+ [
313
+ Window(width=10), # Smaller left spacer
314
+ dialog,
315
+ Window(width=10), # Smaller right spacer
316
+ ]
317
+ )
318
+
319
+ # Use field navigation mode as default
320
+ global text_navigation_mode
321
+ text_navigation_mode = False
322
+
323
+ # Key bindings
324
+ kb = KeyBindings()
325
+
326
+ @kb.add("tab")
327
+ def focus_next_with_refresh(event):
328
+ focus_next(event)
329
+
330
+ @kb.add("s-tab")
331
+ def focus_previous_with_refresh(event):
332
+ focus_previous(event)
333
+
334
+ # Toggle between text navigation mode and field navigation mode
335
+ @kb.add("c-t")
336
+ def toggle_text_navigation_mode(event):
337
+ global text_navigation_mode
338
+ text_navigation_mode = not text_navigation_mode
339
+ event.app.invalidate() # Force redraw the app to update toolbar
340
+
341
+ # Arrow key navigation - let radio lists handle up/down first
342
+ @kb.add("down", filter=Condition(lambda: not text_navigation_mode))
343
+ def focus_next_arrow(event):
344
+ focus_next(event)
345
+
346
+ @kb.add("up", filter=Condition(lambda: not text_navigation_mode))
347
+ def focus_previous_arrow(event):
348
+ focus_previous(event)
349
+
350
+ @kb.add("right", eager=True, filter=Condition(lambda: not text_navigation_mode))
351
+ def focus_next_right(event):
352
+ focus_next(event)
353
+
354
+ @kb.add("left", eager=True, filter=Condition(lambda: not text_navigation_mode))
355
+ def focus_previous_left(event):
356
+ focus_previous(event)
357
+
358
+ # Enter submits in field navigation mode
359
+ @kb.add("c-m", filter=Condition(lambda: not text_navigation_mode))
360
+ def submit_enter(event):
361
+ self._accept()
362
+
363
+ # Ctrl+J inserts newlines in field navigation mode
364
+ @kb.add("c-j", filter=Condition(lambda: not text_navigation_mode))
365
+ def insert_newline_cj(event):
366
+ # Insert a newline at the cursor position
367
+ event.current_buffer.insert_text("\n")
368
+ # Mark this field as multiline when user adds a newline
369
+ for field_name, widget in self.field_widgets.items():
370
+ if isinstance(widget, Buffer) and widget == event.current_buffer:
371
+ self.multiline_fields.add(field_name)
372
+ break
373
+
374
+ # Enter inserts new lines in text navigation mode
375
+ @kb.add("c-m", filter=Condition(lambda: text_navigation_mode))
376
+ def insert_newline_enter(event):
377
+ # Insert a newline at the cursor position
378
+ event.current_buffer.insert_text("\n")
379
+ # Mark this field as multiline when user adds a newline
380
+ for field_name, widget in self.field_widgets.items():
381
+ if isinstance(widget, Buffer) and widget == event.current_buffer:
382
+ self.multiline_fields.add(field_name)
383
+ break
384
+
385
+ # deactivate ctrl+j in text navigation mode
386
+ @kb.add("c-j", filter=Condition(lambda: text_navigation_mode))
387
+ def _(event):
388
+ pass
389
+
390
+ # ESC should ALWAYS cancel immediately, no matter what
391
+ @kb.add("escape", eager=True, is_global=True)
392
+ def cancel(event):
393
+ self._cancel()
394
+
395
+ # Create a root layout with the dialog and bottom toolbar
396
+ def get_toolbar():
397
+ # When clearing, return empty to hide the toolbar completely
398
+ if hasattr(self, "_toolbar_hidden") and self._toolbar_hidden:
399
+ return FormattedText([])
400
+
401
+ mode_label = "TEXT MODE" if text_navigation_mode else "FIELD MODE"
402
+ mode_color = "ansired" if text_navigation_mode else "ansigreen"
403
+
404
+ arrow_up = "↑"
405
+ arrow_down = "↓"
406
+ arrow_left = "←"
407
+ arrow_right = "→"
408
+
409
+ if text_navigation_mode:
410
+ actions_line = " <ESC> cancel. <Cancel All> Auto-Cancel further elicitations from this Server."
411
+ navigation_tail = (
412
+ " | <CTRL+T> toggle text mode. <TAB> navigate. <ENTER> insert new line."
413
+ )
414
+ else:
415
+ actions_line = (
416
+ " <ENTER> submit. <ESC> cancel. <Cancel All> Auto-Cancel further elicitations "
417
+ "from this Server."
418
+ )
419
+ navigation_tail = (
420
+ " | <CTRL+T> toggle text mode. "
421
+ f"<TAB>/{arrow_up}{arrow_down}{arrow_right}{arrow_left} navigate. "
422
+ "<Ctrl+J> insert new line."
423
+ )
424
+
425
+ formatted_segments = [
426
+ ("class:bottom-toolbar.text", actions_line),
427
+ ("", "\n"),
428
+ ("class:bottom-toolbar.text", " | "),
429
+ (f"fg:{mode_color} bg:ansiblack", f" {mode_label} "),
430
+ ("class:bottom-toolbar.text", navigation_tail),
431
+ ]
432
+ return FormattedText(formatted_segments)
433
+
434
+ # Store toolbar function reference for later control
435
+ self._get_toolbar = get_toolbar
436
+ self._dialog = dialog
437
+
438
+ # Create toolbar window that we can reference later
439
+ self._toolbar_window = Window(
440
+ FormattedTextControl(get_toolbar), height=2, style="class:bottom-toolbar"
441
+ )
442
+
443
+ # Add toolbar to the layout
444
+ root_layout = HSplit(
445
+ [
446
+ constrained_dialog, # The width-constrained dialog
447
+ self._toolbar_window,
448
+ ]
449
+ )
450
+ self._root_layout = root_layout
451
+
452
+ # Application with toolbar and validation - ensure our styles override defaults
453
+ self.app = Application(
454
+ layout=Layout(root_layout),
455
+ key_bindings=kb,
456
+ full_screen=False, # Back to windowed mode for better integration
457
+ mouse_support=False,
458
+ style=ELICITATION_STYLE,
459
+ include_default_pygments_style=False, # Use only our custom style
460
+ )
461
+
462
+ # Set initial focus to first form field
463
+ def set_initial_focus():
464
+ try:
465
+ # Find first form field to focus on
466
+ first_field = None
467
+ for field_name in self.properties.keys():
468
+ widget = self.field_widgets.get(field_name)
469
+ if widget:
470
+ first_field = widget
471
+ break
472
+
473
+ if first_field:
474
+ self.app.layout.focus(first_field)
475
+ else:
476
+ # Fallback to first button if no fields
477
+ self.app.layout.focus(submit_btn)
478
+ except Exception:
479
+ pass # If focus fails, continue without it
480
+
481
+ # Schedule focus setting for after layout is ready
482
+ self.app.invalidate() # Ensure layout is built
483
+ set_initial_focus()
484
+
485
+ def _extract_enum_schema_options(self, schema_def: dict[str, Any]) -> list[Tuple[str, str]]:
486
+ """Extract options from oneOf/anyOf/enum schema patterns.
487
+
488
+ Args:
489
+ schema_def: Schema definition potentially containing oneOf/anyOf/enum
490
+
491
+ Returns:
492
+ List of (value, title) tuples for the options
493
+ """
494
+ values = []
495
+
496
+ # First check for bare enum (most common pattern for arrays)
497
+ if "enum" in schema_def:
498
+ enum_values = schema_def["enum"]
499
+ enum_names = schema_def.get("enumNames", enum_values)
500
+ for val, name in zip(enum_values, enum_names):
501
+ values.append((val, str(name)))
502
+ return values
503
+
504
+ # Then check for oneOf/anyOf patterns
505
+ options = schema_def.get("oneOf", [])
506
+ if not options:
507
+ options = schema_def.get("anyOf", [])
508
+
509
+ for option in options:
510
+ if "const" in option:
511
+ value = option["const"]
512
+ title = option.get("title", str(value))
513
+ values.append((value, title))
514
+
515
+ return values
516
+
517
+ def _extract_string_constraints(self, field_def: dict[str, Any]) -> dict[str, Any]:
518
+ """Extract string constraints from field definition, handling anyOf schemas."""
519
+ constraints = {}
520
+
521
+ # Check direct constraints
522
+ if field_def.get("minLength") is not None:
523
+ constraints["minLength"] = field_def["minLength"]
524
+ if field_def.get("maxLength") is not None:
525
+ constraints["maxLength"] = field_def["maxLength"]
526
+ if field_def.get("pattern") is not None:
527
+ constraints["pattern"] = field_def["pattern"]
528
+
529
+ # Check anyOf constraints (for Optional fields)
530
+ if "anyOf" in field_def:
531
+ for variant in field_def["anyOf"]:
532
+ if variant.get("type") == "string":
533
+ if variant.get("minLength") is not None:
534
+ constraints["minLength"] = variant["minLength"]
535
+ if variant.get("maxLength") is not None:
536
+ constraints["maxLength"] = variant["maxLength"]
537
+ if variant.get("pattern") is not None:
538
+ constraints["pattern"] = variant["pattern"]
539
+ break
540
+
541
+ return constraints
542
+
543
+ def _create_field(self, field_name: str, field_def: dict[str, Any]):
544
+ """Create a field widget."""
545
+
546
+ field_type = field_def.get("type", "string")
547
+ title = field_def.get("title", field_name)
548
+ description = field_def.get("description", "")
549
+ is_required = field_name in self.required_fields
550
+
551
+ # Build label with validation hints
552
+ label_text = title
553
+ if is_required:
554
+ label_text += " *"
555
+ if description:
556
+ label_text += f" - {description}"
557
+
558
+ # Add validation hints (simple ones stay on same line)
559
+ hints = []
560
+ format_hint = None
561
+
562
+ # Check if this is an array type with enum/oneOf/anyOf items
563
+ if field_type == "array" and "items" in field_def:
564
+ items_def = field_def["items"]
565
+
566
+ # Add minItems/maxItems hints
567
+ min_items = field_def.get("minItems")
568
+ max_items = field_def.get("maxItems")
569
+
570
+ if min_items is not None and max_items is not None:
571
+ if min_items == max_items:
572
+ hints.append(f"select exactly {min_items}")
573
+ else:
574
+ hints.append(f"select {min_items}-{max_items}")
575
+ elif min_items is not None:
576
+ hints.append(f"select at least {min_items}")
577
+ elif max_items is not None:
578
+ hints.append(f"select up to {max_items}")
579
+
580
+ if field_type == "string":
581
+ constraints = self._extract_string_constraints(field_def)
582
+ if constraints.get("minLength"):
583
+ hints.append(f"min {constraints['minLength']} chars")
584
+ if constraints.get("maxLength"):
585
+ hints.append(f"max {constraints['maxLength']} chars")
586
+
587
+ if constraints.get("pattern"):
588
+ # TODO: Wrap or truncate line if too long
589
+ format_hint = f"Pattern: {constraints['pattern']}"
590
+
591
+ # Handle format hints separately (these go on next line)
592
+ format_type = field_def.get("format")
593
+ if format_type:
594
+ format_info = {
595
+ "email": ("Email", "user@example.com"),
596
+ "uri": ("URI", "https://example.com"),
597
+ "date": ("Date", "YYYY-MM-DD"),
598
+ "date-time": ("Date Time", "YYYY-MM-DD HH:MM:SS"),
599
+ }
600
+ if format_type in format_info:
601
+ friendly_name, example = format_info[format_type]
602
+ format_hint = f"{friendly_name}: {example}"
603
+ else:
604
+ format_hint = format_type
605
+
606
+ elif field_type in ["number", "integer"]:
607
+ if field_def.get("minimum") is not None:
608
+ hints.append(f"min {field_def['minimum']}")
609
+ if field_def.get("maximum") is not None:
610
+ hints.append(f"max {field_def['maximum']}")
611
+ elif field_type == "string" and "enum" in field_def:
612
+ enum_names = field_def.get("enumNames", field_def["enum"])
613
+ hints.append(f"choose from: {', '.join(enum_names)}")
614
+
615
+ # Add simple hints to main label line
616
+ if hints:
617
+ label_text += f" ({', '.join(hints)})"
618
+
619
+ # Create multiline label if we have format hints
620
+ if format_hint:
621
+ label_lines = [label_text, f" → {format_hint}"]
622
+ label = Label(text="\n".join(label_lines))
623
+ else:
624
+ label = Label(text=label_text)
625
+
626
+ # Create input widget based on type
627
+ if field_type == "boolean":
628
+ default = field_def.get("default", False)
629
+ checkbox = Checkbox(text="Yes")
630
+ checkbox.checked = default
631
+ self.field_widgets[field_name] = checkbox
632
+
633
+ return HSplit([label, Frame(checkbox)])
634
+
635
+ elif field_type == "string" and "enum" in field_def:
636
+ # Leaving this here for existing enum schema
637
+ enum_values = field_def["enum"]
638
+ enum_names = field_def.get("enumNames", enum_values)
639
+ values = [(val, name) for val, name in zip(enum_values, enum_names)]
640
+
641
+ default_value = field_def.get("default")
642
+ radio_list = RadioList(values=values, default=default_value)
643
+ self.field_widgets[field_name] = radio_list
644
+
645
+ return HSplit([label, Frame(radio_list, height=min(len(values) + 2, 6))])
646
+
647
+ elif field_type == "string" and "oneOf" in field_def:
648
+ # Handle oneOf pattern for single selection enums
649
+ values = self._extract_enum_schema_options(field_def)
650
+ if values:
651
+ default_value = field_def.get("default")
652
+ radio_list = RadioList(values=values, default=default_value)
653
+ self.field_widgets[field_name] = radio_list
654
+ return HSplit([label, Frame(radio_list, height=min(len(values) + 2, 6))])
655
+
656
+ elif field_type == "array" and "items" in field_def:
657
+ # Handle array types with enum/oneOf/anyOf items
658
+ items_def = field_def["items"]
659
+ values = self._extract_enum_schema_options(items_def)
660
+ if values:
661
+ # Create checkbox list for multi-selection
662
+ min_items = field_def.get("minItems")
663
+ max_items = field_def.get("maxItems")
664
+ default_values = field_def.get("default", [])
665
+
666
+ checkbox_list = ValidatedCheckboxList(
667
+ values=values,
668
+ default_values=default_values,
669
+ min_items=min_items,
670
+ max_items=max_items,
671
+ )
672
+
673
+ # Store the widget directly (consistent with other widgets)
674
+ self.field_widgets[field_name] = checkbox_list
675
+
676
+ # Create scrollable frame if many options
677
+ height = min(len(values) + 2, 8)
678
+ return HSplit([label, Frame(checkbox_list, height=height)])
679
+
680
+ else:
681
+ # Text/number input
682
+ validator: Validator | None = None
683
+
684
+ if field_type in ["number", "integer"]:
685
+ validator = SimpleNumberValidator(
686
+ field_type=field_type,
687
+ minimum=field_def.get("minimum"),
688
+ maximum=field_def.get("maximum"),
689
+ )
690
+ elif field_type == "string":
691
+ constraints = self._extract_string_constraints(field_def)
692
+ format_type = field_def.get("format")
693
+
694
+ if format_type in ["email", "uri", "date", "date-time"]:
695
+ # Use format validator for specific formats
696
+ validator = FormatValidator(format_type)
697
+ else:
698
+ # Use string length validator for regular strings
699
+ validator = SimpleStringValidator(
700
+ min_length=constraints.get("minLength"),
701
+ max_length=constraints.get("maxLength"),
702
+ pattern=constraints.get("pattern"),
703
+ )
704
+ else:
705
+ constraints = {}
706
+
707
+ default_value = field_def.get("default")
708
+
709
+ # Determine if field should be multiline based on max_length or default value length
710
+ if field_type == "string":
711
+ max_length = constraints.get("maxLength")
712
+ # Check default value length if maxLength not specified
713
+ if not max_length and default_value is not None:
714
+ max_length = len(str(default_value))
715
+ else:
716
+ max_length = None
717
+
718
+ # Check if default value contains newlines
719
+ if field_type == "string" and default_value is not None and "\n" in str(default_value):
720
+ multiline = True
721
+ self.multiline_fields.add(field_name) # Track multiline fields
722
+ # Set height to actual line count for fields with newlines in default
723
+ initial_height = str(default_value).count("\n") + 1
724
+ elif max_length and max_length > 100:
725
+ # Use multiline for longer fields
726
+ multiline = True
727
+ self.multiline_fields.add(field_name) # Track multiline fields
728
+ if max_length <= 300:
729
+ initial_height = 3
730
+ else:
731
+ initial_height = 5
732
+ else:
733
+ # Single line for shorter fields
734
+ multiline = False
735
+ initial_height = 1
736
+
737
+ buffer = Buffer(
738
+ validator=validator,
739
+ multiline=multiline,
740
+ validate_while_typing=True, # Enable real-time validation
741
+ complete_while_typing=False, # Disable completion for cleaner experience
742
+ enable_history_search=False, # Disable history for cleaner experience
743
+ )
744
+ if default_value is not None:
745
+ buffer.text = str(default_value)
746
+ self.field_widgets[field_name] = buffer
747
+
748
+ # Create dynamic style function for focus highlighting and validation errors
749
+ def get_field_style():
750
+ """Dynamic style that changes based on focus and validation state."""
751
+ from prompt_toolkit.application.current import get_app
752
+
753
+ # Check if buffer has validation errors
754
+ if buffer.validation_error:
755
+ return "class:input-field.error"
756
+ elif get_app().layout.has_focus(buffer):
757
+ return "class:input-field.focused"
758
+ else:
759
+ return "class:input-field"
760
+
761
+ # Create a dynamic height function based on content
762
+ def get_dynamic_height():
763
+ if not buffer.text:
764
+ return initial_height
765
+ # Calculate height based on number of newlines in buffer
766
+ line_count = buffer.text.count("\n") + 1
767
+ # Use initial height as minimum, grow up to 20 lines
768
+ return min(max(line_count, initial_height), 20)
769
+
770
+ text_input = Window(
771
+ BufferControl(buffer=buffer),
772
+ height=get_dynamic_height, # Use dynamic height function
773
+ style=get_field_style, # Use dynamic style function
774
+ wrap_lines=True if multiline else False, # Enable word wrap for multiline
775
+ )
776
+
777
+ return HSplit([label, Frame(text_input)])
778
+
779
+ def _validate_form(self) -> tuple[bool, str | None]:
780
+ """Validate the entire form."""
781
+
782
+ # First, check all fields for validation errors from their validators
783
+ for field_name, field_def in self.properties.items():
784
+ widget = self.field_widgets.get(field_name)
785
+ if widget is None:
786
+ continue
787
+
788
+ # Check for validation errors from validators
789
+ if isinstance(widget, Buffer):
790
+ if widget.validation_error:
791
+ title = field_def.get("title", field_name)
792
+ return False, f"'{title}': {widget.validation_error.message}"
793
+ elif isinstance(widget, ValidatedCheckboxList):
794
+ if widget.validation_error:
795
+ title = field_def.get("title", field_name)
796
+ return False, f"'{title}': {widget.validation_error.message}"
797
+
798
+ # Then check if required fields are empty
799
+ for field_name in self.required_fields:
800
+ widget = self.field_widgets.get(field_name)
801
+ if widget is None:
802
+ continue
803
+
804
+ # Check if required field has value
805
+ if isinstance(widget, Buffer):
806
+ if not widget.text.strip():
807
+ title = self.properties[field_name].get("title", field_name)
808
+ return False, f"'{title}' is required"
809
+ elif isinstance(widget, RadioList):
810
+ if widget.current_value is None:
811
+ title = self.properties[field_name].get("title", field_name)
812
+ return False, f"'{title}' is required"
813
+ elif isinstance(widget, ValidatedCheckboxList):
814
+ if not widget.current_values:
815
+ title = self.properties[field_name].get("title", field_name)
816
+ return False, f"'{title}' is required"
817
+
818
+ return True, None
819
+
820
+ def _get_form_data(self) -> dict[str, Any]:
821
+ """Extract data from form fields."""
822
+ data: dict[str, Any] = {}
823
+
824
+ for field_name, field_def in self.properties.items():
825
+ widget = self.field_widgets.get(field_name)
826
+ if widget is None:
827
+ continue
828
+
829
+ field_type = field_def.get("type", "string")
830
+
831
+ if isinstance(widget, Buffer):
832
+ value = widget.text.strip()
833
+ if value:
834
+ if field_type == "integer":
835
+ try:
836
+ data[field_name] = int(value)
837
+ except ValueError:
838
+ # This should not happen due to validation, but be safe
839
+ raise ValueError(f"Invalid integer value for {field_name}: {value}")
840
+ elif field_type == "number":
841
+ try:
842
+ data[field_name] = float(value)
843
+ except ValueError:
844
+ # This should not happen due to validation, but be safe
845
+ raise ValueError(f"Invalid number value for {field_name}: {value}")
846
+ else:
847
+ data[field_name] = value
848
+ elif field_name not in self.required_fields:
849
+ data[field_name] = None
850
+
851
+ elif isinstance(widget, Checkbox):
852
+ data[field_name] = widget.checked
853
+
854
+ elif isinstance(widget, RadioList):
855
+ if widget.current_value is not None:
856
+ data[field_name] = widget.current_value
857
+
858
+ elif isinstance(widget, ValidatedCheckboxList):
859
+ selected_values = widget.current_values
860
+ if selected_values:
861
+ data[field_name] = list(selected_values)
862
+ elif field_name not in self.required_fields:
863
+ data[field_name] = []
864
+
865
+ return data
866
+
867
+ def _accept(self):
868
+ """Handle form submission."""
869
+ # Validate
870
+ is_valid, error_msg = self._validate_form()
871
+ if not is_valid:
872
+ # Use styled error message
873
+ self.status_control.text = FormattedText(
874
+ [("class:validation-error", f"Error: {error_msg}")]
875
+ )
876
+ return
877
+
878
+ # Get data
879
+ try:
880
+ self.result = self._get_form_data()
881
+ self.action = "accept"
882
+ self._clear_status_bar()
883
+ self.app.exit()
884
+ except Exception as e:
885
+ # Use styled error message
886
+ self.status_control.text = FormattedText(
887
+ [("class:validation-error", f"Error: {str(e)}")]
888
+ )
889
+
890
+ def _cancel(self):
891
+ """Handle cancel."""
892
+ self.action = "cancel"
893
+ self._clear_status_bar()
894
+ self.app.exit()
895
+
896
+ def _decline(self):
897
+ """Handle decline."""
898
+ self.action = "decline"
899
+ self._clear_status_bar()
900
+ self.app.exit()
901
+
902
+ def _cancel_all(self):
903
+ """Handle cancel all: signal disable; no side effects here.
904
+
905
+ UI emits an action; handler/orchestration is responsible for updating state.
906
+ """
907
+ self.action = "disable"
908
+ self._clear_status_bar()
909
+ self.app.exit()
910
+
911
+ def _clear_status_bar(self):
912
+ """Hide the status bar by removing it from the layout."""
913
+ # Create completely clean layout - just empty space with application background
914
+ from prompt_toolkit.layout import HSplit, Window
915
+ from prompt_toolkit.layout.controls import FormattedTextControl
916
+
917
+ # Create a simple empty window with application background
918
+ empty_window = Window(
919
+ FormattedTextControl(FormattedText([("class:application", "")])), height=1
920
+ )
921
+
922
+ # Replace entire layout with just the empty window
923
+ new_layout = HSplit([empty_window])
924
+
925
+ # Update the app's layout
926
+ if hasattr(self, "app") and self.app:
927
+ self.app.layout.container = new_layout
928
+ self.app.invalidate()
929
+
930
+ async def run_async(self) -> tuple[str, dict[str, Any] | None]:
931
+ """Run the form and return result."""
932
+ try:
933
+ await self.app.run_async()
934
+ except Exception as e:
935
+ print(f"Form error: {e}")
936
+ self.action = "cancel"
937
+ self._clear_status_bar()
938
+ return self.action, self.result
939
+
940
+
941
+ async def show_simple_elicitation_form(
942
+ schema: ElicitRequestedSchema, message: str, agent_name: str, server_name: str
943
+ ) -> tuple[str, dict[str, Any] | None]:
944
+ """Show the simplified elicitation form."""
945
+ form = ElicitationForm(schema, message, agent_name, server_name)
946
+ return await form.run_async()