@hansenexus/hud 0.3.0 → 0.4.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.
- package/README.md +186 -4
- package/dist/access-CNvJOwKM.d.ts +54 -0
- package/dist/capabilities-CjulXFQX.js +189 -0
- package/dist/{define-hud-BHQR8Lbv.d.ts → define-hud-BBzsSl8b.d.ts} +6 -1
- package/dist/env-DfxpPfs9.js +28 -0
- package/dist/env-vEjyJfPW.d.ts +17 -0
- package/dist/force-state-BaqWsmrf.js +153 -0
- package/dist/force-state-CCqoTfVB.d.ts +23 -0
- package/dist/handler-WmE4Lu7l.d.ts +39 -0
- package/dist/index-BYsdS-7d.d.ts +55 -0
- package/dist/index.d.ts +6 -36
- package/dist/index.js +119 -202
- package/dist/marker-C66LxOhJ.d.ts +9 -0
- package/dist/marker-CMrvxPmS.d.ts +12 -0
- package/dist/marker-DjeGvL-F.js +9 -0
- package/dist/marker-uZYPOJ4M.js +12 -0
- package/dist/markers.d.ts +3 -0
- package/dist/markers.js +3 -0
- package/dist/next.d.ts +3 -18
- package/dist/next.js +3 -28
- package/dist/payload-CLKDVZWl.d.ts +68 -0
- package/dist/plugins/access/server.d.ts +122 -0
- package/dist/plugins/access/server.js +339 -0
- package/dist/plugins/access.d.ts +41 -0
- package/dist/plugins/access.js +250 -0
- package/dist/plugins/agent/server.d.ts +129 -0
- package/dist/plugins/agent/server.js +429 -0
- package/dist/plugins/agent.d.ts +37 -0
- package/dist/plugins/agent.js +411 -0
- package/dist/plugins/design.d.ts +125 -0
- package/dist/plugins/design.js +619 -0
- package/dist/plugins/observe.d.ts +64 -0
- package/dist/plugins/observe.js +617 -0
- package/dist/plugins/ops/server.d.ts +82 -0
- package/dist/plugins/ops/server.js +380 -0
- package/dist/plugins/ops.d.ts +26 -0
- package/dist/plugins/ops.js +362 -0
- package/dist/plugins/vitals.js +3 -86
- package/dist/{plugins-1QmTmZyL.d.ts → plugins-CQ_0w1ot.d.ts} +39 -1
- package/dist/{plugins-CzUiTn_r.js → plugins-DVy_vaYr.js} +3 -1
- package/dist/review.d.ts +181 -0
- package/dist/review.js +1017 -0
- package/dist/selector-NSYSe7AZ.js +28 -0
- package/dist/server.d.ts +3 -39
- package/dist/server.js +1 -1
- package/dist/shortcut-U6ftVY06.js +222 -0
- package/dist/state-boundary.d.ts +26 -0
- package/dist/state-boundary.js +61 -0
- package/dist/styles--slk86ZQ.js +332 -0
- package/dist/types-DADBRiGk.d.ts +50 -0
- package/dist/types-QJUajwWq.js +35 -0
- package/dist/types-f3jlNxzw.d.ts +92 -0
- package/dist/vitals-panel-Dvc5dnR-.js +87 -0
- package/package.json +23 -3
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { n as HUD_HOST_TAG } from "./marker-uZYPOJ4M.js";
|
|
2
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
//#region src/core/grabber/picker.tsx
|
|
6
|
+
/**
|
|
7
|
+
* Styles for the picker overlay. The picker renders where it is mounted, so
|
|
8
|
+
* the caller puts these into the same (shadow) root. Colours fall back when
|
|
9
|
+
* the `--hud-*` tokens are absent.
|
|
10
|
+
*/
|
|
11
|
+
const PICKER_CSS = `
|
|
12
|
+
.grab-layer { position: fixed; inset: 0; pointer-events: none; }
|
|
13
|
+
.grab-hint {
|
|
14
|
+
position: fixed;
|
|
15
|
+
top: 0;
|
|
16
|
+
left: 50%;
|
|
17
|
+
transform: translateX(-50%);
|
|
18
|
+
padding: 3px 12px;
|
|
19
|
+
border-radius: 0 0 6px 6px;
|
|
20
|
+
white-space: nowrap;
|
|
21
|
+
background: var(--hud-accent, #0369a1);
|
|
22
|
+
color: var(--hud-accent-fg, #ffffff);
|
|
23
|
+
font: 600 11px/1.5 var(--hud-mono, ui-monospace, monospace);
|
|
24
|
+
}
|
|
25
|
+
.grab-outline {
|
|
26
|
+
position: fixed;
|
|
27
|
+
border: 1.5px solid var(--hud-accent, #0369a1);
|
|
28
|
+
background: color-mix(in srgb, var(--hud-accent, #0369a1) 8%, transparent);
|
|
29
|
+
border-radius: 3px;
|
|
30
|
+
}
|
|
31
|
+
.grab-label {
|
|
32
|
+
position: fixed;
|
|
33
|
+
max-width: min(480px, calc(100vw - 8px));
|
|
34
|
+
overflow: hidden;
|
|
35
|
+
text-overflow: ellipsis;
|
|
36
|
+
white-space: nowrap;
|
|
37
|
+
padding: 1px 6px;
|
|
38
|
+
border-radius: 3px;
|
|
39
|
+
background: var(--hud-accent, #0369a1);
|
|
40
|
+
color: var(--hud-accent-fg, #ffffff);
|
|
41
|
+
font: 600 10px/1.6 var(--hud-mono, ui-monospace, monospace);
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
/**
|
|
45
|
+
* Host pages set `cursor` on buttons and links, which beats an inherited
|
|
46
|
+
* crosshair, so grab mode briefly adds one global rule to the document.
|
|
47
|
+
*/
|
|
48
|
+
const CURSOR_RULE = "*, *::before, *::after { cursor: crosshair !important; }";
|
|
49
|
+
/** Pointer events the page must not see while picking: they would focus, submit or navigate. */
|
|
50
|
+
const SWALLOWED = [
|
|
51
|
+
"pointerdown",
|
|
52
|
+
"mousedown",
|
|
53
|
+
"pointerup",
|
|
54
|
+
"mouseup",
|
|
55
|
+
"dblclick"
|
|
56
|
+
];
|
|
57
|
+
function describeTag(element) {
|
|
58
|
+
return element.tagName.toLowerCase();
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Click-to-pick controller: crosshair, hover outline with a label, the next
|
|
62
|
+
* click picks, Escape cancels. Events inside the picker's own shadow host
|
|
63
|
+
* (the HUD's pill and panels) pass through untouched, so panels stay usable
|
|
64
|
+
* and mounted during a grab.
|
|
65
|
+
*/
|
|
66
|
+
function ElementPicker({ active, onPick, onCancel, hint = "Click an element · Esc cancels", describe = describeTag, ignore }) {
|
|
67
|
+
const layer = useRef(null);
|
|
68
|
+
const [highlight, setHighlight] = useState(null);
|
|
69
|
+
const props = useRef({
|
|
70
|
+
onPick,
|
|
71
|
+
onCancel,
|
|
72
|
+
describe,
|
|
73
|
+
ignore
|
|
74
|
+
});
|
|
75
|
+
props.current = {
|
|
76
|
+
onPick,
|
|
77
|
+
onCancel,
|
|
78
|
+
describe,
|
|
79
|
+
ignore
|
|
80
|
+
};
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (!active) {
|
|
83
|
+
setHighlight(null);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const root = layer.current?.getRootNode();
|
|
87
|
+
const ownHost = root instanceof ShadowRoot ? root.host : null;
|
|
88
|
+
const doc = layer.current?.ownerDocument ?? document;
|
|
89
|
+
let hovered = null;
|
|
90
|
+
function target(event) {
|
|
91
|
+
const path = event.composedPath();
|
|
92
|
+
if (ownHost && path.includes(ownHost)) return null;
|
|
93
|
+
const el = path[0];
|
|
94
|
+
if (!(el instanceof Element)) return null;
|
|
95
|
+
if (el === doc.documentElement || el === doc.body) return null;
|
|
96
|
+
return props.current.ignore?.(el) ? null : el;
|
|
97
|
+
}
|
|
98
|
+
function measure() {
|
|
99
|
+
if (!hovered?.isConnected) {
|
|
100
|
+
setHighlight(null);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const r = hovered.getBoundingClientRect();
|
|
104
|
+
setHighlight({
|
|
105
|
+
left: r.left,
|
|
106
|
+
top: r.top,
|
|
107
|
+
width: r.width,
|
|
108
|
+
height: r.height,
|
|
109
|
+
label: props.current.describe(hovered)
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function onMove(event) {
|
|
113
|
+
const el = target(event);
|
|
114
|
+
if (el === hovered) return;
|
|
115
|
+
hovered = el;
|
|
116
|
+
measure();
|
|
117
|
+
}
|
|
118
|
+
function swallow(event) {
|
|
119
|
+
if (!target(event)) return;
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
event.stopPropagation();
|
|
122
|
+
}
|
|
123
|
+
function onClick(event) {
|
|
124
|
+
const el = target(event);
|
|
125
|
+
if (!el) return;
|
|
126
|
+
event.preventDefault();
|
|
127
|
+
event.stopPropagation();
|
|
128
|
+
props.current.onPick(el, {
|
|
129
|
+
clientX: event.clientX,
|
|
130
|
+
clientY: event.clientY
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function onKey(event) {
|
|
134
|
+
if (event.key !== "Escape") return;
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
event.stopPropagation();
|
|
137
|
+
props.current.onCancel();
|
|
138
|
+
}
|
|
139
|
+
const cursor = doc.createElement("style");
|
|
140
|
+
cursor.textContent = CURSOR_RULE;
|
|
141
|
+
doc.head.appendChild(cursor);
|
|
142
|
+
const win = doc.defaultView ?? window;
|
|
143
|
+
doc.addEventListener("pointermove", onMove, true);
|
|
144
|
+
doc.addEventListener("click", onClick, true);
|
|
145
|
+
for (const type of SWALLOWED) doc.addEventListener(type, swallow, true);
|
|
146
|
+
win.addEventListener("keydown", onKey, true);
|
|
147
|
+
win.addEventListener("scroll", measure, true);
|
|
148
|
+
win.addEventListener("resize", measure);
|
|
149
|
+
return () => {
|
|
150
|
+
cursor.remove();
|
|
151
|
+
doc.removeEventListener("pointermove", onMove, true);
|
|
152
|
+
doc.removeEventListener("click", onClick, true);
|
|
153
|
+
for (const type of SWALLOWED) doc.removeEventListener(type, swallow, true);
|
|
154
|
+
win.removeEventListener("keydown", onKey, true);
|
|
155
|
+
win.removeEventListener("scroll", measure, true);
|
|
156
|
+
win.removeEventListener("resize", measure);
|
|
157
|
+
setHighlight(null);
|
|
158
|
+
};
|
|
159
|
+
}, [active]);
|
|
160
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
161
|
+
ref: layer,
|
|
162
|
+
className: "grab-layer",
|
|
163
|
+
hidden: !active,
|
|
164
|
+
children: [active && /* @__PURE__ */ jsx("div", {
|
|
165
|
+
className: "grab-hint",
|
|
166
|
+
role: "status",
|
|
167
|
+
children: hint
|
|
168
|
+
}), active && highlight && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
|
|
169
|
+
className: "grab-outline",
|
|
170
|
+
style: {
|
|
171
|
+
left: highlight.left - 2,
|
|
172
|
+
top: highlight.top - 2,
|
|
173
|
+
width: highlight.width + 4,
|
|
174
|
+
height: highlight.height + 4
|
|
175
|
+
}
|
|
176
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
177
|
+
className: "grab-label",
|
|
178
|
+
style: {
|
|
179
|
+
left: Math.max(highlight.left - 2, 4),
|
|
180
|
+
top: highlight.top >= 22 ? highlight.top - 20 : highlight.top + highlight.height + 4
|
|
181
|
+
},
|
|
182
|
+
children: highlight.label
|
|
183
|
+
})] })]
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
//#endregion
|
|
187
|
+
//#region src/shell/shadow-host.tsx
|
|
188
|
+
/**
|
|
189
|
+
* Inline `!important` on the host element: author `!important` in a host
|
|
190
|
+
* stylesheet (`* { position: static !important }`) loses to inline
|
|
191
|
+
* `!important`, so this is the one set of rules a client site cannot override.
|
|
192
|
+
* The host covers the viewport but lets every click through; HUD surfaces opt
|
|
193
|
+
* back in with `pointer-events: auto`.
|
|
194
|
+
*/
|
|
195
|
+
const HOST_STYLE = {
|
|
196
|
+
all: "initial",
|
|
197
|
+
position: "fixed",
|
|
198
|
+
inset: "0",
|
|
199
|
+
"z-index": "2147483000",
|
|
200
|
+
"pointer-events": "none",
|
|
201
|
+
display: "block"
|
|
202
|
+
};
|
|
203
|
+
/**
|
|
204
|
+
* Renders children into a shadow root on a host element appended to <body>.
|
|
205
|
+
* Mounted in an effect, so server rendering emits nothing and hydration never
|
|
206
|
+
* differs.
|
|
207
|
+
*/
|
|
208
|
+
function ShadowHost({ css, marker, children }) {
|
|
209
|
+
const [mount, setMount] = useState(null);
|
|
210
|
+
useLayoutEffect(() => {
|
|
211
|
+
const host = document.createElement(HUD_HOST_TAG);
|
|
212
|
+
for (const [prop, value] of Object.entries(HOST_STYLE)) host.style.setProperty(prop, value, "important");
|
|
213
|
+
const shadow = host.attachShadow({ mode: "open" });
|
|
214
|
+
const style = document.createElement("style");
|
|
215
|
+
style.textContent = css;
|
|
216
|
+
const root = document.createElement("div");
|
|
217
|
+
root.className = "hud";
|
|
218
|
+
root.setAttribute("data-hud-marker", marker);
|
|
219
|
+
shadow.append(style, root);
|
|
220
|
+
document.body.appendChild(host);
|
|
221
|
+
setMount(root);
|
|
222
|
+
return () => {
|
|
223
|
+
host.remove();
|
|
224
|
+
setMount(null);
|
|
225
|
+
};
|
|
226
|
+
}, [css, marker]);
|
|
227
|
+
return mount ? createPortal(children, mount) : null;
|
|
228
|
+
}
|
|
229
|
+
//#endregion
|
|
230
|
+
//#region src/shell/styles.ts
|
|
231
|
+
/**
|
|
232
|
+
* The HUD's whole stylesheet. It lives inside the shadow root, so host
|
|
233
|
+
* selectors cannot reach in and these rules cannot leak out. Inherited
|
|
234
|
+
* properties (font, color, line-height) still cross the shadow boundary, which
|
|
235
|
+
* is what `all: initial` on the root resets.
|
|
236
|
+
*/
|
|
237
|
+
const HUD_CSS = `
|
|
238
|
+
.hud {
|
|
239
|
+
all: initial;
|
|
240
|
+
--hud-bg: #ffffff;
|
|
241
|
+
--hud-bg-subtle: #f4f4f5;
|
|
242
|
+
--hud-fg: #18181b;
|
|
243
|
+
--hud-fg-muted: #71717a;
|
|
244
|
+
--hud-border: rgba(24, 24, 27, 0.14);
|
|
245
|
+
--hud-accent: #0369a1;
|
|
246
|
+
--hud-accent-fg: #ffffff;
|
|
247
|
+
--hud-good: #15803d;
|
|
248
|
+
--hud-warn: #b45309;
|
|
249
|
+
--hud-poor: #b91c1c;
|
|
250
|
+
--hud-shadow: 0 8px 24px rgba(0, 0, 0, 0.12), 0 2px 6px rgba(0, 0, 0, 0.08);
|
|
251
|
+
--hud-radius: 8px;
|
|
252
|
+
--hud-font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
253
|
+
--hud-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
254
|
+
|
|
255
|
+
font-family: var(--hud-font);
|
|
256
|
+
font-size: 12px;
|
|
257
|
+
line-height: 1.5;
|
|
258
|
+
color: var(--hud-fg);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
@media (prefers-color-scheme: dark) {
|
|
262
|
+
.hud {
|
|
263
|
+
--hud-bg: #18181b;
|
|
264
|
+
--hud-bg-subtle: #27272a;
|
|
265
|
+
--hud-fg: #f4f4f5;
|
|
266
|
+
--hud-fg-muted: #a1a1aa;
|
|
267
|
+
--hud-border: rgba(244, 244, 245, 0.16);
|
|
268
|
+
--hud-accent: #38bdf8;
|
|
269
|
+
--hud-accent-fg: #082f49;
|
|
270
|
+
--hud-good: #4ade80;
|
|
271
|
+
--hud-warn: #fbbf24;
|
|
272
|
+
--hud-poor: #f87171;
|
|
273
|
+
--hud-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.hud *, .hud *::before, .hud *::after { box-sizing: border-box; }
|
|
278
|
+
|
|
279
|
+
.hud button {
|
|
280
|
+
font: inherit;
|
|
281
|
+
color: inherit;
|
|
282
|
+
background: none;
|
|
283
|
+
border: 0;
|
|
284
|
+
margin: 0;
|
|
285
|
+
padding: 0;
|
|
286
|
+
cursor: pointer;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.hud :focus-visible { outline: 2px solid var(--hud-accent); outline-offset: 1px; }
|
|
290
|
+
|
|
291
|
+
.pill {
|
|
292
|
+
position: fixed;
|
|
293
|
+
left: 12px;
|
|
294
|
+
bottom: 12px;
|
|
295
|
+
display: flex;
|
|
296
|
+
align-items: center;
|
|
297
|
+
gap: 2px;
|
|
298
|
+
padding: 3px;
|
|
299
|
+
background: var(--hud-bg);
|
|
300
|
+
border: 1px solid var(--hud-border);
|
|
301
|
+
border-radius: 999px;
|
|
302
|
+
box-shadow: var(--hud-shadow);
|
|
303
|
+
font-family: var(--hud-mono);
|
|
304
|
+
pointer-events: auto;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.hud .pill-item {
|
|
308
|
+
display: flex;
|
|
309
|
+
align-items: center;
|
|
310
|
+
gap: 6px;
|
|
311
|
+
padding: 2px 8px;
|
|
312
|
+
border-radius: 999px;
|
|
313
|
+
white-space: nowrap;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.pill-item:hover, .pill-item[aria-pressed="true"] { background: var(--hud-bg-subtle); }
|
|
317
|
+
|
|
318
|
+
.env { font-weight: 600; color: var(--hud-accent); }
|
|
319
|
+
|
|
320
|
+
.dot { width: 6px; height: 6px; border-radius: 50%; background: var(--hud-fg-muted); }
|
|
321
|
+
.dot[data-rating="good"] { background: var(--hud-good); }
|
|
322
|
+
.dot[data-rating="needs-improvement"] { background: var(--hud-warn); }
|
|
323
|
+
.dot[data-rating="poor"] { background: var(--hud-poor); }
|
|
324
|
+
|
|
325
|
+
.vitals { width: 100%; border-collapse: collapse; font-family: var(--hud-mono); }
|
|
326
|
+
.vitals td { padding: 3px 0; }
|
|
327
|
+
.vitals td:last-child { text-align: right; }
|
|
328
|
+
.vitals .label { color: var(--hud-fg-muted); }
|
|
329
|
+
.empty { color: var(--hud-fg-muted); }
|
|
330
|
+
`;
|
|
331
|
+
//#endregion
|
|
332
|
+
export { PICKER_CSS as i, ShadowHost as n, ElementPicker as r, HUD_CSS as t };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { r as GrabPayload, t as ComponentHint } from "./payload-CLKDVZWl.js";
|
|
2
|
+
//#region src/plugins/agent/types.d.ts
|
|
3
|
+
/** Wire types shared by the agent plugins' client and server halves. */
|
|
4
|
+
/** One dispatch target as `GET <apiBase>/agent-dispatch/targets` lists it. */
|
|
5
|
+
interface DispatchTargetInfo {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
available: boolean;
|
|
10
|
+
/** Why the target is unavailable, e.g. "no Hermes token configured". */
|
|
11
|
+
reason?: string;
|
|
12
|
+
}
|
|
13
|
+
/** `POST <apiBase>/agent-dispatch/dispatch`. Needs a `note`, a `payload`, or both. */
|
|
14
|
+
interface DispatchBody {
|
|
15
|
+
target: string;
|
|
16
|
+
note: string;
|
|
17
|
+
payload: GrabPayload | null;
|
|
18
|
+
/** Component-manifest matches for the payload, see `matchComponentHints`. */
|
|
19
|
+
hints?: ComponentHint[];
|
|
20
|
+
}
|
|
21
|
+
interface DispatchReceipt {
|
|
22
|
+
ok: boolean;
|
|
23
|
+
target: string;
|
|
24
|
+
runId?: string;
|
|
25
|
+
/** Provider detail, e.g. the log file of a local run. */
|
|
26
|
+
detail?: string;
|
|
27
|
+
error?: string;
|
|
28
|
+
}
|
|
29
|
+
type AgentSessionStatus = "queued" | "running" | "idle" | "done" | "failed";
|
|
30
|
+
interface AgentSession {
|
|
31
|
+
/** Provider id, e.g. `claude-code` or `hermes`. */
|
|
32
|
+
provider: string;
|
|
33
|
+
id: string;
|
|
34
|
+
title: string;
|
|
35
|
+
status: AgentSessionStatus;
|
|
36
|
+
startedAt?: string;
|
|
37
|
+
updatedAt?: string;
|
|
38
|
+
detail?: string;
|
|
39
|
+
}
|
|
40
|
+
/** `GET <apiBase>/agent-sessions/sessions`: every provider's sessions, newest first. */
|
|
41
|
+
interface SessionsResponse {
|
|
42
|
+
sessions: AgentSession[];
|
|
43
|
+
/** Providers that failed to answer; the others still list. */
|
|
44
|
+
errors: {
|
|
45
|
+
provider: string;
|
|
46
|
+
error: string;
|
|
47
|
+
}[];
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
export { DispatchTargetInfo as a, DispatchReceipt as i, AgentSessionStatus as n, SessionsResponse as o, DispatchBody as r, AgentSession as t };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//#region src/plugins/ops/meta.ts
|
|
2
|
+
const OPS_CAPABILITY = "cicd.read";
|
|
3
|
+
//#endregion
|
|
4
|
+
//#region src/plugins/ops/state.ts
|
|
5
|
+
/**
|
|
6
|
+
* Combines the states of one commit's runs: any failure fails it, anything
|
|
7
|
+
* still going keeps it running, and it only passes when something passed.
|
|
8
|
+
*/
|
|
9
|
+
function combineStates(states) {
|
|
10
|
+
if (states.length === 0) return null;
|
|
11
|
+
if (states.includes("failure")) return "failure";
|
|
12
|
+
if (states.includes("running")) return "running";
|
|
13
|
+
if (states.includes("queued")) return "queued";
|
|
14
|
+
if (states.includes("cancelled")) return "cancelled";
|
|
15
|
+
return states.includes("success") ? "success" : "neutral";
|
|
16
|
+
}
|
|
17
|
+
/** Maps a state onto the shell's `.dot[data-rating]` colours. */
|
|
18
|
+
function stateRating(state) {
|
|
19
|
+
switch (state) {
|
|
20
|
+
case "success": return "good";
|
|
21
|
+
case "running":
|
|
22
|
+
case "queued": return "needs-improvement";
|
|
23
|
+
case "failure": return "poor";
|
|
24
|
+
default: return;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/plugins/ops/types.ts
|
|
29
|
+
const TELEMETRY_RANGES = [
|
|
30
|
+
"1h",
|
|
31
|
+
"24h",
|
|
32
|
+
"7d"
|
|
33
|
+
];
|
|
34
|
+
//#endregion
|
|
35
|
+
export { OPS_CAPABILITY as i, combineStates as n, stateRating as r, TELEMETRY_RANGES as t };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
//#region src/plugins/ops/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Provider seams of the `ops` plugin and the JSON its routes answer with.
|
|
4
|
+
* Type-only, so the client half can share them without pulling in a provider.
|
|
5
|
+
*/
|
|
6
|
+
/** Normalised run outcome, whatever the CI provider calls it. */
|
|
7
|
+
type CiState = "success" | "failure" | "cancelled" | "running" | "queued" | "neutral";
|
|
8
|
+
interface CiRun {
|
|
9
|
+
id: string;
|
|
10
|
+
/** Workflow or pipeline name. */
|
|
11
|
+
name: string;
|
|
12
|
+
/** First line of the commit message, or the run name. */
|
|
13
|
+
title: string;
|
|
14
|
+
state: CiState;
|
|
15
|
+
branch?: string;
|
|
16
|
+
/** Short commit sha. */
|
|
17
|
+
sha?: string;
|
|
18
|
+
url?: string;
|
|
19
|
+
/** ISO timestamp. */
|
|
20
|
+
startedAt?: string;
|
|
21
|
+
}
|
|
22
|
+
/** What is deployed right now, for providers that know. */
|
|
23
|
+
interface CiDeployment {
|
|
24
|
+
/** Environment name, such as `production`. */
|
|
25
|
+
environment?: string;
|
|
26
|
+
/** Deployed image reference, if the deploy is an image. */
|
|
27
|
+
image?: string;
|
|
28
|
+
sha?: string;
|
|
29
|
+
/** Ready replicas as `ready/desired`, or a provider's own status text. */
|
|
30
|
+
ready?: string;
|
|
31
|
+
url?: string;
|
|
32
|
+
/** ISO timestamp. */
|
|
33
|
+
at?: string;
|
|
34
|
+
}
|
|
35
|
+
interface CiSnapshot {
|
|
36
|
+
/** Provider name, such as `github-actions`. */
|
|
37
|
+
provider: string;
|
|
38
|
+
/** False when the provider's env is incomplete; `missing` names what to set. */
|
|
39
|
+
configured: boolean;
|
|
40
|
+
missing?: string;
|
|
41
|
+
/** Repository or project the runs belong to. */
|
|
42
|
+
repo: string;
|
|
43
|
+
/** Branch the runs were filtered by; null when it could not be resolved. */
|
|
44
|
+
branch: string | null;
|
|
45
|
+
/** Combined state of the latest commit's runs on `branch`; null without runs. */
|
|
46
|
+
state: CiState | null;
|
|
47
|
+
/** Short sha of the commit `state` describes. */
|
|
48
|
+
sha: string | null;
|
|
49
|
+
/** Newest first. */
|
|
50
|
+
runs: CiRun[];
|
|
51
|
+
}
|
|
52
|
+
/** A CI provider. Runs server side only; it may read tokens from env. */
|
|
53
|
+
interface CiProvider {
|
|
54
|
+
readonly name: string;
|
|
55
|
+
snapshot(): Promise<CiSnapshot>;
|
|
56
|
+
/** Optional: current deployment. Null when nothing is deployed. */
|
|
57
|
+
deployment?(): Promise<CiDeployment | null>;
|
|
58
|
+
}
|
|
59
|
+
/** `GET <apiBase>/ops/ci` */
|
|
60
|
+
interface CiResponse extends CiSnapshot {
|
|
61
|
+
ok: true;
|
|
62
|
+
/** Present when the provider implements `deployment`. */
|
|
63
|
+
deployment?: CiDeployment | null;
|
|
64
|
+
}
|
|
65
|
+
declare const TELEMETRY_RANGES: readonly ["1h", "24h", "7d"];
|
|
66
|
+
type TelemetryRange = (typeof TELEMETRY_RANGES)[number];
|
|
67
|
+
interface TelemetrySnapshot {
|
|
68
|
+
/** Provider name, such as `signoz`. */
|
|
69
|
+
provider: string;
|
|
70
|
+
/** False when the provider's env is incomplete; `missing` names what to set. */
|
|
71
|
+
configured: boolean;
|
|
72
|
+
missing?: string;
|
|
73
|
+
service: string;
|
|
74
|
+
range: TelemetryRange;
|
|
75
|
+
/** Errored calls per second, latest sample. */
|
|
76
|
+
errorRate: number | null;
|
|
77
|
+
/** 95th percentile latency in ms, latest sample. */
|
|
78
|
+
p95Ms: number | null;
|
|
79
|
+
/** Calls per second, latest sample. */
|
|
80
|
+
requestRate: number | null;
|
|
81
|
+
}
|
|
82
|
+
/** A telemetry provider. Runs server side only; it may read keys from env. */
|
|
83
|
+
interface TelemetryProvider {
|
|
84
|
+
readonly name: string;
|
|
85
|
+
snapshot(range: TelemetryRange): Promise<TelemetrySnapshot>;
|
|
86
|
+
}
|
|
87
|
+
/** `GET <apiBase>/ops/telemetry?range=1h` */
|
|
88
|
+
interface TelemetryResponse extends TelemetrySnapshot {
|
|
89
|
+
ok: true;
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
export { CiSnapshot as a, TelemetryRange as c, CiRun as i, TelemetryResponse as l, CiProvider as n, CiState as o, CiResponse as r, TelemetryProvider as s, CiDeployment as t, TelemetrySnapshot as u };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { useSyncExternalStore } from "react";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { onCLS, onFCP, onINP, onLCP, onTTFB } from "web-vitals";
|
|
4
|
+
//#region src/plugins/vitals/store.ts
|
|
5
|
+
/** Display order: the three Core Web Vitals first, then the diagnostics. */
|
|
6
|
+
const VITAL_ORDER = [
|
|
7
|
+
"LCP",
|
|
8
|
+
"INP",
|
|
9
|
+
"CLS",
|
|
10
|
+
"FCP",
|
|
11
|
+
"TTFB"
|
|
12
|
+
];
|
|
13
|
+
let snapshot = {};
|
|
14
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
15
|
+
let started = false;
|
|
16
|
+
function record(metric) {
|
|
17
|
+
snapshot = {
|
|
18
|
+
...snapshot,
|
|
19
|
+
[metric.name]: {
|
|
20
|
+
name: metric.name,
|
|
21
|
+
value: metric.value,
|
|
22
|
+
rating: metric.rating
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
for (const listener of listeners) listener();
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Starts collection once per page. Called when the HUD mounts, not when the
|
|
29
|
+
* panel opens: LCP and FCP are only reported for the initial load, so a lazy
|
|
30
|
+
* start would miss them.
|
|
31
|
+
*/
|
|
32
|
+
function startVitals() {
|
|
33
|
+
if (started) return;
|
|
34
|
+
started = true;
|
|
35
|
+
const opts = { reportAllChanges: true };
|
|
36
|
+
try {
|
|
37
|
+
onLCP(record, opts);
|
|
38
|
+
onINP(record, opts);
|
|
39
|
+
onCLS(record, opts);
|
|
40
|
+
onFCP(record, opts);
|
|
41
|
+
onTTFB(record, opts);
|
|
42
|
+
} catch {}
|
|
43
|
+
}
|
|
44
|
+
function getVitals() {
|
|
45
|
+
return snapshot;
|
|
46
|
+
}
|
|
47
|
+
function subscribeVitals(listener) {
|
|
48
|
+
listeners.add(listener);
|
|
49
|
+
return () => listeners.delete(listener);
|
|
50
|
+
}
|
|
51
|
+
function useVitals() {
|
|
52
|
+
return useSyncExternalStore(subscribeVitals, getVitals, getVitals);
|
|
53
|
+
}
|
|
54
|
+
function formatVital(name, value) {
|
|
55
|
+
if (name === "CLS") return value.toFixed(2);
|
|
56
|
+
if (value >= 1e3) return `${(value / 1e3).toFixed(1)} s`;
|
|
57
|
+
return `${Math.round(value)} ms`;
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/plugins/vitals/vitals-panel.tsx
|
|
61
|
+
function VitalsPanel() {
|
|
62
|
+
const vitals = useVitals();
|
|
63
|
+
const rows = VITAL_ORDER.flatMap((name) => {
|
|
64
|
+
const vital = vitals[name];
|
|
65
|
+
return vital ? [vital] : [];
|
|
66
|
+
});
|
|
67
|
+
if (rows.length === 0) return /* @__PURE__ */ jsx("p", {
|
|
68
|
+
className: "empty",
|
|
69
|
+
children: "No metrics yet. LCP reports after the first interaction or tab switch."
|
|
70
|
+
});
|
|
71
|
+
return /* @__PURE__ */ jsx("table", {
|
|
72
|
+
className: "vitals",
|
|
73
|
+
children: /* @__PURE__ */ jsx("tbody", { children: rows.map((vital) => /* @__PURE__ */ jsxs("tr", { children: [/* @__PURE__ */ jsx("td", {
|
|
74
|
+
className: "label",
|
|
75
|
+
children: vital.name
|
|
76
|
+
}), /* @__PURE__ */ jsxs("td", { children: [/* @__PURE__ */ jsx("span", {
|
|
77
|
+
className: "dot",
|
|
78
|
+
"data-rating": vital.rating,
|
|
79
|
+
style: {
|
|
80
|
+
display: "inline-block",
|
|
81
|
+
marginRight: 6
|
|
82
|
+
}
|
|
83
|
+
}), formatVital(vital.name, vital.value)] })] }, vital.name)) })
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
export { useVitals as i, formatVital as n, startVitals as r, VitalsPanel as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hansenexus/hud",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Modular dev HUD for React apps: status pill, floating panels, element grabber, plugin API.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
},
|
|
11
11
|
"workspaces": [
|
|
12
12
|
".",
|
|
13
|
-
"playground"
|
|
13
|
+
"playground",
|
|
14
|
+
"fixtures/next-app"
|
|
14
15
|
],
|
|
15
16
|
"files": [
|
|
16
17
|
"dist",
|
|
@@ -33,6 +34,18 @@
|
|
|
33
34
|
"./plugins/*": {
|
|
34
35
|
"types": "./dist/plugins/*.d.ts",
|
|
35
36
|
"import": "./dist/plugins/*.js"
|
|
37
|
+
},
|
|
38
|
+
"./state-boundary": {
|
|
39
|
+
"types": "./dist/state-boundary.d.ts",
|
|
40
|
+
"import": "./dist/state-boundary.js"
|
|
41
|
+
},
|
|
42
|
+
"./review": {
|
|
43
|
+
"types": "./dist/review.d.ts",
|
|
44
|
+
"import": "./dist/review.js"
|
|
45
|
+
},
|
|
46
|
+
"./markers": {
|
|
47
|
+
"types": "./dist/markers.d.ts",
|
|
48
|
+
"import": "./dist/markers.js"
|
|
36
49
|
}
|
|
37
50
|
},
|
|
38
51
|
"sideEffects": false,
|
|
@@ -54,7 +67,13 @@
|
|
|
54
67
|
},
|
|
55
68
|
"peerDependencies": {
|
|
56
69
|
"react": "^19.0.0",
|
|
57
|
-
"react-dom": "^19.0.0"
|
|
70
|
+
"react-dom": "^19.0.0",
|
|
71
|
+
"convex": "^1.43.0"
|
|
72
|
+
},
|
|
73
|
+
"peerDependenciesMeta": {
|
|
74
|
+
"convex": {
|
|
75
|
+
"optional": true
|
|
76
|
+
}
|
|
58
77
|
},
|
|
59
78
|
"devDependencies": {
|
|
60
79
|
"@biomejs/biome": "^2.5.15",
|
|
@@ -63,6 +82,7 @@
|
|
|
63
82
|
"@testing-library/react": "^16.3.3",
|
|
64
83
|
"@types/react": "^19.3.0",
|
|
65
84
|
"@types/react-dom": "^19.3.0",
|
|
85
|
+
"convex": "^1.46.0",
|
|
66
86
|
"jsdom": "^30.1.2",
|
|
67
87
|
"rolldown": "^1.2.12",
|
|
68
88
|
"tsdown": "^0.23.0",
|