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,1057 @@
1
+ # Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
2
+
3
+ """Markdown token parser — converts markdown-it tokens into MarkdownBlock objects."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import html
8
+ import re
9
+ from collections.abc import Callable, Iterable
10
+ from typing import TYPE_CHECKING, Any, cast
11
+ from urllib.parse import quote
12
+
13
+ from markdown_it import MarkdownIt
14
+ from markdown_it.common.utils import isLinkClose, isLinkOpen
15
+ from markdown_it.rules_block import fence as _markdown_it_block_fence
16
+ from markdown_it.rules_core.linkify import linkify as _markdown_it_core_linkify
17
+ from markdown_it.rules_inline.linkify import SCHEME_RE
18
+ from markdown_it.rules_inline.linkify import linkify as _markdown_it_inline_linkify
19
+ from markdown_it.token import Token
20
+ from textual._cells import cell_len
21
+ from textual._slug import slug_for_tcss_id
22
+ from textual.content import Content, Span
23
+ from textual.highlight import guess_language, highlight
24
+ from textual.style import Style
25
+
26
+ from chrys.app.tui.widgets.markdown.blocks import BULLETS, MarkdownBlock, MarkdownGutter
27
+ from chrys.app.tui.widgets.markdown.diagram.model import DiagnosticSeverity
28
+ from chrys.app.tui.widgets.markdown.links import terminal_link_target
29
+ from chrys.app.tui.widgets.markdown.math import CompiledMath, compile_math
30
+ from chrys.app.tui.widgets.markdown.math.markdown import disable_math, enable_math
31
+ from chrys.app.tui.widgets.syntax_theme import NoErrorHighlightTheme
32
+
33
+ if TYPE_CHECKING:
34
+ from markdown_it.ruler import Ruler
35
+ from markdown_it.rules_block.state_block import StateBlock
36
+ from markdown_it.rules_core.state_core import StateCore
37
+ from markdown_it.rules_inline.state_inline import StateInline
38
+
39
+ from chrys.app.tui.widgets.markdown.diagram.model import CompiledDiagram
40
+
41
+ _ZERO_WIDTH_JOINER = "\u200d"
42
+ _LINKIFY_SOURCE_META = "_chrys_linkify_source"
43
+ _FENCE_CLOSED_META = "_chrys_fence_closed"
44
+ _HTML_LINE_BREAK_RULE = "chrys_html_line_break"
45
+ _HTML_LINE_BREAK = re.compile(r"<br\s*/?>[ \t]*(?:\n[ \t]*)?", re.IGNORECASE)
46
+ # CJK sentence punctuation (fullwidth/halfwidth variants and their vertical and
47
+ # small presentation forms included), the ellipsis leaders CJK prose typically
48
+ # doubles, and the full set of CJK/fullwidth paired delimiters (both sides of
49
+ # each pair): parentheses, square/curly/angle/corner/lenticular/tortoise
50
+ # brackets and quotation marks, including the curly quotes CJK prose shares
51
+ # with English typography. Dash and wave-dash families are deliberately
52
+ # excluded: they double as joiners inside prose and URLs.
53
+ _CJK_LINK_BOUNDARIES = frozenset(
54
+ "\uff0c\u3002\uff01\uff1f\uff1b\uff1a\u3001\u2026\u2025\uff0e\uff61\uff64"
55
+ "\uff08\uff09\u3010\u3011\u300a\u300b\u300c\u300d\u300e\u300f"
56
+ "\uff3b\uff3d\uff5b\uff5d\u3008\u3009\u3014\u3015\u3016\u3017"
57
+ "\u3018\u3019\u301a\u301b\uff62\uff63\u301d\u301e\u301f\uff5f\uff60"
58
+ "\u201c\u201d\u2018\u2019\uff02\uff07\uff1c\uff1e"
59
+ "\ufe10\ufe11\ufe12\ufe13\ufe14\ufe15\ufe16\ufe17\ufe18\ufe19\ufe30"
60
+ "\ufe35\ufe36\ufe37\ufe38\ufe39\ufe3a\ufe3b\ufe3c\ufe3d\ufe3e"
61
+ "\ufe3f\ufe40\ufe41\ufe42\ufe43\ufe44\ufe47\ufe48"
62
+ "\ufe50\ufe51\ufe52\ufe54\ufe55\ufe56\ufe57"
63
+ "\ufe59\ufe5a\ufe5b\ufe5c\ufe5d\ufe5e\ufe64\ufe65"
64
+ )
65
+
66
+
67
+ HEADING_STYLES = {
68
+ 1: "virtualized-markdown--h1",
69
+ 2: "virtualized-markdown--h2",
70
+ 3: "virtualized-markdown--h3",
71
+ 4: "virtualized-markdown--h4",
72
+ 5: "virtualized-markdown--h5",
73
+ 6: "virtualized-markdown--h6",
74
+ }
75
+
76
+
77
+ def _is_hex_pair(text: str) -> bool:
78
+ """Whether *text* is exactly two hexadecimal digits."""
79
+ return len(text) == 2 and all(char in "0123456789abcdefABCDEF" for char in text)
80
+
81
+
82
+ def _raw_boundary_index(display: str, raw: str, start: int) -> tuple[int | None, int, bool]:
83
+ """Align linkified display text against its raw source spelling.
84
+
85
+ Walks *display* and ``raw[start:]`` in lockstep. A display character that
86
+ the source spells as a percent escape, an HTML entity, or a backslash
87
+ escape was explicitly written into the construct and is never treated as a
88
+ prose boundary. Returns ``(boundary, end, complete)``: *boundary* is the
89
+ display index of the first literally-written CJK boundary character
90
+ (``None`` when every one is escaped), *end* is the raw index one past the
91
+ aligned text, and *complete* is False when the walk stopped early because
92
+ ``raw`` stopped spelling *display* — normalization this walk cannot model,
93
+ such as a punycode host rendered decoded. ``raw`` is always the
94
+ construct's own stamped source, so a boundary found before that point was
95
+ aligned lockstep against it and remains trustworthy; only positions after
96
+ the stop are unknown.
97
+ """
98
+ boundary: int | None = None
99
+ raw_index = start
100
+ display_index = 0
101
+ while display_index < len(display):
102
+ char = display[display_index]
103
+ if raw_index >= len(raw):
104
+ return boundary, raw_index, False
105
+ # ``urllib.parse.quote`` always treats RFC 3986 unreserved ASCII as
106
+ # safe, even when ``safe`` is empty. Build the byte spelling directly
107
+ # so source escapes such as ``%41`` and ``%2D`` align as decoded text.
108
+ encoded = "".join(f"%{byte:02X}" for byte in char.encode())
109
+ if (
110
+ len(encoded) > 1
111
+ and raw[raw_index : raw_index + len(encoded)].upper() == encoded.upper()
112
+ and display[display_index : display_index + len(encoded)] != raw[raw_index : raw_index + len(encoded)]
113
+ ):
114
+ # Percent-encoded in the source: written intent, never a boundary.
115
+ # If the display also spells the same ``%XX`` text, linkify kept it
116
+ # verbatim rather than decoding it (notably ``%25``).
117
+ raw_index += len(encoded)
118
+ display_index += 1
119
+ continue
120
+ if (
121
+ char == "%"
122
+ and raw[raw_index] == "%"
123
+ and display[display_index + 1 : display_index + 3].upper() == raw[raw_index + 1 : raw_index + 3].upper()
124
+ and _is_hex_pair(display[display_index + 1 : display_index + 3])
125
+ ):
126
+ # An escape the renderer kept verbatim but re-emitted in uppercase
127
+ # (``%2c`` displays as ``%2C``); consume the whole triple on both
128
+ # sides. Same-case triples take this branch too, equivalently.
129
+ raw_index += 3
130
+ display_index += 3
131
+ continue
132
+ if (
133
+ raw[raw_index] == "&"
134
+ and (semi := raw.find(";", raw_index + 1, raw_index + 40)) > 0
135
+ and html.unescape(raw[raw_index : semi + 1]) == char
136
+ and display[display_index : display_index + semi + 1 - raw_index] != raw[raw_index : semi + 1]
137
+ ):
138
+ # An HTML entity or numeric character reference the renderer decoded
139
+ # (linkify keeps entities verbatim — then display spells the same
140
+ # ``&...;`` and the literal branch below must consume it instead).
141
+ raw_index = semi + 1
142
+ display_index += 1
143
+ continue
144
+ if (
145
+ raw[raw_index] == "\\"
146
+ and raw_index + 1 < len(raw)
147
+ and raw[raw_index + 1] == char
148
+ and char.isascii()
149
+ and not char.isalnum()
150
+ ):
151
+ # A backslash escape (CommonMark escapes ASCII punctuation only).
152
+ raw_index += 2
153
+ display_index += 1
154
+ continue
155
+ if raw[raw_index] == char:
156
+ if boundary is None and char in _CJK_LINK_BOUNDARIES:
157
+ boundary = display_index
158
+ raw_index += 1
159
+ display_index += 1
160
+ continue
161
+ return boundary, raw_index, False
162
+ return boundary, raw_index, True
163
+
164
+
165
+ def _path_start(url: str) -> int:
166
+ """The index where a URL's path/query/fragment begins (its length if none)."""
167
+ if url.startswith("//"):
168
+ # Protocol-relative: the authority follows immediately.
169
+ index = 2
170
+ else:
171
+ scheme_end = url.find("://")
172
+ index = scheme_end + 3 if scheme_end >= 0 else 0
173
+ while index < len(url) and url[index] not in "/?#":
174
+ index += 1
175
+ return index
176
+
177
+
178
+ def _boundary_past_authority(display: str, raw: str) -> int | None:
179
+ """Fallback boundary alignment that skips the authority component.
180
+
181
+ A punycode host is spelled ``xn--…`` in the source but rendered decoded
182
+ (``例え.jp``), so the lockstep walk cannot align it. IDNA labels cannot
183
+ contain CJK punctuation, so no prose boundary is lost by fast-forwarding
184
+ both sides to where the path begins and walking only the remainder.
185
+ """
186
+ display_start = _path_start(display)
187
+ boundary, _, _ = _raw_boundary_index(display[display_start:], raw, _path_start(raw))
188
+ return None if boundary is None else display_start + boundary
189
+
190
+
191
+ def _inline_linkify_with_source(state: StateInline, silent: bool) -> bool:
192
+ """Run markdown-it's inline linkifier and retain the exact source spelling."""
193
+ start = state.pos
194
+ token_count = len(state.tokens)
195
+ scheme_match = SCHEME_RE.search(state.pending)
196
+ matched = _markdown_it_inline_linkify(state, silent)
197
+ if not matched or silent or scheme_match is None:
198
+ return matched
199
+
200
+ source = state.src[start - len(scheme_match.group(1)) : state.pos]
201
+ for token in state.tokens[token_count:]:
202
+ if token.type == "link_open" and token.markup == "linkify":
203
+ token.meta[_LINKIFY_SOURCE_META] = source
204
+ break
205
+ return matched
206
+
207
+
208
+ def _core_linkify_sources(state: StateCore, tokens: list[Token]) -> list[str]:
209
+ """Collect the raw matches markdown-it's core linkifier will replace."""
210
+ linkify = state.md.linkify
211
+ if linkify is None:
212
+ return []
213
+ html_link_level = 0
214
+ matches_by_position: list[tuple[int, list[str]]] = []
215
+ index = len(tokens)
216
+ while index >= 1:
217
+ index -= 1
218
+ current = tokens[index]
219
+
220
+ if current.type == "link_close":
221
+ index -= 1
222
+ while tokens[index].level != current.level and tokens[index].type != "link_open":
223
+ index -= 1
224
+ continue
225
+
226
+ if current.type == "html_inline":
227
+ if isLinkOpen(current.content) and html_link_level > 0:
228
+ html_link_level -= 1
229
+ if isLinkClose(current.content):
230
+ html_link_level += 1
231
+ if html_link_level > 0 or current.type != "text" or not linkify.test(current.content):
232
+ continue
233
+
234
+ links = linkify.match(current.content) or []
235
+ if links and links[0].index == 0 and index > 0 and tokens[index - 1].type == "text_special":
236
+ links = links[1:]
237
+ sources = [link.text for link in links if state.md.validateLink(state.md.normalizeLink(link.url))]
238
+ if sources:
239
+ matches_by_position.append((index, sources))
240
+
241
+ return [source for _, sources in reversed(matches_by_position) for source in sources]
242
+
243
+
244
+ def _core_linkify_with_source(state: StateCore) -> None:
245
+ """Run markdown-it's core linkifier and annotate its generated links."""
246
+ sources_by_inline = [
247
+ (inline_token, _core_linkify_sources(state, inline_token.children))
248
+ for inline_token in state.tokens
249
+ if (
250
+ inline_token.type == "inline"
251
+ and state.md.linkify
252
+ and state.md.linkify.pretest(inline_token.content)
253
+ and inline_token.children is not None
254
+ )
255
+ ]
256
+
257
+ _markdown_it_core_linkify(state)
258
+
259
+ for inline_token, sources in sources_by_inline:
260
+ source_index = 0
261
+ for token in inline_token.children or []:
262
+ if (
263
+ token.type == "link_open"
264
+ and token.markup == "linkify"
265
+ and _LINKIFY_SOURCE_META not in token.meta
266
+ and source_index < len(sources)
267
+ ):
268
+ token.meta[_LINKIFY_SOURCE_META] = sources[source_index]
269
+ source_index += 1
270
+
271
+
272
+ def _fence_closing_line(state: StateBlock, line: int, markup: str) -> bool:
273
+ """Whether *line* is the closing marker consumed for a fence token."""
274
+ if line < 0 or line >= len(state.bMarks) or not markup:
275
+ return False
276
+
277
+ position = state.bMarks[line] + state.tShift[line]
278
+ maximum = state.eMarks[line]
279
+ if position >= maximum or state.is_code_block(line) or state.src[position] != markup[0]:
280
+ return False
281
+
282
+ marker_end = state.skipCharsStr(position, markup[0])
283
+ if marker_end - position < len(markup):
284
+ return False
285
+ return state.skipSpaces(marker_end) >= maximum
286
+
287
+
288
+ def _block_fence_with_closed_meta(state: StateBlock, start_line: int, end_line: int, silent: bool) -> bool:
289
+ """Run markdown-it's fence rule and retain whether it found an end marker."""
290
+ token_count = len(state.tokens)
291
+ matched = _markdown_it_block_fence(state, start_line, end_line, silent)
292
+ if not matched or silent or len(state.tokens) == token_count:
293
+ return matched
294
+
295
+ token = state.tokens[-1]
296
+ token.meta[_FENCE_CLOSED_META] = bool(
297
+ token.map is not None and _fence_closing_line(state, token.map[1] - 1, token.markup)
298
+ )
299
+ return matched
300
+
301
+
302
+ def _installed_rule_fn(ruler: Ruler[Any], name: str) -> Callable[..., Any] | None:
303
+ """The function currently installed for rule *name* (``None`` when absent)."""
304
+ for rule in ruler.__rules__:
305
+ if rule.name == name:
306
+ return rule.fn
307
+ return None
308
+
309
+
310
+ def _replace_rule_fn(ruler: Ruler[Any], name: str, fn: Callable[..., Any]) -> None:
311
+ """Swap rule *name*'s function, keeping the rules it may interrupt.
312
+
313
+ ``Ruler.at`` resets ``alt`` unless given one; a fence that lost its
314
+ ``alt`` no longer interrupts a paragraph, so a code block right after a
315
+ line of text would parse as that paragraph's text.
316
+ """
317
+ alt = next(list(rule.alt) for rule in ruler.__rules__ if rule.name == name)
318
+ ruler.at(name, fn, {"alt": alt})
319
+
320
+
321
+ def _configure_markdown_parser(parser: MarkdownIt) -> MarkdownIt:
322
+ """Instrument a MarkdownIt parser to preserve raw auto-link spellings.
323
+
324
+ Only the stock linkify rules are replaced, with wrappers that delegate to
325
+ them. A caller-supplied parser factory may have installed custom linkify
326
+ rules; the source-stamping wrappers are semantically valid only around the
327
+ stock behavior they mirror, so custom rules are left in place and their
328
+ links simply render without boundary splitting.
329
+ """
330
+ if _installed_rule_fn(parser.inline.ruler, "linkify") is _markdown_it_inline_linkify:
331
+ _replace_rule_fn(parser.inline.ruler, "linkify", _inline_linkify_with_source)
332
+ if _installed_rule_fn(parser.core.ruler, "linkify") is _markdown_it_core_linkify:
333
+ _replace_rule_fn(parser.core.ruler, "linkify", _core_linkify_with_source)
334
+ if _installed_rule_fn(parser.block.ruler, "fence") is _markdown_it_block_fence:
335
+ _replace_rule_fn(parser.block.ruler, "fence", _block_fence_with_closed_meta)
336
+ return parser
337
+
338
+
339
+ def _restore_ipv6_authority(url: str) -> str:
340
+ """Restore IPv6 authority brackets in an already normalized destination."""
341
+ # mdurl percent-encodes IPv6 brackets as if they were path characters.
342
+ # Restore only authority brackets; encoded path/query data stays intact.
343
+ return re.sub(
344
+ r"^(https?://(?:[^/?#]*@)?)%5B([^/?#]+)%5D(?=[:/?#]|$)",
345
+ r"\1[\2]",
346
+ url,
347
+ flags=re.IGNORECASE,
348
+ )
349
+
350
+
351
+ def _restore_ipv6_link_targets(state: StateCore) -> None:
352
+ """Repair normalized link and image targets after the linkify rules run."""
353
+ for token in state.tokens:
354
+ for child in token.children or []:
355
+ if child.type not in {"link_open", "image"}:
356
+ continue
357
+ attribute = "href" if child.type == "link_open" else "src"
358
+ if isinstance(target := child.attrGet(attribute), str):
359
+ child.attrSet(attribute, _restore_ipv6_authority(target))
360
+
361
+
362
+ def _html_line_break(state: StateInline, silent: bool) -> bool:
363
+ """Read ``<br>`` as a line break, the one HTML tag with a meaning in plain text.
364
+
365
+ Models write it for line breaks inside table cells. Like a newline, it
366
+ takes the spaces around it and the newline after it, so ``one<br>``
367
+ followed by a new line breaks once, not twice. One that ends the text
368
+ breaks nothing, as in a browser, instead of adding an empty row.
369
+ """
370
+ if state.src[state.pos] != "<":
371
+ return False
372
+ match = _HTML_LINE_BREAK.match(state.src, state.pos, state.posMax)
373
+ if match is None:
374
+ return False
375
+ if not silent:
376
+ state.pending = state.pending.rstrip(" ")
377
+ if match.end() < state.posMax:
378
+ state.push("hardbreak", "br", 0)
379
+ state.pos = match.end()
380
+ return True
381
+
382
+
383
+ def _create_markdown_parser() -> MarkdownIt:
384
+ """Create the default Markdown parser used by the TUI.
385
+
386
+ HTML stays text: the renderer draws none, and ``List<String>`` or
387
+ ``<file>`` mean those characters, so reading them as tags would drop them
388
+ along with the text an HTML block holds. Only ``<br>`` breaks the line.
389
+ """
390
+ parser = MarkdownIt("gfm-like", {"html": False})
391
+ enable_math(parser)
392
+ parser.inline.ruler.before("html_inline", _HTML_LINE_BREAK_RULE, _html_line_break)
393
+ parser.core.ruler.after("linkify", "chrys_ipv6_links", _restore_ipv6_link_targets)
394
+ if parser.linkify is not None:
395
+ # Bare filenames and identifiers can also be valid domain names.
396
+ # Only explicit HTTP(S) prefixes opt ordinary prose into auto-linking.
397
+ parser.linkify.set({"fuzzy_link": False, "fuzzy_email": False, "fuzzy_ip": False})
398
+ for scheme in ("ftp:", "mailto:", "//"):
399
+ parser.linkify.add(scheme, None)
400
+ return _configure_markdown_parser(parser)
401
+
402
+
403
+ def _hard_line_breaks(state: StateCore) -> None:
404
+ for token in state.tokens:
405
+ for child in token.children or []:
406
+ if child.type == "softbreak":
407
+ child.type = "hardbreak"
408
+
409
+
410
+ def create_line_break_markdown_parser() -> MarkdownIt:
411
+ """The default parser, keeping each newline inside a paragraph as a line break."""
412
+ parser = _create_markdown_parser()
413
+ parser.core.ruler.push("chrys_hard_line_breaks", _hard_line_breaks)
414
+ return parser
415
+
416
+
417
+ def create_user_text_markdown_parser() -> MarkdownIt:
418
+ """The line-break parser for text a person typed, where ``<br>`` stays text too.
419
+
420
+ A person typing ``<br>`` means those characters, as with any other tag.
421
+ """
422
+ parser = create_line_break_markdown_parser()
423
+ parser.inline.ruler.disable(_HTML_LINE_BREAK_RULE)
424
+ disable_math(parser)
425
+ return parser
426
+
427
+
428
+ def _token_to_content(
429
+ token: Token,
430
+ *,
431
+ open_links: bool = True,
432
+ math_compiler: Callable[[str], CompiledMath] = compile_math,
433
+ ) -> Content:
434
+ """Convert an inline token to Textual Content.
435
+
436
+ Args:
437
+ token: A markdown token.
438
+ open_links: Emit terminal hyperlinks for allowed external destinations.
439
+
440
+ Returns:
441
+ Content instance.
442
+ """
443
+ if token.children is None:
444
+ return Content("")
445
+
446
+ tokens: list[str] = []
447
+ spans: list[Span] = []
448
+ style_stack: list[tuple[Style | str, int]] = []
449
+ position: int = 0
450
+ split_linkified_text: tuple[str, str] | None = None
451
+ skip_link_close = False
452
+
453
+ def add_content(text: str) -> None:
454
+ nonlocal position
455
+ tokens.append(text)
456
+ position += len(text)
457
+
458
+ def add_style(style: Style | str) -> None:
459
+ style_stack.append((style, position))
460
+
461
+ def close_tag() -> None:
462
+ style, start = style_stack.pop()
463
+ spans.append(Span(start, position, style))
464
+
465
+ for child_index, child in enumerate(token.children):
466
+ child_type = child.type
467
+ if child_type == "text":
468
+ if split_linkified_text is None:
469
+ add_content(re.sub(r"\s+", " ", child.content))
470
+ else:
471
+ link_text, suffix = split_linkified_text
472
+ add_content(link_text)
473
+ close_tag()
474
+ add_content(suffix)
475
+ split_linkified_text = None
476
+ skip_link_close = True
477
+ if child_type == "hardbreak":
478
+ add_content("\n")
479
+ if child_type == "softbreak":
480
+ add_content(" ")
481
+ elif child_type == "code_inline":
482
+ add_style(".code_inline")
483
+ add_content(child.content)
484
+ close_tag()
485
+ elif child_type == "math_inline":
486
+ formula = math_compiler(child.content)
487
+ add_content(formula.linear if formula.rows else child.meta["math_source"])
488
+ elif child_type == "math_literal":
489
+ add_content(child.content)
490
+ elif child_type == "em_open":
491
+ add_style(".em")
492
+ elif child_type == "strong_open":
493
+ add_style(".strong")
494
+ elif child_type == "s_open":
495
+ add_style(".s")
496
+ elif child_type == "link_open":
497
+ href = cast("str", child.attrs.get("href", ""))
498
+ if child.markup == "linkify" and child_index + 1 < len(token.children):
499
+ next_child = token.children[child_index + 1]
500
+ if next_child.type == "text" and not _CJK_LINK_BOUNDARIES.isdisjoint(next_child.content):
501
+ display = next_child.content
502
+ boundary = None
503
+ raw_source = child.meta.get(_LINKIFY_SOURCE_META)
504
+ if isinstance(raw_source, str):
505
+ boundary, _, complete = _raw_boundary_index(display, raw_source, 0)
506
+ if boundary is None and not complete:
507
+ # A punycode host renders decoded and defeats the
508
+ # lockstep walk before any literal boundary was
509
+ # seen; retry from where the path begins.
510
+ boundary = _boundary_past_authority(display, raw_source)
511
+ if boundary is not None:
512
+ link_text = display[:boundary]
513
+ suffix = display[boundary:]
514
+ # Truncate the href at the occurrence matching the display
515
+ # boundary: earlier occurrences may be explicitly encoded
516
+ # URL content that must be preserved.
517
+ encoded_boundary = quote(display[boundary], safe="").upper()
518
+ occurrence = link_text.count(display[boundary]) + 1
519
+ href_upper = href.upper()
520
+ href_boundary = -1
521
+ for _ in range(occurrence):
522
+ href_boundary = href_upper.find(encoded_boundary, href_boundary + 1)
523
+ if href_boundary < 0:
524
+ break
525
+ href = href[:href_boundary] if href_boundary >= 0 else link_text
526
+ split_linkified_text = (link_text, suffix)
527
+ action = f"link({href!r})"
528
+ target = terminal_link_target(href) if open_links else None
529
+ # None inherits a nested image's link; an empty link explicitly clears it.
530
+ add_style(Style(underline=True, link=target or "") + Style.from_meta({"@click": action}))
531
+ elif child_type == "image":
532
+ href = cast("str", child.attrs.get("src", ""))
533
+ alt = child.attrs.get("alt", "")
534
+ action = f"link({href!r})"
535
+ target = terminal_link_target(href) if open_links else None
536
+ add_style(Style(underline=True, link=target or "") + Style.from_meta({"@click": action}))
537
+ add_content("\U0001f5bc ")
538
+ if alt:
539
+ add_content(f"({alt})")
540
+ if child.children is not None:
541
+ for grandchild in child.children:
542
+ add_content(grandchild.content)
543
+ close_tag()
544
+ elif child_type.endswith("_close"):
545
+ if child_type == "link_close" and skip_link_close:
546
+ skip_link_close = False
547
+ else:
548
+ close_tag()
549
+
550
+ content = Content("".join(tokens), spans=spans)
551
+ return content
552
+
553
+
554
+ _FENCE_GUESS_SAMPLE_CHARS = 2048
555
+ """An unlabeled code block guesses its language from at most this much leading code.
556
+
557
+ Pygments' ``guess_lexer`` scores every lexer's ``analyse_text`` over its whole
558
+ input: on a long bare fence (a log, command output) that is hundreds of
559
+ milliseconds of GIL-held time on every parse of the message. The deciding
560
+ signals (shebangs, doctypes, leading keywords) sit at the top, and a block
561
+ that fits in the sample is guessed exactly as before.
562
+ """
563
+
564
+
565
+ def _guess_code_language(code: str) -> str:
566
+ """Return the lexer name for an unlabeled code block, guessed from a line-aligned prefix."""
567
+ if len(code) > _FENCE_GUESS_SAMPLE_CHARS:
568
+ cut = code.rfind("\n", 0, _FENCE_GUESS_SAMPLE_CHARS)
569
+ code = code[: cut if cut > 0 else _FENCE_GUESS_SAMPLE_CHARS]
570
+ return guess_language(code, None)
571
+
572
+
573
+ def _get_list_indent(stack: list[dict]) -> int:
574
+ """Get the indent from the nearest list_item in the stack."""
575
+ for parent in reversed(stack):
576
+ if parent["type"] == "list_item":
577
+ return parent.get("indent", 0)
578
+ return 0
579
+
580
+
581
+ def _apply_containers(block: MarkdownBlock, stack: list[dict]) -> None:
582
+ """Preserve container order for every block, including non-paragraph items."""
583
+ gutter: list[MarkdownGutter] = []
584
+ indent = 0
585
+ for parent in stack:
586
+ if parent["type"] == "blockquote":
587
+ gutter.append(MarkdownGutter(parent["container_id"], "quote", 2))
588
+ elif parent["type"] == "list_item":
589
+ prefix = "" if parent["first_block_done"] else parent["prefix"]
590
+ parent["first_block_done"] = True
591
+ gutter.append(MarkdownGutter(parent["container_id"], "list", parent["indent"] - indent, prefix))
592
+ indent = parent["indent"]
593
+ if not gutter:
594
+ return
595
+ block.gutter = tuple(gutter)
596
+ block.indent = indent
597
+ block.bq_depth = sum(part.kind == "quote" for part in gutter)
598
+ block.border_left = "▌ " * block.bq_depth
599
+ block.prefix = next((part.prefix for part in reversed(gutter) if part.prefix), "")
600
+
601
+
602
+ def _parse_tokens(
603
+ tokens: Iterable[Token],
604
+ unhandled_token: Callable[[Token], MarkdownBlock | None] | None = None,
605
+ compile_diagram: Callable[[str], CompiledDiagram] | None = None,
606
+ *,
607
+ open_links: bool = True,
608
+ math_enabled: bool = True,
609
+ math_compiler: Callable[[str], CompiledMath] = compile_math,
610
+ ) -> list[MarkdownBlock]:
611
+ """Parse markdown-it tokens into a flat list of MarkdownBlock objects.
612
+
613
+ Args:
614
+ tokens: Iterable of markdown-it tokens.
615
+ unhandled_token: Optional callback for unhandled token types.
616
+ open_links: Emit terminal hyperlinks for allowed external destinations.
617
+
618
+ Returns:
619
+ A list of MarkdownBlock data objects.
620
+ """
621
+ blocks: list[MarkdownBlock] = []
622
+
623
+ stack: list[dict] = []
624
+ list_stack: list[dict] = []
625
+
626
+ def append_block(block: MarkdownBlock) -> None:
627
+ _apply_containers(block, stack)
628
+ blocks.append(block)
629
+
630
+ for token_index, token in enumerate(tokens):
631
+ token_type = token.type
632
+ source_range = (token.map[0], token.map[1]) if token.map is not None else (0, 0)
633
+
634
+ if token_type == "heading_open":
635
+ level = int(token.tag[1])
636
+ stack.append(
637
+ {
638
+ "type": "heading",
639
+ "level": level,
640
+ "source_range": source_range,
641
+ }
642
+ )
643
+
644
+ elif token_type == "heading_close":
645
+ ctx = stack.pop()
646
+ content = ctx.get("content", Content(""))
647
+ slug_text = content.plain
648
+ block_id = f"heading-{slug_for_tcss_id(slug_text)}"
649
+
650
+ top_margin = 2 if ctx["level"] <= 2 else 1
651
+ append_block(
652
+ MarkdownBlock(
653
+ block_type="heading",
654
+ content=content,
655
+ level=ctx["level"],
656
+ block_id=block_id,
657
+ source_range=ctx["source_range"],
658
+ style_name=HEADING_STYLES.get(ctx["level"], "virtualized-markdown--h1"),
659
+ top_margin=top_margin,
660
+ bottom_margin=1,
661
+ text_align="center" if ctx["level"] == 1 else "left",
662
+ )
663
+ )
664
+
665
+ elif token_type == "paragraph_open":
666
+ stack.append({"type": "paragraph", "source_range": source_range})
667
+
668
+ elif token_type == "paragraph_close":
669
+ ctx = stack.pop()
670
+ content = ctx.get("content", Content(""))
671
+ append_block(
672
+ MarkdownBlock(
673
+ block_type="paragraph",
674
+ content=content,
675
+ source_range=ctx["source_range"],
676
+ style_name="virtualized-markdown--paragraph",
677
+ top_margin=0 if token.hidden else 1,
678
+ bottom_margin=0 if token.hidden else 1,
679
+ )
680
+ )
681
+
682
+ elif token_type == "blockquote_open":
683
+ if blocks and blocks[-1].bottom_margin < 1:
684
+ blocks[-1].bottom_margin = 1
685
+ stack.append(
686
+ {
687
+ "type": "blockquote",
688
+ "container_id": token_index,
689
+ "source_range": source_range,
690
+ "block_count_at_open": len(blocks),
691
+ }
692
+ )
693
+
694
+ elif token_type == "blockquote_close":
695
+ if len(blocks) == stack[-1]["block_count_at_open"]:
696
+ append_block(MarkdownBlock("paragraph", Content(" "), bottom_margin=0))
697
+ stack.pop()
698
+ if blocks:
699
+ blocks[-1].bottom_margin = 1
700
+
701
+ elif token_type == "bullet_list_open":
702
+ depth = sum(1 for s in list_stack if s["type"] in ("bullet_list", "ordered_list"))
703
+ list_stack.append(
704
+ {
705
+ "type": "bullet_list",
706
+ "depth": depth,
707
+ "item_count": 0,
708
+ }
709
+ )
710
+ stack.append({"type": "bullet_list", "source_range": source_range})
711
+
712
+ elif token_type == "bullet_list_close":
713
+ list_stack.pop()
714
+ stack.pop()
715
+ if not list_stack and blocks:
716
+ blocks[-1].bottom_margin = 1
717
+
718
+ elif token_type == "ordered_list_open":
719
+ depth = sum(1 for s in list_stack if s["type"] in ("bullet_list", "ordered_list"))
720
+ list_stack.append(
721
+ {
722
+ "type": "ordered_list",
723
+ "depth": depth,
724
+ "item_count": 0,
725
+ "start": int(token.attrGet("start") or 1),
726
+ }
727
+ )
728
+ stack.append({"type": "ordered_list", "source_range": source_range})
729
+
730
+ elif token_type == "ordered_list_close":
731
+ list_stack.pop()
732
+ stack.pop()
733
+ if not list_stack and blocks:
734
+ blocks[-1].bottom_margin = 1
735
+
736
+ elif token_type == "list_item_open":
737
+ if not list_stack:
738
+ continue
739
+ list_ctx = list_stack[-1]
740
+ list_ctx["item_count"] += 1
741
+
742
+ depth = list_ctx["depth"]
743
+ if list_ctx["type"] == "bullet_list":
744
+ bullet_idx = depth % len(BULLETS)
745
+ prefix = BULLETS[bullet_idx]
746
+ else:
747
+ number = list_ctx["start"] + list_ctx["item_count"] - 1
748
+ prefix = f"{number}. "
749
+
750
+ indent = _get_list_indent(stack) + max(4 if depth == 0 else 2, cell_len(prefix))
751
+ stack.append(
752
+ {
753
+ "type": "list_item",
754
+ "container_id": token_index,
755
+ "indent": indent,
756
+ "prefix": prefix,
757
+ "first_block_done": False,
758
+ "source_range": source_range,
759
+ }
760
+ )
761
+
762
+ elif token_type == "list_item_close":
763
+ if not stack[-1]["first_block_done"]:
764
+ append_block(MarkdownBlock("paragraph", Content(" "), bottom_margin=0))
765
+ stack.pop()
766
+
767
+ elif token_type == "hr":
768
+ append_block(
769
+ MarkdownBlock(
770
+ block_type="hr",
771
+ content=Content(""),
772
+ source_range=source_range,
773
+ style_name="virtualized-markdown--hr",
774
+ top_margin=2,
775
+ bottom_margin=1,
776
+ )
777
+ )
778
+
779
+ elif token_type == "table_open":
780
+ stack.append(
781
+ {
782
+ "type": "table",
783
+ "headers": [],
784
+ "rows": [],
785
+ "current_row": [],
786
+ "in_head": False,
787
+ "source_range": source_range,
788
+ }
789
+ )
790
+
791
+ elif token_type == "thead_open":
792
+ if stack and stack[-1]["type"] == "table":
793
+ stack[-1]["in_head"] = True
794
+
795
+ elif token_type == "thead_close":
796
+ if stack and stack[-1]["type"] == "table":
797
+ stack[-1]["in_head"] = False
798
+
799
+ elif token_type == "tbody_open" or token_type == "tbody_close":
800
+ pass
801
+
802
+ elif token_type == "tr_open":
803
+ if stack and stack[-1]["type"] == "table":
804
+ stack[-1]["current_row"] = []
805
+
806
+ elif token_type == "tr_close":
807
+ if stack and stack[-1]["type"] == "table":
808
+ if stack[-1]["in_head"]:
809
+ stack[-1]["headers"] = stack[-1]["current_row"][:]
810
+ else:
811
+ stack[-1]["rows"].append(stack[-1]["current_row"][:])
812
+ stack[-1]["current_row"] = []
813
+
814
+ elif token_type in ("th_open", "td_open"):
815
+ stack.append({"type": "table_cell"})
816
+
817
+ elif token_type in ("th_close", "td_close"):
818
+ ctx = stack.pop()
819
+ content = ctx.get("content", Content(""))
820
+ for parent in reversed(stack):
821
+ if parent["type"] == "table":
822
+ parent["current_row"].append(content)
823
+ break
824
+
825
+ elif token_type == "table_close":
826
+ ctx = stack.pop()
827
+ headers = ctx.get("headers", [])
828
+ rows = ctx.get("rows", [])
829
+ for block in _build_table_blocks(headers, rows, ctx["source_range"]):
830
+ append_block(block)
831
+
832
+ elif token_type == "inline":
833
+ if stack:
834
+ content = _token_to_content(token, open_links=open_links, math_compiler=math_compiler)
835
+ stack[-1]["content"] = content
836
+
837
+ elif token_type in ("fence", "code_block", "math_block"):
838
+ code = token.content.rstrip()
839
+ language = token.info or ""
840
+ normalized_language = language.split(maxsplit=1)[0].casefold() if language.strip() else ""
841
+
842
+ if math_enabled and (
843
+ token_type == "math_block"
844
+ or (
845
+ token_type == "fence"
846
+ and normalized_language == "math"
847
+ and token.meta.get(_FENCE_CLOSED_META, False)
848
+ )
849
+ ):
850
+ formula = math_compiler(token.content.strip())
851
+ source = token.meta.get("math_source", token.content.rstrip("\n"))
852
+ append_block(
853
+ MarkdownBlock(
854
+ block_type="math",
855
+ content=Content.from_text(source, markup=False),
856
+ source_range=source_range,
857
+ math=formula,
858
+ style_name="virtualized-markdown--fence",
859
+ top_margin=1,
860
+ padding_left=2,
861
+ padding_right=2,
862
+ )
863
+ )
864
+ continue
865
+
866
+ if token_type == "fence" and normalized_language == "mermaid" and token.meta.get(_FENCE_CLOSED_META, False):
867
+ if compile_diagram is None:
868
+ from chrys.app.tui.widgets.markdown.diagram import compile_mermaid
869
+
870
+ compile_diagram = compile_mermaid
871
+ diagram_source = token.content.rstrip("\n")
872
+ diagram = compile_diagram(diagram_source)
873
+
874
+ if not any(diagnostic.severity is DiagnosticSeverity.ERROR for diagnostic in diagram.diagnostics):
875
+ append_block(
876
+ MarkdownBlock(
877
+ block_type="diagram",
878
+ content=Content.from_text("", markup=False),
879
+ source_range=source_range,
880
+ style_name="virtualized-markdown--diagram",
881
+ top_margin=1,
882
+ bottom_margin=1,
883
+ code_language="mermaid",
884
+ padding_top=1,
885
+ padding_bottom=1,
886
+ padding_left=1,
887
+ padding_right=1,
888
+ diagram=diagram,
889
+ )
890
+ )
891
+ continue
892
+
893
+ highlighted = highlight(code, language=language or _guess_code_language(code), theme=NoErrorHighlightTheme)
894
+
895
+ append_block(
896
+ MarkdownBlock(
897
+ block_type="fence",
898
+ content=highlighted,
899
+ source_range=source_range,
900
+ style_name="virtualized-markdown--fence",
901
+ top_margin=1,
902
+ bottom_margin=1,
903
+ code_language=language,
904
+ padding_top=1,
905
+ padding_bottom=1,
906
+ padding_left=2,
907
+ padding_right=1,
908
+ )
909
+ )
910
+
911
+ elif unhandled_token is not None:
912
+ external = unhandled_token(token)
913
+ if external is not None:
914
+ append_block(external)
915
+
916
+ return blocks
917
+
918
+
919
+ def _build_table_blocks(
920
+ headers: list[Content],
921
+ rows: list[list[Content]],
922
+ source_range: tuple[int, int],
923
+ ) -> list[MarkdownBlock]:
924
+ """Build MarkdownBlock objects for a table.
925
+
926
+ Stores raw table data; actual rendering is done at layout time
927
+ when the available width is known.
928
+ """
929
+ if not headers:
930
+ return []
931
+
932
+ return [
933
+ MarkdownBlock(
934
+ block_type="table",
935
+ content=Content(""),
936
+ source_range=source_range,
937
+ style_name="virtualized-markdown--table",
938
+ bottom_margin=1,
939
+ table_headers=headers,
940
+ table_rows=rows,
941
+ )
942
+ ]
943
+
944
+
945
+ def _wrap_cell_text(text: str, width: int) -> list[str]:
946
+ """Wrap text to fit within width cells (CJK-aware).
947
+
948
+ Args:
949
+ text: The text to wrap.
950
+ width: Maximum cell width per line.
951
+
952
+ Returns:
953
+ A list of wrapped lines.
954
+ """
955
+ if width <= 0:
956
+ return [text]
957
+ if text.isascii() and "\t" not in text:
958
+ result: list[str] = []
959
+ for paragraph in text.split("\n"):
960
+ if paragraph:
961
+ result.extend(paragraph[index : index + width] for index in range(0, len(paragraph), width))
962
+ else:
963
+ result.append("")
964
+ return result or [""]
965
+ result: list[str] = []
966
+ for paragraph in text.split("\n"):
967
+ current = ""
968
+ for char in paragraph:
969
+ candidate = current + char
970
+ if cell_len(candidate) > width and current and not _is_cluster_continuation(current, char):
971
+ result.append(current)
972
+ current = char
973
+ else:
974
+ current = candidate
975
+ result.append(current)
976
+ return result or [""]
977
+
978
+
979
+ def _cell_wrapped_height(text: str, width: int) -> int:
980
+ """Return the number of visual rows ``_wrap_cell_text`` would produce."""
981
+ if width <= 0:
982
+ return 1
983
+ if text.isascii() and "\t" not in text:
984
+ return sum(max(1, (len(paragraph) + width - 1) // width) for paragraph in text.split("\n"))
985
+ height = 0
986
+ for paragraph in text.split("\n"):
987
+ current_chars: list[str] = []
988
+ current_width = 0
989
+ previous_char = ""
990
+ for char in paragraph:
991
+ continuation = char == _ZERO_WIDTH_JOINER or previous_char == _ZERO_WIDTH_JOINER or cell_len(char) == 0
992
+ if continuation:
993
+ current_chars.append(char)
994
+ current_width = cell_len("".join(current_chars))
995
+ previous_char = char
996
+ continue
997
+
998
+ char_width = cell_len(char)
999
+ if current_width + char_width > width and current_chars:
1000
+ height += 1
1001
+ current_chars = [char]
1002
+ current_width = char_width
1003
+ else:
1004
+ current_chars.append(char)
1005
+ current_width += char_width
1006
+ previous_char = char
1007
+ height += 1
1008
+ return max(1, height)
1009
+
1010
+
1011
+ def _is_cluster_continuation(current: str, char: str) -> bool:
1012
+ """Return whether ``char`` should stay attached to the current cluster."""
1013
+ return char == _ZERO_WIDTH_JOINER or current.endswith(_ZERO_WIDTH_JOINER) or cell_len(char) == 0
1014
+
1015
+
1016
+ def _cell_natural_width(text: str) -> int:
1017
+ """Return the width a cell needs unwrapped: its widest line (``<br>`` splits lines)."""
1018
+ return max(cell_len(line) for line in text.split("\n"))
1019
+
1020
+
1021
+ def _cell_min_width(text: str) -> int:
1022
+ """Return the narrowest width that can keep each display cluster intact."""
1023
+ if text.isascii():
1024
+ return 1
1025
+ max_width = 1
1026
+ cluster = ""
1027
+ for char in text:
1028
+ if cluster and not _is_cluster_continuation(cluster, char):
1029
+ max_width = max(max_width, cell_len(cluster))
1030
+ cluster = char
1031
+ else:
1032
+ cluster += char
1033
+ if cluster:
1034
+ max_width = max(max_width, cell_len(cluster))
1035
+ return max_width
1036
+
1037
+
1038
+ def _cell_prefix_length(text: str, target_cells: int) -> int:
1039
+ """Return the character count that fits without splitting a wide glyph.
1040
+
1041
+ When the text is wider than the target — e.g. a double-width CJK glyph in a
1042
+ column the layout squeezed narrower than the glyph — only the whole leading
1043
+ characters that fit are kept; the table renderer pads the remainder. A cell
1044
+ that renders even one cell wide of its column shifts every separator to its
1045
+ right (the misaligned-bars bug).
1046
+ """
1047
+ if target_cells <= 0:
1048
+ return 0
1049
+ if text.isascii() and "\t" not in text:
1050
+ return min(len(text), target_cells)
1051
+ out = ""
1052
+ for char in text:
1053
+ candidate = out + char
1054
+ if cell_len(candidate) > target_cells:
1055
+ break
1056
+ out = candidate
1057
+ return len(out)