agent-relay-server 0.4.22 → 0.4.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/public/dashboard.js +1661 -17
- package/public/index.html +1026 -32
- package/src/db.ts +252 -1
- package/src/routes.ts +336 -3
- package/src/security.ts +3 -1
- package/src/types.ts +61 -0
package/public/dashboard.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
(() => {
|
|
2
2
|
const PREF_PREFIX = "ar-";
|
|
3
|
+
const HUMAN_AGENT_ID = "user";
|
|
4
|
+
const INBOX_OPERATOR_ID = HUMAN_AGENT_ID;
|
|
3
5
|
const DEFAULT_COMPOSE = { from: "", to: "", body: "", channel: "", subject: "", claimable: false };
|
|
6
|
+
const DEFAULT_INBOX_COMPOSE = { toMode: "agent", to: "", body: "", channel: "", subject: "", claimable: false };
|
|
7
|
+
const DEFAULT_PAIR_MESSAGE = { pairId: "", from: "", body: "", subject: "" };
|
|
8
|
+
const DEFAULT_PAIR_INVITE = { requesterId: "", targetId: "", objective: "" };
|
|
4
9
|
const CLOSED_TASK_STATUSES = new Set(["done", "failed", "canceled"]);
|
|
10
|
+
const WAITING_TASK_STATUSES = new Set(["open", "blocked"]);
|
|
5
11
|
const STATUS_SORT_ORDER = { online: 0, idle: 1, busy: 2, offline: 3 };
|
|
6
12
|
const LIVE_REFRESH_MS = 5_000;
|
|
7
13
|
const AGENT_TYPE_ICONS = {
|
|
@@ -33,30 +39,52 @@
|
|
|
33
39
|
view: loadPref("view", "overview"),
|
|
34
40
|
|
|
35
41
|
showOffline: loadPref("showOffline", false),
|
|
42
|
+
showBuiltIns: loadPref("showBuiltIns", false),
|
|
36
43
|
autoRefresh: loadPref("autoRefresh", true),
|
|
37
44
|
agentSort: loadPref("agentSort", "status"),
|
|
38
45
|
agentSortDir: loadPref("agentSortDir", "asc"),
|
|
46
|
+
agentPresetFilter: loadPref("agentPresetFilter", ""),
|
|
39
47
|
|
|
40
48
|
agents: [],
|
|
41
49
|
agentsById: {},
|
|
50
|
+
pairs: [],
|
|
42
51
|
messages: [],
|
|
43
52
|
tasks: [],
|
|
44
53
|
taskEvents: [],
|
|
54
|
+
taskEventCache: {},
|
|
45
55
|
stats: {},
|
|
46
56
|
health: null,
|
|
47
57
|
now: Date.now(),
|
|
48
58
|
authToken: loadPref("authToken", ""),
|
|
59
|
+
inboxReadCursors: loadPref("inboxReadCursors", {}),
|
|
60
|
+
inboxArchivedThreads: loadPref("inboxArchivedThreads", {}),
|
|
61
|
+
inboxDrafts: loadPref("inboxDrafts", {}),
|
|
62
|
+
inboxSearch: "",
|
|
63
|
+
inboxShowArchived: loadPref("inboxShowArchived", false),
|
|
64
|
+
operatorActivity: loadPref("operatorActivity", []),
|
|
65
|
+
activityEvents: [],
|
|
66
|
+
activityFilter: loadPref("activityFilter", ""),
|
|
49
67
|
|
|
50
68
|
selectedAgent: "",
|
|
69
|
+
agentDetailOpen: false,
|
|
70
|
+
agentDetailId: "",
|
|
71
|
+
selectedInboxThread: "",
|
|
51
72
|
replyTo: null,
|
|
52
73
|
composeOpen: false,
|
|
74
|
+
pairInviteOpen: false,
|
|
75
|
+
pairMessageOpen: false,
|
|
53
76
|
threadOpen: false,
|
|
54
77
|
threadMessages: [],
|
|
55
78
|
taskEventsOpen: false,
|
|
79
|
+
commandPaletteOpen: false,
|
|
80
|
+
commandQuery: "",
|
|
56
81
|
connected: false,
|
|
57
82
|
authNeeded: false,
|
|
58
83
|
|
|
59
84
|
compose: { ...DEFAULT_COMPOSE },
|
|
85
|
+
pairInvite: { ...DEFAULT_PAIR_INVITE },
|
|
86
|
+
pairMessage: { ...DEFAULT_PAIR_MESSAGE },
|
|
87
|
+
inboxCompose: { ...DEFAULT_INBOX_COMPOSE },
|
|
60
88
|
|
|
61
89
|
confirmModal: { show: false, title: "", message: "", action: null },
|
|
62
90
|
renameModal: { show: false, agentId: "", label: "" },
|
|
@@ -65,6 +93,7 @@
|
|
|
65
93
|
tagFilter: "",
|
|
66
94
|
agentStatusFilter: loadPref("agentStatusFilter", ""),
|
|
67
95
|
agentTagFilter: loadPref("agentTagFilter", ""),
|
|
96
|
+
pairStatusFilter: loadPref("pairStatusFilter", "open"),
|
|
68
97
|
taskStatusFilter: "",
|
|
69
98
|
taskSourceFilter: "",
|
|
70
99
|
|
|
@@ -78,6 +107,7 @@
|
|
|
78
107
|
|
|
79
108
|
function watchPersistedPrefs(vm) {
|
|
80
109
|
vm.$watch("showOffline", (value) => vm.save("showOffline", value));
|
|
110
|
+
vm.$watch("showBuiltIns", (value) => vm.save("showBuiltIns", value));
|
|
81
111
|
vm.$watch("autoRefresh", (value) => {
|
|
82
112
|
vm.save("autoRefresh", value);
|
|
83
113
|
if (value) vm.startAutoRefresh();
|
|
@@ -85,8 +115,12 @@
|
|
|
85
115
|
});
|
|
86
116
|
vm.$watch("agentSort", (value) => vm.save("agentSort", value));
|
|
87
117
|
vm.$watch("agentSortDir", (value) => vm.save("agentSortDir", value));
|
|
118
|
+
vm.$watch("agentPresetFilter", (value) => vm.save("agentPresetFilter", value));
|
|
88
119
|
vm.$watch("agentStatusFilter", (value) => vm.save("agentStatusFilter", value));
|
|
89
120
|
vm.$watch("agentTagFilter", (value) => vm.save("agentTagFilter", value));
|
|
121
|
+
vm.$watch("pairStatusFilter", (value) => vm.save("pairStatusFilter", value));
|
|
122
|
+
vm.$watch("inboxShowArchived", (value) => vm.save("inboxShowArchived", value));
|
|
123
|
+
vm.$watch("activityFilter", (value) => vm.save("activityFilter", value));
|
|
90
124
|
vm.$watch("view", (value) => {
|
|
91
125
|
vm.save("view", value);
|
|
92
126
|
if (value === "analytics") vm.$nextTick(() => vm.renderCharts());
|
|
@@ -133,11 +167,27 @@
|
|
|
133
167
|
vm.$nextTick(() => vm.renderCharts());
|
|
134
168
|
}
|
|
135
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
|
+
|
|
136
185
|
function createLifecycleMethods() {
|
|
137
186
|
return {
|
|
138
187
|
async init() {
|
|
139
188
|
this.startClock();
|
|
140
189
|
watchPersistedPrefs(this);
|
|
190
|
+
registerKeyboardShortcuts(this);
|
|
141
191
|
|
|
142
192
|
try {
|
|
143
193
|
this.stats = await this.api("GET", "/stats");
|
|
@@ -154,9 +204,13 @@
|
|
|
154
204
|
savePref(key, value);
|
|
155
205
|
},
|
|
156
206
|
|
|
157
|
-
switchView(view) {
|
|
207
|
+
async switchView(view) {
|
|
158
208
|
this.view = view;
|
|
159
|
-
if (view === "messages") this.fetchMessages();
|
|
209
|
+
if (view === "inbox" || view === "messages") await this.fetchMessages();
|
|
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()]);
|
|
213
|
+
if (view === "pairs") this.fetchPairs();
|
|
160
214
|
if (view === "tasks") this.fetchTasks();
|
|
161
215
|
},
|
|
162
216
|
|
|
@@ -222,8 +276,8 @@
|
|
|
222
276
|
|
|
223
277
|
function handleNewMessage(vm, msg) {
|
|
224
278
|
if (vm.messages.some((existing) => existing.id === msg.id)) return;
|
|
225
|
-
if (vm.selectedAgent && msg.from !== vm.selectedAgent && msg.to !== vm.selectedAgent) return;
|
|
226
|
-
if (vm.channelFilter && msg.channel !== vm.channelFilter) return;
|
|
279
|
+
if (vm.view === "messages" && vm.selectedAgent && msg.from !== vm.selectedAgent && msg.to !== vm.selectedAgent) return;
|
|
280
|
+
if (vm.view === "messages" && vm.channelFilter && msg.channel !== vm.channelFilter) return;
|
|
227
281
|
|
|
228
282
|
vm.messages.push(msg);
|
|
229
283
|
if (vm.messages.length > 200) vm.messages.shift();
|
|
@@ -299,7 +353,7 @@
|
|
|
299
353
|
},
|
|
300
354
|
|
|
301
355
|
async refresh() {
|
|
302
|
-
await Promise.all([this.fetchStats(), this.fetchHealth(), this.fetchAgents(), this.fetchMessages(), this.fetchTasks()]);
|
|
356
|
+
await Promise.all([this.fetchStats(), this.fetchHealth(), this.fetchAgents(), this.fetchPairs(), this.fetchMessages(), this.fetchTasks(), this.fetchInboxState(), this.fetchActivityEvents()]);
|
|
303
357
|
},
|
|
304
358
|
|
|
305
359
|
async refreshLiveData() {
|
|
@@ -332,11 +386,31 @@
|
|
|
332
386
|
} catch {}
|
|
333
387
|
},
|
|
334
388
|
|
|
389
|
+
async fetchPairs() {
|
|
390
|
+
try {
|
|
391
|
+
let pairs;
|
|
392
|
+
if (this.view === "activity") {
|
|
393
|
+
pairs = await this.api("GET", "/pairs");
|
|
394
|
+
} else if (this.pairStatusFilter === "open") {
|
|
395
|
+
const [active, pending] = await Promise.all([
|
|
396
|
+
this.api("GET", "/pairs?status=active"),
|
|
397
|
+
this.api("GET", "/pairs?status=pending"),
|
|
398
|
+
]);
|
|
399
|
+
pairs = [...active, ...pending];
|
|
400
|
+
} else if (this.pairStatusFilter) {
|
|
401
|
+
pairs = await this.api("GET", "/pairs?status=" + encodeURIComponent(this.pairStatusFilter));
|
|
402
|
+
} else {
|
|
403
|
+
pairs = await this.api("GET", "/pairs");
|
|
404
|
+
}
|
|
405
|
+
this.pairs = pairs.sort(comparePairs);
|
|
406
|
+
} catch {}
|
|
407
|
+
},
|
|
408
|
+
|
|
335
409
|
async fetchMessages() {
|
|
336
410
|
try {
|
|
337
411
|
let path = "/messages?limit=100";
|
|
338
|
-
if (this.selectedAgent) path += "&for=" + encodeURIComponent(this.selectedAgent);
|
|
339
|
-
if (this.channelFilter) path += "&channel=" + encodeURIComponent(this.channelFilter);
|
|
412
|
+
if (this.view === "messages" && this.selectedAgent) path += "&for=" + encodeURIComponent(this.selectedAgent);
|
|
413
|
+
if (this.view === "messages" && this.channelFilter) path += "&channel=" + encodeURIComponent(this.channelFilter);
|
|
340
414
|
this.messages = await this.api("GET", path);
|
|
341
415
|
} catch {}
|
|
342
416
|
},
|
|
@@ -349,13 +423,69 @@
|
|
|
349
423
|
this.tasks = await this.api("GET", "/tasks?" + params.toString());
|
|
350
424
|
} catch {}
|
|
351
425
|
},
|
|
426
|
+
|
|
427
|
+
async fetchInboxState() {
|
|
428
|
+
try {
|
|
429
|
+
const state = await this.api("GET", "/inbox/state?operatorId=" + encodeURIComponent(INBOX_OPERATOR_ID));
|
|
430
|
+
applyInboxState(this, state);
|
|
431
|
+
} catch {}
|
|
432
|
+
},
|
|
433
|
+
|
|
434
|
+
async fetchActivityEvents() {
|
|
435
|
+
try {
|
|
436
|
+
this.activityEvents = await this.api("GET", "/activity?limit=200");
|
|
437
|
+
pruneSyncedOperatorActivity(this);
|
|
438
|
+
} catch {}
|
|
439
|
+
},
|
|
352
440
|
};
|
|
353
441
|
}
|
|
354
442
|
|
|
443
|
+
function applyInboxState(vm, state) {
|
|
444
|
+
const readCursors = {};
|
|
445
|
+
const archivedThreads = {};
|
|
446
|
+
const drafts = {};
|
|
447
|
+
|
|
448
|
+
for (const thread of state?.threads || []) {
|
|
449
|
+
if (thread.readCursorMessageId) readCursors[thread.peerId] = thread.readCursorMessageId;
|
|
450
|
+
if (thread.archivedAtMessageId) archivedThreads[thread.peerId] = thread.archivedAtMessageId;
|
|
451
|
+
}
|
|
452
|
+
for (const draft of state?.drafts || []) {
|
|
453
|
+
if (draft.body) drafts[draft.peerId] = draft.body;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
vm.inboxReadCursors = readCursors;
|
|
457
|
+
vm.inboxArchivedThreads = archivedThreads;
|
|
458
|
+
vm.inboxDrafts = drafts;
|
|
459
|
+
savePref("inboxReadCursors", readCursors);
|
|
460
|
+
savePref("inboxArchivedThreads", archivedThreads);
|
|
461
|
+
savePref("inboxDrafts", drafts);
|
|
462
|
+
}
|
|
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
|
+
|
|
355
471
|
function createComputedDescriptors() {
|
|
356
472
|
return {
|
|
357
473
|
onlineCount: { get: getOnlineCount },
|
|
474
|
+
hiddenBuiltInAgentCount: { get: getHiddenBuiltInAgentCount },
|
|
358
475
|
sortedAgents: { get: getSortedAgents },
|
|
476
|
+
pairsByAgentId: { get: getPairsByAgentId },
|
|
477
|
+
selectedAgentDetail: { get: getSelectedAgentDetail },
|
|
478
|
+
agentDetailMessages: { get: getAgentDetailMessages },
|
|
479
|
+
pairMessagePair: { get: getPairMessagePair },
|
|
480
|
+
allInboxThreads: { get: getAllInboxThreads },
|
|
481
|
+
inboxThreads: { get: getInboxThreads },
|
|
482
|
+
selectedInboxThreadData: { get: getSelectedInboxThreadData },
|
|
483
|
+
selectedInboxMessages: { get: getSelectedInboxMessages },
|
|
484
|
+
inboxComposeTargetOptions: { get: getInboxComposeTargetOptions },
|
|
485
|
+
attentionSummary: { get: getAttentionSummary },
|
|
486
|
+
attentionAgentCount: { get: getAttentionAgentCount },
|
|
487
|
+
activityItems: { get: getActivityItems },
|
|
488
|
+
workQueueItems: { get: getWorkQueueItems },
|
|
359
489
|
filteredMessages: { get: getFilteredMessages },
|
|
360
490
|
groupedMessages: { get: getGroupedMessages },
|
|
361
491
|
filteredTasks: { get: getFilteredTasks },
|
|
@@ -364,15 +494,23 @@
|
|
|
364
494
|
uniqueCaps: { get: getUniqueCaps },
|
|
365
495
|
uniqueTags: { get: getUniqueTags },
|
|
366
496
|
healthIssues: { get: getHealthIssues },
|
|
497
|
+
healthDiagnostics: { get: getHealthDiagnostics },
|
|
498
|
+
commandPaletteItems: { get: getCommandPaletteItems },
|
|
367
499
|
};
|
|
368
500
|
}
|
|
369
501
|
|
|
370
502
|
function getOnlineCount() {
|
|
371
|
-
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;
|
|
372
508
|
}
|
|
373
509
|
|
|
374
510
|
function getSortedAgents() {
|
|
375
|
-
let list = this
|
|
511
|
+
let list = visibleAgents(this);
|
|
512
|
+
list = applyAgentPreset(this, list);
|
|
513
|
+
if (!this.showOffline) list = list.filter((agent) => agent.status !== "offline");
|
|
376
514
|
if (this.agentStatusFilter === "starting") {
|
|
377
515
|
list = list.filter((agent) => agent.status !== "offline" && !agent.ready);
|
|
378
516
|
} else if (this.agentStatusFilter) {
|
|
@@ -386,6 +524,448 @@
|
|
|
386
524
|
return list.sort((a, b) => compareAgents(this, a, b) * dir);
|
|
387
525
|
}
|
|
388
526
|
|
|
527
|
+
function getPairsByAgentId() {
|
|
528
|
+
const byAgent = {};
|
|
529
|
+
for (const pair of this.pairs || []) {
|
|
530
|
+
if (pair.requesterId) byAgent[pair.requesterId] = pair;
|
|
531
|
+
if (pair.targetId) byAgent[pair.targetId] = pair;
|
|
532
|
+
}
|
|
533
|
+
return byAgent;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function comparePairs(a, b) {
|
|
537
|
+
return new Date(b.updatedAt || b.createdAt || 0) - new Date(a.updatedAt || a.createdAt || 0);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function getSelectedAgentDetail() {
|
|
541
|
+
if (!this.agentDetailId) return null;
|
|
542
|
+
return this.agentsById[this.agentDetailId] || null;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function getAgentDetailMessages() {
|
|
546
|
+
if (!this.agentDetailId) return [];
|
|
547
|
+
return this.messages
|
|
548
|
+
.filter((msg) => msg.from === this.agentDetailId || msg.to === this.agentDetailId)
|
|
549
|
+
.slice()
|
|
550
|
+
.sort((a, b) => b.id - a.id)
|
|
551
|
+
.slice(0, 8);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function getPairMessagePair() {
|
|
555
|
+
return this.pairs.find((pair) => pair.id === this.pairMessage.pairId) || null;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function getAllInboxThreads() {
|
|
559
|
+
return buildInboxThreads(this);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function getInboxThreads() {
|
|
563
|
+
const search = this.inboxSearch.trim().toLowerCase();
|
|
564
|
+
return this.allInboxThreads.filter((thread) => {
|
|
565
|
+
if (!this.inboxShowArchived && thread.archived) return false;
|
|
566
|
+
if (search && !threadMatchesSearch(this, thread, search)) return false;
|
|
567
|
+
return true;
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function buildInboxThreads(vm) {
|
|
572
|
+
const threads = new Map();
|
|
573
|
+
for (const msg of vm.messages) {
|
|
574
|
+
const peer = inboxPeer(msg);
|
|
575
|
+
if (!peer) continue;
|
|
576
|
+
if (!threads.has(peer)) threads.set(peer, { id: peer, peer, messages: [], lastMessage: null });
|
|
577
|
+
threads.get(peer).messages.push(msg);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
for (const thread of threads.values()) {
|
|
581
|
+
thread.messages.sort((a, b) => a.id - b.id);
|
|
582
|
+
thread.lastMessage = thread.messages[thread.messages.length - 1] || null;
|
|
583
|
+
thread.attention = getThreadAttention(vm, thread);
|
|
584
|
+
thread.archived = isInboxThreadArchived(vm, thread);
|
|
585
|
+
thread.draft = draftForPeer(vm, thread.peer);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return [...threads.values()].sort(compareInboxThreads);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function threadMatchesSearch(vm, thread, search) {
|
|
592
|
+
const haystack = [
|
|
593
|
+
vm.displayTarget(thread.peer),
|
|
594
|
+
thread.peer,
|
|
595
|
+
...thread.messages.flatMap((msg) => [msg.subject || "", msg.body || "", msg.channel || "", vm.displayTarget(msg.from), vm.displayTarget(msg.to)]),
|
|
596
|
+
].join("\n").toLowerCase();
|
|
597
|
+
return haystack.includes(search);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function isInboxThreadArchived(vm, thread) {
|
|
601
|
+
const archivedAtId = Number(vm.inboxArchivedThreads?.[thread.peer] || 0);
|
|
602
|
+
return Boolean(thread.lastMessage?.id && archivedAtId >= thread.lastMessage.id);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function compareInboxThreads(a, b) {
|
|
606
|
+
const scoreDelta = (b.attention?.score || 0) - (a.attention?.score || 0);
|
|
607
|
+
if (scoreDelta !== 0) return scoreDelta;
|
|
608
|
+
return (b.lastMessage?.id || 0) - (a.lastMessage?.id || 0);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function getSelectedInboxThreadData() {
|
|
612
|
+
if (!this.selectedInboxThread) return null;
|
|
613
|
+
return this.inboxThreads.find((thread) => thread.id === this.selectedInboxThread) || null;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function getSelectedInboxMessages() {
|
|
617
|
+
return this.selectedInboxThreadData?.messages || [];
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function getInboxComposeTargetOptions() {
|
|
621
|
+
if (this.inboxCompose.toMode === "tag") return this.uniqueTags.map((value) => ({ value, label: "#" + value }));
|
|
622
|
+
if (this.inboxCompose.toMode === "cap") return this.uniqueCaps.map((value) => ({ value, label: value }));
|
|
623
|
+
return this.composeAgents.map((agent) => ({ value: agent.id, label: `${this.displayName(agent)} [${agent.id.slice(-6)}]` }));
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function getAttentionSummary() {
|
|
627
|
+
const threads = this.allInboxThreads.filter((thread) => !thread.archived);
|
|
628
|
+
const pendingPairInvites = this.pairs.filter((pair) => pair.status === "pending").length;
|
|
629
|
+
const claimableTasks = countClaimableWaiting(this);
|
|
630
|
+
const unreadInbox = threads.reduce((sum, thread) => sum + (thread.attention?.unread || 0), 0);
|
|
631
|
+
const needsHumanResponse = threads.filter((thread) => thread.attention?.needsHumanResponse).length;
|
|
632
|
+
const agentQuestions = threads.filter((thread) => thread.attention?.agentQuestion).length;
|
|
633
|
+
|
|
634
|
+
return {
|
|
635
|
+
unreadInbox,
|
|
636
|
+
needsHumanResponse,
|
|
637
|
+
agentQuestions,
|
|
638
|
+
pendingPairInvites,
|
|
639
|
+
claimableTasks,
|
|
640
|
+
total: unreadInbox + needsHumanResponse + agentQuestions + pendingPairInvites + claimableTasks,
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function getAttentionAgentCount() {
|
|
645
|
+
return this.sortedAgents.filter((agent) => agentAttention.call(this, agent).total > 0).length;
|
|
646
|
+
}
|
|
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
|
+
|
|
886
|
+
function inboxPeer(msg) {
|
|
887
|
+
if (msg.from === HUMAN_AGENT_ID && msg.to) return msg.to;
|
|
888
|
+
if (msg.to === HUMAN_AGENT_ID && msg.from) return msg.from;
|
|
889
|
+
return "";
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function getThreadAttention(vm, thread) {
|
|
893
|
+
const lastHumanReplyId = maxMessageId(thread.messages, (msg) => msg.from === HUMAN_AGENT_ID);
|
|
894
|
+
const lastInboundId = maxMessageId(thread.messages, isHumanInboundMessage);
|
|
895
|
+
const unread = thread.messages.filter((msg) => isUnreadHumanMessage(vm, thread.peer, msg)).length;
|
|
896
|
+
const needsHumanResponse = lastInboundId > lastHumanReplyId;
|
|
897
|
+
const agentQuestion = thread.messages.some((msg) =>
|
|
898
|
+
isHumanInboundMessage(msg) && msg.id > lastHumanReplyId && messageLooksLikeQuestion(msg)
|
|
899
|
+
);
|
|
900
|
+
|
|
901
|
+
return {
|
|
902
|
+
unread,
|
|
903
|
+
needsHumanResponse,
|
|
904
|
+
agentQuestion,
|
|
905
|
+
score: unread * 10 + (needsHumanResponse ? 5 : 0) + (agentQuestion ? 3 : 0),
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function maxMessageId(messages, predicate) {
|
|
910
|
+
let max = 0;
|
|
911
|
+
for (const msg of messages) {
|
|
912
|
+
if (predicate(msg) && msg.id > max) max = msg.id;
|
|
913
|
+
}
|
|
914
|
+
return max;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function isHumanInboundMessage(msg) {
|
|
918
|
+
return msg.to === HUMAN_AGENT_ID && msg.from !== HUMAN_AGENT_ID;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function isUnreadHumanMessage(vm, peer, msg) {
|
|
922
|
+
if (!isHumanInboundMessage(msg)) return false;
|
|
923
|
+
if ((msg.readBy || []).includes(HUMAN_AGENT_ID)) return false;
|
|
924
|
+
return msg.id > readCursorForPeer(vm, peer);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function readCursorForPeer(vm, peer) {
|
|
928
|
+
const value = Number(vm.inboxReadCursors?.[peer] || 0);
|
|
929
|
+
return Number.isFinite(value) ? value : 0;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function draftForPeer(vm, peer) {
|
|
933
|
+
return typeof vm.inboxDrafts?.[peer] === "string" ? vm.inboxDrafts[peer] : "";
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function messageLooksLikeQuestion(msg) {
|
|
937
|
+
return /\?/.test(`${msg.subject || ""}\n${msg.body || ""}`);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function countClaimableWaiting(vm) {
|
|
941
|
+
const taskCount = vm.tasks.filter(isClaimableTaskWaiting).length;
|
|
942
|
+
const messageCount = vm.messages.filter(isClaimableMessageWaiting).length;
|
|
943
|
+
return taskCount + messageCount;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function countAgentClaimableWaiting(vm, agent) {
|
|
947
|
+
const taskCount = vm.tasks.filter((task) => isClaimableTaskWaiting(task) && targetMatchesAgent(task.target, agent)).length;
|
|
948
|
+
const messageCount = vm.messages.filter((msg) => isClaimableMessageWaiting(msg) && targetMatchesAgent(msg.to, agent)).length;
|
|
949
|
+
return taskCount + messageCount;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function isClaimableTaskWaiting(task) {
|
|
953
|
+
return WAITING_TASK_STATUSES.has(task.status) && !task.claimedBy;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function isClaimableMessageWaiting(msg) {
|
|
957
|
+
return Boolean(msg.claimable && !msg.claimedBy);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function targetMatchesAgent(target, agent) {
|
|
961
|
+
if (!target || !agent) return false;
|
|
962
|
+
if (target === "broadcast" || target === agent.id) return true;
|
|
963
|
+
if (target.startsWith("tag:")) return (agent.tags || []).includes(target.slice(4));
|
|
964
|
+
if (target.startsWith("cap:")) return (agent.capabilities || []).includes(target.slice(4));
|
|
965
|
+
if (target.startsWith("label:")) return agent.label === target.slice(6);
|
|
966
|
+
return false;
|
|
967
|
+
}
|
|
968
|
+
|
|
389
969
|
function getFilteredMessages() {
|
|
390
970
|
if (!this.tagFilter) return this.messages;
|
|
391
971
|
return this.messages.filter((msg) => messageMatchesTag(this, msg, this.tagFilter));
|
|
@@ -425,26 +1005,142 @@
|
|
|
425
1005
|
}
|
|
426
1006
|
|
|
427
1007
|
function getComposeAgents() {
|
|
428
|
-
|
|
1008
|
+
const list = visibleAgents(this);
|
|
1009
|
+
return this.showOffline ? list : list.filter((agent) => agent.status !== "offline");
|
|
429
1010
|
}
|
|
430
1011
|
|
|
431
1012
|
function getUniqueLabels() {
|
|
432
|
-
return [...new Set(this.
|
|
1013
|
+
return [...new Set(visibleAgents(this).filter((agent) => agent.label).map((agent) => agent.label))];
|
|
433
1014
|
}
|
|
434
1015
|
|
|
435
1016
|
function getUniqueCaps() {
|
|
436
|
-
return [...new Set(this.
|
|
1017
|
+
return [...new Set(visibleAgents(this).flatMap((agent) => agent.capabilities || []))];
|
|
437
1018
|
}
|
|
438
1019
|
|
|
439
1020
|
function getUniqueTags() {
|
|
440
|
-
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;
|
|
441
1061
|
}
|
|
442
1062
|
|
|
443
1063
|
function getHealthIssues() {
|
|
444
1064
|
return (this.health?.checks || []).filter((check) => check.status !== "ok");
|
|
445
1065
|
}
|
|
446
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
|
+
|
|
447
1139
|
function compareAgents(vm, a, b) {
|
|
1140
|
+
if (vm.agentSort === "status") {
|
|
1141
|
+
const attentionDelta = agentAttention.call(vm, b).score - agentAttention.call(vm, a).score;
|
|
1142
|
+
if (attentionDelta !== 0) return attentionDelta;
|
|
1143
|
+
}
|
|
448
1144
|
switch (vm.agentSort) {
|
|
449
1145
|
case "name":
|
|
450
1146
|
return vm.displayName(a).localeCompare(vm.displayName(b));
|
|
@@ -463,14 +1159,28 @@
|
|
|
463
1159
|
return {
|
|
464
1160
|
displayName,
|
|
465
1161
|
displayTarget,
|
|
1162
|
+
conversationTitle,
|
|
1163
|
+
messagePreview,
|
|
1164
|
+
agentPair,
|
|
1165
|
+
pairPeerId,
|
|
1166
|
+
pairBadgeClass,
|
|
1167
|
+
pairStatusClass,
|
|
1168
|
+
pairBadgeLabel,
|
|
1169
|
+
pairTitle,
|
|
1170
|
+
agentAttention,
|
|
1171
|
+
agentAttentionTitle,
|
|
466
1172
|
agentType,
|
|
467
1173
|
agentTypeIcon,
|
|
468
1174
|
agentTypeTitle,
|
|
1175
|
+
agentPresence,
|
|
1176
|
+
agentPresenceBadges,
|
|
1177
|
+
agentStatusClass,
|
|
469
1178
|
severityClass,
|
|
470
1179
|
agentStatusTitle,
|
|
471
1180
|
timeAgo,
|
|
472
1181
|
fmtTime,
|
|
473
1182
|
healthAlertClass,
|
|
1183
|
+
activityKindClass,
|
|
474
1184
|
};
|
|
475
1185
|
}
|
|
476
1186
|
|
|
@@ -490,6 +1200,103 @@
|
|
|
490
1200
|
return agent ? this.displayName(agent) : target.slice(-8);
|
|
491
1201
|
}
|
|
492
1202
|
|
|
1203
|
+
function conversationTitle(thread) {
|
|
1204
|
+
if (!thread) return "Inbox";
|
|
1205
|
+
return this.displayTarget(thread.peer);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
function messagePreview(msg) {
|
|
1209
|
+
const text = msg?.subject || msg?.body || "";
|
|
1210
|
+
return text.length > 90 ? text.slice(0, 90) + "..." : text;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
function agentPair(agent) {
|
|
1214
|
+
return agent ? this.pairsByAgentId[agent.id] : null;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
function pairPeerId(pair, agentId) {
|
|
1218
|
+
if (!pair) return "";
|
|
1219
|
+
return pair.requesterId === agentId ? pair.targetId : pair.requesterId;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
function pairBadgeClass(pair) {
|
|
1223
|
+
if (pair?.status === "active") return "bg-success-lt";
|
|
1224
|
+
if (pair?.status === "pending") return "bg-warning-lt";
|
|
1225
|
+
return "bg-secondary-lt";
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
function pairStatusClass(pair) {
|
|
1229
|
+
if (pair?.status === "active") return "bg-success";
|
|
1230
|
+
if (pair?.status === "pending") return "bg-warning";
|
|
1231
|
+
if (pair?.status === "rejected" || pair?.status === "expired") return "bg-danger";
|
|
1232
|
+
return "bg-secondary";
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
function pairBadgeLabel(pair, agentId) {
|
|
1236
|
+
if (!pair) return "";
|
|
1237
|
+
const peer = this.displayTarget(pairPeerId(pair, agentId));
|
|
1238
|
+
if (pair.status === "active") return "paired with " + peer;
|
|
1239
|
+
if (pair.status === "pending" && pair.requesterId === agentId) return "invite to " + peer;
|
|
1240
|
+
if (pair.status === "pending") return "invite from " + peer;
|
|
1241
|
+
return pair.status;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function pairTitle(pair, agentId) {
|
|
1245
|
+
if (!pair) return "";
|
|
1246
|
+
const label = pairBadgeLabel.call(this, pair, agentId);
|
|
1247
|
+
const objective = pair.objective ? " - " + pair.objective : "";
|
|
1248
|
+
return `${label} (${pair.id})${objective}`;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
function agentAttention(agent) {
|
|
1252
|
+
if (!agent) return emptyAttention();
|
|
1253
|
+
const thread = this.allInboxThreads.find((item) => item.peer === agent.id && !item.archived);
|
|
1254
|
+
const pair = this.agentPair(agent);
|
|
1255
|
+
const pendingPairInvite = pair?.status === "pending";
|
|
1256
|
+
const claimableTasks = countAgentClaimableWaiting(this, agent);
|
|
1257
|
+
const attention = {
|
|
1258
|
+
unread: thread?.attention?.unread || 0,
|
|
1259
|
+
needsHumanResponse: Boolean(thread?.attention?.needsHumanResponse),
|
|
1260
|
+
agentQuestion: Boolean(thread?.attention?.agentQuestion),
|
|
1261
|
+
pendingPairInvite,
|
|
1262
|
+
claimableTasks,
|
|
1263
|
+
};
|
|
1264
|
+
attention.total = attention.unread +
|
|
1265
|
+
(attention.needsHumanResponse ? 1 : 0) +
|
|
1266
|
+
(attention.agentQuestion ? 1 : 0) +
|
|
1267
|
+
(attention.pendingPairInvite ? 1 : 0) +
|
|
1268
|
+
attention.claimableTasks;
|
|
1269
|
+
attention.score = attention.unread * 10 +
|
|
1270
|
+
(attention.needsHumanResponse ? 5 : 0) +
|
|
1271
|
+
(attention.agentQuestion ? 3 : 0) +
|
|
1272
|
+
(attention.pendingPairInvite ? 4 : 0) +
|
|
1273
|
+
attention.claimableTasks * 2;
|
|
1274
|
+
return attention;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function emptyAttention() {
|
|
1278
|
+
return {
|
|
1279
|
+
unread: 0,
|
|
1280
|
+
needsHumanResponse: false,
|
|
1281
|
+
agentQuestion: false,
|
|
1282
|
+
pendingPairInvite: false,
|
|
1283
|
+
claimableTasks: 0,
|
|
1284
|
+
total: 0,
|
|
1285
|
+
score: 0,
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function agentAttentionTitle(agent) {
|
|
1290
|
+
const attention = agentAttention.call(this, agent);
|
|
1291
|
+
const parts = [];
|
|
1292
|
+
if (attention.unread) parts.push(`${attention.unread} unread`);
|
|
1293
|
+
if (attention.needsHumanResponse) parts.push("needs human response");
|
|
1294
|
+
if (attention.agentQuestion) parts.push("agent asked a question");
|
|
1295
|
+
if (attention.pendingPairInvite) parts.push("pair invite pending");
|
|
1296
|
+
if (attention.claimableTasks) parts.push(`${attention.claimableTasks} claimable waiting`);
|
|
1297
|
+
return parts.join(", ");
|
|
1298
|
+
}
|
|
1299
|
+
|
|
493
1300
|
function agentType(agent) {
|
|
494
1301
|
const values = [
|
|
495
1302
|
...(agent?.tags || []),
|
|
@@ -516,6 +1323,66 @@
|
|
|
516
1323
|
return AGENT_TYPE_TITLES[agentType(agent)] || AGENT_TYPE_TITLES.agent;
|
|
517
1324
|
}
|
|
518
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
|
+
|
|
519
1386
|
function severityClass(severity) {
|
|
520
1387
|
if (severity === "critical") return "bg-danger-lt";
|
|
521
1388
|
if (severity === "warning") return "bg-warning-lt";
|
|
@@ -525,6 +1392,11 @@
|
|
|
525
1392
|
function agentStatusTitle(agent) {
|
|
526
1393
|
if (!agent) return "";
|
|
527
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";
|
|
528
1400
|
if (agent.ready) return agent.status;
|
|
529
1401
|
|
|
530
1402
|
const lastSeenMs = new Date(agent.lastSeen).getTime();
|
|
@@ -557,17 +1429,66 @@
|
|
|
557
1429
|
return "alert-success";
|
|
558
1430
|
}
|
|
559
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
|
+
|
|
560
1441
|
function createMessageActions() {
|
|
561
1442
|
return {
|
|
562
1443
|
openCompose,
|
|
563
1444
|
openComposeToAgent,
|
|
1445
|
+
openComposeToInboxThread,
|
|
1446
|
+
openInboxThread,
|
|
1447
|
+
markInboxThreadRead,
|
|
1448
|
+
markInboxThreadUnread,
|
|
1449
|
+
archiveInboxThread,
|
|
1450
|
+
unarchiveInboxThread,
|
|
1451
|
+
confirmDeleteInboxThread,
|
|
1452
|
+
doDeleteInboxThread,
|
|
1453
|
+
replyDraftForThread,
|
|
1454
|
+
setReplyDraft,
|
|
1455
|
+
clearReplyDraft,
|
|
1456
|
+
sendInboxReply,
|
|
1457
|
+
resetInboxComposeTarget,
|
|
1458
|
+
doSendInboxCompose,
|
|
564
1459
|
startReply,
|
|
565
1460
|
cancelReply,
|
|
566
1461
|
doSend,
|
|
567
1462
|
doClaim,
|
|
1463
|
+
doClaimTask,
|
|
1464
|
+
doUpdateTaskStatus,
|
|
568
1465
|
doDeleteMessage,
|
|
569
1466
|
openThread,
|
|
570
1467
|
openTaskEvents,
|
|
1468
|
+
recordOperatorActivity,
|
|
1469
|
+
openActivityItem,
|
|
1470
|
+
runHealthAction,
|
|
1471
|
+
openCommandPalette,
|
|
1472
|
+
closeCommandPalette,
|
|
1473
|
+
runCommand,
|
|
1474
|
+
exportActivity,
|
|
1475
|
+
exportThread,
|
|
1476
|
+
exportPair,
|
|
1477
|
+
exportTask,
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
function createPairActions() {
|
|
1482
|
+
return {
|
|
1483
|
+
openPairMessage,
|
|
1484
|
+
closePairMessage,
|
|
1485
|
+
openPairInvite,
|
|
1486
|
+
closePairInvite,
|
|
1487
|
+
doCreatePair,
|
|
1488
|
+
doSendPairMessage,
|
|
1489
|
+
doAcceptPair,
|
|
1490
|
+
doRejectPair,
|
|
1491
|
+
doHangupPair,
|
|
571
1492
|
};
|
|
572
1493
|
}
|
|
573
1494
|
|
|
@@ -588,12 +1509,243 @@
|
|
|
588
1509
|
focusComposeBody(this);
|
|
589
1510
|
}
|
|
590
1511
|
|
|
1512
|
+
function openComposeToInboxThread(thread) {
|
|
1513
|
+
if (!thread) return;
|
|
1514
|
+
this.replyTo = thread.lastMessage ? { id: thread.lastMessage.id, from: thread.lastMessage.from } : null;
|
|
1515
|
+
this.compose = { ...DEFAULT_COMPOSE, from: HUMAN_AGENT_ID, to: thread.peer, channel: thread.lastMessage?.channel || "" };
|
|
1516
|
+
this.composeOpen = true;
|
|
1517
|
+
focusComposeBody(this);
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
function openInboxThread(thread) {
|
|
1521
|
+
this.selectedInboxThread = thread?.id || "";
|
|
1522
|
+
this.markInboxThreadRead(thread);
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
function markInboxThreadRead(thread) {
|
|
1526
|
+
if (!thread?.peer || !thread.messages?.length) return;
|
|
1527
|
+
const lastInboundId = maxMessageId(thread.messages, isHumanInboundMessage);
|
|
1528
|
+
if (lastInboundId <= readCursorForPeer(this, thread.peer)) return;
|
|
1529
|
+
this.inboxReadCursors = { ...this.inboxReadCursors, [thread.peer]: lastInboundId };
|
|
1530
|
+
savePref("inboxReadCursors", this.inboxReadCursors);
|
|
1531
|
+
void saveInboxThreadState(this, {
|
|
1532
|
+
peerId: thread.peer,
|
|
1533
|
+
readCursorMessageId: lastInboundId,
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
function markInboxThreadUnread(thread) {
|
|
1538
|
+
if (!thread?.peer) return;
|
|
1539
|
+
const next = { ...this.inboxReadCursors };
|
|
1540
|
+
delete next[thread.peer];
|
|
1541
|
+
this.inboxReadCursors = next;
|
|
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
|
+
});
|
|
1551
|
+
void saveInboxThreadState(this, {
|
|
1552
|
+
peerId: thread.peer,
|
|
1553
|
+
readCursorMessageId: null,
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
function archiveInboxThread(thread) {
|
|
1558
|
+
if (!thread?.peer || !thread.lastMessage) return;
|
|
1559
|
+
this.inboxArchivedThreads = { ...this.inboxArchivedThreads, [thread.peer]: thread.lastMessage.id };
|
|
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
|
+
});
|
|
1569
|
+
void saveInboxThreadState(this, {
|
|
1570
|
+
peerId: thread.peer,
|
|
1571
|
+
archivedAtMessageId: thread.lastMessage.id,
|
|
1572
|
+
});
|
|
1573
|
+
if (this.selectedInboxThread === thread.id) this.selectedInboxThread = "";
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
function unarchiveInboxThread(thread) {
|
|
1577
|
+
if (!thread?.peer) return;
|
|
1578
|
+
const next = { ...this.inboxArchivedThreads };
|
|
1579
|
+
delete next[thread.peer];
|
|
1580
|
+
this.inboxArchivedThreads = next;
|
|
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
|
+
});
|
|
1590
|
+
void saveInboxThreadState(this, {
|
|
1591
|
+
peerId: thread.peer,
|
|
1592
|
+
archivedAtMessageId: null,
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
async function saveInboxThreadState(vm, patch) {
|
|
1597
|
+
try {
|
|
1598
|
+
await vm.api("PATCH", "/inbox/threads", { operatorId: INBOX_OPERATOR_ID, ...patch });
|
|
1599
|
+
} catch {}
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
function confirmDeleteInboxThread(thread) {
|
|
1603
|
+
if (!thread) return;
|
|
1604
|
+
this.openConfirm(
|
|
1605
|
+
"Delete Thread",
|
|
1606
|
+
`Delete ${thread.messages.length} message(s) in ${this.conversationTitle(thread)}? This cannot be undone.`,
|
|
1607
|
+
() => this.doDeleteInboxThread(thread)
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
async function doDeleteInboxThread(thread) {
|
|
1612
|
+
if (!thread?.messages?.length) return;
|
|
1613
|
+
try {
|
|
1614
|
+
await Promise.all(thread.messages.map((msg) => this.api("DELETE", "/messages/" + msg.id)));
|
|
1615
|
+
this.messages = this.messages.filter((msg) => !thread.messages.some((item) => item.id === msg.id));
|
|
1616
|
+
this.selectedInboxThread = "";
|
|
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
|
+
});
|
|
1625
|
+
} catch (e) {
|
|
1626
|
+
alert("Delete thread failed: " + e.message);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
function replyDraftForThread(thread) {
|
|
1631
|
+
return thread?.peer ? draftForPeer(this, thread.peer) : "";
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
function setReplyDraft(thread, value) {
|
|
1635
|
+
if (!thread?.peer) return;
|
|
1636
|
+
const next = { ...this.inboxDrafts, [thread.peer]: value };
|
|
1637
|
+
if (!value) delete next[thread.peer];
|
|
1638
|
+
this.inboxDrafts = next;
|
|
1639
|
+
savePref("inboxDrafts", this.inboxDrafts);
|
|
1640
|
+
if (value) {
|
|
1641
|
+
void saveInboxDraft(this, thread.peer, value);
|
|
1642
|
+
} else {
|
|
1643
|
+
void deleteInboxDraftState(this, thread.peer);
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
function clearReplyDraft(thread) {
|
|
1648
|
+
if (!thread?.peer) return;
|
|
1649
|
+
this.setReplyDraft(thread, "");
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
async function saveInboxDraft(vm, peerId, body) {
|
|
1653
|
+
try {
|
|
1654
|
+
await vm.api("PUT", "/inbox/drafts", { operatorId: INBOX_OPERATOR_ID, peerId, body });
|
|
1655
|
+
} catch {}
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
async function deleteInboxDraftState(vm, peerId) {
|
|
1659
|
+
try {
|
|
1660
|
+
await vm.api("DELETE", "/inbox/drafts?operatorId=" + encodeURIComponent(INBOX_OPERATOR_ID) + "&peerId=" + encodeURIComponent(peerId));
|
|
1661
|
+
} catch {}
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
async function sendInboxReply(thread) {
|
|
1665
|
+
if (!thread) return;
|
|
1666
|
+
const body = this.replyDraftForThread(thread).trim();
|
|
1667
|
+
if (!body) {
|
|
1668
|
+
alert("Reply body is required.");
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
try {
|
|
1673
|
+
const payload = {
|
|
1674
|
+
from: HUMAN_AGENT_ID,
|
|
1675
|
+
to: thread.peer,
|
|
1676
|
+
body,
|
|
1677
|
+
};
|
|
1678
|
+
if (thread.lastMessage?.channel) payload.channel = thread.lastMessage.channel;
|
|
1679
|
+
if (thread.lastMessage?.id) payload.replyTo = thread.lastMessage.id;
|
|
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
|
+
});
|
|
1689
|
+
this.clearReplyDraft(thread);
|
|
1690
|
+
this.markInboxThreadRead(thread);
|
|
1691
|
+
await this.fetchMessages();
|
|
1692
|
+
} catch (e) {
|
|
1693
|
+
alert("Reply failed: " + e.message);
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
function resetInboxComposeTarget() {
|
|
1698
|
+
this.inboxCompose = { ...this.inboxCompose, to: "" };
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
function inboxComposeTarget(vm) {
|
|
1702
|
+
const target = vm.inboxCompose.to;
|
|
1703
|
+
if (!target) return "";
|
|
1704
|
+
if (vm.inboxCompose.toMode === "tag") return "tag:" + target;
|
|
1705
|
+
if (vm.inboxCompose.toMode === "cap") return "cap:" + target;
|
|
1706
|
+
return target;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
async function doSendInboxCompose() {
|
|
1710
|
+
const target = inboxComposeTarget(this);
|
|
1711
|
+
if (!target || !this.inboxCompose.body) {
|
|
1712
|
+
alert("Target and Message are required.");
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
try {
|
|
1717
|
+
const payload = {
|
|
1718
|
+
from: HUMAN_AGENT_ID,
|
|
1719
|
+
to: target,
|
|
1720
|
+
body: this.inboxCompose.body,
|
|
1721
|
+
};
|
|
1722
|
+
if (this.inboxCompose.channel) payload.channel = this.inboxCompose.channel;
|
|
1723
|
+
if (this.inboxCompose.subject) payload.subject = this.inboxCompose.subject;
|
|
1724
|
+
if (this.inboxCompose.claimable) payload.claimable = true;
|
|
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
|
+
});
|
|
1735
|
+
this.inboxCompose = { ...DEFAULT_INBOX_COMPOSE, toMode: this.inboxCompose.toMode, to: this.inboxCompose.to };
|
|
1736
|
+
await this.fetchMessages();
|
|
1737
|
+
} catch (e) {
|
|
1738
|
+
alert("Send failed: " + e.message);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
|
|
591
1742
|
function startReply(msg) {
|
|
1743
|
+
const replyTarget = msg.from === HUMAN_AGENT_ID ? msg.to : msg.from;
|
|
592
1744
|
this.replyTo = { id: msg.id, from: msg.from };
|
|
593
1745
|
this.compose = {
|
|
594
1746
|
...DEFAULT_COMPOSE,
|
|
595
|
-
from: "",
|
|
596
|
-
to:
|
|
1747
|
+
from: this.view === "inbox" ? HUMAN_AGENT_ID : "",
|
|
1748
|
+
to: replyTarget,
|
|
597
1749
|
channel: msg.channel || "",
|
|
598
1750
|
};
|
|
599
1751
|
this.openCompose();
|
|
@@ -623,10 +1775,21 @@
|
|
|
623
1775
|
}
|
|
624
1776
|
|
|
625
1777
|
try {
|
|
626
|
-
|
|
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
|
+
});
|
|
627
1789
|
this.composeOpen = false;
|
|
628
1790
|
this.replyTo = null;
|
|
629
1791
|
this.compose = { ...DEFAULT_COMPOSE };
|
|
1792
|
+
await this.fetchMessages();
|
|
630
1793
|
} catch (e) {
|
|
631
1794
|
alert("Send failed: " + e.message);
|
|
632
1795
|
}
|
|
@@ -642,15 +1805,85 @@
|
|
|
642
1805
|
try {
|
|
643
1806
|
const result = await this.api("POST", "/messages/" + msgId + "/claim", { agentId });
|
|
644
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()]);
|
|
645
1817
|
} catch (e) {
|
|
646
1818
|
alert("Claim failed: " + e.message);
|
|
647
1819
|
}
|
|
648
1820
|
}
|
|
649
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
|
+
|
|
650
1876
|
async function doDeleteMessage(id) {
|
|
651
1877
|
try {
|
|
652
1878
|
await this.api("DELETE", "/messages/" + id);
|
|
653
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
|
+
});
|
|
654
1887
|
} catch (e) {
|
|
655
1888
|
alert("Delete failed: " + e.message);
|
|
656
1889
|
}
|
|
@@ -671,13 +1904,423 @@
|
|
|
671
1904
|
this.taskEventsOpen = true;
|
|
672
1905
|
try {
|
|
673
1906
|
this.taskEvents = await this.api("GET", "/tasks/" + task.id + "/events");
|
|
1907
|
+
this.taskEventCache = { ...this.taskEventCache, [task.id]: this.taskEvents };
|
|
674
1908
|
} catch (e) {
|
|
675
1909
|
alert("Failed to load task events: " + e.message);
|
|
676
1910
|
}
|
|
677
1911
|
}
|
|
678
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
|
+
|
|
2171
|
+
function openPairMessage(pair, fromId) {
|
|
2172
|
+
if (!pair) return;
|
|
2173
|
+
this.pairMessage = {
|
|
2174
|
+
...DEFAULT_PAIR_MESSAGE,
|
|
2175
|
+
pairId: pair.id,
|
|
2176
|
+
from: fromId || pair.requesterId || pair.targetId || "",
|
|
2177
|
+
};
|
|
2178
|
+
this.pairMessageOpen = true;
|
|
2179
|
+
this.$nextTick(() => this.$refs?.pairMessageBody?.focus());
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
function openPairInvite(requesterId) {
|
|
2183
|
+
this.pairInvite = {
|
|
2184
|
+
...DEFAULT_PAIR_INVITE,
|
|
2185
|
+
requesterId: requesterId || this.selectedAgent || "",
|
|
2186
|
+
};
|
|
2187
|
+
this.pairInviteOpen = true;
|
|
2188
|
+
this.$nextTick(() => this.$refs?.pairInviteObjective?.focus());
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
function closePairInvite() {
|
|
2192
|
+
this.pairInviteOpen = false;
|
|
2193
|
+
this.pairInvite = { ...DEFAULT_PAIR_INVITE };
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
async function doCreatePair() {
|
|
2197
|
+
if (!this.pairInvite.requesterId || !this.pairInvite.targetId) {
|
|
2198
|
+
alert("Requester and Target are required.");
|
|
2199
|
+
return;
|
|
2200
|
+
}
|
|
2201
|
+
if (this.pairInvite.requesterId === this.pairInvite.targetId) {
|
|
2202
|
+
alert("Requester and Target must be different agents.");
|
|
2203
|
+
return;
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
try {
|
|
2207
|
+
const payload = {
|
|
2208
|
+
from: this.pairInvite.requesterId,
|
|
2209
|
+
target: this.pairInvite.targetId,
|
|
2210
|
+
};
|
|
2211
|
+
if (this.pairInvite.objective) payload.objective = this.pairInvite.objective;
|
|
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
|
+
});
|
|
2221
|
+
this.closePairInvite();
|
|
2222
|
+
await Promise.all([this.fetchPairs(), this.fetchMessages()]);
|
|
2223
|
+
} catch (e) {
|
|
2224
|
+
alert("Pair invite failed: " + e.message);
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
function closePairMessage() {
|
|
2229
|
+
this.pairMessageOpen = false;
|
|
2230
|
+
this.pairMessage = { ...DEFAULT_PAIR_MESSAGE };
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
async function doSendPairMessage() {
|
|
2234
|
+
if (!this.pairMessage.pairId || !this.pairMessage.from || !this.pairMessage.body) {
|
|
2235
|
+
alert("Pair, From, and Message are required.");
|
|
2236
|
+
return;
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
try {
|
|
2240
|
+
const payload = { from: this.pairMessage.from, body: this.pairMessage.body };
|
|
2241
|
+
if (this.pairMessage.subject) payload.subject = this.pairMessage.subject;
|
|
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
|
+
});
|
|
2251
|
+
this.closePairMessage();
|
|
2252
|
+
await Promise.all([this.fetchPairs(), this.fetchMessages()]);
|
|
2253
|
+
} catch (e) {
|
|
2254
|
+
alert("Pair message failed: " + e.message);
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
async function doAcceptPair(pair) {
|
|
2259
|
+
if (!pair) return;
|
|
2260
|
+
try {
|
|
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
|
+
});
|
|
2270
|
+
await Promise.all([this.fetchPairs(), this.fetchMessages()]);
|
|
2271
|
+
} catch (e) {
|
|
2272
|
+
alert("Accept failed: " + e.message);
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
async function doRejectPair(pair) {
|
|
2277
|
+
if (!pair) return;
|
|
2278
|
+
try {
|
|
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
|
+
});
|
|
2288
|
+
await Promise.all([this.fetchPairs(), this.fetchMessages()]);
|
|
2289
|
+
} catch (e) {
|
|
2290
|
+
alert("Reject failed: " + e.message);
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
async function doHangupPair(pair, agentId) {
|
|
2295
|
+
if (!pair) return;
|
|
2296
|
+
try {
|
|
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
|
+
});
|
|
2306
|
+
await Promise.all([this.fetchPairs(), this.fetchMessages()]);
|
|
2307
|
+
} catch (e) {
|
|
2308
|
+
alert("Hang up failed: " + e.message);
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
|
|
679
2312
|
function createAgentActions() {
|
|
680
2313
|
return {
|
|
2314
|
+
openAgentDetail(agent) {
|
|
2315
|
+
if (!agent) return;
|
|
2316
|
+
this.agentDetailId = agent.id;
|
|
2317
|
+
this.agentDetailOpen = true;
|
|
2318
|
+
},
|
|
2319
|
+
|
|
2320
|
+
closeAgentDetail() {
|
|
2321
|
+
this.agentDetailOpen = false;
|
|
2322
|
+
},
|
|
2323
|
+
|
|
681
2324
|
openRename(agent) {
|
|
682
2325
|
this.renameModal = { show: true, agentId: agent.id, label: agent.label || "" };
|
|
683
2326
|
this.$nextTick(() => this.$refs.renameInput?.focus());
|
|
@@ -826,6 +2469,7 @@
|
|
|
826
2469
|
...createApiMethods(),
|
|
827
2470
|
...createDisplayMethods(),
|
|
828
2471
|
...createMessageActions(),
|
|
2472
|
+
...createPairActions(),
|
|
829
2473
|
...createAgentActions(),
|
|
830
2474
|
...createChartMethods(),
|
|
831
2475
|
};
|
|
@@ -835,7 +2479,7 @@
|
|
|
835
2479
|
|
|
836
2480
|
window.AgentRelayDashboard = {
|
|
837
2481
|
createRelayDashboard,
|
|
838
|
-
helpers: { loadPref, savePref, indexAgents, upsertById, upsertTask, compareAgents, agentType },
|
|
2482
|
+
helpers: { loadPref, savePref, indexAgents, upsertById, upsertTask, compareAgents, agentType, isBuiltInAgent },
|
|
839
2483
|
};
|
|
840
2484
|
window.relay = createRelayDashboard;
|
|
841
2485
|
|