@hansenexus/hud 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +48 -0
  3. package/chunk-0cj8dkk7.js +127 -0
  4. package/chunk-1atxwdhw.js +48 -0
  5. package/chunk-24vrg16w.js +39 -0
  6. package/chunk-3j9sm249.js +26 -0
  7. package/chunk-50pq4sg6.js +41 -0
  8. package/chunk-5gtx3pza.js +9 -0
  9. package/chunk-6gw4x08j.js +264 -0
  10. package/chunk-6jt8yg4m.js +117 -0
  11. package/chunk-71se605m.js +85 -0
  12. package/chunk-82nm9wcg.js +316 -0
  13. package/chunk-8tam7r3h.js +68 -0
  14. package/chunk-babjpqkb.js +52 -0
  15. package/chunk-c5y856ad.js +98 -0
  16. package/chunk-ghxh8926.js +172 -0
  17. package/chunk-hc2g0n35.js +24 -0
  18. package/chunk-k0vr8dsy.js +179 -0
  19. package/chunk-k50yfp5d.js +166 -0
  20. package/chunk-kyeb6qeq.js +95 -0
  21. package/chunk-msqkkqw7.js +140 -0
  22. package/chunk-nm82wr8r.js +58 -0
  23. package/chunk-r06bpmy6.js +79 -0
  24. package/chunk-t9eh4t0b.js +89 -0
  25. package/chunk-tf774c4w.js +115 -0
  26. package/chunk-v086j53b.js +19 -0
  27. package/chunk-wspz08q7.js +67 -0
  28. package/chunk-xezxydm3.js +47 -0
  29. package/chunk-xjn0896g.js +574 -0
  30. package/chunk-xztw0xyt.js +113 -0
  31. package/chunk-zzrpd92r.js +172 -0
  32. package/core/access.d.mts +53 -0
  33. package/core/access.js +27 -0
  34. package/core/api-types.d.mts +146 -0
  35. package/core/api-types.js +9 -0
  36. package/core/capabilities.d.mts +27 -0
  37. package/core/capabilities.js +14 -0
  38. package/core/capture-buffer.d.mts +37 -0
  39. package/core/capture-buffer.js +9 -0
  40. package/core/force-state.d.mts +68 -0
  41. package/core/force-state.js +21 -0
  42. package/core/mock-cookie.d.mts +30 -0
  43. package/core/mock-cookie.js +15 -0
  44. package/core/types.d.mts +59 -0
  45. package/core/types.js +9 -0
  46. package/hud-provider.d.mts +38 -0
  47. package/hud-provider.js +15 -0
  48. package/hud.d.mts +15 -0
  49. package/hud.js +18 -0
  50. package/index.d.mts +94 -0
  51. package/index.js +170 -0
  52. package/lazy-panel.d.mts +28 -0
  53. package/lazy-panel.js +11 -0
  54. package/package.json +153 -0
  55. package/panels/cicd-panel.d.mts +9 -0
  56. package/panels/cicd-panel.js +13 -0
  57. package/panels/debug-info-panel.d.mts +21 -0
  58. package/panels/debug-info-panel.js +9 -0
  59. package/panels/dispatch-panel.d.mts +10 -0
  60. package/panels/dispatch-panel.js +14 -0
  61. package/panels/env-info-panel.d.mts +9 -0
  62. package/panels/env-info-panel.js +12 -0
  63. package/panels/feature-flags-panel.d.mts +26 -0
  64. package/panels/feature-flags-panel.js +9 -0
  65. package/panels/force-state-panel.d.mts +11 -0
  66. package/panels/force-state-panel.js +113 -0
  67. package/panels/info-row.d.mts +21 -0
  68. package/panels/info-row.js +14 -0
  69. package/panels/mock-seed-panel.d.mts +15 -0
  70. package/panels/mock-seed-panel.js +14 -0
  71. package/panels/network-console-panel.d.mts +6 -0
  72. package/panels/network-console-panel.js +9 -0
  73. package/panels/role-switcher-panel.d.mts +28 -0
  74. package/panels/role-switcher-panel.js +9 -0
  75. package/panels/sessions-panel.d.mts +6 -0
  76. package/panels/sessions-panel.js +13 -0
  77. package/panels/tokens-panel.d.mts +38 -0
  78. package/panels/tokens-panel.js +9 -0
  79. package/panels/unlock-panel.d.mts +33 -0
  80. package/panels/unlock-panel.js +14 -0
  81. package/panels/variants-panel.d.mts +51 -0
  82. package/panels/variants-panel.js +12 -0
  83. package/panels/vitals-panel.d.mts +11 -0
  84. package/panels/vitals-panel.js +15 -0
  85. package/server/guard.d.mts +42 -0
  86. package/server/guard.js +12 -0
  87. package/server/unlock.d.mts +28 -0
  88. package/server/unlock.js +108 -0
  89. package/state-boundary.d.mts +25 -0
  90. package/state-boundary.js +54 -0
@@ -0,0 +1,95 @@
1
+ import {
2
+ Button
3
+ } from "./chunk-xztw0xyt.js";
4
+
5
+ // panels/info-row.tsx
6
+ import { Copy } from "lucide-react";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ "use client";
9
+ function PanelSection({
10
+ icon: Icon,
11
+ title,
12
+ children
13
+ }) {
14
+ return /* @__PURE__ */ jsxs("div", {
15
+ children: [
16
+ /* @__PURE__ */ jsxs("h4", {
17
+ className: "mb-2 flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground",
18
+ children: [
19
+ Icon && /* @__PURE__ */ jsx(Icon, {
20
+ className: "h-3 w-3"
21
+ }),
22
+ title
23
+ ]
24
+ }),
25
+ /* @__PURE__ */ jsx("div", {
26
+ className: "space-y-1.5",
27
+ children
28
+ })
29
+ ]
30
+ });
31
+ }
32
+ function InfoRow({
33
+ label,
34
+ value,
35
+ copyable = false
36
+ }) {
37
+ function handleCopy() {
38
+ navigator.clipboard.writeText(value);
39
+ }
40
+ return /* @__PURE__ */ jsxs("div", {
41
+ className: "flex items-center justify-between px-2 py-1 hover:bg-muted/50",
42
+ children: [
43
+ /* @__PURE__ */ jsx("span", {
44
+ className: "text-[10px] text-muted-foreground",
45
+ children: label
46
+ }),
47
+ /* @__PURE__ */ jsxs("div", {
48
+ className: "flex items-center gap-1",
49
+ children: [
50
+ /* @__PURE__ */ jsx("span", {
51
+ className: "max-w-[220px] truncate font-mono text-[10px]",
52
+ children: value
53
+ }),
54
+ copyable && /* @__PURE__ */ jsx(Button, {
55
+ variant: "ghost",
56
+ size: "icon",
57
+ className: "h-4 w-4 shrink-0",
58
+ onClick: handleCopy,
59
+ children: /* @__PURE__ */ jsx(Copy, {
60
+ className: "h-2.5 w-2.5"
61
+ })
62
+ })
63
+ ]
64
+ })
65
+ ]
66
+ });
67
+ }
68
+ function ShortcutRow({ keys, label }) {
69
+ return /* @__PURE__ */ jsxs("div", {
70
+ className: "flex items-center justify-between px-2 py-1",
71
+ children: [
72
+ /* @__PURE__ */ jsx("span", {
73
+ className: "text-[10px] text-muted-foreground",
74
+ children: label
75
+ }),
76
+ /* @__PURE__ */ jsx("div", {
77
+ className: "flex gap-0.5",
78
+ children: keys.map((key) => /* @__PURE__ */ jsx("kbd", {
79
+ className: "flex h-5 min-w-[20px] items-center justify-center rounded border bg-muted/50 px-1 font-mono text-[10px]",
80
+ children: key
81
+ }, key))
82
+ })
83
+ ]
84
+ });
85
+ }
86
+ function LoadingSpinner() {
87
+ return /* @__PURE__ */ jsx("div", {
88
+ className: "flex items-center justify-center py-8",
89
+ children: /* @__PURE__ */ jsx("div", {
90
+ className: "h-5 w-5 animate-spin rounded-full border-2 border-muted-foreground border-t-transparent"
91
+ })
92
+ });
93
+ }
94
+
95
+ export { PanelSection, InfoRow, ShortcutRow, LoadingSpinner };
@@ -0,0 +1,140 @@
1
+ import {
2
+ hudRequiresUnlock
3
+ } from "./chunk-r06bpmy6.js";
4
+ import {
5
+ PanelSection
6
+ } from "./chunk-kyeb6qeq.js";
7
+ import {
8
+ Button
9
+ } from "./chunk-xztw0xyt.js";
10
+
11
+ // panels/unlock-panel.tsx
12
+ import { Lock, ShieldAlert } from "lucide-react";
13
+ import { useCallback, useEffect, useState } from "react";
14
+ import { jsx, jsxs } from "react/jsx-runtime";
15
+ "use client";
16
+ function useHudUnlock(apiBase, access) {
17
+ const required = hudRequiresUnlock(access);
18
+ const [state, setState] = useState(required ? "checking" : "unlocked");
19
+ const [error, setError] = useState(null);
20
+ useEffect(() => {
21
+ if (!required) {
22
+ setState("unlocked");
23
+ return;
24
+ }
25
+ let cancelled = false;
26
+ (async () => {
27
+ try {
28
+ const res = await fetch(`${apiBase}/unlock`, { cache: "no-store" });
29
+ const body = await res.json().catch(() => ({}));
30
+ if (cancelled)
31
+ return;
32
+ if (body.configured === false)
33
+ setState("unconfigured");
34
+ else
35
+ setState(body.unlocked ? "unlocked" : "locked");
36
+ } catch {
37
+ if (!cancelled)
38
+ setState("locked");
39
+ }
40
+ })();
41
+ return () => {
42
+ cancelled = true;
43
+ };
44
+ }, [apiBase, required]);
45
+ const unlock = useCallback(async (password) => {
46
+ setError(null);
47
+ try {
48
+ const res = await fetch(`${apiBase}/unlock`, {
49
+ method: "POST",
50
+ headers: { "Content-Type": "application/json" },
51
+ body: JSON.stringify({ password })
52
+ });
53
+ const body = await res.json().catch(() => ({}));
54
+ if (res.ok && body.unlocked) {
55
+ setState("unlocked");
56
+ return true;
57
+ }
58
+ setError(body.error ?? "unlock refused");
59
+ } catch (cause) {
60
+ setError(cause instanceof Error ? cause.message : "unlock failed");
61
+ }
62
+ return false;
63
+ }, [apiBase]);
64
+ return { state, error, unlock };
65
+ }
66
+ function HudUnlockPanel({ env, state, error, onUnlock }) {
67
+ const [password, setPassword] = useState("");
68
+ const [submitting, setSubmitting] = useState(false);
69
+ async function handleSubmit(event) {
70
+ event.preventDefault();
71
+ if (!password || submitting)
72
+ return;
73
+ setSubmitting(true);
74
+ const ok = await onUnlock(password);
75
+ setSubmitting(false);
76
+ if (ok)
77
+ setPassword("");
78
+ }
79
+ if (state === "unconfigured") {
80
+ return /* @__PURE__ */ jsx("div", {
81
+ className: "space-y-3 p-3",
82
+ children: /* @__PURE__ */ jsx(PanelSection, {
83
+ icon: ShieldAlert,
84
+ title: "hud locked",
85
+ children: /* @__PURE__ */ jsxs("p", {
86
+ className: "px-2 text-[11px] leading-relaxed text-muted-foreground",
87
+ children: [
88
+ "No ",
89
+ /* @__PURE__ */ jsx("code", {
90
+ className: "font-mono",
91
+ children: "HUD_ACCESS_PASSWORD"
92
+ }),
93
+ " is configured for this deployment, so the hud cannot be unlocked here. Set it from the app's 1Password item and restart the pod."
94
+ ]
95
+ })
96
+ })
97
+ });
98
+ }
99
+ return /* @__PURE__ */ jsx("div", {
100
+ className: "space-y-3 p-3",
101
+ children: /* @__PURE__ */ jsxs(PanelSection, {
102
+ icon: Lock,
103
+ title: `hud locked · ${env}`,
104
+ children: [
105
+ /* @__PURE__ */ jsx("p", {
106
+ className: "px-2 text-[11px] leading-relaxed text-muted-foreground",
107
+ children: env === "production" ? "Production is observe-only: vitals, logs and CI state, nothing that writes. The unlock lasts 2 hours." : "Enter the deployment's hud password to open the panels. The unlock lasts 12 hours."
108
+ }),
109
+ /* @__PURE__ */ jsxs("form", {
110
+ className: "flex items-center gap-2 px-2 pt-1",
111
+ onSubmit: handleSubmit,
112
+ children: [
113
+ /* @__PURE__ */ jsx("input", {
114
+ type: "password",
115
+ autoComplete: "off",
116
+ value: password,
117
+ onChange: (event) => setPassword(event.target.value),
118
+ placeholder: "hud password",
119
+ "aria-label": "hud password",
120
+ className: "h-7 flex-1 rounded border bg-background px-2 font-mono text-[11px] outline-none focus:ring-1 focus:ring-ring"
121
+ }),
122
+ /* @__PURE__ */ jsx(Button, {
123
+ type: "submit",
124
+ size: "sm",
125
+ className: "h-7 text-[11px]",
126
+ disabled: !password || submitting,
127
+ children: submitting ? "Checking…" : "Unlock"
128
+ })
129
+ ]
130
+ }),
131
+ error && /* @__PURE__ */ jsx("p", {
132
+ className: "px-2 text-[10px] text-destructive",
133
+ children: error
134
+ })
135
+ ]
136
+ })
137
+ });
138
+ }
139
+
140
+ export { useHudUnlock, HudUnlockPanel };
@@ -0,0 +1,58 @@
1
+ import {
2
+ PanelSection
3
+ } from "./chunk-kyeb6qeq.js";
4
+
5
+ // panels/tokens-panel.tsx
6
+ import { Palette } from "lucide-react";
7
+ import { useState } from "react";
8
+ import { jsx, jsxs } from "react/jsx-runtime";
9
+ "use client";
10
+ function TokensPanel({ adapter }) {
11
+ const { groups, sourceHint } = adapter;
12
+ const [copied, setCopied] = useState(null);
13
+ function copy(cssVar) {
14
+ navigator.clipboard?.writeText(`var(${cssVar})`).catch(() => {});
15
+ setCopied(cssVar);
16
+ window.setTimeout(() => setCopied((prev) => prev === cssVar ? null : prev), 1200);
17
+ }
18
+ return /* @__PURE__ */ jsxs("div", {
19
+ className: "space-y-4",
20
+ children: [
21
+ groups.map((group) => {
22
+ const scopeProps = group.scope ? { [group.scope.attribute]: group.scope.value ?? "" } : {};
23
+ return /* @__PURE__ */ jsx("div", {
24
+ ...scopeProps,
25
+ children: /* @__PURE__ */ jsx(PanelSection, {
26
+ icon: Palette,
27
+ title: group.label,
28
+ children: /* @__PURE__ */ jsx("div", {
29
+ className: "flex flex-wrap gap-1.5 px-2",
30
+ children: group.tokens.map((token) => /* @__PURE__ */ jsxs("button", {
31
+ type: "button",
32
+ onClick: () => copy(token.cssVar),
33
+ title: `var(${token.cssVar}) — click to copy`,
34
+ className: "group flex w-[104px] flex-col overflow-hidden rounded-md border text-left",
35
+ children: [
36
+ /* @__PURE__ */ jsx("span", {
37
+ className: "block h-8 w-full border-b",
38
+ style: { background: `var(${token.cssVar})` }
39
+ }),
40
+ /* @__PURE__ */ jsx("span", {
41
+ className: "truncate px-1.5 py-1 font-mono text-[9px] text-muted-foreground group-hover:text-foreground",
42
+ children: copied === token.cssVar ? "copied!" : token.label ?? token.cssVar.replace(/^--[a-z]+-/, "")
43
+ })
44
+ ]
45
+ }, token.cssVar))
46
+ })
47
+ })
48
+ }, group.label);
49
+ }),
50
+ sourceHint && /* @__PURE__ */ jsx("p", {
51
+ className: "px-2 font-mono text-[9.5px] text-muted-foreground/50",
52
+ children: sourceHint
53
+ })
54
+ ]
55
+ });
56
+ }
57
+
58
+ export { TokensPanel };
@@ -0,0 +1,79 @@
1
+ import {
2
+ resolveHudEnv
3
+ } from "./chunk-babjpqkb.js";
4
+
5
+ // core/access.ts
6
+ var HUD_UNLOCK_COOKIE = "hud-unlock";
7
+ var HUD_UNLOCK_TTL_MS = {
8
+ open: 0,
9
+ password: 12 * 60 * 60 * 1000,
10
+ flag: 2 * 60 * 60 * 1000,
11
+ denied: 0
12
+ };
13
+ function resolveHudAccess(env = resolveHudEnv()) {
14
+ if (env === "disabled")
15
+ return "denied";
16
+ if (env === "development")
17
+ return "open";
18
+ if (env === "production")
19
+ return "flag";
20
+ return "password";
21
+ }
22
+ function hudRequiresUnlock(access) {
23
+ return access === "password" || access === "flag";
24
+ }
25
+ function hudUnlockTtlMs(access) {
26
+ return HUD_UNLOCK_TTL_MS[access];
27
+ }
28
+ var encoder = new TextEncoder;
29
+ async function hmacHex(password, message) {
30
+ const key = await crypto.subtle.importKey("raw", encoder.encode(password), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
31
+ const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(message));
32
+ return Array.from(new Uint8Array(signature)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
33
+ }
34
+ function timingSafeEqual(a, b) {
35
+ const length = Math.max(a.length, b.length);
36
+ let diff = a.length ^ b.length;
37
+ for (let i = 0;i < length; i++) {
38
+ diff |= (a.charCodeAt(i) || 0) ^ (b.charCodeAt(i) || 0);
39
+ }
40
+ return diff === 0;
41
+ }
42
+ async function signHudUnlockToken(password, issuedAt = Date.now()) {
43
+ return `${issuedAt}.${await hmacHex(password, String(issuedAt))}`;
44
+ }
45
+ async function verifyHudUnlockToken(token, password, options = {}) {
46
+ if (!token || !password)
47
+ return false;
48
+ const separator = token.indexOf(".");
49
+ if (separator <= 0)
50
+ return false;
51
+ const issuedAtRaw = token.slice(0, separator);
52
+ const signature = token.slice(separator + 1);
53
+ const issuedAt = Number(issuedAtRaw);
54
+ if (!Number.isSafeInteger(issuedAt) || issuedAt <= 0)
55
+ return false;
56
+ const now = options.now ?? Date.now();
57
+ const ttlMs = options.ttlMs ?? HUD_UNLOCK_TTL_MS.flag;
58
+ const age = now - issuedAt;
59
+ if (age < 0 || age > ttlMs)
60
+ return false;
61
+ return timingSafeEqual(signature, await hmacHex(password, issuedAtRaw));
62
+ }
63
+ function readCookie(header, name) {
64
+ if (!header)
65
+ return;
66
+ for (const part of header.split(";")) {
67
+ const trimmed = part.trim();
68
+ if (trimmed.startsWith(`${name}=`)) {
69
+ return decodeURIComponent(trimmed.slice(name.length + 1));
70
+ }
71
+ }
72
+ return;
73
+ }
74
+ function hudUnlockCookieString(token, ttlMs) {
75
+ const base = `${HUD_UNLOCK_COOKIE}=${token ?? ""}; Path=/; HttpOnly; SameSite=Strict; Secure`;
76
+ return token ? `${base}; Max-Age=${Math.floor(ttlMs / 1000)}` : `${base}; Max-Age=0`;
77
+ }
78
+
79
+ export { HUD_UNLOCK_COOKIE, HUD_UNLOCK_TTL_MS, resolveHudAccess, hudRequiresUnlock, hudUnlockTtlMs, timingSafeEqual, signHudUnlockToken, verifyHudUnlockToken, readCookie, hudUnlockCookieString };
@@ -0,0 +1,89 @@
1
+ import {
2
+ bridgeFetch,
3
+ getBridgeInfo
4
+ } from "./chunk-50pq4sg6.js";
5
+ import {
6
+ useHud
7
+ } from "./chunk-wspz08q7.js";
8
+ import {
9
+ LoadingSpinner,
10
+ PanelSection
11
+ } from "./chunk-kyeb6qeq.js";
12
+ import {
13
+ Badge
14
+ } from "./chunk-xztw0xyt.js";
15
+
16
+ // panels/sessions-panel.tsx
17
+ import { CircleDot } from "lucide-react";
18
+ import { useEffect, useState } from "react";
19
+ import { jsx, jsxs } from "react/jsx-runtime";
20
+ "use client";
21
+ function SessionsPanel() {
22
+ const { config } = useHud();
23
+ const [state, setState] = useState({ kind: "loading" });
24
+ useEffect(() => {
25
+ let cancelled = false;
26
+ async function load() {
27
+ const info = await getBridgeInfo(config.apiBase ?? "/api/hud");
28
+ if (cancelled)
29
+ return;
30
+ if (!info) {
31
+ setState({ kind: "no-bridge" });
32
+ return;
33
+ }
34
+ try {
35
+ const res = await bridgeFetch(info, "/sessions");
36
+ const sessions = await res.json();
37
+ if (!cancelled)
38
+ setState({ kind: "ready", sessions });
39
+ } catch {
40
+ if (!cancelled)
41
+ setState({ kind: "no-bridge" });
42
+ }
43
+ }
44
+ load();
45
+ const timer = setInterval(() => void load(), 30000);
46
+ return () => {
47
+ cancelled = true;
48
+ clearInterval(timer);
49
+ };
50
+ }, [config.apiBase]);
51
+ if (state.kind === "loading")
52
+ return /* @__PURE__ */ jsx(LoadingSpinner, {});
53
+ if (state.kind === "no-bridge") {
54
+ return /* @__PURE__ */ jsx("p", {
55
+ className: "px-2 text-[10px] text-amber-500",
56
+ children: "bridge not reachable — run `bun hud:bridge` in the repo to see local Claude sessions."
57
+ });
58
+ }
59
+ return /* @__PURE__ */ jsxs(PanelSection, {
60
+ icon: CircleDot,
61
+ title: "Claude sessions · this machine",
62
+ children: [
63
+ state.sessions.length === 0 && /* @__PURE__ */ jsx("p", {
64
+ className: "px-2 text-[10px] text-muted-foreground/60",
65
+ children: "No active sessions."
66
+ }),
67
+ state.sessions.map((session) => /* @__PURE__ */ jsxs("div", {
68
+ className: "flex items-center gap-2 px-2 py-1 font-mono text-[10.5px]",
69
+ children: [
70
+ /* @__PURE__ */ jsx(Badge, {
71
+ variant: "outline",
72
+ className: `h-4 px-1.5 text-[9px] ${session.idleSeconds < 120 ? "text-emerald-500" : "text-muted-foreground"}`,
73
+ children: session.idleSeconds < 120 ? "active" : `idle ${Math.round(session.idleSeconds / 60)} m`
74
+ }),
75
+ /* @__PURE__ */ jsx("span", {
76
+ className: "min-w-0 flex-1 truncate",
77
+ children: session.name ?? session.sessionId.slice(0, 12)
78
+ }),
79
+ /* @__PURE__ */ jsx("span", {
80
+ className: "max-w-[200px] truncate text-muted-foreground/60",
81
+ children: session.branch || "—"
82
+ })
83
+ ]
84
+ }, session.sessionId))
85
+ ]
86
+ });
87
+ }
88
+
89
+ export { SessionsPanel };
@@ -0,0 +1,115 @@
1
+ import {
2
+ captureBuffer
3
+ } from "./chunk-24vrg16w.js";
4
+ import {
5
+ Button
6
+ } from "./chunk-xztw0xyt.js";
7
+
8
+ // panels/network-console-panel.tsx
9
+ import { Trash2 } from "lucide-react";
10
+ import { useState, useSyncExternalStore } from "react";
11
+ import { jsx, jsxs } from "react/jsx-runtime";
12
+ "use client";
13
+ var FILTERS = ["all", "log", "warn", "error", "fetch", "longtask"];
14
+ var KIND_CLASS = {
15
+ log: "text-muted-foreground",
16
+ warn: "text-amber-500",
17
+ error: "text-red-500",
18
+ fetch: "text-sky-500",
19
+ longtask: "text-violet-500"
20
+ };
21
+ function statusClass(status) {
22
+ if (status === undefined)
23
+ return "";
24
+ if (status === 0 || status >= 500)
25
+ return "text-red-500";
26
+ if (status >= 400)
27
+ return "text-amber-500";
28
+ return "text-emerald-500";
29
+ }
30
+ function NetworkConsolePanel() {
31
+ const entries = useSyncExternalStore(captureBuffer.subscribe, captureBuffer.getSnapshot, captureBuffer.getSnapshot);
32
+ const [filter, setFilter] = useState("all");
33
+ const visible = filter === "all" ? entries : entries.filter((e) => e.kind === filter);
34
+ return /* @__PURE__ */ jsxs("div", {
35
+ className: "flex h-full flex-col gap-2",
36
+ children: [
37
+ /* @__PURE__ */ jsxs("div", {
38
+ className: "flex items-center gap-1",
39
+ children: [
40
+ FILTERS.map((f) => /* @__PURE__ */ jsxs(Button, {
41
+ variant: filter === f ? "secondary" : "ghost",
42
+ size: "sm",
43
+ className: "h-6 px-2 font-mono text-[10px]",
44
+ onClick: () => setFilter(f),
45
+ children: [
46
+ f,
47
+ f !== "all" && /* @__PURE__ */ jsx("span", {
48
+ className: "ml-1 text-muted-foreground/60",
49
+ children: entries.filter((e) => e.kind === f).length
50
+ })
51
+ ]
52
+ }, f)),
53
+ /* @__PURE__ */ jsxs(Button, {
54
+ variant: "ghost",
55
+ size: "sm",
56
+ className: "ml-auto h-6 px-2 text-[10px]",
57
+ onClick: () => captureBuffer.clear(),
58
+ children: [
59
+ /* @__PURE__ */ jsx(Trash2, {
60
+ className: "mr-1 h-3 w-3"
61
+ }),
62
+ "clear"
63
+ ]
64
+ })
65
+ ]
66
+ }),
67
+ /* @__PURE__ */ jsxs("div", {
68
+ className: "min-h-0 flex-1 overflow-auto rounded-md border bg-muted/10 p-2",
69
+ children: [
70
+ visible.length === 0 && /* @__PURE__ */ jsx("p", {
71
+ className: "px-1 text-[10px] text-muted-foreground/60",
72
+ children: "Nothing captured yet — console, fetch, and long tasks land here."
73
+ }),
74
+ visible.slice(-200).map((entry) => /* @__PURE__ */ jsx(EntryRow, {
75
+ entry
76
+ }, entry.id))
77
+ ]
78
+ })
79
+ ]
80
+ });
81
+ }
82
+ function EntryRow({ entry }) {
83
+ const time = new Date(entry.ts).toLocaleTimeString(undefined, { hour12: false });
84
+ return /* @__PURE__ */ jsxs("div", {
85
+ className: "flex items-baseline gap-2 px-1 py-0.5 font-mono text-[10.5px] hover:bg-muted/40",
86
+ children: [
87
+ /* @__PURE__ */ jsx("span", {
88
+ className: "shrink-0 text-muted-foreground/50",
89
+ children: time
90
+ }),
91
+ /* @__PURE__ */ jsx("span", {
92
+ className: `w-14 shrink-0 uppercase ${KIND_CLASS[entry.kind]}`,
93
+ children: entry.kind
94
+ }),
95
+ /* @__PURE__ */ jsx("span", {
96
+ className: "min-w-0 flex-1 truncate",
97
+ title: entry.text,
98
+ children: entry.text
99
+ }),
100
+ entry.status !== undefined && /* @__PURE__ */ jsx("span", {
101
+ className: `shrink-0 ${statusClass(entry.status)}`,
102
+ children: entry.status || "ERR"
103
+ }),
104
+ entry.durationMs !== undefined && /* @__PURE__ */ jsxs("span", {
105
+ className: "shrink-0 text-muted-foreground/60",
106
+ children: [
107
+ entry.durationMs,
108
+ " ms"
109
+ ]
110
+ })
111
+ ]
112
+ });
113
+ }
114
+
115
+ export { NetworkConsolePanel };
@@ -0,0 +1,19 @@
1
+ // core/types.ts
2
+ var HUD_BUNDLE_MARKER = "__HN_HUD_BUNDLE_MARKER__";
3
+ var HUD_CAPABILITIES = [
4
+ "observe.app",
5
+ "observe.estate",
6
+ "cicd.read",
7
+ "cicd.trigger",
8
+ "inject.seed",
9
+ "mock.toggle",
10
+ "auth.impersonate",
11
+ "dispatch.local",
12
+ "dispatch.hermes",
13
+ "variants.switch",
14
+ "variants.vote",
15
+ "variants.generate",
16
+ "state.force"
17
+ ];
18
+
19
+ export { HUD_BUNDLE_MARKER, HUD_CAPABILITIES };
@@ -0,0 +1,67 @@
1
+ import {
2
+ hasCapability
3
+ } from "./chunk-babjpqkb.js";
4
+
5
+ // hud-provider.tsx
6
+ import {
7
+ createContext,
8
+ useCallback,
9
+ useContext,
10
+ useEffect,
11
+ useMemo,
12
+ useState
13
+ } from "react";
14
+ import { jsx } from "react/jsx-runtime";
15
+ "use client";
16
+ var HudContext = createContext(null);
17
+ function HudProvider({ config, env, panels: basePanels, children }) {
18
+ const [sources, setSources] = useState({});
19
+ const registerSource = useCallback((id, source) => {
20
+ setSources((prev) => ({ ...prev, [id]: source }));
21
+ }, []);
22
+ const unregisterSource = useCallback((id) => {
23
+ setSources((prev) => {
24
+ if (!(id in prev))
25
+ return prev;
26
+ const next = { ...prev };
27
+ delete next[id];
28
+ return next;
29
+ });
30
+ }, []);
31
+ const can = useCallback((capability) => hasCapability(env, capability), [env]);
32
+ const panels = useMemo(() => {
33
+ const registered = Object.values(sources).sort((a, b) => (a.order ?? 0) - (b.order ?? 0)).flatMap((source) => source.panels);
34
+ return [...basePanels ?? [], ...registered].filter((panel) => can(panel.capability)).sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
35
+ }, [basePanels, sources, can]);
36
+ const value = useMemo(() => ({
37
+ config,
38
+ env,
39
+ name: config.name ?? "hud",
40
+ can,
41
+ panels,
42
+ registerSource,
43
+ unregisterSource
44
+ }), [config, env, can, panels, registerSource, unregisterSource]);
45
+ return /* @__PURE__ */ jsx(HudContext.Provider, {
46
+ value,
47
+ children
48
+ });
49
+ }
50
+ function useHud() {
51
+ const ctx = useContext(HudContext);
52
+ if (!ctx)
53
+ throw new Error("useHud must be used within a HudProvider");
54
+ return ctx;
55
+ }
56
+ function useHudSafe() {
57
+ return useContext(HudContext);
58
+ }
59
+ function useRegisterHudPanels(id, panels, order = 0) {
60
+ const ctx = useContext(HudContext);
61
+ useEffect(() => {
62
+ ctx?.registerSource(id, { panels, order });
63
+ }, [ctx, id, panels, order]);
64
+ useEffect(() => () => ctx?.unregisterSource(id), [ctx, id]);
65
+ }
66
+
67
+ export { HudProvider, useHud, useHudSafe, useRegisterHudPanels };