Skip to content

Commit f7d1066

Browse files
committed
feat(cli): refresh interactive terminal UI
Refresh the interactive terminal transcript, activity rail, and prompt visuals. Document the 2026-05-23 CLI update in README and keep generated CLI screenshots out of git.
1 parent a74bca2 commit f7d1066

15 files changed

Lines changed: 1139 additions & 169 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ data/backup/
4343
docs/
4444
!wiki/docs/
4545
!wiki/docs/**
46+
docs/screenshots/agent-run.png
47+
docs/screenshots/boot.png
4648
data/minutes/
4749
data/overnight*.log
4850
data/*.log

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ This project adheres to [Semantic Versioning](https://semver.org/).
55

66
## [Unreleased]
77

8+
### UI
9+
- Redesigned the interactive CLI startup and run-state visuals with a large
10+
figlet-style banner plus a Claude Code-style activity rail using ``, ``,
11+
and `·` glyphs. Non-TTY prompt runs now fall back to plain text output.
12+
813
### Added
914

1015
### Changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,13 @@
4646

4747
## 📰 News
4848

49+
- **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.
4950
- **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.
5051
- **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.
51-
- **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.
5252
<details>
5353
<summary>Earlier news</summary>
5454

55+
- **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.
5556
- **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).
5657
- **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**.
5758
- **2026-05-17** 🧬 **Alpha Zoo v1 (0.1.8)**: 452 pre-built quant alphas across 4 zoos — `qlib158` (Microsoft Qlib, Apache-2 attribution), `alpha101` (Kakushadze 101 Formulaic Alphas, paper rewrite from arXiv:1601.00991), `gtja191` (Guotai Junan 2014 short-horizon factor report), and `academic` (Fama-French 5 + Carhart price-based proxies). One-line CLI to bench any zoo on your universe: `vibe-trading alpha bench --zoo gtja191 --universe csi300 --period 2018-2025`. Ships with AST purity gate, lookahead-guard test, `pytest-socket` network kill-switch, per-zoo LICENSE.md, and a Developer Certificate of Origin (DCO) workflow for community PRs. Auto-rendered Alpha Library at [vibetrading.wiki/alpha-library/](https://vibetrading.wiki/alpha-library/) + research-lab post [Which of the 191 GTJA alphas still work in 2026?](https://vibetrading.wiki/research-lab/posts/alpha-191-in-2026.html).
@@ -463,6 +464,8 @@ The default `agent/.env.example` ships with DeepSeek official API + `deepseek-v4
463464

464465
## 🖥 CLI Reference
465466

467+
The interactive TUI (`vibe-trading`) now uses a terminal-native transcript: a startup banner, prompt rule, previous-turn recap, live activity rail, Markdown/table rendering, and run timing all stay in the CLI. Non-interactive invocations such as `vibe-trading run`, pipes, and `--json` remain script-friendly.
468+
466469
```bash
467470
vibe-trading # interactive TUI
468471
vibe-trading run -p "..." # single run

agent/cli/_legacy.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -784,6 +784,9 @@ def render(self) -> Panel:
784784
return Panel(body, title="Vibe-Trading", border_style="cyan", padding=(1, 1 if compact else 2))
785785

786786

787+
from cli.ui.rail import RailRunDashboard as _RunDashboard # noqa: E402,F811
788+
789+
787790
# ---------------------------------------------------------------------------
788791
# Agent execution core
789792
# ---------------------------------------------------------------------------
@@ -1241,6 +1244,7 @@ def cmd_run(prompt: str, max_iter: int, *, json_mode: bool = False, no_rich: boo
12411244
with Live(dashboard.render(), console=console, refresh_per_second=6, transient=True) as live:
12421245
dashboard.live = live
12431246
result = _run_agent(prompt, max_iter=max_iter, dashboard=dashboard)
1247+
dashboard.finish(result, time.perf_counter() - start)
12441248
except KeyboardInterrupt:
12451249
if json_mode:
12461250
_print_json_result({"status": "cancelled", "run_id": None, "run_dir": None, "reason": "Interrupted"})
@@ -1334,6 +1338,7 @@ def cmd_continue(
13341338
max_iter=max_iter,
13351339
dashboard=dashboard,
13361340
)
1341+
dashboard.finish(result, time.perf_counter() - start)
13371342
except KeyboardInterrupt:
13381343
console.print("\n[yellow]Interrupted[/yellow]")
13391344
return EXIT_RUN_FAILED
@@ -1727,6 +1732,7 @@ def cmd_interactive(max_iter: int) -> None:
17271732
with Live(dashboard.render(), console=console, refresh_per_second=6, transient=True) as live:
17281733
dashboard.live = live
17291734
result = _run_agent(user_input, history=history[-6:], max_iter=max_iter, dashboard=dashboard)
1735+
dashboard.finish(result, time.perf_counter() - start)
17301736
except KeyboardInterrupt:
17311737
console.print("\n[yellow]Interrupted[/yellow]")
17321738
continue
@@ -3118,6 +3124,10 @@ def main(argv: list[str] | None = None) -> int:
31183124
args = parser.parse_args(raw_argv)
31193125
except SystemExit as exc:
31203126
return int(exc.code) if isinstance(exc.code, int) else EXIT_USAGE_ERROR
3127+
if not sys.stdout.isatty():
3128+
args.no_rich = True
3129+
if hasattr(args, "run_no_rich"):
3130+
args.run_no_rich = True
31213131

31223132
if args.command == "init":
31233133
return cmd_init()

agent/cli/components/tool_event.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
33
Layout (matches design_proposal §3.5):
44
5-
Get Financials ("AAPL", quarterly, last 8 quarters) 1.4s · 8 quarters
5+
Get Financials ("AAPL", quarterly, last 8 quarters) 1.4s · 8 quarters
66
7-
* ```` (U+23FA media-record) marker, color-coded by status:
7+
* ```` (U+25CF black circle) marker, color-coded by status:
88
running → ``--warning`` amber (pulse via Rich style ``blink`` when
99
the terminal advertises it, otherwise solid amber)
1010
ok → ``--success`` green, solid
@@ -174,7 +174,7 @@ def render_tool_event(
174174
# raising; CLI rendering should never crash the agent loop.
175175
status = "running"
176176

177-
marker = Text(" ", style=_STATUS_STYLE[status])
177+
marker = Text(" ", style=_STATUS_STYLE[status])
178178
line = Text()
179179
line.append(marker)
180180
line.append(_pretty_tool_name(name), style="bold")

agent/cli/input.py

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
a plain Enter on a balanced buffer submits.
88
* Ctrl+C with three-state semantics (clear buffer → exit hint → exit)
99
* A surrogate-safe :class:`FileHistory` subclass for Windows users
10-
* UTF-8 stdout reconfigure on Windows so the brand glyph ```` and the
10+
* UTF-8 stdout reconfigure on Windows so the brand glyph ```` and the
1111
Rich box-drawing characters print without ``UnicodeEncodeError``
1212
1313
Cancel-during-generation lives outside the input loop — that is owned by
@@ -18,6 +18,7 @@
1818
from __future__ import annotations
1919

2020
import sys
21+
import shutil
2122
import time
2223
from pathlib import Path
2324
from typing import Optional
@@ -26,6 +27,10 @@
2627
from prompt_toolkit.formatted_text import FormattedText
2728
from prompt_toolkit.history import FileHistory
2829
from prompt_toolkit.key_binding import KeyBindings
30+
from prompt_toolkit.layout.containers import Window
31+
from prompt_toolkit.layout.controls import FormattedTextControl
32+
from prompt_toolkit.layout.dimension import Dimension
33+
from prompt_toolkit.styles import Style
2934

3035

3136
# Sentinel raised by the Ctrl+C path so the caller can distinguish
@@ -73,6 +78,32 @@ def _strip_surrogates(text: str) -> str:
7378
# ---------------------------------------------------------------- session ----
7479

7580

81+
class _VibePromptSession(PromptSession):
82+
"""PromptSession with a prompt-height that hugs the edited text."""
83+
84+
def _create_layout(self): # type: ignore[no-untyped-def]
85+
layout = super()._create_layout()
86+
# prompt_toolkit's bottom_toolbar is a screen-bottom status bar. Insert
87+
# our divider directly after the input container so it hugs the prompt.
88+
layout.container.children.insert(1, _prompt_divider_window())
89+
return layout
90+
91+
def _get_default_buffer_control_height(self) -> Dimension: # type: ignore[override]
92+
line_count = self.default_buffer.document.line_count
93+
return Dimension.exact(max(1, line_count))
94+
95+
96+
def _prompt_divider_window() -> Window:
97+
return Window(
98+
FormattedTextControl(
99+
lambda: FormattedText([("class:prompt-border", _prompt_rule())])
100+
),
101+
height=1,
102+
style="class:prompt-border",
103+
dont_extend_height=True,
104+
)
105+
106+
76107
def _force_utf8_stdout() -> None:
77108
"""Reconfigure stdout to UTF-8 on Windows so brand glyphs render."""
78109
if sys.platform != "win32":
@@ -252,14 +283,21 @@ def make_session(history_path: Optional[Path] = None) -> PromptSession:
252283
from cli.completer import SlashCompleter
253284

254285
ctrl_c_state = _CtrlCState()
255-
session = PromptSession(
286+
session = _VibePromptSession(
256287
history=SafeFileHistory(str(path)),
257288
completer=SlashCompleter(),
258289
complete_while_typing=True,
259290
key_bindings=_build_keybindings(ctrl_c_state),
260291
enable_history_search=True,
261292
mouse_support=False,
262293
multiline=True,
294+
reserve_space_for_menu=0,
295+
style=Style.from_dict(
296+
{
297+
"prompt": "#258bff bold",
298+
"prompt-border": "#4b5563",
299+
}
300+
),
263301
)
264302
# Expose the state so the outer loop can implement two-press exit.
265303
setattr(session, "vibe_ctrl_c_state", ctrl_c_state)
@@ -270,7 +308,7 @@ def make_session(history_path: Optional[Path] = None) -> PromptSession:
270308

271309

272310
def get_user_input(
273-
prompt_message: str = "> ",
311+
prompt_message: str = " ",
274312
*,
275313
session: Optional[PromptSession] = None,
276314
) -> str:
@@ -284,10 +322,20 @@ def get_user_input(
284322
EOFError: When the user hits Ctrl+D, or Ctrl+C on an empty line.
285323
"""
286324
sess = session or make_session()
287-
formatted = FormattedText([("class:prompt", prompt_message)])
325+
formatted = FormattedText(
326+
[
327+
("class:prompt-border", _prompt_rule() + "\n"),
328+
("class:prompt", prompt_message),
329+
]
330+
)
288331
return sess.prompt(formatted)
289332

290333

334+
def _prompt_rule() -> str:
335+
cols = shutil.get_terminal_size((88, 24)).columns
336+
return "─" * max(10, cols)
337+
338+
291339
def ctrl_c_within_window(session: PromptSession, window_sec: float = _EXIT_HINT_GAP_SEC) -> bool:
292340
"""Return True if the most recent Ctrl+C press was a "second press".
293341

0 commit comments

Comments
 (0)