@decentnetwork/beagle 0.1.95 → 0.1.97

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- window.__DK_UI_VERSION="0.1.95";
1
+ window.__DK_UI_VERSION="0.1.97";
2
2
  "use strict";
3
3
  (() => {
4
4
  // packages/beagle-ui/dist/beagle-ui.js
@@ -110,6 +110,7 @@ window.__DK_UI_VERSION="0.1.95";
110
110
  unlink: '<path d="M9 15 5.5 18.5a3.5 3.5 0 0 1-5-5L4 10"/><path d="m15 9 3.5-3.5a3.5 3.5 0 0 1 5 5L20 14"/><path d="M8 8 4 4M16 16l4 4M3 11h2M11 3v2"/>',
111
111
  zap: '<path d="M13 2 4 13h7l-1 9 9-11h-7l1-9Z"/>',
112
112
  clock: '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3.5 2"/>',
113
+ bellOff: '<path d="M8.7 3.4A6 6 0 0 1 18 8.5c0 3 .5 5 1.1 6.3M6.1 6.1A6 6 0 0 0 6 8.5c0 6.5-2.5 8-2.5 8H16"/><path d="M13.7 20.5a2 2 0 0 1-3.4 0"/><path d="m3 3 18 18"/>',
113
114
  plug: '<path d="M9 2v6M15 2v6"/><path d="M7 8h10v3a5 5 0 0 1-10 0V8Z"/><path d="M12 16v6"/>'
114
115
  });
115
116
  var H = {};
@@ -493,7 +494,7 @@ window.__DK_UI_VERSION="0.1.95";
493
494
  paddingLeft: 8
494
495
  } }, 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))))));
495
496
  }
496
- function PeerRow({ peer, T, active, onClick }) {
497
+ function PeerRow({ peer, T, active, onClick, muted }) {
497
498
  const name = peer.alias || peer.userId;
498
499
  return /* @__PURE__ */ React.createElement("button", { onClick, style: {
499
500
  display: "flex",
@@ -515,7 +516,7 @@ window.__DK_UI_VERSION="0.1.95";
515
516
  whiteSpace: "nowrap",
516
517
  overflow: "hidden",
517
518
  textOverflow: "ellipsis"
518
- } }, peer.alias || shortKey(peer.userId, 8, 5)), peer.agent && /* @__PURE__ */ React.createElement(Icon, { name: "bot", size: 13, color: "var(--faint)", stroke: 2 }), peer.wire === "64" && /* @__PURE__ */ React.createElement(Tag, { tone: "off", style: { height: 15, fontSize: 9.5, padding: "0 4px" } }, "w64")), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 6, marginTop: 2 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)" } }, peer.ip), /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)", fontSize: 11 } }, "\xB7"), /* @__PURE__ */ React.createElement("span", { style: {
519
+ } }, peer.alias || shortKey(peer.userId, 8, 5)), peer.agent && /* @__PURE__ */ React.createElement(Icon, { name: "bot", size: 13, color: "var(--faint)", stroke: 2 }), muted && /* @__PURE__ */ React.createElement(Icon, { name: "bellOff", size: 12, color: "var(--faint)", stroke: 2, title: T && T.mutedTag || "sound muted" }), peer.wire === "64" && /* @__PURE__ */ React.createElement(Tag, { tone: "off", style: { height: 15, fontSize: 9.5, padding: "0 4px" } }, "w64")), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 6, marginTop: 2 } }, /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 11, color: "var(--faint)" } }, peer.ip), /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)", fontSize: 11 } }, "\xB7"), /* @__PURE__ */ React.createElement("span", { style: {
519
520
  fontSize: 11,
520
521
  color: "var(--dim)",
521
522
  whiteSpace: "nowrap",
@@ -528,7 +529,7 @@ window.__DK_UI_VERSION="0.1.95";
528
529
  function dkActivityTs(p) {
529
530
  return Math.max(p.lastTs || 0, p.acceptedAt || 0, p.requestedAt || 0);
530
531
  }
531
- function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed }) {
532
+ function PeerSidebar({ T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed, muted }) {
532
533
  const [q, setQ] = React.useState("");
533
534
  const [addr, setAddr] = React.useState("");
534
535
  const [addState, setAddState] = React.useState(null);
@@ -610,7 +611,7 @@ window.__DK_UI_VERSION="0.1.95";
610
611
  fontSize: 11.5,
611
612
  lineHeight: 1.35,
612
613
  color: addState.kind === "err" ? "var(--bad, #e5484d)" : addState.kind === "ok" ? "var(--good, #46a758)" : "var(--faint)"
613
- } }, addState.msg), /* @__PURE__ */ React.createElement("div", { style: { position: "relative", display: "flex", alignItems: "center" } }, /* @__PURE__ */ React.createElement(Icon, { name: "search", size: 15, color: "var(--faint)", stroke: 2, style: { position: "absolute", left: 9 } }), /* @__PURE__ */ React.createElement("input", { value: q, onChange: (e) => setQ(e.target.value), placeholder: T.search, style: { ...inputStyle, paddingLeft: 30 } }))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, overflow: "auto", padding: "10px 8px 16px" } }, /* @__PURE__ */ React.createElement(RequestsBlock, { T, requests, onAct }), /* @__PURE__ */ React.createElement(Section, { label: T.peers, count: `${online}/${peers.length}`, style: { margin: "6px 4px 8px" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 2 } }, filtered.map((p) => /* @__PURE__ */ React.createElement(PeerRow, { key: p.id, peer: p, T, active: p.id === activeId, onClick: () => onSelect(p.id) })), !filtered.length && /* @__PURE__ */ React.createElement("div", { style: { padding: 16, textAlign: "center", fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)" } }, "no matches"))));
614
+ } }, addState.msg), /* @__PURE__ */ React.createElement("div", { style: { position: "relative", display: "flex", alignItems: "center" } }, /* @__PURE__ */ React.createElement(Icon, { name: "search", size: 15, color: "var(--faint)", stroke: 2, style: { position: "absolute", left: 9 } }), /* @__PURE__ */ React.createElement("input", { value: q, onChange: (e) => setQ(e.target.value), placeholder: T.search, style: { ...inputStyle, paddingLeft: 30 } }))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, overflow: "auto", padding: "10px 8px 16px" } }, /* @__PURE__ */ React.createElement(RequestsBlock, { T, requests, onAct }), /* @__PURE__ */ React.createElement(Section, { label: T.peers, count: `${online}/${peers.length}`, style: { margin: "6px 4px 8px" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 2 } }, filtered.map((p) => /* @__PURE__ */ React.createElement(PeerRow, { key: p.id, peer: p, T, active: p.id === activeId, onClick: () => onSelect(p.id), muted: !!muted && muted.includes(p.id) })), !filtered.length && /* @__PURE__ */ React.createElement("div", { style: { padding: 16, textAlign: "center", fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)" } }, "no matches"))));
614
615
  }
615
616
  var inputStyle = {
616
617
  flex: 1,
@@ -1595,7 +1596,7 @@ window.__DK_UI_VERSION="0.1.95";
1595
1596
  ) : mine && m.status === "sent" && m.confirmed === false ? /* @__PURE__ */ React.createElement(Icon, { name: "check", size: 12, stroke: 2.2, color: "var(--faint)", title: T.sentUnconfirmed || "sent \u2014 not confirmed by the peer" }) : mine && m.status && /* @__PURE__ */ React.createElement(Icon, { name: "checkCheck", size: 12, stroke: 2.2, color: m.status === "read" ? "var(--accent)" : "var(--faint)" })))
1596
1597
  );
1597
1598
  }
1598
- function Conversation({ T, peer, lang, peers, onOpenChat, thread: threadProp, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread }) {
1599
+ function Conversation({ T, peer, lang, peers, onOpenChat, thread: threadProp, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, muted, onToggleMute }) {
1599
1600
  const scrollRef = React.useRef(null);
1600
1601
  const isGroup = React.useMemo(() => dkThreadIsGroup(threadProp), [threadProp]);
1601
1602
  const followScrollRef = React.useRef(true);
@@ -1879,7 +1880,14 @@ window.__DK_UI_VERSION="0.1.95";
1879
1880
  ${peer.address}`
1880
1881
  },
1881
1882
  shortKey(peer.address, 8, 6)
1882
- )))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), onCall && /* @__PURE__ */ React.createElement(Btn, { icon: "phone", title: T && T.audioCall || "Audio call", onClick: () => onCall(peer.userId, false) }), onCall && /* @__PURE__ */ React.createElement(Btn, { icon: "video", title: T && T.videoCall || "Video call", onClick: () => onCall(peer.userId, true) }), onSendRtcFile && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(
1883
+ )))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), onToggleMute && /* @__PURE__ */ React.createElement(
1884
+ Btn,
1885
+ {
1886
+ icon: muted ? "bellOff" : "bell",
1887
+ title: muted ? T && T.unmuteSound || "Turn sound back on for this chat" : T && T.muteSound || "Mute sound for this chat",
1888
+ onClick: () => onToggleMute(peer.id)
1889
+ }
1890
+ ), onCall && /* @__PURE__ */ React.createElement(Btn, { icon: "phone", title: T && T.audioCall || "Audio call", onClick: () => onCall(peer.userId, false) }), onCall && /* @__PURE__ */ React.createElement(Btn, { icon: "video", title: T && T.videoCall || "Video call", onClick: () => onCall(peer.userId, true) }), onSendRtcFile && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(
1883
1891
  "input",
1884
1892
  {
1885
1893
  ref: rtcFileRef,
@@ -2071,9 +2079,9 @@ ${peer.address}`
2071
2079
  function ChatEmpty({ T }) {
2072
2080
  return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 12, color: "var(--faint)", background: "var(--bg)" } }, /* @__PURE__ */ React.createElement(Icon, { name: "message", size: 40, stroke: 1.4, color: "var(--line)" }), /* @__PURE__ */ React.createElement("div", { style: { fontFamily: "var(--mono)", fontSize: 13 } }, T.pickPeer));
2073
2081
  }
2074
- function ChatTab({ T, lang, peers, requests, activeId, thread, onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, prefillAddr, onPrefillConsumed }) {
2082
+ function ChatTab({ T, lang, peers, requests, activeId, thread, onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, prefillAddr, onPrefillConsumed, muted, onToggleMute }) {
2075
2083
  const peer = peers.find((p) => p.id === activeId);
2076
- return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", minWidth: 0, minHeight: 0 } }, /* @__PURE__ */ React.createElement(PeerSidebar, { T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed }), peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, peers, onOpenChat: onSelect, thread, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread }) : /* @__PURE__ */ React.createElement(ChatEmpty, { T }));
2084
+ return /* @__PURE__ */ React.createElement("div", { style: { flex: 1, display: "flex", minWidth: 0, minHeight: 0 } }, /* @__PURE__ */ React.createElement(PeerSidebar, { T, peers, requests, activeId, onSelect, onAct, onAdd, prefillAddr, onPrefillConsumed, muted }), peer ? /* @__PURE__ */ React.createElement(Conversation, { T, peer, lang, peers, onOpenChat: onSelect, thread, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread, muted: !!muted && muted.includes(peer.id), onToggleMute }) : /* @__PURE__ */ React.createElement(ChatEmpty, { T }));
2077
2085
  }
2078
2086
  Object.assign(window, { ChatTab });
2079
2087
  function CopyBtn({ value, copiedText = "Copied", copyFailedText = "Copy failed", copyTitle = "Copy" }) {
@@ -4447,8 +4455,10 @@ ${peer.address}`
4447
4455
  function dkLooking() {
4448
4456
  return document.visibilityState === "visible" && document.hasFocus();
4449
4457
  }
4450
- function useIncomingAlerts({ T, peers, requests, loaded, activeId, sound = true, flash = true }) {
4458
+ function useIncomingAlerts({ T, peers, requests, loaded, activeId, sound = true, flash = true, muted }) {
4451
4459
  const prev = React.useRef(null);
4460
+ const mutedRef = React.useRef(/* @__PURE__ */ new Set());
4461
+ mutedRef.current = new Set(muted || []);
4452
4462
  React.useEffect(() => dkUnlockAudioOnGesture(), []);
4453
4463
  React.useEffect(() => {
4454
4464
  if (!flash)
@@ -4482,7 +4492,7 @@ ${peer.address}`
4482
4492
  prev.current = { byId, reqIds };
4483
4493
  if (!was)
4484
4494
  return;
4485
- let msgs = 0, elsewhere = 0, reqs = 0;
4495
+ let msgs = 0, elsewhere = 0, reqs = 0, loud = 0, loudElsewhere = 0;
4486
4496
  for (const [id, cur] of byId) {
4487
4497
  if (cur.dir !== "in" || !cur.ts)
4488
4498
  continue;
@@ -4492,6 +4502,11 @@ ${peer.address}`
4492
4502
  msgs += 1;
4493
4503
  if (id !== activeId)
4494
4504
  elsewhere += 1;
4505
+ if (!mutedRef.current.has(id)) {
4506
+ loud += 1;
4507
+ if (id !== activeId)
4508
+ loudElsewhere += 1;
4509
+ }
4495
4510
  }
4496
4511
  for (const id of reqIds)
4497
4512
  if (!was.reqIds.has(id))
@@ -4499,8 +4514,8 @@ ${peer.address}`
4499
4514
  if (!msgs && !reqs)
4500
4515
  return;
4501
4516
  const looking = dkLooking();
4502
- if (sound && !(looking && !reqs && !elsewhere))
4503
- dkChime(reqs && !msgs ? "request" : "message");
4517
+ if (sound && (reqs || loud) && !(looking && !reqs && !loudElsewhere))
4518
+ dkChime(reqs && !loud ? "request" : "message");
4504
4519
  if (flash && !looking) {
4505
4520
  const label = msgs ? T && T.alertMsg || "New message" : T && T.alertReq || "New friend request";
4506
4521
  dkFlashStart(label, msgs + reqs);
@@ -4637,6 +4652,9 @@ ${peer.address}`
4637
4652
  profile: "Profile",
4638
4653
  alertMsg: "New message",
4639
4654
  alertReq: "New friend request",
4655
+ muteSound: "Mute sound for this chat",
4656
+ unmuteSound: "Turn sound back on for this chat",
4657
+ mutedTag: "sound muted",
4640
4658
  recommended: "Discover",
4641
4659
  registered: "Names",
4642
4660
  here: "Here",
@@ -4819,6 +4837,9 @@ ${peer.address}`
4819
4837
  profile: "\u6211\u7684",
4820
4838
  alertMsg: "\u65B0\u6D88\u606F",
4821
4839
  alertReq: "\u65B0\u7684\u597D\u53CB\u8BF7\u6C42",
4840
+ muteSound: "\u5173\u95ED\u6B64\u4F1A\u8BDD\u7684\u63D0\u793A\u97F3",
4841
+ unmuteSound: "\u6062\u590D\u6B64\u4F1A\u8BDD\u7684\u63D0\u793A\u97F3",
4842
+ mutedTag: "\u5DF2\u9759\u97F3",
4822
4843
  recommended: "\u63A8\u8350",
4823
4844
  registered: "\u540D\u5F55",
4824
4845
  here: "\u5728\u573A",
@@ -5217,6 +5238,8 @@ ${peer.address}`
5217
5238
  const T = STR[t.lang] || STR.en;
5218
5239
  const vars = dkTheme(t.theme, t.accent);
5219
5240
  const rowPad = t.density === "comfortable" ? "11px 12px" : "7px 10px";
5241
+ const mutedIds = Array.isArray(t.alertMuted) ? t.alertMuted : [];
5242
+ const onToggleMute = (id) => setTweak("alertMuted", mutedIds.includes(id) ? mutedIds.filter((x) => x !== id) : mutedIds.concat(id));
5220
5243
  useIncomingAlerts({
5221
5244
  T,
5222
5245
  peers,
@@ -5224,7 +5247,8 @@ ${peer.address}`
5224
5247
  loaded: data.loaded,
5225
5248
  activeId,
5226
5249
  sound: t.alertSound !== false,
5227
- flash: t.alertFlash !== false
5250
+ flash: t.alertFlash !== false,
5251
+ muted: mutedIds
5228
5252
  });
5229
5253
  const callCtl = useCallController(me.userId, (peerId, video, direction) => {
5230
5254
  const text = `WebRTC ${video ? "video" : "audio"} call: ${direction}`;
@@ -5376,7 +5400,7 @@ ${peer.address}`
5376
5400
  display: "flex",
5377
5401
  border: "1px solid " + (tab === "profile" ? "var(--line)" : "transparent"),
5378
5402
  background: tab === "profile" ? "var(--panel-2)" : "transparent"
5379
- } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 36, radius: 9 }))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, minHeight: 0, display: "flex", flexDirection: "column" } }, /* @__PURE__ */ React.createElement(DkBrowserNotice, { lang: t.lang }), /* @__PURE__ */ React.createElement("div", { style: { height: 46, flexShrink: 0, borderBottom: "1px solid var(--line)", background: "var(--panel)", display: "flex", alignItems: "center", gap: 12, padding: "0 16px" } }, /* @__PURE__ */ React.createElement("svg", { width: 20, height: 20, viewBox: "0 0 24 24", fill: "none", stroke: "var(--accent)", strokeWidth: 2.2, strokeLinecap: "round", strokeLinejoin: "round", style: { display: "block", flexShrink: 0 } }, /* @__PURE__ */ React.createElement("path", { d: "m4.5 17 6-6-6-6" }), /* @__PURE__ */ React.createElement("path", { d: "M12 18.5h7.5" })), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700, letterSpacing: -0.3, color: "var(--text)" } }, "beagle"), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)" } }, "\xB7 ", (nav.find((n) => n.id === tab) || { label: T.profile }).label.toLowerCase()), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, me.channel, " \xB7 lan ", me.lanVer), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 7, padding: "0 4px" } }, /* @__PURE__ */ React.createElement(StatusDot, { online: me.online }), /* @__PURE__ */ React.createElement(Mono, { size: 12.5, copy: me.ip }, me.ip)), /* @__PURE__ */ React.createElement("span", { style: { width: 1, height: 22, background: "var(--line)" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 26, radius: 7 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: "var(--text)" } }, me.name))), tab === "chat" && /* @__PURE__ */ React.createElement(ChatTab, { T, lang: t.lang, peers, requests, activeId, thread: data.threads[activeId], onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread: () => activeId && data.loadThread(activeId), prefillAddr: pendingAddr, onPrefillConsumed: () => setPendingAddr("") }), 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 }), tab === "profile" && /* @__PURE__ */ React.createElement(ProfileTab, { T, me, onEdit, t, setTweak })), data.locked && /* @__PURE__ */ React.createElement(
5403
+ } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 36, radius: 9 }))), /* @__PURE__ */ React.createElement("div", { style: { flex: 1, minWidth: 0, minHeight: 0, display: "flex", flexDirection: "column" } }, /* @__PURE__ */ React.createElement(DkBrowserNotice, { lang: t.lang }), /* @__PURE__ */ React.createElement("div", { style: { height: 46, flexShrink: 0, borderBottom: "1px solid var(--line)", background: "var(--panel)", display: "flex", alignItems: "center", gap: 12, padding: "0 16px" } }, /* @__PURE__ */ React.createElement("svg", { width: 20, height: 20, viewBox: "0 0 24 24", fill: "none", stroke: "var(--accent)", strokeWidth: 2.2, strokeLinecap: "round", strokeLinejoin: "round", style: { display: "block", flexShrink: 0 } }, /* @__PURE__ */ React.createElement("path", { d: "m4.5 17 6-6-6-6" }), /* @__PURE__ */ React.createElement("path", { d: "M12 18.5h7.5" })), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 14, fontWeight: 700, letterSpacing: -0.3, color: "var(--text)" } }, "beagle"), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12, color: "var(--faint)" } }, "\xB7 ", (nav.find((n) => n.id === tab) || { label: T.profile }).label.toLowerCase()), /* @__PURE__ */ React.createElement("div", { style: { flex: 1 } }), /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, me.channel, " \xB7 lan ", me.lanVer), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 7, padding: "0 4px" } }, /* @__PURE__ */ React.createElement(StatusDot, { online: me.online }), /* @__PURE__ */ React.createElement(Mono, { size: 12.5, copy: me.ip }, me.ip)), /* @__PURE__ */ React.createElement("span", { style: { width: 1, height: 22, background: "var(--line)" } }), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer: { ...me, id: me.userId, agent: false }, size: 26, radius: 7 }), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "var(--mono)", fontSize: 12.5, fontWeight: 600, color: "var(--text)" } }, me.name))), tab === "chat" && /* @__PURE__ */ React.createElement(ChatTab, { T, lang: t.lang, peers, requests, activeId, thread: data.threads[activeId], onSelect, onAct, onAdd, onSend, onSendFile, onSendRtcFile, onAlias, onRemove, onOpenNet, onCall, onReloadThread: () => activeId && data.loadThread(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 }), tab === "profile" && /* @__PURE__ */ React.createElement(ProfileTab, { T, me, onEdit, t, setTweak })), data.locked && /* @__PURE__ */ React.createElement(
5380
5404
  DkLockedOut,
5381
5405
  {
5382
5406
  compact: true,
package/dist/ipc.d.ts CHANGED
@@ -44,9 +44,13 @@ export declare function ipcSocketPath(dataDir: string, platform?: NodeJS.Platfor
44
44
  * Windows named pipes, so on Windows we connect and let net report. */
45
45
  export declare function ipcSocketSupportsFsExistenceCheck(platform?: NodeJS.Platform): boolean;
46
46
  /** How long the liveness probe waits for a connect before calling it dead.
47
- * Local IPC: a listening socket accepts in microseconds, so this is a
48
- * generous ceiling that still keeps the 1500ms watch tick honest. */
49
- export declare const DAEMON_PROBE_TIMEOUT_MS = 400;
47
+ * A listening local socket accepts in well under a millisecond, so this
48
+ * budget never measured the daemon — it measured how long OUR event loop took
49
+ * to run the connect callback. At 400 ms a GC pause or a big JSON body in
50
+ * beagle read as "daemon gone": 814 false flaps on mac-dev while the daemon's
51
+ * pid never changed. One miss is not believed either; see DAEMON_MISS_STREAK
52
+ * in lan-handoff.ts. */
53
+ export declare const DAEMON_PROBE_TIMEOUT_MS = 2000;
50
54
  /**
51
55
  * Is a decentlan daemon actually LISTENING for this identity?
52
56
  *
package/dist/ipc.js CHANGED
@@ -28,9 +28,13 @@ export function ipcSocketSupportsFsExistenceCheck(platform = process.platform) {
28
28
  return platform !== "win32";
29
29
  }
30
30
  /** How long the liveness probe waits for a connect before calling it dead.
31
- * Local IPC: a listening socket accepts in microseconds, so this is a
32
- * generous ceiling that still keeps the 1500ms watch tick honest. */
33
- export const DAEMON_PROBE_TIMEOUT_MS = 400;
31
+ * A listening local socket accepts in well under a millisecond, so this
32
+ * budget never measured the daemon — it measured how long OUR event loop took
33
+ * to run the connect callback. At 400 ms a GC pause or a big JSON body in
34
+ * beagle read as "daemon gone": 814 false flaps on mac-dev while the daemon's
35
+ * pid never changed. One miss is not believed either; see DAEMON_MISS_STREAK
36
+ * in lan-handoff.ts. */
37
+ export const DAEMON_PROBE_TIMEOUT_MS = 2000;
34
38
  /**
35
39
  * Is a decentlan daemon actually LISTENING for this identity?
36
40
  *
@@ -6,6 +6,13 @@ export type HandoffState = "embedded" | "releasing" | "daemon" | "waiting";
6
6
  * gives up on it and takes the identity back. `agentnet restart` and a
7
7
  * launchd/systemd kickstart are both back well inside this. */
8
8
  export declare const DAEMON_GRACE_MS: number;
9
+ /** One missed probe is not a verdict. The probe runs on beagle's own event
10
+ * loop, so any stall here (GC, a large /api/desktop body, an npm child) fires
11
+ * its timer before the queued `connect` callback runs, and a healthy daemon
12
+ * reads as gone — 814 times on mac-dev while daemon pid 75338 never changed.
13
+ * Only a streak of misses that also spans real time is believed. */
14
+ export declare const DAEMON_MISS_STREAK = 3;
15
+ export declare const DAEMON_MISS_WINDOW_MS = 5000;
9
16
  export interface SwitchableHostOptions {
10
17
  dataDir: string;
11
18
  /** Build a fresh embedded host. Called on first start and on every fallback,
@@ -37,6 +44,9 @@ export declare class SwitchablePeerHost implements PeerHost {
37
44
  get kind(): PeerHost["kind"];
38
45
  get hasVirtualLan(): boolean;
39
46
  get state(): HandoffState;
47
+ /** The daemon's data dir (socket and pidfile), for callers that need to
48
+ * watch a daemon restart through. */
49
+ get dataDir(): string;
40
50
  /** Seconds left before `releasing` gives up; 0 when not releasing. */
41
51
  get releaseSecondsLeft(): number;
42
52
  call(req: IpcRequest): Promise<IpcResponse>;
@@ -23,6 +23,13 @@ const RELEASE_TIMEOUT_MS = 10 * 60 * 1000;
23
23
  * gives up on it and takes the identity back. `agentnet restart` and a
24
24
  * launchd/systemd kickstart are both back well inside this. */
25
25
  export const DAEMON_GRACE_MS = 60 * 1000;
26
+ /** One missed probe is not a verdict. The probe runs on beagle's own event
27
+ * loop, so any stall here (GC, a large /api/desktop body, an npm child) fires
28
+ * its timer before the queued `connect` callback runs, and a healthy daemon
29
+ * reads as gone — 814 times on mac-dev while daemon pid 75338 never changed.
30
+ * Only a streak of misses that also spans real time is believed. */
31
+ export const DAEMON_MISS_STREAK = 3;
32
+ export const DAEMON_MISS_WINDOW_MS = 5000;
26
33
  /**
27
34
  * A PeerHost whose backend can change under it.
28
35
  *
@@ -59,6 +66,11 @@ export class SwitchablePeerHost {
59
66
  get state() {
60
67
  return this.#state;
61
68
  }
69
+ /** The daemon's data dir (socket and pidfile), for callers that need to
70
+ * watch a daemon restart through. */
71
+ get dataDir() {
72
+ return this.#opts.dataDir;
73
+ }
62
74
  /** Seconds left before `releasing` gives up; 0 when not releasing. */
63
75
  get releaseSecondsLeft() {
64
76
  if (this.#state !== "releasing")
@@ -130,14 +142,33 @@ export class SwitchablePeerHost {
130
142
  }, WATCH_INTERVAL_MS);
131
143
  this.#timer.unref?.();
132
144
  }
145
+ /** Consecutive missed probes; 0 the moment the daemon answers. */
146
+ #missStreak = 0;
147
+ /** A probe can take its full 2 s timeout against a 1.5 s watch interval.
148
+ * Overlapping ticks would each act on the other's half-made decision. */
149
+ #ticking = false;
133
150
  async #tick() {
134
- if (this.#busy)
151
+ if (this.#busy || this.#ticking)
135
152
  return;
153
+ this.#ticking = true;
154
+ try {
155
+ await this.#step();
156
+ }
157
+ finally {
158
+ this.#ticking = false;
159
+ }
160
+ }
161
+ async #step() {
136
162
  const daemonUp = await this.#probe(this.#opts.dataDir);
137
- if (daemonUp)
163
+ if (daemonUp) {
138
164
  this.#daemonMissingSince = 0;
139
- else if (this.#daemonMissingSince === 0)
140
- this.#daemonMissingSince = Date.now();
165
+ this.#missStreak = 0;
166
+ }
167
+ else {
168
+ if (this.#daemonMissingSince === 0)
169
+ this.#daemonMissingSince = Date.now();
170
+ this.#missStreak++;
171
+ }
141
172
  // A tick can await; make sure nothing started while we were probing.
142
173
  if (this.#busy)
143
174
  return;
@@ -152,7 +183,14 @@ export class SwitchablePeerHost {
152
183
  // and rm the pidfile.
153
184
  if (this.#opts.force === "daemon") {
154
185
  if (this.#state === "daemon" && !daemonUp) {
155
- this.#log.info("the daemon went away — waiting for it to come back (forced daemon backend)");
186
+ // Keep serving until the absence is confirmed. Every call made during
187
+ // a false `waiting` failed with "the daemon is restarting", which the
188
+ // UI showed as an empty friend list and a hung /api/desktop.
189
+ const missedMs = Date.now() - this.#daemonMissingSince;
190
+ if (this.#missStreak < DAEMON_MISS_STREAK || missedMs < DAEMON_MISS_WINDOW_MS)
191
+ return;
192
+ this.#log.info(`the daemon went away — waiting for it to come back (forced daemon backend; ` +
193
+ `${this.#missStreak} probes missed over ${(missedMs / 1000).toFixed(1)} s)`);
156
194
  const old = this.#inner;
157
195
  this.#inner = null;
158
196
  this.#setState("waiting");
package/dist/server.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { IpcRequest, IpcResponse } from "./ipc.js";
1
+ import { type IpcRequest, type IpcResponse } from "./ipc.js";
2
2
  /** How this copy of beagle got onto disk — decides what "update" can do.
3
3
  * npx cache paths contain _npx AND node_modules, so test _npx first. */
4
4
  export declare function installMode(): "global" | "npx" | "dev" | "unknown";
@@ -32,6 +32,8 @@ export interface BeagleServerOptions {
32
32
  readonly hasVirtualLan: boolean;
33
33
  readonly state: string;
34
34
  readonly releaseSecondsLeft: number;
35
+ /** The daemon's socket/pidfile dir, so "Update lan" can wait for a new pid. */
36
+ readonly dataDir?: string;
35
37
  armForDaemon: () => Promise<{
36
38
  state: string;
37
39
  command: string;
package/dist/server.js CHANGED
@@ -19,6 +19,7 @@ import { fileURLToPath } from "node:url";
19
19
  import { dirname, join } from "node:path";
20
20
  import { INSTALL_COMMAND as LAN_INSTALL_COMMAND } from "./lan-handoff.js";
21
21
  import yaml from "js-yaml";
22
+ import { probeDaemon } from "./ipc.js";
22
23
  import { DEFAULT_EXITS } from "./exits.js";
23
24
  import * as peerAddr from "@decentnetwork/peer";
24
25
  import { bridgeRoster } from "./bridge-roster.js";
@@ -513,10 +514,45 @@ async function npmQuery(args) {
513
514
  }
514
515
  });
515
516
  }
516
- /** Ask the running agentnet daemon to re-exec itself so it picks up the
517
- * just-installed lan version. Tries the PATH binary, then the global
518
- * install's CLI entry directly (pm2 PATH often lacks the nvm shims). */
519
- async function agentnetRestart() {
517
+ /** Ask the agentnet daemon to restart onto the lan that was just installed,
518
+ * and report success only when a NEW daemon answers. `agentnet restart`
519
+ * exiting 0 means the request was accepted, nothing more: under launchd a
520
+ * lan ≥ 0.1.302 daemon exits and launchd starts the new code, while an older
521
+ * one re-execs outside launchd — which left mac-dev's KeepAlive job failing
522
+ * every ~10 s. Either way the proof is a different pid that accepts a
523
+ * connection. Without a data dir there is nothing to watch, so the old
524
+ * answer stands. */
525
+ async function agentnetRestart(dataDir) {
526
+ const pidBefore = dataDir ? readDaemonPid(dataDir) : "";
527
+ const r = await requestAgentnetRestart();
528
+ if (!r.ok || !dataDir)
529
+ return r;
530
+ return waitForNewDaemon(dataDir, pidBefore);
531
+ }
532
+ const readDaemonPid = (dataDir) => {
533
+ try {
534
+ return readFileSync(join(dataDir, "daemon.pid"), "utf-8").trim();
535
+ }
536
+ catch {
537
+ return "";
538
+ }
539
+ };
540
+ async function waitForNewDaemon(dataDir, pidBefore) {
541
+ const deadline = Date.now() + 60_000;
542
+ while (Date.now() < deadline) {
543
+ const pid = readDaemonPid(dataDir);
544
+ if (pid && pid !== pidBefore && (await probeDaemon(dataDir)))
545
+ return { ok: true };
546
+ await new Promise((r) => setTimeout(r, 1000));
547
+ }
548
+ return {
549
+ ok: false,
550
+ error: `agentnet accepted the restart, but no new daemon answered within 60 s (pid still ${readDaemonPid(dataDir) || "none"})`,
551
+ };
552
+ }
553
+ /** Run `agentnet restart`: the PATH binary, then the global install's CLI entry
554
+ * directly (pm2's PATH often lacks the nvm shims). */
555
+ async function requestAgentnetRestart() {
520
556
  const { spawn } = await import("node:child_process");
521
557
  const cliJs = join(dirname(process.execPath), "..", "lib", "node_modules", "@decentnetwork", "lan", "dist", "cli", "index.js");
522
558
  const attempts = [
@@ -1826,7 +1862,7 @@ export function startBeagleServer(opts) {
1826
1862
  sendJson(res, 502, inst);
1827
1863
  return;
1828
1864
  }
1829
- const rr = await agentnetRestart();
1865
+ const rr = await agentnetRestart(opts.lanHost?.dataDir);
1830
1866
  updateCache = null;
1831
1867
  sendJson(res, 200, { ok: true, restarted: rr.ok, ...(rr.ok ? {} : { note: rr.error }) });
1832
1868
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/beagle",
3
- "version": "0.1.95",
3
+ "version": "0.1.97",
4
4
  "description": "Beagle — P2P chat, file transfer and calls for regular users, on the Decent Network. No admin privilege required.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",