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,1769 @@
1
+ # Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
2
+
3
+ """ChatPanel — scrollable chat area that dynamically mounts message widgets.
4
+
5
+ Replaces the old WorkPanel (plain Log) with rich, interactive widgets for
6
+ each message type: user, agent, tool calls, context folds, errors, etc.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import itertools
12
+ import logging
13
+ from collections.abc import Awaitable, Callable
14
+ from typing import TYPE_CHECKING, Any
15
+ from weakref import WeakKeyDictionary, WeakSet
16
+
17
+ from rich.text import Text
18
+ from textual import events as textual_events
19
+ from textual._arrange import DockArrangeResult
20
+ from textual.containers import VerticalScroll
21
+ from textual.dom import NoScreen
22
+ from textual.geometry import Offset, Region, Size
23
+ from textual.message import Message
24
+ from textual.reactive import reactive
25
+ from textual.scrollbar import ScrollBar, ScrollDown, ScrollTo, ScrollUp
26
+ from textual.timer import Timer
27
+ from textual.widget import Widget
28
+
29
+ from chrys.app.tui.behaviors.right_click_copy import cancel_textual_mouse_chain
30
+ from chrys.app.tui.support.gc_freeze import GcFreezeBlockReason, raise_gc_freeze_hook_errors
31
+ from chrys.app.tui.util.removal import finish_shielded, remove_shielded
32
+ from chrys.app.tui.widgets import ASK_USER_INTERACTIVE_CLASS
33
+ from chrys.foundation.patches.textual_precompose import precompose_tree
34
+
35
+ if TYPE_CHECKING:
36
+ from textual.events import Click
37
+
38
+ from chrys.app.tui.i18n import LocaleController
39
+ from chrys.app.tui.widgets import PromptDraft
40
+ from chrys.foundation.events.types import ProvisionalPresentation
41
+ from chrys.foundation.models.ask_user import AskUserQuestion
42
+ from chrys.service.context.providers.history import CompressedBlock
43
+ from chrys.service.session.sub_agent_transcript import PersistedSubAgentTranscript
44
+
45
+ from chrys.app.tui.widgets.chat.chrome import (
46
+ ChatBottomSpacer,
47
+ ChatPanelChrome,
48
+ ScrollToBottomButton,
49
+ ScrollToBottomRequested,
50
+ )
51
+ from chrys.app.tui.widgets.chat.file_snapshot import FileSnapshotPayload
52
+ from chrys.app.tui.widgets.chat.live_renderer import LiveTranscriptRenderer
53
+ from chrys.app.tui.widgets.chat.messages import (
54
+ AgentMessage,
55
+ ErrorMessage,
56
+ InterruptedMessage,
57
+ RetryMessage,
58
+ SystemMessage,
59
+ UserMessage,
60
+ )
61
+ from chrys.app.tui.widgets.chat.ports import ChatTranscriptPanelMarker, TranscriptLocalizationPort
62
+ from chrys.app.tui.widgets.chat.renderers.ask_user import AskUserInlineResized, AskUserToolCall
63
+ from chrys.app.tui.widgets.chat.renderers.sub_agent import SubAgentToolCall
64
+ from chrys.app.tui.widgets.chat.replay import HistoryReplayRenderer
65
+ from chrys.app.tui.widgets.chat.replay_mount import ReplayMountController
66
+ from chrys.app.tui.widgets.chat.scroll_controller import ChatScrollController
67
+ from chrys.app.tui.widgets.chat.toc_model import TurnTocModel
68
+ from chrys.app.tui.widgets.chat.tool_call import ToolGroup
69
+ from chrys.app.tui.widgets.chat.tool_registry import ToolGroupRegistry
70
+ from chrys.app.tui.widgets.chat.transcript_reader import TranscriptReader
71
+ from chrys.app.tui.widgets.sidebar.toc_types import TocItem
72
+ from chrys.app.tui.widgets.welcome import WelcomeWidget
73
+
74
+ logger = logging.getLogger(__name__)
75
+
76
+ _ChatBottomSpacer = ChatBottomSpacer
77
+ _ScrollToBottomButton = ScrollToBottomButton
78
+
79
+ # Document-order stamps for top-level transcript widgets. The chat selection
80
+ # model orders leaves by (stamp, sibling path) so drag selection never needs
81
+ # geometry for widgets outside the viewport. A process-wide counter keeps
82
+ # stamps monotonic across session resets and multiple panels.
83
+ _transcript_seq_counter = itertools.count(1)
84
+ _REPLAY_PLACEHOLDER_CLASS = "-replay-placeholder"
85
+
86
+
87
+ class ChatPanel(VerticalScroll, ChatTranscriptPanelMarker, can_focus=True):
88
+ """Main scrollable chat area that dynamically adds message widgets.
89
+
90
+ API mirrors the old WorkPanel but mounts rich widgets instead of writing
91
+ plain text lines.
92
+ """
93
+
94
+ DEFAULT_CSS = """
95
+ ChatPanel {
96
+ min-width: 35;
97
+ min-height: 3;
98
+ height: 1fr;
99
+ border: round $tui-border-primary $border-opacity;
100
+ scrollbar-size: 1 1;
101
+ padding: 0 0 0 1;
102
+ border-title-align: left;
103
+ border-title-color: $primary;
104
+ border-subtitle-align: right;
105
+ border-subtitle-color: $primary;
106
+ }
107
+ ChatPanel > .-replay-placeholder {
108
+ hatch: left $hatch-color;
109
+ }
110
+ """
111
+
112
+ agent_running: reactive[bool] = reactive(False)
113
+ profile_name: reactive[str] = reactive("")
114
+ session_id_value: reactive[str] = reactive("")
115
+ session_title_value: reactive[str] = reactive("")
116
+ workspace_cwd: reactive[str] = reactive("")
117
+ workspace_branch: reactive[str] = reactive("")
118
+
119
+ def __init__(
120
+ self,
121
+ cwd: str = "",
122
+ *,
123
+ localization: TranscriptLocalizationPort | None = None,
124
+ locale_controller: LocaleController | None = None,
125
+ tool_groups_expanded: Callable[[], bool] | None = None,
126
+ sub_agent_transcript_loader: Callable[[str], Awaitable[PersistedSubAgentTranscript | None]] | None = None,
127
+ **kwargs: Any,
128
+ ) -> None:
129
+ super().__init__(**kwargs)
130
+ # Document-order stamps for top-level transcript children; weak keys
131
+ # so removed widgets drop their entries with the widget itself.
132
+ self._transcript_seqs: WeakKeyDictionary[Widget, int] = WeakKeyDictionary()
133
+ self._ask_user_tool_widgets: WeakSet[AskUserToolCall] = WeakSet()
134
+ self._replay_progress_callback: Callable[[int, int], None] | None = None
135
+ self._locale_controller = locale_controller
136
+ self._sub_agent_transcript_loader = sub_agent_transcript_loader
137
+ self._chrome = ChatPanelChrome(self, locale_controller=locale_controller)
138
+ self._scroll_controller = ChatScrollController(self)
139
+ self._toc = TurnTocModel()
140
+ self._tool_registry = ToolGroupRegistry(
141
+ self,
142
+ self,
143
+ self,
144
+ finalize_agent_message=self._finalize_current_agent_message,
145
+ tool_groups_expanded=tool_groups_expanded,
146
+ tool_widget_configurator=self._configure_tool_widget,
147
+ )
148
+ self._live_renderer = LiveTranscriptRenderer(
149
+ self,
150
+ self,
151
+ self,
152
+ self,
153
+ self._toc,
154
+ self._tool_registry,
155
+ localization=localization,
156
+ )
157
+ self._replay_renderer = HistoryReplayRenderer(
158
+ self,
159
+ self,
160
+ self._toc,
161
+ self._tool_registry.resolve_tool_kind,
162
+ default_profile=lambda: self._profile_name,
163
+ localization=localization,
164
+ tool_widget_configurator=self._configure_tool_widget,
165
+ )
166
+ self._transcript_reader = TranscriptReader(self)
167
+ self._replay_mount = ReplayMountController(self)
168
+ if cwd:
169
+ self.workspace_cwd = cwd
170
+
171
+ def _configure_tool_widget(self, widget: Widget) -> None:
172
+ if isinstance(widget, AskUserToolCall):
173
+ self._ask_user_tool_widgets.add(widget)
174
+ widget.handle_chat_viewport_resize(self.size.height)
175
+ if isinstance(widget, SubAgentToolCall) and self._sub_agent_transcript_loader is not None:
176
+ widget.configure_transcript_loader(self._sub_agent_transcript_loader)
177
+
178
+ def gc_freeze_block_reason(self) -> GcFreezeBlockReason | None:
179
+ """The MainScreen owns ChatPanel's activity and scroll hard gates."""
180
+ return None
181
+
182
+ def focus_on_click(self) -> bool:
183
+ """Keep passive transcript clicks from stealing composer focus."""
184
+ return False
185
+
186
+ def prepare_for_gc_freeze(self) -> None:
187
+ """Release cyclic transcript render caches before a GC freeze action."""
188
+ for surface in self._gc_freeze_render_cache_surfaces():
189
+ surface.prepare_for_gc_freeze()
190
+
191
+ def after_gc_freeze(self) -> None:
192
+ """Renew all transcript render caches, then report every failure."""
193
+ errors: list[Exception] = []
194
+ for surface in self._gc_freeze_render_cache_surfaces():
195
+ try:
196
+ surface.after_gc_freeze()
197
+ except Exception as error:
198
+ errors.append(error)
199
+ raise_gc_freeze_hook_errors("Transcript cache renewal failed", errors)
200
+
201
+ def abort_gc_freeze(self) -> None:
202
+ """Best-effort restore every detached transcript render cache."""
203
+ for surface in self._gc_freeze_render_cache_surfaces():
204
+ try:
205
+ surface.abort_gc_freeze()
206
+ except Exception:
207
+ logger.exception("Failed to restore detached transcript cache on %s", type(surface).__name__)
208
+
209
+ def _gc_freeze_render_cache_surfaces(self) -> list[Any]:
210
+ """Return mounted transcript descendants with renewable cyclic LRUs."""
211
+ from chrys.app.tui.widgets.diff_view.code import CodeColumn
212
+ from chrys.app.tui.widgets.diff_view.gutter import GutterColumn
213
+ from chrys.app.tui.widgets.diff_view.unified import UnifiedDiffLines
214
+ from chrys.app.tui.widgets.markdown.widget import VirtualizedMarkdown
215
+
216
+ surface_types = (VirtualizedMarkdown, UnifiedDiffLines, CodeColumn, GutterColumn)
217
+ return [widget for widget in self.walk_children() if isinstance(widget, surface_types)]
218
+
219
+ def watch_agent_running(self, old_running: bool, running: bool) -> None:
220
+ self._scroll_controller.set_agent_running(running)
221
+
222
+ def watch_profile_name(self, profile_name: str) -> None:
223
+ self._live_renderer.set_profile(profile_name)
224
+
225
+ def watch_session_id_value(self, session_id: str) -> None:
226
+ self._chrome.set_session_id(session_id)
227
+
228
+ def watch_session_title_value(self, title: str) -> None:
229
+ self._chrome.set_session_title(title)
230
+
231
+ def watch_workspace_cwd(self, cwd: str) -> None:
232
+ self._chrome.set_workspace_cwd(cwd)
233
+
234
+ def watch_workspace_branch(self, branch: str) -> None:
235
+ self._chrome.set_workspace_branch(branch)
236
+
237
+ def scroll_to_region(self, region: Region, **kwargs: Any) -> Offset:
238
+ """Ignore focus-restoration center scrolls inside the transcript.
239
+
240
+ Chat has explicit navigation paths (``top=True`` and bottom-anchor
241
+ sync); modal dismissal focus recovery must not yank it to old content.
242
+ """
243
+ return self._scroll_controller.scroll_to_region(region, **kwargs)
244
+
245
+ def _check_anchor(self) -> None:
246
+ """Re-engage bottom-follow only at exact bottom."""
247
+ self._scroll_controller.check_anchor()
248
+
249
+ def set_tool_kinds(self, tool_kinds: dict[str, str]) -> None:
250
+ """Set tool name → kind map (used during session replay)."""
251
+ self._tool_registry.set_tool_kinds(tool_kinds)
252
+
253
+ def set_profile(self, name: str) -> None:
254
+ """Set the current agent profile name (shown in agent message headers)."""
255
+ self.profile_name = name
256
+
257
+ @property
258
+ def _current_agent_msg(self) -> AgentMessage | None:
259
+ return self._live_renderer.current_agent_msg
260
+
261
+ @_current_agent_msg.setter
262
+ def _current_agent_msg(self, message: AgentMessage | None) -> None:
263
+ self._live_renderer.current_agent_msg = message
264
+
265
+ def _finalize_current_agent_message(self) -> None:
266
+ """Terminate live streamed prose before another transcript boundary."""
267
+ self._live_renderer._finalize_current_agent_message()
268
+
269
+ @property
270
+ def _profile_name(self) -> str:
271
+ return self._live_renderer.profile_name
272
+
273
+ @_profile_name.setter
274
+ def _profile_name(self, name: str) -> None:
275
+ self._live_renderer.profile_name = name
276
+
277
+ @property
278
+ def _agent_running(self) -> bool:
279
+ return self._scroll_controller.agent_running
280
+
281
+ @_agent_running.setter
282
+ def _agent_running(self, running: bool) -> None:
283
+ self._scroll_controller.agent_running = running
284
+
285
+ @property
286
+ def _current_tool_group(self) -> ToolGroup | None:
287
+ return self._tool_registry.current_tool_group
288
+
289
+ @_current_tool_group.setter
290
+ def _current_tool_group(self, group: ToolGroup | None) -> None:
291
+ self._tool_registry.current_tool_group = group
292
+
293
+ @property
294
+ def _sub_agent_widgets(self) -> dict[str, SubAgentToolCall]:
295
+ return self._tool_registry.sub_agent_widgets
296
+
297
+ @property
298
+ def _unlinked_sub_agents(self) -> dict[str, list[SubAgentToolCall]]:
299
+ return self._tool_registry.unlinked_sub_agents
300
+
301
+ @property
302
+ def _tool_groups_by_call_id(self) -> dict[str, ToolGroup]:
303
+ return self._tool_registry.tool_groups_by_call_id
304
+
305
+ @property
306
+ def _tool_kinds(self) -> dict[str, str]:
307
+ return self._tool_registry.tool_kinds
308
+
309
+ @_tool_kinds.setter
310
+ def _tool_kinds(self, tool_kinds: dict[str, str]) -> None:
311
+ self._tool_registry.tool_kinds = tool_kinds
312
+
313
+ @property
314
+ def _welcome(self) -> WelcomeWidget | None:
315
+ return self._chrome.welcome
316
+
317
+ @_welcome.setter
318
+ def _welcome(self, widget: WelcomeWidget | None) -> None:
319
+ self._chrome.welcome = widget
320
+
321
+ @property
322
+ def _bottom_spacer(self) -> ChatBottomSpacer | None:
323
+ return self._chrome.bottom_spacer
324
+
325
+ @_bottom_spacer.setter
326
+ def _bottom_spacer(self, widget: ChatBottomSpacer | None) -> None:
327
+ self._chrome.bottom_spacer = widget
328
+
329
+ @property
330
+ def _scroll_to_bottom_button(self) -> ScrollToBottomButton | None:
331
+ return self._chrome.scroll_to_bottom_button
332
+
333
+ @_scroll_to_bottom_button.setter
334
+ def _scroll_to_bottom_button(self, widget: ScrollToBottomButton | None) -> None:
335
+ self._chrome.scroll_to_bottom_button = widget
336
+
337
+ @property
338
+ def _session_id(self) -> str:
339
+ return self._chrome.session_id
340
+
341
+ @_session_id.setter
342
+ def _session_id(self, session_id: str) -> None:
343
+ self._chrome.set_session_id(session_id)
344
+
345
+ @property
346
+ def _held_shrink_height(self) -> int:
347
+ return self._scroll_controller.held_shrink_height
348
+
349
+ @_held_shrink_height.setter
350
+ def _held_shrink_height(self, value: int) -> None:
351
+ self._scroll_controller.held_shrink_height = value
352
+
353
+ @property
354
+ def _manual_scroll_gc_timer(self) -> Timer | None:
355
+ return self._scroll_controller.manual_scroll_gc_timer
356
+
357
+ @_manual_scroll_gc_timer.setter
358
+ def _manual_scroll_gc_timer(self, timer: Timer | None) -> None:
359
+ self._scroll_controller.manual_scroll_gc_timer = timer
360
+
361
+ @property
362
+ def _manual_scroll_gc_paused(self) -> bool:
363
+ return self._scroll_controller.manual_scroll_gc_paused
364
+
365
+ @_manual_scroll_gc_paused.setter
366
+ def _manual_scroll_gc_paused(self, value: bool) -> None:
367
+ self._scroll_controller.manual_scroll_gc_paused = value
368
+
369
+ @property
370
+ def _auto_scroll_paused_by_user(self) -> bool:
371
+ return self._scroll_controller.auto_scroll_paused_by_user
372
+
373
+ @_auto_scroll_paused_by_user.setter
374
+ def _auto_scroll_paused_by_user(self, value: bool) -> None:
375
+ self._scroll_controller.auto_scroll_paused_by_user = value
376
+
377
+ @property
378
+ def _final_response_started(self) -> bool:
379
+ return self._scroll_controller.final_response_started
380
+
381
+ @_final_response_started.setter
382
+ def _final_response_started(self, value: bool) -> None:
383
+ self._scroll_controller.final_response_started = value
384
+
385
+ @property
386
+ def _programmatic_scroll(self) -> bool:
387
+ return self._scroll_controller.programmatic_scroll
388
+
389
+ @_programmatic_scroll.setter
390
+ def _programmatic_scroll(self, value: bool) -> None:
391
+ self._scroll_controller.programmatic_scroll = value
392
+
393
+ @property
394
+ def _anchor_sync_scheduled(self) -> bool:
395
+ return self._scroll_controller.anchor_sync_scheduled
396
+
397
+ @_anchor_sync_scheduled.setter
398
+ def _anchor_sync_scheduled(self, value: bool) -> None:
399
+ self._scroll_controller.anchor_sync_scheduled = value
400
+
401
+ def compose(self):
402
+ welcome, bottom_spacer, scroll_button = self._chrome.initial_widgets()
403
+ yield welcome
404
+ yield bottom_spacer
405
+ yield scroll_button
406
+
407
+ def mount(self, *widgets, **kwargs): # type: ignore[override]
408
+ """Mount widgets before the bottom spacer (spacer stays last).
409
+
410
+ Gates ``before=`` injection on the spacer still being an attached
411
+ child: :meth:`clear` nulls the reference before ``remove_children``
412
+ awaits, and a mid-reset mount could otherwise hit ``MountError:
413
+ Unable to find relative location`` (observed on Windows during
414
+ session restore). Stale reference is then a safe no-op.
415
+ """
416
+ spacer = self._bottom_spacer
417
+ if (
418
+ spacer is not None
419
+ and spacer.parent is self
420
+ and "before" not in kwargs
421
+ and "after" not in kwargs
422
+ and spacer not in widgets
423
+ and self._scroll_to_bottom_button not in widgets
424
+ ):
425
+ kwargs["before"] = spacer
426
+ for widget in widgets:
427
+ if isinstance(widget, Widget) and widget not in self._transcript_seqs:
428
+ self._transcript_seqs[widget] = next(_transcript_seq_counter)
429
+ return super().mount(*widgets, **kwargs)
430
+
431
+ def transcript_seq(self, widget: Widget) -> int | None:
432
+ """Document-order stamp for a top-level child; ``None`` if never
433
+ mounted through this panel."""
434
+ return self._transcript_seqs.get(widget)
435
+
436
+ def stamp_transcript_order(self, widgets: list[Widget]) -> None:
437
+ """Stamp document order up front for entries mounted newest first."""
438
+ for widget in widgets:
439
+ if widget not in self._transcript_seqs:
440
+ self._transcript_seqs[widget] = next(_transcript_seq_counter)
441
+
442
+ def on_mount(self) -> None:
443
+ if self._locale_controller is not None:
444
+ self._locale_controller.register_surface(self)
445
+ # Textual's native anchor: pin to bottom as content grows, auto-release
446
+ # when user scrolls up, auto-re-engage when they return to the bottom.
447
+ self.anchor()
448
+ # Scrollbar thumb drags must hold the scroll-GC pause for the whole
449
+ # gesture: a slow drag's gaps between mouse moves can exceed any
450
+ # cadence debounce, and a mid-gesture GC resume triggers a backlog
451
+ # collection spike on the very next drag movement.
452
+ self.watch(self.vertical_scrollbar, "grabbed", self._on_scrollbar_grabbed_changed, init=False)
453
+
454
+ def _on_scrollbar_grabbed_changed(self, grabbed: object) -> None:
455
+ """Map scrollbar grab state onto the gesture-scoped GC pause."""
456
+ if grabbed is not None:
457
+ self.note_user_scroll()
458
+ self._scroll_controller.on_scrollbar_grab()
459
+ else:
460
+ self._scroll_controller.on_scrollbar_release()
461
+
462
+ def on_unmount(self) -> None:
463
+ if self._locale_controller is not None:
464
+ self._locale_controller.unregister_surface(self)
465
+ self._replay_mount.abandon()
466
+ self._scroll_controller.stop()
467
+
468
+ def refresh_localization(self) -> None:
469
+ """Retranslate bounded chat chrome without walking transcript children."""
470
+ self._chrome.refresh_localization()
471
+
472
+ def arrange(self, size: Size, optimal: bool = False) -> DockArrangeResult:
473
+ """Defuse Textual anchor pins while chat layout is settling.
474
+
475
+ The compositor writes ``scroll_y`` directly from arranged height.
476
+ Keep that math current, clamp stale negative pins, and skip one
477
+ anchored shrink frame; ``_size_updated`` handles the validator side.
478
+ """
479
+ return self._scroll_controller.arrange(size, optimal=optimal)
480
+
481
+ def _place_scroll_to_bottom_button(self, result: DockArrangeResult, size: Size) -> None:
482
+ """Keep the scroll-bottom affordance fixed to the viewport bottom.
483
+
484
+ Runs on every arrange call — including cache hits that hand back the
485
+ same ``DockArrangeResult`` instance — so it must be idempotent and
486
+ cheap: the button sits at the tail of the transcript children, and an
487
+ already-patched placement returns without touching the result.
488
+ Resetting ``result._spatial_map`` forces an O(children) rebuild on the
489
+ next spatial query; doing that per scroll frame dominated scrolling on
490
+ large transcripts.
491
+ """
492
+ button = self._scroll_to_bottom_button
493
+ if button is None:
494
+ return
495
+ placements = result.placements
496
+ for index in range(len(placements) - 1, -1, -1):
497
+ placement = placements[index]
498
+ if placement.widget is not button:
499
+ continue
500
+ width = min(max(1, placement.region.width), max(1, size.width))
501
+ height = min(max(1, placement.region.height), max(1, size.height))
502
+ x = max(0, (size.width - width) // 2)
503
+ y = max(0, size.height - height)
504
+ region = Region(x, y, width, height)
505
+ if placement.fixed and placement.region == region and placement.offset == Offset():
506
+ return
507
+ placements[index] = placement._replace(
508
+ region=region,
509
+ offset=Offset(),
510
+ order=2**31 - 1,
511
+ # Textual will otherwise apply the container's scroll offset
512
+ # to this normal child. Keep it viewport-relative while still
513
+ # mounted inside ChatPanel for click routing and lifecycle.
514
+ fixed=True,
515
+ # The fixed region owns positioning; the button's CSS
516
+ # ``position: absolute`` only exists to keep it out of flow.
517
+ absolute=False,
518
+ )
519
+ result._spatial_map = None
520
+ return
521
+
522
+ def _reanchor_after_settle(self) -> None:
523
+ """Re-engage the anchor and pin to settled ``max_scroll_y``."""
524
+ self._scroll_controller.reanchor_after_settle()
525
+
526
+ def _set_scroll_y_programmatically(self, y: float) -> None:
527
+ """Set vertical scroll without treating the movement as a user pause."""
528
+ self._scroll_controller.set_scroll_y_programmatically(y)
529
+
530
+ def jump_to_bottom(self) -> None:
531
+ """Scroll to the current bottom and resume bottom-follow."""
532
+ self.note_user_scroll()
533
+ self._scroll_controller.jump_to_bottom()
534
+
535
+ def note_user_scroll(self) -> None:
536
+ """A scroll the user asked for supersedes a TOC jump still waiting for its turn to mount.
537
+
538
+ Hooked on user intents (wheel, scrollbar, keys), never on ``scroll_y``:
539
+ the prepend itself moves ``scroll_y`` to keep the view steady.
540
+ """
541
+ self._replay_mount.forget_jump()
542
+
543
+ def on_resize(self, _event: object | None = None) -> None:
544
+ """Refresh the bottom-jump affordance when viewport height changes."""
545
+ self._scroll_controller.on_resize()
546
+ viewport_height = self.size.height
547
+ for widget in tuple(self._ask_user_tool_widgets):
548
+ if widget.is_attached and not widget._closing and not widget._pruning:
549
+ widget.handle_chat_viewport_resize(viewport_height)
550
+
551
+ def _size_updated(
552
+ self,
553
+ size: Size,
554
+ virtual_size: Size,
555
+ container_size: Size,
556
+ layout: bool = True,
557
+ ) -> bool:
558
+ """Hold one anchored shrink frame before validator clamping.
559
+
560
+ A transient ``virtual_size`` drop can clamp ``scroll_y`` to a false
561
+ smaller bottom. Hold each shrink height once, refresh layout, then
562
+ accept it if it persists.
563
+ """
564
+ return self._scroll_controller.size_updated(size, virtual_size, container_size, layout)
565
+
566
+ def _release_size_hold(self) -> None:
567
+ """Force a layout pass that either recovers or accepts the held shrink."""
568
+ self._scroll_controller.release_size_hold()
569
+
570
+ def watch_scroll_y(self, old_value: float, new_value: float) -> None:
571
+ """Track manual scrolling; no cleanup repaint is scheduled.
572
+
573
+ Textual's scroll fast-path marks every moved widget's old and new
574
+ region dirty, so newly exposed rows repaint correctly on their own —
575
+ pinned cell-exactly by ``test_chat_scroll_fastpath.py``. Manual
576
+ ticks only feed the controller's GC pause and follow-state
577
+ bookkeeping, plus the selection reprojection below.
578
+ """
579
+ self._scroll_controller.watch_scroll_y(old_value, new_value)
580
+ self._notify_screen_selection_scrolled()
581
+
582
+ def _notify_screen_selection_scrolled(self) -> None:
583
+ """Let the owning screen re-align chat selection highlights.
584
+
585
+ The chat drag-selection model keeps ``screen.selections`` bounded to
586
+ the visible transcript slice, so scrolling must re-project highlights
587
+ onto newly revealed widgets. No-op (a getattr and an ``active`` check)
588
+ unless a chat selection exists.
589
+ """
590
+ try:
591
+ screen = self.screen
592
+ except NoScreen:
593
+ return
594
+ schedule = getattr(screen, "schedule_chat_selection_reprojection", None)
595
+ if schedule is not None:
596
+ schedule(self)
597
+
598
+ def _on_mouse_scroll_up(self, event: textual_events.MouseScrollUp) -> None:
599
+ """Pause cyclic GC before Textual mutates scroll state for wheel input."""
600
+ # Do not call super(): Textual will dispatch Widget._on_mouse_scroll_up
601
+ # later in the same MRO handler walk, and a manual super() scrolls twice.
602
+ self.note_user_scroll()
603
+ self._scroll_controller.pause_gc_for_manual_scroll()
604
+
605
+ def _on_mouse_scroll_down(self, event: textual_events.MouseScrollDown) -> None:
606
+ """Pause cyclic GC before Textual mutates scroll state for wheel input."""
607
+ # Do not call super(): Textual will dispatch Widget._on_mouse_scroll_down
608
+ # later in the same MRO handler walk, and a manual super() scrolls twice.
609
+ self.note_user_scroll()
610
+ self._scroll_controller.pause_gc_for_manual_scroll()
611
+
612
+ def _on_scroll_to(self, message: ScrollTo) -> None:
613
+ """Pause cyclic GC before scrollbar thumb drags update scroll state."""
614
+ # Do not call super(): Textual will dispatch Widget._on_scroll_to
615
+ # later in the same MRO handler walk, restarting the drag scroll work.
616
+ self.note_user_scroll()
617
+ self._scroll_controller.pause_gc_for_manual_scroll()
618
+
619
+ def _on_scroll_up(self, event: ScrollUp) -> None:
620
+ # Scrollbar track click; Widget._on_scroll_up pages later in the same MRO walk.
621
+ self.note_user_scroll()
622
+
623
+ def _on_scroll_down(self, event: ScrollDown) -> None:
624
+ # Scrollbar track click; Widget._on_scroll_down pages later in the same MRO walk.
625
+ self.note_user_scroll()
626
+
627
+ def action_scroll_up(self) -> None:
628
+ self.note_user_scroll()
629
+ super().action_scroll_up()
630
+
631
+ def action_scroll_down(self) -> None:
632
+ self.note_user_scroll()
633
+ super().action_scroll_down()
634
+
635
+ def action_page_up(self) -> None:
636
+ self.note_user_scroll()
637
+ super().action_page_up()
638
+
639
+ def action_page_down(self) -> None:
640
+ self.note_user_scroll()
641
+ super().action_page_down()
642
+
643
+ def action_scroll_home(self) -> None:
644
+ self.note_user_scroll()
645
+ super().action_scroll_home()
646
+
647
+ def action_scroll_end(self) -> None:
648
+ self.note_user_scroll()
649
+ super().action_scroll_end()
650
+
651
+ def _sync_scroll_to_bottom_button(self) -> None:
652
+ """Show the bottom-jump affordance only when there is unseen content below."""
653
+ self._scroll_controller.sync_scroll_to_bottom_button()
654
+
655
+ def _pause_gc_for_manual_scroll(self) -> None:
656
+ """Temporarily disable cyclic GC while user scrolling is active."""
657
+ self._scroll_controller.pause_gc_for_manual_scroll()
658
+
659
+ def _resume_gc_after_manual_scroll(self) -> None:
660
+ """Restore cyclic GC after manual scrolling has settled."""
661
+ self._scroll_controller.resume_gc_after_manual_scroll()
662
+
663
+ def _start_final_response_autoscroll(self) -> None:
664
+ """Resume bottom-follow once when the agent starts its final answer."""
665
+ self._scroll_controller.on_final_response_started()
666
+
667
+ def watch_virtual_size(self, old: Size, new: Size) -> None:
668
+ """Post-layout hook: re-engage anchor, sync scrollbar, toggle spacer.
669
+
670
+ 1. Anchor re-engage on growth: ``add_user_message`` releases the
671
+ anchor (via ``scroll_visible(top=True)``) so the user's message
672
+ shows at viewport top while the response streams in below.
673
+ Once the response overflows, resume following the bottom.
674
+ 2. Drive ``scroll_y`` through its own setter (not the compositor's
675
+ ``set_reactive`` pin) so ``watch_scroll_y`` → ``ScrollBar.position``
676
+ marks the scrollbar dirty on the same frame the content scrolls.
677
+ Otherwise the thumb freezes for one frame because Textual's pin
678
+ writes ``vertical_scrollbar._reactive_position`` directly.
679
+ 3. Bottom spacer (``1fr``, min-height 1) — hide when content fills
680
+ the viewport (no stray 1-cell tail), re-show when it shrinks
681
+ back below (e.g. ``ToolGroup`` auto-collapse) so the next
682
+ growth phase has a flexible tail again.
683
+
684
+ The negative-pin defense lives in :meth:`arrange` — it must run
685
+ before the compositor reads the anchor flag.
686
+ """
687
+ self._scroll_controller.watch_virtual_size(old, new)
688
+
689
+ async def _dismiss_welcome(self) -> None:
690
+ """Remove the welcome widget on first interaction."""
691
+ await self._chrome.dismiss_welcome()
692
+
693
+ @property
694
+ def session_id(self) -> str:
695
+ """Current session id, or empty string before ``set_session_id`` is called."""
696
+ return self._session_id
697
+
698
+ def set_session_id(self, session_id: str) -> None:
699
+ """Display the current session ID in the panel border title."""
700
+ self.session_id_value = session_id
701
+
702
+ def set_session_title(self, title: str) -> None:
703
+ """Display the session title next to the session ID in the border title."""
704
+ self.session_title_value = title
705
+
706
+ def set_workspace_cwd(self, cwd: str) -> None:
707
+ """Display the current workspace cwd in the panel border subtitle."""
708
+ self.workspace_cwd = cwd
709
+
710
+ def set_workspace_branch(self, branch: str) -> None:
711
+ """Display the current workspace git branch in the panel border subtitle."""
712
+ self.workspace_branch = branch
713
+
714
+ class TitleClicked(Message):
715
+ """Posted when the user clicks the border title (session id / title)."""
716
+
717
+ class WorkingDirClicked(Message):
718
+ """Posted when the user clicks the border subtitle (working directory)."""
719
+
720
+ def on_click(self, event: Click) -> None:
721
+ """Click on border title opens the title editor; subtitle opens dir picker."""
722
+ self._chrome.handle_click(event)
723
+
724
+ def on_scroll_to_bottom_requested(self, event: ScrollToBottomRequested) -> None:
725
+ """Handle the chrome bottom-jump affordance."""
726
+ event.stop()
727
+ self.jump_to_bottom()
728
+
729
+ async def mount_transcript_widget(self, widget: Widget) -> bool:
730
+ """Mount a transcript widget through the migration mount port."""
731
+ await self.mount(widget)
732
+ return (
733
+ widget.is_attached
734
+ and not widget._closing
735
+ and not widget._pruning
736
+ and not self._closing
737
+ and not self._pruning
738
+ )
739
+
740
+ async def mount_transcript_widgets(self, widgets: list[Widget]) -> None:
741
+ """Mount replayed entries, newest first, and return once the view is filled.
742
+
743
+ Entries beyond the first few viewports are prepended above by a
744
+ panel-owned task after this returns (see ``replay_mount``);
745
+ :meth:`wait_replay_complete` waits for them.
746
+ """
747
+ await self._replay_mount.mount(widgets)
748
+
749
+ @property
750
+ def replay_in_progress(self) -> bool:
751
+ """Whether older replayed entries are still being prepended."""
752
+ return self._replay_mount.in_progress
753
+
754
+ async def wait_replay_complete(self) -> None:
755
+ """Wait until every replayed entry is mounted (or the prepend stopped)."""
756
+ await self._replay_mount.wait_complete()
757
+
758
+ async def mount_replay_batch(self, batch: list[Widget], *, before: Widget | None) -> None:
759
+ """Mount one bounded replay batch, hatched until its descendants are ready."""
760
+ for widget in batch:
761
+ widget.add_class(_REPLAY_PLACEHOLDER_CLASS, update=False)
762
+ precompose_tree(batch)
763
+ try:
764
+ # An explicit ``before=None`` would skip ``mount``'s default place above the bottom spacer.
765
+ if before is None:
766
+ await self.mount(*batch)
767
+ else:
768
+ await self.mount(*batch, before=before)
769
+ # AwaitMount only covers the roots passed to ``mount``. The
770
+ # precomposed descendants run their own async Mount handlers;
771
+ # in particular, VirtualizedMarkdown parses and lays out its
772
+ # document there. Keep the hatch visible and progress at the
773
+ # prior batch boundary until every descendant is actually
774
+ # ready to paint.
775
+ descendants = [descendant for widget in batch for descendant in widget.walk_children()]
776
+ for descendant in descendants:
777
+ await descendant._mounted_event.wait()
778
+ finally:
779
+ attached: list[Widget] = []
780
+ for widget in batch:
781
+ widget.remove_class(_REPLAY_PLACEHOLDER_CLASS, update=False)
782
+ if widget.is_attached:
783
+ attached.append(widget)
784
+ if attached:
785
+ # Re-evaluate only these batch roots. Hatch is repaint-only,
786
+ # so this never invalidates MainScreen's transcript layout.
787
+ self.app.stylesheet.update_nodes(attached)
788
+
789
+ def transcript_entry_after(self, widget: Widget) -> Widget | None:
790
+ """Return the oldest mounted entry stamped after ``widget``: where ``widget`` mounts above.
791
+
792
+ Entries mounted before a replay (a restore warning) are stamped before
793
+ every replayed entry, so older history lands below them.
794
+ """
795
+ seq = self._transcript_seqs[widget]
796
+ for child in self.children:
797
+ child_seq = self._transcript_seqs.get(child)
798
+ if child_seq is not None and child_seq > seq and not self._chrome.is_infrastructure(child):
799
+ return child
800
+ return None
801
+
802
+ def can_mount_replay(self) -> bool:
803
+ """Whether a deferred replay batch may still mount into this panel."""
804
+ return (
805
+ self.is_attached
806
+ and not self._closing
807
+ and not self._pruning
808
+ and self.app.is_running
809
+ and not self.app._closing
810
+ )
811
+
812
+ def report_replay_progress(self, current: int, total: int) -> None:
813
+ """Forward replay mount progress to the screen-owned sink."""
814
+ self._report_replay_progress(current, total)
815
+
816
+ def begin_view_hold(self) -> None:
817
+ self._scroll_controller.begin_view_hold()
818
+
819
+ def end_view_hold(self) -> None:
820
+ self._scroll_controller.end_view_hold()
821
+
822
+ def set_replay_progress_callback(self, callback: Callable[[int, int], None] | None) -> None:
823
+ """Set the screen-owned sink for persisted replay progress."""
824
+ self._replay_progress_callback = callback
825
+
826
+ def _report_replay_progress(self, current: int, total: int) -> None:
827
+ callback = self._replay_progress_callback
828
+ if callback is not None:
829
+ callback(current, total)
830
+
831
+ async def remove_transcript_widget(self, widget: Widget) -> None:
832
+ """Remove a transcript widget if it is still mounted on this panel."""
833
+ if widget.parent is self:
834
+ await remove_shielded(widget)
835
+
836
+ async def dismiss_welcome_for_content(self) -> None:
837
+ """Dismiss the welcome widget before transcript content is mounted."""
838
+ await self._dismiss_welcome()
839
+
840
+ def on_live_user_turn_started(self) -> None:
841
+ """Reset live-turn scroll gates without mounting or cleanup side effects."""
842
+ self._scroll_controller.on_user_turn_started()
843
+
844
+ def on_replay_started(self) -> None:
845
+ """Reset replay scroll gates after welcome dismissal."""
846
+ self._scroll_controller.on_replay_started()
847
+
848
+ def scroll_user_message_to_top(self, widget: Widget) -> None:
849
+ """Scroll a newly mounted user message to the viewport top."""
850
+ self._scroll_controller.scroll_user_message_to_top(widget)
851
+
852
+ def on_final_response_started(self) -> None:
853
+ """Run the final-response one-shot autoscroll policy."""
854
+ self._scroll_controller.on_final_response_started()
855
+
856
+ def schedule_anchor_sync(self) -> None:
857
+ """Schedule a coalesced bottom-anchor sync."""
858
+ self._scroll_controller.schedule_anchor_sync()
859
+
860
+ def on_status_message_mounted(self) -> None:
861
+ """Force status messages to the bottom, matching current status rendering."""
862
+ self._scroll_controller.on_status_message_mounted()
863
+
864
+ def scroll_inline_prompt_to_top_after_refresh(self, widget: Widget) -> None:
865
+ """Schedule inline ask_user prompt scroll-to-top after refresh."""
866
+ self._scroll_controller.scroll_inline_prompt_to_top_after_refresh(widget)
867
+
868
+ def on_inline_prompt_resized(self, group: ToolGroup | None) -> None:
869
+ """Relayout once after a live inline ask_user control changes size."""
870
+ _ = group
871
+ self.refresh(layout=True)
872
+ self._schedule_anchor_sync()
873
+
874
+ def after_replay_history_mounted(self) -> None:
875
+ """Schedule the final replay scroll-to-bottom call."""
876
+ self._scroll_controller.after_replay_history_mounted()
877
+
878
+ async def remove_trailing_status(self) -> None:
879
+ """StatusCleanupPort wrapper for trailing status removal."""
880
+ await self._remove_trailing_status()
881
+
882
+ def failed_turn_status(self) -> Widget | None:
883
+ """Find a failure eligible for replacement by a fresh user message."""
884
+ return self._find_trailing_status(allow_retry_notes=False)
885
+
886
+ def preceding_content(self, widget: Widget) -> Widget | None:
887
+ """Find the strict predecessor of a status, independent of later notices."""
888
+ previous = None
889
+ for child in self.children:
890
+ if child is widget:
891
+ return previous
892
+ if not self._chrome.is_infrastructure(child):
893
+ previous = child
894
+ return None
895
+
896
+ def mounted_tool_groups(self) -> list[ToolGroup]:
897
+ """Return mounted tool groups in Textual query order."""
898
+ return list(self.query(ToolGroup))
899
+
900
+ def mounted_agent_messages(self) -> list[AgentMessage]:
901
+ """Return mounted agent messages in Textual query order."""
902
+ return list(self.query(AgentMessage))
903
+
904
+ def mounted_user_messages(self) -> list[UserMessage]:
905
+ """Return mounted user messages in Textual query order."""
906
+ return list(self.query(UserMessage))
907
+
908
+ def direct_children(self) -> list[Widget]:
909
+ """Return a snapshot of direct children."""
910
+ return list(self.children)
911
+
912
+ def transcript_content_children(self) -> list[Widget]:
913
+ """Return direct children excluding persistent chat chrome infrastructure."""
914
+ return [child for child in self.children if not self._chrome.is_infrastructure(child)]
915
+
916
+ def pending_transcript_widgets(self) -> list[Widget]:
917
+ """Return replayed entries still waiting to be prepended, in document order."""
918
+ return self._replay_mount.pending_widgets()
919
+
920
+ def set_border_title(self, title: Text | None) -> None:
921
+ self.border_title = title
922
+
923
+ def set_border_subtitle(self, subtitle: Text | None) -> None:
924
+ self.border_subtitle = subtitle
925
+
926
+ def border_region(self) -> Region:
927
+ return self.region
928
+
929
+ def has_border_subtitle(self) -> bool:
930
+ return bool(self.border_subtitle)
931
+
932
+ def is_ask_user_interactive_target(self, target: object) -> bool:
933
+ if not isinstance(target, Widget):
934
+ return False
935
+ return any(ancestor.has_class(ASK_USER_INTERACTIVE_CLASS) for ancestor in target.ancestors_with_self)
936
+
937
+ def post_title_clicked(self) -> None:
938
+ self.post_message(self.TitleClicked())
939
+
940
+ def post_working_dir_clicked(self) -> None:
941
+ self.post_message(self.WorkingDirClicked())
942
+
943
+ def call_super_arrange(self, size: Size, *, optimal: bool = False) -> DockArrangeResult:
944
+ return super().arrange(size, optimal=optimal)
945
+
946
+ def call_super_scroll_to_region(self, region: Region, **kwargs: Any) -> Offset:
947
+ return super().scroll_to_region(region, **kwargs)
948
+
949
+ def call_super_size_updated(
950
+ self,
951
+ size: Size,
952
+ virtual_size: Size,
953
+ container_size: Size,
954
+ layout: bool,
955
+ ) -> bool:
956
+ return super()._size_updated(size, virtual_size, container_size, layout)
957
+
958
+ def call_super_watch_scroll_y(self, old_value: float, new_value: float) -> None:
959
+ super().watch_scroll_y(old_value, new_value)
960
+
961
+ def is_anchored(self) -> bool:
962
+ return self._anchored
963
+
964
+ def is_anchor_released(self) -> bool:
965
+ return self._anchor_released
966
+
967
+ def set_anchor_released(self, released: bool) -> None:
968
+ self._anchor_released = released
969
+
970
+ def set_scroll_y_reactive(self, y: float) -> None:
971
+ self.set_reactive(Widget.scroll_y, y)
972
+
973
+ def set_scroll_target_y_reactive(self, y: float) -> None:
974
+ self.set_reactive(Widget.scroll_target_y, y)
975
+
976
+ def set_scroll_target_y(self, y: float) -> None:
977
+ self.scroll_target_y = y
978
+
979
+ def set_vertical_scrollbar_position(self, y: float) -> None:
980
+ # Like the compositor's own anchor pin: move the thumb without watchers.
981
+ self.vertical_scrollbar.set_reactive(ScrollBar.position, y)
982
+
983
+ def get_container_size(self) -> Size:
984
+ return self._container_size
985
+
986
+ def set_container_size(self, size: Size) -> None:
987
+ self._container_size = size
988
+
989
+ def apply_held_shrink_size_update(self, size: Size, container_size: Size) -> bool:
990
+ changed = False
991
+ if self._size != size:
992
+ self._set_dirty()
993
+ self._size = size
994
+ changed = True
995
+ if self._container_size != container_size:
996
+ self._container_size = container_size
997
+ changed = True
998
+ if changed:
999
+ self._layout_cache.clear()
1000
+ return changed
1001
+
1002
+ def place_fixed_scroll_button(self, result: DockArrangeResult, size: Size) -> None:
1003
+ self._place_scroll_to_bottom_button(result, size)
1004
+
1005
+ def bottom_spacer(self) -> Widget | None:
1006
+ return self._bottom_spacer
1007
+
1008
+ def scroll_to_bottom_button(self) -> Widget | None:
1009
+ return self._scroll_to_bottom_button
1010
+
1011
+ def set_screen_scroll_required_and_check_idle(self) -> None:
1012
+ self.screen._scroll_required = True
1013
+ self.screen.check_idle()
1014
+
1015
+ def scroll_to_widget_top(self, widget: Widget) -> None:
1016
+ self.scroll_to_widget(widget, animate=False, top=True, immediate=True)
1017
+
1018
+ async def clear(self) -> None:
1019
+ """Remove all children and show a fresh welcome widget."""
1020
+ # Backend event handlers clear the panel inside the publishing task; a
1021
+ # cancelled clear still finishes, so the panel is never left half-built.
1022
+ await finish_shielded(self._clear_and_rebuild())
1023
+
1024
+ async def _clear_and_rebuild(self) -> None:
1025
+ # A prepend ends at a batch boundary first: never remove children
1026
+ # around a mount that is still registering.
1027
+ await self._replay_mount.stop()
1028
+ self._scroll_controller.reset_for_clear()
1029
+ if self.is_attached:
1030
+ self.screen.clear_selection()
1031
+ cancel_textual_mouse_chain(self.screen)
1032
+ # Clear the spacer reference BEFORE awaiting ``remove_children`` —
1033
+ # the await yields to the event loop, and any mount scheduled by
1034
+ # another coroutine during that window would otherwise see the
1035
+ # now-detached spacer via the overridden ``mount()`` and crash with
1036
+ # ``MountError: Unable to find relative location``. The override
1037
+ # also guards on ``parent is self`` as a belt-and-braces check.
1038
+ self._chrome.detach_infrastructure_refs()
1039
+ await self.remove_children()
1040
+ self._current_tool_group = None
1041
+ self._agent_running = False
1042
+ self._live_renderer.reset_for_clear()
1043
+ self._tool_registry.reset_for_clear()
1044
+ self._toc.clear()
1045
+
1046
+ welcome, bottom_spacer, scroll_button = self._chrome.rebuild_widgets()
1047
+ await self.mount(welcome)
1048
+ await self.mount(bottom_spacer)
1049
+ await self.mount(scroll_button, after=bottom_spacer)
1050
+
1051
+ def update_welcome(self, profile: str = "", cwd: str = "") -> None:
1052
+ """Keep welcome context in panel state, including while the widget is absent."""
1053
+ if profile:
1054
+ self.profile_name = profile
1055
+ if cwd:
1056
+ self.workspace_cwd = cwd
1057
+ self._chrome.update_welcome(profile=self.profile_name, cwd=self.workspace_cwd)
1058
+
1059
+ def toggle_fold_all(self) -> bool:
1060
+ """Collapse or expand all tool groups.
1061
+
1062
+ If any are expanded, collapses all. If all are already collapsed,
1063
+ expands all. Returns the new collapsed state.
1064
+ """
1065
+ tool_groups = [group for group in self.query(ToolGroup) if not group.collapse_locked]
1066
+ # Groups still waiting to be prepended take the choice as they mount.
1067
+ pending_collapsed = self._replay_mount.pending_tool_groups_collapsed()
1068
+
1069
+ if not tool_groups and pending_collapsed is None:
1070
+ return False
1071
+
1072
+ any_expanded = any(not group.collapsed for group in tool_groups) or pending_collapsed is False
1073
+ for group in tool_groups:
1074
+ group.collapsed = any_expanded
1075
+ self._replay_mount.set_fold_state(any_expanded)
1076
+ return any_expanded
1077
+
1078
+ def get_agent_responses(self) -> list[tuple[str, str]]:
1079
+ """Return (profile_name, raw_text) of all agent messages in mount order."""
1080
+ return self._transcript_reader.get_agent_responses()
1081
+
1082
+ def get_user_messages(self) -> list[tuple[str, str]]:
1083
+ """Return (role_label, raw_text) of all user messages in mount order."""
1084
+ return self._transcript_reader.get_user_messages()
1085
+
1086
+ def get_all_messages(self) -> list[tuple[str, str]]:
1087
+ """Return (role_label, raw_text) of all user+agent messages in mount order."""
1088
+ return self._transcript_reader.get_all_messages()
1089
+
1090
+ # --- helpers ---
1091
+
1092
+ def _end_tool_group(self) -> None:
1093
+ """Close current tool group so a new one starts on next tool call."""
1094
+ self._tool_registry.end_group()
1095
+
1096
+ # --- public API ---
1097
+
1098
+ def mark_turn_range_compressed(self, turn_range: tuple[int, int]) -> bool:
1099
+ """Mark live TOC entries in the backend turn range as compressed."""
1100
+ return self._toc.mark_turn_range_compressed(turn_range)
1101
+
1102
+ async def add_user_message(
1103
+ self,
1104
+ text: str,
1105
+ *,
1106
+ is_injection: bool = False,
1107
+ created_at: Any = None,
1108
+ contents: list[Any] | None = None,
1109
+ ) -> None:
1110
+ """Display a user message."""
1111
+ await self._live_renderer.add_user_message(
1112
+ text,
1113
+ is_injection=is_injection,
1114
+ created_at=created_at,
1115
+ contents=contents,
1116
+ )
1117
+
1118
+ async def _remove_failed_turn(self) -> None:
1119
+ """Remove a trailing failed turn (Error/Interrupted + its preceding UserMessage)."""
1120
+ await self._live_renderer._remove_failed_turn()
1121
+
1122
+ async def add_agent_message(
1123
+ self,
1124
+ text: str,
1125
+ is_final: bool = True,
1126
+ *,
1127
+ is_intermediate: bool = False,
1128
+ structured_output_completed: bool = False,
1129
+ presentation: ProvisionalPresentation | None = None,
1130
+ created_at: Any = None,
1131
+ ) -> None:
1132
+ """Display or update an agent message (supports streaming).
1133
+
1134
+ Args:
1135
+ text: The message text.
1136
+ is_final: Whether this is the last message of the agent turn.
1137
+ is_intermediate: If ``True``, this is intermediate text returned
1138
+ alongside tool calls. Non-empty prose is rendered as a
1139
+ complete message and closes the current tool group (the next
1140
+ tool batch opens a new one); empty or think-only text is
1141
+ ignored and leaves the open group untouched.
1142
+ """
1143
+ await self._live_renderer.add_agent_message(
1144
+ text,
1145
+ is_final=is_final,
1146
+ is_intermediate=is_intermediate,
1147
+ structured_output_completed=structured_output_completed,
1148
+ presentation=presentation,
1149
+ created_at=created_at,
1150
+ )
1151
+
1152
+ async def accept_presentation_attempt(self, attempt_id: str, segment_ids: tuple[str, ...]) -> None:
1153
+ """Commit the canonical provisional messages for one response attempt."""
1154
+ await self._live_renderer.accept_presentation_attempt(attempt_id, segment_ids)
1155
+
1156
+ async def reject_presentation_attempt(self, attempt_id: str) -> None:
1157
+ """Retract provisional messages for one rejected response attempt."""
1158
+ await self._live_renderer.reject_presentation_attempt(attempt_id)
1159
+
1160
+ def _schedule_anchor_sync(self) -> None:
1161
+ """Sync scroll position to the bottom after the next refresh.
1162
+
1163
+ Used by the streaming agent-message path, where
1164
+ :class:`VirtualizedMarkdown`'s async ``update()`` means the
1165
+ ChatPanel's ``virtual_size`` changes in a later frame — often after
1166
+ the tick that triggered it. ``watch_virtual_size`` may still fire,
1167
+ but rapid coalesced ticks can leave the scrollbar out of sync with
1168
+ the final content height. Running ``_sync_anchor_bottom`` via
1169
+ ``call_after_refresh`` guarantees it sees the settled
1170
+ ``max_scroll_y`` and pushes it through the ``scroll_y`` setter
1171
+ (driving ``watch_scroll_y`` → ``ScrollBar.position`` setter →
1172
+ scrollbar repaint).
1173
+ """
1174
+ self._scroll_controller.schedule_anchor_sync()
1175
+
1176
+ def _sync_anchor_bottom(self) -> None:
1177
+ """Push ``scroll_y`` to the current ``max_scroll_y`` via setter.
1178
+
1179
+ Re-checks anchor state in case the user scrolled away between the
1180
+ schedule and this callback.
1181
+ """
1182
+ self._scroll_controller.sync_anchor_bottom()
1183
+
1184
+ async def add_tool_start(
1185
+ self,
1186
+ call_id: str,
1187
+ tool_name: str,
1188
+ tool_kind: str,
1189
+ args_summary: str = "",
1190
+ args: dict[str, Any] | None = None,
1191
+ *,
1192
+ provider_hosted: bool = False,
1193
+ hosted_family: str = "",
1194
+ provider: str = "",
1195
+ provider_item_type: str = "",
1196
+ provider_status: str = "",
1197
+ provider_call_id: str = "",
1198
+ canonical_status: str = "running",
1199
+ ) -> None:
1200
+ """Add a running tool call (creates tool group if needed)."""
1201
+ await self._tool_registry.add_tool_start(
1202
+ call_id,
1203
+ tool_name,
1204
+ tool_kind,
1205
+ args_summary,
1206
+ args=args,
1207
+ provider_hosted=provider_hosted,
1208
+ hosted_family=hosted_family,
1209
+ provider=provider,
1210
+ provider_item_type=provider_item_type,
1211
+ provider_status=provider_status,
1212
+ provider_call_id=provider_call_id,
1213
+ canonical_status=canonical_status,
1214
+ )
1215
+
1216
+ async def add_tool_result(
1217
+ self,
1218
+ call_id: str,
1219
+ tool_name: str,
1220
+ result: str,
1221
+ duration_ms: int = 0,
1222
+ *,
1223
+ image_contents: list[Any] | None = None,
1224
+ file_snapshot: FileSnapshotPayload | None = None,
1225
+ approval: str | None = None,
1226
+ metadata: dict[str, Any] | None = None,
1227
+ artifacts: list[dict[str, Any]] | None = None,
1228
+ provider_status: str = "",
1229
+ canonical_status: str = "completed",
1230
+ ) -> None:
1231
+ """Update a tool call as complete."""
1232
+ await self._tool_registry.add_tool_result(
1233
+ call_id,
1234
+ tool_name,
1235
+ result,
1236
+ duration_ms,
1237
+ image_contents=image_contents,
1238
+ file_snapshot=file_snapshot,
1239
+ approval=approval,
1240
+ metadata=metadata,
1241
+ artifacts=artifacts,
1242
+ provider_status=provider_status,
1243
+ canonical_status=canonical_status,
1244
+ )
1245
+
1246
+ def update_tool_progress(
1247
+ self,
1248
+ call_id: str,
1249
+ lines: list[str],
1250
+ *,
1251
+ image_contents: list[Any] | None = None,
1252
+ snapshot_metadata: dict[str, Any] | None = None,
1253
+ provider_status: str = "",
1254
+ ) -> None:
1255
+ """Forward streaming progress lines to a running tool widget."""
1256
+ self._tool_registry.update_tool_progress(
1257
+ call_id,
1258
+ lines,
1259
+ image_contents=image_contents,
1260
+ snapshot_metadata=snapshot_metadata,
1261
+ provider_status=provider_status,
1262
+ )
1263
+
1264
+ def update_tool_args(self, call_id: str, args: dict[str, Any]) -> None:
1265
+ """Refresh a running tool widget after approval edits its arguments."""
1266
+ self._tool_registry.update_tool_args(call_id, args)
1267
+
1268
+ def update_tool_status(
1269
+ self,
1270
+ call_id: str,
1271
+ status: str,
1272
+ *,
1273
+ provider_status: str = "",
1274
+ metadata: dict[str, Any] | None = None,
1275
+ ) -> None:
1276
+ """Apply a canonical provider-hosted status update."""
1277
+ self._tool_registry.update_tool_status(
1278
+ call_id,
1279
+ status,
1280
+ provider_status=provider_status,
1281
+ metadata=metadata,
1282
+ )
1283
+
1284
+ def show_ask_user_inline(
1285
+ self,
1286
+ call_id: str,
1287
+ request_id: str,
1288
+ questions: tuple[AskUserQuestion, ...],
1289
+ *,
1290
+ draft: PromptDraft | None = None,
1291
+ ) -> bool:
1292
+ """Show a live ask_user response form inside the matching tool renderer."""
1293
+ return self._tool_registry.show_ask_user_inline(call_id, request_id, questions, draft=draft)
1294
+
1295
+ def on_ask_user_inline_resized(self, event: AskUserInlineResized) -> None:
1296
+ """Relayout the transcript when live inline ask_user controls grow."""
1297
+ self._tool_registry.handle_ask_user_inline_resized(event.call_id)
1298
+
1299
+ def is_tool_running(self, call_id: str) -> bool:
1300
+ """Return whether the tool record for ``call_id`` is still running."""
1301
+ return self._tool_registry.is_tool_running(call_id)
1302
+
1303
+ def clear_ask_user_inline_prompts(self) -> None:
1304
+ """Clear live inline ask_user prompts from every known tool group."""
1305
+ self._tool_registry.clear_ask_user_inline_prompts()
1306
+
1307
+ def clear_ask_user_inline(self, call_id: str, request_id: str) -> bool:
1308
+ """Clear one request-matched inline ask_user prompt."""
1309
+ return self._tool_registry.clear_ask_user_inline(call_id, request_id)
1310
+
1311
+ def link_sub_agent_invocation(
1312
+ self,
1313
+ parent_call_id: str,
1314
+ invocation_id: str,
1315
+ agent_name: str,
1316
+ sub_agent_log_file: str = "",
1317
+ *,
1318
+ tool_name: str = "",
1319
+ ) -> None:
1320
+ """Bind an ``invocation_id`` to the widget mounted for ``parent_call_id``."""
1321
+ self._tool_registry.link_sub_agent_invocation(
1322
+ parent_call_id,
1323
+ invocation_id,
1324
+ agent_name,
1325
+ sub_agent_log_file,
1326
+ tool_name=tool_name,
1327
+ )
1328
+
1329
+ def add_sub_agent_message(
1330
+ self,
1331
+ agent_name: str,
1332
+ invocation_id: str,
1333
+ text: str,
1334
+ *,
1335
+ presentation: ProvisionalPresentation | None = None,
1336
+ ) -> None:
1337
+ """Route one intermediate assistant message to a nested transcript."""
1338
+ self._tool_registry.add_sub_agent_message(
1339
+ agent_name,
1340
+ invocation_id,
1341
+ text,
1342
+ presentation=presentation,
1343
+ )
1344
+
1345
+ def accept_sub_agent_presentation(
1346
+ self,
1347
+ invocation_id: str,
1348
+ attempt_id: str,
1349
+ segment_ids: tuple[str, ...],
1350
+ ) -> None:
1351
+ """Commit provisional assistant messages in a nested transcript."""
1352
+ self._tool_registry.accept_sub_agent_presentation(invocation_id, attempt_id, segment_ids)
1353
+
1354
+ def reject_sub_agent_presentation(self, invocation_id: str, attempt_id: str) -> None:
1355
+ """Retract provisional assistant messages in a nested transcript."""
1356
+ self._tool_registry.reject_sub_agent_presentation(invocation_id, attempt_id)
1357
+
1358
+ async def add_sub_agent_tool_start(
1359
+ self,
1360
+ agent_name: str,
1361
+ invocation_id: str,
1362
+ tool_name: str,
1363
+ args: dict[str, Any],
1364
+ call_id: str,
1365
+ *,
1366
+ tool_kind: str = "",
1367
+ provider_hosted: bool = False,
1368
+ hosted_family: str = "",
1369
+ provider: str = "",
1370
+ provider_item_type: str = "",
1371
+ provider_status: str = "",
1372
+ provider_call_id: str = "",
1373
+ ) -> None:
1374
+ """Route an inner tool call start to the correct SubAgentToolCall widget.
1375
+
1376
+ :class:`InvocationStarted` establishes the route first, using
1377
+ exact parent ownership or the internal-tool-name FIFO fallback in
1378
+ :meth:`link_sub_agent_invocation`. Subsequent events resolve strictly
1379
+ by ``invocation_id``; display names never participate in identity.
1380
+ """
1381
+ await self._tool_registry.add_sub_agent_tool_start(
1382
+ agent_name,
1383
+ invocation_id,
1384
+ tool_name,
1385
+ args,
1386
+ call_id,
1387
+ tool_kind=tool_kind,
1388
+ provider_hosted=provider_hosted,
1389
+ hosted_family=hosted_family,
1390
+ provider=provider,
1391
+ provider_item_type=provider_item_type,
1392
+ provider_status=provider_status,
1393
+ provider_call_id=provider_call_id,
1394
+ )
1395
+
1396
+ def update_sub_agent_tool_args(self, invocation_id: str, call_id: str, args: dict[str, Any]) -> None:
1397
+ """Update one nested sub-agent tool's arguments."""
1398
+ self._tool_registry.update_sub_agent_tool_args(invocation_id, call_id, args)
1399
+
1400
+ def update_sub_agent_tool_status(
1401
+ self,
1402
+ invocation_id: str,
1403
+ call_id: str,
1404
+ status: str,
1405
+ *,
1406
+ metadata: dict[str, Any] | None = None,
1407
+ provider_status: str = "",
1408
+ ) -> None:
1409
+ """Apply one nested sub-agent tool lifecycle status."""
1410
+ self._tool_registry.update_sub_agent_tool_status(
1411
+ invocation_id,
1412
+ call_id,
1413
+ status,
1414
+ metadata=metadata,
1415
+ provider_status=provider_status,
1416
+ )
1417
+
1418
+ def update_sub_agent_tool_progress(
1419
+ self,
1420
+ invocation_id: str,
1421
+ call_id: str,
1422
+ lines: list[str],
1423
+ *,
1424
+ image_contents: list[Any] | None = None,
1425
+ snapshot_metadata: dict[str, Any] | None = None,
1426
+ provider_status: str = "",
1427
+ ) -> None:
1428
+ """Update one nested sub-agent tool progress snapshot."""
1429
+ self._tool_registry.update_sub_agent_tool_progress(
1430
+ invocation_id,
1431
+ call_id,
1432
+ lines,
1433
+ image_contents=image_contents,
1434
+ snapshot_metadata=snapshot_metadata,
1435
+ provider_status=provider_status,
1436
+ )
1437
+
1438
+ def complete_sub_agent_tool(
1439
+ self,
1440
+ agent_name: str,
1441
+ invocation_id: str,
1442
+ call_id: str,
1443
+ result: str,
1444
+ duration_ms: int,
1445
+ image_contents: list[Any] | None = None,
1446
+ artifacts: list[dict[str, Any]] | None = None,
1447
+ approval: str | None = None,
1448
+ metadata: dict[str, Any] | None = None,
1449
+ provider_status: str = "",
1450
+ canonical_status: str = "completed",
1451
+ ) -> None:
1452
+ """Route an inner tool call result to the correct SubAgentToolCall widget."""
1453
+ self._tool_registry.complete_sub_agent_tool(
1454
+ agent_name,
1455
+ invocation_id,
1456
+ call_id,
1457
+ result,
1458
+ duration_ms,
1459
+ image_contents=image_contents,
1460
+ artifacts=artifacts,
1461
+ approval=approval,
1462
+ metadata=metadata,
1463
+ provider_status=provider_status,
1464
+ canonical_status=canonical_status,
1465
+ )
1466
+
1467
+ def update_sub_agent_progress(
1468
+ self,
1469
+ invocation_id: str,
1470
+ tool_call_count: int,
1471
+ total_tokens: int,
1472
+ total_usage_tokens: int = 0,
1473
+ usage_unreported_attempts: int = 0,
1474
+ ) -> None:
1475
+ """Update progress stats on a running SubAgentToolCall widget."""
1476
+ self._tool_registry.update_sub_agent_progress(
1477
+ invocation_id,
1478
+ tool_call_count,
1479
+ total_tokens,
1480
+ total_usage_tokens,
1481
+ usage_unreported_attempts,
1482
+ )
1483
+
1484
+ def sub_agent_retry_attempt(
1485
+ self,
1486
+ invocation_id: str,
1487
+ message: str,
1488
+ attempt: int,
1489
+ max_attempts: int,
1490
+ delay_seconds: int,
1491
+ ) -> None:
1492
+ """Dispatch a sub-agent auto-retry banner to the correct card."""
1493
+ self._tool_registry.sub_agent_retry_attempt(invocation_id, message, attempt, max_attempts, delay_seconds)
1494
+
1495
+ def sub_agent_paused(
1496
+ self,
1497
+ invocation_id: str,
1498
+ reason: str,
1499
+ last_error: str,
1500
+ retry_attempts: int,
1501
+ diagnostic_path: str | None = None,
1502
+ last_error_display: str | None = None,
1503
+ ) -> None:
1504
+ """Dispatch a pause transition to the correct card."""
1505
+ self._tool_registry.sub_agent_paused(
1506
+ invocation_id,
1507
+ reason,
1508
+ last_error,
1509
+ retry_attempts,
1510
+ diagnostic_path,
1511
+ last_error_display,
1512
+ )
1513
+
1514
+ def sub_agent_resumed_after_pause(self, invocation_id: str) -> None:
1515
+ """Dispatch a resume-after-pause transition to the correct card."""
1516
+ self._tool_registry.sub_agent_resumed_after_pause(invocation_id)
1517
+
1518
+ def sub_agent_cascade_aborted(self, invocation_id: str) -> None:
1519
+ """Dispatch a cascade-abort transition (from global interrupt)."""
1520
+ self._tool_registry.sub_agent_cascade_aborted(invocation_id)
1521
+
1522
+ def sub_agent_aborted(self, invocation_id: str, last_error: str) -> None:
1523
+ """Dispatch a user-abort transition to the correct card.
1524
+
1525
+ Clears the paused banner as soon as ``InvocationAborted`` arrives,
1526
+ independent of the trailing parent ``ToolCallResult``. The card
1527
+ ends in the ``-aborted`` style (distinct from ``-error`` so the
1528
+ user can tell their own Abort click from a sub-agent crash).
1529
+ """
1530
+ self._tool_registry.sub_agent_aborted(invocation_id, last_error)
1531
+
1532
+ async def add_context_fold(
1533
+ self,
1534
+ compressed_context_id: str,
1535
+ summary: str,
1536
+ freed_messages: int = 0,
1537
+ turn_range: tuple[int, int] = (0, 0),
1538
+ ) -> None:
1539
+ """Display a context compression event."""
1540
+ await self._live_renderer.add_context_fold(compressed_context_id, summary, freed_messages, turn_range)
1541
+
1542
+ async def add_compaction_start(self, compaction_id: str) -> None:
1543
+ """Mount a live Phase-4 compaction card for the main agent."""
1544
+ await self._live_renderer.add_compaction_start(compaction_id)
1545
+
1546
+ def complete_compaction(
1547
+ self,
1548
+ compaction_id: str,
1549
+ *,
1550
+ outcome: str,
1551
+ duration_ms: int = 0,
1552
+ last_words: str = "",
1553
+ format_violation: str = "",
1554
+ failure_reason: str = "",
1555
+ ) -> None:
1556
+ """Finalize the matching live compaction card."""
1557
+ self._live_renderer.complete_compaction(
1558
+ compaction_id,
1559
+ outcome=outcome,
1560
+ duration_ms=duration_ms,
1561
+ last_words=last_words,
1562
+ format_violation=format_violation,
1563
+ failure_reason=failure_reason,
1564
+ )
1565
+
1566
+ def show_compaction_retry(self, message: str, attempt: int, max_attempts: int, delay_seconds: int) -> None:
1567
+ """Show a Phase-4 side-call retry notice inside the running compaction card."""
1568
+ self._live_renderer.show_compaction_retry(message, attempt, max_attempts, delay_seconds)
1569
+
1570
+ def add_sub_agent_compaction_start(self, agent_name: str, invocation_id: str, compaction_id: str) -> None:
1571
+ """Show a live compaction line on the matching sub-agent card."""
1572
+ self._tool_registry.add_sub_agent_compaction_start(agent_name, invocation_id, compaction_id)
1573
+
1574
+ def complete_sub_agent_compaction(
1575
+ self,
1576
+ invocation_id: str,
1577
+ compaction_id: str,
1578
+ *,
1579
+ outcome: str,
1580
+ duration_ms: int = 0,
1581
+ format_violation: str = "",
1582
+ failure_reason: str = "",
1583
+ ) -> None:
1584
+ """Flip the compaction line on the matching sub-agent card."""
1585
+ self._tool_registry.complete_sub_agent_compaction(
1586
+ invocation_id,
1587
+ compaction_id,
1588
+ outcome=outcome,
1589
+ duration_ms=duration_ms,
1590
+ format_violation=format_violation,
1591
+ failure_reason=failure_reason,
1592
+ )
1593
+
1594
+ def record_sub_agent_compaction_committed(self, invocation_id: str, compaction_id: str) -> None:
1595
+ """Bump the compaction counter on the matching sub-agent card."""
1596
+ self._tool_registry.record_sub_agent_compaction_committed(invocation_id, compaction_id)
1597
+
1598
+ async def add_system(self, text: str, *, key: str | None = None) -> None:
1599
+ """Display a system message.
1600
+
1601
+ If ``key`` is provided, the message will be tagged with this key so it
1602
+ can be updated or removed later (used for things like profile switch
1603
+ de-duplication).
1604
+ """
1605
+ await self._live_renderer.add_system(text, key=key)
1606
+
1607
+ async def update_system(self, key: str, new_text: str) -> None:
1608
+ """Update the first system message matching the given key (if any)."""
1609
+ await self._live_renderer.update_system(key, new_text)
1610
+
1611
+ async def remove_system(self, key: str) -> None:
1612
+ """Remove the first system message matching the given key (if any)."""
1613
+ await self._live_renderer.remove_system(key)
1614
+
1615
+ async def add_interrupted(self, reason: str = "Execution interrupted", source: str = "user") -> None:
1616
+ """Display an interruption notice, replacing any trailing error/interrupted widget."""
1617
+ await self._live_renderer.add_interrupted(reason, source)
1618
+
1619
+ async def add_error(self, text: str, *, action_label: str | None = "Retry") -> None:
1620
+ """Display an error message, replacing any trailing error/interrupted widget."""
1621
+ await self._live_renderer.add_error(text, action_label=action_label)
1622
+
1623
+ async def add_retry(self, message: str, attempt: int, max_attempts: int, delay_seconds: int) -> None:
1624
+ """Display a retry notice, replacing any trailing status widget."""
1625
+ await self._live_renderer.add_retry(message, attempt, max_attempts, delay_seconds)
1626
+
1627
+ async def prepare_retry(self) -> None:
1628
+ """Finalise pending widgets before a main-agent stream retry.
1629
+
1630
+ The backend's :class:`StreamRetryLoop` rolls ``chrys_history`` back
1631
+ to the pre-attempt snapshot before the next attempt, so every
1632
+ intermediate widget mounted during the failed run (assistant
1633
+ intermediate text, its tool group, any sub-agent cards still
1634
+ running inside it) is now disconnected from the authoritative
1635
+ history. Without cleanup, the next attempt's fresh intermediate
1636
+ text would create a NEW widget while ``_current_tool_group`` and
1637
+ friends still point at the OLD one — causing subsequent
1638
+ ``ToolCallStart`` events to mount sub-agent cards under the
1639
+ wrong assistant block.
1640
+
1641
+ This method:
1642
+
1643
+ 1. Marks any still-running tool cards in the open group as
1644
+ "cancelled" (the backend's stream cleanup hooks have already
1645
+ cancelled the underlying tool tasks; this is the matching
1646
+ visual terminal state for the old invocations).
1647
+ 2. Closes the open tool group and clears
1648
+ ``_current_tool_group``/``_unlinked_sub_agents`` so the next
1649
+ ``ToolCallStart`` creates a fresh group.
1650
+ 3. Clears ``_current_agent_msg`` so the next intermediate text
1651
+ mounts a new widget under which the next group will attach.
1652
+
1653
+ Completed tool cards from the failed attempt stay visible — they
1654
+ carry real results the user may want to inspect. The retry
1655
+ notice itself (``add_retry``) is mounted by the caller AFTER
1656
+ this cleanup so it sits between the old run's artefacts and the
1657
+ new run's output.
1658
+ """
1659
+ await self._live_renderer.prepare_retry()
1660
+
1661
+ def hide_trailing_status_action(self) -> None:
1662
+ """Hide the inline action on the current trailing error/interruption."""
1663
+ last = self._find_trailing_status()
1664
+ if isinstance(last, (ErrorMessage, InterruptedMessage)):
1665
+ last.hide_action()
1666
+
1667
+ def set_trailing_status_action_disabled(self, disabled: bool) -> None:
1668
+ """Keep the retry action visible but unavailable during admission."""
1669
+ last = self._find_trailing_status()
1670
+ if isinstance(last, (ErrorMessage, InterruptedMessage)):
1671
+ last.set_action_disabled(disabled)
1672
+
1673
+ async def _remove_trailing_status(self) -> None:
1674
+ """Remove the active status widget, allowing trailing retry notes."""
1675
+ status = self._find_trailing_status()
1676
+ if status is not None:
1677
+ await remove_shielded(status)
1678
+
1679
+ def _find_trailing_status(
1680
+ self, *, allow_retry_notes: bool = True
1681
+ ) -> ErrorMessage | InterruptedMessage | RetryMessage | None:
1682
+ """Find the active status past context notices and mid-turn retry notes.
1683
+
1684
+ Action updates and removal must agree on the same card. A new user
1685
+ turn or run output is a boundary; older status cards stay untouched.
1686
+ Fresh-submit cleanup also treats a pending retry note as newer input.
1687
+ """
1688
+ for child in reversed(list(self.children)):
1689
+ if self._chrome.is_infrastructure(child):
1690
+ continue
1691
+ if isinstance(child, (ErrorMessage, InterruptedMessage, RetryMessage)):
1692
+ return child
1693
+ if allow_retry_notes and isinstance(child, UserMessage) and child.is_injection:
1694
+ continue
1695
+ if isinstance(child, SystemMessage):
1696
+ continue
1697
+ return None
1698
+ return None
1699
+
1700
+ async def cancel_running_tools(self) -> None:
1701
+ """Cancel all in-progress tool calls."""
1702
+ await self._live_renderer.cancel_running_tools()
1703
+
1704
+ async def replay_history(
1705
+ self,
1706
+ messages: list[dict[str, Any]],
1707
+ initial_profile: str = "",
1708
+ file_snapshots: dict[str, list[FileSnapshotPayload]] | None = None,
1709
+ compressed_blocks: dict[str, CompressedBlock] | None = None,
1710
+ ) -> None:
1711
+ """Replay saved session messages into the chat panel.
1712
+
1713
+ Reconstructs user messages, agent text responses, and tool call groups
1714
+ from the deserialized message history (list of dicts with 'role' and
1715
+ 'contents'). Intermediate text (agent text returned alongside tool
1716
+ calls) is read from each message's
1717
+ ``additional_properties["_intermediate_text"]``.
1718
+
1719
+ Args:
1720
+ messages: Deserialized session messages.
1721
+ """
1722
+ await self._replay_renderer.replay_history(
1723
+ messages,
1724
+ initial_profile=initial_profile,
1725
+ file_snapshots=file_snapshots,
1726
+ compressed_blocks=compressed_blocks,
1727
+ )
1728
+
1729
+ async def _replay_compressed_block(self, block: CompressedBlock, replay_profile: str) -> str:
1730
+ """Compatibility delegate for compressed-block replay rendering."""
1731
+ return await self._replay_renderer._replay_compressed_block(block, replay_profile)
1732
+
1733
+ async def _replay_assistant(
1734
+ self,
1735
+ contents: list[Any],
1736
+ profile_name: str = "",
1737
+ is_intermediate: bool = False,
1738
+ created_at: Any = None,
1739
+ ) -> None:
1740
+ """Compatibility delegate for assistant replay rendering."""
1741
+ await self._replay_renderer._replay_assistant(
1742
+ contents,
1743
+ profile_name=profile_name,
1744
+ is_intermediate=is_intermediate,
1745
+ created_at=created_at,
1746
+ )
1747
+
1748
+ @property
1749
+ def toc_items(self) -> list[TocItem]:
1750
+ return self._toc.items()
1751
+
1752
+ def scroll_to_turn(self, turn_id: str) -> None:
1753
+ # A turn older than the replayed tail, or in a batch not laid out yet, is jumped to once it lays out.
1754
+ if self._replay_mount.jump_when_laid_out(turn_id):
1755
+ return
1756
+ for child in self.children:
1757
+ if isinstance(child, UserMessage) and child.id == turn_id:
1758
+ for c in self.query(UserMessage):
1759
+ c.remove_class("-highlighted")
1760
+ child.add_class("-highlighted")
1761
+ if self._agent_running:
1762
+ self._auto_scroll_paused_by_user = True
1763
+ self.scroll_to_widget(child, animate=False, top=True, immediate=True)
1764
+ # After the scroll: its geometry read can run the layout that queues a settle re-pin.
1765
+ self._scroll_controller.note_toc_jump()
1766
+ # Bypass the async widget → UpdateScroll → screen idle chain
1767
+ # that can lose the race against the next render frame.
1768
+ self.set_screen_scroll_required_and_check_idle()
1769
+ return