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.
- package/package.json +1 -1
- package/public/dashboard.js +770 -10
- package/public/index.html +711 -5
- package/src/db.ts +154 -1
- package/src/routes.ts +105 -0
- package/src/security.ts +2 -1
- package/src/types.ts +23 -0
package/public/dashboard.js
CHANGED
|
@@ -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:
|
|
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
|
|