@solongate/proxy 0.61.0 → 0.63.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/dist/tui/index.js CHANGED
@@ -9,7 +9,7 @@ import { render } from "ink";
9
9
 
10
10
  // src/tui/App.tsx
11
11
  import { Box as Box8, Text as Text8, useApp, useInput as useInput5 } from "ink";
12
- import { useState as useState6 } from "react";
12
+ import { useState as useState7 } from "react";
13
13
 
14
14
  // src/cli-utils.ts
15
15
  var c = {
@@ -124,9 +124,9 @@ function KeyHints({ hints }) {
124
124
  ] }, i)) });
125
125
  }
126
126
 
127
- // src/tui/panels/Policies.tsx
128
- import { Box as Box2, Text as Text2, useInput } from "ink";
129
- import { useEffect as useEffect2, useState as useState2 } from "react";
127
+ // src/tui/panels/Live.tsx
128
+ import { Box as Box2, Text as Text2 } from "ink";
129
+ import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
130
130
 
131
131
  // src/api-client/client.ts
132
132
  import { readFileSync, existsSync } from "fs";
@@ -429,45 +429,244 @@ function usePoll(reload, intervalMs, enabled = true) {
429
429
  }, [reload, intervalMs, enabled]);
430
430
  }
431
431
 
432
- // src/tui/panels/Policies.tsx
432
+ // src/tui/panels/Live.tsx
433
433
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
434
+ var SPIN = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
435
+ var hhmmss = (ts) => {
436
+ const d = new Date(ts);
437
+ return Number.isNaN(d.getTime()) ? "--:--:--" : d.toTimeString().slice(0, 8);
438
+ };
439
+ var fmtUp = (ms) => {
440
+ const s = Math.floor(ms / 1e3);
441
+ const p = (n) => String(n).padStart(2, "0");
442
+ return `${p(Math.floor(s / 3600))}:${p(Math.floor(s % 3600 / 60))}:${p(s % 60)}`;
443
+ };
444
+ function LivePanel({ active: active2 }) {
445
+ const stats = useLoader(() => api.stats.get());
446
+ const ts = useLoader(() => api.stats.timeseries({ period: "24h" }));
447
+ const feed = useLoader(() => api.audit.list({ limit: 40 }));
448
+ usePoll(() => {
449
+ stats.reload();
450
+ ts.reload();
451
+ feed.reload();
452
+ }, 2e3, active2);
453
+ const [tick, setTick] = useState2(0);
454
+ useEffect2(() => {
455
+ if (!active2) return;
456
+ const t = setInterval(() => setTick((n) => n + 1), 200);
457
+ return () => clearInterval(t);
458
+ }, [active2]);
459
+ const startRef = useRef2(Date.now());
460
+ const [buffer, setBuffer] = useState2([]);
461
+ useEffect2(() => {
462
+ const incoming = feed.data?.entries;
463
+ if (!incoming?.length) return;
464
+ setBuffer((prev) => {
465
+ const seen = new Set(prev.map((e) => e.id));
466
+ const fresh = incoming.filter((e) => !seen.has(e.id)).sort((a, b) => Date.parse(a.created_at) - Date.parse(b.created_at));
467
+ return fresh.length ? [...prev, ...fresh].slice(-300) : prev;
468
+ });
469
+ }, [feed.data]);
470
+ const s = stats.data;
471
+ const points = ts.data?.timeseries ?? [];
472
+ const cols = process.stdout.columns ?? 100;
473
+ const rows = process.stdout.rows ?? 32;
474
+ const feedRows = Math.max(8, Math.min(20, rows - 18));
475
+ const wide = cols >= 112;
476
+ const tail = buffer.slice(-feedRows);
477
+ const denies = buffer.filter((e) => e.decision !== "ALLOW");
478
+ const dlpHits = buffer.filter((e) => e.dlp_matches?.length);
479
+ const lastDeny = denies[denies.length - 1];
480
+ const spin = SPIN[tick % SPIN.length];
481
+ const cursor = tick % 4 < 2 ? "\u258C" : " ";
482
+ if (stats.error) return /* @__PURE__ */ jsxs2(Text2, { color: theme.bad, children: [
483
+ "\u2717 ",
484
+ stats.error
485
+ ] });
486
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
487
+ /* @__PURE__ */ jsxs2(Box2, { children: [
488
+ /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "\u25CF " }),
489
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: theme.accentBright, children: "LIVE" }),
490
+ /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
491
+ " ",
492
+ spin,
493
+ " scanning"
494
+ ] }),
495
+ /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
496
+ " up ",
497
+ fmtUp(Date.now() - startRef.current)
498
+ ] }),
499
+ /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
500
+ " ",
501
+ hhmmss(Date.now())
502
+ ] }),
503
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " poll 2s" })
504
+ ] }),
505
+ /* @__PURE__ */ jsxs2(Box2, { children: [
506
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "calls " }),
507
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: s ? s.total_calls : "\xB7\xB7\xB7\xB7" }),
508
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " allow " }),
509
+ /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: s ? s.allowed : "\xB7\xB7\xB7" }),
510
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " deny " }),
511
+ /* @__PURE__ */ jsx2(Text2, { color: theme.bad, bold: true, children: s ? s.denied : "\xB7\xB7" }),
512
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " dlp(buf) " }),
513
+ /* @__PURE__ */ jsx2(Text2, { color: dlpHits.length ? theme.bad : theme.dim, children: dlpHits.length }),
514
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " policies " }),
515
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: s ? s.active_policies : "\xB7" })
516
+ ] }),
517
+ /* @__PURE__ */ jsxs2(Box2, { children: [
518
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "act " }),
519
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: sparkline(points.map((p) => p.total), Math.min(64, cols - 30)) })
520
+ ] }),
521
+ /* @__PURE__ */ jsxs2(Box2, { children: [
522
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "dny " }),
523
+ /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: sparkline(points.map((p) => p.denied), Math.min(64, cols - 30)) })
524
+ ] }),
525
+ /* @__PURE__ */ jsxs2(Box2, { children: [
526
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ">> " }),
527
+ lastDeny ? /* @__PURE__ */ jsxs2(Text2, { color: theme.bad, children: [
528
+ "last deny ",
529
+ hhmmss(lastDeny.created_at),
530
+ " ",
531
+ lastDeny.tool_name,
532
+ " \xB7 ",
533
+ truncate(lastDeny.reason ?? "policy", Math.max(20, cols - 50))
534
+ ] }) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "no denials in buffer" })
535
+ ] }),
536
+ /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
537
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", flexGrow: 3, borderStyle: "single", borderColor: "gray", paddingX: 1, children: [
538
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, bold: true, children: "\u2500\u2500 tool stream \u2500\u2500" }),
539
+ tail.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "awaiting traffic\u2026" }) : null,
540
+ tail.map((e) => /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
541
+ /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
542
+ "[",
543
+ hhmmss(e.created_at),
544
+ "] "
545
+ ] }),
546
+ /* @__PURE__ */ jsx2(Text2, { color: decisionColor(e.decision), bold: e.decision !== "ALLOW", children: e.decision.padEnd(6) }),
547
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate(e.tool_name, 14).padEnd(15) }),
548
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: (e.permission ?? "").slice(0, 4).padEnd(5) }),
549
+ e.dlp_matches?.length ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "DLP! " }) : null,
550
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: (e.arguments_summary ? JSON.stringify(e.arguments_summary) : e.reason ?? "").replace(/\s+/g, " ") })
551
+ ] }, e.id))
552
+ ] }),
553
+ wide ? /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: 34, borderStyle: "single", borderColor: "gray", paddingX: 1, children: [
554
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, bold: true, children: "\u2500\u2500 raw \u2500\u2500" }),
555
+ tail.slice(-Math.floor(feedRows / 2)).flatMap((e) => {
556
+ const raw = e.arguments_summary ? JSON.stringify(e.arguments_summary) : `hash:${e.id.slice(0, 12)}`;
557
+ return [
558
+ /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", color: e.decision === "ALLOW" ? theme.dim : theme.bad, children: [
559
+ e.id.slice(0, 8),
560
+ " ",
561
+ e.tool_name.slice(0, 12)
562
+ ] }, e.id + ":h"),
563
+ /* @__PURE__ */ jsx2(Text2, { wrap: "truncate", color: theme.dim, children: " " + raw.replace(/\s+/g, " ") }, e.id + ":b")
564
+ ];
565
+ })
566
+ ] }) : null
567
+ ] }),
568
+ /* @__PURE__ */ jsxs2(Box2, { children: [
569
+ /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "root@solongate" }),
570
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ":" }),
571
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, children: "~/live" }),
572
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "$ " }),
573
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, children: cursor })
574
+ ] })
575
+ ] });
576
+ }
577
+
578
+ // src/tui/panels/Policies.tsx
579
+ import { Box as Box3, Text as Text3, useInput } from "ink";
580
+ import TextInput from "ink-text-input";
581
+ import { useEffect as useEffect3, useState as useState3 } from "react";
582
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
583
+ var constr = (r, g, side) => (r[g]?.[side] ?? []).join(", ");
584
+ var setConstr = (r, g, side, val) => {
585
+ const arr = val.split(",").map((s) => s.trim()).filter(Boolean);
586
+ const prev = r[g] ?? {};
587
+ return { ...r, [g]: { ...prev, [side]: arr } };
588
+ };
589
+ var FIELDS = [
590
+ { label: "Effect", kind: "effect", get: (r) => r.effect },
591
+ { label: "Enabled", kind: "enabled", get: (r) => r.enabled ? "yes" : "no" },
592
+ { label: "Priority", kind: "priority", get: (r) => String(r.priority) },
593
+ { label: "Tool pattern", kind: "text", get: (r) => r.toolPattern, set: (r, v) => ({ ...r, toolPattern: v || "*" }) },
594
+ { label: "Description", kind: "text", get: (r) => r.description ?? "", set: (r, v) => ({ ...r, description: v }) },
595
+ { label: "Cmd allow", kind: "text", get: (r) => constr(r, "commandConstraints", "allowed"), set: (r, v) => setConstr(r, "commandConstraints", "allowed", v) },
596
+ { label: "Cmd deny", kind: "text", get: (r) => constr(r, "commandConstraints", "denied"), set: (r, v) => setConstr(r, "commandConstraints", "denied", v) },
597
+ { label: "Path allow", kind: "text", get: (r) => constr(r, "pathConstraints", "allowed"), set: (r, v) => setConstr(r, "pathConstraints", "allowed", v) },
598
+ { label: "Path deny", kind: "text", get: (r) => constr(r, "pathConstraints", "denied"), set: (r, v) => setConstr(r, "pathConstraints", "denied", v) },
599
+ { label: "File allow", kind: "text", get: (r) => constr(r, "filenameConstraints", "allowed"), set: (r, v) => setConstr(r, "filenameConstraints", "allowed", v) },
600
+ { label: "File deny", kind: "text", get: (r) => constr(r, "filenameConstraints", "denied"), set: (r, v) => setConstr(r, "filenameConstraints", "denied", v) },
601
+ { label: "URL allow", kind: "text", get: (r) => constr(r, "urlConstraints", "allowed"), set: (r, v) => setConstr(r, "urlConstraints", "allowed", v) },
602
+ { label: "URL deny", kind: "text", get: (r) => constr(r, "urlConstraints", "denied"), set: (r, v) => setConstr(r, "urlConstraints", "denied", v) }
603
+ ];
604
+ function ruleSummary(r) {
605
+ const bits = [];
606
+ for (const [g, tag] of [["commandConstraints", "cmd"], ["pathConstraints", "path"], ["filenameConstraints", "file"], ["urlConstraints", "url"]]) {
607
+ const c2 = r[g];
608
+ const n = (c2?.allowed?.length ?? 0) + (c2?.denied?.length ?? 0);
609
+ if (n) bits.push(`${tag}:${n}`);
610
+ }
611
+ return bits.join(" ");
612
+ }
434
613
  function PoliciesPanel({ focused }) {
435
614
  const list3 = useLoader(() => api.policies.list());
436
615
  const policies = list3.data?.policies ?? [];
437
- const [view, setView] = useState2("list");
438
- const [pi, setPi] = useState2(0);
439
- const [ri, setRi] = useState2(0);
440
- const [rules, setRules] = useState2(null);
441
- const [mode, setMode] = useState2("denylist");
442
- const [status, setStatus] = useState2(null);
616
+ const [view, setView] = useState3("list");
617
+ const [pi, setPi] = useState3(0);
618
+ const [ri, setRi] = useState3(0);
619
+ const [fi, setFi] = useState3(0);
620
+ const [rules, setRules] = useState3([]);
621
+ const [mode, setMode] = useState3("denylist");
622
+ const [dirty, setDirty] = useState3(false);
623
+ const [editing, setEditing] = useState3(false);
624
+ const [editVal, setEditVal] = useState3("");
625
+ const [status, setStatus] = useState3(null);
443
626
  const selected = policies[Math.min(pi, Math.max(0, policies.length - 1))];
444
627
  const detail = useLoader(() => selected ? api.policies.get(selected.id) : Promise.resolve(null), [selected?.id]);
445
- useEffect2(() => {
628
+ useEffect3(() => {
446
629
  if (detail.data) {
447
630
  setRules(detail.data.rules);
448
631
  setMode(detail.data.mode ?? "denylist");
632
+ setDirty(false);
449
633
  setRi(0);
450
634
  }
451
635
  }, [detail.data]);
452
- const persist = async (nextRules, nextMode) => {
453
- if (!detail.data || !selected) return;
636
+ const mutate = (nextRules, nextMode = mode) => {
637
+ setRules(nextRules);
638
+ setMode(nextMode);
639
+ setDirty(true);
640
+ setStatus(null);
641
+ };
642
+ const save = async () => {
643
+ if (!selected || !detail.data) return;
454
644
  setStatus("Saving\u2026");
455
645
  try {
456
646
  const res = await api.policies.update(selected.id, {
457
647
  id: selected.id,
458
648
  name: detail.data.name,
459
649
  description: detail.data.description,
460
- mode: nextMode,
650
+ mode,
461
651
  agents: detail.data.agents,
462
- rules: nextRules
652
+ rules
463
653
  });
464
654
  setRules(res.rules);
655
+ setDirty(false);
465
656
  setStatus("\u2713 Saved v" + res._version);
466
657
  list3.reload();
467
658
  } catch (e) {
468
659
  setStatus("\u2717 " + (e instanceof Error ? e.message : String(e)));
469
660
  }
470
661
  };
662
+ const discard = () => {
663
+ if (detail.data) {
664
+ setRules(detail.data.rules);
665
+ setMode(detail.data.mode ?? "denylist");
666
+ }
667
+ setDirty(false);
668
+ setStatus("discarded");
669
+ };
471
670
  useInput(
472
671
  (input, key) => {
473
672
  if (view === "list") {
@@ -475,44 +674,61 @@ function PoliciesPanel({ focused }) {
475
674
  else if (key.downArrow) setPi((n) => Math.min(policies.length - 1, n + 1));
476
675
  else if (key.return || key.rightArrow) {
477
676
  setStatus(null);
478
- setView("detail");
677
+ setView("rules");
479
678
  }
480
679
  return;
481
680
  }
482
- const rs2 = rules ?? [];
483
- if (key.leftArrow) {
484
- setView("list");
485
- setStatus(null);
486
- } else if (key.upArrow) setRi((n) => Math.max(0, n - 1));
487
- else if (key.downArrow) setRi((n) => Math.min(rs2.length - 1, n + 1));
488
- else if (input === " " || input === "e") {
489
- if (!rs2[ri]) return;
490
- const next = rs2.map((r, i) => i === ri ? { ...r, enabled: !r.enabled } : r);
491
- setRules(next);
492
- void persist(next, mode);
493
- } else if (input === "d") {
494
- const target = rs2[ri];
495
- if (!target || !selected) return;
496
- setStatus("Deleting\u2026");
497
- void api.policies.revokeRule(selected.id, target.id).then(() => {
498
- setStatus("\u2713 Deleted");
499
- detail.reload();
500
- list3.reload();
501
- }).catch((e) => setStatus("\u2717 " + (e instanceof Error ? e.message : String(e))));
502
- } else if (input === "m") {
503
- const nm = mode === "denylist" ? "whitelist" : "denylist";
504
- setMode(nm);
505
- void persist(rs2, nm);
681
+ if (view === "rules") {
682
+ if (key.leftArrow) return setView("list"), void setStatus(null);
683
+ if (key.upArrow) setRi((n) => Math.max(0, n - 1));
684
+ else if (key.downArrow) setRi((n) => Math.min(rules.length - 1, n + 1));
685
+ else if (key.return || key.rightArrow) {
686
+ if (rules[ri]) {
687
+ setFi(0);
688
+ setView("rule");
689
+ }
690
+ } else if (input === " ") {
691
+ if (rules[ri]) mutate(rules.map((r, i) => i === ri ? { ...r, enabled: !r.enabled } : r));
692
+ } else if (input === "e") {
693
+ if (rules[ri]) mutate(rules.map((r, i) => i === ri ? { ...r, effect: r.effect === "ALLOW" ? "DENY" : "ALLOW" } : r));
694
+ } else if (input === "d") {
695
+ if (rules[ri]) {
696
+ const next = rules.filter((_, i) => i !== ri);
697
+ setRi((n) => Math.max(0, Math.min(n, next.length - 1)));
698
+ mutate(next);
699
+ }
700
+ } else if (input === "m") {
701
+ mutate(rules, mode === "denylist" ? "whitelist" : "denylist");
702
+ } else if (input === "s") void save();
703
+ else if (input === "x") discard();
704
+ return;
506
705
  }
706
+ const rule2 = rules[ri];
707
+ if (!rule2) return setView("rules");
708
+ const field = FIELDS[fi];
709
+ if (key.leftArrow) setView("rules");
710
+ else if (key.upArrow) setFi((n) => Math.max(0, n - 1));
711
+ else if (key.downArrow) setFi((n) => Math.min(FIELDS.length - 1, n + 1));
712
+ else if (field.kind === "effect" && (input === " " || key.rightArrow)) {
713
+ mutate(rules.map((r, i) => i === ri ? { ...r, effect: r.effect === "ALLOW" ? "DENY" : "ALLOW" } : r));
714
+ } else if (field.kind === "enabled" && (input === " " || key.rightArrow)) {
715
+ mutate(rules.map((r, i) => i === ri ? { ...r, enabled: !r.enabled } : r));
716
+ } else if (field.kind === "priority" && (key.rightArrow || key.leftArrow)) {
717
+ const d = key.rightArrow ? 1 : -1;
718
+ mutate(rules.map((r, i) => i === ri ? { ...r, priority: Math.max(0, r.priority + d) } : r));
719
+ } else if (field.kind === "text" && key.return) {
720
+ setEditVal(field.get(rule2));
721
+ setEditing(true);
722
+ } else if (input === "s") void save();
507
723
  },
508
- { isActive: focused }
724
+ { isActive: focused && !editing }
509
725
  );
510
726
  if (view === "list") {
511
- return /* @__PURE__ */ jsx2(DataView, { loading: list3.loading && !list3.data, error: list3.error, empty: !!list3.data && policies.length === 0, emptyText: "No policies.", children: /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
512
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: focused ? "\u2191\u2193 select \xB7 enter open" : "press \u2192 to browse" }),
513
- /* @__PURE__ */ jsx2(Box2, { marginTop: 1, flexDirection: "column", children: policies.map((p, i) => /* @__PURE__ */ jsxs2(Text2, { color: i === pi ? theme.accentBright : void 0, bold: i === pi, children: [
727
+ return /* @__PURE__ */ jsx3(DataView, { loading: list3.loading && !list3.data, error: list3.error, empty: !!list3.data && policies.length === 0, emptyText: "No policies.", children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
728
+ /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: focused ? "\u2191\u2193 select \xB7 enter open" : "press \u2192 to browse" }),
729
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 1, flexDirection: "column", children: policies.map((p, i) => /* @__PURE__ */ jsxs3(Text3, { color: i === pi ? theme.accentBright : void 0, bold: i === pi, children: [
514
730
  (i === pi ? "\u25B8 " : " ") + truncate(p.name, 26).padEnd(27),
515
- /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
731
+ /* @__PURE__ */ jsxs3(Text3, { color: theme.dim, children: [
516
732
  p.mode.padEnd(10),
517
733
  " ",
518
734
  p.rules.length,
@@ -522,58 +738,108 @@ function PoliciesPanel({ focused }) {
522
738
  ] }, p.id)) })
523
739
  ] }) });
524
740
  }
525
- const rs = rules ?? [];
526
- return /* @__PURE__ */ jsx2(DataView, { loading: detail.loading && !rules, error: detail.error, children: /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
527
- /* @__PURE__ */ jsxs2(Box2, { children: [
528
- /* @__PURE__ */ jsx2(Text2, { bold: true, color: theme.accentBright, children: truncate(selected?.name ?? "", 30) }),
529
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " mode: " }),
530
- /* @__PURE__ */ jsx2(Text2, { color: mode === "whitelist" ? theme.ok : void 0, children: mode })
741
+ const dirtyTag = dirty ? /* @__PURE__ */ jsx3(Text3, { color: theme.warn, children: " \u25CF unsaved (s save \xB7 x discard)" }) : null;
742
+ if (view === "rules") {
743
+ return /* @__PURE__ */ jsx3(DataView, { loading: detail.loading && !detail.data, error: detail.error, children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
744
+ /* @__PURE__ */ jsxs3(Box3, { children: [
745
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: theme.accentBright, children: truncate(selected?.name ?? "", 28) }),
746
+ /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: " mode: " }),
747
+ /* @__PURE__ */ jsx3(Text3, { color: mode === "whitelist" ? theme.warn : void 0, children: mode }),
748
+ dirtyTag
749
+ ] }),
750
+ /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "\u2191\u2193 rule \xB7 enter edit \xB7 space on/off \xB7 e effect \xB7 d delete \xB7 m mode \xB7 s save \xB7 \u2190 back" }),
751
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(
752
+ Table,
753
+ {
754
+ columns: [
755
+ { header: "", width: 2 },
756
+ { header: "ON", width: 2 },
757
+ { header: "EFFECT", width: 7 },
758
+ { header: "PRIO", width: 4 },
759
+ { header: "TOOL", width: 20 },
760
+ { header: "CONSTRAINTS", width: 22 }
761
+ ],
762
+ rows: rules.map((r, i) => [
763
+ { value: i === ri ? "\u25B8" : "", color: theme.accentBright },
764
+ { value: r.enabled ? "\u25CF" : "\u25CB", color: r.enabled ? theme.ok : theme.dim },
765
+ { value: r.effect, color: r.effect === "ALLOW" ? theme.ok : theme.bad, dim: !r.enabled },
766
+ { value: String(r.priority), dim: true },
767
+ { value: truncate(r.toolPattern, 20), color: theme.accent, dim: !r.enabled },
768
+ { value: ruleSummary(r) || truncate(r.description || "\u2014", 22), dim: true }
769
+ ])
770
+ }
771
+ ) }),
772
+ rules.length === 0 ? /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "(no rules)" }) : null,
773
+ status ? /* @__PURE__ */ jsx3(Text3, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) : null
774
+ ] }) });
775
+ }
776
+ const rule = rules[ri];
777
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
778
+ /* @__PURE__ */ jsxs3(Box3, { children: [
779
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: theme.accentBright, children: "Rule " }),
780
+ /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: truncate(rule?.id ?? "", 26) }),
781
+ dirtyTag
531
782
  ] }),
532
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "\u2191\u2193 rule \xB7 space toggle \xB7 d delete \xB7 m mode \xB7 \u2190 back" }),
533
- /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(
534
- Table,
535
- {
536
- columns: [
537
- { header: "", width: 2 },
538
- { header: "EFFECT", width: 7 },
539
- { header: "PRIO", width: 4 },
540
- { header: "TOOL", width: 20 },
541
- { header: "DESCRIPTION", width: 34 }
542
- ],
543
- rows: rs.map((r, i) => [
544
- { value: i === ri ? "\u25B8" : "", color: theme.accentBright },
545
- { value: r.effect, color: r.effect === "ALLOW" ? theme.ok : theme.bad, dim: !r.enabled },
546
- { value: String(r.priority), dim: true },
547
- { value: (r.enabled ? "" : "\u2298 ") + truncate(r.toolPattern, 18), color: theme.accent, dim: !r.enabled },
548
- { value: truncate(r.description || "\u2014", 34), dim: !r.enabled }
549
- ])
550
- }
551
- ) }),
552
- rs.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "(no rules)" }) : null,
553
- status ? /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) }) : null
554
- ] }) });
783
+ /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: "\u2191\u2193 field \xB7 enter edit text \xB7 space/\u2192 toggle \xB7 s save \xB7 \u2190 back" }),
784
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 1, flexDirection: "column", children: FIELDS.map((f, i) => {
785
+ const val = rule ? f.get(rule) : "";
786
+ const active2 = i === fi;
787
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
788
+ /* @__PURE__ */ jsx3(Text3, { color: active2 ? theme.accentBright : void 0, children: (active2 ? "\u25B8 " : " ") + f.label.padEnd(13) }),
789
+ active2 && editing && f.kind === "text" ? /* @__PURE__ */ jsx3(
790
+ TextInput,
791
+ {
792
+ value: editVal,
793
+ onChange: setEditVal,
794
+ onSubmit: (v) => {
795
+ if (rule && f.set) mutate(rules.map((r, idx) => idx === ri ? f.set(r, v) : r));
796
+ setEditing(false);
797
+ }
798
+ }
799
+ ) : /* @__PURE__ */ jsx3(
800
+ Text3,
801
+ {
802
+ color: f.kind === "effect" ? val === "ALLOW" ? theme.ok : theme.bad : void 0,
803
+ dimColor: !val,
804
+ children: val || "\u2014"
805
+ }
806
+ )
807
+ ] }, f.label);
808
+ }) }),
809
+ status ? /* @__PURE__ */ jsx3(Text3, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) : null
810
+ ] });
555
811
  }
556
812
 
557
813
  // src/tui/panels/RateLimit.tsx
558
- import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
559
- import { useEffect as useEffect3, useState as useState3 } from "react";
560
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
814
+ import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
815
+ import { useEffect as useEffect4, useState as useState4 } from "react";
816
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
561
817
  var MODES = ["off", "detect", "block"];
562
- var FIELDS = ["mode", "perMinute", "perHour", "perDay"];
818
+ var FIELDS2 = ["mode", "perMinute", "perHour", "perDay"];
819
+ var BLOCKS2 = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
820
+ function ColoredSpark({ values, limit, width = 60 }) {
821
+ const v = values.length > width ? values.slice(values.length - width) : values;
822
+ const max = Math.max(1, ...v);
823
+ return /* @__PURE__ */ jsx4(Text4, { children: v.map((n, i) => {
824
+ const ch = BLOCKS2[Math.min(BLOCKS2.length - 1, Math.round(n / max * (BLOCKS2.length - 1)))];
825
+ const over = limit > 0 && n > limit;
826
+ return /* @__PURE__ */ jsx4(Text4, { color: over ? theme.bad : theme.accent, children: ch }, i);
827
+ }) });
828
+ }
563
829
  function RateLimitPanel({ focused }) {
564
830
  const layersQ = useLoader(() => api.settings.getSecurityLayers());
565
831
  const historyQ = useLoader(() => api.settings.getRateLimitHistory());
566
832
  const insightsQ = useLoader(() => api.stats.securityInsights(7));
567
- const [draft, setDraft] = useState3(null);
568
- const [dirty, setDirty] = useState3(false);
569
- const [fi, setFi] = useState3(0);
570
- const [status, setStatus] = useState3(null);
571
- useEffect3(() => {
833
+ const [draft, setDraft] = useState4(null);
834
+ const [dirty, setDirty] = useState4(false);
835
+ const [fi, setFi] = useState4(0);
836
+ const [status, setStatus] = useState4(null);
837
+ useEffect4(() => {
572
838
  if (layersQ.data && !draft) setDraft({ ...layersQ.data.layers.rateLimit });
573
839
  }, [layersQ.data, draft]);
574
840
  const adjust = (dir, step) => {
575
841
  if (!draft) return;
576
- const field = FIELDS[fi];
842
+ const field = FIELDS2[fi];
577
843
  if (field === "mode") {
578
844
  const idx = (MODES.indexOf(draft.mode) + dir + MODES.length) % MODES.length;
579
845
  setDraft({ ...draft, mode: MODES[idx] });
@@ -587,8 +853,7 @@ function RateLimitPanel({ focused }) {
587
853
  if (!draft || !layersQ.data) return;
588
854
  setStatus("Saving\u2026");
589
855
  try {
590
- const next = { ...layersQ.data.layers, rateLimit: draft };
591
- const res = await api.settings.setSecurityLayers(next);
856
+ const res = await api.settings.setSecurityLayers({ ...layersQ.data.layers, rateLimit: draft });
592
857
  setDraft({ ...res.layers.rateLimit });
593
858
  setDirty(false);
594
859
  setStatus("\u2713 Saved");
@@ -600,8 +865,8 @@ function RateLimitPanel({ focused }) {
600
865
  useInput2(
601
866
  (input, key) => {
602
867
  const step = key.shift ? 10 : 1;
603
- if (key.upArrow) setFi((n) => (n - 1 + FIELDS.length) % FIELDS.length);
604
- else if (key.downArrow) setFi((n) => (n + 1) % FIELDS.length);
868
+ if (key.upArrow) setFi((n) => (n - 1 + FIELDS2.length) % FIELDS2.length);
869
+ else if (key.downArrow) setFi((n) => (n + 1) % FIELDS2.length);
605
870
  else if (key.leftArrow) adjust(-1, step);
606
871
  else if (key.rightArrow) adjust(1, step);
607
872
  else if (input === "s") void save();
@@ -609,155 +874,206 @@ function RateLimitPanel({ focused }) {
609
874
  { isActive: focused }
610
875
  );
611
876
  const history = (historyQ.data?.history ?? []).slice().sort((a, b) => b.ts - a.ts);
612
- const bursts = (insightsQ.data?.["anomalies"] ?? []).slice(0, 6);
613
- return /* @__PURE__ */ jsx3(DataView, { loading: layersQ.loading && !draft, error: layersQ.error, children: draft ? /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
614
- /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: focused ? "\u2191\u2193 field \xB7 \u2190\u2192 \xB11 \xB7 shift+\u2190\u2192 \xB110 \xB7 s save" : "press \u2192 to edit" }),
615
- /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
616
- /* @__PURE__ */ jsx3(FieldRow, { label: "Mode", active: focused && fi === 0, children: /* @__PURE__ */ jsx3(Text3, { color: modeColor(draft.mode), children: draft.mode }) }),
617
- /* @__PURE__ */ jsx3(FieldRow, { label: "Per minute", active: focused && fi === 1, children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: draft.perMinute }) }),
618
- /* @__PURE__ */ jsx3(FieldRow, { label: "Per hour", active: focused && fi === 2, children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: draft.perHour === 0 ? "off" : draft.perHour }) }),
619
- /* @__PURE__ */ jsx3(FieldRow, { label: "Per day", active: focused && fi === 3, children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: draft.perDay === 0 ? "off" : draft.perDay }) })
877
+ const activity = insightsQ.data?.["activity"] ?? {};
878
+ const minuteSeries = (activity.minute ?? []).map((b) => b.count);
879
+ const hourSeries = (activity.hour ?? []).map((b) => b.count);
880
+ const bursts = minuteSeries.filter((c2) => draft && draft.perMinute > 0 && c2 > draft.perMinute).length;
881
+ return /* @__PURE__ */ jsx4(DataView, { loading: layersQ.loading && !draft, error: layersQ.error, children: draft ? /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
882
+ /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: focused ? "\u2191\u2193 field \xB7 \u2190\u2192 \xB11 \xB7 shift+\u2190\u2192 \xB110 \xB7 s save" : "press \u2192 to edit" }),
883
+ /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
884
+ /* @__PURE__ */ jsx4(FieldRow, { label: "Mode", active: focused && fi === 0, children: /* @__PURE__ */ jsx4(Text4, { color: modeColor(draft.mode), children: draft.mode }) }),
885
+ /* @__PURE__ */ jsx4(FieldRow, { label: "Per minute", active: focused && fi === 1, children: /* @__PURE__ */ jsx4(Text4, { bold: true, children: draft.perMinute }) }),
886
+ /* @__PURE__ */ jsx4(FieldRow, { label: "Per hour", active: focused && fi === 2, children: /* @__PURE__ */ jsx4(Text4, { bold: true, children: draft.perHour === 0 ? "off" : draft.perHour }) }),
887
+ /* @__PURE__ */ jsx4(FieldRow, { label: "Per day", active: focused && fi === 3, children: /* @__PURE__ */ jsx4(Text4, { bold: true, children: draft.perDay === 0 ? "off" : draft.perDay }) })
620
888
  ] }),
621
- dirty || status ? /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, children: [
622
- dirty ? /* @__PURE__ */ jsx3(Text3, { color: theme.warn, children: "\u25CF unsaved \u2014 press s to apply " }) : null,
623
- status ? /* @__PURE__ */ jsx3(Text3, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) : null
889
+ dirty || status ? /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, children: [
890
+ dirty ? /* @__PURE__ */ jsx4(Text4, { color: theme.warn, children: "\u25CF unsaved \u2014 press s to apply " }) : null,
891
+ status ? /* @__PURE__ */ jsx4(Text4, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) : null
624
892
  ] }) : null,
625
- /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
626
- /* @__PURE__ */ jsxs3(Text3, { color: theme.dim, children: [
893
+ /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
894
+ /* @__PURE__ */ jsxs4(Box4, { children: [
895
+ /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: "Activity " }),
896
+ bursts > 0 ? /* @__PURE__ */ jsxs4(Text4, { color: theme.bad, children: [
897
+ bursts,
898
+ " burst min"
899
+ ] }) : /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: "no bursts" }),
900
+ insightsQ.loading && !insightsQ.data ? /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: " \u2026" }) : null
901
+ ] }),
902
+ /* @__PURE__ */ jsxs4(Box4, { children: [
903
+ /* @__PURE__ */ jsx4(Text4, { children: "/min " }),
904
+ minuteSeries.length ? /* @__PURE__ */ jsx4(ColoredSpark, { values: minuteSeries, limit: draft.perMinute }) : /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: "no data" })
905
+ ] }),
906
+ /* @__PURE__ */ jsxs4(Box4, { children: [
907
+ /* @__PURE__ */ jsx4(Text4, { children: "/hour " }),
908
+ hourSeries.length ? /* @__PURE__ */ jsx4(ColoredSpark, { values: hourSeries, limit: draft.perHour, width: 24 }) : /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: "no data" })
909
+ ] })
910
+ ] }),
911
+ /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
912
+ /* @__PURE__ */ jsxs4(Text4, { color: theme.dim, children: [
627
913
  "Change history ",
628
914
  history.length ? `(${history.length})` : ""
629
915
  ] }),
630
- history.length ? /* @__PURE__ */ jsx3(
916
+ history.length ? /* @__PURE__ */ jsx4(
631
917
  Table,
632
918
  {
633
919
  columns: [
634
- { header: "WHEN", width: 8 },
920
+ { header: "WHEN", width: 10 },
635
921
  { header: "/MIN", width: 6 },
636
922
  { header: "/HOUR", width: 7 },
637
923
  { header: "/DAY", width: 6 }
638
924
  ],
639
- rows: history.slice(0, 5).map((h, i) => [
925
+ rows: history.slice(0, 4).map((h, i) => [
640
926
  { value: ago(h.ts) + " ago", dim: true, bold: i === 0 },
641
927
  { value: String(h.minute), bold: i === 0 },
642
928
  { value: h.hour === 0 ? "off" : String(h.hour), dim: h.hour === 0 },
643
929
  { value: h.day === 0 ? "off" : String(h.day), dim: h.day === 0 }
644
930
  ])
645
931
  }
646
- ) : /* @__PURE__ */ jsx3(Text3, { color: theme.dim, children: " no changes" })
647
- ] }),
648
- /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
649
- /* @__PURE__ */ jsxs3(Text3, { color: theme.dim, children: [
650
- "Recent bursts ",
651
- insightsQ.loading && !insightsQ.data ? "\u2026" : `(${bursts.length})`
652
- ] }),
653
- bursts.length ? /* @__PURE__ */ jsx3(
654
- Table,
655
- {
656
- columns: [
657
- { header: "RESULT", width: 9 },
658
- { header: "AGENT", width: 16 },
659
- { header: "CALLS/LIMIT", width: 12 },
660
- { header: "WHEN", width: 8 }
661
- ],
662
- rows: bursts.map((b) => [
663
- { value: b.blocked ? "blocked" : "BYPASSED", color: b.blocked ? theme.accent : theme.bad, bold: !b.blocked },
664
- { value: (b.agent || "\u2014").slice(0, 16), dim: true },
665
- { value: `${b.count}/${b.limit}` },
666
- { value: ago(b.minute) + " ago", dim: true }
667
- ])
668
- }
669
- ) : /* @__PURE__ */ jsxs3(Text3, { color: theme.dim, children: [
670
- " ",
671
- insightsQ.error ? insightsQ.error : "none in 7d"
672
- ] })
932
+ ) : /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: " no changes" })
673
933
  ] })
674
934
  ] }) : null });
675
935
  }
676
936
  function FieldRow({ label, active: active2, children }) {
677
- return /* @__PURE__ */ jsxs3(Box3, { children: [
678
- /* @__PURE__ */ jsx3(Text3, { color: active2 ? theme.accentBright : void 0, children: (active2 ? "\u25B8 " : " ") + label.padEnd(12) }),
937
+ return /* @__PURE__ */ jsxs4(Box4, { children: [
938
+ /* @__PURE__ */ jsx4(Text4, { color: active2 ? theme.accentBright : void 0, children: (active2 ? "\u25B8 " : " ") + label.padEnd(12) }),
679
939
  children
680
940
  ] });
681
941
  }
682
942
 
683
943
  // src/tui/panels/Dlp.tsx
684
- import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
685
- import { useEffect as useEffect4, useState as useState4 } from "react";
686
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
944
+ import { Box as Box5, Text as Text5, useInput as useInput3 } from "ink";
945
+ import TextInput2 from "ink-text-input";
946
+ import { useEffect as useEffect5, useState as useState5 } from "react";
947
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
687
948
  var MODES2 = ["off", "detect", "block"];
688
949
  function DlpPanel({ focused }) {
689
950
  const q = useLoader(() => api.settings.getSecurityLayers());
690
- const [dlp, setDlp] = useState4(null);
691
- const [available, setAvailable] = useState4([]);
692
- const [sel, setSel] = useState4(0);
693
- const [status, setStatus] = useState4(null);
694
- useEffect4(() => {
951
+ const [dlp, setDlp] = useState5(null);
952
+ const [available, setAvailable] = useState5([]);
953
+ const [sel, setSel] = useState5(0);
954
+ const [dirty, setDirty] = useState5(false);
955
+ const [status, setStatus] = useState5(null);
956
+ const [adding, setAdding] = useState5(null);
957
+ const [newName, setNewName] = useState5("");
958
+ const [newRe, setNewRe] = useState5("");
959
+ useEffect5(() => {
695
960
  if (q.data && !dlp) {
696
- setDlp({ ...q.data.layers.dlp, patterns: [...q.data.layers.dlp.patterns] });
961
+ setDlp({ ...q.data.layers.dlp, patterns: [...q.data.layers.dlp.patterns], custom: [...q.data.layers.dlp.custom] });
697
962
  setAvailable(q.data.availablePatterns);
698
963
  }
699
964
  }, [q.data, dlp]);
700
- const persist = async (nextDlp) => {
701
- if (!q.data) return;
702
- setDlp(nextDlp);
965
+ const mutate = (next) => {
966
+ setDlp(next);
967
+ setDirty(true);
968
+ setStatus(null);
969
+ };
970
+ const save = async () => {
971
+ if (!dlp || !q.data) return;
703
972
  setStatus("Saving\u2026");
704
973
  try {
705
- const res = await api.settings.setSecurityLayers({ ...q.data.layers, dlp: nextDlp });
706
- setDlp({ ...res.layers.dlp, patterns: [...res.layers.dlp.patterns] });
974
+ const res = await api.settings.setSecurityLayers({ ...q.data.layers, dlp });
975
+ setDlp({ ...res.layers.dlp, patterns: [...res.layers.dlp.patterns], custom: [...res.layers.dlp.custom] });
976
+ setDirty(false);
707
977
  setStatus("\u2713 Saved");
708
978
  } catch (e) {
709
979
  setStatus("\u2717 " + (e instanceof Error ? e.message : String(e)));
710
980
  }
711
981
  };
982
+ const discard = () => {
983
+ if (q.data) setDlp({ ...q.data.layers.dlp, patterns: [...q.data.layers.dlp.patterns], custom: [...q.data.layers.dlp.custom] });
984
+ setDirty(false);
985
+ setStatus("discarded");
986
+ };
987
+ const customCount = dlp?.custom.length ?? 0;
988
+ const total = available.length + customCount;
712
989
  useInput3(
713
990
  (input, key) => {
714
991
  if (!dlp) return;
715
992
  if (key.upArrow) setSel((n) => Math.max(0, n - 1));
716
- else if (key.downArrow) setSel((n) => Math.min(available.length - 1, n + 1));
717
- else if (input === " " || key.return) {
993
+ else if (key.downArrow) setSel((n) => Math.min(total - 1, n + 1));
994
+ else if (input === "m") {
995
+ const idx = (MODES2.indexOf(dlp.mode) + 1) % MODES2.length;
996
+ mutate({ ...dlp, mode: MODES2[idx] });
997
+ } else if (input === "a") {
998
+ setNewName("");
999
+ setNewRe("");
1000
+ setAdding("name");
1001
+ } else if (input === " " && sel < available.length) {
718
1002
  const p = available[sel];
719
- if (!p) return;
720
1003
  const set = new Set(dlp.patterns);
721
1004
  if (set.has(p)) set.delete(p);
722
1005
  else set.add(p);
723
- void persist({ ...dlp, patterns: [...set] });
724
- } else if (input === "m") {
725
- const idx = (MODES2.indexOf(dlp.mode) + 1) % MODES2.length;
726
- void persist({ ...dlp, mode: MODES2[idx] });
727
- }
1006
+ mutate({ ...dlp, patterns: [...set] });
1007
+ } else if (input === "d" && sel >= available.length) {
1008
+ const ci = sel - available.length;
1009
+ mutate({ ...dlp, custom: dlp.custom.filter((_, i) => i !== ci) });
1010
+ setSel((n) => Math.max(0, n - 1));
1011
+ } else if (input === "s") void save();
1012
+ else if (input === "x") discard();
728
1013
  },
729
- { isActive: focused }
1014
+ { isActive: focused && !adding }
730
1015
  );
731
1016
  const enabled = new Set(dlp?.patterns ?? []);
732
- return /* @__PURE__ */ jsx4(DataView, { loading: q.loading && !dlp, error: q.error, children: dlp ? /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
733
- /* @__PURE__ */ jsxs4(Box4, { children: [
734
- /* @__PURE__ */ jsx4(Text4, { children: "mode: " }),
735
- /* @__PURE__ */ jsx4(Text4, { color: modeColor(dlp.mode), children: dlp.mode }),
736
- /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: focused ? " \u2191\u2193 move \xB7 space toggle \xB7 m mode" : " press \u2192 to edit" })
1017
+ return /* @__PURE__ */ jsx5(DataView, { loading: q.loading && !dlp, error: q.error, children: dlp ? /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
1018
+ /* @__PURE__ */ jsxs5(Box5, { children: [
1019
+ /* @__PURE__ */ jsx5(Text5, { children: "mode: " }),
1020
+ /* @__PURE__ */ jsx5(Text5, { color: modeColor(dlp.mode), children: dlp.mode }),
1021
+ dirty ? /* @__PURE__ */ jsx5(Text5, { color: theme.warn, children: " \u25CF unsaved (s save \xB7 x discard)" }) : null
737
1022
  ] }),
738
- /* @__PURE__ */ jsx4(Box4, { marginTop: 1, flexDirection: "column", children: available.map((p, i) => {
1023
+ /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: focused ? "\u2191\u2193 move \xB7 space toggle \xB7 m mode \xB7 a add \xB7 d remove \xB7 s save" : "press \u2192 to edit" }),
1024
+ /* @__PURE__ */ jsx5(Box5, { marginTop: 1, flexDirection: "column", children: available.map((p, i) => {
739
1025
  const on = enabled.has(p);
740
- return /* @__PURE__ */ jsxs4(Text4, { color: focused && i === sel ? theme.accentBright : void 0, bold: focused && i === sel, children: [
741
- (focused && i === sel ? "\u25B8 " : " ") + (on ? "\u25CF " : "\u25CB "),
742
- /* @__PURE__ */ jsx4(Text4, { color: on ? theme.ok : theme.dim, children: p })
1026
+ const active2 = focused && sel === i;
1027
+ return /* @__PURE__ */ jsxs5(Text5, { color: active2 ? theme.accentBright : void 0, bold: active2, children: [
1028
+ (active2 ? "\u25B8 " : " ") + (on ? "\u25CF " : "\u25CB "),
1029
+ /* @__PURE__ */ jsx5(Text5, { color: on ? theme.ok : theme.dim, children: p })
743
1030
  ] }, p);
744
1031
  }) }),
745
- dlp.custom.length ? /* @__PURE__ */ jsxs4(Box4, { marginTop: 1, flexDirection: "column", children: [
746
- /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: "Custom:" }),
747
- dlp.custom.map((c2) => /* @__PURE__ */ jsxs4(Text4, { children: [
748
- " ",
749
- /* @__PURE__ */ jsx4(Text4, { color: theme.accent, children: c2.name }),
750
- " ",
751
- /* @__PURE__ */ jsx4(Text4, { color: theme.dim, children: c2.re })
752
- ] }, c2.name))
1032
+ /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
1033
+ /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: "Custom patterns" }),
1034
+ dlp.custom.length === 0 ? /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: " (none \u2014 press a to add)" }) : null,
1035
+ dlp.custom.map((c2, i) => {
1036
+ const active2 = focused && sel === available.length + i;
1037
+ return /* @__PURE__ */ jsxs5(Text5, { color: active2 ? theme.accentBright : void 0, bold: active2, children: [
1038
+ active2 ? "\u25B8 " : " ",
1039
+ /* @__PURE__ */ jsx5(Text5, { color: theme.accent, children: c2.name }),
1040
+ " ",
1041
+ /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: c2.re })
1042
+ ] }, c2.name + i);
1043
+ })
1044
+ ] }),
1045
+ adding ? /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, children: [
1046
+ /* @__PURE__ */ jsx5(Text5, { color: theme.warn, children: adding === "name" ? "New pattern name: " : `Regex for "${newName}": ` }),
1047
+ adding === "name" ? /* @__PURE__ */ jsx5(
1048
+ TextInput2,
1049
+ {
1050
+ value: newName,
1051
+ onChange: setNewName,
1052
+ onSubmit: (v) => {
1053
+ if (!v.trim()) return setAdding(null);
1054
+ setNewName(v.trim());
1055
+ setAdding("re");
1056
+ }
1057
+ }
1058
+ ) : /* @__PURE__ */ jsx5(
1059
+ TextInput2,
1060
+ {
1061
+ value: newRe,
1062
+ onChange: setNewRe,
1063
+ onSubmit: (v) => {
1064
+ if (v.trim() && dlp) mutate({ ...dlp, custom: [...dlp.custom, { name: newName, re: v.trim() }] });
1065
+ setAdding(null);
1066
+ }
1067
+ }
1068
+ )
753
1069
  ] }) : null,
754
- status ? /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) }) : null
1070
+ status ? /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { color: status.startsWith("\u2717") ? theme.bad : theme.ok, children: status }) }) : null
755
1071
  ] }) : null });
756
1072
  }
757
1073
 
758
1074
  // src/tui/panels/Stats.tsx
759
- import { Box as Box5, Text as Text5 } from "ink";
760
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1075
+ import { Box as Box6, Text as Text6 } from "ink";
1076
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
761
1077
  function StatsPanel({ active: active2 }) {
762
1078
  const stats = useLoader(() => api.stats.get());
763
1079
  const ts = useLoader(() => api.stats.timeseries({ period: "24h" }));
@@ -767,50 +1083,50 @@ function StatsPanel({ active: active2 }) {
767
1083
  }, 5e3, active2);
768
1084
  const s = stats.data;
769
1085
  const points = ts.data?.timeseries ?? [];
770
- return /* @__PURE__ */ jsx5(DataView, { loading: stats.loading && !s, error: stats.error, children: s ? /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
771
- /* @__PURE__ */ jsxs5(Box5, { children: [
772
- /* @__PURE__ */ jsxs5(Text5, { bold: true, children: [
1086
+ return /* @__PURE__ */ jsx6(DataView, { loading: stats.loading && !s, error: stats.error, children: s ? /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
1087
+ /* @__PURE__ */ jsxs6(Box6, { children: [
1088
+ /* @__PURE__ */ jsxs6(Text6, { bold: true, children: [
773
1089
  s.total_calls,
774
1090
  " "
775
1091
  ] }),
776
- /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: "calls " }),
777
- /* @__PURE__ */ jsxs5(Text5, { color: theme.ok, children: [
1092
+ /* @__PURE__ */ jsx6(Text6, { color: theme.dim, children: "calls " }),
1093
+ /* @__PURE__ */ jsxs6(Text6, { color: theme.ok, children: [
778
1094
  s.allowed,
779
1095
  " allowed"
780
1096
  ] }),
781
- /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: " " }),
782
- /* @__PURE__ */ jsxs5(Text5, { color: theme.bad, children: [
1097
+ /* @__PURE__ */ jsx6(Text6, { color: theme.dim, children: " " }),
1098
+ /* @__PURE__ */ jsxs6(Text6, { color: theme.bad, children: [
783
1099
  s.denied,
784
1100
  " denied"
785
1101
  ] })
786
1102
  ] }),
787
- /* @__PURE__ */ jsxs5(Text5, { color: theme.dim, children: [
1103
+ /* @__PURE__ */ jsxs6(Text6, { color: theme.dim, children: [
788
1104
  s.active_policies,
789
1105
  " policies \xB7 ",
790
1106
  s.registered_tools,
791
1107
  " tools"
792
1108
  ] }),
793
- /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
794
- /* @__PURE__ */ jsxs5(Text5, { color: theme.dim, children: [
1109
+ /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, flexDirection: "column", children: [
1110
+ /* @__PURE__ */ jsxs6(Text6, { color: theme.dim, children: [
795
1111
  "Last 24h ",
796
1112
  ts.data ? `(per ${ts.data.granularity})` : ""
797
1113
  ] }),
798
- /* @__PURE__ */ jsxs5(Box5, { children: [
799
- /* @__PURE__ */ jsx5(Text5, { children: "total " }),
800
- /* @__PURE__ */ jsx5(Text5, { color: theme.accent, children: sparkline(points.map((p) => p.total), 64) })
1114
+ /* @__PURE__ */ jsxs6(Box6, { children: [
1115
+ /* @__PURE__ */ jsx6(Text6, { children: "total " }),
1116
+ /* @__PURE__ */ jsx6(Text6, { color: theme.accent, children: sparkline(points.map((p) => p.total), 64) })
801
1117
  ] }),
802
- /* @__PURE__ */ jsxs5(Box5, { children: [
803
- /* @__PURE__ */ jsx5(Text5, { children: "allowed " }),
804
- /* @__PURE__ */ jsx5(Text5, { color: theme.ok, children: sparkline(points.map((p) => p.allowed), 64) })
1118
+ /* @__PURE__ */ jsxs6(Box6, { children: [
1119
+ /* @__PURE__ */ jsx6(Text6, { children: "allowed " }),
1120
+ /* @__PURE__ */ jsx6(Text6, { color: theme.ok, children: sparkline(points.map((p) => p.allowed), 64) })
805
1121
  ] }),
806
- /* @__PURE__ */ jsxs5(Box5, { children: [
807
- /* @__PURE__ */ jsx5(Text5, { children: "denied " }),
808
- /* @__PURE__ */ jsx5(Text5, { color: theme.bad, children: sparkline(points.map((p) => p.denied), 64) })
1122
+ /* @__PURE__ */ jsxs6(Box6, { children: [
1123
+ /* @__PURE__ */ jsx6(Text6, { children: "denied " }),
1124
+ /* @__PURE__ */ jsx6(Text6, { color: theme.bad, children: sparkline(points.map((p) => p.denied), 64) })
809
1125
  ] })
810
1126
  ] }),
811
- /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, flexDirection: "column", children: [
812
- /* @__PURE__ */ jsx5(Text5, { color: theme.dim, children: "Recent" }),
813
- /* @__PURE__ */ jsx5(
1127
+ /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, flexDirection: "column", children: [
1128
+ /* @__PURE__ */ jsx6(Text6, { color: theme.dim, children: "Recent" }),
1129
+ /* @__PURE__ */ jsx6(
814
1130
  Table,
815
1131
  {
816
1132
  columns: [
@@ -832,111 +1148,183 @@ function StatsPanel({ active: active2 }) {
832
1148
  }
833
1149
 
834
1150
  // src/tui/panels/Audit.tsx
835
- import { Box as Box6, Text as Text6, useInput as useInput4 } from "ink";
836
- import { useState as useState5 } from "react";
837
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
838
- var FILTERS = [void 0, "DENY", "ALLOW"];
1151
+ import { Box as Box7, Text as Text7, useInput as useInput4 } from "ink";
1152
+ import TextInput3 from "ink-text-input";
1153
+ import { useState as useState6 } from "react";
1154
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1155
+ var DECISIONS = [void 0, "DENY", "ALLOW"];
1156
+ var SIGNALS = [void 0, "dlp", "ratelimit"];
839
1157
  function AuditPanel({ active: active2, focused }) {
840
- const [fi, setFi] = useState5(0);
841
- const filter = FILTERS[fi];
842
- const audit = useLoader(() => api.audit.list({ filter, limit: 14 }), [fi]);
843
- usePoll(audit.reload, 5e3, active2);
1158
+ const [di, setDi] = useState6(0);
1159
+ const [gi, setGi] = useState6(0);
1160
+ const [tool, setTool] = useState6("");
1161
+ const [agent, setAgent] = useState6("");
1162
+ const [search, setSearch] = useState6("");
1163
+ const [sel, setSel] = useState6(0);
1164
+ const [view, setView] = useState6("list");
1165
+ const [editing, setEditing] = useState6(null);
1166
+ const query = {
1167
+ filter: DECISIONS[di],
1168
+ signal: SIGNALS[gi],
1169
+ tool: tool || void 0,
1170
+ agent_name: agent || void 0,
1171
+ search: search || void 0,
1172
+ limit: 100
1173
+ };
1174
+ const auditQ = useLoader(() => api.audit.list(query), [di, gi, tool, agent, search]);
1175
+ usePoll(auditQ.reload, 6e3, active2 && view === "list" && !editing);
1176
+ const entries = auditQ.data?.entries ?? [];
1177
+ const current = entries[Math.min(sel, Math.max(0, entries.length - 1))];
1178
+ const sessionQ = useLoader(
1179
+ () => view === "detail" && current?.session_id ? api.audit.list({ session_id: current.session_id, limit: 40 }) : Promise.resolve(null),
1180
+ [view, current?.session_id]
1181
+ );
844
1182
  useInput4(
845
- (input) => {
846
- if (input === "f") setFi((n) => (n + 1) % FILTERS.length);
1183
+ (input, key) => {
1184
+ if (view === "detail") {
1185
+ if (key.leftArrow || key.escape) setView("list");
1186
+ return;
1187
+ }
1188
+ if (key.upArrow) setSel((n) => Math.max(0, n - 1));
1189
+ else if (key.downArrow) setSel((n) => Math.min(entries.length - 1, n + 1));
1190
+ else if (key.return || key.rightArrow) {
1191
+ if (current) setView("detail");
1192
+ } else if (input === "f") setDi((n) => (n + 1) % DECISIONS.length);
1193
+ else if (input === "g") setGi((n) => (n + 1) % SIGNALS.length);
1194
+ else if (input === "t") setEditing("tool");
1195
+ else if (input === "n") setEditing("agent");
1196
+ else if (input === "/") setEditing("search");
1197
+ else if (input === "c") {
1198
+ setDi(0);
1199
+ setGi(0);
1200
+ setTool("");
1201
+ setAgent("");
1202
+ setSearch("");
1203
+ }
847
1204
  },
848
- { isActive: focused }
1205
+ { isActive: focused && !editing }
849
1206
  );
850
- const entries = audit.data?.entries ?? [];
851
- return /* @__PURE__ */ jsx6(DataView, { loading: audit.loading && !audit.data, error: audit.error, empty: !!audit.data && entries.length === 0, emptyText: "No audit entries.", children: /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
852
- /* @__PURE__ */ jsxs6(Text6, { color: theme.dim, children: [
853
- audit.data?.total ?? 0,
854
- " entries \xB7 filter: ",
855
- /* @__PURE__ */ jsx6(Text6, { color: theme.accent, children: filter ?? "all" }),
856
- " (press f)"
1207
+ if (view === "detail" && current) {
1208
+ const sessionCalls = sessionQ.data?.entries ?? [];
1209
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1210
+ /* @__PURE__ */ jsxs7(Box7, { children: [
1211
+ /* @__PURE__ */ jsx7(Text7, { color: decisionColor(current.decision), bold: true, children: current.decision }),
1212
+ /* @__PURE__ */ jsx7(Text7, { color: theme.accent, children: " " + current.tool_name }),
1213
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " " + current.permission + " \xB7 " + current.trust_level })
1214
+ ] }),
1215
+ /* @__PURE__ */ jsx7(Detail, { label: "reason", value: current.reason ?? "\u2014" }),
1216
+ /* @__PURE__ */ jsx7(Detail, { label: "rule", value: current.matched_rule_id ?? "\u2014" }),
1217
+ /* @__PURE__ */ jsx7(Detail, { label: "agent", value: current.agent_name ?? "\u2014" }),
1218
+ /* @__PURE__ */ jsx7(Detail, { label: "session", value: current.session_id ?? "\u2014" }),
1219
+ /* @__PURE__ */ jsx7(Detail, { label: "dlp", value: current.dlp_matches?.length ? current.dlp_matches.join(", ") : "none", color: current.dlp_matches?.length ? theme.bad : void 0 }),
1220
+ /* @__PURE__ */ jsx7(Detail, { label: "when", value: new Date(current.created_at).toLocaleString() }),
1221
+ current.arguments_summary ? /* @__PURE__ */ jsx7(Detail, { label: "args", value: truncate(JSON.stringify(current.arguments_summary), 60) }) : null,
1222
+ /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, flexDirection: "column", children: [
1223
+ /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
1224
+ "Session ",
1225
+ current.session_id ? `\xB7 ${sessionCalls.length} calls` : "(no session id)"
1226
+ ] }),
1227
+ current.session_id ? /* @__PURE__ */ jsx7(
1228
+ Table,
1229
+ {
1230
+ columns: [
1231
+ { header: "DECISION", width: 9 },
1232
+ { header: "TOOL", width: 20 },
1233
+ { header: "REASON", width: 26 },
1234
+ { header: "WHEN", width: 6 }
1235
+ ],
1236
+ rows: sessionCalls.slice(0, 12).map((e) => [
1237
+ { value: e.decision, color: decisionColor(e.decision) },
1238
+ { value: truncate(e.tool_name, 20), color: theme.accent },
1239
+ { value: truncate(e.reason ?? "\u2014", 26) },
1240
+ { value: ago(e.created_at), dim: true }
1241
+ ])
1242
+ }
1243
+ ) : null
1244
+ ] }),
1245
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "\u2190 back to list" })
1246
+ ] });
1247
+ }
1248
+ const chip = (label, val, on) => /* @__PURE__ */ jsxs7(Text7, { children: [
1249
+ /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
1250
+ label,
1251
+ ":"
1252
+ ] }),
1253
+ /* @__PURE__ */ jsx7(Text7, { color: on ? theme.accentBright : theme.dim, children: val }),
1254
+ /* @__PURE__ */ jsx7(Text7, { children: " " })
1255
+ ] });
1256
+ return /* @__PURE__ */ jsx7(DataView, { loading: auditQ.loading && !auditQ.data, error: auditQ.error, children: /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1257
+ /* @__PURE__ */ jsxs7(Box7, { children: [
1258
+ chip("dec", DECISIONS[di] ?? "all", di !== 0),
1259
+ chip("sig", SIGNALS[gi] ?? "all", gi !== 0),
1260
+ chip("tool", tool || "\xB7", !!tool),
1261
+ chip("agent", agent || "\xB7", !!agent),
1262
+ chip("search", search || "\xB7", !!search)
857
1263
  ] }),
858
- /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(
1264
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: focused ? "\u2191\u2193 \xB7 enter detail \xB7 f dec \xB7 g sig \xB7 t tool \xB7 n agent \xB7 / search \xB7 c clear" : "press \u2192 to browse" }),
1265
+ editing ? /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, children: [
1266
+ /* @__PURE__ */ jsxs7(Text7, { color: theme.warn, children: [
1267
+ editing,
1268
+ ": "
1269
+ ] }),
1270
+ /* @__PURE__ */ jsx7(
1271
+ TextInput3,
1272
+ {
1273
+ value: editing === "tool" ? tool : editing === "agent" ? agent : search,
1274
+ onChange: editing === "tool" ? setTool : editing === "agent" ? setAgent : setSearch,
1275
+ onSubmit: () => setEditing(null)
1276
+ }
1277
+ )
1278
+ ] }) : null,
1279
+ /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
1280
+ auditQ.data?.total ?? 0,
1281
+ " matched \xB7 showing ",
1282
+ Math.min(entries.length, 12)
1283
+ ] }) }),
1284
+ /* @__PURE__ */ jsx7(
859
1285
  Table,
860
1286
  {
861
1287
  columns: [
1288
+ { header: "", width: 2 },
862
1289
  { header: "DECISION", width: 9 },
863
1290
  { header: "TOOL", width: 18 },
864
1291
  { header: "AGENT", width: 14 },
865
- { header: "REASON", width: 26 },
1292
+ { header: "REASON", width: 24 },
866
1293
  { header: "DLP", width: 4 },
867
1294
  { header: "WHEN", width: 6 }
868
1295
  ],
869
- rows: entries.map((e) => [
870
- { value: e.decision, color: decisionColor(e.decision) },
871
- { value: truncate(e.tool_name, 18), color: theme.accent },
872
- { value: truncate(e.agent_name ?? "\u2014", 14), dim: true },
873
- { value: truncate(e.reason ?? "\u2014", 26) },
874
- { value: e.dlp_matches?.length ? String(e.dlp_matches.length) : "0", color: e.dlp_matches?.length ? theme.bad : void 0, dim: !e.dlp_matches?.length },
875
- { value: ago(e.created_at), dim: true }
876
- ])
1296
+ rows: entries.slice(0, 12).map((e, i) => rowFor(e, i === sel && focused))
877
1297
  }
878
- ) })
1298
+ )
879
1299
  ] }) });
880
1300
  }
881
-
882
- // src/tui/panels/Agents.tsx
883
- import { Box as Box7, Text as Text7 } from "ink";
884
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
885
- function AgentsPanel({ active: active2 }) {
886
- const agents = useLoader(() => api.agents.live({ limit: 14 }));
887
- usePoll(agents.reload, 4e3, active2);
888
- const data = agents.data;
889
- const rows = data?.agents ?? [];
890
- return /* @__PURE__ */ jsx7(DataView, { loading: agents.loading && !data, error: agents.error, empty: !!data && rows.length === 0, emptyText: "No agent sessions.", children: /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
891
- data ? /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
892
- /* @__PURE__ */ jsxs7(Text7, { color: theme.ok, children: [
893
- data.counts.active,
894
- " active"
895
- ] }),
896
- " \xB7 ",
897
- /* @__PURE__ */ jsxs7(Text7, { color: theme.warn, children: [
898
- data.counts.idle,
899
- " idle"
900
- ] }),
901
- " \xB7 ",
902
- data.counts.deactivated,
903
- " off"
904
- ] }) : null,
905
- /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(
906
- Table,
907
- {
908
- columns: [
909
- { header: "STATUS", width: 7 },
910
- { header: "AGENT", width: 20 },
911
- { header: "CALLS", width: 6 },
912
- { header: "DENY", width: 5 },
913
- { header: "DLP", width: 4 },
914
- { header: "TRUST", width: 6 },
915
- { header: "CHARACTER", width: 20 }
916
- ],
917
- rows: rows.map((a) => [
918
- { value: a.status, color: modeColor(a.status) },
919
- { value: truncate(a.agent_name ?? a.agent_id ?? a.session_id, 20), color: theme.accent },
920
- { value: String(a.total_calls) },
921
- { value: String(a.denied_calls), color: a.denied_calls ? theme.bad : void 0, dim: !a.denied_calls },
922
- { value: String(a.dlp_events), color: a.dlp_events ? theme.bad : void 0, dim: !a.dlp_events },
923
- { value: `${a.trust_score}` },
924
- { value: truncate(a.character || "\u2014", 20), dim: true }
925
- ])
926
- }
927
- ) })
928
- ] }) });
1301
+ function rowFor(e, active2) {
1302
+ return [
1303
+ { value: active2 ? "\u25B8" : "", color: theme.accentBright },
1304
+ { value: e.decision, color: decisionColor(e.decision) },
1305
+ { value: truncate(e.tool_name, 18), color: theme.accent },
1306
+ { value: truncate(e.agent_name ?? "\u2014", 14), dim: true },
1307
+ { value: truncate(e.reason ?? "\u2014", 24) },
1308
+ { value: e.dlp_matches?.length ? String(e.dlp_matches.length) : "0", color: e.dlp_matches?.length ? theme.bad : void 0, dim: !e.dlp_matches?.length },
1309
+ { value: ago(e.created_at), dim: true }
1310
+ ];
1311
+ }
1312
+ function Detail({ label, value, color }) {
1313
+ return /* @__PURE__ */ jsxs7(Box7, { children: [
1314
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: label.padEnd(9) }),
1315
+ /* @__PURE__ */ jsx7(Text7, { color, children: value })
1316
+ ] });
929
1317
  }
930
1318
 
931
1319
  // src/tui/App.tsx
932
1320
  import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
933
1321
  var SECTIONS = [
1322
+ { label: "Live", Panel: LivePanel },
934
1323
  { label: "Policies", Panel: PoliciesPanel },
935
1324
  { label: "Rate Limit", Panel: RateLimitPanel },
936
1325
  { label: "DLP", Panel: DlpPanel },
937
1326
  { label: "Stats", Panel: StatsPanel },
938
- { label: "Audit", Panel: AuditPanel },
939
- { label: "Agents", Panel: AgentsPanel }
1327
+ { label: "Audit", Panel: AuditPanel }
940
1328
  ];
941
1329
  var BANNER_HEX = ["#1432A0", "#2850BE", "#3C6ED7", "#5A8CE6", "#82AAF0", "#AAC8FA"];
942
1330
  function Banner() {
@@ -954,8 +1342,8 @@ function Banner() {
954
1342
  }
955
1343
  function App() {
956
1344
  const { exit } = useApp();
957
- const [section, setSection] = useState6(0);
958
- const [focus, setFocus] = useState6("nav");
1345
+ const [section, setSection] = useState7(0);
1346
+ const [focus, setFocus] = useState7("nav");
959
1347
  useInput5((input, key) => {
960
1348
  if (focus === "nav") {
961
1349
  if (key.upArrow) setSection((n) => (n - 1 + SECTIONS.length) % SECTIONS.length);