@decentnetwork/beagle-ui 0.2.21 → 0.2.23

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 +382 -50
  2. package/package.json +1 -1
package/dist/beagle-ui.js CHANGED
@@ -1,4 +1,4 @@
1
- export const BEAGLE_UI_VERSION = "0.2.21";
1
+ export const BEAGLE_UI_VERSION = "0.2.23";
2
2
  // src/icons.jsx
3
3
  var ICON_PATHS = {
4
4
  // ---- tab bar (the four must feel like one set) ----
@@ -516,23 +516,39 @@ var actions_default = {
516
516
  name: "help",
517
517
  usage: "/help",
518
518
  summary: "List what the Assistant can do.",
519
- params: { type: "object", properties: {}, required: [] },
519
+ params: {
520
+ type: "object",
521
+ properties: {},
522
+ required: []
523
+ },
520
524
  confirm: "none",
521
525
  scope: "assistant"
522
526
  },
523
527
  {
524
528
  name: "friends-list",
525
529
  usage: "/friends list [filter\u2026]",
526
- summary: "Show friends as a table: name, last heard from, messages in/out. With no filter, everyone.",
530
+ summary: "Show friends as a list: name, last heard from, messages in/out. With no filter, everyone. Service peers are never included \u2014 they are /nodes.",
527
531
  params: {
528
532
  type: "object",
529
533
  properties: {
530
534
  filters: {
531
535
  type: "array",
532
- items: { enum: ["never-messaged", "never-replied", "silent", "pending"] },
536
+ items: {
537
+ enum: [
538
+ "never-messaged",
539
+ "never-replied",
540
+ "silent",
541
+ "pending"
542
+ ]
543
+ },
533
544
  description: "Several filters are OR'd together."
534
545
  },
535
- silentDays: { type: "integer", minimum: 1, default: 90, description: "Argument of the silent filter: nothing from them in this many days." }
546
+ silentDays: {
547
+ type: "integer",
548
+ minimum: 1,
549
+ default: 90,
550
+ description: "Argument of the silent filter: nothing from them in this many days."
551
+ }
536
552
  },
537
553
  required: []
538
554
  },
@@ -542,18 +558,94 @@ var actions_default = {
542
558
  {
543
559
  name: "friends-remove",
544
560
  usage: "/friends remove <filter\u2026>",
545
- summary: "Same table with a tick per row and a Remove button. Nothing is removed by typing alone.",
561
+ summary: "Same list with a tick per row and a Remove button. Nothing is removed by typing alone. Service peers are never matched.",
562
+ params: {
563
+ type: "object",
564
+ properties: {
565
+ filters: {
566
+ type: "array",
567
+ items: {
568
+ enum: [
569
+ "never-messaged",
570
+ "never-replied",
571
+ "silent",
572
+ "pending"
573
+ ]
574
+ },
575
+ minItems: 1
576
+ },
577
+ silentDays: {
578
+ type: "integer",
579
+ minimum: 1,
580
+ default: 90
581
+ }
582
+ },
583
+ required: [
584
+ "filters"
585
+ ]
586
+ },
587
+ confirm: "list",
588
+ scope: "assistant"
589
+ },
590
+ {
591
+ name: "nodes-list",
592
+ usage: "/nodes list [filter\u2026]",
593
+ summary: "Service peers \u2014 network nodes and agents \u2014 with their kind and role. The same filters as /friends list.",
594
+ params: {
595
+ type: "object",
596
+ properties: {
597
+ filters: {
598
+ type: "array",
599
+ items: {
600
+ enum: [
601
+ "never-messaged",
602
+ "never-replied",
603
+ "silent",
604
+ "pending"
605
+ ]
606
+ },
607
+ description: "Several filters are OR'd together."
608
+ },
609
+ silentDays: {
610
+ type: "integer",
611
+ minimum: 1,
612
+ default: 90
613
+ }
614
+ },
615
+ required: []
616
+ },
617
+ confirm: "none",
618
+ scope: "assistant"
619
+ },
620
+ {
621
+ name: "nodes-remove",
622
+ usage: "/nodes remove <filter\u2026>",
623
+ summary: "The same table with a tick per row and a Remove button, over service peers only.",
546
624
  params: {
547
625
  type: "object",
548
626
  properties: {
549
627
  filters: {
550
628
  type: "array",
551
- items: { enum: ["never-messaged", "never-replied", "silent", "pending"] },
629
+ items: {
630
+ enum: [
631
+ "never-messaged",
632
+ "never-replied",
633
+ "silent",
634
+ "pending"
635
+ ]
636
+ },
637
+ description: "Several filters are OR'd together.",
552
638
  minItems: 1
553
639
  },
554
- silentDays: { type: "integer", minimum: 1, default: 90 }
640
+ silentDays: {
641
+ type: "integer",
642
+ minimum: 1,
643
+ default: 90
644
+ }
555
645
  },
556
- required: ["filters"]
646
+ required: [
647
+ "filters"
648
+ ]
557
649
  },
558
650
  confirm: "list",
559
651
  scope: "assistant"
@@ -564,8 +656,15 @@ var actions_default = {
564
656
  summary: "Friends whose name, alias, description or ens name contains the text.",
565
657
  params: {
566
658
  type: "object",
567
- properties: { text: { type: "string", minLength: 1 } },
568
- required: ["text"]
659
+ properties: {
660
+ text: {
661
+ type: "string",
662
+ minLength: 1
663
+ }
664
+ },
665
+ required: [
666
+ "text"
667
+ ]
569
668
  },
570
669
  confirm: "none",
571
670
  scope: "assistant"
@@ -577,10 +676,19 @@ var actions_default = {
577
676
  params: {
578
677
  type: "object",
579
678
  properties: {
580
- friend: { type: "string", description: "Matches name, alias or userid prefix. Ambiguous = nothing happens, the candidates are listed." },
581
- alias: { type: "string", minLength: 1 }
679
+ friend: {
680
+ type: "string",
681
+ description: "Matches name, alias or userid prefix. Ambiguous = nothing happens, the candidates are listed."
682
+ },
683
+ alias: {
684
+ type: "string",
685
+ minLength: 1
686
+ }
582
687
  },
583
- required: ["friend", "alias"]
688
+ required: [
689
+ "friend",
690
+ "alias"
691
+ ]
584
692
  },
585
693
  confirm: "none",
586
694
  scope: "assistant"
@@ -591,8 +699,14 @@ var actions_default = {
591
699
  summary: "Stop alerts from one conversation.",
592
700
  params: {
593
701
  type: "object",
594
- properties: { friend: { type: "string" } },
595
- required: ["friend"]
702
+ properties: {
703
+ friend: {
704
+ type: "string"
705
+ }
706
+ },
707
+ required: [
708
+ "friend"
709
+ ]
596
710
  },
597
711
  confirm: "none",
598
712
  scope: "assistant"
@@ -603,8 +717,14 @@ var actions_default = {
603
717
  summary: "Alerts from that conversation again.",
604
718
  params: {
605
719
  type: "object",
606
- properties: { friend: { type: "string" } },
607
- required: ["friend"]
720
+ properties: {
721
+ friend: {
722
+ type: "string"
723
+ }
724
+ },
725
+ required: [
726
+ "friend"
727
+ ]
608
728
  },
609
729
  confirm: "none",
610
730
  scope: "assistant"
@@ -615,8 +735,14 @@ var actions_default = {
615
735
  summary: "Sound on a new message.",
616
736
  params: {
617
737
  type: "object",
618
- properties: { on: { type: "boolean" } },
619
- required: ["on"]
738
+ properties: {
739
+ on: {
740
+ type: "boolean"
741
+ }
742
+ },
743
+ required: [
744
+ "on"
745
+ ]
620
746
  },
621
747
  confirm: "none",
622
748
  scope: "assistant"
@@ -627,8 +753,14 @@ var actions_default = {
627
753
  summary: "Flash the window on a new message.",
628
754
  params: {
629
755
  type: "object",
630
- properties: { on: { type: "boolean" } },
631
- required: ["on"]
756
+ properties: {
757
+ on: {
758
+ type: "boolean"
759
+ }
760
+ },
761
+ required: [
762
+ "on"
763
+ ]
632
764
  },
633
765
  confirm: "none",
634
766
  scope: "assistant"
@@ -636,8 +768,12 @@ var actions_default = {
636
768
  {
637
769
  name: "status",
638
770
  usage: "/status",
639
- summary: "Friends, pending requests, how far back the history on this device goes, app version.",
640
- params: { type: "object", properties: {}, required: [] },
771
+ summary: "Friends and service peers counted separately, pending requests, how far back the history on this device goes, app version.",
772
+ params: {
773
+ type: "object",
774
+ properties: {},
775
+ required: []
776
+ },
641
777
  confirm: "none",
642
778
  scope: "assistant"
643
779
  }
@@ -660,7 +796,12 @@ var actions_default = {
660
796
  usage: "silent [days]",
661
797
  summary: "Nothing from them for this many days (default 90).",
662
798
  guarded: true,
663
- arg: { name: "days", type: "integer", default: 90, minimum: 1 }
799
+ arg: {
800
+ name: "days",
801
+ type: "integer",
802
+ default: 90,
803
+ minimum: 1
804
+ }
664
805
  },
665
806
  {
666
807
  name: "pending",
@@ -678,9 +819,145 @@ var actions_default = {
678
819
  guard: {
679
820
  recentDays: 14,
680
821
  applies: "Filters marked guarded never match a friend accepted (or, with no acceptance stamp, requested) less than recentDays ago; the result says how many were skipped."
822
+ },
823
+ servicePeers: {
824
+ file: "service-peers.json",
825
+ loader: "loadServicePeers()",
826
+ rule: "A peer is a service peer if and only if its userid is in this list. The Assistant never infers it from a name or from having no messages. /friends excludes them entirely; /nodes is only them.",
827
+ later: "A dora / ENS / bridge / directory service returns the same shape; the bundled file is v1."
681
828
  }
682
829
  };
683
830
 
831
+ // src/assistant/service-peers.json
832
+ var service_peers_default = {
833
+ version: 1,
834
+ source: "static",
835
+ updated: "2026-09-24",
836
+ note: "Service providers and network nodes: peers that are infrastructure or agents, not people. The Assistant manages them with /nodes, never /friends. Later this list comes from a service (dora / ENS / bridge / directory) in this same shape; until then it ships inside the app.",
837
+ peers: [
838
+ {
839
+ userid: "7xmbDbouSYEA5mbAgWxV2BTwjc59XGcVPqXSZx6VibUd",
840
+ name: "lens",
841
+ kind: "node",
842
+ role: "bridge + web host"
843
+ },
844
+ {
845
+ userid: "4uNnLnAQHVJ7td657MDaz4UnEuhnTW9sRAPSVeAEEQe4",
846
+ name: "tokyo",
847
+ kind: "node",
848
+ role: "exit + relay + dora + meeting"
849
+ },
850
+ {
851
+ userid: "5Aj6uQMd1cNRb9cGT4AwHCN4RdVZjfaGLtgKGhdP1LzN",
852
+ name: "cn",
853
+ kind: "node",
854
+ role: "exit"
855
+ },
856
+ {
857
+ userid: "DZN2L9RV1YkHjqGHMTA6juZhskKA65AD2RyPt4umZVc7",
858
+ name: "callpass",
859
+ kind: "node",
860
+ role: "server"
861
+ },
862
+ {
863
+ userid: "BcGvk2h5XCxBZWJrYwzKpZ8GbxaZRoakEzt2pQpathyA",
864
+ name: "gojipower",
865
+ kind: "node",
866
+ role: "server"
867
+ },
868
+ {
869
+ userid: "BeMbuKf1poh3U4rjw3eeUAKZsbN2qBSjVZdPSvehmHsw",
870
+ name: "snoopy",
871
+ kind: "node",
872
+ role: "dora + meeting + openclaw host"
873
+ },
874
+ {
875
+ userid: "CzwUZv6LfiGM7EXd9vXr6DutG8ASkA3TnNbqQzjKRJN5",
876
+ name: "GFAX",
877
+ kind: "node",
878
+ role: "server"
879
+ },
880
+ {
881
+ userid: "72a4A7awtn6BqZnBt3eNhZH9MdYXRkM6Ht5HG2sqoiKR",
882
+ name: "nv4090-wsl",
883
+ kind: "node",
884
+ role: "GPU node"
885
+ },
886
+ {
887
+ userid: "6riqxxw18vjwXZEEtFUxbgNBMMe1dcv2kVHqVRQdihjE",
888
+ name: "node-3145",
889
+ kind: "node",
890
+ role: "GPU node (4090w)"
891
+ },
892
+ {
893
+ userid: "6D1PLSVqbSpcxnDMAd8ZXLWycYveeP2hU4g3igDQEqA7",
894
+ name: "node-5123",
895
+ kind: "node",
896
+ role: "node"
897
+ },
898
+ {
899
+ userid: "5EU9Ee3Vai1XwTr4pGaJFt4wmNVPUoYnJsdBM9LwNhrJ",
900
+ name: "node-2998",
901
+ kind: "node",
902
+ role: "node"
903
+ },
904
+ {
905
+ userid: "GujioyKQRucQgw8mwMPNWtYTdNdrooCuFq3R1D4RqjCW",
906
+ name: "beagle-help",
907
+ kind: "agent",
908
+ role: "product support"
909
+ },
910
+ {
911
+ userid: "FhbohSLrj5UjdyFKCEYNeEWAPq3QD9hRg6hsso5ipag2",
912
+ name: "help.beagles.eth",
913
+ kind: "agent",
914
+ role: "bridge agent"
915
+ },
916
+ {
917
+ userid: "9EaFGWuXH2UfREzYMkPPitzjoH6E8tvey4JvqTKtHFJh",
918
+ name: "openclaw",
919
+ kind: "agent",
920
+ role: "OpenClaw agent"
921
+ },
922
+ {
923
+ userid: "6z2o8ojPVjFGRzcffUdd5btvDYAt4tLuwJ8j1jvMBU8p",
924
+ name: "ClawAlan",
925
+ kind: "agent",
926
+ role: "OpenClaw agent on snoopy"
927
+ },
928
+ {
929
+ userid: "G7s3YjVfNLTY5pKBd83qQLAdzDddS7ZstsfrUySwK2Cb",
930
+ name: "interview",
931
+ kind: "agent",
932
+ role: "interview"
933
+ },
934
+ {
935
+ userid: "6jbXjrUWJ4zYvG8BDLnu8KVpoL7rhsdHYSwWER6WTfdt",
936
+ name: "interview editor",
937
+ kind: "agent",
938
+ role: "interview"
939
+ },
940
+ {
941
+ userid: "8BGemh6uqmh4X7KpnPVMeMSU8hZwAc6ZDo4MBRMFrq2L",
942
+ name: "readyplayer",
943
+ kind: "agent",
944
+ role: "readyplayer"
945
+ },
946
+ {
947
+ userid: "Ev1XmWRwtjQf8n33wgTwEhzXnYEyfqTyanehuDE6nczF",
948
+ name: "ReadyPlayer membership manager",
949
+ kind: "agent",
950
+ role: "readyplayer"
951
+ },
952
+ {
953
+ userid: "7xLPH2kjk7Nn8GrsAt7TYwa6b1WMPLkC2XRx7BzhiXYT",
954
+ name: "ai-btc-trader",
955
+ kind: "agent",
956
+ role: "btc signals (245)"
957
+ }
958
+ ]
959
+ };
960
+
684
961
  // src/assistant/filters.js
685
962
  var ASSISTANT_ID = "__assistant__";
686
963
  var DAY_MS = 864e5;
@@ -696,11 +973,11 @@ function parseCommand(line) {
696
973
  if (!parts.length)
697
974
  return null;
698
975
  const head = parts[0].toLowerCase();
699
- if (head === "friends") {
976
+ if (head === "friends" || head === "nodes") {
700
977
  const sub = (parts[1] || "").toLowerCase();
701
978
  if (sub === "list" || sub === "remove")
702
- return { ok: true, name: `friends-${sub}`, args: parts.slice(2) };
703
- return { ok: false, name: "friends", args: parts.slice(1), error: "friends-subcommand" };
979
+ return { ok: true, name: `${head}-${sub}`, args: parts.slice(2) };
980
+ return { ok: false, name: head, args: parts.slice(1), error: `${head}-subcommand` };
704
981
  }
705
982
  return { ok: true, name: head, args: parts.slice(1) };
706
983
  }
@@ -727,6 +1004,30 @@ function parseFilters(args) {
727
1004
  }
728
1005
  return { filters, silentDays, unknown };
729
1006
  }
1007
+ function servicePeerMap(list) {
1008
+ const map = /* @__PURE__ */ new Map();
1009
+ const peers = list && list.peers || [];
1010
+ for (const p of peers) {
1011
+ if (p && typeof p.userid === "string" && p.userid) {
1012
+ map.set(p.userid, { kind: p.kind === "agent" ? "agent" : "node", role: p.role || "", name: p.name || "" });
1013
+ }
1014
+ }
1015
+ return map;
1016
+ }
1017
+ function partitionPeers(friends, svcMap) {
1018
+ const people = [];
1019
+ const nodes = [];
1020
+ for (const f of friends != null ? friends : []) {
1021
+ if (!f)
1022
+ continue;
1023
+ const svc = svcMap && svcMap.get(f.userid);
1024
+ if (svc)
1025
+ nodes.push(Object.assign({}, f, { kind: svc.kind, role: svc.role, service: true }));
1026
+ else
1027
+ people.push(f);
1028
+ }
1029
+ return { people, nodes };
1030
+ }
730
1031
  var acceptedOrRequestedAt = (f) => f.acceptedAt || f.requestedAt || 0;
731
1032
  function isRecent(friend, now, recentDays = RECENT_DAYS) {
732
1033
  const at = acceptedOrRequestedAt(friend);
@@ -797,6 +1098,9 @@ function resolveFriend(friends, token) {
797
1098
  }
798
1099
 
799
1100
  // src/assistant.jsx
1101
+ function loadServicePeers() {
1102
+ return service_peers_default;
1103
+ }
800
1104
  var ASSISTANT_PEER = {
801
1105
  id: ASSISTANT_ID,
802
1106
  userId: ASSISTANT_ID,
@@ -835,11 +1139,14 @@ var fmtAgo = (ts, now, lang) => {
835
1139
  var nameOf = (f) => f.alias || f.name || f.userid;
836
1140
  function sinceLine(historySince, lang) {
837
1141
  if (!historySince)
838
- return lang === "zh" ? "_\u57FA\u4E8E\u672C\u8BBE\u5907\u4E0A\u7684\u6D88\u606F\uFF08\u672C\u673A\u6682\u65E0\u5386\u53F2\u8BB0\u5F55\uFF09_" : "_based on messages on this device (no history here yet)_";
839
- return lang === "zh" ? `_\u57FA\u4E8E\u672C\u8BBE\u5907\u81EA ${fmtDate(historySince, lang)} \u8D77\u7684\u6D88\u606F_` : `_based on messages on this device since ${fmtDate(historySince, lang)}_`;
1142
+ return lang === "zh" ? "*\u57FA\u4E8E\u672C\u8BBE\u5907\u4E0A\u7684\u6D88\u606F\uFF08\u672C\u673A\u6682\u65E0\u5386\u53F2\u8BB0\u5F55\uFF09*" : "*based on messages on this device (no history here yet)*";
1143
+ return lang === "zh" ? `*\u57FA\u4E8E\u672C\u8BBE\u5907\u81EA ${fmtDate(historySince, lang)} \u8D77\u7684\u6D88\u606F*` : `*based on messages on this device since ${fmtDate(historySince, lang)}*`;
840
1144
  }
841
1145
  function friendTable(rows, now, lang) {
842
- return rows.map((f) => lang === "zh" ? `- **${nameOf(f)}** \u2014 \u6700\u540E\u6536\u5230 ${fmtAgo(f.lastInboundTs, now, lang)} \xB7 \u6536 ${f.inboundCount || 0} / \u53D1 ${f.outboundCount || 0}` : `- **${nameOf(f)}** \u2014 last heard ${fmtAgo(f.lastInboundTs, now, lang)} \xB7 in ${f.inboundCount || 0} / out ${f.outboundCount || 0}`).join("\n");
1146
+ return rows.map((f) => {
1147
+ const what = f.service ? ` \`${f.kind}${f.role ? " \xB7 " + f.role : ""}\`` : "";
1148
+ return lang === "zh" ? `- **${nameOf(f)}**${what} \u2014 \u6700\u540E\u6536\u5230 ${fmtAgo(f.lastInboundTs, now, lang)} \xB7 \u6536 ${f.inboundCount || 0} / \u53D1 ${f.outboundCount || 0}` : `- **${nameOf(f)}**${what} \u2014 last heard ${fmtAgo(f.lastInboundTs, now, lang)} \xB7 in ${f.inboundCount || 0} / out ${f.outboundCount || 0}`;
1149
+ }).join("\n");
843
1150
  }
844
1151
  function helpText(lang) {
845
1152
  const intro = lang === "zh" ? "**Beagle \u52A9\u624B** \u2014 \u53EA\u5728\u672C\u673A\u8FD0\u884C\uFF0C\u4E0D\u4F1A\u628A\u4EFB\u4F55\u5185\u5BB9\u53D1\u5230\u7F51\u7EDC\u4E0A\u3002\n\n\u53EF\u7528\u547D\u4EE4\uFF1A" : "**Beagle Assistant** \u2014 runs on this machine only; nothing here is sent over the network.\n\nCommands:";
@@ -908,7 +1215,15 @@ function useAssistant({ lang, T, muted, onToggleMute, sound, flash, setAlert, on
908
1215
  }, [lang]);
909
1216
  const fetchFriends = async () => {
910
1217
  const r = await fetch("/api/assistant/friends").then((x) => x.json());
911
- return { friends: r.friends || [], historySince: r.historySince || null, now: r.now || Date.now(), ver: r.beagleVer || "" };
1218
+ const { people, nodes } = partitionPeers(r.friends || [], servicePeerMap(loadServicePeers()));
1219
+ return {
1220
+ friends: people,
1221
+ nodes,
1222
+ all: r.friends || [],
1223
+ historySince: r.historySince || null,
1224
+ now: r.now || Date.now(),
1225
+ ver: r.beagleVer || ""
1226
+ };
912
1227
  };
913
1228
  const listReply = (rows, skipped, data, title) => {
914
1229
  if (!rows.length) {
@@ -920,9 +1235,9 @@ ${sinceLine(data.historySince, lang)}`;
920
1235
  }
921
1236
  const skip = skipped.length ? lang === "zh" ? `
922
1237
 
923
- _\u8DF3\u8FC7\u4E86 ${skipped.length} \u4F4D\u6700\u8FD1 ${RECENT_DAYS} \u5929\u5185\u6DFB\u52A0\u7684\u597D\u53CB\u3002_` : `
1238
+ *\u8DF3\u8FC7\u4E86 ${skipped.length} \u4F4D\u6700\u8FD1 ${RECENT_DAYS} \u5929\u5185\u6DFB\u52A0\u7684\u597D\u53CB\u3002*` : `
924
1239
 
925
- _${skipped.length} friend${skipped.length > 1 ? "s" : ""} added in the last ${RECENT_DAYS} days ${skipped.length > 1 ? "were" : "was"} skipped._` : "";
1240
+ *${skipped.length} friend${skipped.length > 1 ? "s" : ""} added in the last ${RECENT_DAYS} days ${skipped.length > 1 ? "were" : "was"} skipped.*` : "";
926
1241
  return `${title}
927
1242
 
928
1243
  ${friendTable(rows, data.now, lang)}${skip}
@@ -940,7 +1255,7 @@ ${cands.map((c) => `- ${nameOf(c)}`).join("\n")}`;
940
1255
  return append(asMsg("them", lang === "zh" ? "\u6211\u53EA\u8BA4\u8BC6\u4EE5 `/` \u5F00\u5934\u7684\u547D\u4EE4\u3002\u8F93\u5165 `/help` \u770B\u770B\u80FD\u505A\u4EC0\u4E48\u3002" : "I only understand commands starting with `/`. Type `/help` to see them."));
941
1256
  }
942
1257
  if (cmd.ok === false) {
943
- return append(asMsg("them", lang === "zh" ? "`/friends` \u9700\u8981 `list` \u6216 `remove`\uFF0C\u4F8B\u5982 `/friends list silent 90`\u3002" : "`/friends` needs `list` or `remove`, for example `/friends list silent 90`."));
1258
+ return append(asMsg("them", lang === "zh" ? `\`/${cmd.name}\` \u9700\u8981 \`list\` \u6216 \`remove\`\uFF0C\u4F8B\u5982 \`/${cmd.name} list silent 90\`\u3002` : `\`/${cmd.name}\` needs \`list\` or \`remove\`, for example \`/${cmd.name} list silent 90\`.`));
944
1259
  }
945
1260
  try {
946
1261
  switch (cmd.name) {
@@ -948,37 +1263,48 @@ ${cands.map((c) => `- ${nameOf(c)}`).join("\n")}`;
948
1263
  return append(asMsg("them", helpText(lang)));
949
1264
  case "status": {
950
1265
  const d = await fetchFriends();
951
- const pending = d.friends.filter((f) => f.pending).length;
1266
+ const pending = d.all.filter((f) => f.pending).length;
1267
+ const nodeCount = d.nodes.filter((n) => n.kind === "node").length;
1268
+ const agentCount = d.nodes.length - nodeCount;
952
1269
  return append(asMsg("them", lang === "zh" ? `**\u72B6\u6001**
953
1270
 
954
1271
  - \u597D\u53CB\uFF1A${d.friends.length}
1272
+ - \u670D\u52A1\u8282\u70B9\uFF1A${d.nodes.length}\uFF08\u8282\u70B9 ${nodeCount} \xB7 \u667A\u80FD\u4F53 ${agentCount}\uFF09\uFF0C\u7528 \`/nodes list\` \u67E5\u770B
955
1273
  - \u5F85\u5BF9\u65B9\u63A5\u53D7\uFF1A${pending}
956
1274
  - \u672C\u673A\u5386\u53F2\u8D77\u81EA\uFF1A${fmtDate(d.historySince, lang)}
957
1275
  - \u7248\u672C\uFF1A${d.ver}` : `**Status**
958
1276
 
959
1277
  - Friends: ${d.friends.length}
1278
+ - Service peers: ${d.nodes.length} (${nodeCount} node${nodeCount === 1 ? "" : "s"} \xB7 ${agentCount} agent${agentCount === 1 ? "" : "s"}) \u2014 see \`/nodes list\`
960
1279
  - Waiting to be accepted: ${pending}
961
1280
  - History on this device since: ${fmtDate(d.historySince, lang)}
962
1281
  - Version: ${d.ver}`));
963
1282
  }
1283
+ case "nodes-list":
964
1284
  case "friends-list": {
1285
+ const nodesMode = cmd.name === "nodes-list";
965
1286
  const { filters, silentDays, unknown } = parseFilters(cmd.args);
966
1287
  if (unknown.length) {
967
1288
  return append(asMsg("them", lang === "zh" ? `\u4E0D\u8BA4\u8BC6\u7684\u7B5B\u9009\u6761\u4EF6\uFF1A${unknown.join(", ")}\u3002\u53EF\u7528\uFF1A${actions_default.filters.map((f) => f.name).join(", ")}\u3002` : `I don't know the filter${unknown.length > 1 ? "s" : ""} ${unknown.join(", ")}. Try: ${actions_default.filters.map((f) => f.name).join(", ")}.`));
968
1289
  }
969
1290
  const d = await fetchFriends();
970
- const { matched, skippedRecent } = selectFriends(d.friends, filters, { now: d.now, silentDays });
971
- const title = filters.length ? `**${filters.join(" + ")}**${filters.includes("silent") ? ` (${silentDays}d)` : ""} \u2014 ${matched.length}` : lang === "zh" ? `**\u5168\u90E8\u597D\u53CB** \u2014 ${matched.length}` : `**All friends** \u2014 ${matched.length}`;
1291
+ const pool = nodesMode ? d.nodes : d.friends;
1292
+ const { matched, skippedRecent } = selectFriends(pool, filters, { now: d.now, silentDays });
1293
+ const all = nodesMode ? lang === "zh" ? `**\u5168\u90E8\u670D\u52A1\u8282\u70B9** \u2014 ${matched.length}` : `**All service peers** \u2014 ${matched.length}` : lang === "zh" ? `**\u5168\u90E8\u597D\u53CB** \u2014 ${matched.length}` : `**All friends** \u2014 ${matched.length}`;
1294
+ const title = filters.length ? `**${nodesMode ? (lang === "zh" ? "\u670D\u52A1\u8282\u70B9" : "service peers") + ": " : ""}${filters.join(" + ")}**${filters.includes("silent") ? ` (${silentDays}d)` : ""} \u2014 ${matched.length}` : all;
972
1295
  return append(asMsg("them", listReply(matched, skippedRecent, d, title)));
973
1296
  }
1297
+ case "nodes-remove":
974
1298
  case "friends-remove": {
1299
+ const nodesMode = cmd.name === "nodes-remove";
975
1300
  const { filters, silentDays, unknown } = parseFilters(cmd.args);
976
1301
  if (unknown.length || !filters.length) {
977
1302
  return append(asMsg("them", lang === "zh" ? `\u5220\u9664\u9700\u8981\u7B5B\u9009\u6761\u4EF6\uFF0C\u4F8B\u5982 \`/friends remove never-messaged\`\u3002\u53EF\u7528\uFF1A${actions_default.filters.map((f) => f.name).join(", ")}\u3002` : `Removing needs a filter, for example \`/friends remove never-messaged\`. Available: ${actions_default.filters.map((f) => f.name).join(", ")}.`));
978
1303
  }
979
1304
  const d = await fetchFriends();
980
- const { matched, skippedRecent } = selectFriends(d.friends, filters, { now: d.now, silentDays });
981
- const title = lang === "zh" ? `**\u5C06\u8981\u5220\u9664 ${matched.length} \u4F4D\u597D\u53CB** \u2014 \u8BF7\u5148\u6838\u5BF9\u8FD9\u4EFD\u540D\u5355` : `**${matched.length} friend${matched.length === 1 ? "" : "s"} to remove** \u2014 check the list first`;
1305
+ const { matched, skippedRecent } = selectFriends(nodesMode ? d.nodes : d.friends, filters, { now: d.now, silentDays });
1306
+ const what = nodesMode ? lang === "zh" ? "\u4E2A\u670D\u52A1\u8282\u70B9" : `service peer${matched.length === 1 ? "" : "s"}` : lang === "zh" ? "\u4F4D\u597D\u53CB" : `friend${matched.length === 1 ? "" : "s"}`;
1307
+ const title = lang === "zh" ? `**\u5C06\u8981\u5220\u9664 ${matched.length} ${what}** \u2014 \u8BF7\u5148\u6838\u5BF9\u8FD9\u4EFD\u540D\u5355` : `**${matched.length} ${what} to remove** \u2014 check the list first`;
982
1308
  return append(asMsg(
983
1309
  "them",
984
1310
  listReply(matched, skippedRecent, d, title),
@@ -990,7 +1316,7 @@ ${cands.map((c) => `- ${nameOf(c)}`).join("\n")}`;
990
1316
  if (!q)
991
1317
  return append(asMsg("them", lang === "zh" ? "\u7528\u6CD5\uFF1A`/find <\u6587\u5B57>`" : "Usage: `/find <text>`"));
992
1318
  const d = await fetchFriends();
993
- const rows = findFriends(d.friends, q);
1319
+ const rows = findFriends(d.friends.concat(d.nodes), q);
994
1320
  const title = lang === "zh" ? `**"${q}"** \u2014 ${rows.length} \u4F4D` : `**"${q}"** \u2014 ${rows.length} found`;
995
1321
  return append(asMsg("them", listReply(rows, [], d, title)));
996
1322
  }
@@ -1000,7 +1326,7 @@ ${cands.map((c) => `- ${nameOf(c)}`).join("\n")}`;
1000
1326
  if (!tok || !alias)
1001
1327
  return append(asMsg("them", lang === "zh" ? "\u7528\u6CD5\uFF1A`/rename <\u597D\u53CB> <\u65B0\u540D\u5B57>`" : "Usage: `/rename <friend> <new name>`"));
1002
1328
  const d = await fetchFriends();
1003
- const { match, candidates } = resolveFriend(d.friends, tok);
1329
+ const { match, candidates } = resolveFriend(d.friends.concat(d.nodes), tok);
1004
1330
  if (!match)
1005
1331
  return append(asMsg("them", candidates.length ? ambiguous(candidates) : notFound(tok)));
1006
1332
  const was = nameOf(match);
@@ -1015,7 +1341,7 @@ ${cands.map((c) => `- ${nameOf(c)}`).join("\n")}`;
1015
1341
  if (!tok)
1016
1342
  return append(asMsg("them", `${lang === "zh" ? "\u7528\u6CD5" : "Usage"}: \`/${cmd.name} <friend>\``));
1017
1343
  const d = await fetchFriends();
1018
- const { match, candidates } = resolveFriend(d.friends, tok);
1344
+ const { match, candidates } = resolveFriend(d.friends.concat(d.nodes), tok);
1019
1345
  if (!match)
1020
1346
  return append(asMsg("them", candidates.length ? ambiguous(candidates) : notFound(tok)));
1021
1347
  const isMuted = (muted || []).includes(match.userid);
@@ -2547,7 +2873,11 @@ function Conversation({ T, peer, lang, peers, onOpenChat, thread: threadProp, on
2547
2873
  style: { width: 40, height: 44, flexShrink: 0, border: "none", background: "transparent", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", padding: 0 }
2548
2874
  },
2549
2875
  /* @__PURE__ */ React.createElement(Icon, { name: "chevronLeft", size: 26, stroke: 2.2, color: "var(--accent)" })
2550
- ), /* @__PURE__ */ React.createElement("div", { onClick: () => setPubProfile(true), title: T && T.pubOpen || "View public profile", style: { cursor: "pointer", flexShrink: 0 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer, size: 34, radius: 8 })), /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0, flex: phone ? 1 : void 0 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, minWidth: 0 } }, /* @__PURE__ */ React.createElement("span", { onClick: () => setPubProfile(true), style: { fontFamily: peer.alias ? "var(--ui)" : "var(--mono)", fontSize: 15, fontWeight: 700, color: "var(--text)", cursor: "pointer", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", minWidth: 0 } }, peer.alias || shortKey(peer.userId, phone ? 6 : 10, phone ? 4 : 6)), peer.app && !phone && /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, "APP"), peer.agent && !phone && /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, "agent"), isGroup && !phone && /* @__PURE__ */ React.createElement(Tag, null, T && T.groupTag || "group"), !phone && !peer.app && /* @__PURE__ */ React.createElement(RouteTag, { peer })), phone ? /* @__PURE__ */ React.createElement("div", { style: { marginTop: 2, display: "flex" } }, /* @__PURE__ */ React.createElement(RouteTag, { peer })) : /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 2 } }, /* @__PURE__ */ React.createElement(Mono, { size: 11.5, dim: true, copy: peer.userId, title: peer.userId }, shortKey(peer.userId, 10, 6)), /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)" } }, "\xB7"), /* @__PURE__ */ React.createElement("button", { onClick: () => onOpenNet(peer), title: T && T.openNet || "Network view", style: { background: "none", border: "none", cursor: "pointer", padding: 0, color: "var(--accent)", display: "inline-flex", alignItems: "center" } }, /* @__PURE__ */ React.createElement(Icon, { name: "network", size: 12, stroke: 2 })), /* @__PURE__ */ React.createElement(Mono, { size: 11.5, dim: true, copy: peer.ip, title: peer.ip }, peer.ip), peer.address && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)" } }, "\xB7"), /* @__PURE__ */ React.createElement(
2876
+ ), /* @__PURE__ */ React.createElement("div", { onClick: () => setPubProfile(true), title: T && T.pubOpen || "View public profile", style: { cursor: "pointer", flexShrink: 0 } }, /* @__PURE__ */ React.createElement(DkAvatar, { peer, size: 34, radius: 8 })), /* @__PURE__ */ React.createElement("div", { style: { minWidth: 0, flex: phone ? 1 : void 0 } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, minWidth: 0 } }, /* @__PURE__ */ React.createElement("span", { onClick: () => setPubProfile(true), style: { fontFamily: peer.alias ? "var(--ui)" : "var(--mono)", fontSize: 15, fontWeight: 700, color: "var(--text)", cursor: "pointer", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", minWidth: 0 } }, peer.alias || shortKey(peer.userId, phone ? 6 : 10, phone ? 4 : 6)), peer.app && !phone && /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, "APP"), peer.agent && !phone && /* @__PURE__ */ React.createElement(Tag, { tone: "accent" }, "agent"), isGroup && !phone && /* @__PURE__ */ React.createElement(Tag, null, T && T.groupTag || "group"), !phone && !peer.app && /* @__PURE__ */ React.createElement(RouteTag, { peer })), peer.app ? (
2877
+ // The internal id is not an address anyone can use and not a name
2878
+ // anyone chose; saying what this row IS is more use than showing it.
2879
+ /* @__PURE__ */ React.createElement("div", { style: { marginTop: 2, fontFamily: "var(--mono)", fontSize: 11.5, color: "var(--faint)" } }, T && T.assistantSub || "Local \xB7 APP")
2880
+ ) : phone ? /* @__PURE__ */ React.createElement("div", { style: { marginTop: 2, display: "flex" } }, /* @__PURE__ */ React.createElement(RouteTag, { peer })) : /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 2 } }, /* @__PURE__ */ React.createElement(Mono, { size: 11.5, dim: true, copy: peer.userId, title: peer.userId }, shortKey(peer.userId, 10, 6)), /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)" } }, "\xB7"), /* @__PURE__ */ React.createElement("button", { onClick: () => onOpenNet(peer), title: T && T.openNet || "Network view", style: { background: "none", border: "none", cursor: "pointer", padding: 0, color: "var(--accent)", display: "inline-flex", alignItems: "center" } }, /* @__PURE__ */ React.createElement(Icon, { name: "network", size: 12, stroke: 2 })), /* @__PURE__ */ React.createElement(Mono, { size: 11.5, dim: true, copy: peer.ip, title: peer.ip }, peer.ip), peer.address && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("span", { style: { color: "var(--line)" } }, "\xB7"), /* @__PURE__ */ React.createElement(
2551
2881
  Mono,
2552
2882
  {
2553
2883
  size: 11.5,
@@ -2565,7 +2895,7 @@ ${peer.address}`
2565
2895
  title: muted ? T && T.unmuteSound || "Turn sound back on for this chat" : T && T.muteSound || "Mute sound for this chat",
2566
2896
  onClick: () => onToggleMute(peer.id)
2567
2897
  }
2568
- ), 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(
2898
+ ), onCall && !peer.app && /* @__PURE__ */ React.createElement(Btn, { icon: "phone", title: T && T.audioCall || "Audio call", onClick: () => onCall(peer.userId, false) }), onCall && !peer.app && /* @__PURE__ */ React.createElement(Btn, { icon: "video", title: T && T.videoCall || "Video call", onClick: () => onCall(peer.userId, true) }), onSendRtcFile && !peer.app && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(
2569
2899
  "input",
2570
2900
  {
2571
2901
  ref: rtcFileRef,
@@ -2579,19 +2909,19 @@ ${peer.address}`
2579
2909
  ), !phone && /* @__PURE__ */ React.createElement(Btn, { icon: "file", title: T && T.sendWebrtcFile || "Send file via WebRTC", onClick: () => rtcFileRef.current && rtcFileRef.current.click() })), /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(Btn, { icon: "more", onClick: () => setMenu((v) => !v) }), menu && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { onClick: () => setMenu(false), style: { position: "fixed", inset: 0, zIndex: 40 } }), /* @__PURE__ */ React.createElement("div", { style: { position: "absolute", right: 0, top: 36, zIndex: 50, width: 180, background: "var(--panel-2)", border: "1px solid var(--line)", borderRadius: 9, padding: 6, boxShadow: "0 14px 40px rgba(0,0,0,0.4)" } }, phone && onToggleMute && /* @__PURE__ */ React.createElement(MenuItem, { icon: muted ? "bell" : "bellOff", label: muted ? T && T.unmuteSound || "Turn sound back on" : T && T.muteSound || "Mute sound", onClick: () => {
2580
2910
  setMenu(false);
2581
2911
  onToggleMute(peer.id);
2582
- } }), phone && onSendRtcFile && /* @__PURE__ */ React.createElement(MenuItem, { icon: "file", label: T && T.sendWebrtcFile || "Send file via WebRTC", onClick: () => {
2912
+ } }), phone && onSendRtcFile && !peer.app && /* @__PURE__ */ React.createElement(MenuItem, { icon: "file", label: T && T.sendWebrtcFile || "Send file via WebRTC", onClick: () => {
2583
2913
  setMenu(false);
2584
2914
  rtcFileRef.current && rtcFileRef.current.click();
2585
- } }), phone && /* @__PURE__ */ React.createElement(MenuItem, { icon: "network", label: T && T.openNet || "Network view", onClick: () => {
2915
+ } }), phone && !peer.app && /* @__PURE__ */ React.createElement(MenuItem, { icon: "network", label: T && T.openNet || "Network view", onClick: () => {
2586
2916
  setMenu(false);
2587
2917
  onOpenNet(peer);
2588
- } }), /* @__PURE__ */ React.createElement(MenuItem, { icon: "hash", label: T.alias, onClick: () => {
2918
+ } }), !peer.app && /* @__PURE__ */ React.createElement(MenuItem, { icon: "hash", label: T.alias, onClick: () => {
2589
2919
  setMenu(false);
2590
2920
  onAlias(peer);
2591
2921
  } }), /* @__PURE__ */ React.createElement(MenuItem, { icon: "check", label: T.selectDelete || "Select & delete", onClick: () => {
2592
2922
  setMenu(false);
2593
2923
  setSelMode(true);
2594
- } }), /* @__PURE__ */ React.createElement("div", { style: { height: 1, background: "var(--line)", margin: "5px 4px" } }), /* @__PURE__ */ React.createElement(MenuItem, { icon: "trash", label: T.remove, danger: true, onClick: () => {
2924
+ } }), !peer.app && /* @__PURE__ */ React.createElement("div", { style: { height: 1, background: "var(--line)", margin: "5px 4px" } }), !peer.app && /* @__PURE__ */ React.createElement(MenuItem, { icon: "trash", label: T.remove, danger: true, onClick: () => {
2595
2925
  setMenu(false);
2596
2926
  onRemove(peer);
2597
2927
  } }))))),
@@ -2665,7 +2995,7 @@ ${peer.address}`
2665
2995
  e.target.value = "";
2666
2996
  }
2667
2997
  }
2668
- ), /* @__PURE__ */ React.createElement(Btn, { icon: "paperclip", title: T.sendFile, onClick: () => fileRef.current && fileRef.current.click() }), /* @__PURE__ */ React.createElement(
2998
+ ), !peer.app && /* @__PURE__ */ React.createElement(Btn, { icon: "paperclip", title: T.sendFile, onClick: () => fileRef.current && fileRef.current.click() }), /* @__PURE__ */ React.createElement(
2669
2999
  "input",
2670
3000
  {
2671
3001
  value: draft,
@@ -5558,6 +5888,7 @@ var STR = {
5558
5888
  autoAcceptLabel: "Auto-accept friend requests",
5559
5889
  autoAcceptSub: "Off: requests wait in the sidebar for your approval.",
5560
5890
  groupSenderUnknown: "The group relays only this display name. Register Beagle as a group agent (/agent add <beagle address> in the group) to receive full member identities you can add as friends.",
5891
+ assistantSub: "Local \xB7 APP",
5561
5892
  loadEarlier: "Load earlier messages",
5562
5893
  loadingEarlier: "Loading\u2026",
5563
5894
  pendingFriend: "Waiting for them to accept your friend request \u2014 messages will queue and deliver once they do.",
@@ -5752,6 +6083,7 @@ var STR = {
5752
6083
  autoAcceptLabel: "\u81EA\u52A8\u63A5\u53D7\u597D\u53CB\u8BF7\u6C42",
5753
6084
  autoAcceptSub: "\u5173\u95ED\u540E\uFF0C\u597D\u53CB\u8BF7\u6C42\u4F1A\u7B49\u5F85\u4F60\u5728\u4FA7\u8FB9\u680F\u624B\u52A8\u786E\u8BA4\u3002",
5754
6085
  groupSenderUnknown: "\u7FA4\u670D\u52A1\u53EA\u8F6C\u53D1\u4E86\u663E\u793A\u540D\uFF0C\u62FF\u4E0D\u5230\u8BE5\u6210\u5458\u7684\u5B8C\u6574\u8EAB\u4EFD\u3002\u5728\u7FA4\u91CC\u6267\u884C /agent add <beagle \u5730\u5740> \u628A Beagle \u6CE8\u518C\u4E3A\u7FA4 agent \u540E\uFF0C\u5373\u53EF\u6536\u5230\u5B8C\u6574\u8EAB\u4EFD\u5E76\u4E00\u952E\u52A0\u597D\u53CB\u3002",
6086
+ assistantSub: "\u672C\u673A \xB7 APP",
5755
6087
  loadEarlier: "\u52A0\u8F7D\u66F4\u65E9\u7684\u6D88\u606F",
5756
6088
  loadingEarlier: "\u52A0\u8F7D\u4E2D\u2026",
5757
6089
  pendingFriend: "\u7B49\u5F85\u5BF9\u65B9\u63A5\u53D7\u597D\u53CB\u8BF7\u6C42 \u2014 \u6D88\u606F\u4F1A\u5148\u6392\u961F\uFF0C\u5BF9\u65B9\u63A5\u53D7\u540E\u81EA\u52A8\u9001\u8FBE\u3002",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/beagle-ui",
3
- "version": "0.2.21",
3
+ "version": "0.2.23",
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",