agent-relay-server 0.4.23 → 0.4.25
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 +935 -12
- package/public/index.html +353 -32
- package/src/db.ts +98 -0
- package/src/routes.ts +231 -3
- package/src/security.ts +1 -0
- package/src/types.ts +38 -0
package/public/dashboard.js
CHANGED
|
@@ -39,9 +39,11 @@
|
|
|
39
39
|
view: loadPref("view", "overview"),
|
|
40
40
|
|
|
41
41
|
showOffline: loadPref("showOffline", false),
|
|
42
|
+
showBuiltIns: loadPref("showBuiltIns", false),
|
|
42
43
|
autoRefresh: loadPref("autoRefresh", true),
|
|
43
44
|
agentSort: loadPref("agentSort", "status"),
|
|
44
45
|
agentSortDir: loadPref("agentSortDir", "asc"),
|
|
46
|
+
agentPresetFilter: loadPref("agentPresetFilter", ""),
|
|
45
47
|
|
|
46
48
|
agents: [],
|
|
47
49
|
agentsById: {},
|
|
@@ -49,6 +51,7 @@
|
|
|
49
51
|
messages: [],
|
|
50
52
|
tasks: [],
|
|
51
53
|
taskEvents: [],
|
|
54
|
+
taskEventCache: {},
|
|
52
55
|
stats: {},
|
|
53
56
|
health: null,
|
|
54
57
|
now: Date.now(),
|
|
@@ -58,6 +61,9 @@
|
|
|
58
61
|
inboxDrafts: loadPref("inboxDrafts", {}),
|
|
59
62
|
inboxSearch: "",
|
|
60
63
|
inboxShowArchived: loadPref("inboxShowArchived", false),
|
|
64
|
+
operatorActivity: loadPref("operatorActivity", []),
|
|
65
|
+
activityEvents: [],
|
|
66
|
+
activityFilter: loadPref("activityFilter", ""),
|
|
61
67
|
|
|
62
68
|
selectedAgent: "",
|
|
63
69
|
agentDetailOpen: false,
|
|
@@ -70,6 +76,8 @@
|
|
|
70
76
|
threadOpen: false,
|
|
71
77
|
threadMessages: [],
|
|
72
78
|
taskEventsOpen: false,
|
|
79
|
+
commandPaletteOpen: false,
|
|
80
|
+
commandQuery: "",
|
|
73
81
|
connected: false,
|
|
74
82
|
authNeeded: false,
|
|
75
83
|
|
|
@@ -99,6 +107,7 @@
|
|
|
99
107
|
|
|
100
108
|
function watchPersistedPrefs(vm) {
|
|
101
109
|
vm.$watch("showOffline", (value) => vm.save("showOffline", value));
|
|
110
|
+
vm.$watch("showBuiltIns", (value) => vm.save("showBuiltIns", value));
|
|
102
111
|
vm.$watch("autoRefresh", (value) => {
|
|
103
112
|
vm.save("autoRefresh", value);
|
|
104
113
|
if (value) vm.startAutoRefresh();
|
|
@@ -106,10 +115,12 @@
|
|
|
106
115
|
});
|
|
107
116
|
vm.$watch("agentSort", (value) => vm.save("agentSort", value));
|
|
108
117
|
vm.$watch("agentSortDir", (value) => vm.save("agentSortDir", value));
|
|
118
|
+
vm.$watch("agentPresetFilter", (value) => vm.save("agentPresetFilter", value));
|
|
109
119
|
vm.$watch("agentStatusFilter", (value) => vm.save("agentStatusFilter", value));
|
|
110
120
|
vm.$watch("agentTagFilter", (value) => vm.save("agentTagFilter", value));
|
|
111
121
|
vm.$watch("pairStatusFilter", (value) => vm.save("pairStatusFilter", value));
|
|
112
122
|
vm.$watch("inboxShowArchived", (value) => vm.save("inboxShowArchived", value));
|
|
123
|
+
vm.$watch("activityFilter", (value) => vm.save("activityFilter", value));
|
|
113
124
|
vm.$watch("view", (value) => {
|
|
114
125
|
vm.save("view", value);
|
|
115
126
|
if (value === "analytics") vm.$nextTick(() => vm.renderCharts());
|
|
@@ -156,11 +167,27 @@
|
|
|
156
167
|
vm.$nextTick(() => vm.renderCharts());
|
|
157
168
|
}
|
|
158
169
|
|
|
170
|
+
function registerKeyboardShortcuts(vm) {
|
|
171
|
+
if (typeof window === "undefined" || !window.addEventListener || vm._keyboardShortcutsRegistered) return;
|
|
172
|
+
window.addEventListener("keydown", (event) => {
|
|
173
|
+
const key = event.key?.toLowerCase();
|
|
174
|
+
if ((event.metaKey || event.ctrlKey) && key === "k") {
|
|
175
|
+
event.preventDefault();
|
|
176
|
+
vm.openCommandPalette();
|
|
177
|
+
} else if (key === "escape" && vm.commandPaletteOpen) {
|
|
178
|
+
event.preventDefault();
|
|
179
|
+
vm.closeCommandPalette();
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
vm._keyboardShortcutsRegistered = true;
|
|
183
|
+
}
|
|
184
|
+
|
|
159
185
|
function createLifecycleMethods() {
|
|
160
186
|
return {
|
|
161
187
|
async init() {
|
|
162
188
|
this.startClock();
|
|
163
189
|
watchPersistedPrefs(this);
|
|
190
|
+
registerKeyboardShortcuts(this);
|
|
164
191
|
|
|
165
192
|
try {
|
|
166
193
|
this.stats = await this.api("GET", "/stats");
|
|
@@ -181,6 +208,8 @@
|
|
|
181
208
|
this.view = view;
|
|
182
209
|
if (view === "inbox" || view === "messages") await this.fetchMessages();
|
|
183
210
|
if (view === "inbox") this.markInboxThreadRead(this.selectedInboxThreadData);
|
|
211
|
+
if (view === "activity") await Promise.all([this.fetchMessages(), this.fetchPairs(), this.fetchTasks(), this.fetchActivityEvents()]);
|
|
212
|
+
if (view === "work") await Promise.all([this.fetchMessages(), this.fetchTasks()]);
|
|
184
213
|
if (view === "pairs") this.fetchPairs();
|
|
185
214
|
if (view === "tasks") this.fetchTasks();
|
|
186
215
|
},
|
|
@@ -324,7 +353,7 @@
|
|
|
324
353
|
},
|
|
325
354
|
|
|
326
355
|
async refresh() {
|
|
327
|
-
await Promise.all([this.fetchStats(), this.fetchHealth(), this.fetchAgents(), this.fetchPairs(), this.fetchMessages(), this.fetchTasks(), this.fetchInboxState()]);
|
|
356
|
+
await Promise.all([this.fetchStats(), this.fetchHealth(), this.fetchAgents(), this.fetchPairs(), this.fetchMessages(), this.fetchTasks(), this.fetchInboxState(), this.fetchActivityEvents()]);
|
|
328
357
|
},
|
|
329
358
|
|
|
330
359
|
async refreshLiveData() {
|
|
@@ -360,7 +389,9 @@
|
|
|
360
389
|
async fetchPairs() {
|
|
361
390
|
try {
|
|
362
391
|
let pairs;
|
|
363
|
-
if (this.
|
|
392
|
+
if (this.view === "activity") {
|
|
393
|
+
pairs = await this.api("GET", "/pairs");
|
|
394
|
+
} else if (this.pairStatusFilter === "open") {
|
|
364
395
|
const [active, pending] = await Promise.all([
|
|
365
396
|
this.api("GET", "/pairs?status=active"),
|
|
366
397
|
this.api("GET", "/pairs?status=pending"),
|
|
@@ -399,6 +430,13 @@
|
|
|
399
430
|
applyInboxState(this, state);
|
|
400
431
|
} catch {}
|
|
401
432
|
},
|
|
433
|
+
|
|
434
|
+
async fetchActivityEvents() {
|
|
435
|
+
try {
|
|
436
|
+
this.activityEvents = await this.api("GET", "/activity?limit=200");
|
|
437
|
+
pruneSyncedOperatorActivity(this);
|
|
438
|
+
} catch {}
|
|
439
|
+
},
|
|
402
440
|
};
|
|
403
441
|
}
|
|
404
442
|
|
|
@@ -423,9 +461,17 @@
|
|
|
423
461
|
savePref("inboxDrafts", drafts);
|
|
424
462
|
}
|
|
425
463
|
|
|
464
|
+
function pruneSyncedOperatorActivity(vm) {
|
|
465
|
+
const serverClientIds = new Set((vm.activityEvents || []).map((event) => event.clientId).filter(Boolean));
|
|
466
|
+
if (!serverClientIds.size || !vm.operatorActivity?.length) return;
|
|
467
|
+
vm.operatorActivity = vm.operatorActivity.filter((item) => !serverClientIds.has(item.clientId || item.id));
|
|
468
|
+
savePref("operatorActivity", vm.operatorActivity);
|
|
469
|
+
}
|
|
470
|
+
|
|
426
471
|
function createComputedDescriptors() {
|
|
427
472
|
return {
|
|
428
473
|
onlineCount: { get: getOnlineCount },
|
|
474
|
+
hiddenBuiltInAgentCount: { get: getHiddenBuiltInAgentCount },
|
|
429
475
|
sortedAgents: { get: getSortedAgents },
|
|
430
476
|
pairsByAgentId: { get: getPairsByAgentId },
|
|
431
477
|
selectedAgentDetail: { get: getSelectedAgentDetail },
|
|
@@ -438,6 +484,8 @@
|
|
|
438
484
|
inboxComposeTargetOptions: { get: getInboxComposeTargetOptions },
|
|
439
485
|
attentionSummary: { get: getAttentionSummary },
|
|
440
486
|
attentionAgentCount: { get: getAttentionAgentCount },
|
|
487
|
+
activityItems: { get: getActivityItems },
|
|
488
|
+
workQueueItems: { get: getWorkQueueItems },
|
|
441
489
|
filteredMessages: { get: getFilteredMessages },
|
|
442
490
|
groupedMessages: { get: getGroupedMessages },
|
|
443
491
|
filteredTasks: { get: getFilteredTasks },
|
|
@@ -446,15 +494,23 @@
|
|
|
446
494
|
uniqueCaps: { get: getUniqueCaps },
|
|
447
495
|
uniqueTags: { get: getUniqueTags },
|
|
448
496
|
healthIssues: { get: getHealthIssues },
|
|
497
|
+
healthDiagnostics: { get: getHealthDiagnostics },
|
|
498
|
+
commandPaletteItems: { get: getCommandPaletteItems },
|
|
449
499
|
};
|
|
450
500
|
}
|
|
451
501
|
|
|
452
502
|
function getOnlineCount() {
|
|
453
|
-
return this.
|
|
503
|
+
return visibleAgents(this).filter((agent) => agent.status !== "offline").length;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function getHiddenBuiltInAgentCount() {
|
|
507
|
+
return this.showBuiltIns ? 0 : this.agents.filter(isBuiltInAgent).length;
|
|
454
508
|
}
|
|
455
509
|
|
|
456
510
|
function getSortedAgents() {
|
|
457
|
-
let list = this
|
|
511
|
+
let list = visibleAgents(this);
|
|
512
|
+
list = applyAgentPreset(this, list);
|
|
513
|
+
if (!this.showOffline) list = list.filter((agent) => agent.status !== "offline");
|
|
458
514
|
if (this.agentStatusFilter === "starting") {
|
|
459
515
|
list = list.filter((agent) => agent.status !== "offline" && !agent.ready);
|
|
460
516
|
} else if (this.agentStatusFilter) {
|
|
@@ -589,6 +645,244 @@
|
|
|
589
645
|
return this.sortedAgents.filter((agent) => agentAttention.call(this, agent).total > 0).length;
|
|
590
646
|
}
|
|
591
647
|
|
|
648
|
+
function getActivityItems() {
|
|
649
|
+
const items = [
|
|
650
|
+
...serverActivityItems(this),
|
|
651
|
+
...messageActivityItems(this),
|
|
652
|
+
...pairActivityItems(this),
|
|
653
|
+
...taskActivityItems(this),
|
|
654
|
+
...operatorActivityItems(this),
|
|
655
|
+
].filter((item) => item.ts);
|
|
656
|
+
|
|
657
|
+
const filter = this.activityFilter;
|
|
658
|
+
const filtered = filter ? items.filter((item) => item.kind === filter) : items;
|
|
659
|
+
return filtered
|
|
660
|
+
.sort((a, b) => b.ts - a.ts)
|
|
661
|
+
.slice(0, 150);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function getWorkQueueItems() {
|
|
665
|
+
const taskItems = (this.tasks || [])
|
|
666
|
+
.filter((task) => !CLOSED_TASK_STATUSES.has(task.status))
|
|
667
|
+
.map((task) => ({
|
|
668
|
+
id: "task-" + task.id,
|
|
669
|
+
sourceType: "task",
|
|
670
|
+
title: task.title,
|
|
671
|
+
body: task.body,
|
|
672
|
+
severity: task.severity || "info",
|
|
673
|
+
status: task.status,
|
|
674
|
+
owner: task.claimedBy || "",
|
|
675
|
+
target: task.target,
|
|
676
|
+
source: task.source,
|
|
677
|
+
channel: task.channel || "",
|
|
678
|
+
updatedAt: task.updatedAt || task.createdAt,
|
|
679
|
+
createdAt: task.createdAt,
|
|
680
|
+
claimable: isClaimableTaskWaiting(task),
|
|
681
|
+
task,
|
|
682
|
+
}));
|
|
683
|
+
|
|
684
|
+
const messageItems = (this.messages || [])
|
|
685
|
+
.filter((msg) => msg.claimable)
|
|
686
|
+
.map((msg) => ({
|
|
687
|
+
id: "message-" + msg.id,
|
|
688
|
+
sourceType: "message",
|
|
689
|
+
title: msg.subject || "Claimable message #" + msg.id,
|
|
690
|
+
body: msg.body,
|
|
691
|
+
severity: "warning",
|
|
692
|
+
status: msg.claimedBy ? "claimed" : "open",
|
|
693
|
+
owner: msg.claimedBy || "",
|
|
694
|
+
target: msg.to,
|
|
695
|
+
source: "message",
|
|
696
|
+
channel: msg.channel || "",
|
|
697
|
+
updatedAt: msg.claimedAt || msg.createdAt,
|
|
698
|
+
createdAt: msg.createdAt,
|
|
699
|
+
claimable: isClaimableMessageWaiting(msg),
|
|
700
|
+
message: msg,
|
|
701
|
+
}));
|
|
702
|
+
|
|
703
|
+
return [...taskItems, ...messageItems].sort(compareWorkQueueItems);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function compareWorkQueueItems(a, b) {
|
|
707
|
+
const claimableDelta = Number(b.claimable) - Number(a.claimable);
|
|
708
|
+
if (claimableDelta !== 0) return claimableDelta;
|
|
709
|
+
const severityOrder = { critical: 0, warning: 1, info: 2 };
|
|
710
|
+
const severityDelta = (severityOrder[a.severity] ?? 9) - (severityOrder[b.severity] ?? 9);
|
|
711
|
+
if (severityDelta !== 0) return severityDelta;
|
|
712
|
+
return toTimestamp(a.updatedAt) - toTimestamp(b.updatedAt);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function serverActivityItems(vm) {
|
|
716
|
+
return (vm.activityEvents || []).map((event) => activityItem({
|
|
717
|
+
id: "activity-" + event.id,
|
|
718
|
+
clientId: event.clientId,
|
|
719
|
+
kind: event.kind,
|
|
720
|
+
ts: toTimestamp(event.createdAt),
|
|
721
|
+
icon: event.icon,
|
|
722
|
+
title: event.title,
|
|
723
|
+
body: event.body,
|
|
724
|
+
meta: event.meta,
|
|
725
|
+
view: event.view,
|
|
726
|
+
peer: event.peer,
|
|
727
|
+
messageId: event.messageId,
|
|
728
|
+
pairId: event.pairId,
|
|
729
|
+
taskId: event.taskId,
|
|
730
|
+
agentId: event.agentId,
|
|
731
|
+
}));
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function messageActivityItems(vm) {
|
|
735
|
+
return (vm.messages || []).flatMap((msg) => {
|
|
736
|
+
const ts = toTimestamp(msg.createdAt);
|
|
737
|
+
const items = [];
|
|
738
|
+
const pairEvent = msg.meta?.pairEvent;
|
|
739
|
+
if (pairEvent) {
|
|
740
|
+
items.push(activityItem({
|
|
741
|
+
id: "pair-message-" + msg.id,
|
|
742
|
+
kind: pairEvent === "message" ? "pair" : "state",
|
|
743
|
+
ts,
|
|
744
|
+
icon: pairEvent === "message" ? "ti-messages" : "ti-link",
|
|
745
|
+
title: pairEvent === "message" ? "Pair message" : "Pair " + pairEvent,
|
|
746
|
+
body: vm.messagePreview(msg),
|
|
747
|
+
meta: `${vm.displayTarget(msg.from)} -> ${vm.displayTarget(msg.to)}`,
|
|
748
|
+
view: "pairs",
|
|
749
|
+
messageId: msg.id,
|
|
750
|
+
}));
|
|
751
|
+
} else if (msg.from === HUMAN_AGENT_ID) {
|
|
752
|
+
items.push(activityItem({
|
|
753
|
+
id: "human-send-" + msg.id,
|
|
754
|
+
kind: msg.claimable ? "task" : "message",
|
|
755
|
+
ts,
|
|
756
|
+
icon: msg.claimable ? "ti-hand-grab" : "ti-send",
|
|
757
|
+
title: msg.claimable ? "Claimable task sent" : "Message sent",
|
|
758
|
+
body: vm.messagePreview(msg),
|
|
759
|
+
meta: "to " + vm.displayTarget(msg.to),
|
|
760
|
+
view: inboxPeer(msg) ? "inbox" : "messages",
|
|
761
|
+
peer: inboxPeer(msg),
|
|
762
|
+
messageId: msg.id,
|
|
763
|
+
}));
|
|
764
|
+
} else if (msg.to === HUMAN_AGENT_ID) {
|
|
765
|
+
items.push(activityItem({
|
|
766
|
+
id: "agent-reply-" + msg.id,
|
|
767
|
+
kind: messageLooksLikeQuestion(msg) ? "question" : "reply",
|
|
768
|
+
ts,
|
|
769
|
+
icon: messageLooksLikeQuestion(msg) ? "ti-help-circle" : "ti-message-reply",
|
|
770
|
+
title: messageLooksLikeQuestion(msg) ? "Agent asked a question" : "Agent replied",
|
|
771
|
+
body: vm.messagePreview(msg),
|
|
772
|
+
meta: "from " + vm.displayTarget(msg.from),
|
|
773
|
+
view: "inbox",
|
|
774
|
+
peer: msg.from,
|
|
775
|
+
agentId: msg.from,
|
|
776
|
+
messageId: msg.id,
|
|
777
|
+
}));
|
|
778
|
+
} else {
|
|
779
|
+
items.push(activityItem({
|
|
780
|
+
id: "message-" + msg.id,
|
|
781
|
+
kind: "message",
|
|
782
|
+
ts,
|
|
783
|
+
icon: "ti-messages",
|
|
784
|
+
title: "Agent message",
|
|
785
|
+
body: vm.messagePreview(msg),
|
|
786
|
+
meta: `${vm.displayTarget(msg.from)} -> ${vm.displayTarget(msg.to)}`,
|
|
787
|
+
view: "messages",
|
|
788
|
+
messageId: msg.id,
|
|
789
|
+
}));
|
|
790
|
+
}
|
|
791
|
+
if (msg.claimedBy) {
|
|
792
|
+
items.push(activityItem({
|
|
793
|
+
id: "claim-" + msg.id,
|
|
794
|
+
kind: "task",
|
|
795
|
+
ts: toTimestamp(msg.claimedAt || msg.createdAt),
|
|
796
|
+
icon: "ti-user-check",
|
|
797
|
+
title: "Task claimed",
|
|
798
|
+
body: vm.messagePreview(msg),
|
|
799
|
+
meta: "by " + vm.displayTarget(msg.claimedBy),
|
|
800
|
+
view: "tasks",
|
|
801
|
+
messageId: msg.id,
|
|
802
|
+
agentId: msg.claimedBy,
|
|
803
|
+
}));
|
|
804
|
+
}
|
|
805
|
+
return items;
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function pairActivityItems(vm) {
|
|
810
|
+
return (vm.pairs || []).flatMap((pair) => {
|
|
811
|
+
const created = activityItem({
|
|
812
|
+
id: "pair-created-" + pair.id,
|
|
813
|
+
kind: "pair",
|
|
814
|
+
ts: toTimestamp(pair.createdAt),
|
|
815
|
+
icon: "ti-link-plus",
|
|
816
|
+
title: "Pair invite created",
|
|
817
|
+
body: pair.objective || "",
|
|
818
|
+
meta: `${vm.displayTarget(pair.requesterId)} <-> ${vm.displayTarget(pair.targetId)}`,
|
|
819
|
+
view: "pairs",
|
|
820
|
+
pairId: pair.id,
|
|
821
|
+
});
|
|
822
|
+
const statusTs = toTimestamp(pair.updatedAt || pair.acceptedAt || pair.endedAt);
|
|
823
|
+
if (!statusTs || statusTs === created.ts || pair.status === "pending") return [created];
|
|
824
|
+
return [created, activityItem({
|
|
825
|
+
id: "pair-status-" + pair.id + "-" + pair.status,
|
|
826
|
+
kind: "state",
|
|
827
|
+
ts: statusTs,
|
|
828
|
+
icon: pair.status === "active" ? "ti-link" : "ti-phone-off",
|
|
829
|
+
title: "Pair " + pair.status,
|
|
830
|
+
body: pair.objective || "",
|
|
831
|
+
meta: pair.endedBy ? "by " + vm.displayTarget(pair.endedBy) : `${vm.displayTarget(pair.requesterId)} <-> ${vm.displayTarget(pair.targetId)}`,
|
|
832
|
+
view: "pairs",
|
|
833
|
+
pairId: pair.id,
|
|
834
|
+
})];
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function taskActivityItems(vm) {
|
|
839
|
+
return (vm.tasks || []).map((task) => activityItem({
|
|
840
|
+
id: "task-" + task.id + "-" + task.status,
|
|
841
|
+
kind: "task",
|
|
842
|
+
ts: toTimestamp(task.updatedAt || task.createdAt),
|
|
843
|
+
icon: task.claimedBy ? "ti-user-check" : "ti-checkup-list",
|
|
844
|
+
title: task.status === "open" ? "Claimable task waiting" : "Task " + task.status,
|
|
845
|
+
body: task.title || task.body || "",
|
|
846
|
+
meta: task.claimedBy ? "claimed by " + vm.displayTarget(task.claimedBy) : vm.displayTarget(task.target),
|
|
847
|
+
view: "tasks",
|
|
848
|
+
taskId: task.id,
|
|
849
|
+
agentId: task.claimedBy || task.target,
|
|
850
|
+
}));
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function operatorActivityItems(vm) {
|
|
854
|
+
const serverClientIds = new Set((vm.activityEvents || []).map((event) => event.clientId).filter(Boolean));
|
|
855
|
+
return (vm.operatorActivity || []).map((item) => activityItem({
|
|
856
|
+
...item,
|
|
857
|
+
id: item.id || "operator-" + item.ts + "-" + item.title,
|
|
858
|
+
clientId: item.clientId || item.id,
|
|
859
|
+
})).filter((item) => !serverClientIds.has(item.clientId));
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function activityItem(input) {
|
|
863
|
+
return {
|
|
864
|
+
id: input.id,
|
|
865
|
+
kind: input.kind || "state",
|
|
866
|
+
ts: Number(input.ts) || 0,
|
|
867
|
+
icon: input.icon || "ti-activity",
|
|
868
|
+
title: input.title || "Activity",
|
|
869
|
+
body: input.body || "",
|
|
870
|
+
meta: input.meta || "",
|
|
871
|
+
view: input.view || "",
|
|
872
|
+
peer: input.peer || "",
|
|
873
|
+
clientId: input.clientId || "",
|
|
874
|
+
messageId: input.messageId,
|
|
875
|
+
pairId: input.pairId,
|
|
876
|
+
taskId: input.taskId,
|
|
877
|
+
agentId: input.agentId,
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function toTimestamp(value) {
|
|
882
|
+
const ts = typeof value === "number" ? value : new Date(value || 0).getTime();
|
|
883
|
+
return Number.isFinite(ts) ? ts : 0;
|
|
884
|
+
}
|
|
885
|
+
|
|
592
886
|
function inboxPeer(msg) {
|
|
593
887
|
if (msg.from === HUMAN_AGENT_ID && msg.to) return msg.to;
|
|
594
888
|
if (msg.to === HUMAN_AGENT_ID && msg.from) return msg.from;
|
|
@@ -711,25 +1005,137 @@
|
|
|
711
1005
|
}
|
|
712
1006
|
|
|
713
1007
|
function getComposeAgents() {
|
|
714
|
-
|
|
1008
|
+
const list = visibleAgents(this);
|
|
1009
|
+
return this.showOffline ? list : list.filter((agent) => agent.status !== "offline");
|
|
715
1010
|
}
|
|
716
1011
|
|
|
717
1012
|
function getUniqueLabels() {
|
|
718
|
-
return [...new Set(this.
|
|
1013
|
+
return [...new Set(visibleAgents(this).filter((agent) => agent.label).map((agent) => agent.label))];
|
|
719
1014
|
}
|
|
720
1015
|
|
|
721
1016
|
function getUniqueCaps() {
|
|
722
|
-
return [...new Set(this.
|
|
1017
|
+
return [...new Set(visibleAgents(this).flatMap((agent) => agent.capabilities || []))];
|
|
723
1018
|
}
|
|
724
1019
|
|
|
725
1020
|
function getUniqueTags() {
|
|
726
|
-
return [...new Set(this.
|
|
1021
|
+
return [...new Set(visibleAgents(this).flatMap((agent) => agent.tags || []))];
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function visibleAgents(vm) {
|
|
1025
|
+
return vm.showBuiltIns ? [...vm.agents] : vm.agents.filter((agent) => !isBuiltInAgent(agent));
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function isBuiltInAgent(agent) {
|
|
1029
|
+
return agent?.meta?.builtin === true || agent?.id === HUMAN_AGENT_ID || agent?.id === "system";
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function applyAgentPreset(vm, list) {
|
|
1033
|
+
switch (vm.agentPresetFilter) {
|
|
1034
|
+
case "active":
|
|
1035
|
+
return list.filter((agent) => agent.status !== "offline");
|
|
1036
|
+
case "offline_stale":
|
|
1037
|
+
return list.filter((agent) => agent.status === "offline" || isAgentStale(vm, agent));
|
|
1038
|
+
case "claude":
|
|
1039
|
+
case "codex":
|
|
1040
|
+
return list.filter((agent) => agentType(agent) === vm.agentPresetFilter);
|
|
1041
|
+
case "paired":
|
|
1042
|
+
return list.filter((agent) => Boolean(vm.agentPair(agent)));
|
|
1043
|
+
case "unpaired":
|
|
1044
|
+
return list.filter((agent) => !vm.agentPair(agent));
|
|
1045
|
+
case "waiting":
|
|
1046
|
+
return list.filter((agent) => agentAttention.call(vm, agent).total > 0);
|
|
1047
|
+
case "claimable":
|
|
1048
|
+
return list.filter((agent) => agentAttention.call(vm, agent).claimableTasks > 0);
|
|
1049
|
+
case "errors":
|
|
1050
|
+
return list.filter((agent) => agent.status === "offline" || (agent.status !== "offline" && !agent.ready) || isAgentStale(vm, agent));
|
|
1051
|
+
default:
|
|
1052
|
+
return list;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function isAgentStale(vm, agent) {
|
|
1057
|
+
if (!agent?.lastSeen || agent.status === "offline") return false;
|
|
1058
|
+
const lastSeenMs = new Date(agent.lastSeen).getTime();
|
|
1059
|
+
if (!Number.isFinite(lastSeenMs)) return false;
|
|
1060
|
+
return (vm.now || Date.now()) - lastSeenMs > 60_000;
|
|
727
1061
|
}
|
|
728
1062
|
|
|
729
1063
|
function getHealthIssues() {
|
|
730
1064
|
return (this.health?.checks || []).filter((check) => check.status !== "ok");
|
|
731
1065
|
}
|
|
732
1066
|
|
|
1067
|
+
function getHealthDiagnostics() {
|
|
1068
|
+
return this.healthIssues.map((check) => {
|
|
1069
|
+
const base = {
|
|
1070
|
+
name: check.name,
|
|
1071
|
+
status: check.status,
|
|
1072
|
+
detail: check.detail || check.name,
|
|
1073
|
+
impact: healthImpact(check),
|
|
1074
|
+
actions: [
|
|
1075
|
+
{ label: "Inspect logs", icon: "ti-file-search", copy: "agent-relay daemon logs" },
|
|
1076
|
+
{ label: "Restart daemon", icon: "ti-refresh", copy: "agent-relay daemon restart" },
|
|
1077
|
+
{ label: "Copy env", icon: "ti-copy", copy: "agent-relay doctor" },
|
|
1078
|
+
],
|
|
1079
|
+
};
|
|
1080
|
+
if (check.name === "stale-live-agents") {
|
|
1081
|
+
base.actions.unshift(
|
|
1082
|
+
{ label: "Run reaper", icon: "ti-broom", api: "POST", path: "/system/reap" },
|
|
1083
|
+
{ label: "Show stale", icon: "ti-filter", view: "agents", preset: "offline_stale" }
|
|
1084
|
+
);
|
|
1085
|
+
} else if (check.name === "expired-message-claims" || check.name === "expired-task-claims" || check.name === "offline-claimed-tasks") {
|
|
1086
|
+
base.actions.unshift(
|
|
1087
|
+
{ label: "Run reaper", icon: "ti-broom", api: "POST", path: "/system/reap" },
|
|
1088
|
+
{ label: "Open work", icon: "ti-list-check", view: "work" }
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
return base;
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function healthImpact(check) {
|
|
1096
|
+
if (check.name === "database") return "Relay persistence is unavailable; messages, state, and audit writes may fail.";
|
|
1097
|
+
if (check.name === "stale-live-agents") return "Agents may look online even though their heartbeat has stopped.";
|
|
1098
|
+
if (check.name === "expired-message-claims") return "Claimable messages may be stuck until the reaper releases expired claims.";
|
|
1099
|
+
if (check.name === "expired-task-claims") return "Tasks can appear owned by agents that no longer hold a live lease.";
|
|
1100
|
+
if (check.name === "offline-claimed-tasks") return "Offline agents are still shown as owners for active work.";
|
|
1101
|
+
return "Relay health is degraded for this check.";
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function getCommandPaletteItems() {
|
|
1105
|
+
const query = this.commandQuery.trim().toLowerCase();
|
|
1106
|
+
const commands = [
|
|
1107
|
+
commandItem("open-inbox", "Open inbox", "Human console", "ti-inbox", "openView", { view: "inbox" }),
|
|
1108
|
+
commandItem("open-work", "Open work queue", "Claimable messages and tasks", "ti-list-check", "openView", { view: "work" }),
|
|
1109
|
+
commandItem("show-stale-agents", "Show stale agents", "Filter agents to offline or stale heartbeat", "ti-filter", "agentPreset", { preset: "offline_stale" }),
|
|
1110
|
+
commandItem("copy-relay-url", "Copy relay URL", baseUrl(), "ti-copy", "copy", { value: baseUrl() }),
|
|
1111
|
+
commandItem("export-timeline-md", "Export full timeline as Markdown", "Activity audit trace", "ti-file-export", "exportActivity", { format: "markdown" }),
|
|
1112
|
+
commandItem("export-timeline-json", "Export full timeline as JSON", "Activity audit trace", "ti-braces", "exportActivity", { format: "json" }),
|
|
1113
|
+
...this.composeAgents.slice(0, 12).map((agent) =>
|
|
1114
|
+
commandItem("message-" + agent.id, "Message agent: " + this.displayName(agent), agent.id, "ti-send", "messageAgent", { agentId: agent.id })
|
|
1115
|
+
),
|
|
1116
|
+
...this.composeAgents.filter((agent) => agentType(agent) === "codex").slice(0, 8).map((agent) =>
|
|
1117
|
+
commandItem("pair-codex-" + agent.id, "Pair Codex: " + this.displayName(agent), agent.id, "ti-link-plus", "pairAgent", { agentId: agent.id })
|
|
1118
|
+
),
|
|
1119
|
+
...this.uniqueTags.map((tag) =>
|
|
1120
|
+
commandItem("filter-tag-" + tag, "Filter tag: " + tag, "#" + tag, "ti-tag", "filterTag", { tag })
|
|
1121
|
+
),
|
|
1122
|
+
];
|
|
1123
|
+
if (!query) return commands.slice(0, 24);
|
|
1124
|
+
return commands.filter((command) => command.search.includes(query)).slice(0, 24);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
function commandItem(id, title, subtitle, icon, action, payload) {
|
|
1128
|
+
return {
|
|
1129
|
+
id,
|
|
1130
|
+
title,
|
|
1131
|
+
subtitle,
|
|
1132
|
+
icon,
|
|
1133
|
+
action,
|
|
1134
|
+
payload: payload || {},
|
|
1135
|
+
search: `${title}\n${subtitle || ""}\n${action}`.toLowerCase(),
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
|
|
733
1139
|
function compareAgents(vm, a, b) {
|
|
734
1140
|
if (vm.agentSort === "status") {
|
|
735
1141
|
const attentionDelta = agentAttention.call(vm, b).score - agentAttention.call(vm, a).score;
|
|
@@ -766,11 +1172,15 @@
|
|
|
766
1172
|
agentType,
|
|
767
1173
|
agentTypeIcon,
|
|
768
1174
|
agentTypeTitle,
|
|
1175
|
+
agentPresence,
|
|
1176
|
+
agentPresenceBadges,
|
|
1177
|
+
agentStatusClass,
|
|
769
1178
|
severityClass,
|
|
770
1179
|
agentStatusTitle,
|
|
771
1180
|
timeAgo,
|
|
772
1181
|
fmtTime,
|
|
773
1182
|
healthAlertClass,
|
|
1183
|
+
activityKindClass,
|
|
774
1184
|
};
|
|
775
1185
|
}
|
|
776
1186
|
|
|
@@ -913,6 +1323,66 @@
|
|
|
913
1323
|
return AGENT_TYPE_TITLES[agentType(agent)] || AGENT_TYPE_TITLES.agent;
|
|
914
1324
|
}
|
|
915
1325
|
|
|
1326
|
+
function agentPresence(agent) {
|
|
1327
|
+
const attention = agentAttention.call(this, agent);
|
|
1328
|
+
const pair = this.agentPair(agent);
|
|
1329
|
+
const stale = isAgentStale(this, agent);
|
|
1330
|
+
const reconnecting = agent?.status !== "offline" && !agent?.ready && stale;
|
|
1331
|
+
const starting = agent?.status !== "offline" && !agent?.ready && !stale;
|
|
1332
|
+
const unreadIdle = attention.unread > 0 && agent?.status !== "busy";
|
|
1333
|
+
|
|
1334
|
+
if (agent?.status === "offline") {
|
|
1335
|
+
return { label: "offline", tone: "secondary", icon: "ti-plug-off", stale: false, reconnecting: false, badges: presenceBadges(attention, pair, { offline: true }) };
|
|
1336
|
+
}
|
|
1337
|
+
if (reconnecting) {
|
|
1338
|
+
return { label: "reconnecting", tone: "danger", icon: "ti-refresh", stale, reconnecting, badges: presenceBadges(attention, pair, { reconnecting }) };
|
|
1339
|
+
}
|
|
1340
|
+
if (starting) {
|
|
1341
|
+
return { label: "online, not ready", tone: "warning", icon: "ti-loader", stale, reconnecting, badges: presenceBadges(attention, pair, { starting }) };
|
|
1342
|
+
}
|
|
1343
|
+
if (agent?.status === "busy") {
|
|
1344
|
+
return { label: "busy in turn", tone: "warning", icon: "ti-player-play", stale, reconnecting, badges: presenceBadges(attention, pair, { busy: true }) };
|
|
1345
|
+
}
|
|
1346
|
+
if (pair?.status === "active") {
|
|
1347
|
+
return { label: "paired", tone: "success", icon: "ti-link", stale, reconnecting, badges: presenceBadges(attention, pair, { paired: true }) };
|
|
1348
|
+
}
|
|
1349
|
+
if (unreadIdle) {
|
|
1350
|
+
return { label: "idle, unread", tone: "danger", icon: "ti-bell", stale, reconnecting, badges: presenceBadges(attention, pair, { unreadIdle: true }) };
|
|
1351
|
+
}
|
|
1352
|
+
return { label: agent?.status === "idle" ? "idle" : "ready", tone: "success", icon: "ti-circle-check", stale, reconnecting, badges: presenceBadges(attention, pair, {}) };
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
function presenceBadges(attention, pair, flags) {
|
|
1356
|
+
const badges = [];
|
|
1357
|
+
if (flags.reconnecting) badges.push({ label: "reconnecting", className: "bg-danger-lt" });
|
|
1358
|
+
if (flags.starting) badges.push({ label: "online, not ready", className: "bg-warning-lt" });
|
|
1359
|
+
if (flags.busy) badges.push({ label: "busy in turn", className: "bg-warning-lt" });
|
|
1360
|
+
if (pair?.status === "active") badges.push({ label: "paired", className: "bg-success-lt" });
|
|
1361
|
+
if (pair?.status === "pending") badges.push({ label: "pair invite pending", className: "bg-warning-lt" });
|
|
1362
|
+
if (attention.unread) badges.push({ label: attention.unread + " unread", className: "bg-danger-lt" });
|
|
1363
|
+
if (attention.needsHumanResponse) badges.push({ label: "needs response", className: "bg-warning-lt" });
|
|
1364
|
+
if (attention.agentQuestion) badges.push({ label: "question", className: "bg-info-lt" });
|
|
1365
|
+
if (attention.claimableTasks) badges.push({ label: attention.claimableTasks + " claimable", className: "bg-orange-lt" });
|
|
1366
|
+
if (!badges.length && !flags.offline) badges.push({ label: "ready", className: "bg-success-lt" });
|
|
1367
|
+
return badges;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
function agentPresenceBadges(agent) {
|
|
1371
|
+
return agentPresence.call(this, agent).badges;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
function agentStatusClass(agent) {
|
|
1375
|
+
const presence = agentPresence.call(this, agent);
|
|
1376
|
+
return [
|
|
1377
|
+
agent?.status || "offline",
|
|
1378
|
+
agent?.status !== "offline" && !agent?.ready ? "not-ready" : "",
|
|
1379
|
+
presence.stale ? "stale" : "",
|
|
1380
|
+
presence.reconnecting ? "reconnecting" : "",
|
|
1381
|
+
presence.label === "paired" ? "paired" : "",
|
|
1382
|
+
presence.label === "idle, unread" ? "attention" : "",
|
|
1383
|
+
].filter(Boolean).join(" ");
|
|
1384
|
+
}
|
|
1385
|
+
|
|
916
1386
|
function severityClass(severity) {
|
|
917
1387
|
if (severity === "critical") return "bg-danger-lt";
|
|
918
1388
|
if (severity === "warning") return "bg-warning-lt";
|
|
@@ -922,6 +1392,11 @@
|
|
|
922
1392
|
function agentStatusTitle(agent) {
|
|
923
1393
|
if (!agent) return "";
|
|
924
1394
|
if (agent.status === "offline") return "offline";
|
|
1395
|
+
if (isAgentStale(this, agent) && !agent.ready) return "reconnecting";
|
|
1396
|
+
if (isAgentStale(this, agent)) return "stale heartbeat";
|
|
1397
|
+
if (agent.status === "busy") return "busy in turn";
|
|
1398
|
+
if (this.agentPair(agent)?.status === "active") return "paired";
|
|
1399
|
+
if (agentAttention.call(this, agent).unread) return "idle but has unread";
|
|
925
1400
|
if (agent.ready) return agent.status;
|
|
926
1401
|
|
|
927
1402
|
const lastSeenMs = new Date(agent.lastSeen).getTime();
|
|
@@ -954,6 +1429,15 @@
|
|
|
954
1429
|
return "alert-success";
|
|
955
1430
|
}
|
|
956
1431
|
|
|
1432
|
+
function activityKindClass(kind) {
|
|
1433
|
+
if (kind === "question") return "bg-info-lt";
|
|
1434
|
+
if (kind === "task") return "bg-warning-lt";
|
|
1435
|
+
if (kind === "pair") return "bg-success-lt";
|
|
1436
|
+
if (kind === "operator") return "bg-primary-lt";
|
|
1437
|
+
if (kind === "reply") return "bg-purple-lt";
|
|
1438
|
+
return "bg-secondary-lt";
|
|
1439
|
+
}
|
|
1440
|
+
|
|
957
1441
|
function createMessageActions() {
|
|
958
1442
|
return {
|
|
959
1443
|
openCompose,
|
|
@@ -976,9 +1460,21 @@
|
|
|
976
1460
|
cancelReply,
|
|
977
1461
|
doSend,
|
|
978
1462
|
doClaim,
|
|
1463
|
+
doClaimTask,
|
|
1464
|
+
doUpdateTaskStatus,
|
|
979
1465
|
doDeleteMessage,
|
|
980
1466
|
openThread,
|
|
981
1467
|
openTaskEvents,
|
|
1468
|
+
recordOperatorActivity,
|
|
1469
|
+
openActivityItem,
|
|
1470
|
+
runHealthAction,
|
|
1471
|
+
openCommandPalette,
|
|
1472
|
+
closeCommandPalette,
|
|
1473
|
+
runCommand,
|
|
1474
|
+
exportActivity,
|
|
1475
|
+
exportThread,
|
|
1476
|
+
exportPair,
|
|
1477
|
+
exportTask,
|
|
982
1478
|
};
|
|
983
1479
|
}
|
|
984
1480
|
|
|
@@ -1044,6 +1540,14 @@
|
|
|
1044
1540
|
delete next[thread.peer];
|
|
1045
1541
|
this.inboxReadCursors = next;
|
|
1046
1542
|
savePref("inboxReadCursors", this.inboxReadCursors);
|
|
1543
|
+
this.recordOperatorActivity({
|
|
1544
|
+
title: "Marked thread unread",
|
|
1545
|
+
body: this.conversationTitle(thread),
|
|
1546
|
+
meta: "Inbox",
|
|
1547
|
+
icon: "ti-mail",
|
|
1548
|
+
view: "inbox",
|
|
1549
|
+
peer: thread.peer,
|
|
1550
|
+
});
|
|
1047
1551
|
void saveInboxThreadState(this, {
|
|
1048
1552
|
peerId: thread.peer,
|
|
1049
1553
|
readCursorMessageId: null,
|
|
@@ -1054,6 +1558,14 @@
|
|
|
1054
1558
|
if (!thread?.peer || !thread.lastMessage) return;
|
|
1055
1559
|
this.inboxArchivedThreads = { ...this.inboxArchivedThreads, [thread.peer]: thread.lastMessage.id };
|
|
1056
1560
|
savePref("inboxArchivedThreads", this.inboxArchivedThreads);
|
|
1561
|
+
this.recordOperatorActivity({
|
|
1562
|
+
title: "Archived thread",
|
|
1563
|
+
body: this.conversationTitle(thread),
|
|
1564
|
+
meta: "Inbox",
|
|
1565
|
+
icon: "ti-archive",
|
|
1566
|
+
view: "inbox",
|
|
1567
|
+
peer: thread.peer,
|
|
1568
|
+
});
|
|
1057
1569
|
void saveInboxThreadState(this, {
|
|
1058
1570
|
peerId: thread.peer,
|
|
1059
1571
|
archivedAtMessageId: thread.lastMessage.id,
|
|
@@ -1067,6 +1579,14 @@
|
|
|
1067
1579
|
delete next[thread.peer];
|
|
1068
1580
|
this.inboxArchivedThreads = next;
|
|
1069
1581
|
savePref("inboxArchivedThreads", this.inboxArchivedThreads);
|
|
1582
|
+
this.recordOperatorActivity({
|
|
1583
|
+
title: "Unarchived thread",
|
|
1584
|
+
body: this.conversationTitle(thread),
|
|
1585
|
+
meta: "Inbox",
|
|
1586
|
+
icon: "ti-archive-off",
|
|
1587
|
+
view: "inbox",
|
|
1588
|
+
peer: thread.peer,
|
|
1589
|
+
});
|
|
1070
1590
|
void saveInboxThreadState(this, {
|
|
1071
1591
|
peerId: thread.peer,
|
|
1072
1592
|
archivedAtMessageId: null,
|
|
@@ -1095,6 +1615,13 @@
|
|
|
1095
1615
|
this.messages = this.messages.filter((msg) => !thread.messages.some((item) => item.id === msg.id));
|
|
1096
1616
|
this.selectedInboxThread = "";
|
|
1097
1617
|
this.clearReplyDraft(thread);
|
|
1618
|
+
this.recordOperatorActivity({
|
|
1619
|
+
title: "Deleted thread",
|
|
1620
|
+
body: this.conversationTitle(thread),
|
|
1621
|
+
meta: thread.messages.length + " message(s)",
|
|
1622
|
+
icon: "ti-trash",
|
|
1623
|
+
view: "inbox",
|
|
1624
|
+
});
|
|
1098
1625
|
} catch (e) {
|
|
1099
1626
|
alert("Delete thread failed: " + e.message);
|
|
1100
1627
|
}
|
|
@@ -1151,6 +1678,14 @@
|
|
|
1151
1678
|
if (thread.lastMessage?.channel) payload.channel = thread.lastMessage.channel;
|
|
1152
1679
|
if (thread.lastMessage?.id) payload.replyTo = thread.lastMessage.id;
|
|
1153
1680
|
await this.api("POST", "/messages", payload);
|
|
1681
|
+
this.recordOperatorActivity({
|
|
1682
|
+
title: "Reply sent",
|
|
1683
|
+
body,
|
|
1684
|
+
meta: "to " + this.displayTarget(thread.peer),
|
|
1685
|
+
icon: "ti-corner-up-left",
|
|
1686
|
+
view: "inbox",
|
|
1687
|
+
peer: thread.peer,
|
|
1688
|
+
});
|
|
1154
1689
|
this.clearReplyDraft(thread);
|
|
1155
1690
|
this.markInboxThreadRead(thread);
|
|
1156
1691
|
await this.fetchMessages();
|
|
@@ -1188,6 +1723,15 @@
|
|
|
1188
1723
|
if (this.inboxCompose.subject) payload.subject = this.inboxCompose.subject;
|
|
1189
1724
|
if (this.inboxCompose.claimable) payload.claimable = true;
|
|
1190
1725
|
await this.api("POST", "/messages", payload);
|
|
1726
|
+
this.recordOperatorActivity({
|
|
1727
|
+
title: this.inboxCompose.claimable ? "Claimable task sent" : "Message sent",
|
|
1728
|
+
body: this.inboxCompose.subject || this.inboxCompose.body,
|
|
1729
|
+
meta: "to " + this.displayTarget(target),
|
|
1730
|
+
icon: this.inboxCompose.claimable ? "ti-hand-grab" : "ti-send",
|
|
1731
|
+
kind: this.inboxCompose.claimable ? "task" : "operator",
|
|
1732
|
+
view: inboxPeer(payload) ? "inbox" : "messages",
|
|
1733
|
+
peer: inboxPeer(payload),
|
|
1734
|
+
});
|
|
1191
1735
|
this.inboxCompose = { ...DEFAULT_INBOX_COMPOSE, toMode: this.inboxCompose.toMode, to: this.inboxCompose.to };
|
|
1192
1736
|
await this.fetchMessages();
|
|
1193
1737
|
} catch (e) {
|
|
@@ -1231,7 +1775,17 @@
|
|
|
1231
1775
|
}
|
|
1232
1776
|
|
|
1233
1777
|
try {
|
|
1234
|
-
|
|
1778
|
+
const payload = buildMessagePayload(this);
|
|
1779
|
+
await this.api("POST", "/messages", payload);
|
|
1780
|
+
this.recordOperatorActivity({
|
|
1781
|
+
title: payload.claimable ? "Claimable task sent" : "Message sent",
|
|
1782
|
+
body: payload.subject || payload.body,
|
|
1783
|
+
meta: `${this.displayTarget(payload.from)} -> ${this.displayTarget(payload.to)}`,
|
|
1784
|
+
icon: payload.claimable ? "ti-hand-grab" : "ti-send",
|
|
1785
|
+
kind: payload.claimable ? "task" : "operator",
|
|
1786
|
+
view: inboxPeer(payload) ? "inbox" : "messages",
|
|
1787
|
+
peer: inboxPeer(payload),
|
|
1788
|
+
});
|
|
1235
1789
|
this.composeOpen = false;
|
|
1236
1790
|
this.replyTo = null;
|
|
1237
1791
|
this.compose = { ...DEFAULT_COMPOSE };
|
|
@@ -1251,15 +1805,85 @@
|
|
|
1251
1805
|
try {
|
|
1252
1806
|
const result = await this.api("POST", "/messages/" + msgId + "/claim", { agentId });
|
|
1253
1807
|
if (!result.ok) alert("Claim failed: " + (result.error || "unknown"));
|
|
1808
|
+
else this.recordOperatorActivity({
|
|
1809
|
+
title: "Claim requested",
|
|
1810
|
+
body: "Message #" + msgId,
|
|
1811
|
+
meta: "as " + this.displayTarget(agentId),
|
|
1812
|
+
icon: "ti-hand-grab",
|
|
1813
|
+
kind: "task",
|
|
1814
|
+
view: "tasks",
|
|
1815
|
+
});
|
|
1816
|
+
if (result.ok) await Promise.all([this.fetchMessages(), this.fetchTasks()]);
|
|
1254
1817
|
} catch (e) {
|
|
1255
1818
|
alert("Claim failed: " + e.message);
|
|
1256
1819
|
}
|
|
1257
1820
|
}
|
|
1258
1821
|
|
|
1822
|
+
async function doClaimTask(taskId) {
|
|
1823
|
+
if (!this.compose.from && !this.selectedAgent) {
|
|
1824
|
+
alert('Select an agent first (use the Messages agent filter or Compose From).');
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
const agentId = this.compose.from || this.selectedAgent;
|
|
1829
|
+
try {
|
|
1830
|
+
const result = await this.api("POST", "/tasks/" + taskId + "/claim", { agentId });
|
|
1831
|
+
if (!result.ok) alert("Task claim failed: " + (result.error || "unknown"));
|
|
1832
|
+
else {
|
|
1833
|
+
this.recordOperatorActivity({
|
|
1834
|
+
title: "Task claimed",
|
|
1835
|
+
body: "Task #" + taskId,
|
|
1836
|
+
meta: "as " + this.displayTarget(agentId),
|
|
1837
|
+
icon: "ti-user-check",
|
|
1838
|
+
kind: "task",
|
|
1839
|
+
view: "work",
|
|
1840
|
+
taskId,
|
|
1841
|
+
agentId,
|
|
1842
|
+
});
|
|
1843
|
+
await Promise.all([this.fetchTasks(), this.fetchMessages()]);
|
|
1844
|
+
}
|
|
1845
|
+
} catch (e) {
|
|
1846
|
+
alert("Task claim failed: " + e.message);
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
async function doUpdateTaskStatus(task, status) {
|
|
1851
|
+
if (!task || !status || status === task.status) return;
|
|
1852
|
+
try {
|
|
1853
|
+
const body = { status };
|
|
1854
|
+
const agentId = task.claimedBy || this.compose.from || this.selectedAgent;
|
|
1855
|
+
if (agentId) body.agentId = agentId;
|
|
1856
|
+
const result = await this.api("PATCH", "/tasks/" + task.id + "/status", body);
|
|
1857
|
+
const updated = result.task || result;
|
|
1858
|
+
const idx = this.tasks.findIndex((item) => item.id === task.id);
|
|
1859
|
+
if (idx >= 0) this.tasks[idx] = updated;
|
|
1860
|
+
this.recordOperatorActivity({
|
|
1861
|
+
title: "Task moved to " + status,
|
|
1862
|
+
body: updated.title || "Task #" + task.id,
|
|
1863
|
+
meta: agentId ? "by " + this.displayTarget(agentId) : "Work Queue",
|
|
1864
|
+
icon: "ti-arrows-exchange",
|
|
1865
|
+
kind: "task",
|
|
1866
|
+
view: "work",
|
|
1867
|
+
taskId: task.id,
|
|
1868
|
+
agentId,
|
|
1869
|
+
});
|
|
1870
|
+
} catch (e) {
|
|
1871
|
+
alert("Task status update failed: " + e.message);
|
|
1872
|
+
await this.fetchTasks();
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1259
1876
|
async function doDeleteMessage(id) {
|
|
1260
1877
|
try {
|
|
1261
1878
|
await this.api("DELETE", "/messages/" + id);
|
|
1262
1879
|
this.messages = this.messages.filter((msg) => msg.id !== id);
|
|
1880
|
+
this.recordOperatorActivity({
|
|
1881
|
+
title: "Deleted message",
|
|
1882
|
+
body: "Message #" + id,
|
|
1883
|
+
meta: "Messages",
|
|
1884
|
+
icon: "ti-trash",
|
|
1885
|
+
view: "messages",
|
|
1886
|
+
});
|
|
1263
1887
|
} catch (e) {
|
|
1264
1888
|
alert("Delete failed: " + e.message);
|
|
1265
1889
|
}
|
|
@@ -1280,11 +1904,270 @@
|
|
|
1280
1904
|
this.taskEventsOpen = true;
|
|
1281
1905
|
try {
|
|
1282
1906
|
this.taskEvents = await this.api("GET", "/tasks/" + task.id + "/events");
|
|
1907
|
+
this.taskEventCache = { ...this.taskEventCache, [task.id]: this.taskEvents };
|
|
1283
1908
|
} catch (e) {
|
|
1284
1909
|
alert("Failed to load task events: " + e.message);
|
|
1285
1910
|
}
|
|
1286
1911
|
}
|
|
1287
1912
|
|
|
1913
|
+
function recordOperatorActivity(input) {
|
|
1914
|
+
const item = activityItem({
|
|
1915
|
+
kind: "operator",
|
|
1916
|
+
ts: Date.now(),
|
|
1917
|
+
...input,
|
|
1918
|
+
});
|
|
1919
|
+
item.id = item.id || "operator-" + item.ts + "-" + (this.operatorActivity?.length || 0);
|
|
1920
|
+
item.clientId = item.clientId || item.id;
|
|
1921
|
+
this.operatorActivity = [
|
|
1922
|
+
item,
|
|
1923
|
+
...(this.operatorActivity || []).filter((existing) => existing.id !== item.id),
|
|
1924
|
+
].slice(0, 80);
|
|
1925
|
+
savePref("operatorActivity", this.operatorActivity);
|
|
1926
|
+
void saveActivityEvent(this, item);
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
async function saveActivityEvent(vm, item) {
|
|
1930
|
+
try {
|
|
1931
|
+
const event = await vm.api("POST", "/activity", {
|
|
1932
|
+
operatorId: INBOX_OPERATOR_ID,
|
|
1933
|
+
clientId: item.clientId,
|
|
1934
|
+
kind: item.kind,
|
|
1935
|
+
title: item.title,
|
|
1936
|
+
body: item.body || undefined,
|
|
1937
|
+
meta: item.meta || undefined,
|
|
1938
|
+
icon: item.icon || undefined,
|
|
1939
|
+
view: item.view || undefined,
|
|
1940
|
+
peer: item.peer || undefined,
|
|
1941
|
+
messageId: item.messageId,
|
|
1942
|
+
pairId: item.pairId,
|
|
1943
|
+
taskId: item.taskId,
|
|
1944
|
+
agentId: item.agentId,
|
|
1945
|
+
});
|
|
1946
|
+
const existing = new Set((vm.activityEvents || []).map((entry) => entry.id));
|
|
1947
|
+
vm.activityEvents = existing.has(event.id)
|
|
1948
|
+
? vm.activityEvents.map((entry) => entry.id === event.id ? event : entry)
|
|
1949
|
+
: [event, ...(vm.activityEvents || [])].slice(0, 200);
|
|
1950
|
+
pruneSyncedOperatorActivity(vm);
|
|
1951
|
+
} catch {}
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
async function openActivityItem(item) {
|
|
1955
|
+
if (!item) return;
|
|
1956
|
+
if (item.view) await this.switchView(item.view);
|
|
1957
|
+
if (item.peer) {
|
|
1958
|
+
const thread = this.allInboxThreads.find((candidate) => candidate.peer === item.peer);
|
|
1959
|
+
if (thread?.archived) this.inboxShowArchived = true;
|
|
1960
|
+
this.selectedInboxThread = item.peer;
|
|
1961
|
+
if (thread) this.markInboxThreadRead(thread);
|
|
1962
|
+
}
|
|
1963
|
+
if (item.agentId && this.agentsById[item.agentId]) this.openAgentDetail(this.agentsById[item.agentId]);
|
|
1964
|
+
if (item.taskId) {
|
|
1965
|
+
const task = this.tasks.find((candidate) => candidate.id === item.taskId);
|
|
1966
|
+
if (task) await this.openTaskEvents(task);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
async function runHealthAction(action) {
|
|
1971
|
+
if (!action) return;
|
|
1972
|
+
if (action.preset) {
|
|
1973
|
+
this.agentPresetFilter = action.preset;
|
|
1974
|
+
this.showOffline = true;
|
|
1975
|
+
}
|
|
1976
|
+
if (action.api && action.path) {
|
|
1977
|
+
await this.api(action.api, action.path);
|
|
1978
|
+
await this.refreshLiveData();
|
|
1979
|
+
}
|
|
1980
|
+
if (action.view) await this.switchView(action.view);
|
|
1981
|
+
if (action.copy) await copyText(action.copy);
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
async function copyText(value) {
|
|
1985
|
+
if (typeof navigator === "undefined") return;
|
|
1986
|
+
try {
|
|
1987
|
+
await navigator.clipboard?.writeText(value);
|
|
1988
|
+
} catch {}
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
function openCommandPalette() {
|
|
1992
|
+
this.commandQuery = "";
|
|
1993
|
+
this.commandPaletteOpen = true;
|
|
1994
|
+
this.$nextTick(() => this.$refs?.commandSearch?.focus());
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
function closeCommandPalette() {
|
|
1998
|
+
this.commandPaletteOpen = false;
|
|
1999
|
+
this.commandQuery = "";
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
async function runCommand(command) {
|
|
2003
|
+
if (!command) return;
|
|
2004
|
+
const payload = command.payload || {};
|
|
2005
|
+
this.closeCommandPalette();
|
|
2006
|
+
if (command.action === "openView") {
|
|
2007
|
+
await this.switchView(payload.view);
|
|
2008
|
+
} else if (command.action === "agentPreset") {
|
|
2009
|
+
this.showOffline = true;
|
|
2010
|
+
this.agentPresetFilter = payload.preset || "";
|
|
2011
|
+
await this.switchView("agents");
|
|
2012
|
+
} else if (command.action === "copy") {
|
|
2013
|
+
await copyText(payload.value || "");
|
|
2014
|
+
} else if (command.action === "messageAgent") {
|
|
2015
|
+
const agent = this.agentsById[payload.agentId];
|
|
2016
|
+
if (agent) this.openComposeToAgent(agent);
|
|
2017
|
+
} else if (command.action === "pairAgent") {
|
|
2018
|
+
this.openPairInvite(payload.agentId);
|
|
2019
|
+
} else if (command.action === "filterTag") {
|
|
2020
|
+
this.agentTagFilter = payload.tag || "";
|
|
2021
|
+
this.tagFilter = payload.tag || "";
|
|
2022
|
+
await this.switchView("agents");
|
|
2023
|
+
} else if (command.action === "exportActivity") {
|
|
2024
|
+
this.exportActivity(payload.format || "markdown");
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
function exportActivity(format) {
|
|
2029
|
+
exportDocument(this, "timeline", format, {
|
|
2030
|
+
title: "Agent Relay Timeline",
|
|
2031
|
+
items: this.activityItems,
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
function exportThread(thread, format) {
|
|
2036
|
+
if (!thread) return;
|
|
2037
|
+
exportDocument(this, "thread-" + safeFilename(thread.peer), format, {
|
|
2038
|
+
title: "Thread: " + this.conversationTitle(thread),
|
|
2039
|
+
thread,
|
|
2040
|
+
messages: thread.messages || [],
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
function exportPair(pair, format) {
|
|
2045
|
+
if (!pair) return;
|
|
2046
|
+
exportDocument(this, "pair-" + safeFilename(pair.id), format, {
|
|
2047
|
+
title: "Pair: " + pair.id,
|
|
2048
|
+
pair,
|
|
2049
|
+
messages: pairMessages(this, pair),
|
|
2050
|
+
});
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
async function exportTask(task, format) {
|
|
2054
|
+
if (!task) return;
|
|
2055
|
+
let events = this.taskEventCache[task.id] || [];
|
|
2056
|
+
if (!events.length) {
|
|
2057
|
+
try {
|
|
2058
|
+
events = await this.api("GET", "/tasks/" + task.id + "/events");
|
|
2059
|
+
this.taskEventCache = { ...this.taskEventCache, [task.id]: events };
|
|
2060
|
+
} catch {
|
|
2061
|
+
events = [];
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
exportDocument(this, "task-" + task.id, format, {
|
|
2065
|
+
title: "Task: " + (task.title || "#" + task.id),
|
|
2066
|
+
task,
|
|
2067
|
+
events,
|
|
2068
|
+
});
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
function pairMessages(vm, pair) {
|
|
2072
|
+
return (vm.messages || []).filter((msg) =>
|
|
2073
|
+
msg.meta?.pairId === pair.id ||
|
|
2074
|
+
(msg.meta?.pairEvent && [pair.requesterId, pair.targetId].includes(msg.from) && [pair.requesterId, pair.targetId].includes(msg.to))
|
|
2075
|
+
);
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
function exportDocument(vm, scope, format, data) {
|
|
2079
|
+
const normalizedFormat = format === "json" ? "json" : "markdown";
|
|
2080
|
+
const filename = `agent-relay-${scope}-${new Date().toISOString().slice(0, 10)}.${normalizedFormat === "json" ? "json" : "md"}`;
|
|
2081
|
+
const text = normalizedFormat === "json" ? JSON.stringify(exportJson(data), null, 2) : exportMarkdown(vm, data);
|
|
2082
|
+
downloadText(filename, text, normalizedFormat === "json" ? "application/json" : "text/markdown");
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
function exportJson(data) {
|
|
2086
|
+
return {
|
|
2087
|
+
exportedAt: new Date().toISOString(),
|
|
2088
|
+
...data,
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
function exportMarkdown(vm, data) {
|
|
2093
|
+
const lines = ["# " + data.title, "", "Exported: " + new Date().toISOString(), ""];
|
|
2094
|
+
if (data.thread) {
|
|
2095
|
+
lines.push("## Messages", "");
|
|
2096
|
+
appendMessages(lines, vm, data.messages || []);
|
|
2097
|
+
} else if (data.pair) {
|
|
2098
|
+
lines.push("## Pair", "", "- ID: " + data.pair.id, "- Status: " + data.pair.status, "- Requester: " + vm.displayTarget(data.pair.requesterId), "- Target: " + vm.displayTarget(data.pair.targetId));
|
|
2099
|
+
if (data.pair.objective) lines.push("- Objective: " + data.pair.objective);
|
|
2100
|
+
lines.push("", "## Messages", "");
|
|
2101
|
+
appendMessages(lines, vm, data.messages || []);
|
|
2102
|
+
} else if (data.task) {
|
|
2103
|
+
lines.push("## Task", "", "- ID: " + data.task.id, "- Status: " + data.task.status, "- Severity: " + (data.task.severity || "info"), "- Target: " + vm.displayTarget(data.task.target || ""));
|
|
2104
|
+
if (data.task.claimedBy) lines.push("- Claimed by: " + vm.displayTarget(data.task.claimedBy));
|
|
2105
|
+
if (data.task.title) lines.push("- Title: " + data.task.title);
|
|
2106
|
+
if (data.task.body) lines.push("", data.task.body);
|
|
2107
|
+
lines.push("", "## History", "");
|
|
2108
|
+
appendEvents(lines, vm, data.events || []);
|
|
2109
|
+
} else {
|
|
2110
|
+
lines.push("## Events", "");
|
|
2111
|
+
appendActivity(lines, vm, data.items || []);
|
|
2112
|
+
}
|
|
2113
|
+
return lines.join("\n").trim() + "\n";
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
function appendMessages(lines, vm, messages) {
|
|
2117
|
+
if (!messages.length) {
|
|
2118
|
+
lines.push("_No messages loaded._", "");
|
|
2119
|
+
return;
|
|
2120
|
+
}
|
|
2121
|
+
for (const msg of messages) {
|
|
2122
|
+
lines.push(`### #${msg.id} ${vm.displayTarget(msg.from)} -> ${vm.displayTarget(msg.to)}`, "");
|
|
2123
|
+
if (msg.createdAt) lines.push("- Created: " + msg.createdAt);
|
|
2124
|
+
if (msg.channel) lines.push("- Channel: " + msg.channel);
|
|
2125
|
+
if (msg.subject) lines.push("- Subject: " + msg.subject);
|
|
2126
|
+
if (msg.claimedBy) lines.push("- Claimed by: " + vm.displayTarget(msg.claimedBy));
|
|
2127
|
+
lines.push("", msg.body || "", "");
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
function appendEvents(lines, vm, events) {
|
|
2132
|
+
if (!events.length) {
|
|
2133
|
+
lines.push("_No task events loaded._", "");
|
|
2134
|
+
return;
|
|
2135
|
+
}
|
|
2136
|
+
for (const event of events) {
|
|
2137
|
+
lines.push(`- ${event.createdAt || ""} [${event.severity || "info"}] ${event.type || "event"}: ${event.title || event.body || ""}`.trim());
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
function appendActivity(lines, vm, items) {
|
|
2142
|
+
if (!items.length) {
|
|
2143
|
+
lines.push("_No activity loaded._", "");
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
2146
|
+
for (const item of items) {
|
|
2147
|
+
const when = item.ts ? new Date(item.ts).toISOString() : "";
|
|
2148
|
+
const meta = item.meta ? " - " + item.meta : "";
|
|
2149
|
+
lines.push(`- ${when} [${item.kind}] ${item.title}${meta}`);
|
|
2150
|
+
if (item.body) lines.push(" " + item.body.replace(/\n/g, "\n "));
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
function downloadText(filename, text, type) {
|
|
2155
|
+
if (typeof document === "undefined" || typeof URL === "undefined" || typeof Blob === "undefined") {
|
|
2156
|
+
void copyText(text);
|
|
2157
|
+
return;
|
|
2158
|
+
}
|
|
2159
|
+
const url = URL.createObjectURL(new Blob([text], { type }));
|
|
2160
|
+
const link = document.createElement("a");
|
|
2161
|
+
link.href = url;
|
|
2162
|
+
link.download = filename;
|
|
2163
|
+
link.click();
|
|
2164
|
+
URL.revokeObjectURL(url);
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
function safeFilename(value) {
|
|
2168
|
+
return String(value || "export").replace(/[^a-z0-9._-]+/gi, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "export";
|
|
2169
|
+
}
|
|
2170
|
+
|
|
1288
2171
|
function openPairMessage(pair, fromId) {
|
|
1289
2172
|
if (!pair) return;
|
|
1290
2173
|
this.pairMessage = {
|
|
@@ -1322,11 +2205,19 @@
|
|
|
1322
2205
|
|
|
1323
2206
|
try {
|
|
1324
2207
|
const payload = {
|
|
1325
|
-
|
|
1326
|
-
|
|
2208
|
+
from: this.pairInvite.requesterId,
|
|
2209
|
+
target: this.pairInvite.targetId,
|
|
1327
2210
|
};
|
|
1328
2211
|
if (this.pairInvite.objective) payload.objective = this.pairInvite.objective;
|
|
1329
2212
|
await this.api("POST", "/pairs", payload);
|
|
2213
|
+
this.recordOperatorActivity({
|
|
2214
|
+
title: "Pair invite sent",
|
|
2215
|
+
body: payload.objective || "",
|
|
2216
|
+
meta: `${this.displayTarget(payload.from)} <-> ${this.displayTarget(payload.target)}`,
|
|
2217
|
+
icon: "ti-link-plus",
|
|
2218
|
+
kind: "pair",
|
|
2219
|
+
view: "pairs",
|
|
2220
|
+
});
|
|
1330
2221
|
this.closePairInvite();
|
|
1331
2222
|
await Promise.all([this.fetchPairs(), this.fetchMessages()]);
|
|
1332
2223
|
} catch (e) {
|
|
@@ -1349,6 +2240,14 @@
|
|
|
1349
2240
|
const payload = { from: this.pairMessage.from, body: this.pairMessage.body };
|
|
1350
2241
|
if (this.pairMessage.subject) payload.subject = this.pairMessage.subject;
|
|
1351
2242
|
await this.api("POST", "/pairs/" + encodeURIComponent(this.pairMessage.pairId) + "/messages", payload);
|
|
2243
|
+
this.recordOperatorActivity({
|
|
2244
|
+
title: "Pair message sent",
|
|
2245
|
+
body: payload.subject || payload.body,
|
|
2246
|
+
meta: "from " + this.displayTarget(payload.from),
|
|
2247
|
+
icon: "ti-messages",
|
|
2248
|
+
kind: "pair",
|
|
2249
|
+
view: "pairs",
|
|
2250
|
+
});
|
|
1352
2251
|
this.closePairMessage();
|
|
1353
2252
|
await Promise.all([this.fetchPairs(), this.fetchMessages()]);
|
|
1354
2253
|
} catch (e) {
|
|
@@ -1360,6 +2259,14 @@
|
|
|
1360
2259
|
if (!pair) return;
|
|
1361
2260
|
try {
|
|
1362
2261
|
await this.api("POST", "/pairs/" + encodeURIComponent(pair.id) + "/accept", { agentId: pair.targetId });
|
|
2262
|
+
this.recordOperatorActivity({
|
|
2263
|
+
title: "Pair accepted",
|
|
2264
|
+
body: pair.objective || "",
|
|
2265
|
+
meta: `${this.displayTarget(pair.requesterId)} <-> ${this.displayTarget(pair.targetId)}`,
|
|
2266
|
+
icon: "ti-check",
|
|
2267
|
+
kind: "pair",
|
|
2268
|
+
view: "pairs",
|
|
2269
|
+
});
|
|
1363
2270
|
await Promise.all([this.fetchPairs(), this.fetchMessages()]);
|
|
1364
2271
|
} catch (e) {
|
|
1365
2272
|
alert("Accept failed: " + e.message);
|
|
@@ -1370,6 +2277,14 @@
|
|
|
1370
2277
|
if (!pair) return;
|
|
1371
2278
|
try {
|
|
1372
2279
|
await this.api("POST", "/pairs/" + encodeURIComponent(pair.id) + "/reject", { agentId: pair.targetId });
|
|
2280
|
+
this.recordOperatorActivity({
|
|
2281
|
+
title: "Pair rejected",
|
|
2282
|
+
body: pair.objective || "",
|
|
2283
|
+
meta: `${this.displayTarget(pair.requesterId)} <-> ${this.displayTarget(pair.targetId)}`,
|
|
2284
|
+
icon: "ti-x",
|
|
2285
|
+
kind: "pair",
|
|
2286
|
+
view: "pairs",
|
|
2287
|
+
});
|
|
1373
2288
|
await Promise.all([this.fetchPairs(), this.fetchMessages()]);
|
|
1374
2289
|
} catch (e) {
|
|
1375
2290
|
alert("Reject failed: " + e.message);
|
|
@@ -1380,6 +2295,14 @@
|
|
|
1380
2295
|
if (!pair) return;
|
|
1381
2296
|
try {
|
|
1382
2297
|
await this.api("POST", "/pairs/" + encodeURIComponent(pair.id) + "/hangup", { agentId: agentId || pair.requesterId });
|
|
2298
|
+
this.recordOperatorActivity({
|
|
2299
|
+
title: "Pair hung up",
|
|
2300
|
+
body: pair.objective || "",
|
|
2301
|
+
meta: "by " + this.displayTarget(agentId || pair.requesterId),
|
|
2302
|
+
icon: "ti-phone-off",
|
|
2303
|
+
kind: "pair",
|
|
2304
|
+
view: "pairs",
|
|
2305
|
+
});
|
|
1383
2306
|
await Promise.all([this.fetchPairs(), this.fetchMessages()]);
|
|
1384
2307
|
} catch (e) {
|
|
1385
2308
|
alert("Hang up failed: " + e.message);
|
|
@@ -1556,7 +2479,7 @@
|
|
|
1556
2479
|
|
|
1557
2480
|
window.AgentRelayDashboard = {
|
|
1558
2481
|
createRelayDashboard,
|
|
1559
|
-
helpers: { loadPref, savePref, indexAgents, upsertById, upsertTask, compareAgents, agentType },
|
|
2482
|
+
helpers: { loadPref, savePref, indexAgents, upsertById, upsertTask, compareAgents, agentType, isBuiltInAgent },
|
|
1560
2483
|
};
|
|
1561
2484
|
window.relay = createRelayDashboard;
|
|
1562
2485
|
|