@solongate/proxy 0.68.0 → 0.70.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.js +348 -156
  2. package/dist/tui/index.js +318 -126
  3. package/package.json +1 -1
package/dist/tui/index.js CHANGED
@@ -126,9 +126,10 @@ function KeyHints({ hints }) {
126
126
 
127
127
  // src/tui/panels/Live.tsx
128
128
  import { Box as Box2, Text as Text2, useInput } from "ink";
129
- import { closeSync, openSync, readSync, statSync } from "fs";
129
+ import { spawnSync } from "child_process";
130
+ import { closeSync, mkdirSync, openSync, readSync, statSync, writeFileSync } from "fs";
130
131
  import { homedir as homedir2 } from "os";
131
- import { join as join2 } from "path";
132
+ import { dirname, join as join2 } from "path";
132
133
  import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
133
134
 
134
135
  // src/api-client/client.ts
@@ -436,8 +437,11 @@ function usePoll(reload, intervalMs, enabled = true) {
436
437
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
437
438
  var SPIN = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
438
439
  var BG = "#12234f";
440
+ var DIM_FLOOR = "#233457";
439
441
  var LOCAL_LOG = join2(homedir2(), ".solongate", "local-logs", "solongate-audit.jsonl");
440
442
  var RING = join2(process.cwd(), ".solongate", ".eval-ring.jsonl");
443
+ var DUMP = join2(homedir2(), ".solongate", "live-stream.log");
444
+ var STREAM_KEEP = 100;
441
445
  var hhmmss = (ts) => {
442
446
  const d = new Date(ts);
443
447
  return Number.isNaN(d.getTime()) ? "--:--:--" : d.toTimeString().slice(0, 8);
@@ -447,7 +451,7 @@ var fmtUp = (ms) => {
447
451
  const p = (n) => String(n).padStart(2, "0");
448
452
  return `${p(Math.floor(s / 3600))}:${p(Math.floor(s % 3600 / 60))}:${p(s % 60)}`;
449
453
  };
450
- function tailLines(file, maxBytes = 65536) {
454
+ function tailLines(file, maxBytes = 131072) {
451
455
  try {
452
456
  const size = statSync(file).size;
453
457
  const start = Math.max(0, size - maxBytes);
@@ -462,6 +466,22 @@ function tailLines(file, maxBytes = 65536) {
462
466
  return [];
463
467
  }
464
468
  }
469
+ function copyToClipboard(text) {
470
+ const tools = [
471
+ ["wl-copy", []],
472
+ ["xclip", ["-selection", "clipboard"]],
473
+ ["xsel", ["--clipboard", "--input"]]
474
+ ];
475
+ for (const [cmd, args] of tools) {
476
+ try {
477
+ const r = spawnSync(cmd, args, { input: text, timeout: 2e3 });
478
+ if (r.status === 0) return cmd;
479
+ } catch {
480
+ }
481
+ }
482
+ process.stdout.write(`\x1B]52;c;${Buffer.from(text, "utf-8").toString("base64")}\x07`);
483
+ return "osc52";
484
+ }
465
485
  function cloudItem(e) {
466
486
  return {
467
487
  id: "c:" + e.id,
@@ -471,7 +491,11 @@ function cloudItem(e) {
471
491
  permission: (e.permission ?? "").slice(0, 4),
472
492
  detail: (e.arguments_summary ? JSON.stringify(e.arguments_summary) : e.reason ?? "").replace(/\s+/g, " "),
473
493
  dlp: !!e.dlp_matches?.length,
474
- source: "cloud"
494
+ source: "cloud",
495
+ session: e.session_id ?? void 0,
496
+ agent: e.agent_name ?? void 0,
497
+ evalMs: e.evaluation_time_ms ?? void 0,
498
+ rule: e.matched_rule_id ?? void 0
475
499
  };
476
500
  }
477
501
  function ColumnChart({ series, hot, height, width, color, hotColor = theme.bad }) {
@@ -490,7 +514,7 @@ function ColumnChart({ series, hot, height, width, color, hotColor = theme.bad }
490
514
  else if (frac >= (r - 0.5) / height) ch = "\u2584";
491
515
  else if (r === 1) ch = "\u2581";
492
516
  else ch = " ";
493
- const c2 = r === 1 && v === 0 ? "#233457" : hotPad?.[i] ? hotColor : color;
517
+ const c2 = r === 1 && v === 0 ? DIM_FLOOR : hotPad?.[i] ? hotColor : color;
494
518
  const last = segs[segs.length - 1];
495
519
  if (last && last.color === c2) last.text += ch;
496
520
  else segs.push({ text: ch, color: c2 });
@@ -506,7 +530,7 @@ function HBar({ label, value, max, width, color }) {
506
530
  return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
507
531
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: label.padEnd(10) }),
508
532
  /* @__PURE__ */ jsx2(Text2, { color, children: "\u2588".repeat(Math.min(width, filled)) }),
509
- /* @__PURE__ */ jsx2(Text2, { color: "#233457", children: "\u2591".repeat(Math.max(0, width - filled)) }),
533
+ /* @__PURE__ */ jsx2(Text2, { color: DIM_FLOOR, children: "\u2591".repeat(Math.max(0, width - filled)) }),
510
534
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " " + value })
511
535
  ] });
512
536
  }
@@ -522,6 +546,26 @@ function PaneTitle({ label, extra, width }) {
522
546
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: tail + "\u2500".repeat(Math.max(0, width - used)) })
523
547
  ] });
524
548
  }
549
+ function StreamLine({ e }) {
550
+ return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
551
+ /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
552
+ "[",
553
+ hhmmss(e.at),
554
+ " "
555
+ ] }),
556
+ /* @__PURE__ */ jsx2(Text2, { color: e.source === "local" ? theme.ok : "#4f6db8", children: e.source === "local" ? "LOC" : "CLD" }),
557
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "] " }),
558
+ /* @__PURE__ */ jsx2(Text2, { color: decisionColor(e.decision), bold: e.decision !== "ALLOW", children: e.decision.padEnd(6) }),
559
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate(e.tool, 12).padEnd(13) }),
560
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: e.permission.padEnd(5) }),
561
+ /* @__PURE__ */ jsx2(Text2, { color: e.evalMs != null && e.evalMs > 500 ? theme.warn : theme.dim, children: (e.evalMs != null ? `${e.evalMs}ms` : "\u2014").padEnd(7) }),
562
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: truncate(e.agent ?? "-", 11).padEnd(12) }),
563
+ e.dlp ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "DLP! " }) : null,
564
+ e.rule && e.decision !== "ALLOW" ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: truncate(e.rule, 14) + " " }) : null,
565
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: e.detail })
566
+ ] });
567
+ }
568
+ var FILTERS = ["all", "local", "cloud"];
525
569
  function LivePanel({ active: active2 }) {
526
570
  const [s, setS] = useState2(null);
527
571
  const [tsData, setTsData] = useState2(null);
@@ -531,18 +575,17 @@ function LivePanel({ active: active2 }) {
531
575
  const [localOn, setLocalOn] = useState2(null);
532
576
  const [ring, setRing] = useState2(null);
533
577
  const [log, setLog] = useState2([]);
578
+ const [filter, setFilter] = useState2("all");
579
+ const [scroll, setScroll] = useState2(0);
580
+ const [toast, setToast] = useState2(null);
581
+ const [mode, setMode] = useState2("stream");
582
+ const [pickIdx, setPickIdx] = useState2(0);
583
+ const [detail, setDetail] = useState2(null);
584
+ const [detailCloud, setDetailCloud] = useState2([]);
585
+ const [detailScroll, setDetailScroll] = useState2(0);
534
586
  const seenRef = useRef2(/* @__PURE__ */ new Set());
535
587
  const lastLocalTs = useRef2(0);
536
588
  const pausedUntil = useRef2(0);
537
- const [frozen, setFrozen] = useState2(false);
538
- const frozenRef = useRef2(false);
539
- frozenRef.current = frozen;
540
- useInput(
541
- (input) => {
542
- if (input === "p") setFrozen((f) => !f);
543
- },
544
- { isActive: active2 }
545
- );
546
589
  const pushLog = useCallback2((msg, level = "ok") => {
547
590
  setLog((prev) => [...prev.slice(-59), { ts: Date.now(), msg, level }]);
548
591
  }, []);
@@ -558,9 +601,8 @@ function LivePanel({ active: active2 }) {
558
601
  },
559
602
  [pushLog]
560
603
  );
561
- const paused = () => frozenRef.current || Date.now() < pausedUntil.current;
604
+ const paused = () => Date.now() < pausedUntil.current;
562
605
  const pollLocal = useCallback2(() => {
563
- if (frozenRef.current) return;
564
606
  const lines = tailLines(LOCAL_LOG);
565
607
  if (!lines.length) {
566
608
  setLocalOn((prev) => prev === null ? false : prev);
@@ -583,7 +625,9 @@ function LivePanel({ active: active2 }) {
583
625
  dlp: !!j.dlp,
584
626
  source: "local",
585
627
  session: j.session_id,
586
- agent: j.agent_name
628
+ agent: j.agent_name,
629
+ evalMs: j.evaluation_time_ms,
630
+ rule: j.matched_rule_id
587
631
  });
588
632
  } catch {
589
633
  }
@@ -592,9 +636,7 @@ function LivePanel({ active: active2 }) {
592
636
  lastLocalTs.current = fresh[fresh.length - 1].at;
593
637
  setLocalBuf((prev) => [...prev, ...fresh].slice(-400));
594
638
  const denies = fresh.filter((f) => f.decision !== "ALLOW").length;
595
- if (lastLocalTs.current !== fresh[0].at || fresh.length < 10) {
596
- pushLog(`local +${fresh.length} calls${denies ? ` \xB7 ${denies} DENIED` : ""}`, denies ? "bad" : "warn");
597
- }
639
+ if (fresh.length < 10) pushLog(`local +${fresh.length} calls${denies ? ` \xB7 ${denies} DENIED` : ""}`, denies ? "bad" : "warn");
598
640
  }
599
641
  const ringLines = tailLines(RING, 8192).slice(-30);
600
642
  if (ringLines.length) {
@@ -631,12 +673,11 @@ function LivePanel({ active: active2 }) {
631
673
  const fresh = fd.entries.filter((e) => !seenRef.current.has(e.id));
632
674
  const firstLoad = seenRef.current.size === 0 && fresh.length > 1;
633
675
  for (const e of fresh) seenRef.current.add(e.id);
634
- if (fresh.length) {
635
- setCloudBuf((prev) => [...prev, ...fresh.map(cloudItem).sort((a, b) => a.at - b.at)].slice(-400));
636
- }
676
+ if (fresh.length) setCloudBuf((prev) => [...prev, ...fresh.map(cloudItem).sort((a, b) => a.at - b.at)].slice(-400));
637
677
  const denies = fresh.filter((e) => e.decision !== "ALLOW").length;
638
678
  if (firstLoad) pushLog(`cloud link up \xB7 api ${ms}ms \xB7 ${fresh.length} calls`);
639
- else pushLog(`api ${ms}ms \xB7 +${fresh.length} cloud${denies ? ` \xB7 ${denies} DENIED` : ""}`, denies ? "bad" : fresh.length ? "warn" : "ok");
679
+ else if (fresh.length) pushLog(`api ${ms}ms \xB7 +${fresh.length} cloud${denies ? ` \xB7 ${denies} DENIED` : ""}`, denies ? "bad" : "warn");
680
+ else pushLog(`api ${ms}ms \xB7 idle`, "ok");
640
681
  } catch (e) {
641
682
  onApiError(e);
642
683
  }
@@ -673,30 +714,50 @@ function LivePanel({ active: active2 }) {
673
714
  }, [active2, pollFeed, pollMedium, pollSlow]);
674
715
  const sessions = useLoader(() => api.agents.live({ limit: 30, includeDeactivated: true }));
675
716
  const insights = useLoader(() => api.stats.securityInsights(7));
717
+ const guard = useLoader(() => api.settings.getGuardStatus());
676
718
  usePoll(() => {
677
719
  if (!paused()) sessions.reload();
678
720
  }, 1e4, active2);
679
721
  usePoll(() => {
680
722
  if (!paused()) insights.reload();
681
723
  }, 3e4, active2);
724
+ usePoll(() => {
725
+ if (!paused()) guard.reload();
726
+ }, 6e4, active2);
727
+ useEffect2(() => {
728
+ if (!detail) return;
729
+ setDetailCloud([]);
730
+ setDetailScroll(0);
731
+ let alive = true;
732
+ api.audit.list({ session_id: detail.id, limit: 100 }).then((r) => {
733
+ if (alive) setDetailCloud(r.entries.map(cloudItem));
734
+ }).catch(() => {
735
+ });
736
+ return () => {
737
+ alive = false;
738
+ };
739
+ }, [detail]);
682
740
  const [tick, setTick] = useState2(0);
683
741
  useEffect2(() => {
684
- if (!active2 || frozen) return;
685
- const t = setInterval(() => setTick((n) => n + 1), 200);
742
+ if (!active2) return;
743
+ const t = setInterval(() => setTick((n) => n + 1), 500);
686
744
  return () => clearInterval(t);
687
- }, [active2, frozen]);
745
+ }, [active2]);
688
746
  const startRef = useRef2(Date.now());
689
747
  const cols = process.stdout.columns ?? 100;
690
748
  const rows = process.stdout.rows ?? 30;
691
749
  const ins = insights.data ?? {};
692
750
  const spin = SPIN[tick % SPIN.length];
693
- const points = tsData?.timeseries ?? [];
694
- const mergedAll = [...cloudBuf, ...localBuf].sort((a, b) => a.at - b.at);
695
751
  const nowMs = Date.now();
752
+ const mergedAll = [...cloudBuf, ...localBuf].sort((a, b) => a.at - b.at);
753
+ const filtered = filter === "all" ? mergedAll : mergedAll.filter((e) => e.source === filter);
754
+ const visibleDesc = filtered.slice(-STREAM_KEEP).reverse();
755
+ const points = tsData?.timeseries ?? [];
756
+ const nowMin = Math.floor(nowMs / 6e4);
696
757
  const minuteCounts = new Array(60).fill(0);
697
758
  const minuteHot = new Array(60).fill(false);
698
759
  for (const e of mergedAll) {
699
- const idx = 59 - Math.floor((nowMs - e.at) / 6e4);
760
+ const idx = 59 - (nowMin - Math.floor(e.at / 6e4));
700
761
  if (idx >= 0 && idx < 60) {
701
762
  minuteCounts[idx]++;
702
763
  if (e.decision !== "ALLOW") minuteHot[idx] = true;
@@ -708,14 +769,14 @@ function LivePanel({ active: active2 }) {
708
769
  const trafficLabel = localTraffic ? "calls/min \xB7 60m \xB7 local+cloud" : "24h \xB7 cloud";
709
770
  const rl = ins.layers?.rateLimit;
710
771
  const dl = ins.layers?.dlp;
711
- const minuteNow = (ins.activity?.minute ?? []).slice(-1)[0]?.count ?? 0;
772
+ const gh = ins.layers?.ghost;
773
+ const minuteNow = minuteCounts[59] ?? 0;
712
774
  const dlpBars = (ins.dlpByPattern ?? []).slice(0, 2);
713
775
  const maxDlpBar = dlpBars[0]?.count ?? 1;
714
- const merged = mergedAll;
715
- const denials = merged.filter((e) => e.decision !== "ALLOW");
716
- const lastDeny = denials[denials.length - 1];
776
+ const denialsAll = mergedAll.filter((e) => e.decision !== "ALLOW");
777
+ const lastDeny = denialsAll[denialsAll.length - 1];
717
778
  const toolCounts = /* @__PURE__ */ new Map();
718
- for (const e of merged) toolCounts.set(e.tool, (toolCounts.get(e.tool) ?? 0) + 1);
779
+ for (const e of mergedAll) toolCounts.set(e.tool, (toolCounts.get(e.tool) ?? 0) + 1);
719
780
  const topTools = [...toolCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
720
781
  const latNow = lat[lat.length - 1] ?? 0;
721
782
  const sortedLat = [...lat].sort((a, b) => a - b);
@@ -734,22 +795,174 @@ function LivePanel({ active: active2 }) {
734
795
  if (e.agent) cur.agent = e.agent;
735
796
  localSess.set(e.session, cur);
736
797
  }
737
- const localSessList = [...localSess.entries()].sort((a, b) => b[1].lastAt - a[1].lastAt);
798
+ const combinedSess = [
799
+ ...[...localSess.entries()].sort((a, b) => b[1].lastAt - a[1].lastAt).map(([id, v]) => ({ source: "local", id, agent: v.agent, calls: v.calls, denies: v.denies, lastAt: v.lastAt, isMe: ring?.session === id })),
800
+ ...sess.filter((a) => !localSess.has(a.session_id)).map((a) => ({
801
+ source: "cloud",
802
+ id: a.session_id,
803
+ agent: a.agent_name ?? a.session_id,
804
+ calls: a.total_calls,
805
+ denies: a.denied_calls,
806
+ lastAt: Date.parse(a.last_seen_at),
807
+ trust: a.trust_score,
808
+ isMe: false
809
+ }))
810
+ ];
738
811
  const chartH = rows >= 36 ? 6 : 4;
739
- const colH = rows >= 32 ? 6 : 5;
740
- const streamRows = Math.max(4, rows - 6 - chartH - colH - (localOn === false ? 1 : 0));
741
- const tail = merged.slice(-streamRows);
812
+ const colH = rows >= 32 ? 7 : 5;
813
+ const streamRows = Math.max(4, rows - 5 - (1 + chartH) - colH);
742
814
  const innerW = cols - 2;
743
815
  const leftW = Math.floor(innerW * 0.55);
744
816
  const rightW = innerW - leftW - 2;
745
817
  const colW = Math.max(20, Math.floor((innerW - 4) / 3));
818
+ const pickable = combinedSess.slice(0, colH - 1);
819
+ const maxScroll = Math.max(0, visibleDesc.length - streamRows);
820
+ const clampedScroll = Math.min(scroll, maxScroll);
821
+ const windowed = visibleDesc.slice(clampedScroll, clampedScroll + streamRows);
822
+ const detailEntries = detail ? (() => {
823
+ const seen = /* @__PURE__ */ new Set();
824
+ const all = [];
825
+ for (const e of [...detailCloud, ...mergedAll.filter((x) => x.session === detail.id)]) {
826
+ const key = `${e.at}:${e.tool}:${e.decision}`;
827
+ if (seen.has(key)) continue;
828
+ seen.add(key);
829
+ all.push(e);
830
+ }
831
+ return all.sort((a, b) => b.at - a.at);
832
+ })() : [];
833
+ useInput(
834
+ (input, key) => {
835
+ if (mode === "detail") {
836
+ const maxD = Math.max(0, detailEntries.length - (rows - 10));
837
+ if (key.leftArrow) {
838
+ setMode("stream");
839
+ setDetail(null);
840
+ } else if (key.upArrow) setDetailScroll((n) => Math.max(0, n - 1));
841
+ else if (key.downArrow) setDetailScroll((n) => Math.min(maxD, n + 1));
842
+ else if (key.pageUp) setDetailScroll((n) => Math.max(0, n - 10));
843
+ else if (key.pageDown) setDetailScroll((n) => Math.min(maxD, n + 10));
844
+ return;
845
+ }
846
+ if (mode === "pick") {
847
+ if (key.upArrow) setPickIdx((n) => Math.max(0, n - 1));
848
+ else if (key.downArrow) setPickIdx((n) => Math.min(pickable.length - 1, n + 1));
849
+ else if (key.return) {
850
+ const row = pickable[pickIdx];
851
+ if (row) {
852
+ setDetail(row);
853
+ setMode("detail");
854
+ }
855
+ } else if (input === "s" || key.leftArrow) setMode("stream");
856
+ return;
857
+ }
858
+ if (input === "f") {
859
+ setFilter((f) => FILTERS[(FILTERS.indexOf(f) + 1) % FILTERS.length]);
860
+ setScroll(0);
861
+ } else if (input === "s") {
862
+ setPickIdx(0);
863
+ setMode("pick");
864
+ } else if (key.downArrow) setScroll((n) => Math.min(maxScroll, n + 1));
865
+ else if (key.upArrow) setScroll((n) => Math.max(0, n - 1));
866
+ else if (key.pageDown) setScroll((n) => Math.min(maxScroll, n + streamRows));
867
+ else if (key.pageUp) setScroll((n) => Math.max(0, n - streamRows));
868
+ else if (input === "c") {
869
+ const dump = visibleDesc.map(
870
+ (e) => `[${new Date(e.at).toISOString()}] ${e.source.toUpperCase().padEnd(5)} ${e.decision.padEnd(6)} ${e.tool.padEnd(16)} ${e.permission.padEnd(5)} ${e.evalMs != null ? String(e.evalMs).padStart(5) + "ms " : " "}${(e.agent ?? "-").padEnd(14)} ${e.session ? e.session.slice(0, 8) : "- "} ${e.detail}`
871
+ ).join("\n");
872
+ let via = "file";
873
+ try {
874
+ mkdirSync(dirname(DUMP), { recursive: true });
875
+ writeFileSync(DUMP, dump + "\n");
876
+ } catch {
877
+ }
878
+ via = copyToClipboard(dump);
879
+ setToast({ msg: `copied ${visibleDesc.length} lines via ${via} + ${DUMP}`, until: Date.now() + 5e3 });
880
+ }
881
+ },
882
+ { isActive: active2 }
883
+ );
746
884
  const sessionDot = (st) => st === "active" ? { ch: "\u25CF", color: theme.ok } : st === "idle" ? { ch: "\u25CC", color: theme.warn } : { ch: "\u25CB", color: theme.dim };
885
+ void sessionDot;
886
+ if (s === null && localOn === null) {
887
+ return /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
888
+ spin,
889
+ " connecting\u2026"
890
+ ] });
891
+ }
892
+ const titleBar = /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
893
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#1432A0", color: "#AAC8FA", bold: true, children: " SOLONGATE LIVE " }),
894
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: ` ${spin} up ${fmtUp(nowMs - startRef.current)} \xB7 ${hhmmss(nowMs)} \xB7 api ${latNow}ms ` }),
895
+ backingOff ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#3d2a12", color: "#ffb454", bold: true, children: " RATE LIMITED \xB7 backing off " }) : lastDeny ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#3d1220", color: "#ff6b6b", bold: true, children: ` \u26A0 ${hhmmss(lastDeny.at)} ${lastDeny.tool} DENIED ` }) : /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#4f6db8", children: " \u2713 clean " }),
896
+ toast && nowMs < toast.until ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#123d1f", color: "#7bd88f", bold: true, children: ` ${toast.msg} ` }) : null
897
+ ] });
898
+ if (mode === "detail" && detail) {
899
+ const d = detailEntries;
900
+ const allow = d.filter((e) => e.decision === "ALLOW").length;
901
+ const deny = d.length - allow;
902
+ const dlpN = d.filter((e) => e.dlp).length;
903
+ const evals = d.filter((e) => e.evalMs != null).map((e) => e.evalMs);
904
+ const evalAvg = evals.length ? Math.round(evals.reduce((a, b) => a + b, 0) / evals.length) : null;
905
+ const tCounts = /* @__PURE__ */ new Map();
906
+ const pCounts = /* @__PURE__ */ new Map();
907
+ for (const e of d) {
908
+ tCounts.set(e.tool, (tCounts.get(e.tool) ?? 0) + 1);
909
+ if (e.permission) pCounts.set(e.permission, (pCounts.get(e.permission) ?? 0) + 1);
910
+ }
911
+ const top = [...tCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 6);
912
+ const perms = [...pCounts.entries()].sort((a, b) => b[1] - a[1]);
913
+ const first = d[d.length - 1]?.at;
914
+ const last = d[0]?.at;
915
+ const tlRows = Math.max(4, rows - 10);
916
+ const maxD = Math.max(0, d.length - tlRows);
917
+ const dScroll = Math.min(detailScroll, maxD);
918
+ const tl = d.slice(dScroll, dScroll + tlRows);
919
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
920
+ titleBar,
921
+ /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
922
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#AAC8FA", bold: true, children: ` SESSION ${detail.id.slice(0, 8)} ` }),
923
+ /* @__PURE__ */ jsx2(Text2, { color: detail.source === "local" ? theme.ok : "#4f6db8", children: " " + detail.source.toUpperCase() }),
924
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: " " + truncate(detail.isMe ? detail.agent + " (this machine)" : detail.agent, 34) }),
925
+ detail.trust != null ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ` trust ${detail.trust}/100` }) : null,
926
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2190 back" })
927
+ ] }),
928
+ /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
929
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "\u2502 calls " }),
930
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: d.length }),
931
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 allow " }),
932
+ /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: allow }),
933
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 deny " }),
934
+ /* @__PURE__ */ jsx2(Text2, { color: deny ? theme.bad : theme.dim, bold: deny > 0, children: deny }),
935
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 dlp " }),
936
+ /* @__PURE__ */ jsx2(Text2, { color: dlpN ? theme.bad : theme.dim, children: dlpN }),
937
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 avg eval " }),
938
+ /* @__PURE__ */ jsx2(Text2, { children: evalAvg != null ? `${evalAvg}ms` : "\u2014" }),
939
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 first " }),
940
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: first ? `${hhmmss(first)} (${ago(first)} ago)` : "\u2014" }),
941
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 last " }),
942
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: last ? `${hhmmss(last)} (${ago(last)} ago)` : "\u2014" }),
943
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
944
+ ] }),
945
+ /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
946
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "\u2502 tools " }),
947
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: top.map(([t, c2]) => `${t}\xD7${c2}`).join(" ") || "\u2014" }),
948
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 perms " }),
949
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: perms.map(([p, c2]) => `${p}\xD7${c2}`).join(" ") || "\u2014" }),
950
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
951
+ ] }),
952
+ /* @__PURE__ */ jsx2(PaneTitle, { label: "TIMELINE", extra: `${d.length} calls \xB7 newest first${dScroll ? ` \xB7 \u25BC${dScroll} older` : ""} \xB7 \u2191\u2193 scroll \xB7 \u2190 back`, width: innerW }),
953
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: tlRows, overflow: "hidden", children: [
954
+ tl.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: detailCloud.length === 0 ? spin + " loading history\u2026" : "no entries" }) : null,
955
+ tl.map((e) => /* @__PURE__ */ jsx2(StreamLine, { e }, e.id))
956
+ ] }),
957
+ /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
958
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", bold: true, children: " SESSION " }),
959
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#0b1530", color: "#4f6db8", children: ` ${detail.id} ` }),
960
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: " \u2191\u2193 scroll \xB7 \u2190 back \xB7 esc menu " })
961
+ ] })
962
+ ] });
963
+ }
747
964
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
748
- /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
749
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#1432A0", color: "#AAC8FA", bold: true, children: " SOLONGATE LIVE " }),
750
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: ` ${spin} up ${fmtUp(Date.now() - startRef.current)} \xB7 ${hhmmss(Date.now())} \xB7 api ${latNow}ms ` }),
751
- frozen ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#3d2a12", color: "#ffb454", bold: true, children: " PAUSED \xB7 copy freely \xB7 p resume " }) : backingOff ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#3d2a12", color: "#ffb454", bold: true, children: " RATE LIMITED \xB7 backing off " }) : lastDeny ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#3d1220", color: "#ff6b6b", bold: true, children: ` \u26A0 ${hhmmss(lastDeny.at)} ${lastDeny.tool} DENIED ` }) : /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#4f6db8", children: " \u2713 clean " })
752
- ] }),
965
+ titleBar,
753
966
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
754
967
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "\u2502 calls " }),
755
968
  /* @__PURE__ */ jsx2(Text2, { bold: true, children: s ? s.total_calls : "\xB7\xB7\xB7\xB7" }),
@@ -758,16 +971,20 @@ function LivePanel({ active: active2 }) {
758
971
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 deny " }),
759
972
  /* @__PURE__ */ jsx2(Text2, { color: theme.bad, bold: true, children: s ? s.denied : "\xB7\xB7" }),
760
973
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 local " }),
761
- localOn ? /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "\u2713 streaming" }) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "off" }),
762
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 ratelimit " }),
974
+ localOn ? /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "\u2713" }) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "off" }),
975
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 rl " }),
763
976
  /* @__PURE__ */ jsx2(Text2, { color: rl?.mode === "block" ? theme.ok : rl?.mode === "detect" ? theme.warn : theme.dim, children: rl?.mode ?? "?" }),
764
977
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 dlp " }),
765
978
  /* @__PURE__ */ jsx2(Text2, { color: dl?.mode === "block" ? theme.ok : dl?.mode === "detect" ? theme.warn : theme.dim, children: dl?.mode ?? "?" }),
766
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 sessions " }),
767
- /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: sessCounts ? `${sessCounts.active}\u25B2 ${sessCounts.idle}\u25CC ${sessCounts.deactivated}\u25CB` : "\xB7" }),
979
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 ghost " }),
980
+ /* @__PURE__ */ jsx2(Text2, { color: gh?.mode === "on" ? theme.ok : theme.dim, children: gh?.mode ?? "?" }),
981
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 sess " }),
982
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: `${combinedSess.length} (${sessCounts?.active ?? 0} live)` }),
983
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 hooks " }),
984
+ /* @__PURE__ */ jsx2(Text2, { color: guard.data?.up_to_date ? theme.ok : theme.warn, children: guard.data ? `v${guard.data.installed ?? "?"}${guard.data.up_to_date ? "" : "\u2192v" + guard.data.latest} ${guard.data.device_count}dev` : "\xB7" }),
768
985
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
769
986
  ] }),
770
- /* @__PURE__ */ jsxs2(Box2, { children: [
987
+ /* @__PURE__ */ jsxs2(Box2, { height: 1 + chartH, children: [
771
988
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: leftW, marginRight: 2, children: [
772
989
  /* @__PURE__ */ jsx2(PaneTitle, { label: "TRAFFIC", extra: `${trafficLabel} \xB7 peak ${Math.max(0, ...traffic)} \xB7 red = denials`, width: leftW }),
773
990
  /* @__PURE__ */ jsx2(ColumnChart, { series: traffic, hot: trafficHot, height: chartH, width: leftW, color: theme.accent })
@@ -777,77 +994,56 @@ function LivePanel({ active: active2 }) {
777
994
  /* @__PURE__ */ jsx2(ColumnChart, { series: lat, hot: lat.map((v) => v > latHotAt), height: chartH, width: rightW, color: "#82AAF0", hotColor: "#ffb454" })
778
995
  ] })
779
996
  ] }),
780
- /* @__PURE__ */ jsxs2(Box2, { children: [
781
- /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, marginRight: 2, children: [
997
+ /* @__PURE__ */ jsxs2(Box2, { height: colH, children: [
998
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, marginRight: 2, overflow: "hidden", children: [
782
999
  /* @__PURE__ */ jsx2(PaneTitle, { label: "LAYERS", width: colW }),
783
1000
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
784
1001
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "RATELIMIT ".padEnd(10) }),
785
1002
  /* @__PURE__ */ jsx2(Text2, { color: rl?.mode === "block" ? theme.ok : rl?.mode === "detect" ? theme.warn : theme.dim, children: (rl?.mode ?? "?").padEnd(7) }),
786
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: rl?.perMinute ? `${minuteNow}/${rl.perMinute} per min` : "no limit set" })
1003
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: rl?.perMinute ? `${minuteNow}/${rl.perMinute}m ${rl.perHour || "\u2014"}h ${rl.perDay || "\u2014"}d` : "no limits set" })
787
1004
  ] }),
788
- rl?.perMinute ? /* @__PURE__ */ jsx2(HBar, { label: "load", value: minuteNow, max: rl.perMinute, width: Math.max(6, colW - 16), color: minuteNow > rl.perMinute ? theme.bad : theme.accent }) : null,
1005
+ rl?.perMinute ? /* @__PURE__ */ jsx2(HBar, { label: "load/min", value: minuteNow, max: rl.perMinute, width: Math.max(6, colW - 16), color: minuteNow > rl.perMinute ? theme.bad : theme.accent }) : null,
789
1006
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
790
1007
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "DLP".padEnd(10) }),
791
1008
  /* @__PURE__ */ jsx2(Text2, { color: dl?.mode === "block" ? theme.ok : dl?.mode === "detect" ? theme.warn : theme.dim, children: (dl?.mode ?? "?").padEnd(7) }),
792
- /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
793
- (dl?.patterns ?? []).length,
794
- " patterns armed"
795
- ] })
1009
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: `${(dl?.patterns ?? []).length} builtin \xB7 ${(dl?.custom ?? []).length} custom` })
796
1010
  ] }),
797
- dlpBars.length ? dlpBars.map((d) => /* @__PURE__ */ jsx2(HBar, { label: truncate(d.pattern, 10), value: d.count, max: maxDlpBar, width: Math.max(6, colW - 16), color: theme.bad }, d.pattern)) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " no dlp hits in last 7 days" }),
798
- ring ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
1011
+ dlpBars.length ? dlpBars.map((d2) => /* @__PURE__ */ jsx2(HBar, { label: truncate(d2.pattern, 10), value: d2.count, max: maxDlpBar, width: Math.max(6, colW - 16), color: theme.bad }, d2.pattern)) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " no dlp hits in last 7 days" }),
1012
+ /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
799
1013
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "GUARD".padEnd(10) }),
800
- /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "local \u2713" }),
801
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ` eval avg ${ring.avgMs}ms \xB7 ${ring.count} recent` })
802
- ] }) : null
1014
+ ring ? /* @__PURE__ */ jsxs2(Text2, { color: theme.ok, children: [
1015
+ "local \u2713 ",
1016
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: `eval avg ${ring.avgMs}ms` })
1017
+ ] }) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "no local ring in cwd" })
1018
+ ] })
803
1019
  ] }),
804
- /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, marginRight: 2, children: [
805
- /* @__PURE__ */ jsx2(PaneTitle, { label: "SESSIONS", extra: `${localSessList.length} local \xB7 ${sessCounts?.active ?? 0} cloud live`, width: colW }),
806
- localSessList.length === 0 && sess.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "scanning\u2026" }) : null,
807
- localSessList.slice(0, Math.max(1, colH - 2)).map(([id, v]) => {
808
- const fresh = nowMs - v.lastAt < 9e4;
809
- const isMe = ring?.session === id;
810
- return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
811
- /* @__PURE__ */ jsxs2(Text2, { color: fresh ? theme.ok : theme.dim, children: [
812
- fresh ? "\u25CF" : "\u25CB",
813
- " "
814
- ] }),
815
- /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "LOC " }),
816
- /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, bold: isMe, children: truncate(isMe ? "this machine" : v.agent, 12).padEnd(13) }),
817
- /* @__PURE__ */ jsxs2(Text2, { children: [
818
- String(v.calls).padStart(4),
819
- "c "
820
- ] }),
821
- /* @__PURE__ */ jsxs2(Text2, { color: v.denies ? theme.bad : theme.dim, children: [
822
- String(v.denies).padStart(3),
823
- "d "
824
- ] }),
825
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ago(v.lastAt) })
826
- ] }, id);
827
- }),
828
- sess.slice(0, Math.max(0, colH - 1 - Math.min(localSessList.length, Math.max(1, colH - 2)))).map((a) => {
829
- const dot = sessionDot(a.status);
830
- return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
831
- /* @__PURE__ */ jsxs2(Text2, { color: dot.color, children: [
832
- dot.ch,
1020
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, marginRight: 2, overflow: "hidden", children: [
1021
+ /* @__PURE__ */ jsx2(PaneTitle, { label: "SESSIONS", extra: mode === "pick" ? "\u2191\u2193 pick \xB7 enter open" : `${combinedSess.length} \xB7 s = inspect`, width: colW }),
1022
+ pickable.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "scanning\u2026" }) : null,
1023
+ pickable.map((r, i) => {
1024
+ const freshDot = nowMs - r.lastAt < 9e4;
1025
+ const sel = mode === "pick" && i === pickIdx;
1026
+ return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", backgroundColor: sel ? "#1c2f63" : void 0, bold: sel || r.isMe, children: [
1027
+ /* @__PURE__ */ jsxs2(Text2, { color: sel ? theme.accentBright : freshDot ? theme.ok : theme.dim, children: [
1028
+ sel ? "\u25B8" : freshDot ? "\u25CF" : "\u25CB",
833
1029
  " "
834
1030
  ] }),
835
- /* @__PURE__ */ jsx2(Text2, { color: "#4f6db8", children: "CLD " }),
836
- /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate(a.agent_name ?? a.session_id, 12).padEnd(13) }),
1031
+ /* @__PURE__ */ jsx2(Text2, { color: r.source === "local" ? theme.ok : "#4f6db8", children: r.source === "local" ? "LOC " : "CLD " }),
1032
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, children: truncate(r.isMe ? "this machine" : r.agent, 12).padEnd(13) }),
837
1033
  /* @__PURE__ */ jsxs2(Text2, { children: [
838
- String(a.total_calls).padStart(4),
1034
+ String(r.calls).padStart(4),
839
1035
  "c "
840
1036
  ] }),
841
- /* @__PURE__ */ jsxs2(Text2, { color: a.denied_calls ? theme.bad : theme.dim, children: [
842
- String(a.denied_calls).padStart(3),
1037
+ /* @__PURE__ */ jsxs2(Text2, { color: r.denies ? theme.bad : theme.dim, children: [
1038
+ String(r.denies).padStart(3),
843
1039
  "d "
844
1040
  ] }),
845
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ago(a.last_seen_at) })
846
- ] }, a.session_id);
1041
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: (r.trust != null ? `t${r.trust} ` : "") + ago(r.lastAt) })
1042
+ ] }, r.id);
847
1043
  })
848
1044
  ] }),
849
- /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, children: [
850
- /* @__PURE__ */ jsx2(PaneTitle, { label: "EVENT LOG", extra: "streaming", width: colW }),
1045
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, overflow: "hidden", children: [
1046
+ /* @__PURE__ */ jsx2(PaneTitle, { label: "EVENT LOG", extra: "system heartbeat", width: colW }),
851
1047
  log.slice(-(colH - 1)).map((l, i) => /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
852
1048
  /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
853
1049
  hhmmss(l.ts),
@@ -858,33 +1054,29 @@ function LivePanel({ active: active2 }) {
858
1054
  ] }, l.ts + ":" + i))
859
1055
  ] })
860
1056
  ] }),
861
- /* @__PURE__ */ jsx2(PaneTitle, { label: "TOOL STREAM", extra: `local+cloud tail \xB7 ${merged.length} buffered`, width: innerW }),
862
- localOn === false ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
863
- /* @__PURE__ */ jsx2(Text2, { color: theme.warn, children: "local logs off" }),
864
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2014 enable in dashboard \u2192 Settings \u2192 Local logs; hooks then write ~/.solongate/local-logs (view them there with `solongate logs-server`)" })
865
- ] }) : null,
866
- tail.length === 0 ? /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
867
- spin,
868
- " awaiting traffic\u2026"
869
- ] }) : null,
870
- tail.map((e) => /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
871
- /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
872
- "[",
873
- hhmmss(e.at),
874
- " "
875
- ] }),
876
- /* @__PURE__ */ jsx2(Text2, { color: e.source === "local" ? theme.ok : "#4f6db8", children: e.source === "local" ? "LOC" : "CLD" }),
877
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "] " }),
878
- /* @__PURE__ */ jsx2(Text2, { color: decisionColor(e.decision), bold: e.decision !== "ALLOW", children: e.decision.padEnd(6) }),
879
- /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate(e.tool, 14).padEnd(15) }),
880
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: e.permission.padEnd(5) }),
881
- e.dlp ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "DLP! " }) : null,
882
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: e.detail })
883
- ] }, e.id)),
1057
+ /* @__PURE__ */ jsx2(
1058
+ PaneTitle,
1059
+ {
1060
+ label: "TOOL STREAM",
1061
+ extra: `newest first \xB7 last ${STREAM_KEEP} \xB7 filter ${filter}${clampedScroll > 0 ? ` \xB7 \u25BC${clampedScroll} older` : ""} \xB7 f filter \xB7 \u2191\u2193 scroll \xB7 c copy \xB7 s sessions`,
1062
+ width: innerW
1063
+ }
1064
+ ),
1065
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: streamRows, overflow: "hidden", children: [
1066
+ localOn === false ? /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
1067
+ /* @__PURE__ */ jsx2(Text2, { color: theme.warn, children: "local logs off" }),
1068
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2014 enable: dashboard \u2192 Settings \u2192 Local logs \xB7 hooks write ~/.solongate/local-logs \xB7 view: `solongate logs-server`" })
1069
+ ] }) : null,
1070
+ windowed.length === 0 ? /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
1071
+ spin,
1072
+ " awaiting traffic\u2026"
1073
+ ] }) : null,
1074
+ windowed.map((e) => /* @__PURE__ */ jsx2(StreamLine, { e }, e.id))
1075
+ ] }),
884
1076
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
885
1077
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", bold: true, children: " LIVE " }),
886
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#0b1530", color: "#4f6db8", children: ` local ${localOn ? "on" : "off"} \xB7 top ${topTools.map(([t, c2]) => `${t}\xD7${c2}`).join(" ") || "\u2014"} \xB7 buf ${merged.length} ` }),
887
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: " p pause/copy \xB7 esc menu \xB7 q quit " })
1078
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#0b1530", color: "#4f6db8", children: ` local ${localOn ? "on" : "off"} \xB7 ${localBuf.length} loc/${cloudBuf.length} cld \xB7 top ${topTools.map(([t, c2]) => `${t}\xD7${c2}`).join(" ") || "\u2014"} ` }),
1079
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: " f filter \xB7 s sessions \xB7 c copy \xB7 esc menu \xB7 q quit " })
888
1080
  ] })
889
1081
  ] });
890
1082
  }