agent-relay-server 0.4.21 → 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,9 +1,25 @@
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;
13
+ const AGENT_TYPE_ICONS = {
14
+ codex: "ti-terminal-2",
15
+ claude: "ti-sparkles",
16
+ agent: "ti-robot",
17
+ };
18
+ const AGENT_TYPE_TITLES = {
19
+ codex: "Codex agent",
20
+ claude: "Claude agent",
21
+ agent: "Agent",
22
+ };
7
23
 
8
24
  function loadPref(key, fallback) {
9
25
  try {
@@ -29,6 +45,7 @@
29
45
 
30
46
  agents: [],
31
47
  agentsById: {},
48
+ pairs: [],
32
49
  messages: [],
33
50
  tasks: [],
34
51
  taskEvents: [],
@@ -36,10 +53,20 @@
36
53
  health: null,
37
54
  now: Date.now(),
38
55
  authToken: loadPref("authToken", ""),
56
+ inboxReadCursors: loadPref("inboxReadCursors", {}),
57
+ inboxArchivedThreads: loadPref("inboxArchivedThreads", {}),
58
+ inboxDrafts: loadPref("inboxDrafts", {}),
59
+ inboxSearch: "",
60
+ inboxShowArchived: loadPref("inboxShowArchived", false),
39
61
 
40
62
  selectedAgent: "",
63
+ agentDetailOpen: false,
64
+ agentDetailId: "",
65
+ selectedInboxThread: "",
41
66
  replyTo: null,
42
67
  composeOpen: false,
68
+ pairInviteOpen: false,
69
+ pairMessageOpen: false,
43
70
  threadOpen: false,
44
71
  threadMessages: [],
45
72
  taskEventsOpen: false,
@@ -47,6 +74,9 @@
47
74
  authNeeded: false,
48
75
 
49
76
  compose: { ...DEFAULT_COMPOSE },
77
+ pairInvite: { ...DEFAULT_PAIR_INVITE },
78
+ pairMessage: { ...DEFAULT_PAIR_MESSAGE },
79
+ inboxCompose: { ...DEFAULT_INBOX_COMPOSE },
50
80
 
51
81
  confirmModal: { show: false, title: "", message: "", action: null },
52
82
  renameModal: { show: false, agentId: "", label: "" },
@@ -55,6 +85,7 @@
55
85
  tagFilter: "",
56
86
  agentStatusFilter: loadPref("agentStatusFilter", ""),
57
87
  agentTagFilter: loadPref("agentTagFilter", ""),
88
+ pairStatusFilter: loadPref("pairStatusFilter", "open"),
58
89
  taskStatusFilter: "",
59
90
  taskSourceFilter: "",
60
91
 
@@ -77,6 +108,8 @@
77
108
  vm.$watch("agentSortDir", (value) => vm.save("agentSortDir", value));
78
109
  vm.$watch("agentStatusFilter", (value) => vm.save("agentStatusFilter", value));
79
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));
80
113
  vm.$watch("view", (value) => {
81
114
  vm.save("view", value);
82
115
  if (value === "analytics") vm.$nextTick(() => vm.renderCharts());
@@ -144,9 +177,11 @@
144
177
  savePref(key, value);
145
178
  },
146
179
 
147
- switchView(view) {
180
+ async switchView(view) {
148
181
  this.view = view;
149
- 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();
150
185
  if (view === "tasks") this.fetchTasks();
151
186
  },
152
187
 
@@ -212,8 +247,8 @@
212
247
 
213
248
  function handleNewMessage(vm, msg) {
214
249
  if (vm.messages.some((existing) => existing.id === msg.id)) return;
215
- if (vm.selectedAgent && msg.from !== vm.selectedAgent && msg.to !== vm.selectedAgent) return;
216
- 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;
217
252
 
218
253
  vm.messages.push(msg);
219
254
  if (vm.messages.length > 200) vm.messages.shift();
@@ -289,7 +324,7 @@
289
324
  },
290
325
 
291
326
  async refresh() {
292
- 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()]);
293
328
  },
294
329
 
295
330
  async refreshLiveData() {
@@ -322,11 +357,29 @@
322
357
  } catch {}
323
358
  },
324
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
+
325
378
  async fetchMessages() {
326
379
  try {
327
380
  let path = "/messages?limit=100";
328
- if (this.selectedAgent) path += "&for=" + encodeURIComponent(this.selectedAgent);
329
- 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);
330
383
  this.messages = await this.api("GET", path);
331
384
  } catch {}
332
385
  },
@@ -339,13 +392,52 @@
339
392
  this.tasks = await this.api("GET", "/tasks?" + params.toString());
340
393
  } catch {}
341
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
+ },
342
402
  };
343
403
  }
344
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
+
345
426
  function createComputedDescriptors() {
346
427
  return {
347
428
  onlineCount: { get: getOnlineCount },
348
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 },
349
441
  filteredMessages: { get: getFilteredMessages },
350
442
  groupedMessages: { get: getGroupedMessages },
351
443
  filteredTasks: { get: getFilteredTasks },
@@ -376,6 +468,210 @@
376
468
  return list.sort((a, b) => compareAgents(this, a, b) * dir);
377
469
  }
378
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
+
379
675
  function getFilteredMessages() {
380
676
  if (!this.tagFilter) return this.messages;
381
677
  return this.messages.filter((msg) => messageMatchesTag(this, msg, this.tagFilter));
@@ -435,6 +731,10 @@
435
731
  }
436
732
 
437
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
+ }
438
738
  switch (vm.agentSort) {
439
739
  case "name":
440
740
  return vm.displayName(a).localeCompare(vm.displayName(b));
@@ -453,6 +753,19 @@
453
753
  return {
454
754
  displayName,
455
755
  displayTarget,
756
+ conversationTitle,
757
+ messagePreview,
758
+ agentPair,
759
+ pairPeerId,
760
+ pairBadgeClass,
761
+ pairStatusClass,
762
+ pairBadgeLabel,
763
+ pairTitle,
764
+ agentAttention,
765
+ agentAttentionTitle,
766
+ agentType,
767
+ agentTypeIcon,
768
+ agentTypeTitle,
456
769
  severityClass,
457
770
  agentStatusTitle,
458
771
  timeAgo,
@@ -477,6 +790,129 @@
477
790
  return agent ? this.displayName(agent) : target.slice(-8);
478
791
  }
479
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
+
890
+ function agentType(agent) {
891
+ const values = [
892
+ ...(agent?.tags || []),
893
+ agent?.meta?.provider,
894
+ agent?.meta?.client,
895
+ agent?.meta?.runtime,
896
+ agent?.meta?.agentType,
897
+ agent?.id,
898
+ agent?.name,
899
+ ]
900
+ .filter((value) => typeof value === "string")
901
+ .map((value) => value.toLowerCase());
902
+
903
+ if (values.some((value) => value.includes("codex"))) return "codex";
904
+ if (values.some((value) => value.includes("claude"))) return "claude";
905
+ return "agent";
906
+ }
907
+
908
+ function agentTypeIcon(agent) {
909
+ return AGENT_TYPE_ICONS[agentType(agent)] || AGENT_TYPE_ICONS.agent;
910
+ }
911
+
912
+ function agentTypeTitle(agent) {
913
+ return AGENT_TYPE_TITLES[agentType(agent)] || AGENT_TYPE_TITLES.agent;
914
+ }
915
+
480
916
  function severityClass(severity) {
481
917
  if (severity === "critical") return "bg-danger-lt";
482
918
  if (severity === "warning") return "bg-warning-lt";
@@ -522,6 +958,20 @@
522
958
  return {
523
959
  openCompose,
524
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,
525
975
  startReply,
526
976
  cancelReply,
527
977
  doSend,
@@ -532,6 +982,20 @@
532
982
  };
533
983
  }
534
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
+
535
999
  function focusComposeBody(vm) {
536
1000
  vm.$nextTick(() => vm.$refs.composeBody?.focus());
537
1001
  }
@@ -549,12 +1013,195 @@
549
1013
  focusComposeBody(this);
550
1014
  }
551
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
+
552
1198
  function startReply(msg) {
1199
+ const replyTarget = msg.from === HUMAN_AGENT_ID ? msg.to : msg.from;
553
1200
  this.replyTo = { id: msg.id, from: msg.from };
554
1201
  this.compose = {
555
1202
  ...DEFAULT_COMPOSE,
556
- from: "",
557
- to: msg.from,
1203
+ from: this.view === "inbox" ? HUMAN_AGENT_ID : "",
1204
+ to: replyTarget,
558
1205
  channel: msg.channel || "",
559
1206
  };
560
1207
  this.openCompose();
@@ -588,6 +1235,7 @@
588
1235
  this.composeOpen = false;
589
1236
  this.replyTo = null;
590
1237
  this.compose = { ...DEFAULT_COMPOSE };
1238
+ await this.fetchMessages();
591
1239
  } catch (e) {
592
1240
  alert("Send failed: " + e.message);
593
1241
  }
@@ -637,8 +1285,119 @@
637
1285
  }
638
1286
  }
639
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
+
640
1389
  function createAgentActions() {
641
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
+
642
1401
  openRename(agent) {
643
1402
  this.renameModal = { show: true, agentId: agent.id, label: agent.label || "" };
644
1403
  this.$nextTick(() => this.$refs.renameInput?.focus());
@@ -787,6 +1546,7 @@
787
1546
  ...createApiMethods(),
788
1547
  ...createDisplayMethods(),
789
1548
  ...createMessageActions(),
1549
+ ...createPairActions(),
790
1550
  ...createAgentActions(),
791
1551
  ...createChartMethods(),
792
1552
  };
@@ -796,7 +1556,7 @@
796
1556
 
797
1557
  window.AgentRelayDashboard = {
798
1558
  createRelayDashboard,
799
- helpers: { loadPref, savePref, indexAgents, upsertById, upsertTask, compareAgents },
1559
+ helpers: { loadPref, savePref, indexAgents, upsertById, upsertTask, compareAgents, agentType },
800
1560
  };
801
1561
  window.relay = createRelayDashboard;
802
1562