fix(ui): streamline composer goal workflow

Move Research Goal entry into the chat composer: the + menu now switches the
normal input into goal mode, sends create the session goal directly, and the
active goal stays above the input as an inline chip with expandable criteria
and evidence details.

Stabilize the local dev launcher used for browser smoke tests by wiring custom
backend ports into VITE_API_URL, launching long-lived process groups, cleaning
up child processes reliably, and reporting actual frontend/backend status.
This commit is contained in:
Haozhe Wu committed 2026-05-25 00:41:31 +08:00
1 parent 2a18d33c23
commit 8b2273c6de
8 files changed
+283 -418

No files matched your search

+4 -2
View File
@@ -46,12 +46,14 @@
## 📰 News
- **2026-05-25** 🧼 **Cleaner chat UI + composer workflow**: The Web UI keeps chat focused on the next action: upload, swarm, and research-goal modes now live behind the composer `+` menu instead of floating panels. Active context appears above the input as compact chips, and goal details expand inline only when needed. The UI also drops the old custom i18n layer in favor of direct English copy, gates Full Report cards to report-worthy runs, and hardens local dev startup/status reporting for reliable browser smoke tests.
- **2026-05-24** 🎯 **Research Goal runtime**: Added a session-scoped Research Goal layer across backend, CLI, API/MCP, SSE, and Web UI. Goals persist claims, acceptance criteria, evidence rows, budgets, and completion policy; agent tools can create goals and attach evidence; `/goal` gives the CLI a direct entry point; REST/MCP expose goal snapshots and evidence writes; SSE keeps chat clients fresh. Follow-up audit fixes locked down verified evidence, blocked live-trading risk tiers through agent tools, wired CLI-created goals into later turns, cleaned goal ledgers on session deletion, enabled replay-all, and fixed cross-session frontend races.
- **2026-05-23** 🖥️ **Interactive CLI refresh**: The terminal front door now opens with a larger Vibe-Trading banner, a cleaner prompt divider, prior-turn recap, post-run timing, and a Claude Code-style activity rail for live agent work. Tool calls, web/data fetches, shell-style actions, Markdown answers, and pipe tables render in a more readable transcript, while piped or non-TTY runs keep plain-text output for automation. Generated CLI screenshots are now treated as local artifacts instead of committed docs files, keeping the repository lighter.
- **2026-05-22** 🧭 **Swarm recovery + MCP keepalive**: Swarm status now reconciles from live task files on every read, so API/MCP/SSE/list views recover crashed or stale runs instead of showing permanent `running` snapshots. `run_swarm` sends MCP progress heartbeats while it polls, with a fixed first frame of `swarm_started run_id=<id>` for clients that reconnect after transport drops; workers now heartbeat through LLM streaming, grounding fetches, and tool execution. The stale-run reaper uses per-run thresholds and derives terminal status from task states, `SwarmTool` no longer cancels a still-running team just because its wait budget elapsed, and MCP clients can call `reap_stale_runs()` for explicit cleanup. Today's DX pass also refreshed provider default models and aligned CI syntax checks with the new `agent/cli/` package. 22 new regressions cover hydration, terminal recovery, stale reaping, keepalive cadence, env parsing, and heartbeat wiring; the full swarm/MCP suite is at 169 passed, 4 skipped.
- **2026-05-21** 🧱 **CLI package refactor**: `agent/cli.py` (3216 LOC) split into the `agent/cli/` package — interactive front door, slash router, Rich components, plus a `_legacy.py` shim that preserves every subcommand and re-exports every public symbol so `cli.cmd_*` / `cli._INIT_ENV_PATH` / `cli.Confirm` keep working. New FastAPI middleware serves the SPA shell when a browser opens `/runs/{id}` or `/correlation` directly; same narrowing landed in the Vite dev proxy. Version unified via `cli/_version.py` (no more drift between `--version` and the banner), `python -m cli` restored via `__main__.py`, and the chat-gate narrowed so `chat --help` / `chat extra` reach legacy argparse instead of being swallowed by the REPL.
<details>
<summary>Earlier news</summary>
- **2026-05-22** 🧭 **Swarm recovery + MCP keepalive**: Swarm status now reconciles from live task files on every read, so API/MCP/SSE/list views recover crashed or stale runs instead of showing permanent `running` snapshots. `run_swarm` sends MCP progress heartbeats while it polls, with a fixed first frame of `swarm_started run_id=<id>` for clients that reconnect after transport drops; workers now heartbeat through LLM streaming, grounding fetches, and tool execution. The stale-run reaper uses per-run thresholds and derives terminal status from task states, `SwarmTool` no longer cancels a still-running team just because its wait budget elapsed, and MCP clients can call `reap_stale_runs()` for explicit cleanup. Today's DX pass also refreshed provider default models and aligned CI syntax checks with the new `agent/cli/` package. 22 new regressions cover hydration, terminal recovery, stale reaping, keepalive cadence, env parsing, and heartbeat wiring; the full swarm/MCP suite is at 169 passed, 4 skipped.
- **2026-05-21** 🧱 **CLI package refactor**: `agent/cli.py` (3216 LOC) split into the `agent/cli/` package — interactive front door, slash router, Rich components, plus a `_legacy.py` shim that preserves every subcommand and re-exports every public symbol so `cli.cmd_*` / `cli._INIT_ENV_PATH` / `cli.Confirm` keep working. New FastAPI middleware serves the SPA shell when a browser opens `/runs/{id}` or `/correlation` directly; same narrowing landed in the Vite dev proxy. Version unified via `cli/_version.py` (no more drift between `--version` and the banner), `python -m cli` restored via `__main__.py`, and the chat-gate narrowed so `chat --help` / `chat extra` reach legacy argparse instead of being swallowed by the REPL.
- **2026-05-20** 🔬 **Hypothesis Registry CLI**: Closes the CLI side of the Hypothesis Registry shipped backend-only on 2026-05-16. `vibe-trading hypothesis list` prints a Rich table or JSON (`--status` filter, `--limit`); `show <id>` renders a detail panel including linked run cards; `invalidate <id> --note "..."` flips status to `rejected` while preserving prior invalidation notes when `--note` is omitted. Honors the existing `VIBE_TRADING_HYPOTHESES_PATH` env override and adds a per-invocation `--path`. 22 new tests cover wiring, JSON output, status filter, limit, missing-id errors, and note persistence.
- **2026-05-19** ✨ **Live tool feedback + graceful cancel**: Long-running tools (backtests, large PDFs, swarm workers) no longer look frozen. Each tool call now emits a 3-second heartbeat plus structured per-stage progress — `run_backtest` shows phase markers (`validate` / `simulate` / `finalize`), `read_document` ticks per page on PDF or per sheet on Excel, `read_url` marks `fetch` / `parse`. The CLI Rich Live dashboard renders a Unicode spinner, ASCII progress bar, ETA, and stacks up to 3 parallel tools keyed by name; the frontend chat ships a new `ToolProgressIndicator` with rAF-coalesced renders, ARIA `role="status"` + hidden native `<progress>` for screen readers, and a determinate `ProgressRing` SVG when total is known. First `Ctrl+C` during a CLI run now calls `agent.cancel()` for graceful exit (current step finishes, trace closes cleanly); a second within 2s force-quits. Reusable primitives extracted along the way: `ProgressBar.tsx` and `lib/tools.ts` (shared tool-name i18n).
- **2026-05-18** 🧹 **Cleanup pass + three latent bug fixes**: `CompositeEngine` no longer misroutes bare Chinese-futures codes like `RB2410` to `GlobalFuturesEngine` — `_is_china_futures` moved into a shared `_market_hooks` module with a case-normalized product table and a non-CN exchange guard, plus 9 new regression cases. Session FTS5 indexes now persist timestamps so cross-session search can sort by date; the same path also fixed a re-upsert that was wall-clocking every session's `started_at`. The Vite dev-mode proxy gained the missing `/alpha` entry so the AlphaZoo page resolves on `npm run dev`. `tests/test_e2e_harness_v2.py` (real-LLM e2e suite) is now gated behind `VIBE_TRADING_RUN_LIVE_E2E=1` so CI no longer changes shape based on env-key presence. Ruff `per-file-ignores` added for the factor zoo (3783 → 0 F401 noise), frontend tsconfig enables `noUnusedLocals` / `noUnusedParameters` as regression guards, and 76 unused `vw = vwap(...)` boilerplate lines were dropped from `gtja191` alphas. Net **-918 LOC**.
+6 -3
View File
@@ -46,12 +46,15 @@
## 📰 الأخبار
- **2026-05-22** 🧭 **استعادة Swarm + إبقاء MCP حياً**: أصبحت حالة Swarm تُصالح من ملفات المهام الحية عند كل قراءة، لذلك تستعيد عروض API/MCP/SSE/list التشغيلات التي تعطلت أو صارت stale بدلاً من عرض لقطة `running` للأبد. يرسل `run_swarm` نبضات MCP progress أثناء polling، مع إطار أول ثابت `swarm_started run_id=<id>` كي يستطيع العملاء استعادة المقبض بعد سقوط النقل؛ كما يصدر worker نبضات خلال LLM streaming وgrounding fetch وتنفيذ الأدوات. يستخدم stale-run reaper عتبات خاصة بكل run ويستنتج الحالة النهائية من حالات المهام. لم يعد `SwarmTool` يلغي team ما زال يعمل لمجرد انتهاء wait budget، ويمكن لعملاء MCP استدعاء `reap_stale_runs()` للتنظيف الصريح. حدّثت دفعة DX اليوم أيضاً النماذج الافتراضية للمزودين، وواءمت فحص CI syntax مع حزمة `agent/cli/` الجديدة. تغطي 22 اختباراً انحدارياً جديداً hydration، واستعادة الحالات النهائية، وجمع التشغيلات stale، وإيقاع keepalive، وتحمل env parsing، وربط heartbeat؛ ومجموعة swarm/MCP الكاملة عند 169 passed و4 skipped.
- **2026-05-21** 🧱 **إعادة هيكلة حزمة CLI**: تقسيم `agent/cli.py` (3216 سطراً) إلى حزمة `agent/cli/` — واجهة تفاعلية، موجّه slash، مكوّنات Rich، وطبقة `_legacy.py` تحافظ على كل الأوامر الفرعية وتعيد تصدير كل الرموز العامة فتبقى `cli.cmd_*` / `cli._INIT_ENV_PATH` / `cli.Confirm` كما هي. Middleware جديد في FastAPI يخدم قشرة SPA عند فتح `/runs/{id}` أو `/correlation` مباشرة من المتصفح، مع نفس التضييق في بروكسي Vite للتطوير. توحيد سلسلة الإصدار عبر `cli/_version.py` (إنهاء الانحراف بين `--version` والبانر)، استعادة `python -m cli` عبر `__main__.py`، وتضييق بوابة chat بحيث تصل `chat --help` / `chat extra` إلى argparse القديم بدلاً من ابتلاع REPL لها.
- **2026-05-20** 🔬 **Hypothesis Registry CLI**: استكمال جانب CLI لـ Hypothesis Registry الذي شُحن backend فقط في 2026-05-16. يُخرج `vibe-trading hypothesis list` جدول Rich أو JSON (مع فلتر `--status` و`--limit`)؛ يعرض `show <id>` لوحة تفاصيل تتضمن run cards المرتبطة؛ يقلب `invalidate <id> --note "..."` الحالة إلى `rejected` ويُبقي ملاحظات الإبطال السابقة عند حذف `--note`. متغير البيئة `VIBE_TRADING_HYPOTHESES_PATH` ما زال مدعوماً، مع إضافة `--path` لكل استدعاء. تغطي 22 اختباراً جديداً الربط، إخراج JSON، فلتر الحالة، الحد، أخطاء معرّف مفقود، وثبات الملاحظات.
- **2026-05-25** 🧼 **واجهة Chat أنظف + سير composer**: أصبحت واجهة Web UI تترك التركيز للمدخل التالي: انتقلت أوضاع upload وswarm وresearch-goal إلى قائمة `+` في composer بدلاً من لوحات عائمة تقاطع المحادثة. يظهر السياق النشط فوق حقل الإدخال كشرائح compact، ولا تتوسع تفاصيل goal إلا inline عند النقر على الشريحة. أزيلت طبقة i18n المخصصة القديمة لصالح نصوص إنجليزية مباشرة، وتظهر بطاقة Full Report فقط للتشغيلات ذات تقرير فعلي، كما أصبح تشغيل التطوير المحلي وتقارير الحالة أكثر ثباتاً لاختبارات browser smoke.
- **2026-05-24** 🎯 **Research Goal runtime**: أضيفت طبقة Research Goal مرتبطة بالجلسة عبر backend وCLI وAPI/MCP وSSE وWeb UI. تحفظ الأهداف claims وacceptance criteria وevidence rows وbudgets وcompletion policy؛ تستطيع agent tools إنشاء الأهداف وإضافة evidence؛ أصبح `/goal` مدخل CLI؛ تعرض REST/MCP لقطات goal وكتابات evidence؛ وتحافظ SSE على حداثة حالة chat clients. أغلقت إصلاحات audit اللاحقة مسارات verified evidence، ومنعت live-trading risk tiers عبر agent tools، وربطت goals المنشأة من CLI بالمنعطفات اللاحقة، ونظفت goal ledger عند حذف الجلسة، ووصلت replay-all، وأصلحت race في frontend snapshot بين الجلسات.
- **2026-05-23** 🖥️ **تحديث CLI التفاعلي**: تفتح واجهة الطرفية الآن ببانر Vibe-Trading أكبر، وفاصل prompt أوضح، وملخص للدورة السابقة، وتوقيت بعد التشغيل، ومسار نشاط بأسلوب Claude Code لعمل الوكيل الحي. تُعرض استدعاءات الأدوات، وجلب الويب/البيانات، وأفعال نمط shell، وإجابات Markdown، وجداول pipe كسجل أكثر قابلية للقراءة، بينما تحافظ تشغيلات pipe أو non-TTY على إخراج نصي مناسب للأتمتة. أصبحت لقطات CLI المولدة artifacts محلية بدلاً من ملفات docs ملتزم بها، مما يبقي المستودع أخف.
<details>
<summary>أخبار سابقة</summary>
- **2026-05-22** 🧭 **استعادة Swarm + إبقاء MCP حياً**: أصبحت حالة Swarm تُصالح من ملفات المهام الحية عند كل قراءة، لذلك تستعيد عروض API/MCP/SSE/list التشغيلات التي تعطلت أو صارت stale بدلاً من عرض لقطة `running` للأبد. يرسل `run_swarm` نبضات MCP progress أثناء polling، مع إطار أول ثابت `swarm_started run_id=<id>` كي يستطيع العملاء استعادة المقبض بعد سقوط النقل؛ كما يصدر worker نبضات خلال LLM streaming وgrounding fetch وتنفيذ الأدوات. يستخدم stale-run reaper عتبات خاصة بكل run ويستنتج الحالة النهائية من حالات المهام. لم يعد `SwarmTool` يلغي team ما زال يعمل لمجرد انتهاء wait budget، ويمكن لعملاء MCP استدعاء `reap_stale_runs()` للتنظيف الصريح. حدّثت دفعة DX اليوم أيضاً النماذج الافتراضية للمزودين، وواءمت فحص CI syntax مع حزمة `agent/cli/` الجديدة. تغطي 22 اختباراً انحدارياً جديداً hydration، واستعادة الحالات النهائية، وجمع التشغيلات stale، وإيقاع keepalive، وتحمل env parsing، وربط heartbeat؛ ومجموعة swarm/MCP الكاملة عند 169 passed و4 skipped.
- **2026-05-21** 🧱 **إعادة هيكلة حزمة CLI**: تقسيم `agent/cli.py` (3216 سطراً) إلى حزمة `agent/cli/` — واجهة تفاعلية، موجّه slash، مكوّنات Rich، وطبقة `_legacy.py` تحافظ على كل الأوامر الفرعية وتعيد تصدير كل الرموز العامة فتبقى `cli.cmd_*` / `cli._INIT_ENV_PATH` / `cli.Confirm` كما هي. Middleware جديد في FastAPI يخدم قشرة SPA عند فتح `/runs/{id}` أو `/correlation` مباشرة من المتصفح، مع نفس التضييق في بروكسي Vite للتطوير. توحيد سلسلة الإصدار عبر `cli/_version.py` (إنهاء الانحراف بين `--version` والبانر)، استعادة `python -m cli` عبر `__main__.py`، وتضييق بوابة chat بحيث تصل `chat --help` / `chat extra` إلى argparse القديم بدلاً من ابتلاع REPL لها.
- **2026-05-20** 🔬 **Hypothesis Registry CLI**: استكمال جانب CLI لـ Hypothesis Registry الذي شُحن backend فقط في 2026-05-16. يُخرج `vibe-trading hypothesis list` جدول Rich أو JSON (مع فلتر `--status` و`--limit`)؛ يعرض `show <id>` لوحة تفاصيل تتضمن run cards المرتبطة؛ يقلب `invalidate <id> --note "..."` الحالة إلى `rejected` ويُبقي ملاحظات الإبطال السابقة عند حذف `--note`. متغير البيئة `VIBE_TRADING_HYPOTHESES_PATH` ما زال مدعوماً، مع إضافة `--path` لكل استدعاء. تغطي 22 اختباراً جديداً الربط، إخراج JSON، فلتر الحالة، الحد، أخطاء معرّف مفقود، وثبات الملاحظات.
- **2026-05-19** ✨ **تغذية راجعة حيّة للأدوات + إلغاء سلس**: لم تعد الأدوات الطويلة (backtests، PDF كبيرة، عمّال swarm) تبدو متجمدة. كل استدعاء أداة يُصدر الآن نبضة قلب كل 3 ثوانٍ، بالإضافة إلى تقدّم مرحلي مهيكل — يُظهر `run_backtest` علامات الأطوار (`validate` / `simulate` / `finalize`)، ويُحدّث `read_document` عدّاد كل صفحة على PDF أو كل ورقة على Excel، ويُعلِم `read_url` بمرحلتي `fetch` / `parse`. تعرض لوحة Rich Live في CLI دوّاراً Unicode وشريط تقدّم ASCII وETA، وتُكدّس حتى 3 أدوات متوازية مفهرسة بالاسم. تضيف الواجهة الأمامية مكوّن `ToolProgressIndicator` جديد مع تجميع rAF، وARIA `role="status"` + `<progress>` أصلي مخفي لقارئات الشاشة، وSVG `ProgressRing` حتمي عندما يكون المجموع معروفاً. أول `Ctrl+C` أثناء تشغيل CLI يستدعي الآن `agent.cancel()` للخروج السلس (تكتمل الخطوة الحالية وتُغلق التتبعات بنظافة)، والثاني خلال ثانيتين يفرض الإنهاء. تم استخراج عناصر أساسية قابلة لإعادة الاستخدام: `ProgressBar.tsx` و`lib/tools.ts` (تعيين i18n لأسماء الأدوات المشترك).
- **2026-05-18** 🧹 **تنظيف + إصلاح 3 أخطاء كامنة**: لم يعد `CompositeEngine` يوجّه رموز العقود الآجلة الصينية بدون لاحقة (مثل `RB2410`) إلى `GlobalFuturesEngine` بشكل خاطئ — انتقل `_is_china_futures` إلى وحدة `_market_hooks` المشتركة مع تطبيع حالة جدول المنتجات + حارس لبورصة غير صينية، وأُضيفت 9 حالات اختبار انحدار. تحفظ فهارس FTS5 للجلسات الآن الطوابع الزمنية، فيمكن لبحث الجلسات الفرز بالتاريخ، ونفس التغيير أصلح مسار إعادة الإدراج الذي كان يستبدل `started_at` بساعة الحائط في كل مرة. أُضيف `/alpha` المفقود إلى بروكسي تطوير Vite، فتُحلّ صفحة AlphaZoo الآن على `npm run dev`. تم تقييد `tests/test_e2e_harness_v2.py` (مجموعة e2e بـ LLM حقيقي) خلف `VIBE_TRADING_RUN_LIVE_E2E=1` كي لا تغيّر CI شكلها بناءً على وجود مفتاح البيئة. أُضيفت إلى ruff قاعدة `per-file-ignores` لمكتبة المعاملات (الضوضاء F401 من 3783 إلى 0)، وفُعِّلت `noUnusedLocals` / `noUnusedParameters` في tsconfig الواجهة كحواجز انحدار، وحُذف 76 سطراً من نموذج `vw = vwap(...)` غير المستخدم في ملفات `gtja191`. الصافي **-918 سطراً**.
- **2026-05-17** 🧬 **Alpha Zoo v1 (0.1.8)**: 452 ألفا كمّي جاهز عبر 4 zoos — `qlib158` (ميزات Alpha158 من Microsoft Qlib، إسناد Apache-2.0)، `alpha101` (إعادة تنفيذ "101 Formulaic Alphas" من Kakushadze بناءً على ورقة arXiv:1601.00991)، `gtja191` (تقرير بحث Guotai Junan 2014 لعوامل تداول قصيرة الأجل)، `academic` (Fama-French 5 + Carhart momentum كـ proxy قائم على الأسعار). سطر أوامر واحد للـ bench على أي universe: `vibe-trading alpha bench --zoo gtja191 --universe csi300 --period 2018-2025`. تتضمن بوابة AST للنقاء، اختبار حماية lookahead، عزل الشبكة عبر `pytest-socket`، LICENSE.md لكل zoo، وسير عمل توقيع DCO لمساهمات المجتمع. تقديم Alpha Library تلقائياً على [vibetrading.wiki/alpha-library/](https://vibetrading.wiki/alpha-library/)، مع منشور Research Lab [Which of the 191 GTJA alphas still work in 2026?](https://vibetrading.wiki/research-lab/posts/alpha-191-in-2026.html).
+6 -3
View File
@@ -46,12 +46,15 @@
## 📰 ニュース
- **2026-05-22** 🧭 **Swarm リカバリ + MCP keepalive**: Swarm の状態は読み取りのたびに live task ファイルから reconcile されるようになり、API/MCP/SSE/list ビューはクラッシュ済みまたは stale な run を復旧し、永遠に `running` のスナップショットを見せ続けません。`run_swarm` は polling 中に MCP progress heartbeat を送り、transport drop 後に再接続するクライアントでも handle を拾えるよう最初のフレームを `swarm_started run_id=<id>` に固定しました。worker も LLM streaming、grounding fetch、tool execution の各段階で heartbeat を出します。stale-run reaper は run ごとの閾値を使い、task 状態から終端状態を導出します。`SwarmTool` は待機予算が尽きても進行中の team をキャンセルせず、MCP クライアントは `reap_stale_runs()` で明示的に cleanup できます。今日の DX pass では provider の既定モデルも更新し、CI syntax check を新しい `agent/cli/` パッケージに合わせました。hydrate、終端復旧、stale reap、keepalive cadence、env parsing、heartbeat wiring を 22 件の新規回帰テストでカバーし、swarm/MCP 全体スイートは 169 passed、4 skipped です。
- **2026-05-21** 🧱 **CLI パッケージリファクタ**: `agent/cli.py`(3216 LOC)を `agent/cli/` パッケージへ分割 — インタラクティブな入口、slash ルーター、Rich コンポーネント、そしてすべてのサブコマンドを保ち `cli.cmd_*` / `cli._INIT_ENV_PATH` / `cli.Confirm` などの公開シンボルを再エクスポートする `_legacy.py` shim。新しい FastAPI ミドルウェアはブラウザが `/runs/{id}` または `/correlation` を直接開いた際に SPA シェルを返し、同じ絞り込みを Vite dev プロキシにも反映。バージョン文字列は `cli/_version.py` で一本化(`--version` とバナーのドリフト解消)、`python -m cli` を `__main__.py` で復活、chat ゲートを絞り `chat --help` / `chat extra` は REPL に飲み込まれずレガシー argparse に届きます。
- **2026-05-20** 🔬 **Hypothesis Registry CLI**: 2026-05-16 にバックエンドのみで公開された Hypothesis Registry の CLI 側を完成させました。`vibe-trading hypothesis list` は Rich テーブルまたは JSON を出力(`--status` フィルタと `--limit` をサポート)、`show <id>` はリンクされた run card を含む詳細パネルを描画、`invalidate <id> --note "..."` はステータスを `rejected` に切り替え、`--note` を省略すると既存の invalidation notes を保持します。既存の `VIBE_TRADING_HYPOTHESES_PATH` 環境変数オーバーライドに加え、呼び出し単位の `--path` も使えます。配線、JSON 出力、ステータスフィルタ、limit、ID 不在エラー、ノート永続化を 22 のテストでカバー。
- **2026-05-25** 🧼 **よりクリーンな Chat UI + composer workflow**: Web UI は次の入力に集中できる形になりました。upload、swarm、research-goal mode は composer の `+` メニューにまとまり、floating panel で会話を邪魔しません。現在の context は input 上の compact chip として表示され、goal details は chip クリック時だけ inline 展開されます。旧 custom i18n layer も削除し、直接 English copy に統一。Full Report card は report-worthy run のみに表示され、local dev startup/status reporting もブラウザ smoke test 向けに安定化しました。
- **2026-05-24** 🎯 **Research Goal runtime**: backend、CLI、API/MCP、SSE、Web UI をまたぐ session-scoped Research Goal layer を追加しました。Goal は claim、acceptance criteria、evidence row、budget、completion policy を永続化します。agent tool は goal 作成と evidence 追加に対応し、`/goal` が CLI 入口になり、REST/MCP は goal snapshot と evidence write を公開し、SSE は chat client の状態を fresh に保ちます。後続 audit fixes では verified evidence をロックダウンし、agent tool からの live-trading risk tier をブロックし、CLI-created goal を後続 turn に接続し、session 削除時の goal ledger cleanup、replay-all 接続、frontend の cross-session snapshot race 修正を行いました。
- **2026-05-23** 🖥️ **インタラクティブ CLI の刷新**: ターミナル入口は大きな Vibe-Trading バナー、より見やすい prompt 区切り、前ターンの recap、実行後の所要時間、Claude Code 風の activity rail で live agent 作業を表示します。tool call、web/data fetch、shell 風 action、Markdown 回答、pipe table は読みやすい transcript として描画され、pipe や非 TTY 実行では自動化向けの plain-text 出力を維持します。生成 CLI スクリーンショットは committed docs ではなく local artifact として扱い、リポジトリを軽く保ちます。
<details>
<summary>過去のニュース</summary>
- **2026-05-22** 🧭 **Swarm リカバリ + MCP keepalive**: Swarm の状態は読み取りのたびに live task ファイルから reconcile されるようになり、API/MCP/SSE/list ビューはクラッシュ済みまたは stale な run を復旧し、永遠に `running` のスナップショットを見せ続けません。`run_swarm` は polling 中に MCP progress heartbeat を送り、transport drop 後に再接続するクライアントでも handle を拾えるよう最初のフレームを `swarm_started run_id=<id>` に固定しました。worker も LLM streaming、grounding fetch、tool execution の各段階で heartbeat を出します。stale-run reaper は run ごとの閾値を使い、task 状態から終端状態を導出します。`SwarmTool` は待機予算が尽きても進行中の team をキャンセルせず、MCP クライアントは `reap_stale_runs()` で明示的に cleanup できます。今日の DX pass では provider の既定モデルも更新し、CI syntax check を新しい `agent/cli/` パッケージに合わせました。hydrate、終端復旧、stale reap、keepalive cadence、env parsing、heartbeat wiring を 22 件の新規回帰テストでカバーし、swarm/MCP 全体スイートは 169 passed、4 skipped です。
- **2026-05-21** 🧱 **CLI パッケージリファクタ**: `agent/cli.py`(3216 LOC)を `agent/cli/` パッケージへ分割 — インタラクティブな入口、slash ルーター、Rich コンポーネント、そしてすべてのサブコマンドを保ち `cli.cmd_*` / `cli._INIT_ENV_PATH` / `cli.Confirm` などの公開シンボルを再エクスポートする `_legacy.py` shim。新しい FastAPI ミドルウェアはブラウザが `/runs/{id}` または `/correlation` を直接開いた際に SPA シェルを返し、同じ絞り込みを Vite dev プロキシにも反映。バージョン文字列は `cli/_version.py` で一本化(`--version` とバナーのドリフト解消)、`python -m cli` を `__main__.py` で復活、chat ゲートを絞り `chat --help` / `chat extra` は REPL に飲み込まれずレガシー argparse に届きます。
- **2026-05-20** 🔬 **Hypothesis Registry CLI**: 2026-05-16 にバックエンドのみで公開された Hypothesis Registry の CLI 側を完成させました。`vibe-trading hypothesis list` は Rich テーブルまたは JSON を出力(`--status` フィルタと `--limit` をサポート)、`show <id>` はリンクされた run card を含む詳細パネルを描画、`invalidate <id> --note "..."` はステータスを `rejected` に切り替え、`--note` を省略すると既存の invalidation notes を保持します。既存の `VIBE_TRADING_HYPOTHESES_PATH` 環境変数オーバーライドに加え、呼び出し単位の `--path` も使えます。配線、JSON 出力、ステータスフィルタ、limit、ID 不在エラー、ノート永続化を 22 のテストでカバー。
- **2026-05-19** ✨ **ツールのライブフィードバック + グレースフルキャンセル**: 長時間実行されるツール(バックテスト、大きい PDF、swarm worker)が固まったように見えなくなりました。各ツール呼び出しは 3 秒ごとのハートビートに加え、構造化された段階進捗を発行します — `run_backtest` はフェーズマーカー(`validate` / `simulate` / `finalize`)、`read_document` は PDF ではページ単位、Excel ではシート単位、`read_url` は `fetch` / `parse` をマーク。CLI の Rich Live ダッシュボードは Unicode スピナー、ASCII プログレスバー、ETA を描画し、ツール名でキー付けして最大 3 つの並列ツールをスタック表示します。フロントエンドのチャットには新規 `ToolProgressIndicator` を追加し、rAF コアレッシング、ARIA `role="status"` + スクリーンリーダー向けの非表示 `<progress>`、合計が既知の場合は determinate な `ProgressRing` SVG を備えます。CLI 実行中の最初の `Ctrl+C` は `agent.cancel()` を呼んでグレースフル終了(現在のステップが完了し、trace がクリーンに閉じる)し、2 秒以内に 2 度目を押すと強制終了します。再利用可能なプリミティブ `ProgressBar.tsx` と `lib/tools.ts`(共有ツール名 i18n マッピング)も抽出。
- **2026-05-18** 🧹 **クリーンアップ + 3 つの潜在バグ修正**: `CompositeEngine` が取引所サフィックスのない中国先物コード(`RB2410` 等)を `GlobalFuturesEngine` に誤ルーティングしていた問題を修正。`_is_china_futures` を共有の `_market_hooks` モジュールに移し、製品コード表を大小文字正規化 + 非中国取引所のガードを追加、回帰ケース 9 件を新設しました。session FTS5 インデックスがタイムスタンプを永続化するようになり、クロスセッション検索を日付ソートできるようになりました。同じ修正で、re-upsert 経路が `started_at` を wall-clock で上書きしていた副次バグも解消しました。Vite 開発プロキシに `/alpha` を追加し、AlphaZoo ページが `npm run dev` で解決されるようになりました。`tests/test_e2e_harness_v2.py`(実 LLM の e2e スイート)は `VIBE_TRADING_RUN_LIVE_E2E=1` でゲート化し、CI が環境変数の有無で形を変えないようにしました。ruff に factor zoo 用の `per-file-ignores` を追加(F401 ノイズ 3783 → 0)、フロントエンド tsconfig は `noUnusedLocals` / `noUnusedParameters` を有効化して回帰ガードとし、`gtja191` alpha の未使用 `vw = vwap(...)` 雛形 76 件も削除しました。正味 **-918 行**。
- **2026-05-17** 🧬 **Alpha Zoo v1(0.1.8)**: 4 つの zoo にまたがる 452 個の事前構築 quant alpha を同梱しました — `qlib158`(Microsoft Qlib の Alpha158 特徴量、Apache-2.0 出処明示)、`alpha101`(Kakushadze の "101 Formulaic Alphas" を arXiv:1601.00991 から論文ベースで書き直し)、`gtja191`(国泰君安 2014 年の短期取引型 alpha レポート)、`academic`(Fama-French 5 + Carhart 動量の価格ベース proxy 実装)。任意の universe で 1 行 CLI: `vibe-trading alpha bench --zoo gtja191 --universe csi300 --period 2018-2025`。AST 純関数ゲート、look-ahead ガードテスト、`pytest-socket` ネットワーク遮断、各 zoo ごとの LICENSE.md、コミュニティ PR 用の DCO 署名フローも同梱。Alpha Library 自動レンダリングは [vibetrading.wiki/alpha-library/](https://vibetrading.wiki/alpha-library/)、Research Lab には [Which of the 191 GTJA alphas still work in 2026?](https://vibetrading.wiki/research-lab/posts/alpha-191-in-2026.html) を公開。
+6 -3
View File
@@ -46,12 +46,15 @@
## 📰 뉴스
- **2026-05-22** 🧭 **Swarm 복구 + MCP keepalive**: Swarm 상태는 이제 읽을 때마다 live task 파일에서 reconcile되므로 API/MCP/SSE/list 뷰가 크래시되었거나 오래된 run을 복구하고 영구 `running` 스냅샷을 보여주지 않습니다. `run_swarm`는 polling 중 MCP progress heartbeat를 보내며, transport drop 이후 재연결한 클라이언트가 handle을 회수할 수 있도록 첫 프레임을 `swarm_started run_id=<id>`로 고정했습니다. worker도 LLM streaming, grounding fetch, tool execution 전 과정에서 heartbeat를 냅니다. stale-run reaper는 run별 임계값을 사용하고 task 상태에서 최종 상태를 도출합니다. `SwarmTool`은 wait budget이 끝났다는 이유만으로 진행 중인 team을 취소하지 않으며, MCP 클라이언트는 `reap_stale_runs()`로 명시적 cleanup을 실행할 수 있습니다. 오늘의 DX pass에서는 provider 기본 모델도 갱신하고 CI syntax check를 새 `agent/cli/` 패키지에 맞췄습니다. 22개의 새 회귀 테스트가 hydrate, 최종 상태 복구, stale reap, keepalive cadence, env parsing, heartbeat wiring을 다루며, 전체 swarm/MCP 스위트는 169 passed, 4 skipped입니다.
- **2026-05-21** 🧱 **CLI 패키지 리팩토링**: `agent/cli.py`(3216 LOC)를 `agent/cli/` 패키지로 분할 — 대화형 진입점, 슬래시 라우터, Rich 컴포넌트, 그리고 모든 서브커맨드를 보존하고 `cli.cmd_*` / `cli._INIT_ENV_PATH` / `cli.Confirm` 등 공개 심볼을 재내보내는 `_legacy.py` shim. 새 FastAPI 미들웨어는 브라우저가 `/runs/{id}` 또는 `/correlation`에 직접 접근할 때 SPA 셸을 반환하며, 동일한 좁힘을 Vite dev 프록시에도 반영했습니다. 버전 문자열은 `cli/_version.py` 단일 소스로 통합(`--version`과 배너 드리프트 해결), `python -m cli`는 `__main__.py`로 복원, chat 게이트를 좁혀 `chat --help` / `chat extra`가 REPL에 삼켜지지 않고 레거시 argparse에 도달합니다.
- **2026-05-20** 🔬 **Hypothesis Registry CLI**: 2026-05-16에 백엔드만 출시된 Hypothesis Registry의 CLI 측을 완성했습니다. `vibe-trading hypothesis list`는 Rich 테이블 또는 JSON을 출력합니다(`--status` 필터, `--limit` 지원). `show <id>`는 링크된 run card를 포함한 상세 패널을 렌더링합니다. `invalidate <id> --note "..."`는 상태를 `rejected`로 전환하며, `--note`를 생략하면 기존 invalidation notes를 유지합니다. 기존 `VIBE_TRADING_HYPOTHESES_PATH` 환경변수 오버라이드와 호출별 `--path`를 모두 지원합니다. 22개의 새 테스트가 와이어링, JSON 출력, 상태 필터, limit, ID 누락 오류, 노트 영속성을 다룹니다.
- **2026-05-25** 🧼 **더 깔끔한 Chat UI + composer 워크플로**: Web UI는 이제 다음 입력에 집중하도록 정리되었습니다. upload, swarm, research-goal 모드는 composer의 `+` 메뉴 뒤로 모이고, floating panel로 채팅을 방해하지 않습니다. 현재 context는 입력창 위 compact chip으로 표시되며, goal 세부 정보는 chip을 클릭할 때만 inline으로 펼쳐집니다. 기존 custom i18n layer도 제거하고 직접 English copy로 통일했습니다. Full Report card는 report-worthy run에만 표시되며, 로컬 dev startup/status reporting도 브라우저 smoke test에 맞게 안정화했습니다.
- **2026-05-24** 🎯 **Research Goal runtime**: backend, CLI, API/MCP, SSE, Web UI 전반에 session-scoped Research Goal layer를 추가했습니다. Goal은 claim, acceptance criteria, evidence row, budget, completion policy를 영속화합니다. agent tool은 goal 생성과 evidence 추가를 지원하고, `/goal`은 CLI 진입점이 되었으며, REST/MCP는 goal snapshot과 evidence write를 노출하고, SSE는 chat client 상태를 최신으로 유지합니다. 후속 audit fixes에서는 verified evidence 경계를 잠그고, agent tool의 live-trading risk tier 입력을 차단하며, CLI-created goal을 이후 turn에 연결하고, session 삭제 시 goal ledger를 정리하고, replay-all을 연결하고, frontend cross-session snapshot race를 수정했습니다.
- **2026-05-23** 🖥️ **대화형 CLI 새 단장**: 터미널 진입점은 더 큰 Vibe-Trading 배너, 더 깔끔한 prompt 구분선, 이전 턴 요약, 실행 후 소요 시간, Claude Code 스타일 activity rail로 live agent 작업을 보여줍니다. 도구 호출, 웹/데이터 fetch, shell 스타일 동작, Markdown 답변, pipe table은 더 읽기 쉬운 transcript로 렌더링되며, pipe 또는 non-TTY 실행은 자동화에 적합한 plain-text 출력을 유지합니다. 생성된 CLI 스크린샷은 커밋되는 docs 파일이 아니라 local artifact로 처리되어 저장소를 가볍게 유지합니다.
<details>
<summary>이전 뉴스</summary>
- **2026-05-22** 🧭 **Swarm 복구 + MCP keepalive**: Swarm 상태는 이제 읽을 때마다 live task 파일에서 reconcile되므로 API/MCP/SSE/list 뷰가 크래시되었거나 오래된 run을 복구하고 영구 `running` 스냅샷을 보여주지 않습니다. `run_swarm`는 polling 중 MCP progress heartbeat를 보내며, transport drop 이후 재연결한 클라이언트가 handle을 회수할 수 있도록 첫 프레임을 `swarm_started run_id=<id>`로 고정했습니다. worker도 LLM streaming, grounding fetch, tool execution 전 과정에서 heartbeat를 냅니다. stale-run reaper는 run별 임계값을 사용하고 task 상태에서 최종 상태를 도출합니다. `SwarmTool`은 wait budget이 끝났다는 이유만으로 진행 중인 team을 취소하지 않으며, MCP 클라이언트는 `reap_stale_runs()`로 명시적 cleanup을 실행할 수 있습니다. 오늘의 DX pass에서는 provider 기본 모델도 갱신하고 CI syntax check를 새 `agent/cli/` 패키지에 맞췄습니다. 22개의 새 회귀 테스트가 hydrate, 최종 상태 복구, stale reap, keepalive cadence, env parsing, heartbeat wiring을 다루며, 전체 swarm/MCP 스위트는 169 passed, 4 skipped입니다.
- **2026-05-21** 🧱 **CLI 패키지 리팩토링**: `agent/cli.py`(3216 LOC)를 `agent/cli/` 패키지로 분할 — 대화형 진입점, 슬래시 라우터, Rich 컴포넌트, 그리고 모든 서브커맨드를 보존하고 `cli.cmd_*` / `cli._INIT_ENV_PATH` / `cli.Confirm` 등 공개 심볼을 재내보내는 `_legacy.py` shim. 새 FastAPI 미들웨어는 브라우저가 `/runs/{id}` 또는 `/correlation`에 직접 접근할 때 SPA 셸을 반환하며, 동일한 좁힘을 Vite dev 프록시에도 반영했습니다. 버전 문자열은 `cli/_version.py` 단일 소스로 통합(`--version`과 배너 드리프트 해결), `python -m cli`는 `__main__.py`로 복원, chat 게이트를 좁혀 `chat --help` / `chat extra`가 REPL에 삼켜지지 않고 레거시 argparse에 도달합니다.
- **2026-05-20** 🔬 **Hypothesis Registry CLI**: 2026-05-16에 백엔드만 출시된 Hypothesis Registry의 CLI 측을 완성했습니다. `vibe-trading hypothesis list`는 Rich 테이블 또는 JSON을 출력합니다(`--status` 필터, `--limit` 지원). `show <id>`는 링크된 run card를 포함한 상세 패널을 렌더링합니다. `invalidate <id> --note "..."`는 상태를 `rejected`로 전환하며, `--note`를 생략하면 기존 invalidation notes를 유지합니다. 기존 `VIBE_TRADING_HYPOTHESES_PATH` 환경변수 오버라이드와 호출별 `--path`를 모두 지원합니다. 22개의 새 테스트가 와이어링, JSON 출력, 상태 필터, limit, ID 누락 오류, 노트 영속성을 다룹니다.
- **2026-05-19** ✨ **도구 라이브 피드백 + 우아한 취소**: 오래 걸리는 도구(백테스트, 큰 PDF, swarm worker)가 멈춘 것처럼 보이지 않게 되었습니다. 모든 도구 호출은 이제 3초 간격의 하트비트와 구조화된 단계별 진행 상황을 발행합니다 — `run_backtest`는 단계 마커(`validate` / `simulate` / `finalize`), `read_document`는 PDF에서는 페이지 단위, Excel에서는 시트 단위, `read_url`은 `fetch` / `parse`를 표시합니다. CLI의 Rich Live 대시보드는 유니코드 스피너, ASCII 진행 표시줄, ETA를 렌더링하고 도구 이름으로 키된 최대 3개의 병렬 도구를 스택 표시합니다. 프런트엔드 채팅에는 새로운 `ToolProgressIndicator`를 추가했으며, rAF 코얼레싱, ARIA `role="status"` + 스크린 리더용 숨겨진 네이티브 `<progress>`, 총량을 알 때 결정적 `ProgressRing` SVG로 전환합니다. CLI 실행 중 첫 번째 `Ctrl+C`는 이제 `agent.cancel()`을 호출해 우아하게 종료(현재 단계가 끝나고 trace가 깨끗하게 닫힘)하고, 2초 이내 두 번째는 강제 종료합니다. 재사용 가능한 기본 요소도 추출했습니다: `ProgressBar.tsx`와 `lib/tools.ts`(공유 도구 이름 i18n).
- **2026-05-18** 🧹 **정리 + 3개의 잠재 버그 수정**: `CompositeEngine`이 거래소 접미사가 없는 중국 선물 코드(`RB2410` 등)를 `GlobalFuturesEngine`으로 잘못 라우팅하던 문제를 수정했습니다. `_is_china_futures`를 공유 `_market_hooks` 모듈로 옮기고, 상품 코드 테이블에 대소문자 정규화 + 비중국 거래소 가드를 추가했으며, 회귀 케이스 9개를 새로 작성했습니다. session FTS5 인덱스가 타임스탬프를 영구 저장하게 되어 크로스 세션 검색에서 날짜 정렬이 가능해졌으며, 동일 변경으로 re-upsert 경로가 `started_at`을 wall-clock으로 덮어쓰던 부수 버그도 해결했습니다. Vite 개발 프록시에 누락되었던 `/alpha`를 추가하여 AlphaZoo 페이지가 `npm run dev`에서 정상 해석됩니다. `tests/test_e2e_harness_v2.py`(실 LLM e2e 스위트)는 `VIBE_TRADING_RUN_LIVE_E2E=1`로 게이트하여 CI가 환경변수 유무에 따라 형태를 바꾸지 않도록 했습니다. ruff에 factor zoo용 `per-file-ignores`를 추가(F401 잡음 3783 → 0)하고, 프런트엔드 tsconfig에 `noUnusedLocals` / `noUnusedParameters`를 활성화해 회귀 가드로 두었으며, `gtja191` alpha 파일들의 사용되지 않는 `vw = vwap(...)` 보일러플레이트 76개도 삭제했습니다. 순 **-918줄**.
- **2026-05-17** 🧬 **Alpha Zoo v1 (0.1.8)**: 4개 zoo에 걸친 452개의 사전 빌드된 quant alpha를 번들로 제공 — `qlib158`(Microsoft Qlib의 Alpha158 특성, Apache-2.0 출처 표기), `alpha101`(Kakushadze의 "101 Formulaic Alphas"를 arXiv:1601.00991 논문 부록에서 재구현), `gtja191`(국태군안 2014 단기 거래형 alpha 리서치 보고서), `academic`(Fama-French 5 + Carhart 모멘텀의 가격 기반 proxy 구현). 한 줄 CLI로 임의 universe에서 벤치: `vibe-trading alpha bench --zoo gtja191 --universe csi300 --period 2018-2025`. AST 순수 함수 게이트, look-ahead 가드 테스트, `pytest-socket` 네트워크 차단, zoo별 LICENSE.md, 커뮤니티 PR용 DCO 서명 워크플로우 포함. Alpha Library 자동 렌더링: [vibetrading.wiki/alpha-library/](https://vibetrading.wiki/alpha-library/), Research Lab 글: [Which of the 191 GTJA alphas still work in 2026?](https://vibetrading.wiki/research-lab/posts/alpha-191-in-2026.html).
+6 -3
View File
@@ -46,12 +46,15 @@
## 📰 News
- **2026-05-22** 🧭 **Swarm 恢复 + MCP keepalive**:Swarm 状态现在每次读取都会从实时 task 文件 reconcile,API/MCP/SSE/list 视图可以自动恢复 crash 或过期 run,不再永久停在 `running` 快照。`run_swarm` 在 MCP polling 期间持续发送 progress heartbeat,首帧固定为 `swarm_started run_id=<id>`,方便 transport 掉线后的客户端找回句柄;worker 的 LLM streaming、grounding fetch、tool execution 也都包上了 heartbeat。stale-run reaper 按每个 run 的阈值判断,并从 task 状态推导终态;`SwarmTool` wait budget 用尽后不再取消仍在跑的 team,MCP 客户端也可以调用 `reap_stale_runs()` 显式清理。今天的 DX pass 还同步刷新 provider 默认模型,并把 CI syntax check 对齐到新的 `agent/cli/` 包。22 条新回归覆盖 hydrate、终态恢复、stale 回收、keepalive cadence、env 容错和 heartbeat wiring;完整 swarm/MCP 套件 169 passed、4 skipped。
- **2026-05-21** 🧱 **CLI 包重构**:`agent/cli.py`(3216 行)拆成 `agent/cli/` 包 —— 交互入口、slash 路由、Rich 组件,加 `_legacy.py` shim 保留所有子命令并 re-export 所有公共符号,`cli.cmd_*` / `cli._INIT_ENV_PATH` / `cli.Confirm` 不变。新增 FastAPI middleware:浏览器直开 `/runs/{id}` 或 `/correlation` 时返回 SPA shell;Vite dev proxy 同步收窄到相同 regex。版本号通过 `cli/_version.py` 单一来源(`--version` 与 banner 不再 drift),`python -m cli` 通过 `__main__.py` 恢复,chat-gate 收窄使 `chat --help` / `chat extra` 正确走 legacy argparse 而不被新 REPL 吞掉。
- **2026-05-20** 🔬 **Hypothesis Registry CLI**:补齐了 5-16 上线但只有后端的 Hypothesis Registry 的 CLI 侧。`vibe-trading hypothesis list` 输出 Rich 表格或 JSON(支持 `--status` 过滤、`--limit`);`show <id>` 渲染详情面板,包含已 link 的 run card;`invalidate <id> --note "..."` 把 status 翻成 `rejected`,省略 `--note` 时保留原有 invalidation notes。沿用 `VIBE_TRADING_HYPOTHESES_PATH` 环境变量,并新增按调用覆盖的 `--path`。22 个新单测覆盖 wiring、JSON 输出、状态过滤、limit、缺 id 报错、备注持久化。
- **2026-05-25** 🧼 **更干净的 Chat UI + composer 工作流**:Web UI 现在把注意力留给下一步输入:upload、swarm 和 research-goal 模式都收进 composer 的 `+` 菜单,不再用漂浮面板打断聊天。当前上下文会以紧凑 chip 附在输入框上方,goal 详情只在点击 chip 时原地展开。UI 也移除了旧的自定义 i18n 层,改用直接英文文案;Full Report card 只在真正有报告价值的 run 出现;本地 dev 启动与状态报告也加固,方便稳定做浏览器 smoke test。
- **2026-05-24** 🎯 **Research Goal runtime**:新增 session 级 Research Goal 层,贯通 backend、CLI、API/MCP、SSE 和 Web UI。Goal 会持久化 claim、acceptance criteria、evidence row、budget 与 completion policy;agent tools 可以创建 goal 并追加 evidence;`/goal` 成为 CLI 入口;REST/MCP 暴露 goal snapshot 和 evidence 写入;SSE 保持 chat client 状态新鲜。后续审计修复锁紧 verified evidence,阻断 agent tool 写入 live-trading 风险层,串起 CLI 创建的 goal 与后续 turn,删除 session 时清理 goal ledger,接上 replay-all,并修复前端跨 session snapshot race。
- **2026-05-23** 🖥️ **交互式 CLI 刷新**:终端入口现在使用更大的 Vibe-Trading banner、更清晰的 prompt 分隔线、上一轮摘要、运行后耗时,以及 Claude Code 风格的活动轨来展示实时 agent 工作。工具调用、网页/数据抓取、shell 风格动作、Markdown 回答和管道表格都会以更易读的 transcript 渲染;pipe 或非 TTY 运行仍保留适合自动化的纯文本输出。生成的 CLI 截图现在作为本地 artifact 处理,不再提交进 docs,让仓库更轻。
<details>
<summary>更早的更新</summary>
- **2026-05-22** 🧭 **Swarm 恢复 + MCP keepalive**:Swarm 状态现在每次读取都会从实时 task 文件 reconcile,API/MCP/SSE/list 视图可以自动恢复 crash 或过期 run,不再永久停在 `running` 快照。`run_swarm` 在 MCP polling 期间持续发送 progress heartbeat,首帧固定为 `swarm_started run_id=<id>`,方便 transport 掉线后的客户端找回句柄;worker 的 LLM streaming、grounding fetch、tool execution 也都包上了 heartbeat。stale-run reaper 按每个 run 的阈值判断,并从 task 状态推导终态;`SwarmTool` wait budget 用尽后不再取消仍在跑的 team,MCP 客户端也可以调用 `reap_stale_runs()` 显式清理。今天的 DX pass 还同步刷新 provider 默认模型,并把 CI syntax check 对齐到新的 `agent/cli/` 包。22 条新回归覆盖 hydrate、终态恢复、stale 回收、keepalive cadence、env 容错和 heartbeat wiring;完整 swarm/MCP 套件 169 passed、4 skipped。
- **2026-05-21** 🧱 **CLI 包重构**:`agent/cli.py`(3216 行)拆成 `agent/cli/` 包 —— 交互入口、slash 路由、Rich 组件,加 `_legacy.py` shim 保留所有子命令并 re-export 所有公共符号,`cli.cmd_*` / `cli._INIT_ENV_PATH` / `cli.Confirm` 不变。新增 FastAPI middleware:浏览器直开 `/runs/{id}` 或 `/correlation` 时返回 SPA shell;Vite dev proxy 同步收窄到相同 regex。版本号通过 `cli/_version.py` 单一来源(`--version` 与 banner 不再 drift),`python -m cli` 通过 `__main__.py` 恢复,chat-gate 收窄使 `chat --help` / `chat extra` 正确走 legacy argparse 而不被新 REPL 吞掉。
- **2026-05-20** 🔬 **Hypothesis Registry CLI**:补齐了 5-16 上线但只有后端的 Hypothesis Registry 的 CLI 侧。`vibe-trading hypothesis list` 输出 Rich 表格或 JSON(支持 `--status` 过滤、`--limit`);`show <id>` 渲染详情面板,包含已 link 的 run card;`invalidate <id> --note "..."` 把 status 翻成 `rejected`,省略 `--note` 时保留原有 invalidation notes。沿用 `VIBE_TRADING_HYPOTHESES_PATH` 环境变量,并新增按调用覆盖的 `--path`。22 个新单测覆盖 wiring、JSON 输出、状态过滤、limit、缺 id 报错、备注持久化。
- **2026-05-19** ✨ **工具实时反馈 + 优雅取消**:长时间运行的工具(回测、大 PDF、swarm worker)不再看起来卡死。每个工具调用现在会发出 3 秒一次的心跳,以及结构化的阶段进度 —— `run_backtest` 输出阶段标记(`validate` / `simulate` / `finalize`),`read_document` 在 PDF 上按页打点 / Excel 上按工作表打点,`read_url` 标记 `fetch` / `parse`。CLI 的 Rich Live 面板渲染 Unicode 转轮、ASCII 进度条、ETA,按工具名最多堆叠 3 个并行工具;前端 chat 新增 `ToolProgressIndicator`,rAF 合并刷新、ARIA `role="status"` + 隐藏的原生 `<progress>` 供屏幕阅读器使用,已知总数时切换为 determinate 的 `ProgressRing` SVG。CLI 中第一次 `Ctrl+C` 现在会调 `agent.cancel()` 优雅退出(当前步骤跑完、trace 干净关闭);2 秒内第二次 `Ctrl+C` 强制退出。顺手抽出可复用基础件:`ProgressBar.tsx` 和 `lib/tools.ts`(共享工具名 i18n 映射)。
- **2026-05-18** 🧹 **清理一次 + 3 个潜伏 bug 修复**:`CompositeEngine` 不再把无交易所后缀的中国期货代码(如 `RB2410`)错误路由到 `GlobalFuturesEngine` —— `_is_china_futures` 移到共享的 `_market_hooks` 模块,产品代码表做了大小写归一并加入非中国交易所守卫,新增 9 条回归用例。session FTS5 索引现在会持久化时间戳,跨 session 搜索可按日期排序;同一改动也修复了 re-upsert 路径每次都用 wall-clock 覆盖 `started_at` 的副作用 bug。前端 Vite dev proxy 补上漏配的 `/alpha`,AlphaZoo 页在 `npm run dev` 下不再 404。`tests/test_e2e_harness_v2.py`(真 LLM 的 e2e 套件)现在用 `VIBE_TRADING_RUN_LIVE_E2E=1` 做环境门控,CI 不再因为有无 LLM key 而静默切状态。Ruff 为 factor zoo 添加 `per-file-ignores`(3783 → 0 F401 噪音),前端 tsconfig 打开 `noUnusedLocals` / `noUnusedParameters` 做回归护栏,并删掉了 76 个 `gtja191` alpha 文件里没用上的 `vw = vwap(...)` 残留。净 **-918 行**。
- **2026-05-17** 🧬 **Alpha Zoo v1(0.1.8)**:内置 452 个量化 alpha,覆盖 4 个 zoo —— `qlib158`(Microsoft Qlib 的 Alpha158 特征,Apache-2.0 出处声明)、`alpha101`(Kakushadze 的 "101 Formulaic Alphas",从 arXiv:1601.00991 论文公式重写)、`gtja191`(国君证券 2014 短周期交易型因子研报)、`academic`(Fama-French 5 因子 + Carhart 动量的价格代理实现)。一行 CLI 就能在自己的 universe 上跑横评:`vibe-trading alpha bench --zoo gtja191 --universe csi300 --period 2018-2025`。配套设施包括 AST 纯函数门禁、lookahead 防护测试、`pytest-socket` 网络隔离、每个 zoo 一份 LICENSE.md、社区贡献用的 DCO 签名流程;Alpha Library 自动渲染上线 [vibetrading.wiki/alpha-library/](https://vibetrading.wiki/alpha-library/);Research Lab 同步发布 [《191 个 GTJA alpha 哪些在 2026 还能用》](https://vibetrading.wiki/research-lab/posts/alpha-191-in-2026.html)。
-292
View File
@@ -1,292 +0,0 @@
import { useEffect, useMemo, useState } from "react";
import {
AlertTriangle,
Check,
Circle,
Loader2,
Plus,
RefreshCcw,
Target,
X,
} from "lucide-react";
import type { GoalCriterion, GoalSnapshot } from "@/lib/api";
interface GoalDrawerProps {
open: boolean;
snapshot: GoalSnapshot | null;
loading: boolean;
error: string | null;
onClose: () => void;
onRefresh: () => void;
onStart: (objective: string) => Promise<void>;
onAddEvidence: (criterionId: string, text: string) => Promise<void>;
}
function isCriterionStatusMet(status: string): boolean {
return !["", "pending", "open", "unsatisfied"].includes(status.toLowerCase());
}
function criterionEvidenceCount(snapshot: GoalSnapshot | null, criterionId: string): number {
if (!snapshot) return 0;
return snapshot.evidence.filter((item) => item.criterion_id === criterionId).length;
}
function statusLabel(status: string): string {
return status.replace(/_/g, " ");
}
function criterionIndexLabel(criterion: GoalCriterion, index: number): string {
const step = criterion.protocol_step?.match(/step_(\d+)/)?.[1];
return step || String(index + 1);
}
export function getGoalProgress(snapshot: GoalSnapshot | null): { met: number; total: number; label: string } {
const total = snapshot?.criteria.length ?? 0;
const met = snapshot?.criteria.filter((item) => isCriterionStatusMet(item.status)).length ?? 0;
return {
met,
total,
label: total > 0 ? `${met}/${total}` : "",
};
}
export function GoalDrawer({
open,
snapshot,
loading,
error,
onClose,
onRefresh,
onStart,
onAddEvidence,
}: GoalDrawerProps) {
const [objective, setObjective] = useState("");
const [evidenceText, setEvidenceText] = useState("");
const [criterionId, setCriterionId] = useState("");
const [submitting, setSubmitting] = useState<"start" | "evidence" | null>(null);
const criteria = useMemo(() => snapshot?.criteria ?? [], [snapshot]);
const progress = useMemo(() => getGoalProgress(snapshot), [snapshot]);
useEffect(() => {
if (criteria.length === 0 && criterionId) {
setCriterionId("");
return;
}
if (!criterionId && criteria.length > 0) {
setCriterionId(criteria[0].criterion_id);
}
if (criterionId && criteria.length > 0 && !criteria.some((item) => item.criterion_id === criterionId)) {
setCriterionId(criteria[0].criterion_id);
}
}, [criterionId, criteria]);
if (!open) return null;
const handleStart = async () => {
const trimmed = objective.trim();
if (!trimmed) return;
setSubmitting("start");
try {
await onStart(trimmed);
setObjective("");
} finally {
setSubmitting(null);
}
};
const handleAddEvidence = async () => {
const trimmed = evidenceText.trim();
if (!criterionId || !trimmed) return;
setSubmitting("evidence");
try {
await onAddEvidence(criterionId, trimmed);
setEvidenceText("");
} finally {
setSubmitting(null);
}
};
return (
<aside
aria-label="Goal panel"
className="fixed right-6 top-20 z-40 flex max-h-[calc(100vh-7.5rem)] w-[min(390px,calc(100vw-2rem))] flex-col overflow-hidden rounded-xl border bg-background/95 shadow-2xl backdrop-blur-xl"
>
<div className="border-b px-4 py-3">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="flex items-center gap-2">
<Target className="h-4 w-4 text-primary" aria-hidden="true" />
<h2 className="text-sm font-semibold text-foreground">Research Goal</h2>
</div>
{snapshot ? (
<p className="mt-2 line-clamp-3 text-xs leading-relaxed text-muted-foreground">
{snapshot.goal.objective}
</p>
) : (
<p className="mt-2 text-xs leading-relaxed text-muted-foreground">
Attach a research goal to keep this session evidence-driven.
</p>
)}
</div>
<button
type="button"
onClick={onClose}
className="rounded-md p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
aria-label="Close goal panel"
>
<X className="h-4 w-4" aria-hidden="true" />
</button>
</div>
<div className="mt-3 flex items-center justify-between gap-3">
{snapshot ? (
<span className="inline-flex h-6 items-center gap-1.5 rounded-full border border-emerald-500/30 bg-emerald-500/10 px-2.5 text-[11px] font-medium capitalize text-emerald-600 dark:text-emerald-400">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500" aria-hidden="true" />
{statusLabel(snapshot.goal.status)}
</span>
) : (
<span className="inline-flex h-6 items-center gap-1.5 rounded-full border bg-muted/50 px-2.5 text-[11px] font-medium text-muted-foreground">
<Circle className="h-2.5 w-2.5" aria-hidden="true" />
No goal
</span>
)}
<button
type="button"
onClick={onRefresh}
className="inline-flex h-7 items-center gap-1.5 rounded-md border bg-background px-2 text-[11px] text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:opacity-50"
disabled={loading}
>
{loading ? (
<Loader2 className="h-3 w-3 animate-spin" aria-hidden="true" />
) : (
<RefreshCcw className="h-3 w-3" aria-hidden="true" />
)}
Refresh
</button>
</div>
</div>
<div className="grid gap-4 overflow-auto p-4">
{error && (
<div className="flex gap-2 rounded-lg border border-destructive/30 bg-destructive/10 p-3 text-xs leading-relaxed text-destructive">
<AlertTriangle className="mt-0.5 h-3.5 w-3.5 shrink-0" aria-hidden="true" />
<span>{error}</span>
</div>
)}
{snapshot && (
<section className="grid gap-2">
<div className="flex items-center justify-between text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
<span>Acceptance Criteria</span>
<span className="font-mono tracking-normal text-foreground">{progress.label} met</span>
</div>
<div className="grid gap-2">
{criteria.map((criterion, index) => {
const count = criterionEvidenceCount(snapshot, criterion.criterion_id);
const met = isCriterionStatusMet(criterion.status);
return (
<div
key={criterion.criterion_id}
className="grid grid-cols-[1.25rem_minmax(0,1fr)_auto] items-start gap-2 rounded-lg border bg-muted/20 p-2.5"
>
<span
className={[
"mt-0.5 inline-flex h-5 w-5 items-center justify-center rounded-full text-[10px]",
met ? "bg-emerald-500/15 text-emerald-600 dark:text-emerald-400" : "bg-muted text-muted-foreground",
].join(" ")}
>
{met ? <Check className="h-3 w-3" aria-hidden="true" /> : criterionIndexLabel(criterion, index)}
</span>
<div className="min-w-0">
<div className="text-xs font-medium leading-snug text-foreground">
{criterion.text}
</div>
<div className="mt-1 text-[11px] text-muted-foreground">
{statusLabel(criterion.status)}
</div>
</div>
<span className="rounded-full border px-2 py-0.5 text-[10px] text-muted-foreground">
{count} ev
</span>
</div>
);
})}
</div>
</section>
)}
{snapshot && criteria.length > 0 && (
<section className="grid gap-2">
<div className="flex items-center justify-between text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
<span>Add Evidence</span>
<span className="font-mono tracking-normal text-foreground">{snapshot.evidence_count} total</span>
</div>
<div className="grid gap-2 rounded-lg border border-primary/25 bg-primary/5 p-2.5">
<select
value={criterionId}
onChange={(event) => setCriterionId(event.target.value)}
className="h-8 rounded-md border bg-background px-2 text-xs outline-none focus:ring-2 focus:ring-primary/25"
aria-label="Evidence criterion"
>
{criteria.map((criterion, index) => (
<option key={criterion.criterion_id} value={criterion.criterion_id}>
{criterionIndexLabel(criterion, index)}. {criterion.text}
</option>
))}
</select>
<textarea
value={evidenceText}
onChange={(event) => setEvidenceText(event.target.value)}
aria-label="Evidence note"
placeholder="Paste a concise evidence note or artifact summary."
className="min-h-20 resize-y rounded-md border bg-background px-3 py-2 text-xs leading-relaxed outline-none placeholder:text-muted-foreground focus:ring-2 focus:ring-primary/25"
/>
<button
type="button"
onClick={handleAddEvidence}
disabled={!evidenceText.trim() || !criterionId || submitting === "evidence"}
className="inline-flex items-center gap-1.5 justify-self-end rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground transition-opacity hover:opacity-90 disabled:opacity-40"
>
{submitting === "evidence" ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" aria-hidden="true" />
) : (
<Plus className="h-3.5 w-3.5" aria-hidden="true" />
)}
Attach evidence
</button>
</div>
</section>
)}
<section className="grid gap-2">
<div className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
Start New Goal
</div>
<div className="grid gap-2">
<textarea
value={objective}
onChange={(event) => setObjective(event.target.value)}
aria-label="Research goal"
placeholder="Research whether SOL relative strength is beta or idiosyncratic flow."
className="min-h-20 resize-y rounded-md border bg-background px-3 py-2 text-xs leading-relaxed outline-none placeholder:text-muted-foreground focus:ring-2 focus:ring-primary/25"
/>
<button
type="button"
onClick={handleStart}
disabled={!objective.trim() || submitting === "start"}
className="inline-flex items-center gap-1.5 justify-self-end rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground transition-opacity hover:opacity-90 disabled:opacity-40"
>
{submitting === "start" ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" aria-hidden="true" />
) : (
<Target className="h-3.5 w-3.5" aria-hidden="true" />
)}
Start goal
</button>
</div>
</section>
</div>
</aside>
);
}
+196 -89
View File
@@ -1,6 +1,6 @@
import { useEffect, useRef, useState, useMemo, useCallback, type FormEvent } from "react";
import { useSearchParams } from "react-router-dom";
import { Send, Loader2, ArrowDown, Square, Download, Plus, Paperclip, X, Users, Target } from "lucide-react";
import { Send, Loader2, ArrowDown, Square, Download, Plus, Paperclip, X, Users, Target, ChevronDown } from "lucide-react";
import { toast } from "sonner";
import { useAgentStore } from "@/stores/agent";
import { useSSE } from "@/hooks/useSSE";
@@ -13,7 +13,6 @@ import { MessageBubble } from "@/components/chat/MessageBubble";
import { ThinkingTimeline } from "@/components/chat/ThinkingTimeline";
import { ConversationTimeline } from "@/components/chat/ConversationTimeline";
import { ToolProgressIndicator } from "@/components/chat/ToolProgressIndicator";
import { GoalDrawer, getGoalProgress } from "@/components/chat/GoalDrawer";
/* ---------- Message grouping ---------- */
type MsgGroup =
@@ -38,6 +37,47 @@ function groupMessages(msgs: AgentMessage[]): MsgGroup[] {
const act = () => useAgentStore.getState();
function isCriterionStatusMet(status: string): boolean {
return !["", "pending", "open", "unsatisfied"].includes(status.toLowerCase());
}
function getGoalProgress(snapshot: GoalSnapshot | null): {
met: number;
total: number;
label: string;
metLabel: string;
evidenceTotal: number;
} {
const total = snapshot?.criteria.length ?? 0;
const met = snapshot?.criteria.filter((item) => isCriterionStatusMet(item.status)).length ?? 0;
const evidenceTotal = snapshot?.evidence_count ?? 0;
return {
met,
total,
label: total > 0 ? `${met}/${total}` : "",
metLabel: total > 0 ? `${met}/${total} met` : "",
evidenceTotal,
};
}
function statusLabel(status: string): string {
return status.replace(/_/g, " ");
}
function criterionIndexLabel(index: number): string {
return String(index + 1);
}
function criterionEvidenceCount(snapshot: GoalSnapshot, criterionId: string): number {
return snapshot.evidence.filter((item) => item.criterion_id === criterionId).length;
}
function latestGoalEvidence(snapshot: GoalSnapshot) {
return [...snapshot.evidence]
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
.slice(0, 2);
}
/* ---------- Component ---------- */
export function Agent() {
const [input, setInput] = useState("");
@@ -61,10 +101,9 @@ export function Agent() {
const uploadMenuRef = useRef<HTMLDivElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const [swarmPreset, setSwarmPreset] = useState<{ name: string; title: string } | null>(null);
const [goalOpen, setGoalOpen] = useState(false);
const [goalComposerActive, setGoalComposerActive] = useState(false);
const [goalDetailsOpen, setGoalDetailsOpen] = useState(false);
const [goalSnapshot, setGoalSnapshot] = useState<GoalSnapshot | null>(null);
const [goalLoading, setGoalLoading] = useState(false);
const [goalError, setGoalError] = useState<string | null>(null);
const messages = useAgentStore(s => s.messages);
const streamingText = useAgentStore(s => s.streamingText);
@@ -132,11 +171,9 @@ export function Agent() {
const targetSession = sid || act().sessionId;
if (!targetSession) {
setGoalSnapshot(null);
setGoalError(null);
setGoalDetailsOpen(false);
return;
}
setGoalLoading(true);
setGoalError(null);
try {
const snapshot = await api.getGoal(targetSession);
if (act().sessionId !== targetSession) return;
@@ -145,11 +182,10 @@ export function Agent() {
if (act().sessionId !== targetSession) return;
if (error instanceof ApiError && error.status === 404) {
setGoalSnapshot(null);
setGoalDetailsOpen(false);
} else {
setGoalError(error instanceof Error ? error.message : "Failed to load goal.");
toast.error(error instanceof Error ? error.message : "Failed to load goal.");
}
} finally {
if (act().sessionId === targetSession) setGoalLoading(false);
}
}, []);
@@ -357,10 +393,10 @@ export function Agent() {
}, [connect, disconnect, loadGoalSnapshot, scrollToBottom]);
useEffect(() => {
const gen = ++genRef.current;
const { sessionId: curSid, messages: curMsgs, cacheSession, reset, getCachedSession, switchSession } = act();
if (urlSessionId && urlSessionId !== curSid) {
const gen = ++genRef.current;
doDisconnect();
if (curSid && curMsgs.length > 0) cacheSession(curSid, curMsgs);
@@ -374,6 +410,7 @@ export function Agent() {
}
setupSSE(urlSessionId);
} else if (!urlSessionId && curSid) {
++genRef.current;
doDisconnect();
if (curMsgs.length > 0) cacheSession(curSid, curMsgs);
reset();
@@ -383,7 +420,7 @@ export function Agent() {
useEffect(() => {
if (!sessionId) {
setGoalSnapshot(null);
setGoalError(null);
setGoalDetailsOpen(false);
return;
}
if (pendingGoalSessionRef.current === sessionId) {
@@ -410,6 +447,22 @@ export function Agent() {
const runPrompt = async (prompt: string) => {
if (!prompt.trim() || status === "streaming") return;
if (goalComposerActive) {
setInput("");
inputRef.current?.focus();
try {
const sid = await ensureGoalSession(prompt);
const snapshot = await api.createGoal(sid, { objective: prompt });
setGoalSnapshot(snapshot);
setGoalComposerActive(false);
setGoalDetailsOpen(true);
toast.success("Research goal attached");
} catch (error) {
toast.error(error instanceof Error ? error.message : "Failed to start goal.");
}
return;
}
let finalPrompt = prompt;
// Swarm mode: let agent auto-select the right preset
@@ -457,48 +510,6 @@ export function Agent() {
return sid;
}, [setSearchParams, setupSSE]);
const handleStartGoal = useCallback(async (objective: string) => {
try {
const sid = await ensureGoalSession(objective);
const snapshot = await api.createGoal(sid, { objective });
setGoalSnapshot(snapshot);
setGoalError(null);
toast.success("Goal started");
} catch (error) {
const message = error instanceof Error ? error.message : "Failed to start goal.";
setGoalError(message);
toast.error(message);
}
}, [ensureGoalSession]);
const handleAddGoalEvidence = useCallback(async (criterionId: string, text: string) => {
const sid = act().sessionId;
const current = goalSnapshot;
if (!sid || !current) {
const message = "Start a goal before attaching evidence.";
setGoalError(message);
toast.error(message);
return;
}
try {
const result = await api.addGoalEvidence(sid, {
goal_id: current.goal.goal_id,
expected_goal_id: current.goal.goal_id,
criterion_id: criterionId,
text,
source_provider: "webui",
source_type: "manual_note",
});
setGoalSnapshot(result.snapshot);
setGoalError(null);
toast.success("Evidence attached");
} catch (error) {
const message = error instanceof Error ? error.message : "Failed to attach evidence.";
setGoalError(message);
toast.error(message);
}
}, [goalSnapshot]);
const handleSubmit = (e: FormEvent) => { e.preventDefault(); runPrompt(input.trim()); };
const handleCancel = async () => {
@@ -609,38 +620,6 @@ export function Agent() {
return (
<div className="flex flex-col flex-1 min-w-0 overflow-hidden h-full">
<div ref={listRef} className="flex-1 overflow-auto p-6 scroll-smooth relative">
<div className="fixed right-6 top-4 z-40 flex items-center gap-2">
<button
type="button"
title="Research goal"
aria-label="Research goal"
aria-expanded={goalOpen}
onClick={() => {
const nextOpen = !goalOpen;
setGoalOpen(nextOpen);
if (nextOpen) loadGoalSnapshot(sessionId);
}}
className="inline-flex h-8 items-center gap-1.5 rounded-full border bg-background px-3 text-xs font-medium text-foreground shadow-sm transition-colors hover:bg-muted"
>
<Target className="h-3.5 w-3.5 text-primary" aria-hidden="true" />
<span>Goal</span>
{goalProgress.label && (
<span className="font-mono text-[11px] text-emerald-600 dark:text-emerald-400">{goalProgress.label}</span>
)}
</button>
</div>
<GoalDrawer
open={goalOpen}
snapshot={goalSnapshot}
loading={goalLoading}
error={goalError}
onClose={() => setGoalOpen(false)}
onRefresh={() => loadGoalSnapshot(sessionId)}
onStart={handleStartGoal}
onAddEvidence={handleAddGoalEvidence}
/>
<div className="max-w-3xl mx-auto space-y-4">
{sessionLoading && (
<div className="space-y-4 py-4">
@@ -732,6 +711,116 @@ export function Agent() {
</span>
</div>
)}
{goalComposerActive && (
<div className="flex items-center gap-1">
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-primary/10 text-primary text-xs font-medium">
<Target className="h-3 w-3" />
New Research Goal
<button type="button" onClick={() => setGoalComposerActive(false)} className="hover:text-destructive transition-colors">
<X className="h-3 w-3" />
</button>
</span>
</div>
)}
{goalSnapshot && !goalComposerActive && (
<div className="grid gap-2">
<button
type="button"
onClick={() => setGoalDetailsOpen((open) => !open)}
className="inline-flex max-w-full items-center gap-1.5 justify-self-start rounded-lg bg-primary/10 px-2.5 py-1 text-left text-xs font-medium text-primary transition-colors hover:bg-primary/15"
title={goalSnapshot.goal.objective}
aria-label="Active research goal"
aria-expanded={goalDetailsOpen}
>
<Target className="h-3 w-3 shrink-0" />
<span className="shrink-0">Goal</span>
<span className="truncate text-muted-foreground">
{goalSnapshot.goal.ui_summary || goalSnapshot.goal.objective}
</span>
{goalProgress.metLabel && (
<span className="shrink-0 font-mono text-[11px] text-emerald-600 dark:text-emerald-400">
{goalProgress.metLabel}
</span>
)}
{goalProgress.evidenceTotal > 0 && (
<span className="shrink-0 rounded bg-background px-1 font-mono text-[10px] text-primary">
{goalProgress.evidenceTotal} ev
</span>
)}
<ChevronDown
className={[
"h-3 w-3 shrink-0 transition-transform",
goalDetailsOpen ? "rotate-180" : "",
].join(" ")}
aria-hidden="true"
/>
</button>
{goalDetailsOpen && (
<div className="grid gap-3 rounded-xl border border-primary/20 bg-background/95 p-3 text-xs shadow-sm">
<div className="grid grid-cols-2 gap-2">
<div className="rounded-lg border bg-muted/20 p-2.5">
<div className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
Criteria
</div>
<div className="mt-1 font-mono text-base font-semibold text-foreground">
{goalProgress.label || "0/0"}
</div>
</div>
<div className="rounded-lg border bg-muted/20 p-2.5">
<div className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
Evidence
</div>
<div className="mt-1 font-mono text-base font-semibold text-foreground">
{goalProgress.evidenceTotal}
</div>
</div>
</div>
<div className="grid gap-1.5">
{goalSnapshot.criteria.map((criterion, index) => {
const evidenceCount = criterionEvidenceCount(goalSnapshot, criterion.criterion_id);
return (
<div
key={criterion.criterion_id}
className="grid grid-cols-[1.25rem_minmax(0,1fr)_auto] items-start gap-2 rounded-lg border bg-muted/20 p-2"
>
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-muted text-[10px] text-muted-foreground">
{criterionIndexLabel(index)}
</span>
<span className="min-w-0">
<span className="block truncate font-medium text-foreground">{criterion.text}</span>
<span className="block text-[11px] text-muted-foreground">
{statusLabel(criterion.status)}
</span>
</span>
<span className="rounded-full border px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground">
{evidenceCount} ev
</span>
</div>
);
})}
</div>
{goalSnapshot.evidence.length > 0 && (
<div className="grid gap-1.5 border-t pt-2">
<div className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
Recent Evidence
</div>
{latestGoalEvidence(goalSnapshot).map((item) => (
<div key={item.evidence_id} className="rounded-lg bg-muted/20 px-2 py-1.5">
<div className="mb-0.5 flex items-center justify-between gap-2 text-[10px] text-muted-foreground">
<span className="truncate">{item.source_provider || "evidence"}</span>
<span>{statusLabel(item.verification_status)}</span>
</div>
<div className="line-clamp-2 text-[11px] leading-relaxed text-foreground">
{item.text}
</div>
</div>
))}
</div>
)}
</div>
)}
</div>
)}
{/* Attachment badge */}
{attachment && (
<div className="flex items-center gap-1">
@@ -778,6 +867,20 @@ export function Agent() {
type="button"
onClick={() => {
setShowUploadMenu(false);
setSwarmPreset(null);
setGoalComposerActive(true);
inputRef.current?.focus();
}}
className="w-full px-3 py-2 text-left text-sm hover:bg-muted transition-colors flex items-center gap-2"
>
<Target className="h-4 w-4" />
Research Goal
</button>
<button
type="button"
onClick={() => {
setShowUploadMenu(false);
setGoalComposerActive(false);
setSwarmPreset({ name: "auto", title: "Agent Swarm" });
inputRef.current?.focus();
}}
@@ -812,7 +915,11 @@ export function Agent() {
runPrompt(input.trim());
}
}}
placeholder="e.g. Create a dual MA crossover strategy for 000001.SZ, backtest 2024"
placeholder={
goalComposerActive
? "Describe the research goal to attach to this session"
: "e.g. Create a dual MA crossover strategy for 000001.SZ, backtest 2024"
}
className="flex-1 px-4 py-2.5 rounded-xl border bg-background text-sm focus:outline-none focus:ring-2 focus:ring-primary/40 transition-shadow resize-none max-h-32 overflow-y-auto"
disabled={status === "streaming"}
/>
@@ -838,7 +945,7 @@ export function Agent() {
) : (
<button
type="submit"
disabled={!input.trim() && !attachment}
disabled={goalComposerActive ? !input.trim() : (!input.trim() && !attachment)}
className="px-4 py-2.5 rounded-xl bg-primary text-primary-foreground text-sm font-medium disabled:opacity-40 hover:opacity-90 transition-opacity"
>
<Send className="h-4 w-4" />
+59 -23
View File
@@ -66,8 +66,26 @@ start_service() {
fi
printf "starting %s...\n" "$service"
nohup "$@" >"$(log_file "$service")" 2>&1 </dev/null &
echo "$!" >"$(pid_file "$service")"
"$PYTHON_BIN" - "$(log_file "$service")" "$(pid_file "$service")" "$@" <<'PY'
import subprocess
import sys
log_path = sys.argv[1]
pid_path = sys.argv[2]
cmd = sys.argv[3:]
log = open(log_path, "ab", buffering=0)
proc = subprocess.Popen(
cmd,
stdin=subprocess.DEVNULL,
stdout=log,
stderr=subprocess.STDOUT,
start_new_session=True,
close_fds=True,
)
with open(pid_path, "w", encoding="utf-8") as fh:
fh.write(str(proc.pid))
PY
printf "%s pid %s, log %s\n" "$service" "$(cat "$(pid_file "$service")")" "$(log_file "$service")"
}
@@ -84,11 +102,9 @@ stop_service() {
pid="$(cat "$file")"
if kill -0 "$pid" 2>/dev/null; then
printf "stopping %s (pid %s)...\n" "$service" "$pid"
pkill -TERM -P "$pid" 2>/dev/null || true
kill "$pid" 2>/dev/null || true
kill -TERM "-$pid" 2>/dev/null || kill "$pid" 2>/dev/null || true
sleep 0.5
pkill -KILL -P "$pid" 2>/dev/null || true
kill -KILL "$pid" 2>/dev/null || true
kill -KILL "-$pid" 2>/dev/null || kill -KILL "$pid" 2>/dev/null || true
fi
rm -f "$file"
}
@@ -129,20 +145,47 @@ wait_for_url() {
return 1
}
cmd_up() {
service_url() {
case "$1" in
backend) printf "http://127.0.0.1:%s/health" "$BACKEND_PORT" ;;
frontend) printf "http://127.0.0.1:%s" "$FRONTEND_PORT" ;;
*) return 2 ;;
esac
}
start_backend() {
if ! is_running backend && url_ok "$(service_url backend)"; then
rm -f "$(pid_file backend)"
printf "backend already reachable at %s (external)\n" "$(service_url backend)"
return
fi
start_service backend env PYTHONPATH="$ROOT/agent" "$PYTHON_BIN" -c 'import cli, sys; raise SystemExit(cli.main(sys.argv[1:]))' serve \
--host "$BACKEND_HOST" \
--port "$BACKEND_PORT"
}
start_frontend() {
if ! is_running frontend && url_ok "$(service_url frontend)"; then
rm -f "$(pid_file frontend)"
printf "frontend already reachable at %s (external)\n" "$(service_url frontend)"
return
fi
start_service frontend bash -lc '
set -euo pipefail
cd "$1"
if ! [[ -d node_modules ]]; then
npm install
fi
export VITE_API_URL="${VITE_API_URL:-http://127.0.0.1:$4}"
exec npm run dev -- --host "$2" --port "$3"
' bash "$ROOT/frontend" "$FRONTEND_HOST" "$FRONTEND_PORT"
if ! wait_for_url backend "http://127.0.0.1:$BACKEND_PORT/health" 30 || \
! wait_for_url frontend "http://127.0.0.1:$FRONTEND_PORT" 30; then
' bash "$ROOT/frontend" "$FRONTEND_HOST" "$FRONTEND_PORT" "$BACKEND_PORT"
}
cmd_up() {
start_backend
start_frontend
if ! wait_for_url backend "$(service_url backend)" 30 || \
! wait_for_url frontend "$(service_url frontend)" 30; then
cmd_stop
exit 1
fi
@@ -159,22 +202,13 @@ cmd_restart() {
case "$service" in
backend)
stop_service backend
start_service backend env PYTHONPATH="$ROOT/agent" "$PYTHON_BIN" -c 'import cli, sys; raise SystemExit(cli.main(sys.argv[1:]))' serve \
--host "$BACKEND_HOST" \
--port "$BACKEND_PORT"
wait_for_url backend "http://127.0.0.1:$BACKEND_PORT/health" 30
start_backend
wait_for_url backend "$(service_url backend)" 30
;;
frontend)
stop_service frontend
start_service frontend bash -lc '
set -euo pipefail
cd "$1"
if ! [[ -d node_modules ]]; then
npm install
fi
exec npm run dev -- --host "$2" --port "$3"
' bash "$ROOT/frontend" "$FRONTEND_HOST" "$FRONTEND_PORT"
wait_for_url frontend "http://127.0.0.1:$FRONTEND_PORT" 30
start_frontend
wait_for_url frontend "$(service_url frontend)" 30
;;
all)
cmd_stop
@@ -191,6 +225,8 @@ cmd_status() {
for service in backend frontend; do
if is_running "$service"; then
printf "%-8s running pid=%s log=%s\n" "$service" "$(cat "$(pid_file "$service")")" "$(log_file "$service")"
elif url_ok "$(service_url "$service")"; then
printf "%-8s reachable external url=%s\n" "$service" "$(service_url "$service")"
else
printf "%-8s stopped\n" "$service"
fi