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,566 @@
1
+ """In-app overlay that hosts the TUI configuration menu (:class:`ConfigFlow`).
2
+
3
+ Unlike the setup wizard (:mod:`cli_impl.tui.setup_app`), which is a standalone
4
+ :class:`~prompt_toolkit.application.Application` run before chat, ``/config`` must
5
+ open *while the chat application is already running* — the transcript, session, and
6
+ scroll position all need to survive. So this is not a second ``Application`` but a
7
+ full-screen :class:`~prompt_toolkit.layout.containers.Float` added to the chat app's
8
+ ``FloatContainer``, plus key bindings registered on the chat app's
9
+ ``KeyBindings``. It reuses the chat/setup render grammar (the picker renderer, the
10
+ ``setup.*`` style classes, the cursor-pin scrollable body) so it looks identical to
11
+ the rest of the TUI.
12
+
13
+ The host (:mod:`cli_impl.tui.app`) wires it up:
14
+
15
+ * appends :attr:`ConfigOverlay.float` to the ``FloatContainer`` floats,
16
+ * calls :meth:`ConfigOverlay.register` to add the overlay's key bindings,
17
+ * gates its own global bindings with ``& ~overlay.open_condition`` so they don't
18
+ fire underneath the overlay,
19
+ * routes bare ``/config`` to :meth:`ConfigOverlay.open` and Ctrl+C to
20
+ :meth:`ConfigOverlay.request_cancel` while open.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import threading
26
+ from collections.abc import Callable
27
+ from typing import Any
28
+
29
+ from prompt_toolkit.application.current import get_app
30
+ from prompt_toolkit.data_structures import Point
31
+ from prompt_toolkit.filters import Condition
32
+ from prompt_toolkit.formatted_text import FormattedText
33
+ from prompt_toolkit.key_binding import KeyBindings
34
+ from prompt_toolkit.layout.containers import (
35
+ ConditionalContainer,
36
+ Float,
37
+ HSplit,
38
+ ScrollOffsets,
39
+ Window,
40
+ )
41
+ from prompt_toolkit.layout.controls import FormattedTextControl
42
+ from prompt_toolkit.layout.margins import ScrollbarMargin
43
+ from prompt_toolkit.layout.processors import (
44
+ BeforeInput,
45
+ ConditionalProcessor,
46
+ PasswordProcessor,
47
+ )
48
+ from prompt_toolkit.widgets import Frame, TextArea
49
+
50
+ from .app import (
51
+ _SPINNER_FRAMES,
52
+ _render_picker_rows,
53
+ _transcript_content_width_for,
54
+ _wrap_line,
55
+ )
56
+ from .setup_app import _TONE_STYLE, _row_to_dict
57
+
58
+ # Opaque dark background so the full-screen float fully covers the chat behind it.
59
+ _BG = "bg:#0d1117"
60
+
61
+
62
+ class ConfigOverlay:
63
+ """A full-screen ``/config`` overlay driven by a :class:`ConfigFlow`."""
64
+
65
+ def __init__(
66
+ self,
67
+ *,
68
+ flow_factory: Callable[[], Any],
69
+ on_saved: Callable[[int], None] | None = None,
70
+ on_error: Callable[[str], None] | None = None,
71
+ on_switch_workspace: Callable[[str], None] | None = None,
72
+ focus_chat: Callable[[], None] | None = None,
73
+ ) -> None:
74
+ self._flow_factory = flow_factory
75
+ self._on_saved = on_saved
76
+ self._on_error = on_error
77
+ self._on_switch = on_switch_workspace
78
+ self._focus_chat = focus_chat
79
+ self.flow: Any | None = None
80
+ self._open = {"on": False}
81
+ self._busy = {"running": False}
82
+ self._spinner = {"i": 0}
83
+ self._body_cursor = {"row": 0}
84
+ self._last_field: dict[str, Any] = {"key": None}
85
+
86
+ self.open_condition = Condition(lambda: self._open["on"])
87
+
88
+ # ----------------------------------------------------------- windows
89
+ self._body_window = Window(
90
+ FormattedTextControl(
91
+ self._body_fragments,
92
+ focusable=True,
93
+ get_cursor_position=lambda: Point(0, self._body_cursor["row"]),
94
+ ),
95
+ wrap_lines=True,
96
+ scroll_offsets=ScrollOffsets(top=1, bottom=1),
97
+ right_margins=[ScrollbarMargin(display_arrows=False)],
98
+ style=_BG,
99
+ )
100
+ self._status_window = Window(
101
+ FormattedTextControl(self._status_fragments, focusable=False),
102
+ height=1,
103
+ style=_BG,
104
+ )
105
+ # Mask the input whenever the active input screen asks for it.
106
+ _is_password = Condition(
107
+ lambda: (
108
+ self._open["on"]
109
+ and self.flow is not None
110
+ and self.flow.current_mode() == "input"
111
+ and bool(getattr(self.flow.screen(), "input_password", False))
112
+ )
113
+ )
114
+ self._input = TextArea(
115
+ height=1,
116
+ multiline=False,
117
+ wrap_lines=False,
118
+ style="class:tui.input",
119
+ input_processors=[
120
+ ConditionalProcessor(PasswordProcessor(), filter=_is_password),
121
+ BeforeInput("> ", style="class:tui.prompt"),
122
+ ],
123
+ )
124
+ self._input_frame = Frame(self._input)
125
+
126
+ _is_input = Condition(lambda: self._open["on"] and self._mode() == "input")
127
+
128
+ body = HSplit(
129
+ [
130
+ Window(
131
+ FormattedTextControl(self._header_fragments, focusable=False),
132
+ height=1,
133
+ style=_BG,
134
+ ),
135
+ Window(height=1, style=_BG),
136
+ self._body_window,
137
+ self._status_window,
138
+ ConditionalContainer(self._input_frame, filter=_is_input),
139
+ Window(height=1, style=_BG),
140
+ Window(
141
+ FormattedTextControl(self._footer_fragments, focusable=False),
142
+ height=1,
143
+ style=_BG,
144
+ ),
145
+ ],
146
+ style=_BG,
147
+ )
148
+ self.float = Float(
149
+ content=ConditionalContainer(body, filter=self.open_condition),
150
+ left=0,
151
+ right=0,
152
+ top=0,
153
+ bottom=0,
154
+ )
155
+
156
+ # ---------------------------------------------------------------- helpers
157
+
158
+ def is_open(self) -> bool:
159
+ return self._open["on"]
160
+
161
+ def is_busy(self) -> bool:
162
+ return self._open["on"] and self.flow is not None and self.flow.current_mode() == "busy"
163
+
164
+ def tick_spinner(self) -> None:
165
+ """Advance the saving spinner one frame (driven by the app's repaint loop)."""
166
+ self._spinner["i"] = (self._spinner["i"] + 1) % len(_SPINNER_FRAMES)
167
+
168
+ def _mode(self) -> str:
169
+ return self.flow.current_mode() if self.flow is not None else "message"
170
+
171
+ def _invalidate(self) -> None:
172
+ try:
173
+ get_app().invalidate()
174
+ except Exception:
175
+ pass
176
+
177
+ def _width(self) -> int:
178
+ # Derived from the terminal, not read back from render_info: the overlay
179
+ # body is a full-screen float (no frame) carrying one scrollbar margin, so
180
+ # it is exactly one column narrower than the screen. render_info is a frame
181
+ # BEHIND — after a resize it still reports the old, wider size, and the
182
+ # rows built from it overflow and hard-wrap into the new, narrower window.
183
+ try:
184
+ cols = get_app().output.get_size().columns
185
+ except Exception:
186
+ return 80
187
+ return max(20, _transcript_content_width_for(cols))
188
+
189
+ # ------------------------------------------------------------- rendering
190
+
191
+ def _header_fragments(self) -> FormattedText:
192
+ return FormattedText(
193
+ [("class:setup.header", "◇ alysis"), ("class:setup.headerdim", " · config")]
194
+ )
195
+
196
+ def _body_fragments(self) -> FormattedText:
197
+ flow = self.flow
198
+ if flow is None:
199
+ return FormattedText([("", "")])
200
+ scr = flow.screen()
201
+ width = self._width()
202
+ rows: list[list[tuple[str, str]]] = []
203
+ if scr.title:
204
+ for line in _wrap_line(scr.title, width):
205
+ rows.append([("class:setup.title", line)])
206
+ if scr.subtitle:
207
+ for line in _wrap_line(scr.subtitle, width):
208
+ rows.append([("class:setup.subtitle", line)])
209
+ rows.append([("", "")])
210
+ header_lines = len(rows)
211
+ sel_line = header_lines
212
+
213
+ if scr.mode == "list" and scr.rows:
214
+ picker = _render_picker_rows(
215
+ [_row_to_dict(r) for r in scr.rows],
216
+ scr.index,
217
+ width,
218
+ hint=scr.hint,
219
+ numbered=getattr(scr, "numbered", True),
220
+ )
221
+ located = next(
222
+ (i for i, prow in enumerate(picker) if any("selcaret" in s for s, _ in prow)),
223
+ 0,
224
+ )
225
+ sel_line = header_lines + located
226
+ rows.extend(picker)
227
+ elif scr.mode == "confirm":
228
+ for text, tone in scr.lines:
229
+ for line in _wrap_line(text, width):
230
+ rows.append([(_TONE_STYLE.get(tone, "class:setup.text"), line)])
231
+ rows.append([("", "")])
232
+ if scr.hint:
233
+ rows.append([("class:setup.dim", scr.hint)])
234
+ elif scr.mode == "input":
235
+ if scr.input_label:
236
+ for line in _wrap_line(scr.input_label + ":", width):
237
+ rows.append([("class:setup.text", line)])
238
+ if scr.input_default and not scr.input_password:
239
+ for line in _wrap_line(f"current: {scr.input_default}", width):
240
+ rows.append([("class:setup.dim", line)])
241
+ # No in-body hint: the footer is the single key legend and shows
242
+ # this screen's action verb (see _footer_fragments).
243
+ elif scr.mode == "busy":
244
+ frame = _SPINNER_FRAMES[self._spinner["i"] % len(_SPINNER_FRAMES)]
245
+ rows.append(
246
+ [
247
+ ("class:setup.spinner", f"{frame} "),
248
+ ("class:setup.text", scr.busy_label or "Working…"),
249
+ ]
250
+ )
251
+
252
+ self._body_cursor["row"] = sel_line
253
+ out: list[tuple[str, str]] = []
254
+ for row in rows:
255
+ out.extend(row)
256
+ out.append(("", "\n"))
257
+ return FormattedText(out)
258
+
259
+ def _status_fragments(self) -> FormattedText:
260
+ flow = self.flow
261
+ if flow is None:
262
+ return FormattedText([("", "")])
263
+ scr = flow.screen()
264
+ if scr.mode == "busy" or not scr.status:
265
+ return FormattedText([("", "")])
266
+ return FormattedText([(_TONE_STYLE.get(scr.status_tone, "class:setup.dim"), scr.status)])
267
+
268
+ def _footer_fragments(self) -> FormattedText:
269
+ flow = self.flow
270
+ try:
271
+ width = get_app().output.get_size().columns
272
+ except Exception:
273
+ width = 80
274
+ crumb = flow._breadcrumb() if flow is not None else "configuration"
275
+ left = [
276
+ ("class:setup.footer.brand", "◇ alysis"),
277
+ ("class:setup.footer.dim", f" · {crumb}"),
278
+ ]
279
+ is_menu = flow is not None and getattr(flow, "stage", "") == "menu"
280
+ mode = self._mode()
281
+ if mode == "list":
282
+ if is_menu:
283
+ right_text = "↑↓ move · Enter open · s save · Esc close"
284
+ else:
285
+ # Advertise digit shortcuts only on numbered lists; the footer is
286
+ # the single key legend now that in-body nav hints are gone.
287
+ numbered = True
288
+ if flow is not None:
289
+ try:
290
+ numbered = bool(getattr(flow.screen(), "numbered", True))
291
+ except Exception:
292
+ numbered = True
293
+ right_text = (
294
+ "↑↓ move · 1-9 pick · Enter select · Esc back"
295
+ if numbered
296
+ else "↑↓ move · Enter select · Esc back"
297
+ )
298
+ elif mode == "input":
299
+ # Honour the screen's own action verb ("Enter save", "Enter add",
300
+ # …) so the legend never contradicts what Enter actually does.
301
+ screen_hint = ""
302
+ if flow is not None:
303
+ try:
304
+ screen_hint = str(getattr(flow.screen(), "hint", "") or "")
305
+ except Exception:
306
+ screen_hint = ""
307
+ right_text = (
308
+ f"{screen_hint} · Ctrl+C cancel"
309
+ if screen_hint
310
+ else "Enter next · Esc back · Ctrl+C cancel"
311
+ )
312
+ elif mode == "confirm":
313
+ right_text = "Y / N · Esc back"
314
+ elif mode == "busy":
315
+ right_text = "Working…"
316
+ else:
317
+ right_text = "Ctrl+C cancel"
318
+ right = [("class:setup.footer.dim", right_text)]
319
+ left_len = sum(len(t) for _s, t in left)
320
+ right_len = sum(len(t) for _s, t in right)
321
+ gap = max(1, width - left_len - right_len)
322
+ return FormattedText([*left, ("", " " * gap), *right])
323
+
324
+ # ------------------------------------------------------------- lifecycle
325
+
326
+ def open(self, flow_factory: Callable[[], Any] | None = None) -> None:
327
+ if self._open["on"]:
328
+ return
329
+ try:
330
+ self.flow = (flow_factory or self._flow_factory)()
331
+ except Exception as exc: # noqa: BLE001 - surface, don't crash the chat UI
332
+ if self._on_error is not None:
333
+ try:
334
+ self._on_error(f"configuration could not open: {exc}")
335
+ except Exception:
336
+ pass
337
+ self.flow = None
338
+ return
339
+ self._open["on"] = True
340
+ self._busy["running"] = False
341
+ self._spinner["i"] = 0
342
+ self._last_field["key"] = None
343
+ self._pump()
344
+
345
+ def request_cancel(self) -> None:
346
+ if not self._open["on"] or self.flow is None:
347
+ return
348
+ if self.flow.current_mode() == "busy":
349
+ return
350
+ self.flow.request_cancel()
351
+ self._pump()
352
+
353
+ def _close(self, *, saved: bool) -> None:
354
+ on_saved = self._on_saved
355
+ on_switch = self._on_switch
356
+ flow = self.flow
357
+ count = int(getattr(flow, "changes_count", 0) or 0) if flow is not None else 0
358
+ switch_target = getattr(flow, "switch_workspace", None) if flow is not None else None
359
+ self._open["on"] = False
360
+ self.flow = None
361
+ self._busy["running"] = False
362
+ self._last_field["key"] = None
363
+ try:
364
+ self._input.text = ""
365
+ except Exception:
366
+ pass
367
+ self._restore_focus()
368
+ self._invalidate()
369
+ # "Switch now" takes priority: hand the chosen folder to the host to relaunch
370
+ # the chat there (no "saved N changes" note — we're leaving this session).
371
+ if switch_target and on_switch is not None:
372
+ try:
373
+ on_switch(str(switch_target))
374
+ except Exception:
375
+ pass
376
+ return
377
+ if saved and on_saved is not None:
378
+ try:
379
+ on_saved(count)
380
+ except Exception:
381
+ pass
382
+
383
+ # -------------------------------------------------------------- focus
384
+
385
+ def _reconcile_focus(self) -> None:
386
+ if not self._open["on"]:
387
+ return
388
+ mode = self._mode()
389
+ target = self._input.window if mode == "input" else self._body_window
390
+ try:
391
+ app = get_app()
392
+ if app.layout.current_window is not target:
393
+ app.layout.focus(target)
394
+ except Exception:
395
+ pass
396
+
397
+ def _restore_focus(self) -> None:
398
+ if self._focus_chat is not None:
399
+ try:
400
+ self._focus_chat()
401
+ except Exception:
402
+ pass
403
+
404
+ # --------------------------------------------------------------- pump
405
+
406
+ def _pump(self) -> None:
407
+ flow = self.flow
408
+ if flow is None:
409
+ return
410
+ if flow.stage == "done":
411
+ self._close(saved=bool(flow.saved))
412
+ return
413
+ mode = flow.current_mode()
414
+ key = flow.field_key()
415
+ if key != self._last_field["key"]:
416
+ # Seed the input box on field change (UI thread only — the save worker
417
+ # never lands on an input stage, so this is race-free).
418
+ if mode == "input":
419
+ scr = flow.screen()
420
+ self._input.text = "" if scr.input_password else (scr.input_default or "")
421
+ self._input.buffer.cursor_position = len(self._input.text)
422
+ else:
423
+ self._input.text = ""
424
+ self._last_field["key"] = key
425
+ self._reconcile_focus()
426
+ if mode == "busy" and not self._busy["running"]:
427
+ self._busy["running"] = True
428
+ threading.Thread(target=self._busy_worker, daemon=True).start()
429
+ self._invalidate()
430
+
431
+ def _busy_worker(self) -> None:
432
+ # Run ONLY the blocking save I/O here; it records its outcome on the flow but
433
+ # writes no renderer-visible stage/status. The stage transition + close/reload
434
+ # are then applied on the UI thread (``_finalize_save``), so the render
435
+ # callbacks never read flow state written from this worker.
436
+ flow = self.flow
437
+ if flow is not None:
438
+ try:
439
+ perform_busy = getattr(flow, "perform_busy", None)
440
+ if callable(perform_busy):
441
+ perform_busy()
442
+ else:
443
+ flow.perform_save()
444
+ except BaseException as exc: # noqa: BLE001 - record, never hang the UI
445
+ set_busy_failure = getattr(flow, "set_busy_failure", None)
446
+ if callable(set_busy_failure):
447
+ set_busy_failure(f"{type(exc).__name__}: {exc}")
448
+ else:
449
+ flow.set_save_failure(f"{type(exc).__name__}: {exc}")
450
+ self._invalidate()
451
+ try:
452
+ loop = getattr(get_app(), "loop", None)
453
+ if loop is not None:
454
+ loop.call_soon_threadsafe(self._finalize_save)
455
+ else:
456
+ self._finalize_save()
457
+ except Exception:
458
+ self._finalize_save()
459
+
460
+ def _finalize_save(self) -> None:
461
+ flow = self.flow
462
+ if flow is not None:
463
+ apply_busy_outcome = getattr(flow, "apply_busy_outcome", None)
464
+ if callable(apply_busy_outcome):
465
+ apply_busy_outcome()
466
+ else:
467
+ flow.apply_save_outcome()
468
+ self._busy["running"] = False
469
+ self._pump()
470
+
471
+ # ----------------------------------------------------------- key handling
472
+
473
+ def _on_enter(self) -> None:
474
+ flow = self.flow
475
+ if flow is None:
476
+ return
477
+ mode = flow.current_mode()
478
+ if mode == "input":
479
+ flow.submit_input(self._input.text)
480
+ elif mode == "list":
481
+ flow.choose_current()
482
+ elif mode == "confirm":
483
+ flow.confirm(flow.screen().confirm_default)
484
+ self._pump()
485
+
486
+ def register(self, kb: KeyBindings) -> None:
487
+ is_open = self.open_condition
488
+ is_list = Condition(lambda: self._open["on"] and self._mode() == "list")
489
+ is_confirm = Condition(lambda: self._open["on"] and self._mode() == "confirm")
490
+ is_busy = Condition(lambda: self._open["on"] and self._mode() == "busy")
491
+ is_menu = Condition(
492
+ lambda: (
493
+ self._open["on"]
494
+ and self.flow is not None
495
+ and getattr(self.flow, "stage", "") == "menu"
496
+ )
497
+ )
498
+
499
+ @kb.add("enter", filter=is_open & ~is_busy, eager=True)
500
+ def _enter(event: Any) -> None:
501
+ self._on_enter()
502
+
503
+ @kb.add("escape", filter=is_open & ~is_busy)
504
+ def _escape(event: Any) -> None:
505
+ if self.flow is not None:
506
+ self.flow.back()
507
+ self._pump()
508
+
509
+ @kb.add("up", filter=is_list, eager=True)
510
+ @kb.add("k", filter=is_list, eager=True)
511
+ @kb.add("K", filter=is_list, eager=True)
512
+ def _up(event: Any) -> None:
513
+ if self.flow is not None:
514
+ self.flow.move(-1)
515
+ self._invalidate()
516
+
517
+ @kb.add("down", filter=is_list, eager=True)
518
+ @kb.add("j", filter=is_list, eager=True)
519
+ @kb.add("J", filter=is_list, eager=True)
520
+ def _down(event: Any) -> None:
521
+ if self.flow is not None:
522
+ self.flow.move(1)
523
+ self._invalidate()
524
+
525
+ for _digit in "123456789":
526
+
527
+ @kb.add(_digit, filter=is_list, eager=True)
528
+ def _pick(event: Any) -> None:
529
+ if self.flow is None:
530
+ return
531
+ try:
532
+ self.flow.choose_index(int(event.data) - 1)
533
+ except (TypeError, ValueError):
534
+ return
535
+ self._pump()
536
+
537
+ @kb.add("q", filter=(is_list | is_confirm), eager=True)
538
+ @kb.add("Q", filter=(is_list | is_confirm), eager=True)
539
+ def _q(event: Any) -> None:
540
+ if self.flow is not None:
541
+ self.flow.back()
542
+ self._pump()
543
+
544
+ @kb.add("s", filter=is_menu, eager=True)
545
+ @kb.add("S", filter=is_menu, eager=True)
546
+ def _save(event: Any) -> None:
547
+ if self.flow is not None:
548
+ self.flow._goto("saving")
549
+ self._pump()
550
+
551
+ @kb.add("y", filter=is_confirm, eager=True)
552
+ @kb.add("Y", filter=is_confirm, eager=True)
553
+ def _yes(event: Any) -> None:
554
+ if self.flow is not None:
555
+ self.flow.confirm(True)
556
+ self._pump()
557
+
558
+ @kb.add("n", filter=is_confirm, eager=True)
559
+ @kb.add("N", filter=is_confirm, eager=True)
560
+ def _no(event: Any) -> None:
561
+ if self.flow is not None:
562
+ self.flow.confirm(False)
563
+ self._pump()
564
+
565
+
566
+ __all__ = ["ConfigOverlay"]
@@ -0,0 +1,78 @@
1
+ """Static text + the model-name prettifier for the TUI welcome/footer.
2
+
3
+ Kept as pure functions so they can be unit-tested without constructing the
4
+ prompt_toolkit application.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+
11
+ from prompt_toolkit.formatted_text import FormattedText
12
+
13
+ HEADING_TEXT = "Alysis Code" # plain wordmark fallback (narrow terminals)
14
+ CREDIT_TEXT = "crafted by AlysisAI"
15
+ HINT_TEXT = "/forge for an autonomous run · tab to switch persona · /help for everything"
16
+ # Welcome screen: full greeting. Once the conversation is underway the input
17
+ # switches to the shorter follow-up placeholder below.
18
+ INPUT_PLACEHOLDER = "I'm Alysis Code, your coding buddy — how can I help you?"
19
+ INPUT_PLACEHOLDER_FOLLOWUP = "Message Alysis Code…"
20
+ # Forge planning session: nudge the forge verbs (and that plain text becomes a
21
+ # requirement) so the input reads as the plan editor it now is.
22
+ INPUT_PLACEHOLDER_FORGE = "Forge — /goal · /task · /show · /execute plan · /done"
23
+
24
+ # Tokens that should keep a specific casing instead of naive title-casing.
25
+ _ACRONYMS = {
26
+ "ai": "AI",
27
+ "deepseek": "DeepSeek",
28
+ "glm": "GLM",
29
+ "gpt": "GPT",
30
+ "llm": "LLM",
31
+ "mimo": "MiMo",
32
+ "openai": "OpenAI",
33
+ "qwen": "Qwen",
34
+ "xai": "xAI",
35
+ }
36
+
37
+
38
+ def pretty_model_label(model: str | None) -> str:
39
+ """Turn a raw model id into a friendly footer label.
40
+
41
+ ``deepseek-chat`` -> ``DeepSeek Chat``; ``gpt-4o`` -> ``GPT 4o``. Falls back
42
+ to the raw id when there is nothing sensible to do.
43
+ """
44
+ raw = (model or "").strip()
45
+ if not raw:
46
+ return "model"
47
+ name = raw.rsplit("/", 1)[-1]
48
+ tokens = [t for t in re.split(r"[-_\s]+", name) if t]
49
+ out: list[str] = []
50
+ for token in tokens:
51
+ low = token.lower()
52
+ if low in _ACRONYMS:
53
+ out.append(_ACRONYMS[low])
54
+ elif token.isupper():
55
+ out.append(token)
56
+ else:
57
+ out.append(token[:1].upper() + token[1:])
58
+ return " ".join(out) or raw
59
+
60
+
61
+ def heading_fragments() -> FormattedText:
62
+ return FormattedText([("class:tui.heading", HEADING_TEXT)])
63
+
64
+
65
+ def hint_fragments() -> FormattedText:
66
+ return FormattedText([("class:tui.hint", HINT_TEXT)])
67
+
68
+
69
+ __all__ = [
70
+ "HEADING_TEXT",
71
+ "HINT_TEXT",
72
+ "INPUT_PLACEHOLDER",
73
+ "INPUT_PLACEHOLDER_FOLLOWUP",
74
+ "INPUT_PLACEHOLDER_FORGE",
75
+ "heading_fragments",
76
+ "hint_fragments",
77
+ "pretty_model_label",
78
+ ]