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,1127 @@
1
+ # Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
2
+
3
+ """Session lifecycle handlers — create, restore, delete, profile/workspace switching."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import asyncio
8
+ import contextlib
9
+ import inspect
10
+ import os
11
+ from collections.abc import Awaitable, Callable
12
+ from dataclasses import dataclass
13
+ from enum import Enum
14
+ from functools import partial
15
+ from typing import TYPE_CHECKING, Any
16
+
17
+ from chrys.app.tui.i18n import render_str
18
+ from chrys.app.tui.screens.main.main_screen_presenter import MainScreenPresenter
19
+ from chrys.app.tui.screens.main.ports import (
20
+ AgentLoadUiPort,
21
+ BackendEventView,
22
+ NotificationSeverity,
23
+ ProfileDescriptionProvider,
24
+ RuntimeInfoProvider,
25
+ StatusMessage,
26
+ StatusTrail,
27
+ )
28
+ from chrys.app.tui.screens.main.runtime_info import RegistryRuntimeInfoProvider
29
+ from chrys.app.tui.screens.main.state import MainScreenServices, MainScreenState
30
+ from chrys.app.tui.screens.sessions import WorkflowSessionPick
31
+ from chrys.app.tui.support.gc_freeze import (
32
+ GcAbsorbReason,
33
+ GcAbsorbRequested,
34
+ GcReclaimReason,
35
+ GcReclaimRequested,
36
+ )
37
+ from chrys.app.tui.support.workspace_mru import schedule_workspace_mru_touches
38
+ from chrys.app.tui.widgets.chrome.input_bar import INPUT_CONTINUE, INPUT_RETRY
39
+ from chrys.app.tui.widgets.chrome.status_bar import (
40
+ STATUS_CUSTOM_TITLE_CLEARED,
41
+ STATUS_FORK_CREATED,
42
+ STATUS_FORK_NOTICE,
43
+ STATUS_OPENED_FORK,
44
+ STATUS_SESSION_RESTORED,
45
+ STATUS_SESSION_TITLE_UPDATED,
46
+ )
47
+ from chrys.app.tui.widgets.sidebar.context import ContextUsageState
48
+ from chrys.foundation.events.types import (
49
+ AgentRuntimeDetails,
50
+ Error,
51
+ ProfileSwitched,
52
+ SessionFork,
53
+ SessionForked,
54
+ SessionRestored,
55
+ SessionSaved,
56
+ SessionTitleUpdated,
57
+ WorkspaceUpdated,
58
+ )
59
+ from chrys.foundation.i18n import DisplayPath, MessageRef, msg
60
+ from chrys.foundation.i18n.formatting import format_message
61
+ from chrys.foundation.models.history_markers import HistoryMarkerKind
62
+ from chrys.foundation.models.todos import parse_todo_items
63
+ from chrys.foundation.platform import safe_getcwd
64
+ from chrys.foundation.util.session_ids import session_short_id
65
+ from chrys.service.session.persistence import has_real_messages
66
+
67
+ if TYPE_CHECKING:
68
+ from chrys.app.tui.i18n import LocaleController
69
+ from chrys.app.tui.screens.dialogs.fork_session import ForkSessionDialog
70
+ from chrys.app.tui.screens.main.ports import StartWorker
71
+ from chrys.app.tui.widgets.chat.file_snapshot import FileSnapshotPayload
72
+ from chrys.foundation.events.bus import EventBus
73
+ from chrys.service.context.providers.history import CompressedBlock
74
+ from chrys.service.state.store import StateStore
75
+
76
+
77
+ _FINISH_SESSION_RESTORED = msg(
78
+ "tui.agent_load.finish.session_restored",
79
+ fallback="Session restored: {session_id}",
80
+ )
81
+ _FINISH_AGENT_READY = msg("tui.agent_load.finish.agent_ready", fallback="Agent ready: {agent}")
82
+ _FINISH_PROFILE_SWITCHED = msg(
83
+ "tui.agent_load.finish.profile_switched",
84
+ fallback="Profile switched: {from_label} -> {to_label}",
85
+ )
86
+ _RESUME_TITLE = msg("tui.session.title.resume", fallback="Resume")
87
+ _FORK_TITLE = msg("tui.session.title.fork", fallback="Fork")
88
+ CLEAR_SESSION_TITLE = msg("tui.main.confirm.clear_title", fallback="Clear Session")
89
+ _CLEAR_FAILED = msg(
90
+ "tui.session.clear.failed",
91
+ fallback="The current session was kept: {message}",
92
+ )
93
+ _SESSION_TITLE_TITLE = msg("tui.session.title.session_title", fallback="Session Title")
94
+ _NO_SAVED_SESSIONS = msg("tui.session.resume.none", fallback="No saved sessions found in this mode")
95
+ _FORK_TURN_RUNNING = msg(
96
+ "tui.session.fork.turn_running",
97
+ fallback="Cannot fork while a turn is running",
98
+ )
99
+ _FORK_EMPTY_SESSION = msg(
100
+ "tui.session.fork.empty_session",
101
+ fallback="Cannot fork an empty session",
102
+ )
103
+ _FORK_NO_ACTIVE_SESSION = msg(
104
+ "tui.session.fork.no_active_session",
105
+ fallback="No active session to fork",
106
+ )
107
+ _FORK_DIALOG_FAILED = msg(
108
+ "tui.session.fork.dialog_failed",
109
+ fallback="Could not open fork dialog",
110
+ )
111
+ _SESSION_TITLE_SAVE_FAILED = msg(
112
+ "tui.session.title.save_failed",
113
+ fallback="Failed to save session title",
114
+ )
115
+ _FORK_CREATED = msg("tui.session.fork.created", fallback="Created fork {fork_id}")
116
+ _PROFILE_SWITCH_INDICATOR = msg(
117
+ "tui.session.profile_switch_indicator",
118
+ fallback="Agent profile switched: {from_label} → {to_label}",
119
+ )
120
+ _WORKING_DIRECTORY_INDICATOR = msg(
121
+ "tui.session.working_directory_indicator",
122
+ fallback="Working directory → {path}",
123
+ )
124
+
125
+
126
+ class RestoreRequest(Enum):
127
+ """What :meth:`SessionHandler.do_session_restore` did."""
128
+
129
+ REQUESTED = "requested"
130
+ """A restore was published; ``SessionRestored`` or an ``Error`` follows."""
131
+ DECLINED = "declined"
132
+ """The session's folder is gone and the user chose no other one."""
133
+ SKIPPED = "skipped"
134
+ """Nothing was published (busy, or the loading UI could not open)."""
135
+
136
+
137
+ async def _decline_missing_working_dir(_missing: str) -> str | None:
138
+ return None
139
+
140
+
141
+ @dataclass(frozen=True, slots=True)
142
+ class SessionCallbacks:
143
+ """Screen-owned effects required by session handlers."""
144
+
145
+ set_agent_loading: Callable[[bool], None]
146
+ set_has_messages: Callable[[bool], None]
147
+ set_creating_new_session: Callable[[bool], None]
148
+ set_restoring_session: Callable[[bool], None]
149
+ set_profile_display: Callable[[str], None]
150
+ set_active_model_profile_id: Callable[[str], None]
151
+ set_workspace_cwd: Callable[[str], None]
152
+ update_subtitle: Callable[[], None]
153
+ update_toc: Callable[[], None]
154
+ clear_suggestion_file_cache: Callable[[], None]
155
+ start_worker: StartWorker
156
+ post_gc_message: Callable[[GcAbsorbRequested | GcReclaimRequested], object]
157
+ debug: Callable[[str, str], None]
158
+ refresh_model_indicator: Callable[[], None]
159
+ choose_missing_working_dir: Callable[[str], Awaitable[str | None]] = _decline_missing_working_dir
160
+
161
+
162
+ def _samepath(a: str, b: str) -> bool:
163
+ """Compare paths using OS semantics (handles case-insensitive FS, symlinks)."""
164
+ try:
165
+ return os.path.samefile(a, b)
166
+ except OSError, ValueError:
167
+ return a == b
168
+
169
+
170
+ class SessionHandler:
171
+ """Handles session lifecycle and profile/workspace switching.
172
+
173
+ Owns chain-tracking state for profile switches and chdir operations.
174
+ Constructed once by the main-screen owner.
175
+ """
176
+
177
+ def __init__(
178
+ self,
179
+ *,
180
+ state: MainScreenState,
181
+ services: MainScreenServices,
182
+ view: BackendEventView,
183
+ callbacks: SessionCallbacks,
184
+ agent_load: AgentLoadUiPort,
185
+ runtime_info: RuntimeInfoProvider | None = None,
186
+ profile_descriptions: ProfileDescriptionProvider | None = None,
187
+ locale_controller: LocaleController | None = None,
188
+ ) -> None:
189
+ self._state = state
190
+ self._services = services
191
+ self._view = view
192
+ self._callbacks = callbacks
193
+ self._presenter = MainScreenPresenter(view, state)
194
+ runtime_info = runtime_info or RegistryRuntimeInfoProvider(services)
195
+ self._agent_load = agent_load
196
+ self._runtime_info = runtime_info
197
+ self._profile_descriptions = profile_descriptions or runtime_info
198
+ self._locale_controller = locale_controller
199
+ self._fork_dialog: ForkSessionDialog | None = None
200
+ self._fork_session_id = ""
201
+ self._fork_new_session_id = ""
202
+ # Serializes custom-title saves: FileLock acquisition is not FIFO,
203
+ # so two rapid dialog saves could otherwise land out of order.
204
+ self._custom_title_apply_lock = asyncio.Lock()
205
+
206
+ def _ui(self) -> MainScreenPresenter:
207
+ """Return the screen presenter."""
208
+ return self._presenter
209
+
210
+ def _render_message(self, reference: MessageRef) -> str:
211
+ controller = self._locale_controller
212
+ return format_message(reference) if controller is None else render_str(controller.localizer, reference)
213
+
214
+ def _format_tool_info(
215
+ self,
216
+ tool_names: list[str],
217
+ skill_names: list[str],
218
+ *,
219
+ memory_files: list[str] | None = None,
220
+ runtime_details: AgentRuntimeDetails | None = None,
221
+ ) -> StatusTrail:
222
+ return self._runtime_info.format_tool_info(
223
+ tool_names,
224
+ skill_names,
225
+ memory_files=memory_files,
226
+ runtime_details=runtime_details,
227
+ )
228
+
229
+ def _get_profile_description(self, profile_name: str) -> str:
230
+ return self._profile_descriptions.get_profile_description(profile_name)
231
+
232
+ @property
233
+ def bus(self) -> EventBus:
234
+ return self._services.bus
235
+
236
+ @property
237
+ def state_store(self) -> StateStore | None:
238
+ return self._services.state_store
239
+
240
+ @property
241
+ def agent_running(self) -> bool:
242
+ return self._state.run.agent_running
243
+
244
+ @property
245
+ def agent_loading(self) -> bool:
246
+ return self._state.run.agent_loading
247
+
248
+ def set_agent_loading(self, loading: bool) -> None:
249
+ self._state.run.agent_loading = loading
250
+ self._callbacks.set_agent_loading(loading)
251
+
252
+ @property
253
+ def has_messages(self) -> bool:
254
+ return self._state.run.has_messages
255
+
256
+ def set_has_messages(self, has_messages: bool) -> None:
257
+ self._state.run.has_messages = has_messages
258
+ self._callbacks.set_has_messages(has_messages)
259
+
260
+ @property
261
+ def creating_new_session(self) -> bool:
262
+ return self._state.session.creating_new_session
263
+
264
+ @creating_new_session.setter
265
+ def creating_new_session(self, value: bool) -> None:
266
+ self._state.session.creating_new_session = value
267
+ self._callbacks.set_creating_new_session(value)
268
+
269
+ @property
270
+ def restoring_session(self) -> bool:
271
+ return self._state.session.restoring_session
272
+
273
+ @restoring_session.setter
274
+ def restoring_session(self, value: bool) -> None:
275
+ self._state.session.restoring_session = value
276
+ self._callbacks.set_restoring_session(value)
277
+
278
+ @property
279
+ def profile(self) -> str:
280
+ return self._state.runtime.profile
281
+
282
+ @profile.setter
283
+ def profile(self, value: str) -> None:
284
+ self._state.runtime.profile = value
285
+ self._callbacks.set_profile_display(value)
286
+
287
+ @property
288
+ def runtime_metadata(self) -> AgentRuntimeDetails:
289
+ return self._state.runtime.details
290
+
291
+ @runtime_metadata.setter
292
+ def runtime_metadata(self, value: AgentRuntimeDetails) -> None:
293
+ self._state.runtime.details = value
294
+
295
+ @property
296
+ def main_usage_source_id(self) -> str:
297
+ return self._state.runtime.main_usage_source_id
298
+
299
+ @main_usage_source_id.setter
300
+ def main_usage_source_id(self, value: str) -> None:
301
+ self._state.runtime.main_usage_source_id = value
302
+
303
+ @property
304
+ def last_usage_tokens(self) -> int:
305
+ return self._state.usage.last_usage_tokens
306
+
307
+ @last_usage_tokens.setter
308
+ def last_usage_tokens(self, value: int) -> None:
309
+ self._state.usage.last_usage_tokens = value
310
+
311
+ @property
312
+ def last_total_session_tokens(self) -> int:
313
+ return self._state.usage.last_total_session_tokens
314
+
315
+ @last_total_session_tokens.setter
316
+ def last_total_session_tokens(self, value: int) -> None:
317
+ self._state.usage.last_total_session_tokens = value
318
+
319
+ @property
320
+ def context_usage_state(self) -> ContextUsageState | None:
321
+ return self._view.context_usage_state
322
+
323
+ @property
324
+ def chdir_current_cwd(self) -> str:
325
+ return self._state.workspace_marker.current_cwd
326
+
327
+ @chdir_current_cwd.setter
328
+ def chdir_current_cwd(self, value: str) -> None:
329
+ self._state.workspace.current_cwd = value
330
+ self._state.workspace_marker.current_cwd = value
331
+ self._callbacks.set_workspace_cwd(value)
332
+
333
+ @property
334
+ def chdir_original_cwd(self) -> str | None:
335
+ return self._state.workspace_marker.original_cwd
336
+
337
+ @chdir_original_cwd.setter
338
+ def chdir_original_cwd(self, value: str | None) -> None:
339
+ self._state.workspace_marker.original_cwd = value
340
+
341
+ @property
342
+ def profile_switch_from(self) -> str | None:
343
+ return self._state.profile_marker.from_profile
344
+
345
+ @profile_switch_from.setter
346
+ def profile_switch_from(self, value: str | None) -> None:
347
+ self._state.profile_marker.from_profile = value
348
+
349
+ @property
350
+ def profile_switch_to(self) -> str | None:
351
+ return self._state.profile_marker.to_profile
352
+
353
+ @profile_switch_to.setter
354
+ def profile_switch_to(self, value: str | None) -> None:
355
+ self._state.profile_marker.to_profile = value
356
+
357
+ @property
358
+ def profile_switch_seq(self) -> int:
359
+ return self._state.profile_marker.seq
360
+
361
+ @profile_switch_seq.setter
362
+ def profile_switch_seq(self, value: int) -> None:
363
+ self._state.profile_marker.seq = value
364
+
365
+ def notify(
366
+ self,
367
+ message: StatusMessage,
368
+ *,
369
+ title: StatusMessage,
370
+ severity: NotificationSeverity = "information",
371
+ timeout: float | None = 3,
372
+ ) -> None:
373
+ self._view.notify(message, title=title, severity=severity, timeout=timeout)
374
+
375
+ def push_screen(self, screen: object, callback: object | None = None) -> object:
376
+ return self._view.push_screen(screen, callback)
377
+
378
+ def start_session_restore(self, session_id: str) -> object:
379
+ """Run :meth:`do_session_restore` in a screen worker."""
380
+ return self._callbacks.start_worker(partial(self.do_session_restore, session_id))
381
+
382
+ def workspace_cwd(self) -> str:
383
+ return self.chdir_current_cwd or self._state.workspace.current_cwd or safe_getcwd()
384
+
385
+ def update_subtitle(self) -> None:
386
+ self._callbacks.update_subtitle()
387
+
388
+ def update_toc(self) -> None:
389
+ self._callbacks.update_toc()
390
+
391
+ def clear_suggestion_file_cache(self) -> None:
392
+ self._callbacks.clear_suggestion_file_cache()
393
+
394
+ def debug(self, key: str, message: str = "") -> None:
395
+ self._callbacks.debug(key, message)
396
+
397
+ # -------------------------------------------------------------- #
398
+ # Session lifecycle
399
+ # -------------------------------------------------------------- #
400
+
401
+ async def delete_current_and_new(self, session_id: str) -> None:
402
+ """Delete the current session and start fresh as ONE fenced backend operation.
403
+
404
+ Input is blocked for the whole operation (typed text is kept), and the
405
+ fresh session only starts once the backend acknowledged the delete with
406
+ ``SessionDeleted``. A failed delete keeps the current session: the
407
+ backend reports ``session_clear_failed`` (toast via
408
+ :meth:`on_session_clear_error`) and never starts a new session.
409
+ """
410
+ s = self
411
+ # Guard and claim the UI synchronously — no await between them — so
412
+ # the /clear preflight and "input blocked" are one step: a submit that
413
+ # is still being admitted (``submit.active``; ``agent_running`` flips
414
+ # only afterwards) vetoes the clear, and nothing can be admitted once
415
+ # ``agent_loading`` is set.
416
+ if s.agent_loading or s.agent_running or self._state.submit.active or self._services.execution_busy():
417
+ return
418
+ s.set_agent_loading(True)
419
+ s.creating_new_session = True
420
+ from chrys.foundation.events.types import SessionClear, SessionDeleted
421
+
422
+ deleted = False
423
+
424
+ async def _on_deleted(event: SessionDeleted) -> None:
425
+ nonlocal deleted
426
+ if event.session_id == session_id:
427
+ deleted = True
428
+
429
+ await s.bus.subscribe(SessionDeleted, _on_deleted)
430
+ try:
431
+ await s.bus.publish(SessionClear(session_id=session_id))
432
+ finally:
433
+ await s.bus.unsubscribe(SessionDeleted, _on_deleted)
434
+ if not deleted:
435
+ # Nothing was deleted, so no fresh session was started: undo the
436
+ # optimistic new-session/loading state (idempotent with the
437
+ # ``session_clear_failed`` toast path).
438
+ s.creating_new_session = False
439
+ s.set_agent_loading(False)
440
+
441
+ async def create_new_session(self) -> None:
442
+ s = self
443
+ if s.agent_loading or s.agent_running or self._services.execution_busy():
444
+ return
445
+ from chrys.foundation.events.types import SessionNew
446
+
447
+ s.creating_new_session = True
448
+ await s.bus.publish(SessionNew())
449
+
450
+ async def resume_last_session(self) -> None:
451
+ """Resume the most recent chat session.
452
+
453
+ The restore modal opens before the lookup so a slow first-time index
454
+ backfill still shows progress.
455
+ """
456
+ s = self
457
+ if not s.state_store or s.agent_running or s.agent_loading or self._services.execution_busy():
458
+ return
459
+ s.restoring_session = True
460
+ try:
461
+ # Empty id: the modal opens with a blank subtitle and the resolved
462
+ # id fills it in via ``do_session_restore`` (same dialog).
463
+ await self._agent_load.begin_session_restore_load("")
464
+ selection = await s.state_store.load_latest_session_id(chat_only=True)
465
+ except Exception as exc:
466
+ s.restoring_session = False
467
+ with contextlib.suppress(Exception):
468
+ self._agent_load.cancel_agent_load()
469
+ s.debug("SessionRestore", f"failed to look up latest session: {exc}")
470
+ return
471
+ if selection is None:
472
+ s.restoring_session = False
473
+ self._agent_load.cancel_agent_load()
474
+ s.notify(_NO_SAVED_SESSIONS.bind(), title=_RESUME_TITLE.bind(), severity="warning", timeout=3)
475
+ return
476
+ # The lookup modal already marked the screen as loading.
477
+ await self.do_session_restore(selection, allow_while_loading=True)
478
+
479
+ async def fork_current_session(self) -> None:
480
+ """Request a backend-managed fork of the current saved session."""
481
+ s = self
482
+ ui = self._ui()
483
+
484
+ if s.agent_loading:
485
+ return
486
+ if s.agent_running or self._services.execution_busy():
487
+ s.notify(_FORK_TURN_RUNNING.bind(), title=_FORK_TITLE.bind(), severity="warning", timeout=3)
488
+ return
489
+ if not s.has_messages:
490
+ s.notify(_FORK_EMPTY_SESSION.bind(), title=_FORK_TITLE.bind(), severity="warning", timeout=3)
491
+ return
492
+ session_id = ui.chat_session_id()
493
+ if not session_id:
494
+ s.notify(_FORK_NO_ACTIVE_SESSION.bind(), title=_FORK_TITLE.bind(), severity="warning", timeout=3)
495
+ return
496
+ try:
497
+ await self._open_session_fork_dialog(session_id)
498
+ except Exception as exc:
499
+ self._reset_session_fork_dialog()
500
+ s.set_agent_loading(False)
501
+ s.notify(_FORK_DIALOG_FAILED.bind(), title=_FORK_TITLE.bind(), severity="error", timeout=5)
502
+ s.debug("SessionFork", f"failed to open dialog: {exc}")
503
+ return
504
+ try:
505
+ await s.bus.publish(SessionFork(session_id=session_id))
506
+ except Exception as exc:
507
+ self.on_session_fork_error(
508
+ Error(session_id=session_id, code="session_fork_failed", message=str(exc)),
509
+ message=str(exc),
510
+ severity="error",
511
+ )
512
+
513
+ async def _open_session_fork_dialog(self, session_id: str) -> None:
514
+ """Open the non-dismissible fork progress dialog and mark the UI busy."""
515
+ s = self
516
+ from chrys.app.tui.screens.dialogs.fork_session import ForkSessionDialog
517
+ from chrys.app.tui.terminal.launcher import can_open_new_chrys_window
518
+
519
+ if self._fork_dialog is not None:
520
+ return
521
+ dialog = ForkSessionDialog(
522
+ show_new_window=can_open_new_chrys_window(),
523
+ locale_controller=self._locale_controller,
524
+ )
525
+ self._fork_dialog = dialog
526
+ self._fork_session_id = session_id
527
+ self._fork_new_session_id = ""
528
+ s.set_agent_loading(True)
529
+ pushed = s.push_screen(dialog, callback=self._handle_session_fork_result)
530
+ if inspect.isawaitable(pushed):
531
+ await pushed
532
+
533
+ def _handle_session_fork_result(self, result: object) -> None:
534
+ """Route the resolved fork dialog action."""
535
+ s = self
536
+ ui = self._ui()
537
+
538
+ new_session_id = self._fork_new_session_id
539
+ fork_short_id = session_short_id(new_session_id) if new_session_id else ""
540
+ self._reset_session_fork_dialog()
541
+ s.set_agent_loading(False)
542
+
543
+ if result == "switch" and new_session_id:
544
+ s.start_session_restore(new_session_id)
545
+ return
546
+ if result == "new_window" and new_session_id:
547
+ from chrys.app.tui.terminal.launcher import TerminalLaunchError, launch_new_chrys_window
548
+
549
+ try:
550
+ launch_new_chrys_window(new_session_id, cwd=s.workspace_cwd())
551
+ except TerminalLaunchError as exc:
552
+ message = str(exc) if exc.display_message is None else self._render_message(exc.display_message)
553
+ s.notify(message, title=_FORK_TITLE.bind(), severity="warning", timeout=5)
554
+ return
555
+ ui.flash_status(STATUS_OPENED_FORK.bind(fork_id=fork_short_id))
556
+ return
557
+ if result == "stay" and fork_short_id:
558
+ ui.flash_status(STATUS_FORK_CREATED.bind(fork_id=fork_short_id))
559
+
560
+ def _reset_session_fork_dialog(self) -> None:
561
+ self._fork_dialog = None
562
+ self._fork_session_id = ""
563
+ self._fork_new_session_id = ""
564
+
565
+ async def do_session_restore(
566
+ self,
567
+ session_id: str | WorkflowSessionPick,
568
+ *,
569
+ allow_while_loading: bool = False,
570
+ primary_cwd: str = "",
571
+ ) -> RestoreRequest:
572
+ """Restore *session_id*, in *primary_cwd* when given instead of its saved folder.
573
+
574
+ A session whose saved folder is gone is restored only in a folder the
575
+ user picks; declining leaves the current session as it was.
576
+ """
577
+ s = self
578
+ if s.agent_running or self._services.execution_busy() or (s.agent_loading and not allow_while_loading):
579
+ return RestoreRequest.SKIPPED
580
+ if isinstance(session_id, WorkflowSessionPick):
581
+ await self._restore_workflow_session(session_id)
582
+ return RestoreRequest.REQUESTED
583
+ from chrys.foundation.events.types import SessionRestore
584
+
585
+ if not primary_cwd and (missing := await self._missing_saved_cwd(session_id)) is not None:
586
+ # /resume and startup open their loading UI first; the prompt replaces it.
587
+ if s.agent_loading:
588
+ with contextlib.suppress(Exception):
589
+ self._agent_load.cancel_agent_load()
590
+ s.restoring_session = False
591
+ chosen = await self._callbacks.choose_missing_working_dir(missing)
592
+ if chosen is None:
593
+ return RestoreRequest.DECLINED
594
+ return await self.do_session_restore(
595
+ session_id, allow_while_loading=allow_while_loading, primary_cwd=chosen
596
+ )
597
+
598
+ s.restoring_session = True
599
+ try:
600
+ await self._agent_load.begin_session_restore_load(session_id)
601
+ except Exception as exc:
602
+ s.restoring_session = False
603
+ with contextlib.suppress(Exception):
604
+ self._agent_load.cancel_agent_load()
605
+ s.debug("SessionRestore", f"failed to open loading UI: {exc}")
606
+ return RestoreRequest.SKIPPED
607
+ await s.bus.publish(
608
+ SessionRestore(
609
+ session_id=session_id,
610
+ primary_cwd=primary_cwd,
611
+ apply_saved_model=self._services.apply_saved_model_on_restore,
612
+ )
613
+ )
614
+ return RestoreRequest.REQUESTED
615
+
616
+ async def _missing_saved_cwd(self, session_id: str) -> str | None:
617
+ """The saved folder of *session_id* when it no longer exists, read the way restore reads it.
618
+
619
+ Restoring another session prefers its crash-recovery copy, so this
620
+ does too. Read failures are left for the restore itself to report.
621
+ """
622
+ store = self.state_store
623
+ if store is None:
624
+ return None
625
+ try:
626
+ meta = await store.load_session_meta(session_id, prefer_recovery=True)
627
+ except Exception:
628
+ return None
629
+ saved = meta.primary_cwd if meta is not None else ""
630
+ return saved if saved and not os.path.isdir(saved) else None
631
+
632
+ async def _restore_workflow_session(self, selection: WorkflowSessionPick) -> None:
633
+ self.restoring_session = True
634
+ try:
635
+ await self._view.restore_workflow_session(selection.session_id)
636
+ finally:
637
+ self.restoring_session = False
638
+
639
+ def load_file_edit_snapshots(
640
+ self,
641
+ session_id: str,
642
+ messages: list[dict],
643
+ state: dict[str, Any] | None,
644
+ ) -> dict[str, list[FileSnapshotPayload]]:
645
+ """Build ``{fw_call_id: [snapshot_payload, ...]}`` from MutationTracker data.
646
+
647
+ Returns a list per ``call_id`` so duplicate call_ids (observed when
648
+ some LLMs reuse ids across retries after approval rejection) keep
649
+ distinct snapshots. ``replay_history`` consumes these as available
650
+ file-tool snapshot cursors independent of tool-result pairing.
651
+ """
652
+ s = self
653
+ if s.state_store is None:
654
+ return {}
655
+ try:
656
+ from chrys.app.tui.widgets.chat.file_snapshot import snapshot_payload_from_hashes
657
+ from chrys.service.mutations.store import SnapshotStore
658
+ from chrys.service.mutations.tracker import MutationTracker
659
+
660
+ mutations_data = state.get("chrys_mutations") if state is not None else None
661
+ if not mutations_data:
662
+ return {}
663
+
664
+ # Route through the store so the folder derivation matches
665
+ # wherever the session was actually saved (handles short/long
666
+ # ids and alternate store roots, e.g. in tests).
667
+ session_dir = s.state_store.session_dir(session_id)
668
+ store = SnapshotStore(session_dir)
669
+ tracker = MutationTracker.deserialize(mutations_data, store)
670
+ edit_snapshot_refs = tracker.get_file_edit_snapshot_refs()
671
+ if not edit_snapshot_refs:
672
+ return {}
673
+
674
+ _file_tools = {"edit_file", "write_file"}
675
+ fw_call_ids: list[str] = []
676
+ for message_item in messages:
677
+ if message_item.get("role") != "assistant":
678
+ continue
679
+ additional = message_item.get("additional_properties")
680
+ if isinstance(additional, dict) and HistoryMarkerKind.KEY in additional:
681
+ # Marker payloads are chrome that replay never renders;
682
+ # the tracker records snapshots only for executed calls,
683
+ # so a marker-carried call must not shift the zip.
684
+ continue
685
+ for c in message_item.get("contents", []):
686
+ if isinstance(c, dict) and c.get("type") == "function_call":
687
+ name = c.get("name", "")
688
+ cid = c.get("call_id", "")
689
+ if name in _file_tools and cid:
690
+ fw_call_ids.append(cid)
691
+
692
+ # Positional pairing: fw_call_ids[i] ↔ edit_snapshots[i]. Bucket
693
+ # into per-call_id lists so duplicate call_ids each keep their
694
+ # own snapshot. ``zip(..., strict=False)`` silently truncates if
695
+ # the lists disagree in length — preserves existing fail-soft
696
+ # semantics when history and tracker data are out of sync.
697
+ buckets: dict[str, list[FileSnapshotPayload]] = {}
698
+ for cid, snap_ref in zip(fw_call_ids, edit_snapshot_refs, strict=False):
699
+ buckets.setdefault(cid, []).append(
700
+ snapshot_payload_from_hashes(store.mutations_dir, snap_ref.before, snap_ref.after)
701
+ )
702
+ return buckets
703
+ except Exception:
704
+ return {}
705
+
706
+ # -------------------------------------------------------------- #
707
+ # EventBus callbacks
708
+ # -------------------------------------------------------------- #
709
+
710
+ async def on_session_restored(self, event: SessionRestored) -> None:
711
+ s = self
712
+ ui = self._ui()
713
+
714
+ self._state.workspace.roots = [event.primary_cwd, *event.working_dirs]
715
+ s.restoring_session = False
716
+ restored_has_messages = event.message_count > 0
717
+ s.chdir_original_cwd = None
718
+ cwd = event.primary_cwd or safe_getcwd()
719
+ s.chdir_current_cwd = cwd
720
+ ui.set_terminal_title_for_cwd(cwd)
721
+ if os.path.isdir(cwd):
722
+ await ui.change_shell_directory(cwd)
723
+ ui.set_input_retry_mode(False)
724
+ ui.set_input_paste_cwd(cwd)
725
+ ui.set_input_clipboard_dir(event.session_id)
726
+ display = event.display_name or event.agent_profile
727
+ s.main_usage_source_id = event.session_id
728
+ await ui.clear_chat()
729
+ ui.set_chat_workspace_cwd(cwd)
730
+ ui.update_welcome(profile=display, cwd=cwd)
731
+ ui.set_chat_session_id(event.session_id)
732
+ await self._restore_session_title_state(
733
+ event.session_id,
734
+ prefer_recovery=event.recovered_from_sidecar,
735
+ )
736
+
737
+ # Load compressed blocks (needed for both replay and Context Panel)
738
+ compressed_blocks_map: dict[str, CompressedBlock] = {}
739
+ loaded_state: dict[str, Any] | None = None
740
+ with contextlib.suppress(Exception):
741
+ ui.clear_compressed_blocks()
742
+ prefer_recovery = event.recovered_from_sidecar
743
+ if s.state_store:
744
+ try:
745
+ loaded = await s.state_store.load_session(event.session_id, prefer_recovery=prefer_recovery)
746
+ if loaded:
747
+ loaded_state = loaded
748
+ restored_has_messages = has_real_messages(loaded_state)
749
+ for block in loaded_state.get("compressed_msgs", []):
750
+ ctx_id = block.compressed_context_id
751
+ if ctx_id:
752
+ compressed_blocks_map[ctx_id] = block
753
+ ui.add_compressed_block(
754
+ ctx_id,
755
+ block.summary_text,
756
+ len(block.messages),
757
+ block.turn_range,
758
+ )
759
+ except Exception:
760
+ pass
761
+ s.set_has_messages(restored_has_messages)
762
+
763
+ # Reseed the Tasks panel from the restored state (covers session
764
+ # switch AND rollback-to-N) via the shared tolerant decoder — an
765
+ # ad-hoc parse could crash the sidebar on malformed/legacy state.
766
+ ui.set_todo_state(parse_todo_items(loaded_state.get("chrys_todos") if loaded_state else None))
767
+
768
+ # Replay conversation history from saved state
769
+ messages: list[dict[str, Any]] | None = None
770
+ if s.state_store:
771
+ messages = await s.state_store.load_session_raw(event.session_id, prefer_recovery=prefer_recovery)
772
+ if messages:
773
+ file_snapshots = self.load_file_edit_snapshots(event.session_id, messages, loaded_state)
774
+
775
+ await ui.replay_history(
776
+ messages,
777
+ initial_profile=event.initial_agent_profile or display,
778
+ file_snapshots=file_snapshots or None,
779
+ compressed_blocks=compressed_blocks_map or None,
780
+ )
781
+ s.update_toc()
782
+
783
+ # Check if the session ended with an error/interrupt — enable retry
784
+ if messages:
785
+ for m in reversed(messages):
786
+ ct = m.get("additional_properties", {}).get(HistoryMarkerKind.KEY)
787
+ if ct == HistoryMarkerKind.TURN:
788
+ continue
789
+ if ct == HistoryMarkerKind.INTERRUPTED:
790
+ source = m.get("additional_properties", {}).get("_interrupted_by", "user")
791
+ ui.set_input_retry(INPUT_RETRY.bind() if source == "error" else INPUT_CONTINUE.bind())
792
+ break
793
+
794
+ # Restored workspace roots are backend-confirmed — record them in the
795
+ # MRU as a detached background task (never inline in the handler).
796
+ schedule_workspace_mru_touches(
797
+ [cwd, *event.working_dirs],
798
+ max_entries=self._services.workspace_mru_max_entries,
799
+ session_id=event.session_id or "",
800
+ used_at=event.timestamp,
801
+ )
802
+ short_id = session_short_id(event.session_id)
803
+ info = STATUS_SESSION_RESTORED.bind(session_id=short_id)
804
+ ui.flash_status(info)
805
+ self._agent_load.finish_agent_load(_FINISH_SESSION_RESTORED.bind(session_id=short_id))
806
+ s.debug("SessionRestored", short_id)
807
+ self._callbacks.post_gc_message(GcReclaimRequested(GcReclaimReason.SESSION_RESTORED, prompt=True))
808
+
809
+ async def on_session_saved(self, event: SessionSaved) -> None:
810
+ self.debug("SessionSaved", session_short_id(event.session_id))
811
+
812
+ async def _restore_session_title_state(self, session_id: str, *, prefer_recovery: bool = False) -> None:
813
+ """Reload title overlays from the session meta into the UI.
814
+
815
+ A crash-recovered session's history comes from the recovery
816
+ sidecar; its meta (title patches are mirrored into a live sidecar,
817
+ and a never-saved session has no primary at all) must come from
818
+ the same source.
819
+ """
820
+ s = self
821
+ ui = self._ui()
822
+ ui.reset_session_title_state()
823
+ if not s.state_store:
824
+ return
825
+ try:
826
+ meta = await s.state_store.load_session_meta(session_id, prefer_recovery=prefer_recovery)
827
+ except Exception:
828
+ return
829
+ if meta is None:
830
+ return
831
+ ui.set_session_title_state(
832
+ custom=meta.custom_title,
833
+ generated=meta.generated_title,
834
+ fallback=meta.title,
835
+ )
836
+
837
+ async def apply_custom_session_title(self, custom_title: str, session_id: str) -> None:
838
+ """Persist a user-edited custom title (empty clears it) and refresh the UI.
839
+
840
+ ``session_id`` is the session the edit dialog was opened for; the
841
+ title is persisted there even if the UI has since moved to another
842
+ session, but the on-screen title only updates while it still shows
843
+ that session.
844
+ """
845
+ s = self
846
+ ui = self._ui()
847
+ if not session_id or not s.state_store:
848
+ return
849
+ try:
850
+ async with self._custom_title_apply_lock:
851
+ meta = await s.state_store.update_session_titles(session_id, custom_title=custom_title)
852
+ except Exception:
853
+ s.debug("SessionTitle", "custom title save failed")
854
+ s.notify(
855
+ _SESSION_TITLE_SAVE_FAILED.bind(),
856
+ title=_SESSION_TITLE_TITLE.bind(),
857
+ severity="error",
858
+ timeout=3,
859
+ )
860
+ return
861
+ with contextlib.suppress(Exception):
862
+ await s.bus.publish(
863
+ SessionTitleUpdated(
864
+ session_id=session_id,
865
+ title=custom_title,
866
+ custom=True,
867
+ # A clear falls back to the generated/first-message
868
+ # title; ``meta`` is None only for unsaved sessions,
869
+ # which have no persisted fallback to fall back to.
870
+ display_title=meta.display_title if meta is not None else custom_title,
871
+ ),
872
+ )
873
+ if ui.chat_session_id() != session_id:
874
+ return
875
+ if meta is not None:
876
+ ui.set_session_title_state(
877
+ custom=meta.custom_title,
878
+ generated=meta.generated_title,
879
+ fallback=meta.title,
880
+ )
881
+ else:
882
+ ui.set_session_title_state(custom=custom_title)
883
+ definition = STATUS_SESSION_TITLE_UPDATED if custom_title else STATUS_CUSTOM_TITLE_CLEARED
884
+ ui.flash_status(definition.bind())
885
+
886
+ async def on_session_title_updated(self, event: SessionTitleUpdated) -> None:
887
+ """Reflect a persisted auto-generated title for the active session."""
888
+ ui = self._ui()
889
+ if event.custom or event.session_id != ui.chat_session_id():
890
+ return
891
+ if ui.session_custom_title():
892
+ return
893
+ ui.set_session_title_state(generated=event.title)
894
+ self.debug("SessionTitleUpdated", event.title)
895
+
896
+ async def on_session_forked(self, event: SessionForked) -> None:
897
+ s = self
898
+ ui = self._ui()
899
+
900
+ current_session_id = ui.chat_session_id()
901
+ if event.session_id != current_session_id:
902
+ return
903
+
904
+ fork_short_id = session_short_id(event.new_session_id)
905
+ if self._fork_dialog is None:
906
+ try:
907
+ await self._open_session_fork_dialog(event.session_id)
908
+ except Exception as exc:
909
+ s.set_agent_loading(False)
910
+ s.notify(
911
+ _FORK_CREATED.bind(fork_id=fork_short_id),
912
+ title=_FORK_TITLE.bind(),
913
+ severity="information",
914
+ timeout=3,
915
+ )
916
+ s.debug("SessionForked", f"{fork_short_id}; dialog failed: {exc}")
917
+ return
918
+
919
+ self._fork_new_session_id = event.new_session_id
920
+ fork_dialog = self._fork_dialog
921
+ if fork_dialog is not None:
922
+ fork_dialog.set_success(fork_short_id)
923
+ s.set_agent_loading(False)
924
+ s.notify(
925
+ _FORK_CREATED.bind(fork_id=fork_short_id),
926
+ title=_FORK_TITLE.bind(),
927
+ severity="information",
928
+ timeout=3,
929
+ )
930
+ s.debug("SessionForked", fork_short_id)
931
+
932
+ def on_session_clear_error(self, event: Error, *, message: str) -> None:
933
+ """A /clear failed before anything was deleted: the current session stays."""
934
+ s = self
935
+ ui = self._ui()
936
+
937
+ s.creating_new_session = False
938
+ s.set_agent_loading(False)
939
+ ui.unlock_input_keep_if_locked()
940
+ s.notify(
941
+ _CLEAR_FAILED.bind(message=message),
942
+ title=CLEAR_SESSION_TITLE.bind(),
943
+ severity="error",
944
+ timeout=5,
945
+ )
946
+
947
+ def on_session_fork_error(self, event: Error, *, message: str, severity: NotificationSeverity) -> None:
948
+ """Show a fork failure in the active fork dialog, if present."""
949
+ s = self
950
+ ui = self._ui()
951
+
952
+ if event.session_id and self._fork_session_id and event.session_id != self._fork_session_id:
953
+ return
954
+
955
+ if self._fork_dialog is not None:
956
+ self._fork_dialog.set_error(message)
957
+ else:
958
+ self._reset_session_fork_dialog()
959
+ s.set_agent_loading(False)
960
+ ui.flash_status(STATUS_FORK_NOTICE.bind(message=message), error=severity == "error")
961
+ ui.unlock_input_keep_if_locked()
962
+ s.notify(message, title=_FORK_TITLE.bind(), severity=severity, timeout=5)
963
+
964
+ async def on_profile_switched(self, event: ProfileSwitched) -> None:
965
+ """Handle profile switch — update UI and show switch marker in chat."""
966
+ s = self
967
+ ui = self._ui()
968
+
969
+ to_label = event.to_display_name or event.to_profile
970
+ from_label = event.from_display_name or event.from_profile
971
+ s.runtime_metadata = event.runtime_details
972
+ self._state.runtime.details_confirmed = True
973
+ if event.runtime_details.model.selection_source == "active":
974
+ self._callbacks.set_active_model_profile_id(event.runtime_details.model.profile_id)
975
+
976
+ # Same profile (settings reload / workspace change) — update tool info
977
+ # and context panel but skip profile switch UI.
978
+ if event.from_profile == event.to_profile:
979
+ self._callbacks.refresh_model_indicator()
980
+ trail = self._format_tool_info(
981
+ event.tool_names,
982
+ event.skill_names,
983
+ memory_files=event.memory_files,
984
+ runtime_details=event.runtime_details,
985
+ )
986
+ ui.set_tool_info(trail)
987
+ ui.clear_status()
988
+ if event.max_context_tokens:
989
+ current_context_usage = s.context_usage_state
990
+ ui.set_context_usage_state(
991
+ ContextUsageState.with_window(
992
+ used_tokens=current_context_usage.used_tokens if current_context_usage else s.last_usage_tokens,
993
+ max_context_tokens=event.max_context_tokens,
994
+ total_session_tokens=(
995
+ current_context_usage.total_session_tokens
996
+ if current_context_usage
997
+ else s.last_total_session_tokens
998
+ ),
999
+ total_session_input_tokens=(
1000
+ current_context_usage.total_session_input_tokens if current_context_usage else 0
1001
+ ),
1002
+ total_session_output_tokens=(
1003
+ current_context_usage.total_session_output_tokens if current_context_usage else 0
1004
+ ),
1005
+ total_session_cache_hit_tokens=(
1006
+ current_context_usage.total_session_cache_hit_tokens if current_context_usage else None
1007
+ ),
1008
+ )
1009
+ )
1010
+ self._agent_load.finish_agent_load(_FINISH_AGENT_READY.bind(agent=to_label))
1011
+ self._callbacks.post_gc_message(GcAbsorbRequested(GcAbsorbReason.PROFILE_UI_UPDATED))
1012
+ return
1013
+
1014
+ s.profile = to_label
1015
+ self._callbacks.refresh_model_indicator()
1016
+ s.update_subtitle()
1017
+ ui.set_chat_profile(to_label)
1018
+ desc = self._get_profile_description(event.to_profile)
1019
+ ui.set_status_profile(to_label, description=desc)
1020
+
1021
+ if s.has_messages:
1022
+ # Each fresh switch (after messages) gets a unique key so committed
1023
+ # switch indicators stay permanently in the chat history.
1024
+ existing_from_label = s.profile_switch_from
1025
+ if existing_from_label is None:
1026
+ # Fresh switch — new unique key
1027
+ s.profile_switch_seq += 1
1028
+ key = f"profile-switch-{s.profile_switch_seq}"
1029
+ s.profile_switch_from = from_label
1030
+ s.profile_switch_to = to_label
1031
+ await ui.add_system(
1032
+ self._render_message(_PROFILE_SWITCH_INDICATOR.bind(from_label=from_label, to_label=to_label)),
1033
+ key=key,
1034
+ )
1035
+ elif s.profile_switch_to == from_label:
1036
+ # Consecutive switch (no messages since last switch)
1037
+ key = f"profile-switch-{s.profile_switch_seq}"
1038
+ s.profile_switch_to = to_label
1039
+ if existing_from_label == to_label:
1040
+ await ui.remove_system(key)
1041
+ s.profile_switch_from = None
1042
+ s.profile_switch_to = None
1043
+ else:
1044
+ await ui.update_system(
1045
+ key,
1046
+ self._render_message(
1047
+ _PROFILE_SWITCH_INDICATOR.bind(
1048
+ from_label=existing_from_label,
1049
+ to_label=to_label,
1050
+ )
1051
+ ),
1052
+ )
1053
+ else:
1054
+ # Chain broken (e.g. A→B then C→D where C != B) — remove old
1055
+ key = f"profile-switch-{s.profile_switch_seq}"
1056
+ await ui.remove_system(key)
1057
+ s.profile_switch_from = None
1058
+ s.profile_switch_to = None
1059
+ else:
1060
+ ui.update_welcome(profile=to_label, cwd=s.workspace_cwd())
1061
+
1062
+ trail = self._format_tool_info(
1063
+ event.tool_names,
1064
+ event.skill_names,
1065
+ memory_files=event.memory_files,
1066
+ runtime_details=event.runtime_details,
1067
+ )
1068
+ ui.set_tool_info(trail)
1069
+ ui.clear_status()
1070
+ if event.sub_agent_tool_names:
1071
+ from chrys.app.tui.widgets.chat.renderers.sub_agent import SubAgentToolCall
1072
+ from chrys.app.tui.widgets.chat.tool_renderers import register_dynamic_renderer
1073
+
1074
+ for name in event.sub_agent_tool_names:
1075
+ register_dynamic_renderer(name, SubAgentToolCall)
1076
+ s.debug("ProfileSwitched", f"{from_label} -> {to_label}")
1077
+ self._agent_load.finish_agent_load(_FINISH_PROFILE_SWITCHED.bind(from_label=from_label, to_label=to_label))
1078
+ self._callbacks.post_gc_message(GcAbsorbRequested(GcAbsorbReason.PROFILE_UI_UPDATED))
1079
+
1080
+ async def on_workspace_updated(self, event: WorkspaceUpdated) -> None:
1081
+ """Handle workspace update — refresh all cwd-dependent TUI state."""
1082
+ s = self
1083
+ ui = self._ui()
1084
+
1085
+ self._state.workspace.roots = [event.primary_cwd, *event.working_dirs]
1086
+ s.clear_suggestion_file_cache()
1087
+ ui.set_input_paste_cwd(event.primary_cwd)
1088
+ ui.set_chat_workspace_cwd(event.primary_cwd)
1089
+ if os.path.isdir(event.primary_cwd):
1090
+ await ui.change_shell_directory(event.primary_cwd)
1091
+ # Always track the latest cwd so callers (e.g. agent config screen)
1092
+ # can see the live workspace, even before the first message.
1093
+ previous_cwd = s.chdir_current_cwd
1094
+ s.chdir_current_cwd = event.primary_cwd
1095
+ ui.set_terminal_title_for_cwd(event.primary_cwd)
1096
+
1097
+ panel_key = "chdir"
1098
+ if s.has_messages:
1099
+ if s.chdir_original_cwd is None:
1100
+ await ui.remove_system(panel_key)
1101
+ s.chdir_original_cwd = previous_cwd
1102
+ await ui.add_system(
1103
+ self._render_message(_WORKING_DIRECTORY_INDICATOR.bind(path=DisplayPath(event.primary_cwd))),
1104
+ key=panel_key,
1105
+ )
1106
+ else:
1107
+ if _samepath(s.chdir_original_cwd, s.chdir_current_cwd):
1108
+ await ui.remove_system(panel_key)
1109
+ s.chdir_original_cwd = None
1110
+ else:
1111
+ await ui.update_system(
1112
+ panel_key,
1113
+ self._render_message(_WORKING_DIRECTORY_INDICATOR.bind(path=DisplayPath(event.primary_cwd))),
1114
+ )
1115
+ else:
1116
+ ui.update_welcome(profile=s.profile, cwd=event.primary_cwd)
1117
+
1118
+ # The workspace change is backend-confirmed at this point — record it
1119
+ # in the MRU as a detached background task (never inline).
1120
+ schedule_workspace_mru_touches(
1121
+ [event.primary_cwd, *event.working_dirs],
1122
+ max_entries=self._services.workspace_mru_max_entries,
1123
+ session_id=event.session_id or "",
1124
+ used_at=event.timestamp,
1125
+ )
1126
+ s.debug("WorkspaceUpdated", event.primary_cwd)
1127
+ self._callbacks.post_gc_message(GcAbsorbRequested(GcAbsorbReason.WORKSPACE_UI_UPDATED))