aria-code 0.167.0__py3-none-win_amd64.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 (601) hide show
  1. aria_bots/__init__.py +7 -0
  2. aria_bots/channels/README.md +97 -0
  3. aria_bots/channels/__init__.py +1 -0
  4. aria_bots/channels/approvals.py +263 -0
  5. aria_bots/channels/conversation.py +280 -0
  6. aria_bots/channels/digest.py +186 -0
  7. aria_bots/channels/registry.py +95 -0
  8. aria_bots/engine.py +144 -0
  9. aria_bots/feishu_bot.py +1385 -0
  10. aria_bots/logistics/__init__.py +1 -0
  11. aria_bots/logistics/carriers.py +253 -0
  12. aria_bots/logistics/freight.py +154 -0
  13. aria_bots/logistics/inventory.py +242 -0
  14. aria_bots/logistics/mcp_server.py +156 -0
  15. aria_bots/logistics/records.py +119 -0
  16. aria_bots/logistics/tenancy.py +152 -0
  17. aria_bots/market.py +91 -0
  18. aria_bots/relay_client.py +326 -0
  19. aria_bots/telegram_bot.py +299 -0
  20. aria_code/__init__.py +111 -0
  21. aria_code/_bin/aria-native.exe +0 -0
  22. aria_code/_bin/aria-runtime.exe +0 -0
  23. aria_code/_version.py +20 -0
  24. aria_code/account_memory.py +296 -0
  25. aria_code/account_session.py +166 -0
  26. aria_code/adk_apps/__init__.py +1 -0
  27. aria_code/adk_apps/aria_code_review/__init__.py +1 -0
  28. aria_code/adk_apps/aria_code_review/agent.py +32 -0
  29. aria_code/adk_apps/aria_research/__init__.py +1 -0
  30. aria_code/adk_apps/aria_research/agent.py +31 -0
  31. aria_code/agents/__init__.py +49 -0
  32. aria_code/agents/base.py +329 -0
  33. aria_code/agents/code_review.py +200 -0
  34. aria_code/agents/deep/__init__.py +37 -0
  35. aria_code/agents/deep/calibration_loop.py +144 -0
  36. aria_code/agents/deep/critic.py +125 -0
  37. aria_code/agents/deep/deepen.py +193 -0
  38. aria_code/agents/deep/models.py +149 -0
  39. aria_code/agents/deep/pipeline.py +164 -0
  40. aria_code/agents/deep/quant_fusion.py +192 -0
  41. aria_code/agents/deep/themes.py +95 -0
  42. aria_code/agents/deep/tiers.py +106 -0
  43. aria_code/agents/engineering/__init__.py +14 -0
  44. aria_code/agents/engineering/coder.py +222 -0
  45. aria_code/agents/engineering/tester.py +145 -0
  46. aria_code/agents/financial/__init__.py +21 -0
  47. aria_code/agents/financial/cashflow_burn.py +119 -0
  48. aria_code/agents/financial/catalyst.py +279 -0
  49. aria_code/agents/financial/corporate_finance.py +175 -0
  50. aria_code/agents/financial/debate.py +224 -0
  51. aria_code/agents/financial/earnings.py +315 -0
  52. aria_code/agents/financial/fundamental.py +178 -0
  53. aria_code/agents/financial/macro.py +111 -0
  54. aria_code/agents/financial/news.py +231 -0
  55. aria_code/agents/financial/risk.py +146 -0
  56. aria_code/agents/financial/sector.py +279 -0
  57. aria_code/agents/financial/strategist.py +121 -0
  58. aria_code/agents/financial/stripe_revenue.py +127 -0
  59. aria_code/agents/financial/synthesis.py +274 -0
  60. aria_code/agents/financial/technical.py +277 -0
  61. aria_code/agents/orchestrator.py +44 -0
  62. aria_code/agents/portfolio_agent.py +392 -0
  63. aria_code/agents/realty/__init__.py +62 -0
  64. aria_code/agents/realty/asset_diagnosis.py +152 -0
  65. aria_code/agents/realty/business_match.py +166 -0
  66. aria_code/agents/realty/cashflow_verify.py +209 -0
  67. aria_code/agents/realty/contract_rules.py +210 -0
  68. aria_code/agents/realty/energy_anomaly.py +189 -0
  69. aria_code/agents/realty/exit_settlement.py +208 -0
  70. aria_code/agents/realty/fulfillment_risk.py +206 -0
  71. aria_code/agents/realty/ops_optimize.py +160 -0
  72. aria_code/agents/realty/revenue_share.py +215 -0
  73. aria_code/agents/registry.py +168 -0
  74. aria_code/agents/signal_scheme.py +111 -0
  75. aria_code/agents/sports/__init__.py +0 -0
  76. aria_code/agents/sports/football_agent.py +197 -0
  77. aria_code/agents/supervisor.py +113 -0
  78. aria_code/agents/team.py +500 -0
  79. aria_code/agents/video/__init__.py +16 -0
  80. aria_code/agents/video/director.py +376 -0
  81. aria_code/agents/video/qa_agent.py +143 -0
  82. aria_code/agents/video/script_doctor.py +112 -0
  83. aria_code/agents/warehouse/__init__.py +25 -0
  84. aria_code/agents/warehouse/contracts.py +26 -0
  85. aria_code/agents/warehouse/dashboard.py +155 -0
  86. aria_code/agents/warehouse/fulfillment_leadtime.py +95 -0
  87. aria_code/agents/warehouse/inbound_exceptions.py +25 -0
  88. aria_code/agents/warehouse/inventory_health.py +24 -0
  89. aria_code/agents/warehouse/local_run.py +71 -0
  90. aria_code/agents/warehouse/local_snapshot.py +132 -0
  91. aria_code/agents/warehouse/logistics_cost.py +84 -0
  92. aria_code/agents/warehouse/logistics_sync.py +21 -0
  93. aria_code/agents/warehouse/navigation.schema.json +110 -0
  94. aria_code/agents/warehouse/task_agent.py +172 -0
  95. aria_code/agents/warehouse/workflow.py +31 -0
  96. aria_code/aliyun_data_client.py +4 -0
  97. aria_code/apps/README.md +12 -0
  98. aria_code/apps/__init__.py +2 -0
  99. aria_code/apps/channels/README.md +9 -0
  100. aria_code/apps/channels/__init__.py +17 -0
  101. aria_code/apps/channels/approvals.py +11 -0
  102. aria_code/apps/channels/conversation.py +11 -0
  103. aria_code/apps/channels/digest.py +11 -0
  104. aria_code/apps/channels/intake.py +92 -0
  105. aria_code/apps/channels/registry.py +11 -0
  106. aria_code/apps/channels/tradingview.py +94 -0
  107. aria_code/apps/cli/README.md +13 -0
  108. aria_code/apps/cli/__init__.py +2 -0
  109. aria_code/apps/cli/_optional.py +120 -0
  110. aria_code/apps/cli/app_server.py +656 -0
  111. aria_code/apps/cli/auto_update.py +125 -0
  112. aria_code/apps/cli/bootstrap.py +206 -0
  113. aria_code/apps/cli/broker_render.py +206 -0
  114. aria_code/apps/cli/chat_turn.py +1327 -0
  115. aria_code/apps/cli/cloud_models.py +231 -0
  116. aria_code/apps/cli/codegen_paths.py +29 -0
  117. aria_code/apps/cli/commands/__init__.py +34 -0
  118. aria_code/apps/cli/commands/_business_render.py +78 -0
  119. aria_code/apps/cli/commands/_ui.py +71 -0
  120. aria_code/apps/cli/commands/analysis_cmds.py +378 -0
  121. aria_code/apps/cli/commands/ashare_prediction_cmds.py +327 -0
  122. aria_code/apps/cli/commands/auth_cmds.py +215 -0
  123. aria_code/apps/cli/commands/backtest_cmds.py +2518 -0
  124. aria_code/apps/cli/commands/broker_cmds.py +1236 -0
  125. aria_code/apps/cli/commands/business_workflow_cmds.py +379 -0
  126. aria_code/apps/cli/commands/canvas_cmds.py +77 -0
  127. aria_code/apps/cli/commands/catalog.py +189 -0
  128. aria_code/apps/cli/commands/core_cmds.py +2748 -0
  129. aria_code/apps/cli/commands/data_cmds.py +521 -0
  130. aria_code/apps/cli/commands/diagnostic_cmds.py +285 -0
  131. aria_code/apps/cli/commands/diagnostic_ops_cmds.py +813 -0
  132. aria_code/apps/cli/commands/engineering_cmds.py +399 -0
  133. aria_code/apps/cli/commands/file_cmds.py +312 -0
  134. aria_code/apps/cli/commands/finance_service_cmds.py +60 -0
  135. aria_code/apps/cli/commands/fx_commodity_cmds.py +123 -0
  136. aria_code/apps/cli/commands/market.py +544 -0
  137. aria_code/apps/cli/commands/market_cmds.py +1419 -0
  138. aria_code/apps/cli/commands/market_context.py +432 -0
  139. aria_code/apps/cli/commands/market_render.py +7 -0
  140. aria_code/apps/cli/commands/model_cmds.py +2374 -0
  141. aria_code/apps/cli/commands/ops_cmds.py +721 -0
  142. aria_code/apps/cli/commands/orchestrator_cmds.py +181 -0
  143. aria_code/apps/cli/commands/pdf_export_cmds.py +125 -0
  144. aria_code/apps/cli/commands/portfolio_cmds.py +1316 -0
  145. aria_code/apps/cli/commands/report.py +476 -0
  146. aria_code/apps/cli/commands/scaffold_templates.py +617 -0
  147. aria_code/apps/cli/commands/session_cmds.py +236 -0
  148. aria_code/apps/cli/commands/session_ux_cmds.py +359 -0
  149. aria_code/apps/cli/commands/team.py +723 -0
  150. aria_code/apps/cli/commands/team_render.py +8 -0
  151. aria_code/apps/cli/commands/ui_cmds.py +401 -0
  152. aria_code/apps/cli/commands/warehouse_cmds.py +297 -0
  153. aria_code/apps/cli/commands/workflow_cmds.py +629 -0
  154. aria_code/apps/cli/commands/workspace_cmds.py +1543 -0
  155. aria_code/apps/cli/config_paths.py +73 -0
  156. aria_code/apps/cli/config_store.py +158 -0
  157. aria_code/apps/cli/constants.py +88 -0
  158. aria_code/apps/cli/context.py +34 -0
  159. aria_code/apps/cli/conversation_policy.py +53 -0
  160. aria_code/apps/cli/deterministic.py +200 -0
  161. aria_code/apps/cli/direct.py +48 -0
  162. aria_code/apps/cli/earnings_view.py +172 -0
  163. aria_code/apps/cli/endpoint_migration.py +64 -0
  164. aria_code/apps/cli/exec_events.py +255 -0
  165. aria_code/apps/cli/file_references.py +155 -0
  166. aria_code/apps/cli/finance_service_catalog.py +111 -0
  167. aria_code/apps/cli/football_reports.py +231 -0
  168. aria_code/apps/cli/github_app_auth.py +135 -0
  169. aria_code/apps/cli/google_login.py +239 -0
  170. aria_code/apps/cli/handlers/__init__.py +11 -0
  171. aria_code/apps/cli/handlers/broker_handlers.py +122 -0
  172. aria_code/apps/cli/handlers/chart_handlers.py +1314 -0
  173. aria_code/apps/cli/handlers/market_handlers.py +2570 -0
  174. aria_code/apps/cli/handlers/realty_handlers.py +114 -0
  175. aria_code/apps/cli/handlers/sports_handlers.py +22 -0
  176. aria_code/apps/cli/handlers/strategy_advice.py +82 -0
  177. aria_code/apps/cli/headless.py +352 -0
  178. aria_code/apps/cli/helpers.py +326 -0
  179. aria_code/apps/cli/hooks.py +180 -0
  180. aria_code/apps/cli/i18n.py +310 -0
  181. aria_code/apps/cli/intent.py +136 -0
  182. aria_code/apps/cli/intent_router.py +305 -0
  183. aria_code/apps/cli/intent_shadow.py +72 -0
  184. aria_code/apps/cli/intent_signals.py +347 -0
  185. aria_code/apps/cli/lifecycle_hooks.py +48 -0
  186. aria_code/apps/cli/local_tool_schemas.py +550 -0
  187. aria_code/apps/cli/main.py +39 -0
  188. aria_code/apps/cli/market_hours.py +100 -0
  189. aria_code/apps/cli/market_metadata.py +135 -0
  190. aria_code/apps/cli/market_universe.py +487 -0
  191. aria_code/apps/cli/message_processing.py +310 -0
  192. aria_code/apps/cli/mixin_binding.py +32 -0
  193. aria_code/apps/cli/model_catalog.py +652 -0
  194. aria_code/apps/cli/model_probe.py +169 -0
  195. aria_code/apps/cli/model_retirement.py +65 -0
  196. aria_code/apps/cli/native_branding.py +12 -0
  197. aria_code/apps/cli/native_bridge.py +118 -0
  198. aria_code/apps/cli/native_entry.py +58 -0
  199. aria_code/apps/cli/pdf_report.py +977 -0
  200. aria_code/apps/cli/plan_mode.py +139 -0
  201. aria_code/apps/cli/plotly_html.py +15 -0
  202. aria_code/apps/cli/prediction_feedback.py +202 -0
  203. aria_code/apps/cli/preflight.py +502 -0
  204. aria_code/apps/cli/project_aria.py +60 -0
  205. aria_code/apps/cli/prompt_assembly.py +78 -0
  206. aria_code/apps/cli/prompts/__init__.py +0 -0
  207. aria_code/apps/cli/prompts/coding.py +71 -0
  208. aria_code/apps/cli/prompts/select.py +193 -0
  209. aria_code/apps/cli/prompts/system_prompts.py +562 -0
  210. aria_code/apps/cli/prompts/ui.py +436 -0
  211. aria_code/apps/cli/provider_endpoints.py +237 -0
  212. aria_code/apps/cli/provider_keys.py +73 -0
  213. aria_code/apps/cli/providers/__init__.py +1 -0
  214. aria_code/apps/cli/providers/base.py +775 -0
  215. aria_code/apps/cli/providers/chat_routing.py +139 -0
  216. aria_code/apps/cli/providers/collaboration.py +98 -0
  217. aria_code/apps/cli/providers/llm/__init__.py +1 -0
  218. aria_code/apps/cli/providers/llm/offline_responses.py +68 -0
  219. aria_code/apps/cli/providers/llm/ollama_stream.py +1574 -0
  220. aria_code/apps/cli/providers/llm/response_cache.py +60 -0
  221. aria_code/apps/cli/providers/llm/sse_stream.py +298 -0
  222. aria_code/apps/cli/providers/runtime_bridge.py +586 -0
  223. aria_code/apps/cli/providers/vertexai_stream.py +592 -0
  224. aria_code/apps/cli/release_download.py +159 -0
  225. aria_code/apps/cli/review_runner.py +111 -0
  226. aria_code/apps/cli/runtime_consumer.py +769 -0
  227. aria_code/apps/cli/sandbox.py +32 -0
  228. aria_code/apps/cli/session_export.py +120 -0
  229. aria_code/apps/cli/session_jsonl.py +208 -0
  230. aria_code/apps/cli/session_refresh.py +123 -0
  231. aria_code/apps/cli/session_store.py +125 -0
  232. aria_code/apps/cli/skills_catalog.py +273 -0
  233. aria_code/apps/cli/task_isolation.py +211 -0
  234. aria_code/apps/cli/terminal_chrome.py +412 -0
  235. aria_code/apps/cli/todo_tracker.py +190 -0
  236. aria_code/apps/cli/tool_executor.py +932 -0
  237. aria_code/apps/cli/tool_registry.py +34 -0
  238. aria_code/apps/cli/tool_scope.py +113 -0
  239. aria_code/apps/cli/tools/__init__.py +40 -0
  240. aria_code/apps/cli/tools/context.py +46 -0
  241. aria_code/apps/cli/tools/file_tools.py +181 -0
  242. aria_code/apps/cli/tools/market_tools.py +626 -0
  243. aria_code/apps/cli/tools/notebook_tools.py +111 -0
  244. aria_code/apps/cli/tools/system_tools.py +656 -0
  245. aria_code/apps/cli/tools/write_tools.py +961 -0
  246. aria_code/apps/cli/tradingview_bridge.py +529 -0
  247. aria_code/apps/cli/transactions.py +269 -0
  248. aria_code/apps/cli/turn_planning.py +137 -0
  249. aria_code/apps/cli/update_check.py +257 -0
  250. aria_code/apps/cli/updater.py +373 -0
  251. aria_code/apps/cli/utils/__init__.py +0 -0
  252. aria_code/apps/cli/utils/market_detect.py +1663 -0
  253. aria_code/apps/cli/web_login.py +136 -0
  254. aria_code/apps/cli/workflow_runner.py +127 -0
  255. aria_code/apps/cli/workspace_route.py +121 -0
  256. aria_code/apps/daemon/README.md +14 -0
  257. aria_code/apps/vscode/README.md +115 -0
  258. aria_code/apps/vscode/package.json +70 -0
  259. aria_code/aria_cli.py +5244 -0
  260. aria_code/aria_daemon.py +1372 -0
  261. aria_code/aria_feishu_bot.py +16 -0
  262. aria_code/aria_mcp_server.py +61 -0
  263. aria_code/aria_relay_client.py +4 -0
  264. aria_code/aria_telegram_bot.py +11 -0
  265. aria_code/ariarc.py +385 -0
  266. aria_code/artifacts.py +581 -0
  267. aria_code/backtest_engine.py +4 -0
  268. aria_code/backtest_report.py +4 -0
  269. aria_code/brokers/__init__.py +78 -0
  270. aria_code/brokers/automation.py +193 -0
  271. aria_code/brokers/base.py +231 -0
  272. aria_code/brokers/capabilities.py +264 -0
  273. aria_code/brokers/cn/__init__.py +10 -0
  274. aria_code/brokers/cn/easytrader_broker.py +193 -0
  275. aria_code/brokers/cn/futu_broker.py +194 -0
  276. aria_code/brokers/cn/longbridge_broker.py +190 -0
  277. aria_code/brokers/cn/tiger_broker.py +196 -0
  278. aria_code/brokers/cn/xtquant_broker.py +225 -0
  279. aria_code/brokers/config.py +414 -0
  280. aria_code/brokers/intl/__init__.py +5 -0
  281. aria_code/brokers/intl/alpaca_broker.py +183 -0
  282. aria_code/brokers/intl/ibkr_broker.py +215 -0
  283. aria_code/brokers/intl/trading212_broker.py +183 -0
  284. aria_code/brokers/intl/webull_broker.py +156 -0
  285. aria_code/brokers/paper_broker.py +266 -0
  286. aria_code/brokers/planning.py +296 -0
  287. aria_code/brokers/registry.py +213 -0
  288. aria_code/brokers/trading.py +267 -0
  289. aria_code/canva_client.py +4 -0
  290. aria_code/change_store.py +127 -0
  291. aria_code/clients/__init__.py +0 -0
  292. aria_code/clients/aliyun_data_client.py +660 -0
  293. aria_code/clients/aria_relay_client.py +16 -0
  294. aria_code/clients/canva_client.py +284 -0
  295. aria_code/clients/figma_client.py +100 -0
  296. aria_code/clients/football_data_client.py +1671 -0
  297. aria_code/clients/kling_video_client.py +182 -0
  298. aria_code/clients/market_data_client.py +1985 -0
  299. aria_code/clients/openai_image_client.py +242 -0
  300. aria_code/clients/runway_video_client.py +157 -0
  301. aria_code/clients/warehouse_erp_client.py +105 -0
  302. aria_code/cloud_memory.py +190 -0
  303. aria_code/command_safety.py +19 -0
  304. aria_code/computer_use_server.py +276 -0
  305. aria_code/computer_use_tools.py +4 -0
  306. aria_code/dashboard_generator.py +579 -0
  307. aria_code/data_analysis_tools.py +4 -0
  308. aria_code/data_cleaner.py +483 -0
  309. aria_code/data_service.py +513 -0
  310. aria_code/datasources/__init__.py +23 -0
  311. aria_code/datasources/base.py +166 -0
  312. aria_code/datasources/router.py +253 -0
  313. aria_code/datasources/sec_statements.py +217 -0
  314. aria_code/datasources/sources/__init__.py +15 -0
  315. aria_code/datasources/sources/akshare_source.py +269 -0
  316. aria_code/datasources/sources/alpha_vantage_source.py +203 -0
  317. aria_code/datasources/sources/edgar_source.py +219 -0
  318. aria_code/datasources/sources/finnhub_source.py +198 -0
  319. aria_code/datasources/sources/fred_source.py +223 -0
  320. aria_code/datasources/sources/tushare_source.py +143 -0
  321. aria_code/datasources/sources/web_scraper_source.py +278 -0
  322. aria_code/datasources/sources/world_bank_source.py +206 -0
  323. aria_code/datasources/sources/yfinance_source.py +152 -0
  324. aria_code/doctor.py +860 -0
  325. aria_code/domain/__init__.py +0 -0
  326. aria_code/domain/_finance_core/__init__.py +5 -0
  327. aria_code/domain/_finance_core/backtest_engine.py +325 -0
  328. aria_code/domain/_finance_core/performance_stats.py +101 -0
  329. aria_code/domain/backtest_engine.py +75 -0
  330. aria_code/domain/backtest_report.py +488 -0
  331. aria_code/domain/finance_formulas.py +389 -0
  332. aria_code/domain/performance_stats.py +6 -0
  333. aria_code/domain/portfolio_ledger.py +307 -0
  334. aria_code/domain/strategy_vault.py +421 -0
  335. aria_code/evals/__init__.py +37 -0
  336. aria_code/evals/behavior.py +113 -0
  337. aria_code/evals/gap.py +93 -0
  338. aria_code/evals/harness.py +949 -0
  339. aria_code/evals/review_probe.py +132 -0
  340. aria_code/evals/runner.py +425 -0
  341. aria_code/evals/scorecard.py +322 -0
  342. aria_code/evals/summary.py +125 -0
  343. aria_code/external_agent_runner.py +115 -0
  344. aria_code/figma_client.py +4 -0
  345. aria_code/file_analysis_tools.py +4 -0
  346. aria_code/finance_formulas.py +4 -0
  347. aria_code/football_data_client.py +4 -0
  348. aria_code/image_gen_tools.py +4 -0
  349. aria_code/intent_classifier.py +459 -0
  350. aria_code/kling_video_client.py +4 -0
  351. aria_code/licensing.py +151 -0
  352. aria_code/local_finance_tools.py +4 -0
  353. aria_code/local_image_provider.py +4 -0
  354. aria_code/local_llm_provider.py +4 -0
  355. aria_code/macro_tools.py +4 -0
  356. aria_code/markdown_pdf.py +289 -0
  357. aria_code/market_data_client.py +4 -0
  358. aria_code/mcp_client.py +744 -0
  359. aria_code/memory_manager.py +273 -0
  360. aria_code/model_capability.py +512 -0
  361. aria_code/notification_tools.py +4 -0
  362. aria_code/openai_image_client.py +4 -0
  363. aria_code/packages/__init__.py +26 -0
  364. aria_code/packages/adk_bridge/__init__.py +11 -0
  365. aria_code/packages/adk_bridge/code_review_tools.py +49 -0
  366. aria_code/packages/adk_bridge/market_tools.py +125 -0
  367. aria_code/packages/aria_agents/__init__.py +5 -0
  368. aria_code/packages/aria_agents/manifest.py +78 -0
  369. aria_code/packages/aria_core/__init__.py +34 -0
  370. aria_code/packages/aria_core/architecture.py +269 -0
  371. aria_code/packages/aria_core/export.py +147 -0
  372. aria_code/packages/aria_core/manifest.py +65 -0
  373. aria_code/packages/aria_core/paths.py +57 -0
  374. aria_code/packages/aria_core/secure_file.py +68 -0
  375. aria_code/packages/aria_infra/__init__.py +15 -0
  376. aria_code/packages/aria_infra/arthera.py +58 -0
  377. aria_code/packages/aria_infra/doctor.py +246 -0
  378. aria_code/packages/aria_infra/product.py +40 -0
  379. aria_code/packages/aria_mcp/__init__.py +25 -0
  380. aria_code/packages/aria_mcp/bridge.py +232 -0
  381. aria_code/packages/aria_mcp/circuit.py +82 -0
  382. aria_code/packages/aria_mcp/config.py +162 -0
  383. aria_code/packages/aria_mcp/installer.py +95 -0
  384. aria_code/packages/aria_mcp/server.py +1705 -0
  385. aria_code/packages/aria_mcp/tools.py +81 -0
  386. aria_code/packages/aria_sdk/__init__.py +19 -0
  387. aria_code/packages/aria_sdk/client.py +393 -0
  388. aria_code/packages/aria_sdk/providers.py +70 -0
  389. aria_code/packages/aria_sdk/streaming.py +75 -0
  390. aria_code/packages/aria_sdk/types.py +86 -0
  391. aria_code/packages/aria_services/__init__.py +92 -0
  392. aria_code/packages/aria_services/cache.py +60 -0
  393. aria_code/packages/aria_services/context.py +356 -0
  394. aria_code/packages/aria_services/context_checkpoints.py +157 -0
  395. aria_code/packages/aria_services/data.py +11 -0
  396. aria_code/packages/aria_services/provider_health.py +304 -0
  397. aria_code/packages/aria_services/references.py +247 -0
  398. aria_code/packages/aria_services/registry.py +249 -0
  399. aria_code/packages/aria_services/research_protocol.py +137 -0
  400. aria_code/packages/aria_services/research_quality.py +186 -0
  401. aria_code/packages/aria_services/settings.py +160 -0
  402. aria_code/packages/aria_services/usage.py +151 -0
  403. aria_code/packages/aria_skills/__init__.py +46 -0
  404. aria_code/packages/aria_skills/catalog.py +149 -0
  405. aria_code/packages/aria_skills/loader.py +455 -0
  406. aria_code/packages/aria_skills/registry.py +84 -0
  407. aria_code/packages/aria_tools/__init__.py +5 -0
  408. aria_code/packages/aria_tools/financial/__init__.py +5 -0
  409. aria_code/packages/aria_tools/financial/_paths.py +92 -0
  410. aria_code/packages/aria_tools/registry.py +129 -0
  411. aria_code/packages/quant_engine/__init__.py +33 -0
  412. aria_code/packages/quant_engine/_namespace.py +24 -0
  413. aria_code/packages/quant_engine/backtest/__init__.py +10 -0
  414. aria_code/packages/quant_engine/backtest/core.py +53 -0
  415. aria_code/packages/quant_engine/backtest/engine.py +102 -0
  416. aria_code/packages/quant_engine/portfolio/__init__.py +9 -0
  417. aria_code/packages/quant_engine/portfolio/optimizer.py +85 -0
  418. aria_code/packages/quant_engine/sports/__init__.py +76 -0
  419. aria_code/packages/quant_engine/sports/calibrator.py +354 -0
  420. aria_code/packages/quant_engine/sports/dixon_coles.py +234 -0
  421. aria_code/packages/quant_engine/sports/elo.py +300 -0
  422. aria_code/packages/quant_engine/sports/form.py +188 -0
  423. aria_code/packages/quant_engine/sports/h2h.py +195 -0
  424. aria_code/packages/quant_engine/sports/predictor.py +312 -0
  425. aria_code/packages/quant_engine/sports/tracker.py +665 -0
  426. aria_code/packages/quant_engine/stochastic/__init__.py +31 -0
  427. aria_code/packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
  428. aria_code/packages/quant_engine/stochastic/ito_calculus.py +477 -0
  429. aria_code/packages/quant_engine/stochastic/kelly_criterion.py +181 -0
  430. aria_code/packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
  431. aria_code/packages/quant_engine/stochastic/options_pricing.py +576 -0
  432. aria_code/packages/quant_engine/stochastic/stochastic_processes.py +90 -0
  433. aria_code/packs/__init__.py +85 -0
  434. aria_code/packs/base.py +168 -0
  435. aria_code/packs/finance/__init__.py +250 -0
  436. aria_code/packs/healthcare/__init__.py +40 -0
  437. aria_code/packs/logistics/__init__.py +171 -0
  438. aria_code/packs/payments/__init__.py +139 -0
  439. aria_code/packs/realty/__init__.py +190 -0
  440. aria_code/packs/registry.py +202 -0
  441. aria_code/packs/rules.py +90 -0
  442. aria_code/plan_utils.py +194 -0
  443. aria_code/plugin_loader.py +328 -0
  444. aria_code/portfolio_ledger.py +4 -0
  445. aria_code/preview_server.py +449 -0
  446. aria_code/privacy/__init__.py +5 -0
  447. aria_code/privacy/feedback.py +123 -0
  448. aria_code/project_review.py +278 -0
  449. aria_code/project_review_api.py +235 -0
  450. aria_code/project_review_server.py +18 -0
  451. aria_code/project_tools.py +4 -0
  452. aria_code/providers/__init__.py +30 -0
  453. aria_code/providers/google_video_provider.py +256 -0
  454. aria_code/providers/llm/__init__.py +19 -0
  455. aria_code/providers/llm/anthropic.py +184 -0
  456. aria_code/providers/llm/arthera_gateway.py +90 -0
  457. aria_code/providers/llm/autoconfig.py +213 -0
  458. aria_code/providers/llm/base.py +139 -0
  459. aria_code/providers/llm/custom.py +158 -0
  460. aria_code/providers/llm/ollama.py +137 -0
  461. aria_code/providers/llm/openai_compat.py +350 -0
  462. aria_code/providers/llm/registry.py +457 -0
  463. aria_code/providers/local_image_provider.py +202 -0
  464. aria_code/providers/local_llm_provider.py +549 -0
  465. aria_code/providers/ollama_models.py +79 -0
  466. aria_code/providers/openai_params.py +32 -0
  467. aria_code/providers/tool_messages.py +100 -0
  468. aria_code/realty_data_tools.py +4 -0
  469. aria_code/report_exporters.py +150 -0
  470. aria_code/report_generator.py +1764 -0
  471. aria_code/review_service.py +497 -0
  472. aria_code/runtime/__init__.py +176 -0
  473. aria_code/runtime/acceptance.py +632 -0
  474. aria_code/runtime/agent_loop.py +1851 -0
  475. aria_code/runtime/agent_profiles.py +75 -0
  476. aria_code/runtime/approval.py +71 -0
  477. aria_code/runtime/baseline.py +219 -0
  478. aria_code/runtime/budget.py +232 -0
  479. aria_code/runtime/checkpoints.py +481 -0
  480. aria_code/runtime/consult.py +65 -0
  481. aria_code/runtime/contract.py +282 -0
  482. aria_code/runtime/delivery.py +432 -0
  483. aria_code/runtime/events.py +113 -0
  484. aria_code/runtime/gateway.py +167 -0
  485. aria_code/runtime/lsp.py +484 -0
  486. aria_code/runtime/native_index.py +153 -0
  487. aria_code/runtime/orchestrator.py +55 -0
  488. aria_code/runtime/processes.py +343 -0
  489. aria_code/runtime/project_graph.py +614 -0
  490. aria_code/runtime/repo_map.py +712 -0
  491. aria_code/runtime/research_bundle.py +105 -0
  492. aria_code/runtime/review.py +269 -0
  493. aria_code/runtime/run_state.py +127 -0
  494. aria_code/runtime/run_store.py +522 -0
  495. aria_code/runtime/scoped_runs.py +113 -0
  496. aria_code/runtime/self_healing.py +422 -0
  497. aria_code/runtime/semantic_diff.py +171 -0
  498. aria_code/runtime/subagent.py +721 -0
  499. aria_code/runtime/symbol_edit.py +117 -0
  500. aria_code/runtime/task_brief.py +46 -0
  501. aria_code/runtime/task_context.py +145 -0
  502. aria_code/runtime/task_ledger.py +108 -0
  503. aria_code/runtime/task_orchestrator.py +90 -0
  504. aria_code/runtime/task_outcome.py +33 -0
  505. aria_code/runtime/task_verification.py +266 -0
  506. aria_code/runtime/task_worktree.py +448 -0
  507. aria_code/runtime/tool_executor.py +329 -0
  508. aria_code/runtime/tool_policy.py +107 -0
  509. aria_code/runtime/transactions.py +216 -0
  510. aria_code/runtime/windows_job.py +115 -0
  511. aria_code/runtime/workflows.py +222 -0
  512. aria_code/runtime/worktrees.py +215 -0
  513. aria_code/runway_video_client.py +4 -0
  514. aria_code/safety/__init__.py +27 -0
  515. aria_code/safety/client_isolation.py +178 -0
  516. aria_code/safety/permissions.py +339 -0
  517. aria_code/safety/risk.py +699 -0
  518. aria_code/safety/sandbox.py +148 -0
  519. aria_code/safety/service.py +102 -0
  520. aria_code/service_endpoints.py +14 -0
  521. aria_code/setup_wizard.py +684 -0
  522. aria_code/spreadsheet_tools.py +4 -0
  523. aria_code/strategy_vault.py +4 -0
  524. aria_code/tools/__init__.py +0 -0
  525. aria_code/tools/artifact_tools.py +252 -0
  526. aria_code/tools/broker_tools.py +92 -0
  527. aria_code/tools/build_quant_engine.py +97 -0
  528. aria_code/tools/code_audit_tools.py +192 -0
  529. aria_code/tools/computer_use_tools.py +509 -0
  530. aria_code/tools/data_analysis_tools.py +831 -0
  531. aria_code/tools/enterprise_finance_tools.py +202 -0
  532. aria_code/tools/extended_tools.py +240 -0
  533. aria_code/tools/file_analysis_tools.py +880 -0
  534. aria_code/tools/image_gen_tools.py +114 -0
  535. aria_code/tools/local_finance_tools.py +3488 -0
  536. aria_code/tools/logistics_carriers.py +11 -0
  537. aria_code/tools/logistics_inventory.py +11 -0
  538. aria_code/tools/logistics_io.py +11 -0
  539. aria_code/tools/logistics_tenancy.py +11 -0
  540. aria_code/tools/logistics_tools.py +11 -0
  541. aria_code/tools/macro_tools.py +412 -0
  542. aria_code/tools/notification_tools.py +249 -0
  543. aria_code/tools/project_tools.py +525 -0
  544. aria_code/tools/realty_data_tools.py +662 -0
  545. aria_code/tools/spreadsheet_tools.py +399 -0
  546. aria_code/tools/stripe_tools.py +115 -0
  547. aria_code/tools/video_tools.py +500 -0
  548. aria_code/training/__init__.py +1 -0
  549. aria_code/training/compare.py +117 -0
  550. aria_code/training/prelude_commands.py +111 -0
  551. aria_code/training/prelude_eval.py +390 -0
  552. aria_code/training/prelude_real.py +183 -0
  553. aria_code/training/prelude_train.py +248 -0
  554. aria_code/training/sft_dataset.py +230 -0
  555. aria_code/training/trajectory_dataset.py +263 -0
  556. aria_code/ui/__init__.py +105 -0
  557. aria_code/ui/banner.py +404 -0
  558. aria_code/ui/completer.py +571 -0
  559. aria_code/ui/console.py +285 -0
  560. aria_code/ui/input_box.py +505 -0
  561. aria_code/ui/mentions.py +206 -0
  562. aria_code/ui/picker.py +248 -0
  563. aria_code/ui/render/__init__.py +11 -0
  564. aria_code/ui/render/actions.py +297 -0
  565. aria_code/ui/render/finance.py +1732 -0
  566. aria_code/ui/render/market.py +253 -0
  567. aria_code/ui/render/output.py +954 -0
  568. aria_code/ui/render/responsive.py +75 -0
  569. aria_code/ui/render/team.py +367 -0
  570. aria_code/ui/robot.py +239 -0
  571. aria_code/ui/startup_dashboard.py +110 -0
  572. aria_code/video_analysis.py +147 -0
  573. aria_code/video_editor.py +518 -0
  574. aria_code/video_workflow/__init__.py +8 -0
  575. aria_code/video_workflow/__main__.py +61 -0
  576. aria_code/video_workflow/engines/__init__.py +18 -0
  577. aria_code/video_workflow/engines/base.py +69 -0
  578. aria_code/video_workflow/engines/custom_model.py +204 -0
  579. aria_code/video_workflow/engines/google_veo.py +94 -0
  580. aria_code/video_workflow/engines/local_engine.py +71 -0
  581. aria_code/video_workflow/engines/registry.py +98 -0
  582. aria_code/video_workflow/google_models.py +278 -0
  583. aria_code/video_workflow/music/__init__.py +5 -0
  584. aria_code/video_workflow/music/foley_service.py +132 -0
  585. aria_code/video_workflow/music/service.py +212 -0
  586. aria_code/video_workflow/pipeline.py +620 -0
  587. aria_code/video_workflow/plan.py +202 -0
  588. aria_code/video_workflow/session.py +297 -0
  589. aria_code/workspace/__init__.py +6 -0
  590. aria_code/workspace/files.py +219 -0
  591. aria_code/workspace/verify.py +167 -0
  592. aria_code/workspace_changes.py +199 -0
  593. aria_code/workspace_service.py +196 -0
  594. aria_code/workspace_tools.py +150 -0
  595. aria_code-0.167.0.dist-info/METADATA +330 -0
  596. aria_code-0.167.0.dist-info/RECORD +601 -0
  597. aria_code-0.167.0.dist-info/WHEEL +5 -0
  598. aria_code-0.167.0.dist-info/entry_points.txt +5 -0
  599. aria_code-0.167.0.dist-info/licenses/LICENSE +201 -0
  600. aria_code-0.167.0.dist-info/licenses/NOTICE +49 -0
  601. aria_code-0.167.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,954 @@
1
+ """Generic tool-result and error rendering for Aria Code.
2
+
3
+ All functions accept console / has_rich as parameters so they stay
4
+ import-free from aria_cli.py and testable in isolation.
5
+
6
+ Public surface
7
+ --------------
8
+ FINANCE_TOOL_NAMES frozenset of tool names with dedicated renderers
9
+ clean_tool_error_message(e) short user-facing string from any exception
10
+ error_hint(msg, context) actionable recovery suggestion
11
+ print_error(msg, context, *, console, has_rich, rich_box)
12
+ print_tool_result(...)
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import difflib
18
+ import pathlib
19
+ import re
20
+ import time
21
+
22
+
23
+ # ── Finance tool name registry ─────────────────────────────────────────────────
24
+
25
+ FINANCE_TOOL_NAMES: frozenset = frozenset({
26
+ "get_market_data", "get_market_history", "get_crypto_data", "get_forex_data",
27
+ "get_commodities_data", "get_futures_data", "calculate_factors",
28
+ "backtest_strategy", "cloud_backtest", "run_portfolio_backtest", "get_risk_metrics",
29
+ "optimize_positions", "get_sector_performance", "get_northbound_flow",
30
+ "screen_ashare", "get_limit_up_pool", "get_market_indices",
31
+ "analyze_news", "get_bonds_data", "get_ai_signal",
32
+ "get_market_insights", "get_predictions",
33
+ "broker_query", "broker_order",
34
+ })
35
+
36
+
37
+ # ── Narrow-terminal Markdown adaptation ──────────────────────────────────────
38
+
39
+ _TABLE_SEPARATOR_CELL = re.compile(r"^:?-{3,}:?$")
40
+
41
+
42
+ def _markdown_table_cells(line: str) -> list[str]:
43
+ """Split one simple GFM table row without leaking surrounding pipes."""
44
+ stripped = line.strip()
45
+ if not stripped.startswith("|"):
46
+ return []
47
+ return [cell.strip() for cell in stripped.strip("|").split("|")]
48
+
49
+
50
+ def _is_markdown_table_separator(line: str) -> bool:
51
+ cells = _markdown_table_cells(line)
52
+ return bool(cells) and all(_TABLE_SEPARATOR_CELL.fullmatch(cell) for cell in cells)
53
+
54
+
55
+ # Column headers that name no dimension — the value column of a key/value table.
56
+ _GENERIC_TABLE_HEADERS = {"value", "values", "meaning", "数值", "值", "含义", "说明"}
57
+
58
+
59
+ def _table_rows_as_lines(headers: list[str], rows: list[list[str]]) -> list[str]:
60
+ """One line per table row: the first cell as its label, the rest after it.
61
+
62
+ Each cell used to become its own bullet — "• 指标:最新价 / • 数值:USD 332.89"
63
+ — so a ten-row snapshot table took about thirty lines in an 80-column
64
+ terminal. Generic value columns ("Value", "数值") lose their header; a
65
+ column that names a dimension keeps it ("观察:价格高于 MA20"). A value
66
+ already contained in the one before it ("51.9,中性" then "中性") is dropped.
67
+ """
68
+ lines: list[str] = []
69
+ for row in rows:
70
+ cells = [(header, (row[pos] if pos < len(row) else "").strip())
71
+ for pos, header in enumerate(headers) if header]
72
+ if not cells:
73
+ continue
74
+ label = cells[0][1] or "—"
75
+ parts: list[str] = []
76
+ previous = ""
77
+ for header, value in cells[1:]:
78
+ if not value or value in ("—", "-") or (previous and value in previous):
79
+ continue
80
+ generic = header.strip().lower() in _GENERIC_TABLE_HEADERS
81
+ parts.append(value if generic else f"{header}{':' if _has_cjk(header) else ': '}{value}")
82
+ previous = value
83
+ colon = ":" if _has_cjk(label) or any(_has_cjk(h) for h, _ in cells) else ": "
84
+ lines.append(f"- **{label}**{colon}{' · '.join(parts) or '—'}")
85
+ return lines
86
+
87
+
88
+ def _has_cjk(text: str) -> bool:
89
+ return any("\u4e00" <= ch <= "\u9fff" for ch in text or "")
90
+
91
+
92
+ def adapt_markdown_for_width(markup: str, width: int, *, table_breakpoint: int = 96) -> str:
93
+ """Convert GFM tables to stacked records when the terminal is narrow.
94
+
95
+ Rich correctly renders tables, but a three-column research table inside an
96
+ 80-column terminal has too little room for Chinese prose. Cells are then
97
+ truncated or wrapped until row relationships become unreadable. The source
98
+ Markdown remains unchanged in saved reports; only the terminal presentation
99
+ is adapted.
100
+ """
101
+ if width >= table_breakpoint or "|" not in (markup or ""):
102
+ return markup
103
+
104
+ lines = (markup or "").splitlines()
105
+ output: list[str] = []
106
+ index = 0
107
+ in_fence = False
108
+ while index < len(lines):
109
+ line = lines[index]
110
+ if line.lstrip().startswith("```"):
111
+ in_fence = not in_fence
112
+ output.append(line)
113
+ index += 1
114
+ continue
115
+
116
+ if (
117
+ not in_fence
118
+ and index + 1 < len(lines)
119
+ and line.strip().startswith("|")
120
+ and _is_markdown_table_separator(lines[index + 1])
121
+ ):
122
+ headers = _markdown_table_cells(line)
123
+ cursor = index + 2
124
+ rows: list[list[str]] = []
125
+ while cursor < len(lines) and lines[cursor].strip().startswith("|"):
126
+ row = _markdown_table_cells(lines[cursor])
127
+ if row:
128
+ rows.append(row)
129
+ cursor += 1
130
+
131
+ if headers and rows:
132
+ output.extend(_table_rows_as_lines(headers, rows))
133
+ index = cursor
134
+ continue
135
+
136
+ output.append(line)
137
+ index += 1
138
+ return "\n".join(output)
139
+
140
+
141
+ # ── Tool display helpers ──────────────────────────────────────────────────────
142
+
143
+ def tool_display_kind(tool_name: str) -> str:
144
+ """Return a user-facing service/tool kind without exposing local targets."""
145
+ if tool_name.startswith("mcp__"):
146
+ return "MCP"
147
+ if tool_name in FINANCE_TOOL_NAMES:
148
+ return "finance tool"
149
+ if tool_name in {"web_search", "search_web"}:
150
+ return "web search"
151
+ if tool_name == "web_fetch":
152
+ return "web fetch"
153
+ if tool_name in {"read_file", "write_file", "edit_file", "list_files", "search_code"}:
154
+ return "file tool"
155
+ if tool_name == "run_command":
156
+ return "shell tool"
157
+ if tool_name.startswith("skill") or tool_name in {"TaskCreate", "TaskUpdate"}:
158
+ return "skill"
159
+ if tool_name.startswith("broker_") or tool_name in {"broker_query", "broker_order"}:
160
+ return "broker tool"
161
+ return "tool"
162
+
163
+
164
+ def tool_display_label(tool_name: str) -> str:
165
+ """Short label for activity UI: tool name plus its service kind."""
166
+ if tool_name.startswith("mcp__"):
167
+ parts = tool_name.split("__")
168
+ if len(parts) >= 3:
169
+ return f"{parts[1]} · {parts[2].replace('_', ' ')} · MCP"
170
+ return "MCP"
171
+ return f"{tool_name} · {tool_display_kind(tool_name)}"
172
+
173
+
174
+ _RISK_STYLE = {0: "green", 1: "green", 2: "yellow", 3: "bold yellow", 4: "bold red"}
175
+
176
+
177
+ def format_risk_card(assessment, *, impact: str = "") -> list[tuple[str, str]]:
178
+ """What an action touches, as (style, line) pairs, shown above its approval.
179
+
180
+ The prompt used to ask about a command; this says what the command does:
181
+ its level and score, whether it can be undone, the hosts, files and
182
+ systems it reaches, and why it is rated that way.
183
+ """
184
+ level = assessment.level
185
+ head = (f"Risk L{level} {assessment.name} · {assessment.score}/100 · "
186
+ f"{'reversible' if assessment.reversible else 'not reversible'}")
187
+ lines = [(_RISK_STYLE.get(level, ""), head)]
188
+
189
+ def _row(label: str, values) -> None:
190
+ values = [str(v) for v in values if str(v)]
191
+ if values:
192
+ shown = ", ".join(values[:4]) + (f" +{len(values) - 4}" if len(values) > 4 else "")
193
+ lines.append(("dim", f"{label:<9}{shown}"))
194
+
195
+ _row("Network", assessment.hosts)
196
+ _row("Files", assessment.files)
197
+ _row("Impact", [impact] if impact else [])
198
+ _row("Affects", assessment.systems)
199
+ _row("Why", assessment.reasons[:2])
200
+ return lines
201
+
202
+
203
+ def _file_impact(assessment) -> str:
204
+ """What depends on the files an edit touches, from the cached project graph."""
205
+ files = [str(f) for f in (getattr(assessment, "files", None) or ()) if str(f).startswith("/")]
206
+ if not files:
207
+ return ""
208
+ try:
209
+ from aria_code.runtime.project_graph import impact_line, impact_of_paths
210
+
211
+ return impact_line(impact_of_paths(files))
212
+ except Exception:
213
+ return ""
214
+
215
+
216
+ def print_risk_card(console, assessment) -> None:
217
+ from rich.markup import escape
218
+
219
+ for style, line in format_risk_card(assessment, impact=_file_impact(assessment)):
220
+ if console is None:
221
+ print(f" {line}")
222
+ else:
223
+ text = f" {escape(line)}"
224
+ console.print(f"[{style}]{text}[/{style}]" if style else text, highlight=False)
225
+
226
+
227
+ _DELIVERY_HEADINGS = {"Changed", "Verified", "Behaviour", "Review", "Risk", "Contract", "Checkpoint", "Next"}
228
+
229
+
230
+ def format_delivery_report(delivery: dict, *, run_id: str = "", root=None) -> list[tuple[str, str]]:
231
+ """The runtime's delivery report as (style, line) pairs, for any console.
232
+
233
+ The text comes from :meth:`DeliveryReport.render`; this only colours it:
234
+ the status by outcome, headings dim, ✓ green, ✗ red, × and ⚠ yellow.
235
+ """
236
+ from aria_code.runtime.delivery import DeliveryReport
237
+
238
+ report = DeliveryReport.from_dict(delivery)
239
+ hint = f"/rewind code {run_id}" if run_id and report.checkpoints else ""
240
+ styled: list[tuple[str, str]] = []
241
+ for index, line in enumerate(report.render(rewind_hint=hint, root=root).splitlines()):
242
+ stripped = line.strip()
243
+ if index == 0:
244
+ style = "bold green" if report.status == "done" else "bold yellow"
245
+ elif stripped in _DELIVERY_HEADINGS:
246
+ style = "dim"
247
+ elif stripped.startswith("✓"):
248
+ style = "green"
249
+ elif stripped.startswith("✗"):
250
+ style = "red"
251
+ elif stripped.startswith(("×", "⚠")):
252
+ style = "yellow"
253
+ else:
254
+ style = ""
255
+ styled.append((style, line))
256
+ return styled
257
+
258
+
259
+ def print_delivery_report(console, delivery: dict, *, run_id: str = "", root=None) -> None:
260
+ """Print the report indented under the answer; plain print without Rich."""
261
+ from rich.markup import escape
262
+
263
+ lines = format_delivery_report(delivery, run_id=run_id, root=root)
264
+ if console is None:
265
+ print()
266
+ for _, line in lines:
267
+ print(f" {line}" if line else "")
268
+ return
269
+ console.print()
270
+ for style, line in lines:
271
+ text = f" {escape(line)}" if line else ""
272
+ console.print(f"[{style}]{text}[/{style}]" if style and text else text, highlight=False)
273
+
274
+
275
+ def format_turn_footer(metadata, *, mode: str = "compact", copy_available: bool = False) -> str:
276
+ """Return the post-response status line.
277
+
278
+ ``full`` keeps the historical token-heavy line for debugging. ``compact``
279
+ is the default interactive UI: elapsed time, provider, tools, and /copy.
280
+ """
281
+ mode = (mode or "compact").strip().lower()
282
+ if mode in {"off", "none", "false", "0"}:
283
+ return ""
284
+ parts = list(getattr(metadata, "parts", []) or [])
285
+ if mode in {"full", "debug", "verbose"}:
286
+ footer = " · ".join(parts)
287
+ if copy_available:
288
+ footer = f"{footer} /copy" if footer else "/copy"
289
+ return footer
290
+
291
+ elapsed = parts[0] if parts else ""
292
+ provider = str(getattr(metadata, "provider", "") or "").strip()
293
+ tools = list(getattr(metadata, "tools", []) or [])
294
+ out: list[str] = []
295
+ if elapsed:
296
+ out.append(elapsed)
297
+ if provider and provider not in {"aws", "local"}:
298
+ out.append(provider)
299
+ if tools:
300
+ shown = " ".join(str(t) for t in tools[:3])
301
+ if len(tools) > 3:
302
+ shown += f" +{len(tools) - 3}"
303
+ out.append(shown)
304
+ if copy_available:
305
+ out.append("/copy")
306
+ return " · ".join(out)
307
+
308
+
309
+ def file_uri(path: object) -> str:
310
+ """A terminal-hyperlink target for a local file: file:///Users/…/chart.html.
311
+
312
+ Saved files were linked as [link=/Users/…/chart.html]: OSC 8 hyperlinks
313
+ take a URI, so terminals that support them had nothing to open. A value
314
+ that already has a scheme (https://…) is returned unchanged.
315
+ """
316
+ text = str(path or "")
317
+ if not text or "://" in text:
318
+ return text
319
+ try:
320
+ return pathlib.Path(text).expanduser().resolve().as_uri()
321
+ except (OSError, ValueError):
322
+ return text
323
+
324
+
325
+ def display_path(path: object, *, fallback: str = "file") -> str:
326
+ """Return a path-safe display value for user-facing UI."""
327
+ if not path:
328
+ return fallback
329
+ try:
330
+ name = pathlib.Path(str(path)).name
331
+ except Exception:
332
+ name = ""
333
+ return name or fallback
334
+
335
+
336
+ # ── Error helpers ──────────────────────────────────────────────────────────────
337
+
338
+ def clean_tool_error_message(error: object) -> str:
339
+ raw = str(error or "failed").strip()
340
+ low = raw.lower()
341
+ if not raw:
342
+ return "操作失败"
343
+ if "curl: (28)" in low or "timed out" in low or "timeout" in low:
344
+ return "请求超时,数据源暂时不可用。请稍后重试或运行 /health 检查服务。"
345
+ if "connection refused" in low:
346
+ return "连接被拒绝,服务暂时不可用。请检查本地服务或网络。"
347
+ if "connection aborted" in low or "remotedisconnected" in low:
348
+ return "网络连接中断,数据源未完成响应。请稍后重试。"
349
+ # Generic connection / proxy / DNS failures — collapse the verbose
350
+ # urllib3 HTTPSConnectionPool(...) dump into a single readable line.
351
+ if any(s in low for s in (
352
+ "httpsconnectionpool", "httpconnectionpool", "max retries exceeded",
353
+ "proxyerror", "failed to establish a new connection",
354
+ "nameresolutionerror", "getaddrinfo failed", "newconnectionerror",
355
+ )):
356
+ import re as _re3
357
+ _host = _re3.search(r"host=['\"]([^'\"]+)['\"]", raw)
358
+ _hint = f"(数据源 {_host.group(1)})" if _host else ""
359
+ return f"数据源连接失败{_hint},可能是网络或代理问题。请检查网络后重试。"
360
+ if "rate" in low or "429" in low or "too many requests" in low:
361
+ return "数据源请求频率受限,请稍后重试。"
362
+ # Collapse verbose HTTP error strings: "web_fetch failed: 401 Client Error: Unauthorized for url: https://..."
363
+ import re as _re
364
+ _http = _re.match(r"web_fetch failed:\s*(\d{3})\s+\w[\w\s]+?:\s*([\w\s]+?)(?:\s+for url:.*)?$", raw, _re.I)
365
+ if _http:
366
+ code, phrase = _http.group(1), _http.group(2).strip()
367
+ return f"HTTP {code} {phrase}"
368
+ if "traceback" in low:
369
+ return raw.splitlines()[-1][:160] if raw.splitlines() else "运行失败"
370
+ return raw[:200]
371
+
372
+
373
+ def error_hint(error: str, context: str = "") -> str:
374
+ err_lower = error.lower() if error else ""
375
+ if "connection" in err_lower or "refused" in err_lower or "unreachable" in err_lower:
376
+ return "Hint: Backend unreachable. Try /health or check your network."
377
+ if "timeout" in err_lower or "timed out" in err_lower:
378
+ return "Hint: Request timed out. Try again or check /health."
379
+ # External web pages that block scraping (paywall / anti-bot) — NOT an Aria
380
+ # login problem, so /login must not be suggested.
381
+ _is_web = any(m in err_lower for m in (
382
+ "http://", "https://", "www.", ".com", ".org", ".net",
383
+ "web_fetch", "web fetch", "forbidden",
384
+ ))
385
+ def code(n: str) -> bool:
386
+ # A status code as a number of its own: "1500 bars" is not a 500.
387
+ return re.search(rf"(?<!\d){n}(?!\d)", err_lower) is not None
388
+
389
+ if code("401") or "unauthorized" in err_lower:
390
+ if any(h in err_lower for h in ("finnhub", "alphavantage", "polygon", "api/v1", "api/v2/finance")):
391
+ return "Hint: API key required — /apikey set finnhub <KEY> (free at finnhub.io)"
392
+ if _is_web:
393
+ return "Hint: This site blocks automated access (paywall/anti-bot). Try another source."
394
+ return "Hint: Authentication required. Run /login to sign in."
395
+ if code("403") or "forbidden" in err_lower:
396
+ if _is_web:
397
+ return "Hint: This site blocks automated access (paywall/anti-bot). Try another source."
398
+ return "Hint: Access denied. Check your API key or subscription."
399
+ # "rate" alone matched strategy, generate and separate: "Unknown strategy"
400
+ # came with "Rate limited. Wait a moment and try again."
401
+ if code("429") or re.search(r"rate[\s_-]?limit|too many requests", err_lower):
402
+ return "Hint: Rate limited. Wait a moment and try again."
403
+ if ("ollama" in err_lower or "ollama http" in err_lower) and (
404
+ "not found" in err_lower or "404" in err_lower
405
+ ):
406
+ m = re.search(r"model ['\"]?([^'\"]+)['\"]? not found", err_lower)
407
+ model_hint = m.group(1) if m else "the requested model"
408
+ try:
409
+ from local_llm_provider import list_ollama_models
410
+ available = list_ollama_models("http://localhost:11434")
411
+ if available:
412
+ suggestion = available[0]
413
+ return (
414
+ f"Hint: Ollama model '{model_hint}' not found.\n"
415
+ f" Available: {', '.join(available[:4])}\n"
416
+ f" Run: /config model {suggestion}"
417
+ )
418
+ except Exception:
419
+ pass
420
+ return (
421
+ "Hint: Ollama model not found. Run `ollama list` to see available models.\n"
422
+ " Or pull one: ollama pull qwen2.5-coder:7b"
423
+ )
424
+ # "File not found" is a path error. Tell the model firmly NOT to keep
425
+ # guessing filenames (it otherwise loops app.py→script.py→main.py…).
426
+ if "file not found" in err_lower or "no such file" in err_lower:
427
+ return ("Hint: This file does not exist. Do NOT guess other filenames — "
428
+ "list the directory first, or this question may not need a file at all.")
429
+ if code("404") and context == "tool":
430
+ return "Hint: Tool not available. Check /tools for available tools."
431
+ if "not found" in err_lower and context == "session":
432
+ return "Hint: Session not found. Run /sessions to list available."
433
+ if code("404") or ("not found" in err_lower and context not in ("tool", "")):
434
+ return "Hint: Resource not found. Check the symbol or path."
435
+ if "no data" in err_lower or "no result" in err_lower:
436
+ return "Hint: No data returned. Verify the symbol spelling."
437
+ if code("500") or "internal server" in err_lower:
438
+ return "Hint: Server error. Try again in a moment or /health to check."
439
+ if context == "login":
440
+ return "Hint: Check email/password. Usage: /login email password"
441
+ return ""
442
+
443
+
444
+ # ── Error panel & tree formatting ──────────────────────────────────────────────
445
+
446
+ def print_hanging(console, prefix: str, text: str, style: str = "") -> None:
447
+ """One message after a fixed prefix, wrapped lines aligned under its first word.
448
+
449
+ The text is plain, never markup: an error such as "/init [--force]" lost
450
+ its brackets to Rich, and a long one wrapped back to column 0 under the
451
+ "└" instead of beside it.
452
+ """
453
+ from rich.table import Table
454
+ from rich.text import Text
455
+
456
+ grid = Table.grid(padding=0)
457
+ grid.add_column(no_wrap=True)
458
+ grid.add_column(overflow="fold")
459
+ grid.add_row(Text(prefix, style=style), Text(text, style=style))
460
+ console.print(grid)
461
+
462
+
463
+ # The contexts error_hint() understands; anything else given is a hint to show.
464
+ ERROR_CONTEXTS = frozenset({"", "tool", "login", "session", "screenshot", "vision", "browser",
465
+ "browser screenshot", "deep"})
466
+
467
+
468
+ def print_error(
469
+ msg: str,
470
+ context: str = "",
471
+ *,
472
+ console,
473
+ has_rich: bool,
474
+ rich_box=None,
475
+ use_panel: bool = False,
476
+ ) -> None:
477
+ # The second argument is either where the error came from ("tool",
478
+ # "login"), from which a hint is guessed, or the hint itself. Most callers
479
+ # pass the hint: "请先用 /broker add 添加", "Usage: /compare SYMBOL …",
480
+ # "Install: pip install mss pillow". It was taken for a context and never
481
+ # shown, and a guess took its place.
482
+ if str(context or "").strip().lower() in ERROR_CONTEXTS:
483
+ hint = error_hint(msg, context)
484
+ else:
485
+ hint = str(context)
486
+ if not use_panel:
487
+ # Claude Code style: clean └ tree connector for inline error guidance
488
+ if has_rich:
489
+ print_hanging(console, " └ ", msg, "red")
490
+ if hint:
491
+ for hline in hint.splitlines():
492
+ print_hanging(console, " ", hline, "dim")
493
+ else:
494
+ print(f" └ {msg}")
495
+ if hint:
496
+ for hline in hint.splitlines():
497
+ print(f" {hline}")
498
+ return
499
+
500
+ if has_rich:
501
+ from rich.panel import Panel
502
+ box_style = getattr(rich_box, "ROUNDED", None) if rich_box else None
503
+ body = f"[red]{msg}[/red]"
504
+ if hint:
505
+ body += f"\n[dim]{hint}[/dim]"
506
+ console.print(Panel(body, border_style="red", box=box_style, padding=(0, 1)))
507
+ else:
508
+ print(msg)
509
+
510
+
511
+ # ── Tool result ────────────────────────────────────────────────────────────────
512
+
513
+ def print_tool_result(
514
+ tool_name: str,
515
+ result: dict,
516
+ elapsed: float = 0,
517
+ params: dict = None,
518
+ *,
519
+ console,
520
+ has_rich: bool,
521
+ rich_box,
522
+ print_finance_fn, # callable(tool_name, result) for finance tools
523
+ bot_mode: bool = False,
524
+ ) -> None:
525
+ """Render a tool result summary — Codex-style ⎿ tree connector."""
526
+ if bot_mode:
527
+ return
528
+
529
+ ts = f" [dim]{elapsed:.1f}s[/dim]" if elapsed >= 0.1 else ""
530
+ ts_plain = f" {elapsed:.1f}s" if elapsed >= 0.1 else ""
531
+ params = params or {}
532
+
533
+ if tool_name in FINANCE_TOOL_NAMES:
534
+ print_finance_fn(tool_name, result)
535
+ if ts and has_rich:
536
+ console.print(f" [dim]⎿[/dim]{ts}")
537
+ return
538
+
539
+ if result.get("success"):
540
+ data = result.get("data", {})
541
+
542
+ if tool_name == "write_file":
543
+ lines = data.get("lines") or (params.get("content", "").count("\n") + 1 if params.get("content") else 0)
544
+ size = data.get("size_bytes") or len((params.get("content", "") or "").encode())
545
+ size_str = f"{size}B" if size < 1024 else f"{size // 1024}KB"
546
+ if has_rich:
547
+ console.print(f" [dim]⎿[/dim] [green]✓[/green] [dim]file tool {lines} lines {size_str}[/dim]{ts}")
548
+ else:
549
+ print(f" ⎿ ✓ file tool {lines} lines {size_str}{ts_plain}")
550
+
551
+ elif tool_name == "edit_file":
552
+ old = params.get("old_string", "")
553
+ new = params.get("new_string", "")
554
+ if old and new and has_rich:
555
+ diff = list(difflib.unified_diff(
556
+ old.splitlines(keepends=True),
557
+ new.splitlines(keepends=True),
558
+ lineterm="",
559
+ ))
560
+ if diff:
561
+ _hdr = " [dim]⎿[/dim] [#C08050]file tool[/#C08050]"
562
+ console.print(f"{_hdr}{ts}")
563
+ from rich.syntax import Syntax
564
+ from rich.panel import Panel
565
+ diff_text = "".join(diff[2:])
566
+ console.print(Panel(
567
+ Syntax(diff_text, "diff", theme="monokai", padding=(0, 1)),
568
+ border_style="dim",
569
+ box=rich_box.SIMPLE,
570
+ ))
571
+ else:
572
+ console.print(f" [dim]⎿ no change[/dim]{ts}")
573
+ elif has_rich:
574
+ console.print(f" [dim]⎿ edited[/dim]{ts}")
575
+ else:
576
+ print(f" ⎿ edited{ts_plain}")
577
+
578
+ elif tool_name == "run_command":
579
+ stdout = data.get("stdout", "").strip()
580
+ returncode = data.get("returncode", data.get("exit_code", 0))
581
+ if has_rich:
582
+ from rich.panel import Panel
583
+ rc_color = "green" if returncode == 0 else "red"
584
+ rc_icon = "✓" if returncode == 0 else "✗"
585
+ console.print(f" [dim]⎿[/dim] [{rc_color}]{rc_icon} exit {returncode}[/{rc_color}]{ts}")
586
+ if stdout:
587
+ out_lines = stdout.splitlines()
588
+ if len(out_lines) > 15:
589
+ head = out_lines[:5]
590
+ tail = out_lines[-10:]
591
+ truncated = "\n".join(head) + f"\n[dim]… ({len(out_lines) - 15} lines hidden) …[/dim]\n" + "\n".join(tail)
592
+ if data.get("full_output_path"):
593
+ truncated += f"\n[dim]full output saved: {data.get('full_output_path')}[/dim]"
594
+ console.print(Panel(
595
+ f"[dim]{truncated}[/dim]",
596
+ border_style="dim",
597
+ box=rich_box.SIMPLE,
598
+ padding=(0, 1),
599
+ ))
600
+ else:
601
+ truncated = "\n".join(out_lines)
602
+ if data.get("full_output_path"):
603
+ truncated += f"\n[dim]full output saved: {data.get('full_output_path')}[/dim]"
604
+ console.print(Panel(
605
+ f"[dim]{truncated}[/dim]",
606
+ border_style="dim",
607
+ box=rich_box.SIMPLE,
608
+ padding=(0, 1),
609
+ ))
610
+ else:
611
+ print(f" ⎿ exit {returncode}{ts_plain}")
612
+ for ol in stdout.splitlines()[:4]:
613
+ print(f" {ol[:100]}")
614
+
615
+ elif tool_name == "read_file":
616
+ lines = data.get("lines", 0)
617
+ if has_rich:
618
+ console.print(f" [dim]⎿ file tool {lines} lines[/dim]{ts}")
619
+ else:
620
+ print(f" ⎿ file tool {lines} lines{ts_plain}")
621
+
622
+ elif tool_name == "list_files":
623
+ count = data.get("count", 0)
624
+ if has_rich:
625
+ color = "yellow" if count == 0 else "dim"
626
+ msg = "0 items — no matches" if count == 0 else f"{count} items"
627
+ console.print(f" [{color}]⎿ {msg}[/{color}]{ts}")
628
+ else:
629
+ print(f" ⎿ {count} items{ts_plain}")
630
+
631
+ elif tool_name == "search_code":
632
+ matches = len(data.get("matches", []))
633
+ if has_rich:
634
+ console.print(f" [dim]⎿ {matches} matches[/dim]{ts}")
635
+ else:
636
+ print(f" ⎿ {matches} matches{ts_plain}")
637
+
638
+ elif tool_name == "web_fetch":
639
+ length = data.get("length", 0)
640
+ trunc = data.get("truncated", False)
641
+ len_str = f" {length:,} chars" if length else ""
642
+ trunc_str = " [dim]truncated[/dim]" if trunc else ""
643
+ if has_rich:
644
+ console.print(f" [dim]⎿ web fetch{len_str}[/dim]{trunc_str}{ts}")
645
+ else:
646
+ print(f" ⎿ web fetch{ts_plain}")
647
+
648
+ elif tool_name in ("web_search", "search_web"):
649
+ results = data.get("results", [])
650
+ count = len(results)
651
+ if has_rich:
652
+ console.print(f" [dim]⎿ {count} results[/dim]{ts}")
653
+ else:
654
+ print(f" ⎿ {count} results{ts_plain}")
655
+
656
+ else:
657
+ short = tool_display_kind(tool_name)
658
+ if has_rich:
659
+ console.print(f" [dim]⎿ {short} done[/dim]{ts}")
660
+ else:
661
+ print(f" ⎿ done{ts_plain}")
662
+
663
+ else:
664
+ error = clean_tool_error_message(result.get("error", "failed"))
665
+ hint = error_hint(str(error), context="tool")
666
+ if has_rich:
667
+ console.print(f" [dim]⎿[/dim] [red]✗ {error[:120]}[/red]")
668
+ if hint:
669
+ console.print(f" [dim]{hint}[/dim]")
670
+ else:
671
+ print(f" ⎿ ✗ {error[:80]}")
672
+
673
+
674
+ # ── Activity group (OpenClaw-style batch summary) ──────────────────────────────
675
+
676
+ def _one_line_tool_summary(
677
+ tool_name: str,
678
+ result: dict,
679
+ elapsed: float,
680
+ params: dict,
681
+ ) -> tuple[str, str]:
682
+ """Return (status_markup, detail_markup) for one tool in an activity table."""
683
+ params = params or {}
684
+ ts = f"[dim] {elapsed:.1f}s[/dim]" if elapsed >= 0.1 else ""
685
+
686
+ if not result.get("success"):
687
+ error = clean_tool_error_message(result.get("error", "failed"))
688
+ return "[red]✗[/red]", f"[red]{error[:80]}[/red]{ts}"
689
+
690
+ data = result.get("data", {})
691
+ kind = tool_display_kind(tool_name)
692
+
693
+ if tool_name == "write_file":
694
+ lines = data.get("lines") or (params.get("content", "").count("\n") + 1 if params.get("content") else 0)
695
+ size = data.get("size_bytes") or len((params.get("content", "") or "").encode())
696
+ size_str = f"{size}B" if size < 1024 else f"{size // 1024}KB"
697
+ return "[green]✓[/green]", f"[dim]{kind} {lines} lines {size_str}[/dim]{ts}"
698
+
699
+ elif tool_name == "edit_file":
700
+ return "[green]✓[/green]", f"[dim]edited {kind}[/dim]{ts}"
701
+
702
+ elif tool_name == "run_command":
703
+ rc = data.get("returncode", data.get("exit_code", 0))
704
+ icon = "[green]✓[/green]" if rc == 0 else "[red]✗[/red]"
705
+ color = "green" if rc == 0 else "red"
706
+ suffix = " [dim]· full output saved[/dim]" if data.get("full_output_path") else ""
707
+ return icon, f"[{color}]exit {rc}[/{color}]{suffix}{ts}"
708
+
709
+ elif tool_name == "read_file":
710
+ lines = data.get("lines", 0)
711
+ return "[green]✓[/green]", f"[dim]{kind} {lines} lines[/dim]{ts}"
712
+
713
+ elif tool_name == "list_files":
714
+ count = data.get("count", 0)
715
+ color = "yellow" if count == 0 else "dim"
716
+ msg = "no matches" if count == 0 else f"{count} items"
717
+ return "[green]✓[/green]", f"[{color}]{msg}[/{color}]{ts}"
718
+
719
+ elif tool_name == "search_code":
720
+ matches = len(data.get("matches", []))
721
+ return "[green]✓[/green]", f"[dim]{matches} matches[/dim]{ts}"
722
+
723
+ elif tool_name == "web_fetch":
724
+ length = data.get("length", 0)
725
+ len_s = f" {length:,}c" if length else ""
726
+ return "[green]✓[/green]", f"[dim]{kind}{len_s}[/dim]{ts}"
727
+
728
+ elif tool_name in ("web_search", "search_web"):
729
+ count = len(data.get("results", []))
730
+ return "[green]✓[/green]", f"[dim]{count} results[/dim]{ts}"
731
+
732
+ else:
733
+ return "[green]✓[/green]", f"[dim]{kind} done[/dim]{ts}"
734
+
735
+
736
+ def print_tool_activity_group(
737
+ results: list, # list of (tool_name, result, elapsed, params)
738
+ *,
739
+ console,
740
+ has_rich: bool,
741
+ rich_box,
742
+ print_finance_fn,
743
+ bot_mode: bool = False,
744
+ ) -> None:
745
+ """Render multiple tool results as a compact Activity block (OpenClaw style).
746
+
747
+ For N >= 2 tools: prints a titled table.
748
+ For N == 1: delegates to print_tool_result (single-line).
749
+ """
750
+ if bot_mode or not results:
751
+ return
752
+
753
+ if len(results) == 1:
754
+ tool_name, result, elapsed, params = results[0]
755
+ print_tool_result(tool_name, result, elapsed, params,
756
+ console=console, has_rich=has_rich, rich_box=rich_box,
757
+ print_finance_fn=print_finance_fn, bot_mode=bot_mode)
758
+ return
759
+
760
+ total_elapsed = sum(e for _, _, e, _ in results)
761
+ n = len(results)
762
+
763
+ # Finance tools: print with dedicated renderer, then add to activity table
764
+ finance_rows = []
765
+ for tool_name, result, elapsed, params in results:
766
+ if tool_name in FINANCE_TOOL_NAMES:
767
+ print_finance_fn(tool_name, result)
768
+ finance_rows.append(tool_name)
769
+
770
+ if has_rich:
771
+ from rich.table import Table
772
+ ts_total = f" [dim]{total_elapsed:.1f}s[/dim]" if total_elapsed >= 0.1 else ""
773
+ header = f"[dim]Activity · {n} tools[/dim]{ts_total}"
774
+ console.print(f"\n {header}")
775
+ tbl = Table.grid(padding=(0, 2))
776
+ tbl.add_column(no_wrap=True, min_width=14, style="dim") # tool name
777
+ tbl.add_column(no_wrap=True, min_width=2) # status icon
778
+ tbl.add_column() # detail
779
+
780
+ from collections import OrderedDict
781
+ _mcp_groups: "OrderedDict[str, list]" = OrderedDict()
782
+ for tool_name, result, elapsed, params in results:
783
+ if tool_name in finance_rows:
784
+ icon = "[green]✓[/green]" if result.get("success") else "[red]✗[/red]"
785
+ tbl.add_row(tool_name, icon, "")
786
+ elif tool_name.startswith("mcp__"):
787
+ # Defer MCP calls — collapse per server below
788
+ _server = tool_name.split("__")[1] if len(tool_name.split("__")) >= 2 else "mcp"
789
+ _mcp_groups.setdefault(_server, []).append((tool_name, result))
790
+ else:
791
+ icon, detail = _one_line_tool_summary(tool_name, result, elapsed, params)
792
+ tbl.add_row(f"[dim]{tool_name}[/dim]", icon, detail)
793
+
794
+ # Collapsed MCP rows: "server · tool" for one, "called N times" for many
795
+ for _server, _calls in _mcp_groups.items():
796
+ _all_ok = all(r.get("success") for _, r in _calls)
797
+ _icon = "[green]✓[/green]" if _all_ok else "[red]✗[/red]"
798
+ if len(_calls) == 1:
799
+ _tn = _calls[0][0].split("__")
800
+ _label = _tn[2].replace("_", " ") if len(_tn) >= 3 else _server
801
+ tbl.add_row(f"[dim]{_server}[/dim]", _icon, f"[dim]{_label} · MCP[/dim]")
802
+ else:
803
+ tbl.add_row(f"[dim]{_server}[/dim]", _icon,
804
+ f"[dim]called {len(_calls)} times · MCP[/dim]")
805
+
806
+ from rich.padding import Padding
807
+ console.print(Padding(tbl, (0, 0, 0, 4)))
808
+
809
+ # For run_command with stdout, still print the output panel
810
+ for tool_name, result, elapsed, params in results:
811
+ if tool_name == "run_command" and result.get("success"):
812
+ stdout = result.get("data", {}).get("stdout", "").strip()
813
+ if stdout:
814
+ from rich.panel import Panel
815
+ out_lines = stdout.splitlines()
816
+ if len(out_lines) > 15:
817
+ head = out_lines[:5]
818
+ tail = out_lines[-10:]
819
+ truncated = "\n".join(head) + f"\n[dim]… ({len(out_lines) - 15} lines hidden) …[/dim]\n" + "\n".join(tail)
820
+ if result.get("data", {}).get("full_output_path"):
821
+ truncated += f"\n[dim]full output saved: {result.get('data', {}).get('full_output_path')}[/dim]"
822
+ console.print(Panel(f"[dim]{truncated}[/dim]",
823
+ border_style="dim", box=rich_box.SIMPLE,
824
+ padding=(0, 1)))
825
+ else:
826
+ truncated = "\n".join(out_lines)
827
+ if result.get("data", {}).get("full_output_path"):
828
+ truncated += f"\n[dim]full output saved: {result.get('data', {}).get('full_output_path')}[/dim]"
829
+ console.print(Panel(f"[dim]{truncated}[/dim]",
830
+ border_style="dim", box=rich_box.SIMPLE,
831
+ padding=(0, 1)))
832
+
833
+ # For edit_file, still print diff
834
+ elif tool_name == "edit_file" and result.get("success"):
835
+ old = (params or {}).get("old_string", "")
836
+ new = (params or {}).get("new_string", "")
837
+ if old and new:
838
+ diff = list(difflib.unified_diff(
839
+ old.splitlines(keepends=True),
840
+ new.splitlines(keepends=True),
841
+ lineterm="",
842
+ ))
843
+ if diff:
844
+ from rich.syntax import Syntax
845
+ from rich.padding import Padding
846
+ diff_text = "".join(diff[2:])
847
+ console.print(Padding(Panel(
848
+ Syntax(diff_text, "diff", theme="monokai", padding=(0, 1)),
849
+ border_style="dim", box=rich_box.SIMPLE
850
+ ), (0, 0, 0, 4)))
851
+ else:
852
+ ts_total = f" {total_elapsed:.1f}s" if total_elapsed >= 0.1 else ""
853
+ print(f"\n Activity · {n} tools{ts_total}")
854
+ for tool_name, result, elapsed, params in results:
855
+ icon, detail = _one_line_tool_summary(tool_name, result, elapsed, params)
856
+ detail_plain = re.sub(r"\[/?[^\]]+\]", "", detail)
857
+ icon_plain = "✓" if result.get("success") else "✗"
858
+ print(f" {tool_name:<18}{icon_plain} {detail_plain}")
859
+
860
+
861
+ # ── Fallback / model-switch toast ──────────────────────────────────────────────
862
+
863
+ def print_fallback_toast(
864
+ from_provider: str,
865
+ to_provider: str,
866
+ reason: str = "",
867
+ *,
868
+ console,
869
+ has_rich: bool,
870
+ ) -> None:
871
+ """Show a transient yellow notification when the active model/provider switches."""
872
+ if not has_rich:
873
+ print(f"\n ⚡ 模型切换 {from_provider} → {to_provider}{(' ' + reason) if reason else ''}")
874
+ return
875
+ body = f"[bold #C08050]⚡[/bold #C08050] [#C08050]{from_provider}[/#C08050] [dim]→[/dim] [#C08050]{to_provider}[/#C08050]"
876
+ if reason:
877
+ body += f"\n [dim]{reason}[/dim]"
878
+ console.print(f"\n {body}")
879
+
880
+
881
+ # ── Context pressure warning ───────────────────────────────────────────────────
882
+
883
+ _CTX_WARNED: dict[str, float] = {} # session_id → last warn time
884
+
885
+ def print_context_warning(
886
+ est_tokens: int,
887
+ max_tokens: int,
888
+ *,
889
+ console,
890
+ has_rich: bool,
891
+ session_id: str = "",
892
+ cooldown: float = 120.0, # only warn once every 2 min per session
893
+ ) -> None:
894
+ """Warn when context is >85% full; rate-limited to avoid spam."""
895
+ if max_tokens <= 0:
896
+ return
897
+ ratio = est_tokens / max_tokens
898
+ if ratio < 0.85:
899
+ return
900
+ now = time.monotonic()
901
+ if now - _CTX_WARNED.get(session_id, 0) < cooldown:
902
+ return
903
+ _CTX_WARNED[session_id] = now
904
+
905
+ def _k(n: int) -> str:
906
+ return f"{n // 1000}K" if n >= 1000 else str(n)
907
+
908
+ pct = int(ratio * 100)
909
+ if has_rich:
910
+ color = "red" if ratio >= 0.95 else "#C08050"
911
+ icon = "●" if ratio >= 0.95 else "⚠"
912
+ msg = f" [{color}]{icon} 上下文 {pct}% 已满 ({_k(est_tokens)}/{_k(max_tokens)} tokens)[/{color}]"
913
+ msg += " [dim]→ /compact 压缩历史 /clear 重置[/dim]"
914
+ console.print(msg)
915
+ else:
916
+ print(f" ⚠ 上下文 {pct}% ({_k(est_tokens)}/{_k(max_tokens)} tokens) — /compact 或 /clear")
917
+
918
+
919
+ # ── Blocked / cancelled tool visual ───────────────────────────────────────────
920
+
921
+ def print_tool_blocked(
922
+ tool_name: str,
923
+ reason: str = "用户取消",
924
+ *,
925
+ console,
926
+ has_rich: bool,
927
+ ) -> None:
928
+ """Show a styled 'Blocked' line when tool execution is denied or cancelled."""
929
+ if has_rich:
930
+ console.print(
931
+ f" [dim]⎿[/dim] [#C08050]⊘ {tool_name}[/#C08050] [dim]{reason}[/dim]"
932
+ )
933
+ else:
934
+ print(f" ⎿ ⊘ {tool_name} {reason}")
935
+
936
+
937
+ # ── Robot thinking / response header ──────────────────────────────────────────
938
+
939
+ def print_thinking_header(*, console, has_rich: bool, agent_name: str = "Aria", color: str = "#C08050") -> None:
940
+ """Print a subtle copper 'Aria ▸' header before each AI response stream.
941
+
942
+ Gives the response a clear starting-point rather than appearing inline.
943
+ Called once per turn, right before the first streaming token is printed.
944
+ """
945
+ if not has_rich:
946
+ return
947
+ console.print(f"[bold {color}]▣[/bold {color}] [dim {color}]{agent_name}[/dim {color}]", end=" ")
948
+
949
+
950
+ def print_done_footer(elapsed: float, *, console, has_rich: bool) -> None:
951
+ """Print a dim elapsed-time line after the response stream ends."""
952
+ if not has_rich:
953
+ return
954
+ console.print(f"\n[dim] ✓ {elapsed:.1f}s[/dim]")