agent-relay-server 0.4.39 → 0.6.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/README.md CHANGED
@@ -40,20 +40,20 @@ Agent Relay has two parts:
40
40
 
41
41
  Pick your provider: install the Claude Code plugin, the Codex connector, or both. The shared config is the same four env vars either way. See the [full mental model](docs/mental-model.md) for routing, identity, and task lifecycle details, or the [provider spec](docs/provider-spec.md) if you want to build your own integration.
42
42
 
43
- ## Extension Model
43
+ ## Connector Model
44
44
 
45
- Agent Relay is built around four extension points:
45
+ Agent Relay extensions run as external **Connectors**. The relay discovers them from `~/.agent-relay/connectors/<id>/manifest.json`, then can show status and run lifecycle actions without loading connector code in-process.
46
46
 
47
47
  > **Agents do work. Providers host agents. Integrations create work. Channels carry conversations.**
48
48
 
49
- That split keeps the system small without making everything a one-off plugin:
49
+ Connector kinds:
50
50
 
51
- - **Agents** are running AI sessions or workers that can receive messages, claim tasks, and produce results.
52
- - **Providers** connect an AI runtime to the relay. The Claude plugin and Codex connector are providers.
53
- - **Integrations** connect external systems that create or update work. CI, monitoring, support desks, and deployment tools fit here.
54
- - **Channels** connect communication surfaces where humans and systems already talk. Telegram is the first channel; Slack, email, SMS, Matrix, Discord, and web chat are natural next ones.
51
+ - **Provider connectors** host AI runtimes. The Claude plugin and Codex connector register coding-agent sessions as Relay agents.
52
+ - **Event connectors** connect external systems that create or update work. CI, monitoring, support desks, and deployment tools fit here.
53
+ - **Channel connectors** connect communication surfaces where humans and systems already talk. Telegram is the first channel; Slack, email, SMS, Matrix, Discord, and web chat are natural next ones.
54
+ - **Orchestrator connectors** supervise hosts and can spawn or control provider sessions.
55
55
 
56
- If you want to contribute, pick the shape that matches your system. Building a new AI runtime adapter? Build a provider. Sending alerts or tickets into Agent Relay? Build an integration. Opening Agent Relay to another messaging app? Build a channel.
56
+ If you want to contribute, pick the shape that matches your system. Building a new AI runtime adapter? Build a provider connector. Sending alerts or tickets into Agent Relay? Build an event connector. Opening Agent Relay to another messaging app? Build a channel connector.
57
57
 
58
58
  ## Quick Start
59
59
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-server",
3
- "version": "0.4.39",
3
+ "version": "0.6.0",
4
4
  "description": "Lightweight HTTP message relay for inter-agent communication across machines",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
@@ -54,11 +54,13 @@
54
54
 
55
55
  agents: [],
56
56
  agentsById: {},
57
+ orchestrators: [],
57
58
  pairs: [],
58
59
  messages: [],
59
60
  tasks: [],
60
61
  integrations: [],
61
62
  channels: [],
63
+ connectors: [],
62
64
  taskEvents: [],
63
65
  taskEventCache: {},
64
66
  stats: {},
@@ -85,6 +87,14 @@
85
87
  replyTo: null,
86
88
  composeOpen: false,
87
89
  agentSpawnOpen: false,
90
+ orchestratorSpawnOpen: false,
91
+ spawnOrchId: "",
92
+ spawnProvider: "claude",
93
+ spawnCwd: "",
94
+ spawnLabel: "",
95
+ spawnApproval: "guarded",
96
+ spawnPrompt: "",
97
+ spawnDirListing: null,
88
98
  agentDirectoryBrowser: { open: false, loading: false, path: "", parent: "", home: "", cwd: "", entries: [], error: "" },
89
99
  pairInviteOpen: false,
90
100
  pairMessageOpen: false,
@@ -231,6 +241,7 @@
231
241
  if (view === "work") await Promise.all([this.fetchMessages(), this.fetchTasks()]);
232
242
  if (view === "pairs") this.fetchPairs();
233
243
  if (view === "channels") await this.fetchChannels();
244
+ if (view === "connectors") await this.fetchConnectors();
234
245
  if (view === "integrations") await this.fetchIntegrations();
235
246
  if (view === "tasks") this.fetchTasks();
236
247
  },
@@ -281,6 +292,8 @@
281
292
  es.addEventListener("message.claimed", (event) => handleMessageClaimed(this, parseEventData(event)));
282
293
  es.addEventListener("message.claim_released", (event) => handleMessageClaimReleased(this, parseEventData(event)));
283
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)));
284
297
  registerTaskEvents(this, es);
285
298
  }
286
299
 
@@ -320,6 +333,14 @@
320
333
  refreshChartsIfVisible(vm);
321
334
  }
322
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
+
323
344
  function handleMessageClaimed(vm, data) {
324
345
  const msg = vm.messages.find((item) => item.id === data.messageId);
325
346
  if (!msg) return;
@@ -374,7 +395,7 @@
374
395
  },
375
396
 
376
397
  async refresh() {
377
- await Promise.all([this.fetchStats(), this.fetchHealth(), this.fetchAgents(), this.fetchPairs(), this.fetchMessages(), this.fetchTasks(), this.fetchChannels(), this.fetchIntegrations(), this.fetchInboxState(), this.fetchActivityEvents()]);
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()]);
378
399
  },
379
400
 
380
401
  async refreshLiveData() {
@@ -407,6 +428,12 @@
407
428
  } catch {}
408
429
  },
409
430
 
431
+ async fetchOrchestrators() {
432
+ try {
433
+ this.orchestrators = await this.api("GET", "/orchestrators");
434
+ } catch {}
435
+ },
436
+
410
437
  async fetchPairs() {
411
438
  try {
412
439
  let pairs;
@@ -451,6 +478,12 @@
451
478
  } catch {}
452
479
  },
453
480
 
481
+ async fetchConnectors() {
482
+ try {
483
+ this.connectors = await this.api("GET", "/connectors");
484
+ } catch {}
485
+ },
486
+
454
487
  async fetchChannels() {
455
488
  try {
456
489
  this.channels = await this.api("GET", "/channels");
@@ -523,6 +556,7 @@
523
556
  activityItems: { get: getActivityItems },
524
557
  workQueueItems: { get: getWorkQueueItems },
525
558
  channelCards: { get: getChannelCards },
559
+ connectorCards: { get: getConnectorCards },
526
560
  integrationCards: { get: getIntegrationCards },
527
561
  openPairCount: { get: getOpenPairCount },
528
562
  filteredMessages: { get: getFilteredMessages },
@@ -535,9 +569,15 @@
535
569
  healthIssues: { get: getHealthIssues },
536
570
  healthDiagnostics: { get: getHealthDiagnostics },
537
571
  commandPaletteItems: { get: getCommandPaletteItems },
572
+ spawnAvailableProviders: { get: getSpawnAvailableProviders },
538
573
  };
539
574
  }
540
575
 
576
+ function getSpawnAvailableProviders() {
577
+ const orch = this.orchestrators.find((o) => o.id === this.spawnOrchId);
578
+ return orch ? orch.providers : ["claude", "codex"];
579
+ }
580
+
541
581
  function getOnlineCount() {
542
582
  return visibleAgents(this).filter((agent) => agent.status !== "offline").length;
543
583
  }
@@ -613,6 +653,11 @@
613
653
  if (!message || !channel) return false;
614
654
  const channelKeys = [channel.id, channel.type, ...(channel.topicChannels || [])].filter(Boolean);
615
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
+ }
616
661
  if (channel.agentId && (message.from === channel.agentId || message.to === channel.agentId)) return true;
617
662
  return false;
618
663
  }
@@ -679,7 +724,7 @@
679
724
  const haystack = [
680
725
  vm.displayTarget(thread.peer),
681
726
  thread.peer,
682
- ...thread.messages.flatMap((msg) => [msg.subject || "", msg.body || "", msg.channel || "", vm.displayTarget(msg.from), vm.displayTarget(msg.to)]),
727
+ ...thread.messages.flatMap((msg) => [msg.subject || "", messageBody.call(vm, msg) || "", msg.channel || "", vm.displayTarget(msg.from), vm.displayTarget(msg.to)]),
683
728
  ].join("\n").toLowerCase();
684
729
  return haystack.includes(search);
685
730
  }
@@ -768,12 +813,12 @@
768
813
  }));
769
814
 
770
815
  const messageItems = (this.messages || [])
771
- .filter((msg) => msg.claimable)
816
+ .filter(isClaimableMessageWaiting)
772
817
  .map((msg) => ({
773
818
  id: "message-" + msg.id,
774
819
  sourceType: "message",
775
820
  title: msg.subject || "Claimable message #" + msg.id,
776
- body: msg.body,
821
+ body: messageBody(msg),
777
822
  severity: "warning",
778
823
  status: msg.claimedBy ? "claimed" : "open",
779
824
  owner: msg.claimedBy || "",
@@ -821,7 +866,7 @@
821
866
  return (vm.messages || []).flatMap((msg) => {
822
867
  const ts = toTimestamp(msg.createdAt);
823
868
  const items = [];
824
- const pairEvent = msg.meta?.pairEvent;
869
+ const pairEvent = msg.payload?.pairEvent;
825
870
  if (pairEvent) {
826
871
  items.push(activityItem({
827
872
  id: "pair-message-" + msg.id,
@@ -837,10 +882,10 @@
837
882
  } else if (msg.from === HUMAN_AGENT_ID) {
838
883
  items.push(activityItem({
839
884
  id: "human-send-" + msg.id,
840
- kind: msg.claimable ? "task" : "message",
885
+ kind: msg.kind === "task" ? "task" : "message",
841
886
  ts,
842
- icon: msg.claimable ? "ti-hand-grab" : "ti-send",
843
- title: msg.claimable ? "Claimable task sent" : "Message sent",
887
+ icon: msg.kind === "task" ? "ti-hand-grab" : "ti-send",
888
+ title: msg.kind === "task" ? "Claimable task sent" : "Message sent",
844
889
  body: vm.messagePreview(msg),
845
890
  meta: "to " + vm.displayTarget(msg.to),
846
891
  view: inboxPeer(msg) ? "inbox" : "messages",
@@ -1018,7 +1063,7 @@
1018
1063
  }
1019
1064
 
1020
1065
  function messageLooksLikeQuestion(msg) {
1021
- return /\?/.test(`${msg.subject || ""}\n${msg.body || ""}`);
1066
+ return /\?/.test(`${msg.subject || ""}\n${messageBody(msg)}`);
1022
1067
  }
1023
1068
 
1024
1069
  function countClaimableWaiting(vm) {
@@ -1038,7 +1083,7 @@
1038
1083
  }
1039
1084
 
1040
1085
  function isClaimableMessageWaiting(msg) {
1041
- return Boolean(msg.claimable && !msg.claimedBy);
1086
+ return Boolean(msg.claimable && !msg.claimedBy && !(msg.kind === "task" && Number.isSafeInteger(msg.payload?.taskId)));
1042
1087
  }
1043
1088
 
1044
1089
  function targetMatchesAgent(target, agent) {
@@ -1110,6 +1155,17 @@
1110
1155
  });
1111
1156
  }
1112
1157
 
1158
+ function getConnectorCards() {
1159
+ return [...(this.connectors || [])].sort((a, b) => {
1160
+ const statusRank = { error: 0, warn: 1, unknown: 2, ok: 3 };
1161
+ const aStatus = a.runtime?.status || "unknown";
1162
+ const bStatus = b.runtime?.status || "unknown";
1163
+ const statusDiff = (statusRank[aStatus] ?? 2) - (statusRank[bStatus] ?? 2);
1164
+ if (statusDiff !== 0) return statusDiff;
1165
+ return String(a.displayName || a.id || "").localeCompare(String(b.displayName || b.id || ""));
1166
+ });
1167
+ }
1168
+
1113
1169
  function getOpenPairCount() {
1114
1170
  return (this.pairs || []).filter(isOpenPair).length;
1115
1171
  }
@@ -1173,7 +1229,7 @@
1173
1229
  }
1174
1230
 
1175
1231
  function isChannelAgent(agent) {
1176
- return agent?.meta?.kind === "channel" || agent?.tags?.includes("channel");
1232
+ return agent?.kind === "channel" || agent?.meta?.kind === "channel" || agent?.tags?.includes("channel");
1177
1233
  }
1178
1234
 
1179
1235
  function agentSupportsControlActions(agent) {
@@ -1312,6 +1368,7 @@
1312
1368
  displayName,
1313
1369
  displayTarget,
1314
1370
  conversationTitle,
1371
+ messageBody,
1315
1372
  messagePreview,
1316
1373
  agentPair,
1317
1374
  pairPeerId,
@@ -1332,6 +1389,7 @@
1332
1389
  isBuiltInAgent,
1333
1390
  agentChannels,
1334
1391
  channelPresence,
1392
+ connectorPresence,
1335
1393
  integrationPresence,
1336
1394
  composeTargetAllowsClaimable,
1337
1395
  inboxComposeTargetAllowsClaimable,
@@ -1365,10 +1423,43 @@
1365
1423
  }
1366
1424
 
1367
1425
  function messagePreview(msg) {
1368
- const text = msg?.subject || msg?.body || "";
1426
+ const text = msg?.subject || messageBody(msg) || "";
1369
1427
  return text.length > 90 ? text.slice(0, 90) + "..." : text;
1370
1428
  }
1371
1429
 
1430
+ function messageBody(msg) {
1431
+ if (!msg) return "";
1432
+ const payload = msg.payload || {};
1433
+ const channelMessage = payload.message;
1434
+ if (channelMessage && typeof channelMessage === "object" && typeof channelMessage.text === "string" && channelMessage.text.trim()) {
1435
+ return channelMessage.text;
1436
+ }
1437
+ const interaction = payload.interaction;
1438
+ if (interaction && typeof interaction === "object") {
1439
+ const title = typeof interaction.title === "string" ? interaction.title.trim() : "";
1440
+ const description = typeof interaction.description === "string" ? interaction.description.trim() : "";
1441
+ if (title && description) return title + "\n" + description;
1442
+ if (title) return title;
1443
+ if (description) return description;
1444
+ }
1445
+ const reaction = payload.reaction;
1446
+ if (reaction && typeof reaction === "object") {
1447
+ const name = typeof reaction.name === "string" ? reaction.name : "";
1448
+ const emoji = typeof reaction.emoji === "string" ? reaction.emoji : "";
1449
+ const value = typeof reaction.value === "string" ? reaction.value : "";
1450
+ return ["Reaction", emoji || name || value].filter(Boolean).join(": ");
1451
+ }
1452
+ const activity = payload.activity;
1453
+ if (activity && typeof activity === "object") {
1454
+ const kind = typeof activity.kind === "string" ? activity.kind : "activity";
1455
+ const state = typeof activity.state === "string" ? activity.state : "";
1456
+ return [kind, state].filter(Boolean).join(" ");
1457
+ }
1458
+ if (typeof payload.text === "string" && payload.text.trim()) return payload.text;
1459
+ if (typeof payload.message === "string" && payload.message.trim()) return payload.message;
1460
+ return msg.body || "";
1461
+ }
1462
+
1372
1463
  function agentPair(agent) {
1373
1464
  return agent ? this.pairsByAgentId[agent.id] : null;
1374
1465
  }
@@ -1552,6 +1643,16 @@
1552
1643
  return { label: "configured", tone: "primary", icon: "ti-plug-connected" };
1553
1644
  }
1554
1645
 
1646
+ function connectorPresence(connector) {
1647
+ const runtime = connector?.runtime || {};
1648
+ if (runtime.status === "error") return { label: "error", tone: "danger", icon: "ti-alert-triangle" };
1649
+ if (runtime.status === "warn") return { label: "warning", tone: "warning", icon: "ti-alert-circle" };
1650
+ if (runtime.running) return { label: "running", tone: "success", icon: "ti-circle-check" };
1651
+ if (runtime.enabled === false) return { label: "disabled", tone: "secondary", icon: "ti-player-pause" };
1652
+ if (runtime.status === "ok") return { label: "ok", tone: "success", icon: "ti-circle-check" };
1653
+ return { label: "unknown", tone: "secondary", icon: "ti-help-circle" };
1654
+ }
1655
+
1555
1656
  function agentChannels(agent) {
1556
1657
  if (!agent) return [];
1557
1658
  return (this.channels || []).filter((ch) => ch.agentId === agent.id);
@@ -1644,6 +1745,7 @@
1644
1745
  recordOperatorActivity,
1645
1746
  openActivityItem,
1646
1747
  runHealthAction,
1748
+ runConnectorAction,
1647
1749
  openCommandPalette,
1648
1750
  closeCommandPalette,
1649
1751
  runCommand,
@@ -1667,6 +1769,12 @@
1667
1769
  openAgentDirectoryBrowser,
1668
1770
  browseAgentDirectory,
1669
1771
  selectAgentDirectory,
1772
+ openOrchestratorSpawn,
1773
+ openOrchestratorSpawnFor,
1774
+ browseOrchestratorDirs,
1775
+ submitOrchestratorSpawn,
1776
+ orchestratorAction,
1777
+ deleteOrchestrator,
1670
1778
  doSendPairMessage,
1671
1779
  doAcceptPair,
1672
1780
  doRejectPair,
@@ -1903,7 +2011,11 @@
1903
2011
  };
1904
2012
  if (this.inboxCompose.channel) payload.channel = this.inboxCompose.channel;
1905
2013
  if (this.inboxCompose.subject) payload.subject = this.inboxCompose.subject;
1906
- if (this.inboxCompose.claimable && inboxComposeTargetAllowsClaimable.call(this)) payload.claimable = true;
2014
+ if (this.inboxCompose.claimable && inboxComposeTargetAllowsClaimable.call(this)) {
2015
+ payload.claimable = true;
2016
+ payload.kind = "task";
2017
+ payload.payload = { title: this.inboxCompose.subject || "Claimable task" };
2018
+ }
1907
2019
  await this.api("POST", "/messages", payload);
1908
2020
  this.recordOperatorActivity({
1909
2021
  title: payload.claimable ? "Claimable task sent" : "Message sent",
@@ -1946,7 +2058,11 @@
1946
2058
  if (vm.compose.channel) payload.channel = vm.compose.channel;
1947
2059
  if (vm.compose.subject) payload.subject = vm.compose.subject;
1948
2060
  if (vm.replyTo) payload.replyTo = vm.replyTo.id;
1949
- if (vm.compose.claimable && composeTargetAllowsClaimable.call(vm)) payload.claimable = true;
2061
+ if (vm.compose.claimable && composeTargetAllowsClaimable.call(vm)) {
2062
+ payload.claimable = true;
2063
+ payload.kind = "task";
2064
+ payload.payload = { title: vm.compose.subject || "Claimable task" };
2065
+ }
1950
2066
  return payload;
1951
2067
  }
1952
2068
 
@@ -2163,6 +2279,16 @@
2163
2279
  if (action.copy) await copyText(action.copy);
2164
2280
  }
2165
2281
 
2282
+ async function runConnectorAction(connector, action) {
2283
+ if (!connector || !action) return;
2284
+ try {
2285
+ await this.api("POST", "/connectors/" + encodeURIComponent(connector.id) + "/actions", { action });
2286
+ await this.fetchConnectors();
2287
+ } catch (e) {
2288
+ alert("Connector action failed: " + e.message);
2289
+ }
2290
+ }
2291
+
2166
2292
  async function copyText(value) {
2167
2293
  if (typeof navigator === "undefined") return;
2168
2294
  try {
@@ -2252,8 +2378,8 @@
2252
2378
 
2253
2379
  function pairMessages(vm, pair) {
2254
2380
  return (vm.messages || []).filter((msg) =>
2255
- msg.meta?.pairId === pair.id ||
2256
- (msg.meta?.pairEvent && [pair.requesterId, pair.targetId].includes(msg.from) && [pair.requesterId, pair.targetId].includes(msg.to))
2381
+ msg.payload?.pairId === pair.id ||
2382
+ (msg.payload?.pairEvent && [pair.requesterId, pair.targetId].includes(msg.from) && [pair.requesterId, pair.targetId].includes(msg.to))
2257
2383
  );
2258
2384
  }
2259
2385
 
@@ -2306,7 +2432,7 @@
2306
2432
  if (msg.channel) lines.push("- Channel: " + msg.channel);
2307
2433
  if (msg.subject) lines.push("- Subject: " + msg.subject);
2308
2434
  if (msg.claimedBy) lines.push("- Claimed by: " + vm.displayTarget(msg.claimedBy));
2309
- lines.push("", msg.body || "", "");
2435
+ lines.push("", messageBody.call(vm, msg) || "", "");
2310
2436
  }
2311
2437
  }
2312
2438
 
@@ -2478,6 +2604,96 @@
2478
2604
  }
2479
2605
  }
2480
2606
 
2607
+ // --- Orchestrator methods ---
2608
+
2609
+ function openOrchestratorSpawn() {
2610
+ const online = this.orchestrators.filter((o) => o.status === "online");
2611
+ if (online.length === 0) return alert("No orchestrators online");
2612
+ this.spawnOrchId = online[0].id;
2613
+ this.spawnProvider = "claude";
2614
+ this.spawnCwd = "";
2615
+ this.spawnLabel = "";
2616
+ this.spawnApproval = "guarded";
2617
+ this.spawnPrompt = "";
2618
+ this.spawnDirListing = null;
2619
+ this.orchestratorSpawnOpen = true;
2620
+ }
2621
+
2622
+ function openOrchestratorSpawnFor(orchId) {
2623
+ this.spawnOrchId = orchId;
2624
+ this.spawnProvider = "claude";
2625
+ this.spawnCwd = "";
2626
+ this.spawnLabel = "";
2627
+ this.spawnApproval = "guarded";
2628
+ this.spawnPrompt = "";
2629
+ this.spawnDirListing = null;
2630
+ this.orchestratorSpawnOpen = true;
2631
+ }
2632
+
2633
+ async function browseOrchestratorDirs() {
2634
+ try {
2635
+ const query = this.spawnCwd ? "?path=" + encodeURIComponent(this.spawnCwd) : "";
2636
+ this.spawnDirListing = await this.api("GET", "/agents/spawn/directories" + query);
2637
+ } catch (e) {
2638
+ alert("Directory browse failed: " + e.message);
2639
+ }
2640
+ }
2641
+
2642
+ async function submitOrchestratorSpawn() {
2643
+ if (!this.spawnOrchId) return alert("Select an orchestrator");
2644
+ try {
2645
+ const payload = {
2646
+ provider: this.spawnProvider,
2647
+ approvalMode: this.spawnApproval,
2648
+ };
2649
+ if (this.spawnCwd) payload.cwd = this.spawnCwd;
2650
+ if (this.spawnLabel) payload.label = this.spawnLabel;
2651
+ if (this.spawnPrompt) payload.prompt = this.spawnPrompt;
2652
+ await this.api("POST", "/orchestrators/" + encodeURIComponent(this.spawnOrchId) + "/spawn", payload);
2653
+ this.orchestratorSpawnOpen = false;
2654
+ this.recordOperatorActivity({
2655
+ title: `${this.spawnProvider} agent spawn requested`,
2656
+ body: this.spawnCwd || "",
2657
+ meta: this.spawnOrchId,
2658
+ icon: "ti-plus",
2659
+ kind: "state",
2660
+ view: "orchestrators",
2661
+ });
2662
+ await Promise.all([this.fetchOrchestrators(), this.fetchActivityEvents()]);
2663
+ } catch (e) {
2664
+ alert("Spawn failed: " + e.message);
2665
+ }
2666
+ }
2667
+
2668
+ async function orchestratorAction(orchId, action, agentId) {
2669
+ const label = action === "restart" ? "Restart" : "Shutdown";
2670
+ if (!confirm(`${label} agent "${agentId || "all"}" on orchestrator "${orchId}"?`)) return;
2671
+ try {
2672
+ await this.api("POST", "/orchestrators/" + encodeURIComponent(orchId) + "/actions", { action, agentId });
2673
+ this.recordOperatorActivity({
2674
+ title: `Agent ${action} requested`,
2675
+ body: agentId || "all agents",
2676
+ meta: orchId,
2677
+ icon: action === "restart" ? "ti-refresh" : "ti-power",
2678
+ kind: "state",
2679
+ view: "orchestrators",
2680
+ });
2681
+ await Promise.all([this.fetchOrchestrators(), this.fetchActivityEvents()]);
2682
+ } catch (e) {
2683
+ alert(`${label} failed: ` + e.message);
2684
+ }
2685
+ }
2686
+
2687
+ async function deleteOrchestrator(orchId) {
2688
+ if (!confirm(`Remove orchestrator "${orchId}"? This will NOT stop its managed agents.`)) return;
2689
+ try {
2690
+ await this.api("DELETE", "/orchestrators/" + encodeURIComponent(orchId));
2691
+ await this.fetchOrchestrators();
2692
+ } catch (e) {
2693
+ alert("Delete failed: " + e.message);
2694
+ }
2695
+ }
2696
+
2481
2697
  function closePairMessage() {
2482
2698
  this.pairMessageOpen = false;
2483
2699
  this.pairMessage = { ...DEFAULT_PAIR_MESSAGE };
Binary file
Binary file
@@ -0,0 +1,14 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
2
+ <rect width="512" height="512" rx="96" fill="#0d1117"/>
3
+ <g stroke="#30363d" stroke-width="22" stroke-linecap="round">
4
+ <line x1="137" y1="151" x2="221" y2="229"/>
5
+ <line x1="375" y1="151" x2="291" y2="229"/>
6
+ <line x1="137" y1="361" x2="221" y2="283"/>
7
+ <line x1="375" y1="361" x2="291" y2="283"/>
8
+ </g>
9
+ <circle cx="256" cy="256" r="72" fill="#58a6ff"/>
10
+ <circle cx="96" cy="116" r="42" fill="#3fb950"/>
11
+ <circle cx="416" cy="116" r="42" fill="#3fb950"/>
12
+ <circle cx="96" cy="396" r="42" fill="#3fb950"/>
13
+ <circle cx="416" cy="396" r="42" fill="#3fb950"/>
14
+ </svg>