iCode-TUI 0.29.1__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 (1138) hide show
  1. chrys/__init__.py +16 -0
  2. chrys/app/__init__.py +3 -0
  3. chrys/app/acp/__init__.py +3 -0
  4. chrys/app/acp/bridge.py +664 -0
  5. chrys/app/acp/content.py +132 -0
  6. chrys/app/acp/doc/frontend-api.md +425 -0
  7. chrys/app/acp/history.py +585 -0
  8. chrys/app/acp/server.py +1796 -0
  9. chrys/app/acp/session_manager.py +1815 -0
  10. chrys/app/acp/tool_status.py +45 -0
  11. chrys/app/cli/__init__.py +3 -0
  12. chrys/app/cli/acp.py +178 -0
  13. chrys/app/cli/app.py +165 -0
  14. chrys/app/cli/approvals.py +101 -0
  15. chrys/app/cli/headless.py +223 -0
  16. chrys/app/cli/launch_cwd.py +42 -0
  17. chrys/app/cli/profiles.py +298 -0
  18. chrys/app/cli/progress.py +857 -0
  19. chrys/app/cli/run.py +425 -0
  20. chrys/app/cli/serve.py +1593 -0
  21. chrys/app/cli/tool_summary.py +225 -0
  22. chrys/app/cli/trajectory.py +201 -0
  23. chrys/app/cli/workflow.py +601 -0
  24. chrys/app/features/__init__.py +3 -0
  25. chrys/app/features/buddy/__init__.py +3 -0
  26. chrys/app/features/buddy/actions.py +68 -0
  27. chrys/app/features/buddy/animation.py +76 -0
  28. chrys/app/features/buddy/commands.py +196 -0
  29. chrys/app/features/buddy/docs/design.md +187 -0
  30. chrys/app/features/buddy/hatchery.py +80 -0
  31. chrys/app/features/buddy/lifecycle.py +41 -0
  32. chrys/app/features/buddy/model.py +265 -0
  33. chrys/app/features/buddy/pixel_renderer.py +113 -0
  34. chrys/app/features/buddy/pixel_sprites.py +279 -0
  35. chrys/app/features/buddy/portrait.py +76 -0
  36. chrys/app/features/buddy/progression.py +58 -0
  37. chrys/app/features/buddy/replies.py +191 -0
  38. chrys/app/features/buddy/sprites/alien.toml +70 -0
  39. chrys/app/features/buddy/sprites/axolotl.toml +71 -0
  40. chrys/app/features/buddy/sprites/bat.toml +69 -0
  41. chrys/app/features/buddy/sprites/bee.toml +69 -0
  42. chrys/app/features/buddy/sprites/cactus.toml +73 -0
  43. chrys/app/features/buddy/sprites/capybara.toml +70 -0
  44. chrys/app/features/buddy/sprites/cat.toml +69 -0
  45. chrys/app/features/buddy/sprites/crab.toml +70 -0
  46. chrys/app/features/buddy/sprites/dragon.toml +71 -0
  47. chrys/app/features/buddy/sprites/duck.toml +69 -0
  48. chrys/app/features/buddy/sprites/fox.toml +69 -0
  49. chrys/app/features/buddy/sprites/frog.toml +70 -0
  50. chrys/app/features/buddy/sprites/ghost.toml +70 -0
  51. chrys/app/features/buddy/sprites/goose.toml +69 -0
  52. chrys/app/features/buddy/sprites/jelly.toml +70 -0
  53. chrys/app/features/buddy/sprites/llama.toml +70 -0
  54. chrys/app/features/buddy/sprites/mushroom.toml +69 -0
  55. chrys/app/features/buddy/sprites/octopus.toml +70 -0
  56. chrys/app/features/buddy/sprites/owl.toml +70 -0
  57. chrys/app/features/buddy/sprites/panda.toml +71 -0
  58. chrys/app/features/buddy/sprites/penguin.toml +69 -0
  59. chrys/app/features/buddy/sprites/rabbit.toml +70 -0
  60. chrys/app/features/buddy/sprites/robot.toml +72 -0
  61. chrys/app/features/buddy/sprites/shark.toml +70 -0
  62. chrys/app/features/buddy/sprites/snail.toml +69 -0
  63. chrys/app/features/buddy/sprites/snake.toml +69 -0
  64. chrys/app/features/buddy/sprites/tabby.toml +69 -0
  65. chrys/app/features/buddy/sprites/turtle.toml +69 -0
  66. chrys/app/features/buddy/store.py +143 -0
  67. chrys/app/features/session_title/__init__.py +7 -0
  68. chrys/app/features/session_title/updater.py +582 -0
  69. chrys/app/installer.py +651 -0
  70. chrys/app/parsing.py +17 -0
  71. chrys/app/tui/__init__.py +3 -0
  72. chrys/app/tui/app.py +1287 -0
  73. chrys/app/tui/behaviors/__init__.py +3 -0
  74. chrys/app/tui/behaviors/click_outside_dismiss.py +90 -0
  75. chrys/app/tui/behaviors/insert_clipboard.py +73 -0
  76. chrys/app/tui/behaviors/right_click_copy.py +146 -0
  77. chrys/app/tui/binding_display.py +139 -0
  78. chrys/app/tui/buddy_messages.py +17 -0
  79. chrys/app/tui/buddy_reply.py +86 -0
  80. chrys/app/tui/chrys.tcss +549 -0
  81. chrys/app/tui/clipboard.py +109 -0
  82. chrys/app/tui/copy_messages.py +10 -0
  83. chrys/app/tui/i18n.py +230 -0
  84. chrys/app/tui/language.py +58 -0
  85. chrys/app/tui/notifications/__init__.py +12 -0
  86. chrys/app/tui/notifications/drivers.py +180 -0
  87. chrys/app/tui/notifications/service.py +126 -0
  88. chrys/app/tui/notifications/settings.py +141 -0
  89. chrys/app/tui/screens/__init__.py +3 -0
  90. chrys/app/tui/screens/agents/__init__.py +8 -0
  91. chrys/app/tui/screens/agents/agent_draft_store.py +937 -0
  92. chrys/app/tui/screens/agents/config.py +2142 -0
  93. chrys/app/tui/screens/agents/config.tcss +169 -0
  94. chrys/app/tui/screens/agents/panels/__init__.py +3 -0
  95. chrys/app/tui/screens/agents/panels/acp.py +1176 -0
  96. chrys/app/tui/screens/agents/panels/basic.py +497 -0
  97. chrys/app/tui/screens/agents/panels/compaction.py +231 -0
  98. chrys/app/tui/screens/agents/panels/config_card.py +218 -0
  99. chrys/app/tui/screens/agents/panels/instructions.py +91 -0
  100. chrys/app/tui/screens/agents/panels/markdown_report.py +76 -0
  101. chrys/app/tui/screens/agents/panels/mcp.py +2262 -0
  102. chrys/app/tui/screens/agents/panels/memory.py +684 -0
  103. chrys/app/tui/screens/agents/panels/path_entry.py +227 -0
  104. chrys/app/tui/screens/agents/panels/skills.py +806 -0
  105. chrys/app/tui/screens/agents/panels/subagents.py +652 -0
  106. chrys/app/tui/screens/agents/panels/tools.py +205 -0
  107. chrys/app/tui/screens/agents/picker.py +103 -0
  108. chrys/app/tui/screens/agents/skill_paths.py +38 -0
  109. chrys/app/tui/screens/agents/validation_messages.py +452 -0
  110. chrys/app/tui/screens/dialogs/__init__.py +35 -0
  111. chrys/app/tui/screens/dialogs/agent_load.py +834 -0
  112. chrys/app/tui/screens/dialogs/agent_load.tcss +78 -0
  113. chrys/app/tui/screens/dialogs/app_mode.py +55 -0
  114. chrys/app/tui/screens/dialogs/approval/__init__.py +15 -0
  115. chrys/app/tui/screens/dialogs/approval/approval.tcss +311 -0
  116. chrys/app/tui/screens/dialogs/approval/bodies/__init__.py +3 -0
  117. chrys/app/tui/screens/dialogs/approval/bodies/file_edit.py +210 -0
  118. chrys/app/tui/screens/dialogs/approval/bodies/sub_agent.py +82 -0
  119. chrys/app/tui/screens/dialogs/approval/body.py +110 -0
  120. chrys/app/tui/screens/dialogs/approval/dialog.py +565 -0
  121. chrys/app/tui/screens/dialogs/approval/mode.py +105 -0
  122. chrys/app/tui/screens/dialogs/ask_user.py +148 -0
  123. chrys/app/tui/screens/dialogs/ask_user.tcss +62 -0
  124. chrys/app/tui/screens/dialogs/base.py +84 -0
  125. chrys/app/tui/screens/dialogs/confirm.py +184 -0
  126. chrys/app/tui/screens/dialogs/confirm.tcss +46 -0
  127. chrys/app/tui/screens/dialogs/connection_test.py +183 -0
  128. chrys/app/tui/screens/dialogs/connection_test.tcss +92 -0
  129. chrys/app/tui/screens/dialogs/diagram.py +102 -0
  130. chrys/app/tui/screens/dialogs/diagram.tcss +24 -0
  131. chrys/app/tui/screens/dialogs/editor.py +378 -0
  132. chrys/app/tui/screens/dialogs/editor.tcss +132 -0
  133. chrys/app/tui/screens/dialogs/file_picker.py +582 -0
  134. chrys/app/tui/screens/dialogs/file_picker.tcss +78 -0
  135. chrys/app/tui/screens/dialogs/fork_session.py +219 -0
  136. chrys/app/tui/screens/dialogs/fork_session.tcss +75 -0
  137. chrys/app/tui/screens/dialogs/image_compression.py +102 -0
  138. chrys/app/tui/screens/dialogs/image_compression.tcss +28 -0
  139. chrys/app/tui/screens/dialogs/man_page.py +172 -0
  140. chrys/app/tui/screens/dialogs/man_page.tcss +39 -0
  141. chrys/app/tui/screens/dialogs/new_folder.py +277 -0
  142. chrys/app/tui/screens/dialogs/new_folder.tcss +51 -0
  143. chrys/app/tui/screens/dialogs/notice.tcss +14 -0
  144. chrys/app/tui/screens/dialogs/prompt_cache.py +144 -0
  145. chrys/app/tui/screens/dialogs/prompt_cache.tcss +38 -0
  146. chrys/app/tui/screens/dialogs/runtime_details.py +371 -0
  147. chrys/app/tui/screens/dialogs/runtime_details.tcss +49 -0
  148. chrys/app/tui/screens/dialogs/session_title.py +110 -0
  149. chrys/app/tui/screens/dialogs/session_title.tcss +43 -0
  150. chrys/app/tui/screens/dialogs/tool_view.py +207 -0
  151. chrys/app/tui/screens/dialogs/tool_view.tcss +43 -0
  152. chrys/app/tui/screens/dialogs/vision_unsupported.py +122 -0
  153. chrys/app/tui/screens/dialogs/vision_unsupported.tcss +35 -0
  154. chrys/app/tui/screens/dialogs/workflow_confirm.py +91 -0
  155. chrys/app/tui/screens/dialogs/workflow_confirm.tcss +17 -0
  156. chrys/app/tui/screens/dialogs/workflow_input.py +258 -0
  157. chrys/app/tui/screens/dialogs/workflow_input.tcss +73 -0
  158. chrys/app/tui/screens/dialogs/workflow_node.py +462 -0
  159. chrys/app/tui/screens/dialogs/workflow_node.tcss +41 -0
  160. chrys/app/tui/screens/dialogs/workflow_picker.py +46 -0
  161. chrys/app/tui/screens/dialogs/workflow_result.py +136 -0
  162. chrys/app/tui/screens/dialogs/workflow_result.tcss +14 -0
  163. chrys/app/tui/screens/diff/__init__.py +22 -0
  164. chrys/app/tui/screens/diff/rollback_modal.py +1381 -0
  165. chrys/app/tui/screens/diff/rollback_modal.tcss +248 -0
  166. chrys/app/tui/screens/diff/screen.py +1532 -0
  167. chrys/app/tui/screens/diff/screen.tcss +104 -0
  168. chrys/app/tui/screens/guides/__init__.py +7 -0
  169. chrys/app/tui/screens/guides/_docs/en/guides/configuration/agents.md +178 -0
  170. chrys/app/tui/screens/guides/_docs/en/guides/configuration/approval.md +117 -0
  171. chrys/app/tui/screens/guides/_docs/en/guides/configuration/chat-options.md +276 -0
  172. chrys/app/tui/screens/guides/_docs/en/guides/configuration/compaction.md +42 -0
  173. chrys/app/tui/screens/guides/_docs/en/guides/configuration/memory.md +48 -0
  174. chrys/app/tui/screens/guides/_docs/en/guides/configuration/models.md +158 -0
  175. chrys/app/tui/screens/guides/_docs/en/guides/configuration/settings.md +210 -0
  176. chrys/app/tui/screens/guides/_docs/en/guides/configuration/themes.md +61 -0
  177. chrys/app/tui/screens/guides/_docs/en/guides/configuration/tools.md +60 -0
  178. chrys/app/tui/screens/guides/_docs/en/guides/configuration/web-tools.md +286 -0
  179. chrys/app/tui/screens/guides/_docs/en/guides/daily-use/sessions.md +77 -0
  180. chrys/app/tui/screens/guides/_docs/en/guides/daily-use/workspaces.md +51 -0
  181. chrys/app/tui/screens/guides/_docs/en/guides/extensions/external-acp-agents.md +126 -0
  182. chrys/app/tui/screens/guides/_docs/en/guides/extensions/hooks.md +167 -0
  183. chrys/app/tui/screens/guides/_docs/en/guides/extensions/mcp.md +95 -0
  184. chrys/app/tui/screens/guides/_docs/en/guides/extensions/skills.md +175 -0
  185. chrys/app/tui/screens/guides/_docs/en/guides/running/icode-acp.md +61 -0
  186. chrys/app/tui/screens/guides/_docs/en/guides/running/icode-run.md +190 -0
  187. chrys/app/tui/screens/guides/_docs/en/guides/running/icode-serve.md +239 -0
  188. chrys/app/tui/screens/guides/_docs/en/guides/running/workflows.md +664 -0
  189. chrys/app/tui/screens/guides/_docs/en/reference/agent-profile.md +548 -0
  190. chrys/app/tui/screens/guides/_docs/en/reference/hooks.md +602 -0
  191. chrys/app/tui/screens/guides/_docs/en/reference/opentelemetry.md +177 -0
  192. chrys/app/tui/screens/guides/_docs/en/reference/settings.md +245 -0
  193. chrys/app/tui/screens/guides/_docs/en/reference/themes.md +149 -0
  194. chrys/app/tui/screens/guides/_docs/en/reference/tool-kinds-and-names.md +58 -0
  195. chrys/app/tui/screens/guides/_docs/en/reference/tui-slash-commands.md +76 -0
  196. chrys/app/tui/screens/guides/_docs/en/reference/workflows.md +1006 -0
  197. chrys/app/tui/screens/guides/_docs/en/start/getting-started.md +170 -0
  198. chrys/app/tui/screens/guides/_docs/en/start/what-is-icode.md +34 -0
  199. chrys/app/tui/screens/guides/_docs/index.yaml +100 -0
  200. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/agents.md +178 -0
  201. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/approval.md +117 -0
  202. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/chat-options.md +276 -0
  203. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/compaction.md +42 -0
  204. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/memory.md +48 -0
  205. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/models.md +158 -0
  206. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/settings.md +210 -0
  207. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/themes.md +61 -0
  208. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/tools.md +60 -0
  209. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/web-tools.md +286 -0
  210. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/daily-use/sessions.md +77 -0
  211. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/daily-use/workspaces.md +51 -0
  212. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/external-acp-agents.md +126 -0
  213. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/hooks.md +167 -0
  214. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/mcp.md +95 -0
  215. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/skills.md +175 -0
  216. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-acp.md +61 -0
  217. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-run.md +190 -0
  218. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-serve.md +239 -0
  219. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/workflows.md +664 -0
  220. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/agent-profile.md +548 -0
  221. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/hooks.md +602 -0
  222. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/opentelemetry.md +177 -0
  223. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/settings.md +245 -0
  224. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/themes.md +149 -0
  225. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/tool-kinds-and-names.md +58 -0
  226. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/tui-slash-commands.md +76 -0
  227. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/workflows.md +1005 -0
  228. chrys/app/tui/screens/guides/_docs/zh-Hans/start/getting-started.md +170 -0
  229. chrys/app/tui/screens/guides/_docs/zh-Hans/start/what-is-icode.md +34 -0
  230. chrys/app/tui/screens/guides/index.py +215 -0
  231. chrys/app/tui/screens/guides/reader.py +173 -0
  232. chrys/app/tui/screens/guides/screen.py +442 -0
  233. chrys/app/tui/screens/guides/screen.tcss +52 -0
  234. chrys/app/tui/screens/logs/__init__.py +7 -0
  235. chrys/app/tui/screens/logs/screen.py +504 -0
  236. chrys/app/tui/screens/logs/screen.tcss +45 -0
  237. chrys/app/tui/screens/main/__init__.py +7 -0
  238. chrys/app/tui/screens/main/action_availability.py +30 -0
  239. chrys/app/tui/screens/main/buddy_command.py +143 -0
  240. chrys/app/tui/screens/main/commands.py +1073 -0
  241. chrys/app/tui/screens/main/config_actions.py +464 -0
  242. chrys/app/tui/screens/main/copy_actions.py +117 -0
  243. chrys/app/tui/screens/main/dialog_controllers.py +826 -0
  244. chrys/app/tui/screens/main/dialog_gateway.py +166 -0
  245. chrys/app/tui/screens/main/diff_controller.py +478 -0
  246. chrys/app/tui/screens/main/engine_read_model.py +184 -0
  247. chrys/app/tui/screens/main/event_handlers.py +2014 -0
  248. chrys/app/tui/screens/main/input_flow.py +241 -0
  249. chrys/app/tui/screens/main/live_diff.py +244 -0
  250. chrys/app/tui/screens/main/main_screen_presenter.py +295 -0
  251. chrys/app/tui/screens/main/model_indicator.py +217 -0
  252. chrys/app/tui/screens/main/navigation.py +343 -0
  253. chrys/app/tui/screens/main/ports.py +571 -0
  254. chrys/app/tui/screens/main/recent_dirs.py +123 -0
  255. chrys/app/tui/screens/main/rollback_controller.py +586 -0
  256. chrys/app/tui/screens/main/runtime_info.py +71 -0
  257. chrys/app/tui/screens/main/screen.py +2265 -0
  258. chrys/app/tui/screens/main/screen.tcss +21 -0
  259. chrys/app/tui/screens/main/session_handlers.py +1127 -0
  260. chrys/app/tui/screens/main/session_title.py +239 -0
  261. chrys/app/tui/screens/main/settings_coordinator.py +365 -0
  262. chrys/app/tui/screens/main/settings_persistence.py +184 -0
  263. chrys/app/tui/screens/main/shell_mode.py +112 -0
  264. chrys/app/tui/screens/main/state.py +289 -0
  265. chrys/app/tui/screens/main/submit_state.py +11 -0
  266. chrys/app/tui/screens/main/subscriptions.py +177 -0
  267. chrys/app/tui/screens/main/suggestions.py +982 -0
  268. chrys/app/tui/screens/main/tool_action_bridge.py +75 -0
  269. chrys/app/tui/screens/main/view_adapter.py +1567 -0
  270. chrys/app/tui/screens/main/workflow_browser.py +747 -0
  271. chrys/app/tui/screens/main/workflow_content.py +248 -0
  272. chrys/app/tui/screens/main/workflow_controller.py +240 -0
  273. chrys/app/tui/screens/main/workflow_feedback.py +137 -0
  274. chrys/app/tui/screens/main/workflow_flow.py +49 -0
  275. chrys/app/tui/screens/main/workflow_run_control.py +456 -0
  276. chrys/app/tui/screens/main/workflow_session_view.py +316 -0
  277. chrys/app/tui/screens/main/workspace_actions.py +272 -0
  278. chrys/app/tui/screens/main/workspace_branch.py +194 -0
  279. chrys/app/tui/screens/menus/__init__.py +8 -0
  280. chrys/app/tui/screens/menus/languages.py +114 -0
  281. chrys/app/tui/screens/models/__init__.py +7 -0
  282. chrys/app/tui/screens/models/picker.py +147 -0
  283. chrys/app/tui/screens/models/screen.py +2043 -0
  284. chrys/app/tui/screens/models/screen.tcss +255 -0
  285. chrys/app/tui/screens/sessions/__init__.py +8 -0
  286. chrys/app/tui/screens/sessions/presenter.py +586 -0
  287. chrys/app/tui/screens/sessions/screen.py +779 -0
  288. chrys/app/tui/screens/sessions/screen.tcss +127 -0
  289. chrys/app/tui/screens/settings/__init__.py +29 -0
  290. chrys/app/tui/screens/settings/danger.py +26 -0
  291. chrys/app/tui/screens/settings/dialog.py +281 -0
  292. chrys/app/tui/screens/settings/layout.py +519 -0
  293. chrys/app/tui/screens/settings/migrate_dialog.py +253 -0
  294. chrys/app/tui/screens/settings/panes/__init__.py +3 -0
  295. chrys/app/tui/screens/settings/panes/notifications.py +248 -0
  296. chrys/app/tui/screens/settings/panes/sessions.py +148 -0
  297. chrys/app/tui/screens/settings/ports.py +55 -0
  298. chrys/app/tui/screens/settings/provenance.py +102 -0
  299. chrys/app/tui/screens/settings/rows.py +546 -0
  300. chrys/app/tui/screens/settings/settings.tcss +406 -0
  301. chrys/app/tui/screens/themes/__init__.py +8 -0
  302. chrys/app/tui/screens/themes/dialogs.py +330 -0
  303. chrys/app/tui/screens/themes/editor.py +632 -0
  304. chrys/app/tui/screens/themes/editor.tcss +268 -0
  305. chrys/app/tui/screens/themes/messages.py +95 -0
  306. chrys/app/tui/screens/themes/palette.py +694 -0
  307. chrys/app/tui/screens/themes/panel.py +113 -0
  308. chrys/app/tui/screens/themes/picker.py +203 -0
  309. chrys/app/tui/screens/themes/save.py +191 -0
  310. chrys/app/tui/support/__init__.py +3 -0
  311. chrys/app/tui/support/file_manager.py +64 -0
  312. chrys/app/tui/support/gc_freeze.py +1114 -0
  313. chrys/app/tui/support/path_shortcuts.py +50 -0
  314. chrys/app/tui/support/prompt_history.py +298 -0
  315. chrys/app/tui/support/workspace_mru.py +476 -0
  316. chrys/app/tui/terminal/__init__.py +6 -0
  317. chrys/app/tui/terminal/_pty_backend.py +114 -0
  318. chrys/app/tui/terminal/emulator/__init__.py +24 -0
  319. chrys/app/tui/terminal/emulator/buffer.py +456 -0
  320. chrys/app/tui/terminal/emulator/charsets.py +113 -0
  321. chrys/app/tui/terminal/emulator/core.py +963 -0
  322. chrys/app/tui/terminal/emulator/input.py +342 -0
  323. chrys/app/tui/terminal/emulator/parser.py +227 -0
  324. chrys/app/tui/terminal/emulator/pen.py +158 -0
  325. chrys/app/tui/terminal/launcher.py +455 -0
  326. chrys/app/tui/terminal/leaked_reports.py +44 -0
  327. chrys/app/tui/terminal/panel.py +323 -0
  328. chrys/app/tui/terminal/pty_process.py +405 -0
  329. chrys/app/tui/terminal/rendering.py +151 -0
  330. chrys/app/tui/terminal/shell.py +273 -0
  331. chrys/app/tui/terminal/shell_integration.py +199 -0
  332. chrys/app/tui/terminal/title.py +224 -0
  333. chrys/app/tui/terminal/widget.py +654 -0
  334. chrys/app/tui/theme.py +422 -0
  335. chrys/app/tui/theme_loader.py +201 -0
  336. chrys/app/tui/theme_stylesheet.py +49 -0
  337. chrys/app/tui/themes/__init__.py +3 -0
  338. chrys/app/tui/themes/document.py +120 -0
  339. chrys/app/tui/themes/preview.py +170 -0
  340. chrys/app/tui/themes/store.py +176 -0
  341. chrys/app/tui/toasts.py +69 -0
  342. chrys/app/tui/util/__init__.py +3 -0
  343. chrys/app/tui/util/context_pressure.py +146 -0
  344. chrys/app/tui/util/debug_log.py +292 -0
  345. chrys/app/tui/util/diff_entries.py +64 -0
  346. chrys/app/tui/util/formatting.py +53 -0
  347. chrys/app/tui/util/git_branch.py +326 -0
  348. chrys/app/tui/util/invocation_progress.py +57 -0
  349. chrys/app/tui/util/logo.py +32 -0
  350. chrys/app/tui/util/message_gate.py +35 -0
  351. chrys/app/tui/util/mutation_sources.py +14 -0
  352. chrys/app/tui/util/picker_text.py +27 -0
  353. chrys/app/tui/util/removal.py +47 -0
  354. chrys/app/tui/util/rich_style.py +36 -0
  355. chrys/app/tui/util/source_text.py +12 -0
  356. chrys/app/tui/util/static_update.py +88 -0
  357. chrys/app/tui/util/todo_format.py +51 -0
  358. chrys/app/tui/util/visibility.py +214 -0
  359. chrys/app/tui/widgets/__init__.py +60 -0
  360. chrys/app/tui/widgets/ask_user_controls.py +652 -0
  361. chrys/app/tui/widgets/ask_user_markdown.py +17 -0
  362. chrys/app/tui/widgets/ask_user_prompt.py +606 -0
  363. chrys/app/tui/widgets/buttons.py +52 -0
  364. chrys/app/tui/widgets/chat/__init__.py +3 -0
  365. chrys/app/tui/widgets/chat/agent_transcript_surface.py +818 -0
  366. chrys/app/tui/widgets/chat/chrome.py +296 -0
  367. chrys/app/tui/widgets/chat/compaction_card.py +353 -0
  368. chrys/app/tui/widgets/chat/context_fold.py +77 -0
  369. chrys/app/tui/widgets/chat/file_snapshot.py +91 -0
  370. chrys/app/tui/widgets/chat/image_preview.py +214 -0
  371. chrys/app/tui/widgets/chat/live_renderer.py +383 -0
  372. chrys/app/tui/widgets/chat/messages.py +1039 -0
  373. chrys/app/tui/widgets/chat/panel.py +1769 -0
  374. chrys/app/tui/widgets/chat/ports.py +245 -0
  375. chrys/app/tui/widgets/chat/renderers/__init__.py +7 -0
  376. chrys/app/tui/widgets/chat/renderers/ask_user.py +747 -0
  377. chrys/app/tui/widgets/chat/renderers/execute.py +431 -0
  378. chrys/app/tui/widgets/chat/renderers/file_edit.py +925 -0
  379. chrys/app/tui/widgets/chat/renderers/hosted_code.py +83 -0
  380. chrys/app/tui/widgets/chat/renderers/hosted_generic.py +330 -0
  381. chrys/app/tui/widgets/chat/renderers/hosted_image.py +107 -0
  382. chrys/app/tui/widgets/chat/renderers/hosted_mcp.py +43 -0
  383. chrys/app/tui/widgets/chat/renderers/hosted_search.py +167 -0
  384. chrys/app/tui/widgets/chat/renderers/hosted_shell.py +132 -0
  385. chrys/app/tui/widgets/chat/renderers/read_file.py +399 -0
  386. chrys/app/tui/widgets/chat/renderers/search.py +348 -0
  387. chrys/app/tui/widgets/chat/renderers/skill.py +564 -0
  388. chrys/app/tui/widgets/chat/renderers/sleep.py +287 -0
  389. chrys/app/tui/widgets/chat/renderers/sub_agent.py +1385 -0
  390. chrys/app/tui/widgets/chat/renderers/todo.py +114 -0
  391. chrys/app/tui/widgets/chat/renderers/web.py +69 -0
  392. chrys/app/tui/widgets/chat/replay.py +1605 -0
  393. chrys/app/tui/widgets/chat/replay_mount.py +322 -0
  394. chrys/app/tui/widgets/chat/scroll_controller.py +579 -0
  395. chrys/app/tui/widgets/chat/selection_controller.py +435 -0
  396. chrys/app/tui/widgets/chat/session_json.py +508 -0
  397. chrys/app/tui/widgets/chat/toc_model.py +156 -0
  398. chrys/app/tui/widgets/chat/tool_call.py +2478 -0
  399. chrys/app/tui/widgets/chat/tool_registry.py +753 -0
  400. chrys/app/tui/widgets/chat/tool_renderers.py +247 -0
  401. chrys/app/tui/widgets/chat/tool_view_builders.py +260 -0
  402. chrys/app/tui/widgets/chat/transcript_reader.py +57 -0
  403. chrys/app/tui/widgets/checkbox.py +24 -0
  404. chrys/app/tui/widgets/chrome/__init__.py +3 -0
  405. chrys/app/tui/widgets/chrome/app_header.py +283 -0
  406. chrys/app/tui/widgets/chrome/commands.py +95 -0
  407. chrys/app/tui/widgets/chrome/file_index.py +68 -0
  408. chrys/app/tui/widgets/chrome/file_scanner.py +670 -0
  409. chrys/app/tui/widgets/chrome/footer.py +284 -0
  410. chrys/app/tui/widgets/chrome/image_paste.py +351 -0
  411. chrys/app/tui/widgets/chrome/input_bar.py +1299 -0
  412. chrys/app/tui/widgets/chrome/status_bar.py +911 -0
  413. chrys/app/tui/widgets/chrome/suggestion_list.py +652 -0
  414. chrys/app/tui/widgets/click_affordance.py +24 -0
  415. chrys/app/tui/widgets/color_picker/__init__.py +254 -0
  416. chrys/app/tui/widgets/color_picker/controls.py +171 -0
  417. chrys/app/tui/widgets/color_picker/model.py +89 -0
  418. chrys/app/tui/widgets/dialog_buttons.py +58 -0
  419. chrys/app/tui/widgets/dialog_scroll.py +24 -0
  420. chrys/app/tui/widgets/diff_view/__init__.py +20 -0
  421. chrys/app/tui/widgets/diff_view/cells.py +59 -0
  422. chrys/app/tui/widgets/diff_view/code.py +280 -0
  423. chrys/app/tui/widgets/diff_view/compute.py +286 -0
  424. chrys/app/tui/widgets/diff_view/gutter.py +161 -0
  425. chrys/app/tui/widgets/diff_view/inline.py +77 -0
  426. chrys/app/tui/widgets/diff_view/palette.py +87 -0
  427. chrys/app/tui/widgets/diff_view/protocols.py +24 -0
  428. chrys/app/tui/widgets/diff_view/rows.py +138 -0
  429. chrys/app/tui/widgets/diff_view/unified.py +267 -0
  430. chrys/app/tui/widgets/diff_view/widget.py +209 -0
  431. chrys/app/tui/widgets/editor/__init__.py +79 -0
  432. chrys/app/tui/widgets/editor/coordinates.py +30 -0
  433. chrys/app/tui/widgets/editor/highlighter.py +139 -0
  434. chrys/app/tui/widgets/editor/keymaps.py +357 -0
  435. chrys/app/tui/widgets/editor/types.py +216 -0
  436. chrys/app/tui/widgets/editor/vim.py +1195 -0
  437. chrys/app/tui/widgets/editor/widget.py +401 -0
  438. chrys/app/tui/widgets/file_preview.py +58 -0
  439. chrys/app/tui/widgets/hatch.py +138 -0
  440. chrys/app/tui/widgets/input.py +66 -0
  441. chrys/app/tui/widgets/loading.py +125 -0
  442. chrys/app/tui/widgets/markdown/__init__.py +25 -0
  443. chrys/app/tui/widgets/markdown/_utils.py +21 -0
  444. chrys/app/tui/widgets/markdown/anchors.py +71 -0
  445. chrys/app/tui/widgets/markdown/blocks.py +115 -0
  446. chrys/app/tui/widgets/markdown/copyable.py +163 -0
  447. chrys/app/tui/widgets/markdown/diagram/__init__.py +35 -0
  448. chrys/app/tui/widgets/markdown/diagram/canvas.py +409 -0
  449. chrys/app/tui/widgets/markdown/diagram/charts.py +52 -0
  450. chrys/app/tui/widgets/markdown/diagram/diagnostics.py +101 -0
  451. chrys/app/tui/widgets/markdown/diagram/geometry.py +46 -0
  452. chrys/app/tui/widgets/markdown/diagram/layout.py +494 -0
  453. chrys/app/tui/widgets/markdown/diagram/messages.py +18 -0
  454. chrys/app/tui/widgets/markdown/diagram/model.py +275 -0
  455. chrys/app/tui/widgets/markdown/diagram/parser.py +173 -0
  456. chrys/app/tui/widgets/markdown/diagram/parsers/__init__.py +3 -0
  457. chrys/app/tui/widgets/markdown/diagram/parsers/charts.py +349 -0
  458. chrys/app/tui/widgets/markdown/diagram/parsers/common.py +403 -0
  459. chrys/app/tui/widgets/markdown/diagram/parsers/graphs.py +1072 -0
  460. chrys/app/tui/widgets/markdown/diagram/parsers/planning.py +317 -0
  461. chrys/app/tui/widgets/markdown/diagram/parsers/schedule.py +298 -0
  462. chrys/app/tui/widgets/markdown/diagram/parsers/schemas.py +241 -0
  463. chrys/app/tui/widgets/markdown/diagram/parsers/structure.py +221 -0
  464. chrys/app/tui/widgets/markdown/diagram/presentation.py +32 -0
  465. chrys/app/tui/widgets/markdown/diagram/renderers/__init__.py +3 -0
  466. chrys/app/tui/widgets/markdown/diagram/renderers/charts.py +275 -0
  467. chrys/app/tui/widgets/markdown/diagram/renderers/common.py +73 -0
  468. chrys/app/tui/widgets/markdown/diagram/renderers/planning.py +157 -0
  469. chrys/app/tui/widgets/markdown/diagram/renderers/schedule.py +202 -0
  470. chrys/app/tui/widgets/markdown/diagram/renderers/structure.py +77 -0
  471. chrys/app/tui/widgets/markdown/diagram/renderers/xy.py +385 -0
  472. chrys/app/tui/widgets/markdown/diagram/router.py +651 -0
  473. chrys/app/tui/widgets/markdown/diagram/specs/__init__.py +3 -0
  474. chrys/app/tui/widgets/markdown/diagram/specs/charts.py +94 -0
  475. chrys/app/tui/widgets/markdown/diagram/specs/planning.py +68 -0
  476. chrys/app/tui/widgets/markdown/diagram/specs/schedule.py +65 -0
  477. chrys/app/tui/widgets/markdown/diagram/specs/structure.py +23 -0
  478. chrys/app/tui/widgets/markdown/diagram/viewer.py +92 -0
  479. chrys/app/tui/widgets/markdown/links.py +47 -0
  480. chrys/app/tui/widgets/markdown/math/__init__.py +36 -0
  481. chrys/app/tui/widgets/markdown/math/layout.py +739 -0
  482. chrys/app/tui/widgets/markdown/math/markdown.py +291 -0
  483. chrys/app/tui/widgets/markdown/math/parser.py +325 -0
  484. chrys/app/tui/widgets/markdown/math/symbols.py +325 -0
  485. chrys/app/tui/widgets/markdown/navigator.py +80 -0
  486. chrys/app/tui/widgets/markdown/parser.py +1057 -0
  487. chrys/app/tui/widgets/markdown/stream.py +73 -0
  488. chrys/app/tui/widgets/markdown/toc.py +98 -0
  489. chrys/app/tui/widgets/markdown/viewer.py +146 -0
  490. chrys/app/tui/widgets/markdown/widget.py +2046 -0
  491. chrys/app/tui/widgets/marquee.py +101 -0
  492. chrys/app/tui/widgets/option_menu.py +82 -0
  493. chrys/app/tui/widgets/page_navigator.py +118 -0
  494. chrys/app/tui/widgets/select.py +196 -0
  495. chrys/app/tui/widgets/selection.py +59 -0
  496. chrys/app/tui/widgets/sidebar/__init__.py +3 -0
  497. chrys/app/tui/widgets/sidebar/buddy.py +367 -0
  498. chrys/app/tui/widgets/sidebar/context.py +482 -0
  499. chrys/app/tui/widgets/sidebar/debug.py +195 -0
  500. chrys/app/tui/widgets/sidebar/empty_state.py +11 -0
  501. chrys/app/tui/widgets/sidebar/panel.py +198 -0
  502. chrys/app/tui/widgets/sidebar/tasks.py +198 -0
  503. chrys/app/tui/widgets/sidebar/toc.py +166 -0
  504. chrys/app/tui/widgets/sidebar/toc_types.py +30 -0
  505. chrys/app/tui/widgets/syntax_theme.py +85 -0
  506. chrys/app/tui/widgets/text_area.py +258 -0
  507. chrys/app/tui/widgets/trajectory/__init__.py +8 -0
  508. chrys/app/tui/widgets/trajectory/chartkit.py +264 -0
  509. chrys/app/tui/widgets/trajectory/insights.py +1266 -0
  510. chrys/app/tui/widgets/trajectory/overview.py +1101 -0
  511. chrys/app/tui/widgets/trajectory/panel.py +983 -0
  512. chrys/app/tui/widgets/trajectory/presentation.py +311 -0
  513. chrys/app/tui/widgets/trajectory/session_info.py +124 -0
  514. chrys/app/tui/widgets/trajectory/text_view.py +124 -0
  515. chrys/app/tui/widgets/trajectory/timeline.py +380 -0
  516. chrys/app/tui/widgets/welcome.py +126 -0
  517. chrys/app/tui/widgets/workflow/__init__.py +5 -0
  518. chrys/app/tui/widgets/workflow/edges.py +89 -0
  519. chrys/app/tui/widgets/workflow/graph.py +869 -0
  520. chrys/app/tui/widgets/workflow/info.py +310 -0
  521. chrys/app/tui/widgets/workflow/node_view.py +27 -0
  522. chrys/app/tui/widgets/workflow/output.py +225 -0
  523. chrys/app/tui/widgets/workflow/panel.py +658 -0
  524. chrys/app/tui/widgets/workflow/projector.py +324 -0
  525. chrys/app/tui/widgets/workflow/records.py +69 -0
  526. chrys/app/tui/widgets/workflow/scrollbar.py +82 -0
  527. chrys/app/tui/widgets/workflow/selection.py +151 -0
  528. chrys/app/tui/widgets/workflow/source.py +65 -0
  529. chrys/app/tui/widgets/workflow/text.py +257 -0
  530. chrys/app/tui/widgets/workflow/values.py +317 -0
  531. chrys/foundation/__init__.py +0 -0
  532. chrys/foundation/branding.py +15 -0
  533. chrys/foundation/config/__init__.py +7 -0
  534. chrys/foundation/config/coercion.py +374 -0
  535. chrys/foundation/config/context.py +34 -0
  536. chrys/foundation/config/env_file.py +141 -0
  537. chrys/foundation/config/env_layers.py +370 -0
  538. chrys/foundation/config/migrations.py +521 -0
  539. chrys/foundation/config/notification_events.py +22 -0
  540. chrys/foundation/config/process_settings.py +332 -0
  541. chrys/foundation/config/runtime_pointer.py +133 -0
  542. chrys/foundation/config/settings.py +1952 -0
  543. chrys/foundation/config/settings_store.py +996 -0
  544. chrys/foundation/config/spec.py +396 -0
  545. chrys/foundation/config/user_settings.py +140 -0
  546. chrys/foundation/config/warnings.py +262 -0
  547. chrys/foundation/config/web_values.py +135 -0
  548. chrys/foundation/config/yaml_store.py +487 -0
  549. chrys/foundation/errors/__init__.py +68 -0
  550. chrys/foundation/errors/_legacy.py +117 -0
  551. chrys/foundation/errors/_text.py +13 -0
  552. chrys/foundation/errors/_walk.py +85 -0
  553. chrys/foundation/errors/classify.py +278 -0
  554. chrys/foundation/errors/display.py +274 -0
  555. chrys/foundation/errors/formatting.py +283 -0
  556. chrys/foundation/errors/kinds.py +67 -0
  557. chrys/foundation/errors/network.py +497 -0
  558. chrys/foundation/errors/provider.py +445 -0
  559. chrys/foundation/errors/route.py +85 -0
  560. chrys/foundation/events/__init__.py +8 -0
  561. chrys/foundation/events/bus.py +143 -0
  562. chrys/foundation/events/types.py +1629 -0
  563. chrys/foundation/events/workflow.py +38 -0
  564. chrys/foundation/hosted_tools.py +142 -0
  565. chrys/foundation/i18n/__init__.py +32 -0
  566. chrys/foundation/i18n/_catalogs/zh-Hans/LC_MESSAGES/chrys.mo +0 -0
  567. chrys/foundation/i18n/catalogs.py +42 -0
  568. chrys/foundation/i18n/formatting.py +202 -0
  569. chrys/foundation/i18n/locale.py +92 -0
  570. chrys/foundation/i18n/localizer.py +187 -0
  571. chrys/foundation/i18n/messages.py +268 -0
  572. chrys/foundation/io/__init__.py +3 -0
  573. chrys/foundation/io/result_content.py +237 -0
  574. chrys/foundation/models/__init__.py +3 -0
  575. chrys/foundation/models/approval_reuse.py +23 -0
  576. chrys/foundation/models/ask_user.py +468 -0
  577. chrys/foundation/models/execution.py +18 -0
  578. chrys/foundation/models/history_markers.py +115 -0
  579. chrys/foundation/models/invocations.py +63 -0
  580. chrys/foundation/models/mutation_scope.py +28 -0
  581. chrys/foundation/models/session_env.py +87 -0
  582. chrys/foundation/models/session_surface.py +29 -0
  583. chrys/foundation/models/todos.py +74 -0
  584. chrys/foundation/models/turns.py +125 -0
  585. chrys/foundation/models/workflow_session.py +89 -0
  586. chrys/foundation/models/workspace.py +59 -0
  587. chrys/foundation/net/__init__.py +3 -0
  588. chrys/foundation/net/address_class.py +57 -0
  589. chrys/foundation/net/route_probe.py +94 -0
  590. chrys/foundation/net/url.py +107 -0
  591. chrys/foundation/observability/__init__.py +3 -0
  592. chrys/foundation/observability/exporters.py +174 -0
  593. chrys/foundation/observability/gate.py +31 -0
  594. chrys/foundation/observability/setup.py +497 -0
  595. chrys/foundation/observability/sink.py +228 -0
  596. chrys/foundation/patches/__init__.py +19 -0
  597. chrys/foundation/patches/mdurl_cache.py +59 -0
  598. chrys/foundation/patches/patcher.py +287 -0
  599. chrys/foundation/patches/staged_members.py +205 -0
  600. chrys/foundation/patches/textual_block_border.py +105 -0
  601. chrys/foundation/patches/textual_button_ansi.py +65 -0
  602. chrys/foundation/patches/textual_button_ansi_old.txt +68 -0
  603. chrys/foundation/patches/textual_callback_cache.py +248 -0
  604. chrys/foundation/patches/textual_callback_dispatch.py +109 -0
  605. chrys/foundation/patches/textual_compositor_cjk.py +566 -0
  606. chrys/foundation/patches/textual_dispatch_cache.py +150 -0
  607. chrys/foundation/patches/textual_ime_cursor_anchor.py +92 -0
  608. chrys/foundation/patches/textual_kitty_keyboard.py +268 -0
  609. chrys/foundation/patches/textual_lru_acyclic.py +241 -0
  610. chrys/foundation/patches/textual_message_pump.py +136 -0
  611. chrys/foundation/patches/textual_node_diet.py +875 -0
  612. chrys/foundation/patches/textual_one_shot_timer.py +48 -0
  613. chrys/foundation/patches/textual_option_list.py +90 -0
  614. chrys/foundation/patches/textual_precompose.py +89 -0
  615. chrys/foundation/patches/textual_pruned_tabs.py +69 -0
  616. chrys/foundation/patches/textual_reflow_reuse.py +1753 -0
  617. chrys/foundation/patches/textual_removed_node_caches.py +86 -0
  618. chrys/foundation/patches/textual_removed_screen_callbacks.py +55 -0
  619. chrys/foundation/patches/textual_selection_extract.py +48 -0
  620. chrys/foundation/patches/textual_strip_cycles.py +119 -0
  621. chrys/foundation/patches/textual_tab_selection.py +1611 -0
  622. chrys/foundation/patches/textual_timer_skip.py +70 -0
  623. chrys/foundation/patches/textual_utf8_decoder.py +106 -0
  624. chrys/foundation/patches/textual_win_sleep.py +167 -0
  625. chrys/foundation/patches/watchdog_fsevents.py +70 -0
  626. chrys/foundation/patches/watchdog_windows.py +52 -0
  627. chrys/foundation/platform/__init__.py +556 -0
  628. chrys/foundation/platform/c_api.py +32 -0
  629. chrys/foundation/platform/child_reap.py +107 -0
  630. chrys/foundation/platform/command_line.py +95 -0
  631. chrys/foundation/platform/files.py +1470 -0
  632. chrys/foundation/platform/output_capture.py +113 -0
  633. chrys/foundation/platform/paths.py +193 -0
  634. chrys/foundation/platform/process.py +1468 -0
  635. chrys/foundation/platform/pty_output.py +19 -0
  636. chrys/foundation/platform/runtime_paths.py +281 -0
  637. chrys/foundation/platform/win_console.py +129 -0
  638. chrys/foundation/reasoning_origin.py +67 -0
  639. chrys/foundation/recovery.py +16 -0
  640. chrys/foundation/retry.py +278 -0
  641. chrys/foundation/text/__init__.py +3 -0
  642. chrys/foundation/text/encoding.py +603 -0
  643. chrys/foundation/text/images.py +304 -0
  644. chrys/foundation/text/lines.py +24 -0
  645. chrys/foundation/text/mentions.py +90 -0
  646. chrys/foundation/text/model_json.py +25 -0
  647. chrys/foundation/text/reading_order.py +20 -0
  648. chrys/foundation/text/tokenizer.py +25 -0
  649. chrys/foundation/text/tool_output.py +287 -0
  650. chrys/foundation/text/yaml_io.py +62 -0
  651. chrys/foundation/tool_call_context.py +202 -0
  652. chrys/foundation/tool_execution_stamp.py +184 -0
  653. chrys/foundation/tool_invocation_order.py +48 -0
  654. chrys/foundation/tool_kinds.py +102 -0
  655. chrys/foundation/tool_result_metadata.py +287 -0
  656. chrys/foundation/trajectory/__init__.py +10 -0
  657. chrys/foundation/trajectory/context.py +481 -0
  658. chrys/foundation/trajectory/envelope.py +571 -0
  659. chrys/foundation/trajectory/event_types.py +294 -0
  660. chrys/foundation/trajectory/finalizers.py +46 -0
  661. chrys/foundation/trajectory/fingerprint.py +62 -0
  662. chrys/foundation/trajectory/ids.py +155 -0
  663. chrys/foundation/trajectory/keys.py +99 -0
  664. chrys/foundation/trajectory/lease.py +173 -0
  665. chrys/foundation/trajectory/metadata.py +72 -0
  666. chrys/foundation/trajectory/reader.py +271 -0
  667. chrys/foundation/trajectory/recovery.py +225 -0
  668. chrys/foundation/trajectory/revisions.py +152 -0
  669. chrys/foundation/trajectory/segments.py +225 -0
  670. chrys/foundation/trajectory/tools.py +117 -0
  671. chrys/foundation/trajectory/usage.py +151 -0
  672. chrys/foundation/trajectory/writer.py +972 -0
  673. chrys/foundation/trajectory_timing.py +97 -0
  674. chrys/foundation/util/__init__.py +3 -0
  675. chrys/foundation/util/async_tasks.py +29 -0
  676. chrys/foundation/util/chrys_headers.py +23 -0
  677. chrys/foundation/util/env_templates.py +104 -0
  678. chrys/foundation/util/filenames.py +96 -0
  679. chrys/foundation/util/header_charset.py +138 -0
  680. chrys/foundation/util/httpx_helpers.py +12 -0
  681. chrys/foundation/util/lock.py +236 -0
  682. chrys/foundation/util/once_close.py +46 -0
  683. chrys/foundation/util/session_ids.py +29 -0
  684. chrys/foundation/util/shell_tokens.py +88 -0
  685. chrys/foundation/util/sub_agent_context.py +38 -0
  686. chrys/foundation/util/time.py +35 -0
  687. chrys/foundation/util/unicode_scalars.py +36 -0
  688. chrys/foundation/vendor/__init__.py +43 -0
  689. chrys/foundation/vendor/ripgrep/rg-aarch64-apple-darwin +0 -0
  690. chrys/foundation/vendor/ripgrep/rg-aarch64-pc-windows-msvc.exe +0 -0
  691. chrys/foundation/vendor/ripgrep/rg-aarch64-unknown-linux-gnu +0 -0
  692. chrys/foundation/vendor/ripgrep/rg-x86_64-apple-darwin +0 -0
  693. chrys/foundation/vendor/ripgrep/rg-x86_64-pc-windows-msvc.exe +0 -0
  694. chrys/foundation/vendor/ripgrep/rg-x86_64-unknown-linux-musl +0 -0
  695. chrys/kernel/__init__.py +256 -0
  696. chrys/kernel/_content.py +1866 -0
  697. chrys/kernel/_loop_recorder.py +631 -0
  698. chrys/kernel/_null_overlay.py +1026 -0
  699. chrys/kernel/_result_ceiling.py +216 -0
  700. chrys/kernel/_serialization.py +431 -0
  701. chrys/kernel/_tool_arg_errors.py +1629 -0
  702. chrys/kernel/_tool_execution.py +1172 -0
  703. chrys/kernel/_tool_expansion.py +26 -0
  704. chrys/kernel/_types.py +2598 -0
  705. chrys/kernel/agent.py +994 -0
  706. chrys/kernel/client.py +1084 -0
  707. chrys/kernel/compaction.py +1315 -0
  708. chrys/kernel/exceptions.py +211 -0
  709. chrys/kernel/exchanges.py +683 -0
  710. chrys/kernel/identity.py +95 -0
  711. chrys/kernel/images.py +36 -0
  712. chrys/kernel/instrumentation.py +1618 -0
  713. chrys/kernel/loop.py +2308 -0
  714. chrys/kernel/middleware.py +616 -0
  715. chrys/kernel/sessions.py +604 -0
  716. chrys/kernel/tools.py +1061 -0
  717. chrys/kernel/types.py +126 -0
  718. chrys/orchestration/__init__.py +3 -0
  719. chrys/orchestration/engine/__init__.py +3 -0
  720. chrys/orchestration/engine/assembly.py +236 -0
  721. chrys/orchestration/engine/build/__init__.py +3 -0
  722. chrys/orchestration/engine/build/builder.py +1061 -0
  723. chrys/orchestration/engine/build/construction.py +408 -0
  724. chrys/orchestration/engine/build/loaded.py +139 -0
  725. chrys/orchestration/engine/engine.py +789 -0
  726. chrys/orchestration/engine/execution.py +771 -0
  727. chrys/orchestration/engine/loader.py +865 -0
  728. chrys/orchestration/engine/rollback.py +918 -0
  729. chrys/orchestration/engine/run/__init__.py +3 -0
  730. chrys/orchestration/engine/run/active_injection.py +419 -0
  731. chrys/orchestration/engine/run/attachments.py +519 -0
  732. chrys/orchestration/engine/run/bindings.py +1095 -0
  733. chrys/orchestration/engine/run/coordinator.py +1062 -0
  734. chrys/orchestration/engine/run/finalizer.py +417 -0
  735. chrys/orchestration/engine/run/input_refs.py +60 -0
  736. chrys/orchestration/engine/run/prompt_content.py +457 -0
  737. chrys/orchestration/engine/run/resume.py +299 -0
  738. chrys/orchestration/engine/run/retry.py +948 -0
  739. chrys/orchestration/engine/run/runner.py +625 -0
  740. chrys/orchestration/engine/run/runtime_skills.py +146 -0
  741. chrys/orchestration/engine/run/sub_agent_coordination.py +126 -0
  742. chrys/orchestration/engine/run/turn_hooks.py +258 -0
  743. chrys/orchestration/engine/run/turn_state.py +149 -0
  744. chrys/orchestration/engine/run/working_dir.py +50 -0
  745. chrys/orchestration/engine/session_lifecycle.py +2013 -0
  746. chrys/orchestration/engine/state/__init__.py +3 -0
  747. chrys/orchestration/engine/state/active_session.py +183 -0
  748. chrys/orchestration/engine/state/controls.py +639 -0
  749. chrys/orchestration/engine/state/current_agent.py +26 -0
  750. chrys/orchestration/engine/state/lifecycle_permits.py +395 -0
  751. chrys/orchestration/engine/state/machine.py +193 -0
  752. chrys/orchestration/engine/state/session_writer.py +434 -0
  753. chrys/orchestration/engine/trajectory.py +714 -0
  754. chrys/orchestration/engine/usage.py +118 -0
  755. chrys/orchestration/invoker/__init__.py +3 -0
  756. chrys/orchestration/invoker/acp.py +727 -0
  757. chrys/orchestration/invoker/acp_protocol.py +1115 -0
  758. chrys/orchestration/invoker/acp_spec.py +79 -0
  759. chrys/orchestration/invoker/attempts.py +994 -0
  760. chrys/orchestration/invoker/child_compaction.py +131 -0
  761. chrys/orchestration/invoker/child_history.py +99 -0
  762. chrys/orchestration/invoker/contracts.py +253 -0
  763. chrys/orchestration/invoker/evidence.py +94 -0
  764. chrys/orchestration/invoker/kernel.py +343 -0
  765. chrys/orchestration/invoker/origin.py +79 -0
  766. chrys/orchestration/invoker/resources.py +343 -0
  767. chrys/orchestration/invoker/runtime.py +299 -0
  768. chrys/orchestration/invoker/usage.py +80 -0
  769. chrys/orchestration/session_hooks.py +117 -0
  770. chrys/orchestration/session_host.py +1023 -0
  771. chrys/orchestration/session_resources.py +95 -0
  772. chrys/orchestration/session_usage.py +219 -0
  773. chrys/orchestration/startup.py +255 -0
  774. chrys/orchestration/sub_agents/__init__.py +3 -0
  775. chrys/orchestration/sub_agents/acp_policy.py +269 -0
  776. chrys/orchestration/sub_agents/kernel_policy.py +848 -0
  777. chrys/orchestration/sub_agents/registration.py +44 -0
  778. chrys/orchestration/sub_agents/shell.py +337 -0
  779. chrys/orchestration/sub_agents/tools.py +2228 -0
  780. chrys/orchestration/workflows/__init__.py +3 -0
  781. chrys/orchestration/workflows/agent_archive.py +156 -0
  782. chrys/orchestration/workflows/agent_node.py +894 -0
  783. chrys/orchestration/workflows/agent_node_build.py +687 -0
  784. chrys/orchestration/workflows/catalog.py +332 -0
  785. chrys/orchestration/workflows/coordinator.py +771 -0
  786. chrys/orchestration/workflows/hooks.py +176 -0
  787. chrys/orchestration/workflows/preview.py +322 -0
  788. chrys/orchestration/workflows/rollback.py +151 -0
  789. chrys/orchestration/workflows/runner.py +986 -0
  790. chrys/orchestration/workflows/session.py +390 -0
  791. chrys/orchestration/workflows/settings.py +97 -0
  792. chrys/orchestration/workflows/validation.py +674 -0
  793. chrys/orchestration/workflows/worker_client.py +1002 -0
  794. chrys/service/__init__.py +3 -0
  795. chrys/service/acp_client/__init__.py +65 -0
  796. chrys/service/acp_client/_transport.py +922 -0
  797. chrys/service/acp_client/client.py +1311 -0
  798. chrys/service/acp_client/errors.py +224 -0
  799. chrys/service/acp_client/protocol.py +220 -0
  800. chrys/service/acp_client/spawn.py +239 -0
  801. chrys/service/acp_client/spec.py +100 -0
  802. chrys/service/agent_middleware/__init__.py +60 -0
  803. chrys/service/agent_middleware/_metadata_keys.py +52 -0
  804. chrys/service/agent_middleware/control/__init__.py +3 -0
  805. chrys/service/agent_middleware/control/approval.py +769 -0
  806. chrys/service/agent_middleware/control/approval_hooks.py +62 -0
  807. chrys/service/agent_middleware/control/ask_user.py +207 -0
  808. chrys/service/agent_middleware/control/interrupt.py +50 -0
  809. chrys/service/agent_middleware/control/sleep.py +118 -0
  810. chrys/service/agent_middleware/control/todo.py +117 -0
  811. chrys/service/agent_middleware/events/__init__.py +3 -0
  812. chrys/service/agent_middleware/events/hook_dispatch.py +250 -0
  813. chrys/service/agent_middleware/events/hosted_tools.py +1762 -0
  814. chrys/service/agent_middleware/events/intermediate_text.py +128 -0
  815. chrys/service/agent_middleware/events/rejection_metadata.py +50 -0
  816. chrys/service/agent_middleware/events/result_persistence.py +98 -0
  817. chrys/service/agent_middleware/events/sub_agent_events.py +951 -0
  818. chrys/service/agent_middleware/events/tool_error_text.py +22 -0
  819. chrys/service/agent_middleware/events/tool_events.py +779 -0
  820. chrys/service/agent_middleware/injection.py +510 -0
  821. chrys/service/agent_middleware/reminders/__init__.py +91 -0
  822. chrys/service/agent_middleware/reminders/archive_pointer.py +154 -0
  823. chrys/service/agent_middleware/reminders/base.py +25 -0
  824. chrys/service/agent_middleware/reminders/context_usage.py +74 -0
  825. chrys/service/agent_middleware/reminders/file_change.py +63 -0
  826. chrys/service/agent_middleware/reminders/mcp.py +39 -0
  827. chrys/service/agent_middleware/reminders/profile_switch.py +106 -0
  828. chrys/service/agent_middleware/reminders/runtime_env.py +190 -0
  829. chrys/service/agent_middleware/reminders/skills.py +44 -0
  830. chrys/service/agent_middleware/reminders/sub_agents.py +35 -0
  831. chrys/service/agent_middleware/reminders/todo.py +35 -0
  832. chrys/service/agent_middleware/reminders/turn_line.py +48 -0
  833. chrys/service/agent_middleware/response_validation.py +1380 -0
  834. chrys/service/agent_middleware/system_reminder.py +1107 -0
  835. chrys/service/agent_middleware/validators.py +396 -0
  836. chrys/service/analytics/__init__.py +124 -0
  837. chrys/service/analytics/_actions.py +896 -0
  838. chrys/service/analytics/_context_evidence.py +357 -0
  839. chrys/service/analytics/_critical_path.py +179 -0
  840. chrys/service/analytics/_facts.py +919 -0
  841. chrys/service/analytics/_insights.py +769 -0
  842. chrys/service/analytics/_metric_ops.py +83 -0
  843. chrys/service/analytics/_session_projection.py +460 -0
  844. chrys/service/analytics/_timeline.py +434 -0
  845. chrys/service/analytics/_turn_graph.py +1237 -0
  846. chrys/service/analytics/_turns.py +1377 -0
  847. chrys/service/analytics/_workflows.py +171 -0
  848. chrys/service/analytics/aggregation.py +1474 -0
  849. chrys/service/analytics/classification.py +128 -0
  850. chrys/service/analytics/export.py +803 -0
  851. chrys/service/analytics/findings.py +282 -0
  852. chrys/service/analytics/math.py +79 -0
  853. chrys/service/analytics/model.py +886 -0
  854. chrys/service/analytics/reader.py +321 -0
  855. chrys/service/approval/__init__.py +3 -0
  856. chrys/service/approval/arbitration.py +102 -0
  857. chrys/service/approval/command_identity.py +109 -0
  858. chrys/service/approval/correlation.py +57 -0
  859. chrys/service/approval/grant_store.py +108 -0
  860. chrys/service/approval/judge.py +589 -0
  861. chrys/service/approval/level.py +15 -0
  862. chrys/service/approval/policy.py +209 -0
  863. chrys/service/approval/reuse.py +223 -0
  864. chrys/service/approval/reuse_binding.py +133 -0
  865. chrys/service/approval/safety_classifier.py +536 -0
  866. chrys/service/approval/turn_context.py +49 -0
  867. chrys/service/context/__init__.py +3 -0
  868. chrys/service/context/compaction/__init__.py +38 -0
  869. chrys/service/context/compaction/breaker.py +156 -0
  870. chrys/service/context/compaction/budgets.py +93 -0
  871. chrys/service/context/compaction/calibration.py +77 -0
  872. chrys/service/context/compaction/compression.py +690 -0
  873. chrys/service/context/compaction/current_turn_drop.py +458 -0
  874. chrys/service/context/compaction/events.py +201 -0
  875. chrys/service/context/compaction/exclusions.py +261 -0
  876. chrys/service/context/compaction/groups.py +361 -0
  877. chrys/service/context/compaction/last_words.py +1996 -0
  878. chrys/service/context/compaction/last_words_state.py +752 -0
  879. chrys/service/context/compaction/scoped.py +472 -0
  880. chrys/service/context/compaction/spill.py +1947 -0
  881. chrys/service/context/compaction/strategy.py +1116 -0
  882. chrys/service/context/compaction/summaries.py +319 -0
  883. chrys/service/context/compaction/turns.py +375 -0
  884. chrys/service/context/manager.py +174 -0
  885. chrys/service/context/memory_loader.py +792 -0
  886. chrys/service/context/middleware/__init__.py +3 -0
  887. chrys/service/context/middleware/usage.py +332 -0
  888. chrys/service/context/providers/__init__.py +3 -0
  889. chrys/service/context/providers/context_management.py +432 -0
  890. chrys/service/context/providers/history.py +647 -0
  891. chrys/service/context/providers/memory.py +50 -0
  892. chrys/service/context/tool_content.py +22 -0
  893. chrys/service/hooks/__init__.py +37 -0
  894. chrys/service/hooks/detached_worker.py +118 -0
  895. chrys/service/hooks/docs/README.md +14 -0
  896. chrys/service/hooks/docs/authoring.md +684 -0
  897. chrys/service/hooks/docs/configuration.md +499 -0
  898. chrys/service/hooks/docs/design.md +359 -0
  899. chrys/service/hooks/docs/project-hooks.md +348 -0
  900. chrys/service/hooks/events.py +157 -0
  901. chrys/service/hooks/loader.py +665 -0
  902. chrys/service/hooks/manager.py +807 -0
  903. chrys/service/hooks/matcher.py +111 -0
  904. chrys/service/hooks/outbox.py +294 -0
  905. chrys/service/hooks/runner.py +572 -0
  906. chrys/service/hooks/schema.py +407 -0
  907. chrys/service/llm/__init__.py +7 -0
  908. chrys/service/llm/_structured_outputs.py +192 -0
  909. chrys/service/llm/anthropic_messages/__init__.py +9 -0
  910. chrys/service/llm/anthropic_messages/client.py +254 -0
  911. chrys/service/llm/anthropic_messages/decode.py +331 -0
  912. chrys/service/llm/anthropic_messages/history.py +279 -0
  913. chrys/service/llm/anthropic_messages/request.py +323 -0
  914. chrys/service/llm/anthropic_messages/server_tools.py +319 -0
  915. chrys/service/llm/anthropic_messages/stream.py +297 -0
  916. chrys/service/llm/anthropic_messages/thinking_binding.py +156 -0
  917. chrys/service/llm/chat_completions/__init__.py +9 -0
  918. chrys/service/llm/chat_completions/client.py +369 -0
  919. chrys/service/llm/chat_completions/decode.py +338 -0
  920. chrys/service/llm/chat_completions/history.py +657 -0
  921. chrys/service/llm/chat_completions/reasoning.py +244 -0
  922. chrys/service/llm/chat_completions/request.py +192 -0
  923. chrys/service/llm/chat_completions/stream.py +546 -0
  924. chrys/service/llm/chat_completions/validation.py +308 -0
  925. chrys/service/llm/clients.py +613 -0
  926. chrys/service/llm/images.py +65 -0
  927. chrys/service/llm/mock.py +413 -0
  928. chrys/service/llm/observer.py +446 -0
  929. chrys/service/llm/one_shot.py +106 -0
  930. chrys/service/llm/openai_exceptions.py +93 -0
  931. chrys/service/llm/openai_responses/__init__.py +10 -0
  932. chrys/service/llm/openai_responses/client.py +323 -0
  933. chrys/service/llm/openai_responses/decode.py +432 -0
  934. chrys/service/llm/openai_responses/history.py +519 -0
  935. chrys/service/llm/openai_responses/hosted.py +625 -0
  936. chrys/service/llm/openai_responses/replay.py +629 -0
  937. chrys/service/llm/openai_responses/request.py +342 -0
  938. chrys/service/llm/openai_responses/stream.py +847 -0
  939. chrys/service/llm/openai_timestamps.py +57 -0
  940. chrys/service/llm/providers.py +107 -0
  941. chrys/service/llm/proxy_route.py +54 -0
  942. chrys/service/llm/raw_http_log.py +309 -0
  943. chrys/service/llm/route_facts.py +63 -0
  944. chrys/service/llm/route_sessions.py +49 -0
  945. chrys/service/llm/sdk_anthropic.py +18 -0
  946. chrys/service/llm/sdk_headers.py +47 -0
  947. chrys/service/llm/sdk_openai.py +18 -0
  948. chrys/service/llm/sdk_retry.py +59 -0
  949. chrys/service/llm/wire_client.py +246 -0
  950. chrys/service/mcp/__init__.py +3 -0
  951. chrys/service/mcp/_connection.py +235 -0
  952. chrys/service/mcp/_http_transport.py +350 -0
  953. chrys/service/mcp/_progressive.py +459 -0
  954. chrys/service/mcp/_stdio_transport.py +614 -0
  955. chrys/service/mcp/_tool_mixins.py +96 -0
  956. chrys/service/mcp/adapter.py +1031 -0
  957. chrys/service/mcp/cache.py +476 -0
  958. chrys/service/mcp/content.py +32 -0
  959. chrys/service/mcp/errors.py +169 -0
  960. chrys/service/mcp/owned.py +1330 -0
  961. chrys/service/mcp/result_limits.py +168 -0
  962. chrys/service/mcp/thinking_warning.py +95 -0
  963. chrys/service/mcp/validation.py +74 -0
  964. chrys/service/mutations/__init__.py +84 -0
  965. chrys/service/mutations/coordination.py +894 -0
  966. chrys/service/mutations/detector.py +458 -0
  967. chrys/service/mutations/git_calibrator.py +397 -0
  968. chrys/service/mutations/git_state.py +647 -0
  969. chrys/service/mutations/pipeline.py +779 -0
  970. chrys/service/mutations/scanner.py +466 -0
  971. chrys/service/mutations/snapshot_files.py +233 -0
  972. chrys/service/mutations/store.py +625 -0
  973. chrys/service/mutations/tool_names.py +15 -0
  974. chrys/service/mutations/trace.py +246 -0
  975. chrys/service/mutations/tracker.py +1289 -0
  976. chrys/service/mutations/types.py +559 -0
  977. chrys/service/mutations/workspace_changes.py +1583 -0
  978. chrys/service/profiles/__init__.py +2 -0
  979. chrys/service/profiles/agents/__init__.py +3 -0
  980. chrys/service/profiles/agents/builtins/Code.yaml +199 -0
  981. chrys/service/profiles/agents/builtins/Explore.yaml +62 -0
  982. chrys/service/profiles/agents/builtins/General.yaml +114 -0
  983. chrys/service/profiles/agents/builtins/QA.yaml +128 -0
  984. chrys/service/profiles/agents/loader.py +808 -0
  985. chrys/service/profiles/agents/registry.py +727 -0
  986. chrys/service/profiles/agents/schema.py +371 -0
  987. chrys/service/profiles/agents/serializer.py +196 -0
  988. chrys/service/profiles/coercion.py +22 -0
  989. chrys/service/profiles/models/__init__.py +3 -0
  990. chrys/service/profiles/models/env_bridge.py +157 -0
  991. chrys/service/profiles/models/loader.py +282 -0
  992. chrys/service/profiles/models/options.py +438 -0
  993. chrys/service/profiles/models/registry.py +104 -0
  994. chrys/service/profiles/models/resolver.py +286 -0
  995. chrys/service/profiles/models/schema.py +91 -0
  996. chrys/service/profiles/models/serializer.py +76 -0
  997. chrys/service/session/__init__.py +3 -0
  998. chrys/service/session/checkpoint.py +156 -0
  999. chrys/service/session/history.py +1616 -0
  1000. chrys/service/session/message_metadata.py +151 -0
  1001. chrys/service/session/persistence.py +435 -0
  1002. chrys/service/session/runtime_metadata.py +210 -0
  1003. chrys/service/session/sub_agent_logs.py +1069 -0
  1004. chrys/service/session/sub_agent_transcript.py +654 -0
  1005. chrys/service/skills/__init__.py +3 -0
  1006. chrys/service/skills/adapter.py +408 -0
  1007. chrys/service/skills/constants.py +8 -0
  1008. chrys/service/skills/loader.py +575 -0
  1009. chrys/service/skills/model.py +130 -0
  1010. chrys/service/skills/provider.py +841 -0
  1011. chrys/service/skills/runner.py +390 -0
  1012. chrys/service/state/__init__.py +3 -0
  1013. chrys/service/state/_fork.py +710 -0
  1014. chrys/service/state/_session_files.py +175 -0
  1015. chrys/service/state/_session_meta.py +552 -0
  1016. chrys/service/state/locks.py +154 -0
  1017. chrys/service/state/serializers.py +156 -0
  1018. chrys/service/state/session_catalog.py +361 -0
  1019. chrys/service/state/session_listing.py +84 -0
  1020. chrys/service/state/session_migration.py +446 -0
  1021. chrys/service/state/session_mru.py +315 -0
  1022. chrys/service/state/store.py +2132 -0
  1023. chrys/service/state/workflow.py +158 -0
  1024. chrys/service/text_blocks.py +47 -0
  1025. chrys/service/todos/__init__.py +3 -0
  1026. chrys/service/todos/reminder.py +31 -0
  1027. chrys/service/todos/tracker.py +58 -0
  1028. chrys/service/tools/__init__.py +3 -0
  1029. chrys/service/tools/approval_targets.py +92 -0
  1030. chrys/service/tools/builtins/__init__.py +3 -0
  1031. chrys/service/tools/builtins/ask_user.py +326 -0
  1032. chrys/service/tools/builtins/doc_converter/__init__.py +494 -0
  1033. chrys/service/tools/builtins/doc_converter/artifacts.py +392 -0
  1034. chrys/service/tools/builtins/doc_converter/parsers/__init__.py +3 -0
  1035. chrys/service/tools/builtins/doc_converter/parsers/_table.py +34 -0
  1036. chrys/service/tools/builtins/doc_converter/parsers/base.py +101 -0
  1037. chrys/service/tools/builtins/doc_converter/parsers/docx.py +88 -0
  1038. chrys/service/tools/builtins/doc_converter/parsers/pdf.py +144 -0
  1039. chrys/service/tools/builtins/doc_converter/parsers/pptx.py +103 -0
  1040. chrys/service/tools/builtins/doc_converter/parsers/xls.py +32 -0
  1041. chrys/service/tools/builtins/doc_converter/parsers/xlsx.py +51 -0
  1042. chrys/service/tools/builtins/doc_converter/registry.py +55 -0
  1043. chrys/service/tools/builtins/filesystem.py +1071 -0
  1044. chrys/service/tools/builtins/search.py +1265 -0
  1045. chrys/service/tools/builtins/shell.py +967 -0
  1046. chrys/service/tools/builtins/shell_command_filtering/__init__.py +5 -0
  1047. chrys/service/tools/builtins/shell_command_filtering/policy.py +60 -0
  1048. chrys/service/tools/builtins/shell_command_filtering/presets.py +152 -0
  1049. chrys/service/tools/builtins/shell_command_filtering/readonly/__init__.py +358 -0
  1050. chrys/service/tools/builtins/shell_command_filtering/readonly/commands.py +416 -0
  1051. chrys/service/tools/builtins/shell_command_filtering/readonly/git.py +408 -0
  1052. chrys/service/tools/builtins/shell_command_filtering/readonly/syntax.py +87 -0
  1053. chrys/service/tools/builtins/shell_command_filtering/tokenizer.py +214 -0
  1054. chrys/service/tools/builtins/shell_filter.py +24 -0
  1055. chrys/service/tools/builtins/sleep.py +91 -0
  1056. chrys/service/tools/builtins/todo.py +64 -0
  1057. chrys/service/tools/builtins/web/__init__.py +6 -0
  1058. chrys/service/tools/builtins/web/build.py +268 -0
  1059. chrys/service/tools/builtins/web/config.py +338 -0
  1060. chrys/service/tools/builtins/web/credentials.py +37 -0
  1061. chrys/service/tools/builtins/web/fetch/__init__.py +3 -0
  1062. chrys/service/tools/builtins/web/fetch/cache.py +68 -0
  1063. chrys/service/tools/builtins/web/fetch/convert.py +56 -0
  1064. chrys/service/tools/builtins/web/fetch/tool.py +223 -0
  1065. chrys/service/tools/builtins/web/hosted.py +61 -0
  1066. chrys/service/tools/builtins/web/http.py +330 -0
  1067. chrys/service/tools/builtins/web/progress.py +18 -0
  1068. chrys/service/tools/builtins/web/search/__init__.py +3 -0
  1069. chrys/service/tools/builtins/web/search/exa_mcp.py +146 -0
  1070. chrys/service/tools/builtins/web/search/html.py +152 -0
  1071. chrys/service/tools/builtins/web/search/json_api.py +227 -0
  1072. chrys/service/tools/builtins/web/search/providers.py +77 -0
  1073. chrys/service/tools/builtins/web/search/tool.py +218 -0
  1074. chrys/service/tools/builtins/web/search/types.py +28 -0
  1075. chrys/service/tools/builtins/web/tls_identity.py +66 -0
  1076. chrys/service/tools/kinds.py +115 -0
  1077. chrys/service/tools/names.py +47 -0
  1078. chrys/service/tools/registry.py +242 -0
  1079. chrys/service/tools/result_metadata.py +103 -0
  1080. chrys/service/tools/session_artifacts.py +196 -0
  1081. chrys/service/tools/spill.py +218 -0
  1082. chrys/service/tools/workspace_paths.py +81 -0
  1083. chrys/service/trajectory/__init__.py +11 -0
  1084. chrys/service/trajectory/approvals.py +201 -0
  1085. chrys/service/trajectory/compaction.py +420 -0
  1086. chrys/service/trajectory/fork.py +75 -0
  1087. chrys/service/trajectory/hooks.py +217 -0
  1088. chrys/service/trajectory/items.py +41 -0
  1089. chrys/service/trajectory/preparation.py +374 -0
  1090. chrys/service/trajectory/retries.py +95 -0
  1091. chrys/service/trajectory/revisions.py +129 -0
  1092. chrys/service/trajectory/segmented.py +163 -0
  1093. chrys/service/trajectory/session.py +762 -0
  1094. chrys/service/trajectory/state.py +83 -0
  1095. chrys/service/trajectory/sub_agents.py +169 -0
  1096. chrys/service/trajectory/tombstone.py +262 -0
  1097. chrys/service/trajectory/tools.py +323 -0
  1098. chrys/service/trajectory/validation.py +130 -0
  1099. chrys/service/trajectory/waits.py +161 -0
  1100. chrys/service/trajectory/workflow.py +148 -0
  1101. chrys/service/vision.py +128 -0
  1102. chrys/service/workflows/__init__.py +3 -0
  1103. chrys/service/workflows/admission.py +151 -0
  1104. chrys/service/workflows/artifacts.py +109 -0
  1105. chrys/service/workflows/asks.py +130 -0
  1106. chrys/service/workflows/builtins/__init__.py +12 -0
  1107. chrys/service/workflows/builtins/demo-workflow.manifest.json +635 -0
  1108. chrys/service/workflows/builtins/demo-workflow.py +756 -0
  1109. chrys/service/workflows/discovery.py +554 -0
  1110. chrys/service/workflows/environment.py +284 -0
  1111. chrys/service/workflows/graph.py +430 -0
  1112. chrys/service/workflows/history.py +82 -0
  1113. chrys/service/workflows/interpreter.py +107 -0
  1114. chrys/service/workflows/journal.py +334 -0
  1115. chrys/service/workflows/layout.py +56 -0
  1116. chrys/service/workflows/ledger.py +149 -0
  1117. chrys/service/workflows/model_selection.py +18 -0
  1118. chrys/service/workflows/orphans.py +209 -0
  1119. chrys/service/workflows/outcomes.py +46 -0
  1120. chrys/service/workflows/protocol.py +210 -0
  1121. chrys/service/workflows/records.py +142 -0
  1122. chrys/service/workflows/scheduler.py +770 -0
  1123. chrys/service/workflows/sdk/__init__.py +51 -0
  1124. chrys/service/workflows/sdk/_ask.py +167 -0
  1125. chrys/service/workflows/sdk/_builder.py +781 -0
  1126. chrys/service/workflows/sdk/_values.py +92 -0
  1127. chrys/service/workflows/sdk_artifact.py +74 -0
  1128. chrys/service/workflows/store.py +494 -0
  1129. chrys/service/workflows/transcript.py +102 -0
  1130. chrys/service/workflows/values.py +73 -0
  1131. chrys/service/workflows/worker_host.py +1820 -0
  1132. chrys/workflows.py +38 -0
  1133. icode_tui-0.29.1.dist-info/METADATA +104 -0
  1134. icode_tui-0.29.1.dist-info/RECORD +1138 -0
  1135. icode_tui-0.29.1.dist-info/WHEEL +4 -0
  1136. icode_tui-0.29.1.dist-info/entry_points.txt +2 -0
  1137. icode_tui-0.29.1.dist-info/licenses/LICENSE +202 -0
  1138. icode_tui-0.29.1.dist-info/licenses/NOTICE +1955 -0
@@ -0,0 +1,2262 @@
1
+ # Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
2
+
3
+ """MCP configuration panel — composable widget for the MCP tab."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import asyncio
8
+ import contextlib
9
+ import logging
10
+ import shlex
11
+ import socket
12
+ from collections.abc import Callable, Collection, Sequence
13
+ from dataclasses import dataclass, field
14
+ from functools import partial
15
+ from typing import TYPE_CHECKING, Literal, TypeGuard
16
+
17
+ from rich.markup import escape
18
+ from textual import on, work
19
+ from textual.containers import Horizontal, Vertical, VerticalScroll
20
+ from textual.css.query import NoMatches
21
+ from textual.message import Message
22
+ from textual.widget import Widget
23
+ from textual.widgets import Button, Label
24
+
25
+ from chrys.app.tui.i18n import render_str, widget_locale_controller, widget_localizer
26
+ from chrys.app.tui.screens.agents.panels.config_card import CardListChanged, CollapsibleConfigCard
27
+ from chrys.app.tui.screens.agents.panels.markdown_report import (
28
+ NOT_ADVERTISED,
29
+ SUPPORTED_MARK,
30
+ UNSUPPORTED_MARK,
31
+ code_span,
32
+ definition_bullets,
33
+ inline_text,
34
+ md_table,
35
+ )
36
+ from chrys.app.tui.screens.agents.validation_messages import (
37
+ CONTEXT_ERROR,
38
+ DUPLICATE_KEY_ROW,
39
+ FIELD_REQUIRED,
40
+ MCP_COMMAND_REQUIRED,
41
+ MCP_DUPLICATE_SERVER,
42
+ MCP_INITIAL_TOOLS_PERMITTED,
43
+ MCP_INITIALLY_VISIBLE_TOOLS_VALIDATION,
44
+ MCP_INVALID_COMMAND,
45
+ MCP_ROW_KEY_REQUIRED,
46
+ MCP_ROW_VALUE_REQUIRED,
47
+ MCP_SELECT_PERMITTED,
48
+ MCP_SERVER_CONTEXT,
49
+ MCP_TIMEOUT_POSITIVE,
50
+ MCP_TIMEOUT_VALID,
51
+ MCP_TOOL_LIST_DUPLICATE,
52
+ MCP_TOOL_LIST_EMPTY,
53
+ MCP_URL_REQUIRED,
54
+ MCP_URL_SCHEME,
55
+ NAME_FIELD,
56
+ SERVER_ERROR,
57
+ TOOL_NAME_ITEM,
58
+ )
59
+ from chrys.app.tui.screens.agents.validation_messages import (
60
+ MCP_ENVIRONMENT_VARIABLES as _ENVIRONMENT_VARIABLES,
61
+ )
62
+ from chrys.app.tui.screens.agents.validation_messages import (
63
+ MCP_HEADERS as _HEADERS,
64
+ )
65
+ from chrys.app.tui.screens.agents.validation_messages import (
66
+ MCP_INITIALLY_VISIBLE_TOOLS as _INITIALLY_VISIBLE_TOOLS,
67
+ )
68
+ from chrys.app.tui.screens.agents.validation_messages import (
69
+ MCP_SELECTED_TOOL_NAMES as _SELECTED_TOOL_NAMES,
70
+ )
71
+ from chrys.app.tui.screens.agents.validation_messages import (
72
+ MCP_TOOL_ACCESS_NONE as _TOOL_ACCESS_NONE_LABEL,
73
+ )
74
+ from chrys.app.tui.screens.dialogs.confirm import ConfirmDialog
75
+ from chrys.app.tui.screens.dialogs.connection_test import ConnectionTestDialog
76
+ from chrys.app.tui.widgets import Checkbox, ConfigAddButton, EnhancedTextArea, HatchedEmptyState, Select
77
+ from chrys.app.tui.widgets import EnhancedInput as Input
78
+ from chrys.foundation.branding import APP_DISPLAY_NAME
79
+ from chrys.foundation.i18n import DisplayBlock, MessageDef, MessageRef, msg
80
+ from chrys.foundation.i18n.formatting import format_message
81
+ from chrys.foundation.platform import get_platform
82
+ from chrys.foundation.platform.command_line import join_windows_command_line, split_windows_command_line
83
+ from chrys.service.mcp.adapter import (
84
+ DEFAULT_CONNECT_TIMEOUT_SECONDS,
85
+ STANDARD_CAPABILITY_GROUPS,
86
+ MCPAdapter,
87
+ MCPToolConfigurationError,
88
+ )
89
+ from chrys.service.mcp.validation import MCP_PROGRESSIVE_CONTROL_TOOL_NAMES, validate_mcp_tool_name_prefix
90
+ from chrys.service.tools.names import chrys_reserved_tool_names
91
+
92
+ logger = logging.getLogger(__name__)
93
+
94
+ _MCP_TITLE = msg("tui.mcp.title", fallback="MCP")
95
+ _MCP_SERVER_SUBJECT = msg("tui.mcp.connection_test.subject", fallback="MCP Server")
96
+ _WAIT_BEFORE_ADDING = msg(
97
+ "tui.mcp.wait_before_adding",
98
+ fallback="Wait for MCP connection tests to finish before adding servers.",
99
+ )
100
+ _WAIT_BEFORE_REMOVING = msg(
101
+ "tui.mcp.wait_before_removing",
102
+ fallback="Wait for MCP connection tests to finish before removing servers.",
103
+ )
104
+ _DELETE_SERVER_TITLE = msg("tui.mcp.confirm_delete.title", fallback="Delete MCP Server")
105
+ _DELETE_SERVER_MESSAGE = msg(
106
+ "tui.mcp.confirm_delete.message",
107
+ fallback='Delete MCP server\n"{name}"?',
108
+ multiline=True,
109
+ )
110
+ _DELETE_SERVER_CONFIRM = msg("tui.mcp.confirm_delete.confirm", fallback="Delete")
111
+ _TOOL_ACCESS_ALL_LABEL = msg("tui.mcp.tool_access.all", fallback="All server tools")
112
+ _TOOL_ACCESS_SELECTED_LABEL = msg("tui.mcp.tool_access.selected", fallback="Only selected tools")
113
+ _TOOL_LOADING_FULL_LABEL = msg(
114
+ "tui.mcp.tool_loading.full",
115
+ fallback="Full — load all available tools",
116
+ )
117
+ _TOOL_LOADING_PROGRESSIVE_LABEL = msg(
118
+ "tui.mcp.tool_loading.progressive",
119
+ fallback="On demand — load tools as needed",
120
+ )
121
+ _LOAD_PROMPTS_TOOLTIP = msg(
122
+ "tui.mcp.load_prompts_tooltip",
123
+ fallback=(
124
+ "When enabled, {app} loads prompt templates advertised by this MCP server as callable tools. If the server "
125
+ "does not advertise prompt support, prompt loading is skipped automatically. Disable this to keep server "
126
+ "prompt templates out of the model's tool list."
127
+ ),
128
+ )
129
+ _LOAD_PROMPTS_HINT = msg(
130
+ "tui.mcp.load_prompts_hint",
131
+ fallback="Prompt templates advertised by the server are loaded as callable tools in the model's tool list.",
132
+ )
133
+ _EXPOSE_INSTRUCTIONS_TOOLTIP = msg(
134
+ "tui.mcp.expose_instructions_tooltip",
135
+ fallback=(
136
+ "When enabled, {app} includes the server's usage instructions from the MCP handshake in the model's context "
137
+ "as a per-turn system reminder. Servers that provide no instructions are unaffected. Disable this to keep "
138
+ "the server's guidance out of the model's context."
139
+ ),
140
+ )
141
+ _EXPOSE_INSTRUCTIONS_HINT = msg(
142
+ "tui.mcp.expose_instructions_hint",
143
+ fallback="The server's usage guidance from the MCP handshake is injected as a system reminder.",
144
+ )
145
+ _TOOL_ACCESS_TOOLTIP = msg(
146
+ "tui.mcp.tool_access_tooltip",
147
+ fallback=(
148
+ "Defines the MCP catalog boundary. Choose all tools advertised by the server, an explicit allowlist, or no "
149
+ "callable tools."
150
+ ),
151
+ )
152
+ _PROGRESSIVE_DISCLOSURE_TOOLTIP = msg(
153
+ "tui.mcp.progressive_disclosure_tooltip",
154
+ fallback=(
155
+ "When enabled, only loader controls and the optional initially visible tools are sent first. The model can "
156
+ "load other tools from the available-tool scope during the current run."
157
+ ),
158
+ )
159
+ _ALWAYS_LOAD_TOOLTIP = msg(
160
+ "tui.mcp.always_load_tooltip",
161
+ fallback="Optional comma-separated names from the available-tool scope to send to the model from the first request.",
162
+ )
163
+ _HEADER_NAME_PLACEHOLDER = msg(
164
+ "tui.mcp.placeholder.header_name",
165
+ fallback="e.g. X-Auth-Token",
166
+ )
167
+ _HEADER_VALUE_PLACEHOLDER = msg(
168
+ "tui.mcp.placeholder.header_value",
169
+ fallback="e.g. {{{{AUTH_TOKEN}}}}",
170
+ )
171
+ _VARIABLE_NAME_PLACEHOLDER = msg("tui.mcp.placeholder.variable_name", fallback="Variable name")
172
+ _VALUE_PLACEHOLDER = msg("tui.mcp.placeholder.value", fallback="Value")
173
+ _TRANSPORT = msg("tui.mcp.transport", fallback="Transport")
174
+ _SERVER_NAME = msg("tui.mcp.server_name", fallback="Server Name")
175
+ _SERVER_NAME_PLACEHOLDER = msg("tui.mcp.placeholder.server_name", fallback="Server name")
176
+ _DESCRIPTION = msg("tui.mcp.description", fallback="Description")
177
+ _PROMPTS_AND_INSTRUCTIONS = msg(
178
+ "tui.mcp.prompts_and_instructions",
179
+ fallback="Server Instructions & Prompt Templates",
180
+ )
181
+ _EXPOSE_SERVER_INSTRUCTIONS = msg(
182
+ "tui.mcp.expose_server_instructions",
183
+ fallback="Include server instructions in model context (if available)",
184
+ )
185
+ _EXPOSE_SERVER_PROMPTS = msg(
186
+ "tui.mcp.expose_server_prompts",
187
+ fallback="Load server prompt templates as tools (if available)",
188
+ )
189
+ _TOOL_ACCESS = msg("tui.mcp.tool_access", fallback="Tool Access")
190
+ _PERMITTED_TOOL_SET = msg("tui.mcp.permitted_tool_set", fallback="Available Tool Scope")
191
+ _SELECTED_TOOLS_PLACEHOLDER = msg("tui.mcp.placeholder.selected_tools", fallback="tool_a, tool_b")
192
+ _TOOL_LOADING = msg("tui.mcp.tool_loading", fallback="Tool Loading")
193
+ _LOADING_STRATEGY = msg("tui.mcp.loading_strategy", fallback="Loading Strategy")
194
+ _INITIAL_TOOLS_PLACEHOLDER = msg("tui.mcp.placeholder.initial_tools", fallback="search, read_file")
195
+ _NAMING_AND_LIMITS = msg("tui.mcp.naming_and_limits", fallback="Naming & Limits")
196
+ _TOOL_NAME_PREFIX = msg("tui.mcp.tool_name_prefix", fallback="Tool Name Prefix")
197
+ _TOOL_NAME_PREFIX_PLACEHOLDER = msg(
198
+ "tui.mcp.placeholder.tool_name_prefix",
199
+ fallback="e.g. github → tools named github_<tool>",
200
+ )
201
+ _REQUEST_TIMEOUT = msg("tui.mcp.request_timeout", fallback="MCP Request Timeout (seconds)")
202
+ _REQUEST_TIMEOUT_PLACEHOLDER = msg(
203
+ "tui.mcp.placeholder.request_timeout",
204
+ fallback="Leave blank for default ({default})",
205
+ )
206
+ _ENABLED = msg("tui.mcp.enabled", fallback="Enabled")
207
+ _TEST = msg("tui.mcp.test", fallback="Test")
208
+ _TESTING = msg("tui.mcp.testing", fallback="Testing...")
209
+ _URL = msg("tui.mcp.url", fallback="URL")
210
+ _URL_PLACEHOLDER = msg("tui.mcp.placeholder.url", fallback="https://api.example.com/mcp")
211
+ _SKIP_TLS_VERIFICATION = msg("tui.mcp.skip_tls_verification", fallback="Skip TLS verification")
212
+ _INSECURE_TLS = msg(
213
+ "tui.mcp.insecure_tls",
214
+ fallback="Insecure: disables HTTPS certificate validation. Use only for trusted self-signed servers.",
215
+ )
216
+ _BYPASS_PROXY = msg("tui.mcp.bypass_proxy", fallback="Bypass proxy")
217
+ _COMMAND = msg("tui.mcp.command", fallback="Command")
218
+ _COMMAND_PLACEHOLDER = msg(
219
+ "tui.mcp.placeholder.command",
220
+ fallback="e.g. npx -y @modelcontextprotocol/server-filesystem /path",
221
+ )
222
+ _HTTP_OPTIONS = msg("tui.mcp.http_options", fallback="HTTP Options")
223
+ _STDIO_OPTIONS = msg("tui.mcp.stdio_options", fallback="STDIO Options")
224
+ _ADD = msg("tui.mcp.add", fallback="+ Add")
225
+ _SERVERS = msg("tui.mcp.servers", fallback="MCP Servers")
226
+ _SERVERS_DESCRIPTION = msg(
227
+ "tui.mcp.servers_description",
228
+ fallback="Configure Model Context Protocol servers",
229
+ )
230
+ _EMPTY = msg("tui.mcp.empty", fallback="No MCP servers configured")
231
+ _CONNECTION_SUCCESSFUL = msg("tui.mcp.connection_test.successful", fallback="Connection successful.")
232
+ _CONNECTION_CONFIGURATION_CONFLICT = msg(
233
+ "tui.mcp.connection_test.error.configuration_conflict",
234
+ fallback="MCP tool configuration conflict.",
235
+ )
236
+ _CONNECTION_CANCELLED = msg(
237
+ "tui.mcp.connection_test.error.cancelled",
238
+ fallback="Connection test was cancelled. Please try again.",
239
+ )
240
+ _CONNECTION_TIMED_OUT = msg(
241
+ "tui.mcp.connection_test.error.timed_out",
242
+ fallback="Connection test timed out. Please check server and timeout settings.",
243
+ )
244
+ _CONNECTION_UNREACHABLE = msg(
245
+ "tui.mcp.connection_test.error.unreachable",
246
+ fallback="Unable to reach MCP server. Please check URL, network, and server status.",
247
+ )
248
+ _CONNECTION_REQUEST_FAILED = msg(
249
+ "tui.mcp.connection_test.error.request_failed",
250
+ fallback="MCP HTTP request failed. Please verify endpoint and authentication.",
251
+ )
252
+ _CONNECTION_FAILED = msg(
253
+ "tui.mcp.connection_test.error.failed",
254
+ fallback="MCP server connection failed.",
255
+ )
256
+ _CAPABILITIES_HEADING = msg("tui.mcp.connection_report.heading.capabilities", fallback="Capabilities")
257
+ _INSTRUCTIONS_HEADING = msg("tui.mcp.connection_report.heading.instructions", fallback="Instructions")
258
+ _TOOLS_HEADING = msg("tui.mcp.connection_report.heading.tools", fallback="Tools")
259
+ _PROMPTS_HEADING = msg("tui.mcp.connection_report.heading.prompts", fallback="Prompts")
260
+ _PROGRESSIVE_DISCLOSURE_HEADING = msg(
261
+ "tui.mcp.connection_report.heading.progressive_disclosure",
262
+ fallback="On-demand Loading",
263
+ )
264
+ _IDENTITY_NOT_REPORTED = msg(
265
+ "tui.mcp.connection_report.identity_not_reported",
266
+ fallback="*The server did not report its identity.*",
267
+ )
268
+ _NO_COMMAND_CONFIGURED = msg(
269
+ "tui.mcp.connection_report.no_command_configured",
270
+ fallback="*no command configured*",
271
+ )
272
+ _NO_URL_CONFIGURED = msg(
273
+ "tui.mcp.connection_report.no_url_configured",
274
+ fallback="*no URL configured*",
275
+ )
276
+ _CONNECTED_VIA = msg(
277
+ "tui.mcp.connection_report.connected_via",
278
+ fallback="Connected via {transport}: {target}",
279
+ )
280
+ _TOOLS_EXPOSED = msg(
281
+ "tui.mcp.connection_report.tools_exposed",
282
+ fallback="{count} tool available to the model.\n\n{catalog}",
283
+ plural_fallback="{count} tools available to the model.\n\n{catalog}",
284
+ multiline=True,
285
+ )
286
+ _ADVERTISED_NO_TOOLS = msg(
287
+ "tui.mcp.connection_report.advertised_no_tools",
288
+ fallback="*The server supports tools, but none are available (check the available-tool scope).*",
289
+ )
290
+ _MORE_ENTRIES = msg("tui.mcp.connection_report.more_entries", fallback="…and {extra_count} more.")
291
+ _CAPABILITY_HEADER = msg("tui.mcp.connection_report.header.capability", fallback="Capability")
292
+ _ADVERTISED_HEADER = msg("tui.mcp.connection_report.header.advertised", fallback="Advertised")
293
+ _PROMPTS_EXPOSED = msg(
294
+ "tui.mcp.connection_report.prompts_exposed",
295
+ fallback="{count} prompt template loaded as a tool.\n\n{catalog}",
296
+ plural_fallback="{count} prompt templates loaded as tools.\n\n{catalog}",
297
+ multiline=True,
298
+ )
299
+ _PROMPT_LOADING_DISABLED = msg(
300
+ "tui.mcp.connection_report.prompt_loading_disabled",
301
+ fallback="*Prompt loading is disabled for this server.*",
302
+ )
303
+ _ADVERTISED_NO_PROMPTS = msg(
304
+ "tui.mcp.connection_report.advertised_no_prompts",
305
+ fallback="*The server supports prompt templates, but none are available.*",
306
+ )
307
+ _PROGRESSIVE_ENABLED = msg(
308
+ "tui.mcp.connection_report.progressive_enabled",
309
+ fallback="Enabled — only these tools are visible initially; the rest load on demand:",
310
+ )
311
+
312
+ if TYPE_CHECKING:
313
+ from textual.app import ComposeResult
314
+
315
+ from chrys.service.mcp.adapter import MCPTestReport
316
+ from chrys.service.profiles.agents.schema import MCPServerConfig
317
+
318
+
319
+ _TRANSPORT_OPTIONS = [
320
+ ("HTTP", "http"),
321
+ ("STDIO", "stdio"),
322
+ ]
323
+
324
+ _TOOL_ACCESS_ALL = "all"
325
+ _TOOL_ACCESS_SELECTED = "selected"
326
+ _TOOL_ACCESS_NONE = "none"
327
+ _TOOL_ACCESS_OPTIONS = [
328
+ (_TOOL_ACCESS_ALL_LABEL, _TOOL_ACCESS_ALL),
329
+ (_TOOL_ACCESS_SELECTED_LABEL, _TOOL_ACCESS_SELECTED),
330
+ (_TOOL_ACCESS_NONE_LABEL, _TOOL_ACCESS_NONE),
331
+ ]
332
+ _TOOL_LOADING_FULL = "full"
333
+ _TOOL_LOADING_PROGRESSIVE = "progressive"
334
+ _TOOL_LOADING_OPTIONS = [
335
+ (_TOOL_LOADING_FULL_LABEL, _TOOL_LOADING_FULL),
336
+ (_TOOL_LOADING_PROGRESSIVE_LABEL, _TOOL_LOADING_PROGRESSIVE),
337
+ ]
338
+
339
+
340
+ def _is_transport(value: str) -> TypeGuard[Literal["stdio", "http"]]:
341
+ """Narrow the schema-backed transport discriminator."""
342
+ return value in {"stdio", "http"}
343
+
344
+
345
+ @dataclass(frozen=True, slots=True)
346
+ class _MCPKeyValueRows:
347
+ """Widget-id and placeholder policy for one MCP key/value row list."""
348
+
349
+ container_suffix: str
350
+ action_prefix: str
351
+ key_placeholder: MessageDef
352
+ value_placeholder: MessageDef
353
+
354
+
355
+ @dataclass(frozen=True, slots=True)
356
+ class _MCPKeyValueDrafts:
357
+ """UI-only key/value row text preserved across card rebuilds."""
358
+
359
+ headers: list[tuple[str, str]] | None = None
360
+ env: list[tuple[str, str]] | None = None
361
+
362
+
363
+ @dataclass(slots=True)
364
+ class _MCPServerRow:
365
+ """One server in the panel: its config plus the UI-only state a card rebuild carries over."""
366
+
367
+ config: MCPServerConfig
368
+ command_line_text: str | None = None
369
+ key_value_drafts: _MCPKeyValueDrafts = field(default_factory=_MCPKeyValueDrafts)
370
+ # Saved servers open folded to their name row; a server added in the panel opens unfolded.
371
+ collapsed: bool = True
372
+
373
+
374
+ _MCP_HEADER_ROWS = _MCPKeyValueRows(
375
+ container_suffix="headers",
376
+ action_prefix="h",
377
+ key_placeholder=_HEADER_NAME_PLACEHOLDER,
378
+ value_placeholder=_HEADER_VALUE_PLACEHOLDER,
379
+ )
380
+ _MCP_ENV_ROWS = _MCPKeyValueRows(
381
+ container_suffix="env",
382
+ action_prefix="e",
383
+ key_placeholder=_VARIABLE_NAME_PLACEHOLDER,
384
+ value_placeholder=_VALUE_PLACEHOLDER,
385
+ )
386
+
387
+
388
+ class MCPConnectionCard(CollapsibleConfigCard):
389
+ """A single MCP server entry with name, transport, and transport-specific fields.
390
+
391
+ The header shows the server name, so a collapsed card shows just that name in the server list.
392
+ """
393
+
394
+ DEFAULT_CSS = """
395
+ MCPConnectionCard .mcp-row {
396
+ height: auto;
397
+ margin: 0 0 1 0;
398
+ }
399
+ MCPConnectionCard .mcp-row Input {
400
+ margin: 0 1 0 0;
401
+ }
402
+ MCPConnectionCard .mcp-header-row {
403
+ height: auto;
404
+ margin: 0;
405
+ }
406
+ MCPConnectionCard .mcp-allowed-tools-row {
407
+ height: auto;
408
+ margin: 1 0 0 0;
409
+ }
410
+ MCPConnectionCard .mcp-progressive-fields,
411
+ MCPConnectionCard .mcp-policy-fields {
412
+ height: auto;
413
+ }
414
+ MCPConnectionCard .mcp-option-section {
415
+ height: auto;
416
+ margin: 1 0 0 0;
417
+ padding: 1 0 1 2;
418
+ border: solid $tui-border-foreground 15%;
419
+ border-title-color: $text-muted;
420
+ }
421
+ MCPConnectionCard .mcp-tool-access-section {
422
+ margin: 1 0 0 0;
423
+ }
424
+ MCPConnectionCard .mcp-option-section .mcp-label:first-of-type {
425
+ margin-top: 0;
426
+ }
427
+ MCPConnectionCard .mcp-option-section .mcp-policy-fields .mcp-label:first-of-type,
428
+ MCPConnectionCard .mcp-option-section .mcp-progressive-fields .mcp-label:first-of-type {
429
+ margin-top: 1;
430
+ }
431
+ MCPConnectionCard .mcp-option-section Select,
432
+ MCPConnectionCard .mcp-option-section .mcp-policy-fields Input,
433
+ MCPConnectionCard .mcp-option-section .mcp-progressive-fields Input,
434
+ MCPConnectionCard .mcp-naming-input {
435
+ margin: 0 2 0 0;
436
+ }
437
+ MCPConnectionCard .mcp-transport-option-section > Input,
438
+ MCPConnectionCard .mcp-transport-option-section > EnhancedTextArea,
439
+ MCPConnectionCard .mcp-transport-option-section > .mcp-list {
440
+ margin-right: 2;
441
+ }
442
+ MCPConnectionCard .mcp-transport-option-section .mcp-list > .mcp-row:last-child {
443
+ margin-bottom: 0;
444
+ }
445
+ MCPConnectionCard .mcp-field {
446
+ width: 1fr;
447
+ height: auto;
448
+ margin: 0;
449
+ }
450
+ MCPConnectionCard .mcp-label {
451
+ height: 1;
452
+ color: $text-muted;
453
+ margin: 1 0 0 0;
454
+ }
455
+ MCPConnectionCard .mcp-allowed-tools-row .mcp-label {
456
+ width: auto;
457
+ margin: 0 2 0 0;
458
+ }
459
+ MCPConnectionCard Select {
460
+ height: auto;
461
+ margin: 0;
462
+ }
463
+ MCPConnectionCard SelectCurrent {
464
+ height: 1;
465
+ border: none;
466
+ background: $foreground 8%;
467
+ padding: 0 1;
468
+ }
469
+ MCPConnectionCard SelectOverlay {
470
+ border: round $tui-border-primary $border-opacity;
471
+ background: $surface;
472
+ }
473
+ MCPConnectionCard .mcp-transport-fields,
474
+ MCPConnectionCard .mcp-common-fields,
475
+ MCPConnectionCard .mcp-list {
476
+ height: auto;
477
+ }
478
+ MCPConnectionCard .mcp-footer-row {
479
+ height: auto;
480
+ margin: 1 0 0 0;
481
+ }
482
+ MCPConnectionCard .mcp-footer-left {
483
+ width: 1fr;
484
+ height: auto;
485
+ }
486
+ MCPConnectionCard .mcp-test-btn {
487
+ min-width: 14;
488
+ height: 1;
489
+ }
490
+ MCPConnectionCard .mcp-test-btn.-disabled {
491
+ color: $text-muted;
492
+ }
493
+ MCPConnectionCard EnhancedTextArea {
494
+ height: 5;
495
+ border: none;
496
+ background: $foreground 8%;
497
+ padding: 0 0;
498
+ margin: 0;
499
+ scrollbar-size-vertical: 1;
500
+ }
501
+ MCPConnectionCard EnhancedTextArea:focus {
502
+ border: none;
503
+ background: $foreground 12%;
504
+ }
505
+ MCPConnectionCard EnhancedTextArea .text-area--cursor-line {
506
+ background: $primary 15%;
507
+ }
508
+ MCPConnectionCard Input {
509
+ height: 1;
510
+ border: none;
511
+ background: $foreground 8%;
512
+ padding: 0 1;
513
+ margin: 0;
514
+ }
515
+ MCPConnectionCard Input:focus {
516
+ border: none;
517
+ background: $foreground 12%;
518
+ }
519
+ MCPConnectionCard Checkbox {
520
+ width: auto;
521
+ height: 1;
522
+ color: $text-muted;
523
+ margin: 0 1 0 0;
524
+ padding: 0 2;
525
+ border: solid $tui-border-foreground 15%;
526
+ background: $foreground 6%;
527
+ }
528
+ MCPConnectionCard Checkbox.-on {
529
+ color: $secondary;
530
+ border: solid $tui-border-secondary $border-opacity;
531
+ background: $secondary 12%;
532
+ }
533
+ MCPConnectionCard Checkbox > .toggle--button {
534
+ color: $foreground 35%;
535
+ background: $foreground 6%;
536
+ }
537
+ MCPConnectionCard Checkbox.-on > .toggle--button {
538
+ color: $success;
539
+ background: $secondary 12%;
540
+ }
541
+ MCPConnectionCard Checkbox .toggle--label {
542
+ text-style: bold;
543
+ }
544
+ MCPConnectionCard .mcp-skip-tls {
545
+ margin: 1 1 0 0;
546
+ }
547
+ MCPConnectionCard .mcp-bypass-proxy {
548
+ margin: 1 1 1 0;
549
+ }
550
+ MCPConnectionCard .mcp-skip-tls-hint {
551
+ width: 100%;
552
+ height: auto;
553
+ color: $warning;
554
+ margin: 0 1 1 0;
555
+ padding: 0 0 0 3;
556
+ }
557
+ MCPConnectionCard .mcp-checkbox-hint {
558
+ width: 100%;
559
+ height: auto;
560
+ color: $text-disabled;
561
+ margin: 0 1 1 0;
562
+ padding: 0 0 0 3;
563
+ }
564
+ MCPConnectionCard .mcp-checkbox-hint:last-of-type {
565
+ margin-bottom: 0;
566
+ }
567
+ MCPConnectionCard Checkbox.mcp-guidance-checkbox .toggle--label {
568
+ color: $text;
569
+ }
570
+ MCPConnectionCard Checkbox.mcp-guidance-checkbox.-hint-collapsed {
571
+ margin-bottom: 1;
572
+ }
573
+ MCPConnectionCard Checkbox.mcp-guidance-checkbox.-hint-collapsed:last-of-type {
574
+ margin-bottom: 0;
575
+ }
576
+ MCPConnectionCard .mcp-item-row {
577
+ height: auto;
578
+ background: transparent;
579
+ padding: 0;
580
+ margin: 0 0 1 0;
581
+ }
582
+ MCPConnectionCard .mcp-item-row Input {
583
+ margin: 0 1 0 0;
584
+ }
585
+ MCPConnectionCard .mcp-item-row .mcp-item-value-input {
586
+ margin-right: 0;
587
+ }
588
+ MCPConnectionCard .mcp-small-btn {
589
+ min-width: 6;
590
+ height: 1;
591
+ text-style: bold;
592
+ padding: 0 1;
593
+ }
594
+ MCPConnectionCard .mcp-remove-btn {
595
+ min-width: 3;
596
+ height: 1;
597
+ border: none;
598
+ background: transparent;
599
+ color: $error;
600
+ padding: 0;
601
+ content-align: right middle;
602
+ text-align: right;
603
+ }
604
+ """
605
+
606
+ class TestRequested(Message):
607
+ """Posted when the user requests a connection test; carries the card, which a later rebuild may re-index."""
608
+
609
+ def __init__(self, card: MCPConnectionCard) -> None:
610
+ self.card = card
611
+ super().__init__()
612
+
613
+ @property
614
+ def control(self) -> MCPConnectionCard:
615
+ return self.card
616
+
617
+ class Removed(CollapsibleConfigCard.Removed):
618
+ """Posted when the user clicks delete; carries the card, which a later rebuild may re-index."""
619
+
620
+ def __init__(self, card: MCPConnectionCard) -> None:
621
+ self.card = card
622
+ super().__init__(card.index)
623
+
624
+ @property
625
+ def control(self) -> MCPConnectionCard:
626
+ return self.card
627
+
628
+ # Class-level counter for generating unique widget IDs across all card instances.
629
+ _uid_counter: int = 0
630
+ _delete_button_prefix = "mcp-delete-btn"
631
+
632
+ def __init__(
633
+ self,
634
+ config: MCPServerConfig,
635
+ index: int,
636
+ *,
637
+ command_line_text: str | None = None,
638
+ key_value_drafts: _MCPKeyValueDrafts | None = None,
639
+ read_only: bool = False,
640
+ collapsed: bool = False,
641
+ ) -> None:
642
+ self._config = config
643
+ self._index = index
644
+ self._command_line_text = command_line_text
645
+ self._key_value_drafts = key_value_drafts or _MCPKeyValueDrafts()
646
+ self._transport: Literal["stdio", "http"] = config.transport if _is_transport(config.transport) else "stdio"
647
+ self._headers_user_modified = False
648
+ self._env_user_modified = False
649
+ super().__init__(index=index, collapsed=collapsed, read_only=read_only)
650
+
651
+ @property
652
+ def index(self) -> int:
653
+ """Stable server index assigned by the panel."""
654
+ return self._index
655
+
656
+ def _removed_message(self) -> MCPConnectionCard.Removed:
657
+ return self.Removed(self)
658
+
659
+ @staticmethod
660
+ def _format_command_line(command: str, args: list[str]) -> str:
661
+ if not command:
662
+ return ""
663
+ parts = [command, *args]
664
+ if get_platform().is_windows:
665
+ return join_windows_command_line(parts)
666
+ return shlex.join(parts)
667
+
668
+ @staticmethod
669
+ def _split_command_line(
670
+ command_line: str,
671
+ *,
672
+ render_message: Callable[[MessageRef], str] = format_message,
673
+ ) -> tuple[str, list[str]]:
674
+ text = command_line.strip()
675
+ if not text:
676
+ return "", []
677
+ try:
678
+ parts = split_windows_command_line(text) if get_platform().is_windows else shlex.split(text)
679
+ except ValueError as exc:
680
+ error_message = render_message(MCP_INVALID_COMMAND.bind(detail=DisplayBlock(str(exc))))
681
+ raise ValueError(error_message) from exc
682
+ if not parts:
683
+ return "", []
684
+ return parts[0], parts[1:]
685
+
686
+ @staticmethod
687
+ def _parse_allowed_tools(value: str) -> list[str]:
688
+ return [item.strip() for item in value.split(",") if item.strip()]
689
+
690
+ def _validate_comma_separated_tool_names(
691
+ self,
692
+ value: str,
693
+ *,
694
+ label_definition: MessageDef,
695
+ ) -> list[str]:
696
+ """Validate editable tool-name list syntax without constraining MCP names."""
697
+ if not value.strip():
698
+ return []
699
+ localizer = widget_localizer(self)
700
+ label = render_str(localizer, label_definition.bind())
701
+ names = [item.strip() for item in value.split(",")]
702
+ errors: list[str] = []
703
+ if any(not name for name in names):
704
+ errors.append(
705
+ render_str(
706
+ localizer,
707
+ MCP_TOOL_LIST_EMPTY.bind(
708
+ label=label,
709
+ item=render_str(localizer, TOOL_NAME_ITEM.bind()),
710
+ ),
711
+ )
712
+ )
713
+ seen: set[str] = set()
714
+ for name in names:
715
+ if not name:
716
+ continue
717
+ if name in seen:
718
+ errors.append(
719
+ render_str(
720
+ localizer,
721
+ MCP_TOOL_LIST_DUPLICATE.bind(
722
+ label=label,
723
+ name=DisplayBlock(name),
724
+ ),
725
+ )
726
+ )
727
+ seen.add(name)
728
+ return errors
729
+
730
+ @staticmethod
731
+ def _tool_access_mode(allowed_tools: list[str] | None) -> str:
732
+ if allowed_tools is None:
733
+ return _TOOL_ACCESS_ALL
734
+ return _TOOL_ACCESS_SELECTED if allowed_tools else _TOOL_ACCESS_NONE
735
+
736
+ def _mounted_tool_access_mode(self) -> str:
737
+ mode = self._tool_access_mode(self._config.allowed_tools)
738
+ with contextlib.suppress(NoMatches):
739
+ mode = str(self.query_one(f"#mcp-tool-access-{self._index}", Select).value)
740
+ return mode
741
+
742
+ def _mounted_loading_strategy(self) -> str:
743
+ strategy = _TOOL_LOADING_PROGRESSIVE if self._config.use_progressive_disclosure else _TOOL_LOADING_FULL
744
+ with contextlib.suppress(NoMatches):
745
+ strategy = str(self.query_one(f"#mcp-loading-strategy-{self._index}", Select).value)
746
+ return strategy
747
+
748
+ def _sync_tool_policy_fields(self) -> None:
749
+ """Apply the access -> loading strategy -> initial subset hierarchy."""
750
+ with contextlib.suppress(NoMatches):
751
+ access_mode = self._mounted_tool_access_mode()
752
+ selected_fields = self.query_one(f"#mcp-selected-tools-fields-{self._index}", Vertical)
753
+ selected_input = self.query_one(f"#mcp-tools-{self._index}", Input)
754
+ exposure_fields = self.query_one(f"#mcp-model-exposure-fields-{self._index}", Vertical)
755
+ loading = self.query_one(f"#mcp-loading-strategy-{self._index}", Select)
756
+ initial_fields = self.query_one(f"#mcp-always-load-fields-{self._index}", Vertical)
757
+ initial_input = self.query_one(f"#mcp-always-load-{self._index}", Input)
758
+
759
+ selected = access_mode == _TOOL_ACCESS_SELECTED
760
+ no_tools = access_mode == _TOOL_ACCESS_NONE
761
+ progressive = self._mounted_loading_strategy() == _TOOL_LOADING_PROGRESSIVE
762
+
763
+ selected_fields.display = selected
764
+ selected_input.disabled = not selected or self._read_only
765
+ exposure_fields.display = not no_tools
766
+ loading.disabled = no_tools or self._read_only
767
+ initial_fields.display = not no_tools and progressive
768
+ initial_input.disabled = no_tools or not progressive or self._read_only
769
+
770
+ @staticmethod
771
+ def _format_timeout(value: int | None) -> str:
772
+ return "" if value is None else str(value)
773
+
774
+ def _parse_key_value_rows(
775
+ self,
776
+ container_id: str,
777
+ add_row_id: str,
778
+ *,
779
+ user_modified: bool,
780
+ ) -> dict[str, str]:
781
+ container = self.query_one(f"#{container_id}", Vertical)
782
+ if not user_modified:
783
+ self.query_one(f"#{add_row_id}", Horizontal)
784
+
785
+ parsed: dict[str, str] = {}
786
+ for row in container.query(".mcp-item-row"):
787
+ key = row.query_one(".mcp-item-key-input", Input).value.strip()
788
+ value = row.query_one(".mcp-item-value-input", Input).value.strip()
789
+ if key:
790
+ parsed[key] = value
791
+ return parsed
792
+
793
+ def compose(self) -> ComposeResult:
794
+ cfg = self._config
795
+ localizer = widget_localizer(self)
796
+ yield from self.compose_header(
797
+ self._header_title(cfg.name),
798
+ row_class="mcp-header-row",
799
+ title_class="mcp-title",
800
+ )
801
+
802
+ with self.card_body():
803
+ with Vertical(classes="mcp-field"):
804
+ yield Label(render_str(localizer, _TRANSPORT.bind()), classes="mcp-label")
805
+ transport = Select(
806
+ _TRANSPORT_OPTIONS,
807
+ value=self._transport,
808
+ id=f"mcp-transport-{self._index}",
809
+ allow_blank=False,
810
+ )
811
+ transport.disabled = self._read_only
812
+ yield transport
813
+ yield Label(f"[red]*[/red] {escape(render_str(localizer, _SERVER_NAME.bind()))}", classes="mcp-label")
814
+ name = Input(
815
+ value=cfg.name,
816
+ placeholder=render_str(localizer, _SERVER_NAME_PLACEHOLDER.bind()),
817
+ id=f"mcp-name-{self._index}",
818
+ )
819
+ name.disabled = self._read_only
820
+ yield name
821
+ yield Label(render_str(localizer, _DESCRIPTION.bind()), classes="mcp-label")
822
+ description = EnhancedTextArea(
823
+ cfg.description or "",
824
+ id=f"mcp-desc-{self._index}",
825
+ )
826
+ description.read_only = self._read_only
827
+ yield description
828
+
829
+ with Vertical(
830
+ classes="mcp-transport-fields mcp-option-section mcp-transport-option-section",
831
+ id=f"mcp-transport-fields-{self._index}",
832
+ ) as transport_section:
833
+ transport_section.border_title = self._transport_options_title()
834
+ yield from self._compose_transport_fields(cfg)
835
+
836
+ with Vertical(classes="mcp-common-fields"):
837
+ with Vertical(
838
+ id=f"mcp-prompts-instructions-section-{self._index}",
839
+ classes="mcp-option-section",
840
+ ) as guidance_section:
841
+ guidance_section.border_title = render_str(localizer, _PROMPTS_AND_INSTRUCTIONS.bind())
842
+ expose_instructions_cb = Checkbox(
843
+ render_str(localizer, _EXPOSE_SERVER_INSTRUCTIONS.bind()),
844
+ value=cfg.expose_instructions,
845
+ id=f"mcp-expose-instructions-{self._index}",
846
+ classes="mcp-guidance-checkbox",
847
+ )
848
+ expose_instructions_cb.tooltip = render_str(
849
+ localizer,
850
+ _EXPOSE_INSTRUCTIONS_TOOLTIP.bind(app=APP_DISPLAY_NAME),
851
+ )
852
+ expose_instructions_cb.disabled = self._read_only
853
+ expose_instructions_cb.set_class(not cfg.expose_instructions, "-hint-collapsed")
854
+ yield expose_instructions_cb
855
+ expose_instructions_hint = Label(
856
+ render_str(localizer, _EXPOSE_INSTRUCTIONS_HINT.bind()),
857
+ id=f"mcp-expose-instructions-hint-{self._index}",
858
+ classes="mcp-checkbox-hint",
859
+ )
860
+ expose_instructions_hint.display = cfg.expose_instructions
861
+ yield expose_instructions_hint
862
+ load_prompts_cb = Checkbox(
863
+ render_str(localizer, _EXPOSE_SERVER_PROMPTS.bind()),
864
+ value=cfg.load_prompts,
865
+ id=f"mcp-load-prompts-{self._index}",
866
+ classes="mcp-guidance-checkbox",
867
+ )
868
+ load_prompts_cb.tooltip = render_str(localizer, _LOAD_PROMPTS_TOOLTIP.bind(app=APP_DISPLAY_NAME))
869
+ load_prompts_cb.disabled = self._read_only
870
+ load_prompts_cb.set_class(not cfg.load_prompts, "-hint-collapsed")
871
+ yield load_prompts_cb
872
+ load_prompts_hint = Label(
873
+ render_str(localizer, _LOAD_PROMPTS_HINT.bind()),
874
+ id=f"mcp-load-prompts-hint-{self._index}",
875
+ classes="mcp-checkbox-hint",
876
+ )
877
+ load_prompts_hint.display = cfg.load_prompts
878
+ yield load_prompts_hint
879
+
880
+ access_mode = self._tool_access_mode(cfg.allowed_tools)
881
+ with Vertical(
882
+ id=f"mcp-tool-access-section-{self._index}",
883
+ classes="mcp-option-section mcp-tool-access-section",
884
+ ) as access_section:
885
+ access_section.border_title = render_str(localizer, _TOOL_ACCESS.bind())
886
+ yield Label(render_str(localizer, _PERMITTED_TOOL_SET.bind()), classes="mcp-label")
887
+ access = Select(
888
+ [(render_str(localizer, label.bind()), value) for label, value in _TOOL_ACCESS_OPTIONS],
889
+ value=access_mode,
890
+ id=f"mcp-tool-access-{self._index}",
891
+ allow_blank=False,
892
+ )
893
+ access.tooltip = render_str(localizer, _TOOL_ACCESS_TOOLTIP.bind())
894
+ access.disabled = self._read_only
895
+ yield access
896
+ selected_tools_fields = Vertical(
897
+ id=f"mcp-selected-tools-fields-{self._index}",
898
+ classes="mcp-policy-fields",
899
+ )
900
+ selected_tools_fields.display = access_mode == _TOOL_ACCESS_SELECTED
901
+ with selected_tools_fields:
902
+ yield Label(render_str(localizer, _SELECTED_TOOL_NAMES.bind()), classes="mcp-label")
903
+ allowed_tools_input = Input(
904
+ value=", ".join(cfg.allowed_tools or []),
905
+ placeholder=render_str(localizer, _SELECTED_TOOLS_PLACEHOLDER.bind()),
906
+ id=f"mcp-tools-{self._index}",
907
+ )
908
+ allowed_tools_input.disabled = access_mode != _TOOL_ACCESS_SELECTED or self._read_only
909
+ yield allowed_tools_input
910
+
911
+ no_tools = access_mode == _TOOL_ACCESS_NONE
912
+ loading_strategy = _TOOL_LOADING_PROGRESSIVE if cfg.use_progressive_disclosure else _TOOL_LOADING_FULL
913
+ model_exposure_fields = Vertical(
914
+ id=f"mcp-model-exposure-fields-{self._index}",
915
+ classes="mcp-option-section",
916
+ )
917
+ model_exposure_fields.display = not no_tools
918
+ with model_exposure_fields:
919
+ model_exposure_fields.border_title = render_str(localizer, _TOOL_LOADING.bind())
920
+ yield Label(render_str(localizer, _LOADING_STRATEGY.bind()), classes="mcp-label")
921
+ loading = Select(
922
+ [(render_str(localizer, label.bind()), value) for label, value in _TOOL_LOADING_OPTIONS],
923
+ value=loading_strategy,
924
+ id=f"mcp-loading-strategy-{self._index}",
925
+ allow_blank=False,
926
+ )
927
+ loading.tooltip = render_str(localizer, _PROGRESSIVE_DISCLOSURE_TOOLTIP.bind())
928
+ loading.disabled = no_tools or self._read_only
929
+ yield loading
930
+ always_load_fields = Vertical(
931
+ id=f"mcp-always-load-fields-{self._index}",
932
+ classes="mcp-progressive-fields",
933
+ )
934
+ always_load_fields.display = not no_tools and cfg.use_progressive_disclosure
935
+ with always_load_fields:
936
+ always_load_label = Label(
937
+ render_str(localizer, _INITIALLY_VISIBLE_TOOLS.bind()),
938
+ classes="mcp-label",
939
+ )
940
+ always_load_label.tooltip = render_str(localizer, _ALWAYS_LOAD_TOOLTIP.bind())
941
+ yield always_load_label
942
+ always_load = Input(
943
+ value=", ".join(cfg.always_load),
944
+ placeholder=render_str(localizer, _INITIAL_TOOLS_PLACEHOLDER.bind()),
945
+ id=f"mcp-always-load-{self._index}",
946
+ )
947
+ always_load.tooltip = render_str(localizer, _ALWAYS_LOAD_TOOLTIP.bind())
948
+ always_load.disabled = no_tools or not cfg.use_progressive_disclosure or self._read_only
949
+ yield always_load
950
+
951
+ with Vertical(
952
+ id=f"mcp-naming-limits-section-{self._index}",
953
+ classes="mcp-option-section",
954
+ ) as naming_section:
955
+ naming_section.border_title = render_str(localizer, _NAMING_AND_LIMITS.bind())
956
+ yield Label(render_str(localizer, _TOOL_NAME_PREFIX.bind()), classes="mcp-label")
957
+ prefix = Input(
958
+ value=cfg.tool_name_prefix,
959
+ placeholder=render_str(localizer, _TOOL_NAME_PREFIX_PLACEHOLDER.bind()),
960
+ id=f"mcp-prefix-{self._index}",
961
+ classes="mcp-naming-input",
962
+ )
963
+ prefix.disabled = self._read_only
964
+ yield prefix
965
+ yield Label(render_str(localizer, _REQUEST_TIMEOUT.bind()), classes="mcp-label")
966
+ timeout = Input(
967
+ value=self._format_timeout(cfg.request_timeout),
968
+ placeholder=render_str(
969
+ localizer,
970
+ _REQUEST_TIMEOUT_PLACEHOLDER.bind(default=DEFAULT_CONNECT_TIMEOUT_SECONDS),
971
+ ),
972
+ id=f"mcp-timeout-{self._index}",
973
+ classes="mcp-naming-input",
974
+ )
975
+ timeout.disabled = self._read_only
976
+ yield timeout
977
+
978
+ with Horizontal(classes="mcp-footer-row"):
979
+ with Horizontal(classes="mcp-footer-left"):
980
+ enabled = Checkbox(
981
+ render_str(localizer, _ENABLED.bind()),
982
+ value=cfg.enabled,
983
+ id=f"mcp-enabled-{self._index}",
984
+ )
985
+ enabled.disabled = self._read_only
986
+ yield enabled
987
+ test_button = ConfigAddButton(
988
+ render_str(localizer, _TEST.bind()),
989
+ id=f"mcp-test-btn-{self._index}",
990
+ classes="mcp-test-btn",
991
+ )
992
+ test_button.disabled = self._read_only
993
+ test_button.display = not self._read_only
994
+ yield test_button
995
+
996
+ def _header_title(self, name: str) -> str:
997
+ # An unnamed server is told apart by position, with the label its Save errors use.
998
+ return name.strip() or render_str(widget_localizer(self), MCP_SERVER_CONTEXT.bind(index=self._index + 1))
999
+
1000
+ @on(Input.Changed)
1001
+ def _on_name_changed(self, event: Input.Changed) -> None:
1002
+ # Not stopped: the config screen's dirty tracking also reads this event.
1003
+ if event.input.id == f"mcp-name-{self._index}":
1004
+ self.set_title(self._header_title(event.value))
1005
+
1006
+ def _compose_transport_fields(self, cfg: MCPServerConfig) -> ComposeResult:
1007
+ localizer = widget_localizer(self)
1008
+ if self._transport == "http":
1009
+ yield Label(f"[red]*[/red] {escape(render_str(localizer, _URL.bind()))}", classes="mcp-label")
1010
+ url = Input(
1011
+ value=cfg.url or "",
1012
+ placeholder=render_str(localizer, _URL_PLACEHOLDER.bind()),
1013
+ id=f"mcp-url-{self._index}",
1014
+ )
1015
+ url.disabled = self._read_only
1016
+ yield url
1017
+ skip_tls = not cfg.verify_ssl
1018
+ skip_tls_checkbox = Checkbox(
1019
+ render_str(localizer, _SKIP_TLS_VERIFICATION.bind()),
1020
+ value=skip_tls,
1021
+ id=f"mcp-skip-tls-{self._index}",
1022
+ classes="mcp-skip-tls",
1023
+ )
1024
+ skip_tls_checkbox.disabled = self._read_only
1025
+ yield skip_tls_checkbox
1026
+ hint = Label(
1027
+ render_str(localizer, _INSECURE_TLS.bind()),
1028
+ id=f"mcp-skip-tls-hint-{self._index}",
1029
+ classes="mcp-skip-tls-hint",
1030
+ )
1031
+ hint.display = skip_tls
1032
+ yield hint
1033
+ bypass_proxy = Checkbox(
1034
+ render_str(localizer, _BYPASS_PROXY.bind()),
1035
+ value=cfg.bypass_proxy,
1036
+ id=f"mcp-bypass-proxy-{self._index}",
1037
+ classes="mcp-bypass-proxy",
1038
+ )
1039
+ bypass_proxy.disabled = self._read_only
1040
+ yield bypass_proxy
1041
+ yield Label(render_str(localizer, _HEADERS.bind()), classes="mcp-label")
1042
+ with Vertical(classes="mcp-list", id=f"mcp-headers-{self._index}"):
1043
+ header_rows = self._key_value_drafts.headers
1044
+ if header_rows is None:
1045
+ header_rows = list(cfg.headers.items())
1046
+ for key, val in header_rows:
1047
+ yield self._compose_key_value_row(_MCP_HEADER_ROWS, key, val)
1048
+ if not header_rows:
1049
+ yield self._compose_key_value_row(_MCP_HEADER_ROWS, "", "")
1050
+ yield self._make_key_value_add_row(_MCP_HEADER_ROWS)
1051
+ return
1052
+
1053
+ yield Label(f"[red]*[/red] {escape(render_str(localizer, _COMMAND.bind()))}", classes="mcp-label")
1054
+ command_line_text = (
1055
+ self._command_line_text
1056
+ if self._command_line_text is not None
1057
+ else self._format_command_line(cfg.command, cfg.args)
1058
+ )
1059
+ command = EnhancedTextArea(
1060
+ command_line_text,
1061
+ id=f"mcp-cmd-{self._index}",
1062
+ placeholder=render_str(localizer, _COMMAND_PLACEHOLDER.bind()),
1063
+ )
1064
+ command.read_only = self._read_only
1065
+ yield command
1066
+ yield Label(render_str(localizer, _ENVIRONMENT_VARIABLES.bind()), classes="mcp-label")
1067
+ with Vertical(classes="mcp-list", id=f"mcp-env-{self._index}"):
1068
+ env_rows = self._key_value_drafts.env
1069
+ if env_rows is None:
1070
+ env_rows = list(cfg.env.items())
1071
+ for key, val in env_rows:
1072
+ yield self._compose_key_value_row(_MCP_ENV_ROWS, key, val)
1073
+ if not env_rows:
1074
+ yield self._compose_key_value_row(_MCP_ENV_ROWS, "", "")
1075
+ yield self._make_key_value_add_row(_MCP_ENV_ROWS)
1076
+
1077
+ def _transport_options_title(self) -> str:
1078
+ definition = _HTTP_OPTIONS if self._transport == "http" else _STDIO_OPTIONS
1079
+ return render_str(widget_localizer(self), definition.bind())
1080
+
1081
+ @classmethod
1082
+ def _next_uid(cls) -> int:
1083
+ cls._uid_counter += 1
1084
+ return cls._uid_counter
1085
+
1086
+ def _compose_key_value_row(self, spec: _MCPKeyValueRows, key: str, val: str) -> Horizontal:
1087
+ uid = f"{self._index}-{self._next_uid()}"
1088
+ localizer = widget_localizer(self)
1089
+ key_input = Input(
1090
+ value=key,
1091
+ placeholder=render_str(localizer, spec.key_placeholder.bind()),
1092
+ classes="mcp-field mcp-item-key-input",
1093
+ )
1094
+ key_input.disabled = self._read_only
1095
+ value_input = Input(
1096
+ value=val,
1097
+ placeholder=render_str(localizer, spec.value_placeholder.bind()),
1098
+ classes="mcp-field mcp-item-value-input",
1099
+ )
1100
+ value_input.disabled = self._read_only
1101
+ remove_button = Button("✕", classes="mcp-remove-btn", id=f"mcp-{spec.action_prefix}del-{uid}")
1102
+ remove_button.disabled = self._read_only
1103
+ remove_button.display = not self._read_only
1104
+ return Horizontal(
1105
+ key_input,
1106
+ value_input,
1107
+ remove_button,
1108
+ classes="mcp-item-row",
1109
+ )
1110
+
1111
+ def _key_value_container_id(self, spec: _MCPKeyValueRows) -> str:
1112
+ return f"mcp-{spec.container_suffix}-{self._index}"
1113
+
1114
+ def _key_value_add_button_id(self, spec: _MCPKeyValueRows) -> str:
1115
+ return f"mcp-{spec.action_prefix}add-{self._index}"
1116
+
1117
+ def _key_value_add_row_id(self, spec: _MCPKeyValueRows) -> str:
1118
+ return f"mcp-{spec.action_prefix}add-row-{self._index}"
1119
+
1120
+ def _key_value_delete_prefix(self, spec: _MCPKeyValueRows) -> str:
1121
+ return f"mcp-{spec.action_prefix}del-{self._index}-"
1122
+
1123
+ def _mark_key_value_user_modified(self, spec: _MCPKeyValueRows) -> None:
1124
+ if spec is _MCP_HEADER_ROWS:
1125
+ self._headers_user_modified = True
1126
+ return
1127
+ if spec is _MCP_ENV_ROWS:
1128
+ self._env_user_modified = True
1129
+ return
1130
+ raise ValueError("Unknown MCP key/value row spec")
1131
+
1132
+ def _make_key_value_add_row(self, spec: _MCPKeyValueRows) -> Horizontal:
1133
+ add_button = ConfigAddButton(
1134
+ render_str(widget_localizer(self), _ADD.bind()),
1135
+ compact=True,
1136
+ classes="mcp-small-btn",
1137
+ id=self._key_value_add_button_id(spec),
1138
+ )
1139
+ add_button.disabled = self._read_only
1140
+ add_button.display = not self._read_only
1141
+ return Horizontal(
1142
+ add_button,
1143
+ classes="mcp-row",
1144
+ id=self._key_value_add_row_id(spec),
1145
+ )
1146
+
1147
+ async def _add_key_value_row(self, spec: _MCPKeyValueRows) -> None:
1148
+ if self._read_only:
1149
+ return
1150
+ try:
1151
+ container = self.query_one(f"#{self._key_value_container_id(spec)}", Vertical)
1152
+ add_row = self.query_one(f"#{self._key_value_add_row_id(spec)}", Horizontal)
1153
+ except Exception:
1154
+ return
1155
+
1156
+ self._mark_key_value_user_modified(spec)
1157
+ await container.mount(self._compose_key_value_row(spec, "", ""), before=add_row)
1158
+ container.refresh(layout=True)
1159
+ self.refresh(layout=True)
1160
+
1161
+ @on(Checkbox.Changed)
1162
+ def _on_checkbox_changed(self, event: Checkbox.Changed) -> None:
1163
+ if self._read_only:
1164
+ return
1165
+ hint_id_by_checkbox_id = {
1166
+ f"mcp-skip-tls-{self._index}": f"mcp-skip-tls-hint-{self._index}",
1167
+ f"mcp-load-prompts-{self._index}": f"mcp-load-prompts-hint-{self._index}",
1168
+ f"mcp-expose-instructions-{self._index}": f"mcp-expose-instructions-hint-{self._index}",
1169
+ }
1170
+ hint_id = hint_id_by_checkbox_id.get(event.checkbox.id or "")
1171
+ if hint_id is None:
1172
+ return
1173
+ with contextlib.suppress(Exception):
1174
+ hint = self.query_one(f"#{hint_id}", Label)
1175
+ hint.display = bool(event.value)
1176
+ event.checkbox.set_class(not event.value, "-hint-collapsed")
1177
+
1178
+ @on(Select.Changed)
1179
+ async def _on_select_changed(self, event: Select.Changed) -> None:
1180
+ if self._read_only:
1181
+ return
1182
+ widget_id = event.select.id or ""
1183
+ if widget_id in {
1184
+ f"mcp-tool-access-{self._index}",
1185
+ f"mcp-loading-strategy-{self._index}",
1186
+ }:
1187
+ self._sync_tool_policy_fields()
1188
+ return
1189
+ if widget_id != f"mcp-transport-{self._index}":
1190
+ return
1191
+ transport = str(event.value)
1192
+ if not _is_transport(transport) or transport == self._transport:
1193
+ return
1194
+ self._transport = transport
1195
+ await self._rebuild_transport_fields()
1196
+
1197
+ @on(Button.Pressed)
1198
+ async def _on_button(self, event: Button.Pressed) -> None:
1199
+ if self._read_only:
1200
+ return
1201
+ button = event.button
1202
+ if button.id == f"mcp-test-btn-{self._index}":
1203
+ self.post_message(self.TestRequested(self))
1204
+ return
1205
+
1206
+ if button.id == self._key_value_add_button_id(_MCP_HEADER_ROWS):
1207
+ await self._add_key_value_row(_MCP_HEADER_ROWS)
1208
+ return
1209
+
1210
+ if button.id == self._key_value_add_button_id(_MCP_ENV_ROWS):
1211
+ await self._add_key_value_row(_MCP_ENV_ROWS)
1212
+ return
1213
+
1214
+ button_id = button.id or ""
1215
+ for spec in (_MCP_HEADER_ROWS, _MCP_ENV_ROWS):
1216
+ if button_id.startswith(self._key_value_delete_prefix(spec)):
1217
+ # Await the removal so the row is gone from the DOM before this
1218
+ # handler returns and Button.Pressed bubbles to AgentsConfigScreen.
1219
+ # The screen's call_after_refresh dirty re-evaluation rebuilds the
1220
+ # profile from the mounted rows; if the row were still present (the
1221
+ # prior fire-and-forget row.remove()/call_later races lost this on
1222
+ # CI) the rebuilt profile would match saved and clear the dirty flag.
1223
+ await self._remove_key_value_row(spec, button)
1224
+ return
1225
+
1226
+ async def _remove_key_value_row(self, spec: _MCPKeyValueRows, button: Button) -> None:
1227
+ if self._read_only:
1228
+ return
1229
+ self._mark_key_value_user_modified(spec)
1230
+ row = button.parent
1231
+ if isinstance(row, Widget) and isinstance(row.parent, Vertical):
1232
+ container = row.parent
1233
+ was_last_row = len(list(container.query(".mcp-item-row"))) <= 1
1234
+ if was_last_row:
1235
+ row.query_one(".mcp-item-key-input", Input).value = ""
1236
+ row.query_one(".mcp-item-value-input", Input).value = ""
1237
+ row.refresh(layout=True)
1238
+ else:
1239
+ await row.remove()
1240
+ self.refresh(layout=True)
1241
+
1242
+ async def _rebuild_transport_fields(self) -> None:
1243
+ container = self.query_one(f"#mcp-transport-fields-{self._index}", Vertical)
1244
+ await container.remove_children()
1245
+ container.border_title = self._transport_options_title()
1246
+ if self._transport == "http":
1247
+ self._headers_user_modified = True
1248
+ else:
1249
+ self._env_user_modified = True
1250
+
1251
+ # Build the new subtree fully composed before mounting. Mounting
1252
+ # an empty container and then mounting children into it races on
1253
+ # Windows: ``await parent.mount(child)`` doesn't always leave
1254
+ # ``child.is_attached`` True by the time the next mount fires.
1255
+ new_widgets: list[Widget] = []
1256
+ if self._transport == "http":
1257
+ localizer = widget_localizer(self)
1258
+ hint = Label(
1259
+ render_str(localizer, _INSECURE_TLS.bind()),
1260
+ id=f"mcp-skip-tls-hint-{self._index}",
1261
+ classes="mcp-skip-tls-hint",
1262
+ )
1263
+ hint.display = False
1264
+ headers_container = Vertical(
1265
+ self._compose_key_value_row(_MCP_HEADER_ROWS, "", ""),
1266
+ self._make_key_value_add_row(_MCP_HEADER_ROWS),
1267
+ classes="mcp-list",
1268
+ id=f"mcp-headers-{self._index}",
1269
+ )
1270
+ new_widgets.extend(
1271
+ [
1272
+ Label(f"[red]*[/red] {escape(render_str(localizer, _URL.bind()))}", classes="mcp-label"),
1273
+ Input(
1274
+ placeholder=render_str(localizer, _URL_PLACEHOLDER.bind()),
1275
+ id=f"mcp-url-{self._index}",
1276
+ ),
1277
+ Checkbox(
1278
+ render_str(localizer, _SKIP_TLS_VERIFICATION.bind()),
1279
+ value=False,
1280
+ id=f"mcp-skip-tls-{self._index}",
1281
+ classes="mcp-skip-tls",
1282
+ ),
1283
+ hint,
1284
+ Checkbox(
1285
+ render_str(localizer, _BYPASS_PROXY.bind()),
1286
+ value=False,
1287
+ id=f"mcp-bypass-proxy-{self._index}",
1288
+ classes="mcp-bypass-proxy",
1289
+ ),
1290
+ Label(render_str(localizer, _HEADERS.bind()), classes="mcp-label"),
1291
+ headers_container,
1292
+ ]
1293
+ )
1294
+ else:
1295
+ localizer = widget_localizer(self)
1296
+ env_container = Vertical(
1297
+ self._compose_key_value_row(_MCP_ENV_ROWS, "", ""),
1298
+ self._make_key_value_add_row(_MCP_ENV_ROWS),
1299
+ classes="mcp-list",
1300
+ id=f"mcp-env-{self._index}",
1301
+ )
1302
+ new_widgets.extend(
1303
+ [
1304
+ Label(f"[red]*[/red] {escape(render_str(localizer, _COMMAND.bind()))}", classes="mcp-label"),
1305
+ EnhancedTextArea(
1306
+ id=f"mcp-cmd-{self._index}",
1307
+ placeholder=render_str(localizer, _COMMAND_PLACEHOLDER.bind()),
1308
+ ),
1309
+ Label(render_str(localizer, _ENVIRONMENT_VARIABLES.bind()), classes="mcp-label"),
1310
+ env_container,
1311
+ ]
1312
+ )
1313
+
1314
+ await container.mount(*new_widgets)
1315
+ container.refresh(layout=True)
1316
+ self.refresh(layout=True)
1317
+
1318
+ def get_config(self) -> MCPServerConfig:
1319
+ """Read current widget state into an MCPServerConfig."""
1320
+ return self._read_config(default_blank_name=True)
1321
+
1322
+ def _snapshot_config(self) -> MCPServerConfig:
1323
+ """Read widget state without normalizing incomplete user input."""
1324
+ return self._read_config(default_blank_name=False, allow_invalid_command=True)
1325
+
1326
+ def _snapshot_command_line_text(self) -> str | None:
1327
+ """Return raw stdio command text for UI-only rebuild preservation."""
1328
+ if self._transport != "stdio":
1329
+ return None
1330
+ with contextlib.suppress(NoMatches):
1331
+ return self.query_one(f"#mcp-cmd-{self._index}", EnhancedTextArea).text
1332
+ return self._command_line_text
1333
+
1334
+ def _snapshot_key_value_rows(self, spec: _MCPKeyValueRows) -> list[tuple[str, str]] | None:
1335
+ """Return raw header/env rows for UI-only rebuild preservation."""
1336
+ if spec is _MCP_HEADER_ROWS and self._transport != "http":
1337
+ return None
1338
+ if spec is _MCP_ENV_ROWS and self._transport != "stdio":
1339
+ return None
1340
+
1341
+ with contextlib.suppress(NoMatches):
1342
+ container = self.query_one(f"#{self._key_value_container_id(spec)}", Vertical)
1343
+ rows: list[tuple[str, str]] = []
1344
+ for row in container.query(".mcp-item-row"):
1345
+ key = row.query_one(".mcp-item-key-input", Input).value
1346
+ value = row.query_one(".mcp-item-value-input", Input).value
1347
+ rows.append((key, value))
1348
+ return rows
1349
+
1350
+ if spec is _MCP_HEADER_ROWS:
1351
+ return self._key_value_drafts.headers
1352
+ return self._key_value_drafts.env
1353
+
1354
+ def _snapshot_key_value_drafts(self) -> _MCPKeyValueDrafts:
1355
+ """Return raw key/value rows for UI-only rebuild preservation."""
1356
+ return _MCPKeyValueDrafts(
1357
+ headers=self._snapshot_key_value_rows(_MCP_HEADER_ROWS),
1358
+ env=self._snapshot_key_value_rows(_MCP_ENV_ROWS),
1359
+ )
1360
+
1361
+ def _read_config(self, *, default_blank_name: bool, allow_invalid_command: bool = False) -> MCPServerConfig:
1362
+ from chrys.service.profiles.agents.schema import MCPServerConfig as MCPCfg
1363
+
1364
+ enabled = self._config.enabled
1365
+ with contextlib.suppress(NoMatches):
1366
+ enabled = bool(self.query_one(f"#mcp-enabled-{self._index}", Checkbox).value)
1367
+
1368
+ name = self._config.name.strip()
1369
+ with contextlib.suppress(NoMatches):
1370
+ name = self.query_one(f"#mcp-name-{self._index}", Input).value.strip()
1371
+ transport = self._transport
1372
+ description = self._config.description.strip()
1373
+ with contextlib.suppress(NoMatches):
1374
+ description = self.query_one(f"#mcp-desc-{self._index}", EnhancedTextArea).text.strip()
1375
+ access_mode = self._tool_access_mode(self._config.allowed_tools)
1376
+ with contextlib.suppress(NoMatches):
1377
+ access_mode = str(self.query_one(f"#mcp-tool-access-{self._index}", Select).value)
1378
+ if access_mode == _TOOL_ACCESS_ALL:
1379
+ allowed_tools = None
1380
+ elif access_mode == _TOOL_ACCESS_NONE:
1381
+ allowed_tools = []
1382
+ else:
1383
+ allowed_tools = list(self._config.allowed_tools or [])
1384
+ with contextlib.suppress(NoMatches):
1385
+ allowed_tools = self._parse_allowed_tools(self.query_one(f"#mcp-tools-{self._index}", Input).value)
1386
+ tool_name_prefix = self._config.tool_name_prefix.strip()
1387
+ with contextlib.suppress(NoMatches):
1388
+ tool_name_prefix = self.query_one(f"#mcp-prefix-{self._index}", Input).value.strip()
1389
+
1390
+ timeout = self._config.request_timeout
1391
+ with contextlib.suppress(NoMatches):
1392
+ timeout_text = self.query_one(f"#mcp-timeout-{self._index}", Input).value.strip()
1393
+ timeout = None
1394
+ if timeout_text:
1395
+ with contextlib.suppress(ValueError):
1396
+ timeout = int(timeout_text)
1397
+
1398
+ url = self._config.url if transport == "http" else ""
1399
+ command = self._config.command if transport == "stdio" else ""
1400
+ args = list(self._config.args) if transport == "stdio" else []
1401
+ headers = dict(self._config.headers) if transport == "http" else {}
1402
+ env = dict(self._config.env) if transport == "stdio" else {}
1403
+ verify_ssl = self._config.verify_ssl if transport == "http" else True
1404
+ bypass_proxy = self._config.bypass_proxy if transport == "http" else False
1405
+
1406
+ if transport == "http":
1407
+ with contextlib.suppress(NoMatches):
1408
+ url = self.query_one(f"#mcp-url-{self._index}", Input).value.strip()
1409
+ with contextlib.suppress(NoMatches):
1410
+ skip_tls = bool(self.query_one(f"#mcp-skip-tls-{self._index}", Checkbox).value)
1411
+ verify_ssl = not skip_tls
1412
+ with contextlib.suppress(NoMatches):
1413
+ bypass_proxy = bool(self.query_one(f"#mcp-bypass-proxy-{self._index}", Checkbox).value)
1414
+ # Keep seeded headers unless the mounted rows can be read as a complete set.
1415
+ try:
1416
+ parsed_headers = self._parse_key_value_rows(
1417
+ self._key_value_container_id(_MCP_HEADER_ROWS),
1418
+ self._key_value_add_row_id(_MCP_HEADER_ROWS),
1419
+ user_modified=self._headers_user_modified,
1420
+ )
1421
+ except NoMatches:
1422
+ pass
1423
+ else:
1424
+ headers = parsed_headers
1425
+ else:
1426
+ with contextlib.suppress(NoMatches):
1427
+ command_line = self.query_one(f"#mcp-cmd-{self._index}", EnhancedTextArea).text.strip()
1428
+ try:
1429
+ command, args = self._split_command_line(command_line, render_message=self._render_message)
1430
+ except ValueError:
1431
+ if not allow_invalid_command:
1432
+ raise
1433
+ # Keep seeded env unless the mounted rows can be read as a complete set.
1434
+ try:
1435
+ parsed_env = self._parse_key_value_rows(
1436
+ self._key_value_container_id(_MCP_ENV_ROWS),
1437
+ self._key_value_add_row_id(_MCP_ENV_ROWS),
1438
+ user_modified=self._env_user_modified,
1439
+ )
1440
+ except NoMatches:
1441
+ pass
1442
+ else:
1443
+ env = parsed_env
1444
+
1445
+ load_prompts = self._config.load_prompts
1446
+ with contextlib.suppress(NoMatches):
1447
+ load_prompts = bool(self.query_one(f"#mcp-load-prompts-{self._index}", Checkbox).value)
1448
+ expose_instructions = self._config.expose_instructions
1449
+ with contextlib.suppress(NoMatches):
1450
+ expose_instructions = bool(self.query_one(f"#mcp-expose-instructions-{self._index}", Checkbox).value)
1451
+ loading_strategy = _TOOL_LOADING_PROGRESSIVE if self._config.use_progressive_disclosure else _TOOL_LOADING_FULL
1452
+ with contextlib.suppress(NoMatches):
1453
+ loading_strategy = str(self.query_one(f"#mcp-loading-strategy-{self._index}", Select).value)
1454
+ use_progressive_disclosure = access_mode != _TOOL_ACCESS_NONE and loading_strategy == _TOOL_LOADING_PROGRESSIVE
1455
+ always_load: list[str] = []
1456
+ if use_progressive_disclosure:
1457
+ always_load = list(self._config.always_load)
1458
+ with contextlib.suppress(NoMatches):
1459
+ always_load = self._parse_allowed_tools(self.query_one(f"#mcp-always-load-{self._index}", Input).value)
1460
+
1461
+ if default_blank_name and not name:
1462
+ name = f"server-{self._index}"
1463
+
1464
+ return MCPCfg(
1465
+ name=name,
1466
+ transport=transport,
1467
+ command=command,
1468
+ args=args,
1469
+ encoding=self._config.encoding if transport == "stdio" else None,
1470
+ env=env,
1471
+ url=url,
1472
+ headers=headers,
1473
+ resolve_header_templates=self._config.resolve_header_templates if transport == "http" else True,
1474
+ terminate_on_close=self._config.terminate_on_close if transport == "http" else None,
1475
+ verify_ssl=verify_ssl,
1476
+ bypass_proxy=bypass_proxy,
1477
+ enabled=enabled,
1478
+ description=description,
1479
+ tool_name_prefix=tool_name_prefix,
1480
+ allowed_tools=allowed_tools,
1481
+ request_timeout=timeout,
1482
+ max_tool_result_tokens=self._config.max_tool_result_tokens,
1483
+ load_prompts=load_prompts,
1484
+ expose_instructions=expose_instructions,
1485
+ use_progressive_disclosure=use_progressive_disclosure,
1486
+ always_load=always_load,
1487
+ )
1488
+
1489
+ def validate(self) -> list[str]:
1490
+ """Return validation errors for this server."""
1491
+ localizer = widget_localizer(self)
1492
+ errors: list[str] = []
1493
+ name = self.query_one(f"#mcp-name-{self._index}", Input).value.strip()
1494
+ display_name = name or render_str(localizer, MCP_SERVER_CONTEXT.bind(index=self._index + 1))
1495
+
1496
+ if not name:
1497
+ context = render_str(localizer, MCP_SERVER_CONTEXT.bind(index=self._index + 1))
1498
+ errors.append(
1499
+ self._render_context_error(
1500
+ context,
1501
+ FIELD_REQUIRED.bind(field=render_str(localizer, NAME_FIELD.bind())),
1502
+ )
1503
+ )
1504
+
1505
+ if self._transport == "http":
1506
+ with contextlib.suppress(Exception):
1507
+ url = self.query_one(f"#mcp-url-{self._index}", Input).value.strip()
1508
+ if not url:
1509
+ errors.append(self._render_server_error(display_name, MCP_URL_REQUIRED.bind()))
1510
+ elif not url.startswith(("http://", "https://")):
1511
+ errors.append(self._render_server_error(display_name, MCP_URL_SCHEME.bind()))
1512
+ else:
1513
+ with contextlib.suppress(Exception):
1514
+ command_line = self.query_one(f"#mcp-cmd-{self._index}", EnhancedTextArea).text.strip()
1515
+ if not command_line:
1516
+ errors.append(self._render_server_error(display_name, MCP_COMMAND_REQUIRED.bind()))
1517
+ else:
1518
+ try:
1519
+ command, _args = self._split_command_line(command_line, render_message=self._render_message)
1520
+ if not command:
1521
+ errors.append(self._render_server_error(display_name, MCP_COMMAND_REQUIRED.bind()))
1522
+ except ValueError as exc:
1523
+ errors.append(self._render_server_message(display_name, str(exc)))
1524
+
1525
+ timeout_text = self.query_one(f"#mcp-timeout-{self._index}", Input).value.strip()
1526
+ if timeout_text:
1527
+ try:
1528
+ timeout = int(timeout_text)
1529
+ if timeout <= 0:
1530
+ errors.append(self._render_server_error(display_name, MCP_TIMEOUT_POSITIVE.bind()))
1531
+ except ValueError:
1532
+ errors.append(self._render_server_error(display_name, MCP_TIMEOUT_VALID.bind()))
1533
+
1534
+ prefix_text = self.query_one(f"#mcp-prefix-{self._index}", Input).value.strip()
1535
+
1536
+ access_mode = str(self.query_one(f"#mcp-tool-access-{self._index}", Select).value)
1537
+ selected_tools_text = self.query_one(f"#mcp-tools-{self._index}", Input).value
1538
+ selected_tools = self._parse_allowed_tools(selected_tools_text)
1539
+ if access_mode == _TOOL_ACCESS_SELECTED:
1540
+ errors.extend(
1541
+ self._render_server_message(display_name, error)
1542
+ for error in self._validate_comma_separated_tool_names(
1543
+ selected_tools_text,
1544
+ label_definition=_SELECTED_TOOL_NAMES,
1545
+ )
1546
+ )
1547
+ if not selected_tools:
1548
+ errors.append(
1549
+ self._render_server_error(
1550
+ display_name,
1551
+ MCP_SELECT_PERMITTED.bind(
1552
+ no_tools=render_str(localizer, _TOOL_ACCESS_NONE_LABEL.bind()),
1553
+ ),
1554
+ )
1555
+ )
1556
+
1557
+ loading_strategy = str(self.query_one(f"#mcp-loading-strategy-{self._index}", Select).value)
1558
+ progressive = access_mode != _TOOL_ACCESS_NONE and loading_strategy == _TOOL_LOADING_PROGRESSIVE
1559
+ prefix_error = validate_mcp_tool_name_prefix(
1560
+ prefix_text,
1561
+ generated_suffixes=MCP_PROGRESSIVE_CONTROL_TOOL_NAMES if progressive else (),
1562
+ )
1563
+ if prefix_error is not None:
1564
+ errors.append(self._render_server_message(display_name, prefix_error))
1565
+ if progressive:
1566
+ always_load_text = self.query_one(f"#mcp-always-load-{self._index}", Input).value
1567
+ always_load = self._parse_allowed_tools(always_load_text)
1568
+ errors.extend(
1569
+ self._render_server_message(display_name, error)
1570
+ for error in self._validate_comma_separated_tool_names(
1571
+ always_load_text,
1572
+ label_definition=MCP_INITIALLY_VISIBLE_TOOLS_VALIDATION,
1573
+ )
1574
+ )
1575
+ if access_mode == _TOOL_ACCESS_SELECTED:
1576
+ outside_scope = [name for name in always_load if name not in set(selected_tools)]
1577
+ if outside_scope:
1578
+ errors.append(
1579
+ self._render_server_error(
1580
+ display_name,
1581
+ MCP_INITIAL_TOOLS_PERMITTED.bind(
1582
+ label=render_str(localizer, MCP_INITIALLY_VISIBLE_TOOLS_VALIDATION.bind()),
1583
+ names=DisplayBlock(", ".join(outside_scope)),
1584
+ ),
1585
+ )
1586
+ )
1587
+
1588
+ if self._transport == "http":
1589
+ with contextlib.suppress(Exception):
1590
+ errors.extend(
1591
+ self._validate_key_value_rows(
1592
+ self._key_value_container_id(_MCP_HEADER_ROWS),
1593
+ _HEADERS,
1594
+ display_name,
1595
+ )
1596
+ )
1597
+
1598
+ if self._transport == "stdio":
1599
+ with contextlib.suppress(Exception):
1600
+ errors.extend(
1601
+ self._validate_key_value_rows(
1602
+ self._key_value_container_id(_MCP_ENV_ROWS),
1603
+ _ENVIRONMENT_VARIABLES,
1604
+ display_name,
1605
+ )
1606
+ )
1607
+
1608
+ return errors
1609
+
1610
+ def _render_message(self, reference: MessageRef) -> str:
1611
+ return render_str(widget_localizer(self), reference)
1612
+
1613
+ def _render_context_error(self, context: str, reference: MessageRef) -> str:
1614
+ return self._render_message(
1615
+ CONTEXT_ERROR.bind(
1616
+ context=DisplayBlock(context),
1617
+ message=DisplayBlock(self._render_message(reference)),
1618
+ )
1619
+ )
1620
+
1621
+ def _render_server_error(self, display_name: str, reference: MessageRef) -> str:
1622
+ return self._render_server_message(display_name, self._render_message(reference))
1623
+
1624
+ def _render_server_message(self, display_name: str, message: str) -> str:
1625
+ return self._render_message(
1626
+ SERVER_ERROR.bind(
1627
+ name=DisplayBlock(display_name),
1628
+ message=DisplayBlock(message),
1629
+ )
1630
+ )
1631
+
1632
+ def _validate_key_value_rows(
1633
+ self,
1634
+ container_id: str,
1635
+ label_definition: MessageDef,
1636
+ display_name: str,
1637
+ ) -> list[str]:
1638
+ """Validate that partially-filled key-value rows are not saved silently."""
1639
+ localizer = widget_localizer(self)
1640
+ label = render_str(localizer, label_definition.bind())
1641
+ errors: list[str] = []
1642
+ seen: set[str] = set()
1643
+ container = self.query_one(f"#{container_id}", Vertical)
1644
+ for index, row in enumerate(container.query(".mcp-item-row"), start=1):
1645
+ key = row.query_one(".mcp-item-key-input", Input).value.strip()
1646
+ value = row.query_one(".mcp-item-value-input", Input).value.strip()
1647
+ if not key and not value:
1648
+ continue
1649
+ if key and not value:
1650
+ errors.append(
1651
+ self._render_server_error(
1652
+ display_name,
1653
+ MCP_ROW_VALUE_REQUIRED.bind(
1654
+ label=label,
1655
+ row=index,
1656
+ key=DisplayBlock(key),
1657
+ ),
1658
+ )
1659
+ )
1660
+ elif value and not key:
1661
+ errors.append(
1662
+ self._render_server_error(
1663
+ display_name,
1664
+ MCP_ROW_KEY_REQUIRED.bind(label=label, row=index),
1665
+ )
1666
+ )
1667
+ if key in seen:
1668
+ errors.append(
1669
+ self._render_server_error(
1670
+ display_name,
1671
+ DUPLICATE_KEY_ROW.bind(
1672
+ label=label,
1673
+ row=index,
1674
+ key=DisplayBlock(key),
1675
+ ),
1676
+ )
1677
+ )
1678
+ if key:
1679
+ seen.add(key)
1680
+ return errors
1681
+
1682
+
1683
+ class MCPConfigPanel(VerticalScroll):
1684
+ """Composable widget for the MCP configuration tab."""
1685
+
1686
+ DEFAULT_CSS = """
1687
+ MCPConfigPanel {
1688
+ height: 1fr;
1689
+ padding: 0 0 0 2;
1690
+ scrollbar-size-vertical: 1;
1691
+ }
1692
+ MCPConfigPanel .mcp-section-title {
1693
+ color: $secondary;
1694
+ text-style: bold;
1695
+ height: 1;
1696
+ }
1697
+ MCPConfigPanel .mcp-section-desc {
1698
+ color: $text-muted;
1699
+ height: 1;
1700
+ margin: 0 0 1 0;
1701
+ }
1702
+ MCPConfigPanel .mcp-field {
1703
+ width: 1fr;
1704
+ height: auto;
1705
+ margin: 0 1 0 0;
1706
+ }
1707
+ MCPConfigPanel #mcp-add-btn {
1708
+ min-width: 10;
1709
+ height: 1;
1710
+ margin: 0;
1711
+ }
1712
+ MCPConfigPanel .mcp-header-bar {
1713
+ height: auto;
1714
+ margin: 0 0 1 0;
1715
+ }
1716
+ MCPConfigPanel #mcp-cards {
1717
+ height: auto;
1718
+ }
1719
+ MCPConfigPanel .mcp-empty {
1720
+ margin: 0 2 1 0;
1721
+ }
1722
+ """
1723
+
1724
+ def __init__(
1725
+ self,
1726
+ mcp_servers: list[MCPServerConfig] | None = None,
1727
+ *,
1728
+ card_folds: Sequence[bool] | None = None,
1729
+ workspace_cwd: str | None = None,
1730
+ read_only: bool = False,
1731
+ additional_reserved_tool_names: Callable[[], Collection[str]] | None = None,
1732
+ ) -> None:
1733
+ servers = list(mcp_servers or [])
1734
+ # Folds from an earlier panel for these servers (see ``card_folds``); otherwise saved servers open folded.
1735
+ folds = list(card_folds) if card_folds is not None and len(card_folds) == len(servers) else None
1736
+ self._rows = [
1737
+ _MCPServerRow(server, collapsed=True if folds is None else folds[index])
1738
+ for index, server in enumerate(servers)
1739
+ ]
1740
+ self._read_only = read_only
1741
+ self._additional_reserved_tool_names = additional_reserved_tool_names
1742
+ self._testing: set[int] = set()
1743
+ self._adapter = MCPAdapter(
1744
+ stdio_cwd=workspace_cwd or None,
1745
+ reserved_tool_names=self._current_reserved_tool_names(),
1746
+ )
1747
+ super().__init__()
1748
+
1749
+ def _render_message(self, reference: MessageRef) -> str:
1750
+ return render_str(widget_localizer(self), reference)
1751
+
1752
+ def _current_reserved_tool_names(self) -> set[str]:
1753
+ names = chrys_reserved_tool_names()
1754
+ if self._additional_reserved_tool_names is not None:
1755
+ names.update(self._additional_reserved_tool_names())
1756
+ return names
1757
+
1758
+ @property
1759
+ def test_adapter(self) -> MCPAdapter:
1760
+ """Expose adapter for tests."""
1761
+ return self._adapter
1762
+
1763
+ def compose(self) -> ComposeResult:
1764
+ localizer = widget_localizer(self)
1765
+ with Vertical(classes="mcp-header-bar"), Vertical(classes="mcp-field"):
1766
+ yield Label(render_str(localizer, _SERVERS.bind()), classes="mcp-section-title")
1767
+ yield Label(render_str(localizer, _SERVERS_DESCRIPTION.bind()), classes="mcp-section-desc")
1768
+ add_button = ConfigAddButton(render_str(localizer, _ADD.bind()), id="mcp-add-btn")
1769
+ add_button.disabled = self._read_only
1770
+ add_button.display = not self._read_only
1771
+ yield add_button
1772
+ yield Vertical(id="mcp-cards")
1773
+
1774
+ async def on_mount(self) -> None:
1775
+ await self._rebuild_cards()
1776
+
1777
+ async def _rebuild_cards(self) -> None:
1778
+ container = self.query_one("#mcp-cards", Vertical)
1779
+ focused = self.screen.focused
1780
+ if focused is not None and container in focused.ancestors and self.focusable:
1781
+ # Removing the focused card (its ✕ after a delete) makes Textual focus a neighbour and
1782
+ # animate the scroll to it; the panel itself stays put and keeps the keyboard.
1783
+ self.screen.set_focus(self, scroll_visible=False)
1784
+ replacements = (
1785
+ [
1786
+ MCPConnectionCard(
1787
+ row.config,
1788
+ index,
1789
+ command_line_text=row.command_line_text,
1790
+ key_value_drafts=row.key_value_drafts,
1791
+ read_only=self._read_only,
1792
+ collapsed=row.collapsed,
1793
+ )
1794
+ for index, row in enumerate(self._rows)
1795
+ ]
1796
+ if self._rows
1797
+ else [HatchedEmptyState(render_str(widget_localizer(self), _EMPTY.bind()), classes="mcp-empty")]
1798
+ )
1799
+ # One batch: a layout between the removal and the mount would clamp the scroll to the emptied list.
1800
+ with self.app.batch_update():
1801
+ await container.remove_children()
1802
+ await container.mount(*replacements)
1803
+
1804
+ @on(Button.Pressed, "#mcp-add-btn")
1805
+ async def _on_add(self, _event: Button.Pressed) -> None:
1806
+ if self._read_only:
1807
+ return
1808
+ from chrys.service.profiles.agents.schema import MCPServerConfig
1809
+
1810
+ if self._refuse_while_testing(_WAIT_BEFORE_ADDING):
1811
+ return
1812
+ new_config = MCPServerConfig(name="New Server", transport="stdio")
1813
+ self._rows = self._snapshot_rows(preserve_blank_names=True)
1814
+ self._rows.insert(0, _MCPServerRow(new_config, collapsed=False))
1815
+ await self._rebuild_cards()
1816
+
1817
+ def _refuse_while_testing(self, reason: MessageDef) -> bool:
1818
+ """Tell the user why the card list can't change yet; tests hold card indexes until they finish."""
1819
+ if not self._testing:
1820
+ return False
1821
+ localizer = widget_localizer(self)
1822
+ self.notify(
1823
+ render_str(localizer, reason.bind()),
1824
+ title=render_str(localizer, _MCP_TITLE.bind()),
1825
+ timeout=3,
1826
+ markup=False,
1827
+ )
1828
+ return True
1829
+
1830
+ @on(MCPConnectionCard.Removed)
1831
+ def _on_remove(self, event: MCPConnectionCard.Removed) -> None:
1832
+ card = event.card
1833
+ if self._read_only or not self._shows(card) or self._refuse_while_testing(_WAIT_BEFORE_REMOVING):
1834
+ return
1835
+ dialog = ConfirmDialog(
1836
+ title=_DELETE_SERVER_TITLE.bind(),
1837
+ message=_DELETE_SERVER_MESSAGE.bind(name=card.header_title),
1838
+ confirm_label=_DELETE_SERVER_CONFIRM.bind(),
1839
+ confirm_variant="error",
1840
+ locale_controller=widget_locale_controller(self),
1841
+ )
1842
+ self.app.push_screen(dialog, callback=partial(self._remove_confirmed, card))
1843
+
1844
+ async def _remove_confirmed(self, card: MCPConnectionCard, confirmed: bool | None) -> None:
1845
+ if not confirmed or self._read_only or self._refuse_while_testing(_WAIT_BEFORE_REMOVING):
1846
+ return
1847
+ if not self._shows(card):
1848
+ # The cards were rebuilt while the dialog was open (or a second ✕ press
1849
+ # opened a second dialog): the confirmation names a card that is gone.
1850
+ return
1851
+ index = card.index
1852
+ self._rows = self._snapshot_rows(preserve_blank_names=True)
1853
+ if 0 <= index < len(self._rows):
1854
+ self._rows.pop(index)
1855
+ await self._rebuild_cards()
1856
+ self.post_message(CardListChanged())
1857
+
1858
+ @on(MCPConnectionCard.TestRequested)
1859
+ def _on_test_requested(self, event: MCPConnectionCard.TestRequested) -> None:
1860
+ card = event.card
1861
+ if self._read_only or not self._shows(card) or card.index in self._testing:
1862
+ return
1863
+ # Held until the test ends; adding and removing servers wait for it, so the index stays put.
1864
+ self._testing.add(card.index)
1865
+ self._set_test_button_busy(card, True)
1866
+ self._run_test_requested(card)
1867
+
1868
+ @work(thread=False)
1869
+ async def _run_test_requested(self, card: MCPConnectionCard) -> None:
1870
+ index = card.index
1871
+ server_name = ""
1872
+ with contextlib.suppress(Exception):
1873
+ server_name = card.query_one(f"#mcp-name-{index}", Input).value.strip()
1874
+
1875
+ dialog = ConnectionTestDialog(server_name=server_name, subject_label=_MCP_SERVER_SUBJECT.bind())
1876
+ self.app.push_screen(dialog)
1877
+
1878
+ try:
1879
+ errors = card.validate()
1880
+ if errors:
1881
+ dialog.set_result(success=False, message="\n".join(errors))
1882
+ return
1883
+
1884
+ config = card.get_config()
1885
+ self._adapter.set_reserved_tool_names(self._current_reserved_tool_names())
1886
+ report = await self._adapter.test_connection(config)
1887
+ except asyncio.CancelledError as exc:
1888
+ logger.info("MCP connection test cancelled: %s", self._exception_chain_summary(exc))
1889
+ dialog.set_result(
1890
+ success=False,
1891
+ message=self._format_connection_error(exc, render_message=self._render_message),
1892
+ )
1893
+ current = asyncio.current_task()
1894
+ if current is not None and current.cancelling() > 0:
1895
+ raise
1896
+ except Exception as exc:
1897
+ kind = self._classify_connection_error(exc)
1898
+ logger.warning(
1899
+ "MCP connection test failed (%s): %s",
1900
+ kind,
1901
+ self._exception_chain_summary(exc),
1902
+ exc_info=exc,
1903
+ )
1904
+ dialog.set_result(
1905
+ success=False,
1906
+ message=self._format_connection_error(exc, render_message=self._render_message),
1907
+ allow_esc=(kind == "timeout"),
1908
+ )
1909
+ else:
1910
+ # Test doubles may stub test_connection to return None; keep the
1911
+ # plain confirmation for that shape instead of crashing the worker.
1912
+ if report is None:
1913
+ dialog.set_result(success=True, message=self._render_message(_CONNECTION_SUCCESSFUL.bind()))
1914
+ else:
1915
+ dialog.set_result(
1916
+ success=True,
1917
+ message=_format_mcp_report(config, report, render_message=self._render_message),
1918
+ markdown=True,
1919
+ )
1920
+ finally:
1921
+ self._testing.discard(index)
1922
+ self._set_test_button_busy(card, False)
1923
+
1924
+ @staticmethod
1925
+ def _exception_chain(exc: BaseException) -> list[BaseException]:
1926
+ chain: list[BaseException] = []
1927
+ seen: set[int] = set()
1928
+ current: BaseException | None = exc
1929
+ while current is not None and id(current) not in seen:
1930
+ chain.append(current)
1931
+ seen.add(id(current))
1932
+ next_exc = current.__cause__ if current.__cause__ is not None else current.__context__
1933
+ current = next_exc if isinstance(next_exc, BaseException) else None
1934
+ return chain
1935
+
1936
+ @classmethod
1937
+ def _classify_connection_error(cls, exc: BaseException) -> str:
1938
+ chain = cls._exception_chain(exc)
1939
+
1940
+ if any(isinstance(err, MCPToolConfigurationError) for err in chain):
1941
+ return "configuration"
1942
+
1943
+ if any(isinstance(err, asyncio.CancelledError) for err in chain):
1944
+ return "cancelled"
1945
+
1946
+ if any(isinstance(err, (TimeoutError, asyncio.TimeoutError)) for err in chain):
1947
+ return "timeout"
1948
+
1949
+ with contextlib.suppress(ModuleNotFoundError):
1950
+ import httpx
1951
+
1952
+ if any(isinstance(err, httpx.TimeoutException) for err in chain):
1953
+ return "timeout"
1954
+ if any(isinstance(err, httpx.ConnectError) for err in chain):
1955
+ return "connect_failed"
1956
+ if any(isinstance(err, httpx.RequestError) for err in chain):
1957
+ return "request_failed"
1958
+
1959
+ if any(isinstance(err, (socket.gaierror, ConnectionRefusedError, ConnectionError)) for err in chain):
1960
+ return "connect_failed"
1961
+
1962
+ return "unknown"
1963
+
1964
+ @classmethod
1965
+ def _exception_chain_summary(cls, exc: BaseException) -> str:
1966
+ parts: list[str] = []
1967
+ for err in cls._exception_chain(exc):
1968
+ text = str(err).strip()
1969
+ parts.append(f"{err.__class__.__name__}: {text}" if text else err.__class__.__name__)
1970
+ return " <- ".join(parts)
1971
+
1972
+ @classmethod
1973
+ def _format_connection_error(
1974
+ cls,
1975
+ exc: BaseException,
1976
+ *,
1977
+ render_message: Callable[[MessageRef], str] = format_message,
1978
+ ) -> str:
1979
+ """Normalize transport/query errors into readable text for the test dialog."""
1980
+ kind = cls._classify_connection_error(exc)
1981
+
1982
+ raw = str(exc).strip() or exc.__class__.__name__
1983
+ marker = " on TabPane(id='mcp')"
1984
+ if marker in raw:
1985
+ raw = raw.replace(marker, "")
1986
+
1987
+ if kind == "configuration":
1988
+ friendly = render_message(_CONNECTION_CONFIGURATION_CONFLICT.bind())
1989
+ elif kind == "cancelled":
1990
+ friendly = render_message(_CONNECTION_CANCELLED.bind())
1991
+ elif kind == "timeout":
1992
+ friendly = render_message(_CONNECTION_TIMED_OUT.bind())
1993
+ elif kind == "connect_failed":
1994
+ friendly = render_message(_CONNECTION_UNREACHABLE.bind())
1995
+ elif kind == "request_failed":
1996
+ friendly = render_message(_CONNECTION_REQUEST_FAILED.bind())
1997
+ else:
1998
+ friendly = render_message(_CONNECTION_FAILED.bind())
1999
+
2000
+ return f"{friendly}\n\n{raw}"
2001
+
2002
+ def _shows(self, card: MCPConnectionCard) -> bool:
2003
+ """Whether *card* is one of the mounted cards; a rebuild replaces them all, and re-indexes the new ones."""
2004
+ return any(mounted is card for mounted in self.query(MCPConnectionCard))
2005
+
2006
+ def _set_test_button_busy(self, card: MCPConnectionCard, busy: bool) -> None:
2007
+ with contextlib.suppress(Exception):
2008
+ button = card.query_one(f"#mcp-test-btn-{card.index}", Button)
2009
+ if self._read_only:
2010
+ button.disabled = True
2011
+ button.display = False
2012
+ return
2013
+ button.disabled = busy
2014
+ button.label = render_str(widget_localizer(self), (_TESTING if busy else _TEST).bind())
2015
+
2016
+ def _snapshot_rows(self, *, preserve_blank_names: bool = False) -> list[_MCPServerRow]:
2017
+ """Snapshot each card's config and UI-only state, in card order, for a rebuild or ``get_config``."""
2018
+ cards = list(self.query(MCPConnectionCard))
2019
+ if len(cards) != len(self._rows):
2020
+ # Not mounted yet (or mid-rebuild): the rows are still the truth.
2021
+ return list(self._rows)
2022
+ return [
2023
+ _MCPServerRow(
2024
+ card._snapshot_config() if preserve_blank_names else card.get_config(),
2025
+ command_line_text=card._snapshot_command_line_text(),
2026
+ key_value_drafts=card._snapshot_key_value_drafts(),
2027
+ collapsed=card.collapsed,
2028
+ )
2029
+ for card in cards
2030
+ ]
2031
+
2032
+ def get_config(self) -> list[MCPServerConfig]:
2033
+ """Collect config from all connection cards."""
2034
+ return [row.config for row in self._snapshot_rows()]
2035
+
2036
+ def card_folds(self) -> list[bool]:
2037
+ """Whether each server's card is folded, in ``get_config`` order.
2038
+
2039
+ The agent config screen rebuilds this panel from the draft when it loads
2040
+ the agent again, and passes these back so an unsaved server's card stays
2041
+ unfolded.
2042
+ """
2043
+ cards = list(self.query(MCPConnectionCard))
2044
+ if len(cards) != len(self._rows):
2045
+ return [row.collapsed for row in self._rows]
2046
+ return [card.collapsed for card in cards]
2047
+
2048
+ def _validation_errors_by_card(self) -> list[tuple[MCPConnectionCard, list[str]]]:
2049
+ localizer = widget_localizer(self)
2050
+ results: list[tuple[MCPConnectionCard, list[str]]] = []
2051
+ seen_names: set[str] = set()
2052
+ for card in self.query(MCPConnectionCard):
2053
+ errors = list(card.validate())
2054
+ try:
2055
+ name = card.query_one(f"#mcp-name-{card.index}", Input).value.strip()
2056
+ if name:
2057
+ lower_name = name.lower()
2058
+ if lower_name in seen_names:
2059
+ errors.append(
2060
+ render_str(
2061
+ localizer,
2062
+ MCP_DUPLICATE_SERVER.bind(name=DisplayBlock(name)),
2063
+ )
2064
+ )
2065
+ seen_names.add(lower_name)
2066
+ except Exception:
2067
+ pass
2068
+ results.append((card, errors))
2069
+ return results
2070
+
2071
+ def validate(self) -> list[str]:
2072
+ """Validate all connection cards."""
2073
+ return [error for _card, errors in self._validation_errors_by_card() for error in errors]
2074
+
2075
+ def unfold_invalid_cards(self, invalid_indexes: Collection[int] = ()) -> None:
2076
+ """Unfold the cards that fail validation or sit at *invalid_indexes*, and scroll to the first.
2077
+
2078
+ A folded card hides the fields a Save error names, and the error names
2079
+ the first invalid card first.
2080
+ """
2081
+ invalid = [
2082
+ card for card, errors in self._validation_errors_by_card() if errors or card.index in invalid_indexes
2083
+ ]
2084
+ for card in invalid:
2085
+ card.set_collapsed(False, scroll_visible=False)
2086
+ if invalid:
2087
+ self.call_after_refresh(invalid[0].scroll_visible, animate=False)
2088
+
2089
+
2090
+ # --- Test-dialog Markdown report -------------------------------------------
2091
+ #
2092
+ # Every server-controlled string below must pass through ``inline_text`` /
2093
+ # ``code_span`` so a crafted server cannot forge block structure in the
2094
+ # rendered report. Env values and HTTP headers are NEVER rendered — they
2095
+ # routinely carry secrets.
2096
+
2097
+ # Caps keep a pathological server (thousands of tools, book-length
2098
+ # instructions) from turning the report into an unbounded document.
2099
+ _MAX_LISTED_ENTRIES = 40
2100
+ _INSTRUCTIONS_PREVIEW_CHARS = 600
2101
+ _DESCRIPTION_PREVIEW_CHARS = 200
2102
+ _IDENTITY_PREVIEW_CHARS = 120
2103
+
2104
+
2105
+ def _preview(value: object, limit: int) -> str:
2106
+ flat = inline_text(value or "")
2107
+ if len(flat) > limit:
2108
+ return flat[:limit].rstrip() + "…"
2109
+ return flat
2110
+
2111
+
2112
+ def _format_mcp_report(
2113
+ config: MCPServerConfig,
2114
+ report: MCPTestReport,
2115
+ *,
2116
+ render_message: Callable[[MessageRef], str] = format_message,
2117
+ ) -> str:
2118
+ """Render a successful connection test as the dialog's Markdown report."""
2119
+ sections = [
2120
+ _server_heading(report, render_message),
2121
+ _connection_caption(config, render_message),
2122
+ f"### {render_message(_CAPABILITIES_HEADING.bind())}",
2123
+ _mcp_capabilities_section(report.capabilities, render_message),
2124
+ f"### {render_message(_INSTRUCTIONS_HEADING.bind())}",
2125
+ _instructions_section(report.instructions, render_message),
2126
+ f"### {render_message(_TOOLS_HEADING.bind())}",
2127
+ _tools_section(report, render_message),
2128
+ f"### {render_message(_PROMPTS_HEADING.bind())}",
2129
+ _prompts_section(config, report, render_message),
2130
+ ]
2131
+ if report.initial_tool_names is not None:
2132
+ sections.extend(
2133
+ (
2134
+ f"### {render_message(_PROGRESSIVE_DISCLOSURE_HEADING.bind())}",
2135
+ _progressive_section(report.initial_tool_names, render_message),
2136
+ )
2137
+ )
2138
+ return "\n\n".join(sections)
2139
+
2140
+
2141
+ def _server_heading(report: MCPTestReport, render_message: Callable[[MessageRef], str] = format_message) -> str:
2142
+ name = _preview(report.server_name, _IDENTITY_PREVIEW_CHARS)
2143
+ title = _preview(report.server_title, _IDENTITY_PREVIEW_CHARS) or name
2144
+ parts = []
2145
+ if title:
2146
+ parts.append(f"**{title}**")
2147
+ if name and name != title:
2148
+ parts.append(code_span(name))
2149
+ if version := _preview(report.server_version, _IDENTITY_PREVIEW_CHARS):
2150
+ parts.append(f"v{version}")
2151
+ if protocol := _preview(report.protocol_version, _IDENTITY_PREVIEW_CHARS):
2152
+ parts.append(f"MCP {code_span(protocol)}")
2153
+ if not parts:
2154
+ return render_message(_IDENTITY_NOT_REPORTED.bind())
2155
+ return " · ".join(parts)
2156
+
2157
+
2158
+ def _connection_caption(config: MCPServerConfig, render_message: Callable[[MessageRef], str] = format_message) -> str:
2159
+ # Command line / URL only: env values and headers may carry secrets.
2160
+ if config.transport == "stdio":
2161
+ command_line = " ".join([config.command, *config.args]).strip()
2162
+ target = code_span(command_line) if command_line else render_message(_NO_COMMAND_CONFIGURED.bind())
2163
+ else:
2164
+ target = code_span(config.url) if config.url.strip() else render_message(_NO_URL_CONFIGURED.bind())
2165
+ return render_message(_CONNECTED_VIA.bind(transport=code_span(config.transport), target=target))
2166
+
2167
+
2168
+ def _mcp_capabilities_section(
2169
+ advertised: tuple[str, ...],
2170
+ render_message: Callable[[MessageRef], str] = format_message,
2171
+ ) -> str:
2172
+ advertised_set = set(advertised)
2173
+ if not advertised_set:
2174
+ return render_message(NOT_ADVERTISED.bind())
2175
+ # Standard groups always render (that is the point of the ✓/— column);
2176
+ # server-invented extras only fill the remaining row budget, and each
2177
+ # name is clamped — extras are unbounded server-controlled strings.
2178
+ extras = sorted((name for name in advertised_set if name not in STANDARD_CAPABILITY_GROUPS), key=str.casefold)
2179
+ shown = list(STANDARD_CAPABILITY_GROUPS) + extras[: _MAX_LISTED_ENTRIES - len(STANDARD_CAPABILITY_GROUPS)]
2180
+ table = md_table(
2181
+ (render_message(_CAPABILITY_HEADER.bind()), render_message(_ADVERTISED_HEADER.bind())),
2182
+ [
2183
+ (
2184
+ code_span(_preview(name, _IDENTITY_PREVIEW_CHARS)),
2185
+ SUPPORTED_MARK if name in advertised_set else UNSUPPORTED_MARK,
2186
+ )
2187
+ for name in shown
2188
+ ],
2189
+ )
2190
+ if hidden := len(extras) - (len(shown) - len(STANDARD_CAPABILITY_GROUPS)):
2191
+ table += f"\n\n*{render_message(_MORE_ENTRIES.bind(extra_count=hidden))}*"
2192
+ return table
2193
+
2194
+
2195
+ def _instructions_section(
2196
+ instructions: str | None,
2197
+ render_message: Callable[[MessageRef], str] = format_message,
2198
+ ) -> str:
2199
+ flat = _preview(instructions, _INSTRUCTIONS_PREVIEW_CHARS)
2200
+ if not flat:
2201
+ return render_message(NOT_ADVERTISED.bind())
2202
+ # Blockquote + italic wrap: blockquote content is block-parsed, so bare
2203
+ # remote text at line start could still forge a heading/bullet — the
2204
+ # leading ``*`` keeps our own marker first on the line.
2205
+ return f"> *{flat}*"
2206
+
2207
+
2208
+ def _catalog_bullets(
2209
+ entries: tuple[tuple[str, str], ...],
2210
+ render_message: Callable[[MessageRef], str] = format_message,
2211
+ ) -> str:
2212
+ shown = entries[:_MAX_LISTED_ENTRIES]
2213
+ bullets = definition_bullets(
2214
+ [(name, "", _preview(description, _DESCRIPTION_PREVIEW_CHARS)) for name, description in shown]
2215
+ )
2216
+ if len(entries) > len(shown):
2217
+ bullets += f"\n- *{render_message(_MORE_ENTRIES.bind(extra_count=len(entries) - len(shown)))}*"
2218
+ return bullets
2219
+
2220
+
2221
+ def _tools_section(report: MCPTestReport, render_message: Callable[[MessageRef], str] = format_message) -> str:
2222
+ if report.tools:
2223
+ return render_message(
2224
+ _TOOLS_EXPOSED.bind(
2225
+ count=len(report.tools),
2226
+ catalog=DisplayBlock(_catalog_bullets(report.tools, render_message)),
2227
+ )
2228
+ )
2229
+ if "tools" in report.capabilities:
2230
+ return render_message(_ADVERTISED_NO_TOOLS.bind())
2231
+ return render_message(NOT_ADVERTISED.bind())
2232
+
2233
+
2234
+ def _prompts_section(
2235
+ config: MCPServerConfig,
2236
+ report: MCPTestReport,
2237
+ render_message: Callable[[MessageRef], str] = format_message,
2238
+ ) -> str:
2239
+ if report.prompts:
2240
+ return render_message(
2241
+ _PROMPTS_EXPOSED.bind(
2242
+ count=len(report.prompts),
2243
+ catalog=DisplayBlock(_catalog_bullets(report.prompts, render_message)),
2244
+ )
2245
+ )
2246
+ if not config.load_prompts:
2247
+ return render_message(_PROMPT_LOADING_DISABLED.bind())
2248
+ if "prompts" in report.capabilities:
2249
+ return render_message(_ADVERTISED_NO_PROMPTS.bind())
2250
+ return render_message(NOT_ADVERTISED.bind())
2251
+
2252
+
2253
+ def _progressive_section(
2254
+ initial_tool_names: tuple[str, ...],
2255
+ render_message: Callable[[MessageRef], str] = format_message,
2256
+ ) -> str:
2257
+ lines = [render_message(_PROGRESSIVE_ENABLED.bind()), ""]
2258
+ shown = initial_tool_names[:_MAX_LISTED_ENTRIES]
2259
+ lines.extend(f"- {code_span(name)}" for name in shown)
2260
+ if len(initial_tool_names) > len(shown):
2261
+ lines.append(f"- *{render_message(_MORE_ENTRIES.bind(extra_count=len(initial_tool_names) - len(shown)))}*")
2262
+ return "\n".join(lines)