@decentnetwork/beagle-ui 0.2.22 → 0.2.24

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 +436 -40
  2. package/package.json +1 -1
package/dist/beagle-ui.js CHANGED
@@ -1,4 +1,4 @@
1
- export const BEAGLE_UI_VERSION = "0.2.22";
1
+ export const BEAGLE_UI_VERSION = "0.2.24";
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,151 @@ 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: "5SxEBHAx49Hknq1VzrA1HDtZHjW31vetX3iMEi4P6pK3",
882
+ name: "gfax118",
883
+ kind: "node",
884
+ role: "server"
885
+ },
886
+ {
887
+ userid: "72a4A7awtn6BqZnBt3eNhZH9MdYXRkM6Ht5HG2sqoiKR",
888
+ name: "nv4090-wsl",
889
+ kind: "node",
890
+ role: "GPU node"
891
+ },
892
+ {
893
+ userid: "6riqxxw18vjwXZEEtFUxbgNBMMe1dcv2kVHqVRQdihjE",
894
+ name: "node-3145",
895
+ kind: "node",
896
+ role: "GPU node (4090w)"
897
+ },
898
+ {
899
+ userid: "6D1PLSVqbSpcxnDMAd8ZXLWycYveeP2hU4g3igDQEqA7",
900
+ name: "node-5123",
901
+ kind: "node",
902
+ role: "node"
903
+ },
904
+ {
905
+ userid: "5EU9Ee3Vai1XwTr4pGaJFt4wmNVPUoYnJsdBM9LwNhrJ",
906
+ name: "node-2998",
907
+ kind: "node",
908
+ role: "node"
909
+ },
910
+ {
911
+ userid: "GujioyKQRucQgw8mwMPNWtYTdNdrooCuFq3R1D4RqjCW",
912
+ name: "beagle-help",
913
+ kind: "agent",
914
+ role: "product support"
915
+ },
916
+ {
917
+ userid: "FhbohSLrj5UjdyFKCEYNeEWAPq3QD9hRg6hsso5ipag2",
918
+ name: "help.beagles.eth",
919
+ kind: "agent",
920
+ role: "bridge agent"
921
+ },
922
+ {
923
+ userid: "9EaFGWuXH2UfREzYMkPPitzjoH6E8tvey4JvqTKtHFJh",
924
+ name: "openclaw",
925
+ kind: "agent",
926
+ role: "OpenClaw agent"
927
+ },
928
+ {
929
+ userid: "6z2o8ojPVjFGRzcffUdd5btvDYAt4tLuwJ8j1jvMBU8p",
930
+ name: "ClawAlan",
931
+ kind: "agent",
932
+ role: "OpenClaw agent on snoopy"
933
+ },
934
+ {
935
+ userid: "G7s3YjVfNLTY5pKBd83qQLAdzDddS7ZstsfrUySwK2Cb",
936
+ name: "interview",
937
+ kind: "agent",
938
+ role: "interview"
939
+ },
940
+ {
941
+ userid: "6jbXjrUWJ4zYvG8BDLnu8KVpoL7rhsdHYSwWER6WTfdt",
942
+ name: "interview editor",
943
+ kind: "agent",
944
+ role: "interview"
945
+ },
946
+ {
947
+ userid: "8BGemh6uqmh4X7KpnPVMeMSU8hZwAc6ZDo4MBRMFrq2L",
948
+ name: "readyplayer",
949
+ kind: "agent",
950
+ role: "readyplayer"
951
+ },
952
+ {
953
+ userid: "Ev1XmWRwtjQf8n33wgTwEhzXnYEyfqTyanehuDE6nczF",
954
+ name: "ReadyPlayer membership manager",
955
+ kind: "agent",
956
+ role: "readyplayer"
957
+ },
958
+ {
959
+ userid: "7xLPH2kjk7Nn8GrsAt7TYwa6b1WMPLkC2XRx7BzhiXYT",
960
+ name: "ai-btc-trader",
961
+ kind: "agent",
962
+ role: "btc signals (245)"
963
+ }
964
+ ]
965
+ };
966
+
684
967
  // src/assistant/filters.js
685
968
  var ASSISTANT_ID = "__assistant__";
686
969
  var DAY_MS = 864e5;
@@ -696,11 +979,11 @@ function parseCommand(line) {
696
979
  if (!parts.length)
697
980
  return null;
698
981
  const head = parts[0].toLowerCase();
699
- if (head === "friends") {
982
+ if (head === "friends" || head === "nodes") {
700
983
  const sub = (parts[1] || "").toLowerCase();
701
984
  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" };
985
+ return { ok: true, name: `${head}-${sub}`, args: parts.slice(2) };
986
+ return { ok: false, name: head, args: parts.slice(1), error: `${head}-subcommand` };
704
987
  }
705
988
  return { ok: true, name: head, args: parts.slice(1) };
706
989
  }
@@ -727,6 +1010,30 @@ function parseFilters(args) {
727
1010
  }
728
1011
  return { filters, silentDays, unknown };
729
1012
  }
1013
+ function servicePeerMap(list) {
1014
+ const map = /* @__PURE__ */ new Map();
1015
+ const peers = list && list.peers || [];
1016
+ for (const p of peers) {
1017
+ if (p && typeof p.userid === "string" && p.userid) {
1018
+ map.set(p.userid, { kind: p.kind === "agent" ? "agent" : "node", role: p.role || "", name: p.name || "" });
1019
+ }
1020
+ }
1021
+ return map;
1022
+ }
1023
+ function partitionPeers(friends, svcMap) {
1024
+ const people = [];
1025
+ const nodes = [];
1026
+ for (const f of friends != null ? friends : []) {
1027
+ if (!f)
1028
+ continue;
1029
+ const svc = svcMap && svcMap.get(f.userid);
1030
+ if (svc)
1031
+ nodes.push(Object.assign({}, f, { kind: svc.kind, role: svc.role, service: true }));
1032
+ else
1033
+ people.push(f);
1034
+ }
1035
+ return { people, nodes };
1036
+ }
730
1037
  var acceptedOrRequestedAt = (f) => f.acceptedAt || f.requestedAt || 0;
731
1038
  function isRecent(friend, now, recentDays = RECENT_DAYS) {
732
1039
  const at = acceptedOrRequestedAt(friend);
@@ -797,6 +1104,9 @@ function resolveFriend(friends, token) {
797
1104
  }
798
1105
 
799
1106
  // src/assistant.jsx
1107
+ function loadServicePeers() {
1108
+ return service_peers_default;
1109
+ }
800
1110
  var ASSISTANT_PEER = {
801
1111
  id: ASSISTANT_ID,
802
1112
  userId: ASSISTANT_ID,
@@ -839,7 +1149,10 @@ function sinceLine(historySince, lang) {
839
1149
  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
1150
  }
841
1151
  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");
1152
+ return rows.map((f) => {
1153
+ const what = f.service ? ` \`${f.kind}${f.role ? " \xB7 " + f.role : ""}\`` : "";
1154
+ 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}`;
1155
+ }).join("\n");
843
1156
  }
844
1157
  function helpText(lang) {
845
1158
  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 +1221,15 @@ function useAssistant({ lang, T, muted, onToggleMute, sound, flash, setAlert, on
908
1221
  }, [lang]);
909
1222
  const fetchFriends = async () => {
910
1223
  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 || "" };
1224
+ const { people, nodes } = partitionPeers(r.friends || [], servicePeerMap(loadServicePeers()));
1225
+ return {
1226
+ friends: people,
1227
+ nodes,
1228
+ all: r.friends || [],
1229
+ historySince: r.historySince || null,
1230
+ now: r.now || Date.now(),
1231
+ ver: r.beagleVer || ""
1232
+ };
912
1233
  };
913
1234
  const listReply = (rows, skipped, data, title) => {
914
1235
  if (!rows.length) {
@@ -940,7 +1261,7 @@ ${cands.map((c) => `- ${nameOf(c)}`).join("\n")}`;
940
1261
  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
1262
  }
942
1263
  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`."));
1264
+ 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
1265
  }
945
1266
  try {
946
1267
  switch (cmd.name) {
@@ -948,37 +1269,48 @@ ${cands.map((c) => `- ${nameOf(c)}`).join("\n")}`;
948
1269
  return append(asMsg("them", helpText(lang)));
949
1270
  case "status": {
950
1271
  const d = await fetchFriends();
951
- const pending = d.friends.filter((f) => f.pending).length;
1272
+ const pending = d.all.filter((f) => f.pending).length;
1273
+ const nodeCount = d.nodes.filter((n) => n.kind === "node").length;
1274
+ const agentCount = d.nodes.length - nodeCount;
952
1275
  return append(asMsg("them", lang === "zh" ? `**\u72B6\u6001**
953
1276
 
954
1277
  - \u597D\u53CB\uFF1A${d.friends.length}
1278
+ - \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
1279
  - \u5F85\u5BF9\u65B9\u63A5\u53D7\uFF1A${pending}
956
1280
  - \u672C\u673A\u5386\u53F2\u8D77\u81EA\uFF1A${fmtDate(d.historySince, lang)}
957
1281
  - \u7248\u672C\uFF1A${d.ver}` : `**Status**
958
1282
 
959
1283
  - Friends: ${d.friends.length}
1284
+ - Service peers: ${d.nodes.length} (${nodeCount} node${nodeCount === 1 ? "" : "s"} \xB7 ${agentCount} agent${agentCount === 1 ? "" : "s"}) \u2014 see \`/nodes list\`
960
1285
  - Waiting to be accepted: ${pending}
961
1286
  - History on this device since: ${fmtDate(d.historySince, lang)}
962
1287
  - Version: ${d.ver}`));
963
1288
  }
1289
+ case "nodes-list":
964
1290
  case "friends-list": {
1291
+ const nodesMode = cmd.name === "nodes-list";
965
1292
  const { filters, silentDays, unknown } = parseFilters(cmd.args);
966
1293
  if (unknown.length) {
967
1294
  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
1295
  }
969
1296
  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}`;
1297
+ const pool = nodesMode ? d.nodes : d.friends;
1298
+ const { matched, skippedRecent } = selectFriends(pool, filters, { now: d.now, silentDays });
1299
+ 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}`;
1300
+ 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
1301
  return append(asMsg("them", listReply(matched, skippedRecent, d, title)));
973
1302
  }
1303
+ case "nodes-remove":
974
1304
  case "friends-remove": {
1305
+ const nodesMode = cmd.name === "nodes-remove";
975
1306
  const { filters, silentDays, unknown } = parseFilters(cmd.args);
976
1307
  if (unknown.length || !filters.length) {
977
1308
  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
1309
  }
979
1310
  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`;
1311
+ const { matched, skippedRecent } = selectFriends(nodesMode ? d.nodes : d.friends, filters, { now: d.now, silentDays });
1312
+ 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"}`;
1313
+ 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
1314
  return append(asMsg(
983
1315
  "them",
984
1316
  listReply(matched, skippedRecent, d, title),
@@ -990,7 +1322,7 @@ ${cands.map((c) => `- ${nameOf(c)}`).join("\n")}`;
990
1322
  if (!q)
991
1323
  return append(asMsg("them", lang === "zh" ? "\u7528\u6CD5\uFF1A`/find <\u6587\u5B57>`" : "Usage: `/find <text>`"));
992
1324
  const d = await fetchFriends();
993
- const rows = findFriends(d.friends, q);
1325
+ const rows = findFriends(d.friends.concat(d.nodes), q);
994
1326
  const title = lang === "zh" ? `**"${q}"** \u2014 ${rows.length} \u4F4D` : `**"${q}"** \u2014 ${rows.length} found`;
995
1327
  return append(asMsg("them", listReply(rows, [], d, title)));
996
1328
  }
@@ -1000,7 +1332,7 @@ ${cands.map((c) => `- ${nameOf(c)}`).join("\n")}`;
1000
1332
  if (!tok || !alias)
1001
1333
  return append(asMsg("them", lang === "zh" ? "\u7528\u6CD5\uFF1A`/rename <\u597D\u53CB> <\u65B0\u540D\u5B57>`" : "Usage: `/rename <friend> <new name>`"));
1002
1334
  const d = await fetchFriends();
1003
- const { match, candidates } = resolveFriend(d.friends, tok);
1335
+ const { match, candidates } = resolveFriend(d.friends.concat(d.nodes), tok);
1004
1336
  if (!match)
1005
1337
  return append(asMsg("them", candidates.length ? ambiguous(candidates) : notFound(tok)));
1006
1338
  const was = nameOf(match);
@@ -1015,7 +1347,7 @@ ${cands.map((c) => `- ${nameOf(c)}`).join("\n")}`;
1015
1347
  if (!tok)
1016
1348
  return append(asMsg("them", `${lang === "zh" ? "\u7528\u6CD5" : "Usage"}: \`/${cmd.name} <friend>\``));
1017
1349
  const d = await fetchFriends();
1018
- const { match, candidates } = resolveFriend(d.friends, tok);
1350
+ const { match, candidates } = resolveFriend(d.friends.concat(d.nodes), tok);
1019
1351
  if (!match)
1020
1352
  return append(asMsg("them", candidates.length ? ambiguous(candidates) : notFound(tok)));
1021
1353
  const isMuted = (muted || []).includes(match.userid);
@@ -1065,6 +1397,8 @@ ${cands.map((c) => `- ${nameOf(c)}`).join("\n")}`;
1065
1397
  }
1066
1398
 
1067
1399
  // src/chat.jsx
1400
+ var DK_CMD_HIST_KEY = "beagle.assistant.history";
1401
+ var DK_CMD_HIST_MAX = 50;
1068
1402
  function RequestsBlock({ T, requests, onAct }) {
1069
1403
  const [acted, setActed] = React.useState({});
1070
1404
  const [failed, setFailed] = React.useState({});
@@ -2466,6 +2800,59 @@ function Conversation({ T, peer, lang, peers, onOpenChat, thread: threadProp, on
2466
2800
  setSelMode(false);
2467
2801
  };
2468
2802
  const [draft, setDraft] = React.useState("");
2803
+ const [hist, setHist] = React.useState([]);
2804
+ const histIdx = React.useRef(null);
2805
+ const histStash = React.useRef("");
2806
+ React.useEffect(() => {
2807
+ histIdx.current = null;
2808
+ histStash.current = "";
2809
+ if (!peer.app) {
2810
+ setHist([]);
2811
+ return;
2812
+ }
2813
+ try {
2814
+ setHist(JSON.parse(localStorage.getItem(DK_CMD_HIST_KEY) || "[]"));
2815
+ } catch (e) {
2816
+ setHist([]);
2817
+ }
2818
+ }, [peer.id, peer.app]);
2819
+ const rememberCommand = (text) => {
2820
+ setHist((h) => {
2821
+ const next = (h[h.length - 1] === text ? h : h.concat(text)).slice(-DK_CMD_HIST_MAX);
2822
+ if (peer.app) {
2823
+ try {
2824
+ localStorage.setItem(DK_CMD_HIST_KEY, JSON.stringify(next));
2825
+ } catch (e) {
2826
+ }
2827
+ }
2828
+ return next;
2829
+ });
2830
+ histIdx.current = null;
2831
+ histStash.current = "";
2832
+ };
2833
+ const walkHistory = (dir) => {
2834
+ if (!hist.length)
2835
+ return false;
2836
+ let i = histIdx.current;
2837
+ if (i === null) {
2838
+ if (dir > 0)
2839
+ return false;
2840
+ histStash.current = draft;
2841
+ i = hist.length - 1;
2842
+ } else {
2843
+ i += dir;
2844
+ if (i >= hist.length) {
2845
+ histIdx.current = null;
2846
+ setDraft(histStash.current);
2847
+ return true;
2848
+ }
2849
+ if (i < 0)
2850
+ i = 0;
2851
+ }
2852
+ histIdx.current = i;
2853
+ setDraft(hist[i]);
2854
+ return true;
2855
+ };
2469
2856
  const [dragOver, setDragOver] = React.useState(false);
2470
2857
  const [sending, setSending] = React.useState(null);
2471
2858
  const [theater, setTheater] = React.useState(null);
@@ -2488,6 +2875,7 @@ function Conversation({ T, peer, lang, peers, onOpenChat, thread: threadProp, on
2488
2875
  const sendDraft = () => {
2489
2876
  if (draft.trim()) {
2490
2877
  followScrollRef.current = true;
2878
+ rememberCommand(draft.trim());
2491
2879
  onSend(draft);
2492
2880
  setDraft("");
2493
2881
  requestAnimationFrame(scrollToBottom);
@@ -2673,11 +3061,19 @@ ${peer.address}`
2673
3061
  "input",
2674
3062
  {
2675
3063
  value: draft,
2676
- onChange: (e) => setDraft(e.target.value),
3064
+ onChange: (e) => {
3065
+ histIdx.current = null;
3066
+ setDraft(e.target.value);
3067
+ },
2677
3068
  onKeyDown: (e) => {
2678
3069
  if (e.key === "Enter" && !e.shiftKey) {
2679
3070
  e.preventDefault();
2680
3071
  sendDraft();
3072
+ return;
3073
+ }
3074
+ if (e.key === "ArrowUp" || e.key === "ArrowDown") {
3075
+ if (walkHistory(e.key === "ArrowUp" ? -1 : 1))
3076
+ e.preventDefault();
2681
3077
  }
2682
3078
  },
2683
3079
  placeholder: phone ? `${T.message}\u2026` : `${T.message} ${peer.alias || shortKey(peer.userId, 6, 4)}\u2026`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/beagle-ui",
3
- "version": "0.2.22",
3
+ "version": "0.2.24",
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",