agent-relay-server 0.6.1 → 0.7.1
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 +3 -1
- package/public/dashboard/actions.js +819 -0
- package/public/dashboard/api.js +336 -0
- package/public/dashboard/app.js +34 -0
- package/public/dashboard/charts.js +128 -0
- package/public/dashboard/computed.js +693 -0
- package/public/dashboard/constants.js +28 -0
- package/public/dashboard/display.js +345 -0
- package/public/dashboard/state.js +129 -0
- package/public/dashboard/utils.js +207 -0
- package/public/index.html +48 -36
- package/scripts/orchestrator-spawn-smoke.ts +140 -0
- package/src/cli.ts +5 -4
- package/src/config.ts +1 -0
- package/src/db.ts +52 -4
- package/src/routes.ts +74 -48
- package/src/types.ts +16 -0
- package/src/upgrade.ts +80 -7
- package/public/dashboard.js +0 -3032
package/public/dashboard.js
DELETED
|
@@ -1,3032 +0,0 @@
|
|
|
1
|
-
(() => {
|
|
2
|
-
const PREF_PREFIX = "ar-";
|
|
3
|
-
const HUMAN_AGENT_ID = "user";
|
|
4
|
-
const INBOX_OPERATOR_ID = HUMAN_AGENT_ID;
|
|
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: "" };
|
|
9
|
-
const DEFAULT_AGENT_SPAWN = { provider: "codex", approvalMode: "guarded", cwd: "", label: "" };
|
|
10
|
-
const CLOSED_TASK_STATUSES = new Set(["done", "failed", "canceled"]);
|
|
11
|
-
const WAITING_TASK_STATUSES = new Set(["open", "blocked"]);
|
|
12
|
-
const STATUS_SORT_ORDER = { online: 0, idle: 1, busy: 2, offline: 3 };
|
|
13
|
-
const LIVE_REFRESH_MS = 5_000;
|
|
14
|
-
const AGENT_TYPE_ICONS = {
|
|
15
|
-
codex: "ti-terminal-2",
|
|
16
|
-
claude: "claude-sol",
|
|
17
|
-
user: "ti-user",
|
|
18
|
-
system: "ti-server",
|
|
19
|
-
channel: "ti-messages",
|
|
20
|
-
agent: "ti-robot",
|
|
21
|
-
};
|
|
22
|
-
const AGENT_TYPE_TITLES = {
|
|
23
|
-
codex: "Codex agent",
|
|
24
|
-
claude: "Claude agent",
|
|
25
|
-
user: "Human operator",
|
|
26
|
-
system: "System",
|
|
27
|
-
channel: "Channel",
|
|
28
|
-
agent: "Agent",
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
function loadPref(key, fallback) {
|
|
32
|
-
try {
|
|
33
|
-
const value = localStorage.getItem(PREF_PREFIX + key);
|
|
34
|
-
return value !== null ? JSON.parse(value) : fallback;
|
|
35
|
-
} catch {
|
|
36
|
-
return fallback;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function savePref(key, value) {
|
|
41
|
-
localStorage.setItem(PREF_PREFIX + key, JSON.stringify(value));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function initialState() {
|
|
45
|
-
return {
|
|
46
|
-
view: loadPref("view", "overview"),
|
|
47
|
-
|
|
48
|
-
showOffline: loadPref("showOffline", false),
|
|
49
|
-
showBuiltIns: loadPref("showBuiltIns", false),
|
|
50
|
-
autoRefresh: loadPref("autoRefresh", true),
|
|
51
|
-
agentSort: loadPref("agentSort", "status"),
|
|
52
|
-
agentSortDir: loadPref("agentSortDir", "asc"),
|
|
53
|
-
agentPresetFilter: loadPref("agentPresetFilter", ""),
|
|
54
|
-
|
|
55
|
-
agents: [],
|
|
56
|
-
agentsById: {},
|
|
57
|
-
orchestrators: [],
|
|
58
|
-
pairs: [],
|
|
59
|
-
messages: [],
|
|
60
|
-
tasks: [],
|
|
61
|
-
integrations: [],
|
|
62
|
-
channels: [],
|
|
63
|
-
connectors: [],
|
|
64
|
-
taskEvents: [],
|
|
65
|
-
taskEventCache: {},
|
|
66
|
-
stats: {},
|
|
67
|
-
health: null,
|
|
68
|
-
now: Date.now(),
|
|
69
|
-
authToken: loadPref("authToken", ""),
|
|
70
|
-
inboxReadCursors: loadPref("inboxReadCursors", {}),
|
|
71
|
-
inboxArchivedThreads: loadPref("inboxArchivedThreads", {}),
|
|
72
|
-
inboxDrafts: loadPref("inboxDrafts", {}),
|
|
73
|
-
inboxSearch: "",
|
|
74
|
-
inboxSort: loadPref("inboxSort", "attention"),
|
|
75
|
-
inboxSortDir: loadPref("inboxSortDir", "desc"),
|
|
76
|
-
inboxShowArchived: loadPref("inboxShowArchived", false),
|
|
77
|
-
operatorActivity: loadPref("operatorActivity", []),
|
|
78
|
-
activityEvents: [],
|
|
79
|
-
activityFilter: loadPref("activityFilter", ""),
|
|
80
|
-
|
|
81
|
-
selectedAgent: "",
|
|
82
|
-
agentDetailOpen: false,
|
|
83
|
-
agentDetailId: "",
|
|
84
|
-
channelDetailOpen: false,
|
|
85
|
-
channelDetailId: "",
|
|
86
|
-
selectedInboxThread: "",
|
|
87
|
-
replyTo: null,
|
|
88
|
-
composeOpen: false,
|
|
89
|
-
agentSpawnOpen: false,
|
|
90
|
-
orchestratorSpawnOpen: false,
|
|
91
|
-
spawnOrchId: "",
|
|
92
|
-
spawnProvider: "claude",
|
|
93
|
-
spawnCwd: "",
|
|
94
|
-
spawnLabel: "",
|
|
95
|
-
spawnApproval: "guarded",
|
|
96
|
-
spawnPrompt: "",
|
|
97
|
-
spawnDirListing: null,
|
|
98
|
-
agentDirectoryBrowser: { open: false, loading: false, path: "", parent: "", home: "", cwd: "", entries: [], error: "" },
|
|
99
|
-
pairInviteOpen: false,
|
|
100
|
-
pairMessageOpen: false,
|
|
101
|
-
threadOpen: false,
|
|
102
|
-
threadMessages: [],
|
|
103
|
-
taskEventsOpen: false,
|
|
104
|
-
commandPaletteOpen: false,
|
|
105
|
-
commandQuery: "",
|
|
106
|
-
connected: false,
|
|
107
|
-
authNeeded: false,
|
|
108
|
-
|
|
109
|
-
compose: { ...DEFAULT_COMPOSE },
|
|
110
|
-
agentSpawn: { ...DEFAULT_AGENT_SPAWN },
|
|
111
|
-
pairInvite: { ...DEFAULT_PAIR_INVITE },
|
|
112
|
-
pairMessage: { ...DEFAULT_PAIR_MESSAGE },
|
|
113
|
-
inboxCompose: { ...DEFAULT_INBOX_COMPOSE },
|
|
114
|
-
|
|
115
|
-
confirmModal: { show: false, title: "", message: "", action: null },
|
|
116
|
-
renameModal: { show: false, agentId: "", label: "" },
|
|
117
|
-
|
|
118
|
-
channelFilter: "",
|
|
119
|
-
tagFilter: "",
|
|
120
|
-
agentStatusFilter: loadPref("agentStatusFilter", ""),
|
|
121
|
-
agentTagFilter: loadPref("agentTagFilter", ""),
|
|
122
|
-
pairStatusFilter: loadPref("pairStatusFilter", "open"),
|
|
123
|
-
taskStatusFilter: "",
|
|
124
|
-
taskSourceFilter: "",
|
|
125
|
-
|
|
126
|
-
chartInstances: {},
|
|
127
|
-
_es: null,
|
|
128
|
-
_clockTimer: null,
|
|
129
|
-
_refreshTimer: null,
|
|
130
|
-
_refreshInFlight: false,
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function watchPersistedPrefs(vm) {
|
|
135
|
-
vm.$watch("showOffline", (value) => vm.save("showOffline", value));
|
|
136
|
-
vm.$watch("showBuiltIns", (value) => vm.save("showBuiltIns", value));
|
|
137
|
-
vm.$watch("autoRefresh", (value) => {
|
|
138
|
-
vm.save("autoRefresh", value);
|
|
139
|
-
if (value) vm.startAutoRefresh();
|
|
140
|
-
else vm.stopAutoRefresh();
|
|
141
|
-
});
|
|
142
|
-
vm.$watch("agentSort", (value) => vm.save("agentSort", value));
|
|
143
|
-
vm.$watch("agentSortDir", (value) => vm.save("agentSortDir", value));
|
|
144
|
-
vm.$watch("agentPresetFilter", (value) => vm.save("agentPresetFilter", value));
|
|
145
|
-
vm.$watch("agentStatusFilter", (value) => vm.save("agentStatusFilter", value));
|
|
146
|
-
vm.$watch("agentTagFilter", (value) => vm.save("agentTagFilter", value));
|
|
147
|
-
vm.$watch("pairStatusFilter", (value) => vm.save("pairStatusFilter", value));
|
|
148
|
-
vm.$watch("inboxSort", (value) => vm.save("inboxSort", value));
|
|
149
|
-
vm.$watch("inboxSortDir", (value) => vm.save("inboxSortDir", value));
|
|
150
|
-
vm.$watch("inboxShowArchived", (value) => vm.save("inboxShowArchived", value));
|
|
151
|
-
vm.$watch("activityFilter", (value) => vm.save("activityFilter", value));
|
|
152
|
-
vm.$watch("view", (value, oldValue) => {
|
|
153
|
-
vm.save("view", value);
|
|
154
|
-
if (oldValue === "analytics") vm.destroyAllCharts();
|
|
155
|
-
if (value === "analytics") vm.$nextTick(() => vm.renderCharts());
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function parseEventData(event) {
|
|
160
|
-
return JSON.parse(event.data);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function indexAgents(agents) {
|
|
164
|
-
const byId = {};
|
|
165
|
-
for (const agent of agents) byId[agent.id] = agent;
|
|
166
|
-
return byId;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function upsertById(list, item) {
|
|
170
|
-
const idx = list.findIndex((existing) => existing.id === item.id);
|
|
171
|
-
if (idx >= 0) list.splice(idx, 1, item);
|
|
172
|
-
else list.push(item);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function upsertTask(vm, task) {
|
|
176
|
-
const idx = vm.tasks.findIndex((existing) => existing.id === task.id);
|
|
177
|
-
if (idx >= 0) vm.tasks[idx] = task;
|
|
178
|
-
else vm.tasks.unshift(task);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function syncAgentStats(vm) {
|
|
182
|
-
vm.stats.agents = vm.agents.length;
|
|
183
|
-
vm.stats.online = vm.agents.filter((agent) => agent.status !== "offline").length;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function syncMessageStats(vm, msg) {
|
|
187
|
-
vm.stats.messages = (vm.stats.messages ?? 0) + 1;
|
|
188
|
-
const createdAt = new Date(msg.createdAt || Date.now()).getTime();
|
|
189
|
-
if (Number.isFinite(createdAt) && Date.now() - createdAt <= 86_400_000) {
|
|
190
|
-
vm.stats.messagesLast24h = (vm.stats.messagesLast24h ?? 0) + 1;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function refreshChartsIfVisible(vm) {
|
|
195
|
-
if (vm.view !== "analytics") return;
|
|
196
|
-
vm.$nextTick(() => vm.renderCharts());
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function registerKeyboardShortcuts(vm) {
|
|
200
|
-
if (typeof window === "undefined" || !window.addEventListener || vm._keyboardShortcutsRegistered) return;
|
|
201
|
-
window.addEventListener("keydown", (event) => {
|
|
202
|
-
const key = event.key?.toLowerCase();
|
|
203
|
-
if ((event.metaKey || event.ctrlKey) && key === "k") {
|
|
204
|
-
event.preventDefault();
|
|
205
|
-
vm.openCommandPalette();
|
|
206
|
-
} else if (key === "escape" && vm.commandPaletteOpen) {
|
|
207
|
-
event.preventDefault();
|
|
208
|
-
vm.closeCommandPalette();
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
vm._keyboardShortcutsRegistered = true;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function createLifecycleMethods() {
|
|
215
|
-
return {
|
|
216
|
-
async init() {
|
|
217
|
-
this.startClock();
|
|
218
|
-
watchPersistedPrefs(this);
|
|
219
|
-
registerKeyboardShortcuts(this);
|
|
220
|
-
|
|
221
|
-
try {
|
|
222
|
-
this.stats = await this.api("GET", "/stats");
|
|
223
|
-
} catch {
|
|
224
|
-
if (this.authNeeded) return;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
await this.refresh();
|
|
228
|
-
this.connectSSE();
|
|
229
|
-
this.startAutoRefresh();
|
|
230
|
-
},
|
|
231
|
-
|
|
232
|
-
save(key, value) {
|
|
233
|
-
savePref(key, value);
|
|
234
|
-
},
|
|
235
|
-
|
|
236
|
-
async switchView(view) {
|
|
237
|
-
this.view = view;
|
|
238
|
-
if (view === "inbox" || view === "messages") await this.fetchMessages();
|
|
239
|
-
if (view === "inbox") this.markInboxThreadRead(this.selectedInboxThreadData);
|
|
240
|
-
if (view === "activity") await Promise.all([this.fetchMessages(), this.fetchPairs(), this.fetchTasks(), this.fetchActivityEvents()]);
|
|
241
|
-
if (view === "work") await Promise.all([this.fetchMessages(), this.fetchTasks()]);
|
|
242
|
-
if (view === "pairs") this.fetchPairs();
|
|
243
|
-
if (view === "channels") await this.fetchChannels();
|
|
244
|
-
if (view === "connectors") await this.fetchConnectors();
|
|
245
|
-
if (view === "integrations") await this.fetchIntegrations();
|
|
246
|
-
if (view === "tasks") this.fetchTasks();
|
|
247
|
-
},
|
|
248
|
-
|
|
249
|
-
startClock() {
|
|
250
|
-
if (this._clockTimer) return;
|
|
251
|
-
this._clockTimer = setInterval(() => {
|
|
252
|
-
this.now = Date.now();
|
|
253
|
-
}, 1_000);
|
|
254
|
-
},
|
|
255
|
-
|
|
256
|
-
startAutoRefresh() {
|
|
257
|
-
this.stopAutoRefresh();
|
|
258
|
-
if (!this.autoRefresh) return;
|
|
259
|
-
this._refreshTimer = setInterval(() => this.refreshLiveData(), LIVE_REFRESH_MS);
|
|
260
|
-
},
|
|
261
|
-
|
|
262
|
-
stopAutoRefresh() {
|
|
263
|
-
if (!this._refreshTimer) return;
|
|
264
|
-
clearInterval(this._refreshTimer);
|
|
265
|
-
this._refreshTimer = null;
|
|
266
|
-
},
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function createSseMethods() {
|
|
271
|
-
return {
|
|
272
|
-
connectSSE,
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function connectSSE() {
|
|
277
|
-
if (this._es) this._es.close();
|
|
278
|
-
|
|
279
|
-
const es = new EventSource(buildEventsUrl(this.authToken));
|
|
280
|
-
this._es = es;
|
|
281
|
-
|
|
282
|
-
es.addEventListener("connected", () => {
|
|
283
|
-
this.connected = true;
|
|
284
|
-
});
|
|
285
|
-
es.onerror = () => {
|
|
286
|
-
this.connected = false;
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
es.addEventListener("message.new", (event) => handleNewMessage(this, parseEventData(event)));
|
|
290
|
-
es.addEventListener("agent.status", (event) => handleAgentStatus(this, parseEventData(event)));
|
|
291
|
-
es.addEventListener("agent.removed", (event) => handleAgentRemoved(this, parseEventData(event)));
|
|
292
|
-
es.addEventListener("message.claimed", (event) => handleMessageClaimed(this, parseEventData(event)));
|
|
293
|
-
es.addEventListener("message.claim_released", (event) => handleMessageClaimReleased(this, parseEventData(event)));
|
|
294
|
-
es.addEventListener("message.deleted", (event) => handleMessageDeleted(this, parseEventData(event)));
|
|
295
|
-
es.addEventListener("orchestrator.status", (event) => handleOrchestratorStatus(this, parseEventData(event)));
|
|
296
|
-
es.addEventListener("orchestrator.removed", (event) => handleOrchestratorRemoved(this, parseEventData(event)));
|
|
297
|
-
registerTaskEvents(this, es);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function baseUrl() {
|
|
301
|
-
const href = window.location.href.split("?")[0].split("#")[0];
|
|
302
|
-
return href.endsWith("/") ? href : href + "/";
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function buildEventsUrl(authToken) {
|
|
306
|
-
const eventUrl = new URL("api/events", baseUrl());
|
|
307
|
-
if (authToken) eventUrl.searchParams.set("token", authToken);
|
|
308
|
-
return eventUrl.toString();
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function handleNewMessage(vm, msg) {
|
|
312
|
-
if (vm.messages.some((existing) => existing.id === msg.id)) return;
|
|
313
|
-
if (vm.view === "messages" && vm.selectedAgent && msg.from !== vm.selectedAgent && msg.to !== vm.selectedAgent) return;
|
|
314
|
-
if (vm.view === "messages" && vm.channelFilter && msg.channel !== vm.channelFilter) return;
|
|
315
|
-
|
|
316
|
-
vm.messages.push(msg);
|
|
317
|
-
if (vm.messages.length > 200) vm.messages.shift();
|
|
318
|
-
syncMessageStats(vm, msg);
|
|
319
|
-
refreshChartsIfVisible(vm);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function handleAgentStatus(vm, agent) {
|
|
323
|
-
upsertById(vm.agents, agent);
|
|
324
|
-
vm.agentsById[agent.id] = agent;
|
|
325
|
-
syncAgentStats(vm);
|
|
326
|
-
refreshChartsIfVisible(vm);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function handleAgentRemoved(vm, data) {
|
|
330
|
-
vm.agents = vm.agents.filter((agent) => agent.id !== data.id);
|
|
331
|
-
delete vm.agentsById[data.id];
|
|
332
|
-
syncAgentStats(vm);
|
|
333
|
-
refreshChartsIfVisible(vm);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function handleOrchestratorStatus(vm, orch) {
|
|
337
|
-
upsertById(vm.orchestrators, orch);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function handleOrchestratorRemoved(vm, data) {
|
|
341
|
-
vm.orchestrators = vm.orchestrators.filter((o) => o.id !== data.id);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function handleMessageClaimed(vm, data) {
|
|
345
|
-
const msg = vm.messages.find((item) => item.id === data.messageId);
|
|
346
|
-
if (!msg) return;
|
|
347
|
-
msg.claimedBy = data.claimedBy;
|
|
348
|
-
msg.claimExpiresAt = data.claimExpiresAt;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function handleMessageClaimReleased(vm, data) {
|
|
352
|
-
const msg = vm.messages.find((item) => item.id === data.messageId);
|
|
353
|
-
if (!msg) return;
|
|
354
|
-
delete msg.claimedBy;
|
|
355
|
-
delete msg.claimedAt;
|
|
356
|
-
delete msg.claimExpiresAt;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function handleMessageDeleted(vm, data) {
|
|
360
|
-
vm.messages = vm.messages.filter((msg) => msg.id !== data.messageId);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
function registerTaskEvents(vm, es) {
|
|
364
|
-
for (const eventName of ["task.created", "task.updated", "task.claimed", "task.status"]) {
|
|
365
|
-
es.addEventListener(eventName, (event) => upsertTask(vm, parseEventData(event)));
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function createApiMethods() {
|
|
370
|
-
return {
|
|
371
|
-
async api(method, path, body) {
|
|
372
|
-
const opts = { method, headers: {} };
|
|
373
|
-
if (this.authToken) opts.headers["X-Agent-Relay-Token"] = this.authToken;
|
|
374
|
-
if (body) {
|
|
375
|
-
opts.headers["Content-Type"] = "application/json";
|
|
376
|
-
opts.body = JSON.stringify(body);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
const response = await fetch(new URL("api" + path, baseUrl()), opts);
|
|
380
|
-
if (!response.ok) {
|
|
381
|
-
if (response.status === 401) this.authNeeded = true;
|
|
382
|
-
const text = await response.text();
|
|
383
|
-
throw new Error(text || response.statusText);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
this.authNeeded = false;
|
|
387
|
-
return response.json();
|
|
388
|
-
},
|
|
389
|
-
|
|
390
|
-
saveTokenAndRefresh() {
|
|
391
|
-
this.save("authToken", this.authToken);
|
|
392
|
-
this.authNeeded = false;
|
|
393
|
-
this.connectSSE();
|
|
394
|
-
this.refresh();
|
|
395
|
-
},
|
|
396
|
-
|
|
397
|
-
async refresh() {
|
|
398
|
-
await Promise.all([this.fetchStats(), this.fetchHealth(), this.fetchAgents(), this.fetchOrchestrators(), this.fetchPairs(), this.fetchMessages(), this.fetchTasks(), this.fetchChannels(), this.fetchConnectors(), this.fetchIntegrations(), this.fetchInboxState(), this.fetchActivityEvents()]);
|
|
399
|
-
},
|
|
400
|
-
|
|
401
|
-
async refreshLiveData() {
|
|
402
|
-
if (this._refreshInFlight || this.authNeeded) return;
|
|
403
|
-
this._refreshInFlight = true;
|
|
404
|
-
try {
|
|
405
|
-
await this.refresh();
|
|
406
|
-
refreshChartsIfVisible(this);
|
|
407
|
-
} finally {
|
|
408
|
-
this._refreshInFlight = false;
|
|
409
|
-
}
|
|
410
|
-
},
|
|
411
|
-
|
|
412
|
-
async fetchStats() {
|
|
413
|
-
try {
|
|
414
|
-
this.stats = await this.api("GET", "/stats");
|
|
415
|
-
} catch {}
|
|
416
|
-
},
|
|
417
|
-
|
|
418
|
-
async fetchHealth() {
|
|
419
|
-
try {
|
|
420
|
-
this.health = await this.api("GET", "/health");
|
|
421
|
-
} catch {}
|
|
422
|
-
},
|
|
423
|
-
|
|
424
|
-
async fetchAgents() {
|
|
425
|
-
try {
|
|
426
|
-
this.agents = await this.api("GET", "/agents");
|
|
427
|
-
this.agentsById = indexAgents(this.agents);
|
|
428
|
-
} catch {}
|
|
429
|
-
},
|
|
430
|
-
|
|
431
|
-
async fetchOrchestrators() {
|
|
432
|
-
try {
|
|
433
|
-
this.orchestrators = await this.api("GET", "/orchestrators");
|
|
434
|
-
} catch {}
|
|
435
|
-
},
|
|
436
|
-
|
|
437
|
-
async fetchPairs() {
|
|
438
|
-
try {
|
|
439
|
-
let pairs;
|
|
440
|
-
if (this.view === "activity") {
|
|
441
|
-
pairs = await this.api("GET", "/pairs");
|
|
442
|
-
} else if (this.pairStatusFilter === "open") {
|
|
443
|
-
const [active, pending] = await Promise.all([
|
|
444
|
-
this.api("GET", "/pairs?status=active"),
|
|
445
|
-
this.api("GET", "/pairs?status=pending"),
|
|
446
|
-
]);
|
|
447
|
-
pairs = [...active, ...pending];
|
|
448
|
-
} else if (this.pairStatusFilter) {
|
|
449
|
-
pairs = await this.api("GET", "/pairs?status=" + encodeURIComponent(this.pairStatusFilter));
|
|
450
|
-
} else {
|
|
451
|
-
pairs = await this.api("GET", "/pairs");
|
|
452
|
-
}
|
|
453
|
-
this.pairs = pairs.sort(comparePairs);
|
|
454
|
-
} catch {}
|
|
455
|
-
},
|
|
456
|
-
|
|
457
|
-
async fetchMessages() {
|
|
458
|
-
try {
|
|
459
|
-
let path = "/messages?limit=100";
|
|
460
|
-
if (this.view === "messages" && this.selectedAgent) path += "&for=" + encodeURIComponent(this.selectedAgent);
|
|
461
|
-
if (this.view === "messages" && this.channelFilter) path += "&channel=" + encodeURIComponent(this.channelFilter);
|
|
462
|
-
this.messages = await this.api("GET", path);
|
|
463
|
-
} catch {}
|
|
464
|
-
},
|
|
465
|
-
|
|
466
|
-
async fetchTasks() {
|
|
467
|
-
try {
|
|
468
|
-
const params = new URLSearchParams({ limit: "100" });
|
|
469
|
-
if (this.taskStatusFilter) params.set("status", this.taskStatusFilter);
|
|
470
|
-
if (this.taskSourceFilter) params.set("source", this.taskSourceFilter);
|
|
471
|
-
this.tasks = await this.api("GET", "/tasks?" + params.toString());
|
|
472
|
-
} catch {}
|
|
473
|
-
},
|
|
474
|
-
|
|
475
|
-
async fetchIntegrations() {
|
|
476
|
-
try {
|
|
477
|
-
this.integrations = await this.api("GET", "/integrations");
|
|
478
|
-
} catch {}
|
|
479
|
-
},
|
|
480
|
-
|
|
481
|
-
async fetchConnectors() {
|
|
482
|
-
try {
|
|
483
|
-
this.connectors = await this.api("GET", "/connectors");
|
|
484
|
-
} catch {}
|
|
485
|
-
},
|
|
486
|
-
|
|
487
|
-
async fetchChannels() {
|
|
488
|
-
try {
|
|
489
|
-
this.channels = await this.api("GET", "/channels");
|
|
490
|
-
} catch {}
|
|
491
|
-
},
|
|
492
|
-
|
|
493
|
-
async fetchInboxState() {
|
|
494
|
-
try {
|
|
495
|
-
const state = await this.api("GET", "/inbox/state?operatorId=" + encodeURIComponent(INBOX_OPERATOR_ID));
|
|
496
|
-
applyInboxState(this, state);
|
|
497
|
-
} catch {}
|
|
498
|
-
},
|
|
499
|
-
|
|
500
|
-
async fetchActivityEvents() {
|
|
501
|
-
try {
|
|
502
|
-
this.activityEvents = await this.api("GET", "/activity?limit=200");
|
|
503
|
-
pruneSyncedOperatorActivity(this);
|
|
504
|
-
} catch {}
|
|
505
|
-
},
|
|
506
|
-
};
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
function applyInboxState(vm, state) {
|
|
510
|
-
const readCursors = {};
|
|
511
|
-
const archivedThreads = {};
|
|
512
|
-
const drafts = {};
|
|
513
|
-
|
|
514
|
-
for (const thread of state?.threads || []) {
|
|
515
|
-
if (thread.readCursorMessageId) readCursors[thread.peerId] = thread.readCursorMessageId;
|
|
516
|
-
if (thread.archivedAtMessageId) archivedThreads[thread.peerId] = thread.archivedAtMessageId;
|
|
517
|
-
}
|
|
518
|
-
for (const draft of state?.drafts || []) {
|
|
519
|
-
if (draft.body) drafts[draft.peerId] = draft.body;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
vm.inboxReadCursors = readCursors;
|
|
523
|
-
vm.inboxArchivedThreads = archivedThreads;
|
|
524
|
-
vm.inboxDrafts = drafts;
|
|
525
|
-
savePref("inboxReadCursors", readCursors);
|
|
526
|
-
savePref("inboxArchivedThreads", archivedThreads);
|
|
527
|
-
savePref("inboxDrafts", drafts);
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
function pruneSyncedOperatorActivity(vm) {
|
|
531
|
-
const serverClientIds = new Set((vm.activityEvents || []).map((event) => event.clientId).filter(Boolean));
|
|
532
|
-
if (!serverClientIds.size || !vm.operatorActivity?.length) return;
|
|
533
|
-
vm.operatorActivity = vm.operatorActivity.filter((item) => !serverClientIds.has(item.clientId || item.id));
|
|
534
|
-
savePref("operatorActivity", vm.operatorActivity);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
function createComputedDescriptors() {
|
|
538
|
-
return {
|
|
539
|
-
onlineCount: { get: getOnlineCount },
|
|
540
|
-
busyAgentCount: { get: getBusyAgentCount },
|
|
541
|
-
hiddenBuiltInAgentCount: { get: getHiddenBuiltInAgentCount },
|
|
542
|
-
sortedAgents: { get: getSortedAgents },
|
|
543
|
-
pairsByAgentId: { get: getPairsByAgentId },
|
|
544
|
-
selectedAgentDetail: { get: getSelectedAgentDetail },
|
|
545
|
-
agentDetailMessages: { get: getAgentDetailMessages },
|
|
546
|
-
selectedChannelDetail: { get: getSelectedChannelDetail },
|
|
547
|
-
channelDetailMessages: { get: getChannelDetailMessages },
|
|
548
|
-
pairMessagePair: { get: getPairMessagePair },
|
|
549
|
-
allInboxThreads: { get: getAllInboxThreads },
|
|
550
|
-
inboxThreads: { get: getInboxThreads },
|
|
551
|
-
selectedInboxThreadData: { get: getSelectedInboxThreadData },
|
|
552
|
-
selectedInboxMessages: { get: getSelectedInboxMessages },
|
|
553
|
-
inboxComposeTargetOptions: { get: getInboxComposeTargetOptions },
|
|
554
|
-
attentionSummary: { get: getAttentionSummary },
|
|
555
|
-
attentionAgentCount: { get: getAttentionAgentCount },
|
|
556
|
-
activityItems: { get: getActivityItems },
|
|
557
|
-
workQueueItems: { get: getWorkQueueItems },
|
|
558
|
-
channelCards: { get: getChannelCards },
|
|
559
|
-
connectorCards: { get: getConnectorCards },
|
|
560
|
-
integrationCards: { get: getIntegrationCards },
|
|
561
|
-
openPairCount: { get: getOpenPairCount },
|
|
562
|
-
filteredMessages: { get: getFilteredMessages },
|
|
563
|
-
groupedMessages: { get: getGroupedMessages },
|
|
564
|
-
filteredTasks: { get: getFilteredTasks },
|
|
565
|
-
composeAgents: { get: getComposeAgents },
|
|
566
|
-
uniqueLabels: { get: getUniqueLabels },
|
|
567
|
-
uniqueCaps: { get: getUniqueCaps },
|
|
568
|
-
uniqueTags: { get: getUniqueTags },
|
|
569
|
-
healthIssues: { get: getHealthIssues },
|
|
570
|
-
healthDiagnostics: { get: getHealthDiagnostics },
|
|
571
|
-
commandPaletteItems: { get: getCommandPaletteItems },
|
|
572
|
-
spawnAvailableProviders: { get: getSpawnAvailableProviders },
|
|
573
|
-
};
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
function getSpawnAvailableProviders() {
|
|
577
|
-
const orch = this.orchestrators.find((o) => o.id === this.spawnOrchId);
|
|
578
|
-
return orch ? orch.providers : ["claude", "codex"];
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
function getOnlineCount() {
|
|
582
|
-
return visibleAgents(this).filter((agent) => agent.status !== "offline").length;
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
function getBusyAgentCount() {
|
|
586
|
-
return this.agents.filter((agent) => agent.status === "busy").length;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
function getHiddenBuiltInAgentCount() {
|
|
590
|
-
return this.showBuiltIns ? 0 : this.agents.filter(isBuiltInAgent).length;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
function getSortedAgents() {
|
|
594
|
-
let list = visibleAgents(this);
|
|
595
|
-
list = applyAgentPreset(this, list);
|
|
596
|
-
if (!this.showOffline) list = list.filter((agent) => agent.status !== "offline");
|
|
597
|
-
if (this.agentStatusFilter === "starting") {
|
|
598
|
-
list = list.filter((agent) => agent.status !== "offline" && !agent.ready);
|
|
599
|
-
} else if (this.agentStatusFilter) {
|
|
600
|
-
list = list.filter((agent) => agent.status === this.agentStatusFilter);
|
|
601
|
-
}
|
|
602
|
-
if (this.agentTagFilter) {
|
|
603
|
-
list = list.filter((agent) => (agent.tags || []).includes(this.agentTagFilter));
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
const dir = this.agentSortDir === "desc" ? -1 : 1;
|
|
607
|
-
return list.sort((a, b) => compareAgents(this, a, b) * dir);
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
function getPairsByAgentId() {
|
|
611
|
-
const byAgent = {};
|
|
612
|
-
for (const pair of this.pairs || []) {
|
|
613
|
-
if (pair.requesterId) byAgent[pair.requesterId] = pair;
|
|
614
|
-
if (pair.targetId) byAgent[pair.targetId] = pair;
|
|
615
|
-
}
|
|
616
|
-
return byAgent;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
function comparePairs(a, b) {
|
|
620
|
-
return new Date(b.updatedAt || b.createdAt || 0) - new Date(a.updatedAt || a.createdAt || 0);
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
function getSelectedAgentDetail() {
|
|
624
|
-
if (!this.agentDetailId) return null;
|
|
625
|
-
return this.agentsById[this.agentDetailId] || null;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
function getAgentDetailMessages() {
|
|
629
|
-
if (!this.agentDetailId) return [];
|
|
630
|
-
return this.messages
|
|
631
|
-
.filter((msg) => msg.from === this.agentDetailId || msg.to === this.agentDetailId)
|
|
632
|
-
.slice()
|
|
633
|
-
.sort((a, b) => b.id - a.id)
|
|
634
|
-
.slice(0, 8);
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
function getSelectedChannelDetail() {
|
|
638
|
-
if (!this.channelDetailId) return null;
|
|
639
|
-
return (this.channels || []).find((channel) => channel.id === this.channelDetailId) || null;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
function getChannelDetailMessages() {
|
|
643
|
-
const channel = this.selectedChannelDetail;
|
|
644
|
-
if (!channel) return [];
|
|
645
|
-
return this.messages
|
|
646
|
-
.filter((msg) => messageMatchesChannel(msg, channel))
|
|
647
|
-
.slice()
|
|
648
|
-
.sort((a, b) => b.id - a.id)
|
|
649
|
-
.slice(0, 8);
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
function messageMatchesChannel(message, channel) {
|
|
653
|
-
if (!message || !channel) return false;
|
|
654
|
-
const channelKeys = [channel.id, channel.type, ...(channel.topicChannels || [])].filter(Boolean);
|
|
655
|
-
if (message.channel && channelKeys.includes(message.channel)) return true;
|
|
656
|
-
const payloadChannel = message.payload?.channel;
|
|
657
|
-
if (payloadChannel && typeof payloadChannel === "object") {
|
|
658
|
-
const payloadKeys = [payloadChannel.agentId, payloadChannel.provider, payloadChannel.accountId].filter(Boolean);
|
|
659
|
-
if (payloadKeys.includes(channel.id) || payloadKeys.includes(channel.type) || payloadKeys.includes(channel.accountId)) return true;
|
|
660
|
-
}
|
|
661
|
-
if (channel.agentId && (message.from === channel.agentId || message.to === channel.agentId)) return true;
|
|
662
|
-
return false;
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
function getPairMessagePair() {
|
|
666
|
-
return this.pairs.find((pair) => pair.id === this.pairMessage.pairId) || null;
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
function getAllInboxThreads() {
|
|
670
|
-
return buildInboxThreads(this);
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
function getInboxThreads() {
|
|
674
|
-
const search = this.inboxSearch.trim().toLowerCase();
|
|
675
|
-
const filtered = this.allInboxThreads.filter((thread) => {
|
|
676
|
-
if (!this.inboxShowArchived && thread.archived) return false;
|
|
677
|
-
if (search && !threadMatchesSearch(this, thread, search)) return false;
|
|
678
|
-
return true;
|
|
679
|
-
});
|
|
680
|
-
return sortInboxThreads(filtered, this.inboxSort, this.inboxSortDir);
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
function sortInboxThreads(threads, sort, dir) {
|
|
684
|
-
const mul = dir === "asc" ? 1 : -1;
|
|
685
|
-
return [...threads].sort((a, b) => {
|
|
686
|
-
switch (sort) {
|
|
687
|
-
case "updated":
|
|
688
|
-
return mul * ((b.lastMessage?.id || 0) - (a.lastMessage?.id || 0));
|
|
689
|
-
case "created": {
|
|
690
|
-
const aFirst = a.messages[0]?.id || 0;
|
|
691
|
-
const bFirst = b.messages[0]?.id || 0;
|
|
692
|
-
return mul * (bFirst - aFirst);
|
|
693
|
-
}
|
|
694
|
-
case "name":
|
|
695
|
-
return mul * String(a.peer).localeCompare(String(b.peer));
|
|
696
|
-
case "attention":
|
|
697
|
-
default:
|
|
698
|
-
return compareInboxThreads(a, b) * mul;
|
|
699
|
-
}
|
|
700
|
-
});
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
function buildInboxThreads(vm) {
|
|
704
|
-
const threads = new Map();
|
|
705
|
-
for (const msg of vm.messages) {
|
|
706
|
-
const peer = inboxPeer(msg);
|
|
707
|
-
if (!peer) continue;
|
|
708
|
-
if (!threads.has(peer)) threads.set(peer, { id: peer, peer, messages: [], lastMessage: null });
|
|
709
|
-
threads.get(peer).messages.push(msg);
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
for (const thread of threads.values()) {
|
|
713
|
-
thread.messages.sort((a, b) => a.id - b.id);
|
|
714
|
-
thread.lastMessage = thread.messages[thread.messages.length - 1] || null;
|
|
715
|
-
thread.attention = getThreadAttention(vm, thread);
|
|
716
|
-
thread.archived = isInboxThreadArchived(vm, thread);
|
|
717
|
-
thread.draft = draftForPeer(vm, thread.peer);
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
return [...threads.values()].sort(compareInboxThreads);
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
function threadMatchesSearch(vm, thread, search) {
|
|
724
|
-
const haystack = [
|
|
725
|
-
vm.displayTarget(thread.peer),
|
|
726
|
-
thread.peer,
|
|
727
|
-
...thread.messages.flatMap((msg) => [msg.subject || "", messageBody.call(vm, msg) || "", msg.channel || "", vm.displayTarget(msg.from), vm.displayTarget(msg.to)]),
|
|
728
|
-
].join("\n").toLowerCase();
|
|
729
|
-
return haystack.includes(search);
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
function isInboxThreadArchived(vm, thread) {
|
|
733
|
-
const archivedAtId = Number(vm.inboxArchivedThreads?.[thread.peer] || 0);
|
|
734
|
-
return Boolean(thread.lastMessage?.id && archivedAtId >= thread.lastMessage.id);
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
function compareInboxThreads(a, b) {
|
|
738
|
-
const scoreDelta = (b.attention?.score || 0) - (a.attention?.score || 0);
|
|
739
|
-
if (scoreDelta !== 0) return scoreDelta;
|
|
740
|
-
return (b.lastMessage?.id || 0) - (a.lastMessage?.id || 0);
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
function getSelectedInboxThreadData() {
|
|
744
|
-
if (!this.selectedInboxThread) return null;
|
|
745
|
-
return this.inboxThreads.find((thread) => thread.id === this.selectedInboxThread) || null;
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
function getSelectedInboxMessages() {
|
|
749
|
-
return this.selectedInboxThreadData?.messages || [];
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
function getInboxComposeTargetOptions() {
|
|
753
|
-
if (this.inboxCompose.toMode === "tag") return this.uniqueTags.map((value) => ({ value, label: "#" + value }));
|
|
754
|
-
if (this.inboxCompose.toMode === "cap") return this.uniqueCaps.map((value) => ({ value, label: value }));
|
|
755
|
-
return this.composeAgents.map((agent) => ({ value: agent.id, label: `${this.displayName(agent)} [${agent.id.slice(-6)}]` }));
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
function getAttentionSummary() {
|
|
759
|
-
const threads = this.allInboxThreads.filter((thread) => !thread.archived);
|
|
760
|
-
const pendingPairInvites = this.pairs.filter((pair) => pair.status === "pending").length;
|
|
761
|
-
const claimableTasks = countClaimableWaiting(this);
|
|
762
|
-
const unreadInbox = threads.reduce((sum, thread) => sum + (thread.attention?.unread || 0), 0);
|
|
763
|
-
const agentQuestions = threads.filter((thread) => thread.attention?.agentQuestion).length;
|
|
764
|
-
|
|
765
|
-
return {
|
|
766
|
-
unreadInbox,
|
|
767
|
-
needsHumanResponse: 0,
|
|
768
|
-
agentQuestions,
|
|
769
|
-
pendingPairInvites,
|
|
770
|
-
claimableTasks,
|
|
771
|
-
total: unreadInbox + agentQuestions + pendingPairInvites + claimableTasks,
|
|
772
|
-
};
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
function getAttentionAgentCount() {
|
|
776
|
-
return this.sortedAgents.filter((agent) => agentAttention.call(this, agent).total > 0).length;
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
function getActivityItems() {
|
|
780
|
-
const items = [
|
|
781
|
-
...serverActivityItems(this),
|
|
782
|
-
...messageActivityItems(this),
|
|
783
|
-
...pairActivityItems(this),
|
|
784
|
-
...taskActivityItems(this),
|
|
785
|
-
...operatorActivityItems(this),
|
|
786
|
-
].filter((item) => item.ts);
|
|
787
|
-
|
|
788
|
-
const filter = this.activityFilter;
|
|
789
|
-
const filtered = filter ? items.filter((item) => item.kind === filter) : items;
|
|
790
|
-
return filtered
|
|
791
|
-
.sort((a, b) => b.ts - a.ts)
|
|
792
|
-
.slice(0, 150);
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
function getWorkQueueItems() {
|
|
796
|
-
const taskItems = (this.tasks || [])
|
|
797
|
-
.filter((task) => !CLOSED_TASK_STATUSES.has(task.status))
|
|
798
|
-
.map((task) => ({
|
|
799
|
-
id: "task-" + task.id,
|
|
800
|
-
sourceType: "task",
|
|
801
|
-
title: task.title,
|
|
802
|
-
body: task.body,
|
|
803
|
-
severity: task.severity || "info",
|
|
804
|
-
status: task.status,
|
|
805
|
-
owner: task.claimedBy || "",
|
|
806
|
-
target: task.target,
|
|
807
|
-
source: task.source,
|
|
808
|
-
channel: task.channel || "",
|
|
809
|
-
updatedAt: task.updatedAt || task.createdAt,
|
|
810
|
-
createdAt: task.createdAt,
|
|
811
|
-
claimable: isClaimableTaskWaiting(task),
|
|
812
|
-
task,
|
|
813
|
-
}));
|
|
814
|
-
|
|
815
|
-
const messageItems = (this.messages || [])
|
|
816
|
-
.filter(isClaimableMessageWaiting)
|
|
817
|
-
.map((msg) => ({
|
|
818
|
-
id: "message-" + msg.id,
|
|
819
|
-
sourceType: "message",
|
|
820
|
-
title: msg.subject || "Claimable message #" + msg.id,
|
|
821
|
-
body: messageBody(msg),
|
|
822
|
-
severity: "warning",
|
|
823
|
-
status: msg.claimedBy ? "claimed" : "open",
|
|
824
|
-
owner: msg.claimedBy || "",
|
|
825
|
-
target: msg.to,
|
|
826
|
-
source: "message",
|
|
827
|
-
channel: msg.channel || "",
|
|
828
|
-
updatedAt: msg.claimedAt || msg.createdAt,
|
|
829
|
-
createdAt: msg.createdAt,
|
|
830
|
-
claimable: isClaimableMessageWaiting(msg),
|
|
831
|
-
message: msg,
|
|
832
|
-
}));
|
|
833
|
-
|
|
834
|
-
return [...taskItems, ...messageItems].sort(compareWorkQueueItems);
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
function compareWorkQueueItems(a, b) {
|
|
838
|
-
const claimableDelta = Number(b.claimable) - Number(a.claimable);
|
|
839
|
-
if (claimableDelta !== 0) return claimableDelta;
|
|
840
|
-
const severityOrder = { critical: 0, warning: 1, info: 2 };
|
|
841
|
-
const severityDelta = (severityOrder[a.severity] ?? 9) - (severityOrder[b.severity] ?? 9);
|
|
842
|
-
if (severityDelta !== 0) return severityDelta;
|
|
843
|
-
return toTimestamp(a.updatedAt) - toTimestamp(b.updatedAt);
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
function serverActivityItems(vm) {
|
|
847
|
-
return (vm.activityEvents || []).map((event) => activityItem({
|
|
848
|
-
id: "activity-" + event.id,
|
|
849
|
-
clientId: event.clientId,
|
|
850
|
-
kind: event.kind,
|
|
851
|
-
ts: toTimestamp(event.createdAt),
|
|
852
|
-
icon: event.icon,
|
|
853
|
-
title: event.title,
|
|
854
|
-
body: event.body,
|
|
855
|
-
meta: event.meta,
|
|
856
|
-
view: event.view,
|
|
857
|
-
peer: event.peer,
|
|
858
|
-
messageId: event.messageId,
|
|
859
|
-
pairId: event.pairId,
|
|
860
|
-
taskId: event.taskId,
|
|
861
|
-
agentId: event.agentId,
|
|
862
|
-
}));
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
function messageActivityItems(vm) {
|
|
866
|
-
return (vm.messages || []).flatMap((msg) => {
|
|
867
|
-
const ts = toTimestamp(msg.createdAt);
|
|
868
|
-
const items = [];
|
|
869
|
-
const pairEvent = msg.payload?.pairEvent;
|
|
870
|
-
if (pairEvent) {
|
|
871
|
-
items.push(activityItem({
|
|
872
|
-
id: "pair-message-" + msg.id,
|
|
873
|
-
kind: pairEvent === "message" ? "pair" : "state",
|
|
874
|
-
ts,
|
|
875
|
-
icon: pairEvent === "message" ? "ti-messages" : "ti-link",
|
|
876
|
-
title: pairEvent === "message" ? "Pair message" : "Pair " + pairEvent,
|
|
877
|
-
body: vm.messagePreview(msg),
|
|
878
|
-
meta: `${vm.displayTarget(msg.from)} -> ${vm.displayTarget(msg.to)}`,
|
|
879
|
-
view: "pairs",
|
|
880
|
-
messageId: msg.id,
|
|
881
|
-
}));
|
|
882
|
-
} else if (msg.from === HUMAN_AGENT_ID) {
|
|
883
|
-
items.push(activityItem({
|
|
884
|
-
id: "human-send-" + msg.id,
|
|
885
|
-
kind: msg.kind === "task" ? "task" : "message",
|
|
886
|
-
ts,
|
|
887
|
-
icon: msg.kind === "task" ? "ti-hand-grab" : "ti-send",
|
|
888
|
-
title: msg.kind === "task" ? "Claimable task sent" : "Message sent",
|
|
889
|
-
body: vm.messagePreview(msg),
|
|
890
|
-
meta: "to " + vm.displayTarget(msg.to),
|
|
891
|
-
view: inboxPeer(msg) ? "inbox" : "messages",
|
|
892
|
-
peer: inboxPeer(msg),
|
|
893
|
-
messageId: msg.id,
|
|
894
|
-
}));
|
|
895
|
-
} else if (msg.to === HUMAN_AGENT_ID) {
|
|
896
|
-
items.push(activityItem({
|
|
897
|
-
id: "agent-reply-" + msg.id,
|
|
898
|
-
kind: messageLooksLikeQuestion(msg) ? "question" : "reply",
|
|
899
|
-
ts,
|
|
900
|
-
icon: messageLooksLikeQuestion(msg) ? "ti-help-circle" : "ti-message-reply",
|
|
901
|
-
title: messageLooksLikeQuestion(msg) ? "Agent asked a question" : "Agent replied",
|
|
902
|
-
body: vm.messagePreview(msg),
|
|
903
|
-
meta: "from " + vm.displayTarget(msg.from),
|
|
904
|
-
view: "inbox",
|
|
905
|
-
peer: msg.from,
|
|
906
|
-
agentId: msg.from,
|
|
907
|
-
messageId: msg.id,
|
|
908
|
-
}));
|
|
909
|
-
} else {
|
|
910
|
-
items.push(activityItem({
|
|
911
|
-
id: "message-" + msg.id,
|
|
912
|
-
kind: "message",
|
|
913
|
-
ts,
|
|
914
|
-
icon: "ti-messages",
|
|
915
|
-
title: "Agent message",
|
|
916
|
-
body: vm.messagePreview(msg),
|
|
917
|
-
meta: `${vm.displayTarget(msg.from)} -> ${vm.displayTarget(msg.to)}`,
|
|
918
|
-
view: "messages",
|
|
919
|
-
messageId: msg.id,
|
|
920
|
-
}));
|
|
921
|
-
}
|
|
922
|
-
if (msg.claimedBy) {
|
|
923
|
-
items.push(activityItem({
|
|
924
|
-
id: "claim-" + msg.id,
|
|
925
|
-
kind: "task",
|
|
926
|
-
ts: toTimestamp(msg.claimedAt || msg.createdAt),
|
|
927
|
-
icon: "ti-user-check",
|
|
928
|
-
title: "Task claimed",
|
|
929
|
-
body: vm.messagePreview(msg),
|
|
930
|
-
meta: "by " + vm.displayTarget(msg.claimedBy),
|
|
931
|
-
view: "tasks",
|
|
932
|
-
messageId: msg.id,
|
|
933
|
-
agentId: msg.claimedBy,
|
|
934
|
-
}));
|
|
935
|
-
}
|
|
936
|
-
return items;
|
|
937
|
-
});
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
function pairActivityItems(vm) {
|
|
941
|
-
return (vm.pairs || []).flatMap((pair) => {
|
|
942
|
-
const created = activityItem({
|
|
943
|
-
id: "pair-created-" + pair.id,
|
|
944
|
-
kind: "pair",
|
|
945
|
-
ts: toTimestamp(pair.createdAt),
|
|
946
|
-
icon: "ti-link-plus",
|
|
947
|
-
title: "Pair invite created",
|
|
948
|
-
body: pair.objective || "",
|
|
949
|
-
meta: `${vm.displayTarget(pair.requesterId)} <-> ${vm.displayTarget(pair.targetId)}`,
|
|
950
|
-
view: "pairs",
|
|
951
|
-
pairId: pair.id,
|
|
952
|
-
});
|
|
953
|
-
const statusTs = toTimestamp(pair.updatedAt || pair.acceptedAt || pair.endedAt);
|
|
954
|
-
if (!statusTs || statusTs === created.ts || pair.status === "pending") return [created];
|
|
955
|
-
return [created, activityItem({
|
|
956
|
-
id: "pair-status-" + pair.id + "-" + pair.status,
|
|
957
|
-
kind: "state",
|
|
958
|
-
ts: statusTs,
|
|
959
|
-
icon: pair.status === "active" ? "ti-link" : "ti-phone-off",
|
|
960
|
-
title: "Pair " + pair.status,
|
|
961
|
-
body: pair.objective || "",
|
|
962
|
-
meta: pair.endedBy ? "by " + vm.displayTarget(pair.endedBy) : `${vm.displayTarget(pair.requesterId)} <-> ${vm.displayTarget(pair.targetId)}`,
|
|
963
|
-
view: "pairs",
|
|
964
|
-
pairId: pair.id,
|
|
965
|
-
})];
|
|
966
|
-
});
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
function taskActivityItems(vm) {
|
|
970
|
-
return (vm.tasks || []).map((task) => activityItem({
|
|
971
|
-
id: "task-" + task.id + "-" + task.status,
|
|
972
|
-
kind: "task",
|
|
973
|
-
ts: toTimestamp(task.updatedAt || task.createdAt),
|
|
974
|
-
icon: task.claimedBy ? "ti-user-check" : "ti-checkup-list",
|
|
975
|
-
title: task.status === "open" ? "Claimable task waiting" : "Task " + task.status,
|
|
976
|
-
body: task.title || task.body || "",
|
|
977
|
-
meta: task.claimedBy ? "claimed by " + vm.displayTarget(task.claimedBy) : vm.displayTarget(task.target),
|
|
978
|
-
view: "tasks",
|
|
979
|
-
taskId: task.id,
|
|
980
|
-
agentId: task.claimedBy || task.target,
|
|
981
|
-
}));
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
function operatorActivityItems(vm) {
|
|
985
|
-
const serverClientIds = new Set((vm.activityEvents || []).map((event) => event.clientId).filter(Boolean));
|
|
986
|
-
return (vm.operatorActivity || []).map((item) => activityItem({
|
|
987
|
-
...item,
|
|
988
|
-
id: item.id || "operator-" + item.ts + "-" + item.title,
|
|
989
|
-
clientId: item.clientId || item.id,
|
|
990
|
-
})).filter((item) => !serverClientIds.has(item.clientId));
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
function activityItem(input) {
|
|
994
|
-
return {
|
|
995
|
-
id: input.id,
|
|
996
|
-
kind: input.kind || "state",
|
|
997
|
-
ts: Number(input.ts) || 0,
|
|
998
|
-
icon: input.icon || "ti-activity",
|
|
999
|
-
title: input.title || "Activity",
|
|
1000
|
-
body: input.body || "",
|
|
1001
|
-
meta: input.meta || "",
|
|
1002
|
-
view: input.view || "",
|
|
1003
|
-
peer: input.peer || "",
|
|
1004
|
-
clientId: input.clientId || "",
|
|
1005
|
-
messageId: input.messageId,
|
|
1006
|
-
pairId: input.pairId,
|
|
1007
|
-
taskId: input.taskId,
|
|
1008
|
-
agentId: input.agentId,
|
|
1009
|
-
};
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
function toTimestamp(value) {
|
|
1013
|
-
const ts = typeof value === "number" ? value : new Date(value || 0).getTime();
|
|
1014
|
-
return Number.isFinite(ts) ? ts : 0;
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
function inboxPeer(msg) {
|
|
1018
|
-
if (msg.from === HUMAN_AGENT_ID && msg.to) return msg.to;
|
|
1019
|
-
if (msg.to === HUMAN_AGENT_ID && msg.from) return msg.from;
|
|
1020
|
-
return "";
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
function getThreadAttention(vm, thread) {
|
|
1024
|
-
const lastHumanReplyId = maxMessageId(thread.messages, (msg) => msg.from === HUMAN_AGENT_ID);
|
|
1025
|
-
const unread = thread.messages.filter((msg) => isUnreadHumanMessage(vm, thread.peer, msg)).length;
|
|
1026
|
-
const agentQuestion = thread.messages.some((msg) =>
|
|
1027
|
-
isHumanInboundMessage(msg) && msg.id > lastHumanReplyId && messageLooksLikeQuestion(msg)
|
|
1028
|
-
);
|
|
1029
|
-
|
|
1030
|
-
return {
|
|
1031
|
-
unread,
|
|
1032
|
-
needsHumanResponse: false,
|
|
1033
|
-
agentQuestion,
|
|
1034
|
-
score: unread * 10 + (agentQuestion ? 3 : 0),
|
|
1035
|
-
};
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
function maxMessageId(messages, predicate) {
|
|
1039
|
-
let max = 0;
|
|
1040
|
-
for (const msg of messages) {
|
|
1041
|
-
if (predicate(msg) && msg.id > max) max = msg.id;
|
|
1042
|
-
}
|
|
1043
|
-
return max;
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
function isHumanInboundMessage(msg) {
|
|
1047
|
-
return msg.to === HUMAN_AGENT_ID && msg.from !== HUMAN_AGENT_ID;
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
function isUnreadHumanMessage(vm, peer, msg) {
|
|
1051
|
-
if (!isHumanInboundMessage(msg)) return false;
|
|
1052
|
-
if ((msg.readBy || []).includes(HUMAN_AGENT_ID)) return false;
|
|
1053
|
-
return msg.id > readCursorForPeer(vm, peer);
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
function readCursorForPeer(vm, peer) {
|
|
1057
|
-
const value = Number(vm.inboxReadCursors?.[peer] || 0);
|
|
1058
|
-
return Number.isFinite(value) ? value : 0;
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
function draftForPeer(vm, peer) {
|
|
1062
|
-
return typeof vm.inboxDrafts?.[peer] === "string" ? vm.inboxDrafts[peer] : "";
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
function messageLooksLikeQuestion(msg) {
|
|
1066
|
-
return /\?/.test(`${msg.subject || ""}\n${messageBody(msg)}`);
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
function countClaimableWaiting(vm) {
|
|
1070
|
-
const taskCount = vm.tasks.filter(isClaimableTaskWaiting).length;
|
|
1071
|
-
const messageCount = vm.messages.filter(isClaimableMessageWaiting).length;
|
|
1072
|
-
return taskCount + messageCount;
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
function countAgentClaimableWaiting(vm, agent) {
|
|
1076
|
-
const taskCount = vm.tasks.filter((task) => isClaimableTaskWaiting(task) && targetMatchesAgent(task.target, agent)).length;
|
|
1077
|
-
const messageCount = vm.messages.filter((msg) => isClaimableMessageWaiting(msg) && targetMatchesAgent(msg.to, agent)).length;
|
|
1078
|
-
return taskCount + messageCount;
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
function isClaimableTaskWaiting(task) {
|
|
1082
|
-
return WAITING_TASK_STATUSES.has(task.status) && !task.claimedBy;
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
function isClaimableMessageWaiting(msg) {
|
|
1086
|
-
return Boolean(msg.claimable && !msg.claimedBy && !(msg.kind === "task" && Number.isSafeInteger(msg.payload?.taskId)));
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
function targetMatchesAgent(target, agent) {
|
|
1090
|
-
if (!target || !agent) return false;
|
|
1091
|
-
if (target === "broadcast" || target === agent.id) return true;
|
|
1092
|
-
if (target.startsWith("tag:")) return (agent.tags || []).includes(target.slice(4));
|
|
1093
|
-
if (target.startsWith("cap:")) return (agent.capabilities || []).includes(target.slice(4));
|
|
1094
|
-
if (target.startsWith("label:")) return agent.label === target.slice(6);
|
|
1095
|
-
return false;
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
function getFilteredMessages() {
|
|
1099
|
-
if (!this.tagFilter) return this.messages;
|
|
1100
|
-
return this.messages.filter((msg) => messageMatchesTag(this, msg, this.tagFilter));
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
function messageMatchesTag(vm, msg, tag) {
|
|
1104
|
-
if (msg.to === "tag:" + tag) return true;
|
|
1105
|
-
if (vm.agentsById[msg.from]?.tags?.includes(tag)) return true;
|
|
1106
|
-
if (vm.agentsById[msg.to]?.tags?.includes(tag)) return true;
|
|
1107
|
-
return false;
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
function getGroupedMessages() {
|
|
1111
|
-
const threads = new Map();
|
|
1112
|
-
for (const msg of this.filteredMessages) {
|
|
1113
|
-
const threadId = msg.threadId || msg.id;
|
|
1114
|
-
if (!threads.has(threadId)) threads.set(threadId, { threadId, messages: [] });
|
|
1115
|
-
threads.get(threadId).messages.push(msg);
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
for (const group of threads.values()) {
|
|
1119
|
-
group.messages.sort((a, b) => a.id - b.id);
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
return [...threads.values()].sort(compareThreadGroups);
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
function compareThreadGroups(a, b) {
|
|
1126
|
-
const aLast = a.messages[a.messages.length - 1].id;
|
|
1127
|
-
const bLast = b.messages[b.messages.length - 1].id;
|
|
1128
|
-
return bLast - aLast;
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
function getFilteredTasks() {
|
|
1132
|
-
if (this.taskStatusFilter) return this.tasks;
|
|
1133
|
-
return this.tasks.filter((task) => !CLOSED_TASK_STATUSES.has(task.status));
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
function getIntegrationCards() {
|
|
1137
|
-
return [...(this.integrations || [])].sort((a, b) => {
|
|
1138
|
-
const aStats = a.taskStats || {};
|
|
1139
|
-
const bStats = b.taskStats || {};
|
|
1140
|
-
const openDiff = (bStats.openTasks || 0) - (aStats.openTasks || 0);
|
|
1141
|
-
if (openDiff !== 0) return openDiff;
|
|
1142
|
-
const waitingDiff = (bStats.waitingTasks || 0) - (aStats.waitingTasks || 0);
|
|
1143
|
-
if (waitingDiff !== 0) return waitingDiff;
|
|
1144
|
-
return String(a.name || "").localeCompare(String(b.name || ""));
|
|
1145
|
-
});
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
function getChannelCards() {
|
|
1149
|
-
return [...(this.channels || [])].sort((a, b) => {
|
|
1150
|
-
const healthRank = { error: 0, warning: 1, ok: 2 };
|
|
1151
|
-
const aHealth = a.targetHealth?.status || "ok";
|
|
1152
|
-
const bHealth = b.targetHealth?.status || "ok";
|
|
1153
|
-
const healthDiff = (healthRank[aHealth] ?? 2) - (healthRank[bHealth] ?? 2);
|
|
1154
|
-
if (healthDiff !== 0) return healthDiff;
|
|
1155
|
-
const readyDiff = Number(Boolean(b.ready)) - Number(Boolean(a.ready));
|
|
1156
|
-
if (readyDiff !== 0) return readyDiff;
|
|
1157
|
-
const statusDiff = String(a.status || "").localeCompare(String(b.status || ""));
|
|
1158
|
-
if (statusDiff !== 0) return statusDiff;
|
|
1159
|
-
return String(a.name || "").localeCompare(String(b.name || ""));
|
|
1160
|
-
});
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
function getConnectorCards() {
|
|
1164
|
-
return [...(this.connectors || [])].sort((a, b) => {
|
|
1165
|
-
const statusRank = { error: 0, warn: 1, unknown: 2, ok: 3 };
|
|
1166
|
-
const aStatus = a.runtime?.status || "unknown";
|
|
1167
|
-
const bStatus = b.runtime?.status || "unknown";
|
|
1168
|
-
const statusDiff = (statusRank[aStatus] ?? 2) - (statusRank[bStatus] ?? 2);
|
|
1169
|
-
if (statusDiff !== 0) return statusDiff;
|
|
1170
|
-
return String(a.displayName || a.id || "").localeCompare(String(b.displayName || b.id || ""));
|
|
1171
|
-
});
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
function getOpenPairCount() {
|
|
1175
|
-
return (this.pairs || []).filter(isOpenPair).length;
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
function isOpenPair(pair) {
|
|
1179
|
-
return pair?.status === "active" || pair?.status === "pending";
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
function getComposeAgents() {
|
|
1183
|
-
const list = visibleAgents(this);
|
|
1184
|
-
return this.showOffline ? list : list.filter((agent) => agent.status !== "offline");
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
function composeTargetAllowsClaimable() {
|
|
1188
|
-
return matchingComposeRecipientCount(this, this.compose.to) > 1;
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
function inboxComposeTargetAllowsClaimable() {
|
|
1192
|
-
return matchingComposeRecipientCount(this, inboxComposeTarget(this)) > 1;
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
function matchingComposeRecipientCount(vm, target) {
|
|
1196
|
-
if (!target) return 0;
|
|
1197
|
-
const candidates = vm.composeAgents.filter((agent) => !isBuiltInAgent(agent) && !isChannelAgent(agent));
|
|
1198
|
-
if (target === "broadcast") return candidates.length;
|
|
1199
|
-
if (vm.agentsById[target]) return 1;
|
|
1200
|
-
if (target.startsWith("tag:")) {
|
|
1201
|
-
const tag = target.slice(4);
|
|
1202
|
-
return candidates.filter((agent) => (agent.tags || []).includes(tag)).length;
|
|
1203
|
-
}
|
|
1204
|
-
if (target.startsWith("cap:")) {
|
|
1205
|
-
const cap = target.slice(4);
|
|
1206
|
-
return candidates.filter((agent) => (agent.capabilities || []).includes(cap)).length;
|
|
1207
|
-
}
|
|
1208
|
-
if (target.startsWith("label:")) {
|
|
1209
|
-
const label = target.slice(6);
|
|
1210
|
-
return candidates.filter((agent) => agent.label === label).length;
|
|
1211
|
-
}
|
|
1212
|
-
return 1;
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
function getUniqueLabels() {
|
|
1216
|
-
return [...new Set(visibleAgents(this).filter((agent) => agent.label).map((agent) => agent.label))];
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
function getUniqueCaps() {
|
|
1220
|
-
return [...new Set(visibleAgents(this).flatMap((agent) => agent.capabilities || []))];
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
function getUniqueTags() {
|
|
1224
|
-
return [...new Set(visibleAgents(this).flatMap((agent) => agent.tags || []))];
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
function visibleAgents(vm) {
|
|
1228
|
-
const nonChannelAgents = vm.agents.filter((agent) => !isChannelAgent(agent));
|
|
1229
|
-
return vm.showBuiltIns ? nonChannelAgents : nonChannelAgents.filter((agent) => !isBuiltInAgent(agent));
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
function isBuiltInAgent(agent) {
|
|
1233
|
-
return agent?.meta?.builtin === true || agent?.id === HUMAN_AGENT_ID || agent?.id === "system";
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
function isChannelAgent(agent) {
|
|
1237
|
-
return agent?.kind === "channel" || agent?.meta?.kind === "channel" || agent?.tags?.includes("channel");
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
function agentSupportsControlActions(agent) {
|
|
1241
|
-
return Boolean(agent && !isBuiltInAgent(agent) && !isChannelAgent(agent));
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
function applyAgentPreset(vm, list) {
|
|
1245
|
-
switch (vm.agentPresetFilter) {
|
|
1246
|
-
case "active":
|
|
1247
|
-
return list.filter((agent) => agent.status !== "offline");
|
|
1248
|
-
case "offline_stale":
|
|
1249
|
-
return list.filter((agent) => agent.status === "offline" || isAgentStale(vm, agent));
|
|
1250
|
-
case "claude":
|
|
1251
|
-
case "codex":
|
|
1252
|
-
return list.filter((agent) => agentType(agent) === vm.agentPresetFilter);
|
|
1253
|
-
case "paired":
|
|
1254
|
-
return list.filter((agent) => Boolean(vm.agentPair(agent)));
|
|
1255
|
-
case "unpaired":
|
|
1256
|
-
return list.filter((agent) => !vm.agentPair(agent));
|
|
1257
|
-
case "waiting":
|
|
1258
|
-
return list.filter((agent) => agentAttention.call(vm, agent).total > 0);
|
|
1259
|
-
case "claimable":
|
|
1260
|
-
return list.filter((agent) => agentAttention.call(vm, agent).claimableTasks > 0);
|
|
1261
|
-
case "errors":
|
|
1262
|
-
return list.filter((agent) => agent.status === "offline" || (agent.status !== "offline" && !agent.ready) || isAgentStale(vm, agent));
|
|
1263
|
-
default:
|
|
1264
|
-
return list;
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
function isAgentStale(vm, agent) {
|
|
1269
|
-
if (!agent?.lastSeen || agent.status === "offline") return false;
|
|
1270
|
-
if (agent.id === "user" || agent.id === "system") return false;
|
|
1271
|
-
const lastSeenMs = new Date(agent.lastSeen).getTime();
|
|
1272
|
-
if (!Number.isFinite(lastSeenMs)) return false;
|
|
1273
|
-
return (vm.now || Date.now()) - lastSeenMs > 60_000;
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
function getHealthIssues() {
|
|
1277
|
-
return (this.health?.checks || []).filter((check) => check.status !== "ok");
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
function getHealthDiagnostics() {
|
|
1281
|
-
return this.healthIssues.map((check) => {
|
|
1282
|
-
const base = {
|
|
1283
|
-
name: check.name,
|
|
1284
|
-
status: check.status,
|
|
1285
|
-
detail: check.detail || check.name,
|
|
1286
|
-
impact: healthImpact(check),
|
|
1287
|
-
actions: [
|
|
1288
|
-
{ label: "Inspect logs", icon: "ti-file-search", copy: "agent-relay daemon logs" },
|
|
1289
|
-
{ label: "Restart daemon", icon: "ti-refresh", copy: "agent-relay daemon restart" },
|
|
1290
|
-
{ label: "Copy env", icon: "ti-copy", copy: "agent-relay doctor" },
|
|
1291
|
-
],
|
|
1292
|
-
};
|
|
1293
|
-
if (check.name === "stale-live-agents") {
|
|
1294
|
-
base.actions.unshift(
|
|
1295
|
-
{ label: "Run reaper", icon: "ti-broom", api: "POST", path: "/system/reap" },
|
|
1296
|
-
{ label: "Show stale", icon: "ti-filter", view: "agents", preset: "offline_stale" }
|
|
1297
|
-
);
|
|
1298
|
-
} else if (check.name === "channel-delivery-targets") {
|
|
1299
|
-
base.actions.unshift(
|
|
1300
|
-
{ label: "Open channels", icon: "ti-messages", view: "channels" },
|
|
1301
|
-
{ label: "Run reaper", icon: "ti-broom", api: "POST", path: "/system/reap" }
|
|
1302
|
-
);
|
|
1303
|
-
} else if (check.name === "expired-message-claims" || check.name === "expired-task-claims" || check.name === "offline-claimed-tasks") {
|
|
1304
|
-
base.actions.unshift(
|
|
1305
|
-
{ label: "Run reaper", icon: "ti-broom", api: "POST", path: "/system/reap" },
|
|
1306
|
-
{ label: "Open work", icon: "ti-list-check", view: "work" }
|
|
1307
|
-
);
|
|
1308
|
-
}
|
|
1309
|
-
return base;
|
|
1310
|
-
});
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
function healthImpact(check) {
|
|
1314
|
-
if (check.name === "database") return "Relay persistence is unavailable; messages, state, and audit writes may fail.";
|
|
1315
|
-
if (check.name === "stale-live-agents") return "Agents may look online even though their heartbeat has stopped.";
|
|
1316
|
-
if (check.name === "expired-message-claims") return "Claimable messages may be stuck until the reaper releases expired claims.";
|
|
1317
|
-
if (check.name === "expired-task-claims") return "Tasks can appear owned by agents that no longer hold a live lease.";
|
|
1318
|
-
if (check.name === "offline-claimed-tasks") return "Offline agents are still shown as owners for active work.";
|
|
1319
|
-
if (check.name === "channel-delivery-targets") return "Inbound channel messages may be accepted but routed to no live delivery agent.";
|
|
1320
|
-
return "Relay health is degraded for this check.";
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
function getCommandPaletteItems() {
|
|
1324
|
-
const query = this.commandQuery.trim().toLowerCase();
|
|
1325
|
-
const commands = [
|
|
1326
|
-
commandItem("open-inbox", "Open inbox", "Human console", "ti-inbox", "openView", { view: "inbox" }),
|
|
1327
|
-
commandItem("open-work", "Open work queue", "Claimable messages and tasks", "ti-list-check", "openView", { view: "work" }),
|
|
1328
|
-
commandItem("show-stale-agents", "Show stale agents", "Filter agents to offline or stale heartbeat", "ti-filter", "agentPreset", { preset: "offline_stale" }),
|
|
1329
|
-
commandItem("copy-relay-url", "Copy relay URL", baseUrl(), "ti-copy", "copy", { value: baseUrl() }),
|
|
1330
|
-
commandItem("export-timeline-md", "Export full timeline as Markdown", "Activity audit trace", "ti-file-export", "exportActivity", { format: "markdown" }),
|
|
1331
|
-
commandItem("export-timeline-json", "Export full timeline as JSON", "Activity audit trace", "ti-braces", "exportActivity", { format: "json" }),
|
|
1332
|
-
...this.composeAgents.slice(0, 12).map((agent) =>
|
|
1333
|
-
commandItem("message-" + agent.id, "Message agent: " + this.displayName(agent), agent.id, "ti-send", "messageAgent", { agentId: agent.id })
|
|
1334
|
-
),
|
|
1335
|
-
...this.composeAgents.filter((agent) => agentType(agent) === "codex").slice(0, 8).map((agent) =>
|
|
1336
|
-
commandItem("pair-codex-" + agent.id, "Pair Codex: " + this.displayName(agent), agent.id, "ti-link-plus", "pairAgent", { agentId: agent.id })
|
|
1337
|
-
),
|
|
1338
|
-
...this.uniqueTags.map((tag) =>
|
|
1339
|
-
commandItem("filter-tag-" + tag, "Filter tag: " + tag, "#" + tag, "ti-tag", "filterTag", { tag })
|
|
1340
|
-
),
|
|
1341
|
-
];
|
|
1342
|
-
if (!query) return commands.slice(0, 24);
|
|
1343
|
-
return commands.filter((command) => command.search.includes(query)).slice(0, 24);
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
function commandItem(id, title, subtitle, icon, action, payload) {
|
|
1347
|
-
return {
|
|
1348
|
-
id,
|
|
1349
|
-
title,
|
|
1350
|
-
subtitle,
|
|
1351
|
-
icon,
|
|
1352
|
-
action,
|
|
1353
|
-
payload: payload || {},
|
|
1354
|
-
search: `${title}\n${subtitle || ""}\n${action}`.toLowerCase(),
|
|
1355
|
-
};
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
function compareAgents(vm, a, b) {
|
|
1359
|
-
if (vm.agentSort === "status") {
|
|
1360
|
-
const attentionDelta = agentAttention.call(vm, b).score - agentAttention.call(vm, a).score;
|
|
1361
|
-
if (attentionDelta !== 0) return attentionDelta;
|
|
1362
|
-
}
|
|
1363
|
-
switch (vm.agentSort) {
|
|
1364
|
-
case "name":
|
|
1365
|
-
return vm.displayName(a).localeCompare(vm.displayName(b));
|
|
1366
|
-
case "status":
|
|
1367
|
-
return (STATUS_SORT_ORDER[a.status] ?? 9) - (STATUS_SORT_ORDER[b.status] ?? 9);
|
|
1368
|
-
case "lastSeen":
|
|
1369
|
-
return new Date(b.lastSeen) - new Date(a.lastSeen);
|
|
1370
|
-
case "created":
|
|
1371
|
-
return new Date(b.createdAt) - new Date(a.createdAt);
|
|
1372
|
-
default:
|
|
1373
|
-
return 0;
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
function createDisplayMethods() {
|
|
1378
|
-
return {
|
|
1379
|
-
displayName,
|
|
1380
|
-
displayTarget,
|
|
1381
|
-
conversationTitle,
|
|
1382
|
-
messageBody,
|
|
1383
|
-
messagePreview,
|
|
1384
|
-
agentPair,
|
|
1385
|
-
pairPeerId,
|
|
1386
|
-
pairBadgeClass,
|
|
1387
|
-
pairStatusClass,
|
|
1388
|
-
pairBadgeLabel,
|
|
1389
|
-
pairTitle,
|
|
1390
|
-
agentAttention,
|
|
1391
|
-
agentAttentionTitle,
|
|
1392
|
-
agentType,
|
|
1393
|
-
agentTypeIcon,
|
|
1394
|
-
agentTypeTitle,
|
|
1395
|
-
agentPresence,
|
|
1396
|
-
agentPresenceBadges,
|
|
1397
|
-
agentStatusClass,
|
|
1398
|
-
severityClass,
|
|
1399
|
-
agentStatusTitle,
|
|
1400
|
-
isBuiltInAgent,
|
|
1401
|
-
agentChannels,
|
|
1402
|
-
channelPresence,
|
|
1403
|
-
connectorPresence,
|
|
1404
|
-
integrationPresence,
|
|
1405
|
-
composeTargetAllowsClaimable,
|
|
1406
|
-
inboxComposeTargetAllowsClaimable,
|
|
1407
|
-
agentSupportsControlActions,
|
|
1408
|
-
timeAgo,
|
|
1409
|
-
fmtTime,
|
|
1410
|
-
healthAlertClass,
|
|
1411
|
-
activityKindClass,
|
|
1412
|
-
};
|
|
1413
|
-
}
|
|
1414
|
-
|
|
1415
|
-
function displayName(agent) {
|
|
1416
|
-
if (!agent) return "?";
|
|
1417
|
-
return agent.label || agent.name || agent.id.slice(-12);
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
function displayTarget(target) {
|
|
1421
|
-
if (!target) return "?";
|
|
1422
|
-
if (target === "broadcast") return "broadcast";
|
|
1423
|
-
if (target.startsWith("tag:")) return "#" + target.slice(4);
|
|
1424
|
-
if (target.startsWith("cap:")) return target.slice(4);
|
|
1425
|
-
if (target.startsWith("label:")) return target.slice(6);
|
|
1426
|
-
|
|
1427
|
-
const agent = this.agentsById[target];
|
|
1428
|
-
return agent ? this.displayName(agent) : target.slice(-8);
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
function conversationTitle(thread) {
|
|
1432
|
-
if (!thread) return "Inbox";
|
|
1433
|
-
return this.displayTarget(thread.peer);
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
function messagePreview(msg) {
|
|
1437
|
-
const text = msg?.subject || messageBody(msg) || "";
|
|
1438
|
-
return text.length > 90 ? text.slice(0, 90) + "..." : text;
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
function messageBody(msg) {
|
|
1442
|
-
if (!msg) return "";
|
|
1443
|
-
const payload = msg.payload || {};
|
|
1444
|
-
const channelMessage = payload.message;
|
|
1445
|
-
if (channelMessage && typeof channelMessage === "object" && typeof channelMessage.text === "string" && channelMessage.text.trim()) {
|
|
1446
|
-
return channelMessage.text;
|
|
1447
|
-
}
|
|
1448
|
-
const interaction = payload.interaction;
|
|
1449
|
-
if (interaction && typeof interaction === "object") {
|
|
1450
|
-
const title = typeof interaction.title === "string" ? interaction.title.trim() : "";
|
|
1451
|
-
const description = typeof interaction.description === "string" ? interaction.description.trim() : "";
|
|
1452
|
-
if (title && description) return title + "\n" + description;
|
|
1453
|
-
if (title) return title;
|
|
1454
|
-
if (description) return description;
|
|
1455
|
-
}
|
|
1456
|
-
const reaction = payload.reaction;
|
|
1457
|
-
if (reaction && typeof reaction === "object") {
|
|
1458
|
-
const name = typeof reaction.name === "string" ? reaction.name : "";
|
|
1459
|
-
const emoji = typeof reaction.emoji === "string" ? reaction.emoji : "";
|
|
1460
|
-
const value = typeof reaction.value === "string" ? reaction.value : "";
|
|
1461
|
-
return ["Reaction", emoji || name || value].filter(Boolean).join(": ");
|
|
1462
|
-
}
|
|
1463
|
-
const activity = payload.activity;
|
|
1464
|
-
if (activity && typeof activity === "object") {
|
|
1465
|
-
const kind = typeof activity.kind === "string" ? activity.kind : "activity";
|
|
1466
|
-
const state = typeof activity.state === "string" ? activity.state : "";
|
|
1467
|
-
return [kind, state].filter(Boolean).join(" ");
|
|
1468
|
-
}
|
|
1469
|
-
if (typeof payload.text === "string" && payload.text.trim()) return payload.text;
|
|
1470
|
-
if (typeof payload.message === "string" && payload.message.trim()) return payload.message;
|
|
1471
|
-
return msg.body || "";
|
|
1472
|
-
}
|
|
1473
|
-
|
|
1474
|
-
function agentPair(agent) {
|
|
1475
|
-
return agent ? this.pairsByAgentId[agent.id] : null;
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
function pairPeerId(pair, agentId) {
|
|
1479
|
-
if (!pair) return "";
|
|
1480
|
-
return pair.requesterId === agentId ? pair.targetId : pair.requesterId;
|
|
1481
|
-
}
|
|
1482
|
-
|
|
1483
|
-
function pairBadgeClass(pair) {
|
|
1484
|
-
if (pair?.status === "active") return "bg-success-lt";
|
|
1485
|
-
if (pair?.status === "pending") return "bg-warning-lt";
|
|
1486
|
-
return "bg-secondary-lt";
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
function pairStatusClass(pair) {
|
|
1490
|
-
if (pair?.status === "active") return "bg-success";
|
|
1491
|
-
if (pair?.status === "pending") return "bg-warning";
|
|
1492
|
-
if (pair?.status === "rejected" || pair?.status === "expired") return "bg-danger";
|
|
1493
|
-
return "bg-secondary";
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
function pairBadgeLabel(pair, agentId) {
|
|
1497
|
-
if (!pair) return "";
|
|
1498
|
-
const peer = this.displayTarget(pairPeerId(pair, agentId));
|
|
1499
|
-
if (pair.status === "active") return "paired with " + peer;
|
|
1500
|
-
if (pair.status === "pending" && pair.requesterId === agentId) return "invite to " + peer;
|
|
1501
|
-
if (pair.status === "pending") return "invite from " + peer;
|
|
1502
|
-
return pair.status;
|
|
1503
|
-
}
|
|
1504
|
-
|
|
1505
|
-
function pairTitle(pair, agentId) {
|
|
1506
|
-
if (!pair) return "";
|
|
1507
|
-
const label = pairBadgeLabel.call(this, pair, agentId);
|
|
1508
|
-
const objective = pair.objective ? " - " + pair.objective : "";
|
|
1509
|
-
return `${label} (${pair.id})${objective}`;
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
function agentAttention(agent) {
|
|
1513
|
-
if (!agent) return emptyAttention();
|
|
1514
|
-
const thread = this.allInboxThreads.find((item) => item.peer === agent.id && !item.archived);
|
|
1515
|
-
const pair = this.agentPair(agent);
|
|
1516
|
-
const pendingPairInvite = pair?.status === "pending";
|
|
1517
|
-
const claimableTasks = countAgentClaimableWaiting(this, agent);
|
|
1518
|
-
const attention = {
|
|
1519
|
-
unread: thread?.attention?.unread || 0,
|
|
1520
|
-
needsHumanResponse: false,
|
|
1521
|
-
agentQuestion: Boolean(thread?.attention?.agentQuestion),
|
|
1522
|
-
pendingPairInvite,
|
|
1523
|
-
claimableTasks,
|
|
1524
|
-
};
|
|
1525
|
-
attention.total = attention.unread +
|
|
1526
|
-
(attention.agentQuestion ? 1 : 0) +
|
|
1527
|
-
(attention.pendingPairInvite ? 1 : 0) +
|
|
1528
|
-
attention.claimableTasks;
|
|
1529
|
-
attention.score = attention.unread * 10 +
|
|
1530
|
-
(attention.agentQuestion ? 3 : 0) +
|
|
1531
|
-
(attention.pendingPairInvite ? 4 : 0) +
|
|
1532
|
-
attention.claimableTasks * 2;
|
|
1533
|
-
return attention;
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
function emptyAttention() {
|
|
1537
|
-
return {
|
|
1538
|
-
unread: 0,
|
|
1539
|
-
agentQuestion: false,
|
|
1540
|
-
pendingPairInvite: false,
|
|
1541
|
-
claimableTasks: 0,
|
|
1542
|
-
total: 0,
|
|
1543
|
-
score: 0,
|
|
1544
|
-
};
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
|
-
function agentAttentionTitle(agent) {
|
|
1548
|
-
const attention = agentAttention.call(this, agent);
|
|
1549
|
-
const parts = [];
|
|
1550
|
-
if (attention.unread) parts.push(`${attention.unread} unread`);
|
|
1551
|
-
if (attention.agentQuestion) parts.push("agent asked a question");
|
|
1552
|
-
if (attention.pendingPairInvite) parts.push("pair invite pending");
|
|
1553
|
-
if (attention.claimableTasks) parts.push(`${attention.claimableTasks} claimable waiting`);
|
|
1554
|
-
return parts.join(", ");
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
function agentType(agent) {
|
|
1558
|
-
if (agent?.id === HUMAN_AGENT_ID) return "user";
|
|
1559
|
-
if (agent?.id === "system") return "system";
|
|
1560
|
-
if (isChannelAgent(agent)) return "channel";
|
|
1561
|
-
|
|
1562
|
-
const values = [
|
|
1563
|
-
...(agent?.tags || []),
|
|
1564
|
-
agent?.meta?.provider,
|
|
1565
|
-
agent?.meta?.client,
|
|
1566
|
-
agent?.meta?.runtime,
|
|
1567
|
-
agent?.meta?.agentType,
|
|
1568
|
-
agent?.id,
|
|
1569
|
-
agent?.name,
|
|
1570
|
-
]
|
|
1571
|
-
.filter((value) => typeof value === "string")
|
|
1572
|
-
.map((value) => value.toLowerCase());
|
|
1573
|
-
|
|
1574
|
-
if (values.some((value) => value.includes("codex"))) return "codex";
|
|
1575
|
-
if (values.some((value) => value.includes("claude"))) return "claude";
|
|
1576
|
-
return "agent";
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
function agentTypeIcon(agent) {
|
|
1580
|
-
return AGENT_TYPE_ICONS[agentType(agent)] || AGENT_TYPE_ICONS.agent;
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
function agentTypeTitle(agent) {
|
|
1584
|
-
return AGENT_TYPE_TITLES[agentType(agent)] || AGENT_TYPE_TITLES.agent;
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
function agentPresence(agent) {
|
|
1588
|
-
const attention = agentAttention.call(this, agent);
|
|
1589
|
-
const pair = this.agentPair(agent);
|
|
1590
|
-
const stale = isAgentStale(this, agent);
|
|
1591
|
-
const reconnecting = agent?.status !== "offline" && !agent?.ready && stale;
|
|
1592
|
-
const starting = agent?.status !== "offline" && !agent?.ready && !stale;
|
|
1593
|
-
const unreadIdle = attention.unread > 0 && agent?.status !== "busy";
|
|
1594
|
-
|
|
1595
|
-
if (agent?.status === "offline") {
|
|
1596
|
-
return { label: "offline", tone: "secondary", icon: "ti-plug-off", stale: false, reconnecting: false, badges: presenceBadges(attention, pair, { offline: true }) };
|
|
1597
|
-
}
|
|
1598
|
-
if (reconnecting) {
|
|
1599
|
-
return { label: "reconnecting", tone: "danger", icon: "ti-refresh", stale, reconnecting, badges: presenceBadges(attention, pair, { reconnecting }) };
|
|
1600
|
-
}
|
|
1601
|
-
if (starting) {
|
|
1602
|
-
return { label: "online, not ready", tone: "warning", icon: "ti-loader", stale, reconnecting, badges: presenceBadges(attention, pair, { starting }) };
|
|
1603
|
-
}
|
|
1604
|
-
if (agent?.status === "busy") {
|
|
1605
|
-
return { label: "busy in turn", tone: "warning", icon: "ti-player-play", stale, reconnecting, badges: presenceBadges(attention, pair, { busy: true }) };
|
|
1606
|
-
}
|
|
1607
|
-
if (pair?.status === "active") {
|
|
1608
|
-
return { label: "paired", tone: "success", icon: "ti-link", stale, reconnecting, badges: presenceBadges(attention, pair, { paired: true }) };
|
|
1609
|
-
}
|
|
1610
|
-
if (unreadIdle) {
|
|
1611
|
-
return { label: "idle, unread", tone: "danger", icon: "ti-bell", stale, reconnecting, badges: presenceBadges(attention, pair, { unreadIdle: true }) };
|
|
1612
|
-
}
|
|
1613
|
-
return { label: agent?.status === "idle" ? "idle" : "ready", tone: "success", icon: "ti-circle-check", stale, reconnecting, badges: presenceBadges(attention, pair, {}) };
|
|
1614
|
-
}
|
|
1615
|
-
|
|
1616
|
-
function presenceBadges(attention, pair, _flags) {
|
|
1617
|
-
const badges = [];
|
|
1618
|
-
if (pair?.status === "active") badges.push({ label: "paired", className: "bg-success-lt" });
|
|
1619
|
-
if (pair?.status === "pending") badges.push({ label: "pair invite pending", className: "bg-warning-lt" });
|
|
1620
|
-
if (attention.unread) badges.push({ label: attention.unread + " unread", className: "bg-danger-lt" });
|
|
1621
|
-
if (attention.agentQuestion) badges.push({ label: "question", className: "bg-info-lt" });
|
|
1622
|
-
if (attention.claimableTasks) badges.push({ label: attention.claimableTasks + " claimable", className: "bg-orange-lt" });
|
|
1623
|
-
return badges;
|
|
1624
|
-
}
|
|
1625
|
-
|
|
1626
|
-
function agentPresenceBadges(agent) {
|
|
1627
|
-
return agentPresence.call(this, agent).badges;
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
function agentStatusClass(agent) {
|
|
1631
|
-
const presence = agentPresence.call(this, agent);
|
|
1632
|
-
return [
|
|
1633
|
-
agent?.status || "offline",
|
|
1634
|
-
agent?.status !== "offline" && !agent?.ready ? "not-ready" : "",
|
|
1635
|
-
presence.stale ? "stale" : "",
|
|
1636
|
-
presence.reconnecting ? "reconnecting" : "",
|
|
1637
|
-
presence.label === "paired" ? "paired" : "",
|
|
1638
|
-
presence.label === "idle, unread" ? "attention" : "",
|
|
1639
|
-
].filter(Boolean).join(" ");
|
|
1640
|
-
}
|
|
1641
|
-
|
|
1642
|
-
function severityClass(severity) {
|
|
1643
|
-
if (severity === "critical") return "bg-danger-lt";
|
|
1644
|
-
if (severity === "warning") return "bg-warning-lt";
|
|
1645
|
-
return "bg-info-lt";
|
|
1646
|
-
}
|
|
1647
|
-
|
|
1648
|
-
function integrationPresence(integration) {
|
|
1649
|
-
const stats = integration?.taskStats || {};
|
|
1650
|
-
if ((stats.waitingTasks || 0) > 0) return { label: "waiting", tone: "warning", icon: "ti-alert-circle" };
|
|
1651
|
-
if ((stats.openTasks || 0) > 0) return { label: "active", tone: "info", icon: "ti-activity" };
|
|
1652
|
-
if (!integration?.configured) return { label: "observed only", tone: "secondary", icon: "ti-eye" };
|
|
1653
|
-
if (integration.observed) return { label: "quiet", tone: "success", icon: "ti-circle-check" };
|
|
1654
|
-
return { label: "configured", tone: "primary", icon: "ti-plug-connected" };
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
function connectorPresence(connector) {
|
|
1658
|
-
const runtime = connector?.runtime || {};
|
|
1659
|
-
if (runtime.status === "error") return { label: "error", tone: "danger", icon: "ti-alert-triangle" };
|
|
1660
|
-
if (runtime.status === "warn") return { label: "warning", tone: "warning", icon: "ti-alert-circle" };
|
|
1661
|
-
if (runtime.running) return { label: "running", tone: "success", icon: "ti-circle-check" };
|
|
1662
|
-
if (runtime.enabled === false) return { label: "disabled", tone: "secondary", icon: "ti-player-pause" };
|
|
1663
|
-
if (runtime.status === "ok") return { label: "ok", tone: "success", icon: "ti-circle-check" };
|
|
1664
|
-
return { label: "unknown", tone: "secondary", icon: "ti-help-circle" };
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
function agentChannels(agent) {
|
|
1668
|
-
if (!agent) return [];
|
|
1669
|
-
return (this.channels || []).filter((ch) => ch.agentId === agent.id);
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
|
-
function channelPresence(channel) {
|
|
1673
|
-
if (!channel) return { label: "unknown", tone: "secondary", icon: "ti-plug-off" };
|
|
1674
|
-
if (channel.targetHealth?.status === "error") return { label: "target broken", tone: "danger", icon: "ti-alert-triangle" };
|
|
1675
|
-
if (channel.targetHealth?.status === "warning") return { label: "target warning", tone: "warning", icon: "ti-alert-circle" };
|
|
1676
|
-
if (channel.status === "offline") return { label: "offline", tone: "secondary", icon: "ti-plug-off" };
|
|
1677
|
-
if (!channel.ready) return { label: "not ready", tone: "warning", icon: "ti-loader" };
|
|
1678
|
-
if (channel.status === "busy") return { label: "busy", tone: "warning", icon: "ti-activity" };
|
|
1679
|
-
return { label: "ready", tone: "success", icon: "ti-circle-check" };
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
function agentStatusTitle(agent) {
|
|
1683
|
-
if (!agent) return "";
|
|
1684
|
-
if (agent.status === "offline") return "offline";
|
|
1685
|
-
if (isAgentStale(this, agent) && !agent.ready) return "reconnecting";
|
|
1686
|
-
if (isAgentStale(this, agent)) return "stale heartbeat";
|
|
1687
|
-
if (agent.status === "busy") return "busy in turn";
|
|
1688
|
-
if (this.agentPair(agent)?.status === "active") return "paired";
|
|
1689
|
-
if (agentAttention.call(this, agent).unread) return "idle but has unread";
|
|
1690
|
-
if (agent.ready) return agent.status;
|
|
1691
|
-
|
|
1692
|
-
const lastSeenMs = new Date(agent.lastSeen).getTime();
|
|
1693
|
-
if (!Number.isFinite(lastSeenMs)) return "Trying to reconnect...";
|
|
1694
|
-
|
|
1695
|
-
const ageSec = Math.max(0, ((this.now || Date.now()) - lastSeenMs) / 1000);
|
|
1696
|
-
return ageSec <= 45 ? "Starting up..." : "Trying to reconnect...";
|
|
1697
|
-
}
|
|
1698
|
-
|
|
1699
|
-
function timeAgo(iso) {
|
|
1700
|
-
if (!iso) return "";
|
|
1701
|
-
const ts = new Date(iso).getTime();
|
|
1702
|
-
if (!Number.isFinite(ts)) return "";
|
|
1703
|
-
|
|
1704
|
-
const diff = Math.max(0, ((this.now || Date.now()) - ts) / 1000);
|
|
1705
|
-
if (diff < 60) return Math.floor(diff) + "s ago";
|
|
1706
|
-
if (diff < 3600) return Math.floor(diff / 60) + "m ago";
|
|
1707
|
-
if (diff < 86400) return Math.floor(diff / 3600) + "h ago";
|
|
1708
|
-
return Math.floor(diff / 86400) + "d ago";
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
function fmtTime(iso) {
|
|
1712
|
-
if (!iso) return "";
|
|
1713
|
-
return new Date(iso).toLocaleString();
|
|
1714
|
-
}
|
|
1715
|
-
|
|
1716
|
-
function healthAlertClass(status) {
|
|
1717
|
-
if (status === "error") return "alert-danger";
|
|
1718
|
-
if (status === "degraded") return "alert-warning";
|
|
1719
|
-
return "alert-success";
|
|
1720
|
-
}
|
|
1721
|
-
|
|
1722
|
-
function activityKindClass(kind) {
|
|
1723
|
-
if (kind === "question") return "bg-info-lt";
|
|
1724
|
-
if (kind === "task") return "bg-warning-lt";
|
|
1725
|
-
if (kind === "pair") return "bg-success-lt";
|
|
1726
|
-
if (kind === "operator") return "bg-primary-lt";
|
|
1727
|
-
if (kind === "reply") return "bg-purple-lt";
|
|
1728
|
-
return "bg-secondary-lt";
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
function createMessageActions() {
|
|
1732
|
-
return {
|
|
1733
|
-
openCompose,
|
|
1734
|
-
openComposeToAgent,
|
|
1735
|
-
openComposeToInboxThread,
|
|
1736
|
-
openInboxThread,
|
|
1737
|
-
markInboxThreadRead,
|
|
1738
|
-
markInboxThreadUnread,
|
|
1739
|
-
archiveInboxThread,
|
|
1740
|
-
unarchiveInboxThread,
|
|
1741
|
-
confirmDeleteInboxThread,
|
|
1742
|
-
doDeleteInboxThread,
|
|
1743
|
-
replyDraftForThread,
|
|
1744
|
-
setReplyDraft,
|
|
1745
|
-
clearReplyDraft,
|
|
1746
|
-
sendInboxReply,
|
|
1747
|
-
resetInboxComposeTarget,
|
|
1748
|
-
doSendInboxCompose,
|
|
1749
|
-
startReply,
|
|
1750
|
-
cancelReply,
|
|
1751
|
-
doSend,
|
|
1752
|
-
doClaim,
|
|
1753
|
-
doClaimTask,
|
|
1754
|
-
doUpdateTaskStatus,
|
|
1755
|
-
doDeleteMessage,
|
|
1756
|
-
openThread,
|
|
1757
|
-
openTaskEvents,
|
|
1758
|
-
recordOperatorActivity,
|
|
1759
|
-
openActivityItem,
|
|
1760
|
-
runHealthAction,
|
|
1761
|
-
runConnectorAction,
|
|
1762
|
-
openCommandPalette,
|
|
1763
|
-
closeCommandPalette,
|
|
1764
|
-
runCommand,
|
|
1765
|
-
exportActivity,
|
|
1766
|
-
exportThread,
|
|
1767
|
-
exportPair,
|
|
1768
|
-
exportTask,
|
|
1769
|
-
};
|
|
1770
|
-
}
|
|
1771
|
-
|
|
1772
|
-
function createPairActions() {
|
|
1773
|
-
return {
|
|
1774
|
-
openPairMessage,
|
|
1775
|
-
closePairMessage,
|
|
1776
|
-
openPairInvite,
|
|
1777
|
-
closePairInvite,
|
|
1778
|
-
doCreatePair,
|
|
1779
|
-
openAgentSpawn,
|
|
1780
|
-
closeAgentSpawn,
|
|
1781
|
-
doSpawnAgent,
|
|
1782
|
-
openAgentDirectoryBrowser,
|
|
1783
|
-
browseAgentDirectory,
|
|
1784
|
-
selectAgentDirectory,
|
|
1785
|
-
openOrchestratorSpawn,
|
|
1786
|
-
openOrchestratorSpawnFor,
|
|
1787
|
-
browseOrchestratorDirs,
|
|
1788
|
-
submitOrchestratorSpawn,
|
|
1789
|
-
orchestratorAction,
|
|
1790
|
-
deleteOrchestrator,
|
|
1791
|
-
doSendPairMessage,
|
|
1792
|
-
doAcceptPair,
|
|
1793
|
-
doRejectPair,
|
|
1794
|
-
doHangupPair,
|
|
1795
|
-
};
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
|
-
function focusComposeBody(vm) {
|
|
1799
|
-
vm.$nextTick(() => vm.$refs.composeBody?.focus());
|
|
1800
|
-
}
|
|
1801
|
-
|
|
1802
|
-
function openCompose() {
|
|
1803
|
-
if (!this.replyTo) this.compose = { ...DEFAULT_COMPOSE, from: "user" };
|
|
1804
|
-
this.composeOpen = true;
|
|
1805
|
-
focusComposeBody(this);
|
|
1806
|
-
}
|
|
1807
|
-
|
|
1808
|
-
function openComposeToAgent(agent) {
|
|
1809
|
-
this.replyTo = null;
|
|
1810
|
-
this.compose = { ...DEFAULT_COMPOSE, from: "user", to: agent.id };
|
|
1811
|
-
this.composeOpen = true;
|
|
1812
|
-
focusComposeBody(this);
|
|
1813
|
-
}
|
|
1814
|
-
|
|
1815
|
-
function openComposeToInboxThread(thread) {
|
|
1816
|
-
if (!thread) return;
|
|
1817
|
-
this.replyTo = thread.lastMessage ? { id: thread.lastMessage.id, from: thread.lastMessage.from } : null;
|
|
1818
|
-
this.compose = { ...DEFAULT_COMPOSE, from: HUMAN_AGENT_ID, to: thread.peer, channel: thread.lastMessage?.channel || "" };
|
|
1819
|
-
this.composeOpen = true;
|
|
1820
|
-
focusComposeBody(this);
|
|
1821
|
-
}
|
|
1822
|
-
|
|
1823
|
-
function openInboxThread(thread) {
|
|
1824
|
-
this.selectedInboxThread = thread?.id || "";
|
|
1825
|
-
this.markInboxThreadRead(thread);
|
|
1826
|
-
}
|
|
1827
|
-
|
|
1828
|
-
function markInboxThreadRead(thread) {
|
|
1829
|
-
if (!thread?.peer || !thread.messages?.length) return;
|
|
1830
|
-
const lastInboundId = maxMessageId(thread.messages, isHumanInboundMessage);
|
|
1831
|
-
if (lastInboundId <= readCursorForPeer(this, thread.peer)) return;
|
|
1832
|
-
this.inboxReadCursors = { ...this.inboxReadCursors, [thread.peer]: lastInboundId };
|
|
1833
|
-
savePref("inboxReadCursors", this.inboxReadCursors);
|
|
1834
|
-
void saveInboxThreadState(this, {
|
|
1835
|
-
peerId: thread.peer,
|
|
1836
|
-
readCursorMessageId: lastInboundId,
|
|
1837
|
-
});
|
|
1838
|
-
}
|
|
1839
|
-
|
|
1840
|
-
function markInboxThreadUnread(thread) {
|
|
1841
|
-
if (!thread?.peer) return;
|
|
1842
|
-
const next = { ...this.inboxReadCursors };
|
|
1843
|
-
delete next[thread.peer];
|
|
1844
|
-
this.inboxReadCursors = next;
|
|
1845
|
-
savePref("inboxReadCursors", this.inboxReadCursors);
|
|
1846
|
-
this.recordOperatorActivity({
|
|
1847
|
-
title: "Marked thread unread",
|
|
1848
|
-
body: this.conversationTitle(thread),
|
|
1849
|
-
meta: "Inbox",
|
|
1850
|
-
icon: "ti-mail",
|
|
1851
|
-
view: "inbox",
|
|
1852
|
-
peer: thread.peer,
|
|
1853
|
-
});
|
|
1854
|
-
void saveInboxThreadState(this, {
|
|
1855
|
-
peerId: thread.peer,
|
|
1856
|
-
readCursorMessageId: null,
|
|
1857
|
-
});
|
|
1858
|
-
}
|
|
1859
|
-
|
|
1860
|
-
function archiveInboxThread(thread) {
|
|
1861
|
-
if (!thread?.peer || !thread.lastMessage) return;
|
|
1862
|
-
this.inboxArchivedThreads = { ...this.inboxArchivedThreads, [thread.peer]: thread.lastMessage.id };
|
|
1863
|
-
savePref("inboxArchivedThreads", this.inboxArchivedThreads);
|
|
1864
|
-
this.recordOperatorActivity({
|
|
1865
|
-
title: "Archived thread",
|
|
1866
|
-
body: this.conversationTitle(thread),
|
|
1867
|
-
meta: "Inbox",
|
|
1868
|
-
icon: "ti-archive",
|
|
1869
|
-
view: "inbox",
|
|
1870
|
-
peer: thread.peer,
|
|
1871
|
-
});
|
|
1872
|
-
void saveInboxThreadState(this, {
|
|
1873
|
-
peerId: thread.peer,
|
|
1874
|
-
archivedAtMessageId: thread.lastMessage.id,
|
|
1875
|
-
});
|
|
1876
|
-
if (this.selectedInboxThread === thread.id) this.selectedInboxThread = "";
|
|
1877
|
-
}
|
|
1878
|
-
|
|
1879
|
-
function unarchiveInboxThread(thread) {
|
|
1880
|
-
if (!thread?.peer) return;
|
|
1881
|
-
const next = { ...this.inboxArchivedThreads };
|
|
1882
|
-
delete next[thread.peer];
|
|
1883
|
-
this.inboxArchivedThreads = next;
|
|
1884
|
-
savePref("inboxArchivedThreads", this.inboxArchivedThreads);
|
|
1885
|
-
this.recordOperatorActivity({
|
|
1886
|
-
title: "Unarchived thread",
|
|
1887
|
-
body: this.conversationTitle(thread),
|
|
1888
|
-
meta: "Inbox",
|
|
1889
|
-
icon: "ti-archive-off",
|
|
1890
|
-
view: "inbox",
|
|
1891
|
-
peer: thread.peer,
|
|
1892
|
-
});
|
|
1893
|
-
void saveInboxThreadState(this, {
|
|
1894
|
-
peerId: thread.peer,
|
|
1895
|
-
archivedAtMessageId: null,
|
|
1896
|
-
});
|
|
1897
|
-
}
|
|
1898
|
-
|
|
1899
|
-
async function saveInboxThreadState(vm, patch) {
|
|
1900
|
-
try {
|
|
1901
|
-
await vm.api("PATCH", "/inbox/threads", { operatorId: INBOX_OPERATOR_ID, ...patch });
|
|
1902
|
-
} catch {}
|
|
1903
|
-
}
|
|
1904
|
-
|
|
1905
|
-
function confirmDeleteInboxThread(thread) {
|
|
1906
|
-
if (!thread) return;
|
|
1907
|
-
this.openConfirm(
|
|
1908
|
-
"Delete Thread",
|
|
1909
|
-
`Delete ${thread.messages.length} message(s) in ${this.conversationTitle(thread)}? This cannot be undone.`,
|
|
1910
|
-
() => this.doDeleteInboxThread(thread)
|
|
1911
|
-
);
|
|
1912
|
-
}
|
|
1913
|
-
|
|
1914
|
-
async function doDeleteInboxThread(thread) {
|
|
1915
|
-
if (!thread?.messages?.length) return;
|
|
1916
|
-
try {
|
|
1917
|
-
await Promise.all(thread.messages.map((msg) => this.api("DELETE", "/messages/" + msg.id)));
|
|
1918
|
-
this.messages = this.messages.filter((msg) => !thread.messages.some((item) => item.id === msg.id));
|
|
1919
|
-
this.selectedInboxThread = "";
|
|
1920
|
-
this.clearReplyDraft(thread);
|
|
1921
|
-
this.recordOperatorActivity({
|
|
1922
|
-
title: "Deleted thread",
|
|
1923
|
-
body: this.conversationTitle(thread),
|
|
1924
|
-
meta: thread.messages.length + " message(s)",
|
|
1925
|
-
icon: "ti-trash",
|
|
1926
|
-
view: "inbox",
|
|
1927
|
-
});
|
|
1928
|
-
} catch (e) {
|
|
1929
|
-
alert("Delete thread failed: " + e.message);
|
|
1930
|
-
}
|
|
1931
|
-
}
|
|
1932
|
-
|
|
1933
|
-
function replyDraftForThread(thread) {
|
|
1934
|
-
return thread?.peer ? draftForPeer(this, thread.peer) : "";
|
|
1935
|
-
}
|
|
1936
|
-
|
|
1937
|
-
function setReplyDraft(thread, value) {
|
|
1938
|
-
if (!thread?.peer) return;
|
|
1939
|
-
const next = { ...this.inboxDrafts, [thread.peer]: value };
|
|
1940
|
-
if (!value) delete next[thread.peer];
|
|
1941
|
-
this.inboxDrafts = next;
|
|
1942
|
-
savePref("inboxDrafts", this.inboxDrafts);
|
|
1943
|
-
if (value) {
|
|
1944
|
-
void saveInboxDraft(this, thread.peer, value);
|
|
1945
|
-
} else {
|
|
1946
|
-
void deleteInboxDraftState(this, thread.peer);
|
|
1947
|
-
}
|
|
1948
|
-
}
|
|
1949
|
-
|
|
1950
|
-
function clearReplyDraft(thread) {
|
|
1951
|
-
if (!thread?.peer) return;
|
|
1952
|
-
this.setReplyDraft(thread, "");
|
|
1953
|
-
}
|
|
1954
|
-
|
|
1955
|
-
async function saveInboxDraft(vm, peerId, body) {
|
|
1956
|
-
try {
|
|
1957
|
-
await vm.api("PUT", "/inbox/drafts", { operatorId: INBOX_OPERATOR_ID, peerId, body });
|
|
1958
|
-
} catch {}
|
|
1959
|
-
}
|
|
1960
|
-
|
|
1961
|
-
async function deleteInboxDraftState(vm, peerId) {
|
|
1962
|
-
try {
|
|
1963
|
-
await vm.api("DELETE", "/inbox/drafts?operatorId=" + encodeURIComponent(INBOX_OPERATOR_ID) + "&peerId=" + encodeURIComponent(peerId));
|
|
1964
|
-
} catch {}
|
|
1965
|
-
}
|
|
1966
|
-
|
|
1967
|
-
async function sendInboxReply(thread) {
|
|
1968
|
-
if (!thread) return;
|
|
1969
|
-
const body = this.replyDraftForThread(thread).trim();
|
|
1970
|
-
if (!body) {
|
|
1971
|
-
alert("Reply body is required.");
|
|
1972
|
-
return;
|
|
1973
|
-
}
|
|
1974
|
-
|
|
1975
|
-
try {
|
|
1976
|
-
const payload = {
|
|
1977
|
-
from: HUMAN_AGENT_ID,
|
|
1978
|
-
to: thread.peer,
|
|
1979
|
-
body,
|
|
1980
|
-
};
|
|
1981
|
-
if (thread.lastMessage?.channel) payload.channel = thread.lastMessage.channel;
|
|
1982
|
-
if (thread.lastMessage?.id) payload.replyTo = thread.lastMessage.id;
|
|
1983
|
-
await this.api("POST", "/messages", payload);
|
|
1984
|
-
this.recordOperatorActivity({
|
|
1985
|
-
title: "Reply sent",
|
|
1986
|
-
body,
|
|
1987
|
-
meta: "to " + this.displayTarget(thread.peer),
|
|
1988
|
-
icon: "ti-corner-up-left",
|
|
1989
|
-
view: "inbox",
|
|
1990
|
-
peer: thread.peer,
|
|
1991
|
-
});
|
|
1992
|
-
this.clearReplyDraft(thread);
|
|
1993
|
-
this.markInboxThreadRead(thread);
|
|
1994
|
-
await this.fetchMessages();
|
|
1995
|
-
} catch (e) {
|
|
1996
|
-
alert("Reply failed: " + e.message);
|
|
1997
|
-
}
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2000
|
-
function resetInboxComposeTarget() {
|
|
2001
|
-
this.inboxCompose = { ...this.inboxCompose, to: "", claimable: false };
|
|
2002
|
-
}
|
|
2003
|
-
|
|
2004
|
-
function inboxComposeTarget(vm) {
|
|
2005
|
-
const target = vm.inboxCompose.to;
|
|
2006
|
-
if (!target) return "";
|
|
2007
|
-
if (vm.inboxCompose.toMode === "tag") return "tag:" + target;
|
|
2008
|
-
if (vm.inboxCompose.toMode === "cap") return "cap:" + target;
|
|
2009
|
-
return target;
|
|
2010
|
-
}
|
|
2011
|
-
|
|
2012
|
-
async function doSendInboxCompose() {
|
|
2013
|
-
const target = inboxComposeTarget(this);
|
|
2014
|
-
if (!target || !this.inboxCompose.body) {
|
|
2015
|
-
alert("Target and Message are required.");
|
|
2016
|
-
return;
|
|
2017
|
-
}
|
|
2018
|
-
|
|
2019
|
-
try {
|
|
2020
|
-
const payload = {
|
|
2021
|
-
from: HUMAN_AGENT_ID,
|
|
2022
|
-
to: target,
|
|
2023
|
-
body: this.inboxCompose.body,
|
|
2024
|
-
};
|
|
2025
|
-
if (this.inboxCompose.channel) payload.channel = this.inboxCompose.channel;
|
|
2026
|
-
if (this.inboxCompose.subject) payload.subject = this.inboxCompose.subject;
|
|
2027
|
-
if (this.inboxCompose.claimable && inboxComposeTargetAllowsClaimable.call(this)) {
|
|
2028
|
-
payload.claimable = true;
|
|
2029
|
-
payload.kind = "task";
|
|
2030
|
-
payload.payload = { title: this.inboxCompose.subject || "Claimable task" };
|
|
2031
|
-
}
|
|
2032
|
-
await this.api("POST", "/messages", payload);
|
|
2033
|
-
this.recordOperatorActivity({
|
|
2034
|
-
title: payload.claimable ? "Claimable task sent" : "Message sent",
|
|
2035
|
-
body: this.inboxCompose.subject || this.inboxCompose.body,
|
|
2036
|
-
meta: "to " + this.displayTarget(target),
|
|
2037
|
-
icon: payload.claimable ? "ti-hand-grab" : "ti-send",
|
|
2038
|
-
kind: payload.claimable ? "task" : "operator",
|
|
2039
|
-
view: inboxPeer(payload) ? "inbox" : "messages",
|
|
2040
|
-
peer: inboxPeer(payload),
|
|
2041
|
-
});
|
|
2042
|
-
this.inboxCompose = { ...DEFAULT_INBOX_COMPOSE, toMode: this.inboxCompose.toMode, to: this.inboxCompose.to };
|
|
2043
|
-
await this.fetchMessages();
|
|
2044
|
-
} catch (e) {
|
|
2045
|
-
alert("Send failed: " + e.message);
|
|
2046
|
-
}
|
|
2047
|
-
}
|
|
2048
|
-
|
|
2049
|
-
function startReply(msg) {
|
|
2050
|
-
const replyTarget = msg.from === HUMAN_AGENT_ID ? msg.to : msg.from;
|
|
2051
|
-
this.replyTo = { id: msg.id, from: msg.from };
|
|
2052
|
-
this.compose = {
|
|
2053
|
-
...DEFAULT_COMPOSE,
|
|
2054
|
-
from: HUMAN_AGENT_ID,
|
|
2055
|
-
to: replyTarget,
|
|
2056
|
-
channel: msg.channel || "",
|
|
2057
|
-
};
|
|
2058
|
-
this.openCompose();
|
|
2059
|
-
}
|
|
2060
|
-
|
|
2061
|
-
function cancelReply() {
|
|
2062
|
-
this.replyTo = null;
|
|
2063
|
-
}
|
|
2064
|
-
|
|
2065
|
-
function buildMessagePayload(vm) {
|
|
2066
|
-
const payload = {
|
|
2067
|
-
from: vm.compose.from,
|
|
2068
|
-
to: vm.compose.to,
|
|
2069
|
-
body: vm.compose.body,
|
|
2070
|
-
};
|
|
2071
|
-
if (vm.compose.channel) payload.channel = vm.compose.channel;
|
|
2072
|
-
if (vm.compose.subject) payload.subject = vm.compose.subject;
|
|
2073
|
-
if (vm.replyTo) payload.replyTo = vm.replyTo.id;
|
|
2074
|
-
if (vm.compose.claimable && composeTargetAllowsClaimable.call(vm)) {
|
|
2075
|
-
payload.claimable = true;
|
|
2076
|
-
payload.kind = "task";
|
|
2077
|
-
payload.payload = { title: vm.compose.subject || "Claimable task" };
|
|
2078
|
-
}
|
|
2079
|
-
return payload;
|
|
2080
|
-
}
|
|
2081
|
-
|
|
2082
|
-
async function doSend() {
|
|
2083
|
-
if (!this.compose.from || !this.compose.to || !this.compose.body) {
|
|
2084
|
-
alert("From, To, and Message are required.");
|
|
2085
|
-
return;
|
|
2086
|
-
}
|
|
2087
|
-
|
|
2088
|
-
try {
|
|
2089
|
-
const payload = buildMessagePayload(this);
|
|
2090
|
-
await this.api("POST", "/messages", payload);
|
|
2091
|
-
this.recordOperatorActivity({
|
|
2092
|
-
title: payload.claimable ? "Claimable task sent" : "Message sent",
|
|
2093
|
-
body: payload.subject || payload.body,
|
|
2094
|
-
meta: `${this.displayTarget(payload.from)} -> ${this.displayTarget(payload.to)}`,
|
|
2095
|
-
icon: payload.claimable ? "ti-hand-grab" : "ti-send",
|
|
2096
|
-
kind: payload.claimable ? "task" : "operator",
|
|
2097
|
-
view: inboxPeer(payload) ? "inbox" : "messages",
|
|
2098
|
-
peer: inboxPeer(payload),
|
|
2099
|
-
});
|
|
2100
|
-
this.composeOpen = false;
|
|
2101
|
-
this.replyTo = null;
|
|
2102
|
-
this.compose = { ...DEFAULT_COMPOSE, from: HUMAN_AGENT_ID };
|
|
2103
|
-
await this.fetchMessages();
|
|
2104
|
-
} catch (e) {
|
|
2105
|
-
alert("Send failed: " + e.message);
|
|
2106
|
-
}
|
|
2107
|
-
}
|
|
2108
|
-
|
|
2109
|
-
async function doClaim(msgId) {
|
|
2110
|
-
if (!this.compose.from && !this.selectedAgent) {
|
|
2111
|
-
alert('Select a "From" agent first (open Compose to pick one).');
|
|
2112
|
-
return;
|
|
2113
|
-
}
|
|
2114
|
-
|
|
2115
|
-
const agentId = this.compose.from || this.selectedAgent;
|
|
2116
|
-
try {
|
|
2117
|
-
const result = await this.api("POST", "/messages/" + msgId + "/claim", { agentId });
|
|
2118
|
-
if (!result.ok) alert("Claim failed: " + (result.error || "unknown"));
|
|
2119
|
-
else this.recordOperatorActivity({
|
|
2120
|
-
title: "Claim requested",
|
|
2121
|
-
body: "Message #" + msgId,
|
|
2122
|
-
meta: "as " + this.displayTarget(agentId),
|
|
2123
|
-
icon: "ti-hand-grab",
|
|
2124
|
-
kind: "task",
|
|
2125
|
-
view: "tasks",
|
|
2126
|
-
});
|
|
2127
|
-
if (result.ok) await Promise.all([this.fetchMessages(), this.fetchTasks()]);
|
|
2128
|
-
} catch (e) {
|
|
2129
|
-
alert("Claim failed: " + e.message);
|
|
2130
|
-
}
|
|
2131
|
-
}
|
|
2132
|
-
|
|
2133
|
-
async function doClaimTask(taskId) {
|
|
2134
|
-
if (!this.compose.from && !this.selectedAgent) {
|
|
2135
|
-
alert('Select an agent first (use the Messages agent filter or Compose From).');
|
|
2136
|
-
return;
|
|
2137
|
-
}
|
|
2138
|
-
|
|
2139
|
-
const agentId = this.compose.from || this.selectedAgent;
|
|
2140
|
-
try {
|
|
2141
|
-
const result = await this.api("POST", "/tasks/" + taskId + "/claim", { agentId });
|
|
2142
|
-
if (!result.ok) alert("Task claim failed: " + (result.error || "unknown"));
|
|
2143
|
-
else {
|
|
2144
|
-
this.recordOperatorActivity({
|
|
2145
|
-
title: "Task claimed",
|
|
2146
|
-
body: "Task #" + taskId,
|
|
2147
|
-
meta: "as " + this.displayTarget(agentId),
|
|
2148
|
-
icon: "ti-user-check",
|
|
2149
|
-
kind: "task",
|
|
2150
|
-
view: "work",
|
|
2151
|
-
taskId,
|
|
2152
|
-
agentId,
|
|
2153
|
-
});
|
|
2154
|
-
await Promise.all([this.fetchTasks(), this.fetchMessages()]);
|
|
2155
|
-
}
|
|
2156
|
-
} catch (e) {
|
|
2157
|
-
alert("Task claim failed: " + e.message);
|
|
2158
|
-
}
|
|
2159
|
-
}
|
|
2160
|
-
|
|
2161
|
-
async function doUpdateTaskStatus(task, status) {
|
|
2162
|
-
if (!task || !status || status === task.status) return;
|
|
2163
|
-
try {
|
|
2164
|
-
const body = { status };
|
|
2165
|
-
const agentId = task.claimedBy || this.compose.from || this.selectedAgent;
|
|
2166
|
-
if (agentId) body.agentId = agentId;
|
|
2167
|
-
const result = await this.api("PATCH", "/tasks/" + task.id + "/status", body);
|
|
2168
|
-
const updated = result.task || result;
|
|
2169
|
-
const idx = this.tasks.findIndex((item) => item.id === task.id);
|
|
2170
|
-
if (idx >= 0) this.tasks[idx] = updated;
|
|
2171
|
-
this.recordOperatorActivity({
|
|
2172
|
-
title: "Task moved to " + status,
|
|
2173
|
-
body: updated.title || "Task #" + task.id,
|
|
2174
|
-
meta: agentId ? "by " + this.displayTarget(agentId) : "Work Queue",
|
|
2175
|
-
icon: "ti-arrows-exchange",
|
|
2176
|
-
kind: "task",
|
|
2177
|
-
view: "work",
|
|
2178
|
-
taskId: task.id,
|
|
2179
|
-
agentId,
|
|
2180
|
-
});
|
|
2181
|
-
} catch (e) {
|
|
2182
|
-
alert("Task status update failed: " + e.message);
|
|
2183
|
-
await this.fetchTasks();
|
|
2184
|
-
}
|
|
2185
|
-
}
|
|
2186
|
-
|
|
2187
|
-
async function doDeleteMessage(id) {
|
|
2188
|
-
try {
|
|
2189
|
-
await this.api("DELETE", "/messages/" + id);
|
|
2190
|
-
this.messages = this.messages.filter((msg) => msg.id !== id);
|
|
2191
|
-
this.recordOperatorActivity({
|
|
2192
|
-
title: "Deleted message",
|
|
2193
|
-
body: "Message #" + id,
|
|
2194
|
-
meta: "Messages",
|
|
2195
|
-
icon: "ti-trash",
|
|
2196
|
-
view: "messages",
|
|
2197
|
-
});
|
|
2198
|
-
} catch (e) {
|
|
2199
|
-
alert("Delete failed: " + e.message);
|
|
2200
|
-
}
|
|
2201
|
-
}
|
|
2202
|
-
|
|
2203
|
-
async function openThread(threadId) {
|
|
2204
|
-
this.threadMessages = [];
|
|
2205
|
-
this.threadOpen = true;
|
|
2206
|
-
try {
|
|
2207
|
-
this.threadMessages = await this.api("GET", "/messages/" + threadId + "/thread");
|
|
2208
|
-
} catch (e) {
|
|
2209
|
-
alert("Failed to load thread: " + e.message);
|
|
2210
|
-
}
|
|
2211
|
-
}
|
|
2212
|
-
|
|
2213
|
-
async function openTaskEvents(task) {
|
|
2214
|
-
this.taskEvents = [];
|
|
2215
|
-
this.taskEventsOpen = true;
|
|
2216
|
-
try {
|
|
2217
|
-
this.taskEvents = await this.api("GET", "/tasks/" + task.id + "/events");
|
|
2218
|
-
this.taskEventCache = { ...this.taskEventCache, [task.id]: this.taskEvents };
|
|
2219
|
-
} catch (e) {
|
|
2220
|
-
alert("Failed to load task events: " + e.message);
|
|
2221
|
-
}
|
|
2222
|
-
}
|
|
2223
|
-
|
|
2224
|
-
function recordOperatorActivity(input) {
|
|
2225
|
-
const item = activityItem({
|
|
2226
|
-
kind: "operator",
|
|
2227
|
-
ts: Date.now(),
|
|
2228
|
-
...input,
|
|
2229
|
-
});
|
|
2230
|
-
item.id = item.id || "operator-" + item.ts + "-" + (this.operatorActivity?.length || 0);
|
|
2231
|
-
item.clientId = item.clientId || item.id;
|
|
2232
|
-
this.operatorActivity = [
|
|
2233
|
-
item,
|
|
2234
|
-
...(this.operatorActivity || []).filter((existing) => existing.id !== item.id),
|
|
2235
|
-
].slice(0, 80);
|
|
2236
|
-
savePref("operatorActivity", this.operatorActivity);
|
|
2237
|
-
void saveActivityEvent(this, item);
|
|
2238
|
-
}
|
|
2239
|
-
|
|
2240
|
-
async function saveActivityEvent(vm, item) {
|
|
2241
|
-
try {
|
|
2242
|
-
const event = await vm.api("POST", "/activity", {
|
|
2243
|
-
operatorId: INBOX_OPERATOR_ID,
|
|
2244
|
-
clientId: item.clientId,
|
|
2245
|
-
kind: item.kind,
|
|
2246
|
-
title: item.title,
|
|
2247
|
-
body: item.body || undefined,
|
|
2248
|
-
meta: item.meta || undefined,
|
|
2249
|
-
icon: item.icon || undefined,
|
|
2250
|
-
view: item.view || undefined,
|
|
2251
|
-
peer: item.peer || undefined,
|
|
2252
|
-
messageId: item.messageId,
|
|
2253
|
-
pairId: item.pairId,
|
|
2254
|
-
taskId: item.taskId,
|
|
2255
|
-
agentId: item.agentId,
|
|
2256
|
-
});
|
|
2257
|
-
const existing = new Set((vm.activityEvents || []).map((entry) => entry.id));
|
|
2258
|
-
vm.activityEvents = existing.has(event.id)
|
|
2259
|
-
? vm.activityEvents.map((entry) => entry.id === event.id ? event : entry)
|
|
2260
|
-
: [event, ...(vm.activityEvents || [])].slice(0, 200);
|
|
2261
|
-
pruneSyncedOperatorActivity(vm);
|
|
2262
|
-
} catch {}
|
|
2263
|
-
}
|
|
2264
|
-
|
|
2265
|
-
async function openActivityItem(item) {
|
|
2266
|
-
if (!item) return;
|
|
2267
|
-
if (item.view) await this.switchView(item.view);
|
|
2268
|
-
if (item.peer) {
|
|
2269
|
-
const thread = this.allInboxThreads.find((candidate) => candidate.peer === item.peer);
|
|
2270
|
-
if (thread?.archived) this.inboxShowArchived = true;
|
|
2271
|
-
this.selectedInboxThread = item.peer;
|
|
2272
|
-
if (thread) this.markInboxThreadRead(thread);
|
|
2273
|
-
}
|
|
2274
|
-
if (item.agentId && this.agentsById[item.agentId]) this.openAgentDetail(this.agentsById[item.agentId]);
|
|
2275
|
-
if (item.taskId) {
|
|
2276
|
-
const task = this.tasks.find((candidate) => candidate.id === item.taskId);
|
|
2277
|
-
if (task) await this.openTaskEvents(task);
|
|
2278
|
-
}
|
|
2279
|
-
}
|
|
2280
|
-
|
|
2281
|
-
async function runHealthAction(action) {
|
|
2282
|
-
if (!action) return;
|
|
2283
|
-
if (action.preset) {
|
|
2284
|
-
this.agentPresetFilter = action.preset;
|
|
2285
|
-
this.showOffline = true;
|
|
2286
|
-
}
|
|
2287
|
-
if (action.api && action.path) {
|
|
2288
|
-
await this.api(action.api, action.path);
|
|
2289
|
-
await this.refreshLiveData();
|
|
2290
|
-
}
|
|
2291
|
-
if (action.view) await this.switchView(action.view);
|
|
2292
|
-
if (action.copy) await copyText(action.copy);
|
|
2293
|
-
}
|
|
2294
|
-
|
|
2295
|
-
async function runConnectorAction(connector, action) {
|
|
2296
|
-
if (!connector || !action) return;
|
|
2297
|
-
try {
|
|
2298
|
-
await this.api("POST", "/connectors/" + encodeURIComponent(connector.id) + "/actions", { action });
|
|
2299
|
-
await this.fetchConnectors();
|
|
2300
|
-
} catch (e) {
|
|
2301
|
-
alert("Connector action failed: " + e.message);
|
|
2302
|
-
}
|
|
2303
|
-
}
|
|
2304
|
-
|
|
2305
|
-
async function copyText(value) {
|
|
2306
|
-
if (typeof navigator === "undefined") return;
|
|
2307
|
-
try {
|
|
2308
|
-
await navigator.clipboard?.writeText(value);
|
|
2309
|
-
} catch {}
|
|
2310
|
-
}
|
|
2311
|
-
|
|
2312
|
-
function openCommandPalette() {
|
|
2313
|
-
this.commandQuery = "";
|
|
2314
|
-
this.commandPaletteOpen = true;
|
|
2315
|
-
this.$nextTick(() => this.$refs?.commandSearch?.focus());
|
|
2316
|
-
}
|
|
2317
|
-
|
|
2318
|
-
function closeCommandPalette() {
|
|
2319
|
-
this.commandPaletteOpen = false;
|
|
2320
|
-
this.commandQuery = "";
|
|
2321
|
-
}
|
|
2322
|
-
|
|
2323
|
-
async function runCommand(command) {
|
|
2324
|
-
if (!command) return;
|
|
2325
|
-
const payload = command.payload || {};
|
|
2326
|
-
this.closeCommandPalette();
|
|
2327
|
-
if (command.action === "openView") {
|
|
2328
|
-
await this.switchView(payload.view);
|
|
2329
|
-
} else if (command.action === "agentPreset") {
|
|
2330
|
-
this.showOffline = true;
|
|
2331
|
-
this.agentPresetFilter = payload.preset || "";
|
|
2332
|
-
await this.switchView("agents");
|
|
2333
|
-
} else if (command.action === "copy") {
|
|
2334
|
-
await copyText(payload.value || "");
|
|
2335
|
-
} else if (command.action === "messageAgent") {
|
|
2336
|
-
const agent = this.agentsById[payload.agentId];
|
|
2337
|
-
if (agent) this.openComposeToAgent(agent);
|
|
2338
|
-
} else if (command.action === "pairAgent") {
|
|
2339
|
-
this.openPairInvite(payload.agentId);
|
|
2340
|
-
} else if (command.action === "filterTag") {
|
|
2341
|
-
this.agentTagFilter = payload.tag || "";
|
|
2342
|
-
this.tagFilter = payload.tag || "";
|
|
2343
|
-
await this.switchView("agents");
|
|
2344
|
-
} else if (command.action === "exportActivity") {
|
|
2345
|
-
this.exportActivity(payload.format || "markdown");
|
|
2346
|
-
}
|
|
2347
|
-
}
|
|
2348
|
-
|
|
2349
|
-
function exportActivity(format) {
|
|
2350
|
-
exportDocument(this, "timeline", format, {
|
|
2351
|
-
title: "Agent Relay Timeline",
|
|
2352
|
-
items: this.activityItems,
|
|
2353
|
-
});
|
|
2354
|
-
}
|
|
2355
|
-
|
|
2356
|
-
function exportThread(thread, format) {
|
|
2357
|
-
if (!thread) return;
|
|
2358
|
-
exportDocument(this, "thread-" + safeFilename(thread.peer), format, {
|
|
2359
|
-
title: "Thread: " + this.conversationTitle(thread),
|
|
2360
|
-
thread,
|
|
2361
|
-
messages: thread.messages || [],
|
|
2362
|
-
});
|
|
2363
|
-
}
|
|
2364
|
-
|
|
2365
|
-
function exportPair(pair, format) {
|
|
2366
|
-
if (!pair) return;
|
|
2367
|
-
exportDocument(this, "pair-" + safeFilename(pair.id), format, {
|
|
2368
|
-
title: "Pair: " + pair.id,
|
|
2369
|
-
pair,
|
|
2370
|
-
messages: pairMessages(this, pair),
|
|
2371
|
-
});
|
|
2372
|
-
}
|
|
2373
|
-
|
|
2374
|
-
async function exportTask(task, format) {
|
|
2375
|
-
if (!task) return;
|
|
2376
|
-
let events = this.taskEventCache[task.id] || [];
|
|
2377
|
-
if (!events.length) {
|
|
2378
|
-
try {
|
|
2379
|
-
events = await this.api("GET", "/tasks/" + task.id + "/events");
|
|
2380
|
-
this.taskEventCache = { ...this.taskEventCache, [task.id]: events };
|
|
2381
|
-
} catch {
|
|
2382
|
-
events = [];
|
|
2383
|
-
}
|
|
2384
|
-
}
|
|
2385
|
-
exportDocument(this, "task-" + task.id, format, {
|
|
2386
|
-
title: "Task: " + (task.title || "#" + task.id),
|
|
2387
|
-
task,
|
|
2388
|
-
events,
|
|
2389
|
-
});
|
|
2390
|
-
}
|
|
2391
|
-
|
|
2392
|
-
function pairMessages(vm, pair) {
|
|
2393
|
-
return (vm.messages || []).filter((msg) =>
|
|
2394
|
-
msg.payload?.pairId === pair.id ||
|
|
2395
|
-
(msg.payload?.pairEvent && [pair.requesterId, pair.targetId].includes(msg.from) && [pair.requesterId, pair.targetId].includes(msg.to))
|
|
2396
|
-
);
|
|
2397
|
-
}
|
|
2398
|
-
|
|
2399
|
-
function exportDocument(vm, scope, format, data) {
|
|
2400
|
-
const normalizedFormat = format === "json" ? "json" : "markdown";
|
|
2401
|
-
const filename = `agent-relay-${scope}-${new Date().toISOString().slice(0, 10)}.${normalizedFormat === "json" ? "json" : "md"}`;
|
|
2402
|
-
const text = normalizedFormat === "json" ? JSON.stringify(exportJson(data), null, 2) : exportMarkdown(vm, data);
|
|
2403
|
-
downloadText(filename, text, normalizedFormat === "json" ? "application/json" : "text/markdown");
|
|
2404
|
-
}
|
|
2405
|
-
|
|
2406
|
-
function exportJson(data) {
|
|
2407
|
-
return {
|
|
2408
|
-
exportedAt: new Date().toISOString(),
|
|
2409
|
-
...data,
|
|
2410
|
-
};
|
|
2411
|
-
}
|
|
2412
|
-
|
|
2413
|
-
function exportMarkdown(vm, data) {
|
|
2414
|
-
const lines = ["# " + data.title, "", "Exported: " + new Date().toISOString(), ""];
|
|
2415
|
-
if (data.thread) {
|
|
2416
|
-
lines.push("## Messages", "");
|
|
2417
|
-
appendMessages(lines, vm, data.messages || []);
|
|
2418
|
-
} else if (data.pair) {
|
|
2419
|
-
lines.push("## Pair", "", "- ID: " + data.pair.id, "- Status: " + data.pair.status, "- Requester: " + vm.displayTarget(data.pair.requesterId), "- Target: " + vm.displayTarget(data.pair.targetId));
|
|
2420
|
-
if (data.pair.objective) lines.push("- Objective: " + data.pair.objective);
|
|
2421
|
-
lines.push("", "## Messages", "");
|
|
2422
|
-
appendMessages(lines, vm, data.messages || []);
|
|
2423
|
-
} else if (data.task) {
|
|
2424
|
-
lines.push("## Task", "", "- ID: " + data.task.id, "- Status: " + data.task.status, "- Severity: " + (data.task.severity || "info"), "- Target: " + vm.displayTarget(data.task.target || ""));
|
|
2425
|
-
if (data.task.claimedBy) lines.push("- Claimed by: " + vm.displayTarget(data.task.claimedBy));
|
|
2426
|
-
if (data.task.title) lines.push("- Title: " + data.task.title);
|
|
2427
|
-
if (data.task.body) lines.push("", data.task.body);
|
|
2428
|
-
lines.push("", "## History", "");
|
|
2429
|
-
appendEvents(lines, vm, data.events || []);
|
|
2430
|
-
} else {
|
|
2431
|
-
lines.push("## Events", "");
|
|
2432
|
-
appendActivity(lines, vm, data.items || []);
|
|
2433
|
-
}
|
|
2434
|
-
return lines.join("\n").trim() + "\n";
|
|
2435
|
-
}
|
|
2436
|
-
|
|
2437
|
-
function appendMessages(lines, vm, messages) {
|
|
2438
|
-
if (!messages.length) {
|
|
2439
|
-
lines.push("_No messages loaded._", "");
|
|
2440
|
-
return;
|
|
2441
|
-
}
|
|
2442
|
-
for (const msg of messages) {
|
|
2443
|
-
lines.push(`### #${msg.id} ${vm.displayTarget(msg.from)} -> ${vm.displayTarget(msg.to)}`, "");
|
|
2444
|
-
if (msg.createdAt) lines.push("- Created: " + msg.createdAt);
|
|
2445
|
-
if (msg.channel) lines.push("- Channel: " + msg.channel);
|
|
2446
|
-
if (msg.subject) lines.push("- Subject: " + msg.subject);
|
|
2447
|
-
if (msg.claimedBy) lines.push("- Claimed by: " + vm.displayTarget(msg.claimedBy));
|
|
2448
|
-
lines.push("", messageBody.call(vm, msg) || "", "");
|
|
2449
|
-
}
|
|
2450
|
-
}
|
|
2451
|
-
|
|
2452
|
-
function appendEvents(lines, vm, events) {
|
|
2453
|
-
if (!events.length) {
|
|
2454
|
-
lines.push("_No task events loaded._", "");
|
|
2455
|
-
return;
|
|
2456
|
-
}
|
|
2457
|
-
for (const event of events) {
|
|
2458
|
-
lines.push(`- ${event.createdAt || ""} [${event.severity || "info"}] ${event.type || "event"}: ${event.title || event.body || ""}`.trim());
|
|
2459
|
-
}
|
|
2460
|
-
}
|
|
2461
|
-
|
|
2462
|
-
function appendActivity(lines, vm, items) {
|
|
2463
|
-
if (!items.length) {
|
|
2464
|
-
lines.push("_No activity loaded._", "");
|
|
2465
|
-
return;
|
|
2466
|
-
}
|
|
2467
|
-
for (const item of items) {
|
|
2468
|
-
const when = item.ts ? new Date(item.ts).toISOString() : "";
|
|
2469
|
-
const meta = item.meta ? " - " + item.meta : "";
|
|
2470
|
-
lines.push(`- ${when} [${item.kind}] ${item.title}${meta}`);
|
|
2471
|
-
if (item.body) lines.push(" " + item.body.replace(/\n/g, "\n "));
|
|
2472
|
-
}
|
|
2473
|
-
}
|
|
2474
|
-
|
|
2475
|
-
function downloadText(filename, text, type) {
|
|
2476
|
-
if (typeof document === "undefined" || typeof URL === "undefined" || typeof Blob === "undefined") {
|
|
2477
|
-
void copyText(text);
|
|
2478
|
-
return;
|
|
2479
|
-
}
|
|
2480
|
-
const url = URL.createObjectURL(new Blob([text], { type }));
|
|
2481
|
-
const link = document.createElement("a");
|
|
2482
|
-
link.href = url;
|
|
2483
|
-
link.download = filename;
|
|
2484
|
-
link.click();
|
|
2485
|
-
URL.revokeObjectURL(url);
|
|
2486
|
-
}
|
|
2487
|
-
|
|
2488
|
-
function safeFilename(value) {
|
|
2489
|
-
return String(value || "export").replace(/[^a-z0-9._-]+/gi, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "export";
|
|
2490
|
-
}
|
|
2491
|
-
|
|
2492
|
-
function openPairMessage(pair, fromId) {
|
|
2493
|
-
if (!pair) return;
|
|
2494
|
-
this.pairMessage = {
|
|
2495
|
-
...DEFAULT_PAIR_MESSAGE,
|
|
2496
|
-
pairId: pair.id,
|
|
2497
|
-
from: fromId || pair.requesterId || pair.targetId || "",
|
|
2498
|
-
};
|
|
2499
|
-
this.pairMessageOpen = true;
|
|
2500
|
-
this.$nextTick(() => this.$refs?.pairMessageBody?.focus());
|
|
2501
|
-
}
|
|
2502
|
-
|
|
2503
|
-
function openPairInvite(requesterId) {
|
|
2504
|
-
this.pairInvite = {
|
|
2505
|
-
...DEFAULT_PAIR_INVITE,
|
|
2506
|
-
requesterId: requesterId || this.selectedAgent || "",
|
|
2507
|
-
};
|
|
2508
|
-
this.pairInviteOpen = true;
|
|
2509
|
-
this.$nextTick(() => this.$refs?.pairInviteObjective?.focus());
|
|
2510
|
-
}
|
|
2511
|
-
|
|
2512
|
-
function closePairInvite() {
|
|
2513
|
-
this.pairInviteOpen = false;
|
|
2514
|
-
this.pairInvite = { ...DEFAULT_PAIR_INVITE };
|
|
2515
|
-
}
|
|
2516
|
-
|
|
2517
|
-
async function doCreatePair() {
|
|
2518
|
-
if (!this.pairInvite.requesterId || !this.pairInvite.targetId) {
|
|
2519
|
-
alert("Requester and Target are required.");
|
|
2520
|
-
return;
|
|
2521
|
-
}
|
|
2522
|
-
if (this.pairInvite.requesterId === this.pairInvite.targetId) {
|
|
2523
|
-
alert("Requester and Target must be different agents.");
|
|
2524
|
-
return;
|
|
2525
|
-
}
|
|
2526
|
-
|
|
2527
|
-
try {
|
|
2528
|
-
const payload = {
|
|
2529
|
-
from: this.pairInvite.requesterId,
|
|
2530
|
-
target: this.pairInvite.targetId,
|
|
2531
|
-
};
|
|
2532
|
-
if (this.pairInvite.objective) payload.objective = this.pairInvite.objective;
|
|
2533
|
-
await this.api("POST", "/pairs", payload);
|
|
2534
|
-
this.recordOperatorActivity({
|
|
2535
|
-
title: "Pair invite sent",
|
|
2536
|
-
body: payload.objective || "",
|
|
2537
|
-
meta: `${this.displayTarget(payload.from)} <-> ${this.displayTarget(payload.target)}`,
|
|
2538
|
-
icon: "ti-link-plus",
|
|
2539
|
-
kind: "pair",
|
|
2540
|
-
view: "pairs",
|
|
2541
|
-
});
|
|
2542
|
-
this.closePairInvite();
|
|
2543
|
-
await Promise.all([this.fetchPairs(), this.fetchMessages()]);
|
|
2544
|
-
} catch (e) {
|
|
2545
|
-
alert("Pair invite failed: " + e.message);
|
|
2546
|
-
}
|
|
2547
|
-
}
|
|
2548
|
-
|
|
2549
|
-
function openAgentSpawn() {
|
|
2550
|
-
this.agentSpawn = { ...DEFAULT_AGENT_SPAWN };
|
|
2551
|
-
this.agentDirectoryBrowser = { open: false, loading: false, path: "", parent: "", home: "", cwd: "", entries: [], error: "" };
|
|
2552
|
-
this.agentSpawnOpen = true;
|
|
2553
|
-
this.$nextTick(() => this.$refs?.agentSpawnCwd?.focus());
|
|
2554
|
-
}
|
|
2555
|
-
|
|
2556
|
-
function closeAgentSpawn() {
|
|
2557
|
-
this.agentSpawnOpen = false;
|
|
2558
|
-
this.agentSpawn = { ...DEFAULT_AGENT_SPAWN };
|
|
2559
|
-
}
|
|
2560
|
-
|
|
2561
|
-
async function openAgentDirectoryBrowser() {
|
|
2562
|
-
this.agentDirectoryBrowser = { ...this.agentDirectoryBrowser, open: true };
|
|
2563
|
-
await this.browseAgentDirectory(this.agentSpawn.cwd || "");
|
|
2564
|
-
}
|
|
2565
|
-
|
|
2566
|
-
async function browseAgentDirectory(path) {
|
|
2567
|
-
this.agentDirectoryBrowser = { ...this.agentDirectoryBrowser, loading: true, error: "" };
|
|
2568
|
-
try {
|
|
2569
|
-
const query = path ? "?path=" + encodeURIComponent(path) : "";
|
|
2570
|
-
const listing = await this.api("GET", "/agents/spawn/directories" + query);
|
|
2571
|
-
this.agentDirectoryBrowser = {
|
|
2572
|
-
open: true,
|
|
2573
|
-
loading: false,
|
|
2574
|
-
path: listing.path || "",
|
|
2575
|
-
parent: listing.parent || "",
|
|
2576
|
-
home: listing.home || "",
|
|
2577
|
-
cwd: listing.cwd || "",
|
|
2578
|
-
entries: listing.entries || [],
|
|
2579
|
-
error: "",
|
|
2580
|
-
};
|
|
2581
|
-
this.agentSpawn = { ...this.agentSpawn, cwd: listing.path || this.agentSpawn.cwd };
|
|
2582
|
-
} catch (e) {
|
|
2583
|
-
this.agentDirectoryBrowser = { ...this.agentDirectoryBrowser, loading: false, error: e.message };
|
|
2584
|
-
}
|
|
2585
|
-
}
|
|
2586
|
-
|
|
2587
|
-
function selectAgentDirectory(path) {
|
|
2588
|
-
this.agentSpawn = { ...this.agentSpawn, cwd: path || this.agentDirectoryBrowser.path };
|
|
2589
|
-
this.agentDirectoryBrowser = { ...this.agentDirectoryBrowser, open: false };
|
|
2590
|
-
}
|
|
2591
|
-
|
|
2592
|
-
async function doSpawnAgent() {
|
|
2593
|
-
if (this.agentSpawn.provider !== "codex") {
|
|
2594
|
-
alert("Only Codex live sessions can be spawned from the dashboard right now.");
|
|
2595
|
-
return;
|
|
2596
|
-
}
|
|
2597
|
-
try {
|
|
2598
|
-
const payload = {
|
|
2599
|
-
provider: "codex",
|
|
2600
|
-
approvalMode: this.agentSpawn.approvalMode || "guarded",
|
|
2601
|
-
};
|
|
2602
|
-
if (this.agentSpawn.cwd) payload.cwd = this.agentSpawn.cwd;
|
|
2603
|
-
if (this.agentSpawn.label) payload.label = this.agentSpawn.label;
|
|
2604
|
-
const result = await this.api("POST", "/agents/spawn", payload);
|
|
2605
|
-
this.recordOperatorActivity({
|
|
2606
|
-
title: "Codex agent spawn requested",
|
|
2607
|
-
body: result?.cwd || payload.cwd || "",
|
|
2608
|
-
meta: result?.pid ? "pid " + result.pid : "starting",
|
|
2609
|
-
icon: "ti-plus",
|
|
2610
|
-
kind: "state",
|
|
2611
|
-
view: "agents",
|
|
2612
|
-
});
|
|
2613
|
-
this.closeAgentSpawn();
|
|
2614
|
-
await Promise.all([this.fetchAgents(), this.fetchActivityEvents()]);
|
|
2615
|
-
} catch (e) {
|
|
2616
|
-
alert("Spawn failed: " + e.message);
|
|
2617
|
-
}
|
|
2618
|
-
}
|
|
2619
|
-
|
|
2620
|
-
// --- Orchestrator methods ---
|
|
2621
|
-
|
|
2622
|
-
function openOrchestratorSpawn() {
|
|
2623
|
-
const online = this.orchestrators.filter((o) => o.status === "online");
|
|
2624
|
-
if (online.length === 0) return alert("No orchestrators online");
|
|
2625
|
-
this.spawnOrchId = online[0].id;
|
|
2626
|
-
this.spawnProvider = "claude";
|
|
2627
|
-
this.spawnCwd = "";
|
|
2628
|
-
this.spawnLabel = "";
|
|
2629
|
-
this.spawnApproval = "guarded";
|
|
2630
|
-
this.spawnPrompt = "";
|
|
2631
|
-
this.spawnDirListing = null;
|
|
2632
|
-
this.orchestratorSpawnOpen = true;
|
|
2633
|
-
}
|
|
2634
|
-
|
|
2635
|
-
function openOrchestratorSpawnFor(orchId) {
|
|
2636
|
-
this.spawnOrchId = orchId;
|
|
2637
|
-
this.spawnProvider = "claude";
|
|
2638
|
-
this.spawnCwd = "";
|
|
2639
|
-
this.spawnLabel = "";
|
|
2640
|
-
this.spawnApproval = "guarded";
|
|
2641
|
-
this.spawnPrompt = "";
|
|
2642
|
-
this.spawnDirListing = null;
|
|
2643
|
-
this.orchestratorSpawnOpen = true;
|
|
2644
|
-
}
|
|
2645
|
-
|
|
2646
|
-
async function browseOrchestratorDirs() {
|
|
2647
|
-
try {
|
|
2648
|
-
const query = this.spawnCwd ? "?path=" + encodeURIComponent(this.spawnCwd) : "";
|
|
2649
|
-
this.spawnDirListing = await this.api("GET", "/agents/spawn/directories" + query);
|
|
2650
|
-
} catch (e) {
|
|
2651
|
-
alert("Directory browse failed: " + e.message);
|
|
2652
|
-
}
|
|
2653
|
-
}
|
|
2654
|
-
|
|
2655
|
-
async function submitOrchestratorSpawn() {
|
|
2656
|
-
if (!this.spawnOrchId) return alert("Select an orchestrator");
|
|
2657
|
-
try {
|
|
2658
|
-
const payload = {
|
|
2659
|
-
provider: this.spawnProvider,
|
|
2660
|
-
approvalMode: this.spawnApproval,
|
|
2661
|
-
};
|
|
2662
|
-
if (this.spawnCwd) payload.cwd = this.spawnCwd;
|
|
2663
|
-
if (this.spawnLabel) payload.label = this.spawnLabel;
|
|
2664
|
-
if (this.spawnPrompt) payload.prompt = this.spawnPrompt;
|
|
2665
|
-
await this.api("POST", "/orchestrators/" + encodeURIComponent(this.spawnOrchId) + "/spawn", payload);
|
|
2666
|
-
this.orchestratorSpawnOpen = false;
|
|
2667
|
-
this.recordOperatorActivity({
|
|
2668
|
-
title: `${this.spawnProvider} agent spawn requested`,
|
|
2669
|
-
body: this.spawnCwd || "",
|
|
2670
|
-
meta: this.spawnOrchId,
|
|
2671
|
-
icon: "ti-plus",
|
|
2672
|
-
kind: "state",
|
|
2673
|
-
view: "orchestrators",
|
|
2674
|
-
});
|
|
2675
|
-
await Promise.all([this.fetchOrchestrators(), this.fetchActivityEvents()]);
|
|
2676
|
-
} catch (e) {
|
|
2677
|
-
alert("Spawn failed: " + e.message);
|
|
2678
|
-
}
|
|
2679
|
-
}
|
|
2680
|
-
|
|
2681
|
-
async function orchestratorAction(orchId, action, agentId) {
|
|
2682
|
-
const label = action === "restart" ? "Restart" : "Shutdown";
|
|
2683
|
-
if (!confirm(`${label} agent "${agentId || "all"}" on orchestrator "${orchId}"?`)) return;
|
|
2684
|
-
try {
|
|
2685
|
-
await this.api("POST", "/orchestrators/" + encodeURIComponent(orchId) + "/actions", { action, agentId });
|
|
2686
|
-
this.recordOperatorActivity({
|
|
2687
|
-
title: `Agent ${action} requested`,
|
|
2688
|
-
body: agentId || "all agents",
|
|
2689
|
-
meta: orchId,
|
|
2690
|
-
icon: action === "restart" ? "ti-refresh" : "ti-power",
|
|
2691
|
-
kind: "state",
|
|
2692
|
-
view: "orchestrators",
|
|
2693
|
-
});
|
|
2694
|
-
await Promise.all([this.fetchOrchestrators(), this.fetchActivityEvents()]);
|
|
2695
|
-
} catch (e) {
|
|
2696
|
-
alert(`${label} failed: ` + e.message);
|
|
2697
|
-
}
|
|
2698
|
-
}
|
|
2699
|
-
|
|
2700
|
-
async function deleteOrchestrator(orchId) {
|
|
2701
|
-
if (!confirm(`Remove orchestrator "${orchId}"? This will NOT stop its managed agents.`)) return;
|
|
2702
|
-
try {
|
|
2703
|
-
await this.api("DELETE", "/orchestrators/" + encodeURIComponent(orchId));
|
|
2704
|
-
await this.fetchOrchestrators();
|
|
2705
|
-
} catch (e) {
|
|
2706
|
-
alert("Delete failed: " + e.message);
|
|
2707
|
-
}
|
|
2708
|
-
}
|
|
2709
|
-
|
|
2710
|
-
function closePairMessage() {
|
|
2711
|
-
this.pairMessageOpen = false;
|
|
2712
|
-
this.pairMessage = { ...DEFAULT_PAIR_MESSAGE };
|
|
2713
|
-
}
|
|
2714
|
-
|
|
2715
|
-
async function doSendPairMessage() {
|
|
2716
|
-
if (!this.pairMessage.pairId || !this.pairMessage.from || !this.pairMessage.body) {
|
|
2717
|
-
alert("Pair, From, and Message are required.");
|
|
2718
|
-
return;
|
|
2719
|
-
}
|
|
2720
|
-
|
|
2721
|
-
try {
|
|
2722
|
-
const payload = { from: this.pairMessage.from, body: this.pairMessage.body };
|
|
2723
|
-
if (this.pairMessage.subject) payload.subject = this.pairMessage.subject;
|
|
2724
|
-
await this.api("POST", "/pairs/" + encodeURIComponent(this.pairMessage.pairId) + "/messages", payload);
|
|
2725
|
-
this.recordOperatorActivity({
|
|
2726
|
-
title: "Pair message sent",
|
|
2727
|
-
body: payload.subject || payload.body,
|
|
2728
|
-
meta: "from " + this.displayTarget(payload.from),
|
|
2729
|
-
icon: "ti-messages",
|
|
2730
|
-
kind: "pair",
|
|
2731
|
-
view: "pairs",
|
|
2732
|
-
});
|
|
2733
|
-
this.closePairMessage();
|
|
2734
|
-
await Promise.all([this.fetchPairs(), this.fetchMessages()]);
|
|
2735
|
-
} catch (e) {
|
|
2736
|
-
alert("Pair message failed: " + e.message);
|
|
2737
|
-
}
|
|
2738
|
-
}
|
|
2739
|
-
|
|
2740
|
-
async function doAcceptPair(pair) {
|
|
2741
|
-
if (!pair) return;
|
|
2742
|
-
try {
|
|
2743
|
-
await this.api("POST", "/pairs/" + encodeURIComponent(pair.id) + "/accept", { agentId: pair.targetId });
|
|
2744
|
-
this.recordOperatorActivity({
|
|
2745
|
-
title: "Pair accepted",
|
|
2746
|
-
body: pair.objective || "",
|
|
2747
|
-
meta: `${this.displayTarget(pair.requesterId)} <-> ${this.displayTarget(pair.targetId)}`,
|
|
2748
|
-
icon: "ti-check",
|
|
2749
|
-
kind: "pair",
|
|
2750
|
-
view: "pairs",
|
|
2751
|
-
});
|
|
2752
|
-
await Promise.all([this.fetchPairs(), this.fetchMessages()]);
|
|
2753
|
-
} catch (e) {
|
|
2754
|
-
alert("Accept failed: " + e.message);
|
|
2755
|
-
}
|
|
2756
|
-
}
|
|
2757
|
-
|
|
2758
|
-
async function doRejectPair(pair) {
|
|
2759
|
-
if (!pair) return;
|
|
2760
|
-
try {
|
|
2761
|
-
await this.api("POST", "/pairs/" + encodeURIComponent(pair.id) + "/reject", { agentId: pair.targetId });
|
|
2762
|
-
this.recordOperatorActivity({
|
|
2763
|
-
title: "Pair rejected",
|
|
2764
|
-
body: pair.objective || "",
|
|
2765
|
-
meta: `${this.displayTarget(pair.requesterId)} <-> ${this.displayTarget(pair.targetId)}`,
|
|
2766
|
-
icon: "ti-x",
|
|
2767
|
-
kind: "pair",
|
|
2768
|
-
view: "pairs",
|
|
2769
|
-
});
|
|
2770
|
-
await Promise.all([this.fetchPairs(), this.fetchMessages()]);
|
|
2771
|
-
} catch (e) {
|
|
2772
|
-
alert("Reject failed: " + e.message);
|
|
2773
|
-
}
|
|
2774
|
-
}
|
|
2775
|
-
|
|
2776
|
-
async function doHangupPair(pair, agentId) {
|
|
2777
|
-
if (!pair) return;
|
|
2778
|
-
try {
|
|
2779
|
-
await this.api("POST", "/pairs/" + encodeURIComponent(pair.id) + "/hangup", { agentId: agentId || pair.requesterId });
|
|
2780
|
-
this.recordOperatorActivity({
|
|
2781
|
-
title: "Pair hung up",
|
|
2782
|
-
body: pair.objective || "",
|
|
2783
|
-
meta: "by " + this.displayTarget(agentId || pair.requesterId),
|
|
2784
|
-
icon: "ti-phone-off",
|
|
2785
|
-
kind: "pair",
|
|
2786
|
-
view: "pairs",
|
|
2787
|
-
});
|
|
2788
|
-
await Promise.all([this.fetchPairs(), this.fetchMessages()]);
|
|
2789
|
-
} catch (e) {
|
|
2790
|
-
alert("Hang up failed: " + e.message);
|
|
2791
|
-
}
|
|
2792
|
-
}
|
|
2793
|
-
|
|
2794
|
-
function createAgentActions() {
|
|
2795
|
-
return {
|
|
2796
|
-
openAgentDetail(agent) {
|
|
2797
|
-
if (!agent) return;
|
|
2798
|
-
this.agentDetailId = agent.id;
|
|
2799
|
-
this.agentDetailOpen = true;
|
|
2800
|
-
},
|
|
2801
|
-
|
|
2802
|
-
closeAgentDetail() {
|
|
2803
|
-
this.agentDetailOpen = false;
|
|
2804
|
-
},
|
|
2805
|
-
|
|
2806
|
-
openChannelDetail(channel) {
|
|
2807
|
-
if (!channel) return;
|
|
2808
|
-
this.channelDetailId = channel.id;
|
|
2809
|
-
this.channelDetailOpen = true;
|
|
2810
|
-
},
|
|
2811
|
-
|
|
2812
|
-
closeChannelDetail() {
|
|
2813
|
-
this.channelDetailOpen = false;
|
|
2814
|
-
},
|
|
2815
|
-
|
|
2816
|
-
openRename(agent) {
|
|
2817
|
-
this.renameModal = { show: true, agentId: agent.id, label: agent.label || "" };
|
|
2818
|
-
this.$nextTick(() => this.$refs.renameInput?.focus());
|
|
2819
|
-
},
|
|
2820
|
-
|
|
2821
|
-
async doRename() {
|
|
2822
|
-
const label = this.renameModal.label.trim() || null;
|
|
2823
|
-
try {
|
|
2824
|
-
await this.api("PATCH", "/agents/" + this.renameModal.agentId + "/label", { label });
|
|
2825
|
-
this.renameModal.show = false;
|
|
2826
|
-
} catch (e) {
|
|
2827
|
-
alert("Rename failed: " + e.message);
|
|
2828
|
-
}
|
|
2829
|
-
},
|
|
2830
|
-
|
|
2831
|
-
openConfirm(title, message, action) {
|
|
2832
|
-
this.confirmModal = { show: true, title, message, action };
|
|
2833
|
-
},
|
|
2834
|
-
|
|
2835
|
-
async doDeleteAgent(id) {
|
|
2836
|
-
try {
|
|
2837
|
-
await this.api("DELETE", "/agents/" + id);
|
|
2838
|
-
if (this.selectedAgent === id) this.selectedAgent = "";
|
|
2839
|
-
this.agents = this.agents.filter((agent) => agent.id !== id);
|
|
2840
|
-
delete this.agentsById[id];
|
|
2841
|
-
} catch (e) {
|
|
2842
|
-
alert("Delete failed: " + e.message);
|
|
2843
|
-
}
|
|
2844
|
-
},
|
|
2845
|
-
|
|
2846
|
-
async doAgentAction(agent, action) {
|
|
2847
|
-
if (!agent || !action) return;
|
|
2848
|
-
try {
|
|
2849
|
-
const result = await this.api("POST", "/agents/" + encodeURIComponent(agent.id) + "/actions", { action });
|
|
2850
|
-
this.recordOperatorActivity({
|
|
2851
|
-
title: action === "restart" ? "Agent restart requested" : "Agent shutdown requested",
|
|
2852
|
-
body: this.displayName(agent),
|
|
2853
|
-
meta: agent.id,
|
|
2854
|
-
icon: action === "restart" ? "ti-refresh" : "ti-power",
|
|
2855
|
-
kind: "state",
|
|
2856
|
-
view: "agents",
|
|
2857
|
-
agentId: agent.id,
|
|
2858
|
-
messageId: result?.message?.id,
|
|
2859
|
-
});
|
|
2860
|
-
await Promise.all([this.fetchMessages(), this.fetchActivityEvents()]);
|
|
2861
|
-
} catch (e) {
|
|
2862
|
-
alert("Agent action failed: " + e.message);
|
|
2863
|
-
}
|
|
2864
|
-
},
|
|
2865
|
-
};
|
|
2866
|
-
}
|
|
2867
|
-
|
|
2868
|
-
function createChartMethods() {
|
|
2869
|
-
return {
|
|
2870
|
-
renderCharts,
|
|
2871
|
-
renderVolumeChart,
|
|
2872
|
-
renderStatusChart,
|
|
2873
|
-
renderAgentChart,
|
|
2874
|
-
destroyAllCharts,
|
|
2875
|
-
};
|
|
2876
|
-
}
|
|
2877
|
-
|
|
2878
|
-
function renderCharts() {
|
|
2879
|
-
this.renderVolumeChart();
|
|
2880
|
-
this.renderStatusChart();
|
|
2881
|
-
this.renderAgentChart();
|
|
2882
|
-
}
|
|
2883
|
-
|
|
2884
|
-
function renderVolumeChart() {
|
|
2885
|
-
const data = buildVolumeSeries(this.messages);
|
|
2886
|
-
|
|
2887
|
-
if (this.chartInstances.volume) {
|
|
2888
|
-
this.chartInstances.volume.updateSeries([{ name: "Messages", data }]);
|
|
2889
|
-
return;
|
|
2890
|
-
}
|
|
2891
|
-
|
|
2892
|
-
const el = document.querySelector("#chart-volume");
|
|
2893
|
-
if (!el) return;
|
|
2894
|
-
|
|
2895
|
-
this.chartInstances.volume = new ApexCharts(el, {
|
|
2896
|
-
chart: { type: "area", height: 280, background: "transparent", toolbar: { show: false }, animations: { dynamicAnimation: { speed: 350 } } },
|
|
2897
|
-
theme: { mode: "dark" },
|
|
2898
|
-
series: [{ name: "Messages", data }],
|
|
2899
|
-
xaxis: { type: "datetime" },
|
|
2900
|
-
stroke: { curve: "smooth", width: 2 },
|
|
2901
|
-
fill: { type: "gradient", gradient: { opacityFrom: 0.4, opacityTo: 0 } },
|
|
2902
|
-
dataLabels: { enabled: false },
|
|
2903
|
-
colors: ["#4299e1"],
|
|
2904
|
-
grid: { borderColor: "rgba(255,255,255,0.06)" },
|
|
2905
|
-
});
|
|
2906
|
-
this.chartInstances.volume.render();
|
|
2907
|
-
}
|
|
2908
|
-
|
|
2909
|
-
function buildVolumeSeries(messages) {
|
|
2910
|
-
const buckets = {};
|
|
2911
|
-
for (const msg of messages) {
|
|
2912
|
-
const day = msg.createdAt ? new Date(msg.createdAt).toISOString().split("T")[0] : null;
|
|
2913
|
-
if (day) buckets[day] = (buckets[day] || 0) + 1;
|
|
2914
|
-
}
|
|
2915
|
-
return Object.entries(buckets)
|
|
2916
|
-
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
2917
|
-
.map(([day, count]) => ({ x: day, y: count }));
|
|
2918
|
-
}
|
|
2919
|
-
|
|
2920
|
-
function renderStatusChart() {
|
|
2921
|
-
const { labels, series } = countAgentStatuses(this.agents);
|
|
2922
|
-
const colorMap = { idle: "#4299e1", busy: "#ecc94b" };
|
|
2923
|
-
|
|
2924
|
-
if (this.chartInstances.status) {
|
|
2925
|
-
this.chartInstances.status.updateOptions({
|
|
2926
|
-
series,
|
|
2927
|
-
labels,
|
|
2928
|
-
colors: labels.map((label) => colorMap[label] || "#718096"),
|
|
2929
|
-
});
|
|
2930
|
-
return;
|
|
2931
|
-
}
|
|
2932
|
-
|
|
2933
|
-
const el = document.querySelector("#chart-status");
|
|
2934
|
-
if (!el) return;
|
|
2935
|
-
|
|
2936
|
-
this.chartInstances.status = new ApexCharts(el, {
|
|
2937
|
-
chart: { type: "donut", height: 280, background: "transparent", animations: { dynamicAnimation: { speed: 350 } } },
|
|
2938
|
-
theme: { mode: "dark" },
|
|
2939
|
-
series,
|
|
2940
|
-
labels,
|
|
2941
|
-
colors: labels.map((label) => colorMap[label] || "#718096"),
|
|
2942
|
-
legend: { position: "bottom" },
|
|
2943
|
-
dataLabels: { enabled: true },
|
|
2944
|
-
});
|
|
2945
|
-
this.chartInstances.status.render();
|
|
2946
|
-
}
|
|
2947
|
-
|
|
2948
|
-
function countAgentStatuses(agents) {
|
|
2949
|
-
const counts = { idle: 0, busy: 0 };
|
|
2950
|
-
for (const agent of agents) {
|
|
2951
|
-
if (agent.status in counts) counts[agent.status] += 1;
|
|
2952
|
-
}
|
|
2953
|
-
|
|
2954
|
-
const labels = Object.keys(counts);
|
|
2955
|
-
return { labels, series: labels.map((key) => counts[key]) };
|
|
2956
|
-
}
|
|
2957
|
-
|
|
2958
|
-
function renderAgentChart() {
|
|
2959
|
-
const sorted = countMessagesByAgent(this);
|
|
2960
|
-
|
|
2961
|
-
if (this.chartInstances.agents) {
|
|
2962
|
-
this.chartInstances.agents.updateOptions({
|
|
2963
|
-
series: [{ name: "Messages", data: sorted.map(([, count]) => count) }],
|
|
2964
|
-
xaxis: { categories: sorted.map(([name]) => name) },
|
|
2965
|
-
});
|
|
2966
|
-
return;
|
|
2967
|
-
}
|
|
2968
|
-
|
|
2969
|
-
const el = document.querySelector("#chart-agents");
|
|
2970
|
-
if (!el) return;
|
|
2971
|
-
|
|
2972
|
-
this.chartInstances.agents = new ApexCharts(el, {
|
|
2973
|
-
chart: { type: "bar", height: 280, background: "transparent", toolbar: { show: false }, animations: { dynamicAnimation: { speed: 350 } } },
|
|
2974
|
-
theme: { mode: "dark" },
|
|
2975
|
-
series: [{ name: "Messages", data: sorted.map(([, count]) => count) }],
|
|
2976
|
-
xaxis: { categories: sorted.map(([name]) => name) },
|
|
2977
|
-
colors: ["#4299e1"],
|
|
2978
|
-
plotOptions: { bar: { borderRadius: 4, distributed: true } },
|
|
2979
|
-
legend: { show: false },
|
|
2980
|
-
grid: { borderColor: "rgba(255,255,255,0.06)" },
|
|
2981
|
-
});
|
|
2982
|
-
this.chartInstances.agents.render();
|
|
2983
|
-
}
|
|
2984
|
-
|
|
2985
|
-
function countMessagesByAgent(vm) {
|
|
2986
|
-
const counts = {};
|
|
2987
|
-
for (const msg of vm.messages) {
|
|
2988
|
-
const name = vm.displayTarget(msg.from);
|
|
2989
|
-
counts[name] = (counts[name] || 0) + 1;
|
|
2990
|
-
}
|
|
2991
|
-
return Object.entries(counts).sort((a, b) => b[1] - a[1]).slice(0, 10);
|
|
2992
|
-
}
|
|
2993
|
-
|
|
2994
|
-
function destroyChart(vm, name) {
|
|
2995
|
-
if (vm.chartInstances[name]) {
|
|
2996
|
-
vm.chartInstances[name].destroy();
|
|
2997
|
-
vm.chartInstances[name] = null;
|
|
2998
|
-
}
|
|
2999
|
-
}
|
|
3000
|
-
|
|
3001
|
-
function destroyAllCharts() {
|
|
3002
|
-
destroyChart(this, "volume");
|
|
3003
|
-
destroyChart(this, "status");
|
|
3004
|
-
destroyChart(this, "agents");
|
|
3005
|
-
}
|
|
3006
|
-
|
|
3007
|
-
function createRelayDashboard() {
|
|
3008
|
-
const dashboard = {
|
|
3009
|
-
...initialState(),
|
|
3010
|
-
...createLifecycleMethods(),
|
|
3011
|
-
...createSseMethods(),
|
|
3012
|
-
...createApiMethods(),
|
|
3013
|
-
...createDisplayMethods(),
|
|
3014
|
-
...createMessageActions(),
|
|
3015
|
-
...createPairActions(),
|
|
3016
|
-
...createAgentActions(),
|
|
3017
|
-
...createChartMethods(),
|
|
3018
|
-
};
|
|
3019
|
-
Object.defineProperties(dashboard, createComputedDescriptors());
|
|
3020
|
-
return dashboard;
|
|
3021
|
-
}
|
|
3022
|
-
|
|
3023
|
-
window.AgentRelayDashboard = {
|
|
3024
|
-
createRelayDashboard,
|
|
3025
|
-
helpers: { loadPref, savePref, indexAgents, upsertById, upsertTask, compareAgents, agentType, isBuiltInAgent },
|
|
3026
|
-
};
|
|
3027
|
-
window.relay = createRelayDashboard;
|
|
3028
|
-
|
|
3029
|
-
document.addEventListener("alpine:init", () => {
|
|
3030
|
-
Alpine.data("relay", createRelayDashboard);
|
|
3031
|
-
});
|
|
3032
|
-
})();
|