agent-relay-server 0.4.38 → 0.5.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-server",
3
- "version": "0.4.38",
3
+ "version": "0.5.0",
4
4
  "description": "Lightweight HTTP message relay for inter-agent communication across machines",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
@@ -6,6 +6,7 @@
6
6
  const DEFAULT_INBOX_COMPOSE = { toMode: "agent", to: "", body: "", channel: "", subject: "", claimable: false };
7
7
  const DEFAULT_PAIR_MESSAGE = { pairId: "", from: "", body: "", subject: "" };
8
8
  const DEFAULT_PAIR_INVITE = { requesterId: "", targetId: "", objective: "" };
9
+ const DEFAULT_AGENT_SPAWN = { provider: "codex", approvalMode: "guarded", cwd: "", label: "" };
9
10
  const CLOSED_TASK_STATUSES = new Set(["done", "failed", "canceled"]);
10
11
  const WAITING_TASK_STATUSES = new Set(["open", "blocked"]);
11
12
  const STATUS_SORT_ORDER = { online: 0, idle: 1, busy: 2, offline: 3 };
@@ -15,6 +16,7 @@
15
16
  claude: "claude-sol",
16
17
  user: "ti-user",
17
18
  system: "ti-server",
19
+ channel: "ti-messages",
18
20
  agent: "ti-robot",
19
21
  };
20
22
  const AGENT_TYPE_TITLES = {
@@ -22,6 +24,7 @@
22
24
  claude: "Claude agent",
23
25
  user: "Human operator",
24
26
  system: "System",
27
+ channel: "Channel",
25
28
  agent: "Agent",
26
29
  };
27
30
 
@@ -51,6 +54,7 @@
51
54
 
52
55
  agents: [],
53
56
  agentsById: {},
57
+ orchestrators: [],
54
58
  pairs: [],
55
59
  messages: [],
56
60
  tasks: [],
@@ -76,9 +80,21 @@
76
80
  selectedAgent: "",
77
81
  agentDetailOpen: false,
78
82
  agentDetailId: "",
83
+ channelDetailOpen: false,
84
+ channelDetailId: "",
79
85
  selectedInboxThread: "",
80
86
  replyTo: null,
81
87
  composeOpen: false,
88
+ agentSpawnOpen: false,
89
+ orchestratorSpawnOpen: false,
90
+ spawnOrchId: "",
91
+ spawnProvider: "claude",
92
+ spawnCwd: "",
93
+ spawnLabel: "",
94
+ spawnApproval: "guarded",
95
+ spawnPrompt: "",
96
+ spawnDirListing: null,
97
+ agentDirectoryBrowser: { open: false, loading: false, path: "", parent: "", home: "", cwd: "", entries: [], error: "" },
82
98
  pairInviteOpen: false,
83
99
  pairMessageOpen: false,
84
100
  threadOpen: false,
@@ -90,6 +106,7 @@
90
106
  authNeeded: false,
91
107
 
92
108
  compose: { ...DEFAULT_COMPOSE },
109
+ agentSpawn: { ...DEFAULT_AGENT_SPAWN },
93
110
  pairInvite: { ...DEFAULT_PAIR_INVITE },
94
111
  pairMessage: { ...DEFAULT_PAIR_MESSAGE },
95
112
  inboxCompose: { ...DEFAULT_INBOX_COMPOSE },
@@ -273,6 +290,8 @@
273
290
  es.addEventListener("message.claimed", (event) => handleMessageClaimed(this, parseEventData(event)));
274
291
  es.addEventListener("message.claim_released", (event) => handleMessageClaimReleased(this, parseEventData(event)));
275
292
  es.addEventListener("message.deleted", (event) => handleMessageDeleted(this, parseEventData(event)));
293
+ es.addEventListener("orchestrator.status", (event) => handleOrchestratorStatus(this, parseEventData(event)));
294
+ es.addEventListener("orchestrator.removed", (event) => handleOrchestratorRemoved(this, parseEventData(event)));
276
295
  registerTaskEvents(this, es);
277
296
  }
278
297
 
@@ -312,6 +331,14 @@
312
331
  refreshChartsIfVisible(vm);
313
332
  }
314
333
 
334
+ function handleOrchestratorStatus(vm, orch) {
335
+ upsertById(vm.orchestrators, orch);
336
+ }
337
+
338
+ function handleOrchestratorRemoved(vm, data) {
339
+ vm.orchestrators = vm.orchestrators.filter((o) => o.id !== data.id);
340
+ }
341
+
315
342
  function handleMessageClaimed(vm, data) {
316
343
  const msg = vm.messages.find((item) => item.id === data.messageId);
317
344
  if (!msg) return;
@@ -366,7 +393,7 @@
366
393
  },
367
394
 
368
395
  async refresh() {
369
- await Promise.all([this.fetchStats(), this.fetchHealth(), this.fetchAgents(), this.fetchPairs(), this.fetchMessages(), this.fetchTasks(), this.fetchChannels(), this.fetchIntegrations(), this.fetchInboxState(), this.fetchActivityEvents()]);
396
+ await Promise.all([this.fetchStats(), this.fetchHealth(), this.fetchAgents(), this.fetchOrchestrators(), this.fetchPairs(), this.fetchMessages(), this.fetchTasks(), this.fetchChannels(), this.fetchIntegrations(), this.fetchInboxState(), this.fetchActivityEvents()]);
370
397
  },
371
398
 
372
399
  async refreshLiveData() {
@@ -399,6 +426,12 @@
399
426
  } catch {}
400
427
  },
401
428
 
429
+ async fetchOrchestrators() {
430
+ try {
431
+ this.orchestrators = await this.api("GET", "/orchestrators");
432
+ } catch {}
433
+ },
434
+
402
435
  async fetchPairs() {
403
436
  try {
404
437
  let pairs;
@@ -496,11 +529,14 @@
496
529
  function createComputedDescriptors() {
497
530
  return {
498
531
  onlineCount: { get: getOnlineCount },
532
+ busyAgentCount: { get: getBusyAgentCount },
499
533
  hiddenBuiltInAgentCount: { get: getHiddenBuiltInAgentCount },
500
534
  sortedAgents: { get: getSortedAgents },
501
535
  pairsByAgentId: { get: getPairsByAgentId },
502
536
  selectedAgentDetail: { get: getSelectedAgentDetail },
503
537
  agentDetailMessages: { get: getAgentDetailMessages },
538
+ selectedChannelDetail: { get: getSelectedChannelDetail },
539
+ channelDetailMessages: { get: getChannelDetailMessages },
504
540
  pairMessagePair: { get: getPairMessagePair },
505
541
  allInboxThreads: { get: getAllInboxThreads },
506
542
  inboxThreads: { get: getInboxThreads },
@@ -513,6 +549,7 @@
513
549
  workQueueItems: { get: getWorkQueueItems },
514
550
  channelCards: { get: getChannelCards },
515
551
  integrationCards: { get: getIntegrationCards },
552
+ openPairCount: { get: getOpenPairCount },
516
553
  filteredMessages: { get: getFilteredMessages },
517
554
  groupedMessages: { get: getGroupedMessages },
518
555
  filteredTasks: { get: getFilteredTasks },
@@ -523,13 +560,23 @@
523
560
  healthIssues: { get: getHealthIssues },
524
561
  healthDiagnostics: { get: getHealthDiagnostics },
525
562
  commandPaletteItems: { get: getCommandPaletteItems },
563
+ spawnAvailableProviders: { get: getSpawnAvailableProviders },
526
564
  };
527
565
  }
528
566
 
567
+ function getSpawnAvailableProviders() {
568
+ const orch = this.orchestrators.find((o) => o.id === this.spawnOrchId);
569
+ return orch ? orch.providers : ["claude", "codex"];
570
+ }
571
+
529
572
  function getOnlineCount() {
530
573
  return visibleAgents(this).filter((agent) => agent.status !== "offline").length;
531
574
  }
532
575
 
576
+ function getBusyAgentCount() {
577
+ return this.agents.filter((agent) => agent.status === "busy").length;
578
+ }
579
+
533
580
  function getHiddenBuiltInAgentCount() {
534
581
  return this.showBuiltIns ? 0 : this.agents.filter(isBuiltInAgent).length;
535
582
  }
@@ -578,6 +625,29 @@
578
625
  .slice(0, 8);
579
626
  }
580
627
 
628
+ function getSelectedChannelDetail() {
629
+ if (!this.channelDetailId) return null;
630
+ return (this.channels || []).find((channel) => channel.id === this.channelDetailId) || null;
631
+ }
632
+
633
+ function getChannelDetailMessages() {
634
+ const channel = this.selectedChannelDetail;
635
+ if (!channel) return [];
636
+ return this.messages
637
+ .filter((msg) => messageMatchesChannel(msg, channel))
638
+ .slice()
639
+ .sort((a, b) => b.id - a.id)
640
+ .slice(0, 8);
641
+ }
642
+
643
+ function messageMatchesChannel(message, channel) {
644
+ if (!message || !channel) return false;
645
+ const channelKeys = [channel.id, channel.type, ...(channel.topicChannels || [])].filter(Boolean);
646
+ if (message.channel && channelKeys.includes(message.channel)) return true;
647
+ if (channel.agentId && (message.from === channel.agentId || message.to === channel.agentId)) return true;
648
+ return false;
649
+ }
650
+
581
651
  function getPairMessagePair() {
582
652
  return this.pairs.find((pair) => pair.id === this.pairMessage.pairId) || null;
583
653
  }
@@ -1071,11 +1141,47 @@
1071
1141
  });
1072
1142
  }
1073
1143
 
1144
+ function getOpenPairCount() {
1145
+ return (this.pairs || []).filter(isOpenPair).length;
1146
+ }
1147
+
1148
+ function isOpenPair(pair) {
1149
+ return pair?.status === "active" || pair?.status === "pending";
1150
+ }
1151
+
1074
1152
  function getComposeAgents() {
1075
1153
  const list = visibleAgents(this);
1076
1154
  return this.showOffline ? list : list.filter((agent) => agent.status !== "offline");
1077
1155
  }
1078
1156
 
1157
+ function composeTargetAllowsClaimable() {
1158
+ return matchingComposeRecipientCount(this, this.compose.to) > 1;
1159
+ }
1160
+
1161
+ function inboxComposeTargetAllowsClaimable() {
1162
+ return matchingComposeRecipientCount(this, inboxComposeTarget(this)) > 1;
1163
+ }
1164
+
1165
+ function matchingComposeRecipientCount(vm, target) {
1166
+ if (!target) return 0;
1167
+ const candidates = vm.composeAgents.filter((agent) => !isBuiltInAgent(agent) && !isChannelAgent(agent));
1168
+ if (target === "broadcast") return candidates.length;
1169
+ if (vm.agentsById[target]) return 1;
1170
+ if (target.startsWith("tag:")) {
1171
+ const tag = target.slice(4);
1172
+ return candidates.filter((agent) => (agent.tags || []).includes(tag)).length;
1173
+ }
1174
+ if (target.startsWith("cap:")) {
1175
+ const cap = target.slice(4);
1176
+ return candidates.filter((agent) => (agent.capabilities || []).includes(cap)).length;
1177
+ }
1178
+ if (target.startsWith("label:")) {
1179
+ const label = target.slice(6);
1180
+ return candidates.filter((agent) => agent.label === label).length;
1181
+ }
1182
+ return 1;
1183
+ }
1184
+
1079
1185
  function getUniqueLabels() {
1080
1186
  return [...new Set(visibleAgents(this).filter((agent) => agent.label).map((agent) => agent.label))];
1081
1187
  }
@@ -1089,13 +1195,22 @@
1089
1195
  }
1090
1196
 
1091
1197
  function visibleAgents(vm) {
1092
- return vm.showBuiltIns ? [...vm.agents] : vm.agents.filter((agent) => !isBuiltInAgent(agent));
1198
+ const nonChannelAgents = vm.agents.filter((agent) => !isChannelAgent(agent));
1199
+ return vm.showBuiltIns ? nonChannelAgents : nonChannelAgents.filter((agent) => !isBuiltInAgent(agent));
1093
1200
  }
1094
1201
 
1095
1202
  function isBuiltInAgent(agent) {
1096
1203
  return agent?.meta?.builtin === true || agent?.id === HUMAN_AGENT_ID || agent?.id === "system";
1097
1204
  }
1098
1205
 
1206
+ function isChannelAgent(agent) {
1207
+ return agent?.meta?.kind === "channel" || agent?.tags?.includes("channel");
1208
+ }
1209
+
1210
+ function agentSupportsControlActions(agent) {
1211
+ return Boolean(agent && !isBuiltInAgent(agent) && !isChannelAgent(agent));
1212
+ }
1213
+
1099
1214
  function applyAgentPreset(vm, list) {
1100
1215
  switch (vm.agentPresetFilter) {
1101
1216
  case "active":
@@ -1249,6 +1364,9 @@
1249
1364
  agentChannels,
1250
1365
  channelPresence,
1251
1366
  integrationPresence,
1367
+ composeTargetAllowsClaimable,
1368
+ inboxComposeTargetAllowsClaimable,
1369
+ agentSupportsControlActions,
1252
1370
  timeAgo,
1253
1371
  fmtTime,
1254
1372
  healthAlertClass,
@@ -1368,6 +1486,7 @@
1368
1486
  function agentType(agent) {
1369
1487
  if (agent?.id === HUMAN_AGENT_ID) return "user";
1370
1488
  if (agent?.id === "system") return "system";
1489
+ if (isChannelAgent(agent)) return "channel";
1371
1490
 
1372
1491
  const values = [
1373
1492
  ...(agent?.tags || []),
@@ -1573,6 +1692,18 @@
1573
1692
  openPairInvite,
1574
1693
  closePairInvite,
1575
1694
  doCreatePair,
1695
+ openAgentSpawn,
1696
+ closeAgentSpawn,
1697
+ doSpawnAgent,
1698
+ openAgentDirectoryBrowser,
1699
+ browseAgentDirectory,
1700
+ selectAgentDirectory,
1701
+ openOrchestratorSpawn,
1702
+ openOrchestratorSpawnFor,
1703
+ browseOrchestratorDirs,
1704
+ submitOrchestratorSpawn,
1705
+ orchestratorAction,
1706
+ deleteOrchestrator,
1576
1707
  doSendPairMessage,
1577
1708
  doAcceptPair,
1578
1709
  doRejectPair,
@@ -1783,7 +1914,7 @@
1783
1914
  }
1784
1915
 
1785
1916
  function resetInboxComposeTarget() {
1786
- this.inboxCompose = { ...this.inboxCompose, to: "" };
1917
+ this.inboxCompose = { ...this.inboxCompose, to: "", claimable: false };
1787
1918
  }
1788
1919
 
1789
1920
  function inboxComposeTarget(vm) {
@@ -1809,14 +1940,14 @@
1809
1940
  };
1810
1941
  if (this.inboxCompose.channel) payload.channel = this.inboxCompose.channel;
1811
1942
  if (this.inboxCompose.subject) payload.subject = this.inboxCompose.subject;
1812
- if (this.inboxCompose.claimable) payload.claimable = true;
1943
+ if (this.inboxCompose.claimable && inboxComposeTargetAllowsClaimable.call(this)) payload.claimable = true;
1813
1944
  await this.api("POST", "/messages", payload);
1814
1945
  this.recordOperatorActivity({
1815
- title: this.inboxCompose.claimable ? "Claimable task sent" : "Message sent",
1946
+ title: payload.claimable ? "Claimable task sent" : "Message sent",
1816
1947
  body: this.inboxCompose.subject || this.inboxCompose.body,
1817
1948
  meta: "to " + this.displayTarget(target),
1818
- icon: this.inboxCompose.claimable ? "ti-hand-grab" : "ti-send",
1819
- kind: this.inboxCompose.claimable ? "task" : "operator",
1949
+ icon: payload.claimable ? "ti-hand-grab" : "ti-send",
1950
+ kind: payload.claimable ? "task" : "operator",
1820
1951
  view: inboxPeer(payload) ? "inbox" : "messages",
1821
1952
  peer: inboxPeer(payload),
1822
1953
  });
@@ -1832,7 +1963,7 @@
1832
1963
  this.replyTo = { id: msg.id, from: msg.from };
1833
1964
  this.compose = {
1834
1965
  ...DEFAULT_COMPOSE,
1835
- from: this.view === "inbox" ? HUMAN_AGENT_ID : "",
1966
+ from: HUMAN_AGENT_ID,
1836
1967
  to: replyTarget,
1837
1968
  channel: msg.channel || "",
1838
1969
  };
@@ -1852,7 +1983,7 @@
1852
1983
  if (vm.compose.channel) payload.channel = vm.compose.channel;
1853
1984
  if (vm.compose.subject) payload.subject = vm.compose.subject;
1854
1985
  if (vm.replyTo) payload.replyTo = vm.replyTo.id;
1855
- if (vm.compose.claimable) payload.claimable = true;
1986
+ if (vm.compose.claimable && composeTargetAllowsClaimable.call(vm)) payload.claimable = true;
1856
1987
  return payload;
1857
1988
  }
1858
1989
 
@@ -1876,7 +2007,7 @@
1876
2007
  });
1877
2008
  this.composeOpen = false;
1878
2009
  this.replyTo = null;
1879
- this.compose = { ...DEFAULT_COMPOSE };
2010
+ this.compose = { ...DEFAULT_COMPOSE, from: HUMAN_AGENT_ID };
1880
2011
  await this.fetchMessages();
1881
2012
  } catch (e) {
1882
2013
  alert("Send failed: " + e.message);
@@ -2313,6 +2444,167 @@
2313
2444
  }
2314
2445
  }
2315
2446
 
2447
+ function openAgentSpawn() {
2448
+ this.agentSpawn = { ...DEFAULT_AGENT_SPAWN };
2449
+ this.agentDirectoryBrowser = { open: false, loading: false, path: "", parent: "", home: "", cwd: "", entries: [], error: "" };
2450
+ this.agentSpawnOpen = true;
2451
+ this.$nextTick(() => this.$refs?.agentSpawnCwd?.focus());
2452
+ }
2453
+
2454
+ function closeAgentSpawn() {
2455
+ this.agentSpawnOpen = false;
2456
+ this.agentSpawn = { ...DEFAULT_AGENT_SPAWN };
2457
+ }
2458
+
2459
+ async function openAgentDirectoryBrowser() {
2460
+ this.agentDirectoryBrowser = { ...this.agentDirectoryBrowser, open: true };
2461
+ await this.browseAgentDirectory(this.agentSpawn.cwd || "");
2462
+ }
2463
+
2464
+ async function browseAgentDirectory(path) {
2465
+ this.agentDirectoryBrowser = { ...this.agentDirectoryBrowser, loading: true, error: "" };
2466
+ try {
2467
+ const query = path ? "?path=" + encodeURIComponent(path) : "";
2468
+ const listing = await this.api("GET", "/agents/spawn/directories" + query);
2469
+ this.agentDirectoryBrowser = {
2470
+ open: true,
2471
+ loading: false,
2472
+ path: listing.path || "",
2473
+ parent: listing.parent || "",
2474
+ home: listing.home || "",
2475
+ cwd: listing.cwd || "",
2476
+ entries: listing.entries || [],
2477
+ error: "",
2478
+ };
2479
+ this.agentSpawn = { ...this.agentSpawn, cwd: listing.path || this.agentSpawn.cwd };
2480
+ } catch (e) {
2481
+ this.agentDirectoryBrowser = { ...this.agentDirectoryBrowser, loading: false, error: e.message };
2482
+ }
2483
+ }
2484
+
2485
+ function selectAgentDirectory(path) {
2486
+ this.agentSpawn = { ...this.agentSpawn, cwd: path || this.agentDirectoryBrowser.path };
2487
+ this.agentDirectoryBrowser = { ...this.agentDirectoryBrowser, open: false };
2488
+ }
2489
+
2490
+ async function doSpawnAgent() {
2491
+ if (this.agentSpawn.provider !== "codex") {
2492
+ alert("Only Codex live sessions can be spawned from the dashboard right now.");
2493
+ return;
2494
+ }
2495
+ try {
2496
+ const payload = {
2497
+ provider: "codex",
2498
+ approvalMode: this.agentSpawn.approvalMode || "guarded",
2499
+ };
2500
+ if (this.agentSpawn.cwd) payload.cwd = this.agentSpawn.cwd;
2501
+ if (this.agentSpawn.label) payload.label = this.agentSpawn.label;
2502
+ const result = await this.api("POST", "/agents/spawn", payload);
2503
+ this.recordOperatorActivity({
2504
+ title: "Codex agent spawn requested",
2505
+ body: result?.cwd || payload.cwd || "",
2506
+ meta: result?.pid ? "pid " + result.pid : "starting",
2507
+ icon: "ti-plus",
2508
+ kind: "state",
2509
+ view: "agents",
2510
+ });
2511
+ this.closeAgentSpawn();
2512
+ await Promise.all([this.fetchAgents(), this.fetchActivityEvents()]);
2513
+ } catch (e) {
2514
+ alert("Spawn failed: " + e.message);
2515
+ }
2516
+ }
2517
+
2518
+ // --- Orchestrator methods ---
2519
+
2520
+ function openOrchestratorSpawn() {
2521
+ const online = this.orchestrators.filter((o) => o.status === "online");
2522
+ if (online.length === 0) return alert("No orchestrators online");
2523
+ this.spawnOrchId = online[0].id;
2524
+ this.spawnProvider = "claude";
2525
+ this.spawnCwd = "";
2526
+ this.spawnLabel = "";
2527
+ this.spawnApproval = "guarded";
2528
+ this.spawnPrompt = "";
2529
+ this.spawnDirListing = null;
2530
+ this.orchestratorSpawnOpen = true;
2531
+ }
2532
+
2533
+ function openOrchestratorSpawnFor(orchId) {
2534
+ this.spawnOrchId = orchId;
2535
+ this.spawnProvider = "claude";
2536
+ this.spawnCwd = "";
2537
+ this.spawnLabel = "";
2538
+ this.spawnApproval = "guarded";
2539
+ this.spawnPrompt = "";
2540
+ this.spawnDirListing = null;
2541
+ this.orchestratorSpawnOpen = true;
2542
+ }
2543
+
2544
+ async function browseOrchestratorDirs() {
2545
+ try {
2546
+ const query = this.spawnCwd ? "?path=" + encodeURIComponent(this.spawnCwd) : "";
2547
+ this.spawnDirListing = await this.api("GET", "/agents/spawn/directories" + query);
2548
+ } catch (e) {
2549
+ alert("Directory browse failed: " + e.message);
2550
+ }
2551
+ }
2552
+
2553
+ async function submitOrchestratorSpawn() {
2554
+ if (!this.spawnOrchId) return alert("Select an orchestrator");
2555
+ try {
2556
+ const payload = {
2557
+ provider: this.spawnProvider,
2558
+ approvalMode: this.spawnApproval,
2559
+ };
2560
+ if (this.spawnCwd) payload.cwd = this.spawnCwd;
2561
+ if (this.spawnLabel) payload.label = this.spawnLabel;
2562
+ if (this.spawnPrompt) payload.prompt = this.spawnPrompt;
2563
+ await this.api("POST", "/orchestrators/" + encodeURIComponent(this.spawnOrchId) + "/spawn", payload);
2564
+ this.orchestratorSpawnOpen = false;
2565
+ this.recordOperatorActivity({
2566
+ title: `${this.spawnProvider} agent spawn requested`,
2567
+ body: this.spawnCwd || "",
2568
+ meta: this.spawnOrchId,
2569
+ icon: "ti-plus",
2570
+ kind: "state",
2571
+ view: "orchestrators",
2572
+ });
2573
+ await Promise.all([this.fetchOrchestrators(), this.fetchActivityEvents()]);
2574
+ } catch (e) {
2575
+ alert("Spawn failed: " + e.message);
2576
+ }
2577
+ }
2578
+
2579
+ async function orchestratorAction(orchId, action, agentId) {
2580
+ const label = action === "restart" ? "Restart" : "Shutdown";
2581
+ if (!confirm(`${label} agent "${agentId || "all"}" on orchestrator "${orchId}"?`)) return;
2582
+ try {
2583
+ await this.api("POST", "/orchestrators/" + encodeURIComponent(orchId) + "/actions", { action, agentId });
2584
+ this.recordOperatorActivity({
2585
+ title: `Agent ${action} requested`,
2586
+ body: agentId || "all agents",
2587
+ meta: orchId,
2588
+ icon: action === "restart" ? "ti-refresh" : "ti-power",
2589
+ kind: "state",
2590
+ view: "orchestrators",
2591
+ });
2592
+ await Promise.all([this.fetchOrchestrators(), this.fetchActivityEvents()]);
2593
+ } catch (e) {
2594
+ alert(`${label} failed: ` + e.message);
2595
+ }
2596
+ }
2597
+
2598
+ async function deleteOrchestrator(orchId) {
2599
+ if (!confirm(`Remove orchestrator "${orchId}"? This will NOT stop its managed agents.`)) return;
2600
+ try {
2601
+ await this.api("DELETE", "/orchestrators/" + encodeURIComponent(orchId));
2602
+ await this.fetchOrchestrators();
2603
+ } catch (e) {
2604
+ alert("Delete failed: " + e.message);
2605
+ }
2606
+ }
2607
+
2316
2608
  function closePairMessage() {
2317
2609
  this.pairMessageOpen = false;
2318
2610
  this.pairMessage = { ...DEFAULT_PAIR_MESSAGE };
@@ -2409,6 +2701,16 @@
2409
2701
  this.agentDetailOpen = false;
2410
2702
  },
2411
2703
 
2704
+ openChannelDetail(channel) {
2705
+ if (!channel) return;
2706
+ this.channelDetailId = channel.id;
2707
+ this.channelDetailOpen = true;
2708
+ },
2709
+
2710
+ closeChannelDetail() {
2711
+ this.channelDetailOpen = false;
2712
+ },
2713
+
2412
2714
  openRename(agent) {
2413
2715
  this.renameModal = { show: true, agentId: agent.id, label: agent.label || "" };
2414
2716
  this.$nextTick(() => this.$refs.renameInput?.focus());
@@ -2438,6 +2740,26 @@
2438
2740
  alert("Delete failed: " + e.message);
2439
2741
  }
2440
2742
  },
2743
+
2744
+ async doAgentAction(agent, action) {
2745
+ if (!agent || !action) return;
2746
+ try {
2747
+ const result = await this.api("POST", "/agents/" + encodeURIComponent(agent.id) + "/actions", { action });
2748
+ this.recordOperatorActivity({
2749
+ title: action === "restart" ? "Agent restart requested" : "Agent shutdown requested",
2750
+ body: this.displayName(agent),
2751
+ meta: agent.id,
2752
+ icon: action === "restart" ? "ti-refresh" : "ti-power",
2753
+ kind: "state",
2754
+ view: "agents",
2755
+ agentId: agent.id,
2756
+ messageId: result?.message?.id,
2757
+ });
2758
+ await Promise.all([this.fetchMessages(), this.fetchActivityEvents()]);
2759
+ } catch (e) {
2760
+ alert("Agent action failed: " + e.message);
2761
+ }
2762
+ },
2441
2763
  };
2442
2764
  }
2443
2765
 
@@ -2495,7 +2817,7 @@
2495
2817
 
2496
2818
  function renderStatusChart() {
2497
2819
  const { labels, series } = countAgentStatuses(this.agents);
2498
- const colorMap = { online: "#48bb78", idle: "#48bb78", busy: "#ecc94b", offline: "#718096" };
2820
+ const colorMap = { idle: "#4299e1", busy: "#ecc94b" };
2499
2821
 
2500
2822
  if (this.chartInstances.status) {
2501
2823
  this.chartInstances.status.updateOptions({
@@ -2522,10 +2844,12 @@
2522
2844
  }
2523
2845
 
2524
2846
  function countAgentStatuses(agents) {
2525
- const counts = { online: 0, idle: 0, busy: 0, offline: 0 };
2526
- for (const agent of agents) counts[agent.status] = (counts[agent.status] || 0) + 1;
2847
+ const counts = { idle: 0, busy: 0 };
2848
+ for (const agent of agents) {
2849
+ if (agent.status in counts) counts[agent.status] += 1;
2850
+ }
2527
2851
 
2528
- const labels = Object.keys(counts).filter((key) => counts[key] > 0);
2852
+ const labels = Object.keys(counts);
2529
2853
  return { labels, series: labels.map((key) => counts[key]) };
2530
2854
  }
2531
2855