@omg-dev/admin 0.4.27 → 0.4.28

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.
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useMemo, useState } from "react";
1
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  //#region src/styles.ts
4
4
  const STYLE_ID = "vibes-admin-styles";
@@ -20,6 +20,7 @@ const CSS = `
20
20
  --vadm-accent-fg: var(--primary-foreground, #ffffff);
21
21
  --vadm-card: var(--card, #fafafa);
22
22
  --vadm-danger: #dc2626;
23
+ --vadm-warn: color-mix(in srgb, #f59e0b 72%, var(--vadm-fg));
23
24
  color: var(--vadm-fg);
24
25
  font-family: inherit;
25
26
  font-size: 14px;
@@ -160,6 +161,240 @@ button.vadm-user-row { cursor: pointer; }
160
161
  animation: vadmPulse 1.4s ease-in-out infinite;
161
162
  }
162
163
  .vadm-skel-avatar { width: 38px; height: 38px; border-radius: 50%; flex: none; }
164
+ /* ── routing flow graph ───────────────────────────────────────────────────
165
+ Port of the docs site's <Flow> (apps/docs/app/components/flow.tsx) onto
166
+ --vadm-* tokens: hairline connector, 12px SVG chevron, pill edge label,
167
+ rounded card node with muted sub + accent ring. Horizontal by default so a
168
+ 100-trace log stays scannable; the 760px breakpoint below collapses it to
169
+ the docs' vertical trunk. No per-trace canvas — the row border is the
170
+ canvas — and rows are left-aligned so decision nodes column-align. */
171
+ .vadm-brain-trace { position: relative; background: var(--vadm-bg); padding: 13px 16px 12px; }
172
+ .vadm-brain-trace[data-failed="true"] { border-color: color-mix(in srgb, var(--vadm-danger) 55%, var(--vadm-border)); }
173
+ .vadm-flow-time {
174
+ position: absolute; top: 12px; right: 14px;
175
+ color: var(--vadm-muted); font-size: 11px; white-space: nowrap;
176
+ }
177
+ .vadm-flow { display: flex; align-items: center; justify-content: flex-start; min-width: 0; padding-right: 76px; }
178
+
179
+ .vadm-flow-node {
180
+ flex: 0 1 auto; min-width: 118px; max-width: 260px; min-height: 0;
181
+ display: flex; flex-direction: column; align-items: center; text-align: center;
182
+ border: 1px solid var(--vadm-border); border-radius: 12px;
183
+ background: var(--vadm-card); padding: 7px 12px;
184
+ text-decoration: none; color: inherit;
185
+ }
186
+ .vadm-flow-node--bare {
187
+ flex: 0 1 260px; min-width: 0; align-items: flex-start; text-align: left;
188
+ border-color: transparent; background: none; padding: 7px 10px 7px 0;
189
+ }
190
+ .vadm-flow-node--accent {
191
+ border-color: var(--vadm-accent);
192
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--vadm-accent) 25%, transparent);
193
+ }
194
+ .vadm-flow-node--accent .vadm-flow-label { color: var(--vadm-accent); }
195
+ /* Amber is mixed toward the host's foreground so it stays legible whether the
196
+ host theme is light or dark (the panel can be dark while the OS is light). */
197
+ .vadm-flow-node--changed {
198
+ border-color: color-mix(in srgb, var(--vadm-warn) 60%, var(--vadm-border));
199
+ background: color-mix(in srgb, #f59e0b 9%, var(--vadm-card));
200
+ }
201
+ .vadm-flow-node--changed .vadm-flow-sub { color: var(--vadm-warn); }
202
+ .vadm-flow-node--danger {
203
+ border-color: var(--vadm-danger);
204
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--vadm-danger) 22%, transparent);
205
+ }
206
+ .vadm-flow-node--danger .vadm-flow-label { color: var(--vadm-danger); }
207
+ a.vadm-flow-node:hover { border-color: var(--vadm-accent); }
208
+
209
+ .vadm-flow-label {
210
+ display: block; max-width: 100%; font-size: 13px; font-weight: 500; line-height: 1.35;
211
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
212
+ }
213
+ .vadm-flow-label[data-mono="true"] { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
214
+ .vadm-flow-pre { color: var(--vadm-muted); font-weight: 400; }
215
+ .vadm-flow-out { color: var(--vadm-muted); font-size: 10px; margin-left: 4px; }
216
+ .vadm-flow-quote::before { content: "\\201C"; }
217
+ .vadm-flow-quote::after { content: "\\201D"; }
218
+ .vadm-flow-sub {
219
+ display: block; max-width: 100%; margin-top: 2px;
220
+ font-size: 11px; line-height: 1.35; color: var(--vadm-muted);
221
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
222
+ }
223
+
224
+ .vadm-flow-edge {
225
+ position: relative; flex: none; align-self: center;
226
+ display: flex; align-items: center; justify-content: flex-end;
227
+ width: 68px; height: 12px; color: var(--vadm-muted);
228
+ }
229
+ .vadm-flow-edge::before {
230
+ content: ""; position: absolute; left: 0; right: 0; top: 50%;
231
+ border-top: 1px solid var(--vadm-border);
232
+ }
233
+ .vadm-flow-edge[data-danger="true"] { color: var(--vadm-danger); }
234
+ .vadm-flow-edge[data-danger="true"]::before { border-top-style: dashed; border-top-color: var(--vadm-danger); }
235
+ .vadm-flow-edge[data-running="true"]::before { animation: vadmPulse 1.4s ease-in-out infinite; }
236
+ .vadm-flow-arrow { position: relative; flex: none; margin-right: -2px; transform: rotate(-90deg); }
237
+ .vadm-flow-pill {
238
+ position: absolute; left: 50%; bottom: calc(50% + 5px); transform: translateX(-50%);
239
+ white-space: nowrap; font-size: 10px; font-weight: 500; line-height: 16px;
240
+ padding: 0 6px; border-radius: 999px;
241
+ border: 1px solid var(--vadm-border); background: var(--vadm-card); color: var(--vadm-muted);
242
+ }
243
+
244
+ /* ── message-list-first view ───────────────────────────────────────────────
245
+ The Chat Brain panel leads with a scannable list of inbound iMessages;
246
+ tapping a row unveils the routing flow graph (TraceRouting) inline. */
247
+ .vadm-filter-pill {
248
+ display: flex; align-items: center; gap: 8px; margin-bottom: 12px;
249
+ padding: 6px 6px 6px 12px; border: 1px solid var(--vadm-border); border-radius: 999px;
250
+ background: color-mix(in srgb, var(--vadm-accent) 7%, var(--vadm-bg)); font-size: 13px;
251
+ width: fit-content; max-width: 100%;
252
+ }
253
+ .vadm-filter-pill strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
254
+ .vadm-userchips { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-bottom: 12px; }
255
+ .vadm-userchip {
256
+ appearance: none; cursor: pointer; font: inherit; font-size: 12px;
257
+ display: inline-flex; align-items: center; gap: 6px;
258
+ border: 1px solid var(--vadm-border); border-radius: 999px;
259
+ background: var(--vadm-bg); color: var(--vadm-fg); padding: 3px 6px 3px 10px;
260
+ }
261
+ .vadm-userchip:hover { border-color: var(--vadm-accent); }
262
+ .vadm-userchip-count {
263
+ min-width: 18px; padding: 0 5px; border-radius: 999px; text-align: center;
264
+ font-size: 11px; font-weight: 600; color: var(--vadm-muted);
265
+ background: color-mix(in srgb, var(--vadm-muted) 15%, transparent);
266
+ }
267
+
268
+ /* Conversation (user view): a self-contained, scrollable chat pane. Oldest at
269
+ the top, newest at the bottom; "Load older" prepends above. iMessage-style
270
+ bubbles — their inbound messages (left) + our replies (right). A bubble with
271
+ a routing trace is tappable and reveals TraceRouting full-width below it.
272
+ Every color reads from the shared --vadm-* design tokens so the pane stays
273
+ consistent with the rest of the admin console in both light and dark. */
274
+ .vadm-conv {
275
+ display: flex; flex-direction: column; gap: 4px; min-width: 0;
276
+ max-height: min(64vh, 640px); overflow-y: auto; overscroll-behavior: contain;
277
+ padding: 12px; border: 1px solid var(--vadm-border); border-radius: 12px;
278
+ background: color-mix(in srgb, var(--vadm-muted) 5%, var(--vadm-bg));
279
+ }
280
+ .vadm-conv-more {
281
+ position: sticky; top: -12px; z-index: 1;
282
+ display: flex; justify-content: center; padding: 4px 0 8px;
283
+ margin: -12px -12px 4px; /* bleed to pane edges so the blur covers full width */
284
+ background: linear-gradient(
285
+ to bottom,
286
+ color-mix(in srgb, var(--vadm-muted) 5%, var(--vadm-bg)) 60%,
287
+ transparent
288
+ );
289
+ }
290
+ .vadm-bubble-row { display: flex; flex-direction: column; min-width: 0; }
291
+ .vadm-bubble-row--in { align-items: flex-start; }
292
+ .vadm-bubble-row--out { align-items: flex-end; }
293
+ .vadm-bubble {
294
+ appearance: none; font: inherit; text-align: left; cursor: default;
295
+ max-width: 82%; min-width: 0; border-radius: 16px; padding: 8px 12px;
296
+ border: 1px solid transparent; display: flex; flex-direction: column; gap: 3px;
297
+ }
298
+ .vadm-bubble[data-dir="in"] {
299
+ background: var(--vadm-card); border-color: var(--vadm-border);
300
+ color: var(--vadm-fg); border-bottom-left-radius: 5px;
301
+ }
302
+ .vadm-bubble[data-dir="out"] {
303
+ background: var(--vadm-accent); color: var(--vadm-accent-fg);
304
+ border-bottom-right-radius: 5px;
305
+ }
306
+ .vadm-bubble[data-has-trace] { cursor: pointer; }
307
+ .vadm-bubble[data-has-trace]:hover { filter: brightness(0.97); }
308
+ .vadm-bubble[data-open] {
309
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--vadm-accent) 60%, transparent);
310
+ }
311
+ .vadm-bubble-text { font-size: 14px; line-height: 1.35; white-space: pre-wrap; overflow-wrap: anywhere; }
312
+ .vadm-bubble-media { display: flex; flex-wrap: wrap; gap: 6px; }
313
+ .vadm-bubble-att {
314
+ display: block; max-width: 220px; max-height: 260px; width: auto; height: auto;
315
+ border-radius: 10px; object-fit: cover; background: rgba(0,0,0,0.2);
316
+ }
317
+ .vadm-bubble-meta {
318
+ display: flex; align-items: center; gap: 5px; font-size: 11px;
319
+ color: var(--vadm-muted);
320
+ }
321
+ .vadm-bubble[data-dir="out"] .vadm-bubble-meta {
322
+ color: color-mix(in srgb, var(--vadm-accent-fg) 80%, var(--vadm-accent));
323
+ }
324
+ .vadm-bubble-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--vadm-muted); }
325
+ .vadm-bubble-dot[data-outcome="completed"] { background: color-mix(in srgb, #16a34a 78%, var(--vadm-fg)); }
326
+ .vadm-bubble-dot[data-outcome="failed"] { background: var(--vadm-danger); }
327
+ .vadm-bubble-dot[data-outcome="running"] {
328
+ background: var(--vadm-accent-fg); animation: vadmPulse 1.4s ease-in-out infinite;
329
+ }
330
+ .vadm-bubble-detail {
331
+ align-self: stretch; margin: 4px 0 8px;
332
+ border: 1px solid color-mix(in srgb, var(--vadm-accent) 40%, var(--vadm-border));
333
+ border-radius: 10px; background: var(--vadm-bg); padding: 12px 14px;
334
+ }
335
+
336
+ .vadm-msg-list { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
337
+ .vadm-msg {
338
+ border: 1px solid var(--vadm-border); border-radius: 10px;
339
+ background: var(--vadm-bg); overflow: hidden;
340
+ }
341
+ .vadm-msg[data-open="true"] {
342
+ border-color: color-mix(in srgb, var(--vadm-accent) 45%, var(--vadm-border));
343
+ background: var(--vadm-card);
344
+ }
345
+ .vadm-msg[data-failed="true"] { border-color: color-mix(in srgb, var(--vadm-danger) 45%, var(--vadm-border)); }
346
+ .vadm-msg-row {
347
+ appearance: none; width: 100%; border: none; background: none; cursor: pointer;
348
+ color: inherit; font: inherit; text-align: left;
349
+ display: flex; align-items: center; gap: 11px; padding: 11px 13px; min-width: 0;
350
+ }
351
+ .vadm-msg-row:hover { background: color-mix(in srgb, var(--vadm-muted) 6%, transparent); }
352
+ .vadm-msg-dot {
353
+ flex: none; width: 9px; height: 9px; border-radius: 50%;
354
+ background: var(--vadm-muted);
355
+ }
356
+ .vadm-msg-dot[data-outcome="completed"] { background: color-mix(in srgb, #16a34a 78%, var(--vadm-fg)); }
357
+ .vadm-msg-dot[data-outcome="failed"] { background: var(--vadm-danger); }
358
+ .vadm-msg-dot[data-outcome="running"] {
359
+ background: var(--vadm-accent); animation: vadmPulse 1.4s ease-in-out infinite;
360
+ }
361
+ .vadm-msg-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
362
+ .vadm-msg-text {
363
+ font-weight: 550; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
364
+ }
365
+ .vadm-msg-meta {
366
+ display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
367
+ color: var(--vadm-muted); font-size: 12px; min-width: 0;
368
+ }
369
+ .vadm-msg-decision {
370
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px;
371
+ color: var(--vadm-fg);
372
+ }
373
+ .vadm-msg-decision[data-variant="accent"] { color: var(--vadm-accent); }
374
+ .vadm-msg-decision[data-variant="danger"] { color: var(--vadm-danger); }
375
+ .vadm-msg-sender { cursor: pointer; text-decoration: underline dotted; text-underline-offset: 2px; }
376
+ .vadm-msg-sender:hover { color: var(--vadm-fg); }
377
+ .vadm-msg-chevron {
378
+ flex: none; width: 8px; height: 8px; margin-right: 3px;
379
+ border-right: 1.5px solid var(--vadm-muted); border-bottom: 1.5px solid var(--vadm-muted);
380
+ transform: rotate(-45deg); transition: transform 0.15s;
381
+ }
382
+ .vadm-msg[data-open="true"] .vadm-msg-chevron { transform: rotate(45deg); }
383
+ .vadm-msg-detail { padding: 4px 14px 14px; border-top: 1px dashed var(--vadm-border); }
384
+
385
+ .vadm-pager { display: flex; align-items: center; justify-content: center; gap: 14px; margin-top: 14px; }
386
+
387
+ .vadm-flow-error { margin: 10px 0 0; font-size: 12px; }
388
+ .vadm-flow-more { margin-top: 2px; }
389
+ .vadm-flow-more > summary {
390
+ list-style: none; cursor: pointer; text-align: right;
391
+ color: var(--vadm-muted); font-size: 12px; line-height: 1.2;
392
+ }
393
+ .vadm-flow-more > summary::-webkit-details-marker { display: none; }
394
+ .vadm-flow-more > summary::after { content: "\\2026"; letter-spacing: 1px; }
395
+ .vadm-flow-more[open] > summary::after { content: "\\2212"; }
396
+ .vadm-flow-more > summary:hover { color: var(--vadm-fg); }
397
+ .vadm-flow-more .vadm-kv { margin-top: 8px; padding-top: 10px; border-top: 1px dashed var(--vadm-border); }
163
398
  @keyframes vadmPulse {
164
399
  0%, 100% { opacity: 0.55; }
165
400
  50% { opacity: 1; }
@@ -178,6 +413,23 @@ button.vadm-user-row { cursor: pointer; }
178
413
  .vadm-user-stats { flex-direction: row; align-items: center; gap: 8px; width: 100%; padding-left: 48px; margin-top: -4px; }
179
414
  .vadm-table { display: block; overflow-x: auto; white-space: nowrap; -webkit-overflow-scrolling: touch; }
180
415
  .vadm-detail.vadm-card { width: 100% !important; max-width: 100%; }
416
+ /* Below the fold the graph becomes the docs' vertical trunk verbatim:
417
+ full-width nodes stacked, hairline + chevron between, pill beside it. */
418
+ .vadm-flow { flex-direction: column; align-items: stretch; padding-right: 0; }
419
+ .vadm-flow-node { flex: none; width: 100%; max-width: none; align-items: center; text-align: center; }
420
+ .vadm-flow-node--bare { align-items: center; text-align: center; padding: 4px 0 6px; }
421
+ .vadm-flow-edge {
422
+ flex-direction: column; justify-content: flex-end; align-self: stretch;
423
+ width: 100%; height: 26px;
424
+ }
425
+ .vadm-flow-edge::before {
426
+ left: 50%; right: auto; top: 0; bottom: 0;
427
+ border-top: 0; border-left: 1px solid var(--vadm-border);
428
+ }
429
+ .vadm-flow-edge[data-danger="true"]::before { border-left-style: dashed; border-left-color: var(--vadm-danger); }
430
+ .vadm-flow-arrow { transform: none; margin: 0 0 -2px; }
431
+ .vadm-flow-pill { left: calc(50% + 8px); bottom: auto; top: 50%; transform: translateY(-50%); }
432
+ .vadm-flow-time { position: static; display: block; text-align: right; margin-bottom: 4px; }
181
433
  }
182
434
 
183
435
  @media (max-width: 480px) {
@@ -190,6 +442,19 @@ button.vadm-user-row { cursor: pointer; }
190
442
  }
191
443
  `;
192
444
  //#endregion
445
+ //#region src/safe-url.ts
446
+ /** URLs in the admin console can originate from reports or user-controlled
447
+ * Computers. Only ordinary web links may become clickable operator actions. */
448
+ function safeHref(value) {
449
+ if (typeof value !== "string") return void 0;
450
+ try {
451
+ const url = new URL(value);
452
+ return url.protocol === "http:" || url.protocol === "https:" ? value : void 0;
453
+ } catch {
454
+ return;
455
+ }
456
+ }
457
+ //#endregion
193
458
  //#region src/client.ts
194
459
  var AdminError = class extends Error {
195
460
  status;
@@ -303,6 +568,12 @@ var AdminClient = class {
303
568
  removeReport(id) {
304
569
  return this.rpc("bugReports", "removeReport", { id });
305
570
  }
571
+ listImessageBrainRouting(args) {
572
+ return this.rpc("imessageDebug", "listBrainRouting", args ?? {});
573
+ }
574
+ listImessageBrainTranscript(args) {
575
+ return this.rpc("imessageDebug", "listBrainTranscript", args);
576
+ }
306
577
  };
307
578
  //#endregion
308
579
  //#region src/react.tsx
@@ -1168,15 +1439,6 @@ function fmtTime(ms) {
1168
1439
  return String(ms);
1169
1440
  }
1170
1441
  }
1171
- function safeHref(v) {
1172
- if (typeof v !== "string") return void 0;
1173
- try {
1174
- const u = new URL(v);
1175
- return u.protocol === "http:" || u.protocol === "https:" ? v : void 0;
1176
- } catch {
1177
- return;
1178
- }
1179
- }
1180
1442
  const STORAGE_HOSTS = ["t3.storage.dev"];
1181
1443
  function safeImageSrc(v) {
1182
1444
  if (typeof v !== "string") return void 0;
@@ -1409,7 +1671,7 @@ function ReportsPanel({ client }) {
1409
1671
  "app:",
1410
1672
  " ",
1411
1673
  /* @__PURE__ */ jsx("a", {
1412
- href: `https://omg.dev/${detail.slug}`,
1674
+ href: `https://app.omg.dev/${detail.slug}`,
1413
1675
  target: "_blank",
1414
1676
  rel: "noreferrer",
1415
1677
  children: detail.slug
@@ -1449,13 +1711,13 @@ function ReportsPanel({ client }) {
1449
1711
  },
1450
1712
  children: [/* @__PURE__ */ jsx("a", {
1451
1713
  className: "vadm-btn vadm-btn-sm",
1452
- href: `https://omg.dev/${detail.slug}`,
1714
+ href: `https://app.omg.dev/${detail.slug}`,
1453
1715
  target: "_blank",
1454
1716
  rel: "noreferrer",
1455
1717
  children: "Open app"
1456
1718
  }), /* @__PURE__ */ jsx("a", {
1457
1719
  className: "vadm-btn vadm-btn-sm",
1458
- href: `https://omg.dev/${detail.slug}/inspect/data`,
1720
+ href: `https://app.omg.dev/${detail.slug}/inspect/data`,
1459
1721
  target: "_blank",
1460
1722
  rel: "noreferrer",
1461
1723
  children: "Inspect"
@@ -1550,7 +1812,729 @@ function ReportsPanel({ client }) {
1550
1812
  })
1551
1813
  ] });
1552
1814
  }
1815
+ function shortId(value) {
1816
+ if (!value) return "none";
1817
+ return value.length > 16 ? `${value.slice(0, 8)}…${value.slice(-5)}` : value;
1818
+ }
1819
+ function maskedPhone(value) {
1820
+ const tail = value.replace(/\D/g, "").slice(-4);
1821
+ return tail ? `••• ${tail}` : "unknown";
1822
+ }
1823
+ function formatMs(ms) {
1824
+ return ms < 1e3 ? `${ms}ms` : `${(ms / 1e3).toFixed(1)}s`;
1825
+ }
1826
+ function shortTime(at) {
1827
+ const d = new Date(at);
1828
+ const time = d.toLocaleTimeString([], {
1829
+ hour: "2-digit",
1830
+ minute: "2-digit"
1831
+ });
1832
+ return (/* @__PURE__ */ new Date()).toDateString() === d.toDateString() ? time : `${d.toLocaleDateString([], {
1833
+ month: "short",
1834
+ day: "numeric"
1835
+ })} ${time}`;
1836
+ }
1837
+ /** `computer.steer` → ["computer.", "steer"] so the namespace can be demoted. */
1838
+ function splitDecision(decision) {
1839
+ const dot = decision.indexOf(".");
1840
+ return dot === -1 ? ["", decision] : [decision.slice(0, dot + 1), decision.slice(dot + 1)];
1841
+ }
1842
+ /** routeVia is only ever "reply-reference" | "fallback"; never render null. */
1843
+ function routeViaLabel(via) {
1844
+ if (!via) return null;
1845
+ return via === "reply-reference" ? "reply-ref" : via;
1846
+ }
1847
+ function timingLine(fleetMs, llmMs, total) {
1848
+ const parts = [
1849
+ total != null ? `${total}ms total` : null,
1850
+ llmMs != null ? `brain ${llmMs}ms` : null,
1851
+ fleetMs != null ? `fleet ${fleetMs}ms` : null
1852
+ ].filter(Boolean);
1853
+ return parts.length ? parts.join(" · ") : "—";
1854
+ }
1855
+ function FlowArrow() {
1856
+ return /* @__PURE__ */ jsx("svg", {
1857
+ className: "vadm-flow-arrow",
1858
+ width: "12",
1859
+ height: "12",
1860
+ viewBox: "0 0 24 24",
1861
+ fill: "none",
1862
+ "aria-hidden": "true",
1863
+ children: /* @__PURE__ */ jsx("path", {
1864
+ d: "M6 13l6 6 6-6",
1865
+ stroke: "currentColor",
1866
+ strokeWidth: "2.2",
1867
+ strokeLinecap: "round",
1868
+ strokeLinejoin: "round"
1869
+ })
1870
+ });
1871
+ }
1872
+ /**
1873
+ * Connector between two nodes — hairline + chevron + optional pill edge label
1874
+ * (the docs Trunk). `danger` dashes it red, `running` pulses it.
1875
+ */
1876
+ function FlowEdge({ label, danger, running }) {
1877
+ return /* @__PURE__ */ jsxs("div", {
1878
+ className: "vadm-flow-edge",
1879
+ "data-danger": danger ? "true" : void 0,
1880
+ "data-running": running ? "true" : void 0,
1881
+ children: [label ? /* @__PURE__ */ jsx("span", {
1882
+ className: "vadm-flow-pill",
1883
+ children: label
1884
+ }) : null, /* @__PURE__ */ jsx(FlowArrow, {})]
1885
+ });
1886
+ }
1887
+ function FlowNode({ variant, mono, href, children, sub }) {
1888
+ const className = `vadm-flow-node${variant ? ` vadm-flow-node--${variant}` : ""}`;
1889
+ const body = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
1890
+ className: "vadm-flow-label",
1891
+ "data-mono": mono ? "true" : void 0,
1892
+ children: [children, href ? /* @__PURE__ */ jsx("span", {
1893
+ className: "vadm-flow-out",
1894
+ "aria-hidden": "true",
1895
+ children: "↗"
1896
+ }) : null]
1897
+ }), sub ? /* @__PURE__ */ jsx("span", {
1898
+ className: "vadm-flow-sub",
1899
+ children: sub
1900
+ }) : null] });
1901
+ if (href) return /* @__PURE__ */ jsx("a", {
1902
+ className,
1903
+ href,
1904
+ target: "_blank",
1905
+ rel: "noreferrer",
1906
+ children: body
1907
+ });
1908
+ return /* @__PURE__ */ jsx("div", {
1909
+ className,
1910
+ children: body
1911
+ });
1912
+ }
1913
+ function BrainTraceSkeleton() {
1914
+ return /* @__PURE__ */ jsx("div", {
1915
+ className: "vadm-list",
1916
+ "aria-label": "Loading Brain routing traces",
1917
+ children: Array.from({ length: 4 }).map((_, i) => /* @__PURE__ */ jsx("div", {
1918
+ className: "vadm-card vadm-brain-trace",
1919
+ "aria-hidden": "true",
1920
+ children: /* @__PURE__ */ jsxs("div", {
1921
+ className: "vadm-flow",
1922
+ children: [
1923
+ /* @__PURE__ */ jsx("div", {
1924
+ className: "vadm-flow-node vadm-flow-node--bare",
1925
+ children: /* @__PURE__ */ jsx("span", {
1926
+ className: "vadm-skel",
1927
+ style: { width: `${86 - i * 8}%` }
1928
+ })
1929
+ }),
1930
+ /* @__PURE__ */ jsx(FlowEdge, {}),
1931
+ /* @__PURE__ */ jsxs("div", {
1932
+ className: "vadm-flow-node",
1933
+ children: [/* @__PURE__ */ jsx("span", {
1934
+ className: "vadm-skel",
1935
+ style: { width: "70%" }
1936
+ }), /* @__PURE__ */ jsx("span", {
1937
+ className: "vadm-skel",
1938
+ style: {
1939
+ width: "88%",
1940
+ marginTop: 7
1941
+ }
1942
+ })]
1943
+ }),
1944
+ i % 2 === 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(FlowEdge, {}), /* @__PURE__ */ jsx("div", {
1945
+ className: "vadm-flow-node",
1946
+ children: /* @__PURE__ */ jsx("span", {
1947
+ className: "vadm-skel",
1948
+ style: { width: "64%" }
1949
+ })
1950
+ })] })
1951
+ ]
1952
+ })
1953
+ }, i))
1954
+ });
1955
+ }
1956
+ const BRAIN_FETCH_LIMIT = 200;
1957
+ const BRAIN_TRANSCRIPT_PAGE = 30;
1958
+ function BrainRoutingPanel({ client }) {
1959
+ const [traces, setTraces] = useState(null);
1960
+ const [steps, setSteps] = useState(/* @__PURE__ */ new Map());
1961
+ const [capture, setCapture] = useState(null);
1962
+ const [transcript, setTranscript] = useState(null);
1963
+ const [hasMore, setHasMore] = useState(false);
1964
+ const [loadingMore, setLoadingMore] = useState(false);
1965
+ const [error, setError] = useState(null);
1966
+ const [query, setQuery] = useState("");
1967
+ const [hits, setHits] = useState([]);
1968
+ const [filter, setFilter] = useState(null);
1969
+ const [openMsgId, setOpenMsgId] = useState(null);
1970
+ const scrollRef = useRef(null);
1971
+ const restoreRef = useRef(null);
1972
+ const bottomRef = useRef(false);
1973
+ useLayoutEffect(() => {
1974
+ const el = scrollRef.current;
1975
+ if (!el) return;
1976
+ if (restoreRef.current) {
1977
+ el.scrollTop = restoreRef.current.top + (el.scrollHeight - restoreRef.current.height);
1978
+ restoreRef.current = null;
1979
+ } else if (bottomRef.current) {
1980
+ el.scrollTop = el.scrollHeight;
1981
+ bottomRef.current = false;
1982
+ }
1983
+ }, [transcript]);
1984
+ const load = useCallback(async (userId) => {
1985
+ setTraces(null);
1986
+ try {
1987
+ const result = await client.listImessageBrainRouting({
1988
+ userId,
1989
+ limit: BRAIN_FETCH_LIMIT,
1990
+ steps: true
1991
+ });
1992
+ setTraces(result.traces);
1993
+ const grouped = /* @__PURE__ */ new Map();
1994
+ for (const row of result.steps ?? []) {
1995
+ const list = grouped.get(row.traceId);
1996
+ if (list) list.push(row);
1997
+ else grouped.set(row.traceId, [row]);
1998
+ }
1999
+ setSteps(grouped);
2000
+ setCapture(result.capture);
2001
+ setError(null);
2002
+ } catch (e) {
2003
+ setError(e);
2004
+ setTraces([]);
2005
+ }
2006
+ }, [client]);
2007
+ const loadTranscript = useCallback(async (userId) => {
2008
+ setTranscript(null);
2009
+ setHasMore(false);
2010
+ setOpenMsgId(null);
2011
+ try {
2012
+ const result = await client.listImessageBrainTranscript({
2013
+ userId,
2014
+ limit: BRAIN_TRANSCRIPT_PAGE
2015
+ });
2016
+ bottomRef.current = true;
2017
+ setTranscript(result.messages);
2018
+ setHasMore(result.hasMore);
2019
+ } catch (e) {
2020
+ setError(e);
2021
+ setTranscript([]);
2022
+ }
2023
+ }, [client]);
2024
+ const loadOlder = useCallback(async () => {
2025
+ if (!filter || loadingMore || !hasMore) return;
2026
+ const el = scrollRef.current;
2027
+ const oldest = transcript?.[0];
2028
+ if (!oldest) return;
2029
+ setLoadingMore(true);
2030
+ try {
2031
+ const result = await client.listImessageBrainTranscript({
2032
+ userId: filter.id,
2033
+ limit: BRAIN_TRANSCRIPT_PAGE,
2034
+ before: oldest.id
2035
+ });
2036
+ if (el) restoreRef.current = {
2037
+ top: el.scrollTop,
2038
+ height: el.scrollHeight
2039
+ };
2040
+ setTranscript((cur) => cur ? [...result.messages, ...cur] : result.messages);
2041
+ setHasMore(result.hasMore);
2042
+ } catch (e) {
2043
+ setError(e);
2044
+ } finally {
2045
+ setLoadingMore(false);
2046
+ }
2047
+ }, [
2048
+ client,
2049
+ filter,
2050
+ hasMore,
2051
+ loadingMore,
2052
+ transcript
2053
+ ]);
2054
+ useEffect(() => {
2055
+ ensureStyles();
2056
+ load();
2057
+ }, [load]);
2058
+ useEffect(() => {
2059
+ if (!query.trim() || filter) {
2060
+ setHits([]);
2061
+ return;
2062
+ }
2063
+ let live = true;
2064
+ const timer = setTimeout(async () => {
2065
+ try {
2066
+ const result = await client.findUsers(query.trim());
2067
+ if (live) setHits(result);
2068
+ } catch {
2069
+ if (live) setHits([]);
2070
+ }
2071
+ }, 200);
2072
+ return () => {
2073
+ live = false;
2074
+ clearTimeout(timer);
2075
+ };
2076
+ }, [
2077
+ client,
2078
+ query,
2079
+ filter
2080
+ ]);
2081
+ function openConversation(next) {
2082
+ setFilter(next);
2083
+ setQuery("");
2084
+ setHits([]);
2085
+ load(next.id);
2086
+ loadTranscript(next.id);
2087
+ }
2088
+ function pickUser(hit) {
2089
+ openConversation({
2090
+ id: hit.id,
2091
+ label: hit.name || hit.email
2092
+ });
2093
+ }
2094
+ function clearFilter() {
2095
+ setFilter(null);
2096
+ setQuery("");
2097
+ setTranscript(null);
2098
+ setHasMore(false);
2099
+ load();
2100
+ }
2101
+ function refresh() {
2102
+ if (filter) {
2103
+ load(filter.id);
2104
+ loadTranscript(filter.id);
2105
+ } else load();
2106
+ }
2107
+ const traceByMsgId = useMemo(() => {
2108
+ const m = /* @__PURE__ */ new Map();
2109
+ for (const t of traces ?? []) if (t.inboundMessageId) m.set(t.inboundMessageId, t);
2110
+ return m;
2111
+ }, [traces]);
2112
+ const traceByText = useMemo(() => {
2113
+ const m = /* @__PURE__ */ new Map();
2114
+ for (const t of traces ?? []) m.set(t.text, t);
2115
+ return m;
2116
+ }, [traces]);
2117
+ function traceForMessage(msg) {
2118
+ if (msg.direction !== "in") return null;
2119
+ return (msg.providerMessageId ? traceByMsgId.get(msg.providerMessageId) : void 0) ?? traceByText.get(msg.text) ?? null;
2120
+ }
2121
+ const senders = useMemo(() => {
2122
+ if (!traces) return [];
2123
+ const m = /* @__PURE__ */ new Map();
2124
+ for (const t of traces) {
2125
+ const e = m.get(t.userId);
2126
+ if (e) e.count++;
2127
+ else m.set(t.userId, {
2128
+ userId: t.userId,
2129
+ phone: t.phone,
2130
+ count: 1
2131
+ });
2132
+ }
2133
+ return [...m.values()].sort((a, b) => b.count - a.count);
2134
+ }, [traces]);
2135
+ return /* @__PURE__ */ jsxs("div", { children: [
2136
+ /* @__PURE__ */ jsxs("div", {
2137
+ className: "vadm-spread vadm-panel-head",
2138
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
2139
+ className: "vadm-h",
2140
+ children: "Chat Brain conversations"
2141
+ }), /* @__PURE__ */ jsx("p", {
2142
+ className: "vadm-sub",
2143
+ style: { margin: 0 },
2144
+ children: filter ? "Their messages and our replies — tap any message to reveal the backend routing behind it." : "Pick a conversation to see the full thread and the routing behind each message."
2145
+ })] }), /* @__PURE__ */ jsx("button", {
2146
+ className: "vadm-btn vadm-btn-sm",
2147
+ onClick: refresh,
2148
+ children: "Refresh"
2149
+ })]
2150
+ }),
2151
+ filter ? /* @__PURE__ */ jsxs("div", {
2152
+ className: "vadm-filter-pill",
2153
+ children: [
2154
+ /* @__PURE__ */ jsx("span", {
2155
+ className: "vadm-muted",
2156
+ children: "Conversation with"
2157
+ }),
2158
+ /* @__PURE__ */ jsx("strong", { children: filter.label }),
2159
+ /* @__PURE__ */ jsx("button", {
2160
+ className: "vadm-btn vadm-btn-sm",
2161
+ onClick: clearFilter,
2162
+ children: "← All"
2163
+ })
2164
+ ]
2165
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
2166
+ className: "vadm-search",
2167
+ style: {
2168
+ position: "relative",
2169
+ marginBottom: 12
2170
+ },
2171
+ children: [/* @__PURE__ */ jsx("input", {
2172
+ className: "vadm-input",
2173
+ placeholder: "Find by account email…",
2174
+ value: query,
2175
+ onChange: (e) => setQuery(e.target.value)
2176
+ }), hits.length > 0 && /* @__PURE__ */ jsx("div", {
2177
+ className: "vadm-card",
2178
+ style: {
2179
+ position: "absolute",
2180
+ zIndex: 5,
2181
+ top: 38,
2182
+ left: 0,
2183
+ right: 0,
2184
+ padding: 4
2185
+ },
2186
+ children: hits.map((hit) => /* @__PURE__ */ jsxs("button", {
2187
+ className: "vadm-user-row",
2188
+ onClick: () => pickUser(hit),
2189
+ children: [/* @__PURE__ */ jsx(UserAvatar, { user: hit }), /* @__PURE__ */ jsxs("span", {
2190
+ className: "vadm-grow",
2191
+ children: [/* @__PURE__ */ jsx("span", {
2192
+ style: {
2193
+ display: "block",
2194
+ fontWeight: 600
2195
+ },
2196
+ children: hit.name || hit.email
2197
+ }), /* @__PURE__ */ jsx("span", {
2198
+ className: "vadm-muted",
2199
+ style: { fontSize: 12 },
2200
+ children: hit.email
2201
+ })]
2202
+ })]
2203
+ }, hit.id))
2204
+ })]
2205
+ }), senders.length > 0 && /* @__PURE__ */ jsxs("div", {
2206
+ className: "vadm-userchips",
2207
+ children: [/* @__PURE__ */ jsx("span", {
2208
+ className: "vadm-muted",
2209
+ style: { fontSize: 12 },
2210
+ children: "Recent:"
2211
+ }), senders.slice(0, 8).map((s) => /* @__PURE__ */ jsxs("button", {
2212
+ className: "vadm-userchip",
2213
+ onClick: () => openConversation({
2214
+ id: s.userId,
2215
+ label: maskedPhone(s.phone)
2216
+ }),
2217
+ children: [maskedPhone(s.phone), /* @__PURE__ */ jsx("span", {
2218
+ className: "vadm-userchip-count",
2219
+ children: s.count
2220
+ })]
2221
+ }, s.userId))]
2222
+ })] }),
2223
+ /* @__PURE__ */ jsx(ErrorBanner, { error }),
2224
+ filter ? transcript === null ? /* @__PURE__ */ jsx(BrainTraceSkeleton, {}) : transcript.length === 0 ? /* @__PURE__ */ jsxs("div", {
2225
+ className: "vadm-empty",
2226
+ children: [/* @__PURE__ */ jsx("strong", {
2227
+ style: {
2228
+ display: "block",
2229
+ color: "var(--foreground)",
2230
+ marginBottom: 5
2231
+ },
2232
+ children: "No messages yet"
2233
+ }), "This account has no iMessage history with the line."]
2234
+ }) : /* @__PURE__ */ jsxs("div", {
2235
+ className: "vadm-conv",
2236
+ ref: scrollRef,
2237
+ children: [hasMore && /* @__PURE__ */ jsx("div", {
2238
+ className: "vadm-conv-more",
2239
+ children: /* @__PURE__ */ jsx("button", {
2240
+ className: "vadm-btn vadm-btn-sm",
2241
+ onClick: loadOlder,
2242
+ disabled: loadingMore,
2243
+ children: loadingMore ? "Loading…" : "Load older messages"
2244
+ })
2245
+ }), transcript.map((msg) => {
2246
+ const inbound = msg.direction === "in";
2247
+ const trace = traceForMessage(msg);
2248
+ const open = openMsgId === msg.id;
2249
+ return /* @__PURE__ */ jsxs("div", {
2250
+ className: `vadm-bubble-row vadm-bubble-row--${inbound ? "in" : "out"}`,
2251
+ children: [/* @__PURE__ */ jsxs("button", {
2252
+ type: "button",
2253
+ className: "vadm-bubble",
2254
+ "data-dir": inbound ? "in" : "out",
2255
+ "data-open": open ? "true" : void 0,
2256
+ "data-has-trace": trace ? "true" : void 0,
2257
+ "aria-expanded": trace ? open : void 0,
2258
+ onClick: trace ? () => setOpenMsgId(open ? null : msg.id) : void 0,
2259
+ children: [
2260
+ msg.media && msg.media.length > 0 && /* @__PURE__ */ jsx("span", {
2261
+ className: "vadm-bubble-media",
2262
+ children: msg.media.map((url) => /\.(mp4|mov|webm)(?:$|\?)/i.test(url) ? /* @__PURE__ */ jsx("video", {
2263
+ className: "vadm-bubble-att",
2264
+ src: url,
2265
+ controls: true,
2266
+ preload: "metadata"
2267
+ }, url) : /* @__PURE__ */ jsx("img", {
2268
+ className: "vadm-bubble-att",
2269
+ src: url,
2270
+ alt: "attachment",
2271
+ loading: "lazy"
2272
+ }, url))
2273
+ }),
2274
+ msg.text && /* @__PURE__ */ jsx("span", {
2275
+ className: "vadm-bubble-text",
2276
+ children: msg.text
2277
+ }),
2278
+ /* @__PURE__ */ jsxs("span", {
2279
+ className: "vadm-bubble-meta",
2280
+ children: [
2281
+ /* @__PURE__ */ jsx("span", { children: inbound ? "them" : "us" }),
2282
+ /* @__PURE__ */ jsx("span", {
2283
+ "aria-hidden": "true",
2284
+ children: "·"
2285
+ }),
2286
+ /* @__PURE__ */ jsx("time", {
2287
+ dateTime: new Date(msg.at).toISOString(),
2288
+ children: shortTime(msg.at)
2289
+ }),
2290
+ trace && /* @__PURE__ */ jsx("span", {
2291
+ className: "vadm-bubble-dot",
2292
+ "data-outcome": trace.outcome,
2293
+ "aria-hidden": "true"
2294
+ })
2295
+ ]
2296
+ })
2297
+ ]
2298
+ }), open && trace && /* @__PURE__ */ jsx("div", {
2299
+ className: "vadm-bubble-detail",
2300
+ children: /* @__PURE__ */ jsx(TraceRouting, {
2301
+ trace,
2302
+ steps: steps.get(trace.id) ?? []
2303
+ })
2304
+ })]
2305
+ }, msg.id);
2306
+ })]
2307
+ }) : traces === null ? /* @__PURE__ */ jsx(BrainTraceSkeleton, {}) : senders.length === 0 ? /* @__PURE__ */ jsxs("div", {
2308
+ className: "vadm-empty",
2309
+ children: [
2310
+ /* @__PURE__ */ jsx("strong", {
2311
+ style: {
2312
+ display: "block",
2313
+ color: "var(--foreground)",
2314
+ marginBottom: 5
2315
+ },
2316
+ children: "No conversations yet"
2317
+ }),
2318
+ "No inbound iMessage has arrived since capture started. Send the line a message, then refresh.",
2319
+ capture?.gatewayStartedAt && /* @__PURE__ */ jsxs("span", {
2320
+ style: {
2321
+ display: "block",
2322
+ marginTop: 7,
2323
+ fontSize: 12
2324
+ },
2325
+ children: ["Gateway online since ", new Date(capture.gatewayStartedAt).toLocaleString()]
2326
+ })
2327
+ ]
2328
+ }) : null
2329
+ ] });
2330
+ }
2331
+ /** The routing story for one message: the flow graph + error + detail grid.
2332
+ * Rendered inline when a message row is expanded. */
2333
+ /** Kind → label + accent. Rules read as ✓/· in their own name (set by the
2334
+ * gateway), so the badge only needs to name the category. */
2335
+ const STEP_KIND_LABEL = {
2336
+ rule: "rule",
2337
+ context: "context",
2338
+ prompt: "prompt",
2339
+ "llm-text": "model",
2340
+ "tool-call": "tool →",
2341
+ "tool-result": "← result",
2342
+ effect: "effect"
2343
+ };
2344
+ /**
2345
+ * The turn, step by step: which deterministic rules were evaluated (and which
2346
+ * declined), the prompt blocks the model was given, every tool call with its
2347
+ * arguments, every result, and the effects committed.
2348
+ *
2349
+ * This is the panel's answer to the operator complaint that motivated it —
2350
+ * "we don't actually see the actual routing logic … what tool calling is
2351
+ * happening behind the scene" (2026-07-25). Before this, the only thing
2352
+ * recorded about a model turn was its duration.
2353
+ *
2354
+ * Long payloads (a prompt snapshot is thousands of chars) collapse to a
2355
+ * summary line and expand on click, so the common case stays scannable.
2356
+ */
2357
+ function TraceSteps({ steps }) {
2358
+ const [openSeq, setOpenSeq] = useState(null);
2359
+ if (!steps.length) return null;
2360
+ const t0 = steps[0].at;
2361
+ return /* @__PURE__ */ jsxs("div", {
2362
+ style: {
2363
+ marginTop: 12,
2364
+ paddingTop: 10,
2365
+ borderTop: "1px dashed var(--vadm-border)"
2366
+ },
2367
+ children: [/* @__PURE__ */ jsxs("div", {
2368
+ style: {
2369
+ fontSize: 11,
2370
+ opacity: .6,
2371
+ marginBottom: 6
2372
+ },
2373
+ children: [
2374
+ steps.length,
2375
+ " step",
2376
+ steps.length === 1 ? "" : "s",
2377
+ " — tap one to see its payload"
2378
+ ]
2379
+ }), steps.map((step) => {
2380
+ const open = openSeq === step.seq;
2381
+ const detail = step.detail ?? "";
2382
+ const inline = step.kind === "rule" || detail.length <= 120;
2383
+ const firstLine = detail.split("\n")[0] ?? "";
2384
+ return /* @__PURE__ */ jsxs("div", {
2385
+ style: {
2386
+ borderTop: "1px solid var(--vadm-border)",
2387
+ padding: "5px 0"
2388
+ },
2389
+ children: [/* @__PURE__ */ jsxs("button", {
2390
+ type: "button",
2391
+ onClick: () => setOpenSeq(open ? null : step.seq),
2392
+ style: {
2393
+ display: "flex",
2394
+ gap: 8,
2395
+ alignItems: "baseline",
2396
+ width: "100%",
2397
+ background: "none",
2398
+ border: "none",
2399
+ padding: 0,
2400
+ textAlign: "left",
2401
+ cursor: detail ? "pointer" : "default",
2402
+ color: "inherit",
2403
+ font: "inherit"
2404
+ },
2405
+ children: [
2406
+ /* @__PURE__ */ jsxs("code", {
2407
+ style: {
2408
+ fontSize: 10,
2409
+ opacity: .5,
2410
+ minWidth: 44
2411
+ },
2412
+ children: ["+", formatMs(step.at - t0)]
2413
+ }),
2414
+ /* @__PURE__ */ jsx("span", {
2415
+ style: {
2416
+ fontSize: 10,
2417
+ opacity: .6,
2418
+ minWidth: 58
2419
+ },
2420
+ children: STEP_KIND_LABEL[step.kind]
2421
+ }),
2422
+ /* @__PURE__ */ jsx("strong", {
2423
+ style: { fontSize: 12 },
2424
+ children: step.name ?? ""
2425
+ }),
2426
+ step.durationMs != null && /* @__PURE__ */ jsx("span", {
2427
+ style: {
2428
+ fontSize: 10,
2429
+ opacity: .5
2430
+ },
2431
+ children: formatMs(step.durationMs)
2432
+ }),
2433
+ !open && !inline && firstLine && /* @__PURE__ */ jsx("span", {
2434
+ style: {
2435
+ fontSize: 11,
2436
+ opacity: .55,
2437
+ overflow: "hidden",
2438
+ whiteSpace: "nowrap",
2439
+ textOverflow: "ellipsis"
2440
+ },
2441
+ children: firstLine
2442
+ })
2443
+ ]
2444
+ }), (open || inline) && detail && /* @__PURE__ */ jsx("pre", {
2445
+ style: {
2446
+ margin: "4px 0 0 52px",
2447
+ fontSize: 11,
2448
+ whiteSpace: "pre-wrap",
2449
+ wordBreak: "break-word",
2450
+ opacity: .8,
2451
+ maxHeight: open ? 420 : void 0,
2452
+ overflow: open ? "auto" : void 0
2453
+ },
2454
+ children: detail
2455
+ })]
2456
+ }, step.seq);
2457
+ })]
2458
+ });
2459
+ }
2460
+ function TraceRouting({ trace, steps }) {
2461
+ const failed = trace.outcome === "failed";
2462
+ const running = trace.outcome === "running";
2463
+ const sessionChanged = trace.activeSessionBefore !== trace.activeSessionAfter;
2464
+ const duration = trace.completedAt ? trace.completedAt - trace.createdAt : null;
2465
+ const targetHref = safeHref(trace.targetSessionUrl);
2466
+ const routed = trace.targetSessionId != null;
2467
+ const [decisionPrefix, decisionVerb] = splitDecision(trace.decision);
2468
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2469
+ /* @__PURE__ */ jsxs("div", {
2470
+ className: "vadm-flow",
2471
+ children: [
2472
+ /* @__PURE__ */ jsx(FlowNode, {
2473
+ variant: "bare",
2474
+ children: /* @__PURE__ */ jsx("span", {
2475
+ className: "vadm-flow-quote",
2476
+ children: trace.text
2477
+ })
2478
+ }),
2479
+ /* @__PURE__ */ jsx(FlowEdge, { label: routeViaLabel(trace.routeVia) }),
2480
+ /* @__PURE__ */ jsxs(FlowNode, {
2481
+ variant: failed ? "danger" : trace.decision.startsWith("computer.") ? "accent" : void 0,
2482
+ sub: trace.reason ? /* @__PURE__ */ jsx("span", {
2483
+ title: trace.reason,
2484
+ children: trace.reason
2485
+ }) : null,
2486
+ children: [decisionPrefix && /* @__PURE__ */ jsx("span", {
2487
+ className: "vadm-flow-pre",
2488
+ children: decisionPrefix
2489
+ }), decisionVerb]
2490
+ }),
2491
+ routed && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(FlowEdge, {
2492
+ label: running ? null : duration != null ? formatMs(duration) : null,
2493
+ danger: failed,
2494
+ running
2495
+ }), /* @__PURE__ */ jsx(FlowNode, {
2496
+ variant: sessionChanged ? "changed" : void 0,
2497
+ mono: true,
2498
+ href: targetHref ?? void 0,
2499
+ sub: sessionChanged ? `was ${shortId(trace.activeSessionBefore)}` : null,
2500
+ children: shortId(trace.targetSessionId)
2501
+ })] })
2502
+ ]
2503
+ }),
2504
+ trace.error && /* @__PURE__ */ jsx("div", {
2505
+ className: "vadm-err vadm-flow-error",
2506
+ children: trace.error
2507
+ }),
2508
+ /* @__PURE__ */ jsxs("div", {
2509
+ className: "vadm-kv",
2510
+ style: {
2511
+ marginTop: 12,
2512
+ paddingTop: 10,
2513
+ borderTop: "1px dashed var(--vadm-border)"
2514
+ },
2515
+ children: [
2516
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", { children: "Generation" }), /* @__PURE__ */ jsx("strong", { children: trace.generation })] }),
2517
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", { children: "From" }), /* @__PURE__ */ jsx("strong", { children: maskedPhone(trace.phone) })] }),
2518
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", { children: "Reply to" }), /* @__PURE__ */ jsx("strong", { children: /* @__PURE__ */ jsx("code", { children: shortId(trace.replyToMessageId) }) })] }),
2519
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", { children: "Timing" }), /* @__PURE__ */ jsx("strong", { children: timingLine(trace.fleetMs, trace.llmMs, duration) })] }),
2520
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", { children: "Session" }), /* @__PURE__ */ jsx("strong", { children: /* @__PURE__ */ jsxs("code", { children: [
2521
+ shortId(trace.activeSessionBefore),
2522
+ " → ",
2523
+ shortId(trace.activeSessionAfter)
2524
+ ] }) })] }),
2525
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", { children: "Message" }), /* @__PURE__ */ jsx("strong", { children: trace.text })] }),
2526
+ trace.reason && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", { children: "Reason" }), /* @__PURE__ */ jsx("strong", { children: trace.reason })] })
2527
+ ]
2528
+ }),
2529
+ /* @__PURE__ */ jsx(TraceSteps, { steps })
2530
+ ] });
2531
+ }
1553
2532
  const DEFAULT_PANELS = [
2533
+ {
2534
+ id: "brain",
2535
+ label: "Chat Brain",
2536
+ render: (c) => /* @__PURE__ */ jsx(BrainRoutingPanel, { client: c })
2537
+ },
1554
2538
  {
1555
2539
  id: "flags",
1556
2540
  label: "Flags",
@@ -1600,4 +2584,4 @@ function AdminConsole({ client, config, panels = DEFAULT_PANELS }) {
1600
2584
  });
1601
2585
  }
1602
2586
  //#endregion
1603
- export { UsersPanel as a, STYLE_ID as c, ReportsPanel as i, ensureStyles as l, FlagsPanel as n, AdminClient as o, PricingPanel as r, AdminError as s, AdminConsole as t };
2587
+ export { ReportsPanel as a, AdminError as c, PricingPanel as i, STYLE_ID as l, BrainRoutingPanel as n, UsersPanel as o, FlagsPanel as r, AdminClient as s, AdminConsole as t, ensureStyles as u };