Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ __pycache__/
/build/
develop-eggs/
dist/
!dhee/ui/web/dist/
!dhee/ui/web/dist/**
downloads/
eggs/
.eggs/
Expand Down Expand Up @@ -124,6 +126,7 @@ pip-delete-this-directory.txt

# Node (dashboard)
node_modules/
*.tsbuildinfo

# Rust build artifacts (dhee-accel)
target/
Expand Down
10 changes: 8 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
- Public Dhee is now positioned and packaged as **Dhee Developer Brain**:
local memory, handoff, harness setup, and git-backed repo context.
- Rewrote the README as a concise first-read product page focused on why Dhee
matters, the 30-second token-router proof, install, integrations, benchmarks,
and the public-core/paid-team-layer boundary.
matters, the UI demo, install, integrations, benchmarks, and the
public-core/paid-team-layer boundary.
- Restored the public `dhee ui` Sankhya workspace app: router screen, infinite
folders canvas, workspace/task/memory/context views, and the FastAPI bridge
that maps those screens onto local Dhee state.
- Added product-grade UI screens for Dhee's context-governance workflow:
Command Center, Context Firewall, Handoff Hub, Proof Replay, Repo Brain,
Learning Inbox, and Portability & Trust.
- Added `dhee demo token-router`, a deterministic context-firewall demo that
shows raw tool-output tokens, digest tokens, savings, and expansion pointers
without requiring a live agent session.
Expand Down
41 changes: 21 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<p align="center">
<img src="docs/dhee-logo.png" alt="Dhee" width="80">
<img src="docs/dhee-hero.png" alt="Dhee - the context firewall for AI coding agents" width="100%">
</p>

<h1 align="center">Dhee</h1>
Expand All @@ -19,7 +19,7 @@

<p align="center">
<a href="#why-dhee">Why</a> |
<a href="#try-it">Try it</a> |
<a href="#dhee-ui">Dhee UI</a> |
<a href="#install">Install</a> |
<a href="#how-it-works">How it works</a> |
<a href="#integrations">Integrations</a> |
Expand Down Expand Up @@ -58,32 +58,33 @@ The promise is simple:

---

## Try It
## Dhee UI

Run the built-in context-router demo. It needs no API key and no connected agent:
Run the local Dhee workspace UI. It needs no API key and no connected agent:

```bash
dhee demo token-router
dhee ui
```

Example result:
<p align="center">
<video src="docs/dhee-ui-demo.mp4" controls muted loop poster="docs/dhee-ui-demo-poster.png" width="100%"></video>
</p>

```text
Dhee token-router demo
context firewall: agent sees the right thing, not everything
raw tokens: 25,208
digest tokens: 1,742
saved: 23,466 (93.1%)
```
<p align="center">
<a href="docs/dhee-ui-demo.mp4">Watch the 13-second UI demo</a>
</p>

The demo shows how Dhee handles three common agent hazards:
The UI opens on a command center, then lets you inspect:

- a noisy pytest failure log
- a large git diff
- a long source file read
- Context Firewall: token savings, digests, evidence pointers, expansions, and session history
- Repo Brain: an infinite folders canvas for linked repos, projects, active sessions, tasks, and shared context
- Handoff Hub: resumable task state without replaying the transcript
- Proof Replay: what Dhee injected, hid, digested, expanded, promoted, or rejected
- Learning Inbox: evidence-backed candidate learnings with promote/reject actions
- Portability & Trust: signed `.dheemem` export/import readiness and dry-run inspection

In each case the agent receives a useful digest, while exact raw evidence stays
behind `dhee_expand_result(ptr="...")` for explicit expansion.
The raw evidence still stays behind `dhee_expand_result(ptr="...")`; the UI
makes the routing and expansion decisions inspectable.

---

Expand All @@ -110,7 +111,7 @@ Useful first commands:
```bash
dhee status
dhee doctor
dhee demo token-router
dhee ui
dhee handoff
dhee context state --card
dhee runtime status
Expand Down
30 changes: 30 additions & 0 deletions dhee/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
dhee handoff Emit structured resume JSON for a new harness/agent
dhee harness status Show native Claude Code / Codex integration state
dhee demo token-router Show how Dhee keeps raw tool output behind pointers
dhee ui Open the local Dhee dashboard
dhee benchmark Run performance benchmarks
dhee status Version, config, DB info
"""
Expand Down Expand Up @@ -1240,6 +1241,20 @@ def cmd_demo(args: argparse.Namespace) -> None:
print(format_token_router_demo(report, show_digests=not getattr(args, "no_digests", False)))


def cmd_ui(args: argparse.Namespace) -> None:
"""Run the local Dhee web UI."""
from dhee.ui.cli import cmd_ui as run_ui

run_ui(args)


def cmd_ui_build(args: argparse.Namespace) -> None:
"""Build the local Dhee web UI assets."""
from dhee.ui.cli import cmd_ui_build as run_ui_build

run_ui_build(args)


def cmd_status(args: argparse.Namespace) -> None:
"""Show version, config, DB size, detected agents, and brain health.

Expand Down Expand Up @@ -2531,6 +2546,20 @@ def build_parser() -> argparse.ArgumentParser:
p_demo.add_argument("--no-digests", action="store_true", help="Hide digest previews")
p_demo.add_argument("--json", action="store_true", help="JSON output")

# ui
p_ui = sub.add_parser("ui", help="Run the local Dhee web UI")
p_ui.add_argument("--host", default="127.0.0.1", help="Bind host (loopback by default)")
p_ui.add_argument("--port", type=int, default=8787, help="Bind port")
p_ui.add_argument("--repo", help="Repo/workspace to inspect (default: cwd)")
p_ui.add_argument("--dev", action="store_true", help="Start the Vite frontend with hot reload")
p_ui.add_argument("--verbose", action="store_true", help="Show frontend logs in dev mode")
p_ui.add_argument("--open", action="store_true", help=argparse.SUPPRESS)
p_ui.add_argument("--no-open", action="store_true", help="Don't auto-open the UI in the default browser")

p_ui_build = sub.add_parser("ui-build", help="Build the Dhee web UI assets")
p_ui_build.add_argument("--install", action="store_true", help="Force `npm install` before building")
p_ui_build.set_defaults(func=cmd_ui_build)

# list
p_list = sub.add_parser("list", help="List all memories")
p_list.add_argument("--user-id", default="default", help="User ID")
Expand Down Expand Up @@ -3082,6 +3111,7 @@ def build_parser() -> argparse.ArgumentParser:
"search": cmd_search,
"checkpoint": cmd_checkpoint,
"demo": cmd_demo,
"ui": cmd_ui,
"list": cmd_list,
"stats": cmd_stats,
"decay": cmd_decay,
Expand Down
20 changes: 20 additions & 0 deletions dhee/ui/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""Sankhya — Dhee's web UI.

`dhee ui` starts a FastAPI server that serves the built React SPA and
exposes the Dhee substrate (memories, router stats, policy, evolution,
conflicts, tasks) as JSON endpoints.
"""

try:
from dhee.ui.server import app, create_app # noqa: F401
except ModuleNotFoundError:
# FastAPI is an optional dep (`pip install dhee[api]`). The package
# still imports so `dhee ui` can print a useful message.
app = None # type: ignore[assignment]

def create_app(*args, **kwargs): # type: ignore[no-redef]
raise ModuleNotFoundError(
"dhee.ui requires fastapi + uvicorn. Install with `pip install 'dhee[api]'`."
)

__all__ = ["app", "create_app"]
171 changes: 171 additions & 0 deletions dhee/ui/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
"""`dhee ui` — start Sankhya.

Starts the FastAPI bridge (which also serves the built SPA) on a local
port. If the SPA hasn't been built, prints the build instructions and
exits cleanly so users know exactly what to do.
"""

from __future__ import annotations

import argparse
import logging
import os
import subprocess
import sys
import threading
import time
import webbrowser
from pathlib import Path

_LOOPBACK_HOSTS = {"127.0.0.1", "localhost", "::1"}


def _schedule_browser_open(url: str, delay: float = 1.2) -> None:
"""Open the UI in the default browser shortly after startup.

Runs in a thread so we don't block uvicorn. Silent on failure —
headless boxes (CI, servers, SSH sessions without $DISPLAY) fall
back cleanly to the printed URL.
"""

def _run() -> None:
time.sleep(delay)
try:
webbrowser.open_new_tab(url)
except Exception:
pass

threading.Thread(target=_run, daemon=True).start()


def cmd_ui(args: argparse.Namespace) -> None:
try:
import uvicorn
except ModuleNotFoundError as exc:
print("Dhee UI requires uvicorn. Install with `pip install 'dhee[api]'`.", file=sys.stderr)
raise SystemExit(1) from exc
try:
from dhee.ui.server import create_app
except ModuleNotFoundError as exc:
print("Dhee UI requires FastAPI. Install with `pip install 'dhee[api]'`.", file=sys.stderr)
raise SystemExit(1) from exc

if args.host not in _LOOPBACK_HOSTS and os.environ.get("DHEE_UI_ALLOW_PUBLIC") != "1":
raise SystemExit(
"Refusing to expose Dhee UI on a non-loopback host. "
"Set DHEE_UI_ALLOW_PUBLIC=1 only behind a trusted auth proxy."
)

if getattr(args, "repo", None):
os.environ["DHEE_UI_REPO"] = str(Path(args.repo).expanduser().resolve())

web_dir = Path(__file__).parent / "web"
dist = web_dir / "dist"

# Auto-fallback to dev mode if dist is missing and we're in a source tree
if not dist.exists() and not args.dev:
if (web_dir / "package.json").exists():
print("Sankhya SPA not built. Falling back to dev mode (hot-reloading)...")
args.dev = True
else:
print(f"Sankhya SPA not built yet.")
print(f" cd {web_dir}")
print(f" npm install && npm run build")
sys.exit(1)

logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")

app = create_app(serve_static=not args.dev, dev_mode=args.dev)
host_display = "127.0.0.1" if args.host in ("0.0.0.0", "::") else args.host
ui_url = f"http://{host_display}:{args.port}/"
print(f"Sankhya — Dhee UI")
print(f" API http://{args.host}:{args.port}/api")

frontend_proc = None
if args.dev:
print(f" Dev Starting Vite frontend (http://127.0.0.1:5173)...")
print(f" Dashboard http://{args.host}:{args.port}/ (Proxied to Vite)")
try:
frontend_proc = subprocess.Popen(
["npm", "run", "dev"],
cwd=str(web_dir),
stdout=subprocess.DEVNULL if not args.verbose else None,
stderr=subprocess.STDOUT if not args.verbose else None,
)
except Exception as e:
print(f"Warning: Could not start frontend: {e}")
else:
print(f" Dashboard http://{args.host}:{args.port}/")
if args.host == "127.0.0.1" and args.port == 8080:
print(f" Tip: Add '127.0.0.1 dhee.ui' to /etc/hosts to use http://dhee.ui:8080/")

should_open = not args.no_open and os.environ.get("DHEE_UI_NO_OPEN") != "1"
# Skip auto-open on headless servers (no DISPLAY on X11).
if should_open and sys.platform.startswith("linux") and not os.environ.get("DISPLAY"):
should_open = False
if should_open:
_schedule_browser_open(ui_url, delay=1.5 if args.dev else 1.0)

try:
uvicorn.run(app, host=args.host, port=args.port, log_level="info")
finally:
if frontend_proc:
frontend_proc.terminate()
try:
frontend_proc.wait(timeout=5)
except subprocess.TimeoutExpired:
frontend_proc.kill()


def cmd_ui_build(args: argparse.Namespace) -> None:
web = Path(__file__).parent / "web"
if not (web / "package.json").exists():
print(f"No package.json at {web}", file=sys.stderr)
sys.exit(1)
if args.install or not (web / "node_modules").exists():
print("→ npm install")
subprocess.check_call(["npm", "install"], cwd=str(web))
print("→ npm run build")
subprocess.check_call(["npm", "run", "build"], cwd=str(web))
print("✓ Built at", web / "dist")


def register(sub: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
p = sub.add_parser("ui", help="Start Sankhya (Dhee web UI)")
p.add_argument("--host", default="127.0.0.1")
p.add_argument("--port", type=int, default=8787)
p.add_argument("--repo", help="Repo/workspace to inspect (default: cwd)")
p.add_argument(
"--dev",
action="store_true",
help="Start both API bridge and Vite frontend with hot-reloading.",
)
p.add_argument(
"--verbose",
action="store_true",
help="Show frontend (Vite) logs in dev mode.",
)
p.add_argument(
"--no-open",
action="store_true",
help="Don't auto-open the UI in the default browser.",
)
p.add_argument("--open", action="store_true", help=argparse.SUPPRESS)
p.set_defaults(func=cmd_ui)

pb = sub.add_parser("ui-build", help="Build the Sankhya SPA (npm install + npm run build)")
pb.add_argument("--install", action="store_true", help="Force `npm install` even if node_modules exists")
pb.set_defaults(func=cmd_ui_build)


def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(prog="dhee-ui")
sub = parser.add_subparsers(dest="command")
register(sub)
args = parser.parse_args(["ui", *(argv or sys.argv[1:])])
args.func(args)
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading
Loading