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.
- agents/__init__.py +32 -0
- agents/base.py +190 -0
- agents/deep/__init__.py +37 -0
- agents/deep/calibration_loop.py +144 -0
- agents/deep/critic.py +125 -0
- agents/deep/deepen.py +193 -0
- agents/deep/models.py +149 -0
- agents/deep/pipeline.py +164 -0
- agents/deep/quant_fusion.py +192 -0
- agents/deep/themes.py +95 -0
- agents/deep/tiers.py +106 -0
- agents/financial/__init__.py +10 -0
- agents/financial/catalyst.py +279 -0
- agents/financial/debate.py +145 -0
- agents/financial/earnings.py +303 -0
- agents/financial/fundamental.py +159 -0
- agents/financial/macro.py +99 -0
- agents/financial/news.py +207 -0
- agents/financial/risk.py +132 -0
- agents/financial/sector.py +279 -0
- agents/financial/synthesis.py +274 -0
- agents/financial/technical.py +258 -0
- agents/portfolio_agent.py +333 -0
- agents/realty/__init__.py +62 -0
- agents/realty/asset_diagnosis.py +150 -0
- agents/realty/business_match.py +165 -0
- agents/realty/cashflow_verify.py +208 -0
- agents/realty/contract_rules.py +209 -0
- agents/realty/energy_anomaly.py +188 -0
- agents/realty/exit_settlement.py +207 -0
- agents/realty/fulfillment_risk.py +205 -0
- agents/realty/ops_optimize.py +159 -0
- agents/realty/revenue_share.py +214 -0
- agents/registry.py +144 -0
- agents/sports/__init__.py +0 -0
- agents/sports/football_agent.py +169 -0
- agents/team.py +289 -0
- aliyun_data_client.py +660 -0
- apps/README.md +12 -0
- apps/__init__.py +2 -0
- apps/channels/README.md +15 -0
- apps/cli/README.md +13 -0
- apps/cli/__init__.py +2 -0
- apps/cli/bootstrap.py +99 -0
- apps/cli/codegen_paths.py +29 -0
- apps/cli/commands/__init__.py +16 -0
- apps/cli/commands/analysis_cmds.py +288 -0
- apps/cli/commands/backtest_cmds.py +1887 -0
- apps/cli/commands/broker_cmds.py +1154 -0
- apps/cli/commands/business_workflow_cmds.py +289 -0
- apps/cli/commands/catalog.py +84 -0
- apps/cli/commands/data_cmds.py +405 -0
- apps/cli/commands/diagnostic_cmds.py +179 -0
- apps/cli/commands/diagnostic_ops_cmds.py +696 -0
- apps/cli/commands/finance_render.py +12 -0
- apps/cli/commands/market.py +399 -0
- apps/cli/commands/market_cmds.py +1276 -0
- apps/cli/commands/market_context.py +425 -0
- apps/cli/commands/market_render.py +7 -0
- apps/cli/commands/model_cmds.py +1579 -0
- apps/cli/commands/ops_cmds.py +668 -0
- apps/cli/commands/portfolio_cmds.py +962 -0
- apps/cli/commands/report.py +377 -0
- apps/cli/commands/scaffold_templates.py +617 -0
- apps/cli/commands/session_cmds.py +179 -0
- apps/cli/commands/session_ux_cmds.py +280 -0
- apps/cli/commands/team.py +588 -0
- apps/cli/commands/team_render.py +8 -0
- apps/cli/commands/ui_cmds.py +358 -0
- apps/cli/commands/workflow_cmds.py +279 -0
- apps/cli/commands/workspace_cmds.py +1414 -0
- apps/cli/config_paths.py +70 -0
- apps/cli/config_store.py +61 -0
- apps/cli/deterministic.py +122 -0
- apps/cli/direct.py +48 -0
- apps/cli/github_app_auth.py +135 -0
- apps/cli/handlers/__init__.py +11 -0
- apps/cli/handlers/broker_handlers.py +122 -0
- apps/cli/handlers/chart_handlers.py +1309 -0
- apps/cli/handlers/market_handlers.py +2509 -0
- apps/cli/handlers/realty_handlers.py +114 -0
- apps/cli/handlers/strategy_advice.py +82 -0
- apps/cli/hooks.py +180 -0
- apps/cli/i18n.py +284 -0
- apps/cli/intent.py +136 -0
- apps/cli/intent_router.py +217 -0
- apps/cli/lifecycle_hooks.py +48 -0
- apps/cli/main.py +29 -0
- apps/cli/market_metadata.py +135 -0
- apps/cli/market_universe.py +265 -0
- apps/cli/message_processing.py +257 -0
- apps/cli/plan_mode.py +139 -0
- apps/cli/plotly_html.py +15 -0
- apps/cli/prediction_feedback.py +202 -0
- apps/cli/preflight.py +497 -0
- apps/cli/project_aria.py +60 -0
- apps/cli/prompts/__init__.py +0 -0
- apps/cli/prompts/coding.py +658 -0
- apps/cli/prompts/system_prompts.py +531 -0
- apps/cli/prompts/ui.py +434 -0
- apps/cli/providers/__init__.py +1 -0
- apps/cli/providers/base.py +271 -0
- apps/cli/providers/chat_routing.py +80 -0
- apps/cli/providers/llm/__init__.py +1 -0
- apps/cli/providers/llm/ollama_stream.py +1170 -0
- apps/cli/providers/llm/sse_stream.py +216 -0
- apps/cli/providers/runtime_bridge.py +185 -0
- apps/cli/runtime_consumer.py +489 -0
- apps/cli/session_export.py +87 -0
- apps/cli/session_jsonl.py +207 -0
- apps/cli/session_store.py +112 -0
- apps/cli/todo_tracker.py +190 -0
- apps/cli/tools/__init__.py +40 -0
- apps/cli/tools/context.py +46 -0
- apps/cli/tools/file_tools.py +112 -0
- apps/cli/tools/market_tools.py +549 -0
- apps/cli/tools/notebook_tools.py +111 -0
- apps/cli/tools/system_tools.py +669 -0
- apps/cli/tools/write_tools.py +715 -0
- apps/cli/tradingview_bridge.py +434 -0
- apps/cli/update_check.py +152 -0
- apps/cli/utils/__init__.py +0 -0
- apps/cli/utils/market_detect.py +1578 -0
- apps/daemon/README.md +14 -0
- apps/vscode/README.md +115 -0
- apps/vscode/package.json +70 -0
- aria_cli.py +11636 -0
- aria_code-4.1.3.dist-info/METADATA +952 -0
- aria_code-4.1.3.dist-info/RECORD +284 -0
- aria_code-4.1.3.dist-info/WHEEL +5 -0
- aria_code-4.1.3.dist-info/entry_points.txt +2 -0
- aria_code-4.1.3.dist-info/licenses/LICENSE +121 -0
- aria_code-4.1.3.dist-info/top_level.txt +50 -0
- aria_daemon.py +1295 -0
- aria_feishu_bot.py +1359 -0
- aria_relay_client.py +182 -0
- aria_relay_server.py +405 -0
- aria_telegram_bot.py +202 -0
- ariarc.py +328 -0
- artifacts.py +491 -0
- backtest_report.py +472 -0
- brokers/__init__.py +72 -0
- brokers/base.py +207 -0
- brokers/capabilities.py +264 -0
- brokers/cn/__init__.py +10 -0
- brokers/cn/easytrader_broker.py +193 -0
- brokers/cn/futu_broker.py +194 -0
- brokers/cn/longbridge_broker.py +190 -0
- brokers/cn/tiger_broker.py +196 -0
- brokers/cn/xtquant_broker.py +175 -0
- brokers/config.py +364 -0
- brokers/intl/__init__.py +5 -0
- brokers/intl/alpaca_broker.py +183 -0
- brokers/intl/ibkr_broker.py +215 -0
- brokers/intl/webull_broker.py +156 -0
- brokers/paper_broker.py +259 -0
- brokers/planning.py +296 -0
- brokers/registry.py +181 -0
- brokers/trading.py +237 -0
- change_store.py +127 -0
- command_safety.py +19 -0
- computer_use_tools.py +504 -0
- dashboard_generator.py +578 -0
- data_analysis_tools.py +808 -0
- data_cleaner.py +483 -0
- data_service.py +481 -0
- datasources/__init__.py +23 -0
- datasources/base.py +166 -0
- datasources/router.py +221 -0
- datasources/sources/__init__.py +15 -0
- datasources/sources/akshare_source.py +269 -0
- datasources/sources/alpha_vantage_source.py +202 -0
- datasources/sources/edgar_source.py +218 -0
- datasources/sources/finnhub_source.py +197 -0
- datasources/sources/fred_source.py +219 -0
- datasources/sources/tushare_source.py +141 -0
- datasources/sources/web_scraper_source.py +278 -0
- datasources/sources/world_bank_source.py +205 -0
- datasources/sources/yfinance_source.py +152 -0
- demo_player.py +204 -0
- doctor.py +508 -0
- file_analysis_tools.py +734 -0
- finance_formulas.py +389 -0
- football_data_client.py +1670 -0
- intent_classifier.py +358 -0
- local_finance_tools.py +3221 -0
- local_llm_provider.py +552 -0
- macro_tools.py +368 -0
- market_data_client.py +1899 -0
- mcp_client.py +506 -0
- memory_manager.py +245 -0
- model_capability.py +416 -0
- notification_tools.py +248 -0
- packages/__init__.py +23 -0
- packages/aria_agents/__init__.py +5 -0
- packages/aria_agents/manifest.py +69 -0
- packages/aria_core/__init__.py +34 -0
- packages/aria_core/architecture.py +192 -0
- packages/aria_core/export.py +124 -0
- packages/aria_core/manifest.py +65 -0
- packages/aria_infra/__init__.py +15 -0
- packages/aria_infra/arthera.py +52 -0
- packages/aria_infra/doctor.py +246 -0
- packages/aria_infra/product.py +37 -0
- packages/aria_mcp/__init__.py +25 -0
- packages/aria_mcp/bridge.py +38 -0
- packages/aria_mcp/config.py +97 -0
- packages/aria_mcp/tools.py +61 -0
- packages/aria_sdk/__init__.py +19 -0
- packages/aria_sdk/client.py +396 -0
- packages/aria_sdk/providers.py +70 -0
- packages/aria_sdk/streaming.py +73 -0
- packages/aria_sdk/types.py +86 -0
- packages/aria_services/__init__.py +55 -0
- packages/aria_services/context.py +258 -0
- packages/aria_services/data.py +11 -0
- packages/aria_services/provider_health.py +189 -0
- packages/aria_services/registry.py +213 -0
- packages/aria_services/usage.py +138 -0
- packages/aria_skills/__init__.py +5 -0
- packages/aria_skills/registry.py +59 -0
- packages/aria_tools/__init__.py +5 -0
- packages/aria_tools/registry.py +128 -0
- packages/quant_engine/__init__.py +6 -0
- packages/quant_engine/sports/__init__.py +72 -0
- packages/quant_engine/sports/calibrator.py +353 -0
- packages/quant_engine/sports/dixon_coles.py +234 -0
- packages/quant_engine/sports/elo.py +299 -0
- packages/quant_engine/sports/form.py +188 -0
- packages/quant_engine/sports/h2h.py +195 -0
- packages/quant_engine/sports/ml_model.py +354 -0
- packages/quant_engine/sports/predictor.py +311 -0
- packages/quant_engine/sports/tracker.py +664 -0
- packages/quant_engine/stochastic/__init__.py +27 -0
- packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
- packages/quant_engine/stochastic/ito_calculus.py +477 -0
- packages/quant_engine/stochastic/kelly_criterion.py +181 -0
- packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
- packages/quant_engine/stochastic/options_pricing.py +573 -0
- packages/quant_engine/stochastic/stochastic_processes.py +90 -0
- plan_utils.py +194 -0
- plugin_loader.py +328 -0
- portfolio_ledger.py +262 -0
- privacy/__init__.py +5 -0
- privacy/feedback.py +123 -0
- project_tools.py +525 -0
- providers/__init__.py +30 -0
- providers/llm/__init__.py +19 -0
- providers/llm/anthropic.py +184 -0
- providers/llm/base.py +139 -0
- providers/llm/ollama.py +128 -0
- providers/llm/openai_compat.py +282 -0
- providers/llm/registry.py +358 -0
- realty_data_tools.py +659 -0
- report_generator.py +1314 -0
- runtime/__init__.py +103 -0
- runtime/agent_loop.py +1183 -0
- runtime/approval.py +51 -0
- runtime/events.py +102 -0
- runtime/gateway.py +128 -0
- runtime/lsp.py +346 -0
- runtime/subagent.py +258 -0
- runtime/tool_executor.py +104 -0
- runtime/tool_policy.py +106 -0
- safety/__init__.py +21 -0
- safety/permissions.py +275 -0
- setup_wizard.py +653 -0
- strategy_vault.py +420 -0
- ui/__init__.py +100 -0
- ui/banner.py +310 -0
- ui/completer.py +391 -0
- ui/console.py +271 -0
- ui/image_render.py +243 -0
- ui/input_box.py +376 -0
- ui/picker.py +195 -0
- ui/render/__init__.py +11 -0
- ui/render/finance.py +1480 -0
- ui/render/market.py +225 -0
- ui/render/output.py +681 -0
- ui/render/team.py +346 -0
- ui/robot.py +235 -0
- workspace/__init__.py +6 -0
- workspace/files.py +170 -0
- 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 ""
|