From cfff18b0051c213cec6d7ac08349fbee51f75b33 Mon Sep 17 00:00:00 2001 From: Ashish-dwi99 Date: Wed, 13 May 2026 17:09:18 +0530 Subject: [PATCH 1/5] Restore public Dhee UI --- CHANGELOG.md | 8 +- README.md | 35 +-- dhee/cli.py | 21 ++ dhee/ui/__init__.py | 5 + dhee/ui/server.py | 278 ++++++++++++++++++ dhee/ui/static/app.js | 270 +++++++++++++++++ dhee/ui/static/index.html | 151 ++++++++++ dhee/ui/static/styles.css | 591 ++++++++++++++++++++++++++++++++++++++ pyproject.toml | 5 +- tests/test_packaging.py | 3 + tests/test_ui.py | 35 +++ 11 files changed, 1378 insertions(+), 24 deletions(-) create mode 100644 dhee/ui/__init__.py create mode 100644 dhee/ui/server.py create mode 100644 dhee/ui/static/app.js create mode 100644 dhee/ui/static/index.html create mode 100644 dhee/ui/static/styles.css create mode 100644 tests/test_ui.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e60bced..af1d4ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,12 @@ 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` dashboard and `dhee-ui` entrypoint. The OSS UI + uses the same dashboard design language while rendering local developer-brain + surfaces: context firewall, current state, runtime, integrations, repo + context, and portability. - 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. diff --git a/README.md b/README.md index ce93d54..e21fe19 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@

Why | - Try it | + UI demo | Install | How it works | Integrations | @@ -58,32 +58,25 @@ The promise is simple: --- -## Try It +## UI Demo -Run the built-in context-router demo. It needs no API key and no connected agent: +Run the local dashboard. It needs no API key and no connected agent: ```bash -dhee demo token-router +dhee ui --open ``` -Example result: +The first screen shows: -```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%) -``` - -The demo shows how Dhee handles three common agent hazards: - -- a noisy pytest failure log -- a large git diff -- a long source file read +- router savings from noisy pytest logs, git diffs, and long source reads +- current state health and latest handoff +- local runtime status +- repo-shared context entries +- integration surfaces for Claude Code, Codex, MCP clients, and Hermes +- export/import/uninstall commands so the local data stays portable -In each case the agent receives a useful digest, while exact raw evidence stays -behind `dhee_expand_result(ptr="...")` for explicit expansion. +The UI is a view over the same public CLI/MCP primitives. The raw evidence still +stays behind `dhee_expand_result(ptr="...")`; the dashboard makes that visible. --- @@ -110,7 +103,7 @@ Useful first commands: ```bash dhee status dhee doctor -dhee demo token-router +dhee ui --open dhee handoff dhee context state --card dhee runtime status diff --git a/dhee/cli.py b/dhee/cli.py index 1b65661..b844be3 100644 --- a/dhee/cli.py +++ b/dhee/cli.py @@ -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 """ @@ -1240,6 +1241,18 @@ 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 dashboard.""" + from dhee.ui.server import serve + + serve( + host=getattr(args, "host", "127.0.0.1"), + port=int(getattr(args, "port", 8765) or 8765), + repo=getattr(args, "repo", None), + open_browser=bool(getattr(args, "open", False)), + ) + + def cmd_status(args: argparse.Namespace) -> None: """Show version, config, DB size, detected agents, and brain health. @@ -2531,6 +2544,13 @@ 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 dashboard") + p_ui.add_argument("--host", default="127.0.0.1", help="Bind host (loopback by default)") + p_ui.add_argument("--port", type=int, default=8765, help="Bind port") + p_ui.add_argument("--repo", help="Repo/workspace to inspect (default: cwd)") + p_ui.add_argument("--open", action="store_true", help="Open in the default browser") + # list p_list = sub.add_parser("list", help="List all memories") p_list.add_argument("--user-id", default="default", help="User ID") @@ -3082,6 +3102,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, diff --git a/dhee/ui/__init__.py b/dhee/ui/__init__.py new file mode 100644 index 0000000..2ba1084 --- /dev/null +++ b/dhee/ui/__init__.py @@ -0,0 +1,5 @@ +"""Local Dhee dashboard.""" + +from .server import build_dashboard_payload, serve + +__all__ = ["build_dashboard_payload", "serve"] diff --git a/dhee/ui/server.py b/dhee/ui/server.py new file mode 100644 index 0000000..d79187d --- /dev/null +++ b/dhee/ui/server.py @@ -0,0 +1,278 @@ +"""Local-first Dhee dashboard. + +The public UI intentionally uses the same small HTTP/static-file shape as the +team dashboard, but its data model is local developer context: router savings, +runtime health, handoff state, repo context, integrations, and portability. +""" + +from __future__ import annotations + +import argparse +import json +import os +import subprocess +import threading +import webbrowser +from http import HTTPStatus +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +from typing import Any +from urllib.parse import parse_qs, urlparse + +from dhee.context_state import ContextStateStore +from dhee.core.kernel import get_last_session +from dhee.demo import token_router_demo + + +STATIC_DIR = Path(__file__).with_name("static") +_LOOPBACK_HOSTS = {"127.0.0.1", "localhost", "::1"} + + +def _repo_root(repo: str | None = None) -> Path: + return Path(repo or os.environ.get("DHEE_UI_REPO") or Path.cwd()).expanduser().resolve() + + +def _git_value(repo: Path, args: list[str], default: str = "") -> str: + try: + out = subprocess.check_output(["git", *args], cwd=str(repo), stderr=subprocess.DEVNULL, text=True) + return out.strip() or default + except Exception: + return default + + +def _read_repo_context(repo: Path, *, limit: int = 30) -> dict[str, Any]: + path = repo / ".dhee" / "context" / "entries.jsonl" + entries: list[dict[str, Any]] = [] + if path.exists(): + try: + for line in path.read_text(encoding="utf-8").splitlines(): + if not line.strip(): + continue + try: + item = json.loads(line) + except json.JSONDecodeError: + continue + entries.append(item) + except OSError: + pass + + kind_counts: dict[str, int] = {} + for item in entries: + kind = str(item.get("kind") or item.get("type") or "note") + kind_counts[kind] = kind_counts.get(kind, 0) + 1 + return { + "path": str(path), + "exists": path.exists(), + "count": len(entries), + "kind_counts": kind_counts, + "entries": entries[-limit:], + } + + +def _runtime_status() -> dict[str, Any]: + try: + from dhee import runtime + + return runtime.status(timeout=0.25) + except Exception as exc: + return {"status": "unavailable", "error": str(exc)} + + +def _context_status(repo: Path) -> dict[str, Any]: + store = ContextStateStore(repo=str(repo), workspace_id=str(repo), user_id="default", agent_id="dhee-ui") + try: + status = store.status() + card = store.render_state_card() + except Exception as exc: + return {"status": {"level": "unknown", "error": str(exc)}, "card": ""} + return {"status": status, "card": card} + + +def _handoff(repo: Path) -> dict[str, Any]: + session = get_last_session(agent_id="codex", repo=str(repo), requester_agent_id="dhee-ui") + if not session: + return {"available": False} + return { + "available": True, + "id": session.get("id"), + "agent_id": session.get("agent_id"), + "status": session.get("status"), + "task_summary": session.get("task_summary") or session.get("summary"), + "updated": session.get("updated") or session.get("updated_at"), + "decisions": session.get("decisions") or [], + "todos": session.get("todos") or [], + "files_touched": session.get("files_touched") or [], + } + + +def _integrations() -> list[dict[str, Any]]: + return [ + { + "name": "Claude Code", + "level": "deep", + "status": "hooks + MCP", + "detail": "Best routing surface for read, bash, grep, handoff, and learned playbooks.", + }, + { + "name": "Codex", + "level": "native", + "status": "MCP + AGENTS + session sync", + "detail": "Uses the strongest truthful Codex surfaces available without claiming pre-tool hooks.", + }, + { + "name": "Cursor / Gemini / Cline / Goose", + "level": "mcp", + "status": "dhee-mcp", + "detail": "One local MCP server exposes the same context firewall primitives.", + }, + { + "name": "Hermes", + "level": "provider", + "status": "MemoryProvider", + "detail": "Promoted learnings can flow between Hermes, Claude Code, Codex, and MCP clients.", + }, + ] + + +def build_dashboard_payload(*, repo: str | None = None) -> dict[str, Any]: + root = _repo_root(repo) + firewall = token_router_demo() + aggregate = firewall.get("aggregate") or {} + repo_context = _read_repo_context(root) + context = _context_status(root) + runtime_status = _runtime_status() + handoff = _handoff(root) + integrations = _integrations() + branch = _git_value(root, ["branch", "--show-current"], "unknown") + remote = _git_value(root, ["remote", "get-url", "origin"], "") + + runtime_running = bool((runtime_status.get("daemon") or {}).get("running")) + context_status = context.get("status") or {} + totals = { + "router_saved_pct": aggregate.get("saved_pct", 0), + "raw_tokens": aggregate.get("raw_tokens", 0), + "digest_tokens": aggregate.get("digest_tokens", 0), + "state_level": context_status.get("level") or "unknown", + "runtime": 1 if runtime_running else 0, + "repo_context": repo_context.get("count", 0), + "integrations": len(integrations), + "portable": 1, + } + + return { + "format": "dhee_public_dashboard", + "version": 1, + "workspace": { + "name": root.name or "Dhee Local Brain", + "root_path": str(root), + "branch": branch, + "remote": remote, + }, + "totals": totals, + "context_firewall": firewall, + "runtime": runtime_status, + "context_state": context, + "handoff": handoff, + "repo_context": repo_context, + "integrations": integrations, + "portability": { + "export": "dhee export --format dheemem --output backup.dheemem", + "dry_run_import": "dhee import backup.dheemem --format dheemem --strategy dry-run", + "uninstall": "dhee uninstall --yes", + }, + } + + +class DheeDashboardHandler(BaseHTTPRequestHandler): + server_version = "DheeUI/0.1" + + def _query(self) -> dict[str, list[str]]: + return parse_qs(urlparse(self.path).query) + + def _repo(self) -> str | None: + return (self._query().get("repo") or [None])[0] + + def _send_json(self, payload: dict[str, Any], status: HTTPStatus = HTTPStatus.OK) -> None: + body = json.dumps(payload, indent=2, default=str).encode("utf-8") + self.send_response(status) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Cache-Control", "no-store") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def _send_file(self, path: Path, content_type: str) -> None: + body = path.read_bytes() + self.send_response(HTTPStatus.OK) + self.send_header("Content-Type", content_type) + self.send_header("Cache-Control", "no-store") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def do_GET(self) -> None: + parsed = urlparse(self.path) + if parsed.path == "/api/dashboard": + self._send_json(build_dashboard_payload(repo=self._repo())) + return + if parsed.path == "/api/context-firewall": + self._send_json(token_router_demo()) + return + if parsed.path in {"/", "/index.html"}: + self._send_file(STATIC_DIR / "index.html", "text/html; charset=utf-8") + return + if parsed.path == "/app.js": + self._send_file(STATIC_DIR / "app.js", "application/javascript; charset=utf-8") + return + if parsed.path == "/styles.css": + self._send_file(STATIC_DIR / "styles.css", "text/css; charset=utf-8") + return + self._send_json({"error": "not found"}, HTTPStatus.NOT_FOUND) + + def do_POST(self) -> None: + parsed = urlparse(self.path) + if parsed.path == "/api/demo": + self._send_json({"seeded": True, "dashboard": build_dashboard_payload(repo=self._repo())}) + return + self._send_json({"error": "not found"}, HTTPStatus.NOT_FOUND) + + def log_message(self, format: str, *args: Any) -> None: + return + + +def serve( + *, + host: str = "127.0.0.1", + port: int = 8765, + repo: str | None = None, + open_browser: bool = False, +) -> ThreadingHTTPServer: + if host not in _LOOPBACK_HOSTS and os.environ.get("DHEE_UI_ALLOW_PUBLIC") != "1": + raise ValueError( + "Refusing to expose Dhee UI on a non-loopback host. " + "Set DHEE_UI_ALLOW_PUBLIC=1 only behind a trusted auth proxy." + ) + if repo: + os.environ["DHEE_UI_REPO"] = str(_repo_root(repo)) + httpd = ThreadingHTTPServer((host, port), DheeDashboardHandler) + url = f"http://{host}:{port}" + print(f"Dhee UI running at {url}") + if open_browser: + threading.Timer(0.4, lambda: webbrowser.open(url)).start() + httpd.serve_forever() + return httpd + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(prog="dhee-ui") + parser.add_argument("--host", default="127.0.0.1") + parser.add_argument("--port", type=int, default=8765) + parser.add_argument("--repo", default=None) + parser.add_argument("--open", action="store_true") + args = parser.parse_args(argv) + serve(host=args.host, port=args.port, repo=args.repo, open_browser=args.open) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/dhee/ui/static/app.js b/dhee/ui/static/app.js new file mode 100644 index 0000000..eac9d76 --- /dev/null +++ b/dhee/ui/static/app.js @@ -0,0 +1,270 @@ +const state = { + dashboard: null, + activeView: "overview", +}; + +const $ = (selector) => document.querySelector(selector); + +function text(value, fallback = "0") { + if (value === undefined || value === null || value === "") return fallback; + return String(value); +} + +function number(value) { + const parsed = Number(value || 0); + return Number.isFinite(parsed) ? parsed.toLocaleString() : "0"; +} + +function emptyNode() { + return document.querySelector("#empty-template").content.cloneNode(true); +} + +function setMetric(id, value) { + const node = document.getElementById(id); + if (node) node.textContent = text(value); +} + +async function requestJson(url, options = {}) { + const res = await fetch(url, { + headers: { "Content-Type": "application/json" }, + ...options, + }); + if (!res.ok) throw new Error(await res.text()); + return res.json(); +} + +async function loadDashboard() { + const dashboard = await requestJson("/api/dashboard"); + state.dashboard = dashboard; + renderDashboard(dashboard); +} + +async function reloadDemo() { + await requestJson("/api/demo", { method: "POST" }); + await loadDashboard(); +} + +function copySnapshot() { + const raw = JSON.stringify(state.dashboard || {}, null, 2); + if (navigator.clipboard) { + navigator.clipboard.writeText(raw).catch(() => {}); + } +} + +function renderDashboard(dashboard) { + const totals = dashboard.totals || {}; + const workspace = dashboard.workspace || {}; + const firewall = dashboard.context_firewall || {}; + const aggregate = firewall.aggregate || {}; + $("#workspace-title").textContent = workspace.name || "Developer Brain"; + $("#branch-pill").textContent = workspace.branch || "local"; + $("#workspace-branch-pill").textContent = workspace.branch || "branch"; + setMetric("metric-firewall", `${text(totals.router_saved_pct, "0")}%`); + setMetric("metric-raw", number(totals.raw_tokens)); + setMetric("metric-digest", number(totals.digest_tokens)); + setMetric("metric-state", totals.state_level || "unknown"); + setMetric("metric-runtime", totals.runtime ? "On" : "Off"); + setMetric("metric-repo-context", number(totals.repo_context)); + setMetric("metric-integrations", number(totals.integrations)); + setMetric("metric-portable", totals.portable ? "Yes" : "No"); + renderIntegrations(dashboard.integrations || []); + renderWhyList(aggregate); + renderWorkspace(workspace, dashboard.runtime || {}); + renderContextFirewall(firewall); + renderState(dashboard.context_state || {}, dashboard.handoff || {}); + renderRepoContext(dashboard.repo_context || {}); + renderPortability(dashboard.portability || {}); +} + +function renderIntegrations(items) { + const root = $("#integration-list"); + root.replaceChildren(); + if (!items.length) { + root.appendChild(emptyNode()); + return; + } + items.forEach((item) => { + const node = document.createElement("section"); + node.className = "org-node"; + node.innerHTML = ` +

${text(item.name)}

+
${text(item.status)} / ${text(item.level)}
+

${text(item.detail, "")}

+ `; + root.appendChild(node); + }); +} + +function renderWhyList(aggregate) { + const root = $("#why-list"); + root.replaceChildren(); + const items = [ + { + title: "Tool output stays bounded", + body: `Demo routing saves ${text(aggregate.saved_pct, "0")}% while preserving raw evidence behind pointers.`, + pill: "token control", + }, + { + title: "Current truth beats transcript replay", + body: "Dhee keeps task state, decisions, files, tests, and evidence separate from noisy chat history.", + pill: "state", + }, + { + title: "Local data remains inspectable", + body: "The UI is a view over local CLI/MCP primitives, not a hosted memory silo.", + pill: "local-first", + }, + ]; + items.forEach((item) => root.appendChild(summaryCard(item))); +} + +function renderWorkspace(workspace, runtime) { + const root = $("#workspace-card"); + root.replaceChildren(); + const daemon = runtime.daemon || {}; + [ + { title: "Root", body: text(workspace.root_path, "unknown"), pill: text(workspace.branch, "branch") }, + { title: "Remote", body: text(workspace.remote, "no origin remote"), pill: "git" }, + { title: "Runtime", body: daemon.running ? text(daemon.endpoint, "daemon running") : "daemon stopped; CLI/MCP fall back in-process", pill: daemon.running ? "running" : "stopped" }, + ].forEach((item) => root.appendChild(summaryCard(item))); +} + +function renderContextFirewall(report) { + const root = $("#firewall-list"); + root.replaceChildren(); + const aggregate = report.aggregate || {}; + $("#firewall-pill").textContent = `${text(aggregate.saved_pct, "0")}% saved`; + const cases = report.cases || []; + if (!cases.length) { + root.appendChild(emptyNode()); + return; + } + cases.forEach((item) => { + const card = document.createElement("article"); + card.className = "firewall-card"; + + const heading = document.createElement("div"); + heading.className = "panel-heading"; + heading.innerHTML = ` +

${text(item.name)}

+ ${text(item.surface)} + `; + card.appendChild(heading); + + const decision = document.createElement("div"); + decision.className = "firewall-decision"; + decision.textContent = text(item.decision, ""); + card.appendChild(decision); + + const stats = document.createElement("div"); + stats.className = "firewall-stats"; + stats.innerHTML = ` + ${number(item.raw_tokens)} raw + ${number(item.digest_tokens)} digest + ${text(item.saved_pct)}% saved + ${text(item.expand)} + `; + card.appendChild(stats); + + const pre = document.createElement("pre"); + pre.className = "digest-preview"; + pre.textContent = text(item.digest, ""); + card.appendChild(pre); + + root.appendChild(card); + }); +} + +function renderState(contextState, handoff) { + const root = $("#state-list"); + root.replaceChildren(); + const status = contextState.status || {}; + $("#state-pill").textContent = text(status.level, "unknown"); + const items = [ + { + title: "Compiled State", + body: `epoch ${text(status.task_epoch, "1")} / revision ${text(status.state_revision, "0")} / ${text(status.state_card_tokens, "0")} card tokens`, + pill: text(status.level, "unknown"), + }, + { + title: "Expansion Health", + body: `expansion rate ${text(status.expansion_rate, "0")} / projected cache read ${text(status.projected_cache_read_tokens, "0")} tokens`, + pill: text(status.expansion_level, "unknown"), + }, + { + title: "Latest Handoff", + body: handoff.available ? text(handoff.task_summary, "handoff available") : "No handoff saved yet.", + pill: handoff.available ? text(handoff.status, "active") : "empty", + }, + ]; + items.forEach((item) => root.appendChild(summaryCard(item))); + + if (contextState.card) { + const pre = document.createElement("pre"); + pre.className = "digest-preview"; + pre.textContent = contextState.card; + root.appendChild(pre); + } +} + +function renderRepoContext(repoContext) { + const root = $("#repo-context-list"); + root.replaceChildren(); + const entries = repoContext.entries || []; + $("#repo-context-pill").textContent = `${text(repoContext.count, "0")} entries`; + if (!entries.length) { + root.appendChild(summaryCard({ + title: "No repo context yet", + body: "Run `dhee link /path/to/repo` and promote decisions or conventions into .dhee/context.", + pill: repoContext.exists ? "empty" : "not linked", + })); + return; + } + entries.forEach((item) => { + root.appendChild(summaryCard({ + title: text(item.title || item.summary || item.id, "repo context"), + body: text(item.content || item.body || item.summary || item.reason, ""), + pill: text(item.kind || item.type || "note"), + })); + }); +} + +function renderPortability(portability) { + const root = $("#portability-list"); + root.replaceChildren(); + [ + { title: "Export", body: text(portability.export), pill: ".dheemem" }, + { title: "Dry-run Import", body: text(portability.dry_run_import), pill: "inspect first" }, + { title: "Clean Uninstall", body: text(portability.uninstall), pill: "no lock-in" }, + ].forEach((item) => root.appendChild(summaryCard(item))); +} + +function summaryCard(item) { + const node = document.createElement("article"); + node.className = "context-item"; + node.innerHTML = ` +
+

${text(item.title)}

+ ${text(item.pill, "")} +
+
${text(item.body, "")}
+ `; + return node; +} + +document.querySelectorAll(".tab").forEach((tab) => { + tab.addEventListener("click", () => { + const view = tab.dataset.view; + document.querySelectorAll(".tab").forEach((node) => node.classList.toggle("active", node === tab)); + document.querySelectorAll(".view").forEach((node) => node.classList.toggle("active", node.id === `view-${view}`)); + state.activeView = view; + }); +}); + +$("#refresh-button").addEventListener("click", loadDashboard); +$("#seed-button").addEventListener("click", reloadDemo); +$("#snapshot-button").addEventListener("click", copySnapshot); + +loadDashboard().catch((error) => { + document.body.innerHTML = `
Dashboard error${error.message}
`; +}); diff --git a/dhee/ui/static/index.html b/dhee/ui/static/index.html new file mode 100644 index 0000000..4aa3ad2 --- /dev/null +++ b/dhee/ui/static/index.html @@ -0,0 +1,151 @@ + + + + + + Dhee UI + + + +
+
+
+

Local Dhee

+

Developer Brain

+
+ +
+ +
+
+ Router Savings + 0% +
+
+ Raw Tokens + 0 +
+
+ Digest Tokens + 0 +
+
+ State Level + - +
+
+ Runtime + Off +
+
+ Repo Context + 0 +
+
+ Integrations + 0 +
+
+ Portable + Yes +
+
+ +
+ + +
+
+ + + + + +
+ +
+
+
+
+

Why Dhee Is Running

+ local-first +
+
+
+
+
+

Workspace

+ branch +
+
+
+
+
+ +
+
+

Context Firewall

+ 0 saved +
+
+ Agents see compact truth first. Raw evidence stays behind explicit expansion pointers. +
+
+
+ +
+
+

Current State

+ unknown +
+
+
+ +
+
+

Repo Context

+ 0 entries +
+
+
+ +
+
+

Portability

+ no lock-in +
+
+
+
+
+
+ + + + + + diff --git a/dhee/ui/static/styles.css b/dhee/ui/static/styles.css new file mode 100644 index 0000000..e7da6f1 --- /dev/null +++ b/dhee/ui/static/styles.css @@ -0,0 +1,591 @@ +:root { + --bg: #f4f7f4; + --ink: #17231e; + --muted: #64726c; + --panel: #ffffff; + --line: #d9e0dc; + --soft: #edf2ef; + --green: #2f7d58; + --blue: #2d6997; + --amber: #a46b1f; + --red: #a94438; + --violet: #7656a6; + --shadow: 0 18px 60px rgba(28, 42, 36, 0.10); +} + +* { + box-sizing: border-box; +} + +html, +body { + margin: 0; + min-height: 100%; +} + +body { + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.74), rgba(244, 247, 244, 0.92)), + repeating-linear-gradient(90deg, rgba(23, 35, 30, 0.035) 0, rgba(23, 35, 30, 0.035) 1px, transparent 1px, transparent 38px); + color: var(--ink); + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + letter-spacing: 0; +} + +button { + font: inherit; +} + +.app-shell { + width: min(1500px, 100%); + margin: 0 auto; + padding: 28px; +} + +.topbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + margin-bottom: 22px; +} + +.eyebrow { + margin: 0 0 4px; + color: var(--muted); + font-size: 12px; + font-weight: 700; + text-transform: uppercase; +} + +h1, +h2, +h3, +p { + margin: 0; +} + +h1 { + font-size: clamp(30px, 4vw, 48px); + line-height: 1; +} + +h2 { + font-size: 16px; +} + +h3 { + font-size: 14px; +} + +.toolbar, +.panel-heading, +.row-actions { + display: flex; + align-items: center; + gap: 10px; +} + +.toolbar { + flex-wrap: wrap; + justify-content: flex-end; +} + +.icon-button { + min-height: 38px; + display: inline-flex; + align-items: center; + gap: 9px; + border: 1px solid var(--line); + border-radius: 8px; + background: var(--panel); + color: var(--ink); + padding: 8px 12px; + cursor: pointer; + box-shadow: 0 4px 16px rgba(28, 42, 36, 0.06); +} + +.icon-button:hover { + border-color: #b7c4bd; +} + +.icon-button.primary { + background: var(--ink); + color: #ffffff; + border-color: var(--ink); +} + +.icon { + width: 15px; + height: 15px; + position: relative; + display: inline-block; + flex: 0 0 auto; +} + +.refresh-icon { + border: 2px solid currentColor; + border-right-color: transparent; + border-radius: 50%; +} + +.refresh-icon::after { + content: ""; + position: absolute; + right: -2px; + top: 1px; + width: 5px; + height: 5px; + border-top: 2px solid currentColor; + border-right: 2px solid currentColor; + transform: rotate(25deg); +} + +.plus-icon::before, +.plus-icon::after { + content: ""; + position: absolute; + background: currentColor; + border-radius: 2px; +} + +.plus-icon::before { + width: 15px; + height: 2px; + top: 6px; + left: 0; +} + +.plus-icon::after { + width: 2px; + height: 15px; + top: 0; + left: 6px; +} + +.repo-icon { + border: 2px solid currentColor; + border-radius: 3px; +} + +.repo-icon::before { + content: ""; + position: absolute; + width: 5px; + height: 2px; + left: 3px; + top: -4px; + background: currentColor; + border-radius: 2px 2px 0 0; +} + +.sync-icon { + border: 2px solid currentColor; + border-left-color: transparent; + border-radius: 50%; +} + +.sync-icon::before, +.sync-icon::after { + content: ""; + position: absolute; + width: 5px; + height: 5px; + border-top: 2px solid currentColor; + border-right: 2px solid currentColor; +} + +.sync-icon::before { + right: -3px; + top: 0; + transform: rotate(40deg); +} + +.sync-icon::after { + left: -3px; + bottom: 0; + transform: rotate(220deg); +} + +.metric-grid { + display: grid; + grid-template-columns: repeat(8, minmax(0, 1fr)); + gap: 12px; + margin-bottom: 16px; +} + +.metric-tile, +.side-panel, +.main-panel, +.panel-block { + background: rgba(255, 255, 255, 0.92); + border: 1px solid var(--line); + border-radius: 8px; + box-shadow: var(--shadow); +} + +.metric-tile { + min-height: 90px; + padding: 16px; + display: flex; + flex-direction: column; + justify-content: space-between; + border-top: 4px solid var(--green); +} + +.metric-tile:nth-child(2) { + border-top-color: var(--blue); +} + +.metric-tile:nth-child(3) { + border-top-color: var(--violet); +} + +.metric-tile:nth-child(4) { + border-top-color: var(--amber); +} + +.metric-tile:nth-child(5), +.metric-tile:nth-child(7) { + border-top-color: #527568; +} + +.metric-tile.attention { + border-top-color: var(--red); +} + +.metric-tile.firewall { + border-top-color: var(--violet); +} + +.metric-label { + color: var(--muted); + font-size: 12px; + font-weight: 700; + text-transform: uppercase; +} + +.metric-tile strong { + font-size: 32px; + line-height: 1; +} + +.workbench { + display: grid; + grid-template-columns: 360px minmax(0, 1fr); + gap: 16px; + align-items: start; +} + +.side-panel, +.main-panel, +.panel-block { + padding: 16px; +} + +.side-panel { + position: sticky; + top: 18px; + max-height: calc(100vh - 36px); + overflow: auto; +} + +.panel-heading { + justify-content: space-between; + margin-bottom: 14px; +} + +.pill, +.status { + display: inline-flex; + align-items: center; + min-height: 24px; + border-radius: 999px; + padding: 3px 9px; + background: var(--soft); + color: var(--muted); + font-size: 12px; + font-weight: 700; + white-space: nowrap; +} + +.org-chart { + display: grid; + gap: 12px; +} + +.org-node { + border-left: 3px solid var(--green); + padding: 10px 0 2px 12px; +} + +.org-node.project { + border-left-color: var(--blue); +} + +.org-node.global { + border-left-color: var(--violet); +} + +.org-node .meta { + margin-top: 5px; + color: var(--muted); + font-size: 12px; +} + +.small-copy { + margin-top: 8px; + color: var(--muted); + font-size: 12px; + line-height: 1.45; +} + +.team-stack { + display: grid; + gap: 8px; + margin-top: 10px; +} + +.team-chip { + border: 1px solid var(--line); + border-radius: 8px; + padding: 9px; + background: #fbfcfb; +} + +.team-chip .meta { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.tabs { + display: flex; + gap: 8px; + margin-bottom: 16px; + border-bottom: 1px solid var(--line); + padding-bottom: 8px; + overflow-x: auto; +} + +.tab { + border: 0; + border-radius: 8px; + background: transparent; + color: var(--muted); + padding: 8px 12px; + cursor: pointer; +} + +.tab.active { + background: var(--ink); + color: #ffffff; +} + +.view { + display: none; +} + +.view.active { + display: block; +} + +.split-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: 14px; +} + +.bars, +.repo-list, +.context-list, +.finding-list, +.firewall-list, +.table { + display: grid; + gap: 10px; +} + +.bar-row { + display: grid; + grid-template-columns: 110px minmax(0, 1fr) 36px; + gap: 10px; + align-items: center; + font-size: 13px; +} + +.bar-track { + height: 10px; + overflow: hidden; + border-radius: 999px; + background: var(--soft); +} + +.bar-fill { + height: 100%; + border-radius: inherit; + background: linear-gradient(90deg, var(--green), var(--blue)); +} + +.repo-item, +.context-item, +.finding-item, +.firewall-card, +.team-row { + border: 1px solid var(--line); + border-radius: 8px; + background: #fbfcfb; + padding: 12px; +} + +.repo-path, +.context-summary, +.finding-detail, +.firewall-intro, +.firewall-decision { + color: var(--muted); + font-size: 13px; + line-height: 1.45; + margin-top: 5px; + overflow-wrap: anywhere; +} + +.firewall-intro { + margin: -4px 0 14px; +} + +.firewall-stats { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 8px; + margin-top: 10px; +} + +.firewall-stats span { + min-height: 40px; + border: 1px solid var(--line); + border-radius: 8px; + background: #ffffff; + color: var(--muted); + padding: 8px; + font-size: 12px; + overflow-wrap: anywhere; +} + +.firewall-stats strong { + display: block; + color: var(--ink); + font-size: 18px; +} + +.digest-preview { + max-height: 260px; + overflow: auto; + margin: 12px 0 0; + border: 1px solid var(--line); + border-radius: 8px; + background: #17231e; + color: #edf2ef; + padding: 12px; + font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + white-space: pre-wrap; +} + +.team-row { + display: grid; + grid-template-columns: minmax(180px, 1.1fr) minmax(180px, 1fr) repeat(3, minmax(80px, 0.35fr)) minmax(120px, 0.5fr); + gap: 12px; + align-items: center; +} + +.team-title { + display: grid; + gap: 4px; +} + +.muted { + color: var(--muted); + font-size: 12px; +} + +.status.healthy { + background: #e5f4ea; + color: var(--green); +} + +.status.watch { + background: #fff3d8; + color: var(--amber); +} + +.status.needs_work { + background: #fbe7e2; + color: var(--red); +} + +.finding-item.high { + border-left: 4px solid var(--red); +} + +.finding-item.medium { + border-left: 4px solid var(--amber); +} + +.finding-item.low { + border-left: 4px solid var(--blue); +} + +.empty-state { + display: grid; + place-items: center; + gap: 8px; + min-height: 180px; + color: var(--muted); + text-align: center; + border: 1px dashed var(--line); + border-radius: 8px; + background: rgba(255, 255, 255, 0.55); +} + +.empty-state strong { + color: var(--ink); +} + +@media (max-width: 1150px) { + .metric-grid { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } + + .workbench, + .split-grid { + grid-template-columns: 1fr; + } + + .side-panel { + position: static; + max-height: none; + } +} + +@media (max-width: 720px) { + .app-shell { + padding: 16px; + } + + .topbar { + align-items: flex-start; + flex-direction: column; + } + + .metric-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .team-row { + grid-template-columns: 1fr; + } + + .firewall-stats { + grid-template-columns: 1fr; + } +} diff --git a/pyproject.toml b/pyproject.toml index 469b35b..05b86f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,6 +82,7 @@ dev = [ [project.scripts] dhee = "dhee.cli:main" +dhee-ui = "dhee.ui.server:main" dhee-mcp = "dhee.mcp_slim:run" dhee-mcp-full = "dhee.mcp_server:run" dhee-debugger-api = "dhee.debugger_api:run" @@ -101,7 +102,6 @@ include-package-data = false where = [".", "engram-bus"] include = ["dhee*", "engram*", "engram_bus*"] exclude = [ - "dhee.ui*", "engram-bus*", "engram-bridge*", "engram-enterprise*", @@ -111,3 +111,6 @@ exclude = [ "engram-metamemory*", "engram-policy*", ] + +[tool.setuptools.package-data] +"dhee.ui" = ["static/*"] diff --git a/tests/test_packaging.py b/tests/test_packaging.py index 76865b0..e96b68e 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -15,6 +15,9 @@ def test_handoff_bus_is_bundled_not_external_dependency(): assert 'where = [".", "engram-bus"]' in pyproject assert 'include = ["dhee*", "engram*", "engram_bus*"]' in pyproject + assert '"dhee.ui*"' not in pyproject + assert 'dhee-ui = "dhee.ui.server:main"' in pyproject + assert '"dhee.ui" = ["static/*"]' in pyproject assert (ROOT / "engram-bus" / "engram_bus" / "__init__.py").exists() diff --git a/tests/test_ui.py b/tests/test_ui.py new file mode 100644 index 0000000..d4bf5e8 --- /dev/null +++ b/tests/test_ui.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +import json + +from dhee.ui.server import STATIC_DIR, build_dashboard_payload + + +def test_public_ui_payload_uses_local_dhee_primitives(tmp_path, monkeypatch): + monkeypatch.setenv("DHEE_DATA_DIR", str(tmp_path / "dhee-data")) + monkeypatch.setenv("ENGRAM_HANDOFF_DB", str(tmp_path / "handoff.db")) + repo = tmp_path / "repo" + context_dir = repo / ".dhee" / "context" + context_dir.mkdir(parents=True) + (context_dir / "entries.jsonl").write_text( + json.dumps({"id": "ctx-1", "kind": "decision", "title": "Use compact digests", "content": "Keep raw logs behind ptrs."}) + "\n", + encoding="utf-8", + ) + + out = build_dashboard_payload(repo=str(repo)) + + assert out["format"] == "dhee_public_dashboard" + assert out["workspace"]["root_path"] == str(repo.resolve()) + assert out["context_firewall"]["aggregate"]["saved_pct"] > 50 + assert out["totals"]["repo_context"] == 1 + assert any(item["name"] == "Codex" for item in out["integrations"]) + assert out["portability"]["export"].startswith("dhee export") + + +def test_public_ui_static_assets_are_packaged(): + assert (STATIC_DIR / "index.html").exists() + assert (STATIC_DIR / "styles.css").exists() + assert (STATIC_DIR / "app.js").exists() + assert "Local Dhee" in (STATIC_DIR / "index.html").read_text(encoding="utf-8") + assert "Context Firewall" in (STATIC_DIR / "index.html").read_text(encoding="utf-8") + assert "renderContextFirewall" in (STATIC_DIR / "app.js").read_text(encoding="utf-8") From d78b3f00138a900fea7fabeb68ebbc1b7c5bd443 Mon Sep 17 00:00:00 2001 From: Ashish-dwi99 Date: Wed, 13 May 2026 17:25:32 +0530 Subject: [PATCH 2/5] Copy team dashboard UI into OSS --- CHANGELOG.md | 7 +- README.md | 18 +- dhee/cli.py | 2 + dhee/ui/server.py | 446 +++++++++++++++++++++++++------------- dhee/ui/static/app.js | 363 +++++++++++++++++++------------ dhee/ui/static/index.html | 129 ++++++----- dhee/ui/static/styles.css | 7 - tests/test_ui.py | 21 +- 8 files changed, 628 insertions(+), 365 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af1d4ed..fe37a08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,10 +11,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this - Rewrote the README as a concise first-read product page focused on why Dhee matters, the UI demo, install, integrations, benchmarks, and the public-core/paid-team-layer boundary. -- Restored the public `dhee ui` dashboard and `dhee-ui` entrypoint. The OSS UI - uses the same dashboard design language while rendering local developer-brain - surfaces: context firewall, current state, runtime, integrations, repo - context, and portability. +- Restored the public `dhee ui` dashboard and `dhee-ui` entrypoint by copying + the same dashboard UI and API shape into OSS. The public backend maps those + screens onto local repo/Dhee data instead of requiring paid-team services. - 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. diff --git a/README.md b/README.md index e21fe19..6f9e45b 100644 --- a/README.md +++ b/README.md @@ -68,15 +68,17 @@ dhee ui --open The first screen shows: +- the same dashboard screens used by the team layer: Org Chart, Teams, Repo + Brain, Context Firewall, Context, and Findings +- an OSS backend that maps those screens onto your local repo, Dhee context, + router proof, and code index summary - router savings from noisy pytest logs, git diffs, and long source reads -- current state health and latest handoff -- local runtime status -- repo-shared context entries -- integration surfaces for Claude Code, Codex, MCP clients, and Hermes -- export/import/uninstall commands so the local data stays portable - -The UI is a view over the same public CLI/MCP primitives. The raw evidence still -stays behind `dhee_expand_result(ptr="...")`; the dashboard makes that visible. +- repo-shared context entries and local findings +- Connect Real / Sync Repos flows that operate against your current workspace + +The OSS UI copies the same frontend and API shape used by the team dashboard. +The raw evidence still stays behind `dhee_expand_result(ptr="...")`; the +dashboard makes that visible. --- diff --git a/dhee/cli.py b/dhee/cli.py index b844be3..b15f4cc 100644 --- a/dhee/cli.py +++ b/dhee/cli.py @@ -1248,6 +1248,7 @@ def cmd_ui(args: argparse.Namespace) -> None: serve( host=getattr(args, "host", "127.0.0.1"), port=int(getattr(args, "port", 8765) or 8765), + org_id=getattr(args, "org", None), repo=getattr(args, "repo", None), open_browser=bool(getattr(args, "open", False)), ) @@ -2548,6 +2549,7 @@ def build_parser() -> argparse.ArgumentParser: p_ui = sub.add_parser("ui", help="Run the local Dhee dashboard") p_ui.add_argument("--host", default="127.0.0.1", help="Bind host (loopback by default)") p_ui.add_argument("--port", type=int, default=8765, help="Bind port") + p_ui.add_argument("--org", help="Dashboard org/workspace id") p_ui.add_argument("--repo", help="Repo/workspace to inspect (default: cwd)") p_ui.add_argument("--open", action="store_true", help="Open in the default browser") diff --git a/dhee/ui/server.py b/dhee/ui/server.py index d79187d..a16b611 100644 --- a/dhee/ui/server.py +++ b/dhee/ui/server.py @@ -1,9 +1,4 @@ -"""Local-first Dhee dashboard. - -The public UI intentionally uses the same small HTTP/static-file shape as the -team dashboard, but its data model is local developer context: router savings, -runtime health, handoff state, repo context, integrations, and portability. -""" +"""Public Dhee dashboard using the same UI/API shape as the team dashboard.""" from __future__ import annotations @@ -19,8 +14,6 @@ from typing import Any from urllib.parse import parse_qs, urlparse -from dhee.context_state import ContextStateStore -from dhee.core.kernel import get_last_session from dhee.demo import token_router_demo @@ -28,8 +21,12 @@ _LOOPBACK_HOSTS = {"127.0.0.1", "localhost", "::1"} -def _repo_root(repo: str | None = None) -> Path: - return Path(repo or os.environ.get("DHEE_UI_REPO") or Path.cwd()).expanduser().resolve() +def _org_from_env(default: str = "local") -> str: + return os.environ.get("DHEE_UI_ORG_ID", default) + + +def _repo_root(root_path: str | None = None) -> Path: + return Path(root_path or os.environ.get("DHEE_UI_ROOT") or Path.cwd()).expanduser().resolve() def _git_value(repo: Path, args: list[str], default: str = "") -> str: @@ -40,146 +37,268 @@ def _git_value(repo: Path, args: list[str], default: str = "") -> str: return default -def _read_repo_context(repo: Path, *, limit: int = 30) -> dict[str, Any]: - path = repo / ".dhee" / "context" / "entries.jsonl" - entries: list[dict[str, Any]] = [] - if path.exists(): - try: - for line in path.read_text(encoding="utf-8").splitlines(): - if not line.strip(): - continue - try: - item = json.loads(line) - except json.JSONDecodeError: - continue - entries.append(item) - except OSError: - pass - - kind_counts: dict[str, int] = {} - for item in entries: - kind = str(item.get("kind") or item.get("type") or "note") - kind_counts[kind] = kind_counts.get(kind, 0) + 1 - return { - "path": str(path), - "exists": path.exists(), - "count": len(entries), - "kind_counts": kind_counts, - "entries": entries[-limit:], - } - - -def _runtime_status() -> dict[str, Any]: - try: - from dhee import runtime - - return runtime.status(timeout=0.25) - except Exception as exc: - return {"status": "unavailable", "error": str(exc)} +def _team_health_from_findings(findings: list[dict[str, Any]]) -> str: + if any(f.get("severity") == "high" for f in findings): + return "needs_work" + if findings: + return "watch" + return "healthy" -def _context_status(repo: Path) -> dict[str, Any]: - store = ContextStateStore(repo=str(repo), workspace_id=str(repo), user_id="default", agent_id="dhee-ui") +def _repo_context_entries(root: Path, *, limit: int = 100) -> list[dict[str, Any]]: + path = root / ".dhee" / "context" / "entries.jsonl" + entries: list[dict[str, Any]] = [] + if not path.exists(): + return entries try: - status = store.status() - card = store.render_state_card() - except Exception as exc: - return {"status": {"level": "unknown", "error": str(exc)}, "card": ""} - return {"status": status, "card": card} + for line in path.read_text(encoding="utf-8").splitlines(): + if not line.strip(): + continue + try: + entries.append(json.loads(line)) + except json.JSONDecodeError: + continue + except OSError: + return [] + return entries[-limit:] + + +def _indexed_files(root: Path, *, limit: int = 600) -> tuple[int, int]: + ignored = {".git", ".hg", ".svn", ".venv", "node_modules", "__pycache__", ".mypy_cache", ".pytest_cache"} + total_files = 0 + total_bytes = 0 + for base, dirs, files in os.walk(root): + dirs[:] = [d for d in dirs if d not in ignored and not d.startswith(".tox")] + for name in files: + if name.endswith((".pyc", ".pyo", ".png", ".jpg", ".jpeg", ".gif", ".sqlite", ".db")): + continue + path = Path(base) / name + try: + size = path.stat().st_size + except OSError: + continue + total_files += 1 + total_bytes += size + if total_files >= limit: + return total_files, total_bytes + return total_files, total_bytes + + +def _context_index(root: Path) -> list[dict[str, Any]]: + entries = _repo_context_entries(root) + if entries: + out = [] + for item in entries: + out.append( + { + "context_id": item.get("id") or item.get("context_id"), + "title": item.get("title") or item.get("summary") or "Repo context", + "summary": item.get("summary") or item.get("content") or item.get("body") or item.get("reason") or "", + "scope": item.get("scope") or "repo", + "kind": item.get("kind") or item.get("type") or "note", + "project_id": item.get("project_id") or "local", + "team_id": item.get("team_id") or "local-dev", + "shares": item.get("shares") or [], + } + ) + return out + return [ + { + "context_id": "oss-policy", + "title": "Context firewall baseline", + "summary": "Agents should see compact truth first and expand raw evidence only when needed.", + "scope": "company", + "kind": "policy", + "project_id": "developer-brain", + "team_id": "local-dev", + "shares": [], + }, + { + "context_id": "oss-runbook", + "title": "Local handoff runbook", + "summary": "Use `dhee handoff` before switching agents or resuming long-running work.", + "scope": "team", + "kind": "runbook", + "project_id": "developer-brain", + "team_id": "local-dev", + "shares": [], + }, + ] -def _handoff(repo: Path) -> dict[str, Any]: - session = get_last_session(agent_id="codex", repo=str(repo), requester_agent_id="dhee-ui") - if not session: - return {"available": False} +def _code_brain_summary(root: Path, *, org_id: str, repo_mappings: list[dict[str, Any]]) -> dict[str, Any]: + indexed_files, indexed_bytes = _indexed_files(root) + mapping_status = [] + for mapping in repo_mappings: + mapping_status.append( + { + "mapping_id": mapping.get("mapping_id"), + "team_id": mapping.get("team_id"), + "project_id": mapping.get("project_id"), + "local_path": str(root), + "repo_url": mapping.get("repo_url"), + "indexed_files": indexed_files, + "indexed_bytes": indexed_bytes, + "updated_at": None, + "last_sync": {"files_warmed": indexed_files, "mode": "oss-local"}, + "sync_status": "indexed" if indexed_files else "not_indexed", + } + ) return { - "available": True, - "id": session.get("id"), - "agent_id": session.get("agent_id"), - "status": session.get("status"), - "task_summary": session.get("task_summary") or session.get("summary"), - "updated": session.get("updated") or session.get("updated_at"), - "decisions": session.get("decisions") or [], - "todos": session.get("todos") or [], - "files_touched": session.get("files_touched") or [], + "indexed_files": indexed_files, + "indexed_bytes": indexed_bytes, + "telemetry": {"events": 0, "source": "oss-local"}, + "repo_paths": [{"repo_path": str(root), "indexed_files": indexed_files, "indexed_bytes": indexed_bytes}], + "mapping_status": mapping_status, } -def _integrations() -> list[dict[str, Any]]: - return [ - { - "name": "Claude Code", - "level": "deep", - "status": "hooks + MCP", - "detail": "Best routing surface for read, bash, grep, handoff, and learned playbooks.", - }, +def build_dashboard_payload(*, org_id: str | None = None, root_path: str | None = None, repo: str | None = None) -> dict[str, Any]: + root = _repo_root(root_path or repo) + org = org_id or _org_from_env() + branch = _git_value(root, ["branch", "--show-current"], "main") + remote = _git_value(root, ["remote", "get-url", "origin"], str(root)) + context_index = _context_index(root) + findings = [ { - "name": "Codex", - "level": "native", - "status": "MCP + AGENTS + session sync", - "detail": "Uses the strongest truthful Codex surfaces available without claiming pre-tool hooks.", - }, + "finding_id": "oss-router-proof", + "team_id": "local-dev", + "manager_id": "dhee-context-manager", + "title": "Router demo available", + "detail": "Use the Context Firewall tab to inspect digest-first routing and expansion pointers.", + "severity": "low", + "finding_type": "proof", + } + ] + repo_mappings = [ { - "name": "Cursor / Gemini / Cline / Goose", - "level": "mcp", - "status": "dhee-mcp", - "detail": "One local MCP server exposes the same context firewall primitives.", - }, + "mapping_id": "local-repo", + "team_id": "local-dev", + "project_id": "developer-brain", + "repo_url": remote, + "local_path": str(root), + "branch": branch, + "provider": "git", + "metadata": {"last_sync": {"mode": "oss-local"}}, + } + ] + code_brain = _code_brain_summary(root, org_id=org, repo_mappings=repo_mappings) + kind_counts: dict[str, int] = {} + scope_counts: dict[str, int] = {} + for item in context_index: + kind = str(item.get("kind") or "note") + scope = str(item.get("scope") or "unknown") + kind_counts[kind] = kind_counts.get(kind, 0) + 1 + scope_counts[scope] = scope_counts.get(scope, 0) + 1 + + managers_by_team = { + "local-dev": { + "manager_id": "dhee-context-manager", + "display_name": "Dhee Context Manager", + "team_id": "local-dev", + } + } + team_rows = [ { - "name": "Hermes", - "level": "provider", - "status": "MemoryProvider", - "detail": "Promoted learnings can flow between Hermes, Claude Code, Codex, and MCP clients.", - }, + "team_id": "local-dev", + "name": "Local Developer", + "team_type": "project", + "project_id": "developer-brain", + "manager": managers_by_team["local-dev"], + "repo_count": len(repo_mappings), + "context_count": len(context_index), + "open_findings": len(findings), + "health": _team_health_from_findings(findings), + } ] - - -def build_dashboard_payload(*, repo: str | None = None) -> dict[str, Any]: - root = _repo_root(repo) - firewall = token_router_demo() - aggregate = firewall.get("aggregate") or {} - repo_context = _read_repo_context(root) - context = _context_status(root) - runtime_status = _runtime_status() - handoff = _handoff(root) - integrations = _integrations() - branch = _git_value(root, ["branch", "--show-current"], "unknown") - remote = _git_value(root, ["remote", "get-url", "origin"], "") - - runtime_running = bool((runtime_status.get("daemon") or {}).get("running")) - context_status = context.get("status") or {} + org_chart = { + "workspace": {"name": root.name or "Dhee", "root_path": str(root), "default_branch": branch}, + "global_teams": [ + { + "team_id": "global-context", + "name": "Context Governance", + "context_manager": {"manager_id": "dhee-context-manager"}, + "repo_mappings": [], + "open_findings": [], + } + ], + "projects": [ + { + "project_id": "developer-brain", + "name": "Developer Brain", + "teams": [ + { + "team_id": "local-dev", + "name": "Local Developer", + "context_manager": {"manager_id": "dhee-context-manager"}, + "repo_mappings": repo_mappings, + "open_findings": findings, + } + ], + "repo_mappings": repo_mappings, + } + ], + } totals = { - "router_saved_pct": aggregate.get("saved_pct", 0), - "raw_tokens": aggregate.get("raw_tokens", 0), - "digest_tokens": aggregate.get("digest_tokens", 0), - "state_level": context_status.get("level") or "unknown", - "runtime": 1 if runtime_running else 0, - "repo_context": repo_context.get("count", 0), - "integrations": len(integrations), - "portable": 1, + "projects": 1, + "teams": 2, + "global_teams": 1, + "repo_mappings": len(repo_mappings), + "context_items": len(context_index), + "context_managers": 1, + "open_findings": len(findings), + "shares": 0, + "indexed_files": code_brain["indexed_files"], + } + context_firewall = token_router_demo() + raw = { + "org_id": org, + "workspace": org_chart["workspace"], + "projects": org_chart["projects"], + "global_teams": org_chart["global_teams"], + "repo_mappings": repo_mappings, + "context_index": context_index, + "context_managers": list(managers_by_team.values()), + "context_manager_findings": findings, + "context_manager_findings_by_team": {"local-dev": findings}, + "context_managers_by_team": managers_by_team, + "repo_mappings_by_team": {"local-dev": repo_mappings}, + "team_context": {"local-dev": context_index}, + "context_shares": [], + "org_chart": org_chart, } - return { - "format": "dhee_public_dashboard", - "version": 1, - "workspace": { - "name": root.name or "Dhee Local Brain", - "root_path": str(root), - "branch": branch, - "remote": remote, - }, + "org_id": org, + "workspace": org_chart["workspace"], "totals": totals, - "context_firewall": firewall, - "runtime": runtime_status, - "context_state": context, - "handoff": handoff, - "repo_context": repo_context, - "integrations": integrations, - "portability": { - "export": "dhee export --format dheemem --output backup.dheemem", - "dry_run_import": "dhee import backup.dheemem --format dheemem --strategy dry-run", - "uninstall": "dhee uninstall --yes", + "commercial": { + "license": {"edition": "public", "status": "active"}, + "billing": {"plan": "public", "usage": 0}, }, + "org_chart": org_chart, + "team_rows": team_rows, + "kind_counts": kind_counts, + "scope_counts": scope_counts, + "repo_mappings": repo_mappings, + "code_brain": code_brain, + "context_firewall": context_firewall, + "context_index": context_index[:100], + "findings": findings, + "raw": raw, + } + + +def seed_demo_workspace(*, org_id: str | None = None) -> dict[str, Any]: + return {"seeded": True, "dashboard": build_dashboard_payload(org_id=org_id)} + + +def connect_real_workspace(*, org_id: str | None = None, root_path: str | None = None, limit: int | None = None) -> dict[str, Any]: + root = _repo_root(root_path) + return { + "connected": True, + "root_path": str(root), + "real": {"limit": limit, "mode": "oss-local"}, + "dashboard": build_dashboard_payload(org_id=org_id, root_path=str(root)), } @@ -189,8 +308,12 @@ class DheeDashboardHandler(BaseHTTPRequestHandler): def _query(self) -> dict[str, list[str]]: return parse_qs(urlparse(self.path).query) - def _repo(self) -> str | None: - return (self._query().get("repo") or [None])[0] + def _org(self) -> str: + query = self._query() + return (query.get("org") or [_org_from_env()])[0] + + def _root_path(self) -> str | None: + return (self._query().get("root") or [None])[0] def _send_json(self, payload: dict[str, Any], status: HTTPStatus = HTTPStatus.OK) -> None: body = json.dumps(payload, indent=2, default=str).encode("utf-8") @@ -213,11 +336,25 @@ def _send_file(self, path: Path, content_type: str) -> None: def do_GET(self) -> None: parsed = urlparse(self.path) if parsed.path == "/api/dashboard": - self._send_json(build_dashboard_payload(repo=self._repo())) + self._send_json(build_dashboard_payload(org_id=self._org(), root_path=self._root_path())) return if parsed.path == "/api/context-firewall": self._send_json(token_router_demo()) return + if parsed.path == "/api/team": + team = (self._query().get("team") or [""])[0] + if not team: + self._send_json({"error": "team is required"}, HTTPStatus.BAD_REQUEST) + return + dashboard = build_dashboard_payload(org_id=self._org(), root_path=self._root_path()) + self._send_json( + { + "team_id": team, + "context": [item for item in dashboard["context_index"] if item.get("team_id") in {team, "local-dev"}], + "findings": [item for item in dashboard["findings"] if item.get("team_id") in {team, "local-dev"}], + } + ) + return if parsed.path in {"/", "/index.html"}: self._send_file(STATIC_DIR / "index.html", "text/html; charset=utf-8") return @@ -232,7 +369,29 @@ def do_GET(self) -> None: def do_POST(self) -> None: parsed = urlparse(self.path) if parsed.path == "/api/demo": - self._send_json({"seeded": True, "dashboard": build_dashboard_payload(repo=self._repo())}) + self._send_json(seed_demo_workspace(org_id=self._org())) + return + if parsed.path == "/api/real": + query = self._query() + limit_raw = (query.get("limit") or [""])[0] + limit = int(limit_raw) if limit_raw.strip().isdigit() else None + root_path = (query.get("root") or [None])[0] + self._send_json(connect_real_workspace(org_id=self._org(), root_path=root_path, limit=limit)) + return + if parsed.path == "/api/sync": + self._send_json({"sync": {"mode": "oss-local", "ok": True}, "dashboard": build_dashboard_payload(org_id=self._org())}) + return + if parsed.path == "/api/review": + team = (self._query().get("team") or [""])[0] + if not team: + self._send_json({"error": "team is required"}, HTTPStatus.BAD_REQUEST) + return + self._send_json( + { + "review": {"team_id": team, "mode": "oss-local", "ok": True}, + "dashboard": build_dashboard_payload(org_id=self._org()), + } + ) return self._send_json({"error": "not found"}, HTTPStatus.NOT_FOUND) @@ -240,20 +399,16 @@ def log_message(self, format: str, *args: Any) -> None: return -def serve( - *, - host: str = "127.0.0.1", - port: int = 8765, - repo: str | None = None, - open_browser: bool = False, -) -> ThreadingHTTPServer: +def serve(*, host: str = "127.0.0.1", port: int = 8765, org_id: str | None = None, repo: str | None = None, open_browser: bool = False) -> ThreadingHTTPServer: if host not in _LOOPBACK_HOSTS and os.environ.get("DHEE_UI_ALLOW_PUBLIC") != "1": raise ValueError( "Refusing to expose Dhee UI on a non-loopback host. " "Set DHEE_UI_ALLOW_PUBLIC=1 only behind a trusted auth proxy." ) + if org_id: + os.environ["DHEE_UI_ORG_ID"] = org_id if repo: - os.environ["DHEE_UI_REPO"] = str(_repo_root(repo)) + os.environ["DHEE_UI_ROOT"] = str(_repo_root(repo)) httpd = ThreadingHTTPServer((host, port), DheeDashboardHandler) url = f"http://{host}:{port}" print(f"Dhee UI running at {url}") @@ -267,10 +422,11 @@ def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(prog="dhee-ui") parser.add_argument("--host", default="127.0.0.1") parser.add_argument("--port", type=int, default=8765) + parser.add_argument("--org", default=None) parser.add_argument("--repo", default=None) parser.add_argument("--open", action="store_true") args = parser.parse_args(argv) - serve(host=args.host, port=args.port, repo=args.repo, open_browser=args.open) + serve(host=args.host, port=args.port, org_id=args.org, repo=args.repo, open_browser=args.open) return 0 diff --git a/dhee/ui/static/app.js b/dhee/ui/static/app.js index eac9d76..4720bba 100644 --- a/dhee/ui/static/app.js +++ b/dhee/ui/static/app.js @@ -10,11 +10,6 @@ function text(value, fallback = "0") { return String(value); } -function number(value) { - const parsed = Number(value || 0); - return Number.isFinite(parsed) ? parsed.toLocaleString() : "0"; -} - function emptyNode() { return document.querySelector("#empty-template").content.cloneNode(true); } @@ -39,98 +34,188 @@ async function loadDashboard() { renderDashboard(dashboard); } -async function reloadDemo() { +async function seedDemo() { await requestJson("/api/demo", { method: "POST" }); await loadDashboard(); } -function copySnapshot() { - const raw = JSON.stringify(state.dashboard || {}, null, 2); - if (navigator.clipboard) { - navigator.clipboard.writeText(raw).catch(() => {}); - } +async function connectRealWorkspace() { + await requestJson("/api/real?limit=80", { method: "POST" }); + await loadDashboard(); +} + +async function syncRepos() { + await requestJson("/api/sync?limit=80", { method: "POST" }); + await loadDashboard(); +} + +async function reviewTeam(teamId) { + await requestJson(`/api/review?team=${encodeURIComponent(teamId)}`, { method: "POST" }); + await loadDashboard(); } function renderDashboard(dashboard) { const totals = dashboard.totals || {}; const workspace = dashboard.workspace || {}; + $("#workspace-title").textContent = workspace.name || "Company Brain"; + $("#org-id-pill").textContent = dashboard.org_id || "default"; + setMetric("metric-projects", totals.projects); + setMetric("metric-teams", totals.teams); + setMetric("metric-managers", totals.context_managers); + setMetric("metric-repos", totals.repo_mappings); + setMetric("metric-context", totals.context_items); + setMetric("metric-findings", totals.open_findings); + setMetric("metric-indexed", totals.indexed_files); const firewall = dashboard.context_firewall || {}; - const aggregate = firewall.aggregate || {}; - $("#workspace-title").textContent = workspace.name || "Developer Brain"; - $("#branch-pill").textContent = workspace.branch || "local"; - $("#workspace-branch-pill").textContent = workspace.branch || "branch"; - setMetric("metric-firewall", `${text(totals.router_saved_pct, "0")}%`); - setMetric("metric-raw", number(totals.raw_tokens)); - setMetric("metric-digest", number(totals.digest_tokens)); - setMetric("metric-state", totals.state_level || "unknown"); - setMetric("metric-runtime", totals.runtime ? "On" : "Off"); - setMetric("metric-repo-context", number(totals.repo_context)); - setMetric("metric-integrations", number(totals.integrations)); - setMetric("metric-portable", totals.portable ? "Yes" : "No"); - renderIntegrations(dashboard.integrations || []); - renderWhyList(aggregate); - renderWorkspace(workspace, dashboard.runtime || {}); + const firewallAggregate = firewall.aggregate || {}; + setMetric("metric-firewall", `${text(firewallAggregate.saved_pct, "0")}%`); + renderOrgChart(dashboard.org_chart || {}); + renderCoverage(dashboard); + renderRepos(dashboard.repo_mappings || []); + renderRepoBrain(dashboard.code_brain || {}); renderContextFirewall(firewall); - renderState(dashboard.context_state || {}, dashboard.handoff || {}); - renderRepoContext(dashboard.repo_context || {}); - renderPortability(dashboard.portability || {}); + renderTeams(dashboard.team_rows || []); + renderContext(dashboard.context_index || []); + renderFindings(dashboard.findings || []); } -function renderIntegrations(items) { - const root = $("#integration-list"); +function renderOrgChart(orgChart) { + const root = $("#org-chart"); root.replaceChildren(); - if (!items.length) { + const workspace = orgChart.workspace || {}; + const projects = orgChart.projects || []; + const globals = orgChart.global_teams || []; + if (!projects.length && !globals.length) { root.appendChild(emptyNode()); return; } - items.forEach((item) => { + + const workspaceNode = document.createElement("section"); + workspaceNode.className = "org-node"; + workspaceNode.innerHTML = ` +

${text(workspace.name, "Workspace")}

+
${text(workspace.root_path, "No root path")} / ${text(workspace.default_branch, "main")}
+ `; + root.appendChild(workspaceNode); + + if (globals.length) { + const globalNode = document.createElement("section"); + globalNode.className = "org-node global"; + globalNode.innerHTML = `

Global Teams

`; + const stack = document.createElement("div"); + stack.className = "team-stack"; + globals.forEach((team) => stack.appendChild(teamChip(team))); + globalNode.appendChild(stack); + root.appendChild(globalNode); + } + + projects.forEach((project) => { const node = document.createElement("section"); - node.className = "org-node"; + node.className = "org-node project"; node.innerHTML = ` -

${text(item.name)}

-
${text(item.status)} / ${text(item.level)}
-

${text(item.detail, "")}

+

${text(project.name, project.project_id)}

+
${(project.teams || []).length} teams / ${(project.repo_mappings || []).length} mapped repos
`; + const stack = document.createElement("div"); + stack.className = "team-stack"; + (project.teams || []).forEach((team) => stack.appendChild(teamChip(team))); + node.appendChild(stack); root.appendChild(node); }); } -function renderWhyList(aggregate) { - const root = $("#why-list"); +function teamChip(team) { + const node = document.createElement("article"); + node.className = "team-chip"; + const manager = team.context_manager || {}; + const findings = team.open_findings || []; + node.innerHTML = ` +

${text(team.name, team.team_id)}

+
+ ${text(manager.manager_id, "no manager")} + ${(team.repo_mappings || []).length} repos + ${findings.length} findings +
+ `; + return node; +} + +function renderCoverage(dashboard) { + const root = $("#coverage-bars"); + root.replaceChildren(); + const counts = dashboard.kind_counts || {}; + const entries = Object.entries(counts).sort((a, b) => b[1] - a[1]); + if (!entries.length) { + root.appendChild(emptyNode()); + return; + } + const max = Math.max(...entries.map((entry) => entry[1]), 1); + entries.forEach(([kind, count]) => { + const row = document.createElement("div"); + row.className = "bar-row"; + row.innerHTML = ` + ${kind} + + ${count} + `; + root.appendChild(row); + }); +} + +function renderRepos(repos) { + const root = $("#repo-list"); root.replaceChildren(); - const items = [ - { - title: "Tool output stays bounded", - body: `Demo routing saves ${text(aggregate.saved_pct, "0")}% while preserving raw evidence behind pointers.`, - pill: "token control", - }, - { - title: "Current truth beats transcript replay", - body: "Dhee keeps task state, decisions, files, tests, and evidence separate from noisy chat history.", - pill: "state", - }, - { - title: "Local data remains inspectable", - body: "The UI is a view over local CLI/MCP primitives, not a hosted memory silo.", - pill: "local-first", - }, - ]; - items.forEach((item) => root.appendChild(summaryCard(item))); + if (!repos.length) { + root.appendChild(emptyNode()); + return; + } + repos.slice(0, 12).forEach((repo) => { + const item = document.createElement("article"); + item.className = "repo-item"; + item.innerHTML = ` +

${text(repo.team_id)} ${text(repo.project_id, "global")}

+
${text(repo.repo_url || repo.local_path, "unmapped")}
+
+ ${text(repo.branch, "main")} + ${text(repo.provider, "git")} +
+ `; + root.appendChild(item); + }); } -function renderWorkspace(workspace, runtime) { - const root = $("#workspace-card"); +function renderRepoBrain(codeBrain) { + const root = $("#repo-brain-list"); root.replaceChildren(); - const daemon = runtime.daemon || {}; - [ - { title: "Root", body: text(workspace.root_path, "unknown"), pill: text(workspace.branch, "branch") }, - { title: "Remote", body: text(workspace.remote, "no origin remote"), pill: "git" }, - { title: "Runtime", body: daemon.running ? text(daemon.endpoint, "daemon running") : "daemon stopped; CLI/MCP fall back in-process", pill: daemon.running ? "running" : "stopped" }, - ].forEach((item) => root.appendChild(summaryCard(item))); + const mappings = codeBrain.mapping_status || []; + $("#repo-brain-pill").textContent = `${codeBrain.indexed_files || 0} indexed`; + if (!mappings.length) { + root.appendChild(emptyNode()); + return; + } + mappings.forEach((mapping) => { + const lastSync = mapping.last_sync || {}; + const item = document.createElement("article"); + item.className = "repo-item"; + item.innerHTML = ` +
+

${text(mapping.team_id)} ${text(mapping.project_id, "global")}

+ ${text(mapping.sync_status)} +
+
${text(mapping.local_path || mapping.repo_url, "unmapped")}
+
+ ${mapping.indexed_files || 0} files + ${Math.round((mapping.indexed_bytes || 0) / 1024)} KB + ${lastSync.files_warmed || 0} warmed +
+ `; + root.appendChild(item); + }); } function renderContextFirewall(report) { const root = $("#firewall-list"); + if (!root) return; root.replaceChildren(); const aggregate = report.aggregate || {}; $("#firewall-pill").textContent = `${text(aggregate.saved_pct, "0")}% saved`; @@ -159,8 +244,8 @@ function renderContextFirewall(report) { const stats = document.createElement("div"); stats.className = "firewall-stats"; stats.innerHTML = ` - ${number(item.raw_tokens)} raw - ${number(item.digest_tokens)} digest + ${text(item.raw_tokens)} raw + ${text(item.digest_tokens)} digest ${text(item.saved_pct)}% saved ${text(item.expand)} `; @@ -175,81 +260,92 @@ function renderContextFirewall(report) { }); } -function renderState(contextState, handoff) { - const root = $("#state-list"); +function renderTeams(teams) { + const root = $("#team-table"); root.replaceChildren(); - const status = contextState.status || {}; - $("#state-pill").textContent = text(status.level, "unknown"); - const items = [ - { - title: "Compiled State", - body: `epoch ${text(status.task_epoch, "1")} / revision ${text(status.state_revision, "0")} / ${text(status.state_card_tokens, "0")} card tokens`, - pill: text(status.level, "unknown"), - }, - { - title: "Expansion Health", - body: `expansion rate ${text(status.expansion_rate, "0")} / projected cache read ${text(status.projected_cache_read_tokens, "0")} tokens`, - pill: text(status.expansion_level, "unknown"), - }, - { - title: "Latest Handoff", - body: handoff.available ? text(handoff.task_summary, "handoff available") : "No handoff saved yet.", - pill: handoff.available ? text(handoff.status, "active") : "empty", - }, - ]; - items.forEach((item) => root.appendChild(summaryCard(item))); - - if (contextState.card) { - const pre = document.createElement("pre"); - pre.className = "digest-preview"; - pre.textContent = contextState.card; - root.appendChild(pre); + $("#team-count-pill").textContent = `${teams.length} teams`; + if (!teams.length) { + root.appendChild(emptyNode()); + return; } + teams.forEach((team) => { + const manager = team.manager || {}; + const row = document.createElement("article"); + row.className = "team-row"; + row.innerHTML = ` +
+ ${text(team.name, team.team_id)} + ${text(team.project_id, team.team_type)} +
+
+ ${text(manager.display_name, "Context Manager")} + ${text(manager.manager_id, "not assigned")} +
+ ${team.repo_count} + ${team.context_count} + ${team.open_findings} + + `; + row.querySelector("button").addEventListener("click", () => reviewTeam(team.team_id)); + row.querySelector(".team-title").insertAdjacentHTML("beforeend", `${team.health}`); + root.appendChild(row); + }); } -function renderRepoContext(repoContext) { - const root = $("#repo-context-list"); +function renderContext(items) { + const root = $("#context-list"); root.replaceChildren(); - const entries = repoContext.entries || []; - $("#repo-context-pill").textContent = `${text(repoContext.count, "0")} entries`; - if (!entries.length) { - root.appendChild(summaryCard({ - title: "No repo context yet", - body: "Run `dhee link /path/to/repo` and promote decisions or conventions into .dhee/context.", - pill: repoContext.exists ? "empty" : "not linked", - })); + $("#context-count-pill").textContent = `${items.length} items`; + if (!items.length) { + root.appendChild(emptyNode()); return; } - entries.forEach((item) => { - root.appendChild(summaryCard({ - title: text(item.title || item.summary || item.id, "repo context"), - body: text(item.content || item.body || item.summary || item.reason, ""), - pill: text(item.kind || item.type || "note"), - })); + items.forEach((item) => { + const node = document.createElement("article"); + node.className = "context-item"; + node.innerHTML = ` +
+

${text(item.title, item.kind)}

+ ${text(item.scope)} / ${text(item.kind)} +
+
${text(item.summary, "")}
+
+ ${text(item.project_id, "company")} + ${text(item.team_id, "shared")} + ${(item.shares || []).length} shares +
+ `; + root.appendChild(node); }); } -function renderPortability(portability) { - const root = $("#portability-list"); +function renderFindings(findings) { + const root = $("#finding-list"); root.replaceChildren(); - [ - { title: "Export", body: text(portability.export), pill: ".dheemem" }, - { title: "Dry-run Import", body: text(portability.dry_run_import), pill: "inspect first" }, - { title: "Clean Uninstall", body: text(portability.uninstall), pill: "no lock-in" }, - ].forEach((item) => root.appendChild(summaryCard(item))); -} - -function summaryCard(item) { - const node = document.createElement("article"); - node.className = "context-item"; - node.innerHTML = ` -
-

${text(item.title)}

- ${text(item.pill, "")} -
-
${text(item.body, "")}
- `; - return node; + $("#finding-count-pill").textContent = `${findings.length} open`; + if (!findings.length) { + root.appendChild(emptyNode()); + return; + } + findings.forEach((finding) => { + const node = document.createElement("article"); + node.className = `finding-item ${finding.severity || "medium"}`; + node.innerHTML = ` +
+

${text(finding.title)}

+ ${text(finding.severity)} / ${text(finding.finding_type)} +
+
${text(finding.detail, "")}
+
+ ${text(finding.team_id)} + ${text(finding.manager_id)} +
+ `; + root.appendChild(node); + }); } document.querySelectorAll(".tab").forEach((tab) => { @@ -262,8 +358,9 @@ document.querySelectorAll(".tab").forEach((tab) => { }); $("#refresh-button").addEventListener("click", loadDashboard); -$("#seed-button").addEventListener("click", reloadDemo); -$("#snapshot-button").addEventListener("click", copySnapshot); +$("#seed-button").addEventListener("click", seedDemo); +$("#real-button").addEventListener("click", connectRealWorkspace); +$("#sync-button").addEventListener("click", syncRepos); loadDashboard().catch((error) => { document.body.innerHTML = `
Dashboard error${error.message}
`; diff --git a/dhee/ui/static/index.html b/dhee/ui/static/index.html index 4aa3ad2..09020c5 100644 --- a/dhee/ui/static/index.html +++ b/dhee/ui/static/index.html @@ -3,137 +3,148 @@ - Dhee UI + Dhee Enterprise
-

Local Dhee

-

Developer Brain

+

Enterprise Dhee

+

Company Brain

-
-
- Router Savings - 0% -
+
- Raw Tokens - 0 + Projects + 0
- Digest Tokens - 0 + Teams + 0
- State Level - - + Context Managers + 0
- Runtime - Off + Mapped Repos + 0
- Repo Context - 0 + Context Items + 0
-
- Integrations - 0 +
+ Open Findings + 0
- Portable - Yes + Indexed Files + 0 +
+
+ Router Savings + 0%
-
+
+ + - - - + +
-

Why Dhee Is Running

- local-first +

Coverage

-
+
-

Workspace

- branch +

Repo Mappings

-
+
+
+
+

Teams

+ 0 teams +
+
+
+ +
+
+

Repo Brain

+ 0 indexed +
+
+
+

Context Firewall

0 saved
- Agents see compact truth first. Raw evidence stays behind explicit expansion pointers. + Dhee shows agents compact truth first and keeps exact evidence behind pointers.
-
-
-

Current State

- unknown -
-
-
- -
+
-

Repo Context

- 0 entries +

Context Inventory

+ 0 items
-
+
-
+
-

Portability

- no lock-in +

Manager Findings

+ 0 open
-
+
@@ -141,8 +152,8 @@

Portability

diff --git a/dhee/ui/static/styles.css b/dhee/ui/static/styles.css index e7da6f1..b8c4a67 100644 --- a/dhee/ui/static/styles.css +++ b/dhee/ui/static/styles.css @@ -333,13 +333,6 @@ h3 { font-size: 12px; } -.small-copy { - margin-top: 8px; - color: var(--muted); - font-size: 12px; - line-height: 1.45; -} - .team-stack { display: grid; gap: 8px; diff --git a/tests/test_ui.py b/tests/test_ui.py index d4bf5e8..e4b7f39 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -5,7 +5,7 @@ from dhee.ui.server import STATIC_DIR, build_dashboard_payload -def test_public_ui_payload_uses_local_dhee_primitives(tmp_path, monkeypatch): +def test_public_ui_payload_matches_team_dashboard_shape(tmp_path, monkeypatch): monkeypatch.setenv("DHEE_DATA_DIR", str(tmp_path / "dhee-data")) monkeypatch.setenv("ENGRAM_HANDOFF_DB", str(tmp_path / "handoff.db")) repo = tmp_path / "repo" @@ -18,18 +18,21 @@ def test_public_ui_payload_uses_local_dhee_primitives(tmp_path, monkeypatch): out = build_dashboard_payload(repo=str(repo)) - assert out["format"] == "dhee_public_dashboard" assert out["workspace"]["root_path"] == str(repo.resolve()) + assert out["org_chart"]["projects"][0]["project_id"] == "developer-brain" + assert out["team_rows"][0]["team_id"] == "local-dev" assert out["context_firewall"]["aggregate"]["saved_pct"] > 50 - assert out["totals"]["repo_context"] == 1 - assert any(item["name"] == "Codex" for item in out["integrations"]) - assert out["portability"]["export"].startswith("dhee export") + assert out["totals"]["repo_mappings"] == 1 + assert out["totals"]["context_items"] == 1 + assert out["code_brain"]["mapping_status"][0]["sync_status"] in {"indexed", "not_indexed"} + assert out["context_index"][0]["title"] == "Use compact digests" -def test_public_ui_static_assets_are_packaged(): +def test_public_ui_static_assets_are_the_same_dashboard_as_team_ui(): + root = STATIC_DIR.parents[2] + team_static = root / "enterprise" / "dhee_enterprise" / "ui" / "static" assert (STATIC_DIR / "index.html").exists() assert (STATIC_DIR / "styles.css").exists() assert (STATIC_DIR / "app.js").exists() - assert "Local Dhee" in (STATIC_DIR / "index.html").read_text(encoding="utf-8") - assert "Context Firewall" in (STATIC_DIR / "index.html").read_text(encoding="utf-8") - assert "renderContextFirewall" in (STATIC_DIR / "app.js").read_text(encoding="utf-8") + for name in ["index.html", "styles.css", "app.js"]: + assert (STATIC_DIR / name).read_text(encoding="utf-8") == (team_static / name).read_text(encoding="utf-8") From 9bd8146106f7c8434bd0d5dde9248cc176cafed6 Mon Sep 17 00:00:00 2001 From: Ashish-dwi99 Date: Wed, 13 May 2026 19:29:42 +0530 Subject: [PATCH 3/5] Add context governance UI --- .gitignore | 3 + CHANGELOG.md | 9 +- README.md | 28 +- dhee/cli.py | 33 +- dhee/ui/__init__.py | 21 +- dhee/ui/cli.py | 171 + dhee/ui/server.py | 8451 ++++++++++++++++- dhee/ui/static/app.js | 367 - dhee/ui/static/index.html | 162 - dhee/ui/static/styles.css | 584 -- .../ui/web/dist/assets/CanvasView-Dg15id0Q.js | 1 + dhee/ui/web/dist/assets/index-BKI2JxEf.js | 51 + dhee/ui/web/dist/assets/index-DoQLFf8M.css | 1 + dhee/ui/web/dist/dhee-logo.png | Bin 0 -> 27501 bytes dhee/ui/web/dist/index.html | 18 + dhee/ui/web/index.html | 17 + dhee/ui/web/package-lock.json | 2034 ++++ dhee/ui/web/package.json | 24 + dhee/ui/web/public/dhee-logo.png | Bin 0 -> 27501 bytes dhee/ui/web/src/App.js | 534 ++ dhee/ui/web/src/App.tsx | 714 ++ dhee/ui/web/src/api.js | 356 + dhee/ui/web/src/api.ts | 728 ++ dhee/ui/web/src/components/AssetDrawer.js | 358 + dhee/ui/web/src/components/AssetDrawer.tsx | 559 ++ dhee/ui/web/src/components/ChatMessage.js | 39 + dhee/ui/web/src/components/ChatMessage.tsx | 101 + dhee/ui/web/src/components/FirstRunPanel.js | 74 + dhee/ui/web/src/components/FirstRunPanel.tsx | 151 + dhee/ui/web/src/components/LinePanel.js | 387 + dhee/ui/web/src/components/LinePanel.tsx | 639 ++ dhee/ui/web/src/components/NavRail.js | 100 + dhee/ui/web/src/components/NavRail.tsx | 192 + dhee/ui/web/src/components/OrgDrawer.js | 655 ++ dhee/ui/web/src/components/OrgDrawer.tsx | 1458 +++ dhee/ui/web/src/components/TopBar.js | 196 + dhee/ui/web/src/components/TopBar.tsx | 319 + dhee/ui/web/src/components/TweaksPanel.js | 84 + dhee/ui/web/src/components/TweaksPanel.tsx | 175 + .../src/components/WorkspaceManagerModal.js | 396 + .../src/components/WorkspaceManagerModal.tsx | 865 ++ .../src/components/canvas/CanvasControls.js | 88 + .../src/components/canvas/CanvasControls.tsx | 224 + .../src/components/canvas/CanvasSkeleton.js | 31 + .../src/components/canvas/CanvasSkeleton.tsx | 62 + .../src/components/canvas/DirectionHints.js | 44 + .../src/components/canvas/DirectionHints.tsx | 80 + dhee/ui/web/src/components/canvas/Minimap.js | 90 + dhee/ui/web/src/components/canvas/Minimap.tsx | 164 + dhee/ui/web/src/components/canvas/NodeCard.js | 170 + .../ui/web/src/components/canvas/NodeCard.tsx | 299 + .../canvas/__tests__/layout.smoke.js | 68 + .../canvas/__tests__/layout.smoke.ts | 79 + .../web/src/components/canvas/forceLayout.js | 198 + .../web/src/components/canvas/forceLayout.ts | 219 + dhee/ui/web/src/components/canvas/layout.js | 252 + dhee/ui/web/src/components/canvas/layout.ts | 305 + .../web/src/components/canvas/treeLayout.js | 120 + .../web/src/components/canvas/treeLayout.ts | 159 + .../components/canvas/useInfiniteCanvas.js | 383 + .../components/canvas/useInfiniteCanvas.ts | 471 + dhee/ui/web/src/components/cards/Cards.js | 172 + dhee/ui/web/src/components/cards/Cards.tsx | 403 + .../src/components/graph/CanvasNodeCard.js | 53 + .../src/components/graph/CanvasNodeCard.tsx | 110 + dhee/ui/web/src/components/ui/DecayBar.js | 21 + dhee/ui/web/src/components/ui/DecayBar.tsx | 36 + dhee/ui/web/src/components/ui/Markdown.js | 130 + dhee/ui/web/src/components/ui/Markdown.tsx | 155 + .../ui/web/src/components/ui/SectionHeader.js | 21 + .../web/src/components/ui/SectionHeader.tsx | 47 + dhee/ui/web/src/components/ui/StatPill.js | 15 + dhee/ui/web/src/components/ui/StatPill.tsx | 26 + dhee/ui/web/src/components/ui/TierBadge.js | 41 + dhee/ui/web/src/components/ui/TierBadge.tsx | 49 + dhee/ui/web/src/main.js | 6 + dhee/ui/web/src/main.tsx | 10 + dhee/ui/web/src/styles.css | 329 + dhee/ui/web/src/types.js | 1 + dhee/ui/web/src/types.ts | 944 ++ dhee/ui/web/src/views/CanvasView.js | 460 + dhee/ui/web/src/views/CanvasView.tsx | 830 ++ dhee/ui/web/src/views/ChannelView.js | 385 + dhee/ui/web/src/views/ChannelView.tsx | 794 ++ dhee/ui/web/src/views/ConflictView.js | 261 + dhee/ui/web/src/views/ConflictView.tsx | 517 + dhee/ui/web/src/views/ContextView.js | 309 + dhee/ui/web/src/views/ContextView.tsx | 573 ++ dhee/ui/web/src/views/EvolutionView.js | 321 + dhee/ui/web/src/views/EvolutionView.tsx | 629 ++ dhee/ui/web/src/views/LaunchView.js | 294 + dhee/ui/web/src/views/LaunchView.tsx | 601 ++ dhee/ui/web/src/views/MemoryView.js | 1080 +++ dhee/ui/web/src/views/MemoryView.tsx | 1814 ++++ dhee/ui/web/src/views/NotepadView.js | 216 + dhee/ui/web/src/views/NotepadView.tsx | 481 + dhee/ui/web/src/views/OrgCanvas.js | 515 + dhee/ui/web/src/views/OrgCanvas.tsx | 751 ++ dhee/ui/web/src/views/ProductViews.js | 306 + dhee/ui/web/src/views/ProductViews.tsx | 687 ++ dhee/ui/web/src/views/RouterView.js | 644 ++ dhee/ui/web/src/views/RouterView.tsx | 1252 +++ dhee/ui/web/src/views/TasksView.js | 71 + dhee/ui/web/src/views/TasksView.tsx | 168 + dhee/ui/web/src/views/WorkspaceView.js | 593 ++ dhee/ui/web/src/views/WorkspaceView.tsx | 1200 +++ dhee/ui/web/tsconfig.json | 21 + dhee/ui/web/vite.config.ts | 16 + pyproject.toml | 24 +- tests/test_packaging.py | 6 +- tests/test_ui.py | 103 +- 111 files changed, 41498 insertions(+), 1554 deletions(-) create mode 100644 dhee/ui/cli.py delete mode 100644 dhee/ui/static/app.js delete mode 100644 dhee/ui/static/index.html delete mode 100644 dhee/ui/static/styles.css create mode 100644 dhee/ui/web/dist/assets/CanvasView-Dg15id0Q.js create mode 100644 dhee/ui/web/dist/assets/index-BKI2JxEf.js create mode 100644 dhee/ui/web/dist/assets/index-DoQLFf8M.css create mode 100644 dhee/ui/web/dist/dhee-logo.png create mode 100644 dhee/ui/web/dist/index.html create mode 100644 dhee/ui/web/index.html create mode 100644 dhee/ui/web/package-lock.json create mode 100644 dhee/ui/web/package.json create mode 100644 dhee/ui/web/public/dhee-logo.png create mode 100644 dhee/ui/web/src/App.js create mode 100644 dhee/ui/web/src/App.tsx create mode 100644 dhee/ui/web/src/api.js create mode 100644 dhee/ui/web/src/api.ts create mode 100644 dhee/ui/web/src/components/AssetDrawer.js create mode 100644 dhee/ui/web/src/components/AssetDrawer.tsx create mode 100644 dhee/ui/web/src/components/ChatMessage.js create mode 100644 dhee/ui/web/src/components/ChatMessage.tsx create mode 100644 dhee/ui/web/src/components/FirstRunPanel.js create mode 100644 dhee/ui/web/src/components/FirstRunPanel.tsx create mode 100644 dhee/ui/web/src/components/LinePanel.js create mode 100644 dhee/ui/web/src/components/LinePanel.tsx create mode 100644 dhee/ui/web/src/components/NavRail.js create mode 100644 dhee/ui/web/src/components/NavRail.tsx create mode 100644 dhee/ui/web/src/components/OrgDrawer.js create mode 100644 dhee/ui/web/src/components/OrgDrawer.tsx create mode 100644 dhee/ui/web/src/components/TopBar.js create mode 100644 dhee/ui/web/src/components/TopBar.tsx create mode 100644 dhee/ui/web/src/components/TweaksPanel.js create mode 100644 dhee/ui/web/src/components/TweaksPanel.tsx create mode 100644 dhee/ui/web/src/components/WorkspaceManagerModal.js create mode 100644 dhee/ui/web/src/components/WorkspaceManagerModal.tsx create mode 100644 dhee/ui/web/src/components/canvas/CanvasControls.js create mode 100644 dhee/ui/web/src/components/canvas/CanvasControls.tsx create mode 100644 dhee/ui/web/src/components/canvas/CanvasSkeleton.js create mode 100644 dhee/ui/web/src/components/canvas/CanvasSkeleton.tsx create mode 100644 dhee/ui/web/src/components/canvas/DirectionHints.js create mode 100644 dhee/ui/web/src/components/canvas/DirectionHints.tsx create mode 100644 dhee/ui/web/src/components/canvas/Minimap.js create mode 100644 dhee/ui/web/src/components/canvas/Minimap.tsx create mode 100644 dhee/ui/web/src/components/canvas/NodeCard.js create mode 100644 dhee/ui/web/src/components/canvas/NodeCard.tsx create mode 100644 dhee/ui/web/src/components/canvas/__tests__/layout.smoke.js create mode 100644 dhee/ui/web/src/components/canvas/__tests__/layout.smoke.ts create mode 100644 dhee/ui/web/src/components/canvas/forceLayout.js create mode 100644 dhee/ui/web/src/components/canvas/forceLayout.ts create mode 100644 dhee/ui/web/src/components/canvas/layout.js create mode 100644 dhee/ui/web/src/components/canvas/layout.ts create mode 100644 dhee/ui/web/src/components/canvas/treeLayout.js create mode 100644 dhee/ui/web/src/components/canvas/treeLayout.ts create mode 100644 dhee/ui/web/src/components/canvas/useInfiniteCanvas.js create mode 100644 dhee/ui/web/src/components/canvas/useInfiniteCanvas.ts create mode 100644 dhee/ui/web/src/components/cards/Cards.js create mode 100644 dhee/ui/web/src/components/cards/Cards.tsx create mode 100644 dhee/ui/web/src/components/graph/CanvasNodeCard.js create mode 100644 dhee/ui/web/src/components/graph/CanvasNodeCard.tsx create mode 100644 dhee/ui/web/src/components/ui/DecayBar.js create mode 100644 dhee/ui/web/src/components/ui/DecayBar.tsx create mode 100644 dhee/ui/web/src/components/ui/Markdown.js create mode 100644 dhee/ui/web/src/components/ui/Markdown.tsx create mode 100644 dhee/ui/web/src/components/ui/SectionHeader.js create mode 100644 dhee/ui/web/src/components/ui/SectionHeader.tsx create mode 100644 dhee/ui/web/src/components/ui/StatPill.js create mode 100644 dhee/ui/web/src/components/ui/StatPill.tsx create mode 100644 dhee/ui/web/src/components/ui/TierBadge.js create mode 100644 dhee/ui/web/src/components/ui/TierBadge.tsx create mode 100644 dhee/ui/web/src/main.js create mode 100644 dhee/ui/web/src/main.tsx create mode 100644 dhee/ui/web/src/styles.css create mode 100644 dhee/ui/web/src/types.js create mode 100644 dhee/ui/web/src/types.ts create mode 100644 dhee/ui/web/src/views/CanvasView.js create mode 100644 dhee/ui/web/src/views/CanvasView.tsx create mode 100644 dhee/ui/web/src/views/ChannelView.js create mode 100644 dhee/ui/web/src/views/ChannelView.tsx create mode 100644 dhee/ui/web/src/views/ConflictView.js create mode 100644 dhee/ui/web/src/views/ConflictView.tsx create mode 100644 dhee/ui/web/src/views/ContextView.js create mode 100644 dhee/ui/web/src/views/ContextView.tsx create mode 100644 dhee/ui/web/src/views/EvolutionView.js create mode 100644 dhee/ui/web/src/views/EvolutionView.tsx create mode 100644 dhee/ui/web/src/views/LaunchView.js create mode 100644 dhee/ui/web/src/views/LaunchView.tsx create mode 100644 dhee/ui/web/src/views/MemoryView.js create mode 100644 dhee/ui/web/src/views/MemoryView.tsx create mode 100644 dhee/ui/web/src/views/NotepadView.js create mode 100644 dhee/ui/web/src/views/NotepadView.tsx create mode 100644 dhee/ui/web/src/views/OrgCanvas.js create mode 100644 dhee/ui/web/src/views/OrgCanvas.tsx create mode 100644 dhee/ui/web/src/views/ProductViews.js create mode 100644 dhee/ui/web/src/views/ProductViews.tsx create mode 100644 dhee/ui/web/src/views/RouterView.js create mode 100644 dhee/ui/web/src/views/RouterView.tsx create mode 100644 dhee/ui/web/src/views/TasksView.js create mode 100644 dhee/ui/web/src/views/TasksView.tsx create mode 100644 dhee/ui/web/src/views/WorkspaceView.js create mode 100644 dhee/ui/web/src/views/WorkspaceView.tsx create mode 100644 dhee/ui/web/tsconfig.json create mode 100644 dhee/ui/web/vite.config.ts diff --git a/.gitignore b/.gitignore index fbd5a96..f45dbe7 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,8 @@ __pycache__/ /build/ develop-eggs/ dist/ +!dhee/ui/web/dist/ +!dhee/ui/web/dist/** downloads/ eggs/ .eggs/ @@ -124,6 +126,7 @@ pip-delete-this-directory.txt # Node (dashboard) node_modules/ +*.tsbuildinfo # Rust build artifacts (dhee-accel) target/ diff --git a/CHANGELOG.md b/CHANGELOG.md index fe37a08..6d554cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this - Rewrote the README as a concise first-read product page focused on why Dhee matters, the UI demo, install, integrations, benchmarks, and the public-core/paid-team-layer boundary. -- Restored the public `dhee ui` dashboard and `dhee-ui` entrypoint by copying - the same dashboard UI and API shape into OSS. The public backend maps those - screens onto local repo/Dhee data instead of requiring paid-team services. +- 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. diff --git a/README.md b/README.md index 6f9e45b..ec74647 100644 --- a/README.md +++ b/README.md @@ -58,27 +58,25 @@ The promise is simple: --- -## UI Demo +## Dhee UI -Run the local dashboard. 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 ui --open +dhee ui ``` -The first screen shows: +The UI opens on a command center, then lets you inspect: -- the same dashboard screens used by the team layer: Org Chart, Teams, Repo - Brain, Context Firewall, Context, and Findings -- an OSS backend that maps those screens onto your local repo, Dhee context, - router proof, and code index summary -- router savings from noisy pytest logs, git diffs, and long source reads -- repo-shared context entries and local findings -- Connect Real / Sync Repos flows that operate against your current workspace +- 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 -The OSS UI copies the same frontend and API shape used by the team dashboard. -The raw evidence still stays behind `dhee_expand_result(ptr="...")`; the -dashboard makes that visible. +The raw evidence still stays behind `dhee_expand_result(ptr="...")`; the UI +makes the routing and expansion decisions inspectable. --- @@ -105,7 +103,7 @@ Useful first commands: ```bash dhee status dhee doctor -dhee ui --open +dhee ui dhee handoff dhee context state --card dhee runtime status diff --git a/dhee/cli.py b/dhee/cli.py index b15f4cc..7d788a6 100644 --- a/dhee/cli.py +++ b/dhee/cli.py @@ -1242,16 +1242,17 @@ def cmd_demo(args: argparse.Namespace) -> None: def cmd_ui(args: argparse.Namespace) -> None: - """Run the local Dhee dashboard.""" - from dhee.ui.server import serve + """Run the local Dhee web UI.""" + from dhee.ui.cli import cmd_ui as run_ui - serve( - host=getattr(args, "host", "127.0.0.1"), - port=int(getattr(args, "port", 8765) or 8765), - org_id=getattr(args, "org", None), - repo=getattr(args, "repo", None), - open_browser=bool(getattr(args, "open", False)), - ) + 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: @@ -2546,12 +2547,18 @@ def build_parser() -> argparse.ArgumentParser: p_demo.add_argument("--json", action="store_true", help="JSON output") # ui - p_ui = sub.add_parser("ui", help="Run the local Dhee dashboard") + 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=8765, help="Bind port") - p_ui.add_argument("--org", help="Dashboard org/workspace id") + 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("--open", action="store_true", help="Open in the default browser") + 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") diff --git a/dhee/ui/__init__.py b/dhee/ui/__init__.py index 2ba1084..b0fbb59 100644 --- a/dhee/ui/__init__.py +++ b/dhee/ui/__init__.py @@ -1,5 +1,20 @@ -"""Local Dhee dashboard.""" +"""Sankhya — Dhee's web UI. -from .server import build_dashboard_payload, serve +`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. +""" -__all__ = ["build_dashboard_payload", "serve"] +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"] diff --git a/dhee/ui/cli.py b/dhee/ui/cli.py new file mode 100644 index 0000000..e9c768b --- /dev/null +++ b/dhee/ui/cli.py @@ -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()) diff --git a/dhee/ui/server.py b/dhee/ui/server.py index a16b611..f9436b4 100644 --- a/dhee/ui/server.py +++ b/dhee/ui/server.py @@ -1,434 +1,8143 @@ -"""Public Dhee dashboard using the same UI/API shape as the team dashboard.""" +"""FastAPI bridge between the Sankhya SPA and the Dhee substrate. + +Endpoints map the fields the prototype UI expects onto real Dhee state: + +- /api/memories FullMemory.get_all -> engram shape +- /api/memories (POST) remember() +- /api/memories/{id} DELETE archive +- /api/router/stats router.stats.compute_stats +- /api/router/policy router.policy.load + router.tune.build_report +- /api/router/tune (POST) router.tune.apply +- /api/meta-buddhi MetaBuddhi snapshot +- /api/evolution samskara / evolution log +- /api/conflicts derived from memory history +- /api/tasks shared_tasks.shared_task_snapshot +- /api/status overall health +- /api/security/api-keys encrypted API-key storage + rotation + +Honesty: where a piece of the prototype has no live adapter yet (e.g. +the curated evolution timeline with pretty labels), we synthesize a +minimal shape from real session logs and mark `live: false` on that +endpoint so the UI can show a "derived" badge. No silent mocks. +""" from __future__ import annotations -import argparse -import json -import os -import subprocess -import threading -import webbrowser -from http import HTTPStatus -from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer -from pathlib import Path -from typing import Any -from urllib.parse import parse_qs, urlparse +import asyncio +import hashlib +import json +import logging +import os +import re +import shutil +import sqlite3 +import subprocess +import threading +import time +from collections import Counter, deque +from datetime import datetime, timedelta, timezone +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple + +from fastapi import Body, FastAPI, File, Form, HTTPException, Request, UploadFile, WebSocket, WebSocketDisconnect +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import StreamingResponse +from fastapi.staticfiles import StaticFiles +from pydantic import AliasChoices, BaseModel, ConfigDict, Field + +log = logging.getLogger(__name__) + +_LOCAL_UI_ORIGIN_REGEX = r"https?://(localhost|127\.0\.0\.1|\[::1\])(:[0-9]+)?" +_SESSION_LOG_TAIL_BYTES = int(os.environ.get("DHEE_UI_SESSION_LOG_TAIL_BYTES", str(768 * 1024))) +_RUNTIME_MIRROR_CODEX_LIMIT = int(os.environ.get("DHEE_UI_RUNTIME_CODEX_LIMIT", "6")) +_RUNTIME_MIRROR_CLAUDE_LIMIT = int(os.environ.get("DHEE_UI_RUNTIME_CLAUDE_LIMIT", "6")) +_SESSION_LOG_PARSE_CACHE: Dict[Tuple[str, str, str], Tuple[int, int, Dict[str, Any]]] = {} +_MIRROR_RUNTIME_CACHE_TTL_SECONDS = float(os.environ.get("DHEE_UI_MIRROR_RUNTIME_TTL_SECONDS", "3")) +_MIRROR_RUNTIME_CACHE: Dict[Tuple[str, ...], Tuple[float, Dict[str, Any]]] = {} +_MIRROR_RUNTIME_LOCK = threading.Lock() +_UI_DB = None + + +class CaptureSessionStartPayload(BaseModel): + source_app: str + namespace: Optional[str] = None + metadata: Optional[Dict[str, Any]] = None + + +class CaptureSessionEndPayload(BaseModel): + session_id: Optional[str] = None + distill: bool = True + summary_hint: Optional[str] = None + + +class CaptureActionPayload(BaseModel): + session_id: str + source_app: Optional[str] = None + namespace: Optional[str] = None + created_at: Optional[str] = None + action_type: str + target: Optional[Dict[str, Any]] = None + surface: Optional[Dict[str, Any]] = None + surface_id: Optional[str] = None + surface_type: Optional[str] = None + title: Optional[str] = None + url: Optional[str] = None + path: Optional[str] = None + path_hint: Optional[List[str]] = None + previous_surface_id: Optional[str] = None + capture_mode: Optional[str] = None + confidence: Optional[float] = None + action_payload: Optional[Dict[str, Any]] = None + source_kind: Optional[str] = None + before_context: Optional[str] = None + after_context: Optional[str] = None + before_frame_ref: Optional[str] = None + after_frame_ref: Optional[str] = None + task_instruction: Optional[str] = None + recent_actions: Optional[List[str]] = None + metadata: Optional[Dict[str, Any]] = None + + +class CaptureObservationPayload(BaseModel): + session_id: str + source_app: Optional[str] = None + namespace: Optional[str] = None + created_at: Optional[str] = None + action_id: Optional[str] = None + source_kind: Optional[str] = None + kind: Optional[str] = None + text: Optional[str] = None + text_payload: Optional[str] = None + structured: Optional[Dict[str, Any]] = None + structured_payload: Optional[Dict[str, Any]] = None + confidence: Optional[float] = None + surface: Optional[Dict[str, Any]] = None + + +class RememberPayload(BaseModel): + content: str + tier: Optional[str] = None + tags: Optional[List[str]] = None + source: Optional[str] = None + + +class CaptureArtifactPayload(BaseModel): + session_id: str + source_app: Optional[str] = None + namespace: Optional[str] = None + created_at: Optional[str] = None + action_id: Optional[str] = None + content_base64: Optional[str] = None + path: Optional[str] = None + mime_type: Optional[str] = None + artifact_type: Optional[str] = None + retention: Optional[str] = None + ttl_hours: Optional[int] = None + surface: Optional[Dict[str, Any]] = None + surface_id: Optional[str] = None + surface_type: Optional[str] = None + title: Optional[str] = None + url: Optional[str] = None + path_hint: Optional[List[str]] = None + label: Optional[str] = None + metadata: Optional[Dict[str, Any]] = None + + +class CapturePreferencePayload(BaseModel): + source_app: str + enabled: bool + mode: str = "sampled" + metadata: Optional[Dict[str, Any]] = None + + +class MemoryAskPayload(BaseModel): + query: str + source_app: Optional[str] = None + limit: int = 6 + + +class AgentContextPackPayload(BaseModel): + task_instruction: str + agent_id: Optional[str] = None + source_app: Optional[str] = None + current_frame_ref: Optional[str] = None + current_context_text: Optional[str] = None + recent_actions: Optional[List[str]] = None + limit: int = 5 + + +class WorldContextPackPayload(BaseModel): + current_frame_ref: str + current_context_text: str + task_instruction: str + recent_actions: Optional[List[str]] = None + limit: int = 5 + + +class UiPayload(BaseModel): + model_config = ConfigDict(populate_by_name=True, extra="ignore") + + +class WorkspaceRootCreatePayload(UiPayload): + name: Optional[str] = Field(default=None, validation_alias=AliasChoices("name", "label")) + description: Optional[str] = None + root_path: Optional[str] = Field( + default=None, + validation_alias=AliasChoices("root_path", "rootPath", "workspace_path", "workspacePath", "path"), + ) + + +class WorkspaceRootUpdatePayload(UiPayload): + name: Optional[str] = Field(default=None, validation_alias=AliasChoices("name", "label")) + description: Optional[str] = None + + +class WorkspaceCreatePayload(UiPayload): + workspace_path: Optional[str] = Field( + default=None, + validation_alias=AliasChoices("workspace_path", "workspacePath", "root_path", "rootPath", "path"), + ) + label: Optional[str] = None + folder_path: Optional[str] = Field(default=None, validation_alias=AliasChoices("folder_path", "folderPath")) + is_primary: bool = Field(default=False, validation_alias=AliasChoices("is_primary", "isPrimary")) + folders: Optional[List[str]] = None + + +class WorkspaceFolderPayload(UiPayload): + path: Optional[str] = Field(default=None, validation_alias=AliasChoices("path", "mount_path", "mountPath", "rootPath")) + label: Optional[str] = None + + +class WorkspaceUpdatePayload(UiPayload): + label: Optional[str] = Field(default=None, validation_alias=AliasChoices("label", "name")) + description: Optional[str] = None + root_path: Optional[str] = Field( + default=None, + validation_alias=AliasChoices("root_path", "rootPath", "workspace_path", "workspacePath", "path"), + ) + + +class WorkspaceProjectCreatePayload(UiPayload): + name: Optional[str] = Field(default=None, validation_alias=AliasChoices("name", "label")) + description: Optional[str] = None + default_runtime: Optional[str] = Field(default=None, validation_alias=AliasChoices("default_runtime", "defaultRuntime")) + color: Optional[str] = None + icon: Optional[str] = None + scope_rules: Optional[List[Dict[str, Any]]] = Field(default=None, validation_alias=AliasChoices("scope_rules", "scopeRules")) + + +class WorkspaceProjectUpdatePayload(UiPayload): + name: Optional[str] = Field(default=None, validation_alias=AliasChoices("name", "label")) + description: Optional[str] = None + default_runtime: Optional[str] = Field(default=None, validation_alias=AliasChoices("default_runtime", "defaultRuntime")) + color: Optional[str] = None + icon: Optional[str] = None + scope_rules: Optional[List[Dict[str, Any]]] = Field(default=None, validation_alias=AliasChoices("scope_rules", "scopeRules")) + + +class FolderPickPayload(BaseModel): + prompt: Optional[str] = None + + +class AssetAskPayload(BaseModel): + question: str + + +class ConflictResolutionPayload(BaseModel): + action: str + merged_content: Optional[str] = None + reason: Optional[str] = None + + +class SessionLaunchPayload(BaseModel): + runtime: str + title: Optional[str] = None + permission_mode: Optional[str] = None + task_id: Optional[str] = None + project_id: Optional[str] = None + + +class WorkspaceLineMessagePayload(BaseModel): + project_id: Optional[str] = None + target_project_id: Optional[str] = None + channel: Optional[str] = None + session_id: Optional[str] = None + task_id: Optional[str] = None + message_kind: str = "update" + title: Optional[str] = None + body: str + metadata: Optional[Dict[str, Any]] = None + + +class ContextUpsertPayload(BaseModel): + context_id: Optional[str] = None + expected_content_hash: Optional[str] = None + title: str + content: str + scope: str + kind: str = "note" + project_id: Optional[str] = None + team_id: Optional[str] = None + user_id: Optional[str] = None + tags: Optional[List[str]] = None + summary: Optional[str] = None + metadata: Optional[Dict[str, Any]] = None + + +class ProposalCreatePayload(BaseModel): + title: str + content: str + scope: str + kind: str + project_id: Optional[str] = None + team_id: Optional[str] = None + proposed_by_user_id: str + supersedes_id: Optional[str] = None + tags: Optional[List[str]] = None + metadata: Optional[Dict[str, Any]] = None + + +class ProposalDecisionPayload(BaseModel): + reviewer_user_id: str + + +class FindingResolvePayload(BaseModel): + resolved_by: Optional[str] = None + + +class IntegrationPayload(BaseModel): + scope: str + target_id: str + type: str + value: Any + metadata: Optional[Dict[str, Any]] = None + + +class TeamJoinPayload(BaseModel): + org_id: str + project_id: Optional[str] = None + team_id: Optional[str] = None + role: Optional[str] = "developer" + repo_root: Optional[str] = None + + +class EnterpriseWorkspacePayload(BaseModel): + name: str + root_path: Optional[str] = None + default_branch: Optional[str] = "main" + + +class EnterpriseProjectCreatePayload(BaseModel): + name: str + project_id: Optional[str] = None + description: Optional[str] = "" + + +class ProjectTeamCreatePayload(BaseModel): + name: str + team_id: Optional[str] = None + description: Optional[str] = "" + + +class ProjectFolderAddPayload(BaseModel): + local_path: Optional[str] = None + repo_url: Optional[str] = None + label: Optional[str] = None + kind: Optional[str] = "folder" + + +class LocalContextFolderPayload(BaseModel): + path: str + shared: Optional[bool] = True + + +class TeamCollaborationPayload(BaseModel): + target_team_id: str + + +# Module-level payloads for the new workspace + context endpoints. +# These need to live outside ``create_app()`` because FastAPI's body +# resolution can't see classes that live in nested function scope when +# ``from __future__ import annotations`` defers annotation evaluation +# (the existing pattern in this file is module-level for body params, +# inline only for ad-hoc query helpers). + + +class LocalWorkspaceCreatePayload(BaseModel): + name: Optional[str] = None + id: Optional[str] = None + + +class ContextPromotePayload(BaseModel): + memory_id: str + repo: Optional[str] = None + kind: Optional[str] = "learning" + title: Optional[str] = None + + +class ContextDemotePayload(BaseModel): + entry_id: str + repo: Optional[str] = None + + +class UiLearningDecisionPayload(BaseModel): + scope: Optional[str] = "personal" + repo: Optional[str] = None + approved_by: Optional[str] = "dhee-ui" + reason: Optional[str] = None + + +class UiPortabilityExportPayload(BaseModel): + output_path: Optional[str] = None + user_id: str = "default" + repo: Optional[str] = None + + +class UiPortabilityImportPayload(BaseModel): + input_path: str + user_id: str = "default" + repo: Optional[str] = None + + +# ─── Shape helpers ──────────────────────────────────────────────────────────── + +_TIER_BY_SCORE = [ + (0.90, "canonical"), + (0.70, "high"), + (0.40, "medium"), + (0.00, "short-term"), +] + + +def _tier_for(mem: Dict[str, Any]) -> str: + meta = mem.get("metadata") or {} + explicit = meta.get("tier") or mem.get("tier") + if explicit in {"canonical", "high", "medium", "short-term", "avoid"}: + return explicit + if meta.get("avoid") or mem.get("avoid"): + return "avoid" + score = float( + mem.get("score") + or meta.get("confidence") + or meta.get("importance") + or 0.5 + ) + for threshold, tier in _TIER_BY_SCORE: + if score >= threshold: + return tier + return "short-term" + + +def _estimate_tokens(text: str) -> int: + return max(1, len(text) // 4) + + +def _engram_from_memory(mem: Dict[str, Any]) -> Dict[str, Any]: + meta = mem.get("metadata") or {} + content = mem.get("memory") or mem.get("content") or mem.get("text") or "" + created = ( + mem.get("created_at") + or mem.get("createdAt") + or meta.get("created_at") + or "" + ) + if isinstance(created, (int, float)): + created = time.strftime("%Y-%m-%d", time.localtime(float(created))) + elif isinstance(created, str) and "T" in created: + created = created.split("T", 1)[0] + decay = mem.get("decay") + if decay is None: + decay = meta.get("decay") + if decay is None: + # Flat 1.0 unless Dhee has populated a decay signal. + decay = 1.0 + tags = meta.get("tags") or mem.get("tags") or [] + if isinstance(tags, str): + tags = [t.strip() for t in tags.split(",") if t.strip()] + return { + "id": str(mem.get("id") or meta.get("id") or meta.get("memory_id") or ""), + "tier": _tier_for(mem), + "content": content, + "source": str(meta.get("source") or mem.get("source") or "dhee"), + "created": created or time.strftime("%Y-%m-%d"), + "tags": list(tags), + "decay": float(decay), + "reaffirmed": int(meta.get("reaffirmed") or mem.get("reaffirmed") or 0), + "tokens": _estimate_tokens(content), + } + + +_AUTO_MEMORY_TIER_BY_TYPE = { + "user": "high", + "feedback": "high", + "project": "medium", + "reference": "medium", +} + + +def _parse_auto_memory_file(path: Path) -> Optional[Dict[str, Any]]: + try: + text = path.read_text(encoding="utf-8") + except OSError: + return None + name = path.stem + description = "" + mem_type = "project" + body = text + if text.startswith("---"): + end = text.find("\n---", 3) + if end != -1: + header = text[3:end] + body = text[end + 4 :].lstrip("\n") + for line in header.splitlines(): + key, sep, value = line.partition(":") + if not sep: + continue + key = key.strip().lower() + value = value.strip().strip('"').strip("'") + if key == "name" and value: + name = value + elif key == "description" and value: + description = value + elif key == "type" and value: + mem_type = value + body = body.strip() + if not body and not description: + return None + content_parts: List[str] = [f"[{mem_type}] {name}"] + if description: + content_parts.append(description) + if body: + content_parts.append(body) + content = "\n\n".join(content_parts) + try: + created = datetime.fromtimestamp(path.stat().st_mtime, tz=timezone.utc).strftime("%Y-%m-%d") + except OSError: + created = time.strftime("%Y-%m-%d") + tier = _AUTO_MEMORY_TIER_BY_TYPE.get(mem_type, "medium") + file_id = hashlib.sha1(str(path).encode("utf-8")).hexdigest()[:16] + return { + "id": f"auto:{file_id}", + "tier": tier, + "content": content, + "source": "claude_auto_memory", + "created": created, + "tags": [mem_type, "auto-memory"], + "decay": 1.0, + "reaffirmed": 0, + "tokens": _estimate_tokens(content), + } + + +def _auto_memory_engrams() -> List[Dict[str, Any]]: + roots = [Path.home() / ".claude" / "projects"] + engrams: List[Dict[str, Any]] = [] + for root in roots: + if not root.is_dir(): + continue + try: + project_dirs = [p for p in root.iterdir() if p.is_dir()] + except OSError: + continue + for project_dir in project_dirs: + memory_dir = project_dir / "memory" + if not memory_dir.is_dir(): + continue + try: + files = [p for p in memory_dir.iterdir() if p.is_file() and p.suffix == ".md"] + except OSError: + continue + for path in files: + if path.name.upper() == "MEMORY.MD": + continue + eng = _parse_auto_memory_file(path) + if eng: + engrams.append(eng) + engrams.sort(key=lambda e: e.get("created") or "", reverse=True) + return engrams + + +# ─── App factory ────────────────────────────────────────────────────────────── + + +def create_app(*, serve_static: bool = True, dev_mode: bool = False) -> FastAPI: + app = FastAPI(title="Sankhya — Dhee UI", version="1.0.0") + app.add_middleware( + CORSMiddleware, + allow_origin_regex=os.environ.get("DHEE_UI_CORS_ORIGIN_REGEX") or _LOCAL_UI_ORIGIN_REGEX, + allow_credentials=False, + allow_methods=["*"], + allow_headers=["*"], + ) + + # ─── Memory ────────────────────────────────────────────────────────────── + + def _get_memory(): + from dhee.cli_config import get_memory_instance + + return get_memory_instance(None) + + _memory_os_service = None + + def _get_memory_os_service(): + nonlocal _memory_os_service + if _memory_os_service is None: + from dhee.world_memory import MemoryOSService + + _memory_os_service = MemoryOSService.from_default_runtime(memory=_get_memory()) + return _memory_os_service + + @app.get("/api/memories") + def list_memories(limit: int = 500) -> Dict[str, Any]: + try: + mem = _get_memory() + raw = mem.get_all( + user_id=os.environ.get("DHEE_UI_USER_ID", "default"), + limit=limit, + ) + # get_all returns {"results": [...]} in some codepaths + if isinstance(raw, dict): + raw = raw.get("results") or raw.get("memories") or [] + engrams = [_engram_from_memory(m) for m in raw if m] + engrams.extend(_auto_memory_engrams()) + return {"live": True, "engrams": engrams, "count": len(engrams)} + except Exception as exc: # noqa: BLE001 + log.warning("list_memories failed: %s", exc) + return {"live": False, "engrams": [], "count": 0, "error": str(exc)} + + + @app.post("/api/memories") + def remember(payload: RememberPayload) -> Dict[str, Any]: + try: + mem = _get_memory() + result = mem.add( + messages=[{"role": "user", "content": payload.content}], + user_id=os.environ.get("DHEE_UI_USER_ID", "default"), + metadata={ + "source": payload.source or "sankhya-ui", + "tier": payload.tier or "short-term", + "tags": payload.tags or [], + }, + ) + return {"ok": True, "result": result} + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=500, detail=str(exc)) from exc + + @app.delete("/api/memories/{memory_id}") + def archive_memory(memory_id: str) -> Dict[str, Any]: + try: + mem = _get_memory() + mem.delete(memory_id) + return {"ok": True, "id": memory_id} + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=500, detail=str(exc)) from exc + + # ─── Router ────────────────────────────────────────────────────────────── + + @app.get("/api/router/stats") + def router_stats(agent_id: Optional[str] = None) -> Dict[str, Any]: + try: + from dhee.router import stats as rstats + + selected_agent = None if agent_id in (None, "", "all") else agent_id + s = rstats.compute_stats(agent_id=selected_agent).to_dict() + agents = rstats.list_agent_stats() + codex_native = _router_codex_native_usage(_ui_repo()) + tools = [] + for name, calls in s.get("calls_by_tool", {}).items(): + tokens_saved = int( + s.get("est_tokens_diverted", 0) + * (calls / max(1, s.get("total_calls", 1))) + ) + tools.append( + { + "name": name, + "calls": calls, + "tokensSaved": tokens_saved, + "expansions": int( + s.get("expansion_calls", 0) + * (calls / max(1, s.get("total_calls", 1))) + ), + "avgDigest": 60, + "avgRaw": int(tokens_saved / max(1, calls)) if calls else 0, + } + ) + enterprise_saved_tokens = 0 + enterprise_saved_pct = 0.0 + enterprise_raw_tokens = 0 + enterprise_summary_tokens = 0 + enterprise_raw_fallbacks = 0 + enterprise_gate_suggestions = 0 + enterprise_gate_denials = 0 + enterprise_gate_fallbacks = 0 + return { + "live": True, + "selectedAgent": selected_agent or "all", + "sessionTokensSaved": s.get("est_tokens_diverted", 0), + "enterpriseSavedTokens": enterprise_saved_tokens, + "enterpriseSavedPct": enterprise_saved_pct, + "enterpriseRawTokens": enterprise_raw_tokens, + "enterpriseSummaryTokens": enterprise_summary_tokens, + "enterpriseRawFallbacks": enterprise_raw_fallbacks, + "enterpriseGateSuggestions": enterprise_gate_suggestions, + "enterpriseGateDenials": enterprise_gate_denials, + "enterpriseGateFallbacks": enterprise_gate_fallbacks, + "totalCalls": s.get("total_calls", 0), + "expansionRate": s.get("expansion_rate", 0.0), + "sessionCost": round( + s.get("est_tokens_diverted", 0) * 1e-6 * 3.0, 4 + ), + "estimatedFullCost": round( + (s.get("est_tokens_diverted", 0) + s.get("bytes_stored", 0) / 3.5) + * 1e-6 + * 3.0, + 4, + ), + "tools": tools, + "agents": agents, + "sessions": s.get("sessions", 0), + "bytesStored": s.get("bytes_stored", 0), + "dailySavings": _seven_day_savings(selected_agent), + "days": _seven_day_labels(), + "codexNative": codex_native, + } + except Exception as exc: # noqa: BLE001 + log.warning("router_stats failed: %s", exc) + return {"live": False, "error": str(exc)} + + @app.get("/api/router/policy") + def router_policy() -> Dict[str, Any]: + try: + from dhee.router import policy, tune + + data = policy.load() + report = tune.build_report() + prev = { + (s.tool, s.intent): s.current_depth for s in getattr(report, "suggestions", []) + } + depths_map = data.get("depths", {}) + depth_rank = {"shallow": 1, "normal": 2, "deep": 3} + policies = [] + for tool, intents in depths_map.items(): + for intent, depth in intents.items(): + expansion = 0.0 + for s in getattr(report, "suggestions", []): + if s.tool == tool and s.intent == intent: + expansion = getattr(s, "expansion_rate", 0.0) + prev_depth = prev.get((tool, intent), depth) + policies.append( + { + "intent": intent, + "label": intent.replace("_", " ").title(), + "depth": depth_rank.get(depth, 2), + "prevDepth": depth_rank.get(prev_depth, 2), + "expansionRate": expansion, + "tuned": depth != prev_depth, + "tool": tool, + } + ) + return {"live": True, "policies": policies, "raw": data} + except Exception as exc: # noqa: BLE001 + log.warning("router_policy failed: %s", exc) + return {"live": False, "policies": [], "error": str(exc)} + + @app.post("/api/router/tune") + def router_tune_apply() -> Dict[str, Any]: + try: + from dhee.router import tune + + report = tune.build_report() + applied = tune.apply(report) + return { + "ok": True, + "applied": applied, + "human": tune.format_human(report), + "suggestions": [ + { + "tool": s.tool, + "intent": s.intent, + "from": s.current_depth, + "to": s.proposed_depth, + "reason": getattr(s, "reason", ""), + } + for s in getattr(report, "suggestions", []) + ], + } + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=500, detail=str(exc)) from exc + + # ─── MetaBuddhi + Evolution ────────────────────────────────────────────── + + @app.get("/api/meta-buddhi") + def meta_buddhi_status() -> Dict[str, Any]: + try: + from dhee.core import meta_buddhi as mb + + snapshot: Dict[str, Any] = { + "status": "active", + "strategy": "Adaptive depth v2.3", + } + if hasattr(mb, "latest_snapshot"): + snapshot.update(mb.latest_snapshot() or {}) # type: ignore[attr-defined] + snapshot.setdefault("sessionInsights", 0) + snapshot.setdefault("totalInsights", 0) + snapshot.setdefault("pendingProposals", 0) + snapshot.setdefault("lastGate", "") + snapshot.setdefault("confidenceGroups", _default_confidence_groups()) + return {"live": True, **snapshot} + except Exception as exc: # noqa: BLE001 + log.warning("meta_buddhi failed: %s", exc) + return { + "live": False, + "status": "unknown", + "strategy": "—", + "sessionInsights": 0, + "totalInsights": 0, + "pendingProposals": 0, + "lastGate": "", + "confidenceGroups": _default_confidence_groups(), + "error": str(exc), + } + + @app.get("/api/evolution") + def evolution_timeline() -> Dict[str, Any]: + events = _load_evolution_events() + return {"live": bool(events), "events": events} + + # ─── Conflicts ─────────────────────────────────────────────────────────── + + @app.get("/api/conflicts") + def conflicts() -> Dict[str, Any]: + try: + mem = _get_memory() + items: List[Dict[str, Any]] = [] + supported = bool(hasattr(mem, "resolve_conflict")) + if hasattr(mem, "get_conflicts"): + items = list(mem.get_conflicts()) # type: ignore[attr-defined] + return { + "live": True, + "supported": supported, + "conflicts": items, + "resolutionMode": "native" if supported else "read-only", + } + except Exception as exc: # noqa: BLE001 + log.info("conflicts: no live adapter (%s)", exc) + return { + "live": False, + "supported": False, + "conflicts": [], + "resolutionMode": "unavailable", + } + + @app.post("/api/conflicts/{conflict_id}/resolve") + def resolve_conflict( + conflict_id: str, + payload: ConflictResolutionPayload = Body(...), + ) -> Dict[str, Any]: + try: + mem = _get_memory() + if not hasattr(mem, "resolve_conflict"): + raise HTTPException( + status_code=501, + detail="Conflict resolution is not available in this Dhee runtime yet", + ) + result = mem.resolve_conflict( # type: ignore[attr-defined] + conflict_id, + payload.action, + merged_content=payload.merged_content, + reason=payload.reason, + ) + return {"ok": True, "id": conflict_id, "action": payload.action, "result": result} + except HTTPException: + raise + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=500, detail=str(exc)) from exc + + # ─── Projects / Workspaces / Sessions ─────────────────────────────────── + + @app.get("/api/projects") + def list_projects_api() -> Dict[str, Any]: + try: + return _build_project_index_payload() + except Exception as exc: # noqa: BLE001 + log.warning("projects failed: %s", exc) + return { + "live": False, + "workspaces": [], + "currentProjectId": "", + "currentWorkspaceId": "", + "currentSessionId": "", + "error": str(exc), + } + + @app.get("/api/workspaces") + def list_workspaces_api() -> Dict[str, Any]: + try: + return _build_project_index_payload() + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.post("/api/workspaces") + def create_workspace_api(payload: WorkspaceRootCreatePayload) -> Dict[str, Any]: + try: + db = _get_db() + workspace_name = _display_name(payload.name, fallback="Workspace") + if not workspace_name: + raise HTTPException(status_code=400, detail="Workspace name is required") + workspace = db.upsert_workspace( + { + "user_id": _ui_user_id(), + "name": workspace_name, + "description": payload.description, + "root_path": None, + "metadata": {"created_via": "sankhya-ui"}, + } + ) + _mirror_runtime_sessions(db) + return {"ok": True, "workspace": _workspace_summary(db, workspace)} + except HTTPException: + raise + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.post("/api/legacy-projects") + def create_project_api(payload: WorkspaceRootCreatePayload) -> Dict[str, Any]: + try: + db = _get_db() + workspace_name = _display_name(payload.name, fallback_path=payload.root_path) + workspace = db.upsert_workspace( + { + "user_id": _ui_user_id(), + "name": workspace_name, + "description": payload.description, + "root_path": _abs_user_path(payload.root_path) or None, + "metadata": {"created_via": "sankhya-ui"}, + } + ) + _mirror_runtime_sessions(db, extra_paths=[_workspace_primary_path(workspace)]) + return {"ok": True, "workspace": _workspace_summary(db, workspace)} + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.get("/api/legacy-projects/{project_id}") + def get_project_api(project_id: str) -> Dict[str, Any]: + try: + db = _get_db() + _mirror_runtime_sessions(db) + workspace = db.get_workspace(project_id, user_id=_ui_user_id()) + if not workspace: + raise HTTPException(status_code=404, detail="Workspace not found") + return {"live": True, "workspace": _workspace_summary(db, workspace)} + except HTTPException: + raise + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.get("/api/projects/{project_id}/canvas") + def project_canvas_api(project_id: str) -> Dict[str, Any]: + try: + return _build_project_canvas_payload(project_id) + except HTTPException: + raise + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.get("/api/projects/{project_id}/workspaces") + def list_project_workspaces_api(project_id: str) -> Dict[str, Any]: + try: + db = _get_db() + _mirror_runtime_sessions(db) + rows = db.list_project_workspaces( + user_id=_ui_user_id(), + project_id=project_id, + limit=100, + ) + return { + "live": True, + "workspaces": [_workspace_summary(db, row) for row in rows], + } + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.post("/api/projects/{project_id}/workspaces") + def create_project_workspace_api( + project_id: str, + payload: WorkspaceCreatePayload, + ) -> Dict[str, Any]: + try: + db = _get_db() + workspace_path = _abs_user_path(payload.workspace_path) + if not workspace_path: + raise HTTPException(status_code=400, detail="workspace_path is required") + workspace = db.upsert_project_workspace( + { + "user_id": _ui_user_id(), + "project_id": project_id, + "workspace_path": workspace_path, + "label": payload.label or (os.path.basename(workspace_path) or workspace_path), + "folder_path": payload.folder_path or ".", + "is_primary": payload.is_primary, + "metadata": { + "created_via": "sankhya-ui", + "folders": [ + {"path": os.path.abspath(os.path.expanduser(folder))} + for folder in (payload.folders or []) + if str(folder).strip() + ], + }, + } + ) + _mirror_runtime_sessions(db, extra_paths=[workspace_path]) + return {"ok": True, "workspace": _workspace_summary(db, workspace)} + except HTTPException: + raise + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.get("/api/workspaces/{workspace_id}/projects") + def list_workspace_projects_api(workspace_id: str) -> Dict[str, Any]: + try: + db = _get_db() + _mirror_runtime_sessions(db) + workspace = db.get_workspace(workspace_id, user_id=_ui_user_id()) + if not workspace: + raise HTTPException(status_code=404, detail="Workspace not found") + projects = [ + _project_summary(db, project) + for project in db.list_workspace_projects( + workspace_id=workspace_id, + user_id=_ui_user_id(), + limit=100, + ) + ] + return {"live": True, "projects": projects} + except HTTPException: + raise + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.post("/api/workspaces/{workspace_id}/projects") + def create_workspace_project_api( + workspace_id: str, + payload: WorkspaceProjectCreatePayload, + ) -> Dict[str, Any]: + try: + db = _get_db() + workspace = db.get_workspace(workspace_id, user_id=_ui_user_id()) + if not workspace: + raise HTTPException(status_code=404, detail="Workspace not found") + project_name = _display_name(payload.name, fallback="Project") + if not project_name: + raise HTTPException(status_code=400, detail="Project name is required") + project = db.upsert_workspace_project( + { + "workspace_id": workspace_id, + "user_id": _ui_user_id(), + "name": project_name, + "description": payload.description, + "default_runtime": payload.default_runtime or "codex", + "color": payload.color, + "icon": payload.icon, + "metadata": {"created_via": "sankhya-ui"}, + } + ) + rules = _normalize_scope_rules(payload.scope_rules) + db.replace_workspace_project_scope_rules( + project_id=str(project.get("id") or ""), + user_id=_ui_user_id(), + rules=rules, + ) + _mirror_runtime_sessions( + db, + extra_paths=[str(rule.get("path_prefix") or "") for rule in rules], + ) + return {"ok": True, "project": _project_summary(db, project)} + except HTTPException: + raise + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.get("/api/projects/{project_id}") + def get_workspace_project_api(project_id: str) -> Dict[str, Any]: + try: + db = _get_db() + _mirror_runtime_sessions(db) + project = db.get_workspace_project(project_id, user_id=_ui_user_id()) + if not project: + raise HTTPException(status_code=404, detail="Project not found") + return {"live": True, "project": _project_summary(db, project)} + except HTTPException: + raise + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.patch("/api/projects/{project_id}") + def update_workspace_project_api( + project_id: str, + payload: WorkspaceProjectUpdatePayload, + ) -> Dict[str, Any]: + try: + db = _get_db() + project = db.get_workspace_project(project_id, user_id=_ui_user_id()) + if not project: + raise HTTPException(status_code=404, detail="Project not found") + updated = db.upsert_workspace_project( + { + "id": project_id, + "workspace_id": project.get("workspace_id"), + "user_id": _ui_user_id(), + "name": str(payload.name or project.get("name") or "").strip() or project.get("name"), + "description": payload.description if payload.description is not None else project.get("description"), + "default_runtime": payload.default_runtime or project.get("default_runtime") or "codex", + "color": payload.color if payload.color is not None else project.get("color"), + "icon": payload.icon if payload.icon is not None else project.get("icon"), + "metadata": dict(project.get("metadata") or {}), + } + ) + scan_paths: List[str] = [] + if payload.scope_rules is not None: + rules = _normalize_scope_rules(payload.scope_rules) + db.replace_workspace_project_scope_rules( + project_id=project_id, + user_id=_ui_user_id(), + rules=rules, + ) + scan_paths.extend(str(rule.get("path_prefix") or "") for rule in rules) + workspace = db.get_workspace(str(project.get("workspace_id") or ""), user_id=_ui_user_id()) + if workspace: + scan_paths.append(_workspace_primary_path(workspace)) + _mirror_runtime_sessions(db, extra_paths=scan_paths) + return {"ok": True, "project": _project_summary(db, updated)} + except HTTPException: + raise + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.get("/api/projects/{project_id}/sessions") + def list_workspace_project_sessions_api(project_id: str) -> Dict[str, Any]: + try: + db = _get_db() + _mirror_runtime_sessions(db) + project = db.get_workspace_project(project_id, user_id=_ui_user_id()) + if not project: + raise HTTPException(status_code=404, detail="Project not found") + sessions = [ + _session_summary(db, session) + for session in _workspace_project_sessions(db, project) + ] + return {"live": True, "sessions": sessions} + except HTTPException: + raise + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.post("/api/folders/pick") + def pick_folder_api(payload: Optional[FolderPickPayload] = None) -> Dict[str, Any]: + try: + prompt = (payload.prompt if payload else None) or "Select folder" + safe_prompt = prompt.replace("\\", "\\\\").replace('"', '\\"') + script = ( + f'set chosenFolder to choose folder with prompt "{safe_prompt}"\n' + "POSIX path of chosenFolder" + ) + result = subprocess.run( + ["osascript", "-e", script], + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + detail = (result.stderr or result.stdout or "").strip() + if "User canceled" in detail or "User cancelled" in detail: + return {"ok": False, "cancelled": True} + raise HTTPException(status_code=400, detail=detail or "Folder picker failed") + return {"ok": True, "path": (result.stdout or "").strip()} + except HTTPException: + raise + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.post("/api/workspaces/{workspace_id}/folders") + @app.post("/api/workspaces/{workspace_id}/mounts") + def add_workspace_folder_api( + workspace_id: str, + payload: WorkspaceFolderPayload, + ) -> Dict[str, Any]: + try: + db = _get_db() + workspace = db.get_workspace(workspace_id, user_id=_ui_user_id()) + if not workspace: + raise HTTPException(status_code=404, detail="Workspace not found") + resolved = _abs_user_path(payload.path) + if not resolved: + raise HTTPException(status_code=400, detail="path is required") + mounts = db.list_workspace_mounts(workspace_id=workspace_id, user_id=_ui_user_id()) + if not any(str(mount.get("mount_path") or "") == resolved for mount in mounts): + db.upsert_workspace_mount( + { + "workspace_id": workspace_id, + "user_id": _ui_user_id(), + "mount_path": resolved, + "label": payload.label or os.path.basename(resolved) or resolved, + "is_primary": not mounts, + } + ) + updated = db.get_workspace(workspace_id, user_id=_ui_user_id()) + if not updated: + raise HTTPException(status_code=404, detail="Workspace not found") + _mirror_runtime_sessions(db, extra_paths=[resolved]) + return {"ok": True, "workspace": _workspace_summary(db, updated)} + except HTTPException: + raise + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.patch("/api/workspaces/{workspace_id}") + def update_workspace_api( + workspace_id: str, + payload: WorkspaceUpdatePayload, + ) -> Dict[str, Any]: + try: + db = _get_db() + workspace = db.get_workspace(workspace_id, user_id=_ui_user_id()) + if not workspace: + raise HTTPException(status_code=404, detail="Workspace not found") + next_name = str(payload.label or "").strip() or str(workspace.get("name") or "").strip() + if not next_name: + raise HTTPException(status_code=400, detail="Workspace name is required") + next_description = ( + payload.description + if payload.description is not None + else workspace.get("description") + ) + next_root = ( + _abs_user_path(payload.root_path) + if payload.root_path is not None and str(payload.root_path).strip() + else _workspace_primary_path(workspace) + ) + updated = db.upsert_workspace( + { + "id": workspace_id, + "user_id": _ui_user_id(), + "name": next_name, + "description": next_description, + "root_path": next_root, + "metadata": dict(workspace.get("metadata") or {}), + } + ) + # Keep the mount table in sync so the workspace graph, + # asset drawer path resolution, and folder list all see + # the new primary root immediately. + if payload.root_path is not None and str(payload.root_path).strip(): + resolved_root = _abs_user_path(next_root) + try: + # Demote any pre-existing primary mounts to non-primary. + for mount in db.list_workspace_mounts(workspace_id=workspace_id, user_id=_ui_user_id()): + if mount.get("is_primary") and str(mount.get("mount_path") or "") != resolved_root: + db.upsert_workspace_mount( + { + "workspace_id": workspace_id, + "user_id": _ui_user_id(), + "mount_path": str(mount.get("mount_path") or ""), + "label": mount.get("label"), + "is_primary": False, + } + ) + db.upsert_workspace_mount( + { + "workspace_id": workspace_id, + "user_id": _ui_user_id(), + "mount_path": resolved_root, + "label": os.path.basename(resolved_root.rstrip(os.sep)) or resolved_root, + "is_primary": True, + } + ) + except Exception: + pass + _mirror_runtime_sessions(db, extra_paths=[next_root]) + return {"ok": True, "workspace": _workspace_summary(db, updated)} + except HTTPException: + raise + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.delete("/api/workspaces/{workspace_id}") + def delete_workspace_api(workspace_id: str) -> Dict[str, Any]: + """Delete a workspace and everything scoped to it. + + Cascades through projects, mounts, line messages, project assets, + and session assets. We do the cascade application-side so we can + surface a friendly error if any step fails instead of leaving a + half-deleted workspace behind. + """ + try: + db = _get_db() + workspace = db.get_workspace(workspace_id, user_id=_ui_user_id()) + if not workspace: + raise HTTPException(status_code=404, detail="Workspace not found") + ok = False + if hasattr(db, "delete_workspace_cascade"): + ok = bool(db.delete_workspace_cascade(workspace_id, user_id=_ui_user_id())) + else: + raise HTTPException(status_code=501, detail="Cascade delete not supported by this DB") + return {"ok": ok, "id": workspace_id} + except HTTPException: + raise + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.delete("/api/projects/{project_id}") + def delete_project_api(project_id: str) -> Dict[str, Any]: + """Delete a project and its project-scoped assets + line messages. + + Workspace survives. Project-linked assets get their storage + files removed best-effort; anything that fails logs silently + rather than aborting the delete. + """ + try: + db = _get_db() + project = db.get_workspace_project(project_id, user_id=_ui_user_id()) + if not project: + raise HTTPException(status_code=404, detail="Project not found") + ok = False + if hasattr(db, "delete_workspace_project_cascade"): + # Best-effort asset file cleanup before the DB cascade drops rows. + try: + for asset in db.list_project_assets(project_id=project_id, user_id=_ui_user_id()): + storage_path = str(asset.get("storage_path") or "") + if storage_path and os.path.exists(storage_path): + try: + os.remove(storage_path) + except Exception: + pass + except Exception: + pass + ok = bool(db.delete_workspace_project_cascade(project_id, user_id=_ui_user_id())) + else: + raise HTTPException(status_code=501, detail="Cascade delete not supported by this DB") + return {"ok": ok, "id": project_id, "workspace_id": project.get("workspace_id")} + except HTTPException: + raise + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.delete("/api/workspaces/{workspace_id}/folders") + @app.delete("/api/workspaces/{workspace_id}/mounts") + def remove_workspace_folder_api(workspace_id: str, path: str) -> Dict[str, Any]: + try: + db = _get_db() + workspace = db.get_workspace(workspace_id, user_id=_ui_user_id()) + if not workspace: + raise HTTPException(status_code=404, detail="Workspace not found") + resolved = os.path.abspath(os.path.expanduser(path)) + mounts = _workspace_folder_mounts( + {**workspace, "mounts": db.list_workspace_mounts(workspace_id=workspace_id, user_id=_ui_user_id())} + ) + retained = [mount for mount in mounts if str(mount.get("path") or "") != resolved] + if not retained: + raise HTTPException(status_code=400, detail="Workspace must keep at least one mounted folder") + primary_mount = next((mount for mount in retained if mount.get("primary")), None) or retained[0] + for mount in mounts: + db.delete_workspace_mount( + workspace_id=workspace_id, + mount_path=str(mount.get("path") or ""), + user_id=_ui_user_id(), + ) + for mount in retained: + db.upsert_workspace_mount( + { + "workspace_id": workspace_id, + "user_id": _ui_user_id(), + "mount_path": mount["path"], + "label": mount.get("label") or os.path.basename(mount["path"]) or mount["path"], + "is_primary": mount["path"] == primary_mount["path"], + } + ) + updated = db.upsert_workspace( + { + "id": workspace_id, + "user_id": _ui_user_id(), + "name": workspace.get("name"), + "description": workspace.get("description"), + "root_path": primary_mount["path"], + "metadata": dict(workspace.get("metadata") or {}), + } + ) + return {"ok": True, "workspace": _workspace_summary(db, updated)} + except HTTPException: + raise + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.get("/api/workspaces/{workspace_id}") + def get_workspace_api(workspace_id: str) -> Dict[str, Any]: + try: + db = _get_db() + _mirror_runtime_sessions(db) + return _workspace_detail_payload(db, workspace_id) + except HTTPException: + raise + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.get("/api/workspaces/{workspace_id}/sessions") + def list_workspace_sessions_api(workspace_id: str) -> Dict[str, Any]: + try: + db = _get_db() + _mirror_runtime_sessions(db) + workspace = db.get_workspace(workspace_id, user_id=_ui_user_id()) + if not workspace: + raise HTTPException(status_code=404, detail="Workspace not found") + sessions = _workspace_sessions(db, workspace)[:100] + return { + "live": True, + "sessions": [_session_summary(db, session) for session in sessions], + } + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.get("/api/workspaces/{workspace_id}/canvas") + def get_workspace_canvas_api(workspace_id: str) -> Dict[str, Any]: + try: + return _build_workspace_canvas_payload(workspace_id) + except HTTPException: + raise + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.get("/api/sessions/{session_id}") + def get_session_api(session_id: str) -> Dict[str, Any]: + try: + db = _get_db() + _mirror_runtime_sessions(db) + return _session_detail_payload(db, session_id) + except HTTPException: + raise + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.get("/api/sessions/{session_id}/messages") + def get_session_messages_api(session_id: str) -> Dict[str, Any]: + try: + db = _get_db() + _mirror_runtime_sessions(db) + session = db.get_agent_session(session_id) + if not session: + raise HTTPException(status_code=404, detail="Session not found") + return {"live": True, "messages": _session_messages_from_agent_session(session)} + except HTTPException: + raise + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.post("/api/sessions/{session_id}/assets") + async def upload_session_asset_api( + session_id: str, + file: UploadFile = File(...), + label: Optional[str] = Form(None), + ) -> Dict[str, Any]: + try: + db = _get_db() + session = db.get_agent_session(session_id) + if not session: + raise HTTPException(status_code=404, detail="Session not found") + asset_root = Path(_dhee_data_dir_str()) / "session_assets" / session_id + asset_root.mkdir(parents=True, exist_ok=True) + filename = os.path.basename(file.filename or "asset") + stored = asset_root / f"{int(time.time() * 1000)}-{filename}" + size_bytes = 0 + with stored.open("wb") as handle: + while True: + chunk = await file.read(1024 * 1024) + if not chunk: + break + size_bytes += len(chunk) + handle.write(chunk) + artifact_id = None + try: + from dhee.core.artifacts import ArtifactManager + + manager = ArtifactManager(db) + extracted_text = _extract_asset_text(str(stored), file.content_type) + if extracted_text: + parsed = manager.capture_host_parse( + path=str(stored), + extracted_text=extracted_text, + user_id=_ui_user_id(), + cwd=str(session.get("cwd") or session.get("workspace_id") or ""), + harness=str(session.get("runtime_id") or "dhee"), + extraction_source="sankhya-upload", + project_id=session.get("project_id"), + metadata={ + "label": label or filename, + "session_id": session_id, + "uploaded_via": "sankhya-ui", + }, + ) + artifact_id = str((parsed or {}).get("artifact_id") or "") or None + else: + attached = manager.attach( + str(stored), + user_id=_ui_user_id(), + cwd=str(session.get("cwd") or session.get("workspace_id") or ""), + harness=str(session.get("runtime_id") or "dhee"), + project_id=session.get("project_id"), + metadata={ + "label": label or filename, + "session_id": session_id, + "uploaded_via": "sankhya-ui", + }, + ) + artifact_id = str((attached or {}).get("artifact_id") or "") or None + except Exception: + artifact_id = None + asset = db.add_session_asset( + { + "project_id": session.get("project_id"), + "workspace_id": session.get("workspace_id"), + "session_id": session_id, + "user_id": _ui_user_id(), + "artifact_id": artifact_id, + "storage_path": str(stored), + "name": label or filename, + "mime_type": file.content_type, + "size_bytes": size_bytes, + "metadata": {"uploaded_via": "sankhya-ui", "original_name": filename}, + } + ) + return {"ok": True, "asset": asset} + except HTTPException: + raise + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + # --------- Project / workspace assets (PR 3) ------------------------ + # Distinct from session assets: these live with the project and are + # visible to every agent the workspace ever runs. Dedup by SHA-256 + # within (workspace, project) so re-uploads don't duplicate storage. + + async def _persist_project_asset( + *, + workspace_id: str, + project_id: Optional[str], + folder: Optional[str], + file: UploadFile, + label: Optional[str], + ) -> Dict[str, Any]: + db = _get_db() + workspace = db.get_workspace(workspace_id, user_id=_ui_user_id()) + if not workspace: + raise HTTPException(status_code=404, detail="Workspace not found") + if project_id: + project = db.get_workspace_project(project_id, user_id=_ui_user_id()) + if not project or str(project.get("workspace_id") or "") != workspace_id: + raise HTTPException(status_code=404, detail="Project not found in workspace") + + asset_root = ( + Path(_dhee_data_dir_str()) + / "project_assets" + / workspace_id + / (project_id or "_workspace") + ) + asset_root.mkdir(parents=True, exist_ok=True) + filename = os.path.basename(file.filename or "asset") + stored = asset_root / f"{int(time.time() * 1000)}-{filename}" + + size_bytes = 0 + hasher = hashlib.sha256() + with stored.open("wb") as handle: + while True: + chunk = await file.read(1024 * 1024) + if not chunk: + break + size_bytes += len(chunk) + hasher.update(chunk) + handle.write(chunk) + checksum = hasher.hexdigest() + + artifact_id: Optional[str] = None + try: + from dhee.core.artifacts import ArtifactManager + + manager = ArtifactManager(db) + extracted_text = _extract_asset_text(str(stored), file.content_type) + if extracted_text: + parsed = manager.capture_host_parse( + path=str(stored), + extracted_text=extracted_text, + user_id=_ui_user_id(), + cwd=str(workspace.get("root_path") or ""), + harness="dhee", + extraction_source="sankhya-upload", + project_id=project_id, + metadata={ + "label": label or filename, + "uploaded_via": "sankhya-ui", + "scope": "project" if project_id else "workspace", + }, + ) + artifact_id = str((parsed or {}).get("artifact_id") or "") or None + else: + attached = manager.attach( + str(stored), + user_id=_ui_user_id(), + cwd=str(workspace.get("root_path") or ""), + harness="dhee", + project_id=project_id, + metadata={ + "label": label or filename, + "uploaded_via": "sankhya-ui", + "scope": "project" if project_id else "workspace", + }, + ) + artifact_id = str((attached or {}).get("artifact_id") or "") or None + except Exception: + artifact_id = None + + asset = db.upsert_project_asset( + { + "workspace_id": workspace_id, + "project_id": project_id, + "user_id": _ui_user_id(), + "artifact_id": artifact_id, + "folder": folder, + "storage_path": str(stored), + "name": label or filename, + "mime_type": file.content_type, + "size_bytes": size_bytes, + "checksum": checksum, + "metadata": { + "uploaded_via": "sankhya-ui", + "original_name": filename, + "scope": "project" if project_id else "workspace", + }, + } + ) + + # Announce the upload on the workspace information line so sibling + # agents see that a new asset is available. + try: + from dhee.core.workspace_line import emit_agent_activity + + emit_agent_activity( + db, + user_id=_ui_user_id(), + tool_name="Upload", + packet_kind="asset_upload", + digest=f"asset uploaded · {asset.get('name')}", + cwd=str(workspace.get("root_path") or ""), + source_path=str(stored), + source_event_id=str(asset.get("id") or ""), + artifact_id=artifact_id, + harness="dhee", + agent_id="dhee", + metadata={ + "asset_id": asset.get("id"), + "scope": "project" if project_id else "workspace", + "project_id": project_id, + "workspace_id": workspace_id, + "size_bytes": size_bytes, + "checksum": checksum, + }, + ) + except Exception: + pass + + return asset + + def _project_asset_results_payload( + db: Any, asset: Dict[str, Any], *, limit: int = 16 + ) -> List[Dict[str, Any]]: + """Recent shared-task results that mention this asset's storage path. + + This is what makes the drawer feel alive: "Claude read it 4m ago", + "Codex grepped it 12m ago". We match on storage_path (exact) and + on the matching artifact_id when present, to cover the router + path (which only knows ptrs) and the hook path (which knows files). + """ + storage_path = str(asset.get("storage_path") or "") + if not storage_path: + return [] + # Match only on source_path. `shared_tasks.workspace_id` is a path + # string for router-tracked sessions, not a UUID — filtering on + # `project_assets.workspace_id` (a UUID) would always miss. + # source_path is unique enough. + try: + return db.list_shared_task_results_for_path( + user_id=_ui_user_id(), + source_path=storage_path, + limit=limit, + ) + except Exception: + return [] + + def _project_asset_to_payload(db: Any, asset: Dict[str, Any]) -> Dict[str, Any]: + return { + **asset, + "results": _project_asset_results_payload(db, asset), + } + + @app.get("/api/projects/{project_id}/assets") + def list_project_assets_api(project_id: str, limit: int = 100) -> Dict[str, Any]: + try: + db = _get_db() + project = db.get_workspace_project(project_id, user_id=_ui_user_id()) + if not project: + raise HTTPException(status_code=404, detail="Project not found") + assets = db.list_project_assets( + project_id=project_id, user_id=_ui_user_id(), limit=limit + ) + return { + "live": True, + "project_id": project_id, + "workspace_id": project.get("workspace_id"), + "assets": [_project_asset_to_payload(db, asset) for asset in assets], + } + except HTTPException: + raise + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.post("/api/projects/{project_id}/assets") + async def upload_project_asset_api( + project_id: str, + file: UploadFile = File(...), + label: Optional[str] = Form(None), + folder: Optional[str] = Form(None), + ) -> Dict[str, Any]: + try: + db = _get_db() + project = db.get_workspace_project(project_id, user_id=_ui_user_id()) + if not project: + raise HTTPException(status_code=404, detail="Project not found") + asset = await _persist_project_asset( + workspace_id=str(project.get("workspace_id") or ""), + project_id=project_id, + folder=folder, + file=file, + label=label, + ) + return {"ok": True, "asset": _project_asset_to_payload(db, asset)} + except HTTPException: + raise + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.get("/api/workspaces/{workspace_id}/assets") + def list_workspace_assets_api( + workspace_id: str, + include_project_assets: bool = True, + limit: int = 200, + ) -> Dict[str, Any]: + try: + db = _get_db() + workspace = db.get_workspace(workspace_id, user_id=_ui_user_id()) + if not workspace: + raise HTTPException(status_code=404, detail="Workspace not found") + assets = db.list_workspace_assets( + workspace_id=workspace_id, + user_id=_ui_user_id(), + include_project_assets=include_project_assets, + limit=limit, + ) + return { + "live": True, + "workspace_id": workspace_id, + "assets": [_project_asset_to_payload(db, asset) for asset in assets], + } + except HTTPException: + raise + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.post("/api/workspaces/{workspace_id}/assets") + async def upload_workspace_asset_api( + workspace_id: str, + file: UploadFile = File(...), + label: Optional[str] = Form(None), + folder: Optional[str] = Form(None), + project_id: Optional[str] = Form(None), + ) -> Dict[str, Any]: + try: + db = _get_db() + asset = await _persist_project_asset( + workspace_id=workspace_id, + project_id=project_id or None, + folder=folder, + file=file, + label=label, + ) + return {"ok": True, "asset": _project_asset_to_payload(db, asset)} + except HTTPException: + raise + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.delete("/api/project-assets/{asset_id}") + def delete_project_asset_api(asset_id: str) -> Dict[str, Any]: + try: + db = _get_db() + asset = db.get_project_asset(asset_id) + if not asset: + raise HTTPException(status_code=404, detail="Asset not found") + ok = db.delete_project_asset(asset_id, user_id=_ui_user_id()) + if ok: + try: + storage_path = str(asset.get("storage_path") or "") + if storage_path and os.path.exists(storage_path): + os.remove(storage_path) + except Exception: + pass + return {"ok": bool(ok)} + except HTTPException: + raise + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.get("/api/files/{file_id:path}/context") + def file_context_api(file_id: str, workspace_id: Optional[str] = None) -> Dict[str, Any]: + try: + return {"live": True, **_file_context_payload(_get_db(), file_id, workspace_id=workspace_id)} + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.get("/api/assets/{asset_id}/context") + def asset_context_api(asset_id: str) -> Dict[str, Any]: + try: + return {"live": True, **_asset_context_payload(_get_db(), asset_id)} + except HTTPException: + raise + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.post("/api/assets/{asset_id}/ask") + def ask_asset_api(asset_id: str, payload: AssetAskPayload) -> Dict[str, Any]: + try: + db = _get_db() + context = _asset_context_payload(db, asset_id) + asset = context.get("asset") or {} + session = context.get("session") or {} + workspace_id = str(asset.get("workspace_id") or session.get("workspace_id") or "") + if not workspace_id: + raise HTTPException(status_code=400, detail="Asset is not attached to a workspace") + question = str(payload.question or "").strip() + if not question: + raise HTTPException(status_code=400, detail="Question is required") + launch = launch_workspace_session( + workspace_id, + SessionLaunchPayload( + runtime="claude-code", + title=f"Ask {asset.get('name') or 'asset'}", + permission_mode="standard", + ), + ) + task_id = str(launch.get("task_id") or "") + if task_id: + chunk_text = "\n\n".join( + f"chunk {chunk.get('chunk_index')}: {chunk.get('content')}" + for chunk in (context.get("chunks") or [])[:4] + ).strip() + db.save_shared_task_result( + { + "shared_task_id": task_id, + "project_id": asset.get("project_id"), + "workspace_id": workspace_id, + "result_key": f"asset-ask:{asset_id}:{int(time.time())}", + "packet_kind": "note", + "tool_name": "user_note", + "result_status": "completed", + "source_path": str(asset.get("storage_path") or ""), + "artifact_id": asset.get("artifact_id"), + "digest": ( + f"Answer the following question using the attached asset '{asset.get('name')}'.\n\n" + f"Question: {question}\n\n" + f"Known extracted context:\n{chunk_text or context.get('summary') or 'No extracted text available yet.'}" + ), + "metadata": { + "source": "asset_ask", + "asset_id": asset_id, + "question": question, + }, + } + ) + return {"ok": True, "launch": launch, "asset": asset, "question": question} + except HTTPException: + raise + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + # ─── Tasks ─────────────────────────────────────────────────────────────── + + @app.get("/api/tasks") + def tasks() -> Dict[str, Any]: + """Repo-scoped shared tasks → UI task cards.""" + try: + db = _get_db() + sync = _mirror_runtime_sessions(db) + user_id = _ui_user_id() + repo = _ui_repo() + rows = db.list_shared_tasks( + user_id=user_id, + project_id=str((sync.get("project") or {}).get("id") or "") or None, + repo=repo, + limit=48, + ) + out = [ + _shared_task_to_ui_task(db, row, index=i) + for i, row in enumerate(rows) + if isinstance(row, dict) + ] + return {"live": True, "tasks": out, "repo": repo} + except Exception as exc: # noqa: BLE001 + log.warning("tasks failed: %s", exc) + return {"live": False, "tasks": [], "error": str(exc)} + + class TaskCreate(BaseModel): + title: str + harness: Optional[str] = None + + class TaskStatusUpdate(BaseModel): + status: str + + class TaskNotePayload(BaseModel): + content: str + + @app.post("/api/tasks") + def create_task(payload: TaskCreate) -> Dict[str, Any]: + try: + db = _get_db() + sync = _mirror_runtime_sessions(db) + task = db.upsert_shared_task( + { + "user_id": _ui_user_id(), + "project_id": (sync.get("project") or {}).get("id"), + "repo": _ui_repo(), + "workspace_id": (sync.get("workspace") or {}).get("id"), + "folder_path": (sync.get("workspace") or {}).get("folder_path"), + "title": payload.title, + "status": "paused", + "created_by": "sankhya-ui", + "metadata": { + "harness": _normalize_runtime(payload.harness), + "created_via": "sankhya-ui", + }, + } + ) + return { + "ok": True, + "task": _shared_task_to_ui_task(db, task, index=0), + } + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.get("/api/tasks/{task_id}") + def task_detail(task_id: str, limit: int = 24) -> Dict[str, Any]: + try: + db = _get_db() + _mirror_runtime_sessions(db) + task = _get_shared_task_or_404(db, task_id) + ui_task = _shared_task_to_ui_task(db, task, index=0, result_limit=limit) + results = db.list_shared_task_results(shared_task_id=task_id, limit=limit) + return { + "live": True, + "task": ui_task, + "results": results, + "runtime": _get_runtime_status_payload(), + } + except HTTPException: + raise + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.post("/api/tasks/{task_id}/status") + def task_status_update(task_id: str, payload: TaskStatusUpdate) -> Dict[str, Any]: + try: + db = _get_db() + _mirror_runtime_sessions(db) + task = _get_shared_task_or_404(db, task_id) + status = str(payload.status or "").strip().lower() + if status not in {"active", "paused", "completed", "closed", "abandoned"}: + raise HTTPException(status_code=400, detail="Unsupported task status") + updated = _touch_shared_task( + db, + task, + status=status, + metadata_updates={"updated_via": "sankhya-ui"}, + ) + return { + "ok": True, + "task": _shared_task_to_ui_task(db, updated, index=0, result_limit=12), + } + except HTTPException: + raise + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.post("/api/tasks/{task_id}/notes") + def task_note_add(task_id: str, payload: TaskNotePayload) -> Dict[str, Any]: + try: + content = str(payload.content or "").strip() + if not content: + raise HTTPException(status_code=400, detail="content is required") + db = _get_db() + _mirror_runtime_sessions(db) + task = _get_shared_task_or_404(db, task_id) + metadata = dict(task.get("metadata") or {}) + result_id = db.save_shared_task_result( + { + "result_key": f"{task_id}:note:{int(time.time() * 1000)}", + "shared_task_id": task_id, + "user_id": _ui_user_id(), + "project_id": task.get("project_id"), + "repo": task.get("repo") or _ui_repo(), + "workspace_id": task.get("workspace_id"), + "folder_path": task.get("folder_path"), + "packet_kind": "note", + "tool_name": "user_note", + "result_status": "completed", + "digest": content, + "metadata": { + "created_via": "sankhya-ui", + "kind": "task_note", + "task_title": task.get("title"), + "harness": metadata.get("harness"), + }, + "harness": _normalize_runtime(metadata.get("harness")), + "agent_id": "sankhya-ui", + } + ) + updated = _touch_shared_task( + db, + task, + metadata_updates={"updated_via": "sankhya-ui"}, + ) + return { + "ok": True, + "task": _shared_task_to_ui_task(db, updated, index=0, result_limit=12), + "result": db.get_shared_task_result(result_id), + } + except HTTPException: + raise + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + # ─── Pointer Capture Memory OS ───────────────────────────────────────── + + @app.post("/api/capture/session/start") + def capture_session_start(payload: CaptureSessionStartPayload) -> Dict[str, Any]: + try: + return _get_memory_os_service().start_capture_session( + user_id=_ui_user_id(), + source_app=payload.source_app, + namespace=payload.namespace, + metadata=payload.metadata, + ) + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.post("/api/capture/session/{session_id}/end") + def capture_session_end(session_id: str, payload: CaptureSessionEndPayload) -> Dict[str, Any]: + try: + return _get_memory_os_service().end_capture_session( + session_id, + distill=payload.distill, + summary_hint=payload.summary_hint or "", + ) + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.post("/api/capture/session/end") + def capture_session_end_legacy(payload: CaptureSessionEndPayload) -> Dict[str, Any]: + session_id = str(payload.session_id or "").strip() + if not session_id: + raise HTTPException(status_code=400, detail="session_id is required") + return capture_session_end(session_id, payload) + + @app.get("/api/capture/session/{session_id}") + def capture_session_get(session_id: str) -> Dict[str, Any]: + try: + return _get_memory_os_service().get_capture_session(session_id) + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=404, detail=str(exc)) from exc + + @app.post("/api/capture/action") + def capture_action(payload: CaptureActionPayload) -> Dict[str, Any]: + try: + return _get_memory_os_service().record_action(payload.model_dump(exclude_none=True)) + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.post("/api/capture/navigation") + def capture_navigation(payload: CaptureActionPayload) -> Dict[str, Any]: + try: + return _get_memory_os_service().record_navigation(payload.model_dump(exclude_none=True)) + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.post("/api/capture/observation") + def capture_observation(payload: CaptureObservationPayload) -> Dict[str, Any]: + try: + return _get_memory_os_service().record_observation(payload.model_dump(exclude_none=True)) + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.post("/api/capture/artifact") + def capture_artifact(payload: CaptureArtifactPayload) -> Dict[str, Any]: + try: + return _get_memory_os_service().record_artifact(payload.model_dump(exclude_none=True)) + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.get("/api/capture/timeline") + def capture_timeline(source_app: Optional[str] = None, limit: int = 30) -> Dict[str, Any]: + try: + return _get_memory_os_service().timeline( + user_id=_ui_user_id(), + source_app=source_app, + limit=limit, + ) + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.get("/api/capture/preferences") + def capture_preferences() -> Dict[str, Any]: + try: + return _get_memory_os_service().list_capture_policies() + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.post("/api/capture/preferences") + def capture_preferences_set(payload: CapturePreferencePayload) -> Dict[str, Any]: + try: + return _get_memory_os_service().set_capture_policy( + source_app=payload.source_app, + enabled=payload.enabled, + mode=payload.mode, + metadata=payload.metadata, + ) + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.get("/api/memory/now") + def memory_now(source_app: Optional[str] = None, limit: int = 8) -> Dict[str, Any]: + try: + return _get_memory_os_service().memory_now( + user_id=_ui_user_id(), + source_app=source_app, + limit=limit, + ) + except Exception as exc: # noqa: BLE001 + return {"live": False, "memories": [], "engrams": [], "error": str(exc)} + + @app.post("/api/memory/ask") + def memory_ask(payload: MemoryAskPayload) -> Dict[str, Any]: + try: + return _get_memory_os_service().memory_ask( + user_id=_ui_user_id(), + query=payload.query, + source_app=payload.source_app, + limit=payload.limit, + ) + except Exception as exc: # noqa: BLE001 + return {"live": False, "answer": "", "memories": [], "error": str(exc)} + + @app.post("/api/world-memory/context-pack") + def world_context_pack(payload: WorldContextPackPayload) -> Dict[str, Any]: + try: + return _get_memory_os_service().world_context_pack( + user_id=_ui_user_id(), + current_frame_ref=payload.current_frame_ref, + current_context_text=payload.current_context_text, + task_instruction=payload.task_instruction, + recent_actions=payload.recent_actions, + limit=payload.limit, + ) + except Exception as exc: # noqa: BLE001 + return {"live": False, "context": [], "error": str(exc)} + + @app.post("/api/agents/context-pack") + def agents_context_pack(payload: AgentContextPackPayload) -> Dict[str, Any]: + try: + return _get_memory_os_service().agent_context_pack( + user_id=_ui_user_id(), + agent_id=payload.agent_id, + task_instruction=payload.task_instruction, + source_app=payload.source_app, + current_frame_ref=payload.current_frame_ref or "", + current_context_text=payload.current_context_text or "", + recent_actions=payload.recent_actions, + limit=payload.limit, + ) + except Exception as exc: # noqa: BLE001 + return {"live": False, "context": [], "error": str(exc)} + + # ─── Security / API Keys ──────────────────────────────────────────────── + + class ApiKeyPayload(BaseModel): + provider: str + apiKey: str + label: Optional[str] = None + + class RotateApiKeyPayload(BaseModel): + apiKey: str + label: Optional[str] = None + + @app.get("/api/security/api-keys") + def list_api_keys() -> Dict[str, Any]: + try: + from dhee import secret_store + + return {"live": True, "providers": secret_store.list_provider_statuses()} + except Exception as exc: # noqa: BLE001 + log.warning("list_api_keys failed: %s", exc) + return {"live": False, "providers": [], "error": str(exc)} + + @app.post("/api/security/api-keys") + def store_api_key(payload: ApiKeyPayload) -> Dict[str, Any]: + try: + from dhee import secret_store + + provider = secret_store.store_api_key( + payload.provider, + payload.apiKey, + label=payload.label, + ) + return {"ok": True, "provider": provider} + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.post("/api/security/api-keys/{provider}/rotate") + def rotate_api_key(provider: str, payload: RotateApiKeyPayload) -> Dict[str, Any]: + try: + from dhee import secret_store + + rotated = secret_store.rotate_api_key( + provider, + payload.apiKey, + label=payload.label, + ) + return {"ok": True, "provider": rotated} + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + # ─── Status + Launch ───────────────────────────────────────────────────── + + @app.get("/api/status") + def status() -> Dict[str, Any]: + try: + from dhee.router import stats as rstats + + s = rstats.compute_stats() + return { + "ok": True, + "router": { + "sessions": s.sessions, + "calls": s.total_calls, + "tokensSaved": s.est_tokens_diverted, + }, + "dhee_data_dir": _dhee_data_dir_str(), + } + except Exception as exc: # noqa: BLE001 + return {"ok": False, "error": str(exc)} + + @app.get("/api/runtime-status") + def runtime_status() -> Dict[str, Any]: + try: + return _get_runtime_status_payload() + except Exception as exc: # noqa: BLE001 + log.warning("runtime_status failed: %s", exc) + return { + "live": False, + "repo": _ui_repo(), + "runtimes": [], + "error": str(exc), + } + + class LaunchPayload(BaseModel): + taskId: Optional[str] = None + runtime: str # claude-code | codex | both + title: Optional[str] = None + permission_mode: Optional[str] = None + + @app.post("/api/workspaces/{workspace_id}/sessions/launch") + def launch_workspace_session( + workspace_id: str, + payload: SessionLaunchPayload, + ) -> Dict[str, Any]: + runtime = _normalize_runtime(payload.runtime) + if runtime is None: + raise HTTPException(status_code=400, detail="Unsupported runtime") + try: + db = _get_db() + _mirror_runtime_sessions(db) + workspace = db.get_workspace(workspace_id, user_id=_ui_user_id()) + if not workspace: + raise HTTPException(status_code=404, detail="Workspace not found") + project = None + if payload.project_id: + project = db.get_workspace_project(payload.project_id, user_id=_ui_user_id()) + if not project: + project = _resolve_workspace_project_for_path( + db, + workspace_id=workspace_id, + path=_workspace_primary_path(workspace), + ) + if not project: + projects = db.list_workspace_projects( + workspace_id=workspace_id, + user_id=_ui_user_id(), + limit=20, + ) + project = (projects[0] if projects else None) or _ensure_unassigned_workspace_project(db, workspace) + existing = None + if payload.task_id: + existing = db.get_shared_task(payload.task_id, user_id=_ui_user_id()) + title = str(payload.title or (existing or {}).get("title") or "").strip() + if not title: + raise HTTPException(status_code=400, detail="Task title is required") + permission_mode = _normalize_permission_mode(runtime, payload.permission_mode) + native_seed = f"{runtime}:{workspace_id}:{int(time.time() * 1000)}" + session_id = _agent_session_id(runtime, native_seed) + launch_cwd = _workspace_primary_path(workspace) + scope_rules = _workspace_project_scope_rules(db, str(project.get("id") or "")) + if scope_rules: + launch_cwd = str((scope_rules[0] or {}).get("path_prefix") or launch_cwd) + task = db.upsert_shared_task( + { + "id": (existing or {}).get("id") or _session_task_id(runtime, native_seed), + "user_id": _ui_user_id(), + "project_id": project.get("id") if project else None, + "repo": _ui_repo(), + "workspace_id": workspace_id, + "folder_path": ".", + "session_id": session_id, + "thread_id": native_seed, + "runtime_id": runtime, + "native_session_id": native_seed, + "title": title, + "status": "active", + "created_by": "sankhya-ui", + "metadata": { + "harness": runtime, + "created_via": "sankhya-ui", + "launch_requested": True, + "permission_mode": permission_mode, + }, + } + ) + session = db.upsert_agent_session( + { + "id": session_id, + "project_id": project.get("id") if project else None, + "workspace_id": workspace_id, + "user_id": _ui_user_id(), + "runtime_id": runtime, + "native_session_id": native_seed, + "task_id": task.get("id"), + "title": title, + "state": "launch-requested", + "cwd": launch_cwd, + "permission_mode": permission_mode, + "updated_at": _now_iso(), + "metadata": { + "messages": [], + "recent_tools": [], + "plan": [], + "touched_files": [], + "rate_limits": {}, + "preview": "Launch requested from Dhee.", + }, + } + ) + if runtime == "claude-code": + launch_command = f'cd "{launch_cwd}" && claude' + if permission_mode == "full-access": + launch_command += " --dangerously-skip-permissions" + elif runtime == "codex": + launch_command = f'cd "{launch_cwd}" && codex' + else: + launch_command = f'cd "{launch_cwd}" && dhee install --harness all' + return { + "ok": True, + "project_id": project.get("id") if project else None, + "workspace_id": workspace_id, + "session_id": session.get("id"), + "task_id": task.get("id"), + "runtime": runtime, + "permission_mode": permission_mode, + "launch_command": launch_command, + "control_state": "mirrored", + "session": _session_summary(db, session), + "task": _shared_task_to_ui_task(db, task, index=0, result_limit=12), + } + except HTTPException: + raise + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=500, detail=str(exc)) from exc + + @app.get("/api/workspaces/{workspace_id}/line/messages") + def list_workspace_line_messages_api( + workspace_id: str, + project_id: Optional[str] = None, + channel: Optional[str] = None, + cursor: Optional[str] = None, + limit: int = 50, + ) -> Dict[str, Any]: + try: + db = _get_db() + rows = db.list_workspace_line_messages( + workspace_id=workspace_id, + user_id=_ui_user_id(), + project_id=project_id, + channel=channel, + cursor=cursor, + limit=limit, + ) + return { + "live": True, + "messages": rows, + "nextCursor": _line_cursor(rows[-1] if rows else None), + } + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.post("/api/workspaces/{workspace_id}/line/messages") + def publish_workspace_line_message_api( + workspace_id: str, + payload: WorkspaceLineMessagePayload, + ) -> Dict[str, Any]: + try: + db = _get_db() + workspace = db.get_workspace(workspace_id, user_id=_ui_user_id()) + if not workspace: + raise HTTPException(status_code=404, detail="Workspace not found") + source_project_id = str(payload.project_id or "").strip() or None + target_project_id = str(payload.target_project_id or "").strip() or None + suggested_task = None + if target_project_id and target_project_id != source_project_id: + suggested_task = _create_suggested_task_from_broadcast( + db, + workspace_id=workspace_id, + project_id=target_project_id, + source_project_id=source_project_id, + title=str(payload.title or "Workspace broadcast").strip() or "Workspace broadcast", + body=str(payload.body or "").strip(), + session_id=payload.session_id, + ) + message = db.add_workspace_line_message( + { + "workspace_id": workspace_id, + "project_id": source_project_id, + "target_project_id": target_project_id, + "user_id": _ui_user_id(), + "channel": payload.channel or ("project" if source_project_id else "workspace"), + "session_id": payload.session_id, + "task_id": (suggested_task or {}).get("id") or payload.task_id, + "message_kind": payload.message_kind, + "title": payload.title, + "body": payload.body, + "metadata": payload.metadata or {}, + } + ) + # Fan this onto the in-process bus so every SSE subscriber on + # this workspace gets it in the same tick — no 1s poll lag. + if message: + try: + from dhee.core.workspace_line_bus import publish as _publish_bus + + _publish_bus(message) + except Exception: + pass + return { + "ok": True, + "message": message, + "suggestedTask": _shared_task_to_ui_task(db, suggested_task, index=0, result_limit=8) + if suggested_task + else None, + } + except HTTPException: + raise + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.get("/api/workspaces/{workspace_id}/line/stream") + async def workspace_line_stream_api( + workspace_id: str, + project_id: Optional[str] = None, + channel: Optional[str] = None, + cursor: Optional[str] = None, + backfill: int = 0, + ) -> StreamingResponse: + """Real-time line stream via an in-process pub/sub bus. + + Replaces the prior 1s DB-poll loop. Each subscription gets + messages pushed in the same event-loop tick as the write, with + heartbeat keep-alives every 15s. A best-effort ``backfill`` + parameter lets the client catch up on the N most recent + messages on (re)connect without a separate REST call. + """ + from dhee.core.workspace_line_bus import iter_messages + + async def gen(): + db = _get_db() + # Optional backfill — send the most recent N messages so a + # reconnect doesn't require a separate REST hop. + if backfill and backfill > 0: + try: + rows = db.list_workspace_line_messages( + workspace_id=workspace_id, + user_id=_ui_user_id(), + project_id=project_id, + channel=channel, + cursor=cursor, + limit=min(int(backfill), 100), + ) + for row in reversed(rows): + yield f"data: {json.dumps(row)}\n\n" + except Exception: + pass + + try: + async for message in iter_messages( + workspace_id=workspace_id, + project_id=project_id, + channel=channel, + ): + if message is None: + yield ": keep-alive\n\n" + else: + yield f"data: {json.dumps(message)}\n\n" + except asyncio.CancelledError: + return + + return StreamingResponse(gen(), media_type="text/event-stream") + + @app.post("/api/projects/{project_id}/tasks/from-broadcast") + def create_task_from_broadcast_api( + project_id: str, + payload: WorkspaceLineMessagePayload, + ) -> Dict[str, Any]: + try: + db = _get_db() + project = db.get_workspace_project(project_id, user_id=_ui_user_id()) + if not project: + raise HTTPException(status_code=404, detail="Project not found") + task = _create_suggested_task_from_broadcast( + db, + workspace_id=str(project.get("workspace_id") or ""), + project_id=project_id, + source_project_id=str(payload.project_id or "").strip() or None, + title=str(payload.title or "Workspace broadcast").strip() or "Workspace broadcast", + body=str(payload.body or "").strip(), + session_id=payload.session_id, + ) + return {"ok": True, "task": _shared_task_to_ui_task(db, task, index=0, result_limit=8)} + except HTTPException: + raise + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.post("/api/launch") + def launch(payload: LaunchPayload) -> Dict[str, Any]: + try: + db = _get_db() + sync = _mirror_runtime_sessions(db) + workspace_id = str((sync.get("workspace") or {}).get("id") or "") + if not workspace_id: + raise HTTPException(status_code=400, detail="No workspace available") + launched = launch_workspace_session( + workspace_id, + SessionLaunchPayload( + runtime=payload.runtime, + title=payload.title, + permission_mode=payload.permission_mode, + task_id=payload.taskId, + ), + ) + launched["taskId"] = launched.get("task_id") + launched["command"] = launched.get("launch_command") + launched["message"] = "Session prepared in Dhee. Open the native runtime with the launch command." + return launched + except HTTPException: + raise + + @app.get("/api/workspace/graph") + def workspace_graph( + workspace_id: Optional[str] = None, + project_id: Optional[str] = None, + ) -> Dict[str, Any]: + try: + if workspace_id: + return _build_workspace_canvas_payload(workspace_id, focus_project_id=project_id) + return _build_workspace_graph_payload() + except Exception as exc: # noqa: BLE001 + log.warning("workspace_graph failed: %s", exc) + return { + "live": False, + "repo": _ui_repo(), + "graph": {"nodes": [], "links": []}, + "sessions": [], + "tasks": [], + "files": [], + "workspaces": [], + "error": str(exc), + } + + # ─── Local context service used by the workspace UI ────────────────────── + + class _LocalContextStore: + def __init__(self, service: "_LocalContextService") -> None: + self.service = service + + def list_context_items(self, **kwargs: Any) -> List[Dict[str, Any]]: + rows = self.service.list_context_items() + team_id = kwargs.get("team_id") + project_id = kwargs.get("project_id") + scope = kwargs.get("scope") + if team_id: + rows = [r for r in rows if r.get("team_id") in {team_id, None, ""}] + if project_id: + rows = [r for r in rows if r.get("project_id") in {project_id, None, ""}] + if scope: + rows = [r for r in rows if r.get("scope") == scope] + return rows[: int(kwargs.get("limit") or 200)] + + def get_context_item(self, org_id: str, context_id: str) -> Optional[Dict[str, Any]]: + del org_id + for row in self.service.list_context_items(): + if str(row.get("context_id") or row.get("id") or "") == context_id: + return row + return None + + def add_context_item(self, item: Dict[str, Any]) -> Dict[str, Any]: + return self.service.add_context_item(item) + + def list_context_shares(self, **kwargs: Any) -> List[Dict[str, Any]]: + del kwargs + return [] + + def get_team(self, org_id: str, team_id: str) -> Optional[Dict[str, Any]]: + del org_id + return self.service.get_team(team_id) + + class _LocalContextService: + org_id = "local" + + def __init__(self) -> None: + self.store = _LocalContextStore(self) + + def close(self) -> None: + return + + def _state(self) -> Dict[str, Any]: + return _read_local_context_state() + + def _save(self, state: Dict[str, Any]) -> None: + _write_local_context_state(state) + + def get_context_manager(self, team_id: str) -> Dict[str, Any]: + return { + "manager_id": f"context-manager:{team_id}", + "owner_user_id": _ui_user_id(), + "display_name": "Dhee Context Manager", + "team_id": team_id, + } + + def get_team(self, team_id: str) -> Optional[Dict[str, Any]]: + state = self._state() + teams = state.get("teams") or {} + if team_id in teams: + return teams[team_id] + for row in (state.get("folders") or {}).values(): + if isinstance(row, dict) and row.get("team_id") == team_id: + return { + "team_id": team_id, + "name": team_id, + "project_id": row.get("project_id") or "local", + } + return None + + def list_context_items(self) -> List[Dict[str, Any]]: + path = Path(_ui_repo()) / ".dhee" / "context" / "entries.jsonl" + rows: List[Dict[str, Any]] = [] + if not path.exists(): + return rows + try: + for line in path.read_text(encoding="utf-8").splitlines(): + if not line.strip(): + continue + try: + item = json.loads(line) + except json.JSONDecodeError: + continue + if not isinstance(item, dict): + continue + cid = str(item.get("context_id") or item.get("id") or "") + rows.append({ + "context_id": cid or hashlib.sha1(line.encode("utf-8")).hexdigest()[:12], + "title": item.get("title") or item.get("summary") or "Repo context", + "content": item.get("content") or item.get("body") or item.get("summary") or "", + "summary": item.get("summary") or item.get("content") or "", + "scope": item.get("scope") or "repo", + "kind": item.get("kind") or item.get("type") or "note", + "project_id": item.get("project_id"), + "team_id": item.get("team_id"), + "tags": item.get("tags") or [], + "metadata": item.get("metadata") or {}, + }) + except OSError: + return [] + return rows + + def add_context_item(self, item: Dict[str, Any]) -> Dict[str, Any]: + context_id = str(item.get("context_id") or f"ctx-{hashlib.sha1(json.dumps(item, sort_keys=True, default=str).encode('utf-8')).hexdigest()[:12]}") + row = {**item, "context_id": context_id, "id": context_id, "updated_at": _now_iso()} + path = Path(_ui_repo()) / ".dhee" / "context" / "entries.jsonl" + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as fh: + fh.write(json.dumps(row, sort_keys=True) + "\n") + return row + + def add_context(self, **kwargs: Any) -> Dict[str, Any]: + return self.add_context_item(kwargs) + + def propose_context(self, **kwargs: Any) -> Dict[str, Any]: + return self.add_context_item({**kwargs, "status": "proposed"}) + + def approve_proposal(self, context_id: str, reviewer_user_id: Optional[str] = None) -> Dict[str, Any]: + return {"context_id": context_id, "status": "approved", "reviewer_user_id": reviewer_user_id} + + def reject_proposal(self, context_id: str, reviewer_user_id: Optional[str] = None) -> Dict[str, Any]: + return {"context_id": context_id, "status": "rejected", "reviewer_user_id": reviewer_user_id} + + def inbox_for_viewer(self, **kwargs: Any) -> Dict[str, Any]: + del kwargs + return {"proposals": [], "findings": []} + + def resolve_finding(self, finding_id: str, resolved_by: Optional[str] = None) -> Dict[str, Any]: + return {"finding_id": finding_id, "resolved_by": resolved_by, "status": "resolved"} + + def list_backlinks(self, context_id: str, limit: int = 50) -> List[Dict[str, Any]]: + del context_id, limit + return [] + + def set_integration(self, **kwargs: Any) -> Dict[str, Any]: + return {"integration_id": hashlib.sha1(json.dumps(kwargs, sort_keys=True, default=str).encode("utf-8")).hexdigest()[:12], **kwargs} + + def create_workspace(self, name: str, root_path: Optional[str] = None, default_branch: str = "main") -> Dict[str, Any]: + state = self._state() + ws_id = _ensure_default_workspace(state) + state["workspaces"][ws_id].update({ + "id": ws_id, + "name": name or "My Workspace", + "root_path": _abs_user_path(root_path) or _ui_repo(), + "default_branch": default_branch, + "updated_at": _now_iso(), + }) + self._save(state) + return state["workspaces"][ws_id] + + def reset_workspace(self) -> Dict[str, int]: + state = self._state() + counts = { + "projects": len(state.get("projects") or {}), + "teams": len(state.get("teams") or {}), + "folders": len(state.get("folders") or {}), + } + self._save(_empty_state()) + return counts + + def create_project(self, project_id: Optional[str], name: str, description: str = "") -> Dict[str, Any]: + state = self._state() + projects = state.setdefault("projects", {}) + pid = project_id or re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-") or "local" + project = {"project_id": pid, "name": name, "description": description, "updated_at": _now_iso()} + projects[pid] = project + self._save(state) + return project + + def delete_project(self, project_id: str) -> Dict[str, Any]: + state = self._state() + (state.get("projects") or {}).pop(project_id, None) + self._save(state) + return {"project_id": project_id} + + def create_project_team(self, project_id: str, team_id: Optional[str], name: str, description: str = "") -> Dict[str, Any]: + state = self._state() + teams = state.setdefault("teams", {}) + tid = team_id or re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-") or "local-dev" + team = {"team_id": tid, "project_id": project_id, "name": name, "description": description, "updated_at": _now_iso()} + teams[tid] = team + self._save(state) + return team + + def _add_folder(self, *, project_id: Optional[str], team_id: Optional[str], local_path: Optional[str], repo_url: Optional[str], label: Optional[str], kind: str) -> Dict[str, Any]: + state = self._state() + workspace_id = _ensure_default_workspace(state) + path = _abs_user_path(local_path) or repo_url or _ui_repo() + mapping_id = _folder_node_id(path).split(":", 1)[-1] + row = { + "mapping_id": mapping_id, + "path": path, + "local_path": _abs_user_path(local_path) or None, + "repo_url": repo_url, + "label": label or _folder_label(path), + "kind": kind, + "project_id": project_id, + "team_id": team_id, + "workspace_id": workspace_id, + "shared": True, + "linked_at": _now_iso(), + "source": "ui", + } + state.setdefault("folders", {})[path] = row + self._save(state) + return {"mapping": row, "folder": row, "mapping_id": mapping_id} + + def add_team_folder(self, team_id: str, local_path: Optional[str] = None, repo_url: Optional[str] = None, label: Optional[str] = None, kind: str = "folder") -> Dict[str, Any]: + team = self.get_team(team_id) or {} + return self._add_folder(project_id=team.get("project_id"), team_id=team_id, local_path=local_path, repo_url=repo_url, label=label, kind=kind) + + def add_project_folder(self, project_id: str, local_path: Optional[str] = None, repo_url: Optional[str] = None, label: Optional[str] = None, kind: str = "folder") -> Dict[str, Any]: + return self._add_folder(project_id=project_id, team_id=None, local_path=local_path, repo_url=repo_url, label=label, kind=kind) + + def remove_project_folder(self, mapping_id: str) -> Dict[str, Any]: + state = self._state() + folders = state.get("folders") or {} + for path, row in list(folders.items()): + if isinstance(row, dict) and str(row.get("mapping_id") or _folder_node_id(path).split(":", 1)[-1]) == mapping_id: + folders.pop(path, None) + self._save(state) + return {"mapping_id": mapping_id} + raise ValueError(f"folder mapping not found: {mapping_id}") + + def add_team_collaborator(self, team_id: str, target_team_id: str) -> Dict[str, Any]: + return { + "team": self.get_team(team_id) or {"team_id": team_id}, + "target_team": self.get_team(target_team_id) or {"team_id": target_team_id}, + "collaborating_team_ids": [target_team_id], + } + + def run_ast_extraction(self, project_id: str, team_id: Optional[str] = None) -> Dict[str, Any]: + state = self._state() + folders = [ + row for row in (state.get("folders") or {}).values() + if isinstance(row, dict) + and (not project_id or row.get("project_id") in {project_id, None}) + and (not team_id or row.get("team_id") == team_id) + ] + files_seen = 0 + for row in folders: + root = _abs_user_path(row.get("local_path") or row.get("path")) + if not root or not os.path.isdir(root): + continue + for _base, dirs, files in os.walk(root): + dirs[:] = [d for d in dirs if d not in {".git", "node_modules", ".venv", "__pycache__"}] + files_seen += len(files) + if files_seen >= 1000: + break + return { + "project_id": project_id, + "team_id": team_id, + "folders_seen": len(folders), + "files_seen": files_seen, + "files_extracted": 0, + "files_cached": files_seen, + "nodes_upserted": 0, + "edges_upserted": 0, + "errors": [], + } + + def _enterprise_service() -> _LocalContextService: + return _LocalContextService() + + def _resolve_repo_pointer() -> Dict[str, Any]: + cwd = Path(os.environ.get("DHEE_UI_REPO") or os.getcwd()).resolve() + home = Path.home().resolve() + for candidate in [cwd, *cwd.parents]: + if candidate == home: + break + cfg = candidate / ".dhee" / "config.json" + if cfg.is_file(): + try: + data = json.loads(cfg.read_text(encoding="utf-8")) + return {"repo_root": str(candidate), **(data if isinstance(data, dict) else {})} + except Exception: # noqa: BLE001 + return {} + digest = hashlib.sha1(str(candidate).encode("utf-8")).hexdigest()[:16] + private_cfg = Path.home() / ".dhee" / "repo_orgs" / f"{digest}.json" + if private_cfg.is_file(): + try: + data = json.loads(private_cfg.read_text(encoding="utf-8")) + return data if isinstance(data, dict) else {} + except Exception: # noqa: BLE001 + return {} + return {} + + @app.get("/api/me") + def api_me() -> Dict[str, Any]: + pointer = _resolve_repo_pointer() + org_id = ( + pointer.get("org_id") + or os.environ.get("DHEE_UI_ORG_ID") + or "default" + ) + user_id = _ui_user_id() + team_id = pointer.get("team_id") + team_ids = [team_id] if team_id else [] + # role defaults to developer; manager flag flips when this user is the + # team's assigned context_manager.owner_user_id. + role = pointer.get("default_role") or "developer" + try: + svc = _enterprise_service() + try: + if team_id: + mgr = svc.get_context_manager(team_id) + if mgr and ( + mgr.get("owner_user_id") == user_id + or mgr.get("manager_id") == user_id + ): + role = "manager" + finally: + svc.close() + except Exception: # noqa: BLE001 + pass + return { + "live": True, + "user_id": user_id, + "org_id": org_id, + "project_id": pointer.get("project_id"), + "team_id": team_id, + "team_ids": team_ids, + "role": role, + "repo_root": str(pointer.get("repo_root") or _ui_repo()), + } + + @app.get("/api/continuity") + def api_continuity() -> Dict[str, Any]: + repo = _ui_repo() + pointer = _resolve_repo_pointer() + last_session: Optional[Dict[str, Any]] = None + error = "" + try: + from dhee.core.kernel import get_last_session + + last_session = get_last_session( + agent_id="claude-code", + repo=repo, + fallback_log_recovery=True, + user_id=_ui_user_id(), + requester_agent_id="dhee-ui", + ) + except Exception as exc: # noqa: BLE001 + error = str(exc) + try: + claude_sessions = _find_claude_sessions(repo, limit=5) + except Exception: # noqa: BLE001 + claude_sessions = [] + return { + "live": bool(last_session or claude_sessions), + "repo": repo, + "repo_config": pointer, + "last_session": last_session, + "claude_sessions": claude_sessions, + "error": error or None, + } + + # ── Local-context state file: workspaces + folders ─────────────── + # v1 schema (legacy): {"folders": {path: {...}}} + # v2 schema (now): {"schema_version": 2, + # "workspaces": {ws_id: {"name": ..., "created_at": ...}}, + # "folders": {path: {workspace_id: ws_id, ...}}} + # Reads accept both shapes. Writes always emit v2. Multi-workspace + # is supported in storage today; the create endpoint enforces a + # single-workspace cap so the UI stays simple — lift the cap by + # changing one constant when we're ready. + + LOCAL_CONTEXT_SCHEMA_VERSION = 2 + DEFAULT_WORKSPACE_ID = "default" + MAX_WORKSPACES = 1 # raise this when we ship multi-workspace UX + + def _local_context_state_path() -> Path: + return Path.home() / ".dhee" / "local_context_folders.json" + + def _empty_state() -> Dict[str, Any]: + return { + "schema_version": LOCAL_CONTEXT_SCHEMA_VERSION, + "workspaces": {}, + "folders": {}, + "projects": {}, + "teams": {}, + } + + def _read_local_context_state() -> Dict[str, Any]: + path = _local_context_state_path() + if not path.exists(): + return _empty_state() + try: + data = json.loads(path.read_text(encoding="utf-8")) + except Exception: # noqa: BLE001 + return _empty_state() + if not isinstance(data, dict): + return _empty_state() + + folders_raw = data.get("folders") + folders = folders_raw if isinstance(folders_raw, dict) else {} + + workspaces_raw = data.get("workspaces") + workspaces = workspaces_raw if isinstance(workspaces_raw, dict) else {} + + # v1 → v2 migration: any pre-existing folders implicitly join + # the default workspace. We auto-create that workspace here so + # the rest of the code can assume every folder has one. + if folders and not workspaces: + workspaces = { + DEFAULT_WORKSPACE_ID: { + "id": DEFAULT_WORKSPACE_ID, + "name": "My Workspace", + "created_at": _now_iso(), + } + } + + # Stamp every folder with a workspace_id so reads never have to + # guess. Doesn't persist until the next write — that's fine. + for fpath, frow in list(folders.items()): + if not isinstance(frow, dict): + continue + if not frow.get("workspace_id"): + frow["workspace_id"] = ( + next(iter(workspaces)) if workspaces else DEFAULT_WORKSPACE_ID + ) + + out = { + "schema_version": LOCAL_CONTEXT_SCHEMA_VERSION, + "workspaces": workspaces, + "folders": folders, + } + for key in ("projects", "teams", "collaborators"): + if isinstance(data.get(key), dict): + out[key] = data[key] + return out + + def _ensure_default_workspace(state: Dict[str, Any]) -> str: + """Make sure the state has at least one workspace; return its id. + + Single-workspace mode means: if no workspace exists yet, the + first folder add creates the default one. The cap is enforced + on explicit ``/api/workspace/create`` calls only; auto-creating + the implicit default never hits the cap. + """ + workspaces = state.get("workspaces") or {} + if not isinstance(workspaces, dict): + workspaces = {} + state["workspaces"] = workspaces + if workspaces: + return next(iter(workspaces)) + ws_id = DEFAULT_WORKSPACE_ID + workspaces[ws_id] = { + "id": ws_id, + "name": "My Workspace", + "created_at": _now_iso(), + } + return ws_id + + def _write_local_context_state(state: Dict[str, Any]) -> None: + path = _local_context_state_path() + path.parent.mkdir(parents=True, exist_ok=True) + # Always normalise on write so the disk format is exactly v2. + out = { + "schema_version": LOCAL_CONTEXT_SCHEMA_VERSION, + "workspaces": state.get("workspaces") or {}, + "folders": state.get("folders") or {}, + } + for key in ("projects", "teams", "collaborators"): + if isinstance(state.get(key), dict): + out[key] = state[key] + path.write_text(json.dumps(out, indent=2, sort_keys=True), encoding="utf-8") + + def _folder_node_id(path: str) -> str: + digest = hashlib.sha1(path.encode("utf-8")).hexdigest()[:14] + return f"folder:{digest}" + + def _session_node_id(session: Dict[str, Any]) -> str: + return str(session.get("id") or session.get("nativeSessionId") or hashlib.sha1( + json.dumps(session, sort_keys=True, default=str).encode("utf-8") + ).hexdigest()[:14]) + + def _folder_label(path: str) -> str: + return os.path.basename(path.rstrip(os.sep)) or path + + def _path_is_within(path: str, root: str) -> bool: + try: + return os.path.commonpath([path, root]) == root + except ValueError: + return False + + git_root_cache: Dict[str, str] = {} + + def _git_root_for_path(path: str) -> str: + resolved = _abs_user_path(path) + if not resolved: + return "" + probe = resolved if os.path.isdir(resolved) else os.path.dirname(resolved) + if not probe: + return "" + cached = git_root_cache.get(probe) + if cached is not None: + return cached + try: + result = subprocess.run( + ["git", "-C", probe, "rev-parse", "--show-toplevel"], + capture_output=True, + text=True, + check=False, + timeout=0.8, + ) + except Exception: # noqa: BLE001 + git_root_cache[probe] = "" + return "" + if result.returncode != 0: + git_root_cache[probe] = "" + return "" + git_root_cache[probe] = _abs_user_path((result.stdout or "").strip()) or "" + return git_root_cache[probe] + + def _best_configured_root(path: str, configured_paths: List[str]) -> str: + best = "" + best_len = -1 + for folder in configured_paths: + try: + common = os.path.commonpath([path, folder]) + except ValueError: + continue + if common == folder and len(folder) > best_len: + best = folder + best_len = len(folder) + return best + + def _root_folder_for_path(path: Optional[str], configured_paths: Optional[List[str]] = None) -> str: + resolved = _abs_user_path(path) + if not resolved: + return "" + git_root = _git_root_for_path(resolved) + if git_root: + return git_root + ui_repo = _abs_user_path(_ui_repo()) + if ui_repo and _path_is_within(resolved, ui_repo): + return ui_repo + configured_root = _best_configured_root(resolved, configured_paths or []) + return configured_root or resolved + + def _folder_context_manager(path: str) -> Dict[str, Any]: + label = _folder_label(path) + return { + "manager_id": f"context-manager:{_folder_node_id(path).split(':', 1)[-1]}", + "display_name": f"{label} Context Manager", + "folder_path": path, + "charter": "Own context quality, freshness, and sharing for this local folder.", + "status": "active", + } + + ACTIVE_GRACE_SECONDS = 30 * 60 # survives quiet turns without reviving old rows + + def _is_session_active(session: Dict[str, Any]) -> bool: + """Return whether a session should be shown in live-only surfaces. + + Older mirrored rows were sometimes left with ``state=active`` forever. + Treat the stored state as a hint, then require either a fresh update, + a current mirror marker, or a live native process/pid. This keeps + FOLDERS and ROUTER from showing stale historical sessions as live. + """ + state_value = str(session.get("state") or "").lower() + if state_value in ("paused", "stale", "ended", "completed", "killed"): + return False + + metadata = dict(session.get("metadata") or {}) + runtime = str(session.get("runtime_id") or session.get("runtime") or "").lower() + updated_at = session.get("updated_at") or session.get("updatedAt") + + if _recent_enough(updated_at, seconds=ACTIVE_GRACE_SECONDS): + return True + + # Claude's local registry gives us the real process id. Prefer that + # over cwd-level process checks, because one Claude process in a repo + # should not resurrect every old session that once used that folder. + if runtime in {"claude-code", "claude"} and _pid_alive(metadata.get("pid")): + return True + + return False + + def _local_context_graph_payload(*, active_only: bool = False) -> Dict[str, Any]: + db = _get_db() + state = _read_local_context_state() + configured = state.get("folders") or {} + workspaces = state.get("workspaces") or {} + configured_paths = [ + _abs_user_path(path) + for path in configured.keys() + if _abs_user_path(path) + ] + process_paths = [ + _abs_user_path(proc.get("cwd")) + for proc in _runtime_processes() + if str(proc.get("runtime_id") or "") in {"codex", "claude-code"} + ] + extra_paths = [path for path in [*configured_paths, *process_paths] if path] + sync = _mirror_runtime_sessions(db, extra_paths=extra_paths) + raw_sessions = [ + session + for session in list(sync.get("sessions") or []) + if str(session.get("state") or "") != "stale" + ] + if not raw_sessions: + raw_sessions = [ + session + for session in db.list_agent_sessions(user_id=_ui_user_id(), limit=40) + if str(session.get("state") or "") == "active" + or _recent_enough(session.get("updated_at"), seconds=86_400) + ] + if active_only: + raw_sessions = [s for s in raw_sessions if _is_session_active(s)] + sessions = [_session_summary(db, session) for session in raw_sessions] + folder_paths: List[str] = [] + seen_paths: set[str] = set() + + def add_folder(path: Optional[str]) -> None: + resolved = _abs_user_path(path) + if not resolved or resolved in seen_paths: + return + seen_paths.add(resolved) + folder_paths.append(resolved) + + # Only the UI repo and configured paths seed the folder set. Sessions + # whose cwd lives in a subfolder are clustered into their root folder + # (git root → ui repo → nearest configured root), so the canvas shows + # one node per project and all its sessions share a single context. + add_folder(_root_folder_for_path(_ui_repo(), configured_paths) or _ui_repo()) + for path in configured_paths: + add_folder(_root_folder_for_path(path, configured_paths) or path) + + nodes: List[Dict[str, Any]] = [] + edges: List[Dict[str, Any]] = [] + folders: Dict[str, Dict[str, Any]] = {} + session_counts: Dict[str, int] = Counter() + active_counts: Dict[str, int] = Counter() + latest_by_folder: Dict[str, str] = {} + folder_for_session: Dict[str, str] = {} + + for session in sessions: + cwd = _abs_user_path(session.get("cwd")) + if not cwd: + continue + folder = _root_folder_for_path(cwd, configured_paths) + if not folder: + folder = cwd + add_folder(folder) + folder_for_session[_session_node_id(session)] = folder + session_counts[folder] += 1 + if str(session.get("state") or "") == "active" or session.get("isCurrent"): + active_counts[folder] += 1 + updated = str(session.get("updatedAt") or "") + if updated and updated > latest_by_folder.get(folder, ""): + latest_by_folder[folder] = updated + + def saved_rows_for_root(root: str) -> List[Dict[str, Any]]: + rows: List[Dict[str, Any]] = [] + for saved_path, row in configured.items(): + resolved = _abs_user_path(saved_path) + if not resolved or not isinstance(row, dict): + continue + row_root = _root_folder_for_path(resolved, configured_paths) or resolved + if row_root == root: + rows.append(row) + return rows + + for path in folder_paths: + saved_rows = saved_rows_for_root(path) + if active_only and session_counts.get(path, 0) == 0 and not saved_rows: + continue + saved = saved_rows[0] if saved_rows else {} + shared = any(bool(row.get("shared")) for row in saved_rows) + linked = any(bool(row.get("linked")) for row in saved_rows) + folders[path] = { + "id": _folder_node_id(path), + "type": "folder", + "label": _folder_label(path), + "health": "healthy" if (shared or linked) else "watch", + "meta": { + "path": path, + "shared": shared, + "linked": linked, + "linked_at": saved.get("linked_at") if isinstance(saved, dict) else None, + "session_count": session_counts.get(path, 0), + "active_session_count": active_counts.get(path, 0), + "updated_at": latest_by_folder.get(path), + "source": "manual" if saved_rows else "session", + "context_manager": _folder_context_manager(path), + }, + } + + for path in sorted(folders): + nodes.append(folders[path]) + + for session in sessions: + session_id = _session_node_id(session) + folder = folder_for_session.get(session_id) + if not folder or folder not in folders: + continue + nodes.append({ + "id": session_id, + "type": "session", + "label": _session_title( + session.get("title"), + preview=session.get("preview"), + runtime=session.get("runtime"), + cwd=session.get("cwd"), + session_id=session.get("nativeSessionId") or session_id, + ), + "health": "healthy" if session.get("isCurrent") or session.get("state") == "active" else "watch", + "meta": { + "folder_path": folder, + "cwd": session.get("cwd"), + "runtime": session.get("runtime"), + "state": session.get("state"), + "model": session.get("model"), + "updated_at": session.get("updatedAt"), + "task_id": session.get("taskId"), + "preview": session.get("preview"), + "permission_mode": session.get("permissionMode"), + }, + }) + edges.append({"source": folders[folder]["id"], "target": session_id, "kind": "contains"}) + + shared_folder_ids = [row["id"] for row in folders.values() if (row.get("meta") or {}).get("shared")] + for idx in range(1, len(shared_folder_ids)): + edges.append({ + "source": shared_folder_ids[idx - 1], + "target": shared_folder_ids[idx], + "kind": "shares", + }) + + # Workspace summary — list every known workspace so the UI can + # render the workspace switcher (single-cap today, multi later). + workspace_list = [] + for ws_id, ws_row in (workspaces or {}).items(): + if not isinstance(ws_row, dict): + continue + ws_folders = [ + p for p, f in (configured or {}).items() + if isinstance(f, dict) and (f.get("workspace_id") or DEFAULT_WORKSPACE_ID) == ws_id + ] + workspace_list.append({ + "id": ws_id, + "name": ws_row.get("name") or "Workspace", + "created_at": ws_row.get("created_at"), + "folder_count": len(ws_folders), + }) + + return { + "live": True, + "org_id": "local", + "active_only": bool(active_only), + "workspaces": workspace_list, + "active_workspace_id": (workspace_list[0]["id"] if workspace_list else None), + "nodes": nodes, + "edges": edges, + "totals": { + "projects": 0, + "teams": len(sessions), + "repos": len(folders), + "context_items": 0, + "pending_proposals": 0, + "folders": len(folders), + "sessions": len(sessions), + "shared_folders": len(shared_folder_ids), + }, + "raw": { + "mode": "local_context", + "folders": list(folders.values()), + "sessions": sessions, + "shared_folder_paths": [ + str((row.get("meta") or {}).get("path") or "") + for row in folders.values() + if (row.get("meta") or {}).get("shared") + ], + "context_index": [], + "pending_proposals": [], + "context_managers_by_team": {}, + }, + } + + @app.get("/api/local-context") + def api_local_context() -> Dict[str, Any]: + return _local_context_graph_payload() + + @app.post("/api/local-context/folders") + def api_local_context_folder_add(payload: LocalContextFolderPayload) -> Dict[str, Any]: + """Add a folder for shared context. + + Two-step flow that matches the CLI: + + 1. If the path is inside a git repo, call ``repo_link.link()``. + That creates ``/.dhee/``, installs the post-merge / + post-checkout / post-rewrite hooks, and **mirrors the repo + into the same JSON state file we read here** — so the user + gets the full git-shared-context pipeline, not just a row in + ``local_context_folders.json``. + 2. If the path isn't a git repo, fall through to the legacy + behaviour: store the row, let the canvas cluster by path. We + return ``link.linked: false`` so the UI can offer a "git init + + link" follow-up. + """ + path = _abs_user_path(payload.path) + if not path: + raise HTTPException(status_code=400, detail="path is required") + + link_info: Dict[str, Any] = {"linked": False} + try: + from dhee import repo_link + + try: + info = repo_link.link(path) + link_info = {"linked": True, **info} + # repo_link.link() resolves to the git root, which may + # differ from the user-picked sub-path. Anchor the state + # row at the git root so clustering and link-state agree. + path = info["repo_root"] + except ValueError as exc: + # Not a git repo. Keep the row but mark unlinked. + link_info = {"linked": False, "reason": str(exc)} + except Exception as exc: # noqa: BLE001 + link_info = {"linked": False, "reason": f"link error: {exc}"} + + state = _read_local_context_state() + workspace_id = _ensure_default_workspace(state) + folders = dict(state.get("folders") or {}) + current = dict(folders.get(path) or {}) + current.update({ + "path": path, + "shared": bool(payload.shared), + "workspace_id": current.get("workspace_id") or workspace_id, + "updated_at": _now_iso(), + }) + if link_info.get("linked"): + current["linked"] = True + current.setdefault("linked_at", current.get("linked_at") or _now_iso()) + current["source"] = "ui_link" + folders[path] = current + state["folders"] = folders + _write_local_context_state(state) + return {"ok": True, "folder": current, "link": link_info} + + @app.post("/api/local-context/folders/share") + def api_local_context_folder_share(payload: LocalContextFolderPayload) -> Dict[str, Any]: + return api_local_context_folder_add(payload) + + @app.post("/api/local-context/folders/link") + def api_local_context_folder_link(payload: LocalContextFolderPayload) -> Dict[str, Any]: + """Link an already-added folder to git-shared context. + + Same effect as ``api_local_context_folder_add`` for git repos, + but exposed as its own endpoint so UI buttons read cleanly + (``Link`` vs ``Add``). Returns 400 with a friendly reason if + the path isn't inside a git repo. + """ + path = _abs_user_path(payload.path) + if not path: + raise HTTPException(status_code=400, detail="path is required") + try: + from dhee import repo_link + + info = repo_link.link(path) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=500, detail=f"link error: {exc}") + + state = _read_local_context_state() + workspace_id = _ensure_default_workspace(state) + folders = dict(state.get("folders") or {}) + repo_root = info["repo_root"] + current = dict(folders.get(repo_root) or {}) + current.update({ + "path": repo_root, + "shared": True, + "linked": True, + "linked_at": current.get("linked_at") or _now_iso(), + "source": current.get("source") or "ui_link", + "workspace_id": current.get("workspace_id") or workspace_id, + "updated_at": _now_iso(), + }) + folders[repo_root] = current + state["folders"] = folders + _write_local_context_state(state) + return {"ok": True, "folder": current, "link": {"linked": True, **info}} + + @app.post("/api/local-context/folders/unlink") + def api_local_context_folder_unlink(payload: LocalContextFolderPayload) -> Dict[str, Any]: + path = _abs_user_path(payload.path) + if not path: + raise HTTPException(status_code=400, detail="path is required") + try: + from dhee import repo_link + + info = repo_link.unlink(path) + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=500, detail=f"unlink error: {exc}") + return {"ok": True, "unlink": info} + + @app.get("/api/org/graph") + def api_org_graph( + org: Optional[str] = None, + active: bool = False, + ) -> Dict[str, Any]: + del org + try: + return _local_context_graph_payload(active_only=active) + except Exception as exc: # noqa: BLE001 + return {"live": False, "nodes": [], "edges": [], "error": str(exc)} + + # ─── Workspace primitive (NEW: workspace = container of linked folders) ── + # Distinct from the legacy ``/api/workspaces`` (workspace = project). + # We namespace under ``/api/local/workspaces`` so the legacy routes + # keep working until the rebrand finishes. Single workspace today + # (MAX_WORKSPACES=1) — lift by raising the constant. + + @app.get("/api/local/workspaces") + def api_workspaces_list() -> Dict[str, Any]: + state = _read_local_context_state() + ws = state.get("workspaces") or {} + out = [] + for ws_id, row in ws.items(): + if not isinstance(row, dict): + continue + folders = [ + p for p, f in (state.get("folders") or {}).items() + if isinstance(f, dict) + and (f.get("workspace_id") or DEFAULT_WORKSPACE_ID) == ws_id + ] + out.append({ + "id": ws_id, + "name": row.get("name") or "Workspace", + "created_at": row.get("created_at"), + "folder_count": len(folders), + "folders": folders, + }) + return {"workspaces": out, "max_workspaces": MAX_WORKSPACES} + + @app.post("/api/local/workspaces") + def api_workspaces_create(payload: LocalWorkspaceCreatePayload) -> Dict[str, Any]: + state = _read_local_context_state() + existing = state.get("workspaces") or {} + # Single-workspace cap. Storage allows more; the UX gate lives here. + if len([w for w in existing.values() if isinstance(w, dict)]) >= MAX_WORKSPACES: + raise HTTPException( + status_code=409, + detail=( + f"workspace cap reached ({MAX_WORKSPACES}); " + "multi-workspace is on the roadmap" + ), + ) + ws_id = (payload.id or DEFAULT_WORKSPACE_ID).strip() or DEFAULT_WORKSPACE_ID + if ws_id in existing: + raise HTTPException(status_code=409, detail=f"workspace {ws_id!r} exists") + existing[ws_id] = { + "id": ws_id, + "name": (payload.name or "My Workspace").strip() or "My Workspace", + "created_at": _now_iso(), + } + state["workspaces"] = existing + _write_local_context_state(state) + return {"ok": True, "workspace": existing[ws_id]} + + # ─── Per-session router view (Router dashboard data) ──────────── + # Returns one row per session, joining the agent_sessions table + # with the router's ptr-store byte counts. This is what powers the + # paginated table on the new Router landing page. + + @app.get("/api/router/sessions") + def api_router_sessions( + active: bool = True, + cursor: Optional[str] = None, + limit: int = 25, + agent: Optional[str] = None, + ) -> Dict[str, Any]: + from dhee.router import ptr_store as _ptr + from dhee.router import stats as _rstats + + # Per-session ptr-store roll-up. We walk the directory tree + # ourselves rather than calling ``compute_stats`` because the + # aggregate API doesn't expose the per-session breakdown we + # need here. + ptr_records: List[Dict[str, Any]] = [] + try: + ptr_root = _ptr._root() + except Exception: # noqa: BLE001 + ptr_root = None + if ptr_root is not None and ptr_root.exists(): + try: + ptr_scan_limit = max( + 100, + min(int(os.environ.get("DHEE_UI_ROUTER_PTR_SCAN_LIMIT", "1200")), 10000), + ) + except (TypeError, ValueError): + ptr_scan_limit = 1200 + + def _path_mtime(path: Path) -> float: + try: + return path.stat().st_mtime + except OSError: + return 0.0 + + scanned = 0 + session_dirs = sorted( + (path for path in ptr_root.iterdir() if path.is_dir()), + key=_path_mtime, + reverse=True, + )[: min(ptr_scan_limit, 500)] + for sdir in session_dirs: + for meta_file in sorted(sdir.glob("*.json"), key=_path_mtime, reverse=True): + if scanned >= ptr_scan_limit: + break + scanned += 1 + try: + meta = json.loads(meta_file.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + continue + if not isinstance(meta, dict): + continue + tool_name = str(meta.get("tool") or "unknown") + ptr_records.append( + { + "bucket": sdir.name, + "chars": _rstats._stored_chars(meta, meta_file), + "tool": tool_name, + "agent": _rstats._agent_from_meta(meta), + "cwd": _abs_user_path(meta.get("cwd")), + "repo": _abs_user_path(meta.get("repo")), + "session_id": str(meta.get("session_id") or ""), + "thread_id": str(meta.get("thread_id") or ""), + "stored_at": _coerce_datetime(meta.get("stored_at")) + or _coerce_datetime(meta_file.stat().st_mtime), + } + ) + if scanned >= ptr_scan_limit: + break + + chars_per_token = float(getattr(_rstats, "CHARS_PER_TOKEN", 4)) + + # Pull session records from the durable agent_sessions table. + # We over-fetch then filter+sort+page in Python because the + # join key (router session_id ↔ session row id) isn't indexed. + db = _get_db() + rows = db.list_agent_sessions(user_id=_ui_user_id(), limit=500) or [] + summaries = [_session_summary(db, row) for row in rows] + codex_live_by_id: Dict[str, Dict[str, Any]] = {} + try: + for thread in _repo_codex_threads(_ui_repo(), limit=2): + usage = _router_codex_live_usage_from_thread(thread) + if not usage.get("available"): + continue + thread_id = str(thread.get("id") or "") + if not thread_id: + continue + codex_live_by_id[thread_id] = usage + codex_live_by_id[f"session:codex:{thread_id}"] = usage + except Exception: # noqa: BLE001 + codex_live_by_id = {} + + def router_bucket_for_cwd(cwd: Any) -> str: + path = _abs_user_path(cwd) + if not path: + return "" + user = os.environ.get("USER") or os.environ.get("LOGNAME") or "user" + digest = hashlib.sha1(f"{user}|{path}".encode("utf-8", errors="replace")).hexdigest()[:12] + return f"s-{digest}" + + candidates: List[Dict[str, Any]] = [] + now = datetime.now(timezone.utc) + for row, summary in zip(rows, summaries): + session_id = str(summary.get("id") or "") + native_id = str(summary.get("nativeSessionId") or "") + cwd = _abs_user_path(summary.get("cwd")) + updated_at = _coerce_datetime(summary.get("updatedAt")) + started_at = _coerce_datetime(summary.get("startedAt")) + row_active = _is_session_active(row) + candidates.append( + { + "session_id": session_id, + "native_id": native_id, + "runtime": _rstats._normalize_agent_id(summary.get("runtime")), + "cwd": cwd, + "repo_root": _git_root_for_path(cwd or "") or cwd or "", + "bucket": router_bucket_for_cwd(cwd), + "started_at": started_at, + "updated_at": updated_at, + "active": row_active, + } + ) + + def score_record_for_session(rec: Dict[str, Any], cand: Dict[str, Any]) -> int: + score = 0 + rec_session = str(rec.get("session_id") or "") + rec_thread = str(rec.get("thread_id") or "") + explicit_match = False + if rec_session and rec_session in {cand["session_id"], cand["native_id"]}: + score += 1000 + explicit_match = True + if rec_thread and rec_thread in {cand["session_id"], cand["native_id"]}: + score += 900 + explicit_match = True + if rec.get("bucket") and rec.get("bucket") == cand.get("session_id"): + score += 800 + explicit_match = True + if rec.get("bucket") and rec.get("bucket") == cand.get("bucket"): + score += 80 + + rec_agent = _rstats._normalize_agent_id(rec.get("agent")) + runtime = str(cand.get("runtime") or "unknown") + if rec_agent in {"", "unknown"}: + score += 5 + elif rec_agent == runtime: + score += 40 + else: + score -= 60 + + rec_path = rec.get("cwd") or rec.get("repo") or "" + if rec_path: + if cand.get("cwd") and _paths_overlap(rec_path, cand["cwd"]): + score += 45 + elif cand.get("repo_root") and _paths_overlap(rec_path, cand["repo_root"]): + score += 35 + else: + score -= 30 + + rec_time = rec.get("stored_at") + if isinstance(rec_time, datetime): + start = cand.get("started_at") + end = now if cand.get("active") else cand.get("updated_at") + if not explicit_match and isinstance(start, datetime): + start_floor = start - timedelta(minutes=5) + if rec_time < start_floor: + return -10_000 + if ( + not explicit_match + and not isinstance(start, datetime) + and isinstance(end, datetime) + and rec_time < end - timedelta(seconds=ACTIVE_GRACE_SECONDS) + ): + return -10_000 + if ( + not explicit_match + and not cand.get("active") + and isinstance(end, datetime) + and rec_time > end + timedelta(minutes=5) + ): + return -10_000 + if isinstance(start, datetime) and rec_time < start: + delta_hours = abs((start - rec_time).total_seconds()) / 3600 + score -= min(40, int(delta_hours)) + elif isinstance(start, datetime): + score += 18 + if isinstance(end, datetime): + if rec_time <= end: + score += 28 + else: + delta_hours = abs((rec_time - end).total_seconds()) / 3600 + score -= min(50, int(delta_hours)) + + return score + + per_session: Dict[str, Dict[str, Any]] = { + str(summary.get("id") or ""): {"chars": 0, "calls": 0, "tools": {}, "agents": set()} + for summary in summaries + if summary.get("id") + } + for rec in ptr_records: + best: Optional[Dict[str, Any]] = None + best_score = -10_000 + for cand in candidates: + score = score_record_for_session(rec, cand) + if score > best_score: + best = cand + best_score = score + if not best or best_score < 20: + continue + row = per_session.setdefault( + str(best["session_id"]), + {"chars": 0, "calls": 0, "tools": {}, "agents": set()}, + ) + row["calls"] += 1 + row["chars"] += int(rec.get("chars") or 0) + tool_name = str(rec.get("tool") or "unknown") + row["tools"][tool_name] = row["tools"].get(tool_name, 0) + 1 + row["agents"].add(str(rec.get("agent") or "unknown")) + + items: List[Dict[str, Any]] = [] + for row, summary in zip(rows, summaries): + session_id = summary.get("id") or "" + ptr_row = per_session.get(session_id) or { + "chars": 0, + "calls": 0, + "tools": {}, + "agents": set(), + } + tokens_saved = int(ptr_row["chars"] / chars_per_token) if chars_per_token else 0 + known_agent_ids = sorted( + a for a in ptr_row["agents"] + if a and str(a).lower() not in {"unknown", "none", "null"} + ) + agent_ids = known_agent_ids or [str(summary.get("runtime") or "unknown")] + updated_at = summary.get("updatedAt") or "" + pricing = _router_payg_pricing(summary.get("runtime"), summary.get("model")) + estimated_cost_saved = ( + tokens_saved + * float(pricing.get("input_cost_per_million") or 0.0) + / 1_000_000 + ) + normalized_runtime = _rstats._normalize_agent_id(summary.get("runtime")) + live_usage = None + if normalized_runtime == "codex" or any( + _rstats._normalize_agent_id(a) == "codex" for a in agent_ids + ): + live_usage = ( + codex_live_by_id.get(str(session_id)) + or codex_live_by_id.get(str(summary.get("nativeSessionId") or "")) + ) + elif normalized_runtime == "claude-code" or any( + _rstats._normalize_agent_id(a) == "claude-code" for a in agent_ids + ): + live_usage = _router_claude_live_usage_from_summary(summary) + item = { + "session_id": session_id, + "title": _session_title( + summary.get("title"), + preview=summary.get("preview"), + runtime=summary.get("runtime"), + cwd=summary.get("cwd"), + session_id=summary.get("nativeSessionId") or session_id, + ), + "state": summary.get("state") or "", + "agent": agent_ids[0] if agent_ids else "unknown", + "agents": agent_ids, + "cwd": summary.get("cwd") or "", + "repo_root": _git_root_for_path(summary.get("cwd") or "") or summary.get("cwd") or "", + "runtime": summary.get("runtime"), + "model": summary.get("model"), + "updated_at": updated_at, + "started_at": summary.get("startedAt"), + "tokens_saved": tokens_saved, + "estimated_cost_saved_usd": round(estimated_cost_saved, 6), + "pricing": pricing, + "live_usage": live_usage, + "router_calls": int(ptr_row["calls"]), + "tool_breakdown": dict(ptr_row["tools"]), + "active": _is_session_active(row), + "task": { + "id": summary.get("taskId"), + "status": summary.get("taskStatus"), + }, + "preview": summary.get("preview") or "", + } + if active and not item["active"]: + continue + if agent and item["agent"] != _rstats._normalize_agent_id(agent): + continue + items.append(item) + + # Sort newest-first; cursor is just an opaque (updated_at, session_id) + # tuple so a future paginator can resume past it. + items.sort(key=lambda r: (r["updated_at"], r["session_id"]), reverse=True) + + if cursor: + # Cursor is "|". Drop everything <= cursor. + try: + cur_updated, cur_id = cursor.split("|", 1) + except ValueError: + cur_updated, cur_id = "", "" + items = [ + r for r in items + if (r["updated_at"], r["session_id"]) < (cur_updated, cur_id) + ] + + bounded_limit = max(1, min(int(limit), 100)) + page = items[:bounded_limit] + next_cursor = None + if len(items) > bounded_limit: + tail = page[-1] + next_cursor = f"{tail['updated_at']}|{tail['session_id']}" + + estimated_total = round( + sum(float(r.get("estimated_cost_saved_usd") or 0.0) for r in items), + 6, + ) + return { + "items": page, + "next_cursor": next_cursor, + "active_only": bool(active), + "totals": { + "tokens_saved": sum(r["tokens_saved"] for r in items), + "estimated_cost_saved_usd": estimated_total, + "theoretical_api_value_usd": estimated_total, + "realized_cost_saved_usd": _budget_cap_usd(estimated_total, period="month"), + "router_calls": sum(r["router_calls"] for r in items), + "sessions": len(items), + }, + "budget": _router_budget_payload(), + "money_math": { + "tokens_basis": "raw input tokens avoided by cached router context", + "dollar_basis": "avoided input tokens multiplied by mapped official provider input rates", + "realized_cap": "monthly paid AI budget", + "honesty_note": ( + "Theoretical API value can be higher than the user's paid budget. " + "Dhee claims realized savings only up to that configured budget." + ), + }, + } + + # ─── Context Management screen data ───────────────────────────── + # One endpoint per folder: returns repo entries (shared via git), + # personal memories that have been promoted into this repo, and + # personal memories that came from this repo via demote. Plus a + # share matrix listing every other linked repo and how many + # entries cross between them. The UI uses this to render the + # per-folder context management screen + bidirectional shared + # context view. + + @app.get("/api/context/entries") + def api_context_entries( + repo: Optional[str] = None, + limit: int = 200, + ) -> Dict[str, Any]: + from dhee import repo_link + + target_path = _abs_user_path(repo) if repo else None + repo_root = repo_link.repo_for_path(target_path) if target_path else None + if repo_root is None: + # Fallback: pick the first linked repo so the UI has something. + links = repo_link.list_links() or {} + if not links: + return { + "repo_root": None, + "linked": False, + "repo_entries": [], + "promoted_in": [], + "demoted_out": [], + "share_matrix": [], + } + repo_root = Path(next(iter(links.keys()))) + + entries = repo_link.list_entries(repo_root) + limit = max(1, min(int(limit), 1000)) + entry_rows = [e.to_json() for e in entries[:limit]] + + # Personal memories cross-referenced with this repo. We scan + # the personal store for ``promoted_to`` / ``demoted_from_repo`` + # markers we wrote during the original promote/demote calls. + promoted_in: List[Dict[str, Any]] = [] + demoted_out: List[Dict[str, Any]] = [] + try: + from dhee.cli_config import get_memory_instance + + memory = get_memory_instance(None) + try: + personal = memory.get_all( + user_id=_ui_user_id(), + limit=500, + ) or {} + except Exception: # noqa: BLE001 + personal = {} + for record in personal.get("results") or []: + if not isinstance(record, dict): + continue + meta = record.get("metadata") or {} + if isinstance(meta, str): + try: + meta = json.loads(meta) + except (ValueError, TypeError): + meta = {} + # Promoted INTO this repo (this personal memory landed + # in /.dhee/ as an entry). + for promoted in (meta.get("promoted_to") or []): + if not isinstance(promoted, dict): + continue + if str(promoted.get("repo_root") or "") == str(repo_root): + promoted_in.append({ + "memory_id": record.get("id"), + "memory": record.get("memory") or "", + "entry_id": promoted.get("entry_id"), + "promoted_at": promoted.get("at"), + }) + # Demoted OUT of this repo (a repo entry was copied + # into the user's personal store). + if str(meta.get("demoted_from_repo") or "") == str(repo_root): + demoted_out.append({ + "memory_id": record.get("id"), + "memory": record.get("memory") or "", + "entry_id": meta.get("demoted_from_entry"), + "demoted_at": meta.get("demoted_at"), + }) + except Exception as exc: # noqa: BLE001 + log.debug("context/entries: personal cross-ref skipped (%s)", exc) + + # Share matrix: which other linked repos exist, how many + # entries each side has (lightweight overview, no fusion of + # the entries themselves). + share_matrix: List[Dict[str, Any]] = [] + try: + for other_root_str in (repo_link.list_links() or {}).keys(): + other_root = Path(other_root_str) + if other_root == repo_root: + continue + other_entries = repo_link.list_entries(other_root) + share_matrix.append({ + "repo_root": str(other_root), + "label": other_root.name, + "entry_count": len(other_entries), + }) + except Exception as exc: # noqa: BLE001 + log.debug("context/entries: share_matrix skipped (%s)", exc) + + return { + "repo_root": str(repo_root), + "linked": True, + "repo_entries": entry_rows, + "promoted_in": promoted_in, + "demoted_out": demoted_out, + "share_matrix": share_matrix, + "totals": { + "repo_entries": len(entries), + "promoted_in": len(promoted_in), + "demoted_out": len(demoted_out), + "linked_peers": len(share_matrix), + }, + } + + @app.post("/api/context/promote") + def api_context_promote(payload: ContextPromotePayload) -> Dict[str, Any]: + from dhee import repo_link + from dhee.cli_config import get_memory_instance + + memory = get_memory_instance(None) + try: + entry, repo_root = repo_link.promote( + payload.memory_id, + memory=memory, + repo=payload.repo, + kind=payload.kind or "learning", + title=payload.title, + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) + return {"ok": True, "entry": entry.to_json(), "repo_root": str(repo_root)} + + @app.post("/api/context/demote") + def api_context_demote(payload: ContextDemotePayload) -> Dict[str, Any]: + from dhee import repo_link + from dhee.cli_config import get_memory_instance + + memory = get_memory_instance(None) + try: + new_id, entry = repo_link.demote( + payload.entry_id, + memory=memory, + repo=payload.repo, + user_id=_ui_user_id(), + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) + return {"ok": True, "memory_id": new_id, "entry": entry.to_json()} + + @app.delete("/api/local/workspaces/{ws_id}") + def api_workspaces_delete(ws_id: str) -> Dict[str, Any]: + state = _read_local_context_state() + existing = state.get("workspaces") or {} + if ws_id not in existing: + raise HTTPException(status_code=404, detail="workspace not found") + # Folders that belonged to it become orphaned with workspace_id=None; + # the next read will re-stamp them onto the surviving default. + for fpath, frow in (state.get("folders") or {}).items(): + if isinstance(frow, dict) and frow.get("workspace_id") == ws_id: + frow["workspace_id"] = None + existing.pop(ws_id, None) + state["workspaces"] = existing + _write_local_context_state(state) + return {"ok": True, "deleted": ws_id} + + @app.get("/api/context/items") + def api_context_items( + team: Optional[str] = None, + scope: Optional[str] = None, + kind: Optional[str] = None, + project: Optional[str] = None, + limit: int = 200, + ) -> Dict[str, Any]: + try: + svc = _enterprise_service() + try: + rows = svc.store.list_context_items( + org_id=svc.org_id, + team_id=team, + project_id=project, + scope=scope, + limit=int(limit), + ) + if kind: + rows = [r for r in rows if r.get("kind") == kind] + finally: + svc.close() + return {"live": True, "items": rows} + except Exception as exc: # noqa: BLE001 + return {"live": False, "items": [], "error": str(exc)} + + @app.get("/api/context/usage") + def api_context_usage( + team: Optional[str] = None, + scope: Optional[str] = None, + kind: Optional[str] = None, + project: Optional[str] = None, + limit: int = 200, + ) -> Dict[str, Any]: + try: + svc = _enterprise_service() + try: + rows = svc.store.list_context_items( + org_id=svc.org_id, + team_id=team, + project_id=project, + scope=scope, + limit=max(1, min(int(limit), 1000)), + ) + if kind: + rows = [r for r in rows if r.get("kind") == kind] + finally: + svc.close() + except Exception as exc: # noqa: BLE001 + return { + "live": False, + "items": [], + "totals": { + "contexts": 0, + "used_contexts": 0, + "usage_count": 0, + "tokens_served": 0, + "proven_tokens_saved": 0, + "theoretical_api_value_usd": 0.0, + "realized_cost_saved_usd": 0.0, + }, + "budget": _router_budget_payload(), + "error": str(exc), + } + + context_ids = {str(row.get("context_id")) for row in rows if row.get("context_id")} + proven = _context_proven_router_savings(context_ids) + items: List[Dict[str, Any]] = [] + for row in rows: + cid = str(row.get("context_id") or "") + meta = row.get("metadata") if isinstance(row.get("metadata"), dict) else {} + usage_count = _metadata_int(row.get("usage_count")) + token_cost = _metadata_int( + row.get("token_cost"), + max(1, len(str(row.get("summary") or row.get("content") or "")) // 4), + ) + meta_saved = _metadata_int( + meta.get("proven_tokens_saved"), + meta.get("saved_tokens"), + meta.get("tokens_saved"), + meta.get("raw_tokens_saved"), + ) + if not meta_saved: + raw_tokens = _metadata_int(meta.get("raw_tokens")) + summary_tokens = _metadata_int(meta.get("summary_tokens"), row.get("token_cost")) + meta_saved = max(0, raw_tokens - summary_tokens) if raw_tokens else 0 + meta_value = 0.0 + try: + meta_value = max( + 0.0, + float( + meta.get("proven_cost_saved_usd") + or meta.get("cost_saved_usd") + or meta.get("api_value_usd") + or 0.0 + ), + ) + except (TypeError, ValueError): + meta_value = 0.0 + router_row = proven.get(cid) or {} + proven_tokens = int(router_row.get("tokens") or 0) + meta_saved + api_value = float(router_row.get("api_value_usd") or 0.0) + meta_value + items.append({ + "context_id": cid, + "title": row.get("title") or cid, + "scope": row.get("scope"), + "kind": row.get("kind"), + "team_id": row.get("team_id"), + "project_id": row.get("project_id"), + "usage_count": usage_count, + "last_used_at": row.get("last_used_at"), + "token_cost": token_cost, + "tokens_served": usage_count * token_cost, + "proven_tokens_saved": proven_tokens, + "theoretical_api_value_usd": round(api_value, 6), + "realized_cost_saved_usd": 0.0, + "quality_score": row.get("quality_score"), + "freshness_score": row.get("freshness_score"), + "confidence": row.get("confidence"), + "evidence": { + "router_calls": int(router_row.get("calls") or 0), + "metadata_tokens": meta_saved, + "has_direct_savings_evidence": bool(proven_tokens or api_value), + }, + }) + total_api_value = sum(float(item["theoretical_api_value_usd"] or 0.0) for item in items) + monthly_cap = float(_router_budget_payload().get("monthly_budget_usd") or 0.0) + cap_factor = 1.0 + if total_api_value > 0 and monthly_cap > 0: + cap_factor = min(1.0, monthly_cap / total_api_value) + for item in items: + item["realized_cost_saved_usd"] = round( + float(item["theoretical_api_value_usd"] or 0.0) * cap_factor, + 6, + ) + items.sort( + key=lambda item: ( + -int(item.get("usage_count") or 0), + -int(item.get("proven_tokens_saved") or 0), + str(item.get("title") or ""), + ) + ) + realized_total = round(sum(float(item["realized_cost_saved_usd"] or 0.0) for item in items), 6) + return { + "live": True, + "items": items[: max(1, min(int(limit), 1000))], + "totals": { + "contexts": len(items), + "used_contexts": sum(1 for item in items if int(item.get("usage_count") or 0) > 0), + "usage_count": sum(int(item.get("usage_count") or 0) for item in items), + "tokens_served": sum(int(item.get("tokens_served") or 0) for item in items), + "proven_tokens_saved": sum(int(item.get("proven_tokens_saved") or 0) for item in items), + "theoretical_api_value_usd": round(total_api_value, 6), + "realized_cost_saved_usd": realized_total, + }, + "budget": _router_budget_payload(), + "money_math": { + "usage_basis": "actual context injections recorded by Dhee", + "savings_basis": ( + "per-context savings require direct evidence: router metadata " + "with context ids or explicit saved-token metadata on the context item" + ), + "unattributed_note": ( + "Router savings that cannot be tied to a specific context stay in " + "the Router total and are not allocated across contexts." + ), + }, + } + + @app.post("/api/context") + def api_context_upsert(payload: ContextUpsertPayload) -> Dict[str, Any]: + try: + svc = _enterprise_service() + try: + if payload.context_id: + existing = svc.store.get_context_item(svc.org_id, payload.context_id) or {} + expected_hash = str(payload.expected_content_hash or "").strip() + current_hash = str(existing.get("content_hash") or "").strip() + if expected_hash and current_hash and expected_hash != current_hash: + raise HTTPException( + status_code=409, + detail={ + "reason": "context_changed", + "context_id": payload.context_id, + "expected_content_hash": expected_hash, + "current_content_hash": current_hash, + }, + ) + merged = { + **existing, + "context_id": payload.context_id, + "title": payload.title, + "content": payload.content, + "scope": payload.scope, + "kind": payload.kind, + "project_id": payload.project_id or existing.get("project_id"), + "team_id": payload.team_id or existing.get("team_id"), + "user_id": payload.user_id or existing.get("user_id"), + "tags": payload.tags or existing.get("tags") or [], + "summary": payload.summary or "", + "metadata": payload.metadata or existing.get("metadata") or {}, + "org_id": svc.org_id, + "status": "active", + } + item = svc.store.add_context_item(merged) + else: + item = svc.add_context( + title=payload.title, + content=payload.content, + scope=payload.scope, + kind=payload.kind, + project_id=payload.project_id, + team_id=payload.team_id, + user_id=payload.user_id, + tags=payload.tags or [], + summary=payload.summary or "", + metadata=payload.metadata or {}, + ) + finally: + svc.close() + return {"ok": True, "item": item} + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=500, detail=str(exc)) from exc + + @app.post("/api/proposals") + def api_proposal_create(payload: ProposalCreatePayload) -> Dict[str, Any]: + try: + svc = _enterprise_service() + try: + item = svc.propose_context( + title=payload.title, + content=payload.content, + scope=payload.scope, + kind=payload.kind, + proposed_by_user_id=payload.proposed_by_user_id, + project_id=payload.project_id, + team_id=payload.team_id, + tags=payload.tags or [], + supersedes_id=payload.supersedes_id, + metadata=payload.metadata or {}, + ) + finally: + svc.close() + return {"ok": True, "proposal": item} + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=500, detail=str(exc)) from exc + + @app.post("/api/proposals/{context_id}/approve") + def api_proposal_approve(context_id: str, payload: ProposalDecisionPayload) -> Dict[str, Any]: + try: + svc = _enterprise_service() + try: + item = svc.approve_proposal(context_id, reviewer_user_id=payload.reviewer_user_id) + finally: + svc.close() + return {"ok": True, "proposal": item} + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=500, detail=str(exc)) from exc + + @app.post("/api/proposals/{context_id}/reject") + def api_proposal_reject(context_id: str, payload: ProposalDecisionPayload) -> Dict[str, Any]: + try: + svc = _enterprise_service() + try: + item = svc.reject_proposal(context_id, reviewer_user_id=payload.reviewer_user_id) + finally: + svc.close() + return {"ok": True, "proposal": item} + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=500, detail=str(exc)) from exc + + @app.get("/api/inbox") + def api_inbox( + team: Optional[str] = None, + user: Optional[str] = None, + ) -> Dict[str, Any]: + proposals: List[Dict[str, Any]] = [] + findings: List[Dict[str, Any]] = [] + try: + svc = _enterprise_service() + try: + team_ids = [team] if team else None + box = svc.inbox_for_viewer(user_id=user, team_ids=team_ids) + proposals = box.get("proposals") or [] + findings = box.get("findings") or [] + finally: + svc.close() + except Exception: # noqa: BLE001 + pass + # Memory conflicts (existing path) + try: + from dhee.full_memory import FullMemory # type: ignore + + mem = FullMemory() + mem_conflicts = mem.list_conflicts() or [] + mem.close() + except Exception: # noqa: BLE001 + mem_conflicts = [] + return { + "live": True, + "proposals": proposals, + "findings": findings, + "conflicts": mem_conflicts, + "totals": { + "proposals": len(proposals), + "findings": len(findings), + "conflicts": len(mem_conflicts), + }, + } + + @app.post("/api/findings/{finding_id}/resolve") + def api_finding_resolve(finding_id: str, payload: FindingResolvePayload) -> Dict[str, Any]: + try: + svc = _enterprise_service() + try: + item = svc.resolve_finding(finding_id, resolved_by=payload.resolved_by) + finally: + svc.close() + return {"ok": True, "finding": item} + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=500, detail=str(exc)) from exc + + @app.get("/api/backlinks") + def api_backlinks(context_id: str, limit: int = 50) -> Dict[str, Any]: + try: + svc = _enterprise_service() + try: + links = svc.list_backlinks(context_id, limit=int(limit)) + shares = svc.store.list_context_shares(org_id=svc.org_id, context_id=context_id) + finally: + svc.close() + return {"live": True, "backlinks": links, "shares": shares} + except Exception as exc: # noqa: BLE001 + return {"live": False, "backlinks": [], "shares": [], "error": str(exc)} + + # ─── Product screens: context firewall era ─────────────────────────────── + + def _product_safe(label: str, fn, fallback: Any) -> Any: + try: + return fn() + except Exception as exc: # noqa: BLE001 + log.debug("ui product payload %s failed: %s", label, exc) + if isinstance(fallback, dict): + return {**fallback, "live": False, "error": str(exc)} + return fallback + + def _compact_learning_row(row: Dict[str, Any]) -> Dict[str, Any]: + evidence = row.get("evidence") or [] + if not isinstance(evidence, list): + evidence = [] + success_count = int(row.get("success_count") or 0) + failure_count = int(row.get("failure_count") or 0) + if row.get("status") == "promoted": + gate = "approved" + elif success_count: + gate = "successful outcome" + elif failure_count: + gate = "failure evidence" + elif evidence: + gate = str((evidence[0] or {}).get("kind") or "evidence backed") + else: + gate = "needs approval" + return {**row, "evidence_gate": gate, "evidence_count": len(evidence)} + + def _learning_snapshot(limit: int = 80) -> Dict[str, Any]: + from dhee.core.learnings import LearningExchange + + exchange = LearningExchange() + rows = [_compact_learning_row(item.to_dict()) for item in exchange.list()] + rows.sort(key=lambda item: str(item.get("updated_at") or ""), reverse=True) + counts: Dict[str, int] = {} + for row in rows: + status_key = str(row.get("status") or "candidate") + counts[status_key] = counts.get(status_key, 0) + 1 + return { + "live": True, + "repo": _ui_repo(), + "items": rows[: max(1, min(int(limit), 500))], + "totals": { + "all": len(rows), + "candidate": counts.get("candidate", 0), + "promoted": counts.get("promoted", 0), + "rejected": counts.get("rejected", 0), + "archived": counts.get("archived", 0), + }, + } + + def _ui_fast_continuity() -> Dict[str, Any]: + repo = _ui_repo() + pointer = _resolve_repo_pointer() + last_session: Optional[Dict[str, Any]] = None + error = "" + for agent_id in ("codex", "claude-code", "mcp-server"): + try: + from dhee.core.kernel import get_last_session + + last_session = get_last_session( + agent_id=agent_id, + repo=repo, + fallback_log_recovery=False, + user_id=_ui_user_id(), + requester_agent_id="dhee-ui", + ) + if last_session: + break + except Exception as exc: # noqa: BLE001 + error = str(exc) + return { + "live": bool(last_session), + "repo": repo, + "repo_config": pointer, + "last_session": last_session, + "claude_sessions": [], + "error": error or None, + } + + def _ui_light_router_sessions(active: bool = False, limit: int = 20) -> Dict[str, Any]: + try: + db = _get_db() + rows = db.list_agent_sessions(user_id=_ui_user_id(), limit=max(1, min(int(limit), 50))) or [] + items: List[Dict[str, Any]] = [] + for row in rows: + summary = _session_summary(db, row) + item_active = _is_session_active(row) + if active and not item_active: + continue + session_id = str(summary.get("id") or "") + item = { + "session_id": session_id, + "title": _session_title( + summary.get("title"), + preview=summary.get("preview"), + runtime=summary.get("runtime"), + cwd=summary.get("cwd"), + session_id=summary.get("nativeSessionId") or session_id, + ), + "state": summary.get("state") or "", + "agent": summary.get("runtime") or "unknown", + "agents": [summary.get("runtime") or "unknown"], + "cwd": summary.get("cwd") or "", + "repo_root": _git_root_for_path(summary.get("cwd") or "") or summary.get("cwd") or "", + "runtime": summary.get("runtime"), + "model": summary.get("model"), + "updated_at": summary.get("updatedAt") or "", + "started_at": summary.get("startedAt"), + "tokens_saved": 0, + "estimated_cost_saved_usd": 0.0, + "router_calls": 0, + "tool_breakdown": {}, + "active": item_active, + "task": { + "id": summary.get("taskId"), + "status": summary.get("taskStatus"), + }, + "preview": summary.get("preview") or "", + } + items.append(item) + return { + "items": items, + "next_cursor": None, + "active_only": bool(active), + "totals": { + "tokens_saved": 0, + "estimated_cost_saved_usd": 0.0, + "theoretical_api_value_usd": 0.0, + "realized_cost_saved_usd": 0.0, + "router_calls": 0, + "sessions": len(items), + }, + } + except Exception as exc: # noqa: BLE001 + return {"items": [], "totals": {"sessions": 0}, "live": False, "error": str(exc)} + + def _ui_pack_counts() -> Dict[str, Any]: + db = _get_db() + counts: Dict[str, Any] = {"memories": 0, "artifacts": 0, "repo_context_entries": 0} + try: + counts["memories"] = len(db.get_all_memories(user_id=_ui_user_id(), limit=200000) or []) + except Exception: # noqa: BLE001 + pass + try: + counts["artifacts"] = len(db.list_artifacts(user_id=_ui_user_id(), limit=200000) or []) + except Exception: # noqa: BLE001 + pass + try: + counts["repo_context_entries"] = int( + (api_context_entries(repo=_ui_repo(), limit=1000).get("totals") or {}).get("repo_entries") or 0 + ) + except Exception: # noqa: BLE001 + pass + return counts + + def _latest_dheemem_packs(limit: int = 8) -> List[Dict[str, Any]]: + try: + from dhee.protocol import inspect_pack + except Exception: # noqa: BLE001 + inspect_pack = None # type: ignore[assignment] + roots = [ + Path(_dhee_data_dir_str()) / "exports", + Path(_ui_repo()), + ] + seen: set[str] = set() + packs: List[Dict[str, Any]] = [] + for root in roots: + if not root.exists(): + continue + for path in root.glob("*.dheemem"): + resolved = str(path.expanduser().resolve()) + if resolved in seen: + continue + seen.add(resolved) + stat = path.stat() + row: Dict[str, Any] = { + "path": resolved, + "name": path.name, + "size_bytes": stat.st_size, + "updated_at": datetime.fromtimestamp(stat.st_mtime, timezone.utc).isoformat(), + "verified": False, + } + if inspect_pack is not None: + try: + inspected = inspect_pack(path) + row["verified"] = True + row["format"] = inspected.get("format") + row["version"] = inspected.get("version") + row["created_at"] = inspected.get("created_at") + row["files"] = inspected.get("files") + row["handoff"] = inspected.get("handoff") + row["repo_context"] = inspected.get("repo_context") + except Exception as exc: # noqa: BLE001 + row["error"] = str(exc) + packs.append(row) + packs.sort(key=lambda item: str(item.get("updated_at") or ""), reverse=True) + return packs[: max(1, min(int(limit), 50))] + + @app.get("/api/ui/command-center") + def api_ui_command_center() -> Dict[str, Any]: + router_sessions = _product_safe("router_sessions", lambda: _ui_light_router_sessions(active=False, limit=12), {"items": [], "totals": {}}) + router_totals = router_sessions.get("totals") or {} + router = { + "live": bool(router_sessions.get("items")), + "sessionTokensSaved": int(router_totals.get("tokens_saved") or 0), + "totalCalls": int(router_totals.get("router_calls") or 0), + "estimatedCostSavedUsd": router_totals.get("estimated_cost_saved_usd") or 0, + } + continuity = _product_safe("continuity", lambda: _ui_fast_continuity(), {"live": False}) + task_data = _product_safe("tasks", lambda: tasks(), {"tasks": []}) + inbox = _product_safe("inbox", lambda: api_inbox(user=_ui_user_id()), {"totals": {}}) + context_entries = _product_safe( + "context_entries", + lambda: api_context_entries(repo=_ui_repo(), limit=12), + {"linked": False, "repo_entries": [], "totals": {}}, + ) + learnings = _product_safe("learnings", lambda: _learning_snapshot(limit=24), {"items": [], "totals": {}}) + workspaces = _product_safe("workspaces", lambda: list_workspaces_api(), {"workspaces": []}) + sessions = router_sessions.get("items") or [] + active_task = next( + (row for row in (task_data.get("tasks") or []) if str(row.get("status") or "") == "active"), + (task_data.get("tasks") or [None])[0], + ) + next_action = "Start a routed agent task from this repo" + if active_task: + next_action = "Resume the active task with Dhee handoff" + elif int((context_entries.get("totals") or {}).get("repo_entries") or 0) == 0: + next_action = "Promote the first repo context entry" + elif int((learnings.get("totals") or {}).get("candidate") or 0) > 0: + next_action = "Review candidate learnings" + return { + "live": True, + "repo": _ui_repo(), + "active_task": active_task, + "router": router, + "router_sessions": sessions[:8], + "continuity": continuity, + "context": context_entries, + "learnings": learnings, + "inbox": inbox, + "workspaces": workspaces, + "next_action": next_action, + "dhee_aliases": [ + "dhee://state/current", + "dhee://handoff/latest", + "dhee://router/ptr/", + "dhee://repo/context/", + ], + } + + @app.get("/api/ui/handoff") + def api_ui_handoff() -> Dict[str, Any]: + continuity = _product_safe("continuity", lambda: _ui_fast_continuity(), {"live": False}) + task_data = _product_safe("tasks", lambda: tasks(), {"tasks": []}) + router_sessions = _product_safe("router_sessions", lambda: _ui_light_router_sessions(active=False, limit=20), {"items": []}) + last_session = continuity.get("last_session") or {} + confidence = 0.0 + if last_session: + confidence += 0.35 + if last_session.get("files_touched") or last_session.get("filesTouched"): + confidence += 0.2 + if last_session.get("todos") or last_session.get("decisions"): + confidence += 0.25 + if task_data.get("tasks"): + confidence += 0.2 + return { + "live": bool(continuity.get("live") or task_data.get("tasks")), + "repo": _ui_repo(), + "continuity": continuity, + "tasks": task_data.get("tasks") or [], + "sessions": router_sessions.get("items") or [], + "resume_confidence": round(min(confidence, 1.0), 2), + "command": f"dhee handoff --repo {_ui_repo()} --json", + } + + @app.get("/api/ui/proof-replay") + def api_ui_proof_replay(limit: int = 80) -> Dict[str, Any]: + limit = max(10, min(int(limit), 200)) + events: List[Dict[str, Any]] = [] + router_sessions = _product_safe("router_sessions", lambda: _ui_light_router_sessions(active=False, limit=30), {"items": []}) + for row in router_sessions.get("items") or []: + title = str(row.get("title") or row.get("session_id") or "Agent session") + events.append({ + "id": f"router:{row.get('session_id')}", + "time": row.get("updated_at") or row.get("started_at"), + "kind": "digest", + "title": f"Context firewall digested {int(row.get('router_calls') or 0)} tool call(s)", + "detail": title, + "agent": row.get("agent") or row.get("runtime"), + "tokens_saved": row.get("tokens_saved") or 0, + "source": "router session", + "derived": False, + }) + for tool, calls in (row.get("tool_breakdown") or {}).items(): + events.append({ + "id": f"tool:{row.get('session_id')}:{tool}", + "time": row.get("updated_at") or row.get("started_at"), + "kind": "hidden_raw", + "title": f"{tool} raw output held behind pointer", + "detail": f"{calls} routed call(s) stayed outside prompt context until expansion.", + "source": "router metadata", + "derived": True, + }) + try: + db = _get_db() + for task in db.list_shared_tasks(user_id=_ui_user_id(), repo=_ui_repo(), limit=12) or []: + task_id = str(task.get("id") or "") + for result in db.list_shared_task_results(shared_task_id=task_id, limit=12) or []: + events.append({ + "id": f"result:{result.get('id')}", + "time": result.get("updated_at") or result.get("created_at"), + "kind": "evidence", + "title": str(result.get("tool_name") or "Shared result"), + "detail": str(result.get("digest") or result.get("ptr") or "Pointer-backed result"), + "ptr": result.get("ptr"), + "source": "shared task result", + "derived": False, + }) + except Exception: # noqa: BLE001 + pass + context_entries = _product_safe( + "context_entries", + lambda: api_context_entries(repo=_ui_repo(), limit=30), + {"repo_entries": []}, + ) + for entry in context_entries.get("repo_entries") or []: + events.append({ + "id": f"context:{entry.get('id') or entry.get('context_id')}", + "time": entry.get("updated_at") or entry.get("created_at"), + "kind": "injected_context", + "title": str(entry.get("title") or "Repo context available"), + "detail": str(entry.get("summary") or entry.get("content") or ""), + "source": "repo context", + "derived": True, + }) + inbox = _product_safe("inbox", lambda: api_inbox(user=_ui_user_id()), {"proposals": [], "findings": []}) + for item in (inbox.get("proposals") or []) + (inbox.get("findings") or []): + events.append({ + "id": f"review:{item.get('context_id') or item.get('finding_id') or item.get('id')}", + "time": item.get("updated_at") or item.get("created_at"), + "kind": "review", + "title": str(item.get("title") or "Review item"), + "detail": str(item.get("detail") or item.get("summary") or item.get("content") or ""), + "source": "inbox", + "derived": False, + }) + events.sort(key=lambda item: str(item.get("time") or ""), reverse=True) + return { + "live": bool(events), + "repo": _ui_repo(), + "items": events[:limit], + "totals": { + "events": len(events), + "digests": sum(1 for item in events if item.get("kind") == "digest"), + "expansions": sum(1 for item in events if item.get("kind") == "hidden_raw"), + "evidence": sum(1 for item in events if item.get("kind") == "evidence"), + "derived": sum(1 for item in events if item.get("derived")), + }, + } + + @app.get("/api/ui/learnings") + def api_ui_learnings(limit: int = 120) -> Dict[str, Any]: + return _product_safe("learnings", lambda: _learning_snapshot(limit=limit), {"items": [], "totals": {}}) + + @app.post("/api/ui/learnings/{learning_id}/promote") + def api_ui_learning_promote(learning_id: str, payload: UiLearningDecisionPayload) -> Dict[str, Any]: + from dhee.core.learnings import LearningExchange + + try: + item = LearningExchange().promote( + learning_id, + scope=payload.scope or "personal", + repo=payload.repo or _ui_repo(), + approved_by=payload.approved_by or "dhee-ui", + ) + return {"ok": True, "learning": _compact_learning_row(item.to_dict())} + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.post("/api/ui/learnings/{learning_id}/reject") + def api_ui_learning_reject(learning_id: str, payload: UiLearningDecisionPayload) -> Dict[str, Any]: + from dhee.core.learnings import LearningExchange + + try: + item = LearningExchange().reject(learning_id, reason=payload.reason or "rejected in Dhee UI") + return {"ok": True, "learning": _compact_learning_row(item.to_dict())} + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.get("/api/ui/portability") + def api_ui_portability() -> Dict[str, Any]: + try: + from dhee.protocol import PACK_VERSION + except Exception: # noqa: BLE001 + PACK_VERSION = "unknown" # type: ignore[assignment] + return { + "live": True, + "repo": _ui_repo(), + "format": ".dheemem", + "version": PACK_VERSION, + "counts": _ui_pack_counts(), + "packs": _latest_dheemem_packs(), + "contract": [ + "memories", + "history", + "vectors", + "artifacts", + "provenance", + "repo context", + "handoff bootstrap", + ], + "trust": { + "signed": True, + "local_first": True, + "clean_import_dry_run": True, + "no_required_hosted_account": True, + }, + } + + @app.post("/api/ui/portability/export") + def api_ui_portability_export(payload: UiPortabilityExportPayload) -> Dict[str, Any]: + from dhee.cli_config import CONFIG_DIR + from dhee.protocol import export_pack + + repo = payload.repo or _ui_repo() + output = payload.output_path + if not output: + stamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S") + output = str(Path(_dhee_data_dir_str()) / "exports" / f"dhee-ui-{stamp}.dheemem") + try: + memory = _get_memory() + vector_store = getattr(memory, "vector_store", None) + db = getattr(memory, "db", None) or _get_db() + if vector_store is None: + raise RuntimeError("portable export requires a memory vector store") + result = export_pack( + db=db, + vector_store=vector_store, + output_path=output, + user_id=payload.user_id or _ui_user_id(), + key_dir=CONFIG_DIR, + repo=repo, + ) + return {"ok": True, "result": result, "packs": _latest_dheemem_packs()} + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.post("/api/ui/portability/import-dry-run") + def api_ui_portability_import_dry_run(payload: UiPortabilityImportPayload) -> Dict[str, Any]: + from dhee.protocol import import_pack + + input_path = _abs_user_path(payload.input_path) + if not input_path or not Path(input_path).exists(): + raise HTTPException(status_code=400, detail="input_path does not exist") + try: + memory = _get_memory() + vector_store = getattr(memory, "vector_store", None) + db = getattr(memory, "db", None) or _get_db() + if vector_store is None: + raise RuntimeError("portable import dry-run requires a memory vector store") + result = import_pack( + db=db, + vector_store=vector_store, + input_path=input_path, + user_id=payload.user_id or _ui_user_id(), + strategy="dry-run", + repo=payload.repo or _ui_repo(), + ) + return {"ok": True, "result": result} + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.post("/api/integrations") + def api_integration_set(payload: IntegrationPayload) -> Dict[str, Any]: + try: + svc = _enterprise_service() + try: + node = svc.set_integration( + scope=payload.scope, + target_id=payload.target_id, + integration_type=payload.type, + value=payload.value, + metadata=payload.metadata or {}, + ) + finally: + svc.close() + return {"ok": True, "node": node} + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=500, detail=str(exc)) from exc + + @app.post("/api/team-join") + def api_team_join(payload: TeamJoinPayload) -> Dict[str, Any]: + repo_root = _abs_user_path(payload.repo_root) or _ui_repo() + joined = { + "org_id": payload.org_id, + "project_id": payload.project_id, + "team_id": payload.team_id, + "role": payload.role, + "repo_root": repo_root, + "received_at": _now_iso(), + } + try: + out_dir = Path.home() / ".dhee" / "repo_orgs" + out_dir.mkdir(parents=True, exist_ok=True) + digest = hashlib.sha1(str(repo_root).encode("utf-8")).hexdigest()[:16] + (out_dir / f"{digest}.json").write_text( + json.dumps(joined, indent=2), + encoding="utf-8", + ) + except Exception: # noqa: BLE001 + pass + return { + "ok": True, + "joined": joined, + } + + @app.post("/api/teams/{team_id}/collaborators") + def api_team_collaborator_add( + team_id: str, + payload: TeamCollaborationPayload, + ) -> Dict[str, Any]: + try: + svc = _enterprise_service() + try: + result = svc.add_team_collaborator( + team_id=team_id, + target_team_id=payload.target_team_id, + ) + finally: + svc.close() + return {"ok": True, **result} + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=500, detail=str(exc)) from exc + + @app.post("/api/workspace") + def api_workspace_set(payload: EnterpriseWorkspacePayload) -> Dict[str, Any]: + try: + svc = _enterprise_service() + try: + ws = svc.create_workspace( + name=payload.name, + root_path=_abs_user_path(payload.root_path) if payload.root_path else None, + default_branch=payload.default_branch or "main", + ) + finally: + svc.close() + return {"ok": True, "workspace": ws} + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=500, detail=str(exc)) from exc + + @app.post("/api/workspace/reset") + def api_workspace_reset() -> Dict[str, Any]: + try: + svc = _enterprise_service() + try: + counts = svc.reset_workspace() + finally: + svc.close() + return {"ok": True, "deleted": counts} + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=500, detail=str(exc)) from exc + + @app.post("/api/projects") + def api_project_create(payload: EnterpriseProjectCreatePayload) -> Dict[str, Any]: + try: + svc = _enterprise_service() + try: + project = svc.create_project( + project_id=payload.project_id, + name=payload.name, + description=payload.description or "", + ) + finally: + svc.close() + return {"ok": True, "project": project} + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=500, detail=str(exc)) from exc + + @app.delete("/api/projects/{project_id}") + def api_project_delete(project_id: str) -> Dict[str, Any]: + try: + svc = _enterprise_service() + try: + result = svc.delete_project(project_id) + finally: + svc.close() + return {"ok": True, **result} + except ValueError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=500, detail=str(exc)) from exc + + @app.post("/api/projects/{project_id}/teams") + def api_project_team_create( + project_id: str, + payload: ProjectTeamCreatePayload, + ) -> Dict[str, Any]: + try: + svc = _enterprise_service() + try: + team = svc.create_project_team( + project_id=project_id, + team_id=payload.team_id, + name=payload.name, + description=payload.description or "", + ) + finally: + svc.close() + return {"ok": True, "team": team} + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=500, detail=str(exc)) from exc + + @app.post("/api/teams/{team_id}/folders") + def api_team_folder_add( + team_id: str, + payload: ProjectFolderAddPayload, + ) -> Dict[str, Any]: + try: + svc = _enterprise_service() + try: + local = ( + _abs_user_path(payload.local_path) + if payload.local_path else None + ) + result = svc.add_team_folder( + team_id=team_id, + local_path=local, + repo_url=payload.repo_url, + label=payload.label, + kind=payload.kind or "folder", + ) + finally: + svc.close() + return {"ok": True, **result} + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=500, detail=str(exc)) from exc + + @app.post("/api/projects/{project_id}/folders") + def api_project_folder_add( + project_id: str, + payload: ProjectFolderAddPayload, + ) -> Dict[str, Any]: + try: + svc = _enterprise_service() + try: + local = ( + _abs_user_path(payload.local_path) + if payload.local_path else None + ) + result = svc.add_project_folder( + project_id=project_id, + local_path=local, + repo_url=payload.repo_url, + label=payload.label, + kind=payload.kind or "folder", + ) + finally: + svc.close() + return {"ok": True, **result} + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=500, detail=str(exc)) from exc + + @app.post("/api/projects/{project_id}/extract") + def api_project_extract(project_id: str) -> Dict[str, Any]: + try: + svc = _enterprise_service() + try: + result = svc.run_ast_extraction(project_id) + finally: + svc.close() + return {"ok": True, **result} + except ValueError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=500, detail=str(exc)) from exc + + @app.post("/api/teams/{team_id}/extract") + def api_team_extract(team_id: str) -> Dict[str, Any]: + try: + svc = _enterprise_service() + try: + team = svc.store.get_team(svc.org_id, team_id) + if not team: + raise ValueError(f"team_id not found: {team_id}") + project_id = str(team.get("project_id") or "") + if not project_id: + raise ValueError("team has no project_id") + result = svc.run_ast_extraction(project_id, team_id=team_id) + finally: + svc.close() + return {"ok": True, **result} + except ValueError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=500, detail=str(exc)) from exc + + @app.delete("/api/folders/{mapping_id}") + def api_project_folder_remove(mapping_id: str) -> Dict[str, Any]: + try: + svc = _enterprise_service() + try: + result = svc.remove_project_folder(mapping_id) + finally: + svc.close() + return {"ok": True, **result} + except ValueError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + except Exception as exc: # noqa: BLE001 + raise HTTPException(status_code=500, detail=str(exc)) from exc + + # ─── Static SPA ────────────────────────────────────────────────────────── + + if serve_static: + dist = Path(__file__).parent / "web" / "dist" + if dist.exists(): + app.mount("/", StaticFiles(directory=str(dist), html=True), name="spa") + else: + @app.get("/") + def _no_build() -> Dict[str, str]: + return { + "error": "SPA not built. Run `cd dhee/ui/web && npm install && npm run build`.", + "dist_expected": str(dist), + } + + if dev_mode: + import httpx + client = httpx.AsyncClient(base_url="http://127.0.0.1:5173") + + @app.api_route("/{path_name:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH"]) + async def proxy_vite(path_name: str, request: Request): + url = httpx.URL(path=path_name, query=request.url.query.encode("utf-8")) + rp_req = client.build_request( + request.method, + url, + headers=request.headers.raw, + content=await request.body(), + ) + rp_resp = await client.send(rp_req, stream=True) + from fastapi.responses import StreamingResponse + return StreamingResponse( + rp_resp.aiter_raw(), + status_code=rp_resp.status_code, + headers=rp_resp.headers, + background=None, # type: ignore + ) + + @app.websocket("/{path_name:path}") + async def proxy_vite_ws(path_name: str, websocket: WebSocket): + import websockets + await websocket.accept() + try: + # Vite HMR is usually at the root or / (empty path_name) + # But we'll try to connect to Vite's port 5173 + vite_ws_url = f"ws://127.0.0.1:5173/{path_name}" + async with websockets.connect(vite_ws_url) as vite_ws: + # Bi-directional proxy + async def forward_to_vite(): + try: + while True: + data = await websocket.receive_text() + await vite_ws.send(data) + except Exception: + pass + + async def forward_from_vite(): + try: + async for message in vite_ws: + await websocket.send_text(str(message)) + except Exception: + pass + + import asyncio + await asyncio.gather(forward_to_vite(), forward_from_vite()) + except WebSocketDisconnect: + pass + except Exception as e: + log.debug("Vite WS Proxy error: %s", e) + finally: + try: + await websocket.close() + except Exception: + pass + + return app + + +# ─── Helpers ────────────────────────────────────────────────────────────────── + + +def _now_iso() -> str: + return datetime.now(timezone.utc).isoformat() + + +def _ui_user_id() -> str: + return str( + os.environ.get("DHEE_UI_USER_ID") + or os.environ.get("DHEE_USER_ID") + or "default" + ) + + +def _ui_repo() -> str: + return os.path.abspath( + os.path.expanduser(os.environ.get("DHEE_UI_REPO") or os.getcwd()) + ) + + +def _abs_user_path(value: Optional[str]) -> str: + raw = str(value or "").strip() + if not raw: + return "" + return os.path.abspath(os.path.expanduser(raw)) + + +def _display_name(value: Optional[str], *, fallback_path: Optional[str] = None, fallback: str = "Workspace") -> str: + name = str(value or "").strip() + if name: + return name + path = _abs_user_path(fallback_path) + if path: + return os.path.basename(path.rstrip(os.sep)) or path + return fallback + + +def _get_db(): + global _UI_DB + from dhee.configs.base import _dhee_data_dir + from dhee.db.sqlite import SQLiteManager + + db_path = os.environ.get("DHEE_UI_HISTORY_DB") or os.path.join(_dhee_data_dir(), "history.db") + if _UI_DB is None or getattr(_UI_DB, "db_path", None) != db_path: + _UI_DB = SQLiteManager(db_path) + return _UI_DB + + +def _auto_project_name(repo: str) -> str: + return os.path.basename(repo.rstrip(os.sep)) or "Project" + + +def _workspace_folder_mounts(workspace: Optional[Dict[str, Any]]) -> List[Dict[str, Any]]: + if not workspace: + return [] + explicit_mounts = workspace.get("mounts") or workspace.get("folders") + if isinstance(explicit_mounts, list) and explicit_mounts: + mounts: List[Dict[str, Any]] = [] + for item in explicit_mounts: + if isinstance(item, str): + raw_path = item + label = "" + primary = False + elif isinstance(item, dict): + raw_path = str(item.get("path") or item.get("mount_path") or "").strip() + label = str(item.get("label") or "").strip() + primary = bool(item.get("primary") or item.get("is_primary")) + else: + continue + if not raw_path: + continue + resolved = os.path.abspath(os.path.expanduser(raw_path)) + if any(existing["path"] == resolved for existing in mounts): + continue + mounts.append( + { + "path": resolved, + "label": label or os.path.basename(resolved) or resolved, + "primary": primary or not mounts, + } + ) + if mounts: + mounts[0]["primary"] = any(mount.get("primary") for mount in mounts) or True + return mounts + metadata = dict(workspace.get("metadata") or {}) + mounts: List[Dict[str, Any]] = [] + primary_path = str( + workspace.get("workspace_path") + or workspace.get("root_path") + or "" + ).strip() + if primary_path: + mounts.append( + { + "path": os.path.abspath(os.path.expanduser(primary_path)), + "label": str(workspace.get("label") or os.path.basename(primary_path) or primary_path), + "primary": True, + } + ) + for item in metadata.get("folders") or []: + if isinstance(item, str): + raw_path = item + label = "" + elif isinstance(item, dict): + raw_path = str(item.get("path") or "").strip() + label = str(item.get("label") or "").strip() + else: + continue + if not raw_path: + continue + resolved = os.path.abspath(os.path.expanduser(raw_path)) + if any(existing["path"] == resolved for existing in mounts): + continue + mounts.append( + { + "path": resolved, + "label": label or os.path.basename(resolved) or resolved, + "primary": False, + } + ) + return mounts + + +def _workspace_contains_path(workspace: Optional[Dict[str, Any]], path: Optional[str]) -> bool: + raw = str(path or "").strip() + if not workspace or not raw: + return False + candidate = os.path.abspath(os.path.expanduser(raw)) + for mount in _workspace_folder_mounts(workspace): + mount_path = str(mount.get("path") or "").strip() + if not mount_path: + continue + try: + common = os.path.commonpath([candidate, mount_path]) + except ValueError: + continue + if common == mount_path: + return True + return False + + +def _workspace_primary_path(workspace: Optional[Dict[str, Any]]) -> str: + mounts = _workspace_folder_mounts(workspace) + primary = next((mount for mount in mounts if mount.get("primary")), None) or (mounts[0] if mounts else None) + return str((primary or {}).get("path") or workspace.get("root_path") or "").strip() + + +def _workspace_project_scope_rules(db: Any, project_id: str) -> List[Dict[str, Any]]: + try: + return db.list_workspace_project_scope_rules(project_id=project_id, user_id=_ui_user_id()) + except Exception: + return [] + + +def _normalize_scope_rules(rules: Optional[List[Dict[str, Any]]]) -> List[Dict[str, Any]]: + normalized: List[Dict[str, Any]] = [] + seen: set[str] = set() + for item in rules or []: + if not isinstance(item, dict): + continue + prefix = str( + item.get("path_prefix") + or item.get("pathPrefix") + or item.get("path") + or item.get("prefix") + or "" + ).strip() + if not prefix: + continue + resolved = _abs_user_path(prefix) + if not resolved or resolved in seen: + continue + seen.add(resolved) + normalized.append( + { + "path_prefix": resolved, + "label": str(item.get("label") or item.get("name") or "").strip(), + } + ) + return normalized + + +def _resolve_workspace_project_for_path( + db: Any, + *, + workspace_id: str, + path: Optional[str], +) -> Optional[Dict[str, Any]]: + raw = str(path or "").strip() + if not workspace_id: + return None + candidate = os.path.abspath(os.path.expanduser(raw)) if raw else "" + best: Optional[Dict[str, Any]] = None + best_len = -1 + for project in db.list_workspace_projects( + workspace_id=workspace_id, + user_id=_ui_user_id(), + limit=200, + ): + for rule in _workspace_project_scope_rules(db, str(project.get("id") or "")): + prefix = str(rule.get("path_prefix") or "").strip() + if not prefix or not candidate: + continue + try: + common = os.path.commonpath([candidate, prefix]) + except ValueError: + continue + if common != prefix: + continue + if len(prefix) > best_len: + best = project + best_len = len(prefix) + return best + + +def _ensure_unassigned_workspace_project( + db: Any, + workspace: Dict[str, Any], +) -> Dict[str, Any]: + workspace_id = str(workspace.get("id") or "") + for project in db.list_workspace_projects( + workspace_id=workspace_id, + user_id=_ui_user_id(), + limit=200, + ): + if str(project.get("name") or "").strip().lower() == "unassigned": + return project + return db.upsert_workspace_project( + { + "workspace_id": workspace_id, + "user_id": _ui_user_id(), + "name": "Unassigned", + "description": "Sessions that do not match any explicit project scope rule.", + "default_runtime": "codex", + "metadata": {"system": True, "auto_created": True}, + } + ) + + +def _normalize_permission_mode(runtime: Optional[str], value: Optional[str]) -> str: + normalized_runtime = _normalize_runtime(runtime) + raw = str(value or "").strip().lower() + if normalized_runtime != "claude-code": + return "native" + if raw in {"full-access", "full_access", "bypasspermissions", "bypass"}: + return "full-access" + return "standard" + + +_LOCAL_COMMAND_CAVEAT_RE = re.compile( + r".*?(?:|do not respond\.?|$)", + re.IGNORECASE | re.DOTALL, +) + + +def _clean_session_text(value: Any, *, max_len: int = 160) -> str: + text = str(value or "").replace("\x00", " ").strip() + if not text: + return "" + text = _LOCAL_COMMAND_CAVEAT_RE.sub("", text) + text = re.sub( + r"(?is)^caveat:\s*the messages below were generated by the user while running local commands\.\s*do not respond\.?", + "", + text, + ) + text = re.sub(r"\s+", " ", text).strip(" \t\n\r-—:|") + if not text: + return "" + return text[:max_len].rstrip() + + +def _is_bad_session_title(value: Any) -> bool: + raw = str(value or "").strip().lower() + if not raw: + return True + return ( + "local-command-caveat" in raw + or "do not respond" in raw + or raw.startswith("caveat: the messages below were generated") + ) + + +def _fallback_session_title(runtime: Any, cwd: Any = None) -> str: + normalized = _normalize_runtime(runtime) or str(runtime or "").strip().lower() + if normalized == "claude-code": + return "Claude Code session" + if normalized == "codex": + return "Codex session" + folder = os.path.basename(_abs_user_path(cwd).rstrip(os.sep)) if cwd else "" + return f"{folder} session" if folder else "Agent session" + + +def _jsonl_tail_lines(path: Path, *, max_bytes: int = _SESSION_LOG_TAIL_BYTES) -> List[str]: + """Return complete JSONL lines from the end of a potentially huge agent log.""" + try: + size = path.stat().st_size + start = max(0, size - max(1024, int(max_bytes))) + with path.open("rb") as handle: + if start: + handle.seek(start) + handle.readline() + else: + handle.seek(0) + data = handle.read() + except OSError: + return [] + return [ + raw.decode("utf-8", errors="ignore") + for raw in data.splitlines() + if raw.strip() + ] + + +def _session_log_cache_get(kind: str, path: Path, variant: str, stat: os.stat_result) -> Optional[Dict[str, Any]]: + cached = _SESSION_LOG_PARSE_CACHE.get((kind, str(path), variant)) + if not cached: + return None + mtime_ns, size, payload = cached + if mtime_ns == stat.st_mtime_ns and size == stat.st_size: + return dict(payload) + return None + + +def _session_log_cache_put(kind: str, path: Path, variant: str, stat: os.stat_result, payload: Dict[str, Any]) -> Dict[str, Any]: + _SESSION_LOG_PARSE_CACHE[(kind, str(path), variant)] = ( + int(stat.st_mtime_ns), + int(stat.st_size), + dict(payload), + ) + if len(_SESSION_LOG_PARSE_CACHE) > 128: + for key in list(_SESSION_LOG_PARSE_CACHE.keys())[:32]: + _SESSION_LOG_PARSE_CACHE.pop(key, None) + return payload + + +def _session_title( + value: Any, + *, + preview: Any = None, + runtime: Any = None, + cwd: Any = None, + session_id: Any = None, + max_len: int = 120, +) -> str: + candidates = [value] + if preview: + candidates.append(preview) + if session_id: + candidates.append(session_id) + for candidate in candidates: + if _is_bad_session_title(candidate): + continue + cleaned = _clean_session_text(candidate, max_len=max_len) + if cleaned and not _is_bad_session_title(cleaned): + return cleaned + return _fallback_session_title(runtime, cwd) + + +def _ensure_default_project_workspace(db: Any, repo: str) -> tuple[Dict[str, Any], Dict[str, Any]]: + user_id = _ui_user_id() + repo_abs = os.path.abspath(os.path.expanduser(repo)) + workspaces = db.list_workspaces(user_id=user_id, limit=200) + for workspace in workspaces: + if _workspace_contains_path(workspace, repo_abs): + project = _resolve_workspace_project_for_path( + db, + workspace_id=str(workspace.get("id") or ""), + path=repo_abs, + ) or _ensure_unassigned_workspace_project(db, workspace) + return project, workspace + + workspace = db.upsert_workspace( + { + "user_id": user_id, + "name": _auto_project_name(repo_abs), + "description": f"Workspace auto-created for {repo_abs}", + "root_path": repo_abs, + "metadata": {"auto_created": True}, + } + ) + db.upsert_workspace_mount( + { + "workspace_id": workspace["id"], + "user_id": user_id, + "mount_path": repo_abs, + "label": os.path.basename(repo_abs.rstrip(os.sep)) or repo_abs, + "is_primary": True, + } + ) + project = db.upsert_workspace_project( + { + "workspace_id": workspace["id"], + "user_id": user_id, + "name": "General", + "description": f"Default project for {repo_abs}", + "default_runtime": "codex", + "metadata": {"auto_created": True}, + } + ) + db.replace_workspace_project_scope_rules( + project_id=str(project.get("id") or ""), + user_id=user_id, + rules=[ + { + "path_prefix": repo_abs, + "label": "root", + } + ], + ) + return project, workspace + + +def _resolve_workspace_for_path( + db: Any, + *, + path: Optional[str], + project_id: Optional[str] = None, +) -> Optional[Dict[str, Any]]: + raw = str(path or "").strip() + if not raw: + return None + candidate = os.path.abspath(os.path.expanduser(raw)) + best: Optional[Dict[str, Any]] = None + best_len = -1 + for workspace in db.list_workspaces(user_id=_ui_user_id(), limit=500): + if project_id: + project = db.get_workspace_project(project_id, user_id=_ui_user_id()) + if not project or str(project.get("workspace_id") or "") != str(workspace.get("id") or ""): + continue + mounts = db.list_workspace_mounts( + workspace_id=str(workspace.get("id") or ""), + user_id=_ui_user_id(), + ) + workspace_with_mounts = {**workspace, "mounts": mounts} + for mount in _workspace_folder_mounts(workspace_with_mounts): + mount_path = str(mount.get("path") or "").strip() + if not mount_path: + continue + try: + common = os.path.commonpath([candidate, mount_path]) + except ValueError: + continue + if common != mount_path: + continue + if len(mount_path) > best_len: + best = workspace_with_mounts + best_len = len(mount_path) + return best + + +def _session_task_id(runtime_id: str, native_session_id: str) -> str: + return f"task:{runtime_id}:{native_session_id}" + + +def _agent_session_id(runtime_id: str, native_session_id: str) -> str: + return f"session:{runtime_id}:{native_session_id}" + + +_RUNTIME_PROCESS_CACHE: Dict[str, Any] = {"at": 0.0, "items": []} + + +def _process_cwd(pid: Any) -> Optional[str]: + try: + pid_int = int(pid) + except (TypeError, ValueError): + return None + if pid_int <= 0: + return None + proc_cwd = Path("/proc") / str(pid_int) / "cwd" + try: + if proc_cwd.exists(): + return os.path.abspath(os.readlink(proc_cwd)) + except Exception: + pass + try: + result = subprocess.run( + ["lsof", "-a", "-p", str(pid_int), "-d", "cwd", "-Fn"], + capture_output=True, + text=True, + check=False, + timeout=0.6, + ) + except Exception: + return None + for line in (result.stdout or "").splitlines(): + if line.startswith("n") and len(line) > 1: + return os.path.abspath(os.path.expanduser(line[1:])) + return None + + +def _runtime_processes() -> List[Dict[str, Any]]: + now = time.time() + cached_at = float(_RUNTIME_PROCESS_CACHE.get("at") or 0.0) + if now - cached_at < 5: + return list(_RUNTIME_PROCESS_CACHE.get("items") or []) + items: List[Dict[str, Any]] = [] + try: + result = subprocess.run( + ["ps", "-axo", "pid=,comm=,args="], + capture_output=True, + text=True, + check=False, + timeout=0.8, + ) + except Exception: + _RUNTIME_PROCESS_CACHE.update({"at": now, "items": []}) + return [] + for line in (result.stdout or "").splitlines(): + parts = line.strip().split(None, 2) + if len(parts) < 2: + continue + pid = parts[0] + comm = parts[1] + args = parts[2] if len(parts) > 2 else comm + haystack = f"{comm} {args}".lower() + lower_args = str(args or "").lower() + runtime_id = "" + if os.path.basename(comm).lower() == "claude" or re.search(r"(^|[/\s])claude(\s|$)", haystack): + runtime_id = "claude-code" + elif ( + ".app/" not in lower_args + and "codex computer use" not in lower_args + and ( + os.path.basename(comm).lower() == "codex" + or re.search(r"(^|[/\s])codex(\s|$)", haystack) + ) + ): + runtime_id = "codex" + if not runtime_id: + continue + items.append( + { + "pid": int(pid) if str(pid).isdigit() else pid, + "runtime_id": runtime_id, + "command": args, + "cwd": _process_cwd(pid), + } + ) + _RUNTIME_PROCESS_CACHE.update({"at": now, "items": items}) + return list(items) + + +def _paths_overlap(left: Optional[str], right: Optional[str]) -> bool: + left_abs = _abs_user_path(left) + right_abs = _abs_user_path(right) + if not left_abs or not right_abs: + return False + try: + common = os.path.commonpath([left_abs, right_abs]) + except ValueError: + return False + return common in {left_abs, right_abs} + + +def _runtime_has_process_for_path(runtime_id: str, path: Optional[str]) -> bool: + candidate = _abs_user_path(path) + if not candidate: + return False + for proc in _runtime_processes(): + if str(proc.get("runtime_id") or "") != runtime_id: + continue + if _paths_overlap(proc.get("cwd"), candidate): + return True + return False + + +def _recent_enough(value: Any, *, seconds: int = 1800) -> bool: + dt = _coerce_datetime(value) + if dt is None: + return False + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + age = datetime.now(timezone.utc) - dt.astimezone(timezone.utc) + return 0 <= age.total_seconds() <= seconds + + +def _workspace_scan_roots(db: Any, extra_paths: Optional[List[str]] = None) -> List[str]: + roots: List[str] = [] + seen: set[str] = set() + + def add(path: Optional[str]) -> None: + resolved = _abs_user_path(path) + if not resolved or resolved in seen: + return + seen.add(resolved) + roots.append(resolved) + + add(_ui_repo()) + for path in extra_paths or []: + add(path) + try: + workspaces = db.list_workspaces(user_id=_ui_user_id(), limit=500) + except Exception: + workspaces = [] + for workspace in workspaces: + add(workspace.get("root_path")) + workspace_id = str(workspace.get("id") or "") + try: + mounts = db.list_workspace_mounts(workspace_id=workspace_id, user_id=_ui_user_id()) + except Exception: + mounts = [] + for mount in mounts: + add(mount.get("mount_path") or mount.get("path")) + return roots + + +def _mirror_runtime_cache_key(extra_paths: Optional[List[str]]) -> Tuple[str, ...]: + parts = [_abs_user_path(_ui_repo()) or _ui_repo()] + for path in extra_paths or []: + resolved = _abs_user_path(path) + if resolved: + parts.append(resolved) + return tuple(sorted(set(parts))) + + +def _mirror_runtime_sessions(db: Any, extra_paths: Optional[List[str]] = None) -> Dict[str, Any]: + cache_key = _mirror_runtime_cache_key(extra_paths) + now = time.monotonic() + cached = _MIRROR_RUNTIME_CACHE.get(cache_key) + if cached and now - cached[0] <= _MIRROR_RUNTIME_CACHE_TTL_SECONDS: + return cached[1] + + with _MIRROR_RUNTIME_LOCK: + now = time.monotonic() + cached = _MIRROR_RUNTIME_CACHE.get(cache_key) + if cached and now - cached[0] <= _MIRROR_RUNTIME_CACHE_TTL_SECONDS: + return cached[1] + result = _mirror_runtime_sessions_uncached(db, extra_paths=extra_paths) + _MIRROR_RUNTIME_CACHE[cache_key] = (time.monotonic(), result) + if len(_MIRROR_RUNTIME_CACHE) > 32: + for key in list(_MIRROR_RUNTIME_CACHE.keys())[:8]: + _MIRROR_RUNTIME_CACHE.pop(key, None) + return result + + +def _mirror_runtime_sessions_uncached(db: Any, extra_paths: Optional[List[str]] = None) -> Dict[str, Any]: + repo = _ui_repo() + project, default_workspace = _ensure_default_project_workspace(db, repo) + mirrored: List[Dict[str, Any]] = [] + seen_sessions: set[tuple[str, str]] = set() + + def mirror_one( + runtime_id: str, + native_session_id: str, + *, + title: str, + cwd: Optional[str], + model: Optional[str], + state: str, + rollout_path: Optional[str] = None, + started_at: Optional[str] = None, + updated_at: Optional[str] = None, + permission_mode: Optional[str] = None, + metadata: Optional[Dict[str, Any]] = None, + is_current: bool = False, + repo_hint: Optional[str] = None, + ) -> Dict[str, Any]: + dedupe_key = (runtime_id, native_session_id) + if dedupe_key in seen_sessions: + return next((item for item in mirrored if item.get("runtime_id") == runtime_id and item.get("native_session_id") == native_session_id), {}) + seen_sessions.add(dedupe_key) + task_repo = _abs_user_path(repo_hint or cwd or repo) or repo + workspace = _resolve_workspace_for_path(db, path=cwd, project_id=None) or default_workspace + resolved_project = _resolve_workspace_project_for_path( + db, + workspace_id=str(workspace.get("id") or ""), + path=cwd or task_repo, + ) or _ensure_unassigned_workspace_project(db, workspace) + session_id = _agent_session_id(runtime_id, native_session_id) + task_id = _session_task_id(runtime_id, native_session_id) + task_status = "active" if is_current or state == "active" else "paused" + metadata_payload = metadata or {} + clean_title = _session_title( + title, + preview=metadata_payload.get("preview"), + runtime=runtime_id, + cwd=cwd, + session_id=native_session_id, + ) + task = db.upsert_shared_task( + { + "id": task_id, + "user_id": _ui_user_id(), + "project_id": resolved_project["id"], + "repo": task_repo, + "workspace_id": workspace["id"], + "folder_path": ".", + "session_id": session_id, + "thread_id": native_session_id, + "runtime_id": runtime_id, + "native_session_id": native_session_id, + "title": clean_title, + "status": task_status, + "created_by": runtime_id, + "metadata": { + "harness": runtime_id, + "project_id": resolved_project["id"], + "workspace_id": workspace["id"], + "session_id": session_id, + "native_session_id": native_session_id, + "permission_mode": permission_mode, + "is_current": is_current, + **metadata_payload, + }, + } + ) + session = db.upsert_agent_session( + { + "id": session_id, + "project_id": resolved_project["id"], + "workspace_id": workspace["id"], + "user_id": _ui_user_id(), + "runtime_id": runtime_id, + "native_session_id": native_session_id, + "task_id": task["id"], + "title": clean_title, + "state": state, + "model": model, + "cwd": cwd, + "rollout_path": rollout_path, + "permission_mode": permission_mode, + "started_at": started_at, + "updated_at": updated_at or _now_iso(), + "metadata": metadata_payload, + } + ) + mirrored.append(session) + return session + + for scan_root in _workspace_scan_roots(db, extra_paths=extra_paths): + for thread in _repo_codex_threads(scan_root, limit=_RUNTIME_MIRROR_CODEX_LIMIT): + thread_id = str(thread.get("id") or "").strip() + if not thread_id: + continue + thread_cwd = str(thread.get("cwd") or scan_root) + is_current = bool(thread.get("isCurrent")) + mirror_one( + "codex", + thread_id, + title=str(thread.get("title") or "Untitled Codex session"), + cwd=thread_cwd, + model=thread.get("model"), + state="active" if is_current else str(thread.get("state") or "recent"), + rollout_path=thread.get("rolloutPath"), + started_at=thread.get("startedAt"), + updated_at=thread.get("updatedAt"), + permission_mode="native", + metadata={ + "messages": thread.get("messages") or [], + "recent_tools": thread.get("recentTools") or [], + "plan": thread.get("plan") or [], + "touched_files": thread.get("touchedFiles") or [], + "rate_limits": thread.get("rateLimits") or {}, + "updated_at_label": thread.get("updatedAtLabel"), + "preview": thread.get("preview"), + "is_current": is_current, + }, + is_current=is_current, + repo_hint=scan_root, + ) + + for claude_session in _find_claude_sessions(scan_root, limit=_RUNTIME_MIRROR_CLAUDE_LIMIT): + native_id = str(claude_session.get("id") or "claude-local") + if not native_id: + continue + claude_cwd = str(claude_session.get("cwd") or scan_root) + is_current = str(claude_session.get("state") or "") == "active" + mirror_one( + "claude-code", + native_id, + title=str(claude_session.get("title") or "Claude Code session"), + cwd=claude_cwd, + model=claude_session.get("model"), + state=str(claude_session.get("state") or "recent"), + started_at=claude_session.get("startedAt"), + updated_at=claude_session.get("updatedAt"), + permission_mode=str(claude_session.get("permissionMode") or "native"), + metadata={ + "version": claude_session.get("version"), + "entrypoint": claude_session.get("entrypoint"), + "note": claude_session.get("note"), + "messages": claude_session.get("messages") or [], + "recent_tools": claude_session.get("recentTools") or [], + "plan": [], + "touched_files": claude_session.get("touchedFiles") or [], + "rate_limits": {}, + "token_usage": claude_session.get("tokenUsage") or {}, + "last_token_usage": claude_session.get("lastTokenUsage") or {}, + "token_usage_complete": claude_session.get("tokenUsageComplete"), + "preview": claude_session.get("preview"), + "is_current": is_current, + "pid": claude_session.get("pid"), + }, + is_current=is_current, + repo_hint=scan_root, + ) + + return {"project": project, "workspace": default_workspace, "sessions": mirrored} + + +def _normalize_runtime(value: Optional[str]) -> Optional[str]: + raw = str(value or "").strip().lower() + if not raw: + return None + if raw in {"claude", "claude-code", "claude_code"}: + return "claude-code" + if raw == "codex": + return "codex" + if raw == "both": + return "both" + return None + + +def _router_payg_pricing(runtime: Any, model: Any) -> Dict[str, Any]: + """Return PAYG input-token pricing used to value avoided raw reads.""" + + haystack = re.sub( + r"[\s_]+", + "-", + f"{runtime or ''} {model or ''}".strip().lower(), + ) + + def pricing( + provider: str, + family: str, + input_rate: float, + cached_rate: Optional[float], + output_rate: Optional[float], + source: str, + note: str, + ) -> Dict[str, Any]: + return { + "provider": provider, + "model_family": family, + "input_cost_per_million": input_rate, + "cached_input_cost_per_million": cached_rate, + "output_cost_per_million": output_rate, + "currency": "USD", + "unit": "1M input tokens", + "source": source, + "note": note, + } + + anthropic_source = "https://www.anthropic.com/api" + openai_source = "https://openai.com/api/pricing/" + codex_source = "https://developers.openai.com/api/docs/pricing" + + if "claude" in haystack or "anthropic" in haystack: + if "haiku-3" in haystack or "3-haiku" in haystack: + return pricing( + "anthropic", + "Claude 3 Haiku", + 0.25, + None, + 1.25, + anthropic_source, + "Input-token estimate for avoided raw context.", + ) + if "3.5-haiku" in haystack or "3-5-haiku" in haystack: + return pricing( + "anthropic", + "Claude 3.5 Haiku", + 0.80, + None, + 4.0, + anthropic_source, + "Input-token estimate for avoided raw context.", + ) + if "haiku" in haystack: + return pricing( + "anthropic", + "Claude Haiku 4.5", + 1.0, + 0.10, + 5.0, + anthropic_source, + "Input-token estimate for avoided raw context.", + ) + if "sonnet" in haystack: + return pricing( + "anthropic", + "Claude Sonnet 4.6", + 3.0, + 0.30, + 15.0, + anthropic_source, + "Input-token estimate for avoided raw context.", + ) + if "opus-4.1" in haystack or "opus-4-1" in haystack: + return pricing( + "anthropic", + "Claude Opus 4.1", + 15.0, + None, + 75.0, + anthropic_source, + "Input-token estimate for avoided raw context.", + ) + if "opus" in haystack: + return pricing( + "anthropic", + "Claude Opus 4.7", + 5.0, + 0.50, + 25.0, + anthropic_source, + "Input-token estimate for avoided raw context.", + ) + return pricing( + "anthropic", + "Unpriced Claude model", + 0.0, + None, + None, + anthropic_source, + "Claude runtime was captured, but the exact model was not mapped to an official price.", + ) + + if "gpt-5.5" in haystack or "gpt-5-5" in haystack: + return pricing( + "openai", + "GPT-5.5", + 5.0, + 0.50, + 30.0, + openai_source, + "Input-token estimate for avoided raw context.", + ) + if "gpt-5.4-mini" in haystack or "gpt-5-4-mini" in haystack: + return pricing( + "openai", + "GPT-5.4 mini", + 0.75, + 0.075, + 4.50, + openai_source, + "Input-token estimate for avoided raw context.", + ) + if "gpt-5.4" in haystack or "gpt-5-4" in haystack: + return pricing( + "openai", + "GPT-5.4", + 2.50, + 0.25, + 15.0, + openai_source, + "Input-token estimate for avoided raw context.", + ) + if ( + "gpt-5.3-codex" in haystack + or "gpt-5-3-codex" in haystack + or "gpt-5.2-codex" in haystack + or "gpt-5-2-codex" in haystack + ): + return pricing( + "openai", + "GPT-5.3-Codex", + 1.75, + 0.175, + 14.0, + codex_source, + "Input-token estimate for avoided raw context.", + ) + if "gpt-5.2" in haystack or "gpt-5-2" in haystack: + return pricing( + "openai", + "GPT-5.2", + 1.75, + 0.175, + 14.0, + openai_source, + "Input-token estimate for avoided raw context.", + ) + if "gpt-5.1" in haystack or "gpt-5-1" in haystack or re.search(r"\bgpt-5\b", haystack): + return pricing( + "openai", + "GPT-5", + 1.25, + 0.125, + 10.0, + openai_source, + "Input-token estimate for avoided raw context.", + ) + if "gpt-4.1" in haystack or "gpt-4-1" in haystack: + return pricing( + "openai", + "GPT-4.1", + 2.0, + 0.50, + 8.0, + openai_source, + "Input-token estimate for avoided raw context.", + ) + if "gpt-4o" in haystack: + return pricing( + "openai", + "GPT-4o", + 2.50, + 1.25, + 10.0, + openai_source, + "Input-token estimate for avoided raw context.", + ) + if "codex" in haystack: + return pricing( + "openai", + "Unpriced Codex model", + 0.0, + None, + None, + codex_source, + "Codex runtime was captured, but the exact model was not mapped to an official price.", + ) + if "gpt" in haystack or "openai" in haystack: + return pricing( + "openai", + "Unpriced OpenAI model", + 0.0, + None, + None, + openai_source, + "OpenAI runtime was captured, but the exact model was not mapped to an official price.", + ) + + return pricing( + "unknown", + "Unpriced model", + 0.0, + None, + None, + "", + "Provider/model was not captured, so Dhee does not estimate dollar savings.", + ) + + +def _router_monthly_budget_usd() -> float: + """Maximum monthly dollar value Dhee should claim as realised savings. + + The default matches the product assumption discussed in the UI work: + $20 base + $20 Claude + $20 Codex = $60/month. Teams can override this + with DHEE_MONTHLY_AI_BUDGET_USD. Provider-specific env vars are also + accepted; if any are set, their sum wins. + """ + + def _env_float(name: str) -> Optional[float]: + raw = os.environ.get(name) + if raw in (None, ""): + return None + try: + return max(0.0, float(raw)) + except (TypeError, ValueError): + return None + + provider_values = [ + _env_float("DHEE_BASE_MONTHLY_BUDGET_USD"), + _env_float("DHEE_CLAUDE_MONTHLY_BUDGET_USD"), + _env_float("DHEE_CODEX_MONTHLY_BUDGET_USD"), + ] + if any(value is not None for value in provider_values): + return round(sum(value or 0.0 for value in provider_values), 6) + return round(_env_float("DHEE_MONTHLY_AI_BUDGET_USD") or 60.0, 6) + + +def _router_budget_payload() -> Dict[str, Any]: + monthly = _router_monthly_budget_usd() + return { + "currency": "USD", + "monthly_budget_usd": monthly, + "daily_budget_usd": round(monthly / 30.0, 6), + "weekly_budget_usd": round(monthly * 7.0 / 30.0, 6), + "yearly_budget_usd": round(monthly * 12.0, 6), + "basis": "configured monthly AI budget", + "note": ( + "Realised saved dollars are capped by the user's configured paid " + "budget. Avoided API value is shown separately and can exceed this " + "only as an estimate, not as claimed cash saved." + ), + } + + +def _budget_cap_usd(value: float, *, period: str = "month") -> float: + budget = _router_budget_payload() + key = { + "day": "daily_budget_usd", + "week": "weekly_budget_usd", + "month": "monthly_budget_usd", + "year": "yearly_budget_usd", + }.get(period, "monthly_budget_usd") + cap = float(budget.get(key) or 0.0) + return round(min(max(0.0, float(value or 0.0)), cap), 6) + + +def _context_ids_from_meta(meta: Dict[str, Any]) -> List[str]: + out: List[str] = [] + for key in ( + "context_id", + "context_ids", + "selected_context_ids", + "injected_context_ids", + "dhee_context_ids", + ): + value = meta.get(key) + if not value: + continue + if isinstance(value, str): + bits = re.split(r"[\s,]+", value) + elif isinstance(value, (list, tuple, set)): + bits = [str(v) for v in value] + else: + bits = [str(value)] + out.extend(bit.strip() for bit in bits if bit and bit.strip()) + return sorted(set(out)) + + +def _metadata_int(*values: Any) -> int: + for value in values: + try: + if value in (None, ""): + continue + return max(0, int(float(value))) + except (TypeError, ValueError): + continue + return 0 + + +def _context_proven_router_savings(context_ids: set[str]) -> Dict[str, Dict[str, Any]]: + if not context_ids: + return {} + from dhee.router import ptr_store as _ptr + from dhee.router import stats as _rstats + + out: Dict[str, Dict[str, Any]] = { + cid: {"tokens": 0, "api_value_usd": 0.0, "calls": 0} + for cid in context_ids + } + try: + ptr_root = _ptr._root() + except Exception: # noqa: BLE001 + return out + if not ptr_root.exists(): + return out + chars_per_token = float(getattr(_rstats, "CHARS_PER_TOKEN", 4)) + for sdir in ptr_root.iterdir(): + if not sdir.is_dir(): + continue + for meta_file in sdir.glob("*.json"): + try: + meta = json.loads(meta_file.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + continue + if not isinstance(meta, dict): + continue + matched = [cid for cid in _context_ids_from_meta(meta) if cid in context_ids] + if not matched: + continue + tokens = int(_rstats._stored_chars(meta, meta_file) / chars_per_token) if chars_per_token else 0 + if tokens <= 0: + continue + pricing = _router_payg_pricing( + meta.get("harness") or meta.get("agent_id"), + meta.get("model"), + ) + api_value = tokens * float(pricing.get("input_cost_per_million") or 0.0) / 1_000_000 + share_tokens = int(tokens / max(1, len(matched))) + share_value = api_value / max(1, len(matched)) + for cid in matched: + row = out.setdefault(cid, {"tokens": 0, "api_value_usd": 0.0, "calls": 0}) + row["tokens"] += share_tokens + row["api_value_usd"] += share_value + row["calls"] += 1 + return out + + +def _coerce_datetime(value: Any) -> Optional[datetime]: + if value in (None, ""): + return None + if isinstance(value, datetime): + return value + if isinstance(value, (int, float)): + ts = float(value) + if ts > 1_000_000_000_000: + ts /= 1000.0 + return datetime.fromtimestamp(ts, tz=timezone.utc) + raw = str(value).strip() + if not raw: + return None + try: + if raw.endswith("Z"): + raw = raw[:-1] + "+00:00" + return datetime.fromisoformat(raw) + except ValueError: + pass + try: + ts = float(raw) + except ValueError: + return None + if ts > 1_000_000_000_000: + ts /= 1000.0 + return datetime.fromtimestamp(ts, tz=timezone.utc) + + +def _format_ui_clock(value: Any) -> str: + dt = _coerce_datetime(value) + if dt is None: + return time.strftime("%H:%M") + return dt.astimezone().strftime("%H:%M") + + +def _iso_or_none(value: Any) -> Optional[str]: + dt = _coerce_datetime(value) + if dt is None: + return None + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + return dt.astimezone(timezone.utc).isoformat() + + +def _path_matches_repo(candidate: Optional[str], repo: Optional[str]) -> bool: + if not candidate or not repo: + return False + try: + candidate_abs = os.path.abspath(os.path.expanduser(str(candidate))) + repo_abs = os.path.abspath(os.path.expanduser(str(repo))) + return os.path.commonpath([candidate_abs, repo_abs]) == repo_abs + except Exception: + return False + + +def _pid_alive(pid: Any) -> bool: + try: + pid_int = int(pid) + except (TypeError, ValueError): + return False + if pid_int <= 0: + return False + try: + os.kill(pid_int, 0) + except OSError: + return False + return True + + +def _task_color(status: Optional[str]) -> str: + value = str(status or "").strip().lower() + if value == "active": + return "green" + if value == "paused": + return "indigo" + if value in {"completed", "closed"}: + return "orange" + if value == "abandoned": + return "rose" + return "green" + + +def _shared_task_result_to_ui_message(result: Dict[str, Any]) -> Optional[Dict[str, Any]]: + digest = str(result.get("digest") or "").strip() + if not digest: + return None + packet_kind = str(result.get("packet_kind") or "result").strip().lower() + tool_name = str(result.get("tool_name") or packet_kind or "update").strip() + result_status = str(result.get("result_status") or "completed").strip().lower() + created_at = _iso_or_none(result.get("updated_at") or result.get("created_at")) + if packet_kind == "note" or tool_name == "user_note": + return { + "id": str(result.get("id") or f"note:{hash(digest)}"), + "role": "user", + "content": digest[:1600], + "createdAt": created_at, + "label": "note", + } + prefix = tool_name.replace("_", " ").strip() + if result_status == "in_flight": + prefix = f"{prefix} · in flight" + return { + "id": str(result.get("id") or f"result:{hash(digest)}"), + "role": "agent", + "content": f"{prefix}: {digest[:1600]}", + "createdAt": created_at, + "label": packet_kind, + } + + +def _shared_task_messages(title: str, results: List[Dict[str, Any]], *, task_id: str) -> List[Dict[str, Any]]: + messages: List[Dict[str, Any]] = [ + { + "id": f"{task_id}:seed", + "role": "user", + "content": title, + } + ] + for result in reversed(results): + message = _shared_task_result_to_ui_message(result) + if message: + messages.append(message) + return messages + + +def _shared_task_to_ui_task( + db: Any, + row: Dict[str, Any], + *, + index: int, + result_limit: int = 3, +) -> Dict[str, Any]: + metadata = dict(row.get("metadata") or {}) + task_id = str(row.get("id") or f"task-{index + 1}") + title = str(row.get("title") or "(untitled)") + harness = _normalize_runtime(metadata.get("harness")) + results = db.list_shared_task_results(shared_task_id=task_id, limit=result_limit) + messages = _shared_task_messages(title, results, task_id=task_id) + return { + "id": task_id, + "color": _task_color(row.get("status")), + "title": title, + "created": _format_ui_clock(row.get("created_at") or row.get("updated_at")), + "updatedAt": _iso_or_none(row.get("updated_at")), + "status": str(row.get("status") or "active"), + "links": metadata.get("links") or [], + "pos": {"x": 150 + (index % 3) * 260, "y": 180 + (index // 3) * 200}, + "harness": harness, + "source": str(row.get("created_by") or metadata.get("created_via") or "dhee"), + "messages": messages, + } + + +def _get_shared_task_or_404(db: Any, task_id: str) -> Dict[str, Any]: + row = db.get_shared_task(task_id, user_id=_ui_user_id()) + if not row: + raise HTTPException(status_code=404, detail="Task not found") + return row + + +def _touch_shared_task( + db: Any, + row: Dict[str, Any], + *, + status: Optional[str] = None, + metadata_updates: Optional[Dict[str, Any]] = None, +) -> Dict[str, Any]: + metadata = dict(row.get("metadata") or {}) + metadata.update(metadata_updates or {}) + return db.upsert_shared_task( + { + "id": row.get("id"), + "user_id": row.get("user_id") or _ui_user_id(), + "repo": row.get("repo") or _ui_repo(), + "workspace_id": row.get("workspace_id") or _ui_repo(), + "folder_path": row.get("folder_path"), + "title": row.get("title"), + "status": status or row.get("status") or "active", + "created_by": row.get("created_by") or "sankhya-ui", + "metadata": metadata, + } + ) + + +def _claude_message_text(content: Any) -> str: + if isinstance(content, str): + return content.strip() + if isinstance(content, list): + parts: List[str] = [] + for block in content: + if isinstance(block, dict) and block.get("type") == "text": + text = str(block.get("text") or "").strip() + if text: + parts.append(text) + return "\n".join(parts).strip() + return "" + + +def _claude_log_details(path: Path, *, fallback_cwd: str) -> Dict[str, Any]: + try: + stat = path.stat() + except OSError: + stat = None + if stat is not None: + cached = _session_log_cache_get("claude", path, fallback_cwd, stat) + if cached is not None: + return cached + + messages: deque[Dict[str, Any]] = deque(maxlen=8) + recent_tools: deque[str] = deque(maxlen=8) + touched_files: set[str] = set() + session_id = path.stem + cwd = fallback_cwd + version = None + model = None + started_at = None + updated_at = _iso_or_none(stat.st_mtime) if stat is not None else None + first_user = "" + last_user = "" + last_assistant = "" + permission_mode = "native" + usage_seen: set[str] = set() + token_usage: Dict[str, int] = { + "input_tokens": 0, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "output_tokens": 0, + } + last_token_usage: Dict[str, int] = {} + + def absorb_usage(raw_usage: Any, usage_key: str) -> None: + nonlocal last_token_usage + if not isinstance(raw_usage, dict) or not usage_key or usage_key in usage_seen: + return + usage_seen.add(usage_key) + current = { + "input_tokens": int(raw_usage.get("input_tokens") or 0), + "cache_creation_input_tokens": int(raw_usage.get("cache_creation_input_tokens") or 0), + "cache_read_input_tokens": int(raw_usage.get("cache_read_input_tokens") or 0), + "output_tokens": int(raw_usage.get("output_tokens") or 0), + } + for key, value in current.items(): + token_usage[key] = token_usage.get(key, 0) + value + last_token_usage = current + + for line in _jsonl_tail_lines(path): + try: + item = json.loads(line) + except json.JSONDecodeError: + continue + session_id = str(item.get("sessionId") or session_id) + cwd = str(item.get("cwd") or cwd or fallback_cwd) + version = item.get("version") or version + timestamp = item.get("timestamp") + if timestamp and started_at is None: + started_at = _iso_or_none(timestamp) + if timestamp: + updated_at = _iso_or_none(timestamp) or updated_at + if item.get("type") == "permission-mode": + raw_mode = str(item.get("permissionMode") or item.get("mode") or "").strip() + if raw_mode: + permission_mode = _normalize_permission_mode("claude-code", raw_mode) + message = item.get("message") or {} + if isinstance(message, dict): + role = str(message.get("role") or item.get("type") or "").strip() + model = message.get("model") or model + content = message.get("content") + absorb_usage( + message.get("usage"), + str(message.get("id") or item.get("requestId") or item.get("uuid") or timestamp or ""), + ) + text = _claude_message_text(content) + if text and role in {"user", "assistant"}: + messages.append( + { + "role": role, + "content": text, + "timestamp": timestamp, + } + ) + if role == "user": + if not first_user: + first_user = text + last_user = text + elif role == "assistant": + last_assistant = text + if isinstance(content, list): + for block in content: + if not isinstance(block, dict) or block.get("type") != "tool_use": + continue + tool_name = str(block.get("name") or "").strip() + if tool_name: + recent_tools.append(tool_name) + tool_input = block.get("input") or {} + if isinstance(tool_input, dict): + file_path = str( + tool_input.get("file_path") + or tool_input.get("path") + or "" + ).strip() + if file_path: + touched_files.add(_abs_user_path(file_path) or file_path) + + preview = last_assistant or last_user + title = _session_title( + first_user or last_user or path.stem, + preview=last_user or preview, + runtime="claude-code", + cwd=cwd or fallback_cwd, + session_id=session_id, + ) + result = { + "id": session_id, + "cwd": cwd or fallback_cwd, + "title": title, + "model": model, + "startedAt": started_at, + "updatedAt": updated_at, + "version": version, + "permissionMode": permission_mode, + "messages": list(messages), + "recentTools": list(recent_tools), + "touchedFiles": sorted(touched_files)[:80], + "preview": preview[:600], + "logPath": str(path), + "tokenUsage": token_usage, + "lastTokenUsage": last_token_usage, + "tokenUsageComplete": bool(stat is not None and stat.st_size <= _SESSION_LOG_TAIL_BYTES), + } + if stat is not None: + return _session_log_cache_put("claude", path, fallback_cwd, stat, result) + return result -from dhee.demo import token_router_demo +def _claude_logs_for_repo(repo: str, *, limit: int = 6) -> List[Path]: + try: + from dhee.core.log_parser import _escape_path + except Exception: + escaped = repo.replace("/", "-").replace("\\", "-") + else: + escaped = _escape_path(repo) + root = Path.home() / ".claude" / "projects" / escaped + if not root.is_dir(): + return [] + try: + files = [path for path in root.iterdir() if path.is_file() and path.suffix == ".jsonl"] + except OSError: + return [] + return sorted(files, key=lambda p: p.stat().st_mtime, reverse=True)[: max(1, int(limit))] -STATIC_DIR = Path(__file__).with_name("static") -_LOOPBACK_HOSTS = {"127.0.0.1", "localhost", "::1"} +def _find_claude_sessions(repo: str, limit: int = 6) -> List[Dict[str, Any]]: + repo_abs = _abs_user_path(repo) + sessions: Dict[str, Dict[str, Any]] = {} -def _org_from_env(default: str = "local") -> str: - return os.environ.get("DHEE_UI_ORG_ID", default) + root = Path.home() / ".claude" / "sessions" + if root.exists(): + for path in sorted(root.glob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True): + try: + data = json.loads(path.read_text(encoding="utf-8")) + except Exception: + continue + cwd = _abs_user_path(data.get("cwd")) + pid = data.get("pid") + process_cwd = _process_cwd(pid) if _pid_alive(pid) else None + effective_cwd = cwd or process_cwd or repo_abs + if repo_abs and effective_cwd and not _paths_overlap(effective_cwd, repo_abs): + continue + session_id = str(data.get("sessionId") or path.stem) + if not session_id: + continue + sessions[session_id] = { + "id": session_id, + "cwd": effective_cwd, + "pid": pid, + "startedAt": _iso_or_none(data.get("startedAt")), + "updatedAt": _iso_or_none(path.stat().st_mtime), + "state": "active" if _pid_alive(pid) else "stale", + "version": data.get("version"), + "entrypoint": data.get("entrypoint"), + "title": _session_title( + data.get("title"), + preview=data.get("preview") or data.get("lastUser") or data.get("last_user"), + runtime="claude-code", + cwd=effective_cwd, + session_id=session_id, + ), + "permissionMode": _normalize_permission_mode("claude-code", data.get("permissionMode") or data.get("permission_mode")), + "note": "Claude Code session discovered from the local session registry.", + } + for log_path in _claude_logs_for_repo(repo_abs, limit=limit): + details = _claude_log_details(log_path, fallback_cwd=repo_abs) + session_id = str(details.get("id") or log_path.stem) + if not session_id: + continue + existing = sessions.get(session_id, {}) + cwd = _abs_user_path(details.get("cwd") or existing.get("cwd") or repo_abs) + active = bool(existing.get("state") == "active") + state = "active" if active else ("recent" if _recent_enough(details.get("updatedAt"), seconds=86_400) else "stale") + sessions[session_id] = { + **existing, + **details, + "id": session_id, + "cwd": cwd, + "pid": existing.get("pid"), + "state": state, + "entrypoint": existing.get("entrypoint") or "cli", + "note": "Claude Code conversation log mirrored from ~/.claude/projects.", + } -def _repo_root(root_path: str | None = None) -> Path: - return Path(root_path or os.environ.get("DHEE_UI_ROOT") or Path.cwd()).expanduser().resolve() + return sorted( + sessions.values(), + key=lambda item: _coerce_datetime(item.get("updatedAt")) or datetime.fromtimestamp(0, tz=timezone.utc), + reverse=True, + )[: max(1, int(limit))] -def _git_value(repo: Path, args: list[str], default: str = "") -> str: - try: - out = subprocess.check_output(["git", *args], cwd=str(repo), stderr=subprocess.DEVNULL, text=True) - return out.strip() or default - except Exception: - return default +def _find_claude_session(repo: str) -> Optional[Dict[str, Any]]: + sessions = _find_claude_sessions(repo, limit=1) + return sessions[0] if sessions else None -def _team_health_from_findings(findings: list[dict[str, Any]]) -> str: - if any(f.get("severity") == "high" for f in findings): - return "needs_work" - if findings: - return "watch" - return "healthy" +def _find_codex_session(repo: str) -> Optional[Dict[str, Any]]: + threads = _repo_codex_threads(repo, limit=1) + if not threads: + return None + thread = threads[0] + return { + "id": str(thread.get("id") or ""), + "cwd": thread.get("cwd"), + "title": thread.get("title"), + "model": thread.get("model"), + "rolloutPath": thread.get("rolloutPath"), + "startedAt": thread.get("startedAt"), + "updatedAt": thread.get("updatedAt"), + "state": "active" if thread.get("isCurrent") else str(thread.get("state") or "recent"), + "note": ( + "Codex local state shows the most recent thread for this repo; " + "recently updated threads are treated as live for the UI." + ), + } -def _repo_context_entries(root: Path, *, limit: int = 100) -> list[dict[str, Any]]: - path = root / ".dhee" / "context" / "entries.jsonl" - entries: list[dict[str, Any]] = [] - if not path.exists(): - return entries - try: - for line in path.read_text(encoding="utf-8").splitlines(): - if not line.strip(): +def _latest_claude_limit_event() -> Optional[Dict[str, Any]]: + root = Path.home() / ".claude" / "telemetry" + if not root.exists(): + return None + latest: Optional[Dict[str, Any]] = None + latest_at: Optional[datetime] = None + for path in sorted(root.glob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True)[:25]: + try: + lines = path.read_text(encoding="utf-8").splitlines() + except Exception: + continue + for line in lines: + if not line.strip() or "threshold" not in line and "rate_limit" not in line: continue try: - entries.append(json.loads(line)) + payload = json.loads(line) except json.JSONDecodeError: continue - except OSError: - return [] - return entries[-limit:] - - -def _indexed_files(root: Path, *, limit: int = 600) -> tuple[int, int]: - ignored = {".git", ".hg", ".svn", ".venv", "node_modules", "__pycache__", ".mypy_cache", ".pytest_cache"} - total_files = 0 - total_bytes = 0 - for base, dirs, files in os.walk(root): - dirs[:] = [d for d in dirs if d not in ignored and not d.startswith(".tox")] - for name in files: - if name.endswith((".pyc", ".pyo", ".png", ".jpg", ".jpeg", ".gif", ".sqlite", ".db")): + event = payload.get("event_data") or {} + name = str(event.get("event_name") or "").strip().lower() + if name not in { + "tengu_cost_threshold_reached", + "rate_limit_reached", + "quota_limit_reached", + }: continue - path = Path(base) / name - try: - size = path.stat().st_size - except OSError: + when = _coerce_datetime(event.get("client_timestamp")) + if when is None: continue - total_files += 1 - total_bytes += size - if total_files >= limit: - return total_files, total_bytes - return total_files, total_bytes + if latest_at is None or when > latest_at: + latest_at = when + latest = { + "eventName": name, + "model": event.get("model"), + "lastHitAt": when.astimezone(timezone.utc).isoformat(), + } + return latest -def _context_index(root: Path) -> list[dict[str, Any]]: - entries = _repo_context_entries(root) - if entries: - out = [] - for item in entries: - out.append( - { - "context_id": item.get("id") or item.get("context_id"), - "title": item.get("title") or item.get("summary") or "Repo context", - "summary": item.get("summary") or item.get("content") or item.get("body") or item.get("reason") or "", - "scope": item.get("scope") or "repo", - "kind": item.get("kind") or item.get("type") or "note", - "project_id": item.get("project_id") or "local", - "team_id": item.get("team_id") or "local-dev", - "shares": item.get("shares") or [], - } - ) - return out - return [ - { - "context_id": "oss-policy", - "title": "Context firewall baseline", - "summary": "Agents should see compact truth first and expand raw evidence only when needed.", - "scope": "company", - "kind": "policy", - "project_id": "developer-brain", - "team_id": "local-dev", - "shares": [], - }, - { - "context_id": "oss-runbook", - "title": "Local handoff runbook", - "summary": "Use `dhee handoff` before switching agents or resuming long-running work.", - "scope": "team", - "kind": "runbook", - "project_id": "developer-brain", - "team_id": "local-dev", - "shares": [], - }, - ] +def _claude_limit_status() -> Dict[str, Any]: + latest = _latest_claude_limit_event() + return { + "supported": True, + "lastHitAt": (latest or {}).get("lastHitAt"), + "resetAt": None, + "state": "hit" if latest else "unknown", + "model": (latest or {}).get("model"), + "note": ( + "Local Claude telemetry exposes the latest detected threshold event, " + "but not a reliable reset timestamp." + ), + } + + +def _codex_limit_status() -> Dict[str, Any]: + return { + "supported": False, + "lastHitAt": None, + "resetAt": None, + "state": "unknown", + "note": ( + "Codex local state does not currently expose a reliable limit-hit " + "or reset timestamp." + ), + } -def _code_brain_summary(root: Path, *, org_id: str, repo_mappings: list[dict[str, Any]]) -> dict[str, Any]: - indexed_files, indexed_bytes = _indexed_files(root) - mapping_status = [] - for mapping in repo_mappings: - mapping_status.append( +def _runtime_entry( + *, + runtime_id: str, + label: str, + installed: bool, + configured: Dict[str, Any], + session: Optional[Dict[str, Any]], + limits: Dict[str, Any], +) -> Dict[str, Any]: + if session and session.get("state") == "active": + state = "active" + elif installed: + state = "ready" + elif session: + state = "session-detected" + else: + state = "not-configured" + return { + "id": runtime_id, + "label": label, + "installed": installed, + "state": state, + "configured": configured, + "currentSession": session, + "limits": limits, + } + + +def _get_runtime_status_payload() -> Dict[str, Any]: + from dhee.harness.install import harness_status + + repo = _ui_repo() + native = harness_status(harness="all") + claude_native = dict(native.get("claude_code") or {}) + codex_native = dict(native.get("codex") or {}) + claude_installed = bool( + claude_native.get("enabled_in_config") + and claude_native.get("hooks_present") + and claude_native.get("mcp_registered") + ) + codex_installed = bool( + codex_native.get("enabled_in_config") + and codex_native.get("mcp_registered") + and codex_native.get("instructions_present") + ) + return { + "live": True, + "repo": repo, + "runtimes": [ + _runtime_entry( + runtime_id="claude-code", + label="Claude Code", + installed=claude_installed, + configured=claude_native, + session=_find_claude_session(repo), + limits=_claude_limit_status(), + ), + _runtime_entry( + runtime_id="codex", + label="Codex", + installed=codex_installed, + configured=codex_native, + session=_find_codex_session(repo), + limits=_codex_limit_status(), + ), + ], + } + + +def _session_messages_from_agent_session(session: Dict[str, Any]) -> List[Dict[str, Any]]: + metadata = dict(session.get("metadata") or {}) + messages = metadata.get("messages") or [] + out: List[Dict[str, Any]] = [] + for index, message in enumerate(messages): + if not isinstance(message, dict): + continue + content = str(message.get("content") or "").strip() + if not content: + continue + out.append( { - "mapping_id": mapping.get("mapping_id"), - "team_id": mapping.get("team_id"), - "project_id": mapping.get("project_id"), - "local_path": str(root), - "repo_url": mapping.get("repo_url"), - "indexed_files": indexed_files, - "indexed_bytes": indexed_bytes, - "updated_at": None, - "last_sync": {"files_warmed": indexed_files, "mode": "oss-local"}, - "sync_status": "indexed" if indexed_files else "not_indexed", + "id": f"{session.get('id')}:msg:{index}", + "role": str(message.get("role") or "agent"), + "content": content, + "createdAt": _iso_or_none(message.get("timestamp")), } ) + if out: + return out + note = str(metadata.get("note") or "").strip() + if note: + return [ + { + "id": f"{session.get('id')}:note", + "role": "agent", + "content": note, + "createdAt": _iso_or_none(session.get("updated_at")), + } + ] + return [] + + +def _session_summary(db: Any, session: Dict[str, Any]) -> Dict[str, Any]: + metadata = dict(session.get("metadata") or {}) + task = db.get_shared_task(str(session.get("task_id") or ""), user_id=_ui_user_id()) + title = _session_title( + session.get("title"), + preview=metadata.get("preview"), + runtime=session.get("runtime_id"), + cwd=session.get("cwd"), + session_id=session.get("native_session_id") or session.get("id"), + ) return { - "indexed_files": indexed_files, - "indexed_bytes": indexed_bytes, - "telemetry": {"events": 0, "source": "oss-local"}, - "repo_paths": [{"repo_path": str(root), "indexed_files": indexed_files, "indexed_bytes": indexed_bytes}], - "mapping_status": mapping_status, + "id": str(session.get("id") or ""), + "nativeSessionId": str(session.get("native_session_id") or ""), + "projectId": session.get("project_id"), + "workspaceId": session.get("workspace_id"), + "taskId": session.get("task_id"), + "runtime": session.get("runtime_id"), + "title": title, + "state": session.get("state"), + "model": session.get("model"), + "cwd": session.get("cwd"), + "rolloutPath": session.get("rollout_path"), + "startedAt": session.get("started_at"), + "updatedAt": session.get("updated_at"), + "permissionMode": session.get("permission_mode") or "native", + "isCurrent": bool(metadata.get("is_current") or str(session.get("state") or "") == "active"), + "preview": metadata.get("preview") or "", + "messages": _session_messages_from_agent_session(session), + "recentTools": list(metadata.get("recent_tools") or []), + "plan": list(metadata.get("plan") or []), + "touchedFiles": list(metadata.get("touched_files") or []), + "rateLimits": dict(metadata.get("rate_limits") or {}), + "tokenUsage": dict(metadata.get("token_usage") or {}), + "lastTokenUsage": dict(metadata.get("last_token_usage") or {}), + "tokenUsageComplete": metadata.get("token_usage_complete"), + "taskStatus": (task or {}).get("status"), } -def build_dashboard_payload(*, org_id: str | None = None, root_path: str | None = None, repo: str | None = None) -> dict[str, Any]: - root = _repo_root(root_path or repo) - org = org_id or _org_from_env() - branch = _git_value(root, ["branch", "--show-current"], "main") - remote = _git_value(root, ["remote", "get-url", "origin"], str(root)) - context_index = _context_index(root) - findings = [ - { - "finding_id": "oss-router-proof", - "team_id": "local-dev", - "manager_id": "dhee-context-manager", - "title": "Router demo available", - "detail": "Use the Context Firewall tab to inspect digest-first routing and expansion pointers.", - "severity": "low", - "finding_type": "proof", - } +def _workspace_project_sessions(db: Any, project: Dict[str, Any]) -> List[Dict[str, Any]]: + return db.list_agent_sessions( + user_id=_ui_user_id(), + project_id=str(project.get("id") or ""), + workspace_id=str(project.get("workspace_id") or ""), + limit=200, + ) + + +def _workspace_sessions(db: Any, workspace: Dict[str, Any]) -> List[Dict[str, Any]]: + return db.list_agent_sessions( + user_id=_ui_user_id(), + workspace_id=str(workspace.get("id") or ""), + limit=300, + ) + + +def _project_summary(db: Any, project: Dict[str, Any]) -> Dict[str, Any]: + sessions = [ + _session_summary(db, session) + for session in _workspace_project_sessions(db, project)[:80] ] - repo_mappings = [ + scope_rules = [ { - "mapping_id": "local-repo", - "team_id": "local-dev", - "project_id": "developer-brain", - "repo_url": remote, - "local_path": str(root), - "branch": branch, - "provider": "git", - "metadata": {"last_sync": {"mode": "oss-local"}}, + "id": str(rule.get("id") or ""), + "pathPrefix": str(rule.get("path_prefix") or ""), + "label": str(rule.get("label") or ""), } + for rule in _workspace_project_scope_rules(db, str(project.get("id") or "")) ] - code_brain = _code_brain_summary(root, org_id=org, repo_mappings=repo_mappings) - kind_counts: dict[str, int] = {} - scope_counts: dict[str, int] = {} - for item in context_index: - kind = str(item.get("kind") or "note") - scope = str(item.get("scope") or "unknown") - kind_counts[kind] = kind_counts.get(kind, 0) + 1 - scope_counts[scope] = scope_counts.get(scope, 0) + 1 - - managers_by_team = { - "local-dev": { - "manager_id": "dhee-context-manager", - "display_name": "Dhee Context Manager", - "team_id": "local-dev", - } + return { + "id": str(project.get("id") or ""), + "workspaceId": project.get("workspace_id"), + "name": project.get("name"), + "label": project.get("name"), + "description": project.get("description"), + "defaultRuntime": project.get("default_runtime") or "codex", + "color": project.get("color"), + "icon": project.get("icon"), + "updatedAt": project.get("updated_at"), + "scopeRules": scope_rules, + "sessions": sessions, } - team_rows = [ - { - "team_id": "local-dev", - "name": "Local Developer", - "team_type": "project", - "project_id": "developer-brain", - "manager": managers_by_team["local-dev"], - "repo_count": len(repo_mappings), - "context_count": len(context_index), - "open_findings": len(findings), - "health": _team_health_from_findings(findings), - } + + +def _workspace_summary(db: Any, workspace: Dict[str, Any]) -> Dict[str, Any]: + mounts = db.list_workspace_mounts( + workspace_id=str(workspace.get("id") or ""), + user_id=_ui_user_id(), + ) + workspace_with_mounts = {**workspace, "mounts": mounts} + projects = [ + _project_summary(db, project) + for project in db.list_workspace_projects( + workspace_id=str(workspace.get("id") or ""), + user_id=_ui_user_id(), + limit=100, + ) ] - org_chart = { - "workspace": {"name": root.name or "Dhee", "root_path": str(root), "default_branch": branch}, - "global_teams": [ - { - "team_id": "global-context", - "name": "Context Governance", - "context_manager": {"manager_id": "dhee-context-manager"}, - "repo_mappings": [], - "open_findings": [], - } - ], - "projects": [ - { - "project_id": "developer-brain", - "name": "Developer Brain", - "teams": [ - { - "team_id": "local-dev", - "name": "Local Developer", - "context_manager": {"manager_id": "dhee-context-manager"}, - "repo_mappings": repo_mappings, - "open_findings": findings, - } - ], - "repo_mappings": repo_mappings, - } - ], + sessions = [ + _session_summary(db, session) + for session in _workspace_sessions(db, workspace)[:80] + ] + return { + "id": str(workspace.get("id") or ""), + "name": workspace.get("name"), + "label": workspace.get("name"), + "description": workspace.get("description"), + "rootPath": _workspace_primary_path(workspace_with_mounts), + "workspacePath": _workspace_primary_path(workspace_with_mounts), + "folders": _workspace_folder_mounts(workspace_with_mounts), + "mounts": _workspace_folder_mounts(workspace_with_mounts), + "updatedAt": workspace.get("updated_at"), + "projects": projects, + "sessions": sessions, + "sessionCount": len(sessions), } - totals = { - "projects": 1, - "teams": 2, - "global_teams": 1, - "repo_mappings": len(repo_mappings), - "context_items": len(context_index), - "context_managers": 1, - "open_findings": len(findings), - "shares": 0, - "indexed_files": code_brain["indexed_files"], + + +def _file_context_payload(db: Any, source_path: str, *, workspace_id: Optional[str]) -> Dict[str, Any]: + results = db.list_shared_task_results_for_path( + user_id=_ui_user_id(), + workspace_id=workspace_id, + source_path=source_path, + limit=12, + ) + memories = [] + try: + from dhee.cli_config import get_memory_instance + + memory = get_memory_instance(None) + searched = memory.search(source_path, user_id=_ui_user_id(), limit=5) + raw = searched.get("results") if isinstance(searched, dict) else searched + for row in raw or []: + memories.append(_engram_from_memory(row)) + except Exception: + memories = [] + return { + "path": source_path, + "workspaceId": workspace_id, + "results": results, + "memories": memories, + "summary": str((results[0] or {}).get("digest") or "") if results else "", } - context_firewall = token_router_demo() - raw = { - "org_id": org, - "workspace": org_chart["workspace"], - "projects": org_chart["projects"], - "global_teams": org_chart["global_teams"], - "repo_mappings": repo_mappings, - "context_index": context_index, - "context_managers": list(managers_by_team.values()), - "context_manager_findings": findings, - "context_manager_findings_by_team": {"local-dev": findings}, - "context_managers_by_team": managers_by_team, - "repo_mappings_by_team": {"local-dev": repo_mappings}, - "team_context": {"local-dev": context_index}, - "context_shares": [], - "org_chart": org_chart, + + +def _extract_asset_text(path: str, mime_type: Optional[str] = None) -> str: + suffix = Path(path).suffix.lower() + try: + if suffix in {".txt", ".md", ".rst", ".json", ".csv", ".tsv"}: + return Path(path).read_text(encoding="utf-8", errors="replace") + if suffix == ".pdf": + try: + from pypdf import PdfReader # type: ignore + except Exception: + return "" + reader = PdfReader(path) + parts = [] + for page in reader.pages: + try: + parts.append(page.extract_text() or "") + except Exception: + continue + return "\n\n".join(part for part in parts if part).strip() + except Exception: + return "" + return "" + + +def _asset_context_payload(db: Any, asset_id: str) -> Dict[str, Any]: + asset = db.get_session_asset(asset_id) + if not asset: + raise HTTPException(status_code=404, detail="Asset not found") + artifact = None + artifact_id = str(asset.get("artifact_id") or "").strip() + if artifact_id: + try: + artifact = db.get_artifact(artifact_id) + except Exception: + artifact = None + top_chunks = [] + if artifact: + for chunk in (artifact.get("chunks") or [])[:6]: + top_chunks.append( + { + "chunk_index": chunk.get("chunk_index"), + "content": str(chunk.get("content") or "")[:1500], + } + ) + summary = "" + if artifact and (artifact.get("extractions") or []): + summary = str((artifact["extractions"][0] or {}).get("extracted_text") or "")[:1200] + return { + "asset": asset, + "session": db.get_agent_session(str(asset.get("session_id") or "")), + "artifact": artifact, + "summary": summary, + "chunks": top_chunks, } + + +def _session_detail_payload(db: Any, session_id: str) -> Dict[str, Any]: + session = db.get_agent_session(session_id) + if not session: + raise HTTPException(status_code=404, detail="Session not found") + task = db.get_shared_task(str(session.get("task_id") or ""), user_id=_ui_user_id()) + results = db.list_shared_task_results( + shared_task_id=str((task or {}).get("id") or ""), + limit=40, + ) if task else [] + assets = db.list_session_assets(session_id=session_id, limit=40) + summary = _session_summary(db, session) + files = [ + _file_context_payload( + db, + path, + workspace_id=str(session.get("workspace_id") or "") or None, + ) + for path in summary.get("touchedFiles") or [] + ] + workspace = db.get_workspace(str(session.get("workspace_id") or ""), user_id=_ui_user_id()) + project = db.get_workspace_project(str(session.get("project_id") or ""), user_id=_ui_user_id()) + line_messages = db.list_workspace_line_messages( + workspace_id=str(session.get("workspace_id") or ""), + user_id=_ui_user_id(), + project_id=str(session.get("project_id") or "") or None, + limit=20, + ) return { - "org_id": org, - "workspace": org_chart["workspace"], - "totals": totals, - "commercial": { - "license": {"edition": "public", "status": "active"}, - "billing": {"plan": "public", "usage": 0}, + "live": True, + "project": _project_summary(db, project) if project else None, + "workspace": _workspace_summary(db, workspace) if workspace else None, + "session": summary, + "task": _shared_task_to_ui_task(db, task, index=0, result_limit=12) if task else None, + "results": results, + "assets": assets, + "files": files, + "line": { + "messages": line_messages, }, - "org_chart": org_chart, - "team_rows": team_rows, - "kind_counts": kind_counts, - "scope_counts": scope_counts, - "repo_mappings": repo_mappings, - "code_brain": code_brain, - "context_firewall": context_firewall, - "context_index": context_index[:100], - "findings": findings, - "raw": raw, + "runtime": _get_runtime_status_payload(), } -def seed_demo_workspace(*, org_id: str | None = None) -> dict[str, Any]: - return {"seeded": True, "dashboard": build_dashboard_payload(org_id=org_id)} +def _workspace_detail_payload(db: Any, workspace_id: str) -> Dict[str, Any]: + workspace = db.get_workspace(workspace_id, user_id=_ui_user_id()) + if not workspace: + raise HTTPException(status_code=404, detail="Workspace not found") + sessions = [_session_summary(db, session) for session in _workspace_sessions(db, workspace)[:120]] + projects = [ + _project_summary(db, project) + for project in db.list_workspace_projects( + workspace_id=workspace_id, + user_id=_ui_user_id(), + limit=100, + ) + ] + return { + "live": True, + "workspace": _workspace_summary(db, workspace), + "projects": projects, + "sessions": sessions, + "line": { + "messages": db.list_workspace_line_messages( + workspace_id=workspace_id, + user_id=_ui_user_id(), + limit=40, + ) + }, + "runtime": _get_runtime_status_payload(), + } -def connect_real_workspace(*, org_id: str | None = None, root_path: str | None = None, limit: int | None = None) -> dict[str, Any]: - root = _repo_root(root_path) +def _build_project_index_payload() -> Dict[str, Any]: + db = _get_db() + sync = _mirror_runtime_sessions(db) + workspaces = [ + _workspace_summary(db, workspace) + for workspace in db.list_workspaces(user_id=_ui_user_id(), limit=100) + ] + current_workspace_id = "" + current_project_id = "" + current_session_id = "" + latest_sessions = db.list_agent_sessions(user_id=_ui_user_id(), limit=1) + if latest_sessions: + latest = latest_sessions[0] or {} + current_workspace_id = str(latest.get("workspace_id") or "") + current_project_id = str(latest.get("project_id") or "") + current_session_id = str(latest.get("id") or "") + if not current_workspace_id: + current_workspace_id = str((sync.get("workspace") or {}).get("id") or "") + if not current_project_id: + current_project_id = str((sync.get("project") or {}).get("id") or "") + if not current_session_id: + sessions = sync.get("sessions") or [] + if sessions: + current_session_id = str((sessions[0] or {}).get("id") or "") return { - "connected": True, - "root_path": str(root), - "real": {"limit": limit, "mode": "oss-local"}, - "dashboard": build_dashboard_payload(org_id=org_id, root_path=str(root)), + "live": True, + "workspaces": workspaces, + "currentProjectId": current_project_id, + "currentWorkspaceId": current_workspace_id, + "currentSessionId": current_session_id, } -class DheeDashboardHandler(BaseHTTPRequestHandler): - server_version = "DheeUI/0.1" - - def _query(self) -> dict[str, list[str]]: - return parse_qs(urlparse(self.path).query) - - def _org(self) -> str: - query = self._query() - return (query.get("org") or [_org_from_env()])[0] - - def _root_path(self) -> str | None: - return (self._query().get("root") or [None])[0] - - def _send_json(self, payload: dict[str, Any], status: HTTPStatus = HTTPStatus.OK) -> None: - body = json.dumps(payload, indent=2, default=str).encode("utf-8") - self.send_response(status) - self.send_header("Content-Type", "application/json; charset=utf-8") - self.send_header("Cache-Control", "no-store") - self.send_header("Content-Length", str(len(body))) - self.end_headers() - self.wfile.write(body) - - def _send_file(self, path: Path, content_type: str) -> None: - body = path.read_bytes() - self.send_response(HTTPStatus.OK) - self.send_header("Content-Type", content_type) - self.send_header("Cache-Control", "no-store") - self.send_header("Content-Length", str(len(body))) - self.end_headers() - self.wfile.write(body) - - def do_GET(self) -> None: - parsed = urlparse(self.path) - if parsed.path == "/api/dashboard": - self._send_json(build_dashboard_payload(org_id=self._org(), root_path=self._root_path())) - return - if parsed.path == "/api/context-firewall": - self._send_json(token_router_demo()) +def _build_project_canvas_payload(project_id: str) -> Dict[str, Any]: + project = _get_db().get_workspace_project(project_id, user_id=_ui_user_id()) + if not project: + raise HTTPException(status_code=404, detail="Project not found") + return _build_workspace_canvas_payload(str(project.get("workspace_id") or ""), focus_project_id=project_id) + + +def _build_workspace_canvas_payload( + workspace_id: str, + *, + focus_project_id: Optional[str] = None, +) -> Dict[str, Any]: + db = _get_db() + _mirror_runtime_sessions(db) + workspace = db.get_workspace(workspace_id, user_id=_ui_user_id()) + if not workspace: + raise HTTPException(status_code=404, detail="Workspace not found") + workspace_summary = _workspace_summary(db, workspace) + projects = workspace_summary.get("projects") or [] + current_project_id = str(focus_project_id or "") or str((projects[0] or {}).get("id") or "") + nodes: List[Dict[str, Any]] = [] + links: List[Dict[str, Any]] = [] + file_nodes: Dict[str, Dict[str, Any]] = {} + asset_nodes: Dict[str, Dict[str, Any]] = {} + result_nodes: Dict[str, Dict[str, Any]] = {} + current_session_id = "" + + def add_node(node: Dict[str, Any]) -> None: + if not any(str(existing.get("id") or "") == str(node.get("id") or "") for existing in nodes): + nodes.append(node) + + def add_link(source: str, target: str, label: str, curvature: float = 0.08) -> None: + link_id = f"{source}->{target}:{label}" + if any(str(existing.get("id") or "") == link_id for existing in links): return - if parsed.path == "/api/team": - team = (self._query().get("team") or [""])[0] - if not team: - self._send_json({"error": "team is required"}, HTTPStatus.BAD_REQUEST) - return - dashboard = build_dashboard_payload(org_id=self._org(), root_path=self._root_path()) - self._send_json( + links.append( + { + "id": link_id, + "source": source, + "target": target, + "label": label, + "curvature": curvature, + } + ) + + workspace_node_id = f"workspace:{workspace_id}" + bus_node_id = f"channel:workspace:{workspace_id}" + add_node( + { + "id": workspace_node_id, + "type": "workspace", + "label": str(workspace_summary.get("name") or "Workspace"), + "subLabel": "workspace", + "body": str(workspace_summary.get("description") or _workspace_primary_path(workspace_summary)), + "accent": "var(--accent)", + "val": 36, + "meta": {"workspaceId": workspace_id}, + } + ) + add_node( + { + "id": bus_node_id, + "type": "channel", + "label": "workspace line", + "subLabel": "shared bus", + "body": "Broadcasts and shared context visible to every project in this workspace.", + "accent": "var(--green)", + "val": 18, + "meta": {"workspaceId": workspace_id, "channel": "workspace"}, + } + ) + add_link(workspace_node_id, bus_node_id, "bus", 0.02) + + session_summaries: List[Dict[str, Any]] = [] + task_payload: List[Dict[str, Any]] = [] + for project_index, project in enumerate(projects): + project_id = str(project.get("id") or "") + project_node_id = f"project:{project_id}" + channel_node_id = f"channel:project:{project_id}" + add_node( + { + "id": project_node_id, + "type": "project", + "label": str(project.get("name") or "Project"), + "subLabel": f"default {project.get('defaultRuntime') or 'codex'}", + "body": str(project.get("description") or "Logical stream inside the workspace."), + "accent": str(project.get("color") or "var(--indigo)"), + "val": 24, + "meta": {"workspaceId": workspace_id, "projectId": project_id}, + } + ) + add_node( + { + "id": channel_node_id, + "type": "channel", + "label": f"{project.get('name') or 'Project'} channel", + "subLabel": "project channel", + "body": "Project-local collaboration line for runtime broadcasts and suggested work.", + "accent": "var(--green)", + "val": 12, + "meta": {"workspaceId": workspace_id, "projectId": project_id, "channel": "project"}, + } + ) + add_link(workspace_node_id, project_node_id, "project", 0.04 + project_index * 0.01) + add_link(project_node_id, channel_node_id, "channel", 0.06) + + for session in project.get("sessions") or []: + session_id = str(session.get("id") or "") + if not current_session_id: + current_session_id = session_id + session_summaries.append(session) + add_node( { - "team_id": team, - "context": [item for item in dashboard["context_index"] if item.get("team_id") in {team, "local-dev"}], - "findings": [item for item in dashboard["findings"] if item.get("team_id") in {team, "local-dev"}], + "id": session_id, + "type": "session", + "label": _session_title( + session.get("title"), + preview=session.get("preview"), + runtime=session.get("runtime"), + cwd=session.get("cwd"), + session_id=session.get("nativeSessionId") or session_id, + ), + "subLabel": f"{session.get('runtime')} · {session.get('model') or 'unknown'}", + "body": str(session.get("preview") or "Mirrored native session."), + "accent": "var(--green)" if session.get("state") == "active" else "var(--accent)", + "status": session.get("state"), + "val": 10, + "meta": { + "workspaceId": workspace_id, + "projectId": project_id, + "taskId": session.get("taskId"), + "permissionMode": session.get("permissionMode"), + }, } ) - return - if parsed.path in {"/", "/index.html"}: - self._send_file(STATIC_DIR / "index.html", "text/html; charset=utf-8") - return - if parsed.path == "/app.js": - self._send_file(STATIC_DIR / "app.js", "application/javascript; charset=utf-8") - return - if parsed.path == "/styles.css": - self._send_file(STATIC_DIR / "styles.css", "text/css; charset=utf-8") - return - self._send_json({"error": "not found"}, HTTPStatus.NOT_FOUND) + add_link(project_node_id, session_id, "session", 0.08) + add_link(channel_node_id, session_id, "reads", 0.14) + for path in session.get("touchedFiles") or []: + if path not in file_nodes: + file_nodes[path] = { + "id": path, + "type": "file", + "label": os.path.basename(path) or path, + "subLabel": _repo_relative_path(path, _ui_repo()), + "body": "Shared file context", + "accent": "var(--indigo)", + "val": 8, + "meta": {"workspaceId": workspace_id, "path": path}, + } + add_link(session_id, path, "touched", 0.12) + for asset in db.list_session_assets(session_id=session_id, limit=20): + asset_id = str(asset.get("id") or "") + if asset_id not in asset_nodes: + asset_nodes[asset_id] = { + "id": asset_id, + "type": "asset", + "label": str(asset.get("name") or "asset"), + "subLabel": str(asset.get("mime_type") or "file"), + "body": "Reusable workspace asset", + "accent": "var(--rose)", + "val": 8, + "meta": {"assetId": asset_id, "workspaceId": workspace_id}, + } + add_link(session_id, asset_id, "asset", 0.18) - def do_POST(self) -> None: - parsed = urlparse(self.path) - if parsed.path == "/api/demo": - self._send_json(seed_demo_workspace(org_id=self._org())) - return - if parsed.path == "/api/real": - query = self._query() - limit_raw = (query.get("limit") or [""])[0] - limit = int(limit_raw) if limit_raw.strip().isdigit() else None - root_path = (query.get("root") or [None])[0] - self._send_json(connect_real_workspace(org_id=self._org(), root_path=root_path, limit=limit)) - return - if parsed.path == "/api/sync": - self._send_json({"sync": {"mode": "oss-local", "ok": True}, "dashboard": build_dashboard_payload(org_id=self._org())}) - return - if parsed.path == "/api/review": - team = (self._query().get("team") or [""])[0] - if not team: - self._send_json({"error": "team is required"}, HTTPStatus.BAD_REQUEST) - return - self._send_json( + task_rows = db.list_shared_tasks( + user_id=_ui_user_id(), + workspace_id=workspace_id, + project_id=project_id, + limit=100, + ) + for task_index, row in enumerate(task_rows): + if not isinstance(row, dict): + continue + task_ui = _shared_task_to_ui_task(db, row, index=task_index, result_limit=12) + task_payload.append(task_ui) + task_id = str(task_ui.get("id") or "") + add_node( { - "review": {"team_id": team, "mode": "oss-local", "ok": True}, - "dashboard": build_dashboard_payload(org_id=self._org()), + "id": task_id, + "type": "task", + "label": str(task_ui.get("title") or "Task"), + "subLabel": str(row.get("created_by") or "dhee"), + "body": ( + task_ui["messages"][-1]["content"] + if task_ui.get("messages") + else "Shared task context" + ), + "accent": "var(--green)" if str(row.get("status") or "") == "active" else "var(--accent)", + "status": row.get("status"), + "val": 10, + "meta": { + "workspaceId": workspace_id, + "projectId": project_id, + "taskId": task_id, + }, } ) - return - self._send_json({"error": "not found"}, HTTPStatus.NOT_FOUND) - - def log_message(self, format: str, *args: Any) -> None: - return - - -def serve(*, host: str = "127.0.0.1", port: int = 8765, org_id: str | None = None, repo: str | None = None, open_browser: bool = False) -> ThreadingHTTPServer: - if host not in _LOOPBACK_HOSTS and os.environ.get("DHEE_UI_ALLOW_PUBLIC") != "1": - raise ValueError( - "Refusing to expose Dhee UI on a non-loopback host. " - "Set DHEE_UI_ALLOW_PUBLIC=1 only behind a trusted auth proxy." - ) - if org_id: - os.environ["DHEE_UI_ORG_ID"] = org_id - if repo: - os.environ["DHEE_UI_ROOT"] = str(_repo_root(repo)) - httpd = ThreadingHTTPServer((host, port), DheeDashboardHandler) - url = f"http://{host}:{port}" - print(f"Dhee UI running at {url}") - if open_browser: - threading.Timer(0.4, lambda: webbrowser.open(url)).start() - httpd.serve_forever() - return httpd - - -def main(argv: list[str] | None = None) -> int: - parser = argparse.ArgumentParser(prog="dhee-ui") - parser.add_argument("--host", default="127.0.0.1") - parser.add_argument("--port", type=int, default=8765) - parser.add_argument("--org", default=None) - parser.add_argument("--repo", default=None) - parser.add_argument("--open", action="store_true") - args = parser.parse_args(argv) - serve(host=args.host, port=args.port, org_id=args.org, repo=args.repo, open_browser=args.open) - return 0 + add_link(project_node_id, task_id, "task", 0.16) + session_id = str(row.get("session_id") or "") + if session_id: + add_link(session_id, task_id, "owns", 0.12) + for result in db.list_shared_task_results(shared_task_id=task_id, limit=24): + result_id = str(result.get("id") or "") + if result_id not in result_nodes: + result_nodes[result_id] = { + "id": result_id, + "type": "result", + "label": str(result.get("tool_name") or "result"), + "subLabel": str(result.get("packet_kind") or "digest"), + "body": str(result.get("digest") or "Shared result"), + "accent": "var(--green)", + "val": 6, + "meta": { + "workspaceId": workspace_id, + "projectId": project_id, + "taskId": task_id, + "resultId": result_id, + }, + } + add_link(task_id, result_id, "result", 0.22) + source_path = str(result.get("source_path") or "").strip() + if source_path: + if source_path not in file_nodes: + file_nodes[source_path] = { + "id": source_path, + "type": "file", + "label": os.path.basename(source_path) or source_path, + "subLabel": _repo_relative_path(source_path, _ui_repo()), + "body": "Shared file context", + "accent": "var(--indigo)", + "val": 8, + "meta": {"workspaceId": workspace_id, "path": source_path}, + } + add_link(result_id, source_path, "touches", 0.18) + + for node in file_nodes.values(): + add_node(node) + for node in asset_nodes.values(): + add_node(node) + for node in result_nodes.values(): + add_node(node) + + line_messages = db.list_workspace_line_messages( + workspace_id=workspace_id, + user_id=_ui_user_id(), + limit=120, + ) + for message in line_messages: + message_id = str(message.get("id") or "") + source_project_id = str(message.get("project_id") or "") + target_project_id = str(message.get("target_project_id") or "") + message_title = str(message.get("title") or "").strip() + body = str(message.get("body") or "").strip() + add_node( + { + "id": message_id, + "type": "broadcast", + "label": message_title or (body[:48] + ("…" if len(body) > 48 else "")) or "Broadcast", + "subLabel": f"{message.get('message_kind') or 'update'} · {message.get('channel') or 'workspace'}", + "body": body, + "accent": "var(--accent)", + "val": 7, + "meta": { + "workspaceId": workspace_id, + "projectId": source_project_id or None, + "targetProjectId": target_project_id or None, + "taskId": message.get("task_id"), + "sessionId": message.get("session_id"), + }, + } + ) + if source_project_id: + add_link(f"channel:project:{source_project_id}", message_id, "broadcast", 0.3) + else: + add_link(bus_node_id, message_id, "broadcast", 0.3) + if target_project_id: + add_link(message_id, f"project:{target_project_id}", "targets", 0.36) + if message.get("task_id"): + add_link(message_id, str(message.get("task_id") or ""), "suggests", 0.4) + + return { + "live": True, + "repo": _ui_repo(), + "workspace": workspace_summary, + "graph": {"nodes": nodes, "links": links}, + "sessions": session_summaries, + "tasks": task_payload, + "files": list(file_nodes.values()), + "currentSessionId": current_session_id, + "currentProjectId": current_project_id, + "currentWorkspaceId": workspace_id, + "runtime": _get_runtime_status_payload(), + "line": {"messages": line_messages}, + } + + +def _build_workspace_graph_payload() -> Dict[str, Any]: + index = _build_project_index_payload() + workspace_id = str(index.get("currentWorkspaceId") or "") + canvas = _build_workspace_canvas_payload( + workspace_id, + focus_project_id=str(index.get("currentProjectId") or "") or None, + ) + return { + **canvas, + "workspaces": index.get("workspaces") or [], + } + + +def _line_cursor(message: Optional[Dict[str, Any]]) -> str: + if not message: + return "" + return f"{str(message.get('created_at') or '')}|{str(message.get('id') or '')}" + + +def _create_suggested_task_from_broadcast( + db: Any, + *, + workspace_id: str, + project_id: str, + source_project_id: Optional[str], + title: str, + body: str, + session_id: Optional[str], +) -> Dict[str, Any]: + task = db.upsert_shared_task( + { + "user_id": _ui_user_id(), + "project_id": project_id, + "repo": _ui_repo(), + "workspace_id": workspace_id, + "folder_path": ".", + "session_id": session_id, + "runtime_id": "workspace-line", + "title": title, + "status": "paused", + "created_by": "workspace-line", + "metadata": { + "suggested": True, + "source_project_id": source_project_id, + "workspace_id": workspace_id, + "project_id": project_id, + }, + } + ) + db.save_shared_task_result( + { + "shared_task_id": task.get("id"), + "result_key": f"broadcast:{workspace_id}:{project_id}:{int(time.time() * 1000)}", + "project_id": project_id, + "workspace_id": workspace_id, + "repo": _ui_repo(), + "packet_kind": "broadcast", + "tool_name": "workspace-line", + "result_status": "completed", + "digest": body, + "metadata": { + "source_project_id": source_project_id, + "workspace_id": workspace_id, + }, + } + ) + return task + + + +def _repo_codex_threads(repo: str, limit: int = 6) -> List[Dict[str, Any]]: + repo_abs = _abs_user_path(repo) + state_db = Path.home() / ".codex" / "state_5.sqlite" + if not state_db.exists(): + return [] + conn = sqlite3.connect(str(state_db)) + conn.row_factory = sqlite3.Row + try: + rows = conn.execute( + """ + SELECT id, cwd, title, model, rollout_path, updated_at, updated_at_ms, + created_at, created_at_ms + FROM threads + WHERE archived = 0 AND cwd LIKE ? + ORDER BY COALESCE(updated_at_ms, updated_at) DESC + LIMIT ? + """, + (f"{repo_abs}%", limit), + ).fetchall() + finally: + conn.close() + items: List[Dict[str, Any]] = [] + for index, row in enumerate(rows): + data = dict(row) + cwd = _abs_user_path(data.get("cwd") or repo_abs) + if repo_abs and cwd and not _path_matches_repo(cwd, repo_abs): + continue + rollout_path = str(data.get("rollout_path") or "") + rollout = _parse_codex_rollout(Path(rollout_path), repo=repo_abs) + updated_at = _iso_or_none(data.get("updated_at_ms") or data.get("updated_at")) + started_at = _iso_or_none(data.get("created_at_ms") or data.get("created_at")) + is_current = index == 0 and ( + _recent_enough(updated_at, seconds=1800) or _runtime_has_process_for_path("codex", cwd) + ) + items.append( + { + "id": str(data.get("id") or f"thread-{index}"), + "title": _session_title( + data.get("title") or "Untitled Codex session", + preview=rollout.get("preview"), + runtime="codex", + cwd=cwd, + session_id=data.get("id") or f"thread-{index}", + ), + "cwd": cwd, + "model": data.get("model"), + "startedAt": started_at, + "updatedAt": updated_at, + "updatedAtLabel": _format_ui_clock(updated_at), + "rolloutPath": rollout_path, + "isCurrent": is_current, + "state": "active" if is_current else ("recent" if _recent_enough(updated_at, seconds=86_400) else "stale"), + **rollout, + } + ) + return items + + +def _parse_codex_rollout(path: Path, *, repo: str) -> Dict[str, Any]: + empty = { + "preview": "", + "messages": [], + "recentTools": [], + "plan": [], + "touchedFiles": [], + "rateLimits": {}, + "tokenUsage": {}, + "lastTokenUsage": {}, + "contextWindow": None, + } + if not path.exists(): + return empty + try: + stat = path.stat() + except OSError: + return empty + cached = _session_log_cache_get("codex", path, repo, stat) + if cached is not None: + return cached + messages: deque[Dict[str, Any]] = deque(maxlen=8) + recent_tools: deque[str] = deque(maxlen=8) + touched_files: set[str] = set() + latest_plan: List[Dict[str, Any]] = [] + latest_rate_limits: Dict[str, Any] = {} + latest_token_usage: Dict[str, Any] = {} + latest_last_token_usage: Dict[str, Any] = {} + context_window: Optional[int] = None + for line in _jsonl_tail_lines(path): + try: + item = json.loads(line) + except json.JSONDecodeError: + continue + kind = item.get("type") + payload = item.get("payload") or {} + timestamp = item.get("timestamp") + if kind == "response_item": + payload_type = payload.get("type") + if payload_type == "message": + text = _rollout_message_text(payload) + if text: + messages.append( + { + "role": payload.get("role") or "assistant", + "content": text, + "timestamp": timestamp, + } + ) + elif payload_type == "function_call": + name = str(payload.get("name") or "").strip() + if name: + recent_tools.append(name) + arguments = str(payload.get("arguments") or "") + touched_files.update(_extract_repo_paths(arguments, repo)) + if name == "update_plan": + latest_plan = _parse_update_plan(arguments) + if name == "apply_patch": + touched_files.update(_extract_patch_paths(arguments, repo)) + elif kind == "event_msg": + event_type = payload.get("type") + if event_type == "agent_message": + message = str(payload.get("message") or "").strip() + if message: + messages.append( + { + "role": "assistant", + "content": message, + "timestamp": timestamp, + } + ) + elif event_type == "token_count": + latest_rate_limits = dict(payload.get("rate_limits") or {}) + info = payload.get("info") or {} + latest_token_usage = dict(info.get("total_token_usage") or {}) + latest_last_token_usage = dict(info.get("last_token_usage") or {}) + if info.get("model_context_window") is not None: + try: + context_window = int(info.get("model_context_window")) + except (TypeError, ValueError): + context_window = None + preview = "" + if messages: + for candidate in reversed(messages): + if candidate.get("role") == "assistant": + preview = str(candidate.get("content") or "") + break + if not preview: + preview = str(messages[-1].get("content") or "") + result = { + "preview": preview[:260], + "messages": list(messages), + "recentTools": list(recent_tools), + "plan": latest_plan, + "touchedFiles": sorted(touched_files)[:10], + "rateLimits": latest_rate_limits, + "tokenUsage": latest_token_usage, + "lastTokenUsage": latest_last_token_usage, + "contextWindow": context_window, + } + return _session_log_cache_put("codex", path, repo, stat, result) + + +def _router_codex_live_usage_from_thread(thread: Dict[str, Any]) -> Dict[str, Any]: + total = dict(thread.get("tokenUsage") or {}) + last = dict(thread.get("lastTokenUsage") or {}) + + def _coerce_int(value: Any) -> Optional[int]: + try: + if value is None: + return None + return int(value) + except (TypeError, ValueError): + return None + + return { + "available": bool(total or last), + "source": "codex token_count event", + "exact": True, + "input_tokens": _coerce_int(total.get("input_tokens")), + "cached_input_tokens": _coerce_int(total.get("cached_input_tokens")), + "output_tokens": _coerce_int(total.get("output_tokens")), + "reasoning_output_tokens": _coerce_int(total.get("reasoning_output_tokens")), + "total_tokens": _coerce_int(total.get("total_tokens")), + "last_turn_tokens": _coerce_int(last.get("total_tokens")), + "last_turn_input_tokens": _coerce_int(last.get("input_tokens")), + "last_turn_cached_input_tokens": _coerce_int(last.get("cached_input_tokens")), + "last_turn_output_tokens": _coerce_int(last.get("output_tokens")), + "context_window": thread.get("contextWindow"), + "updated_at": thread.get("updatedAt"), + "note": "Actual native token telemetry reported by the local Codex session.", + } + + +def _router_claude_live_usage_from_summary(summary: Dict[str, Any]) -> Optional[Dict[str, Any]]: + total = dict(summary.get("tokenUsage") or {}) + last = dict(summary.get("lastTokenUsage") or {}) + if not total and not last: + return None + + input_tokens = int(total.get("input_tokens") or 0) + cache_creation = int(total.get("cache_creation_input_tokens") or 0) + cache_read = int(total.get("cache_read_input_tokens") or 0) + output_tokens = int(total.get("output_tokens") or 0) + last_input = int(last.get("input_tokens") or 0) + last_cache_creation = int(last.get("cache_creation_input_tokens") or 0) + last_cache_read = int(last.get("cache_read_input_tokens") or 0) + last_output = int(last.get("output_tokens") or 0) + complete = bool(summary.get("tokenUsageComplete")) + + return { + "available": True, + "source": "claude-code transcript usage", + "exact": complete, + "input_tokens": input_tokens + cache_creation + cache_read, + "cached_input_tokens": cache_read, + "cache_creation_input_tokens": cache_creation, + "cache_read_input_tokens": cache_read, + "output_tokens": output_tokens, + "reasoning_output_tokens": None, + "total_tokens": input_tokens + cache_creation + cache_read + output_tokens, + "last_turn_tokens": last_input + last_cache_creation + last_cache_read + last_output, + "last_turn_input_tokens": last_input + last_cache_creation + last_cache_read, + "last_turn_cached_input_tokens": last_cache_read, + "last_turn_output_tokens": last_output, + "context_window": None, + "updated_at": summary.get("updatedAt"), + "note": ( + "Actual Claude Code token usage parsed from the local transcript." + if complete + else "Recent Claude Code token usage parsed from the local transcript tail." + ), + } + + +def _router_codex_native_usage(repo: str) -> Dict[str, Any]: + threads = _repo_codex_threads(repo, limit=3) + if not threads: + return {"available": False} + current = next((thread for thread in threads if thread.get("isCurrent")), threads[0]) + total = dict(current.get("tokenUsage") or {}) + last = dict(current.get("lastTokenUsage") or {}) + rate_limits = dict(current.get("rateLimits") or {}) + primary = dict(rate_limits.get("primary") or {}) + secondary = dict(rate_limits.get("secondary") or {}) + + def _coerce_int(value: Any) -> Optional[int]: + try: + if value is None: + return None + return int(value) + except (TypeError, ValueError): + return None + + def _coerce_float(value: Any) -> Optional[float]: + try: + if value is None: + return None + return float(value) + except (TypeError, ValueError): + return None + + def _epoch_to_iso(value: Any) -> Optional[str]: + try: + if value in (None, ""): + return None + return datetime.fromtimestamp(float(value), tz=timezone.utc).isoformat() + except (TypeError, ValueError, OSError): + return None + + return { + "available": bool(total or last or rate_limits), + "threadId": current.get("id"), + "title": current.get("title"), + "model": current.get("model"), + "updatedAt": current.get("updatedAt"), + "totalTokens": _coerce_int(total.get("total_tokens")), + "inputTokens": _coerce_int(total.get("input_tokens")), + "cachedInputTokens": _coerce_int(total.get("cached_input_tokens")), + "outputTokens": _coerce_int(total.get("output_tokens")), + "reasoningOutputTokens": _coerce_int(total.get("reasoning_output_tokens")), + "lastTurnTokens": _coerce_int(last.get("total_tokens")), + "lastTurnInputTokens": _coerce_int(last.get("input_tokens")), + "lastTurnCachedInputTokens": _coerce_int(last.get("cached_input_tokens")), + "lastTurnOutputTokens": _coerce_int(last.get("output_tokens")), + "contextWindow": current.get("contextWindow"), + "primaryUsedPercent": _coerce_float(primary.get("used_percent")), + "secondaryUsedPercent": _coerce_float(secondary.get("used_percent")), + "resetAt": _epoch_to_iso(primary.get("resets_at")), + "secondaryResetAt": _epoch_to_iso(secondary.get("resets_at")), + "rateLimits": rate_limits, + } + + +def _rollout_message_text(payload: Dict[str, Any]) -> str: + content = payload.get("content") or [] + parts: List[str] = [] + for block in content: + if not isinstance(block, dict): + continue + text = block.get("text") + if text: + parts.append(str(text)) + return "\n".join(parts).strip() + + +def _parse_update_plan(arguments: str) -> List[Dict[str, Any]]: + try: + data = json.loads(arguments) + except Exception: + return [] + items = [] + for row in data.get("plan") or []: + if not isinstance(row, dict): + continue + step = str(row.get("step") or "").strip() + if not step: + continue + items.append( + { + "step": step, + "status": str(row.get("status") or "pending"), + } + ) + return items[:8] + + +def _extract_repo_paths(text: str, repo: str) -> List[str]: + matches = re.findall(r"/Users/[^\s\"']+", text or "") + out: List[str] = [] + for raw in matches: + candidate = raw.rstrip(",:;)]}") + if candidate.startswith(repo): + out.append(candidate) + return out + + +def _extract_patch_paths(text: str, repo: str) -> List[str]: + paths = [] + for match in re.findall(r"\*\*\* (?:Add|Update|Delete) File: (.+)", text or ""): + candidate = str(match).strip() + if candidate.startswith(repo): + paths.append(candidate) + return paths + + +def _repo_relative_path(path: str, repo: str) -> str: + try: + return str(Path(path).resolve().relative_to(Path(repo).resolve())) + except Exception: + return path + + +def _task_preview(task: Dict[str, Any]) -> str: + messages = list(task.get("messages") or []) + for message in reversed(messages): + content = str((message or {}).get("content") or "").strip() + if content and content != task.get("title"): + return content[:220] + return "No shared outputs yet." + + +def _task_color_value(status: Optional[str]) -> str: + color = _task_color(status) + mapping = { + "green": "var(--green)", + "indigo": "var(--indigo)", + "orange": "var(--accent)", + "rose": "var(--rose)", + } + return mapping.get(color, "var(--ink)") + + +def _dhee_data_dir_str() -> str: + try: + from dhee.configs.base import _dhee_data_dir + + return str(_dhee_data_dir()) + except Exception: + return str(Path.home() / ".dhee") + + +def _default_confidence_groups() -> List[Dict[str, Any]]: + return [ + {"group": "source_code", "confidence": 0.0, "trend": "stable"}, + {"group": "test", "confidence": 0.0, "trend": "stable"}, + {"group": "data", "confidence": 0.0, "trend": "stable"}, + {"group": "doc", "confidence": 0.0, "trend": "stable"}, + ] + + +def _seven_day_savings(agent_id: Optional[str] = None) -> List[int]: + try: + from dhee.router import ptr_store, stats as rstats + + root = ptr_store._root() + if not root.exists(): + return [0] * 7 + now = time.time() + buckets = [0] * 7 + selected_agent = None if agent_id in (None, "", "all") else rstats._normalize_agent_id(agent_id) + for session_dir in root.iterdir(): + if not session_dir.is_dir(): + continue + for meta_file in session_dir.glob("*.json"): + try: + mtime = meta_file.stat().st_mtime + except OSError: + continue + age_days = int((now - mtime) // 86400) + if 0 <= age_days < 7: + try: + meta = json.loads(meta_file.read_text(encoding="utf-8")) + except Exception: + continue + meta_agent = rstats._agent_from_meta(meta) + if selected_agent and meta_agent != selected_agent: + continue + chars = meta.get("char_count") or 0 + if not chars: + chars = int(meta.get("stdout_bytes", 0) or 0) + int( + meta.get("stderr_bytes", 0) or 0 + ) + buckets[6 - age_days] += int(chars / 3.5) + return buckets + except Exception: + return [0] * 7 + + +def _seven_day_labels() -> List[str]: + names = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] + today = time.localtime().tm_wday # Mon=0..Sun=6 + today_idx = (today + 1) % 7 # Sun=0..Sat=6 + out = [] + for i in range(6, -1, -1): + out.append(names[(today_idx - i) % 7]) + out[-1] = "Now" + return out + + +def _load_evolution_events() -> List[Dict[str, Any]]: + events: List[Dict[str, Any]] = [] + try: + from dhee.configs.base import _dhee_data_dir + + log_dir = Path(_dhee_data_dir()) / "evolution" + except Exception: + log_dir = Path.home() / ".dhee" / "evolution" + if not log_dir.exists(): + return [] + for f in sorted(log_dir.glob("*.jsonl"))[-3:]: + try: + for line in f.read_text(encoding="utf-8").splitlines(): + if not line.strip(): + continue + try: + ev = json.loads(line) + except json.JSONDecodeError: + continue + events.append( + { + "id": str(ev.get("id") or f"ev-{len(events)+1}"), + "time": ev.get("time") + or time.strftime( + "%H:%M", time.localtime(ev.get("ts", time.time())) + ), + "type": ev.get("type", "tune"), + "label": ev.get("label", "event"), + "detail": ev.get("detail", ""), + "impact": ev.get("impact", "neutral"), + } + ) + except OSError: + continue + return events[-40:][::-1] -if __name__ == "__main__": - raise SystemExit(main()) +# default app for `uvicorn dhee.ui.server:app` +app = create_app() diff --git a/dhee/ui/static/app.js b/dhee/ui/static/app.js deleted file mode 100644 index 4720bba..0000000 --- a/dhee/ui/static/app.js +++ /dev/null @@ -1,367 +0,0 @@ -const state = { - dashboard: null, - activeView: "overview", -}; - -const $ = (selector) => document.querySelector(selector); - -function text(value, fallback = "0") { - if (value === undefined || value === null || value === "") return fallback; - return String(value); -} - -function emptyNode() { - return document.querySelector("#empty-template").content.cloneNode(true); -} - -function setMetric(id, value) { - const node = document.getElementById(id); - if (node) node.textContent = text(value); -} - -async function requestJson(url, options = {}) { - const res = await fetch(url, { - headers: { "Content-Type": "application/json" }, - ...options, - }); - if (!res.ok) throw new Error(await res.text()); - return res.json(); -} - -async function loadDashboard() { - const dashboard = await requestJson("/api/dashboard"); - state.dashboard = dashboard; - renderDashboard(dashboard); -} - -async function seedDemo() { - await requestJson("/api/demo", { method: "POST" }); - await loadDashboard(); -} - -async function connectRealWorkspace() { - await requestJson("/api/real?limit=80", { method: "POST" }); - await loadDashboard(); -} - -async function syncRepos() { - await requestJson("/api/sync?limit=80", { method: "POST" }); - await loadDashboard(); -} - -async function reviewTeam(teamId) { - await requestJson(`/api/review?team=${encodeURIComponent(teamId)}`, { method: "POST" }); - await loadDashboard(); -} - -function renderDashboard(dashboard) { - const totals = dashboard.totals || {}; - const workspace = dashboard.workspace || {}; - $("#workspace-title").textContent = workspace.name || "Company Brain"; - $("#org-id-pill").textContent = dashboard.org_id || "default"; - setMetric("metric-projects", totals.projects); - setMetric("metric-teams", totals.teams); - setMetric("metric-managers", totals.context_managers); - setMetric("metric-repos", totals.repo_mappings); - setMetric("metric-context", totals.context_items); - setMetric("metric-findings", totals.open_findings); - setMetric("metric-indexed", totals.indexed_files); - const firewall = dashboard.context_firewall || {}; - const firewallAggregate = firewall.aggregate || {}; - setMetric("metric-firewall", `${text(firewallAggregate.saved_pct, "0")}%`); - renderOrgChart(dashboard.org_chart || {}); - renderCoverage(dashboard); - renderRepos(dashboard.repo_mappings || []); - renderRepoBrain(dashboard.code_brain || {}); - renderContextFirewall(firewall); - renderTeams(dashboard.team_rows || []); - renderContext(dashboard.context_index || []); - renderFindings(dashboard.findings || []); -} - -function renderOrgChart(orgChart) { - const root = $("#org-chart"); - root.replaceChildren(); - const workspace = orgChart.workspace || {}; - const projects = orgChart.projects || []; - const globals = orgChart.global_teams || []; - if (!projects.length && !globals.length) { - root.appendChild(emptyNode()); - return; - } - - const workspaceNode = document.createElement("section"); - workspaceNode.className = "org-node"; - workspaceNode.innerHTML = ` -

${text(workspace.name, "Workspace")}

-
${text(workspace.root_path, "No root path")} / ${text(workspace.default_branch, "main")}
- `; - root.appendChild(workspaceNode); - - if (globals.length) { - const globalNode = document.createElement("section"); - globalNode.className = "org-node global"; - globalNode.innerHTML = `

Global Teams

`; - const stack = document.createElement("div"); - stack.className = "team-stack"; - globals.forEach((team) => stack.appendChild(teamChip(team))); - globalNode.appendChild(stack); - root.appendChild(globalNode); - } - - projects.forEach((project) => { - const node = document.createElement("section"); - node.className = "org-node project"; - node.innerHTML = ` -

${text(project.name, project.project_id)}

-
${(project.teams || []).length} teams / ${(project.repo_mappings || []).length} mapped repos
- `; - const stack = document.createElement("div"); - stack.className = "team-stack"; - (project.teams || []).forEach((team) => stack.appendChild(teamChip(team))); - node.appendChild(stack); - root.appendChild(node); - }); -} - -function teamChip(team) { - const node = document.createElement("article"); - node.className = "team-chip"; - const manager = team.context_manager || {}; - const findings = team.open_findings || []; - node.innerHTML = ` -

${text(team.name, team.team_id)}

-
- ${text(manager.manager_id, "no manager")} - ${(team.repo_mappings || []).length} repos - ${findings.length} findings -
- `; - return node; -} - -function renderCoverage(dashboard) { - const root = $("#coverage-bars"); - root.replaceChildren(); - const counts = dashboard.kind_counts || {}; - const entries = Object.entries(counts).sort((a, b) => b[1] - a[1]); - if (!entries.length) { - root.appendChild(emptyNode()); - return; - } - const max = Math.max(...entries.map((entry) => entry[1]), 1); - entries.forEach(([kind, count]) => { - const row = document.createElement("div"); - row.className = "bar-row"; - row.innerHTML = ` - ${kind} - - ${count} - `; - root.appendChild(row); - }); -} - -function renderRepos(repos) { - const root = $("#repo-list"); - root.replaceChildren(); - if (!repos.length) { - root.appendChild(emptyNode()); - return; - } - repos.slice(0, 12).forEach((repo) => { - const item = document.createElement("article"); - item.className = "repo-item"; - item.innerHTML = ` -

${text(repo.team_id)} ${text(repo.project_id, "global")}

-
${text(repo.repo_url || repo.local_path, "unmapped")}
-
- ${text(repo.branch, "main")} - ${text(repo.provider, "git")} -
- `; - root.appendChild(item); - }); -} - -function renderRepoBrain(codeBrain) { - const root = $("#repo-brain-list"); - root.replaceChildren(); - const mappings = codeBrain.mapping_status || []; - $("#repo-brain-pill").textContent = `${codeBrain.indexed_files || 0} indexed`; - if (!mappings.length) { - root.appendChild(emptyNode()); - return; - } - mappings.forEach((mapping) => { - const lastSync = mapping.last_sync || {}; - const item = document.createElement("article"); - item.className = "repo-item"; - item.innerHTML = ` -
-

${text(mapping.team_id)} ${text(mapping.project_id, "global")}

- ${text(mapping.sync_status)} -
-
${text(mapping.local_path || mapping.repo_url, "unmapped")}
-
- ${mapping.indexed_files || 0} files - ${Math.round((mapping.indexed_bytes || 0) / 1024)} KB - ${lastSync.files_warmed || 0} warmed -
- `; - root.appendChild(item); - }); -} - -function renderContextFirewall(report) { - const root = $("#firewall-list"); - if (!root) return; - root.replaceChildren(); - const aggregate = report.aggregate || {}; - $("#firewall-pill").textContent = `${text(aggregate.saved_pct, "0")}% saved`; - const cases = report.cases || []; - if (!cases.length) { - root.appendChild(emptyNode()); - return; - } - cases.forEach((item) => { - const card = document.createElement("article"); - card.className = "firewall-card"; - - const heading = document.createElement("div"); - heading.className = "panel-heading"; - heading.innerHTML = ` -

${text(item.name)}

- ${text(item.surface)} - `; - card.appendChild(heading); - - const decision = document.createElement("div"); - decision.className = "firewall-decision"; - decision.textContent = text(item.decision, ""); - card.appendChild(decision); - - const stats = document.createElement("div"); - stats.className = "firewall-stats"; - stats.innerHTML = ` - ${text(item.raw_tokens)} raw - ${text(item.digest_tokens)} digest - ${text(item.saved_pct)}% saved - ${text(item.expand)} - `; - card.appendChild(stats); - - const pre = document.createElement("pre"); - pre.className = "digest-preview"; - pre.textContent = text(item.digest, ""); - card.appendChild(pre); - - root.appendChild(card); - }); -} - -function renderTeams(teams) { - const root = $("#team-table"); - root.replaceChildren(); - $("#team-count-pill").textContent = `${teams.length} teams`; - if (!teams.length) { - root.appendChild(emptyNode()); - return; - } - teams.forEach((team) => { - const manager = team.manager || {}; - const row = document.createElement("article"); - row.className = "team-row"; - row.innerHTML = ` -
- ${text(team.name, team.team_id)} - ${text(team.project_id, team.team_type)} -
-
- ${text(manager.display_name, "Context Manager")} - ${text(manager.manager_id, "not assigned")} -
- ${team.repo_count} - ${team.context_count} - ${team.open_findings} - - `; - row.querySelector("button").addEventListener("click", () => reviewTeam(team.team_id)); - row.querySelector(".team-title").insertAdjacentHTML("beforeend", `${team.health}`); - root.appendChild(row); - }); -} - -function renderContext(items) { - const root = $("#context-list"); - root.replaceChildren(); - $("#context-count-pill").textContent = `${items.length} items`; - if (!items.length) { - root.appendChild(emptyNode()); - return; - } - items.forEach((item) => { - const node = document.createElement("article"); - node.className = "context-item"; - node.innerHTML = ` -
-

${text(item.title, item.kind)}

- ${text(item.scope)} / ${text(item.kind)} -
-
${text(item.summary, "")}
-
- ${text(item.project_id, "company")} - ${text(item.team_id, "shared")} - ${(item.shares || []).length} shares -
- `; - root.appendChild(node); - }); -} - -function renderFindings(findings) { - const root = $("#finding-list"); - root.replaceChildren(); - $("#finding-count-pill").textContent = `${findings.length} open`; - if (!findings.length) { - root.appendChild(emptyNode()); - return; - } - findings.forEach((finding) => { - const node = document.createElement("article"); - node.className = `finding-item ${finding.severity || "medium"}`; - node.innerHTML = ` -
-

${text(finding.title)}

- ${text(finding.severity)} / ${text(finding.finding_type)} -
-
${text(finding.detail, "")}
-
- ${text(finding.team_id)} - ${text(finding.manager_id)} -
- `; - root.appendChild(node); - }); -} - -document.querySelectorAll(".tab").forEach((tab) => { - tab.addEventListener("click", () => { - const view = tab.dataset.view; - document.querySelectorAll(".tab").forEach((node) => node.classList.toggle("active", node === tab)); - document.querySelectorAll(".view").forEach((node) => node.classList.toggle("active", node.id === `view-${view}`)); - state.activeView = view; - }); -}); - -$("#refresh-button").addEventListener("click", loadDashboard); -$("#seed-button").addEventListener("click", seedDemo); -$("#real-button").addEventListener("click", connectRealWorkspace); -$("#sync-button").addEventListener("click", syncRepos); - -loadDashboard().catch((error) => { - document.body.innerHTML = `
Dashboard error${error.message}
`; -}); diff --git a/dhee/ui/static/index.html b/dhee/ui/static/index.html deleted file mode 100644 index 09020c5..0000000 --- a/dhee/ui/static/index.html +++ /dev/null @@ -1,162 +0,0 @@ - - - - - - Dhee Enterprise - - - -
-
-
-

Enterprise Dhee

-

Company Brain

-
- -
- -
-
- Projects - 0 -
-
- Teams - 0 -
-
- Context Managers - 0 -
-
- Mapped Repos - 0 -
-
- Context Items - 0 -
-
- Open Findings - 0 -
-
- Indexed Files - 0 -
-
- Router Savings - 0% -
-
- -
- - -
-
- - - - - - -
- -
-
-
-
-

Coverage

-
-
-
-
-
-

Repo Mappings

-
-
-
-
-
- -
-
-

Teams

- 0 teams -
-
-
- -
-
-

Repo Brain

- 0 indexed -
-
-
- -
-
-

Context Firewall

- 0 saved -
-
- Dhee shows agents compact truth first and keeps exact evidence behind pointers. -
-
-
- -
-
-

Context Inventory

- 0 items -
-
-
- -
-
-

Manager Findings

- 0 open -
-
-
-
-
-
- - - - - - diff --git a/dhee/ui/static/styles.css b/dhee/ui/static/styles.css deleted file mode 100644 index b8c4a67..0000000 --- a/dhee/ui/static/styles.css +++ /dev/null @@ -1,584 +0,0 @@ -:root { - --bg: #f4f7f4; - --ink: #17231e; - --muted: #64726c; - --panel: #ffffff; - --line: #d9e0dc; - --soft: #edf2ef; - --green: #2f7d58; - --blue: #2d6997; - --amber: #a46b1f; - --red: #a94438; - --violet: #7656a6; - --shadow: 0 18px 60px rgba(28, 42, 36, 0.10); -} - -* { - box-sizing: border-box; -} - -html, -body { - margin: 0; - min-height: 100%; -} - -body { - background: - linear-gradient(180deg, rgba(255, 255, 255, 0.74), rgba(244, 247, 244, 0.92)), - repeating-linear-gradient(90deg, rgba(23, 35, 30, 0.035) 0, rgba(23, 35, 30, 0.035) 1px, transparent 1px, transparent 38px); - color: var(--ink); - font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - letter-spacing: 0; -} - -button { - font: inherit; -} - -.app-shell { - width: min(1500px, 100%); - margin: 0 auto; - padding: 28px; -} - -.topbar { - display: flex; - align-items: center; - justify-content: space-between; - gap: 20px; - margin-bottom: 22px; -} - -.eyebrow { - margin: 0 0 4px; - color: var(--muted); - font-size: 12px; - font-weight: 700; - text-transform: uppercase; -} - -h1, -h2, -h3, -p { - margin: 0; -} - -h1 { - font-size: clamp(30px, 4vw, 48px); - line-height: 1; -} - -h2 { - font-size: 16px; -} - -h3 { - font-size: 14px; -} - -.toolbar, -.panel-heading, -.row-actions { - display: flex; - align-items: center; - gap: 10px; -} - -.toolbar { - flex-wrap: wrap; - justify-content: flex-end; -} - -.icon-button { - min-height: 38px; - display: inline-flex; - align-items: center; - gap: 9px; - border: 1px solid var(--line); - border-radius: 8px; - background: var(--panel); - color: var(--ink); - padding: 8px 12px; - cursor: pointer; - box-shadow: 0 4px 16px rgba(28, 42, 36, 0.06); -} - -.icon-button:hover { - border-color: #b7c4bd; -} - -.icon-button.primary { - background: var(--ink); - color: #ffffff; - border-color: var(--ink); -} - -.icon { - width: 15px; - height: 15px; - position: relative; - display: inline-block; - flex: 0 0 auto; -} - -.refresh-icon { - border: 2px solid currentColor; - border-right-color: transparent; - border-radius: 50%; -} - -.refresh-icon::after { - content: ""; - position: absolute; - right: -2px; - top: 1px; - width: 5px; - height: 5px; - border-top: 2px solid currentColor; - border-right: 2px solid currentColor; - transform: rotate(25deg); -} - -.plus-icon::before, -.plus-icon::after { - content: ""; - position: absolute; - background: currentColor; - border-radius: 2px; -} - -.plus-icon::before { - width: 15px; - height: 2px; - top: 6px; - left: 0; -} - -.plus-icon::after { - width: 2px; - height: 15px; - top: 0; - left: 6px; -} - -.repo-icon { - border: 2px solid currentColor; - border-radius: 3px; -} - -.repo-icon::before { - content: ""; - position: absolute; - width: 5px; - height: 2px; - left: 3px; - top: -4px; - background: currentColor; - border-radius: 2px 2px 0 0; -} - -.sync-icon { - border: 2px solid currentColor; - border-left-color: transparent; - border-radius: 50%; -} - -.sync-icon::before, -.sync-icon::after { - content: ""; - position: absolute; - width: 5px; - height: 5px; - border-top: 2px solid currentColor; - border-right: 2px solid currentColor; -} - -.sync-icon::before { - right: -3px; - top: 0; - transform: rotate(40deg); -} - -.sync-icon::after { - left: -3px; - bottom: 0; - transform: rotate(220deg); -} - -.metric-grid { - display: grid; - grid-template-columns: repeat(8, minmax(0, 1fr)); - gap: 12px; - margin-bottom: 16px; -} - -.metric-tile, -.side-panel, -.main-panel, -.panel-block { - background: rgba(255, 255, 255, 0.92); - border: 1px solid var(--line); - border-radius: 8px; - box-shadow: var(--shadow); -} - -.metric-tile { - min-height: 90px; - padding: 16px; - display: flex; - flex-direction: column; - justify-content: space-between; - border-top: 4px solid var(--green); -} - -.metric-tile:nth-child(2) { - border-top-color: var(--blue); -} - -.metric-tile:nth-child(3) { - border-top-color: var(--violet); -} - -.metric-tile:nth-child(4) { - border-top-color: var(--amber); -} - -.metric-tile:nth-child(5), -.metric-tile:nth-child(7) { - border-top-color: #527568; -} - -.metric-tile.attention { - border-top-color: var(--red); -} - -.metric-tile.firewall { - border-top-color: var(--violet); -} - -.metric-label { - color: var(--muted); - font-size: 12px; - font-weight: 700; - text-transform: uppercase; -} - -.metric-tile strong { - font-size: 32px; - line-height: 1; -} - -.workbench { - display: grid; - grid-template-columns: 360px minmax(0, 1fr); - gap: 16px; - align-items: start; -} - -.side-panel, -.main-panel, -.panel-block { - padding: 16px; -} - -.side-panel { - position: sticky; - top: 18px; - max-height: calc(100vh - 36px); - overflow: auto; -} - -.panel-heading { - justify-content: space-between; - margin-bottom: 14px; -} - -.pill, -.status { - display: inline-flex; - align-items: center; - min-height: 24px; - border-radius: 999px; - padding: 3px 9px; - background: var(--soft); - color: var(--muted); - font-size: 12px; - font-weight: 700; - white-space: nowrap; -} - -.org-chart { - display: grid; - gap: 12px; -} - -.org-node { - border-left: 3px solid var(--green); - padding: 10px 0 2px 12px; -} - -.org-node.project { - border-left-color: var(--blue); -} - -.org-node.global { - border-left-color: var(--violet); -} - -.org-node .meta { - margin-top: 5px; - color: var(--muted); - font-size: 12px; -} - -.team-stack { - display: grid; - gap: 8px; - margin-top: 10px; -} - -.team-chip { - border: 1px solid var(--line); - border-radius: 8px; - padding: 9px; - background: #fbfcfb; -} - -.team-chip .meta { - display: flex; - flex-wrap: wrap; - gap: 6px; -} - -.tabs { - display: flex; - gap: 8px; - margin-bottom: 16px; - border-bottom: 1px solid var(--line); - padding-bottom: 8px; - overflow-x: auto; -} - -.tab { - border: 0; - border-radius: 8px; - background: transparent; - color: var(--muted); - padding: 8px 12px; - cursor: pointer; -} - -.tab.active { - background: var(--ink); - color: #ffffff; -} - -.view { - display: none; -} - -.view.active { - display: block; -} - -.split-grid { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); - gap: 14px; -} - -.bars, -.repo-list, -.context-list, -.finding-list, -.firewall-list, -.table { - display: grid; - gap: 10px; -} - -.bar-row { - display: grid; - grid-template-columns: 110px minmax(0, 1fr) 36px; - gap: 10px; - align-items: center; - font-size: 13px; -} - -.bar-track { - height: 10px; - overflow: hidden; - border-radius: 999px; - background: var(--soft); -} - -.bar-fill { - height: 100%; - border-radius: inherit; - background: linear-gradient(90deg, var(--green), var(--blue)); -} - -.repo-item, -.context-item, -.finding-item, -.firewall-card, -.team-row { - border: 1px solid var(--line); - border-radius: 8px; - background: #fbfcfb; - padding: 12px; -} - -.repo-path, -.context-summary, -.finding-detail, -.firewall-intro, -.firewall-decision { - color: var(--muted); - font-size: 13px; - line-height: 1.45; - margin-top: 5px; - overflow-wrap: anywhere; -} - -.firewall-intro { - margin: -4px 0 14px; -} - -.firewall-stats { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 8px; - margin-top: 10px; -} - -.firewall-stats span { - min-height: 40px; - border: 1px solid var(--line); - border-radius: 8px; - background: #ffffff; - color: var(--muted); - padding: 8px; - font-size: 12px; - overflow-wrap: anywhere; -} - -.firewall-stats strong { - display: block; - color: var(--ink); - font-size: 18px; -} - -.digest-preview { - max-height: 260px; - overflow: auto; - margin: 12px 0 0; - border: 1px solid var(--line); - border-radius: 8px; - background: #17231e; - color: #edf2ef; - padding: 12px; - font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; - white-space: pre-wrap; -} - -.team-row { - display: grid; - grid-template-columns: minmax(180px, 1.1fr) minmax(180px, 1fr) repeat(3, minmax(80px, 0.35fr)) minmax(120px, 0.5fr); - gap: 12px; - align-items: center; -} - -.team-title { - display: grid; - gap: 4px; -} - -.muted { - color: var(--muted); - font-size: 12px; -} - -.status.healthy { - background: #e5f4ea; - color: var(--green); -} - -.status.watch { - background: #fff3d8; - color: var(--amber); -} - -.status.needs_work { - background: #fbe7e2; - color: var(--red); -} - -.finding-item.high { - border-left: 4px solid var(--red); -} - -.finding-item.medium { - border-left: 4px solid var(--amber); -} - -.finding-item.low { - border-left: 4px solid var(--blue); -} - -.empty-state { - display: grid; - place-items: center; - gap: 8px; - min-height: 180px; - color: var(--muted); - text-align: center; - border: 1px dashed var(--line); - border-radius: 8px; - background: rgba(255, 255, 255, 0.55); -} - -.empty-state strong { - color: var(--ink); -} - -@media (max-width: 1150px) { - .metric-grid { - grid-template-columns: repeat(4, minmax(0, 1fr)); - } - - .workbench, - .split-grid { - grid-template-columns: 1fr; - } - - .side-panel { - position: static; - max-height: none; - } -} - -@media (max-width: 720px) { - .app-shell { - padding: 16px; - } - - .topbar { - align-items: flex-start; - flex-direction: column; - } - - .metric-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .team-row { - grid-template-columns: 1fr; - } - - .firewall-stats { - grid-template-columns: 1fr; - } -} diff --git a/dhee/ui/web/dist/assets/CanvasView-Dg15id0Q.js b/dhee/ui/web/dist/assets/CanvasView-Dg15id0Q.js new file mode 100644 index 0000000..7eba39c --- /dev/null +++ b/dhee/ui/web/dist/assets/CanvasView-Dg15id0Q.js @@ -0,0 +1 @@ +import{r as b,j as e,a as W,R as ye}from"./index-BKI2JxEf.js";const Te={workspace:"#e06b3f",project:"#4d6cff",channel:"#1fa971",session:"#1a1a1a",task:"#0f9f55",result:"#0b8b5f",file:"#64748b",asset:"#d74b7b",broadcast:"#e08b3f"},Re={workspace:"Workspace",project:"Project",channel:"Channel",session:"Session",task:"Task",result:"Tool result",file:"File",asset:"Asset",broadcast:"Broadcast"};function Ee(t){return t.accent||Te[t.type]||"#555"}function be(t){if(!t)return"";const r=new Date(String(t));return Number.isNaN(r.getTime())?"":r.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}function Fe(t,r){const s=t.trim();return s.length<=r?s:`${s.slice(0,r-1).trimEnd()}…`}function De({node:t,x:r,y:s,width:u,height:x,selected:C,dim:y,onSelect:p,onHover:h,entranceDelay:o=0}){const k=Ee(t),c=t.meta||{},w=x<120,j=t.type,N={position:"absolute",left:r,top:s,width:u,height:x,display:"flex",boxSizing:"border-box",background:"white",border:`1px solid ${C?k:"rgba(20,16,10,0.12)"}`,borderLeft:`3px solid ${k}`,borderRadius:8,boxShadow:C?`0 10px 26px rgba(20,16,10,0.12), 0 0 0 3px ${k}22`:"0 1px 2px rgba(20,16,10,0.04), 0 2px 10px rgba(20,16,10,0.04)",transition:"box-shadow 0.18s ease, border-color 0.18s ease, transform 0.18s ease, opacity 0.18s ease",cursor:"pointer",userSelect:"none",opacity:y?.32:1,willChange:"transform",transform:"translate3d(0, 0, 0)",overflow:"hidden",animation:`dhee-card-in 320ms ${o}ms cubic-bezier(0.17, 0.67, 0.3, 1) both`},z={flex:1,padding:w?"10px 12px":"12px 14px",display:"flex",flexDirection:"column",gap:w?4:6,minWidth:0},M={display:"flex",alignItems:"center",gap:6},T=(O,se)=>({fontFamily:"var(--mono)",fontSize:9,color:se||"var(--ink3)",letterSpacing:.4,textTransform:"uppercase",lineHeight:1.1,padding:"2px 6px",border:"1px solid var(--border)",borderRadius:2,whiteSpace:"nowrap",background:"white",...O?{}:{}}),i={fontSize:w?12:14,fontWeight:600,lineHeight:1.25,color:"var(--ink)",whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis"},v={fontSize:w?11:12,color:"var(--ink2)",lineHeight:1.5,display:"-webkit-box",WebkitLineClamp:w?2:3,WebkitBoxOrient:"vertical",overflow:"hidden"},m={fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",letterSpacing:.3},S=Re[j]||j,R=String(c.runtime||""),_=String(c.state||""),$=String(c.ptr||""),X=String(c.toolName||c.tool_name||""),L=String(c.sourcePath||c.source_path||""),d=String(c.model||""),P=String(c.harness||""),Q=Number(c.sessionCount??0),U=Number(c.projectCount??0),I=Number(c.taskCount??0),V=Number(c.messageCount??0),G=c.updatedAt||c.last_seen_at,ne=()=>h(t),oe=()=>h(null),H=O=>{O.stopPropagation(),p(t)};return e.jsx("div",{style:N,onClick:H,onMouseEnter:ne,onMouseLeave:oe,"data-canvas-draggable":"false","data-node-id":t.id,className:"dhee-node-card",children:e.jsxs("div",{style:z,children:[e.jsxs("div",{style:M,children:[e.jsx("span",{style:{width:8,height:8,borderRadius:"50%",background:k,flexShrink:0}}),e.jsx("span",{style:{...m,color:k},children:S}),t.status?e.jsx("span",{style:T(t.status),children:t.status}):null,_?e.jsx("span",{style:T(_),children:_}):null,R?e.jsx("span",{style:T(R),children:R}):null,P&&!R?e.jsx("span",{style:T(P),children:P}):null,j==="session"&&c.isCurrent?e.jsx("span",{style:{width:7,height:7,borderRadius:"50%",background:"var(--green)",boxShadow:"0 0 0 3px rgba(31,169,113,0.22)",marginLeft:"auto"}}):null]}),e.jsx("div",{style:i,children:t.label||"(unnamed)"}),t.subLabel?e.jsx("div",{style:{...m,whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis"},children:t.subLabel}):null,!w&&t.body?e.jsx("div",{style:v,children:t.body}):null,j==="workspace"&&!w?e.jsxs("div",{style:{display:"flex",gap:12,...m},children:[e.jsxs("span",{children:[U||"—"," projects"]}),e.jsxs("span",{children:[Q||"—"," sessions"]})]}):null,j==="project"&&!w?e.jsxs("div",{style:{display:"flex",gap:12,...m},children:[e.jsxs("span",{children:[Q||"—"," sessions"]}),e.jsxs("span",{children:[I||"—"," tasks"]})]}):null,j==="session"&&!w?e.jsxs("div",{style:{display:"flex",gap:12,...m},children:[d?e.jsx("span",{children:Fe(d,22)}):null,G?e.jsx("span",{children:be(G)}):null]}):null,j==="task"&&!w?e.jsxs("div",{style:{display:"flex",gap:12,...m},children:[V?e.jsxs("span",{children:[V," messages"]}):null,G?e.jsx("span",{children:be(G)}):null]}):null,j==="result"?e.jsxs("div",{style:{display:"flex",gap:8,alignItems:"center",...m},children:[X?e.jsx("span",{children:X}):null,$?e.jsx("span",{children:$}):null,L?e.jsxs("span",{style:{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:["· ",L.split("/").pop()]}):null]}):null,j==="broadcast"?e.jsxs("div",{style:{display:"flex",gap:8,alignItems:"center",...m},children:[String(c.sourceChannel||c.sourceProject||"")?e.jsxs("span",{children:["from ",String(c.sourceChannel||c.sourceProject||"")]}):null,String(c.targetProject||"")?e.jsxs("span",{children:["→ ",String(c.targetProject)]}):null]}):null]})})}b.memo(De);function A({label:t,sub:r,children:s}){const u=t||s;return e.jsxs("div",{style:{display:"flex",alignItems:"baseline",gap:10,marginBottom:12},children:[e.jsx("span",{style:{fontFamily:"var(--mono)",fontSize:9,fontWeight:700,color:"var(--ink3)",letterSpacing:"0.1em",textTransform:"uppercase"},children:u}),r&&e.jsx("span",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--border2)"},children:r})]})}function Z(t){return(t==null?void 0:t.meta)||{}}function je(t){const r=Z(t);return String(r.task_id||r.taskId||"")||null}function Se(t){const r=Z(t).repo_mappings;return Array.isArray(r)?r:[]}function Ae(t){const r=t.metadata||{},u=(typeof r.label=="string"?r.label.trim():"")||t.local_path||t.repo_url||"folder";return String(u).split("/").filter(Boolean).pop()||String(u)}function ze(t){const r=String(t||"").toLowerCase();return r==="codex"?"var(--indigo)":r==="claude-code"||r==="claude"?"var(--accent)":"var(--ink3)"}function ke(t){const r=new Set,s=[];for(const u of t){const x=String(u.mapping_id||u.local_path||u.repo_url||"");!x||r.has(x)||(r.add(x),s.push(u))}return s}function Me({node:t,graph:r,viewer:s,isManager:u,onClose:x,onOpenVault:C,onOpenSession:y,onChanged:p}){var ve;const[h,o]=b.useState(null),[k,c]=b.useState(""),[w,j]=b.useState(""),[N,z]=b.useState(""),[M,T]=b.useState(""),[i,v]=b.useState(""),[m,S]=b.useState(""),[R,_]=b.useState(!1),[$,X]=b.useState(!1);if(b.useEffect(()=>{c(""),j(""),z(""),T(""),v(""),S(""),_(!1),X(!1)},[t==null?void 0:t.id]),!t)return null;const L=t.type==="workspace",d=t.type==="project",P=t.type==="team"||t.type==="global_team",Q=t.type==="repo",U=t.type==="folder",I=t.type==="session",V=L&&r?r.edges.filter(a=>a.source===t.id&&a.kind==="contains").map(a=>r.nodes.find(q=>q.id===a.target)).filter(a=>!!a&&a.type==="project"):[],G=d?String(((ve=t.meta)==null?void 0:ve.project_id)||""):"",ne=d&&r?r.edges.filter(a=>a.kind==="contains"&&a.source===t.id).map(a=>r.nodes.find(q=>q.id===a.target)).filter(a=>!!a&&(a.type==="team"||a.type==="global_team")):[],oe=P?Se(t):[],H=Z(t),O=P?String(H.team_id||""):"",se=typeof H.developer_count=="number"?H.developer_count:0,ue=Array.isArray(H.developer_join_events)?H.developer_join_events:[],pe=Array.isArray(H.collaborating_teams)?H.collaborating_teams:[],ae=r?r.nodes.filter(a=>{var q;return(a.type==="team"||a.type==="global_team")&&String(((q=a.meta)==null?void 0:q.team_id)||"")!==O}):[],n=Z(t),l=U?String(n.path||""):"",g=U?!!n.shared:!1,E=U&&r?r.edges.filter(a=>a.source===t.id&&a.kind==="contains").map(a=>r.nodes.find(q=>q.id===a.target)).filter(a=>!!a&&a.type==="session"):[],F=async()=>{o("reset");try{await W.enterpriseResetWorkspace(),p()}finally{o(null)}},D=async()=>{if(N.trim()){o("create-project");try{await W.enterpriseCreateProject({name:N.trim()}),z(""),p()}finally{o(null)}}},J=async()=>{if(!(!G||!M.trim())){o("create-team");try{await W.enterpriseCreateProjectTeam(G,{name:M.trim()}),T(""),p()}finally{o(null)}}},K=async()=>{if(!(!O||!k.trim())){o("add-folder");try{await W.enterpriseAddTeamFolder(O,{local_path:k.trim(),label:w.trim()||void 0,kind:"folder"}),c(""),j(""),p()}finally{o(null)}}},f=async()=>{if(!(!O||!i.trim())){o("add-git");try{await W.enterpriseAddTeamFolder(O,{repo_url:i.trim(),kind:"git"}),v(""),p()}finally{o(null)}}},Y=async()=>{o("pick-folder");try{const a=await W.pickFolderPath("Pick a folder for this team");a.ok&&a.path&&c(a.path)}finally{o(null)}},fe=async()=>{if(G){o("delete-project");try{await W.enterpriseDeleteProject(G),p()}finally{o(null)}}},ge=async a=>{if(a){o("remove-folder");try{await W.enterpriseRemoveFolder(a),p()}finally{o(null)}}},we=async()=>{if(!(!O||!m.trim())){o("collaborate");try{await W.enterpriseAddTeamCollaborator(O,m.trim()),S(""),p()}finally{o(null)}}},Ce=async()=>{if(O){o("extract");try{const a=await W.enterpriseExtractTeam(O);p();const q=`AST extraction · ${a.folders_seen} folder(s) · ${a.files_seen} files (${a.files_extracted} new, ${a.files_cached} cached) · ${a.nodes_upserted} nodes · ${a.edges_upserted} edges`;window.alert(q)}catch(a){window.alert(`Extraction failed: ${String(a)}`)}finally{o(null)}}},_e=async()=>{if(l){o("share-folder");try{await W.localContextShareFolder({path:l,shared:!g}),p()}finally{o(null)}}};return e.jsxs("aside",{style:{position:"absolute",top:0,right:0,bottom:0,width:440,background:"var(--bg)",borderLeft:"1px solid var(--border)",boxShadow:"-12px 0 30px rgba(20,16,10,0.06)",display:"flex",flexDirection:"column",zIndex:25,animation:"fadein 0.18s ease"},children:[e.jsxs("header",{style:{padding:"12px 16px",borderBottom:"1px solid var(--border)",display:"flex",alignItems:"center",justifyContent:"space-between"},children:[e.jsxs("div",{children:[e.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:9,letterSpacing:"0.12em",color:"var(--ink3)",textTransform:"uppercase"},children:Ne(t.type)}),e.jsx("div",{style:{fontSize:16,fontWeight:500,color:"var(--ink)"},children:t.label})]}),e.jsx("button",{onClick:x,"aria-label":"Close drawer",style:{width:24,height:24,borderRadius:4,background:"var(--surface)",border:"1px solid var(--border)",color:"var(--ink2)"},children:"×"})]}),e.jsxs("div",{style:{flex:1,overflowY:"auto",padding:14},children:[U?e.jsx(Pe,{node:t,sessions:E,shared:g,onToggleShare:_e,onOpenVault:()=>C(),onOpenSession:y,busy:h}):null,I?e.jsx(Oe,{node:t,onOpenSession:()=>y(t.id,je(t))}):null,L?e.jsx(Le,{projects:V,projectName:N,onProjectName:z,onCreateProject:D,confirmReset:R,onAskReset:()=>_(!0),onCancelReset:()=>_(!1),onConfirmReset:F,busy:h}):null,d?e.jsx(Ie,{teams:ne,teamName:M,onTeamName:T,onCreateTeam:J,confirmDelete:$,onAskDelete:()=>X(!0),onCancelDelete:()=>X(!1),onConfirmDelete:fe,busy:h}):null,P?e.jsx(Be,{node:t,repoMappings:oe,developerCount:se,developerJoinEvents:ue,collaboratingTeams:pe,collaboratorOptions:ae,collabTeamId:m,onCollabTeamId:S,onAddCollaborator:we,folderPath:k,folderLabel:w,gitUrl:i,onFolderPath:c,onFolderLabel:j,onGitUrl:v,onPickFolder:Y,onAddFolder:K,onAddGit:f,onExtract:Ce,onRemoveFolder:ge,onOpenVault:()=>{var a;return C(String(((a=t.meta)==null?void 0:a.team_id)||""))},isManager:u,viewer:s,busy:h}):null,Q?e.jsx($e,{node:t,onRemove:()=>{var a;return ge(String(((a=t.meta)==null?void 0:a.mapping_id)||""))},busy:h}):null]})]})}function Ne(t){return t==="global_team"?"GLOBAL TEAM":t==="folder"?"LOCAL FOLDER":t==="session"?"AGENT SESSION":t.toUpperCase()}function Pe({node:t,sessions:r,shared:s,onToggleShare:u,onOpenVault:x,onOpenSession:C,busy:y}){const p=Z(t),h=String(p.path||""),o=Number(p.active_session_count||0),k=typeof p.context_manager=="object"&&p.context_manager?p.context_manager:null;return e.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:14},children:[e.jsx("button",{onClick:x,style:ce(!1),children:"OPEN CONTEXT →"}),e.jsxs("div",{children:[e.jsx(A,{children:"Folder"}),e.jsxs("div",{style:{marginTop:6,padding:"8px 10px",border:"1px solid var(--border)",borderRadius:4,background:"var(--surface)",display:"grid",gap:4},children:[e.jsx(B,{label:"path",value:h||t.label,mono:!0}),e.jsx(B,{label:"sessions",value:`${r.length}`}),e.jsx(B,{label:"active",value:`${o}`})]})]}),e.jsxs("div",{children:[e.jsx(A,{children:"Context manager"}),e.jsxs("div",{style:{marginTop:6,padding:"8px 10px",border:"1px solid var(--border)",borderRadius:4,background:"var(--surface)",display:"grid",gap:4},children:[e.jsx(B,{label:"owner",value:String((k==null?void 0:k.display_name)||`${t.label} Context Manager`)}),e.jsx(B,{label:"scope",value:String((k==null?void 0:k.folder_path)||h||t.label),mono:!0})]})]}),e.jsxs("div",{children:[e.jsx(A,{children:"Context sharing"}),e.jsx("button",{onClick:u,disabled:y==="share-folder",style:s?ce(y==="share-folder"):ee(y==="share-folder"),children:y==="share-folder"?"UPDATING...":s?"SHARING ENABLED":"SHARE THIS FOLDER"}),e.jsx(re,{children:"Shared folders exchange local context with the other folders you enable here."})]}),e.jsxs("div",{children:[e.jsxs(A,{children:["Agent sessions (",r.length,")"]}),r.length===0?e.jsx(re,{children:"No Claude Code or Codex sessions detected for this folder yet."}):e.jsx("div",{style:{display:"grid",gap:4,marginTop:6},children:r.map(c=>{const w=Z(c),j=ze(w.runtime);return e.jsxs("div",{style:{padding:"7px 10px",border:"1px solid var(--border)",borderLeft:`3px solid ${j}`,borderRadius:4,background:"var(--surface)",display:"grid",gridTemplateColumns:"minmax(0, 1fr) auto",gap:8,alignItems:"center"},children:[e.jsxs("div",{style:{minWidth:0},children:[e.jsx("div",{style:{fontSize:12,color:"var(--ink)",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},title:c.label,children:c.label}),e.jsxs("div",{style:{fontFamily:"var(--mono)",fontSize:10,color:j},children:[String(w.runtime||"agent")," · ",String(w.state||"recent")]})]}),e.jsx("button",{onClick:()=>C(c.id,je(c)),style:We(j),children:"OPEN"})]},c.id)})})]})]})}function Oe({node:t,onOpenSession:r}){const s=Z(t);return e.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:14},children:[e.jsx("button",{onClick:r,style:ce(!1),children:"OPEN SESSION TASK →"}),e.jsxs("div",{children:[e.jsx(A,{children:"Session"}),e.jsxs("div",{style:{marginTop:6,padding:"8px 10px",border:"1px solid var(--border)",borderRadius:4,background:"var(--surface)",display:"grid",gap:4},children:[e.jsx(B,{label:"runtime",value:String(s.runtime||"agent")}),e.jsx(B,{label:"state",value:String(s.state||"recent")}),s.model?e.jsx(B,{label:"model",value:String(s.model)}):null,s.cwd?e.jsx(B,{label:"folder",value:String(s.cwd),mono:!0}):null,s.updated_at?e.jsx(B,{label:"updated",value:String(s.updated_at)}):null]})]}),s.preview?e.jsxs("div",{children:[e.jsx(A,{children:"Preview"}),e.jsx("div",{style:{marginTop:6,padding:"8px 10px",border:"1px solid var(--border)",borderRadius:4,background:"var(--surface)",color:"var(--ink2)",fontSize:12,lineHeight:1.5,whiteSpace:"pre-wrap"},children:String(s.preview)})]}):null]})}function Le({projects:t,projectName:r,onProjectName:s,onCreateProject:u,confirmReset:x,onAskReset:C,onCancelReset:y,onConfirmReset:p,busy:h}){return e.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:16},children:[e.jsxs("div",{children:[e.jsx(A,{children:"Add a project"}),e.jsxs("div",{style:{display:"flex",gap:6,marginTop:8},children:[e.jsx("input",{value:r,onChange:o=>s(o.target.value),placeholder:"e.g. Text_to_Speech",onKeyDown:o=>{o.key==="Enter"&&u()},style:te}),e.jsx("button",{onClick:u,disabled:h==="create-project"||!r.trim(),style:ee(h==="create-project"),children:"CREATE"})]})]}),e.jsxs("div",{children:[e.jsxs(A,{children:["Projects (",t.length,")"]}),t.length===0?e.jsx(re,{children:"No projects yet. Add one above."}):e.jsx("div",{style:{display:"flex",flexDirection:"column",gap:4,marginTop:8},children:t.map(o=>e.jsxs("div",{style:{padding:"6px 10px",border:"1px solid var(--border)",borderRadius:4,background:"var(--surface)",display:"flex",alignItems:"center",justifyContent:"space-between",fontSize:12,color:"var(--ink)"},children:[e.jsx("span",{children:o.label}),e.jsx(xe,{label:"open",tone:"default"})]},o.id))})]}),e.jsxs("div",{style:{marginTop:8,paddingTop:14,borderTop:"1px solid var(--border)"},children:[e.jsx(A,{children:"Danger zone"}),x?e.jsxs("div",{style:{marginTop:8,padding:10,border:"1px solid var(--rose)",background:"var(--rose-dim)",borderRadius:4,fontSize:12,color:"var(--ink)"},children:[e.jsx("div",{style:{marginBottom:8},children:"This deletes projects, teams, folders, context items, proposals, and findings for this org. Memory engrams in the Dhee tier are not affected. Continue?"}),e.jsxs("div",{style:{display:"flex",gap:6},children:[e.jsx("button",{onClick:p,disabled:h==="reset",style:ie,children:h==="reset"?"RESETTING…":"YES, RESET"}),e.jsx("button",{onClick:y,style:he,children:"CANCEL"})]})]}):e.jsx("button",{onClick:C,style:ie,children:"RESET WORKSPACE"})]})]})}function Ie({teams:t,teamName:r,onTeamName:s,onCreateTeam:u,confirmDelete:x,onAskDelete:C,onCancelDelete:y,onConfirmDelete:p,busy:h}){return e.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:16},children:[e.jsxs("div",{children:[e.jsx(A,{children:"Add a team"}),e.jsxs("div",{style:{display:"flex",gap:6,marginTop:8},children:[e.jsx("input",{value:r,onChange:o=>s(o.target.value),placeholder:"Backend, Frontend, Data, Mobile",onKeyDown:o=>{o.key==="Enter"&&u()},style:te}),e.jsx("button",{onClick:u,disabled:h==="create-team"||!r.trim(),style:ee(h==="create-team"),children:"ADD"})]})]}),e.jsxs("div",{children:[e.jsxs(A,{children:["Teams (",t.length,")"]}),t.length===0?e.jsx(re,{children:"No teams yet."}):e.jsx("div",{style:{display:"flex",flexDirection:"column",gap:4,marginTop:8},children:t.map(o=>{const k=ke(Se(o));return e.jsxs("div",{style:{padding:"6px 10px",border:"1px solid var(--border)",borderRadius:4,background:"var(--surface)",display:"flex",alignItems:"center",justifyContent:"space-between",gap:8},children:[e.jsx("span",{style:{color:"var(--ink)",fontSize:12},children:o.label}),e.jsx(xe,{label:`${k.length} ${k.length===1?"repo":"repos"}`,tone:k.length?"green":"default"})]},o.id)})})]}),e.jsx("div",{style:{marginTop:8,paddingTop:14,borderTop:"1px solid var(--border)"},children:x?e.jsxs("div",{style:{marginTop:4,padding:10,border:"1px solid var(--rose)",background:"var(--rose-dim)",borderRadius:4,fontSize:12,color:"var(--ink)"},children:[e.jsx("div",{style:{marginBottom:8},children:"Deletes the project and all its teams + context. Continue?"}),e.jsxs("div",{style:{display:"flex",gap:6},children:[e.jsx("button",{onClick:p,disabled:h==="delete-project",style:ie,children:h==="delete-project"?"DELETING…":"YES, DELETE"}),e.jsx("button",{onClick:y,style:he,children:"CANCEL"})]})]}):e.jsx("button",{onClick:C,style:ie,children:"DELETE PROJECT"})})]})}function Be({node:t,repoMappings:r,developerCount:s,developerJoinEvents:u,collaboratingTeams:x,collaboratorOptions:C,collabTeamId:y,onCollabTeamId:p,onAddCollaborator:h,folderPath:o,folderLabel:k,gitUrl:c,onFolderPath:w,onFolderLabel:j,onGitUrl:N,onPickFolder:z,onAddFolder:M,onAddGit:T,onExtract:i,onRemoveFolder:v,onOpenVault:m,busy:S}){const R=Z(t),_=R.context_manager,$=String(R.team_id||""),X=String(R.project_id||""),L=ke(r);return e.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:14},children:[e.jsx("button",{onClick:m,style:ce(!1),children:"OPEN CONTEXT →"}),e.jsxs("div",{children:[e.jsx(A,{children:"Team details"}),e.jsxs("div",{style:{marginTop:6,display:"grid",gap:4,padding:"8px 10px",border:"1px solid var(--border)",borderRadius:4,background:"var(--surface)",fontSize:12},children:[e.jsx(B,{label:"team",value:$||t.label}),X?e.jsx(B,{label:"project",value:X}):null,e.jsx(B,{label:"git access",value:`${s} dev${s===1?"":"s"} joined`})]})]}),e.jsxs("div",{children:[e.jsx(A,{children:"Manager"}),e.jsx("div",{style:{marginTop:6,padding:"8px 10px",border:"1px solid var(--border)",borderRadius:4,background:"var(--surface)",fontSize:12},children:_!=null&&_.display_name?e.jsxs(e.Fragment,{children:[e.jsx("div",{children:_.display_name}),e.jsx("div",{style:{fontSize:10,color:"var(--ink3)"},children:_.manager_id})]}):e.jsx("span",{style:{color:"var(--ink3)"},children:"no manager assigned"})})]}),e.jsxs("div",{children:[e.jsx(A,{children:"Add a local folder"}),e.jsxs("div",{style:{display:"flex",gap:6,marginTop:8},children:[e.jsx("input",{value:o,onChange:d=>w(d.target.value),placeholder:"/Users/me/code/backend",onKeyDown:d=>{d.key==="Enter"&&M()},style:te}),e.jsx("button",{onClick:z,disabled:S==="pick-folder",style:he,title:"Browse",children:"BROWSE"})]}),e.jsxs("div",{style:{display:"flex",gap:6,marginTop:6},children:[e.jsx("input",{value:k,onChange:d=>j(d.target.value),placeholder:"Optional label",style:te}),e.jsx("button",{onClick:M,disabled:S==="add-folder"||!o.trim(),style:ee(S==="add-folder"),children:"ADD"})]})]}),e.jsxs("div",{children:[e.jsx(A,{children:"Add a git repo"}),e.jsxs("div",{style:{display:"flex",gap:6,marginTop:8},children:[e.jsx("input",{value:c,onChange:d=>N(d.target.value),placeholder:"git@github.com:org/backend.git",onKeyDown:d=>{d.key==="Enter"&&T()},style:te}),e.jsx("button",{onClick:T,disabled:S==="add-git"||!c.trim(),style:ee(S==="add-git"),children:"ADD"})]})]}),e.jsxs("div",{children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",justifyContent:"space-between",gap:8},children:[e.jsxs(A,{children:["Git + local folders (",L.length,")"]}),e.jsx("button",{onClick:i,disabled:S==="extract"||L.length===0,title:"Run AST extraction for this team's local folders",style:ee(S==="extract"),children:S==="extract"?"INDEXING...":"INDEX TEAM"})]}),L.length===0?e.jsx(re,{children:"None mapped to this team."}):e.jsx("div",{style:{display:"flex",flexDirection:"column",gap:4,marginTop:6},children:L.map(d=>{const P=String(d.mapping_id||d.local_path||d.repo_url);return e.jsxs("div",{style:{padding:"8px 10px",border:"1px solid var(--border)",borderRadius:4,background:"var(--surface)",display:"grid",gap:4},children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",justifyContent:"space-between",gap:8},children:[e.jsx("div",{style:{fontSize:12,color:"var(--ink)"},children:Ae(d)}),e.jsx("button",{onClick:()=>v(d.mapping_id),style:Ge,title:"Remove mapping","aria-label":"Remove mapping",children:"×"})]}),d.repo_url?e.jsx(B,{label:"repo",value:String(d.repo_url),mono:!0}):null,d.local_path?e.jsx(B,{label:"folder",value:String(d.local_path),mono:!0}):null]},P)})})]}),u.length?e.jsxs("div",{children:[e.jsx(A,{children:"Recent joins"}),e.jsx("div",{style:{display:"grid",gap:4,marginTop:6},children:u.slice(0,4).map((d,P)=>e.jsxs("div",{style:{padding:"6px 10px",border:"1px solid var(--border)",borderRadius:4,background:"var(--surface)",fontSize:11,color:"var(--ink2)"},children:[e.jsx("div",{style:{fontFamily:"var(--mono)",overflow:"hidden",textOverflow:"ellipsis"},children:d.repo_root||"workspace"}),e.jsxs("div",{style:{color:"var(--ink3)",marginTop:2},children:[d.role||"developer"," - ",d.received_at||"recent"]})]},`${d.repo_root||"join"}-${P}`))})]}):null,e.jsxs("div",{children:[e.jsx(A,{children:"Collaborate teams"}),x.length===0?e.jsx(re,{children:"No team context shares yet."}):e.jsx("div",{style:{display:"flex",flexWrap:"wrap",gap:6,marginTop:6},children:x.map(d=>e.jsx(xe,{label:String(d.name||d.team_id),tone:"default"},String(d.team_id||d.name)))}),e.jsxs("div",{style:{display:"flex",gap:6,marginTop:8},children:[e.jsxs("select",{value:y,onChange:d=>p(d.target.value),style:te,children:[e.jsx("option",{value:"",children:"Select team"}),C.map(d=>{var Q;const P=String(((Q=d.meta)==null?void 0:Q.team_id)||"");return e.jsx("option",{value:P,children:d.label},d.id)})]}),e.jsx("button",{onClick:h,disabled:S==="collaborate"||!y,style:ee(S==="collaborate"),children:"ADD"})]})]})]})}function $e({node:t,onRemove:r,busy:s}){const u=t.meta||{};return e.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:12},children:[e.jsx(A,{children:"Folder / path"}),e.jsx("div",{style:{padding:"8px 10px",border:"1px solid var(--border)",borderRadius:4,background:"var(--surface)",fontFamily:"var(--mono)",fontSize:11,color:"var(--ink2)",wordBreak:"break-all"},children:u.local_path||u.repo_url||t.label}),e.jsx("button",{onClick:r,disabled:s==="remove-folder",style:ie,children:s==="remove-folder"?"REMOVING…":"REMOVE"})]})}function B({label:t,value:r,mono:s=!1}){return e.jsxs("div",{style:{display:"grid",gridTemplateColumns:"82px minmax(0, 1fr)",gap:8,alignItems:"baseline"},children:[e.jsx("span",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)",textTransform:"uppercase"},children:t}),e.jsx("span",{title:r,style:{minWidth:0,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap",fontFamily:s?"var(--mono)":void 0,fontSize:s?10:12,color:"var(--ink2)"},children:r})]})}function re({children:t}){return e.jsx("div",{style:{fontSize:11,color:"var(--ink3)",marginTop:6},children:t})}function xe({label:t,tone:r="default"}){const u={default:{bg:"var(--surface)",fg:"var(--ink2)"},green:{bg:"var(--green-dim)",fg:"var(--green)"},indigo:{bg:"var(--indigo-dim)",fg:"var(--indigo)"},rose:{bg:"var(--rose-dim)",fg:"var(--rose)"},accent:{bg:"var(--accent-dim)",fg:"var(--accent)"}}[r];return e.jsx("span",{style:{display:"inline-flex",padding:"2px 7px",borderRadius:3,background:u.bg,color:u.fg,fontFamily:"var(--mono)",fontSize:9,letterSpacing:"0.04em"},children:t})}const te={flex:1,fontFamily:"var(--mono)",fontSize:11,padding:"6px 8px",background:"var(--surface)",border:"1px solid var(--border)",borderRadius:3,color:"var(--ink)"};function ee(t){return{fontFamily:"var(--mono)",fontSize:10,padding:"5px 12px",background:t?"var(--surface)":"var(--accent-dim)",color:"var(--accent)",border:"1px solid var(--accent)",borderRadius:3,cursor:t?"wait":"pointer"}}function ce(t){return{fontFamily:"var(--mono)",fontSize:11,padding:"8px 12px",background:t?"var(--surface)":"var(--accent-dim)",color:"var(--accent)",border:"1px solid var(--accent)",borderRadius:4,textAlign:"center",cursor:t?"wait":"pointer"}}function We(t){return{fontFamily:"var(--mono)",fontSize:9,padding:"5px 8px",background:"white",color:t,border:`1px solid ${t}`,borderRadius:3,cursor:"pointer",whiteSpace:"nowrap"}}const he={fontFamily:"var(--mono)",fontSize:10,padding:"5px 10px",background:"var(--surface)",color:"var(--ink2)",border:"1px solid var(--border)",borderRadius:3},ie={fontFamily:"var(--mono)",fontSize:10,padding:"5px 12px",background:"var(--rose-dim)",color:"var(--rose)",border:"1px solid var(--rose)",borderRadius:3},Ge={width:22,height:22,borderRadius:3,background:"var(--surface)",color:"var(--ink2)",border:"1px solid var(--border)",fontSize:12,lineHeight:1};function Ye(t,r={}){const s=r.siblingGap??24,u=r.levelGap??100,x=new Map;for(const i of t)x.set(i.id,{id:i.id,parent:i.parent,children:[],width:i.width,height:i.height,depth:i.depth,subtreeWidth:i.width,x:0,y:0});const C=[];for(const i of x.values())i.parent&&x.has(i.parent)?x.get(i.parent).children.push(i.id):C.push(i);const y=i=>{const v=x.get(i);if(!v.children.length)return v.subtreeWidth=v.width,v.subtreeWidth;let m=0;for(const S of v.children)m+=y(S);return m+=s*(v.children.length-1),v.subtreeWidth=Math.max(v.width,m),v.subtreeWidth},p=new Map;for(const i of x.values())p.set(i.depth,Math.max(p.get(i.depth)||0,i.height));const h=new Map;let o=0;const k=Math.max(0,...Array.from(p.keys()));for(let i=0;i<=k;i+=1)h.set(i,o),o+=(p.get(i)||0)+u;const c=(i,v)=>{const m=x.get(i);if(m.y=h.get(m.depth)||0,!m.children.length){m.x=v+m.width/2;return}let S=v,R=[];for(const _ of m.children){const $=x.get(_);c(_,S),R.push($.x),S+=$.subtreeWidth+s}if(R.length===0)m.x=v+m.width/2;else{const _=Math.min(...R),$=Math.max(...R);m.x=(_+$)/2}};let w=0;for(const i of C)y(i.id),c(i.id,w),w+=i.subtreeWidth+s*2;let j=1/0,N=1/0,z=-1/0,M=-1/0;const T=[];for(const i of x.values()){const v=i.width/2;j=Math.min(j,i.x-v),z=Math.max(z,i.x+v),N=Math.min(N,i.y),M=Math.max(M,i.y+i.height),T.push({id:i.id,parent:i.parent,width:i.width,height:i.height,depth:i.depth,x:i.x,y:i.y})}return Number.isFinite(j)||(j=0,N=0,z=0,M=0),{nodes:T,bounds:{minX:j,minY:N,maxX:z,maxY:M}}}const le={workspace:{w:280,h:76},project:{w:200,h:64},team:{w:180,h:56},global_team:{w:200,h:60},repo:{w:180,h:52},folder:{w:230,h:70},session:{w:190,h:58},integration:{w:150,h:46},default:{w:160,h:50}};function Xe(t){return le[t]?le[t]:t.startsWith("integration:")?le.integration:le.default}function me(t){return String((t.meta||{}).runtime||"").toLowerCase()}function He(t){return t==="codex"?"var(--indigo)":t==="claude-code"||t==="claude"?"var(--accent)":"var(--ink3)"}function Ke(t){return t==="codex"?"var(--indigo-dim)":t==="claude-code"||t==="claude"?"var(--accent-dim)":"var(--surface)"}function Ue(t,r){return r==="needs_work"?"var(--rose)":r==="watch"?"var(--accent)":t==="folder"?"var(--green)":t==="session"?"var(--accent)":t==="workspace"?"var(--ink2)":t==="project"?"var(--accent)":t==="team"?"var(--green)":t==="global_team"?"var(--indigo)":t==="repo"?"var(--green)":t==="integration:slack"?"var(--indigo)":t==="integration:docs"?"var(--accent)":t==="integration:email"?"var(--rose)":t==="integration:git"?"var(--green)":"var(--ink3)"}function Ve(t){return t.health==="needs_work"?"var(--rose)":t.type==="session"?He(me(t)):Ue(t.type,t.health)}function Je(t){return t==="workspace"?"white":t==="folder"?"var(--green-dim)":t==="session"?"var(--surface)":t==="project"?"var(--accent-dim)":t==="team"||t==="global_team"?"var(--surface)":t==="repo"?"var(--green-dim)":(t.startsWith("integration:"),"var(--surface)")}function qe(t){return t.type==="session"?Ke(me(t)):Je(t.type)}function Qe({graph:t,viewer:r,onOpenVault:s,onOpenSession:u,onChanged:x}){var ae;const C=b.useRef(null),y=b.useRef({down:!1,lastX:0,lastY:0}),p=b.useRef(!1),h=b.useRef(null),[o,k]=b.useState({w:1200,h:700}),[c,w]=b.useState(0),[j,N]=b.useState(0),[z,M]=b.useState(1),[T,i]=b.useState(null),[v,m]=b.useState(null),[S,R]=b.useState(null),[_,$]=b.useState(""),[X,L]=b.useState(null),d=((ae=t.raw)==null?void 0:ae.mode)==="local_context",P=async()=>{R("add-folder"),L(null);try{const n=await W.pickFolderPath("Choose a folder to share context");n.ok&&n.path&&(await W.localContextAddFolder({path:n.path,shared:!0}),x())}catch(n){L(String(n))}finally{R(null)}},Q=async()=>{const n=_.trim();if(!(!n||S)){R("manual-folder"),L(null);try{await W.localContextAddFolder({path:n,shared:!0}),$(""),x()}catch(l){L(String(l))}finally{R(null)}}};b.useEffect(()=>{const n=C.current;if(!n)return;const l=()=>{const E=n.getBoundingClientRect(),F={w:Math.round(E.width),h:Math.round(E.height)};k(D=>D.w===F.w&&D.h===F.h?D:F)};l();const g=new ResizeObserver(l);return g.observe(n),()=>g.disconnect()},[]);const{positions:U,bounds:I}=b.useMemo(()=>{const n=new Map;for(const f of t.edges){if(f.kind!=="contains"&&f.kind!=="uses")continue;const Y=n.get(f.source)||[];Y.push(f.target),n.set(f.source,Y)}const l=new Map;for(const f of t.edges)f.kind!=="contains"&&f.kind!=="uses"||l.set(f.target,f.source);const g=new Map,F=t.nodes.filter(f=>f.type==="workspace"||!l.has(f.id)).map(f=>f.id).map(f=>({id:f,depth:0}));for(;F.length;){const{id:f,depth:Y}=F.shift();if(!g.has(f)){g.set(f,Y);for(const fe of n.get(f)||[])F.push({id:fe,depth:Y+1})}}const D=t.nodes.map(f=>{const Y=Xe(f.type);return{id:f.id,parent:l.get(f.id)||null,width:Y.w,height:Y.h,depth:g.get(f.id)??0}}),J=Ye(D,{siblingGap:24,levelGap:90}),K=new Map;for(const f of J.nodes)K.set(f.id,{x:f.x,y:f.y,w:f.width,h:f.height});return{positions:K,bounds:J.bounds}},[t.nodes,t.edges]),V=b.useMemo(()=>JSON.stringify({nodes:t.nodes.map(n=>[n.id,n.type,n.label,n.health||""]),edges:t.edges.map(n=>[n.source,n.target,n.kind])}),[t.nodes,t.edges]),G=()=>{const n=I.maxX-I.minX,l=I.maxY-I.minY;if(n<=0||l<=0||o.w<=0||o.h<=0)return!1;const g=80,E=(o.w-g*2)/n,F=(o.h-g*2)/l,D=Math.min(1.2,Math.max(.45,Math.min(E,F)));return M(D),w(o.w/2-(I.minX+I.maxX)/2*D),N(g-I.minY*D),!0};b.useEffect(()=>{const n=`${o.w}x${o.h}`,l=h.current,g=(l==null?void 0:l.structure)!==V,E=(l==null?void 0:l.size)!==n;if(!(!g&&!E)){if(l&&p.current){h.current={structure:V,size:n};return}G()&&(h.current={structure:V,size:n})}},[I.minX,I.maxX,I.minY,I.maxY,o.w,o.h,V]);const ne=n=>{var J;n.preventDefault(),p.current=!0;const l=Math.exp(-n.deltaY*.001),g=Math.min(2.5,Math.max(.3,z*l)),E=(J=C.current)==null?void 0:J.getBoundingClientRect();if(!E){M(g);return}const F=n.clientX-E.left,D=n.clientY-E.top;w(K=>F-(F-K)/z*g),N(K=>D-(D-K)/z*g),M(g)},oe=n=>{y.current={down:!0,lastX:n.clientX,lastY:n.clientY}},H=n=>{const l=y.current;l.down&&(p.current=!0,w(g=>g+(n.clientX-l.lastX)),N(g=>g+(n.clientY-l.lastY)),y.current={down:!0,lastX:n.clientX,lastY:n.clientY})},O=()=>{y.current.down=!1},se=()=>{p.current=!1,G()&&(h.current={structure:V,size:`${o.w}x${o.h}`})},ue=t.totals.projects>0||t.totals.teams>0||t.totals.repos>0||(t.totals.folders||0)>0||(t.totals.sessions||0)>0||t.totals.context_items>0;if(!t.live||!ue)return e.jsxs("div",{className:"dhee-canvas-bg",style:{position:"relative",flex:1,display:"flex",flexDirection:"column",overflow:"hidden"},children:[e.jsx(ye,{onOpenContext:()=>s()}),e.jsx(rt,{viewer:r,busy:S,onAddFolder:P,manualPath:_,onManualPathChange:$,onAddManualFolder:Q,error:X})]});const pe=(r==null?void 0:r.role)==="manager"||(r==null?void 0:r.role)==="admin";return e.jsxs("div",{ref:C,onWheel:ne,onMouseDown:oe,onMouseMove:H,onMouseUp:O,onMouseLeave:O,onDoubleClick:se,className:"dhee-canvas-bg",style:{position:"relative",flex:1,cursor:y.current.down?"grabbing":"grab",overflow:"hidden"},children:[e.jsx(ye,{onOpenContext:()=>s()}),e.jsx("svg",{width:o.w,height:o.h,style:{display:"block",userSelect:"none"},children:e.jsxs("g",{transform:`translate(${c},${j}) scale(${z})`,children:[t.edges.map((n,l)=>{const g=U.get(n.source),E=U.get(n.target);if(!g||!E)return null;const F=g.x,D=g.y+g.h/2,J=E.x,K=E.y-E.h/2,f=(D+K)/2,Y=v&&(n.source===v||n.target===v);return e.jsx("path",{d:`M ${F},${D} C ${F},${f} ${J},${f} ${J},${K}`,className:Y?"dhee-edge-path dhee-edge-path--highlight":v?"dhee-edge-path dhee-edge-path--dim":"dhee-edge-path"},`${n.source}-${n.target}-${l}`)}),t.nodes.map(n=>{const l=U.get(n.id);if(!l)return null;const g=Ve(n),E=qe(n),F=(T==null?void 0:T.id)===n.id;return e.jsxs("g",{onMouseEnter:()=>m(n.id),onMouseLeave:()=>m(null),onClick:D=>{D.stopPropagation(),i(n)},style:{cursor:"pointer"},children:[e.jsx("rect",{x:l.x-l.w/2,y:l.y,width:l.w,height:l.h,rx:8,ry:8,fill:E,stroke:g,strokeWidth:F?2.4:n.type==="workspace"?1.6:1.2}),n.type==="workspace"?e.jsx("text",{x:l.x,y:l.y+24,textAnchor:"middle",fontFamily:"var(--mono)",fontSize:9,letterSpacing:"0.12em",fill:"var(--ink3)",pointerEvents:"none",children:"WORKSPACE"}):null,e.jsx("text",{x:l.x,y:n.type==="workspace"?l.y+50:l.y+l.h/2+4,textAnchor:"middle",fontFamily:"var(--font)",fontSize:n.type==="workspace"?16:12,fontWeight:n.type==="workspace"?500:400,fill:"var(--ink)",pointerEvents:"none",children:et(n.label,n.type==="workspace"?30:22)}),n.type!=="workspace"?e.jsx("text",{x:l.x,y:l.y+16,textAnchor:"middle",fontFamily:"var(--mono)",fontSize:8,letterSpacing:"0.1em",fill:"var(--ink3)",pointerEvents:"none",children:Ze(n)}):null]},n.id)})]})}),d?e.jsxs("div",{style:{position:"absolute",left:12,top:12,display:"flex",gap:8,alignItems:"center",background:"var(--bg)",border:"1px solid var(--border)",borderRadius:4,padding:"6px 8px",boxShadow:"0 4px 14px rgba(20,16,10,0.05)"},children:[e.jsx("button",{onClick:P,disabled:S==="add-folder",style:{fontFamily:"var(--mono)",fontSize:10,border:"1px solid var(--accent)",color:"var(--accent)",background:"var(--accent-dim)",borderRadius:4,padding:"5px 8px",cursor:S==="add-folder"?"wait":"pointer"},children:S==="add-folder"?"ADDING...":"ADD FOLDER"}),e.jsx("span",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)"},children:"local context sharing"})]}):null,e.jsx(tt,{graph:t}),T?e.jsx(Me,{node:T,graph:t,viewer:r,isManager:pe,onClose:()=>i(null),onOpenVault:s,onOpenSession:u,onChanged:()=>{i(null),x()}}):null]})}function Ze(t){const r=t.type;if(r==="session"){const s=me(t);if(s==="codex")return"CODEX";if(s==="claude-code"||s==="claude")return"CLAUDE CODE"}return r.startsWith("integration:")?r.split(":")[1].toUpperCase():r.toUpperCase()}function et(t,r){return t?t.length<=r?t:t.slice(0,r-1)+"…":""}function tt({graph:t}){var r;return((r=t.raw)==null?void 0:r.mode)==="local_context"?e.jsxs("div",{style:{position:"absolute",left:12,bottom:12,fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)",background:"var(--bg)",border:"1px solid var(--border)",borderRadius:4,padding:"5px 9px",letterSpacing:"0.04em"},children:[t.totals.folders||t.totals.repos," folders ·"," ",t.totals.sessions||t.totals.teams," sessions ·"," ",t.totals.shared_folders||0," shared"]}):e.jsxs("div",{style:{position:"absolute",left:12,bottom:12,fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)",background:"var(--bg)",border:"1px solid var(--border)",borderRadius:4,padding:"5px 9px",letterSpacing:"0.04em"},children:[t.totals.projects," projects · ",t.totals.teams," teams ·"," ",t.totals.repos," folders · ",t.totals.pending_proposals," pending"]})}function rt({viewer:t,busy:r,onAddFolder:s,manualPath:u,onManualPathChange:x,onAddManualFolder:C,error:y}){return e.jsx("div",{className:"dhee-canvas-bg",style:{flex:1,display:"flex",alignItems:"center",justifyContent:"center",padding:32},children:e.jsxs("div",{style:{width:"min(640px, calc(100vw - 64px))",padding:24,background:"var(--bg)",border:"1px solid var(--border)",borderRadius:8,boxShadow:"0 6px 18px rgba(20,16,10,0.05)",animation:"dhee-card-in 0.22s ease"},children:[e.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:9,letterSpacing:"0.12em",color:"var(--ink3)",textTransform:"uppercase",marginBottom:6},children:t!=null&&t.user_id?`local · ${t.user_id}`:"local context"}),e.jsx("div",{style:{fontSize:18,fontWeight:500,color:"var(--ink)",marginBottom:6},children:"No local agent folders yet"}),e.jsx("div",{style:{fontSize:12,color:"var(--ink2)",lineHeight:1.5,marginBottom:14},children:"Dhee will show local Claude Code and Codex sessions grouped by folder. Add any folder you want to share context with the rest of your local agent folders."}),e.jsx("div",{style:{display:"flex",gap:8,flexWrap:"wrap"},children:e.jsx("button",{onClick:s,disabled:r==="add-folder",style:{fontFamily:"var(--mono)",fontSize:11,padding:"8px 14px",background:r==="add-folder"?"var(--surface)":"var(--accent-dim)",color:"var(--accent)",border:"1px solid var(--accent)",borderRadius:4,cursor:r==="add-folder"?"wait":"pointer"},children:r==="add-folder"?"ADDING...":"ADD FOLDER"})}),e.jsxs("div",{style:{marginTop:14,borderTop:"1px solid var(--border)",paddingTop:14,display:"grid",gap:8},children:[e.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)"},children:"REPO PATH"}),e.jsxs("div",{style:{display:"flex",gap:8},children:[e.jsx("input",{value:u,onChange:p=>x(p.target.value),onKeyDown:p=>{p.key==="Enter"&&C()},placeholder:"/Users/me/work/repo",style:{flex:1,minWidth:0,border:"1px solid var(--border)",borderRadius:4,padding:"9px 10px",background:"white",fontFamily:"var(--mono)",fontSize:11,color:"var(--ink)"}}),e.jsx("button",{onClick:()=>void C(),disabled:!u.trim()||r==="manual-folder",style:{fontFamily:"var(--mono)",fontSize:11,padding:"8px 12px",background:"white",color:"var(--accent)",border:"1px solid var(--border)",borderRadius:4,opacity:!u.trim()||r==="manual-folder"?.55:1,cursor:!u.trim()||r==="manual-folder"?"not-allowed":"pointer"},children:r==="manual-folder"?"LINKING...":"LINK PATH"})]}),e.jsx("code",{style:{display:"block",border:"1px solid var(--border)",background:"var(--surface)",borderRadius:4,padding:"8px 9px",fontFamily:"var(--mono)",fontSize:10,color:"var(--ink2)",overflowWrap:"anywhere"},children:"dhee onboard --root ."}),y?e.jsx("div",{style:{fontSize:11,color:"var(--rose)"},children:y}):null]})]})})}const de={live:!1,org_id:"local",nodes:[],edges:[],totals:{projects:0,teams:0,repos:0,context_items:0,pending_proposals:0,folders:0,sessions:0,shared_folders:0},raw:{mode:"local_context",folders:[],sessions:[],shared_folder_paths:[],context_index:[],pending_proposals:[],context_managers_by_team:{}}};function nt(t){var r;return t?{...de,...t,org_id:t.org_id||"local",nodes:t.nodes||[],edges:t.edges||[],totals:{...de.totals,...t.totals||{}},raw:{...de.raw,...t.raw||{},mode:((r=t.raw)==null?void 0:r.mode)||"local_context"}}:de}function st(t){return e.jsx(Qe,{graph:nt(t.orgGraph),viewer:t.viewer||null,tweaks:t.tweaks,onOpenVault:t.onOpenVault||(()=>{}),onOpenSession:t.onSelectSession,onChanged:t.onOrgGraphChanged||(()=>{})})}export{st as CanvasView}; diff --git a/dhee/ui/web/dist/assets/index-BKI2JxEf.js b/dhee/ui/web/dist/assets/index-BKI2JxEf.js new file mode 100644 index 0000000..2ce244b --- /dev/null +++ b/dhee/ui/web/dist/assets/index-BKI2JxEf.js @@ -0,0 +1,51 @@ +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))r(o);new MutationObserver(o=>{for(const l of o)if(l.type==="childList")for(const s of l.addedNodes)s.tagName==="LINK"&&s.rel==="modulepreload"&&r(s)}).observe(document,{childList:!0,subtree:!0});function n(o){const l={};return o.integrity&&(l.integrity=o.integrity),o.referrerPolicy&&(l.referrerPolicy=o.referrerPolicy),o.crossOrigin==="use-credentials"?l.credentials="include":o.crossOrigin==="anonymous"?l.credentials="omit":l.credentials="same-origin",l}function r(o){if(o.ep)return;o.ep=!0;const l=n(o);fetch(o.href,l)}})();function kp(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var Ju={exports:{}},Mo={},Xu={exports:{}},ue={};/** + * @license React + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var xi=Symbol.for("react.element"),jp=Symbol.for("react.portal"),wp=Symbol.for("react.fragment"),Cp=Symbol.for("react.strict_mode"),bp=Symbol.for("react.profiler"),_p=Symbol.for("react.provider"),Ep=Symbol.for("react.context"),Tp=Symbol.for("react.forward_ref"),zp=Symbol.for("react.suspense"),Rp=Symbol.for("react.memo"),Np=Symbol.for("react.lazy"),fd=Symbol.iterator;function Pp(e){return e===null||typeof e!="object"?null:(e=fd&&e[fd]||e["@@iterator"],typeof e=="function"?e:null)}var qu={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},Zu=Object.assign,Gu={};function jr(e,t,n){this.props=e,this.context=t,this.refs=Gu,this.updater=n||qu}jr.prototype.isReactComponent={};jr.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};jr.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function ec(){}ec.prototype=jr.prototype;function Ns(e,t,n){this.props=e,this.context=t,this.refs=Gu,this.updater=n||qu}var Ps=Ns.prototype=new ec;Ps.constructor=Ns;Zu(Ps,jr.prototype);Ps.isPureReactComponent=!0;var pd=Array.isArray,tc=Object.prototype.hasOwnProperty,Fs={current:null},nc={key:!0,ref:!0,__self:!0,__source:!0};function rc(e,t,n){var r,o={},l=null,s=null;if(t!=null)for(r in t.ref!==void 0&&(s=t.ref),t.key!==void 0&&(l=""+t.key),t)tc.call(t,r)&&!nc.hasOwnProperty(r)&&(o[r]=t[r]);var a=arguments.length-2;if(a===1)o.children=n;else if(1>>1,ie=C[ee];if(0>>1;eeo(se,W))meo($,se)?(C[ee]=$,C[me]=W,ee=me):(C[ee]=se,C[fe]=W,ee=fe);else if(meo($,W))C[ee]=$,C[me]=W,ee=me;else break e}}return A}function o(C,A){var W=C.sortIndex-A.sortIndex;return W!==0?W:C.id-A.id}if(typeof performance=="object"&&typeof performance.now=="function"){var l=performance;e.unstable_now=function(){return l.now()}}else{var s=Date,a=s.now();e.unstable_now=function(){return s.now()-a}}var d=[],c=[],g=1,m=null,x=3,S=!1,E=!1,T=!1,H=typeof setTimeout=="function"?setTimeout:null,h=typeof clearTimeout=="function"?clearTimeout:null,u=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function f(C){for(var A=n(c);A!==null;){if(A.callback===null)r(c);else if(A.startTime<=C)r(c),A.sortIndex=A.expirationTime,t(d,A);else break;A=n(c)}}function p(C){if(T=!1,f(C),!E)if(n(d)!==null)E=!0,V(j);else{var A=n(c);A!==null&&ye(p,A.startTime-C)}}function j(C,A){E=!1,T&&(T=!1,h(z),z=-1),S=!0;var W=x;try{for(f(A),m=n(d);m!==null&&(!(m.expirationTime>A)||C&&!J());){var ee=m.callback;if(typeof ee=="function"){m.callback=null,x=m.priorityLevel;var ie=ee(m.expirationTime<=A);A=e.unstable_now(),typeof ie=="function"?m.callback=ie:m===n(d)&&r(d),f(A)}else r(d);m=n(d)}if(m!==null)var D=!0;else{var fe=n(c);fe!==null&&ye(p,fe.startTime-A),D=!1}return D}finally{m=null,x=W,S=!1}}var _=!1,P=null,z=-1,Q=5,R=-1;function J(){return!(e.unstable_now()-RC||125ee?(C.sortIndex=W,t(c,C),n(d)===null&&C===n(c)&&(T?(h(z),z=-1):T=!0,ye(p,W-ee))):(C.sortIndex=ie,t(d,C),E||S||(E=!0,V(j))),C},e.unstable_shouldYield=J,e.unstable_wrapCallback=function(C){var A=x;return function(){var W=x;x=A;try{return C.apply(this,arguments)}finally{x=W}}}})(ac);sc.exports=ac;var Hp=sc.exports;/** + * @license React + * react-dom.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Vp=v,vt=Hp;function L(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Ml=Object.prototype.hasOwnProperty,Kp=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,hd={},gd={};function Qp(e){return Ml.call(gd,e)?!0:Ml.call(hd,e)?!1:Kp.test(e)?gd[e]=!0:(hd[e]=!0,!1)}function Yp(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function Jp(e,t,n,r){if(t===null||typeof t>"u"||Yp(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function lt(e,t,n,r,o,l,s){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=o,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=l,this.removeEmptyString=s}var Xe={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){Xe[e]=new lt(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];Xe[t]=new lt(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){Xe[e]=new lt(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){Xe[e]=new lt(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){Xe[e]=new lt(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){Xe[e]=new lt(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){Xe[e]=new lt(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){Xe[e]=new lt(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){Xe[e]=new lt(e,5,!1,e.toLowerCase(),null,!1,!1)});var Os=/[\-:]([a-z])/g;function $s(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(Os,$s);Xe[t]=new lt(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(Os,$s);Xe[t]=new lt(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(Os,$s);Xe[t]=new lt(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){Xe[e]=new lt(e,1,!1,e.toLowerCase(),null,!1,!1)});Xe.xlinkHref=new lt("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){Xe[e]=new lt(e,1,!1,e.toLowerCase(),null,!0,!0)});function Ds(e,t,n,r){var o=Xe.hasOwnProperty(t)?Xe[t]:null;(o!==null?o.type!==0:r||!(2a||o[s]!==l[a]){var d=` +`+o[s].replace(" at new "," at ");return e.displayName&&d.includes("")&&(d=d.replace("",e.displayName)),d}while(1<=s&&0<=a);break}}}finally{il=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?Ir(e):""}function Xp(e){switch(e.tag){case 5:return Ir(e.type);case 16:return Ir("Lazy");case 13:return Ir("Suspense");case 19:return Ir("SuspenseList");case 0:case 2:case 15:return e=ol(e.type,!1),e;case 11:return e=ol(e.type.render,!1),e;case 1:return e=ol(e.type,!0),e;default:return""}}function Ul(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case Zn:return"Fragment";case qn:return"Portal";case Il:return"Profiler";case Ms:return"StrictMode";case Bl:return"Suspense";case Al:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case cc:return(e.displayName||"Context")+".Consumer";case uc:return(e._context.displayName||"Context")+".Provider";case Is:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case Bs:return t=e.displayName||null,t!==null?t:Ul(e.type)||"Memo";case en:t=e._payload,e=e._init;try{return Ul(e(t))}catch{}}return null}function qp(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return Ul(t);case 8:return t===Ms?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function yn(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function pc(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function Zp(e){var t=pc(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var o=n.get,l=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return o.call(this)},set:function(s){r=""+s,l.call(this,s)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(s){r=""+s},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function zi(e){e._valueTracker||(e._valueTracker=Zp(e))}function mc(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=pc(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function fo(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function Wl(e,t){var n=t.checked;return Ne({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function yd(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=yn(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function hc(e,t){t=t.checked,t!=null&&Ds(e,"checked",t,!1)}function Hl(e,t){hc(e,t);var n=yn(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?Vl(e,t.type,n):t.hasOwnProperty("defaultValue")&&Vl(e,t.type,yn(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function xd(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function Vl(e,t,n){(t!=="number"||fo(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var Br=Array.isArray;function dr(e,t,n,r){if(e=e.options,t){t={};for(var o=0;o"+t.valueOf().toString()+"",t=Ri.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function ri(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var Kr={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},Gp=["Webkit","ms","Moz","O"];Object.keys(Kr).forEach(function(e){Gp.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Kr[t]=Kr[e]})});function xc(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||Kr.hasOwnProperty(e)&&Kr[e]?(""+t).trim():t+"px"}function Sc(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,o=xc(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,o):e[n]=o}}var em=Ne({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function Yl(e,t){if(t){if(em[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(L(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(L(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(L(61))}if(t.style!=null&&typeof t.style!="object")throw Error(L(62))}}function Jl(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var Xl=null;function As(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var ql=null,ur=null,cr=null;function jd(e){if(e=ji(e)){if(typeof ql!="function")throw Error(L(280));var t=e.stateNode;t&&(t=Wo(t),ql(e.stateNode,e.type,t))}}function kc(e){ur?cr?cr.push(e):cr=[e]:ur=e}function jc(){if(ur){var e=ur,t=cr;if(cr=ur=null,jd(e),t)for(e=0;e>>=0,e===0?32:31-(cm(e)/fm|0)|0}var Ni=64,Pi=4194304;function Ar(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function go(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,o=e.suspendedLanes,l=e.pingedLanes,s=n&268435455;if(s!==0){var a=s&~o;a!==0?r=Ar(a):(l&=s,l!==0&&(r=Ar(l)))}else s=n&~o,s!==0?r=Ar(s):l!==0&&(r=Ar(l));if(r===0)return 0;if(t!==0&&t!==r&&!(t&o)&&(o=r&-r,l=t&-t,o>=l||o===16&&(l&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function Si(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-Rt(t),e[t]=n}function gm(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=Yr),Nd=" ",Pd=!1;function Uc(e,t){switch(e){case"keyup":return Hm.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Wc(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Gn=!1;function Km(e,t){switch(e){case"compositionend":return Wc(t);case"keypress":return t.which!==32?null:(Pd=!0,Nd);case"textInput":return e=t.data,e===Nd&&Pd?null:e;default:return null}}function Qm(e,t){if(Gn)return e==="compositionend"||!Js&&Uc(e,t)?(e=Bc(),Gi=Ks=ln=null,Gn=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=$d(n)}}function Qc(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Qc(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Yc(){for(var e=window,t=fo();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=fo(e.document)}return t}function Xs(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function nh(e){var t=Yc(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&Qc(n.ownerDocument.documentElement,n)){if(r!==null&&Xs(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var o=n.textContent.length,l=Math.min(r.start,o);r=r.end===void 0?l:Math.min(r.end,o),!e.extend&&l>r&&(o=r,r=l,l=o),o=Dd(n,l);var s=Dd(n,r);o&&s&&(e.rangeCount!==1||e.anchorNode!==o.node||e.anchorOffset!==o.offset||e.focusNode!==s.node||e.focusOffset!==s.offset)&&(t=t.createRange(),t.setStart(o.node,o.offset),e.removeAllRanges(),l>r?(e.addRange(t),e.extend(s.node,s.offset)):(t.setEnd(s.node,s.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,er=null,rs=null,Xr=null,is=!1;function Md(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;is||er==null||er!==fo(r)||(r=er,"selectionStart"in r&&Xs(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Xr&&di(Xr,r)||(Xr=r,r=xo(rs,"onSelect"),0rr||(e.current=us[rr],us[rr]=null,rr--)}function ke(e,t){rr++,us[rr]=e.current,e.current=t}var xn={},nt=kn(xn),ut=kn(!1),In=xn;function gr(e,t){var n=e.type.contextTypes;if(!n)return xn;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var o={},l;for(l in n)o[l]=t[l];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=o),o}function ct(e){return e=e.childContextTypes,e!=null}function ko(){Ce(ut),Ce(nt)}function Vd(e,t,n){if(nt.current!==xn)throw Error(L(168));ke(nt,t),ke(ut,n)}function rf(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var o in r)if(!(o in t))throw Error(L(108,qp(e)||"Unknown",o));return Ne({},n,r)}function jo(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||xn,In=nt.current,ke(nt,e),ke(ut,ut.current),!0}function Kd(e,t,n){var r=e.stateNode;if(!r)throw Error(L(169));n?(e=rf(e,t,In),r.__reactInternalMemoizedMergedChildContext=e,Ce(ut),Ce(nt),ke(nt,e)):Ce(ut),ke(ut,n)}var Ht=null,Ho=!1,xl=!1;function of(e){Ht===null?Ht=[e]:Ht.push(e)}function mh(e){Ho=!0,of(e)}function jn(){if(!xl&&Ht!==null){xl=!0;var e=0,t=ge;try{var n=Ht;for(ge=1;e>=s,o-=s,Vt=1<<32-Rt(t)+o|n<z?(Q=P,P=null):Q=P.sibling;var R=x(h,P,f[z],p);if(R===null){P===null&&(P=Q);break}e&&P&&R.alternate===null&&t(h,P),u=l(R,u,z),_===null?j=R:_.sibling=R,_=R,P=Q}if(z===f.length)return n(h,P),be&&zn(h,z),j;if(P===null){for(;zz?(Q=P,P=null):Q=P.sibling;var J=x(h,P,R.value,p);if(J===null){P===null&&(P=Q);break}e&&P&&J.alternate===null&&t(h,P),u=l(J,u,z),_===null?j=J:_.sibling=J,_=J,P=Q}if(R.done)return n(h,P),be&&zn(h,z),j;if(P===null){for(;!R.done;z++,R=f.next())R=m(h,R.value,p),R!==null&&(u=l(R,u,z),_===null?j=R:_.sibling=R,_=R);return be&&zn(h,z),j}for(P=r(h,P);!R.done;z++,R=f.next())R=S(P,h,z,R.value,p),R!==null&&(e&&R.alternate!==null&&P.delete(R.key===null?z:R.key),u=l(R,u,z),_===null?j=R:_.sibling=R,_=R);return e&&P.forEach(function(X){return t(h,X)}),be&&zn(h,z),j}function H(h,u,f,p){if(typeof f=="object"&&f!==null&&f.type===Zn&&f.key===null&&(f=f.props.children),typeof f=="object"&&f!==null){switch(f.$$typeof){case Ti:e:{for(var j=f.key,_=u;_!==null;){if(_.key===j){if(j=f.type,j===Zn){if(_.tag===7){n(h,_.sibling),u=o(_,f.props.children),u.return=h,h=u;break e}}else if(_.elementType===j||typeof j=="object"&&j!==null&&j.$$typeof===en&&Jd(j)===_.type){n(h,_.sibling),u=o(_,f.props),u.ref=Nr(h,_,f),u.return=h,h=u;break e}n(h,_);break}else t(h,_);_=_.sibling}f.type===Zn?(u=Dn(f.props.children,h.mode,p,f.key),u.return=h,h=u):(p=so(f.type,f.key,f.props,null,h.mode,p),p.ref=Nr(h,u,f),p.return=h,h=p)}return s(h);case qn:e:{for(_=f.key;u!==null;){if(u.key===_)if(u.tag===4&&u.stateNode.containerInfo===f.containerInfo&&u.stateNode.implementation===f.implementation){n(h,u.sibling),u=o(u,f.children||[]),u.return=h,h=u;break e}else{n(h,u);break}else t(h,u);u=u.sibling}u=El(f,h.mode,p),u.return=h,h=u}return s(h);case en:return _=f._init,H(h,u,_(f._payload),p)}if(Br(f))return E(h,u,f,p);if(_r(f))return T(h,u,f,p);Ii(h,f)}return typeof f=="string"&&f!==""||typeof f=="number"?(f=""+f,u!==null&&u.tag===6?(n(h,u.sibling),u=o(u,f),u.return=h,h=u):(n(h,u),u=_l(f,h.mode,p),u.return=h,h=u),s(h)):n(h,u)}return H}var yr=df(!0),uf=df(!1),bo=kn(null),_o=null,lr=null,ea=null;function ta(){ea=lr=_o=null}function na(e){var t=bo.current;Ce(bo),e._currentValue=t}function ps(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,r!==null&&(r.childLanes|=t)):r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function pr(e,t){_o=e,ea=lr=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&(dt=!0),e.firstContext=null)}function Ct(e){var t=e._currentValue;if(ea!==e)if(e={context:e,memoizedValue:t,next:null},lr===null){if(_o===null)throw Error(L(308));lr=e,_o.dependencies={lanes:0,firstContext:e}}else lr=lr.next=e;return t}var Fn=null;function ra(e){Fn===null?Fn=[e]:Fn.push(e)}function cf(e,t,n,r){var o=t.interleaved;return o===null?(n.next=n,ra(t)):(n.next=o.next,o.next=n),t.interleaved=n,Xt(e,r)}function Xt(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var tn=!1;function ia(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function ff(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Qt(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function pn(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,pe&2){var o=r.pending;return o===null?t.next=t:(t.next=o.next,o.next=t),r.pending=t,Xt(e,n)}return o=r.interleaved,o===null?(t.next=t,ra(r)):(t.next=o.next,o.next=t),r.interleaved=t,Xt(e,n)}function to(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Ws(e,n)}}function Xd(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var o=null,l=null;if(n=n.firstBaseUpdate,n!==null){do{var s={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};l===null?o=l=s:l=l.next=s,n=n.next}while(n!==null);l===null?o=l=t:l=l.next=t}else o=l=t;n={baseState:r.baseState,firstBaseUpdate:o,lastBaseUpdate:l,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function Eo(e,t,n,r){var o=e.updateQueue;tn=!1;var l=o.firstBaseUpdate,s=o.lastBaseUpdate,a=o.shared.pending;if(a!==null){o.shared.pending=null;var d=a,c=d.next;d.next=null,s===null?l=c:s.next=c,s=d;var g=e.alternate;g!==null&&(g=g.updateQueue,a=g.lastBaseUpdate,a!==s&&(a===null?g.firstBaseUpdate=c:a.next=c,g.lastBaseUpdate=d))}if(l!==null){var m=o.baseState;s=0,g=c=d=null,a=l;do{var x=a.lane,S=a.eventTime;if((r&x)===x){g!==null&&(g=g.next={eventTime:S,lane:0,tag:a.tag,payload:a.payload,callback:a.callback,next:null});e:{var E=e,T=a;switch(x=t,S=n,T.tag){case 1:if(E=T.payload,typeof E=="function"){m=E.call(S,m,x);break e}m=E;break e;case 3:E.flags=E.flags&-65537|128;case 0:if(E=T.payload,x=typeof E=="function"?E.call(S,m,x):E,x==null)break e;m=Ne({},m,x);break e;case 2:tn=!0}}a.callback!==null&&a.lane!==0&&(e.flags|=64,x=o.effects,x===null?o.effects=[a]:x.push(a))}else S={eventTime:S,lane:x,tag:a.tag,payload:a.payload,callback:a.callback,next:null},g===null?(c=g=S,d=m):g=g.next=S,s|=x;if(a=a.next,a===null){if(a=o.shared.pending,a===null)break;x=a,a=x.next,x.next=null,o.lastBaseUpdate=x,o.shared.pending=null}}while(!0);if(g===null&&(d=m),o.baseState=d,o.firstBaseUpdate=c,o.lastBaseUpdate=g,t=o.shared.interleaved,t!==null){o=t;do s|=o.lane,o=o.next;while(o!==t)}else l===null&&(o.shared.lanes=0);Un|=s,e.lanes=s,e.memoizedState=m}}function qd(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=kl.transition;kl.transition={};try{e(!1),t()}finally{ge=n,kl.transition=r}}function zf(){return bt().memoizedState}function yh(e,t,n){var r=hn(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},Rf(e))Nf(t,n);else if(n=cf(e,t,n,r),n!==null){var o=it();Nt(n,e,r,o),Pf(n,t,r)}}function xh(e,t,n){var r=hn(e),o={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(Rf(e))Nf(t,o);else{var l=e.alternate;if(e.lanes===0&&(l===null||l.lanes===0)&&(l=t.lastRenderedReducer,l!==null))try{var s=t.lastRenderedState,a=l(s,n);if(o.hasEagerState=!0,o.eagerState=a,Pt(a,s)){var d=t.interleaved;d===null?(o.next=o,ra(t)):(o.next=d.next,d.next=o),t.interleaved=o;return}}catch{}finally{}n=cf(e,t,o,r),n!==null&&(o=it(),Nt(n,e,r,o),Pf(n,t,r))}}function Rf(e){var t=e.alternate;return e===Re||t!==null&&t===Re}function Nf(e,t){qr=zo=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Pf(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Ws(e,n)}}var Ro={readContext:Ct,useCallback:qe,useContext:qe,useEffect:qe,useImperativeHandle:qe,useInsertionEffect:qe,useLayoutEffect:qe,useMemo:qe,useReducer:qe,useRef:qe,useState:qe,useDebugValue:qe,useDeferredValue:qe,useTransition:qe,useMutableSource:qe,useSyncExternalStore:qe,useId:qe,unstable_isNewReconciler:!1},Sh={readContext:Ct,useCallback:function(e,t){return $t().memoizedState=[e,t===void 0?null:t],e},useContext:Ct,useEffect:Gd,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,ro(4194308,4,Cf.bind(null,t,e),n)},useLayoutEffect:function(e,t){return ro(4194308,4,e,t)},useInsertionEffect:function(e,t){return ro(4,2,e,t)},useMemo:function(e,t){var n=$t();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=$t();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=yh.bind(null,Re,e),[r.memoizedState,e]},useRef:function(e){var t=$t();return e={current:e},t.memoizedState=e},useState:Zd,useDebugValue:fa,useDeferredValue:function(e){return $t().memoizedState=e},useTransition:function(){var e=Zd(!1),t=e[0];return e=vh.bind(null,e[1]),$t().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=Re,o=$t();if(be){if(n===void 0)throw Error(L(407));n=n()}else{if(n=t(),Ve===null)throw Error(L(349));An&30||gf(r,t,n)}o.memoizedState=n;var l={value:n,getSnapshot:t};return o.queue=l,Gd(yf.bind(null,r,l,e),[e]),r.flags|=2048,vi(9,vf.bind(null,r,l,n,t),void 0,null),n},useId:function(){var e=$t(),t=Ve.identifierPrefix;if(be){var n=Kt,r=Vt;n=(r&~(1<<32-Rt(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=hi++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=s.createElement(n,{is:r.is}):(e=s.createElement(n),n==="select"&&(s=e,r.multiple?s.multiple=!0:r.size&&(s.size=r.size))):e=s.createElementNS(e,n),e[Dt]=t,e[fi]=r,Uf(e,t,!1,!1),t.stateNode=e;e:{switch(s=Jl(n,r),n){case"dialog":we("cancel",e),we("close",e),o=r;break;case"iframe":case"object":case"embed":we("load",e),o=r;break;case"video":case"audio":for(o=0;okr&&(t.flags|=128,r=!0,Pr(l,!1),t.lanes=4194304)}else{if(!r)if(e=To(s),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),Pr(l,!0),l.tail===null&&l.tailMode==="hidden"&&!s.alternate&&!be)return Ze(t),null}else 2*Oe()-l.renderingStartTime>kr&&n!==1073741824&&(t.flags|=128,r=!0,Pr(l,!1),t.lanes=4194304);l.isBackwards?(s.sibling=t.child,t.child=s):(n=l.last,n!==null?n.sibling=s:t.child=s,l.last=s)}return l.tail!==null?(t=l.tail,l.rendering=t,l.tail=t.sibling,l.renderingStartTime=Oe(),t.sibling=null,n=Te.current,ke(Te,r?n&1|2:n&1),t):(Ze(t),null);case 22:case 23:return ya(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?mt&1073741824&&(Ze(t),t.subtreeFlags&6&&(t.flags|=8192)):Ze(t),null;case 24:return null;case 25:return null}throw Error(L(156,t.tag))}function Th(e,t){switch(Zs(t),t.tag){case 1:return ct(t.type)&&ko(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return xr(),Ce(ut),Ce(nt),sa(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return la(t),null;case 13:if(Ce(Te),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(L(340));vr()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return Ce(Te),null;case 4:return xr(),null;case 10:return na(t.type._context),null;case 22:case 23:return ya(),null;case 24:return null;default:return null}}var Ai=!1,et=!1,zh=typeof WeakSet=="function"?WeakSet:Set,U=null;function sr(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){Pe(e,t,r)}else n.current=null}function js(e,t,n){try{n()}catch(r){Pe(e,t,r)}}var uu=!1;function Rh(e,t){if(os=vo,e=Yc(),Xs(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var o=r.anchorOffset,l=r.focusNode;r=r.focusOffset;try{n.nodeType,l.nodeType}catch{n=null;break e}var s=0,a=-1,d=-1,c=0,g=0,m=e,x=null;t:for(;;){for(var S;m!==n||o!==0&&m.nodeType!==3||(a=s+o),m!==l||r!==0&&m.nodeType!==3||(d=s+r),m.nodeType===3&&(s+=m.nodeValue.length),(S=m.firstChild)!==null;)x=m,m=S;for(;;){if(m===e)break t;if(x===n&&++c===o&&(a=s),x===l&&++g===r&&(d=s),(S=m.nextSibling)!==null)break;m=x,x=m.parentNode}m=S}n=a===-1||d===-1?null:{start:a,end:d}}else n=null}n=n||{start:0,end:0}}else n=null;for(ls={focusedElem:e,selectionRange:n},vo=!1,U=t;U!==null;)if(t=U,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,U=e;else for(;U!==null;){t=U;try{var E=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(E!==null){var T=E.memoizedProps,H=E.memoizedState,h=t.stateNode,u=h.getSnapshotBeforeUpdate(t.elementType===t.type?T:Et(t.type,T),H);h.__reactInternalSnapshotBeforeUpdate=u}break;case 3:var f=t.stateNode.containerInfo;f.nodeType===1?f.textContent="":f.nodeType===9&&f.documentElement&&f.removeChild(f.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(L(163))}}catch(p){Pe(t,t.return,p)}if(e=t.sibling,e!==null){e.return=t.return,U=e;break}U=t.return}return E=uu,uu=!1,E}function Zr(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var o=r=r.next;do{if((o.tag&e)===e){var l=o.destroy;o.destroy=void 0,l!==void 0&&js(t,n,l)}o=o.next}while(o!==r)}}function Qo(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function ws(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function Vf(e){var t=e.alternate;t!==null&&(e.alternate=null,Vf(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Dt],delete t[fi],delete t[ds],delete t[fh],delete t[ph])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function Kf(e){return e.tag===5||e.tag===3||e.tag===4}function cu(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||Kf(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Cs(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=So));else if(r!==4&&(e=e.child,e!==null))for(Cs(e,t,n),e=e.sibling;e!==null;)Cs(e,t,n),e=e.sibling}function bs(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(bs(e,t,n),e=e.sibling;e!==null;)bs(e,t,n),e=e.sibling}var Qe=null,Tt=!1;function Gt(e,t,n){for(n=n.child;n!==null;)Qf(e,t,n),n=n.sibling}function Qf(e,t,n){if(It&&typeof It.onCommitFiberUnmount=="function")try{It.onCommitFiberUnmount(Io,n)}catch{}switch(n.tag){case 5:et||sr(n,t);case 6:var r=Qe,o=Tt;Qe=null,Gt(e,t,n),Qe=r,Tt=o,Qe!==null&&(Tt?(e=Qe,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):Qe.removeChild(n.stateNode));break;case 18:Qe!==null&&(Tt?(e=Qe,n=n.stateNode,e.nodeType===8?yl(e.parentNode,n):e.nodeType===1&&yl(e,n),si(e)):yl(Qe,n.stateNode));break;case 4:r=Qe,o=Tt,Qe=n.stateNode.containerInfo,Tt=!0,Gt(e,t,n),Qe=r,Tt=o;break;case 0:case 11:case 14:case 15:if(!et&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){o=r=r.next;do{var l=o,s=l.destroy;l=l.tag,s!==void 0&&(l&2||l&4)&&js(n,t,s),o=o.next}while(o!==r)}Gt(e,t,n);break;case 1:if(!et&&(sr(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(a){Pe(n,t,a)}Gt(e,t,n);break;case 21:Gt(e,t,n);break;case 22:n.mode&1?(et=(r=et)||n.memoizedState!==null,Gt(e,t,n),et=r):Gt(e,t,n);break;default:Gt(e,t,n)}}function fu(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new zh),t.forEach(function(r){var o=Ih.bind(null,e,r);n.has(r)||(n.add(r),r.then(o,o))})}}function _t(e,t){var n=t.deletions;if(n!==null)for(var r=0;ro&&(o=s),r&=~l}if(r=o,r=Oe()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*Ph(r/1960))-r,10e?16:e,sn===null)var r=!1;else{if(e=sn,sn=null,Fo=0,pe&6)throw Error(L(331));var o=pe;for(pe|=4,U=e.current;U!==null;){var l=U,s=l.child;if(U.flags&16){var a=l.deletions;if(a!==null){for(var d=0;dOe()-ga?$n(e,0):ha|=n),ft(e,t)}function tp(e,t){t===0&&(e.mode&1?(t=Pi,Pi<<=1,!(Pi&130023424)&&(Pi=4194304)):t=1);var n=it();e=Xt(e,t),e!==null&&(Si(e,t,n),ft(e,n))}function Mh(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),tp(e,n)}function Ih(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,o=e.memoizedState;o!==null&&(n=o.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(L(314))}r!==null&&r.delete(t),tp(e,n)}var np;np=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||ut.current)dt=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return dt=!1,_h(e,t,n);dt=!!(e.flags&131072)}else dt=!1,be&&t.flags&1048576&&lf(t,Co,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;io(e,t),e=t.pendingProps;var o=gr(t,nt.current);pr(t,n),o=da(null,t,r,e,o,n);var l=ua();return t.flags|=1,typeof o=="object"&&o!==null&&typeof o.render=="function"&&o.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,ct(r)?(l=!0,jo(t)):l=!1,t.memoizedState=o.state!==null&&o.state!==void 0?o.state:null,ia(t),o.updater=Ko,t.stateNode=o,o._reactInternals=t,hs(t,r,e,n),t=ys(null,t,r,!0,l,n)):(t.tag=0,be&&l&&qs(t),rt(null,t,o,n),t=t.child),t;case 16:r=t.elementType;e:{switch(io(e,t),e=t.pendingProps,o=r._init,r=o(r._payload),t.type=r,o=t.tag=Ah(r),e=Et(r,e),o){case 0:t=vs(null,t,r,e,n);break e;case 1:t=su(null,t,r,e,n);break e;case 11:t=ou(null,t,r,e,n);break e;case 14:t=lu(null,t,r,Et(r.type,e),n);break e}throw Error(L(306,r,""))}return t;case 0:return r=t.type,o=t.pendingProps,o=t.elementType===r?o:Et(r,o),vs(e,t,r,o,n);case 1:return r=t.type,o=t.pendingProps,o=t.elementType===r?o:Et(r,o),su(e,t,r,o,n);case 3:e:{if(If(t),e===null)throw Error(L(387));r=t.pendingProps,l=t.memoizedState,o=l.element,ff(e,t),Eo(t,r,null,n);var s=t.memoizedState;if(r=s.element,l.isDehydrated)if(l={element:r,isDehydrated:!1,cache:s.cache,pendingSuspenseBoundaries:s.pendingSuspenseBoundaries,transitions:s.transitions},t.updateQueue.baseState=l,t.memoizedState=l,t.flags&256){o=Sr(Error(L(423)),t),t=au(e,t,r,n,o);break e}else if(r!==o){o=Sr(Error(L(424)),t),t=au(e,t,r,n,o);break e}else for(ht=fn(t.stateNode.containerInfo.firstChild),gt=t,be=!0,zt=null,n=uf(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(vr(),r===o){t=qt(e,t,n);break e}rt(e,t,r,n)}t=t.child}return t;case 5:return pf(t),e===null&&fs(t),r=t.type,o=t.pendingProps,l=e!==null?e.memoizedProps:null,s=o.children,ss(r,o)?s=null:l!==null&&ss(r,l)&&(t.flags|=32),Mf(e,t),rt(e,t,s,n),t.child;case 6:return e===null&&fs(t),null;case 13:return Bf(e,t,n);case 4:return oa(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=yr(t,null,r,n):rt(e,t,r,n),t.child;case 11:return r=t.type,o=t.pendingProps,o=t.elementType===r?o:Et(r,o),ou(e,t,r,o,n);case 7:return rt(e,t,t.pendingProps,n),t.child;case 8:return rt(e,t,t.pendingProps.children,n),t.child;case 12:return rt(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,o=t.pendingProps,l=t.memoizedProps,s=o.value,ke(bo,r._currentValue),r._currentValue=s,l!==null)if(Pt(l.value,s)){if(l.children===o.children&&!ut.current){t=qt(e,t,n);break e}}else for(l=t.child,l!==null&&(l.return=t);l!==null;){var a=l.dependencies;if(a!==null){s=l.child;for(var d=a.firstContext;d!==null;){if(d.context===r){if(l.tag===1){d=Qt(-1,n&-n),d.tag=2;var c=l.updateQueue;if(c!==null){c=c.shared;var g=c.pending;g===null?d.next=d:(d.next=g.next,g.next=d),c.pending=d}}l.lanes|=n,d=l.alternate,d!==null&&(d.lanes|=n),ps(l.return,n,t),a.lanes|=n;break}d=d.next}}else if(l.tag===10)s=l.type===t.type?null:l.child;else if(l.tag===18){if(s=l.return,s===null)throw Error(L(341));s.lanes|=n,a=s.alternate,a!==null&&(a.lanes|=n),ps(s,n,t),s=l.sibling}else s=l.child;if(s!==null)s.return=l;else for(s=l;s!==null;){if(s===t){s=null;break}if(l=s.sibling,l!==null){l.return=s.return,s=l;break}s=s.return}l=s}rt(e,t,o.children,n),t=t.child}return t;case 9:return o=t.type,r=t.pendingProps.children,pr(t,n),o=Ct(o),r=r(o),t.flags|=1,rt(e,t,r,n),t.child;case 14:return r=t.type,o=Et(r,t.pendingProps),o=Et(r.type,o),lu(e,t,r,o,n);case 15:return $f(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,o=t.pendingProps,o=t.elementType===r?o:Et(r,o),io(e,t),t.tag=1,ct(r)?(e=!0,jo(t)):e=!1,pr(t,n),Ff(t,r,o),hs(t,r,o,n),ys(null,t,r,!0,e,n);case 19:return Af(e,t,n);case 22:return Df(e,t,n)}throw Error(L(156,t.tag))};function rp(e,t){return zc(e,t)}function Bh(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function jt(e,t,n,r){return new Bh(e,t,n,r)}function Sa(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Ah(e){if(typeof e=="function")return Sa(e)?1:0;if(e!=null){if(e=e.$$typeof,e===Is)return 11;if(e===Bs)return 14}return 2}function gn(e,t){var n=e.alternate;return n===null?(n=jt(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function so(e,t,n,r,o,l){var s=2;if(r=e,typeof e=="function")Sa(e)&&(s=1);else if(typeof e=="string")s=5;else e:switch(e){case Zn:return Dn(n.children,o,l,t);case Ms:s=8,o|=8;break;case Il:return e=jt(12,n,t,o|2),e.elementType=Il,e.lanes=l,e;case Bl:return e=jt(13,n,t,o),e.elementType=Bl,e.lanes=l,e;case Al:return e=jt(19,n,t,o),e.elementType=Al,e.lanes=l,e;case fc:return Jo(n,o,l,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case uc:s=10;break e;case cc:s=9;break e;case Is:s=11;break e;case Bs:s=14;break e;case en:s=16,r=null;break e}throw Error(L(130,e==null?e:typeof e,""))}return t=jt(s,n,t,o),t.elementType=e,t.type=r,t.lanes=l,t}function Dn(e,t,n,r){return e=jt(7,e,r,t),e.lanes=n,e}function Jo(e,t,n,r){return e=jt(22,e,r,t),e.elementType=fc,e.lanes=n,e.stateNode={isHidden:!1},e}function _l(e,t,n){return e=jt(6,e,null,t),e.lanes=n,e}function El(e,t,n){return t=jt(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function Uh(e,t,n,r,o){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=sl(0),this.expirationTimes=sl(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=sl(0),this.identifierPrefix=r,this.onRecoverableError=o,this.mutableSourceEagerHydrationData=null}function ka(e,t,n,r,o,l,s,a,d){return e=new Uh(e,t,n,a,d),t===1?(t=1,l===!0&&(t|=8)):t=0,l=jt(3,null,null,t),e.current=l,l.stateNode=e,l.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},ia(l),e}function Wh(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(sp)}catch(e){console.error(e)}}sp(),lc.exports=yt;var Yh=lc.exports,Su=Yh;Dl.createRoot=Su.createRoot,Dl.hydrateRoot=Su.hydrateRoot;const Jh="modulepreload",Xh=function(e){return"/"+e},ku={},qh=function(t,n,r){let o=Promise.resolve();if(n&&n.length>0){document.getElementsByTagName("link");const s=document.querySelector("meta[property=csp-nonce]"),a=(s==null?void 0:s.nonce)||(s==null?void 0:s.getAttribute("nonce"));o=Promise.allSettled(n.map(d=>{if(d=Xh(d),d in ku)return;ku[d]=!0;const c=d.endsWith(".css"),g=c?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${d}"]${g}`))return;const m=document.createElement("link");if(m.rel=c?"stylesheet":Jh,c||(m.as="script"),m.crossOrigin="",m.href=d,a&&m.setAttribute("nonce",a),document.head.appendChild(m),c)return new Promise((x,S)=>{m.addEventListener("load",x),m.addEventListener("error",()=>S(new Error(`Unable to preload CSS for ${d}`)))})}))}function l(s){const a=new Event("vite:preloadError",{cancelable:!0});if(a.payload=s,window.dispatchEvent(a),!a.defaultPrevented)throw s}return o.then(s=>{for(const a of s||[])a.status==="rejected"&&l(a.reason);return t().catch(l)})},ao="/api";async function F(e,t){const n=await fetch(ao+e,{...t,headers:{"Content-Type":"application/json",...(t==null?void 0:t.headers)||{}}});if(!n.ok)throw new Error(`${n.status} ${n.statusText}`);return await n.json()}const B={listMemories:()=>F("/memories"),remember:(e,t,n)=>F("/memories",{method:"POST",body:JSON.stringify({content:e,tier:t,tags:n})}),archiveMemory:e=>F(`/memories/${encodeURIComponent(e)}`,{method:"DELETE"}),routerStats:e=>F(`/router/stats${e?`?agent_id=${encodeURIComponent(e)}`:""}`),routerPolicy:()=>F("/router/policy"),routerTune:()=>F("/router/tune",{method:"POST"}),metaBuddhi:()=>F("/meta-buddhi"),evolution:()=>F("/evolution"),conflicts:()=>F("/conflicts"),resolveConflict:(e,t)=>F(`/conflicts/${encodeURIComponent(e)}/resolve`,{method:"POST",body:JSON.stringify({action:t})}),resolveConflictDetailed:(e,t)=>F(`/conflicts/${encodeURIComponent(e)}/resolve`,{method:"POST",body:JSON.stringify(t)}),tasks:()=>F("/tasks"),createTask:(e,t)=>F("/tasks",{method:"POST",body:JSON.stringify({title:e,harness:t})}),taskDetail:(e,t=24)=>F(`/tasks/${encodeURIComponent(e)}?limit=${encodeURIComponent(String(t))}`),updateTaskStatus:(e,t)=>F(`/tasks/${encodeURIComponent(e)}/status`,{method:"POST",body:JSON.stringify({status:t})}),addTaskNote:(e,t)=>F(`/tasks/${encodeURIComponent(e)}/notes`,{method:"POST",body:JSON.stringify({content:t})}),workspaceGraph:(e,t)=>F(`/workspace/graph${e?`?workspace_id=${encodeURIComponent(e)}${t?`&project_id=${encodeURIComponent(t)}`:""}`:""}`),projects:()=>F("/workspaces"),workspaces:()=>F("/workspaces"),createWorkspaceRoot:(e,t)=>F("/workspaces",{method:"POST",body:JSON.stringify({name:e,description:t})}),createProject:(e,t)=>F(`/workspaces/${encodeURIComponent(e)}/projects`,{method:"POST",body:JSON.stringify(t)}),updateProject:(e,t)=>F(`/projects/${encodeURIComponent(e)}`,{method:"PATCH",body:JSON.stringify(t)}),projectSessions:e=>F(`/projects/${encodeURIComponent(e)}/sessions`),projectCanvas:e=>F(`/projects/${encodeURIComponent(e)}/canvas`),workspaceCanvas:e=>F(`/workspaces/${encodeURIComponent(e)}/canvas`),pickFolder:e=>F("/folders/pick",{method:"POST",body:JSON.stringify({prompt:e})}),addWorkspaceFolder:(e,t,n)=>F(`/workspaces/${encodeURIComponent(e)}/folders`,{method:"POST",body:JSON.stringify({path:t,label:n})}),removeWorkspaceFolder:(e,t)=>F(`/workspaces/${encodeURIComponent(e)}/mounts?path=${encodeURIComponent(t)}`,{method:"DELETE"}),updateWorkspace:(e,t)=>F(`/workspaces/${encodeURIComponent(e)}`,{method:"PATCH",body:JSON.stringify(t)}),deleteWorkspace:e=>F(`/workspaces/${encodeURIComponent(e)}`,{method:"DELETE"}),deleteProject:e=>F(`/projects/${encodeURIComponent(e)}`,{method:"DELETE"}),workspaceDetail:e=>F(`/workspaces/${encodeURIComponent(e)}`),workspaceSessions:e=>F(`/workspaces/${encodeURIComponent(e)}/sessions`),sessionDetail:e=>F(`/sessions/${encodeURIComponent(e)}`),launchWorkspaceSession:(e,t,n,r,o,l)=>F(`/workspaces/${encodeURIComponent(e)}/sessions/launch`,{method:"POST",body:JSON.stringify({runtime:t,title:n,permission_mode:r,task_id:o,project_id:l})}),workspaceLineMessages:(e,t)=>F(`/workspaces/${encodeURIComponent(e)}/line/messages?${new URLSearchParams(Object.entries({project_id:t==null?void 0:t.project_id,channel:t==null?void 0:t.channel,cursor:t==null?void 0:t.cursor,limit:t!=null&&t.limit?String(t.limit):void 0}).filter(n=>!!n[1])).toString()}`),publishWorkspaceLineMessage:(e,t)=>F(`/workspaces/${encodeURIComponent(e)}/line/messages`,{method:"POST",body:JSON.stringify(t)}),uploadSessionAsset:async(e,t,n)=>{const r=new FormData;r.append("file",t),n&&r.append("label",n);const o=await fetch(`${ao}/sessions/${encodeURIComponent(e)}/assets`,{method:"POST",body:r});if(!o.ok)throw new Error(`${o.status} ${o.statusText}`);return await o.json()},listProjectAssets:e=>F(`/projects/${encodeURIComponent(e)}/assets`),listWorkspaceAssets:(e,t=!0)=>F(`/workspaces/${encodeURIComponent(e)}/assets?include_project_assets=${t?"true":"false"}`),uploadProjectAsset:async(e,t,n)=>{const r=new FormData;r.append("file",t),n!=null&&n.label&&r.append("label",n.label),n!=null&&n.folder&&r.append("folder",n.folder);const o=await fetch(`${ao}/projects/${encodeURIComponent(e)}/assets`,{method:"POST",body:r});if(!o.ok)throw new Error(`${o.status} ${o.statusText}`);return await o.json()},uploadWorkspaceAsset:async(e,t,n)=>{const r=new FormData;r.append("file",t),n!=null&&n.label&&r.append("label",n.label),n!=null&&n.folder&&r.append("folder",n.folder),n!=null&&n.project_id&&r.append("project_id",n.project_id);const o=await fetch(`${ao}/workspaces/${encodeURIComponent(e)}/assets`,{method:"POST",body:r});if(!o.ok)throw new Error(`${o.status} ${o.statusText}`);return await o.json()},deleteProjectAsset:e=>F(`/project-assets/${encodeURIComponent(e)}`,{method:"DELETE"}),fileContext:(e,t)=>F(`/files/${encodeURIComponent(e)}/context${t?`?workspace_id=${encodeURIComponent(t)}`:""}`),assetContext:e=>F(`/assets/${encodeURIComponent(e)}/context`),askAsset:(e,t)=>F(`/assets/${encodeURIComponent(e)}/ask`,{method:"POST",body:JSON.stringify({question:t})}),runtimeStatus:()=>F("/runtime-status"),status:()=>F("/status"),memoryNow:()=>F("/memory/now"),captureTimeline:(e=16)=>F(`/capture/timeline?limit=${encodeURIComponent(String(e))}`),launch:(e,t,n)=>F("/launch",{method:"POST",body:JSON.stringify({runtime:e,taskId:t,title:n})}),apiKeys:()=>F("/security/api-keys"),storeApiKey:(e,t,n)=>F("/security/api-keys",{method:"POST",body:JSON.stringify({provider:e,apiKey:t,label:n})}),rotateApiKey:(e,t,n)=>F(`/security/api-keys/${encodeURIComponent(e)}/rotate`,{method:"POST",body:JSON.stringify({apiKey:t,label:n})}),me:()=>F("/me"),continuity:()=>F("/continuity"),orgGraph:(e,t)=>{const n=new URLSearchParams;e&&n.set("org",e),t!=null&&t.active&&n.set("active","true");const r=n.toString();return F(`/org/graph${r?`?${r}`:""}`)},routerSessions:e=>{const t=new URLSearchParams;(e==null?void 0:e.active)!=null&&t.set("active",e.active?"true":"false"),e!=null&&e.cursor&&t.set("cursor",e.cursor),e!=null&&e.limit&&t.set("limit",String(e.limit)),e!=null&&e.agent&&t.set("agent",e.agent);const n=t.toString();return F(`/router/sessions${n?`?${n}`:""}`)},contextEntries:(e,t=200)=>{const n=new URLSearchParams;return e&&n.set("repo",e),n.set("limit",String(t)),F(`/context/entries?${n.toString()}`)},contextPromote:e=>F("/context/promote",{method:"POST",body:JSON.stringify(e)}),contextDemote:e=>F("/context/demote",{method:"POST",body:JSON.stringify(e)}),localWorkspaces:()=>F("/local/workspaces"),localWorkspaceCreate:e=>F("/local/workspaces",{method:"POST",body:JSON.stringify(e)}),localContextLinkFolder:e=>F("/local-context/folders/link",{method:"POST",body:JSON.stringify({path:e})}),localContextUnlinkFolder:e=>F("/local-context/folders/unlink",{method:"POST",body:JSON.stringify({path:e})}),contextItems:(e={})=>{const t=new URLSearchParams;e.team&&t.set("team",e.team),e.project&&t.set("project",e.project),e.scope&&t.set("scope",e.scope),e.kind&&t.set("kind",e.kind),e.limit&&t.set("limit",String(e.limit));const n=t.toString();return F(`/context/items${n?`?${n}`:""}`)},contextUsage:(e={})=>{const t=new URLSearchParams;e.team&&t.set("team",e.team),e.project&&t.set("project",e.project),e.scope&&t.set("scope",e.scope),e.kind&&t.set("kind",e.kind),e.limit&&t.set("limit",String(e.limit));const n=t.toString();return F(`/context/usage${n?`?${n}`:""}`)},commandCenter:()=>F("/ui/command-center"),proofReplay:(e=80)=>F(`/ui/proof-replay?limit=${encodeURIComponent(String(e))}`),handoffUi:()=>F("/ui/handoff"),learningsUi:(e=120)=>F(`/ui/learnings?limit=${encodeURIComponent(String(e))}`),promoteLearning:(e,t)=>F(`/ui/learnings/${encodeURIComponent(e)}/promote`,{method:"POST",body:JSON.stringify(t||{approved_by:"dhee-ui"})}),rejectLearning:(e,t)=>F(`/ui/learnings/${encodeURIComponent(e)}/reject`,{method:"POST",body:JSON.stringify(t||{reason:"rejected in Dhee UI"})}),portabilityUi:()=>F("/ui/portability"),exportPackUi:e=>F("/ui/portability/export",{method:"POST",body:JSON.stringify(e||{})}),importPackDryRunUi:e=>F("/ui/portability/import-dry-run",{method:"POST",body:JSON.stringify(e)}),upsertContext:e=>F("/context",{method:"POST",body:JSON.stringify(e)}),proposeContext:e=>F("/proposals",{method:"POST",body:JSON.stringify(e)}),approveProposal:(e,t)=>F(`/proposals/${encodeURIComponent(e)}/approve`,{method:"POST",body:JSON.stringify({reviewer_user_id:t})}),rejectProposal:(e,t)=>F(`/proposals/${encodeURIComponent(e)}/reject`,{method:"POST",body:JSON.stringify({reviewer_user_id:t})}),inbox:(e={})=>{const t=new URLSearchParams;e.team&&t.set("team",e.team),e.user&&t.set("user",e.user);const n=t.toString();return F(`/inbox${n?`?${n}`:""}`)},resolveFinding:(e,t)=>F(`/findings/${encodeURIComponent(e)}/resolve`,{method:"POST",body:JSON.stringify({resolved_by:t})}),backlinks:(e,t=50)=>F(`/backlinks?context_id=${encodeURIComponent(e)}&limit=${encodeURIComponent(String(t))}`),setIntegration:e=>F("/integrations",{method:"POST",body:JSON.stringify(e)}),teamJoin:e=>F("/team-join",{method:"POST",body:JSON.stringify(e)}),localContextAddFolder:e=>F("/local-context/folders",{method:"POST",body:JSON.stringify(e)}),localContextShareFolder:e=>F("/local-context/folders/share",{method:"POST",body:JSON.stringify(e)}),enterpriseSetWorkspace:e=>F("/workspace",{method:"POST",body:JSON.stringify(e)}),enterpriseResetWorkspace:()=>F("/workspace/reset",{method:"POST",body:"{}"}),enterpriseCreateProject:e=>F("/projects",{method:"POST",body:JSON.stringify(e)}),enterpriseDeleteProject:e=>F(`/projects/${encodeURIComponent(e)}`,{method:"DELETE"}),enterpriseCreateProjectTeam:(e,t)=>F(`/projects/${encodeURIComponent(e)}/teams`,{method:"POST",body:JSON.stringify(t)}),enterpriseAddProjectFolder:(e,t)=>F(`/projects/${encodeURIComponent(e)}/folders`,{method:"POST",body:JSON.stringify(t)}),enterpriseAddTeamFolder:(e,t)=>F(`/teams/${encodeURIComponent(e)}/folders`,{method:"POST",body:JSON.stringify(t)}),enterpriseRemoveFolder:e=>F(`/folders/${encodeURIComponent(e)}`,{method:"DELETE"}),enterpriseAddTeamCollaborator:(e,t)=>F(`/teams/${encodeURIComponent(e)}/collaborators`,{method:"POST",body:JSON.stringify({target_team_id:t})}),enterpriseExtractProject:e=>F(`/projects/${encodeURIComponent(e)}/extract`,{method:"POST",body:"{}"}),enterpriseExtractTeam:e=>F(`/teams/${encodeURIComponent(e)}/extract`,{method:"POST",body:"{}"}),pickFolderPath:e=>F("/folders/pick",{method:"POST",body:JSON.stringify({prompt:e})})};function Zh({view:e,setView:t,conflictCount:n}){const r=[{id:"command",icon:"⌂",label:"HOME",tip:"Command center · current truth · next action"},{id:"router",icon:"⇌",label:"FIREWALL",tip:"Context firewall · routing · expansions · tokens saved"},{id:"canvas",icon:"⊞",label:"BRAIN",tip:"Repo Brain · linked folders · active sessions"},{id:"handoff",icon:"↗",label:"HANDOFF",tip:"Resume state across agents"},{id:"replay",icon:"◌",label:"REPLAY",tip:"Context decision replay"},{id:"learnings",icon:"↑",label:"LEARN",tip:"Evidence-backed learning review"},{id:"context",icon:"◐",label:"CONTEXT",tip:"Context vault · personal and shared memory"},{id:"portability",icon:"□",label:"PACKS",tip:".dheemem export · import dry-run · portability"},{id:"conflicts",icon:"⟷",label:"INBOX",tip:"Proposals · findings · conflicts",badge:n}];return i.jsxs("div",{style:{width:"var(--nav)",borderRight:"1px solid var(--border)",display:"flex",flexDirection:"column",flexShrink:0,background:"var(--bg)",zIndex:20},children:[i.jsx("div",{style:{height:48,borderBottom:"1px solid var(--border)",display:"flex",alignItems:"center",justifyContent:"center"},children:i.jsx("img",{src:"/dhee-logo.png",alt:"Dhee",style:{width:22,height:22,objectFit:"contain"}})}),i.jsx("div",{style:{flex:1,display:"flex",flexDirection:"column",padding:"6px 0",gap:0},children:r.map(o=>{const l=o.id==="router"?e==="router"||e.startsWith("router/"):e===o.id;return i.jsxs("div",{title:o.tip,onClick:()=>t(o.id),style:{position:"relative",height:44,display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",cursor:"pointer",background:l?"var(--surface)":"transparent",borderLeft:`2px solid ${l?"var(--accent)":"transparent"}`,gap:2,transition:"all 0.1s"},onMouseEnter:s=>{l||(s.currentTarget.style.background="var(--surface)")},onMouseLeave:s=>{l||(s.currentTarget.style.background="transparent")},children:[i.jsx("span",{style:{fontSize:14,color:l?"var(--accent)":"var(--ink3)",lineHeight:1},children:o.icon}),i.jsx("span",{style:{fontFamily:"var(--mono)",fontSize:7,color:l?"var(--accent)":"var(--ink3)",letterSpacing:"0.04em"},children:o.label}),o.badge&&o.badge>0?i.jsx("div",{style:{position:"absolute",top:6,right:6,width:14,height:14,borderRadius:"50%",background:"var(--rose)",display:"flex",alignItems:"center",justifyContent:"center"},children:i.jsx("span",{style:{fontFamily:"var(--mono)",fontSize:8,color:"white",fontWeight:700},children:o.badge})}):null]},o.id)})}),i.jsx("div",{style:{borderTop:"1px solid var(--border)",height:44,display:"flex",alignItems:"center",justifyContent:"center"},children:i.jsx("div",{title:"Dhee active",style:{width:5,height:5,borderRadius:"50%",background:"var(--green)"}})})]})}function Lr(e){return!e||e<=0?"0":new Intl.NumberFormat("en",{notation:"compact",maximumFractionDigits:1}).format(e)}function Gh(e){if(!e)return 0;const t=Number(e.sessionTokensSaved||0),n=Number(e.enterpriseSavedTokens||0);return t+n}function eg(e){return e?Number(e.enterpriseSavedPct||0):0}function tg({viewer:e,routerStats:t,onRefresh:n,onOpenTweaks:r,onResetWorkspace:o}){const[l,s]=v.useState(!1),[a,d]=v.useState(null),c=v.useRef(null);v.useEffect(()=>{if(!l)return;const u=f=>{c.current&&(c.current.contains(f.target)||s(!1))};return window.addEventListener("mousedown",u),()=>window.removeEventListener("mousedown",u)},[l]),v.useEffect(()=>{if(t){d(null);return}let u=!1;const f=async()=>{try{const j=await B.routerStats();u||d(j)}catch{}};f();const p=window.setInterval(f,5e3);return()=>{u=!0,window.clearInterval(p)}},[t]);const g=(e==null?void 0:e.org_id)||"default",m=(e==null?void 0:e.project_id)||null,x=(e==null?void 0:e.team_id)||null,S=[g,m,x].filter(Boolean).join(" · "),E=t||a,T=Gh(E),H=eg(E),h=(()=>{if(!E)return"loading";const u=Number(E.sessionTokensSaved||0),f=Number(E.enterpriseSavedTokens||0),p=Number(E.enterpriseRawTokens||0),j=Number(E.enterpriseSummaryTokens||0),_=Number(E.enterpriseRawFallbacks||0),P=Number(E.enterpriseGateSuggestions||0);return`Session: ${Lr(u)} · Repo index: ${Lr(f)} · Raw avoided: ${Lr(p)} -> ${Lr(j)} · Fallbacks: ${_} · Gates: ${P}`})();return i.jsxs("div",{style:{height:32,borderBottom:"1px solid var(--border)",background:"var(--bg)",display:"flex",alignItems:"center",padding:"0 12px",gap:10,flexShrink:0,zIndex:15},children:[i.jsxs("div",{className:"workspace-pill",title:S,style:{display:"inline-flex",alignItems:"center",gap:6,padding:"3px 9px",borderRadius:4,background:"var(--surface)",border:"1px solid var(--border)",fontFamily:"var(--mono)",fontSize:10,color:"var(--ink2)",letterSpacing:"0.04em"},children:[i.jsx("span",{style:{width:5,height:5,borderRadius:"50%",background:e!=null&&e.live?"var(--green)":"var(--ink3)"}}),i.jsx("span",{children:S||"no workspace"}),e!=null&&e.role?i.jsx("span",{style:{marginLeft:6,padding:"1px 5px",borderRadius:3,background:"var(--surface2)",color:"var(--ink2)",fontSize:9},children:String(e.role).toUpperCase()}):null]}),i.jsx("div",{style:{flex:1}}),i.jsxs("div",{className:"tokens-chip",title:h,style:{display:"inline-flex",alignItems:"center",gap:6,padding:"3px 9px",borderRadius:4,background:"var(--accent-dim)",border:"1px solid var(--accent)",color:"var(--accent)",fontFamily:"var(--mono)",fontSize:10,letterSpacing:"0.04em"},children:[i.jsx("span",{style:{fontSize:11},children:"↯"}),i.jsxs("span",{children:[Lr(T)," saved"]}),H>0?i.jsxs("span",{style:{color:"var(--ink3)"},children:["· ",H.toFixed(0),"%"]}):null]}),i.jsxs("div",{ref:c,style:{position:"relative",display:"inline-block"},children:[i.jsx("button",{"aria-label":"Menu",onClick:()=>s(u=>!u),style:{width:22,height:22,borderRadius:4,background:l?"var(--surface2)":"var(--surface)",border:"1px solid var(--border)",color:"var(--ink2)",fontSize:12,lineHeight:1,display:"inline-flex",alignItems:"center",justifyContent:"center"},children:"⋮"}),l?i.jsxs("div",{style:{position:"absolute",top:"calc(100% + 4px)",right:0,minWidth:180,background:"var(--bg)",border:"1px solid var(--border)",borderRadius:4,boxShadow:"0 6px 18px rgba(20,16,10,0.08)",zIndex:30,padding:4,fontFamily:"var(--mono)",fontSize:10,letterSpacing:"0.04em"},children:[i.jsx(Hi,{label:"REFRESH",onClick:()=>{s(!1),n()}}),i.jsx(Hi,{label:"TWEAKS",hint:"⌘K",onClick:()=>{s(!1),r()}}),o?i.jsxs(i.Fragment,{children:[i.jsx("div",{style:{height:1,background:"var(--border)",margin:"3px 0"}}),i.jsx(Hi,{label:"RESET WORKSPACE",onClick:()=>{s(!1),o()},danger:!0})]}):null,i.jsx("div",{style:{height:1,background:"var(--border)",margin:"3px 0"}}),i.jsx(Hi,{label:"USER ID",hint:(e==null?void 0:e.user_id)||"—",onClick:()=>s(!1),dim:!0})]}):null]})]})}function Hi({label:e,hint:t,onClick:n,dim:r,danger:o}){return i.jsxs("button",{onClick:n,style:{width:"100%",textAlign:"left",padding:"5px 8px",borderRadius:3,background:"transparent",color:o?"var(--rose)":r?"var(--ink3)":"var(--ink2)",display:"flex",justifyContent:"space-between",alignItems:"center",gap:8},onMouseEnter:l=>{l.currentTarget.style.background=o?"var(--rose-dim)":"var(--surface)"},onMouseLeave:l=>{l.currentTarget.style.background="transparent"},children:[i.jsx("span",{children:e}),t?i.jsx("span",{style:{color:"var(--ink3)",fontSize:9},children:t}):null]})}function ng({tweaks:e,setTweaks:t,visible:n}){if(!n)return null;const r=(o,l)=>{const s={...e,[o]:l};t(s)};return i.jsxs("div",{style:{position:"fixed",bottom:20,right:20,width:236,border:"1px solid var(--border)",background:"white",zIndex:1e3,boxShadow:"0 8px 32px rgba(0,0,0,0.1)"},children:[i.jsx("div",{style:{padding:"9px 14px",borderBottom:"1px solid var(--border)",fontFamily:"var(--mono)",fontSize:10,fontWeight:700,letterSpacing:"0.06em"},children:"TWEAKS"}),i.jsxs("div",{style:{padding:"14px"},children:[i.jsxs("div",{style:{marginBottom:14},children:[i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",marginBottom:6,textTransform:"uppercase"},children:"Accent hue"}),i.jsx("input",{type:"range",min:"0",max:"360",value:e.accentHue,onChange:o=>{const l=o.target.value;r("accentHue",l),document.documentElement.style.setProperty("--accent",`oklch(0.64 0.18 ${l})`),document.documentElement.style.setProperty("--accent-dim",`oklch(0.97 0.04 ${l})`)},style:{width:"100%"}}),i.jsxs("div",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",marginTop:2},children:["hue ",e.accentHue,"°"]})]}),i.jsxs("div",{style:{marginBottom:14},children:[i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",marginBottom:6,textTransform:"uppercase"},children:"Compact nav"}),i.jsx("button",{onClick:()=>r("compactNav",!e.compactNav),style:{padding:"4px 10px",border:"1px solid var(--border)",fontFamily:"var(--mono)",fontSize:10,background:e.compactNav?"var(--ink)":"transparent",color:e.compactNav?"var(--bg)":"var(--ink)",cursor:"pointer"},children:e.compactNav?"ON":"OFF"})]}),i.jsxs("div",{style:{marginBottom:14},children:[i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",marginBottom:6,textTransform:"uppercase"},children:"Canvas style"}),i.jsx("div",{style:{display:"flex",gap:5},children:["dots","grid"].map(o=>i.jsx("button",{onClick:()=>r("canvasStyle",o),style:{padding:"4px 10px",border:"1px solid var(--border)",fontFamily:"var(--mono)",fontSize:9,background:e.canvasStyle===o?"var(--ink)":"transparent",color:e.canvasStyle===o?"var(--bg)":"var(--ink)",cursor:"pointer"},children:o},o))})]}),i.jsxs("div",{children:[i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",marginBottom:6,textTransform:"uppercase"},children:"Timestamps"}),i.jsx("button",{onClick:()=>r("showTimestamps",!e.showTimestamps),style:{padding:"4px 10px",border:"1px solid var(--border)",fontFamily:"var(--mono)",fontSize:10,background:e.showTimestamps?"var(--ink)":"transparent",color:e.showTimestamps?"var(--bg)":"var(--ink)",cursor:"pointer"},children:e.showTimestamps?"ON":"OFF"})]})]})]})}const rg={position:"fixed",inset:0,background:"rgba(20,16,10,0.28)",backdropFilter:"blur(4px)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:60},ig={width:640,maxWidth:"calc(100vw - 32px)",maxHeight:"calc(100vh - 60px)",background:"white",border:"1px solid var(--border)",borderRadius:10,boxShadow:"0 20px 60px rgba(20,16,10,0.20)",display:"flex",flexDirection:"column",overflow:"hidden"},pt={width:"100%",border:"1px solid var(--border)",padding:"9px 11px",background:"var(--bg)",fontSize:13,lineHeight:1.4},Nn={fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",letterSpacing:.5,textTransform:"uppercase",marginBottom:4,display:"block"},Vi={padding:"8px 14px",border:"1px solid var(--ink)",background:"var(--ink)",color:"white",fontFamily:"var(--mono)",fontSize:10,letterSpacing:.4,cursor:"pointer"},Wr={padding:"8px 14px",border:"1px solid var(--border)",background:"white",color:"var(--ink2)",fontFamily:"var(--mono)",fontSize:10,letterSpacing:.4,cursor:"pointer"},ju={padding:"8px 14px",border:"1px solid var(--rose)",background:"white",color:"var(--rose)",fontFamily:"var(--mono)",fontSize:10,letterSpacing:.4,cursor:"pointer"},wu=["codex","claude-code"];function og({open:e,onClose:t,projectIndex:n,initialWorkspaceId:r,initialTab:o="workspaces",onChanged:l}){var he,_e;const s=(n==null?void 0:n.workspaces)||[],[a,d]=v.useState(o),[c,g]=v.useState(r||((he=s[0])==null?void 0:he.id)||""),m=s.find(N=>N.id===c)||null,[x,S]=v.useState(""),[E,T]=v.useState(""),[H,h]=v.useState(""),[u,f]=v.useState(""),[p,j]=v.useState(""),[_,P]=v.useState(""),[z,Q]=v.useState(""),[R,J]=v.useState("codex"),[X,K]=v.useState([]),[G,M]=v.useState({}),[V,ye]=v.useState(!1),[C,A]=v.useState(null),[W,ee]=v.useState(null);if(v.useEffect(()=>{var N;e&&(d(o),g(r||((N=s[0])==null?void 0:N.id)||""),A(null),ee(null),S(""),T(""),P(""),Q(""),J("codex"),K([]),j(""))},[e]),v.useEffect(()=>{if(!m){h(""),f("");return}h(String(m.label||m.name||"")),f(String(m.description||"")),j("")},[m==null?void 0:m.id,m==null?void 0:m.label,m==null?void 0:m.description]),v.useEffect(()=>{m&&M(N=>{const oe={};for(const q of m.projects||[]){const te=(q.scopeRules||[]).map(Ae=>Ae.pathPrefix);oe[q.id]=N[q.id]||{name:q.name,description:q.description||"",defaultRuntime:q.defaultRuntime||"codex",folders:te}}return oe})},[m==null?void 0:m.id,(_e=m==null?void 0:m.projects)==null?void 0:_e.length]),!e)return null;const ie=async(N,oe)=>{A(null);try{const q=await B.pickFolder(oe||"Choose a folder");q.ok&&q.path&&N(q.path)}catch(q){A(String(q))}},D=async(N,oe)=>{ye(!0),A(null),ee(null);try{await oe(),ee(N),await l()}catch(q){A(String(q))}finally{ye(!1)}},fe=()=>D("Workspace created.",async()=>{const N=x.trim();if(!N)throw new Error("Name is required.");await B.createWorkspaceRoot(N,E.trim()||void 0),S(""),T("")}),se=()=>D("Workspace updated.",async()=>{if(!m)return;const N={};if(H.trim()&&H!==(m.label||m.name)&&(N.label=H.trim()),u!==(m.description||"")&&(N.description=u),!Object.keys(N).length)throw new Error("Nothing to save.");await B.updateWorkspace(m.id,N)}),me=()=>D("Workspace deleted.",async()=>{if(m){if(p.trim()!==(m.label||m.name))throw new Error("Type the workspace name exactly to confirm.");await B.deleteWorkspace(m.id),g(""),j("")}}),$=()=>D("Project created.",async()=>{if(!m)throw new Error("Pick a workspace first.");const N=_.trim();if(!N)throw new Error("Project name is required.");const oe=X.map(q=>q.trim()).filter(Boolean).map(q=>({path_prefix:q}));await B.createProject(m.id,{name:N,description:z.trim()||void 0,default_runtime:R,scope_rules:oe}),P(""),Q(""),J("codex"),K([])}),de=N=>()=>D("Project updated.",async()=>{const oe=G[N.id];if(!oe)return;const q={};oe.name.trim()&&oe.name.trim()!==N.name&&(q.name=oe.name.trim()),oe.description!==(N.description||"")&&(q.description=oe.description),oe.defaultRuntime&&oe.defaultRuntime!==(N.defaultRuntime||"codex")&&(q.default_runtime=oe.defaultRuntime);const te=(N.scopeRules||[]).map(xe=>xe.pathPrefix),Ae=oe.folders.map(xe=>xe.trim()).filter(Boolean);if((te.length!==Ae.length||te.some((xe,Me)=>xe!==Ae[Me]))&&(q.scope_rules=Ae.map(xe=>({path_prefix:xe}))),!Object.keys(q).length)throw new Error("Nothing to save.");await B.updateProject(N.id,q)}),ce=N=>()=>D("Project deleted.",async()=>{if(!window.confirm(`Delete project "${N.name}"? This removes its assets. The workspace stays.`))throw new Error("cancelled");await B.deleteProject(N.id)});return i.jsx("div",{style:rg,onClick:t,children:i.jsxs("div",{style:ig,onClick:N=>N.stopPropagation(),children:[i.jsxs("div",{style:{padding:"14px 20px",borderBottom:"1px solid var(--border)",display:"flex",alignItems:"center",justifyContent:"space-between"},children:[i.jsxs("div",{children:[i.jsx("div",{style:{fontSize:15,fontWeight:700},children:"Workspaces & projects"}),i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",marginTop:2},children:"Organise every agent under a shared brain."})]}),i.jsx("button",{onClick:t,style:{width:28,height:28,border:"1px solid var(--border)",borderRadius:4,background:"white",cursor:"pointer",display:"flex",alignItems:"center",justifyContent:"center",color:"var(--ink3)"},children:i.jsx("svg",{width:12,height:12,viewBox:"0 0 24 24",fill:"none",children:i.jsx("path",{d:"M6 6l12 12M18 6L6 18",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round"})})})]}),i.jsx("div",{style:{padding:"10px 20px",borderBottom:"1px solid var(--border)",display:"flex",gap:6},children:["workspaces","projects"].map(N=>i.jsx("button",{onClick:()=>d(N),style:{padding:"6px 12px",border:`1px solid ${a===N?"var(--ink)":"var(--border)"}`,background:a===N?"var(--ink)":"white",color:a===N?"white":"var(--ink2)",fontFamily:"var(--mono)",fontSize:10,letterSpacing:.4,textTransform:"uppercase",cursor:"pointer"},children:N},N))}),i.jsxs("div",{style:{padding:20,overflowY:"auto",display:"flex",flexDirection:"column",gap:16},children:[a==="workspaces"&&i.jsxs(i.Fragment,{children:[i.jsxs("section",{style:{display:"flex",flexDirection:"column",gap:8},children:[i.jsx("div",{style:Nn,children:"New workspace"}),i.jsx("input",{placeholder:"Name (e.g. Office, Personal, Sankhya AI Labs)",value:x,onChange:N=>S(N.target.value),style:pt}),i.jsx("textarea",{placeholder:"Description (optional)",value:E,onChange:N=>T(N.target.value),rows:2,style:{...pt,resize:"vertical"}}),i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",lineHeight:1.5},children:"A workspace is a collection of projects. Folders attach to projects, not workspaces."}),i.jsx("div",{style:{display:"flex",justifyContent:"flex-end"},children:i.jsx("button",{onClick:()=>void fe(),disabled:V||!x.trim(),style:{...Vi,opacity:V||!x.trim()?.5:1},children:"create workspace"})})]}),i.jsxs("section",{style:{display:"flex",flexDirection:"column",gap:8},children:[i.jsxs("div",{style:Nn,children:["Existing · ",s.length]}),s.length===0?i.jsx("div",{style:{padding:14,border:"1px dashed var(--border)",fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)",lineHeight:1.55},children:"No workspaces yet. Create one above."}):i.jsx("div",{style:{display:"grid",gap:6},children:s.map(N=>{var te;const oe=N.id===c,q=((te=N.projects)==null?void 0:te.length)||0;return i.jsxs("button",{onClick:()=>g(N.id),style:{textAlign:"left",padding:"10px 12px",border:`1px solid ${oe?"var(--accent)":"var(--border)"}`,background:oe?"var(--surface)":"white",display:"flex",justifyContent:"space-between",gap:12,cursor:"pointer"},children:[i.jsxs("div",{style:{minWidth:0},children:[i.jsx("div",{style:{fontSize:13,fontWeight:600,lineHeight:1.3},children:N.label||N.name}),N.description&&i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",marginTop:2},children:N.description})]}),i.jsxs("span",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)"},children:[q," project",q===1?"":"s"]})]},N.id)})})]}),m&&i.jsxs("section",{style:{display:"flex",flexDirection:"column",gap:8,borderTop:"1px solid var(--border)",paddingTop:14},children:[i.jsxs("div",{style:Nn,children:["Edit · ",m.label||m.name]}),i.jsx("input",{value:H,onChange:N=>h(N.target.value),placeholder:"Name",style:pt}),i.jsx("textarea",{value:u,onChange:N=>f(N.target.value),placeholder:"Description",rows:2,style:{...pt,resize:"vertical"}}),i.jsx("div",{style:{display:"flex",justifyContent:"flex-end",gap:8},children:i.jsx("button",{onClick:()=>void se(),disabled:V,style:{...Vi,opacity:V?.5:1},children:"save changes"})}),i.jsxs("div",{style:{marginTop:8,padding:12,border:"1px solid rgba(203,63,78,0.3)",borderRadius:4,background:"rgba(203,63,78,0.04)"},children:[i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--rose)",letterSpacing:.5,textTransform:"uppercase",marginBottom:6},children:"Danger zone"}),i.jsxs("div",{style:{fontSize:11,color:"var(--ink2)",lineHeight:1.5,marginBottom:8},children:["Deleting ",i.jsx("strong",{children:m.label||m.name})," removes every project, asset, and line message. Sessions remain but detach. Type the workspace name to confirm."]}),i.jsxs("div",{style:{display:"flex",gap:8},children:[i.jsx("input",{value:p,onChange:N=>j(N.target.value),placeholder:m.label||m.name,style:{...pt,flex:1}}),i.jsx("button",{onClick:()=>void me(),disabled:V||p.trim()!==(m.label||m.name),style:{...ju,opacity:V||p.trim()!==(m.label||m.name)?.5:1},children:"delete workspace"})]})]})]})]}),a==="projects"&&i.jsxs(i.Fragment,{children:[i.jsxs("section",{style:{display:"flex",flexDirection:"column",gap:8},children:[i.jsx("div",{style:Nn,children:"Workspace"}),i.jsxs("select",{value:c,onChange:N=>g(N.target.value),style:pt,children:[i.jsx("option",{value:"",children:"— pick a workspace —"}),s.map(N=>i.jsx("option",{value:N.id,children:N.label||N.name},N.id))]})]}),m&&i.jsxs(i.Fragment,{children:[i.jsxs("section",{style:{display:"flex",flexDirection:"column",gap:8},children:[i.jsxs("div",{style:Nn,children:["Add project to ",m.label||m.name]}),i.jsx("input",{placeholder:"Project name (e.g. frontend, backend, design)",value:_,onChange:N=>P(N.target.value),style:pt}),i.jsx("textarea",{placeholder:"Description (optional)",value:z,onChange:N=>Q(N.target.value),rows:2,style:{...pt,resize:"vertical"}}),i.jsxs("div",{style:{display:"flex",gap:8,alignItems:"center"},children:[i.jsx("span",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)"},children:"default runtime"}),i.jsx("select",{value:R,onChange:N=>J(N.target.value),style:{...pt,flex:1},children:wu.map(N=>i.jsx("option",{value:N,children:N},N))})]}),i.jsx(Cu,{folders:X,onChange:K,onPick:N=>void ie(N,"Choose a project folder"),disabled:V}),i.jsx("div",{style:{display:"flex",justifyContent:"flex-end"},children:i.jsx("button",{onClick:()=>void $(),disabled:V||!_.trim(),style:{...Vi,opacity:V||!_.trim()?.5:1},children:"add project"})})]}),i.jsxs("section",{style:{display:"flex",flexDirection:"column",gap:8},children:[i.jsxs("div",{style:Nn,children:["Projects · ",(m.projects||[]).length]}),(m.projects||[]).length===0?i.jsx("div",{style:{padding:14,border:"1px dashed var(--border)",fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)",lineHeight:1.55},children:"No projects yet — add the first one above."}):i.jsx("div",{style:{display:"grid",gap:10},children:(m.projects||[]).map(N=>{const oe=G[N.id]||{name:N.name,description:N.description||"",defaultRuntime:N.defaultRuntime||"codex",folders:(N.scopeRules||[]).map(te=>te.pathPrefix)},q=te=>M(Ae=>({...Ae,[N.id]:{...oe,...te}}));return i.jsxs("div",{style:{border:"1px solid var(--border)",borderRadius:6,padding:10,display:"flex",flexDirection:"column",gap:8},children:[i.jsx("input",{value:oe.name,onChange:te=>q({name:te.target.value}),style:pt}),i.jsx("textarea",{value:oe.description,onChange:te=>q({description:te.target.value}),rows:2,style:{...pt,resize:"vertical"}}),i.jsxs("div",{style:{display:"flex",gap:8,alignItems:"center"},children:[i.jsx("span",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)"},children:"runtime"}),i.jsx("select",{value:oe.defaultRuntime,onChange:te=>q({defaultRuntime:te.target.value}),style:{...pt,flex:1},children:wu.map(te=>i.jsx("option",{value:te,children:te},te))})]}),i.jsx(Cu,{folders:oe.folders,onChange:te=>q({folders:te}),onPick:te=>void ie(te,`Add a folder to ${N.name}`),disabled:V}),i.jsxs("div",{style:{display:"flex",justifyContent:"space-between",gap:8},children:[i.jsx("button",{onClick:()=>void ce(N)(),disabled:V,style:{...ju,opacity:V?.5:1},children:"delete"}),i.jsx("button",{onClick:()=>void de(N)(),disabled:V,style:{...Vi,opacity:V?.5:1},children:"save"})]})]},N.id)})})]})]})]})]}),i.jsxs("div",{style:{padding:"10px 20px",borderTop:"1px solid var(--border)",background:"var(--bg)",display:"flex",alignItems:"center",justifyContent:"space-between",minHeight:44},children:[i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:10,lineHeight:1.4},children:C?i.jsx("span",{style:{color:"var(--rose)"},children:C}):W?i.jsx("span",{style:{color:"var(--green)"},children:W}):i.jsx("span",{style:{color:"var(--ink3)"},children:"Changes save immediately."})}),i.jsx("button",{onClick:t,style:Wr,children:"close"})]})]})})}function Cu({folders:e,onChange:t,onPick:n,disabled:r}){const o=(d,c)=>{const g=e.slice();g[d]=c,t(g)},l=d=>{const c=e.slice();c.splice(d,1),t(c)},s=()=>t([...e,""]),a=()=>n(d=>t([...e,d]));return i.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:6},children:[i.jsxs("div",{style:Nn,children:["Folders · ",e.length]}),e.length===0&&i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",lineHeight:1.5},children:"No folders yet. A project can have one or many."}),e.map((d,c)=>i.jsxs("div",{style:{display:"flex",gap:6},children:[i.jsx("input",{value:d,onChange:g=>o(c,g.target.value),placeholder:"/absolute/path",style:{...pt,flex:1}}),i.jsx("button",{onClick:()=>n(g=>o(c,g)),style:Wr,disabled:r,children:"browse…"}),i.jsx("button",{onClick:()=>l(c),style:Wr,disabled:r,title:"Remove folder",children:"✕"})]},c)),i.jsxs("div",{style:{display:"flex",gap:6},children:[i.jsx("button",{onClick:a,style:Wr,disabled:r,children:"+ pick folder"}),i.jsx("button",{onClick:s,style:Wr,disabled:r,children:"+ type path"})]})]})}function $e({label:e,tone:t="var(--ink3)"}){return i.jsx("span",{style:{display:"inline-flex",alignItems:"center",gap:5,padding:"2px 7px",border:`1px solid ${t}`,color:t,fontFamily:"var(--mono)",fontSize:9,lineHeight:1.2,whiteSpace:"nowrap"},children:e})}const bu=6e4,_u=36e5,Eu=864e5;function ap(e){if(!e)return"";const t=Date.parse(e);if(Number.isNaN(t))return String(e);const n=Date.now()-t;return nvoid t(e),disabled:n,title:"Remove asset","aria-label":"Remove asset",style:{width:22,height:22,display:"flex",alignItems:"center",justifyContent:"center",border:"1px solid transparent",borderRadius:3,background:"transparent",color:"var(--ink3)",cursor:n?"not-allowed":"pointer",opacity:n?.5:1,padding:0,flexShrink:0},onMouseEnter:a=>{n||(a.currentTarget.style.background="rgba(203,63,78,0.08)",a.currentTarget.style.color="var(--rose)")},onMouseLeave:a=>{a.currentTarget.style.background="transparent",a.currentTarget.style.color="var(--ink3)"},children:i.jsx("svg",{width:12,height:12,viewBox:"0 0 24 24",fill:"none",children:i.jsx("path",{d:"M6 6l12 12M18 6L6 18",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round"})})})]}),i.jsxs("div",{style:{display:"flex",gap:6,flexWrap:"wrap",alignItems:"center"},children:[s.size>0?i.jsx($e,{label:`${l.length} processed`,tone:"var(--green)"}):i.jsx($e,{label:"not yet processed"}),Array.from(s).slice(0,3).map(a=>i.jsx($e,{label:a,tone:Rs(a)},a))]}),l.length>0&&i.jsxs(i.Fragment,{children:[i.jsx("button",{onClick:()=>o(a=>!a),style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",background:"transparent",border:0,padding:0,textAlign:"left",cursor:"pointer",letterSpacing:.4},children:r?"▾ hide processing feed":`▸ show processing feed (${l.length})`}),r&&i.jsx("div",{style:{display:"flex",flexDirection:"column",borderTop:"1px dashed var(--border)",paddingTop:6},children:l.slice(0,8).map(a=>i.jsx(dg,{result:a},a.id))})]})]})}function cg({workspace:e,project:t,onActivity:n}){const[r,o]=v.useState([]),[l,s]=v.useState(!1),[a,d]=v.useState(null),[c,g]=v.useState("idle"),[m,x]=v.useState(""),[S,E]=v.useState(!1),[T,H]=v.useState(null),h=v.useRef(null),u=v.useMemo(()=>t?t.name:e?`${e.label||e.name} (workspace)`:"—",[t,e]),f=v.useCallback(async()=>{if(!e){o([]);return}s(!0),d(null);try{const R=t?await B.listProjectAssets(t.id):await B.listWorkspaceAssets(e.id,!1);o(R.assets||[])}catch(R){d(String(R))}finally{s(!1)}},[t,e]);v.useEffect(()=>{f()},[f]),v.useEffect(()=>{if(!e)return;const R=window.setInterval(()=>void f(),5e3);return()=>window.clearInterval(R)},[f,e]);const p=v.useCallback(async R=>{if(!e)return;const J=Array.from(R);if(J.length!==0){g("uploading"),x(`uploading ${J.length} file${J.length===1?"":"s"}…`),d(null);try{for(const X of J)t?await B.uploadProjectAsset(t.id,X):await B.uploadWorkspaceAsset(e.id,X);g("success"),x(J.length===1?`uploaded ${J[0].name}`:`uploaded ${J.length} files`),await f(),n==null||n()}catch(X){g("error"),x(String(X))}finally{window.setTimeout(()=>{g("idle"),x("")},2200)}}},[t,e,f,n]),j=R=>{var X;R.preventDefault(),R.stopPropagation(),E(!1);const J=R.dataTransfer;(X=J==null?void 0:J.files)!=null&&X.length&&p(J.files)},_=R=>{var J;e&&(R.preventDefault(),R.stopPropagation(),(J=R.dataTransfer.types)!=null&&J.includes("Files")&&!S&&E(!0))},P=R=>{R.currentTarget===R.target&&E(!1)},z=R=>{const J=R.target.files;J!=null&&J.length&&p(J),R.target.value=""},Q=async R=>{if(window.confirm(`Remove "${R.name}"?`)){H(R.id);try{await B.deleteProjectAsset(R.id),o(J=>J.filter(X=>X.id!==R.id)),n==null||n()}catch(J){d(String(J))}finally{H(null)}}};return i.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:10},onDrop:j,onDragOver:_,onDragLeave:P,children:[i.jsxs("div",{style:{display:"flex",alignItems:"center",justifyContent:"space-between",fontFamily:"var(--mono)",fontSize:9,letterSpacing:.5,color:"var(--ink3)",textTransform:"uppercase"},children:[i.jsxs("span",{children:["Assets · ",u]}),i.jsx("span",{children:r.length})]}),i.jsxs("div",{onClick:()=>{var R;return e&&((R=h.current)==null?void 0:R.click())},style:{padding:"16px 14px",border:`1px dashed ${S?"var(--accent)":"var(--border)"}`,background:S?"rgba(224,107,63,0.06)":"white",borderRadius:6,textAlign:"center",cursor:e?"pointer":"not-allowed",opacity:e?1:.55,transition:"background 0.18s ease, border-color 0.18s ease"},children:[i.jsx("input",{ref:h,type:"file",multiple:!0,style:{display:"none"},onChange:z}),i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:11,color:S?"var(--accent)":"var(--ink2)",fontWeight:500,marginBottom:4},children:c==="uploading"?m:S?"release to upload":"drop files here or click to upload"}),i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",letterSpacing:.4},children:t?"visible to every agent working on this project":e?"workspace-wide — every project sees it":"select a workspace first"})]}),c==="success"&&i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--green)",lineHeight:1.4},children:m}),c==="error"&&i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--rose)",lineHeight:1.4},children:m}),a&&c!=="error"?i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--rose)",lineHeight:1.4},children:a}):null,l&&r.length===0?i.jsx("div",{style:{display:"grid",gap:8},children:[0,1,2].map(R=>i.jsx("div",{style:{height:66,borderRadius:6,background:"linear-gradient(90deg, rgba(20,16,10,0.04) 0%, rgba(20,16,10,0.08) 50%, rgba(20,16,10,0.04) 100%)",backgroundSize:"200% 100%",animation:`dhee-shimmer 1.4s linear ${R*140}ms infinite`,border:"1px solid rgba(20,16,10,0.06)"}},R))}):r.length===0?i.jsxs("div",{style:{padding:14,border:"1px dashed var(--border)",borderRadius:6,fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)",lineHeight:1.55,background:"white"},children:["No assets yet. Drop a spec PDF, design export, or schema doc here — every agent in this",t?" project":" workspace"," will see it."]}):i.jsx("div",{style:{display:"grid",gap:8},children:r.map(R=>i.jsx(ug,{asset:R,onDelete:Q,busyDelete:T===R.id},R.id))})]})}const Tu=6e4,zu=36e5,Ru=864e5;function dp(e){if(!e)return"";const t=Date.parse(e);if(Number.isNaN(t))return String(e);const n=Date.now()-t;return n{const l=String(o.runtime||"unknown").toLowerCase(),s=n.get(l)||{count:0,latestUpdate:null,isLive:!1};s.count+=1,o.updatedAt&&(!s.latestUpdate||o.updatedAt>s.latestUpdate)&&(s.latestUpdate=o.updatedAt),(o.isCurrent||o.state==="active"||o.state==="recent")&&(s.isLive=!0),n.set(l,s)};for(const o of t||[])r(o);for(const o of e||[])for(const l of o.sessions||[])r(l);return Array.from(n.entries()).map(([o,l])=>({runtime:o,...l})).sort((o,l)=>l.count-o.count)}function gg({workspace:e,projects:t,workspaceSessions:n}){const r=v.useMemo(()=>hg(t,n),[t,n]),o=r.reduce((l,s)=>l+s.count,0);return i.jsxs("div",{style:{border:"1px solid var(--border)",background:"white",padding:14},children:[i.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"baseline",marginBottom:10},children:[i.jsxs("span",{style:{fontFamily:"var(--mono)",fontSize:9,letterSpacing:.6,color:"var(--ink3)",textTransform:"uppercase"},children:["Connected agents · ",o]}),e?i.jsx("span",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)"},children:e.label||e.name}):null]}),r.length===0?i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)",lineHeight:1.55},children:"No agent sessions yet. Launch claude-code or codex in this workspace — they will register here and start publishing to the line."}):i.jsx("div",{style:{display:"grid",gap:6},children:r.map(l=>i.jsxs("div",{style:{display:"flex",alignItems:"center",justifyContent:"space-between",gap:10},children:[i.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8,minWidth:0},children:[i.jsx("span",{style:{width:7,height:7,borderRadius:"50%",background:l.isLive?"var(--green)":"var(--ink3)",boxShadow:l.isLive?"0 0 0 3px rgba(31,169,113,0.18)":"none",flexShrink:0}}),i.jsx("span",{style:{fontFamily:"var(--mono)",fontSize:11,color:up(l.runtime),whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis"},children:l.runtime}),i.jsxs("span",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)"},children:["· ",l.count]})]}),l.latestUpdate?i.jsx("span",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)"},children:dp(l.latestUpdate)}):null]},l.runtime))})]})}function cp({message:e,workspace:t,onOpenTask:n}){var m,x;const r=((m=t==null?void 0:t.projects.find(S=>S.id===e.project_id))==null?void 0:m.name)||"workspace",o=((x=t==null?void 0:t.projects.find(S=>S.id===e.target_project_id))==null?void 0:x.name)||"",l=e.metadata||{},s=String(l.harness||l.runtime||""),a=String(l.tool_name||l.toolName||""),d=String(l.ptr||""),c=e.body||"",g=pg(e.message_kind);return i.jsxs("div",{style:{border:"1px solid var(--border)",borderLeft:`3px solid ${g}`,background:"white",padding:"11px 13px"},children:[i.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"baseline",gap:10,marginBottom:6},children:[i.jsxs("div",{style:{fontFamily:"var(--mono)",fontSize:10,color:up(s),letterSpacing:.4},children:[fg(e.created_at),s?` · ${s}`:"",r!=="workspace"?` · ${r}`:""]}),i.jsx("span",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)"},children:dp(e.created_at)})]}),e.title?i.jsx("div",{style:{fontSize:13,fontWeight:600,marginBottom:5,lineHeight:1.35},children:e.title}):null,c?i.jsx("div",{style:{fontSize:12,color:"var(--ink2)",lineHeight:1.55,whiteSpace:"pre-wrap",wordBreak:"break-word"},children:c}):null,i.jsxs("div",{style:{display:"flex",gap:5,flexWrap:"wrap",marginTop:8},children:[i.jsx($e,{label:mg(e.message_kind),tone:g}),a?i.jsx($e,{label:a.toLowerCase()}):null,o?i.jsx($e,{label:`→ ${o}`,tone:"#4d6cff"}):null,d?i.jsx($e,{label:d}):null,e.task_id&&n?i.jsx("button",{onClick:()=>n(String(e.task_id)),style:{padding:"2px 8px",border:"1px solid var(--ink)",background:"var(--ink)",color:"white",fontFamily:"var(--mono)",fontSize:9,letterSpacing:.4,textTransform:"uppercase",cursor:"pointer"},children:"open task"}):null]})]})}function fp({workspace:e,activeProjectId:t,sessionId:n,taskId:r,onPublished:o}){var P;const[l,s]=v.useState(""),[a,d]=v.useState(""),[c,g]=v.useState(""),[m,x]=v.useState(!1),[S,E]=v.useState(null),[T,H]=v.useState(null),h=v.useRef(null);v.useEffect(()=>{var z;(z=h.current)==null||z.focus()},[e==null?void 0:e.id]);const u=v.useMemo(()=>((e==null?void 0:e.projects)||[]).filter(z=>!t||z.id!==t),[e==null?void 0:e.projects,t]),f=async()=>{var z;if(!(!(e!=null&&e.id)||!a.trim()||m)){x(!0),H(null),E(null);try{const Q=await B.publishWorkspaceLineMessage(e.id,{project_id:t||void 0,target_project_id:c||void 0,channel:t?"project":"workspace",session_id:n||void 0,task_id:r||void 0,message_kind:c?"broadcast":"note",title:l.trim()||void 0,body:a.trim(),metadata:{sourceProject:(z=e.projects.find(R=>R.id===t))==null?void 0:z.name}});if(Q.suggestedTask){const R=e.projects.find(J=>J.id===c);E(R?`Broadcast sent · suggested task created in ${R.name}.`:"Broadcast sent · suggested task created.")}else E("Published to the workspace line.");s(""),d(""),g(""),o==null||o(Q.message,Q.suggestedTask)}catch(Q){H(String(Q))}finally{x(!1)}}};if(!e)return i.jsx("div",{style:{padding:16,border:"1px solid var(--border)",fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)",background:"white"},children:"No workspace selected."});const p=!a.trim()||m,j=(P=u.find(z=>z.id===c))==null?void 0:P.name,_=m?"publishing…":c?`broadcast → ${j||"project"}`:"publish update";return i.jsxs("div",{style:{border:"1px solid var(--border)",background:"white",padding:14,display:"flex",flexDirection:"column",gap:8},children:[i.jsx("input",{value:l,onChange:z=>s(z.target.value),placeholder:"headline (optional) — e.g. user.plan field added",style:{border:"1px solid var(--border)",padding:"9px 11px",background:"var(--bg)",fontSize:13}}),i.jsxs("select",{value:c,onChange:z=>g(z.target.value),style:{border:"1px solid var(--border)",padding:"9px 11px",background:"var(--bg)",fontFamily:"var(--mono)",fontSize:10},children:[i.jsxs("option",{value:"",children:["Publish to current ",t?"project":"workspace"," only"]}),u.map(z=>i.jsxs("option",{value:z.id,children:["Broadcast into ",z.name," (creates task)"]},z.id))]}),i.jsx("textarea",{ref:h,value:a,onChange:z=>d(z.target.value),placeholder:c?"What should the target project's agent know? It will spawn a task with this context.":"Broadcast a dependency change, a tool result, or a follow-up signal to the workspace line…",rows:4,onKeyDown:z=>{(z.metaKey||z.ctrlKey)&&z.key==="Enter"&&(z.preventDefault(),f())},style:{border:"1px solid var(--border)",padding:"10px 12px",background:"var(--bg)",fontSize:13,lineHeight:1.55,resize:"vertical"}}),i.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",gap:10},children:[i.jsx("span",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",letterSpacing:.3},children:"⌘/Ctrl + Enter to publish"}),i.jsx("button",{onClick:()=>void f(),disabled:p,style:{padding:"8px 14px",border:"1px solid var(--ink)",background:p?"var(--ink3)":"var(--ink)",color:"white",fontFamily:"var(--mono)",fontSize:10,letterSpacing:.4,opacity:p?.7:1,cursor:p?"not-allowed":"pointer"},children:_})]}),S?i.jsx("div",{style:{fontSize:11,color:"var(--green)",lineHeight:1.5},children:S}):null,T?i.jsx("div",{style:{fontSize:11,color:"var(--rose)",lineHeight:1.5},children:T}):null]})}function pp(e,t){const[n,r]=v.useState([]),[o,l]=v.useState(!1),[s,a]=v.useState(null),d=g=>{r(m=>{const x=new Map;return[...g,...m].forEach(S=>{S!=null&&S.id&&x.set(S.id,S)}),Array.from(x.values()).sort((S,E)=>String(E.created_at||"").localeCompare(String(S.created_at||"")))})},c=async()=>{if(!e){r([]);return}try{const g=await B.workspaceLineMessages(e,{project_id:t||void 0,limit:100});r(g.messages||[])}catch(g){a(String(g))}};return v.useEffect(()=>{c()},[e,t]),v.useEffect(()=>{if(!e){l(!1);return}const g=new URLSearchParams;t&&g.set("project_id",t);const m=new EventSource(`/api/workspaces/${encodeURIComponent(e)}/line/stream${g.toString()?`?${g.toString()}`:""}`);return m.onopen=()=>l(!0),m.onmessage=x=>{try{const S=JSON.parse(x.data);d([S])}catch{}},m.onerror=()=>{l(!1),m.close()},()=>{m.close(),l(!1)}},[e,t]),{messages:n,live:o,error:s,merge:d,refresh:c}}function vg(e,t){if(!e)return[];const n=new Set(e.projects.map(r=>r.id));return t.filter(r=>{const o=String(r.source||"").toLowerCase();if(o!=="broadcast"&&!o.includes("suggested"))return!1;const l=r.project_id;return!l||n.has(String(l))})}function yg({projectIndex:e,workspaceGraph:t,tasks:n,viewer:r,orgGraph:o,selectedWorkspaceId:l,selectedProjectId:s,onSelectWorkspace:a,onSelectProject:d,onSelectTask:c,onTasksRefresh:g,onOpenCanvas:m,onLaunchSession:x,onOpenManager:S}){var A,W,ee,ie,D,fe,se,me;const E=(e==null?void 0:e.workspaces)||[],[T,H]=v.useState(null),h=v.useMemo(()=>{var he;const $=((he=o==null?void 0:o.raw)==null?void 0:he.context_index)||[],de=(r==null?void 0:r.team_id)||"",ce=(r==null?void 0:r.project_id)||"";return $.filter(_e=>!!(_e.scope==="company"||de&&_e.team_id===de||ce&&_e.project_id===ce||_e.scope==="user"&&_e.user_id===(r==null?void 0:r.user_id)))},[(A=o==null?void 0:o.raw)==null?void 0:A.context_index,r==null?void 0:r.project_id,r==null?void 0:r.team_id,r==null?void 0:r.user_id]),u=v.useMemo(()=>{var ce;const $=((ce=o==null?void 0:o.raw)==null?void 0:ce.pending_proposals)||[],de=(r==null?void 0:r.team_id)||"";return de?$.filter(he=>!he.team_id||he.team_id===de):$},[(W=o==null?void 0:o.raw)==null?void 0:W.pending_proposals,r==null?void 0:r.team_id]),[f,p]=v.useState("all"),j=v.useMemo(()=>E.find($=>$.id===l)||E.find($=>$.id===(e==null?void 0:e.currentWorkspaceId))||E[0]||(t==null?void 0:t.workspace)||null,[E,l,e==null?void 0:e.currentWorkspaceId,t]),_=v.useMemo(()=>j&&(j.projects.find($=>$.id===s)||j.projects.find($=>$.id===(e==null?void 0:e.currentProjectId)))||null,[j,s,e==null?void 0:e.currentProjectId]),P=(j==null?void 0:j.sessions)||[],z=v.useMemo(()=>{var ce,he;const $=e==null?void 0:e.currentSessionId;if(!j)return null;const de=((ce=_==null?void 0:_.sessions)==null?void 0:ce.find(_e=>_e.id===$))||((he=_==null?void 0:_.sessions)==null?void 0:he[0]);return de||P.find(_e=>_e.id===$)||P[0]||null},[j,_,e==null?void 0:e.currentSessionId,P]),{messages:Q,live:R,error:J,refresh:X}=pp(j==null?void 0:j.id,_==null?void 0:_.id);v.useEffect(()=>{let $=!0;return B.continuity().then(de=>{$&&H(de)}).catch(()=>{$&&H(null)}),()=>{$=!1}},[r==null?void 0:r.org_id,r==null?void 0:r.team_id]);const K=v.useMemo(()=>f==="all"?Q:Q.filter($=>{const de=String($.message_kind||"").toLowerCase();return f==="broadcast"?de==="broadcast":f==="tool"?de.startsWith("tool."):f==="note"?de==="note"||de==="update":!0}),[Q,f]),G=v.useMemo(()=>vg(j,n),[j,n]),[M,V]=v.useState(null);v.useEffect(()=>{var de;const $=(de=Q[0])==null?void 0:de.id;$&&$!==M&&V($)},[Q,M]);const ye=$=>({padding:"5px 10px",border:`1px solid ${$?"var(--ink)":"var(--border)"}`,background:$?"var(--ink)":"white",color:$?"white":"var(--ink2)",fontFamily:"var(--mono)",fontSize:9,letterSpacing:.5,textTransform:"uppercase",cursor:"pointer"}),C=$=>({width:"100%",textAlign:"left",padding:"9px 10px",border:`1px solid ${$?"var(--accent)":"var(--border)"}`,background:$?"var(--surface)":"white",fontFamily:$?"var(--sans)":"var(--mono)",fontSize:$?12:11,color:"var(--ink)",cursor:"pointer",display:"flex",justifyContent:"space-between",alignItems:"center",gap:8});return i.jsxs("div",{style:{height:"100%",display:"flex",flexDirection:"column",overflow:"hidden"},children:[i.jsxs("div",{style:{height:48,borderBottom:"1px solid var(--border)",padding:"0 20px",display:"flex",alignItems:"center",justifyContent:"space-between",flexShrink:0},children:[i.jsxs("div",{style:{display:"flex",alignItems:"center",gap:10,minWidth:0},children:[i.jsxs("span",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)"},children:[(j==null?void 0:j.label)||(j==null?void 0:j.name)||"channel",_?` / ${_.name}`:""]}),i.jsx($e,{label:R?"live":"offline",tone:R?"var(--green)":"var(--ink3)"}),i.jsxs("span",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)"},children:[K.length," events · ",G.length," suggested tasks"]}),i.jsx($e,{label:`${(r==null?void 0:r.role)||"developer"} context`,tone:(r==null?void 0:r.role)==="manager"||(r==null?void 0:r.role)==="admin"?"var(--accent)":"var(--indigo)"})]}),i.jsxs("div",{style:{display:"flex",gap:8,alignItems:"center"},children:[i.jsx("button",{onClick:m,style:{padding:"6px 12px",border:"1px solid var(--border)",fontFamily:"var(--mono)",fontSize:9,color:"var(--ink2)",background:"white",cursor:"pointer",letterSpacing:.4},children:"open canvas"}),i.jsx("button",{onClick:()=>void X(),style:{padding:"6px 12px",border:"1px solid var(--border)",fontFamily:"var(--mono)",fontSize:9,color:"var(--ink2)",background:"white",cursor:"pointer",letterSpacing:.4},children:"refresh"})]})]}),i.jsxs("div",{style:{flex:1,display:"grid",gridTemplateColumns:"260px minmax(0, 1fr) 360px",overflow:"hidden"},children:[i.jsxs("div",{style:{borderRight:"1px solid var(--border)",padding:16,overflowY:"auto",display:"flex",flexDirection:"column",gap:14},children:[i.jsxs("div",{style:{padding:12,border:"1px solid var(--border)",background:"var(--surface)",display:"grid",gap:8},children:[i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:9,letterSpacing:.6,color:"var(--ink3)",textTransform:"uppercase"},children:"Context scope"}),i.jsx("div",{style:{fontSize:13,color:"var(--ink)"},children:(r==null?void 0:r.team_id)||(r==null?void 0:r.project_id)||(r==null?void 0:r.org_id)||"local dhee"}),i.jsxs("div",{style:{display:"flex",flexWrap:"wrap",gap:6,fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)"},children:[i.jsxs("span",{children:[h.length," context items"]}),i.jsx("span",{children:"·"}),i.jsxs("span",{children:[u.length," pending"]})]}),i.jsx("button",{onClick:m,style:{padding:"6px 8px",border:"1px solid var(--accent)",background:"var(--accent-dim)",color:"var(--accent)",fontFamily:"var(--mono)",fontSize:9,letterSpacing:.5,cursor:"pointer"},children:"OPEN ORG MAP"})]}),i.jsxs("div",{style:{padding:12,border:"1px solid var(--border)",background:"white",display:"grid",gap:8},children:[i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:9,letterSpacing:.6,color:"var(--ink3)",textTransform:"uppercase"},children:"Continue context"}),i.jsx("div",{style:{fontSize:12,color:"var(--ink2)",lineHeight:1.45},children:((ee=T==null?void 0:T.last_session)==null?void 0:ee.task_summary)||((D=(ie=T==null?void 0:T.claude_sessions)==null?void 0:ie[0])==null?void 0:D.preview)||"No Claude session recovered yet for this repo."}),i.jsxs("div",{style:{display:"flex",gap:6,flexWrap:"wrap",fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)"},children:[i.jsx("span",{children:((fe=T==null?void 0:T.last_session)==null?void 0:fe.source)||"waiting"}),i.jsx("span",{children:"·"}),i.jsxs("span",{children:[((se=T==null?void 0:T.claude_sessions)==null?void 0:se.length)||0," claude sessions"]})]})]}),i.jsxs("div",{children:[i.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:8,gap:8},children:[i.jsx("span",{style:{fontFamily:"var(--mono)",fontSize:9,letterSpacing:.6,color:"var(--ink3)",textTransform:"uppercase"},children:"Workspace"}),S?i.jsx("button",{onClick:()=>S("workspaces"),title:"Manage workspaces",style:{padding:"3px 7px",border:"1px solid var(--border)",background:"white",fontFamily:"var(--mono)",fontSize:9,letterSpacing:.4,color:"var(--ink3)",cursor:"pointer"},children:"+ new / manage"}):null]}),E.length===0?i.jsx("button",{onClick:()=>S==null?void 0:S("workspaces"),style:{width:"100%",padding:"10px 12px",border:"1px dashed var(--border)",background:"white",fontFamily:"var(--mono)",fontSize:10,color:"var(--ink2)",textAlign:"left",cursor:"pointer",lineHeight:1.5},children:"Create your first workspace → e.g. Office, Personal, Sankhya AI Labs."}):i.jsx("select",{value:(j==null?void 0:j.id)||"",onChange:$=>a($.target.value),style:{width:"100%",padding:"9px 10px",border:"1px solid var(--border)",background:"white",fontSize:12},children:E.map($=>i.jsx("option",{value:$.id,children:$.label||$.name},$.id))})]}),i.jsxs("div",{children:[i.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:8,gap:8},children:[i.jsxs("span",{style:{fontFamily:"var(--mono)",fontSize:9,letterSpacing:.6,color:"var(--ink3)",textTransform:"uppercase"},children:["Projects · ",((me=j==null?void 0:j.projects)==null?void 0:me.length)||0]}),S&&j?i.jsx("button",{onClick:()=>S("projects"),title:"Add or edit projects",style:{padding:"3px 7px",border:"1px solid var(--border)",background:"white",fontFamily:"var(--mono)",fontSize:9,letterSpacing:.4,color:"var(--ink3)",cursor:"pointer"},children:"+ project"}):null]}),i.jsxs("div",{style:{display:"grid",gap:6},children:[i.jsxs("button",{onClick:()=>d("",j==null?void 0:j.id),style:C(!_),children:[i.jsx("span",{children:"All projects (workspace line)"}),i.jsx("span",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)"},children:P.length})]}),((j==null?void 0:j.projects)||[]).map($=>{var he;const de=$.id===(_==null?void 0:_.id),ce=((he=$.sessions)==null?void 0:he.length)||0;return i.jsxs("button",{onClick:()=>d($.id,j==null?void 0:j.id),style:C(de),children:[i.jsx("span",{children:$.name}),i.jsx("span",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)"},children:ce})]},$.id)})]})]}),i.jsx(gg,{workspace:j,projects:(j==null?void 0:j.projects)||[],workspaceSessions:P}),j&&i.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:6},children:[i.jsx("span",{style:{fontFamily:"var(--mono)",fontSize:9,letterSpacing:.6,color:"var(--ink3)",textTransform:"uppercase"},children:"Launch"}),i.jsxs("div",{style:{display:"flex",gap:6,flexWrap:"wrap"},children:[i.jsx("button",{onClick:()=>void x("channel session","codex",j.id,void 0,_==null?void 0:_.id),style:ye(!1),children:"+ codex"}),i.jsx("button",{onClick:()=>void x("channel session","claude-code",j.id,"standard",_==null?void 0:_.id),style:ye(!1),children:"+ claude"})]})]})]}),i.jsxs("div",{style:{display:"flex",flexDirection:"column",overflow:"hidden",background:"var(--bg)"},children:[i.jsxs("div",{style:{padding:"12px 20px",borderBottom:"1px solid var(--border)",display:"flex",alignItems:"center",gap:8,flexWrap:"wrap"},children:[i.jsx("span",{style:{fontFamily:"var(--mono)",fontSize:9,letterSpacing:.6,color:"var(--ink3)",textTransform:"uppercase"},children:"Shared line"}),i.jsx("button",{onClick:()=>p("all"),style:ye(f==="all"),children:"all"}),i.jsx("button",{onClick:()=>p("broadcast"),style:ye(f==="broadcast"),children:"broadcasts"}),i.jsx("button",{onClick:()=>p("tool"),style:ye(f==="tool"),children:"tool events"}),i.jsx("button",{onClick:()=>p("note"),style:ye(f==="note"),children:"notes"}),i.jsx("span",{style:{flex:1}}),J?i.jsx("span",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--rose)"},children:J}):null]}),i.jsx("div",{style:{flex:1,overflowY:"auto",padding:20,display:"flex",flexDirection:"column",gap:10},children:K.length===0?i.jsxs("div",{style:{padding:24,border:"1px dashed var(--border)",background:"white",textAlign:"center"},children:[i.jsx("div",{style:{fontSize:13,fontWeight:600,marginBottom:6},children:"The line is quiet."}),i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)",lineHeight:1.55},children:"Every agent tool-call in this workspace will appear here. Launch a session from the left rail, or broadcast a note from the composer to get started."})]}):K.map($=>i.jsx(cp,{message:$,workspace:j,onOpenTask:c},$.id))})]}),i.jsxs("div",{style:{borderLeft:"1px solid var(--border)",padding:16,overflowY:"auto",display:"flex",flexDirection:"column",gap:14},children:[i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:9,letterSpacing:.6,color:"var(--ink3)",textTransform:"uppercase"},children:"Broadcast"}),i.jsx(fp,{workspace:j,activeProjectId:_==null?void 0:_.id,sessionId:z==null?void 0:z.id,onPublished:async()=>{await g(),X()}}),i.jsx(cg,{workspace:j,project:_,onActivity:()=>void X()}),i.jsxs("div",{style:{fontFamily:"var(--mono)",fontSize:9,letterSpacing:.6,color:"var(--ink3)",textTransform:"uppercase",marginTop:2},children:["Suggested tasks · ",G.length]}),i.jsx("div",{style:{display:"grid",gap:8},children:G.length===0?i.jsx("div",{style:{padding:12,border:"1px dashed var(--border)",fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)",lineHeight:1.55,background:"white"},children:"When an agent broadcasts to another project, a task is auto-created there. It will show up here."}):G.slice(0,10).map($=>i.jsxs("button",{onClick:()=>c($.id),style:{textAlign:"left",padding:"10px 12px",border:"1px solid var(--border)",background:"white",cursor:"pointer",display:"flex",flexDirection:"column",gap:6},children:[i.jsx("div",{style:{fontSize:12,fontWeight:600,lineHeight:1.35},children:$.title}),i.jsxs("div",{style:{display:"flex",gap:6,flexWrap:"wrap"},children:[i.jsx($e,{label:$.status||"active",tone:"var(--accent)"}),$.harness?i.jsx($e,{label:String($.harness)}):null]})]},$.id))})]})]})]})}const xg={live:!1,proposals:[],findings:[],conflicts:[],totals:{proposals:0,findings:0,conflicts:0}};function Pu(e){return e==="high"?"var(--rose)":e==="medium"?"var(--accent)":"var(--indigo)"}function Sg(e){const t=e.summary||e.content||"";return t.length>260?`${t.slice(0,260)}...`:t}function kg({viewer:e,onChanged:t}){var T,H,h;const[n,r]=v.useState(xg),[o,l]=v.useState(null),[s,a]=v.useState(null),[d,c]=v.useState(null),g=async()=>{try{const u=await B.inbox(e!=null&&e.team_id?{team:e.team_id,user:e.user_id}:{user:e==null?void 0:e.user_id});r(u),l(f=>{var p,j,_,P,z,Q;return f||((j=(p=u.proposals)==null?void 0:p[0])==null?void 0:j.context_id)||((P=(_=u.findings)==null?void 0:_[0])==null?void 0:P.finding_id)||String(((Q=(z=u.conflicts)==null?void 0:z[0])==null?void 0:Q.id)||"")||null})}catch(u){c(u instanceof Error?u.message:String(u))}};v.useEffect(()=>{g();const u=window.setInterval(()=>void g(),6e3);return()=>window.clearInterval(u)},[e==null?void 0:e.team_id,e==null?void 0:e.user_id]);const m=v.useMemo(()=>{var u,f,p;return(((u=n.totals)==null?void 0:u.proposals)||0)+(((f=n.totals)==null?void 0:f.findings)||0)+(((p=n.totals)==null?void 0:p.conflicts)||0)},[n.totals]),x=async(u,f)=>{a(`${f}:${u.context_id}`),c(null);try{f==="approve"?await B.approveProposal(u.context_id,(e==null?void 0:e.user_id)||"manager"):await B.rejectProposal(u.context_id,(e==null?void 0:e.user_id)||"manager"),await g(),await(t==null?void 0:t())}catch(p){c(p instanceof Error?p.message:String(p))}finally{a(null)}},S=async u=>{a(`finding:${u.finding_id}`),c(null);try{await B.resolveFinding(u.finding_id,(e==null?void 0:e.user_id)||"manager"),await g(),await(t==null?void 0:t())}catch(f){c(f instanceof Error?f.message:String(f))}finally{a(null)}},E=async(u,f)=>{const p=String(u.id||"");if(p){a(`conflict:${p}:${f}`),c(null);try{await B.resolveConflictDetailed(p,{action:f}),await g(),await(t==null?void 0:t())}catch(j){c(j instanceof Error?j.message:String(j))}finally{a(null)}}};return i.jsxs("div",{style:{display:"flex",height:"100%",minHeight:0},children:[i.jsxs("aside",{style:{width:300,borderRight:"1px solid var(--border)",background:"white",padding:16,overflowY:"auto",flexShrink:0},children:[i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)",letterSpacing:"0.08em",textTransform:"uppercase"},children:"Inbox"}),i.jsxs("div",{style:{fontSize:22,fontWeight:650,marginTop:6},children:[m," open"]}),i.jsxs("div",{style:{marginTop:10,display:"grid",gap:8,fontFamily:"var(--mono)",fontSize:10},children:[i.jsx(Tl,{label:"Proposals",value:((T=n.totals)==null?void 0:T.proposals)||0}),i.jsx(Tl,{label:"Findings",value:((H=n.totals)==null?void 0:H.findings)||0}),i.jsx(Tl,{label:"Conflicts",value:((h=n.totals)==null?void 0:h.conflicts)||0})]}),i.jsx("div",{style:{marginTop:18,padding:12,border:"1px solid var(--border)",background:"var(--surface)",fontSize:12,lineHeight:1.5,color:"var(--ink2)"},children:"Review context changes, stale-context findings, and memory conflicts from one queue. Approvals activate context used by routing."}),d?i.jsx("div",{style:{marginTop:12,padding:10,border:"1px solid var(--rose)",background:"var(--rose-dim)",color:"var(--rose)",fontFamily:"var(--mono)",fontSize:10,lineHeight:1.5},children:d}):null]}),i.jsxs("main",{style:{flex:1,minWidth:0,overflowY:"auto",background:"var(--bg)",padding:18,display:"grid",gap:16,alignContent:"start"},children:[i.jsx(zl,{title:"Pending Proposals",count:n.proposals.length,empty:"No context edits are waiting for approval.",children:n.proposals.map(u=>i.jsxs("article",{onClick:()=>l(u.context_id),style:Fl(o===u.context_id),children:[i.jsxs("div",{style:Rl,children:[i.jsxs("div",{children:[i.jsx("div",{style:Nl,children:u.title}),i.jsxs("div",{style:Pl,children:[u.proposed_by_user_id||"developer"," · ",u.team_id||u.project_id||u.scope]})]}),i.jsx(uo,{color:"var(--accent)",children:"pending"})]}),i.jsx("p",{style:Lu,children:Sg(u)||"No preview available."}),i.jsxs("div",{style:{display:"flex",gap:8,justifyContent:"flex-end"},children:[i.jsx(Or,{label:"Open in Context",onClick:f=>{f.stopPropagation(),window.location.hash=`#vault/item/${u.context_id}`,window.history.replaceState(null,"",`?view=context${window.location.hash}`),window.dispatchEvent(new PopStateEvent("popstate"))}}),i.jsx(Or,{label:"Reject",color:"var(--rose)",busy:s===`reject:${u.context_id}`,onClick:f=>{f.stopPropagation(),x(u,"reject")}}),i.jsx(Or,{label:"Approve",color:"var(--green)",busy:s===`approve:${u.context_id}`,onClick:f=>{f.stopPropagation(),x(u,"approve")}})]})]},u.context_id))}),i.jsx(zl,{title:"Manager Findings",count:n.findings.length,empty:"No stale, low-quality, or duplicate context findings.",children:n.findings.map(u=>i.jsxs("article",{onClick:()=>l(u.finding_id),style:Fl(o===u.finding_id),children:[i.jsxs("div",{style:Rl,children:[i.jsxs("div",{children:[i.jsx("div",{style:Nl,children:u.title}),i.jsxs("div",{style:Pl,children:[u.team_id," · ",u.finding_type]})]}),i.jsx(uo,{color:Pu(u.severity),children:u.severity})]}),i.jsx("p",{style:Lu,children:u.detail}),i.jsx("div",{style:{display:"flex",justifyContent:"flex-end"},children:i.jsx(Or,{label:"Resolve",color:"var(--green)",busy:s===`finding:${u.finding_id}`,onClick:f=>{f.stopPropagation(),S(u)}})})]},u.finding_id))}),i.jsx(zl,{title:"Memory Conflicts",count:n.conflicts.length,empty:"No memory contradictions detected.",children:n.conflicts.map(u=>{var j,_;const f=u,p=String(f.id||Math.random());return i.jsxs("article",{onClick:()=>l(p),style:Fl(o===p),children:[i.jsxs("div",{style:Rl,children:[i.jsxs("div",{children:[i.jsx("div",{style:Nl,children:"Memory conflict"}),i.jsx("div",{style:Pl,children:f.reason||"Contradiction"})]}),i.jsx(uo,{color:Pu(f.severity),children:f.severity||"open"})]}),i.jsxs("div",{style:{display:"grid",gap:6,marginTop:10},children:[i.jsx(Fu,{label:"A",text:(j=f.belief_a)==null?void 0:j.content}),i.jsx(Fu,{label:"B",text:(_=f.belief_b)==null?void 0:_.content})]}),i.jsx("div",{style:{display:"flex",gap:8,justifyContent:"flex-end",marginTop:10},children:["KEEP A","KEEP B","MERGE"].map(P=>i.jsx(Or,{label:P,busy:s===`conflict:${p}:${P}`,onClick:z=>{z.stopPropagation(),E(u,P)}},P))})]},p)})})]})]})}function Tl({label:e,value:t}){return i.jsxs("div",{style:{display:"flex",justifyContent:"space-between"},children:[i.jsx("span",{style:{color:"var(--ink3)"},children:e}),i.jsx("span",{style:{color:t?"var(--accent)":"var(--ink2)"},children:t})]})}function zl({title:e,count:t,empty:n,children:r}){return i.jsxs("section",{style:{display:"grid",gap:10},children:[i.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8,fontFamily:"var(--mono)",fontSize:10,letterSpacing:"0.08em",color:"var(--ink3)",textTransform:"uppercase"},children:[i.jsx("span",{children:e}),i.jsx(uo,{children:t})]}),t===0?i.jsx("div",{style:{border:"1px dashed var(--border)",background:"white",color:"var(--ink3)",padding:16,fontSize:12},children:n}):r]})}function uo({children:e,color:t="var(--ink3)"}){return i.jsx("span",{style:{display:"inline-flex",alignItems:"center",padding:"2px 7px",border:`1px solid ${t}`,color:t,background:"white",borderRadius:3,fontFamily:"var(--mono)",fontSize:9},children:e})}function Or({label:e,onClick:t,color:n="var(--ink2)",busy:r}){return i.jsx("button",{onClick:t,disabled:r,style:{padding:"6px 9px",border:`1px solid ${n}`,color:n,background:"white",fontFamily:"var(--mono)",fontSize:9,borderRadius:3,cursor:r?"wait":"pointer"},children:r?"...":e})}function Fu({label:e,text:t}){return i.jsxs("div",{style:{border:"1px solid var(--border)",background:"var(--surface)",padding:10,display:"grid",gridTemplateColumns:"20px minmax(0, 1fr)",gap:8},children:[i.jsx("span",{style:{fontFamily:"var(--mono)",color:"var(--ink3)"},children:e}),i.jsx("span",{style:{color:"var(--ink2)",fontSize:12,lineHeight:1.5},children:t||"No content"})]})}const Rl={display:"flex",justifyContent:"space-between",alignItems:"flex-start",gap:12},Nl={fontSize:15,fontWeight:650,color:"var(--ink)"},Pl={fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)",marginTop:3},Lu={margin:"10px 0",color:"var(--ink2)",fontSize:12,lineHeight:1.55};function Fl(e){return{border:`1px solid ${e?"var(--accent)":"var(--border)"}`,background:"white",padding:14,boxShadow:e?"0 10px 24px rgba(20,16,10,0.06)":"none",cursor:"pointer"}}const jg={"&":"&","<":"<",">":">",'"':""","'":"'"};function ti(e){return e.replace(/[&<>"']/g,t=>jg[t]||t)}function wg(e){const t=e.trim();return/^(https?:|mailto:|#|\/)/i.test(t)?t:"#"}function Cn(e,t){let n=ti(e);return n=n.replace(/`([^`]+)`/g,(r,o)=>`${o}`),n=n.replace(/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g,(r,o,l)=>{const s=(l||o).trim(),a=t?t(o.trim()):null;return a?`${s}`:`${s}`}),n=n.replace(/\[([^\]]+)\]\(([^)]+)\)/g,(r,o,l)=>`${o}`),n=n.replace(/\*\*([^*]+)\*\*/g,"$1"),n=n.replace(/(^|[^*])\*([^*]+)\*/g,"$1$2"),n}function Cg({source:e,wikiResolve:t}){const n=(e||"").replace(/\r\n/g,` +`).split(` +`),r=[];let o=0;for(;o${ti(d.join(` +`))}`);continue}if(/^### /.test(l)){r.push(`

${Cn(l.slice(4),t)}

`),o+=1;continue}if(/^## /.test(l)){r.push(`

${Cn(l.slice(3),t)}

`),o+=1;continue}if(/^# /.test(l)){r.push(`

${Cn(l.slice(2),t)}

`),o+=1;continue}if(/^>\s?/.test(l)){const a=[];for(;o\s?/.test(n[o]);)a.push(n[o].replace(/^>\s?/,"")),o+=1;r.push(`
${Cn(a.join(" "),t)}
`);continue}if(/^\s*[-*]\s/.test(l)){const a=[];for(;o${Cn(n[o].replace(/^\s*[-*]\s/,""),t)}`),o+=1;r.push(`
    ${a.join("")}
`);continue}if(/^\s*\d+\.\s/.test(l)){const a=[];for(;o${Cn(n[o].replace(/^\s*\d+\.\s/,""),t)}`),o+=1;r.push(`
    ${a.join("")}
`);continue}if(l.trim()===""){o+=1;continue}const s=[l];for(o+=1;o\s?)/.test(n[o]);)s.push(n[o]),o+=1;r.push(`

${Cn(s.join(` +`),t)}

`)}return i.jsx("div",{className:"md-root",style:{fontFamily:"var(--font)",fontSize:13.5,lineHeight:1.55,color:"var(--ink)"},dangerouslySetInnerHTML:{__html:r.join("")}})}const Ki={title:"",content:"",scope:"team",kind:"note",project_id:"",team_id:"",tags:""},bg=["user","company","global_team","project","team","agent"],Ou={items:[],next_cursor:null,active_only:!1,totals:{tokens_saved:0,estimated_cost_saved_usd:0,router_calls:0,sessions:0}},$u={live:!1,items:[],totals:{contexts:0,used_contexts:0,usage_count:0,tokens_served:0,proven_tokens_saved:0,theoretical_api_value_usd:0,realized_cost_saved_usd:0}};function _g(e){return{...e,tags:Array.isArray(e.tags)?e.tags:[]}}function Du(e){return(e==null?void 0:e.role)==="manager"||(e==null?void 0:e.role)==="admin"?e.team_id?{...Ki,scope:"team",team_id:e.team_id,project_id:e.project_id||""}:e.project_id?{...Ki,scope:"project",project_id:e.project_id}:{...Ki,scope:"company"}:{...Ki,scope:"user",user_id:e==null?void 0:e.user_id}}function Eg(e){return{title:e.title||"Untitled context",content:e.content||e.summary||"",scope:e.scope||"team",kind:e.kind||"note",project_id:e.project_id||"",team_id:e.team_id||"",tags:(e.tags||[]).join(", ")}}function Tg(e){return{title:ba(e),content:e.content||"",scope:"memory",kind:e.tier,project_id:"",team_id:"",tags:(e.tags||[]).join(", ")}}function Mu(e,t,n){return(e==null?void 0:e.role)==="admin"||(e==null?void 0:e.role)==="manager"?!0:((t==null?void 0:t.scope)||(n==null?void 0:n.scope))==="user"?!(t!=null&&t.user_id)||t.user_id===(e==null?void 0:e.user_id):!1}function Iu(e){return e==="pending_review"?"var(--accent)":e==="rejected"||e==="inactive"?"var(--rose)":"var(--green)"}function Ll(e){return e.split(",").map(t=>t.trim()).filter(Boolean)}function zg(e){return e.team_id?e.team_id:e.project_id?e.project_id:e.user_id?"mine":e.scope}function Rg(e){return e==="user"?"Mine":e==="company"?"Company":e==="global_team"?"Global Teams":e==="project"?"Projects":e==="team"?"Teams":e==="agent"?"Agents":e}function Bu(e){return e.replace(/[-_]+/g," ").replace(/\s+/g," ").trim().replace(/\b\w/g,t=>t.toUpperCase())}function ba(e){var n;return(e.content||"").split(/\r?\n/).map(r=>r.trim()).find(Boolean)||""||((n=e.tags)==null?void 0:n[0])||`${e.tier} memory`}function Ng(e){var l;const t=ba(e),n=t.match(/^([^>]{3,72})\s*>\s*.+$/);if(n!=null&&n[1])return n[1].trim();const r=[t,e.source,...e.tags||[]].join(" ").toLowerCase();if(r.includes("repository guideline")||r.includes("agents.md"))return"Repository Guidelines";if(r.startsWith("edited ")||r.includes("/users/")||r.includes("/tmp/"))return"File Edits";if(r.includes("session")||r.includes("continuity"))return"Session Continuity";const o=(l=e.tags)==null?void 0:l.find(s=>s&&s!==e.tier);return o?Bu(o):`${Bu(e.tier)} Memory`}function Pg(e){const t=ba(e),n=t.split(/\s*>\s*/);return(n.length>1?n.slice(1).join(" > "):t).slice(0,72)||"Memory"}function on(e){return!e||e<=0?"0":new Intl.NumberFormat("en",{notation:"compact",maximumFractionDigits:1}).format(e)}function On(e){const t=Math.max(0,Number(e||0));return t>0&&t<.01?"<$0.01":new Intl.NumberFormat("en-US",{style:"currency",currency:"USD",maximumFractionDigits:t>=10?0:2}).format(t)}function Fg(e){const t=e.pricing;if(!t)return"PAYG input-token estimate";const n=t.input_cost_per_million,r=t.model_family||e.model||"model",o=t.provider||e.runtime||e.agent||"provider",l=typeof n=="number"?`$${n}/1M input tokens`:"input-token rate";return`${o} · ${r} · ${l}`}function Lg(e,t){const n=(t==null?void 0:t.usage_count)??e.usage_count??0,r=(t==null?void 0:t.proven_tokens_saved)??0,o=[`${e.kind} · ${zg(e)}`,`${n} use${n===1?"":"s"}`];return o.push(r>0?`${on(r)} proven saved`:"no proven $"),o.join(" · ")}function Og(e){const t=String(e.runtime||e.agent||"").toLowerCase();return t==="codex"?"var(--indigo)":t==="claude-code"||t==="claude"?"var(--accent)":"var(--green)"}function $g(e){const t=(e.title||"").trim();return t||(e.cwd||e.repo_root||"").split("/").filter(Boolean).pop()||e.session_id||"Session"}function Dg({onMemoryCountChange:e,viewer:t,orgGraph:n,onInboxChanged:r}){var wn,Ut,Z,ae,Se,le,Ee,Lt,Qn;const[o,l]=v.useState([]),[s,a]=v.useState([]),[d,c]=v.useState(null),[g,m]=v.useState(()=>Du(t||null)),[x,S]=v.useState(!1),[E,T]=v.useState(""),[H,h]=v.useState(null),[u,f]=v.useState(null),[p,j]=v.useState(!0),[_,P]=v.useState(null),[z,Q]=v.useState(Ou),[R,J]=v.useState($u),[X,K]=v.useState({}),[G,M]=v.useState(""),V=v.useRef({}),ye=async()=>{var w,b,k;try{const[O,I,ne,ve]=await Promise.all([B.listMemories().catch(Ue=>({live:!1,engrams:[],count:0,error:String(Ue)})),B.contextItems({limit:500}).catch(Ue=>({live:!1,items:[],error:String(Ue)})),B.routerSessions({active:!1,limit:50}).catch(()=>Ou),B.contextUsage({limit:500}).catch(()=>$u)]),Ke=(w=I.items)!=null&&w.length||!((b=n==null?void 0:n.raw)!=null&&b.context_index)?I.items||[]:n.raw.context_index,st=new Map((ve.items||[]).map(Ue=>[Ue.context_id,Ue]));l(Ke.map(_g).map(Ue=>{const Yn=st.get(Ue.context_id);return Yn?{...Ue,usage_count:Yn.usage_count,last_used_at:Yn.last_used_at,token_cost:Yn.token_cost}:Ue})),a(O.engrams||[]),Q(ne),J(ve),j(!!(O.live||I.live||n!=null&&n.live)),f(I.error||O.error||null),e==null||e(((k=O.engrams)==null?void 0:k.length)||0)}catch(O){f(O instanceof Error?O.message:String(O)),j(!1)}};v.useEffect(()=>{ye();const w=window.setInterval(()=>void ye(),6e3);return()=>window.clearInterval(w)},[]),v.useEffect(()=>{if(d)return;const w=decodeURIComponent(window.location.hash||""),b=w.startsWith("#vault/")?w.replace("#vault/","").trim():"",k=b?o.find(O=>O.team_id===b):null;if(k){c({kind:"context",id:k.context_id});return}if(o[0]){c({kind:"context",id:o[0].context_id});return}s[0]&&c({kind:"memory",id:s[0].id})},[o,s,d]);const C=(d==null?void 0:d.kind)==="context"&&o.find(w=>w.context_id===d.id)||null,A=(d==null?void 0:d.kind)==="memory"&&s.find(w=>w.id===d.id)||null;v.useEffect(()=>{if(C){m(Eg(C)),S(!1);return}if(A){m(Tg(A)),S(!1);return}(d==null?void 0:d.kind)==="new"&&(m(Du(t||null)),S(!0))},[C,A,d==null?void 0:d.kind,t]),v.useEffect(()=>{if(!C){P(null);return}let w=!0;return B.backlinks(C.context_id).then(b=>{w&&P(b)}).catch(()=>{w&&P(null)}),()=>{w=!1}},[C==null?void 0:C.context_id]);const W=v.useMemo(()=>{const w=E.trim().toLowerCase();return w?o.filter(b=>[b.title,b.summary,b.content,b.kind,b.scope,b.team_id,b.project_id,...b.tags||[]].filter(Boolean).join(" ").toLowerCase().includes(w)):o},[o,E]),ee=v.useMemo(()=>{const w=new Map;for(const b of bg)w.set(b,[]);for(const b of W){const k=b.scope||"team";w.set(k,[...w.get(k)||[],b])}return w},[W]),ie=v.useMemo(()=>{const w=E.trim().toLowerCase();return w?s.filter(b=>[b.content,b.source,b.tier,...b.tags||[]].filter(Boolean).join(" ").toLowerCase().includes(w)):s},[s,E]),D=v.useMemo(()=>{const w=new Map;for(const b of ie){const k=Ng(b);w.set(k,[...w.get(k)||[],b])}return new Map([...w.entries()].sort(([b],[k])=>b.localeCompare(k)))},[ie]),fe=v.useMemo(()=>{const w=[];for(const[b,k]of ee.entries())k.length!==0&&w.push({id:`context:${b}`,kind:"context",label:Rg(b),count:k.length,rows:k});for(const[b,k]of D.entries())k.length!==0&&w.push({id:`memory:${b}`,kind:"memory",label:b,count:k.length,rows:k});return w},[ee,D]),se=v.useMemo(()=>{const w=[];for(const b of fe)if(w.push({id:`section:${b.id}`,type:"section",sectionId:b.id}),!!X[b.id])if(b.kind==="context")for(const k of b.rows)w.push({id:`context-item:${k.context_id}`,type:"context",sectionId:b.id,item:k});else for(const k of b.rows)w.push({id:`memory-item:${k.id}`,type:"memory",sectionId:b.id,memory:k});return w},[X,fe]);v.useEffect(()=>{E.trim()&&K({})},[E]),v.useEffect(()=>{if(!se.length){G&&M("");return}(!G||!se.some(w=>w.id===G))&&M(se[0].id)},[G,se]),v.useEffect(()=>{var w;G&&((w=V.current[G])==null||w.focus())},[G]);const me=(w,b)=>{K(k=>({...k,[w]:typeof b=="boolean"?b:!k[w]}))},$=w=>{c({kind:"context",id:w.context_id}),window.location.hash=`#vault/item/${w.context_id}`},de=w=>{c({kind:"memory",id:w.id})},ce=w=>{M(w)},he=(w,b)=>{const k=se.findIndex(I=>I.id===w);if(k<0)return;const O=se[Math.max(0,Math.min(se.length-1,k+b))];O&&ce(O.id)},_e=(w,b)=>{if(w.key==="ArrowDown"){w.preventDefault(),he(b.id,1);return}if(w.key==="ArrowUp"){w.preventDefault(),he(b.id,-1);return}if(w.key==="Home"){w.preventDefault(),se[0]&&ce(se[0].id);return}if(w.key==="End"){w.preventDefault();const k=se[se.length-1];k&&ce(k.id);return}if(w.key==="ArrowRight"&&b.type==="section"){if(w.preventDefault(),!X[b.sectionId])me(b.sectionId,!0);else{const k=se.find(O=>O.sectionId===b.sectionId&&O.type!=="section");k&&ce(k.id)}return}if(w.key==="ArrowLeft"){w.preventDefault(),b.type==="section"?X[b.sectionId]&&me(b.sectionId,!1):ce(`section:${b.sectionId}`);return}(w.key==="Enter"||w.key===" ")&&b.type==="section"&&(w.preventDefault(),me(b.sectionId))},N=w=>{const b=o.find(k=>k.title.toLowerCase()===w.toLowerCase());return b?`#vault/item/${b.context_id}`:null},oe=()=>{c({kind:"new",id:"new"})},q=async()=>{if(!g.title.trim()||!g.content.trim()){f("Title and content are required.");return}h("save"),f(null);const w={title:g.title.trim(),content:g.content,scope:g.scope,kind:g.kind||"note",project_id:g.project_id||(t==null?void 0:t.project_id)||void 0,team_id:g.team_id||(t==null?void 0:t.team_id)||void 0,tags:Ll(g.tags),metadata:{source:"sankhya-vault"}};try{if(Mu(t||null,C,g)){const b=await B.upsertContext({...w,context_id:C==null?void 0:C.context_id,user_id:g.scope==="user"?t==null?void 0:t.user_id:void 0});c({kind:"context",id:b.item.context_id})}else{const b=await B.proposeContext({...w,proposed_by_user_id:(t==null?void 0:t.user_id)||"developer",supersedes_id:C==null?void 0:C.context_id});c({kind:"context",id:b.proposal.context_id}),await(r==null?void 0:r())}await ye(),S(!1)}catch(b){f(b instanceof Error?b.message:String(b))}finally{h(null)}},te=async w=>{if(C){h(w);try{w==="approve"?await B.approveProposal(C.context_id,(t==null?void 0:t.user_id)||"manager"):await B.rejectProposal(C.context_id,(t==null?void 0:t.user_id)||"manager"),await ye(),await(r==null?void 0:r())}catch(b){f(b instanceof Error?b.message:String(b))}finally{h(null)}}},Ae=(d==null?void 0:d.kind)==="memory",Ft=Mu(t||null,C,g),xe=v.useMemo(()=>[...z.items||[]].sort((b,k)=>{const O=(k.tokens_saved||0)-(b.tokens_saved||0);return O!==0?O:String(k.updated_at||"").localeCompare(String(b.updated_at||""))}).filter(b=>b.tokens_saved>0).slice(0,4),[z.items]),Me=v.useMemo(()=>new Map((R.items||[]).map(w=>[w.context_id,w])),[R.items]),je=C&&Me.get(C.context_id)||null,Fe=v.useMemo(()=>[...R.items||[]].filter(w=>(w.usage_count||0)>0).sort((w,b)=>{const k=(b.usage_count||0)-(w.usage_count||0);return k!==0?k:(b.proven_tokens_saved||0)-(w.proven_tokens_saved||0)}).slice(0,5),[R.items]);return i.jsxs("div",{style:{display:"flex",height:"100%",minHeight:0},children:[i.jsxs("aside",{style:{width:282,borderRight:"1px solid var(--border)",background:"white",display:"flex",flexDirection:"column",flexShrink:0},children:[i.jsxs("div",{style:{padding:14,borderBottom:"1px solid var(--border)"},children:[i.jsxs("div",{style:{display:"flex",alignItems:"center",justifyContent:"space-between",gap:8,marginBottom:10},children:[i.jsxs("div",{children:[i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)",letterSpacing:"0.08em",textTransform:"uppercase"},children:"Context"}),i.jsx("div",{style:{fontSize:14,fontWeight:600,color:"var(--ink)"},children:"Manager"})]}),i.jsx("button",{onClick:oe,style:{padding:"5px 8px",border:"1px solid var(--accent)",background:"var(--accent-dim)",color:"var(--accent)",fontFamily:"var(--mono)",fontSize:10,borderRadius:3},children:"NEW"})]}),i.jsx("input",{value:E,onChange:w=>T(w.target.value),placeholder:"search context or memory",style:{width:"100%",boxSizing:"border-box",border:"1px solid var(--border)",background:"var(--surface)",color:"var(--ink)",padding:"8px 9px",fontSize:12}}),i.jsxs("div",{style:{marginTop:8},children:[i.jsx(Bg,{rows:Fe,totals:R.totals,onSelect:w=>{c({kind:"context",id:w}),window.location.hash=`#vault/item/${w}`}}),i.jsx(Ag,{rows:xe,totalTokens:((wn=z.totals)==null?void 0:wn.tokens_saved)||0,totalCost:((Ut=z.totals)==null?void 0:Ut.realized_cost_saved_usd)??((Z=z.totals)==null?void 0:Z.estimated_cost_saved_usd)??0,apiValue:((ae=z.totals)==null?void 0:ae.theoretical_api_value_usd)??((Se=z.totals)==null?void 0:Se.estimated_cost_saved_usd)??0,totalSessions:((le=z.totals)==null?void 0:le.sessions)||0,budget:z.budget})]})]}),i.jsx("div",{style:{flex:1,overflowY:"auto",padding:10},role:"tree","aria-label":"Context categories",children:fe.map(w=>{const b={id:`section:${w.id}`,type:"section",sectionId:w.id},k=!!X[w.id],O=w.kind==="context"?w.rows.some(I=>(d==null?void 0:d.kind)==="context"&&d.id===I.context_id):w.rows.some(I=>(d==null?void 0:d.kind)==="memory"&&d.id===I.id);return i.jsx(Ig,{label:w.label,count:w.count,open:k,active:O,focused:G===b.id,buttonRef:I=>{V.current[b.id]=I},onFocus:()=>M(b.id),onToggle:()=>me(w.id),onKeyDown:I=>_e(I,b),children:w.kind==="context"?w.rows.map(I=>{const ne={id:`context-item:${I.context_id}`,type:"context",sectionId:w.id};return i.jsx(Au,{treeId:ne.id,buttonRef:ve=>{V.current[ne.id]=ve},focused:G===ne.id,active:(d==null?void 0:d.kind)==="context"&&d.id===I.context_id,dot:Iu(I.proposal_status||I.status),title:I.title,meta:Lg(I,Me.get(I.context_id)),onFocus:()=>M(ne.id),onKeyDown:ve=>_e(ve,ne),onClick:()=>$(I)},I.context_id)}):w.rows.map(I=>{const ne={id:`memory-item:${I.id}`,type:"memory",sectionId:w.id};return i.jsx(Au,{treeId:ne.id,buttonRef:ve=>{V.current[ne.id]=ve},focused:G===ne.id,active:(d==null?void 0:d.kind)==="memory"&&d.id===I.id,dot:"var(--indigo)",title:Pg(I),meta:`${I.tier} · ${I.tokens||0} tok`,onFocus:()=>M(ne.id),onKeyDown:ve=>_e(ve,ne),onClick:()=>de(I)},I.id)})},w.id)})})]}),i.jsxs("main",{style:{flex:1,minWidth:0,display:"grid",gridTemplateColumns:"minmax(0, 1fr) 320px",background:"var(--bg)"},children:[i.jsxs("section",{style:{minWidth:0,display:"flex",flexDirection:"column"},children:[i.jsxs("header",{style:{height:52,borderBottom:"1px solid var(--border)",display:"flex",alignItems:"center",justifyContent:"space-between",padding:"0 18px",background:"white",gap:12},children:[i.jsxs("div",{style:{minWidth:0},children:[i.jsxs("div",{style:{fontFamily:"var(--mono)",fontSize:9,letterSpacing:"0.08em",color:"var(--ink3)",textTransform:"uppercase"},children:[A?"Legacy memory":g.scope||"context",p?"":" · offline"]}),i.jsx("div",{style:{fontSize:16,fontWeight:600,color:"var(--ink)",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:g.title||"New context item"})]}),i.jsxs("div",{style:{display:"flex",gap:8,flexShrink:0},children:[(C==null?void 0:C.proposal_status)==="pending_review"&&((t==null?void 0:t.role)==="manager"||(t==null?void 0:t.role)==="admin")?i.jsxs(i.Fragment,{children:[i.jsx(Qi,{label:"APPROVE",tone:"green",busy:H==="approve",onClick:()=>void te("approve")}),i.jsx(Qi,{label:"REJECT",tone:"rose",busy:H==="reject",onClick:()=>void te("reject")})]}):null,Ae?null:i.jsxs(i.Fragment,{children:[i.jsx(Qi,{label:x?"PREVIEW":"EDIT",onClick:()=>S(w=>!w)}),i.jsx(Qi,{label:Ft?"SAVE":"REQUEST",tone:Ft?"accent":"indigo",busy:H==="save",onClick:()=>void q()})]})]})]}),i.jsxs("div",{style:{flex:1,minHeight:0,overflow:"auto",padding:18,display:"grid",gridTemplateColumns:x?"minmax(0, 1fr) minmax(0, 1fr)":"minmax(0, 760px)",gap:16,alignContent:"start"},children:[x&&!Ae?i.jsxs("div",{style:{display:"grid",gap:10,alignContent:"start"},children:[i.jsx("input",{value:g.title,onChange:w=>m(b=>({...b,title:w.target.value})),placeholder:"Context title",style:bn}),i.jsxs("div",{style:{display:"grid",gridTemplateColumns:"1fr 1fr",gap:8},children:[i.jsxs("select",{value:g.scope,onChange:w=>m(b=>({...b,scope:w.target.value})),style:bn,children:[i.jsx("option",{value:"company",children:"company"}),i.jsx("option",{value:"project",children:"project"}),i.jsx("option",{value:"global_team",children:"global_team"}),i.jsx("option",{value:"team",children:"team"}),i.jsx("option",{value:"user",children:"user"}),i.jsx("option",{value:"agent",children:"agent"})]}),i.jsx("input",{value:g.kind,onChange:w=>m(b=>({...b,kind:w.target.value})),placeholder:"kind: runbook / policy / decision",style:bn})]}),i.jsxs("div",{style:{display:"grid",gridTemplateColumns:"1fr 1fr",gap:8},children:[i.jsx("input",{value:g.project_id,onChange:w=>m(b=>({...b,project_id:w.target.value})),placeholder:"project id",style:bn}),i.jsx("input",{value:g.team_id,onChange:w=>m(b=>({...b,team_id:w.target.value})),placeholder:"team id",style:bn})]}),i.jsx("input",{value:g.tags,onChange:w=>m(b=>({...b,tags:w.target.value})),placeholder:"tags, comma separated",style:bn}),i.jsx("textarea",{value:g.content,onChange:w=>m(b=>({...b,content:w.target.value})),rows:20,placeholder:"Write markdown context here...",style:{...bn,minHeight:420,resize:"vertical",lineHeight:1.55,fontFamily:"var(--mono)"}})]}):null,i.jsx("article",{style:{border:"1px solid var(--border)",background:"white",padding:18,minHeight:420,boxShadow:"0 10px 28px rgba(20,16,10,0.04)"},children:i.jsx(Cg,{source:g.content||"_No context selected._",wikiResolve:N})})]})]}),i.jsxs("aside",{style:{borderLeft:"1px solid var(--border)",background:"white",padding:16,overflowY:"auto",display:"flex",flexDirection:"column",gap:14},children:[i.jsxs(_n,{label:"Scope",children:[i.jsx($r,{children:g.scope}),g.kind?i.jsx($r,{children:g.kind}):null,C!=null&&C.proposal_status?i.jsx($r,{color:Iu(C.proposal_status),children:C.proposal_status}):null]}),i.jsxs(_n,{label:"Ownership",children:[i.jsx(Ge,{k:"org",v:t==null?void 0:t.org_id}),i.jsx(Ge,{k:"project",v:g.project_id||(t==null?void 0:t.project_id)}),i.jsx(Ge,{k:"team",v:g.team_id||(t==null?void 0:t.team_id)}),i.jsx(Ge,{k:"viewer",v:t==null?void 0:t.user_id}),i.jsx(Ge,{k:"role",v:(t==null?void 0:t.role)||"developer"})]}),i.jsxs(_n,{label:"Health",children:[i.jsx(Ge,{k:"quality",v:C==null?void 0:C.quality_score}),i.jsx(Ge,{k:"freshness",v:C==null?void 0:C.freshness_score}),i.jsx(Ge,{k:"confidence",v:C==null?void 0:C.confidence}),i.jsx(Ge,{k:"token cost",v:C==null?void 0:C.token_cost}),i.jsx(Ge,{k:"updated",v:(C==null?void 0:C.updated_at)||(A==null?void 0:A.created)})]}),C?i.jsxs(_n,{label:"Usage",children:[i.jsx(Ge,{k:"uses",v:(je==null?void 0:je.usage_count)??C.usage_count??0}),i.jsx(Ge,{k:"last used",v:(je==null?void 0:je.last_used_at)??C.last_used_at}),i.jsx(Ge,{k:"tokens served",v:(je==null?void 0:je.tokens_served)??0}),i.jsx(Ge,{k:"proven saved",v:(je==null?void 0:je.proven_tokens_saved)??0}),i.jsx(Ge,{k:"proven $",v:On(je==null?void 0:je.realized_cost_saved_usd)}),i.jsx(Ge,{k:"evidence",v:(Ee=je==null?void 0:je.evidence)!=null&&Ee.has_direct_savings_evidence?"direct attribution":"usage only"})]}):null,i.jsx(_n,{label:"Tags",children:Ll(g.tags).length?Ll(g.tags).map(w=>i.jsx($r,{children:w},w)):i.jsx("span",{style:{color:"var(--ink3)",fontSize:12},children:"none"})}),i.jsx(_n,{label:"Backlinks",children:(Lt=_==null?void 0:_.backlinks)!=null&&Lt.length?_.backlinks.map(w=>i.jsx("button",{onClick:()=>c({kind:"context",id:w.src}),style:Mg,children:w.src_title||w.src},`${w.src}:${w.edge_type}`)):i.jsx("span",{style:{color:"var(--ink3)",fontSize:12},children:"none"})}),i.jsx(_n,{label:"Shares",children:(Qn=_==null?void 0:_.shares)!=null&&Qn.length?_.shares.map((w,b)=>i.jsx($r,{children:String(w.scope||w.team_id||"share")},b)):i.jsx("span",{style:{color:"var(--ink3)",fontSize:12},children:"private"})}),u?i.jsx("div",{style:{border:"1px solid var(--rose)",color:"var(--rose)",background:"var(--rose-dim)",padding:10,fontFamily:"var(--mono)",fontSize:10,lineHeight:1.5},children:u}):null]})]})]})}const bn={width:"100%",boxSizing:"border-box",border:"1px solid var(--border)",background:"white",color:"var(--ink)",padding:"8px 9px",fontSize:12},Mg={display:"block",width:"100%",textAlign:"left",border:"1px solid var(--border)",background:"var(--surface)",color:"var(--ink2)",padding:"7px 8px",fontSize:12,cursor:"pointer"};function Ig({label:e,count:t,open:n,active:r,focused:o,buttonRef:l,onFocus:s,onToggle:a,onKeyDown:d,children:c}){return i.jsxs("div",{style:{marginBottom:6},children:[i.jsxs("button",{ref:l,type:"button",role:"treeitem","aria-expanded":n,"aria-selected":r,tabIndex:o?0:-1,onFocus:s,onClick:a,onKeyDown:d,style:{width:"100%",border:`1px solid ${r?"var(--accent)":"var(--border)"}`,background:r?"var(--accent-dim)":"var(--surface)",color:"var(--ink)",borderRadius:4,padding:"7px 8px",cursor:"pointer",display:"grid",gridTemplateColumns:"12px minmax(0, 1fr) auto",alignItems:"center",gap:8,textAlign:"left",fontFamily:"var(--mono)",fontSize:10,letterSpacing:"0.04em",textTransform:"uppercase",outline:o?"2px solid var(--accent-dim)":"none"},children:[i.jsx("span",{style:{color:"var(--ink3)"},children:n?"v":">"}),i.jsx("span",{style:{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:e}),i.jsx("span",{style:{color:"var(--ink3)"},children:t})]}),n?i.jsx("div",{role:"group",style:{display:"grid",gap:4,margin:"4px 0 8px 14px"},children:c}):null]})}function Bg({rows:e,totals:t,onSelect:n}){return i.jsxs("div",{style:{border:"1px solid var(--border)",background:"white",borderRadius:4,padding:8,marginBottom:8},children:[i.jsxs("div",{style:{display:"flex",alignItems:"baseline",justifyContent:"space-between",gap:8},children:[i.jsx("span",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",letterSpacing:"0.04em",textTransform:"uppercase"},children:"Frequent context"}),i.jsxs("span",{style:{fontSize:13,fontWeight:700,color:"var(--ink)"},children:[on((t==null?void 0:t.usage_count)||0)," uses"]})]}),i.jsxs("div",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)",marginTop:2,lineHeight:1.35},children:[on((t==null?void 0:t.tokens_served)||0)," served ·"," ",on((t==null?void 0:t.proven_tokens_saved)||0)," proven saved ·"," ",On(t==null?void 0:t.realized_cost_saved_usd)]}),i.jsx("div",{style:{display:"grid",gap:5,marginTop:8},children:e.length?e.map(r=>i.jsxs("button",{type:"button",onClick:()=>n(r.context_id),style:{border:"0",borderTop:"1px solid var(--border)",background:"transparent",padding:"6px 0 0",textAlign:"left",cursor:"pointer",display:"grid",gridTemplateColumns:"minmax(0, 1fr) auto",gap:8},children:[i.jsxs("span",{style:{minWidth:0},children:[i.jsx("span",{style:{display:"block",color:"var(--ink)",fontSize:11,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},title:r.title,children:r.title}),i.jsxs("span",{style:{display:"block",fontFamily:"var(--mono)",color:"var(--ink3)",fontSize:9,marginTop:2},children:[r.usage_count," uses · ",on(r.tokens_served)," served"]})]}),i.jsxs("span",{style:{textAlign:"right",fontFamily:"var(--mono)"},children:[i.jsx("span",{style:{display:"block",color:"var(--green)",fontSize:11},children:on(r.proven_tokens_saved)}),i.jsx("span",{style:{display:"block",color:"var(--ink3)",fontSize:9},children:On(r.realized_cost_saved_usd)})]})]},r.context_id)):i.jsx("div",{style:{borderTop:"1px solid var(--border)",paddingTop:7,fontSize:11,color:"var(--ink3)",lineHeight:1.4},children:"No context has been injected yet."})}),i.jsx("div",{style:{borderTop:"1px solid var(--border)",marginTop:8,paddingTop:6,color:"var(--ink3)",fontSize:10,lineHeight:1.35},children:"Per-context dollars are shown only when Dhee has direct attribution."})]})}function Ag({rows:e,totalTokens:t,totalCost:n,apiValue:r,totalSessions:o,budget:l}){return i.jsxs("div",{style:{border:"1px solid var(--border)",background:"white",borderRadius:4,padding:8},children:[i.jsxs("div",{style:{display:"flex",alignItems:"baseline",justifyContent:"space-between",gap:8},children:[i.jsx("span",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",letterSpacing:"0.04em",textTransform:"uppercase"},children:"Budget-capped savings"}),i.jsx("span",{style:{fontSize:14,fontWeight:700,color:"var(--green)"},children:On(n)})]}),i.jsxs("div",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)",marginTop:2},children:[on(t)," input tokens avoided across ",o," sessions"]}),i.jsxs("div",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",marginTop:2,lineHeight:1.35},children:["API value ",On(r),l!=null&&l.monthly_budget_usd?`; monthly cap ${On(l.monthly_budget_usd)}`:""]}),i.jsx("div",{style:{display:"grid",gap:5,marginTop:8},children:e.length?e.map(s=>{const a=Og(s);return i.jsxs("div",{style:{display:"grid",gridTemplateColumns:"minmax(0, 1fr) auto",gap:8,borderTop:"1px solid var(--border)",paddingTop:6},children:[i.jsxs("div",{style:{minWidth:0},children:[i.jsxs("div",{style:{display:"flex",alignItems:"center",gap:6,minWidth:0},children:[i.jsx("span",{style:{width:6,height:6,borderRadius:99,background:a,flexShrink:0}}),i.jsx("span",{style:{fontSize:11,color:"var(--ink)",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:$g(s)})]}),i.jsxs("div",{style:{fontFamily:"var(--mono)",fontSize:9,color:a,marginTop:2},children:[s.runtime||s.agent||"agent"," · ",s.router_calls," calls"]})]}),i.jsxs("div",{style:{textAlign:"right"},title:Fg(s),children:[i.jsx("div",{style:{fontSize:12,fontWeight:700,color:"var(--ink)"},children:On(s.estimated_cost_saved_usd)}),i.jsxs("div",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)"},children:[on(s.tokens_saved)," tok"]})]})]},s.session_id)}):i.jsx("div",{style:{borderTop:"1px solid var(--border)",paddingTop:7,fontSize:11,color:"var(--ink3)",lineHeight:1.4},children:"No session-level savings recorded yet."})})]})}function Au({treeId:e,buttonRef:t,focused:n,active:r,dot:o,title:l,meta:s,onFocus:a,onKeyDown:d,onClick:c}){return i.jsxs("button",{ref:t,type:"button","data-tree-id":e,role:"treeitem","aria-selected":r,tabIndex:n?0:-1,onFocus:a,onKeyDown:d,onClick:c,style:{textAlign:"left",border:`1px solid ${r?"var(--accent)":"transparent"}`,background:r?"var(--accent-dim)":"transparent",color:"var(--ink)",padding:"7px 8px",borderRadius:4,cursor:"pointer",display:"grid",gridTemplateColumns:"8px minmax(0, 1fr)",gap:8,alignItems:"start",outline:n?"2px solid var(--accent-dim)":"none"},children:[i.jsx("span",{style:{width:6,height:6,borderRadius:99,background:o,marginTop:5}}),i.jsxs("span",{style:{minWidth:0},children:[i.jsx("span",{style:{display:"block",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap",fontSize:12},children:l}),i.jsx("span",{style:{display:"block",fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",marginTop:2},children:s})]})]})}function Qi({label:e,onClick:t,tone:n="default",busy:r}){const o=n==="accent"?"var(--accent)":n==="indigo"?"var(--indigo)":n==="green"?"var(--green)":n==="rose"?"var(--rose)":"var(--ink2)";return i.jsx("button",{onClick:t,disabled:r,style:{padding:"7px 10px",border:`1px solid ${o}`,background:n==="default"?"white":"var(--surface)",color:o,fontFamily:"var(--mono)",fontSize:10,letterSpacing:"0.06em",borderRadius:3,cursor:r?"wait":"pointer"},children:r?"...":e})}function _n({label:e,children:t}){return i.jsxs("section",{style:{display:"grid",gap:8},children:[i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:9,letterSpacing:"0.08em",color:"var(--ink3)",textTransform:"uppercase"},children:e}),i.jsx("div",{style:{display:"flex",flexWrap:"wrap",gap:6},children:t})]})}function $r({children:e,color:t="var(--ink2)"}){return i.jsx("span",{style:{display:"inline-flex",border:"1px solid var(--border)",background:"var(--surface)",color:t,padding:"3px 7px",borderRadius:3,fontFamily:"var(--mono)",fontSize:10},children:e})}function Ge({k:e,v:t}){return t==null||t===""?null:i.jsxs("div",{style:{display:"grid",gridTemplateColumns:"76px minmax(0, 1fr)",gap:6,width:"100%",fontSize:12},children:[i.jsx("span",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)"},children:e}),i.jsx("span",{style:{color:"var(--ink2)",wordBreak:"break-word"},children:String(t)})]})}function Ug({projectIndex:e,memories:t,tokensSaved:n,onAddMemory:r,onSelectSession:o,onCreateWorkspace:l,onLaunchSession:s,onOpenWorkspace:a,onOpenTasks:d}){var ie;const[c,g]=v.useState(""),[m,x]=v.useState("task"),[S,E]=v.useState("codex"),[T,H]=v.useState("standard"),[h,u]=v.useState(""),[f,p]=v.useState(""),[j,_]=v.useState(!1),[P,z]=v.useState(""),[Q,R]=v.useState(!1),[J,X]=v.useState(null),K=v.useRef(null);v.useEffect(()=>{var D;(D=K.current)==null||D.focus()},[]);const G=(e==null?void 0:e.workspaces)||[],M=v.useMemo(()=>G.find(D=>D.id===(h||(e==null?void 0:e.currentWorkspaceId)))||G[0]||null,[e==null?void 0:e.currentWorkspaceId,h,G]),V=v.useMemo(()=>{var D,fe;return((D=M==null?void 0:M.projects)==null?void 0:D.find(se=>se.id===(f||(e==null?void 0:e.currentProjectId))))||((fe=M==null?void 0:M.projects)==null?void 0:fe[0])||null},[M,e==null?void 0:e.currentProjectId,f]),ye=v.useMemo(()=>{var D,fe,se,me;return((D=V==null?void 0:V.sessions)==null?void 0:D.find($=>$.id===(e==null?void 0:e.currentSessionId)))||((fe=V==null?void 0:V.sessions)==null?void 0:fe[0])||((se=M==null?void 0:M.sessions)==null?void 0:se.find($=>$.id===(e==null?void 0:e.currentSessionId)))||((me=M==null?void 0:M.sessions)==null?void 0:me[0])||null},[V,M,e==null?void 0:e.currentSessionId]);v.useEffect(()=>{!h&&(e!=null&&e.currentWorkspaceId)&&u(e.currentWorkspaceId)},[e==null?void 0:e.currentWorkspaceId,h]),v.useEffect(()=>{!f&&(e!=null&&e.currentProjectId)&&p(e.currentProjectId)},[e==null?void 0:e.currentProjectId,f]),v.useEffect(()=>{M&&V&&!M.projects.some(D=>D.id===f)&&p(V.id)},[M,V,f]);const C=c.split(` +`).map(D=>D.trim()).filter(Boolean),A=async()=>{if(!(!C.length||Q)){R(!0),X(null);try{if(m==="memory")for(const D of C)await r(D);else for(const D of C)await s(D,S,M==null?void 0:M.id,S==="claude-code"?T:void 0,V==null?void 0:V.id);g("")}catch(D){X(String(D))}finally{R(!1)}}},W=async()=>{if(!(!P.trim()||Q)){R(!0),X(null);try{await l(P.trim()),z(""),_(!1)}catch(D){X(String(D))}finally{R(!1)}}},ee=D=>({padding:"6px 10px",border:"1px solid var(--border)",background:D?"var(--ink)":"white",color:D?"white":"var(--ink2)",fontFamily:"var(--mono)",fontSize:9});return i.jsxs("div",{style:{height:"100%",display:"flex",flexDirection:"column",overflow:"hidden"},children:[i.jsxs("div",{style:{height:48,borderBottom:"1px solid var(--border)",padding:"0 24px",display:"flex",alignItems:"center",justifyContent:"space-between",flexShrink:0},children:[i.jsx("div",{style:{display:"flex",gap:10,alignItems:"center",minWidth:0},children:i.jsxs("span",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)"},children:[(M==null?void 0:M.label)||(M==null?void 0:M.name)||"workspace",V?` / ${V.name}`:""]})}),i.jsxs("div",{style:{display:"flex",gap:18,alignItems:"center",fontFamily:"var(--mono)",fontSize:10},children:[i.jsx("button",{onClick:d,style:{color:"var(--ink3)"},children:"tasks"}),i.jsxs("span",{style:{color:"var(--ink3)"},children:[t," engrams"]})]})]}),i.jsxs("div",{style:{flex:1,overflow:"auto",padding:28,display:"grid",gridTemplateColumns:"minmax(0, 1fr) 280px",gap:24},children:[i.jsxs("div",{style:{border:"1px solid var(--border)",background:"transparent",padding:24,display:"flex",flexDirection:"column",minHeight:0},children:[i.jsxs("div",{style:{display:"flex",justifyContent:"space-between",gap:12,alignItems:"center",marginBottom:16,flexWrap:"wrap"},children:[i.jsxs("div",{style:{display:"flex",gap:8,alignItems:"center",flexWrap:"wrap"},children:[i.jsx("button",{onClick:()=>x("task"),style:ee(m==="task"),children:"create task"}),i.jsx("button",{onClick:()=>x("memory"),style:ee(m==="memory"),children:"save memory"}),m==="task"&&i.jsxs(i.Fragment,{children:[i.jsx("button",{onClick:()=>E("codex"),style:ee(S==="codex"),children:"codex"}),i.jsx("button",{onClick:()=>E("claude-code"),style:ee(S==="claude-code"),children:"claude-code"}),S==="claude-code"&&i.jsxs(i.Fragment,{children:[i.jsx("button",{onClick:()=>H("standard"),style:ee(T==="standard"),children:"standard"}),i.jsx("button",{onClick:()=>H("full-access"),style:ee(T==="full-access"),children:"full access"})]})]})]}),i.jsx("button",{onClick:a,style:{padding:"7px 10px",border:"1px solid var(--border)",fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)"},children:"open workspace"})]}),m==="task"&&i.jsxs("div",{style:{display:"grid",gap:10,marginBottom:14},children:[i.jsxs("div",{style:{display:"flex",gap:8,alignItems:"center",flexWrap:"wrap"},children:[i.jsx("span",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)"},children:"workspace"}),i.jsx("select",{value:(M==null?void 0:M.id)||"",onChange:D=>u(D.target.value),style:{border:"1px solid var(--border)",padding:"7px 10px",background:"white",minWidth:220},children:G.map(D=>i.jsx("option",{value:D.id,children:D.label||D.name},D.id))})]}),(ie=M==null?void 0:M.projects)!=null&&ie.length?i.jsxs("div",{style:{display:"flex",gap:8,alignItems:"center",flexWrap:"wrap"},children:[i.jsx("span",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)"},children:"project"}),M.projects.map(D=>i.jsx("button",{onClick:()=>p(D.id),style:{...ee(D.id===(V==null?void 0:V.id)),background:D.id===(V==null?void 0:V.id)?"var(--accent)":"white"},children:D.name},D.id))]}):null]}),i.jsx("textarea",{ref:K,value:c,onChange:D=>g(D.target.value),onKeyDown:D=>{(D.metaKey||D.ctrlKey)&&D.key==="Enter"&&(D.preventDefault(),A())},placeholder:m==="task"?`- broadcast backend contract changes +- compare project scope rules +- create follow-up task for frontend stream`:`- backend project now emits model version updates +- paper asset is chunked and queryable +- avoid reprocessing shared results`,style:{width:"100%",flex:1,border:"1px solid var(--border)",padding:"18px 20px",fontSize:22,lineHeight:1.55,background:"white",resize:"none",minHeight:420}}),i.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginTop:14},children:[i.jsxs("div",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)"},children:[C.length," point",C.length===1?"":"s"]}),i.jsx("button",{onClick:()=>void A(),style:{padding:"10px 16px",border:"1px solid var(--ink)",background:"var(--ink)",color:"white",fontFamily:"var(--mono)",fontSize:10},children:Q?"saving…":m==="task"?"create task":"save memory"})]}),J&&i.jsx("div",{style:{marginTop:12,fontSize:11,color:"var(--rose)"},children:J})]}),i.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:12},children:[i.jsxs("div",{onClick:a,style:{border:"1px solid var(--border)",background:"white",padding:16,cursor:"pointer"},children:[i.jsx("div",{style:{marginBottom:8,fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)"},children:"CURRENT WORKSPACE"}),i.jsx("div",{style:{fontSize:16,fontWeight:600},children:(M==null?void 0:M.label)||(M==null?void 0:M.name)||"No workspace"}),i.jsx("div",{style:{marginTop:6,fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)"},children:(M==null?void 0:M.workspacePath)||"Select or create a workspace"}),V&&i.jsxs("div",{style:{marginTop:10,fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)"},children:[V.name," · default ",V.defaultRuntime||"codex"]}),ye&&i.jsx("button",{onClick:D=>{D.stopPropagation(),o(ye.id,ye.taskId||null)},style:{marginTop:12,padding:"7px 10px",border:"1px solid var(--border)",fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)"},children:"open current session"})]}),i.jsx("button",{onClick:()=>_(!0),style:{padding:"12px 14px",border:"1px solid var(--border)",background:"white",fontFamily:"var(--mono)",fontSize:10,textAlign:"left"},children:"+ add workspace"})]})]}),j&&i.jsx("div",{style:{position:"fixed",inset:0,background:"rgba(0,0,0,0.18)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:40},onClick:()=>_(!1),children:i.jsxs("div",{onClick:D=>D.stopPropagation(),style:{width:460,maxWidth:"calc(100vw - 32px)",border:"1px solid var(--border)",background:"white",padding:20},children:[i.jsx("div",{style:{marginBottom:14,fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)"},children:"ADD WORKSPACE"}),i.jsxs("div",{style:{display:"grid",gap:10},children:[i.jsx("input",{value:P,onChange:D=>z(D.target.value),placeholder:"Workspace name",style:{border:"1px solid var(--border)",padding:"11px 12px",background:"white"}}),i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",lineHeight:1.5},children:"A workspace is a collection of projects. Add projects and their folders after creating it."}),i.jsxs("div",{style:{display:"flex",justifyContent:"flex-end",gap:8,marginTop:4},children:[i.jsx("button",{onClick:()=>_(!1),style:{padding:"10px 12px",border:"1px solid var(--border)",background:"white",fontFamily:"var(--mono)",fontSize:10},children:"cancel"}),i.jsx("button",{onClick:()=>void W(),style:{padding:"10px 12px",border:"1px solid var(--ink)",background:"var(--ink)",color:"white",fontFamily:"var(--mono)",fontSize:10},children:Q?"creating…":"create workspace"})]})]})]})})]})}const el={fontFamily:"var(--mono)",fontSize:10,letterSpacing:"0.04em",textTransform:"uppercase"};function Mt(e){return Array.isArray(e)?e.filter(Boolean):[]}function Y(e,t,n=void 0){return!e||typeof e!="object"?n:e[t]??n}function ze(e){const t=Number(e||0);return new Intl.NumberFormat("en",{notation:Math.abs(t)>=1e4?"compact":"standard",maximumFractionDigits:Math.abs(t)>=1e4?1:0}).format(t)}function Wg(e){const t=String(e||"");if(!t)return"not linked";const n=t.split("/").filter(Boolean);return n.length>3?`.../${n.slice(-3).join("/")}`:t}function _a(e){if(!e)return"no timestamp";const t=new Date(e).getTime();if(Number.isNaN(t))return String(e);const n=Date.now()-t;return n<6e4?"just now":n<36e5?`${Math.floor(n/6e4)}m ago`:n<864e5?`${Math.floor(n/36e5)}h ago`:`${Math.floor(n/864e5)}d ago`}function $o(e){const t=String(e||"").toLowerCase();return t.includes("reject")||t.includes("fail")||t.includes("stale")?"var(--rose)":t.includes("pending")||t.includes("candidate")||t.includes("derived")?"var(--accent)":t.includes("promoted")||t.includes("active")||t.includes("ok")?"var(--green)":t.includes("evidence")||t.includes("digest")?"var(--indigo)":"var(--ink3)"}function Ci(e){const[t,n]=v.useState(null),[r,o]=v.useState(""),[l,s]=v.useState(!0),a=async()=>{s(!0),o("");try{n(await e())}catch(d){o(String(d))}finally{s(!1)}};return v.useEffect(()=>{a()},[]),{data:t,error:r,loading:l,refresh:a}}function bi({eyebrow:e,title:t,subtitle:n,children:r,action:o}){return i.jsx("div",{style:{height:"100%",overflowY:"auto",background:"var(--surface)"},children:i.jsxs("div",{style:{minHeight:"100%",padding:24,display:"flex",flexDirection:"column",gap:18},children:[i.jsxs("div",{style:{display:"flex",alignItems:"flex-start",justifyContent:"space-between",gap:20,borderBottom:"1px solid var(--border)",paddingBottom:18},children:[i.jsxs("div",{style:{maxWidth:760},children:[i.jsx("div",{style:{...el,color:"var(--accent)",marginBottom:8},children:e}),i.jsx("h1",{style:{fontSize:30,lineHeight:1.08,fontWeight:700,letterSpacing:0},children:t}),i.jsx("p",{style:{marginTop:8,color:"var(--ink2)",fontSize:14,lineHeight:1.55},children:n})]}),o]}),r]})})}function He({label:e,children:t,style:n}){return i.jsxs("section",{style:{background:"white",border:"1px solid var(--border)",padding:16,minWidth:0,...n},children:[e?i.jsx("div",{style:{...el,color:"var(--ink3)",marginBottom:12},children:e}):null,t]})}function Ye({label:e,value:t,tone:n}){return i.jsxs(He,{style:{minHeight:94},children:[i.jsx("div",{style:{fontSize:26,lineHeight:1,fontWeight:700,color:n||"var(--ink)"},children:t}),i.jsx("div",{style:{marginTop:8,color:"var(--ink3)",fontSize:12},children:e})]})}function tt({children:e,tone:t}){return i.jsx("span",{style:{...el,display:"inline-flex",alignItems:"center",minHeight:22,padding:"3px 8px",color:t||"var(--ink2)",background:"var(--surface2)",border:"1px solid var(--border)"},children:e})}function tl({rows:e,empty:t,render:n}){return e.length?i.jsx("div",{style:{display:"grid",gap:10},children:e.map(n)}):i.jsx("div",{style:{color:"var(--ink3)",fontSize:13},children:t})}function _i({loading:e,error:t}){return e?i.jsx(He,{children:"Loading Dhee state..."}):t?i.jsx(He,{children:i.jsx("span",{style:{color:"var(--rose)"},children:t})}):null}function Hg({onNavigate:e}){const{data:t,error:n,loading:r,refresh:o}=Ci(B.commandCenter),l=Y(t,"router",{}),s=Y(t,"context",{}),a=Y(t,"learnings",{}),d=Y(t,"inbox",{}),c=Y(t,"active_task",null),g=Mt(Y(t,"router_sessions",[])),m=Y(a,"totals",{}),x=Y(d,"totals",{});return i.jsxs(bi,{eyebrow:"COMMAND CENTER",title:"The current truth before the agent sees anything.",subtitle:"Start here to see task continuity, context health, routed savings, review queues, and the next best action for this repo.",action:i.jsx("button",{onClick:o,style:At,children:"refresh"}),children:[i.jsx(_i,{loading:r,error:n}),t?i.jsxs(i.Fragment,{children:[i.jsxs("div",{style:{display:"grid",gridTemplateColumns:"repeat(4, minmax(0, 1fr))",gap:12},children:[i.jsx(Ye,{label:"tokens avoided",value:ze(Y(l,"sessionTokensSaved",0)),tone:"var(--green)"}),i.jsx(Ye,{label:"router calls",value:ze(Y(l,"totalCalls",0)),tone:"var(--accent)"}),i.jsx(Ye,{label:"repo context",value:ze(Y(Y(s,"totals",{}),"repo_entries",0)),tone:"var(--indigo)"}),i.jsx(Ye,{label:"learning candidates",value:ze(Y(m,"candidate",0)),tone:"var(--accent)"})]}),i.jsxs("div",{style:{display:"grid",gridTemplateColumns:"1.1fr 0.9fr",gap:14},children:[i.jsx(He,{label:"ACTIVE WORK",children:c?i.jsxs("div",{children:[i.jsx("div",{style:{fontSize:22,lineHeight:1.2,fontWeight:700},children:String(Y(c,"title","Active task"))}),i.jsxs("div",{style:{marginTop:8,display:"flex",gap:8,flexWrap:"wrap"},children:[i.jsx(tt,{tone:$o(Y(c,"status")),children:String(Y(c,"status","active"))}),i.jsx(tt,{children:String(Y(c,"harness","agent"))})]})]}):i.jsx("div",{style:{color:"var(--ink3)"},children:"No active task yet. Start from a linked repo to let Dhee compile state."})}),i.jsxs(He,{label:"NEXT ACTION",children:[i.jsx("div",{style:{fontSize:18,fontWeight:650,lineHeight:1.35},children:String(Y(t,"next_action","Start a routed agent task"))}),i.jsxs("div",{style:{marginTop:14,display:"flex",gap:8,flexWrap:"wrap"},children:[i.jsx("button",{onClick:()=>e("handoff"),style:At,children:"handoff"}),i.jsx("button",{onClick:()=>e("router"),style:Do,children:"firewall"}),i.jsx("button",{onClick:()=>e("learnings"),style:Do,children:"learnings"})]})]})]}),i.jsxs("div",{style:{display:"grid",gridTemplateColumns:"1fr 1fr 1fr",gap:14},children:[i.jsx(He,{label:"LIVE SESSIONS",children:i.jsx(tl,{rows:g.slice(0,5),empty:"No routed sessions yet.",render:S=>i.jsx(Hr,{title:String(S.title||S.session_id||"session"),meta:`${S.agent||S.runtime||"agent"} - ${ze(S.tokens_saved)} tokens`,tone:$o(S.state)},String(S.session_id))})}),i.jsxs(He,{label:"REVIEW QUEUE",children:[i.jsx(Hr,{title:"proposals",meta:ze(Y(x,"proposals",0)),tone:"var(--accent)"}),i.jsx(Hr,{title:"findings",meta:ze(Y(x,"findings",0)),tone:"var(--rose)"}),i.jsx(Hr,{title:"conflicts",meta:ze(Y(x,"conflicts",0)),tone:"var(--indigo)"})]}),i.jsxs(He,{label:"ADDRESSABLE CONTEXT",children:[(Mt(Y(t,"dhee_aliases",[])).length,null),(Y(t,"dhee_aliases",[])||[]).map(S=>i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:11,padding:"5px 0",color:"var(--ink2)"},children:S},S))]})]})]}):null]})}function Vg(){const{data:e,error:t,loading:n,refresh:r}=Ci(B.handoffUi),o=Y(e,"continuity",{}),l=Y(o,"last_session",{})||{},s=Mt(Y(e,"tasks",[])),a=Mt(Y(e,"sessions",[])),d=Mt(Y(l,"files_touched",Y(l,"filesTouched",[]))),c=Mt(Y(l,"decisions",[])),g=Mt(Y(l,"todos",[]));return i.jsxs(bi,{eyebrow:"HANDOFF HUB",title:"Resume without replaying the transcript.",subtitle:"Dhee turns the latest work into task state: decisions, files, blockers, commands, tests, resume confidence, and the next step.",action:i.jsx("button",{onClick:r,style:At,children:"refresh"}),children:[i.jsx(_i,{loading:n,error:t}),e?i.jsxs("div",{style:{display:"grid",gridTemplateColumns:"1.1fr 0.9fr",gap:14},children:[i.jsxs(He,{label:"LATEST HANDOFF",children:[i.jsx("div",{style:{fontSize:24,lineHeight:1.15,fontWeight:700},children:String(Y(l,"task_summary","No handoff saved yet"))}),i.jsxs("div",{style:{marginTop:12,display:"flex",gap:8,flexWrap:"wrap"},children:[i.jsxs(tt,{tone:"var(--green)",children:["confidence ",Math.round(Number(Y(e,"resume_confidence",0))*100),"%"]}),i.jsx(tt,{children:_a(Y(l,"updated")||Y(l,"ended_at"))}),i.jsx(tt,{children:String(Y(l,"agent_id",Y(l,"source","dhee")))})]}),i.jsx("pre",{style:mp,children:String(Y(e,"command",""))})]}),i.jsx(He,{label:"RESUME INVENTORY",children:i.jsx(Jg,{rows:[["tasks",s.length],["sessions",a.length],["files",d.length],["decisions",c.length],["todos",g.length]]})}),i.jsx(He,{label:"DECISIONS",style:{gridColumn:"span 1"},children:i.jsx(Uu,{rows:c,empty:"No decisions captured yet."})}),i.jsx(He,{label:"FILES TOUCHED",children:i.jsx(Uu,{rows:d.map(m=>Wg(String(m))),empty:"No files in the latest handoff."})})]}):null]})}function Kg(){const{data:e,error:t,loading:n,refresh:r}=Ci(()=>B.proofReplay(120)),o=Mt(Y(e,"items",[])),l=Y(e,"totals",{});return i.jsxs(bi,{eyebrow:"PROOF REPLAY",title:"Replay the context decisions, not just the chat.",subtitle:"See the expansion trace: what Dhee digested, hid, expanded, injected, promoted, rejected, or derived from local records.",action:i.jsx("button",{onClick:r,style:At,children:"refresh"}),children:[i.jsx(_i,{loading:n,error:t}),i.jsxs("div",{style:{display:"grid",gridTemplateColumns:"repeat(4, minmax(0, 1fr))",gap:12},children:[i.jsx(Ye,{label:"events",value:ze(Y(l,"events",o.length))}),i.jsx(Ye,{label:"digests",value:ze(Y(l,"digests",0)),tone:"var(--green)"}),i.jsx(Ye,{label:"expansion trace",value:ze(Y(l,"expansions",0)),tone:"var(--accent)"}),i.jsx(Ye,{label:"evidence",value:ze(Y(l,"evidence",0)),tone:"var(--indigo)"}),i.jsx(Ye,{label:"derived rows",value:ze(Y(l,"derived",0))})]}),i.jsx(He,{label:"DECISION TIMELINE",children:i.jsx(tl,{rows:o,empty:"No context decisions recorded yet.",render:(s,a)=>i.jsx(Xg,{index:a,title:String(s.title||"Decision"),meta:`${s.source||"dhee"} - ${_a(s.time)}`,detail:String(s.detail||""),kind:String(s.kind||"event"),derived:!!s.derived},String(s.id||a))})})]})}function Qg(){const{data:e,error:t,loading:n,refresh:r}=Ci(()=>B.learningsUi(160)),[o,l]=v.useState(""),s=Mt(Y(e,"items",[])),a=Y(e,"totals",{}),d=async(c,g)=>{l(c);try{g==="promote"?await B.promoteLearning(c,{approved_by:"dhee-ui"}):await B.rejectLearning(c,{reason:"rejected in Dhee UI"}),await r()}finally{l("")}};return i.jsxs(bi,{eyebrow:"LEARNING INBOX",title:"Only evidence-backed learnings get promoted.",subtitle:"Clear pending review candidates from agent work. Dhee should learn from success, avoided failure, repeated utility, or explicit approval.",action:i.jsx("button",{onClick:r,style:At,children:"refresh"}),children:[i.jsx(_i,{loading:n,error:t}),i.jsxs("div",{style:{display:"grid",gridTemplateColumns:"repeat(4, minmax(0, 1fr))",gap:12},children:[i.jsx(Ye,{label:"candidates",value:ze(Y(a,"candidate",0)),tone:"var(--accent)"}),i.jsx(Ye,{label:"promoted",value:ze(Y(a,"promoted",0)),tone:"var(--green)"}),i.jsx(Ye,{label:"rejected",value:ze(Y(a,"rejected",0)),tone:"var(--rose)"}),i.jsx(Ye,{label:"all learnings",value:ze(Y(a,"all",s.length))})]}),i.jsx(He,{label:"LEARNING REVIEW",children:i.jsx(tl,{rows:s,empty:"No learning candidates yet.",render:c=>{const g=String(c.id||""),m=String(c.status||"candidate");return i.jsxs("div",{style:Zg,children:[i.jsxs("div",{style:{minWidth:0},children:[i.jsxs("div",{style:{display:"flex",gap:8,flexWrap:"wrap",marginBottom:8},children:[i.jsx(tt,{tone:$o(m),children:m}),i.jsx(tt,{children:String(c.evidence_gate||"needs approval")}),i.jsx(tt,{children:String(c.source_harness||c.source_agent_id||"agent")})]}),i.jsx("div",{style:{fontSize:17,fontWeight:700,lineHeight:1.25},children:String(c.title||g)}),i.jsx("div",{style:{marginTop:6,color:"var(--ink2)",lineHeight:1.55},children:String(c.body||"")})]}),i.jsxs("div",{style:{display:"flex",gap:8,flexShrink:0},children:[i.jsx("button",{disabled:!g||o===g||m==="promoted",onClick:()=>d(g,"promote"),style:At,children:"promote"}),i.jsx("button",{disabled:!g||o===g||m==="rejected",onClick:()=>d(g,"reject"),style:Do,children:"reject"})]})]},g)}})})]})}function Yg(){const{data:e,error:t,loading:n,refresh:r}=Ci(B.portabilityUi),[o,l]=v.useState(!1),[s,a]=v.useState(""),[d,c]=v.useState(null),[g,m]=v.useState(""),x=Y(e,"counts",{}),S=Mt(Y(e,"packs",[])),E=(Y(e,"contract",[])||[]).filter(Boolean),T=async()=>{l(!0),m("");try{await B.exportPackUi({}),await r()}catch(h){m(String(h))}finally{l(!1)}},H=async()=>{m(""),c(null);try{c(await B.importPackDryRunUi({input_path:s}))}catch(h){m(String(h))}};return i.jsxs(bi,{eyebrow:"PORTABILITY & TRUST",title:"Local memory should be inspectable, signed, and movable.",subtitle:"Dhee keeps export/import as a product surface, not an afterthought. No lock-in tricks, no hidden hosted dependency.",action:i.jsx("button",{onClick:r,style:At,children:"refresh"}),children:[i.jsx(_i,{loading:n,error:t}),i.jsxs("div",{style:{display:"grid",gridTemplateColumns:"repeat(4, minmax(0, 1fr))",gap:12},children:[i.jsx(Ye,{label:"memories",value:ze(Y(x,"memories",0))}),i.jsx(Ye,{label:"artifacts",value:ze(Y(x,"artifacts",0)),tone:"var(--indigo)"}),i.jsx(Ye,{label:"repo context",value:ze(Y(x,"repo_context_entries",0)),tone:"var(--green)"}),i.jsx(Ye,{label:"packs found",value:ze(S.length),tone:"var(--accent)"})]}),g?i.jsx(He,{children:i.jsx("span",{style:{color:"var(--rose)"},children:g})}):null,i.jsxs("div",{style:{display:"grid",gridTemplateColumns:"0.9fr 1.1fr",gap:14},children:[i.jsxs(He,{label:"PORTABLE SUBSTRATE",children:[i.jsx("div",{style:{display:"flex",gap:8,flexWrap:"wrap"},children:E.map(h=>i.jsx(tt,{tone:"var(--green)",children:h},h))}),i.jsx("button",{disabled:o,onClick:T,style:{...At,marginTop:16},children:o?"exporting...":"export .dheemem"})]}),i.jsxs(He,{label:"IMPORT DRY RUN",children:[i.jsxs("div",{style:{display:"flex",gap:10},children:[i.jsx("input",{value:s,onChange:h=>a(h.target.value),placeholder:"/path/to/backup.dheemem",style:qg}),i.jsx("button",{disabled:!s.trim(),onClick:H,style:At,children:"dry run"})]}),d?i.jsx("pre",{style:mp,children:JSON.stringify(Y(d,"result",d),null,2)}):null]})]}),i.jsx(He,{label:"RECENT PACKS",children:i.jsx(tl,{rows:S,empty:"No .dheemem packs found yet.",render:h=>i.jsx(Hr,{title:String(h.name||h.path),meta:`${h.verified?"verified":"unverified"} - ${ze(Number(h.size_bytes||0))} bytes - ${_a(h.updated_at)}`,tone:h.verified?"var(--green)":"var(--accent)"},String(h.path))})})]})}function Ev({onOpenContext:e}){return i.jsxs("div",{style:{position:"absolute",left:68,top:14,zIndex:8,display:"flex",gap:10,alignItems:"center",pointerEvents:"auto"},children:[i.jsx(tt,{tone:"var(--green)",children:"REPO BRAIN"}),i.jsx(tt,{children:"dhee://state/current"}),i.jsx(tt,{children:"dhee://handoff/latest"}),e?i.jsx("button",{onClick:e,style:Do,children:"context vault"}):null]})}function Uu({rows:e,empty:t}){return e.length?i.jsx("div",{style:{display:"grid",gap:8},children:e.map((n,r)=>i.jsx("div",{style:{padding:"8px 0",borderBottom:"1px solid var(--border)",color:"var(--ink2)"},children:String(n)},r))}):i.jsx("div",{style:{color:"var(--ink3)"},children:t})}function Jg({rows:e}){return i.jsx("div",{style:{display:"grid",gap:8},children:e.map(([t,n])=>i.jsxs("div",{style:{display:"flex",justifyContent:"space-between",gap:20},children:[i.jsx("span",{style:{color:"var(--ink3)"},children:t}),i.jsx("strong",{children:ze(n)})]},t))})}function Hr({title:e,meta:t,tone:n}){return i.jsxs("div",{style:{display:"flex",gap:10,alignItems:"flex-start",padding:"7px 0",borderBottom:"1px solid var(--border)"},children:[i.jsx("span",{style:{width:8,height:8,marginTop:6,background:n||"var(--ink3)",flexShrink:0}}),i.jsxs("div",{style:{minWidth:0},children:[i.jsx("div",{style:{fontSize:13,fontWeight:650,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:e}),i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)",marginTop:2},children:t})]})]})}function Xg({index:e,title:t,meta:n,detail:r,kind:o,derived:l}){return i.jsxs("div",{style:{display:"grid",gridTemplateColumns:"44px 1fr",gap:14,padding:"12px 0",borderBottom:"1px solid var(--border)"},children:[i.jsx("div",{style:{...el,color:"var(--ink3)"},children:String(e+1).padStart(2,"0")}),i.jsxs("div",{children:[i.jsxs("div",{style:{display:"flex",gap:8,flexWrap:"wrap",marginBottom:6},children:[i.jsx(tt,{tone:$o(o),children:o}),l?i.jsx(tt,{tone:"var(--accent)",children:"derived"}):i.jsx(tt,{tone:"var(--green)",children:"recorded"}),i.jsx(tt,{children:n})]}),i.jsx("div",{style:{fontSize:16,fontWeight:700},children:t}),r?i.jsx("div",{style:{marginTop:4,color:"var(--ink2)",lineHeight:1.55},children:r}):null]})]})}const At={border:"1px solid var(--ink)",background:"var(--ink)",color:"white",padding:"8px 12px",fontFamily:"var(--mono)",fontSize:10,letterSpacing:"0.04em",textTransform:"uppercase"},Do={...At,color:"var(--ink)",background:"white",borderColor:"var(--border2)"},qg={minHeight:36,flex:1,border:"1px solid var(--border2)",background:"white",padding:"0 10px",fontFamily:"var(--mono)",fontSize:11},mp={marginTop:14,border:"1px solid var(--border)",background:"var(--surface2)",padding:12,fontFamily:"var(--mono)",fontSize:11,whiteSpace:"pre-wrap",overflowX:"auto"},Zg={display:"grid",gridTemplateColumns:"1fr auto",gap:18,padding:"14px 0",borderBottom:"1px solid var(--border)",alignItems:"start"},Gg={border:"1px solid var(--border)",background:"var(--bg)",borderRadius:8,padding:18,width:"min(760px, 100%)",boxSizing:"border-box",display:"flex",flexWrap:"wrap",gap:18,boxShadow:"0 10px 28px rgba(20,16,10,0.06)"},Wu={fontFamily:"var(--mono)",fontSize:9,letterSpacing:"0.08em",textTransform:"uppercase",color:"var(--ink3)"},ev={borderRadius:5,cursor:"pointer",fontFamily:"var(--mono)",fontSize:10,padding:"8px 11px",whiteSpace:"nowrap"};function tv(e="secondary",t){const n=e==="primary";return{...ev,border:`1px solid ${n?"var(--ink)":"var(--border)"}`,background:n?"var(--ink)":"white",color:n?"white":"var(--accent)",opacity:t?.55:1,cursor:t?"not-allowed":"pointer"}}function hp({title:e="Set up a developer workspace",eyebrow:t="First run",body:n="Connect a repo folder, then start Codex or Claude Code from that folder so Dhee can mirror sessions and context.",actions:r=[],commands:o=["dhee onboard --root .","dhee doctor"],aside:l}){return i.jsxs("section",{style:Gg,children:[i.jsxs("div",{style:{flex:"1 1 300px",minWidth:0},children:[i.jsx("div",{style:Wu,children:t}),i.jsx("h2",{style:{margin:"5px 0 7px",fontSize:22,lineHeight:1.15,color:"var(--ink)",letterSpacing:0},children:e}),i.jsx("div",{style:{color:"var(--ink2)",fontSize:12.5,lineHeight:1.55,maxWidth:680},children:n}),r.length?i.jsx("div",{style:{display:"flex",gap:8,flexWrap:"wrap",marginTop:14},children:r.map(s=>i.jsx("button",{type:"button",onClick:s.onClick,disabled:s.disabled,style:tv(s.tone,s.disabled),children:s.label},s.label))}):null]}),i.jsxs("div",{style:{flex:"1 1 260px",border:"1px solid var(--border)",background:"var(--surface)",borderRadius:6,padding:12,minWidth:0},children:[i.jsx("div",{style:{...Wu,marginBottom:8},children:"Terminal path"}),i.jsx("div",{style:{display:"grid",gap:7},children:o.map(s=>i.jsx("code",{style:{display:"block",border:"1px solid var(--border)",background:"white",borderRadius:4,padding:"8px 9px",fontFamily:"var(--mono)",fontSize:10,color:"var(--ink)",lineHeight:1.45,overflowWrap:"anywhere"},children:s},s))}),l?i.jsx("div",{style:{marginTop:10},children:l}):null]})]})}const Dr=[{key:"day",label:"Daily",short:"24h",ms:24*60*60*1e3},{key:"week",label:"Weekly",short:"7d",ms:7*24*60*60*1e3},{key:"month",label:"Monthly",short:"30d",ms:30*24*60*60*1e3},{key:"year",label:"Yearly",short:"365d",ms:365*24*60*60*1e3}],Hu={items:[],next_cursor:null,active_only:!1,totals:{tokens_saved:0,estimated_cost_saved_usd:0,router_calls:0,sessions:0}};function vn(e){return e==null?"0":new Intl.NumberFormat("en",{notation:Math.abs(e)>=1e4?"compact":"standard",maximumFractionDigits:Math.abs(e)>=1e4?1:0}).format(e)}function Mn(e){return new Intl.NumberFormat("en-US").format(Math.max(0,Number(e||0)))}function Vr(e){const t=Math.max(0,Number(e||0));return t>0&&t<.01?"<$0.01":new Intl.NumberFormat("en-US",{style:"currency",currency:"USD",maximumFractionDigits:t>=100?0:2}).format(t)}function Xn(e){if(!e)return 0;const t=new Date(e).getTime();return Number.isNaN(t)?0:t}function Ea(e){const t=Xn(e);if(!t)return"n/a";const n=Date.now()-t;return n<6e4?"just now":n<36e5?`${Math.floor(n/6e4)}m ago`:n<864e5?`${Math.floor(n/36e5)}h ago`:`${Math.floor(n/864e5)}d ago`}function Ta(e){if(!e)return"n/a";const t=e.split("/").filter(Boolean);return t.length<=2?e:".../"+t.slice(-2).join("/")}function gp(e){var r;const t=typeof e=="string"?e:e.agent||e.runtime||((r=e.agents)==null?void 0:r[0])||"unknown",n=String(t||"").toLowerCase();return n.includes("codex")?"codex":n.includes("claude")?"claude-code":n||"unknown"}function nv(e){const t=gp(e||"");return t==="codex"?"Codex":t==="claude-code"?"Claude Code":e||"Unknown"}function za(e){const t=gp(e||"");return t==="codex"?"var(--indigo)":t==="claude-code"?"var(--accent)":"var(--ink3)"}function Ra(e){var t;return Number(((t=e.pricing)==null?void 0:t.input_cost_per_million)||0)>0}function Na(e){return e.tokens_saved>0&&!Ra(e)?"unpriced":Vr(e.estimated_cost_saved_usd)}function rv(e){const t=String(e.state||"").toLowerCase();return!!e.active&&(t==="active"||t==="running"||t==="live")}function vp(e){const t=e.pricing;if(!t||!Ra(e))return(t==null?void 0:t.note)||"No official provider/model rate mapped yet.";const n=t.provider||e.runtime||e.agent||"provider",r=t.model_family||e.model||"model";return`${n} ${r}: $${t.input_cost_per_million}/1M input tokens`}function yp(e,t){if(!e)return Number.POSITIVE_INFINITY;const r=Number(e[t==="day"?"daily_budget_usd":t==="week"?"weekly_budget_usd":t==="year"?"yearly_budget_usd":"monthly_budget_usd"]||0);return r>0?r:Number.POSITIVE_INFINITY}function Vu(e,t,n){return e.reduce((r,o)=>(r.tokens+=o.tokens_saved||0,r.apiValue+=Number(o.estimated_cost_saved_usd||0),r.calls+=o.router_calls||0,r.sessions+=1,r.cost=Math.min(r.apiValue,yp(t,n)),r),{tokens:0,apiValue:0,cost:0,calls:0,sessions:0})}function Ku(){const e=new URLSearchParams(window.location.search),t=String(e.get("view")||"").toLowerCase(),n=window.location.pathname.replace(/^\/+|\/+$/g,"").toLowerCase();return t==="router/sessionshistory"||t==="router/session-history"||t==="router/history"||n==="router/sessionshistory"?"history":"live"}function iv(e){const t=new URLSearchParams(window.location.search);t.set("view",e==="history"?"router/sessionshistory":"router");const n=t.toString(),r=`${window.location.pathname}${n?`?${n}`:""}${window.location.hash||""}`;window.history.pushState({},"",r),window.dispatchEvent(new Event("popstate"))}function ov({onOpenFolders:e,onOpenSetup:t}){return i.jsx("div",{style:{height:"100%",overflowY:"auto",background:"var(--surface)"},children:i.jsx(lv,{onOpenFolders:e,onOpenSetup:t})})}function lv({onOpenFolders:e,onOpenSetup:t}){const n=v.useRef(null),[r,o]=v.useState(Hu),[l,s]=v.useState(Hu),[a,d]=v.useState("week"),[c,g]=v.useState(()=>Ku()),[m,x]=v.useState(""),[S,E]=v.useState(!1),[T,H]=v.useState(null),h=async(K=!1)=>{K||E(!0),H(null);try{const[G,M]=await Promise.all([B.routerSessions({active:!1,limit:100}),B.routerSessions({active:!0,limit:50})]);o(G),s(M)}catch(G){H(String(G))}finally{K||E(!1)}};v.useEffect(()=>{h(!1);const K=window.setInterval(()=>void h(!0),15e3);return()=>window.clearInterval(K)},[]),v.useEffect(()=>{const K=()=>g(Ku());return window.addEventListener("popstate",K),()=>window.removeEventListener("popstate",K)},[]),v.useEffect(()=>{var K,G;(G=(K=n.current)==null?void 0:K.parentElement)==null||G.scrollTo({top:0,behavior:"auto"})},[c]);const u=v.useMemo(()=>[...l.items].filter(rv).sort((K,G)=>Xn(G.updated_at)-Xn(K.updated_at)),[l.items]),f=v.useMemo(()=>{const K=Dr.find(M=>M.key===a)||Dr[1],G=Date.now()-K.ms;return[...r.items].filter(M=>Xn(M.updated_at||M.started_at)>=G).sort((M,V)=>Xn(V.updated_at)-Xn(M.updated_at))},[r.items,a]),p=c==="history"?f.find(K=>K.session_id===m)||null:u.find(K=>K.session_id===m)||null,j=r.budget||l.budget,_=Vu(f,j,a),P=Vu(u,j,"day"),z=l.items.length>0||r.items.length>0,Q=Dr.find(K=>K.key===a)||Dr[1],R=yp(j,a),J=Number.isFinite(R)&&_.apiValue>R,X=K=>{g(K),iv(K)};return i.jsxs("div",{ref:n,style:{padding:"clamp(10px, 3vw, 18px)",display:"grid",gap:14,minWidth:0,width:"100%",boxSizing:"border-box"},children:[i.jsxs("section",{style:{border:"1px solid var(--border)",background:"var(--bg)",borderRadius:8,padding:16,minWidth:0,boxSizing:"border-box"},children:[i.jsxs("div",{style:{display:"flex",flexWrap:"wrap",gap:14,alignItems:"start",justifyContent:"space-between",marginBottom:14},children:[i.jsxs("div",{style:{flex:"1 1 240px",minWidth:0},children:[i.jsx(Hn,{children:"Context Firewall"}),i.jsx("h1",{style:{margin:"2px 0 0",fontSize:26,lineHeight:1.1,color:"var(--ink)",letterSpacing:0},children:c==="history"?"Firewall session history":"Live context firewall"}),i.jsx("p",{style:{margin:"6px 0 0",color:"var(--ink3)",fontSize:12,maxWidth:780},children:c==="history"?"Every completed and recent local agent session, with pointer-backed evidence and avoided raw context.":"Running Claude Code and Codex sessions, with raw output kept behind digests until the agent asks to expand."})]}),i.jsxs("div",{style:{display:"flex",gap:8,justifyContent:"flex-start",flexWrap:"wrap",flex:"0 1 auto"},children:[i.jsx("button",{onClick:()=>X(c==="history"?"live":"history"),style:{border:"1px solid var(--accent)",background:c==="history"?"white":"var(--accent-dim)",borderRadius:5,color:"var(--accent)",cursor:"pointer",fontFamily:"var(--mono)",fontSize:10,padding:"8px 11px"},children:c==="history"?"LIVE FIREWALL":"SESSION HISTORY"}),i.jsx("button",{onClick:()=>h(!1),disabled:S,style:{border:"1px solid var(--border)",background:"white",borderRadius:5,color:S?"var(--ink3)":"var(--accent)",cursor:S?"wait":"pointer",fontFamily:"var(--mono)",fontSize:10,padding:"8px 11px"},children:S?"SYNCING":"REFRESH"})]})]}),i.jsx("div",{style:{display:"flex",gap:7,flexWrap:"wrap",marginBottom:12},children:Dr.map(K=>{const G=K.key===a;return i.jsxs("button",{onClick:()=>d(K.key),style:{border:`1px solid ${G?"var(--accent)":"var(--border)"}`,background:G?"var(--accent-dim)":"white",color:G?"var(--accent)":"var(--ink2)",borderRadius:5,padding:"6px 10px",fontFamily:"var(--mono)",fontSize:10,cursor:"pointer"},children:[K.label,i.jsx("span",{style:{color:"var(--ink3)",marginLeft:6},children:K.short})]},K.key)})}),i.jsxs("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(118px, 1fr))",gap:10},children:[i.jsx(an,{label:`${Q.label} API value`,value:Vr(_.apiValue),sub:"official input-rate estimate",accent:"var(--green)"}),i.jsx(an,{label:"Budget-capped savings",value:Vr(_.cost),sub:J?`capped at ${Vr(R)}`:"same as API value for this range",accent:"var(--green)"}),i.jsx(an,{label:`${Q.label} raw tokens avoided`,value:vn(_.tokens),sub:`${Mn(_.tokens)} avoided input tokens`,accent:"var(--green)"}),i.jsx(an,{label:"Live governed sessions",value:Mn(P.sessions),sub:`${vn(P.tokens)} active-session savings`,accent:"var(--accent)"})]})]}),T?i.jsxs("div",{style:{border:"1px solid var(--rose)",background:"white",color:"var(--rose)",padding:"10px 12px",borderRadius:6,fontFamily:"var(--mono)",fontSize:11},children:["context firewall data unavailable: ",T]}):null,!S&&!T&&!z?i.jsx(hp,{body:"Point Dhee at a repo folder, then start an agent task from that folder. The context firewall will fill with digests, evidence pointers, and expansions after the first mirrored Codex or Claude Code run.",actions:[...e?[{label:"ADD REPO FOLDER",onClick:e,tone:"primary"}]:[],...t?[{label:"START TASK",onClick:t}]:[]]}):null,c==="history"?i.jsx(Qu,{title:"Session history",sub:`${f.length} sessions in the last ${Q.short} · ${vn(_.tokens)} tokens · ${Vr(_.apiValue)} API value`,action:i.jsx("button",{onClick:()=>X("live"),style:{border:"1px solid var(--border)",background:"white",borderRadius:5,color:"var(--accent)",cursor:"pointer",fontFamily:"var(--mono)",fontSize:10,padding:"7px 10px",whiteSpace:"nowrap"},children:"LIVE FIREWALL"}),children:i.jsx(av,{rows:f,selectedId:(p==null?void 0:p.session_id)||"",onSelect:x,loading:S})}):i.jsx(Qu,{title:"Live governed sessions",sub:`${u.length} active local agent session${u.length===1?"":"s"} · click a session to inspect routing, evidence, and savings`,action:i.jsx("button",{onClick:()=>X("history"),style:{border:"1px solid var(--border)",background:"white",borderRadius:5,color:"var(--accent)",cursor:"pointer",fontFamily:"var(--mono)",fontSize:10,padding:"7px 10px",whiteSpace:"nowrap"},children:"HISTORY"}),children:u.length===0?i.jsx(xp,{children:"No active Claude Code or Codex sessions detected."}):i.jsx("div",{style:{display:"grid",gap:8},children:u.map(K=>i.jsx(sv,{row:K,selected:(p==null?void 0:p.session_id)===K.session_id,onSelect:()=>x(G=>G===K.session_id?"":K.session_id)},K.session_id))})})]})}function sv({row:e,selected:t,onSelect:n}){const r=za(e.agent||e.runtime),o=e.live_usage;return i.jsxs("div",{style:{width:"100%",border:`1px solid ${t?r:"var(--border)"}`,background:t?"var(--surface)":"white",borderRadius:6,overflow:"hidden"},children:[i.jsx("button",{type:"button","aria-expanded":t,onClick:n,style:{width:"100%",textAlign:"left",background:"transparent",border:0,padding:12,cursor:"pointer"},children:i.jsxs("div",{style:{display:"grid",gridTemplateColumns:"minmax(0, 1fr) repeat(3, minmax(86px, max-content)) 22px",gap:16,alignItems:"center"},children:[i.jsxs("div",{style:{minWidth:0},children:[i.jsx(Pa,{agent:e.agent||e.runtime||"unknown"}),i.jsx("div",{style:{fontSize:15,fontWeight:600,color:"var(--ink)",whiteSpace:"nowrap",textOverflow:"ellipsis",overflow:"hidden",marginTop:4},title:e.title,children:e.title||e.session_id}),i.jsxs("div",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)",marginTop:3},title:e.cwd||e.repo_root,children:[Ta(e.repo_root||e.cwd)," - updated ",Ea(e.updated_at)]})]}),i.jsx(co,{label:"saved",value:vn(e.tokens_saved)}),i.jsx(co,{label:"API value",value:Na(e)}),i.jsx(co,{label:"live tokens",value:o!=null&&o.available?vn(o.total_tokens):"n/a"}),i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:18,lineHeight:1,color:t?r:"var(--ink3)",textAlign:"right"},"aria-hidden":"true",children:t?"-":"+"})]})}),t?i.jsx("div",{style:{borderTop:"1px solid var(--border)",padding:"12px 12px 14px",background:"white"},children:i.jsx(dv,{row:e,showHeader:!1})}):null]})}function av({rows:e,selectedId:t,onSelect:n,loading:r}){return e.length===0?i.jsx(xp,{children:r?"Loading sessions...":"No sessions in this range."}):i.jsx("div",{style:{border:"1px solid var(--border)",borderRadius:6,overflowX:"auto",background:"white"},children:i.jsxs("table",{style:{width:"100%",borderCollapse:"collapse",fontFamily:"var(--mono)",fontSize:11},children:[i.jsx("thead",{children:i.jsxs("tr",{style:{background:"var(--surface)"},children:[i.jsx(En,{children:"Session"}),i.jsx(En,{children:"Agent"}),i.jsx(En,{children:"State"}),i.jsx(En,{children:"Updated"}),i.jsx(En,{align:"right",children:"Tokens saved"}),i.jsx(En,{align:"right",children:"API value"}),i.jsx(En,{align:"right",children:"Calls"})]})}),i.jsx("tbody",{children:e.map(o=>{const l=t===o.session_id;return i.jsxs("tr",{onClick:()=>n(o.session_id),style:{borderTop:"1px solid var(--border)",background:l?"oklch(0.98 0.02 262)":"white",cursor:"pointer"},children:[i.jsxs(Tn,{title:o.title||o.session_id,children:[i.jsx("div",{style:{color:"var(--ink)",fontWeight:l?700:500,maxWidth:420,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:o.title||o.session_id}),i.jsx("div",{style:{color:"var(--ink3)",marginTop:2,maxWidth:420,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},title:o.cwd||o.repo_root,children:Ta(o.repo_root||o.cwd)})]}),i.jsx(Tn,{children:i.jsx(Pa,{agent:o.agent||o.runtime||"unknown"})}),i.jsx(Tn,{children:i.jsx(cv,{state:o.state,active:o.active})}),i.jsx(Tn,{children:Ea(o.updated_at)}),i.jsx(Tn,{align:"right",children:Mn(o.tokens_saved)}),i.jsx(Tn,{align:"right",title:vp(o),children:Na(o)}),i.jsx(Tn,{align:"right",children:Mn(o.router_calls)})]},o.session_id)})})]})})}function dv({row:e,showHeader:t=!0}){var o;const n=e.live_usage,r=Object.entries(e.tool_breakdown||{}).sort((l,s)=>s[1]-l[1]);return i.jsxs("div",{style:{display:"grid",gap:10},children:[t?i.jsxs("div",{children:[i.jsx(Pa,{agent:e.agent||e.runtime||"unknown"}),i.jsx("h2",{style:{margin:"6px 0 4px",fontSize:18,lineHeight:1.25,color:"var(--ink)",letterSpacing:0,display:"-webkit-box",WebkitLineClamp:3,WebkitBoxOrient:"vertical",overflow:"hidden"},title:e.title||e.session_id,children:e.title||e.session_id}),i.jsxs("div",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},title:e.cwd||e.repo_root||void 0,children:[e.model||"model unavailable"," · ",Ta(e.cwd||e.repo_root)]})]}):null,i.jsxs("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(150px, 1fr))",gap:8},children:[i.jsx(an,{label:"tokens saved",value:vn(e.tokens_saved),sub:`${Mn(e.tokens_saved)} avoided`,accent:"var(--green)"}),i.jsx(an,{label:"API value",value:Na(e),sub:Ra(e)?"API value":"model unpriced",accent:"var(--green)"}),i.jsx(an,{label:"router calls",value:Mn(e.router_calls),sub:"cached reads",accent:"var(--ink2)"}),i.jsx(an,{label:"live tokens",value:n!=null&&n.available?vn(n.total_tokens):"n/a",sub:n!=null&&n.available?"native telemetry":"not captured",accent:za(e.agent||e.runtime)})]}),i.jsxs("div",{style:{border:"1px solid var(--border)",borderRadius:6,padding:10,background:"white"},children:[i.jsx(Hn,{children:"Pricing"}),i.jsx("div",{style:{fontSize:12,color:"var(--ink)",marginTop:5},children:vp(e)}),(o=e.pricing)!=null&&o.source?i.jsx("a",{href:e.pricing.source,target:"_blank",rel:"noreferrer",style:{display:"inline-block",marginTop:7,fontFamily:"var(--mono)",fontSize:10,color:"var(--accent)"},children:"official pricing source"}):null]}),n!=null&&n.available?i.jsx(uv,{row:e}):null,i.jsxs("div",{style:{border:"1px solid var(--border)",borderRadius:6,padding:10,background:"white"},children:[i.jsx(Hn,{children:"Read savings by tool"}),r.length===0?i.jsx("div",{style:{color:"var(--ink3)",fontSize:12,marginTop:7},children:"No cached reads yet."}):i.jsx("div",{style:{display:"grid",gap:6,marginTop:8},children:r.map(([l,s])=>i.jsxs("div",{style:{display:"flex",justifyContent:"space-between",gap:10,fontFamily:"var(--mono)",fontSize:11},children:[i.jsx("span",{style:{color:"var(--ink2)"},children:l}),i.jsxs("span",{style:{color:"var(--ink)"},children:[Mn(s)," calls"]})]},l))})]})]})}function uv({row:e}){const t=e.live_usage;if(!(t!=null&&t.available))return i.jsxs("div",{style:{border:"1px solid var(--border)",borderRadius:6,padding:12,background:"white"},children:[i.jsx(Hn,{children:"Live token usage"}),i.jsx("div",{style:{color:"var(--ink3)",fontSize:12,marginTop:8},children:"No exact live token report captured for this session yet."})]});const n=[["Input",t.input_tokens],["Cached input",t.cached_input_tokens],["Output",t.output_tokens],["Reasoning",t.reasoning_output_tokens],["Last turn",t.last_turn_tokens],["Context",t.context_window]];return i.jsxs("div",{style:{border:"1px solid var(--border)",borderRadius:6,padding:12,background:"white"},children:[i.jsxs("div",{style:{display:"flex",justifyContent:"space-between",gap:12},children:[i.jsx(Hn,{children:"Live token usage"}),i.jsx("span",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--green)",whiteSpace:"nowrap"},children:"exact"})]}),i.jsx("div",{style:{display:"grid",gridTemplateColumns:"repeat(2, minmax(0, 1fr))",gap:9,marginTop:10},children:n.map(([r,o])=>i.jsx(co,{label:r,value:vn(o)},r))}),i.jsxs("div",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)",marginTop:10},children:[t.source||"native telemetry"," - updated ",Ea(t.updated_at||e.updated_at)]})]})}function Qu({title:e,sub:t,action:n,children:r}){return i.jsxs("section",{style:{border:"1px solid var(--border)",background:"var(--bg)",borderRadius:8,padding:16,minWidth:0,boxSizing:"border-box"},children:[i.jsxs("div",{style:{display:"flex",justifyContent:"space-between",gap:12,alignItems:"baseline",marginBottom:12},children:[i.jsxs("div",{style:{minWidth:0},children:[i.jsx(Hn,{children:e}),t?i.jsx("div",{style:{marginTop:4,color:"var(--ink3)",fontSize:12,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},title:t,children:t}):null]}),n?i.jsx("div",{style:{flexShrink:0},children:n}):null]}),r]})}function an({label:e,value:t,sub:n,accent:r}){return i.jsxs("div",{style:{border:"1px solid var(--border)",background:"white",borderRadius:6,padding:11,minWidth:0},children:[i.jsx(Hn,{children:e}),i.jsx("div",{style:{marginTop:7,fontFamily:"var(--mono)",fontSize:22,lineHeight:1.05,fontWeight:700,color:r,overflow:"hidden",textOverflow:"ellipsis"},children:t}),n?i.jsx("div",{style:{color:"var(--ink3)",fontSize:11,marginTop:4},children:n}):null]})}function co({label:e,value:t}){return i.jsxs("div",{style:{minWidth:80},children:[i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",textTransform:"uppercase",letterSpacing:"0.06em",marginBottom:3},children:e}),i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:14,fontWeight:700,color:"var(--ink)",whiteSpace:"nowrap"},children:t})]})}function Pa({agent:e}){const t=za(e);return i.jsxs("span",{style:{display:"inline-flex",alignItems:"center",gap:6,fontFamily:"var(--mono)",fontSize:10,color:t,textTransform:"uppercase",letterSpacing:"0.06em"},children:[i.jsx("span",{style:{width:7,height:7,borderRadius:999,background:t,flexShrink:0}}),nv(e)]})}function cv({state:e,active:t}){const n=t?"var(--green)":"var(--ink3)";return i.jsx("span",{style:{border:`1px solid ${n}`,color:n,borderRadius:4,padding:"1px 6px",fontSize:10},children:t?"active":e||"n/a"})}function xp({children:e}){return i.jsx("div",{style:{border:"1px dashed var(--border)",color:"var(--ink3)",background:"white",borderRadius:6,padding:18,textAlign:"center",fontSize:12},children:e})}function Hn({children:e}){return i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)",letterSpacing:"0.08em",textTransform:"uppercase",fontWeight:700},children:e})}function En({children:e,align:t}){return i.jsx("th",{style:{padding:"8px 10px",textAlign:t||"left",color:"var(--ink2)",fontWeight:700,letterSpacing:"0.04em",borderBottom:"1px solid var(--border)",whiteSpace:"nowrap"},children:e})}function Tn({children:e,align:t,title:n}){return i.jsx("td",{title:n,style:{padding:"8px 10px",textAlign:t||"left",color:"var(--ink2)",verticalAlign:"middle"},children:e})}function fv({tasks:e,projectIndex:t,onSelectTask:n,onSelectSession:r,tweaks:o}){var x,S,E,T,H,h,u,f;const l={green:"var(--green)",indigo:"var(--indigo)",orange:"var(--accent)",rose:"var(--rose)"},s=((x=t==null?void 0:t.workspaces)==null?void 0:x.find(p=>p.id===(t==null?void 0:t.currentWorkspaceId)))||((S=t==null?void 0:t.workspaces)==null?void 0:S[0])||null,a=((E=s==null?void 0:s.projects)==null?void 0:E.find(p=>p.id===(t==null?void 0:t.currentProjectId)))||((T=s==null?void 0:s.projects)==null?void 0:T[0])||null,d=((H=a==null?void 0:a.sessions)==null?void 0:H.find(p=>p.id===(t==null?void 0:t.currentSessionId)))||((h=a==null?void 0:a.sessions)==null?void 0:h[0])||((u=s==null?void 0:s.sessions)==null?void 0:u.find(p=>p.id===(t==null?void 0:t.currentSessionId)))||((f=s==null?void 0:s.sessions)==null?void 0:f[0])||null,c=e.find(p=>p.id===(d==null?void 0:d.taskId))||e[0]||null,g=e.filter(p=>p.id!==(c==null?void 0:c.id)),m=p=>i.jsxs("button",{onClick:()=>n(p.id),style:{display:"grid",gridTemplateColumns:"12px minmax(0, 1fr) 18px",alignItems:"center",gap:14,padding:"14px 0",borderBottom:"1px solid var(--border)",textAlign:"left",background:"transparent"},children:[i.jsx("span",{style:{width:10,height:10,background:l[p.color]||"var(--accent)",display:"inline-block"}}),i.jsxs("span",{style:{minWidth:0},children:[i.jsx("div",{style:{fontSize:16,fontWeight:560,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:p.title}),i.jsxs("div",{style:{marginTop:4,fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)",display:"flex",gap:10,flexWrap:"wrap"},children:[o.showTimestamps&&i.jsx("span",{children:p.created}),i.jsxs("span",{children:[p.messages.length," msgs"]}),p.harness&&i.jsx("span",{children:p.harness})]})]}),i.jsx("span",{style:{color:"var(--ink3)",fontSize:18},children:"→"})]},p.id);return i.jsxs("div",{style:{height:"100%",display:"flex",flexDirection:"column",overflow:"hidden"},children:[i.jsxs("div",{style:{height:48,borderBottom:"1px solid var(--border)",padding:"0 24px",display:"flex",alignItems:"center",justifyContent:"space-between",flexShrink:0},children:[i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:11,color:"var(--ink3)",letterSpacing:"0.08em"},children:"TASKS"}),i.jsxs("div",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)"},children:[e.length," tracked tasks"]})]}),i.jsxs("div",{style:{flex:1,overflow:"auto",padding:24,display:"grid",gap:20},children:[d&&i.jsxs("div",{onClick:()=>r(d.id,d.taskId||null),style:{border:"1px solid var(--green)",background:"white",padding:18,cursor:"pointer"},children:[i.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8,marginBottom:10,flexWrap:"wrap"},children:[i.jsx("span",{style:{width:9,height:9,background:"var(--green)",display:"inline-block"}}),i.jsx("span",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)"},children:"LIVE TASK"}),i.jsx("span",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--accent)"},children:d.runtime})]}),i.jsx("div",{style:{fontSize:24,fontWeight:650,lineHeight:1.2},children:(c==null?void 0:c.title)||d.title}),i.jsx("div",{style:{marginTop:8,fontSize:13,color:"var(--ink2)",lineHeight:1.5},children:d.preview||"Current mirrored session is ready to continue."})]}),i.jsxs("div",{style:{border:"1px solid var(--border)",background:"white",padding:18},children:[i.jsx("div",{style:{marginBottom:12,fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)"},children:"TASK HISTORY"}),i.jsxs("div",{style:{display:"grid",gap:0},children:[g.length===0&&i.jsx("div",{style:{padding:"36px 0",textAlign:"center",fontFamily:"var(--mono)",fontSize:11,color:"var(--ink3)"},children:"No task history yet."}),g.map(m)]})]})]})]})}function pv({url:e,title:t,lines:n}){const[r,o]=v.useState(!0);return i.jsxs("div",{style:{border:"1px solid var(--border)",marginTop:8,background:"white"},children:[i.jsxs("div",{onClick:()=>o(l=>!l),style:{borderBottom:r?"1px solid var(--border)":"none",padding:"6px 10px",display:"flex",alignItems:"center",gap:8,background:"var(--surface)",cursor:"pointer"},children:[i.jsx("span",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",letterSpacing:1},children:"BROWSER"}),i.jsx("span",{style:{flex:1,fontFamily:"var(--mono)",fontSize:10,color:"var(--ink2)",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:e}),i.jsx("span",{style:{fontSize:10,color:"var(--ink3)"},children:r?"▲":"▼"})]}),r&&i.jsxs("div",{style:{padding:"10px 12px"},children:[i.jsx("div",{style:{fontWeight:600,fontSize:13,marginBottom:8},children:t}),n.map((l,s)=>i.jsxs("div",{style:{display:"flex",gap:7,marginBottom:3},children:[i.jsx("span",{style:{color:"var(--accent)",fontFamily:"var(--mono)",fontSize:9,marginTop:2,flexShrink:0},children:"→"}),i.jsx("span",{style:{color:"var(--ink2)",fontSize:12.5,lineHeight:1.4},children:l})]},s))]})]})}function mv({query:e,files:t}){return i.jsxs("div",{style:{border:"1px solid var(--border)",marginTop:8,background:"white"},children:[i.jsxs("div",{style:{borderBottom:"1px solid var(--border)",padding:"6px 10px",display:"flex",gap:8,alignItems:"center",background:"var(--surface)"},children:[i.jsx("span",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",letterSpacing:1},children:"GREP"}),i.jsxs("span",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--accent)"},children:['"',e,'"']}),i.jsxs("span",{style:{marginLeft:"auto",fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)"},children:[t.length," matches"]})]}),i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:11.5},children:t.map((n,r)=>i.jsxs("div",{style:{padding:"8px 12px",borderBottom:ri.jsx("div",{style:{color:n.c==="comment"?"oklch(0.5 0.01 260)":n.c==="bad"?"var(--rose)":n.c==="good"?"var(--green-mid)":"oklch(0.88 0.01 260)"},children:n.t||" "},r))})]})}function gv({title:e,lines:t}){return i.jsxs("div",{style:{border:"1px solid var(--border)",marginTop:8,background:"white"},children:[i.jsxs("div",{style:{borderBottom:"1px solid var(--border)",padding:"6px 12px",display:"flex",gap:8,alignItems:"center",background:"var(--surface)"},children:[i.jsx("span",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",letterSpacing:1},children:"DOCUMENT"}),i.jsx("span",{style:{fontSize:11,fontWeight:500,fontFamily:"var(--mono)",color:"var(--ink2)"},children:e})]}),i.jsx("div",{style:{padding:"12px 16px",fontSize:13,lineHeight:1.65},children:t.map((n,r)=>typeof n=="object"&&"h"in n?i.jsx("div",{style:{fontWeight:700,marginTop:r>0?12:0,marginBottom:3,fontSize:10.5,textTransform:"uppercase",letterSpacing:"0.06em",color:"var(--ink2)"},children:n.h},r):i.jsx("div",{style:{color:"var(--ink)"},children:n},r))})]})}function vv({linkedTask:e,preview:t,tasks:n,onSelectTask:r}){const o=n.find(a=>a.id===e);if(!o)return null;const s={green:"var(--green)",indigo:"var(--indigo)",orange:"var(--accent)",rose:"var(--rose)"}[o.color]||"var(--accent)";return i.jsxs("div",{onClick:()=>r(o.id),style:{border:`1px solid ${s}`,marginTop:8,cursor:"pointer",display:"flex",gap:12,padding:"9px 12px",background:"white",transition:"background 0.12s"},onMouseEnter:a=>a.currentTarget.style.background="var(--surface)",onMouseLeave:a=>a.currentTarget.style.background="white",children:[i.jsx("div",{style:{width:8,height:8,background:s,flexShrink:0,marginTop:3}}),i.jsxs("div",{style:{flex:1},children:[i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",letterSpacing:1,marginBottom:2},children:"LINKED TASK"}),i.jsx("div",{style:{fontWeight:500,fontSize:13},children:o.title}),i.jsx("div",{style:{fontSize:11,color:"var(--ink2)",marginTop:2},children:t})]}),i.jsx("div",{style:{color:s,fontSize:15,alignSelf:"center"},children:"→"})]})}function Yu({msg:e,tasks:t,onSelectTask:n}){if(e.role==="component"){const o=e;return i.jsxs("div",{style:{marginBottom:14,paddingLeft:14},children:[o.type==="browser"&&i.jsx(pv,{url:o.url,title:o.title,lines:o.lines}),o.type==="grep"&&i.jsx(mv,{query:o.query,files:o.files}),o.type==="code"&&i.jsx(hv,{lang:o.lang,lines:o.lines}),o.type==="document"&&i.jsx(gv,{title:o.title,lines:o.lines}),o.type==="link"&&i.jsx(vv,{linkedTask:o.linkedTask,preview:o.preview,tasks:t,onSelectTask:n})]})}const r=e.role==="user";return i.jsxs("div",{style:{marginBottom:13,display:"flex",flexDirection:"column",alignItems:r?"flex-end":"flex-start"},children:[!r&&i.jsxs("div",{style:{display:"flex",alignItems:"center",gap:5,marginBottom:4},children:[i.jsx("div",{style:{width:5,height:5,background:"var(--green)",borderRadius:"50%"}}),i.jsx("span",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",letterSpacing:1},children:"AGENT"})]}),i.jsx("div",{style:{maxWidth:"88%",padding:"9px 13px",background:r?"var(--ink)":"white",color:r?"var(--bg)":"var(--ink)",border:r?"none":"1px solid var(--border)",fontSize:13.5,lineHeight:1.6,whiteSpace:"pre-wrap"},children:e.content})]})}function yv(e){if(!e)return"—";const t=new Date(e);return Number.isNaN(t.getTime())?"—":t.toLocaleString()}function xv(e){var t;return e?((t=e.currentSession)==null?void 0:t.state)==="active"?"session live":e.installed?"installed":"not attached":"No runtime selected"}function Sv(e){const t=(e==null?void 0:e.mounts)||(e==null?void 0:e.folders)||[],n=t.find(r=>r.primary)||t[0];return(n==null?void 0:n.path)||(e==null?void 0:e.rootPath)||(e==null?void 0:e.workspacePath)||""}function kv({tasks:e,activeTaskId:t,selectedProjectId:n,selectedWorkspaceId:r,selectedSessionId:o,projectIndex:l,workspaceGraph:s,onSelectTask:a,onSelectSession:d,onSelectProject:c,onCanvasOpen:g,onNotepadOpen:m,onAddTaskNote:x,onUpdateWorkspace:S,onAddWorkspaceFolder:E,onRemoveWorkspaceFolder:T,onCreateProject:H,onUpdateProject:h,onTasksRefresh:u,tweaks:f}){var Fa,La,Oa,$a,Da,Ma,Ia,Ba,Aa,Ua,Wa,Ha,Va,Ka,Qa,Ya,Ja,Xa,qa,Za,Ga,ed,td,nd,rd,id,od,ld,sd,ad,dd,ud;const[p,j]=v.useState(null),[_,P]=v.useState([]),[z,Q]=v.useState(""),[R,J]=v.useState(!1),[X,K]=v.useState(!1),[G,M]=v.useState(null),[V,ye]=v.useState(null),[C,A]=v.useState(""),[W,ee]=v.useState(!1),[ie,D]=v.useState(""),[fe,se]=v.useState(""),[me,$]=v.useState(""),[de,ce]=v.useState(""),[he,_e]=v.useState("codex"),[N,oe]=v.useState(""),[q,te]=v.useState(""),[Ae,Ft]=v.useState("codex"),[xe,Me]=v.useState(!1),[je,Fe]=v.useState(null),wn=v.useRef(null),Ut=v.useRef(null),Z=((Fa=l==null?void 0:l.workspaces)==null?void 0:Fa.find(y=>y.id===r))||((La=l==null?void 0:l.workspaces)==null?void 0:La.find(y=>y.id===(s==null?void 0:s.currentWorkspaceId)))||((Oa=l==null?void 0:l.workspaces)==null?void 0:Oa[0])||(s==null?void 0:s.workspace)||null,ae=(($a=Z==null?void 0:Z.projects)==null?void 0:$a.find(y=>y.id===n))||((Da=Z==null?void 0:Z.projects)==null?void 0:Da.find(y=>y.id===(s==null?void 0:s.currentProjectId)))||((Ma=Z==null?void 0:Z.projects)==null?void 0:Ma[0])||null,Se=((Ia=ae==null?void 0:ae.sessions)==null?void 0:Ia.find(y=>y.id===o))||((Ba=ae==null?void 0:ae.sessions)==null?void 0:Ba.find(y=>y.id===(s==null?void 0:s.currentSessionId)))||((Aa=ae==null?void 0:ae.sessions)==null?void 0:Aa[0])||((Ua=Z==null?void 0:Z.sessions)==null?void 0:Ua.find(y=>y.id===o))||((Wa=Z==null?void 0:Z.sessions)==null?void 0:Wa.find(y=>y.id===(s==null?void 0:s.currentSessionId)))||((Ha=Z==null?void 0:Z.sessions)==null?void 0:Ha[0])||null,le=((Va=l==null?void 0:l.workspaces)==null?void 0:Va.find(y=>y.id===(ie||(Z==null?void 0:Z.id))))||Z||null,Ee=((Ka=le==null?void 0:le.projects)==null?void 0:Ka.find(y=>y.id===(fe||(ae==null?void 0:ae.id))))||((Qa=le==null?void 0:le.projects)==null?void 0:Qa[0])||null;v.useEffect(()=>{Z&&!ie&&D(Z.id)},[Z,ie]),v.useEffect(()=>{var y;le&&!le.projects.some(re=>re.id===fe)&&se(((y=le.projects[0])==null?void 0:y.id)||"")},[le,fe]),v.useEffect(()=>{le&&$(le.label||le.name)},[le==null?void 0:le.id]),v.useEffect(()=>{ae&&ce(ae.name)},[ae==null?void 0:ae.id]),v.useEffect(()=>{Ee&&(ce(Ee.name),_e(Ee.defaultRuntime==="claude-code"?"claude-code":"codex"),oe((Ee.scopeRules||[]).map(y=>y.pathPrefix).filter(Boolean).join(` +`)))},[Ee==null?void 0:Ee.id]);const Lt=async y=>{var Le,cd;const re=y||(Se==null?void 0:Se.id);if(re)try{const br=await B.sessionDetail(re);j(br),P(((Le=br.runtime)==null?void 0:Le.runtimes)||[]),M(((cd=br.files)==null?void 0:cd[0])||null),ye(null),Fe(null)}catch(br){Fe(String(br))}},{messages:Qn}=pp(Z==null?void 0:Z.id,ae==null?void 0:ae.id);v.useEffect(()=>{Lt(o||(Se==null?void 0:Se.id))},[o,Z==null?void 0:Z.id]),v.useEffect(()=>{const y=window.setInterval(()=>{Lt(o||(Se==null?void 0:Se.id))},5e3);return()=>window.clearInterval(y)},[o,Se==null?void 0:Se.id]),v.useEffect(()=>{var y;(y=wn.current)==null||y.focus()},[(Ya=p==null?void 0:p.session)==null?void 0:Ya.id]);const w=e.find(y=>y.id===t)||null,b=(p==null?void 0:p.task)||w||null,k=v.useMemo(()=>{var y,re,Le;return(re=(y=p==null?void 0:p.session)==null?void 0:y.messages)!=null&&re.length?p.session.messages:(Le=b==null?void 0:b.messages)!=null&&Le.length?b.messages:[]},[(Ja=p==null?void 0:p.session)==null?void 0:Ja.messages,b]),O=v.useMemo(()=>{var y;return(y=p==null?void 0:p.session)!=null&&y.runtime?_.filter(re=>re.id===p.session.runtime):_},[(Xa=p==null?void 0:p.session)==null?void 0:Xa.runtime,_]),I=async()=>{var re;const y=z.trim();if(!(!y||!b||R)){J(!0);try{await x(b.id,y),Q(""),await u(),await Lt((re=p==null?void 0:p.session)==null?void 0:re.id)}catch(Le){Fe(String(Le))}finally{J(!1)}}},ne=async y=>{var re;if(!(!y||!((re=p==null?void 0:p.session)!=null&&re.id))){K(!0);try{await B.uploadSessionAsset(p.session.id,y),await Lt(p.session.id)}catch(Le){Fe(String(Le))}finally{K(!1),Ut.current&&(Ut.current.value="")}}},ve=async y=>{try{const re=await B.assetContext(y);ye(re)}catch(re){Fe(String(re))}},Ke=()=>{var y,re;Z&&(D(Z.id),$(Z.label||Z.name),se((ae==null?void 0:ae.id)||((re=(y=Z.projects)==null?void 0:y[0])==null?void 0:re.id)||"")),ee(!0),Fe(null)},st=async()=>{if(!(!le||!me.trim()||xe)){Me(!0);try{await S(le.id,me.trim()),Fe(null)}catch(y){Fe(String(y))}finally{Me(!1)}}},Ue=async()=>{if(!(!le||xe)){Me(!0),Fe(null);try{const y=await B.pickFolder("Select a folder to mount in this workspace");y.ok&&y.path&&await E(le.id,y.path)}catch(y){Fe(String(y))}finally{Me(!1)}}},Yn=async y=>{if(!(!le||xe)){Me(!0),Fe(null);try{await T(le.id,y)}catch(re){Fe(String(re))}finally{Me(!1)}}},Sp=async()=>{if(!Ee||xe)return;const y=N.split(` +`).map(re=>re.trim()).filter(Boolean).map((re,Le)=>({path_prefix:re,label:Le===0?"primary":`scope-${Le+1}`}));Me(!0),Fe(null);try{await h(Ee.id,{name:de.trim()||Ee.name,default_runtime:he,scope_rules:y})}catch(re){Fe(String(re))}finally{Me(!1)}};return!ae||!Z||!Se?i.jsx("div",{style:{height:"100%",display:"grid",alignItems:"center",justifyContent:"center",padding:28},children:i.jsx(hp,{title:"No mirrored workspace session yet",body:"Add a repo folder or start a task from an existing workspace. Once an agent session is mirrored, this view becomes the working console.",actions:[{label:"ADD REPO FOLDER",onClick:g,tone:"primary"},{label:"START TASK",onClick:m}]})}):i.jsxs("div",{style:{display:"flex",flexDirection:"column",height:"100%"},children:[i.jsxs("div",{style:{borderBottom:"1px solid var(--border)",padding:"0 14px",height:48,display:"flex",alignItems:"center",gap:10,flexShrink:0},children:[i.jsxs("div",{style:{display:"flex",alignItems:"center",gap:10,minWidth:0,flex:1},children:[i.jsxs("span",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)"},children:[Z.label||Z.name," / ",ae.name]}),i.jsx("span",{style:{fontSize:14,fontWeight:600,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:((qa=p==null?void 0:p.session)==null?void 0:qa.title)||Se.title}),i.jsx($e,{label:`${((Za=p==null?void 0:p.session)==null?void 0:Za.runtime)||Se.runtime}`.replace("-"," "),tone:"var(--green)"}),i.jsx($e,{label:((Ga=p==null?void 0:p.session)==null?void 0:Ga.permissionMode)||Se.permissionMode||"native"}),i.jsx($e,{label:((ed=p==null?void 0:p.session)==null?void 0:ed.state)||Se.state})]}),i.jsx("button",{onClick:()=>{var y;return void Lt((y=p==null?void 0:p.session)==null?void 0:y.id)},style:{height:48,padding:"0 12px",borderLeft:"1px solid var(--border)",fontFamily:"var(--mono)",fontSize:9,color:"var(--ink2)"},children:"REFRESH"}),i.jsx("button",{onClick:g,style:{height:48,padding:"0 14px",borderLeft:"1px solid var(--border)",fontFamily:"var(--mono)",fontSize:9,color:"var(--ink2)"},children:"⊞ CANVAS"}),i.jsx("button",{onClick:Ke,style:{height:48,padding:"0 14px",borderLeft:"1px solid var(--border)",fontFamily:"var(--mono)",fontSize:9,color:"var(--ink2)"},children:"MANAGE"})]}),i.jsxs("div",{style:{flex:1,display:"flex",overflow:"hidden"},children:[i.jsxs("div",{style:{width:f.compactNav?74:260,borderRight:"1px solid var(--border)",display:"flex",flexDirection:"column",overflowY:"auto"},children:[i.jsxs("div",{style:{padding:"12px 12px 10px",borderBottom:"1px solid var(--border)"},children:[i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",marginBottom:8},children:"WORKSPACE"}),i.jsxs("div",{style:{border:"1px solid var(--border)",background:"white",padding:10,marginBottom:12},children:[i.jsx("div",{style:{fontSize:12,fontWeight:600,marginBottom:4},children:Z.label||Z.name}),i.jsxs("div",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",lineHeight:1.5},children:[(Z.folders||Z.mounts||[]).length," mounted folders · ",Z.sessionCount||Z.sessions.length," sessions"]}),i.jsx("button",{onClick:Ke,style:{marginTop:8,padding:"6px 10px",border:"1px solid var(--border)",fontFamily:"var(--mono)",fontSize:9,color:"var(--accent)"},children:"manage workspace"})]}),i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",marginBottom:8},children:"PROJECTS"}),Z.projects.map(y=>i.jsx("div",{style:{marginBottom:10},children:i.jsxs("button",{onClick:()=>c(y.id,Z.id),style:{width:"100%",textAlign:"left",padding:"8px 9px",border:`1px solid ${y.id===ae.id?"var(--border2)":"var(--border)"}`,background:y.id===ae.id?"var(--surface)":"white",fontSize:12,fontWeight:y.id===ae.id?700:500,color:y.id===ae.id?"var(--ink)":"var(--ink2)"},children:[y.name,i.jsxs("div",{style:{marginTop:4,fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)"},children:[y.sessions.length," sessions · default ",y.defaultRuntime||"codex"]})]})},y.id))]}),i.jsxs("div",{style:{padding:"12px"},children:[i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",marginBottom:8},children:"SESSIONS"}),(ae.sessions||[]).map(y=>{var re,Le;return i.jsxs("button",{onClick:()=>d(y.id,y.taskId),style:{width:"100%",textAlign:"left",padding:"9px 10px",marginBottom:8,border:`1px solid ${((re=p==null?void 0:p.session)==null?void 0:re.id)===y.id?"var(--green)":"var(--border)"}`,background:((Le=p==null?void 0:p.session)==null?void 0:Le.id)===y.id?"oklch(0.98 0.02 145)":"white"},children:[i.jsx("div",{style:{fontSize:12,fontWeight:600,marginBottom:4},children:y.title}),i.jsxs("div",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)"},children:[y.runtime," · ",y.model||"unknown model"]}),i.jsx("div",{style:{fontSize:10.5,color:"var(--ink3)",marginTop:6,lineHeight:1.4},children:y.preview||"No preview yet."})]},y.id)})]})]}),i.jsxs("div",{style:{flex:1,display:"flex",flexDirection:"column",minWidth:0},children:[i.jsxs("div",{style:{padding:"12px 16px",borderBottom:"1px solid var(--border)",display:"flex",gap:8,flexWrap:"wrap"},children:[i.jsx($e,{label:((td=p==null?void 0:p.session)==null?void 0:td.runtime)||Se.runtime,tone:"var(--green)"}),i.jsx($e,{label:((nd=p==null?void 0:p.session)==null?void 0:nd.taskStatus)||(b==null?void 0:b.status)||"mirrored"}),i.jsx($e,{label:`${((id=(rd=p==null?void 0:p.session)==null?void 0:rd.touchedFiles)==null?void 0:id.length)||0} files`}),i.jsx($e,{label:`${((od=p==null?void 0:p.assets)==null?void 0:od.length)||0} assets`}),i.jsx($e,{label:`${((ld=p==null?void 0:p.results)==null?void 0:ld.length)||0} shared results`})]}),i.jsxs("div",{style:{flex:1,overflowY:"auto",padding:"20px 18px 8px"},children:[k.length===0&&i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:11,color:"var(--ink3)"},children:"No mirrored messages yet."}),k.map(y=>i.jsx(Yu,{msg:y,tasks:e,onSelectTask:a},y.id)),(sd=p==null?void 0:p.results)==null?void 0:sd.slice(0,8).map(y=>i.jsx(Yu,{msg:{id:`result:${y.id}`,role:"agent",content:`${y.tool_name}: ${y.digest||"No digest recorded."}`},tasks:e,onSelectTask:a},y.id)),i.jsx("div",{style:{height:1}})]}),i.jsxs("div",{style:{borderTop:"1px solid var(--border)",padding:"12px 16px"},children:[i.jsx("textarea",{ref:wn,value:z,onChange:y=>Q(y.target.value),placeholder:b?"Add a Dhee note to this session task…":"This mirrored session has no linked Dhee task yet.",rows:3,disabled:!b,style:{width:"100%",fontFamily:"var(--font)",fontSize:14,lineHeight:1.5,border:"1px solid var(--border)",padding:"12px 14px",background:b?"white":"var(--surface)"}}),i.jsxs("div",{style:{marginTop:10,display:"flex",gap:10,alignItems:"center"},children:[i.jsx("button",{onClick:()=>void I(),disabled:!b||R,style:{padding:"7px 14px",border:"1px solid var(--ink)",background:b?"var(--ink)":"transparent",color:b?"white":"var(--ink3)",fontFamily:"var(--mono)",fontSize:10},children:R?"saving…":"save note"}),je&&i.jsx("span",{style:{fontSize:11,color:"var(--rose)"},children:je})]})]})]}),i.jsxs("div",{style:{width:320,borderLeft:"1px solid var(--border)",overflowY:"auto",padding:"14px 14px 18px"},children:[i.jsxs("div",{style:{marginBottom:18},children:[i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",marginBottom:10},children:"RUNTIME"}),O.map(y=>{var re,Le;return i.jsxs("div",{style:{border:"1px solid var(--border)",background:"white",padding:12,marginBottom:10},children:[i.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",gap:8,marginBottom:6},children:[i.jsx("div",{style:{fontSize:12.5,fontWeight:600},children:y.label}),i.jsx($e,{label:xv(y),tone:y.installed?"var(--green)":"var(--rose)"})]}),i.jsx("div",{style:{fontSize:11,color:"var(--ink2)"},children:((re=y.currentSession)==null?void 0:re.title)||((Le=y.currentSession)==null?void 0:Le.cwd)||"No active session"}),i.jsxs("div",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",marginTop:6},children:["limit: ",y.limits.state,y.limits.resetAt?` · reset ${yv(y.limits.resetAt)}`:""]})]},y.id)})]}),i.jsxs("div",{style:{marginBottom:18},children:[i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",marginBottom:10},children:"COLLABORATION LINE"}),i.jsx(fp,{workspace:Z,activeProjectId:ae==null?void 0:ae.id,sessionId:((ad=p==null?void 0:p.session)==null?void 0:ad.id)||(Se==null?void 0:Se.id),taskId:b==null?void 0:b.id,onPublished:async()=>{await u()}}),i.jsx("div",{style:{display:"grid",gap:8,marginTop:10},children:(Qn||[]).slice(0,8).map(y=>i.jsx(cp,{message:y,workspace:Z},y.id))})]}),i.jsxs("div",{style:{marginBottom:18},children:[i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",marginBottom:10},children:"FILE CONTEXT"}),(dd=p==null?void 0:p.files)==null?void 0:dd.map(y=>i.jsxs("button",{onClick:()=>M(y),style:{width:"100%",textAlign:"left",padding:"10px 11px",border:`1px solid ${(G==null?void 0:G.path)===y.path?"var(--indigo)":"var(--border)"}`,background:"white",marginBottom:8},children:[i.jsx("div",{style:{fontSize:12,fontWeight:600,marginBottom:4},children:y.path.split("/").pop()}),i.jsxs("div",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)"},children:[y.results.length," results · ",y.memories.length," memories"]})]},y.path)),G&&i.jsxs("div",{style:{border:"1px solid var(--border)",background:"white",padding:12},children:[i.jsx("div",{style:{fontSize:12,fontWeight:600,marginBottom:6},children:G.path}),i.jsx("div",{style:{fontSize:11,color:"var(--ink2)",lineHeight:1.5,marginBottom:8},children:G.summary||"No stored summary yet."}),G.results.slice(0,3).map(y=>i.jsxs("div",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",marginBottom:6},children:[y.tool_name,": ",String(y.digest||"").slice(0,120)]},y.id))]})]}),i.jsxs("div",{children:[i.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:10},children:[i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)"},children:"SESSION ASSETS"}),i.jsx("button",{onClick:()=>{var y;return(y=Ut.current)==null?void 0:y.click()},style:{padding:"4px 10px",border:"1px solid var(--border)",fontFamily:"var(--mono)",fontSize:9,color:"var(--accent)"},children:X?"uploading…":"upload"})]}),i.jsx("input",{ref:Ut,type:"file",hidden:!0,onChange:y=>{var re;return void ne((re=y.target.files)==null?void 0:re[0])}}),(ud=p==null?void 0:p.assets)==null?void 0:ud.map(y=>i.jsxs("button",{onClick:()=>void ve(y.id),style:{width:"100%",textAlign:"left",border:"1px solid var(--border)",background:"white",padding:12,marginBottom:8},children:[i.jsx("div",{style:{fontSize:12,fontWeight:600,marginBottom:4},children:y.name}),i.jsxs("div",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)"},children:[y.mime_type||"file"," · ",(y.size_bytes||0).toLocaleString()," bytes"]})]},y.id)),V&&i.jsxs("div",{style:{border:"1px solid var(--border)",background:"white",padding:12,marginTop:10},children:[i.jsx("div",{style:{fontSize:12,fontWeight:600,marginBottom:6},children:V.asset.name}),i.jsx("div",{style:{fontSize:11,color:"var(--ink2)",lineHeight:1.5,marginBottom:8},children:V.summary||"No extracted summary yet. Re-upload or parse this asset to make it queryable."}),(V.chunks||[]).slice(0,3).map(y=>i.jsxs("div",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",marginBottom:6},children:["chunk ",y.chunk_index,": ",y.content.slice(0,180)]},`${V.asset.id}:${y.chunk_index}`)),i.jsx("textarea",{value:C,onChange:y=>A(y.target.value),placeholder:"Ask Dhee about this asset…",rows:3,style:{width:"100%",border:"1px solid var(--border)",padding:"10px 12px",marginTop:8,background:"var(--bg)"}}),i.jsx("button",{onClick:async()=>{var y;if(C.trim())try{const re=await B.askAsset(V.asset.id,C.trim());A(""),(y=re.launch)!=null&&y.session_id&&d(re.launch.session_id,re.launch.task_id||null)}catch(re){Fe(String(re))}},style:{marginTop:8,padding:"8px 12px",border:"1px solid var(--ink)",background:"var(--ink)",color:"white",fontFamily:"var(--mono)",fontSize:10},children:"ask with default claude code"})]})]})]})]}),W&&ae&&le&&i.jsx("div",{onClick:()=>ee(!1),style:{position:"fixed",inset:0,background:"rgba(12, 12, 12, 0.22)",display:"flex",alignItems:"center",justifyContent:"center",padding:24,zIndex:80},children:i.jsxs("div",{onClick:y=>y.stopPropagation(),style:{width:"min(1040px, calc(100vw - 80px))",maxHeight:"calc(100vh - 80px)",background:"var(--bg)",border:"1px solid var(--border2)",display:"grid",gridTemplateColumns:"280px minmax(0, 1fr)",overflow:"hidden",boxShadow:"0 30px 80px rgba(0,0,0,0.12)"},children:[i.jsxs("div",{style:{borderRight:"1px solid var(--border)",background:"white",overflowY:"auto"},children:[i.jsxs("div",{style:{padding:16,borderBottom:"1px solid var(--border)"},children:[i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)"},children:"WORKSPACES"}),i.jsx("div",{style:{marginTop:6,fontSize:13,fontWeight:600},children:"switch workspace"})]}),i.jsx("div",{style:{padding:12},children:((l==null?void 0:l.workspaces)||[]).map(y=>i.jsxs("button",{onClick:()=>{var re,Le;D(y.id),se(((Le=(re=y.projects)==null?void 0:re[0])==null?void 0:Le.id)||"")},style:{width:"100%",textAlign:"left",padding:"10px 12px",marginBottom:8,border:`1px solid ${y.id===le.id?"var(--accent)":"var(--border)"}`,background:y.id===le.id?"rgba(224, 107, 63, 0.06)":"white"},children:[i.jsx("div",{style:{fontSize:12,fontWeight:600,marginBottom:4},children:y.label||y.name}),i.jsxs("div",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)"},children:[y.sessionCount||y.sessions.length," sessions · ",(y.folders||y.mounts||[]).length," folders"]})]},y.id))})]}),i.jsxs("div",{style:{display:"flex",flexDirection:"column",overflow:"hidden"},children:[i.jsxs("div",{style:{padding:"16px 18px",borderBottom:"1px solid var(--border)",background:"white",display:"flex",alignItems:"center",justifyContent:"space-between",gap:12},children:[i.jsxs("div",{children:[i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)"},children:"WORKSPACE SETTINGS"}),i.jsx("div",{style:{marginTop:6,fontSize:18,fontWeight:650},children:le.label||le.name})]}),i.jsx("button",{onClick:()=>ee(!1),style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)"},children:"close"})]}),i.jsxs("div",{style:{flex:1,overflowY:"auto",padding:18},children:[i.jsxs("div",{style:{marginBottom:24},children:[i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)",marginBottom:10},children:"RENAME"}),i.jsxs("div",{style:{display:"flex",gap:10,alignItems:"center"},children:[i.jsx("input",{value:me,onChange:y=>$(y.target.value),placeholder:"Workspace name",style:{flex:1,border:"1px solid var(--border)",padding:"10px 12px",background:"white",fontSize:14}}),i.jsx("button",{onClick:()=>void st(),disabled:!me.trim()||xe,style:{padding:"10px 12px",border:"1px solid var(--ink)",background:"var(--ink)",color:"white",fontFamily:"var(--mono)",fontSize:10,opacity:!me.trim()||xe?.6:1},children:xe?"saving...":"save name"})]})]}),i.jsxs("div",{children:[i.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",gap:12,marginBottom:10},children:[i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)"},children:"MOUNTED FOLDERS"}),i.jsx("button",{onClick:()=>void Ue(),disabled:xe,style:{padding:"8px 10px",border:"1px solid var(--border)",background:"white",fontFamily:"var(--mono)",fontSize:9,color:"var(--accent)"},children:xe?"working...":"add folder"})]}),i.jsx("div",{style:{display:"grid",gap:10},children:(le.folders||le.mounts||[]).map(y=>i.jsxs("div",{style:{border:"1px solid var(--border)",background:"white",padding:12,display:"flex",justifyContent:"space-between",gap:12,alignItems:"flex-start"},children:[i.jsxs("div",{style:{minWidth:0},children:[i.jsx("div",{style:{fontSize:12,fontWeight:600,marginBottom:4},children:y.primary?"root folder":y.label}),i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",lineHeight:1.5,wordBreak:"break-all"},children:y.path})]}),i.jsx("button",{onClick:()=>void Yn(y.path),disabled:xe||(le.folders||le.mounts||[]).length<=1,style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--rose)",opacity:xe||(le.folders||le.mounts||[]).length<=1?.5:1},children:"remove"})]},y.path))}),i.jsx("div",{style:{marginTop:10,fontSize:11,color:"var(--ink3)",lineHeight:1.5},children:"Sessions are included in this workspace by matching their working directory against these mounted folders. Removing a folder removes those sessions from this workspace view without deleting the mirrored session record."})]}),i.jsxs("div",{style:{marginTop:24},children:[i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)",marginBottom:10},children:"PROJECTS"}),i.jsxs("div",{style:{display:"grid",gridTemplateColumns:"240px minmax(0, 1fr)",gap:14},children:[i.jsx("div",{style:{display:"grid",gap:10,alignContent:"start"},children:(le.projects||[]).map(y=>i.jsxs("button",{onClick:()=>se(y.id),style:{width:"100%",textAlign:"left",border:`1px solid ${y.id===(Ee==null?void 0:Ee.id)?"var(--accent)":"var(--border)"}`,background:y.id===(Ee==null?void 0:Ee.id)?"rgba(224, 107, 63, 0.06)":"white",padding:12},children:[i.jsx("div",{style:{fontSize:12,fontWeight:600},children:y.name}),i.jsxs("div",{style:{fontFamily:"var(--mono)",fontSize:9,color:"var(--ink3)",marginTop:4},children:[y.sessions.length," sessions · default ",y.defaultRuntime||"codex"]})]},y.id))}),i.jsxs("div",{style:{display:"grid",gap:12,alignContent:"start"},children:[Ee?i.jsxs("div",{style:{border:"1px solid var(--border)",background:"white",padding:14},children:[i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)",marginBottom:10},children:"EDIT PROJECT"}),i.jsxs("div",{style:{display:"grid",gap:10},children:[i.jsx("input",{value:de,onChange:y=>ce(y.target.value),placeholder:"Project name",style:{border:"1px solid var(--border)",padding:"10px 12px",background:"white",fontSize:14}}),i.jsxs("select",{value:he,onChange:y=>_e(y.target.value),style:{border:"1px solid var(--border)",padding:"10px 12px",background:"white",fontSize:14},children:[i.jsx("option",{value:"codex",children:"Codex default runtime"}),i.jsx("option",{value:"claude-code",children:"Claude Code default runtime"})]}),i.jsx("textarea",{value:N,onChange:y=>oe(y.target.value),rows:5,placeholder:"One path scope rule per line",style:{border:"1px solid var(--border)",padding:"10px 12px",background:"white",fontSize:13,lineHeight:1.5}}),i.jsx("div",{style:{fontSize:11,color:"var(--ink3)",lineHeight:1.5},children:"Sessions are assigned to this project by the longest matching scope rule path."}),i.jsx("button",{onClick:()=>void Sp(),disabled:xe||!de.trim(),style:{justifySelf:"start",padding:"10px 12px",border:"1px solid var(--ink)",background:"var(--ink)",color:"white",fontFamily:"var(--mono)",fontSize:10,opacity:xe||!de.trim()?.6:1},children:xe?"saving...":"save project"})]})]}):null,i.jsxs("div",{style:{border:"1px solid var(--border)",background:"white",padding:14},children:[i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)",marginBottom:10},children:"ADD PROJECT"}),i.jsxs("div",{style:{display:"grid",gap:10},children:[i.jsx("input",{value:q,onChange:y=>te(y.target.value),placeholder:"New project name",style:{border:"1px solid var(--border)",padding:"10px 12px",background:"white",fontSize:14}}),i.jsxs("select",{value:Ae,onChange:y=>Ft(y.target.value),style:{border:"1px solid var(--border)",padding:"10px 12px",background:"white",fontSize:14},children:[i.jsx("option",{value:"codex",children:"Codex default runtime"}),i.jsx("option",{value:"claude-code",children:"Claude Code default runtime"})]}),i.jsx("button",{onClick:async()=>{if(!(!le||!q.trim())){Me(!0);try{await H(le.id,{name:q.trim(),default_runtime:Ae,scope_rules:[{path_prefix:Sv(le),label:"root"}]}),te("")}catch(y){Fe(String(y))}finally{Me(!1)}}},style:{justifySelf:"start",padding:"10px 12px",border:"1px solid var(--ink)",background:"var(--ink)",color:"white",fontFamily:"var(--mono)",fontSize:10},children:"add project"})]})]})]})]})]}),je&&i.jsx("div",{style:{marginTop:18,fontSize:11,color:"var(--rose)"},children:je})]})]})]})})]})}const jv=v.lazy(()=>qh(()=>import("./CanvasView-Dg15id0Q.js"),[]).then(e=>({default:e.CanvasView}))),wv={accentHue:"36",compactNav:!1,showTimestamps:!0,canvasStyle:"force"},Cv=15e3,bv=new Set(["command","handoff","replay","learnings","portability"]);function Yi(e){return bv.has(e)}function Ol(e){return e==="canvas"}function $l(e){return e==="router"||e==="router/sessionshistory"}function Ji(e){const t=String(e||"").toLowerCase();return t==="home"||t==="overview"?"command":t==="memory"?"context":t==="firewall"||t==="context-firewall"?"router":t==="repo"||t==="brain"||t==="repo-brain"||t==="folders"?"canvas":t==="learn"||t==="learning"?"learnings":t==="packs"||t==="portable"||t==="trust"?"portability":t==="router/sessionshistory"||t==="router/session-history"||t==="router/history"?"router/sessionshistory":t||"command"}function Mr(e){return typeof window>"u"?"":new URLSearchParams(window.location.search).get(e)||""}function _v(){const[e,t]=v.useState(()=>Ji(Mr("view"))),[n,r]=v.useState(!1),[o,l]=v.useState("workspaces"),[s,a]=v.useState([]),[d,c]=v.useState(()=>Mr("task")),[g,m]=v.useState(wv),[x,S]=v.useState(!1),[E,T]=v.useState(0),[H,h]=v.useState([]),[u,f]=v.useState(0),[p,j]=v.useState(0),[_,P]=v.useState(!1),[z,Q]=v.useState(null),[R,J]=v.useState(null),[X,K]=v.useState(null),[G,M]=v.useState(null),[V,ye]=v.useState(0),[C,A]=v.useState(null),[W,ee]=v.useState(()=>Mr("project")),[ie,D]=v.useState(()=>Mr("workspace")),[fe,se]=v.useState(()=>Mr("session")),me=v.useRef({}),$=(k,O,I)=>{let ne="";try{ne=JSON.stringify(O)||""}catch{ne=String(Date.now())}me.current[k]!==ne&&(me.current[k]=ne,I(O))};v.useEffect(()=>{const k=new URLSearchParams(window.location.search),O=k.get("view"),I=k.get("task"),ne=k.get("project"),ve=k.get("workspace"),Ke=k.get("session");O&&t(Ji(O)),I&&c(I),ne&&ee(ne),ve&&D(ve),Ke&&se(Ke);const st=()=>{const Ue=new URLSearchParams(window.location.search);t(Ji(Ue.get("view"))),c(Ue.get("task")||""),ee(Ue.get("project")||""),D(Ue.get("workspace")||""),se(Ue.get("session")||"")};return window.addEventListener("popstate",st),()=>window.removeEventListener("popstate",st)},[]);const de=async()=>{try{const k=await B.tasks();$("tasks",k.tasks||[],a),c(O=>{var I,ne;return O||((ne=(I=k.tasks)==null?void 0:I[0])==null?void 0:ne.id)||""})}catch{}},ce=async()=>{try{const k=ie?await B.workspaceGraph(ie,W||void 0):await B.workspaceGraph();$("workspaceGraph",k,Q)}catch{}},he=async()=>{try{const k=await B.orgGraph(void 0,{active:!0});$("orgGraph",k,J)}catch{}},_e=async()=>{try{const k=await B.me();$("viewer",k,K)}catch{}},N=async()=>{try{const k=await B.routerStats();$("routerStats",k,M),f(k.sessionTokensSaved||0)}catch{}},oe=async()=>{var k,O,I,ne;try{const ve=await B.inbox(X!=null&&X.team_id?{team:X.team_id,user:X.user_id}:{user:X==null?void 0:X.user_id}),Ke=(((k=ve.totals)==null?void 0:k.proposals)||0)+(((O=ve.totals)==null?void 0:O.findings)||0)+(((I=ve.totals)==null?void 0:I.conflicts)||0);ye(Ke),j(((ne=ve.totals)==null?void 0:ne.conflicts)||0)}catch{}},q=async()=>{try{const k=await B.workspaces();$("projectIndex",k,A),D(O=>{var I,ne;return O||k.currentWorkspaceId||((ne=(I=k.workspaces)==null?void 0:I[0])==null?void 0:ne.id)||""}),ee(O=>{var I,ne,ve,Ke;return O||k.currentProjectId||((Ke=(ve=(ne=(I=k.workspaces)==null?void 0:I[0])==null?void 0:ne.projects)==null?void 0:ve[0])==null?void 0:Ke.id)||""}),se(O=>O||k.currentSessionId||"")}catch{}};v.useEffect(()=>{if(_e(),oe(),Yi(e)||N(),Ol(e))he();else{if($l(e))return;Yi(e)||(de(),he(),q().then(()=>ce()),(async()=>{var k;try{const O=await B.listMemories();T(((k=O.engrams)==null?void 0:k.length)||0),h(O.engrams||[])}catch{}})())}},[]),v.useEffect(()=>{const k=window.setInterval(()=>{if(Yi(e)){oe();return}if($l(e)){N(),oe();return}if(Ol(e)){he(),oe();return}de(),q(),ce(),he(),N(),oe()},Cv);return()=>window.clearInterval(k)},[e,ie,W,X==null?void 0:X.team_id]),v.useEffect(()=>{if(!Yi(e)){if($l(e)){N(),oe();return}if(Ol(e)){he();return}de(),q().then(()=>ce()),he(),(async()=>{var k;try{const O=await B.listMemories();T(((k=O.engrams)==null?void 0:k.length)||0),h(O.engrams||[])}catch{}})()}},[e]);const te=(k,O)=>{const I=Ji(k);P(!0),setTimeout(()=>{O&&c(O),t(I);const ne=new URLSearchParams(window.location.search);ne.set("view",I),O||d?ne.set("task",O||d):ne.delete("task"),W?ne.set("project",W):ne.delete("project"),ie?ne.set("workspace",ie):ne.delete("workspace"),fe?ne.set("session",fe):ne.delete("session");const ve=ne.toString(),Ke=ve?`?${ve}`:"",st=I==="context"&&window.location.hash.startsWith("#vault")?window.location.hash:"";window.history.pushState({},"",`${window.location.pathname}${Ke}${st}`),P(!1)},140)},Ae=k=>te("workspace",k),Ft=(k,O)=>{k&&se(k),O&&c(O),te("workspace",O||void 0)},xe=k=>{D(k),ee(""),se(""),te("workspace")},Me=(k,O)=>{O&&D(O),ee(k),se(""),te("workspace")};v.useEffect(()=>{const k=new URLSearchParams(window.location.search);k.set("view",e),d?k.set("task",d):k.delete("task"),W?k.set("project",W):k.delete("project"),ie?k.set("workspace",ie):k.delete("workspace"),fe?k.set("session",fe):k.delete("session");const O=k.toString(),I=`${window.location.pathname}${O?"?"+O:""}`;I!==`${window.location.pathname}${window.location.search}`&&window.history.replaceState({},"",I)},[e,d,W,ie,fe]);const je=async k=>{await B.createWorkspaceRoot(k),await q(),await ce()},Fe=async(k,O)=>{await B.createProject(k,O),await q(),await ce()},wn=async(k,O)=>{await B.updateProject(k,O),await q(),await ce()},Ut=async(k,O,I)=>{await B.addWorkspaceFolder(k,O,I),await q(),await ce()},Z=async(k,O)=>{await B.updateWorkspace(k,{label:O}),await q(),await ce()},ae=async(k,O)=>{await B.removeWorkspaceFolder(k,O),await q(),await ce()},Se=async k=>{try{const O=await B.createTask(k);a(I=>[...I,O.task]),await q(),setTimeout(()=>te("workspace",O.task.id),80)}catch(O){console.warn("createTask failed",O)}},le=async k=>{try{await B.remember(k,"short-term",[]),T(I=>I+1);const O=await B.listMemories();h(O.engrams||[])}catch(O){console.warn("remember failed",O)}},Ee=async(k,O,I,ne,ve)=>{const Ke=I||ie||(C==null?void 0:C.currentWorkspaceId);if(!Ke||!k.trim())return;const st=await B.launchWorkspaceSession(Ke,O,k.trim(),O==="claude-code"?ne:void 0,void 0,ve||W||(C==null?void 0:C.currentProjectId));await de(),await q(),await ce(),st.session_id&&se(st.session_id),st.task_id&&c(st.task_id),te("workspace",st.task_id||void 0)},Lt=async(k,O)=>{try{await B.addTaskNote(k,O),await de(),await q()}catch(I){console.warn("addTaskNote failed",I)}},Qn={opacity:_?0:1,transform:_?"translateY(3px)":"translateY(0)",transition:"opacity 0.14s ease, transform 0.14s ease",flex:1,overflow:"hidden",display:"flex",flexDirection:"column"},w=()=>e==="command"?i.jsx(Hg,{onNavigate:k=>te(k)}):e==="channel"?i.jsx(yg,{projectIndex:C,workspaceGraph:z,tasks:s,viewer:X,orgGraph:R,selectedWorkspaceId:ie,selectedProjectId:W,onSelectWorkspace:xe,onSelectProject:Me,onSelectTask:Ae,onTasksRefresh:de,onOpenCanvas:()=>te("canvas"),onLaunchSession:Ee,onOpenManager:k=>{l(k||"workspaces"),r(!0)},tweaks:g}):e==="notepad"?i.jsx(Ug,{projectIndex:C,memories:E,tokensSaved:u,onAddTask:Se,onAddMemory:le,onSelectSession:Ft,onCreateWorkspace:je,onLaunchSession:Ee,onCreateProject:Fe,onOpenWorkspace:()=>te("workspace"),onOpenTasks:()=>te("tasks"),tweaks:g}):e==="tasks"?i.jsx(fv,{tasks:s,projectIndex:C,onSelectTask:Ae,onSelectSession:Ft,tweaks:g}):e==="workspace"?i.jsx(kv,{tasks:s,activeTaskId:d,selectedProjectId:W,selectedWorkspaceId:ie,selectedSessionId:fe,projectIndex:C,workspaceGraph:z,onSelectTask:Ae,onSelectSession:Ft,onSelectProject:Me,onCanvasOpen:()=>te("canvas"),onNotepadOpen:()=>te("context"),onAddTaskNote:Lt,onUpdateWorkspace:Z,onAddWorkspaceFolder:Ut,onRemoveWorkspaceFolder:ae,onCreateProject:Fe,onUpdateProject:wn,onTasksRefresh:de,tweaks:g}):e==="canvas"?i.jsx(v.Suspense,{fallback:i.jsx("div",{style:{height:"100%",display:"grid",alignItems:"start",background:"var(--surface)",padding:20},children:i.jsxs("div",{style:{border:"1px solid var(--border)",background:"white",padding:"16px 18px",display:"flex",justifyContent:"space-between",alignItems:"center",gap:18},children:[i.jsxs("div",{children:[i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--green)",letterSpacing:"0.12em",textTransform:"uppercase"},children:"Repo Brain"}),i.jsx("div",{style:{marginTop:6,fontSize:20,fontWeight:650},children:"Loading folder canvas"})]}),i.jsx("div",{style:{fontFamily:"var(--mono)",fontSize:10,color:"var(--ink3)",textTransform:"uppercase"},children:"Context Vault"})]})}),children:i.jsx(jv,{tasks:s,selectedProjectId:W,workspaceGraph:z,orgGraph:R,viewer:X,onOpenVault:k=>{k&&(window.location.hash=`#vault/${k}`),te("context")},onOrgGraphChanged:()=>{he(),_e(),oe()},onSelectTask:Ae,onSelectSession:Ft,onSelectWorkspace:xe,onSelectProject:Me,onClose:()=>te("workspace"),tweaks:g})}):e==="memory"||e==="context"?i.jsx(Dg,{onMemoryCountChange:T,viewer:X,orgGraph:R,onInboxChanged:oe}):e==="router"||e==="router/sessionshistory"?i.jsx(ov,{onOpenFolders:()=>te("canvas"),onOpenSetup:()=>te("notepad")}):e==="handoff"?i.jsx(Vg,{}):e==="replay"?i.jsx(Kg,{}):e==="learnings"?i.jsx(Qg,{}):e==="portability"?i.jsx(Yg,{}):e==="conflicts"?i.jsx(kg,{viewer:X,onChanged:oe}):null;v.useEffect(()=>{const k=O=>{(O.ctrlKey||O.metaKey)&&O.key.toLowerCase()==="k"&&(O.preventDefault(),S(I=>!I))};return window.addEventListener("keydown",k),()=>window.removeEventListener("keydown",k)},[]);const b=()=>{_e(),de(),q(),ce(),he(),N(),oe()};return i.jsxs("div",{style:{height:"100vh",display:"flex",overflow:"hidden"},children:[i.jsx(Zh,{view:e,setView:k=>te(k),conflictCount:V||p}),i.jsxs("div",{style:{flex:1,display:"flex",flexDirection:"column",overflow:"hidden"},children:[i.jsx(tg,{viewer:X,routerStats:G,onRefresh:b,onOpenTweaks:()=>S(k=>!k),onResetWorkspace:async()=>{if(window.confirm("Reset workspace? Deletes projects, teams, folders, context items, proposals and findings for this org. Memory engrams are unaffected."))try{await B.enterpriseResetWorkspace()}finally{b()}}}),i.jsx("div",{style:Qn,children:w()})]}),i.jsx(ng,{tweaks:g,setTweaks:m,visible:x}),i.jsx(og,{open:n,onClose:()=>r(!1),projectIndex:C,initialWorkspaceId:ie,initialTab:o,onChanged:async()=>{await q(),await ce()}})]})}Dl.createRoot(document.getElementById("root")).render(i.jsx(Dp.StrictMode,{children:i.jsx(_v,{})}));export{Ev as R,B as a,i as j,v as r}; diff --git a/dhee/ui/web/dist/assets/index-DoQLFf8M.css b/dhee/ui/web/dist/assets/index-DoQLFf8M.css new file mode 100644 index 0000000..cf4f2a4 --- /dev/null +++ b/dhee/ui/web/dist/assets/index-DoQLFf8M.css @@ -0,0 +1 @@ +:root{--bg: oklch(.975 .006 80);--ink: oklch(.1 .01 260);--ink2: oklch(.42 .015 260);--ink3: oklch(.64 .01 260);--surface: oklch(.955 .008 80);--surface2: oklch(.93 .01 80);--border: oklch(.87 .012 80);--border2: oklch(.78 .012 80);--accent: oklch(.64 .18 36);--accent-dim: oklch(.97 .04 36);--green: oklch(.52 .22 145);--green-dim: oklch(.94 .06 145);--green-mid: oklch(.72 .14 145);--indigo: oklch(.52 .2 265);--indigo-dim: oklch(.95 .04 265);--rose: oklch(.58 .2 10);--rose-dim: oklch(.96 .05 10);--font: "Space Grotesk", sans-serif;--mono: "JetBrains Mono", monospace;--nav: 52px}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}html,body,#root{height:100%;width:100%}body{font-family:var(--font);background:var(--bg);color:var(--ink);overflow:hidden;font-size:13.5px;line-height:1.5;-webkit-font-smoothing:antialiased}::selection{background:var(--accent-dim)}::-webkit-scrollbar{width:3px;height:3px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--border2)}button{font-family:var(--font);cursor:pointer;border:none;background:none;color:inherit}textarea,input{font-family:var(--font);color:var(--ink);background:transparent;border:none;outline:none;resize:none}@keyframes blink{0%,to{opacity:1}50%{opacity:0}}@keyframes fadein{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}@keyframes dhee-card-in{0%{opacity:0;transform:translate3d(0,8px,0) scale(.98)}to{opacity:1;transform:translateZ(0) scale(1)}}@keyframes dhee-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}@keyframes dhee-pulse{0%,to{box-shadow:0 0 #e06b3f59}50%{box-shadow:0 0 0 6px #e06b3f00}}.dhee-node-card:hover{transform:translate3d(0,-2px,0)!important;box-shadow:0 6px 18px #14100a14,0 2px 6px #14100a0f!important}.dhee-canvas-bg{background-color:#faf7ef;background-image:radial-gradient(rgba(20,16,10,.06) 1px,transparent 1px);background-size:28px 28px;background-position:0 0}.dhee-edge-path{fill:none;stroke:#14100a24;stroke-width:1.4;transition:stroke .18s ease,stroke-width .18s ease,opacity .18s ease}.dhee-edge-path--highlight{stroke:#e06b3f8c;stroke-width:2}.dhee-edge-path--dim{opacity:.28}.md-root{word-wrap:break-word;overflow-wrap:break-word}.md-root .md-h1,.md-root .md-h2,.md-root .md-h3{margin:14px 0 8px;line-height:1.3;color:var(--ink)}.md-root .md-h1{font-size:22px;font-weight:600;border-bottom:1px solid var(--border);padding-bottom:5px}.md-root .md-h2{font-size:18px;font-weight:600}.md-root .md-h3{font-size:15px;font-weight:600;color:var(--ink2)}.md-root .md-p{margin:8px 0}.md-root .md-code{background:var(--surface2);border:1px solid var(--border);border-radius:3px;padding:1px 5px;font-family:var(--mono);font-size:12px;color:var(--ink)}.md-root .md-pre{background:var(--surface2);border:1px solid var(--border);border-radius:4px;padding:10px 12px;overflow-x:auto;margin:10px 0}.md-root .md-pre .md-code{background:transparent;border:0;padding:0;font-size:12px;line-height:1.5}.md-root .md-link{color:var(--accent);text-decoration:none;border-bottom:1px solid rgba(224,107,63,.3)}.md-root .md-link:hover{border-bottom-color:var(--accent)}.md-root .md-wiki{color:var(--indigo);font-weight:500}.md-root .md-wiki-missing{color:var(--ink3);border-bottom:1px dashed var(--ink3);cursor:not-allowed}.md-root .md-list{margin:8px 0 8px 22px;padding:0}.md-root .md-list li{margin:3px 0}.md-root .md-quote{border-left:3px solid var(--border2);padding:2px 12px;margin:10px 0;color:var(--ink2);font-style:italic}.proposal-status{display:inline-flex;align-items:center;padding:2px 7px;border-radius:3px;font-family:var(--mono);font-size:9px;letter-spacing:.04em;text-transform:uppercase}.proposal-status.active{background:var(--green-dim);color:var(--green)}.proposal-status.pending_review{background:var(--accent-dim);color:var(--accent)}.proposal-status.rejected{background:var(--rose-dim);color:var(--rose)}.inbox-row{border:1px solid var(--border);border-radius:4px;background:var(--surface);padding:10px 12px;display:flex;flex-direction:column;gap:6px;animation:dhee-card-in .18s ease}.inbox-row[data-severity=high]{border-left:3px solid var(--rose)}.inbox-row[data-severity=medium]{border-left:3px solid var(--accent)}.inbox-row[data-severity=low]{border-left:3px solid var(--indigo)}.workspace-pill{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:360px}.tokens-chip{cursor:default}.vault-leaf{display:flex;align-items:center;gap:6px;padding:4px 8px;border-radius:3px;cursor:pointer;font-size:12px;color:var(--ink2);background:transparent;border:0;text-align:left;width:100%}.vault-leaf:hover{background:var(--surface)}.vault-leaf[aria-selected=true]{background:var(--accent-dim);color:var(--accent)}.vault-leaf[data-status=pending_review]:before{content:"";width:6px;height:6px;border-radius:50%;background:var(--accent);flex-shrink:0}.vault-leaf[data-status=active]:before{content:"";width:6px;height:6px;border-radius:50%;background:var(--green);flex-shrink:0} diff --git a/dhee/ui/web/dist/dhee-logo.png b/dhee/ui/web/dist/dhee-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..8ab85fda2e6b545c76e81009e90b79928345c127 GIT binary patch literal 27501 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 + + + + +Sankhya — Dhee + + + + + + +
+ + diff --git a/dhee/ui/web/index.html b/dhee/ui/web/index.html new file mode 100644 index 0000000..90a70a8 --- /dev/null +++ b/dhee/ui/web/index.html @@ -0,0 +1,17 @@ + + + + + +Sankhya — Dhee + + + + +
+ + + diff --git a/dhee/ui/web/package-lock.json b/dhee/ui/web/package-lock.json new file mode 100644 index 0000000..0c8c7c4 --- /dev/null +++ b/dhee/ui/web/package-lock.json @@ -0,0 +1,2034 @@ +{ + "name": "sankhya", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sankhya", + "version": "1.0.0", + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "concurrently": "^9.2.1", + "typescript": "^5.5.3", + "vite": "^5.4.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.20", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.20.tgz", + "integrity": "sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001790", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001790.tgz", + "integrity": "sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.343", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.343.tgz", + "integrity": "sha512-YHnQ3MXI08icvL9ZKnEBy05F2EQ8ob01UaMOuMbM8l+4UcAq6MPPbBTJBbsBUg3H8JeZNt+O4fjsoWth3p6IFg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/dhee/ui/web/package.json b/dhee/ui/web/package.json new file mode 100644 index 0000000..76b18b3 --- /dev/null +++ b/dhee/ui/web/package.json @@ -0,0 +1,24 @@ +{ + "name": "sankhya", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "dev:all": "bash ../../../scripts/dev_ui.sh", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "concurrently": "^9.2.1", + "typescript": "^5.5.3", + "vite": "^5.4.0" + } +} diff --git a/dhee/ui/web/public/dhee-logo.png b/dhee/ui/web/public/dhee-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..8ab85fda2e6b545c76e81009e90b79928345c127 GIT binary patch literal 27501 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 import("./views/CanvasView").then((module) => ({ default: module.CanvasView }))); +const DEFAULT_TWEAKS = { + accentHue: "36", + compactNav: false, + showTimestamps: true, + canvasStyle: "force", +}; +const GLOBAL_REFRESH_MS = 15000; +const PRODUCT_VIEWS = new Set([ + "command", + "handoff", + "replay", + "learnings", + "portability", +]); +function isProductView(view) { + return PRODUCT_VIEWS.has(view); +} +function isGraphOnlyView(view) { + return view === "canvas"; +} +function isSelfHydratedView(view) { + return view === "router" || view === "router/sessionshistory"; +} +function normalizeView(view) { + const raw = String(view || "").toLowerCase(); + if (raw === "home" || raw === "overview") + return "command"; + if (raw === "memory") + return "context"; + if (raw === "firewall" || raw === "context-firewall") + return "router"; + if (raw === "repo" || raw === "brain" || raw === "repo-brain" || raw === "folders") + return "canvas"; + if (raw === "learn" || raw === "learning") + return "learnings"; + if (raw === "packs" || raw === "portable" || raw === "trust") + return "portability"; + if (raw === "router/sessionshistory" || + raw === "router/session-history" || + raw === "router/history") + return "router/sessionshistory"; + return raw || "command"; +} +function initialParam(name) { + if (typeof window === "undefined") + return ""; + return new URLSearchParams(window.location.search).get(name) || ""; +} +export default function App() { + const [view, setView] = useState(() => normalizeView(initialParam("view"))); + const [managerOpen, setManagerOpen] = useState(false); + const [managerTab, setManagerTab] = useState("workspaces"); + const [tasks, setTasks] = useState([]); + const [activeTaskId, setActiveTaskId] = useState(() => initialParam("task")); + const [tweaks, setTweaks] = useState(DEFAULT_TWEAKS); + const [showTweaks, setShowTweaks] = useState(false); + const [memoryCount, setMemoryCount] = useState(0); + const [memoriesCache, setMemoriesCache] = useState([]); + const [tokensSaved, setTokensSaved] = useState(0); + const [conflictCount, setConflictCount] = useState(0); + const [transitioning, setTransitioning] = useState(false); + const [workspaceGraph, setWorkspaceGraph] = useState(null); + const [orgGraph, setOrgGraph] = useState(null); + const [viewer, setViewer] = useState(null); + const [routerStats, setRouterStats] = useState(null); + const [inboxCount, setInboxCount] = useState(0); + const [projectIndex, setProjectIndex] = useState(null); + const [selectedProjectId, setSelectedProjectId] = useState(() => initialParam("project")); + const [selectedWorkspaceId, setSelectedWorkspaceId] = useState(() => initialParam("workspace")); + const [selectedSessionId, setSelectedSessionId] = useState(() => initialParam("session")); + const snapshotSignatures = useRef({}); + const updateSnapshot = (key, next, apply) => { + let signature = ""; + try { + signature = JSON.stringify(next) || ""; + } + catch { + signature = String(Date.now()); + } + if (snapshotSignatures.current[key] === signature) + return; + snapshotSignatures.current[key] = signature; + apply(next); + }; + useEffect(() => { + const params = new URLSearchParams(window.location.search); + const urlView = params.get("view"); + const urlTask = params.get("task"); + const urlProject = params.get("project"); + const urlWorkspace = params.get("workspace"); + const urlSession = params.get("session"); + if (urlView) + setView(normalizeView(urlView)); + if (urlTask) + setActiveTaskId(urlTask); + if (urlProject) + setSelectedProjectId(urlProject); + if (urlWorkspace) + setSelectedWorkspaceId(urlWorkspace); + if (urlSession) + setSelectedSessionId(urlSession); + const onPop = () => { + const next = new URLSearchParams(window.location.search); + setView(normalizeView(next.get("view"))); + setActiveTaskId(next.get("task") || ""); + setSelectedProjectId(next.get("project") || ""); + setSelectedWorkspaceId(next.get("workspace") || ""); + setSelectedSessionId(next.get("session") || ""); + }; + window.addEventListener("popstate", onPop); + return () => window.removeEventListener("popstate", onPop); + }, []); + const refreshTasks = async () => { + try { + const t = await api.tasks(); + updateSnapshot("tasks", t.tasks || [], setTasks); + setActiveTaskId((current) => current || t.tasks?.[0]?.id || ""); + } + catch { } + }; + const refreshWorkspaceGraph = async () => { + try { + const graph = selectedWorkspaceId + ? await api.workspaceGraph(selectedWorkspaceId, selectedProjectId || undefined) + : await api.workspaceGraph(); + updateSnapshot("workspaceGraph", graph, setWorkspaceGraph); + } + catch { } + }; + const refreshOrgGraph = async () => { + try { + // Active-only on the canvas — no stale or paused sessions clutter the + // FOLDERS view. Slice-1 server gates on ?active=true. + const g = await api.orgGraph(undefined, { active: true }); + updateSnapshot("orgGraph", g, setOrgGraph); + } + catch { } + }; + const refreshViewer = async () => { + try { + const v = await api.me(); + updateSnapshot("viewer", v, setViewer); + } + catch { } + }; + const refreshRouterStats = async () => { + try { + const s = await api.routerStats(); + updateSnapshot("routerStats", s, setRouterStats); + setTokensSaved(s.sessionTokensSaved || 0); + } + catch { } + }; + const refreshInbox = async () => { + try { + const box = await api.inbox(viewer?.team_id ? { team: viewer.team_id, user: viewer.user_id } : { user: viewer?.user_id }); + const total = (box.totals?.proposals || 0) + + (box.totals?.findings || 0) + + (box.totals?.conflicts || 0); + setInboxCount(total); + setConflictCount(box.totals?.conflicts || 0); + } + catch { } + }; + const refreshProjects = async () => { + try { + const snapshot = await api.workspaces(); + updateSnapshot("projectIndex", snapshot, setProjectIndex); + setSelectedWorkspaceId((current) => current || snapshot.currentWorkspaceId || snapshot.workspaces?.[0]?.id || ""); + setSelectedProjectId((current) => current || snapshot.currentProjectId || snapshot.workspaces?.[0]?.projects?.[0]?.id || ""); + setSelectedSessionId((current) => current || snapshot.currentSessionId || ""); + } + catch { } + }; + useEffect(() => { + void refreshViewer(); + void refreshInbox(); + if (!isProductView(view)) { + void refreshRouterStats(); + } + if (isGraphOnlyView(view)) { + void refreshOrgGraph(); + } + else if (isSelfHydratedView(view)) { + return; + } + else if (!isProductView(view)) { + void refreshTasks(); + void refreshOrgGraph(); + void refreshProjects().then(() => refreshWorkspaceGraph()); + void (async () => { + try { + const m = await api.listMemories(); + setMemoryCount(m.engrams?.length || 0); + setMemoriesCache(m.engrams || []); + } + catch { } + })(); + } + }, []); + useEffect(() => { + const timer = window.setInterval(() => { + if (isProductView(view)) { + void refreshInbox(); + return; + } + if (isSelfHydratedView(view)) { + void refreshRouterStats(); + void refreshInbox(); + return; + } + if (isGraphOnlyView(view)) { + void refreshOrgGraph(); + void refreshInbox(); + return; + } + void refreshTasks(); + void refreshProjects(); + void refreshWorkspaceGraph(); + void refreshOrgGraph(); + void refreshRouterStats(); + void refreshInbox(); + }, GLOBAL_REFRESH_MS); + return () => window.clearInterval(timer); + }, [view, selectedWorkspaceId, selectedProjectId, viewer?.team_id]); + useEffect(() => { + if (isProductView(view)) + return; + if (isSelfHydratedView(view)) { + void refreshRouterStats(); + void refreshInbox(); + return; + } + if (isGraphOnlyView(view)) { + void refreshOrgGraph(); + return; + } + void refreshTasks(); + void refreshProjects().then(() => refreshWorkspaceGraph()); + void refreshOrgGraph(); + void (async () => { + try { + const m = await api.listMemories(); + setMemoryCount(m.engrams?.length || 0); + setMemoriesCache(m.engrams || []); + } + catch { } + })(); + }, [view]); + const go = (v, taskId) => { + const targetView = normalizeView(v); + setTransitioning(true); + setTimeout(() => { + if (taskId) + setActiveTaskId(taskId); + setView(targetView); + const params = new URLSearchParams(window.location.search); + params.set("view", targetView); + if (taskId || activeTaskId) + params.set("task", taskId || activeTaskId); + else + params.delete("task"); + if (selectedProjectId) + params.set("project", selectedProjectId); + else + params.delete("project"); + if (selectedWorkspaceId) + params.set("workspace", selectedWorkspaceId); + else + params.delete("workspace"); + if (selectedSessionId) + params.set("session", selectedSessionId); + else + params.delete("session"); + const qs = params.toString(); + const next = qs ? `?${qs}` : ""; + const hash = targetView === "context" && window.location.hash.startsWith("#vault") + ? window.location.hash + : ""; + window.history.pushState({}, "", `${window.location.pathname}${next}${hash}`); + setTransitioning(false); + }, 140); + }; + const handleSelectTask = (id) => go("workspace", id); + const handleSelectSession = (sessionId, taskId) => { + if (sessionId) + setSelectedSessionId(sessionId); + if (taskId) + setActiveTaskId(taskId); + go("workspace", taskId || undefined); + }; + const handleSelectWorkspace = (workspaceId) => { + setSelectedWorkspaceId(workspaceId); + setSelectedProjectId(""); + setSelectedSessionId(""); + go("workspace"); + }; + const handleSelectProject = (projectId, workspaceId) => { + if (workspaceId) + setSelectedWorkspaceId(workspaceId); + setSelectedProjectId(projectId); + setSelectedSessionId(""); + go("workspace"); + }; + useEffect(() => { + const params = new URLSearchParams(window.location.search); + params.set("view", view); + if (activeTaskId) + params.set("task", activeTaskId); + else + params.delete("task"); + if (selectedProjectId) + params.set("project", selectedProjectId); + else + params.delete("project"); + if (selectedWorkspaceId) + params.set("workspace", selectedWorkspaceId); + else + params.delete("workspace"); + if (selectedSessionId) + params.set("session", selectedSessionId); + else + params.delete("session"); + const qs = params.toString(); + const next = `${window.location.pathname}${qs ? "?" + qs : ""}`; + if (next !== `${window.location.pathname}${window.location.search}`) { + window.history.replaceState({}, "", next); + } + }, [view, activeTaskId, selectedProjectId, selectedWorkspaceId, selectedSessionId]); + const handleCreateWorkspace = async (name) => { + await api.createWorkspaceRoot(name); + await refreshProjects(); + await refreshWorkspaceGraph(); + }; + const handleCreateProject = async (workspaceId, payload) => { + await api.createProject(workspaceId, payload); + await refreshProjects(); + await refreshWorkspaceGraph(); + }; + const handleUpdateProject = async (projectId, payload) => { + await api.updateProject(projectId, payload); + await refreshProjects(); + await refreshWorkspaceGraph(); + }; + const handleAddWorkspaceFolder = async (workspaceId, path, label) => { + await api.addWorkspaceFolder(workspaceId, path, label); + await refreshProjects(); + await refreshWorkspaceGraph(); + }; + const handleUpdateWorkspace = async (workspaceId, label) => { + await api.updateWorkspace(workspaceId, { label }); + await refreshProjects(); + await refreshWorkspaceGraph(); + }; + const handleRemoveWorkspaceFolder = async (workspaceId, path) => { + await api.removeWorkspaceFolder(workspaceId, path); + await refreshProjects(); + await refreshWorkspaceGraph(); + }; + const handleAddTask = async (title) => { + try { + const res = await api.createTask(title); + setTasks((t) => [...t, res.task]); + await refreshProjects(); + setTimeout(() => go("workspace", res.task.id), 80); + } + catch (e) { + console.warn("createTask failed", e); + } + }; + const handleAddMemory = async (text) => { + try { + await api.remember(text, "short-term", []); + setMemoryCount((c) => c + 1); + const m = await api.listMemories(); + setMemoriesCache(m.engrams || []); + } + catch (e) { + console.warn("remember failed", e); + } + }; + const handleLaunchFromNotepad = async (title, runtime, workspaceId, permissionMode, projectId) => { + const targetWorkspace = workspaceId || selectedWorkspaceId || projectIndex?.currentWorkspaceId; + if (!targetWorkspace || !title.trim()) + return; + const res = await api.launchWorkspaceSession(targetWorkspace, runtime, title.trim(), runtime === "claude-code" ? permissionMode : undefined, undefined, projectId || selectedProjectId || projectIndex?.currentProjectId); + await refreshTasks(); + await refreshProjects(); + await refreshWorkspaceGraph(); + if (res.session_id) + setSelectedSessionId(res.session_id); + if (res.task_id) + setActiveTaskId(res.task_id); + go("workspace", res.task_id || undefined); + }; + const handleAddTaskNote = async (taskId, content) => { + try { + await api.addTaskNote(taskId, content); + await refreshTasks(); + await refreshProjects(); + } + catch (e) { + console.warn("addTaskNote failed", e); + } + }; + const viewStyle = { + opacity: transitioning ? 0 : 1, + transform: transitioning ? "translateY(3px)" : "translateY(0)", + transition: "opacity 0.14s ease, transform 0.14s ease", + flex: 1, + overflow: "hidden", + display: "flex", + flexDirection: "column", + }; + const renderView = () => { + if (view === "command") + return _jsx(CommandCenterView, { onNavigate: (target) => go(target) }); + if (view === "channel") + return (_jsx(ChannelView, { projectIndex: projectIndex, workspaceGraph: workspaceGraph, tasks: tasks, viewer: viewer, orgGraph: orgGraph, selectedWorkspaceId: selectedWorkspaceId, selectedProjectId: selectedProjectId, onSelectWorkspace: handleSelectWorkspace, onSelectProject: handleSelectProject, onSelectTask: handleSelectTask, onTasksRefresh: refreshTasks, onOpenCanvas: () => go("canvas"), onLaunchSession: handleLaunchFromNotepad, onOpenManager: (tab) => { + setManagerTab(tab || "workspaces"); + setManagerOpen(true); + }, tweaks: tweaks })); + if (view === "notepad") + return (_jsx(NotepadView, { projectIndex: projectIndex, memories: memoryCount, tokensSaved: tokensSaved, onAddTask: handleAddTask, onAddMemory: handleAddMemory, onSelectSession: handleSelectSession, onCreateWorkspace: handleCreateWorkspace, onLaunchSession: handleLaunchFromNotepad, onCreateProject: handleCreateProject, onOpenWorkspace: () => go("workspace"), onOpenTasks: () => go("tasks"), tweaks: tweaks })); + if (view === "tasks") + return (_jsx(TasksView, { tasks: tasks, projectIndex: projectIndex, onSelectTask: handleSelectTask, onSelectSession: handleSelectSession, tweaks: tweaks })); + if (view === "workspace") + return (_jsx(WorkspaceView, { tasks: tasks, activeTaskId: activeTaskId, selectedProjectId: selectedProjectId, selectedWorkspaceId: selectedWorkspaceId, selectedSessionId: selectedSessionId, projectIndex: projectIndex, workspaceGraph: workspaceGraph, onSelectTask: handleSelectTask, onSelectSession: handleSelectSession, onSelectProject: handleSelectProject, onCanvasOpen: () => go("canvas"), onNotepadOpen: () => go("context"), onAddTaskNote: handleAddTaskNote, onUpdateWorkspace: handleUpdateWorkspace, onAddWorkspaceFolder: handleAddWorkspaceFolder, onRemoveWorkspaceFolder: handleRemoveWorkspaceFolder, onCreateProject: handleCreateProject, onUpdateProject: handleUpdateProject, onTasksRefresh: refreshTasks, tweaks: tweaks })); + if (view === "canvas") + return (_jsx(Suspense, { fallback: _jsx("div", { style: { + height: "100%", + display: "grid", + alignItems: "start", + background: "var(--surface)", + padding: 20, + }, children: _jsxs("div", { style: { + border: "1px solid var(--border)", + background: "white", + padding: "16px 18px", + display: "flex", + justifyContent: "space-between", + alignItems: "center", + gap: 18, + }, children: [_jsxs("div", { children: [_jsx("div", { style: { + fontFamily: "var(--mono)", + fontSize: 10, + color: "var(--green)", + letterSpacing: "0.12em", + textTransform: "uppercase", + }, children: "Repo Brain" }), _jsx("div", { style: { marginTop: 6, fontSize: 20, fontWeight: 650 }, children: "Loading folder canvas" })] }), _jsx("div", { style: { + fontFamily: "var(--mono)", + fontSize: 10, + color: "var(--ink3)", + textTransform: "uppercase", + }, children: "Context Vault" })] }) }), children: _jsx(CanvasView, { tasks: tasks, selectedProjectId: selectedProjectId, workspaceGraph: workspaceGraph, orgGraph: orgGraph, viewer: viewer, onOpenVault: (teamId) => { + if (teamId) + window.location.hash = `#vault/${teamId}`; + go("context"); + }, onOrgGraphChanged: () => { + void refreshOrgGraph(); + void refreshViewer(); + void refreshInbox(); + }, onSelectTask: handleSelectTask, onSelectSession: handleSelectSession, onSelectWorkspace: handleSelectWorkspace, onSelectProject: handleSelectProject, onClose: () => go("workspace"), tweaks: tweaks }) })); + if (view === "memory" || view === "context") + return (_jsx(MemoryView, { onMemoryCountChange: setMemoryCount, viewer: viewer, orgGraph: orgGraph, onInboxChanged: refreshInbox })); + if (view === "router" || view === "router/sessionshistory") + return _jsx(RouterView, { onOpenFolders: () => go("canvas"), onOpenSetup: () => go("notepad") }); + if (view === "handoff") + return _jsx(HandoffHubView, {}); + if (view === "replay") + return _jsx(ProofReplayView, {}); + if (view === "learnings") + return _jsx(LearningInboxView, {}); + if (view === "portability") + return _jsx(PortabilityTrustView, {}); + if (view === "conflicts") + return _jsx(ConflictView, { viewer: viewer, onChanged: refreshInbox }); + return null; + }; + useEffect(() => { + // Keyboard shortcut: Ctrl/Cmd+K shows tweaks panel. + const onKey = (e) => { + if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") { + e.preventDefault(); + setShowTweaks((v) => !v); + } + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, []); + const handleRefreshAll = () => { + void refreshViewer(); + void refreshTasks(); + void refreshProjects(); + void refreshWorkspaceGraph(); + void refreshOrgGraph(); + void refreshRouterStats(); + void refreshInbox(); + }; + return (_jsxs("div", { style: { height: "100vh", display: "flex", overflow: "hidden" }, children: [_jsx(NavRail, { view: view, setView: (v) => go(v), conflictCount: inboxCount || conflictCount }), _jsxs("div", { style: { + flex: 1, + display: "flex", + flexDirection: "column", + overflow: "hidden", + }, children: [_jsx(TopBar, { viewer: viewer, routerStats: routerStats, onRefresh: handleRefreshAll, onOpenTweaks: () => setShowTweaks((v) => !v), onResetWorkspace: async () => { + if (!window.confirm("Reset workspace? Deletes projects, teams, folders, context items, proposals and findings for this org. Memory engrams are unaffected.")) + return; + try { + await api.enterpriseResetWorkspace(); + } + finally { + handleRefreshAll(); + } + } }), _jsx("div", { style: viewStyle, children: renderView() })] }), _jsx(TweaksPanel, { tweaks: tweaks, setTweaks: setTweaks, visible: showTweaks }), _jsx(WorkspaceManagerModal, { open: managerOpen, onClose: () => setManagerOpen(false), projectIndex: projectIndex, initialWorkspaceId: selectedWorkspaceId, initialTab: managerTab, onChanged: async () => { + await refreshProjects(); + await refreshWorkspaceGraph(); + } })] })); +} diff --git a/dhee/ui/web/src/App.tsx b/dhee/ui/web/src/App.tsx new file mode 100644 index 0000000..402ac4d --- /dev/null +++ b/dhee/ui/web/src/App.tsx @@ -0,0 +1,714 @@ +import { Suspense, lazy, useEffect, useRef, useState } from "react"; +import { api } from "./api"; +import { NavRail, type View } from "./components/NavRail"; +import { TopBar } from "./components/TopBar"; +import { TweaksPanel } from "./components/TweaksPanel"; +import { WorkspaceManagerModal } from "./components/WorkspaceManagerModal"; +import type { + Engram, + OrgGraphSnapshot, + ProjectIndexSnapshot, + RouterStats, + SankhyaTask, + Tweaks, + Viewer, + WorkspaceGraphSnapshot, +} from "./types"; +import { ChannelView } from "./views/ChannelView"; +import { ConflictView } from "./views/ConflictView"; +import { MemoryView } from "./views/MemoryView"; +import { NotepadView } from "./views/NotepadView"; +import { + CommandCenterView, + HandoffHubView, + LearningInboxView, + PortabilityTrustView, + ProofReplayView, +} from "./views/ProductViews"; +import { RouterView } from "./views/RouterView"; +import { TasksView } from "./views/TasksView"; +import { WorkspaceView } from "./views/WorkspaceView"; + +const CanvasView = lazy(() => + import("./views/CanvasView").then((module) => ({ default: module.CanvasView })) +); + +const DEFAULT_TWEAKS: Tweaks = { + accentHue: "36", + compactNav: false, + showTimestamps: true, + canvasStyle: "force", +}; +const GLOBAL_REFRESH_MS = 15_000; +const PRODUCT_VIEWS = new Set([ + "command", + "handoff", + "replay", + "learnings", + "portability", +]); + +function isProductView(view: View): boolean { + return PRODUCT_VIEWS.has(view); +} + +function isGraphOnlyView(view: View): boolean { + return view === "canvas"; +} + +function isSelfHydratedView(view: View): boolean { + return view === "router" || view === "router/sessionshistory"; +} + +function normalizeView(view: View | string | null): View { + const raw = String(view || "").toLowerCase(); + if (raw === "home" || raw === "overview") return "command"; + if (raw === "memory") return "context"; + if (raw === "firewall" || raw === "context-firewall") return "router"; + if (raw === "repo" || raw === "brain" || raw === "repo-brain" || raw === "folders") return "canvas"; + if (raw === "learn" || raw === "learning") return "learnings"; + if (raw === "packs" || raw === "portable" || raw === "trust") return "portability"; + if ( + raw === "router/sessionshistory" || + raw === "router/session-history" || + raw === "router/history" + ) + return "router/sessionshistory"; + return (raw as View) || "command"; +} + +function initialParam(name: string): string { + if (typeof window === "undefined") return ""; + return new URLSearchParams(window.location.search).get(name) || ""; +} + +export default function App() { + const [view, setView] = useState(() => normalizeView(initialParam("view"))); + const [managerOpen, setManagerOpen] = useState(false); + const [managerTab, setManagerTab] = useState<"workspaces" | "projects">("workspaces"); + const [tasks, setTasks] = useState([]); + const [activeTaskId, setActiveTaskId] = useState(() => initialParam("task")); + const [tweaks, setTweaks] = useState(DEFAULT_TWEAKS); + const [showTweaks, setShowTweaks] = useState(false); + const [memoryCount, setMemoryCount] = useState(0); + const [memoriesCache, setMemoriesCache] = useState([]); + const [tokensSaved, setTokensSaved] = useState(0); + const [conflictCount, setConflictCount] = useState(0); + const [transitioning, setTransitioning] = useState(false); + const [workspaceGraph, setWorkspaceGraph] = + useState(null); + const [orgGraph, setOrgGraph] = useState(null); + const [viewer, setViewer] = useState(null); + const [routerStats, setRouterStats] = useState(null); + const [inboxCount, setInboxCount] = useState(0); + const [projectIndex, setProjectIndex] = useState(null); + const [selectedProjectId, setSelectedProjectId] = useState(() => initialParam("project")); + const [selectedWorkspaceId, setSelectedWorkspaceId] = useState(() => initialParam("workspace")); + const [selectedSessionId, setSelectedSessionId] = useState(() => initialParam("session")); + const snapshotSignatures = useRef>({}); + + const updateSnapshot = ( + key: string, + next: T, + apply: (value: T) => void + ) => { + let signature = ""; + try { + signature = JSON.stringify(next) || ""; + } catch { + signature = String(Date.now()); + } + if (snapshotSignatures.current[key] === signature) return; + snapshotSignatures.current[key] = signature; + apply(next); + }; + + useEffect(() => { + const params = new URLSearchParams(window.location.search); + const urlView = params.get("view") as View | null; + const urlTask = params.get("task"); + const urlProject = params.get("project"); + const urlWorkspace = params.get("workspace"); + const urlSession = params.get("session"); + if (urlView) setView(normalizeView(urlView)); + if (urlTask) setActiveTaskId(urlTask); + if (urlProject) setSelectedProjectId(urlProject); + if (urlWorkspace) setSelectedWorkspaceId(urlWorkspace); + if (urlSession) setSelectedSessionId(urlSession); + const onPop = () => { + const next = new URLSearchParams(window.location.search); + setView(normalizeView(next.get("view") as View | null)); + setActiveTaskId(next.get("task") || ""); + setSelectedProjectId(next.get("project") || ""); + setSelectedWorkspaceId(next.get("workspace") || ""); + setSelectedSessionId(next.get("session") || ""); + }; + window.addEventListener("popstate", onPop); + return () => window.removeEventListener("popstate", onPop); + }, []); + + const refreshTasks = async () => { + try { + const t = await api.tasks(); + updateSnapshot("tasks", t.tasks || [], setTasks); + setActiveTaskId((current) => current || t.tasks?.[0]?.id || ""); + } catch {} + }; + + const refreshWorkspaceGraph = async () => { + try { + const graph = selectedWorkspaceId + ? await api.workspaceGraph(selectedWorkspaceId, selectedProjectId || undefined) + : await api.workspaceGraph(); + updateSnapshot("workspaceGraph", graph, setWorkspaceGraph); + } catch {} + }; + + const refreshOrgGraph = async () => { + try { + // Active-only on the canvas — no stale or paused sessions clutter the + // FOLDERS view. Slice-1 server gates on ?active=true. + const g = await api.orgGraph(undefined, { active: true }); + updateSnapshot("orgGraph", g, setOrgGraph); + } catch {} + }; + + const refreshViewer = async () => { + try { + const v = await api.me(); + updateSnapshot("viewer", v, setViewer); + } catch {} + }; + + const refreshRouterStats = async () => { + try { + const s = await api.routerStats(); + updateSnapshot("routerStats", s, setRouterStats); + setTokensSaved(s.sessionTokensSaved || 0); + } catch {} + }; + + const refreshInbox = async () => { + try { + const box = await api.inbox( + viewer?.team_id ? { team: viewer.team_id, user: viewer.user_id } : { user: viewer?.user_id } + ); + const total = + (box.totals?.proposals || 0) + + (box.totals?.findings || 0) + + (box.totals?.conflicts || 0); + setInboxCount(total); + setConflictCount(box.totals?.conflicts || 0); + } catch {} + }; + + const refreshProjects = async () => { + try { + const snapshot = await api.workspaces(); + updateSnapshot("projectIndex", snapshot, setProjectIndex); + setSelectedWorkspaceId((current) => current || snapshot.currentWorkspaceId || snapshot.workspaces?.[0]?.id || ""); + setSelectedProjectId((current) => current || snapshot.currentProjectId || snapshot.workspaces?.[0]?.projects?.[0]?.id || ""); + setSelectedSessionId((current) => current || snapshot.currentSessionId || ""); + } catch {} + }; + + useEffect(() => { + void refreshViewer(); + void refreshInbox(); + if (!isProductView(view)) { + void refreshRouterStats(); + } + if (isGraphOnlyView(view)) { + void refreshOrgGraph(); + } else if (isSelfHydratedView(view)) { + return; + } else if (!isProductView(view)) { + void refreshTasks(); + void refreshOrgGraph(); + void refreshProjects().then(() => refreshWorkspaceGraph()); + void (async () => { + try { + const m = await api.listMemories(); + setMemoryCount(m.engrams?.length || 0); + setMemoriesCache(m.engrams || []); + } catch {} + })(); + } + }, []); + + useEffect(() => { + const timer = window.setInterval(() => { + if (isProductView(view)) { + void refreshInbox(); + return; + } + if (isSelfHydratedView(view)) { + void refreshRouterStats(); + void refreshInbox(); + return; + } + if (isGraphOnlyView(view)) { + void refreshOrgGraph(); + void refreshInbox(); + return; + } + void refreshTasks(); + void refreshProjects(); + void refreshWorkspaceGraph(); + void refreshOrgGraph(); + void refreshRouterStats(); + void refreshInbox(); + }, GLOBAL_REFRESH_MS); + return () => window.clearInterval(timer); + }, [view, selectedWorkspaceId, selectedProjectId, viewer?.team_id]); + + useEffect(() => { + if (isProductView(view)) return; + if (isSelfHydratedView(view)) { + void refreshRouterStats(); + void refreshInbox(); + return; + } + if (isGraphOnlyView(view)) { + void refreshOrgGraph(); + return; + } + void refreshTasks(); + void refreshProjects().then(() => refreshWorkspaceGraph()); + void refreshOrgGraph(); + void (async () => { + try { + const m = await api.listMemories(); + setMemoryCount(m.engrams?.length || 0); + setMemoriesCache(m.engrams || []); + } catch {} + })(); + }, [view]); + + const go = (v: View, taskId?: string) => { + const targetView = normalizeView(v); + setTransitioning(true); + setTimeout(() => { + if (taskId) setActiveTaskId(taskId); + setView(targetView); + const params = new URLSearchParams(window.location.search); + params.set("view", targetView); + if (taskId || activeTaskId) params.set("task", taskId || activeTaskId); + else params.delete("task"); + + if (selectedProjectId) params.set("project", selectedProjectId); + else params.delete("project"); + + if (selectedWorkspaceId) params.set("workspace", selectedWorkspaceId); + else params.delete("workspace"); + + if (selectedSessionId) params.set("session", selectedSessionId); + else params.delete("session"); + + const qs = params.toString(); + const next = qs ? `?${qs}` : ""; + const hash = + targetView === "context" && window.location.hash.startsWith("#vault") + ? window.location.hash + : ""; + window.history.pushState({}, "", `${window.location.pathname}${next}${hash}`); + setTransitioning(false); + }, 140); + }; + + const handleSelectTask = (id: string) => go("workspace", id); + const handleSelectSession = (sessionId: string, taskId?: string | null) => { + if (sessionId) setSelectedSessionId(sessionId); + if (taskId) setActiveTaskId(taskId); + go("workspace", taskId || undefined); + }; + const handleSelectWorkspace = (workspaceId: string) => { + setSelectedWorkspaceId(workspaceId); + setSelectedProjectId(""); + setSelectedSessionId(""); + go("workspace"); + }; + const handleSelectProject = (projectId: string, workspaceId?: string | null) => { + if (workspaceId) setSelectedWorkspaceId(workspaceId); + setSelectedProjectId(projectId); + setSelectedSessionId(""); + go("workspace"); + }; + + useEffect(() => { + const params = new URLSearchParams(window.location.search); + params.set("view", view); + if (activeTaskId) params.set("task", activeTaskId); else params.delete("task"); + if (selectedProjectId) params.set("project", selectedProjectId); else params.delete("project"); + if (selectedWorkspaceId) params.set("workspace", selectedWorkspaceId); else params.delete("workspace"); + if (selectedSessionId) params.set("session", selectedSessionId); else params.delete("session"); + + const qs = params.toString(); + const next = `${window.location.pathname}${qs ? "?" + qs : ""}`; + if (next !== `${window.location.pathname}${window.location.search}`) { + window.history.replaceState({}, "", next); + } + }, [view, activeTaskId, selectedProjectId, selectedWorkspaceId, selectedSessionId]); + + const handleCreateWorkspace = async (name: string) => { + await api.createWorkspaceRoot(name); + await refreshProjects(); + await refreshWorkspaceGraph(); + }; + + const handleCreateProject = async ( + workspaceId: string, + payload: { name: string; description?: string; default_runtime?: string; scope_rules?: { path_prefix: string; label?: string }[] } + ) => { + await api.createProject(workspaceId, payload); + await refreshProjects(); + await refreshWorkspaceGraph(); + }; + + const handleUpdateProject = async ( + projectId: string, + payload: { name?: string; description?: string; default_runtime?: string; scope_rules?: { path_prefix: string; label?: string }[] } + ) => { + await api.updateProject(projectId, payload); + await refreshProjects(); + await refreshWorkspaceGraph(); + }; + + const handleAddWorkspaceFolder = async (workspaceId: string, path: string, label?: string) => { + await api.addWorkspaceFolder(workspaceId, path, label); + await refreshProjects(); + await refreshWorkspaceGraph(); + }; + + const handleUpdateWorkspace = async (workspaceId: string, label: string) => { + await api.updateWorkspace(workspaceId, { label }); + await refreshProjects(); + await refreshWorkspaceGraph(); + }; + + const handleRemoveWorkspaceFolder = async (workspaceId: string, path: string) => { + await api.removeWorkspaceFolder(workspaceId, path); + await refreshProjects(); + await refreshWorkspaceGraph(); + }; + + const handleAddTask = async (title: string) => { + try { + const res = await api.createTask(title); + setTasks((t) => [...t, res.task]); + await refreshProjects(); + setTimeout(() => go("workspace", res.task.id), 80); + } catch (e) { + console.warn("createTask failed", e); + } + }; + + const handleAddMemory = async (text: string) => { + try { + await api.remember(text, "short-term", []); + setMemoryCount((c) => c + 1); + const m = await api.listMemories(); + setMemoriesCache(m.engrams || []); + } catch (e) { + console.warn("remember failed", e); + } + }; + + const handleLaunchFromNotepad = async ( + title: string, + runtime: "claude-code" | "codex", + workspaceId?: string, + permissionMode?: "standard" | "full-access", + projectId?: string + ) => { + const targetWorkspace = workspaceId || selectedWorkspaceId || projectIndex?.currentWorkspaceId; + if (!targetWorkspace || !title.trim()) return; + const res = await api.launchWorkspaceSession( + targetWorkspace, + runtime, + title.trim(), + runtime === "claude-code" ? permissionMode : undefined, + undefined, + projectId || selectedProjectId || projectIndex?.currentProjectId + ); + await refreshTasks(); + await refreshProjects(); + await refreshWorkspaceGraph(); + if (res.session_id) setSelectedSessionId(res.session_id); + if (res.task_id) setActiveTaskId(res.task_id); + go("workspace", res.task_id || undefined); + }; + + const handleAddTaskNote = async (taskId: string, content: string) => { + try { + await api.addTaskNote(taskId, content); + await refreshTasks(); + await refreshProjects(); + } catch (e) { + console.warn("addTaskNote failed", e); + } + }; + + const viewStyle: React.CSSProperties = { + opacity: transitioning ? 0 : 1, + transform: transitioning ? "translateY(3px)" : "translateY(0)", + transition: "opacity 0.14s ease, transform 0.14s ease", + flex: 1, + overflow: "hidden", + display: "flex", + flexDirection: "column", + }; + + const renderView = () => { + if (view === "command") + return go(target)} />; + if (view === "channel") + return ( + go("canvas")} + onLaunchSession={handleLaunchFromNotepad} + onOpenManager={(tab) => { + setManagerTab(tab || "workspaces"); + setManagerOpen(true); + }} + tweaks={tweaks} + /> + ); + if (view === "notepad") + return ( + go("workspace")} + onOpenTasks={() => go("tasks")} + tweaks={tweaks} + /> + ); + if (view === "tasks") + return ( + + ); + if (view === "workspace") + return ( + go("canvas")} + onNotepadOpen={() => go("context")} + onAddTaskNote={handleAddTaskNote} + onUpdateWorkspace={handleUpdateWorkspace} + onAddWorkspaceFolder={handleAddWorkspaceFolder} + onRemoveWorkspaceFolder={handleRemoveWorkspaceFolder} + onCreateProject={handleCreateProject} + onUpdateProject={handleUpdateProject} + onTasksRefresh={refreshTasks} + tweaks={tweaks} + /> + ); + if (view === "canvas") + return ( + +
+
+
+ Repo Brain +
+
+ Loading folder canvas +
+
+
+ Context Vault +
+
+ + } + > + { + if (teamId) window.location.hash = `#vault/${teamId}`; + go("context"); + }} + onOrgGraphChanged={() => { + void refreshOrgGraph(); + void refreshViewer(); + void refreshInbox(); + }} + onSelectTask={handleSelectTask} + onSelectSession={handleSelectSession} + onSelectWorkspace={handleSelectWorkspace} + onSelectProject={handleSelectProject} + onClose={() => go("workspace")} + tweaks={tweaks} + /> +
+ ); + if (view === "memory" || view === "context") + return ( + + ); + if (view === "router" || view === "router/sessionshistory") + return go("canvas")} onOpenSetup={() => go("notepad")} />; + if (view === "handoff") + return ; + if (view === "replay") + return ; + if (view === "learnings") + return ; + if (view === "portability") + return ; + if (view === "conflicts") + return ; + return null; + }; + + useEffect(() => { + // Keyboard shortcut: Ctrl/Cmd+K shows tweaks panel. + const onKey = (e: KeyboardEvent) => { + if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") { + e.preventDefault(); + setShowTweaks((v) => !v); + } + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, []); + + const handleRefreshAll = () => { + void refreshViewer(); + void refreshTasks(); + void refreshProjects(); + void refreshWorkspaceGraph(); + void refreshOrgGraph(); + void refreshRouterStats(); + void refreshInbox(); + }; + + return ( +
+ go(v)} + conflictCount={inboxCount || conflictCount} + /> +
+ setShowTweaks((v) => !v)} + onResetWorkspace={async () => { + if ( + !window.confirm( + "Reset workspace? Deletes projects, teams, folders, context items, proposals and findings for this org. Memory engrams are unaffected." + ) + ) + return; + try { + await api.enterpriseResetWorkspace(); + } finally { + handleRefreshAll(); + } + }} + /> +
{renderView()}
+
+ + setManagerOpen(false)} + projectIndex={projectIndex} + initialWorkspaceId={selectedWorkspaceId} + initialTab={managerTab} + onChanged={async () => { + await refreshProjects(); + await refreshWorkspaceGraph(); + }} + /> +
+ ); +} diff --git a/dhee/ui/web/src/api.js b/dhee/ui/web/src/api.js new file mode 100644 index 0000000..cebd047 --- /dev/null +++ b/dhee/ui/web/src/api.js @@ -0,0 +1,356 @@ +const BASE = "/api"; +async function j(path, init) { + const res = await fetch(BASE + path, { + ...init, + headers: { + "Content-Type": "application/json", + ...(init?.headers || {}), + }, + }); + if (!res.ok) + throw new Error(`${res.status} ${res.statusText}`); + return (await res.json()); +} +export const api = { + listMemories: () => j("/memories"), + remember: (content, tier, tags) => j("/memories", { + method: "POST", + body: JSON.stringify({ content, tier, tags }), + }), + archiveMemory: (id) => j(`/memories/${encodeURIComponent(id)}`, { + method: "DELETE", + }), + routerStats: (agentId) => j(`/router/stats${agentId ? `?agent_id=${encodeURIComponent(agentId)}` : ""}`), + routerPolicy: () => j("/router/policy"), + routerTune: () => j("/router/tune", { method: "POST" }), + metaBuddhi: () => j("/meta-buddhi"), + evolution: () => j("/evolution"), + conflicts: () => j("/conflicts"), + resolveConflict: (id, action) => j(`/conflicts/${encodeURIComponent(id)}/resolve`, { + method: "POST", + body: JSON.stringify({ action }), + }), + resolveConflictDetailed: (id, payload) => j(`/conflicts/${encodeURIComponent(id)}/resolve`, { + method: "POST", + body: JSON.stringify(payload), + }), + tasks: () => j("/tasks"), + createTask: (title, harness) => j("/tasks", { + method: "POST", + body: JSON.stringify({ title, harness }), + }), + taskDetail: (taskId, limit = 24) => j(`/tasks/${encodeURIComponent(taskId)}?limit=${encodeURIComponent(String(limit))}`), + updateTaskStatus: (taskId, status) => j(`/tasks/${encodeURIComponent(taskId)}/status`, { + method: "POST", + body: JSON.stringify({ status }), + }), + addTaskNote: (taskId, content) => j(`/tasks/${encodeURIComponent(taskId)}/notes`, { + method: "POST", + body: JSON.stringify({ content }), + }), + workspaceGraph: (workspaceId, projectId) => j(`/workspace/graph${workspaceId + ? `?workspace_id=${encodeURIComponent(workspaceId)}${projectId ? `&project_id=${encodeURIComponent(projectId)}` : ""}` + : ""}`), + projects: () => j("/workspaces"), + workspaces: () => j("/workspaces"), + createWorkspaceRoot: (name, description) => j("/workspaces", { + method: "POST", + body: JSON.stringify({ name, description }), + }), + createProject: (workspaceId, payload) => j(`/workspaces/${encodeURIComponent(workspaceId)}/projects`, { + method: "POST", + body: JSON.stringify(payload), + }), + updateProject: (projectId, payload) => j(`/projects/${encodeURIComponent(projectId)}`, { + method: "PATCH", + body: JSON.stringify(payload), + }), + projectSessions: (projectId) => j(`/projects/${encodeURIComponent(projectId)}/sessions`), + projectCanvas: (projectId) => j(`/projects/${encodeURIComponent(projectId)}/canvas`), + workspaceCanvas: (workspaceId) => j(`/workspaces/${encodeURIComponent(workspaceId)}/canvas`), + pickFolder: (prompt) => j("/folders/pick", { + method: "POST", + body: JSON.stringify({ prompt }), + }), + addWorkspaceFolder: (workspaceId, path, label) => j(`/workspaces/${encodeURIComponent(workspaceId)}/folders`, { + method: "POST", + body: JSON.stringify({ path, label }), + }), + removeWorkspaceFolder: (workspaceId, path) => j(`/workspaces/${encodeURIComponent(workspaceId)}/mounts?path=${encodeURIComponent(path)}`, { method: "DELETE" }), + updateWorkspace: (workspaceId, payload) => j(`/workspaces/${encodeURIComponent(workspaceId)}`, { + method: "PATCH", + body: JSON.stringify(payload), + }), + deleteWorkspace: (workspaceId) => j(`/workspaces/${encodeURIComponent(workspaceId)}`, { method: "DELETE" }), + deleteProject: (projectId) => j(`/projects/${encodeURIComponent(projectId)}`, { method: "DELETE" }), + workspaceDetail: (workspaceId) => j(`/workspaces/${encodeURIComponent(workspaceId)}`), + workspaceSessions: (workspaceId) => j(`/workspaces/${encodeURIComponent(workspaceId)}/sessions`), + sessionDetail: (sessionId) => j(`/sessions/${encodeURIComponent(sessionId)}`), + launchWorkspaceSession: (workspaceId, runtime, title, permission_mode, task_id, project_id) => j(`/workspaces/${encodeURIComponent(workspaceId)}/sessions/launch`, { + method: "POST", + body: JSON.stringify({ runtime, title, permission_mode, task_id, project_id }), + }), + workspaceLineMessages: (workspaceId, opts) => j(`/workspaces/${encodeURIComponent(workspaceId)}/line/messages?${new URLSearchParams(Object.entries({ + project_id: opts?.project_id, + channel: opts?.channel, + cursor: opts?.cursor, + limit: opts?.limit ? String(opts.limit) : undefined, + }).filter((entry) => Boolean(entry[1]))).toString()}`), + publishWorkspaceLineMessage: (workspaceId, payload) => j(`/workspaces/${encodeURIComponent(workspaceId)}/line/messages`, { + method: "POST", + body: JSON.stringify(payload), + }), + uploadSessionAsset: async (sessionId, file, label) => { + const form = new FormData(); + form.append("file", file); + if (label) + form.append("label", label); + const res = await fetch(`${BASE}/sessions/${encodeURIComponent(sessionId)}/assets`, { + method: "POST", + body: form, + }); + if (!res.ok) + throw new Error(`${res.status} ${res.statusText}`); + return (await res.json()); + }, + listProjectAssets: (projectId) => j(`/projects/${encodeURIComponent(projectId)}/assets`), + listWorkspaceAssets: (workspaceId, includeProjectAssets = true) => j(`/workspaces/${encodeURIComponent(workspaceId)}/assets?include_project_assets=${includeProjectAssets ? "true" : "false"}`), + uploadProjectAsset: async (projectId, file, opts) => { + const form = new FormData(); + form.append("file", file); + if (opts?.label) + form.append("label", opts.label); + if (opts?.folder) + form.append("folder", opts.folder); + const res = await fetch(`${BASE}/projects/${encodeURIComponent(projectId)}/assets`, { + method: "POST", + body: form, + }); + if (!res.ok) + throw new Error(`${res.status} ${res.statusText}`); + return (await res.json()); + }, + uploadWorkspaceAsset: async (workspaceId, file, opts) => { + const form = new FormData(); + form.append("file", file); + if (opts?.label) + form.append("label", opts.label); + if (opts?.folder) + form.append("folder", opts.folder); + if (opts?.project_id) + form.append("project_id", opts.project_id); + const res = await fetch(`${BASE}/workspaces/${encodeURIComponent(workspaceId)}/assets`, { + method: "POST", + body: form, + }); + if (!res.ok) + throw new Error(`${res.status} ${res.statusText}`); + return (await res.json()); + }, + deleteProjectAsset: (assetId) => j(`/project-assets/${encodeURIComponent(assetId)}`, { method: "DELETE" }), + fileContext: (path, workspaceId) => j(`/files/${encodeURIComponent(path)}/context${workspaceId ? `?workspace_id=${encodeURIComponent(workspaceId)}` : ""}`), + assetContext: (assetId) => j(`/assets/${encodeURIComponent(assetId)}/context`), + askAsset: (assetId, question) => j(`/assets/${encodeURIComponent(assetId)}/ask`, { + method: "POST", + body: JSON.stringify({ question }), + }), + runtimeStatus: () => j("/runtime-status"), + status: () => j("/status"), + memoryNow: () => j("/memory/now"), + captureTimeline: (limit = 16) => j(`/capture/timeline?limit=${encodeURIComponent(String(limit))}`), + launch: (runtime, taskId, title) => j("/launch", { + method: "POST", + body: JSON.stringify({ runtime, taskId, title }), + }), + apiKeys: () => j("/security/api-keys"), + storeApiKey: (provider, apiKey, label) => j("/security/api-keys", { + method: "POST", + body: JSON.stringify({ provider, apiKey, label }), + }), + rotateApiKey: (provider, apiKey, label) => j(`/security/api-keys/${encodeURIComponent(provider)}/rotate`, { + method: "POST", + body: JSON.stringify({ apiKey, label }), + }), + me: () => j("/me"), + continuity: () => j("/continuity"), + orgGraph: (org, opts) => { + const qs = new URLSearchParams(); + if (org) + qs.set("org", org); + if (opts?.active) + qs.set("active", "true"); + const q = qs.toString(); + return j(`/org/graph${q ? `?${q}` : ""}`); + }, + routerSessions: (opts) => { + const qs = new URLSearchParams(); + if (opts?.active != null) + qs.set("active", opts.active ? "true" : "false"); + if (opts?.cursor) + qs.set("cursor", opts.cursor); + if (opts?.limit) + qs.set("limit", String(opts.limit)); + if (opts?.agent) + qs.set("agent", opts.agent); + const q = qs.toString(); + return j(`/router/sessions${q ? `?${q}` : ""}`); + }, + contextEntries: (repo, limit = 200) => { + const qs = new URLSearchParams(); + if (repo) + qs.set("repo", repo); + qs.set("limit", String(limit)); + return j(`/context/entries?${qs.toString()}`); + }, + contextPromote: (payload) => j("/context/promote", { method: "POST", body: JSON.stringify(payload) }), + contextDemote: (payload) => j("/context/demote", { method: "POST", body: JSON.stringify(payload) }), + localWorkspaces: () => j("/local/workspaces"), + localWorkspaceCreate: (payload) => j("/local/workspaces", { + method: "POST", + body: JSON.stringify(payload), + }), + localContextLinkFolder: (path) => j("/local-context/folders/link", { method: "POST", body: JSON.stringify({ path }) }), + localContextUnlinkFolder: (path) => j("/local-context/folders/unlink", { + method: "POST", + body: JSON.stringify({ path }), + }), + contextItems: (filters = {}) => { + const qs = new URLSearchParams(); + if (filters.team) + qs.set("team", filters.team); + if (filters.project) + qs.set("project", filters.project); + if (filters.scope) + qs.set("scope", filters.scope); + if (filters.kind) + qs.set("kind", filters.kind); + if (filters.limit) + qs.set("limit", String(filters.limit)); + const q = qs.toString(); + return j(`/context/items${q ? `?${q}` : ""}`); + }, + contextUsage: (filters = {}) => { + const qs = new URLSearchParams(); + if (filters.team) + qs.set("team", filters.team); + if (filters.project) + qs.set("project", filters.project); + if (filters.scope) + qs.set("scope", filters.scope); + if (filters.kind) + qs.set("kind", filters.kind); + if (filters.limit) + qs.set("limit", String(filters.limit)); + const q = qs.toString(); + return j(`/context/usage${q ? `?${q}` : ""}`); + }, + commandCenter: () => j("/ui/command-center"), + proofReplay: (limit = 80) => j(`/ui/proof-replay?limit=${encodeURIComponent(String(limit))}`), + handoffUi: () => j("/ui/handoff"), + learningsUi: (limit = 120) => j(`/ui/learnings?limit=${encodeURIComponent(String(limit))}`), + promoteLearning: (id, payload) => j(`/ui/learnings/${encodeURIComponent(id)}/promote`, { + method: "POST", + body: JSON.stringify(payload || { approved_by: "dhee-ui" }), + }), + rejectLearning: (id, payload) => j(`/ui/learnings/${encodeURIComponent(id)}/reject`, { + method: "POST", + body: JSON.stringify(payload || { reason: "rejected in Dhee UI" }), + }), + portabilityUi: () => j("/ui/portability"), + exportPackUi: (payload) => j("/ui/portability/export", { + method: "POST", + body: JSON.stringify(payload || {}), + }), + importPackDryRunUi: (payload) => j("/ui/portability/import-dry-run", { + method: "POST", + body: JSON.stringify(payload), + }), + upsertContext: (payload) => j("/context", { + method: "POST", + body: JSON.stringify(payload), + }), + proposeContext: (payload) => j("/proposals", { + method: "POST", + body: JSON.stringify(payload), + }), + approveProposal: (contextId, reviewerUserId) => j(`/proposals/${encodeURIComponent(contextId)}/approve`, { + method: "POST", + body: JSON.stringify({ reviewer_user_id: reviewerUserId }), + }), + rejectProposal: (contextId, reviewerUserId) => j(`/proposals/${encodeURIComponent(contextId)}/reject`, { + method: "POST", + body: JSON.stringify({ reviewer_user_id: reviewerUserId }), + }), + inbox: (filter = {}) => { + const qs = new URLSearchParams(); + if (filter.team) + qs.set("team", filter.team); + if (filter.user) + qs.set("user", filter.user); + const q = qs.toString(); + return j(`/inbox${q ? `?${q}` : ""}`); + }, + resolveFinding: (findingId, resolvedBy) => j(`/findings/${encodeURIComponent(findingId)}/resolve`, { + method: "POST", + body: JSON.stringify({ resolved_by: resolvedBy }), + }), + backlinks: (contextId, limit = 50) => j(`/backlinks?context_id=${encodeURIComponent(contextId)}&limit=${encodeURIComponent(String(limit))}`), + setIntegration: (payload) => j("/integrations", { + method: "POST", + body: JSON.stringify(payload), + }), + teamJoin: (payload) => j("/team-join", { + method: "POST", + body: JSON.stringify(payload), + }), + localContextAddFolder: (payload) => j("/local-context/folders", { + method: "POST", + body: JSON.stringify(payload), + }), + localContextShareFolder: (payload) => j("/local-context/folders/share", { + method: "POST", + body: JSON.stringify(payload), + }), + enterpriseSetWorkspace: (payload) => j("/workspace", { + method: "POST", + body: JSON.stringify(payload), + }), + enterpriseResetWorkspace: () => j("/workspace/reset", { + method: "POST", + body: "{}", + }), + enterpriseCreateProject: (payload) => j("/projects", { + method: "POST", + body: JSON.stringify(payload), + }), + enterpriseDeleteProject: (projectId) => j(`/projects/${encodeURIComponent(projectId)}`, { method: "DELETE" }), + enterpriseCreateProjectTeam: (projectId, payload) => j(`/projects/${encodeURIComponent(projectId)}/teams`, { + method: "POST", + body: JSON.stringify(payload), + }), + enterpriseAddProjectFolder: (projectId, payload) => j(`/projects/${encodeURIComponent(projectId)}/folders`, { + method: "POST", + body: JSON.stringify(payload), + }), + enterpriseAddTeamFolder: (teamId, payload) => j(`/teams/${encodeURIComponent(teamId)}/folders`, { + method: "POST", + body: JSON.stringify(payload), + }), + enterpriseRemoveFolder: (mappingId) => j(`/folders/${encodeURIComponent(mappingId)}`, { method: "DELETE" }), + enterpriseAddTeamCollaborator: (teamId, targetTeamId) => j(`/teams/${encodeURIComponent(teamId)}/collaborators`, { + method: "POST", + body: JSON.stringify({ target_team_id: targetTeamId }), + }), + enterpriseExtractProject: (projectId) => j(`/projects/${encodeURIComponent(projectId)}/extract`, { + method: "POST", + body: "{}", + }), + enterpriseExtractTeam: (teamId) => j(`/teams/${encodeURIComponent(teamId)}/extract`, { + method: "POST", + body: "{}", + }), + pickFolderPath: (prompt) => j("/folders/pick", { + method: "POST", + body: JSON.stringify({ prompt }), + }), +}; diff --git a/dhee/ui/web/src/api.ts b/dhee/ui/web/src/api.ts new file mode 100644 index 0000000..2c785d8 --- /dev/null +++ b/dhee/ui/web/src/api.ts @@ -0,0 +1,728 @@ +import type { + CaptureTimelineItem, + ApiKeyProviderStatus, + ConflictSnapshot, + Engram, + EvolutionEvent, + MemoryNowSnapshot, + MetaBuddhiSnapshot, + PolicyRow, + ProjectAsset, + RuntimeStatusCard, + RouterStats, + ProjectIndexSnapshot, + ProjectSummary, + WorkspaceLineMessage, + SessionDetailSnapshot, + AssetContextSummary, + FileContextSummary, + SessionAsset, + TaskDetailSnapshot, + SankhyaTask, + WorkspaceDetailSnapshot, + WorkspaceGraphSnapshot, + Viewer, + OrgGraphSnapshot, + ContextItem, + Proposal, + Finding, + InboxSnapshot, + BacklinksSnapshot, + ContinuitySnapshot, + LocalWorkspace, + RouterSessionsPage, + ContextUsageSnapshot, + ContextEntriesSnapshot, +} from "./types"; + +const BASE = "/api"; + +async function j(path: string, init?: RequestInit): Promise { + const res = await fetch(BASE + path, { + ...init, + headers: { + "Content-Type": "application/json", + ...(init?.headers || {}), + }, + }); + if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); + return (await res.json()) as T; +} + +export const api = { + listMemories: () => + j<{ live: boolean; engrams: Engram[]; count: number; error?: string }>( + "/memories" + ), + remember: (content: string, tier?: string, tags?: string[]) => + j<{ ok: boolean }>("/memories", { + method: "POST", + body: JSON.stringify({ content, tier, tags }), + }), + archiveMemory: (id: string) => + j<{ ok: boolean }>(`/memories/${encodeURIComponent(id)}`, { + method: "DELETE", + }), + routerStats: (agentId?: string) => + j( + `/router/stats${agentId ? `?agent_id=${encodeURIComponent(agentId)}` : ""}` + ), + routerPolicy: () => + j<{ live: boolean; policies: PolicyRow[]; error?: string }>( + "/router/policy" + ), + routerTune: () => + j<{ + ok: boolean; + applied: number; + human: string; + suggestions: { + tool: string; + intent: string; + from: string; + to: string; + reason: string; + }[]; + }>("/router/tune", { method: "POST" }), + metaBuddhi: () => j("/meta-buddhi"), + evolution: () => + j<{ live: boolean; events: EvolutionEvent[] }>("/evolution"), + conflicts: () => j("/conflicts"), + resolveConflict: (id: string, action: string) => + j<{ ok: boolean }>(`/conflicts/${encodeURIComponent(id)}/resolve`, { + method: "POST", + body: JSON.stringify({ action }), + }), + resolveConflictDetailed: ( + id: string, + payload: { action: string; merged_content?: string; reason?: string } + ) => + j<{ ok: boolean; result?: Record }>( + `/conflicts/${encodeURIComponent(id)}/resolve`, + { + method: "POST", + body: JSON.stringify(payload), + } + ), + tasks: () => j<{ live: boolean; tasks: SankhyaTask[] }>("/tasks"), + createTask: (title: string, harness?: string | null) => + j<{ ok: boolean; task: SankhyaTask }>("/tasks", { + method: "POST", + body: JSON.stringify({ title, harness }), + }), + taskDetail: (taskId: string, limit = 24) => + j( + `/tasks/${encodeURIComponent(taskId)}?limit=${encodeURIComponent(String(limit))}` + ), + updateTaskStatus: (taskId: string, status: string) => + j<{ ok: boolean; task: SankhyaTask }>( + `/tasks/${encodeURIComponent(taskId)}/status`, + { + method: "POST", + body: JSON.stringify({ status }), + } + ), + addTaskNote: (taskId: string, content: string) => + j<{ ok: boolean; task: SankhyaTask; result: Record }>( + `/tasks/${encodeURIComponent(taskId)}/notes`, + { + method: "POST", + body: JSON.stringify({ content }), + } + ), + workspaceGraph: (workspaceId?: string, projectId?: string) => + j( + `/workspace/graph${ + workspaceId + ? `?workspace_id=${encodeURIComponent(workspaceId)}${ + projectId ? `&project_id=${encodeURIComponent(projectId)}` : "" + }` + : "" + }` + ), + projects: () => j("/workspaces"), + workspaces: () => j("/workspaces"), + createWorkspaceRoot: (name: string, description?: string) => + j<{ ok: boolean; workspace: WorkspaceDetailSnapshot["workspace"] }>("/workspaces", { + method: "POST", + body: JSON.stringify({ name, description }), + }), + createProject: (workspaceId: string, payload: { name: string; description?: string; default_runtime?: string; color?: string; icon?: string; scope_rules?: { path_prefix: string; label?: string }[] }) => + j<{ ok: boolean; project: ProjectSummary }>( + `/workspaces/${encodeURIComponent(workspaceId)}/projects`, + { + method: "POST", + body: JSON.stringify(payload), + } + ), + updateProject: (projectId: string, payload: { name?: string; description?: string; default_runtime?: string; color?: string; icon?: string; scope_rules?: { path_prefix: string; label?: string }[] }) => + j<{ ok: boolean; project: ProjectSummary }>( + `/projects/${encodeURIComponent(projectId)}`, + { + method: "PATCH", + body: JSON.stringify(payload), + } + ), + projectSessions: (projectId: string) => + j<{ live: boolean; sessions: SessionDetailSnapshot["session"][] }>( + `/projects/${encodeURIComponent(projectId)}/sessions` + ), + projectCanvas: (projectId: string) => + j(`/projects/${encodeURIComponent(projectId)}/canvas`), + workspaceCanvas: (workspaceId: string) => + j(`/workspaces/${encodeURIComponent(workspaceId)}/canvas`), + pickFolder: (prompt?: string) => + j<{ ok: boolean; cancelled?: boolean; path?: string }>("/folders/pick", { + method: "POST", + body: JSON.stringify({ prompt }), + }), + addWorkspaceFolder: (workspaceId: string, path: string, label?: string) => + j<{ ok: boolean; workspace: WorkspaceDetailSnapshot["workspace"] }>( + `/workspaces/${encodeURIComponent(workspaceId)}/folders`, + { + method: "POST", + body: JSON.stringify({ path, label }), + } + ), + removeWorkspaceFolder: (workspaceId: string, path: string) => + j<{ ok: boolean; workspace: WorkspaceDetailSnapshot["workspace"] }>( + `/workspaces/${encodeURIComponent(workspaceId)}/mounts?path=${encodeURIComponent(path)}`, + { method: "DELETE" } + ), + updateWorkspace: ( + workspaceId: string, + payload: { label?: string; description?: string }, + ) => + j<{ ok: boolean; workspace: WorkspaceDetailSnapshot["workspace"] }>( + `/workspaces/${encodeURIComponent(workspaceId)}`, + { + method: "PATCH", + body: JSON.stringify(payload), + } + ), + deleteWorkspace: (workspaceId: string) => + j<{ ok: boolean; id: string }>( + `/workspaces/${encodeURIComponent(workspaceId)}`, + { method: "DELETE" }, + ), + deleteProject: (projectId: string) => + j<{ ok: boolean; id: string; workspace_id?: string }>( + `/projects/${encodeURIComponent(projectId)}`, + { method: "DELETE" }, + ), + workspaceDetail: (workspaceId: string) => + j(`/workspaces/${encodeURIComponent(workspaceId)}`), + workspaceSessions: (workspaceId: string) => + j<{ live: boolean; sessions: WorkspaceDetailSnapshot["sessions"] }>( + `/workspaces/${encodeURIComponent(workspaceId)}/sessions` + ), + sessionDetail: (sessionId: string) => + j(`/sessions/${encodeURIComponent(sessionId)}`), + launchWorkspaceSession: ( + workspaceId: string, + runtime: string, + title?: string, + permission_mode?: string, + task_id?: string, + project_id?: string + ) => + j<{ + ok: boolean; + project_id?: string; + workspace_id?: string; + session_id?: string; + task_id?: string; + runtime: string; + permission_mode?: string; + launch_command: string; + control_state: string; + }>(`/workspaces/${encodeURIComponent(workspaceId)}/sessions/launch`, { + method: "POST", + body: JSON.stringify({ runtime, title, permission_mode, task_id, project_id }), + }), + workspaceLineMessages: ( + workspaceId: string, + opts?: { project_id?: string; channel?: string; cursor?: string; limit?: number } + ) => + j<{ live: boolean; messages: WorkspaceLineMessage[]; nextCursor?: string }>( + `/workspaces/${encodeURIComponent(workspaceId)}/line/messages?${new URLSearchParams( + Object.entries({ + project_id: opts?.project_id, + channel: opts?.channel, + cursor: opts?.cursor, + limit: opts?.limit ? String(opts.limit) : undefined, + }).filter((entry): entry is [string, string] => Boolean(entry[1])) + ).toString()}` + ), + publishWorkspaceLineMessage: ( + workspaceId: string, + payload: { + project_id?: string; + target_project_id?: string; + channel?: string; + session_id?: string; + task_id?: string; + message_kind?: string; + title?: string; + body: string; + metadata?: Record; + } + ) => + j<{ ok: boolean; message: WorkspaceLineMessage; suggestedTask?: SankhyaTask | null }>( + `/workspaces/${encodeURIComponent(workspaceId)}/line/messages`, + { + method: "POST", + body: JSON.stringify(payload), + } + ), + uploadSessionAsset: async (sessionId: string, file: File, label?: string) => { + const form = new FormData(); + form.append("file", file); + if (label) form.append("label", label); + const res = await fetch(`${BASE}/sessions/${encodeURIComponent(sessionId)}/assets`, { + method: "POST", + body: form, + }); + if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); + return (await res.json()) as { ok: boolean; asset: SessionAsset }; + }, + listProjectAssets: (projectId: string) => + j<{ live: boolean; project_id: string; workspace_id: string; assets: ProjectAsset[] }>( + `/projects/${encodeURIComponent(projectId)}/assets`, + ), + listWorkspaceAssets: (workspaceId: string, includeProjectAssets = true) => + j<{ live: boolean; workspace_id: string; assets: ProjectAsset[] }>( + `/workspaces/${encodeURIComponent(workspaceId)}/assets?include_project_assets=${ + includeProjectAssets ? "true" : "false" + }`, + ), + uploadProjectAsset: async (projectId: string, file: File, opts?: { label?: string; folder?: string }) => { + const form = new FormData(); + form.append("file", file); + if (opts?.label) form.append("label", opts.label); + if (opts?.folder) form.append("folder", opts.folder); + const res = await fetch(`${BASE}/projects/${encodeURIComponent(projectId)}/assets`, { + method: "POST", + body: form, + }); + if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); + return (await res.json()) as { ok: boolean; asset: ProjectAsset }; + }, + uploadWorkspaceAsset: async ( + workspaceId: string, + file: File, + opts?: { label?: string; folder?: string; project_id?: string }, + ) => { + const form = new FormData(); + form.append("file", file); + if (opts?.label) form.append("label", opts.label); + if (opts?.folder) form.append("folder", opts.folder); + if (opts?.project_id) form.append("project_id", opts.project_id); + const res = await fetch(`${BASE}/workspaces/${encodeURIComponent(workspaceId)}/assets`, { + method: "POST", + body: form, + }); + if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); + return (await res.json()) as { ok: boolean; asset: ProjectAsset }; + }, + deleteProjectAsset: (assetId: string) => + j<{ ok: boolean }>(`/project-assets/${encodeURIComponent(assetId)}`, { method: "DELETE" }), + fileContext: (path: string, workspaceId?: string) => + j<{ live: boolean } & FileContextSummary>( + `/files/${encodeURIComponent(path)}/context${workspaceId ? `?workspace_id=${encodeURIComponent(workspaceId)}` : ""}` + ), + assetContext: (assetId: string) => + j<{ live: boolean } & AssetContextSummary>(`/assets/${encodeURIComponent(assetId)}/context`), + askAsset: (assetId: string, question: string) => + j<{ + ok: boolean; + launch: { + task_id?: string; + session_id?: string; + launch_command?: string; + }; + question: string; + }>(`/assets/${encodeURIComponent(assetId)}/ask`, { + method: "POST", + body: JSON.stringify({ question }), + }), + runtimeStatus: () => + j<{ live: boolean; repo: string; runtimes: RuntimeStatusCard[]; error?: string }>( + "/runtime-status" + ), + status: () => + j<{ + ok: boolean; + router?: { sessions: number; calls: number; tokensSaved: number }; + dhee_data_dir?: string; + error?: string; + }>("/status"), + memoryNow: () => j("/memory/now"), + captureTimeline: (limit = 16) => + j<{ items: CaptureTimelineItem[] }>( + `/capture/timeline?limit=${encodeURIComponent(String(limit))}` + ), + launch: (runtime: string, taskId?: string, title?: string) => + j<{ ok: boolean; command: string; message: string; task?: SankhyaTask; taskId?: string }>( + "/launch", + { + method: "POST", + body: JSON.stringify({ runtime, taskId, title }), + } + ), + apiKeys: () => + j<{ live: boolean; providers: ApiKeyProviderStatus[]; error?: string }>( + "/security/api-keys" + ), + storeApiKey: (provider: string, apiKey: string, label?: string) => + j<{ ok: boolean; provider: ApiKeyProviderStatus }>("/security/api-keys", { + method: "POST", + body: JSON.stringify({ provider, apiKey, label }), + }), + rotateApiKey: (provider: string, apiKey: string, label?: string) => + j<{ ok: boolean; provider: ApiKeyProviderStatus }>( + `/security/api-keys/${encodeURIComponent(provider)}/rotate`, + { + method: "POST", + body: JSON.stringify({ apiKey, label }), + } + ), + me: () => j("/me"), + continuity: () => j("/continuity"), + orgGraph: (org?: string, opts?: { active?: boolean }) => { + const qs = new URLSearchParams(); + if (org) qs.set("org", org); + if (opts?.active) qs.set("active", "true"); + const q = qs.toString(); + return j(`/org/graph${q ? `?${q}` : ""}`); + }, + routerSessions: (opts?: { + active?: boolean; + cursor?: string; + limit?: number; + agent?: string; + }) => { + const qs = new URLSearchParams(); + if (opts?.active != null) qs.set("active", opts.active ? "true" : "false"); + if (opts?.cursor) qs.set("cursor", opts.cursor); + if (opts?.limit) qs.set("limit", String(opts.limit)); + if (opts?.agent) qs.set("agent", opts.agent); + const q = qs.toString(); + return j(`/router/sessions${q ? `?${q}` : ""}`); + }, + contextEntries: (repo?: string, limit = 200) => { + const qs = new URLSearchParams(); + if (repo) qs.set("repo", repo); + qs.set("limit", String(limit)); + return j(`/context/entries?${qs.toString()}`); + }, + contextPromote: (payload: { + memory_id: string; + repo?: string; + kind?: string; + title?: string; + }) => + j<{ ok: boolean; entry: Record; repo_root: string }>( + "/context/promote", + { method: "POST", body: JSON.stringify(payload) }, + ), + contextDemote: (payload: { entry_id: string; repo?: string }) => + j<{ ok: boolean; memory_id: string; entry: Record }>( + "/context/demote", + { method: "POST", body: JSON.stringify(payload) }, + ), + localWorkspaces: () => + j<{ workspaces: LocalWorkspace[]; max_workspaces: number }>( + "/local/workspaces", + ), + localWorkspaceCreate: (payload: { id?: string; name?: string }) => + j<{ ok: boolean; workspace: LocalWorkspace }>("/local/workspaces", { + method: "POST", + body: JSON.stringify(payload), + }), + localContextLinkFolder: (path: string) => + j<{ ok: boolean; folder: Record; link: { linked: boolean } }>( + "/local-context/folders/link", + { method: "POST", body: JSON.stringify({ path }) }, + ), + localContextUnlinkFolder: (path: string) => + j<{ ok: boolean }>("/local-context/folders/unlink", { + method: "POST", + body: JSON.stringify({ path }), + }), + contextItems: (filters: { team?: string; project?: string; scope?: string; kind?: string; limit?: number } = {}) => { + const qs = new URLSearchParams(); + if (filters.team) qs.set("team", filters.team); + if (filters.project) qs.set("project", filters.project); + if (filters.scope) qs.set("scope", filters.scope); + if (filters.kind) qs.set("kind", filters.kind); + if (filters.limit) qs.set("limit", String(filters.limit)); + const q = qs.toString(); + return j<{ live: boolean; items: ContextItem[]; error?: string }>( + `/context/items${q ? `?${q}` : ""}` + ); + }, + contextUsage: (filters: { team?: string; project?: string; scope?: string; kind?: string; limit?: number } = {}) => { + const qs = new URLSearchParams(); + if (filters.team) qs.set("team", filters.team); + if (filters.project) qs.set("project", filters.project); + if (filters.scope) qs.set("scope", filters.scope); + if (filters.kind) qs.set("kind", filters.kind); + if (filters.limit) qs.set("limit", String(filters.limit)); + const q = qs.toString(); + return j(`/context/usage${q ? `?${q}` : ""}`); + }, + commandCenter: () => j>("/ui/command-center"), + proofReplay: (limit = 80) => + j>(`/ui/proof-replay?limit=${encodeURIComponent(String(limit))}`), + handoffUi: () => j>("/ui/handoff"), + learningsUi: (limit = 120) => + j>(`/ui/learnings?limit=${encodeURIComponent(String(limit))}`), + promoteLearning: (id: string, payload?: { scope?: string; repo?: string; approved_by?: string }) => + j>(`/ui/learnings/${encodeURIComponent(id)}/promote`, { + method: "POST", + body: JSON.stringify(payload || { approved_by: "dhee-ui" }), + }), + rejectLearning: (id: string, payload?: { reason?: string }) => + j>(`/ui/learnings/${encodeURIComponent(id)}/reject`, { + method: "POST", + body: JSON.stringify(payload || { reason: "rejected in Dhee UI" }), + }), + portabilityUi: () => j>("/ui/portability"), + exportPackUi: (payload?: { output_path?: string; user_id?: string; repo?: string }) => + j>("/ui/portability/export", { + method: "POST", + body: JSON.stringify(payload || {}), + }), + importPackDryRunUi: (payload: { input_path: string; user_id?: string; repo?: string }) => + j>("/ui/portability/import-dry-run", { + method: "POST", + body: JSON.stringify(payload), + }), + upsertContext: (payload: { + context_id?: string; + title: string; + content: string; + scope: string; + kind?: string; + project_id?: string; + team_id?: string; + user_id?: string; + tags?: string[]; + summary?: string; + metadata?: Record; + }) => + j<{ ok: boolean; item: ContextItem }>("/context", { + method: "POST", + body: JSON.stringify(payload), + }), + proposeContext: (payload: { + title: string; + content: string; + scope: string; + kind: string; + proposed_by_user_id: string; + project_id?: string; + team_id?: string; + supersedes_id?: string; + tags?: string[]; + metadata?: Record; + }) => + j<{ ok: boolean; proposal: Proposal }>("/proposals", { + method: "POST", + body: JSON.stringify(payload), + }), + approveProposal: (contextId: string, reviewerUserId: string) => + j<{ ok: boolean; proposal: Proposal }>( + `/proposals/${encodeURIComponent(contextId)}/approve`, + { + method: "POST", + body: JSON.stringify({ reviewer_user_id: reviewerUserId }), + } + ), + rejectProposal: (contextId: string, reviewerUserId: string) => + j<{ ok: boolean; proposal: Proposal }>( + `/proposals/${encodeURIComponent(contextId)}/reject`, + { + method: "POST", + body: JSON.stringify({ reviewer_user_id: reviewerUserId }), + } + ), + inbox: (filter: { team?: string; user?: string } = {}) => { + const qs = new URLSearchParams(); + if (filter.team) qs.set("team", filter.team); + if (filter.user) qs.set("user", filter.user); + const q = qs.toString(); + return j(`/inbox${q ? `?${q}` : ""}`); + }, + resolveFinding: (findingId: string, resolvedBy?: string) => + j<{ ok: boolean; finding: Finding }>( + `/findings/${encodeURIComponent(findingId)}/resolve`, + { + method: "POST", + body: JSON.stringify({ resolved_by: resolvedBy }), + } + ), + backlinks: (contextId: string, limit = 50) => + j( + `/backlinks?context_id=${encodeURIComponent(contextId)}&limit=${encodeURIComponent(String(limit))}` + ), + setIntegration: (payload: { + scope: string; + target_id: string; + type: string; + value: unknown; + metadata?: Record; + }) => + j<{ ok: boolean; node: Record }>("/integrations", { + method: "POST", + body: JSON.stringify(payload), + }), + teamJoin: (payload: { + org_id: string; + project_id?: string; + team_id?: string; + role?: string; + repo_root?: string; + }) => + j<{ ok: boolean; joined: Record }>("/team-join", { + method: "POST", + body: JSON.stringify(payload), + }), + localContextAddFolder: (payload: { path: string; shared?: boolean }) => + j<{ ok: boolean; folder: Record }>("/local-context/folders", { + method: "POST", + body: JSON.stringify(payload), + }), + localContextShareFolder: (payload: { path: string; shared?: boolean }) => + j<{ ok: boolean; folder: Record }>("/local-context/folders/share", { + method: "POST", + body: JSON.stringify(payload), + }), + enterpriseSetWorkspace: (payload: { + name: string; + root_path?: string; + default_branch?: string; + }) => + j<{ ok: boolean; workspace: Record }>("/workspace", { + method: "POST", + body: JSON.stringify(payload), + }), + enterpriseResetWorkspace: () => + j<{ ok: boolean; deleted: Record }>("/workspace/reset", { + method: "POST", + body: "{}", + }), + enterpriseCreateProject: (payload: { + name: string; + project_id?: string; + description?: string; + }) => + j<{ ok: boolean; project: Record }>("/projects", { + method: "POST", + body: JSON.stringify(payload), + }), + enterpriseDeleteProject: (projectId: string) => + j<{ ok: boolean; project_id: string }>( + `/projects/${encodeURIComponent(projectId)}`, + { method: "DELETE" } + ), + enterpriseCreateProjectTeam: ( + projectId: string, + payload: { + name: string; + team_id?: string; + description?: string; + } + ) => + j<{ ok: boolean; team: Record }>( + `/projects/${encodeURIComponent(projectId)}/teams`, + { + method: "POST", + body: JSON.stringify(payload), + } + ), + enterpriseAddProjectFolder: ( + projectId: string, + payload: { + local_path?: string; + repo_url?: string; + label?: string; + kind?: string; + } + ) => + j<{ ok: boolean; mapping: Record }>( + `/projects/${encodeURIComponent(projectId)}/folders`, + { + method: "POST", + body: JSON.stringify(payload), + } + ), + enterpriseAddTeamFolder: ( + teamId: string, + payload: { + local_path?: string; + repo_url?: string; + label?: string; + kind?: string; + } + ) => + j<{ ok: boolean; mapping: Record }>( + `/teams/${encodeURIComponent(teamId)}/folders`, + { + method: "POST", + body: JSON.stringify(payload), + } + ), + enterpriseRemoveFolder: (mappingId: string) => + j<{ ok: boolean; mapping_id: string }>( + `/folders/${encodeURIComponent(mappingId)}`, + { method: "DELETE" } + ), + enterpriseAddTeamCollaborator: (teamId: string, targetTeamId: string) => + j<{ + ok: boolean; + team: Record; + target_team: Record; + collaborating_team_ids: string[]; + }>(`/teams/${encodeURIComponent(teamId)}/collaborators`, { + method: "POST", + body: JSON.stringify({ target_team_id: targetTeamId }), + }), + enterpriseExtractProject: (projectId: string) => + j<{ + ok: boolean; + project_id: string; + folders_seen: number; + files_seen: number; + files_extracted: number; + files_cached: number; + nodes_upserted: number; + edges_upserted: number; + errors: { path: string; error: string }[]; + }>(`/projects/${encodeURIComponent(projectId)}/extract`, { + method: "POST", + body: "{}", + }), + enterpriseExtractTeam: (teamId: string) => + j<{ + ok: boolean; + project_id: string; + team_id?: string | null; + folders_seen: number; + files_seen: number; + files_extracted: number; + files_cached: number; + nodes_upserted: number; + edges_upserted: number; + errors: { path: string; error: string }[]; + }>(`/teams/${encodeURIComponent(teamId)}/extract`, { + method: "POST", + body: "{}", + }), + pickFolderPath: (prompt?: string) => + j<{ ok: boolean; cancelled?: boolean; path?: string }>("/folders/pick", { + method: "POST", + body: JSON.stringify({ prompt }), + }), +}; diff --git a/dhee/ui/web/src/components/AssetDrawer.js b/dhee/ui/web/src/components/AssetDrawer.js new file mode 100644 index 0000000..61a3d8f --- /dev/null +++ b/dhee/ui/web/src/components/AssetDrawer.js @@ -0,0 +1,358 @@ +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { api } from "../api"; +import { StatPill } from "./ui/StatPill"; +const MS_MINUTE = 60000; +const MS_HOUR = 3600000; +const MS_DAY = 86400000; +function fmtRelative(value) { + if (!value) + return ""; + const when = Date.parse(value); + if (Number.isNaN(when)) + return String(value); + const delta = Date.now() - when; + if (delta < MS_MINUTE) + return "just now"; + if (delta < MS_HOUR) + return `${Math.round(delta / MS_MINUTE)}m ago`; + if (delta < MS_DAY) + return `${Math.round(delta / MS_HOUR)}h ago`; + return `${Math.round(delta / MS_DAY)}d ago`; +} +function fmtSize(bytes) { + if (!bytes || bytes <= 0) + return "—"; + if (bytes < 1024) + return `${bytes} B`; + if (bytes < 1024 * 1024) + return `${(bytes / 1024).toFixed(1)} KB`; + if (bytes < 1024 * 1024 * 1024) + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; + return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`; +} +function extensionLabel(asset) { + const name = asset.name || ""; + const dot = name.lastIndexOf("."); + if (dot <= 0 || dot === name.length - 1) + return "file"; + return name.slice(dot + 1).toLowerCase().slice(0, 5); +} +function runtimeTone(runtime) { + const key = runtime.toLowerCase(); + if (key.includes("claude")) + return "#e06b3f"; + if (key.includes("codex")) + return "#1a1a1a"; + if (key.includes("cursor")) + return "#4d6cff"; + if (key.includes("browser")) + return "#1fa971"; + return "var(--ink3)"; +} +function ExtensionBadge({ asset }) { + const ext = extensionLabel(asset); + return (_jsx("div", { style: { + width: 38, + height: 44, + flexShrink: 0, + borderRadius: 4, + background: "rgba(20,16,10,0.04)", + border: "1px solid rgba(20,16,10,0.12)", + display: "flex", + alignItems: "center", + justifyContent: "center", + fontFamily: "var(--mono)", + fontSize: 9, + fontWeight: 600, + color: "var(--ink2)", + letterSpacing: 0.4, + textTransform: "uppercase", + }, children: ext })); +} +function AssetResultRow({ result }) { + const runtime = String(result.harness || "dhee"); + const tool = String(result.tool_name || ""); + const kind = String(result.packet_kind || "").replace(/^routed_/, ""); + return (_jsxs("div", { style: { + display: "flex", + alignItems: "center", + gap: 8, + fontFamily: "var(--mono)", + fontSize: 10, + color: "var(--ink2)", + padding: "4px 0", + }, children: [_jsx("span", { style: { + width: 5, + height: 5, + borderRadius: "50%", + background: runtimeTone(runtime), + flexShrink: 0, + } }), _jsx("span", { style: { color: runtimeTone(runtime), minWidth: 60 }, children: runtime }), _jsx("span", { style: { color: "var(--ink3)" }, children: tool.toLowerCase() || kind }), _jsx("span", { style: { marginLeft: "auto", color: "var(--ink3)" }, children: fmtRelative(result.updated_at || result.created_at) })] })); +} +function AssetCard({ asset, onDelete, busyDelete, }) { + const [showResults, setShowResults] = useState(false); + const results = asset.results || []; + const processors = new Set(); + for (const r of results) + processors.add(String(r.harness || "dhee")); + return (_jsxs("div", { style: { + border: "1px solid var(--border)", + background: "white", + padding: 12, + borderRadius: 6, + display: "flex", + flexDirection: "column", + gap: 8, + }, children: [_jsxs("div", { style: { display: "flex", gap: 10, alignItems: "flex-start" }, children: [_jsx(ExtensionBadge, { asset: asset }), _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [_jsx("div", { style: { + fontSize: 13, + fontWeight: 600, + lineHeight: 1.3, + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis", + }, title: asset.name, children: asset.name }), _jsxs("div", { style: { + fontFamily: "var(--mono)", + fontSize: 9, + color: "var(--ink3)", + letterSpacing: 0.3, + marginTop: 4, + display: "flex", + gap: 8, + }, children: [_jsx("span", { children: fmtSize(asset.size_bytes) }), asset.updated_at ? _jsxs("span", { children: ["uploaded ", fmtRelative(asset.updated_at)] }) : null] })] }), _jsx("button", { onClick: () => void onDelete(asset), disabled: busyDelete, title: "Remove asset", "aria-label": "Remove asset", style: { + width: 22, + height: 22, + display: "flex", + alignItems: "center", + justifyContent: "center", + border: "1px solid transparent", + borderRadius: 3, + background: "transparent", + color: "var(--ink3)", + cursor: busyDelete ? "not-allowed" : "pointer", + opacity: busyDelete ? 0.5 : 1, + padding: 0, + flexShrink: 0, + }, onMouseEnter: (e) => { + if (!busyDelete) { + e.currentTarget.style.background = "rgba(203,63,78,0.08)"; + e.currentTarget.style.color = "var(--rose)"; + } + }, onMouseLeave: (e) => { + e.currentTarget.style.background = "transparent"; + e.currentTarget.style.color = "var(--ink3)"; + }, children: _jsx("svg", { width: 12, height: 12, viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M6 6l12 12M18 6L6 18", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round" }) }) })] }), _jsxs("div", { style: { display: "flex", gap: 6, flexWrap: "wrap", alignItems: "center" }, children: [processors.size > 0 ? (_jsx(StatPill, { label: `${results.length} processed`, tone: "var(--green)" })) : (_jsx(StatPill, { label: "not yet processed" })), Array.from(processors) + .slice(0, 3) + .map((runtime) => (_jsx(StatPill, { label: runtime, tone: runtimeTone(runtime) }, runtime)))] }), results.length > 0 && (_jsxs(_Fragment, { children: [_jsx("button", { onClick: () => setShowResults((v) => !v), style: { + fontFamily: "var(--mono)", + fontSize: 9, + color: "var(--ink3)", + background: "transparent", + border: 0, + padding: 0, + textAlign: "left", + cursor: "pointer", + letterSpacing: 0.4, + }, children: showResults ? "▾ hide processing feed" : `▸ show processing feed (${results.length})` }), showResults && (_jsx("div", { style: { + display: "flex", + flexDirection: "column", + borderTop: "1px dashed var(--border)", + paddingTop: 6, + }, children: results.slice(0, 8).map((result) => (_jsx(AssetResultRow, { result: result }, result.id))) }))] }))] })); +} +export function AssetDrawer({ workspace, project, onActivity, }) { + const [assets, setAssets] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [status, setStatus] = useState("idle"); + const [statusMessage, setStatusMessage] = useState(""); + const [dragHover, setDragHover] = useState(false); + const [busyDeleteId, setBusyDeleteId] = useState(null); + const fileInputRef = useRef(null); + const scopeLabel = useMemo(() => { + if (project) + return project.name; + if (workspace) + return `${workspace.label || workspace.name} (workspace)`; + return "—"; + }, [project, workspace]); + const refresh = useCallback(async () => { + if (!workspace) { + setAssets([]); + return; + } + setLoading(true); + setError(null); + try { + const res = project + ? await api.listProjectAssets(project.id) + : await api.listWorkspaceAssets(workspace.id, false); + setAssets(res.assets || []); + } + catch (e) { + setError(String(e)); + } + finally { + setLoading(false); + } + }, [project, workspace]); + useEffect(() => { + void refresh(); + }, [refresh]); + // Light polling so the processing feed stays fresh without a full SSE + // implementation (PR 4). + useEffect(() => { + if (!workspace) + return; + const timer = window.setInterval(() => void refresh(), 5000); + return () => window.clearInterval(timer); + }, [refresh, workspace]); + const uploadFiles = useCallback(async (files) => { + if (!workspace) + return; + const list = Array.from(files); + if (list.length === 0) + return; + setStatus("uploading"); + setStatusMessage(`uploading ${list.length} file${list.length === 1 ? "" : "s"}…`); + setError(null); + try { + for (const file of list) { + if (project) { + await api.uploadProjectAsset(project.id, file); + } + else { + await api.uploadWorkspaceAsset(workspace.id, file); + } + } + setStatus("success"); + setStatusMessage(list.length === 1 + ? `uploaded ${list[0].name}` + : `uploaded ${list.length} files`); + await refresh(); + onActivity?.(); + } + catch (e) { + setStatus("error"); + setStatusMessage(String(e)); + } + finally { + window.setTimeout(() => { + setStatus("idle"); + setStatusMessage(""); + }, 2200); + } + }, [project, workspace, refresh, onActivity]); + const onDrop = (e) => { + e.preventDefault(); + e.stopPropagation(); + setDragHover(false); + const dt = e.dataTransfer; + if (dt?.files?.length) { + void uploadFiles(dt.files); + } + }; + const onDragOver = (e) => { + if (!workspace) + return; + e.preventDefault(); + e.stopPropagation(); + if (e.dataTransfer.types?.includes("Files") && !dragHover) + setDragHover(true); + }; + const onDragLeave = (e) => { + if (e.currentTarget === e.target) + setDragHover(false); + }; + const onFilePick = (e) => { + const files = e.target.files; + if (files?.length) + void uploadFiles(files); + e.target.value = ""; + }; + const deleteAsset = async (asset) => { + if (!window.confirm(`Remove "${asset.name}"?`)) + return; + setBusyDeleteId(asset.id); + try { + await api.deleteProjectAsset(asset.id); + setAssets((current) => current.filter((a) => a.id !== asset.id)); + onActivity?.(); + } + catch (e) { + setError(String(e)); + } + finally { + setBusyDeleteId(null); + } + }; + return (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 10 }, onDrop: onDrop, onDragOver: onDragOver, onDragLeave: onDragLeave, children: [_jsxs("div", { style: { + display: "flex", + alignItems: "center", + justifyContent: "space-between", + fontFamily: "var(--mono)", + fontSize: 9, + letterSpacing: 0.5, + color: "var(--ink3)", + textTransform: "uppercase", + }, children: [_jsxs("span", { children: ["Assets \u00B7 ", scopeLabel] }), _jsx("span", { children: assets.length })] }), _jsxs("div", { onClick: () => workspace && fileInputRef.current?.click(), style: { + padding: "16px 14px", + border: `1px dashed ${dragHover ? "var(--accent)" : "var(--border)"}`, + background: dragHover ? "rgba(224,107,63,0.06)" : "white", + borderRadius: 6, + textAlign: "center", + cursor: workspace ? "pointer" : "not-allowed", + opacity: workspace ? 1 : 0.55, + transition: "background 0.18s ease, border-color 0.18s ease", + }, children: [_jsx("input", { ref: fileInputRef, type: "file", multiple: true, style: { display: "none" }, onChange: onFilePick }), _jsx("div", { style: { + fontFamily: "var(--mono)", + fontSize: 11, + color: dragHover ? "var(--accent)" : "var(--ink2)", + fontWeight: 500, + marginBottom: 4, + }, children: status === "uploading" + ? statusMessage + : dragHover + ? "release to upload" + : "drop files here or click to upload" }), _jsx("div", { style: { + fontFamily: "var(--mono)", + fontSize: 9, + color: "var(--ink3)", + letterSpacing: 0.4, + }, children: project + ? "visible to every agent working on this project" + : workspace + ? "workspace-wide — every project sees it" + : "select a workspace first" })] }), status === "success" && (_jsx("div", { style: { + fontFamily: "var(--mono)", + fontSize: 10, + color: "var(--green)", + lineHeight: 1.4, + }, children: statusMessage })), status === "error" && (_jsx("div", { style: { + fontFamily: "var(--mono)", + fontSize: 10, + color: "var(--rose)", + lineHeight: 1.4, + }, children: statusMessage })), error && status !== "error" ? (_jsx("div", { style: { + fontFamily: "var(--mono)", + fontSize: 10, + color: "var(--rose)", + lineHeight: 1.4, + }, children: error })) : null, loading && assets.length === 0 ? (_jsx("div", { style: { display: "grid", gap: 8 }, children: [0, 1, 2].map((i) => (_jsx("div", { style: { + height: 66, + borderRadius: 6, + background: "linear-gradient(90deg, rgba(20,16,10,0.04) 0%, rgba(20,16,10,0.08) 50%, rgba(20,16,10,0.04) 100%)", + backgroundSize: "200% 100%", + animation: `dhee-shimmer 1.4s linear ${i * 140}ms infinite`, + border: "1px solid rgba(20,16,10,0.06)", + } }, i))) })) : assets.length === 0 ? (_jsxs("div", { style: { + padding: 14, + border: "1px dashed var(--border)", + borderRadius: 6, + fontFamily: "var(--mono)", + fontSize: 10, + color: "var(--ink3)", + lineHeight: 1.55, + background: "white", + }, children: ["No assets yet. Drop a spec PDF, design export, or schema doc here \u2014 every agent in this", project ? " project" : " workspace", " will see it."] })) : (_jsx("div", { style: { display: "grid", gap: 8 }, children: assets.map((asset) => (_jsx(AssetCard, { asset: asset, onDelete: deleteAsset, busyDelete: busyDeleteId === asset.id }, asset.id))) }))] })); +} diff --git a/dhee/ui/web/src/components/AssetDrawer.tsx b/dhee/ui/web/src/components/AssetDrawer.tsx new file mode 100644 index 0000000..129d083 --- /dev/null +++ b/dhee/ui/web/src/components/AssetDrawer.tsx @@ -0,0 +1,559 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { api } from "../api"; +import type { + ProjectAsset, + ProjectSummary, + SharedTaskResult, + WorkspaceSummary, +} from "../types"; +import { StatPill } from "./ui/StatPill"; + +// --------------------------------------------------------------------------- +// AssetDrawer — project/workspace-scoped asset list + drag-drop uploader. +// +// The killer feature is the per-asset "processed by …" feed underneath +// each card: whenever a connected agent reads / greps / edits the file, +// the shared-task result shows up here, attributed with runtime + time. +// That's the pitch deck's "one agent read the 40-page contract, every +// other agent benefits" made tangible. +// --------------------------------------------------------------------------- + +type UploadStatus = "idle" | "uploading" | "success" | "error"; + +const MS_MINUTE = 60_000; +const MS_HOUR = 3_600_000; +const MS_DAY = 86_400_000; + +function fmtRelative(value?: string | null): string { + if (!value) return ""; + const when = Date.parse(value); + if (Number.isNaN(when)) return String(value); + const delta = Date.now() - when; + if (delta < MS_MINUTE) return "just now"; + if (delta < MS_HOUR) return `${Math.round(delta / MS_MINUTE)}m ago`; + if (delta < MS_DAY) return `${Math.round(delta / MS_HOUR)}h ago`; + return `${Math.round(delta / MS_DAY)}d ago`; +} + +function fmtSize(bytes?: number | null): string { + if (!bytes || bytes <= 0) return "—"; + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; + return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`; +} + +function extensionLabel(asset: ProjectAsset): string { + const name = asset.name || ""; + const dot = name.lastIndexOf("."); + if (dot <= 0 || dot === name.length - 1) return "file"; + return name.slice(dot + 1).toLowerCase().slice(0, 5); +} + +function runtimeTone(runtime: string): string { + const key = runtime.toLowerCase(); + if (key.includes("claude")) return "#e06b3f"; + if (key.includes("codex")) return "#1a1a1a"; + if (key.includes("cursor")) return "#4d6cff"; + if (key.includes("browser")) return "#1fa971"; + return "var(--ink3)"; +} + +function ExtensionBadge({ asset }: { asset: ProjectAsset }) { + const ext = extensionLabel(asset); + return ( +
+ {ext} +
+ ); +} + +function AssetResultRow({ result }: { result: SharedTaskResult }) { + const runtime = String(result.harness || "dhee"); + const tool = String(result.tool_name || ""); + const kind = String(result.packet_kind || "").replace(/^routed_/, ""); + return ( +
+ + {runtime} + {tool.toLowerCase() || kind} + + {fmtRelative(result.updated_at || result.created_at)} + +
+ ); +} + +function AssetCard({ + asset, + onDelete, + busyDelete, +}: { + asset: ProjectAsset; + onDelete: (asset: ProjectAsset) => Promise | void; + busyDelete: boolean; +}) { + const [showResults, setShowResults] = useState(false); + const results = asset.results || []; + const processors = new Set(); + for (const r of results) processors.add(String(r.harness || "dhee")); + return ( +
+
+ +
+
+ {asset.name} +
+
+ {fmtSize(asset.size_bytes)} + {asset.updated_at ? uploaded {fmtRelative(asset.updated_at)} : null} +
+
+ +
+ +
+ {processors.size > 0 ? ( + + ) : ( + + )} + {Array.from(processors) + .slice(0, 3) + .map((runtime) => ( + + ))} +
+ + {results.length > 0 && ( + <> + + {showResults && ( +
+ {results.slice(0, 8).map((result) => ( + + ))} +
+ )} + + )} +
+ ); +} + +export function AssetDrawer({ + workspace, + project, + onActivity, +}: { + workspace: WorkspaceSummary | null; + project: ProjectSummary | null; + onActivity?: () => void; +}) { + const [assets, setAssets] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [status, setStatus] = useState("idle"); + const [statusMessage, setStatusMessage] = useState(""); + const [dragHover, setDragHover] = useState(false); + const [busyDeleteId, setBusyDeleteId] = useState(null); + const fileInputRef = useRef(null); + + const scopeLabel = useMemo(() => { + if (project) return project.name; + if (workspace) return `${workspace.label || workspace.name} (workspace)`; + return "—"; + }, [project, workspace]); + + const refresh = useCallback(async () => { + if (!workspace) { + setAssets([]); + return; + } + setLoading(true); + setError(null); + try { + const res = project + ? await api.listProjectAssets(project.id) + : await api.listWorkspaceAssets(workspace.id, false); + setAssets(res.assets || []); + } catch (e) { + setError(String(e)); + } finally { + setLoading(false); + } + }, [project, workspace]); + + useEffect(() => { + void refresh(); + }, [refresh]); + + // Light polling so the processing feed stays fresh without a full SSE + // implementation (PR 4). + useEffect(() => { + if (!workspace) return; + const timer = window.setInterval(() => void refresh(), 5000); + return () => window.clearInterval(timer); + }, [refresh, workspace]); + + const uploadFiles = useCallback( + async (files: FileList | File[]) => { + if (!workspace) return; + const list = Array.from(files); + if (list.length === 0) return; + setStatus("uploading"); + setStatusMessage(`uploading ${list.length} file${list.length === 1 ? "" : "s"}…`); + setError(null); + try { + for (const file of list) { + if (project) { + await api.uploadProjectAsset(project.id, file); + } else { + await api.uploadWorkspaceAsset(workspace.id, file); + } + } + setStatus("success"); + setStatusMessage( + list.length === 1 + ? `uploaded ${list[0].name}` + : `uploaded ${list.length} files`, + ); + await refresh(); + onActivity?.(); + } catch (e) { + setStatus("error"); + setStatusMessage(String(e)); + } finally { + window.setTimeout(() => { + setStatus("idle"); + setStatusMessage(""); + }, 2200); + } + }, + [project, workspace, refresh, onActivity], + ); + + const onDrop = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setDragHover(false); + const dt = e.dataTransfer; + if (dt?.files?.length) { + void uploadFiles(dt.files); + } + }; + + const onDragOver = (e: React.DragEvent) => { + if (!workspace) return; + e.preventDefault(); + e.stopPropagation(); + if (e.dataTransfer.types?.includes("Files") && !dragHover) setDragHover(true); + }; + const onDragLeave = (e: React.DragEvent) => { + if (e.currentTarget === e.target) setDragHover(false); + }; + + const onFilePick = (e: React.ChangeEvent) => { + const files = e.target.files; + if (files?.length) void uploadFiles(files); + e.target.value = ""; + }; + + const deleteAsset = async (asset: ProjectAsset) => { + if (!window.confirm(`Remove "${asset.name}"?`)) return; + setBusyDeleteId(asset.id); + try { + await api.deleteProjectAsset(asset.id); + setAssets((current) => current.filter((a) => a.id !== asset.id)); + onActivity?.(); + } catch (e) { + setError(String(e)); + } finally { + setBusyDeleteId(null); + } + }; + + return ( +
+ {/* Scope indicator */} +
+ Assets · {scopeLabel} + {assets.length} +
+ + {/* Dropzone / picker */} +
workspace && fileInputRef.current?.click()} + style={{ + padding: "16px 14px", + border: `1px dashed ${dragHover ? "var(--accent)" : "var(--border)"}`, + background: dragHover ? "rgba(224,107,63,0.06)" : "white", + borderRadius: 6, + textAlign: "center", + cursor: workspace ? "pointer" : "not-allowed", + opacity: workspace ? 1 : 0.55, + transition: "background 0.18s ease, border-color 0.18s ease", + }} + > + +
+ {status === "uploading" + ? statusMessage + : dragHover + ? "release to upload" + : "drop files here or click to upload"} +
+
+ {project + ? "visible to every agent working on this project" + : workspace + ? "workspace-wide — every project sees it" + : "select a workspace first"} +
+
+ + {status === "success" && ( +
+ {statusMessage} +
+ )} + {status === "error" && ( +
+ {statusMessage} +
+ )} + {error && status !== "error" ? ( +
+ {error} +
+ ) : null} + + {loading && assets.length === 0 ? ( +
+ {[0, 1, 2].map((i) => ( +
+ ))} +
+ ) : assets.length === 0 ? ( +
+ No assets yet. Drop a spec PDF, design export, or schema doc here — every agent in this + {project ? " project" : " workspace"} will see it. +
+ ) : ( +
+ {assets.map((asset) => ( + + ))} +
+ )} +
+ ); +} diff --git a/dhee/ui/web/src/components/ChatMessage.js b/dhee/ui/web/src/components/ChatMessage.js new file mode 100644 index 0000000..cd724e2 --- /dev/null +++ b/dhee/ui/web/src/components/ChatMessage.js @@ -0,0 +1,39 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { BrowserCard, CodeCard, DocumentCard, GrepCard, LinkCard, } from "./cards/Cards"; +export function ChatMessage({ msg, tasks, onSelectTask, }) { + if (msg.role === "component") { + const anyMsg = msg; + return (_jsxs("div", { style: { marginBottom: 14, paddingLeft: 14 }, children: [anyMsg.type === "browser" && (_jsx(BrowserCard, { url: anyMsg.url, title: anyMsg.title, lines: anyMsg.lines })), anyMsg.type === "grep" && (_jsx(GrepCard, { query: anyMsg.query, files: anyMsg.files })), anyMsg.type === "code" && (_jsx(CodeCard, { lang: anyMsg.lang, lines: anyMsg.lines })), anyMsg.type === "document" && (_jsx(DocumentCard, { title: anyMsg.title, lines: anyMsg.lines })), anyMsg.type === "link" && (_jsx(LinkCard, { linkedTask: anyMsg.linkedTask, preview: anyMsg.preview, tasks: tasks, onSelectTask: onSelectTask }))] })); + } + const isUser = msg.role === "user"; + return (_jsxs("div", { style: { + marginBottom: 13, + display: "flex", + flexDirection: "column", + alignItems: isUser ? "flex-end" : "flex-start", + }, children: [!isUser && (_jsxs("div", { style: { + display: "flex", + alignItems: "center", + gap: 5, + marginBottom: 4, + }, children: [_jsx("div", { style: { + width: 5, + height: 5, + background: "var(--green)", + borderRadius: "50%", + } }), _jsx("span", { style: { + fontFamily: "var(--mono)", + fontSize: 9, + color: "var(--ink3)", + letterSpacing: 1, + }, children: "AGENT" })] })), _jsx("div", { style: { + maxWidth: "88%", + padding: "9px 13px", + background: isUser ? "var(--ink)" : "white", + color: isUser ? "var(--bg)" : "var(--ink)", + border: isUser ? "none" : "1px solid var(--border)", + fontSize: 13.5, + lineHeight: 1.6, + whiteSpace: "pre-wrap", + }, children: msg.content })] })); +} diff --git a/dhee/ui/web/src/components/ChatMessage.tsx b/dhee/ui/web/src/components/ChatMessage.tsx new file mode 100644 index 0000000..8add042 --- /dev/null +++ b/dhee/ui/web/src/components/ChatMessage.tsx @@ -0,0 +1,101 @@ +import type { SankhyaTask, TaskMessage } from "../types"; +import { + BrowserCard, + CodeCard, + DocumentCard, + GrepCard, + LinkCard, +} from "./cards/Cards"; + +export function ChatMessage({ + msg, + tasks, + onSelectTask, +}: { + msg: TaskMessage; + tasks: SankhyaTask[]; + onSelectTask: (id: string) => void; +}) { + if (msg.role === "component") { + const anyMsg = msg as any; + return ( +
+ {anyMsg.type === "browser" && ( + + )} + {anyMsg.type === "grep" && ( + + )} + {anyMsg.type === "code" && ( + + )} + {anyMsg.type === "document" && ( + + )} + {anyMsg.type === "link" && ( + + )} +
+ ); + } + const isUser = msg.role === "user"; + return ( +
+ {!isUser && ( +
+
+ + AGENT + +
+ )} +
+ {msg.content} +
+
+ ); +} diff --git a/dhee/ui/web/src/components/FirstRunPanel.js b/dhee/ui/web/src/components/FirstRunPanel.js new file mode 100644 index 0000000..be3468f --- /dev/null +++ b/dhee/ui/web/src/components/FirstRunPanel.js @@ -0,0 +1,74 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +const panelStyle = { + border: "1px solid var(--border)", + background: "var(--bg)", + borderRadius: 8, + padding: 18, + width: "min(760px, 100%)", + boxSizing: "border-box", + display: "flex", + flexWrap: "wrap", + gap: 18, + boxShadow: "0 10px 28px rgba(20,16,10,0.06)", +}; +const monoCaps = { + fontFamily: "var(--mono)", + fontSize: 9, + letterSpacing: "0.08em", + textTransform: "uppercase", + color: "var(--ink3)", +}; +const actionBase = { + borderRadius: 5, + cursor: "pointer", + fontFamily: "var(--mono)", + fontSize: 10, + padding: "8px 11px", + whiteSpace: "nowrap", +}; +function actionStyle(tone = "secondary", disabled) { + const primary = tone === "primary"; + return { + ...actionBase, + border: `1px solid ${primary ? "var(--ink)" : "var(--border)"}`, + background: primary ? "var(--ink)" : "white", + color: primary ? "white" : "var(--accent)", + opacity: disabled ? 0.55 : 1, + cursor: disabled ? "not-allowed" : "pointer", + }; +} +export function FirstRunPanel({ title = "Set up a developer workspace", eyebrow = "First run", body = "Connect a repo folder, then start Codex or Claude Code from that folder so Dhee can mirror sessions and context.", actions = [], commands = [ + "dhee onboard --root .", + "dhee doctor", +], aside, }) { + return (_jsxs("section", { style: panelStyle, children: [_jsxs("div", { style: { flex: "1 1 300px", minWidth: 0 }, children: [_jsx("div", { style: monoCaps, children: eyebrow }), _jsx("h2", { style: { + margin: "5px 0 7px", + fontSize: 22, + lineHeight: 1.15, + color: "var(--ink)", + letterSpacing: 0, + }, children: title }), _jsx("div", { style: { + color: "var(--ink2)", + fontSize: 12.5, + lineHeight: 1.55, + maxWidth: 680, + }, children: body }), actions.length ? (_jsx("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", marginTop: 14 }, children: actions.map((action) => (_jsx("button", { type: "button", onClick: action.onClick, disabled: action.disabled, style: actionStyle(action.tone, action.disabled), children: action.label }, action.label))) })) : null] }), _jsxs("div", { style: { + flex: "1 1 260px", + border: "1px solid var(--border)", + background: "var(--surface)", + borderRadius: 6, + padding: 12, + minWidth: 0, + }, children: [_jsx("div", { style: { ...monoCaps, marginBottom: 8 }, children: "Terminal path" }), _jsx("div", { style: { display: "grid", gap: 7 }, children: commands.map((command) => (_jsx("code", { style: { + display: "block", + border: "1px solid var(--border)", + background: "white", + borderRadius: 4, + padding: "8px 9px", + fontFamily: "var(--mono)", + fontSize: 10, + color: "var(--ink)", + lineHeight: 1.45, + overflowWrap: "anywhere", + }, children: command }, command))) }), aside ? _jsx("div", { style: { marginTop: 10 }, children: aside }) : null] })] })); +} diff --git a/dhee/ui/web/src/components/FirstRunPanel.tsx b/dhee/ui/web/src/components/FirstRunPanel.tsx new file mode 100644 index 0000000..43c2cc6 --- /dev/null +++ b/dhee/ui/web/src/components/FirstRunPanel.tsx @@ -0,0 +1,151 @@ +import type { ReactNode } from "react"; + +type ActionTone = "primary" | "secondary"; + +export interface FirstRunAction { + label: string; + onClick: () => void; + tone?: ActionTone; + disabled?: boolean; +} + +interface FirstRunPanelProps { + title?: string; + eyebrow?: string; + body?: string; + actions?: FirstRunAction[]; + commands?: string[]; + aside?: ReactNode; +} + +const panelStyle: React.CSSProperties = { + border: "1px solid var(--border)", + background: "var(--bg)", + borderRadius: 8, + padding: 18, + width: "min(760px, 100%)", + boxSizing: "border-box", + display: "flex", + flexWrap: "wrap", + gap: 18, + boxShadow: "0 10px 28px rgba(20,16,10,0.06)", +}; + +const monoCaps: React.CSSProperties = { + fontFamily: "var(--mono)", + fontSize: 9, + letterSpacing: "0.08em", + textTransform: "uppercase", + color: "var(--ink3)", +}; + +const actionBase: React.CSSProperties = { + borderRadius: 5, + cursor: "pointer", + fontFamily: "var(--mono)", + fontSize: 10, + padding: "8px 11px", + whiteSpace: "nowrap", +}; + +function actionStyle(tone: ActionTone = "secondary", disabled?: boolean): React.CSSProperties { + const primary = tone === "primary"; + return { + ...actionBase, + border: `1px solid ${primary ? "var(--ink)" : "var(--border)"}`, + background: primary ? "var(--ink)" : "white", + color: primary ? "white" : "var(--accent)", + opacity: disabled ? 0.55 : 1, + cursor: disabled ? "not-allowed" : "pointer", + }; +} + +export function FirstRunPanel({ + title = "Set up a developer workspace", + eyebrow = "First run", + body = "Connect a repo folder, then start Codex or Claude Code from that folder so Dhee can mirror sessions and context.", + actions = [], + commands = [ + "dhee onboard --root .", + "dhee doctor", + ], + aside, +}: FirstRunPanelProps) { + return ( +
+
+
{eyebrow}
+

+ {title} +

+
+ {body} +
+ {actions.length ? ( +
+ {actions.map((action) => ( + + ))} +
+ ) : null} +
+
+
Terminal path
+
+ {commands.map((command) => ( + + {command} + + ))} +
+ {aside ?
{aside}
: null} +
+
+ ); +} diff --git a/dhee/ui/web/src/components/LinePanel.js b/dhee/ui/web/src/components/LinePanel.js new file mode 100644 index 0000000..739dfa5 --- /dev/null +++ b/dhee/ui/web/src/components/LinePanel.js @@ -0,0 +1,387 @@ +import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { api } from "../api"; +import { StatPill } from "./ui/StatPill"; +const MS_MINUTE = 60000; +const MS_HOUR = 3600000; +const MS_DAY = 86400000; +function fmtRelative(value) { + if (!value) + return ""; + const when = Date.parse(value); + if (Number.isNaN(when)) + return String(value); + const delta = Date.now() - when; + if (delta < MS_MINUTE) + return "just now"; + if (delta < MS_HOUR) + return `${Math.round(delta / MS_MINUTE)}m ago`; + if (delta < MS_DAY) + return `${Math.round(delta / MS_HOUR)}h ago`; + return `${Math.round(delta / MS_DAY)}d ago`; +} +function fmtClock(value) { + if (!value) + return ""; + const when = new Date(value); + if (Number.isNaN(when.getTime())) + return String(value); + return when.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); +} +// Tool-kind → accent. Matches canvas palette so broadcast linkages feel +// coherent across views. +const KIND_TONE = { + broadcast: "#e06b3f", + "tool.routed_read": "#4d6cff", + "tool.routed_bash": "#0b8b5f", + "tool.routed_grep": "#1fa971", + "tool.routed_agent": "#d74b7b", + "tool.hook_post_tool": "#64748b", + "tool.artifact_parse": "#d74b7b", + note: "#1a1a1a", + update: "#1a1a1a", +}; +function kindTone(kind) { + const key = String(kind || "").toLowerCase(); + if (KIND_TONE[key]) + return KIND_TONE[key]; + if (key.startsWith("tool.")) + return "#4d6cff"; + return "var(--accent)"; +} +function formatKindLabel(kind) { + const raw = String(kind || ""); + if (!raw) + return "note"; + if (raw.startsWith("tool.")) + return raw.slice(5); + return raw; +} +function runtimeTone(runtime) { + const key = String(runtime || "").toLowerCase(); + if (key.includes("claude")) + return "#e06b3f"; + if (key.includes("codex")) + return "#1a1a1a"; + if (key.includes("cursor")) + return "#4d6cff"; + if (key.includes("browser")) + return "#1fa971"; + return "var(--ink3)"; +} +function sessionsForRuntime(projects, workspaceSessions) { + const buckets = new Map(); + const visit = (session) => { + const runtime = String(session.runtime || "unknown").toLowerCase(); + const current = buckets.get(runtime) || { count: 0, latestUpdate: null, isLive: false }; + current.count += 1; + if (session.updatedAt && (!current.latestUpdate || session.updatedAt > current.latestUpdate)) { + current.latestUpdate = session.updatedAt; + } + if (session.isCurrent || session.state === "active" || session.state === "recent") { + current.isLive = true; + } + buckets.set(runtime, current); + }; + for (const session of workspaceSessions || []) + visit(session); + for (const project of projects || []) { + for (const session of project.sessions || []) + visit(session); + } + return Array.from(buckets.entries()) + .map(([runtime, value]) => ({ runtime, ...value })) + .sort((a, b) => b.count - a.count); +} +export function ConnectedAgents({ workspace, projects, workspaceSessions, }) { + const buckets = useMemo(() => sessionsForRuntime(projects, workspaceSessions), [projects, workspaceSessions]); + const total = buckets.reduce((sum, item) => sum + item.count, 0); + return (_jsxs("div", { style: { + border: "1px solid var(--border)", + background: "white", + padding: 14, + }, children: [_jsxs("div", { style: { + display: "flex", + justifyContent: "space-between", + alignItems: "baseline", + marginBottom: 10, + }, children: [_jsxs("span", { style: { + fontFamily: "var(--mono)", + fontSize: 9, + letterSpacing: 0.6, + color: "var(--ink3)", + textTransform: "uppercase", + }, children: ["Connected agents \u00B7 ", total] }), workspace ? (_jsx("span", { style: { + fontFamily: "var(--mono)", + fontSize: 9, + color: "var(--ink3)", + }, children: workspace.label || workspace.name })) : null] }), buckets.length === 0 ? (_jsx("div", { style: { + fontFamily: "var(--mono)", + fontSize: 10, + color: "var(--ink3)", + lineHeight: 1.55, + }, children: "No agent sessions yet. Launch claude-code or codex in this workspace \u2014 they will register here and start publishing to the line." })) : (_jsx("div", { style: { display: "grid", gap: 6 }, children: buckets.map((bucket) => (_jsxs("div", { style: { + display: "flex", + alignItems: "center", + justifyContent: "space-between", + gap: 10, + }, children: [_jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8, minWidth: 0 }, children: [_jsx("span", { style: { + width: 7, + height: 7, + borderRadius: "50%", + background: bucket.isLive ? "var(--green)" : "var(--ink3)", + boxShadow: bucket.isLive ? "0 0 0 3px rgba(31,169,113,0.18)" : "none", + flexShrink: 0, + } }), _jsx("span", { style: { + fontFamily: "var(--mono)", + fontSize: 11, + color: runtimeTone(bucket.runtime), + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis", + }, children: bucket.runtime }), _jsxs("span", { style: { + fontFamily: "var(--mono)", + fontSize: 9, + color: "var(--ink3)", + }, children: ["\u00B7 ", bucket.count] })] }), bucket.latestUpdate ? (_jsx("span", { style: { + fontFamily: "var(--mono)", + fontSize: 9, + color: "var(--ink3)", + }, children: fmtRelative(bucket.latestUpdate) })) : null] }, bucket.runtime))) }))] })); +} +export function LineMessageCard({ message, workspace, onOpenTask, }) { + const sourceProject = workspace?.projects.find((project) => project.id === message.project_id)?.name || "workspace"; + const targetProject = workspace?.projects.find((project) => project.id === message.target_project_id)?.name || ""; + const meta = (message.metadata || {}); + const runtime = String(meta.harness || meta.runtime || ""); + const tool = String(meta.tool_name || meta.toolName || ""); + const ptr = String(meta.ptr || ""); + const body = message.body || ""; + const accent = kindTone(message.message_kind); + return (_jsxs("div", { style: { + border: "1px solid var(--border)", + borderLeft: `3px solid ${accent}`, + background: "white", + padding: "11px 13px", + }, children: [_jsxs("div", { style: { + display: "flex", + justifyContent: "space-between", + alignItems: "baseline", + gap: 10, + marginBottom: 6, + }, children: [_jsxs("div", { style: { + fontFamily: "var(--mono)", + fontSize: 10, + color: runtimeTone(runtime), + letterSpacing: 0.4, + }, children: [fmtClock(message.created_at), runtime ? ` · ${runtime}` : "", sourceProject && sourceProject !== "workspace" ? ` · ${sourceProject}` : ""] }), _jsx("span", { style: { fontFamily: "var(--mono)", fontSize: 9, color: "var(--ink3)" }, children: fmtRelative(message.created_at) })] }), message.title ? (_jsx("div", { style: { fontSize: 13, fontWeight: 600, marginBottom: 5, lineHeight: 1.35 }, children: message.title })) : null, body ? (_jsx("div", { style: { + fontSize: 12, + color: "var(--ink2)", + lineHeight: 1.55, + whiteSpace: "pre-wrap", + wordBreak: "break-word", + }, children: body })) : null, _jsxs("div", { style: { display: "flex", gap: 5, flexWrap: "wrap", marginTop: 8 }, children: [_jsx(StatPill, { label: formatKindLabel(message.message_kind), tone: accent }), tool ? _jsx(StatPill, { label: tool.toLowerCase() }) : null, targetProject ? _jsx(StatPill, { label: `→ ${targetProject}`, tone: "#4d6cff" }) : null, ptr ? _jsx(StatPill, { label: ptr }) : null, message.task_id && onOpenTask ? (_jsx("button", { onClick: () => onOpenTask(String(message.task_id)), style: { + padding: "2px 8px", + border: "1px solid var(--ink)", + background: "var(--ink)", + color: "white", + fontFamily: "var(--mono)", + fontSize: 9, + letterSpacing: 0.4, + textTransform: "uppercase", + cursor: "pointer", + }, children: "open task" })) : null] })] })); +} +export function LineComposer({ workspace, activeProjectId, sessionId, taskId, onPublished, }) { + const [title, setTitle] = useState(""); + const [body, setBody] = useState(""); + const [targetProjectId, setTargetProjectId] = useState(""); + const [busy, setBusy] = useState(false); + const [notice, setNotice] = useState(null); + const [error, setError] = useState(null); + const bodyRef = useRef(null); + useEffect(() => { + bodyRef.current?.focus(); + }, [workspace?.id]); + const targetOptions = useMemo(() => (workspace?.projects || []).filter((project) => !activeProjectId || project.id !== activeProjectId), [workspace?.projects, activeProjectId]); + const publish = async () => { + if (!workspace?.id || !body.trim() || busy) + return; + setBusy(true); + setError(null); + setNotice(null); + try { + const response = await api.publishWorkspaceLineMessage(workspace.id, { + project_id: activeProjectId || undefined, + target_project_id: targetProjectId || undefined, + channel: activeProjectId ? "project" : "workspace", + session_id: sessionId || undefined, + task_id: taskId || undefined, + message_kind: targetProjectId ? "broadcast" : "note", + title: title.trim() || undefined, + body: body.trim(), + metadata: { + sourceProject: workspace.projects.find((p) => p.id === activeProjectId)?.name, + }, + }); + if (response.suggestedTask) { + const target = workspace.projects.find((p) => p.id === targetProjectId); + setNotice(target + ? `Broadcast sent · suggested task created in ${target.name}.` + : "Broadcast sent · suggested task created."); + } + else { + setNotice("Published to the workspace line."); + } + setTitle(""); + setBody(""); + setTargetProjectId(""); + onPublished?.(response.message, response.suggestedTask); + } + catch (e) { + setError(String(e)); + } + finally { + setBusy(false); + } + }; + if (!workspace) { + return (_jsx("div", { style: { + padding: 16, + border: "1px solid var(--border)", + fontFamily: "var(--mono)", + fontSize: 10, + color: "var(--ink3)", + background: "white", + }, children: "No workspace selected." })); + } + const disabled = !body.trim() || busy; + const targetName = targetOptions.find((p) => p.id === targetProjectId)?.name; + const ctaLabel = busy + ? "publishing…" + : targetProjectId + ? `broadcast → ${targetName || "project"}` + : "publish update"; + return (_jsxs("div", { style: { + border: "1px solid var(--border)", + background: "white", + padding: 14, + display: "flex", + flexDirection: "column", + gap: 8, + }, children: [_jsx("input", { value: title, onChange: (e) => setTitle(e.target.value), placeholder: "headline (optional) \u2014 e.g. user.plan field added", style: { + border: "1px solid var(--border)", + padding: "9px 11px", + background: "var(--bg)", + fontSize: 13, + } }), _jsxs("select", { value: targetProjectId, onChange: (e) => setTargetProjectId(e.target.value), style: { + border: "1px solid var(--border)", + padding: "9px 11px", + background: "var(--bg)", + fontFamily: "var(--mono)", + fontSize: 10, + }, children: [_jsxs("option", { value: "", children: ["Publish to current ", activeProjectId ? "project" : "workspace", " only"] }), targetOptions.map((project) => (_jsxs("option", { value: project.id, children: ["Broadcast into ", project.name, " (creates task)"] }, project.id)))] }), _jsx("textarea", { ref: bodyRef, value: body, onChange: (e) => setBody(e.target.value), placeholder: targetProjectId + ? "What should the target project's agent know? It will spawn a task with this context." + : "Broadcast a dependency change, a tool result, or a follow-up signal to the workspace line…", rows: 4, onKeyDown: (e) => { + if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { + e.preventDefault(); + void publish(); + } + }, style: { + border: "1px solid var(--border)", + padding: "10px 12px", + background: "var(--bg)", + fontSize: 13, + lineHeight: 1.55, + resize: "vertical", + } }), _jsxs("div", { style: { + display: "flex", + justifyContent: "space-between", + alignItems: "center", + gap: 10, + }, children: [_jsx("span", { style: { + fontFamily: "var(--mono)", + fontSize: 9, + color: "var(--ink3)", + letterSpacing: 0.3, + }, children: "\u2318/Ctrl + Enter to publish" }), _jsx("button", { onClick: () => void publish(), disabled: disabled, style: { + padding: "8px 14px", + border: "1px solid var(--ink)", + background: disabled ? "var(--ink3)" : "var(--ink)", + color: "white", + fontFamily: "var(--mono)", + fontSize: 10, + letterSpacing: 0.4, + opacity: disabled ? 0.7 : 1, + cursor: disabled ? "not-allowed" : "pointer", + }, children: ctaLabel })] }), notice ? (_jsx("div", { style: { fontSize: 11, color: "var(--green)", lineHeight: 1.5 }, children: notice })) : null, error ? (_jsx("div", { style: { fontSize: 11, color: "var(--rose)", lineHeight: 1.5 }, children: error })) : null] })); +} +// --------------------------------------------------------------------------- +// Live stream hook — owns the EventSource + initial fetch + dedup merge. +// Exposed so both ChannelView (full page) and WorkspaceView (right rail) +// share one subscription. +// --------------------------------------------------------------------------- +export function useWorkspaceLine(workspaceId, projectId) { + const [messages, setMessages] = useState([]); + const [live, setLive] = useState(false); + const [error, setError] = useState(null); + const merge = (incoming) => { + setMessages((current) => { + const seen = new Map(); + [...incoming, ...current].forEach((message) => { + if (message?.id) + seen.set(message.id, message); + }); + return Array.from(seen.values()).sort((a, b) => String(b.created_at || "").localeCompare(String(a.created_at || ""))); + }); + }; + const refresh = async () => { + if (!workspaceId) { + setMessages([]); + return; + } + try { + const snapshot = await api.workspaceLineMessages(workspaceId, { + project_id: projectId || undefined, + limit: 100, + }); + setMessages(snapshot.messages || []); + } + catch (e) { + setError(String(e)); + } + }; + useEffect(() => { + void refresh(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [workspaceId, projectId]); + useEffect(() => { + if (!workspaceId) { + setLive(false); + return; + } + const qs = new URLSearchParams(); + if (projectId) + qs.set("project_id", projectId); + const source = new EventSource(`/api/workspaces/${encodeURIComponent(workspaceId)}/line/stream${qs.toString() ? `?${qs.toString()}` : ""}`); + source.onopen = () => setLive(true); + source.onmessage = (event) => { + try { + const message = JSON.parse(event.data); + merge([message]); + } + catch { + /* keep-alive frames */ + } + }; + source.onerror = () => { + setLive(false); + source.close(); + }; + return () => { + source.close(); + setLive(false); + }; + }, [workspaceId, projectId]); + return { messages, live, error, merge, refresh }; +} diff --git a/dhee/ui/web/src/components/LinePanel.tsx b/dhee/ui/web/src/components/LinePanel.tsx new file mode 100644 index 0000000..929b2e1 --- /dev/null +++ b/dhee/ui/web/src/components/LinePanel.tsx @@ -0,0 +1,639 @@ +import { useEffect, useMemo, useRef, useState } from "react"; +import { api } from "../api"; +import type { + AgentSessionSummary, + ProjectSummary, + SankhyaTask, + WorkspaceLineMessage, + WorkspaceSummary, +} from "../types"; +import { StatPill } from "./ui/StatPill"; + +// --------------------------------------------------------------------------- +// LinePanel — the workspace *information line*. The entire product hangs +// off of this component: every agent tool call that lands on the line +// appears here, attributed by runtime + project, and humans can broadcast +// into any project to spawn a suggested task. +// +// Ownership: +// - Holds its own message list + SSE connection (single source of truth +// so embedders don't duplicate fetch/stream state). +// - Emits optional `onPublished` so hosts that care about side-effects +// (suggested task created, etc.) can refresh adjacent panels. +// +// Used by ChannelView (full-width) and WorkspaceView (right rail); both +// share the same control surface. +// --------------------------------------------------------------------------- + +export type LineMessage = WorkspaceLineMessage; + +const MS_MINUTE = 60_000; +const MS_HOUR = 3_600_000; +const MS_DAY = 86_400_000; + +function fmtRelative(value?: string | null): string { + if (!value) return ""; + const when = Date.parse(value); + if (Number.isNaN(when)) return String(value); + const delta = Date.now() - when; + if (delta < MS_MINUTE) return "just now"; + if (delta < MS_HOUR) return `${Math.round(delta / MS_MINUTE)}m ago`; + if (delta < MS_DAY) return `${Math.round(delta / MS_HOUR)}h ago`; + return `${Math.round(delta / MS_DAY)}d ago`; +} + +function fmtClock(value?: string | null): string { + if (!value) return ""; + const when = new Date(value); + if (Number.isNaN(when.getTime())) return String(value); + return when.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); +} + +// Tool-kind → accent. Matches canvas palette so broadcast linkages feel +// coherent across views. +const KIND_TONE: Record = { + broadcast: "#e06b3f", + "tool.routed_read": "#4d6cff", + "tool.routed_bash": "#0b8b5f", + "tool.routed_grep": "#1fa971", + "tool.routed_agent": "#d74b7b", + "tool.hook_post_tool": "#64748b", + "tool.artifact_parse": "#d74b7b", + note: "#1a1a1a", + update: "#1a1a1a", +}; + +function kindTone(kind?: string | null): string { + const key = String(kind || "").toLowerCase(); + if (KIND_TONE[key]) return KIND_TONE[key]; + if (key.startsWith("tool.")) return "#4d6cff"; + return "var(--accent)"; +} + +function formatKindLabel(kind?: string | null): string { + const raw = String(kind || ""); + if (!raw) return "note"; + if (raw.startsWith("tool.")) return raw.slice(5); + return raw; +} + +function runtimeTone(runtime?: string | null): string { + const key = String(runtime || "").toLowerCase(); + if (key.includes("claude")) return "#e06b3f"; + if (key.includes("codex")) return "#1a1a1a"; + if (key.includes("cursor")) return "#4d6cff"; + if (key.includes("browser")) return "#1fa971"; + return "var(--ink3)"; +} + +function sessionsForRuntime( + projects: ProjectSummary[], + workspaceSessions: AgentSessionSummary[], +): { runtime: string; count: number; latestUpdate?: string | null; isLive: boolean }[] { + const buckets = new Map(); + const visit = (session: AgentSessionSummary) => { + const runtime = String(session.runtime || "unknown").toLowerCase(); + const current = buckets.get(runtime) || { count: 0, latestUpdate: null, isLive: false }; + current.count += 1; + if (session.updatedAt && (!current.latestUpdate || session.updatedAt > current.latestUpdate)) { + current.latestUpdate = session.updatedAt; + } + if (session.isCurrent || session.state === "active" || session.state === "recent") { + current.isLive = true; + } + buckets.set(runtime, current); + }; + for (const session of workspaceSessions || []) visit(session); + for (const project of projects || []) { + for (const session of project.sessions || []) visit(session); + } + return Array.from(buckets.entries()) + .map(([runtime, value]) => ({ runtime, ...value })) + .sort((a, b) => b.count - a.count); +} + +export function ConnectedAgents({ + workspace, + projects, + workspaceSessions, +}: { + workspace: WorkspaceSummary | null; + projects: ProjectSummary[]; + workspaceSessions: AgentSessionSummary[]; +}) { + const buckets = useMemo( + () => sessionsForRuntime(projects, workspaceSessions), + [projects, workspaceSessions], + ); + const total = buckets.reduce((sum, item) => sum + item.count, 0); + return ( +
+
+ + Connected agents · {total} + + {workspace ? ( + + {workspace.label || workspace.name} + + ) : null} +
+ {buckets.length === 0 ? ( +
+ No agent sessions yet. Launch claude-code or codex in this workspace — + they will register here and start publishing to the line. +
+ ) : ( +
+ {buckets.map((bucket) => ( +
+
+ + + {bucket.runtime} + + + · {bucket.count} + +
+ {bucket.latestUpdate ? ( + + {fmtRelative(bucket.latestUpdate)} + + ) : null} +
+ ))} +
+ )} +
+ ); +} + +export function LineMessageCard({ + message, + workspace, + onOpenTask, +}: { + message: LineMessage; + workspace: WorkspaceSummary | null; + onOpenTask?: (taskId: string) => void; +}) { + const sourceProject = + workspace?.projects.find((project) => project.id === message.project_id)?.name || "workspace"; + const targetProject = + workspace?.projects.find((project) => project.id === message.target_project_id)?.name || ""; + const meta = (message.metadata || {}) as Record; + const runtime = String(meta.harness || meta.runtime || ""); + const tool = String(meta.tool_name || meta.toolName || ""); + const ptr = String(meta.ptr || ""); + const body = message.body || ""; + const accent = kindTone(message.message_kind); + return ( +
+
+
+ {fmtClock(message.created_at)} + {runtime ? ` · ${runtime}` : ""} + {sourceProject && sourceProject !== "workspace" ? ` · ${sourceProject}` : ""} +
+ + {fmtRelative(message.created_at)} + +
+ + {message.title ? ( +
+ {message.title} +
+ ) : null} + + {body ? ( +
+ {body} +
+ ) : null} + +
+ + {tool ? : null} + {targetProject ? : null} + {ptr ? : null} + {message.task_id && onOpenTask ? ( + + ) : null} +
+
+ ); +} + +export function LineComposer({ + workspace, + activeProjectId, + sessionId, + taskId, + onPublished, +}: { + workspace: WorkspaceSummary | null; + activeProjectId?: string | null; + sessionId?: string | null; + taskId?: string | null; + onPublished?: (message: LineMessage, suggestedTask?: SankhyaTask | null) => void; +}) { + const [title, setTitle] = useState(""); + const [body, setBody] = useState(""); + const [targetProjectId, setTargetProjectId] = useState(""); + const [busy, setBusy] = useState(false); + const [notice, setNotice] = useState(null); + const [error, setError] = useState(null); + const bodyRef = useRef(null); + + useEffect(() => { + bodyRef.current?.focus(); + }, [workspace?.id]); + + const targetOptions = useMemo( + () => + (workspace?.projects || []).filter( + (project) => !activeProjectId || project.id !== activeProjectId, + ), + [workspace?.projects, activeProjectId], + ); + + const publish = async () => { + if (!workspace?.id || !body.trim() || busy) return; + setBusy(true); + setError(null); + setNotice(null); + try { + const response = await api.publishWorkspaceLineMessage(workspace.id, { + project_id: activeProjectId || undefined, + target_project_id: targetProjectId || undefined, + channel: activeProjectId ? "project" : "workspace", + session_id: sessionId || undefined, + task_id: taskId || undefined, + message_kind: targetProjectId ? "broadcast" : "note", + title: title.trim() || undefined, + body: body.trim(), + metadata: { + sourceProject: workspace.projects.find((p) => p.id === activeProjectId)?.name, + }, + }); + if (response.suggestedTask) { + const target = workspace.projects.find((p) => p.id === targetProjectId); + setNotice( + target + ? `Broadcast sent · suggested task created in ${target.name}.` + : "Broadcast sent · suggested task created.", + ); + } else { + setNotice("Published to the workspace line."); + } + setTitle(""); + setBody(""); + setTargetProjectId(""); + onPublished?.(response.message, response.suggestedTask); + } catch (e) { + setError(String(e)); + } finally { + setBusy(false); + } + }; + + if (!workspace) { + return ( +
+ No workspace selected. +
+ ); + } + + const disabled = !body.trim() || busy; + const targetName = targetOptions.find((p) => p.id === targetProjectId)?.name; + const ctaLabel = busy + ? "publishing…" + : targetProjectId + ? `broadcast → ${targetName || "project"}` + : "publish update"; + + return ( +
+ setTitle(e.target.value)} + placeholder="headline (optional) — e.g. user.plan field added" + style={{ + border: "1px solid var(--border)", + padding: "9px 11px", + background: "var(--bg)", + fontSize: 13, + }} + /> + +