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
chrys/app/tui/app.py ADDED
@@ -0,0 +1,1287 @@
1
+ # Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
2
+
3
+ """Chrys TUI — Textual-based terminal UI entry point.
4
+
5
+ This is the Textual app entry point used by ``chrys``.
6
+ It launches the Textual app which communicates with the backend Engine
7
+ exclusively through the EventBus.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import asyncio
14
+ import contextlib
15
+ import inspect
16
+ import logging
17
+ import os
18
+ from pathlib import Path
19
+ from typing import TYPE_CHECKING, ClassVar
20
+
21
+ from textual import __version__ as TEXTUAL_VERSION
22
+ from textual import events as textual_events
23
+ from textual.app import App, ScreenStackError
24
+ from textual.await_remove import AwaitRemove
25
+ from textual.css.stylesheet import Stylesheet, StylesheetError
26
+ from textual.css.tokenizer import TokenError
27
+ from textual.dom import DOMNode, NoScreen
28
+ from textual.errors import NoWidget
29
+ from textual.screen import Screen
30
+ from textual.theme import Theme
31
+ from textual.widget import Widget
32
+
33
+ from chrys import __version__
34
+ from chrys.app.cli.launch_cwd import launch_cwd_missing_message
35
+ from chrys.app.features.buddy.lifecycle import on_successful_turn as on_buddy_successful_turn
36
+ from chrys.app.features.session_title import SessionTitleUpdater
37
+ from chrys.app.parsing import SanitizingArgumentParser
38
+ from chrys.app.tui.behaviors.insert_clipboard import INSERT_CLIPBOARD_BINDINGS, copy_with_insert, paste_with_insert
39
+ from chrys.app.tui.binding_display import QUIT_BINDING, localized_binding
40
+ from chrys.app.tui.i18n import LocaleController, render_str
41
+ from chrys.app.tui.notifications import NotificationService, NotificationSettings
42
+ from chrys.app.tui.screens.main import MainScreen
43
+ from chrys.app.tui.screens.settings import SettingsDialog
44
+ from chrys.app.tui.screens.settings.dialog import THEME_PREVIEW_HINT
45
+ from chrys.app.tui.support.gc_freeze import (
46
+ GC_FREEZE_ENABLED,
47
+ GC_FREEZE_WATCHDOG_PERIOD_SECONDS,
48
+ GcAbsorbReason,
49
+ GcAbsorbRequested,
50
+ GcFreezeBlockReason,
51
+ GcFreezeCoordinator,
52
+ GcReclaimReason,
53
+ GcReclaimRequested,
54
+ )
55
+ from chrys.app.tui.terminal.title import (
56
+ running_under_textual_web_driver,
57
+ set_app_terminal_title_for_current_cwd,
58
+ set_terminal_title_for_current_cwd,
59
+ )
60
+ from chrys.app.tui.theme import (
61
+ CHRYS_ANSI_THEME,
62
+ CHRYS_LEGACY_THEME,
63
+ CHRYS_THEME,
64
+ TuiVariableDefaultsMixin,
65
+ with_tui_css_variables,
66
+ )
67
+ from chrys.app.tui.theme_loader import load_user_themes, user_theme_warning
68
+ from chrys.app.tui.theme_stylesheet import ThemeStylesheet
69
+ from chrys.app.tui.themes.preview import ThemePreview
70
+ from chrys.app.tui.toasts import TEXTUAL_TOAST_RACK_FORK_VERSION, refresh_newest_first
71
+ from chrys.app.tui.util.debug_log import configure_debug_logging, stop_debug_logging
72
+ from chrys.app.tui.widgets.chrome.status_bar import STATUS_AGENT_STARTUP_FAILED
73
+ from chrys.foundation import platform as chrys_platform
74
+ from chrys.foundation.branding import APP_DISPLAY_NAME, format_app_version_title
75
+ from chrys.foundation.config.runtime_pointer import set_model_pointer
76
+ from chrys.foundation.config.settings import DEFAULT_AGENT_PROFILE, DEFAULT_THEME, Settings, persist_theme
77
+ from chrys.foundation.config.settings_store import LoadedSettings, SettingsHandle
78
+ from chrys.foundation.config.spec import SettingOrigin, Source
79
+ from chrys.foundation.events.bus import EventBus
80
+ from chrys.foundation.events.types import Warning
81
+ from chrys.foundation.i18n import Localizer, MessageRef, msg
82
+ from chrys.foundation.models.session_surface import SessionSurface
83
+ from chrys.foundation.util.session_ids import session_short_id
84
+ from chrys.orchestration.engine.assembly import assemble_agent_engine
85
+ from chrys.orchestration.engine.engine import AgentEngine
86
+ from chrys.orchestration.startup import catalog_load_warning
87
+ from chrys.service.profiles.agents.registry import AgentProfileRegistry, resolve_profile_selector
88
+ from chrys.service.profiles.models.registry import ModelProfileRegistry
89
+ from chrys.service.profiles.models.resolver import (
90
+ format_available_profile_labels,
91
+ loaded_with_active_model_profile,
92
+ )
93
+ from chrys.service.profiles.models.resolver import (
94
+ resolve_profile_selector as resolve_model_profile_selector,
95
+ )
96
+ from chrys.service.state.store import JsonFileStateStore
97
+
98
+ logger = logging.getLogger(__name__)
99
+
100
+ _SESSION_NOTIFICATION_TITLE = msg("tui.startup.title.session", fallback="Session")
101
+ _AGENT_STARTUP_FAILED_TITLE = msg(
102
+ "tui.startup.title.agent_failed",
103
+ fallback="Agent startup failed",
104
+ )
105
+ _SESSION_READ_FAILED = msg(
106
+ "tui.startup.session.read_failed",
107
+ fallback="Could not read session {session_id}: {error}",
108
+ )
109
+ _SESSION_NOT_FOUND = msg(
110
+ "tui.startup.session.not_found",
111
+ fallback="Session {session_id} was not found.",
112
+ )
113
+ _SESSION_RESTORE_FALLBACK = msg(
114
+ "tui.startup.session.restore_fallback",
115
+ fallback="Could not restore session {session_id}; started a new session instead.",
116
+ )
117
+ _SESSION_RESTORE_FAILED = msg(
118
+ "tui.startup.session.restore_failed",
119
+ fallback="Could not restore session {session_id}: {error}",
120
+ )
121
+ _QUIT_HELP = msg("tui.app.quit_help", fallback="Press {key} to quit the app")
122
+ _QUIT_HELP_TITLE = msg("tui.app.quit_help_title", fallback="Do you want to quit?")
123
+
124
+ _GC_FREEZE_DISABLED_WARNING_CODE = "gc_freeze_disabled"
125
+ _TEXTUAL_LOAD_SCREEN_CSS_FORK_VERSION = "8.2.7"
126
+ _GC_ACTIVITY_EVENTS = (
127
+ textual_events.Key,
128
+ textual_events.Paste,
129
+ textual_events.MouseDown,
130
+ textual_events.MouseUp,
131
+ textual_events.MouseScrollDown,
132
+ textual_events.MouseScrollUp,
133
+ textual_events.MouseScrollLeft,
134
+ textual_events.MouseScrollRight,
135
+ textual_events.Resize,
136
+ textual_events.AppFocus,
137
+ )
138
+
139
+ _TERMINAL_MODE_RESET = (
140
+ "\x1b[?1000l" # Disable VT200 mouse
141
+ "\x1b[?1002l" # Disable button-event mouse
142
+ "\x1b[?1003l" # Disable any-event mouse
143
+ "\x1b[?1005l" # Disable UTF-8 extended mouse
144
+ "\x1b[?1006l" # Disable SGR mouse
145
+ "\x1b[?1015l" # Disable urxvt mouse
146
+ "\x1b[?1004l" # Disable focus-in/focus-out reports
147
+ "\x1b[?1007l" # Disable alternate-scroll mouse mode
148
+ "\x1b[?25h" # Show cursor
149
+ "\x1b[?2004l" # Disable bracketed paste
150
+ "\x1b[<u" # Disable Kitty keyboard protocol
151
+ "\x1b[=0;1u" # Hard-reset Kitty keyboard flags
152
+ )
153
+ if TYPE_CHECKING:
154
+ from textual.timer import Timer
155
+
156
+ from chrys.app.tui.widgets.markdown.diagram.messages import DiagramOpenRequested
157
+ from chrys.service.profiles.agents.schema import AgentProfile
158
+
159
+
160
+ def _resolve_startup_profile(registry: AgentProfileRegistry, preferred_name: str) -> AgentProfile | None:
161
+ """Resolve the TUI launch profile, falling back through Code to the first visible profile."""
162
+ requested = preferred_name.strip() or DEFAULT_AGENT_PROFILE
163
+ # Match the TUI picker: hidden and sub-agent-only profiles are not valid main launch profiles.
164
+ profiles = registry.list_profiles()
165
+ by_name = {profile.name: profile for profile in profiles}
166
+
167
+ profile = resolve_profile_selector(profiles, requested)
168
+ if profile is not None:
169
+ return profile
170
+
171
+ if requested != DEFAULT_AGENT_PROFILE:
172
+ fallback = by_name.get(DEFAULT_AGENT_PROFILE)
173
+ if fallback is not None:
174
+ logger.warning(
175
+ "Default agent profile %r is not usable as a launch profile, falling back to %r.",
176
+ requested,
177
+ DEFAULT_AGENT_PROFILE,
178
+ )
179
+ return fallback
180
+
181
+ if profiles:
182
+ logger.warning(
183
+ "Default agent profile %r is not usable as a launch profile, falling back to %r.",
184
+ requested,
185
+ profiles[0].name,
186
+ )
187
+ return profiles[0]
188
+ return None
189
+
190
+
191
+ def _apply_startup_active_model_selection(
192
+ parser: argparse.ArgumentParser,
193
+ loaded: LoadedSettings,
194
+ model_registry: ModelProfileRegistry,
195
+ model_profile: str,
196
+ ) -> LoadedSettings:
197
+ model = model_profile.strip()
198
+ if not model:
199
+ return loaded
200
+ profile = resolve_model_profile_selector(model_registry, model)
201
+ if profile is None:
202
+ error_message = f"Model profile not found: {model}"
203
+ available = format_available_profile_labels(model_registry)
204
+ if available:
205
+ error_message = f"{error_message}. Available model profiles: {available}"
206
+ parser.error(error_message)
207
+ # Keep startup --model durable across SettingsReload without writing the
208
+ # user's .env. ModelConfigScreen may still replace this process-local value
209
+ # later when the user intentionally applies a different active profile.
210
+ set_model_pointer(profile.id, origin=SettingOrigin(layer=Source.CLI))
211
+ return loaded_with_active_model_profile(loaded, profile, Source.CLI)
212
+
213
+
214
+ def _restore_terminal_after_run(app: ChrysApp) -> None:
215
+ """Restore terminal state after a native TUI run."""
216
+ if running_under_textual_web_driver():
217
+ return
218
+
219
+ import sys
220
+
221
+ reset = _TERMINAL_MODE_RESET
222
+ # Include alt-screen exit only when Textual exited normally. On error exit,
223
+ # Textual already sent \x1b[?1049l in stop_application_mode() before
224
+ # printing the traceback to stderr.
225
+ if app._return_code == 0:
226
+ reset += "\x1b[?1049l" # Exit alt screen
227
+ sys.stderr.write(reset)
228
+ sys.stderr.flush()
229
+
230
+
231
+ def _reset_terminal_title_after_run() -> None:
232
+ """Leave the host shell with a cwd title instead of the last prompt preview."""
233
+ set_terminal_title_for_current_cwd()
234
+
235
+
236
+ class ChrysApp(TuiVariableDefaultsMixin, App):
237
+ """Textual application for Chrys."""
238
+
239
+ TITLE = format_app_version_title(__version__)
240
+ CSS_PATH: ClassVar[list[str]] = ["chrys.tcss", "screens/themes/editor.tcss"]
241
+ COMMAND_PALETTE_BINDING = "ctrl+q"
242
+ BINDINGS: ClassVar[list] = [
243
+ # Application fallbacks cover selectable screens and stock Textual
244
+ # controls. Chrys editors bind these keys themselves so specialized
245
+ # paste behavior (image paste and Vim mode gating) runs first, and
246
+ # modal roots repeat the bindings because Textual stops the
247
+ # non-priority binding chain at a modal screen.
248
+ *INSERT_CLIPBOARD_BINDINGS,
249
+ # Shadow stock App's ctrl+q so screens without their own quit binding
250
+ # (e.g. DiffScreen) still resolve a localized command-palette slot.
251
+ localized_binding("ctrl+q", "quit", QUIT_BINDING, show=False, priority=True),
252
+ ]
253
+
254
+ def __init__(
255
+ self,
256
+ event_bus: EventBus,
257
+ engine: AgentEngine,
258
+ settings: Settings | None = None,
259
+ profile_name: str = "",
260
+ state_store: JsonFileStateStore | None = None,
261
+ agent_registry: AgentProfileRegistry | None = None,
262
+ model_registry: ModelProfileRegistry | None = None,
263
+ startup_warnings: list[Warning] | None = None,
264
+ settings_warnings: list[Warning] | None = None,
265
+ startup_session_id: str = "",
266
+ apply_saved_model_on_restore: bool = True,
267
+ session_title_updater: SessionTitleUpdater | None = None,
268
+ *,
269
+ localizer: Localizer | None = None,
270
+ settings_handle: SettingsHandle | None = None,
271
+ gc_freeze_enabled: bool | None = None,
272
+ gc_freeze_measure_counts: bool = False,
273
+ ) -> None:
274
+ self._bus = event_bus
275
+ self._engine = engine
276
+ if settings_handle is not None and settings is not None and settings is not settings_handle.settings:
277
+ error_message = "Pass either settings or settings_handle, not two different ones."
278
+ raise ValueError(error_message)
279
+ # ``main()`` passes the engine's handle, so the two read one value
280
+ # rather than two that started equal. An app built without one owns
281
+ # its settings alone.
282
+ self._settings_handle = settings_handle or SettingsHandle(
283
+ LoadedSettings(settings=settings or Settings.from_env(), provenance={})
284
+ )
285
+ self._locale_controller = LocaleController(
286
+ localizer=localizer,
287
+ settings_handle=self._settings_handle,
288
+ )
289
+ self._profile_name = (profile_name or self._settings.default_agent).strip() or DEFAULT_AGENT_PROFILE
290
+ self._state_store = state_store or JsonFileStateStore()
291
+ self._agent_registry = agent_registry or AgentProfileRegistry()
292
+ self._model_registry = model_registry or ModelProfileRegistry()
293
+ # Warnings collected during pre-mount setup (e.g. invalid env
294
+ # values scrubbed before the bus had subscribers); flushed after
295
+ # MainScreen subscribes to ``Warning`` so the user sees them as
296
+ # toast notifications.
297
+ self._startup_session_id = startup_session_id.strip()
298
+ self._startup_warnings: list[Warning] = list(startup_warnings or [])
299
+ # Settings-derived warnings describe the bootstrap load rooted at the
300
+ # process cwd. A startup restore replaces those settings with ones
301
+ # loaded from the restored session's own root — and the restore
302
+ # republishes that load's warnings itself — so flushing these up
303
+ # front would report a repository the session doesn't live in (or
304
+ # duplicate every warning when the roots coincide). Deferred until
305
+ # the restore's outcome is known; dropped on success, flushed when
306
+ # the app falls back to the bootstrap settings.
307
+ self._deferred_settings_warnings: list[Warning] = []
308
+ if self._startup_session_id:
309
+ self._deferred_settings_warnings.extend(settings_warnings or [])
310
+ else:
311
+ self._startup_warnings.extend(settings_warnings or [])
312
+ if self.localizer.first_load_warning is not None:
313
+ self._startup_warnings.append(catalog_load_warning(self.localizer.first_load_warning))
314
+ self._apply_saved_model_on_restore = apply_saved_model_on_restore
315
+ self._session_title_updater = session_title_updater
316
+ # Projected from the loaded settings: bootstrap folds any legacy
317
+ # ``notifications.yaml`` into the settings document before those
318
+ # settings are loaded, so this view never reads the retired file.
319
+ self._notification_service = NotificationService(
320
+ self,
321
+ NotificationSettings.from_settings(self._settings),
322
+ locale_controller=self._locale_controller,
323
+ )
324
+ # Crash-log state: first crash within a run truncates error.log,
325
+ # subsequent crashes append (so cascading teardown errors survive).
326
+ self._crash_log_initialized = False
327
+ self._startup_task: asyncio.Task[None] | None = None
328
+ self._main_screen: MainScreen | None = None
329
+ self._gc_freeze_watchdog: Timer | None = None
330
+ self._gc_pointer_buttons_down: set[int] = set()
331
+ self._gc_effective_theme = ""
332
+ # Set before ``super().__init__()``: Textual initialization can invoke
333
+ # the theme watcher, which must not treat startup as a user's choice.
334
+ self._theme_persist_armed = False
335
+ self.theme_preview: ThemePreview | None = None
336
+ self._theme_preview_source = ""
337
+ self._theme_selection_depth = 0
338
+ freeze_enabled = GC_FREEZE_ENABLED if gc_freeze_enabled is None else gc_freeze_enabled
339
+ # Textual initialization may invoke reactive watchers, so the
340
+ # coordinator exists first. It stays inert until ``start()`` after the
341
+ # main screen has mounted.
342
+ self._gc_freeze = GcFreezeCoordinator(
343
+ self,
344
+ enabled=freeze_enabled,
345
+ measure_freeze_counts=gc_freeze_measure_counts,
346
+ )
347
+ super().__init__()
348
+ self.register_theme(CHRYS_THEME)
349
+ self.register_theme(CHRYS_LEGACY_THEME)
350
+ self.register_theme(CHRYS_ANSI_THEME)
351
+ # User themes come from <config_dir>/themes/*.yaml; per-file failures
352
+ # surface as startup warnings and never block the app.
353
+ user_themes, user_theme_warnings = load_user_themes()
354
+ for user_theme in user_themes:
355
+ self.register_theme(user_theme)
356
+ self._user_theme_names = {theme.name for theme in user_themes}
357
+ stylesheet = ThemeStylesheet(variables=self.get_css_variables(), recover=self._recover_user_theme)
358
+ stylesheet.source = self.stylesheet.source.copy()
359
+ self.stylesheet = stylesheet
360
+ self._startup_warnings.extend(user_theme_warnings)
361
+ # Resolve the persisted theme; fall back to the default if empty,
362
+ # unknown, or unregistered.
363
+ theme_name = self._settings.theme
364
+ if not theme_name or theme_name not in self.available_themes:
365
+ theme_name = DEFAULT_THEME
366
+ self.theme = theme_name
367
+ # Only now may a theme change be written back. The fallback above is a
368
+ # decision about what to *draw* when the saved name resolves to nothing
369
+ # — a theme renamed between releases, a typo, a value some other writer
370
+ # damaged. Persisting it would delete the name the user chose, which is
371
+ # the one thing the loader's ``FALL_THROUGH`` policy exists to preserve:
372
+ # an unusable value costs them this session's colours, not the setting.
373
+ self._theme_persist_armed = True
374
+
375
+ @property
376
+ def _settings(self) -> Settings:
377
+ """Read-only: settings change through the handle, not the holder."""
378
+ return self._settings_handle.settings
379
+
380
+ @property
381
+ def _loaded_settings(self) -> LoadedSettings:
382
+ return self._settings_handle.loaded
383
+
384
+ @property
385
+ def settings_handle(self) -> SettingsHandle:
386
+ """The shared settings cell, for screens that project or overlay it."""
387
+ return self._settings_handle
388
+
389
+ @property
390
+ def engine(self) -> AgentEngine:
391
+ """Public accessor for the backend engine.
392
+
393
+ Screens and widgets should read the engine through this property
394
+ rather than poking at ``self.app._engine`` — keeps the
395
+ private-attr boundary explicit and lets the TUI layer mock a
396
+ stand-in engine in tests by subclassing ``ChrysApp``.
397
+ """
398
+ return self._engine
399
+
400
+ @property
401
+ def localizer(self) -> Localizer:
402
+ """Application-owned display localizer."""
403
+ return self._locale_controller.localizer
404
+
405
+ @property
406
+ def locale_controller(self) -> LocaleController:
407
+ """Application-owned locale switch and live-surface controller."""
408
+ return self._locale_controller
409
+
410
+ @property
411
+ def notification_service(self) -> NotificationService:
412
+ """Runtime desktop notification service."""
413
+ return self._notification_service
414
+
415
+ def override_notification_settings(self, settings: NotificationSettings) -> None:
416
+ """Install a live notification choice in the shared handle.
417
+
418
+ The same pairing every other live surface uses — theme, locale and the
419
+ approval mode each persist *and* overlay — and for the same reason. The
420
+ write to disk is debounced, so between the edit and the write the handle
421
+ is the only place the choice exists; a reload landing in that window
422
+ installs the pre-edit document, and :meth:`refresh_notification_settings`
423
+ then projects it straight back over the delivery service. The overlay
424
+ survives that reload by construction, which is what makes the edit
425
+ survive it too.
426
+ """
427
+ self._settings_handle.override(**settings.to_settings_fields())
428
+
429
+ def refresh_notification_settings(self) -> None:
430
+ """Re-project the notification settings into the delivery service.
431
+
432
+ The service holds a projection taken from ``Settings``, not the
433
+ settings themselves — so when a reload installs new values into the
434
+ shared handle, the LIVE ``notifications.*`` fields only take effect
435
+ once someone projects them again. This is that someone.
436
+ """
437
+ self._notification_service.update_settings(NotificationSettings.from_settings(self._settings))
438
+
439
+ def freeze_block_reason(self) -> GcFreezeBlockReason | None:
440
+ """Return the App-owned top-screen gate for GC freeze dispatch."""
441
+ if self._gc_pointer_buttons_down:
442
+ return GcFreezeBlockReason.POINTER_BUTTON_HELD
443
+ # Tests use deliberately tiny engine stand-ins. Production always
444
+ # owns AgentEngine, whose run task remains active through session save
445
+ # and after-turn hooks even after the UI has rendered its terminal.
446
+ if isinstance(self._engine, AgentEngine) and self._engine.is_turn_lifecycle_active:
447
+ return GcFreezeBlockReason.BACKEND_TURN_LIFECYCLE
448
+ if self._main_screen is None or self.screen is not self._main_screen:
449
+ return GcFreezeBlockReason.TOP_SCREEN
450
+ return self._main_screen.gc_freeze_block_reason()
451
+
452
+ def prepare_for_gc_freeze(self) -> None:
453
+ """Prepare mounted freeze participants.
454
+
455
+ The MainScreen aggregate owns the concrete participant list.
456
+ """
457
+ if self._main_screen is not None:
458
+ self._main_screen.prepare_for_gc_freeze()
459
+
460
+ def after_gc_freeze(self) -> None:
461
+ """Renew mounted mutable cyclic participants after a freeze.
462
+
463
+ The MainScreen aggregate owns the concrete participant list.
464
+ """
465
+ if self._main_screen is not None:
466
+ self._main_screen.after_gc_freeze()
467
+
468
+ def abort_gc_freeze(self) -> None:
469
+ """Best-effort normalize mounted caches after an incomplete hook pass."""
470
+ if self._main_screen is not None:
471
+ self._main_screen.abort_gc_freeze()
472
+
473
+ def on_gc_absorb_requested(self, event: GcAbsorbRequested) -> None:
474
+ """Forward a UI-local absorb request with its source timestamp."""
475
+ self._gc_freeze.request_absorb(
476
+ reason=event.reason,
477
+ terminal_boundary=event.terminal_boundary,
478
+ requested_at=event.time,
479
+ )
480
+
481
+ def on_gc_reclaim_requested(self, event: GcReclaimRequested) -> None:
482
+ """Forward a UI-local reclaim request with its source timestamp."""
483
+ self._gc_freeze.request_reclaim(
484
+ reason=event.reason,
485
+ prompt=event.prompt,
486
+ requested_at=event.time,
487
+ removed=event.removed,
488
+ )
489
+
490
+ def on_diagram_open_requested(self, event: DiagramOpenRequested) -> None:
491
+ """Present a Markdown diagram without introducing a widget-to-screen import."""
492
+ from chrys.app.tui.screens.dialogs.diagram import DiagramDialog
493
+
494
+ event.stop()
495
+ self.push_screen(DiagramDialog(event.diagram))
496
+
497
+ def _watch_app_focus(self, focus: bool) -> None:
498
+ """Preserve native widget focus without restyling or relayout.
499
+
500
+ Textual clears and restores the focused widget across App blur/focus.
501
+ Each transition refreshes bindings, which makes Footer request several
502
+ full-screen layouts. Chrys's native UI has no App-focus-dependent CSS,
503
+ and it receives no input while the terminal is unfocused, so retaining
504
+ the internal focused widget is both sufficient and transcript-size
505
+ independent. The web host keeps Textual's exact behavior.
506
+ """
507
+ if running_under_textual_web_driver():
508
+ super()._watch_app_focus(focus)
509
+ return
510
+
511
+ self._last_focused_on_app_blur = None
512
+
513
+ def _on_app_focus(self, event: textual_events.AppFocus) -> None: # ty: ignore[invalid-method-override] # Textual dispatch accepts sync handlers.
514
+ """Record focus and recover bindings for the visible screen only."""
515
+ self._notification_service.set_focused(True)
516
+ if running_under_textual_web_driver():
517
+ return
518
+ event.prevent_default()
519
+ self.app_focus = True
520
+ screen_stack = self.screen_stack
521
+ if not screen_stack:
522
+ return
523
+ screen = screen_stack[-1]
524
+ if self._main_screen is not None and screen is self._main_screen:
525
+ self._main_screen.sync_footer_bindings()
526
+ else:
527
+ # Stock Footer deliberately ignores binding changes while the App
528
+ # is unfocused. Deferred screens may become interactive during
529
+ # that interval, so mirror Textual's refocus recovery for the
530
+ # active overlay without restyling the suspended transcript.
531
+ screen.refresh_bindings()
532
+
533
+ def _on_app_blur(self, event: textual_events.AppBlur) -> None: # ty: ignore[invalid-method-override] # Textual dispatch accepts sync handlers.
534
+ """Record blur while retaining the native focused widget."""
535
+ self._notification_service.set_focused(False)
536
+ if running_under_textual_web_driver():
537
+ return
538
+ event.prevent_default()
539
+ self.app_focus = False
540
+
541
+ def _load_screen_css(self, screen: Screen) -> None:
542
+ """Load native secondary-screen CSS without restyling the underlay.
543
+
544
+ Textual updates the entire App when it discovers a Screen stylesheet.
545
+ Chrys secondary-screen styles are screen-local, so existing MainScreen
546
+ nodes cannot start matching them. Parse globally so subsequently mounted
547
+ children see the new rules, then update only the already-registered
548
+ screen root. Keep Textual's behavior for web and the startup MainScreen.
549
+
550
+ This is a version-gated fork of Textual's private
551
+ ``App._load_screen_css`` implementation. The intentional semantic deltas
552
+ are updating ``screen`` rather than the entire App, and parsing the live
553
+ stylesheet in place rather than reparsing into a fresh one: the fresh
554
+ stylesheet's empty rule cache re-tokenizes every source, while the live
555
+ cache only holds rules for the current variables (``set_variables``
556
+ clears it), so only the new sources are tokenized.
557
+ """
558
+ if (
559
+ TEXTUAL_VERSION != _TEXTUAL_LOAD_SCREEN_CSS_FORK_VERSION
560
+ or running_under_textual_web_driver()
561
+ or isinstance(screen, MainScreen)
562
+ ):
563
+ super()._load_screen_css(screen)
564
+ return
565
+
566
+ if self.css_monitor is not None:
567
+ self.css_monitor.add_paths(screen.css_path)
568
+
569
+ update = False
570
+ for path in screen.css_path:
571
+ if not self.stylesheet.has_source(str(path), ""):
572
+ self.stylesheet.read(path)
573
+ update = True
574
+ if screen.CSS:
575
+ try:
576
+ screen_path = inspect.getfile(screen.__class__)
577
+ except TypeError, OSError:
578
+ screen_path = ""
579
+ screen_class_var = f"{screen.__class__.__name__}.CSS"
580
+ if not self.stylesheet.has_source(screen_path, screen_class_var):
581
+ self.stylesheet.add_source(
582
+ screen.CSS,
583
+ read_from=(screen_path, screen_class_var),
584
+ is_default_css=False,
585
+ scope=screen._css_type_name if screen.SCOPED_CSS else "",
586
+ )
587
+ update = True
588
+ if update:
589
+ self.stylesheet.parse()
590
+ self.stylesheet.update(screen)
591
+
592
+ def _refresh_notifications(self) -> None:
593
+ """Show toasts under the header with the newest on top.
594
+
595
+ Textual appends each new toast below the older ones and scrolls the
596
+ rack to its end, the right stacking for its bottom-docked rack. Chrys
597
+ docks the rack under the header, so new toasts mount ahead of the
598
+ older ones and the rack keeps its start in view: the stack grows
599
+ downward from the newest toast.
600
+
601
+ This is a version-gated fork of Textual's private
602
+ ``App._refresh_notifications`` and ``ToastRack.show``. The only
603
+ intentional semantic delta is where new toasts mount and which end of
604
+ the rack stays in view.
605
+ """
606
+ if TEXTUAL_VERSION != TEXTUAL_TOAST_RACK_FORK_VERSION:
607
+ super()._refresh_notifications()
608
+ return
609
+ refresh_newest_first(self)
610
+
611
+ def _prune(self, *nodes: Widget, parent: DOMNode | None = None) -> AwaitRemove:
612
+ """Prune whole-screen overlays without relayout of the screen beneath.
613
+
614
+ Textual's generic DOM prune refreshes the parent with ``layout=True``.
615
+ Screens fill the viewport independently, so removing a Screen from the
616
+ App cannot change MainScreen geometry. Descendant pruning retains the
617
+ stock path, including its required parent layout.
618
+ """
619
+ whole_screens = parent is self and nodes and all(isinstance(node, Screen) for node in nodes)
620
+ if not whole_screens:
621
+ return super()._prune(*nodes, parent=parent)
622
+
623
+ # Suppress only App.refresh(layout=True). Textual's normal post-prune
624
+ # path also transfers hover state to the newly revealed screen, which
625
+ # must still happen without moving the mouse.
626
+ await_remove = super()._prune(*nodes, parent=None)
627
+ try:
628
+ revealed_screen = self.screen
629
+ except ScreenStackError, NoScreen:
630
+ return await_remove
631
+
632
+ if self.mouse_over is None:
633
+ return await_remove
634
+
635
+ async def update_mouse_over_after_remove() -> None:
636
+ await await_remove
637
+ if revealed_screen._running:
638
+ try:
639
+ mouse_over, hover_over = revealed_screen.get_hover_widgets_at(*self.mouse_position).widgets
640
+ except NoWidget:
641
+ self._set_mouse_over(None, None)
642
+ else:
643
+ self._set_mouse_over(mouse_over, hover_over)
644
+
645
+ self.call_next(update_mouse_over_after_remove)
646
+ return await_remove
647
+
648
+ async def on_event(self, event: textual_events.Event) -> None:
649
+ """Observe GC-relevant activity before Textual dispatches it."""
650
+ if not event.is_forwarded:
651
+ if isinstance(event, textual_events.MouseDown) and event.button:
652
+ self._gc_pointer_buttons_down.add(event.button)
653
+ elif isinstance(event, textual_events.MouseUp) and event.button:
654
+ self._gc_pointer_buttons_down.discard(event.button)
655
+ elif isinstance(event, textual_events.MouseMove):
656
+ if event.button:
657
+ self._gc_pointer_buttons_down.add(event.button)
658
+ self._gc_freeze.note_input(occurred_at=event.time)
659
+ elif self._gc_pointer_buttons_down:
660
+ self._gc_pointer_buttons_down.clear()
661
+ elif isinstance(event, (textual_events.AppBlur, textual_events.AppFocus)):
662
+ self._gc_pointer_buttons_down.clear()
663
+
664
+ if isinstance(event, _GC_ACTIVITY_EVENTS):
665
+ self._gc_freeze.note_input(occurred_at=event.time)
666
+ if isinstance(event, textual_events.Resize):
667
+ self._gc_freeze.request_absorb(
668
+ reason=GcAbsorbReason.VIEWPORT_UPDATED,
669
+ terminal_boundary=False,
670
+ requested_at=event.time,
671
+ )
672
+ await super().on_event(event)
673
+
674
+ @property
675
+ def current_theme(self) -> Theme:
676
+ if self.theme_preview is not None and self.theme_preview.current is not None:
677
+ return self.theme_preview.current
678
+ return super().current_theme
679
+
680
+ def get_css_variables(self) -> dict[str, str]:
681
+ if self.theme_preview is not None and self.theme_preview.variables is not None:
682
+ self.theme_variables = self.theme_preview.variables.copy()
683
+ return self.theme_variables
684
+ return super().get_css_variables()
685
+
686
+ def begin_theme_preview(self, theme: Theme) -> bool:
687
+ if self.theme_preview is not None:
688
+ return False
689
+ self._theme_preview_source = self.theme
690
+ self.theme_preview = ThemePreview(self)
691
+ return self.theme_preview.show(theme)
692
+
693
+ def end_theme_preview(self) -> None:
694
+ preview = self.theme_preview
695
+ if preview is None:
696
+ return
697
+ source = self.get_theme(self.theme)
698
+ restored = source is not None and preview.show(source)
699
+ self.theme_preview = None
700
+ if not restored:
701
+ # Restore classes, Rich consumers and CSS even if preview admission
702
+ # failed. Closing an editor must never persist a fallback selection.
703
+ self.suspend_theme_choice()
704
+ try:
705
+ if source is None:
706
+ self.set_reactive(App.theme, DEFAULT_THEME)
707
+ self._watch_theme(self.theme)
708
+ finally:
709
+ self.resume_theme_choice()
710
+
711
+ def suspend_theme_choice(self) -> None:
712
+ self._theme_selection_depth += 1
713
+
714
+ def resume_theme_choice(self) -> None:
715
+ self._theme_selection_depth = max(0, self._theme_selection_depth - 1)
716
+
717
+ def register_user_theme(self, theme: Theme) -> None:
718
+ from chrys.app.tui.theme_loader import theme_is_read_only
719
+
720
+ if theme_is_read_only(theme.name):
721
+ raise ValueError("Built-in themes are read-only")
722
+ self.register_theme(theme)
723
+ self._user_theme_names.add(theme.name)
724
+
725
+ def apply_saved_theme(self, theme: Theme) -> None:
726
+ """Apply an editor's successful save and make it the new preview exit target."""
727
+ self.register_user_theme(theme)
728
+ preview = self.theme_preview
729
+ # Only a completed save commits a selection while editing. Reuse the
730
+ # normal watcher/persistence path, then retain the editor's live preview.
731
+ self.theme_preview = None
732
+ try:
733
+ self.apply_theme_setting(theme.name)
734
+ self._theme_preview_source = self.theme
735
+ finally:
736
+ self.theme_preview = preview
737
+
738
+ def unregister_user_theme(self, name: str) -> None:
739
+ """Forget a deleted file and repair its applied selection, including previews."""
740
+ from chrys.app.tui.theme_loader import theme_is_read_only
741
+
742
+ if theme_is_read_only(name):
743
+ raise ValueError("Built-in themes are read-only")
744
+ if name in {self.theme, self._settings.theme}:
745
+ preview = self.theme_preview
746
+ self.theme_preview = None
747
+ previous = self.theme
748
+ self.apply_theme_setting(DEFAULT_THEME)
749
+ if preview is not None:
750
+ # Applying a different name schedules the required style pass.
751
+ # An already active fallback also needs to shed its draft CSS.
752
+ if previous == self.theme:
753
+ self._watch_theme(self.theme)
754
+ self._theme_preview_source = self.theme
755
+ self.theme_preview = ThemePreview(self)
756
+ self.unregister_theme(name)
757
+ self._user_theme_names.discard(name)
758
+
759
+ def _watch_theme(self, theme_name: str) -> None:
760
+ """Apply the selected theme and persist confirmed choices."""
761
+ if self.theme_preview is not None:
762
+ self.set_reactive(App.theme, self._theme_preview_source)
763
+ return
764
+ previous_theme = self._gc_effective_theme
765
+ if self.ansi_color is not None:
766
+ self.ansi_color = None
767
+ super()._watch_theme(theme_name)
768
+ # Persist the choice so it survives restart. Skip no-op writes
769
+ # (startup applies the already-persisted value), ignore transient names
770
+ # not registered as real themes, and stay out of startup entirely —
771
+ # see ``_theme_persist_armed`` in ``__init__``.
772
+ if (
773
+ self._theme_persist_armed
774
+ and theme_name
775
+ and theme_name in self.available_themes
776
+ and theme_name != self._settings.theme
777
+ ):
778
+ self._record_theme_choice(theme_name)
779
+ self._gc_effective_theme = theme_name
780
+ if previous_theme and theme_name != previous_theme:
781
+ self._gc_freeze.request_reclaim(
782
+ reason=GcReclaimReason.THEME_UPDATED,
783
+ prompt=False,
784
+ )
785
+
786
+ def _recover_user_theme(self, stylesheet: Stylesheet, error: Exception) -> bool:
787
+ """Fall back only if these same styles work with the default theme."""
788
+ if self.theme_preview is not None:
789
+ return self.theme_preview.recover(stylesheet, error)
790
+ name = self.theme
791
+ if name not in self._user_theme_names:
792
+ return False
793
+ fallback = with_tui_css_variables(
794
+ CHRYS_THEME,
795
+ {**self.get_theme_variable_defaults(), **CHRYS_THEME.to_color_system().generate(), **CHRYS_THEME.variables},
796
+ )
797
+ probe = Stylesheet(variables=fallback)
798
+ probe.source = stylesheet.source.copy()
799
+ try:
800
+ probe.parse()
801
+ except StylesheetError, TokenError:
802
+ # An application CSS error must still surface as an application error.
803
+ return False
804
+ stylesheet.set_variables(fallback)
805
+ # As with an unknown saved theme, recovery must not overwrite the choice.
806
+ armed = self._theme_persist_armed
807
+ self._theme_persist_armed = False
808
+ try:
809
+ self.theme = DEFAULT_THEME
810
+ finally:
811
+ self._theme_persist_armed = armed
812
+ logger.warning("User theme %r failed CSS parsing; using %s", name, DEFAULT_THEME, exc_info=error)
813
+ warning = user_theme_warning(name, f"Theme {name!r} has invalid CSS; using {DEFAULT_THEME}")
814
+ if self._main_screen is None:
815
+ self._startup_warnings.append(warning)
816
+ else:
817
+ self.call_next(self._bus.publish, warning)
818
+ return True
819
+
820
+ def _record_theme_choice(self, theme_name: str) -> None:
821
+ """Persist a theme choice and overlay it on the shared handle, as a pair."""
822
+ if self.theme_preview is not None or self._theme_selection_depth:
823
+ return
824
+ persist_theme(theme_name)
825
+ self._settings_handle.override(theme=theme_name)
826
+
827
+ def apply_theme_setting(self, theme_name: str) -> bool:
828
+ """Apply and persist an explicit theme choice, including an unchanged fallback.
829
+
830
+ Normally the reactive assignment runs ``_watch_theme`` and that
831
+ persists. The exception is a theme already showing but not saved —
832
+ a saved theme that no longer exists fell back to the default at
833
+ startup, and the user now picks that default: the reactive does not
834
+ fire on an equal value, so the persist/overlay pair runs directly.
835
+ """
836
+ if self.theme_preview is not None:
837
+ self.notify(render_str(self.localizer, THEME_PREVIEW_HINT.bind()), severity="warning", markup=False)
838
+ return False
839
+ if theme_name not in self.available_themes:
840
+ return False
841
+ if theme_name == self.theme:
842
+ if theme_name != self._settings.theme:
843
+ self._record_theme_choice(theme_name)
844
+ return True
845
+ self.theme = theme_name
846
+ return True
847
+
848
+ def action_command_palette(self) -> None:
849
+ """Repurpose command palette shortcut as quit."""
850
+ self.exit()
851
+
852
+ async def action_quit(self) -> None:
853
+ """Ctrl+Q with a modal on top: let the Settings dialog commit what is being typed first.
854
+
855
+ The dialog may sit under another modal (a confirmation, a file picker),
856
+ so the whole stack is checked rather than the top screen alone.
857
+ """
858
+ for screen in self.screen_stack:
859
+ if isinstance(screen, SettingsDialog):
860
+ screen.commit_pending()
861
+ self.exit()
862
+
863
+ def action_copy_with_insert(self) -> None:
864
+ """Copy the focused editor selection, then fall back to the screen selection."""
865
+ copy_with_insert(self)
866
+
867
+ def action_paste_with_insert(self) -> None:
868
+ """Route clipboard text through the same Paste event used by the terminal driver."""
869
+ paste_with_insert(self)
870
+
871
+ def action_help_quit(self) -> None:
872
+ """Localize stock App's inherited ctrl+c hint (English notify)."""
873
+ for key, active_binding in self.active_bindings.items():
874
+ if active_binding.binding.action in ("quit", "app.quit"):
875
+ self.notify(
876
+ render_str(self.localizer, _QUIT_HELP.bind(key=key)),
877
+ title=render_str(self.localizer, _QUIT_HELP_TITLE.bind()),
878
+ markup=False,
879
+ )
880
+ return
881
+
882
+ def _record_editor_keymap_override(self, value: str) -> None:
883
+ """Record the editor's keymap pick like the theme and locale pickers.
884
+
885
+ A RUNTIME override on the shared handle keeps settings describing the
886
+ screen — the persisting write next to it swallows failures, and a
887
+ reload must not un-switch the visible keymap.
888
+ """
889
+ self._settings_handle.override(editor_keymap=value)
890
+
891
+ def _build_main_screen(self) -> MainScreen:
892
+ """Build the main screen mounted at startup.
893
+
894
+ Subclasses can override this factory to provide a MainScreen variant
895
+ while reusing the standard app bootstrap.
896
+ """
897
+ return MainScreen(
898
+ self._bus,
899
+ state_store=self._state_store,
900
+ agent_registry=self._agent_registry,
901
+ model_registry=self._model_registry,
902
+ active_model_profile_id=self._settings.model_profile,
903
+ apply_saved_model_on_restore=self._apply_saved_model_on_restore,
904
+ workspace_mru_max_entries=self._settings.workspace_mru_max_entries,
905
+ editor_keymap=self._settings.editor_keymap,
906
+ trajectory_verify_commands=self._settings.trajectory_verify_commands,
907
+ engine_provider=lambda: self._engine,
908
+ settings_handle=self._settings_handle,
909
+ localization=self.localizer,
910
+ locale_controller=self._locale_controller,
911
+ on_editor_keymap_changed=self._record_editor_keymap_override,
912
+ tool_groups_expanded=lambda: self._settings_handle.settings.tool_groups_expanded,
913
+ approval_mode=self._engine.approval_mode,
914
+ )
915
+
916
+ async def on_mount(self) -> None:
917
+ """Push the main screen and start the engine."""
918
+ self._set_terminal_title_for_cwd()
919
+ screen = self._build_main_screen()
920
+ await self.push_screen(screen)
921
+ self._main_screen = screen
922
+ self._gc_freeze.start()
923
+ if self._gc_freeze.startup_warning is not None:
924
+ self._startup_warnings.append(
925
+ Warning(code=_GC_FREEZE_DISABLED_WARNING_CODE, message=self._gc_freeze.startup_warning)
926
+ )
927
+ if self._gc_freeze.started:
928
+ self._gc_freeze_watchdog = self.set_interval(GC_FREEZE_WATCHDOG_PERIOD_SECONDS, self._gc_freeze.on_tick)
929
+
930
+ # MainScreen has now subscribed to ``Warning``; flush any
931
+ # warnings collected before the bus had listeners.
932
+ for w in self._startup_warnings:
933
+ await self._bus.publish(w)
934
+ self._startup_warnings.clear()
935
+
936
+ registry = self._agent_registry
937
+ if not registry.list_profiles():
938
+ registry.load_all()
939
+
940
+ profile = _resolve_startup_profile(registry, self._profile_name)
941
+
942
+ if profile is not None:
943
+ screen.set_startup_agent_loading(True)
944
+ self._startup_task = asyncio.create_task(self._start_engine(profile, screen))
945
+ else:
946
+ # No engine start means no restore either: the bootstrap settings
947
+ # stay in force, so their warnings are due after all.
948
+ await self._flush_deferred_settings_warnings()
949
+
950
+ async def _flush_deferred_settings_warnings(self) -> None:
951
+ for w in self._deferred_settings_warnings:
952
+ await self._bus.publish(w)
953
+ self._deferred_settings_warnings.clear()
954
+
955
+ async def _start_engine(self, profile: AgentProfile, screen: MainScreen) -> None:
956
+ """Start the backend without holding Textual's mount path."""
957
+ try:
958
+ if self._startup_session_id:
959
+ # Session restore builds the saved profile/runtime itself. Preparing
960
+ # subscriptions first avoids building the default agent only to tear
961
+ # it down immediately in the restore path.
962
+ await self._engine.prepare(profile)
963
+ if await self._restore_startup_session(screen):
964
+ # The restore republished the warnings of its own settings
965
+ # load; the bootstrap ones no longer describe anything live.
966
+ self._deferred_settings_warnings.clear()
967
+ return
968
+ await self._engine.reset_after_failed_startup_restore()
969
+ await self._flush_deferred_settings_warnings()
970
+ await self._engine.start(profile)
971
+ except asyncio.CancelledError:
972
+ raise
973
+ except Exception as exc:
974
+ if screen.is_startup_agent_loading():
975
+ screen.set_startup_agent_loading(False)
976
+ self._show_startup_failure(screen, exc)
977
+ logger.exception("Engine startup failed")
978
+
979
+ async def _restore_startup_session(self, screen: MainScreen) -> bool:
980
+ """Restore the requested startup session, returning whether it succeeded."""
981
+ session_id = self._startup_session_id
982
+ if not session_id:
983
+ return False
984
+ self._startup_session_id = ""
985
+ screen.set_startup_agent_loading(True)
986
+ try:
987
+ meta = await self._state_store.load_session_meta(session_id)
988
+ except Exception as exc:
989
+ screen.set_startup_agent_loading(False)
990
+ logger.warning("Failed to read startup session %s", session_id, exc_info=True)
991
+ self._show_startup_session_warning(
992
+ screen,
993
+ _SESSION_READ_FAILED.bind(session_id=session_short_id(session_id), error=str(exc)),
994
+ )
995
+ return False
996
+ if meta is None:
997
+ screen.set_startup_agent_loading(False)
998
+ self._show_startup_session_warning(
999
+ screen,
1000
+ _SESSION_NOT_FOUND.bind(session_id=session_short_id(session_id)),
1001
+ )
1002
+ return False
1003
+ session_id = meta.session_id or session_id
1004
+ await self._dismiss_startup_load_dialog_before_restore(screen)
1005
+ try:
1006
+ outcome = await screen.restore_startup_session(session_id)
1007
+ if outcome == "restored":
1008
+ return True
1009
+ screen.cancel_startup_session_restore()
1010
+ if outcome == "declined":
1011
+ # The user chose no folder for a session whose folder is gone: start fresh, quietly.
1012
+ return False
1013
+ logger.warning("Startup session restore produced no SessionRestored event for %s", session_id)
1014
+ self._show_startup_session_warning(
1015
+ screen,
1016
+ _SESSION_RESTORE_FALLBACK.bind(session_id=session_short_id(session_id)),
1017
+ )
1018
+ return False
1019
+ except Exception as exc:
1020
+ screen.cancel_startup_session_restore()
1021
+ screen.set_startup_agent_loading(False)
1022
+ logger.warning("Failed to restore startup session %s", session_id, exc_info=True)
1023
+ self._show_startup_session_warning(
1024
+ screen,
1025
+ _SESSION_RESTORE_FAILED.bind(session_id=session_short_id(session_id), error=str(exc)),
1026
+ )
1027
+ return False
1028
+
1029
+ async def _dismiss_startup_load_dialog_before_restore(self, screen: MainScreen) -> None:
1030
+ """Clear the startup loading modal before opening the restore modal."""
1031
+ await screen.dismiss_startup_load_dialog_before_restore()
1032
+
1033
+ def _show_startup_session_warning(self, screen: MainScreen, message: MessageRef | str) -> None:
1034
+ with contextlib.suppress(Exception):
1035
+ from chrys.app.tui.widgets.chrome.status_bar import StatusBar
1036
+
1037
+ screen.query_one(StatusBar).flash(message, error=True)
1038
+ with contextlib.suppress(Exception):
1039
+ screen.notify(
1040
+ message if isinstance(message, str) else render_str(self.localizer, message),
1041
+ title=render_str(self.localizer, _SESSION_NOTIFICATION_TITLE.bind()),
1042
+ severity="warning",
1043
+ timeout=6,
1044
+ markup=False,
1045
+ )
1046
+
1047
+ def _set_terminal_title_for_cwd(self) -> None:
1048
+ """Set the terminal title for the active workspace directory."""
1049
+ set_app_terminal_title_for_current_cwd(self)
1050
+
1051
+ def _show_startup_failure(self, screen: MainScreen, exc: Exception) -> None:
1052
+ """Surface startup failures that occur before AgentLoadStarted exists."""
1053
+ message = str(exc) or type(exc).__name__
1054
+ with contextlib.suppress(Exception):
1055
+ from chrys.app.tui.widgets.chrome.status_bar import StatusBar
1056
+
1057
+ screen.query_one(StatusBar).flash(STATUS_AGENT_STARTUP_FAILED.bind(message=message), error=True)
1058
+ with contextlib.suppress(Exception):
1059
+ screen.notify(
1060
+ message,
1061
+ title=render_str(self.localizer, _AGENT_STARTUP_FAILED_TITLE.bind()),
1062
+ severity="error",
1063
+ timeout=10,
1064
+ markup=False,
1065
+ )
1066
+
1067
+ async def on_unmount(self) -> None:
1068
+ """Shutdown engine on exit."""
1069
+ self._gc_freeze.close()
1070
+ if self._gc_freeze_watchdog is not None:
1071
+ self._gc_freeze_watchdog.stop()
1072
+ self._gc_freeze_watchdog = None
1073
+ if self._startup_task is not None and not self._startup_task.done():
1074
+ self._startup_task.cancel()
1075
+ with contextlib.suppress(asyncio.CancelledError):
1076
+ await self._startup_task
1077
+ self._startup_task = None
1078
+ await self._engine.shutdown()
1079
+ # After the engine: its shutdown drains a still-finalizing run
1080
+ # whose success callback can schedule one last title task; the
1081
+ # updater must be the last thing torn down so that task is
1082
+ # cancelled and awaited (its closed flag backstops other hosts).
1083
+ if self._session_title_updater is not None:
1084
+ await self._session_title_updater.shutdown()
1085
+
1086
+ def _handle_exception(self, error: Exception) -> None:
1087
+ """Mirror Textual's crash handling, but also dump the traceback to a file.
1088
+
1089
+ Textual's default path renders the traceback to ``self.error_console``
1090
+ (stderr) only after the terminal has been restored at shutdown. That
1091
+ is great for visibility but the trace is lost the moment the user
1092
+ clears the terminal. Writing it to ``~/.chrys/logs/error.log`` gives
1093
+ the user a persistent copy without changing Textual's console output.
1094
+ """
1095
+ # File logging must NEVER break the crash-handling path.
1096
+ with contextlib.suppress(Exception):
1097
+ self._write_crash_log(error)
1098
+ super()._handle_exception(error)
1099
+
1100
+ def _write_crash_log(self, error: Exception) -> None:
1101
+ """Write a plain-text crash report to ``~/.chrys/logs/error.log``.
1102
+
1103
+ The first crash within a single app run truncates the file (so stale
1104
+ data from a previous run never confuses the user); subsequent crashes
1105
+ in the same run append, preserving cascading teardown errors that
1106
+ Textual would otherwise collect into ``_exit_renderables``.
1107
+ """
1108
+ import traceback
1109
+ from datetime import datetime
1110
+
1111
+ pinfo = chrys_platform.get_platform()
1112
+ log_path = pinfo.config_dir / "logs" / "error.log"
1113
+ log_path.parent.mkdir(parents=True, exist_ok=True)
1114
+
1115
+ mode = "w" if not self._crash_log_initialized else "a"
1116
+ self._crash_log_initialized = True
1117
+
1118
+ version = __version__
1119
+
1120
+ # Indent every line of the (possibly multi-line) error message by 4
1121
+ # spaces so continuation lines stay visually inside the Error block.
1122
+ msg_lines = str(error).splitlines() or [""]
1123
+ first_msg, *rest_msg = msg_lines
1124
+ message_block = "\n".join([f" Message: {first_msg}", *(f" {line}" for line in rest_msg)])
1125
+
1126
+ header = (
1127
+ "================================================================\n"
1128
+ f"{APP_DISPLAY_NAME} Crash Report\n"
1129
+ f"Time: {datetime.now().astimezone().isoformat(timespec='seconds')}\n"
1130
+ f"Version: {version}\n"
1131
+ f"OS: {pinfo.os_display}\n"
1132
+ f"Release: {pinfo.os_version}\n"
1133
+ f"Arch: {pinfo.arch}\n"
1134
+ f"Shell: {pinfo.shell.path}\n"
1135
+ f"Cwd: {chrys_platform.safe_getcwd()}\n"
1136
+ "Error:\n"
1137
+ f" {type(error).__name__}\n"
1138
+ f"{message_block}\n"
1139
+ "================================================================\n"
1140
+ )
1141
+
1142
+ # Try a rich Traceback for parity with what Textual prints to stderr
1143
+ # (frames + locals). Fall back to stdlib if rich rendering itself
1144
+ # raises — we must always produce *some* output here.
1145
+ body: str
1146
+ try:
1147
+ import io
1148
+
1149
+ import rich
1150
+ from rich.console import Console
1151
+ from rich.traceback import Traceback
1152
+
1153
+ buf = io.StringIO()
1154
+ console = Console(file=buf, no_color=True, force_terminal=False, width=120)
1155
+ tb = Traceback.from_exception(
1156
+ type(error),
1157
+ error,
1158
+ error.__traceback__,
1159
+ show_locals=True,
1160
+ locals_max_length=5,
1161
+ suppress=[rich],
1162
+ )
1163
+ console.print(tb)
1164
+ body = buf.getvalue()
1165
+ except Exception:
1166
+ body = "".join(traceback.format_exception(type(error), error, error.__traceback__))
1167
+
1168
+ with open(log_path, mode, encoding="utf-8") as f:
1169
+ f.write(header)
1170
+ f.write(body)
1171
+ if not body.endswith("\n"):
1172
+ f.write("\n")
1173
+ f.write("\n")
1174
+
1175
+
1176
+ def main(app_cls: type[ChrysApp] = ChrysApp) -> None:
1177
+ """Entry point for the ``chrys`` console script.
1178
+
1179
+ ``app_cls`` lets development tools swap in a ``ChrysApp`` subclass
1180
+ while reusing the rest of the bootstrap wiring. Defaults to
1181
+ ``ChrysApp`` for the production entry point.
1182
+ """
1183
+ import logging
1184
+
1185
+ from chrys.foundation.config.warnings import settings_warning_events
1186
+ from chrys.orchestration.startup import bootstrap_runtime, configure_utf8_stdio
1187
+
1188
+ configure_utf8_stdio()
1189
+
1190
+ # Early argparse prose remains English; dynamic error details are sanitized.
1191
+ parser = SanitizingArgumentParser(
1192
+ description=f"{APP_DISPLAY_NAME} - A general-purpose extensible agent platform for rapidly building and evolving custom agents."
1193
+ )
1194
+ parser.add_argument("-s", "--session", default="", help="Restore the given session id after startup.")
1195
+ parser.add_argument(
1196
+ "-a", "--agent", default="", help="Start with the given agent profile id, name, or display name."
1197
+ )
1198
+ parser.add_argument(
1199
+ "-m",
1200
+ "--model",
1201
+ default="",
1202
+ help="Start with the given active model profile id or name.",
1203
+ )
1204
+ parser.add_argument("-C", "--workdir", default="", metavar="DIR", help="Start in the given working directory.")
1205
+ args = parser.parse_args()
1206
+ if (missing_cwd := launch_cwd_missing_message(workdir_flag=True, workdir=args.workdir)) is not None:
1207
+ parser.exit(1, f"Error: {missing_cwd}\n")
1208
+ if args.workdir:
1209
+ workdir = Path(args.workdir).expanduser()
1210
+ if not workdir.is_dir():
1211
+ parser.error(f"workdir does not exist or is not a directory: {args.workdir}")
1212
+ os.chdir(workdir)
1213
+
1214
+ log_dir = chrys_platform.get_platform().config_dir / "logs"
1215
+ log_dir.mkdir(parents=True, exist_ok=True)
1216
+ debug_log = configure_debug_logging(log_dir / "debug.log")
1217
+ try:
1218
+ # Silence noisy third-party loggers
1219
+ logging.getLogger("markdown_it").setLevel(logging.WARNING)
1220
+
1221
+ # Install the in-memory log handler used by the F6 Log Viewer modal
1222
+ # *before* any engine / agent / profile code runs, so logs emitted
1223
+ # during startup (profile load, engine build, OTel setup, first agent
1224
+ # construction) are captured and visible the moment the viewer is
1225
+ # opened.
1226
+ from chrys.app.tui.screens.logs import install_log_handler
1227
+
1228
+ install_log_handler()
1229
+
1230
+ bootstrap = bootstrap_runtime(dotenv_override=True, project_root=Path(os.getcwd()))
1231
+
1232
+ bus = EventBus()
1233
+ registry = AgentProfileRegistry()
1234
+ registry.load_all()
1235
+ model_registry = ModelProfileRegistry()
1236
+ model_registry.load_all()
1237
+ loaded = _apply_startup_active_model_selection(parser, bootstrap.loaded, model_registry, args.model)
1238
+ state_store = JsonFileStateStore()
1239
+ session_title_updater = SessionTitleUpdater(bus, state_store)
1240
+
1241
+ def _on_successful_turn() -> None:
1242
+ on_buddy_successful_turn()
1243
+ session_title_updater.on_turn_finished()
1244
+
1245
+ engine = assemble_agent_engine(
1246
+ bus,
1247
+ loaded_settings=loaded,
1248
+ agent_registry=registry,
1249
+ state_store=state_store,
1250
+ model_registry=model_registry,
1251
+ on_successful_turn=_on_successful_turn,
1252
+ on_turn_started=session_title_updater.on_turn_started,
1253
+ surface=SessionSurface.TUI,
1254
+ )
1255
+
1256
+ app = app_cls(
1257
+ bus,
1258
+ engine,
1259
+ settings_handle=engine.settings_handle,
1260
+ profile_name=args.agent,
1261
+ state_store=state_store,
1262
+ agent_registry=registry,
1263
+ model_registry=model_registry,
1264
+ startup_warnings=[*bootstrap.warnings],
1265
+ settings_warnings=settings_warning_events(loaded),
1266
+ startup_session_id=args.session,
1267
+ apply_saved_model_on_restore=not bool(args.model.strip()),
1268
+ session_title_updater=session_title_updater,
1269
+ )
1270
+ try:
1271
+ # On Windows, the ProactorEventLoop shutdown can raise KeyboardInterrupt
1272
+ # from GetQueuedCompletionStatus when a pending signal races with asyncio
1273
+ # runner cleanup. The app has already exited at this point — safe to suppress.
1274
+ with contextlib.suppress(KeyboardInterrupt):
1275
+ app.run()
1276
+ finally:
1277
+ # Safety net: ensure terminal is fully restored after native Textual exits.
1278
+ # Textual should handle this, but shell PTY activity during shutdown can
1279
+ # race with cleanup, leaving mouse tracking, focus reports, or alt screen on.
1280
+ _restore_terminal_after_run(app)
1281
+ _reset_terminal_title_after_run()
1282
+ finally:
1283
+ stop_debug_logging(debug_log)
1284
+
1285
+
1286
+ if __name__ == "__main__":
1287
+ main()