iCode-TUI 0.29.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1138) hide show
  1. chrys/__init__.py +16 -0
  2. chrys/app/__init__.py +3 -0
  3. chrys/app/acp/__init__.py +3 -0
  4. chrys/app/acp/bridge.py +664 -0
  5. chrys/app/acp/content.py +132 -0
  6. chrys/app/acp/doc/frontend-api.md +425 -0
  7. chrys/app/acp/history.py +585 -0
  8. chrys/app/acp/server.py +1796 -0
  9. chrys/app/acp/session_manager.py +1815 -0
  10. chrys/app/acp/tool_status.py +45 -0
  11. chrys/app/cli/__init__.py +3 -0
  12. chrys/app/cli/acp.py +178 -0
  13. chrys/app/cli/app.py +165 -0
  14. chrys/app/cli/approvals.py +101 -0
  15. chrys/app/cli/headless.py +223 -0
  16. chrys/app/cli/launch_cwd.py +42 -0
  17. chrys/app/cli/profiles.py +298 -0
  18. chrys/app/cli/progress.py +857 -0
  19. chrys/app/cli/run.py +425 -0
  20. chrys/app/cli/serve.py +1593 -0
  21. chrys/app/cli/tool_summary.py +225 -0
  22. chrys/app/cli/trajectory.py +201 -0
  23. chrys/app/cli/workflow.py +601 -0
  24. chrys/app/features/__init__.py +3 -0
  25. chrys/app/features/buddy/__init__.py +3 -0
  26. chrys/app/features/buddy/actions.py +68 -0
  27. chrys/app/features/buddy/animation.py +76 -0
  28. chrys/app/features/buddy/commands.py +196 -0
  29. chrys/app/features/buddy/docs/design.md +187 -0
  30. chrys/app/features/buddy/hatchery.py +80 -0
  31. chrys/app/features/buddy/lifecycle.py +41 -0
  32. chrys/app/features/buddy/model.py +265 -0
  33. chrys/app/features/buddy/pixel_renderer.py +113 -0
  34. chrys/app/features/buddy/pixel_sprites.py +279 -0
  35. chrys/app/features/buddy/portrait.py +76 -0
  36. chrys/app/features/buddy/progression.py +58 -0
  37. chrys/app/features/buddy/replies.py +191 -0
  38. chrys/app/features/buddy/sprites/alien.toml +70 -0
  39. chrys/app/features/buddy/sprites/axolotl.toml +71 -0
  40. chrys/app/features/buddy/sprites/bat.toml +69 -0
  41. chrys/app/features/buddy/sprites/bee.toml +69 -0
  42. chrys/app/features/buddy/sprites/cactus.toml +73 -0
  43. chrys/app/features/buddy/sprites/capybara.toml +70 -0
  44. chrys/app/features/buddy/sprites/cat.toml +69 -0
  45. chrys/app/features/buddy/sprites/crab.toml +70 -0
  46. chrys/app/features/buddy/sprites/dragon.toml +71 -0
  47. chrys/app/features/buddy/sprites/duck.toml +69 -0
  48. chrys/app/features/buddy/sprites/fox.toml +69 -0
  49. chrys/app/features/buddy/sprites/frog.toml +70 -0
  50. chrys/app/features/buddy/sprites/ghost.toml +70 -0
  51. chrys/app/features/buddy/sprites/goose.toml +69 -0
  52. chrys/app/features/buddy/sprites/jelly.toml +70 -0
  53. chrys/app/features/buddy/sprites/llama.toml +70 -0
  54. chrys/app/features/buddy/sprites/mushroom.toml +69 -0
  55. chrys/app/features/buddy/sprites/octopus.toml +70 -0
  56. chrys/app/features/buddy/sprites/owl.toml +70 -0
  57. chrys/app/features/buddy/sprites/panda.toml +71 -0
  58. chrys/app/features/buddy/sprites/penguin.toml +69 -0
  59. chrys/app/features/buddy/sprites/rabbit.toml +70 -0
  60. chrys/app/features/buddy/sprites/robot.toml +72 -0
  61. chrys/app/features/buddy/sprites/shark.toml +70 -0
  62. chrys/app/features/buddy/sprites/snail.toml +69 -0
  63. chrys/app/features/buddy/sprites/snake.toml +69 -0
  64. chrys/app/features/buddy/sprites/tabby.toml +69 -0
  65. chrys/app/features/buddy/sprites/turtle.toml +69 -0
  66. chrys/app/features/buddy/store.py +143 -0
  67. chrys/app/features/session_title/__init__.py +7 -0
  68. chrys/app/features/session_title/updater.py +582 -0
  69. chrys/app/installer.py +651 -0
  70. chrys/app/parsing.py +17 -0
  71. chrys/app/tui/__init__.py +3 -0
  72. chrys/app/tui/app.py +1287 -0
  73. chrys/app/tui/behaviors/__init__.py +3 -0
  74. chrys/app/tui/behaviors/click_outside_dismiss.py +90 -0
  75. chrys/app/tui/behaviors/insert_clipboard.py +73 -0
  76. chrys/app/tui/behaviors/right_click_copy.py +146 -0
  77. chrys/app/tui/binding_display.py +139 -0
  78. chrys/app/tui/buddy_messages.py +17 -0
  79. chrys/app/tui/buddy_reply.py +86 -0
  80. chrys/app/tui/chrys.tcss +549 -0
  81. chrys/app/tui/clipboard.py +109 -0
  82. chrys/app/tui/copy_messages.py +10 -0
  83. chrys/app/tui/i18n.py +230 -0
  84. chrys/app/tui/language.py +58 -0
  85. chrys/app/tui/notifications/__init__.py +12 -0
  86. chrys/app/tui/notifications/drivers.py +180 -0
  87. chrys/app/tui/notifications/service.py +126 -0
  88. chrys/app/tui/notifications/settings.py +141 -0
  89. chrys/app/tui/screens/__init__.py +3 -0
  90. chrys/app/tui/screens/agents/__init__.py +8 -0
  91. chrys/app/tui/screens/agents/agent_draft_store.py +937 -0
  92. chrys/app/tui/screens/agents/config.py +2142 -0
  93. chrys/app/tui/screens/agents/config.tcss +169 -0
  94. chrys/app/tui/screens/agents/panels/__init__.py +3 -0
  95. chrys/app/tui/screens/agents/panels/acp.py +1176 -0
  96. chrys/app/tui/screens/agents/panels/basic.py +497 -0
  97. chrys/app/tui/screens/agents/panels/compaction.py +231 -0
  98. chrys/app/tui/screens/agents/panels/config_card.py +218 -0
  99. chrys/app/tui/screens/agents/panels/instructions.py +91 -0
  100. chrys/app/tui/screens/agents/panels/markdown_report.py +76 -0
  101. chrys/app/tui/screens/agents/panels/mcp.py +2262 -0
  102. chrys/app/tui/screens/agents/panels/memory.py +684 -0
  103. chrys/app/tui/screens/agents/panels/path_entry.py +227 -0
  104. chrys/app/tui/screens/agents/panels/skills.py +806 -0
  105. chrys/app/tui/screens/agents/panels/subagents.py +652 -0
  106. chrys/app/tui/screens/agents/panels/tools.py +205 -0
  107. chrys/app/tui/screens/agents/picker.py +103 -0
  108. chrys/app/tui/screens/agents/skill_paths.py +38 -0
  109. chrys/app/tui/screens/agents/validation_messages.py +452 -0
  110. chrys/app/tui/screens/dialogs/__init__.py +35 -0
  111. chrys/app/tui/screens/dialogs/agent_load.py +834 -0
  112. chrys/app/tui/screens/dialogs/agent_load.tcss +78 -0
  113. chrys/app/tui/screens/dialogs/app_mode.py +55 -0
  114. chrys/app/tui/screens/dialogs/approval/__init__.py +15 -0
  115. chrys/app/tui/screens/dialogs/approval/approval.tcss +311 -0
  116. chrys/app/tui/screens/dialogs/approval/bodies/__init__.py +3 -0
  117. chrys/app/tui/screens/dialogs/approval/bodies/file_edit.py +210 -0
  118. chrys/app/tui/screens/dialogs/approval/bodies/sub_agent.py +82 -0
  119. chrys/app/tui/screens/dialogs/approval/body.py +110 -0
  120. chrys/app/tui/screens/dialogs/approval/dialog.py +565 -0
  121. chrys/app/tui/screens/dialogs/approval/mode.py +105 -0
  122. chrys/app/tui/screens/dialogs/ask_user.py +148 -0
  123. chrys/app/tui/screens/dialogs/ask_user.tcss +62 -0
  124. chrys/app/tui/screens/dialogs/base.py +84 -0
  125. chrys/app/tui/screens/dialogs/confirm.py +184 -0
  126. chrys/app/tui/screens/dialogs/confirm.tcss +46 -0
  127. chrys/app/tui/screens/dialogs/connection_test.py +183 -0
  128. chrys/app/tui/screens/dialogs/connection_test.tcss +92 -0
  129. chrys/app/tui/screens/dialogs/diagram.py +102 -0
  130. chrys/app/tui/screens/dialogs/diagram.tcss +24 -0
  131. chrys/app/tui/screens/dialogs/editor.py +378 -0
  132. chrys/app/tui/screens/dialogs/editor.tcss +132 -0
  133. chrys/app/tui/screens/dialogs/file_picker.py +582 -0
  134. chrys/app/tui/screens/dialogs/file_picker.tcss +78 -0
  135. chrys/app/tui/screens/dialogs/fork_session.py +219 -0
  136. chrys/app/tui/screens/dialogs/fork_session.tcss +75 -0
  137. chrys/app/tui/screens/dialogs/image_compression.py +102 -0
  138. chrys/app/tui/screens/dialogs/image_compression.tcss +28 -0
  139. chrys/app/tui/screens/dialogs/man_page.py +172 -0
  140. chrys/app/tui/screens/dialogs/man_page.tcss +39 -0
  141. chrys/app/tui/screens/dialogs/new_folder.py +277 -0
  142. chrys/app/tui/screens/dialogs/new_folder.tcss +51 -0
  143. chrys/app/tui/screens/dialogs/notice.tcss +14 -0
  144. chrys/app/tui/screens/dialogs/prompt_cache.py +144 -0
  145. chrys/app/tui/screens/dialogs/prompt_cache.tcss +38 -0
  146. chrys/app/tui/screens/dialogs/runtime_details.py +371 -0
  147. chrys/app/tui/screens/dialogs/runtime_details.tcss +49 -0
  148. chrys/app/tui/screens/dialogs/session_title.py +110 -0
  149. chrys/app/tui/screens/dialogs/session_title.tcss +43 -0
  150. chrys/app/tui/screens/dialogs/tool_view.py +207 -0
  151. chrys/app/tui/screens/dialogs/tool_view.tcss +43 -0
  152. chrys/app/tui/screens/dialogs/vision_unsupported.py +122 -0
  153. chrys/app/tui/screens/dialogs/vision_unsupported.tcss +35 -0
  154. chrys/app/tui/screens/dialogs/workflow_confirm.py +91 -0
  155. chrys/app/tui/screens/dialogs/workflow_confirm.tcss +17 -0
  156. chrys/app/tui/screens/dialogs/workflow_input.py +258 -0
  157. chrys/app/tui/screens/dialogs/workflow_input.tcss +73 -0
  158. chrys/app/tui/screens/dialogs/workflow_node.py +462 -0
  159. chrys/app/tui/screens/dialogs/workflow_node.tcss +41 -0
  160. chrys/app/tui/screens/dialogs/workflow_picker.py +46 -0
  161. chrys/app/tui/screens/dialogs/workflow_result.py +136 -0
  162. chrys/app/tui/screens/dialogs/workflow_result.tcss +14 -0
  163. chrys/app/tui/screens/diff/__init__.py +22 -0
  164. chrys/app/tui/screens/diff/rollback_modal.py +1381 -0
  165. chrys/app/tui/screens/diff/rollback_modal.tcss +248 -0
  166. chrys/app/tui/screens/diff/screen.py +1532 -0
  167. chrys/app/tui/screens/diff/screen.tcss +104 -0
  168. chrys/app/tui/screens/guides/__init__.py +7 -0
  169. chrys/app/tui/screens/guides/_docs/en/guides/configuration/agents.md +178 -0
  170. chrys/app/tui/screens/guides/_docs/en/guides/configuration/approval.md +117 -0
  171. chrys/app/tui/screens/guides/_docs/en/guides/configuration/chat-options.md +276 -0
  172. chrys/app/tui/screens/guides/_docs/en/guides/configuration/compaction.md +42 -0
  173. chrys/app/tui/screens/guides/_docs/en/guides/configuration/memory.md +48 -0
  174. chrys/app/tui/screens/guides/_docs/en/guides/configuration/models.md +158 -0
  175. chrys/app/tui/screens/guides/_docs/en/guides/configuration/settings.md +210 -0
  176. chrys/app/tui/screens/guides/_docs/en/guides/configuration/themes.md +61 -0
  177. chrys/app/tui/screens/guides/_docs/en/guides/configuration/tools.md +60 -0
  178. chrys/app/tui/screens/guides/_docs/en/guides/configuration/web-tools.md +286 -0
  179. chrys/app/tui/screens/guides/_docs/en/guides/daily-use/sessions.md +77 -0
  180. chrys/app/tui/screens/guides/_docs/en/guides/daily-use/workspaces.md +51 -0
  181. chrys/app/tui/screens/guides/_docs/en/guides/extensions/external-acp-agents.md +126 -0
  182. chrys/app/tui/screens/guides/_docs/en/guides/extensions/hooks.md +167 -0
  183. chrys/app/tui/screens/guides/_docs/en/guides/extensions/mcp.md +95 -0
  184. chrys/app/tui/screens/guides/_docs/en/guides/extensions/skills.md +175 -0
  185. chrys/app/tui/screens/guides/_docs/en/guides/running/icode-acp.md +61 -0
  186. chrys/app/tui/screens/guides/_docs/en/guides/running/icode-run.md +190 -0
  187. chrys/app/tui/screens/guides/_docs/en/guides/running/icode-serve.md +239 -0
  188. chrys/app/tui/screens/guides/_docs/en/guides/running/workflows.md +664 -0
  189. chrys/app/tui/screens/guides/_docs/en/reference/agent-profile.md +548 -0
  190. chrys/app/tui/screens/guides/_docs/en/reference/hooks.md +602 -0
  191. chrys/app/tui/screens/guides/_docs/en/reference/opentelemetry.md +177 -0
  192. chrys/app/tui/screens/guides/_docs/en/reference/settings.md +245 -0
  193. chrys/app/tui/screens/guides/_docs/en/reference/themes.md +149 -0
  194. chrys/app/tui/screens/guides/_docs/en/reference/tool-kinds-and-names.md +58 -0
  195. chrys/app/tui/screens/guides/_docs/en/reference/tui-slash-commands.md +76 -0
  196. chrys/app/tui/screens/guides/_docs/en/reference/workflows.md +1006 -0
  197. chrys/app/tui/screens/guides/_docs/en/start/getting-started.md +170 -0
  198. chrys/app/tui/screens/guides/_docs/en/start/what-is-icode.md +34 -0
  199. chrys/app/tui/screens/guides/_docs/index.yaml +100 -0
  200. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/agents.md +178 -0
  201. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/approval.md +117 -0
  202. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/chat-options.md +276 -0
  203. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/compaction.md +42 -0
  204. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/memory.md +48 -0
  205. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/models.md +158 -0
  206. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/settings.md +210 -0
  207. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/themes.md +61 -0
  208. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/tools.md +60 -0
  209. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/web-tools.md +286 -0
  210. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/daily-use/sessions.md +77 -0
  211. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/daily-use/workspaces.md +51 -0
  212. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/external-acp-agents.md +126 -0
  213. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/hooks.md +167 -0
  214. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/mcp.md +95 -0
  215. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/skills.md +175 -0
  216. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-acp.md +61 -0
  217. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-run.md +190 -0
  218. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-serve.md +239 -0
  219. chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/workflows.md +664 -0
  220. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/agent-profile.md +548 -0
  221. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/hooks.md +602 -0
  222. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/opentelemetry.md +177 -0
  223. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/settings.md +245 -0
  224. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/themes.md +149 -0
  225. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/tool-kinds-and-names.md +58 -0
  226. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/tui-slash-commands.md +76 -0
  227. chrys/app/tui/screens/guides/_docs/zh-Hans/reference/workflows.md +1005 -0
  228. chrys/app/tui/screens/guides/_docs/zh-Hans/start/getting-started.md +170 -0
  229. chrys/app/tui/screens/guides/_docs/zh-Hans/start/what-is-icode.md +34 -0
  230. chrys/app/tui/screens/guides/index.py +215 -0
  231. chrys/app/tui/screens/guides/reader.py +173 -0
  232. chrys/app/tui/screens/guides/screen.py +442 -0
  233. chrys/app/tui/screens/guides/screen.tcss +52 -0
  234. chrys/app/tui/screens/logs/__init__.py +7 -0
  235. chrys/app/tui/screens/logs/screen.py +504 -0
  236. chrys/app/tui/screens/logs/screen.tcss +45 -0
  237. chrys/app/tui/screens/main/__init__.py +7 -0
  238. chrys/app/tui/screens/main/action_availability.py +30 -0
  239. chrys/app/tui/screens/main/buddy_command.py +143 -0
  240. chrys/app/tui/screens/main/commands.py +1073 -0
  241. chrys/app/tui/screens/main/config_actions.py +464 -0
  242. chrys/app/tui/screens/main/copy_actions.py +117 -0
  243. chrys/app/tui/screens/main/dialog_controllers.py +826 -0
  244. chrys/app/tui/screens/main/dialog_gateway.py +166 -0
  245. chrys/app/tui/screens/main/diff_controller.py +478 -0
  246. chrys/app/tui/screens/main/engine_read_model.py +184 -0
  247. chrys/app/tui/screens/main/event_handlers.py +2014 -0
  248. chrys/app/tui/screens/main/input_flow.py +241 -0
  249. chrys/app/tui/screens/main/live_diff.py +244 -0
  250. chrys/app/tui/screens/main/main_screen_presenter.py +295 -0
  251. chrys/app/tui/screens/main/model_indicator.py +217 -0
  252. chrys/app/tui/screens/main/navigation.py +343 -0
  253. chrys/app/tui/screens/main/ports.py +571 -0
  254. chrys/app/tui/screens/main/recent_dirs.py +123 -0
  255. chrys/app/tui/screens/main/rollback_controller.py +586 -0
  256. chrys/app/tui/screens/main/runtime_info.py +71 -0
  257. chrys/app/tui/screens/main/screen.py +2265 -0
  258. chrys/app/tui/screens/main/screen.tcss +21 -0
  259. chrys/app/tui/screens/main/session_handlers.py +1127 -0
  260. chrys/app/tui/screens/main/session_title.py +239 -0
  261. chrys/app/tui/screens/main/settings_coordinator.py +365 -0
  262. chrys/app/tui/screens/main/settings_persistence.py +184 -0
  263. chrys/app/tui/screens/main/shell_mode.py +112 -0
  264. chrys/app/tui/screens/main/state.py +289 -0
  265. chrys/app/tui/screens/main/submit_state.py +11 -0
  266. chrys/app/tui/screens/main/subscriptions.py +177 -0
  267. chrys/app/tui/screens/main/suggestions.py +982 -0
  268. chrys/app/tui/screens/main/tool_action_bridge.py +75 -0
  269. chrys/app/tui/screens/main/view_adapter.py +1567 -0
  270. chrys/app/tui/screens/main/workflow_browser.py +747 -0
  271. chrys/app/tui/screens/main/workflow_content.py +248 -0
  272. chrys/app/tui/screens/main/workflow_controller.py +240 -0
  273. chrys/app/tui/screens/main/workflow_feedback.py +137 -0
  274. chrys/app/tui/screens/main/workflow_flow.py +49 -0
  275. chrys/app/tui/screens/main/workflow_run_control.py +456 -0
  276. chrys/app/tui/screens/main/workflow_session_view.py +316 -0
  277. chrys/app/tui/screens/main/workspace_actions.py +272 -0
  278. chrys/app/tui/screens/main/workspace_branch.py +194 -0
  279. chrys/app/tui/screens/menus/__init__.py +8 -0
  280. chrys/app/tui/screens/menus/languages.py +114 -0
  281. chrys/app/tui/screens/models/__init__.py +7 -0
  282. chrys/app/tui/screens/models/picker.py +147 -0
  283. chrys/app/tui/screens/models/screen.py +2043 -0
  284. chrys/app/tui/screens/models/screen.tcss +255 -0
  285. chrys/app/tui/screens/sessions/__init__.py +8 -0
  286. chrys/app/tui/screens/sessions/presenter.py +586 -0
  287. chrys/app/tui/screens/sessions/screen.py +779 -0
  288. chrys/app/tui/screens/sessions/screen.tcss +127 -0
  289. chrys/app/tui/screens/settings/__init__.py +29 -0
  290. chrys/app/tui/screens/settings/danger.py +26 -0
  291. chrys/app/tui/screens/settings/dialog.py +281 -0
  292. chrys/app/tui/screens/settings/layout.py +519 -0
  293. chrys/app/tui/screens/settings/migrate_dialog.py +253 -0
  294. chrys/app/tui/screens/settings/panes/__init__.py +3 -0
  295. chrys/app/tui/screens/settings/panes/notifications.py +248 -0
  296. chrys/app/tui/screens/settings/panes/sessions.py +148 -0
  297. chrys/app/tui/screens/settings/ports.py +55 -0
  298. chrys/app/tui/screens/settings/provenance.py +102 -0
  299. chrys/app/tui/screens/settings/rows.py +546 -0
  300. chrys/app/tui/screens/settings/settings.tcss +406 -0
  301. chrys/app/tui/screens/themes/__init__.py +8 -0
  302. chrys/app/tui/screens/themes/dialogs.py +330 -0
  303. chrys/app/tui/screens/themes/editor.py +632 -0
  304. chrys/app/tui/screens/themes/editor.tcss +268 -0
  305. chrys/app/tui/screens/themes/messages.py +95 -0
  306. chrys/app/tui/screens/themes/palette.py +694 -0
  307. chrys/app/tui/screens/themes/panel.py +113 -0
  308. chrys/app/tui/screens/themes/picker.py +203 -0
  309. chrys/app/tui/screens/themes/save.py +191 -0
  310. chrys/app/tui/support/__init__.py +3 -0
  311. chrys/app/tui/support/file_manager.py +64 -0
  312. chrys/app/tui/support/gc_freeze.py +1114 -0
  313. chrys/app/tui/support/path_shortcuts.py +50 -0
  314. chrys/app/tui/support/prompt_history.py +298 -0
  315. chrys/app/tui/support/workspace_mru.py +476 -0
  316. chrys/app/tui/terminal/__init__.py +6 -0
  317. chrys/app/tui/terminal/_pty_backend.py +114 -0
  318. chrys/app/tui/terminal/emulator/__init__.py +24 -0
  319. chrys/app/tui/terminal/emulator/buffer.py +456 -0
  320. chrys/app/tui/terminal/emulator/charsets.py +113 -0
  321. chrys/app/tui/terminal/emulator/core.py +963 -0
  322. chrys/app/tui/terminal/emulator/input.py +342 -0
  323. chrys/app/tui/terminal/emulator/parser.py +227 -0
  324. chrys/app/tui/terminal/emulator/pen.py +158 -0
  325. chrys/app/tui/terminal/launcher.py +455 -0
  326. chrys/app/tui/terminal/leaked_reports.py +44 -0
  327. chrys/app/tui/terminal/panel.py +323 -0
  328. chrys/app/tui/terminal/pty_process.py +405 -0
  329. chrys/app/tui/terminal/rendering.py +151 -0
  330. chrys/app/tui/terminal/shell.py +273 -0
  331. chrys/app/tui/terminal/shell_integration.py +199 -0
  332. chrys/app/tui/terminal/title.py +224 -0
  333. chrys/app/tui/terminal/widget.py +654 -0
  334. chrys/app/tui/theme.py +422 -0
  335. chrys/app/tui/theme_loader.py +201 -0
  336. chrys/app/tui/theme_stylesheet.py +49 -0
  337. chrys/app/tui/themes/__init__.py +3 -0
  338. chrys/app/tui/themes/document.py +120 -0
  339. chrys/app/tui/themes/preview.py +170 -0
  340. chrys/app/tui/themes/store.py +176 -0
  341. chrys/app/tui/toasts.py +69 -0
  342. chrys/app/tui/util/__init__.py +3 -0
  343. chrys/app/tui/util/context_pressure.py +146 -0
  344. chrys/app/tui/util/debug_log.py +292 -0
  345. chrys/app/tui/util/diff_entries.py +64 -0
  346. chrys/app/tui/util/formatting.py +53 -0
  347. chrys/app/tui/util/git_branch.py +326 -0
  348. chrys/app/tui/util/invocation_progress.py +57 -0
  349. chrys/app/tui/util/logo.py +32 -0
  350. chrys/app/tui/util/message_gate.py +35 -0
  351. chrys/app/tui/util/mutation_sources.py +14 -0
  352. chrys/app/tui/util/picker_text.py +27 -0
  353. chrys/app/tui/util/removal.py +47 -0
  354. chrys/app/tui/util/rich_style.py +36 -0
  355. chrys/app/tui/util/source_text.py +12 -0
  356. chrys/app/tui/util/static_update.py +88 -0
  357. chrys/app/tui/util/todo_format.py +51 -0
  358. chrys/app/tui/util/visibility.py +214 -0
  359. chrys/app/tui/widgets/__init__.py +60 -0
  360. chrys/app/tui/widgets/ask_user_controls.py +652 -0
  361. chrys/app/tui/widgets/ask_user_markdown.py +17 -0
  362. chrys/app/tui/widgets/ask_user_prompt.py +606 -0
  363. chrys/app/tui/widgets/buttons.py +52 -0
  364. chrys/app/tui/widgets/chat/__init__.py +3 -0
  365. chrys/app/tui/widgets/chat/agent_transcript_surface.py +818 -0
  366. chrys/app/tui/widgets/chat/chrome.py +296 -0
  367. chrys/app/tui/widgets/chat/compaction_card.py +353 -0
  368. chrys/app/tui/widgets/chat/context_fold.py +77 -0
  369. chrys/app/tui/widgets/chat/file_snapshot.py +91 -0
  370. chrys/app/tui/widgets/chat/image_preview.py +214 -0
  371. chrys/app/tui/widgets/chat/live_renderer.py +383 -0
  372. chrys/app/tui/widgets/chat/messages.py +1039 -0
  373. chrys/app/tui/widgets/chat/panel.py +1769 -0
  374. chrys/app/tui/widgets/chat/ports.py +245 -0
  375. chrys/app/tui/widgets/chat/renderers/__init__.py +7 -0
  376. chrys/app/tui/widgets/chat/renderers/ask_user.py +747 -0
  377. chrys/app/tui/widgets/chat/renderers/execute.py +431 -0
  378. chrys/app/tui/widgets/chat/renderers/file_edit.py +925 -0
  379. chrys/app/tui/widgets/chat/renderers/hosted_code.py +83 -0
  380. chrys/app/tui/widgets/chat/renderers/hosted_generic.py +330 -0
  381. chrys/app/tui/widgets/chat/renderers/hosted_image.py +107 -0
  382. chrys/app/tui/widgets/chat/renderers/hosted_mcp.py +43 -0
  383. chrys/app/tui/widgets/chat/renderers/hosted_search.py +167 -0
  384. chrys/app/tui/widgets/chat/renderers/hosted_shell.py +132 -0
  385. chrys/app/tui/widgets/chat/renderers/read_file.py +399 -0
  386. chrys/app/tui/widgets/chat/renderers/search.py +348 -0
  387. chrys/app/tui/widgets/chat/renderers/skill.py +564 -0
  388. chrys/app/tui/widgets/chat/renderers/sleep.py +287 -0
  389. chrys/app/tui/widgets/chat/renderers/sub_agent.py +1385 -0
  390. chrys/app/tui/widgets/chat/renderers/todo.py +114 -0
  391. chrys/app/tui/widgets/chat/renderers/web.py +69 -0
  392. chrys/app/tui/widgets/chat/replay.py +1605 -0
  393. chrys/app/tui/widgets/chat/replay_mount.py +322 -0
  394. chrys/app/tui/widgets/chat/scroll_controller.py +579 -0
  395. chrys/app/tui/widgets/chat/selection_controller.py +435 -0
  396. chrys/app/tui/widgets/chat/session_json.py +508 -0
  397. chrys/app/tui/widgets/chat/toc_model.py +156 -0
  398. chrys/app/tui/widgets/chat/tool_call.py +2478 -0
  399. chrys/app/tui/widgets/chat/tool_registry.py +753 -0
  400. chrys/app/tui/widgets/chat/tool_renderers.py +247 -0
  401. chrys/app/tui/widgets/chat/tool_view_builders.py +260 -0
  402. chrys/app/tui/widgets/chat/transcript_reader.py +57 -0
  403. chrys/app/tui/widgets/checkbox.py +24 -0
  404. chrys/app/tui/widgets/chrome/__init__.py +3 -0
  405. chrys/app/tui/widgets/chrome/app_header.py +283 -0
  406. chrys/app/tui/widgets/chrome/commands.py +95 -0
  407. chrys/app/tui/widgets/chrome/file_index.py +68 -0
  408. chrys/app/tui/widgets/chrome/file_scanner.py +670 -0
  409. chrys/app/tui/widgets/chrome/footer.py +284 -0
  410. chrys/app/tui/widgets/chrome/image_paste.py +351 -0
  411. chrys/app/tui/widgets/chrome/input_bar.py +1299 -0
  412. chrys/app/tui/widgets/chrome/status_bar.py +911 -0
  413. chrys/app/tui/widgets/chrome/suggestion_list.py +652 -0
  414. chrys/app/tui/widgets/click_affordance.py +24 -0
  415. chrys/app/tui/widgets/color_picker/__init__.py +254 -0
  416. chrys/app/tui/widgets/color_picker/controls.py +171 -0
  417. chrys/app/tui/widgets/color_picker/model.py +89 -0
  418. chrys/app/tui/widgets/dialog_buttons.py +58 -0
  419. chrys/app/tui/widgets/dialog_scroll.py +24 -0
  420. chrys/app/tui/widgets/diff_view/__init__.py +20 -0
  421. chrys/app/tui/widgets/diff_view/cells.py +59 -0
  422. chrys/app/tui/widgets/diff_view/code.py +280 -0
  423. chrys/app/tui/widgets/diff_view/compute.py +286 -0
  424. chrys/app/tui/widgets/diff_view/gutter.py +161 -0
  425. chrys/app/tui/widgets/diff_view/inline.py +77 -0
  426. chrys/app/tui/widgets/diff_view/palette.py +87 -0
  427. chrys/app/tui/widgets/diff_view/protocols.py +24 -0
  428. chrys/app/tui/widgets/diff_view/rows.py +138 -0
  429. chrys/app/tui/widgets/diff_view/unified.py +267 -0
  430. chrys/app/tui/widgets/diff_view/widget.py +209 -0
  431. chrys/app/tui/widgets/editor/__init__.py +79 -0
  432. chrys/app/tui/widgets/editor/coordinates.py +30 -0
  433. chrys/app/tui/widgets/editor/highlighter.py +139 -0
  434. chrys/app/tui/widgets/editor/keymaps.py +357 -0
  435. chrys/app/tui/widgets/editor/types.py +216 -0
  436. chrys/app/tui/widgets/editor/vim.py +1195 -0
  437. chrys/app/tui/widgets/editor/widget.py +401 -0
  438. chrys/app/tui/widgets/file_preview.py +58 -0
  439. chrys/app/tui/widgets/hatch.py +138 -0
  440. chrys/app/tui/widgets/input.py +66 -0
  441. chrys/app/tui/widgets/loading.py +125 -0
  442. chrys/app/tui/widgets/markdown/__init__.py +25 -0
  443. chrys/app/tui/widgets/markdown/_utils.py +21 -0
  444. chrys/app/tui/widgets/markdown/anchors.py +71 -0
  445. chrys/app/tui/widgets/markdown/blocks.py +115 -0
  446. chrys/app/tui/widgets/markdown/copyable.py +163 -0
  447. chrys/app/tui/widgets/markdown/diagram/__init__.py +35 -0
  448. chrys/app/tui/widgets/markdown/diagram/canvas.py +409 -0
  449. chrys/app/tui/widgets/markdown/diagram/charts.py +52 -0
  450. chrys/app/tui/widgets/markdown/diagram/diagnostics.py +101 -0
  451. chrys/app/tui/widgets/markdown/diagram/geometry.py +46 -0
  452. chrys/app/tui/widgets/markdown/diagram/layout.py +494 -0
  453. chrys/app/tui/widgets/markdown/diagram/messages.py +18 -0
  454. chrys/app/tui/widgets/markdown/diagram/model.py +275 -0
  455. chrys/app/tui/widgets/markdown/diagram/parser.py +173 -0
  456. chrys/app/tui/widgets/markdown/diagram/parsers/__init__.py +3 -0
  457. chrys/app/tui/widgets/markdown/diagram/parsers/charts.py +349 -0
  458. chrys/app/tui/widgets/markdown/diagram/parsers/common.py +403 -0
  459. chrys/app/tui/widgets/markdown/diagram/parsers/graphs.py +1072 -0
  460. chrys/app/tui/widgets/markdown/diagram/parsers/planning.py +317 -0
  461. chrys/app/tui/widgets/markdown/diagram/parsers/schedule.py +298 -0
  462. chrys/app/tui/widgets/markdown/diagram/parsers/schemas.py +241 -0
  463. chrys/app/tui/widgets/markdown/diagram/parsers/structure.py +221 -0
  464. chrys/app/tui/widgets/markdown/diagram/presentation.py +32 -0
  465. chrys/app/tui/widgets/markdown/diagram/renderers/__init__.py +3 -0
  466. chrys/app/tui/widgets/markdown/diagram/renderers/charts.py +275 -0
  467. chrys/app/tui/widgets/markdown/diagram/renderers/common.py +73 -0
  468. chrys/app/tui/widgets/markdown/diagram/renderers/planning.py +157 -0
  469. chrys/app/tui/widgets/markdown/diagram/renderers/schedule.py +202 -0
  470. chrys/app/tui/widgets/markdown/diagram/renderers/structure.py +77 -0
  471. chrys/app/tui/widgets/markdown/diagram/renderers/xy.py +385 -0
  472. chrys/app/tui/widgets/markdown/diagram/router.py +651 -0
  473. chrys/app/tui/widgets/markdown/diagram/specs/__init__.py +3 -0
  474. chrys/app/tui/widgets/markdown/diagram/specs/charts.py +94 -0
  475. chrys/app/tui/widgets/markdown/diagram/specs/planning.py +68 -0
  476. chrys/app/tui/widgets/markdown/diagram/specs/schedule.py +65 -0
  477. chrys/app/tui/widgets/markdown/diagram/specs/structure.py +23 -0
  478. chrys/app/tui/widgets/markdown/diagram/viewer.py +92 -0
  479. chrys/app/tui/widgets/markdown/links.py +47 -0
  480. chrys/app/tui/widgets/markdown/math/__init__.py +36 -0
  481. chrys/app/tui/widgets/markdown/math/layout.py +739 -0
  482. chrys/app/tui/widgets/markdown/math/markdown.py +291 -0
  483. chrys/app/tui/widgets/markdown/math/parser.py +325 -0
  484. chrys/app/tui/widgets/markdown/math/symbols.py +325 -0
  485. chrys/app/tui/widgets/markdown/navigator.py +80 -0
  486. chrys/app/tui/widgets/markdown/parser.py +1057 -0
  487. chrys/app/tui/widgets/markdown/stream.py +73 -0
  488. chrys/app/tui/widgets/markdown/toc.py +98 -0
  489. chrys/app/tui/widgets/markdown/viewer.py +146 -0
  490. chrys/app/tui/widgets/markdown/widget.py +2046 -0
  491. chrys/app/tui/widgets/marquee.py +101 -0
  492. chrys/app/tui/widgets/option_menu.py +82 -0
  493. chrys/app/tui/widgets/page_navigator.py +118 -0
  494. chrys/app/tui/widgets/select.py +196 -0
  495. chrys/app/tui/widgets/selection.py +59 -0
  496. chrys/app/tui/widgets/sidebar/__init__.py +3 -0
  497. chrys/app/tui/widgets/sidebar/buddy.py +367 -0
  498. chrys/app/tui/widgets/sidebar/context.py +482 -0
  499. chrys/app/tui/widgets/sidebar/debug.py +195 -0
  500. chrys/app/tui/widgets/sidebar/empty_state.py +11 -0
  501. chrys/app/tui/widgets/sidebar/panel.py +198 -0
  502. chrys/app/tui/widgets/sidebar/tasks.py +198 -0
  503. chrys/app/tui/widgets/sidebar/toc.py +166 -0
  504. chrys/app/tui/widgets/sidebar/toc_types.py +30 -0
  505. chrys/app/tui/widgets/syntax_theme.py +85 -0
  506. chrys/app/tui/widgets/text_area.py +258 -0
  507. chrys/app/tui/widgets/trajectory/__init__.py +8 -0
  508. chrys/app/tui/widgets/trajectory/chartkit.py +264 -0
  509. chrys/app/tui/widgets/trajectory/insights.py +1266 -0
  510. chrys/app/tui/widgets/trajectory/overview.py +1101 -0
  511. chrys/app/tui/widgets/trajectory/panel.py +983 -0
  512. chrys/app/tui/widgets/trajectory/presentation.py +311 -0
  513. chrys/app/tui/widgets/trajectory/session_info.py +124 -0
  514. chrys/app/tui/widgets/trajectory/text_view.py +124 -0
  515. chrys/app/tui/widgets/trajectory/timeline.py +380 -0
  516. chrys/app/tui/widgets/welcome.py +126 -0
  517. chrys/app/tui/widgets/workflow/__init__.py +5 -0
  518. chrys/app/tui/widgets/workflow/edges.py +89 -0
  519. chrys/app/tui/widgets/workflow/graph.py +869 -0
  520. chrys/app/tui/widgets/workflow/info.py +310 -0
  521. chrys/app/tui/widgets/workflow/node_view.py +27 -0
  522. chrys/app/tui/widgets/workflow/output.py +225 -0
  523. chrys/app/tui/widgets/workflow/panel.py +658 -0
  524. chrys/app/tui/widgets/workflow/projector.py +324 -0
  525. chrys/app/tui/widgets/workflow/records.py +69 -0
  526. chrys/app/tui/widgets/workflow/scrollbar.py +82 -0
  527. chrys/app/tui/widgets/workflow/selection.py +151 -0
  528. chrys/app/tui/widgets/workflow/source.py +65 -0
  529. chrys/app/tui/widgets/workflow/text.py +257 -0
  530. chrys/app/tui/widgets/workflow/values.py +317 -0
  531. chrys/foundation/__init__.py +0 -0
  532. chrys/foundation/branding.py +15 -0
  533. chrys/foundation/config/__init__.py +7 -0
  534. chrys/foundation/config/coercion.py +374 -0
  535. chrys/foundation/config/context.py +34 -0
  536. chrys/foundation/config/env_file.py +141 -0
  537. chrys/foundation/config/env_layers.py +370 -0
  538. chrys/foundation/config/migrations.py +521 -0
  539. chrys/foundation/config/notification_events.py +22 -0
  540. chrys/foundation/config/process_settings.py +332 -0
  541. chrys/foundation/config/runtime_pointer.py +133 -0
  542. chrys/foundation/config/settings.py +1952 -0
  543. chrys/foundation/config/settings_store.py +996 -0
  544. chrys/foundation/config/spec.py +396 -0
  545. chrys/foundation/config/user_settings.py +140 -0
  546. chrys/foundation/config/warnings.py +262 -0
  547. chrys/foundation/config/web_values.py +135 -0
  548. chrys/foundation/config/yaml_store.py +487 -0
  549. chrys/foundation/errors/__init__.py +68 -0
  550. chrys/foundation/errors/_legacy.py +117 -0
  551. chrys/foundation/errors/_text.py +13 -0
  552. chrys/foundation/errors/_walk.py +85 -0
  553. chrys/foundation/errors/classify.py +278 -0
  554. chrys/foundation/errors/display.py +274 -0
  555. chrys/foundation/errors/formatting.py +283 -0
  556. chrys/foundation/errors/kinds.py +67 -0
  557. chrys/foundation/errors/network.py +497 -0
  558. chrys/foundation/errors/provider.py +445 -0
  559. chrys/foundation/errors/route.py +85 -0
  560. chrys/foundation/events/__init__.py +8 -0
  561. chrys/foundation/events/bus.py +143 -0
  562. chrys/foundation/events/types.py +1629 -0
  563. chrys/foundation/events/workflow.py +38 -0
  564. chrys/foundation/hosted_tools.py +142 -0
  565. chrys/foundation/i18n/__init__.py +32 -0
  566. chrys/foundation/i18n/_catalogs/zh-Hans/LC_MESSAGES/chrys.mo +0 -0
  567. chrys/foundation/i18n/catalogs.py +42 -0
  568. chrys/foundation/i18n/formatting.py +202 -0
  569. chrys/foundation/i18n/locale.py +92 -0
  570. chrys/foundation/i18n/localizer.py +187 -0
  571. chrys/foundation/i18n/messages.py +268 -0
  572. chrys/foundation/io/__init__.py +3 -0
  573. chrys/foundation/io/result_content.py +237 -0
  574. chrys/foundation/models/__init__.py +3 -0
  575. chrys/foundation/models/approval_reuse.py +23 -0
  576. chrys/foundation/models/ask_user.py +468 -0
  577. chrys/foundation/models/execution.py +18 -0
  578. chrys/foundation/models/history_markers.py +115 -0
  579. chrys/foundation/models/invocations.py +63 -0
  580. chrys/foundation/models/mutation_scope.py +28 -0
  581. chrys/foundation/models/session_env.py +87 -0
  582. chrys/foundation/models/session_surface.py +29 -0
  583. chrys/foundation/models/todos.py +74 -0
  584. chrys/foundation/models/turns.py +125 -0
  585. chrys/foundation/models/workflow_session.py +89 -0
  586. chrys/foundation/models/workspace.py +59 -0
  587. chrys/foundation/net/__init__.py +3 -0
  588. chrys/foundation/net/address_class.py +57 -0
  589. chrys/foundation/net/route_probe.py +94 -0
  590. chrys/foundation/net/url.py +107 -0
  591. chrys/foundation/observability/__init__.py +3 -0
  592. chrys/foundation/observability/exporters.py +174 -0
  593. chrys/foundation/observability/gate.py +31 -0
  594. chrys/foundation/observability/setup.py +497 -0
  595. chrys/foundation/observability/sink.py +228 -0
  596. chrys/foundation/patches/__init__.py +19 -0
  597. chrys/foundation/patches/mdurl_cache.py +59 -0
  598. chrys/foundation/patches/patcher.py +287 -0
  599. chrys/foundation/patches/staged_members.py +205 -0
  600. chrys/foundation/patches/textual_block_border.py +105 -0
  601. chrys/foundation/patches/textual_button_ansi.py +65 -0
  602. chrys/foundation/patches/textual_button_ansi_old.txt +68 -0
  603. chrys/foundation/patches/textual_callback_cache.py +248 -0
  604. chrys/foundation/patches/textual_callback_dispatch.py +109 -0
  605. chrys/foundation/patches/textual_compositor_cjk.py +566 -0
  606. chrys/foundation/patches/textual_dispatch_cache.py +150 -0
  607. chrys/foundation/patches/textual_ime_cursor_anchor.py +92 -0
  608. chrys/foundation/patches/textual_kitty_keyboard.py +268 -0
  609. chrys/foundation/patches/textual_lru_acyclic.py +241 -0
  610. chrys/foundation/patches/textual_message_pump.py +136 -0
  611. chrys/foundation/patches/textual_node_diet.py +875 -0
  612. chrys/foundation/patches/textual_one_shot_timer.py +48 -0
  613. chrys/foundation/patches/textual_option_list.py +90 -0
  614. chrys/foundation/patches/textual_precompose.py +89 -0
  615. chrys/foundation/patches/textual_pruned_tabs.py +69 -0
  616. chrys/foundation/patches/textual_reflow_reuse.py +1753 -0
  617. chrys/foundation/patches/textual_removed_node_caches.py +86 -0
  618. chrys/foundation/patches/textual_removed_screen_callbacks.py +55 -0
  619. chrys/foundation/patches/textual_selection_extract.py +48 -0
  620. chrys/foundation/patches/textual_strip_cycles.py +119 -0
  621. chrys/foundation/patches/textual_tab_selection.py +1611 -0
  622. chrys/foundation/patches/textual_timer_skip.py +70 -0
  623. chrys/foundation/patches/textual_utf8_decoder.py +106 -0
  624. chrys/foundation/patches/textual_win_sleep.py +167 -0
  625. chrys/foundation/patches/watchdog_fsevents.py +70 -0
  626. chrys/foundation/patches/watchdog_windows.py +52 -0
  627. chrys/foundation/platform/__init__.py +556 -0
  628. chrys/foundation/platform/c_api.py +32 -0
  629. chrys/foundation/platform/child_reap.py +107 -0
  630. chrys/foundation/platform/command_line.py +95 -0
  631. chrys/foundation/platform/files.py +1470 -0
  632. chrys/foundation/platform/output_capture.py +113 -0
  633. chrys/foundation/platform/paths.py +193 -0
  634. chrys/foundation/platform/process.py +1468 -0
  635. chrys/foundation/platform/pty_output.py +19 -0
  636. chrys/foundation/platform/runtime_paths.py +281 -0
  637. chrys/foundation/platform/win_console.py +129 -0
  638. chrys/foundation/reasoning_origin.py +67 -0
  639. chrys/foundation/recovery.py +16 -0
  640. chrys/foundation/retry.py +278 -0
  641. chrys/foundation/text/__init__.py +3 -0
  642. chrys/foundation/text/encoding.py +603 -0
  643. chrys/foundation/text/images.py +304 -0
  644. chrys/foundation/text/lines.py +24 -0
  645. chrys/foundation/text/mentions.py +90 -0
  646. chrys/foundation/text/model_json.py +25 -0
  647. chrys/foundation/text/reading_order.py +20 -0
  648. chrys/foundation/text/tokenizer.py +25 -0
  649. chrys/foundation/text/tool_output.py +287 -0
  650. chrys/foundation/text/yaml_io.py +62 -0
  651. chrys/foundation/tool_call_context.py +202 -0
  652. chrys/foundation/tool_execution_stamp.py +184 -0
  653. chrys/foundation/tool_invocation_order.py +48 -0
  654. chrys/foundation/tool_kinds.py +102 -0
  655. chrys/foundation/tool_result_metadata.py +287 -0
  656. chrys/foundation/trajectory/__init__.py +10 -0
  657. chrys/foundation/trajectory/context.py +481 -0
  658. chrys/foundation/trajectory/envelope.py +571 -0
  659. chrys/foundation/trajectory/event_types.py +294 -0
  660. chrys/foundation/trajectory/finalizers.py +46 -0
  661. chrys/foundation/trajectory/fingerprint.py +62 -0
  662. chrys/foundation/trajectory/ids.py +155 -0
  663. chrys/foundation/trajectory/keys.py +99 -0
  664. chrys/foundation/trajectory/lease.py +173 -0
  665. chrys/foundation/trajectory/metadata.py +72 -0
  666. chrys/foundation/trajectory/reader.py +271 -0
  667. chrys/foundation/trajectory/recovery.py +225 -0
  668. chrys/foundation/trajectory/revisions.py +152 -0
  669. chrys/foundation/trajectory/segments.py +225 -0
  670. chrys/foundation/trajectory/tools.py +117 -0
  671. chrys/foundation/trajectory/usage.py +151 -0
  672. chrys/foundation/trajectory/writer.py +972 -0
  673. chrys/foundation/trajectory_timing.py +97 -0
  674. chrys/foundation/util/__init__.py +3 -0
  675. chrys/foundation/util/async_tasks.py +29 -0
  676. chrys/foundation/util/chrys_headers.py +23 -0
  677. chrys/foundation/util/env_templates.py +104 -0
  678. chrys/foundation/util/filenames.py +96 -0
  679. chrys/foundation/util/header_charset.py +138 -0
  680. chrys/foundation/util/httpx_helpers.py +12 -0
  681. chrys/foundation/util/lock.py +236 -0
  682. chrys/foundation/util/once_close.py +46 -0
  683. chrys/foundation/util/session_ids.py +29 -0
  684. chrys/foundation/util/shell_tokens.py +88 -0
  685. chrys/foundation/util/sub_agent_context.py +38 -0
  686. chrys/foundation/util/time.py +35 -0
  687. chrys/foundation/util/unicode_scalars.py +36 -0
  688. chrys/foundation/vendor/__init__.py +43 -0
  689. chrys/foundation/vendor/ripgrep/rg-aarch64-apple-darwin +0 -0
  690. chrys/foundation/vendor/ripgrep/rg-aarch64-pc-windows-msvc.exe +0 -0
  691. chrys/foundation/vendor/ripgrep/rg-aarch64-unknown-linux-gnu +0 -0
  692. chrys/foundation/vendor/ripgrep/rg-x86_64-apple-darwin +0 -0
  693. chrys/foundation/vendor/ripgrep/rg-x86_64-pc-windows-msvc.exe +0 -0
  694. chrys/foundation/vendor/ripgrep/rg-x86_64-unknown-linux-musl +0 -0
  695. chrys/kernel/__init__.py +256 -0
  696. chrys/kernel/_content.py +1866 -0
  697. chrys/kernel/_loop_recorder.py +631 -0
  698. chrys/kernel/_null_overlay.py +1026 -0
  699. chrys/kernel/_result_ceiling.py +216 -0
  700. chrys/kernel/_serialization.py +431 -0
  701. chrys/kernel/_tool_arg_errors.py +1629 -0
  702. chrys/kernel/_tool_execution.py +1172 -0
  703. chrys/kernel/_tool_expansion.py +26 -0
  704. chrys/kernel/_types.py +2598 -0
  705. chrys/kernel/agent.py +994 -0
  706. chrys/kernel/client.py +1084 -0
  707. chrys/kernel/compaction.py +1315 -0
  708. chrys/kernel/exceptions.py +211 -0
  709. chrys/kernel/exchanges.py +683 -0
  710. chrys/kernel/identity.py +95 -0
  711. chrys/kernel/images.py +36 -0
  712. chrys/kernel/instrumentation.py +1618 -0
  713. chrys/kernel/loop.py +2308 -0
  714. chrys/kernel/middleware.py +616 -0
  715. chrys/kernel/sessions.py +604 -0
  716. chrys/kernel/tools.py +1061 -0
  717. chrys/kernel/types.py +126 -0
  718. chrys/orchestration/__init__.py +3 -0
  719. chrys/orchestration/engine/__init__.py +3 -0
  720. chrys/orchestration/engine/assembly.py +236 -0
  721. chrys/orchestration/engine/build/__init__.py +3 -0
  722. chrys/orchestration/engine/build/builder.py +1061 -0
  723. chrys/orchestration/engine/build/construction.py +408 -0
  724. chrys/orchestration/engine/build/loaded.py +139 -0
  725. chrys/orchestration/engine/engine.py +789 -0
  726. chrys/orchestration/engine/execution.py +771 -0
  727. chrys/orchestration/engine/loader.py +865 -0
  728. chrys/orchestration/engine/rollback.py +918 -0
  729. chrys/orchestration/engine/run/__init__.py +3 -0
  730. chrys/orchestration/engine/run/active_injection.py +419 -0
  731. chrys/orchestration/engine/run/attachments.py +519 -0
  732. chrys/orchestration/engine/run/bindings.py +1095 -0
  733. chrys/orchestration/engine/run/coordinator.py +1062 -0
  734. chrys/orchestration/engine/run/finalizer.py +417 -0
  735. chrys/orchestration/engine/run/input_refs.py +60 -0
  736. chrys/orchestration/engine/run/prompt_content.py +457 -0
  737. chrys/orchestration/engine/run/resume.py +299 -0
  738. chrys/orchestration/engine/run/retry.py +948 -0
  739. chrys/orchestration/engine/run/runner.py +625 -0
  740. chrys/orchestration/engine/run/runtime_skills.py +146 -0
  741. chrys/orchestration/engine/run/sub_agent_coordination.py +126 -0
  742. chrys/orchestration/engine/run/turn_hooks.py +258 -0
  743. chrys/orchestration/engine/run/turn_state.py +149 -0
  744. chrys/orchestration/engine/run/working_dir.py +50 -0
  745. chrys/orchestration/engine/session_lifecycle.py +2013 -0
  746. chrys/orchestration/engine/state/__init__.py +3 -0
  747. chrys/orchestration/engine/state/active_session.py +183 -0
  748. chrys/orchestration/engine/state/controls.py +639 -0
  749. chrys/orchestration/engine/state/current_agent.py +26 -0
  750. chrys/orchestration/engine/state/lifecycle_permits.py +395 -0
  751. chrys/orchestration/engine/state/machine.py +193 -0
  752. chrys/orchestration/engine/state/session_writer.py +434 -0
  753. chrys/orchestration/engine/trajectory.py +714 -0
  754. chrys/orchestration/engine/usage.py +118 -0
  755. chrys/orchestration/invoker/__init__.py +3 -0
  756. chrys/orchestration/invoker/acp.py +727 -0
  757. chrys/orchestration/invoker/acp_protocol.py +1115 -0
  758. chrys/orchestration/invoker/acp_spec.py +79 -0
  759. chrys/orchestration/invoker/attempts.py +994 -0
  760. chrys/orchestration/invoker/child_compaction.py +131 -0
  761. chrys/orchestration/invoker/child_history.py +99 -0
  762. chrys/orchestration/invoker/contracts.py +253 -0
  763. chrys/orchestration/invoker/evidence.py +94 -0
  764. chrys/orchestration/invoker/kernel.py +343 -0
  765. chrys/orchestration/invoker/origin.py +79 -0
  766. chrys/orchestration/invoker/resources.py +343 -0
  767. chrys/orchestration/invoker/runtime.py +299 -0
  768. chrys/orchestration/invoker/usage.py +80 -0
  769. chrys/orchestration/session_hooks.py +117 -0
  770. chrys/orchestration/session_host.py +1023 -0
  771. chrys/orchestration/session_resources.py +95 -0
  772. chrys/orchestration/session_usage.py +219 -0
  773. chrys/orchestration/startup.py +255 -0
  774. chrys/orchestration/sub_agents/__init__.py +3 -0
  775. chrys/orchestration/sub_agents/acp_policy.py +269 -0
  776. chrys/orchestration/sub_agents/kernel_policy.py +848 -0
  777. chrys/orchestration/sub_agents/registration.py +44 -0
  778. chrys/orchestration/sub_agents/shell.py +337 -0
  779. chrys/orchestration/sub_agents/tools.py +2228 -0
  780. chrys/orchestration/workflows/__init__.py +3 -0
  781. chrys/orchestration/workflows/agent_archive.py +156 -0
  782. chrys/orchestration/workflows/agent_node.py +894 -0
  783. chrys/orchestration/workflows/agent_node_build.py +687 -0
  784. chrys/orchestration/workflows/catalog.py +332 -0
  785. chrys/orchestration/workflows/coordinator.py +771 -0
  786. chrys/orchestration/workflows/hooks.py +176 -0
  787. chrys/orchestration/workflows/preview.py +322 -0
  788. chrys/orchestration/workflows/rollback.py +151 -0
  789. chrys/orchestration/workflows/runner.py +986 -0
  790. chrys/orchestration/workflows/session.py +390 -0
  791. chrys/orchestration/workflows/settings.py +97 -0
  792. chrys/orchestration/workflows/validation.py +674 -0
  793. chrys/orchestration/workflows/worker_client.py +1002 -0
  794. chrys/service/__init__.py +3 -0
  795. chrys/service/acp_client/__init__.py +65 -0
  796. chrys/service/acp_client/_transport.py +922 -0
  797. chrys/service/acp_client/client.py +1311 -0
  798. chrys/service/acp_client/errors.py +224 -0
  799. chrys/service/acp_client/protocol.py +220 -0
  800. chrys/service/acp_client/spawn.py +239 -0
  801. chrys/service/acp_client/spec.py +100 -0
  802. chrys/service/agent_middleware/__init__.py +60 -0
  803. chrys/service/agent_middleware/_metadata_keys.py +52 -0
  804. chrys/service/agent_middleware/control/__init__.py +3 -0
  805. chrys/service/agent_middleware/control/approval.py +769 -0
  806. chrys/service/agent_middleware/control/approval_hooks.py +62 -0
  807. chrys/service/agent_middleware/control/ask_user.py +207 -0
  808. chrys/service/agent_middleware/control/interrupt.py +50 -0
  809. chrys/service/agent_middleware/control/sleep.py +118 -0
  810. chrys/service/agent_middleware/control/todo.py +117 -0
  811. chrys/service/agent_middleware/events/__init__.py +3 -0
  812. chrys/service/agent_middleware/events/hook_dispatch.py +250 -0
  813. chrys/service/agent_middleware/events/hosted_tools.py +1762 -0
  814. chrys/service/agent_middleware/events/intermediate_text.py +128 -0
  815. chrys/service/agent_middleware/events/rejection_metadata.py +50 -0
  816. chrys/service/agent_middleware/events/result_persistence.py +98 -0
  817. chrys/service/agent_middleware/events/sub_agent_events.py +951 -0
  818. chrys/service/agent_middleware/events/tool_error_text.py +22 -0
  819. chrys/service/agent_middleware/events/tool_events.py +779 -0
  820. chrys/service/agent_middleware/injection.py +510 -0
  821. chrys/service/agent_middleware/reminders/__init__.py +91 -0
  822. chrys/service/agent_middleware/reminders/archive_pointer.py +154 -0
  823. chrys/service/agent_middleware/reminders/base.py +25 -0
  824. chrys/service/agent_middleware/reminders/context_usage.py +74 -0
  825. chrys/service/agent_middleware/reminders/file_change.py +63 -0
  826. chrys/service/agent_middleware/reminders/mcp.py +39 -0
  827. chrys/service/agent_middleware/reminders/profile_switch.py +106 -0
  828. chrys/service/agent_middleware/reminders/runtime_env.py +190 -0
  829. chrys/service/agent_middleware/reminders/skills.py +44 -0
  830. chrys/service/agent_middleware/reminders/sub_agents.py +35 -0
  831. chrys/service/agent_middleware/reminders/todo.py +35 -0
  832. chrys/service/agent_middleware/reminders/turn_line.py +48 -0
  833. chrys/service/agent_middleware/response_validation.py +1380 -0
  834. chrys/service/agent_middleware/system_reminder.py +1107 -0
  835. chrys/service/agent_middleware/validators.py +396 -0
  836. chrys/service/analytics/__init__.py +124 -0
  837. chrys/service/analytics/_actions.py +896 -0
  838. chrys/service/analytics/_context_evidence.py +357 -0
  839. chrys/service/analytics/_critical_path.py +179 -0
  840. chrys/service/analytics/_facts.py +919 -0
  841. chrys/service/analytics/_insights.py +769 -0
  842. chrys/service/analytics/_metric_ops.py +83 -0
  843. chrys/service/analytics/_session_projection.py +460 -0
  844. chrys/service/analytics/_timeline.py +434 -0
  845. chrys/service/analytics/_turn_graph.py +1237 -0
  846. chrys/service/analytics/_turns.py +1377 -0
  847. chrys/service/analytics/_workflows.py +171 -0
  848. chrys/service/analytics/aggregation.py +1474 -0
  849. chrys/service/analytics/classification.py +128 -0
  850. chrys/service/analytics/export.py +803 -0
  851. chrys/service/analytics/findings.py +282 -0
  852. chrys/service/analytics/math.py +79 -0
  853. chrys/service/analytics/model.py +886 -0
  854. chrys/service/analytics/reader.py +321 -0
  855. chrys/service/approval/__init__.py +3 -0
  856. chrys/service/approval/arbitration.py +102 -0
  857. chrys/service/approval/command_identity.py +109 -0
  858. chrys/service/approval/correlation.py +57 -0
  859. chrys/service/approval/grant_store.py +108 -0
  860. chrys/service/approval/judge.py +589 -0
  861. chrys/service/approval/level.py +15 -0
  862. chrys/service/approval/policy.py +209 -0
  863. chrys/service/approval/reuse.py +223 -0
  864. chrys/service/approval/reuse_binding.py +133 -0
  865. chrys/service/approval/safety_classifier.py +536 -0
  866. chrys/service/approval/turn_context.py +49 -0
  867. chrys/service/context/__init__.py +3 -0
  868. chrys/service/context/compaction/__init__.py +38 -0
  869. chrys/service/context/compaction/breaker.py +156 -0
  870. chrys/service/context/compaction/budgets.py +93 -0
  871. chrys/service/context/compaction/calibration.py +77 -0
  872. chrys/service/context/compaction/compression.py +690 -0
  873. chrys/service/context/compaction/current_turn_drop.py +458 -0
  874. chrys/service/context/compaction/events.py +201 -0
  875. chrys/service/context/compaction/exclusions.py +261 -0
  876. chrys/service/context/compaction/groups.py +361 -0
  877. chrys/service/context/compaction/last_words.py +1996 -0
  878. chrys/service/context/compaction/last_words_state.py +752 -0
  879. chrys/service/context/compaction/scoped.py +472 -0
  880. chrys/service/context/compaction/spill.py +1947 -0
  881. chrys/service/context/compaction/strategy.py +1116 -0
  882. chrys/service/context/compaction/summaries.py +319 -0
  883. chrys/service/context/compaction/turns.py +375 -0
  884. chrys/service/context/manager.py +174 -0
  885. chrys/service/context/memory_loader.py +792 -0
  886. chrys/service/context/middleware/__init__.py +3 -0
  887. chrys/service/context/middleware/usage.py +332 -0
  888. chrys/service/context/providers/__init__.py +3 -0
  889. chrys/service/context/providers/context_management.py +432 -0
  890. chrys/service/context/providers/history.py +647 -0
  891. chrys/service/context/providers/memory.py +50 -0
  892. chrys/service/context/tool_content.py +22 -0
  893. chrys/service/hooks/__init__.py +37 -0
  894. chrys/service/hooks/detached_worker.py +118 -0
  895. chrys/service/hooks/docs/README.md +14 -0
  896. chrys/service/hooks/docs/authoring.md +684 -0
  897. chrys/service/hooks/docs/configuration.md +499 -0
  898. chrys/service/hooks/docs/design.md +359 -0
  899. chrys/service/hooks/docs/project-hooks.md +348 -0
  900. chrys/service/hooks/events.py +157 -0
  901. chrys/service/hooks/loader.py +665 -0
  902. chrys/service/hooks/manager.py +807 -0
  903. chrys/service/hooks/matcher.py +111 -0
  904. chrys/service/hooks/outbox.py +294 -0
  905. chrys/service/hooks/runner.py +572 -0
  906. chrys/service/hooks/schema.py +407 -0
  907. chrys/service/llm/__init__.py +7 -0
  908. chrys/service/llm/_structured_outputs.py +192 -0
  909. chrys/service/llm/anthropic_messages/__init__.py +9 -0
  910. chrys/service/llm/anthropic_messages/client.py +254 -0
  911. chrys/service/llm/anthropic_messages/decode.py +331 -0
  912. chrys/service/llm/anthropic_messages/history.py +279 -0
  913. chrys/service/llm/anthropic_messages/request.py +323 -0
  914. chrys/service/llm/anthropic_messages/server_tools.py +319 -0
  915. chrys/service/llm/anthropic_messages/stream.py +297 -0
  916. chrys/service/llm/anthropic_messages/thinking_binding.py +156 -0
  917. chrys/service/llm/chat_completions/__init__.py +9 -0
  918. chrys/service/llm/chat_completions/client.py +369 -0
  919. chrys/service/llm/chat_completions/decode.py +338 -0
  920. chrys/service/llm/chat_completions/history.py +657 -0
  921. chrys/service/llm/chat_completions/reasoning.py +244 -0
  922. chrys/service/llm/chat_completions/request.py +192 -0
  923. chrys/service/llm/chat_completions/stream.py +546 -0
  924. chrys/service/llm/chat_completions/validation.py +308 -0
  925. chrys/service/llm/clients.py +613 -0
  926. chrys/service/llm/images.py +65 -0
  927. chrys/service/llm/mock.py +413 -0
  928. chrys/service/llm/observer.py +446 -0
  929. chrys/service/llm/one_shot.py +106 -0
  930. chrys/service/llm/openai_exceptions.py +93 -0
  931. chrys/service/llm/openai_responses/__init__.py +10 -0
  932. chrys/service/llm/openai_responses/client.py +323 -0
  933. chrys/service/llm/openai_responses/decode.py +432 -0
  934. chrys/service/llm/openai_responses/history.py +519 -0
  935. chrys/service/llm/openai_responses/hosted.py +625 -0
  936. chrys/service/llm/openai_responses/replay.py +629 -0
  937. chrys/service/llm/openai_responses/request.py +342 -0
  938. chrys/service/llm/openai_responses/stream.py +847 -0
  939. chrys/service/llm/openai_timestamps.py +57 -0
  940. chrys/service/llm/providers.py +107 -0
  941. chrys/service/llm/proxy_route.py +54 -0
  942. chrys/service/llm/raw_http_log.py +309 -0
  943. chrys/service/llm/route_facts.py +63 -0
  944. chrys/service/llm/route_sessions.py +49 -0
  945. chrys/service/llm/sdk_anthropic.py +18 -0
  946. chrys/service/llm/sdk_headers.py +47 -0
  947. chrys/service/llm/sdk_openai.py +18 -0
  948. chrys/service/llm/sdk_retry.py +59 -0
  949. chrys/service/llm/wire_client.py +246 -0
  950. chrys/service/mcp/__init__.py +3 -0
  951. chrys/service/mcp/_connection.py +235 -0
  952. chrys/service/mcp/_http_transport.py +350 -0
  953. chrys/service/mcp/_progressive.py +459 -0
  954. chrys/service/mcp/_stdio_transport.py +614 -0
  955. chrys/service/mcp/_tool_mixins.py +96 -0
  956. chrys/service/mcp/adapter.py +1031 -0
  957. chrys/service/mcp/cache.py +476 -0
  958. chrys/service/mcp/content.py +32 -0
  959. chrys/service/mcp/errors.py +169 -0
  960. chrys/service/mcp/owned.py +1330 -0
  961. chrys/service/mcp/result_limits.py +168 -0
  962. chrys/service/mcp/thinking_warning.py +95 -0
  963. chrys/service/mcp/validation.py +74 -0
  964. chrys/service/mutations/__init__.py +84 -0
  965. chrys/service/mutations/coordination.py +894 -0
  966. chrys/service/mutations/detector.py +458 -0
  967. chrys/service/mutations/git_calibrator.py +397 -0
  968. chrys/service/mutations/git_state.py +647 -0
  969. chrys/service/mutations/pipeline.py +779 -0
  970. chrys/service/mutations/scanner.py +466 -0
  971. chrys/service/mutations/snapshot_files.py +233 -0
  972. chrys/service/mutations/store.py +625 -0
  973. chrys/service/mutations/tool_names.py +15 -0
  974. chrys/service/mutations/trace.py +246 -0
  975. chrys/service/mutations/tracker.py +1289 -0
  976. chrys/service/mutations/types.py +559 -0
  977. chrys/service/mutations/workspace_changes.py +1583 -0
  978. chrys/service/profiles/__init__.py +2 -0
  979. chrys/service/profiles/agents/__init__.py +3 -0
  980. chrys/service/profiles/agents/builtins/Code.yaml +199 -0
  981. chrys/service/profiles/agents/builtins/Explore.yaml +62 -0
  982. chrys/service/profiles/agents/builtins/General.yaml +114 -0
  983. chrys/service/profiles/agents/builtins/QA.yaml +128 -0
  984. chrys/service/profiles/agents/loader.py +808 -0
  985. chrys/service/profiles/agents/registry.py +727 -0
  986. chrys/service/profiles/agents/schema.py +371 -0
  987. chrys/service/profiles/agents/serializer.py +196 -0
  988. chrys/service/profiles/coercion.py +22 -0
  989. chrys/service/profiles/models/__init__.py +3 -0
  990. chrys/service/profiles/models/env_bridge.py +157 -0
  991. chrys/service/profiles/models/loader.py +282 -0
  992. chrys/service/profiles/models/options.py +438 -0
  993. chrys/service/profiles/models/registry.py +104 -0
  994. chrys/service/profiles/models/resolver.py +286 -0
  995. chrys/service/profiles/models/schema.py +91 -0
  996. chrys/service/profiles/models/serializer.py +76 -0
  997. chrys/service/session/__init__.py +3 -0
  998. chrys/service/session/checkpoint.py +156 -0
  999. chrys/service/session/history.py +1616 -0
  1000. chrys/service/session/message_metadata.py +151 -0
  1001. chrys/service/session/persistence.py +435 -0
  1002. chrys/service/session/runtime_metadata.py +210 -0
  1003. chrys/service/session/sub_agent_logs.py +1069 -0
  1004. chrys/service/session/sub_agent_transcript.py +654 -0
  1005. chrys/service/skills/__init__.py +3 -0
  1006. chrys/service/skills/adapter.py +408 -0
  1007. chrys/service/skills/constants.py +8 -0
  1008. chrys/service/skills/loader.py +575 -0
  1009. chrys/service/skills/model.py +130 -0
  1010. chrys/service/skills/provider.py +841 -0
  1011. chrys/service/skills/runner.py +390 -0
  1012. chrys/service/state/__init__.py +3 -0
  1013. chrys/service/state/_fork.py +710 -0
  1014. chrys/service/state/_session_files.py +175 -0
  1015. chrys/service/state/_session_meta.py +552 -0
  1016. chrys/service/state/locks.py +154 -0
  1017. chrys/service/state/serializers.py +156 -0
  1018. chrys/service/state/session_catalog.py +361 -0
  1019. chrys/service/state/session_listing.py +84 -0
  1020. chrys/service/state/session_migration.py +446 -0
  1021. chrys/service/state/session_mru.py +315 -0
  1022. chrys/service/state/store.py +2132 -0
  1023. chrys/service/state/workflow.py +158 -0
  1024. chrys/service/text_blocks.py +47 -0
  1025. chrys/service/todos/__init__.py +3 -0
  1026. chrys/service/todos/reminder.py +31 -0
  1027. chrys/service/todos/tracker.py +58 -0
  1028. chrys/service/tools/__init__.py +3 -0
  1029. chrys/service/tools/approval_targets.py +92 -0
  1030. chrys/service/tools/builtins/__init__.py +3 -0
  1031. chrys/service/tools/builtins/ask_user.py +326 -0
  1032. chrys/service/tools/builtins/doc_converter/__init__.py +494 -0
  1033. chrys/service/tools/builtins/doc_converter/artifacts.py +392 -0
  1034. chrys/service/tools/builtins/doc_converter/parsers/__init__.py +3 -0
  1035. chrys/service/tools/builtins/doc_converter/parsers/_table.py +34 -0
  1036. chrys/service/tools/builtins/doc_converter/parsers/base.py +101 -0
  1037. chrys/service/tools/builtins/doc_converter/parsers/docx.py +88 -0
  1038. chrys/service/tools/builtins/doc_converter/parsers/pdf.py +144 -0
  1039. chrys/service/tools/builtins/doc_converter/parsers/pptx.py +103 -0
  1040. chrys/service/tools/builtins/doc_converter/parsers/xls.py +32 -0
  1041. chrys/service/tools/builtins/doc_converter/parsers/xlsx.py +51 -0
  1042. chrys/service/tools/builtins/doc_converter/registry.py +55 -0
  1043. chrys/service/tools/builtins/filesystem.py +1071 -0
  1044. chrys/service/tools/builtins/search.py +1265 -0
  1045. chrys/service/tools/builtins/shell.py +967 -0
  1046. chrys/service/tools/builtins/shell_command_filtering/__init__.py +5 -0
  1047. chrys/service/tools/builtins/shell_command_filtering/policy.py +60 -0
  1048. chrys/service/tools/builtins/shell_command_filtering/presets.py +152 -0
  1049. chrys/service/tools/builtins/shell_command_filtering/readonly/__init__.py +358 -0
  1050. chrys/service/tools/builtins/shell_command_filtering/readonly/commands.py +416 -0
  1051. chrys/service/tools/builtins/shell_command_filtering/readonly/git.py +408 -0
  1052. chrys/service/tools/builtins/shell_command_filtering/readonly/syntax.py +87 -0
  1053. chrys/service/tools/builtins/shell_command_filtering/tokenizer.py +214 -0
  1054. chrys/service/tools/builtins/shell_filter.py +24 -0
  1055. chrys/service/tools/builtins/sleep.py +91 -0
  1056. chrys/service/tools/builtins/todo.py +64 -0
  1057. chrys/service/tools/builtins/web/__init__.py +6 -0
  1058. chrys/service/tools/builtins/web/build.py +268 -0
  1059. chrys/service/tools/builtins/web/config.py +338 -0
  1060. chrys/service/tools/builtins/web/credentials.py +37 -0
  1061. chrys/service/tools/builtins/web/fetch/__init__.py +3 -0
  1062. chrys/service/tools/builtins/web/fetch/cache.py +68 -0
  1063. chrys/service/tools/builtins/web/fetch/convert.py +56 -0
  1064. chrys/service/tools/builtins/web/fetch/tool.py +223 -0
  1065. chrys/service/tools/builtins/web/hosted.py +61 -0
  1066. chrys/service/tools/builtins/web/http.py +330 -0
  1067. chrys/service/tools/builtins/web/progress.py +18 -0
  1068. chrys/service/tools/builtins/web/search/__init__.py +3 -0
  1069. chrys/service/tools/builtins/web/search/exa_mcp.py +146 -0
  1070. chrys/service/tools/builtins/web/search/html.py +152 -0
  1071. chrys/service/tools/builtins/web/search/json_api.py +227 -0
  1072. chrys/service/tools/builtins/web/search/providers.py +77 -0
  1073. chrys/service/tools/builtins/web/search/tool.py +218 -0
  1074. chrys/service/tools/builtins/web/search/types.py +28 -0
  1075. chrys/service/tools/builtins/web/tls_identity.py +66 -0
  1076. chrys/service/tools/kinds.py +115 -0
  1077. chrys/service/tools/names.py +47 -0
  1078. chrys/service/tools/registry.py +242 -0
  1079. chrys/service/tools/result_metadata.py +103 -0
  1080. chrys/service/tools/session_artifacts.py +196 -0
  1081. chrys/service/tools/spill.py +218 -0
  1082. chrys/service/tools/workspace_paths.py +81 -0
  1083. chrys/service/trajectory/__init__.py +11 -0
  1084. chrys/service/trajectory/approvals.py +201 -0
  1085. chrys/service/trajectory/compaction.py +420 -0
  1086. chrys/service/trajectory/fork.py +75 -0
  1087. chrys/service/trajectory/hooks.py +217 -0
  1088. chrys/service/trajectory/items.py +41 -0
  1089. chrys/service/trajectory/preparation.py +374 -0
  1090. chrys/service/trajectory/retries.py +95 -0
  1091. chrys/service/trajectory/revisions.py +129 -0
  1092. chrys/service/trajectory/segmented.py +163 -0
  1093. chrys/service/trajectory/session.py +762 -0
  1094. chrys/service/trajectory/state.py +83 -0
  1095. chrys/service/trajectory/sub_agents.py +169 -0
  1096. chrys/service/trajectory/tombstone.py +262 -0
  1097. chrys/service/trajectory/tools.py +323 -0
  1098. chrys/service/trajectory/validation.py +130 -0
  1099. chrys/service/trajectory/waits.py +161 -0
  1100. chrys/service/trajectory/workflow.py +148 -0
  1101. chrys/service/vision.py +128 -0
  1102. chrys/service/workflows/__init__.py +3 -0
  1103. chrys/service/workflows/admission.py +151 -0
  1104. chrys/service/workflows/artifacts.py +109 -0
  1105. chrys/service/workflows/asks.py +130 -0
  1106. chrys/service/workflows/builtins/__init__.py +12 -0
  1107. chrys/service/workflows/builtins/demo-workflow.manifest.json +635 -0
  1108. chrys/service/workflows/builtins/demo-workflow.py +756 -0
  1109. chrys/service/workflows/discovery.py +554 -0
  1110. chrys/service/workflows/environment.py +284 -0
  1111. chrys/service/workflows/graph.py +430 -0
  1112. chrys/service/workflows/history.py +82 -0
  1113. chrys/service/workflows/interpreter.py +107 -0
  1114. chrys/service/workflows/journal.py +334 -0
  1115. chrys/service/workflows/layout.py +56 -0
  1116. chrys/service/workflows/ledger.py +149 -0
  1117. chrys/service/workflows/model_selection.py +18 -0
  1118. chrys/service/workflows/orphans.py +209 -0
  1119. chrys/service/workflows/outcomes.py +46 -0
  1120. chrys/service/workflows/protocol.py +210 -0
  1121. chrys/service/workflows/records.py +142 -0
  1122. chrys/service/workflows/scheduler.py +770 -0
  1123. chrys/service/workflows/sdk/__init__.py +51 -0
  1124. chrys/service/workflows/sdk/_ask.py +167 -0
  1125. chrys/service/workflows/sdk/_builder.py +781 -0
  1126. chrys/service/workflows/sdk/_values.py +92 -0
  1127. chrys/service/workflows/sdk_artifact.py +74 -0
  1128. chrys/service/workflows/store.py +494 -0
  1129. chrys/service/workflows/transcript.py +102 -0
  1130. chrys/service/workflows/values.py +73 -0
  1131. chrys/service/workflows/worker_host.py +1820 -0
  1132. chrys/workflows.py +38 -0
  1133. icode_tui-0.29.1.dist-info/METADATA +104 -0
  1134. icode_tui-0.29.1.dist-info/RECORD +1138 -0
  1135. icode_tui-0.29.1.dist-info/WHEEL +4 -0
  1136. icode_tui-0.29.1.dist-info/entry_points.txt +2 -0
  1137. icode_tui-0.29.1.dist-info/licenses/LICENSE +202 -0
  1138. icode_tui-0.29.1.dist-info/licenses/NOTICE +1955 -0
@@ -0,0 +1,2014 @@
1
+ # Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
2
+
3
+ """Backend event handlers — routes EventBus callbacks to TUI widgets."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import contextlib
8
+ import json
9
+ import logging
10
+ from collections.abc import Callable
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+ from typing import TYPE_CHECKING
14
+
15
+ from chrys.app.tui.i18n import render_str
16
+ from chrys.app.tui.notifications import NotificationEvent
17
+ from chrys.app.tui.screens.main.dialog_controllers import (
18
+ AgentLoadDialogController,
19
+ ApprovalDialogHandle,
20
+ ApprovalQueueController,
21
+ ApprovalResponseWorker,
22
+ ImageCompressionDialogController,
23
+ QuestionDialogHandle,
24
+ QuestionQueueController,
25
+ )
26
+ from chrys.app.tui.screens.main.dialog_gateway import UiGateway, UiGatewayCallbacks
27
+ from chrys.app.tui.screens.main.diff_controller import LiveDiffTracker
28
+ from chrys.app.tui.screens.main.main_screen_presenter import MainScreenPresenter
29
+ from chrys.app.tui.screens.main.ports import (
30
+ BackendEventView,
31
+ InputRestore,
32
+ NotificationSeverity,
33
+ StatusMessage,
34
+ StatusTrail,
35
+ )
36
+ from chrys.app.tui.screens.main.runtime_info import RegistryRuntimeInfoProvider
37
+ from chrys.app.tui.screens.main.state import MainScreenServices, MainScreenState
38
+ from chrys.app.tui.support.gc_freeze import (
39
+ GcAbsorbReason,
40
+ GcAbsorbRequested,
41
+ GcReclaimReason,
42
+ GcReclaimRequested,
43
+ )
44
+ from chrys.app.tui.support.workspace_mru import schedule_workspace_mru_touches
45
+ from chrys.app.tui.util.context_pressure import context_pressure_message
46
+ from chrys.app.tui.widgets import PromptDraft
47
+ from chrys.app.tui.widgets.chat.file_snapshot import FileSnapshotPayload, FileSnapshotRef, should_externalize_snapshot
48
+ from chrys.app.tui.widgets.chrome.app_header import APPROVAL_MODE_MESSAGES
49
+ from chrys.app.tui.widgets.chrome.input_bar import INPUT_RETRY
50
+ from chrys.app.tui.widgets.chrome.status_bar import (
51
+ STATUS_COMPACTING,
52
+ STATUS_ERROR,
53
+ STATUS_STREAMING,
54
+ STATUS_THINKING,
55
+ )
56
+ from chrys.app.tui.widgets.sidebar.context import ContextUsageState
57
+ from chrys.foundation.errors.display import DISPLAY_WITH_HINT
58
+ from chrys.foundation.events.types import (
59
+ AgentLoadFailed,
60
+ AgentLoadFinished,
61
+ AgentLoadProgress,
62
+ AgentLoadStarted,
63
+ AgentRuntimeDetails,
64
+ AgentRuntimeUpdated,
65
+ AgentThinking,
66
+ ApprovalAutoFulfillBlocked,
67
+ ApprovalCancelled,
68
+ ApprovalModeUpdated,
69
+ ApprovalRequest,
70
+ ApprovalReviewed,
71
+ AskUserTimedOut,
72
+ CompactionFinished,
73
+ CompactionStarted,
74
+ ContextCompressed,
75
+ Error,
76
+ ImageAttachmentCompressionFinished,
77
+ ImageAttachmentCompressionStarted,
78
+ InvocationAborted,
79
+ InvocationCascadeAborted,
80
+ InvocationCompactionCommitted,
81
+ InvocationCompactionFinished,
82
+ InvocationCompactionStarted,
83
+ InvocationContextPressure,
84
+ InvocationMessage,
85
+ InvocationPaused,
86
+ InvocationPresentationAttemptAccepted,
87
+ InvocationPresentationAttemptRejected,
88
+ InvocationProgress,
89
+ InvocationResumed,
90
+ InvocationRetryAttempt,
91
+ InvocationStarted,
92
+ InvocationToolCallArgsUpdated,
93
+ InvocationToolCallProgress,
94
+ InvocationToolCallResult,
95
+ InvocationToolCallStart,
96
+ InvocationToolCallStatusUpdated,
97
+ QuestionToUser,
98
+ SessionReady,
99
+ SettingsReloaded,
100
+ TodoListUpdated,
101
+ ToolCompacted,
102
+ UsageUpdate,
103
+ UserInjectResult,
104
+ Warning,
105
+ )
106
+ from chrys.foundation.hosted_tools import HostedToolStatus, normalize_hosted_tool_status
107
+ from chrys.foundation.i18n import DisplayBlock, MessageRef, msg
108
+ from chrys.foundation.i18n.formatting import format_message
109
+ from chrys.foundation.models.approval_reuse import ReuseChoice
110
+ from chrys.foundation.models.ask_user import AskUserAnswer
111
+ from chrys.foundation.tool_result_metadata import canonical_tool_result_status
112
+ from chrys.orchestration.engine.run.attachments import replace_image_mentions_with_paths
113
+ from chrys.service.approval.policy import ApprovalMode
114
+ from chrys.service.mutations.types import FileHashDiff
115
+ from chrys.service.profiles.models.schema import DEFAULT_MAX_CONTEXT_TOKENS
116
+ from chrys.service.state.store import StateStore
117
+
118
+ if TYPE_CHECKING:
119
+ from chrys.app.tui.i18n import LocaleController
120
+ from chrys.app.tui.screens.main.workspace_actions import MissingDirReason
121
+ from chrys.foundation.models.invocations import InvocationOrigin
122
+
123
+ _IMAGE_ERROR_CODES = {"image_attachment_error", "vision_unsupported"}
124
+ _IMAGE_WARNING_CODES = {
125
+ "image_attachment_error",
126
+ "image_attachment_retry_unsupported",
127
+ "image_attachment_while_running",
128
+ }
129
+ _SUBMIT_BLOCKING_WARNING_CODES = _IMAGE_WARNING_CODES | {"sub_agent_paused"}
130
+ _WORKING_DIR_MISSING_CODE = "working_dir_missing"
131
+ _SUBMIT_BLOCKING_ERROR_CODES = _IMAGE_ERROR_CODES | {
132
+ "hook_blocked",
133
+ "not_ready",
134
+ "prompt_admission_conflict",
135
+ "retry_missing_user_anchor",
136
+ _WORKING_DIR_MISSING_CODE,
137
+ }
138
+ _STALE_RUN_FILTERED_ERROR_CODES = {"executor_error", "retry_missing_user_anchor"}
139
+
140
+
141
+ _SOFT_AGENT_LOAD_OPERATIONS = {"switch", "settings_reload", "workspace_change", "model_switch"}
142
+
143
+ _APPROVAL_TITLE = msg("tui.approval.title", fallback="Approval")
144
+ _APPROVAL_MODE_CHANGED = msg("tui.approval.mode_changed", fallback="Approval mode: {mode}")
145
+ _WARNING_TITLE = msg("tui.warning.title", fallback="Warning")
146
+ _UNKNOWN_ERROR = msg("tui.error.unknown", fallback="Unknown error")
147
+ _RETRY_NOT_STARTED = msg("tui.retry.not_started", fallback="Retry not started")
148
+ _AGENT_FAILED_TO_LOAD = msg("tui.agent_load.failed", fallback="Agent failed to load.")
149
+ _NON_TURN_NOTIFICATION_ERROR_CODES = {
150
+ "hook_blocked",
151
+ "image_attachment_error",
152
+ "no_state_store",
153
+ "not_ready",
154
+ "session_in_use",
155
+ "vision_unsupported",
156
+ }
157
+ _SESSION_FORK_ERROR_PREFIX = "session_fork_"
158
+ _SESSION_CLEAR_FAILED_CODE = "session_clear_failed"
159
+
160
+ logger = logging.getLogger(__name__)
161
+
162
+ _FINISH_SESSION_READY = msg("tui.agent_load.finish.session_ready", fallback="Session ready: {profile}")
163
+ _SESSION_IN_USE_TITLE = msg("tui.main.session_in_use.title", fallback="Session In Use")
164
+ _SESSION_IN_USE_MESSAGE = msg(
165
+ "tui.main.session_in_use.message",
166
+ fallback="Session Already Open\n\n{message}",
167
+ multiline=True,
168
+ )
169
+ _SESSION_IN_USE_OK = msg("tui.main.session_in_use.ok", fallback="OK")
170
+ _HOSTED_WEB_TOOLS_OK = msg("tui.main.hosted_web_tools.ok", fallback="OK")
171
+ _IMAGE_NOT_ATTACHED = msg("tui.vision_unsupported.title.not_attached", fallback="Image Not Attached")
172
+
173
+
174
+ def _chat_sub_agent(origin: InvocationOrigin) -> bool:
175
+ """A sub-agent with a card in the chat transcript: a workflow node's children have none."""
176
+ return origin.kind == "sub_agent" and origin.root.kind != "workflow_node"
177
+
178
+
179
+ @dataclass(frozen=True, slots=True)
180
+ class _WarningDedupeKey:
181
+ """Session-scoped identity for one screen-lifetime warning toast."""
182
+
183
+ session_id: str | None
184
+ code: str
185
+ message: str
186
+
187
+
188
+ @dataclass(frozen=True, slots=True)
189
+ class BackendEventCallbacks:
190
+ """Screen-owned effects required by backend event handlers."""
191
+
192
+ set_agent_running: Callable[[bool], None]
193
+ set_agent_loading: Callable[[bool], None]
194
+ set_has_messages: Callable[[bool], None]
195
+ set_profile_display: Callable[[str], None]
196
+ set_active_model_profile_id: Callable[[str], None]
197
+ set_creating_new_session: Callable[[bool], None]
198
+ set_restoring_session: Callable[[bool], None]
199
+ set_workspace_cwd: Callable[[str], None]
200
+ refresh_git_branch: Callable[[], None]
201
+ update_subtitle: Callable[[], None]
202
+ update_toc: Callable[[], None]
203
+ on_session_fork_error: Callable[[Error, str, NotificationSeverity], None]
204
+ on_session_clear_error: Callable[[Error, str], None]
205
+ handle_approval_response: Callable[
206
+ [str, bool, str, dict[str, object] | None, ReuseChoice], ApprovalResponseWorker | None
207
+ ]
208
+ handle_ask_user_response: Callable[[str, tuple[AskUserAnswer, ...]], object]
209
+ question_inline_preferred: Callable[[], bool]
210
+ approval_defer_while_judging: Callable[[], bool]
211
+ post_gc_message: Callable[[GcAbsorbRequested | GcReclaimRequested], object]
212
+ debug: Callable[[str, str], None]
213
+ refresh_model_indicator: Callable[[], None]
214
+ refresh_notification_settings: Callable[[], None]
215
+ refresh_trajectory_verify_commands: Callable[[], None]
216
+ settings_reloaded: Callable[[], None]
217
+ accept_approval_update: Callable[[ApprovalModeUpdated], bool] = lambda _event: True
218
+ route_session_error: Callable[[Error], bool] = lambda _event: False
219
+ prompt_missing_working_dir: Callable[[MissingDirReason], None] = lambda _reason: None
220
+
221
+
222
+ def _usage_source_debug_suffix(source_id: str, *, main_source_id: str) -> str:
223
+ """Return a short source marker for non-main usage debug entries."""
224
+ if not source_id or source_id == main_source_id:
225
+ return ""
226
+ short_source = source_id if len(source_id) <= 12 else f"{source_id[:6]}…{source_id[-4:]}"
227
+ return f" source={short_source}"
228
+
229
+
230
+ def _metadata_hashes(value: object) -> FileHashDiff:
231
+ """Read before/after hashes from event metadata."""
232
+ if isinstance(value, FileHashDiff):
233
+ return value
234
+ if value is not None:
235
+ logger.debug("dropping unexpected file_mutation_hashes payload: %r", value)
236
+ return FileHashDiff(before=None, after=None)
237
+
238
+
239
+ def _chat_file_snapshot_payload(
240
+ snapshot: object,
241
+ hashes: FileHashDiff,
242
+ state_store: StateStore | None,
243
+ session_id: str,
244
+ ) -> FileSnapshotPayload | None:
245
+ """Return the snapshot payload retained by chat widgets for a file tool."""
246
+ if not isinstance(snapshot, tuple) or len(snapshot) != 2:
247
+ return None
248
+ before, after = snapshot
249
+ if not isinstance(before, str) or not isinstance(after, str):
250
+ return None
251
+ text_snapshot = (before, after)
252
+ if not should_externalize_snapshot(text_snapshot):
253
+ return text_snapshot
254
+ if state_store is None or not session_id or (hashes.before is None and hashes.after is None):
255
+ return text_snapshot
256
+ try:
257
+ from chrys.service.mutations.store import SnapshotStore
258
+
259
+ mutations_dir = SnapshotStore(state_store.session_dir(session_id)).mutations_dir
260
+ except Exception:
261
+ return text_snapshot
262
+ return FileSnapshotRef(mutations_dir=mutations_dir, before_hash=hashes.before, after_hash=hashes.after)
263
+
264
+
265
+ class BackendEventHandler:
266
+ """Handles all EventBus backend → TUI event routing.
267
+
268
+ Constructed once by the main-screen owner and receives a semantic host
269
+ for shared state, dialog controllers, and presenter-mediated UI.
270
+ """
271
+
272
+ def __init__(
273
+ self,
274
+ *,
275
+ state: MainScreenState,
276
+ services: MainScreenServices,
277
+ view: BackendEventView,
278
+ callbacks: BackendEventCallbacks,
279
+ runtime_info: RegistryRuntimeInfoProvider | None = None,
280
+ live_diff: LiveDiffTracker | None = None,
281
+ locale_controller: LocaleController | None = None,
282
+ ) -> None:
283
+ self._state = state
284
+ self._services = services
285
+ self._view = view
286
+ self._callbacks = callbacks
287
+ self._locale_controller = locale_controller
288
+ self._dialog_gateway = UiGateway(
289
+ view,
290
+ UiGatewayCallbacks(
291
+ debug=callbacks.debug,
292
+ handle_approval_response=callbacks.handle_approval_response,
293
+ publish_auto_fulfill_blocked=self._publish_auto_fulfill_blocked,
294
+ handle_ask_user_response=callbacks.handle_ask_user_response,
295
+ question_inline_preferred=callbacks.question_inline_preferred,
296
+ approval_defer_while_judging=callbacks.approval_defer_while_judging,
297
+ set_agent_loading=self.set_agent_loading,
298
+ ),
299
+ )
300
+ self._approval_controller = ApprovalQueueController(
301
+ self._dialog_gateway,
302
+ render_message=self._render_display,
303
+ )
304
+ self._question_controller = QuestionQueueController(self._dialog_gateway)
305
+ self._agent_load_controller = AgentLoadDialogController(self._dialog_gateway)
306
+ self._image_compression_controller = ImageCompressionDialogController(self._dialog_gateway)
307
+ self._presenter = MainScreenPresenter(view, state)
308
+ self._runtime_info = runtime_info or RegistryRuntimeInfoProvider(services)
309
+ self._live_diff = live_diff or LiveDiffTracker()
310
+ self._seen_warnings: set[_WarningDedupeKey] = set()
311
+
312
+ def _render_display(self, reference: MessageRef) -> str:
313
+ controller = self._locale_controller
314
+ return format_message(reference) if controller is None else render_str(controller.localizer, reference)
315
+
316
+ def _render_display_with_hint(self, message: MessageRef, hint: MessageRef | None) -> str:
317
+ """Render an error's display message and its hint in the current locale, joined as that locale joins them."""
318
+ rendered = self._render_display(message)
319
+ if hint is None:
320
+ return rendered
321
+ return self._render_display(DISPLAY_WITH_HINT.bind(message=rendered, hint=self._render_display(hint)))
322
+
323
+ def _gateway(self) -> UiGateway:
324
+ """Return the Textual dialog gateway."""
325
+ return self._dialog_gateway
326
+
327
+ def _ui(self) -> MainScreenPresenter:
328
+ """Return the screen presenter."""
329
+ return self._presenter
330
+
331
+ def _approval(self) -> ApprovalQueueController:
332
+ return self._approval_controller
333
+
334
+ def _questions(self) -> QuestionQueueController:
335
+ return self._question_controller
336
+
337
+ def _agent_load(self) -> AgentLoadDialogController:
338
+ return self._agent_load_controller
339
+
340
+ def _image_compression(self) -> ImageCompressionDialogController:
341
+ return self._image_compression_controller
342
+
343
+ def _predates_active_run(self, event: InvocationMessage | Error) -> bool:
344
+ """Return whether *event* was sourced before the current UI run."""
345
+ started_at = self._state.run.started_at
346
+ return self._state.run.agent_running and started_at is not None and event.timestamp < started_at
347
+
348
+ @property
349
+ def state_store(self) -> StateStore | None:
350
+ return self._services.state_store
351
+
352
+ @property
353
+ def agent_running(self) -> bool:
354
+ return self._state.run.agent_running
355
+
356
+ def set_agent_running(self, running: bool) -> None:
357
+ self._state.run.agent_running = running
358
+ self._callbacks.set_agent_running(running)
359
+
360
+ @property
361
+ def agent_loading(self) -> bool:
362
+ return self._state.run.agent_loading
363
+
364
+ def set_agent_loading(self, loading: bool) -> None:
365
+ self._state.run.agent_loading = loading
366
+ self._callbacks.set_agent_loading(loading)
367
+
368
+ @property
369
+ def has_messages(self) -> bool:
370
+ return self._state.run.has_messages
371
+
372
+ def set_has_messages(self, has_messages: bool) -> None:
373
+ self._state.run.has_messages = has_messages
374
+ self._callbacks.set_has_messages(has_messages)
375
+
376
+ @property
377
+ def profile(self) -> str:
378
+ return self._state.runtime.profile
379
+
380
+ @profile.setter
381
+ def profile(self, value: str) -> None:
382
+ self._state.runtime.profile = value
383
+ self._callbacks.set_profile_display(value)
384
+
385
+ @property
386
+ def runtime_metadata(self) -> AgentRuntimeDetails:
387
+ return self._state.runtime.details
388
+
389
+ @runtime_metadata.setter
390
+ def runtime_metadata(self, value: AgentRuntimeDetails) -> None:
391
+ self._state.runtime.details = value
392
+
393
+ @property
394
+ def approval_mode(self) -> ApprovalMode:
395
+ return self._state.runtime.approval_mode
396
+
397
+ @approval_mode.setter
398
+ def approval_mode(self, value: ApprovalMode) -> None:
399
+ self._state.runtime.approval_mode = value
400
+
401
+ @property
402
+ def main_usage_source_id(self) -> str:
403
+ return self._state.runtime.main_usage_source_id
404
+
405
+ @main_usage_source_id.setter
406
+ def main_usage_source_id(self, value: str) -> None:
407
+ self._state.runtime.main_usage_source_id = value
408
+
409
+ @property
410
+ def creating_new_session(self) -> bool:
411
+ return self._state.session.creating_new_session
412
+
413
+ @creating_new_session.setter
414
+ def creating_new_session(self, value: bool) -> None:
415
+ self._state.session.creating_new_session = value
416
+ self._callbacks.set_creating_new_session(value)
417
+
418
+ @property
419
+ def restoring_session(self) -> bool:
420
+ return self._state.session.restoring_session
421
+
422
+ @restoring_session.setter
423
+ def restoring_session(self, value: bool) -> None:
424
+ self._state.session.restoring_session = value
425
+ self._callbacks.set_restoring_session(value)
426
+
427
+ @property
428
+ def last_usage_tokens(self) -> int:
429
+ return self._state.usage.last_usage_tokens
430
+
431
+ @last_usage_tokens.setter
432
+ def last_usage_tokens(self, value: int) -> None:
433
+ self._state.usage.last_usage_tokens = value
434
+
435
+ @property
436
+ def last_total_session_tokens(self) -> int:
437
+ return self._state.usage.last_total_session_tokens
438
+
439
+ @last_total_session_tokens.setter
440
+ def last_total_session_tokens(self, value: int) -> None:
441
+ self._state.usage.last_total_session_tokens = value
442
+
443
+ @property
444
+ def context_usage_state(self) -> ContextUsageState | None:
445
+ return self._view.context_usage_state
446
+
447
+ @property
448
+ def chdir_current_cwd(self) -> str:
449
+ return self._state.workspace_marker.current_cwd
450
+
451
+ @chdir_current_cwd.setter
452
+ def chdir_current_cwd(self, value: str) -> None:
453
+ self._state.workspace.current_cwd = value
454
+ self._state.workspace_marker.current_cwd = value
455
+ self._callbacks.set_workspace_cwd(value)
456
+
457
+ @property
458
+ def chdir_original_cwd(self) -> str | None:
459
+ return self._state.workspace_marker.original_cwd
460
+
461
+ @chdir_original_cwd.setter
462
+ def chdir_original_cwd(self, value: str | None) -> None:
463
+ self._state.workspace_marker.original_cwd = value
464
+
465
+ @property
466
+ def pending_user_submit_active(self) -> bool:
467
+ return self._state.submit.active
468
+
469
+ @property
470
+ def pending_user_submit_text(self) -> str:
471
+ return self._state.submit.text
472
+
473
+ @property
474
+ def pending_user_message_render_active(self) -> bool:
475
+ return self._state.render_gate.active
476
+
477
+ def defer_agent_message(self, event: InvocationMessage) -> None:
478
+ self._state.render_gate.defer(event)
479
+
480
+ def defer_error(self, event: Error) -> None:
481
+ self._state.render_gate.defer(event)
482
+
483
+ def block_pending_user_submit(self) -> None:
484
+ self._state.submit.block()
485
+
486
+ def notify(
487
+ self,
488
+ message: StatusMessage,
489
+ *,
490
+ title: StatusMessage,
491
+ severity: NotificationSeverity = "information",
492
+ timeout: float | None = 3,
493
+ ) -> None:
494
+ self._view.notify(message, title=title, severity=severity, timeout=timeout)
495
+
496
+ def push_screen(self, screen: object, callback: object | None = None) -> object:
497
+ return self._view.push_screen(screen, callback)
498
+
499
+ def update_subtitle(self) -> None:
500
+ self._callbacks.update_subtitle()
501
+
502
+ def refresh_git_branch(self) -> None:
503
+ self._callbacks.refresh_git_branch()
504
+
505
+ def update_toc(self) -> None:
506
+ self._callbacks.update_toc()
507
+
508
+ def on_session_fork_error(self, event: Error, *, message: str, severity: NotificationSeverity) -> None:
509
+ self._callbacks.on_session_fork_error(event, message, severity)
510
+
511
+ def on_session_clear_error(self, event: Error, *, message: str) -> None:
512
+ self._callbacks.on_session_clear_error(event, message)
513
+
514
+ def debug(self, key: str, message: str = "") -> None:
515
+ self._callbacks.debug(key, message)
516
+
517
+ @property
518
+ def _approval_queue(self):
519
+ return self._approval().queue
520
+
521
+ @_approval_queue.setter
522
+ def _approval_queue(self, value) -> None:
523
+ self._approval().queue = value
524
+
525
+ @property
526
+ def _approval_request_lock(self):
527
+ return self._approval().request_lock
528
+
529
+ @_approval_request_lock.setter
530
+ def _approval_request_lock(self, value) -> None:
531
+ self._approval().request_lock = value
532
+
533
+ @property
534
+ def _approval_dialog_open(self) -> bool:
535
+ return self._approval().dialog_open
536
+
537
+ @_approval_dialog_open.setter
538
+ def _approval_dialog_open(self, value: bool) -> None:
539
+ self._approval().dialog_open = value
540
+
541
+ @property
542
+ def _approval_bodies(self) -> dict[str, object]:
543
+ return self._approval().bodies
544
+
545
+ @_approval_bodies.setter
546
+ def _approval_bodies(self, value: dict[str, object]) -> None:
547
+ self._approval().bodies = value
548
+
549
+ @property
550
+ def _open_approval_dialogs(self) -> dict[str, ApprovalDialogHandle]:
551
+ return self._approval().open_dialogs
552
+
553
+ @_open_approval_dialogs.setter
554
+ def _open_approval_dialogs(self, value: dict[str, ApprovalDialogHandle]) -> None:
555
+ self._approval().open_dialogs = value
556
+
557
+ @property
558
+ def _pending_verdicts(self) -> dict[str, ApprovalReviewed]:
559
+ return self._approval().pending_verdicts
560
+
561
+ @_pending_verdicts.setter
562
+ def _pending_verdicts(self, value: dict[str, ApprovalReviewed]) -> None:
563
+ self._approval().pending_verdicts = value
564
+
565
+ @property
566
+ def _dismissed_approval_requests(self) -> set[str]:
567
+ return self._approval().dismissed_requests
568
+
569
+ @_dismissed_approval_requests.setter
570
+ def _dismissed_approval_requests(self, value: set[str]) -> None:
571
+ self._approval().dismissed_requests = value
572
+
573
+ @property
574
+ def _reviewed_dismissed_approval_requests(self) -> set[str]:
575
+ return self._approval().reviewed_dismissed_requests
576
+
577
+ @_reviewed_dismissed_approval_requests.setter
578
+ def _reviewed_dismissed_approval_requests(self, value: set[str]) -> None:
579
+ self._approval().reviewed_dismissed_requests = value
580
+
581
+ @property
582
+ def _question_queue(self):
583
+ return self._questions().queue
584
+
585
+ @_question_queue.setter
586
+ def _question_queue(self, value) -> None:
587
+ self._questions().queue = value
588
+
589
+ @property
590
+ def _question_dialog_open(self) -> bool:
591
+ return self._questions().dialog_open
592
+
593
+ @_question_dialog_open.setter
594
+ def _question_dialog_open(self, value: bool) -> None:
595
+ self._questions().dialog_open = value
596
+
597
+ @property
598
+ def _open_question_dialogs(self) -> dict[str, QuestionDialogHandle]:
599
+ return self._questions().open_dialogs
600
+
601
+ @_open_question_dialogs.setter
602
+ def _open_question_dialogs(self, value: dict[str, QuestionDialogHandle]) -> None:
603
+ self._questions().open_dialogs = value
604
+
605
+ @property
606
+ def _inline_question_call_ids(self) -> dict[str, str]:
607
+ return self._questions().inline_call_ids
608
+
609
+ @_inline_question_call_ids.setter
610
+ def _inline_question_call_ids(self, value: dict[str, str]) -> None:
611
+ self._questions().inline_call_ids = value
612
+
613
+ @property
614
+ def _inline_question_request_ids(self) -> dict[str, str]:
615
+ return self._questions().inline_request_ids
616
+
617
+ @_inline_question_request_ids.setter
618
+ def _inline_question_request_ids(self, value: dict[str, str]) -> None:
619
+ self._questions().inline_request_ids = value
620
+
621
+ @property
622
+ def _question_drafts(self) -> dict[str, PromptDraft]:
623
+ return self._questions().drafts
624
+
625
+ @_question_drafts.setter
626
+ def _question_drafts(self, value: dict[str, PromptDraft]) -> None:
627
+ self._questions().drafts = value
628
+
629
+ @property
630
+ def _agent_load_dialog(self):
631
+ return self._agent_load().dialog
632
+
633
+ @_agent_load_dialog.setter
634
+ def _agent_load_dialog(self, value) -> None:
635
+ self._agent_load().dialog = value
636
+
637
+ @property
638
+ def _agent_load_status_snapshot(self) -> dict | None:
639
+ return self._agent_load().status_snapshot
640
+
641
+ @_agent_load_status_snapshot.setter
642
+ def _agent_load_status_snapshot(self, value: dict | None) -> None:
643
+ self._agent_load().status_snapshot = value
644
+
645
+ @property
646
+ def _image_compression_dialog(self):
647
+ return self._image_compression().dialog
648
+
649
+ @_image_compression_dialog.setter
650
+ def _image_compression_dialog(self, value) -> None:
651
+ self._image_compression().dialog = value
652
+
653
+ # -------------------------------------------------------------- #
654
+ # Helpers
655
+ # -------------------------------------------------------------- #
656
+
657
+ @staticmethod
658
+ def _extract_error_detail(raw: str) -> str:
659
+ """Extract a readable error message from an API error string.
660
+
661
+ API errors look like: ``Error code: 400 - {'error': {'message': '...'}}``
662
+ This extracts the inner ``message`` field for the chat panel while
663
+ keeping the error code prefix.
664
+ """
665
+ idx = raw.find("- {")
666
+ if idx < 0:
667
+ return raw
668
+ prefix = raw[:idx].strip()
669
+ payload = raw[idx + 2 :].strip()
670
+ try:
671
+ data = json.loads(payload.replace("'", '"'))
672
+ # Anthropic / OpenAI style: {'error': {'message': '...'}}
673
+ inner = data.get("error", data)
674
+ if isinstance(inner, dict):
675
+ detail = inner.get("message", "")
676
+ if detail:
677
+ return f"{prefix}\n{detail}"
678
+ except json.JSONDecodeError, TypeError, AttributeError:
679
+ pass
680
+ return raw
681
+
682
+ def _fallback_error_message(self, code: str) -> str:
683
+ """Return a readable fallback when an error event has no message."""
684
+ label = code.replace("_", " ").strip()
685
+ return label.capitalize() if label else self._render_display(_UNKNOWN_ERROR.bind())
686
+
687
+ def _notify(self, event: NotificationEvent) -> None:
688
+ """Send a background desktop notification through the active app service."""
689
+ self._view.notify_event(event)
690
+
691
+ async def _publish_auto_fulfill_blocked(self, event: ApprovalReviewed) -> None:
692
+ await self._services.bus.publish(
693
+ ApprovalAutoFulfillBlocked(request_id=event.request_id, session_id=event.session_id)
694
+ )
695
+
696
+ def format_tool_info(
697
+ self,
698
+ tool_names: list[str],
699
+ skill_names: list[str],
700
+ *,
701
+ memory_files: list[str] | None = None,
702
+ runtime_details: AgentRuntimeDetails | None = None,
703
+ ) -> StatusTrail:
704
+ """Build trail text for tool/skill/hook/file counts.
705
+
706
+ ``memory_files`` are auto-loaded reference file display paths from
707
+ :class:`chrys.foundation.events.types.SessionReady` / ``ProfileSwitched``.
708
+ Counted into the trail when non-empty. Detailed metadata is shown in
709
+ the runtime details modal, so the tooltip is only a click hint.
710
+ """
711
+ return self._runtime_info.format_tool_info(
712
+ tool_names,
713
+ skill_names,
714
+ memory_files=memory_files,
715
+ runtime_details=runtime_details,
716
+ )
717
+
718
+ def get_profile_description(self, profile_name: str) -> str:
719
+ """Look up a profile's description from the registry."""
720
+ return self._runtime_info.get_profile_description(profile_name)
721
+
722
+ @staticmethod
723
+ def _load_title(operation: str) -> StatusMessage:
724
+ return AgentLoadDialogController.load_title(operation)
725
+
726
+ @staticmethod
727
+ def _format_load_count(current: int, total: int, failed: int = 0) -> str:
728
+ return AgentLoadDialogController.format_load_count(current, total, failed)
729
+
730
+ async def _show_agent_load_dialog(
731
+ self,
732
+ *,
733
+ title: str,
734
+ subtitle: str,
735
+ session_id: str | None,
736
+ initial_message: str,
737
+ initial_phase: str = "",
738
+ update_clipboard_dir: bool = True,
739
+ ) -> None:
740
+ await self._agent_load().show_dialog(
741
+ title=title,
742
+ subtitle=subtitle,
743
+ session_id=session_id,
744
+ initial_message=initial_message,
745
+ initial_phase=initial_phase,
746
+ update_clipboard_dir=update_clipboard_dir,
747
+ )
748
+
749
+ async def begin_session_restore_load(self, session_id: str) -> None:
750
+ """Open the restore modal before backend session ownership checks run.
751
+
752
+ An empty *session_id* opens the modal for a pending latest-session
753
+ lookup (blank subtitle); a later call with the resolved id reuses the
754
+ same dialog and fills the subtitle in.
755
+ """
756
+ from chrys.foundation.util.session_ids import session_short_id
757
+
758
+ await self._agent_load().begin_session_restore_load(session_id, session_short_id(session_id))
759
+
760
+ def cancel_agent_load(self) -> None:
761
+ """Dismiss any active agent-load UI without showing a load result."""
762
+ self._agent_load().cancel()
763
+
764
+ # -------------------------------------------------------------- #
765
+ # EventBus callbacks
766
+ # -------------------------------------------------------------- #
767
+
768
+ async def on_agent_load_started(self, event: AgentLoadStarted) -> None:
769
+ if event.operation not in _SOFT_AGENT_LOAD_OPERATIONS:
770
+ # Fail-closed for guard/picker reads, but don't repaint the tag
771
+ # yet: that flashes the select label between the old and new
772
+ # model names on every restore/new-session load. The terminal
773
+ # confirmation event repaints on success and AgentLoadFailed
774
+ # repaints the unconfirmed action state on failure.
775
+ self._state.runtime.details_confirmed = False
776
+ await self._agent_load().on_started(event)
777
+
778
+ async def on_agent_load_progress(self, event: AgentLoadProgress) -> None:
779
+ await self._agent_load().on_progress(event)
780
+
781
+ async def on_agent_load_finished(self, event: AgentLoadFinished) -> None:
782
+ self._agent_load().on_finished(event)
783
+ self._callbacks.post_gc_message(GcReclaimRequested(GcReclaimReason.AGENT_REBUILT, prompt=False))
784
+
785
+ def update_session_history_progress(self, current: int, total: int) -> None:
786
+ """Forward mounted transcript counts to the active restore dialog."""
787
+ self._agent_load().update_session_history_progress(current, total)
788
+
789
+ def finish_agent_load(self, message: StatusMessage = "") -> None:
790
+ """Dismiss any active agent-load UI and unlock the main input."""
791
+ self._agent_load().finish(message)
792
+
793
+ async def on_agent_load_failed(self, event: AgentLoadFailed) -> None:
794
+ s = self
795
+
796
+ label = event.display_name or event.agent_profile
797
+ if label:
798
+ s.profile = label
799
+ with contextlib.suppress(Exception):
800
+ s.update_subtitle()
801
+
802
+ if event.display_message is None:
803
+ self._agent_load().on_failed(event)
804
+ else:
805
+ self._agent_load().on_failed(
806
+ event,
807
+ display=self._render_display_with_hint(event.display_message, event.display_hint),
808
+ summary=self._render_display(event.display_message),
809
+ )
810
+ self._callbacks.refresh_model_indicator()
811
+ if event.operation != "startup":
812
+ self._callbacks.post_gc_message(GcReclaimRequested(GcReclaimReason.AGENT_REBUILD_FAILED, prompt=False))
813
+
814
+ async def on_image_attachment_compression_started(self, event: ImageAttachmentCompressionStarted) -> None:
815
+ """Show a loading-only modal while oversized images are prepared."""
816
+ await self._image_compression().on_started(event)
817
+
818
+ async def on_image_attachment_compression_finished(self, event: ImageAttachmentCompressionFinished) -> None:
819
+ """Dismiss the image-compression modal after preparation finishes."""
820
+ await self._image_compression().on_finished(event)
821
+
822
+ async def on_session_ready(self, event: SessionReady) -> None:
823
+ s = self
824
+ ui = self._ui()
825
+
826
+ self._state.workspace.roots = [event.primary_cwd, *event.working_dirs]
827
+ s.main_usage_source_id = event.session_id or ""
828
+ s.profile = event.display_name or event.agent_profile
829
+ s.runtime_metadata = event.runtime_details
830
+ self._state.runtime.details_confirmed = True
831
+ if event.runtime_details.model.selection_source == "active":
832
+ self._callbacks.set_active_model_profile_id(event.runtime_details.model.profile_id)
833
+ self._callbacks.refresh_model_indicator()
834
+ s.update_subtitle()
835
+ ui.set_chat_profile(s.profile)
836
+ ui.set_chat_tool_kinds(event.tool_kinds)
837
+ desc = self.get_profile_description(event.agent_profile)
838
+ ui.set_status_profile(s.profile, description=desc)
839
+ ui.set_input_clipboard_dir(event.session_id)
840
+ trail = self.format_tool_info(
841
+ event.tool_names,
842
+ event.skill_names,
843
+ memory_files=event.memory_files,
844
+ runtime_details=event.runtime_details,
845
+ )
846
+ ui.set_tool_info(trail)
847
+ # During a restore, _on_session_restored handles the chat panel
848
+ if s.creating_new_session:
849
+ from chrys.foundation.platform import safe_getcwd
850
+
851
+ s.creating_new_session = False
852
+ s.set_has_messages(False)
853
+ s.last_usage_tokens = 0
854
+ s.last_total_session_tokens = 0
855
+ # Post-success reset point: clearing on the SessionNew publish
856
+ # instead would desync the sidebar if backend startup fails.
857
+ ui.clear_todos()
858
+ ui.set_input_retry_mode(False)
859
+ s.chdir_original_cwd = None
860
+ cwd = event.primary_cwd or safe_getcwd()
861
+ s.chdir_current_cwd = cwd
862
+ ui.set_context_usage_state(
863
+ ContextUsageState.with_window(
864
+ used_tokens=0,
865
+ max_context_tokens=event.max_context_tokens or DEFAULT_MAX_CONTEXT_TOKENS,
866
+ )
867
+ )
868
+ ui.set_terminal_title_for_cwd(cwd)
869
+ ui.set_input_paste_cwd(cwd)
870
+ await ui.clear_chat()
871
+ s.update_toc()
872
+ with contextlib.suppress(Exception):
873
+ ui.reset_context_usage(event.max_context_tokens)
874
+ ui.set_chat_workspace_cwd(cwd)
875
+ ui.update_welcome(profile=s.profile, cwd=cwd)
876
+ if event.session_id:
877
+ ui.set_chat_session_id(event.session_id)
878
+ ui.reset_session_title_state()
879
+ ui.clear_status()
880
+ elif not s.restoring_session:
881
+ from chrys.foundation.platform import safe_getcwd
882
+
883
+ cwd = event.primary_cwd or safe_getcwd()
884
+ s.chdir_current_cwd = cwd
885
+ ui.set_chat_workspace_cwd(cwd)
886
+ ui.set_input_paste_cwd(cwd)
887
+ ui.update_welcome(profile=s.profile, cwd=cwd)
888
+ if event.session_id:
889
+ ui.set_chat_session_id(event.session_id)
890
+ ui.clear_status()
891
+ # Always propagate max_context_tokens so the Context panel shows the
892
+ # correct ceiling from the moment the session starts (before any LLM call).
893
+ if event.max_context_tokens:
894
+ current_context_usage = s.context_usage_state
895
+ if current_context_usage is None or current_context_usage.max_context_tokens != event.max_context_tokens:
896
+ ui.set_context_usage_state(
897
+ ContextUsageState.with_window(
898
+ used_tokens=current_context_usage.used_tokens if current_context_usage else s.last_usage_tokens,
899
+ max_context_tokens=event.max_context_tokens,
900
+ total_session_tokens=(
901
+ current_context_usage.total_session_tokens
902
+ if current_context_usage
903
+ else s.last_total_session_tokens
904
+ ),
905
+ total_session_input_tokens=(
906
+ current_context_usage.total_session_input_tokens if current_context_usage else 0
907
+ ),
908
+ total_session_output_tokens=(
909
+ current_context_usage.total_session_output_tokens if current_context_usage else 0
910
+ ),
911
+ total_session_cache_hit_tokens=(
912
+ current_context_usage.total_session_cache_hit_tokens if current_context_usage else None
913
+ ),
914
+ )
915
+ )
916
+ if event.sub_agent_tool_names:
917
+ from chrys.app.tui.widgets.chat.renderers.sub_agent import SubAgentToolCall
918
+ from chrys.app.tui.widgets.chat.tool_renderers import register_kind_renderer
919
+ from chrys.foundation.tool_kinds import KIND_SUB_AGENT
920
+
921
+ register_kind_renderer(KIND_SUB_AGENT, SubAgentToolCall)
922
+ from chrys.foundation.util.session_ids import session_short_id
923
+
924
+ # Backend-confirmed workspace roots: record them in the MRU as a
925
+ # detached background task (never inline — publish awaits handlers).
926
+ schedule_workspace_mru_touches(
927
+ [event.primary_cwd, *event.working_dirs],
928
+ max_entries=self._services.workspace_mru_max_entries,
929
+ session_id=event.session_id or "",
930
+ used_at=event.timestamp,
931
+ )
932
+ s.debug("SessionReady", f"{event.agent_profile} [{session_short_id(event.session_id or '')}]")
933
+ if not s.restoring_session:
934
+ self.finish_agent_load(_FINISH_SESSION_READY.bind(profile=s.profile))
935
+ self._callbacks.post_gc_message(GcReclaimRequested(GcReclaimReason.SESSION_READY, prompt=True))
936
+
937
+ async def on_settings_reloaded(self, _event: SettingsReloaded) -> None:
938
+ """Route the LIVE tier: re-project what a reload only installed.
939
+
940
+ A reload replaces the settings in the shared handle, but a LIVE field
941
+ whose consumer holds a projection — the notification service's view,
942
+ the dashboard's verify-commands word list — stays on the old values
943
+ until someone projects again. RELOAD fields need nothing here (the
944
+ rebuild re-read them), and RESTART fields were held back by the
945
+ reload's own routing.
946
+ """
947
+ self._callbacks.refresh_notification_settings()
948
+ self._callbacks.refresh_trajectory_verify_commands()
949
+ # The Settings panel projects written-but-not-yet-live values; a
950
+ # reload is when the live values catch up (or an unrelated reload
951
+ # arrives), so it re-reads and, for its own reload, takes the ack.
952
+ self._callbacks.settings_reloaded()
953
+
954
+ async def on_agent_runtime_updated(self, event: AgentRuntimeUpdated) -> None:
955
+ """Refresh runtime details and the persistent status-bar counts."""
956
+ s = self
957
+
958
+ s.runtime_metadata = event.runtime_details
959
+ self._state.runtime.details_confirmed = True
960
+ self._callbacks.refresh_model_indicator()
961
+ trail = self.format_tool_info(
962
+ event.tool_names,
963
+ event.skill_names,
964
+ memory_files=event.memory_files,
965
+ runtime_details=event.runtime_details,
966
+ )
967
+ self._ui().set_tool_info(trail)
968
+
969
+ async def on_agent_thinking(self, _event: AgentThinking) -> None:
970
+ s = self
971
+ if not s.agent_running:
972
+ return
973
+
974
+ self._ui().show_status(STATUS_THINKING.bind())
975
+ s.debug("AgentThinking")
976
+
977
+ async def on_agent_message(self, event: InvocationMessage) -> None:
978
+ if event.origin.kind == "sub_agent":
979
+ await self.on_sub_agent_message(event)
980
+ return
981
+ if event.origin.kind != "turn":
982
+ return
983
+ s = self
984
+ if self._predates_active_run(event):
985
+ s.debug("InvocationMessage", "ignored stale prior-run event")
986
+ return
987
+ if s.pending_user_message_render_active:
988
+ s.defer_agent_message(event)
989
+ return
990
+ if not s.agent_running:
991
+ return
992
+
993
+ ui = self._ui()
994
+
995
+ if event.is_intermediate:
996
+ await ui.add_agent_message(
997
+ event.text,
998
+ is_final=True,
999
+ is_intermediate=True,
1000
+ presentation=event.presentation,
1001
+ created_at=event.timestamp,
1002
+ )
1003
+ s.debug("InvocationMessage", f"(intermediate, {len(event.text)} chars)")
1004
+ elif event.is_final:
1005
+ was_live = s.agent_running
1006
+ terminal_generation = self._state.run.generation
1007
+ terminal_request = GcAbsorbRequested(GcAbsorbReason.TURN_TERMINAL, terminal_boundary=True)
1008
+ terminal_owned = False
1009
+ try:
1010
+ ui.flash_turn_complete()
1011
+ await ui.add_agent_message(
1012
+ event.text,
1013
+ is_final=True,
1014
+ structured_output_completed=event.structured_output_completed,
1015
+ created_at=event.timestamp,
1016
+ )
1017
+ finally:
1018
+ if self._state.run.generation == terminal_generation and s.agent_running:
1019
+ ui.mark_terminal_title_completed()
1020
+ s.set_agent_running(False)
1021
+ terminal_owned = True
1022
+ if not terminal_owned:
1023
+ s.debug("InvocationMessage", "ignored stale terminal completion")
1024
+ return
1025
+ self._callbacks.post_gc_message(terminal_request)
1026
+ if was_live:
1027
+ self._notify(NotificationEvent.TURN_COMPLETE)
1028
+ s.debug("InvocationMessage", f"({len(event.text)} chars)")
1029
+ else:
1030
+ ui.show_status(STATUS_STREAMING.bind())
1031
+ await ui.add_agent_message(event.text, is_final=False, created_at=event.timestamp)
1032
+
1033
+ async def on_presentation_attempt_accepted(self, event: InvocationPresentationAttemptAccepted) -> None:
1034
+ """Commit canonical provisional text and remove rejected siblings."""
1035
+ if event.origin.kind == "sub_agent":
1036
+ await self.on_sub_agent_presentation_accepted(event)
1037
+ return
1038
+ if event.origin.kind != "turn":
1039
+ return
1040
+ self._state.render_gate.accept_presentation_attempt(event.attempt_id, event.segment_ids)
1041
+ await self._ui().accept_presentation_attempt(event.attempt_id, event.segment_ids)
1042
+
1043
+ async def on_presentation_attempt_rejected(self, event: InvocationPresentationAttemptRejected) -> None:
1044
+ """Retract provisional text from a rejected response attempt."""
1045
+ if event.origin.kind == "sub_agent":
1046
+ await self.on_sub_agent_presentation_rejected(event)
1047
+ return
1048
+ if event.origin.kind != "turn":
1049
+ return
1050
+ self._state.render_gate.reject_presentation_attempt(event.attempt_id)
1051
+ await self._ui().reject_presentation_attempt(event.attempt_id)
1052
+
1053
+ async def on_injection_outcome(self, event: UserInjectResult) -> None:
1054
+ s = self
1055
+ ui = self._ui()
1056
+
1057
+ # Only the tracked pending injection may touch the input bar. A result
1058
+ # for an id the user already cancelled (or replaced with a new submit)
1059
+ # must not clear or unlock text they are editing; id-less results come
1060
+ # from frontends without pending tracking and keep legacy behavior.
1061
+ pending = self._state.pending_injection
1062
+ owns_input = event.injection_id is None or pending.matches(event.injection_id)
1063
+
1064
+ if event.consumed:
1065
+ try:
1066
+ # Render the bubble even when cancelled-too-late: the model
1067
+ # did see this text, so the transcript must show it.
1068
+ await ui.add_user_injection(event.text, created_at=event.created_at)
1069
+ s.update_toc()
1070
+ s.debug("UserInjectResult", "consumed" if owns_input else "consumed (stale id)")
1071
+ finally:
1072
+ if owns_input:
1073
+ pending.clear()
1074
+ ui.unlock_input_after_consumed_injection()
1075
+ elif owns_input:
1076
+ pending.clear()
1077
+ ui.unlock_input_after_abandoned_injection()
1078
+ s.debug("UserInjectResult", "abandoned (text preserved)")
1079
+ else:
1080
+ s.debug("UserInjectResult", "abandoned (stale id, ignored)")
1081
+
1082
+ async def on_tool_start(self, event: InvocationToolCallStart) -> None:
1083
+ if event.origin.kind == "sub_agent":
1084
+ await self.on_sub_agent_tool_start(event)
1085
+ return
1086
+ if event.origin.kind != "turn":
1087
+ return
1088
+ s = self
1089
+ if not s.agent_running:
1090
+ return
1091
+ ui = self._ui()
1092
+ ui.start_tool_status(event.tool_name)
1093
+ args_json = json.dumps(event.args, ensure_ascii=False) if event.args else ""
1094
+ provider_status = normalize_hosted_tool_status(event.provider_status)
1095
+ canonical_status = (
1096
+ provider_status.value
1097
+ if event.provider_hosted and provider_status in {HostedToolStatus.PENDING, HostedToolStatus.RUNNING}
1098
+ else "running"
1099
+ )
1100
+ await ui.add_tool_start(
1101
+ event.call_id,
1102
+ event.tool_name,
1103
+ event.tool_kind,
1104
+ args_json,
1105
+ args=event.args,
1106
+ provider_hosted=event.provider_hosted,
1107
+ hosted_family=event.hosted_family,
1108
+ provider=event.provider,
1109
+ provider_item_type=event.provider_item_type,
1110
+ provider_status=event.provider_status,
1111
+ provider_call_id=event.provider_call_id,
1112
+ canonical_status=canonical_status,
1113
+ )
1114
+ self._live_diff.record_tool_start(event.call_id, event.tool_name, event.args)
1115
+ s.debug("InvocationToolCallStart", f"{event.tool_name}")
1116
+
1117
+ async def on_tool_progress(self, event: InvocationToolCallProgress) -> None:
1118
+ if event.origin.kind == "sub_agent":
1119
+ await self.on_sub_agent_tool_progress(event)
1120
+ return
1121
+ if event.origin.kind != "turn":
1122
+ return
1123
+ s = self
1124
+ if not s.agent_running:
1125
+ return
1126
+ self._ui().update_tool_progress(
1127
+ event.call_id,
1128
+ event.lines,
1129
+ image_contents=event.image_contents,
1130
+ snapshot_metadata=event.snapshot_metadata,
1131
+ provider_status=event.provider_status,
1132
+ )
1133
+
1134
+ async def on_tool_args_updated(self, event: InvocationToolCallArgsUpdated) -> None:
1135
+ if event.origin.kind == "sub_agent":
1136
+ await self.on_sub_agent_tool_args_updated(event)
1137
+ return
1138
+ if event.origin.kind != "turn":
1139
+ return
1140
+ s = self
1141
+ if not s.agent_running:
1142
+ return
1143
+ self._ui().update_tool_args(event.call_id, event.args)
1144
+ s.debug("InvocationToolCallArgsUpdated", f"{event.tool_name}")
1145
+
1146
+ async def on_tool_status_updated(self, event: InvocationToolCallStatusUpdated) -> None:
1147
+ if event.origin.kind == "sub_agent":
1148
+ await self.on_sub_agent_tool_status_updated(event)
1149
+ return
1150
+ if event.origin.kind != "turn":
1151
+ return
1152
+ s = self
1153
+ if not s.agent_running:
1154
+ return
1155
+ self._ui().update_tool_status(
1156
+ event.call_id,
1157
+ event.status,
1158
+ provider_status=event.provider_status,
1159
+ metadata=event.metadata,
1160
+ )
1161
+ s.debug("ToolCallStatusUpdated", f"{event.tool_name}: {event.status}")
1162
+
1163
+ async def on_tool_result(self, event: InvocationToolCallResult) -> None:
1164
+ if event.origin.kind == "sub_agent":
1165
+ await self.on_sub_agent_tool_result(event)
1166
+ return
1167
+ if event.origin.kind != "turn":
1168
+ return
1169
+ s = self
1170
+ if not s.agent_running:
1171
+ return
1172
+ ui = self._ui()
1173
+ file_snapshot = event.metadata.get("file_snapshot")
1174
+ hashes = _metadata_hashes(event.metadata.get("file_mutation_hashes"))
1175
+ chat_file_snapshot = _chat_file_snapshot_payload(file_snapshot, hashes, s.state_store, ui.chat_session_id())
1176
+ approval = event.metadata.get("approval")
1177
+ canonical_status = canonical_tool_result_status(event.provider_status, event.metadata)
1178
+ await ui.add_tool_result(
1179
+ event.call_id,
1180
+ event.tool_name,
1181
+ event.result or "",
1182
+ event.duration_ms,
1183
+ image_contents=event.image_contents,
1184
+ file_snapshot=chat_file_snapshot,
1185
+ approval=approval,
1186
+ metadata=event.metadata,
1187
+ artifacts=event.artifacts,
1188
+ provider_status=event.provider_status,
1189
+ canonical_status=canonical_status,
1190
+ )
1191
+ self._questions().finish_inline_for_tool_result(event.call_id)
1192
+ self._live_diff.record_tool_result(event)
1193
+ s.refresh_git_branch()
1194
+ ui.show_status(STATUS_THINKING.bind())
1195
+ from chrys.app.tui.widgets.chat.tool_call import fmt_duration
1196
+
1197
+ s.debug("InvocationToolCallResult", f"{event.tool_name} ({fmt_duration(event.duration_ms)})")
1198
+
1199
+ async def on_sub_agent_invocation_start(self, event: InvocationStarted) -> None:
1200
+ """Link the just-started invocation to its mounted widget.
1201
+
1202
+ Fires before the sub-agent's first LLM call so the widget lookup
1203
+ by ``invocation_id`` works for every subsequent event — including
1204
+ ``InvocationRetryAttempt`` banners that would otherwise be dropped
1205
+ when the first LLM call hits transient errors.
1206
+ """
1207
+ if not _chat_sub_agent(event.origin):
1208
+ return
1209
+ s = self
1210
+ if not s.agent_running:
1211
+ return
1212
+
1213
+ self._ui().link_sub_agent_invocation(
1214
+ event.parent_call_id,
1215
+ event.origin.invocation_id,
1216
+ event.agent_name,
1217
+ event.sub_agent_log_file,
1218
+ tool_name=event.tool_name,
1219
+ )
1220
+
1221
+ async def on_sub_agent_message(self, event: InvocationMessage) -> None:
1222
+ """Append invocation-scoped assistant prose to the nested transcript."""
1223
+ if not _chat_sub_agent(event.origin):
1224
+ return
1225
+ if not self.agent_running:
1226
+ return
1227
+ self._ui().add_sub_agent_message(
1228
+ event.agent_name,
1229
+ event.origin.invocation_id,
1230
+ event.text,
1231
+ presentation=event.presentation,
1232
+ )
1233
+
1234
+ async def on_sub_agent_presentation_accepted(self, event: InvocationPresentationAttemptAccepted) -> None:
1235
+ """Commit provisional nested assistant segments."""
1236
+ if not _chat_sub_agent(event.origin):
1237
+ return
1238
+ if not self.agent_running:
1239
+ return
1240
+ self._ui().accept_sub_agent_presentation(
1241
+ event.origin.invocation_id,
1242
+ event.attempt_id,
1243
+ event.segment_ids,
1244
+ )
1245
+
1246
+ async def on_sub_agent_presentation_rejected(self, event: InvocationPresentationAttemptRejected) -> None:
1247
+ """Retract provisional nested assistant segments."""
1248
+ if not _chat_sub_agent(event.origin):
1249
+ return
1250
+ if not self.agent_running:
1251
+ return
1252
+ self._ui().reject_sub_agent_presentation(event.origin.invocation_id, event.attempt_id)
1253
+
1254
+ async def on_sub_agent_tool_start(self, event: InvocationToolCallStart) -> None:
1255
+ if not _chat_sub_agent(event.origin):
1256
+ return
1257
+ s = self
1258
+ if not s.agent_running:
1259
+ return
1260
+
1261
+ await self._ui().add_sub_agent_tool_start(
1262
+ event.agent_name,
1263
+ event.origin.invocation_id,
1264
+ event.tool_name,
1265
+ event.args,
1266
+ event.call_id,
1267
+ tool_kind=event.tool_kind,
1268
+ provider_hosted=event.provider_hosted,
1269
+ hosted_family=event.hosted_family,
1270
+ provider=event.provider,
1271
+ provider_item_type=event.provider_item_type,
1272
+ provider_status=event.provider_status,
1273
+ provider_call_id=event.provider_call_id,
1274
+ )
1275
+ self._live_diff.record_tool_start(event.call_id, event.tool_name, event.args)
1276
+
1277
+ async def on_sub_agent_tool_result(self, event: InvocationToolCallResult) -> None:
1278
+ if not _chat_sub_agent(event.origin):
1279
+ return
1280
+ s = self
1281
+ if not s.agent_running:
1282
+ return
1283
+
1284
+ approval = event.metadata.get("approval")
1285
+ canonical_status = canonical_tool_result_status(event.provider_status, event.metadata)
1286
+ self._ui().complete_sub_agent_tool(
1287
+ event.agent_name,
1288
+ event.origin.invocation_id,
1289
+ event.call_id,
1290
+ event.result or "",
1291
+ event.duration_ms,
1292
+ image_contents=event.image_contents,
1293
+ artifacts=event.artifacts,
1294
+ approval=approval,
1295
+ metadata=event.metadata,
1296
+ provider_status=event.provider_status,
1297
+ canonical_status=canonical_status,
1298
+ )
1299
+ self._live_diff.record_tool_result(event)
1300
+ s.refresh_git_branch()
1301
+
1302
+ async def on_sub_agent_tool_args_updated(self, event: InvocationToolCallArgsUpdated) -> None:
1303
+ if not _chat_sub_agent(event.origin):
1304
+ return
1305
+ if not self.agent_running:
1306
+ return
1307
+ self._ui().update_sub_agent_tool_args(event.origin.invocation_id, event.call_id, event.args)
1308
+ self.debug("InvocationToolCallArgsUpdated", event.tool_name)
1309
+
1310
+ async def on_sub_agent_tool_status_updated(self, event: InvocationToolCallStatusUpdated) -> None:
1311
+ if not _chat_sub_agent(event.origin):
1312
+ return
1313
+ if not self.agent_running:
1314
+ return
1315
+ self._ui().update_sub_agent_tool_status(
1316
+ event.origin.invocation_id,
1317
+ event.call_id,
1318
+ event.status,
1319
+ metadata=event.metadata,
1320
+ provider_status=event.provider_status,
1321
+ )
1322
+ self.debug("InvocationToolCallStatusUpdated", f"{event.tool_name}: {event.status}")
1323
+
1324
+ async def on_sub_agent_tool_progress(self, event: InvocationToolCallProgress) -> None:
1325
+ if not _chat_sub_agent(event.origin):
1326
+ return
1327
+ if not self.agent_running:
1328
+ return
1329
+ self._ui().update_sub_agent_tool_progress(
1330
+ event.origin.invocation_id,
1331
+ event.call_id,
1332
+ event.lines,
1333
+ image_contents=event.image_contents,
1334
+ snapshot_metadata=event.snapshot_metadata,
1335
+ provider_status=event.provider_status,
1336
+ )
1337
+ self.debug("InvocationToolCallProgress", event.tool_name)
1338
+
1339
+ async def on_sub_agent_progress(self, event: InvocationProgress) -> None:
1340
+ if not _chat_sub_agent(event.origin):
1341
+ return
1342
+ s = self
1343
+ if not s.agent_running:
1344
+ return
1345
+
1346
+ self._ui().update_sub_agent_progress(
1347
+ event.origin.invocation_id,
1348
+ event.tool_call_count,
1349
+ event.total_tokens,
1350
+ event.total_usage_tokens,
1351
+ event.usage_unreported_attempts,
1352
+ )
1353
+
1354
+ async def on_sub_agent_compaction_started(self, event: InvocationCompactionStarted) -> None:
1355
+ """Show a live compaction line inside the owning sub-agent card."""
1356
+ if not _chat_sub_agent(event.origin):
1357
+ return
1358
+ s = self
1359
+ if not s.agent_running:
1360
+ return
1361
+
1362
+ self._ui().add_sub_agent_compaction_start(event.agent_name, event.origin.invocation_id, event.compaction_id)
1363
+ s.debug("InvocationCompactionStarted", f"{event.agent_name} {event.phase}")
1364
+
1365
+ async def on_sub_agent_compaction_finished(self, event: InvocationCompactionFinished) -> None:
1366
+ """Flip the sub-agent compaction line to its terminal state.
1367
+
1368
+ Deliberately not gated on ``agent_running``: a cancel-outcome signal
1369
+ can land while the run is tearing down, and finalizing a line on an
1370
+ already-aborted card is a safe no-op.
1371
+ """
1372
+ if not _chat_sub_agent(event.origin):
1373
+ return
1374
+ s = self
1375
+
1376
+ self._ui().complete_sub_agent_compaction(
1377
+ event.origin.invocation_id,
1378
+ event.compaction_id,
1379
+ outcome=event.outcome,
1380
+ duration_ms=event.duration_ms,
1381
+ format_violation=event.format_violation,
1382
+ failure_reason=event.failure_reason,
1383
+ )
1384
+ from chrys.app.tui.widgets.chat.tool_call import fmt_duration
1385
+
1386
+ format_warning = f" — {event.format_violation}" if event.format_violation else ""
1387
+ s.debug(
1388
+ "InvocationCompactionFinished",
1389
+ f"{event.agent_name} {event.outcome} ({fmt_duration(event.duration_ms)}){format_warning}",
1390
+ )
1391
+
1392
+ async def on_sub_agent_compaction_committed(self, event: InvocationCompactionCommitted) -> None:
1393
+ """Bump the sub-agent card's compaction counter for a committed round.
1394
+
1395
+ Like the finished handler, deliberately not gated on
1396
+ ``agent_running`` — the committed signal trails finished(ok) and
1397
+ may land during run teardown; counting on a finalized card is
1398
+ harmless.
1399
+ """
1400
+ if not _chat_sub_agent(event.origin):
1401
+ return
1402
+ s = self
1403
+
1404
+ self._ui().record_sub_agent_compaction_committed(event.origin.invocation_id, event.compaction_id)
1405
+ s.debug("InvocationCompactionCommitted", f"{event.agent_name} {event.compaction_id}")
1406
+
1407
+ async def on_sub_agent_retry_attempt(self, event: InvocationRetryAttempt) -> None:
1408
+ """Render an auto-retry banner inside the owning sub-agent card."""
1409
+ if not _chat_sub_agent(event.origin):
1410
+ return
1411
+ message = (
1412
+ event.message
1413
+ if event.display_message is None
1414
+ else self._render_display_with_hint(event.display_message, event.display_hint)
1415
+ )
1416
+ self._ui().sub_agent_retry_attempt(
1417
+ event.origin.invocation_id,
1418
+ message,
1419
+ event.attempt,
1420
+ event.max_attempts,
1421
+ event.delay_seconds,
1422
+ )
1423
+ s = self
1424
+ s.debug(
1425
+ "Retry",
1426
+ f"{event.agent_name}: {event.message} ({event.attempt}/{event.max_attempts})",
1427
+ )
1428
+
1429
+ async def on_sub_agent_paused(self, event: InvocationPaused) -> None:
1430
+ """Flip the sub-agent card into its paused state with Retry/Abort buttons.
1431
+
1432
+ The parent is normally still running (awaiting the sub-agent's
1433
+ tool call result), so in the happy path no gate is needed. But
1434
+ during a user interrupt the frontend flips the run state's
1435
+ ``agent_running`` to False BEFORE the backend cascade finishes, so a late
1436
+ ``InvocationPaused`` that was already in-flight when interrupt
1437
+ fired can arrive after the UI has already torn the run down.
1438
+ Skipping it here keeps cards from flickering into a stale paused
1439
+ state with live Retry/Abort buttons that point at a controller
1440
+ the engine has already dropped.
1441
+ """
1442
+ if not _chat_sub_agent(event.origin):
1443
+ return
1444
+ s = self
1445
+ if not s.agent_running:
1446
+ return
1447
+
1448
+ self._ui().sub_agent_paused(
1449
+ event.origin.invocation_id,
1450
+ event.reason,
1451
+ event.last_error,
1452
+ event.retry_attempts,
1453
+ event.diagnostic_path,
1454
+ last_error_display=(
1455
+ None
1456
+ if event.last_error_display is None
1457
+ else self._render_display_with_hint(event.last_error_display, event.last_error_hint)
1458
+ ),
1459
+ )
1460
+
1461
+ async def on_sub_agent_resumed_after_pause(self, event: InvocationResumed) -> None:
1462
+ """Clear the paused state — the controller is running again."""
1463
+ if not _chat_sub_agent(event.origin):
1464
+ return
1465
+ self._ui().sub_agent_resumed_after_pause(event.origin.invocation_id)
1466
+
1467
+ async def on_sub_agent_cascade_aborted(self, event: InvocationCascadeAborted) -> None:
1468
+ """Mark the sub-agent card as cancelled by a global interrupt."""
1469
+ if not _chat_sub_agent(event.origin):
1470
+ return
1471
+ self._ui().sub_agent_cascade_aborted(event.origin.invocation_id)
1472
+
1473
+ async def on_sub_agent_aborted(self, event: InvocationAborted) -> None:
1474
+ """Mark the sub-agent card as aborted by the user after a pause.
1475
+
1476
+ Cleared independently of the parent :class:`InvocationToolCallResult` so the
1477
+ paused banner disappears the moment the controller publishes
1478
+ ``InvocationAborted`` — without this, the card could stay visually
1479
+ paused if the parent tool result is delayed or lost.
1480
+ """
1481
+ if not _chat_sub_agent(event.origin):
1482
+ return
1483
+ self._ui().sub_agent_aborted(event.origin.invocation_id, event.last_error)
1484
+
1485
+ # -- Live /diff accumulation helpers -----------------------------------
1486
+
1487
+ def _accumulate_live_mutation(
1488
+ self,
1489
+ path: str,
1490
+ before_text: str,
1491
+ after_text: str,
1492
+ op_str: str | None = None,
1493
+ *,
1494
+ bytes_changed: bool | None = None,
1495
+ before_hash: str | None = None,
1496
+ after_hash: str | None = None,
1497
+ source: str = "",
1498
+ ) -> None:
1499
+ """Record a single file mutation for live /diff."""
1500
+ self._live_diff.accumulate_live_mutation(
1501
+ path,
1502
+ before_text,
1503
+ after_text,
1504
+ op_str,
1505
+ bytes_changed=bytes_changed,
1506
+ before_hash=before_hash,
1507
+ after_hash=after_hash,
1508
+ source=source,
1509
+ )
1510
+
1511
+ def _accumulate_shell_snapshots(self, metadata: dict[str, object]) -> None:
1512
+ """Accumulate shell-detected file mutations from event metadata."""
1513
+ self._live_diff.accumulate_shell_snapshots(metadata)
1514
+
1515
+ async def on_approval_request(self, event: ApprovalRequest) -> None:
1516
+ """Queue an approval request and (maybe) show its dialog.
1517
+
1518
+ The backend decides the mode: BYPASS never publishes this event at
1519
+ all, MANUAL publishes with ``judging=False``, AUTO publishes with
1520
+ ``judging=True`` and will later emit an ``ApprovalReviewed``. A
1521
+ judging request waits unseen for that verdict while
1522
+ ``ui.approval.defer_while_judging`` is on; otherwise its dialog opens
1523
+ at once and the verdict updates it.
1524
+ """
1525
+ await self._approval().on_request(event)
1526
+
1527
+ async def on_approval_cancelled(self, event: ApprovalCancelled) -> None:
1528
+ """Dismiss an abandoned approval without sending an ApprovalResponse."""
1529
+ await self._approval().on_cancelled(event)
1530
+
1531
+ def _show_next_approval(self) -> None:
1532
+ """Pop the next queued approval request and show its dialog.
1533
+
1534
+ Drains any cached judge verdict that arrived while the request was
1535
+ still queued (parallel tool execution in AUTO mode — see
1536
+ ``_pending_verdicts``):
1537
+
1538
+ - Approved pre-mount → skip the dialog entirely and continue to the
1539
+ next queued request; the backend already ran the tool.
1540
+ - Flagged pre-mount → open the dialog already flagged, the concern
1541
+ shown and the reason focused, never a stuck "Evaluating" spinner.
1542
+ """
1543
+ self._approval().show_next()
1544
+
1545
+ async def on_approval_reviewed(self, event: ApprovalReviewed) -> None:
1546
+ """Deliver a judge verdict from the backend to the open dialog.
1547
+
1548
+ Four cases handled:
1549
+
1550
+ - Request deferred while judging → an approval drops it unseen; a
1551
+ flag queues it to open already flagged.
1552
+ - Matching dialog is live and undismissed → deliver verdict
1553
+ immediately (approved auto-dismisses; flagged shows the concern).
1554
+ - Dialog not yet pushed but request still queued → stash the verdict;
1555
+ it is applied when ``_show_next_approval`` eventually pops the
1556
+ request. Supports parallel tool execution in AUTO mode where
1557
+ judges for queued requests finish before their dialog is shown.
1558
+ - Late arrival for a request that was already user-dismissed or
1559
+ otherwise resolved → drop silently.
1560
+ """
1561
+ await self._approval().on_reviewed(event)
1562
+
1563
+ async def on_approval_mode_updated(self, event: ApprovalModeUpdated) -> None:
1564
+ """Sync the header badge + local cache from the backend's mode."""
1565
+ if not self._callbacks.accept_approval_update(event):
1566
+ return
1567
+ s = self
1568
+ try:
1569
+ mode = ApprovalMode(event.mode)
1570
+ except ValueError:
1571
+ return
1572
+ changed = s.approval_mode != mode
1573
+ s.approval_mode = mode
1574
+ self._ui().set_header_approval_mode(mode)
1575
+ label = mode.value.upper()
1576
+ # Only toast on a genuine change — skip the initial session-start sync.
1577
+ if changed:
1578
+ mode_label = self._render_display(APPROVAL_MODE_MESSAGES[mode].bind())
1579
+ s.notify(_APPROVAL_MODE_CHANGED.bind(mode=mode_label), title=_APPROVAL_TITLE.bind(), timeout=2)
1580
+ s.debug("ApprovalMode", label)
1581
+
1582
+ async def on_question_to_user(self, event: QuestionToUser) -> None:
1583
+ await self._questions().on_question(event)
1584
+
1585
+ def clear_pending_questions(self) -> None:
1586
+ """Drop any ask_user UI state after the backend turn is cancelled."""
1587
+ self._questions().clear_pending()
1588
+
1589
+ def _show_next_question(self) -> None:
1590
+ """Pop the next queued question and show its dialog."""
1591
+ self._questions().show_next()
1592
+
1593
+ def _show_question_inline(self, event: QuestionToUser, draft: PromptDraft | None = None) -> bool:
1594
+ """Move an active ask_user prompt from modal UI into its tool renderer."""
1595
+ return self._gateway().show_question_inline(event, draft)
1596
+
1597
+ def _question_can_reopen_modal(self, event: QuestionToUser) -> bool:
1598
+ """Return whether a failed inline handoff should reopen the modal."""
1599
+ return self._gateway().question_can_reopen_modal(event)
1600
+
1601
+ async def on_ask_user_timed_out(self, event: AskUserTimedOut) -> None:
1602
+ """Dismiss or dequeue an ask_user dialog after the backend timeout fires."""
1603
+ await self._questions().on_timed_out(event)
1604
+
1605
+ async def on_usage_update(self, event: UsageUpdate) -> None:
1606
+ s = self
1607
+ if event.session_id and s.main_usage_source_id and event.session_id != s.main_usage_source_id:
1608
+ return
1609
+
1610
+ # Only treat an event as the main session window when both sides have
1611
+ # bound a non-empty source id and they match. Earlier we accepted any
1612
+ # empty event as the main window, but that silently mis-routed any
1613
+ # caller that forgot to set ``usage_source_id`` and also misclassified
1614
+ # parent usage that arrived before SessionReady set
1615
+ # ``main_usage_source_id``.
1616
+ is_session_window = bool(s.main_usage_source_id) and (event.usage_source_id == s.main_usage_source_id)
1617
+ if is_session_window:
1618
+ s.last_usage_tokens = event.total_tokens
1619
+ # Session totals advance with every UsageUpdate (parent or sub-agent) —
1620
+ # the cumulative figure is global, not per-source.
1621
+ s.last_total_session_tokens = event.total_session_tokens
1622
+ if is_session_window:
1623
+ # Main session: refresh used/max gauge, sparkline, AND totals row.
1624
+ self._ui().set_context_usage_state(
1625
+ ContextUsageState.with_window(
1626
+ used_tokens=event.total_tokens,
1627
+ max_context_tokens=event.max_context_tokens or DEFAULT_MAX_CONTEXT_TOKENS,
1628
+ total_session_tokens=event.total_session_tokens,
1629
+ total_session_input_tokens=event.total_session_input_tokens,
1630
+ total_session_output_tokens=event.total_session_output_tokens,
1631
+ total_session_cache_hit_tokens=event.total_session_cache_hit_tokens,
1632
+ )
1633
+ )
1634
+ else:
1635
+ # Sub-agent: keep the main session's used/max gauge & sparkline,
1636
+ # but let the cumulative totals row advance. Without this gate the
1637
+ # sidebar gauge would flicker between parent and sub-agent windows.
1638
+ self._ui().set_context_usage_state(
1639
+ ContextUsageState.session_totals_only(
1640
+ s.context_usage_state,
1641
+ fallback_used_tokens=s.last_usage_tokens,
1642
+ total_session_tokens=event.total_session_tokens,
1643
+ total_session_input_tokens=event.total_session_input_tokens,
1644
+ total_session_output_tokens=event.total_session_output_tokens,
1645
+ total_session_cache_hit_tokens=event.total_session_cache_hit_tokens,
1646
+ )
1647
+ )
1648
+ label = f"Usage[{event.agent_profile}]" if event.agent_profile else "UsageUpdate"
1649
+ source = _usage_source_debug_suffix(event.usage_source_id, main_source_id=s.main_usage_source_id)
1650
+ s.debug(label, f"{event.total_tokens:,} ({event.pct:.1f}%) local={event.local_tokens:,}{source}")
1651
+
1652
+ async def on_todo_list_updated(self, event: TodoListUpdated) -> None:
1653
+ s = self
1654
+
1655
+ self._ui().set_todo_state(tuple(event.items))
1656
+ done = sum(1 for item in event.items if item.status == "completed")
1657
+ s.debug("TodoListUpdated", f"{done}/{len(event.items)} done")
1658
+
1659
+ async def on_compaction_started(self, event: CompactionStarted) -> None:
1660
+ """Mount a live "Compacting conversation..." card in the transcript."""
1661
+ s = self
1662
+ if not s.agent_running:
1663
+ return
1664
+
1665
+ await self._ui().add_compaction_start(event.compaction_id)
1666
+ self._ui().show_status(STATUS_COMPACTING.bind())
1667
+ s.debug("CompactionStarted", event.phase)
1668
+
1669
+ async def on_compaction_finished(self, event: CompactionFinished) -> None:
1670
+ """Finalize the live compaction card (summary note, failure, or cancel).
1671
+
1672
+ Deliberately not gated on ``agent_running``: the cancel-outcome
1673
+ signal is published while the interrupted run is tearing down, and
1674
+ the card must still stop spinning. With no matching live card the
1675
+ update is a no-op. The status bar is a different story: a canceled
1676
+ outcome only ever means an interrupted run, and during teardown it
1677
+ can arrive before the frontend flips ``agent_running`` — re-showing
1678
+ the spinner then would overwrite the "Interrupted by user" flash
1679
+ and leave a stuck "Thinking" bar, so canceled never restores it.
1680
+ """
1681
+ s = self
1682
+
1683
+ self._ui().complete_compaction(
1684
+ event.compaction_id,
1685
+ outcome=event.outcome,
1686
+ duration_ms=event.duration_ms,
1687
+ last_words=event.last_words,
1688
+ format_violation=event.format_violation,
1689
+ failure_reason=event.failure_reason,
1690
+ )
1691
+ if s.agent_running and event.outcome != "canceled":
1692
+ self._ui().show_status(STATUS_THINKING.bind())
1693
+ from chrys.app.tui.widgets.chat.tool_call import fmt_duration
1694
+
1695
+ format_warning = f" — {event.format_violation}" if event.format_violation else ""
1696
+ failure_note = f" — {event.failure_reason}" if event.failure_reason else ""
1697
+ s.debug(
1698
+ "CompactionFinished",
1699
+ f"{event.outcome} ({fmt_duration(event.duration_ms)}){failure_note}{format_warning}",
1700
+ )
1701
+
1702
+ async def on_tool_compacted(self, event: ToolCompacted) -> None:
1703
+ s = self
1704
+ freed = event.tokens_before - event.tokens_after
1705
+ event_key = f"ToolCompacted:{event.phase}" if event.phase else "ToolCompacted"
1706
+
1707
+ if event.phase in ("phase1", "phase2"):
1708
+ turns_str = ", ".join(str(t) for t in event.turn_numbers) if event.turn_numbers else "?"
1709
+ action = "summarised" if event.phase == "phase1" else "removed"
1710
+ s.debug(
1711
+ event_key,
1712
+ f"{event.tokens_before:,} \u2192 {event.tokens_after:,} (-{freed:,}) [turns {turns_str} {action}]",
1713
+ )
1714
+ else:
1715
+ action = "removed"
1716
+ s.debug(
1717
+ event_key,
1718
+ f"{event.tokens_before:,} \u2192 {event.tokens_after:,} (-{freed:,}) "
1719
+ f"[{event.compacted_groups} tool calls {action}]",
1720
+ )
1721
+
1722
+ async def on_context_compressed(self, event: ContextCompressed) -> None:
1723
+ s = self
1724
+
1725
+ ui = self._ui()
1726
+ if await ui.add_context_fold(
1727
+ event.compressed_context_id,
1728
+ event.summary,
1729
+ event.freed_messages,
1730
+ event.turn_range,
1731
+ ):
1732
+ s.update_toc()
1733
+ with contextlib.suppress(Exception):
1734
+ ui.add_compressed_block(
1735
+ event.compressed_context_id,
1736
+ event.summary,
1737
+ event.freed_messages,
1738
+ event.turn_range,
1739
+ )
1740
+ source_label = "auto" if event.source == "auto" else "agent"
1741
+ event_key = f"ContextCompressed:{event.source}" if event.source == "auto" else "ContextCompressed"
1742
+ s.debug(event_key, f"-{event.freed_messages:,} messages ({source_label})")
1743
+
1744
+ async def on_context_pressure(self, event: InvocationContextPressure) -> None:
1745
+ """Surface a Phase-4 breaker trip instead of silently losing compaction."""
1746
+ if event.origin.root.kind == "workflow_node":
1747
+ return
1748
+ message = context_pressure_message(event.reason, source=event.source)
1749
+ s = self
1750
+ # The breaker already deduplicates this event once per logical turn.
1751
+ # Do not route it through ``on_warning``: that handler's screen-lifetime
1752
+ # cache would suppress the same pressure reason in every later turn or
1753
+ # sibling sub-agent invocation.
1754
+ s.notify(message, title=_WARNING_TITLE.bind(), severity="warning", timeout=8)
1755
+ s.debug(
1756
+ "InvocationContextPressure",
1757
+ f"{event.source}:{event.reason} attempts={event.attempts} "
1758
+ f"side_calls={event.side_call_tokens:,}/{event.side_call_token_budget:,}",
1759
+ )
1760
+
1761
+ async def on_warning(self, event: Warning) -> None:
1762
+ s = self
1763
+ submit_rejected = event.code in _SUBMIT_BLOCKING_WARNING_CODES and s.pending_user_submit_active
1764
+ if submit_rejected:
1765
+ s.block_pending_user_submit()
1766
+ if event.code in _IMAGE_WARNING_CODES:
1767
+ display = (
1768
+ event.message if event.display_message is None else self._render_display(event.display_message)
1769
+ )
1770
+ self._show_image_rejection_dialog(event.code, display)
1771
+ s.debug("ImageAttachmentBlocked", event.message.splitlines()[0][:80])
1772
+ return
1773
+ self._restore_pending_submit_text()
1774
+ key = _WarningDedupeKey(
1775
+ # Capability ownership is a model configuration fact. Its message
1776
+ # already includes the model profile, so /new must not reopen the
1777
+ # same modal merely because it allocated another session id.
1778
+ session_id=None if event.code == "hosted_web_tools_preferred" else event.session_id,
1779
+ code=event.code,
1780
+ message=event.message,
1781
+ )
1782
+ # Each rejected user action needs feedback, even when the reason is
1783
+ # unchanged. Only background warnings share the screen-lifetime cache.
1784
+ if key in self._seen_warnings and not submit_rejected:
1785
+ return
1786
+ self._seen_warnings.add(key)
1787
+ # A capability handover is worth one modal per configuration, not a toast the
1788
+ # user can miss; the dedupe key already scopes it to the producing fingerprint.
1789
+ if event.code == "hosted_web_tools_preferred":
1790
+ from chrys.app.tui.screens.dialogs.confirm import ConfirmDialog
1791
+
1792
+ dialog = ConfirmDialog(
1793
+ title=_WARNING_TITLE.bind(),
1794
+ message=event.display_message or event.message,
1795
+ confirm_label=_HOSTED_WEB_TOOLS_OK.bind(),
1796
+ cancel_label=None,
1797
+ confirm_variant="warning",
1798
+ locale_controller=self._locale_controller,
1799
+ )
1800
+ dialog.add_class("-warning-border")
1801
+ s.push_screen(dialog)
1802
+ s.debug("Warning", f"[{event.code}] {event.message[:80]}")
1803
+ return
1804
+ s.notify(event.display_message or event.message, title=_WARNING_TITLE.bind(), severity="warning", timeout=8)
1805
+ s.debug("Warning", f"[{event.code}] {event.message[:80]}")
1806
+
1807
+ async def on_error(self, event: Error) -> None:
1808
+ if self._callbacks.route_session_error(event):
1809
+ return
1810
+ s = self
1811
+ if event.code in _STALE_RUN_FILTERED_ERROR_CODES and self._predates_active_run(event):
1812
+ s.debug("Error", "ignored stale prior-run error")
1813
+ return
1814
+ ui = self._ui()
1815
+ was_live = s.agent_running
1816
+ terminal_generation = self._state.run.generation
1817
+ s.restoring_session = False
1818
+ # Agent-load failures should arrive as ``AgentLoadFailed``. Keep
1819
+ # this fallback for failures that happen before the load lifecycle is
1820
+ # established or arrive from older backend paths.
1821
+ raw_msg = event.message.strip()
1822
+ if event.code.startswith(_SESSION_FORK_ERROR_PREFIX):
1823
+ full_msg = raw_msg or self._fallback_error_message(event.code)
1824
+ display_msg = full_msg if event.display_message is None else self._render_display(event.display_message)
1825
+ severity: NotificationSeverity = "error" if event.code == "session_fork_failed" else "warning"
1826
+ s.on_session_fork_error(event, message=display_msg, severity=severity)
1827
+ s.debug("Error", f"[{event.code}] {full_msg[:60]}")
1828
+ return
1829
+ if event.code == _SESSION_CLEAR_FAILED_CODE:
1830
+ # /clear failed before deleting anything: keep the session, toast,
1831
+ # and skip the generic chat-error/retry-mode surface.
1832
+ full_msg = raw_msg or self._fallback_error_message(event.code)
1833
+ display_msg = full_msg if event.display_message is None else self._render_display(event.display_message)
1834
+ s.on_session_clear_error(event, message=display_msg)
1835
+ s.debug("Error", f"[{event.code}] {full_msg[:60]}")
1836
+ return
1837
+ full_msg = raw_msg or (
1838
+ self._render_display(_AGENT_FAILED_TO_LOAD.bind())
1839
+ if s.agent_loading
1840
+ else self._fallback_error_message(event.code)
1841
+ )
1842
+ # UI surfaces show the localized display when the producer attached
1843
+ # one; ``full_msg`` keeps the raw protocol text for debug lines.
1844
+ display_full = (
1845
+ full_msg
1846
+ if event.display_message is None
1847
+ else self._render_display_with_hint(event.display_message, event.display_hint)
1848
+ )
1849
+ if event.code == "session_in_use":
1850
+ if s.agent_loading:
1851
+ self.cancel_agent_load()
1852
+ s.set_agent_running(False)
1853
+ from chrys.app.tui.screens.dialogs.confirm import ConfirmDialog
1854
+
1855
+ dialog = ConfirmDialog(
1856
+ title=_SESSION_IN_USE_TITLE.bind(),
1857
+ message=_SESSION_IN_USE_MESSAGE.bind(message=DisplayBlock(display_full)),
1858
+ confirm_label=_SESSION_IN_USE_OK.bind(),
1859
+ cancel_label=None,
1860
+ confirm_variant="warning",
1861
+ locale_controller=self._locale_controller,
1862
+ bold_message_prefix=True,
1863
+ )
1864
+ dialog.add_class("-warning-border")
1865
+ s.push_screen(dialog)
1866
+ s.debug("Error", f"[{event.code}] {full_msg[:60]}")
1867
+ return
1868
+ if s.pending_user_submit_active and event.code in _SUBMIT_BLOCKING_ERROR_CODES:
1869
+ s.set_agent_running(False)
1870
+ s.block_pending_user_submit()
1871
+ if event.code in _IMAGE_ERROR_CODES:
1872
+ self._show_image_rejection_dialog(event.code, display_full)
1873
+ s.debug("ImageAttachmentBlocked", full_msg.splitlines()[0][:80])
1874
+ return
1875
+ if event.code == _WORKING_DIR_MISSING_CODE:
1876
+ # The draft goes back first; the folder prompt never writes to it.
1877
+ restored = self._restore_pending_submit_text() is not None
1878
+ self._callbacks.prompt_missing_working_dir("submit" if restored else "turn_end")
1879
+ s.debug("Error", f"[{event.code}] {full_msg[:60]}")
1880
+ return
1881
+ await self._show_pending_submit_error(event.code, display_full, raw_message=full_msg)
1882
+ return
1883
+ if s.pending_user_message_render_active:
1884
+ s.defer_error(event)
1885
+ return
1886
+ # A classified failure shows what went wrong, then the service's own
1887
+ # words as evidence; other producers' display is their message in
1888
+ # the current locale, so it stands alone.
1889
+ with_raw = event.code == "executor_error" and event.display_message is not None and bool(raw_msg)
1890
+ if s.agent_loading:
1891
+ self._agent_load().fail(f"{display_full}\n{raw_msg}" if with_raw else display_full)
1892
+ terminal_request = GcAbsorbRequested(GcAbsorbReason.TURN_TERMINAL, terminal_boundary=True) if was_live else None
1893
+ # Build a short version for the status bar (strip JSON payload); its
1894
+ # one line has no room for the hint.
1895
+ short_msg = full_msg if event.display_message is None else self._render_display(event.display_message)
1896
+ if "- {" in short_msg:
1897
+ short_msg = short_msg[: short_msg.index("- {")].strip()
1898
+ # Build a readable version for the chat panel: extract 'message'
1899
+ # from the JSON payload if present, otherwise use full text.
1900
+ if with_raw:
1901
+ chat_msg = f"{display_full}\n{self._extract_error_detail(raw_msg)}"
1902
+ else:
1903
+ chat_msg = self._extract_error_detail(display_full)
1904
+ terminal_owned = not was_live
1905
+ try:
1906
+ ui.flash_status(STATUS_ERROR.bind(message=short_msg), error=True)
1907
+ await ui.add_error(chat_msg)
1908
+ finally:
1909
+ if not was_live or (self._state.run.generation == terminal_generation and s.agent_running):
1910
+ if was_live:
1911
+ ui.mark_terminal_title_failed()
1912
+ s.set_agent_running(False)
1913
+ ui.unlock_input_keep_if_locked()
1914
+ terminal_owned = True
1915
+ if not terminal_owned:
1916
+ s.debug("Error", "ignored stale terminal completion")
1917
+ return
1918
+ if terminal_request is not None:
1919
+ self._callbacks.post_gc_message(terminal_request)
1920
+ ui.set_input_retry(INPUT_RETRY.bind())
1921
+ if was_live and event.code not in _NON_TURN_NOTIFICATION_ERROR_CODES:
1922
+ self._notify(NotificationEvent.TURN_ERROR)
1923
+ s.debug("Error", f"[{event.code}] {full_msg[:60]}")
1924
+
1925
+ def _restore_pending_submit_text(self) -> InputRestore | None:
1926
+ """Restore the prompt text that the input widget cleared on submit."""
1927
+ s = self
1928
+ return self._ui().restore_input_text(s.pending_user_submit_text)
1929
+
1930
+ def _show_image_rejection_dialog(self, code: str, message: str) -> None:
1931
+ """Show the image rejection modal for a backend-validated prompt."""
1932
+ text = self.pending_user_submit_text
1933
+ converted_text = replace_image_mentions_with_paths(text, Path(self._state.workspace.current_cwd))
1934
+ show_path_action = converted_text != text
1935
+ restore = self._restore_pending_submit_text()
1936
+
1937
+ from chrys.app.tui.screens.dialogs.vision_unsupported import (
1938
+ USE_IMAGE_PATHS_RESULT,
1939
+ VISION_UNSUPPORTED_TITLE,
1940
+ VisionUnsupportedDialog,
1941
+ )
1942
+
1943
+ title = VISION_UNSUPPORTED_TITLE.bind() if code == "vision_unsupported" else _IMAGE_NOT_ATTACHED.bind()
1944
+
1945
+ dialog = VisionUnsupportedDialog(
1946
+ message,
1947
+ title=title,
1948
+ show_path_action=show_path_action,
1949
+ locale_controller=self._locale_controller,
1950
+ )
1951
+
1952
+ def on_dismiss(result: object) -> None:
1953
+ # Only the prompt this handler restored may be rewritten in place;
1954
+ # a draft the user typed instead is theirs, dialog or no dialog.
1955
+ if result == USE_IMAGE_PATHS_RESULT:
1956
+ self._ui().rewrite_restored_input(restore, converted_text)
1957
+
1958
+ self.push_screen(dialog, callback=on_dismiss)
1959
+
1960
+ async def _show_pending_submit_error(self, code: str, message: str, *, raw_message: str | None = None) -> None:
1961
+ """Surface a synchronous submit rejection without switching into retry mode.
1962
+
1963
+ ``message`` may be localized display text; the debug stream keeps
1964
+ ``raw_message`` (the protocol English) when the caller provides it.
1965
+ """
1966
+ s = self
1967
+ ui = self._ui()
1968
+
1969
+ self._restore_pending_submit_text()
1970
+ short_msg = message
1971
+ if "- {" in short_msg:
1972
+ short_msg = short_msg[: short_msg.index("- {")].strip()
1973
+ chat_msg = self._extract_error_detail(message)
1974
+ ui.flash_status(STATUS_ERROR.bind(message=short_msg), error=True)
1975
+ if self._state.submit.is_retry:
1976
+ # Admission did not start a new run. Keep the previous terminal
1977
+ # card and its action so the user can retry after resolving this.
1978
+ s.notify(chat_msg, title=_RETRY_NOT_STARTED.bind(), severity="error", timeout=8)
1979
+ else:
1980
+ await ui.add_error(chat_msg, action_label=None)
1981
+ s.debug("Error", f"[{code}] {(message if raw_message is None else raw_message)[:60]}")
1982
+
1983
+ async def on_retry_attempt(self, event: InvocationRetryAttempt) -> None:
1984
+ if event.origin.kind == "sub_agent":
1985
+ await self.on_sub_agent_retry_attempt(event)
1986
+ return
1987
+ if event.origin.kind != "turn":
1988
+ return
1989
+ s = self
1990
+ if not s.agent_running:
1991
+ return # Stale event after interrupt — ignore
1992
+ if event.scope == "compaction":
1993
+ # Phase-4 side-call retries recover on their own most of the
1994
+ # time — they surface as a quiet warning line inside the live
1995
+ # compaction card, never as the transcript-level error banner
1996
+ # (which would also wrongly finalize running tool widgets).
1997
+ reason = event.detail or event.message
1998
+ self._ui().show_compaction_retry(reason, event.attempt, event.max_attempts, event.delay_seconds)
1999
+ s.debug("Retry", f"{event.message} ({event.attempt}/{event.max_attempts})")
2000
+ return
2001
+ # Finalise pending widgets from the failed attempt BEFORE mounting
2002
+ # the retry notice. The backend's ``StreamRetryLoop`` rolls history
2003
+ # back before the next attempt, so any still-running tool cards
2004
+ # (especially sub-agents cancelled by the stream cleanup hooks) and
2005
+ # the open intermediate assistant message would otherwise act as
2006
+ # stale attach points for the re-run's tool calls, causing new
2007
+ # sub-agent cards to appear under the old assistant block.
2008
+ retry_msg = (
2009
+ event.message
2010
+ if event.display_message is None
2011
+ else self._render_display_with_hint(event.display_message, event.display_hint)
2012
+ )
2013
+ await self._ui().show_retry_attempt(retry_msg, event.attempt, event.max_attempts, event.delay_seconds)
2014
+ s.debug("Retry", f"{event.message} ({event.attempt}/{event.max_attempts})")