agent-relay-server 0.9.0 → 0.10.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.
Files changed (44) hide show
  1. package/README.md +12 -14
  2. package/package.json +18 -1
  3. package/public/index.html +979 -2575
  4. package/public/manifest.webmanifest +6 -6
  5. package/public/sw.js +16 -10
  6. package/recipes/code-review.yaml +26 -0
  7. package/recipes/debug.yaml +20 -0
  8. package/recipes/feature.yaml +26 -0
  9. package/recipes/refactor.yaml +20 -0
  10. package/recipes/test.yaml +20 -0
  11. package/runner/src/adapter.ts +69 -0
  12. package/runner/src/config.ts +144 -0
  13. package/scripts/orchestrator-spawn-smoke.ts +2 -9
  14. package/src/agent-spawn.ts +2 -94
  15. package/src/automations.ts +774 -0
  16. package/src/bus-outbox.ts +75 -0
  17. package/src/bus.ts +439 -0
  18. package/src/cli.ts +251 -5
  19. package/src/commands-db.ts +160 -0
  20. package/src/config.ts +1 -1
  21. package/src/connectors.ts +29 -9
  22. package/src/daemon.ts +1 -0
  23. package/src/db.ts +241 -34
  24. package/src/events.ts +33 -0
  25. package/src/index.ts +94 -5
  26. package/src/recipe-db.ts +163 -0
  27. package/src/recipe-loader.ts +100 -0
  28. package/src/recipe-runner.ts +206 -0
  29. package/src/recipe-validator.ts +85 -0
  30. package/src/routes.ts +649 -155
  31. package/src/security.ts +128 -2
  32. package/src/sse.ts +42 -31
  33. package/src/token-db.ts +96 -0
  34. package/src/types.ts +1 -493
  35. package/src/upgrade.ts +14 -28
  36. package/public/dashboard/actions.js +0 -819
  37. package/public/dashboard/api.js +0 -336
  38. package/public/dashboard/app.js +0 -34
  39. package/public/dashboard/charts.js +0 -128
  40. package/public/dashboard/computed.js +0 -693
  41. package/public/dashboard/constants.js +0 -28
  42. package/public/dashboard/display.js +0 -345
  43. package/public/dashboard/state.js +0 -129
  44. package/public/dashboard/utils.js +0 -207
@@ -1,819 +0,0 @@
1
- import {
2
- HUMAN_AGENT_ID, INBOX_OPERATOR_ID, DEFAULT_COMPOSE, DEFAULT_INBOX_COMPOSE,
3
- DEFAULT_PAIR_MESSAGE, DEFAULT_PAIR_INVITE, DEFAULT_AGENT_SPAWN,
4
- } from "./constants.js";
5
- import {
6
- inboxPeer, activityItem, messageBody, safeFilename,
7
- maxMessageId, isHumanInboundMessage, readCursorForPeer, draftForPeer,
8
- } from "./utils.js";
9
- import { savePref } from "./state.js";
10
- import { pruneSyncedOperatorActivity } from "./api.js";
11
- import { inboxComposeTarget } from "./display.js";
12
-
13
- function focusComposeBody(vm) {
14
- vm.$nextTick(() => vm.$refs.composeBody?.focus());
15
- }
16
-
17
- function openCompose() {
18
- if (!this.replyTo) this.compose = { ...DEFAULT_COMPOSE, from: "user" };
19
- this.composeOpen = true;
20
- focusComposeBody(this);
21
- }
22
-
23
- function openComposeToAgent(agent) {
24
- this.replyTo = null;
25
- this.compose = { ...DEFAULT_COMPOSE, from: "user", to: agent.id };
26
- this.composeOpen = true;
27
- focusComposeBody(this);
28
- }
29
-
30
- function openComposeToInboxThread(thread) {
31
- if (!thread) return;
32
- this.replyTo = thread.lastMessage ? { id: thread.lastMessage.id, from: thread.lastMessage.from } : null;
33
- this.compose = { ...DEFAULT_COMPOSE, from: HUMAN_AGENT_ID, to: thread.peer, channel: thread.lastMessage?.channel || "" };
34
- this.composeOpen = true;
35
- focusComposeBody(this);
36
- }
37
-
38
- function openInboxThread(thread) {
39
- this.selectedInboxThread = thread?.id || "";
40
- this.markInboxThreadRead(thread);
41
- }
42
-
43
- function markInboxThreadRead(thread) {
44
- if (!thread?.peer || !thread.messages?.length) return;
45
- const lastInboundId = maxMessageId(thread.messages, isHumanInboundMessage);
46
- if (lastInboundId <= readCursorForPeer(this, thread.peer)) return;
47
- this.inboxReadCursors = { ...this.inboxReadCursors, [thread.peer]: lastInboundId };
48
- savePref("inboxReadCursors", this.inboxReadCursors);
49
- void saveInboxThreadState(this, { peerId: thread.peer, readCursorMessageId: lastInboundId });
50
- }
51
-
52
- function markInboxThreadUnread(thread) {
53
- if (!thread?.peer) return;
54
- const next = { ...this.inboxReadCursors };
55
- delete next[thread.peer];
56
- this.inboxReadCursors = next;
57
- savePref("inboxReadCursors", this.inboxReadCursors);
58
- this.recordOperatorActivity({
59
- title: "Marked thread unread",
60
- body: this.conversationTitle(thread),
61
- meta: "Inbox",
62
- icon: "ti-mail",
63
- view: "inbox",
64
- peer: thread.peer,
65
- });
66
- void saveInboxThreadState(this, { peerId: thread.peer, readCursorMessageId: null });
67
- }
68
-
69
- function archiveInboxThread(thread) {
70
- if (!thread?.peer || !thread.lastMessage) return;
71
- this.inboxArchivedThreads = { ...this.inboxArchivedThreads, [thread.peer]: thread.lastMessage.id };
72
- savePref("inboxArchivedThreads", this.inboxArchivedThreads);
73
- this.recordOperatorActivity({
74
- title: "Archived thread",
75
- body: this.conversationTitle(thread),
76
- meta: "Inbox",
77
- icon: "ti-archive",
78
- view: "inbox",
79
- peer: thread.peer,
80
- });
81
- void saveInboxThreadState(this, { peerId: thread.peer, archivedAtMessageId: thread.lastMessage.id });
82
- if (this.selectedInboxThread === thread.id) this.selectedInboxThread = "";
83
- }
84
-
85
- function unarchiveInboxThread(thread) {
86
- if (!thread?.peer) return;
87
- const next = { ...this.inboxArchivedThreads };
88
- delete next[thread.peer];
89
- this.inboxArchivedThreads = next;
90
- savePref("inboxArchivedThreads", this.inboxArchivedThreads);
91
- this.recordOperatorActivity({
92
- title: "Unarchived thread",
93
- body: this.conversationTitle(thread),
94
- meta: "Inbox",
95
- icon: "ti-archive-off",
96
- view: "inbox",
97
- peer: thread.peer,
98
- });
99
- void saveInboxThreadState(this, { peerId: thread.peer, archivedAtMessageId: null });
100
- }
101
-
102
- async function saveInboxThreadState(vm, patch) {
103
- try {
104
- await vm.api("PATCH", "/inbox/threads", { operatorId: INBOX_OPERATOR_ID, ...patch });
105
- } catch {}
106
- }
107
-
108
- function confirmDeleteInboxThread(thread) {
109
- if (!thread) return;
110
- this.openConfirm(
111
- "Delete Thread",
112
- `Delete ${thread.messages.length} message(s) in ${this.conversationTitle(thread)}? This cannot be undone.`,
113
- () => this.doDeleteInboxThread(thread)
114
- );
115
- }
116
-
117
- async function doDeleteInboxThread(thread) {
118
- if (!thread?.messages?.length) return;
119
- try {
120
- await Promise.all(thread.messages.map((msg) => this.api("DELETE", "/messages/" + msg.id)));
121
- this.messages = this.messages.filter((msg) => !thread.messages.some((item) => item.id === msg.id));
122
- this.selectedInboxThread = "";
123
- this.clearReplyDraft(thread);
124
- this.recordOperatorActivity({
125
- title: "Deleted thread",
126
- body: this.conversationTitle(thread),
127
- meta: thread.messages.length + " message(s)",
128
- icon: "ti-trash",
129
- view: "inbox",
130
- });
131
- } catch (e) {
132
- alert("Delete thread failed: " + e.message);
133
- }
134
- }
135
-
136
- function replyDraftForThread(thread) {
137
- return thread?.peer ? draftForPeer(this, thread.peer) : "";
138
- }
139
-
140
- function setReplyDraft(thread, value) {
141
- if (!thread?.peer) return;
142
- const next = { ...this.inboxDrafts, [thread.peer]: value };
143
- if (!value) delete next[thread.peer];
144
- this.inboxDrafts = next;
145
- savePref("inboxDrafts", this.inboxDrafts);
146
- if (value) {
147
- void saveInboxDraft(this, thread.peer, value);
148
- } else {
149
- void deleteInboxDraftState(this, thread.peer);
150
- }
151
- }
152
-
153
- function clearReplyDraft(thread) {
154
- if (!thread?.peer) return;
155
- this.setReplyDraft(thread, "");
156
- }
157
-
158
- async function saveInboxDraft(vm, peerId, body) {
159
- try {
160
- await vm.api("PUT", "/inbox/drafts", { operatorId: INBOX_OPERATOR_ID, peerId, body });
161
- } catch {}
162
- }
163
-
164
- async function deleteInboxDraftState(vm, peerId) {
165
- try {
166
- await vm.api("DELETE", "/inbox/drafts?operatorId=" + encodeURIComponent(INBOX_OPERATOR_ID) + "&peerId=" + encodeURIComponent(peerId));
167
- } catch {}
168
- }
169
-
170
- async function sendInboxReply(thread) {
171
- if (!thread) return;
172
- const body = this.replyDraftForThread(thread).trim();
173
- if (!body) { alert("Reply body is required."); return; }
174
- try {
175
- const payload = { from: HUMAN_AGENT_ID, to: thread.peer, body };
176
- if (thread.lastMessage?.channel) payload.channel = thread.lastMessage.channel;
177
- if (thread.lastMessage?.id) payload.replyTo = thread.lastMessage.id;
178
- await this.api("POST", "/messages", payload);
179
- this.recordOperatorActivity({
180
- title: "Reply sent", body, meta: "to " + this.displayTarget(thread.peer),
181
- icon: "ti-corner-up-left", view: "inbox", peer: thread.peer,
182
- });
183
- this.clearReplyDraft(thread);
184
- this.markInboxThreadRead(thread);
185
- await this.fetchMessages();
186
- } catch (e) { alert("Reply failed: " + e.message); }
187
- }
188
-
189
- function resetInboxComposeTarget() {
190
- this.inboxCompose = { ...this.inboxCompose, to: "", claimable: false };
191
- }
192
-
193
- async function doSendInboxCompose() {
194
- const target = inboxComposeTarget(this);
195
- if (!target || !this.inboxCompose.body) { alert("Target and Message are required."); return; }
196
- try {
197
- const payload = { from: HUMAN_AGENT_ID, to: target, body: this.inboxCompose.body };
198
- if (this.inboxCompose.channel) payload.channel = this.inboxCompose.channel;
199
- if (this.inboxCompose.subject) payload.subject = this.inboxCompose.subject;
200
- if (this.inboxCompose.claimable && this.inboxComposeTargetAllowsClaimable()) {
201
- payload.claimable = true;
202
- payload.kind = "task";
203
- payload.payload = { title: this.inboxCompose.subject || "Claimable task" };
204
- }
205
- await this.api("POST", "/messages", payload);
206
- this.recordOperatorActivity({
207
- title: payload.claimable ? "Claimable task sent" : "Message sent",
208
- body: this.inboxCompose.subject || this.inboxCompose.body,
209
- meta: "to " + this.displayTarget(target),
210
- icon: payload.claimable ? "ti-hand-grab" : "ti-send",
211
- kind: payload.claimable ? "task" : "operator",
212
- view: inboxPeer(payload) ? "inbox" : "messages",
213
- peer: inboxPeer(payload),
214
- });
215
- this.inboxCompose = { ...DEFAULT_INBOX_COMPOSE, toMode: this.inboxCompose.toMode, to: this.inboxCompose.to };
216
- await this.fetchMessages();
217
- } catch (e) { alert("Send failed: " + e.message); }
218
- }
219
-
220
- function startReply(msg) {
221
- const replyTarget = msg.from === HUMAN_AGENT_ID ? msg.to : msg.from;
222
- this.replyTo = { id: msg.id, from: msg.from };
223
- this.compose = { ...DEFAULT_COMPOSE, from: HUMAN_AGENT_ID, to: replyTarget, channel: msg.channel || "" };
224
- this.openCompose();
225
- }
226
-
227
- function cancelReply() { this.replyTo = null; }
228
-
229
- async function doSend() {
230
- if (!this.compose.from || !this.compose.to || !this.compose.body) {
231
- alert("From, To, and Message are required."); return;
232
- }
233
- try {
234
- const payload = {
235
- from: this.compose.from, to: this.compose.to, body: this.compose.body,
236
- };
237
- if (this.compose.channel) payload.channel = this.compose.channel;
238
- if (this.compose.subject) payload.subject = this.compose.subject;
239
- if (this.replyTo) payload.replyTo = this.replyTo.id;
240
- if (this.compose.claimable && this.composeTargetAllowsClaimable()) {
241
- payload.claimable = true;
242
- payload.kind = "task";
243
- payload.payload = { title: this.compose.subject || "Claimable task" };
244
- }
245
- await this.api("POST", "/messages", payload);
246
- this.recordOperatorActivity({
247
- title: payload.claimable ? "Claimable task sent" : "Message sent",
248
- body: payload.subject || payload.body,
249
- meta: `${this.displayTarget(payload.from)} -> ${this.displayTarget(payload.to)}`,
250
- icon: payload.claimable ? "ti-hand-grab" : "ti-send",
251
- kind: payload.claimable ? "task" : "operator",
252
- view: inboxPeer(payload) ? "inbox" : "messages",
253
- peer: inboxPeer(payload),
254
- });
255
- this.composeOpen = false;
256
- this.replyTo = null;
257
- this.compose = { ...DEFAULT_COMPOSE, from: HUMAN_AGENT_ID };
258
- await this.fetchMessages();
259
- } catch (e) { alert("Send failed: " + e.message); }
260
- }
261
-
262
- async function doClaim(msgId) {
263
- if (!this.compose.from && !this.selectedAgent) {
264
- alert('Select a "From" agent first (open Compose to pick one).'); return;
265
- }
266
- const agentId = this.compose.from || this.selectedAgent;
267
- try {
268
- const result = await this.api("POST", "/messages/" + msgId + "/claim", { agentId });
269
- if (!result.ok) alert("Claim failed: " + (result.error || "unknown"));
270
- else this.recordOperatorActivity({
271
- title: "Claim requested", body: "Message #" + msgId,
272
- meta: "as " + this.displayTarget(agentId), icon: "ti-hand-grab", kind: "task", view: "tasks",
273
- });
274
- if (result.ok) await Promise.all([this.fetchMessages(), this.fetchTasks()]);
275
- } catch (e) { alert("Claim failed: " + e.message); }
276
- }
277
-
278
- async function doClaimTask(taskId) {
279
- if (!this.compose.from && !this.selectedAgent) {
280
- alert('Select an agent first (use the Messages agent filter or Compose From).'); return;
281
- }
282
- const agentId = this.compose.from || this.selectedAgent;
283
- try {
284
- const result = await this.api("POST", "/tasks/" + taskId + "/claim", { agentId });
285
- if (!result.ok) alert("Task claim failed: " + (result.error || "unknown"));
286
- else {
287
- this.recordOperatorActivity({
288
- title: "Task claimed", body: "Task #" + taskId,
289
- meta: "as " + this.displayTarget(agentId), icon: "ti-user-check", kind: "task",
290
- view: "work", taskId, agentId,
291
- });
292
- await Promise.all([this.fetchTasks(), this.fetchMessages()]);
293
- }
294
- } catch (e) { alert("Task claim failed: " + e.message); }
295
- }
296
-
297
- async function doUpdateTaskStatus(task, status) {
298
- if (!task || !status || status === task.status) return;
299
- try {
300
- const body = { status };
301
- const agentId = task.claimedBy || this.compose.from || this.selectedAgent;
302
- if (agentId) body.agentId = agentId;
303
- const result = await this.api("PATCH", "/tasks/" + task.id + "/status", body);
304
- const updated = result.task || result;
305
- const idx = this.tasks.findIndex((item) => item.id === task.id);
306
- if (idx >= 0) this.tasks[idx] = updated;
307
- this.recordOperatorActivity({
308
- title: "Task moved to " + status, body: updated.title || "Task #" + task.id,
309
- meta: agentId ? "by " + this.displayTarget(agentId) : "Work Queue",
310
- icon: "ti-arrows-exchange", kind: "task", view: "work", taskId: task.id, agentId,
311
- });
312
- } catch (e) { alert("Task status update failed: " + e.message); await this.fetchTasks(); }
313
- }
314
-
315
- async function doDeleteMessage(id) {
316
- try {
317
- await this.api("DELETE", "/messages/" + id);
318
- this.messages = this.messages.filter((msg) => msg.id !== id);
319
- this.recordOperatorActivity({
320
- title: "Deleted message", body: "Message #" + id, meta: "Messages", icon: "ti-trash", view: "messages",
321
- });
322
- } catch (e) { alert("Delete failed: " + e.message); }
323
- }
324
-
325
- async function openThread(threadId) {
326
- this.threadMessages = [];
327
- this.threadOpen = true;
328
- try {
329
- this.threadMessages = await this.api("GET", "/messages/" + threadId + "/thread");
330
- } catch (e) { alert("Failed to load thread: " + e.message); }
331
- }
332
-
333
- async function openTaskEvents(task) {
334
- this.taskEvents = [];
335
- this.taskEventsOpen = true;
336
- try {
337
- this.taskEvents = await this.api("GET", "/tasks/" + task.id + "/events");
338
- this.taskEventCache = { ...this.taskEventCache, [task.id]: this.taskEvents };
339
- } catch (e) { alert("Failed to load task events: " + e.message); }
340
- }
341
-
342
- function recordOperatorActivity(input) {
343
- const item = activityItem({ kind: "operator", ts: Date.now(), ...input });
344
- item.id = item.id || "operator-" + item.ts + "-" + (this.operatorActivity?.length || 0);
345
- item.clientId = item.clientId || item.id;
346
- this.operatorActivity = [
347
- item,
348
- ...(this.operatorActivity || []).filter((existing) => existing.id !== item.id),
349
- ].slice(0, 80);
350
- savePref("operatorActivity", this.operatorActivity);
351
- void saveActivityEvent(this, item);
352
- }
353
-
354
- async function saveActivityEvent(vm, item) {
355
- try {
356
- const event = await vm.api("POST", "/activity", {
357
- operatorId: INBOX_OPERATOR_ID,
358
- clientId: item.clientId, kind: item.kind, title: item.title,
359
- body: item.body || undefined, meta: item.meta || undefined,
360
- icon: item.icon || undefined, view: item.view || undefined,
361
- peer: item.peer || undefined, messageId: item.messageId,
362
- pairId: item.pairId, taskId: item.taskId, agentId: item.agentId,
363
- });
364
- const existing = new Set((vm.activityEvents || []).map((entry) => entry.id));
365
- vm.activityEvents = existing.has(event.id)
366
- ? vm.activityEvents.map((entry) => entry.id === event.id ? event : entry)
367
- : [event, ...(vm.activityEvents || [])].slice(0, 200);
368
- pruneSyncedOperatorActivity(vm);
369
- } catch {}
370
- }
371
-
372
- async function openActivityItem(item) {
373
- if (!item) return;
374
- if (item.view) await this.switchView(item.view);
375
- if (item.peer) {
376
- const thread = this.allInboxThreads.find((candidate) => candidate.peer === item.peer);
377
- if (thread?.archived) this.inboxShowArchived = true;
378
- this.selectedInboxThread = item.peer;
379
- if (thread) this.markInboxThreadRead(thread);
380
- }
381
- if (item.agentId && this.agentsById[item.agentId]) this.openAgentDetail(this.agentsById[item.agentId]);
382
- if (item.taskId) {
383
- const task = this.tasks.find((candidate) => candidate.id === item.taskId);
384
- if (task) await this.openTaskEvents(task);
385
- }
386
- }
387
-
388
- async function runHealthAction(action) {
389
- if (!action) return;
390
- if (action.preset) { this.agentPresetFilter = action.preset; this.showOffline = true; }
391
- if (action.api && action.path) { await this.api(action.api, action.path); await this.refreshLiveData(); }
392
- if (action.view) await this.switchView(action.view);
393
- if (action.copy) await copyText(action.copy);
394
- }
395
-
396
- async function runConnectorAction(connector, action) {
397
- if (!connector || !action) return;
398
- try {
399
- await this.api("POST", "/connectors/" + encodeURIComponent(connector.id) + "/actions", { action });
400
- await this.fetchConnectors();
401
- } catch (e) { alert("Connector action failed: " + e.message); }
402
- }
403
-
404
- async function copyText(value) {
405
- if (typeof navigator === "undefined") return;
406
- try { await navigator.clipboard?.writeText(value); } catch {}
407
- }
408
-
409
- function openCommandPalette() {
410
- this.commandQuery = "";
411
- this.commandPaletteOpen = true;
412
- this.$nextTick(() => this.$refs?.commandSearch?.focus());
413
- }
414
-
415
- function closeCommandPalette() {
416
- this.commandPaletteOpen = false;
417
- this.commandQuery = "";
418
- }
419
-
420
- async function runCommand(command) {
421
- if (!command) return;
422
- const payload = command.payload || {};
423
- this.closeCommandPalette();
424
- if (command.action === "openView") await this.switchView(payload.view);
425
- else if (command.action === "agentPreset") { this.showOffline = true; this.agentPresetFilter = payload.preset || ""; await this.switchView("agents"); }
426
- else if (command.action === "copy") await copyText(payload.value || "");
427
- else if (command.action === "messageAgent") { const agent = this.agentsById[payload.agentId]; if (agent) this.openComposeToAgent(agent); }
428
- else if (command.action === "pairAgent") this.openPairInvite(payload.agentId);
429
- else if (command.action === "filterTag") { this.agentTagFilter = payload.tag || ""; this.tagFilter = payload.tag || ""; await this.switchView("agents"); }
430
- else if (command.action === "exportActivity") this.exportActivity(payload.format || "markdown");
431
- }
432
-
433
- function exportActivity(format) {
434
- exportDocument(this, "timeline", format, { title: "Agent Relay Timeline", items: this.activityItems });
435
- }
436
-
437
- function exportThread(thread, format) {
438
- if (!thread) return;
439
- exportDocument(this, "thread-" + safeFilename(thread.peer), format, {
440
- title: "Thread: " + this.conversationTitle(thread), thread, messages: thread.messages || [],
441
- });
442
- }
443
-
444
- function exportPair(pair, format) {
445
- if (!pair) return;
446
- exportDocument(this, "pair-" + safeFilename(pair.id), format, {
447
- title: "Pair: " + pair.id, pair, messages: pairMessages(this, pair),
448
- });
449
- }
450
-
451
- async function exportTask(task, format) {
452
- if (!task) return;
453
- let events = this.taskEventCache[task.id] || [];
454
- if (!events.length) {
455
- try { events = await this.api("GET", "/tasks/" + task.id + "/events"); this.taskEventCache = { ...this.taskEventCache, [task.id]: events }; } catch { events = []; }
456
- }
457
- exportDocument(this, "task-" + task.id, format, { title: "Task: " + (task.title || "#" + task.id), task, events });
458
- }
459
-
460
- function pairMessages(vm, pair) {
461
- return (vm.messages || []).filter((msg) =>
462
- msg.payload?.pairId === pair.id ||
463
- (msg.payload?.pairEvent && [pair.requesterId, pair.targetId].includes(msg.from) && [pair.requesterId, pair.targetId].includes(msg.to))
464
- );
465
- }
466
-
467
- function exportDocument(vm, scope, format, data) {
468
- const normalizedFormat = format === "json" ? "json" : "markdown";
469
- const filename = `agent-relay-${scope}-${new Date().toISOString().slice(0, 10)}.${normalizedFormat === "json" ? "json" : "md"}`;
470
- const text = normalizedFormat === "json" ? JSON.stringify({ exportedAt: new Date().toISOString(), ...data }, null, 2) : exportMarkdown(vm, data);
471
- downloadText(filename, text, normalizedFormat === "json" ? "application/json" : "text/markdown");
472
- }
473
-
474
- function exportMarkdown(vm, data) {
475
- const lines = ["# " + data.title, "", "Exported: " + new Date().toISOString(), ""];
476
- if (data.thread) {
477
- lines.push("## Messages", "");
478
- appendMessages(lines, vm, data.messages || []);
479
- } else if (data.pair) {
480
- lines.push("## Pair", "", "- ID: " + data.pair.id, "- Status: " + data.pair.status, "- Requester: " + vm.displayTarget(data.pair.requesterId), "- Target: " + vm.displayTarget(data.pair.targetId));
481
- if (data.pair.objective) lines.push("- Objective: " + data.pair.objective);
482
- lines.push("", "## Messages", "");
483
- appendMessages(lines, vm, data.messages || []);
484
- } else if (data.task) {
485
- lines.push("## Task", "", "- ID: " + data.task.id, "- Status: " + data.task.status, "- Severity: " + (data.task.severity || "info"), "- Target: " + vm.displayTarget(data.task.target || ""));
486
- if (data.task.claimedBy) lines.push("- Claimed by: " + vm.displayTarget(data.task.claimedBy));
487
- if (data.task.title) lines.push("- Title: " + data.task.title);
488
- if (data.task.body) lines.push("", data.task.body);
489
- lines.push("", "## History", "");
490
- for (const event of data.events || []) {
491
- lines.push(`- ${event.createdAt || ""} [${event.severity || "info"}] ${event.type || "event"}: ${event.title || event.body || ""}`.trim());
492
- }
493
- if (!data.events?.length) lines.push("_No task events loaded._", "");
494
- } else {
495
- lines.push("## Events", "");
496
- for (const item of data.items || []) {
497
- const when = item.ts ? new Date(item.ts).toISOString() : "";
498
- const meta = item.meta ? " - " + item.meta : "";
499
- lines.push(`- ${when} [${item.kind}] ${item.title}${meta}`);
500
- if (item.body) lines.push(" " + item.body.replace(/\n/g, "\n "));
501
- }
502
- if (!data.items?.length) lines.push("_No activity loaded._", "");
503
- }
504
- return lines.join("\n").trim() + "\n";
505
- }
506
-
507
- function appendMessages(lines, vm, messages) {
508
- if (!messages.length) { lines.push("_No messages loaded._", ""); return; }
509
- for (const msg of messages) {
510
- lines.push(`### #${msg.id} ${vm.displayTarget(msg.from)} -> ${vm.displayTarget(msg.to)}`, "");
511
- if (msg.createdAt) lines.push("- Created: " + msg.createdAt);
512
- if (msg.channel) lines.push("- Channel: " + msg.channel);
513
- if (msg.subject) lines.push("- Subject: " + msg.subject);
514
- if (msg.claimedBy) lines.push("- Claimed by: " + vm.displayTarget(msg.claimedBy));
515
- lines.push("", messageBody(msg) || "", "");
516
- }
517
- }
518
-
519
- function downloadText(filename, text, type) {
520
- if (typeof document === "undefined" || typeof URL === "undefined" || typeof Blob === "undefined") { void copyText(text); return; }
521
- const url = URL.createObjectURL(new Blob([text], { type }));
522
- const link = document.createElement("a");
523
- link.href = url;
524
- link.download = filename;
525
- link.click();
526
- URL.revokeObjectURL(url);
527
- }
528
-
529
- function openPairMessage(pair, fromId) {
530
- if (!pair) return;
531
- this.pairMessage = { ...DEFAULT_PAIR_MESSAGE, pairId: pair.id, from: fromId || pair.requesterId || pair.targetId || "" };
532
- this.pairMessageOpen = true;
533
- this.$nextTick(() => this.$refs?.pairMessageBody?.focus());
534
- }
535
-
536
- function openPairInvite(requesterId) {
537
- this.pairInvite = { ...DEFAULT_PAIR_INVITE, requesterId: requesterId || this.selectedAgent || "" };
538
- this.pairInviteOpen = true;
539
- this.$nextTick(() => this.$refs?.pairInviteObjective?.focus());
540
- }
541
-
542
- function closePairInvite() {
543
- this.pairInviteOpen = false;
544
- this.pairInvite = { ...DEFAULT_PAIR_INVITE };
545
- }
546
-
547
- async function doCreatePair() {
548
- if (!this.pairInvite.requesterId || !this.pairInvite.targetId) { alert("Requester and Target are required."); return; }
549
- if (this.pairInvite.requesterId === this.pairInvite.targetId) { alert("Requester and Target must be different agents."); return; }
550
- try {
551
- const payload = { from: this.pairInvite.requesterId, target: this.pairInvite.targetId };
552
- if (this.pairInvite.objective) payload.objective = this.pairInvite.objective;
553
- await this.api("POST", "/pairs", payload);
554
- this.recordOperatorActivity({
555
- title: "Pair invite sent", body: payload.objective || "",
556
- meta: `${this.displayTarget(payload.from)} <-> ${this.displayTarget(payload.target)}`,
557
- icon: "ti-link-plus", kind: "pair", view: "pairs",
558
- });
559
- this.closePairInvite();
560
- await Promise.all([this.fetchPairs(), this.fetchMessages()]);
561
- } catch (e) { alert("Pair invite failed: " + e.message); }
562
- }
563
-
564
- function openAgentSpawn() {
565
- this.agentSpawn = { ...DEFAULT_AGENT_SPAWN };
566
- this.agentDirectoryBrowser = { open: false, loading: false, path: "", parent: "", home: "", cwd: "", entries: [], error: "" };
567
- this.agentSpawnOpen = true;
568
- this.$nextTick(() => this.$refs?.agentSpawnCwd?.focus());
569
- }
570
-
571
- function closeAgentSpawn() {
572
- this.agentSpawnOpen = false;
573
- this.agentSpawn = { ...DEFAULT_AGENT_SPAWN };
574
- }
575
-
576
- async function openAgentDirectoryBrowser() {
577
- this.agentDirectoryBrowser = { ...this.agentDirectoryBrowser, open: true };
578
- await this.browseAgentDirectory(this.agentSpawn.cwd || "");
579
- }
580
-
581
- async function browseAgentDirectory(path) {
582
- this.agentDirectoryBrowser = { ...this.agentDirectoryBrowser, loading: true, error: "" };
583
- try {
584
- const query = path ? "?path=" + encodeURIComponent(path) : "";
585
- const listing = await this.api("GET", "/agents/spawn/directories" + query);
586
- this.agentDirectoryBrowser = {
587
- open: true, loading: false, path: listing.path || "", parent: listing.parent || "",
588
- home: listing.home || "", cwd: listing.cwd || "", entries: listing.entries || [], error: "",
589
- };
590
- this.agentSpawn = { ...this.agentSpawn, cwd: listing.path || this.agentSpawn.cwd };
591
- } catch (e) { this.agentDirectoryBrowser = { ...this.agentDirectoryBrowser, loading: false, error: e.message }; }
592
- }
593
-
594
- function selectAgentDirectory(path) {
595
- this.agentSpawn = { ...this.agentSpawn, cwd: path || this.agentDirectoryBrowser.path };
596
- this.agentDirectoryBrowser = { ...this.agentDirectoryBrowser, open: false };
597
- }
598
-
599
- async function doSpawnAgent() {
600
- if (this.agentSpawn.provider !== "codex") { alert("Only Codex live sessions can be spawned from the dashboard right now."); return; }
601
- try {
602
- const payload = { provider: "codex", approvalMode: this.agentSpawn.approvalMode || "guarded" };
603
- if (this.agentSpawn.cwd) payload.cwd = this.agentSpawn.cwd;
604
- if (this.agentSpawn.label) payload.label = this.agentSpawn.label;
605
- const result = await this.api("POST", "/agents/spawn", payload);
606
- this.recordOperatorActivity({
607
- title: "Codex agent spawn requested", body: result?.cwd || payload.cwd || "",
608
- meta: result?.pid ? "pid " + result.pid : "starting", icon: "ti-plus", kind: "state", view: "agents",
609
- });
610
- this.closeAgentSpawn();
611
- await Promise.all([this.fetchAgents(), this.fetchActivityEvents()]);
612
- } catch (e) { alert("Spawn failed: " + e.message); }
613
- }
614
-
615
- function openOrchestratorSpawn() {
616
- const online = this.orchestrators.filter((o) => o.status === "online");
617
- if (online.length === 0) return alert("No orchestrators online");
618
- this.spawnOrchId = online[0].id;
619
- this.spawnProvider = "claude";
620
- this.spawnCwd = "";
621
- this.spawnLabel = "";
622
- this.spawnApproval = "guarded";
623
- this.spawnPrompt = "";
624
- this.spawnDirListing = null;
625
- this.orchestratorSpawnOpen = true;
626
- }
627
-
628
- function openOrchestratorSpawnFor(orchId) {
629
- this.spawnOrchId = orchId;
630
- this.spawnProvider = "claude";
631
- this.spawnCwd = "";
632
- this.spawnLabel = "";
633
- this.spawnApproval = "guarded";
634
- this.spawnPrompt = "";
635
- this.spawnDirListing = null;
636
- this.orchestratorSpawnOpen = true;
637
- }
638
-
639
- async function browseOrchestratorDirs() {
640
- try {
641
- const query = this.spawnCwd ? "?path=" + encodeURIComponent(this.spawnCwd) : "";
642
- this.spawnDirListing = await this.api("GET", "/agents/spawn/directories" + query);
643
- } catch (e) { alert("Directory browse failed: " + e.message); }
644
- }
645
-
646
- async function submitOrchestratorSpawn() {
647
- if (!this.spawnOrchId) return alert("Select an orchestrator");
648
- try {
649
- const payload = { provider: this.spawnProvider, approvalMode: this.spawnApproval };
650
- if (this.spawnCwd) payload.cwd = this.spawnCwd;
651
- if (this.spawnLabel) payload.label = this.spawnLabel;
652
- if (this.spawnPrompt) payload.prompt = this.spawnPrompt;
653
- await this.api("POST", "/orchestrators/" + encodeURIComponent(this.spawnOrchId) + "/spawn", payload);
654
- this.orchestratorSpawnOpen = false;
655
- this.recordOperatorActivity({
656
- title: `${this.spawnProvider} agent spawn requested`, body: this.spawnCwd || "",
657
- meta: this.spawnOrchId, icon: "ti-plus", kind: "state", view: "orchestrators",
658
- });
659
- await Promise.all([this.fetchOrchestrators(), this.fetchActivityEvents()]);
660
- } catch (e) { alert("Spawn failed: " + e.message); }
661
- }
662
-
663
- async function orchestratorAction(orchId, action, agentId) {
664
- const label = action === "restart" ? "Restart" : "Shutdown";
665
- if (!confirm(`${label} agent "${agentId || "all"}" on orchestrator "${orchId}"?`)) return;
666
- try {
667
- await this.api("POST", "/orchestrators/" + encodeURIComponent(orchId) + "/actions", { action, agentId });
668
- this.recordOperatorActivity({
669
- title: `Agent ${action} requested`, body: agentId || "all agents",
670
- meta: orchId, icon: action === "restart" ? "ti-refresh" : "ti-power", kind: "state", view: "orchestrators",
671
- });
672
- await Promise.all([this.fetchOrchestrators(), this.fetchActivityEvents()]);
673
- } catch (e) { alert(`${label} failed: ` + e.message); }
674
- }
675
-
676
- async function deleteOrchestrator(orchId) {
677
- if (!confirm(`Remove orchestrator "${orchId}"? This will NOT stop its managed agents.`)) return;
678
- try {
679
- await this.api("DELETE", "/orchestrators/" + encodeURIComponent(orchId));
680
- await this.fetchOrchestrators();
681
- } catch (e) { alert("Delete failed: " + e.message); }
682
- }
683
-
684
- function closePairMessage() {
685
- this.pairMessageOpen = false;
686
- this.pairMessage = { ...DEFAULT_PAIR_MESSAGE };
687
- }
688
-
689
- async function doSendPairMessage() {
690
- if (!this.pairMessage.pairId || !this.pairMessage.from || !this.pairMessage.body) {
691
- alert("Pair, From, and Message are required."); return;
692
- }
693
- try {
694
- const payload = { from: this.pairMessage.from, body: this.pairMessage.body };
695
- if (this.pairMessage.subject) payload.subject = this.pairMessage.subject;
696
- await this.api("POST", "/pairs/" + encodeURIComponent(this.pairMessage.pairId) + "/messages", payload);
697
- this.recordOperatorActivity({
698
- title: "Pair message sent", body: payload.subject || payload.body,
699
- meta: "from " + this.displayTarget(payload.from), icon: "ti-messages", kind: "pair", view: "pairs",
700
- });
701
- this.closePairMessage();
702
- await Promise.all([this.fetchPairs(), this.fetchMessages()]);
703
- } catch (e) { alert("Pair message failed: " + e.message); }
704
- }
705
-
706
- async function doAcceptPair(pair) {
707
- if (!pair) return;
708
- try {
709
- await this.api("POST", "/pairs/" + encodeURIComponent(pair.id) + "/accept", { agentId: pair.targetId });
710
- this.recordOperatorActivity({
711
- title: "Pair accepted", body: pair.objective || "",
712
- meta: `${this.displayTarget(pair.requesterId)} <-> ${this.displayTarget(pair.targetId)}`,
713
- icon: "ti-check", kind: "pair", view: "pairs",
714
- });
715
- await Promise.all([this.fetchPairs(), this.fetchMessages()]);
716
- } catch (e) { alert("Accept failed: " + e.message); }
717
- }
718
-
719
- async function doRejectPair(pair) {
720
- if (!pair) return;
721
- try {
722
- await this.api("POST", "/pairs/" + encodeURIComponent(pair.id) + "/reject", { agentId: pair.targetId });
723
- this.recordOperatorActivity({
724
- title: "Pair rejected", body: pair.objective || "",
725
- meta: `${this.displayTarget(pair.requesterId)} <-> ${this.displayTarget(pair.targetId)}`,
726
- icon: "ti-x", kind: "pair", view: "pairs",
727
- });
728
- await Promise.all([this.fetchPairs(), this.fetchMessages()]);
729
- } catch (e) { alert("Reject failed: " + e.message); }
730
- }
731
-
732
- async function doHangupPair(pair, agentId) {
733
- if (!pair) return;
734
- try {
735
- await this.api("POST", "/pairs/" + encodeURIComponent(pair.id) + "/hangup", { agentId: agentId || pair.requesterId });
736
- this.recordOperatorActivity({
737
- title: "Pair hung up", body: pair.objective || "",
738
- meta: "by " + this.displayTarget(agentId || pair.requesterId),
739
- icon: "ti-phone-off", kind: "pair", view: "pairs",
740
- });
741
- await Promise.all([this.fetchPairs(), this.fetchMessages()]);
742
- } catch (e) { alert("Hang up failed: " + e.message); }
743
- }
744
-
745
- export function createMessageActions() {
746
- return {
747
- openCompose, openComposeToAgent, openComposeToInboxThread, openInboxThread,
748
- markInboxThreadRead, markInboxThreadUnread, archiveInboxThread, unarchiveInboxThread,
749
- confirmDeleteInboxThread, doDeleteInboxThread, replyDraftForThread, setReplyDraft,
750
- clearReplyDraft, sendInboxReply, resetInboxComposeTarget, doSendInboxCompose,
751
- startReply, cancelReply, doSend, doClaim, doClaimTask, doUpdateTaskStatus,
752
- doDeleteMessage, openThread, openTaskEvents, recordOperatorActivity,
753
- openActivityItem, runHealthAction, runConnectorAction, openCommandPalette,
754
- closeCommandPalette, runCommand, exportActivity, exportThread, exportPair, exportTask,
755
- };
756
- }
757
-
758
- export function createPairActions() {
759
- return {
760
- openPairMessage, closePairMessage, openPairInvite, closePairInvite,
761
- doCreatePair, openAgentSpawn, closeAgentSpawn, doSpawnAgent,
762
- openAgentDirectoryBrowser, browseAgentDirectory, selectAgentDirectory,
763
- openOrchestratorSpawn, openOrchestratorSpawnFor, browseOrchestratorDirs,
764
- submitOrchestratorSpawn, orchestratorAction, deleteOrchestrator,
765
- doSendPairMessage, doAcceptPair, doRejectPair, doHangupPair,
766
- };
767
- }
768
-
769
- export function createAgentActions() {
770
- return {
771
- openAgentDetail(agent) {
772
- if (!agent) return;
773
- this.agentDetailId = agent.id;
774
- this.agentDetailOpen = true;
775
- },
776
- closeAgentDetail() { this.agentDetailOpen = false; },
777
- openChannelDetail(channel) {
778
- if (!channel) return;
779
- this.channelDetailId = channel.id;
780
- this.channelDetailOpen = true;
781
- },
782
- closeChannelDetail() { this.channelDetailOpen = false; },
783
- openRename(agent) {
784
- this.renameModal = { show: true, agentId: agent.id, label: agent.label || "" };
785
- this.$nextTick(() => this.$refs.renameInput?.focus());
786
- },
787
- async doRename() {
788
- const label = this.renameModal.label.trim() || null;
789
- try {
790
- await this.api("PATCH", "/agents/" + this.renameModal.agentId + "/label", { label });
791
- this.renameModal.show = false;
792
- } catch (e) { alert("Rename failed: " + e.message); }
793
- },
794
- openConfirm(title, message, action) {
795
- this.confirmModal = { show: true, title, message, action };
796
- },
797
- async doDeleteAgent(id) {
798
- try {
799
- await this.api("DELETE", "/agents/" + id);
800
- if (this.selectedAgent === id) this.selectedAgent = "";
801
- this.agents = this.agents.filter((agent) => agent.id !== id);
802
- delete this.agentsById[id];
803
- } catch (e) { alert("Delete failed: " + e.message); }
804
- },
805
- async doAgentAction(agent, action) {
806
- if (!agent || !action) return;
807
- try {
808
- const result = await this.api("POST", "/agents/" + encodeURIComponent(agent.id) + "/actions", { action });
809
- this.recordOperatorActivity({
810
- title: action === "restart" ? "Agent restart requested" : "Agent shutdown requested",
811
- body: this.displayName(agent), meta: agent.id,
812
- icon: action === "restart" ? "ti-refresh" : "ti-power", kind: "state",
813
- view: "agents", agentId: agent.id, messageId: result?.message?.id,
814
- });
815
- await Promise.all([this.fetchMessages(), this.fetchActivityEvents()]);
816
- } catch (e) { alert("Agent action failed: " + e.message); }
817
- },
818
- };
819
- }