agent-relay-server 0.4.22 → 0.4.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.
@@ -1,7 +1,13 @@
1
1
  (() => {
2
2
  const PREF_PREFIX = "ar-";
3
+ const HUMAN_AGENT_ID = "user";
4
+ const INBOX_OPERATOR_ID = HUMAN_AGENT_ID;
3
5
  const DEFAULT_COMPOSE = { from: "", to: "", body: "", channel: "", subject: "", claimable: false };
6
+ const DEFAULT_INBOX_COMPOSE = { toMode: "agent", to: "", body: "", channel: "", subject: "", claimable: false };
7
+ const DEFAULT_PAIR_MESSAGE = { pairId: "", from: "", body: "", subject: "" };
8
+ const DEFAULT_PAIR_INVITE = { requesterId: "", targetId: "", objective: "" };
4
9
  const CLOSED_TASK_STATUSES = new Set(["done", "failed", "canceled"]);
10
+ const WAITING_TASK_STATUSES = new Set(["open", "blocked"]);
5
11
  const STATUS_SORT_ORDER = { online: 0, idle: 1, busy: 2, offline: 3 };
6
12
  const LIVE_REFRESH_MS = 5_000;
7
13
  const AGENT_TYPE_ICONS = {
@@ -39,6 +45,7 @@
39
45
 
40
46
  agents: [],
41
47
  agentsById: {},
48
+ pairs: [],
42
49
  messages: [],
43
50
  tasks: [],
44
51
  taskEvents: [],
@@ -46,10 +53,20 @@
46
53
  health: null,
47
54
  now: Date.now(),
48
55
  authToken: loadPref("authToken", ""),
56
+ inboxReadCursors: loadPref("inboxReadCursors", {}),
57
+ inboxArchivedThreads: loadPref("inboxArchivedThreads", {}),
58
+ inboxDrafts: loadPref("inboxDrafts", {}),
59
+ inboxSearch: "",
60
+ inboxShowArchived: loadPref("inboxShowArchived", false),
49
61
 
50
62
  selectedAgent: "",
63
+ agentDetailOpen: false,
64
+ agentDetailId: "",
65
+ selectedInboxThread: "",
51
66
  replyTo: null,
52
67
  composeOpen: false,
68
+ pairInviteOpen: false,
69
+ pairMessageOpen: false,
53
70
  threadOpen: false,
54
71
  threadMessages: [],
55
72
  taskEventsOpen: false,
@@ -57,6 +74,9 @@
57
74
  authNeeded: false,
58
75
 
59
76
  compose: { ...DEFAULT_COMPOSE },
77
+ pairInvite: { ...DEFAULT_PAIR_INVITE },
78
+ pairMessage: { ...DEFAULT_PAIR_MESSAGE },
79
+ inboxCompose: { ...DEFAULT_INBOX_COMPOSE },
60
80
 
61
81
  confirmModal: { show: false, title: "", message: "", action: null },
62
82
  renameModal: { show: false, agentId: "", label: "" },
@@ -65,6 +85,7 @@
65
85
  tagFilter: "",
66
86
  agentStatusFilter: loadPref("agentStatusFilter", ""),
67
87
  agentTagFilter: loadPref("agentTagFilter", ""),
88
+ pairStatusFilter: loadPref("pairStatusFilter", "open"),
68
89
  taskStatusFilter: "",
69
90
  taskSourceFilter: "",
70
91
 
@@ -87,6 +108,8 @@
87
108
  vm.$watch("agentSortDir", (value) => vm.save("agentSortDir", value));
88
109
  vm.$watch("agentStatusFilter", (value) => vm.save("agentStatusFilter", value));
89
110
  vm.$watch("agentTagFilter", (value) => vm.save("agentTagFilter", value));
111
+ vm.$watch("pairStatusFilter", (value) => vm.save("pairStatusFilter", value));
112
+ vm.$watch("inboxShowArchived", (value) => vm.save("inboxShowArchived", value));
90
113
  vm.$watch("view", (value) => {
91
114
  vm.save("view", value);
92
115
  if (value === "analytics") vm.$nextTick(() => vm.renderCharts());
@@ -154,9 +177,11 @@
154
177
  savePref(key, value);
155
178
  },
156
179
 
157
- switchView(view) {
180
+ async switchView(view) {
158
181
  this.view = view;
159
- if (view === "messages") this.fetchMessages();
182
+ if (view === "inbox" || view === "messages") await this.fetchMessages();
183
+ if (view === "inbox") this.markInboxThreadRead(this.selectedInboxThreadData);
184
+ if (view === "pairs") this.fetchPairs();
160
185
  if (view === "tasks") this.fetchTasks();
161
186
  },
162
187
 
@@ -222,8 +247,8 @@
222
247
 
223
248
  function handleNewMessage(vm, msg) {
224
249
  if (vm.messages.some((existing) => existing.id === msg.id)) return;
225
- if (vm.selectedAgent && msg.from !== vm.selectedAgent && msg.to !== vm.selectedAgent) return;
226
- if (vm.channelFilter && msg.channel !== vm.channelFilter) return;
250
+ if (vm.view === "messages" && vm.selectedAgent && msg.from !== vm.selectedAgent && msg.to !== vm.selectedAgent) return;
251
+ if (vm.view === "messages" && vm.channelFilter && msg.channel !== vm.channelFilter) return;
227
252
 
228
253
  vm.messages.push(msg);
229
254
  if (vm.messages.length > 200) vm.messages.shift();
@@ -299,7 +324,7 @@
299
324
  },
300
325
 
301
326
  async refresh() {
302
- await Promise.all([this.fetchStats(), this.fetchHealth(), this.fetchAgents(), this.fetchMessages(), this.fetchTasks()]);
327
+ await Promise.all([this.fetchStats(), this.fetchHealth(), this.fetchAgents(), this.fetchPairs(), this.fetchMessages(), this.fetchTasks(), this.fetchInboxState()]);
303
328
  },
304
329
 
305
330
  async refreshLiveData() {
@@ -332,11 +357,29 @@
332
357
  } catch {}
333
358
  },
334
359
 
360
+ async fetchPairs() {
361
+ try {
362
+ let pairs;
363
+ if (this.pairStatusFilter === "open") {
364
+ const [active, pending] = await Promise.all([
365
+ this.api("GET", "/pairs?status=active"),
366
+ this.api("GET", "/pairs?status=pending"),
367
+ ]);
368
+ pairs = [...active, ...pending];
369
+ } else if (this.pairStatusFilter) {
370
+ pairs = await this.api("GET", "/pairs?status=" + encodeURIComponent(this.pairStatusFilter));
371
+ } else {
372
+ pairs = await this.api("GET", "/pairs");
373
+ }
374
+ this.pairs = pairs.sort(comparePairs);
375
+ } catch {}
376
+ },
377
+
335
378
  async fetchMessages() {
336
379
  try {
337
380
  let path = "/messages?limit=100";
338
- if (this.selectedAgent) path += "&for=" + encodeURIComponent(this.selectedAgent);
339
- if (this.channelFilter) path += "&channel=" + encodeURIComponent(this.channelFilter);
381
+ if (this.view === "messages" && this.selectedAgent) path += "&for=" + encodeURIComponent(this.selectedAgent);
382
+ if (this.view === "messages" && this.channelFilter) path += "&channel=" + encodeURIComponent(this.channelFilter);
340
383
  this.messages = await this.api("GET", path);
341
384
  } catch {}
342
385
  },
@@ -349,13 +392,52 @@
349
392
  this.tasks = await this.api("GET", "/tasks?" + params.toString());
350
393
  } catch {}
351
394
  },
395
+
396
+ async fetchInboxState() {
397
+ try {
398
+ const state = await this.api("GET", "/inbox/state?operatorId=" + encodeURIComponent(INBOX_OPERATOR_ID));
399
+ applyInboxState(this, state);
400
+ } catch {}
401
+ },
352
402
  };
353
403
  }
354
404
 
405
+ function applyInboxState(vm, state) {
406
+ const readCursors = {};
407
+ const archivedThreads = {};
408
+ const drafts = {};
409
+
410
+ for (const thread of state?.threads || []) {
411
+ if (thread.readCursorMessageId) readCursors[thread.peerId] = thread.readCursorMessageId;
412
+ if (thread.archivedAtMessageId) archivedThreads[thread.peerId] = thread.archivedAtMessageId;
413
+ }
414
+ for (const draft of state?.drafts || []) {
415
+ if (draft.body) drafts[draft.peerId] = draft.body;
416
+ }
417
+
418
+ vm.inboxReadCursors = readCursors;
419
+ vm.inboxArchivedThreads = archivedThreads;
420
+ vm.inboxDrafts = drafts;
421
+ savePref("inboxReadCursors", readCursors);
422
+ savePref("inboxArchivedThreads", archivedThreads);
423
+ savePref("inboxDrafts", drafts);
424
+ }
425
+
355
426
  function createComputedDescriptors() {
356
427
  return {
357
428
  onlineCount: { get: getOnlineCount },
358
429
  sortedAgents: { get: getSortedAgents },
430
+ pairsByAgentId: { get: getPairsByAgentId },
431
+ selectedAgentDetail: { get: getSelectedAgentDetail },
432
+ agentDetailMessages: { get: getAgentDetailMessages },
433
+ pairMessagePair: { get: getPairMessagePair },
434
+ allInboxThreads: { get: getAllInboxThreads },
435
+ inboxThreads: { get: getInboxThreads },
436
+ selectedInboxThreadData: { get: getSelectedInboxThreadData },
437
+ selectedInboxMessages: { get: getSelectedInboxMessages },
438
+ inboxComposeTargetOptions: { get: getInboxComposeTargetOptions },
439
+ attentionSummary: { get: getAttentionSummary },
440
+ attentionAgentCount: { get: getAttentionAgentCount },
359
441
  filteredMessages: { get: getFilteredMessages },
360
442
  groupedMessages: { get: getGroupedMessages },
361
443
  filteredTasks: { get: getFilteredTasks },
@@ -386,6 +468,210 @@
386
468
  return list.sort((a, b) => compareAgents(this, a, b) * dir);
387
469
  }
388
470
 
471
+ function getPairsByAgentId() {
472
+ const byAgent = {};
473
+ for (const pair of this.pairs || []) {
474
+ if (pair.requesterId) byAgent[pair.requesterId] = pair;
475
+ if (pair.targetId) byAgent[pair.targetId] = pair;
476
+ }
477
+ return byAgent;
478
+ }
479
+
480
+ function comparePairs(a, b) {
481
+ return new Date(b.updatedAt || b.createdAt || 0) - new Date(a.updatedAt || a.createdAt || 0);
482
+ }
483
+
484
+ function getSelectedAgentDetail() {
485
+ if (!this.agentDetailId) return null;
486
+ return this.agentsById[this.agentDetailId] || null;
487
+ }
488
+
489
+ function getAgentDetailMessages() {
490
+ if (!this.agentDetailId) return [];
491
+ return this.messages
492
+ .filter((msg) => msg.from === this.agentDetailId || msg.to === this.agentDetailId)
493
+ .slice()
494
+ .sort((a, b) => b.id - a.id)
495
+ .slice(0, 8);
496
+ }
497
+
498
+ function getPairMessagePair() {
499
+ return this.pairs.find((pair) => pair.id === this.pairMessage.pairId) || null;
500
+ }
501
+
502
+ function getAllInboxThreads() {
503
+ return buildInboxThreads(this);
504
+ }
505
+
506
+ function getInboxThreads() {
507
+ const search = this.inboxSearch.trim().toLowerCase();
508
+ return this.allInboxThreads.filter((thread) => {
509
+ if (!this.inboxShowArchived && thread.archived) return false;
510
+ if (search && !threadMatchesSearch(this, thread, search)) return false;
511
+ return true;
512
+ });
513
+ }
514
+
515
+ function buildInboxThreads(vm) {
516
+ const threads = new Map();
517
+ for (const msg of vm.messages) {
518
+ const peer = inboxPeer(msg);
519
+ if (!peer) continue;
520
+ if (!threads.has(peer)) threads.set(peer, { id: peer, peer, messages: [], lastMessage: null });
521
+ threads.get(peer).messages.push(msg);
522
+ }
523
+
524
+ for (const thread of threads.values()) {
525
+ thread.messages.sort((a, b) => a.id - b.id);
526
+ thread.lastMessage = thread.messages[thread.messages.length - 1] || null;
527
+ thread.attention = getThreadAttention(vm, thread);
528
+ thread.archived = isInboxThreadArchived(vm, thread);
529
+ thread.draft = draftForPeer(vm, thread.peer);
530
+ }
531
+
532
+ return [...threads.values()].sort(compareInboxThreads);
533
+ }
534
+
535
+ function threadMatchesSearch(vm, thread, search) {
536
+ const haystack = [
537
+ vm.displayTarget(thread.peer),
538
+ thread.peer,
539
+ ...thread.messages.flatMap((msg) => [msg.subject || "", msg.body || "", msg.channel || "", vm.displayTarget(msg.from), vm.displayTarget(msg.to)]),
540
+ ].join("\n").toLowerCase();
541
+ return haystack.includes(search);
542
+ }
543
+
544
+ function isInboxThreadArchived(vm, thread) {
545
+ const archivedAtId = Number(vm.inboxArchivedThreads?.[thread.peer] || 0);
546
+ return Boolean(thread.lastMessage?.id && archivedAtId >= thread.lastMessage.id);
547
+ }
548
+
549
+ function compareInboxThreads(a, b) {
550
+ const scoreDelta = (b.attention?.score || 0) - (a.attention?.score || 0);
551
+ if (scoreDelta !== 0) return scoreDelta;
552
+ return (b.lastMessage?.id || 0) - (a.lastMessage?.id || 0);
553
+ }
554
+
555
+ function getSelectedInboxThreadData() {
556
+ if (!this.selectedInboxThread) return null;
557
+ return this.inboxThreads.find((thread) => thread.id === this.selectedInboxThread) || null;
558
+ }
559
+
560
+ function getSelectedInboxMessages() {
561
+ return this.selectedInboxThreadData?.messages || [];
562
+ }
563
+
564
+ function getInboxComposeTargetOptions() {
565
+ if (this.inboxCompose.toMode === "tag") return this.uniqueTags.map((value) => ({ value, label: "#" + value }));
566
+ if (this.inboxCompose.toMode === "cap") return this.uniqueCaps.map((value) => ({ value, label: value }));
567
+ return this.composeAgents.map((agent) => ({ value: agent.id, label: `${this.displayName(agent)} [${agent.id.slice(-6)}]` }));
568
+ }
569
+
570
+ function getAttentionSummary() {
571
+ const threads = this.allInboxThreads.filter((thread) => !thread.archived);
572
+ const pendingPairInvites = this.pairs.filter((pair) => pair.status === "pending").length;
573
+ const claimableTasks = countClaimableWaiting(this);
574
+ const unreadInbox = threads.reduce((sum, thread) => sum + (thread.attention?.unread || 0), 0);
575
+ const needsHumanResponse = threads.filter((thread) => thread.attention?.needsHumanResponse).length;
576
+ const agentQuestions = threads.filter((thread) => thread.attention?.agentQuestion).length;
577
+
578
+ return {
579
+ unreadInbox,
580
+ needsHumanResponse,
581
+ agentQuestions,
582
+ pendingPairInvites,
583
+ claimableTasks,
584
+ total: unreadInbox + needsHumanResponse + agentQuestions + pendingPairInvites + claimableTasks,
585
+ };
586
+ }
587
+
588
+ function getAttentionAgentCount() {
589
+ return this.sortedAgents.filter((agent) => agentAttention.call(this, agent).total > 0).length;
590
+ }
591
+
592
+ function inboxPeer(msg) {
593
+ if (msg.from === HUMAN_AGENT_ID && msg.to) return msg.to;
594
+ if (msg.to === HUMAN_AGENT_ID && msg.from) return msg.from;
595
+ return "";
596
+ }
597
+
598
+ function getThreadAttention(vm, thread) {
599
+ const lastHumanReplyId = maxMessageId(thread.messages, (msg) => msg.from === HUMAN_AGENT_ID);
600
+ const lastInboundId = maxMessageId(thread.messages, isHumanInboundMessage);
601
+ const unread = thread.messages.filter((msg) => isUnreadHumanMessage(vm, thread.peer, msg)).length;
602
+ const needsHumanResponse = lastInboundId > lastHumanReplyId;
603
+ const agentQuestion = thread.messages.some((msg) =>
604
+ isHumanInboundMessage(msg) && msg.id > lastHumanReplyId && messageLooksLikeQuestion(msg)
605
+ );
606
+
607
+ return {
608
+ unread,
609
+ needsHumanResponse,
610
+ agentQuestion,
611
+ score: unread * 10 + (needsHumanResponse ? 5 : 0) + (agentQuestion ? 3 : 0),
612
+ };
613
+ }
614
+
615
+ function maxMessageId(messages, predicate) {
616
+ let max = 0;
617
+ for (const msg of messages) {
618
+ if (predicate(msg) && msg.id > max) max = msg.id;
619
+ }
620
+ return max;
621
+ }
622
+
623
+ function isHumanInboundMessage(msg) {
624
+ return msg.to === HUMAN_AGENT_ID && msg.from !== HUMAN_AGENT_ID;
625
+ }
626
+
627
+ function isUnreadHumanMessage(vm, peer, msg) {
628
+ if (!isHumanInboundMessage(msg)) return false;
629
+ if ((msg.readBy || []).includes(HUMAN_AGENT_ID)) return false;
630
+ return msg.id > readCursorForPeer(vm, peer);
631
+ }
632
+
633
+ function readCursorForPeer(vm, peer) {
634
+ const value = Number(vm.inboxReadCursors?.[peer] || 0);
635
+ return Number.isFinite(value) ? value : 0;
636
+ }
637
+
638
+ function draftForPeer(vm, peer) {
639
+ return typeof vm.inboxDrafts?.[peer] === "string" ? vm.inboxDrafts[peer] : "";
640
+ }
641
+
642
+ function messageLooksLikeQuestion(msg) {
643
+ return /\?/.test(`${msg.subject || ""}\n${msg.body || ""}`);
644
+ }
645
+
646
+ function countClaimableWaiting(vm) {
647
+ const taskCount = vm.tasks.filter(isClaimableTaskWaiting).length;
648
+ const messageCount = vm.messages.filter(isClaimableMessageWaiting).length;
649
+ return taskCount + messageCount;
650
+ }
651
+
652
+ function countAgentClaimableWaiting(vm, agent) {
653
+ const taskCount = vm.tasks.filter((task) => isClaimableTaskWaiting(task) && targetMatchesAgent(task.target, agent)).length;
654
+ const messageCount = vm.messages.filter((msg) => isClaimableMessageWaiting(msg) && targetMatchesAgent(msg.to, agent)).length;
655
+ return taskCount + messageCount;
656
+ }
657
+
658
+ function isClaimableTaskWaiting(task) {
659
+ return WAITING_TASK_STATUSES.has(task.status) && !task.claimedBy;
660
+ }
661
+
662
+ function isClaimableMessageWaiting(msg) {
663
+ return Boolean(msg.claimable && !msg.claimedBy);
664
+ }
665
+
666
+ function targetMatchesAgent(target, agent) {
667
+ if (!target || !agent) return false;
668
+ if (target === "broadcast" || target === agent.id) return true;
669
+ if (target.startsWith("tag:")) return (agent.tags || []).includes(target.slice(4));
670
+ if (target.startsWith("cap:")) return (agent.capabilities || []).includes(target.slice(4));
671
+ if (target.startsWith("label:")) return agent.label === target.slice(6);
672
+ return false;
673
+ }
674
+
389
675
  function getFilteredMessages() {
390
676
  if (!this.tagFilter) return this.messages;
391
677
  return this.messages.filter((msg) => messageMatchesTag(this, msg, this.tagFilter));
@@ -445,6 +731,10 @@
445
731
  }
446
732
 
447
733
  function compareAgents(vm, a, b) {
734
+ if (vm.agentSort === "status") {
735
+ const attentionDelta = agentAttention.call(vm, b).score - agentAttention.call(vm, a).score;
736
+ if (attentionDelta !== 0) return attentionDelta;
737
+ }
448
738
  switch (vm.agentSort) {
449
739
  case "name":
450
740
  return vm.displayName(a).localeCompare(vm.displayName(b));
@@ -463,6 +753,16 @@
463
753
  return {
464
754
  displayName,
465
755
  displayTarget,
756
+ conversationTitle,
757
+ messagePreview,
758
+ agentPair,
759
+ pairPeerId,
760
+ pairBadgeClass,
761
+ pairStatusClass,
762
+ pairBadgeLabel,
763
+ pairTitle,
764
+ agentAttention,
765
+ agentAttentionTitle,
466
766
  agentType,
467
767
  agentTypeIcon,
468
768
  agentTypeTitle,
@@ -490,6 +790,103 @@
490
790
  return agent ? this.displayName(agent) : target.slice(-8);
491
791
  }
492
792
 
793
+ function conversationTitle(thread) {
794
+ if (!thread) return "Inbox";
795
+ return this.displayTarget(thread.peer);
796
+ }
797
+
798
+ function messagePreview(msg) {
799
+ const text = msg?.subject || msg?.body || "";
800
+ return text.length > 90 ? text.slice(0, 90) + "..." : text;
801
+ }
802
+
803
+ function agentPair(agent) {
804
+ return agent ? this.pairsByAgentId[agent.id] : null;
805
+ }
806
+
807
+ function pairPeerId(pair, agentId) {
808
+ if (!pair) return "";
809
+ return pair.requesterId === agentId ? pair.targetId : pair.requesterId;
810
+ }
811
+
812
+ function pairBadgeClass(pair) {
813
+ if (pair?.status === "active") return "bg-success-lt";
814
+ if (pair?.status === "pending") return "bg-warning-lt";
815
+ return "bg-secondary-lt";
816
+ }
817
+
818
+ function pairStatusClass(pair) {
819
+ if (pair?.status === "active") return "bg-success";
820
+ if (pair?.status === "pending") return "bg-warning";
821
+ if (pair?.status === "rejected" || pair?.status === "expired") return "bg-danger";
822
+ return "bg-secondary";
823
+ }
824
+
825
+ function pairBadgeLabel(pair, agentId) {
826
+ if (!pair) return "";
827
+ const peer = this.displayTarget(pairPeerId(pair, agentId));
828
+ if (pair.status === "active") return "paired with " + peer;
829
+ if (pair.status === "pending" && pair.requesterId === agentId) return "invite to " + peer;
830
+ if (pair.status === "pending") return "invite from " + peer;
831
+ return pair.status;
832
+ }
833
+
834
+ function pairTitle(pair, agentId) {
835
+ if (!pair) return "";
836
+ const label = pairBadgeLabel.call(this, pair, agentId);
837
+ const objective = pair.objective ? " - " + pair.objective : "";
838
+ return `${label} (${pair.id})${objective}`;
839
+ }
840
+
841
+ function agentAttention(agent) {
842
+ if (!agent) return emptyAttention();
843
+ const thread = this.allInboxThreads.find((item) => item.peer === agent.id && !item.archived);
844
+ const pair = this.agentPair(agent);
845
+ const pendingPairInvite = pair?.status === "pending";
846
+ const claimableTasks = countAgentClaimableWaiting(this, agent);
847
+ const attention = {
848
+ unread: thread?.attention?.unread || 0,
849
+ needsHumanResponse: Boolean(thread?.attention?.needsHumanResponse),
850
+ agentQuestion: Boolean(thread?.attention?.agentQuestion),
851
+ pendingPairInvite,
852
+ claimableTasks,
853
+ };
854
+ attention.total = attention.unread +
855
+ (attention.needsHumanResponse ? 1 : 0) +
856
+ (attention.agentQuestion ? 1 : 0) +
857
+ (attention.pendingPairInvite ? 1 : 0) +
858
+ attention.claimableTasks;
859
+ attention.score = attention.unread * 10 +
860
+ (attention.needsHumanResponse ? 5 : 0) +
861
+ (attention.agentQuestion ? 3 : 0) +
862
+ (attention.pendingPairInvite ? 4 : 0) +
863
+ attention.claimableTasks * 2;
864
+ return attention;
865
+ }
866
+
867
+ function emptyAttention() {
868
+ return {
869
+ unread: 0,
870
+ needsHumanResponse: false,
871
+ agentQuestion: false,
872
+ pendingPairInvite: false,
873
+ claimableTasks: 0,
874
+ total: 0,
875
+ score: 0,
876
+ };
877
+ }
878
+
879
+ function agentAttentionTitle(agent) {
880
+ const attention = agentAttention.call(this, agent);
881
+ const parts = [];
882
+ if (attention.unread) parts.push(`${attention.unread} unread`);
883
+ if (attention.needsHumanResponse) parts.push("needs human response");
884
+ if (attention.agentQuestion) parts.push("agent asked a question");
885
+ if (attention.pendingPairInvite) parts.push("pair invite pending");
886
+ if (attention.claimableTasks) parts.push(`${attention.claimableTasks} claimable waiting`);
887
+ return parts.join(", ");
888
+ }
889
+
493
890
  function agentType(agent) {
494
891
  const values = [
495
892
  ...(agent?.tags || []),
@@ -561,6 +958,20 @@
561
958
  return {
562
959
  openCompose,
563
960
  openComposeToAgent,
961
+ openComposeToInboxThread,
962
+ openInboxThread,
963
+ markInboxThreadRead,
964
+ markInboxThreadUnread,
965
+ archiveInboxThread,
966
+ unarchiveInboxThread,
967
+ confirmDeleteInboxThread,
968
+ doDeleteInboxThread,
969
+ replyDraftForThread,
970
+ setReplyDraft,
971
+ clearReplyDraft,
972
+ sendInboxReply,
973
+ resetInboxComposeTarget,
974
+ doSendInboxCompose,
564
975
  startReply,
565
976
  cancelReply,
566
977
  doSend,
@@ -571,6 +982,20 @@
571
982
  };
572
983
  }
573
984
 
985
+ function createPairActions() {
986
+ return {
987
+ openPairMessage,
988
+ closePairMessage,
989
+ openPairInvite,
990
+ closePairInvite,
991
+ doCreatePair,
992
+ doSendPairMessage,
993
+ doAcceptPair,
994
+ doRejectPair,
995
+ doHangupPair,
996
+ };
997
+ }
998
+
574
999
  function focusComposeBody(vm) {
575
1000
  vm.$nextTick(() => vm.$refs.composeBody?.focus());
576
1001
  }
@@ -588,12 +1013,195 @@
588
1013
  focusComposeBody(this);
589
1014
  }
590
1015
 
1016
+ function openComposeToInboxThread(thread) {
1017
+ if (!thread) return;
1018
+ this.replyTo = thread.lastMessage ? { id: thread.lastMessage.id, from: thread.lastMessage.from } : null;
1019
+ this.compose = { ...DEFAULT_COMPOSE, from: HUMAN_AGENT_ID, to: thread.peer, channel: thread.lastMessage?.channel || "" };
1020
+ this.composeOpen = true;
1021
+ focusComposeBody(this);
1022
+ }
1023
+
1024
+ function openInboxThread(thread) {
1025
+ this.selectedInboxThread = thread?.id || "";
1026
+ this.markInboxThreadRead(thread);
1027
+ }
1028
+
1029
+ function markInboxThreadRead(thread) {
1030
+ if (!thread?.peer || !thread.messages?.length) return;
1031
+ const lastInboundId = maxMessageId(thread.messages, isHumanInboundMessage);
1032
+ if (lastInboundId <= readCursorForPeer(this, thread.peer)) return;
1033
+ this.inboxReadCursors = { ...this.inboxReadCursors, [thread.peer]: lastInboundId };
1034
+ savePref("inboxReadCursors", this.inboxReadCursors);
1035
+ void saveInboxThreadState(this, {
1036
+ peerId: thread.peer,
1037
+ readCursorMessageId: lastInboundId,
1038
+ });
1039
+ }
1040
+
1041
+ function markInboxThreadUnread(thread) {
1042
+ if (!thread?.peer) return;
1043
+ const next = { ...this.inboxReadCursors };
1044
+ delete next[thread.peer];
1045
+ this.inboxReadCursors = next;
1046
+ savePref("inboxReadCursors", this.inboxReadCursors);
1047
+ void saveInboxThreadState(this, {
1048
+ peerId: thread.peer,
1049
+ readCursorMessageId: null,
1050
+ });
1051
+ }
1052
+
1053
+ function archiveInboxThread(thread) {
1054
+ if (!thread?.peer || !thread.lastMessage) return;
1055
+ this.inboxArchivedThreads = { ...this.inboxArchivedThreads, [thread.peer]: thread.lastMessage.id };
1056
+ savePref("inboxArchivedThreads", this.inboxArchivedThreads);
1057
+ void saveInboxThreadState(this, {
1058
+ peerId: thread.peer,
1059
+ archivedAtMessageId: thread.lastMessage.id,
1060
+ });
1061
+ if (this.selectedInboxThread === thread.id) this.selectedInboxThread = "";
1062
+ }
1063
+
1064
+ function unarchiveInboxThread(thread) {
1065
+ if (!thread?.peer) return;
1066
+ const next = { ...this.inboxArchivedThreads };
1067
+ delete next[thread.peer];
1068
+ this.inboxArchivedThreads = next;
1069
+ savePref("inboxArchivedThreads", this.inboxArchivedThreads);
1070
+ void saveInboxThreadState(this, {
1071
+ peerId: thread.peer,
1072
+ archivedAtMessageId: null,
1073
+ });
1074
+ }
1075
+
1076
+ async function saveInboxThreadState(vm, patch) {
1077
+ try {
1078
+ await vm.api("PATCH", "/inbox/threads", { operatorId: INBOX_OPERATOR_ID, ...patch });
1079
+ } catch {}
1080
+ }
1081
+
1082
+ function confirmDeleteInboxThread(thread) {
1083
+ if (!thread) return;
1084
+ this.openConfirm(
1085
+ "Delete Thread",
1086
+ `Delete ${thread.messages.length} message(s) in ${this.conversationTitle(thread)}? This cannot be undone.`,
1087
+ () => this.doDeleteInboxThread(thread)
1088
+ );
1089
+ }
1090
+
1091
+ async function doDeleteInboxThread(thread) {
1092
+ if (!thread?.messages?.length) return;
1093
+ try {
1094
+ await Promise.all(thread.messages.map((msg) => this.api("DELETE", "/messages/" + msg.id)));
1095
+ this.messages = this.messages.filter((msg) => !thread.messages.some((item) => item.id === msg.id));
1096
+ this.selectedInboxThread = "";
1097
+ this.clearReplyDraft(thread);
1098
+ } catch (e) {
1099
+ alert("Delete thread failed: " + e.message);
1100
+ }
1101
+ }
1102
+
1103
+ function replyDraftForThread(thread) {
1104
+ return thread?.peer ? draftForPeer(this, thread.peer) : "";
1105
+ }
1106
+
1107
+ function setReplyDraft(thread, value) {
1108
+ if (!thread?.peer) return;
1109
+ const next = { ...this.inboxDrafts, [thread.peer]: value };
1110
+ if (!value) delete next[thread.peer];
1111
+ this.inboxDrafts = next;
1112
+ savePref("inboxDrafts", this.inboxDrafts);
1113
+ if (value) {
1114
+ void saveInboxDraft(this, thread.peer, value);
1115
+ } else {
1116
+ void deleteInboxDraftState(this, thread.peer);
1117
+ }
1118
+ }
1119
+
1120
+ function clearReplyDraft(thread) {
1121
+ if (!thread?.peer) return;
1122
+ this.setReplyDraft(thread, "");
1123
+ }
1124
+
1125
+ async function saveInboxDraft(vm, peerId, body) {
1126
+ try {
1127
+ await vm.api("PUT", "/inbox/drafts", { operatorId: INBOX_OPERATOR_ID, peerId, body });
1128
+ } catch {}
1129
+ }
1130
+
1131
+ async function deleteInboxDraftState(vm, peerId) {
1132
+ try {
1133
+ await vm.api("DELETE", "/inbox/drafts?operatorId=" + encodeURIComponent(INBOX_OPERATOR_ID) + "&peerId=" + encodeURIComponent(peerId));
1134
+ } catch {}
1135
+ }
1136
+
1137
+ async function sendInboxReply(thread) {
1138
+ if (!thread) return;
1139
+ const body = this.replyDraftForThread(thread).trim();
1140
+ if (!body) {
1141
+ alert("Reply body is required.");
1142
+ return;
1143
+ }
1144
+
1145
+ try {
1146
+ const payload = {
1147
+ from: HUMAN_AGENT_ID,
1148
+ to: thread.peer,
1149
+ body,
1150
+ };
1151
+ if (thread.lastMessage?.channel) payload.channel = thread.lastMessage.channel;
1152
+ if (thread.lastMessage?.id) payload.replyTo = thread.lastMessage.id;
1153
+ await this.api("POST", "/messages", payload);
1154
+ this.clearReplyDraft(thread);
1155
+ this.markInboxThreadRead(thread);
1156
+ await this.fetchMessages();
1157
+ } catch (e) {
1158
+ alert("Reply failed: " + e.message);
1159
+ }
1160
+ }
1161
+
1162
+ function resetInboxComposeTarget() {
1163
+ this.inboxCompose = { ...this.inboxCompose, to: "" };
1164
+ }
1165
+
1166
+ function inboxComposeTarget(vm) {
1167
+ const target = vm.inboxCompose.to;
1168
+ if (!target) return "";
1169
+ if (vm.inboxCompose.toMode === "tag") return "tag:" + target;
1170
+ if (vm.inboxCompose.toMode === "cap") return "cap:" + target;
1171
+ return target;
1172
+ }
1173
+
1174
+ async function doSendInboxCompose() {
1175
+ const target = inboxComposeTarget(this);
1176
+ if (!target || !this.inboxCompose.body) {
1177
+ alert("Target and Message are required.");
1178
+ return;
1179
+ }
1180
+
1181
+ try {
1182
+ const payload = {
1183
+ from: HUMAN_AGENT_ID,
1184
+ to: target,
1185
+ body: this.inboxCompose.body,
1186
+ };
1187
+ if (this.inboxCompose.channel) payload.channel = this.inboxCompose.channel;
1188
+ if (this.inboxCompose.subject) payload.subject = this.inboxCompose.subject;
1189
+ if (this.inboxCompose.claimable) payload.claimable = true;
1190
+ await this.api("POST", "/messages", payload);
1191
+ this.inboxCompose = { ...DEFAULT_INBOX_COMPOSE, toMode: this.inboxCompose.toMode, to: this.inboxCompose.to };
1192
+ await this.fetchMessages();
1193
+ } catch (e) {
1194
+ alert("Send failed: " + e.message);
1195
+ }
1196
+ }
1197
+
591
1198
  function startReply(msg) {
1199
+ const replyTarget = msg.from === HUMAN_AGENT_ID ? msg.to : msg.from;
592
1200
  this.replyTo = { id: msg.id, from: msg.from };
593
1201
  this.compose = {
594
1202
  ...DEFAULT_COMPOSE,
595
- from: "",
596
- to: msg.from,
1203
+ from: this.view === "inbox" ? HUMAN_AGENT_ID : "",
1204
+ to: replyTarget,
597
1205
  channel: msg.channel || "",
598
1206
  };
599
1207
  this.openCompose();
@@ -627,6 +1235,7 @@
627
1235
  this.composeOpen = false;
628
1236
  this.replyTo = null;
629
1237
  this.compose = { ...DEFAULT_COMPOSE };
1238
+ await this.fetchMessages();
630
1239
  } catch (e) {
631
1240
  alert("Send failed: " + e.message);
632
1241
  }
@@ -676,8 +1285,119 @@
676
1285
  }
677
1286
  }
678
1287
 
1288
+ function openPairMessage(pair, fromId) {
1289
+ if (!pair) return;
1290
+ this.pairMessage = {
1291
+ ...DEFAULT_PAIR_MESSAGE,
1292
+ pairId: pair.id,
1293
+ from: fromId || pair.requesterId || pair.targetId || "",
1294
+ };
1295
+ this.pairMessageOpen = true;
1296
+ this.$nextTick(() => this.$refs?.pairMessageBody?.focus());
1297
+ }
1298
+
1299
+ function openPairInvite(requesterId) {
1300
+ this.pairInvite = {
1301
+ ...DEFAULT_PAIR_INVITE,
1302
+ requesterId: requesterId || this.selectedAgent || "",
1303
+ };
1304
+ this.pairInviteOpen = true;
1305
+ this.$nextTick(() => this.$refs?.pairInviteObjective?.focus());
1306
+ }
1307
+
1308
+ function closePairInvite() {
1309
+ this.pairInviteOpen = false;
1310
+ this.pairInvite = { ...DEFAULT_PAIR_INVITE };
1311
+ }
1312
+
1313
+ async function doCreatePair() {
1314
+ if (!this.pairInvite.requesterId || !this.pairInvite.targetId) {
1315
+ alert("Requester and Target are required.");
1316
+ return;
1317
+ }
1318
+ if (this.pairInvite.requesterId === this.pairInvite.targetId) {
1319
+ alert("Requester and Target must be different agents.");
1320
+ return;
1321
+ }
1322
+
1323
+ try {
1324
+ const payload = {
1325
+ requesterId: this.pairInvite.requesterId,
1326
+ targetId: this.pairInvite.targetId,
1327
+ };
1328
+ if (this.pairInvite.objective) payload.objective = this.pairInvite.objective;
1329
+ await this.api("POST", "/pairs", payload);
1330
+ this.closePairInvite();
1331
+ await Promise.all([this.fetchPairs(), this.fetchMessages()]);
1332
+ } catch (e) {
1333
+ alert("Pair invite failed: " + e.message);
1334
+ }
1335
+ }
1336
+
1337
+ function closePairMessage() {
1338
+ this.pairMessageOpen = false;
1339
+ this.pairMessage = { ...DEFAULT_PAIR_MESSAGE };
1340
+ }
1341
+
1342
+ async function doSendPairMessage() {
1343
+ if (!this.pairMessage.pairId || !this.pairMessage.from || !this.pairMessage.body) {
1344
+ alert("Pair, From, and Message are required.");
1345
+ return;
1346
+ }
1347
+
1348
+ try {
1349
+ const payload = { from: this.pairMessage.from, body: this.pairMessage.body };
1350
+ if (this.pairMessage.subject) payload.subject = this.pairMessage.subject;
1351
+ await this.api("POST", "/pairs/" + encodeURIComponent(this.pairMessage.pairId) + "/messages", payload);
1352
+ this.closePairMessage();
1353
+ await Promise.all([this.fetchPairs(), this.fetchMessages()]);
1354
+ } catch (e) {
1355
+ alert("Pair message failed: " + e.message);
1356
+ }
1357
+ }
1358
+
1359
+ async function doAcceptPair(pair) {
1360
+ if (!pair) return;
1361
+ try {
1362
+ await this.api("POST", "/pairs/" + encodeURIComponent(pair.id) + "/accept", { agentId: pair.targetId });
1363
+ await Promise.all([this.fetchPairs(), this.fetchMessages()]);
1364
+ } catch (e) {
1365
+ alert("Accept failed: " + e.message);
1366
+ }
1367
+ }
1368
+
1369
+ async function doRejectPair(pair) {
1370
+ if (!pair) return;
1371
+ try {
1372
+ await this.api("POST", "/pairs/" + encodeURIComponent(pair.id) + "/reject", { agentId: pair.targetId });
1373
+ await Promise.all([this.fetchPairs(), this.fetchMessages()]);
1374
+ } catch (e) {
1375
+ alert("Reject failed: " + e.message);
1376
+ }
1377
+ }
1378
+
1379
+ async function doHangupPair(pair, agentId) {
1380
+ if (!pair) return;
1381
+ try {
1382
+ await this.api("POST", "/pairs/" + encodeURIComponent(pair.id) + "/hangup", { agentId: agentId || pair.requesterId });
1383
+ await Promise.all([this.fetchPairs(), this.fetchMessages()]);
1384
+ } catch (e) {
1385
+ alert("Hang up failed: " + e.message);
1386
+ }
1387
+ }
1388
+
679
1389
  function createAgentActions() {
680
1390
  return {
1391
+ openAgentDetail(agent) {
1392
+ if (!agent) return;
1393
+ this.agentDetailId = agent.id;
1394
+ this.agentDetailOpen = true;
1395
+ },
1396
+
1397
+ closeAgentDetail() {
1398
+ this.agentDetailOpen = false;
1399
+ },
1400
+
681
1401
  openRename(agent) {
682
1402
  this.renameModal = { show: true, agentId: agent.id, label: agent.label || "" };
683
1403
  this.$nextTick(() => this.$refs.renameInput?.focus());
@@ -826,6 +1546,7 @@
826
1546
  ...createApiMethods(),
827
1547
  ...createDisplayMethods(),
828
1548
  ...createMessageActions(),
1549
+ ...createPairActions(),
829
1550
  ...createAgentActions(),
830
1551
  ...createChartMethods(),
831
1552
  };