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,1018 @@
1
+ """TUI-native panel spec builders for chat slash commands.
2
+
3
+ Each builder mirrors a classic ``_print_*`` / ``_chat_*_panel`` command but
4
+ returns a structured *spec* the full-screen TUI renders as a centered popup
5
+ panel (via ``_render_kv_panel_rows`` in :mod:`...cli_impl.tui.app`) instead of
6
+ dumping flat, colourless captured Rich text into the transcript.
7
+
8
+ A panel spec is::
9
+
10
+ {"title": str, "hint"?: str, "sections": [(section_name, rows)]}
11
+
12
+ where ``rows`` is a list of ``(key, value, tone)`` tuples. ``tone`` selects the
13
+ value colour: ``"accent"`` (on/healthy, green), ``"plain"`` (neutral),
14
+ ``"warn"`` (amber), ``"err"`` (red), ``"dim"`` (muted).
15
+
16
+ Unlike the sibling legacy split modules, this one imports every helper it needs
17
+ *explicitly* (no ``from .cli_common import *`` global injection), so the builders
18
+ are importable and unit-testable in isolation with a plain fake session.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from typing import Any
24
+
25
+ from ..tui.forge_status import forge_status_bucket, forge_status_glyph
26
+
27
+ # Canonical, self-contained helpers (defined directly in these modules — not
28
+ # injected at runtime), so the imports resolve at import time and in tests.
29
+ from .startup import (
30
+ _chat_context_percent_value,
31
+ _chat_usage_hud_enabled,
32
+ _format_chat_context_percent,
33
+ _format_exact_token_count,
34
+ _format_reported_token_total,
35
+ _format_usage_billing_for_display,
36
+ _format_usage_source_for_display,
37
+ _known_cost_value,
38
+ _refresh_chat_hud_context_cache,
39
+ _session_skill_listing,
40
+ )
41
+
42
+ PanelSpec = dict[str, Any]
43
+
44
+
45
+ def _format_terminal_runtime_s(runtime_s: float) -> str:
46
+ """Mirror of the classic terminal runtime formatter (kept local so this
47
+ module never imports the injected ``chat/commands.py`` legacy module)."""
48
+ if runtime_s < 0:
49
+ runtime_s = 0.0
50
+ if runtime_s < 60:
51
+ return f"{runtime_s:.1f}s"
52
+ minutes = int(runtime_s // 60)
53
+ seconds = int(runtime_s % 60)
54
+ return f"{minutes}m{seconds:02d}s"
55
+
56
+
57
+ # ------------------------------------------------------------------- /usage
58
+ def _chat_usage_panel_spec(*, session: Any) -> PanelSpec:
59
+ """Token count & cost as a panel (mirrors ``_print_chat_usage``)."""
60
+ summary = getattr(session, "usage_summary", None)
61
+ if summary is None:
62
+ return {
63
+ "title": "Usage",
64
+ "sections": [
65
+ ("Usage", [("status", "Usage tracking unavailable for this session.", "plain")])
66
+ ],
67
+ }
68
+ try:
69
+ rows = summary.by_model_rows()
70
+ except Exception: # noqa: BLE001 - never crash the UI on a panel
71
+ rows = []
72
+ if not rows:
73
+ hud = "on" if _chat_usage_hud_enabled(session) else "off"
74
+ return {
75
+ "title": "Usage",
76
+ "sections": [
77
+ (
78
+ "Usage",
79
+ [
80
+ ("status", "No usage events yet in this session.", "plain"),
81
+ ("hud", hud, "accent" if hud == "on" else "plain"),
82
+ ],
83
+ )
84
+ ],
85
+ "hint": "/usage hud on|off to toggle the toolbar HUD · Esc close",
86
+ }
87
+
88
+ # Index keys (not the model name) so a long, namespaced model id never blows
89
+ # past the panel's narrow key column — the full name lives in the wrap-safe
90
+ # value column instead.
91
+ model_rows: list[tuple[str, str, str]] = []
92
+ for idx, row in enumerate(rows, start=1):
93
+ cost = _format_usage_billing_for_display(row, compact=True)
94
+ source = _format_usage_source_for_display(row)
95
+ cache_read = _format_reported_token_total(
96
+ row,
97
+ token_key="cache_read_input_tokens",
98
+ reported_calls_key="cache_read_reported_calls",
99
+ )
100
+ cache_write = _format_reported_token_total(
101
+ row,
102
+ token_key="cache_creation_input_tokens",
103
+ reported_calls_key="cache_creation_reported_calls",
104
+ )
105
+ # A provider that reports no cache figures at all says nothing useful by
106
+ # repeating "not reported" twice on every row; the /usage table still
107
+ # spells the state out per column.
108
+ cache_bits = (
109
+ ""
110
+ if cache_read == "not reported" and cache_write == "not reported"
111
+ else f" cache r:{cache_read} w:{cache_write}"
112
+ )
113
+ value = (
114
+ f"{row.get('model') or '-'} "
115
+ f"↓ {_format_exact_token_count(row.get('prompt_tokens'))} "
116
+ f"↑ {_format_exact_token_count(row.get('completion_tokens'))} "
117
+ f"Σ {_format_exact_token_count(row.get('total_tokens'))} "
118
+ f"{cost}{cache_bits} ({source})"
119
+ )
120
+ model_rows.append((str(idx), value, "plain"))
121
+
122
+ totals = summary.totals()
123
+ unknown_total = int(totals.get("unknown_cost_calls") or 0)
124
+ total_cost = _format_usage_billing_for_display(totals)
125
+ # Only paint the cost green when it is fully known; an unknown/partial total
126
+ # stays neutral (never reads as healthy) — matching the classic "[yellow]Total
127
+ # cost is partial" warning.
128
+ cost_tone = (
129
+ "accent"
130
+ if (
131
+ _known_cost_value(totals) is not None
132
+ and unknown_total == 0
133
+ and int(totals.get("catalog_estimated_cost_calls") or 0) == 0
134
+ )
135
+ else "plain"
136
+ )
137
+ total_rows: list[tuple[str, str, str]] = [
138
+ ("processed", _format_exact_token_count(totals.get("total_tokens")), "accent"),
139
+ ("input total", _format_exact_token_count(totals.get("prompt_tokens")), "plain"),
140
+ (
141
+ "input uncached",
142
+ _format_reported_token_total(
143
+ totals,
144
+ token_key="input_tokens_uncached",
145
+ reported_calls_key="input_tokens_uncached_reported_calls",
146
+ derived_calls_key="input_tokens_uncached_derived_calls",
147
+ ),
148
+ "plain",
149
+ ),
150
+ (
151
+ "cache read",
152
+ _format_reported_token_total(
153
+ totals,
154
+ token_key="cache_read_input_tokens",
155
+ reported_calls_key="cache_read_reported_calls",
156
+ ),
157
+ "plain",
158
+ ),
159
+ (
160
+ "cache write",
161
+ _format_reported_token_total(
162
+ totals,
163
+ token_key="cache_creation_input_tokens",
164
+ reported_calls_key="cache_creation_reported_calls",
165
+ ),
166
+ "plain",
167
+ ),
168
+ ("output", _format_exact_token_count(totals.get("completion_tokens")), "plain"),
169
+ ("billing", total_cost, cost_tone),
170
+ (
171
+ "meaning",
172
+ "processed is cumulative across calls, not current context",
173
+ "dim",
174
+ ),
175
+ ]
176
+ cache_efficiency = totals.get("cache_efficiency")
177
+ if isinstance(cache_efficiency, dict) and cache_efficiency.get("hit_ratio") is not None:
178
+ largest = cache_efficiency.get("largest_uncached_call")
179
+ largest_text = ""
180
+ if isinstance(largest, dict):
181
+ largest_text = (
182
+ f"; max {_format_exact_token_count(largest.get('uncached_prompt_tokens'))} "
183
+ f"at call {int(largest.get('call_position') or 0)}"
184
+ )
185
+ total_rows.append(
186
+ (
187
+ "cache hit",
188
+ f"{float(cache_efficiency['hit_ratio']) * 100:.2f}%{largest_text}",
189
+ "plain",
190
+ )
191
+ )
192
+ if unknown_total > 0:
193
+ total_rows.append(
194
+ ("note", f"{unknown_total} call(s) have unknown cost (missing pricing)", "warn")
195
+ )
196
+ cache_cost_unknown = int(totals.get("cache_cost_pricing_missing_calls") or 0)
197
+ if cache_cost_unknown > 0:
198
+ total_rows.append(
199
+ (
200
+ "note",
201
+ f"{cache_cost_unknown} cached call(s) need provider-specific cache pricing",
202
+ "warn",
203
+ )
204
+ )
205
+ corrected_total = int(totals.get("corrected_usage_calls") or 0)
206
+ if corrected_total > 0:
207
+ total_rows.append(
208
+ ("note", f"{corrected_total} provider usage record(s) corrected before display", "warn")
209
+ )
210
+ request_estimate = totals.get("request_token_estimate")
211
+ if isinstance(request_estimate, dict):
212
+ schema_over_calls = int(request_estimate.get("tool_schema_budget_exceeded_calls") or 0)
213
+ if schema_over_calls > 0:
214
+ schema_overage = int(request_estimate.get("tool_schema_budget_overage_tokens") or 0)
215
+ largest_tool = int(request_estimate.get("tool_schema_largest_tool_tokens") or 0)
216
+ total_rows.append(
217
+ (
218
+ "note",
219
+ (
220
+ f"{schema_over_calls} call(s) exceeded tool schema shadow budget "
221
+ f"(+{_format_exact_token_count(schema_overage)}; "
222
+ f"largest tool {_format_exact_token_count(largest_tool)})"
223
+ ),
224
+ "warn",
225
+ )
226
+ )
227
+ return {
228
+ "title": "Usage",
229
+ "sections": [("Per model", model_rows), ("Total", total_rows)],
230
+ }
231
+
232
+
233
+ # --------------------------------------------------------------- /context (/ctx alias)
234
+ def _chat_context_panel_spec(*, session: Any) -> PanelSpec:
235
+ """Context-window usage as a panel (mirrors the primary ``/context`` table)."""
236
+ if not callable(getattr(session, "context_left", None)):
237
+ return {
238
+ "title": "Context Window",
239
+ "sections": [
240
+ ("Context", [("status", "Context tracking unavailable for this session.", "plain")])
241
+ ],
242
+ }
243
+ _refresh_chat_hud_context_cache(session)
244
+ ctx = getattr(session, "_hud_context_cache", None)
245
+ if ctx is None:
246
+ return {
247
+ "title": "Context Window",
248
+ "sections": [
249
+ ("Context", [("status", "Context tracking unavailable for this session.", "plain")])
250
+ ],
251
+ }
252
+ context_window = getattr(ctx, "context_window_tokens", getattr(ctx, "max_input_tokens", "n/a"))
253
+ remaining = getattr(
254
+ ctx, "context_window_remaining_tokens", getattr(ctx, "remaining_tokens", "n/a")
255
+ )
256
+ percent = getattr(ctx, "context_window_percent_left", getattr(ctx, "percent_left", None))
257
+ used = getattr(ctx, "used_input_tokens", "n/a")
258
+ effective_budget = getattr(ctx, "effective_input_budget", None)
259
+ effective_remaining = getattr(ctx, "effective_remaining_tokens", None)
260
+ effective_percent = getattr(ctx, "effective_percent_left", None)
261
+ startup_baseline = getattr(ctx, "startup_baseline_tokens", None)
262
+ dynamic_budget = getattr(ctx, "dynamic_context_budget_tokens", None)
263
+ dynamic_used = getattr(ctx, "dynamic_context_used_tokens", None)
264
+ dynamic_remaining = getattr(ctx, "dynamic_context_remaining_tokens", None)
265
+ dynamic_percent = getattr(ctx, "dynamic_context_percent_left", None)
266
+
267
+ # Colour the percent from the SAME metric we display (the context-window
268
+ # percent), not the dynamic-budget percent — otherwise a healthy 60%-left
269
+ # window could be painted red because the baseline-subtracted dynamic value
270
+ # is much lower. Fall back to the session helper only when the window value
271
+ # is missing.
272
+ displayed_percent = percent
273
+ if displayed_percent is None:
274
+ displayed_percent = _chat_context_percent_value(session)
275
+ try:
276
+ percent_float = float(displayed_percent) if displayed_percent is not None else None
277
+ except (TypeError, ValueError):
278
+ percent_float = None
279
+ if percent_float is None:
280
+ pct_tone = "plain"
281
+ elif percent_float < 10.0:
282
+ pct_tone = "err"
283
+ elif percent_float < 25.0:
284
+ pct_tone = "warn"
285
+ else:
286
+ pct_tone = "accent"
287
+ percent_display = _format_chat_context_percent(displayed_percent)
288
+
289
+ context_rows: list[tuple[str, str, str]] = [
290
+ ("model", str(getattr(ctx, "model_name", "-")), "plain"),
291
+ ("metadata source", str(getattr(ctx, "source", "-")), "dim"),
292
+ (
293
+ "capacity route",
294
+ str(getattr(ctx, "capacity_provider_key", None) or "-"),
295
+ "dim",
296
+ ),
297
+ ("token source", str(getattr(ctx, "token_count_source", "-")), "dim"),
298
+ ("confidence", str(getattr(ctx, "token_count_confidence", "-")), "dim"),
299
+ ("context window", str(context_window), "plain"),
300
+ ("used (projected)", str(used), "plain"),
301
+ ("left tokens", str(remaining), "plain"),
302
+ ("left %", percent_display, pct_tone),
303
+ ]
304
+ anchor_source = getattr(ctx, "anchor_token_count_source", None)
305
+ if anchor_source:
306
+ context_rows.insert(
307
+ 4,
308
+ ("request anchor", str(anchor_source), "dim"),
309
+ )
310
+ budget_rows: list[tuple[str, str, str]] = [
311
+ ("input budget", _context_metric_display(effective_budget), "plain"),
312
+ ("budget left", _context_metric_display(effective_remaining), "plain"),
313
+ (
314
+ "budget left %",
315
+ _format_chat_context_percent(_context_float(effective_percent)),
316
+ _context_percent_tone(effective_percent),
317
+ ),
318
+ ]
319
+ conversation_rows: list[tuple[str, str, str]] = [
320
+ ("startup baseline", _context_metric_display(startup_baseline), "plain"),
321
+ ("conversation budget", _context_metric_display(dynamic_budget), "plain"),
322
+ ("conversation used", _context_metric_display(dynamic_used), "plain"),
323
+ ("conversation left", _context_metric_display(dynamic_remaining), "plain"),
324
+ (
325
+ "conversation left %",
326
+ _format_chat_context_percent(_context_float(dynamic_percent)),
327
+ _context_percent_tone(dynamic_percent),
328
+ ),
329
+ ]
330
+ sections: list[tuple[str, list[tuple[str, str, str]]]] = [("Context", context_rows)]
331
+ if any(row[1] != "n/a" for row in budget_rows):
332
+ sections.append(("Effective Input Budget", budget_rows))
333
+ if any(row[1] != "n/a" for row in conversation_rows):
334
+ sections.append(("Conversation Context", conversation_rows))
335
+ return {
336
+ "title": "Context Window",
337
+ "sections": sections,
338
+ "hint": "/compact to reduce context · Esc close",
339
+ }
340
+
341
+
342
+ def _context_float(value: Any) -> float | None:
343
+ if value is None:
344
+ return None
345
+ try:
346
+ return float(value)
347
+ except (TypeError, ValueError):
348
+ return None
349
+
350
+
351
+ def _context_percent_tone(value: Any) -> str:
352
+ percent = _context_float(value)
353
+ if percent is None:
354
+ return "plain"
355
+ if percent < 10.0:
356
+ return "err"
357
+ if percent < 25.0:
358
+ return "warn"
359
+ return "accent"
360
+
361
+
362
+ def _context_metric_display(value: Any) -> str:
363
+ if value is None:
364
+ return "n/a"
365
+ try:
366
+ parsed = int(value)
367
+ except (TypeError, ValueError):
368
+ return str(value)
369
+ return str(max(0, parsed))
370
+
371
+
372
+ # ----------------------------------------------------------------- /model-info
373
+ def _chat_model_info_panel_spec(*, session: Any, model_ref: str = "") -> PanelSpec:
374
+ """Resolved model metadata as a panel (mirrors ``/model-info``)."""
375
+ model_name = (model_ref or "").strip() or str(
376
+ getattr(getattr(session, "client", None), "model", "") or ""
377
+ ).strip()
378
+ if not model_name:
379
+ return {
380
+ "title": "Model Info",
381
+ "sections": [
382
+ ("Model", [("status", "Model info unavailable (missing model name).", "plain")])
383
+ ],
384
+ }
385
+ registry = getattr(session, "model_registry", None)
386
+ if registry is None:
387
+ return {
388
+ "title": "Model Info",
389
+ "sections": [
390
+ ("Model", [("status", "Model info unavailable for this session.", "plain")])
391
+ ],
392
+ }
393
+ try:
394
+ meta = registry.get(model_name)
395
+ except Exception as exc: # noqa: BLE001 - surface a resolve failure inline
396
+ return {
397
+ "title": f"Model Info ({model_name})",
398
+ "sections": [("Model", [("error", str(exc), "err")])],
399
+ }
400
+
401
+ field_sources = getattr(meta, "field_sources", {}) or {}
402
+
403
+ def _src(key: str) -> str:
404
+ return str(field_sources.get(key, "unknown"))
405
+
406
+ rows: list[tuple[str, str, str]] = [
407
+ ("resolved", str(getattr(meta, "model_name", model_name)), "accent"),
408
+ ("source", str(getattr(meta, "source", "unknown")), "plain"),
409
+ (
410
+ "context window",
411
+ f"{getattr(meta, 'context_window_tokens', '-')} ({_src('context_window_tokens')})",
412
+ "plain",
413
+ ),
414
+ (
415
+ "max output",
416
+ f"{getattr(meta, 'max_output_tokens', '-')} ({_src('max_output_tokens')})",
417
+ "plain",
418
+ ),
419
+ (
420
+ "vision",
421
+ f"{getattr(meta, 'supports_vision', '-')} ({_src('supports_vision')})",
422
+ "plain",
423
+ ),
424
+ (
425
+ "input $/tok",
426
+ f"{getattr(meta, 'input_cost_per_token', '-')} ({_src('input_cost_per_token')})",
427
+ "plain",
428
+ ),
429
+ (
430
+ "output $/tok",
431
+ f"{getattr(meta, 'output_cost_per_token', '-')} ({_src('output_cost_per_token')})",
432
+ "plain",
433
+ ),
434
+ ]
435
+ last_error = getattr(registry, "last_error", None)
436
+ if last_error:
437
+ rows.append(("registry error", str(last_error), "err"))
438
+ warnings = "; ".join(getattr(meta, "warnings", ())[:5])
439
+ if warnings:
440
+ rows.append(("warnings", warnings, "warn"))
441
+ return {"title": f"Model Info ({model_name})", "sections": [("Model", rows)]}
442
+
443
+
444
+ # -------------------------------------------------------------------- /config
445
+ def _chat_config_panel_spec(*, session: Any) -> PanelSpec:
446
+ """Tracked-model config as a panel (mirrors ``_chat_config_panel``).
447
+
448
+ The interactive config menu cannot run inside the alt-screen, so the bare
449
+ ``/config`` shows this read-only panel; ``/config set|clear`` still apply via
450
+ the command runner.
451
+ """
452
+ from .chat_status import _chat_used_models # self-contained getter
453
+
454
+ models = _chat_used_models(session)
455
+ registry = getattr(session, "model_registry", None)
456
+
457
+ model_rows: list[tuple[str, str, str]] = []
458
+ if models:
459
+ for idx, name in enumerate(models, start=1):
460
+ summary = ""
461
+ if registry is not None:
462
+ try:
463
+ meta = registry.get(name)
464
+ except Exception as exc: # noqa: BLE001
465
+ summary = f"error={exc}"
466
+ else:
467
+ field_sources = getattr(meta, "field_sources", {}) or {}
468
+ src = field_sources.get(
469
+ "context_window_tokens", getattr(meta, "source", "unknown")
470
+ )
471
+ summary = (
472
+ f"ctx={getattr(meta, 'context_window_tokens', '-')} "
473
+ f"out={getattr(meta, 'max_output_tokens', '-')} src={src}"
474
+ )
475
+ value = f"{name} {summary}".rstrip() if summary else str(name)
476
+ model_rows.append((str(idx), value, "accent" if idx == 1 else "plain"))
477
+ else:
478
+ model_rows.append(("models", "No tracked models in this session yet.", "plain"))
479
+
480
+ usage_rows: list[tuple[str, str, str]] = [
481
+ ("set", "/config set <model|index> <ctx> <max_out> [vision] [in$] [out$]", "dim"),
482
+ ("clear", "/config clear <model|index>", "dim"),
483
+ ("example", "/config set 1 128000 4096 false", "dim"),
484
+ ]
485
+ return {
486
+ "title": "Model Config",
487
+ "sections": [("Tracked models", model_rows), ("Manage", usage_rows)],
488
+ }
489
+
490
+
491
+ # ------------------------------------------------------------------- /toolbar
492
+ def _chat_toolbar_panel_spec(*, session: Any) -> PanelSpec:
493
+ """Status-toolbar items as a panel (mirrors bare ``/toolbar``)."""
494
+ from .cli_common import (
495
+ _CHAT_TOOLBAR_ITEM_ORDER,
496
+ _DEFAULT_TOOLBAR_ITEMS,
497
+ _VALID_TOOLBAR_ITEMS,
498
+ )
499
+
500
+ cfg_obj = getattr(session, "cfg", None)
501
+ raw_items = getattr(cfg_obj, "toolbar_items", None) if cfg_obj is not None else None
502
+ active: list[str] = []
503
+ seen: set[str] = set()
504
+ for raw_item in list(raw_items or list(_DEFAULT_TOOLBAR_ITEMS)):
505
+ item = str(raw_item).strip().lower()
506
+ if not item or item in seen or item not in _VALID_TOOLBAR_ITEMS:
507
+ continue
508
+ seen.add(item)
509
+ active.append(item)
510
+ available = [item for item in _CHAT_TOOLBAR_ITEM_ORDER if item not in set(active)]
511
+
512
+ item_rows: list[tuple[str, str, str]] = [
513
+ ("active", ", ".join(active) if active else "(none)", "accent"),
514
+ ("available", ", ".join(available) if available else "(none)", "plain"),
515
+ ]
516
+ manage_rows: list[tuple[str, str, str]] = [
517
+ ("add", "/toolbar add <item>", "dim"),
518
+ ("remove", "/toolbar remove <item>", "dim"),
519
+ ("reset", "/toolbar reset", "dim"),
520
+ ("save", "/toolbar save", "dim"),
521
+ ]
522
+ return {
523
+ "title": "Toolbar Items",
524
+ "sections": [("Items", item_rows), ("Manage", manage_rows)],
525
+ }
526
+
527
+
528
+ # ----------------------------------------------------------------- /terminals
529
+ def _chat_terminals_panel_spec(*, session: Any) -> PanelSpec:
530
+ """Background processes as a panel (mirrors ``/terminals list``)."""
531
+ terminal_manager = getattr(session, "terminal_manager", None)
532
+ if terminal_manager is None:
533
+ return {
534
+ "title": "Background Terminals",
535
+ "sections": [
536
+ (
537
+ "Terminals",
538
+ [("status", "Background terminals are unavailable in this session.", "plain")],
539
+ )
540
+ ],
541
+ }
542
+ try:
543
+ summaries = terminal_manager.list()
544
+ except Exception as exc: # noqa: BLE001
545
+ return {
546
+ "title": "Background Terminals",
547
+ "sections": [("Terminals", [("error", str(exc), "err")])],
548
+ }
549
+ if not summaries:
550
+ process_rows: list[tuple[str, str, str]] = [("status", "No background processes.", "plain")]
551
+ else:
552
+ process_rows = []
553
+ for summary in summaries:
554
+ status = str(getattr(summary, "status", "-"))
555
+ status_lc = status.casefold()
556
+ if status_lc in {"failed", "error"}:
557
+ tone = "err"
558
+ elif status_lc == "running":
559
+ tone = "accent"
560
+ else:
561
+ tone = "plain"
562
+ exit_code = getattr(summary, "exit_code", None)
563
+ runtime = _format_terminal_runtime_s(float(getattr(summary, "runtime_s", 0) or 0))
564
+ value = (
565
+ f"{status} · exit {('-' if exit_code is None else exit_code)} "
566
+ f"· {runtime} · {getattr(summary, 'cmd', '')}"
567
+ )
568
+ process_rows.append((str(getattr(summary, "process_id", "-")), value, tone))
569
+ manage_rows: list[tuple[str, str, str]] = [
570
+ ("show", "/terminals show <id>", "dim"),
571
+ ("kill", "/terminals kill <id>", "dim"),
572
+ ]
573
+ return {
574
+ "title": "Background Terminals",
575
+ "sections": [("Processes", process_rows), ("Manage", manage_rows)],
576
+ }
577
+
578
+
579
+ # --------------------------------------------------------------------- /skill
580
+ def _chat_skill_listing_panel_spec(*, session: Any) -> PanelSpec:
581
+ """Discovered skills as a panel (mirrors bare ``/skill``)."""
582
+ enabled, ordered, issues = _session_skill_listing(session)
583
+ if not enabled:
584
+ return {
585
+ "title": "Skills",
586
+ "sections": [
587
+ ("Skills", [("status", "Skills are disabled for this session config.", "plain")])
588
+ ],
589
+ }
590
+ if not ordered:
591
+ return {
592
+ "title": "Skills",
593
+ "sections": [
594
+ (
595
+ "Skills",
596
+ [("status", "No skills discovered in the supported skill roots.", "plain")],
597
+ )
598
+ ],
599
+ }
600
+ skill_rows = [
601
+ (str(getattr(skill, "name", "")), str(getattr(skill, "description", "") or ""), "plain")
602
+ for skill in ordered
603
+ ]
604
+ sections: list[tuple[str, list[tuple[str, str, str]]]] = [
605
+ (f"Skills ({len(ordered)})", skill_rows)
606
+ ]
607
+ issue_rows: list[tuple[str, str, str]] = []
608
+ for issue in issues:
609
+ source_path = getattr(issue, "source_path", None)
610
+ message = str(getattr(issue, "message", "") or "").strip()
611
+ if source_path is not None and message:
612
+ issue_rows.append((str(source_path), message, "warn"))
613
+ if issue_rows:
614
+ sections.append(("Skipped", issue_rows))
615
+ return {
616
+ "title": "Skills",
617
+ "sections": sections,
618
+ "hint": "/skill <name> for info · /skill <name> <task> to attach · Esc close",
619
+ }
620
+
621
+
622
+ # ------------------------------------------------------- subagent descriptions
623
+ # Leading boilerplate stripped from a subagent description before summarising
624
+ # (longest-first so the most specific prefix wins). The blurbs all open with some
625
+ # variant of "Use this when you need to …" / "Catch-all subagent for …", which
626
+ # carries no information for a one-line picker label.
627
+ _SUBAGENT_DESC_BOILERPLATE = (
628
+ "use this agent when you need to ",
629
+ "use this agent when you need ",
630
+ "use this agent when ",
631
+ "use this when you need to ",
632
+ "use this when you need an ",
633
+ "use this when you need a ",
634
+ "use this when you need ",
635
+ "use this when answering means ",
636
+ "use this when ",
637
+ "use this for ",
638
+ "use for ",
639
+ "catch-all subagent for ",
640
+ "catch-all agent for ",
641
+ "catch-all for ",
642
+ )
643
+
644
+
645
+ def _short_subagent_desc(text: str, limit: int = 46) -> str:
646
+ """Condense a (potentially huge) subagent description to a short, scannable
647
+ summary shown to the right of the option in the spawn picker.
648
+
649
+ Collapses whitespace, strips the boilerplate "Use this when you need to …"
650
+ lead-in, keeps only the first clause (up to the first ``:`` / ``.`` / dash),
651
+ capitalises it, and hard-truncates with an ellipsis so each option stays a
652
+ single readable line.
653
+ """
654
+ compact = " ".join(str(text or "").split())
655
+ if not compact:
656
+ return ""
657
+ lowered = compact.lower()
658
+ for prefix in _SUBAGENT_DESC_BOILERPLATE:
659
+ if lowered.startswith(prefix):
660
+ compact = compact[len(prefix) :]
661
+ break
662
+ # Keep only the first clause/sentence — the crisp "what it does".
663
+ cut = len(compact)
664
+ for sep in (": ", ". ", "; ", " — ", " - "):
665
+ idx = compact.find(sep)
666
+ if idx != -1:
667
+ cut = min(cut, idx)
668
+ summary = compact[:cut].rstrip(" ,;:.—-")
669
+ if summary:
670
+ summary = summary[0].upper() + summary[1:]
671
+ if len(summary) > limit:
672
+ head = summary[: max(1, limit - 1)]
673
+ # Prefer breaking on a word boundary so we never clip mid-word ("recent c…").
674
+ space = head.rfind(" ")
675
+ if space >= limit // 2:
676
+ head = head[:space]
677
+ summary = head.rstrip(" ,;:.—-") + "…"
678
+ return summary
679
+
680
+
681
+ # --------------------------------------------------------------------- Forge
682
+ # The status→bucket authority lives in ``...tui.forge_status`` so the live swarm
683
+ # table, the ``/show`` plan panel here, and the end-of-run summary can never
684
+ # disagree about whether a task is done, failed, or still running.
685
+ def _forge_status_tone(canonical: str) -> str:
686
+ """Map a canonical task status to a panel value tone (accent/err/dim/plain).
687
+
688
+ done → accent (green), failed → err (red), obsolete → dim, everything else
689
+ (running/planned) → plain — the SAME semantic colours the live swarm table
690
+ uses, so a task reads identically in ``/show`` and while it runs."""
691
+ bucket = forge_status_bucket(canonical)
692
+ if bucket == "done":
693
+ return "accent"
694
+ if bucket == "failed":
695
+ return "err"
696
+ if bucket == "obsolete":
697
+ return "dim"
698
+ return "plain"
699
+
700
+
701
+ # The "how Forge works" guidance shown in the centered popup when the user runs a
702
+ # plain ``/forge``. Markdown so the doc panel renders headings/lists; the closing
703
+ # call-to-action pairs with the panel's "Enter to begin · Esc to cancel" hint.
704
+ _FORGE_INTRO_BODY = """\
705
+ # Forge — autonomous build mode
706
+
707
+ Forge turns a goal into a reviewed plan, then runs a **swarm of agents** to build
708
+ it task by task. You stay in control — nothing is built until you say so.
709
+
710
+ ## How it works
711
+
712
+ 1. **Set the goal** — describe what you want built (just type it, or `/goal`).
713
+ 2. **Shape the plan** — add or refine tasks with `/task`, or let the planner
714
+ assistant draft them for you.
715
+ 3. **Review** — inspect the plan and task table with `/show` and `/plan`.
716
+ 4. **Execute** — `/execute plan` runs the swarm to build every task, live.
717
+ 5. **Finish** — `/done` (or `/back`) returns you to normal chat.
718
+
719
+ ## Handy commands
720
+
721
+ - `/show` — plan summary: goal, requirements, tasks
722
+ - `/plan` — view or edit the plan (tasks · markdown · edit)
723
+ - `/assets` — attach and review reference files
724
+ - `/execute plan` — run the build
725
+ - `/done` · `/back` — leave Forge
726
+
727
+ Press **Enter** to choose your planner and begin, or **Esc** to stay in chat."""
728
+
729
+
730
+ def _chat_forge_intro_panel_spec() -> PanelSpec:
731
+ """Guidance popup shown for a plain ``/forge`` before entering the session.
732
+
733
+ Returns a document-panel spec (rendered through the TUI markdown pipeline)
734
+ carrying a ``confirm`` command: the centered popup explains how Forge works
735
+ and, on Enter, routes ``/forge`` to the command runner to actually enter the
736
+ planning session; Esc cancels. ``/forge resume`` and re-entry while already in
737
+ Forge skip this intro (handled by the caller's gating).
738
+ """
739
+ return {
740
+ "title": "Forge",
741
+ "body": _FORGE_INTRO_BODY,
742
+ "hint": "↵ Enter to begin · Esc to cancel",
743
+ "confirm": "/forge",
744
+ "accent": "forge",
745
+ }
746
+
747
+
748
+ def _chat_forge_plan_panel_spec(*, paths: Any, plan: dict[str, Any]) -> PanelSpec:
749
+ """Forge plan summary as a panel — mirrors ``_show_forge_plan_summary``.
750
+
751
+ Renders the goal/summary + per-task table that the classic ``/show`` and
752
+ ``/plan tasks`` print, as a centered popup instead of a flat Rich table dump.
753
+ Used in Forge mode for ``/show`` and ``/plan tasks|table|view``.
754
+ """
755
+ from ...forge import requirement_text
756
+ from ...swarm_scheduler import canonical_task_status
757
+ from .cli_common import _forge_task_mcp_summary_label, _forge_task_status_counts
758
+ from .forge_asset_view import forge_asset_view_count
759
+
760
+ goal = str(plan.get("project_goal") or "").strip() or "(not set)"
761
+ summary = str(plan.get("summary") or "").strip() or "(not set)"
762
+ tasks_obj = plan.get("tasks") or []
763
+ tasks = tasks_obj if isinstance(tasks_obj, list) else []
764
+ task_count = len(tasks)
765
+ try:
766
+ asset_count = forge_asset_view_count(paths, plan)
767
+ except Exception: # noqa: BLE001 - never crash the UI on a panel
768
+ asset_count = 0
769
+ try:
770
+ done, failed, remaining = _forge_task_status_counts(plan)
771
+ except Exception: # noqa: BLE001
772
+ done = failed = remaining = 0
773
+
774
+ overview: list[tuple[str, str, str]] = [
775
+ ("run", str(getattr(paths, "run_id", "-")), "plain"),
776
+ ("goal", goal, "plain"),
777
+ ("summary", summary, "plain"),
778
+ (
779
+ "tasks",
780
+ f"{task_count} total · {done} done · {failed} failed · {remaining} remaining",
781
+ "accent" if (task_count and failed == 0) else "plain",
782
+ ),
783
+ ("assets", str(asset_count), "plain"),
784
+ ]
785
+ sections: list[tuple[str, list[tuple[str, str, str]]]] = [("Plan", overview)]
786
+
787
+ requirements_obj = plan.get("requirements") or []
788
+ requirements = requirements_obj if isinstance(requirements_obj, list) else []
789
+ if requirements:
790
+ req_rows: list[tuple[str, str, str]] = []
791
+ for idx, req in enumerate(requirements[:12], start=1):
792
+ text = " ".join(str(requirement_text(req) or "").split())
793
+ if not text:
794
+ continue
795
+ req_rows.append((str(idx), text, "plain"))
796
+ if len(requirements) > 12:
797
+ req_rows.append(("…", f"({len(requirements) - 12} more)", "dim"))
798
+ if req_rows:
799
+ sections.append((f"Requirements ({len(requirements)})", req_rows))
800
+
801
+ if tasks:
802
+ task_rows: list[tuple[str, str, str]] = []
803
+ for task in tasks:
804
+ if not isinstance(task, dict):
805
+ continue
806
+ tid = str(task.get("id") or "-")
807
+ raw_status = str(task.get("status") or "planned")
808
+ canonical = canonical_task_status(raw_status)
809
+ tone = _forge_status_tone(canonical)
810
+ # Prefix the SAME glyph the live swarm table uses (✓/✗/○/·) so a task
811
+ # looks identical here and while it executes.
812
+ glyph = forge_status_glyph(canonical)
813
+ title = str(task.get("title") or "-")
814
+ value = f"{glyph} {raw_status} · {title}"
815
+ extra: list[str] = []
816
+ deps = task.get("dependencies") or []
817
+ if isinstance(deps, list) and deps:
818
+ extra.append("deps: " + ", ".join(str(dep) for dep in deps))
819
+ try:
820
+ mcp = _forge_task_mcp_summary_label(task)
821
+ except Exception: # noqa: BLE001
822
+ mcp = "off"
823
+ if mcp and mcp != "off":
824
+ extra.append(f"mcp: {mcp}")
825
+ if extra:
826
+ value += " · " + " · ".join(extra)
827
+ task_rows.append((tid, value, tone))
828
+ sections.append((f"Tasks ({task_count})", task_rows))
829
+ else:
830
+ sections.append(("Tasks", [("-", "(no tasks yet)", "dim")]))
831
+
832
+ return {
833
+ "title": f"Forge Plan · {getattr(paths, 'run_id', '')}".rstrip(" ·"),
834
+ "sections": sections,
835
+ "hint": "/execute plan to run · /plan markdown for PLAN.md · Esc close",
836
+ "accent": "forge",
837
+ }
838
+
839
+
840
+ def _chat_forge_markdown_panel_spec(*, paths: Any, plan: dict[str, Any]) -> PanelSpec:
841
+ """PLAN.md preview as a scrollable doc panel — replaces the classic pager.
842
+
843
+ Saves the in-memory plan first (so PLAN.md reflects edits, like the classic
844
+ ``_show_forge_plan_markdown``), reads PLAN.md, and returns a ``body`` spec the
845
+ TUI renders through its markdown renderer in the centered popup.
846
+ """
847
+ from ...forge import save_plan
848
+
849
+ try:
850
+ save_plan(paths, plan)
851
+ except Exception: # noqa: BLE001 - still try to show whatever is on disk
852
+ pass
853
+ plan_md_path = getattr(paths, "plan_md_path", None)
854
+ body = ""
855
+ if plan_md_path is not None:
856
+ try:
857
+ body = plan_md_path.read_text(encoding="utf-8")
858
+ except Exception as exc: # noqa: BLE001
859
+ return {
860
+ "title": "PLAN.md",
861
+ "sections": [("PLAN.md", [("error", f"Failed to read PLAN.md: {exc}", "err")])],
862
+ }
863
+ if not body.strip():
864
+ body = "(PLAN.md is empty)"
865
+ return {
866
+ "title": f"PLAN.md · {getattr(paths, 'run_id', '')}".rstrip(" ·"),
867
+ "body": body,
868
+ "hint": "↑↓/PgUp/PgDn scroll · Esc close",
869
+ "accent": "forge",
870
+ }
871
+
872
+
873
+ # ---------------------------------------------------------------- Forge assets
874
+ def _format_asset_size(size_bytes: int) -> str:
875
+ """Human-readable byte size (mirror of assets_modal._format_size)."""
876
+ size = float(size_bytes)
877
+ for unit in ("B", "KB", "MB", "GB"):
878
+ if size < 1024 or unit == "GB":
879
+ return f"{size:.0f} {unit}" if unit == "B" else f"{size:.1f} {unit}"
880
+ size /= 1024
881
+ return f"{size_bytes} B"
882
+
883
+
884
+ def _asset_status_tone(status: str) -> str:
885
+ s = str(status or "").strip().lower()
886
+ if s == "ready":
887
+ return "accent"
888
+ if s == "failed":
889
+ return "err"
890
+ if s in {"pending", "running"}:
891
+ return "warn"
892
+ return "plain"
893
+
894
+
895
+ def _chat_assets_picker_spec(*, cfg: Any, paths: Any) -> dict[str, Any] | None:
896
+ """Selectable list of Forge assets — replaces the dead stdin assets modal.
897
+
898
+ Returns ``None`` (so the caller falls through) when the asset surface can't be
899
+ built or there are no assets to pick. Choosing a row submits ``/assets <id>``
900
+ to open the detail panel."""
901
+ from ...assets.surface import build_asset_surface
902
+ from ...forge import load_plan
903
+
904
+ try:
905
+ load_plan(paths)
906
+ except Exception: # noqa: BLE001 - listing still works without a loaded plan
907
+ pass
908
+ try:
909
+ surface = build_asset_surface(cfg=cfg, run_paths=paths)
910
+ entries = surface.list_assets()
911
+ except Exception: # noqa: BLE001
912
+ return None
913
+ if not entries:
914
+ return None
915
+ rows: list[dict[str, Any]] = []
916
+ for entry in entries:
917
+ rec = entry.record
918
+ kind = "img" if str(getattr(rec, "kind", "")) == "image" else "txt"
919
+ size = _format_asset_size(int(getattr(rec, "size_bytes", 0) or 0))
920
+ status = str(getattr(entry, "comprehension_status", "") or "")
921
+ pin = "★ " if getattr(rec, "pinned", False) else ""
922
+ rows.append(
923
+ {
924
+ "label": str(rec.id),
925
+ "description": f"{pin}{rec.title} · {kind} · {status} · {size}",
926
+ "value": str(rec.id),
927
+ "current": False,
928
+ }
929
+ )
930
+ return {
931
+ "title": f"Assets ({len(rows)})",
932
+ "rows": rows,
933
+ "on_select": lambda value: {"submit": f"/assets {value}"},
934
+ "hint": "↑↓ select · Enter to view · Esc cancel",
935
+ }
936
+
937
+
938
+ def _chat_asset_detail_panel_spec(*, cfg: Any, paths: Any, asset_id: str) -> PanelSpec:
939
+ """One asset's metadata + comprehension summary as a panel (replaces the
940
+ interactive modal's detail view)."""
941
+ from ...assets import AssetError
942
+ from ...assets.surface import build_asset_surface
943
+
944
+ try:
945
+ surface = build_asset_surface(cfg=cfg, run_paths=paths)
946
+ detail = surface.show_asset(asset_id)
947
+ except AssetError as exc:
948
+ return {
949
+ "title": f"Asset · {asset_id}",
950
+ "sections": [("Asset", [("error", str(exc), "err")])],
951
+ }
952
+ except Exception as exc: # noqa: BLE001
953
+ return {"title": "Asset", "sections": [("Asset", [("error", str(exc), "err")])]}
954
+
955
+ rec = detail.record
956
+ deleted = getattr(rec, "deleted_at", None) is not None
957
+ status = "deleted" if deleted else str(getattr(detail, "comprehension_status", "") or "")
958
+ overview: list[tuple[str, str, str]] = [
959
+ ("id", str(rec.id), "accent"),
960
+ ("title", str(rec.title), "plain"),
961
+ ("kind", str(getattr(rec, "kind", "-")), "plain"),
962
+ ("size", _format_asset_size(int(getattr(rec, "size_bytes", 0) or 0)), "plain"),
963
+ ("mime", str(getattr(rec, "mime", "-")), "plain"),
964
+ (
965
+ "pinned",
966
+ "yes" if getattr(rec, "pinned", False) else "no",
967
+ "accent" if getattr(rec, "pinned", False) else "plain",
968
+ ),
969
+ ("status", status, "err" if deleted else _asset_status_tone(status)),
970
+ ("file", str(getattr(rec, "original_filename", "-")), "dim"),
971
+ ]
972
+ sections: list[tuple[str, list[tuple[str, str, str]]]] = [("Asset", overview)]
973
+ description = str(getattr(rec, "description", "") or "").strip()
974
+ if description:
975
+ sections.append(("Description", [("text", description, "plain")]))
976
+ comp = getattr(detail, "comprehension", None)
977
+ if comp is not None:
978
+ data = getattr(comp, "data", None)
979
+ comp_rows: list[tuple[str, str, str]] = [
980
+ ("summary", str(getattr(data, "semantic_summary", "") or "-"), "plain"),
981
+ ("language", str(getattr(comp, "detected_language", "") or "-"), "dim"),
982
+ ("source", str(getattr(comp, "source", "") or "-"), "dim"),
983
+ ]
984
+ sections.append(("Comprehension", comp_rows))
985
+ facts = list(getattr(data, "stated_facts", None) or [])
986
+ if facts:
987
+ sections.append(
988
+ (
989
+ "Stated facts",
990
+ [(str(i + 1), str(fact), "plain") for i, fact in enumerate(facts[:8])],
991
+ )
992
+ )
993
+ return {
994
+ "title": f"Asset · {rec.id}",
995
+ "sections": sections,
996
+ "hint": "Esc close",
997
+ "accent": "forge",
998
+ }
999
+
1000
+
1001
+ __all__ = [
1002
+ "_chat_usage_panel_spec",
1003
+ "_chat_context_panel_spec",
1004
+ "_chat_model_info_panel_spec",
1005
+ "_chat_config_panel_spec",
1006
+ "_chat_toolbar_panel_spec",
1007
+ "_chat_terminals_panel_spec",
1008
+ "_chat_skill_listing_panel_spec",
1009
+ "_short_subagent_desc",
1010
+ "_forge_status_tone",
1011
+ "_chat_forge_intro_panel_spec",
1012
+ "_chat_forge_plan_panel_spec",
1013
+ "_chat_forge_markdown_panel_spec",
1014
+ "_format_asset_size",
1015
+ "_asset_status_tone",
1016
+ "_chat_assets_picker_spec",
1017
+ "_chat_asset_detail_panel_spec",
1018
+ ]