@decentnetwork/beagle-ui 0.2.35 → 0.2.37

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 (2) hide show
  1. package/dist/beagle-ui.js +197 -20
  2. package/package.json +1 -1
package/dist/beagle-ui.js CHANGED
@@ -1,4 +1,4 @@
1
- export const BEAGLE_UI_VERSION = "0.2.35";
1
+ export const BEAGLE_UI_VERSION = "0.2.37";
2
2
  // src/icons.jsx
3
3
  var ICON_PATHS = {
4
4
  // ---- tab bar (the four must feel like one set) ----
@@ -276,7 +276,7 @@ function collapseProtocol(thread, isProto, lang) {
276
276
  };
277
277
  (thread || []).forEach((m, i) => {
278
278
  const hit = m && !m.day ? isProto({ text: m.text, dir: m && m.from === "me" ? "out" : "in", ts: m && m.ts }) : false;
279
- if (!hit || m && m.file) {
279
+ if (!hit || m && (m.file || m.kind === "friend-request")) {
280
280
  flush();
281
281
  out.push(m);
282
282
  return;
@@ -448,6 +448,10 @@ var dkApi = {
448
448
  cancelSend: (userid, ids) => dkPost("/api/file-cancel", { userid, ids }),
449
449
  retrySend: (userid, ids) => dkPost("/api/file-retry", { userid, ids }),
450
450
  setProfile: (name, description) => dkPost("/api/set-profile", { name, description }),
451
+ // Probe a failed exit once, now (or every failed exit with all=true). Exits
452
+ // that failed three times in a row are never probed again on their own.
453
+ reconnectExit: (ip) => dkPost("/api/exits/reconnect", { ip }),
454
+ reconnectAllExits: () => dkPost("/api/exits/reconnect", { all: true }),
451
455
  // Used by the browser's welcome flow, which also picks an avatar and marks
452
456
  // first run done. Harmless where there is no welcome flow: nothing calls it.
453
457
  setProfileFull: (p) => dkPost("/api/set-profile", p),
@@ -535,7 +539,10 @@ function dkThreadMsg(m) {
535
539
  // Brief 30: a validated component form, already whitelisted host-side.
536
540
  // Passed through untouched — the renderer treats every string as text,
537
541
  // never markup.
538
- components: m.components
542
+ components: m.components,
543
+ // 'friend-request': the accepted request's greeting, merged in by the
544
+ // host (beagle's friend-greetings.ts); chat.jsx draws its own bubble.
545
+ kind: m.kind
539
546
  };
540
547
  }
541
548
  var DK_TAIL_MIN = 30;
@@ -580,22 +587,27 @@ function useProtocolTails(peers, isProtocol, enabled) {
580
587
  }, [peers, isProtocol, enabled]);
581
588
  return fixes;
582
589
  }
583
- function useDaemonData() {
590
+ function useDaemonData(opts) {
591
+ const wantExits = !!(opts && opts.exits);
592
+ const wantExitsRef = React.useRef(wantExits);
593
+ wantExitsRef.current = wantExits;
584
594
  const [snap, setSnap] = React.useState({
585
595
  me: DK_ME_FALLBACK,
586
596
  peers: [],
587
597
  requests: [],
588
598
  exits: [],
589
599
  activeExit: null,
600
+ exitFeature: null,
601
+ routeFallback: null,
590
602
  loaded: false,
591
603
  locked: false
592
604
  });
593
605
  const [threads, setThreads] = React.useState({});
594
606
  const refreshRef = React.useRef(null);
595
607
  const refresh = React.useCallback(async () => {
596
- var _a;
608
+ var _a, _b, _c;
597
609
  try {
598
- const d = await dkGet("/api/desktop");
610
+ const d = await dkGet(wantExitsRef.current ? "/api/desktop?exits=1" : "/api/desktop");
599
611
  if (d && d.locked) {
600
612
  setSnap((s) => ({ ...s, locked: true, loaded: true }));
601
613
  return;
@@ -613,6 +625,8 @@ function useDaemonData() {
613
625
  requests: d.requests || [],
614
626
  exits: d.exits || [],
615
627
  activeExit: (_a = d.activeExit) != null ? _a : null,
628
+ exitFeature: (_b = d.exitFeature) != null ? _b : null,
629
+ routeFallback: (_c = d.routeFallback) != null ? _c : null,
616
630
  loaded: true
617
631
  });
618
632
  } catch (e) {
@@ -624,6 +638,10 @@ function useDaemonData() {
624
638
  const t = setInterval(refresh, 2500);
625
639
  return () => clearInterval(t);
626
640
  }, [refresh]);
641
+ React.useEffect(() => {
642
+ if (wantExits)
643
+ refresh();
644
+ }, [wantExits, refresh]);
627
645
  const inFlight = React.useRef(/* @__PURE__ */ new Set());
628
646
  const cache = React.useRef({});
629
647
  const [earlier, setEarlier] = React.useState({});
@@ -632,15 +650,23 @@ function useDaemonData() {
632
650
  if (!c)
633
651
  return;
634
652
  const arr = [...c.byId.values()].sort((a, b) => a.ts - b.ts);
635
- const msgs = arr.map(dkThreadMsg);
636
- const withDay = msgs.length ? [{ day: dkDayLabel(arr[0].ts) }].concat(msgs) : [];
653
+ const withDay = [];
654
+ let lastDay = null;
655
+ for (const m of arr) {
656
+ const day = dkDayLabel(m.ts);
657
+ if (day !== lastDay) {
658
+ withDay.push({ day });
659
+ lastDay = day;
660
+ }
661
+ withDay.push(dkThreadMsg(m));
662
+ }
637
663
  setThreads((t) => Object.assign({}, t, { [peerId]: withDay }));
638
664
  setEarlier((e) => e[peerId] === c.more ? e : Object.assign({}, e, { [peerId]: c.more }));
639
665
  }, []);
640
- const loadThread = React.useCallback(async (peerId, opts) => {
666
+ const loadThread = React.useCallback(async (peerId, opts2) => {
641
667
  if (!peerId)
642
668
  return;
643
- if (opts && opts.reset)
669
+ if (opts2 && opts2.reset)
644
670
  delete cache.current[peerId];
645
671
  if (inFlight.current.has(peerId))
646
672
  return;
@@ -1745,6 +1771,27 @@ function requestAvatarSource(r, skipUrl = false) {
1745
1771
  return { kind: "ens" };
1746
1772
  }
1747
1773
 
1774
+ // src/request-greeting.js
1775
+ var REQUEST_TRANSPORTS = ["lan", "carrier", "direct", "relay", "express", "webrtc", "online", "offline", "tcp", "udp", "p2p"];
1776
+ function unquote(s) {
1777
+ const m = /^"([\s\S]*)"$/.exec(s);
1778
+ return m ? m[1] : s;
1779
+ }
1780
+ function requestGreeting(r) {
1781
+ const hello = r && typeof r.hello === "string" ? r.hello.trim() : "";
1782
+ const via = r && typeof r.via === "string" ? r.via.trim() : "";
1783
+ const isTransport = !!via && REQUEST_TRANSPORTS.includes(via.toLowerCase());
1784
+ const isName = !!via && !!r && typeof r.name === "string" && r.name.trim() === via;
1785
+ return {
1786
+ greeting: hello || (via && !isTransport && !isName ? unquote(via).trim() : ""),
1787
+ transport: isTransport ? via : ""
1788
+ };
1789
+ }
1790
+ var GREETING_KIND = "friend-request";
1791
+ function isGreetingItem(m) {
1792
+ return !!m && m.kind === GREETING_KIND;
1793
+ }
1794
+
1748
1795
  // src/chat.jsx
1749
1796
  var DK_CMD_HIST_KEY = "beagle.assistant.history";
1750
1797
  var DK_CMD_HIST_MAX = 50;
@@ -1797,7 +1844,7 @@ function RequestsBlock({ T, requests, onAct }) {
1797
1844
  overflow: "hidden",
1798
1845
  textOverflow: "ellipsis",
1799
1846
  whiteSpace: "nowrap"
1800
- } }, r.name) : /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)", fontStyle: "italic" } }, T.noName || "no name given"), /* @__PURE__ */ React.createElement(Mono, { size: 11, copy: r.carrier, title: r.carrier }, shortKey(r.carrier, 8, 6)), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 10.5, color: "var(--faint)", marginTop: 2 } }, "via ", r.via, " \xB7 ", r.time))), r.descr && /* @__PURE__ */ React.createElement("div", { style: {
1847
+ } }, r.name) : /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)", fontStyle: "italic" } }, T.noName || "no name given"), /* @__PURE__ */ React.createElement(Mono, { size: 11, copy: r.carrier, title: r.carrier }, shortKey(r.carrier, 8, 6)), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 10.5, color: "var(--faint)", marginTop: 2 } }, requestGreeting(r).transport ? "via " + requestGreeting(r).transport + " \xB7 " : "", r.time))), /* @__PURE__ */ React.createElement(DkRequestGreeting, { T, text: requestGreeting(r).greeting }), r.descr && /* @__PURE__ */ React.createElement("div", { style: {
1801
1848
  fontFamily: "var(--ui)",
1802
1849
  fontSize: 12,
1803
1850
  color: "var(--dim, var(--faint))",
@@ -1806,6 +1853,76 @@ function RequestsBlock({ T, requests, onAct }) {
1806
1853
  paddingLeft: 8
1807
1854
  } }, r.descr), failed[r.id] && /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 11, color: "var(--danger)" } }, failed[r.id]), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: 6 } }, /* @__PURE__ */ React.createElement(Btn, { tone: "ok", icon: "check", size: "sm", onClick: () => act(r, "accept"), style: { flex: 1 } }, T.accept), /* @__PURE__ */ React.createElement(Btn, { tone: "danger", icon: "x", size: "sm", onClick: () => act(r, "reject"), style: { flex: 1 } }, T.reject))))));
1808
1855
  }
1856
+ function DkRequestGreeting({ T, text }) {
1857
+ const [open, setOpen] = React.useState(false);
1858
+ const [clipped, setClipped] = React.useState(false);
1859
+ const ref = React.useRef(null);
1860
+ React.useLayoutEffect(() => {
1861
+ const el = ref.current;
1862
+ if (el && !open)
1863
+ setClipped(el.scrollHeight > el.clientHeight + 1);
1864
+ }, [text, open]);
1865
+ if (!text)
1866
+ return null;
1867
+ const toggle = () => {
1868
+ try {
1869
+ const sel = window.getSelection && window.getSelection();
1870
+ if (sel && String(sel).length)
1871
+ return;
1872
+ } catch (e) {
1873
+ }
1874
+ if (clipped || open)
1875
+ setOpen((o) => !o);
1876
+ };
1877
+ return /* @__PURE__ */ React.createElement(
1878
+ "div",
1879
+ {
1880
+ onClick: toggle,
1881
+ title: !open && clipped ? T.showAll || "show all" : void 0,
1882
+ style: { cursor: clipped || open ? "pointer" : "text" }
1883
+ },
1884
+ /* @__PURE__ */ React.createElement("div", { style: { background: "var(--bub-them)", border: "1px solid var(--line)", borderRadius: 8, padding: "6px 9px" } }, /* @__PURE__ */ React.createElement("div", { ref, style: {
1885
+ fontFamily: "var(--ui)",
1886
+ fontSize: 12.5,
1887
+ color: "var(--text)",
1888
+ lineHeight: 1.45,
1889
+ whiteSpace: "pre-wrap",
1890
+ overflowWrap: "anywhere",
1891
+ userSelect: "text",
1892
+ WebkitUserSelect: "text",
1893
+ ...open ? { maxHeight: 240, overflowY: "auto" } : { display: "-webkit-box", WebkitLineClamp: 2, WebkitBoxOrient: "vertical", overflow: "hidden" }
1894
+ } }, text)),
1895
+ (clipped || open) && /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--ui)", fontSize: 11, color: "var(--accent)", marginTop: 3 } }, open ? T.showLess || "show less" : T.showAll || "show all")
1896
+ );
1897
+ }
1898
+ function DkGreetingMsg({ m, peer, T }) {
1899
+ return /* @__PURE__ */ React.createElement("div", { style: { display: "flex", justifyContent: "flex-start", alignItems: "flex-end", gap: 8, margin: "3px 0 10px" } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer, size: 24, radius: 6, dot: false }), /* @__PURE__ */ React.createElement("div", { style: {
1900
+ maxWidth: "75%",
1901
+ padding: "8px 12px",
1902
+ borderRadius: 12,
1903
+ borderBottomLeftRadius: 4,
1904
+ background: "var(--panel-2)",
1905
+ border: "1px dashed var(--accent)",
1906
+ color: "var(--text)"
1907
+ } }, /* @__PURE__ */ React.createElement("div", { style: {
1908
+ display: "flex",
1909
+ alignItems: "center",
1910
+ gap: 6,
1911
+ marginBottom: 4,
1912
+ fontFamily: "var(--mono)",
1913
+ fontSize: 10.5,
1914
+ color: "var(--accent)",
1915
+ fontWeight: 600
1916
+ } }, /* @__PURE__ */ React.createElement(Icon, { name: "userPlus", size: 12, stroke: 2.2, color: "var(--accent)" }), /* @__PURE__ */ React.createElement("span", null, T.friendRequestItem || "Friend request"), /* @__PURE__ */ React.createElement("span", { style: { color: "var(--faint)", fontWeight: 400 } }, "\xB7 ", m.time)), /* @__PURE__ */ React.createElement("div", { style: {
1917
+ fontFamily: "var(--ui)",
1918
+ fontSize: 13.5,
1919
+ lineHeight: 1.5,
1920
+ whiteSpace: "pre-wrap",
1921
+ overflowWrap: "anywhere",
1922
+ userSelect: "text",
1923
+ WebkitUserSelect: "text"
1924
+ } }, m.text)));
1925
+ }
1809
1926
  function DkRequestAvatar({ r, size, radius }) {
1810
1927
  const [urlBroken, setUrlBroken] = React.useState(false);
1811
1928
  const url = requestAvatarUrl(r);
@@ -2580,7 +2697,12 @@ function DkChatForm({ form, submitted, peer, onSubmit }) {
2580
2697
  return null;
2581
2698
  }), err && /* @__PURE__ */ React.createElement("div", { style: { color: "var(--danger, #ff6b6b)", fontSize: 11.5, fontFamily: "var(--mono)" } }, err));
2582
2699
  }
2583
- function Msg({ m, peer, T, lang, onTheater, onDelete, onCancel, onRetry, onRetryMsg, onReveal, onOpenFile, onCall, onFormSubmit, onNameCard, onAssistantRemove, onAssistantAdopt, answered, selMode, selected, onToggleSel, busy, isGroup }) {
2700
+ function Msg(props) {
2701
+ if (isGreetingItem(props.m))
2702
+ return /* @__PURE__ */ React.createElement(DkGreetingMsg, { m: props.m, peer: props.peer, T: props.T });
2703
+ return /* @__PURE__ */ React.createElement(DkMsgBody, { ...props });
2704
+ }
2705
+ function DkMsgBody({ m, peer, T, lang, onTheater, onDelete, onCancel, onRetry, onRetryMsg, onReveal, onOpenFile, onCall, onFormSubmit, onNameCard, onAssistantRemove, onAssistantAdopt, answered, selMode, selected, onToggleSel, busy, isGroup }) {
2584
2706
  const mine = m.from === "me";
2585
2707
  const nativeMedia = !m.file ? dkNativeMediaFromText(m.text) : null;
2586
2708
  const grp = isGroup && m.dir !== "out" && !m.file && !nativeMedia ? dkGroupParse(m.text) : null;
@@ -6054,15 +6176,40 @@ function PeerTable({ T, peers, onOpenChat }) {
6054
6176
  borderBottom: i < peers.length - 1 ? "1px solid var(--line)" : "none"
6055
6177
  } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10, minWidth: 0 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: p, size: 26, radius: 6 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: p.alias ? "var(--ui)" : "var(--mono)", fontSize: 13, fontWeight: 600, color: "var(--text)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, p.alias || shortKey(p.userId, 8, 5)), p.agent && /* @__PURE__ */ React.createElement(Icon, { name: "bot", size: 12, color: "var(--faint)", stroke: 2 })), /* @__PURE__ */ React.createElement(Mono, { size: 12.5, dim: true, copy: p.ip }, p.ip), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement(RouteTag, { peer: p })), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11.5, color: p.wire === "64" ? "var(--warn)" : "var(--dim)" } }, p.wire === "64" ? "old\xB764" : "new\xB7163"), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", justifyContent: "flex-end", gap: 6 } }, /* @__PURE__ */ React.createElement(Btn, { icon: "message", size: "sm", title: T.openChat, onClick: () => onOpenChat(p.id) }), /* @__PURE__ */ React.createElement(Btn, { icon: "signal", size: "sm", title: T.ping })))));
6056
6178
  }
6057
- function ExitCard({ T, region, activeExit, onSetExit }) {
6179
+ function ReconnectBtn({ T, ip, onReconnect, label, icon = "refresh" }) {
6180
+ const [busy, setBusy] = React.useState(false);
6181
+ return /* @__PURE__ */ React.createElement(
6182
+ Btn,
6183
+ {
6184
+ size: "sm",
6185
+ icon,
6186
+ disabled: busy,
6187
+ onClick: async () => {
6188
+ if (busy || !onReconnect)
6189
+ return;
6190
+ setBusy(true);
6191
+ try {
6192
+ await onReconnect(ip);
6193
+ } finally {
6194
+ setBusy(false);
6195
+ }
6196
+ }
6197
+ },
6198
+ busy ? T.exitChecking || "checking\u2026" : label || T.exitReconnect || "Reconnect"
6199
+ );
6200
+ }
6201
+ function ExitCard({ T, region, activeExit, onSetExit, onReconnect, featureOn }) {
6058
6202
  return /* @__PURE__ */ React.createElement("div", { style: { borderRadius: 11, border: "1px solid var(--line)", overflow: "hidden", background: "var(--panel)" } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10, padding: "11px 14px", borderBottom: "1px solid var(--line)", background: "var(--panel-2)" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700, letterSpacing: 0.5, padding: "2px 7px", borderRadius: 5, background: "var(--chip)", color: "var(--dim)" } }, region.flag), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--ui)", fontSize: 13.5, fontWeight: 600, color: "var(--text)" } }, region.label), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), (() => {
6059
6203
  const reach = region.nodes.filter((n) => n.reachable).length;
6060
- const down = region.nodes.length - reach;
6061
- return /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: down > 0 ? "var(--warn)" : "var(--faint)" } }, reach, "/", region.nodes.length, " ", T.up, down > 0 ? ` \xB7 ${down} down` : "");
6204
+ const failed = region.nodes.filter((n) => n.status === "failed").length;
6205
+ const down = region.nodes.length - reach - failed;
6206
+ return /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: down + failed > 0 ? "var(--warn)" : "var(--faint)" } }, reach, "/", region.nodes.length, " ", T.up, failed > 0 ? ` \xB7 ${failed} ${T.exitFailed || "failed"}` : "", down > 0 ? ` \xB7 ${down} down` : "");
6062
6207
  })()), /* @__PURE__ */ React.createElement("div", null, region.nodes.map((n, i) => {
6063
6208
  const active = n.ip === activeExit;
6064
6209
  const stuck = n.online && !n.reachable;
6065
- return /* @__PURE__ */ React.createElement("div", { key: n.ip, className: "dk-wrap", style: { display: "flex", alignItems: "center", gap: 12, rowGap: 6, padding: "10px 14px", borderBottom: i < region.nodes.length - 1 ? "1px solid var(--line)" : "none", background: active ? "color-mix(in oklab, var(--accent), transparent 92%)" : "transparent" } }, /* @__PURE__ */ React.createElement(StatusDot, { online: n.reachable }), /* @__PURE__ */ React.createElement(Mono, { size: 13, copy: n.ip }, n.ip), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)" } }, n.host), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), n.reachable ? /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11.5, color: n.ping > 200 ? "var(--warn)" : "var(--dim)" } }, n.ping, "ms") : stuck ? /* @__PURE__ */ React.createElement(Tag, { tone: "warn", title: "Announces online but IP won't pass \u2014 session desync or NAT-blocked" }, "online \xB7 no route") : /* @__PURE__ */ React.createElement(Tag, { tone: "off" }, "offline"), active ? /* @__PURE__ */ React.createElement(Btn, { tone: "ok", icon: "check", size: "sm", onClick: () => onSetExit(null) }, T.routing) : /* @__PURE__ */ React.createElement(Btn, { size: "sm", icon: "route", onClick: () => n.reachable && onSetExit(n.ip), style: { opacity: n.reachable ? 1 : 0.4 } }, T.routeThru));
6210
+ const failed = n.status === "failed";
6211
+ const usable = n.reachable && featureOn !== false;
6212
+ return /* @__PURE__ */ React.createElement("div", { key: n.ip, className: "dk-wrap", style: { display: "flex", alignItems: "center", gap: 12, rowGap: 6, padding: "10px 14px", borderBottom: i < region.nodes.length - 1 ? "1px solid var(--line)" : "none", background: active ? "color-mix(in oklab, var(--accent), transparent 92%)" : "transparent" } }, /* @__PURE__ */ React.createElement(StatusDot, { online: n.reachable }), /* @__PURE__ */ React.createElement(Mono, { size: 13, copy: n.ip }, n.ip), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)" } }, n.host), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), n.reachable ? /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11.5, color: n.ping > 200 ? "var(--warn)" : "var(--dim)" } }, n.ping, "ms") : failed ? /* @__PURE__ */ React.createElement(Tag, { tone: "danger", title: "Failed 3 checks in a row \u2014 no longer checked automatically" }, T.exitFailed || "failed") : n.status === "unknown" ? /* @__PURE__ */ React.createElement(Tag, { tone: "off" }, T.exitNotChecked || "not checked") : stuck ? /* @__PURE__ */ React.createElement(Tag, { tone: "warn", title: "Announces online but IP won't pass \u2014 session desync or NAT-blocked" }, "online \xB7 no route") : n.status === "retrying" ? /* @__PURE__ */ React.createElement(Tag, { tone: "warn", title: "No answer \u2014 will try again shortly" }, T.exitRetrying || "retrying", " \xB7 ", n.fails, "/3") : /* @__PURE__ */ React.createElement(Tag, { tone: "off" }, "offline"), failed ? /* @__PURE__ */ React.createElement(ReconnectBtn, { T, ip: n.ip, onReconnect }) : active ? /* @__PURE__ */ React.createElement(Btn, { tone: "ok", icon: "check", size: "sm", onClick: () => onSetExit(null) }, T.routing) : /* @__PURE__ */ React.createElement(Btn, { size: "sm", icon: "route", disabled: !usable, onClick: () => usable && onSetExit(n.ip), style: { opacity: usable ? 1 : 0.4 } }, T.routeThru));
6066
6213
  })));
6067
6214
  }
6068
6215
  function LanEnableCard({ backend, onArm, onCancel }) {
@@ -6166,12 +6313,14 @@ function LanEnableCard({ backend, onArm, onCancel }) {
6166
6313
  "\u53D6\u6D88,\u6062\u590D\u6D88\u606F"
6167
6314
  )));
6168
6315
  }
6169
- function NetworkTab({ T, me, peers, exits, activeExit, reqCount, onSetExit, onOpenChat, backend, onArmLan, onCancelLan }) {
6316
+ function NetworkTab({ T, me, peers, exits, activeExit, exitFeature, routeFallback, onReconnectExit, reqCount, onSetExit, onOpenChat, backend, onArmLan, onCancelLan }) {
6317
+ const featureOn = !exitFeature || exitFeature.available !== false;
6318
+ const anyFailed = (exits || []).some((r) => (r.nodes || []).some((n) => n.status === "failed"));
6170
6319
  if (backend && backend.switchable && !backend.hasVirtualLan) {
6171
6320
  return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, overflow: "auto", background: "var(--bg)" } }, /* @__PURE__ */ React.createElement("div", { className: "dk-page", style: { maxWidth: 1040, margin: "0 auto", padding: "24px 28px 60px", display: "flex", flexDirection: "column", gap: 26 } }, /* @__PURE__ */ React.createElement(MyNode, { T, me, activeExit, peers, reqCount }), /* @__PURE__ */ React.createElement(LanEnableCard, { backend, onArm: onArmLan, onCancel: onCancelLan })));
6172
6321
  }
6173
6322
  const lanless = !!backend && backend.hasVirtualLan === false;
6174
- return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, overflow: "auto", background: "var(--bg)" } }, /* @__PURE__ */ React.createElement("div", { className: "dk-page", style: { maxWidth: 1040, margin: "0 auto", padding: "24px 28px 60px", display: "flex", flexDirection: "column", gap: 26 } }, /* @__PURE__ */ React.createElement(MyNode, { T, me, activeExit, peers, reqCount }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 12 } }, /* @__PURE__ */ React.createElement(Section, { label: T.peerRouting, count: peers.length }), /* @__PURE__ */ React.createElement(PeerTable, { T, peers, onOpenChat }), !peers.length && /* @__PURE__ */ React.createElement("div", { style: { padding: "14px 16px", borderRadius: 11, border: "1px dashed var(--line)", fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)" } }, T.noPeersYet || "No friends yet \u2014 add one from Chat and it shows up here with its route.")), lanless ? null : /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 12 } }, /* @__PURE__ */ React.createElement(Section, { label: T.exitNodes, trailing: /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: 6 } }, /* @__PURE__ */ React.createElement(Btn, { icon: "plus", size: "sm" }, T.addExit)) }), activeExit && /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10, padding: "11px 14px", borderRadius: 10, background: "color-mix(in oklab, var(--warn), transparent 90%)", border: "1px solid color-mix(in oklab, var(--warn), transparent 70%)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "route", size: 17, color: "var(--warn)", stroke: 2 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, color: "var(--text)" } }, T.egressVia), /* @__PURE__ */ React.createElement(Mono, { size: 13, copy: activeExit }, activeExit), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Btn, { tone: "danger", icon: "unlink", size: "sm", onClick: () => onSetExit(null) }, T.stopRouting)), /* @__PURE__ */ React.createElement("div", { className: "dk-grid1", style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 } }, exits.map((r) => /* @__PURE__ */ React.createElement(ExitCard, { key: r.region, T, region: r, activeExit, onSetExit }))))));
6323
+ return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, overflow: "auto", background: "var(--bg)" } }, /* @__PURE__ */ React.createElement("div", { className: "dk-page", style: { maxWidth: 1040, margin: "0 auto", padding: "24px 28px 60px", display: "flex", flexDirection: "column", gap: 26 } }, /* @__PURE__ */ React.createElement(MyNode, { T, me, activeExit, peers, reqCount }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 12 } }, /* @__PURE__ */ React.createElement(Section, { label: T.peerRouting, count: peers.length }), /* @__PURE__ */ React.createElement(PeerTable, { T, peers, onOpenChat }), !peers.length && /* @__PURE__ */ React.createElement("div", { style: { padding: "14px 16px", borderRadius: 11, border: "1px dashed var(--line)", fontFamily: "var(--ui)", fontSize: 12.5, color: "var(--faint)" } }, T.noPeersYet || "No friends yet \u2014 add one from Chat and it shows up here with its route.")), lanless ? null : /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 12 } }, /* @__PURE__ */ React.createElement(Section, { label: T.exitNodes, trailing: /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: 6 } }, anyFailed && onReconnectExit && /* @__PURE__ */ React.createElement(ReconnectBtn, { T, ip: null, onReconnect: onReconnectExit, label: T.exitReconnectAll || "Reconnect all" }), /* @__PURE__ */ React.createElement(Btn, { icon: "plus", size: "sm" }, T.addExit)) }), !featureOn && /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 4, padding: "11px 14px", borderRadius: 10, background: "var(--panel-2)", border: "1px dashed var(--line)" } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 700, color: "var(--text)" } }, T.noExitAvailable || "No exit available \u2014 routing and proxy are off."), anyFailed && /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--ui)", fontSize: 12, color: "var(--dim)" } }, T.noExitHint || "Every exit failed 3 times and is no longer checked automatically. Press Reconnect to try one again.")), routeFallback && routeFallback.from && /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10, padding: "11px 14px", borderRadius: 10, background: "color-mix(in oklab, var(--warn), transparent 90%)", border: "1px solid color-mix(in oklab, var(--warn), transparent 70%)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "route", size: 17, color: "var(--warn)", stroke: 2 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, color: "var(--text)" } }, T.routeFellBack || "Routing fell back to direct: every exit it used failed. Reconnect one to route through it again.")), activeExit && /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 10, padding: "11px 14px", borderRadius: 10, background: "color-mix(in oklab, var(--warn), transparent 90%)", border: "1px solid color-mix(in oklab, var(--warn), transparent 70%)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "route", size: 17, color: "var(--warn)", stroke: 2 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, color: "var(--text)" } }, T.egressVia), /* @__PURE__ */ React.createElement(Mono, { size: 13, copy: activeExit }, activeExit), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Btn, { tone: "danger", icon: "unlink", size: "sm", onClick: () => onSetExit(null) }, T.stopRouting)), /* @__PURE__ */ React.createElement("div", { className: "dk-grid1", style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 } }, exits.map((r) => /* @__PURE__ */ React.createElement(ExitCard, { key: r.region, T, region: r, activeExit, onSetExit, onReconnect: onReconnectExit, featureOn }))))));
6175
6324
  }
6176
6325
  Object.assign(window, { NetworkTab });
6177
6326
 
@@ -6512,6 +6661,9 @@ var STR = {
6512
6661
  dirSearch: "search name / userid\u2026",
6513
6662
  dirLoading: "loading directory\u2026",
6514
6663
  noName: "no name given",
6664
+ friendRequestItem: "Friend request",
6665
+ showAll: "show all",
6666
+ showLess: "show less",
6515
6667
  dirEmpty: "nobody matches",
6516
6668
  dirError: "Could not load the directory:",
6517
6669
  bridgeEmpty: "Nobody else is listed on this bridge yet. You are the first.",
@@ -6586,6 +6738,15 @@ var STR = {
6586
6738
  routeThru: "route",
6587
6739
  egressVia: "all egress routed via",
6588
6740
  stopRouting: "stop",
6741
+ exitFailed: "failed",
6742
+ exitReconnect: "Reconnect",
6743
+ exitReconnectAll: "Reconnect all",
6744
+ exitChecking: "checking\u2026",
6745
+ exitNotChecked: "not checked",
6746
+ exitRetrying: "retrying",
6747
+ noExitAvailable: "No exit available \u2014 routing and proxy are off.",
6748
+ noExitHint: "Every exit failed 3 times and is no longer checked automatically. Press Reconnect to try one again.",
6749
+ routeFellBack: "Routing fell back to direct: every exit it used failed. Reconnect one to route through it again.",
6589
6750
  identity: "Identity",
6590
6751
  userId: "user id",
6591
6752
  carrierAddr: "carrier address",
@@ -6711,6 +6872,9 @@ var STR = {
6711
6872
  dirSearch: "\u641C\u7D22\u540D\u5B57 / userid\u2026",
6712
6873
  dirLoading: "\u6B63\u5728\u52A0\u8F7D\u540D\u5355\u2026",
6713
6874
  noName: "\u5BF9\u65B9\u6CA1\u6709\u586B\u540D\u5B57",
6875
+ friendRequestItem: "\u597D\u53CB\u8BF7\u6C42",
6876
+ showAll: "\u5C55\u5F00\u5168\u6587",
6877
+ showLess: "\u6536\u8D77",
6714
6878
  dirEmpty: "\u6CA1\u6709\u5339\u914D\u7684\u4EBA",
6715
6879
  dirError: "\u540D\u5355\u52A0\u8F7D\u5931\u8D25\uFF1A",
6716
6880
  bridgeEmpty: "\u8FD9\u4E2A bridge \u4E0A\u8FD8\u6CA1\u6709\u522B\u4EBA\uFF0C\u4F60\u662F\u7B2C\u4E00\u4E2A\u3002",
@@ -6785,6 +6949,15 @@ var STR = {
6785
6949
  routeThru: "\u8DEF\u7531",
6786
6950
  egressVia: "\u5168\u90E8\u51FA\u53E3\u6D41\u91CF\u7ECF\u7531",
6787
6951
  stopRouting: "\u505C\u6B62",
6952
+ exitFailed: "\u5DF2\u5931\u8D25",
6953
+ exitReconnect: "\u91CD\u65B0\u8FDE\u63A5",
6954
+ exitReconnectAll: "\u5168\u90E8\u91CD\u8FDE",
6955
+ exitChecking: "\u68C0\u6D4B\u4E2D\u2026",
6956
+ exitNotChecked: "\u672A\u68C0\u6D4B",
6957
+ exitRetrying: "\u91CD\u8BD5\u4E2D",
6958
+ noExitAvailable: "\u6CA1\u6709\u53EF\u7528\u7684\u51FA\u53E3 \u2014\u2014 \u8DEF\u7531\u548C\u4EE3\u7406\u5DF2\u5173\u95ED\u3002",
6959
+ noExitHint: "\u6240\u6709\u51FA\u53E3\u90FD\u8FDE\u7EED\u5931\u8D25 3 \u6B21,\u5DF2\u4E0D\u518D\u81EA\u52A8\u68C0\u6D4B\u3002\u70B9\u300C\u91CD\u65B0\u8FDE\u63A5\u300D\u518D\u8BD5\u4E00\u6B21\u3002",
6960
+ routeFellBack: "\u8DEF\u7531\u5DF2\u6539\u56DE\u76F4\u8FDE:\u5B83\u4F7F\u7528\u7684\u51FA\u53E3\u5168\u90E8\u5931\u8D25\u3002\u91CD\u65B0\u8FDE\u63A5\u5176\u4E2D\u4E00\u4E2A\u5373\u53EF\u6062\u590D\u3002",
6788
6961
  identity: "\u8EAB\u4EFD",
6789
6962
  userId: "\u7528\u6237 ID",
6790
6963
  carrierAddr: "Carrier \u5730\u5740",
@@ -7079,7 +7252,7 @@ function DkApp() {
7079
7252
  return hasTab(tabs, want) || DK_PEOPLE_TABS.includes(want) && showPeople ? want : firstTab;
7080
7253
  });
7081
7254
  const [activeId, setActiveId] = React.useState(null);
7082
- const data = useDaemonData();
7255
+ const data = useDaemonData({ exits: tab === "network" });
7083
7256
  const me = data.me;
7084
7257
  const [upd, setUpd] = React.useState(null);
7085
7258
  React.useEffect(() => {
@@ -7442,6 +7615,10 @@ function DkApp() {
7442
7615
  const onEdit = (name, description) => dkApi.setProfile(name, description).then(data.refresh);
7443
7616
  const onSetExit = () => {
7444
7617
  };
7618
+ const onReconnectExit = (ip) => (ip ? dkApi.reconnectExit(ip) : dkApi.reconnectAllExits()).then((r) => {
7619
+ data.refresh();
7620
+ return r;
7621
+ });
7445
7622
  const onOpenChat = (id) => {
7446
7623
  setTab("chat");
7447
7624
  openConversation(id);
@@ -7521,7 +7698,7 @@ function DkApp() {
7521
7698
  fontWeight: 600,
7522
7699
  background: tab === n.id ? "var(--accent)" : "transparent",
7523
7700
  color: tab === n.id ? "#fff" : "var(--dim)"
7524
- } }, /* @__PURE__ */ React.createElement(Icon, { name: n.icon, size: 14, stroke: 2, color: tab === n.id ? "#fff" : "var(--dim)" }), n.label))), tab === "chat" && /* @__PURE__ */ React.createElement(ChatTab, { T, lang: t.lang, phone, isProtocol, protocolFix, onBack: closeConversation, peers, requests, activeId, thread: activeId === ASSISTANT_ID ? assistant.thread : data.threads[activeId], onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread: () => activeId && data.loadThread(activeId), hasEarlier: !!(activeId && activeId !== ASSISTANT_ID && data.earlier[activeId]), onAssistantRemove: activeId === ASSISTANT_ID ? assistant.removeFriends : null, onAssistantAdopt: activeId === ASSISTANT_ID ? assistant.adoptServicePeers : null, onLoadEarlier: () => activeId && data.loadEarlier(activeId), prefillAddr: pendingAddr, onPrefillConsumed: () => setPendingAddr(""), muted: mutedIds, onToggleMute }), tab === "here" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "bridge", peers, meId: me.userId, onAdd, onOpenChat }), tab === "recommended" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "recommended", peers, meId: me.userId, onAdd, onOpenChat }), tab === "registered" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "registered", peers, meId: me.userId, onAdd, onOpenChat }), tab === "network" && /* @__PURE__ */ React.createElement(NetworkTab, { T, me, peers, exits, activeExit, reqCount: requests.length, onSetExit, onOpenChat, backend, onArmLan: armLan, onCancelLan: cancelLan }), tab === "apps" && /* @__PURE__ */ React.createElement(AppsTab, { T, t, me, peers, phone, setTab }), tabs.map((x) => x.kind === "host" && tab === x.id ? /* @__PURE__ */ React.createElement(HostTabPane, { key: x.id, tab: x, props: { T, t, me, peers, phone, setTab }, lang: t.lang }) : null), tab === "profile" && /* @__PURE__ */ React.createElement(
7701
+ } }, /* @__PURE__ */ React.createElement(Icon, { name: n.icon, size: 14, stroke: 2, color: tab === n.id ? "#fff" : "var(--dim)" }), n.label))), tab === "chat" && /* @__PURE__ */ React.createElement(ChatTab, { T, lang: t.lang, phone, isProtocol, protocolFix, onBack: closeConversation, peers, requests, activeId, thread: activeId === ASSISTANT_ID ? assistant.thread : data.threads[activeId], onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread: () => activeId && data.loadThread(activeId), hasEarlier: !!(activeId && activeId !== ASSISTANT_ID && data.earlier[activeId]), onAssistantRemove: activeId === ASSISTANT_ID ? assistant.removeFriends : null, onAssistantAdopt: activeId === ASSISTANT_ID ? assistant.adoptServicePeers : null, onLoadEarlier: () => activeId && data.loadEarlier(activeId), prefillAddr: pendingAddr, onPrefillConsumed: () => setPendingAddr(""), muted: mutedIds, onToggleMute }), tab === "here" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "bridge", peers, meId: me.userId, onAdd, onOpenChat }), tab === "recommended" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "recommended", peers, meId: me.userId, onAdd, onOpenChat }), tab === "registered" && /* @__PURE__ */ React.createElement(DiscoverTab, { T, kind: "registered", peers, meId: me.userId, onAdd, onOpenChat }), tab === "network" && /* @__PURE__ */ React.createElement(NetworkTab, { T, me, peers, exits, activeExit, exitFeature: data.exitFeature, routeFallback: data.routeFallback, onReconnectExit, reqCount: requests.length, onSetExit, onOpenChat, backend, onArmLan: armLan, onCancelLan: cancelLan }), tab === "apps" && /* @__PURE__ */ React.createElement(AppsTab, { T, t, me, peers, phone, setTab }), tabs.map((x) => x.kind === "host" && tab === x.id ? /* @__PURE__ */ React.createElement(HostTabPane, { key: x.id, tab: x, props: { T, t, me, peers, phone, setTab }, lang: t.lang }) : null), tab === "profile" && /* @__PURE__ */ React.createElement(
7525
7702
  ProfileTab,
7526
7703
  {
7527
7704
  T,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/beagle-ui",
3
- "version": "0.2.35",
3
+ "version": "0.2.37",
4
4
  "description": "The Beagle chat UI, minus the machine it runs on — shared by the desktop install and the browser build.",
5
5
  "license": "MIT",
6
6
  "type": "module",