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,514 @@
1
+ """Thread-safe conversation model for the full-screen TUI.
2
+
3
+ The agent turn runs on a worker thread and pushes content here (streamed
4
+ assistant tokens, tool-trace lines, errors); the prompt_toolkit UI thread reads
5
+ snapshots of it on every redraw. All mutation goes through a lock and triggers a
6
+ (thread-safe) ``invalidate`` so the next redraw picks up the change.
7
+
8
+ Kept free of any agent imports so it can be unit-tested in isolation.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import threading
14
+ import time
15
+ from collections.abc import Callable
16
+
17
+ from .forge_status import forge_status_bucket, forge_status_is_terminal
18
+
19
+ # (role, text) — role drives styling in the app: "user" | "assistant" | "reasoning"
20
+ # | "trace" | "error" | "warn" | "info" | "system".
21
+ Entry = tuple[str, str]
22
+
23
+
24
+ def _normalize_visible(text: str) -> str:
25
+ """Whitespace-collapsed comparison key for assistant text, so a re-emitted
26
+ reply that differs only in incidental spacing still reads as a duplicate."""
27
+ return " ".join(str(text or "").split())
28
+
29
+
30
+ class TuiTranscript:
31
+ def __init__(self, *, invalidate: Callable[[], None] | None = None) -> None:
32
+ self.entries: list[Entry] = []
33
+ self._lock = threading.RLock()
34
+ self._status: str | None = None
35
+ self._assistant_index: int | None = None
36
+ # Live provider-generated reasoning-summary block: index of the open
37
+ # entry, when it started, and elapsed seconds for each closed block.
38
+ self._reasoning_index: int | None = None
39
+ self._reasoning_start: float | None = None
40
+ self._reasoning_block_id: str | None = None
41
+ self._reasoning_block_ids: dict[int, str] = {}
42
+ self._reasoning_secs: dict[int, int] = {}
43
+ self._trace_level = "compact"
44
+ # Live Forge execution view: a task table + phase/spinner rendered at the
45
+ # bottom of the transcript while ``/execute plan`` runs the swarm on a
46
+ # worker thread. ``None`` when no run is active. Shape:
47
+ # {"run_id", "tasks": [{"id","title","status"}], "active": str|None,
48
+ # "active_map": {task_id: {"started": float, "phase": str,
49
+ # "message": str}},
50
+ # "phase": str, "message": str, "started": float, "done": bool}
51
+ # ``active`` is the MOST-RECENT task the swarm touched (drives the single
52
+ # bottom phase line, kept for back-compat); ``active_map`` tracks EVERY
53
+ # currently-running task so the renderer can spin them all and show a
54
+ # per-task elapsed timer. A task is pruned from ``active_map`` the moment
55
+ # its status turns terminal (done/failed/obsolete).
56
+ self._forge: dict[str, object] | None = None
57
+ self._invalidate: Callable[[], None] = invalidate or (lambda: None)
58
+
59
+ def set_invalidate(self, fn: Callable[[], None]) -> None:
60
+ self._invalidate = fn
61
+
62
+ @property
63
+ def trace_level(self) -> str:
64
+ with self._lock:
65
+ return self._trace_level
66
+
67
+ def set_trace_level(self, level: str) -> str:
68
+ normalized = str(level or "").strip().lower()
69
+ with self._lock:
70
+ if normalized not in {"off", "compact", "full"}:
71
+ normalized = self._trace_level
72
+ self._trace_level = normalized
73
+ self._touch()
74
+ return normalized
75
+
76
+ def _touch(self) -> None:
77
+ try:
78
+ self._invalidate()
79
+ except Exception:
80
+ pass
81
+
82
+ # ---- safe provider reasoning-summary block ----
83
+ def _close_reasoning_locked(self) -> None:
84
+ """Close the open reasoning block, recording its elapsed seconds. Caller
85
+ holds the lock. Any content/tool/turn boundary collapses the summary."""
86
+ if self._reasoning_index is not None:
87
+ if self._reasoning_start is not None:
88
+ self._reasoning_secs[self._reasoning_index] = max(
89
+ 0, int(time.monotonic() - self._reasoning_start)
90
+ )
91
+ self._reasoning_index = None
92
+ self._reasoning_start = None
93
+ self._reasoning_block_id = None
94
+
95
+ def begin_reasoning(self, block_id: str) -> None:
96
+ """Open a safe-summary block with a stable provider-call identifier.
97
+
98
+ Reusing the active identifier is idempotent. A different identifier
99
+ closes the previous block first so retries and later agent steps can
100
+ never merge their summaries merely because no visible tool line landed
101
+ between the two provider calls.
102
+ """
103
+
104
+ normalized = str(block_id or "").strip()
105
+ if not normalized:
106
+ return
107
+ with self._lock:
108
+ if self._reasoning_index is not None and self._reasoning_block_id == normalized:
109
+ return
110
+ self._close_reasoning_locked()
111
+ self.entries.append(("reasoning", ""))
112
+ self._reasoning_index = len(self.entries) - 1
113
+ self._reasoning_start = time.monotonic()
114
+ self._reasoning_block_id = normalized
115
+ self._reasoning_block_ids[self._reasoning_index] = normalized
116
+ self._touch()
117
+
118
+ def end_reasoning(self, block_id: str | None = None) -> None:
119
+ """Collapse the open summary block (e.g. when a tool starts) without
120
+ appending anything — records its elapsed seconds for the summary."""
121
+ with self._lock:
122
+ normalized = str(block_id or "").strip()
123
+ if normalized and normalized != self._reasoning_block_id:
124
+ return
125
+ self._close_reasoning_locked()
126
+ self._touch()
127
+
128
+ def stream_reasoning(self, delta: str) -> None:
129
+ if not delta:
130
+ return
131
+ with self._lock:
132
+ if self._reasoning_index is None:
133
+ self.entries.append(("reasoning", ""))
134
+ self._reasoning_index = len(self.entries) - 1
135
+ self._reasoning_start = time.monotonic()
136
+ self._reasoning_block_id = f"legacy-{self._reasoning_index}"
137
+ self._reasoning_block_ids[self._reasoning_index] = self._reasoning_block_id
138
+ role, current = self.entries[self._reasoning_index]
139
+ self.entries[self._reasoning_index] = (role, current + delta)
140
+ self._touch()
141
+
142
+ # ---- generic appends ----
143
+ def append(self, role: str, text: str) -> None:
144
+ with self._lock:
145
+ self._close_reasoning_locked()
146
+ self.entries.append((role, text))
147
+ # Any non-streamed line closes the open assistant block.
148
+ if role != "assistant":
149
+ self._assistant_index = None
150
+ self._touch()
151
+
152
+ def append_trace(self, line: str) -> None:
153
+ """Append a trace line, coalescing into the previous trace block.
154
+
155
+ Consecutive trace lines (e.g. a stream of swarm-progress events) join into
156
+ one multi-line entry instead of becoming separate blocks — the renderer
157
+ puts a blank spacer *between* entries, so without coalescing a busy run
158
+ would be sprayed across the pane."""
159
+ with self._lock:
160
+ self._close_reasoning_locked()
161
+ if self.entries and self.entries[-1][0] == "trace":
162
+ role, current = self.entries[-1]
163
+ self.entries[-1] = (role, f"{current}\n{line}")
164
+ else:
165
+ self.entries.append(("trace", line))
166
+ self._assistant_index = None
167
+ self._touch()
168
+
169
+ def append_user(self, text: str) -> None:
170
+ with self._lock:
171
+ self._close_reasoning_locked()
172
+ self.entries.append(("user", text))
173
+ self._assistant_index = None
174
+ # A fresh submission ends the prior live Forge view (its final table
175
+ # stays in scrollback only while it is the latest thing shown).
176
+ self._forge = None
177
+ self._touch()
178
+
179
+ # ---- streaming assistant block ----
180
+ def begin_turn(self) -> None:
181
+ with self._lock:
182
+ self._close_reasoning_locked()
183
+ self._assistant_index = None
184
+ self._status = None
185
+ self._touch()
186
+
187
+ def stream_assistant(self, delta: str) -> None:
188
+ if not delta:
189
+ return
190
+ with self._lock:
191
+ # The first answer token collapses the reasoning block.
192
+ self._close_reasoning_locked()
193
+ if self._assistant_index is None:
194
+ self.entries.append(("assistant", ""))
195
+ self._assistant_index = len(self.entries) - 1
196
+ role, current = self.entries[self._assistant_index]
197
+ self.entries[self._assistant_index] = (role, current + delta)
198
+ self._status = None
199
+ self._touch()
200
+
201
+ def restart_assistant(self) -> None:
202
+ """Clear the open streamed block: a transport retry is about to
203
+ restream the reply from scratch, so the abandoned generation's tokens
204
+ must not remain in front of it."""
205
+ with self._lock:
206
+ if self._assistant_index is not None:
207
+ role, _current = self.entries[self._assistant_index]
208
+ self.entries[self._assistant_index] = (role, "")
209
+ self._touch()
210
+
211
+ def finish_assistant(self, text: str = "") -> None:
212
+ with self._lock:
213
+ self._close_reasoning_locked()
214
+ if self._assistant_index is not None:
215
+ role, current = self.entries[self._assistant_index]
216
+ if not current and text:
217
+ self.entries[self._assistant_index] = (role, text)
218
+ elif current and text:
219
+ cur_norm = _normalize_visible(current)
220
+ txt_norm = _normalize_visible(text)
221
+ if (
222
+ cur_norm != txt_norm
223
+ and cur_norm.endswith(txt_norm)
224
+ and len(txt_norm) * 4 >= len(cur_norm)
225
+ ):
226
+ # A provider-level retry restreamed the reply into the
227
+ # same live block: a mid-stream failure abandons
228
+ # generation A after its tokens are already visible,
229
+ # the transport retry re-runs the request, and
230
+ # generation B streams into the same open block — the
231
+ # block ends up "A + B" (wordings can differ, so the
232
+ # verbatim dedupe below never catches it). The final
233
+ # text is authoritative — it is exactly what the
234
+ # session history keeps — so when the block ends with
235
+ # it and carries an abandoned prefix, snap to the
236
+ # final. The >=25% length guard keeps a pathological
237
+ # tail-fragment final from truncating a legit block.
238
+ self.entries[self._assistant_index] = (role, text)
239
+ elif text.strip():
240
+ # A multi-step turn can re-emit the same answer after its live
241
+ # block was already closed by an intervening tool/trace line (which
242
+ # reset _assistant_index). Only append a fresh block when it is NOT
243
+ # a verbatim repeat of the turn's most recent assistant block —
244
+ # otherwise the same reply stacks up 2-3 times. Content-based and
245
+ # provider-agnostic (see also the render-layer collapse in app.py).
246
+ last = self._last_turn_assistant_text_locked()
247
+ if last is None or _normalize_visible(last) != _normalize_visible(text):
248
+ self.entries.append(("assistant", text))
249
+ self._assistant_index = None
250
+ self._status = None
251
+ self._touch()
252
+
253
+ def _last_turn_assistant_text_locked(self) -> str | None:
254
+ """Text of the most recent assistant entry within the current turn, or
255
+ ``None`` if none precedes a ``user`` boundary. Caller holds the lock."""
256
+ for role, text in reversed(self.entries):
257
+ if role == "user":
258
+ return None
259
+ if role == "assistant":
260
+ return text
261
+ return None
262
+
263
+ # ---- transient status (working line) ----
264
+ def set_status(self, status: str | None) -> None:
265
+ with self._lock:
266
+ self._status = (status or None) if status is None else (status.strip() or None)
267
+ self._touch()
268
+
269
+ @property
270
+ def status(self) -> str | None:
271
+ with self._lock:
272
+ return self._status
273
+
274
+ # ---- live Forge execution view ----
275
+ def forge_begin(self, run_id: str, tasks: list[tuple[str, str, str]]) -> None:
276
+ """Open the live Forge execution view with the initial task list.
277
+
278
+ ``tasks`` is a list of ``(id, title, status)``; statuses then refresh as
279
+ the swarm runs (``forge_update_statuses``) and on completion
280
+ (``forge_finish``)."""
281
+ with self._lock:
282
+ self._forge = {
283
+ "run_id": str(run_id or ""),
284
+ "tasks": [
285
+ {"id": str(tid), "title": str(title), "status": str(status or "planned")}
286
+ for tid, title, status in tasks
287
+ ],
288
+ "active": None,
289
+ "active_map": {},
290
+ "phase": "execute",
291
+ "message": "Starting…",
292
+ "started": time.monotonic(),
293
+ "done": False,
294
+ "ok": True,
295
+ }
296
+ self._touch()
297
+
298
+ def _forge_reconcile_active_map_locked(self) -> None:
299
+ """Keep ``active_map`` in step with the current task statuses.
300
+
301
+ Adds a per-task start stamp when a task enters a running state (so a
302
+ status-driven run — not just an event-driven one — gets a live timer),
303
+ and prunes any task whose status has turned terminal so a finished row
304
+ stops spinning. Caller holds the lock."""
305
+ if self._forge is None:
306
+ return
307
+ active_map: dict = self._forge["active_map"] # type: ignore[assignment]
308
+ for task in self._forge["tasks"]: # type: ignore[index]
309
+ tid = task["id"]
310
+ status = str(task.get("status") or "")
311
+ if forge_status_is_terminal(status):
312
+ active_map.pop(tid, None)
313
+ elif forge_status_bucket(status) == "running" and tid not in active_map:
314
+ active_map[tid] = {"started": time.monotonic(), "phase": "", "message": ""}
315
+
316
+ def forge_update_statuses(self, statuses: dict[str, str]) -> None:
317
+ """Update task statuses (keyed by task id) from the latest plan snapshot."""
318
+ with self._lock:
319
+ if self._forge is None:
320
+ return
321
+ for task in self._forge["tasks"]: # type: ignore[index]
322
+ new_status = statuses.get(task["id"])
323
+ if new_status:
324
+ task["status"] = str(new_status)
325
+ self._forge_reconcile_active_map_locked()
326
+ self._touch()
327
+
328
+ def forge_sync_tasks(self, tasks: list[tuple[str, str, str]]) -> None:
329
+ """Reconcile the live view with the plan on disk: update existing task
330
+ statuses AND append any new tasks (e.g. ones plan enrichment added mid-run),
331
+ preserving order so the table never silently drops a real task."""
332
+ with self._lock:
333
+ if self._forge is None:
334
+ return
335
+ existing = {task["id"]: task for task in self._forge["tasks"]} # type: ignore[index]
336
+ for tid, title, status in tasks:
337
+ row = existing.get(str(tid))
338
+ if row is not None:
339
+ if status:
340
+ row["status"] = str(status)
341
+ else:
342
+ new_row = {
343
+ "id": str(tid),
344
+ "title": str(title),
345
+ "status": str(status or "planned"),
346
+ }
347
+ self._forge["tasks"].append(new_row) # type: ignore[union-attr]
348
+ existing[str(tid)] = new_row
349
+ self._forge_reconcile_active_map_locked()
350
+ self._touch()
351
+
352
+ def forge_set_active(self, task_id: str | None, phase: str = "", message: str = "") -> None:
353
+ """Mark the task the swarm is currently working on, plus the phase/message
354
+ shown on the live status line."""
355
+ with self._lock:
356
+ if self._forge is None:
357
+ return
358
+ if task_id is not None:
359
+ tid = str(task_id)
360
+ self._forge["active"] = tid
361
+ # Upsert into the live worker set: a first sighting stamps its
362
+ # start (for the per-task timer); later events only refresh the
363
+ # phase/message. A terminal task is never (re)added — a late event
364
+ # after completion must not resurrect a finished row.
365
+ active_map: dict = self._forge["active_map"] # type: ignore[assignment]
366
+ if not forge_status_is_terminal(self._forge_task_status_locked(tid)):
367
+ entry = active_map.get(tid)
368
+ if entry is None:
369
+ entry = {"started": time.monotonic(), "phase": "", "message": ""}
370
+ active_map[tid] = entry
371
+ if phase:
372
+ entry["phase"] = str(phase)
373
+ if message:
374
+ entry["message"] = str(message)
375
+ if phase:
376
+ self._forge["phase"] = str(phase)
377
+ if message:
378
+ self._forge["message"] = str(message)
379
+ self._touch()
380
+
381
+ def _forge_task_status_locked(self, task_id: str) -> str:
382
+ """Current status of ``task_id`` in the live view ('' if unknown).
383
+ Caller holds the lock."""
384
+ if self._forge is None:
385
+ return ""
386
+ for task in self._forge["tasks"]: # type: ignore[index]
387
+ if task["id"] == task_id:
388
+ return str(task.get("status") or "")
389
+ return ""
390
+
391
+ def forge_finish(self, statuses: dict[str, str], summary: str = "", ok: bool = True) -> None:
392
+ """Apply final task statuses and mark the run done (table stays visible).
393
+
394
+ ``ok`` is the overall outcome (no failed/remaining tasks) — it colours the
395
+ summary line green vs red so it never contradicts the per-task glyphs."""
396
+ with self._lock:
397
+ if self._forge is None:
398
+ return
399
+ for task in self._forge["tasks"]: # type: ignore[index]
400
+ new_status = statuses.get(task["id"])
401
+ if new_status:
402
+ task["status"] = str(new_status)
403
+ self._forge["active"] = None
404
+ self._forge["active_map"] = {} # run over: no live workers
405
+ self._forge["done"] = True
406
+ self._forge["ok"] = bool(ok)
407
+ self._forge["phase"] = "done"
408
+ if summary:
409
+ self._forge["message"] = str(summary)
410
+ self._touch()
411
+
412
+ def forge_clear(self) -> None:
413
+ with self._lock:
414
+ self._forge = None
415
+ self._touch()
416
+
417
+ def forge_snapshot(self) -> dict[str, object] | None:
418
+ """Return a copy of the live Forge view, or ``None`` when inactive."""
419
+ with self._lock:
420
+ if self._forge is None:
421
+ return None
422
+ return {
423
+ "run_id": self._forge["run_id"],
424
+ "tasks": [dict(task) for task in self._forge["tasks"]], # type: ignore[union-attr]
425
+ "active": self._forge["active"],
426
+ "active_map": {
427
+ tid: dict(info)
428
+ for tid, info in self._forge["active_map"].items() # type: ignore[union-attr]
429
+ },
430
+ "phase": self._forge["phase"],
431
+ "message": self._forge["message"],
432
+ "started": self._forge["started"],
433
+ "done": self._forge["done"],
434
+ "ok": self._forge.get("ok", True),
435
+ }
436
+
437
+ def clear(self) -> None:
438
+ with self._lock:
439
+ self.entries.clear()
440
+ self._assistant_index = None
441
+ self._reasoning_index = None
442
+ self._reasoning_start = None
443
+ self._reasoning_block_id = None
444
+ self._reasoning_block_ids.clear()
445
+ self._reasoning_secs.clear()
446
+ self._status = None
447
+ self._forge = None
448
+ self._touch()
449
+
450
+ def load_history(self, messages: list[dict[str, object]]) -> None:
451
+ """Replace the transcript with a resumed conversation's history.
452
+
453
+ Clears every transient bit (like :meth:`clear`) and repopulates the pane
454
+ from ``messages`` (the loaded history of a ``/resume`` target) so the user
455
+ sees the prior conversation after switching sessions. Only ``user`` and
456
+ ``assistant`` turns with text content are shown — tool calls/results are
457
+ omitted so the reloaded view reads as the clean conversation. Because no
458
+ assistant block stays "open" (``_assistant_index`` is reset), every
459
+ assistant entry renders as a completed (markdown) reply.
460
+ """
461
+ with self._lock:
462
+ self.entries.clear()
463
+ self._assistant_index = None
464
+ self._reasoning_index = None
465
+ self._reasoning_start = None
466
+ self._reasoning_block_id = None
467
+ self._reasoning_block_ids.clear()
468
+ self._reasoning_secs.clear()
469
+ self._status = None
470
+ self._forge = None
471
+ for message in messages or []:
472
+ if not isinstance(message, dict):
473
+ continue
474
+ role = str(message.get("role") or "").strip().lower()
475
+ if role not in ("user", "assistant"):
476
+ continue
477
+ content = message.get("content")
478
+ if not isinstance(content, str):
479
+ continue
480
+ text = content.rstrip()
481
+ if not text:
482
+ continue
483
+ self.entries.append((role, text))
484
+ self._touch()
485
+
486
+ def reasoning_snapshot(self) -> tuple[int | None, dict[int, int]]:
487
+ """Return ``(live_reasoning_index, {entry_index: elapsed_seconds})``.
488
+
489
+ ``live_reasoning_index`` is the still-streaming reasoning entry (rendered
490
+ in full, dim); the map gives the duration of each closed block so the
491
+ renderer can collapse it to a one-line "thought for Ns" summary.
492
+ """
493
+ with self._lock:
494
+ return self._reasoning_index, dict(self._reasoning_secs)
495
+
496
+ def reasoning_block_ids(self) -> dict[int, str]:
497
+ """Return stable identifiers for safe-summary transcript entries."""
498
+
499
+ with self._lock:
500
+ return dict(self._reasoning_block_ids)
501
+
502
+ def snapshot(self) -> tuple[list[Entry], str | None, int | None]:
503
+ """Return ``(entries, status, streaming_index)`` under the lock.
504
+
505
+ ``streaming_index`` is the index of the still-streaming assistant entry
506
+ (``None`` when no block is open), so the renderer keeps that one plain and
507
+ only markdown-renders completed replies — read atomically here to avoid a
508
+ race with a worker finishing the block mid-redraw.
509
+ """
510
+ with self._lock:
511
+ return list(self.entries), self._status, self._assistant_index
512
+
513
+
514
+ __all__ = ["TuiTranscript", "Entry"]
@@ -0,0 +1,79 @@
1
+ """TUI-native startup prompt for a newer Alysis Code release.
2
+
3
+ Shown at launch — before the first-run setup wizard and the guarded-workspace
4
+ picker — whenever the cached update check knows about a newer PyPI release the
5
+ user has not skipped or snoozed. Rendered as its own short-lived full-screen
6
+ ``Application`` via the shared :func:`_run_option_picker`, so it matches the
7
+ visual grammar of the setup + workspace-guard screens.
8
+
9
+ Degrades gracefully: any terminal / prompt_toolkit failure returns
10
+ ``(None, False)`` so the caller can fall back to the classic inline prompt,
11
+ exactly like the workspace-guard pickers do.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import Any
17
+
18
+ from .workspace_guard import _run_option_picker
19
+
20
+ UPDATE_ACTION_UPDATE = "update"
21
+ UPDATE_ACTION_LATER = "later"
22
+ UPDATE_ACTION_SKIP = "skip"
23
+
24
+
25
+ def select_update_action(
26
+ *,
27
+ current_version: str,
28
+ latest_version: str,
29
+ command: str | None = None,
30
+ unsupported_reason: str | None = None,
31
+ input: Any | None = None,
32
+ output: Any | None = None,
33
+ ) -> tuple[str | None, bool]:
34
+ """Ask whether to apply a newer release now.
35
+
36
+ Returns ``(action, interactive_available)`` where ``action`` is one of
37
+ ``"update"`` / ``"later"`` / ``"skip"``, or ``None`` when the user cancels
38
+ (Esc / Ctrl-C — treated as "later" by the caller). Focus starts on
39
+ "Remind me later" so a reflexive Enter never runs an installer — matching
40
+ the classic prompt's ``default="n"`` and the `alysis update` flow's
41
+ ``typer.confirm(default=False)``.
42
+ """
43
+ if command:
44
+ update_label = "Update now"
45
+ update_desc = f"Runs: {command}"
46
+ else:
47
+ # No runnable command for this install: the row shows how to update
48
+ # manually instead of promising an automatic one.
49
+ update_label = "Show update instructions"
50
+ update_desc = unsupported_reason or "Shows how to update this install."
51
+ rows = [
52
+ (UPDATE_ACTION_UPDATE, update_label, update_desc),
53
+ (
54
+ UPDATE_ACTION_LATER,
55
+ "Remind me later",
56
+ "Keep this version for now; ask again on a later launch.",
57
+ ),
58
+ (
59
+ UPDATE_ACTION_SKIP,
60
+ f"Skip version {latest_version}",
61
+ "Don't ask again for this release.",
62
+ ),
63
+ ]
64
+ return _run_option_picker(
65
+ subtitle="update",
66
+ title=f"Alysis Code {latest_version} is available — you have {current_version}",
67
+ rows=rows,
68
+ default_index=1,
69
+ input=input,
70
+ output=output,
71
+ )
72
+
73
+
74
+ __all__ = [
75
+ "UPDATE_ACTION_LATER",
76
+ "UPDATE_ACTION_SKIP",
77
+ "UPDATE_ACTION_UPDATE",
78
+ "select_update_action",
79
+ ]