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/completer.py ADDED
@@ -0,0 +1,391 @@
1
+ """prompt_toolkit completer and base style for the Aria REPL.
2
+
3
+ Improvements over the original:
4
+ - Instant popup: triggers as soon as "/" is typed (no extra keypress needed)
5
+ - Fuzzy match: "/ch" matches /chart, /check, /watch; "/ta" matches /stat-arb
6
+ - Matched chars highlighted in amber so they're visible in all rows
7
+ - Results ranked: exact-prefix > fuzzy-command > description-fuzzy
8
+ - Category tag shown in display_meta (市场 / 分析 / 量化 / 数据源 …)
9
+
10
+ from ui.completer import AriaPTCompleter, ARIA_PT_STYLE
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Iterator, List, Tuple
16
+
17
+ from ui.console import HAS_PT
18
+
19
+ # ── Category map ────────────────────────────────────────────────────────────
20
+ # Keyed on command name fragments; first match wins.
21
+ _CATS: List[Tuple[Tuple[str, ...], str]] = [
22
+ (("/quote", "/market", "/macro", "/watch", "/alert", "/hot", "/indices",
23
+ "/cn", "/hk", "/crypto", "/forex", "/commodity", "/funding", "/feargreed",
24
+ "/edgar", "/datasource"), "市场"),
25
+ (("/team", "/analyze", "/options", "/factor", "/ta", "/ichimoku",
26
+ "/peer", "/quality", "/risk", "/signal", "/predict", "/earnings",
27
+ "/insights", "/deep", "/morning", "/trade-idea", "/research"), "分析"),
28
+ (("/backtest", "/wf", "/compare", "/execution", "/stat-arb",
29
+ "/ptbt", "/corr", "/optimize", "/stress", "/auto-strategy",
30
+ "/portfolio", "/journal"), "量化"),
31
+ (("/chart", "/report", "/shortterm", "/longterm", "/cloudbt"), "图表"),
32
+ (("/project", "/file", "/run", "/code", "/scaffold", "/init",
33
+ "/review", "/vision", "/browser", "/web"), "工具"),
34
+ (("/config", "/model", "/apikey", "/setup", "/local", "/mcp",
35
+ "/memory", "/cost", "/version"), "设置"),
36
+ (("/help", "/clear", "/btw", "/recap", "/exit", "/quit", "/history", "/session",
37
+ "/bug", "/feedback", "/privacy"), "系统"),
38
+ ]
39
+
40
+ _CAT_BADGE: dict[str, str] = {
41
+ "市场": "mkt",
42
+ "分析": "ana",
43
+ "量化": "qnt",
44
+ "图表": "viz",
45
+ "工具": "dev",
46
+ "设置": "cfg",
47
+ "系统": "sys",
48
+ }
49
+
50
+
51
+ def _get_cat(name: str) -> str:
52
+ for prefixes, cat in _CATS:
53
+ for p in prefixes:
54
+ if name.startswith(p):
55
+ return cat
56
+ return ""
57
+
58
+
59
+ # ── Fuzzy matching ───────────────────────────────────────────────────────────
60
+
61
+ def _fuzzy(pattern: str, text: str) -> Tuple[bool, List[int]]:
62
+ """
63
+ Sequential fuzzy match — pattern chars must appear in order in text.
64
+ Returns (matched, indices_of_matched_chars_in_text).
65
+ Consecutive matches score higher because they produce a compact index list.
66
+ """
67
+ if not pattern:
68
+ return True, []
69
+ pi = 0
70
+ indices: List[int] = []
71
+ for i, ch in enumerate(text):
72
+ if ch.lower() == pattern[pi].lower():
73
+ indices.append(i)
74
+ pi += 1
75
+ if pi == len(pattern):
76
+ return True, indices
77
+ return False, []
78
+
79
+
80
+ def _score(name: str, pattern: str, indices: List[int]) -> int:
81
+ """
82
+ Lower score = better.
83
+ 0 exact match
84
+ 1 exact prefix (/ch → /chart)
85
+ 5 word-segment exact match (/arb → /stat-arb because '-arb' segment)
86
+ 10 consecutive run from position 0
87
+ 12+ consecutive run from a word boundary
88
+ 15+ consecutive run from elsewhere
89
+ 20+ non-consecutive from a word boundary
90
+ 30+ scattered fuzzy match
91
+ """
92
+ if name == pattern:
93
+ return 0
94
+ if name.startswith(pattern):
95
+ return 1
96
+ if not indices:
97
+ return 99
98
+
99
+ # Word-segment exact match: pattern matches a whole segment after "-" or "_"
100
+ bare = name.lstrip("/")
101
+ pat_bare = pattern.lstrip("/")
102
+ for sep in ("-", "_"):
103
+ for seg in bare.split(sep)[1:]: # skip first segment (already caught by prefix)
104
+ if seg == pat_bare or seg.startswith(pat_bare):
105
+ return 5 + len(sep) # score 6 for "-", 6 for "_"
106
+
107
+ start = indices[0]
108
+ consecutive = (indices == list(range(start, start + len(indices))))
109
+ at_boundary = start == 0 or (start > 0 and bare[start - 1] in "-_")
110
+
111
+ if consecutive:
112
+ if start == 0:
113
+ return 10
114
+ if at_boundary:
115
+ return 12 + start
116
+ return 15 + start
117
+ if at_boundary:
118
+ return 20 + start
119
+ return 30 + start
120
+
121
+
122
+ # ── FormattedText display builder ────────────────────────────────────────────
123
+
124
+ def _highlighted(name: str, matched_indices: List[int]) -> List[Tuple[str, str]]:
125
+ """
126
+ Build a FormattedText list: matched chars get 'class:fz-hi', rest are plain.
127
+ """
128
+ idx_set = set(matched_indices)
129
+ parts: List[Tuple[str, str]] = []
130
+ for i, ch in enumerate(name):
131
+ if i in idx_set:
132
+ parts.append(("class:fz-hi", ch))
133
+ else:
134
+ parts.append(("", ch))
135
+ return parts
136
+
137
+
138
+ if HAS_PT:
139
+ import os as _os
140
+ from prompt_toolkit.completion import Completer, Completion
141
+ from prompt_toolkit.formatted_text import FormattedText
142
+ from prompt_toolkit.styles import Style as PTStyle
143
+
144
+ class AriaPTCompleter(Completer):
145
+ """
146
+ Slash-command completer with instant popup + fuzzy search.
147
+
148
+ Activates the moment the user types "/" (complete_while_typing=True
149
+ is already set in PromptSession, so no extra keypress is needed).
150
+
151
+ Triggers:
152
+ / → show ALL slash commands (fuzzy matched)
153
+ @ → file/directory path autocomplete (@ anywhere in input)
154
+ ! → shell history autocomplete (first word after !)
155
+
156
+ Matching:
157
+ / → show ALL commands sorted by category order
158
+ /ch → fuzzy-match "ch" against command names, highlight hits
159
+ /stat → matches /stat-arb even though it's not a prefix
160
+ /team AAPL → only complete the command part (stop after first space)
161
+ """
162
+
163
+ def __init__(self, commands_dict: dict, skills: list, watchlist: list):
164
+ self.commands = commands_dict
165
+ self.skills = skills
166
+ self._shell_history: list[str] = [] # populated by REPL after ! commands
167
+ self.symbols = sorted(set([
168
+ "AAPL", "MSFT", "GOOGL", "AMZN", "TSLA", "NVDA", "META",
169
+ "NFLX", "AMD", "INTC", "SPY", "QQQ", "DIA", "IWM",
170
+ "BTC-USD", "ETH-USD", "SOL-USD",
171
+ "JPM", "BAC", "GS", "V", "MA", "UNH", "JNJ", "XOM",
172
+ "GLD", "SLV", "USO", "TLT", "HYG",
173
+ ] + list(watchlist)))
174
+
175
+ # Pre-compute category for each command
176
+ self._cmd_cat: dict[str, str] = {}
177
+ for name in self.commands:
178
+ self._cmd_cat[name] = _get_cat(name)
179
+ for s in self.skills:
180
+ self._cmd_cat[s["command"]] = _get_cat(s["command"])
181
+
182
+ def get_completions(self, document, complete_event) -> Iterator[Completion]:
183
+ text = document.text_before_cursor
184
+ ltext = text.lstrip()
185
+
186
+ # ── @ file path autocomplete ────────────────────────────────────
187
+ # Triggered any time text contains "@" — complete path after it.
188
+ at_idx = text.rfind("@")
189
+ if at_idx != -1:
190
+ path_frag = text[at_idx + 1:]
191
+ # Only complete if fragment has no spaces (i.e. contiguous word)
192
+ if " " not in path_frag:
193
+ yield from self._file_completions(path_frag, at_idx + 1)
194
+ return
195
+
196
+ # ── ! shell command autocomplete ────────────────────────────────
197
+ if ltext.startswith("!"):
198
+ shell_frag = ltext[1:].lstrip()
199
+ if shell_frag:
200
+ for hist_cmd in reversed(self._shell_history):
201
+ if hist_cmd.startswith(shell_frag) and hist_cmd != shell_frag:
202
+ yield Completion(
203
+ hist_cmd,
204
+ start_position=-(len(ltext) - 1),
205
+ display=FormattedText([("class:fz-hi", hist_cmd)]),
206
+ display_meta="shell history",
207
+ )
208
+ return
209
+
210
+ # Only activate for slash commands
211
+ if not ltext.startswith("/"):
212
+ return
213
+
214
+ # Don't complete after first space — user is typing arguments
215
+ if " " in ltext:
216
+ return
217
+
218
+ # The typed prefix after "/"
219
+ pattern = ltext # includes leading "/"
220
+
221
+ # --- Build candidate list with scores ---
222
+ candidates: list[tuple[int, str, list[int], str, str]] = []
223
+ # (score, name, matched_indices, desc, category)
224
+
225
+ all_cmds = list(self.commands.items())
226
+ for name, (_, desc) in all_cmds:
227
+ cmd_part = name # e.g. "/chart"
228
+ matched, indices = _fuzzy(pattern.lstrip("/"), cmd_part.lstrip("/"))
229
+ if not matched and pattern != "/":
230
+ # Also try matching with the slash included
231
+ matched2, indices2 = _fuzzy(pattern, cmd_part)
232
+ if not matched2:
233
+ continue
234
+ indices = indices2
235
+ score = _score(cmd_part, pattern, indices)
236
+ cat = self._cmd_cat.get(name, "")
237
+ candidates.append((score, name, indices, desc, cat))
238
+
239
+ for s in self.skills:
240
+ cmd = s["command"]
241
+ desc = s.get("description", "")
242
+ matched, indices = _fuzzy(pattern.lstrip("/"), cmd.lstrip("/"))
243
+ if not matched and pattern != "/":
244
+ continue
245
+ score = _score(cmd, pattern, indices)
246
+ cat = self._cmd_cat.get(cmd, "")
247
+ candidates.append((score, cmd, indices, desc, cat))
248
+
249
+ # Sort: primary = score, secondary = name
250
+ candidates.sort(key=lambda x: (x[0], x[1]))
251
+
252
+ # Emit Completions
253
+ for score, name, indices, desc, cat in candidates:
254
+ # Build highlighted display (amber on matched chars)
255
+ display_parts = _highlighted(name, indices if pattern != "/" else [])
256
+
257
+ # Category badge as short suffix in display
258
+ badge = _CAT_BADGE.get(cat, "")
259
+ if badge:
260
+ display_parts += [("class:fz-cat", f" {badge}")]
261
+
262
+ # Truncate description to ~45 chars for meta column
263
+ meta_str = desc[:45] + ("…" if len(desc) > 45 else "")
264
+
265
+ # start_position: replace the entire typed prefix
266
+ start = -len(pattern)
267
+
268
+ yield Completion(
269
+ text = name,
270
+ start_position = start,
271
+ display = FormattedText(display_parts),
272
+ display_meta = meta_str,
273
+ )
274
+
275
+ def _file_completions(self, frag: str, cursor_offset: int) -> Iterator[Completion]:
276
+ """Yield file/directory completions for @<frag>."""
277
+ try:
278
+ if frag.startswith("~"):
279
+ frag = _os.path.expanduser(frag)
280
+ base_dir = _os.path.dirname(frag) or "."
281
+ prefix = _os.path.basename(frag)
282
+ if not _os.path.isdir(base_dir):
283
+ return
284
+ for entry in sorted(_os.listdir(base_dir))[:40]:
285
+ if entry.startswith("."):
286
+ continue
287
+ if not entry.lower().startswith(prefix.lower()):
288
+ continue
289
+ full = _os.path.join(base_dir, entry) if base_dir != "." else entry
290
+ is_dir = _os.path.isdir(_os.path.join(base_dir, entry))
291
+ display_parts = [("class:fz-hi", prefix), ("", entry[len(prefix):])]
292
+ if is_dir:
293
+ display_parts.append(("class:fz-cat", "/"))
294
+ yield Completion(
295
+ full + ("/" if is_dir else ""),
296
+ start_position=-len(frag),
297
+ display=FormattedText(display_parts),
298
+ display_meta="dir" if is_dir else "file",
299
+ )
300
+ except Exception:
301
+ pass
302
+
303
+ def add_shell_history(self, cmd: str) -> None:
304
+ """Called by REPL after each ! command to update shell autocomplete."""
305
+ cmd = cmd.strip()
306
+ if cmd and cmd not in self._shell_history:
307
+ self._shell_history.append(cmd)
308
+ if len(self._shell_history) > 200:
309
+ self._shell_history = self._shell_history[-200:]
310
+
311
+ # ── Style ────────────────────────────────────────────────────────────────
312
+ # Theme-aware completion menu in the 5-color palette.
313
+ # Selection + fuzzy highlight use copper (brand); the menu surface
314
+ # matches the terminal theme so it never floats as a dark popup on a
315
+ # light terminal (or vice-versa).
316
+
317
+ # Copper-palette menu colors per theme.
318
+ # bg = menu surface (sits clearly above terminal bg)
319
+ # fg = row text
320
+ # sel_bg = selected row (copper tint)
321
+ # sel_fg = selected row text (copper, bold-applied at use site)
322
+ # meta = description column (dim)
323
+ # hi = fuzzy-matched chars (copper)
324
+ _MENU_THEMES = {
325
+ "dark": dict(
326
+ bg="#161b22", fg="#c9d1d9",
327
+ sel_bg="#3a2e20", sel_fg="#e8c9a6",
328
+ meta="#6e7681", meta_cur="#c0a585",
329
+ scroll_bg="#161b22", scroll_btn="#C08050", # copper position handle
330
+ hi="#C08050", cat="#6e7681",
331
+ base_bg="#0d1117", prompt="#8b949e", ph="#484f58",
332
+ tb_fg="#8b949e", tb_bg="#161b22",
333
+ ),
334
+ "light": dict(
335
+ bg="#f2eee4", fg="#24292f", # warm surface, high contrast
336
+ sel_bg="#e7e1d3", sel_fg="#8a5a00", # stronger copper selection
337
+ meta="#6e7781", meta_cur="#8a5a00",
338
+ scroll_bg="#e7e1d3", scroll_btn="#9a6700", # copper position handle
339
+ hi="#9a6700", cat="#6e7781",
340
+ base_bg="default", prompt="#57606a", ph="#6e7781",
341
+ tb_fg="#57606a", tb_bg="#e7e1d3",
342
+ ),
343
+ }
344
+
345
+ def _detect_theme() -> str:
346
+ try:
347
+ from ui.input_box import detect_terminal_theme
348
+ return detect_terminal_theme()
349
+ except Exception:
350
+ return "dark"
351
+
352
+ def build_aria_pt_style(theme: str = "auto") -> "PTStyle":
353
+ """Build a theme-aware PromptSession style in the copper palette."""
354
+ if theme == "auto":
355
+ theme = _detect_theme()
356
+ c = _MENU_THEMES.get(theme, _MENU_THEMES["dark"])
357
+ base = f"{c['fg']} bg:{c['base_bg']}" if c["base_bg"] != "default" else c["fg"]
358
+ return PTStyle.from_dict({
359
+ "": base,
360
+ "prompt": c["prompt"],
361
+ "placeholder": c["ph"],
362
+ "input-bg": base,
363
+ "bottom-toolbar": f"noreverse {c['tb_fg']} bg:{c['tb_bg']}",
364
+ "bottom-toolbar.text":f"noreverse {c['tb_fg']} bg:{c['tb_bg']}",
365
+
366
+ # Completion menu — theme-aware surface, copper selection
367
+ "completion-menu": f"bg:{c['bg']} {c['fg']}",
368
+ "completion-menu.completion": f"bg:{c['bg']} {c['fg']}",
369
+ "completion-menu.completion.current": f"bg:{c['sel_bg']} {c['sel_fg']} bold",
370
+ "completion-menu.meta.completion": f"bg:{c['bg']} {c['meta']}",
371
+ "completion-menu.meta.completion.current": f"bg:{c['sel_bg']} {c['meta_cur']}",
372
+ "completion-menu.multi-column-meta": f"bg:{c['bg']} {c['meta']}",
373
+ "scrollbar.background": f"bg:{c['scroll_bg']}",
374
+ "scrollbar.button": f"bg:{c['scroll_btn']}",
375
+
376
+ # Fuzzy highlight classes — copper
377
+ "fz-hi": f"bold {c['hi']}",
378
+ "fz-cat": c["cat"],
379
+ })
380
+
381
+ # Back-compat default (dark). Prefer build_aria_pt_style(theme) at call sites.
382
+ ARIA_PT_STYLE = build_aria_pt_style("dark")
383
+
384
+ else:
385
+ def build_aria_pt_style(theme: str = "auto"): # type: ignore
386
+ return None
387
+ class AriaPTCompleter: # type: ignore
388
+ def __init__(self, *a, **kw): pass
389
+ def get_completions(self, *a, **kw): return iter([])
390
+
391
+ ARIA_PT_STYLE = None
ui/console.py ADDED
@@ -0,0 +1,271 @@
1
+ """Shared Rich console, availability flags, and ESC-key watcher.
2
+
3
+ Import this instead of repeating the try/except blocks everywhere:
4
+
5
+ from ui.console import console, HAS_RICH, HAS_PT, _SYNTAX_THEME
6
+ from ui.console import _EscWatcher, _esc_watcher
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import os
13
+ import subprocess
14
+ import sys
15
+ import threading
16
+ import time
17
+ from typing import Optional
18
+
19
+ # ── Rich ───────────────────────────────────────────────────────────────────────
20
+
21
+ try:
22
+ from rich.console import Console
23
+ from rich.markdown import Markdown
24
+ from rich.live import Live
25
+ from rich.text import Text
26
+ from rich.status import Status
27
+ from rich.syntax import Syntax
28
+ from rich.panel import Panel
29
+ from rich.rule import Rule
30
+ from rich import box as rich_box
31
+ from rich.theme import Theme
32
+ HAS_RICH = True
33
+ except ImportError:
34
+ HAS_RICH = False
35
+
36
+ # ── prompt_toolkit ─────────────────────────────────────────────────────────────
37
+
38
+ try:
39
+ from prompt_toolkit import PromptSession
40
+ from prompt_toolkit.completion import Completer, Completion
41
+ from prompt_toolkit.formatted_text import HTML
42
+ from prompt_toolkit.history import FileHistory
43
+ from prompt_toolkit.styles import Style as PTStyle
44
+ HAS_PT = True
45
+ except ImportError:
46
+ HAS_PT = False
47
+
48
+ # ── Console singleton ──────────────────────────────────────────────────────────
49
+
50
+ _SYNTAX_THEME: str = "monokai"
51
+
52
+
53
+ def _detect_terminal_theme() -> str:
54
+ """Return a best-effort terminal theme: ``dark`` or ``light``."""
55
+ explicit = os.getenv("ARIA_RICH_THEME", os.getenv("ARIA_INPUT_THEME", "")).strip().lower()
56
+ if explicit in {"dark", "light"}:
57
+ return explicit
58
+ colorfgbg = os.getenv("COLORFGBG", "")
59
+ if colorfgbg:
60
+ try:
61
+ return "dark" if int(colorfgbg.split(";")[-1]) < 8 else "light"
62
+ except ValueError:
63
+ pass
64
+ if os.uname().sysname == "Darwin":
65
+ try:
66
+ r = subprocess.run(
67
+ ["defaults", "read", "-g", "AppleInterfaceStyle"],
68
+ capture_output=True, text=True, timeout=0.2, check=False,
69
+ )
70
+ return "dark" if (r.returncode == 0 and "dark" in r.stdout.lower()) else "light"
71
+ except Exception:
72
+ pass
73
+ return "dark"
74
+
75
+
76
+ def _build_rich_theme(theme: str) -> "Theme":
77
+ """Build a Markdown palette with enough contrast for the terminal theme."""
78
+ if theme == "light":
79
+ return Theme({
80
+ "markdown.h1": "bold #24292f",
81
+ "markdown.h2": "bold #24292f",
82
+ "markdown.h3": "bold #8a5a00",
83
+ "markdown.h4": "bold #8a5a00",
84
+ "markdown.h5": "bold #8a5a00",
85
+ "markdown.h6": "bold #8a5a00",
86
+ "markdown.heading": "bold #24292f",
87
+ "markdown.code": "bold #8a5a00",
88
+ "markdown.code_inline": "bold #8a5a00",
89
+ "markdown.link": "underline #0969da",
90
+ "markdown.link_url": "underline #57606a",
91
+ "markdown.item.bullet": "bold #8a5a00",
92
+ "markdown.item.number": "bold #8a5a00",
93
+ "markdown.table.header": "bold #8a5a00",
94
+ "markdown.table.border": "#8c959f",
95
+ "markdown.hr": "#8c959f",
96
+ "markdown.strong": "bold #1f2328",
97
+ "markdown.em": "italic #57606a",
98
+ "markdown.block_quote": "#6e7781",
99
+ })
100
+ return Theme({
101
+ # Keep Markdown close to the terminal palette: neutral text, copper
102
+ # accents, no blue/purple headings, and no black inline-code blocks.
103
+ "markdown.h1": "bold #e8e0d4",
104
+ "markdown.h2": "bold #e8e0d4",
105
+ "markdown.h3": "bold #d6ba8e",
106
+ "markdown.h4": "bold #d6ba8e",
107
+ "markdown.h5": "bold #d6ba8e",
108
+ "markdown.h6": "bold #d6ba8e",
109
+ "markdown.heading": "bold #e8e0d4",
110
+ "markdown.code": "bold #c08050",
111
+ "markdown.code_inline": "bold #c08050",
112
+ "markdown.link": "underline #c08050",
113
+ "markdown.link_url": "underline #8f867a",
114
+ "markdown.item.bullet": "bold #d6ba8e",
115
+ "markdown.item.number": "bold #d6ba8e",
116
+ "markdown.table.header": "bold #d6ba8e",
117
+ "markdown.table.border": "#6f675d",
118
+ "markdown.hr": "#6f675d",
119
+ "markdown.strong": "bold #e8e0d4",
120
+ "markdown.em": "italic #c7beb2",
121
+ "markdown.block_quote": "#a8a096",
122
+ })
123
+
124
+
125
+ if HAS_RICH:
126
+ ARIA_RICH_THEME_NAME = _detect_terminal_theme()
127
+ ARIA_RICH_THEME = _build_rich_theme(ARIA_RICH_THEME_NAME)
128
+ console = Console(highlight=False, theme=ARIA_RICH_THEME)
129
+
130
+ def make_markdown(markup: str) -> Markdown:
131
+ """Create Markdown with Aria's low-saturation terminal theme."""
132
+ return Markdown(
133
+ markup,
134
+ code_theme="bw",
135
+ inline_code_theme="bw",
136
+ )
137
+ else:
138
+ class _FallbackConsole:
139
+ def print(self, *a, **kw):
140
+ print(*[str(x) for x in a])
141
+
142
+ def input(self, prompt: str = "") -> str:
143
+ return input(prompt)
144
+
145
+ def status(self, msg: str):
146
+ class _Ctx:
147
+ def __enter__(self):
148
+ print(msg)
149
+ return self
150
+ def __exit__(self, *a):
151
+ pass
152
+ def update(self, msg: str):
153
+ print(msg)
154
+ return _Ctx()
155
+
156
+ console = _FallbackConsole()
157
+
158
+ def make_markdown(markup: str) -> str:
159
+ return markup
160
+
161
+ # ── termios / raw-mode availability ───────────────────────────────────────────
162
+
163
+ try:
164
+ import termios
165
+ import tty
166
+ import select as _select
167
+ _HAS_TERMIOS = True
168
+ except ImportError:
169
+ _HAS_TERMIOS = False
170
+
171
+
172
+ # ── ESC-key watcher ───────────────────────────────────────────────────────────
173
+
174
+ class _EscWatcher:
175
+ """Background thread that watches for ESC key press to cancel streaming."""
176
+
177
+ def __init__(self):
178
+ self._active = False
179
+ self._paused = False
180
+ self._thread: Optional[threading.Thread] = None
181
+ self._old_settings = None
182
+ self._cancel_event: Optional[asyncio.Event] = None
183
+ self._fd: Optional[int] = None
184
+
185
+ def start(self, cancel_event: asyncio.Event):
186
+ if not _HAS_TERMIOS or not sys.stdin.isatty():
187
+ return
188
+ self._cancel_event = cancel_event
189
+ self._fd = sys.stdin.fileno()
190
+ try:
191
+ self._old_settings = termios.tcgetattr(self._fd)
192
+ tty.setcbreak(self._fd)
193
+ except Exception:
194
+ self._old_settings = None
195
+ return
196
+ self._active = True
197
+ self._paused = False
198
+ self._thread = threading.Thread(target=self._run, daemon=True)
199
+ self._thread.start()
200
+
201
+ def pause(self):
202
+ self._paused = True
203
+ if self._old_settings and self._fd is not None:
204
+ try:
205
+ termios.tcsetattr(self._fd, termios.TCSADRAIN, self._old_settings)
206
+ except Exception:
207
+ pass
208
+
209
+ def resume(self):
210
+ if not self._active or not _HAS_TERMIOS or self._fd is None:
211
+ return
212
+ if self._cancel_event and self._cancel_event.is_set():
213
+ return
214
+ try:
215
+ termios.tcflush(self._fd, termios.TCIFLUSH)
216
+ self._old_settings = termios.tcgetattr(self._fd)
217
+ tty.setcbreak(self._fd)
218
+ except Exception:
219
+ return
220
+ self._paused = False
221
+
222
+ def stop(self):
223
+ self._active = False
224
+ self._paused = False
225
+ if self._old_settings and self._fd is not None:
226
+ try:
227
+ termios.tcsetattr(self._fd, termios.TCSADRAIN, self._old_settings)
228
+ except Exception:
229
+ pass
230
+ self._old_settings = None
231
+ if self._thread:
232
+ self._thread.join(timeout=0.3)
233
+ self._thread = None
234
+
235
+ def _run(self):
236
+ fd = self._fd
237
+ try:
238
+ while self._active:
239
+ if self._paused:
240
+ time.sleep(0.1)
241
+ continue
242
+ try:
243
+ ready, _, _ = _select.select([fd], [], [], 0.15)
244
+ except (ValueError, OSError):
245
+ break
246
+ if not self._active or self._paused:
247
+ continue
248
+ if ready:
249
+ try:
250
+ ch = os.read(fd, 1)
251
+ except OSError:
252
+ break
253
+ if ch == b'\x1b':
254
+ try:
255
+ r2, _, _ = _select.select([fd], [], [], 0.05)
256
+ except (ValueError, OSError):
257
+ break
258
+ if r2:
259
+ try:
260
+ os.read(fd, 16)
261
+ except OSError:
262
+ pass
263
+ else:
264
+ if self._cancel_event:
265
+ self._cancel_event.set()
266
+ self._active = False
267
+ except Exception:
268
+ pass
269
+
270
+
271
+ _esc_watcher = _EscWatcher()