agent-relay-server 0.4.21 → 0.4.23

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
@@ -48,12 +48,77 @@
48
48
  .agent-card:hover .agent-actions { opacity: 1; }
49
49
 
50
50
  .agent-label { color: var(--tblr-warning); font-weight: 600; }
51
+ .agent-type-icon {
52
+ width: 20px;
53
+ height: 20px;
54
+ border-radius: 6px;
55
+ display: inline-flex;
56
+ align-items: center;
57
+ justify-content: center;
58
+ flex: 0 0 20px;
59
+ border: 1px solid transparent;
60
+ font-size: 14px;
61
+ line-height: 1;
62
+ }
63
+ .agent-type-icon.codex {
64
+ color: var(--tblr-info);
65
+ background: rgba(var(--tblr-info-rgb), 0.12);
66
+ border-color: rgba(var(--tblr-info-rgb), 0.25);
67
+ }
68
+ .agent-type-icon.claude {
69
+ color: var(--tblr-warning);
70
+ background: rgba(var(--tblr-warning-rgb), 0.12);
71
+ border-color: rgba(var(--tblr-warning-rgb), 0.25);
72
+ }
73
+ .agent-type-icon.agent {
74
+ color: var(--tblr-secondary);
75
+ background: var(--tblr-bg-surface-secondary);
76
+ border-color: var(--tblr-border-color);
77
+ }
51
78
 
52
79
  .msg-card { border-left: 3px solid transparent; }
53
80
  .msg-card.claimed { border-left-color: var(--tblr-success); }
54
81
  .msg-card.thread-child { margin-left: 24px; border-left-color: var(--tblr-border-color); }
55
82
 
56
83
  .msg-body { white-space: pre-wrap; word-break: break-word; font-size: 13px; }
84
+ .inbox-fit { height: calc(100vh - 280px); min-height: 380px; }
85
+ .inbox-thread { cursor: pointer; min-height: 72px; }
86
+ .inbox-thread.active { background: var(--tblr-bg-surface-secondary); border-left: 3px solid var(--tblr-primary); }
87
+ .inbox-thread.attention { border-left: 3px solid var(--tblr-warning); }
88
+ .inbox-thread-snippet { max-width: 100%; }
89
+ .attention-card { border-left: 3px solid var(--tblr-warning); color: inherit; }
90
+ button.attention-card { background: var(--tblr-bg-surface); border-top: 1px solid var(--tblr-border-color); border-right: 1px solid var(--tblr-border-color); border-bottom: 1px solid var(--tblr-border-color); }
91
+ button.attention-card:hover { background: var(--tblr-bg-surface-secondary); }
92
+ .attention-empty { border-left-color: var(--tblr-border-color); }
93
+ .attention-badges .badge { font-weight: 500; }
94
+ .pair-badge .ti { font-size: 13px; vertical-align: -2px; }
95
+ .pair-card { border-left: 3px solid var(--tblr-border-color); }
96
+ .pair-card.active { border-left-color: var(--tblr-success); }
97
+ .pair-card.pending { border-left-color: var(--tblr-warning); }
98
+ .agent-drawer-backdrop {
99
+ position: fixed;
100
+ inset: 0;
101
+ background: rgba(0, 0, 0, 0.45);
102
+ z-index: 1030;
103
+ }
104
+ .agent-drawer {
105
+ position: fixed;
106
+ top: 0;
107
+ right: 0;
108
+ width: min(480px, 100vw);
109
+ height: 100vh;
110
+ overflow-y: auto;
111
+ background: var(--tblr-bg-surface);
112
+ border-left: 1px solid var(--tblr-border-color);
113
+ z-index: 1040;
114
+ box-shadow: -16px 0 48px rgba(0, 0, 0, 0.35);
115
+ }
116
+ .detail-row {
117
+ display: grid;
118
+ grid-template-columns: 96px minmax(0, 1fr);
119
+ gap: 12px;
120
+ align-items: start;
121
+ }
57
122
 
58
123
  .fade-in { animation: fadeIn 0.2s ease-in; }
59
124
  @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@@ -83,17 +148,29 @@
83
148
  <nav class="nav flex-column py-2">
84
149
  <a href="#" class="nav-link" :class="{ active: view === 'overview' }" @click.prevent="switchView('overview')">
85
150
  <i class="ti ti-dashboard"></i>Overview
151
+ <span class="badge bg-warning text-white ms-auto" x-show="attentionSummary.total > 0" x-text="attentionSummary.total"></span>
86
152
  </a>
87
153
  <a href="#" class="nav-link" :class="{ active: view === 'agents' }" @click.prevent="switchView('agents')">
88
154
  <i class="ti ti-robot"></i>Agents
89
- <span class="badge bg-success text-white ms-auto" x-text="onlineCount"></span>
155
+ <span class="badge bg-warning text-white ms-auto" x-show="attentionAgentCount > 0" x-text="attentionAgentCount"></span>
156
+ <span class="badge bg-success text-white ms-1" x-text="onlineCount"></span>
157
+ </a>
158
+ <a href="#" class="nav-link" :class="{ active: view === 'inbox' }" @click.prevent="switchView('inbox')">
159
+ <i class="ti ti-inbox"></i>Inbox
160
+ <span class="badge bg-danger text-white ms-auto" x-show="attentionSummary.unreadInbox > 0" x-text="attentionSummary.unreadInbox"></span>
161
+ </a>
162
+ <a href="#" class="nav-link" :class="{ active: view === 'pairs' }" @click.prevent="switchView('pairs')">
163
+ <i class="ti ti-link"></i>Pairs
164
+ <span class="badge bg-warning text-white ms-auto" x-show="attentionSummary.pendingPairInvites > 0" x-text="attentionSummary.pendingPairInvites"></span>
165
+ <span class="badge bg-primary text-white ms-1" x-text="pairs.length"></span>
90
166
  </a>
91
167
  <a href="#" class="nav-link" :class="{ active: view === 'messages' }" @click.prevent="switchView('messages')">
92
168
  <i class="ti ti-messages"></i>Messages
93
169
  </a>
94
170
  <a href="#" class="nav-link" :class="{ active: view === 'tasks' }" @click.prevent="switchView('tasks')">
95
171
  <i class="ti ti-checkup-list"></i>Tasks
96
- <span class="badge bg-warning text-white ms-auto" x-text="stats.openTasks ?? 0"></span>
172
+ <span class="badge bg-warning text-white ms-auto" x-show="attentionSummary.claimableTasks > 0" x-text="attentionSummary.claimableTasks"></span>
173
+ <span class="badge bg-secondary text-white ms-1" x-text="stats.openTasks ?? 0"></span>
97
174
  </a>
98
175
  <a href="#" class="nav-link" :class="{ active: view === 'analytics' }" @click.prevent="switchView('analytics')">
99
176
  <i class="ti ti-chart-area-line"></i>Analytics
@@ -122,7 +199,7 @@
122
199
 
123
200
  <!-- Mobile nav -->
124
201
  <div class="mobile-nav d-none border-bottom p-2 gap-1 position-fixed top-0 w-100 bg-dark" style="z-index:50">
125
- <template x-for="v in ['overview','agents','messages','tasks','analytics']">
202
+ <template x-for="v in ['overview','agents','inbox','pairs','messages','tasks','analytics']">
126
203
  <button class="btn btn-sm" :class="view === v ? 'btn-primary' : 'btn-ghost-secondary'" @click="switchView(v)" x-text="v.charAt(0).toUpperCase() + v.slice(1)"></button>
127
204
  </template>
128
205
  </div>
@@ -226,6 +303,49 @@
226
303
  </div>
227
304
  </template>
228
305
 
306
+ <div class="row g-3 mb-4">
307
+ <div class="col-md-6 col-xl">
308
+ <button class="card attention-card w-100 text-start" :class="{ 'attention-empty': attentionSummary.unreadInbox === 0 }" @click="switchView('inbox')">
309
+ <div class="card-body py-3">
310
+ <div class="text-secondary small">Unread</div>
311
+ <div class="h2 mb-0" :class="attentionSummary.unreadInbox ? 'text-danger' : ''" x-text="attentionSummary.unreadInbox"></div>
312
+ </div>
313
+ </button>
314
+ </div>
315
+ <div class="col-md-6 col-xl">
316
+ <button class="card attention-card w-100 text-start" :class="{ 'attention-empty': attentionSummary.needsHumanResponse === 0 }" @click="switchView('inbox')">
317
+ <div class="card-body py-3">
318
+ <div class="text-secondary small">Needs response</div>
319
+ <div class="h2 mb-0" :class="attentionSummary.needsHumanResponse ? 'text-warning' : ''" x-text="attentionSummary.needsHumanResponse"></div>
320
+ </div>
321
+ </button>
322
+ </div>
323
+ <div class="col-md-6 col-xl">
324
+ <button class="card attention-card w-100 text-start" :class="{ 'attention-empty': attentionSummary.agentQuestions === 0 }" @click="switchView('inbox')">
325
+ <div class="card-body py-3">
326
+ <div class="text-secondary small">Agent questions</div>
327
+ <div class="h2 mb-0" :class="attentionSummary.agentQuestions ? 'text-info' : ''" x-text="attentionSummary.agentQuestions"></div>
328
+ </div>
329
+ </button>
330
+ </div>
331
+ <div class="col-md-6 col-xl">
332
+ <button class="card attention-card w-100 text-start" :class="{ 'attention-empty': attentionSummary.pendingPairInvites === 0 }" @click="switchView('pairs')">
333
+ <div class="card-body py-3">
334
+ <div class="text-secondary small">Pair invites</div>
335
+ <div class="h2 mb-0" :class="attentionSummary.pendingPairInvites ? 'text-warning' : ''" x-text="attentionSummary.pendingPairInvites"></div>
336
+ </div>
337
+ </button>
338
+ </div>
339
+ <div class="col-md-6 col-xl">
340
+ <button class="card attention-card w-100 text-start" :class="{ 'attention-empty': attentionSummary.claimableTasks === 0 }" @click="switchView('tasks')">
341
+ <div class="card-body py-3">
342
+ <div class="text-secondary small">Claimable waiting</div>
343
+ <div class="h2 mb-0" :class="attentionSummary.claimableTasks ? 'text-warning' : ''" x-text="attentionSummary.claimableTasks"></div>
344
+ </div>
345
+ </button>
346
+ </div>
347
+ </div>
348
+
229
349
  <!-- Two-column: Agents + Recent messages -->
230
350
  <div class="row g-3">
231
351
  <div class="col-lg-5">
@@ -236,8 +356,11 @@
236
356
  </div>
237
357
  <div class="list-group list-group-flush" style="max-height: 60vh; overflow-y: auto">
238
358
  <template x-for="a in sortedAgents.slice(0, 20)" :key="a.id">
239
- <div class="list-group-item d-flex align-items-center gap-2" style="cursor:pointer" @click="selectedAgent = a.id; switchView('messages')">
359
+ <div class="list-group-item d-flex align-items-center gap-2" style="cursor:pointer" @click="openAgentDetail(a)">
240
360
  <span class="status-dot" :class="[a.status, a.status !== 'offline' && !a.ready ? 'not-ready' : '']"></span>
361
+ <span class="agent-type-icon" :class="agentType(a)" :title="agentTypeTitle(a)" :aria-label="agentTypeTitle(a)">
362
+ <i class="ti" :class="agentTypeIcon(a)"></i>
363
+ </span>
241
364
  <div class="flex-grow-1 min-width-0">
242
365
  <div class="text-truncate">
243
366
  <template x-if="a.label">
@@ -248,6 +371,18 @@
248
371
  </template>
249
372
  </div>
250
373
  <div class="text-secondary small text-truncate" x-text="a.id"></div>
374
+ <template x-if="agentPair(a)">
375
+ <span class="badge pair-badge mt-1" :class="pairBadgeClass(agentPair(a))" :title="pairTitle(agentPair(a), a.id)">
376
+ <i class="ti ti-link me-1"></i><span x-text="pairBadgeLabel(agentPair(a), a.id)"></span>
377
+ </span>
378
+ </template>
379
+ <div class="attention-badges d-flex gap-1 mt-1 flex-wrap" x-show="agentAttention(a).total > 0" :title="agentAttentionTitle(a)">
380
+ <span class="badge bg-danger-lt" x-show="agentAttention(a).unread" x-text="agentAttention(a).unread + ' unread'"></span>
381
+ <span class="badge bg-warning-lt" x-show="agentAttention(a).needsHumanResponse">needs response</span>
382
+ <span class="badge bg-info-lt" x-show="agentAttention(a).agentQuestion">question</span>
383
+ <span class="badge bg-warning-lt" x-show="agentAttention(a).pendingPairInvite">pair invite</span>
384
+ <span class="badge bg-orange-lt" x-show="agentAttention(a).claimableTasks" x-text="agentAttention(a).claimableTasks + ' claimable'"></span>
385
+ </div>
251
386
  </div>
252
387
  <span class="text-secondary small" x-text="timeAgo(a.lastSeen)"></span>
253
388
  </div>
@@ -335,10 +470,13 @@
335
470
  <div class="row g-3">
336
471
  <template x-for="a in sortedAgents" :key="a.id">
337
472
  <div class="col-md-6 col-xl-4">
338
- <div class="card agent-card" :class="{ selected: selectedAgent === a.id }">
473
+ <div class="card agent-card" :class="{ selected: selectedAgent === a.id }" @click="openAgentDetail(a)">
339
474
  <div class="card-body">
340
475
  <div class="d-flex align-items-start gap-2">
341
476
  <span class="status-dot mt-1" :class="[a.status, a.status !== 'offline' && !a.ready ? 'not-ready' : '']" :title="agentStatusTitle(a)"></span>
477
+ <span class="agent-type-icon mt-0" :class="agentType(a)" :title="agentTypeTitle(a)" :aria-label="agentTypeTitle(a)">
478
+ <i class="ti" :class="agentTypeIcon(a)"></i>
479
+ </span>
342
480
  <div class="flex-grow-1 min-width-0">
343
481
  <div class="d-flex align-items-center gap-2">
344
482
  <template x-if="a.label">
@@ -348,6 +486,20 @@
348
486
  </div>
349
487
  <div class="text-secondary small text-truncate mt-1" x-text="a.id"></div>
350
488
  <div class="d-flex gap-1 mt-1 flex-wrap">
489
+ <template x-if="agentAttention(a).total > 0">
490
+ <span class="badge bg-warning text-white" :title="agentAttentionTitle(a)">
491
+ <i class="ti ti-bell me-1"></i><span x-text="agentAttention(a).total"></span>
492
+ </span>
493
+ </template>
494
+ <template x-if="agentPair(a)">
495
+ <span class="badge pair-badge" :class="pairBadgeClass(agentPair(a))" :title="pairTitle(agentPair(a), a.id)">
496
+ <i class="ti ti-link me-1"></i><span x-text="pairBadgeLabel(agentPair(a), a.id)"></span>
497
+ </span>
498
+ </template>
499
+ <span class="badge bg-danger-lt" x-show="agentAttention(a).unread" x-text="agentAttention(a).unread + ' unread'"></span>
500
+ <span class="badge bg-warning-lt" x-show="agentAttention(a).needsHumanResponse">needs response</span>
501
+ <span class="badge bg-info-lt" x-show="agentAttention(a).agentQuestion">question</span>
502
+ <span class="badge bg-orange-lt" x-show="agentAttention(a).claimableTasks" x-text="agentAttention(a).claimableTasks + ' claimable'"></span>
351
503
  <template x-if="a.machine">
352
504
  <span class="badge bg-secondary-lt" x-text="a.machine"></span>
353
505
  </template>
@@ -406,6 +558,296 @@
406
558
  </template>
407
559
  </div>
408
560
 
561
+ <!-- ==================== INBOX ==================== -->
562
+ <div x-show="view === 'inbox'" x-cloak class="fade-in">
563
+ <div class="d-flex align-items-center mb-3 gap-2 flex-wrap">
564
+ <h2 class="page-title mb-0">Inbox</h2>
565
+ <span class="badge bg-danger-lt" x-show="attentionSummary.unreadInbox > 0" x-text="attentionSummary.unreadInbox + ' unread'"></span>
566
+ <span class="badge bg-warning-lt" x-show="attentionSummary.needsHumanResponse > 0" x-text="attentionSummary.needsHumanResponse + ' need response'"></span>
567
+ <span class="badge bg-info-lt" x-show="attentionSummary.agentQuestions > 0" x-text="attentionSummary.agentQuestions + ' questions'"></span>
568
+ <div class="ms-auto d-flex gap-2 align-items-center flex-wrap">
569
+ <input type="search" class="form-control form-control-sm" style="width: 220px" placeholder="Search inbox" x-model.debounce.200ms="inboxSearch">
570
+ <label class="form-check form-switch mb-0">
571
+ <input type="checkbox" class="form-check-input" x-model="inboxShowArchived">
572
+ <span class="form-check-label small">Archived</span>
573
+ </label>
574
+ <button class="btn btn-sm btn-ghost-secondary" @click="fetchMessages()">
575
+ <i class="ti ti-refresh"></i>
576
+ </button>
577
+ </div>
578
+ </div>
579
+
580
+ <div class="card mb-3">
581
+ <div class="card-body">
582
+ <div class="row g-2 align-items-start">
583
+ <div class="col-md-2">
584
+ <select class="form-select form-select-sm" x-model="inboxCompose.toMode" @change="resetInboxComposeTarget()">
585
+ <option value="agent">Agent</option>
586
+ <option value="tag">Tag</option>
587
+ <option value="cap">Capability</option>
588
+ </select>
589
+ </div>
590
+ <div class="col-md-3">
591
+ <select class="form-select form-select-sm" x-model="inboxCompose.to">
592
+ <option value="">Target</option>
593
+ <template x-for="option in inboxComposeTargetOptions" :key="option.value">
594
+ <option :value="option.value" x-text="option.label"></option>
595
+ </template>
596
+ </select>
597
+ </div>
598
+ <div class="col-md-3">
599
+ <input type="text" class="form-control form-control-sm" placeholder="Subject" x-model="inboxCompose.subject">
600
+ </div>
601
+ <div class="col-md-2">
602
+ <input type="text" class="form-control form-control-sm" placeholder="Channel" x-model="inboxCompose.channel">
603
+ </div>
604
+ <div class="col-md-2 d-flex gap-2 align-items-center">
605
+ <label class="form-check mb-0">
606
+ <input type="checkbox" class="form-check-input" x-model="inboxCompose.claimable">
607
+ <span class="form-check-label small">Claimable</span>
608
+ </label>
609
+ </div>
610
+ <div class="col-12">
611
+ <div class="d-flex gap-2">
612
+ <textarea class="form-control" rows="2" placeholder="Message" x-model="inboxCompose.body" @keydown.ctrl.enter.prevent="doSendInboxCompose()" @keydown.meta.enter.prevent="doSendInboxCompose()"></textarea>
613
+ <button class="btn btn-primary align-self-stretch" @click="doSendInboxCompose()" :disabled="!inboxCompose.to || !inboxCompose.body">
614
+ <i class="ti ti-send"></i>
615
+ </button>
616
+ </div>
617
+ </div>
618
+ </div>
619
+ </div>
620
+ </div>
621
+
622
+ <div class="row g-3 inbox-fit">
623
+ <div class="col-lg-4 h-100">
624
+ <div class="card h-100">
625
+ <div class="list-group list-group-flush h-100 overflow-auto">
626
+ <template x-for="thread in inboxThreads" :key="thread.id">
627
+ <div
628
+ class="list-group-item list-group-item-action inbox-thread"
629
+ :class="{ active: selectedInboxThreadData?.id === thread.id, attention: thread.attention?.score > 0, 'text-secondary': thread.archived }"
630
+ @click="openInboxThread(thread)"
631
+ >
632
+ <div class="d-flex align-items-center gap-2">
633
+ <span class="agent-type-icon" :class="agentType(agentsById[thread.peer])" :title="agentTypeTitle(agentsById[thread.peer])">
634
+ <i class="ti" :class="agentTypeIcon(agentsById[thread.peer])"></i>
635
+ </span>
636
+ <span class="fw-bold text-truncate" x-text="conversationTitle(thread)"></span>
637
+ <span class="badge bg-danger text-white" x-show="thread.attention?.unread" x-text="thread.attention.unread"></span>
638
+ <span class="text-secondary small ms-auto" x-text="timeAgo(thread.lastMessage?.createdAt)"></span>
639
+ </div>
640
+ <div class="text-secondary small text-truncate mt-1 inbox-thread-snippet" x-text="messagePreview(thread.lastMessage)"></div>
641
+ <div class="attention-badges d-flex gap-1 mt-1 flex-wrap" x-show="thread.attention?.score > 0">
642
+ <span class="badge bg-warning-lt" x-show="thread.attention?.needsHumanResponse">needs response</span>
643
+ <span class="badge bg-info-lt" x-show="thread.attention?.agentQuestion">agent asked a question</span>
644
+ </div>
645
+ <div class="d-flex align-items-center gap-1 mt-2">
646
+ <span class="badge bg-secondary-lt" x-show="thread.archived">archived</span>
647
+ <span class="badge bg-primary-lt" x-show="thread.draft">draft</span>
648
+ <button class="btn btn-sm btn-ghost-secondary py-0 px-1 ms-auto" title="Mark unread" @click.stop="markInboxThreadUnread(thread)">
649
+ <i class="ti ti-mail"></i>
650
+ </button>
651
+ <button class="btn btn-sm btn-ghost-secondary py-0 px-1" :title="thread.archived ? 'Unarchive' : 'Archive'" @click.stop="thread.archived ? unarchiveInboxThread(thread) : archiveInboxThread(thread)">
652
+ <i class="ti" :class="thread.archived ? 'ti-archive-off' : 'ti-archive'"></i>
653
+ </button>
654
+ <button class="btn btn-sm btn-ghost-danger py-0 px-1" title="Delete thread" @click.stop="confirmDeleteInboxThread(thread)">
655
+ <i class="ti ti-trash"></i>
656
+ </button>
657
+ </div>
658
+ </div>
659
+ </template>
660
+ <template x-if="inboxThreads.length === 0">
661
+ <div class="list-group-item text-secondary text-center py-5">
662
+ <i class="ti ti-inbox-off" style="font-size:48px; opacity:0.3"></i>
663
+ <p class="mt-2 mb-0" x-text="inboxSearch ? 'No matching threads' : 'No correspondence'"></p>
664
+ </div>
665
+ </template>
666
+ </div>
667
+ </div>
668
+ </div>
669
+
670
+ <div class="col-lg-8 h-100">
671
+ <div class="card h-100">
672
+ <template x-if="selectedInboxThreadData">
673
+ <div class="card-header d-flex align-items-center gap-2">
674
+ <h3 class="card-title mb-0 text-truncate" x-text="conversationTitle(selectedInboxThreadData)"></h3>
675
+ <div class="attention-badges d-flex gap-1 flex-wrap">
676
+ <span class="badge bg-danger-lt" x-show="selectedInboxThreadData.attention?.unread" x-text="selectedInboxThreadData.attention.unread + ' unread'"></span>
677
+ <span class="badge bg-warning-lt" x-show="selectedInboxThreadData.attention?.needsHumanResponse">needs response</span>
678
+ <span class="badge bg-info-lt" x-show="selectedInboxThreadData.attention?.agentQuestion">agent asked a question</span>
679
+ <span class="badge bg-secondary-lt" x-show="selectedInboxThreadData.archived">archived</span>
680
+ </div>
681
+ <button class="btn btn-sm btn-ghost-secondary ms-auto" title="Mark unread" @click="markInboxThreadUnread(selectedInboxThreadData)">
682
+ <i class="ti ti-mail"></i>
683
+ </button>
684
+ <button class="btn btn-sm btn-ghost-secondary" :title="selectedInboxThreadData.archived ? 'Unarchive' : 'Archive'" @click="selectedInboxThreadData.archived ? unarchiveInboxThread(selectedInboxThreadData) : archiveInboxThread(selectedInboxThreadData)">
685
+ <i class="ti" :class="selectedInboxThreadData.archived ? 'ti-archive-off' : 'ti-archive'"></i>
686
+ </button>
687
+ <button class="btn btn-sm btn-ghost-danger" title="Delete thread" @click="confirmDeleteInboxThread(selectedInboxThreadData)">
688
+ <i class="ti ti-trash"></i>
689
+ </button>
690
+ </div>
691
+ </template>
692
+ <div class="card-body p-0 overflow-auto">
693
+ <template x-if="!selectedInboxThreadData">
694
+ <div class="p-4 text-center text-secondary">
695
+ <i class="ti ti-inbox" style="font-size:48px; opacity:0.3"></i>
696
+ <p class="mt-2">Select a conversation</p>
697
+ </div>
698
+ </template>
699
+ <template x-for="m in selectedInboxMessages" :key="m.id">
700
+ <div class="msg-card p-3 border-bottom" :class="{ claimed: m.claimedBy }">
701
+ <div class="d-flex align-items-center gap-2 mb-1">
702
+ <span class="fw-bold small" x-text="displayTarget(m.from)"></span>
703
+ <i class="ti ti-arrow-right text-secondary" style="font-size:12px"></i>
704
+ <span class="small" x-text="displayTarget(m.to)"></span>
705
+ <span class="text-secondary small ms-auto" x-text="'#' + m.id + ' · ' + fmtTime(m.createdAt)"></span>
706
+ </div>
707
+ <template x-if="m.subject">
708
+ <div class="fw-bold small mb-1" x-text="m.subject"></div>
709
+ </template>
710
+ <div class="msg-body" x-text="m.body"></div>
711
+ <div class="d-flex align-items-center gap-2 mt-2 flex-wrap">
712
+ <button class="btn btn-sm btn-ghost-primary py-0 px-1" @click="startReply(m)">
713
+ <i class="ti ti-corner-up-left" style="font-size:14px"></i> Reply
714
+ </button>
715
+ <template x-if="m.channel">
716
+ <span class="badge bg-warning-lt" x-text="'#' + m.channel"></span>
717
+ </template>
718
+ <template x-if="m.claimedBy">
719
+ <span class="badge bg-success-lt" x-text="'claimed by ' + displayTarget(m.claimedBy)"></span>
720
+ </template>
721
+ <template x-if="m.replyTo">
722
+ <span class="text-secondary small ms-auto" x-text="'reply to #' + m.replyTo"></span>
723
+ </template>
724
+ </div>
725
+ </div>
726
+ </template>
727
+ </div>
728
+ <template x-if="selectedInboxThreadData">
729
+ <div class="card-footer">
730
+ <div class="d-flex gap-2 align-items-start">
731
+ <textarea
732
+ class="form-control"
733
+ rows="3"
734
+ placeholder="Reply"
735
+ :value="replyDraftForThread(selectedInboxThreadData)"
736
+ @input="setReplyDraft(selectedInboxThreadData, $event.target.value)"
737
+ @keydown.ctrl.enter.prevent="sendInboxReply(selectedInboxThreadData)"
738
+ @keydown.meta.enter.prevent="sendInboxReply(selectedInboxThreadData)"
739
+ ></textarea>
740
+ <div class="d-flex flex-column gap-2">
741
+ <button class="btn btn-primary" @click="sendInboxReply(selectedInboxThreadData)" :disabled="!replyDraftForThread(selectedInboxThreadData)">
742
+ <i class="ti ti-corner-up-left"></i>
743
+ </button>
744
+ <button class="btn btn-ghost-secondary" @click="clearReplyDraft(selectedInboxThreadData)" :disabled="!replyDraftForThread(selectedInboxThreadData)">
745
+ <i class="ti ti-eraser"></i>
746
+ </button>
747
+ </div>
748
+ </div>
749
+ </div>
750
+ </template>
751
+ </div>
752
+ </div>
753
+ </div>
754
+ </div>
755
+
756
+ <!-- ==================== PAIRS ==================== -->
757
+ <div x-show="view === 'pairs'" x-cloak class="fade-in">
758
+ <div class="d-flex align-items-center mb-3 gap-2 flex-wrap">
759
+ <h2 class="page-title mb-0">Pairs</h2>
760
+ <span class="badge bg-warning-lt" x-show="attentionSummary.pendingPairInvites > 0" x-text="attentionSummary.pendingPairInvites + ' invites pending'"></span>
761
+ <div class="ms-auto d-flex gap-2 align-items-center flex-wrap">
762
+ <select class="form-select form-select-sm" style="width:auto; min-width: 150px" x-model="pairStatusFilter" @change="fetchPairs()">
763
+ <option value="open">Status: Open</option>
764
+ <option value="">Status: All</option>
765
+ <option value="pending">Status: Pending</option>
766
+ <option value="active">Status: Active</option>
767
+ <option value="ended">Status: Ended</option>
768
+ <option value="rejected">Status: Rejected</option>
769
+ <option value="expired">Status: Expired</option>
770
+ </select>
771
+ <button class="btn btn-sm btn-ghost-secondary" @click="fetchPairs()">
772
+ <i class="ti ti-refresh"></i>
773
+ </button>
774
+ <button class="btn btn-sm btn-primary" @click="openPairInvite()">
775
+ <i class="ti ti-link-plus me-1"></i>Pair agents
776
+ </button>
777
+ </div>
778
+ </div>
779
+
780
+ <div class="row g-3">
781
+ <template x-for="pair in pairs" :key="pair.id">
782
+ <div class="col-lg-6">
783
+ <div class="card pair-card" :class="pair.status">
784
+ <div class="card-body">
785
+ <div class="d-flex align-items-start gap-3">
786
+ <span class="badge text-white mt-1" :class="pairStatusClass(pair)" x-text="pair.status"></span>
787
+ <div class="flex-grow-1 min-width-0">
788
+ <template x-if="pair.status === 'pending'">
789
+ <span class="badge bg-warning-lt mb-2">
790
+ <i class="ti ti-bell me-1"></i>pair invite pending
791
+ </span>
792
+ </template>
793
+ <div class="d-flex align-items-center gap-2 flex-wrap">
794
+ <button class="btn btn-sm btn-ghost-secondary py-0 px-1" @click="openAgentDetail(agentsById[pair.requesterId])">
795
+ <i class="ti ti-arrow-up-right me-1"></i><span x-text="displayTarget(pair.requesterId)"></span>
796
+ </button>
797
+ <i class="ti ti-arrows-exchange text-secondary"></i>
798
+ <button class="btn btn-sm btn-ghost-secondary py-0 px-1" @click="openAgentDetail(agentsById[pair.targetId])">
799
+ <span x-text="displayTarget(pair.targetId)"></span>
800
+ </button>
801
+ </div>
802
+ <div class="text-secondary small mt-1 text-truncate">
803
+ <span x-text="pair.id"></span>
804
+ <span class="mx-1">·</span>
805
+ <span x-text="'Updated ' + timeAgo(pair.updatedAt || pair.createdAt)"></span>
806
+ </div>
807
+ <template x-if="pair.objective">
808
+ <div class="msg-body mt-2" x-text="pair.objective"></div>
809
+ </template>
810
+ <div class="d-flex align-items-center gap-2 mt-3 flex-wrap">
811
+ <template x-if="pair.status === 'pending'">
812
+ <button class="btn btn-sm btn-success" @click="doAcceptPair(pair)">
813
+ <i class="ti ti-check me-1"></i>Accept
814
+ </button>
815
+ </template>
816
+ <template x-if="pair.status === 'pending'">
817
+ <button class="btn btn-sm btn-ghost-danger" @click="doRejectPair(pair)">
818
+ <i class="ti ti-x me-1"></i>Reject
819
+ </button>
820
+ </template>
821
+ <template x-if="pair.status === 'active'">
822
+ <button class="btn btn-sm btn-primary" @click="openPairMessage(pair)">
823
+ <i class="ti ti-send me-1"></i>Message
824
+ </button>
825
+ </template>
826
+ <template x-if="pair.status === 'active'">
827
+ <button class="btn btn-sm btn-ghost-danger" @click="doHangupPair(pair)">
828
+ <i class="ti ti-phone-off me-1"></i>Hang up
829
+ </button>
830
+ </template>
831
+ <span class="text-secondary small ms-auto" x-text="'Created ' + fmtTime(pair.createdAt)"></span>
832
+ </div>
833
+ </div>
834
+ </div>
835
+ </div>
836
+ </div>
837
+ </div>
838
+ </template>
839
+ </div>
840
+
841
+ <template x-if="pairs.length === 0">
842
+ <div class="card">
843
+ <div class="card-body text-center text-secondary py-5">
844
+ <i class="ti ti-link-off" style="font-size:48px; opacity:0.3"></i>
845
+ <p class="mt-2">No pair sessions</p>
846
+ </div>
847
+ </div>
848
+ </template>
849
+ </div>
850
+
409
851
  <!-- ==================== MESSAGES ==================== -->
410
852
  <div x-show="view === 'messages'" x-cloak class="fade-in">
411
853
 
@@ -506,6 +948,7 @@
506
948
  <div x-show="view === 'tasks'" x-cloak class="fade-in">
507
949
  <div class="d-flex align-items-center mb-3 gap-2 flex-wrap">
508
950
  <h2 class="page-title mb-0">Tasks</h2>
951
+ <span class="badge bg-warning-lt" x-show="attentionSummary.claimableTasks > 0" x-text="attentionSummary.claimableTasks + ' claimable waiting'"></span>
509
952
  <div class="ms-auto d-flex gap-2 align-items-center flex-wrap">
510
953
  <select class="form-select form-select-sm" style="width:auto; min-width: 150px" x-model="taskStatusFilter" @change="fetchTasks()">
511
954
  <option value="">Status: Active</option>
@@ -535,6 +978,7 @@
535
978
  <div class="d-flex align-items-center gap-2">
536
979
  <span class="fw-bold text-truncate" x-text="task.title"></span>
537
980
  <span class="badge bg-secondary-lt" x-text="task.status"></span>
981
+ <span class="badge bg-warning-lt" x-show="['open','blocked'].includes(task.status) && !task.claimedBy">claimable waiting</span>
538
982
  </div>
539
983
  <div class="text-secondary small mt-1">
540
984
  <span x-text="'#' + task.id"></span>
@@ -659,6 +1103,173 @@
659
1103
  </div>
660
1104
  </main>
661
1105
 
1106
+ <!-- ==================== AGENT DETAIL DRAWER ==================== -->
1107
+ <template x-if="selectedAgentDetail">
1108
+ <div>
1109
+ <div class="agent-drawer-backdrop" x-show="agentDetailOpen" x-cloak @click="closeAgentDetail()"></div>
1110
+ <aside class="agent-drawer" x-show="agentDetailOpen" x-cloak>
1111
+ <div class="p-3 border-bottom d-flex align-items-start gap-2">
1112
+ <span class="status-dot mt-2" :class="[selectedAgentDetail.status, selectedAgentDetail.status !== 'offline' && !selectedAgentDetail.ready ? 'not-ready' : '']"></span>
1113
+ <span class="agent-type-icon mt-1" :class="agentType(selectedAgentDetail)" :title="agentTypeTitle(selectedAgentDetail)">
1114
+ <i class="ti" :class="agentTypeIcon(selectedAgentDetail)"></i>
1115
+ </span>
1116
+ <div class="flex-grow-1 min-width-0">
1117
+ <div class="d-flex align-items-center gap-2">
1118
+ <template x-if="selectedAgentDetail.label">
1119
+ <span class="agent-label text-truncate" x-text="selectedAgentDetail.label"></span>
1120
+ </template>
1121
+ <span class="fw-bold text-truncate" x-text="selectedAgentDetail.name || selectedAgentDetail.id.slice(-12)"></span>
1122
+ </div>
1123
+ <div class="text-secondary small text-truncate" x-text="selectedAgentDetail.id"></div>
1124
+ </div>
1125
+ <button class="btn btn-sm btn-ghost-secondary p-1" @click="closeAgentDetail()" title="Close">
1126
+ <i class="ti ti-x"></i>
1127
+ </button>
1128
+ </div>
1129
+
1130
+ <div class="p-3 border-bottom d-flex gap-2 flex-wrap">
1131
+ <button class="btn btn-sm btn-primary" @click="openComposeToAgent(selectedAgentDetail)">
1132
+ <i class="ti ti-send me-1"></i>Message
1133
+ </button>
1134
+ <button class="btn btn-sm btn-ghost-secondary" @click="selectedAgent = selectedAgentDetail.id; switchView('messages'); closeAgentDetail()">
1135
+ <i class="ti ti-filter me-1"></i>Messages
1136
+ </button>
1137
+ <button class="btn btn-sm btn-ghost-secondary" @click="switchView('pairs'); closeAgentDetail()">
1138
+ <i class="ti ti-link me-1"></i>Pairs
1139
+ </button>
1140
+ <button class="btn btn-sm btn-ghost-secondary" @click="openPairInvite(selectedAgentDetail.id); closeAgentDetail()">
1141
+ <i class="ti ti-link-plus me-1"></i>Pair
1142
+ </button>
1143
+ <button class="btn btn-sm btn-ghost-secondary" @click="openRename(selectedAgentDetail)">
1144
+ <i class="ti ti-pencil me-1"></i>Rename
1145
+ </button>
1146
+ </div>
1147
+
1148
+ <div class="p-3 border-bottom">
1149
+ <h3 class="card-title mb-3">Status</h3>
1150
+ <template x-if="agentAttention(selectedAgentDetail).total > 0">
1151
+ <div class="alert alert-warning py-2 mb-3">
1152
+ <div class="fw-bold small mb-1">Needs attention</div>
1153
+ <div class="attention-badges d-flex gap-1 flex-wrap">
1154
+ <span class="badge bg-danger-lt" x-show="agentAttention(selectedAgentDetail).unread" x-text="agentAttention(selectedAgentDetail).unread + ' unread'"></span>
1155
+ <span class="badge bg-warning-lt" x-show="agentAttention(selectedAgentDetail).needsHumanResponse">needs human response</span>
1156
+ <span class="badge bg-info-lt" x-show="agentAttention(selectedAgentDetail).agentQuestion">agent asked a question</span>
1157
+ <span class="badge bg-warning-lt" x-show="agentAttention(selectedAgentDetail).pendingPairInvite">pair invite pending</span>
1158
+ <span class="badge bg-orange-lt" x-show="agentAttention(selectedAgentDetail).claimableTasks" x-text="agentAttention(selectedAgentDetail).claimableTasks + ' claimable waiting'"></span>
1159
+ </div>
1160
+ </div>
1161
+ </template>
1162
+ <div class="detail-row mb-2">
1163
+ <div class="text-secondary small">State</div>
1164
+ <div>
1165
+ <span class="badge bg-secondary-lt" x-text="agentStatusTitle(selectedAgentDetail)"></span>
1166
+ <template x-if="selectedAgentDetail.ready">
1167
+ <span class="badge bg-success-lt ms-1">ready</span>
1168
+ </template>
1169
+ </div>
1170
+ </div>
1171
+ <div class="detail-row mb-2">
1172
+ <div class="text-secondary small">Last seen</div>
1173
+ <div class="small" x-text="timeAgo(selectedAgentDetail.lastSeen)"></div>
1174
+ </div>
1175
+ <div class="detail-row mb-2">
1176
+ <div class="text-secondary small">Created</div>
1177
+ <div class="small" x-text="fmtTime(selectedAgentDetail.createdAt)"></div>
1178
+ </div>
1179
+ <template x-if="selectedAgentDetail.machine || selectedAgentDetail.rig">
1180
+ <div class="detail-row mb-2">
1181
+ <div class="text-secondary small">Host</div>
1182
+ <div class="d-flex gap-1 flex-wrap">
1183
+ <template x-if="selectedAgentDetail.machine">
1184
+ <span class="badge bg-secondary-lt" x-text="selectedAgentDetail.machine"></span>
1185
+ </template>
1186
+ <template x-if="selectedAgentDetail.rig">
1187
+ <span class="badge bg-primary-lt" x-text="selectedAgentDetail.rig"></span>
1188
+ </template>
1189
+ </div>
1190
+ </div>
1191
+ </template>
1192
+ </div>
1193
+
1194
+ <template x-if="agentPair(selectedAgentDetail)">
1195
+ <div class="p-3 border-bottom">
1196
+ <h3 class="card-title mb-3">Pair</h3>
1197
+ <div class="d-flex align-items-center gap-2 flex-wrap">
1198
+ <span class="badge pair-badge" :class="pairBadgeClass(agentPair(selectedAgentDetail))" :title="pairTitle(agentPair(selectedAgentDetail), selectedAgentDetail.id)">
1199
+ <i class="ti ti-link me-1"></i><span x-text="pairBadgeLabel(agentPair(selectedAgentDetail), selectedAgentDetail.id)"></span>
1200
+ </span>
1201
+ <span class="text-secondary small" x-text="agentPair(selectedAgentDetail).id"></span>
1202
+ </div>
1203
+ <template x-if="agentPair(selectedAgentDetail).objective">
1204
+ <div class="msg-body mt-2" x-text="agentPair(selectedAgentDetail).objective"></div>
1205
+ </template>
1206
+ <div class="d-flex gap-2 mt-3 flex-wrap">
1207
+ <template x-if="agentPair(selectedAgentDetail).status === 'active'">
1208
+ <button class="btn btn-sm btn-primary" @click="openPairMessage(agentPair(selectedAgentDetail), selectedAgentDetail.id)">
1209
+ <i class="ti ti-send me-1"></i>Pair message
1210
+ </button>
1211
+ </template>
1212
+ <template x-if="agentPair(selectedAgentDetail).status === 'pending' && agentPair(selectedAgentDetail).targetId === selectedAgentDetail.id">
1213
+ <button class="btn btn-sm btn-success" @click="doAcceptPair(agentPair(selectedAgentDetail))">
1214
+ <i class="ti ti-check me-1"></i>Accept
1215
+ </button>
1216
+ </template>
1217
+ <template x-if="agentPair(selectedAgentDetail).status === 'pending' && agentPair(selectedAgentDetail).targetId === selectedAgentDetail.id">
1218
+ <button class="btn btn-sm btn-ghost-danger" @click="doRejectPair(agentPair(selectedAgentDetail))">
1219
+ <i class="ti ti-x me-1"></i>Reject
1220
+ </button>
1221
+ </template>
1222
+ <template x-if="agentPair(selectedAgentDetail).status === 'active'">
1223
+ <button class="btn btn-sm btn-ghost-danger" @click="doHangupPair(agentPair(selectedAgentDetail), selectedAgentDetail.id)">
1224
+ <i class="ti ti-phone-off me-1"></i>Hang up
1225
+ </button>
1226
+ </template>
1227
+ </div>
1228
+ </div>
1229
+ </template>
1230
+
1231
+ <div class="p-3 border-bottom">
1232
+ <h3 class="card-title mb-3">Tags</h3>
1233
+ <div class="d-flex gap-1 flex-wrap">
1234
+ <template x-for="tag in (selectedAgentDetail.tags || [])" :key="tag">
1235
+ <span class="badge bg-cyan-lt" x-text="tag"></span>
1236
+ </template>
1237
+ <template x-if="!(selectedAgentDetail.tags || []).length">
1238
+ <span class="text-secondary small">No tags</span>
1239
+ </template>
1240
+ </div>
1241
+ <template x-if="selectedAgentDetail.capabilities && selectedAgentDetail.capabilities.length">
1242
+ <div class="d-flex gap-1 flex-wrap mt-2">
1243
+ <template x-for="cap in selectedAgentDetail.capabilities" :key="cap">
1244
+ <span class="badge bg-purple-lt" x-text="cap"></span>
1245
+ </template>
1246
+ </div>
1247
+ </template>
1248
+ </div>
1249
+
1250
+ <div class="p-3">
1251
+ <h3 class="card-title mb-3">Recent Messages</h3>
1252
+ <div class="list-group list-group-flush">
1253
+ <template x-for="m in agentDetailMessages" :key="m.id">
1254
+ <div class="list-group-item px-0">
1255
+ <div class="d-flex align-items-center gap-2 mb-1">
1256
+ <span class="fw-bold small" x-text="displayTarget(m.from)"></span>
1257
+ <i class="ti ti-arrow-right text-secondary" style="font-size:12px"></i>
1258
+ <span class="small" x-text="displayTarget(m.to)"></span>
1259
+ <span class="text-secondary small ms-auto" x-text="'#' + m.id"></span>
1260
+ </div>
1261
+ <div class="text-secondary small text-truncate" x-text="messagePreview(m)"></div>
1262
+ </div>
1263
+ </template>
1264
+ <template x-if="agentDetailMessages.length === 0">
1265
+ <div class="text-secondary small">No recent messages loaded</div>
1266
+ </template>
1267
+ </div>
1268
+ </div>
1269
+ </aside>
1270
+ </div>
1271
+ </template>
1272
+
662
1273
  <!-- ==================== COMPOSE MODAL ==================== -->
663
1274
  <div class="modal modal-blur" :class="{ show: composeOpen }" :style="composeOpen ? 'display:block' : 'display:none'" tabindex="-1" @click.self="composeOpen = false">
664
1275
  <div class="modal-dialog modal-lg modal-dialog-centered">
@@ -743,6 +1354,101 @@
743
1354
  </div>
744
1355
  <div class="modal-backdrop fade show" x-show="composeOpen" x-cloak></div>
745
1356
 
1357
+ <!-- ==================== PAIR INVITE MODAL ==================== -->
1358
+ <div class="modal modal-blur" :class="{ show: pairInviteOpen }" :style="pairInviteOpen ? 'display:block' : 'display:none'" tabindex="-1" @click.self="closePairInvite()">
1359
+ <div class="modal-dialog modal-lg modal-dialog-centered">
1360
+ <div class="modal-content">
1361
+ <div class="modal-header">
1362
+ <h5 class="modal-title">Pair Agents</h5>
1363
+ <button class="btn-close" @click="closePairInvite()"></button>
1364
+ </div>
1365
+ <div class="modal-body">
1366
+ <div class="row g-3">
1367
+ <div class="col-md-6">
1368
+ <label class="form-label">Requester</label>
1369
+ <select class="form-select" x-model="pairInvite.requesterId">
1370
+ <option value="">Select requester…</option>
1371
+ <template x-for="a in composeAgents" :key="a.id">
1372
+ <option :value="a.id" x-text="displayName(a) + ' [' + a.id.slice(-6) + ']'"></option>
1373
+ </template>
1374
+ </select>
1375
+ </div>
1376
+ <div class="col-md-6">
1377
+ <label class="form-label">Target</label>
1378
+ <select class="form-select" x-model="pairInvite.targetId">
1379
+ <option value="">Select target…</option>
1380
+ <template x-for="a in composeAgents" :key="a.id">
1381
+ <option :value="a.id" x-text="displayName(a) + ' [' + a.id.slice(-6) + ']'"></option>
1382
+ </template>
1383
+ </select>
1384
+ </div>
1385
+ <div class="col-12">
1386
+ <label class="form-label">Objective <span class="text-secondary">(optional)</span></label>
1387
+ <textarea class="form-control" rows="4" x-model="pairInvite.objective" x-ref="pairInviteObjective"></textarea>
1388
+ </div>
1389
+ </div>
1390
+ </div>
1391
+ <div class="modal-footer">
1392
+ <button class="btn btn-ghost-secondary" @click="closePairInvite()">Cancel</button>
1393
+ <button class="btn btn-primary" @click="doCreatePair()">
1394
+ <i class="ti ti-link-plus me-1"></i>Create invite
1395
+ </button>
1396
+ </div>
1397
+ </div>
1398
+ </div>
1399
+ </div>
1400
+ <div class="modal-backdrop fade show" x-show="pairInviteOpen" x-cloak></div>
1401
+
1402
+ <!-- ==================== PAIR MESSAGE MODAL ==================== -->
1403
+ <div class="modal modal-blur" :class="{ show: pairMessageOpen }" :style="pairMessageOpen ? 'display:block' : 'display:none'" tabindex="-1" @click.self="closePairMessage()">
1404
+ <div class="modal-dialog modal-lg modal-dialog-centered">
1405
+ <div class="modal-content">
1406
+ <div class="modal-header">
1407
+ <h5 class="modal-title">Pair Message</h5>
1408
+ <button class="btn-close" @click="closePairMessage()"></button>
1409
+ </div>
1410
+ <div class="modal-body">
1411
+ <template x-if="pairMessagePair">
1412
+ <div class="alert alert-info py-2">
1413
+ <i class="ti ti-link me-1"></i>
1414
+ <span x-text="displayTarget(pairMessagePair.requesterId)"></span>
1415
+ <span class="mx-1">↔</span>
1416
+ <span x-text="displayTarget(pairMessagePair.targetId)"></span>
1417
+ </div>
1418
+ </template>
1419
+ <div class="row g-3">
1420
+ <div class="col-md-6">
1421
+ <label class="form-label">From</label>
1422
+ <select class="form-select" x-model="pairMessage.from">
1423
+ <template x-if="pairMessagePair">
1424
+ <option :value="pairMessagePair.requesterId" x-text="displayTarget(pairMessagePair.requesterId)"></option>
1425
+ </template>
1426
+ <template x-if="pairMessagePair">
1427
+ <option :value="pairMessagePair.targetId" x-text="displayTarget(pairMessagePair.targetId)"></option>
1428
+ </template>
1429
+ </select>
1430
+ </div>
1431
+ <div class="col-md-6">
1432
+ <label class="form-label">Subject <span class="text-secondary">(optional)</span></label>
1433
+ <input type="text" class="form-control" x-model="pairMessage.subject">
1434
+ </div>
1435
+ <div class="col-12">
1436
+ <label class="form-label">Message</label>
1437
+ <textarea class="form-control" rows="5" x-model="pairMessage.body" x-ref="pairMessageBody"></textarea>
1438
+ </div>
1439
+ </div>
1440
+ </div>
1441
+ <div class="modal-footer">
1442
+ <button class="btn btn-ghost-secondary" @click="closePairMessage()">Cancel</button>
1443
+ <button class="btn btn-primary" @click="doSendPairMessage()">
1444
+ <i class="ti ti-send me-1"></i>Send
1445
+ </button>
1446
+ </div>
1447
+ </div>
1448
+ </div>
1449
+ </div>
1450
+ <div class="modal-backdrop fade show" x-show="pairMessageOpen" x-cloak></div>
1451
+
746
1452
  <!-- ==================== THREAD MODAL ==================== -->
747
1453
  <div class="modal modal-blur" :class="{ show: threadOpen }" :style="threadOpen ? 'display:block' : 'display:none'" tabindex="-1" @click.self="threadOpen = false">
748
1454
  <div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">