aria-code 4.1.3__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 (284) hide show
  1. agents/__init__.py +32 -0
  2. agents/base.py +190 -0
  3. agents/deep/__init__.py +37 -0
  4. agents/deep/calibration_loop.py +144 -0
  5. agents/deep/critic.py +125 -0
  6. agents/deep/deepen.py +193 -0
  7. agents/deep/models.py +149 -0
  8. agents/deep/pipeline.py +164 -0
  9. agents/deep/quant_fusion.py +192 -0
  10. agents/deep/themes.py +95 -0
  11. agents/deep/tiers.py +106 -0
  12. agents/financial/__init__.py +10 -0
  13. agents/financial/catalyst.py +279 -0
  14. agents/financial/debate.py +145 -0
  15. agents/financial/earnings.py +303 -0
  16. agents/financial/fundamental.py +159 -0
  17. agents/financial/macro.py +99 -0
  18. agents/financial/news.py +207 -0
  19. agents/financial/risk.py +132 -0
  20. agents/financial/sector.py +279 -0
  21. agents/financial/synthesis.py +274 -0
  22. agents/financial/technical.py +258 -0
  23. agents/portfolio_agent.py +333 -0
  24. agents/realty/__init__.py +62 -0
  25. agents/realty/asset_diagnosis.py +150 -0
  26. agents/realty/business_match.py +165 -0
  27. agents/realty/cashflow_verify.py +208 -0
  28. agents/realty/contract_rules.py +209 -0
  29. agents/realty/energy_anomaly.py +188 -0
  30. agents/realty/exit_settlement.py +207 -0
  31. agents/realty/fulfillment_risk.py +205 -0
  32. agents/realty/ops_optimize.py +159 -0
  33. agents/realty/revenue_share.py +214 -0
  34. agents/registry.py +144 -0
  35. agents/sports/__init__.py +0 -0
  36. agents/sports/football_agent.py +169 -0
  37. agents/team.py +289 -0
  38. aliyun_data_client.py +660 -0
  39. apps/README.md +12 -0
  40. apps/__init__.py +2 -0
  41. apps/channels/README.md +15 -0
  42. apps/cli/README.md +13 -0
  43. apps/cli/__init__.py +2 -0
  44. apps/cli/bootstrap.py +99 -0
  45. apps/cli/codegen_paths.py +29 -0
  46. apps/cli/commands/__init__.py +16 -0
  47. apps/cli/commands/analysis_cmds.py +288 -0
  48. apps/cli/commands/backtest_cmds.py +1887 -0
  49. apps/cli/commands/broker_cmds.py +1154 -0
  50. apps/cli/commands/business_workflow_cmds.py +289 -0
  51. apps/cli/commands/catalog.py +84 -0
  52. apps/cli/commands/data_cmds.py +405 -0
  53. apps/cli/commands/diagnostic_cmds.py +179 -0
  54. apps/cli/commands/diagnostic_ops_cmds.py +696 -0
  55. apps/cli/commands/finance_render.py +12 -0
  56. apps/cli/commands/market.py +399 -0
  57. apps/cli/commands/market_cmds.py +1276 -0
  58. apps/cli/commands/market_context.py +425 -0
  59. apps/cli/commands/market_render.py +7 -0
  60. apps/cli/commands/model_cmds.py +1579 -0
  61. apps/cli/commands/ops_cmds.py +668 -0
  62. apps/cli/commands/portfolio_cmds.py +962 -0
  63. apps/cli/commands/report.py +377 -0
  64. apps/cli/commands/scaffold_templates.py +617 -0
  65. apps/cli/commands/session_cmds.py +179 -0
  66. apps/cli/commands/session_ux_cmds.py +280 -0
  67. apps/cli/commands/team.py +588 -0
  68. apps/cli/commands/team_render.py +8 -0
  69. apps/cli/commands/ui_cmds.py +358 -0
  70. apps/cli/commands/workflow_cmds.py +279 -0
  71. apps/cli/commands/workspace_cmds.py +1414 -0
  72. apps/cli/config_paths.py +70 -0
  73. apps/cli/config_store.py +61 -0
  74. apps/cli/deterministic.py +122 -0
  75. apps/cli/direct.py +48 -0
  76. apps/cli/github_app_auth.py +135 -0
  77. apps/cli/handlers/__init__.py +11 -0
  78. apps/cli/handlers/broker_handlers.py +122 -0
  79. apps/cli/handlers/chart_handlers.py +1309 -0
  80. apps/cli/handlers/market_handlers.py +2509 -0
  81. apps/cli/handlers/realty_handlers.py +114 -0
  82. apps/cli/handlers/strategy_advice.py +82 -0
  83. apps/cli/hooks.py +180 -0
  84. apps/cli/i18n.py +284 -0
  85. apps/cli/intent.py +136 -0
  86. apps/cli/intent_router.py +217 -0
  87. apps/cli/lifecycle_hooks.py +48 -0
  88. apps/cli/main.py +29 -0
  89. apps/cli/market_metadata.py +135 -0
  90. apps/cli/market_universe.py +265 -0
  91. apps/cli/message_processing.py +257 -0
  92. apps/cli/plan_mode.py +139 -0
  93. apps/cli/plotly_html.py +15 -0
  94. apps/cli/prediction_feedback.py +202 -0
  95. apps/cli/preflight.py +497 -0
  96. apps/cli/project_aria.py +60 -0
  97. apps/cli/prompts/__init__.py +0 -0
  98. apps/cli/prompts/coding.py +658 -0
  99. apps/cli/prompts/system_prompts.py +531 -0
  100. apps/cli/prompts/ui.py +434 -0
  101. apps/cli/providers/__init__.py +1 -0
  102. apps/cli/providers/base.py +271 -0
  103. apps/cli/providers/chat_routing.py +80 -0
  104. apps/cli/providers/llm/__init__.py +1 -0
  105. apps/cli/providers/llm/ollama_stream.py +1170 -0
  106. apps/cli/providers/llm/sse_stream.py +216 -0
  107. apps/cli/providers/runtime_bridge.py +185 -0
  108. apps/cli/runtime_consumer.py +489 -0
  109. apps/cli/session_export.py +87 -0
  110. apps/cli/session_jsonl.py +207 -0
  111. apps/cli/session_store.py +112 -0
  112. apps/cli/todo_tracker.py +190 -0
  113. apps/cli/tools/__init__.py +40 -0
  114. apps/cli/tools/context.py +46 -0
  115. apps/cli/tools/file_tools.py +112 -0
  116. apps/cli/tools/market_tools.py +549 -0
  117. apps/cli/tools/notebook_tools.py +111 -0
  118. apps/cli/tools/system_tools.py +669 -0
  119. apps/cli/tools/write_tools.py +715 -0
  120. apps/cli/tradingview_bridge.py +434 -0
  121. apps/cli/update_check.py +152 -0
  122. apps/cli/utils/__init__.py +0 -0
  123. apps/cli/utils/market_detect.py +1578 -0
  124. apps/daemon/README.md +14 -0
  125. apps/vscode/README.md +115 -0
  126. apps/vscode/package.json +70 -0
  127. aria_cli.py +11636 -0
  128. aria_code-4.1.3.dist-info/METADATA +952 -0
  129. aria_code-4.1.3.dist-info/RECORD +284 -0
  130. aria_code-4.1.3.dist-info/WHEEL +5 -0
  131. aria_code-4.1.3.dist-info/entry_points.txt +2 -0
  132. aria_code-4.1.3.dist-info/licenses/LICENSE +121 -0
  133. aria_code-4.1.3.dist-info/top_level.txt +50 -0
  134. aria_daemon.py +1295 -0
  135. aria_feishu_bot.py +1359 -0
  136. aria_relay_client.py +182 -0
  137. aria_relay_server.py +405 -0
  138. aria_telegram_bot.py +202 -0
  139. ariarc.py +328 -0
  140. artifacts.py +491 -0
  141. backtest_report.py +472 -0
  142. brokers/__init__.py +72 -0
  143. brokers/base.py +207 -0
  144. brokers/capabilities.py +264 -0
  145. brokers/cn/__init__.py +10 -0
  146. brokers/cn/easytrader_broker.py +193 -0
  147. brokers/cn/futu_broker.py +194 -0
  148. brokers/cn/longbridge_broker.py +190 -0
  149. brokers/cn/tiger_broker.py +196 -0
  150. brokers/cn/xtquant_broker.py +175 -0
  151. brokers/config.py +364 -0
  152. brokers/intl/__init__.py +5 -0
  153. brokers/intl/alpaca_broker.py +183 -0
  154. brokers/intl/ibkr_broker.py +215 -0
  155. brokers/intl/webull_broker.py +156 -0
  156. brokers/paper_broker.py +259 -0
  157. brokers/planning.py +296 -0
  158. brokers/registry.py +181 -0
  159. brokers/trading.py +237 -0
  160. change_store.py +127 -0
  161. command_safety.py +19 -0
  162. computer_use_tools.py +504 -0
  163. dashboard_generator.py +578 -0
  164. data_analysis_tools.py +808 -0
  165. data_cleaner.py +483 -0
  166. data_service.py +481 -0
  167. datasources/__init__.py +23 -0
  168. datasources/base.py +166 -0
  169. datasources/router.py +221 -0
  170. datasources/sources/__init__.py +15 -0
  171. datasources/sources/akshare_source.py +269 -0
  172. datasources/sources/alpha_vantage_source.py +202 -0
  173. datasources/sources/edgar_source.py +218 -0
  174. datasources/sources/finnhub_source.py +197 -0
  175. datasources/sources/fred_source.py +219 -0
  176. datasources/sources/tushare_source.py +141 -0
  177. datasources/sources/web_scraper_source.py +278 -0
  178. datasources/sources/world_bank_source.py +205 -0
  179. datasources/sources/yfinance_source.py +152 -0
  180. demo_player.py +204 -0
  181. doctor.py +508 -0
  182. file_analysis_tools.py +734 -0
  183. finance_formulas.py +389 -0
  184. football_data_client.py +1670 -0
  185. intent_classifier.py +358 -0
  186. local_finance_tools.py +3221 -0
  187. local_llm_provider.py +552 -0
  188. macro_tools.py +368 -0
  189. market_data_client.py +1899 -0
  190. mcp_client.py +506 -0
  191. memory_manager.py +245 -0
  192. model_capability.py +416 -0
  193. notification_tools.py +248 -0
  194. packages/__init__.py +23 -0
  195. packages/aria_agents/__init__.py +5 -0
  196. packages/aria_agents/manifest.py +69 -0
  197. packages/aria_core/__init__.py +34 -0
  198. packages/aria_core/architecture.py +192 -0
  199. packages/aria_core/export.py +124 -0
  200. packages/aria_core/manifest.py +65 -0
  201. packages/aria_infra/__init__.py +15 -0
  202. packages/aria_infra/arthera.py +52 -0
  203. packages/aria_infra/doctor.py +246 -0
  204. packages/aria_infra/product.py +37 -0
  205. packages/aria_mcp/__init__.py +25 -0
  206. packages/aria_mcp/bridge.py +38 -0
  207. packages/aria_mcp/config.py +97 -0
  208. packages/aria_mcp/tools.py +61 -0
  209. packages/aria_sdk/__init__.py +19 -0
  210. packages/aria_sdk/client.py +396 -0
  211. packages/aria_sdk/providers.py +70 -0
  212. packages/aria_sdk/streaming.py +73 -0
  213. packages/aria_sdk/types.py +86 -0
  214. packages/aria_services/__init__.py +55 -0
  215. packages/aria_services/context.py +258 -0
  216. packages/aria_services/data.py +11 -0
  217. packages/aria_services/provider_health.py +189 -0
  218. packages/aria_services/registry.py +213 -0
  219. packages/aria_services/usage.py +138 -0
  220. packages/aria_skills/__init__.py +5 -0
  221. packages/aria_skills/registry.py +59 -0
  222. packages/aria_tools/__init__.py +5 -0
  223. packages/aria_tools/registry.py +128 -0
  224. packages/quant_engine/__init__.py +6 -0
  225. packages/quant_engine/sports/__init__.py +72 -0
  226. packages/quant_engine/sports/calibrator.py +353 -0
  227. packages/quant_engine/sports/dixon_coles.py +234 -0
  228. packages/quant_engine/sports/elo.py +299 -0
  229. packages/quant_engine/sports/form.py +188 -0
  230. packages/quant_engine/sports/h2h.py +195 -0
  231. packages/quant_engine/sports/ml_model.py +354 -0
  232. packages/quant_engine/sports/predictor.py +311 -0
  233. packages/quant_engine/sports/tracker.py +664 -0
  234. packages/quant_engine/stochastic/__init__.py +27 -0
  235. packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
  236. packages/quant_engine/stochastic/ito_calculus.py +477 -0
  237. packages/quant_engine/stochastic/kelly_criterion.py +181 -0
  238. packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
  239. packages/quant_engine/stochastic/options_pricing.py +573 -0
  240. packages/quant_engine/stochastic/stochastic_processes.py +90 -0
  241. plan_utils.py +194 -0
  242. plugin_loader.py +328 -0
  243. portfolio_ledger.py +262 -0
  244. privacy/__init__.py +5 -0
  245. privacy/feedback.py +123 -0
  246. project_tools.py +525 -0
  247. providers/__init__.py +30 -0
  248. providers/llm/__init__.py +19 -0
  249. providers/llm/anthropic.py +184 -0
  250. providers/llm/base.py +139 -0
  251. providers/llm/ollama.py +128 -0
  252. providers/llm/openai_compat.py +282 -0
  253. providers/llm/registry.py +358 -0
  254. realty_data_tools.py +659 -0
  255. report_generator.py +1314 -0
  256. runtime/__init__.py +103 -0
  257. runtime/agent_loop.py +1183 -0
  258. runtime/approval.py +51 -0
  259. runtime/events.py +102 -0
  260. runtime/gateway.py +128 -0
  261. runtime/lsp.py +346 -0
  262. runtime/subagent.py +258 -0
  263. runtime/tool_executor.py +104 -0
  264. runtime/tool_policy.py +106 -0
  265. safety/__init__.py +21 -0
  266. safety/permissions.py +275 -0
  267. setup_wizard.py +653 -0
  268. strategy_vault.py +420 -0
  269. ui/__init__.py +100 -0
  270. ui/banner.py +310 -0
  271. ui/completer.py +391 -0
  272. ui/console.py +271 -0
  273. ui/image_render.py +243 -0
  274. ui/input_box.py +376 -0
  275. ui/picker.py +195 -0
  276. ui/render/__init__.py +11 -0
  277. ui/render/finance.py +1480 -0
  278. ui/render/market.py +225 -0
  279. ui/render/output.py +681 -0
  280. ui/render/team.py +346 -0
  281. ui/robot.py +235 -0
  282. workspace/__init__.py +6 -0
  283. workspace/files.py +170 -0
  284. workspace/verify.py +113 -0
ui/render/output.py ADDED
@@ -0,0 +1,681 @@
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_crypto_data", "get_forex_data",
27
+ "get_commodities_data", "get_futures_data", "calculate_factors",
28
+ "backtest_strategy", "cloud_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
+ # ── Tool display helpers ──────────────────────────────────────────────────────
38
+
39
+ def tool_display_kind(tool_name: str) -> str:
40
+ """Return a user-facing service/tool kind without exposing local targets."""
41
+ if tool_name.startswith("mcp__"):
42
+ return "MCP"
43
+ if tool_name in FINANCE_TOOL_NAMES:
44
+ return "finance tool"
45
+ if tool_name in {"web_search", "search_web"}:
46
+ return "web search"
47
+ if tool_name == "web_fetch":
48
+ return "web fetch"
49
+ if tool_name in {"read_file", "write_file", "edit_file", "list_files", "search_code"}:
50
+ return "file tool"
51
+ if tool_name == "run_command":
52
+ return "shell tool"
53
+ if tool_name.startswith("skill") or tool_name in {"TaskCreate", "TaskUpdate"}:
54
+ return "skill"
55
+ if tool_name.startswith("broker_") or tool_name in {"broker_query", "broker_order"}:
56
+ return "broker tool"
57
+ return "tool"
58
+
59
+
60
+ def tool_display_label(tool_name: str) -> str:
61
+ """Short label for activity UI: tool name plus its service kind."""
62
+ if tool_name.startswith("mcp__"):
63
+ parts = tool_name.split("__")
64
+ if len(parts) >= 3:
65
+ return f"{parts[1]} · {parts[2].replace('_', ' ')} · MCP"
66
+ return "MCP"
67
+ return f"{tool_name} · {tool_display_kind(tool_name)}"
68
+
69
+
70
+ def format_turn_footer(metadata, *, mode: str = "compact", copy_available: bool = False) -> str:
71
+ """Return the post-response status line.
72
+
73
+ ``full`` keeps the historical token-heavy line for debugging. ``compact``
74
+ is the default interactive UI: elapsed time, provider, tools, and /copy.
75
+ """
76
+ mode = (mode or "compact").strip().lower()
77
+ if mode in {"off", "none", "false", "0"}:
78
+ return ""
79
+ parts = list(getattr(metadata, "parts", []) or [])
80
+ if mode in {"full", "debug", "verbose"}:
81
+ footer = " · ".join(parts)
82
+ if copy_available:
83
+ footer = f"{footer} /copy" if footer else "/copy"
84
+ return footer
85
+
86
+ elapsed = parts[0] if parts else ""
87
+ provider = str(getattr(metadata, "provider", "") or "").strip()
88
+ tools = list(getattr(metadata, "tools", []) or [])
89
+ out: list[str] = []
90
+ if elapsed:
91
+ out.append(elapsed)
92
+ if provider and provider not in {"aws", "local"}:
93
+ out.append(provider)
94
+ if tools:
95
+ shown = " ".join(str(t) for t in tools[:3])
96
+ if len(tools) > 3:
97
+ shown += f" +{len(tools) - 3}"
98
+ out.append(shown)
99
+ if copy_available:
100
+ out.append("/copy")
101
+ return " · ".join(out)
102
+
103
+
104
+ def display_path(path: object, *, fallback: str = "file") -> str:
105
+ """Return a path-safe display value for user-facing UI."""
106
+ if not path:
107
+ return fallback
108
+ try:
109
+ name = pathlib.Path(str(path)).name
110
+ except Exception:
111
+ name = ""
112
+ return name or fallback
113
+
114
+
115
+ # ── Error helpers ──────────────────────────────────────────────────────────────
116
+
117
+ def clean_tool_error_message(error: object) -> str:
118
+ raw = str(error or "failed").strip()
119
+ low = raw.lower()
120
+ if not raw:
121
+ return "操作失败"
122
+ if "curl: (28)" in low or "timed out" in low or "timeout" in low:
123
+ return "请求超时,数据源暂时不可用。请稍后重试或运行 /health 检查服务。"
124
+ if "connection refused" in low:
125
+ return "连接被拒绝,服务暂时不可用。请检查本地服务或网络。"
126
+ if "connection aborted" in low or "remotedisconnected" in low:
127
+ return "网络连接中断,数据源未完成响应。请稍后重试。"
128
+ # Generic connection / proxy / DNS failures — collapse the verbose
129
+ # urllib3 HTTPSConnectionPool(...) dump into a single readable line.
130
+ if any(s in low for s in (
131
+ "httpsconnectionpool", "httpconnectionpool", "max retries exceeded",
132
+ "proxyerror", "failed to establish a new connection",
133
+ "nameresolutionerror", "getaddrinfo failed", "newconnectionerror",
134
+ )):
135
+ import re as _re3
136
+ _host = _re3.search(r"host=['\"]([^'\"]+)['\"]", raw)
137
+ _hint = f"(数据源 {_host.group(1)})" if _host else ""
138
+ return f"数据源连接失败{_hint},可能是网络或代理问题。请检查网络后重试。"
139
+ if "rate" in low or "429" in low or "too many requests" in low:
140
+ return "数据源请求频率受限,请稍后重试。"
141
+ # Collapse verbose HTTP error strings: "web_fetch failed: 401 Client Error: Unauthorized for url: https://..."
142
+ import re as _re
143
+ _http = _re.match(r"web_fetch failed:\s*(\d{3})\s+\w[\w\s]+?:\s*([\w\s]+?)(?:\s+for url:.*)?$", raw, _re.I)
144
+ if _http:
145
+ code, phrase = _http.group(1), _http.group(2).strip()
146
+ return f"HTTP {code} {phrase}"
147
+ if "traceback" in low:
148
+ return raw.splitlines()[-1][:160] if raw.splitlines() else "运行失败"
149
+ return raw[:200]
150
+
151
+
152
+ def error_hint(error: str, context: str = "") -> str:
153
+ err_lower = error.lower() if error else ""
154
+ if "connection" in err_lower or "refused" in err_lower or "unreachable" in err_lower:
155
+ return "Hint: Backend unreachable. Try /health or check your network."
156
+ if "timeout" in err_lower or "timed out" in err_lower:
157
+ return "Hint: Request timed out. Try again or check /health."
158
+ # External web pages that block scraping (paywall / anti-bot) — NOT an Aria
159
+ # login problem, so /login must not be suggested.
160
+ _is_web = any(m in err_lower for m in (
161
+ "http://", "https://", "www.", ".com", ".org", ".net",
162
+ "web_fetch", "web fetch", "forbidden",
163
+ ))
164
+ if "401" in err_lower or "unauthorized" in err_lower:
165
+ if any(h in err_lower for h in ("finnhub", "alphavantage", "polygon", "api/v1", "api/v2/finance")):
166
+ return "Hint: API key required — /apikey set finnhub <KEY> (free at finnhub.io)"
167
+ if _is_web:
168
+ return "Hint: This site blocks automated access (paywall/anti-bot). Try another source."
169
+ return "Hint: Authentication required. Run /login to sign in."
170
+ if "403" in err_lower or "forbidden" in err_lower:
171
+ if _is_web:
172
+ return "Hint: This site blocks automated access (paywall/anti-bot). Try another source."
173
+ return "Hint: Access denied. Check your API key or subscription."
174
+ if "429" in err_lower or "rate" in err_lower:
175
+ return "Hint: Rate limited. Wait a moment and try again."
176
+ if ("ollama" in err_lower or "ollama http" in err_lower) and (
177
+ "not found" in err_lower or "404" in err_lower
178
+ ):
179
+ m = re.search(r"model ['\"]?([^'\"]+)['\"]? not found", err_lower)
180
+ model_hint = m.group(1) if m else "the requested model"
181
+ try:
182
+ from local_llm_provider import list_ollama_models
183
+ available = list_ollama_models("http://localhost:11434")
184
+ if available:
185
+ suggestion = available[0]
186
+ return (
187
+ f"Hint: Ollama model '{model_hint}' not found.\n"
188
+ f" Available: {', '.join(available[:4])}\n"
189
+ f" Run: /config model {suggestion}"
190
+ )
191
+ except Exception:
192
+ pass
193
+ return (
194
+ f"Hint: Ollama model not found. Run `ollama list` to see available models.\n"
195
+ f" Or pull one: ollama pull qwen2.5-coder:7b"
196
+ )
197
+ # "File not found" is a path error. Tell the model firmly NOT to keep
198
+ # guessing filenames (it otherwise loops app.py→script.py→main.py…).
199
+ if "file not found" in err_lower or "no such file" in err_lower:
200
+ return ("Hint: This file does not exist. Do NOT guess other filenames — "
201
+ "list the directory first, or this question may not need a file at all.")
202
+ if "404" in err_lower and context == "tool":
203
+ return "Hint: Tool not available. Check /tools for available tools."
204
+ if "not found" in err_lower and context == "session":
205
+ return "Hint: Session not found. Run /sessions to list available."
206
+ if "404" in err_lower or ("not found" in err_lower and context not in ("tool", "")):
207
+ return "Hint: Resource not found. Check the symbol or path."
208
+ if "no data" in err_lower or "no result" in err_lower:
209
+ return "Hint: No data returned. Verify the symbol spelling."
210
+ if "500" in err_lower or "internal" in err_lower:
211
+ return "Hint: Server error. Try again in a moment or /health to check."
212
+ if context == "login":
213
+ return "Hint: Check email/password. Usage: /login email password"
214
+ return ""
215
+
216
+
217
+ # ── Error panel ────────────────────────────────────────────────────────────────
218
+
219
+ def print_error(
220
+ msg: str,
221
+ context: str = "",
222
+ *,
223
+ console,
224
+ has_rich: bool,
225
+ rich_box,
226
+ ) -> None:
227
+ if has_rich:
228
+ from rich.panel import Panel
229
+ hint = error_hint(msg, context)
230
+ body = f"[red]{msg}[/red]"
231
+ if hint:
232
+ body += f"\n[dim]{hint}[/dim]"
233
+ console.print(Panel(body, border_style="red", box=rich_box.ROUNDED, padding=(0, 1)))
234
+ else:
235
+ print(msg)
236
+
237
+
238
+ # ── Tool result ────────────────────────────────────────────────────────────────
239
+
240
+ def print_tool_result(
241
+ tool_name: str,
242
+ result: dict,
243
+ elapsed: float = 0,
244
+ params: dict = None,
245
+ *,
246
+ console,
247
+ has_rich: bool,
248
+ rich_box,
249
+ print_finance_fn, # callable(tool_name, result) for finance tools
250
+ bot_mode: bool = False,
251
+ ) -> None:
252
+ """Render a tool result summary — Codex-style ⎿ tree connector."""
253
+ if bot_mode:
254
+ return
255
+
256
+ ts = f" [dim]{elapsed:.1f}s[/dim]" if elapsed >= 0.1 else ""
257
+ ts_plain = f" {elapsed:.1f}s" if elapsed >= 0.1 else ""
258
+ params = params or {}
259
+
260
+ if tool_name in FINANCE_TOOL_NAMES:
261
+ print_finance_fn(tool_name, result)
262
+ if ts and has_rich:
263
+ console.print(f" [dim]⎿[/dim]{ts}")
264
+ return
265
+
266
+ if result.get("success"):
267
+ data = result.get("data", {})
268
+
269
+ if tool_name == "write_file":
270
+ lines = data.get("lines") or (params.get("content", "").count("\n") + 1 if params.get("content") else 0)
271
+ size = data.get("size_bytes") or len((params.get("content", "") or "").encode())
272
+ size_str = f"{size}B" if size < 1024 else f"{size // 1024}KB"
273
+ if has_rich:
274
+ console.print(f" [dim]⎿[/dim] [green]✓[/green] [dim]file tool {lines} lines {size_str}[/dim]{ts}")
275
+ else:
276
+ print(f" ⎿ ✓ file tool {lines} lines {size_str}{ts_plain}")
277
+
278
+ elif tool_name == "edit_file":
279
+ old = params.get("old_string", "")
280
+ new = params.get("new_string", "")
281
+ if old and new and has_rich:
282
+ import re as _re_diff
283
+ diff = list(difflib.unified_diff(
284
+ old.splitlines(),
285
+ new.splitlines(),
286
+ lineterm="",
287
+ ))
288
+ if diff:
289
+ _hdr = " [dim]⎿[/dim] [#C08050]file tool[/#C08050]"
290
+ console.print(f"{_hdr}{ts}")
291
+ o_ln = n_ln = 0
292
+ for line in diff[2:]:
293
+ # Hunk header: @@ -old_start,n +new_start,n @@
294
+ m = _re_diff.match(r"@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@", line)
295
+ if m:
296
+ o_ln, n_ln = int(m.group(1)), int(m.group(2))
297
+ console.print(f" [dim]…[/dim]")
298
+ continue
299
+ body = line[1:].rstrip()
300
+ if line.startswith("+"):
301
+ console.print(f" [dim]{n_ln:>4}[/dim] [green]+ {body}[/green]")
302
+ n_ln += 1
303
+ elif line.startswith("-"):
304
+ console.print(f" [dim]{o_ln:>4}[/dim] [red]- {body}[/red]")
305
+ o_ln += 1
306
+ else:
307
+ console.print(f" [dim]{n_ln:>4}[/dim] [dim] {body}[/dim]")
308
+ o_ln += 1
309
+ n_ln += 1
310
+ else:
311
+ console.print(f" [dim]⎿ no change[/dim]{ts}")
312
+ elif has_rich:
313
+ console.print(f" [dim]⎿ edited[/dim]{ts}")
314
+ else:
315
+ print(f" ⎿ edited{ts_plain}")
316
+
317
+ elif tool_name == "run_command":
318
+ stdout = data.get("stdout", "").strip()
319
+ returncode = data.get("returncode", data.get("exit_code", 0))
320
+ if has_rich:
321
+ from rich.panel import Panel
322
+ rc_color = "green" if returncode == 0 else "red"
323
+ rc_icon = "✓" if returncode == 0 else "✗"
324
+ console.print(f" [dim]⎿[/dim] [{rc_color}]{rc_icon} exit {returncode}[/{rc_color}]{ts}")
325
+ if stdout:
326
+ out_lines = stdout.splitlines()
327
+ if len(out_lines) > 3:
328
+ truncated = "\n".join(out_lines[:12])
329
+ if len(out_lines) > 12:
330
+ truncated += f"\n[dim]… +{len(out_lines) - 12} lines[/dim]"
331
+ if data.get("full_output_path"):
332
+ truncated += f"\n[dim]full output saved: {data.get('full_output_path')}[/dim]"
333
+ console.print(Panel(
334
+ f"[dim]{truncated}[/dim]",
335
+ border_style="dim",
336
+ box=rich_box.SIMPLE,
337
+ padding=(0, 1),
338
+ ))
339
+ else:
340
+ for ol in out_lines:
341
+ console.print(f" [dim]{ol[:120]}[/dim]")
342
+ if data.get("full_output_path"):
343
+ console.print(" [dim]full output saved[/dim]")
344
+ else:
345
+ print(f" ⎿ exit {returncode}{ts_plain}")
346
+ for ol in stdout.splitlines()[:4]:
347
+ print(f" {ol[:100]}")
348
+
349
+ elif tool_name == "read_file":
350
+ lines = data.get("lines", 0)
351
+ if has_rich:
352
+ console.print(f" [dim]⎿ file tool {lines} lines[/dim]{ts}")
353
+ else:
354
+ print(f" ⎿ file tool {lines} lines{ts_plain}")
355
+
356
+ elif tool_name == "list_files":
357
+ count = data.get("count", 0)
358
+ if has_rich:
359
+ color = "yellow" if count == 0 else "dim"
360
+ msg = "0 items — no matches" if count == 0 else f"{count} items"
361
+ console.print(f" [{color}]⎿ {msg}[/{color}]{ts}")
362
+ else:
363
+ print(f" ⎿ {count} items{ts_plain}")
364
+
365
+ elif tool_name == "search_code":
366
+ matches = len(data.get("matches", []))
367
+ if has_rich:
368
+ console.print(f" [dim]⎿ {matches} matches[/dim]{ts}")
369
+ else:
370
+ print(f" ⎿ {matches} matches{ts_plain}")
371
+
372
+ elif tool_name == "web_fetch":
373
+ length = data.get("length", 0)
374
+ trunc = data.get("truncated", False)
375
+ len_str = f" {length:,} chars" if length else ""
376
+ trunc_str = " [dim]truncated[/dim]" if trunc else ""
377
+ if has_rich:
378
+ console.print(f" [dim]⎿ web fetch{len_str}[/dim]{trunc_str}{ts}")
379
+ else:
380
+ print(f" ⎿ web fetch{ts_plain}")
381
+
382
+ elif tool_name in ("web_search", "search_web"):
383
+ results = data.get("results", [])
384
+ count = len(results)
385
+ if has_rich:
386
+ console.print(f" [dim]⎿ {count} results[/dim]{ts}")
387
+ else:
388
+ print(f" ⎿ {count} results{ts_plain}")
389
+
390
+ else:
391
+ short = tool_display_kind(tool_name)
392
+ if has_rich:
393
+ console.print(f" [dim]⎿ {short} done[/dim]{ts}")
394
+ else:
395
+ print(f" ⎿ done{ts_plain}")
396
+
397
+ else:
398
+ error = clean_tool_error_message(result.get("error", "failed"))
399
+ hint = error_hint(str(error), context="tool")
400
+ if has_rich:
401
+ console.print(f" [dim]⎿[/dim] [red]✗ {error[:120]}[/red]")
402
+ if hint:
403
+ console.print(f" [dim]{hint}[/dim]")
404
+ else:
405
+ print(f" ⎿ ✗ {error[:80]}")
406
+
407
+
408
+ # ── Activity group (OpenClaw-style batch summary) ──────────────────────────────
409
+
410
+ def _one_line_tool_summary(
411
+ tool_name: str,
412
+ result: dict,
413
+ elapsed: float,
414
+ params: dict,
415
+ ) -> tuple[str, str]:
416
+ """Return (status_markup, detail_markup) for one tool in an activity table."""
417
+ params = params or {}
418
+ ts = f"[dim] {elapsed:.1f}s[/dim]" if elapsed >= 0.1 else ""
419
+
420
+ if not result.get("success"):
421
+ error = clean_tool_error_message(result.get("error", "failed"))
422
+ return "[red]✗[/red]", f"[red]{error[:80]}[/red]{ts}"
423
+
424
+ data = result.get("data", {})
425
+ kind = tool_display_kind(tool_name)
426
+
427
+ if tool_name == "write_file":
428
+ lines = data.get("lines") or (params.get("content", "").count("\n") + 1 if params.get("content") else 0)
429
+ size = data.get("size_bytes") or len((params.get("content", "") or "").encode())
430
+ size_str = f"{size}B" if size < 1024 else f"{size // 1024}KB"
431
+ return "[green]✓[/green]", f"[dim]{kind} {lines} lines {size_str}[/dim]{ts}"
432
+
433
+ elif tool_name == "edit_file":
434
+ return "[green]✓[/green]", f"[dim]edited {kind}[/dim]{ts}"
435
+
436
+ elif tool_name == "run_command":
437
+ rc = data.get("returncode", data.get("exit_code", 0))
438
+ icon = "[green]✓[/green]" if rc == 0 else "[red]✗[/red]"
439
+ color = "green" if rc == 0 else "red"
440
+ suffix = " [dim]· full output saved[/dim]" if data.get("full_output_path") else ""
441
+ return icon, f"[{color}]exit {rc}[/{color}]{suffix}{ts}"
442
+
443
+ elif tool_name == "read_file":
444
+ lines = data.get("lines", 0)
445
+ return "[green]✓[/green]", f"[dim]{kind} {lines} lines[/dim]{ts}"
446
+
447
+ elif tool_name == "list_files":
448
+ count = data.get("count", 0)
449
+ color = "yellow" if count == 0 else "dim"
450
+ msg = "no matches" if count == 0 else f"{count} items"
451
+ return "[green]✓[/green]", f"[{color}]{msg}[/{color}]{ts}"
452
+
453
+ elif tool_name == "search_code":
454
+ matches = len(data.get("matches", []))
455
+ return "[green]✓[/green]", f"[dim]{matches} matches[/dim]{ts}"
456
+
457
+ elif tool_name == "web_fetch":
458
+ length = data.get("length", 0)
459
+ len_s = f" {length:,}c" if length else ""
460
+ return "[green]✓[/green]", f"[dim]{kind}{len_s}[/dim]{ts}"
461
+
462
+ elif tool_name in ("web_search", "search_web"):
463
+ count = len(data.get("results", []))
464
+ return "[green]✓[/green]", f"[dim]{count} results[/dim]{ts}"
465
+
466
+ else:
467
+ return "[green]✓[/green]", f"[dim]{kind} done[/dim]{ts}"
468
+
469
+
470
+ def print_tool_activity_group(
471
+ results: list, # list of (tool_name, result, elapsed, params)
472
+ *,
473
+ console,
474
+ has_rich: bool,
475
+ rich_box,
476
+ print_finance_fn,
477
+ bot_mode: bool = False,
478
+ ) -> None:
479
+ """Render multiple tool results as a compact Activity block (OpenClaw style).
480
+
481
+ For N >= 2 tools: prints a titled table.
482
+ For N == 1: delegates to print_tool_result (single-line).
483
+ """
484
+ if bot_mode or not results:
485
+ return
486
+
487
+ if len(results) == 1:
488
+ tool_name, result, elapsed, params = results[0]
489
+ print_tool_result(tool_name, result, elapsed, params,
490
+ console=console, has_rich=has_rich, rich_box=rich_box,
491
+ print_finance_fn=print_finance_fn, bot_mode=bot_mode)
492
+ return
493
+
494
+ total_elapsed = sum(e for _, _, e, _ in results)
495
+ n = len(results)
496
+
497
+ # Finance tools: print with dedicated renderer, then add to activity table
498
+ finance_rows = []
499
+ for tool_name, result, elapsed, params in results:
500
+ if tool_name in FINANCE_TOOL_NAMES:
501
+ print_finance_fn(tool_name, result)
502
+ finance_rows.append(tool_name)
503
+
504
+ if has_rich:
505
+ from rich.table import Table
506
+ ts_total = f" [dim]{total_elapsed:.1f}s[/dim]" if total_elapsed >= 0.1 else ""
507
+ header = f"[dim]Activity · {n} tools[/dim]{ts_total}"
508
+ console.print(f"\n {header}")
509
+ tbl = Table.grid(padding=(0, 2))
510
+ tbl.add_column(no_wrap=True, min_width=14, style="dim") # tool name
511
+ tbl.add_column(no_wrap=True, min_width=2) # status icon
512
+ tbl.add_column() # detail
513
+
514
+ from collections import OrderedDict
515
+ _mcp_groups: "OrderedDict[str, list]" = OrderedDict()
516
+ for tool_name, result, elapsed, params in results:
517
+ if tool_name in finance_rows:
518
+ icon = "[green]✓[/green]" if result.get("success") else "[red]✗[/red]"
519
+ tbl.add_row(tool_name, icon, "")
520
+ elif tool_name.startswith("mcp__"):
521
+ # Defer MCP calls — collapse per server below
522
+ _server = tool_name.split("__")[1] if len(tool_name.split("__")) >= 2 else "mcp"
523
+ _mcp_groups.setdefault(_server, []).append((tool_name, result))
524
+ else:
525
+ icon, detail = _one_line_tool_summary(tool_name, result, elapsed, params)
526
+ tbl.add_row(f"[dim]{tool_name}[/dim]", icon, detail)
527
+
528
+ # Collapsed MCP rows: "server · tool" for one, "called N times" for many
529
+ for _server, _calls in _mcp_groups.items():
530
+ _all_ok = all(r.get("success") for _, r in _calls)
531
+ _icon = "[green]✓[/green]" if _all_ok else "[red]✗[/red]"
532
+ if len(_calls) == 1:
533
+ _tn = _calls[0][0].split("__")
534
+ _label = _tn[2].replace("_", " ") if len(_tn) >= 3 else _server
535
+ tbl.add_row(f"[dim]{_server}[/dim]", _icon, f"[dim]{_label} · MCP[/dim]")
536
+ else:
537
+ tbl.add_row(f"[dim]{_server}[/dim]", _icon,
538
+ f"[dim]called {len(_calls)} times · MCP[/dim]")
539
+
540
+ from rich.padding import Padding
541
+ console.print(Padding(tbl, (0, 0, 0, 4)))
542
+
543
+ # For run_command with stdout, still print the output panel
544
+ for tool_name, result, elapsed, params in results:
545
+ if tool_name == "run_command" and result.get("success"):
546
+ stdout = result.get("data", {}).get("stdout", "").strip()
547
+ if stdout:
548
+ from rich.panel import Panel
549
+ out_lines = stdout.splitlines()
550
+ if len(out_lines) > 3:
551
+ truncated = "\n".join(out_lines[:12])
552
+ if len(out_lines) > 12:
553
+ truncated += f"\n[dim]… +{len(out_lines) - 12} lines[/dim]"
554
+ if result.get("data", {}).get("full_output_path"):
555
+ truncated += f"\n[dim]full output saved: {result.get('data', {}).get('full_output_path')}[/dim]"
556
+ console.print(Panel(f"[dim]{truncated}[/dim]",
557
+ border_style="dim", box=rich_box.SIMPLE,
558
+ padding=(0, 1)))
559
+ else:
560
+ for ol in out_lines:
561
+ console.print(f" [dim]{ol[:120]}[/dim]")
562
+
563
+ # For edit_file, still print diff
564
+ elif tool_name == "edit_file" and result.get("success"):
565
+ old = (params or {}).get("old_string", "")
566
+ new = (params or {}).get("new_string", "")
567
+ if old and new:
568
+ diff = list(difflib.unified_diff(
569
+ old.splitlines(keepends=True),
570
+ new.splitlines(keepends=True),
571
+ lineterm="",
572
+ ))
573
+ for line in diff[2:]:
574
+ if line.startswith("+"):
575
+ console.print(f" [green]{line.rstrip()}[/green]")
576
+ elif line.startswith("-"):
577
+ console.print(f" [red]{line.rstrip()}[/red]")
578
+ else:
579
+ ts_total = f" {total_elapsed:.1f}s" if total_elapsed >= 0.1 else ""
580
+ print(f"\n Activity · {n} tools{ts_total}")
581
+ for tool_name, result, elapsed, params in results:
582
+ icon, detail = _one_line_tool_summary(tool_name, result, elapsed, params)
583
+ detail_plain = re.sub(r"\[/?[^\]]+\]", "", detail)
584
+ icon_plain = "✓" if result.get("success") else "✗"
585
+ print(f" {tool_name:<18}{icon_plain} {detail_plain}")
586
+
587
+
588
+ # ── Fallback / model-switch toast ──────────────────────────────────────────────
589
+
590
+ def print_fallback_toast(
591
+ from_provider: str,
592
+ to_provider: str,
593
+ reason: str = "",
594
+ *,
595
+ console,
596
+ has_rich: bool,
597
+ ) -> None:
598
+ """Show a transient yellow notification when the active model/provider switches."""
599
+ if not has_rich:
600
+ print(f"\n ⚡ 模型切换 {from_provider} → {to_provider}{(' ' + reason) if reason else ''}")
601
+ return
602
+ body = f"[bold #C08050]⚡[/bold #C08050] [#C08050]{from_provider}[/#C08050] [dim]→[/dim] [#C08050]{to_provider}[/#C08050]"
603
+ if reason:
604
+ body += f"\n [dim]{reason}[/dim]"
605
+ console.print(f"\n {body}")
606
+
607
+
608
+ # ── Context pressure warning ───────────────────────────────────────────────────
609
+
610
+ _CTX_WARNED: dict[str, float] = {} # session_id → last warn time
611
+
612
+ def print_context_warning(
613
+ est_tokens: int,
614
+ max_tokens: int,
615
+ *,
616
+ console,
617
+ has_rich: bool,
618
+ session_id: str = "",
619
+ cooldown: float = 120.0, # only warn once every 2 min per session
620
+ ) -> None:
621
+ """Warn when context is >85% full; rate-limited to avoid spam."""
622
+ if max_tokens <= 0:
623
+ return
624
+ ratio = est_tokens / max_tokens
625
+ if ratio < 0.85:
626
+ return
627
+ now = time.monotonic()
628
+ if now - _CTX_WARNED.get(session_id, 0) < cooldown:
629
+ return
630
+ _CTX_WARNED[session_id] = now
631
+
632
+ def _k(n: int) -> str:
633
+ return f"{n // 1000}K" if n >= 1000 else str(n)
634
+
635
+ pct = int(ratio * 100)
636
+ if has_rich:
637
+ color = "red" if ratio >= 0.95 else "#C08050"
638
+ icon = "●" if ratio >= 0.95 else "⚠"
639
+ msg = f" [{color}]{icon} 上下文 {pct}% 已满 ({_k(est_tokens)}/{_k(max_tokens)} tokens)[/{color}]"
640
+ msg += " [dim]→ /compact 压缩历史 /clear 重置[/dim]"
641
+ console.print(msg)
642
+ else:
643
+ print(f" ⚠ 上下文 {pct}% ({_k(est_tokens)}/{_k(max_tokens)} tokens) — /compact 或 /clear")
644
+
645
+
646
+ # ── Blocked / cancelled tool visual ───────────────────────────────────────────
647
+
648
+ def print_tool_blocked(
649
+ tool_name: str,
650
+ reason: str = "用户取消",
651
+ *,
652
+ console,
653
+ has_rich: bool,
654
+ ) -> None:
655
+ """Show a styled 'Blocked' line when tool execution is denied or cancelled."""
656
+ if has_rich:
657
+ console.print(
658
+ f" [dim]⎿[/dim] [#C08050]⊘ {tool_name}[/#C08050] [dim]{reason}[/dim]"
659
+ )
660
+ else:
661
+ print(f" ⎿ ⊘ {tool_name} {reason}")
662
+
663
+
664
+ # ── Robot thinking / response header ──────────────────────────────────────────
665
+
666
+ def print_thinking_header(*, console, has_rich: bool) -> None:
667
+ """Print a subtle copper 'Aria ▸' header before each AI response stream.
668
+
669
+ Gives the response a clear starting-point rather than appearing inline.
670
+ Called once per turn, right before the first streaming token is printed.
671
+ """
672
+ if not has_rich:
673
+ return
674
+ console.print("[bold #C08050]▣[/bold #C08050] [dim #C08050]Aria[/dim #C08050]", end=" ")
675
+
676
+
677
+ def print_done_footer(elapsed: float, *, console, has_rich: bool) -> None:
678
+ """Print a dim elapsed-time line after the response stream ends."""
679
+ if not has_rich:
680
+ return
681
+ console.print(f"\n[dim] ✓ {elapsed:.1f}s[/dim]")