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,2265 @@
1
+ # Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
2
+
3
+ """Main chat screen — the primary interaction surface."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import asyncio
8
+ import contextlib
9
+ import logging
10
+ import reprlib
11
+ from collections.abc import Awaitable, Callable
12
+ from functools import partial
13
+ from types import FunctionType, MethodType
14
+ from typing import TYPE_CHECKING, Any, ClassVar, Literal, cast
15
+
16
+ from textual import __version__ as TEXTUAL_VERSION
17
+ from textual import on, work
18
+ from textual.actions import SkipAction
19
+ from textual.binding import Binding
20
+ from textual.containers import Horizontal
21
+ from textual.content import Content
22
+ from textual.events import DescendantFocus, Paste, Resize, ScreenResume, ScreenSuspend
23
+ from textual.geometry import Offset, Size
24
+ from textual.reactive import reactive
25
+ from textual.screen import Screen
26
+ from textual.selection import Selection, SelectState
27
+ from textual.timer import Timer
28
+ from textual.widget import Widget
29
+
30
+ from chrys.app.tui.behaviors.right_click_copy import RightClickScreenCopyMixin
31
+ from chrys.app.tui.binding_display import QUIT_BINDING, localized_binding
32
+ from chrys.app.tui.i18n import render_content, render_str
33
+ from chrys.app.tui.language import LANGUAGE_OPTIONS, LANGUAGE_UNKNOWN_LOCALE
34
+ from chrys.app.tui.screens.dialogs.app_mode import AppModeDialog
35
+ from chrys.app.tui.screens.dialogs.editor import EditorDialog, EditorDialogResult
36
+ from chrys.app.tui.screens.main.action_availability import check_main_action
37
+ from chrys.app.tui.screens.main.commands import INVALID_COMMAND_TITLE_REF, SlashCommandActions
38
+ from chrys.app.tui.screens.main.config_actions import (
39
+ RuntimeConfigCallbacks,
40
+ RuntimeConfigController,
41
+ )
42
+ from chrys.app.tui.screens.main.copy_actions import CopyActionController
43
+ from chrys.app.tui.screens.main.diff_controller import DiffController, LiveDiffOwner, LiveDiffTracker
44
+ from chrys.app.tui.screens.main.event_handlers import BackendEventCallbacks, BackendEventHandler
45
+ from chrys.app.tui.screens.main.input_flow import InputFlowController
46
+ from chrys.app.tui.screens.main.model_indicator import (
47
+ compute_model_indicator_state,
48
+ is_model_selection_locked,
49
+ )
50
+ from chrys.app.tui.screens.main.navigation import MainNavigationController
51
+ from chrys.app.tui.screens.main.ports import StatusMessage
52
+ from chrys.app.tui.screens.main.rollback_controller import RollbackController
53
+ from chrys.app.tui.screens.main.runtime_info import RegistryRuntimeInfoProvider
54
+ from chrys.app.tui.screens.main.session_handlers import RestoreRequest, SessionCallbacks, SessionHandler
55
+ from chrys.app.tui.screens.main.session_title import SessionTitleController
56
+ from chrys.app.tui.screens.main.settings_coordinator import (
57
+ SETTINGS_TITLE,
58
+ SettingsCoordinator,
59
+ SettingsCoordinatorCallbacks,
60
+ )
61
+ from chrys.app.tui.screens.main.settings_persistence import (
62
+ SETTINGS_FLUSH_LOCK_TIMEOUT_SECONDS,
63
+ SETTINGS_SAVE_DELAY_SECONDS,
64
+ SettingsPersistenceQueue,
65
+ )
66
+ from chrys.app.tui.screens.main.shell_mode import ShellModeController
67
+ from chrys.app.tui.screens.main.state import MainScreenServices, MainScreenState
68
+ from chrys.app.tui.screens.main.subscriptions import MainScreenSubscriptions
69
+ from chrys.app.tui.screens.main.suggestions import SuggestionCallbacks, SuggestionHandler
70
+ from chrys.app.tui.screens.main.tool_action_bridge import ToolActionBridge
71
+ from chrys.app.tui.screens.main.view_adapter import MainScreenViewAdapter
72
+ from chrys.app.tui.screens.main.workflow_controller import WorkflowController
73
+ from chrys.app.tui.screens.main.workspace_actions import WorkspaceCallbacks, WorkspaceController
74
+ from chrys.app.tui.screens.main.workspace_branch import WorkspaceBranchController
75
+ from chrys.app.tui.screens.settings import GENERAL_TAB_ID
76
+ from chrys.app.tui.screens.settings.dialog import THEME_PREVIEW_HINT
77
+ from chrys.app.tui.screens.themes import ThemeEditorPanel
78
+ from chrys.app.tui.support.gc_freeze import (
79
+ GcFreezeBlockReason,
80
+ GcFreezeParticipant,
81
+ abort_textual_screen_gc_freeze,
82
+ after_textual_screen_gc_freeze,
83
+ prepare_textual_screen_for_gc,
84
+ raise_gc_freeze_hook_errors,
85
+ )
86
+ from chrys.app.tui.terminal.panel import ShellPanel
87
+ from chrys.app.tui.terminal.widget import Terminal
88
+ from chrys.app.tui.widgets.chat.messages import ConversationStatusAction
89
+ from chrys.app.tui.widgets.chat.panel import ChatPanel
90
+ from chrys.app.tui.widgets.chat.ports import TranscriptLocalizationPort
91
+ from chrys.app.tui.widgets.chat.renderers.ask_user import AskUserInlineSubmitted
92
+ from chrys.app.tui.widgets.chat.renderers.sleep import SleepSkipClicked
93
+ from chrys.app.tui.widgets.chat.renderers.sub_agent import SubAgentAbortClicked, SubAgentRetryClicked
94
+ from chrys.app.tui.widgets.chat.scroll_controller import scroll_gc_paused
95
+ from chrys.app.tui.widgets.chat.selection_controller import ChatSelectionController
96
+ from chrys.app.tui.widgets.chat.session_json import SessionJsonPanel
97
+ from chrys.app.tui.widgets.chat.tool_call import ToolViewRequested, is_tool_copy_excluded
98
+ from chrys.app.tui.widgets.chrome.app_header import AppHeader
99
+ from chrys.app.tui.widgets.chrome.commands import is_slash_command_candidate
100
+ from chrys.app.tui.widgets.chrome.footer import ChrysFooter
101
+ from chrys.app.tui.widgets.chrome.input_bar import InputBar
102
+ from chrys.app.tui.widgets.chrome.status_bar import StatusBar
103
+ from chrys.app.tui.widgets.chrome.suggestion_list import SuggestionList
104
+ from chrys.app.tui.widgets.editor import MESSAGE_EDITOR_MAX_CHARACTERS, EditorBufferSnapshot, EditorMode
105
+ from chrys.app.tui.widgets.sidebar.context import ContextUsageState
106
+ from chrys.app.tui.widgets.sidebar.panel import SidebarPanel
107
+ from chrys.app.tui.widgets.sidebar.tasks import TodoListState
108
+ from chrys.app.tui.widgets.sidebar.toc import ConversationToc
109
+ from chrys.app.tui.widgets.trajectory import TrajectoryDashboard
110
+ from chrys.app.tui.widgets.workflow import text as workflow_text
111
+ from chrys.app.tui.widgets.workflow.graph import WorkflowGraph
112
+ from chrys.app.tui.widgets.workflow.panel import WorkflowPanel
113
+ from chrys.foundation.config.settings import (
114
+ DEFAULT_LOCALE,
115
+ DEFAULT_TRAJECTORY_VERIFY_COMMANDS,
116
+ DEFAULT_WORKSPACE_MRU_MAX_ENTRIES,
117
+ persist_editor_keymap,
118
+ persist_tool_groups_expanded,
119
+ )
120
+ from chrys.foundation.events.types import (
121
+ ApprovalModeUpdated,
122
+ Error,
123
+ ExecutionChanged,
124
+ SessionRestored,
125
+ )
126
+ from chrys.foundation.i18n import DisplaySequence, Localizer, MessageRef, msg
127
+ from chrys.foundation.models.approval_reuse import ReuseChoice
128
+ from chrys.foundation.models.ask_user import AskUserAnswer
129
+ from chrys.foundation.platform import get_platform, safe_getcwd
130
+ from chrys.service.approval.policy import ApprovalMode
131
+ from chrys.service.profiles.models.schema import UNCONFIGURED_MODEL_ID, is_model_profile_selectable
132
+ from chrys.service.session.sub_agent_transcript import load_persisted_sub_agent_transcript
133
+
134
+ if TYPE_CHECKING:
135
+ from textual.app import ComposeResult
136
+ from textual.theme import Theme
137
+
138
+ from chrys.app.tui.app import ChrysApp
139
+ from chrys.app.tui.i18n import LocaleController
140
+ from chrys.app.tui.notifications import NotificationService
141
+ from chrys.app.tui.notifications.settings import NotificationSettings
142
+ from chrys.app.tui.screens.sessions import WorkflowSessionPick
143
+ from chrys.app.tui.screens.themes.picker import ThemesScreen
144
+ from chrys.app.tui.themes.store import ThemeFileRevision, UserThemeStore
145
+ from chrys.foundation.config.settings_store import PersistResult, SettingsHandle
146
+ from chrys.foundation.events.bus import EventBus
147
+ from chrys.orchestration.engine.engine import AgentEngine
148
+ from chrys.service.profiles.agents.registry import AgentProfileRegistry
149
+ from chrys.service.profiles.models.registry import ModelProfileRegistry
150
+ from chrys.service.session.sub_agent_transcript import PersistedSubAgentTranscript
151
+ from chrys.service.state.store import StateStore
152
+
153
+
154
+ logger = logging.getLogger(__name__)
155
+
156
+ type StartupRestore = Literal["restored", "declined", "failed"]
157
+ """Startup restore result; ``declined`` means the user chose no folder for a session whose folder is gone."""
158
+
159
+ _SESSIONS_BINDING = msg("tui.binding.sessions", fallback="Sessions")
160
+ _AGENTS_BINDING = msg("tui.binding.agents", fallback="Agents")
161
+ _MODELS_BINDING = msg("tui.binding.models", fallback="Models")
162
+ _LOGS_BINDING = msg("tui.binding.logs", fallback="Logs")
163
+ _HELP_BINDING = msg("tui.binding.help", fallback="Help")
164
+ _SIDEBAR_BINDING = msg("tui.binding.sidebar", fallback="Sidebar")
165
+ _THEMES_BINDING = msg("tui.binding.themes", fallback="Themes")
166
+ _SETTINGS_BINDING = msg("tui.binding.settings", fallback="Settings")
167
+ _SETTINGS_REJECTED = msg(
168
+ "tui.settings.save_rejected",
169
+ fallback="Could not save settings; the store rejected: {keys}.",
170
+ )
171
+ _BREAK_BINDING = msg("tui.binding.break_agent", fallback="Break")
172
+ _TRAJECTORY_BINDING = msg("tui.binding.trajectory", fallback="Trajectory")
173
+ _CHAT_PAGE_UP_BINDING = msg("tui.binding.chat_page_up", fallback="Chat Page Up")
174
+ _CHAT_PAGE_DOWN_BINDING = msg("tui.binding.chat_page_down", fallback="Chat Page Down")
175
+ _CHAT_SCROLL_BOTTOM_BINDING = msg("tui.binding.chat_scroll_bottom", fallback="Chat Scroll Bottom")
176
+ _EDITOR_DRAFT_TOO_LARGE_TITLE = msg(
177
+ "tui.editor.title.draft_too_large",
178
+ fallback="Draft too large for editor",
179
+ )
180
+ _EDITOR_DRAFT_TOO_LARGE = msg(
181
+ "tui.editor.draft_too_large",
182
+ fallback="Editor supports drafts up to {limit} characters.",
183
+ )
184
+ _MODEL_UNCONFIGURED_TITLE = msg("tui.model_guard.title", fallback="Model not configured or selected")
185
+ _MODEL_UNCONFIGURED_MESSAGE = msg(
186
+ "tui.model_guard.message",
187
+ fallback="Your message was not sent. Configure and select a model to get started.",
188
+ )
189
+ _MODEL_UNCONFIGURED_SETUP = msg("tui.model_guard.button.setup", fallback="Set up model")
190
+
191
+ TEXTUAL_BACKGROUND_REFRESH_FORK_VERSION = "8.2.7"
192
+ """The Textual release whose private ``Screen._compositor_refresh`` ``MainScreen`` mirrors."""
193
+
194
+
195
+ # Work arguments can be whole prompts; worker descriptions show them abbreviated.
196
+ _WORK_ARGUMENT_REPR = reprlib.Repr(maxstring=60, maxother=60)
197
+
198
+
199
+ def _describe_work(work: Callable[[], Awaitable[object]]) -> tuple[str, str]:
200
+ """Name and describe a worker after its work, as ``@work`` did: ``method(args)``."""
201
+ target: Callable[..., object] = work
202
+ args: tuple[object, ...] = ()
203
+ keywords: dict[str, object] = {}
204
+ if isinstance(work, partial):
205
+ target, args, keywords = work.func, work.args, work.keywords
206
+ name = target.__name__ if isinstance(target, FunctionType | MethodType) else type(target).__name__
207
+ arguments = [
208
+ *(_WORK_ARGUMENT_REPR.repr(arg) for arg in args),
209
+ *(f"{key}={_WORK_ARGUMENT_REPR.repr(value)}" for key, value in keywords.items()),
210
+ ]
211
+ return name, f"{name}({', '.join(arguments)})"
212
+
213
+
214
+ class MainScreen(RightClickScreenCopyMixin, Screen):
215
+ """Primary chat screen with header, chat panel, side panel, input bar, and footer."""
216
+
217
+ BINDINGS: ClassVar[list] = [
218
+ localized_binding("ctrl+b", "interrupt", _BREAK_BINDING, show=False),
219
+ localized_binding("f1", "sessions", _SESSIONS_BINDING, priority=True),
220
+ localized_binding("f2", "agents_config", _AGENTS_BINDING, priority=True),
221
+ localized_binding("f4", "models_config", _MODELS_BINDING, priority=True),
222
+ localized_binding("f6", "show_log_viewer", _LOGS_BINDING, priority=True),
223
+ localized_binding("f8", "open_guide", _HELP_BINDING, priority=True),
224
+ Binding("ctrl+r", "prompt_history", show=False, priority=True),
225
+ localized_binding("f9", "pick_theme", _THEMES_BINDING, priority=True),
226
+ localized_binding("f10", "settings", _SETTINGS_BINDING, priority=True),
227
+ localized_binding("f12", "toggle_trajectory_dashboard", _TRAJECTORY_BINDING, show=True, priority=True),
228
+ localized_binding("ctrl+g", "toggle_sidebar", _SIDEBAR_BINDING, priority=True),
229
+ localized_binding("pageup", "chat_page_up", _CHAT_PAGE_UP_BINDING, show=False, priority=True),
230
+ localized_binding("pagedown", "chat_page_down", _CHAT_PAGE_DOWN_BINDING, show=False, priority=True),
231
+ localized_binding(
232
+ "ctrl+end",
233
+ "chat_scroll_bottom",
234
+ _CHAT_SCROLL_BOTTOM_BINDING,
235
+ show=False,
236
+ priority=True,
237
+ ),
238
+ Binding("escape", "escape", show=False, priority=True),
239
+ localized_binding("ctrl+q", "quit", QUIT_BINDING, show=False, priority=True),
240
+ ]
241
+
242
+ CSS_PATH = "screen.tcss"
243
+
244
+ agent_running_state: reactive[bool] = reactive(False, init=False, always_update=True)
245
+ agent_loading_state: reactive[bool] = reactive(False, init=False, always_update=True)
246
+ has_messages_state: reactive[bool] = reactive(False, init=False, always_update=True)
247
+ header_subtitle_parts: reactive[tuple[str, ...]] = reactive(())
248
+ header_approval_mode: reactive[ApprovalMode] = reactive(ApprovalMode.MANUAL, always_update=True)
249
+ chat_profile_name: reactive[str] = reactive("")
250
+ chat_session_id: reactive[str] = reactive("")
251
+ chat_session_title: reactive[str] = reactive("")
252
+ chat_workspace_cwd: reactive[str] = reactive("")
253
+ chat_workspace_branch: reactive[str] = reactive("")
254
+ context_usage_state: reactive[ContextUsageState | None] = reactive(None, always_update=True)
255
+ todo_state: reactive[TodoListState | None] = reactive(None, always_update=True)
256
+ shell_mode_state: reactive[bool] = reactive(False, init=False)
257
+ _reactive_state_initialized: bool = False
258
+
259
+ def __init__(
260
+ self,
261
+ event_bus: EventBus,
262
+ state_store: StateStore | None = None,
263
+ agent_registry: AgentProfileRegistry | None = None,
264
+ model_registry: ModelProfileRegistry | None = None,
265
+ active_model_profile_id: str = "",
266
+ apply_saved_model_on_restore: bool = True,
267
+ workspace_mru_max_entries: int = DEFAULT_WORKSPACE_MRU_MAX_ENTRIES,
268
+ editor_keymap: EditorMode | str = EditorMode.STANDARD,
269
+ trajectory_verify_commands: str = DEFAULT_TRAJECTORY_VERIFY_COMMANDS,
270
+ *,
271
+ engine_provider: Callable[[], AgentEngine] | None,
272
+ settings_handle: SettingsHandle | None = None,
273
+ localization: TranscriptLocalizationPort | None = None,
274
+ locale_controller: LocaleController | None = None,
275
+ on_editor_keymap_changed: Callable[[str], None] | None = None,
276
+ tool_groups_expanded: Callable[[], bool] | None = None,
277
+ approval_mode: ApprovalMode = ApprovalMode.MANUAL,
278
+ ) -> None:
279
+ self._services = MainScreenServices(
280
+ bus=event_bus,
281
+ settings_handle=settings_handle,
282
+ state_store=state_store,
283
+ agent_registry=agent_registry,
284
+ model_registry=model_registry,
285
+ active_model_profile_id=active_model_profile_id,
286
+ apply_saved_model_on_restore=apply_saved_model_on_restore,
287
+ engine_provider=engine_provider,
288
+ workspace_mru_max_entries=workspace_mru_max_entries,
289
+ )
290
+ self._state = MainScreenState()
291
+ self._editor_mode = EditorMode.parse(editor_keymap)
292
+ self._trajectory_verify_commands = trajectory_verify_commands
293
+ self._on_editor_keymap_changed = on_editor_keymap_changed
294
+ self._tool_groups_expanded = tool_groups_expanded or (lambda: True)
295
+ self._localization = localization
296
+ self._locale_controller = locale_controller
297
+ self._gc_freeze_participants: tuple[GcFreezeParticipant, ...] = ()
298
+ self.theme_editor: ThemeEditorPanel | None = None
299
+ self._theme_editor_opening = False
300
+ self._shell_requested_after_editor = False
301
+ # Seeded with the engine's launch mode: the startup ApprovalModeUpdated
302
+ # sync then confirms what the header already shows, instead of
303
+ # flipping a placeholder and toasting a change nobody made.
304
+ self._state.runtime.approval_mode = approval_mode
305
+ self._settings_persistence_queue = self._new_settings_persistence()
306
+ super().__init__()
307
+ self.set_reactive(MainScreen.header_approval_mode, approval_mode)
308
+ self._reactive_state_initialized = True
309
+
310
+ # Live mutation tracking for /diff during agent runs.
311
+ self._live_diff = LiveDiffTracker()
312
+
313
+ # Handler instances
314
+ self._view_adapter = MainScreenViewAdapter(
315
+ self,
316
+ state=self._state,
317
+ state_store=self._services.state_store,
318
+ locale_controller=self._locale_controller,
319
+ )
320
+ self._session_title = SessionTitleController(
321
+ run=self._state.run,
322
+ app=lambda: self.app,
323
+ workspace_cwd=self._workspace_cwd,
324
+ show_display_title=self._show_session_title,
325
+ set_interval=self.set_interval,
326
+ current_session_id=lambda: self.chat_session_id,
327
+ push_screen=self._view_adapter.push_screen,
328
+ start_custom_title_save=lambda title, session_id: self._start_worker(
329
+ partial(self._sessions.apply_custom_session_title, title, session_id)
330
+ ),
331
+ locale_controller=self._locale_controller,
332
+ )
333
+ self._workspace_branch = WorkspaceBranchController(
334
+ workspace=self._state.workspace,
335
+ workspace_cwd=self._workspace_cwd,
336
+ displayed_cwd=lambda: self.chat_workspace_cwd or self._workspace_cwd(),
337
+ show_branch=self._show_workspace_branch,
338
+ set_timer=self.set_timer,
339
+ set_interval=self.set_interval,
340
+ call_from_thread=lambda callback: self.app.call_from_thread(callback), # noqa: PLW0108 — app bound later
341
+ )
342
+ self._runtime_info = RegistryRuntimeInfoProvider(self._services)
343
+ self._config_actions = RuntimeConfigController(
344
+ state=self._state,
345
+ services=self._services,
346
+ view=self._view_adapter,
347
+ callbacks=RuntimeConfigCallbacks(
348
+ start_worker=self._start_worker,
349
+ set_profile_display=self._set_profile_display,
350
+ update_subtitle=self._update_subtitle,
351
+ debug=self._debug,
352
+ notification_service=self._notification_service,
353
+ settings_coordinator=self._settings_coordinator,
354
+ ),
355
+ profile_descriptions=self._runtime_info,
356
+ locale_controller=self._locale_controller,
357
+ )
358
+ self._workspace_actions = WorkspaceController(
359
+ state=self._state,
360
+ services=self._services,
361
+ view=self._view_adapter,
362
+ callbacks=WorkspaceCallbacks(
363
+ start_worker=self._start_worker,
364
+ debug=self._debug,
365
+ allow_change=lambda: self._workflow.browser.request_workspace_change(), # noqa: PLW0108 — constructed later
366
+ selected_cwd=lambda: self._workflow.project_cwd if self._workflow.workflow_mode else "",
367
+ apply_workflow_cwd=self._apply_workflow_cwd,
368
+ workflow_mode=lambda: self._workflow.workflow_mode,
369
+ ),
370
+ locale_controller=self._locale_controller,
371
+ )
372
+ self._copy_actions = CopyActionController(view=self._view_adapter, debug=self._debug)
373
+ self._chat_selection = ChatSelectionController(self)
374
+ self._diff_controller = self._new_diff_controller()
375
+ self._rollback_controller = self._new_rollback_controller()
376
+ self._navigation = self._new_navigation_controller()
377
+ self._tool_actions = ToolActionBridge(publisher=self._services.bus, debug=self._debug)
378
+ self._events = BackendEventHandler(
379
+ state=self._state,
380
+ services=self._services,
381
+ view=self._view_adapter,
382
+ callbacks=BackendEventCallbacks(
383
+ accept_approval_update=self._accept_approval_update,
384
+ route_session_error=self._route_session_error,
385
+ prompt_missing_working_dir=self._workspace_actions.prompt_missing_working_dir,
386
+ set_agent_running=self._set_agent_running,
387
+ set_agent_loading=self._set_agent_loading,
388
+ set_has_messages=self._set_has_messages,
389
+ set_profile_display=self._set_profile_display,
390
+ set_active_model_profile_id=self._set_active_model_profile_id,
391
+ set_creating_new_session=self._set_creating_new_session,
392
+ set_restoring_session=self._set_restoring_session,
393
+ set_workspace_cwd=self._set_workspace_cwd,
394
+ refresh_git_branch=self._workspace_branch.schedule_refresh,
395
+ update_subtitle=self._update_subtitle,
396
+ update_toc=self._update_toc,
397
+ on_session_fork_error=lambda event, message, severity: self._sessions.on_session_fork_error(
398
+ event,
399
+ message=message,
400
+ severity=severity,
401
+ ),
402
+ on_session_clear_error=lambda event, message: self._sessions.on_session_clear_error(
403
+ event,
404
+ message=message,
405
+ ),
406
+ handle_approval_response=self._handle_approval_response,
407
+ handle_ask_user_response=self._handle_ask_user_response,
408
+ question_inline_preferred=self._question_inline_preferred,
409
+ approval_defer_while_judging=self._approval_defer_while_judging,
410
+ post_gc_message=self.post_message,
411
+ debug=self._debug,
412
+ refresh_model_indicator=self._refresh_model_indicator,
413
+ refresh_notification_settings=self._refresh_notification_settings,
414
+ refresh_trajectory_verify_commands=self._refresh_trajectory_verify_commands,
415
+ settings_reloaded=self._on_settings_reloaded_for_panel,
416
+ ),
417
+ runtime_info=self._runtime_info,
418
+ live_diff=self._live_diff,
419
+ locale_controller=self._locale_controller,
420
+ )
421
+ self._sessions = SessionHandler(
422
+ state=self._state,
423
+ services=self._services,
424
+ view=self._view_adapter,
425
+ callbacks=SessionCallbacks(
426
+ set_agent_loading=self._set_agent_loading,
427
+ set_has_messages=self._set_has_messages,
428
+ set_creating_new_session=self._set_creating_new_session,
429
+ set_restoring_session=self._set_restoring_session,
430
+ set_profile_display=self._set_profile_display,
431
+ set_active_model_profile_id=self._set_active_model_profile_id,
432
+ set_workspace_cwd=self._set_workspace_cwd,
433
+ update_subtitle=self._update_subtitle,
434
+ update_toc=self._update_toc,
435
+ clear_suggestion_file_cache=self._clear_suggestion_file_cache,
436
+ start_worker=self._start_worker,
437
+ post_gc_message=self.post_message,
438
+ debug=self._debug,
439
+ refresh_model_indicator=self._refresh_model_indicator,
440
+ choose_missing_working_dir=partial(self._workspace_actions.choose_replacement_dir, reason="restore"),
441
+ ),
442
+ agent_load=self._events,
443
+ runtime_info=self._runtime_info,
444
+ profile_descriptions=self._runtime_info,
445
+ locale_controller=self._locale_controller,
446
+ )
447
+ self._slash_actions = self._new_slash_command_actions()
448
+ self._suggestions = SuggestionHandler(
449
+ state=self._state,
450
+ services=self._services,
451
+ view=self._view_adapter,
452
+ command_actions=self._slash_actions,
453
+ callbacks=SuggestionCallbacks(
454
+ notify_warning=self._warn_slash_command,
455
+ start_worker=self._start_worker,
456
+ submit_user_text=self._submit_user_text,
457
+ start_agent_profile_switch=self._config_actions.start_agent_profile_switch,
458
+ start_model_profile_switch=self._switch_model_profile,
459
+ ),
460
+ buddy_view=self._view_adapter,
461
+ locale_controller=self._locale_controller,
462
+ )
463
+ self._shell_mode_controller = ShellModeController(
464
+ state=self._state,
465
+ shell_view=self._view_adapter,
466
+ focus_view=self._view_adapter,
467
+ set_shell_mode_state=self._set_shell_mode_state,
468
+ panel_focus_changed=self._sync_sidebar_tab_strip_focus,
469
+ dismiss_suggestions=self._suggestions.dismiss_suggestions,
470
+ debug=self._debug,
471
+ )
472
+ self._input_flow = InputFlowController(
473
+ state=self._state,
474
+ services=self._services,
475
+ view=self._view_adapter,
476
+ start_worker=self._start_worker,
477
+ handle_agent_message=self._events.on_agent_message,
478
+ handle_error=self._events.on_error,
479
+ set_agent_running=self._set_agent_running,
480
+ set_has_messages=self._set_has_messages,
481
+ clear_workspace_marker=self._clear_workspace_marker_for_user_message,
482
+ clear_pending_questions=self._events.clear_pending_questions,
483
+ commit_profile_switch_marker=self._commit_profile_switch_marker,
484
+ post_gc_message=self.post_message,
485
+ debug=self._debug,
486
+ )
487
+ self._subscriptions = MainScreenSubscriptions(
488
+ bus=self._services.bus,
489
+ events=self._events,
490
+ sessions=self._sessions,
491
+ rollback_result_handler=self._rollback_controller.on_result,
492
+ execution_handler=self._on_execution_changed,
493
+ )
494
+ self._workflow_panel = WorkflowPanel(locale_controller=self._locale_controller)
495
+ self._workflow = WorkflowController(
496
+ services=self._services,
497
+ panel=self._workflow_panel,
498
+ cwd=self._workspace_cwd,
499
+ # Keep projection callbacks on MainScreen's pump. call_after_refresh
500
+ # forwards to the active modal, which can close with a refresh still queued.
501
+ schedule=self.call_later,
502
+ sync_chrome=self._sync_workflow_chrome,
503
+ locale_controller=self._locale_controller,
504
+ )
505
+ self._workflow_timer: Timer | None = None
506
+ self._trajectory_binding_available = True
507
+ self._execution_binding_busy = False
508
+ self.chat_workspace_cwd = self._workspace_cwd()
509
+
510
+ def _new_settings_persistence(self) -> SettingsPersistenceQueue:
511
+ return SettingsPersistenceQueue(
512
+ notify_failure=self._notify_settings_failure,
513
+ notify_rejected=self._notify_settings_rejected,
514
+ logger=logger,
515
+ on_written=self._on_settings_written,
516
+ save_delay_seconds=SETTINGS_SAVE_DELAY_SECONDS,
517
+ flush_lock_timeout_seconds=SETTINGS_FLUSH_LOCK_TIMEOUT_SECONDS,
518
+ )
519
+
520
+ def _settings_persistence(self) -> SettingsPersistenceQueue:
521
+ """The one debounced writer for every panel-owned setting, built on first use."""
522
+ persistence = self.__dict__.get("_settings_persistence_queue")
523
+ if not isinstance(persistence, SettingsPersistenceQueue):
524
+ persistence = self._new_settings_persistence()
525
+ self.__dict__["_settings_persistence_queue"] = persistence
526
+ return persistence
527
+
528
+ def _settings_coordinator(self) -> SettingsCoordinator:
529
+ """Process-level state behind the Settings dialog, built on first use.
530
+
531
+ Lazy for the same reason as the queue: most sessions never open the
532
+ panel. The RESTART baseline it captures is the same whenever it is
533
+ taken — those values are held in force for the life of the process.
534
+ """
535
+ coordinator = self.__dict__.get("_settings_coordinator_instance")
536
+ if not isinstance(coordinator, SettingsCoordinator):
537
+ coordinator = self._new_settings_coordinator()
538
+ self.__dict__["_settings_coordinator_instance"] = coordinator
539
+ return coordinator
540
+
541
+ def _switch_locale_for_panel(self, requested_locale: str) -> None:
542
+ """The panel's locale row goes through the same action as the picker and
543
+ ``/language``: a bundle that fails to load is reported the same way, and
544
+ the coordinator then reprojects the row from what is still in force."""
545
+ self._navigation.set_language(requested_locale)
546
+
547
+ def _apply_trajectory_verify_commands(self, value: str) -> None:
548
+ app = cast("ChrysApp", self.app)
549
+ app.settings_handle.override(trajectory_verify_commands=value)
550
+ self._trajectory_verify_commands = value
551
+ self.query_one(TrajectoryDashboard).set_verify_commands(value)
552
+
553
+ def _apply_tool_groups_expanded(self, value: bool) -> None:
554
+ """Persist the live choice; existing groups deliberately keep their state."""
555
+ persist_tool_groups_expanded(value)
556
+ cast("ChrysApp", self.app).settings_handle.override(tool_groups_expanded=value)
557
+
558
+ def _apply_approval_defer_while_judging(self, value: bool) -> None:
559
+ """Put the choice in force; requests already waiting keep the choice they arrived under."""
560
+ cast("ChrysApp", self.app).settings_handle.override(approval_defer_while_judging=value)
561
+
562
+ def _approval_defer_while_judging(self) -> bool:
563
+ return cast("ChrysApp", self.app).settings_handle.settings.approval_defer_while_judging
564
+
565
+ def _refresh_trajectory_verify_commands(self) -> None:
566
+ """Re-project the word list a reload only installed into the handle.
567
+
568
+ Reads the effective value rather than routing through the panel apply:
569
+ recording a reload as an override would credit it to the runtime layer
570
+ and pin it over every later reload.
571
+ """
572
+ value = cast("ChrysApp", self.app).settings_handle.settings.trajectory_verify_commands
573
+ if value == self._trajectory_verify_commands:
574
+ return
575
+ self._trajectory_verify_commands = value
576
+ self.query_one(TrajectoryDashboard).set_verify_commands(value)
577
+
578
+ def _new_settings_coordinator(self) -> SettingsCoordinator:
579
+ app = cast("ChrysApp", self.app)
580
+
581
+ return SettingsCoordinator(
582
+ services=self._services,
583
+ settings_handle=app.settings_handle,
584
+ queue=self._settings_persistence(),
585
+ view=self._view_adapter,
586
+ locale_controller=self._locale_controller,
587
+ callbacks=SettingsCoordinatorCallbacks(
588
+ apply_theme=app.apply_theme_setting,
589
+ switch_locale=self._switch_locale_for_panel,
590
+ apply_trajectory_verify_commands=self._apply_trajectory_verify_commands,
591
+ apply_tool_groups_expanded=self._apply_tool_groups_expanded,
592
+ apply_approval_defer_while_judging=self._apply_approval_defer_while_judging,
593
+ list_themes=lambda: sorted(app.available_themes),
594
+ save_notifications=self._schedule_notification_settings_save,
595
+ notification_service=self._notification_service,
596
+ turn_lifecycle_task=self._services.turn_lifecycle_task,
597
+ turn_in_progress=lambda: self._state.run.agent_running or self._services.execution_busy(),
598
+ ),
599
+ )
600
+
601
+ def _existing_settings_coordinator(self) -> SettingsCoordinator | None:
602
+ """The coordinator if the panel has ever been opened; never builds one.
603
+
604
+ Every queue write originates from the panel, so a write outcome or a
605
+ reload arriving before the coordinator exists has nothing to update.
606
+ """
607
+ coordinator = self.__dict__.get("_settings_coordinator_instance")
608
+ return coordinator if isinstance(coordinator, SettingsCoordinator) else None
609
+
610
+ def _on_settings_written(self, result: PersistResult) -> None:
611
+ coordinator = self._existing_settings_coordinator()
612
+ if coordinator is not None:
613
+ coordinator.on_written(result)
614
+
615
+ def _on_settings_reloaded_for_panel(self) -> None:
616
+ coordinator = self._existing_settings_coordinator()
617
+ if coordinator is not None:
618
+ coordinator.on_reloaded()
619
+
620
+ def _notify_settings_failure(self, exc: Exception) -> None:
621
+ self._view_adapter.notify(str(exc), title=SETTINGS_TITLE.bind(), severity="error", timeout=5)
622
+ coordinator = self._existing_settings_coordinator()
623
+ if coordinator is not None:
624
+ coordinator.on_write_failed()
625
+
626
+ def _notify_settings_rejected(self, result: PersistResult) -> None:
627
+ message = _SETTINGS_REJECTED.bind(keys=DisplaySequence(tuple(result.rejected)))
628
+ self._view_adapter.notify(message, title=SETTINGS_TITLE.bind(), severity="error", timeout=5)
629
+ coordinator = self._existing_settings_coordinator()
630
+ if coordinator is not None:
631
+ coordinator.on_write_failed()
632
+
633
+ def _workspace_cwd(self) -> str:
634
+ """Return the TUI-tracked workspace cwd."""
635
+ return self._state.workspace_marker.current_cwd or safe_getcwd()
636
+
637
+ def _notification_service(self) -> NotificationService:
638
+ return cast("ChrysApp", self.app).notification_service
639
+
640
+ def _refresh_notification_settings(self) -> None:
641
+ cast("ChrysApp", self.app).refresh_notification_settings()
642
+
643
+ def _set_active_model_profile_id(self, profile_id: str) -> None:
644
+ self._services.active_model_profile_id = profile_id
645
+
646
+ def _set_profile_display(self, profile: str) -> None:
647
+ self._state.runtime.profile = profile
648
+ self.chat_profile_name = profile
649
+
650
+ def _show_session_title(self, title: str) -> None:
651
+ # ``chat_session_title`` is data-bound to the ChatPanel border.
652
+ self.chat_session_title = title
653
+
654
+ def _set_workspace_cwd(self, cwd: str) -> None:
655
+ self._state.workspace.current_cwd = cwd
656
+ self._state.workspace_marker.current_cwd = cwd
657
+ self._workspace_branch.configure(cwd)
658
+
659
+ def _show_workspace_branch(self, branch: str) -> None:
660
+ self.chat_workspace_branch = branch
661
+ with contextlib.suppress(Exception):
662
+ self.query_one(ChatPanel).set_workspace_branch(branch)
663
+
664
+ def _set_creating_new_session(self, creating: bool) -> None:
665
+ self._state.session.creating_new_session = creating
666
+ if creating:
667
+ self._session_title.clear_terminal_title_result()
668
+
669
+ def _set_restoring_session(self, restoring: bool) -> None:
670
+ self._state.session.restoring_session = restoring
671
+ if restoring:
672
+ self._session_title.clear_terminal_title_result()
673
+
674
+ def _clear_suggestion_file_cache(self) -> None:
675
+ self._suggestions.file_cache = None
676
+
677
+ def _warn_slash_command(
678
+ self,
679
+ message: StatusMessage,
680
+ title: StatusMessage = INVALID_COMMAND_TITLE_REF,
681
+ timeout: float | None = 3,
682
+ ) -> None:
683
+ self._view_adapter.notify(message, title=title, severity="warning", timeout=timeout)
684
+
685
+ def _language_localizer(self) -> Localizer:
686
+ controller = self._locale_controller
687
+ return Localizer(DEFAULT_LOCALE) if controller is None else controller.localizer
688
+
689
+ def _has_selectable_model_profile(self) -> bool:
690
+ """Return whether the registry offers at least one selectable model profile."""
691
+ registry = self._services.model_registry
692
+ return registry is not None and any(
693
+ is_model_profile_selectable(profile) for profile in registry.list_profiles()
694
+ )
695
+
696
+ def _refresh_model_indicator(self) -> None:
697
+ """Recompute the model tag from registry availability and confirmed runtime state."""
698
+ details = self._state.runtime.details.model if self._state.runtime.details_confirmed else None
699
+ state = compute_model_indicator_state(
700
+ details,
701
+ self._has_selectable_model_profile(),
702
+ self._state.runtime.profile,
703
+ self._language_localizer(),
704
+ runtime_confirmed=self._state.runtime.details_confirmed,
705
+ )
706
+ self.query_one(StatusBar).set_model(state)
707
+
708
+ def refresh_localization(self) -> None:
709
+ """Recompute screen-owned localized chrome text in place."""
710
+ self._refresh_model_indicator()
711
+ if self._workflow.workflow_mode:
712
+ self._workflow.request_refresh()
713
+
714
+ def _available_languages(self) -> list[tuple[str, Content]]:
715
+ localizer = self._language_localizer()
716
+ return [
717
+ (requested_locale, render_content(localizer, definition.bind()))
718
+ for requested_locale, definition in LANGUAGE_OPTIONS
719
+ ]
720
+
721
+ def _current_language(self) -> str:
722
+ controller = self._locale_controller
723
+ return DEFAULT_LOCALE if controller is None else controller.requested_locale
724
+
725
+ def _unknown_language_warning(self, requested_locale: str) -> str:
726
+ return render_str(
727
+ self._language_localizer(),
728
+ LANGUAGE_UNKNOWN_LOCALE.bind(locale=requested_locale),
729
+ )
730
+
731
+ def _new_slash_command_actions(self) -> SlashCommandActions:
732
+ return SlashCommandActions(
733
+ list_themes=lambda: sorted(self.app.available_themes),
734
+ get_theme=lambda: self.app.theme,
735
+ apply_theme=self._view_adapter.apply_theme_setting,
736
+ pick_theme=self.action_pick_theme,
737
+ list_languages=self._available_languages,
738
+ get_language=self._current_language,
739
+ apply_language=self.action_set_language,
740
+ pick_language=self.action_pick_language,
741
+ render_unknown_language_warning=self._unknown_language_warning,
742
+ debug_event=self._debug,
743
+ new_session=self._create_new_session,
744
+ clear_session=self._clear_current_session,
745
+ quit_app=self.action_quit,
746
+ resume_session=self._resume_last_session,
747
+ fork_session=self._fork_current_session,
748
+ browse_session_list=self.action_sessions,
749
+ workflow_selection=self.action_workflow,
750
+ open_guide=self._navigation.open_guide,
751
+ edit_session_title=self._session_title.open_editor,
752
+ apply_session_title=self._session_title.apply_custom_title,
753
+ change_directory=self._workspace_actions.start_chdir,
754
+ copy_conversation=self._copy_agent_responses,
755
+ fold_tools=self._toggle_fold,
756
+ open_diff=self.action_show_diff,
757
+ open_rollback=self.action_show_rollback,
758
+ get_approval_mode=lambda: self._state.runtime.approval_mode.value,
759
+ change_approval_mode=self._config_actions.start_approval_mode_change,
760
+ configure_model=self._open_model_config,
761
+ configure_agent=self._open_agent_config,
762
+ configure_agent_tab=self._open_agent_config_tab,
763
+ show_runtime_details=self.action_runtime_details,
764
+ configure_settings=self._open_settings,
765
+ show_manual_pages=lambda pages, start_index: self._view_adapter.show_man_pages(
766
+ pages,
767
+ start_index=start_index,
768
+ ),
769
+ warn=self._warn_slash_command,
770
+ )
771
+
772
+ def _new_diff_controller(self) -> DiffController:
773
+ return DiffController(
774
+ services=self._services,
775
+ live_diff=self._live_diff,
776
+ view=self._view_adapter,
777
+ workspace_cwd=self._workspace_cwd,
778
+ is_agent_running=lambda: self._state.run.agent_running,
779
+ run_generation=lambda: self._state.run.generation,
780
+ session_generation=self._services.session_generation,
781
+ turn_lifecycle_task=self._services.turn_lifecycle_task,
782
+ turn_lifecycle_saved=self._services.was_turn_lifecycle_saved,
783
+ )
784
+
785
+ def _new_rollback_controller(self) -> RollbackController:
786
+ def _reset_welcome_workspace_marker(cwd: str) -> None:
787
+ self._state.workspace_marker.original_cwd = None
788
+ self._set_workspace_cwd(cwd)
789
+
790
+ return RollbackController(
791
+ services=self._services,
792
+ view=self._view_adapter,
793
+ workspace_cwd=self._workspace_cwd,
794
+ is_agent_busy=lambda: (
795
+ self._state.run.agent_running or self._state.run.agent_loading or self._services.execution_busy()
796
+ ),
797
+ current_session_id=self._view_adapter.current_chat_session_id,
798
+ session_generation=self._services.session_generation,
799
+ turn_lifecycle_task=self._services.turn_lifecycle_task,
800
+ profile_name=lambda: self._state.runtime.profile,
801
+ reset_welcome_workspace_marker=_reset_welcome_workspace_marker,
802
+ set_has_messages=self._set_has_messages,
803
+ post_gc_message=self.post_message,
804
+ debug=self._debug,
805
+ locale_controller=self._locale_controller,
806
+ )
807
+
808
+ def _set_interrupt_confirm_active(self, active: bool) -> None:
809
+ self._state.overlays.interrupt_confirm_active = active
810
+
811
+ def _new_navigation_controller(self) -> MainNavigationController:
812
+ async def _flush_notifications() -> None:
813
+ await self._flush_settings_save()
814
+
815
+ async def _delete_current_and_new(session_id: str) -> None:
816
+ await self._delete_session_and_new(session_id)
817
+
818
+ async def _restore_session(session_id: str | WorkflowSessionPick) -> None:
819
+ await self._sessions.do_session_restore(session_id)
820
+
821
+ def _cancel_pending_injection() -> bool:
822
+ # Deferred attribute lookup: the navigation controller is built
823
+ # before the input-flow controller during screen construction.
824
+ return self._input_flow.cancel_pending_injection()
825
+
826
+ def _dismiss_suggestions() -> bool:
827
+ if not self._suggestions_active_for_bindings():
828
+ return False
829
+ self._suggestions.dismiss_suggestions()
830
+ return True
831
+
832
+ return MainNavigationController(
833
+ services=self._services,
834
+ view=self._view_adapter,
835
+ is_agent_loading=lambda: self._state.run.agent_loading,
836
+ is_agent_running=lambda: self._state.run.agent_running or self._services.execution_busy(),
837
+ is_submit_pending=lambda: self._state.submit.active or bool(self._workflow.awaiting_engine),
838
+ is_workflow_mode=lambda: self._workflow.workflow_mode,
839
+ has_messages=lambda: self._state.run.has_messages,
840
+ is_dashboard_visible=self._dashboard_visible,
841
+ set_interrupt_confirm_active=lambda active: MainScreen._set_interrupt_confirm_active(self, active),
842
+ publish_interrupt=lambda: MainScreen._publish_interrupt(self),
843
+ dismiss_suggestions=_dismiss_suggestions,
844
+ cancel_pending_injection=_cancel_pending_injection,
845
+ delete_current_and_new=_delete_current_and_new,
846
+ restore_session=_restore_session,
847
+ flush_notifications=_flush_notifications,
848
+ start_worker=self._start_worker,
849
+ debug=self._debug,
850
+ locale_controller=self._locale_controller,
851
+ )
852
+
853
+ def _start_worker(self, work: Callable[[], Awaitable[object]]) -> object:
854
+ """Run *work* in a worker on this screen's event loop.
855
+
856
+ As with ``@work``, the worker is named after its work, and the
857
+ coroutine is made only when the worker starts, so a worker cancelled
858
+ before it runs leaves nothing unawaited.
859
+ """
860
+
861
+ async def run() -> object:
862
+ return await work()
863
+
864
+ name, description = _describe_work(work)
865
+ return self.run_worker(run, name=name, description=description, thread=False)
866
+
867
+ def set_startup_agent_loading(self, active: bool) -> None:
868
+ """Startup-facing facade for setting agent loading state."""
869
+ self._set_agent_loading(active)
870
+
871
+ def is_startup_agent_loading(self) -> bool:
872
+ """Return whether startup still considers the agent loading."""
873
+ return self._state.run.agent_loading
874
+
875
+ def gc_freeze_block_reason(self) -> GcFreezeBlockReason | None:
876
+ """Return the first active MainScreen or participant hard gate."""
877
+ if self._state.run.agent_loading:
878
+ return GcFreezeBlockReason.AGENT_LOADING
879
+ if self._state.run.agent_running:
880
+ return GcFreezeBlockReason.AGENT_RUNNING
881
+ if scroll_gc_paused():
882
+ return GcFreezeBlockReason.SCROLL_GC_PAUSED
883
+ for participant in self._gc_freeze_participants:
884
+ if (reason := participant.gc_freeze_block_reason()) is not None:
885
+ return reason
886
+ return None
887
+
888
+ def prepare_for_gc_freeze(self) -> None:
889
+ """Synchronously prepare every registered freeze participant."""
890
+ for participant in self._gc_freeze_participants:
891
+ participant.prepare_for_gc_freeze()
892
+ prepare_textual_screen_for_gc(self)
893
+
894
+ def after_gc_freeze(self) -> None:
895
+ """Synchronously renew all caches, continuing past individual failures."""
896
+ errors: list[Exception] = []
897
+ try:
898
+ after_textual_screen_gc_freeze(self)
899
+ except Exception as error:
900
+ errors.append(error)
901
+ for participant in self._gc_freeze_participants:
902
+ try:
903
+ participant.after_gc_freeze()
904
+ except Exception as error:
905
+ errors.append(error)
906
+ raise_gc_freeze_hook_errors("MainScreen GC-freeze renewal failed", errors)
907
+
908
+ def abort_gc_freeze(self) -> None:
909
+ """Best-effort restore every cache after an incomplete prepare/after pass."""
910
+ abort_textual_screen_gc_freeze(self)
911
+ for participant in self._gc_freeze_participants:
912
+ try:
913
+ participant.abort_gc_freeze()
914
+ except Exception:
915
+ logger.exception("Failed to restore GC-freeze participant %s", type(participant).__name__)
916
+
917
+ async def restore_startup_session(self, session_id: str) -> StartupRestore:
918
+ """Restore at startup and confirm the matching backend success event."""
919
+ restored = False
920
+
921
+ async def observe_restored(event: SessionRestored) -> None:
922
+ nonlocal restored
923
+ if event.session_id == session_id:
924
+ restored = True
925
+
926
+ await self._services.bus.subscribe(SessionRestored, observe_restored)
927
+ try:
928
+ request = await self._sessions.do_session_restore(session_id, allow_while_loading=True)
929
+ finally:
930
+ await self._services.bus.unsubscribe(SessionRestored, observe_restored)
931
+ if restored:
932
+ return "restored"
933
+ return "declined" if request is RestoreRequest.DECLINED else "failed"
934
+
935
+ def cancel_startup_session_restore(self) -> None:
936
+ """Close restore loading UI before falling back to a fresh runtime."""
937
+ self._events.cancel_agent_load()
938
+ self._set_restoring_session(False)
939
+
940
+ async def dismiss_startup_load_dialog_before_restore(self) -> None:
941
+ """Dismiss the startup load modal before opening restore-specific loading UI."""
942
+ from chrys.app.tui.screens.dialogs.agent_load import AgentLoadDialog
943
+
944
+ if self._events._agent_load_dialog is not None:
945
+ with contextlib.suppress(Exception):
946
+ self._events.cancel_agent_load()
947
+ startup_modals: list[AgentLoadDialog] = []
948
+ with contextlib.suppress(Exception):
949
+ startup_modals = [modal for modal in list(self.app.screen_stack) if isinstance(modal, AgentLoadDialog)]
950
+ for modal in startup_modals:
951
+ with contextlib.suppress(Exception):
952
+ modal.request_dismiss()
953
+ await asyncio.sleep(0)
954
+
955
+ def check_action(self, action: str, parameters: tuple[object, ...]) -> bool | None:
956
+ """Conditionally show/hide bindings."""
957
+ if action == "toggle_trajectory_dashboard" and not self._navigation.can_toggle_trajectory_dashboard():
958
+ return False
959
+ suggestions_active = (
960
+ self._suggestions_active_for_bindings()
961
+ if action in {"chat_page_up", "chat_page_down", "chat_scroll_bottom"}
962
+ else False
963
+ )
964
+ return check_main_action(
965
+ action,
966
+ fullscreen_terminal=self._state.shell.fullscreen_terminal,
967
+ shell_mode=self._state.shell.active,
968
+ chat_foreground=self._chat_foreground(),
969
+ suggestions_active=suggestions_active,
970
+ agent_running=self._state.run.agent_running or self._services.execution_busy(),
971
+ )
972
+
973
+ def _dashboard_visible(self) -> bool:
974
+ return self.query_one(TrajectoryDashboard).foreground
975
+
976
+ def _chat_foreground(self) -> bool:
977
+ return not self._workflow.workflow_mode and not self._dashboard_visible()
978
+
979
+ def _suggestions_active_for_bindings(self) -> bool:
980
+ """Return whether the input suggestion popup is active."""
981
+ with contextlib.suppress(Exception):
982
+ return bool(self.query_one(InputBar).suggestions_active)
983
+ return False
984
+
985
+ def compose(self) -> ComposeResult:
986
+ yield AppHeader(locale_controller=self._locale_controller, show_mode_badge=True).data_bind(
987
+ subtitle_parts=MainScreen.header_subtitle_parts,
988
+ approval_mode=MainScreen.header_approval_mode,
989
+ )
990
+ with Horizontal():
991
+ yield ChatPanel(
992
+ cwd=self._workspace_cwd(),
993
+ localization=self._localization,
994
+ locale_controller=self._locale_controller,
995
+ tool_groups_expanded=self._tool_groups_expanded,
996
+ sub_agent_transcript_loader=self._load_sub_agent_transcript,
997
+ ).data_bind(
998
+ agent_running=MainScreen.agent_running_state,
999
+ profile_name=MainScreen.chat_profile_name,
1000
+ session_id_value=MainScreen.chat_session_id,
1001
+ session_title_value=MainScreen.chat_session_title,
1002
+ workspace_cwd=MainScreen.chat_workspace_cwd,
1003
+ workspace_branch=MainScreen.chat_workspace_branch,
1004
+ )
1005
+ yield self._workflow_panel
1006
+ yield TrajectoryDashboard(
1007
+ locale_controller=self._locale_controller,
1008
+ verify_commands=self._trajectory_verify_commands,
1009
+ )
1010
+ yield ShellPanel(working_directory=self._workspace_cwd())
1011
+ yield SidebarPanel(locale_controller=self._locale_controller).data_bind(
1012
+ context_usage_state=MainScreen.context_usage_state,
1013
+ todo_state=MainScreen.todo_state,
1014
+ )
1015
+ yield StatusBar(locale_controller=self._locale_controller).data_bind(
1016
+ agent_running=MainScreen.agent_running_state,
1017
+ agent_loading=MainScreen.agent_loading_state,
1018
+ shell_mode=MainScreen.shell_mode_state,
1019
+ )
1020
+ # Anchor the screen overlay at InputBar: its upward offset keeps the
1021
+ # popup adjacent to input and paints over the reserved status row
1022
+ # without changing transcript geometry while suggestions are filtered.
1023
+ yield SuggestionList(locale_controller=self._locale_controller)
1024
+ yield InputBar(locale_controller=self._locale_controller).data_bind(
1025
+ agent_running=MainScreen.agent_running_state,
1026
+ agent_loading=MainScreen.agent_loading_state,
1027
+ has_messages=MainScreen.has_messages_state,
1028
+ shell_mode=MainScreen.shell_mode_state,
1029
+ )
1030
+ yield ChrysFooter(locale_controller=self._locale_controller)
1031
+
1032
+ async def _load_sub_agent_transcript(self, log_file: str) -> PersistedSubAgentTranscript | None:
1033
+ """Load a transcript relative to the currently displayed session."""
1034
+ session_id = self.chat_session_id
1035
+ if self._services.state_store is None or not session_id:
1036
+ return None
1037
+ return await load_persisted_sub_agent_transcript(self._services.state_store.session_dir(session_id), log_file)
1038
+
1039
+ def sync_footer_bindings(self) -> None:
1040
+ """Refresh visible Footer content only when its signature changed."""
1041
+ self.query_one(ChrysFooter).sync_bindings()
1042
+
1043
+ def on_screen_resume(self, _event: ScreenResume) -> None:
1044
+ """Recover deferred workflow and Footer updates after an overlay closes."""
1045
+ if self._workflow.workflow_mode:
1046
+ self._workflow.shown()
1047
+ self._sync_workflow_timer()
1048
+ self.query_one(InputBar).sync_deferred_button_geometry()
1049
+ self.sync_footer_bindings()
1050
+ self.query_one(SuggestionList).resume_marquee()
1051
+ if self.theme_editor is not None:
1052
+ self.theme_editor.resume()
1053
+ self._resume_shell_request()
1054
+
1055
+ def on_resize(self, event: Resize) -> None:
1056
+ self._set_theme_editor_compact(self.theme_editor is not None and event.size.width < 128)
1057
+
1058
+ def _set_theme_editor_compact(self, compact: bool) -> None:
1059
+ if self.has_class("--theme-editor-compact") == compact:
1060
+ return
1061
+ sidebar = self.query_one(SidebarPanel)
1062
+ sidebar.suppress_visibility("theme-editor", compact)
1063
+ # Only these panel roots and the editor use the compact selectors.
1064
+ # Updating MainScreen recursively would restyle the whole transcript.
1065
+ self.update_classes({"--theme-editor-compact": compact}, update=False)
1066
+ self.app.stylesheet.update_nodes((self.query_one(ChatPanel),))
1067
+ if self.theme_editor is not None and self.theme_editor.is_mounted:
1068
+ self.theme_editor.update_node_styles(animate=False)
1069
+
1070
+ def on_screen_suspend(self, _event: ScreenSuspend) -> None:
1071
+ """Stop suggestion animation while an overlay covers this screen."""
1072
+ self.query_one(SuggestionList).pause_marquee()
1073
+ if self._workflow_timer is not None:
1074
+ self._workflow_timer.stop()
1075
+ self._workflow_timer = None
1076
+
1077
+ def _update_subtitle(self) -> None:
1078
+ """Update header to reflect platform info."""
1079
+ platform_label = get_platform().display_label
1080
+ self.header_subtitle_parts = (platform_label,) if platform_label else ()
1081
+
1082
+ def watch_agent_running_state(self, running: bool) -> None:
1083
+ self._apply_agent_running_state(running)
1084
+
1085
+ def watch_agent_loading_state(self, loading: bool) -> None:
1086
+ self._apply_agent_loading_state(loading)
1087
+
1088
+ def watch_has_messages_state(self, has_messages: bool) -> None:
1089
+ self._state.run.has_messages = has_messages
1090
+
1091
+ def watch_header_approval_mode(self, mode: ApprovalMode) -> None:
1092
+ self._state.runtime.approval_mode = mode
1093
+
1094
+ def watch_chat_workspace_cwd(self, old_cwd: str, cwd: str) -> None:
1095
+ self._workspace_branch.displayed_cwd_changed(old_cwd, cwd)
1096
+
1097
+ def watch_shell_mode_state(self, active: bool) -> None:
1098
+ self._shell_mode_controller.apply(active)
1099
+
1100
+ def _reactive_state_ready(self) -> bool:
1101
+ return isinstance(self, MainScreen) and self._reactive_state_initialized
1102
+
1103
+ def get_widget_and_offset_at(self, x: int, y: int) -> tuple[Widget | None, Offset | None]:
1104
+ """Return no selectable target for tool-renderer UI.
1105
+
1106
+ Textual uses this method to start and update text selection. Tool
1107
+ renderers are compact status UI in the chat transcript; their details
1108
+ will get a separate copy path when expanded details mode lands.
1109
+ """
1110
+ widget, offset = super().get_widget_and_offset_at(x, y)
1111
+ if widget is not None and is_tool_copy_excluded(widget):
1112
+ return None, None
1113
+ return widget, offset
1114
+
1115
+ def _watch__select_state(self, select_state: SelectState | None) -> None:
1116
+ """Route chat drags through the O(visible) logical selection model.
1117
+
1118
+ Textual's generic handler re-walks and re-sorts every widget under the
1119
+ selection container on each mouse move — O(total transcript widgets)
1120
+ in a long chat. Drags whose both endpoints sit in the ChatPanel go
1121
+ through :class:`ChatSelectionController` instead; anything else
1122
+ (single-widget selections, non-chat surfaces) keeps stock behavior.
1123
+ """
1124
+ if select_state is None or select_state.end is None:
1125
+ if select_state is None:
1126
+ self._chat_selection.clear()
1127
+ super()._watch__select_state(select_state)
1128
+ return
1129
+ if not select_state.is_attached_to_dom() or select_state.is_single_content_widget:
1130
+ self._chat_selection.clear()
1131
+ super()._watch__select_state(select_state)
1132
+ return
1133
+ if self._chat_selection.handle_select_state(select_state):
1134
+ return
1135
+ self._chat_selection.clear()
1136
+ super()._watch__select_state(select_state)
1137
+
1138
+ async def _watch_selections(
1139
+ self,
1140
+ old_selections: dict[Widget, Selection],
1141
+ selections: dict[Widget, Selection],
1142
+ ) -> None:
1143
+ """Notify only widgets whose selection actually changed.
1144
+
1145
+ Textual notifies the union of old and new maps unconditionally.
1146
+ ``VirtualizedMarkdown.selection_updated`` drops its rendered-line and
1147
+ copy-text caches, so blanket notification re-renders every selected
1148
+ transcript widget on every mouse move of a drag.
1149
+ """
1150
+ for widget in old_selections.keys() | selections.keys():
1151
+ new_selection = selections.get(widget)
1152
+ if old_selections.get(widget) != new_selection:
1153
+ widget.selection_updated(new_selection)
1154
+
1155
+ def clear_selection(self) -> None:
1156
+ """Clear the chat logical selection along with screen selection state."""
1157
+ self._chat_selection.clear()
1158
+ super().clear_selection()
1159
+
1160
+ def schedule_chat_selection_reprojection(self, panel: ChatPanel) -> None:
1161
+ """Keep chat selection highlights aligned after the transcript scrolls."""
1162
+ self._chat_selection.schedule_reproject(panel)
1163
+
1164
+ def _compositor_refresh(self) -> None:
1165
+ """Forward each repaint to the dialog above this screen once.
1166
+
1167
+ Every ``ModalScreen`` lets this screen show through (``chrys.tcss``),
1168
+ so a dialog paints this screen's frame as its backdrop and must receive
1169
+ every region this screen repaints while covered. Textual's background
1170
+ branch forwards those regions, but it also marks them dirty again on
1171
+ this screen and sets ``_repaint_required``. The next update then swaps
1172
+ the dirty widgets for this screen alone and forwards the old regions
1173
+ once more, together with any new damage, and marks the union again.
1174
+ Whatever was forwarded once is forwarded on every later update: after
1175
+ the first layout, scroll or status change under the dialog, every
1176
+ spinner tick makes the dialog re-render and re-blend the whole backdrop.
1177
+
1178
+ Forward only the regions this update repainted. The re-marking keeps
1179
+ nothing fresh: the dialog repaints a forwarded region from this
1180
+ screen's current compositor output, and Textual repaints this screen's
1181
+ whole frame when the dialog closes (``Screen._on_screen_resume`` and
1182
+ ``App._replace_screen``). Underlay animation stays visible because
1183
+ every tick still reaches the dialog.
1184
+
1185
+ This is a version-gated fork of the non-inline background branch of
1186
+ Textual's private ``Screen._compositor_refresh``. The only intentional
1187
+ semantic delta is dropping the re-marking; the top-screen and inline
1188
+ branches, and other Textual releases, take the stock path.
1189
+ """
1190
+ app = self.app
1191
+ dirty_regions = self._compositor._dirty_regions
1192
+ if (
1193
+ TEXTUAL_VERSION != TEXTUAL_BACKGROUND_REFRESH_FORK_VERSION
1194
+ or app.is_inline
1195
+ or not dirty_regions
1196
+ or self is app.screen
1197
+ or self not in app._background_screens
1198
+ ):
1199
+ super()._compositor_refresh()
1200
+ return
1201
+ app.screen.refresh(*dirty_regions)
1202
+ dirty_regions.clear()
1203
+ self._dirty_widgets.clear()
1204
+ app._update_mouse_over(self)
1205
+
1206
+ def _screen_resized(self, size: Size) -> None:
1207
+ """Ignore unchanged resize broadcasts while this screen is under an overlay."""
1208
+ if size == self._size:
1209
+ return
1210
+ super()._screen_resized(size)
1211
+
1212
+ def _refresh_layout(self, size: Size | None = None, scroll: bool = False) -> None:
1213
+ if self._theme_editor_opening:
1214
+ # App.refresh_css calls this directly even inside batch_update.
1215
+ # Mount the complete dock before measuring the resized transcript.
1216
+ self.refresh(layout=True)
1217
+ return
1218
+ super()._refresh_layout(size, scroll)
1219
+ # Now — and only now — the compositor's visible map matches the new
1220
+ # scroll offset, so a pending chat-highlight re-projection is safe.
1221
+ self._chat_selection.on_screen_layout_refreshed()
1222
+ # Likewise a workflow panel shown before this layout placed it.
1223
+ self._workflow.screen_layout_refreshed()
1224
+
1225
+ async def on_mount(self) -> None:
1226
+ """Subscribe to backend events when the screen mounts."""
1227
+ chat_panel = self.query_one(ChatPanel)
1228
+ chat_panel.set_replay_progress_callback(self._events.update_session_history_progress)
1229
+ self._gc_freeze_participants = (
1230
+ chat_panel,
1231
+ self.query_one(TrajectoryDashboard),
1232
+ self.query_one(SessionJsonPanel),
1233
+ self.query_one(ShellPanel),
1234
+ self._suggestions,
1235
+ )
1236
+ self._suggestions.build_slash_commands()
1237
+
1238
+ await self._subscriptions.subscribe_all()
1239
+ await self._workflow.subscribe()
1240
+ self._sync_execution_chrome()
1241
+ self._sync_workflow_timer()
1242
+ if self._locale_controller is not None:
1243
+ self._locale_controller.register_surface(self)
1244
+ self._refresh_model_indicator()
1245
+ self._update_subtitle()
1246
+ self._workspace_branch.start(self._workspace_cwd())
1247
+
1248
+ async def on_unmount(self) -> None:
1249
+ """Flush pending UI-owned settings before the screen is torn down."""
1250
+ if self._locale_controller is not None:
1251
+ self._locale_controller.unregister_surface(self)
1252
+ if self._workflow_timer is not None:
1253
+ self._workflow_timer.stop()
1254
+ await self._workflow.close()
1255
+ try:
1256
+ await self._subscriptions.unsubscribe_all()
1257
+ finally:
1258
+ self._session_title.stop_activity()
1259
+ await self._workspace_branch.close()
1260
+ try:
1261
+ await self._suggestions.buddy_command.shutdown()
1262
+ finally:
1263
+ await self._flush_settings_save()
1264
+
1265
+ # ------------------------------------------------------------------ #
1266
+ # Focus guard — keep input bar focused
1267
+ # ------------------------------------------------------------------ #
1268
+
1269
+ def on_descendant_focus(self, event: DescendantFocus) -> None:
1270
+ """Redirect focus back to the input bar when it lands on a display-only panel."""
1271
+ if self._workflow.workflow_mode:
1272
+ return
1273
+ self._shell_mode_controller.on_descendant_focus(event.widget)
1274
+
1275
+ def on_paste(self, event: Paste) -> None:
1276
+ """Route image path drops/pastes into the chat input when it is not focused."""
1277
+ if self._workflow.workflow_mode:
1278
+ return
1279
+ self._shell_mode_controller.on_paste(event)
1280
+
1281
+ # ------------------------------------------------------------------ #
1282
+ # User input
1283
+ # ------------------------------------------------------------------ #
1284
+
1285
+ @on(InputBar.UserSubmitted)
1286
+ def _on_user_input(self, event: InputBar.UserSubmitted) -> None:
1287
+ """User pressed Enter — handle slash commands or publish message."""
1288
+ if self._workflow.workflow_mode:
1289
+ return
1290
+ text = event.text.strip()
1291
+ if not text:
1292
+ return
1293
+
1294
+ # Dispatch slash commands
1295
+ if is_slash_command_candidate(text):
1296
+ if self._state.run.agent_loading:
1297
+ return
1298
+ if self._suggestions.dispatch_slash_command(text):
1299
+ return
1300
+
1301
+ if self._state.run.agent_loading:
1302
+ return
1303
+ self._submit_user_text(text)
1304
+
1305
+ @on(InputBar.LockedChanged)
1306
+ def _on_input_locked_changed(self, event: InputBar.LockedChanged) -> None:
1307
+ """Keep status-bar selector guards synchronized with input locking."""
1308
+ self.query_one(StatusBar).set_input_locked(event.locked)
1309
+
1310
+ def _model_unconfigured(self) -> bool:
1311
+ """Return whether a chat submit would run with no usable model.
1312
+
1313
+ Confirmed runtime details are authoritative when they name a model:
1314
+ the resolver's built-in fallback loads fine but carries the
1315
+ non-functional placeholder model id, while a real model id proves
1316
+ the session can serve. A blank confirmed id carries no information
1317
+ (event sources may omit runtime details), so it falls back to the
1318
+ registry's selectable profiles — as does the pre-confirmation state.
1319
+ Screens constructed without a registry keep the legacy submit path.
1320
+ """
1321
+ registry = self._services.model_registry
1322
+ if registry is None:
1323
+ return False
1324
+ if self._state.runtime.details_confirmed:
1325
+ model_id = self._state.runtime.details.model.model_id
1326
+ if model_id == UNCONFIGURED_MODEL_ID:
1327
+ return True
1328
+ if model_id.strip():
1329
+ return False
1330
+ return not self._has_selectable_model_profile()
1331
+
1332
+ def _show_model_unconfigured_dialog(self) -> None:
1333
+ """Explain the blocked submit and offer the model tag's action."""
1334
+ from chrys.app.tui.screens.dialogs.confirm import ConfirmDialog
1335
+
1336
+ def _on_result(confirmed: bool | None) -> None:
1337
+ if confirmed:
1338
+ self._config_actions.on_model_tag_clicked(
1339
+ "select" if self._has_selectable_model_profile() else "configure"
1340
+ )
1341
+ else:
1342
+ # Dismissal must hand the keyboard back to the composer.
1343
+ self.query_one(InputBar).focus_input()
1344
+
1345
+ self.app.push_screen(
1346
+ ConfirmDialog(
1347
+ title=_MODEL_UNCONFIGURED_TITLE.bind(),
1348
+ message=_MODEL_UNCONFIGURED_MESSAGE.bind(),
1349
+ confirm_label=_MODEL_UNCONFIGURED_SETUP.bind(),
1350
+ locale_controller=self._locale_controller,
1351
+ ),
1352
+ _on_result,
1353
+ )
1354
+
1355
+ def _submit_user_text(self, text: str) -> None:
1356
+ """Submit normal user text as a new turn or mid-run injection.
1357
+
1358
+ Sole chokepoint for chat submissions — composer submits and
1359
+ suggestion-originated ones (runtime skills, unmatched fallback
1360
+ text) alike — so the unconfigured-model guard cannot be bypassed.
1361
+ """
1362
+ if self._workflow.workflow_mode:
1363
+ return
1364
+ if self._model_unconfigured():
1365
+ # Every submit path clears the composer before reaching here; hand
1366
+ # the text back so the blocked message is not lost. The hand-back
1367
+ # travels as a message, so a draft typed in the meantime wins.
1368
+ self.query_one(InputBar).restore_draft(text)
1369
+ self._show_model_unconfigured_dialog()
1370
+ return
1371
+ self._input_flow.submit_user_text(text)
1372
+
1373
+ def action_chat_page_up(self) -> None:
1374
+ """Scroll the transcript up one page, even while the input is focused."""
1375
+ panel = self.query_one(ChatPanel)
1376
+ panel.note_user_scroll()
1377
+ panel.scroll_page_up(animate=False)
1378
+
1379
+ def action_chat_page_down(self) -> None:
1380
+ """Scroll the transcript down one page, even while the input is focused."""
1381
+ panel = self.query_one(ChatPanel)
1382
+ panel.note_user_scroll()
1383
+ panel.scroll_page_down(animate=False)
1384
+
1385
+ def action_chat_scroll_bottom(self) -> None:
1386
+ """Scroll the transcript to the bottom, even while the input is focused."""
1387
+ self.query_one(ChatPanel).jump_to_bottom()
1388
+
1389
+ # ------------------------------------------------------------------ #
1390
+ # Message editor
1391
+ # ------------------------------------------------------------------ #
1392
+
1393
+ @on(InputBar.EditorRequested)
1394
+ def _on_editor_requested(self, _event: InputBar.EditorRequested) -> None:
1395
+ if isinstance(self.app.screen, EditorDialog):
1396
+ return
1397
+ input_bar = self.query_one(InputBar)
1398
+ snapshot = input_bar.snapshot_draft()
1399
+ if len(snapshot.text) > MESSAGE_EDITOR_MAX_CHARACTERS:
1400
+ self._view_adapter.notify(
1401
+ _EDITOR_DRAFT_TOO_LARGE.bind(limit=f"{MESSAGE_EDITOR_MAX_CHARACTERS:,}"),
1402
+ title=_EDITOR_DRAFT_TOO_LARGE_TITLE.bind(),
1403
+ severity="warning",
1404
+ )
1405
+ return
1406
+ dialog = EditorDialog(
1407
+ EditorBufferSnapshot(snapshot.text, snapshot.cursor_location, snapshot.revision),
1408
+ mode=self._editor_mode,
1409
+ can_commit=input_bar.can_commit_editor_draft,
1410
+ current_draft_revision=lambda: input_bar.draft_revision,
1411
+ )
1412
+ self._view_adapter.push_screen(dialog, self._on_editor_result)
1413
+
1414
+ def _on_editor_result(self, result: EditorDialogResult) -> None:
1415
+ if result.mode is not self._editor_mode:
1416
+ self._editor_mode = result.mode
1417
+ persist_editor_keymap(result.mode.value)
1418
+ # The same write-point contract as theme and locale: the shared
1419
+ # settings handle records the choice as a RUNTIME override, so
1420
+ # settings keep describing the screen even when the swallowed
1421
+ # disk write above failed.
1422
+ if self._on_editor_keymap_changed is not None:
1423
+ self._on_editor_keymap_changed(result.mode.value)
1424
+ if not result.accepted:
1425
+ return
1426
+ input_bar = self.query_one(InputBar)
1427
+ input_bar.replace_draft(result.text)
1428
+ input_bar.focus_input()
1429
+
1430
+ # ------------------------------------------------------------------ #
1431
+ # Shell mode
1432
+ # ------------------------------------------------------------------ #
1433
+
1434
+ @on(InputBar.ShellModeRequested)
1435
+ def _on_shell_mode_requested(self, _event: InputBar.ShellModeRequested) -> None:
1436
+ if self.theme_editor is not None:
1437
+ self.theme_editor.request_close(after_close=self._enter_shell_after_editor)
1438
+ else:
1439
+ self.shell_mode_state = True
1440
+
1441
+ def _enter_shell_after_editor(self) -> None:
1442
+ self._shell_requested_after_editor = True
1443
+ self._resume_shell_request()
1444
+
1445
+ def _resume_shell_request(self) -> None:
1446
+ # A dialog may have opened while the dock was unmounting. Wait for
1447
+ # MainScreen to resume before changing the terminal layout or focus.
1448
+ if self._shell_requested_after_editor and self.is_mounted and self.app.screen is self:
1449
+ self._shell_requested_after_editor = False
1450
+ if self.theme_editor is None:
1451
+ self.shell_mode_state = True
1452
+
1453
+ def _set_shell_mode_state(self, active: bool) -> None:
1454
+ self.shell_mode_state = active
1455
+
1456
+ def _sync_sidebar_tab_strip_focus(self) -> None:
1457
+ """Make the sidebar tab strip focusable exactly where sidebar focus is not handed back.
1458
+
1459
+ Asks what ``on_descendant_focus`` asks: workflow mode returns before the shell
1460
+ controller, whose ``keeps_panel_focus`` covers shell mode and the fullscreen terminal.
1461
+ Focus itself moves only with the view switch that follows each mode change
1462
+ (``exit_shell_mode`` returns it to a visible input).
1463
+ """
1464
+ self.query_one(SidebarPanel).set_tab_strip_focusable(
1465
+ self._workflow.workflow_mode or self._shell_mode_controller.keeps_panel_focus()
1466
+ )
1467
+
1468
+ @on(Terminal.EscapeExited)
1469
+ def _on_terminal_escape_exited(self, _event: Terminal.EscapeExited) -> None:
1470
+ """User double-tapped Escape in terminal — exit shell mode."""
1471
+ self._shell_mode_controller.exit_on_escape()
1472
+
1473
+ @on(ShellPanel.CommandExecuted)
1474
+ def _on_shell_command_executed(self, event: ShellPanel.CommandExecuted) -> None:
1475
+ """Shell command recorded via shell integration hooks."""
1476
+ event.stop()
1477
+ self._shell_mode_controller.command_executed(event.command)
1478
+
1479
+ @on(Terminal.AlternateScreenChanged)
1480
+ def _on_alternate_screen_changed(self, event: Terminal.AlternateScreenChanged) -> None:
1481
+ """When interactive app (vim/htop) activates, enter fullscreen terminal mode."""
1482
+ event.stop()
1483
+ self._shell_mode_controller.set_alternate_screen_active(event.enabled)
1484
+
1485
+ @on(ShellPanel.Exited)
1486
+ def _on_shell_exited(self, _event: ShellPanel.Exited) -> None:
1487
+ """Shell process exited — exit shell mode."""
1488
+ self._shell_mode_controller.exit_on_shell_closed()
1489
+
1490
+ # ------------------------------------------------------------------ #
1491
+ # Suggestion list (/ commands, @ files, # agents, $ models)
1492
+ # ------------------------------------------------------------------ #
1493
+
1494
+ @on(InputBar.SlashTriggered)
1495
+ def _on_slash_triggered(self, _event: InputBar.SlashTriggered) -> None:
1496
+ self._suggestions.on_slash_triggered()
1497
+
1498
+ @on(InputBar.FileTriggered)
1499
+ def _on_file_triggered(self, _event: InputBar.FileTriggered) -> None:
1500
+ self._suggestions.on_file_triggered()
1501
+
1502
+ @on(InputBar.AgentTriggered)
1503
+ def _on_agent_triggered(self, _event: InputBar.AgentTriggered) -> None:
1504
+ self._suggestions.on_agent_triggered()
1505
+
1506
+ @on(InputBar.ModelTriggered)
1507
+ def _on_model_triggered(self, _event: InputBar.ModelTriggered) -> None:
1508
+ self._suggestions.on_model_triggered()
1509
+
1510
+ def action_prompt_history(self) -> None:
1511
+ """Open the cross-session prompt-history suggestion list."""
1512
+ if self._state.shell.fullscreen_terminal or self._state.shell.active or not self._chat_foreground():
1513
+ return
1514
+ input_bar = self.query_one(InputBar)
1515
+ if input_bar.locked:
1516
+ return
1517
+ input_bar.focus_input()
1518
+ revision = self._suggestions.start_prompt_history()
1519
+ self._show_prompt_history(revision)
1520
+
1521
+ @work(thread=False)
1522
+ async def _show_prompt_history(self, revision: int) -> None:
1523
+ await self._suggestions.show_prompt_history_async(revision=revision)
1524
+
1525
+ @on(InputBar.TextChanged)
1526
+ def _on_text_changed_for_suggestions(self, event: InputBar.TextChanged) -> None:
1527
+ self._suggestions.on_text_changed(event.text)
1528
+
1529
+ @on(InputBar.SuggestionNavigate)
1530
+ def _on_suggestion_navigate(self, event: InputBar.SuggestionNavigate) -> None:
1531
+ self._suggestions.on_suggestion_navigate(event.direction)
1532
+
1533
+ @on(InputBar.SuggestionSelect)
1534
+ def _on_suggestion_select(self, event: InputBar.SuggestionSelect) -> None:
1535
+ self._suggestions.on_suggestion_select(event.execute)
1536
+
1537
+ @on(SuggestionList.Selected)
1538
+ def _on_suggestion_selected(self, event: SuggestionList.Selected) -> None:
1539
+ self._suggestions.on_suggestion_selected(event.mode, event.text, event.execute, event.kind)
1540
+
1541
+ @on(InputBar.SuggestionDismiss)
1542
+ @on(SuggestionList.Dismissed)
1543
+ def _on_suggestion_dismissed(self, _event: InputBar.SuggestionDismiss | SuggestionList.Dismissed) -> None:
1544
+ self._suggestions.dismiss_suggestions()
1545
+
1546
+ # ------------------------------------------------------------------ #
1547
+ # Input bar action buttons
1548
+ # ------------------------------------------------------------------ #
1549
+
1550
+ @on(InputBar.InterruptRequested)
1551
+ def _on_interrupt_requested(self, _event: InputBar.InterruptRequested) -> None:
1552
+ """Stop button clicked — interrupt the running agent."""
1553
+ self.action_interrupt()
1554
+
1555
+ @on(InputBar.RetryRequested)
1556
+ def _on_retry_requested(self, event: InputBar.RetryRequested) -> None:
1557
+ """Retry button clicked — retry from current state.
1558
+
1559
+ ``event.text`` carries any note the user typed before clicking
1560
+ Continue/Retry. When non-empty, the engine uses it as the
1561
+ mid-turn continuation prompt.
1562
+ """
1563
+ if self._workflow.workflow_mode:
1564
+ return
1565
+ if self._model_unconfigured():
1566
+ # A retry re-runs the turn against the model just like a fresh
1567
+ # submit. The composer already consumed the typed note, so hand
1568
+ # it back — unless a draft landed in the meantime, which wins; the
1569
+ # inline status action stays visible for after the user configures
1570
+ # a model.
1571
+ self.query_one(InputBar).restore_draft(event.text)
1572
+ self._show_model_unconfigured_dialog()
1573
+ return
1574
+ self._input_flow.request_retry(event.text)
1575
+
1576
+ @on(ConversationStatusAction.Pressed)
1577
+ def _on_conversation_status_action_pressed(self, event: ConversationStatusAction.Pressed) -> None:
1578
+ """Retry/continue button clicked from an error or interrupted status.
1579
+
1580
+ Resumes without a continuation prompt: any draft typed in the input
1581
+ bar stays there. Sending the draft as the mid-turn prompt is the
1582
+ input bar's Continue button (``InputBar.RetryRequested``).
1583
+ """
1584
+ event.stop()
1585
+ input_bar = self.query_one(InputBar)
1586
+ if self._state.run.agent_loading or not input_bar.retry_mode:
1587
+ return
1588
+ if self._model_unconfigured():
1589
+ self._show_model_unconfigured_dialog()
1590
+ return
1591
+ self._input_flow.request_retry("")
1592
+
1593
+ @on(ToolViewRequested)
1594
+ def _on_tool_view_requested(self, event: ToolViewRequested) -> None:
1595
+ """Open a detailed tool-view modal requested by a tool widget."""
1596
+ from chrys.app.tui.screens.dialogs.tool_view import ToolDetailModal
1597
+
1598
+ event.stop()
1599
+ self.app.push_screen(ToolDetailModal.from_request(event))
1600
+
1601
+ @on(InputBar.NewSessionRequested)
1602
+ def _on_new_session_requested(self, _event: InputBar.NewSessionRequested) -> None:
1603
+ """New button clicked — start a fresh session."""
1604
+ if self._state.run.agent_running or self._state.run.agent_loading or self._services.execution_busy():
1605
+ return
1606
+ self._create_new_session()
1607
+
1608
+ @on(StatusBar.DetailsClicked)
1609
+ def _on_status_details_clicked(self, _event: StatusBar.DetailsClicked) -> None:
1610
+ """Open the active runtime details modal."""
1611
+ self.action_runtime_details()
1612
+
1613
+ @work(thread=False)
1614
+ async def _create_new_session(self) -> None:
1615
+ if self._state.run.agent_loading:
1616
+ return
1617
+ self._session_title.set_terminal_title_for_cwd()
1618
+ await self._sessions.create_new_session()
1619
+
1620
+ @work(thread=False)
1621
+ async def _resume_last_session(self) -> None:
1622
+ if self._state.run.agent_loading:
1623
+ return
1624
+ await self._sessions.resume_last_session()
1625
+
1626
+ @work(thread=False)
1627
+ async def _fork_current_session(self) -> None:
1628
+ await self._sessions.fork_current_session()
1629
+
1630
+ def _clear_workspace_marker_for_user_message(self) -> None:
1631
+ self._state.workspace_marker.original_cwd = None
1632
+
1633
+ def _commit_profile_switch_marker(self) -> None:
1634
+ # Once messages are exchanged, the switch indicator becomes permanent.
1635
+ # The next switch will use a new unique key, so the old widget stays
1636
+ # untouched in the chat.
1637
+ if self._state.profile_marker.from_profile is not None:
1638
+ self._state.profile_marker.from_profile = None
1639
+ self._state.profile_marker.to_profile = None
1640
+
1641
+ # ------------------------------------------------------------------ #
1642
+ # Header and status-bar tags, model switching ($)
1643
+ # ------------------------------------------------------------------ #
1644
+
1645
+ @on(AppHeader.ApprovalBadgeClicked)
1646
+ def _on_approval_badge_clicked(self, _event: AppHeader.ApprovalBadgeClicked) -> None:
1647
+ """Open the approval mode picker modal."""
1648
+ self._config_actions.on_approval_badge_clicked()
1649
+
1650
+ @on(StatusBar.ProfileTagClicked)
1651
+ def _on_profile_tag_clicked(self, _event: StatusBar.ProfileTagClicked) -> None:
1652
+ """Open the agent picker modal."""
1653
+ self._config_actions.on_profile_tag_clicked()
1654
+
1655
+ @on(StatusBar.ModelTagClicked)
1656
+ def _on_model_tag_clicked(self, event: StatusBar.ModelTagClicked) -> None:
1657
+ """Route the model tag's semantic action."""
1658
+ if not self._workflow.workflow_mode:
1659
+ self._config_actions.on_model_tag_clicked(event.mode)
1660
+
1661
+ @on(StatusBar.SelectorBusy)
1662
+ def _on_selector_busy(self, event: StatusBar.SelectorBusy) -> None:
1663
+ """Explain why a selector ignored a click during a run."""
1664
+ if not self._workflow.workflow_mode:
1665
+ self._config_actions.on_selector_busy(event.selector)
1666
+
1667
+ @work(thread=False)
1668
+ async def _switch_model_profile(self, profile_id: str) -> None:
1669
+ """Commit a Chat model suggestion only while its owner remains active."""
1670
+ if self._workflow.workflow_mode or not self._model_selection_is_committable(profile_id):
1671
+ return
1672
+ await self._config_actions.on_model_picked(profile_id)
1673
+
1674
+ def _model_selection_is_committable(self, profile_id: str) -> bool:
1675
+ """Re-check every guard the displayed suggestion rows were built on.
1676
+
1677
+ The suggestion popup outlives a pushed screen, so between the rows
1678
+ being built and one being chosen F4 can retire the profile and F2 can
1679
+ hand model ownership to an agent. ``activate_model_profile`` persists
1680
+ whatever id it is handed, so the last word has to be taken here rather
1681
+ than trusted from a row that may have gone stale on screen.
1682
+ """
1683
+ if self._state.run.agent_running or self._state.run.agent_loading or self._services.execution_busy():
1684
+ return False
1685
+ if is_model_selection_locked(
1686
+ self._state.runtime.details.model,
1687
+ runtime_confirmed=self._state.runtime.details_confirmed,
1688
+ ):
1689
+ return False
1690
+ registry = self._services.model_registry
1691
+ profile = None if registry is None else registry.get(profile_id)
1692
+ return profile is not None and is_model_profile_selectable(profile)
1693
+
1694
+ # ------------------------------------------------------------------ #
1695
+ # Model config (/model)
1696
+ # ------------------------------------------------------------------ #
1697
+
1698
+ def _open_model_config(self) -> None:
1699
+ """Open the model configuration modal."""
1700
+ self._config_actions.open_model_config()
1701
+
1702
+ # ------------------------------------------------------------------ #
1703
+ # Agent config (/agents with optional tab subcommands)
1704
+ # ------------------------------------------------------------------ #
1705
+
1706
+ def action_agents_config(self) -> None:
1707
+ """Open the agent configuration modal (F2)."""
1708
+ self._open_agent_config()
1709
+
1710
+ def action_models_config(self) -> None:
1711
+ """Open the model configuration modal (F4)."""
1712
+ self._open_model_config()
1713
+
1714
+ def action_settings(self) -> None:
1715
+ """Open the Settings dialog (F10)."""
1716
+ self._open_settings()
1717
+
1718
+ def action_runtime_details(self) -> None:
1719
+ """Open the active runtime details modal."""
1720
+ self._config_actions.open_runtime_details()
1721
+
1722
+ def _open_settings(self, initial_tab: str = GENERAL_TAB_ID) -> None:
1723
+ """Open the Settings dialog at *initial_tab*."""
1724
+ self._config_actions.open_settings(initial_tab)
1725
+
1726
+ def _schedule_notification_settings_save(self, settings: NotificationSettings) -> None:
1727
+ """Record the live choice, then persist it after a short debounce."""
1728
+ cast("ChrysApp", self.app).override_notification_settings(settings)
1729
+ self._settings_persistence().schedule(settings.to_settings_patch())
1730
+
1731
+ async def _flush_settings_save(self) -> None:
1732
+ """Force any debounced settings write to complete now."""
1733
+ await self._settings_persistence().flush()
1734
+
1735
+ def _open_agent_config(self) -> None:
1736
+ """Open the unified agent configuration modal."""
1737
+ self._config_actions.open_agent_config()
1738
+
1739
+ def _open_agent_config_tab(self, tab: str) -> None:
1740
+ """Open the agent configuration modal at a specific tab."""
1741
+ self._config_actions.open_agent_config_tab(tab)
1742
+
1743
+ @on(ChatPanel.TitleClicked)
1744
+ def _on_chat_panel_title_clicked(self, _event: ChatPanel.TitleClicked) -> None:
1745
+ """Open the session title editor when the user clicks the border title."""
1746
+ self._session_title.open_editor()
1747
+
1748
+ @on(WorkflowPanel.WorkingDirClicked)
1749
+ @on(ChatPanel.WorkingDirClicked)
1750
+ def _on_chat_panel_working_dir_clicked(
1751
+ self, _event: ChatPanel.WorkingDirClicked | WorkflowPanel.WorkingDirClicked
1752
+ ) -> None:
1753
+ """Open the file dialog when the user clicks the working directory subtitle."""
1754
+ self._workspace_actions.open_working_dir_picker()
1755
+
1756
+ def _copy_agent_responses(self, arg: str) -> None:
1757
+ """Handle /copy slash command — copy agent, user, or full-conversation turns.
1758
+
1759
+ Supported forms are ``/copy [N]``, ``/copy agent [N|all]``,
1760
+ ``/copy user [N|all]``, and ``/copy all``. The legacy ``/copy N``
1761
+ form remains equivalent to ``/copy agent N``.
1762
+
1763
+ Writes to both the host OS clipboard (pbcopy/clip.exe/xclip) and the terminal's
1764
+ OSC 52 clipboard. OSC 52 is what lets the copy reach the user's actual terminal
1765
+ clipboard in SSH / remote-tty sessions, where the native tools would otherwise
1766
+ copy to the remote host's clipboard (or fail outright).
1767
+ """
1768
+ self._copy_actions.copy_agent_responses(arg)
1769
+
1770
+ def _toggle_fold(self) -> None:
1771
+ """Handle /fold slash command — collapse or expand all tool groups."""
1772
+ self._copy_actions.toggle_fold()
1773
+
1774
+ def action_show_rollback(self, arg: str = "") -> None:
1775
+ """Open the picker or directly execute an explicit target.
1776
+
1777
+ - ``/rollback`` with no argument: open the modal with the last
1778
+ turn pre-selected.
1779
+ - ``/rollback N``: directly discard the most recent ``N`` turns.
1780
+ - ``/rollback to N``: directly keep turns ``1..N`` and discard later turns.
1781
+ - Explicit targets restore eligible file changes by default and run
1782
+ under a blocking loading modal. Use the picker for conversation-only
1783
+ rollback.
1784
+ """
1785
+ self._rollback_controller.show_rollback(arg)
1786
+
1787
+ def action_show_diff(self) -> None:
1788
+ """Open the full-screen diff viewer showing file changes per turn."""
1789
+ if self._workflow.workflow_mode:
1790
+ self._diff_controller.show_workflow_diff(
1791
+ self._workflow.session_id, self._workflow.project_cwd or self._workflow.cwd()
1792
+ )
1793
+ else:
1794
+ self._diff_controller.show_diff()
1795
+
1796
+ # ------------------------------------------------------------------ #
1797
+ # Actions (keybindings)
1798
+ # ------------------------------------------------------------------ #
1799
+
1800
+ def _set_agent_running(self, running: bool) -> None:
1801
+ if MainScreen._reactive_state_ready(self):
1802
+ self.agent_running_state = running
1803
+ else:
1804
+ MainScreen._apply_agent_running_state(self, running)
1805
+
1806
+ def _apply_agent_running_state(self, running: bool) -> None:
1807
+ was_running = self._state.run.agent_running
1808
+ self._state.run.agent_running = running
1809
+ if running:
1810
+ self._state.run.turn_end_check_pending = True
1811
+ if not was_running:
1812
+ self._session_title.run_started()
1813
+ self._state.run.generation += 1
1814
+ self._live_diff.reset_for_run_start(
1815
+ LiveDiffOwner(
1816
+ session_id=self._view_adapter.current_chat_session_id(),
1817
+ session_generation=self._services.session_generation(),
1818
+ run_generation=self._state.run.generation,
1819
+ )
1820
+ )
1821
+ else:
1822
+ # Agent tool calls (write_file / edit_file / shell) may have
1823
+ # created or deleted files during the turn; drop the cached
1824
+ # ``@`` suggestion list so the next trigger re-scans. The
1825
+ # scan is lazy — invalidation is cheap; the real work only
1826
+ # runs when the user next types ``@``. Mirrors the
1827
+ # invalidation in ``on_workspace_updated``.
1828
+ self._suggestions.file_cache = None
1829
+ if self._state.run.turn_end_check_pending:
1830
+ self._state.run.turn_end_check_pending = False
1831
+ self._workspace_actions.check_after_turn()
1832
+ input_bar = self.query_one(InputBar)
1833
+ if not running and input_bar.locked:
1834
+ input_bar.unlock_and_keep()
1835
+ self.refresh_bindings()
1836
+ # Auto-dismiss interrupt confirmation when agent stops
1837
+ if not running and self._state.overlays.interrupt_confirm_active:
1838
+ self._navigation.dismiss_interrupt_confirm()
1839
+ self._session_title.sync_activity()
1840
+
1841
+ def _set_agent_loading(self, loading: bool) -> None:
1842
+ if MainScreen._reactive_state_ready(self):
1843
+ self.agent_loading_state = loading
1844
+ else:
1845
+ MainScreen._apply_agent_loading_state(self, loading)
1846
+
1847
+ def _apply_agent_loading_state(self, loading: bool) -> None:
1848
+ self._state.run.agent_loading = loading
1849
+ input_bar = self.query_one(InputBar)
1850
+ if not loading and input_bar.locked and not self._state.run.agent_running:
1851
+ input_bar.unlock_and_keep()
1852
+
1853
+ def _set_has_messages(self, has: bool) -> None:
1854
+ if MainScreen._reactive_state_ready(self):
1855
+ self.has_messages_state = has
1856
+ else:
1857
+ self._state.run.has_messages = has
1858
+
1859
+ def action_show_log_viewer(self) -> None:
1860
+ """Open the log viewer modal (F6)."""
1861
+ self._navigation.show_log_viewer()
1862
+
1863
+ def action_open_guide(self) -> None:
1864
+ """Open the user guide (F8), as ``/help`` does."""
1865
+ self._navigation.open_guide()
1866
+
1867
+ def action_copy_text(self) -> None:
1868
+ """Copy selected text to both terminal and system clipboard."""
1869
+ if not self._copy_selected_text():
1870
+ # Match stock Screen semantics: an empty selection defers the
1871
+ # key to the next namespace (the app-level ctrl+c quit hint).
1872
+ raise SkipAction
1873
+
1874
+ def get_selected_text(self) -> str | None:
1875
+ """Get selected text, excluding tool-renderer UI chrome and results.
1876
+
1877
+ Chat drags keep ``self.selections`` bounded to the visible slice of
1878
+ the transcript; the full anchored range is extracted here, once, from
1879
+ the logical selection model.
1880
+ """
1881
+ chat_text = self._chat_selection.extract_text()
1882
+ if chat_text is not None:
1883
+ return chat_text
1884
+ if not self.selections:
1885
+ return None
1886
+
1887
+ widget_text: list[str] = []
1888
+ for widget, selection in self.selections.items():
1889
+ if is_tool_copy_excluded(widget):
1890
+ continue
1891
+ if widget.is_attached and (selected_text_in_widget := widget.get_selection(selection)) is not None:
1892
+ widget_text.extend(selected_text_in_widget)
1893
+
1894
+ if not widget_text:
1895
+ return None
1896
+ return "".join(widget_text).rstrip("\n")
1897
+
1898
+ def action_interrupt(self) -> None:
1899
+ self._publish_interrupt()
1900
+
1901
+ @work(thread=False)
1902
+ async def _publish_interrupt(self) -> None:
1903
+ if self._services.execution().kind == "workflow":
1904
+ self._workflow.run_control.stop()
1905
+ return
1906
+ await self._input_flow.publish_interrupt()
1907
+
1908
+ # -- Per-sub-agent retry/abort bridging ---------------------------
1909
+ #
1910
+ # The :class:`SubAgentToolCall` renderer posts widget-level Textual
1911
+ # ``Message`` subclasses when the user clicks its Retry/Abort buttons.
1912
+ # Those bubble up the DOM to this screen; Textual dispatches them by
1913
+ # snake_case method name (``on_sub_agent_retry_clicked`` matches
1914
+ # ``SubAgentRetryClicked``). The screen then translates them into
1915
+ # bus events so the engine can route by ``invocation_id``.
1916
+
1917
+ async def on_sub_agent_retry_clicked(self, event: SubAgentRetryClicked) -> None:
1918
+ """Bubbled from :class:`SubAgentToolCall` Retry button."""
1919
+ event.stop()
1920
+ await self._tool_actions.request_sub_agent_retry(event.invocation_id)
1921
+
1922
+ async def on_sub_agent_abort_clicked(self, event: SubAgentAbortClicked) -> None:
1923
+ """Bubbled from :class:`SubAgentToolCall` Abort button."""
1924
+ event.stop()
1925
+ await self._tool_actions.request_sub_agent_abort(event.invocation_id)
1926
+
1927
+ async def on_sleep_skip_clicked(self, event: SleepSkipClicked) -> None:
1928
+ """Bubbled from :class:`SleepToolCall` Skip button."""
1929
+ event.stop()
1930
+ await self._tool_actions.skip_sleep(event.call_id)
1931
+
1932
+ async def on_ask_user_inline_submitted(self, event: AskUserInlineSubmitted) -> None:
1933
+ """Bubbled from :class:`AskUserToolCall` inline response controls."""
1934
+ event.stop()
1935
+ await self._tool_actions.submit_ask_user_inline(event.request_id, event.answers)
1936
+
1937
+ def action_quit(self) -> None:
1938
+ self._navigation.quit()
1939
+
1940
+ def action_sessions(self) -> None:
1941
+ """Open the sessions modal (Ctrl+N)."""
1942
+ self._navigation.sessions()
1943
+
1944
+ def _clear_current_session(self) -> None:
1945
+ """/clear — confirm, then delete the current session and start fresh."""
1946
+ self._navigation.clear_session()
1947
+
1948
+ async def _delete_session_and_new(self, session_id: str) -> None:
1949
+ if not self._workflow.workflow_mode:
1950
+ await self._sessions.delete_current_and_new(session_id)
1951
+ return
1952
+ await self._workflow.session_view.delete_current_session(session_id)
1953
+
1954
+ def action_toggle_sidebar(self) -> None:
1955
+ self._navigation.toggle_sidebar()
1956
+
1957
+ def _update_toc(self) -> None:
1958
+ self._navigation.update_toc()
1959
+
1960
+ def action_pick_theme(self) -> None:
1961
+ """Open the theme picker modal."""
1962
+ if self.theme_editor is not None:
1963
+ self.notify(
1964
+ render_str(self._language_localizer(), THEME_PREVIEW_HINT.bind()), severity="warning", markup=False
1965
+ )
1966
+ self.theme_editor.editor.focus_first_swatch()
1967
+ return
1968
+ self._navigation.pick_theme()
1969
+
1970
+ async def open_theme_editor(
1971
+ self,
1972
+ theme: Theme,
1973
+ store: UserThemeStore,
1974
+ revision: ThemeFileRevision | None = None,
1975
+ ) -> None:
1976
+ """Reserve a left dock before the header, transcript and input bar."""
1977
+ if self.theme_editor is not None:
1978
+ self.theme_editor.editor.focus_first_swatch()
1979
+ return
1980
+
1981
+ def closed() -> None:
1982
+ self.theme_editor = None
1983
+ self._set_theme_editor_compact(False)
1984
+
1985
+ panel = ThemeEditorPanel(theme, store, revision, on_close=closed)
1986
+ self.theme_editor = panel
1987
+ self._set_theme_editor_compact(self.size.width < 128)
1988
+ await self.mount(panel, before=0)
1989
+
1990
+ async def manage_themes(self, *, picker: ThemesScreen | None = None) -> None:
1991
+ """Open theme management directly in the editor's document selector."""
1992
+ from chrys.app.tui.theme_loader import theme_is_read_only
1993
+ from chrys.app.tui.themes.store import ThemeStoreError, UserThemeStore
1994
+
1995
+ if self.theme_editor is not None:
1996
+ self.theme_editor.editor.focus_first_swatch()
1997
+ return
1998
+ app = cast("ChrysApp", self.app)
1999
+ store = UserThemeStore()
2000
+ theme = app.get_theme(picker.original_theme if picker is not None else app.theme)
2001
+ if theme is None:
2002
+ raise RuntimeError("The selected theme is not registered.")
2003
+ revision = None
2004
+ if not theme_is_read_only(theme.name):
2005
+ try:
2006
+ theme, revision = await asyncio.to_thread(store.load, theme.name)
2007
+ except ThemeStoreError as error:
2008
+ self.notify(render_str(app.locale_controller.localizer, error.display), severity="error", markup=False)
2009
+ return
2010
+ # Keep file reads outside the render batch. The picker may have been
2011
+ # cancelled or covered while awaiting them; do not mount behind a modal.
2012
+ if picker is None:
2013
+ if app.screen is self:
2014
+ await self.open_theme_editor(theme, store, revision)
2015
+ return
2016
+ with app.batch_update():
2017
+ self._theme_editor_opening = True
2018
+ try:
2019
+ restoring_theme = app.theme != picker.original_theme
2020
+ if not picker.dismiss_for_editor():
2021
+ return
2022
+ if restoring_theme:
2023
+ # Finish the queued CSS/signal work before constructing
2024
+ # the dock, so its children receive the final styles once.
2025
+ restored = asyncio.Event()
2026
+ app.call_next(restored.set)
2027
+ await restored.wait()
2028
+ if app.screen is not self:
2029
+ return
2030
+ await self.open_theme_editor(theme, store, revision)
2031
+ # Drain pending mount/layout messages on MainScreen before the
2032
+ # batch's first paint, rather than reflowing again for late ones.
2033
+ mounted = asyncio.Event()
2034
+ if self.call_later(mounted.set):
2035
+ await mounted.wait()
2036
+ finally:
2037
+ self._theme_editor_opening = False
2038
+
2039
+ def action_pick_language(self) -> None:
2040
+ """Open the no-preview language picker modal."""
2041
+ self._navigation.pick_language()
2042
+
2043
+ def action_set_language(self, requested_locale: str) -> None:
2044
+ """Confirm a requested locale from a slash-command argument."""
2045
+ self._navigation.set_language(requested_locale)
2046
+
2047
+ def action_toggle_trajectory_dashboard(self) -> None:
2048
+ """Toggle the in-place trajectory dashboard (F12)."""
2049
+ self._navigation.toggle_trajectory_dashboard()
2050
+
2051
+ def action_escape(self) -> None:
2052
+ """Unified Esc handler — dismiss overlays, confirm interrupt, or confirm exit."""
2053
+ if self.theme_editor is not None:
2054
+ if self.theme_editor.editor.dismiss_dropdown():
2055
+ return
2056
+ self._navigation.escape(on_idle=self.theme_editor.action_close)
2057
+ return
2058
+ if self._suggestions_active_for_bindings():
2059
+ self._suggestions.dismiss_suggestions()
2060
+ return
2061
+ if not self._dashboard_visible() and self._workflow.back():
2062
+ return
2063
+ self._navigation.escape()
2064
+
2065
+ # ------------------------------------------------------------------ #
2066
+ # Approval response
2067
+ # ------------------------------------------------------------------ #
2068
+
2069
+ @work(thread=False)
2070
+ async def _handle_approval_response(
2071
+ self,
2072
+ request_id: str,
2073
+ approved: bool,
2074
+ reason: str = "",
2075
+ modified_args: dict[str, Any] | None = None,
2076
+ remember_choice: ReuseChoice = "",
2077
+ ) -> None:
2078
+ await self._tool_actions.publish_approval_response(request_id, approved, reason, modified_args, remember_choice)
2079
+
2080
+ @work(thread=False)
2081
+ async def _handle_ask_user_response(self, request_id: str, answers: tuple[AskUserAnswer, ...]) -> None:
2082
+ await self._tool_actions.publish_ask_user_response(request_id, answers)
2083
+
2084
+ def _question_inline_preferred(self) -> bool:
2085
+ """``tools.ask_user.inline`` as the Settings panel shows it right now.
2086
+
2087
+ A panel write lands in the store at once but reaches the in-force
2088
+ settings only after the pending reload (turn end + panel close); a
2089
+ question arriving in between honours what the user just ticked.
2090
+ """
2091
+ coordinator = self._existing_settings_coordinator()
2092
+ if coordinator is not None:
2093
+ return bool(coordinator.projected_value("tools.ask_user.inline"))
2094
+ return cast("ChrysApp", self.app).settings_handle.settings.ask_user_inline
2095
+
2096
+ # ------------------------------------------------------------------ #
2097
+ # Debug logging helper
2098
+ # ------------------------------------------------------------------ #
2099
+
2100
+ def _debug(self, event_type: str, detail: str = "") -> None:
2101
+ """Log an event to the debug panel (if mounted)."""
2102
+ try:
2103
+ sp = self.query_one(SidebarPanel)
2104
+ sp.debug_panel.log_event(event_type, detail)
2105
+ except Exception:
2106
+ pass
2107
+
2108
+ @on(ConversationToc.TurnSelected)
2109
+ def _on_toc_turn_selected(self, event: ConversationToc.TurnSelected) -> None:
2110
+ self._navigation.scroll_to_turn(event.turn_id)
2111
+
2112
+ @on(TrajectoryDashboard.StateChanged)
2113
+ def _on_trajectory_dashboard_state_changed(self, event: TrajectoryDashboard.StateChanged) -> None:
2114
+ event.stop()
2115
+ self._view_adapter.sync_main_surface()
2116
+
2117
+ @on(AppHeader.ModeClicked)
2118
+ def _on_mode_clicked(self) -> None:
2119
+ if not self._can_switch_app_mode():
2120
+ # Shell mode is the one block with no run to name; it stays silent.
2121
+ if (notice := self._app_mode_busy_notice()) is not None:
2122
+ self._view_adapter.notify(notice, title=workflow_text.MODE_BUSY_TITLE.bind(), severity="warning")
2123
+ return
2124
+
2125
+ def selected(workflow: bool | None) -> None:
2126
+ if workflow is not None:
2127
+ self._set_workflow_mode(workflow)
2128
+
2129
+ self.app.push_screen(
2130
+ AppModeDialog(self._workflow.workflow_mode, locale_controller=self._locale_controller), selected
2131
+ )
2132
+
2133
+ def action_workflow(self) -> None:
2134
+ if not self._workflow.workflow_mode and not self._can_switch_app_mode():
2135
+ return
2136
+ self._set_workflow_mode(True)
2137
+ self._workflow.browser.enter_selection()
2138
+
2139
+ def _can_switch_app_mode(self) -> bool:
2140
+ """Keep one presentation mode from submission through execution cleanup."""
2141
+ return not self._state.shell.active and self._app_mode_busy_notice() is None
2142
+
2143
+ def _app_mode_busy_notice(self) -> MessageRef | None:
2144
+ """Name the run that pins the app mode, or None while nothing runs."""
2145
+ if self._workflow.awaiting_engine or self._services.execution().kind == "workflow":
2146
+ return workflow_text.MODE_WORKFLOW_BUSY.bind()
2147
+ if (
2148
+ self._state.run.agent_loading
2149
+ or self._state.run.agent_running
2150
+ or self._state.submit.active
2151
+ or self._services.execution_busy()
2152
+ ):
2153
+ return workflow_text.MODE_AGENT_BUSY.bind()
2154
+ return None
2155
+
2156
+ def _set_workflow_mode(self, workflow: bool) -> None:
2157
+ if workflow == self._workflow.workflow_mode or not self._can_switch_app_mode():
2158
+ return
2159
+ input_bar = self.query_one(InputBar)
2160
+ if self._dashboard_visible():
2161
+ self._view_adapter.hide_trajectory_dashboard()
2162
+ if not workflow:
2163
+ self._workflow.leave()
2164
+ self._workflow.workflow_mode = workflow
2165
+ self.query_one(SidebarPanel).set_workflow_mode(workflow)
2166
+ self._sync_sidebar_tab_strip_focus()
2167
+ self._view_adapter.sync_main_surface()
2168
+ self._suggestions.dismiss_suggestions()
2169
+ self._sync_workflow_chrome()
2170
+ self._sync_execution_chrome()
2171
+ if workflow:
2172
+ self._workflow.enter()
2173
+ else:
2174
+ input_bar.focus_input()
2175
+
2176
+ async def _apply_workflow_cwd(self, cwd: str) -> bool:
2177
+ if not self._workflow.workflow_mode:
2178
+ return False
2179
+ try:
2180
+ await self._workflow.browser.change_draft_workspace(cwd)
2181
+ except (OSError, ValueError) as exc:
2182
+ self._workflow.show_error(str(exc))
2183
+ return True
2184
+
2185
+ def _accept_approval_update(self, event: ApprovalModeUpdated) -> bool:
2186
+ """The engine's launch policy is authoritative in both app modes."""
2187
+ return event.session_id is None or event.session_id == self.chat_session_id
2188
+
2189
+ def _route_session_error(self, event: Error) -> bool:
2190
+ if not event.session_id or event.session_id != self._workflow.session_id:
2191
+ return False
2192
+ self._workflow.feedback.notify(event.display_message or event.message)
2193
+ self._workflow.request_refresh()
2194
+ return True
2195
+
2196
+ def _sync_workflow_chrome(self) -> None:
2197
+ # The approval badge is the same in both app modes: one mode is chosen per launch.
2198
+ self.query_one(AppHeader).set_workflow_mode(self._workflow.workflow_mode)
2199
+ self._sync_main_bindings()
2200
+
2201
+ async def _on_execution_changed(self, _event: ExecutionChanged) -> None:
2202
+ self._sync_execution_chrome()
2203
+ self._workflow.request_refresh()
2204
+
2205
+ def _sync_execution_chrome(self) -> None:
2206
+ self.query_one(StatusBar).set_execution_busy(self._services.execution_busy())
2207
+ self._sync_main_bindings()
2208
+
2209
+ def _sync_main_bindings(self) -> None:
2210
+ execution_busy = self._services.execution_busy()
2211
+ trajectory_available = self._navigation.can_toggle_trajectory_dashboard()
2212
+ if trajectory_available != self._trajectory_binding_available or execution_busy != self._execution_binding_busy:
2213
+ self._trajectory_binding_available = trajectory_available
2214
+ self._execution_binding_busy = execution_busy
2215
+ self.refresh_bindings()
2216
+
2217
+ def _sync_workflow_timer(self) -> None:
2218
+ visible = self._workflow.workflow_mode and self._workflow_panel.display and self.app.screen is self
2219
+ if visible and self._workflow_timer is None:
2220
+ self._workflow_timer = self.set_interval(0.1, self._workflow.tick)
2221
+ elif not visible and self._workflow_timer is not None:
2222
+ self._workflow_timer.stop()
2223
+ self._workflow_timer = None
2224
+
2225
+ @on(WorkflowPanel.RunSelected)
2226
+ async def _on_workflow_run_selected(self, event: WorkflowPanel.RunSelected) -> None:
2227
+ event.stop()
2228
+ await self._workflow.session_view.select_run(event.run_id)
2229
+
2230
+ @on(WorkflowPanel.StartRequested)
2231
+ def _on_workflow_start(self) -> None:
2232
+ self._workflow.run_control.collect_input()
2233
+
2234
+ @on(WorkflowPanel.NewSessionRequested)
2235
+ def _on_workflow_new(self) -> None:
2236
+ self._workflow.browser.enter_selection(new_session=True)
2237
+
2238
+ @on(WorkflowPanel.StopRequested)
2239
+ def _on_workflow_stop(self) -> None:
2240
+ self._workflow.run_control.stop()
2241
+
2242
+ @on(WorkflowPanel.ResultRequested)
2243
+ def _on_workflow_result(self, event: WorkflowPanel.ResultRequested) -> None:
2244
+ event.stop()
2245
+ self._workflow.session_view.open_result()
2246
+
2247
+ @on(WorkflowGraph.NodeSelected)
2248
+ def _on_workflow_node(self, event: WorkflowGraph.NodeSelected) -> None:
2249
+ event.stop()
2250
+ self._workflow.session_view.open_node(event.node_id)
2251
+
2252
+ @on(WorkflowGraph.RetryRequested)
2253
+ def _on_workflow_retry(self, event: WorkflowGraph.RetryRequested) -> None:
2254
+ event.stop()
2255
+ run = self._workflow.session_view.projector.run(event.target.run_id)
2256
+ attempt = run.nodes.get(event.node_id) if run is not None else None
2257
+ if attempt is not None and (attempt.activation_id, attempt.attempt) == (
2258
+ event.target.activation_id,
2259
+ event.target.attempt,
2260
+ ):
2261
+ self._workflow.run_control.retry_node(attempt)
2262
+
2263
+ @on(WorkflowPanel.ViewChanged)
2264
+ def _on_workflow_view_changed(self) -> None:
2265
+ self._workflow.view_changed()