bosun 0.36.0 → 0.36.2
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/.env.example +98 -16
- package/README.md +27 -0
- package/agent-event-bus.mjs +5 -5
- package/agent-pool.mjs +129 -12
- package/agent-prompts.mjs +7 -1
- package/agent-sdk.mjs +13 -2
- package/agent-supervisor.mjs +2 -2
- package/agent-work-report.mjs +1 -1
- package/anomaly-detector.mjs +6 -6
- package/autofix.mjs +15 -15
- package/bosun-skills.mjs +4 -4
- package/bosun.schema.json +160 -4
- package/claude-shell.mjs +11 -11
- package/cli.mjs +21 -21
- package/codex-config.mjs +19 -19
- package/codex-shell.mjs +180 -29
- package/config-doctor.mjs +27 -2
- package/config.mjs +60 -7
- package/copilot-shell.mjs +4 -4
- package/error-detector.mjs +1 -1
- package/fleet-coordinator.mjs +2 -2
- package/gemini-shell.mjs +692 -0
- package/github-oauth-portal.mjs +1 -1
- package/github-reconciler.mjs +2 -2
- package/kanban-adapter.mjs +741 -168
- package/merge-strategy.mjs +25 -25
- package/monitor.mjs +123 -105
- package/opencode-shell.mjs +22 -22
- package/package.json +7 -1
- package/postinstall.mjs +22 -22
- package/pr-cleanup-daemon.mjs +6 -6
- package/prepublish-check.mjs +4 -4
- package/presence.mjs +2 -2
- package/primary-agent.mjs +85 -7
- package/publish.mjs +1 -1
- package/review-agent.mjs +1 -1
- package/session-tracker.mjs +11 -0
- package/setup-web-server.mjs +429 -21
- package/setup.mjs +367 -12
- package/shared-knowledge.mjs +1 -1
- package/startup-service.mjs +9 -9
- package/stream-resilience.mjs +58 -4
- package/sync-engine.mjs +2 -2
- package/task-assessment.mjs +9 -9
- package/task-cli.mjs +1 -1
- package/task-complexity.mjs +71 -2
- package/task-context.mjs +1 -2
- package/task-executor.mjs +104 -41
- package/telegram-bot.mjs +825 -494
- package/telegram-sentinel.mjs +28 -28
- package/ui/app.js +256 -23
- package/ui/app.monolith.js +1 -1
- package/ui/components/agent-selector.js +4 -3
- package/ui/components/chat-view.js +101 -28
- package/ui/components/diff-viewer.js +3 -3
- package/ui/components/kanban-board.js +3 -3
- package/ui/components/session-list.js +255 -35
- package/ui/components/workspace-switcher.js +3 -3
- package/ui/demo.html +209 -194
- package/ui/index.html +3 -3
- package/ui/modules/icon-utils.js +206 -142
- package/ui/modules/icons.js +2 -27
- package/ui/modules/settings-schema.js +29 -5
- package/ui/modules/streaming.js +30 -2
- package/ui/modules/vision-stream.js +275 -0
- package/ui/modules/voice-client.js +102 -9
- package/ui/modules/voice-fallback.js +62 -6
- package/ui/modules/voice-overlay.js +594 -59
- package/ui/modules/voice.js +31 -38
- package/ui/setup.html +284 -34
- package/ui/styles/components.css +47 -0
- package/ui/styles/sessions.css +75 -0
- package/ui/tabs/agents.js +73 -43
- package/ui/tabs/chat.js +37 -40
- package/ui/tabs/control.js +2 -2
- package/ui/tabs/dashboard.js +1 -1
- package/ui/tabs/infra.js +10 -10
- package/ui/tabs/library.js +8 -8
- package/ui/tabs/logs.js +10 -10
- package/ui/tabs/settings.js +20 -20
- package/ui/tabs/tasks.js +76 -47
- package/ui-server.mjs +1761 -124
- package/update-check.mjs +13 -13
- package/ve-kanban.mjs +1 -1
- package/whatsapp-channel.mjs +5 -5
- package/workflow-engine.mjs +20 -1
- package/workflow-nodes.mjs +904 -4
- package/workflow-templates/agents.mjs +321 -7
- package/workflow-templates/ci-cd.mjs +6 -6
- package/workflow-templates/github.mjs +156 -84
- package/workflow-templates/planning.mjs +8 -8
- package/workflow-templates/reliability.mjs +8 -8
- package/workflow-templates/security.mjs +3 -3
- package/workflow-templates.mjs +15 -9
- package/workspace-manager.mjs +85 -1
- package/workspace-monitor.mjs +2 -2
- package/workspace-registry.mjs +2 -2
- package/worktree-manager.mjs +1 -1
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { h } from "preact";
|
|
12
|
-
import { useState, useEffect, useCallback } from "preact/hooks";
|
|
12
|
+
import { useState, useEffect, useCallback, useRef } from "preact/hooks";
|
|
13
13
|
import htm from "htm";
|
|
14
14
|
import { haptic } from "./telegram.js";
|
|
15
|
+
import { apiFetch } from "./api.js";
|
|
15
16
|
import {
|
|
16
17
|
voiceState, voiceTranscript, voiceResponse, voiceError,
|
|
17
18
|
voiceToolCalls, voiceDuration,
|
|
@@ -22,6 +23,14 @@ import {
|
|
|
22
23
|
fallbackError,
|
|
23
24
|
startFallbackSession, stopFallbackSession, interruptFallback,
|
|
24
25
|
} from "./voice-fallback.js";
|
|
26
|
+
import {
|
|
27
|
+
visionShareState,
|
|
28
|
+
visionShareSource,
|
|
29
|
+
visionShareError,
|
|
30
|
+
visionLastSummary,
|
|
31
|
+
toggleVisionShare,
|
|
32
|
+
stopVisionShare,
|
|
33
|
+
} from "./vision-stream.js";
|
|
25
34
|
import { AudioVisualizer } from "./audio-visualizer.js";
|
|
26
35
|
import { resolveIcon } from "./icon-utils.js";
|
|
27
36
|
|
|
@@ -43,13 +52,14 @@ function injectOverlayStyles() {
|
|
|
43
52
|
background: rgba(0, 0, 0, 0.95);
|
|
44
53
|
display: flex;
|
|
45
54
|
flex-direction: column;
|
|
46
|
-
align-items:
|
|
47
|
-
justify-content:
|
|
55
|
+
align-items: stretch;
|
|
56
|
+
justify-content: flex-start;
|
|
48
57
|
color: #fff;
|
|
49
58
|
font-family: var(--tg-theme-font, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
|
|
50
59
|
animation: voiceOverlayFadeIn 0.3s ease;
|
|
51
60
|
backdrop-filter: blur(20px);
|
|
52
61
|
-webkit-backdrop-filter: blur(20px);
|
|
62
|
+
overflow: hidden;
|
|
53
63
|
}
|
|
54
64
|
@keyframes voiceOverlayFadeIn {
|
|
55
65
|
from { opacity: 0; }
|
|
@@ -66,6 +76,32 @@ function injectOverlayStyles() {
|
|
|
66
76
|
padding: 16px 20px;
|
|
67
77
|
z-index: 2;
|
|
68
78
|
}
|
|
79
|
+
.voice-overlay-header-actions {
|
|
80
|
+
display: inline-flex;
|
|
81
|
+
align-items: center;
|
|
82
|
+
gap: 8px;
|
|
83
|
+
}
|
|
84
|
+
.voice-overlay-chat-toggle {
|
|
85
|
+
height: 32px;
|
|
86
|
+
border-radius: 999px;
|
|
87
|
+
border: 1px solid rgba(255,255,255,0.24);
|
|
88
|
+
background: rgba(255,255,255,0.08);
|
|
89
|
+
color: rgba(255,255,255,0.95);
|
|
90
|
+
font-size: 12px;
|
|
91
|
+
cursor: pointer;
|
|
92
|
+
padding: 0 12px;
|
|
93
|
+
}
|
|
94
|
+
.voice-overlay-chat-toggle:disabled {
|
|
95
|
+
opacity: 0.45;
|
|
96
|
+
cursor: not-allowed;
|
|
97
|
+
}
|
|
98
|
+
.voice-overlay-call-pill {
|
|
99
|
+
font-size: 11px;
|
|
100
|
+
border-radius: 999px;
|
|
101
|
+
border: 1px solid rgba(255,255,255,0.2);
|
|
102
|
+
padding: 3px 9px;
|
|
103
|
+
color: rgba(255,255,255,0.86);
|
|
104
|
+
}
|
|
69
105
|
.voice-overlay-close {
|
|
70
106
|
width: 40px;
|
|
71
107
|
height: 40px;
|
|
@@ -89,6 +125,15 @@ function injectOverlayStyles() {
|
|
|
89
125
|
color: rgba(255,255,255,0.6);
|
|
90
126
|
text-transform: capitalize;
|
|
91
127
|
}
|
|
128
|
+
.voice-overlay-bound {
|
|
129
|
+
font-size: 11px;
|
|
130
|
+
color: rgba(255,255,255,0.45);
|
|
131
|
+
margin-top: 2px;
|
|
132
|
+
max-width: 56vw;
|
|
133
|
+
white-space: nowrap;
|
|
134
|
+
overflow: hidden;
|
|
135
|
+
text-overflow: ellipsis;
|
|
136
|
+
}
|
|
92
137
|
.voice-overlay-duration {
|
|
93
138
|
font-size: 12px;
|
|
94
139
|
color: rgba(255,255,255,0.4);
|
|
@@ -101,6 +146,132 @@ function injectOverlayStyles() {
|
|
|
101
146
|
gap: 32px;
|
|
102
147
|
z-index: 1;
|
|
103
148
|
}
|
|
149
|
+
.voice-overlay-main {
|
|
150
|
+
flex: 1;
|
|
151
|
+
min-height: 0;
|
|
152
|
+
display: flex;
|
|
153
|
+
align-items: stretch;
|
|
154
|
+
gap: 14px;
|
|
155
|
+
padding: 76px 16px 18px;
|
|
156
|
+
}
|
|
157
|
+
.voice-overlay-stage {
|
|
158
|
+
flex: 1;
|
|
159
|
+
min-width: 0;
|
|
160
|
+
min-height: 0;
|
|
161
|
+
display: flex;
|
|
162
|
+
flex-direction: column;
|
|
163
|
+
align-items: center;
|
|
164
|
+
justify-content: center;
|
|
165
|
+
gap: 18px;
|
|
166
|
+
}
|
|
167
|
+
.voice-overlay-chat {
|
|
168
|
+
width: min(420px, 42vw);
|
|
169
|
+
min-width: 300px;
|
|
170
|
+
max-width: 460px;
|
|
171
|
+
border-radius: 16px;
|
|
172
|
+
border: 1px solid rgba(255,255,255,0.14);
|
|
173
|
+
background: rgba(10, 10, 12, 0.78);
|
|
174
|
+
display: flex;
|
|
175
|
+
flex-direction: column;
|
|
176
|
+
overflow: hidden;
|
|
177
|
+
}
|
|
178
|
+
.voice-overlay-chat-head {
|
|
179
|
+
display: flex;
|
|
180
|
+
align-items: center;
|
|
181
|
+
justify-content: space-between;
|
|
182
|
+
gap: 8px;
|
|
183
|
+
padding: 10px 12px;
|
|
184
|
+
border-bottom: 1px solid rgba(255,255,255,0.1);
|
|
185
|
+
}
|
|
186
|
+
.voice-overlay-chat-title {
|
|
187
|
+
font-size: 12px;
|
|
188
|
+
font-weight: 600;
|
|
189
|
+
letter-spacing: 0.02em;
|
|
190
|
+
}
|
|
191
|
+
.voice-overlay-chat-status {
|
|
192
|
+
font-size: 11px;
|
|
193
|
+
color: rgba(255,255,255,0.6);
|
|
194
|
+
}
|
|
195
|
+
.voice-overlay-chat-body {
|
|
196
|
+
flex: 1;
|
|
197
|
+
min-height: 0;
|
|
198
|
+
overflow-y: auto;
|
|
199
|
+
padding: 10px;
|
|
200
|
+
display: flex;
|
|
201
|
+
flex-direction: column;
|
|
202
|
+
gap: 8px;
|
|
203
|
+
}
|
|
204
|
+
.voice-overlay-chat-empty {
|
|
205
|
+
color: rgba(255,255,255,0.6);
|
|
206
|
+
text-align: center;
|
|
207
|
+
font-size: 12px;
|
|
208
|
+
padding: 16px 8px;
|
|
209
|
+
}
|
|
210
|
+
.voice-overlay-chat-msg {
|
|
211
|
+
padding: 8px 10px;
|
|
212
|
+
border-radius: 10px;
|
|
213
|
+
background: rgba(255,255,255,0.06);
|
|
214
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
215
|
+
}
|
|
216
|
+
.voice-overlay-chat-msg.user {
|
|
217
|
+
align-self: flex-end;
|
|
218
|
+
background: rgba(59,130,246,0.24);
|
|
219
|
+
border-color: rgba(59,130,246,0.38);
|
|
220
|
+
}
|
|
221
|
+
.voice-overlay-chat-msg.assistant {
|
|
222
|
+
align-self: flex-start;
|
|
223
|
+
background: rgba(34,197,94,0.2);
|
|
224
|
+
border-color: rgba(34,197,94,0.34);
|
|
225
|
+
}
|
|
226
|
+
.voice-overlay-chat-msg.system {
|
|
227
|
+
align-self: stretch;
|
|
228
|
+
}
|
|
229
|
+
.voice-overlay-chat-meta {
|
|
230
|
+
display: flex;
|
|
231
|
+
justify-content: space-between;
|
|
232
|
+
align-items: center;
|
|
233
|
+
gap: 8px;
|
|
234
|
+
font-size: 10px;
|
|
235
|
+
color: rgba(255,255,255,0.56);
|
|
236
|
+
margin-bottom: 4px;
|
|
237
|
+
text-transform: uppercase;
|
|
238
|
+
letter-spacing: 0.03em;
|
|
239
|
+
}
|
|
240
|
+
.voice-overlay-chat-content {
|
|
241
|
+
white-space: pre-wrap;
|
|
242
|
+
word-break: break-word;
|
|
243
|
+
font-size: 13px;
|
|
244
|
+
line-height: 1.35;
|
|
245
|
+
}
|
|
246
|
+
.voice-overlay-chat-input-wrap {
|
|
247
|
+
border-top: 1px solid rgba(255,255,255,0.1);
|
|
248
|
+
padding: 10px;
|
|
249
|
+
display: flex;
|
|
250
|
+
gap: 8px;
|
|
251
|
+
}
|
|
252
|
+
.voice-overlay-chat-input {
|
|
253
|
+
flex: 1;
|
|
254
|
+
min-width: 0;
|
|
255
|
+
border: 1px solid rgba(255,255,255,0.18);
|
|
256
|
+
border-radius: 8px;
|
|
257
|
+
background: rgba(255,255,255,0.06);
|
|
258
|
+
color: #fff;
|
|
259
|
+
padding: 8px 10px;
|
|
260
|
+
font-size: 13px;
|
|
261
|
+
}
|
|
262
|
+
.voice-overlay-chat-send {
|
|
263
|
+
border: none;
|
|
264
|
+
border-radius: 8px;
|
|
265
|
+
background: #2563eb;
|
|
266
|
+
color: #fff;
|
|
267
|
+
font-size: 12px;
|
|
268
|
+
padding: 0 12px;
|
|
269
|
+
cursor: pointer;
|
|
270
|
+
}
|
|
271
|
+
.voice-overlay-chat-send:disabled {
|
|
272
|
+
opacity: 0.48;
|
|
273
|
+
cursor: not-allowed;
|
|
274
|
+
}
|
|
104
275
|
.voice-orb-container {
|
|
105
276
|
width: 200px;
|
|
106
277
|
height: 200px;
|
|
@@ -153,14 +324,10 @@ function injectOverlayStyles() {
|
|
|
153
324
|
color: #f87171;
|
|
154
325
|
}
|
|
155
326
|
.voice-overlay-footer {
|
|
156
|
-
position: absolute;
|
|
157
|
-
bottom: 0;
|
|
158
|
-
left: 0;
|
|
159
|
-
right: 0;
|
|
160
327
|
display: flex;
|
|
161
328
|
justify-content: center;
|
|
162
|
-
padding:
|
|
163
|
-
z-index:
|
|
329
|
+
padding: 0;
|
|
330
|
+
z-index: 1;
|
|
164
331
|
}
|
|
165
332
|
.voice-end-btn {
|
|
166
333
|
width: 64px;
|
|
@@ -181,6 +348,38 @@ function injectOverlayStyles() {
|
|
|
181
348
|
transform: scale(1.05);
|
|
182
349
|
box-shadow: 0 6px 28px rgba(239, 68, 68, 0.5);
|
|
183
350
|
}
|
|
351
|
+
.voice-overlay-vision-controls {
|
|
352
|
+
display: flex;
|
|
353
|
+
gap: 8px;
|
|
354
|
+
align-items: center;
|
|
355
|
+
}
|
|
356
|
+
.voice-vision-btn {
|
|
357
|
+
min-width: 84px;
|
|
358
|
+
height: 32px;
|
|
359
|
+
border-radius: 999px;
|
|
360
|
+
border: 1px solid rgba(255,255,255,0.25);
|
|
361
|
+
background: rgba(255,255,255,0.08);
|
|
362
|
+
color: rgba(255,255,255,0.9);
|
|
363
|
+
font-size: 12px;
|
|
364
|
+
cursor: pointer;
|
|
365
|
+
padding: 0 12px;
|
|
366
|
+
}
|
|
367
|
+
.voice-vision-btn.active {
|
|
368
|
+
border-color: rgba(74, 222, 128, 0.8);
|
|
369
|
+
background: rgba(34, 197, 94, 0.2);
|
|
370
|
+
color: #bbf7d0;
|
|
371
|
+
}
|
|
372
|
+
.voice-vision-btn:disabled {
|
|
373
|
+
opacity: 0.45;
|
|
374
|
+
cursor: not-allowed;
|
|
375
|
+
}
|
|
376
|
+
.voice-vision-status {
|
|
377
|
+
margin-top: 8px;
|
|
378
|
+
max-width: 560px;
|
|
379
|
+
text-align: center;
|
|
380
|
+
font-size: 12px;
|
|
381
|
+
color: rgba(255,255,255,0.58);
|
|
382
|
+
}
|
|
184
383
|
.voice-error-msg {
|
|
185
384
|
color: #f87171;
|
|
186
385
|
font-size: 14px;
|
|
@@ -203,6 +402,26 @@ function injectOverlayStyles() {
|
|
|
203
402
|
@keyframes voiceDotPulse {
|
|
204
403
|
0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); }
|
|
205
404
|
40% { opacity: 1; transform: scale(1.2); }
|
|
405
|
+
}
|
|
406
|
+
@media (max-width: 980px) {
|
|
407
|
+
.voice-overlay-main {
|
|
408
|
+
padding: 74px 12px 14px;
|
|
409
|
+
flex-direction: column;
|
|
410
|
+
gap: 12px;
|
|
411
|
+
}
|
|
412
|
+
.voice-overlay-chat {
|
|
413
|
+
width: 100%;
|
|
414
|
+
min-width: 0;
|
|
415
|
+
max-width: none;
|
|
416
|
+
max-height: 44vh;
|
|
417
|
+
}
|
|
418
|
+
.voice-overlay-center {
|
|
419
|
+
gap: 18px;
|
|
420
|
+
}
|
|
421
|
+
.voice-orb-container {
|
|
422
|
+
width: 162px;
|
|
423
|
+
height: 162px;
|
|
424
|
+
}
|
|
206
425
|
}
|
|
207
426
|
`;
|
|
208
427
|
document.head.appendChild(style);
|
|
@@ -219,10 +438,38 @@ function formatDuration(seconds) {
|
|
|
219
438
|
// ── Voice Overlay Component ─────────────────────────────────────────────────
|
|
220
439
|
|
|
221
440
|
/**
|
|
222
|
-
* @param {{
|
|
441
|
+
* @param {{
|
|
442
|
+
* visible: boolean,
|
|
443
|
+
* onClose: () => void,
|
|
444
|
+
* tier: number,
|
|
445
|
+
* sessionId?: string,
|
|
446
|
+
* executor?: string,
|
|
447
|
+
* mode?: string,
|
|
448
|
+
* model?: string,
|
|
449
|
+
* callType?: "voice" | "video",
|
|
450
|
+
* initialVisionSource?: "camera" | "screen" | null
|
|
451
|
+
* }} props
|
|
223
452
|
*/
|
|
224
|
-
export function VoiceOverlay({
|
|
453
|
+
export function VoiceOverlay({
|
|
454
|
+
visible,
|
|
455
|
+
onClose,
|
|
456
|
+
tier = 1,
|
|
457
|
+
sessionId,
|
|
458
|
+
executor,
|
|
459
|
+
mode,
|
|
460
|
+
model,
|
|
461
|
+
callType = "voice",
|
|
462
|
+
initialVisionSource = null,
|
|
463
|
+
}) {
|
|
225
464
|
const [started, setStarted] = useState(false);
|
|
465
|
+
const [chatOpen, setChatOpen] = useState(true);
|
|
466
|
+
const [meetingMessages, setMeetingMessages] = useState([]);
|
|
467
|
+
const [meetingChatInput, setMeetingChatInput] = useState("");
|
|
468
|
+
const [meetingChatSending, setMeetingChatSending] = useState(false);
|
|
469
|
+
const [meetingChatLoading, setMeetingChatLoading] = useState(false);
|
|
470
|
+
const [meetingChatError, setMeetingChatError] = useState(null);
|
|
471
|
+
const autoVisionAppliedRef = useRef(false);
|
|
472
|
+
const meetingScrollRef = useRef(null);
|
|
226
473
|
|
|
227
474
|
useEffect(() => { injectOverlayStyles(); }, []);
|
|
228
475
|
|
|
@@ -233,20 +480,126 @@ export function VoiceOverlay({ visible, onClose, tier = 1, sessionId }) {
|
|
|
233
480
|
const error = tier === 1 ? voiceError.value : fallbackError.value;
|
|
234
481
|
const toolCalls = tier === 1 ? voiceToolCalls.value : [];
|
|
235
482
|
const duration = tier === 1 ? voiceDuration.value : 0;
|
|
483
|
+
const visionState = visionShareState.value;
|
|
484
|
+
const visionSource = visionShareSource.value;
|
|
485
|
+
const visionErr = visionShareError.value;
|
|
486
|
+
const latestVisionSummary = visionLastSummary.value;
|
|
487
|
+
const canShareVision = Boolean(sessionId);
|
|
488
|
+
const normalizedCallType =
|
|
489
|
+
String(callType || "").trim().toLowerCase() === "video"
|
|
490
|
+
? "video"
|
|
491
|
+
: "voice";
|
|
492
|
+
const normalizedInitialVisionSource = (() => {
|
|
493
|
+
const source = String(initialVisionSource || "").trim().toLowerCase();
|
|
494
|
+
if (source === "camera" || source === "screen") return source;
|
|
495
|
+
return normalizedCallType === "video" ? "camera" : null;
|
|
496
|
+
})();
|
|
236
497
|
|
|
237
498
|
// Start session on mount
|
|
238
499
|
useEffect(() => {
|
|
239
500
|
if (!visible || started) return;
|
|
240
501
|
setStarted(true);
|
|
241
502
|
if (tier === 1) {
|
|
242
|
-
startVoiceSession();
|
|
503
|
+
startVoiceSession({ sessionId, executor, mode, model });
|
|
243
504
|
} else if (sessionId) {
|
|
244
|
-
startFallbackSession(sessionId);
|
|
505
|
+
startFallbackSession(sessionId, { executor, mode, model });
|
|
245
506
|
}
|
|
246
|
-
}, [visible, started, tier, sessionId]);
|
|
507
|
+
}, [visible, started, tier, sessionId, executor, mode, model]);
|
|
508
|
+
|
|
509
|
+
useEffect(() => {
|
|
510
|
+
if (visible) return;
|
|
511
|
+
stopVisionShare().catch(() => {});
|
|
512
|
+
}, [visible]);
|
|
513
|
+
|
|
514
|
+
useEffect(() => {
|
|
515
|
+
if (visible) return;
|
|
516
|
+
autoVisionAppliedRef.current = false;
|
|
517
|
+
}, [visible]);
|
|
518
|
+
|
|
519
|
+
const loadMeetingMessages = useCallback(async () => {
|
|
520
|
+
const activeSessionId = String(sessionId || "").trim();
|
|
521
|
+
if (!activeSessionId) {
|
|
522
|
+
setMeetingMessages([]);
|
|
523
|
+
setMeetingChatError(null);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
const safeSessionId = encodeURIComponent(activeSessionId);
|
|
527
|
+
const response = await apiFetch(`/api/sessions/${safeSessionId}?limit=80`, {
|
|
528
|
+
_silent: true,
|
|
529
|
+
_trackLoading: false,
|
|
530
|
+
});
|
|
531
|
+
const nextMessages = Array.isArray(response?.session?.messages)
|
|
532
|
+
? response.session.messages
|
|
533
|
+
: [];
|
|
534
|
+
setMeetingMessages(nextMessages);
|
|
535
|
+
setMeetingChatError(null);
|
|
536
|
+
}, [sessionId]);
|
|
537
|
+
|
|
538
|
+
useEffect(() => {
|
|
539
|
+
if (!visible || !chatOpen || !sessionId) return;
|
|
540
|
+
let cancelled = false;
|
|
541
|
+
|
|
542
|
+
const refresh = async (isInitial = false) => {
|
|
543
|
+
if (isInitial) setMeetingChatLoading(true);
|
|
544
|
+
try {
|
|
545
|
+
await loadMeetingMessages();
|
|
546
|
+
} catch (err) {
|
|
547
|
+
if (!cancelled) {
|
|
548
|
+
setMeetingChatError(
|
|
549
|
+
String(err?.message || "Could not refresh meeting chat"),
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
} finally {
|
|
553
|
+
if (isInitial && !cancelled) setMeetingChatLoading(false);
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
refresh(true).catch(() => {});
|
|
558
|
+
const timer = setInterval(() => {
|
|
559
|
+
refresh(false).catch(() => {});
|
|
560
|
+
}, 1600);
|
|
561
|
+
return () => {
|
|
562
|
+
cancelled = true;
|
|
563
|
+
clearInterval(timer);
|
|
564
|
+
};
|
|
565
|
+
}, [visible, chatOpen, sessionId, loadMeetingMessages]);
|
|
566
|
+
|
|
567
|
+
useEffect(() => {
|
|
568
|
+
if (!chatOpen) return;
|
|
569
|
+
const el = meetingScrollRef.current;
|
|
570
|
+
if (!el) return;
|
|
571
|
+
el.scrollTop = el.scrollHeight;
|
|
572
|
+
}, [chatOpen, meetingMessages.length]);
|
|
573
|
+
|
|
574
|
+
useEffect(() => {
|
|
575
|
+
if (!visible || !started || !sessionId) return;
|
|
576
|
+
if (!normalizedInitialVisionSource) return;
|
|
577
|
+
if (autoVisionAppliedRef.current) return;
|
|
578
|
+
autoVisionAppliedRef.current = true;
|
|
579
|
+
toggleVisionShare(normalizedInitialVisionSource, {
|
|
580
|
+
sessionId,
|
|
581
|
+
executor,
|
|
582
|
+
mode,
|
|
583
|
+
model,
|
|
584
|
+
intervalMs: 1000,
|
|
585
|
+
maxWidth: normalizedInitialVisionSource === "screen" ? 1280 : 960,
|
|
586
|
+
jpegQuality: normalizedInitialVisionSource === "screen" ? 0.65 : 0.62,
|
|
587
|
+
}).catch(() => {
|
|
588
|
+
// Keep the session running even if camera/screen permissions fail.
|
|
589
|
+
});
|
|
590
|
+
}, [
|
|
591
|
+
visible,
|
|
592
|
+
started,
|
|
593
|
+
sessionId,
|
|
594
|
+
normalizedInitialVisionSource,
|
|
595
|
+
executor,
|
|
596
|
+
mode,
|
|
597
|
+
model,
|
|
598
|
+
]);
|
|
247
599
|
|
|
248
600
|
const handleClose = useCallback(() => {
|
|
249
601
|
haptic("medium");
|
|
602
|
+
stopVisionShare().catch(() => {});
|
|
250
603
|
if (tier === 1) {
|
|
251
604
|
stopVoiceSession();
|
|
252
605
|
} else {
|
|
@@ -265,12 +618,78 @@ export function VoiceOverlay({ visible, onClose, tier = 1, sessionId }) {
|
|
|
265
618
|
}
|
|
266
619
|
}, [tier]);
|
|
267
620
|
|
|
621
|
+
const handleToggleScreenShare = useCallback(() => {
|
|
622
|
+
haptic("light");
|
|
623
|
+
toggleVisionShare("screen", {
|
|
624
|
+
sessionId,
|
|
625
|
+
executor,
|
|
626
|
+
mode,
|
|
627
|
+
model,
|
|
628
|
+
intervalMs: 1000,
|
|
629
|
+
maxWidth: 1280,
|
|
630
|
+
jpegQuality: 0.65,
|
|
631
|
+
}).catch(() => {});
|
|
632
|
+
}, [sessionId, executor, mode, model]);
|
|
633
|
+
|
|
634
|
+
const handleToggleCameraShare = useCallback(() => {
|
|
635
|
+
haptic("light");
|
|
636
|
+
toggleVisionShare("camera", {
|
|
637
|
+
sessionId,
|
|
638
|
+
executor,
|
|
639
|
+
mode,
|
|
640
|
+
model,
|
|
641
|
+
intervalMs: 1000,
|
|
642
|
+
maxWidth: 960,
|
|
643
|
+
jpegQuality: 0.62,
|
|
644
|
+
}).catch(() => {});
|
|
645
|
+
}, [sessionId, executor, mode, model]);
|
|
646
|
+
|
|
647
|
+
const handleSendMeetingChat = useCallback(async () => {
|
|
648
|
+
const activeSessionId = String(sessionId || "").trim();
|
|
649
|
+
const content = String(meetingChatInput || "").trim();
|
|
650
|
+
if (!activeSessionId || !content || meetingChatSending) return;
|
|
651
|
+
setMeetingChatSending(true);
|
|
652
|
+
const safeSessionId = encodeURIComponent(activeSessionId);
|
|
653
|
+
try {
|
|
654
|
+
await apiFetch(`/api/sessions/${safeSessionId}/message`, {
|
|
655
|
+
method: "POST",
|
|
656
|
+
body: JSON.stringify({ content }),
|
|
657
|
+
});
|
|
658
|
+
setMeetingChatInput("");
|
|
659
|
+
await loadMeetingMessages();
|
|
660
|
+
} catch (err) {
|
|
661
|
+
setMeetingChatError(String(err?.message || "Could not send chat message"));
|
|
662
|
+
} finally {
|
|
663
|
+
setMeetingChatSending(false);
|
|
664
|
+
}
|
|
665
|
+
}, [
|
|
666
|
+
meetingChatInput,
|
|
667
|
+
meetingChatSending,
|
|
668
|
+
sessionId,
|
|
669
|
+
loadMeetingMessages,
|
|
670
|
+
]);
|
|
671
|
+
|
|
268
672
|
if (!visible) return null;
|
|
269
673
|
|
|
270
674
|
const statusLabel = state === "connected" ? "ready" : state;
|
|
675
|
+
const boundLabel = [
|
|
676
|
+
sessionId ? `session ${sessionId}` : null,
|
|
677
|
+
executor ? `agent ${executor}` : null,
|
|
678
|
+
mode ? `mode ${mode}` : null,
|
|
679
|
+
model ? `model ${model}` : null,
|
|
680
|
+
]
|
|
681
|
+
.filter(Boolean)
|
|
682
|
+
.join(" · ");
|
|
683
|
+
const chatStatusLabel = meetingChatLoading
|
|
684
|
+
? "syncing"
|
|
685
|
+
: meetingChatSending
|
|
686
|
+
? "sending"
|
|
687
|
+
: meetingChatError
|
|
688
|
+
? "error"
|
|
689
|
+
: "live";
|
|
271
690
|
|
|
272
691
|
return html`
|
|
273
|
-
<div class="voice-overlay"
|
|
692
|
+
<div class="voice-overlay">
|
|
274
693
|
<!-- Header -->
|
|
275
694
|
<div class="voice-overlay-header">
|
|
276
695
|
<button class="voice-overlay-close" onClick=${handleClose} title="End voice session">
|
|
@@ -278,63 +697,179 @@ export function VoiceOverlay({ visible, onClose, tier = 1, sessionId }) {
|
|
|
278
697
|
</button>
|
|
279
698
|
<div>
|
|
280
699
|
<div class="voice-overlay-status">${statusLabel}</div>
|
|
700
|
+
${boundLabel && html`<div class="voice-overlay-bound">${boundLabel}</div>`}
|
|
281
701
|
${duration > 0 && html`
|
|
282
702
|
<div class="voice-overlay-duration">${formatDuration(duration)}</div>
|
|
283
703
|
`}
|
|
284
704
|
</div>
|
|
285
|
-
<div
|
|
705
|
+
<div class="voice-overlay-header-actions">
|
|
706
|
+
<span class="voice-overlay-call-pill">
|
|
707
|
+
${normalizedCallType === "video" ? "video call" : "voice call"}
|
|
708
|
+
</span>
|
|
709
|
+
<button
|
|
710
|
+
class="voice-overlay-chat-toggle"
|
|
711
|
+
onClick=${() => setChatOpen((prev) => !prev)}
|
|
712
|
+
disabled=${!sessionId}
|
|
713
|
+
title=${sessionId ? "Toggle meeting chat" : "Open a session-bound call first"}
|
|
714
|
+
>
|
|
715
|
+
${chatOpen ? "Hide Chat" : "Show Chat"}
|
|
716
|
+
</button>
|
|
717
|
+
</div>
|
|
286
718
|
</div>
|
|
287
719
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
720
|
+
<div class="voice-overlay-main">
|
|
721
|
+
<div class="voice-overlay-stage">
|
|
722
|
+
<!-- Center content -->
|
|
723
|
+
<div class="voice-overlay-center">
|
|
724
|
+
<!-- Orb visualization -->
|
|
725
|
+
<div
|
|
726
|
+
class="voice-orb-container"
|
|
727
|
+
onClick=${state === "speaking" ? handleInterrupt : undefined}
|
|
728
|
+
>
|
|
729
|
+
<${AudioVisualizer} state=${state} />
|
|
730
|
+
</div>
|
|
294
731
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
732
|
+
${state === "connecting" || state === "reconnecting"
|
|
733
|
+
? html`
|
|
734
|
+
<div class="voice-connecting-dots">
|
|
735
|
+
<span /><span /><span />
|
|
736
|
+
</div>
|
|
737
|
+
<div class="voice-overlay-status" style="font-size: 16px">
|
|
738
|
+
${state === "reconnecting" ? "Reconnecting..." : "Connecting..."}
|
|
739
|
+
</div>
|
|
740
|
+
`
|
|
741
|
+
: null}
|
|
305
742
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
743
|
+
${error && html`
|
|
744
|
+
<div class="voice-error-msg">${error}</div>
|
|
745
|
+
`}
|
|
309
746
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
747
|
+
<!-- Transcript area -->
|
|
748
|
+
<div class="voice-transcript-area">
|
|
749
|
+
${transcript && html`
|
|
750
|
+
<div class="voice-transcript-user">"${transcript}"</div>
|
|
751
|
+
`}
|
|
752
|
+
${response && html`
|
|
753
|
+
<div class="voice-transcript-assistant">${response}</div>
|
|
754
|
+
`}
|
|
755
|
+
</div>
|
|
319
756
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
757
|
+
<div class="voice-overlay-vision-controls">
|
|
758
|
+
<button
|
|
759
|
+
class="voice-vision-btn ${visionState === "streaming" && visionSource === "screen" ? "active" : ""}"
|
|
760
|
+
onClick=${handleToggleScreenShare}
|
|
761
|
+
disabled=${!canShareVision}
|
|
762
|
+
title=${canShareVision ? "Share your screen with the active agent call" : "Open a session-bound call first"}
|
|
763
|
+
>
|
|
764
|
+
${visionState === "streaming" && visionSource === "screen" ? "Stop Screen" : "Share Screen"}
|
|
765
|
+
</button>
|
|
766
|
+
<button
|
|
767
|
+
class="voice-vision-btn ${visionState === "streaming" && visionSource === "camera" ? "active" : ""}"
|
|
768
|
+
onClick=${handleToggleCameraShare}
|
|
769
|
+
disabled=${!canShareVision}
|
|
770
|
+
title=${canShareVision ? "Share your camera with the active agent call" : "Open a session-bound call first"}
|
|
771
|
+
>
|
|
772
|
+
${visionState === "streaming" && visionSource === "camera" ? "Stop Camera" : "Share Camera"}
|
|
773
|
+
</button>
|
|
774
|
+
</div>
|
|
775
|
+
|
|
776
|
+
${(visionErr || latestVisionSummary) && html`
|
|
777
|
+
<div class="voice-vision-status">
|
|
778
|
+
${visionErr || latestVisionSummary}
|
|
779
|
+
</div>
|
|
780
|
+
`}
|
|
781
|
+
|
|
782
|
+
<!-- Tool call cards -->
|
|
783
|
+
${toolCalls.length > 0 && html`
|
|
784
|
+
<div class="voice-tool-cards">
|
|
785
|
+
${toolCalls.slice(-5).map(tc => html`
|
|
786
|
+
<div class="voice-tool-card ${tc.status}" key=${tc.callId}>
|
|
787
|
+
<span>${tc.status === "running" ? resolveIcon("loading") : tc.status === "complete" ? resolveIcon("check") : resolveIcon("alert")}</span>
|
|
788
|
+
<span>${tc.name}</span>
|
|
789
|
+
</div>
|
|
790
|
+
`)}
|
|
327
791
|
</div>
|
|
328
|
-
`
|
|
792
|
+
`}
|
|
329
793
|
</div>
|
|
330
|
-
`}
|
|
331
|
-
</div>
|
|
332
794
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
795
|
+
<!-- Footer -->
|
|
796
|
+
<div class="voice-overlay-footer">
|
|
797
|
+
<button class="voice-end-btn" onClick=${handleClose} title="End call">
|
|
798
|
+
${resolveIcon("close")}
|
|
799
|
+
</button>
|
|
800
|
+
</div>
|
|
801
|
+
</div>
|
|
802
|
+
|
|
803
|
+
${chatOpen && sessionId && html`
|
|
804
|
+
<aside class="voice-overlay-chat">
|
|
805
|
+
<div class="voice-overlay-chat-head">
|
|
806
|
+
<div class="voice-overlay-chat-title">Meeting Chat + Transcript</div>
|
|
807
|
+
<div class="voice-overlay-chat-status">${chatStatusLabel}</div>
|
|
808
|
+
</div>
|
|
809
|
+
<div class="voice-overlay-chat-body" ref=${meetingScrollRef}>
|
|
810
|
+
${meetingMessages.length === 0 && !meetingChatLoading
|
|
811
|
+
? html`<div class="voice-overlay-chat-empty">Conversation will appear here once the call starts.</div>`
|
|
812
|
+
: null}
|
|
813
|
+
${meetingMessages.map((msg, idx) => {
|
|
814
|
+
const roleRaw = String(
|
|
815
|
+
msg?.role ||
|
|
816
|
+
(msg?.type === "tool_call" || msg?.type === "tool_result"
|
|
817
|
+
? "system"
|
|
818
|
+
: "assistant"),
|
|
819
|
+
)
|
|
820
|
+
.trim()
|
|
821
|
+
.toLowerCase();
|
|
822
|
+
const role =
|
|
823
|
+
roleRaw === "user" || roleRaw === "assistant" ? roleRaw : "system";
|
|
824
|
+
const timeRaw = String(msg?.timestamp || "").trim();
|
|
825
|
+
const date = timeRaw ? new Date(timeRaw) : null;
|
|
826
|
+
const timeLabel =
|
|
827
|
+
date && Number.isFinite(date.getTime())
|
|
828
|
+
? date.toLocaleTimeString([], {
|
|
829
|
+
hour: "2-digit",
|
|
830
|
+
minute: "2-digit",
|
|
831
|
+
})
|
|
832
|
+
: "";
|
|
833
|
+
const text =
|
|
834
|
+
typeof msg?.content === "string"
|
|
835
|
+
? msg.content
|
|
836
|
+
: msg?.content == null
|
|
837
|
+
? ""
|
|
838
|
+
: JSON.stringify(msg.content);
|
|
839
|
+
return html`
|
|
840
|
+
<div class="voice-overlay-chat-msg ${role}" key=${msg?.id || `${role}-${idx}`}>
|
|
841
|
+
<div class="voice-overlay-chat-meta">
|
|
842
|
+
<span>${role}</span>
|
|
843
|
+
<span>${timeLabel}</span>
|
|
844
|
+
</div>
|
|
845
|
+
<div class="voice-overlay-chat-content">${text}</div>
|
|
846
|
+
</div>
|
|
847
|
+
`;
|
|
848
|
+
})}
|
|
849
|
+
</div>
|
|
850
|
+
<div class="voice-overlay-chat-input-wrap">
|
|
851
|
+
<input
|
|
852
|
+
class="voice-overlay-chat-input"
|
|
853
|
+
placeholder="Message the agent during the call…"
|
|
854
|
+
value=${meetingChatInput}
|
|
855
|
+
onInput=${(e) => setMeetingChatInput(e.target.value)}
|
|
856
|
+
onKeyDown=${(e) => {
|
|
857
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
858
|
+
e.preventDefault();
|
|
859
|
+
handleSendMeetingChat().catch(() => {});
|
|
860
|
+
}
|
|
861
|
+
}}
|
|
862
|
+
/>
|
|
863
|
+
<button
|
|
864
|
+
class="voice-overlay-chat-send"
|
|
865
|
+
onClick=${() => handleSendMeetingChat().catch(() => {})}
|
|
866
|
+
disabled=${!meetingChatInput.trim() || meetingChatSending}
|
|
867
|
+
>
|
|
868
|
+
Send
|
|
869
|
+
</button>
|
|
870
|
+
</div>
|
|
871
|
+
</aside>
|
|
872
|
+
`}
|
|
338
873
|
</div>
|
|
339
874
|
</div>
|
|
340
875
|
`;
|