Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
edef405
feat(dashboard): general monitoring web dashboard (Watch->Finding + S…
wangxingjun778 Jul 16, 2026
fe6e8f2
feat(board): rename /dashboard -> /board and add LeapBoard branding +…
wangxingjun778 Jul 16, 2026
09654e0
bump version
wangxingjun778 Jul 16, 2026
bea7e4b
feat(board): refine LeapBoard report layouts and i18n
wangxingjun778 Jul 16, 2026
7c2146a
Enhance LeapBoard session context monitoring
wangxingjun778 Jul 16, 2026
4b8cf86
Harden LeapBoard watch shutdown lifecycle
wangxingjun778 Jul 16, 2026
c7743f5
Fix /board leaking to LLM in in-process REPL
wangxingjun778 Jul 16, 2026
8776a26
Sweep stale client-coupled watches on daemon startup
wangxingjun778 Jul 16, 2026
a2b36b8
Suggest command instead of chatting on typo'd subcommand
wangxingjun778 Jul 16, 2026
1e6e7ec
Make /board session non-blocking and report watch id
wangxingjun778 Jul 16, 2026
2d98169
Reliably force-stop leapd on exit with transparent progress
wangxingjun778 Jul 16, 2026
5c06256
Require human confirmation for slash-command path changes
wangxingjun778 Jul 16, 2026
4f4ecca
Open the web board on /board new and fix view targeting
wangxingjun778 Jul 17, 2026
188d48a
Refactor LeapBoard to session-only, template-as-hub model
wangxingjun778 Jul 17, 2026
59e2ee5
Fix LeapBoard refactor leftovers in CLI, web UI, and TUI renderer
wangxingjun778 Jul 17, 2026
e62b02b
Harden /board command surface: reject unknown commands, rich status, …
wangxingjun778 Jul 17, 2026
e488297
Complete /config slash completion, mask secrets, and lead install wit…
wangxingjun778 Jul 17, 2026
50ba3e6
Unify slash-command terminator to the flat LEAP footer
wangxingjun778 Jul 17, 2026
fed2370
Fix /board 'missing or invalid token' by validating the dashboard token
wangxingjun778 Jul 17, 2026
435a958
Redesign LeapBoard with a Swiss/minimal/academic light theme
wangxingjun778 Jul 17, 2026
b1e44af
Lock the dashboard optional extra (aiohttp) in uv.lock
wangxingjun778 Jul 17, 2026
3db31fe
Fix LeapBoard generic layout: fill width, empty states, and i18n
wangxingjun778 Jul 17, 2026
95eef8f
Add signal-derived charts and a Series section to LeapBoard
wangxingjun778 Jul 17, 2026
bd8b19c
Fix PR #21 review findings across dashboard, config, and CLI
wangxingjun778 Jul 20, 2026
089184d
remove col `muted`, add new col `url` for LeapBoard
wangxingjun778 Jul 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ This document is the LeapFlow engineering collaboration contract. It is not only

- **Deep review for large changes**: When a change substantially affects architecture, runtime behavior, user flows, persistence, safety, or multiple modules, perform an additional deep review before considering the work complete.
- **Human confirmation for TUI changes**: Any TUI layout or interaction-logic change requires a second human confirmation before it is considered ready.
- **Human confirmation for slash-command paths**: Any change that adds, removes, renames, reroutes, or alters the behavior of a slash command (`/...`) — across the registry, router, in-process REPL, daemon REPL, `command_execute`, completion, and rendering — requires a second human confirmation before it is considered ready. This applies especially to user-experience-facing behavior (dispatch, prompts, confirmations, output, browser/dashboard launches, and error/recovery messaging), which must never be shipped on a single pass.
- **Design goal check**: Verify that the implementation actually achieves the intended design goal and is not just a local patch.
- **Optimality check**: Evaluate whether the solution is the simplest robust design, avoids unnecessary abstractions, and fits the existing architecture.
- **Regression impact check**: Inspect affected modules and user journeys for logic bugs, degraded UX, broken compatibility, slower feedback, weaker diagnostics, or worse failure recovery.
Expand Down
118 changes: 111 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

### News

- **2026-07-16**: **LeapBoard** — general-purpose monitoring web dashboard (Watch→Finding + Server-Driven UI); `/board` entry, live session analysis, and finance/sentiment/research templates.
- **2026-07-16**: v0.0.4 released — protocol-driven recovery coordinator with budget-constrained strategies, unified error classification, checkpoint-based cross-turn resumption, structured audit trail, and tool execution idempotency ledger.
- **2026-07-15**: v0.0.3 released — layered YAML config, Path/Profile/Cache layouts, encrypted secret refs, DuckDB cache indexing, `leap config`, and robustness hardening.
- **2026-07-15**: v0.0.2 released — TUI, Gateway/App Connector, Hub sync, Scheduler, Workflow Copilot, and runtime hardening.
Expand Down Expand Up @@ -110,26 +111,44 @@ uv run leap --help

### 2. Configure Your LLM

If you already have an API key, base URL, and model name, save them through the unified config command:
Launch the interactive TUI and configure your provider with the built-in `/config` command. It stores the API key in the local secret vault and writes only a `secret://` reference into durable config:

```bash
leap config llm set \
--base-url https://dashscope.aliyuncs.com/compatible-mode/v1 \
--model qwen3.7-plus \
--ask-api-key
leap
```

Inside the TUI (subcommands, keys, and values all auto-complete as you type):

```text
/config llm set --base-url https://dashscope.aliyuncs.com/compatible-mode/v1 --model qwen3.7-plus --api-key sk-...
/config show llm.model # verify the model and base URL
/config show llm.api_key # confirm the key is stored (shown masked, e.g. ***abc)
```

Paste your API key when prompted. LeapFlow stores the key in the local secret vault and writes only a `secret://` reference into durable config.
Changes hot-reload the active session when possible, so you can start chatting right away.

<details>
<summary>More: configure from the shell (CLI)</summary>

For scripts or CI, pass the key explicitly through the same config control plane:
The same control plane is available as `leap config` for terminals, scripts, or CI. Use `--ask-api-key` for a secure masked prompt, or `--api-key` to pass it explicitly:

```bash
# Interactive, secure key prompt:
leap config llm set \
--base-url https://dashscope.aliyuncs.com/compatible-mode/v1 \
--model qwen3.7-plus \
--ask-api-key

# Non-interactive (scripts/CI):
leap config llm set \
--base-url https://api.openai.com/v1 \
--model gpt-4o \
--api-key "$OPENAI_API_KEY"
```

LeapFlow stores the key in the local secret vault and writes only a `secret://` reference into durable config.
</details>

### 3. Install Execution Backend (macOS only)

The native execution backend enables screen capture, accessibility tree access, and input injection. Skip this step if you only want to chat or explore with `--mock-host`.
Expand Down Expand Up @@ -511,6 +530,89 @@ For deployment environments, provision platform credentials through the same gat

---

## LeapBoard — Monitoring Dashboard

> **Signals into insight.**

**LeapBoard** extends LeapFlow's Observe/Orient boundary into a general-purpose, always-on monitoring surface. Launch independent, persistent **watches** — for financial-market anomalies, public-opinion (sentiment) shifts, new research papers, or your own live conversation — and each one continuously observes its source, filters signal from noise, and surfaces **findings** (typed, scored, with evidence) onto a real-time web board.

It is domain-neutral by design: finance, sentiment, research, and session analysis differ only by a small YAML template and their finding payload — never by branching in core code.

### How it works

```
Watch (leapd-hosted) ── observe → SNR filter → score → Finding ──▶ NotificationBus
▲ │ push
└── trigger / manual refresh / session batch / salience ───────┘
YAML template ─▶ ViewSpec (validated) ─▶ Web board (SDUI) ◀── WebSocket
```

- **Continuous observation** — a board starts as a `Watch` in `leapd`. Scheduler triggers, manual `/board refresh`, and session-analysis batch thresholds all run the same observe→finding cycle; new findings are persisted, severity-gated, and pushed to browsers over WebSocket.
- **Refresh model** — the session board re-analyzes as the conversation accumulates turns, when written workspace artifacts change, or on manual `/board refresh`; `/board pause`/`stop` halt re-analysis until resumed. The chosen **template** only changes rendering, never what is analyzed (always the current session).
- **Server-Driven UI** — each scenario is authored as a **YAML template** compiled into a validated **ViewSpec** over a fixed component catalog (cards, tables, charts, timelines, gauges, story panels…). Interactive components talk back through a bidirectional action protocol; unknown component types degrade gracefully, and bespoke visuals use a `Custom` escape hatch. The board never renders arbitrary HTML/JS.
- **View client** — the board connects to `leapd` like the TUI does, with no privileged coupling. The web server is optional (`aiohttp`) and degrades with a clear install hint when absent.

### Install & enable

```bash
pip install 'leapflow[dashboard]' # adds the optional aiohttp web server
```

The board binds to `127.0.0.1` with a per-session access token. Tune it through `leap config`:

| Key | Default | Description |
|-----|---------|-------------|
| `dashboard.enabled` | `true` | Enable the local monitoring web board |
| `dashboard.bind` | `127.0.0.1` | Bind address (keep loopback) |
| `dashboard.port` | `8765` | Web server port |
| `dashboard.auto_open` | `true` | Open the default browser automatically |
| `monitor.session_batch_turns` | `6` | Re-analyze the session every N new turns |

### Usage

**Analyze the current session** — from the TUI or shell. LeapBoard always analyzes the *current session*; a **template** is just the lens it renders through (`generic` is the default; an unrecognized name is rejected rather than silently coerced):

```text
/board open the session board with the default (generic) template
/board finance render the same session analysis through the finance lens
/board status show watch state, recent findings, and available templates
/board refresh re-analyze now (append an id to target a specific watch)
/board pause|resume pause / resume analysis (append an id to target a watch)
/board stop stop the current session, or `/board stop <id>` a specific watch
```

```bash
leap board # ensure the server is running and open the browser
leap board --no-open # print the URL instead of opening a browser
```

Executing any open entry auto-launches your default browser at the token-scoped local URL. Inside the browser, the language switcher supports English, Chinese, French, Spanish, Arabic, and Russian.

**Templates (view lenses)** — templates are the single view dimension, and you can add your own with one command:

```text
/board templates list built-in and custom templates
/board templates add ./my.yaml validate and register a custom lens (profile-scoped)
/board templates show <id> show a template's source and title
/board templates remove <id> remove a custom template (builtins stay)
```

A custom template is any SDUI YAML file. `add` validates it (rejecting templates that will not compile), copies it into `profiles/<profile>/dashboard/templates/`, and makes it immediately openable via `/board <id>`. Custom templates override built-ins of the same name.

**Session context coverage** — the session board watches more than chat text. It also scans current-session `file_write` tool results and, when the path is inside the workspace and safe to read without approval, includes a capped/redacted file excerpt in the analysis. The **Observation status** panel shows refresh reason, coverage, observed targets, included/skipped file artifacts, and missing context notes.

**Natural language** — just ask ("analyze this conversation", "show the finance lens"); the agent opens the board with the right template when the runtime can safely map the request.

### Showcase

- **Session analysis (default `generic`)** — a storyline, insights, decisions, action items, open questions, an entity map, artifact coverage, and suggested next prompts; refreshes automatically as the conversation grows or workspace artifacts change.
- **Finance / Sentiment / Research lenses** — the same session analysis, reframed: finance surfaces calls, an execution checklist, and exposures; sentiment surfaces the narrative pulse and themes; research surfaces open questions, evidence, and follow-ups.
- **Custom lenses** — bring your own YAML template and open it with `/board <name>`; it renders the current session through your layout with zero code changes.

> Adding a lens = one YAML template dropped in (or `/board templates add`). The board core stays untouched — one analysis target, no domain branching, no hardcoded layouts.

---

## Safety & Approval

LeapFlow enforces a **layered safety architecture** that balances autonomy with human oversight. The goal is minimal interruption — the agent asks for permission only when an action carries real consequences.
Expand Down Expand Up @@ -768,6 +870,8 @@ uv run pytest -k "test_world_model" -q # By keyword
| `learning/` | Skill distillation, parameterization, and maturity lifecycle |
| `skills/` | Skill library, runtime execution, and self-evolution (Loop γ) |
| `copilot/` | Workflow-level next-step prediction and proactive suggestion |
| `monitor/` | Domain-neutral Watch→Finding monitoring runtime (leapd-hosted scheduler, findings store) |
| `dashboard/` | LeapBoard — Server-Driven UI web board (ViewSpec catalog, YAML templates, WebSocket fan-out) |
| `analysis/` | Six-layer denoising pipeline for trajectory refinement |
| `engine/` | Session orchestration and ReAct execution loop |
| `memory/` | Three-tier event-driven memory (working → episodic → long-term) |
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ dev = [
"ruff>=0.4.0",
]
hub = ["modelscope-hub>=0.1.0"]
dashboard = ["aiohttp>=3.9"]

[project.scripts]
leap = "leapflow.__main__:main"
Expand All @@ -56,6 +57,8 @@ include = ["leapflow*"]

[tool.setuptools.package-data]
"leapflow.gateway.action_packs" = ["*.yaml"]
"leapflow.dashboard.templates" = ["*.yaml"]
"leapflow.dashboard.static" = ["*"]

[tool.pytest.ini_options]
asyncio_mode = "auto"
Expand Down
52 changes: 51 additions & 1 deletion src/leapflow/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,23 @@ def _daemon_enabled(args: argparse.Namespace) -> bool:
return raw not in {"0", "false", "no", "off"}


def _suggest_known_command(token: str, known_commands: set[str]) -> str | None:
"""Return the closest known command when a token looks like a typo, else None.

Uses a conservative similarity cutoff so genuine free-text prompts (e.g.
``leap what is a daemon``) still route to chat, while near-miss command
typos (e.g. ``deamon`` -> ``daemon``) are caught and surfaced as a
suggestion instead of silently becoming an LLM chat turn that also spawns a
daemon.
"""
import difflib

matches = difflib.get_close_matches(
token.lower(), sorted(known_commands), n=1, cutoff=0.82
)
return matches[0] if matches else None


def main(argv: list[str] | None = None) -> int:
common = argparse.ArgumentParser(add_help=False)
common.add_argument(
Expand Down Expand Up @@ -255,6 +272,15 @@ def main(argv: list[str] | None = None) -> int:
serve_parser = daemon_sub.add_parser("serve", help=argparse.SUPPRESS)
serve_parser.add_argument("--internal", action="store_true", help=argparse.SUPPRESS)

# leap board (LeapBoard monitoring dashboard)
dashboard_parser = subparsers.add_parser("board", help="Open the LeapBoard session analysis board")
dashboard_parser.add_argument("template", nargs="?", default="", help="Template lens to render (default: generic)")
dashboard_parser.add_argument("--serve", action="store_true", help=argparse.SUPPRESS)
dashboard_parser.add_argument("--token", default="", help=argparse.SUPPRESS)
dashboard_parser.add_argument("--port", type=int, default=0, help="Override the dashboard port")
dashboard_parser.add_argument("--bind", default="", help="Override the dashboard bind address")
dashboard_parser.add_argument("--no-open", action="store_true", help="Print the URL instead of opening a browser")

# leap config
config_parser = subparsers.add_parser("config", help="View and update LeapFlow configuration")
config_sub = config_parser.add_subparsers(dest="config_action")
Expand Down Expand Up @@ -306,7 +332,7 @@ def main(argv: list[str] | None = None) -> int:

# ── Pre-parse: detect if first non-flag arg is a known subcommand ──
# If not, treat everything non-flag as a chat prompt.
known_commands = {"teach", "run", "skills", "relearn", "host", "daemon", "config"}
known_commands = {"teach", "run", "skills", "relearn", "host", "daemon", "config", "board"}
effective_argv = list(argv) if argv is not None else sys.argv[1:]

# Find first non-flag argument, skipping values owned by global options.
Expand All @@ -327,6 +353,25 @@ def main(argv: list[str] | None = None) -> int:
break

if first_pos is not None and effective_argv[first_pos] not in known_commands:
first_token = effective_argv[first_pos]
non_flag_tokens = [tok for tok in effective_argv if not tok.startswith("-")]
# A short, command-like invocation whose first word is a near-miss of a
# known command is almost certainly a typo (e.g. `leap deamon status`).
# Surface a suggestion instead of silently spawning a daemon + LLM chat.
if len(non_flag_tokens) <= 3:
suggestion = _suggest_known_command(first_token, known_commands)
if suggestion is not None:
# Replace only the mistyped token so preceding global flags
# (e.g. --profile dev) and trailing args are preserved.
corrected_argv = list(effective_argv)
corrected_argv[first_pos] = suggestion
corrected = " ".join(["leap", *corrected_argv])
sys.stderr.write(
f"leap: '{first_token}' is not a leap command. "
f"Did you mean '{suggestion}'?\n"
f"Try: {corrected}\n"
)
return 2
# Collect all non-option prompt tokens while preserving global option values.
flags: list[str] = []
prompt_words = []
Expand Down Expand Up @@ -382,6 +427,11 @@ def main(argv: list[str] | None = None) -> int:
from leapflow.cli.commands.daemon import cmd_daemon
return cmd_daemon(args)

# Dashboard is a view client (connects to leapd); no Context initialization
if args.command == "board":
from leapflow.cli.commands.dashboard import cmd_dashboard
return cmd_dashboard(args)

try:
if args.command in {"interactive", "chat"} and _daemon_enabled(args):
return asyncio.run(_async_daemon_main(args))
Expand Down
70 changes: 70 additions & 0 deletions src/leapflow/cli/commands/dashboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""`leap board` — open or serve the LeapBoard monitoring web dashboard.

Two modes:
- default (client): ensure a dashboard server is running, then open the default
browser at the requested view. Returns immediately; the server runs detached.
- ``--serve`` (server): run the aiohttp view server in the foreground. Used by
the launcher spawn and for a dedicated dashboard terminal.

The dashboard is a view client: it connects to leapd, so it does not initialize
its own Context.
"""

from __future__ import annotations

import argparse
import asyncio

from leapflow.config import load_config
from leapflow.dashboard import launcher

_DEP_HINT = (
"The dashboard web server requires the optional 'aiohttp' dependency.\n"
"Install it with: pip install 'leapflow[dashboard]'"
)


def cmd_dashboard(args: argparse.Namespace) -> int:
"""Entry point for the ``leap board`` subcommand."""
settings = load_config()

if not launcher.aiohttp_available():
print(_DEP_HINT)
return 1

if getattr(args, "serve", False):
return _serve(args, settings)
return _open(args, settings)


def _serve(args: argparse.Namespace, settings: object) -> int:
from leapflow.dashboard.server import run_server

token = getattr(args, "token", "") or launcher.generate_token()
bind = getattr(args, "bind", "") or settings.dashboard_bind
port = getattr(args, "port", 0) or settings.dashboard_port
try:
return asyncio.run(run_server(settings, token=token, bind=bind, port=port))
except KeyboardInterrupt:
return 130


def _open(args: argparse.Namespace, settings: object) -> int:
try:
state = launcher.ensure_server(settings)
except RuntimeError as exc:
print(f"dashboard: {exc}")
return 1

template = getattr(args, "template", "") or ""
url = launcher.build_view_url(state["bind"], state["port"], state["token"], template=template)

auto_open = bool(getattr(settings, "dashboard_auto_open", True)) and not getattr(args, "no_open", False)
if auto_open and launcher.open_in_browser(url):
print(f"Opened dashboard in your browser: {url}")
else:
print(f"Dashboard ready: {url}")
return 0


__all__ = ["cmd_dashboard"]
Loading