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/image_render.py ADDED
@@ -0,0 +1,243 @@
1
+ """Terminal image rendering — show a real PNG in the terminal, with fallback.
2
+
3
+ Three layered techniques, best-first (exactly what chafa/viu/timg do):
4
+
5
+ 1. iTerm2 inline images (OSC 1337) — iTerm2, WezTerm, ghostty
6
+ 2. Kitty graphics protocol — kitty, ghostty
7
+ 3. Half-block + truecolor downscale (``▀``) — any 24-bit colour terminal
8
+
9
+ Layer 3 is the universal floor: it resizes the image and prints one ``▀`` per
10
+ cell, packing two vertical pixels into each character (foreground = top pixel,
11
+ background = bottom pixel). Pixel-art sources render almost perfectly this way.
12
+
13
+ Used for the startup mascot banner, and reusable for ``/vision`` previews,
14
+ ``/screenshot`` echoes, and inline quant charts.
15
+
16
+ CLI: python3 -m ui.image_render <path> [width] [--half|--iterm|--kitty]
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import base64
22
+ import os
23
+ import sys
24
+
25
+ try:
26
+ from PIL import Image
27
+ _HAS_PIL = True
28
+ except Exception: # pragma: no cover - PIL is a hard dep for this module
29
+ _HAS_PIL = False
30
+
31
+
32
+ # ── Terminal capability detection ────────────────────────────────────────────
33
+ def _in_tmux() -> bool:
34
+ # Image protocols need passthrough wrapping inside tmux; play safe and fall
35
+ # back to half-blocks rather than spraying escape bytes the pane won't eat.
36
+ return bool(os.environ.get("TMUX"))
37
+
38
+
39
+ def supports_iterm() -> bool:
40
+ if _in_tmux():
41
+ return False
42
+ if os.environ.get("TERM_PROGRAM") in ("iTerm.app", "WezTerm", "ghostty"):
43
+ return True
44
+ return bool(os.environ.get("ITERM_SESSION_ID"))
45
+
46
+
47
+ def supports_kitty() -> bool:
48
+ if _in_tmux():
49
+ return False
50
+ if os.environ.get("KITTY_WINDOW_ID"):
51
+ return True
52
+ return os.environ.get("TERM") == "xterm-kitty"
53
+
54
+
55
+ def supports_truecolor() -> bool:
56
+ if os.environ.get("COLORTERM") in ("truecolor", "24bit"):
57
+ return True
58
+ # Most modern terminals are truecolor even without advertising it; only the
59
+ # genuinely ancient (TERM=dumb / linux console) are not.
60
+ return os.environ.get("TERM", "") not in ("", "dumb", "linux")
61
+
62
+
63
+ def best_method() -> str:
64
+ """Return the best available render method for the current terminal."""
65
+ if supports_iterm():
66
+ return "iterm"
67
+ if supports_kitty():
68
+ return "kitty"
69
+ if supports_truecolor():
70
+ return "half"
71
+ return "none"
72
+
73
+
74
+ # ── Protocol emitters ────────────────────────────────────────────────────────
75
+ def _iterm_sequence(png_bytes: bytes, cells_wide: int) -> str:
76
+ """iTerm2 OSC 1337 inline image. Width in character cells, height auto."""
77
+ b64 = base64.b64encode(png_bytes).decode()
78
+ return (
79
+ f"\x1b]1337;File=inline=1;width={cells_wide};"
80
+ f"preserveAspectRatio=1:{b64}\x07"
81
+ )
82
+
83
+
84
+ def _kitty_sequence(png_bytes: bytes, cells_wide: int) -> str:
85
+ """Kitty graphics protocol, PNG payload (f=100), chunked at 4096 bytes."""
86
+ b64 = base64.b64encode(png_bytes).decode()
87
+ chunk = 4096
88
+ parts: list[str] = []
89
+ i = 0
90
+ first = True
91
+ while i < len(b64):
92
+ piece = b64[i : i + chunk]
93
+ i += chunk
94
+ more = 1 if i < len(b64) else 0
95
+ if first:
96
+ # a=T transmit+display, f=100 PNG, c=columns to scale into
97
+ ctrl = f"a=T,f=100,c={cells_wide},m={more}"
98
+ first = False
99
+ else:
100
+ ctrl = f"m={more}"
101
+ parts.append(f"\x1b_G{ctrl};{piece}\x1b\\")
102
+ return "".join(parts)
103
+
104
+
105
+ # ── Half-block fallback (universal) ──────────────────────────────────────────
106
+ _UPPER = "▀" # ▀ upper half block
107
+
108
+
109
+ def autocrop(img: "Image.Image", tol: int = 18, pad: int = 1) -> "Image.Image":
110
+ """Trim a uniform border (e.g. the robot's black canvas) so the subject
111
+ fills the frame. Background colour is sampled from the top-left pixel;
112
+ pixels within ``tol`` of it are treated as border. Falls back to the
113
+ original image if nothing distinct is found.
114
+ """
115
+ from PIL import ImageChops, Image as _I
116
+
117
+ rgb = img.convert("RGB")
118
+ bg = _I.new("RGB", rgb.size, rgb.getpixel((0, 0)))
119
+ diff = ImageChops.difference(rgb, bg).convert("L")
120
+ box = diff.point(lambda p: 255 if p > tol else 0).getbbox()
121
+ if not box:
122
+ return img
123
+ l, t, r, b = box
124
+ l, t = max(0, l - pad), max(0, t - pad)
125
+ r, b = min(img.width, r + pad), min(img.height, b + pad)
126
+ return img.crop((l, t, r, b))
127
+
128
+
129
+ def half_block_render(img: "Image.Image", cells_wide: int = 36) -> str:
130
+ """Render a PIL image as ``▀`` half-blocks with 24-bit colour.
131
+
132
+ Each character is 1 pixel wide and 2 pixels tall, so a square source maps to
133
+ ``cells_wide`` columns × ``cells_wide // 2`` rows. Foreground paints the top
134
+ pixel, background the bottom one.
135
+ """
136
+ img = img.convert("RGB")
137
+ w, h = img.size
138
+ # Sample at cells_wide × (2 px per row). Rows chosen to preserve aspect once
139
+ # the 1:2 cell shape is accounted for, so on-screen proportions match.
140
+ px_w = max(1, cells_wide)
141
+ px_h = max(2, round(px_w * h / w))
142
+ if px_h % 2:
143
+ px_h += 1 # even rows so every cell has a top+bottom pixel
144
+ small = img.resize((px_w, px_h), Image.LANCZOS)
145
+ px = small.load()
146
+
147
+ lines: list[str] = []
148
+ for row in range(0, px_h, 2):
149
+ cells: list[str] = []
150
+ for x in range(px_w):
151
+ tr, tg, tb = px[x, row]
152
+ br, bg, bb = px[x, row + 1]
153
+ cells.append(
154
+ f"\x1b[38;2;{tr};{tg};{tb};48;2;{br};{bg};{bb}m{_UPPER}"
155
+ )
156
+ cells.append("\x1b[0m")
157
+ lines.append("".join(cells))
158
+ return "\n".join(lines)
159
+
160
+
161
+ # ── Public entry point ───────────────────────────────────────────────────────
162
+ def render_image(
163
+ path: str,
164
+ cells_wide: int = 36,
165
+ method: str | None = None,
166
+ crop: bool = True,
167
+ ) -> str | None:
168
+ """Return a printable string that draws ``path`` in the terminal.
169
+
170
+ ``method`` forces one of ``iterm`` / ``kitty`` / ``half``; default auto-detects.
171
+ ``crop`` trims a uniform border first so the subject fills the frame.
172
+ Returns ``None`` if the image can't be loaded or no method is usable.
173
+ """
174
+ chosen = method or best_method()
175
+
176
+ # Protocol path (iTerm2/Kitty) only needs the raw PNG bytes — the terminal
177
+ # scales them. PIL is optional here: with it we autocrop so the subject
178
+ # fills the frame; without it we send the file as-is. This means the real
179
+ # image still shows even when Pillow isn't installed in the venv.
180
+ if chosen in ("iterm", "kitty"):
181
+ try:
182
+ if _HAS_PIL:
183
+ import io
184
+
185
+ img = Image.open(path)
186
+ if crop:
187
+ try:
188
+ img = autocrop(img)
189
+ except Exception:
190
+ pass
191
+ buf = io.BytesIO()
192
+ img.convert("RGB").save(buf, format="PNG")
193
+ data = buf.getvalue()
194
+ else:
195
+ with open(path, "rb") as fh:
196
+ data = fh.read()
197
+ if chosen == "iterm":
198
+ return _iterm_sequence(data, cells_wide)
199
+ return _kitty_sequence(data, cells_wide)
200
+ except Exception:
201
+ chosen = "half" # fall through to the universal path
202
+
203
+ # Half-block fallback needs PIL to resize.
204
+ if chosen == "half" and _HAS_PIL:
205
+ try:
206
+ img = Image.open(path)
207
+ if crop:
208
+ try:
209
+ img = autocrop(img)
210
+ except Exception:
211
+ pass
212
+ return half_block_render(img, cells_wide)
213
+ except Exception:
214
+ return None
215
+ return None
216
+
217
+
218
+ def _main(argv: list[str]) -> int:
219
+ args = [a for a in argv if not a.startswith("--")]
220
+ flags = {a for a in argv if a.startswith("--")}
221
+ if not args:
222
+ print("usage: python3 -m ui.image_render <image> [width] [--half|--iterm|--kitty]")
223
+ return 2
224
+ path = args[0]
225
+ width = int(args[1]) if len(args) > 1 and args[1].isdigit() else 36
226
+ method = None
227
+ if "--half" in flags:
228
+ method = "half"
229
+ elif "--iterm" in flags:
230
+ method = "iterm"
231
+ elif "--kitty" in flags:
232
+ method = "kitty"
233
+ out = render_image(path, width, method)
234
+ if out is None:
235
+ print(f"(cannot render {path}; method={method or best_method()})")
236
+ return 1
237
+ sys.stdout.write(out + "\n")
238
+ sys.stdout.flush()
239
+ return 0
240
+
241
+
242
+ if __name__ == "__main__":
243
+ raise SystemExit(_main(sys.argv[1:]))
ui/input_box.py ADDED
@@ -0,0 +1,376 @@
1
+ """Prompt-toolkit input panel — lightweight Claude Code-style input block.
2
+
3
+ Layout:
4
+ ────────────────────────────────────────────── ← subtle top rule
5
+ › cursor_ ← padded input row
6
+ ────────────────────────────────────────────── ← subtle bottom rule
7
+ model · ~/workspace ← dim status bar
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ import shutil
14
+ import subprocess
15
+ import time
16
+ from dataclasses import dataclass, replace
17
+ from typing import Callable, Optional
18
+
19
+ try:
20
+ from prompt_toolkit.application import Application
21
+ from prompt_toolkit.buffer import Buffer
22
+ from prompt_toolkit.key_binding import KeyBindings
23
+ from prompt_toolkit.layout.dimension import Dimension
24
+ from prompt_toolkit.layout import Float, FloatContainer, HSplit, Layout, VSplit, Window
25
+ from prompt_toolkit.layout.controls import FormattedTextControl
26
+ from prompt_toolkit.layout.menus import CompletionsMenu
27
+ from prompt_toolkit.layout.processors import Processor, Transformation
28
+ from prompt_toolkit.styles import Style
29
+ from prompt_toolkit.widgets import TextArea
30
+ HAS_PROMPT_TOOLKIT = True
31
+ except ImportError:
32
+ HAS_PROMPT_TOOLKIT = False
33
+
34
+ class Processor: # type: ignore[no-redef]
35
+ pass
36
+
37
+ class Transformation: # type: ignore[no-redef]
38
+ def __init__(self, fragments, source_to_display=None, display_to_source=None):
39
+ self.fragments = fragments
40
+ self.source_to_display = source_to_display or (lambda i: i)
41
+ self.display_to_source = display_to_source or (lambda i: i)
42
+
43
+ class Style: # type: ignore[no-redef]
44
+ @staticmethod
45
+ def from_dict(values):
46
+ return values
47
+
48
+ Application = Buffer = KeyBindings = Dimension = None # type: ignore
49
+ Float = FloatContainer = HSplit = Layout = VSplit = Window = None # type: ignore
50
+ FormattedTextControl = CompletionsMenu = TextArea = None # type: ignore
51
+
52
+
53
+ # ── Theme detection ────────────────────────────────────────────────────────────
54
+
55
+ def detect_terminal_theme() -> str:
56
+ explicit = os.getenv("ARIA_INPUT_THEME", "").strip().lower()
57
+ if explicit in {"dark", "light"}:
58
+ return explicit
59
+ colorfgbg = os.getenv("COLORFGBG", "")
60
+ if colorfgbg:
61
+ try:
62
+ return "dark" if int(colorfgbg.split(";")[-1]) < 8 else "light"
63
+ except ValueError:
64
+ pass
65
+ if os.uname().sysname == "Darwin":
66
+ try:
67
+ r = subprocess.run(
68
+ ["defaults", "read", "-g", "AppleInterfaceStyle"],
69
+ capture_output=True, text=True, timeout=0.2, check=False,
70
+ )
71
+ return "dark" if (r.returncode == 0 and "dark" in r.stdout.lower()) else "light"
72
+ except Exception:
73
+ pass
74
+ return "dark"
75
+
76
+
77
+ # ── Config ─────────────────────────────────────────────────────────────────────
78
+
79
+ @dataclass
80
+ class PanelInputConfig:
81
+ prompt: str = "› "
82
+ placeholder: str = "问 Aria、编辑文件、运行命令… /命令 @文件 !shell"
83
+ theme: str = "auto"
84
+
85
+ est_tokens: int = 0
86
+ max_tokens: int = 131072
87
+
88
+ # Status bar display
89
+ model_label: str = ""
90
+ cwd: str = ""
91
+
92
+ # Robot mascot — show animated dot in status bar
93
+ show_robot: bool = True
94
+
95
+ # Legacy fields kept for call-site compatibility
96
+ privacy: str = "local-only"
97
+ tools_count: int = 0
98
+ skills_count: int = 0
99
+ ollama_status: str = ""
100
+ pending_file: str = ""
101
+
102
+ # Resolved by .resolved()
103
+ fg: str = ""
104
+ accent: str = ""
105
+ accent_y: str = ""
106
+ accent_b: str = ""
107
+ muted: str = ""
108
+ dim: str = ""
109
+ sep: str = ""
110
+ input_bg: str = "" # intentional input-area background
111
+ ph_color: str = "" # very dim placeholder
112
+ box: str = "" # rounded border color
113
+
114
+ # Completion menu palette (copper, theme-aware)
115
+ menu_bg: str = ""
116
+ menu_fg: str = ""
117
+ menu_sel_bg: str = ""
118
+ menu_sel_fg: str = ""
119
+ menu_meta: str = ""
120
+ menu_meta_cur: str = ""
121
+ scroll_bg: str = ""
122
+ scroll_btn: str = ""
123
+ hi: str = "" # fuzzy-match highlight (copper)
124
+
125
+ def resolved(self) -> "PanelInputConfig":
126
+ theme = self.theme if self.theme != "auto" else detect_terminal_theme()
127
+ if theme == "dark":
128
+ return replace(self, theme=theme,
129
+ fg="#c9d1d9",
130
+ accent="#3fb950", accent_y="#d29922", accent_b="#79c0ff",
131
+ muted="#6e7781", dim="#484f58", sep="#2d333b",
132
+ input_bg="default", # transparent — box border defines the zone
133
+ ph_color="#484f58", # dim placeholder, readable
134
+ box="#C08050", # copper — Aria's brand accent on the frame
135
+ menu_bg="#161b22", menu_fg="#c9d1d9",
136
+ menu_sel_bg="#3a2e20", menu_sel_fg="#e8c9a6",
137
+ menu_meta="#6e7681", menu_meta_cur="#c0a585",
138
+ scroll_bg="#161b22", scroll_btn="#C08050",
139
+ hi="#C08050",
140
+ )
141
+ return replace(self, theme="light",
142
+ fg="#24292f",
143
+ accent="#1a7f37", accent_y="#9a6700", accent_b="#0969da",
144
+ muted="#57606a", dim="#8c959f", sep="#d0d7de",
145
+ input_bg="default",
146
+ ph_color="#6e7781",
147
+ box="#9a6700",
148
+ menu_bg="#f2eee4", menu_fg="#24292f",
149
+ menu_sel_bg="#e7e1d3", menu_sel_fg="#8a5a00",
150
+ menu_meta="#6e7781", menu_meta_cur="#8a5a00",
151
+ scroll_bg="#e7e1d3", scroll_btn="#9a6700",
152
+ hi="#9a6700",
153
+ )
154
+
155
+
156
+ # ── Processor (mode badge + placeholder) ──────────────────────────────────────
157
+
158
+ class PromptAndPlaceholderProcessor(Processor):
159
+ def __init__(self, get_prefix: Callable[[], list], placeholder: str,
160
+ is_empty: Callable[[], bool]) -> None:
161
+ self.get_prefix = get_prefix
162
+ self.placeholder = placeholder
163
+ self.is_empty = is_empty
164
+
165
+ def apply_transformation(self, ti) -> Transformation:
166
+ if ti.lineno == 0:
167
+ empty = self.is_empty()
168
+ prefix = self.get_prefix()
169
+ pw = sum(len(t) for _, t in prefix)
170
+ frags = list(prefix)
171
+ if empty:
172
+ frags.append(("class:ph", self.placeholder))
173
+ frags.extend(ti.fragments)
174
+ return Transformation(
175
+ frags,
176
+ source_to_display=lambda i: pw + i,
177
+ display_to_source=lambda i: 0 if (i <= pw or empty) else max(0, i - pw),
178
+ )
179
+ return Transformation(ti.fragments)
180
+
181
+
182
+ PlaceholderProcessor = PromptAndPlaceholderProcessor
183
+
184
+
185
+ INPUT_MAX_HEIGHT = 6
186
+
187
+
188
+ # ── Style ──────────────────────────────────────────────────────────────────────
189
+
190
+ def _build_style(cfg: PanelInputConfig) -> Style:
191
+ return Style.from_dict({
192
+ # Input row: transparent bg — the rounded box border defines the zone
193
+ "input-bg": cfg.fg if cfg.input_bg == "default" else f"{cfg.fg} bg:{cfg.input_bg}",
194
+ "ph": cfg.ph_color,
195
+ # Rounded box border (Claude Code style)
196
+ "box": cfg.box,
197
+ # Mode prompt glyph — always copper (brand). 5-color discipline:
198
+ # red/green are reserved for 涨跌 semantics, never for chrome.
199
+ "mode-chat": f"bold {cfg.box}",
200
+ "mode-cmd": f"bold {cfg.box}",
201
+ "mode-file": f"bold {cfg.box}",
202
+ "prompt": cfg.muted,
203
+ # Divider (transparent bg — terminal bg shows through)
204
+ "divider": cfg.sep,
205
+ # Status bar (transparent bg)
206
+ "st-model": cfg.muted,
207
+ "st-sep": cfg.dim,
208
+ "st-cwd": cfg.dim,
209
+ "tok-warn": cfg.box, # copper — context-pressure caution
210
+ "tok-crit": "#f85149", # red — critical only
211
+ # Completion menu — theme-aware copper palette (matches terminal theme)
212
+ "completion-menu": f"bg:{cfg.menu_bg} {cfg.menu_fg}",
213
+ "completion-menu.completion": f"bg:{cfg.menu_bg} {cfg.menu_fg}",
214
+ "completion-menu.completion.current": f"bg:{cfg.menu_sel_bg} {cfg.menu_sel_fg} bold",
215
+ "completion-menu.meta.completion": f"bg:{cfg.menu_bg} {cfg.menu_meta}",
216
+ "completion-menu.meta.completion.current": f"bg:{cfg.menu_sel_bg} {cfg.menu_meta_cur}",
217
+ "completion-menu.multi-column-meta": f"bg:{cfg.menu_bg} {cfg.menu_meta}",
218
+ "scrollbar.background": f"bg:{cfg.scroll_bg}",
219
+ "scrollbar.button": f"bg:{cfg.scroll_btn}",
220
+ # Fuzzy-match highlight classes (shared with ui/completer.py) — copper
221
+ "fz-hi": f"bold {cfg.hi}",
222
+ "fz-cat": cfg.dim,
223
+ })
224
+
225
+
226
+ # ── Row builders ───────────────────────────────────────────────────────────────
227
+
228
+ def _input_rule(cfg: PanelInputConfig) -> list:
229
+ w = shutil.get_terminal_size((80, 24)).columns
230
+ return [("class:divider", "─" * w)]
231
+
232
+
233
+ def _input_pad() -> list:
234
+ return [("", " ")]
235
+
236
+
237
+ def _mode_prefix(cfg: PanelInputConfig, text_getter: Callable[[], str]) -> list:
238
+ """Claude Code-style › glyph — color shifts by detected input mode."""
239
+ txt = text_getter().lstrip()
240
+ if txt.startswith("/"):
241
+ return [("class:mode-cmd", "› ")]
242
+ if txt.startswith("@") or txt.startswith("!"):
243
+ return [("class:mode-file", "› ")]
244
+ return [("class:mode-chat", "› ")]
245
+
246
+
247
+ def _status_bar(cfg: PanelInputConfig) -> list:
248
+ """Dim status row: [robot dot] model · cwd [· ctx warning when >60%]"""
249
+ from .robot import get_status_dot, get_robot_state, RobotState
250
+
251
+ tick = int(time.monotonic() * 4) # 4 fps tick without a background thread
252
+ parts: list = []
253
+
254
+ if cfg.show_robot:
255
+ dot_frags = get_status_dot(tick)
256
+ parts.extend(dot_frags)
257
+ parts.append(("class:st-sep", " "))
258
+
259
+ if cfg.model_label:
260
+ parts.append(("class:st-model", cfg.model_label))
261
+
262
+ if cfg.cwd:
263
+ if cfg.model_label:
264
+ parts.append(("class:st-sep", " · "))
265
+ parts.append(("class:st-cwd", cfg.cwd))
266
+
267
+ # Token warning only above 60% — silent below that
268
+ if cfg.est_tokens > 0:
269
+ ratio = cfg.est_tokens / max(cfg.max_tokens, 1)
270
+ if ratio >= 0.60:
271
+ tc = "tok-crit" if ratio >= 0.85 else "tok-warn"
272
+ def _k(n: int) -> str:
273
+ return f"{n // 1000}K" if n >= 1000 else str(n)
274
+ parts += [
275
+ ("class:st-sep", " · "),
276
+ (f"class:{tc}", f"ctx {_k(cfg.est_tokens)}/{_k(cfg.max_tokens)}"),
277
+ ]
278
+
279
+ return parts
280
+
281
+
282
+ # ── Main ───────────────────────────────────────────────────────────────────────
283
+
284
+ def run_panel_input(
285
+ *,
286
+ completer=None,
287
+ history=None,
288
+ status_text: Callable[[], str] | str = "", # kept for compat
289
+ config: Optional[PanelInputConfig] = None,
290
+ ) -> str:
291
+ cfg = (config or PanelInputConfig()).resolved()
292
+ if not HAS_PROMPT_TOOLKIT:
293
+ try:
294
+ return input(cfg.prompt)
295
+ except EOFError:
296
+ return ""
297
+
298
+ def _accept(buf: Buffer) -> bool:
299
+ app.exit(result=buf.text)
300
+ return True
301
+
302
+ def _get_text() -> str:
303
+ try:
304
+ return text_area.text
305
+ except Exception:
306
+ return ""
307
+
308
+ text_area = TextArea(
309
+ height=Dimension(min=1, max=INPUT_MAX_HEIGHT),
310
+ multiline=True,
311
+ wrap_lines=True,
312
+ dont_extend_height=True,
313
+ completer=completer,
314
+ complete_while_typing=True,
315
+ history=history,
316
+ prompt="",
317
+ input_processors=[
318
+ PromptAndPlaceholderProcessor(
319
+ lambda: _mode_prefix(cfg, _get_text),
320
+ cfg.placeholder,
321
+ lambda: _get_text() == "",
322
+ ),
323
+ ],
324
+ accept_handler=_accept,
325
+ style="class:input-bg", # only the input row gets the subtle bg
326
+ )
327
+
328
+ kb = KeyBindings()
329
+
330
+ @kb.add("escape")
331
+ def _cancel(event) -> None:
332
+ event.app.exit(result="")
333
+
334
+ @kb.add("enter", eager=True)
335
+ def _submit(event) -> None:
336
+ event.app.exit(result=text_area.text)
337
+
338
+ @kb.add("s-tab")
339
+ def _shift_tab(event) -> None:
340
+ event.app.current_buffer.complete_previous()
341
+
342
+ root = FloatContainer(
343
+ content=HSplit([
344
+ # Lightweight terminal-native input section.
345
+ Window(height=1,
346
+ content=FormattedTextControl(lambda: _input_rule(cfg), focusable=False)),
347
+ VSplit([
348
+ Window(width=1, content=FormattedTextControl(_input_pad, focusable=False)),
349
+ text_area,
350
+ Window(width=1, content=FormattedTextControl(_input_pad, focusable=False)),
351
+ ]),
352
+ Window(height=1,
353
+ content=FormattedTextControl(lambda: _input_rule(cfg), focusable=False)),
354
+ # Status bar: transparent bg, dim model · cwd
355
+ Window(height=1,
356
+ content=FormattedTextControl(lambda: _status_bar(cfg), focusable=False)),
357
+ ]),
358
+ floats=[
359
+ Float(
360
+ xcursor=True,
361
+ ycursor=True,
362
+ content=CompletionsMenu(max_height=12, scroll_offset=2),
363
+ )
364
+ ],
365
+ )
366
+
367
+ app: Application = Application(
368
+ layout=Layout(root, focused_element=text_area),
369
+ key_bindings=kb,
370
+ style=_build_style(cfg),
371
+ full_screen=False,
372
+ erase_when_done=False,
373
+ mouse_support=False,
374
+ refresh_interval=0.25, # drives robot dot animation at 4 fps
375
+ )
376
+ return app.run() or ""