bosun 0.41.0 → 0.41.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 +8 -0
- package/README.md +20 -0
- package/agent/agent-event-bus.mjs +248 -6
- package/agent/agent-pool.mjs +125 -28
- package/agent/agent-work-analyzer.mjs +8 -16
- package/agent/retry-queue.mjs +164 -0
- package/bosun.config.example.json +25 -0
- package/bosun.schema.json +825 -183
- package/cli.mjs +59 -5
- package/config/config.mjs +130 -3
- package/infra/monitor.mjs +693 -67
- package/infra/runtime-accumulator.mjs +376 -84
- package/infra/session-tracker.mjs +82 -25
- package/lib/codebase-audit.mjs +133 -18
- package/package.json +23 -4
- package/server/setup-web-server.mjs +25 -0
- package/server/ui-server.mjs +248 -29
- package/setup.mjs +27 -24
- package/shell/codex-shell.mjs +34 -3
- package/shell/copilot-shell.mjs +50 -8
- package/task/msg-hub.mjs +193 -0
- package/task/pipeline.mjs +544 -0
- package/task/task-cli.mjs +38 -2
- package/task/task-executor-pipeline.mjs +143 -0
- package/task/task-executor.mjs +36 -27
- package/telegram/get-telegram-chat-id.mjs +57 -47
- package/ui/components/workspace-switcher.js +7 -7
- package/ui/demo-defaults.js +15694 -10573
- package/ui/modules/settings-schema.js +2 -0
- package/ui/modules/state.js +54 -57
- package/ui/modules/voice-client-sdk.js +375 -36
- package/ui/modules/voice-client.js +140 -31
- package/ui/setup.html +68 -2
- package/ui/styles/components.css +57 -0
- package/ui/styles.css +201 -1
- package/ui/tabs/dashboard.js +74 -0
- package/ui/tabs/logs.js +10 -0
- package/ui/tabs/settings.js +178 -99
- package/ui/tabs/tasks.js +31 -1
- package/ui/tabs/telemetry.js +34 -0
- package/ui/tabs/workflow-canvas-utils.mjs +8 -1
- package/ui/tabs/workflows.js +532 -275
- package/voice/voice-agents-sdk.mjs +1 -1
- package/voice/voice-relay.mjs +6 -6
- package/workflow/declarative-workflows.mjs +145 -0
- package/workflow/msg-hub.mjs +237 -0
- package/workflow/pipeline-workflows.mjs +287 -0
- package/workflow/pipeline.mjs +828 -315
- package/workflow/workflow-cli.mjs +128 -0
- package/workflow/workflow-engine.mjs +329 -17
- package/workflow/workflow-nodes/custom-loader.mjs +250 -0
- package/workflow/workflow-nodes.mjs +1955 -223
- package/workflow/workflow-templates.mjs +26 -8
- package/workflow-templates/agents.mjs +0 -1
- package/workflow-templates/bosun-native.mjs +212 -2
- package/workflow-templates/continuation-loop.mjs +339 -0
- package/workflow-templates/github.mjs +516 -40
- package/workflow-templates/planning.mjs +446 -17
- package/workflow-templates/reliability.mjs +65 -12
- package/workflow-templates/task-batch.mjs +24 -8
- package/workflow-templates/task-lifecycle.mjs +83 -6
- package/workspace/context-cache.mjs +66 -18
- package/workspace/workspace-manager.mjs +2 -1
- package/workflow-templates/issue-continuation.mjs +0 -243
|
@@ -249,12 +249,18 @@ let _traceTtsFirstAudioMarked = false;
|
|
|
249
249
|
|
|
250
250
|
const RECONNECT_AT_MS = 28 * 60 * 1000; // 28 minutes
|
|
251
251
|
const MAX_RECONNECT_ATTEMPTS = 3;
|
|
252
|
-
const AUTO_BARGE_IN_COOLDOWN_MS =
|
|
252
|
+
const AUTO_BARGE_IN_COOLDOWN_MS = 1200;
|
|
253
253
|
const AUTO_BARGE_IN_MIC_LEVEL_THRESHOLD = 0.08;
|
|
254
254
|
const AUTO_BARGE_IN_FADE_MS = 220;
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
const
|
|
255
|
+
// Minimum speech duration (ms) before an interrupt is allowed — filters keyboard/click noise
|
|
256
|
+
let _speechStartedAt = 0;
|
|
257
|
+
const MIN_SPEECH_DURATION_FOR_INTERRUPT_MS = 400;
|
|
258
|
+
// Delayed response clear — keep response visible in center after turn ends
|
|
259
|
+
let _responseClearTimer = null;
|
|
260
|
+
const RESPONSE_DISPLAY_HOLD_MS = 8000;
|
|
261
|
+
// User transcript is always enabled — transcription is surfaced from the API's
|
|
262
|
+
// input_audio_transcription feature (primary) or browser SpeechRecognition (backup).
|
|
263
|
+
const ENABLE_USER_TRANSCRIPT = true;
|
|
258
264
|
let _reconnectAttempts = 0;
|
|
259
265
|
let _pendingResponseCreateTimer = null;
|
|
260
266
|
let _awaitingAutoResponse = false;
|
|
@@ -266,6 +272,64 @@ const SpeechRecognition = typeof globalThis !== "undefined"
|
|
|
266
272
|
? (globalThis.SpeechRecognition || globalThis.webkitSpeechRecognition)
|
|
267
273
|
: null;
|
|
268
274
|
|
|
275
|
+
// ── Browser SpeechRecognition (parallel backup for user transcription) ──────
|
|
276
|
+
|
|
277
|
+
let _browserRecognition = null;
|
|
278
|
+
let _browserTranscriptActive = false;
|
|
279
|
+
let _apiTranscriptDelivered = false;
|
|
280
|
+
|
|
281
|
+
function _startBrowserTranscription() {
|
|
282
|
+
if (!SpeechRecognition || _browserRecognition) return;
|
|
283
|
+
try {
|
|
284
|
+
const recognition = new SpeechRecognition();
|
|
285
|
+
recognition.continuous = true;
|
|
286
|
+
recognition.interimResults = true;
|
|
287
|
+
recognition.maxAlternatives = 1;
|
|
288
|
+
recognition.lang = navigator?.language || "en-US";
|
|
289
|
+
|
|
290
|
+
recognition.onresult = (event) => {
|
|
291
|
+
if (_apiTranscriptDelivered) return;
|
|
292
|
+
let transcript = "";
|
|
293
|
+
for (let i = event.resultIndex; i < event.results.length; i++) {
|
|
294
|
+
transcript += event.results[i][0].transcript;
|
|
295
|
+
}
|
|
296
|
+
const text = transcript.trim();
|
|
297
|
+
if (!text) return;
|
|
298
|
+
voiceTranscript.value = text;
|
|
299
|
+
emit("transcript", { text, final: event.results[event.resultIndex]?.isFinal || false, source: "browser" });
|
|
300
|
+
if (event.results[event.resultIndex]?.isFinal) {
|
|
301
|
+
_recordVoiceTranscriptIfNew("user", text, "browser.speech_recognition.final");
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
recognition.onerror = (e) => {
|
|
306
|
+
if (e.error !== "no-speech" && e.error !== "aborted") {
|
|
307
|
+
console.warn("[voice-client] Browser SpeechRecognition error:", e.error);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
recognition.onend = () => {
|
|
312
|
+
if (_browserTranscriptActive && (_dc || _ws)) {
|
|
313
|
+
try { recognition.start(); } catch { /* already running or stopped */ }
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
recognition.start();
|
|
318
|
+
_browserRecognition = recognition;
|
|
319
|
+
_browserTranscriptActive = true;
|
|
320
|
+
} catch (err) {
|
|
321
|
+
console.warn("[voice-client] Browser SpeechRecognition unavailable:", err?.message);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function _stopBrowserTranscription() {
|
|
326
|
+
_browserTranscriptActive = false;
|
|
327
|
+
if (_browserRecognition) {
|
|
328
|
+
try { _browserRecognition.stop(); } catch { /* ignore */ }
|
|
329
|
+
_browserRecognition = null;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
269
333
|
function _normalizeCallContext(options = {}) {
|
|
270
334
|
const sessionId = String(options?.sessionId || "").trim() || null;
|
|
271
335
|
const executor = String(options?.executor || "").trim() || null;
|
|
@@ -448,12 +512,8 @@ async function _processResponsesAudioTurn(text) {
|
|
|
448
512
|
});
|
|
449
513
|
|
|
450
514
|
voiceState.value = "thinking";
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
emit("transcript", { text: inputText, final: true });
|
|
454
|
-
} else {
|
|
455
|
-
voiceTranscript.value = "";
|
|
456
|
-
}
|
|
515
|
+
voiceTranscript.value = inputText;
|
|
516
|
+
emit("transcript", { text: inputText, final: true, source: "api" });
|
|
457
517
|
_recordVoiceTranscriptIfNew("user", inputText, "responses-audio.user_input");
|
|
458
518
|
|
|
459
519
|
if (_responsesAbortController) {
|
|
@@ -503,7 +563,7 @@ async function _processResponsesAudioTurn(text) {
|
|
|
503
563
|
_traceEndTurn("turn_end", {
|
|
504
564
|
reason: "responses-audio.turn_completed",
|
|
505
565
|
});
|
|
506
|
-
|
|
566
|
+
_scheduleResponseClear();
|
|
507
567
|
voiceState.value = "listening";
|
|
508
568
|
}
|
|
509
569
|
|
|
@@ -580,6 +640,9 @@ async function _startResponsesAudioSession(tokenData) {
|
|
|
580
640
|
_sessionStartTime = Date.now();
|
|
581
641
|
startDurationTimer();
|
|
582
642
|
voiceState.value = "connected";
|
|
643
|
+
// Start browser SpeechRecognition as parallel/backup transcription
|
|
644
|
+
_apiTranscriptDelivered = false;
|
|
645
|
+
_startBrowserTranscription();
|
|
583
646
|
emit("connected", {
|
|
584
647
|
provider: tokenData?.provider || "openai",
|
|
585
648
|
sessionId: voiceSessionId.value,
|
|
@@ -668,6 +731,26 @@ function _markAssistantToolResponseObserved() {
|
|
|
668
731
|
_clearToolCompletionAckTimer();
|
|
669
732
|
}
|
|
670
733
|
|
|
734
|
+
// ── Response display hold ──────────────────────────────────────────────────
|
|
735
|
+
// Keep assistant response visible in center for RESPONSE_DISPLAY_HOLD_MS
|
|
736
|
+
// after the turn ends, instead of clearing immediately.
|
|
737
|
+
|
|
738
|
+
function _scheduleResponseClear() {
|
|
739
|
+
if (_responseClearTimer) clearTimeout(_responseClearTimer);
|
|
740
|
+
_responseClearTimer = setTimeout(() => {
|
|
741
|
+
_responseClearTimer = null;
|
|
742
|
+
voiceResponse.value = "";
|
|
743
|
+
}, RESPONSE_DISPLAY_HOLD_MS);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function _clearResponseForNewTurn() {
|
|
747
|
+
if (_responseClearTimer) {
|
|
748
|
+
clearTimeout(_responseClearTimer);
|
|
749
|
+
_responseClearTimer = null;
|
|
750
|
+
}
|
|
751
|
+
voiceResponse.value = "";
|
|
752
|
+
}
|
|
753
|
+
|
|
671
754
|
// ── Event System ────────────────────────────────────────────────────────────
|
|
672
755
|
|
|
673
756
|
export function onVoiceEvent(event, handler) {
|
|
@@ -747,18 +830,18 @@ function sendSessionUpdate(tokenData = {}) {
|
|
|
747
830
|
type: turnDetection,
|
|
748
831
|
...(turnDetection === "server_vad"
|
|
749
832
|
? {
|
|
750
|
-
threshold: 0.
|
|
751
|
-
prefix_padding_ms:
|
|
752
|
-
silence_duration_ms:
|
|
833
|
+
threshold: 0.82,
|
|
834
|
+
prefix_padding_ms: 500,
|
|
835
|
+
silence_duration_ms: 1600,
|
|
753
836
|
create_response: true,
|
|
754
|
-
interrupt_response:
|
|
837
|
+
interrupt_response: false,
|
|
755
838
|
}
|
|
756
839
|
: {}),
|
|
757
840
|
...(turnDetection === "semantic_vad"
|
|
758
841
|
? {
|
|
759
842
|
eagerness: "low",
|
|
760
843
|
create_response: true,
|
|
761
|
-
interrupt_response:
|
|
844
|
+
interrupt_response: false,
|
|
762
845
|
}
|
|
763
846
|
: {}),
|
|
764
847
|
};
|
|
@@ -978,6 +1061,10 @@ async function _startWebSocketTransport(tokenData, mediaStream) {
|
|
|
978
1061
|
_sessionStartTime = Date.now();
|
|
979
1062
|
startDurationTimer();
|
|
980
1063
|
|
|
1064
|
+
// Start browser SpeechRecognition as parallel/backup transcription
|
|
1065
|
+
_apiTranscriptDelivered = false;
|
|
1066
|
+
_startBrowserTranscription();
|
|
1067
|
+
|
|
981
1068
|
emit("connected", {
|
|
982
1069
|
provider: tokenData.provider || "azure",
|
|
983
1070
|
sessionId: voiceSessionId.value,
|
|
@@ -1236,6 +1323,9 @@ export async function startVoiceSession(options = {}) {
|
|
|
1236
1323
|
voiceSessionId.value = _callContext.sessionId || `voice-${Date.now()}`;
|
|
1237
1324
|
startDurationTimer();
|
|
1238
1325
|
startReconnectTimer();
|
|
1326
|
+
// Start browser SpeechRecognition as parallel/backup transcription
|
|
1327
|
+
_apiTranscriptDelivered = false;
|
|
1328
|
+
_startBrowserTranscription();
|
|
1239
1329
|
emit("connected", {
|
|
1240
1330
|
provider: tokenData.provider,
|
|
1241
1331
|
sessionId: voiceSessionId.value,
|
|
@@ -1354,6 +1444,7 @@ export function stopVoiceSession() {
|
|
|
1354
1444
|
_explicitStop = true;
|
|
1355
1445
|
emit("session-ending", { sessionId: voiceSessionId.value });
|
|
1356
1446
|
_stopMicLevelMonitor();
|
|
1447
|
+
_stopBrowserTranscription();
|
|
1357
1448
|
cleanup();
|
|
1358
1449
|
voiceState.value = "idle";
|
|
1359
1450
|
voiceTranscript.value = "";
|
|
@@ -1362,6 +1453,8 @@ export function stopVoiceSession() {
|
|
|
1362
1453
|
voiceSessionId.value = null;
|
|
1363
1454
|
voiceBoundSessionId.value = null;
|
|
1364
1455
|
voiceDuration.value = 0;
|
|
1456
|
+
_speechStartedAt = 0;
|
|
1457
|
+
if (_responseClearTimer) { clearTimeout(_responseClearTimer); _responseClearTimer = null; }
|
|
1365
1458
|
_webrtcUnavailableForProvider = false;
|
|
1366
1459
|
_lastTokenData = null;
|
|
1367
1460
|
_callContext = {
|
|
@@ -1386,31 +1479,40 @@ function handleServerEvent(event) {
|
|
|
1386
1479
|
break;
|
|
1387
1480
|
|
|
1388
1481
|
case "input_audio_buffer.speech_started":
|
|
1482
|
+
_speechStartedAt = Date.now();
|
|
1389
1483
|
_traceBeginTurn("turn_start", { reason: type });
|
|
1390
|
-
|
|
1484
|
+
// Clear lingering response so center shows user's new transcript
|
|
1485
|
+
_clearResponseForNewTurn();
|
|
1486
|
+
// Don't interrupt immediately — wait for MIN_SPEECH_DURATION_FOR_INTERRUPT_MS
|
|
1487
|
+
setTimeout(() => {
|
|
1488
|
+
if (_speechStartedAt > 0 && (Date.now() - _speechStartedAt) >= MIN_SPEECH_DURATION_FOR_INTERRUPT_MS) {
|
|
1489
|
+
triggerAutoBargeIn("speech-started-confirmed");
|
|
1490
|
+
}
|
|
1491
|
+
}, MIN_SPEECH_DURATION_FOR_INTERRUPT_MS);
|
|
1391
1492
|
voiceState.value = "listening";
|
|
1392
1493
|
emit("speech-started", {});
|
|
1393
1494
|
break;
|
|
1394
1495
|
|
|
1395
1496
|
case "input_audio_buffer.speech_stopped":
|
|
1497
|
+
_speechStartedAt = 0;
|
|
1396
1498
|
voiceState.value = "thinking";
|
|
1397
1499
|
scheduleManualResponseCreate("speech-stopped");
|
|
1398
1500
|
emit("speech-stopped", {});
|
|
1399
1501
|
break;
|
|
1400
1502
|
|
|
1401
1503
|
case "conversation.item.input_audio_transcription.completed":
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
voiceTranscript.value = "";
|
|
1407
|
-
}
|
|
1504
|
+
// API-level transcript delivered — prefer over browser SpeechRecognition
|
|
1505
|
+
_apiTranscriptDelivered = true;
|
|
1506
|
+
voiceTranscript.value = event.transcript || "";
|
|
1507
|
+
emit("transcript", { text: event.transcript, final: true, source: "api" });
|
|
1408
1508
|
_recordVoiceTranscriptIfNew(
|
|
1409
1509
|
"user",
|
|
1410
1510
|
event.transcript || "",
|
|
1411
1511
|
"conversation.item.input_audio_transcription.completed",
|
|
1412
1512
|
);
|
|
1413
1513
|
scheduleManualResponseCreate("transcription-completed");
|
|
1514
|
+
// Reset for next utterance
|
|
1515
|
+
setTimeout(() => { _apiTranscriptDelivered = false; }, 500);
|
|
1414
1516
|
break;
|
|
1415
1517
|
|
|
1416
1518
|
case "conversation.item.created": {
|
|
@@ -1421,11 +1523,11 @@ function handleServerEvent(event) {
|
|
|
1421
1523
|
.map((part) => String(part?.transcript || part?.text || ""))
|
|
1422
1524
|
.join("")
|
|
1423
1525
|
.trim();
|
|
1424
|
-
if (transcript
|
|
1526
|
+
if (transcript) {
|
|
1527
|
+
_apiTranscriptDelivered = true;
|
|
1425
1528
|
voiceTranscript.value = transcript;
|
|
1426
|
-
emit("transcript", { text: transcript, final: true });
|
|
1427
|
-
|
|
1428
|
-
voiceTranscript.value = "";
|
|
1529
|
+
emit("transcript", { text: transcript, final: true, source: "api" });
|
|
1530
|
+
setTimeout(() => { _apiTranscriptDelivered = false; }, 500);
|
|
1429
1531
|
}
|
|
1430
1532
|
_recordVoiceTranscriptIfNew(
|
|
1431
1533
|
"user",
|
|
@@ -1477,7 +1579,7 @@ function handleServerEvent(event) {
|
|
|
1477
1579
|
"response.audio_transcript.done",
|
|
1478
1580
|
);
|
|
1479
1581
|
_traceEndTurn("turn_end", { reason: type });
|
|
1480
|
-
|
|
1582
|
+
_scheduleResponseClear();
|
|
1481
1583
|
break;
|
|
1482
1584
|
|
|
1483
1585
|
case "response.text.done":
|
|
@@ -1489,7 +1591,7 @@ function handleServerEvent(event) {
|
|
|
1489
1591
|
"response.text.done",
|
|
1490
1592
|
);
|
|
1491
1593
|
_traceEndTurn("turn_end", { reason: type });
|
|
1492
|
-
|
|
1594
|
+
_scheduleResponseClear();
|
|
1493
1595
|
break;
|
|
1494
1596
|
|
|
1495
1597
|
case "response.output_text.done":
|
|
@@ -1501,7 +1603,7 @@ function handleServerEvent(event) {
|
|
|
1501
1603
|
"response.output_text.done",
|
|
1502
1604
|
);
|
|
1503
1605
|
_traceEndTurn("turn_end", { reason: type });
|
|
1504
|
-
|
|
1606
|
+
_scheduleResponseClear();
|
|
1505
1607
|
break;
|
|
1506
1608
|
|
|
1507
1609
|
case "response.audio.delta":
|
|
@@ -1553,7 +1655,7 @@ function handleServerEvent(event) {
|
|
|
1553
1655
|
voiceResponse.value,
|
|
1554
1656
|
"response.done.fallback",
|
|
1555
1657
|
);
|
|
1556
|
-
|
|
1658
|
+
_scheduleResponseClear();
|
|
1557
1659
|
}
|
|
1558
1660
|
if (voiceState.value !== "listening") {
|
|
1559
1661
|
voiceState.value = "connected";
|
|
@@ -1709,6 +1811,13 @@ function fadeElementVolumeTo(el, targetVolume, durationMs) {
|
|
|
1709
1811
|
|
|
1710
1812
|
function triggerAutoBargeIn(reason = "speech-started") {
|
|
1711
1813
|
const now = Date.now();
|
|
1814
|
+
// Only interrupt if speech has been ongoing long enough to be real speech
|
|
1815
|
+
if (_speechStartedAt > 0) {
|
|
1816
|
+
const speechDuration = now - _speechStartedAt;
|
|
1817
|
+
if (speechDuration < MIN_SPEECH_DURATION_FOR_INTERRUPT_MS) {
|
|
1818
|
+
return false;
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1712
1821
|
const audioActive = isAssistantPlaybackActive();
|
|
1713
1822
|
if (!shouldAutoBargeIn({
|
|
1714
1823
|
muted: isVoiceMicMuted.value,
|
package/ui/setup.html
CHANGED
|
@@ -909,6 +909,9 @@ function App() {
|
|
|
909
909
|
const [kanbanBackend, setKanbanBackend] = useState("internal");
|
|
910
910
|
const [telegramToken, setTelegramToken] = useState("");
|
|
911
911
|
const [telegramChatId, setTelegramChatId] = useState("");
|
|
912
|
+
const [telegramDiscoveredChats, setTelegramDiscoveredChats] = useState([]);
|
|
913
|
+
const [telegramChatLookupLoading, setTelegramChatLookupLoading] = useState(false);
|
|
914
|
+
const [telegramChatLookupMessage, setTelegramChatLookupMessage] = useState("");
|
|
912
915
|
const [maxParallel, setMaxParallel] = useState(4);
|
|
913
916
|
const [maxRetries, setMaxRetries] = useState(3);
|
|
914
917
|
const [failoverStrategy, setFailoverStrategy] = useState("next-in-line");
|
|
@@ -2270,6 +2273,42 @@ function App() {
|
|
|
2270
2273
|
if (idx <= step || completedSteps.has(idx)) setStep(idx);
|
|
2271
2274
|
};
|
|
2272
2275
|
|
|
2276
|
+
const discoverTelegramChatIds = async () => {
|
|
2277
|
+
const token = String(telegramToken || "").trim();
|
|
2278
|
+
if (!token) {
|
|
2279
|
+
setTelegramDiscoveredChats([]);
|
|
2280
|
+
setTelegramChatLookupMessage("Enter a Telegram bot token first.");
|
|
2281
|
+
return;
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
setTelegramChatLookupLoading(true);
|
|
2285
|
+
setTelegramChatLookupMessage("");
|
|
2286
|
+
try {
|
|
2287
|
+
const result = await apiPost("telegram-chat-id", { token });
|
|
2288
|
+
if (!result?.ok) {
|
|
2289
|
+
setTelegramDiscoveredChats([]);
|
|
2290
|
+
setTelegramChatLookupMessage(result?.error || "Failed to discover Telegram chats.");
|
|
2291
|
+
return;
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
const chats = Array.isArray(result.chats) ? result.chats : [];
|
|
2295
|
+
setTelegramDiscoveredChats(chats);
|
|
2296
|
+
if (chats.length === 1) {
|
|
2297
|
+
setTelegramChatId(String(chats[0].id));
|
|
2298
|
+
setTelegramChatLookupMessage(`Found 1 chat: ${chats[0].id}`);
|
|
2299
|
+
} else if (chats.length > 1) {
|
|
2300
|
+
setTelegramChatLookupMessage(`Found ${chats.length} chats. Choose the one Bosun should use.`);
|
|
2301
|
+
} else {
|
|
2302
|
+
setTelegramChatLookupMessage(result.message || "No chats found yet. Send a message to your bot, then try again.");
|
|
2303
|
+
}
|
|
2304
|
+
} catch (err) {
|
|
2305
|
+
setTelegramDiscoveredChats([]);
|
|
2306
|
+
setTelegramChatLookupMessage(err.message || "Failed to discover Telegram chats.");
|
|
2307
|
+
} finally {
|
|
2308
|
+
setTelegramChatLookupLoading(false);
|
|
2309
|
+
}
|
|
2310
|
+
};
|
|
2311
|
+
|
|
2273
2312
|
// ── Build EXECUTORS env string ─────────────────────────────────────────────
|
|
2274
2313
|
|
|
2275
2314
|
const buildExecutorsEnv = () =>
|
|
@@ -3645,7 +3684,11 @@ function App() {
|
|
|
3645
3684
|
${telegramEnabled && html`
|
|
3646
3685
|
<div class="form-group">
|
|
3647
3686
|
<label>Telegram Bot Token</label>
|
|
3648
|
-
<input type="password" value=${telegramToken} oninput=${(e) =>
|
|
3687
|
+
<input type="password" value=${telegramToken} oninput=${(e) => {
|
|
3688
|
+
setTelegramToken(e.target.value);
|
|
3689
|
+
setTelegramDiscoveredChats([]);
|
|
3690
|
+
setTelegramChatLookupMessage("");
|
|
3691
|
+
}}
|
|
3649
3692
|
placeholder="123456:ABCdefGHIjklMNO..." />
|
|
3650
3693
|
<div class="hint">Create a bot via <a href="https://t.me/botfather" target="_blank">@BotFather</a>.</div>
|
|
3651
3694
|
</div>
|
|
@@ -3653,7 +3696,30 @@ function App() {
|
|
|
3653
3696
|
<label>Telegram Chat ID</label>
|
|
3654
3697
|
<input type="text" value=${telegramChatId} oninput=${(e) => setTelegramChatId(e.target.value)}
|
|
3655
3698
|
placeholder="-1001234567890" />
|
|
3656
|
-
<div
|
|
3699
|
+
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:8px">
|
|
3700
|
+
<button class="btn" type="button" onclick=${discoverTelegramChatIds}
|
|
3701
|
+
disabled=${telegramChatLookupLoading || !String(telegramToken || "").trim()}>
|
|
3702
|
+
${telegramChatLookupLoading ? "Finding Chats..." : "Discover Chats"}
|
|
3703
|
+
</button>
|
|
3704
|
+
${telegramDiscoveredChats.length > 1 && html`
|
|
3705
|
+
<select
|
|
3706
|
+
value=${telegramDiscoveredChats.some((chat) => String(chat.id) === String(telegramChatId)) ? String(telegramChatId) : ""}
|
|
3707
|
+
onchange=${(e) => setTelegramChatId(e.target.value)}
|
|
3708
|
+
style="min-width:260px;flex:1"
|
|
3709
|
+
>
|
|
3710
|
+
<option value="">Choose a discovered chat</option>
|
|
3711
|
+
${telegramDiscoveredChats.map((chat) => html`
|
|
3712
|
+
<option value=${String(chat.id)}>
|
|
3713
|
+
${`${chat.id}${chat.username ? ` · @${chat.username}` : ""}${chat.title ? ` · ${chat.title}` : ""}${chat.type ? ` · ${chat.type}` : ""}`}
|
|
3714
|
+
</option>
|
|
3715
|
+
`)}
|
|
3716
|
+
</select>
|
|
3717
|
+
`}
|
|
3718
|
+
</div>
|
|
3719
|
+
${telegramChatLookupMessage && html`
|
|
3720
|
+
<div class="hint" style="margin-top:8px">${telegramChatLookupMessage}</div>
|
|
3721
|
+
`}
|
|
3722
|
+
<div class="hint">Use Discover Chats after sending your bot a message, or run <code style="font-family:var(--font-mono)">bosun --get-chat-id</code>.</div>
|
|
3657
3723
|
</div>
|
|
3658
3724
|
`}
|
|
3659
3725
|
<div class="nav-buttons">
|
package/ui/styles/components.css
CHANGED
|
@@ -3001,6 +3001,63 @@ select.input {
|
|
|
3001
3001
|
font-weight: 600;
|
|
3002
3002
|
}
|
|
3003
3003
|
|
|
3004
|
+
.dashboard-retry-list {
|
|
3005
|
+
display: flex;
|
|
3006
|
+
flex-direction: column;
|
|
3007
|
+
gap: 10px;
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
.dashboard-retry-item {
|
|
3011
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
3012
|
+
border-radius: 12px;
|
|
3013
|
+
padding: 10px 12px;
|
|
3014
|
+
background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0.01));
|
|
3015
|
+
display: flex;
|
|
3016
|
+
justify-content: space-between;
|
|
3017
|
+
gap: 12px;
|
|
3018
|
+
align-items: center;
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
.dashboard-retry-main {
|
|
3022
|
+
min-width: 0;
|
|
3023
|
+
flex: 1;
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
.dashboard-retry-task {
|
|
3027
|
+
font-size: 12px;
|
|
3028
|
+
font-weight: 700;
|
|
3029
|
+
color: var(--text-primary);
|
|
3030
|
+
margin-bottom: 4px;
|
|
3031
|
+
}
|
|
3032
|
+
|
|
3033
|
+
.dashboard-retry-task-title {
|
|
3034
|
+
font-size: 11px;
|
|
3035
|
+
color: var(--text-secondary);
|
|
3036
|
+
margin-bottom: 4px;
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
.dashboard-retry-error {
|
|
3040
|
+
font-size: 12px;
|
|
3041
|
+
color: var(--text-secondary);
|
|
3042
|
+
line-height: 1.35;
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
.dashboard-retry-meta {
|
|
3046
|
+
display: flex;
|
|
3047
|
+
align-items: center;
|
|
3048
|
+
gap: 8px;
|
|
3049
|
+
flex-wrap: wrap;
|
|
3050
|
+
justify-content: flex-end;
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
.dashboard-retry-pill {
|
|
3054
|
+
border: 1px solid rgba(99, 102, 241, 0.25);
|
|
3055
|
+
border-radius: 999px;
|
|
3056
|
+
padding: 3px 9px;
|
|
3057
|
+
font-size: 11px;
|
|
3058
|
+
color: var(--text-secondary);
|
|
3059
|
+
}
|
|
3060
|
+
|
|
3004
3061
|
@media (min-width: 600px) {
|
|
3005
3062
|
.dashboard-header {
|
|
3006
3063
|
flex-direction: row;
|
package/ui/styles.css
CHANGED
|
@@ -199,6 +199,207 @@
|
|
|
199
199
|
padding: 0 12px;
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
.wf-palette-backdrop {
|
|
203
|
+
position: absolute;
|
|
204
|
+
inset: 0;
|
|
205
|
+
z-index: 32;
|
|
206
|
+
background: rgba(3, 7, 18, 0.55);
|
|
207
|
+
backdrop-filter: blur(3px);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.wf-palette {
|
|
211
|
+
position: absolute;
|
|
212
|
+
top: 72px;
|
|
213
|
+
left: 50%;
|
|
214
|
+
transform: translateX(-50%);
|
|
215
|
+
width: min(760px, calc(100% - 32px));
|
|
216
|
+
max-height: min(70vh, 720px);
|
|
217
|
+
overflow: hidden;
|
|
218
|
+
background: var(--color-bg, #0d1117);
|
|
219
|
+
border: 1px solid var(--color-border, #2a3040);
|
|
220
|
+
border-radius: 16px;
|
|
221
|
+
padding: 14px;
|
|
222
|
+
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.45);
|
|
223
|
+
display: flex;
|
|
224
|
+
flex-direction: column;
|
|
225
|
+
gap: 12px;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.wf-palette-header {
|
|
229
|
+
display: flex;
|
|
230
|
+
align-items: center;
|
|
231
|
+
gap: 10px;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.wf-palette-title-group {
|
|
235
|
+
flex: 1;
|
|
236
|
+
min-width: 0;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.wf-palette-title {
|
|
240
|
+
font-size: 14px;
|
|
241
|
+
font-weight: 700;
|
|
242
|
+
color: var(--color-text, #fff);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.wf-palette-subtitle {
|
|
246
|
+
font-size: 11px;
|
|
247
|
+
color: var(--color-text-secondary, #9ca3af);
|
|
248
|
+
opacity: 0.85;
|
|
249
|
+
margin-top: 3px;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.wf-palette-hints {
|
|
253
|
+
display: flex;
|
|
254
|
+
align-items: center;
|
|
255
|
+
gap: 8px;
|
|
256
|
+
font-size: 11px;
|
|
257
|
+
opacity: 0.7;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.wf-palette-results {
|
|
261
|
+
display: flex;
|
|
262
|
+
flex-direction: column;
|
|
263
|
+
gap: 8px;
|
|
264
|
+
overflow-y: auto;
|
|
265
|
+
padding-right: 2px;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.wf-node-search-item {
|
|
269
|
+
display: flex;
|
|
270
|
+
flex-direction: column;
|
|
271
|
+
gap: 8px;
|
|
272
|
+
width: 100%;
|
|
273
|
+
text-align: left;
|
|
274
|
+
border: 1px solid var(--color-border, #2a3040);
|
|
275
|
+
border-radius: 12px;
|
|
276
|
+
padding: 12px 14px;
|
|
277
|
+
color: var(--color-text, #fff);
|
|
278
|
+
cursor: pointer;
|
|
279
|
+
background: var(--color-bg-secondary, #131722);
|
|
280
|
+
transition: border 0.15s, background 0.15s;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.wf-node-search-item.active {
|
|
284
|
+
background: rgba(59, 130, 246, 0.12);
|
|
285
|
+
border-color: rgba(59, 130, 246, 0.66);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.wf-node-search-item-top {
|
|
289
|
+
display: flex;
|
|
290
|
+
align-items: center;
|
|
291
|
+
gap: 8px;
|
|
292
|
+
flex-wrap: wrap;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.wf-node-search-label {
|
|
296
|
+
font-weight: 700;
|
|
297
|
+
font-size: 13px;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.wf-node-category-badge {
|
|
301
|
+
display: inline-flex;
|
|
302
|
+
align-items: center;
|
|
303
|
+
padding: 2px 8px;
|
|
304
|
+
border-radius: 999px;
|
|
305
|
+
font-size: 10px;
|
|
306
|
+
letter-spacing: 0.04em;
|
|
307
|
+
text-transform: uppercase;
|
|
308
|
+
border: 1px solid rgba(148, 163, 184, 0.2);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.wf-node-search-type {
|
|
312
|
+
font-size: 11px;
|
|
313
|
+
opacity: 0.55;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.wf-node-search-description {
|
|
317
|
+
font-size: 12px;
|
|
318
|
+
opacity: 0.78;
|
|
319
|
+
line-height: 1.4;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.wf-node-chip-row {
|
|
323
|
+
display: flex;
|
|
324
|
+
gap: 12px;
|
|
325
|
+
flex-wrap: wrap;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.wf-node-chip-group {
|
|
329
|
+
display: flex;
|
|
330
|
+
flex-direction: column;
|
|
331
|
+
gap: 4px;
|
|
332
|
+
min-width: 0;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.wf-node-chip-label {
|
|
336
|
+
font-size: 11px;
|
|
337
|
+
color: var(--color-text-secondary, #9ca3af);
|
|
338
|
+
text-transform: uppercase;
|
|
339
|
+
letter-spacing: 0.08em;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.wf-node-chip-list {
|
|
343
|
+
display: flex;
|
|
344
|
+
flex-wrap: wrap;
|
|
345
|
+
gap: 4px;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.wf-node-chip {
|
|
349
|
+
display: inline-flex;
|
|
350
|
+
align-items: center;
|
|
351
|
+
padding: 2px 8px;
|
|
352
|
+
border-radius: 999px;
|
|
353
|
+
border: 1px solid rgba(148, 163, 184, 0.4);
|
|
354
|
+
font-size: 11px;
|
|
355
|
+
background: var(--color-bg-secondary, #1e293b);
|
|
356
|
+
color: var(--color-text-secondary, #94a3b8);
|
|
357
|
+
letter-spacing: 0.04em;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.wf-node-chip-more {
|
|
361
|
+
background: rgba(59, 130, 246, 0.15);
|
|
362
|
+
border-color: rgba(59, 130, 246, 0.3);
|
|
363
|
+
color: #93c5fd;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.wf-node-chip-fallback {
|
|
367
|
+
opacity: 0.6;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.wf-node-search-empty {
|
|
371
|
+
text-align: center;
|
|
372
|
+
padding: 20px;
|
|
373
|
+
opacity: 0.6;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.wf-shortcuts-grid {
|
|
377
|
+
display: flex;
|
|
378
|
+
flex-direction: column;
|
|
379
|
+
gap: 10px;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.wf-shortcut-row {
|
|
383
|
+
display: flex;
|
|
384
|
+
align-items: flex-start;
|
|
385
|
+
justify-content: space-between;
|
|
386
|
+
gap: 16px;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.wf-shortcut-key {
|
|
390
|
+
font-size: 12px;
|
|
391
|
+
padding: 4px 10px;
|
|
392
|
+
border-radius: 8px;
|
|
393
|
+
background: rgba(148, 163, 184, 0.14);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.wf-shortcut-desc {
|
|
397
|
+
font-size: 13px;
|
|
398
|
+
opacity: 0.82;
|
|
399
|
+
text-align: right;
|
|
400
|
+
flex: 1;
|
|
401
|
+
}
|
|
402
|
+
|
|
202
403
|
.stream-item-details.expanded .stream-item-details-inner {
|
|
203
404
|
padding: 10px 12px 12px;
|
|
204
405
|
}
|
|
@@ -649,4 +850,3 @@
|
|
|
649
850
|
color: var(--text-primary);
|
|
650
851
|
font-size: 12px;
|
|
651
852
|
}
|
|
652
|
-
|