@solongate/proxy 0.69.0 → 0.71.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.
@@ -84,17 +84,27 @@ async function request(method, path, opts = {}) {
84
84
  headers["Content-Type"] = "application/json";
85
85
  bodyInit = JSON.stringify(opts.body);
86
86
  }
87
+ const attempt = () => fetch(url, {
88
+ method,
89
+ headers,
90
+ body: bodyInit,
91
+ signal: AbortSignal.timeout(opts.timeoutMs ?? 15e3)
92
+ });
87
93
  let res;
88
94
  try {
89
- res = await fetch(url, {
90
- method,
91
- headers,
92
- body: bodyInit,
93
- signal: AbortSignal.timeout(opts.timeoutMs ?? 15e3)
94
- });
95
- } catch (err2) {
96
- const msg = err2 instanceof Error ? err2.message : String(err2);
97
- throw new ApiError(0, "NETWORK_ERROR", `Cannot reach SolonGate API: ${msg}`);
95
+ res = await attempt();
96
+ } catch (firstErr) {
97
+ if (method !== "GET") {
98
+ const msg = firstErr instanceof Error ? firstErr.message : String(firstErr);
99
+ throw new ApiError(0, "NETWORK_ERROR", `Cannot reach SolonGate API: ${msg}`);
100
+ }
101
+ await new Promise((r) => setTimeout(r, 800));
102
+ try {
103
+ res = await attempt();
104
+ } catch (err2) {
105
+ const msg = err2 instanceof Error ? err2.message : String(err2);
106
+ throw new ApiError(0, "NETWORK_ERROR", `Cannot reach SolonGate API: ${msg}`);
107
+ }
98
108
  }
99
109
  const text = await res.text().catch(() => "");
100
110
  let json = void 0;
package/dist/index.js CHANGED
@@ -6633,17 +6633,27 @@ async function request(method, path, opts = {}) {
6633
6633
  headers["Content-Type"] = "application/json";
6634
6634
  bodyInit = JSON.stringify(opts.body);
6635
6635
  }
6636
+ const attempt = () => fetch(url, {
6637
+ method,
6638
+ headers,
6639
+ body: bodyInit,
6640
+ signal: AbortSignal.timeout(opts.timeoutMs ?? 15e3)
6641
+ });
6636
6642
  let res;
6637
6643
  try {
6638
- res = await fetch(url, {
6639
- method,
6640
- headers,
6641
- body: bodyInit,
6642
- signal: AbortSignal.timeout(opts.timeoutMs ?? 15e3)
6643
- });
6644
- } catch (err2) {
6645
- const msg = err2 instanceof Error ? err2.message : String(err2);
6646
- throw new ApiError(0, "NETWORK_ERROR", `Cannot reach SolonGate API: ${msg}`);
6644
+ res = await attempt();
6645
+ } catch (firstErr) {
6646
+ if (method !== "GET") {
6647
+ const msg = firstErr instanceof Error ? firstErr.message : String(firstErr);
6648
+ throw new ApiError(0, "NETWORK_ERROR", `Cannot reach SolonGate API: ${msg}`);
6649
+ }
6650
+ await new Promise((r) => setTimeout(r, 800));
6651
+ try {
6652
+ res = await attempt();
6653
+ } catch (err2) {
6654
+ const msg = err2 instanceof Error ? err2.message : String(err2);
6655
+ throw new ApiError(0, "NETWORK_ERROR", `Cannot reach SolonGate API: ${msg}`);
6656
+ }
6647
6657
  }
6648
6658
  const text = await res.text().catch(() => "");
6649
6659
  let json = void 0;
@@ -7020,6 +7030,7 @@ var init_hooks = __esm({
7020
7030
 
7021
7031
  // src/tui/panels/Live.tsx
7022
7032
  import { Box as Box2, Text as Text2, useInput } from "ink";
7033
+ import { spawnSync } from "child_process";
7023
7034
  import { closeSync, mkdirSync as mkdirSync3, openSync, readSync, statSync, writeFileSync as writeFileSync3 } from "fs";
7024
7035
  import { homedir as homedir3 } from "os";
7025
7036
  import { dirname, join as join5 } from "path";
@@ -7040,8 +7051,21 @@ function tailLines(file, maxBytes = 131072) {
7040
7051
  return [];
7041
7052
  }
7042
7053
  }
7043
- function oscCopy(text) {
7054
+ function copyToClipboard(text) {
7055
+ const tools = [
7056
+ ["wl-copy", []],
7057
+ ["xclip", ["-selection", "clipboard"]],
7058
+ ["xsel", ["--clipboard", "--input"]]
7059
+ ];
7060
+ for (const [cmd, args] of tools) {
7061
+ try {
7062
+ const r = spawnSync(cmd, args, { input: text, timeout: 2e3 });
7063
+ if (r.status === 0) return cmd;
7064
+ } catch {
7065
+ }
7066
+ }
7044
7067
  process.stdout.write(`\x1B]52;c;${Buffer.from(text, "utf-8").toString("base64")}\x07`);
7068
+ return null;
7045
7069
  }
7046
7070
  function cloudItem(e) {
7047
7071
  return {
@@ -7107,6 +7131,25 @@ function PaneTitle({ label, extra, width: width2 }) {
7107
7131
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: tail + "\u2500".repeat(Math.max(0, width2 - used)) })
7108
7132
  ] });
7109
7133
  }
7134
+ function StreamLine({ e, loc }) {
7135
+ return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7136
+ /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
7137
+ "[",
7138
+ hhmmss(e.at),
7139
+ " "
7140
+ ] }),
7141
+ /* @__PURE__ */ jsx2(Text2, { color: loc ? theme.ok : "#4f6db8", children: loc ? "LOC" : "CLD" }),
7142
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "] " }),
7143
+ /* @__PURE__ */ jsx2(Text2, { color: decisionColor(e.decision), bold: e.decision !== "ALLOW", children: e.decision.padEnd(6) }),
7144
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate2(e.tool, 12).padEnd(13) }),
7145
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: e.permission.padEnd(5) }),
7146
+ /* @__PURE__ */ jsx2(Text2, { color: e.evalMs != null && e.evalMs > 500 ? theme.warn : theme.dim, children: (e.evalMs != null ? `${e.evalMs}ms` : "\u2014").padEnd(7) }),
7147
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: truncate2(e.agent ?? "-", 11).padEnd(12) }),
7148
+ e.dlp ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "DLP! " }) : null,
7149
+ e.rule && e.decision !== "ALLOW" ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: truncate2(e.rule, 14) + " " }) : null,
7150
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: e.detail })
7151
+ ] });
7152
+ }
7110
7153
  function LivePanel({ active: active2 }) {
7111
7154
  const [s, setS] = useState2(null);
7112
7155
  const [tsData, setTsData] = useState2(null);
@@ -7119,6 +7162,11 @@ function LivePanel({ active: active2 }) {
7119
7162
  const [filter, setFilter] = useState2("all");
7120
7163
  const [scroll, setScroll] = useState2(0);
7121
7164
  const [toast, setToast] = useState2(null);
7165
+ const [mode, setMode] = useState2("stream");
7166
+ const [pickIdx, setPickIdx] = useState2(0);
7167
+ const [detail, setDetail] = useState2(null);
7168
+ const [detailCloud, setDetailCloud] = useState2([]);
7169
+ const [detailScroll, setDetailScroll] = useState2(0);
7122
7170
  const seenRef = useRef2(/* @__PURE__ */ new Set());
7123
7171
  const lastLocalTs = useRef2(0);
7124
7172
  const pausedUntil = useRef2(0);
@@ -7212,7 +7260,8 @@ function LivePanel({ active: active2 }) {
7212
7260
  if (fresh.length) setCloudBuf((prev) => [...prev, ...fresh.map(cloudItem).sort((a, b) => a.at - b.at)].slice(-400));
7213
7261
  const denies = fresh.filter((e) => e.decision !== "ALLOW").length;
7214
7262
  if (firstLoad) pushLog(`cloud link up \xB7 api ${ms}ms \xB7 ${fresh.length} calls`);
7215
- else pushLog(`api ${ms}ms \xB7 +${fresh.length} cloud${denies ? ` \xB7 ${denies} DENIED` : ""}`, denies ? "bad" : fresh.length ? "warn" : "ok");
7263
+ else if (fresh.length) pushLog(`api ${ms}ms \xB7 +${fresh.length} cloud${denies ? ` \xB7 ${denies} DENIED` : ""}`, denies ? "bad" : "warn");
7264
+ else pushLog(`api ${ms}ms \xB7 idle`, "ok");
7216
7265
  } catch (e) {
7217
7266
  onApiError(e);
7218
7267
  }
@@ -7259,6 +7308,19 @@ function LivePanel({ active: active2 }) {
7259
7308
  usePoll(() => {
7260
7309
  if (!paused()) guard.reload();
7261
7310
  }, 6e4, active2);
7311
+ useEffect2(() => {
7312
+ if (!detail) return;
7313
+ setDetailCloud([]);
7314
+ setDetailScroll(0);
7315
+ let alive = true;
7316
+ api.audit.list({ session_id: detail.id, limit: 100 }).then((r) => {
7317
+ if (alive) setDetailCloud(r.entries.map(cloudItem));
7318
+ }).catch(() => {
7319
+ });
7320
+ return () => {
7321
+ alive = false;
7322
+ };
7323
+ }, [detail]);
7262
7324
  const [tick, setTick] = useState2(0);
7263
7325
  useEffect2(() => {
7264
7326
  if (!active2) return;
@@ -7271,14 +7333,19 @@ function LivePanel({ active: active2 }) {
7271
7333
  const ins = insights.data ?? {};
7272
7334
  const spin = SPIN[tick % SPIN.length];
7273
7335
  const nowMs = Date.now();
7274
- const mergedAll = [...cloudBuf, ...localBuf].sort((a, b) => a.at - b.at);
7275
- const filtered = filter === "all" ? mergedAll : mergedAll.filter((e) => e.source === filter);
7276
- const visible = filtered.slice(-STREAM_KEEP);
7336
+ const localKeys = /* @__PURE__ */ new Set();
7337
+ for (const e of localBuf) {
7338
+ const m = Math.round(e.at / 6e4);
7339
+ for (const d of [-1, 0, 1]) localKeys.add(`${e.tool}|${e.decision}|${m + d}`);
7340
+ }
7341
+ const cloudDeduped = cloudBuf.filter((e) => !localKeys.has(`${e.tool}|${e.decision}|${Math.round(e.at / 6e4)}`));
7342
+ const mergedAll = [...cloudDeduped, ...localBuf].sort((a, b) => a.at - b.at);
7277
7343
  const points = tsData?.timeseries ?? [];
7344
+ const nowMin = Math.floor(nowMs / 6e4);
7278
7345
  const minuteCounts = new Array(60).fill(0);
7279
7346
  const minuteHot = new Array(60).fill(false);
7280
7347
  for (const e of mergedAll) {
7281
- const idx = 59 - Math.floor((nowMs - e.at) / 6e4);
7348
+ const idx = 59 - (nowMin - Math.floor(e.at / 6e4));
7282
7349
  if (idx >= 0 && idx < 60) {
7283
7350
  minuteCounts[idx]++;
7284
7351
  if (e.decision !== "ALLOW") minuteHot[idx] = true;
@@ -7294,8 +7361,8 @@ function LivePanel({ active: active2 }) {
7294
7361
  const minuteNow = minuteCounts[59] ?? 0;
7295
7362
  const dlpBars = (ins.dlpByPattern ?? []).slice(0, 2);
7296
7363
  const maxDlpBar = dlpBars[0]?.count ?? 1;
7297
- const denials = mergedAll.filter((e) => e.decision !== "ALLOW");
7298
- const lastDeny = denials[denials.length - 1];
7364
+ const denialsAll = mergedAll.filter((e) => e.decision !== "ALLOW");
7365
+ const lastDeny = denialsAll[denialsAll.length - 1];
7299
7366
  const toolCounts = /* @__PURE__ */ new Map();
7300
7367
  for (const e of mergedAll) toolCounts.set(e.tool, (toolCounts.get(e.tool) ?? 0) + 1);
7301
7368
  const topTools = [...toolCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
@@ -7316,7 +7383,24 @@ function LivePanel({ active: active2 }) {
7316
7383
  if (e.agent) cur.agent = e.agent;
7317
7384
  localSess.set(e.session, cur);
7318
7385
  }
7319
- const localSessList = [...localSess.entries()].sort((a, b) => b[1].lastAt - a[1].lastAt);
7386
+ const combinedSess = [
7387
+ ...[...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 })),
7388
+ ...sess.filter((a) => !localSess.has(a.session_id)).map((a) => ({
7389
+ source: "cloud",
7390
+ id: a.session_id,
7391
+ agent: a.agent_name ?? a.session_id,
7392
+ calls: a.total_calls,
7393
+ denies: a.denied_calls,
7394
+ lastAt: Date.parse(a.last_seen_at),
7395
+ trust: a.trust_score,
7396
+ isMe: false
7397
+ }))
7398
+ ];
7399
+ const localSessIds = new Set(localSess.keys());
7400
+ if (ring?.session) localSessIds.add(ring.session);
7401
+ const isLoc = (e) => e.source === "local" || !!e.session && localSessIds.has(e.session);
7402
+ const filtered = filter === "all" ? mergedAll : mergedAll.filter((e) => filter === "local" ? isLoc(e) : !isLoc(e));
7403
+ const visibleDesc = filtered.slice(-STREAM_KEEP).reverse();
7320
7404
  const chartH = rows >= 36 ? 6 : 4;
7321
7405
  const colH = rows >= 32 ? 7 : 5;
7322
7406
  const streamRows = Math.max(4, rows - 5 - (1 + chartH) - colH);
@@ -7324,21 +7408,58 @@ function LivePanel({ active: active2 }) {
7324
7408
  const leftW = Math.floor(innerW * 0.55);
7325
7409
  const rightW = innerW - leftW - 2;
7326
7410
  const colW = Math.max(20, Math.floor((innerW - 4) / 3));
7327
- const maxScroll = Math.max(0, visible.length - streamRows);
7411
+ const pickable = combinedSess.slice(0, colH - 1);
7412
+ const maxScroll = Math.max(0, visibleDesc.length - streamRows);
7328
7413
  const clampedScroll = Math.min(scroll, maxScroll);
7329
- const start = Math.max(0, visible.length - streamRows - clampedScroll);
7330
- const windowed = visible.slice(start, start + streamRows);
7414
+ const windowed = visibleDesc.slice(clampedScroll, clampedScroll + streamRows);
7415
+ const detailEntries = detail ? (() => {
7416
+ const seen = /* @__PURE__ */ new Set();
7417
+ const all = [];
7418
+ for (const e of [...detailCloud, ...mergedAll.filter((x) => x.session === detail.id)]) {
7419
+ const key = `${e.at}:${e.tool}:${e.decision}`;
7420
+ if (seen.has(key)) continue;
7421
+ seen.add(key);
7422
+ all.push(e);
7423
+ }
7424
+ return all.sort((a, b) => b.at - a.at);
7425
+ })() : [];
7331
7426
  useInput(
7332
7427
  (input, key) => {
7428
+ if (mode === "detail") {
7429
+ const maxD = Math.max(0, detailEntries.length - (rows - 10));
7430
+ if (key.leftArrow) {
7431
+ setMode("stream");
7432
+ setDetail(null);
7433
+ } else if (key.upArrow) setDetailScroll((n) => Math.max(0, n - 1));
7434
+ else if (key.downArrow) setDetailScroll((n) => Math.min(maxD, n + 1));
7435
+ else if (key.pageUp) setDetailScroll((n) => Math.max(0, n - 10));
7436
+ else if (key.pageDown) setDetailScroll((n) => Math.min(maxD, n + 10));
7437
+ return;
7438
+ }
7439
+ if (mode === "pick") {
7440
+ if (key.upArrow) setPickIdx((n) => Math.max(0, n - 1));
7441
+ else if (key.downArrow) setPickIdx((n) => Math.min(pickable.length - 1, n + 1));
7442
+ else if (key.return) {
7443
+ const row = pickable[pickIdx];
7444
+ if (row) {
7445
+ setDetail(row);
7446
+ setMode("detail");
7447
+ }
7448
+ } else if (input === "s" || key.leftArrow) setMode("stream");
7449
+ return;
7450
+ }
7333
7451
  if (input === "f") {
7334
7452
  setFilter((f) => FILTERS[(FILTERS.indexOf(f) + 1) % FILTERS.length]);
7335
7453
  setScroll(0);
7336
- } else if (key.upArrow) setScroll((n) => Math.min(maxScroll, n + 1));
7337
- else if (key.downArrow) setScroll((n) => Math.max(0, n - 1));
7338
- else if (key.pageUp) setScroll((n) => Math.min(maxScroll, n + streamRows));
7339
- else if (key.pageDown) setScroll((n) => Math.max(0, n - streamRows));
7454
+ } else if (input === "s") {
7455
+ setPickIdx(0);
7456
+ setMode("pick");
7457
+ } else if (key.downArrow) setScroll((n) => Math.min(maxScroll, n + 1));
7458
+ else if (key.upArrow) setScroll((n) => Math.max(0, n - 1));
7459
+ else if (key.pageDown) setScroll((n) => Math.min(maxScroll, n + streamRows));
7460
+ else if (key.pageUp) setScroll((n) => Math.max(0, n - streamRows));
7340
7461
  else if (input === "c") {
7341
- const dump = visible.map(
7462
+ const dump = visibleDesc.map(
7342
7463
  (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}`
7343
7464
  ).join("\n");
7344
7465
  try {
@@ -7346,26 +7467,96 @@ function LivePanel({ active: active2 }) {
7346
7467
  writeFileSync3(DUMP, dump + "\n");
7347
7468
  } catch {
7348
7469
  }
7349
- oscCopy(dump);
7350
- setToast({ msg: `copied ${visible.length} lines \u2192 clipboard + ${DUMP}`, until: Date.now() + 4e3 });
7470
+ const via = copyToClipboard(dump);
7471
+ setToast(
7472
+ via ? { msg: `\u2713 ${visibleDesc.length} lines in clipboard (${via})`, until: Date.now() + 4e3 } : { msg: "\u2717 no clipboard tool \u2014 install wl-clipboard or xclip", until: Date.now() + 6e3 }
7473
+ );
7351
7474
  }
7352
7475
  },
7353
7476
  { isActive: active2 }
7354
7477
  );
7355
7478
  const sessionDot = (st) => st === "active" ? { ch: "\u25CF", color: theme.ok } : st === "idle" ? { ch: "\u25CC", color: theme.warn } : { ch: "\u25CB", color: theme.dim };
7479
+ void sessionDot;
7356
7480
  if (s === null && localOn === null) {
7357
7481
  return /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
7358
7482
  spin,
7359
7483
  " connecting\u2026"
7360
7484
  ] });
7361
7485
  }
7486
+ const titleBar = /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7487
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#1432A0", color: "#AAC8FA", bold: true, children: " SOLONGATE LIVE " }),
7488
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: ` ${spin} up ${fmtUp(nowMs - startRef.current)} \xB7 ${hhmmss(nowMs)} \xB7 api ${latNow}ms ` }),
7489
+ 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 " }),
7490
+ toast && nowMs < toast.until ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#123d1f", color: "#7bd88f", bold: true, children: ` ${toast.msg} ` }) : null
7491
+ ] });
7492
+ if (mode === "detail" && detail) {
7493
+ const d = detailEntries;
7494
+ const allow = d.filter((e) => e.decision === "ALLOW").length;
7495
+ const deny = d.length - allow;
7496
+ const dlpN = d.filter((e) => e.dlp).length;
7497
+ const evals = d.filter((e) => e.evalMs != null).map((e) => e.evalMs);
7498
+ const evalAvg = evals.length ? Math.round(evals.reduce((a, b) => a + b, 0) / evals.length) : null;
7499
+ const tCounts = /* @__PURE__ */ new Map();
7500
+ const pCounts = /* @__PURE__ */ new Map();
7501
+ for (const e of d) {
7502
+ tCounts.set(e.tool, (tCounts.get(e.tool) ?? 0) + 1);
7503
+ if (e.permission) pCounts.set(e.permission, (pCounts.get(e.permission) ?? 0) + 1);
7504
+ }
7505
+ const top = [...tCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 6);
7506
+ const perms = [...pCounts.entries()].sort((a, b) => b[1] - a[1]);
7507
+ const first = d[d.length - 1]?.at;
7508
+ const last = d[0]?.at;
7509
+ const tlRows = Math.max(4, rows - 10);
7510
+ const maxD = Math.max(0, d.length - tlRows);
7511
+ const dScroll = Math.min(detailScroll, maxD);
7512
+ const tl = d.slice(dScroll, dScroll + tlRows);
7513
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
7514
+ titleBar,
7515
+ /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7516
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#AAC8FA", bold: true, children: ` SESSION ${detail.id.slice(0, 8)} ` }),
7517
+ /* @__PURE__ */ jsx2(Text2, { color: detail.source === "local" ? theme.ok : "#4f6db8", children: " " + detail.source.toUpperCase() }),
7518
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: " " + truncate2(detail.isMe ? detail.agent + " (this machine)" : detail.agent, 34) }),
7519
+ detail.trust != null ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ` trust ${detail.trust}/100` }) : null,
7520
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2190 back" })
7521
+ ] }),
7522
+ /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7523
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "\u2502 calls " }),
7524
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: d.length }),
7525
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 allow " }),
7526
+ /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: allow }),
7527
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 deny " }),
7528
+ /* @__PURE__ */ jsx2(Text2, { color: deny ? theme.bad : theme.dim, bold: deny > 0, children: deny }),
7529
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 dlp " }),
7530
+ /* @__PURE__ */ jsx2(Text2, { color: dlpN ? theme.bad : theme.dim, children: dlpN }),
7531
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 avg eval " }),
7532
+ /* @__PURE__ */ jsx2(Text2, { children: evalAvg != null ? `${evalAvg}ms` : "\u2014" }),
7533
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 first " }),
7534
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: first ? `${hhmmss(first)} (${ago(first)} ago)` : "\u2014" }),
7535
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 last " }),
7536
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: last ? `${hhmmss(last)} (${ago(last)} ago)` : "\u2014" }),
7537
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
7538
+ ] }),
7539
+ /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7540
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "\u2502 tools " }),
7541
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: top.map(([t, c2]) => `${t}\xD7${c2}`).join(" ") || "\u2014" }),
7542
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 perms " }),
7543
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: perms.map(([p, c2]) => `${p}\xD7${c2}`).join(" ") || "\u2014" }),
7544
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
7545
+ ] }),
7546
+ /* @__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 }),
7547
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: tlRows, overflow: "hidden", children: [
7548
+ tl.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: detailCloud.length === 0 ? spin + " loading history\u2026" : "no entries" }) : null,
7549
+ tl.map((e) => /* @__PURE__ */ jsx2(StreamLine, { e, loc: isLoc(e) }, e.id))
7550
+ ] }),
7551
+ /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7552
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", bold: true, children: " SESSION " }),
7553
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#0b1530", color: "#4f6db8", children: ` ${detail.id} ` }),
7554
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: " \u2191\u2193 scroll \xB7 \u2190 back \xB7 esc menu " })
7555
+ ] })
7556
+ ] });
7557
+ }
7362
7558
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
7363
- /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7364
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#1432A0", color: "#AAC8FA", bold: true, children: " SOLONGATE LIVE " }),
7365
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: ` ${spin} up ${fmtUp(nowMs - startRef.current)} \xB7 ${hhmmss(nowMs)} \xB7 api ${latNow}ms ` }),
7366
- 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 " }),
7367
- toast && nowMs < toast.until ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#123d1f", color: "#7bd88f", bold: true, children: ` ${toast.msg} ` }) : null
7368
- ] }),
7559
+ titleBar,
7369
7560
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7370
7561
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "\u2502 calls " }),
7371
7562
  /* @__PURE__ */ jsx2(Text2, { bold: true, children: s ? s.total_calls : "\xB7\xB7\xB7\xB7" }),
@@ -7382,7 +7573,7 @@ function LivePanel({ active: active2 }) {
7382
7573
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 ghost " }),
7383
7574
  /* @__PURE__ */ jsx2(Text2, { color: gh?.mode === "on" ? theme.ok : theme.dim, children: gh?.mode ?? "?" }),
7384
7575
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 sess " }),
7385
- /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: `${localSessList.length}loc ${sessCounts?.active ?? 0}\u25B2${sessCounts?.idle ?? 0}\u25CC` }),
7576
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: `${combinedSess.length} (${sessCounts?.active ?? 0} live)` }),
7386
7577
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 hooks " }),
7387
7578
  /* @__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" }),
7388
7579
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
@@ -7411,7 +7602,7 @@ function LivePanel({ active: active2 }) {
7411
7602
  /* @__PURE__ */ jsx2(Text2, { color: dl?.mode === "block" ? theme.ok : dl?.mode === "detect" ? theme.warn : theme.dim, children: (dl?.mode ?? "?").padEnd(7) }),
7412
7603
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: `${(dl?.patterns ?? []).length} builtin \xB7 ${(dl?.custom ?? []).length} custom` })
7413
7604
  ] }),
7414
- dlpBars.length ? dlpBars.map((d) => /* @__PURE__ */ jsx2(HBar, { label: truncate2(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" }),
7605
+ dlpBars.length ? dlpBars.map((d2) => /* @__PURE__ */ jsx2(HBar, { label: truncate2(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" }),
7415
7606
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7416
7607
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "GUARD".padEnd(10) }),
7417
7608
  ring ? /* @__PURE__ */ jsxs2(Text2, { color: theme.ok, children: [
@@ -7421,48 +7612,28 @@ function LivePanel({ active: active2 }) {
7421
7612
  ] })
7422
7613
  ] }),
7423
7614
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, marginRight: 2, overflow: "hidden", children: [
7424
- /* @__PURE__ */ jsx2(PaneTitle, { label: "SESSIONS", extra: `${localSessList.length} local \xB7 ${sessCounts?.active ?? 0} cloud`, width: colW }),
7425
- localSessList.length === 0 && sess.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "scanning\u2026" }) : null,
7426
- localSessList.slice(0, Math.max(1, colH - 2)).map(([id, v]) => {
7427
- const freshDot = nowMs - v.lastAt < 9e4;
7428
- const isMe = ring?.session === id;
7429
- return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7430
- /* @__PURE__ */ jsxs2(Text2, { color: freshDot ? theme.ok : theme.dim, children: [
7431
- freshDot ? "\u25CF" : "\u25CB",
7432
- " "
7433
- ] }),
7434
- /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "LOC " }),
7435
- /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, bold: isMe, children: truncate2(isMe ? "this machine" : v.agent, 12).padEnd(13) }),
7436
- /* @__PURE__ */ jsxs2(Text2, { children: [
7437
- String(v.calls).padStart(4),
7438
- "c "
7439
- ] }),
7440
- /* @__PURE__ */ jsxs2(Text2, { color: v.denies ? theme.bad : theme.dim, children: [
7441
- String(v.denies).padStart(3),
7442
- "d "
7443
- ] }),
7444
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ago(v.lastAt) })
7445
- ] }, id);
7446
- }),
7447
- sess.slice(0, Math.max(0, colH - 1 - Math.min(localSessList.length, Math.max(1, colH - 2)))).map((a) => {
7448
- const dot = sessionDot(a.status);
7449
- return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7450
- /* @__PURE__ */ jsxs2(Text2, { color: dot.color, children: [
7451
- dot.ch,
7615
+ /* @__PURE__ */ jsx2(PaneTitle, { label: "SESSIONS", extra: mode === "pick" ? "\u2191\u2193 pick \xB7 enter open" : `${combinedSess.length} \xB7 s = inspect`, width: colW }),
7616
+ pickable.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "scanning\u2026" }) : null,
7617
+ pickable.map((r, i) => {
7618
+ const freshDot = nowMs - r.lastAt < 9e4;
7619
+ const sel = mode === "pick" && i === pickIdx;
7620
+ return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", backgroundColor: sel ? "#1c2f63" : void 0, bold: sel || r.isMe, children: [
7621
+ /* @__PURE__ */ jsxs2(Text2, { color: sel ? theme.accentBright : freshDot ? theme.ok : theme.dim, children: [
7622
+ sel ? "\u25B8" : freshDot ? "\u25CF" : "\u25CB",
7452
7623
  " "
7453
7624
  ] }),
7454
- /* @__PURE__ */ jsx2(Text2, { color: "#4f6db8", children: "CLD " }),
7455
- /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate2(a.agent_name ?? a.session_id, 12).padEnd(13) }),
7625
+ /* @__PURE__ */ jsx2(Text2, { color: r.source === "local" ? theme.ok : "#4f6db8", children: r.source === "local" ? "LOC " : "CLD " }),
7626
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, children: truncate2(r.isMe ? "this machine" : r.agent, 12).padEnd(13) }),
7456
7627
  /* @__PURE__ */ jsxs2(Text2, { children: [
7457
- String(a.total_calls).padStart(4),
7628
+ String(r.calls).padStart(4),
7458
7629
  "c "
7459
7630
  ] }),
7460
- /* @__PURE__ */ jsxs2(Text2, { color: a.denied_calls ? theme.bad : theme.dim, children: [
7461
- String(a.denied_calls).padStart(3),
7631
+ /* @__PURE__ */ jsxs2(Text2, { color: r.denies ? theme.bad : theme.dim, children: [
7632
+ String(r.denies).padStart(3),
7462
7633
  "d "
7463
7634
  ] }),
7464
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: `t${a.trust_score} ` + ago(a.last_seen_at) })
7465
- ] }, a.session_id);
7635
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: (r.trust != null ? `t${r.trust} ` : "") + ago(r.lastAt) })
7636
+ ] }, r.id);
7466
7637
  })
7467
7638
  ] }),
7468
7639
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, overflow: "hidden", children: [
@@ -7481,7 +7652,7 @@ function LivePanel({ active: active2 }) {
7481
7652
  PaneTitle,
7482
7653
  {
7483
7654
  label: "TOOL STREAM",
7484
- extra: `last ${STREAM_KEEP} \xB7 filter ${filter}${clampedScroll > 0 ? ` \xB7 \u25B2${clampedScroll} scrolled` : " \xB7 following"} \xB7 f filter \xB7 \u2191\u2193 scroll \xB7 c copy`,
7655
+ 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`,
7485
7656
  width: innerW
7486
7657
  }
7487
7658
  ),
@@ -7494,28 +7665,12 @@ function LivePanel({ active: active2 }) {
7494
7665
  spin,
7495
7666
  " awaiting traffic\u2026"
7496
7667
  ] }) : null,
7497
- windowed.map((e) => /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7498
- /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
7499
- "[",
7500
- hhmmss(e.at),
7501
- " "
7502
- ] }),
7503
- /* @__PURE__ */ jsx2(Text2, { color: e.source === "local" ? theme.ok : "#4f6db8", children: e.source === "local" ? "LOC" : "CLD" }),
7504
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "] " }),
7505
- /* @__PURE__ */ jsx2(Text2, { color: decisionColor(e.decision), bold: e.decision !== "ALLOW", children: e.decision.padEnd(6) }),
7506
- /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate2(e.tool, 12).padEnd(13) }),
7507
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: e.permission.padEnd(5) }),
7508
- /* @__PURE__ */ jsx2(Text2, { color: e.evalMs != null && e.evalMs > 500 ? theme.warn : theme.dim, children: (e.evalMs != null ? `${e.evalMs}ms` : "\u2014").padEnd(7) }),
7509
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: truncate2(e.agent ?? "-", 11).padEnd(12) }),
7510
- e.dlp ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "DLP! " }) : null,
7511
- e.rule && e.decision !== "ALLOW" ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: truncate2(e.rule, 14) + " " }) : null,
7512
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: e.detail })
7513
- ] }, e.id))
7668
+ windowed.map((e) => /* @__PURE__ */ jsx2(StreamLine, { e, loc: isLoc(e) }, e.id))
7514
7669
  ] }),
7515
7670
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
7516
7671
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", bold: true, children: " LIVE " }),
7517
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#0b1530", color: "#4f6db8", children: ` local ${localOn ? "on" : "off"} \xB7 ${localBuf.length} loc/${cloudBuf.length} cld buffered \xB7 top ${topTools.map(([t, c2]) => `${t}\xD7${c2}`).join(" ") || "\u2014"} ` }),
7518
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: " f filter \xB7 \u2191\u2193 scroll \xB7 c copy \xB7 esc menu \xB7 q quit " })
7672
+ /* @__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"} ` }),
7673
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: " f filter \xB7 s sessions \xB7 c copy \xB7 esc menu \xB7 q quit " })
7519
7674
  ] })
7520
7675
  ] });
7521
7676
  }
package/dist/tui/index.js CHANGED
@@ -126,6 +126,7 @@ 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 { spawnSync } from "child_process";
129
130
  import { closeSync, mkdirSync, openSync, readSync, statSync, writeFileSync } from "fs";
130
131
  import { homedir as homedir2 } from "os";
131
132
  import { dirname, join as join2 } from "path";
@@ -219,17 +220,27 @@ async function request(method, path, opts = {}) {
219
220
  headers["Content-Type"] = "application/json";
220
221
  bodyInit = JSON.stringify(opts.body);
221
222
  }
223
+ const attempt = () => fetch(url, {
224
+ method,
225
+ headers,
226
+ body: bodyInit,
227
+ signal: AbortSignal.timeout(opts.timeoutMs ?? 15e3)
228
+ });
222
229
  let res;
223
230
  try {
224
- res = await fetch(url, {
225
- method,
226
- headers,
227
- body: bodyInit,
228
- signal: AbortSignal.timeout(opts.timeoutMs ?? 15e3)
229
- });
230
- } catch (err) {
231
- const msg = err instanceof Error ? err.message : String(err);
232
- throw new ApiError(0, "NETWORK_ERROR", `Cannot reach SolonGate API: ${msg}`);
231
+ res = await attempt();
232
+ } catch (firstErr) {
233
+ if (method !== "GET") {
234
+ const msg = firstErr instanceof Error ? firstErr.message : String(firstErr);
235
+ throw new ApiError(0, "NETWORK_ERROR", `Cannot reach SolonGate API: ${msg}`);
236
+ }
237
+ await new Promise((r) => setTimeout(r, 800));
238
+ try {
239
+ res = await attempt();
240
+ } catch (err) {
241
+ const msg = err instanceof Error ? err.message : String(err);
242
+ throw new ApiError(0, "NETWORK_ERROR", `Cannot reach SolonGate API: ${msg}`);
243
+ }
233
244
  }
234
245
  const text = await res.text().catch(() => "");
235
246
  let json = void 0;
@@ -465,8 +476,21 @@ function tailLines(file, maxBytes = 131072) {
465
476
  return [];
466
477
  }
467
478
  }
468
- function oscCopy(text) {
479
+ function copyToClipboard(text) {
480
+ const tools = [
481
+ ["wl-copy", []],
482
+ ["xclip", ["-selection", "clipboard"]],
483
+ ["xsel", ["--clipboard", "--input"]]
484
+ ];
485
+ for (const [cmd, args] of tools) {
486
+ try {
487
+ const r = spawnSync(cmd, args, { input: text, timeout: 2e3 });
488
+ if (r.status === 0) return cmd;
489
+ } catch {
490
+ }
491
+ }
469
492
  process.stdout.write(`\x1B]52;c;${Buffer.from(text, "utf-8").toString("base64")}\x07`);
493
+ return null;
470
494
  }
471
495
  function cloudItem(e) {
472
496
  return {
@@ -532,6 +556,25 @@ function PaneTitle({ label, extra, width }) {
532
556
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: tail + "\u2500".repeat(Math.max(0, width - used)) })
533
557
  ] });
534
558
  }
559
+ function StreamLine({ e, loc }) {
560
+ return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
561
+ /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
562
+ "[",
563
+ hhmmss(e.at),
564
+ " "
565
+ ] }),
566
+ /* @__PURE__ */ jsx2(Text2, { color: loc ? theme.ok : "#4f6db8", children: loc ? "LOC" : "CLD" }),
567
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "] " }),
568
+ /* @__PURE__ */ jsx2(Text2, { color: decisionColor(e.decision), bold: e.decision !== "ALLOW", children: e.decision.padEnd(6) }),
569
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate(e.tool, 12).padEnd(13) }),
570
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: e.permission.padEnd(5) }),
571
+ /* @__PURE__ */ jsx2(Text2, { color: e.evalMs != null && e.evalMs > 500 ? theme.warn : theme.dim, children: (e.evalMs != null ? `${e.evalMs}ms` : "\u2014").padEnd(7) }),
572
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: truncate(e.agent ?? "-", 11).padEnd(12) }),
573
+ e.dlp ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "DLP! " }) : null,
574
+ e.rule && e.decision !== "ALLOW" ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: truncate(e.rule, 14) + " " }) : null,
575
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: e.detail })
576
+ ] });
577
+ }
535
578
  var FILTERS = ["all", "local", "cloud"];
536
579
  function LivePanel({ active: active2 }) {
537
580
  const [s, setS] = useState2(null);
@@ -545,6 +588,11 @@ function LivePanel({ active: active2 }) {
545
588
  const [filter, setFilter] = useState2("all");
546
589
  const [scroll, setScroll] = useState2(0);
547
590
  const [toast, setToast] = useState2(null);
591
+ const [mode, setMode] = useState2("stream");
592
+ const [pickIdx, setPickIdx] = useState2(0);
593
+ const [detail, setDetail] = useState2(null);
594
+ const [detailCloud, setDetailCloud] = useState2([]);
595
+ const [detailScroll, setDetailScroll] = useState2(0);
548
596
  const seenRef = useRef2(/* @__PURE__ */ new Set());
549
597
  const lastLocalTs = useRef2(0);
550
598
  const pausedUntil = useRef2(0);
@@ -638,7 +686,8 @@ function LivePanel({ active: active2 }) {
638
686
  if (fresh.length) setCloudBuf((prev) => [...prev, ...fresh.map(cloudItem).sort((a, b) => a.at - b.at)].slice(-400));
639
687
  const denies = fresh.filter((e) => e.decision !== "ALLOW").length;
640
688
  if (firstLoad) pushLog(`cloud link up \xB7 api ${ms}ms \xB7 ${fresh.length} calls`);
641
- else pushLog(`api ${ms}ms \xB7 +${fresh.length} cloud${denies ? ` \xB7 ${denies} DENIED` : ""}`, denies ? "bad" : fresh.length ? "warn" : "ok");
689
+ else if (fresh.length) pushLog(`api ${ms}ms \xB7 +${fresh.length} cloud${denies ? ` \xB7 ${denies} DENIED` : ""}`, denies ? "bad" : "warn");
690
+ else pushLog(`api ${ms}ms \xB7 idle`, "ok");
642
691
  } catch (e) {
643
692
  onApiError(e);
644
693
  }
@@ -685,6 +734,19 @@ function LivePanel({ active: active2 }) {
685
734
  usePoll(() => {
686
735
  if (!paused()) guard.reload();
687
736
  }, 6e4, active2);
737
+ useEffect2(() => {
738
+ if (!detail) return;
739
+ setDetailCloud([]);
740
+ setDetailScroll(0);
741
+ let alive = true;
742
+ api.audit.list({ session_id: detail.id, limit: 100 }).then((r) => {
743
+ if (alive) setDetailCloud(r.entries.map(cloudItem));
744
+ }).catch(() => {
745
+ });
746
+ return () => {
747
+ alive = false;
748
+ };
749
+ }, [detail]);
688
750
  const [tick, setTick] = useState2(0);
689
751
  useEffect2(() => {
690
752
  if (!active2) return;
@@ -697,14 +759,19 @@ function LivePanel({ active: active2 }) {
697
759
  const ins = insights.data ?? {};
698
760
  const spin = SPIN[tick % SPIN.length];
699
761
  const nowMs = Date.now();
700
- const mergedAll = [...cloudBuf, ...localBuf].sort((a, b) => a.at - b.at);
701
- const filtered = filter === "all" ? mergedAll : mergedAll.filter((e) => e.source === filter);
702
- const visible = filtered.slice(-STREAM_KEEP);
762
+ const localKeys = /* @__PURE__ */ new Set();
763
+ for (const e of localBuf) {
764
+ const m = Math.round(e.at / 6e4);
765
+ for (const d of [-1, 0, 1]) localKeys.add(`${e.tool}|${e.decision}|${m + d}`);
766
+ }
767
+ const cloudDeduped = cloudBuf.filter((e) => !localKeys.has(`${e.tool}|${e.decision}|${Math.round(e.at / 6e4)}`));
768
+ const mergedAll = [...cloudDeduped, ...localBuf].sort((a, b) => a.at - b.at);
703
769
  const points = tsData?.timeseries ?? [];
770
+ const nowMin = Math.floor(nowMs / 6e4);
704
771
  const minuteCounts = new Array(60).fill(0);
705
772
  const minuteHot = new Array(60).fill(false);
706
773
  for (const e of mergedAll) {
707
- const idx = 59 - Math.floor((nowMs - e.at) / 6e4);
774
+ const idx = 59 - (nowMin - Math.floor(e.at / 6e4));
708
775
  if (idx >= 0 && idx < 60) {
709
776
  minuteCounts[idx]++;
710
777
  if (e.decision !== "ALLOW") minuteHot[idx] = true;
@@ -720,8 +787,8 @@ function LivePanel({ active: active2 }) {
720
787
  const minuteNow = minuteCounts[59] ?? 0;
721
788
  const dlpBars = (ins.dlpByPattern ?? []).slice(0, 2);
722
789
  const maxDlpBar = dlpBars[0]?.count ?? 1;
723
- const denials = mergedAll.filter((e) => e.decision !== "ALLOW");
724
- const lastDeny = denials[denials.length - 1];
790
+ const denialsAll = mergedAll.filter((e) => e.decision !== "ALLOW");
791
+ const lastDeny = denialsAll[denialsAll.length - 1];
725
792
  const toolCounts = /* @__PURE__ */ new Map();
726
793
  for (const e of mergedAll) toolCounts.set(e.tool, (toolCounts.get(e.tool) ?? 0) + 1);
727
794
  const topTools = [...toolCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
@@ -742,7 +809,24 @@ function LivePanel({ active: active2 }) {
742
809
  if (e.agent) cur.agent = e.agent;
743
810
  localSess.set(e.session, cur);
744
811
  }
745
- const localSessList = [...localSess.entries()].sort((a, b) => b[1].lastAt - a[1].lastAt);
812
+ const combinedSess = [
813
+ ...[...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 })),
814
+ ...sess.filter((a) => !localSess.has(a.session_id)).map((a) => ({
815
+ source: "cloud",
816
+ id: a.session_id,
817
+ agent: a.agent_name ?? a.session_id,
818
+ calls: a.total_calls,
819
+ denies: a.denied_calls,
820
+ lastAt: Date.parse(a.last_seen_at),
821
+ trust: a.trust_score,
822
+ isMe: false
823
+ }))
824
+ ];
825
+ const localSessIds = new Set(localSess.keys());
826
+ if (ring?.session) localSessIds.add(ring.session);
827
+ const isLoc = (e) => e.source === "local" || !!e.session && localSessIds.has(e.session);
828
+ const filtered = filter === "all" ? mergedAll : mergedAll.filter((e) => filter === "local" ? isLoc(e) : !isLoc(e));
829
+ const visibleDesc = filtered.slice(-STREAM_KEEP).reverse();
746
830
  const chartH = rows >= 36 ? 6 : 4;
747
831
  const colH = rows >= 32 ? 7 : 5;
748
832
  const streamRows = Math.max(4, rows - 5 - (1 + chartH) - colH);
@@ -750,21 +834,58 @@ function LivePanel({ active: active2 }) {
750
834
  const leftW = Math.floor(innerW * 0.55);
751
835
  const rightW = innerW - leftW - 2;
752
836
  const colW = Math.max(20, Math.floor((innerW - 4) / 3));
753
- const maxScroll = Math.max(0, visible.length - streamRows);
837
+ const pickable = combinedSess.slice(0, colH - 1);
838
+ const maxScroll = Math.max(0, visibleDesc.length - streamRows);
754
839
  const clampedScroll = Math.min(scroll, maxScroll);
755
- const start = Math.max(0, visible.length - streamRows - clampedScroll);
756
- const windowed = visible.slice(start, start + streamRows);
840
+ const windowed = visibleDesc.slice(clampedScroll, clampedScroll + streamRows);
841
+ const detailEntries = detail ? (() => {
842
+ const seen = /* @__PURE__ */ new Set();
843
+ const all = [];
844
+ for (const e of [...detailCloud, ...mergedAll.filter((x) => x.session === detail.id)]) {
845
+ const key = `${e.at}:${e.tool}:${e.decision}`;
846
+ if (seen.has(key)) continue;
847
+ seen.add(key);
848
+ all.push(e);
849
+ }
850
+ return all.sort((a, b) => b.at - a.at);
851
+ })() : [];
757
852
  useInput(
758
853
  (input, key) => {
854
+ if (mode === "detail") {
855
+ const maxD = Math.max(0, detailEntries.length - (rows - 10));
856
+ if (key.leftArrow) {
857
+ setMode("stream");
858
+ setDetail(null);
859
+ } else if (key.upArrow) setDetailScroll((n) => Math.max(0, n - 1));
860
+ else if (key.downArrow) setDetailScroll((n) => Math.min(maxD, n + 1));
861
+ else if (key.pageUp) setDetailScroll((n) => Math.max(0, n - 10));
862
+ else if (key.pageDown) setDetailScroll((n) => Math.min(maxD, n + 10));
863
+ return;
864
+ }
865
+ if (mode === "pick") {
866
+ if (key.upArrow) setPickIdx((n) => Math.max(0, n - 1));
867
+ else if (key.downArrow) setPickIdx((n) => Math.min(pickable.length - 1, n + 1));
868
+ else if (key.return) {
869
+ const row = pickable[pickIdx];
870
+ if (row) {
871
+ setDetail(row);
872
+ setMode("detail");
873
+ }
874
+ } else if (input === "s" || key.leftArrow) setMode("stream");
875
+ return;
876
+ }
759
877
  if (input === "f") {
760
878
  setFilter((f) => FILTERS[(FILTERS.indexOf(f) + 1) % FILTERS.length]);
761
879
  setScroll(0);
762
- } else if (key.upArrow) setScroll((n) => Math.min(maxScroll, n + 1));
763
- else if (key.downArrow) setScroll((n) => Math.max(0, n - 1));
764
- else if (key.pageUp) setScroll((n) => Math.min(maxScroll, n + streamRows));
765
- else if (key.pageDown) setScroll((n) => Math.max(0, n - streamRows));
880
+ } else if (input === "s") {
881
+ setPickIdx(0);
882
+ setMode("pick");
883
+ } else if (key.downArrow) setScroll((n) => Math.min(maxScroll, n + 1));
884
+ else if (key.upArrow) setScroll((n) => Math.max(0, n - 1));
885
+ else if (key.pageDown) setScroll((n) => Math.min(maxScroll, n + streamRows));
886
+ else if (key.pageUp) setScroll((n) => Math.max(0, n - streamRows));
766
887
  else if (input === "c") {
767
- const dump = visible.map(
888
+ const dump = visibleDesc.map(
768
889
  (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}`
769
890
  ).join("\n");
770
891
  try {
@@ -772,26 +893,96 @@ function LivePanel({ active: active2 }) {
772
893
  writeFileSync(DUMP, dump + "\n");
773
894
  } catch {
774
895
  }
775
- oscCopy(dump);
776
- setToast({ msg: `copied ${visible.length} lines \u2192 clipboard + ${DUMP}`, until: Date.now() + 4e3 });
896
+ const via = copyToClipboard(dump);
897
+ setToast(
898
+ via ? { msg: `\u2713 ${visibleDesc.length} lines in clipboard (${via})`, until: Date.now() + 4e3 } : { msg: "\u2717 no clipboard tool \u2014 install wl-clipboard or xclip", until: Date.now() + 6e3 }
899
+ );
777
900
  }
778
901
  },
779
902
  { isActive: active2 }
780
903
  );
781
904
  const sessionDot = (st) => st === "active" ? { ch: "\u25CF", color: theme.ok } : st === "idle" ? { ch: "\u25CC", color: theme.warn } : { ch: "\u25CB", color: theme.dim };
905
+ void sessionDot;
782
906
  if (s === null && localOn === null) {
783
907
  return /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
784
908
  spin,
785
909
  " connecting\u2026"
786
910
  ] });
787
911
  }
912
+ const titleBar = /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
913
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#1432A0", color: "#AAC8FA", bold: true, children: " SOLONGATE LIVE " }),
914
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: ` ${spin} up ${fmtUp(nowMs - startRef.current)} \xB7 ${hhmmss(nowMs)} \xB7 api ${latNow}ms ` }),
915
+ 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 " }),
916
+ toast && nowMs < toast.until ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#123d1f", color: "#7bd88f", bold: true, children: ` ${toast.msg} ` }) : null
917
+ ] });
918
+ if (mode === "detail" && detail) {
919
+ const d = detailEntries;
920
+ const allow = d.filter((e) => e.decision === "ALLOW").length;
921
+ const deny = d.length - allow;
922
+ const dlpN = d.filter((e) => e.dlp).length;
923
+ const evals = d.filter((e) => e.evalMs != null).map((e) => e.evalMs);
924
+ const evalAvg = evals.length ? Math.round(evals.reduce((a, b) => a + b, 0) / evals.length) : null;
925
+ const tCounts = /* @__PURE__ */ new Map();
926
+ const pCounts = /* @__PURE__ */ new Map();
927
+ for (const e of d) {
928
+ tCounts.set(e.tool, (tCounts.get(e.tool) ?? 0) + 1);
929
+ if (e.permission) pCounts.set(e.permission, (pCounts.get(e.permission) ?? 0) + 1);
930
+ }
931
+ const top = [...tCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 6);
932
+ const perms = [...pCounts.entries()].sort((a, b) => b[1] - a[1]);
933
+ const first = d[d.length - 1]?.at;
934
+ const last = d[0]?.at;
935
+ const tlRows = Math.max(4, rows - 10);
936
+ const maxD = Math.max(0, d.length - tlRows);
937
+ const dScroll = Math.min(detailScroll, maxD);
938
+ const tl = d.slice(dScroll, dScroll + tlRows);
939
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
940
+ titleBar,
941
+ /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
942
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#AAC8FA", bold: true, children: ` SESSION ${detail.id.slice(0, 8)} ` }),
943
+ /* @__PURE__ */ jsx2(Text2, { color: detail.source === "local" ? theme.ok : "#4f6db8", children: " " + detail.source.toUpperCase() }),
944
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: " " + truncate(detail.isMe ? detail.agent + " (this machine)" : detail.agent, 34) }),
945
+ detail.trust != null ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ` trust ${detail.trust}/100` }) : null,
946
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2190 back" })
947
+ ] }),
948
+ /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
949
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "\u2502 calls " }),
950
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: d.length }),
951
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 allow " }),
952
+ /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: allow }),
953
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 deny " }),
954
+ /* @__PURE__ */ jsx2(Text2, { color: deny ? theme.bad : theme.dim, bold: deny > 0, children: deny }),
955
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 dlp " }),
956
+ /* @__PURE__ */ jsx2(Text2, { color: dlpN ? theme.bad : theme.dim, children: dlpN }),
957
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 avg eval " }),
958
+ /* @__PURE__ */ jsx2(Text2, { children: evalAvg != null ? `${evalAvg}ms` : "\u2014" }),
959
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 first " }),
960
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: first ? `${hhmmss(first)} (${ago(first)} ago)` : "\u2014" }),
961
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 last " }),
962
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: last ? `${hhmmss(last)} (${ago(last)} ago)` : "\u2014" }),
963
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
964
+ ] }),
965
+ /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
966
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "\u2502 tools " }),
967
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: top.map(([t, c2]) => `${t}\xD7${c2}`).join(" ") || "\u2014" }),
968
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 perms " }),
969
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: perms.map(([p, c2]) => `${p}\xD7${c2}`).join(" ") || "\u2014" }),
970
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
971
+ ] }),
972
+ /* @__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 }),
973
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", height: tlRows, overflow: "hidden", children: [
974
+ tl.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: detailCloud.length === 0 ? spin + " loading history\u2026" : "no entries" }) : null,
975
+ tl.map((e) => /* @__PURE__ */ jsx2(StreamLine, { e, loc: isLoc(e) }, e.id))
976
+ ] }),
977
+ /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
978
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", bold: true, children: " SESSION " }),
979
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#0b1530", color: "#4f6db8", children: ` ${detail.id} ` }),
980
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: " \u2191\u2193 scroll \xB7 \u2190 back \xB7 esc menu " })
981
+ ] })
982
+ ] });
983
+ }
788
984
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
789
- /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
790
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#1432A0", color: "#AAC8FA", bold: true, children: " SOLONGATE LIVE " }),
791
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: ` ${spin} up ${fmtUp(nowMs - startRef.current)} \xB7 ${hhmmss(nowMs)} \xB7 api ${latNow}ms ` }),
792
- 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 " }),
793
- toast && nowMs < toast.until ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#123d1f", color: "#7bd88f", bold: true, children: ` ${toast.msg} ` }) : null
794
- ] }),
985
+ titleBar,
795
986
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
796
987
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "\u2502 calls " }),
797
988
  /* @__PURE__ */ jsx2(Text2, { bold: true, children: s ? s.total_calls : "\xB7\xB7\xB7\xB7" }),
@@ -808,7 +999,7 @@ function LivePanel({ active: active2 }) {
808
999
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 ghost " }),
809
1000
  /* @__PURE__ */ jsx2(Text2, { color: gh?.mode === "on" ? theme.ok : theme.dim, children: gh?.mode ?? "?" }),
810
1001
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 sess " }),
811
- /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: `${localSessList.length}loc ${sessCounts?.active ?? 0}\u25B2${sessCounts?.idle ?? 0}\u25CC` }),
1002
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: `${combinedSess.length} (${sessCounts?.active ?? 0} live)` }),
812
1003
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502 hooks " }),
813
1004
  /* @__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" }),
814
1005
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2502" })
@@ -837,7 +1028,7 @@ function LivePanel({ active: active2 }) {
837
1028
  /* @__PURE__ */ jsx2(Text2, { color: dl?.mode === "block" ? theme.ok : dl?.mode === "detect" ? theme.warn : theme.dim, children: (dl?.mode ?? "?").padEnd(7) }),
838
1029
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: `${(dl?.patterns ?? []).length} builtin \xB7 ${(dl?.custom ?? []).length} custom` })
839
1030
  ] }),
840
- 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" }),
1031
+ 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" }),
841
1032
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
842
1033
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "GUARD".padEnd(10) }),
843
1034
  ring ? /* @__PURE__ */ jsxs2(Text2, { color: theme.ok, children: [
@@ -847,48 +1038,28 @@ function LivePanel({ active: active2 }) {
847
1038
  ] })
848
1039
  ] }),
849
1040
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, marginRight: 2, overflow: "hidden", children: [
850
- /* @__PURE__ */ jsx2(PaneTitle, { label: "SESSIONS", extra: `${localSessList.length} local \xB7 ${sessCounts?.active ?? 0} cloud`, width: colW }),
851
- localSessList.length === 0 && sess.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "scanning\u2026" }) : null,
852
- localSessList.slice(0, Math.max(1, colH - 2)).map(([id, v]) => {
853
- const freshDot = nowMs - v.lastAt < 9e4;
854
- const isMe = ring?.session === id;
855
- return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
856
- /* @__PURE__ */ jsxs2(Text2, { color: freshDot ? theme.ok : theme.dim, children: [
857
- freshDot ? "\u25CF" : "\u25CB",
1041
+ /* @__PURE__ */ jsx2(PaneTitle, { label: "SESSIONS", extra: mode === "pick" ? "\u2191\u2193 pick \xB7 enter open" : `${combinedSess.length} \xB7 s = inspect`, width: colW }),
1042
+ pickable.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "scanning\u2026" }) : null,
1043
+ pickable.map((r, i) => {
1044
+ const freshDot = nowMs - r.lastAt < 9e4;
1045
+ const sel = mode === "pick" && i === pickIdx;
1046
+ return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", backgroundColor: sel ? "#1c2f63" : void 0, bold: sel || r.isMe, children: [
1047
+ /* @__PURE__ */ jsxs2(Text2, { color: sel ? theme.accentBright : freshDot ? theme.ok : theme.dim, children: [
1048
+ sel ? "\u25B8" : freshDot ? "\u25CF" : "\u25CB",
858
1049
  " "
859
1050
  ] }),
860
- /* @__PURE__ */ jsx2(Text2, { color: theme.ok, children: "LOC " }),
861
- /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, bold: isMe, children: truncate(isMe ? "this machine" : v.agent, 12).padEnd(13) }),
1051
+ /* @__PURE__ */ jsx2(Text2, { color: r.source === "local" ? theme.ok : "#4f6db8", children: r.source === "local" ? "LOC " : "CLD " }),
1052
+ /* @__PURE__ */ jsx2(Text2, { color: theme.accentBright, children: truncate(r.isMe ? "this machine" : r.agent, 12).padEnd(13) }),
862
1053
  /* @__PURE__ */ jsxs2(Text2, { children: [
863
- String(v.calls).padStart(4),
1054
+ String(r.calls).padStart(4),
864
1055
  "c "
865
1056
  ] }),
866
- /* @__PURE__ */ jsxs2(Text2, { color: v.denies ? theme.bad : theme.dim, children: [
867
- String(v.denies).padStart(3),
1057
+ /* @__PURE__ */ jsxs2(Text2, { color: r.denies ? theme.bad : theme.dim, children: [
1058
+ String(r.denies).padStart(3),
868
1059
  "d "
869
1060
  ] }),
870
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: ago(v.lastAt) })
871
- ] }, id);
872
- }),
873
- sess.slice(0, Math.max(0, colH - 1 - Math.min(localSessList.length, Math.max(1, colH - 2)))).map((a) => {
874
- const dot = sessionDot(a.status);
875
- return /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
876
- /* @__PURE__ */ jsxs2(Text2, { color: dot.color, children: [
877
- dot.ch,
878
- " "
879
- ] }),
880
- /* @__PURE__ */ jsx2(Text2, { color: "#4f6db8", children: "CLD " }),
881
- /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate(a.agent_name ?? a.session_id, 12).padEnd(13) }),
882
- /* @__PURE__ */ jsxs2(Text2, { children: [
883
- String(a.total_calls).padStart(4),
884
- "c "
885
- ] }),
886
- /* @__PURE__ */ jsxs2(Text2, { color: a.denied_calls ? theme.bad : theme.dim, children: [
887
- String(a.denied_calls).padStart(3),
888
- "d "
889
- ] }),
890
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: `t${a.trust_score} ` + ago(a.last_seen_at) })
891
- ] }, a.session_id);
1061
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: (r.trust != null ? `t${r.trust} ` : "") + ago(r.lastAt) })
1062
+ ] }, r.id);
892
1063
  })
893
1064
  ] }),
894
1065
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: colW, overflow: "hidden", children: [
@@ -907,7 +1078,7 @@ function LivePanel({ active: active2 }) {
907
1078
  PaneTitle,
908
1079
  {
909
1080
  label: "TOOL STREAM",
910
- extra: `last ${STREAM_KEEP} \xB7 filter ${filter}${clampedScroll > 0 ? ` \xB7 \u25B2${clampedScroll} scrolled` : " \xB7 following"} \xB7 f filter \xB7 \u2191\u2193 scroll \xB7 c copy`,
1081
+ 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`,
911
1082
  width: innerW
912
1083
  }
913
1084
  ),
@@ -920,28 +1091,12 @@ function LivePanel({ active: active2 }) {
920
1091
  spin,
921
1092
  " awaiting traffic\u2026"
922
1093
  ] }) : null,
923
- windowed.map((e) => /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
924
- /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
925
- "[",
926
- hhmmss(e.at),
927
- " "
928
- ] }),
929
- /* @__PURE__ */ jsx2(Text2, { color: e.source === "local" ? theme.ok : "#4f6db8", children: e.source === "local" ? "LOC" : "CLD" }),
930
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: "] " }),
931
- /* @__PURE__ */ jsx2(Text2, { color: decisionColor(e.decision), bold: e.decision !== "ALLOW", children: e.decision.padEnd(6) }),
932
- /* @__PURE__ */ jsx2(Text2, { color: theme.accent, children: truncate(e.tool, 12).padEnd(13) }),
933
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: e.permission.padEnd(5) }),
934
- /* @__PURE__ */ jsx2(Text2, { color: e.evalMs != null && e.evalMs > 500 ? theme.warn : theme.dim, children: (e.evalMs != null ? `${e.evalMs}ms` : "\u2014").padEnd(7) }),
935
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: truncate(e.agent ?? "-", 11).padEnd(12) }),
936
- e.dlp ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: "DLP! " }) : null,
937
- e.rule && e.decision !== "ALLOW" ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: truncate(e.rule, 14) + " " }) : null,
938
- /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: e.detail })
939
- ] }, e.id))
1094
+ windowed.map((e) => /* @__PURE__ */ jsx2(StreamLine, { e, loc: isLoc(e) }, e.id))
940
1095
  ] }),
941
1096
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
942
1097
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", bold: true, children: " LIVE " }),
943
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#0b1530", color: "#4f6db8", children: ` local ${localOn ? "on" : "off"} \xB7 ${localBuf.length} loc/${cloudBuf.length} cld buffered \xB7 top ${topTools.map(([t, c2]) => `${t}\xD7${c2}`).join(" ") || "\u2014"} ` }),
944
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: " f filter \xB7 \u2191\u2193 scroll \xB7 c copy \xB7 esc menu \xB7 q quit " })
1098
+ /* @__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"} ` }),
1099
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "#82AAF0", children: " f filter \xB7 s sessions \xB7 c copy \xB7 esc menu \xB7 q quit " })
945
1100
  ] })
946
1101
  ] });
947
1102
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.69.0",
3
+ "version": "0.71.0",
4
4
  "description": "AI tool security proxy: protect any AI tool server with customizable policies, path/command constraints, rate limiting, and audit logging. No code changes required.",
5
5
  "type": "module",
6
6
  "bin": {