alysis-code 0.13.0__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 (458) hide show
  1. alysis_code/__init__.py +3 -0
  2. alysis_code/__main__.py +4 -0
  3. alysis_code/_build_info.py +14 -0
  4. alysis_code/account_login.py +468 -0
  5. alysis_code/agent/README.md +35 -0
  6. alysis_code/agent/__init__.py +11 -0
  7. alysis_code/agent/acceptance_contract.py +2217 -0
  8. alysis_code/agent/blast_radius.py +1403 -0
  9. alysis_code/agent/cache_keepalive.py +227 -0
  10. alysis_code/agent/completion_certificate.py +366 -0
  11. alysis_code/agent/completion_gate.py +306 -0
  12. alysis_code/agent/empty_response_stall.py +403 -0
  13. alysis_code/agent/errors.py +28 -0
  14. alysis_code/agent/llm_calls.py +475 -0
  15. alysis_code/agent/mutation_classification.py +227 -0
  16. alysis_code/agent/prompt_context.py +2508 -0
  17. alysis_code/agent/read_ledger.py +253 -0
  18. alysis_code/agent/regression_baseline.py +642 -0
  19. alysis_code/agent/reproduction_first.py +610 -0
  20. alysis_code/agent/sensitive_output.py +629 -0
  21. alysis_code/agent/session.py +3218 -0
  22. alysis_code/agent/steering.py +191 -0
  23. alysis_code/agent/subagent_execution.py +5177 -0
  24. alysis_code/agent/subagent_workspace.py +666 -0
  25. alysis_code/agent/tools_assembly.py +4728 -0
  26. alysis_code/agent/turn/__init__.py +101 -0
  27. alysis_code/agent/turn/core.py +8483 -0
  28. alysis_code/agent/turn/events.py +113 -0
  29. alysis_code/agent/turn/exploration.py +590 -0
  30. alysis_code/agent/turn/interventions.py +65 -0
  31. alysis_code/agent/turn/read_cache.py +420 -0
  32. alysis_code/agent/turn/snapshot.py +179 -0
  33. alysis_code/agent/turn_contract.py +661 -0
  34. alysis_code/agent/turn_path.py +129 -0
  35. alysis_code/agent/verification.py +2885 -0
  36. alysis_code/agent/verification_commands.py +512 -0
  37. alysis_code/agent/verification_evidence.py +738 -0
  38. alysis_code/agent_loop.py +693 -0
  39. alysis_code/agent_runtimes/__init__.py +51 -0
  40. alysis_code/agent_runtimes/base.py +114 -0
  41. alysis_code/agent_runtimes/builtins.py +129 -0
  42. alysis_code/agent_runtimes/codex_cli.py +664 -0
  43. alysis_code/agent_runtimes/host.py +263 -0
  44. alysis_code/agent_runtimes/registry.py +64 -0
  45. alysis_code/agent_runtimes/service.py +150 -0
  46. alysis_code/agentbox_client.py +416 -0
  47. alysis_code/agentbox_integration.py +310 -0
  48. alysis_code/alysis_cloud.py +152 -0
  49. alysis_code/approval_scope.py +276 -0
  50. alysis_code/assets/README.md +33 -0
  51. alysis_code/assets/__init__.py +126 -0
  52. alysis_code/assets/asset_read_core.py +281 -0
  53. alysis_code/assets/budget_allocator.py +456 -0
  54. alysis_code/assets/comprehender.py +759 -0
  55. alysis_code/assets/index.py +654 -0
  56. alysis_code/assets/ingestion.py +275 -0
  57. alysis_code/assets/legacy_migration.py +413 -0
  58. alysis_code/assets/models.py +263 -0
  59. alysis_code/assets/ocr.py +239 -0
  60. alysis_code/assets/owl/ascii/f-000.txt +13 -0
  61. alysis_code/assets/owl/ascii/f-001.txt +13 -0
  62. alysis_code/assets/owl/ascii/f-002.txt +13 -0
  63. alysis_code/assets/owl/ascii/f-003.txt +13 -0
  64. alysis_code/assets/owl/ascii/f-004.txt +13 -0
  65. alysis_code/assets/owl/ascii/f-005.txt +13 -0
  66. alysis_code/assets/owl/ascii/f-006.txt +13 -0
  67. alysis_code/assets/owl/ascii/f-007.txt +13 -0
  68. alysis_code/assets/owl/ascii/f-008.txt +13 -0
  69. alysis_code/assets/owl/ascii/f-009.txt +13 -0
  70. alysis_code/assets/owl/ascii/f-010.txt +13 -0
  71. alysis_code/assets/owl/ascii/f-011.txt +13 -0
  72. alysis_code/assets/owl/ascii/f-012.txt +13 -0
  73. alysis_code/assets/owl/ascii/f-013.txt +13 -0
  74. alysis_code/assets/owl/ascii/f-014.txt +13 -0
  75. alysis_code/assets/owl/ascii/f-015.txt +13 -0
  76. alysis_code/assets/owl/ascii/f-016.txt +13 -0
  77. alysis_code/assets/owl/ascii/f-017.txt +13 -0
  78. alysis_code/assets/owl/ascii/f-018.txt +13 -0
  79. alysis_code/assets/owl/ascii/f-019.txt +13 -0
  80. alysis_code/assets/owl/ascii/f-020.txt +13 -0
  81. alysis_code/assets/owl/index.html +98 -0
  82. alysis_code/assets/owl/show-owl.sh +761 -0
  83. alysis_code/assets/paths.py +49 -0
  84. alysis_code/assets/plan_binding.py +326 -0
  85. alysis_code/assets/planner_context.py +466 -0
  86. alysis_code/assets/planner_tools.py +184 -0
  87. alysis_code/assets/prompts.py +101 -0
  88. alysis_code/assets/replanner_context.py +239 -0
  89. alysis_code/assets/surface.py +521 -0
  90. alysis_code/assets/untrusted_content.py +48 -0
  91. alysis_code/assets/usage_logger.py +94 -0
  92. alysis_code/assets/worker_mirror.py +428 -0
  93. alysis_code/assets/worker_section.py +303 -0
  94. alysis_code/assets/worker_tools.py +468 -0
  95. alysis_code/atomic_io.py +83 -0
  96. alysis_code/auth_diagnostics.py +272 -0
  97. alysis_code/background_runner.py +366 -0
  98. alysis_code/branding.py +270 -0
  99. alysis_code/budget_policy.py +390 -0
  100. alysis_code/build_identity.py +465 -0
  101. alysis_code/builtin_hooks/__init__.py +7 -0
  102. alysis_code/builtin_hooks/notify_done_windows.py +65 -0
  103. alysis_code/bwrap_etc.py +76 -0
  104. alysis_code/cancellation.py +41 -0
  105. alysis_code/capabilities.py +137 -0
  106. alysis_code/chatgpt_codex_static_provider.py +133 -0
  107. alysis_code/cli.py +51 -0
  108. alysis_code/cli_impl/__init__.py +1 -0
  109. alysis_code/cli_impl/assets_cli.py +537 -0
  110. alysis_code/cli_impl/assets_modal.py +412 -0
  111. alysis_code/cli_impl/chat/__init__.py +156 -0
  112. alysis_code/cli_impl/chat/commands.py +2616 -0
  113. alysis_code/cli_impl/chat/loop.py +4508 -0
  114. alysis_code/cli_impl/chat/mid_turn_policy.py +125 -0
  115. alysis_code/cli_impl/chat/rendering.py +444 -0
  116. alysis_code/cli_impl/chat/state.py +124 -0
  117. alysis_code/cli_impl/chat_resume.py +830 -0
  118. alysis_code/cli_impl/chat_slash_completer.py +258 -0
  119. alysis_code/cli_impl/commands/__init__.py +11 -0
  120. alysis_code/cli_impl/commands/_shared.py +89 -0
  121. alysis_code/cli_impl/commands/auth.py +623 -0
  122. alysis_code/cli_impl/commands/chat_resume_helpers.py +1531 -0
  123. alysis_code/cli_impl/commands/chat_state.py +158 -0
  124. alysis_code/cli_impl/commands/chat_status.py +1248 -0
  125. alysis_code/cli_impl/commands/chat_terminal.py +942 -0
  126. alysis_code/cli_impl/commands/chat_tui_panels.py +1018 -0
  127. alysis_code/cli_impl/commands/cli_common.py +1223 -0
  128. alysis_code/cli_impl/commands/cli_surface.py +77 -0
  129. alysis_code/cli_impl/commands/config.py +131 -0
  130. alysis_code/cli_impl/commands/conventions.py +85 -0
  131. alysis_code/cli_impl/commands/execution_helpers.py +350 -0
  132. alysis_code/cli_impl/commands/extensions.py +401 -0
  133. alysis_code/cli_impl/commands/forge.py +1282 -0
  134. alysis_code/cli_impl/commands/forge_asset_view.py +121 -0
  135. alysis_code/cli_impl/commands/forge_helpers.py +1215 -0
  136. alysis_code/cli_impl/commands/hooks.py +737 -0
  137. alysis_code/cli_impl/commands/ide_bridge.py +31 -0
  138. alysis_code/cli_impl/commands/mcp.py +700 -0
  139. alysis_code/cli_impl/commands/profile.py +453 -0
  140. alysis_code/cli_impl/commands/prompt_helpers.py +307 -0
  141. alysis_code/cli_impl/commands/report.py +88 -0
  142. alysis_code/cli_impl/commands/root.py +1118 -0
  143. alysis_code/cli_impl/commands/sandbox.py +184 -0
  144. alysis_code/cli_impl/commands/server.py +54 -0
  145. alysis_code/cli_impl/commands/sessions.py +252 -0
  146. alysis_code/cli_impl/commands/skills.py +404 -0
  147. alysis_code/cli_impl/commands/startup.py +946 -0
  148. alysis_code/cli_impl/commands/tools.py +335 -0
  149. alysis_code/cli_impl/commands/update.py +364 -0
  150. alysis_code/cli_impl/commands/welcome.py +972 -0
  151. alysis_code/cli_impl/config_menu.py +3882 -0
  152. alysis_code/cli_impl/forge.py +4509 -0
  153. alysis_code/cli_impl/forge_recovery.py +485 -0
  154. alysis_code/cli_impl/setup_wizard.py +2409 -0
  155. alysis_code/cli_impl/tui/__init__.py +58 -0
  156. alysis_code/cli_impl/tui/app.py +4551 -0
  157. alysis_code/cli_impl/tui/config.py +32 -0
  158. alysis_code/cli_impl/tui/config_flow.py +2754 -0
  159. alysis_code/cli_impl/tui/config_overlay.py +566 -0
  160. alysis_code/cli_impl/tui/content.py +78 -0
  161. alysis_code/cli_impl/tui/footer.py +218 -0
  162. alysis_code/cli_impl/tui/forge_status.py +136 -0
  163. alysis_code/cli_impl/tui/markdown.py +244 -0
  164. alysis_code/cli_impl/tui/owl.py +109 -0
  165. alysis_code/cli_impl/tui/plan_meta.py +477 -0
  166. alysis_code/cli_impl/tui/setup_app.py +519 -0
  167. alysis_code/cli_impl/tui/setup_flow.py +1622 -0
  168. alysis_code/cli_impl/tui/state.py +101 -0
  169. alysis_code/cli_impl/tui/subagent_identity.py +66 -0
  170. alysis_code/cli_impl/tui/subagent_panel.py +186 -0
  171. alysis_code/cli_impl/tui/surface.py +796 -0
  172. alysis_code/cli_impl/tui/transcript.py +514 -0
  173. alysis_code/cli_impl/tui/update_prompt.py +79 -0
  174. alysis_code/cli_impl/tui/workspace_guard.py +384 -0
  175. alysis_code/clipboard.py +172 -0
  176. alysis_code/code_review.py +1211 -0
  177. alysis_code/compaction/__init__.py +28 -0
  178. alysis_code/compaction/conversation_compactor.py +2932 -0
  179. alysis_code/compaction/importance.py +177 -0
  180. alysis_code/compaction/settings.py +297 -0
  181. alysis_code/compaction/tool_output_offload.py +447 -0
  182. alysis_code/config.py +3509 -0
  183. alysis_code/conflict_auto_resolver.py +895 -0
  184. alysis_code/context/__init__.py +1 -0
  185. alysis_code/context/tool_schema_budgeter.py +220 -0
  186. alysis_code/crash_diagnostics.py +282 -0
  187. alysis_code/custom_tools/README.md +34 -0
  188. alysis_code/custom_tools/__init__.py +43 -0
  189. alysis_code/custom_tools/discovery.py +903 -0
  190. alysis_code/custom_tools/runtime.py +1516 -0
  191. alysis_code/custom_tools/session.py +227 -0
  192. alysis_code/custom_tools/trust.py +232 -0
  193. alysis_code/diff_paths.py +113 -0
  194. alysis_code/direction_change.py +293 -0
  195. alysis_code/dispatch_timing.py +306 -0
  196. alysis_code/durable_service_manager.py +1236 -0
  197. alysis_code/edit_discipline.py +659 -0
  198. alysis_code/error_text.py +73 -0
  199. alysis_code/execution_budget.py +411 -0
  200. alysis_code/execution_context.py +915 -0
  201. alysis_code/execution_deadline.py +1065 -0
  202. alysis_code/execution_shared.py +1904 -0
  203. alysis_code/extensions/README.md +30 -0
  204. alysis_code/extensions/__init__.py +93 -0
  205. alysis_code/extensions/activation.py +138 -0
  206. alysis_code/extensions/install.py +1436 -0
  207. alysis_code/extensions/manifest.py +487 -0
  208. alysis_code/extensions/models.py +74 -0
  209. alysis_code/extensions/paths.py +56 -0
  210. alysis_code/extensions/registry.json +4 -0
  211. alysis_code/extensions/registry.py +52 -0
  212. alysis_code/extensions/state.py +83 -0
  213. alysis_code/extensions/workspace_trust.py +101 -0
  214. alysis_code/failed_task_evidence.py +369 -0
  215. alysis_code/failure_category.py +315 -0
  216. alysis_code/feedback_report.py +1647 -0
  217. alysis_code/file_classification.py +485 -0
  218. alysis_code/forge.py +2064 -0
  219. alysis_code/forge_completion.py +362 -0
  220. alysis_code/forge_events.py +475 -0
  221. alysis_code/frontmatter_utils.py +95 -0
  222. alysis_code/git_evidence.py +1181 -0
  223. alysis_code/git_ops.py +560 -0
  224. alysis_code/git_safe.py +62 -0
  225. alysis_code/git_worktrees.py +190 -0
  226. alysis_code/hooks/README.md +33 -0
  227. alysis_code/hooks/__init__.py +67 -0
  228. alysis_code/hooks/audit.py +171 -0
  229. alysis_code/hooks/config.py +225 -0
  230. alysis_code/hooks/dispatcher.py +1110 -0
  231. alysis_code/hooks/models.py +447 -0
  232. alysis_code/hooks/trust.py +202 -0
  233. alysis_code/host_actions.py +543 -0
  234. alysis_code/host_browser.py +103 -0
  235. alysis_code/ide/__init__.py +5 -0
  236. alysis_code/ide/activity_events.py +399 -0
  237. alysis_code/ide/approvals.py +337 -0
  238. alysis_code/ide/artifacts.py +153 -0
  239. alysis_code/ide/browser_egress_proxy.py +1076 -0
  240. alysis_code/ide/cdp_websocket_transport.py +1192 -0
  241. alysis_code/ide/change_ledger.py +1721 -0
  242. alysis_code/ide/context_blocks.py +979 -0
  243. alysis_code/ide/event_stream.py +531 -0
  244. alysis_code/ide/forge_protocol.py +3112 -0
  245. alysis_code/ide/forge_request_ledger.py +737 -0
  246. alysis_code/ide/health.py +965 -0
  247. alysis_code/ide/managed_browser.py +2251 -0
  248. alysis_code/ide/management_protocol.py +3414 -0
  249. alysis_code/ide/mcp_oauth_coordinator.py +744 -0
  250. alysis_code/ide/mcp_oauth_lifecycle.py +1504 -0
  251. alysis_code/ide/prompt_queue.py +1070 -0
  252. alysis_code/ide/protocol.py +191 -0
  253. alysis_code/ide/resumable_swarm.py +1543 -0
  254. alysis_code/ide/session_search.py +295 -0
  255. alysis_code/ide/stdio_bridge.py +9935 -0
  256. alysis_code/ide/structured_state.py +1579 -0
  257. alysis_code/ide/swarm_protocol.py +816 -0
  258. alysis_code/integration_gate.py +506 -0
  259. alysis_code/interactive_input_guard.py +39 -0
  260. alysis_code/interactive_plan_mode.py +26 -0
  261. alysis_code/internal_artifacts.py +179 -0
  262. alysis_code/knowledge_base.py +1409 -0
  263. alysis_code/knowledge_capture.py +1190 -0
  264. alysis_code/knowledge_librarian.py +605 -0
  265. alysis_code/language_policy.py +34 -0
  266. alysis_code/litellm_static_provider.py +535 -0
  267. alysis_code/llm/__init__.py +1 -0
  268. alysis_code/llm/anthropic_messages.py +2288 -0
  269. alysis_code/llm/base.py +71 -0
  270. alysis_code/llm/cache_capabilities.py +985 -0
  271. alysis_code/llm/cache_control_blocks.py +244 -0
  272. alysis_code/llm/cache_policy.py +388 -0
  273. alysis_code/llm/factory.py +373 -0
  274. alysis_code/llm/gemini_generate_content.py +2652 -0
  275. alysis_code/llm/gemini_interactions.py +739 -0
  276. alysis_code/llm/metadata.py +450 -0
  277. alysis_code/llm/openai_compat.py +2947 -0
  278. alysis_code/llm/openai_responses.py +2604 -0
  279. alysis_code/llm/protocols.py +609 -0
  280. alysis_code/llm/provider_limits.py +525 -0
  281. alysis_code/llm/request_plan.py +389 -0
  282. alysis_code/llm/request_shape.py +238 -0
  283. alysis_code/llm/streaming.py +108 -0
  284. alysis_code/llm/temperature_compat.py +78 -0
  285. alysis_code/llm/types.py +195 -0
  286. alysis_code/llm/usage_normalization.py +222 -0
  287. alysis_code/llm_error_display.py +315 -0
  288. alysis_code/logging_redaction.py +326 -0
  289. alysis_code/managed_host_deadline.py +196 -0
  290. alysis_code/mcp/README.md +33 -0
  291. alysis_code/mcp/__init__.py +24 -0
  292. alysis_code/mcp/client.py +1137 -0
  293. alysis_code/mcp/config.py +597 -0
  294. alysis_code/mcp/errors.py +113 -0
  295. alysis_code/mcp/forge_scope.py +154 -0
  296. alysis_code/mcp/jsonrpc.py +214 -0
  297. alysis_code/mcp/manager.py +2308 -0
  298. alysis_code/mcp/models.py +666 -0
  299. alysis_code/mcp/oauth.py +972 -0
  300. alysis_code/mcp/oauth_runtime.py +310 -0
  301. alysis_code/mcp/oauth_store.py +276 -0
  302. alysis_code/mcp/prompts.py +329 -0
  303. alysis_code/mcp/resources.py +295 -0
  304. alysis_code/mcp/roots.py +106 -0
  305. alysis_code/mcp/server_requests.py +75 -0
  306. alysis_code/mcp/token_store.py +859 -0
  307. alysis_code/mcp/transport_http.py +1338 -0
  308. alysis_code/mcp/transport_stdio.py +1267 -0
  309. alysis_code/mcp/untrusted_content.py +119 -0
  310. alysis_code/merge_conflict_reviewer.py +729 -0
  311. alysis_code/model_catalog/__init__.py +1 -0
  312. alysis_code/model_catalog/chatgpt_codex_subscription_snapshot.json +186 -0
  313. alysis_code/model_catalog/litellm_model_prices_snapshot.json +44715 -0
  314. alysis_code/model_catalog/litellm_model_prices_snapshot.meta.json +17 -0
  315. alysis_code/model_metadata_policy.py +223 -0
  316. alysis_code/model_metadata_utils.py +103 -0
  317. alysis_code/model_registry.py +1420 -0
  318. alysis_code/model_router.py +147 -0
  319. alysis_code/permission_policy.py +1016 -0
  320. alysis_code/personas.py +451 -0
  321. alysis_code/pipeline_facts.py +233 -0
  322. alysis_code/plan_assistant.py +4763 -0
  323. alysis_code/plan_mode.py +393 -0
  324. alysis_code/plan_reconciliation.py +1228 -0
  325. alysis_code/plan_repair.py +652 -0
  326. alysis_code/plan_validation.py +1099 -0
  327. alysis_code/planning_constraints.py +904 -0
  328. alysis_code/policy.py +95 -0
  329. alysis_code/preview_server.py +457 -0
  330. alysis_code/process_reaping.py +566 -0
  331. alysis_code/profile_presets.py +1834 -0
  332. alysis_code/profiles.py +666 -0
  333. alysis_code/provider_auth/__init__.py +29 -0
  334. alysis_code/provider_auth/base.py +99 -0
  335. alysis_code/provider_auth/openai_codex.py +951 -0
  336. alysis_code/provider_auth/registry.py +76 -0
  337. alysis_code/provider_auth/store.py +125 -0
  338. alysis_code/provider_diagnostics.py +1209 -0
  339. alysis_code/provider_model_catalog.py +685 -0
  340. alysis_code/provider_telemetry.py +1699 -0
  341. alysis_code/provider_url.py +75 -0
  342. alysis_code/reasoning_contracts.py +911 -0
  343. alysis_code/remote_sync.py +350 -0
  344. alysis_code/replanning.py +1195 -0
  345. alysis_code/repo_scan.py +1152 -0
  346. alysis_code/request_estimation.py +296 -0
  347. alysis_code/review_gate.py +617 -0
  348. alysis_code/run_lock.py +1141 -0
  349. alysis_code/run_outcome.py +58 -0
  350. alysis_code/run_provenance.py +774 -0
  351. alysis_code/run_state.py +445 -0
  352. alysis_code/runtime_artifacts.py +116 -0
  353. alysis_code/runtime_context_features.py +78 -0
  354. alysis_code/runtime_kind.py +52 -0
  355. alysis_code/safety/__init__.py +11 -0
  356. alysis_code/safety/mcp_sanitize.py +29 -0
  357. alysis_code/safety/safe_http.py +297 -0
  358. alysis_code/safety/subagent_report.py +184 -0
  359. alysis_code/sandbox_doctor.py +682 -0
  360. alysis_code/sandbox_runner.py +1025 -0
  361. alysis_code/sandbox_settings.py +423 -0
  362. alysis_code/serialized_paths.py +355 -0
  363. alysis_code/server/__init__.py +3 -0
  364. alysis_code/server/app.py +367 -0
  365. alysis_code/server/auth.py +34 -0
  366. alysis_code/server/job_config.py +30 -0
  367. alysis_code/server/settings.py +215 -0
  368. alysis_code/server/store.py +193 -0
  369. alysis_code/server/worker_runner.py +657 -0
  370. alysis_code/service_persistence.py +355 -0
  371. alysis_code/session_artifacts.py +108 -0
  372. alysis_code/session_metrics.py +331 -0
  373. alysis_code/session_store.py +624 -0
  374. alysis_code/skills/README.md +34 -0
  375. alysis_code/skills/__init__.py +104 -0
  376. alysis_code/skills/conventions.py +84 -0
  377. alysis_code/skills/discovery.py +176 -0
  378. alysis_code/skills/eval_models.py +232 -0
  379. alysis_code/skills/eval_runner.py +372 -0
  380. alysis_code/skills/evals.py +1344 -0
  381. alysis_code/skills/install.py +293 -0
  382. alysis_code/skills/loader.py +118 -0
  383. alysis_code/skills/matching.py +103 -0
  384. alysis_code/skills/models.py +71 -0
  385. alysis_code/skills/paths.py +56 -0
  386. alysis_code/skills/prompting.py +500 -0
  387. alysis_code/skills/scaffold.py +142 -0
  388. alysis_code/skills/state.py +441 -0
  389. alysis_code/skills/transactions.py +125 -0
  390. alysis_code/skills/validation.py +304 -0
  391. alysis_code/step_budget.py +238 -0
  392. alysis_code/subagent_labels.py +49 -0
  393. alysis_code/subagents.py +1072 -0
  394. alysis_code/surface/__init__.py +80 -0
  395. alysis_code/surface/base.py +305 -0
  396. alysis_code/surface/console.py +387 -0
  397. alysis_code/surface/events.py +372 -0
  398. alysis_code/surface/hidden_surface.py +529 -0
  399. alysis_code/surface/noop_surface.py +219 -0
  400. alysis_code/surface/rich_surface.py +1555 -0
  401. alysis_code/surface/styles.py +67 -0
  402. alysis_code/surface/theme.py +455 -0
  403. alysis_code/surface/types.py +100 -0
  404. alysis_code/swarm_backend.py +926 -0
  405. alysis_code/swarm_orchestrator.py +4020 -0
  406. alysis_code/swarm_scheduler.py +441 -0
  407. alysis_code/swarm_trace.py +429 -0
  408. alysis_code/swarm_worker.py +2119 -0
  409. alysis_code/swarm_write_guard.py +348 -0
  410. alysis_code/task_dependencies.py +170 -0
  411. alysis_code/task_readiness.py +992 -0
  412. alysis_code/task_scope.py +2148 -0
  413. alysis_code/terminal_manager.py +762 -0
  414. alysis_code/terminal_ownership.py +460 -0
  415. alysis_code/text_normalization.py +30 -0
  416. alysis_code/token_budget.py +97 -0
  417. alysis_code/tools/README.md +34 -0
  418. alysis_code/tools/__init__.py +1 -0
  419. alysis_code/tools/artifacts.py +127 -0
  420. alysis_code/tools/availability.py +188 -0
  421. alysis_code/tools/fs.py +1456 -0
  422. alysis_code/tools/git.py +461 -0
  423. alysis_code/tools/history.py +229 -0
  424. alysis_code/tools/http_timeout.py +78 -0
  425. alysis_code/tools/image_generation.py +552 -0
  426. alysis_code/tools/registry.py +2936 -0
  427. alysis_code/tools/repo_map.py +476 -0
  428. alysis_code/tools/search.py +563 -0
  429. alysis_code/tools/shell.py +135 -0
  430. alysis_code/tools/symbols.py +1350 -0
  431. alysis_code/tools/test_discovery.py +643 -0
  432. alysis_code/tools/web.py +482 -0
  433. alysis_code/tools/web_search.py +2012 -0
  434. alysis_code/tools/web_search_dashscope.py +557 -0
  435. alysis_code/tools/web_search_ddgs.py +221 -0
  436. alysis_code/tools/web_search_provider_adapters.py +1429 -0
  437. alysis_code/tools/web_search_tavily.py +194 -0
  438. alysis_code/updates.py +933 -0
  439. alysis_code/usage_tracker.py +1990 -0
  440. alysis_code/verification_command_analysis.py +1004 -0
  441. alysis_code/verification_contract.py +574 -0
  442. alysis_code/verification_failure_summary.py +273 -0
  443. alysis_code/verification_repair.py +385 -0
  444. alysis_code/verify_gate.py +3129 -0
  445. alysis_code/web_research.py +1872 -0
  446. alysis_code/web_search_adapters.py +66 -0
  447. alysis_code/web_search_policy.py +27 -0
  448. alysis_code/workspace_binding.py +389 -0
  449. alysis_code/workspace_binding_ui.py +408 -0
  450. alysis_code/workspace_context.py +273 -0
  451. alysis_code/workspace_isolation.py +138 -0
  452. alysis_code/workspace_provisioning.py +455 -0
  453. alysis_code-0.13.0.dist-info/METADATA +507 -0
  454. alysis_code-0.13.0.dist-info/RECORD +458 -0
  455. alysis_code-0.13.0.dist-info/WHEEL +4 -0
  456. alysis_code-0.13.0.dist-info/entry_points.txt +3 -0
  457. alysis_code-0.13.0.dist-info/licenses/LICENSE +176 -0
  458. alysis_code-0.13.0.dist-info/licenses/NOTICE +4 -0
@@ -0,0 +1,1555 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ import os
5
+ import re
6
+ import sys
7
+ import time
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from rich.console import Console, Group
12
+ from rich.live import Live
13
+ from rich.markdown import Markdown
14
+ from rich.prompt import Prompt
15
+ from rich.text import Text
16
+
17
+ from ..approval_scope import approval_session_scope_for_request
18
+ from ..interactive_input_guard import interactive_prompt_guard
19
+ from ..llm_error_display import classify_llm_error_display, friendly_llm_error_message
20
+ from ..plan_mode import extract_approved_plan_user_message
21
+ from ..subagent_labels import subagent_identity
22
+ from ..tools.registry import (
23
+ summarize_tool_output_chunk as _summarize_tool_output,
24
+ )
25
+ from ..tools.registry import (
26
+ tool_display_name as _tool_display_name,
27
+ )
28
+ from ..tools.registry import (
29
+ tool_input_preview as _tool_input_preview,
30
+ )
31
+ from .console import (
32
+ ascii_sanitize,
33
+ console_glyph,
34
+ console_uses_ascii,
35
+ make_console,
36
+ safe_plain_error,
37
+ safe_write,
38
+ )
39
+ from .events import (
40
+ ConfigFormRequest,
41
+ ErrorRaised,
42
+ Event,
43
+ InfoEmitted,
44
+ MessageDelta,
45
+ MessageEnd,
46
+ ModeChanged,
47
+ PersonaChanged,
48
+ PlanNodeUpdated,
49
+ PromptForInput,
50
+ ReviewGateDecision,
51
+ StatusUpdate,
52
+ SwarmWorkerStateChanged,
53
+ ToolCallCompleted,
54
+ ToolCallProgress,
55
+ ToolCallStarted,
56
+ VerifyGateResult,
57
+ WarningEmitted,
58
+ )
59
+ from .styles import (
60
+ STYLE_CHROME,
61
+ STYLE_CONTENT,
62
+ STYLE_DIM,
63
+ STYLE_EMPHASIS,
64
+ STYLE_ERROR,
65
+ STYLE_SUBAGENT,
66
+ STYLE_SUCCESS,
67
+ STYLE_WARN,
68
+ )
69
+ from .types import (
70
+ ApprovalDecision,
71
+ ApprovalRequest,
72
+ PatchEvent,
73
+ StatusEvent,
74
+ SubagentEndEvent,
75
+ SubagentStartEvent,
76
+ ToolEndEvent,
77
+ ToolOutputEvent,
78
+ ToolStartEvent,
79
+ )
80
+
81
+ _SENSITIVE_PATTERNS = [
82
+ re.compile(r"sk-[A-Za-z0-9_\-]{16,}"),
83
+ re.compile(r"(Bearer\s+)[A-Za-z0-9._\-]{8,}", re.IGNORECASE),
84
+ re.compile(r"(Authorization\s*:\s*)(.+)", re.IGNORECASE),
85
+ ]
86
+ _TRACE_LEVELS = {"off", "compact", "full"}
87
+ _MAX_ERROR_CHARS = 520
88
+ _MAX_REASONING_SUMMARY_PREVIEW_CHARS = 180
89
+ _STYLE_EMPHASIS = STYLE_EMPHASIS
90
+ _STYLE_CONTENT = STYLE_CONTENT
91
+ _STYLE_META = STYLE_DIM
92
+ _STYLE_CHROME = STYLE_CHROME
93
+ _STYLE_SUBAGENT = STYLE_SUBAGENT
94
+ _STYLE_SUCCESS = STYLE_SUCCESS
95
+ _STYLE_FAILURE = STYLE_ERROR
96
+ _STYLE_WARNING = STYLE_WARN
97
+ _THINKING_SPINNER_FRAMES = ("⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏")
98
+ _TERMINAL_PROGRESS_MESSAGES = {
99
+ "Applied planner update to the Forge plan.",
100
+ "Plan draft ready for review.",
101
+ "Planner response ready.",
102
+ "Planner update was a no-op.",
103
+ }
104
+ _TERMINAL_PROGRESS_PREFIXES = (
105
+ "Plan generation failed:",
106
+ "Planner request recovered after",
107
+ "Planner returned an error",
108
+ "Swarm aborted:",
109
+ "Swarm completed with exit code",
110
+ )
111
+
112
+
113
+ def _strip_progress_scope_prefix(message: str) -> str:
114
+ clean = str(message or "").strip()
115
+ if not clean.startswith("["):
116
+ return clean
117
+ closing = clean.find("]")
118
+ if closing <= 1:
119
+ return clean
120
+ return clean[closing + 1 :].strip()
121
+
122
+
123
+ def _redact(text: str) -> str:
124
+ out = text
125
+ for pattern in _SENSITIVE_PATTERNS:
126
+ if pattern.pattern.lower().startswith("(authorization"):
127
+ out = pattern.sub(r"\1[REDACTED]", out)
128
+ continue
129
+ if pattern.pattern.lower().startswith("(bearer"):
130
+ out = pattern.sub(r"\1[REDACTED]", out)
131
+ continue
132
+ out = pattern.sub("[REDACTED]", out)
133
+ return out
134
+
135
+
136
+ def _truncate_inline(text: str, *, max_chars: int = 96) -> str:
137
+ normalized = " ".join(text.split())
138
+ if len(normalized) <= max_chars:
139
+ return normalized
140
+ if max_chars <= 3:
141
+ return normalized[:max_chars]
142
+ return normalized[: max_chars - 3] + "..."
143
+
144
+
145
+ def _split_lines(text: str) -> list[str]:
146
+ lines = str(text).splitlines()
147
+ return lines or [str(text)]
148
+
149
+
150
+ def _looks_like_markdown(text: str) -> bool:
151
+ clean = str(text or "")
152
+ if not clean.strip():
153
+ return False
154
+ if "```" in clean:
155
+ return True
156
+ has_multiple_lines = "\n" in clean
157
+ for line in clean.splitlines():
158
+ stripped = line.lstrip()
159
+ if not stripped:
160
+ continue
161
+ if stripped.startswith(("#", ">", "-", "*")):
162
+ return True
163
+ if re.match(r"\d+\.\s", stripped):
164
+ return True
165
+ if has_multiple_lines and stripped.count("|") >= 2:
166
+ return True
167
+ return False
168
+
169
+
170
+ def _prefixed_text(
171
+ *,
172
+ prefix: str,
173
+ prefix_style: str,
174
+ text: str,
175
+ text_style: str,
176
+ ) -> Text:
177
+ return Text.assemble((prefix, prefix_style), (text, text_style))
178
+
179
+
180
+ def _bar_prefix(console: Console) -> str:
181
+ return console_glyph(console, "│ ", "| ")
182
+
183
+
184
+ def _subagent_top_prefix(console: Console, *, indent: str) -> str:
185
+ return indent + console_glyph(console, "╭─ ", "+- ")
186
+
187
+
188
+ def _subagent_bottom_prefix(console: Console, *, indent: str) -> str:
189
+ return indent + console_glyph(console, "╰─ ", "+- ")
190
+
191
+
192
+ def _subagent_bar_prefix(console: Console, *, indent: str) -> str:
193
+ return indent + console_glyph(console, "│ ", "| ")
194
+
195
+
196
+ def _separator(console: Console) -> str:
197
+ return console_glyph(console, " · ", " - ")
198
+
199
+
200
+ def _surface_text(console: Console, text: str) -> str:
201
+ if not console_uses_ascii(console):
202
+ return str(text)
203
+ return ascii_sanitize(str(text), stream=getattr(console, "file", None))
204
+
205
+
206
+ def _error_renderable(err: str) -> Group:
207
+ clean = _redact(friendly_llm_error_message(err).strip())
208
+ if not clean:
209
+ clean = "No additional error details."
210
+ if len(clean) > _MAX_ERROR_CHARS:
211
+ clean = clean[: _MAX_ERROR_CHARS - 15].rstrip() + "...(truncated)"
212
+
213
+ display = classify_llm_error_display(clean)
214
+ renderables: list[Text] = [
215
+ _prefixed_text(
216
+ prefix="│ ",
217
+ prefix_style="red",
218
+ text=clean,
219
+ text_style="red",
220
+ )
221
+ ]
222
+ renderables.extend(
223
+ _prefixed_text(
224
+ prefix=" ",
225
+ prefix_style=_STYLE_CONTENT,
226
+ text=line,
227
+ text_style=_STYLE_META,
228
+ )
229
+ for line in display.guidance_lines
230
+ )
231
+ return Group(*renderables)
232
+
233
+
234
+ def _warning_renderable(warning: str) -> Group:
235
+ clean = _redact(str(warning).strip())
236
+ if not clean:
237
+ clean = "No additional warning details."
238
+ if len(clean) > _MAX_ERROR_CHARS:
239
+ clean = clean[: _MAX_ERROR_CHARS - 15].rstrip() + "...(truncated)"
240
+ return Group(
241
+ _prefixed_text(
242
+ prefix="│ ",
243
+ prefix_style=_STYLE_WARNING,
244
+ text=f"Warning: {clean}",
245
+ text_style=_STYLE_WARNING,
246
+ )
247
+ )
248
+
249
+
250
+ def _normalize_trace_level(value: str | None, *, fallback: str = "compact") -> str:
251
+ raw = (value or "").strip().lower()
252
+ if raw in _TRACE_LEVELS:
253
+ return raw
254
+ return fallback
255
+
256
+
257
+ def _format_duration_ms(elapsed_ms: int) -> str:
258
+ if elapsed_ms < 1000:
259
+ return f"{elapsed_ms}ms"
260
+ seconds = elapsed_ms / 1000.0
261
+ return f"{seconds:.1f}s"
262
+
263
+
264
+ def _format_step_count(steps_completed: int) -> str:
265
+ step_word = "step" if steps_completed == 1 else "steps"
266
+ return f"{steps_completed} {step_word}"
267
+
268
+
269
+ def _format_turn_elapsed(elapsed_s: float) -> str | None:
270
+ if elapsed_s < 1.0:
271
+ return None
272
+ total_seconds = max(int(elapsed_s), 1)
273
+ minutes, seconds = divmod(total_seconds, 60)
274
+ if minutes <= 0:
275
+ return f"{seconds}s"
276
+ if seconds <= 0:
277
+ return f"{minutes}m"
278
+ return f"{minutes}m {seconds}s"
279
+
280
+
281
+ def _progress_message_completes_activity(message: str) -> bool:
282
+ clean = _strip_progress_scope_prefix(message)
283
+ if clean in _TERMINAL_PROGRESS_MESSAGES:
284
+ return True
285
+ return any(clean.startswith(prefix) for prefix in _TERMINAL_PROGRESS_PREFIXES)
286
+
287
+
288
+ class _ThinkingSpinnerRenderable:
289
+ def __init__(self, surface: RichSurface) -> None:
290
+ self._surface = surface
291
+
292
+ def __rich__(self) -> Text:
293
+ return self._surface._make_thinking_renderable()
294
+
295
+
296
+ class RichSurface:
297
+ def __init__(
298
+ self,
299
+ *,
300
+ console: Console | None = None,
301
+ show_status_line: bool = True,
302
+ ) -> None:
303
+ self.console = console or make_console()
304
+ self.renders_error_panel = True
305
+ self._show_status_line = show_status_line
306
+ self._assistant_stream_open = False
307
+ self._allow_for_session: set[str] = set()
308
+ self._thinking_open = False
309
+ self._last_trace_line_key: str = ""
310
+ self._tool_output_summary: dict[str, str] = {}
311
+ self._tool_start_info: dict[str, tuple[int, str]] = {}
312
+ self._trace_level = "compact"
313
+ self._turn_started = False
314
+ self._working_banner_shown = False
315
+ self._turn_start_time: float | None = None
316
+ self._thinking_live: Live | None = None
317
+ self._thinking_start: float = 0.0
318
+ self._spinner_frame: int = 0
319
+ self._spinner_label: str = "Thinking..."
320
+ self._spinner_active = False
321
+ self._stream_buffer: list[str] = []
322
+ self._reasoning_summary_buffer: list[str] = []
323
+ self._reasoning_summary_live: Live | None = None
324
+ self._reasoning_summary_block_id: str | None = None
325
+ self._event_tool_names: dict[str, str] = {}
326
+
327
+ @property
328
+ def trace_level(self) -> str:
329
+ return self._trace_level
330
+
331
+ @property
332
+ def reasoning_trace_enabled(self) -> bool:
333
+ """Return whether safe provider-generated summaries are visible."""
334
+
335
+ return self._trace_level != "off"
336
+
337
+ def set_trace_level(self, level: str) -> str:
338
+ self._trace_level = _normalize_trace_level(level, fallback=self._trace_level)
339
+ if self._trace_level == "off":
340
+ self._stop_reasoning_summary_live()
341
+ self._reasoning_summary_buffer.clear()
342
+ self._reasoning_summary_block_id = None
343
+ return self._trace_level
344
+
345
+ def _emit_trace_line(
346
+ self,
347
+ message: str,
348
+ *,
349
+ style: str,
350
+ prefix: str,
351
+ prefix_style: str,
352
+ dedupe_key: str | None = None,
353
+ ) -> None:
354
+ self._stop_thinking_spinner()
355
+ self._stop_reasoning_summary_live()
356
+ clean = _redact(message.strip())
357
+ if not clean:
358
+ return
359
+ prefix = _surface_text(self.console, prefix)
360
+ clean = _surface_text(self.console, clean)
361
+ line_key = dedupe_key or f"{prefix}\0{clean}"
362
+ if line_key == self._last_trace_line_key:
363
+ return
364
+ self._last_trace_line_key = line_key
365
+ self._thinking_open = True
366
+ self.console.print(
367
+ Text.assemble((prefix, prefix_style), (clean, style)),
368
+ highlight=False,
369
+ )
370
+
371
+ def _emit_thinking(self, message: str, *, style: str = "dim") -> None:
372
+ self._emit_trace_line(
373
+ message,
374
+ style=style,
375
+ prefix=console_glyph(self.console, "• ", "* "),
376
+ prefix_style=_STYLE_CHROME,
377
+ )
378
+
379
+ def _reset_thinking(self) -> None:
380
+ self._stop_thinking_spinner()
381
+ self._stop_reasoning_summary_live()
382
+ self._thinking_open = False
383
+ self._last_trace_line_key = ""
384
+ self._stream_buffer.clear()
385
+ self._reasoning_summary_buffer.clear()
386
+ self._reasoning_summary_block_id = None
387
+
388
+ def _flush_reasoning_summary(self) -> None:
389
+ self._stop_reasoning_summary_live()
390
+ if not self._reasoning_summary_buffer:
391
+ return
392
+ summary = _redact("".join(self._reasoning_summary_buffer)).strip()
393
+ self._reasoning_summary_buffer.clear()
394
+ self._reasoning_summary_block_id = None
395
+ if self._trace_level == "off" or not summary:
396
+ return
397
+ if self._trace_level == "compact":
398
+ preview = _truncate_inline(
399
+ summary,
400
+ max_chars=_MAX_REASONING_SUMMARY_PREVIEW_CHARS,
401
+ )
402
+ self._emit_thinking(f"Reasoning summary: {preview}", style=_STYLE_META)
403
+ return
404
+ self._emit_thinking("Reasoning summary:", style=_STYLE_CHROME)
405
+ for line in _split_lines(summary):
406
+ self._emit_thinking(line, style=_STYLE_META)
407
+
408
+ def _reasoning_summary_renderable(self) -> Text:
409
+ summary = _redact("".join(self._reasoning_summary_buffer)).strip()
410
+ if self._trace_level == "compact":
411
+ summary = _truncate_inline(
412
+ summary,
413
+ max_chars=_MAX_REASONING_SUMMARY_PREVIEW_CHARS,
414
+ )
415
+ label = "Reasoning summary… " if summary else "Reasoning summary…"
416
+ return Text.assemble(
417
+ (console_glyph(self.console, "• ", "* "), _STYLE_CHROME),
418
+ (label, _STYLE_CHROME),
419
+ (_surface_text(self.console, summary), _STYLE_META),
420
+ )
421
+
422
+ def _update_reasoning_summary_live(self) -> None:
423
+ if not bool(getattr(self.console, "is_terminal", False)):
424
+ return
425
+ self._stop_thinking_spinner()
426
+ renderable = self._reasoning_summary_renderable()
427
+ if self._reasoning_summary_live is None:
428
+ self._reasoning_summary_live = Live(
429
+ renderable,
430
+ console=self.console,
431
+ auto_refresh=False,
432
+ transient=True,
433
+ )
434
+ self._reasoning_summary_live.start(refresh=True)
435
+ return
436
+ self._reasoning_summary_live.update(renderable, refresh=True)
437
+
438
+ def _stop_reasoning_summary_live(self) -> None:
439
+ live = self._reasoning_summary_live
440
+ self._reasoning_summary_live = None
441
+ if live is not None:
442
+ live.stop()
443
+
444
+ def _supports_live_thinking_spinner(self) -> bool:
445
+ if console_uses_ascii(self.console):
446
+ return False
447
+ return bool(getattr(self.console, "is_terminal", False))
448
+
449
+ def _make_thinking_renderable(self) -> Text:
450
+ if not self._spinner_active:
451
+ return Text("")
452
+ elapsed = max(int(time.monotonic() - self._thinking_start), 0)
453
+ spinner_char = _THINKING_SPINNER_FRAMES[self._spinner_frame % len(_THINKING_SPINNER_FRAMES)]
454
+ spinner_char = console_glyph(self.console, spinner_char, "*")
455
+ self._spinner_frame = (self._spinner_frame + 1) % len(_THINKING_SPINNER_FRAMES)
456
+ style = _STYLE_WARNING if elapsed >= 30 else _STYLE_META
457
+ return Text(f"{spinner_char} {self._spinner_label} {elapsed}s", style=style)
458
+
459
+ def _stop_thinking_spinner(self) -> None:
460
+ live = self._thinking_live
461
+ self._spinner_active = False
462
+ self._thinking_live = None
463
+ self._thinking_start = 0.0
464
+ self._spinner_frame = 0
465
+ self._spinner_label = "Thinking..."
466
+ if live is not None:
467
+ live.stop()
468
+
469
+ def _start_thinking_spinner(self, *, label: str) -> None:
470
+ if self._assistant_stream_open:
471
+ return
472
+ self._stop_thinking_spinner()
473
+ self._spinner_label = label
474
+ self._spinner_frame = 0
475
+ if not self._supports_live_thinking_spinner():
476
+ if self._trace_level == "off" and label == "Thinking...":
477
+ self._emit_trace_line(
478
+ label,
479
+ style=_STYLE_META,
480
+ prefix=console_glyph(self.console, "• ", "* "),
481
+ prefix_style=_STYLE_CHROME,
482
+ dedupe_key=f"thinking-fallback:{label}",
483
+ )
484
+ return
485
+ self._thinking_start = time.monotonic()
486
+ self._spinner_active = True
487
+ self._thinking_live = Live(
488
+ _ThinkingSpinnerRenderable(self),
489
+ console=self.console,
490
+ auto_refresh=True,
491
+ refresh_per_second=4,
492
+ transient=True,
493
+ )
494
+ self._thinking_live.start(refresh=True)
495
+
496
+ def _assistant_stream(self) -> object:
497
+ return self.console.file if self.console.file is not None else sys.stdout
498
+
499
+ def _count_terminal_lines(self, text: str) -> int:
500
+ width = max(int(getattr(self.console, "width", 0) or 0), 1)
501
+ total = 0
502
+ for logical_line in str(text).split("\n"):
503
+ if not logical_line:
504
+ total += 1
505
+ continue
506
+ total += max(1, math.ceil(len(logical_line) / width))
507
+ return total
508
+
509
+ def _should_skip_stream_markdown_rerender(self, text: str) -> bool:
510
+ return not _looks_like_markdown(text)
511
+
512
+ def _render_markdown(self, text: str) -> None:
513
+ self.console.print(Markdown(text, code_theme="monokai"))
514
+
515
+ def _erase_streamed_output_lines(self, line_count: int) -> None:
516
+ if line_count <= 0:
517
+ return
518
+ stream = self._assistant_stream()
519
+ safe_write(stream, "\r\033[2K")
520
+ for _ in range(max(0, line_count - 1)):
521
+ safe_write(stream, "\033[A\033[2K")
522
+ safe_write(stream, "\r")
523
+
524
+ def _subagent_indent(self, *, nesting_depth: int = 1) -> str:
525
+ indent = " " * max(nesting_depth, 1)
526
+ return indent
527
+
528
+ def _emit_subagent_border_top(
529
+ self,
530
+ *,
531
+ subagent_name: str,
532
+ subagent_mode: str,
533
+ nesting_depth: int = 1,
534
+ ) -> None:
535
+ indent = self._subagent_indent(nesting_depth=nesting_depth)
536
+ self._emit_trace_line(
537
+ f"{subagent_name} · {subagent_mode}",
538
+ style=f"bold {_STYLE_SUBAGENT}",
539
+ prefix=_subagent_top_prefix(self.console, indent=indent),
540
+ prefix_style=_STYLE_SUBAGENT,
541
+ dedupe_key=f"subagent-top:{subagent_name}:{subagent_mode}:{nesting_depth}",
542
+ )
543
+
544
+ def _emit_subagent_border_bottom(
545
+ self,
546
+ *,
547
+ subagent_name: str,
548
+ status_label: str,
549
+ status_style: str,
550
+ steps_completed: int,
551
+ elapsed_ms: int,
552
+ nesting_depth: int = 1,
553
+ ) -> None:
554
+ self._stop_thinking_spinner()
555
+ indent = self._subagent_indent(nesting_depth=nesting_depth)
556
+ step_text = _format_step_count(steps_completed)
557
+ elapsed = _format_duration_ms(elapsed_ms)
558
+ self._thinking_open = True
559
+ self._last_trace_line_key = f"subagent-bottom:{subagent_name}:{status_label}:{steps_completed}:{elapsed_ms}:{nesting_depth}"
560
+ self.console.print(
561
+ Text.assemble(
562
+ (_subagent_bottom_prefix(self.console, indent=indent), _STYLE_SUBAGENT),
563
+ (status_label, status_style),
564
+ (_separator(self.console), _STYLE_SUBAGENT),
565
+ (step_text, _STYLE_SUBAGENT),
566
+ (_separator(self.console), _STYLE_SUBAGENT),
567
+ (elapsed, _STYLE_SUBAGENT),
568
+ ),
569
+ highlight=False,
570
+ )
571
+
572
+ def _emit_subagent_trace(
573
+ self,
574
+ *,
575
+ subagent_name: str,
576
+ message: str,
577
+ style: str,
578
+ nesting_depth: int = 1,
579
+ ) -> None:
580
+ indent = self._subagent_indent(nesting_depth=nesting_depth)
581
+ self._emit_trace_line(
582
+ message,
583
+ style=style,
584
+ prefix=_subagent_bar_prefix(self.console, indent=indent),
585
+ prefix_style=_STYLE_SUBAGENT,
586
+ dedupe_key=f"subagent:{subagent_name}:{nesting_depth}:{message}",
587
+ )
588
+
589
+ def _print_left_bar_block(
590
+ self,
591
+ lines: list[str],
592
+ *,
593
+ bar_style: str,
594
+ text_style: str,
595
+ ) -> None:
596
+ self._stop_thinking_spinner()
597
+ for line in lines:
598
+ self.console.print(
599
+ _prefixed_text(
600
+ prefix=_bar_prefix(self.console),
601
+ prefix_style=bar_style,
602
+ text=_surface_text(self.console, line),
603
+ text_style=text_style,
604
+ ),
605
+ highlight=False,
606
+ )
607
+
608
+ def _open_answer_section(self) -> None:
609
+ self._stop_thinking_spinner()
610
+ if self._thinking_open:
611
+ self.console.print("")
612
+ self._reset_thinking()
613
+ elapsed = self._consume_turn_elapsed_label()
614
+ if elapsed is None:
615
+ self.console.rule(style=_STYLE_CHROME)
616
+ return
617
+ self.console.rule(Text(elapsed, style=_STYLE_META), style=_STYLE_CHROME, align="right")
618
+
619
+ def _consume_turn_elapsed_label(self) -> str | None:
620
+ start = self._turn_start_time
621
+ self._turn_start_time = None
622
+ if start is None:
623
+ return None
624
+ return _format_turn_elapsed(time.monotonic() - start)
625
+
626
+ def on_status_update(self, status: StatusEvent) -> None:
627
+ if not self._show_status_line:
628
+ return
629
+ cwd = Path(status.workspace).name or status.workspace
630
+ parts = [cwd, status.model, status.mode]
631
+ if status.task and status.task != "-":
632
+ parts.append(status.task)
633
+ if status.dirty:
634
+ parts.append("dirty")
635
+ self.console.print(" · ".join(parts), style=_STYLE_META, highlight=False)
636
+
637
+ def on_user_message(self, text: str) -> None:
638
+ self._reset_thinking()
639
+ self._turn_started = True
640
+ self._working_banner_shown = False
641
+ self._turn_start_time = time.monotonic()
642
+ approved_plan_message = extract_approved_plan_user_message(text)
643
+ if approved_plan_message is not None:
644
+ self._print_left_bar_block(
645
+ ["Plan approved. Executing..."],
646
+ bar_style=_STYLE_META,
647
+ text_style=_STYLE_META,
648
+ )
649
+ display_text = approved_plan_message or text
650
+ clean = _redact(display_text.strip())
651
+ self._print_left_bar_block(
652
+ _split_lines(clean or "(empty message)"),
653
+ bar_style=_STYLE_EMPHASIS,
654
+ text_style=_STYLE_EMPHASIS,
655
+ )
656
+ self._start_thinking_spinner(label="Thinking...")
657
+
658
+ def on_assistant_token(self, delta: str) -> None:
659
+ self._turn_started = False
660
+ self._working_banner_shown = False
661
+ self._flush_reasoning_summary()
662
+ self._stop_thinking_spinner()
663
+ if not self._assistant_stream_open:
664
+ self._open_answer_section()
665
+ self._assistant_stream_open = True
666
+ clean_delta = _redact(delta)
667
+ stream = self._assistant_stream()
668
+ safe_write(stream, clean_delta)
669
+ self._stream_buffer.append(clean_delta)
670
+
671
+ def on_reasoning_start(self, block_id: str) -> None:
672
+ """Open a provider-call-scoped safe-summary block."""
673
+
674
+ if self._trace_level == "off":
675
+ return
676
+ normalized = str(block_id or "").strip()
677
+ if not normalized or normalized == self._reasoning_summary_block_id:
678
+ return
679
+ if self._reasoning_summary_buffer:
680
+ self._flush_reasoning_summary()
681
+ self._reasoning_summary_block_id = normalized
682
+
683
+ def on_reasoning_token(self, delta: str) -> None:
684
+ """Buffer a safe provider-generated reasoning summary for display."""
685
+
686
+ if self._trace_level == "off" or not delta:
687
+ return
688
+ if self._reasoning_summary_block_id is None:
689
+ self._reasoning_summary_block_id = "legacy"
690
+ self._reasoning_summary_buffer.append(delta)
691
+ # Streaming clients call this incrementally; buffered clients call it
692
+ # only after the provider response completes, so live rendering follows
693
+ # `/stream` timing without a second surface-level mode switch.
694
+ self._update_reasoning_summary_live()
695
+
696
+ def on_reasoning_end(self, block_id: str) -> None:
697
+ normalized = str(block_id or "").strip()
698
+ if normalized and normalized != self._reasoning_summary_block_id:
699
+ return
700
+ self._flush_reasoning_summary()
701
+
702
+ def on_assistant_message_done(self, text: str) -> None:
703
+ self._turn_started = False
704
+ self._working_banner_shown = False
705
+ self._flush_reasoning_summary()
706
+ self._stop_thinking_spinner()
707
+ clean = _redact(text.strip())
708
+ if self._assistant_stream_open:
709
+ full_text = "".join(self._stream_buffer)
710
+ should_rerender = (
711
+ bool(full_text.strip())
712
+ and not self._should_skip_stream_markdown_rerender(full_text)
713
+ and bool(getattr(self.console, "is_terminal", False))
714
+ )
715
+ if should_rerender:
716
+ self._erase_streamed_output_lines(self._count_terminal_lines(full_text))
717
+ self._render_markdown(full_text)
718
+ else:
719
+ self.console.print("")
720
+ self._stream_buffer.clear()
721
+ self._assistant_stream_open = False
722
+ return
723
+ if not clean:
724
+ self._stream_buffer.clear()
725
+ return
726
+ self._open_answer_section()
727
+ self._render_markdown(clean)
728
+ self._stream_buffer.clear()
729
+
730
+ def on_progress_update(self, message: str) -> None:
731
+ if self._trace_level == "off":
732
+ return
733
+ self._emit_thinking(message, style="dim")
734
+ if _progress_message_completes_activity(message):
735
+ self._stop_thinking_spinner()
736
+ return
737
+ if not self._assistant_stream_open and not self._tool_start_info:
738
+ self._start_thinking_spinner(label="Thinking...")
739
+
740
+ def on_subagent_start(self, event: SubagentStartEvent) -> None:
741
+ if self._trace_level == "off":
742
+ return
743
+ self._emit_subagent_border_top(
744
+ subagent_name=subagent_identity(event.name, event.label),
745
+ subagent_mode=event.mode,
746
+ )
747
+
748
+ def on_subagent_end(self, event: SubagentEndEvent) -> None:
749
+ if self._trace_level == "off":
750
+ return
751
+ status_label = "finished" if event.status == "success" else event.status
752
+ display_name = subagent_identity(event.name, event.label)
753
+ if event.error:
754
+ self._emit_subagent_trace(
755
+ subagent_name=display_name,
756
+ message=f"Error: {_truncate_inline(_redact(event.error), max_chars=140)}",
757
+ style=_STYLE_FAILURE,
758
+ )
759
+ status_style = _STYLE_SUCCESS if event.status == "success" else _STYLE_FAILURE
760
+ self._emit_subagent_border_bottom(
761
+ subagent_name=display_name,
762
+ status_label=status_label,
763
+ status_style=status_style,
764
+ steps_completed=event.steps_completed,
765
+ elapsed_ms=event.elapsed_ms,
766
+ )
767
+
768
+ def on_tool_start(self, event: ToolStartEvent) -> None:
769
+ self._flush_reasoning_summary()
770
+ self._stop_thinking_spinner()
771
+ if self._turn_started and not self._working_banner_shown:
772
+ elapsed = self._consume_turn_elapsed_label()
773
+ if elapsed is None:
774
+ self.console.print("Working... Press Esc to interrupt.", style=_STYLE_META)
775
+ else:
776
+ self.console.print(
777
+ Text.assemble(
778
+ ("Working... Press Esc to interrupt.", _STYLE_META),
779
+ (" ", _STYLE_META),
780
+ (elapsed, _STYLE_META),
781
+ ),
782
+ highlight=False,
783
+ )
784
+ self._working_banner_shown = True
785
+ display = _tool_display_name(event.name)
786
+ self._tool_start_info[event.tool_call_id] = (event.step, display)
787
+ if self._trace_level == "off":
788
+ return
789
+ if event.subagent_name:
790
+ self._emit_subagent_trace(
791
+ subagent_name=event.subagent_name,
792
+ message=f"Step {event.step}: {display}",
793
+ style=_STYLE_CHROME,
794
+ nesting_depth=event.nesting_depth,
795
+ )
796
+ else:
797
+ self._emit_thinking(f"Step {event.step}: {display}", style=_STYLE_CHROME)
798
+ if self._trace_level != "full":
799
+ return
800
+ if event.subagent_name:
801
+ self._emit_subagent_trace(
802
+ subagent_name=event.subagent_name,
803
+ message=f"Input: {_tool_input_preview(event.name, event.args)}",
804
+ style=_STYLE_META,
805
+ nesting_depth=event.nesting_depth,
806
+ )
807
+ else:
808
+ self._emit_thinking(
809
+ f"Input: {_tool_input_preview(event.name, event.args)}",
810
+ style=_STYLE_META,
811
+ )
812
+
813
+ def on_tool_output(self, event: ToolOutputEvent) -> None:
814
+ summary = _summarize_tool_output(event.name, event.chunk)
815
+ if summary.strip():
816
+ self._tool_output_summary[event.tool_call_id] = summary
817
+
818
+ def on_tool_end(self, event: ToolEndEvent) -> None:
819
+ display = _tool_display_name(event.name)
820
+ elapsed = _format_duration_ms(event.elapsed_ms)
821
+ detail = ""
822
+ err = event.meta.get("error")
823
+ if err:
824
+ err_preview = _truncate_inline(_redact(str(err)), max_chars=140)
825
+ detail = f": {err_preview}"
826
+ self._tool_start_info.pop(event.tool_call_id, None)
827
+ summary = self._tool_output_summary.pop(event.tool_call_id, "").strip()
828
+ should_render_outcome = (
829
+ self._trace_level != "off"
830
+ or event.status != "done"
831
+ or bool(event.meta.get("approval_declined"))
832
+ )
833
+ if should_render_outcome:
834
+ if event.status == "done":
835
+ outcome = f"{display} ({elapsed})"
836
+ if summary:
837
+ outcome = f"{display}: {summary} ({elapsed})"
838
+ if event.subagent_name:
839
+ self._emit_subagent_trace(
840
+ subagent_name=event.subagent_name,
841
+ message=outcome,
842
+ style=_STYLE_CHROME,
843
+ nesting_depth=event.nesting_depth,
844
+ )
845
+ else:
846
+ self._emit_thinking(outcome, style=_STYLE_CHROME)
847
+ elif event.meta.get("approval_declined"):
848
+ message = f"{display} approval declined ({elapsed}){detail}"
849
+ if event.subagent_name:
850
+ self._emit_subagent_trace(
851
+ subagent_name=event.subagent_name,
852
+ message=message,
853
+ style="red",
854
+ nesting_depth=event.nesting_depth,
855
+ )
856
+ else:
857
+ self._emit_thinking(message, style="red")
858
+ elif event.subagent_name:
859
+ self._emit_subagent_trace(
860
+ subagent_name=event.subagent_name,
861
+ message=f"{display} failed ({elapsed}){detail}",
862
+ style="red",
863
+ nesting_depth=event.nesting_depth,
864
+ )
865
+ else:
866
+ self._emit_thinking(f"{display} failed ({elapsed}){detail}", style="red")
867
+ if not self._assistant_stream_open and not self._tool_start_info:
868
+ self._start_thinking_spinner(label="Reasoning...")
869
+
870
+ def on_patch_generated(self, event: PatchEvent) -> None:
871
+ self._flush_reasoning_summary()
872
+ self._stop_thinking_spinner()
873
+ self.console.print(
874
+ _prefixed_text(
875
+ prefix=_bar_prefix(self.console),
876
+ prefix_style=_STYLE_CHROME,
877
+ text=", ".join(event.files[:5]) or "Patch Preview",
878
+ text_style=_STYLE_EMPHASIS,
879
+ ),
880
+ highlight=False,
881
+ )
882
+ if event.summary:
883
+ self.console.print(
884
+ _prefixed_text(
885
+ prefix=_bar_prefix(self.console),
886
+ prefix_style=_STYLE_CHROME,
887
+ text=event.summary,
888
+ text_style=_STYLE_META,
889
+ ),
890
+ highlight=False,
891
+ )
892
+ preview = _redact(event.diff)
893
+ if len(preview) > 1200:
894
+ preview = preview[:1200] + "\n...(truncated)"
895
+ lines = _split_lines(preview or "(empty diff)")
896
+ for line in lines:
897
+ style = _STYLE_META
898
+ if line.startswith(("diff --git", "--- ", "+++ ", "@@")):
899
+ style = _STYLE_EMPHASIS
900
+ elif line.startswith("+"):
901
+ style = _STYLE_EMPHASIS
902
+ elif line.startswith("-"):
903
+ style = _STYLE_CHROME
904
+ self.console.print(
905
+ _prefixed_text(
906
+ prefix=_bar_prefix(self.console),
907
+ prefix_style=_STYLE_CHROME,
908
+ text=line,
909
+ text_style=style,
910
+ ),
911
+ highlight=False,
912
+ )
913
+
914
+ def on_error(self, err: str) -> None:
915
+ self._flush_reasoning_summary()
916
+ self._stop_thinking_spinner()
917
+ if self._assistant_stream_open:
918
+ self.console.print("")
919
+ self._assistant_stream_open = False
920
+ self._turn_started = False
921
+ self._working_banner_shown = False
922
+ self._reset_thinking()
923
+ try:
924
+ self.console.print(_error_renderable(err))
925
+ except Exception as exc: # noqa: BLE001 - final output fallback must not double-crash.
926
+ safe_plain_error(
927
+ stream=getattr(self.console, "file", None),
928
+ error_type=type(exc).__name__,
929
+ message=err,
930
+ )
931
+
932
+ def on_warning(self, warning: str) -> None:
933
+ self._flush_reasoning_summary()
934
+ self._stop_thinking_spinner()
935
+ if self._assistant_stream_open:
936
+ self.console.print("")
937
+ self._assistant_stream_open = False
938
+ try:
939
+ self.console.print(_warning_renderable(warning))
940
+ except Exception:
941
+ safe_plain_error(
942
+ stream=getattr(self.console, "file", None),
943
+ error_type="Warning",
944
+ message=warning,
945
+ )
946
+
947
+ def _emit_event_info(self, message: str) -> None:
948
+ if self._trace_level == "off":
949
+ return
950
+ self._emit_thinking(message, style=_STYLE_META)
951
+
952
+ def _event_scope_prefix(
953
+ self,
954
+ *,
955
+ worker_id: str | None = None,
956
+ role: str | None = None,
957
+ ) -> str:
958
+ parts = [part for part in (worker_id, role) if part]
959
+ if not parts:
960
+ return ""
961
+ return "[" + " · ".join(parts) + "] "
962
+
963
+ # RichSurface renders these through legacy on_* methods; emit_* are no-ops here so
964
+ # dual emission from agent_loop does not double-render in the Rich CLI.
965
+ def emit_message_delta(
966
+ self,
967
+ text: str,
968
+ *,
969
+ worker_id: str | None = None,
970
+ role: str | None = None,
971
+ ) -> None:
972
+ _ = (text, worker_id, role)
973
+
974
+ def emit_message_end(
975
+ self,
976
+ text: str = "",
977
+ *,
978
+ worker_id: str | None = None,
979
+ role: str | None = None,
980
+ ) -> None:
981
+ _ = (text, worker_id, role)
982
+
983
+ def emit_tool_call_started(
984
+ self,
985
+ call_id: str,
986
+ name: str,
987
+ arguments_preview: str,
988
+ *,
989
+ worker_id: str | None = None,
990
+ role: str | None = None,
991
+ ) -> None:
992
+ _ = (call_id, name, arguments_preview, worker_id, role)
993
+
994
+ def emit_tool_call_progress(
995
+ self,
996
+ call_id: str,
997
+ text: str,
998
+ *,
999
+ worker_id: str | None = None,
1000
+ role: str | None = None,
1001
+ ) -> None:
1002
+ _ = (call_id, text, worker_id, role)
1003
+
1004
+ def emit_tool_call_completed(
1005
+ self,
1006
+ call_id: str,
1007
+ success: bool,
1008
+ result_preview: str,
1009
+ *,
1010
+ worker_id: str | None = None,
1011
+ role: str | None = None,
1012
+ ) -> None:
1013
+ _ = (call_id, success, result_preview, worker_id, role)
1014
+
1015
+ def emit_status_update(
1016
+ self,
1017
+ *,
1018
+ tokens_in: int | None = None,
1019
+ tokens_out: int | None = None,
1020
+ cached_tokens: int | None = None,
1021
+ cost_usd: float | None = None,
1022
+ mode: str | None = None,
1023
+ model: str | None = None,
1024
+ step: int | None = None,
1025
+ step_budget: int | None = None,
1026
+ ) -> None:
1027
+ # TODO: dedicated rendering for status_update.
1028
+ parts: list[str] = []
1029
+ if model:
1030
+ parts.append(f"model={model}")
1031
+ if mode:
1032
+ parts.append(f"mode={mode}")
1033
+ if step is not None:
1034
+ step_text = f"step={step}"
1035
+ if step_budget is not None:
1036
+ step_text += f"/{step_budget}"
1037
+ parts.append(step_text)
1038
+ if tokens_in is not None:
1039
+ parts.append(f"in={tokens_in}")
1040
+ if tokens_out is not None:
1041
+ parts.append(f"out={tokens_out}")
1042
+ if cached_tokens is not None:
1043
+ parts.append(f"cached={cached_tokens}")
1044
+ if cost_usd is not None:
1045
+ parts.append(f"cost=${cost_usd:.6f}")
1046
+ self._emit_event_info("Status: " + (" · ".join(parts) if parts else "updated"))
1047
+
1048
+ def emit_mode_changed(self, mode: str) -> None:
1049
+ # TODO: dedicated rendering for mode_changed.
1050
+ self._emit_event_info(f"Mode: {mode}")
1051
+
1052
+ def reset_streamed_assistant(self) -> None:
1053
+ # Classic terminal output cannot be unprinted; mark the restart so the
1054
+ # restreamed reply that follows is readable as a retry, not an echo.
1055
+ self._emit_event_info("Provider retry — restarting the reply…")
1056
+
1057
+ def emit_persona_changed(self, persona: str, effective_mode: str, source: str = "user") -> None:
1058
+ self._emit_event_info(f"Persona: {persona} ({effective_mode}, {source})")
1059
+
1060
+ def emit_plan_node_updated(
1061
+ self,
1062
+ node_id: str,
1063
+ state: str,
1064
+ summary: str | None = None,
1065
+ *,
1066
+ worker_id: str | None = None,
1067
+ role: str | None = None,
1068
+ ) -> None:
1069
+ # TODO: dedicated rendering for plan_node_updated.
1070
+ prefix = self._event_scope_prefix(worker_id=worker_id, role=role)
1071
+ detail = f" · {summary}" if summary else ""
1072
+ self._emit_event_info(f"{prefix}Plan {node_id}: {state}{detail}")
1073
+
1074
+ def emit_swarm_worker_state_changed(
1075
+ self,
1076
+ worker_id: str,
1077
+ state: str,
1078
+ *,
1079
+ role: str | None = None,
1080
+ ) -> None:
1081
+ # TODO: dedicated rendering for swarm_worker_state_changed.
1082
+ suffix = f" · {role}" if role else ""
1083
+ self._emit_event_info(f"Worker {worker_id}: {state}{suffix}")
1084
+
1085
+ def emit_verify_gate_result(
1086
+ self,
1087
+ command: str,
1088
+ success: bool,
1089
+ summary: str,
1090
+ *,
1091
+ worker_id: str | None = None,
1092
+ role: str | None = None,
1093
+ ) -> None:
1094
+ # TODO: dedicated rendering for verify_gate_result.
1095
+ prefix = self._event_scope_prefix(worker_id=worker_id, role=role)
1096
+ state = "passed" if success else "failed"
1097
+ self._emit_event_info(f"{prefix}Verify {state}: {command} · {summary}")
1098
+
1099
+ def emit_review_gate_decision(
1100
+ self,
1101
+ decision: str,
1102
+ summary: str,
1103
+ *,
1104
+ worker_id: str | None = None,
1105
+ role: str | None = None,
1106
+ ) -> None:
1107
+ # TODO: dedicated rendering for review_gate_decision.
1108
+ prefix = self._event_scope_prefix(worker_id=worker_id, role=role)
1109
+ self._emit_event_info(f"{prefix}Review {decision}: {summary}")
1110
+
1111
+ def emit_error(
1112
+ self,
1113
+ code: str,
1114
+ message: str,
1115
+ recoverable: bool,
1116
+ *,
1117
+ worker_id: str | None = None,
1118
+ role: str | None = None,
1119
+ ) -> None:
1120
+ prefix = self._event_scope_prefix(worker_id=worker_id, role=role)
1121
+ code_text = f"{code}: " if code else ""
1122
+ recovery_text = "" if recoverable else " (not recoverable)"
1123
+ self.on_error(f"{prefix}{code_text}{message}{recovery_text}")
1124
+
1125
+ def emit_warning(
1126
+ self,
1127
+ message: str,
1128
+ *,
1129
+ worker_id: str | None = None,
1130
+ role: str | None = None,
1131
+ ) -> None:
1132
+ prefix = self._event_scope_prefix(worker_id=worker_id, role=role)
1133
+ self.on_warning(f"{prefix}{message}")
1134
+
1135
+ def emit_info(
1136
+ self,
1137
+ message: str,
1138
+ *,
1139
+ worker_id: str | None = None,
1140
+ role: str | None = None,
1141
+ ) -> None:
1142
+ prefix = self._event_scope_prefix(worker_id=worker_id, role=role)
1143
+ self.on_progress_update(f"{prefix}{message}")
1144
+
1145
+ def emit_prompt_for_input(self, prompt_id: str, prompt_text: str, kind: str) -> None:
1146
+ # TODO: dedicated rendering for prompt_for_input.
1147
+ self._emit_event_info(f"Prompt {prompt_id} ({kind}): {prompt_text}")
1148
+
1149
+ def emit_config_form_request(self, form_id: str, schema: dict[str, Any]) -> None:
1150
+ # TODO: dedicated rendering for config_form_request.
1151
+ title = str(schema.get("title") or schema.get("name") or "configuration")
1152
+ self._emit_event_info(f"Config form {form_id}: {title}")
1153
+
1154
+ def emit(self, event: Event) -> None:
1155
+ if event.type == MessageDelta.type:
1156
+ self.emit_message_delta(event.text, worker_id=event.worker_id, role=event.role)
1157
+ elif event.type == MessageEnd.type:
1158
+ self.emit_message_end(event.text, worker_id=event.worker_id, role=event.role)
1159
+ elif event.type == ToolCallStarted.type:
1160
+ self.emit_tool_call_started(
1161
+ event.call_id,
1162
+ event.name,
1163
+ event.arguments_preview,
1164
+ worker_id=event.worker_id,
1165
+ role=event.role,
1166
+ )
1167
+ elif event.type == ToolCallProgress.type:
1168
+ self.emit_tool_call_progress(
1169
+ event.call_id,
1170
+ event.text,
1171
+ worker_id=event.worker_id,
1172
+ role=event.role,
1173
+ )
1174
+ elif event.type == ToolCallCompleted.type:
1175
+ self.emit_tool_call_completed(
1176
+ event.call_id,
1177
+ event.success,
1178
+ event.result_preview,
1179
+ worker_id=event.worker_id,
1180
+ role=event.role,
1181
+ )
1182
+ elif event.type == StatusUpdate.type:
1183
+ self.emit_status_update(
1184
+ tokens_in=event.tokens_in,
1185
+ tokens_out=event.tokens_out,
1186
+ cached_tokens=event.cached_tokens,
1187
+ cost_usd=event.cost_usd,
1188
+ mode=event.mode,
1189
+ model=event.model,
1190
+ step=event.step,
1191
+ step_budget=event.step_budget,
1192
+ )
1193
+ elif event.type == ModeChanged.type:
1194
+ self.emit_mode_changed(event.mode)
1195
+ elif event.type == PersonaChanged.type:
1196
+ self.emit_persona_changed(event.persona, event.effective_mode, event.source)
1197
+ elif event.type == PlanNodeUpdated.type:
1198
+ self.emit_plan_node_updated(
1199
+ event.node_id,
1200
+ event.state,
1201
+ event.summary,
1202
+ worker_id=event.worker_id,
1203
+ role=event.role,
1204
+ )
1205
+ elif event.type == SwarmWorkerStateChanged.type:
1206
+ self.emit_swarm_worker_state_changed(event.worker_id, event.state, role=event.role)
1207
+ elif event.type == VerifyGateResult.type:
1208
+ self.emit_verify_gate_result(
1209
+ event.command,
1210
+ event.success,
1211
+ event.summary,
1212
+ worker_id=event.worker_id,
1213
+ role=event.role,
1214
+ )
1215
+ elif event.type == ReviewGateDecision.type:
1216
+ self.emit_review_gate_decision(
1217
+ event.decision,
1218
+ event.summary,
1219
+ worker_id=event.worker_id,
1220
+ role=event.role,
1221
+ )
1222
+ elif event.type == ErrorRaised.type:
1223
+ self.emit_error(
1224
+ event.code,
1225
+ event.message,
1226
+ event.recoverable,
1227
+ worker_id=event.worker_id,
1228
+ role=event.role,
1229
+ )
1230
+ elif event.type == WarningEmitted.type:
1231
+ self.emit_warning(event.message, worker_id=event.worker_id, role=event.role)
1232
+ elif event.type == InfoEmitted.type:
1233
+ self.emit_info(event.message, worker_id=event.worker_id, role=event.role)
1234
+ elif event.type == PromptForInput.type:
1235
+ self.emit_prompt_for_input(event.prompt_id, event.prompt_text, event.kind)
1236
+ elif event.type == ConfigFormRequest.type:
1237
+ self.emit_config_form_request(event.form_id, event.schema)
1238
+
1239
+ def _prompt_approval_choice_fallback(self) -> str:
1240
+ try:
1241
+ return (
1242
+ Prompt.ask(
1243
+ "Select option",
1244
+ console=self.console,
1245
+ default="n",
1246
+ show_default=False,
1247
+ show_choices=False,
1248
+ )
1249
+ .strip()
1250
+ .lower()
1251
+ )
1252
+ except (EOFError, KeyboardInterrupt):
1253
+ return "n"
1254
+
1255
+ def _uses_inline_approval_selector(self) -> bool:
1256
+ if not getattr(self.console, "is_terminal", False):
1257
+ return False
1258
+ if os.name != "posix":
1259
+ return False
1260
+ try:
1261
+ fd = os.open("/dev/tty", os.O_RDWR)
1262
+ except Exception:
1263
+ return False
1264
+ try:
1265
+ return True
1266
+ finally:
1267
+ os.close(fd)
1268
+
1269
+ def _approval_selector_options(self) -> list[tuple[str, str]]:
1270
+ return [
1271
+ ("y", "Allow"),
1272
+ ("a", "Always allow for session"),
1273
+ ("n", "Deny"),
1274
+ ("v", "View context"),
1275
+ ]
1276
+
1277
+ def _approval_confirmation(self, choice: str) -> tuple[str, str] | None:
1278
+ mapping = {
1279
+ "y": (" Allowed", _STYLE_SUCCESS),
1280
+ "a": (" Always allowed", _STYLE_SUCCESS),
1281
+ "n": (" Denied", _STYLE_WARNING),
1282
+ }
1283
+ return mapping.get(choice)
1284
+
1285
+ def _render_approval_selector_menu(self, *, selected_index: int) -> None:
1286
+ options = self._approval_selector_options()
1287
+ for index, (_key, label) in enumerate(options):
1288
+ selected = index == selected_index
1289
+ prefix = " > " if selected else " "
1290
+ prefix_style = "bold green" if selected else _STYLE_META
1291
+ text_style = _STYLE_EMPHASIS if selected else _STYLE_META
1292
+ self.console.print(
1293
+ Text.assemble((prefix, prefix_style), (label, text_style)),
1294
+ highlight=False,
1295
+ )
1296
+
1297
+ def _erase_approval_selector_menu(self) -> None:
1298
+ stream = self.console.file if self.console.file is not None else sys.stdout
1299
+ for _ in range(len(self._approval_selector_options())):
1300
+ safe_write(stream, "\033[A\033[2K")
1301
+ safe_write(stream, "\r")
1302
+
1303
+ def _read_approval_selector_keypress(self) -> str:
1304
+ import select
1305
+ import termios
1306
+ import tty
1307
+
1308
+ fd = os.open("/dev/tty", os.O_RDWR)
1309
+ old_settings = termios.tcgetattr(fd)
1310
+ try:
1311
+ tty.setraw(fd)
1312
+ first = os.read(fd, 1)
1313
+ if not first:
1314
+ raise EOFError
1315
+ ch = first.decode("utf-8", errors="ignore")
1316
+ if ch == "\x1b":
1317
+ if select.select([fd], [], [], 0.05)[0]:
1318
+ second = os.read(fd, 1)
1319
+ if not second:
1320
+ return "\x1b"
1321
+ ch2 = second.decode("utf-8", errors="ignore")
1322
+ if ch2 == "[":
1323
+ if not select.select([fd], [], [], 0.05)[0]:
1324
+ return "\x1b"
1325
+ third = os.read(fd, 1)
1326
+ if not third:
1327
+ return "\x1b"
1328
+ ch3 = third.decode("utf-8", errors="ignore")
1329
+ return f"\x1b[{ch3}"
1330
+ if ch2 == "O":
1331
+ if not select.select([fd], [], [], 0.05)[0]:
1332
+ return "\x1b"
1333
+ third = os.read(fd, 1)
1334
+ if not third:
1335
+ return "\x1b"
1336
+ ch3 = third.decode("utf-8", errors="ignore")
1337
+ return f"\x1bO{ch3}"
1338
+ return "\x1b"
1339
+ return ch
1340
+ finally:
1341
+ try:
1342
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
1343
+ finally:
1344
+ os.close(fd)
1345
+
1346
+ def _prompt_approval_selector(self) -> str:
1347
+ options = self._approval_selector_options()
1348
+ selected_index = 0
1349
+ self._render_approval_selector_menu(selected_index=selected_index)
1350
+ menu_visible = True
1351
+ try:
1352
+ while True:
1353
+ try:
1354
+ key = self._read_approval_selector_keypress()
1355
+ except (EOFError, KeyboardInterrupt):
1356
+ key = "n"
1357
+ except Exception:
1358
+ if menu_visible:
1359
+ self._erase_approval_selector_menu()
1360
+ menu_visible = False
1361
+ return self._prompt_approval_choice_fallback()
1362
+
1363
+ lowered = key.lower() if key else ""
1364
+ if key in {"\r", "\n"}:
1365
+ choice = options[selected_index][0]
1366
+ break
1367
+ if lowered in {"y", "a", "n", "v"}:
1368
+ choice = lowered
1369
+ break
1370
+ if lowered == "k" or key == "\x1b[A":
1371
+ selected_index = (selected_index - 1) % len(options)
1372
+ elif lowered == "j" or key == "\x1b[B":
1373
+ selected_index = (selected_index + 1) % len(options)
1374
+ elif key in {"\x1b[H", "\x1bOH"}:
1375
+ selected_index = 0
1376
+ elif key in {"\x1b[F", "\x1bOF"}:
1377
+ selected_index = len(options) - 1
1378
+ elif key in {"\x03", "\x04", "\x1b"}:
1379
+ choice = "n"
1380
+ break
1381
+ else:
1382
+ continue
1383
+ self._erase_approval_selector_menu()
1384
+ menu_visible = False
1385
+ self._render_approval_selector_menu(selected_index=selected_index)
1386
+ menu_visible = True
1387
+ finally:
1388
+ if menu_visible:
1389
+ self._erase_approval_selector_menu()
1390
+
1391
+ confirmation = self._approval_confirmation(choice)
1392
+ if confirmation is not None:
1393
+ message, style = confirmation
1394
+ self.console.print(message, style=style, highlight=False)
1395
+ return choice
1396
+
1397
+ def _prompt_approval_choice(self) -> str:
1398
+ if self._uses_inline_approval_selector():
1399
+ with interactive_prompt_guard(owns_terminal=True):
1400
+ return self._prompt_approval_selector()
1401
+ with interactive_prompt_guard():
1402
+ return self._prompt_approval_choice_fallback()
1403
+
1404
+ def request_approval(self, request: ApprovalRequest) -> ApprovalDecision:
1405
+ self._stop_thinking_spinner()
1406
+ scope_info = approval_session_scope_for_request(request)
1407
+ if scope_info.key is not None and scope_info.key in self._allow_for_session:
1408
+ return ApprovalDecision(allow=True, allow_for_session=True)
1409
+
1410
+ files = ", ".join(request.files[:8]) if request.files else "-"
1411
+ command = request.command or "-"
1412
+ preview = _redact(request.preview or "")
1413
+ preview_available = bool(preview.strip())
1414
+ lowered_command = command.lower()
1415
+ destructive = request.kind == "fs_delete" or any(
1416
+ token in lowered_command for token in (" rm ", "rm -", "force", "delete")
1417
+ )
1418
+ bar_style = "red" if destructive else _STYLE_EMPHASIS
1419
+ title_style = "red" if destructive else _STYLE_EMPHASIS
1420
+ target = command if request.command else files
1421
+ if request.kind.startswith("custom_tool_run:"):
1422
+ action_label = "Run custom tool"
1423
+ else:
1424
+ action_label = {
1425
+ "shell_run": "Run command",
1426
+ "fs_write": "Write files",
1427
+ "fs_delete": "Delete files",
1428
+ }.get(request.kind, f"Approve {request.kind}")
1429
+ headline = action_label
1430
+ if request.reason:
1431
+ headline += f"{_separator(self.console)}{request.reason}"
1432
+
1433
+ self.console.print(
1434
+ _prefixed_text(
1435
+ prefix=_bar_prefix(self.console),
1436
+ prefix_style=bar_style,
1437
+ text=headline,
1438
+ text_style=title_style,
1439
+ ),
1440
+ highlight=False,
1441
+ )
1442
+ self.console.print(
1443
+ _prefixed_text(
1444
+ prefix=_bar_prefix(self.console),
1445
+ prefix_style=bar_style,
1446
+ text=target,
1447
+ text_style=_STYLE_CONTENT,
1448
+ ),
1449
+ highlight=False,
1450
+ )
1451
+ if request.command and request.files:
1452
+ self.console.print(
1453
+ _prefixed_text(
1454
+ prefix=" ",
1455
+ prefix_style=_STYLE_CONTENT,
1456
+ text=f"Files: {files}",
1457
+ text_style=_STYLE_META,
1458
+ ),
1459
+ highlight=False,
1460
+ )
1461
+ context_line = (
1462
+ f"Context available ({len(preview)} chars) · [v] view"
1463
+ if preview_available
1464
+ else "Context none"
1465
+ )
1466
+ if preview_available:
1467
+ self.console.print(
1468
+ Text.assemble(
1469
+ (" ", _STYLE_CONTENT),
1470
+ ("Context available ", _STYLE_META),
1471
+ ("[v]", _STYLE_EMPHASIS),
1472
+ (" to view", _STYLE_META),
1473
+ ),
1474
+ highlight=False,
1475
+ )
1476
+ else:
1477
+ self.console.print(
1478
+ _prefixed_text(
1479
+ prefix=" ",
1480
+ prefix_style=_STYLE_CONTENT,
1481
+ text=context_line,
1482
+ text_style=_STYLE_META,
1483
+ ),
1484
+ highlight=False,
1485
+ )
1486
+ if not self._uses_inline_approval_selector():
1487
+ options = [
1488
+ (" ", _STYLE_CONTENT),
1489
+ ("[y]", _STYLE_EMPHASIS),
1490
+ (" allow ", _STYLE_META),
1491
+ ]
1492
+ if scope_info.supported:
1493
+ options.extend(
1494
+ [
1495
+ ("[a]", _STYLE_EMPHASIS),
1496
+ (" always ", _STYLE_META),
1497
+ ]
1498
+ )
1499
+ options.extend(
1500
+ [
1501
+ ("[n]", _STYLE_EMPHASIS),
1502
+ (" deny ", _STYLE_META),
1503
+ ("[v]", _STYLE_EMPHASIS),
1504
+ (" view", _STYLE_META),
1505
+ ]
1506
+ )
1507
+ self.console.print(Text.assemble(*options), highlight=False)
1508
+ elif scope_info.warning:
1509
+ self.console.print(
1510
+ _prefixed_text(
1511
+ prefix=" ",
1512
+ prefix_style=_STYLE_CONTENT,
1513
+ text=scope_info.warning,
1514
+ text_style=_STYLE_META,
1515
+ ),
1516
+ highlight=False,
1517
+ )
1518
+ while True:
1519
+ choice = self._prompt_approval_choice()
1520
+ if choice in {"1", "y"}:
1521
+ return ApprovalDecision(allow=True)
1522
+ if choice in {"2", "a"}:
1523
+ if scope_info.supported and scope_info.key is not None:
1524
+ self._allow_for_session.add(scope_info.key)
1525
+ return ApprovalDecision(allow=True, allow_for_session=True)
1526
+ self.console.print(
1527
+ _prefixed_text(
1528
+ prefix=" ",
1529
+ prefix_style=_STYLE_CONTENT,
1530
+ text=scope_info.warning
1531
+ or "Allow for session is unavailable for this approval.",
1532
+ text_style=_STYLE_META,
1533
+ ),
1534
+ highlight=False,
1535
+ )
1536
+ return ApprovalDecision(allow=True)
1537
+ if choice in {"3", "n"}:
1538
+ return ApprovalDecision(allow=False)
1539
+ if choice in {"4", "v"}:
1540
+ self.console.print(
1541
+ _prefixed_text(
1542
+ prefix=_bar_prefix(self.console),
1543
+ prefix_style=_STYLE_CHROME,
1544
+ text="Context",
1545
+ text_style=_STYLE_EMPHASIS,
1546
+ ),
1547
+ highlight=False,
1548
+ )
1549
+ self._print_left_bar_block(
1550
+ _split_lines(_redact(request.preview or "") or "(no preview)"),
1551
+ bar_style=_STYLE_CHROME,
1552
+ text_style=_STYLE_CONTENT,
1553
+ )
1554
+ continue
1555
+ self.console.print("Enter y, a, n, or v.", style="red", highlight=False)