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/public/index.html CHANGED
@@ -205,18 +205,27 @@
205
205
  </a>
206
206
  <a href="#" class="nav-link" :class="{ active: view === 'agents' }" @click.prevent="switchView('agents')">
207
207
  <i class="ti ti-robot"></i>Agents
208
- <span class="badge bg-warning text-white ms-auto" x-show="attentionAgentCount > 0" x-text="attentionAgentCount"></span>
209
- <span class="badge bg-success text-white ms-1" x-text="onlineCount"></span>
208
+ <span class="ms-auto d-inline-flex gap-1 align-items-center">
209
+ <span class="badge bg-warning text-white" x-show="attentionAgentCount > 0" x-text="attentionAgentCount"></span>
210
+ <span class="badge bg-success text-white" x-show="onlineCount > 0" x-text="onlineCount"></span>
211
+ </span>
212
+ </a>
213
+ <a href="#" class="nav-link" :class="{ active: view === 'orchestrators' }" @click.prevent="switchView('orchestrators')">
214
+ <i class="ti ti-server-2"></i>Orchestrators
215
+ <span class="ms-auto d-inline-flex gap-1 align-items-center">
216
+ <span class="badge bg-success text-white" x-show="orchestrators.filter(o => o.status === 'online').length > 0" x-text="orchestrators.filter(o => o.status === 'online').length"></span>
217
+ </span>
210
218
  </a>
211
219
  <a href="#" class="nav-link" :class="{ active: view === 'channels' }" @click.prevent="switchView('channels')">
212
220
  <i class="ti ti-messages"></i>Channels
213
221
  <span class="badge bg-success text-white ms-auto" x-show="channelCards.filter((item) => item.ready).length > 0" x-text="channelCards.filter((item) => item.ready).length"></span>
214
- <span class="badge bg-secondary text-white ms-1" x-text="channelCards.length"></span>
215
222
  </a>
216
223
  <a href="#" class="nav-link" :class="{ active: view === 'integrations' }" @click.prevent="switchView('integrations')">
217
224
  <i class="ti ti-plug-connected"></i>Integrations
218
- <span class="badge bg-warning text-white ms-auto" x-show="integrationCards.some((item) => (item.taskStats?.waitingTasks || 0) > 0)" x-text="integrationCards.reduce((total, item) => total + (item.taskStats?.waitingTasks || 0), 0)"></span>
219
- <span class="badge bg-secondary text-white ms-1" x-text="integrationCards.length"></span>
225
+ <span class="ms-auto d-inline-flex gap-1 align-items-center">
226
+ <span class="badge bg-warning text-white" x-show="integrationCards.some((item) => (item.taskStats?.waitingTasks || 0) > 0)" x-text="integrationCards.reduce((total, item) => total + (item.taskStats?.waitingTasks || 0), 0)"></span>
227
+ <span class="badge bg-secondary text-white" x-show="integrationCards.length > 0" x-text="integrationCards.length"></span>
228
+ </span>
220
229
  </a>
221
230
  <a href="#" class="nav-link" :class="{ active: view === 'inbox' }" @click.prevent="switchView('inbox')">
222
231
  <i class="ti ti-inbox"></i>Inbox
@@ -228,8 +237,10 @@
228
237
  </a>
229
238
  <a href="#" class="nav-link" :class="{ active: view === 'pairs' }" @click.prevent="switchView('pairs')">
230
239
  <i class="ti ti-link"></i>Pairs
231
- <span class="badge bg-warning text-white ms-auto" x-show="attentionSummary.pendingPairInvites > 0" x-text="attentionSummary.pendingPairInvites"></span>
232
- <span class="badge bg-primary text-white ms-1" x-text="pairs.length"></span>
240
+ <span class="ms-auto d-inline-flex gap-1 align-items-center">
241
+ <span class="badge bg-warning text-white" x-show="attentionSummary.pendingPairInvites > 0" x-text="attentionSummary.pendingPairInvites"></span>
242
+ <span class="badge bg-primary text-white" x-show="openPairCount > 0" x-text="openPairCount"></span>
243
+ </span>
233
244
  </a>
234
245
  <a href="#" class="nav-link" :class="{ active: view === 'messages' }" @click.prevent="switchView('messages')">
235
246
  <i class="ti ti-messages"></i>Messages
@@ -240,8 +251,10 @@
240
251
  </a>
241
252
  <a href="#" class="nav-link" :class="{ active: view === 'tasks' }" @click.prevent="switchView('tasks')">
242
253
  <i class="ti ti-checkup-list"></i>Tasks
243
- <span class="badge bg-warning text-white ms-auto" x-show="attentionSummary.claimableTasks > 0" x-text="attentionSummary.claimableTasks"></span>
244
- <span class="badge bg-secondary text-white ms-1" x-text="stats.openTasks ?? 0"></span>
254
+ <span class="ms-auto d-inline-flex gap-1 align-items-center">
255
+ <span class="badge bg-warning text-white" x-show="attentionSummary.claimableTasks > 0" x-text="attentionSummary.claimableTasks"></span>
256
+ <span class="badge bg-secondary text-white" x-show="(stats.openTasks ?? 0) > 0" x-text="stats.openTasks ?? 0"></span>
257
+ </span>
245
258
  </a>
246
259
  <a href="#" class="nav-link" :class="{ active: view === 'analytics' }" @click.prevent="switchView('analytics')">
247
260
  <i class="ti ti-chart-area-line"></i>Analytics
@@ -525,6 +538,9 @@
525
538
  <div class="d-flex align-items-center mb-3 gap-3 flex-wrap">
526
539
  <h2 class="page-title mb-0">Agents</h2>
527
540
  <div class="ms-auto d-flex gap-2 align-items-center">
541
+ <button class="btn btn-sm btn-primary" @click="openAgentSpawn()" title="Create Codex live agent">
542
+ <i class="ti ti-plus"></i>
543
+ </button>
528
544
  <select class="form-select form-select-sm" style="width:auto; min-width: 150px" x-model="agentPresetFilter">
529
545
  <option value="">View: All</option>
530
546
  <option value="active">View: Active agents</option>
@@ -648,6 +664,185 @@
648
664
  </template>
649
665
  </div>
650
666
 
667
+ <!-- ==================== ORCHESTRATORS ==================== -->
668
+ <div x-show="view === 'orchestrators'" x-cloak class="fade-in">
669
+ <div class="d-flex align-items-center mb-3 gap-2 flex-wrap">
670
+ <h2 class="page-title mb-0">Orchestrators</h2>
671
+ <span class="badge bg-success-lt" x-show="orchestrators.filter(o => o.status === 'online').length > 0"
672
+ x-text="orchestrators.filter(o => o.status === 'online').length + ' online'"></span>
673
+ <div class="ms-auto">
674
+ <button class="btn btn-sm btn-primary" @click="openOrchestratorSpawn()" :disabled="orchestrators.filter(o => o.status === 'online').length === 0">
675
+ <i class="ti ti-plus me-1"></i>Spawn Agent
676
+ </button>
677
+ </div>
678
+ </div>
679
+
680
+ <template x-if="orchestrators.length > 0">
681
+ <div class="row g-3">
682
+ <template x-for="orch in orchestrators" :key="orch.id">
683
+ <div class="col-12 col-lg-6">
684
+ <div class="card">
685
+ <div class="card-body">
686
+ <div class="d-flex align-items-center mb-3">
687
+ <i class="ti ti-server-2 me-2" style="font-size: 24px"></i>
688
+ <div>
689
+ <h3 class="mb-0" x-text="orch.hostname"></h3>
690
+ <small class="text-secondary" x-text="orch.id"></small>
691
+ </div>
692
+ <span class="ms-auto badge" :class="orch.status === 'online' ? 'bg-success' : 'bg-secondary'" x-text="orch.status"></span>
693
+ </div>
694
+
695
+ <div class="d-flex gap-3 mb-3 text-secondary" style="font-size: 13px">
696
+ <span><i class="ti ti-folder me-1"></i><span x-text="orch.baseDir"></span></span>
697
+ <span><i class="ti ti-key me-1"></i><span x-text="(orch.envKeys?.length || 0) + ' env vars'"></span></span>
698
+ </div>
699
+
700
+ <div class="d-flex gap-1 mb-3">
701
+ <template x-for="provider in orch.providers" :key="provider">
702
+ <span class="badge" :class="provider === 'claude' ? 'bg-orange-lt' : 'bg-blue-lt'" x-text="provider"></span>
703
+ </template>
704
+ </div>
705
+
706
+ <template x-if="orch.managedAgents?.length > 0">
707
+ <div>
708
+ <h4 class="mb-2" style="font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--tblr-secondary)">
709
+ Managed Agents (<span x-text="orch.managedAgents.length"></span>)
710
+ </h4>
711
+ <div class="list-group list-group-flush">
712
+ <template x-for="agent in orch.managedAgents" :key="agent.tmuxSession">
713
+ <div class="list-group-item px-0 py-2 d-flex align-items-center gap-2">
714
+ <span class="badge" :class="agent.provider === 'claude' ? 'bg-orange-lt' : 'bg-blue-lt'" x-text="agent.provider" style="font-size: 10px"></span>
715
+ <span x-text="agent.label || agent.tmuxSession" style="font-size: 13px"></span>
716
+ <small class="text-secondary" x-text="agent.cwd" style="font-size: 11px"></small>
717
+ <div class="ms-auto d-flex gap-1">
718
+ <button class="btn btn-sm btn-ghost-warning p-1" title="Restart"
719
+ @click="orchestratorAction(orch.id, 'restart', agent.agentId || agent.tmuxSession)">
720
+ <i class="ti ti-refresh" style="font-size: 14px"></i>
721
+ </button>
722
+ <button class="btn btn-sm btn-ghost-danger p-1" title="Shutdown"
723
+ @click="orchestratorAction(orch.id, 'shutdown', agent.agentId || agent.tmuxSession)">
724
+ <i class="ti ti-power" style="font-size: 14px"></i>
725
+ </button>
726
+ </div>
727
+ </div>
728
+ </template>
729
+ </div>
730
+ </div>
731
+ </template>
732
+ <template x-if="!orch.managedAgents?.length">
733
+ <p class="text-secondary mb-0" style="font-size: 13px">No managed agents</p>
734
+ </template>
735
+
736
+ <div class="d-flex gap-1 mt-3">
737
+ <button class="btn btn-sm btn-primary" @click="openOrchestratorSpawnFor(orch.id)" :disabled="orch.status !== 'online'">
738
+ <i class="ti ti-plus me-1"></i>Spawn
739
+ </button>
740
+ <button class="btn btn-sm btn-ghost-danger" @click="deleteOrchestrator(orch.id)">
741
+ <i class="ti ti-trash me-1"></i>Remove
742
+ </button>
743
+ </div>
744
+ </div>
745
+ <div class="card-footer text-secondary" style="font-size: 12px">
746
+ Last seen: <span x-text="timeAgo(orch.lastSeen)"></span>
747
+ </div>
748
+ </div>
749
+ </div>
750
+ </template>
751
+ </div>
752
+ </template>
753
+
754
+ <template x-if="orchestrators.length === 0">
755
+ <div class="card">
756
+ <div class="card-body text-center py-5">
757
+ <i class="ti ti-server-2 mb-3" style="font-size: 48px; color: var(--tblr-secondary)"></i>
758
+ <h3>No orchestrators registered</h3>
759
+ <p class="text-secondary">Install and start <code>agent-relay-orchestrator</code> on each host to enable remote agent spawning.</p>
760
+ <pre class="text-start mx-auto" style="max-width: 500px; font-size: 12px">npm install -g agent-relay-orchestrator
761
+ agent-relay-orchestrator init
762
+ agent-relay-orchestrator</pre>
763
+ </div>
764
+ </div>
765
+ </template>
766
+ </div>
767
+
768
+ <!-- Orchestrator Spawn Modal -->
769
+ <div class="modal" :class="{ show: orchestratorSpawnOpen }" tabindex="-1" :style="orchestratorSpawnOpen ? 'display:block' : 'display:none'" @click.self="orchestratorSpawnOpen = false">
770
+ <div class="modal-dialog modal-dialog-centered">
771
+ <div class="modal-content">
772
+ <div class="modal-header">
773
+ <h5 class="modal-title">Spawn Agent</h5>
774
+ <button type="button" class="btn-close" @click="orchestratorSpawnOpen = false"></button>
775
+ </div>
776
+ <div class="modal-body">
777
+ <div class="mb-3">
778
+ <label class="form-label">Orchestrator</label>
779
+ <select class="form-select" x-model="spawnOrchId">
780
+ <template x-for="orch in orchestrators.filter(o => o.status === 'online')" :key="orch.id">
781
+ <option :value="orch.id" x-text="orch.hostname + ' (' + orch.providers.join(', ') + ')'"></option>
782
+ </template>
783
+ </select>
784
+ </div>
785
+ <div class="mb-3">
786
+ <label class="form-label">Provider</label>
787
+ <select class="form-select" x-model="spawnProvider">
788
+ <template x-for="p in spawnAvailableProviders" :key="p">
789
+ <option :value="p" x-text="p"></option>
790
+ </template>
791
+ </select>
792
+ </div>
793
+ <div class="mb-3">
794
+ <label class="form-label">Working Directory</label>
795
+ <div class="input-group">
796
+ <input type="text" class="form-control" x-model="spawnCwd" placeholder="Leave empty for base directory">
797
+ <button class="btn btn-outline-secondary" @click="browseOrchestratorDirs()" type="button">
798
+ <i class="ti ti-folder"></i>
799
+ </button>
800
+ </div>
801
+ <template x-if="spawnDirListing">
802
+ <div class="border rounded mt-2 p-2" style="max-height: 200px; overflow-y: auto; font-size: 13px">
803
+ <template x-if="spawnDirListing.parent">
804
+ <a href="#" class="d-block py-1 text-secondary" @click.prevent="spawnCwd = spawnDirListing.parent; browseOrchestratorDirs()">
805
+ <i class="ti ti-arrow-up me-1"></i>..
806
+ </a>
807
+ </template>
808
+ <template x-for="entry in spawnDirListing.entries" :key="entry.path">
809
+ <a href="#" class="d-block py-1" @click.prevent="spawnCwd = entry.path; browseOrchestratorDirs()">
810
+ <i class="ti ti-folder me-1"></i><span x-text="entry.name"></span>
811
+ </a>
812
+ </template>
813
+ </div>
814
+ </template>
815
+ </div>
816
+ <div class="mb-3">
817
+ <label class="form-label">Label</label>
818
+ <input type="text" class="form-control" x-model="spawnLabel" placeholder="e.g. backend, reviewer">
819
+ </div>
820
+ <div class="mb-3">
821
+ <label class="form-label">Approval Mode</label>
822
+ <select class="form-select" x-model="spawnApproval">
823
+ <option value="guarded">Guarded (block destructive ops)</option>
824
+ <option value="open">Open (no restrictions)</option>
825
+ <option value="read-only">Read-only (observe only)</option>
826
+ </select>
827
+ </div>
828
+ <template x-if="spawnProvider === 'claude'">
829
+ <div class="mb-3">
830
+ <label class="form-label">Initial Prompt</label>
831
+ <textarea class="form-control" x-model="spawnPrompt" rows="3" placeholder="You are a headless relay agent..."></textarea>
832
+ </div>
833
+ </template>
834
+ </div>
835
+ <div class="modal-footer">
836
+ <button class="btn btn-secondary" @click="orchestratorSpawnOpen = false">Cancel</button>
837
+ <button class="btn btn-primary" @click="submitOrchestratorSpawn()">
838
+ <i class="ti ti-rocket me-1"></i>Spawn
839
+ </button>
840
+ </div>
841
+ </div>
842
+ </div>
843
+ </div>
844
+ <div class="modal-backdrop fade show" x-show="orchestratorSpawnOpen" x-cloak></div>
845
+
651
846
  <!-- ==================== CHANNELS ==================== -->
652
847
  <div x-show="view === 'channels'" x-cloak class="fade-in">
653
848
  <div class="d-flex align-items-center mb-3 gap-2 flex-wrap">
@@ -663,10 +858,10 @@
663
858
  <div class="row g-3">
664
859
  <template x-for="channel in channelCards" :key="channel.id">
665
860
  <div class="col-md-6 col-xl-4">
666
- <div class="card">
861
+ <div class="card" style="cursor:pointer" @click="if (!window.getSelection().toString()) openChannelDetail(channel)">
667
862
  <div class="card-body">
668
863
  <div class="d-flex align-items-start gap-2">
669
- <span class="agent-type-icon agent mt-0">
864
+ <span class="agent-type-icon channel mt-0">
670
865
  <i class="ti" :class="channelPresence(channel).icon"></i>
671
866
  </span>
672
867
  <div class="flex-grow-1 min-width-0">
@@ -696,8 +891,8 @@
696
891
  <span x-text="'Last seen: ' + timeAgo(channel.lastSeen)"></span>
697
892
  </div>
698
893
  </div>
699
- <button class="btn btn-sm btn-ghost-secondary p-1" title="Show agent" @click="openAgentDetail(agentsById[channel.agentId])">
700
- <i class="ti ti-robot"></i>
894
+ <button class="btn btn-sm btn-ghost-secondary p-1" title="Show channel" @click.stop="openChannelDetail(channel)">
895
+ <i class="ti ti-messages"></i>
701
896
  </button>
702
897
  </div>
703
898
  </div>
@@ -844,7 +1039,7 @@
844
1039
  </select>
845
1040
  </div>
846
1041
  <div class="col-md-3">
847
- <select class="form-select form-select-sm" x-model="inboxCompose.to">
1042
+ <select class="form-select form-select-sm" x-model="inboxCompose.to" @change="if (!inboxComposeTargetAllowsClaimable()) inboxCompose.claimable = false">
848
1043
  <option value="">Target</option>
849
1044
  <template x-for="option in inboxComposeTargetOptions" :key="option.value">
850
1045
  <option :value="option.value" x-text="option.label"></option>
@@ -857,7 +1052,7 @@
857
1052
  <div class="col-md-2">
858
1053
  <input type="text" class="form-control form-control-sm" placeholder="Channel" x-model="inboxCompose.channel">
859
1054
  </div>
860
- <div class="col-md-2 d-flex gap-2 align-items-center">
1055
+ <div class="col-md-2 d-flex gap-2 align-items-center" x-show="inboxComposeTargetAllowsClaimable()">
861
1056
  <label class="form-check mb-0">
862
1057
  <input type="checkbox" class="form-check-input" x-model="inboxCompose.claimable">
863
1058
  <span class="form-check-label small">Claimable</span>
@@ -1471,16 +1666,16 @@
1471
1666
  <div class="col-sm-6 col-lg-3">
1472
1667
  <div class="card">
1473
1668
  <div class="card-body text-center">
1474
- <div class="text-secondary small">Total Agents</div>
1475
- <div class="h2 mb-0" x-text="stats.agents ?? 0"></div>
1669
+ <div class="text-secondary small">Agents</div>
1670
+ <div class="h2 mb-0" x-text="onlineCount"></div>
1476
1671
  </div>
1477
1672
  </div>
1478
1673
  </div>
1479
1674
  <div class="col-sm-6 col-lg-3">
1480
1675
  <div class="card">
1481
1676
  <div class="card-body text-center">
1482
- <div class="text-secondary small">Online Now</div>
1483
- <div class="h2 mb-0 text-success" x-text="onlineCount"></div>
1677
+ <div class="text-secondary small">Busy Agents</div>
1678
+ <div class="h2 mb-0 text-warning" x-text="busyAgentCount"></div>
1484
1679
  </div>
1485
1680
  </div>
1486
1681
  </div>
@@ -1488,7 +1683,7 @@
1488
1683
  <div class="card">
1489
1684
  <div class="card-body text-center">
1490
1685
  <div class="text-secondary small">Messages (24h)</div>
1491
- <div class="h2 mb-0 text-info" x-text="stats.messagesLast24h ?? 0"></div>
1686
+ <div class="h2 mb-0 text-info" x-text="stats.messagesLast24h ?? 0"></div>
1492
1687
  </div>
1493
1688
  </div>
1494
1689
  </div>
@@ -1612,6 +1807,16 @@
1612
1807
  <button class="btn btn-sm btn-ghost-secondary" @click="openRename(selectedAgentDetail)">
1613
1808
  <i class="ti ti-pencil me-1"></i>Rename
1614
1809
  </button>
1810
+ <template x-if="agentSupportsControlActions(selectedAgentDetail)">
1811
+ <button class="btn btn-sm btn-ghost-secondary" @click="openConfirm('Restart Agent', 'Restart agent ' + displayName(selectedAgentDetail) + '?', () => doAgentAction(selectedAgentDetail, 'restart'))">
1812
+ <i class="ti ti-refresh me-1"></i>Restart
1813
+ </button>
1814
+ </template>
1815
+ <template x-if="agentSupportsControlActions(selectedAgentDetail)">
1816
+ <button class="btn btn-sm btn-ghost-danger" @click="openConfirm('Shutdown Agent', 'Shut down agent ' + displayName(selectedAgentDetail) + '?', () => doAgentAction(selectedAgentDetail, 'shutdown'))">
1817
+ <i class="ti ti-power me-1"></i>Shutdown
1818
+ </button>
1819
+ </template>
1615
1820
  </div>
1616
1821
 
1617
1822
  <div class="p-3 border-bottom">
@@ -1766,6 +1971,141 @@
1766
1971
  </div>
1767
1972
  </template>
1768
1973
 
1974
+ <!-- ==================== CHANNEL DETAIL DRAWER ==================== -->
1975
+ <template x-if="selectedChannelDetail">
1976
+ <div>
1977
+ <div class="agent-drawer-backdrop" x-show="channelDetailOpen" x-cloak @click="closeChannelDetail()"></div>
1978
+ <aside class="agent-drawer" x-show="channelDetailOpen" x-cloak>
1979
+ <div class="p-3 border-bottom d-flex align-items-start gap-2">
1980
+ <span class="agent-type-icon channel mt-1" title="Channel">
1981
+ <i class="ti ti-messages"></i>
1982
+ </span>
1983
+ <div class="flex-grow-1 min-width-0">
1984
+ <div class="d-flex align-items-center gap-2">
1985
+ <span class="fw-bold text-truncate" x-text="selectedChannelDetail.name"></span>
1986
+ <span class="badge" :class="'bg-' + channelPresence(selectedChannelDetail).tone + '-lt'">
1987
+ <i class="ti me-1" :class="channelPresence(selectedChannelDetail).icon"></i><span x-text="channelPresence(selectedChannelDetail).label"></span>
1988
+ </span>
1989
+ </div>
1990
+ <div class="text-secondary small text-truncate" x-text="selectedChannelDetail.id"></div>
1991
+ </div>
1992
+ <button class="btn btn-sm btn-ghost-secondary p-1" @click="closeChannelDetail()" title="Close">
1993
+ <i class="ti ti-x"></i>
1994
+ </button>
1995
+ </div>
1996
+
1997
+ <div class="p-3 border-bottom d-flex gap-2 flex-wrap">
1998
+ <button class="btn btn-sm btn-primary" @click="compose = { ...compose, from: 'user', to: selectedChannelDetail.target || selectedChannelDetail.agentId, channel: selectedChannelDetail.id, body: '', claimable: false }; composeOpen = true">
1999
+ <i class="ti ti-send me-1"></i>Message
2000
+ </button>
2001
+ <button class="btn btn-sm btn-ghost-secondary" @click="channelFilter = selectedChannelDetail.id; switchView('messages'); closeChannelDetail()">
2002
+ <i class="ti ti-filter me-1"></i>Messages
2003
+ </button>
2004
+ <template x-if="agentsById[selectedChannelDetail.agentId]">
2005
+ <button class="btn btn-sm btn-ghost-secondary" @click="openAgentDetail(agentsById[selectedChannelDetail.agentId]); closeChannelDetail()">
2006
+ <i class="ti ti-robot me-1"></i>Backing agent
2007
+ </button>
2008
+ </template>
2009
+ </div>
2010
+
2011
+ <div class="p-3 border-bottom">
2012
+ <h3 class="card-title mb-3">Route</h3>
2013
+ <div class="detail-row mb-2">
2014
+ <div class="text-secondary small">Target</div>
2015
+ <div class="small text-break" x-text="displayTarget(selectedChannelDetail.target || selectedChannelDetail.agentId)"></div>
2016
+ </div>
2017
+ <div class="detail-row mb-2">
2018
+ <div class="text-secondary small">Backing agent</div>
2019
+ <div class="small text-break user-select-all" x-text="selectedChannelDetail.agentId"></div>
2020
+ </div>
2021
+ <div class="detail-row mb-2">
2022
+ <div class="text-secondary small">Transport</div>
2023
+ <div class="d-flex gap-1 flex-wrap">
2024
+ <span class="badge bg-info-lt" x-text="selectedChannelDetail.type"></span>
2025
+ <span class="badge bg-cyan-lt" x-text="selectedChannelDetail.transport || selectedChannelDetail.type"></span>
2026
+ <span class="badge bg-secondary-lt" x-text="selectedChannelDetail.direction"></span>
2027
+ </div>
2028
+ </div>
2029
+ <div class="detail-row mb-2">
2030
+ <div class="text-secondary small">Last seen</div>
2031
+ <div class="small" x-text="timeAgo(selectedChannelDetail.lastSeen)"></div>
2032
+ </div>
2033
+ </div>
2034
+
2035
+ <div class="p-3 border-bottom">
2036
+ <h3 class="card-title mb-3">Capabilities</h3>
2037
+ <div class="detail-row mb-2">
2038
+ <div class="text-secondary small">Capabilities</div>
2039
+ <div class="d-flex gap-1 flex-wrap">
2040
+ <template x-for="capability in (selectedChannelDetail.capabilities || [])" :key="capability">
2041
+ <span class="badge bg-purple-lt" x-text="capability"></span>
2042
+ </template>
2043
+ <template x-if="!(selectedChannelDetail.capabilities || []).length">
2044
+ <span class="text-secondary small">-</span>
2045
+ </template>
2046
+ </div>
2047
+ </div>
2048
+ <div class="detail-row mb-2">
2049
+ <div class="text-secondary small">Topics</div>
2050
+ <div class="d-flex gap-1 flex-wrap">
2051
+ <template x-for="topic in (selectedChannelDetail.topicChannels || [])" :key="topic">
2052
+ <span class="badge bg-warning-lt" x-text="'#' + topic"></span>
2053
+ </template>
2054
+ <template x-if="!(selectedChannelDetail.topicChannels || []).length">
2055
+ <span class="text-secondary small">-</span>
2056
+ </template>
2057
+ </div>
2058
+ </div>
2059
+ <div class="detail-row mb-2">
2060
+ <div class="text-secondary small">Tags</div>
2061
+ <div class="d-flex gap-1 flex-wrap">
2062
+ <template x-for="tag in (selectedChannelDetail.tags || [])" :key="tag">
2063
+ <span class="badge bg-cyan-lt" x-text="tag"></span>
2064
+ </template>
2065
+ <template x-if="!(selectedChannelDetail.tags || []).length">
2066
+ <span class="text-secondary small">-</span>
2067
+ </template>
2068
+ </div>
2069
+ </div>
2070
+ </div>
2071
+
2072
+ <div class="p-3 border-bottom">
2073
+ <h3 class="card-title mb-3">Metadata</h3>
2074
+ <template x-for="entry in Object.entries(selectedChannelDetail.meta || {})" :key="entry[0]">
2075
+ <div class="detail-row mb-2">
2076
+ <div class="text-secondary small" x-text="entry[0]"></div>
2077
+ <div class="small text-break" x-text="typeof entry[1] === 'object' ? JSON.stringify(entry[1]) : String(entry[1])"></div>
2078
+ </div>
2079
+ </template>
2080
+ <template x-if="!Object.keys(selectedChannelDetail.meta || {}).length">
2081
+ <div class="text-secondary small">No metadata</div>
2082
+ </template>
2083
+ </div>
2084
+
2085
+ <div class="p-3">
2086
+ <h3 class="card-title mb-3">Recent Messages</h3>
2087
+ <div class="list-group list-group-flush">
2088
+ <template x-for="m in channelDetailMessages" :key="m.id">
2089
+ <div class="list-group-item px-0">
2090
+ <div class="d-flex align-items-center gap-2 mb-1">
2091
+ <span class="fw-bold small" x-text="displayTarget(m.from)"></span>
2092
+ <i class="ti ti-arrow-right text-secondary" style="font-size:12px"></i>
2093
+ <span class="small" x-text="displayTarget(m.to)"></span>
2094
+ <span class="badge bg-warning-lt" x-show="m.channel" x-text="'#' + m.channel"></span>
2095
+ <span class="text-secondary small ms-auto" x-text="'#' + m.id"></span>
2096
+ </div>
2097
+ <div class="text-secondary small text-truncate" x-text="messagePreview(m)"></div>
2098
+ </div>
2099
+ </template>
2100
+ <template x-if="channelDetailMessages.length === 0">
2101
+ <div class="text-secondary small">No recent channel messages loaded</div>
2102
+ </template>
2103
+ </div>
2104
+ </div>
2105
+ </aside>
2106
+ </div>
2107
+ </template>
2108
+
1769
2109
  <!-- ==================== COMPOSE MODAL ==================== -->
1770
2110
  <div class="modal modal-blur" :class="{ show: composeOpen }" :style="composeOpen ? 'display:block' : 'display:none'" tabindex="-1" @click.self="composeOpen = false">
1771
2111
  <div class="modal-dialog modal-lg modal-dialog-centered">
@@ -1787,14 +2127,15 @@
1787
2127
  <label class="form-label">From</label>
1788
2128
  <select class="form-select" x-model="compose.from">
1789
2129
  <option value="">Select sender…</option>
1790
- <template x-for="a in composeAgents" :key="a.id">
2130
+ <option value="user">User [user]</option>
2131
+ <template x-for="a in composeAgents.filter((agent) => agent.id !== 'user')" :key="a.id">
1791
2132
  <option :value="a.id" x-text="displayName(a) + ' [' + a.id.slice(-6) + ']'"></option>
1792
2133
  </template>
1793
2134
  </select>
1794
2135
  </div>
1795
2136
  <div class="col-md-6">
1796
2137
  <label class="form-label">To</label>
1797
- <select class="form-select" x-model="compose.to">
2138
+ <select class="form-select" x-model="compose.to" @change="if (!composeTargetAllowsClaimable()) compose.claimable = false">
1798
2139
  <option value="">Select recipient…</option>
1799
2140
  <option value="broadcast">📢 Broadcast (all)</option>
1800
2141
  <optgroup label="Labels">
@@ -1831,7 +2172,7 @@
1831
2172
  <label class="form-label">Message</label>
1832
2173
  <textarea class="form-control" rows="5" x-model="compose.body" placeholder="Type your message…" x-ref="composeBody"></textarea>
1833
2174
  </div>
1834
- <div class="col-12">
2175
+ <div class="col-12" x-show="composeTargetAllowsClaimable()">
1835
2176
  <label class="form-check">
1836
2177
  <input type="checkbox" class="form-check-input" x-model="compose.claimable">
1837
2178
  <span class="form-check-label">Claimable task (only one agent can claim)</span>
@@ -1850,6 +2191,88 @@
1850
2191
  </div>
1851
2192
  <div class="modal-backdrop fade show" x-show="composeOpen" x-cloak></div>
1852
2193
 
2194
+ <!-- ==================== AGENT SPAWN MODAL ==================== -->
2195
+ <div class="modal modal-blur" :class="{ show: agentSpawnOpen }" :style="agentSpawnOpen ? 'display:block' : 'display:none'" tabindex="-1" @click.self="closeAgentSpawn()">
2196
+ <div class="modal-dialog modal-md modal-dialog-centered">
2197
+ <div class="modal-content">
2198
+ <div class="modal-header">
2199
+ <h5 class="modal-title">Create Agent</h5>
2200
+ <button class="btn-close" @click="closeAgentSpawn()"></button>
2201
+ </div>
2202
+ <div class="modal-body">
2203
+ <div class="row g-3">
2204
+ <div class="col-12">
2205
+ <label class="form-label">Provider</label>
2206
+ <select class="form-select" x-model="agentSpawn.provider">
2207
+ <option value="codex">Codex Live</option>
2208
+ </select>
2209
+ </div>
2210
+ <div class="col-12">
2211
+ <label class="form-label">Permission model</label>
2212
+ <select class="form-select" x-model="agentSpawn.approvalMode">
2213
+ <option value="guarded">Guarded</option>
2214
+ <option value="read-only">Read-only</option>
2215
+ <option value="open">Open</option>
2216
+ </select>
2217
+ </div>
2218
+ <div class="col-12">
2219
+ <label class="form-label">Working directory <span class="text-secondary">(optional)</span></label>
2220
+ <div class="input-group">
2221
+ <input type="text" class="form-control" x-model="agentSpawn.cwd" x-ref="agentSpawnCwd" placeholder="Defaults to the relay server working directory">
2222
+ <button class="btn btn-ghost-secondary" @click="openAgentDirectoryBrowser()" title="Browse host directories">
2223
+ <i class="ti ti-folder-open me-1"></i>Browse
2224
+ </button>
2225
+ </div>
2226
+ <div class="border rounded mt-2" x-show="agentDirectoryBrowser.open" x-cloak>
2227
+ <div class="d-flex align-items-center gap-2 p-2 border-bottom">
2228
+ <button class="btn btn-sm btn-ghost-secondary" :disabled="!agentDirectoryBrowser.parent || agentDirectoryBrowser.loading" @click="browseAgentDirectory(agentDirectoryBrowser.parent)" title="Parent directory">
2229
+ <i class="ti ti-arrow-up"></i>
2230
+ </button>
2231
+ <button class="btn btn-sm btn-ghost-secondary" :disabled="!agentDirectoryBrowser.home || agentDirectoryBrowser.loading" @click="browseAgentDirectory(agentDirectoryBrowser.home)" title="Home directory">
2232
+ <i class="ti ti-home"></i>
2233
+ </button>
2234
+ <button class="btn btn-sm btn-ghost-secondary" :disabled="!agentDirectoryBrowser.cwd || agentDirectoryBrowser.loading" @click="browseAgentDirectory(agentDirectoryBrowser.cwd)" title="Server directory">
2235
+ <i class="ti ti-server"></i>
2236
+ </button>
2237
+ <div class="small text-secondary text-truncate flex-grow-1" x-text="agentDirectoryBrowser.path || 'Loading...'"></div>
2238
+ <button class="btn btn-sm btn-primary" :disabled="!agentDirectoryBrowser.path" @click="selectAgentDirectory(agentDirectoryBrowser.path)">Select</button>
2239
+ </div>
2240
+ <div class="p-2" style="max-height: 240px; overflow:auto">
2241
+ <template x-if="agentDirectoryBrowser.loading">
2242
+ <div class="text-secondary small py-3 text-center">Loading directories...</div>
2243
+ </template>
2244
+ <template x-if="agentDirectoryBrowser.error">
2245
+ <div class="alert alert-danger py-2 mb-2" x-text="agentDirectoryBrowser.error"></div>
2246
+ </template>
2247
+ <template x-for="entry in agentDirectoryBrowser.entries" :key="entry.path">
2248
+ <button type="button" class="list-group-item list-group-item-action d-flex align-items-center gap-2 w-100 text-start border-0 rounded" @click="browseAgentDirectory(entry.path)">
2249
+ <i class="ti ti-folder text-warning"></i>
2250
+ <span class="text-truncate" x-text="entry.name"></span>
2251
+ </button>
2252
+ </template>
2253
+ <template x-if="!agentDirectoryBrowser.loading && !agentDirectoryBrowser.error && agentDirectoryBrowser.entries.length === 0">
2254
+ <div class="text-secondary small py-3 text-center">No child directories</div>
2255
+ </template>
2256
+ </div>
2257
+ </div>
2258
+ </div>
2259
+ <div class="col-12">
2260
+ <label class="form-label">Label <span class="text-secondary">(optional)</span></label>
2261
+ <input type="text" class="form-control" x-model="agentSpawn.label" placeholder="e.g. backend helper">
2262
+ </div>
2263
+ </div>
2264
+ </div>
2265
+ <div class="modal-footer">
2266
+ <button class="btn btn-ghost-secondary" @click="closeAgentSpawn()">Cancel</button>
2267
+ <button class="btn btn-primary" @click="doSpawnAgent()">
2268
+ <i class="ti ti-plus me-1"></i>Create
2269
+ </button>
2270
+ </div>
2271
+ </div>
2272
+ </div>
2273
+ </div>
2274
+ <div class="modal-backdrop fade show" x-show="agentSpawnOpen" x-cloak></div>
2275
+
1853
2276
  <!-- ==================== PAIR INVITE MODAL ==================== -->
1854
2277
  <div class="modal modal-blur" :class="{ show: pairInviteOpen }" :style="pairInviteOpen ? 'display:block' : 'display:none'" tabindex="-1" @click.self="closePairInvite()">
1855
2278
  <div class="modal-dialog modal-lg modal-dialog-centered">