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
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"""UiCommandsMixin — vision, browser, screenshot, input, and context commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import io
|
|
7
|
+
import pathlib
|
|
8
|
+
from urllib.parse import urlsplit
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class UiCommandsMixin:
|
|
12
|
+
"""Mixin: visual input and terminal UI commands."""
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def _short_url_label(url: str) -> str:
|
|
16
|
+
try:
|
|
17
|
+
from urllib.parse import urlsplit
|
|
18
|
+
parsed = urlsplit(url if url.startswith(("http://", "https://")) else f"https://{url}")
|
|
19
|
+
host = parsed.netloc or parsed.path
|
|
20
|
+
path = parsed.path.rstrip("/")
|
|
21
|
+
if len(path) > 32:
|
|
22
|
+
path = path[:29] + "..."
|
|
23
|
+
return f"{host}{path}" if path and path != "/" else host
|
|
24
|
+
except Exception:
|
|
25
|
+
return url[:48]
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def _load_image_source(path_or_url: str) -> dict:
|
|
29
|
+
"""Load an image from a local path, URL, or clipboard."""
|
|
30
|
+
raw = (path_or_url or "").strip().strip("\"'")
|
|
31
|
+
if not raw:
|
|
32
|
+
raise ValueError("Missing image source")
|
|
33
|
+
|
|
34
|
+
mime_map = {
|
|
35
|
+
"png": "image/png",
|
|
36
|
+
"jpg": "image/jpeg",
|
|
37
|
+
"jpeg": "image/jpeg",
|
|
38
|
+
"gif": "image/gif",
|
|
39
|
+
"webp": "image/webp",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
def _from_bytes(data: bytes, mime: str, label: str) -> dict:
|
|
43
|
+
if not data:
|
|
44
|
+
raise ValueError("Empty image data")
|
|
45
|
+
return {
|
|
46
|
+
"label": label,
|
|
47
|
+
"mime": mime,
|
|
48
|
+
"b64": base64.b64encode(data).decode(),
|
|
49
|
+
"size_kb": max(1, len(data) // 1024),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if raw.lower() in {"clipboard", "clip", "paste"}:
|
|
53
|
+
try:
|
|
54
|
+
from PIL import ImageGrab
|
|
55
|
+
img = ImageGrab.grabclipboard()
|
|
56
|
+
if img is None:
|
|
57
|
+
raise ValueError("Clipboard does not contain an image")
|
|
58
|
+
if isinstance(img, list):
|
|
59
|
+
for item in img:
|
|
60
|
+
p = pathlib.Path(str(item))
|
|
61
|
+
if p.is_file() and p.suffix.lstrip(".").lower() in mime_map:
|
|
62
|
+
return UiCommandsMixin._load_image_source(str(p))
|
|
63
|
+
raise ValueError("Clipboard does not contain a supported image file")
|
|
64
|
+
if hasattr(img, "save"):
|
|
65
|
+
buf = io.BytesIO()
|
|
66
|
+
img.save(buf, format="PNG")
|
|
67
|
+
return _from_bytes(buf.getvalue(), "image/png", "clipboard")
|
|
68
|
+
raise ValueError("Clipboard image format not supported")
|
|
69
|
+
except Exception as exc:
|
|
70
|
+
raise ValueError(str(exc)) from exc
|
|
71
|
+
|
|
72
|
+
if raw.startswith(("http://", "https://", "www.")):
|
|
73
|
+
try:
|
|
74
|
+
import requests
|
|
75
|
+
url = raw if raw.startswith(("http://", "https://")) else f"https://{raw}"
|
|
76
|
+
resp = requests.get(url, timeout=20, headers={"User-Agent": "Mozilla/5.0"})
|
|
77
|
+
resp.raise_for_status()
|
|
78
|
+
content_type = (resp.headers.get("content-type") or "").split(";", 1)[0].strip().lower()
|
|
79
|
+
mime = content_type if content_type.startswith("image/") else "image/png"
|
|
80
|
+
if mime == "application/octet-stream":
|
|
81
|
+
mime = "image/png"
|
|
82
|
+
return _from_bytes(resp.content, mime, UiCommandsMixin._short_url_label(url))
|
|
83
|
+
except Exception as exc:
|
|
84
|
+
raise ValueError(f"Cannot download image: {exc}") from exc
|
|
85
|
+
|
|
86
|
+
path = pathlib.Path(raw).expanduser().resolve()
|
|
87
|
+
if not path.exists():
|
|
88
|
+
raise FileNotFoundError(f"File not found: {path}")
|
|
89
|
+
suffix = path.suffix.lstrip(".").lower()
|
|
90
|
+
mime = mime_map.get(suffix)
|
|
91
|
+
if not mime:
|
|
92
|
+
raise ValueError(f"Unsupported image type: .{suffix}")
|
|
93
|
+
return _from_bytes(path.read_bytes(), mime, path.name)
|
|
94
|
+
|
|
95
|
+
def cmd_vision(self, args: str):
|
|
96
|
+
_curr_model = self.terminal.config.get("model", "")
|
|
97
|
+
if _curr_model and _HAS_MODEL_CAP:
|
|
98
|
+
_vcap = get_model_capability(_curr_model)
|
|
99
|
+
if not _vcap.vision:
|
|
100
|
+
_warn = (
|
|
101
|
+
f"[yellow]⚠[/yellow] 当前模型 [bold]{_curr_model}[/bold] 不支持图片输入。\n"
|
|
102
|
+
f"[dim]支持视觉的模型:llama3.2:11b · gemma3 · llava · qwen2-vl · moondream[/dim]"
|
|
103
|
+
)
|
|
104
|
+
if HAS_RICH:
|
|
105
|
+
console.print(Panel(_warn, border_style="yellow", box=rich_box.ROUNDED, padding=(0, 1)))
|
|
106
|
+
else:
|
|
107
|
+
print(f"Warning: model {_curr_model} does not support vision input.")
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
path_str = args.strip().strip("\"'")
|
|
111
|
+
if not path_str:
|
|
112
|
+
msg = "Usage: /vision <image_path|image_url|clipboard> (e.g. /vision ~/Pictures/chart.png)"
|
|
113
|
+
console.print(f"[dim]{msg}[/dim]" if HAS_RICH else msg)
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
payload = self._load_image_source(path_str)
|
|
118
|
+
except Exception as e:
|
|
119
|
+
_print_error(str(e), "vision")
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
self.terminal._pending_image = {
|
|
123
|
+
"type": "image_url",
|
|
124
|
+
"image_url": {"url": f"data:{payload['mime']};base64,{payload['b64']}"},
|
|
125
|
+
}
|
|
126
|
+
size_kb = payload["size_kb"]
|
|
127
|
+
if HAS_RICH:
|
|
128
|
+
console.print(Panel(
|
|
129
|
+
f"[green]✓[/green] [dim]{payload['label']}[/dim] [dim]{size_kb} KB · {payload['mime']}[/dim]\n"
|
|
130
|
+
f"[dim]Image queued — ask your question now[/dim]",
|
|
131
|
+
border_style="dim",
|
|
132
|
+
box=rich_box.ROUNDED,
|
|
133
|
+
padding=(0, 1),
|
|
134
|
+
))
|
|
135
|
+
else:
|
|
136
|
+
print(f"Image loaded: {payload['label']} ({size_kb} KB) — send your question now")
|
|
137
|
+
|
|
138
|
+
async def cmd_browser(self, args: str):
|
|
139
|
+
"""Open a URL in a headless browser."""
|
|
140
|
+
if not _HAS_COMPUTER_USE:
|
|
141
|
+
_print_error(
|
|
142
|
+
"computer_use_tools not available.",
|
|
143
|
+
"Install: pip install playwright mss pyautogui pillow && playwright install chromium",
|
|
144
|
+
)
|
|
145
|
+
return
|
|
146
|
+
from computer_use_tools import _tool_browser_navigate, _tool_browser_screenshot
|
|
147
|
+
|
|
148
|
+
parts = args.strip().split(maxsplit=1)
|
|
149
|
+
if not parts:
|
|
150
|
+
if HAS_RICH:
|
|
151
|
+
console.print("[dim]Usage: /browser <url> or /browser screenshot <url>[/dim]")
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
if parts[0].lower() == "screenshot" and len(parts) > 1:
|
|
155
|
+
url = parts[1].strip()
|
|
156
|
+
if HAS_RICH:
|
|
157
|
+
with console.status(f"[dim]Screenshotting {self._short_url_label(url)}…[/dim]", spinner="dots"):
|
|
158
|
+
result = _tool_browser_screenshot({"url": url})
|
|
159
|
+
else:
|
|
160
|
+
result = _tool_browser_screenshot({"url": url})
|
|
161
|
+
if result.get("success"):
|
|
162
|
+
d = result["data"]
|
|
163
|
+
from computer_use_tools import pop_pending_vision_image
|
|
164
|
+
b64 = pop_pending_vision_image()
|
|
165
|
+
if b64:
|
|
166
|
+
self.terminal._pending_image = {
|
|
167
|
+
"type": "image_url",
|
|
168
|
+
"image_url": {"url": f"data:image/png;base64,{b64}"},
|
|
169
|
+
}
|
|
170
|
+
if HAS_RICH:
|
|
171
|
+
console.print(Panel(
|
|
172
|
+
f"[green]✓[/green] [bold]{d.get('title','')[:60]}[/bold]\n"
|
|
173
|
+
f"[dim]{self._short_url_label(url)} · {d.get('size_kb', 0)} KB[/dim]\n"
|
|
174
|
+
f"[dim]Screenshot queued — ask your question now[/dim]",
|
|
175
|
+
border_style="dim", box=rich_box.ROUNDED, padding=(0, 1),
|
|
176
|
+
))
|
|
177
|
+
else:
|
|
178
|
+
print(f"Screenshot ready ({d.get('size_kb', 0)} KB) — send your question")
|
|
179
|
+
else:
|
|
180
|
+
_print_error(result.get("error", "Screenshot failed"), "browser screenshot")
|
|
181
|
+
else:
|
|
182
|
+
url = parts[0].strip()
|
|
183
|
+
if HAS_RICH:
|
|
184
|
+
with console.status(f"[dim]Opening {self._short_url_label(url)}…[/dim]", spinner="dots"):
|
|
185
|
+
result = _tool_browser_navigate({"url": url})
|
|
186
|
+
else:
|
|
187
|
+
result = _tool_browser_navigate({"url": url})
|
|
188
|
+
if result.get("success"):
|
|
189
|
+
d = result["data"]
|
|
190
|
+
title = d.get("title", "")
|
|
191
|
+
text = d.get("text", "")[:2000]
|
|
192
|
+
links = d.get("links", [])[:5]
|
|
193
|
+
engine = d.get("engine", "")
|
|
194
|
+
if HAS_RICH:
|
|
195
|
+
link_str = "\n".join(f" {l}" for l in links) if links else " (none)"
|
|
196
|
+
console.print(Panel(
|
|
197
|
+
f"[bold]{title[:80]}[/bold] [dim]({engine})[/dim]\n\n"
|
|
198
|
+
f"{text}\n\n[dim]Links:[/dim]\n{link_str}",
|
|
199
|
+
border_style="dim", box=rich_box.ROUNDED, padding=(0, 1),
|
|
200
|
+
title=f"[dim]{self._short_url_label(url)}[/dim]", title_align="left",
|
|
201
|
+
))
|
|
202
|
+
else:
|
|
203
|
+
print(f"Title: {title}\n{text[:500]}")
|
|
204
|
+
else:
|
|
205
|
+
_print_error(result.get("error", "Navigation failed"), "browser")
|
|
206
|
+
|
|
207
|
+
async def cmd_screenshot(self, args: str):
|
|
208
|
+
if not _HAS_COMPUTER_USE:
|
|
209
|
+
_print_error(
|
|
210
|
+
"computer_use_tools not available.",
|
|
211
|
+
"Install: pip install mss pillow",
|
|
212
|
+
)
|
|
213
|
+
return
|
|
214
|
+
from computer_use_tools import _tool_computer_screenshot, pop_pending_vision_image
|
|
215
|
+
|
|
216
|
+
monitor = int(args.strip()) if args.strip().isdigit() else 1
|
|
217
|
+
if HAS_RICH:
|
|
218
|
+
with console.status("[dim]Capturing screen…[/dim]", spinner="dots"):
|
|
219
|
+
result = _tool_computer_screenshot({"monitor": monitor})
|
|
220
|
+
else:
|
|
221
|
+
result = _tool_computer_screenshot({"monitor": monitor})
|
|
222
|
+
|
|
223
|
+
if result.get("success"):
|
|
224
|
+
d = result["data"]
|
|
225
|
+
b64 = pop_pending_vision_image()
|
|
226
|
+
if b64:
|
|
227
|
+
self.terminal._pending_image = {
|
|
228
|
+
"type": "image_url",
|
|
229
|
+
"image_url": {"url": f"data:image/png;base64,{b64}"},
|
|
230
|
+
}
|
|
231
|
+
if HAS_RICH:
|
|
232
|
+
console.print(Panel(
|
|
233
|
+
f"[green]✓[/green] [dim]{d['width']}×{d['height']} · {d['size_kb']} KB[/dim]\n"
|
|
234
|
+
f"[dim]Screenshot queued — ask your question now[/dim]",
|
|
235
|
+
border_style="dim", box=rich_box.ROUNDED, padding=(0, 1),
|
|
236
|
+
))
|
|
237
|
+
else:
|
|
238
|
+
print(f"Screenshot {d['width']}×{d['height']} ({d['size_kb']} KB) — send your question")
|
|
239
|
+
else:
|
|
240
|
+
_print_error(result.get("error", "Screenshot failed"), "screenshot")
|
|
241
|
+
|
|
242
|
+
def cmd_input(self, args: str):
|
|
243
|
+
raw = args.strip().lower()
|
|
244
|
+
cfg = self.terminal.config
|
|
245
|
+
valid_styles = {"panel", "box", "plain"}
|
|
246
|
+
valid_themes = {"auto", "dark", "light"}
|
|
247
|
+
|
|
248
|
+
def _save_and_show(message: str) -> None:
|
|
249
|
+
save_config(cfg)
|
|
250
|
+
if HAS_RICH:
|
|
251
|
+
console.print(f"[green]✓[/green] {message}")
|
|
252
|
+
console.print(
|
|
253
|
+
f" [dim]style[/dim] {cfg.get('input_style', 'panel')} "
|
|
254
|
+
f"[dim]theme[/dim] {cfg.get('input_theme', 'auto')}"
|
|
255
|
+
)
|
|
256
|
+
else:
|
|
257
|
+
print(message)
|
|
258
|
+
print(f" style {cfg.get('input_style', 'panel')} theme {cfg.get('input_theme', 'auto')}")
|
|
259
|
+
|
|
260
|
+
if not raw or raw in {"status", "show"}:
|
|
261
|
+
style = cfg.get("input_style", "panel")
|
|
262
|
+
theme = cfg.get("input_theme", "auto")
|
|
263
|
+
if HAS_RICH:
|
|
264
|
+
console.print(Panel(
|
|
265
|
+
f"[bold]style[/bold] {style}\n"
|
|
266
|
+
f"[bold]theme[/bold] {theme}\n\n"
|
|
267
|
+
"[dim]Use[/dim] /input panel [dim]for the Codex-style input block[/dim]\n"
|
|
268
|
+
"[dim]Use[/dim] /input theme auto [dim]to follow the terminal/system theme[/dim]",
|
|
269
|
+
title="Input UI",
|
|
270
|
+
border_style="dim",
|
|
271
|
+
box=rich_box.ROUNDED,
|
|
272
|
+
padding=(0, 1),
|
|
273
|
+
))
|
|
274
|
+
else:
|
|
275
|
+
print(f"input style: {style}")
|
|
276
|
+
print(f"input theme: {theme}")
|
|
277
|
+
print("Usage: /input panel|box|plain | /input theme auto|dark|light")
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
if raw == "reset":
|
|
281
|
+
cfg["input_style"] = "panel"
|
|
282
|
+
cfg["input_theme"] = "auto"
|
|
283
|
+
_save_and_show("input UI reset to panel · auto")
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
parts = raw.split()
|
|
287
|
+
if parts[0] == "theme":
|
|
288
|
+
if len(parts) != 2 or parts[1] not in valid_themes:
|
|
289
|
+
msg = "Usage: /input theme auto|dark|light"
|
|
290
|
+
console.print(f"[red]{msg}[/red]" if HAS_RICH else msg)
|
|
291
|
+
return
|
|
292
|
+
cfg["input_theme"] = parts[1]
|
|
293
|
+
_save_and_show(f"input theme set to {parts[1]}")
|
|
294
|
+
return
|
|
295
|
+
|
|
296
|
+
if parts[0] in valid_themes and len(parts) == 1:
|
|
297
|
+
cfg["input_theme"] = parts[0]
|
|
298
|
+
_save_and_show(f"input theme set to {parts[0]}")
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
if parts[0] in valid_styles and len(parts) == 1:
|
|
302
|
+
cfg["input_style"] = parts[0]
|
|
303
|
+
_save_and_show(f"input style set to {parts[0]}")
|
|
304
|
+
return
|
|
305
|
+
|
|
306
|
+
msg = "Usage: /input panel|box|plain | /input theme auto|dark|light | /input reset"
|
|
307
|
+
console.print(f"[red]{msg}[/red]" if HAS_RICH else msg)
|
|
308
|
+
|
|
309
|
+
def cmd_context(self, args: str):
|
|
310
|
+
cfg = self.terminal.config
|
|
311
|
+
conv = self.terminal.conversation
|
|
312
|
+
conv_len = len(conv)
|
|
313
|
+
model_id = cfg.get("model", "qwen2.5:7b")
|
|
314
|
+
thinking = cfg.get("thinking_mode", "auto")
|
|
315
|
+
has_auth = bool(cfg.get("auth_token"))
|
|
316
|
+
local_mode = cfg.get("local_mode", False)
|
|
317
|
+
auto_compact = bool(cfg.get("auto_compact_context", True))
|
|
318
|
+
try:
|
|
319
|
+
auto_compact_threshold = float(cfg.get("auto_compact_threshold", 0.78))
|
|
320
|
+
except Exception:
|
|
321
|
+
auto_compact_threshold = 0.78
|
|
322
|
+
auto_compact_count = int(getattr(self.terminal, "_auto_compact_count", 0) or 0)
|
|
323
|
+
|
|
324
|
+
total_chars = sum(len(m.get("content", "")) for m in conv)
|
|
325
|
+
est_tokens = total_chars // 3
|
|
326
|
+
max_ctx = get_model_cfg(model_id).get("num_ctx", 16384)
|
|
327
|
+
ctx_pct = min(100, int(est_tokens / max_ctx * 100))
|
|
328
|
+
ctx_color = "green" if ctx_pct < 60 else ("yellow" if ctx_pct < 85 else "red")
|
|
329
|
+
|
|
330
|
+
if HAS_RICH:
|
|
331
|
+
console.print()
|
|
332
|
+
console.print("[bold]Current Context[/bold]")
|
|
333
|
+
console.print()
|
|
334
|
+
console.print(f" [dim]{'Model':<20s}[/dim]{model_id}")
|
|
335
|
+
console.print(f" [dim]{'Provider':<20s}[/dim]{'[green]Local (Ollama)[/green]' if local_mode else 'AWS → Ollama fallback'}")
|
|
336
|
+
console.print(f" [dim]{'Thinking':<20s}[/dim]{thinking}")
|
|
337
|
+
console.print(f" [dim]{'Messages':<20s}[/dim]{conv_len}")
|
|
338
|
+
console.print(f" [dim]{'Est. tokens':<20s}[/dim][{ctx_color}]{est_tokens:,} / {max_ctx:,} ({ctx_pct}%)[/{ctx_color}]")
|
|
339
|
+
console.print(f" [dim]{'Authenticated':<20s}[/dim]{'yes' if has_auth else 'no'}")
|
|
340
|
+
console.print(
|
|
341
|
+
f" [dim]{'Auto compact':<20s}[/dim]"
|
|
342
|
+
f"{'on' if auto_compact else 'off'}"
|
|
343
|
+
f" · threshold {int(auto_compact_threshold * 100)}%"
|
|
344
|
+
f" · runs {auto_compact_count}"
|
|
345
|
+
)
|
|
346
|
+
console.print(f" [dim]{'Session':<20s}[/dim]{self.terminal.session_id}")
|
|
347
|
+
console.print(f" [dim]{'Project context':<20s}[/dim]{'loaded' if _PROJECT_CONTEXT else 'none'}")
|
|
348
|
+
wl = cfg.get("watchlist", [])
|
|
349
|
+
if wl:
|
|
350
|
+
console.print(f" [dim]{'Watchlist':<20s}[/dim]{', '.join(wl)}")
|
|
351
|
+
if ctx_pct >= 80:
|
|
352
|
+
console.print(f"\n [yellow]⚠ Context {ctx_pct}% full — use /compact to free space[/yellow]")
|
|
353
|
+
console.print()
|
|
354
|
+
else:
|
|
355
|
+
print(f" Model: {model_id} ({'local' if local_mode else 'aws'})")
|
|
356
|
+
print(f" Messages: {conv_len} Tokens: ~{est_tokens:,}/{max_ctx:,} ({ctx_pct}%)")
|
|
357
|
+
print(f" Auto compact: {'on' if auto_compact else 'off'} threshold={int(auto_compact_threshold * 100)}% runs={auto_compact_count}")
|
|
358
|
+
print(f" Session: {self.terminal.session_id}")
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""WorkflowCommandsMixin — hooks, regen, undo, retry, note, review commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import pathlib
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class WorkflowCommandsMixin:
|
|
11
|
+
"""Mixin: interactive workflow and edit-review commands."""
|
|
12
|
+
|
|
13
|
+
def cmd_hooks(self, args: str):
|
|
14
|
+
global _JSON_HOOKS
|
|
15
|
+
hooks_dirs = [
|
|
16
|
+
CONFIG_DIR / "hooks",
|
|
17
|
+
pathlib.Path.cwd() / ".aria" / "hooks",
|
|
18
|
+
]
|
|
19
|
+
parts = args.strip().split(maxsplit=1)
|
|
20
|
+
sub = parts[0].lower() if parts else "list"
|
|
21
|
+
rest = parts[1].strip() if len(parts) > 1 else ""
|
|
22
|
+
|
|
23
|
+
if sub == "reload":
|
|
24
|
+
if _HAS_JSON_HOOKS:
|
|
25
|
+
try:
|
|
26
|
+
_JSON_HOOKS = _load_hooks()
|
|
27
|
+
n = sum(len(v) for v in _JSON_HOOKS.values())
|
|
28
|
+
if HAS_RICH:
|
|
29
|
+
console.print(f" [green]✓[/green] [dim]hooks.json reloaded ({n} entries)[/dim]")
|
|
30
|
+
else:
|
|
31
|
+
print(f" hooks.json reloaded ({n} entries)")
|
|
32
|
+
except Exception as exc:
|
|
33
|
+
if HAS_RICH:
|
|
34
|
+
console.print(f" [red]✗ reload failed: {exc}[/red]")
|
|
35
|
+
else:
|
|
36
|
+
print(f" reload failed: {exc}")
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
if sub == "list":
|
|
40
|
+
if _HAS_JSON_HOOKS:
|
|
41
|
+
try:
|
|
42
|
+
from apps.cli.hooks import list_hooks as _list_json_hooks
|
|
43
|
+
_json_rows = _list_json_hooks()
|
|
44
|
+
if _json_rows:
|
|
45
|
+
if HAS_RICH:
|
|
46
|
+
console.print()
|
|
47
|
+
console.print(" [bold]JSON Hooks[/bold] [dim](~/.arthera/hooks.json)[/dim]")
|
|
48
|
+
for r in _json_rows:
|
|
49
|
+
_block = " [red][blocking][/red]" if r["blocking"] else ""
|
|
50
|
+
_tool = f"[{r['tool']}]" if r["tool"] != "*" else ""
|
|
51
|
+
console.print(
|
|
52
|
+
f" [cyan]{r['event']:<16}[/cyan]{_tool:<14} "
|
|
53
|
+
f"[dim]{r['command']}[/dim]{_block}"
|
|
54
|
+
)
|
|
55
|
+
else:
|
|
56
|
+
for r in _json_rows:
|
|
57
|
+
print(f" {r['event']:<16} {r['tool']:<12} {r['command']}")
|
|
58
|
+
except Exception:
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
found: list[tuple] = []
|
|
62
|
+
for hdir in hooks_dirs:
|
|
63
|
+
if hdir.exists():
|
|
64
|
+
for f in sorted(hdir.iterdir()):
|
|
65
|
+
if f.is_file() and not f.name.startswith("."):
|
|
66
|
+
found.append((str(hdir), f.name, str(f)))
|
|
67
|
+
if not found:
|
|
68
|
+
if HAS_RICH:
|
|
69
|
+
console.print(f" [dim]No hooks found.[/dim]")
|
|
70
|
+
console.print(f" [dim]Hook dirs:[/dim]")
|
|
71
|
+
for d in hooks_dirs:
|
|
72
|
+
console.print(f" [dim]{_display_path(d, fallback='hook dir')}[/dim]")
|
|
73
|
+
console.print(f" [dim]Events: prompt_submit response_done tool_use compact[/dim]")
|
|
74
|
+
else:
|
|
75
|
+
print("No hooks. Dirs:", [str(d) for d in hooks_dirs])
|
|
76
|
+
return
|
|
77
|
+
if HAS_RICH:
|
|
78
|
+
console.print()
|
|
79
|
+
for hdir, name, path in found:
|
|
80
|
+
console.print(f" [dim]{name:<28}[/dim] {_display_path(path, fallback='hook')}")
|
|
81
|
+
console.print()
|
|
82
|
+
else:
|
|
83
|
+
for hdir, name, path in found:
|
|
84
|
+
print(f" {name} {_display_path(path, fallback='hook')}")
|
|
85
|
+
|
|
86
|
+
elif sub == "edit":
|
|
87
|
+
if not rest:
|
|
88
|
+
if _HAS_JSON_HOOKS:
|
|
89
|
+
from apps.cli.hooks import hooks_file_path, create_example_hooks
|
|
90
|
+
_hpath = hooks_file_path("global")
|
|
91
|
+
create_example_hooks(_hpath)
|
|
92
|
+
editor = os.getenv("EDITOR", "nano")
|
|
93
|
+
try:
|
|
94
|
+
import subprocess as _sp
|
|
95
|
+
_sp.run([editor, str(_hpath)])
|
|
96
|
+
_JSON_HOOKS = _load_hooks()
|
|
97
|
+
except Exception as exc:
|
|
98
|
+
if HAS_RICH:
|
|
99
|
+
console.print(f"[red]Could not open editor: {exc}[/red]")
|
|
100
|
+
else:
|
|
101
|
+
print(f"Could not open editor: {exc}")
|
|
102
|
+
return
|
|
103
|
+
event = rest
|
|
104
|
+
hdir = CONFIG_DIR / "hooks"
|
|
105
|
+
hdir.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
script = hdir / f"{event}.sh"
|
|
107
|
+
if not script.exists():
|
|
108
|
+
script.write_text(
|
|
109
|
+
f"#!/bin/bash\n# Aria hook: {event}\n# "
|
|
110
|
+
f"Env vars: ARIA_EVENT ARIA_TOOL ARIA_TOOL_PARAMS ARIA_RESPONSE ARIA_SESSION\n\n"
|
|
111
|
+
f'echo "Hook {event} fired"\n',
|
|
112
|
+
encoding="utf-8"
|
|
113
|
+
)
|
|
114
|
+
script.chmod(0o755)
|
|
115
|
+
editor = os.getenv("EDITOR", "nano")
|
|
116
|
+
try:
|
|
117
|
+
import subprocess as _sp
|
|
118
|
+
_sp.run([editor, str(script)])
|
|
119
|
+
except Exception as exc:
|
|
120
|
+
console.print(f"[red]Could not open editor: {exc}[/red]" if HAS_RICH else str(exc))
|
|
121
|
+
|
|
122
|
+
elif sub == "run":
|
|
123
|
+
event = rest or "ResponseDone"
|
|
124
|
+
if _HAS_JSON_HOOKS:
|
|
125
|
+
_fire_json_hook(event, session_id=getattr(self.terminal, "session_id", ""), hooks=_JSON_HOOKS)
|
|
126
|
+
_run_event_hook(event, {"ARIA_EVENT": event, "ARIA_SESSION": getattr(self.terminal, "session_id", "")})
|
|
127
|
+
if HAS_RICH:
|
|
128
|
+
console.print(f" [dim]Hook '{event}' triggered[/dim]")
|
|
129
|
+
else:
|
|
130
|
+
print(f"Hook '{event}' triggered")
|
|
131
|
+
|
|
132
|
+
else:
|
|
133
|
+
if HAS_RICH:
|
|
134
|
+
console.print("[dim]Usage: /hooks list|edit [event]|reload|run [event][/dim]")
|
|
135
|
+
else:
|
|
136
|
+
print("Usage: /hooks list|edit [event]|reload|run [event]")
|
|
137
|
+
|
|
138
|
+
async def cmd_regen(self, args: str):
|
|
139
|
+
last_user_msg = None
|
|
140
|
+
for i in range(len(self.terminal.conversation) - 1, -1, -1):
|
|
141
|
+
if self.terminal.conversation[i]["role"] == "assistant":
|
|
142
|
+
self.terminal.conversation.pop(i)
|
|
143
|
+
break
|
|
144
|
+
for msg in reversed(self.terminal.conversation):
|
|
145
|
+
if msg["role"] == "user":
|
|
146
|
+
last_user_msg = msg["content"]
|
|
147
|
+
break
|
|
148
|
+
if last_user_msg:
|
|
149
|
+
for i in range(len(self.terminal.conversation) - 1, -1, -1):
|
|
150
|
+
if self.terminal.conversation[i]["role"] == "user" and self.terminal.conversation[i]["content"] == last_user_msg:
|
|
151
|
+
self.terminal.conversation.pop(i)
|
|
152
|
+
break
|
|
153
|
+
console.print("[dim]Regenerating...[/dim]" if HAS_RICH else "Regenerating...")
|
|
154
|
+
await self.terminal.send_message(last_user_msg)
|
|
155
|
+
else:
|
|
156
|
+
console.print("[dim]No message to regenerate[/dim]" if HAS_RICH else "Nothing to regenerate")
|
|
157
|
+
|
|
158
|
+
def cmd_undo(self, args: str):
|
|
159
|
+
if len(self.terminal.conversation) < 2:
|
|
160
|
+
console.print("[dim]Nothing to undo[/dim]" if HAS_RICH else "Nothing to undo")
|
|
161
|
+
return
|
|
162
|
+
removed = 0
|
|
163
|
+
for role in ("assistant", "user"):
|
|
164
|
+
for i in range(len(self.terminal.conversation) - 1, -1, -1):
|
|
165
|
+
if self.terminal.conversation[i]["role"] == role:
|
|
166
|
+
self.terminal.conversation.pop(i)
|
|
167
|
+
removed += 1
|
|
168
|
+
break
|
|
169
|
+
if HAS_RICH:
|
|
170
|
+
console.print(f"[dim]Undone ({removed} messages removed, {len(self.terminal.conversation)} remaining)[/dim]")
|
|
171
|
+
else:
|
|
172
|
+
print(f"Undone ({removed} removed)")
|
|
173
|
+
|
|
174
|
+
async def cmd_retry(self, args: str):
|
|
175
|
+
last_user_msg = None
|
|
176
|
+
for i in range(len(self.terminal.conversation) - 1, -1, -1):
|
|
177
|
+
if self.terminal.conversation[i]["role"] == "assistant":
|
|
178
|
+
self.terminal.conversation.pop(i)
|
|
179
|
+
break
|
|
180
|
+
for msg in reversed(self.terminal.conversation):
|
|
181
|
+
if msg["role"] == "user":
|
|
182
|
+
last_user_msg = msg["content"]
|
|
183
|
+
break
|
|
184
|
+
if not last_user_msg:
|
|
185
|
+
console.print("[dim]No message to retry[/dim]" if HAS_RICH else "Nothing to retry")
|
|
186
|
+
return
|
|
187
|
+
for i in range(len(self.terminal.conversation) - 1, -1, -1):
|
|
188
|
+
if self.terminal.conversation[i]["role"] == "user" and self.terminal.conversation[i]["content"] == last_user_msg:
|
|
189
|
+
self.terminal.conversation.pop(i)
|
|
190
|
+
break
|
|
191
|
+
orig_model_key = resolve_model_key(self.terminal.config.get("model", "qwen2.5:7b"))
|
|
192
|
+
_fallback_model = MODELS.get("qwen-fast") or MODELS.get("qwen7b") or next(iter(MODELS.values()))
|
|
193
|
+
orig_temp = MODELS.get(orig_model_key, _fallback_model).get("temperature", 0.3)
|
|
194
|
+
MODELS[orig_model_key]["temperature"] = min(0.9, orig_temp + 0.3)
|
|
195
|
+
if HAS_RICH:
|
|
196
|
+
console.print(f"[dim]Retrying with temperature {MODELS[orig_model_key]['temperature']:.1f}...[/dim]")
|
|
197
|
+
else:
|
|
198
|
+
print(f"Retrying (temp +0.3)...")
|
|
199
|
+
try:
|
|
200
|
+
await self.terminal.send_message(last_user_msg)
|
|
201
|
+
finally:
|
|
202
|
+
MODELS[orig_model_key]["temperature"] = orig_temp
|
|
203
|
+
|
|
204
|
+
def cmd_note(self, args: str):
|
|
205
|
+
text = args.strip()
|
|
206
|
+
if not text:
|
|
207
|
+
console.print("[dim]Usage: /note <text>[/dim]" if HAS_RICH else "Usage: /note <text>")
|
|
208
|
+
return
|
|
209
|
+
aria_md = pathlib.Path.cwd() / "ARIA.md"
|
|
210
|
+
now_str = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
211
|
+
entry = f"\n- [{now_str}] {text}"
|
|
212
|
+
if aria_md.exists():
|
|
213
|
+
content = aria_md.read_text(encoding="utf-8")
|
|
214
|
+
if "## Notes" not in content:
|
|
215
|
+
content += "\n\n## Notes\n"
|
|
216
|
+
content += entry
|
|
217
|
+
else:
|
|
218
|
+
content = f"# Aria Project Notes\n\n## Notes\n{entry}\n"
|
|
219
|
+
aria_md.write_text(content, encoding="utf-8")
|
|
220
|
+
global _PROJECT_CONTEXT
|
|
221
|
+
_PROJECT_CONTEXT = _load_project_context()
|
|
222
|
+
if HAS_RICH:
|
|
223
|
+
console.print(f"[dim]Note saved to {aria_md.name}[/dim]")
|
|
224
|
+
else:
|
|
225
|
+
print(f"Saved to {aria_md.name}")
|
|
226
|
+
|
|
227
|
+
async def cmd_review(self, args: str):
|
|
228
|
+
raw = args.strip()
|
|
229
|
+
policy = self.terminal.config.get("command_policy", "safe")
|
|
230
|
+
|
|
231
|
+
if raw and not raw.startswith("--"):
|
|
232
|
+
p = pathlib.Path(raw).expanduser()
|
|
233
|
+
if not p.exists():
|
|
234
|
+
msg = f"File not found: {raw}"
|
|
235
|
+
console.print(f"[red]{msg}[/red]") if HAS_RICH else print(msg)
|
|
236
|
+
return
|
|
237
|
+
_print_phase("Reading file")
|
|
238
|
+
try:
|
|
239
|
+
content = p.read_text(errors="replace")[:12000]
|
|
240
|
+
except Exception as e:
|
|
241
|
+
console.print(f"[red]Cannot read file: {e}[/red]") if HAS_RICH else print(f"Cannot read: {e}")
|
|
242
|
+
return
|
|
243
|
+
line_count = content.count("\n")
|
|
244
|
+
if HAS_RICH:
|
|
245
|
+
console.print(f" [dim]↳ {p.name} · {line_count} lines[/dim]")
|
|
246
|
+
_print_phase("AI Review")
|
|
247
|
+
prompt = (
|
|
248
|
+
f"请对以下 `{p.name}` 的代码进行专业审查,查找 Bug、安全问题和改进点。\n"
|
|
249
|
+
f"每条发现用严重程度标签开头:**BUG**、**IMPROVEMENT**、**NIT**。\n"
|
|
250
|
+
f"按文件组织输出,直接给结论,不要重复贴出全部代码。\n\n"
|
|
251
|
+
f"```\n{content}\n```"
|
|
252
|
+
)
|
|
253
|
+
else:
|
|
254
|
+
diff_cmd = "git diff --staged" if raw.startswith("--staged") else "git diff HEAD"
|
|
255
|
+
_print_phase("Reading diff")
|
|
256
|
+
tr = _tool_run_command({"command": diff_cmd})
|
|
257
|
+
if not tr.get("success"):
|
|
258
|
+
msg = tr.get("error", "git diff failed")
|
|
259
|
+
console.print(f"[red]{msg}[/red]") if HAS_RICH else print(msg)
|
|
260
|
+
return
|
|
261
|
+
diff_text = (tr.get("data") or {}).get("stdout", "").strip()
|
|
262
|
+
if not diff_text:
|
|
263
|
+
console.print("[dim]No changes to review.[/dim]") if HAS_RICH else print("No changes to review.")
|
|
264
|
+
return
|
|
265
|
+
_adds = diff_text.count("\n+") - diff_text.count("\n+++")
|
|
266
|
+
_dels = diff_text.count("\n-") - diff_text.count("\n---")
|
|
267
|
+
_files = diff_text.count("\ndiff --git")
|
|
268
|
+
if HAS_RICH:
|
|
269
|
+
console.print(f" [dim]↳ {_files} files · +{_adds} −{_dels} lines[/dim]")
|
|
270
|
+
diff_text = diff_text[:12000]
|
|
271
|
+
_print_phase("AI Review")
|
|
272
|
+
prompt = (
|
|
273
|
+
"请审查以下 git diff,找出 Bug、潜在回归、安全问题和代码质量问题。\n"
|
|
274
|
+
"每条发现用严重程度标签开头:**BUG**、**IMPROVEMENT**、**NIT**。\n"
|
|
275
|
+
"按文件分组,直接给出结论。\n\n"
|
|
276
|
+
f"```diff\n{diff_text}\n```"
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
await self.terminal.send_message(prompt)
|