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,1248 @@
1
+ # ruff: noqa: F401,F403,F405,I001
2
+ # Legacy split module: dependencies are synced by cli_surface.py.
3
+ from __future__ import annotations
4
+
5
+ from .cli_common import *
6
+ from .update import _cached_update_status_summary
7
+ from ...config import resolve_web_search_policy
8
+ from ...personas import normalize_persona, persona_modes_enabled
9
+ from ...llm_error_display import classify_llm_error_display, friendly_llm_error_message
10
+ from ...llm.protocols import OPENAI_COMPAT_PROTOCOL, get_provider_protocol_capabilities
11
+ from ...profiles import get_active_profile, resolve_effective_base_url
12
+ from ...provider_telemetry import last_provider_call_summary, last_web_search_summary
13
+ from ...step_budget import (
14
+ DEFAULT_CHAT_MAX_STEPS,
15
+ DEFAULT_SUBAGENT_MAX_STEPS,
16
+ DEFAULT_TASK_MAX_STEPS,
17
+ normalize_step_budget_policy,
18
+ )
19
+
20
+
21
+ _CHAT_MODE_STATUS_LABELS = {
22
+ "review": "safe (review)",
23
+ "auto": "fast (auto)",
24
+ "readonly": "read (readonly)",
25
+ "fullaccess": "full (fullaccess)",
26
+ }
27
+
28
+
29
+ def _chat_mode_status_label(mode: Any) -> str:
30
+ normalized = str(mode or "").strip().lower()
31
+ return _CHAT_MODE_STATUS_LABELS.get(normalized, normalized or "unknown")
32
+
33
+
34
+ def _session_execution_policy(session: Any) -> str:
35
+ return normalize_step_budget_policy(
36
+ getattr(
37
+ session,
38
+ "step_budget_policy",
39
+ getattr(getattr(session, "cfg", None), "step_budget_policy", "autonomous"),
40
+ )
41
+ )
42
+
43
+
44
+ def _session_step_limit_label(session: Any, *, scope: str) -> str:
45
+ policy = _session_execution_policy(session)
46
+ if scope == "chat":
47
+ explicit = getattr(session, "chat_turn_fixed_override", None)
48
+ if isinstance(explicit, int) and explicit > 0:
49
+ return str(explicit)
50
+ if policy == "autonomous":
51
+ return "unlimited"
52
+ field = {
53
+ "chat": "max_steps",
54
+ "task": "task_max_steps",
55
+ "subagent": "subagent_max_steps",
56
+ }[scope]
57
+ default = {
58
+ "chat": DEFAULT_CHAT_MAX_STEPS,
59
+ "task": DEFAULT_TASK_MAX_STEPS,
60
+ "subagent": DEFAULT_SUBAGENT_MAX_STEPS,
61
+ }[scope]
62
+ return str(getattr(session, field, getattr(getattr(session, "cfg", None), field, default)))
63
+
64
+
65
+ def _active_step_limit_label(session: Any) -> str:
66
+ runtime = getattr(session, "step_budget_runtime", None)
67
+ active = getattr(runtime, "active_turn_budget", None)
68
+ if isinstance(active, int) and active > 0:
69
+ return str(active)
70
+ resolution = getattr(runtime, "last_resolution", None)
71
+ if resolution is not None and bool(getattr(resolution, "unlimited", False)):
72
+ return "unlimited"
73
+ resolved = getattr(resolution, "resolved_max_steps", None)
74
+ if isinstance(resolved, int) and resolved > 0:
75
+ return str(resolved)
76
+ return _session_step_limit_label(session, scope="chat")
77
+
78
+
79
+ def _print_chat_status(
80
+ *,
81
+ console: Console,
82
+ session: Any,
83
+ pending_images: list[str],
84
+ ) -> None:
85
+ model = getattr(getattr(session, "client", None), "model", "?")
86
+ temperature = getattr(getattr(session, "client", None), "temperature", "?")
87
+ stream = bool(getattr(session, "stream", False))
88
+ mode = getattr(session, "mode", "?")
89
+ root = Path(getattr(session, "root", Path(".")))
90
+ branch = _current_branch_label(root)
91
+ cfg_obj = getattr(session, "cfg", None)
92
+ resolved_cfg = cfg_obj if isinstance(cfg_obj, AppConfig) else None
93
+ session_api_key = (
94
+ str(getattr(getattr(session, "client", None), "api_key", "") or "").strip() or None
95
+ )
96
+ web_search_status = resolve_web_search_runtime_status(
97
+ cfg=resolved_cfg,
98
+ api_key=session_api_key,
99
+ )
100
+ active_protocol, streaming_supported = _active_protocol_status(
101
+ cfg=resolved_cfg,
102
+ client=getattr(session, "client", None),
103
+ )
104
+ table = _Table(title="Chat Status")
105
+ table.add_column("setting")
106
+ table.add_column("value", overflow="fold")
107
+ table.add_row("mode", _chat_mode_status_label(mode))
108
+ if persona_modes_enabled(getattr(session, "cfg", None)):
109
+ table.add_row(
110
+ "persona",
111
+ normalize_persona(
112
+ getattr(session, "persona", "code"),
113
+ getattr(session, "persona_registry", None),
114
+ ),
115
+ )
116
+ table.add_row("trace", _chat_trace_level(session))
117
+ table.add_row("model", str(model))
118
+ table.add_row("protocol", active_protocol)
119
+ table.add_row(
120
+ "api_key_source",
121
+ _api_key_source_label(str(getattr(session, "api_key_source", "missing") or "missing")),
122
+ )
123
+ table.add_row("temperature", str(temperature))
124
+ table.add_row("stream", "on" if stream else "off")
125
+ table.add_row("streaming_supported", streaming_supported)
126
+ table.add_row(
127
+ "subagents",
128
+ "on" if bool(getattr(session, "subagents_enabled", False)) else "off",
129
+ )
130
+ table.add_row(
131
+ "execution_policy",
132
+ _session_execution_policy(session),
133
+ )
134
+ table.add_row("chat_step_limit", _session_step_limit_label(session, scope="chat"))
135
+ table.add_row("task_step_limit", _session_step_limit_label(session, scope="task"))
136
+ table.add_row("subagent_step_limit", _session_step_limit_label(session, scope="subagent"))
137
+ table.add_row("active_step_limit", _active_step_limit_label(session))
138
+ table.add_row("session", str(getattr(getattr(session, "store", None), "session_id", "-")))
139
+ active_workdir = Path(resolve_session_active_workdir_path(session))
140
+ focus_dir = Path(getattr(session, "focus_dir", root) or root)
141
+ focus_relpath = str(getattr(session, "focus_relpath", ".") or ".")
142
+ active_workdir_relpath = str(resolve_session_active_workdir_relpath(session) or ".")
143
+ table.add_row("workspace_root", os.fspath(root))
144
+ table.add_row("focus_dir", os.fspath(focus_dir))
145
+ table.add_row("focus_relpath", focus_relpath)
146
+ table.add_row("active_workdir", os.fspath(active_workdir))
147
+ table.add_row("active_workdir_relpath", active_workdir_relpath)
148
+ table.add_row("branch", branch)
149
+ table.add_row("dirty", "yes" if _is_git_dirty(root) else "no")
150
+ table.add_row("update", _cached_update_status_summary(resolved_cfg))
151
+ table.add_row("task", "-")
152
+ table.add_row("queued_images", str(len(pending_images)))
153
+ web_search_policy = resolve_web_search_policy(resolved_cfg)
154
+ web_search_ready = web_search_status.registration_ready and web_search_policy != "off"
155
+ web_search_label = (
156
+ "policy-disabled" if web_search_policy == "off" else web_search_status.availability_label
157
+ )
158
+ table.add_row("web_search", web_search_label)
159
+ table.add_row("web_search_policy", web_search_policy)
160
+ table.add_row("web_search_mode", web_search_status.mode)
161
+ table.add_row("web_search_provider", web_search_status.provider or "(none)")
162
+ table.add_row(
163
+ "web_search_registration",
164
+ "yes" if web_search_ready else "no",
165
+ )
166
+ if web_search_status.provider not in {None, "tavily"}:
167
+ table.add_row("web_search_base_url", web_search_status.base_url or "(missing)")
168
+ table.add_row("web_search_model", web_search_status.model or "(missing)")
169
+ table.add_row(
170
+ "web_search_api_key",
171
+ "available" if web_search_status.api_key_available else "missing",
172
+ )
173
+ table.add_row("web_search_note", web_search_status.summary)
174
+ table.add_row(
175
+ "web_search_setup",
176
+ (
177
+ "Set `alysis config set web_search_policy auto` to expose search to the model."
178
+ if web_search_policy == "off"
179
+ else web_search_status.setup_hint
180
+ ),
181
+ )
182
+ last_call = last_provider_call_summary()
183
+ if last_call:
184
+ table.add_row(
185
+ "last_provider_call",
186
+ (
187
+ f"{last_call.get('provider_key') or '?'} / "
188
+ f"{last_call.get('protocol') or '?'} / "
189
+ f"{last_call.get('status_category') or '?'} / "
190
+ f"{last_call.get('latency_ms') or 0}ms"
191
+ ),
192
+ )
193
+ stream_summary = last_call.get("streaming") if isinstance(last_call, dict) else None
194
+ if isinstance(stream_summary, dict):
195
+ table.add_row(
196
+ "last_stream",
197
+ (
198
+ f"events={stream_summary.get('event_count') or 0}, "
199
+ f"deltas={stream_summary.get('text_delta_count') or 0}, "
200
+ f"first_token_ms={stream_summary.get('first_token_latency_ms')}"
201
+ ),
202
+ )
203
+ last_search = last_web_search_summary()
204
+ if last_search:
205
+ table.add_row(
206
+ "last_web_search",
207
+ (
208
+ f"{last_search.get('web_search_mode') or '?'} / "
209
+ f"{last_search.get('web_search_adapter') or '?'} / "
210
+ f"sources={last_search.get('source_count') or 0}"
211
+ ),
212
+ )
213
+ registry = getattr(session, "model_registry", None)
214
+ resolved_model_name = str(model).strip()
215
+ if registry is not None and resolved_model_name:
216
+ try:
217
+ meta = registry.get(resolved_model_name)
218
+ except Exception as e: # noqa: BLE001
219
+ table.add_row("model_metadata_source", "error")
220
+ table.add_row("model_metadata_error", str(e))
221
+ else:
222
+ table.add_row("model_metadata_source", str(getattr(meta, "source", "unknown")))
223
+ table.add_row(
224
+ "model_metadata_error",
225
+ str(getattr(registry, "last_error", None) or "-"),
226
+ )
227
+ table.add_row(
228
+ "model_metadata_warning",
229
+ "; ".join(getattr(meta, "warnings", ())[:3]) or "-",
230
+ )
231
+ field_sources = getattr(meta, "field_sources", {}) or {}
232
+ table.add_row(
233
+ "context_window_source",
234
+ str(field_sources.get("context_window_tokens", "unknown")),
235
+ )
236
+ table.add_row(
237
+ "max_output_source",
238
+ str(field_sources.get("max_output_tokens", "unknown")),
239
+ )
240
+ for key, value in _bundled_catalog_provenance_rows(meta=meta, registry=registry):
241
+ table.add_row(key, value)
242
+ console.print(table)
243
+
244
+
245
+ def _active_protocol_status(*, cfg: AppConfig | None, client: Any) -> tuple[str, str]:
246
+ protocol = str(getattr(client, "protocol", "") or "").strip()
247
+ provider_key = str(getattr(client, "provider_key", "") or "").strip()
248
+ if cfg is not None:
249
+ try:
250
+ profile = get_active_profile(cfg)
251
+ protocol = protocol or str(profile.protocol or OPENAI_COMPAT_PROTOCOL).strip()
252
+ provider_key = provider_key or str(profile.name or "").strip()
253
+ base_url = resolve_effective_base_url(cfg=cfg, profile=profile)
254
+ except Exception: # noqa: BLE001
255
+ base_url = str(getattr(client, "base_url", "") or "")
256
+ else:
257
+ base_url = str(getattr(client, "base_url", "") or "")
258
+ protocol = protocol or OPENAI_COMPAT_PROTOCOL
259
+ capabilities = get_provider_protocol_capabilities(
260
+ provider_key=provider_key,
261
+ protocol=protocol,
262
+ )
263
+ if capabilities is not None:
264
+ supported = "yes" if capabilities.supports_streaming else "no"
265
+ else:
266
+ supported = "yes" if protocol == OPENAI_COMPAT_PROTOCOL else "unknown"
267
+ if not provider_key and base_url:
268
+ provider_key = base_url
269
+ return protocol, supported
270
+
271
+
272
+ def _chat_status_panel_spec(*, session: Any, pending_images: list[str]) -> dict[str, Any]:
273
+ """Build the structured spec for the TUI ``/status`` popup.
274
+
275
+ Mirrors :func:`_print_chat_status` (same getters) but returns grouped
276
+ key/value sections with a health ``tone`` per value instead of a flat Rich
277
+ table, so the full-screen TUI can render it as a centered panel. ``tone`` is
278
+ one of "accent" (on/healthy, green), "plain", "warn", "err", "dim". Green is
279
+ the only accent; degraded states stay "plain", real failures use "err".
280
+ """
281
+
282
+ def tone(flag: bool, true_tone: str = "accent", false_tone: str = "plain") -> str:
283
+ return true_tone if flag else false_tone
284
+
285
+ model = getattr(getattr(session, "client", None), "model", "?")
286
+ temperature = getattr(getattr(session, "client", None), "temperature", "?")
287
+ stream = bool(getattr(session, "stream", False))
288
+ mode = str(getattr(session, "mode", "?"))
289
+ root = Path(getattr(session, "root", Path(".")))
290
+ branch = _current_branch_label(root)
291
+ dirty = _is_git_dirty(root)
292
+ subagents = bool(getattr(session, "subagents_enabled", False))
293
+ cfg_obj = getattr(session, "cfg", None)
294
+ resolved_cfg = cfg_obj if isinstance(cfg_obj, AppConfig) else None
295
+ session_api_key = (
296
+ str(getattr(getattr(session, "client", None), "api_key", "") or "").strip() or None
297
+ )
298
+ web = resolve_web_search_runtime_status(cfg=resolved_cfg, api_key=session_api_key)
299
+
300
+ def _attr(name: str, default: Any) -> Any:
301
+ return getattr(session, name, getattr(getattr(session, "cfg", None), name, default))
302
+
303
+ session_rows: list[tuple[str, str, str]] = [
304
+ ("mode", _chat_mode_status_label(mode), "accent"),
305
+ *(
306
+ [
307
+ (
308
+ "persona",
309
+ normalize_persona(
310
+ getattr(session, "persona", "code"),
311
+ getattr(session, "persona_registry", None),
312
+ ),
313
+ "accent",
314
+ )
315
+ ]
316
+ if persona_modes_enabled(getattr(session, "cfg", None))
317
+ else []
318
+ ),
319
+ ("model", str(model), "accent"),
320
+ ("trace", _chat_trace_level(session), "plain"),
321
+ ("session", str(getattr(getattr(session, "store", None), "session_id", "-")), "dim"),
322
+ ("branch", branch, "plain"),
323
+ ("dirty", "yes" if dirty else "no", tone(not dirty)),
324
+ ("update", _cached_update_status_summary(resolved_cfg), "plain"),
325
+ ]
326
+
327
+ model_rows: list[tuple[str, str, str]] = [
328
+ (
329
+ "api_key_source",
330
+ _api_key_source_label(str(getattr(session, "api_key_source", "missing") or "missing")),
331
+ "plain",
332
+ ),
333
+ ("temperature", str(temperature), "plain"),
334
+ ("stream", "on" if stream else "off", tone(stream)),
335
+ ("subagents", "on" if subagents else "off", tone(subagents)),
336
+ ("execution_policy", _session_execution_policy(session), "plain"),
337
+ ("chat_step_limit", _session_step_limit_label(session, scope="chat"), "plain"),
338
+ ("task_step_limit", _session_step_limit_label(session, scope="task"), "plain"),
339
+ (
340
+ "subagent_step_limit",
341
+ _session_step_limit_label(session, scope="subagent"),
342
+ "plain",
343
+ ),
344
+ ]
345
+ model_rows.append(
346
+ (
347
+ "active_step_limit",
348
+ _active_step_limit_label(session),
349
+ "plain",
350
+ )
351
+ )
352
+
353
+ active_workdir = Path(resolve_session_active_workdir_path(session))
354
+ focus_dir = Path(getattr(session, "focus_dir", root) or root)
355
+ workspace_rows: list[tuple[str, str, str]] = [
356
+ ("workspace_root", os.fspath(root), "plain"),
357
+ ("focus_dir", os.fspath(focus_dir), "plain"),
358
+ ("focus_relpath", str(getattr(session, "focus_relpath", ".") or "."), "plain"),
359
+ ("active_workdir", os.fspath(active_workdir), "accent"),
360
+ (
361
+ "active_workdir_relpath",
362
+ str(resolve_session_active_workdir_relpath(session) or "."),
363
+ "plain",
364
+ ),
365
+ ("queued_images", str(len(pending_images)), "plain"),
366
+ ]
367
+
368
+ web_policy = resolve_web_search_policy(resolved_cfg)
369
+ web_ready = web.registration_ready and web_policy != "off"
370
+ web_label = "policy-disabled" if web_policy == "off" else web.availability_label
371
+ web_unavailable = not web_ready
372
+ web_rows: list[tuple[str, str, str]] = [
373
+ (
374
+ "status",
375
+ web_label,
376
+ "err" if web_unavailable else tone(web_ready),
377
+ ),
378
+ ("policy", web_policy, "plain"),
379
+ ("mode", web.mode, "plain"),
380
+ ("provider", web.provider or "(none)", "plain"),
381
+ ("registration", "yes" if web_ready else "no", tone(web_ready)),
382
+ ]
383
+ if web.provider not in {None, "tavily"}:
384
+ web_rows.append(("base_url", web.base_url or "(missing)", "plain"))
385
+ web_rows.append(("model", web.model or "(missing)", "plain"))
386
+ web_rows.append(
387
+ (
388
+ "api_key",
389
+ "available" if web.api_key_available else "missing",
390
+ tone(web.api_key_available),
391
+ )
392
+ )
393
+ web_rows.append(("note", web.summary, "plain"))
394
+ web_rows.append(
395
+ (
396
+ "setup",
397
+ (
398
+ "Set `alysis config set web_search_policy auto` to expose search to the model."
399
+ if web_policy == "off"
400
+ else web.setup_hint
401
+ ),
402
+ "plain",
403
+ )
404
+ )
405
+
406
+ sections: list[tuple[str, list[tuple[str, str, str]]]] = [
407
+ ("Session", session_rows),
408
+ ("Model", model_rows),
409
+ ("Workspace", workspace_rows),
410
+ ("Web search", web_rows),
411
+ ]
412
+
413
+ registry = getattr(session, "model_registry", None)
414
+ resolved_model_name = str(model).strip()
415
+ if registry is not None and resolved_model_name:
416
+ meta_rows: list[tuple[str, str, str]] = []
417
+ try:
418
+ meta = registry.get(resolved_model_name)
419
+ except Exception as e: # noqa: BLE001
420
+ meta_rows.append(("source", "error", "err"))
421
+ meta_rows.append(("error", str(e), "err"))
422
+ else:
423
+ meta_rows.append(("source", str(getattr(meta, "source", "unknown")), "plain"))
424
+ last_error = str(getattr(registry, "last_error", None) or "-")
425
+ meta_rows.append(("error", last_error, "err" if last_error != "-" else "plain"))
426
+ warning = "; ".join(getattr(meta, "warnings", ())[:3]) or "-"
427
+ meta_rows.append(("warning", warning, "warn" if warning != "-" else "plain"))
428
+ field_sources = getattr(meta, "field_sources", {}) or {}
429
+ meta_rows.append(
430
+ (
431
+ "context_window_source",
432
+ str(field_sources.get("context_window_tokens", "unknown")),
433
+ "plain",
434
+ )
435
+ )
436
+ meta_rows.append(
437
+ (
438
+ "max_output_source",
439
+ str(field_sources.get("max_output_tokens", "unknown")),
440
+ "plain",
441
+ )
442
+ )
443
+ for key, value in _bundled_catalog_provenance_rows(meta=meta, registry=registry):
444
+ meta_rows.append((key, value, "dim"))
445
+ sections.append(("Model metadata", meta_rows))
446
+
447
+ return {"title": "Session Status", "sections": sections}
448
+
449
+
450
+ def _print_chat_pwd(*, console: Console, session: Any) -> None:
451
+ root = Path(getattr(session, "root", Path("."))).resolve()
452
+ focus_dir = Path(getattr(session, "focus_dir", root) or root)
453
+ focus_relpath = str(getattr(session, "focus_relpath", ".") or ".")
454
+ active_workdir = Path(resolve_session_active_workdir_path(session))
455
+ active_workdir_relpath = str(resolve_session_active_workdir_relpath(session) or ".")
456
+ console.print(f"active_workdir: {active_workdir}")
457
+ console.print(f"active_workdir_relpath: {active_workdir_relpath}")
458
+ console.print(f"focus_dir: {focus_dir}")
459
+ console.print(f"focus_relpath: {focus_relpath}")
460
+ console.print(f"workspace_root: {root}")
461
+
462
+
463
+ def _model_metadata_uses_bundled_catalog(*, meta: Any, registry: Any) -> bool:
464
+ if str(getattr(meta, "source", "")).strip() == BUNDLED_MODEL_CATALOG_SOURCE:
465
+ return True
466
+ field_sources = getattr(meta, "field_sources", {}) or {}
467
+ if any(
468
+ str(source).strip() == BUNDLED_MODEL_CATALOG_SOURCE for source in field_sources.values()
469
+ ):
470
+ return True
471
+ last_error = str(getattr(registry, "last_error", "") or "").strip().casefold()
472
+ return "bundled model catalog" in last_error
473
+
474
+
475
+ def _bundled_catalog_provenance_rows(*, meta: Any, registry: Any) -> list[tuple[str, str]]:
476
+ if not _model_metadata_uses_bundled_catalog(meta=meta, registry=registry):
477
+ return []
478
+ provenance = _patchable(
479
+ "get_bundled_model_catalog_provenance",
480
+ get_bundled_model_catalog_provenance,
481
+ )()
482
+ if provenance.error is not None:
483
+ return [("bundled_catalog_provenance", provenance.error)]
484
+ rows: list[tuple[str, str]] = []
485
+ if provenance.upstream_commit_sha is not None:
486
+ rows.append(("bundled_catalog_commit", provenance.upstream_commit_sha))
487
+ if provenance.fetched_at_utc is not None:
488
+ rows.append(("bundled_catalog_fetched_at", provenance.fetched_at_utc))
489
+ return rows
490
+
491
+
492
+ def _print_chat_usage(*, console: Console, session: Any) -> None:
493
+ summary = getattr(session, "usage_summary", None)
494
+ if summary is None:
495
+ console.print("Usage tracking unavailable for this session.")
496
+ return
497
+ rows = summary.by_model_rows()
498
+ if not rows:
499
+ console.print("No usage events yet in this session.")
500
+ return
501
+ table = _Table(title="Usage")
502
+ table.add_column("model")
503
+ table.add_column("input total", justify="right")
504
+ table.add_column("uncached", justify="right")
505
+ table.add_column("cache read", justify="right")
506
+ table.add_column("cache write", justify="right")
507
+ table.add_column("output", justify="right")
508
+ table.add_column("total", justify="right")
509
+ table.add_column("cost", justify="right")
510
+ table.add_column("source", justify="right")
511
+ for row in rows:
512
+ cost_display = _format_usage_billing_for_display(row)
513
+ table.add_row(
514
+ str(row.get("model") or "-"),
515
+ _format_exact_token_count(row.get("prompt_tokens")),
516
+ _format_reported_token_total(
517
+ row,
518
+ token_key="input_tokens_uncached",
519
+ reported_calls_key="input_tokens_uncached_reported_calls",
520
+ derived_calls_key="input_tokens_uncached_derived_calls",
521
+ ),
522
+ _format_reported_token_total(
523
+ row,
524
+ token_key="cache_read_input_tokens",
525
+ reported_calls_key="cache_read_reported_calls",
526
+ ),
527
+ _format_reported_token_total(
528
+ row,
529
+ token_key="cache_creation_input_tokens",
530
+ reported_calls_key="cache_creation_reported_calls",
531
+ ),
532
+ _format_exact_token_count(row.get("completion_tokens")),
533
+ _format_exact_token_count(row.get("total_tokens")),
534
+ cost_display,
535
+ _format_usage_source_for_display(row),
536
+ )
537
+ totals = summary.totals()
538
+ total_cost = _format_usage_billing_for_display(totals)
539
+ unknown_total = int(totals.get("unknown_cost_calls") or 0)
540
+ table.add_row(
541
+ "TOTAL",
542
+ _format_exact_token_count(totals.get("prompt_tokens")),
543
+ _format_reported_token_total(
544
+ totals,
545
+ token_key="input_tokens_uncached",
546
+ reported_calls_key="input_tokens_uncached_reported_calls",
547
+ derived_calls_key="input_tokens_uncached_derived_calls",
548
+ ),
549
+ _format_reported_token_total(
550
+ totals,
551
+ token_key="cache_read_input_tokens",
552
+ reported_calls_key="cache_read_reported_calls",
553
+ ),
554
+ _format_reported_token_total(
555
+ totals,
556
+ token_key="cache_creation_input_tokens",
557
+ reported_calls_key="cache_creation_reported_calls",
558
+ ),
559
+ _format_exact_token_count(totals.get("completion_tokens")),
560
+ _format_exact_token_count(totals.get("total_tokens")),
561
+ total_cost,
562
+ _format_usage_source_for_display(totals),
563
+ )
564
+ console.print(table)
565
+ cache_efficiency = totals.get("cache_efficiency")
566
+ if isinstance(cache_efficiency, dict) and cache_efficiency.get("hit_ratio") is not None:
567
+ largest = cache_efficiency.get("largest_uncached_call")
568
+ largest_text = ""
569
+ if isinstance(largest, dict):
570
+ largest_text = (
571
+ "; largest uncached "
572
+ f"{_format_exact_token_count(largest.get('uncached_prompt_tokens'))} "
573
+ f"at call {int(largest.get('call_position') or 0)}"
574
+ )
575
+ console.print(
576
+ "[dim]Cache efficiency: "
577
+ f"{float(cache_efficiency['hit_ratio']) * 100:.2f}% hit "
578
+ f"({_format_exact_token_count(cache_efficiency.get('cached_prompt_tokens'))} cached, "
579
+ f"{_format_exact_token_count(cache_efficiency.get('uncached_prompt_tokens'))} uncached)"
580
+ f"{largest_text}.[/dim]"
581
+ )
582
+ console.print(
583
+ "[dim]Processed tokens are cumulative input + output across calls; they are not the "
584
+ "current context-window size. Cache read/write are input subdivisions and appear only "
585
+ "when the provider reports them. A ~ dollar amount is a catalog estimate, not an invoice.[/dim]"
586
+ )
587
+ if unknown_total > 0:
588
+ console.print(
589
+ "[yellow]Total cost is partial:[/yellow] "
590
+ f"{unknown_total} call(s) have unknown cost because pricing metadata is missing."
591
+ )
592
+ cache_cost_unknown = int(totals.get("cache_cost_pricing_missing_calls") or 0)
593
+ if cache_cost_unknown > 0:
594
+ console.print(
595
+ "[yellow]Cache pricing omitted:[/yellow] "
596
+ f"{cache_cost_unknown} cached call(s) need provider-specific read/write prices."
597
+ )
598
+ if int(totals.get("cache_read_input_tokens") or 0) or int(
599
+ totals.get("cache_creation_input_tokens") or 0
600
+ ):
601
+ console.print(
602
+ "[dim]Cache read/write tokens can reduce provider cost or latency, but they still "
603
+ "occupy the current request context window.[/dim]"
604
+ )
605
+ corrected_total = int(totals.get("corrected_usage_calls") or 0)
606
+ if corrected_total > 0:
607
+ console.print(
608
+ "[yellow]Usage corrected:[/yellow] "
609
+ f"{corrected_total} provider usage record(s) contained inconsistent or character-like "
610
+ "token fields and were corrected before display."
611
+ )
612
+ request_estimate = totals.get("request_token_estimate")
613
+ if isinstance(request_estimate, dict):
614
+ schema_over_calls = int(request_estimate.get("tool_schema_budget_exceeded_calls") or 0)
615
+ if schema_over_calls > 0:
616
+ schema_overage = int(request_estimate.get("tool_schema_budget_overage_tokens") or 0)
617
+ largest_tool = int(request_estimate.get("tool_schema_largest_tool_tokens") or 0)
618
+ console.print(
619
+ "[yellow]Tool schema shadow budget:[/yellow] "
620
+ f"{schema_over_calls} call(s) exceeded the schema budget by "
621
+ f"{_format_exact_token_count(schema_overage)} total tokens "
622
+ f"(largest single tool {_format_exact_token_count(largest_tool)})."
623
+ )
624
+
625
+
626
+ def _print_chat_context(*, console: Console, session: Any) -> None:
627
+ from ..chat import _print_chat_context_impl
628
+
629
+ return _print_chat_context_impl(_cli_module_for_legacy_impl(), console=console, session=session)
630
+
631
+
632
+ def _chat_used_models(session: Any) -> list[str]:
633
+ models: list[str] = []
634
+ seen: set[str] = set()
635
+
636
+ def _add(raw_value: Any) -> None:
637
+ value = str(raw_value or "").strip()
638
+ if not value:
639
+ return
640
+ key = value.casefold()
641
+ if key in seen:
642
+ return
643
+ seen.add(key)
644
+ models.append(value)
645
+
646
+ _add(getattr(getattr(session, "client", None), "model", ""))
647
+ _add(getattr(getattr(session, "cfg", None), "model", ""))
648
+
649
+ summary = getattr(session, "usage_summary", None)
650
+ rows_fn = getattr(summary, "by_model_rows", None)
651
+ if callable(rows_fn):
652
+ try:
653
+ rows = rows_fn()
654
+ except Exception: # noqa: BLE001
655
+ rows = []
656
+ if isinstance(rows, list):
657
+ for row in rows:
658
+ if not isinstance(row, dict):
659
+ continue
660
+ _add(row.get("model"))
661
+
662
+ return models
663
+
664
+
665
+ def _chat_config_usage_lines() -> list[str]:
666
+ return [
667
+ "/config",
668
+ (
669
+ "/config set <model|index> <context_window_tokens> <max_output_tokens> "
670
+ "[supports_vision] [input_cost_per_token] [output_cost_per_token]"
671
+ ),
672
+ "/config clear <model|index>",
673
+ "Example: /config set 1 128000 4096 false",
674
+ ]
675
+
676
+
677
+ def _print_chat_config_usage(*, console: Console) -> None:
678
+ console.print("[yellow]Usage:[/yellow]")
679
+ for line in _chat_config_usage_lines():
680
+ console.print(f" {line}")
681
+
682
+
683
+ def _resolve_chat_model_ref(*, session: Any, raw_model_ref: str) -> tuple[str | None, str | None]:
684
+ model_ref = raw_model_ref.strip()
685
+ if not model_ref:
686
+ return None, "Missing model reference."
687
+
688
+ if model_ref.isdigit():
689
+ models = _chat_used_models(session)
690
+ if not models:
691
+ return None, "No tracked models yet. Set a model first with /model <name>."
692
+ index = int(model_ref)
693
+ if index < 1 or index > len(models):
694
+ return None, f"Invalid model index: {index}. Run /config to list tracked models."
695
+ return models[index - 1], None
696
+
697
+ return model_ref, None
698
+
699
+
700
+ def _merge_model_metadata_override(
701
+ *,
702
+ cfg: AppConfig,
703
+ model_name: str,
704
+ fields: dict[str, Any],
705
+ ) -> dict[str, Any]:
706
+ raw_overrides = cfg.extra_fields.get("model_metadata_overrides")
707
+ overrides = dict(raw_overrides) if isinstance(raw_overrides, dict) else {}
708
+ raw_models = overrides.get("models")
709
+ models = dict(raw_models) if isinstance(raw_models, dict) else {}
710
+
711
+ raw_existing = models.get(model_name)
712
+ merged = dict(raw_existing) if isinstance(raw_existing, dict) else {}
713
+ merged.update(fields)
714
+ models[model_name] = merged
715
+ overrides["models"] = models
716
+ cfg.extra_fields["model_metadata_overrides"] = overrides
717
+ return merged
718
+
719
+
720
+ def _clear_model_metadata_override(*, cfg: AppConfig, model_name: str) -> bool:
721
+ raw_overrides = cfg.extra_fields.get("model_metadata_overrides")
722
+ if not isinstance(raw_overrides, dict):
723
+ return False
724
+ raw_models = raw_overrides.get("models")
725
+ if not isinstance(raw_models, dict):
726
+ return False
727
+ if model_name not in raw_models:
728
+ return False
729
+
730
+ models = dict(raw_models)
731
+ models.pop(model_name, None)
732
+
733
+ overrides = dict(raw_overrides)
734
+ if models:
735
+ overrides["models"] = models
736
+ else:
737
+ overrides.pop("models", None)
738
+
739
+ if overrides:
740
+ cfg.extra_fields["model_metadata_overrides"] = overrides
741
+ else:
742
+ cfg.extra_fields.pop("model_metadata_overrides", None)
743
+ return True
744
+
745
+
746
+ def _save_chat_model_metadata_override(
747
+ *,
748
+ session: Any,
749
+ model_name: str,
750
+ fields: dict[str, Any],
751
+ ) -> tuple[Path, dict[str, Any]]:
752
+ cfg = load_config()
753
+ merged = _merge_model_metadata_override(cfg=cfg, model_name=model_name, fields=fields)
754
+ save_config(cfg)
755
+
756
+ runtime_cfg = getattr(session, "cfg", None)
757
+ if isinstance(runtime_cfg, AppConfig):
758
+ _merge_model_metadata_override(cfg=runtime_cfg, model_name=model_name, fields=fields)
759
+ return config_path(), merged
760
+
761
+
762
+ def _clear_chat_model_metadata_override(*, session: Any, model_name: str) -> tuple[Path, bool]:
763
+ cfg = load_config()
764
+ cleared = _clear_model_metadata_override(cfg=cfg, model_name=model_name)
765
+ if cleared:
766
+ save_config(cfg)
767
+
768
+ runtime_cfg = getattr(session, "cfg", None)
769
+ if isinstance(runtime_cfg, AppConfig):
770
+ _clear_model_metadata_override(cfg=runtime_cfg, model_name=model_name)
771
+ return config_path(), cleared
772
+
773
+
774
+ def _chat_config_panel(*, session: Any) -> Panel:
775
+ models = _chat_used_models(session)
776
+ registry = getattr(session, "model_registry", None)
777
+ title = f"Model Config ({config_path()})"
778
+
779
+ if _patchable("_is_narrow_terminal", _is_narrow_terminal)():
780
+ lines: list[str] = []
781
+ if models:
782
+ lines.append("Tracked models:")
783
+ for idx, model_name in enumerate(models, start=1):
784
+ summary = ""
785
+ if registry is not None:
786
+ try:
787
+ meta = registry.get(model_name)
788
+ except Exception as e: # noqa: BLE001
789
+ summary = f"error={e}"
790
+ else:
791
+ field_sources = getattr(meta, "field_sources", {}) or {}
792
+ ctx_source = field_sources.get(
793
+ "context_window_tokens",
794
+ getattr(meta, "source", "unknown"),
795
+ )
796
+ summary = (
797
+ f"ctx={meta.context_window_tokens}, out={meta.max_output_tokens}, "
798
+ f"src={ctx_source}"
799
+ )
800
+ if summary:
801
+ lines.append(f"{idx}) {model_name} - {summary}")
802
+ else:
803
+ lines.append(f"{idx}) {model_name}")
804
+ else:
805
+ lines.append("No tracked models in this session yet.")
806
+ lines.append("")
807
+ lines.extend(_chat_config_usage_lines())
808
+ return _Panel("\n".join(lines), title=title, border_style="bright_black")
809
+
810
+ table = _Table(show_header=True, box=None, expand=True, padding=(0, 1), collapse_padding=True)
811
+ table.add_column("#", style=STYLE_EMPHASIS, no_wrap=True, width=3)
812
+ table.add_column("model", style=STYLE_CONTENT, no_wrap=False, ratio=4, overflow="fold")
813
+ table.add_column("ctx", style=STYLE_CONTENT, no_wrap=True, ratio=2)
814
+ table.add_column("max_out", style=STYLE_CONTENT, no_wrap=True, ratio=2)
815
+ table.add_column("source", style=STYLE_CONTENT, no_wrap=False, ratio=2)
816
+ table.add_column("note", style=STYLE_CONTENT, no_wrap=False, ratio=3, overflow="fold")
817
+
818
+ if models:
819
+ for idx, model_name in enumerate(models, start=1):
820
+ ctx_value = "-"
821
+ max_out_value = "-"
822
+ source = "-"
823
+ note = "-"
824
+ if registry is not None:
825
+ try:
826
+ meta = registry.get(model_name)
827
+ except Exception as e: # noqa: BLE001
828
+ note = str(e)
829
+ else:
830
+ field_sources = getattr(meta, "field_sources", {}) or {}
831
+ ctx_value = str(getattr(meta, "context_window_tokens", "-"))
832
+ max_out_value = str(getattr(meta, "max_output_tokens", "-"))
833
+ source = str(
834
+ field_sources.get(
835
+ "context_window_tokens",
836
+ getattr(meta, "source", "unknown"),
837
+ )
838
+ )
839
+ warnings = list(getattr(meta, "warnings", ())[:1])
840
+ note = warnings[0] if warnings else "-"
841
+ table.add_row(str(idx), model_name, ctx_value, max_out_value, source, note)
842
+ else:
843
+ table.add_row("-", "(no tracked models yet)", "-", "-", "-", "-")
844
+
845
+ usage_text = "\n".join(_chat_config_usage_lines())
846
+ content = _table_grid(expand=True)
847
+ content.add_row(table)
848
+ content.add_row("")
849
+ content.add_row(usage_text)
850
+ return _Panel(content, title=title, border_style="bright_black")
851
+
852
+
853
+ def _chat_turn_usage_line(session: Any) -> tuple[str | None, str | None] | None:
854
+ ctx_percent = _chat_context_percent_value(session)
855
+ ctx_display = _format_chat_context_percent(ctx_percent)
856
+ ctx_segment = f"[dim]context left:[/dim] {ctx_display}" if ctx_display != "n/a" else None
857
+ warning_line = _chat_usage_warning_line(_chat_effective_budget_percent_value(session))
858
+ if not _chat_usage_hud_enabled(session):
859
+ return (ctx_segment, warning_line) if ctx_segment or warning_line else None
860
+
861
+ summary = getattr(session, "usage_summary", None)
862
+ if summary is None:
863
+ return (ctx_segment, warning_line) if ctx_segment or warning_line else None
864
+ totals = summary.totals()
865
+ total_tokens = int(totals.get("total_tokens") or 0)
866
+ if total_tokens <= 0:
867
+ return (ctx_segment, warning_line) if ctx_segment or warning_line else None
868
+
869
+ segments = [
870
+ f"{_format_compact_token_count(total_tokens)} [dim]tokens[/dim]",
871
+ f"[dim]↓[/dim] {_format_compact_token_count(totals.get('prompt_tokens'))}",
872
+ f"[dim]↑[/dim] {_format_compact_token_count(totals.get('completion_tokens'))}",
873
+ ]
874
+ cache_read_tokens = int(totals.get("cache_read_input_tokens") or 0)
875
+ cache_write_tokens = int(totals.get("cache_creation_input_tokens") or 0)
876
+ if cache_read_tokens or cache_write_tokens:
877
+ segments.append(
878
+ f"[dim]cache[/dim] r:{_format_compact_token_count(cache_read_tokens)} "
879
+ f"w:{_format_compact_token_count(cache_write_tokens)}"
880
+ )
881
+ if ctx_segment is not None:
882
+ segments.append(ctx_segment)
883
+ return (" ".join(segments), warning_line)
884
+
885
+
886
+ def _chat_bottom_toolbar(
887
+ *,
888
+ session: Any,
889
+ pending_images: list[str],
890
+ forge_state: _ForgeChatState | None = None,
891
+ plan_mode_enabled: bool = False,
892
+ ) -> str:
893
+ model = str(getattr(getattr(session, "client", None), "model", "?"))
894
+ mode = str(getattr(session, "mode", "?"))
895
+ stream_enabled = bool(getattr(session, "stream", False))
896
+ stream = "on" if stream_enabled else "off"
897
+ temperature_raw = getattr(getattr(session, "client", None), "temperature", "?")
898
+ temperature = str(temperature_raw)
899
+ trace = _patchable("_chat_trace_level", _chat_trace_level)(session)
900
+ ctx_hud = _chat_context_hud_value(session)
901
+ subagents = "on" if bool(getattr(session, "subagents_enabled", False)) else "off"
902
+ cfg = getattr(session, "cfg", None)
903
+ toolbar_items_raw = getattr(cfg, "toolbar_items", None)
904
+ if toolbar_items_raw is None:
905
+ visible_items: set[str] = set(_DEFAULT_TOOLBAR_ITEMS)
906
+ else:
907
+ visible_items = {
908
+ str(item).strip().lower() for item in toolbar_items_raw if str(item).strip()
909
+ }
910
+
911
+ tokens_part: str | None = None
912
+ cost_part: str | None = None
913
+ if _chat_usage_hud_enabled(session):
914
+ summary = getattr(session, "usage_summary", None)
915
+ if summary is not None:
916
+ totals = summary.totals()
917
+ total_tokens = int(totals.get("total_tokens") or 0)
918
+ known_cost = _known_cost_value(totals)
919
+ unknown_calls = int(totals.get("unknown_cost_calls") or 0)
920
+ cost_hud = _format_cost_with_unknown(
921
+ known_cost=known_cost,
922
+ unknown_calls=unknown_calls,
923
+ style="hud",
924
+ )
925
+ tokens_part = f"tokens={total_tokens}"
926
+ cost_part = f"cost={cost_hud}"
927
+
928
+ default_temperature_raw = getattr(session, "_toolbar_default_temperature", None)
929
+ if default_temperature_raw is None:
930
+ default_temperature_raw = getattr(getattr(session, "cfg", None), "chat_temperature", None)
931
+ try:
932
+ current_temperature = float(temperature_raw)
933
+ except (TypeError, ValueError):
934
+ current_temperature = None
935
+ try:
936
+ default_temperature = float(default_temperature_raw)
937
+ except (TypeError, ValueError):
938
+ default_temperature = None
939
+ show_temperature = False
940
+ if current_temperature is None:
941
+ show_temperature = temperature.strip() not in {"", "?"}
942
+ elif default_temperature is None:
943
+ show_temperature = True
944
+ else:
945
+ show_temperature = abs(current_temperature - default_temperature) > 1e-9
946
+
947
+ toolbar_parts: list[str] = []
948
+ if "mode" in visible_items:
949
+ toolbar_parts.append(mode)
950
+ if "model" in visible_items:
951
+ toolbar_parts.append(model)
952
+ if "stream" in visible_items and stream == "off":
953
+ toolbar_parts.append("no-stream")
954
+ if "trace" in visible_items and trace != "compact":
955
+ toolbar_parts.append(f"trace {trace}")
956
+ if "images" in visible_items and pending_images:
957
+ count = len(pending_images)
958
+ toolbar_parts.append(f"{count} image" + ("" if count == 1 else "s"))
959
+ if "temp" in visible_items and show_temperature:
960
+ toolbar_parts.append(f"temp {temperature}")
961
+ if "ctx" in visible_items:
962
+ toolbar_parts.append(f"context left: {ctx_hud}")
963
+ if "subagents" in visible_items:
964
+ toolbar_parts.append(f"subagents {subagents}")
965
+ if "tokens" in visible_items and tokens_part:
966
+ token_value = tokens_part.split("=", 1)[-1]
967
+ toolbar_parts.append(f"{token_value} tok")
968
+ if "cost" in visible_items and cost_part:
969
+ cost_value = cost_part.split("=", 1)[-1]
970
+ toolbar_parts.append(cost_value)
971
+
972
+ if (
973
+ forge_state is not None
974
+ and _is_forge_ui_mode(forge_state.ui_mode)
975
+ and "forge" in visible_items
976
+ ):
977
+ run_id = forge_state.paths.run_id if forge_state.paths is not None else "-"
978
+ task_count = len((forge_state.plan or {}).get("tasks") or [])
979
+ toolbar_parts.append(run_id)
980
+ toolbar_parts.append(f"{task_count} task" + ("" if task_count == 1 else "s"))
981
+ elif forge_state is not None and "plan" in visible_items:
982
+ if plan_mode_enabled:
983
+ toolbar_parts.append("plan readonly")
984
+ else:
985
+ toolbar_parts.append("plan /plan <task>")
986
+ if plan_mode_enabled and not (
987
+ forge_state is not None and _is_forge_ui_mode(forge_state.ui_mode)
988
+ ):
989
+ toolbar_parts.append("Esc /plan off")
990
+ if not toolbar_parts:
991
+ return " /help "
992
+ return " " + " · ".join([*toolbar_parts, "/help"]) + " "
993
+
994
+
995
+ def _chat_prompt_label(*, ui_mode: str = "chat", mode: str = "") -> str:
996
+ if _is_forge_ui_mode(ui_mode):
997
+ # ASCII-safe fallback (used when prompt_toolkit HTML is unavailable);
998
+ # the interactive path uses the ``\u25b8`` form below.
999
+ return "forge > "
1000
+ _ = mode
1001
+ return _CHAT_PROMPT_TEXT
1002
+
1003
+
1004
+ def _chat_prompt_label_formatted(*, ui_mode: str = "chat", mode: str = "") -> Any:
1005
+ try:
1006
+ from prompt_toolkit.formatted_text import HTML
1007
+ except Exception:
1008
+ return _chat_prompt_label(ui_mode=ui_mode, mode=mode)
1009
+ if _is_forge_ui_mode(ui_mode):
1010
+ return HTML("<b>forge</b> <ansicyan>\u25b8</ansicyan> ")
1011
+ _ = mode
1012
+ return HTML("<b>&gt;</b> ")
1013
+
1014
+
1015
+ def _chat_prompt_fallback_label(*, ui_mode: str = "chat", mode: str = "") -> str:
1016
+ if _is_forge_ui_mode(ui_mode):
1017
+ return "forge"
1018
+ _ = mode
1019
+ return _CHAT_PROMPT_FALLBACK_LABEL
1020
+
1021
+
1022
+ def _accept_chat_suggestion_or_complete(event: Any) -> None:
1023
+ current_buffer = getattr(event, "current_buffer", None)
1024
+ if current_buffer is None:
1025
+ return
1026
+ complete_state = getattr(current_buffer, "complete_state", None)
1027
+ if complete_state is not None:
1028
+ completion = getattr(complete_state, "current_completion", None)
1029
+ apply_completion = getattr(current_buffer, "apply_completion", None)
1030
+ if completion is not None and callable(apply_completion):
1031
+ apply_completion(completion)
1032
+ return
1033
+ complete_next = getattr(current_buffer, "complete_next", None)
1034
+ if callable(complete_next):
1035
+ complete_next()
1036
+ return
1037
+ suggestion = getattr(current_buffer, "suggestion", None)
1038
+ suggestion_text = str(getattr(suggestion, "text", "") or "")
1039
+ document = getattr(current_buffer, "document", None)
1040
+ if suggestion_text and bool(getattr(document, "is_cursor_at_the_end", False)):
1041
+ insert_text = getattr(current_buffer, "insert_text", None)
1042
+ if callable(insert_text):
1043
+ insert_text(suggestion_text)
1044
+ return
1045
+ start_completion = getattr(current_buffer, "start_completion", None)
1046
+ if callable(start_completion):
1047
+ start_completion(select_first=False)
1048
+
1049
+
1050
+ def _submitted_prompt_total_lines(
1051
+ *,
1052
+ submitted_text: str,
1053
+ prompt_label: str,
1054
+ terminal_columns: int | None = None,
1055
+ ) -> int:
1056
+ columns = int(terminal_columns or 0)
1057
+ if columns <= 0:
1058
+ columns = _terminal_width(default=80)
1059
+ if columns <= 0:
1060
+ columns = 80
1061
+
1062
+ normalized = str(submitted_text or "").replace("\r\n", "\n").replace("\r", "\n")
1063
+ segments = normalized.split("\n")
1064
+ first_prefix = max(1, visibleLength(prompt_label))
1065
+ total_lines = 0
1066
+ for idx, segment in enumerate(segments):
1067
+ segment_width = visibleLength(segment)
1068
+ prefix_width = first_prefix if idx == 0 else 0
1069
+ visual_width = segment_width + prefix_width
1070
+ if visual_width <= 0:
1071
+ total_lines += 1
1072
+ continue
1073
+ total_lines += ((visual_width - 1) // columns) + 1
1074
+ return max(1, total_lines)
1075
+
1076
+
1077
+ def _clear_previous_terminal_lines_ansi(line_count: int) -> str:
1078
+ count = max(1, int(line_count or 1))
1079
+ parts: list[str] = []
1080
+ for _ in range(count):
1081
+ parts.append("\x1b[1A\r\x1b[2K")
1082
+ parts.append("\r")
1083
+ return "".join(parts)
1084
+
1085
+
1086
+ def _clear_submitted_prompt_line(
1087
+ *,
1088
+ submitted_text: str = "",
1089
+ prompt_label: str = _CHAT_PROMPT_TEXT,
1090
+ terminal_columns: int | None = None,
1091
+ ) -> None:
1092
+ if not _patchable("_is_interactive_terminal", _is_interactive_terminal)():
1093
+ return
1094
+ term = (os.environ.get("TERM") or "").strip().lower()
1095
+ if term in {"", "dumb"}:
1096
+ return
1097
+ try:
1098
+ clear_count = _submitted_prompt_total_lines(
1099
+ submitted_text=submitted_text,
1100
+ prompt_label=prompt_label,
1101
+ terminal_columns=terminal_columns,
1102
+ )
1103
+ sys.stdout.write(_clear_previous_terminal_lines_ansi(clear_count))
1104
+ sys.stdout.flush()
1105
+ except Exception:
1106
+ pass
1107
+
1108
+
1109
+ def _redact_chat_error_text(text: str) -> str:
1110
+ out = str(text or "")
1111
+ for pattern in _CHAT_LLM_ERROR_REDACT_PATTERNS:
1112
+ if pattern.pattern.lower().startswith("(authorization"):
1113
+ out = pattern.sub(r"\1[REDACTED]", out)
1114
+ continue
1115
+ if pattern.pattern.lower().startswith("(bearer"):
1116
+ out = pattern.sub(r"\1[REDACTED]", out)
1117
+ continue
1118
+ out = pattern.sub("[REDACTED]", out)
1119
+ return out
1120
+
1121
+
1122
+ def _truncate_chat_error_text(text: str, *, max_chars: int = _CHAT_LLM_ERROR_MAX_CHARS) -> str:
1123
+ clean = _redact_chat_error_text(str(text).strip())
1124
+ if not clean:
1125
+ return "No additional error details."
1126
+ if len(clean) <= max_chars:
1127
+ return clean
1128
+ return clean[: max_chars - 15].rstrip() + "...(truncated)"
1129
+
1130
+
1131
+ def _chat_llm_error_panel(*, message: str) -> Panel:
1132
+ friendly_message = friendly_llm_error_message(message)
1133
+ display = classify_llm_error_display(friendly_message)
1134
+ body_lines = [_truncate_chat_error_text(friendly_message)]
1135
+ if display.guidance_lines:
1136
+ body_lines.append("")
1137
+ body_lines.extend(display.guidance_lines)
1138
+ return _Panel("\n".join(body_lines), title=display.title, border_style="red")
1139
+
1140
+
1141
+ def _humanized_chat_llm_error_message(error: Exception) -> str:
1142
+ return friendly_llm_error_message(error)
1143
+
1144
+
1145
+ def _render_chat_llm_error(*, session: Any, console: Console, error: Exception) -> None:
1146
+ message = _humanized_chat_llm_error_message(error)
1147
+ surface = getattr(session, "surface", None)
1148
+ on_error = getattr(surface, "on_error", None)
1149
+ if callable(on_error):
1150
+ try:
1151
+ on_error(message)
1152
+ except Exception: # noqa: BLE001
1153
+ console.print("")
1154
+ console.print(_chat_llm_error_panel(message=message))
1155
+ return
1156
+ if bool(getattr(surface, "renders_error_panel", False)):
1157
+ return
1158
+ console.print("")
1159
+ console.print(_chat_llm_error_panel(message=message))
1160
+
1161
+
1162
+ def _plan_mode_action_rows() -> list[tuple[str, str, str]]:
1163
+ return [
1164
+ (
1165
+ "approve",
1166
+ "Approve and execute",
1167
+ "Run the task immediately using this approved draft.",
1168
+ ),
1169
+ ("propose", "Propose changes", "Provide feedback and regenerate a revised draft."),
1170
+ ("discard", "Discard this plan", "Cancel this draft and return to chat."),
1171
+ ]
1172
+
1173
+
1174
+ def _plan_mode_actions_panel(
1175
+ *,
1176
+ selected_action: str | None = None,
1177
+ interactive: bool = False,
1178
+ ) -> Any:
1179
+ from rich.console import Group
1180
+
1181
+ selected = (selected_action or "").strip().casefold()
1182
+ renderables: list[Any] = []
1183
+ for idx, (value, label, desc) in enumerate(_plan_mode_action_rows(), start=1):
1184
+ row_selected = str(value).strip().casefold() == selected
1185
+ renderables.extend(
1186
+ _plan_mode_picker_row_renderables(
1187
+ label=f"{idx}) {label}",
1188
+ desc=desc,
1189
+ selected=row_selected,
1190
+ )
1191
+ )
1192
+ if interactive:
1193
+ renderables.append(_plan_mode_picker_hint_renderable())
1194
+ return Group(*renderables)
1195
+
1196
+
1197
+ def _select_plan_mode_action_interactive(*, console: Console) -> tuple[str | None, bool]:
1198
+ rows = _plan_mode_action_rows()
1199
+ return _patchable("_run_inline_option_selector", _run_inline_option_selector)(
1200
+ console=console,
1201
+ rows=rows,
1202
+ current_value="approve",
1203
+ panel_builder=lambda selected, interactive: _plan_mode_actions_panel(
1204
+ selected_action=selected,
1205
+ interactive=interactive,
1206
+ ),
1207
+ unavailable_label="Plan action picker",
1208
+ use_alt_screen=False,
1209
+ )
1210
+
1211
+
1212
+ def _prompt_plan_mode_action(*, console: Console) -> str | None:
1213
+ selected_action, picker_available = _patchable(
1214
+ "_select_plan_mode_action_interactive",
1215
+ _select_plan_mode_action_interactive,
1216
+ )(console=console)
1217
+ if picker_available:
1218
+ return selected_action
1219
+
1220
+ console.print(_plan_mode_actions_panel())
1221
+ try:
1222
+ choice = _patchable("_prompt_ask", _prompt_ask)(
1223
+ "Select option", choices=["1", "2", "3"], console=console
1224
+ )
1225
+ except (EOFError, KeyboardInterrupt):
1226
+ console.print("")
1227
+ return None
1228
+ if choice == "1":
1229
+ return "approve"
1230
+ if choice == "2":
1231
+ return "propose"
1232
+ if choice == "3":
1233
+ return "discard"
1234
+ return None
1235
+
1236
+
1237
+ def _prompt_plan_mode_feedback(*, console: Console) -> str | None:
1238
+ try:
1239
+ return _patchable("_prompt_text_with_escape", _prompt_text_with_escape)(
1240
+ "Plan feedback",
1241
+ escape_hint="Esc to cancel draft",
1242
+ )
1243
+ except (EOFError, KeyboardInterrupt):
1244
+ console.print("")
1245
+ return None
1246
+
1247
+
1248
+ __all__ = [name for name in globals() if (not name.startswith("__") or name == "__version__")]