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,1299 @@
1
+ # Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
2
+
3
+ """InputBar widget — multi-line input area with Send/Stop button.
4
+
5
+ Enter submits. Ctrl+J inserts a newline for multi-line input.
6
+ Cross-platform system clipboard support (macOS pbcopy/pbpaste,
7
+ Win32 ``SetClipboardData``/``GetClipboardData`` on Windows,
8
+ xclip/xsel on Linux).
9
+ When the agent is running, the button becomes Stop to interrupt.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import asyncio
15
+ import unicodedata
16
+ from dataclasses import dataclass, field
17
+ from pathlib import Path
18
+ from secrets import token_hex
19
+ from time import time
20
+ from typing import TYPE_CHECKING, Any, Literal
21
+
22
+ from rich.text import Text
23
+ from textual.containers import Horizontal
24
+ from textual.content import Content
25
+ from textual.css.scalar import Scalar
26
+ from textual.geometry import Spacing
27
+ from textual.message import Message
28
+ from textual.reactive import reactive
29
+ from textual.strip import Strip
30
+ from textual.timer import Timer
31
+ from textual.widget import Widget
32
+ from textual.widgets import Button, TextArea
33
+
34
+ from chrys.app.tui.i18n import render_content, render_str
35
+ from chrys.app.tui.support.prompt_history import DEFAULT_MAX_ENTRIES, PromptHistoryWriter, append_history, load_history
36
+ from chrys.app.tui.util.visibility import resync_compositor_regions
37
+ from chrys.app.tui.widgets import EnhancedTextArea
38
+ from chrys.app.tui.widgets.chrome.commands import is_slash_command_candidate
39
+ from chrys.app.tui.widgets.chrome.image_paste import (
40
+ convert_pasted_image_paths_to_mentions,
41
+ save_clipboard_image_to_file,
42
+ )
43
+ from chrys.app.tui.widgets.selection import NonSelectableTextMixin
44
+ from chrys.app.tui.widgets.text_area import MESSAGE_EDITOR_PASTE_MAX_TOKENS
45
+ from chrys.foundation.i18n import Localizer, MessageRef, msg
46
+ from chrys.foundation.i18n.formatting import format_message
47
+ from chrys.foundation.platform import safe_getcwd
48
+
49
+ # Status-bar chrome is synchronized by MainScreen through widget messages.
50
+ _SPINNER_FRAMES = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
51
+ # Unicode-name markers for CJK-family scripts where a preceding character cannot be an email local part.
52
+ _CJK_NAME_TOKENS = ("CJK", "HIRAGANA", "KATAKANA", "HANGUL", "BOPOMOFO", "IDEOGRAPH")
53
+ INPUT_SEND = msg("tui.input.send", fallback="Send")
54
+ INPUT_NEW = msg("tui.input.new", fallback="New")
55
+ INPUT_NEW_DESCRIPTION = msg("tui.input.new_description", fallback="Create a new session")
56
+ INPUT_QUEUED = msg("tui.input.queued", fallback="Queued")
57
+ INPUT_INTERRUPT = msg("tui.input.interrupt", fallback="Interrupt")
58
+ INPUT_CONTINUE = msg("tui.input.continue", fallback="Continue")
59
+ INPUT_RETRY = msg("tui.input.retry", fallback="Retry")
60
+ _INPUT_PROMPT_PLACEHOLDER = msg(
61
+ "tui.input.placeholder.prompt",
62
+ fallback="Type a message ({shortcut} for Editor)",
63
+ )
64
+ _INPUT_INJECT_PLACEHOLDER = msg("tui.input.placeholder.inject", fallback="Inject a message")
65
+ _INPUT_PLACEHOLDER_AGENTS = msg("tui.input.placeholder.agents", fallback="Agents")
66
+ _INPUT_PLACEHOLDER_MODELS = msg("tui.input.placeholder.models", fallback="Models")
67
+ _INPUT_PLACEHOLDER_SHELL = msg("tui.input.placeholder.shell", fallback="Shell")
68
+ _INPUT_PLACEHOLDER_COMMANDS = msg("tui.input.placeholder.commands", fallback="Commands")
69
+ _INPUT_PLACEHOLDER_FILES = msg("tui.input.placeholder.files", fallback="Files")
70
+ _INPUT_NEWLINE_HINT = msg("tui.input.hint.newline", fallback="{shortcut} for newline")
71
+
72
+ type InputLabel = MessageRef | str
73
+
74
+
75
+ def _is_cjk_character(char: str) -> bool:
76
+ name = unicodedata.name(char, "")
77
+ return any(token in name for token in _CJK_NAME_TOKENS)
78
+
79
+
80
+ def _is_file_trigger_boundary(text: str, at_pos: int) -> bool:
81
+ """Return whether an ``@`` at *at_pos* should open file suggestions."""
82
+ if at_pos == 0:
83
+ return True
84
+ previous = text[at_pos - 1]
85
+ return previous.isspace() or _is_cjk_character(previous)
86
+
87
+
88
+ def _placeholder_fragment(localizer: Localizer | None, reference: MessageRef) -> Content:
89
+ if localizer is None:
90
+ return Content.from_text(format_message(reference), markup=False)
91
+ return render_content(localizer, reference)
92
+
93
+
94
+ def _build_prompt_placeholder(localizer: Localizer | None = None) -> Content:
95
+ """Build the default placeholder with shortcut hints (reverse-video pills)."""
96
+ return Content.assemble(
97
+ _placeholder_fragment(localizer, _INPUT_PROMPT_PLACEHOLDER.bind(shortcut="Ctrl+O")),
98
+ " ",
99
+ (" # ", "r"),
100
+ " ",
101
+ _placeholder_fragment(localizer, _INPUT_PLACEHOLDER_AGENTS.bind()),
102
+ " ",
103
+ (" $ ", "r"),
104
+ " ",
105
+ _placeholder_fragment(localizer, _INPUT_PLACEHOLDER_MODELS.bind()),
106
+ " ",
107
+ (" / ", "r"),
108
+ " ",
109
+ _placeholder_fragment(localizer, _INPUT_PLACEHOLDER_COMMANDS.bind()),
110
+ " ",
111
+ (" @ ", "r"),
112
+ " ",
113
+ _placeholder_fragment(localizer, _INPUT_PLACEHOLDER_FILES.bind()),
114
+ " ",
115
+ (" ! ", "r"),
116
+ " ",
117
+ _placeholder_fragment(localizer, _INPUT_PLACEHOLDER_SHELL.bind()),
118
+ )
119
+
120
+
121
+ def _build_inject_placeholder(localizer: Localizer | None = None) -> Content:
122
+ """Build placeholder for when agent is running (inject + commands/files)."""
123
+ return Content.assemble(
124
+ _placeholder_fragment(localizer, _INPUT_INJECT_PLACEHOLDER.bind()),
125
+ " ",
126
+ (" / ", "r"),
127
+ " ",
128
+ _placeholder_fragment(localizer, _INPUT_PLACEHOLDER_COMMANDS.bind()),
129
+ " ",
130
+ (" @ ", "r"),
131
+ " ",
132
+ _placeholder_fragment(localizer, _INPUT_PLACEHOLDER_FILES.bind()),
133
+ )
134
+
135
+
136
+ if TYPE_CHECKING:
137
+ from textual.app import ComposeResult
138
+
139
+ from chrys.app.tui.i18n import LocaleController
140
+
141
+
142
+ _HISTORY_PREVIOUS_KEYS = frozenset({"up"})
143
+ _HISTORY_NEXT_KEYS = frozenset({"down"})
144
+ _BUTTON_GROUP_HORIZONTAL_PADDING = 2
145
+
146
+
147
+ @dataclass(frozen=True, slots=True)
148
+ class InputDraftSnapshot:
149
+ """Immutable chat draft captured before opening the message editor."""
150
+
151
+ text: str
152
+ cursor_location: tuple[int, int]
153
+ revision: int
154
+
155
+
156
+ @dataclass
157
+ class _HistoryBrowser:
158
+ """Mutable prompt-history browsing state for one history scope."""
159
+
160
+ entries: list[str] = field(default_factory=list)
161
+ index: int = -1
162
+ max_entries: int = DEFAULT_MAX_ENTRIES
163
+ storage_loaded: bool = False
164
+
165
+ def reset(self, entries: list[str] | None = None) -> None:
166
+ if entries is not None:
167
+ self.entries = list(entries)
168
+ self.index = -1
169
+
170
+ def append(self, text: str) -> None:
171
+ text = text.strip()
172
+ if text and (not self.entries or self.entries[-1] != text):
173
+ self.entries.append(text)
174
+ overflow = len(self.entries) - self.max_entries
175
+ if overflow > 0:
176
+ del self.entries[:overflow]
177
+ if self.index != -1:
178
+ self.index = max(0, self.index - overflow)
179
+
180
+
181
+ class _ChatTextArea(EnhancedTextArea):
182
+ """EnhancedTextArea subclass: Enter = submit, Ctrl+J = newline, input history."""
183
+
184
+ _history: _HistoryBrowser
185
+ _history_browsing: bool
186
+ _history_draft: str | None
187
+ _instance_id: str
188
+ _right_placeholder: str
189
+
190
+ class Submitted(Message):
191
+ """Posted when the user presses Enter."""
192
+
193
+ def __init__(self, text: str) -> None:
194
+ self.text = text
195
+ super().__init__()
196
+
197
+ class ShellModeRequested(Message):
198
+ """Posted when user types ``!`` at position (0, 0) with empty text."""
199
+
200
+ class EditorRequested(Message):
201
+ """Posted when the user requests the full-screen message editor."""
202
+
203
+ class ShellModeCancelled(Message):
204
+ """Posted when user exits shell mode (Escape or backspace-to-empty)."""
205
+
206
+ class SlashTrigger(Message):
207
+ """Posted when user types ``/`` at start of empty input."""
208
+
209
+ class FileTrigger(Message):
210
+ """Posted when user types ``@``."""
211
+
212
+ class AgentTrigger(Message):
213
+ """Posted when user types ``#`` at start of empty input."""
214
+
215
+ class ModelTrigger(Message):
216
+ """Posted when user types ``$`` at start of empty input."""
217
+
218
+ def __init__(self, *args, **kwargs) -> None:
219
+ kwargs.setdefault("max_paste_tokens", MESSAGE_EDITOR_PASTE_MAX_TOKENS)
220
+ super().__init__(*args, **kwargs)
221
+ self._history = _HistoryBrowser()
222
+ self._history_browsing = False
223
+ self._history_draft: str | None = None
224
+ self._instance_id = token_hex(8)
225
+ self._history_writer = PromptHistoryWriter(append_history)
226
+ self._suggestions_active: bool = False
227
+ self._right_placeholder = ""
228
+ # Session restore/workspace updates replace this before normal use; this covers early paste during startup.
229
+ self._paste_cwd: str = safe_getcwd()
230
+ self._clipboard_image_dir: Path | None = None
231
+
232
+ def set_right_placeholder(self, text: str) -> None:
233
+ """Set literal placeholder text rendered against the input's right edge."""
234
+ if text == self._right_placeholder:
235
+ return
236
+ self._right_placeholder = text
237
+ self.refresh()
238
+
239
+ def render_line(self, y: int) -> Strip:
240
+ """Render the optional right placeholder without adding a layout widget."""
241
+ line = super().render_line(y)
242
+ if self.text or y != 0 or not self.placeholder or not self._right_placeholder:
243
+ return line
244
+
245
+ hint = Content.from_text(self._right_placeholder, markup=False)
246
+ right_padding = 1
247
+ gap = self.content_size.width - line.cell_length - hint.cell_length - right_padding
248
+ if gap < 1:
249
+ return line
250
+
251
+ placeholder_style = self.get_visual_style("text-area--placeholder")
252
+ styled_hint = hint.stylize(placeholder_style)
253
+ hint_strip = Strip(
254
+ styled_hint.render_segments(self.visual_style),
255
+ styled_hint.cell_length,
256
+ )
257
+ return Strip.join(
258
+ (
259
+ line,
260
+ Strip.blank(gap, self.rich_style),
261
+ hint_strip,
262
+ Strip.blank(right_padding, self.rich_style),
263
+ )
264
+ )
265
+
266
+ def _set_text(self, text: str) -> None:
267
+ """Replace textarea content with *text* and move cursor to end."""
268
+ self.clear()
269
+ if text:
270
+ self.insert(text)
271
+
272
+ async def _refresh_history_entries(self) -> None:
273
+ browser = self._history
274
+ if browser.entries or browser.storage_loaded:
275
+ # Appends reach memory before their queued disk writes. Once this
276
+ # instance has entries, disk can only be an older prefix and must
277
+ # not replace the authoritative in-memory list.
278
+ return
279
+ refreshed_entries = await asyncio.to_thread(
280
+ load_history,
281
+ max_entries=browser.max_entries,
282
+ instance_id=self._instance_id,
283
+ )
284
+ browser.storage_loaded = True
285
+ if not browser.entries:
286
+ browser.entries = list(refreshed_entries)
287
+
288
+ def _capture_visible_draft(self) -> None:
289
+ if not self._history_browsing:
290
+ self._history_draft = self.text
291
+
292
+ async def _browse_history(self, direction: Literal[-1, 1]) -> bool:
293
+ """Move within instance history, returning True when the key was handled."""
294
+ browser = self._history
295
+
296
+ # First entry into browsing mode saves the visible draft.
297
+ self._capture_visible_draft()
298
+
299
+ self._history_browsing = True
300
+
301
+ if browser.index == -1:
302
+ await self._refresh_history_entries()
303
+ if not browser.entries:
304
+ self._history_browsing = False
305
+ return False
306
+ browser.index = len(browser.entries) - 1
307
+ self._set_text(browser.entries[browser.index])
308
+ return True
309
+
310
+ if direction < 0:
311
+ if browser.index <= 0:
312
+ self._set_text(browser.entries[browser.index])
313
+ return True
314
+ browser.index -= 1
315
+ self._set_text(browser.entries[browser.index])
316
+ return True
317
+
318
+ if browser.index < len(browser.entries) - 1:
319
+ browser.index += 1
320
+ self._set_text(browser.entries[browser.index])
321
+ return True
322
+
323
+ # Past the last entry — restore the saved draft and exit browsing.
324
+ browser.index = -1
325
+ self._history_browsing = False
326
+ self._set_text(self._history_draft or "")
327
+ return True
328
+
329
+ def _reset_history_browsing(self) -> None:
330
+ self._history_browsing = False
331
+ self._history_draft = None
332
+ self._history.reset()
333
+
334
+ def _prepare_paste_text(self, text: str) -> str:
335
+ """Convert pasted/dropped supported image paths into ``@`` mentions."""
336
+ try:
337
+ return self._prepare_image_path_paste_text(text) or text
338
+ except OSError, RuntimeError, ValueError:
339
+ return text
340
+
341
+ def _prepare_image_path_paste_text(self, text: str) -> str | None:
342
+ """Return converted image-path paste text, or ``None`` when not applicable."""
343
+ converted = convert_pasted_image_paths_to_mentions(text, cwd=Path(self._paste_cwd))
344
+ if converted == text:
345
+ return None
346
+ start, _end = self.selection
347
+ insert_index = self.document.get_index_from_location(start)
348
+ if insert_index > 0 and not self.text[insert_index - 1].isspace():
349
+ return f" {converted}"
350
+ return converted
351
+
352
+ def set_paste_cwd(self, cwd: str) -> None:
353
+ """Set the cwd used to resolve relative pasted/dropped image paths."""
354
+ self._paste_cwd = cwd
355
+
356
+ def set_clipboard_image_dir(self, directory: str | Path | None) -> None:
357
+ """Set the session-scoped directory used for clipboard image attachments."""
358
+ self._clipboard_image_dir = Path(directory) if directory is not None else None
359
+
360
+ def _prepare_clipboard_image_paste_text(self) -> str | None:
361
+ """Return paste text for a bitmap image currently on the platform clipboard."""
362
+ # TODO(perf): Clipboard capture may run osascript/PIL/file I/O. Move
363
+ # this boundary to a worker when paste completion can remain ordered
364
+ # with subsequent input events.
365
+ path = save_clipboard_image_to_file(self._clipboard_image_dir)
366
+ if path is None:
367
+ return None
368
+ try:
369
+ return self._prepare_image_path_paste_text(str(path)) or str(path)
370
+ except OSError, RuntimeError, ValueError:
371
+ return str(path)
372
+
373
+ def _insert_paste_text(self, text: str) -> bool:
374
+ text = self._sanitize_paste_text(text)
375
+ if result := self._replace_via_keyboard(text, *self.selection):
376
+ self.move_cursor(result.end_location)
377
+ return True
378
+ return False
379
+
380
+ async def _on_paste(self, event) -> None:
381
+ """Handle text paste events and terminal image-clipboard no-ops."""
382
+ if not event.text:
383
+ event.prevent_default()
384
+ if not self.read_only:
385
+ self._insert_clipboard_image_paste()
386
+ event.stop()
387
+ return
388
+ await super()._on_paste(event)
389
+
390
+ def _insert_clipboard_image_paste(self) -> bool:
391
+ text = self._prepare_clipboard_image_paste_text()
392
+ if not text:
393
+ return False
394
+ return self._insert_paste_text(text)
395
+
396
+ def action_paste(self) -> None:
397
+ """Paste text or a platform clipboard image into the chat input."""
398
+ if self.read_only:
399
+ return
400
+ if not self._insert_clipboard_image_paste():
401
+ super().action_paste()
402
+
403
+ async def _on_key(self, event) -> None:
404
+ # Prompt history navigation: up on first line, down on last line.
405
+ # Skip history when suggestion list is active — arrows navigate suggestions instead
406
+ if (
407
+ event.key in _HISTORY_PREVIOUS_KEYS
408
+ and self.cursor_location[0] == 0
409
+ and not self._suggestions_active
410
+ and await self._browse_history(-1)
411
+ ):
412
+ event.stop()
413
+ event.prevent_default()
414
+ return
415
+ if event.key in _HISTORY_NEXT_KEYS and self._history.index != -1 and not self._suggestions_active:
416
+ last_line = self.document.line_count - 1
417
+ if self.cursor_location[0] == last_line:
418
+ if await self._browse_history(1):
419
+ event.stop()
420
+ event.prevent_default()
421
+ return
422
+ if event.key == "ctrl+o":
423
+ event.stop()
424
+ event.prevent_default()
425
+ self.post_message(self.EditorRequested())
426
+ return
427
+ # Shell mode trigger: "!" or fullwidth "!" (U+FF01) at start of empty input
428
+ if event.character in ("!", "\uff01") and not self.text and self.cursor_location == (0, 0):
429
+ event.stop()
430
+ event.prevent_default()
431
+ self.post_message(self.ShellModeRequested())
432
+ return
433
+ if event.key == "escape":
434
+ self.post_message(self.ShellModeCancelled())
435
+ if event.key in ("ctrl+j", "shift+enter"):
436
+ # Insert newline
437
+ event.stop()
438
+ event.prevent_default()
439
+ start, end = self.selection
440
+ if self._replace_via_keyboard("\n", start, end):
441
+ self.scroll_cursor_visible()
442
+ return
443
+ if event.key == "enter":
444
+ # Submit (suppressed when read_only / locked)
445
+ event.stop()
446
+ event.prevent_default()
447
+ if self.read_only:
448
+ return
449
+ text = self.text.strip()
450
+ if text or self._suggestions_active:
451
+ self._reset_history_browsing()
452
+ self.post_message(self.Submitted(text))
453
+ return
454
+ await super()._on_key(event)
455
+ # After character insertion, check for /, @, #, $ triggers.
456
+ # Accept fullwidth variants (U+FF0F, U+FF20, U+FF03, U+FF04) for CJK keyboards.
457
+ if event.character in ("/", "\uff0f") and self.text in ("/", "\uff0f") and self.cursor_location == (0, 1):
458
+ self.post_message(self.SlashTrigger())
459
+ elif event.character in ("@", "\uff20") and (self.text.endswith("@") or self.text.endswith("\uff20")):
460
+ # Trigger after whitespace/start/CJK: "@", "some text @", or "你好@".
461
+ txt = self.text
462
+ at_pos = len(txt) - 1
463
+ if _is_file_trigger_boundary(txt, at_pos):
464
+ self.post_message(self.FileTrigger())
465
+ elif event.character in ("#", "\uff03") and self.text in ("#", "\uff03") and self.cursor_location == (0, 1):
466
+ self.post_message(self.AgentTrigger())
467
+ elif event.character in ("$", "\uff04") and self.text in ("$", "\uff04") and self.cursor_location == (0, 1):
468
+ self.post_message(self.ModelTrigger())
469
+
470
+ def add_to_history(self, text: str, *, session_id: str | None = None) -> None:
471
+ """Append *text* to input history (deduplicates consecutive)."""
472
+ self._history.append(text)
473
+ self._history_writer.enqueue(
474
+ text,
475
+ session_id=session_id,
476
+ instance_id=self._instance_id,
477
+ cwd=self._paste_cwd,
478
+ )
479
+
480
+ async def on_unmount(self) -> None:
481
+ """Best-effort drain persisted prompt history during TUI shutdown."""
482
+ await self._history_writer.close()
483
+
484
+ async def load_prompt_history(self, *, max_entries: int) -> list[str]:
485
+ """Load global MRU entries after making this instance's queued writes visible."""
486
+ await self._history_writer.flush()
487
+ return await asyncio.to_thread(load_history, max_entries=max_entries)
488
+
489
+
490
+ class _InputBarButton(NonSelectableTextMixin, Button):
491
+ """Input action button excluded from terminal drag selection and copy."""
492
+
493
+
494
+ class InputBar(Widget):
495
+ """Multi-line input bar with Send/Stop button.
496
+
497
+ - Enter or clicking Send submits the message.
498
+ - Ctrl+J inserts a newline for multi-line input.
499
+ - When agent is running, button becomes Stop (fires ``InterruptRequested``).
500
+ """
501
+
502
+ DEFAULT_CSS = """
503
+ InputBar {
504
+ height: auto;
505
+ max-height: 9;
506
+ padding: 0;
507
+ }
508
+ InputBar > Horizontal {
509
+ height: auto;
510
+ max-height: 9;
511
+ border: round $tui-border-accent $border-opacity;
512
+ }
513
+ InputBar > Horizontal > #editor-btn {
514
+ width: 3;
515
+ min-width: 3;
516
+ height: 1;
517
+ border: none;
518
+ padding: 0;
519
+ content-align: center middle;
520
+ background: transparent;
521
+ color: $accent;
522
+ text-style: bold;
523
+ }
524
+ InputBar > Horizontal > #editor-btn:hover,
525
+ InputBar > Horizontal > #editor-btn:focus,
526
+ InputBar > Horizontal > #editor-btn.-active {
527
+ background: $foreground;
528
+ color: $background;
529
+ }
530
+ InputBar > Horizontal > _ChatTextArea {
531
+ width: 1fr;
532
+ min-width: 4;
533
+ height: auto;
534
+ max-height: 7;
535
+ background: $foreground 4%;
536
+ padding: 0 0 0 1;
537
+ scrollbar-background: $foreground 12%;
538
+ scrollbar-background-hover: $foreground 16%;
539
+ scrollbar-background-active: $foreground 20%;
540
+ }
541
+ InputBar > Horizontal > #btn-group {
542
+ width: auto;
543
+ height: 1;
544
+ align: left middle;
545
+ padding: 0 1;
546
+ }
547
+ InputBar > Horizontal > #btn-group > Button {
548
+ height: 1;
549
+ border: none;
550
+ padding: 0 1;
551
+ background: $accent;
552
+ color: $button-color-foreground;
553
+ }
554
+ InputBar > Horizontal > #btn-group > #send-btn {
555
+ width: auto;
556
+ min-width: 0;
557
+ }
558
+ InputBar > Horizontal > #btn-group > Button:hover,
559
+ InputBar > Horizontal > #btn-group > Button:focus,
560
+ InputBar > Horizontal > #btn-group > Button.-active {
561
+ background: $foreground;
562
+ color: $background;
563
+ }
564
+ InputBar > Horizontal > #btn-group > #send-btn.-stop {
565
+ background: $error;
566
+ }
567
+ InputBar > Horizontal > #btn-group > #new-btn {
568
+ visibility: hidden;
569
+ width: 0;
570
+ min-width: 0;
571
+ margin: 0;
572
+ }
573
+ InputBar.-locked > Horizontal > #editor-btn {
574
+ color: $text-muted;
575
+ }
576
+ InputBar.-locked > Horizontal > _ChatTextArea {
577
+ color: $text-muted;
578
+ }
579
+ """
580
+
581
+ agent_running: reactive[bool] = reactive(False)
582
+ agent_loading: reactive[bool] = reactive(False)
583
+ locked: reactive[bool] = reactive(False)
584
+ has_messages: reactive[bool] = reactive(False)
585
+ shell_mode: reactive[bool] = reactive(False)
586
+ # ``_retry_label`` is a semantic plain attribute, so label-only transitions
587
+ # (e.g. Retry -> Continue while retry_mode stays True) reach the interaction
588
+ # refresh via the always-firing watcher; the refresh itself dedupes.
589
+ retry_mode: reactive[bool] = reactive(False, always_update=True)
590
+ retry_pending: reactive[bool] = reactive(False)
591
+ _retry_label: InputLabel
592
+
593
+ class UserSubmitted(Message):
594
+ """Posted when the user submits text."""
595
+
596
+ def __init__(self, text: str) -> None:
597
+ self.text = text
598
+ super().__init__()
599
+
600
+ class InterruptRequested(Message):
601
+ """Posted when the user clicks Stop."""
602
+
603
+ class RetryRequested(Message):
604
+ """Posted when the user clicks Retry / Continue.
605
+
606
+ Carries the current input-bar text (may be empty). When
607
+ non-empty, the engine uses it as the mid-turn continuation
608
+ prompt instead of the default ``"continue"`` placeholder.
609
+ """
610
+
611
+ def __init__(self, text: str = "") -> None:
612
+ self.text = text
613
+ super().__init__()
614
+
615
+ class ShellModeRequested(Message):
616
+ """Posted when the user requests shell mode from the input."""
617
+
618
+ class EditorRequested(Message):
619
+ """Posted when the user requests the full-screen message editor."""
620
+
621
+ class NewSessionRequested(Message):
622
+ """Posted when the user clicks New."""
623
+
624
+ class SlashTriggered(Message):
625
+ """Posted when ``/`` is typed at start of empty input."""
626
+
627
+ class FileTriggered(Message):
628
+ """Posted when ``@`` is typed at a file-mention boundary."""
629
+
630
+ class AgentTriggered(Message):
631
+ """Posted when ``#`` is typed at start of empty input."""
632
+
633
+ class ModelTriggered(Message):
634
+ """Posted when ``$`` is typed at start of empty input."""
635
+
636
+ class SuggestionNavigate(Message):
637
+ """Arrow key pressed while suggestions are active."""
638
+
639
+ def __init__(self, direction: str) -> None:
640
+ self.direction = direction
641
+ super().__init__()
642
+
643
+ class SuggestionSelect(Message):
644
+ """Tab or Enter pressed while suggestions are active."""
645
+
646
+ def __init__(self, execute: bool = False) -> None:
647
+ self.execute = execute
648
+ super().__init__()
649
+
650
+ class SuggestionDismiss(Message):
651
+ """Escape pressed while suggestions are active."""
652
+
653
+ class TextChanged(Message):
654
+ """Text changed while suggestions are active."""
655
+
656
+ def __init__(self, text: str) -> None:
657
+ self.text = text
658
+ super().__init__()
659
+
660
+ class LockedChanged(Message):
661
+ """Posted when queued-injection locking changes."""
662
+
663
+ def __init__(self, locked: bool) -> None:
664
+ self.locked = locked
665
+ super().__init__()
666
+
667
+ def __init__(
668
+ self,
669
+ placeholder: str | Content | None = None,
670
+ *,
671
+ locale_controller: LocaleController | None = None,
672
+ **kwargs: Any,
673
+ ) -> None:
674
+ self._draft_revision = 0
675
+ self._locale_controller = locale_controller
676
+ self._uses_default_placeholder = placeholder is None
677
+ self._placeholder = placeholder if placeholder is not None else _build_prompt_placeholder(self._localizer())
678
+ self._suggestions_active: bool = False
679
+ # Session restore/workspace updates replace this before normal use; this covers early paste during startup.
680
+ self._paste_cwd: str = safe_getcwd()
681
+ self._clipboard_image_dir: Path | None = None
682
+ self._spin_timer: Timer | None = None
683
+ self._lock_label: InputLabel = ""
684
+ self._lock_stop_style: bool = False
685
+ self._lock_spin_prompt: bool = False
686
+ self._interaction_signature: tuple[object, ...] | None = None
687
+ self._button_geometry_dirty = False
688
+ self._retry_label = INPUT_CONTINUE.bind()
689
+ self._inject_placeholder = _build_inject_placeholder(self._localizer())
690
+ super().__init__(**kwargs)
691
+
692
+ def set_suggestions_active(self, active: bool, mode: str = "") -> None:
693
+ """Set suggestion-active state on both InputBar and _ChatTextArea."""
694
+ self._suggestions_active = active
695
+ self._suggestion_mode = mode
696
+ self.query_one("#chat-input", _ChatTextArea)._suggestions_active = active
697
+
698
+ @property
699
+ def suggestions_active(self) -> bool:
700
+ """Whether the completion suggestion list is currently using input keys."""
701
+ return self._suggestions_active
702
+
703
+ def set_paste_cwd(self, cwd: str) -> None:
704
+ """Set the cwd used to resolve relative pasted/dropped image paths."""
705
+ self._paste_cwd = cwd
706
+ if self.is_mounted:
707
+ self.query_one("#chat-input", _ChatTextArea).set_paste_cwd(cwd)
708
+
709
+ def set_clipboard_image_dir(self, directory: str | Path | None) -> None:
710
+ """Set the session-scoped directory used for clipboard image attachments."""
711
+ self._clipboard_image_dir = Path(directory) if directory is not None else None
712
+ if self.is_mounted:
713
+ self.query_one("#chat-input", _ChatTextArea).set_clipboard_image_dir(self._clipboard_image_dir)
714
+
715
+ def compose(self) -> ComposeResult:
716
+ with Horizontal():
717
+ yield _InputBarButton(Text(">"), id="editor-btn")
718
+ yield _ChatTextArea(
719
+ id="chat-input",
720
+ compact=True,
721
+ soft_wrap=True,
722
+ show_line_numbers=False,
723
+ )
724
+ with Horizontal(id="btn-group"):
725
+ yield _InputBarButton(Text(self._render_label(INPUT_SEND.bind())), id="send-btn")
726
+ yield _InputBarButton(
727
+ Text(self._render_label(INPUT_NEW.bind())),
728
+ id="new-btn",
729
+ variant="primary",
730
+ flat=True,
731
+ tooltip=Text(self._render_label(INPUT_NEW_DESCRIPTION.bind())),
732
+ )
733
+
734
+ def on_mount(self) -> None:
735
+ if self._locale_controller is not None:
736
+ self._locale_controller.register_surface(self)
737
+ ta = self.query_one("#chat-input", _ChatTextArea)
738
+ ta.set_paste_cwd(self._paste_cwd)
739
+ ta.set_clipboard_image_dir(self._clipboard_image_dir)
740
+ ta.placeholder = self._placeholder
741
+ ta.set_right_placeholder(self._render_newline_hint())
742
+ self.focus_input()
743
+ # Pin the group to the current visible button widths. Subsequent state
744
+ # changes remap only this fixed InputBar row, never MainScreen layout.
745
+ self._set_btn_label(self._render_label(INPUT_SEND.bind()), defer_geometry=True)
746
+ self._refresh_interaction_state()
747
+
748
+ def on_unmount(self) -> None:
749
+ if self._locale_controller is not None:
750
+ self._locale_controller.unregister_surface(self)
751
+
752
+ def refresh_localization(self) -> None:
753
+ """Retranslate placeholders and semantic button state in place."""
754
+ if self._uses_default_placeholder:
755
+ self._placeholder = _build_prompt_placeholder(self._localizer())
756
+ self._inject_placeholder = _build_inject_placeholder(self._localizer())
757
+ self._interaction_signature = None
758
+ if not self.is_mounted:
759
+ return
760
+ text_area = self.query_one("#chat-input", _ChatTextArea)
761
+ text_area.placeholder = self._inject_placeholder if self.agent_running else self._placeholder
762
+ text_area.set_right_placeholder(self._render_newline_hint())
763
+ self._set_btn_label(
764
+ self._render_label(INPUT_NEW.bind()),
765
+ button_id="new-btn",
766
+ defer_geometry=True,
767
+ )
768
+ self.query_one("#new-btn", Button).tooltip = Text(self._render_label(INPUT_NEW_DESCRIPTION.bind()))
769
+ self._refresh_interaction_state()
770
+
771
+ def _localizer(self) -> Localizer | None:
772
+ controller = self._locale_controller
773
+ if controller is None:
774
+ return None
775
+ return controller.localizer
776
+
777
+ def _render_label(self, label: InputLabel) -> str:
778
+ if isinstance(label, str):
779
+ return label
780
+ localizer = self._localizer()
781
+ if localizer is None:
782
+ return format_message(label)
783
+ return render_str(localizer, label)
784
+
785
+ def _render_newline_hint(self) -> str:
786
+ return self._render_label(_INPUT_NEWLINE_HINT.bind(shortcut="Ctrl+J"))
787
+
788
+ def _update_new_btn_visibility(self) -> None:
789
+ """Show New when Chat has history and execution is idle."""
790
+ btn = self.query_one("#new-btn", Button)
791
+ # Admission can outlive an interrupted run's unwind. Reflect the
792
+ # navigation guard instead of leaving a seemingly actionable button.
793
+ btn.disabled = self.retry_pending
794
+ visible = self.has_messages and not self.retry_mode and not self.agent_running and not self.agent_loading
795
+ width = Scalar.from_number(Text(str(btn.label)).cell_len + 4 if visible else 0)
796
+ changed = btn.styles.set_rule("width", width)
797
+ changed = btn.styles.set_rule("min_width", width) or changed
798
+ changed = btn.styles.set_rule("margin", Spacing(0, 0, 0, 1 if visible else 0)) or changed
799
+ changed = btn.styles.set_rule("padding", Spacing(0, 1, 0, 1) if visible else Spacing.all(0)) or changed
800
+ changed = btn.styles.set_rule("visibility", "visible" if visible else "hidden") or changed
801
+ if not visible and btn.has_focus:
802
+ btn.blur()
803
+ if changed:
804
+ self._mark_button_geometry_dirty()
805
+ self._flush_button_geometry()
806
+
807
+ def watch_agent_running(self, running: bool) -> None:
808
+ if running:
809
+ self.retry_mode = False
810
+ self._refresh_interaction_state()
811
+
812
+ def watch_agent_loading(self, loading: bool) -> None:
813
+ self._refresh_interaction_state()
814
+
815
+ def watch_locked(self, locked: bool) -> None:
816
+ self.post_message(self.LockedChanged(locked))
817
+ self._refresh_interaction_state()
818
+
819
+ def watch_retry_mode(self, retry: bool) -> None:
820
+ self._refresh_interaction_state()
821
+
822
+ def watch_retry_pending(self, pending: bool) -> None:
823
+ self._refresh_interaction_state()
824
+
825
+ def _set_btn_label(
826
+ self,
827
+ label: str,
828
+ *,
829
+ button_id: str = "send-btn",
830
+ defer_geometry: bool = False,
831
+ ) -> None:
832
+ """Set a cell-aware natural label width without escalating screen layout."""
833
+ btn = self.query_one(f"#{button_id}", Button)
834
+ if str(btn.label) != label:
835
+ btn.label = Text(label)
836
+ changed = False
837
+ if button_id == "send-btn":
838
+ width = Scalar.from_number(Text(label).cell_len + 4)
839
+ changed = btn.styles.set_rule("width", width)
840
+ changed = btn.styles.set_rule("min_width", width) or changed
841
+ else:
842
+ changed = True
843
+ if changed:
844
+ self._mark_button_geometry_dirty()
845
+ if not defer_geometry:
846
+ self._flush_button_geometry()
847
+
848
+ def _mark_button_geometry_dirty(self) -> None:
849
+ """Invalidate only the InputBar row affected by button geometry."""
850
+ self._button_geometry_dirty = True
851
+ if self.is_mounted:
852
+ group = self.query_one("#btn-group", Horizontal)
853
+ self._pin_button_group_width(group)
854
+ group._clear_arrangement_cache()
855
+ row = group.parent
856
+ if isinstance(row, Widget):
857
+ row._clear_arrangement_cache()
858
+
859
+ def _flush_button_geometry(self) -> None:
860
+ """Apply cache-local geometry, deferring while an overlay is active."""
861
+ if not self._button_geometry_dirty or not self.is_mounted or self.app.screen is not self.screen:
862
+ return
863
+ group = self.query_one("#btn-group", Horizontal)
864
+ group._clear_arrangement_cache()
865
+ row = group.parent
866
+ if not isinstance(row, Widget):
867
+ return
868
+ row._clear_arrangement_cache()
869
+ resync_compositor_regions(row)
870
+ self._button_geometry_dirty = False
871
+
872
+ def _pin_button_group_width(self, group: Horizontal) -> None:
873
+ """Content-size the group from its currently visible buttons."""
874
+ send = self.query_one("#send-btn", Button)
875
+ new = self.query_one("#new-btn", Button)
876
+ send_width = Text(str(send.label)).cell_len + 4
877
+ new_visible = new.styles.visibility == "visible"
878
+ new_width = Text(str(new.label)).cell_len + 4 if new_visible else 0
879
+ inter_button_margin = 1 if new_visible else 0
880
+ width = send_width + new_width + inter_button_margin + _BUTTON_GROUP_HORIZONTAL_PADDING
881
+ group.styles.set_rule("width", Scalar.from_number(width))
882
+
883
+ def sync_deferred_button_geometry(self) -> None:
884
+ """Apply geometry deferred while an overlay covered the owning screen."""
885
+ self._flush_button_geometry()
886
+
887
+ def _set_prompt_spinner(self, active: bool) -> None:
888
+ if active:
889
+ if self._spin_timer is None:
890
+ self._spin_timer = self.set_interval(0.08, self._spin_prompt)
891
+ return
892
+ if self._spin_timer is not None:
893
+ self._spin_timer.stop()
894
+ self._spin_timer = None
895
+ self.query_one("#editor-btn", Button).label = Text(">")
896
+
897
+ def _request_editor(self) -> None:
898
+ """Request the prompt editor through the shared keyboard/button path."""
899
+ if self.locked or self.shell_mode:
900
+ return
901
+ # A mouse press focuses the prompt button before this handler runs.
902
+ # Restore the chat input first so closing the editor returns focus and
903
+ # its cursor to the draft instead of leaving focus on the button.
904
+ self.focus_input()
905
+ if self._suggestions_active:
906
+ self.post_message(self.SuggestionDismiss())
907
+ self.post_message(self.EditorRequested())
908
+
909
+ def _refresh_interaction_state(self) -> None:
910
+ """Derive the complete input/button state from the current mode flags."""
911
+ ta = self.query_one("#chat-input", _ChatTextArea)
912
+ btn = self.query_one("#send-btn", Button)
913
+ self.query_one("#editor-btn", Button).disabled = self.locked or self.shell_mode
914
+
915
+ signature = (
916
+ self.locked,
917
+ self._lock_label,
918
+ self._lock_stop_style,
919
+ self._lock_spin_prompt,
920
+ self.agent_running,
921
+ self.agent_loading,
922
+ self.retry_mode,
923
+ self.retry_pending,
924
+ self._retry_label,
925
+ bool(ta.text.strip()),
926
+ self.has_messages,
927
+ self.shell_mode,
928
+ )
929
+ if signature == self._interaction_signature:
930
+ return
931
+ self._interaction_signature = signature
932
+
933
+ # A hard lock (queued injection) takes precedence over startup
934
+ # loading if both flags are ever set.
935
+ if self.locked:
936
+ self.add_class("-locked")
937
+ # ``read_only`` blocks edits; ``disabled`` also suppresses cursor
938
+ # movement and history navigation while the queued injection is pending.
939
+ ta.read_only = True
940
+ ta.disabled = True
941
+ self._set_btn_label(self._render_label(self._lock_label or INPUT_QUEUED.bind()), defer_geometry=True)
942
+ btn.set_class(self._lock_stop_style, "-stop")
943
+ btn.disabled = True
944
+ self._set_prompt_spinner(self._lock_spin_prompt)
945
+ self._update_new_btn_visibility()
946
+ return
947
+
948
+ self.remove_class("-locked")
949
+ ta.disabled = False
950
+ ta.read_only = False
951
+ ta.show_cursor = True
952
+ self._set_prompt_spinner(False)
953
+
954
+ if self.agent_running:
955
+ self._set_btn_label(self._render_label(INPUT_INTERRUPT.bind()), defer_geometry=True)
956
+ btn.add_class("-stop")
957
+ btn.disabled = False
958
+ ta.placeholder = self._inject_placeholder
959
+ else:
960
+ btn.remove_class("-stop")
961
+ ta.placeholder = self._placeholder
962
+ retry = self.retry_mode
963
+ label = self._retry_label if retry else INPUT_SEND.bind()
964
+ self._set_btn_label(self._render_label(label), defer_geometry=True)
965
+ btn.disabled = self.retry_pending or self.agent_loading or (not retry and not bool(ta.text.strip()))
966
+
967
+ self._update_new_btn_visibility()
968
+
969
+ def watch_has_messages(self, _has_messages: bool) -> None:
970
+ self._refresh_interaction_state()
971
+
972
+ def watch_shell_mode(self, shell_mode: bool) -> None:
973
+ ta = self.query_one("#chat-input", _ChatTextArea)
974
+ if not shell_mode:
975
+ # Restore normal state when exiting shell mode
976
+ ta.clear()
977
+ self._refresh_interaction_state()
978
+
979
+ def on__chat_text_area_shell_mode_requested(self, event: _ChatTextArea.ShellModeRequested) -> None:
980
+ event.stop()
981
+ if self.agent_running or self.locked:
982
+ return
983
+ self.post_message(self.ShellModeRequested())
984
+
985
+ def on__chat_text_area_editor_requested(self, event: _ChatTextArea.EditorRequested) -> None:
986
+ """Forward editor requests when the chat draft remains writable."""
987
+ event.stop()
988
+ self._request_editor()
989
+
990
+ def on__chat_text_area_shell_mode_cancelled(self, event: _ChatTextArea.ShellModeCancelled) -> None:
991
+ event.stop()
992
+ if self._suggestions_active:
993
+ self.post_message(self.SuggestionDismiss())
994
+
995
+ def on__chat_text_area_slash_trigger(self, event: _ChatTextArea.SlashTrigger) -> None:
996
+ event.stop()
997
+ if self.locked or self.shell_mode:
998
+ return
999
+ self.post_message(self.SlashTriggered())
1000
+
1001
+ def on__chat_text_area_file_trigger(self, event: _ChatTextArea.FileTrigger) -> None:
1002
+ event.stop()
1003
+ if self.locked or self.shell_mode:
1004
+ return
1005
+ self.post_message(self.FileTriggered())
1006
+
1007
+ def on__chat_text_area_agent_trigger(self, event: _ChatTextArea.AgentTrigger) -> None:
1008
+ event.stop()
1009
+ if self.agent_running or self.agent_loading or self.locked or self.shell_mode:
1010
+ return
1011
+ self.post_message(self.AgentTriggered())
1012
+
1013
+ def on__chat_text_area_model_trigger(self, event: _ChatTextArea.ModelTrigger) -> None:
1014
+ event.stop()
1015
+ # Same four guards the status-bar selectors use: a profile switch that
1016
+ # lands mid-load is dropped downstream, after the draft is already gone.
1017
+ if self.agent_running or self.agent_loading or self.locked or self.shell_mode:
1018
+ return
1019
+ self.post_message(self.ModelTriggered())
1020
+
1021
+ def on__chat_text_area_submitted(self, event: _ChatTextArea.Submitted) -> None:
1022
+ """Forward Enter-submit, selecting the active suggestion when applicable."""
1023
+ event.stop()
1024
+ if not self.display:
1025
+ return
1026
+ if self._suggestions_active and self._suggestion_mode == "history":
1027
+ self.post_message(self.SuggestionSelect(execute=True))
1028
+ return
1029
+ if self.retry_pending:
1030
+ return
1031
+ if self.agent_loading:
1032
+ self._refresh_interaction_state()
1033
+ return
1034
+ if self._suggestions_active:
1035
+ if self._suggestion_mode in ("files", "agents", "models", "commands", "subcommands"):
1036
+ self.post_message(self.SuggestionSelect(execute=True))
1037
+ return
1038
+ self.post_message(self.SuggestionDismiss())
1039
+ # In retry_mode a plain Enter-submit (no slash command) is treated
1040
+ # the same as clicking Continue/Retry: the typed text rides along
1041
+ # as a mid-turn note rather than starting a brand-new turn.
1042
+ # Slash commands always route through ``UserSubmitted`` so the
1043
+ # screen's dispatcher handles them.
1044
+ is_slash_command = is_slash_command_candidate(event.text)
1045
+ if self.retry_mode and not self.agent_running and not is_slash_command:
1046
+ text = event.text.strip()
1047
+ self.post_message(self.RetryRequested(text=text))
1048
+ self.query_one("#chat-input", _ChatTextArea).clear()
1049
+ self._refresh_interaction_state()
1050
+ return
1051
+ self.post_message(self.UserSubmitted(event.text))
1052
+ if not self.agent_running or is_slash_command:
1053
+ self.query_one("#chat-input", _ChatTextArea).clear()
1054
+ self._refresh_interaction_state()
1055
+
1056
+ def on_text_area_changed(self, event: TextArea.Changed) -> None:
1057
+ """Forward text changes when suggestions are active."""
1058
+ self._draft_revision += 1
1059
+ if self._suggestions_active:
1060
+ self.post_message(self.TextChanged(event.text_area.text))
1061
+ self._refresh_interaction_state()
1062
+
1063
+ def on_key(self, event) -> None:
1064
+ """Intercept arrow keys and Tab when suggestions are active."""
1065
+ if not self._suggestions_active:
1066
+ return
1067
+ if event.key == "up":
1068
+ event.stop()
1069
+ event.prevent_default()
1070
+ self.post_message(self.SuggestionNavigate(direction="up"))
1071
+ elif event.key == "down":
1072
+ event.stop()
1073
+ event.prevent_default()
1074
+ self.post_message(self.SuggestionNavigate(direction="down"))
1075
+ elif event.key == "tab":
1076
+ event.stop()
1077
+ event.prevent_default()
1078
+ self.post_message(self.SuggestionSelect())
1079
+
1080
+ def on_button_pressed(self, event: Button.Pressed) -> None:
1081
+ event.stop()
1082
+ if not self.display:
1083
+ return
1084
+ if event.button.id == "editor-btn":
1085
+ self._request_editor()
1086
+ return
1087
+ if self.locked or self.retry_pending:
1088
+ return # keep the admission owner stable; draft editing stays available
1089
+ if self.agent_loading:
1090
+ return
1091
+ if event.button.id == "new-btn":
1092
+ self.post_message(self.NewSessionRequested())
1093
+ return
1094
+ if self.agent_running:
1095
+ self.post_message(self.InterruptRequested())
1096
+ elif self.retry_mode:
1097
+ # In retry mode the button always resumes the current turn.
1098
+ # Any text the user typed rides along as a mid-turn note;
1099
+ # the engine will use it as the continuation prompt in place
1100
+ # of the default ``"continue"`` placeholder.
1101
+ self.post_message(self.RetryRequested(text=self.consume_retry_text()))
1102
+ else:
1103
+ self._do_submit()
1104
+
1105
+ def _do_submit(self) -> None:
1106
+ """Submit text from the text area."""
1107
+ if not self.display:
1108
+ return
1109
+ if self.agent_loading or self.retry_pending:
1110
+ self._refresh_interaction_state()
1111
+ return
1112
+ ta = self.query_one("#chat-input", _ChatTextArea)
1113
+ text = ta.text.strip()
1114
+ if text:
1115
+ ta._reset_history_browsing()
1116
+ self.post_message(self.UserSubmitted(text))
1117
+ ta.clear()
1118
+ self._refresh_interaction_state()
1119
+
1120
+ def _lock(self, label: InputLabel, *, stop_style: bool, spin_prompt: bool = True) -> None:
1121
+ """Lock the input bar — text stays visible, input becomes read-only."""
1122
+ self._lock_label = label
1123
+ self._lock_stop_style = stop_style
1124
+ self._lock_spin_prompt = spin_prompt
1125
+ self.locked = True
1126
+ self._refresh_interaction_state()
1127
+
1128
+ def lock_with_text(self) -> None:
1129
+ """Lock while a typed injection is queued for a running agent."""
1130
+ self._lock(INPUT_QUEUED.bind(), stop_style=True)
1131
+
1132
+ def _spin_prompt(self) -> None:
1133
+ """Update the prompt indicator with a spinner frame."""
1134
+ frame = _SPINNER_FRAMES[int(time() * 8) % len(_SPINNER_FRAMES)]
1135
+ self.query_one("#editor-btn", Button).label = Text(frame)
1136
+
1137
+ def _unlock(self) -> None:
1138
+ """Common unlock logic."""
1139
+ self.locked = False
1140
+ self._lock_label = ""
1141
+ self._lock_stop_style = False
1142
+ self._lock_spin_prompt = False
1143
+ self._refresh_interaction_state()
1144
+
1145
+ def unlock_and_clear(self) -> None:
1146
+ """Unlock and clear the text — injection was consumed."""
1147
+ self._unlock()
1148
+ ta = self.query_one("#chat-input", _ChatTextArea)
1149
+ ta.clear()
1150
+ self.focus_input()
1151
+ self._refresh_interaction_state()
1152
+
1153
+ def unlock_and_keep(self) -> None:
1154
+ """Unlock but keep the text — injection was abandoned."""
1155
+ self._unlock()
1156
+ self.focus_input()
1157
+ self._refresh_interaction_state()
1158
+
1159
+ def focus_input(self) -> None:
1160
+ """Focus the visible composer without activating a hidden Chat draft."""
1161
+ if self.display:
1162
+ self.query_one("#chat-input", _ChatTextArea).focus()
1163
+
1164
+ def snapshot_draft(self) -> InputDraftSnapshot:
1165
+ """Capture the current chat draft and cursor without mutating them."""
1166
+ text_area = self.query_one("#chat-input", _ChatTextArea)
1167
+ return InputDraftSnapshot(text_area.text, text_area.cursor_location, self._draft_revision)
1168
+
1169
+ @property
1170
+ def draft_revision(self) -> int:
1171
+ """Return the monotonic generation of the underlying chat draft."""
1172
+ return self._draft_revision
1173
+
1174
+ def restore_draft(self, text: str) -> bool:
1175
+ """Hand *text* back to an empty composer; return whether it was written.
1176
+
1177
+ Every caller reaches this from a path that already consumed the draft —
1178
+ a submit, a retry, a rollback, a rejection — and the hand-back travels
1179
+ as a message, so the user may have started typing before it lands.
1180
+ ``replace_draft`` would destroy that draft and move the cursor, which
1181
+ also breaks an in-progress IME composition, so a draft that is already
1182
+ there wins and the caller is told nothing was restored. Whitespace is
1183
+ such a draft: an indent, a newline or a single space is something the
1184
+ user typed, and no consume path can leave one behind — a whitespace-only
1185
+ composer never submits.
1186
+ """
1187
+ if not text:
1188
+ return False
1189
+ if self.query_one("#chat-input", _ChatTextArea).text:
1190
+ return False
1191
+ self.replace_draft(text)
1192
+ return True
1193
+
1194
+ def replace_draft(self, text: str) -> None:
1195
+ """Replace the draft, move its cursor to the end, and refresh controls."""
1196
+ self._draft_revision += 1
1197
+ text_area = self.query_one("#chat-input", _ChatTextArea)
1198
+ text_area._reset_history_browsing()
1199
+ text_area.clear()
1200
+ if text:
1201
+ text_area.insert(text)
1202
+ self._refresh_interaction_state()
1203
+
1204
+ def can_commit_editor_draft(self) -> bool:
1205
+ """Return whether an open editor may currently replace this draft."""
1206
+ return not self.locked and not self.shell_mode
1207
+
1208
+ def insert_image_paste_payload(self, text: str) -> bool:
1209
+ """Insert a pasted/dropped image path payload into the chat input.
1210
+
1211
+ Returns ``True`` only when *text* was recognized as supported image
1212
+ path input and inserted. Normal prose/code pastes are left for the
1213
+ original event target to handle.
1214
+ """
1215
+ # MainScreen also guards shell mode; keep this local guard for direct InputBar callers.
1216
+ if self.shell_mode:
1217
+ return False
1218
+ ta = self.query_one("#chat-input", _ChatTextArea)
1219
+ if ta.read_only:
1220
+ return False
1221
+ try:
1222
+ prepared = ta._prepare_image_path_paste_text(text)
1223
+ except OSError, RuntimeError, ValueError:
1224
+ return False
1225
+ if not prepared:
1226
+ return False
1227
+ # This fallback handles path-only image payloads; normal large text pastes stay on the original target.
1228
+ result = ta._replace_via_keyboard(prepared, *ta.selection)
1229
+ if not result:
1230
+ return False
1231
+ ta.move_cursor(result.end_location)
1232
+ self.focus_input()
1233
+ self._refresh_interaction_state()
1234
+ return True
1235
+
1236
+ def insert_clipboard_image_paste(self) -> bool:
1237
+ """Insert the current platform clipboard bitmap into the chat input."""
1238
+ # MainScreen also guards shell mode; keep this local guard for direct InputBar callers.
1239
+ if self.shell_mode:
1240
+ return False
1241
+ ta = self.query_one("#chat-input", _ChatTextArea)
1242
+ if ta.read_only:
1243
+ return False
1244
+ if not ta._insert_clipboard_image_paste():
1245
+ return False
1246
+ self.focus_input()
1247
+ self._refresh_interaction_state()
1248
+ return True
1249
+
1250
+ def replace_trigger_text(self, trigger: str, replacement: str) -> None:
1251
+ """Replace the trigger token (e.g. ``/query`` or ``@query``) with *replacement*.
1252
+
1253
+ Finds the last occurrence of *trigger* character in the text and replaces
1254
+ everything from that position to the cursor with *replacement*.
1255
+ Also checks for fullwidth CJK equivalents (e.g. U+FF20 for ``@``).
1256
+ """
1257
+ ta = self.query_one("#chat-input", _ChatTextArea)
1258
+ text = ta.text
1259
+ idx = text.rfind(trigger)
1260
+ # Check fullwidth variant for CJK keyboard support (ASCII+0xFEE0)
1261
+ if idx < 0 and len(trigger) == 1 and 0x21 <= ord(trigger) <= 0x7E:
1262
+ idx = text.rfind(chr(ord(trigger) + 0xFEE0))
1263
+ if idx < 0:
1264
+ return
1265
+ before = text[:idx]
1266
+ ta.clear()
1267
+ new_text = before + replacement
1268
+ if new_text:
1269
+ ta.insert(new_text)
1270
+
1271
+ @property
1272
+ def value(self) -> str:
1273
+ return self.query_one("#chat-input", _ChatTextArea).text
1274
+
1275
+ @value.setter
1276
+ def value(self, text: str) -> None:
1277
+ self.replace_draft(text)
1278
+
1279
+ def consume_retry_text(self) -> str:
1280
+ """Return and clear the typed retry/continue note, if any."""
1281
+ ta = self.query_one("#chat-input", _ChatTextArea)
1282
+ text = ta.text.strip()
1283
+ if text:
1284
+ ta._reset_history_browsing()
1285
+ ta.clear()
1286
+ self._refresh_interaction_state()
1287
+ return text
1288
+
1289
+ def add_to_history(self, text: str, *, session_id: str | None = None) -> None:
1290
+ """Add *text* to input history."""
1291
+ self.query_one("#chat-input", _ChatTextArea).add_to_history(text, session_id=session_id)
1292
+
1293
+ async def load_prompt_history(self, *, max_entries: int) -> list[str]:
1294
+ """Load global prompt history for the suggestion popup."""
1295
+ return await self.query_one("#chat-input", _ChatTextArea).load_prompt_history(max_entries=max_entries)
1296
+
1297
+ async def action_submit(self) -> None:
1298
+ """Programmatic submit (for testing)."""
1299
+ self._do_submit()