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,1381 @@
1
+ # Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
2
+
3
+ """Rollback modal — compact diff viewer with turn picker + rollback actions.
4
+
5
+ Dismisses with one of:
6
+
7
+ - ``None`` — user cancelled (Esc / Close).
8
+ - ``(target_turn, revert_changes, selected_paths | None)`` — user chose
9
+ a rollback action. ``target_turn`` follows the engine's "keep N
10
+ turns" convention (``0`` = session start / welcome; ``N >= 1`` =
11
+ keep turns ``1..N``). The caller publishes a :class:`UserRollback`
12
+ event.
13
+
14
+ Layout::
15
+
16
+ ┌────────────────────────── 80% x 80% ──────────────────────────┐
17
+ │ Title: "Rollback — pick a turn to restore" │
18
+ │ ┌─ Select ─────────────────────────────────────────────────┐ │
19
+ │ │ [▼] Turn N (Current) │ Turn N-1 │ … │ Session start │ │
20
+ │ └──────────────────────────────────────────────────────────┘ │
21
+ │ ┌──────────────────────── DiffPeriodPane ──────────────────────┐│
22
+ │ │ Change list │ DiffView for the selected turn's delta ││
23
+ │ └────────────────────────────────────────────────────────────┘│
24
+ │ [ Discard conversation after Turn N ] [*] Revert … [Close] │
25
+ └───────────────────────────────────────────────────────────────┘
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import asyncio
31
+ import logging
32
+ from contextlib import suppress
33
+ from typing import TYPE_CHECKING, ClassVar, Protocol
34
+
35
+ from rich.text import Text
36
+ from textual import events, on
37
+ from textual.binding import Binding
38
+ from textual.containers import HorizontalGroup, VerticalGroup
39
+ from textual.content import Content
40
+ from textual.strip import Strip
41
+ from textual.visual import Visual
42
+ from textual.widgets import Button, Static, Tree
43
+
44
+ from chrys.app.tui.binding_display import CLOSE_BINDING, TOGGLE_VIEW_BINDING, localized_binding
45
+ from chrys.app.tui.i18n import render_str
46
+ from chrys.app.tui.screens.dialogs.base import BaseDialog
47
+ from chrys.app.tui.screens.diff.screen import DiffPeriodPane, _merge_source, filter_and_dedupe_entries
48
+ from chrys.app.tui.util.diff_entries import DiffFileEntry
49
+ from chrys.app.tui.util.removal import remove_children_shielded, remove_shielded
50
+ from chrys.app.tui.widgets import Checkbox, ChrysLoadingIndicator, Select
51
+ from chrys.foundation.i18n import DisplayBlock, DisplayPath, MessageDef, MessageRef, msg
52
+ from chrys.foundation.i18n.formatting import format_message
53
+ from chrys.foundation.platform.paths import display_relpath
54
+ from chrys.service.mutations.types import (
55
+ FileHashDiff,
56
+ MutationOp,
57
+ MutationProvenance,
58
+ RollbackExclusionReason,
59
+ SnapshotSkipReason,
60
+ )
61
+
62
+ if TYPE_CHECKING:
63
+ from collections.abc import Awaitable, Callable
64
+
65
+ from textual.app import ComposeResult
66
+ from textual.css.styles import RulesMap
67
+ from textual.style import Style
68
+ from textual.visual import RenderOptions
69
+
70
+ from chrys.app.tui.i18n import LocaleController
71
+ from chrys.service.mutations.tracker import MutationTracker
72
+ from chrys.service.mutations.types import RollbackPlan, RollbackWarning
73
+
74
+ logger = logging.getLogger(__name__)
75
+
76
+ ROLLBACK_TITLE = msg("tui.rollback.title", fallback="Rollback")
77
+ _CHECK_BINDING = msg("tui.binding.toggle_check", fallback="Check")
78
+ _ROLLBACK_FAILED = msg("tui.rollback.failed", fallback="Rollback failed: {error}")
79
+ _NO_ROLLBACK_SNAPSHOTS = msg(
80
+ "tui.rollback.no_snapshots",
81
+ fallback="No rollback snapshots are available yet.",
82
+ )
83
+ _FILES_EXCLUDED_NOTE = msg(
84
+ "tui.rollback.files_excluded_note",
85
+ fallback="{count} file excluded from revert: {preview}",
86
+ plural_fallback="{count} files excluded from revert: {preview}",
87
+ )
88
+ _REVERT_FILE_CHANGES = msg(
89
+ "tui.rollback.revert_file_changes",
90
+ fallback="Revert {count} file change (WYSIWYG)",
91
+ plural_fallback="Revert {count} file changes (WYSIWYG)",
92
+ )
93
+ _ROLLBACK_TO = msg("tui.rollback.picker.prompt", fallback="Rollback to:")
94
+ _ROLLBACK_CLOSE = msg("tui.rollback.button.close", fallback="Close")
95
+ _NO_FILE_CHANGES = msg("tui.rollback.no_file_changes", fallback="No file changes to revert.")
96
+ _DISCARD_ALL = msg("tui.rollback.button.discard_all", fallback="Discard All")
97
+ _DISCARD_AFTER_TURN = msg(
98
+ "tui.rollback.button.discard_after_turn",
99
+ fallback="Discard conversation after Turn {turn}",
100
+ )
101
+ _TURN_CURRENT = msg("tui.rollback.picker.turn_current", fallback="Turn {turn} (Current)")
102
+ _TURN_CURRENT_PREVIEW = msg(
103
+ "tui.rollback.picker.turn_current_preview",
104
+ fallback="Turn {turn} (Current) - ",
105
+ )
106
+ _TURN = msg("tui.rollback.picker.turn", fallback="Turn {turn}")
107
+ _TURN_PREVIEW = msg("tui.rollback.picker.turn_preview", fallback="Turn {turn} - ")
108
+ _SESSION_START = msg("tui.rollback.picker.session_start", fallback="Session start")
109
+ _DISCARD_ALL_SUFFIX = msg("tui.rollback.picker.discard_all_suffix", fallback=" — discard all")
110
+ _LOAD_PREVIEW_ERROR = msg(
111
+ "tui.rollback.preview.load_error",
112
+ fallback="Error: unable to load rollback preview — {detail}",
113
+ multiline=True,
114
+ )
115
+ _REFRESH_PREVIEW_ERROR = msg(
116
+ "tui.rollback.preview.refresh_error",
117
+ fallback="Error: unable to load this rollback preview — {detail}",
118
+ multiline=True,
119
+ )
120
+ _WARNING_NOTE = msg("tui.rollback.warning_note", fallback="⚠ {message}")
121
+ _MORE_EXCLUSIONS = msg("tui.rollback.exclusions.more", fallback="+{extra_count} more")
122
+ _EXCLUSION_UNRESTORABLE = msg(
123
+ "tui.rollback.exclusion.unrestorable",
124
+ fallback="no restorable backup",
125
+ )
126
+ _EXCLUSION_MOVE_POISONED = msg(
127
+ "tui.rollback.exclusion.move_poisoned",
128
+ fallback="move source unrecoverable",
129
+ )
130
+ _EXCLUSION_FOREIGN = msg(
131
+ "tui.rollback.exclusion.foreign",
132
+ fallback="changed by another session",
133
+ )
134
+ _EXCLUSION_CONTESTED = msg(
135
+ "tui.rollback.exclusion.contested",
136
+ fallback="modified internally and externally",
137
+ )
138
+ _EXCLUSION_PEER_MODIFIED_SINCE = msg(
139
+ "tui.rollback.exclusion.peer_modified_since",
140
+ fallback="changed by another session since",
141
+ )
142
+ EXCLUSION_PREVIEW_ITEM = msg(
143
+ "tui.rollback.exclusion.preview_item",
144
+ fallback="{path} ({reason})",
145
+ )
146
+
147
+
148
+ # Dismissal payload: ``(target_turn, revert_changes, selected_paths | None)``
149
+ # — or ``None`` on close/cancel. ``selected_paths`` is ``None`` when the
150
+ # user pressed the plain rollback button (no file revert) and a concrete
151
+ # list of absolute paths when they pressed "& Revert Changes". An empty
152
+ # list means the user un-checked everything before confirming — still a
153
+ # valid outcome (turns are still popped) and treated as "revert nothing".
154
+ RollbackChoice = tuple[int, bool, list[str] | None]
155
+
156
+
157
+ class RollbackModalState(Protocol):
158
+ """Deferred engine projection consumed after the modal's first paint."""
159
+
160
+ tracker: MutationTracker
161
+ available_turns: list[int]
162
+ current_turn: int
163
+ workspace_cwd: str
164
+ turn_prompts: dict[int, str]
165
+ plan_augment: Callable[[RollbackPlan, str], RollbackPlan] | None
166
+ attribution_refresh: Callable[[], Awaitable[bool]] | None
167
+ projection_acquire: Callable[[], Awaitable[str]] | None
168
+ projection_release: Callable[[str], None] | None
169
+
170
+
171
+ # Sentinel ``Select`` value used for the "you-are-here" entry at the top
172
+ # of the turn picker. Under the "keep N turns" convention the engine
173
+ # accepts ``target_turn >= 0`` (``0`` is the welcome target), so we use
174
+ # ``-1`` to guarantee no collision with a real rollback target — the
175
+ # entry stays visually present (and dimmed) but semantically inert:
176
+ # selecting it in ``_on_turn_changed`` snaps the Select back to the
177
+ # previous value.
178
+ _LATEST_SENTINEL = -1
179
+
180
+
181
+ class RollbackLoadCancelled(Exception):
182
+ """Picker projection was superseded while its loading shell was open."""
183
+
184
+
185
+ class RollbackProgressModal(BaseDialog[None]):
186
+ """Non-dismissible loading shell shown until session restore UI takes over."""
187
+
188
+ BINDINGS: ClassVar[list] = [Binding("escape", "noop", show=False, priority=True)]
189
+
190
+ CSS_PATH = "rollback_modal.tcss"
191
+
192
+ def __init__(
193
+ self,
194
+ operation: Callable[[], Awaitable[None]],
195
+ *,
196
+ start_worker: Callable[[Awaitable[None]], object] | None = None,
197
+ locale_controller: LocaleController | None = None,
198
+ ) -> None:
199
+ super().__init__(dismiss_on_backdrop=False)
200
+ self._operation = operation
201
+ self._start_worker = start_worker
202
+ self._locale_controller = locale_controller
203
+ self._dismiss_requested = False
204
+
205
+ def compose(self) -> ComposeResult:
206
+ with VerticalGroup(id="rollback-progress-container") as container:
207
+ container.border_title = Text(self._render_message(ROLLBACK_TITLE.bind()))
208
+ with VerticalGroup(id="rollback-progress-state"):
209
+ yield ChrysLoadingIndicator(id="rollback-progress-loading")
210
+
211
+ def on_mount(self) -> None:
212
+ # Do not start the potentially expensive state scan/event dispatch
213
+ # until Textual has painted the blocking shell at least once.
214
+ self.call_after_refresh(self._start_operation)
215
+
216
+ def _start_operation(self) -> None:
217
+ operation = self._run_operation()
218
+ if self._start_worker is None:
219
+ self.run_worker(operation, exclusive=True, group="rollback-progress")
220
+ else:
221
+ self._start_worker(operation)
222
+
223
+ async def _run_operation(self) -> None:
224
+ try:
225
+ await self._operation()
226
+ except Exception as exc:
227
+ logger.exception("Rollback operation failed")
228
+ self.notify(
229
+ self._render_message(_ROLLBACK_FAILED.bind(error=str(exc))),
230
+ title=self._render_message(ROLLBACK_TITLE.bind()),
231
+ severity="error",
232
+ timeout=4,
233
+ markup=False,
234
+ )
235
+ finally:
236
+ self._dismiss_requested = True
237
+ self._dismiss_if_current()
238
+
239
+ def _dismiss_if_current(self) -> None:
240
+ """Dismiss only after any restore-time loading screen above us closes."""
241
+ if self.is_attached and self.app.screen is self:
242
+ self._dismiss_requested = False
243
+ self.dismiss(None)
244
+
245
+ def handoff_to_session_restore(self) -> None:
246
+ """Close this shell before the engine's session-restore modal is pushed."""
247
+ self._dismiss_requested = True
248
+ self._dismiss_if_current()
249
+
250
+ def on_screen_resume(self, _event: events.ScreenResume) -> None:
251
+ """Honor completion deferred while agent restore UI covered this modal."""
252
+ if self._dismiss_requested:
253
+ self._dismiss_if_current()
254
+
255
+ def action_noop(self) -> None:
256
+ """Consume Escape while rollback owns the active session state."""
257
+
258
+ def _render_message(self, reference: MessageRef) -> str:
259
+ controller = self._locale_controller
260
+ return format_message(reference) if controller is None else render_str(controller.localizer, reference)
261
+
262
+
263
+ def _build_cumulative_entries(
264
+ tracker: MutationTracker,
265
+ from_turn_id: int,
266
+ cwd: str,
267
+ ) -> list[DiffFileEntry]:
268
+ """Aggregate per-file diffs for all turns with ``turn_id >= from_turn_id``.
269
+
270
+ Matches :func:`_build_session_entries` semantics but filtered to a
271
+ suffix of the turn history: earliest ``before_hash`` wins, latest
272
+ ``after_hash`` wins, net-zero churn is filtered out.
273
+
274
+ Which entries are offered mirrors the rollback plan exactly
275
+ (WYSIWYG): only paths present in
276
+ :meth:`MutationTracker.get_rollback_plan_for_turns` for the same
277
+ window are shown, so every plan-side safety filter (restorability,
278
+ move atomicity) is inherited rather than re-implemented. Entries
279
+ whose content backup was withheld by SnapshotPolicy are still
280
+ offered when the plan can act on them (delete a skipped create,
281
+ restore a grown/binary file from its stored before-blob), carrying
282
+ ``content_omitted`` so the pane renders a message instead of an
283
+ untruthful empty diff.
284
+ """
285
+ from chrys.foundation.text.encoding import EncodingDetector, decode_bytes
286
+
287
+ turns = [t for t in tracker.get_all_turns() if t.turn_id >= from_turn_id]
288
+ if not turns:
289
+ return []
290
+
291
+ result: dict[str, FileHashDiff] = {}
292
+ old_paths: dict[str, str | None] = {}
293
+ sources: dict[str, str] = {}
294
+ inferred_paths: set[str] = set()
295
+
296
+ def _set_before_if_first(path: str, turn_id: int) -> tuple[str | None, SnapshotSkipReason | None]:
297
+ if path not in result:
298
+ snap = tracker.get_snapshot(path, turn_id)
299
+ before = snap.content_hash if snap else None
300
+ before_skip = snap.skip_reason if snap else None
301
+ result[path] = FileHashDiff(before=before, after=None, before_skip=before_skip)
302
+ return before, before_skip
303
+ return result[path].before, result[path].before_skip
304
+
305
+ for turn in sorted(turns, key=lambda t: t.turn_id):
306
+ for m in turn.mutations:
307
+ # Foreign rows never fold into "our" endpoints; their paths
308
+ # are excluded from the plan anyway (intersection below).
309
+ if m.provenance is MutationProvenance.FOREIGN:
310
+ continue
311
+ if m.provenance is MutationProvenance.ASSUMED:
312
+ inferred_paths.add(m.path)
313
+ if m.old_path:
314
+ inferred_paths.add(m.old_path)
315
+ before, before_skip = _set_before_if_first(m.path, turn.turn_id)
316
+ result[m.path] = FileHashDiff(
317
+ before=before, after=m.after_hash, before_skip=before_skip, after_skip=m.after_skip
318
+ )
319
+ sources[m.path] = _merge_source(sources.get(m.path, ""), m.source.value)
320
+ if m.old_path:
321
+ old_before, old_before_skip = _set_before_if_first(m.old_path, turn.turn_id)
322
+ result[m.old_path] = FileHashDiff(before=old_before, after=None, before_skip=old_before_skip)
323
+ old_paths[m.path] = m.old_path
324
+ sources[m.old_path] = _merge_source(sources.get(m.old_path, ""), m.source.value)
325
+
326
+ # Offer exactly what the rollback plan will act on — intersecting
327
+ # with the plan inherits its safety filters (non-restorable
328
+ # snapshots, move atomicity, foreign/contested provenance) without
329
+ # duplicating them. On top of that, drop net-zero churn from the
330
+ # display (``is_net_zero`` is skip-aware: a skipped create is
331
+ # absent → exists, not net-zero).
332
+ plan_paths = tracker.get_rollback_plan_for_turns({t.turn_id for t in turns}).paths
333
+ filtered: dict[str, FileHashDiff] = {}
334
+ for path, diff in result.items():
335
+ if path not in plan_paths:
336
+ continue
337
+ if diff.is_net_zero:
338
+ continue
339
+ filtered[path] = diff
340
+ if not filtered:
341
+ return []
342
+
343
+ detector = EncodingDetector()
344
+ store = tracker.store
345
+ entries: list[DiffFileEntry] = []
346
+ for file_path, diff in filtered.items():
347
+ before_bytes = store.read_blob(diff.before) if diff.before else None
348
+ after_bytes = store.read_blob(diff.after) if diff.after else None
349
+
350
+ binary = bool(
351
+ (before_bytes and EncodingDetector.looks_binary(before_bytes))
352
+ or (after_bytes and EncodingDetector.looks_binary(after_bytes))
353
+ )
354
+ before_text = "" if binary or before_bytes is None else decode_bytes(before_bytes)
355
+ after_text = "" if binary or after_bytes is None else decode_bytes(after_bytes)
356
+
357
+ # Skip-aware existence: a side with a withheld backup still
358
+ # *exists* on disk (a skipped create must show as CREATE, not
359
+ # as a hash-less MODIFY/DELETE).
360
+ if not diff.before_exists and diff.after_exists:
361
+ op = MutationOp.CREATE
362
+ elif diff.before_exists and not diff.after_exists:
363
+ op = MutationOp.DELETE
364
+ else:
365
+ op = MutationOp.MODIFY
366
+
367
+ enc = "utf-8"
368
+ content_for_detect = after_bytes or before_bytes
369
+ if content_for_detect and not binary:
370
+ det = detector.detect_from_bytes(content_for_detect)
371
+ enc = det.encoding or "utf-8"
372
+
373
+ rel = display_relpath(file_path, cwd)
374
+
375
+ entries.append(
376
+ DiffFileEntry(
377
+ path=file_path,
378
+ rel_path=rel,
379
+ operation=op,
380
+ old_path=old_paths.get(file_path),
381
+ before_text=before_text,
382
+ after_text=after_text,
383
+ is_binary=binary,
384
+ encoding=enc,
385
+ bytes_changed=True,
386
+ before_hash=diff.before,
387
+ after_hash=diff.after,
388
+ source=sources.get(file_path, ""),
389
+ # Only the after side can be skip-marked here — a
390
+ # before-side skip makes the earliest snapshot
391
+ # non-restorable, which the filter above already drops.
392
+ content_omitted=diff.after_skip.value if diff.after_skip else "",
393
+ inferred=file_path in inferred_paths,
394
+ ),
395
+ )
396
+
397
+ entries.sort(key=lambda e: e.rel_path)
398
+ return entries
399
+
400
+
401
+ # Human labels for the modal's exclusion note section.
402
+ _EXCLUSION_LABELS: dict[RollbackExclusionReason, MessageDef] = {
403
+ RollbackExclusionReason.UNRESTORABLE: _EXCLUSION_UNRESTORABLE,
404
+ RollbackExclusionReason.MOVE_POISONED: _EXCLUSION_MOVE_POISONED,
405
+ RollbackExclusionReason.FOREIGN: _EXCLUSION_FOREIGN,
406
+ RollbackExclusionReason.CONTESTED: _EXCLUSION_CONTESTED,
407
+ RollbackExclusionReason.PEER_MODIFIED_SINCE: _EXCLUSION_PEER_MODIFIED_SINCE,
408
+ }
409
+
410
+ _EXCLUSION_NOTE_MAX_PATHS = 3
411
+
412
+
413
+ def exclusion_reason_label(reason: RollbackExclusionReason | str) -> MessageRef | str:
414
+ """Human label for an exclusion reason — enum or raw ``.value`` string.
415
+
416
+ Event payloads carry reasons as primitive strings (foundation must
417
+ not import the service enum); unknown values render verbatim so a
418
+ newer engine never breaks an older consumer.
419
+ """
420
+ if isinstance(reason, str):
421
+ try:
422
+ reason = RollbackExclusionReason(reason)
423
+ except ValueError:
424
+ return str(reason)
425
+ definition = _EXCLUSION_LABELS.get(reason)
426
+ return reason.value if definition is None else definition.bind()
427
+
428
+
429
+ def _plan_for_target(tracker: MutationTracker, target_turn: int) -> RollbackPlan | None:
430
+ """Build the rollback plan for the same turn window ``_entries_for_target`` shows."""
431
+ all_turns = tracker.get_all_turns()
432
+ if target_turn < 0 or not all_turns:
433
+ return None
434
+ turn_ids = {t.turn_id for t in all_turns if target_turn == 0 or t.turn_id > target_turn}
435
+ if not turn_ids:
436
+ return None
437
+ return tracker.get_rollback_plan_for_turns(turn_ids)
438
+
439
+
440
+ def _exclusion_note_text(
441
+ exclusions: list[tuple[str, RollbackExclusionReason]],
442
+ cwd: str,
443
+ locale_controller: LocaleController | None = None,
444
+ ) -> str:
445
+ """One-line summary of plan exclusions for the modal note section."""
446
+ parts: list[str] = []
447
+ for path, reason in exclusions[:_EXCLUSION_NOTE_MAX_PATHS]:
448
+ rel = display_relpath(path, cwd)
449
+ label = exclusion_reason_label(reason)
450
+ rendered_label = (
451
+ label
452
+ if isinstance(label, str)
453
+ else format_message(label)
454
+ if locale_controller is None
455
+ else render_str(locale_controller.localizer, label)
456
+ )
457
+ item_reference = EXCLUSION_PREVIEW_ITEM.bind(path=DisplayPath(rel), reason=rendered_label)
458
+ parts.append(
459
+ format_message(item_reference)
460
+ if locale_controller is None
461
+ else render_str(locale_controller.localizer, item_reference)
462
+ )
463
+ more = len(exclusions) - _EXCLUSION_NOTE_MAX_PATHS
464
+ if more > 0:
465
+ more_reference = _MORE_EXCLUSIONS.bind(extra_count=more)
466
+ parts.append(
467
+ format_message(more_reference)
468
+ if locale_controller is None
469
+ else render_str(locale_controller.localizer, more_reference)
470
+ )
471
+ count = len(exclusions)
472
+ reference = _FILES_EXCLUDED_NOTE.bind(count=count, preview=", ".join(parts))
473
+ return (
474
+ format_message(reference) if locale_controller is None else render_str(locale_controller.localizer, reference)
475
+ )
476
+
477
+
478
+ def _warning_note_text(
479
+ warnings: list[RollbackWarning],
480
+ locale_controller: LocaleController | None = None,
481
+ ) -> str:
482
+ """Advisory peer warnings for the modal note section.
483
+
484
+ Deduped by message: the coordinator dedupes per peer session, so two
485
+ active peers yield two identical strings — one line suffices.
486
+ """
487
+ unique = dict.fromkeys(w.message for w in warnings)
488
+ references = (_WARNING_NOTE.bind(message=message) for message in unique)
489
+ if locale_controller is None:
490
+ return "\n".join(format_message(reference) for reference in references)
491
+ return "\n".join(render_str(locale_controller.localizer, reference) for reference in references)
492
+
493
+
494
+ def _invert_entry(entry: DiffFileEntry) -> DiffFileEntry:
495
+ """Return the reverse of ``entry`` — what the revert will apply.
496
+
497
+ Swaps ``before_text`` ↔ ``after_text`` and flips the operation:
498
+
499
+ - ``CREATE`` (file was created) → ``DELETE`` (revert will remove it)
500
+ - ``DELETE`` (file was deleted) → ``CREATE`` (revert will restore it)
501
+ - ``MODIFY`` stays ``MODIFY``, but texts are swapped (revert writes
502
+ the pre-range content)
503
+
504
+ ``old_path`` is cleared: the reverse of a MOVE A→B is naturally
505
+ represented by the already-emitted pair (``DELETE`` at B plus
506
+ ``CREATE`` at A) that :func:`_build_cumulative_entries` produces.
507
+ Carrying the forward-direction ``old_path`` annotation through
508
+ would mis-label those entries.
509
+ """
510
+ if entry.operation is MutationOp.CREATE:
511
+ new_op = MutationOp.DELETE
512
+ elif entry.operation is MutationOp.DELETE:
513
+ new_op = MutationOp.CREATE
514
+ else:
515
+ new_op = entry.operation
516
+
517
+ return DiffFileEntry(
518
+ path=entry.path,
519
+ rel_path=entry.rel_path,
520
+ operation=new_op,
521
+ old_path=None,
522
+ before_text=entry.after_text,
523
+ after_text=entry.before_text,
524
+ is_binary=entry.is_binary,
525
+ encoding=entry.encoding,
526
+ bytes_changed=entry.bytes_changed,
527
+ before_hash=entry.after_hash,
528
+ after_hash=entry.before_hash,
529
+ source=entry.source,
530
+ content_omitted=entry.content_omitted,
531
+ contested=entry.contested,
532
+ inferred=entry.inferred,
533
+ )
534
+
535
+
536
+ def _entries_for_target(
537
+ tracker: MutationTracker,
538
+ target_turn: int,
539
+ cwd: str,
540
+ ) -> list[DiffFileEntry]:
541
+ """Build the diff entries shown when the user previews keeping ``target_turn`` turns.
542
+
543
+ The diff represents **what the workspace will look like AFTER the
544
+ revert** compared to the current state — i.e. the inverse of the
545
+ cumulative changes made across turns with ``tracker_turn_id >
546
+ target_turn``. We reuse the same "All"-tab aggregation (filtered
547
+ to turns that would be discarded) and then invert each entry:
548
+ created files become deletions, deleted files become creations,
549
+ and modifies swap their before/after texts. The effect is that
550
+ "left" in the viewer shows the current on-disk content and "right"
551
+ shows what the file will contain after the revert is applied.
552
+
553
+ Under the "keep N turns" convention, ``target_turn = 0`` aggregates
554
+ every tracker turn (discard everything); ``target_turn = N >= 1``
555
+ aggregates tracker turns with ``turn_id >= target_turn + 1``.
556
+ ``turn_id`` matches against tracker entries directly (NOT treated
557
+ as an index into ``get_all_turns()``) — the two are kept in sync
558
+ by the engine (``start_turn(self._turn_number)`` uses the same
559
+ counter that names snapshot files and history turn markers), so a
560
+ ``turn_id`` lookup is both correct and gap-safe.
561
+ """
562
+ all_turns = tracker.get_all_turns()
563
+ if target_turn < 0 or not all_turns:
564
+ return []
565
+
566
+ if target_turn == 0:
567
+ # "Session start" — aggregate every turn the tracker knows
568
+ # about. Use the min turn_id so ``_build_cumulative_entries``
569
+ # doesn't skip pre-history turns that might arrive with
570
+ # turn_id > 1 from a restored session.
571
+ first_turn_id = min(t.turn_id for t in all_turns)
572
+ forward = _build_cumulative_entries(tracker, first_turn_id, cwd)
573
+ else:
574
+ # Keep turns 1..target_turn, discard turns with id > target_turn.
575
+ # Direct turn_id match — gap-safe and not tied to list ordering.
576
+ forward = _build_cumulative_entries(tracker, target_turn + 1, cwd)
577
+
578
+ return [_invert_entry(e) for e in forward]
579
+
580
+
581
+ def _build_preview_state(
582
+ tracker: MutationTracker,
583
+ target_turn: int,
584
+ cwd: str,
585
+ ) -> tuple[list[DiffFileEntry], RollbackPlan | None]:
586
+ """Build the expensive rollback entry and plan projection off-loop."""
587
+ entries = filter_and_dedupe_entries(_entries_for_target(tracker, target_turn, cwd))
588
+ return entries, _plan_for_target(tracker, target_turn)
589
+
590
+
591
+ class _HangingIndentVisual(Visual):
592
+ """Render a fixed turn prefix beside a width-aware wrapped message."""
593
+
594
+ def __init__(self, prefix: Content, message: Content) -> None:
595
+ self._prefix = prefix
596
+ self._message = message
597
+ self._combined = prefix + message
598
+ self._prefix_width = prefix.cell_length
599
+
600
+ def get_optimal_width(self, rules: RulesMap, container_width: int) -> int:
601
+ return self._combined.get_optimal_width(rules, container_width)
602
+
603
+ def get_height(self, rules: RulesMap, width: int) -> int:
604
+ if width <= self._prefix_width or not self._message:
605
+ return self._combined.get_height(rules, width)
606
+ return max(1, self._message.get_height(rules, width - self._prefix_width))
607
+
608
+ def render_strips(
609
+ self,
610
+ width: int,
611
+ height: int | None,
612
+ style: Style,
613
+ options: RenderOptions,
614
+ ) -> list[Strip]:
615
+ if width <= 0 or height == 0:
616
+ return []
617
+ if width <= self._prefix_width or not self._message:
618
+ return self._combined.render_strips(width, height, style, options)
619
+
620
+ message_width = width - self._prefix_width
621
+ prefix_strips = self._prefix.render_strips(self._prefix_width, 1, style, options)
622
+ message_strips = self._message.render_strips(message_width, height, style, options)
623
+ if not prefix_strips or not message_strips:
624
+ return self._combined.render_strips(width, height, style, options)
625
+
626
+ indent = Strip.blank(self._prefix_width, style.rich_style)
627
+ strips = [
628
+ Strip.join([prefix_strips[0], message_strips[0]]),
629
+ *(Strip.join([indent, message_strip]) for message_strip in message_strips[1:]),
630
+ ]
631
+ return strips if height is None else strips[:height]
632
+
633
+
634
+ class _TurnOptionText(Text):
635
+ """Searchable Rich text whose Textual visual uses a hanging indent."""
636
+
637
+ __slots__ = ("_message_text", "_prefix_text")
638
+
639
+ def __init__(
640
+ self,
641
+ prefix: str,
642
+ message: str,
643
+ *,
644
+ prefix_style: str = "",
645
+ message_style: str = "dim",
646
+ ) -> None:
647
+ super().__init__()
648
+ self._prefix_text = Text(prefix, style=prefix_style)
649
+ self._message_text = Text(message, style=message_style)
650
+ self.append(self._prefix_text)
651
+ self.append(self._message_text)
652
+
653
+ def visualize(self) -> Visual:
654
+ """Build the width-aware visual while retaining ``Text.plain`` for search."""
655
+ return _HangingIndentVisual(
656
+ Content.from_rich_text(self._prefix_text),
657
+ Content.from_rich_text(self._message_text),
658
+ )
659
+
660
+
661
+ class _RollbackContent(VerticalGroup):
662
+ """Modal content composed with the shell.
663
+
664
+ The turn picker and footer are cheap to build (constructor data
665
+ only), so they render immediately; the diff area starts as a
666
+ loading indicator occupying the same slot the "No file changes"
667
+ placeholder uses, and :meth:`RollbackModal._render_diff_pane`
668
+ replaces it once the preview projection is built off-loop.
669
+ """
670
+
671
+ def __init__(
672
+ self,
673
+ options: list[tuple[Text, int]],
674
+ selected_turn: int,
675
+ render_message: Callable[[MessageRef], str],
676
+ ) -> None:
677
+ super().__init__(classes="rollback-content")
678
+ self._options = options
679
+ self._selected_turn = selected_turn
680
+ self._render_message = render_message
681
+
682
+ def compose(self) -> ComposeResult:
683
+ select: Select[int] = Select(
684
+ self._options,
685
+ id="rollback-turn-select",
686
+ value=self._selected_turn,
687
+ allow_blank=False,
688
+ prompt=self._render_message(_ROLLBACK_TO.bind()),
689
+ )
690
+ yield select
691
+
692
+ with VerticalGroup(id="rollback-diff-wrapper"):
693
+ yield ChrysLoadingIndicator(id="rollback-loading")
694
+
695
+ with HorizontalGroup(id="rollback-buttons"):
696
+ checkbox = Checkbox(
697
+ self._render_message(_REVERT_FILE_CHANGES.bind(count=0)),
698
+ value=True,
699
+ id="revert-checkbox",
700
+ )
701
+ # Hidden until a preview with actual file changes mounts.
702
+ checkbox.display = False
703
+ yield checkbox
704
+ yield Static("", id="rollback-buttons-spacer")
705
+ yield Button(
706
+ self._render_message(ROLLBACK_TITLE.bind()),
707
+ id="btn-rollback",
708
+ variant="primary",
709
+ flat=True,
710
+ disabled=True,
711
+ )
712
+ yield Button(self._render_message(_ROLLBACK_CLOSE.bind()), id="btn-close", variant="warning", flat=True)
713
+
714
+
715
+ class RollbackModal(BaseDialog[RollbackChoice | None]):
716
+ """Compact modal for choosing a rollback target and action."""
717
+
718
+ BINDINGS: ClassVar[list] = [
719
+ localized_binding("escape", "close", CLOSE_BINDING, priority=True),
720
+ # ``space`` mirrors the full /diff screen: toggles split/unified
721
+ # view for the file currently shown in the DiffView.
722
+ localized_binding("space", "toggle_view", TOGGLE_VIEW_BINDING, priority=True),
723
+ # ``x`` checks/unchecks the file (or folder) under the cursor in
724
+ # the change-list tree, scoping which files are reverted when the
725
+ # user confirms "& Revert Changes".
726
+ localized_binding("x", "toggle_check", _CHECK_BINDING, priority=True),
727
+ ]
728
+ _CONTENT_ACTIONS: ClassVar[frozenset[str]] = frozenset({"toggle_view", "toggle_check"})
729
+
730
+ CSS_PATH = "rollback_modal.tcss"
731
+
732
+ def __init__(
733
+ self,
734
+ tracker: MutationTracker | None,
735
+ available_turns: list[int],
736
+ cwd: str,
737
+ turn_prompts: dict[int, str] | None = None,
738
+ current_turn: int | None = None,
739
+ plan_augment: Callable[[RollbackPlan, str], RollbackPlan] | None = None,
740
+ attribution_refresh: Callable[[], Awaitable[bool]] | None = None,
741
+ load_state: Callable[[], Awaitable[RollbackModalState | None]] | None = None,
742
+ locale_controller: LocaleController | None = None,
743
+ ) -> None:
744
+ """
745
+ Args:
746
+ tracker: Live mutation tracker to read turn diffs from.
747
+ available_turns: ``target_turn`` values eligible for
748
+ rollback under the "keep N turns" convention.
749
+ Typically ``[0, 1, ..., N-1]`` where ``N`` is the
750
+ current turn count. ``0`` means "session start".
751
+ cwd: Working directory for computing relative paths.
752
+ turn_prompts: Optional ``{turn_number: user_prompt}`` keyed
753
+ by history turn index (``1``-based). Used to show a
754
+ dimmed preview of each turn's user message in the
755
+ picker. Missing turns render without a preview.
756
+ current_turn: Engine's current turn counter — the index
757
+ of the most-recently-started turn. Drives the
758
+ "Turn N (Current)" sentinel row in the picker.
759
+ When ``None`` (back-compat for tests), falls back to
760
+ ``max(available_turns) + 1`` which matches the prior
761
+ behaviour but can mislabel the sentinel when snapshots
762
+ have been pruned/deleted.
763
+ plan_augment: Optional cross-session peer check applied to
764
+ the preview plan (``(plan, cwd) -> plan``), mirroring
765
+ the engine's rollback-time augmentation so the modal
766
+ stays plan-WYSIWYG.
767
+ attribution_refresh: Optional peer reclassification hook
768
+ (``() -> awaitable[changed]``, the engine's refresh
769
+ path) run before each preview build. A peer claim
770
+ published after the modal opened can demote local rows
771
+ (contested/foreign), reshaping the plan itself —
772
+ ``plan_augment`` alone cannot surface that.
773
+ load_state: Optional deferred engine projection. When set,
774
+ the modal paints only its loading shell, then awaits this
775
+ callback and keeps the shell visible through preview
776
+ preparation. This is the production path for long
777
+ sessions; direct tracker arguments remain useful for
778
+ already-loaded callers and focused widget tests.
779
+ """
780
+ super().__init__()
781
+ self._tracker = tracker
782
+ self._available_turns = sorted(available_turns)
783
+ self._cwd = cwd
784
+ self._plan_augment = plan_augment
785
+ self._attribution_refresh = attribution_refresh
786
+ self._projection_acquire: Callable[[], Awaitable[str]] | None = None
787
+ self._projection_release: Callable[[str], None] | None = None
788
+ self._turn_prompts = turn_prompts or {}
789
+ self._current_turn = current_turn
790
+ self._load_state = load_state
791
+ self._locale_controller = locale_controller
792
+ # Default: preview rolling back the most-recent completable turn
793
+ # (largest ``target_turn`` — under "keep N turns" this is the
794
+ # state right after the last completed turn's predecessor).
795
+ self._selected_turn: int = self._available_turns[-1] if self._available_turns else 0
796
+ self._pane: DiffPeriodPane | None = None
797
+ self._content_loading = True
798
+ self._content_ready = False
799
+ self._preview_refreshing = False
800
+ self._preview_failed = False
801
+
802
+ def _render_message(self, reference: MessageRef) -> str:
803
+ controller = self._locale_controller
804
+ return format_message(reference) if controller is None else render_str(controller.localizer, reference)
805
+
806
+ def compose(self) -> ComposeResult:
807
+ # Production starts with a state-loading shell. Already-loaded
808
+ # callers start one stage later with the Select/footer and a preview
809
+ # spinner in the "No file changes" slot. Both remain compact until
810
+ # a real diff expands the modal (``_set_empty_layout``).
811
+ with VerticalGroup(id="rollback-container", classes="empty") as container:
812
+ container.border_title = Text(self._render_message(ROLLBACK_TITLE.bind()))
813
+ if self._load_state is None:
814
+ yield _RollbackContent(self._build_turn_options(), self._selected_turn, self._render_message)
815
+ else:
816
+ with VerticalGroup(id="rollback-initial-loading-state"):
817
+ yield ChrysLoadingIndicator(id="rollback-loading")
818
+
819
+ def on_mount(self) -> None:
820
+ if self._load_state is None:
821
+ self._refresh_button_labels()
822
+ self.call_after_refresh(self._start_initial_load)
823
+
824
+ def _start_initial_load(self) -> None:
825
+ self.run_worker(self._load_and_mount_content(), exclusive=True, group="rollback-modal-content")
826
+
827
+ async def _load_and_mount_content(self) -> None:
828
+ """Resolve state and preview while the first-painted loading shell remains."""
829
+ deferred_content: _RollbackContent | None = None
830
+ try:
831
+ if self._load_state is not None:
832
+ state = await self._load_state()
833
+ if state is None:
834
+ self.notify(
835
+ self._render_message(_NO_ROLLBACK_SNAPSHOTS.bind()),
836
+ title=self._render_message(ROLLBACK_TITLE.bind()),
837
+ severity="warning",
838
+ timeout=3,
839
+ markup=False,
840
+ )
841
+ self._content_loading = False
842
+ self.dismiss(None)
843
+ return
844
+ self._apply_loaded_state(state)
845
+ deferred_content = _RollbackContent(
846
+ self._build_turn_options(),
847
+ self._selected_turn,
848
+ self._render_message,
849
+ )
850
+ deferred_content.display = False
851
+ await self.query_one("#rollback-container", VerticalGroup).mount(deferred_content)
852
+ self._refresh_button_labels()
853
+
854
+ entries, plan = await self._load_preview_state_guarded()
855
+ await self._render_diff_pane(entries, plan)
856
+ if deferred_content is not None:
857
+ await self._reveal_deferred_content(deferred_content)
858
+ self._content_loading = False
859
+ self._content_ready = True
860
+ # Re-enables the rollback button and refreshes bindings.
861
+ self._set_preview_refreshing(False)
862
+ except RollbackLoadCancelled:
863
+ self._content_loading = False
864
+ self.dismiss(None)
865
+ except Exception as exc:
866
+ self._content_ready = False
867
+ try:
868
+ await self._show_load_error(exc)
869
+ if deferred_content is not None:
870
+ await self._reveal_deferred_content(deferred_content)
871
+ finally:
872
+ self._content_loading = False
873
+ self._preview_refreshing = False
874
+ with suppress(Exception):
875
+ self.refresh_bindings()
876
+
877
+ def _apply_loaded_state(self, state: RollbackModalState) -> None:
878
+ """Install one immutable-value engine projection for preview rendering."""
879
+ self._tracker = state.tracker
880
+ self._available_turns = sorted(state.available_turns)
881
+ self._turn_prompts = state.turn_prompts
882
+ self._current_turn = state.current_turn
883
+ self._cwd = state.workspace_cwd
884
+ self._plan_augment = state.plan_augment
885
+ self._attribution_refresh = state.attribution_refresh
886
+ self._projection_acquire = state.projection_acquire
887
+ self._projection_release = state.projection_release
888
+ if self._available_turns:
889
+ self._selected_turn = self._available_turns[-1]
890
+ else:
891
+ self._selected_turn = 0
892
+
893
+ async def _reveal_deferred_content(self, content: _RollbackContent) -> None:
894
+ """Swap fully prepared content for the initial indicator in one event-loop turn."""
895
+ content.display = True
896
+ loading_states = list(self.query("#rollback-initial-loading-state"))
897
+ if loading_states:
898
+ await remove_shielded(loading_states[0])
899
+
900
+ async def _show_load_error(self, exc: Exception) -> None:
901
+ """Replace the preview slot with a recoverable, fail-closed error state."""
902
+ if not self.is_attached:
903
+ return
904
+ with suppress(Exception):
905
+ self.query_one("#btn-rollback", Button).disabled = True
906
+ self._set_revert_checkbox_visible(False)
907
+ self._set_empty_layout(True)
908
+ message = Static(
909
+ Text(self._render_message(_LOAD_PREVIEW_ERROR.bind(detail=DisplayBlock(str(exc))))),
910
+ id="rollback-load-error",
911
+ )
912
+ wrappers = list(self.query("#rollback-diff-wrapper"))
913
+ if wrappers:
914
+ with suppress(Exception):
915
+ wrapper = wrappers[0]
916
+ await remove_children_shielded(wrapper)
917
+ await wrapper.mount(message)
918
+ return
919
+ # State projection itself failed, before the Select/footer could
920
+ # be built. Keep the modal recoverable by replacing the initial
921
+ # spinner with an error and a normal close affordance.
922
+ with suppress(Exception):
923
+ loading_state = self.query_one("#rollback-initial-loading-state", VerticalGroup)
924
+ await remove_children_shielded(loading_state)
925
+ await loading_state.mount(
926
+ message,
927
+ Button(
928
+ self._render_message(_ROLLBACK_CLOSE.bind()),
929
+ id="btn-close",
930
+ variant="warning",
931
+ flat=True,
932
+ ),
933
+ )
934
+
935
+ _PROMPT_PREVIEW_MAX = 160
936
+
937
+ def _build_turn_options(self) -> list[tuple[Text, int]]:
938
+ """Return ``(label, value)`` pairs for the turn-picker ``Select``.
939
+
940
+ Under the "keep N turns" convention, each ``target_turn = N``
941
+ directly labels itself "Turn N" (= last kept turn). Layout,
942
+ top → bottom:
943
+
944
+ 1. ``Turn {current} (Current)`` — disabled sentinel
945
+ communicating "you are here". ``current`` is the engine's
946
+ :attr:`AgentEngine.current_turn_number` when provided, else
947
+ ``max(available_turns) + 1`` as a fallback. Using the
948
+ engine counter keeps the sentinel correct even when every
949
+ snapshot on disk has been pruned/deleted (in which case
950
+ ``available_turns`` collapses to ``[0]``). Value is
951
+ :data:`_LATEST_SENTINEL` so :meth:`_on_turn_changed` can
952
+ detect accidental selection and snap back.
953
+ 2. For each rollback target ``t >= 1`` in ``available_turns``
954
+ (reverse sorted): ``Turn {t} <user prompt preview>``
955
+ (value = ``t``). Preview pulled from
956
+ ``self._turn_prompts[t]`` (history turn index = target).
957
+ 3. ``Session start — discard all`` (value = ``0``), iff ``0``
958
+ is in ``available_turns``.
959
+ """
960
+ if not self._available_turns:
961
+ return []
962
+ options: list[tuple[Text, int]] = []
963
+ # Prefer the engine-supplied current turn number — it's
964
+ # authoritative even when snapshots have been deleted (in
965
+ # which case ``available_turns`` may collapse to just ``[0]``
966
+ # and ``max + 1`` would under-report the true current turn).
967
+ current_turn = self._current_turn if self._current_turn is not None else self._available_turns[-1] + 1
968
+ options.append((self._format_latest_entry(current_turn), _LATEST_SENTINEL))
969
+ for t in sorted(self._available_turns, reverse=True):
970
+ if t == 0:
971
+ continue
972
+ options.append((self._format_keep_entry(t), t))
973
+ if 0 in self._available_turns:
974
+ options.append((self._format_session_start_entry(), 0))
975
+ return options
976
+
977
+ def _format_latest_entry(self, latest: int) -> Text:
978
+ """``Turn N (Current) - <preview>`` — fully dimmed."""
979
+ preview = self._clip_preview(self._turn_prompts.get(latest, ""))
980
+ if preview:
981
+ return _TurnOptionText(
982
+ self._render_message(_TURN_CURRENT_PREVIEW.bind(turn=latest)),
983
+ preview,
984
+ prefix_style="dim",
985
+ message_style="dim italic",
986
+ )
987
+ return Text(self._render_message(_TURN_CURRENT.bind(turn=latest)), style="dim")
988
+
989
+ def _format_keep_entry(self, kept: int) -> Text:
990
+ """``Turn N - <dim user prompt preview>`` for a selectable target."""
991
+ preview = self._clip_preview(self._turn_prompts.get(kept, ""))
992
+ if preview:
993
+ return _TurnOptionText(self._render_message(_TURN_PREVIEW.bind(turn=kept)), preview)
994
+ return Text(self._render_message(_TURN.bind(turn=kept)))
995
+
996
+ def _format_session_start_entry(self) -> Text:
997
+ """``Session start — discard all`` bottom row."""
998
+ label = Text(self._render_message(_SESSION_START.bind()))
999
+ label.append(self._render_message(_DISCARD_ALL_SUFFIX.bind()), style="dim")
1000
+ return label
1001
+
1002
+ def _clip_preview(self, raw: str) -> str:
1003
+ """Collapse whitespace + truncate to :attr:`_PROMPT_PREVIEW_MAX`."""
1004
+ preview = " ".join(raw.split()).strip()
1005
+ if len(preview) > self._PROMPT_PREVIEW_MAX:
1006
+ preview = preview[: self._PROMPT_PREVIEW_MAX - 1] + "…"
1007
+ return preview
1008
+
1009
+ async def _load_preview_state(self) -> tuple[list[DiffFileEntry], RollbackPlan | None]:
1010
+ """Refresh attribution and build the selected preview without touching the DOM."""
1011
+ if self._attribution_refresh is not None:
1012
+ # Peer claims published after the modal opened can demote
1013
+ # local rows (contested/foreign), reshaping the plan the
1014
+ # entries below are built from — reclassify BEFORE reading
1015
+ # the tracker, matching the engine's execution-time order
1016
+ # (reclassify → build plan → augment). This is the ENGINE
1017
+ # refresh (persists on change); cheap when nothing changed;
1018
+ # failures fall back to the stale view, which the engine
1019
+ # re-checks destructively anyway.
1020
+ try:
1021
+ await self._attribution_refresh()
1022
+ except Exception:
1023
+ logger.debug("Rollback preview attribution refresh failed", exc_info=True)
1024
+ tracker = self._tracker
1025
+ if tracker is None:
1026
+ raise RuntimeError("rollback state was not loaded")
1027
+ entries, plan = await asyncio.to_thread(_build_preview_state, tracker, self._selected_turn, self._cwd)
1028
+ # Paths the plan dropped (non-restorable, move-poisoned, foreign,
1029
+ # …) are surfaced as a non-checkable note, never silently.
1030
+ if plan is not None and self._plan_augment is not None:
1031
+ # Same peer check the engine runs at execution time
1032
+ # (PEER_MODIFIED_SINCE) — the preview must not offer files
1033
+ # the backend will then exclude. Registry I/O failures
1034
+ # leave the un-augmented plan; the engine re-checks anyway.
1035
+ # Executor hop: augmentation scans and reads peer registry
1036
+ # files — like every engine-side call of the same method,
1037
+ # that I/O must not run on the UI event loop.
1038
+ try:
1039
+ plan = await asyncio.to_thread(self._plan_augment, plan, self._cwd)
1040
+ except Exception:
1041
+ logger.debug("Rollback preview peer augmentation failed", exc_info=True)
1042
+ else:
1043
+ # ``entries`` intersected the UN-augmented plan (inside
1044
+ # ``_entries_for_target``); drop what the peer check just
1045
+ # excluded so the tree matches the plan.
1046
+ excluded_paths = {path for path, _reason in plan.exclusions}
1047
+ entries = [e for e in entries if e.path not in excluded_paths]
1048
+ return entries, plan
1049
+
1050
+ async def _load_preview_state_guarded(self) -> tuple[list[DiffFileEntry], RollbackPlan | None]:
1051
+ """Build one preview while backend prompt admission remains fenced."""
1052
+ acquire = self._projection_acquire
1053
+ release = self._projection_release
1054
+ if acquire is None or release is None:
1055
+ return await self._load_preview_state()
1056
+
1057
+ owner = await acquire()
1058
+ projection_task = asyncio.create_task(self._load_preview_state())
1059
+ release_deferred = False
1060
+ try:
1061
+ return await asyncio.shield(projection_task)
1062
+ except asyncio.CancelledError:
1063
+ release_deferred = True
1064
+
1065
+ def _finish_background_projection(
1066
+ task: asyncio.Task[tuple[list[DiffFileEntry], RollbackPlan | None]],
1067
+ ) -> None:
1068
+ try:
1069
+ task.result()
1070
+ except asyncio.CancelledError:
1071
+ pass
1072
+ except Exception:
1073
+ logger.debug("Cancelled rollback preview projection failed", exc_info=True)
1074
+ finally:
1075
+ release(owner)
1076
+
1077
+ projection_task.add_done_callback(_finish_background_projection)
1078
+ raise
1079
+ finally:
1080
+ if not release_deferred:
1081
+ release(owner)
1082
+
1083
+ async def _refresh_diff_pane(self) -> None:
1084
+ """Load and rebuild the preview for a newly selected turn."""
1085
+ self._preview_failed = False
1086
+ self._set_preview_refreshing(True)
1087
+ try:
1088
+ entries, plan = await self._load_preview_state_guarded()
1089
+ await self._render_diff_pane(entries, plan)
1090
+ except RollbackLoadCancelled:
1091
+ self.dismiss(None)
1092
+ except asyncio.CancelledError:
1093
+ self._preview_failed = True
1094
+ raise
1095
+ except Exception as exc:
1096
+ # The selected target changes before its projection is loaded. A
1097
+ # failure must therefore invalidate the old pane: otherwise its
1098
+ # checked paths could be confirmed against the new target turn.
1099
+ self._preview_failed = True
1100
+ logger.warning("Rollback preview refresh failed", exc_info=True)
1101
+ await self._show_preview_error(exc)
1102
+ finally:
1103
+ self._set_preview_refreshing(False)
1104
+
1105
+ async def _show_preview_error(self, exc: Exception) -> None:
1106
+ """Replace any prior projection with a non-destructive error state."""
1107
+ self._pane = None
1108
+ self._set_revert_checkbox_visible(False)
1109
+ self._set_empty_layout(True)
1110
+ try:
1111
+ wrapper = self.query_one("#rollback-diff-wrapper", VerticalGroup)
1112
+ await remove_children_shielded(wrapper)
1113
+ await wrapper.mount(
1114
+ Static(
1115
+ Text(self._render_message(_REFRESH_PREVIEW_ERROR.bind(detail=DisplayBlock(str(exc))))),
1116
+ id="rollback-preview-error",
1117
+ )
1118
+ )
1119
+ except Exception:
1120
+ # The safety state is the cleared pane + failed flag. Keep those
1121
+ # authoritative even if teardown races removal of the wrapper.
1122
+ logger.debug("Unable to render rollback preview error", exc_info=True)
1123
+
1124
+ async def _render_diff_pane(self, entries: list[DiffFileEntry], plan: RollbackPlan | None) -> None:
1125
+ """Replace preview widgets from an already-computed entry/plan projection."""
1126
+ wrapper = self.query_one("#rollback-diff-wrapper", VerticalGroup)
1127
+ # Drop the old pane reference before yielding so a queued confirm
1128
+ # can never act on a preview that is being replaced.
1129
+ self._pane = None
1130
+ await remove_children_shielded(wrapper)
1131
+ has_changes = bool(entries)
1132
+ exclusions = plan.exclusions if plan is not None else []
1133
+ warnings = plan.warnings if plan is not None else []
1134
+ # The revert checkbox has nothing to gate when there are no
1135
+ # file changes at all. Hide it (rather than disable it) so
1136
+ # the footer collapses to a clean "Rollback / Close" pair.
1137
+ self._set_revert_checkbox_visible(has_changes)
1138
+ # Collapse the modal to content height when there is no diff
1139
+ # to show — the big 90% overlay feels empty otherwise. A CSS
1140
+ # class on the container drives the layout swap (see
1141
+ # ``#rollback-container.empty`` in the .tcss); the class stays
1142
+ # absent while a diff is mounted so the container returns to
1143
+ # its full 90%.
1144
+ self._set_empty_layout(not has_changes)
1145
+ if not has_changes:
1146
+ widgets: list[Static] = [
1147
+ Static(Text(self._render_message(_NO_FILE_CHANGES.bind())), id="rollback-diff-placeholder")
1148
+ ]
1149
+ if warnings:
1150
+ widgets.append(
1151
+ Static(Text(_warning_note_text(warnings, self._locale_controller)), id="rollback-warnings-note")
1152
+ )
1153
+ if exclusions:
1154
+ # Text() so paths like ``a[bad].py`` render literally
1155
+ # instead of being parsed as Textual markup.
1156
+ widgets.append(
1157
+ Static(
1158
+ Text(_exclusion_note_text(exclusions, self._cwd, self._locale_controller)),
1159
+ id="rollback-exclusions-note",
1160
+ )
1161
+ )
1162
+ await wrapper.mount(*widgets)
1163
+ # Reset checkbox state so the next turn (which may have
1164
+ # changes) starts in a known state.
1165
+ self._sync_revert_checkbox(selected=0, total=0)
1166
+ return
1167
+ # ``checkable=True`` renders [x]/[ ] prefixes on every tree
1168
+ # entry. Defaults to all-checked; rebuilding the pane on turn
1169
+ # change (this method re-runs for every Select change) resets
1170
+ # the selection — satisfies "change turn → re-check everything".
1171
+ pane = DiffPeriodPane(entries, checkable=True, lazy=True)
1172
+ widgets_with_pane: list[Static | DiffPeriodPane] = [pane]
1173
+ if warnings:
1174
+ # The destructive confirm must not be the first place the
1175
+ # user learns a peer is mid-command — surface the advisory
1176
+ # BEFORE the buttons, next to the exclusion note.
1177
+ widgets_with_pane.append(
1178
+ Static(Text(_warning_note_text(warnings, self._locale_controller)), id="rollback-warnings-note")
1179
+ )
1180
+ if exclusions:
1181
+ widgets_with_pane.append(
1182
+ Static(
1183
+ Text(_exclusion_note_text(exclusions, self._cwd, self._locale_controller)),
1184
+ id="rollback-exclusions-note",
1185
+ )
1186
+ )
1187
+ await wrapper.mount(*widgets_with_pane)
1188
+ await pane.ensure_initialized()
1189
+ self._pane = pane
1190
+ # Fresh pane → every entry starts checked.
1191
+ self._sync_revert_checkbox(selected=len(entries), total=len(entries))
1192
+
1193
+ def _set_preview_refreshing(self, refreshing: bool) -> None:
1194
+ """Disable destructive/preview actions while a turn projection is rebuilt."""
1195
+ self._preview_refreshing = refreshing
1196
+ with suppress(Exception):
1197
+ self.query_one("#btn-rollback", Button).disabled = refreshing or self._preview_failed
1198
+ if self._content_ready:
1199
+ self.refresh_bindings()
1200
+
1201
+ def _set_revert_checkbox_visible(self, visible: bool) -> None:
1202
+ """Toggle visibility of the footer revert checkbox."""
1203
+ try:
1204
+ cb = self.query_one("#revert-checkbox", Checkbox)
1205
+ except Exception:
1206
+ return
1207
+ cb.display = visible
1208
+
1209
+ def _set_empty_layout(self, empty: bool) -> None:
1210
+ """Add/remove the ``empty`` class on the container.
1211
+
1212
+ Drives the height collapse/expand in the .tcss: the class
1213
+ swaps ``height: 90%`` for ``height: auto`` on the container
1214
+ and its wrapper children, so the modal shrinks to just the
1215
+ Select + placeholder + button row while loading and when
1216
+ there are no changes to revert. The container composes with
1217
+ the class already set; the first mounted diff flips it off so
1218
+ a turn with changes gets the full overlay.
1219
+ """
1220
+ try:
1221
+ container = self.query_one("#rollback-container", VerticalGroup)
1222
+ except Exception:
1223
+ return
1224
+ container.set_class(empty, "empty")
1225
+
1226
+ def _sync_revert_checkbox(self, *, selected: int, total: int) -> None:
1227
+ """Update the revert checkbox's label to reflect the current count.
1228
+
1229
+ Also auto-flips ``value`` at the edges so the visible state
1230
+ matches what the Rollback button will actually do:
1231
+
1232
+ - ``selected == 0`` → uncheck (nothing to revert, revert=False).
1233
+ - ``selected == total`` → check (every file included).
1234
+
1235
+ Programmatic value changes suppress ``Checkbox.Changed`` at the
1236
+ source so they cannot cascade back onto the tree after this method
1237
+ returns (the tree is the source of truth here).
1238
+ """
1239
+ try:
1240
+ cb = self.query_one("#revert-checkbox", Checkbox)
1241
+ except Exception:
1242
+ return
1243
+ cb.label = self._render_message(_REVERT_FILE_CHANGES.bind(count=selected))
1244
+ with cb.prevent(Checkbox.Changed):
1245
+ if selected == 0 and cb.value:
1246
+ cb.value = False
1247
+ # On turn change (total changed) force-reset value to True so
1248
+ # the user doesn't inherit the previous turn's toggle state.
1249
+ # Heuristic: a new turn always starts with selected == total.
1250
+ if selected == total and total > 0 and not cb.value:
1251
+ cb.value = True
1252
+
1253
+ def _refresh_button_labels(self) -> None:
1254
+ # Under the "keep N turns" convention the selected value *is*
1255
+ # the last kept turn, so the label is a direct substitution
1256
+ # — no gap-aware lookup needed. ``target = 0`` is the welcome
1257
+ # case and collapses to "Discard All".
1258
+ target = self._selected_turn
1259
+ label_plain = (
1260
+ self._render_message(_DISCARD_ALL.bind())
1261
+ if target <= 0
1262
+ else self._render_message(_DISCARD_AFTER_TURN.bind(turn=target))
1263
+ )
1264
+ self.query_one("#btn-rollback", Button).label = label_plain
1265
+
1266
+ @on(Select.Changed, "#rollback-turn-select")
1267
+ async def _on_turn_changed(self, event: Select.Changed) -> None:
1268
+ if event.value is Select.NULL:
1269
+ return
1270
+ new_turn = int(event.value)
1271
+ if new_turn == self._selected_turn:
1272
+ return
1273
+ if new_turn == _LATEST_SENTINEL or not self._content_ready or self._preview_refreshing:
1274
+ # The "you-are-here" sentinel entry is purely informational,
1275
+ # and a change landing before content is ready (or while a
1276
+ # refresh is in flight) cannot be honored. Either way the
1277
+ # Select must not be left displaying a turn that
1278
+ # ``_selected_turn`` doesn't hold — a silent drop here
1279
+ # desyncs the dropdown from the turn a later confirm
1280
+ # dismisses with. Snap back to the prior valid target;
1281
+ # ``prevent`` suppresses the recursive ``Select.Changed``
1282
+ # the reassignment would otherwise emit.
1283
+ with self.prevent(Select.Changed):
1284
+ event.select.value = self._selected_turn
1285
+ return
1286
+ self._selected_turn = new_turn
1287
+ self._refresh_button_labels()
1288
+ await self._refresh_diff_pane()
1289
+
1290
+ @on(Button.Pressed, "#btn-rollback")
1291
+ def _on_rollback(self, _event: Button.Pressed) -> None:
1292
+ """Confirm handler — decide whether to revert files based on
1293
+ the footer checkbox **and** the tree selection.
1294
+
1295
+ The rule the user asked for: revert iff the checkbox is
1296
+ checked *and* at least one file is selected in the tree.
1297
+ Otherwise fall through to a history-only rollback with no
1298
+ file restoration — matches what the user sees in the
1299
+ checkbox state.
1300
+ """
1301
+ if not self._content_ready or self._preview_refreshing or self._preview_failed:
1302
+ return
1303
+ paths = self._pane.selected_paths() if self._pane is not None else []
1304
+ try:
1305
+ cb = self.query_one("#revert-checkbox", Checkbox)
1306
+ want_revert = bool(cb.value)
1307
+ except Exception:
1308
+ want_revert = False
1309
+ if want_revert and paths:
1310
+ self.dismiss((self._selected_turn, True, paths))
1311
+ else:
1312
+ # No revert — selected_paths is moot, pass ``None`` so the
1313
+ # backend takes the plain-rollback path unambiguously.
1314
+ self.dismiss((self._selected_turn, False, None))
1315
+
1316
+ @on(Button.Pressed, "#btn-close")
1317
+ def _on_close(self, _event: Button.Pressed) -> None:
1318
+ self.dismiss(None)
1319
+
1320
+ @on(DiffPeriodPane.SelectionChanged)
1321
+ def _on_selection_changed(self, event: DiffPeriodPane.SelectionChanged) -> None:
1322
+ """Keep the revert-checkbox label in sync with the tree."""
1323
+ event.stop()
1324
+ self._sync_revert_checkbox(selected=event.selected_count, total=event.total_count)
1325
+
1326
+ @on(Checkbox.Changed, "#revert-checkbox")
1327
+ def _on_revert_checkbox_changed(self, event: Checkbox.Changed) -> None:
1328
+ """Cascade footer checkbox → every leaf in the tree.
1329
+
1330
+ Clicking the "Revert N file changes" checkbox off should clear
1331
+ the tree selection; clicking it back on should re-select
1332
+ everything. Programmatic writes from ``_sync_revert_checkbox``
1333
+ prevent this message at the source because they already reflect the
1334
+ tree state.
1335
+ """
1336
+ event.stop()
1337
+ if self._pane is None:
1338
+ return
1339
+ self._pane.set_all_checked(bool(event.value))
1340
+ selected, total = self._pane.selection_counts()
1341
+ self._sync_revert_checkbox(selected=selected, total=total)
1342
+
1343
+ def check_action(self, action: str, parameters: tuple[object, ...]) -> bool | None:
1344
+ """Hide and disable preview actions until interactive content is ready."""
1345
+ if action in self._CONTENT_ACTIONS and (
1346
+ not self._content_ready or self._preview_refreshing or self._preview_failed
1347
+ ):
1348
+ return False
1349
+ return super().check_action(action, parameters)
1350
+
1351
+ def _before_dismiss(self, _result: object | None = None) -> None:
1352
+ """Cancel initial preview work before any Esc, button, or backdrop dismissal."""
1353
+ if self._content_loading and self.is_attached:
1354
+ self.workers.cancel_group(self, "rollback-modal-content")
1355
+ super()._before_dismiss(_result)
1356
+
1357
+ def action_close(self) -> None:
1358
+ self.dismiss(None)
1359
+
1360
+ def action_toggle_view(self) -> None:
1361
+ """Space binding — delegate split/unified toggle to the pane."""
1362
+ if self._content_ready and not self._preview_refreshing and not self._preview_failed and self._pane is not None:
1363
+ self._pane.toggle_view()
1364
+
1365
+ def action_toggle_check(self) -> None:
1366
+ """X binding — toggle the check state of the cursor node.
1367
+
1368
+ Operates on whichever ``Tree`` inside the pane currently has
1369
+ focus. Folder cursor → cascades to every descendant leaf
1370
+ (fully-checked folders flip to unchecked, otherwise everything
1371
+ flips on) per the common tri-state convention.
1372
+ """
1373
+ if not self._content_ready or self._preview_refreshing or self._preview_failed or self._pane is None:
1374
+ return
1375
+ focused = self.focused
1376
+ if not isinstance(focused, Tree):
1377
+ return
1378
+ node = focused.cursor_node
1379
+ if node is None:
1380
+ return
1381
+ self._pane.toggle_check_node(node)