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
package/ui/modules/voice.js
CHANGED
|
@@ -223,41 +223,39 @@ export function useVoiceInput(onTranscript, opts = {}) {
|
|
|
223
223
|
return { listening, supported, start, stop, toggle, error };
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
+
export function requestVoiceModeOpen(detail = {}) {
|
|
227
|
+
try {
|
|
228
|
+
globalThis.dispatchEvent?.(new CustomEvent("ve:open-voice-mode", { detail }));
|
|
229
|
+
} catch {
|
|
230
|
+
// no-op
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
226
234
|
/* ─── VoiceMicButton component ─────────────────────────────────
|
|
227
235
|
*
|
|
228
|
-
*
|
|
229
|
-
* disabled?: boolean,
|
|
230
|
-
* title?: string,
|
|
231
|
-
* className?: string,
|
|
232
|
-
* size?: 'sm' | 'md' }} props
|
|
236
|
+
* In v0.36+, all in-app mic actions open the real voice mode overlay.
|
|
233
237
|
*/
|
|
234
238
|
export function VoiceMicButton({ onTranscript, disabled = false, title, className = "", size = "md" }) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
haptic("light");
|
|
238
|
-
onTranscript(text);
|
|
239
|
-
},
|
|
240
|
-
{},
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
// Inject styles on first render
|
|
239
|
+
// Legacy callback kept for backward compatibility; real voice mode owns transcript flow.
|
|
240
|
+
void onTranscript;
|
|
244
241
|
useEffect(() => { injectVoiceStyles(); }, []);
|
|
245
242
|
|
|
246
|
-
if (!supported) return null;
|
|
247
|
-
|
|
248
243
|
const sizeClass = size === "sm" ? "mic-btn-sm" : "";
|
|
249
244
|
|
|
250
245
|
return html`
|
|
251
246
|
<button
|
|
252
247
|
type="button"
|
|
253
|
-
class="mic-btn ${sizeClass} ${
|
|
248
|
+
class="mic-btn ${sizeClass} ${className}"
|
|
254
249
|
disabled=${disabled}
|
|
255
|
-
onClick=${() => {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
250
|
+
onClick=${() => {
|
|
251
|
+
haptic("light");
|
|
252
|
+
requestVoiceModeOpen();
|
|
253
|
+
}}
|
|
254
|
+
title=${title || "Live voice mode"}
|
|
255
|
+
aria-label="Open live voice mode"
|
|
256
|
+
aria-pressed="false"
|
|
259
257
|
>
|
|
260
|
-
${resolveIcon(
|
|
258
|
+
${resolveIcon(":mic:")}
|
|
261
259
|
</button>
|
|
262
260
|
`;
|
|
263
261
|
}
|
|
@@ -266,29 +264,24 @@ export function VoiceMicButton({ onTranscript, disabled = false, title, classNam
|
|
|
266
264
|
* Positioned absolutely inside a .input-with-mic or .textarea-with-mic wrapper.
|
|
267
265
|
*/
|
|
268
266
|
export function VoiceMicButtonInline({ onTranscript, disabled = false }) {
|
|
269
|
-
|
|
270
|
-
(text) => {
|
|
271
|
-
haptic("light");
|
|
272
|
-
onTranscript(text);
|
|
273
|
-
},
|
|
274
|
-
{},
|
|
275
|
-
);
|
|
276
|
-
|
|
267
|
+
void onTranscript;
|
|
277
268
|
useEffect(() => { injectVoiceStyles(); }, []);
|
|
278
269
|
|
|
279
|
-
if (!supported) return null;
|
|
280
|
-
|
|
281
270
|
return html`
|
|
282
271
|
<button
|
|
283
272
|
type="button"
|
|
284
|
-
class="mic-btn mic-btn-sm mic-btn-inline
|
|
273
|
+
class="mic-btn mic-btn-sm mic-btn-inline"
|
|
285
274
|
disabled=${disabled}
|
|
286
|
-
onClick=${(e) => {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
275
|
+
onClick=${(e) => {
|
|
276
|
+
e.stopPropagation();
|
|
277
|
+
haptic("light");
|
|
278
|
+
requestVoiceModeOpen();
|
|
279
|
+
}}
|
|
280
|
+
title="Live voice mode"
|
|
281
|
+
aria-label="Open live voice mode"
|
|
282
|
+
aria-pressed="false"
|
|
290
283
|
>
|
|
291
|
-
${resolveIcon(
|
|
284
|
+
${resolveIcon(":mic:")}
|
|
292
285
|
</button>
|
|
293
286
|
`;
|
|
294
287
|
}
|
package/ui/setup.html
CHANGED
|
@@ -344,6 +344,28 @@
|
|
|
344
344
|
.success-banner h2 { color: var(--success); margin-bottom: 8px; }
|
|
345
345
|
.success-banner p { color: var(--text-secondary); font-size: 0.9rem; }
|
|
346
346
|
|
|
347
|
+
/* ── Inline icon helpers (shared with iconText output) ───────────── */
|
|
348
|
+
.icon-text {
|
|
349
|
+
display: inline-flex;
|
|
350
|
+
align-items: center;
|
|
351
|
+
gap: 0.35em;
|
|
352
|
+
flex-wrap: wrap;
|
|
353
|
+
}
|
|
354
|
+
.icon-inline {
|
|
355
|
+
display: inline-flex;
|
|
356
|
+
align-items: center;
|
|
357
|
+
justify-content: center;
|
|
358
|
+
width: 1em;
|
|
359
|
+
height: 1em;
|
|
360
|
+
line-height: 1;
|
|
361
|
+
vertical-align: middle;
|
|
362
|
+
}
|
|
363
|
+
.icon-inline svg {
|
|
364
|
+
width: 1em;
|
|
365
|
+
height: 1em;
|
|
366
|
+
display: block;
|
|
367
|
+
}
|
|
368
|
+
|
|
347
369
|
/* ── Profile Cards ───────────────────────────────────────────────── */
|
|
348
370
|
.profile-cards {
|
|
349
371
|
display: grid; grid-template-columns: 1fr 1fr; gap: 12px;
|
|
@@ -489,6 +511,7 @@
|
|
|
489
511
|
import { h, render } from "preact";
|
|
490
512
|
import { useState, useEffect, useRef } from "preact/hooks";
|
|
491
513
|
import htm from "htm";
|
|
514
|
+
import { iconText } from "./modules/icon-utils.js";
|
|
492
515
|
|
|
493
516
|
const html = htm.bind(h);
|
|
494
517
|
|
|
@@ -506,15 +529,15 @@ const apiPost = (path, body) => api(path, { method: "POST", body: JSON.stringify
|
|
|
506
529
|
// ── Steps definition ─────────────────────────────────────────────────────────
|
|
507
530
|
|
|
508
531
|
const STEPS = [
|
|
509
|
-
{ id: "prerequisites", label: "Prerequisites", icon: "
|
|
510
|
-
{ id: "profile", label: "Profile", icon: "
|
|
511
|
-
{ id: "executors", label: "AI Executors", icon: "
|
|
512
|
-
{ id: "repos", label: "Repositories", icon: "
|
|
513
|
-
{ id: "kanban", label: "Task Mgmt", icon: "
|
|
514
|
-
{ id: "workflows", label: "Workflows", icon: "
|
|
515
|
-
{ id: "notifications", label: "Notifications", icon: "
|
|
516
|
-
{ id: "advanced", label: "Advanced", icon: "
|
|
517
|
-
{ id: "review", label: "Review", icon: "
|
|
532
|
+
{ id: "prerequisites", label: "Prerequisites", icon: ":check:" },
|
|
533
|
+
{ id: "profile", label: "Profile", icon: ":user:" },
|
|
534
|
+
{ id: "executors", label: "AI Executors", icon: ":bot:" },
|
|
535
|
+
{ id: "repos", label: "Repositories", icon: ":folder:" },
|
|
536
|
+
{ id: "kanban", label: "Task Mgmt", icon: ":clipboard:" },
|
|
537
|
+
{ id: "workflows", label: "Workflows", icon: ":compass:" },
|
|
538
|
+
{ id: "notifications", label: "Notifications", icon: ":bell:" },
|
|
539
|
+
{ id: "advanced", label: "Advanced", icon: ":settings:" },
|
|
540
|
+
{ id: "review", label: "Review", icon: ":rocket:" },
|
|
518
541
|
];
|
|
519
542
|
|
|
520
543
|
// ── SelectWithCustom component ───────────────────────────────────────────────
|
|
@@ -679,6 +702,19 @@ function App() {
|
|
|
679
702
|
const [whatsappEnabled, setWhatsappEnabled] = useState(false);
|
|
680
703
|
const [telegramIntervalMin, setTelegramIntervalMin] = useState(10);
|
|
681
704
|
const [orchestratorScript, setOrchestratorScript] = useState("");
|
|
705
|
+
// Voice assistant
|
|
706
|
+
const [voiceEnabled, setVoiceEnabled] = useState(true);
|
|
707
|
+
const [voiceProvider, setVoiceProvider] = useState("auto");
|
|
708
|
+
const [voiceModel, setVoiceModel] = useState("gpt-4o-realtime-preview-2024-12-17");
|
|
709
|
+
const [voiceVisionModel, setVoiceVisionModel] = useState("gpt-4.1-mini");
|
|
710
|
+
const [voiceId, setVoiceId] = useState("alloy");
|
|
711
|
+
const [voiceTurnDetection, setVoiceTurnDetection] = useState("server_vad");
|
|
712
|
+
const [voiceFallbackMode, setVoiceFallbackMode] = useState("browser");
|
|
713
|
+
const [voiceDelegateExecutor, setVoiceDelegateExecutor] = useState("codex-sdk");
|
|
714
|
+
const [openaiRealtimeApiKey, setOpenaiRealtimeApiKey] = useState("");
|
|
715
|
+
const [azureOpenaiRealtimeEndpoint, setAzureOpenaiRealtimeEndpoint] = useState("");
|
|
716
|
+
const [azureOpenaiRealtimeApiKey, setAzureOpenaiRealtimeApiKey] = useState("");
|
|
717
|
+
const [azureOpenaiRealtimeDeployment, setAzureOpenaiRealtimeDeployment] = useState("gpt-4o-realtime-preview");
|
|
682
718
|
|
|
683
719
|
const getWorkflowProfileById = (profileId, profileList = workflowProfiles) =>
|
|
684
720
|
(profileList || []).find((profile_) => profile_.id === profileId) || null;
|
|
@@ -866,6 +902,18 @@ function App() {
|
|
|
866
902
|
}
|
|
867
903
|
if (d.bosunHome) setBosunHome(d.bosunHome);
|
|
868
904
|
if (d.workspacesDir) setWorkspacesDir(d.workspacesDir);
|
|
905
|
+
if (d.voiceEnabled !== undefined) { setVoiceEnabled(d.voiceEnabled !== false); }
|
|
906
|
+
if (d.voiceProvider) { setVoiceProvider(d.voiceProvider); }
|
|
907
|
+
if (d.voiceModel) { setVoiceModel(d.voiceModel); }
|
|
908
|
+
if (d.voiceVisionModel) { setVoiceVisionModel(d.voiceVisionModel); }
|
|
909
|
+
if (d.voiceId) { setVoiceId(d.voiceId); }
|
|
910
|
+
if (d.voiceTurnDetection) { setVoiceTurnDetection(d.voiceTurnDetection); }
|
|
911
|
+
if (d.voiceFallbackMode) { setVoiceFallbackMode(d.voiceFallbackMode); }
|
|
912
|
+
if (d.voiceDelegateExecutor) { setVoiceDelegateExecutor(d.voiceDelegateExecutor); }
|
|
913
|
+
if (d.openaiRealtimeApiKey) { setOpenaiRealtimeApiKey(d.openaiRealtimeApiKey); }
|
|
914
|
+
if (d.azureOpenaiRealtimeEndpoint) { setAzureOpenaiRealtimeEndpoint(d.azureOpenaiRealtimeEndpoint); }
|
|
915
|
+
if (d.azureOpenaiRealtimeApiKey) { setAzureOpenaiRealtimeApiKey(d.azureOpenaiRealtimeApiKey); }
|
|
916
|
+
if (d.azureOpenaiRealtimeDeployment) { setAzureOpenaiRealtimeDeployment(d.azureOpenaiRealtimeDeployment); }
|
|
869
917
|
|
|
870
918
|
// Pre-fill repos from existing config (slugs preferred)
|
|
871
919
|
if (statusData.existingConfig?.repos?.length) {
|
|
@@ -877,6 +925,19 @@ function App() {
|
|
|
877
925
|
// ── Pre-fill from existing .env ──────────────────────────────────────
|
|
878
926
|
const env = statusData.existingEnv || {};
|
|
879
927
|
let envLoaded = false;
|
|
928
|
+
const existingVoice = statusData.existingConfig?.voice || {};
|
|
929
|
+
if (existingVoice.enabled != null) { setVoiceEnabled(existingVoice.enabled !== false); envLoaded = true; }
|
|
930
|
+
if (existingVoice.provider) { setVoiceProvider(String(existingVoice.provider)); envLoaded = true; }
|
|
931
|
+
if (existingVoice.model) { setVoiceModel(String(existingVoice.model)); envLoaded = true; }
|
|
932
|
+
if (existingVoice.visionModel) { setVoiceVisionModel(String(existingVoice.visionModel)); envLoaded = true; }
|
|
933
|
+
if (existingVoice.voiceId) { setVoiceId(String(existingVoice.voiceId)); envLoaded = true; }
|
|
934
|
+
if (existingVoice.turnDetection) { setVoiceTurnDetection(String(existingVoice.turnDetection)); envLoaded = true; }
|
|
935
|
+
if (existingVoice.fallbackMode) { setVoiceFallbackMode(String(existingVoice.fallbackMode)); envLoaded = true; }
|
|
936
|
+
if (existingVoice.delegateExecutor) { setVoiceDelegateExecutor(String(existingVoice.delegateExecutor)); envLoaded = true; }
|
|
937
|
+
if (existingVoice.openaiApiKey) { setOpenaiRealtimeApiKey(String(existingVoice.openaiApiKey)); envLoaded = true; }
|
|
938
|
+
if (existingVoice.azureEndpoint) { setAzureOpenaiRealtimeEndpoint(String(existingVoice.azureEndpoint)); envLoaded = true; }
|
|
939
|
+
if (existingVoice.azureApiKey) { setAzureOpenaiRealtimeApiKey(String(existingVoice.azureApiKey)); envLoaded = true; }
|
|
940
|
+
if (existingVoice.azureDeployment) { setAzureOpenaiRealtimeDeployment(String(existingVoice.azureDeployment)); envLoaded = true; }
|
|
880
941
|
if (env.BOSUN_HOME) { setBosunHome(env.BOSUN_HOME); envLoaded = true; }
|
|
881
942
|
if (env.BOSUN_WORKSPACES_DIR) { setWorkspacesDir(env.BOSUN_WORKSPACES_DIR); setWorkspacesDirCustomized(true); envLoaded = true; }
|
|
882
943
|
if (env.PROJECT_NAME) { setProjectName(env.PROJECT_NAME); envLoaded = true; }
|
|
@@ -981,13 +1042,29 @@ function App() {
|
|
|
981
1042
|
if (env.WHATSAPP_ENABLED) { setWhatsappEnabled(env.WHATSAPP_ENABLED === "true"); envLoaded = true; }
|
|
982
1043
|
if (env.TELEGRAM_INTERVAL_MIN) { setTelegramIntervalMin(Number(env.TELEGRAM_INTERVAL_MIN) || 10); envLoaded = true; }
|
|
983
1044
|
if (env.ORCHESTRATOR_SCRIPT) { setOrchestratorScript(env.ORCHESTRATOR_SCRIPT); envLoaded = true; }
|
|
1045
|
+
// Voice settings
|
|
1046
|
+
if (env.VOICE_ENABLED !== undefined) { setVoiceEnabled(env.VOICE_ENABLED !== "false"); envLoaded = true; }
|
|
1047
|
+
if (env.VOICE_PROVIDER) { setVoiceProvider(env.VOICE_PROVIDER); envLoaded = true; }
|
|
1048
|
+
if (env.VOICE_MODEL) { setVoiceModel(env.VOICE_MODEL); envLoaded = true; }
|
|
1049
|
+
if (env.VOICE_VISION_MODEL) { setVoiceVisionModel(env.VOICE_VISION_MODEL); envLoaded = true; }
|
|
1050
|
+
if (env.VOICE_ID) { setVoiceId(env.VOICE_ID); envLoaded = true; }
|
|
1051
|
+
if (env.VOICE_TURN_DETECTION) { setVoiceTurnDetection(env.VOICE_TURN_DETECTION); envLoaded = true; }
|
|
1052
|
+
if (env.VOICE_FALLBACK_MODE) { setVoiceFallbackMode(env.VOICE_FALLBACK_MODE); envLoaded = true; }
|
|
1053
|
+
if (env.VOICE_DELEGATE_EXECUTOR) { setVoiceDelegateExecutor(env.VOICE_DELEGATE_EXECUTOR); envLoaded = true; }
|
|
1054
|
+
if (env.OPENAI_REALTIME_API_KEY) { setOpenaiRealtimeApiKey(env.OPENAI_REALTIME_API_KEY); envLoaded = true; }
|
|
1055
|
+
if (env.AZURE_OPENAI_REALTIME_ENDPOINT) { setAzureOpenaiRealtimeEndpoint(env.AZURE_OPENAI_REALTIME_ENDPOINT); envLoaded = true; }
|
|
1056
|
+
if (env.AZURE_OPENAI_REALTIME_API_KEY) { setAzureOpenaiRealtimeApiKey(env.AZURE_OPENAI_REALTIME_API_KEY); envLoaded = true; }
|
|
1057
|
+
if (env.AZURE_OPENAI_REALTIME_DEPLOYMENT) { setAzureOpenaiRealtimeDeployment(env.AZURE_OPENAI_REALTIME_DEPLOYMENT); envLoaded = true; }
|
|
984
1058
|
// Multi-workspace: load workspaces[] from existing config
|
|
985
|
-
if (statusData.existingConfig?.workspaces?.length >
|
|
1059
|
+
if (statusData.existingConfig?.workspaces?.length > 0) {
|
|
986
1060
|
setMultiWorkspaceEnabled(true);
|
|
987
1061
|
setWorkspaces(statusData.existingConfig.workspaces.map((ws) => ({
|
|
988
1062
|
id: ws.id || Date.now() + Math.random(),
|
|
989
1063
|
name: ws.name || "",
|
|
990
|
-
repos: (ws.repos || []).map((r) =>
|
|
1064
|
+
repos: (ws.repos || []).map((r) => {
|
|
1065
|
+
if (typeof r === "string") return r;
|
|
1066
|
+
return r.slug || r.url || r.name || r.path || "";
|
|
1067
|
+
}),
|
|
991
1068
|
})));
|
|
992
1069
|
envLoaded = true;
|
|
993
1070
|
}
|
|
@@ -1148,6 +1225,24 @@ function App() {
|
|
|
1148
1225
|
};
|
|
1149
1226
|
});
|
|
1150
1227
|
|
|
1228
|
+
const buildRepoConfigEntry = (rawRepo, index = 0) => {
|
|
1229
|
+
const value = String(rawRepo || "").trim();
|
|
1230
|
+
if (!value) return null;
|
|
1231
|
+
const slugMatch = value.match(/^([a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+)$/);
|
|
1232
|
+
const slug = slugMatch ? slugMatch[1] : "";
|
|
1233
|
+
const urlMatch = value.match(/^https?:\/\/github\.com\/([a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+)(?:\.git)?$/i);
|
|
1234
|
+
const resolvedSlug = slug || (urlMatch ? urlMatch[1] : "");
|
|
1235
|
+
const name = (resolvedSlug.split("/").pop() || value.split("/").pop() || `repo-${index + 1}`)
|
|
1236
|
+
.replace(/\.git$/i, "")
|
|
1237
|
+
.trim();
|
|
1238
|
+
return {
|
|
1239
|
+
name,
|
|
1240
|
+
slug: resolvedSlug,
|
|
1241
|
+
url: resolvedSlug ? `https://github.com/${resolvedSlug}.git` : value,
|
|
1242
|
+
primary: index === 0,
|
|
1243
|
+
};
|
|
1244
|
+
};
|
|
1245
|
+
|
|
1151
1246
|
// ── Apply config ───────────────────────────────────────────────────────────
|
|
1152
1247
|
|
|
1153
1248
|
const handleApply = async () => {
|
|
@@ -1214,6 +1309,19 @@ function App() {
|
|
|
1214
1309
|
copilotEnableAskUser,
|
|
1215
1310
|
copilotEnableAllMcpTools,
|
|
1216
1311
|
copilotMcpConfig,
|
|
1312
|
+
// Voice assistant
|
|
1313
|
+
voiceEnabled,
|
|
1314
|
+
voiceProvider,
|
|
1315
|
+
voiceModel,
|
|
1316
|
+
voiceVisionModel,
|
|
1317
|
+
voiceId,
|
|
1318
|
+
voiceTurnDetection,
|
|
1319
|
+
voiceFallbackMode,
|
|
1320
|
+
voiceDelegateExecutor,
|
|
1321
|
+
openaiRealtimeApiKey,
|
|
1322
|
+
azureOpenaiRealtimeEndpoint,
|
|
1323
|
+
azureOpenaiRealtimeApiKey,
|
|
1324
|
+
azureOpenaiRealtimeDeployment,
|
|
1217
1325
|
// Infrastructure
|
|
1218
1326
|
containerEnabled,
|
|
1219
1327
|
containerRuntime,
|
|
@@ -1228,7 +1336,9 @@ function App() {
|
|
|
1228
1336
|
bosunHome,
|
|
1229
1337
|
workspacesDir,
|
|
1230
1338
|
executors: buildExecutorsConfig(),
|
|
1231
|
-
repos: filteredRepos
|
|
1339
|
+
repos: filteredRepos
|
|
1340
|
+
.map((r, idx) => buildRepoConfigEntry(r, idx))
|
|
1341
|
+
.filter(Boolean),
|
|
1232
1342
|
kanban: { backend: kanbanBackend, syncPolicy: kanbanSyncPolicy },
|
|
1233
1343
|
failover: {
|
|
1234
1344
|
strategy: failoverStrategy,
|
|
@@ -1261,7 +1371,9 @@ function App() {
|
|
|
1261
1371
|
? workspaces.filter((ws) => ws.name.trim()).map((ws) => ({
|
|
1262
1372
|
id: ws.id,
|
|
1263
1373
|
name: ws.name,
|
|
1264
|
-
repos: (ws.repos || [])
|
|
1374
|
+
repos: (ws.repos || [])
|
|
1375
|
+
.map((r, idx) => buildRepoConfigEntry(r, idx))
|
|
1376
|
+
.filter(Boolean),
|
|
1265
1377
|
}))
|
|
1266
1378
|
: undefined,
|
|
1267
1379
|
},
|
|
@@ -1506,7 +1618,7 @@ function App() {
|
|
|
1506
1618
|
<div class="setup-container">
|
|
1507
1619
|
<div class="step-panel">
|
|
1508
1620
|
<div class="success-banner">
|
|
1509
|
-
<div class="icon"
|
|
1621
|
+
<div class="icon">${iconText(":star:")}</div>
|
|
1510
1622
|
<h2>Setup Complete!</h2>
|
|
1511
1623
|
<p>Bosun is configured and ready to go.</p>
|
|
1512
1624
|
<p style="margin-top:12px;font-size:0.8rem;color:var(--text-dim)">
|
|
@@ -1568,10 +1680,10 @@ function App() {
|
|
|
1568
1680
|
<ul class="prereq-list">
|
|
1569
1681
|
${items.map((item) => {
|
|
1570
1682
|
const statusClass = item.installed ? "ok" : item.required ? "fail" : "warn";
|
|
1571
|
-
const icon = item.installed ? "
|
|
1683
|
+
const icon = item.installed ? ":check:" : item.required ? ":close:" : ":alert:";
|
|
1572
1684
|
return html`
|
|
1573
1685
|
<li class="prereq-item ${statusClass}">
|
|
1574
|
-
<span class="icon">${icon}</span>
|
|
1686
|
+
<span class="icon">${iconText(icon)}</span>
|
|
1575
1687
|
<span class="name">${item.label}${!item.required ? " (optional)" : ""}</span>
|
|
1576
1688
|
<span class="version">${item.version || (item.installed ? "found" : "not found")}</span>
|
|
1577
1689
|
</li>
|
|
@@ -1579,7 +1691,7 @@ function App() {
|
|
|
1579
1691
|
})}
|
|
1580
1692
|
${prereqs.gh && !prereqs.gh.authenticated && prereqs.gh.installed ? html`
|
|
1581
1693
|
<li class="prereq-item warn">
|
|
1582
|
-
<span class="icon"
|
|
1694
|
+
<span class="icon">${iconText(":alert:")}</span>
|
|
1583
1695
|
<span class="name">GitHub CLI not authenticated</span>
|
|
1584
1696
|
<span class="version">Run: gh auth login</span>
|
|
1585
1697
|
</li>
|
|
@@ -1630,12 +1742,12 @@ function App() {
|
|
|
1630
1742
|
|
|
1631
1743
|
<div class="profile-cards">
|
|
1632
1744
|
<div class="profile-card ${profile === "standard" ? "selected" : ""}" onclick=${() => setProfile("standard")}>
|
|
1633
|
-
<div class="icon"
|
|
1745
|
+
<div class="icon">${iconText(":zap:")}</div>
|
|
1634
1746
|
<h4>Standard</h4>
|
|
1635
1747
|
<p>Sensible defaults with primary & backup executors. Best for most users.</p>
|
|
1636
1748
|
</div>
|
|
1637
1749
|
<div class="profile-card ${profile === "advanced" ? "selected" : ""}" onclick=${() => setProfile("advanced")}>
|
|
1638
|
-
<div class="icon"
|
|
1750
|
+
<div class="icon">${iconText(":settings:")}</div>
|
|
1639
1751
|
<h4>Advanced</h4>
|
|
1640
1752
|
<p>Full control over executors, failover, distribution weights, and all settings.</p>
|
|
1641
1753
|
</div>
|
|
@@ -1665,7 +1777,7 @@ function App() {
|
|
|
1665
1777
|
<p class="step-desc">Configure which AI coding agents bosun will use. You can add multiple executors with weighted distribution.</p>
|
|
1666
1778
|
|
|
1667
1779
|
<div style="background:rgba(99,102,241,.08);border:1px solid rgba(99,102,241,.25);border-radius:var(--radius-sm);padding:12px 16px;margin-bottom:16px;font-size:0.8rem;line-height:1.6">
|
|
1668
|
-
<strong
|
|
1780
|
+
<strong>${iconText(":lock: No API key required in most cases.")}</strong>
|
|
1669
1781
|
GitHub Copilot, Codex CLI, and Claude Code all support OAuth — just run
|
|
1670
1782
|
<code style="font-family:var(--font-mono);color:var(--accent-light)">gh auth login</code>,
|
|
1671
1783
|
<code style="font-family:var(--font-mono);color:var(--accent-light)">codex auth login</code>, or
|
|
@@ -1685,10 +1797,11 @@ function App() {
|
|
|
1685
1797
|
? configuredModelOptions
|
|
1686
1798
|
: getModelsForExecutor(ex.executor);
|
|
1687
1799
|
const authMode = ex.authMode || "oauth";
|
|
1800
|
+
const executorHeading = `${ex.enabled === false ? ":dot:" : ex.role === "primary" ? ":dot:" : ":dot:"} Executor ${i + 1}: ${ex.name}`;
|
|
1688
1801
|
return html`
|
|
1689
1802
|
<div class="executor-card">
|
|
1690
1803
|
<div class="executor-card-header">
|
|
1691
|
-
<h4>${
|
|
1804
|
+
<h4>${iconText(executorHeading)}</h4>
|
|
1692
1805
|
${executors.length > 1 && html`
|
|
1693
1806
|
<button class="btn btn-sm btn-danger" onclick=${() => removeExecutor(i)}>Remove</button>
|
|
1694
1807
|
`}
|
|
@@ -1773,7 +1886,7 @@ function App() {
|
|
|
1773
1886
|
return html`
|
|
1774
1887
|
<div class="connection-card">
|
|
1775
1888
|
<div class="connection-card-header">
|
|
1776
|
-
<span class="connection-label">${isPrimary ? "
|
|
1889
|
+
<span class="connection-label">${iconText(`${isPrimary ? ":star: Primary" : `:globe: Profile ${ci + 1}`}: ${conn.name || (isPrimary ? "default" : "(unnamed)")}`)}</span>
|
|
1777
1890
|
<button class="btn btn-sm btn-danger"
|
|
1778
1891
|
onclick=${() => removeConnection(i, ci)}>✕ Remove</button>
|
|
1779
1892
|
</div>
|
|
@@ -1920,7 +2033,7 @@ function App() {
|
|
|
1920
2033
|
|
|
1921
2034
|
${!multiWorkspaceEnabled && html`
|
|
1922
2035
|
<div style="background:var(--bg-input);border:1px solid var(--border-primary);border-radius:var(--radius-sm);padding:10px 14px;margin-bottom:16px;font-size:0.78rem;color:var(--text-secondary)">
|
|
1923
|
-
|
|
2036
|
+
${iconText(":folder: Repos will be cloned into:")}
|
|
1924
2037
|
<code style="font-family:var(--font-mono);color:var(--accent-light);margin-left:4px">${cloneRoot}/${"<repo-name>"}</code>
|
|
1925
2038
|
</div>
|
|
1926
2039
|
`}
|
|
@@ -2186,12 +2299,12 @@ function App() {
|
|
|
2186
2299
|
${/* ── GitHub App installation callout ── */ ""}
|
|
2187
2300
|
<div style="background:rgba(56,139,253,.07);border:1px solid rgba(56,139,253,.25);border-radius:var(--radius-sm);padding:14px 16px;margin-bottom:20px">
|
|
2188
2301
|
<div style="display:flex;align-items:flex-start;gap:10px">
|
|
2189
|
-
<span style="font-size:1.3em;line-height:1.2;flex-shrink:0"
|
|
2302
|
+
<span style="font-size:1.3em;line-height:1.2;flex-shrink:0">${iconText(":box:")}</span>
|
|
2190
2303
|
<div style="flex:1;min-width:0">
|
|
2191
2304
|
<div style="font-weight:600;font-size:0.9rem;color:var(--accent-light);margin-bottom:4px">GitHub App — Bosun[VE]</div>
|
|
2192
2305
|
${oauthStatus === "received"
|
|
2193
2306
|
? html`<div style="color:#4ade80;font-size:0.82rem">
|
|
2194
|
-
|
|
2307
|
+
${iconText(":check: GitHub App authorized!")}
|
|
2195
2308
|
${oauthInstallationId ? html` (Installation <code style="font-family:var(--font-mono)">${oauthInstallationId}</code>)` : null}
|
|
2196
2309
|
</div>`
|
|
2197
2310
|
: html`<div style="font-size:0.82rem;color:var(--text-secondary);line-height:1.5">
|
|
@@ -2201,7 +2314,7 @@ function App() {
|
|
|
2201
2314
|
<div style="margin-top:8px;display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
|
2202
2315
|
<a href="https://github.com/apps/bosun-ve" target="_blank" rel="noopener"
|
|
2203
2316
|
style="display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:var(--accent);color:#fff;border-radius:5px;font-size:0.8rem;font-weight:600;text-decoration:none">
|
|
2204
|
-
|
|
2317
|
+
${iconText(":link: Install from GitHub Marketplace")}
|
|
2205
2318
|
</a>
|
|
2206
2319
|
<span style="font-size:0.75rem;color:var(--text-dim)">
|
|
2207
2320
|
<span class="spinner" style="width:10px;height:10px;border-width:2px;vertical-align:middle;margin-right:4px"></span>
|
|
@@ -2253,7 +2366,7 @@ function App() {
|
|
|
2253
2366
|
<div style="border:1px solid var(--border-primary);border-radius:var(--radius-sm);margin-bottom:10px;overflow:hidden">
|
|
2254
2367
|
<div style="display:flex;align-items:center;justify-content:space-between;padding:12px 16px;cursor:pointer;background:var(--bg-input);user-select:none"
|
|
2255
2368
|
onclick=${() => setOpen((v) => !v)}>
|
|
2256
|
-
<span style="font-weight:600;font-size:0.88rem">${title}</span>
|
|
2369
|
+
<span style="font-weight:600;font-size:0.88rem">${iconText(title)}</span>
|
|
2257
2370
|
<span style="color:var(--text-dim);font-size:0.8rem">${open ? "▲" : "▼"}</span>
|
|
2258
2371
|
</div>
|
|
2259
2372
|
${open && html`<div style="padding:16px">${children}</div>`}
|
|
@@ -2265,7 +2378,7 @@ function App() {
|
|
|
2265
2378
|
<h2>Advanced Settings</h2>
|
|
2266
2379
|
<p class="step-desc">Fine-tune execution, model profiles, and infrastructure. Defaults are sensible — only change what you need.</p>
|
|
2267
2380
|
|
|
2268
|
-
<${Section} title="
|
|
2381
|
+
<${Section} title=":zap: Execution & Distribution" defaultOpen=${true}>
|
|
2269
2382
|
<div class="form-group">
|
|
2270
2383
|
<label>Executor Mode</label>
|
|
2271
2384
|
<select value=${executorMode} onchange=${(e) => setExecutorMode(e.target.value)}>
|
|
@@ -2343,7 +2456,7 @@ function App() {
|
|
|
2343
2456
|
`}
|
|
2344
2457
|
<//>
|
|
2345
2458
|
|
|
2346
|
-
<${Section} title="
|
|
2459
|
+
<${Section} title=":git: Failover">
|
|
2347
2460
|
<div class="form-group">
|
|
2348
2461
|
<label>Failover Strategy</label>
|
|
2349
2462
|
<select value=${failoverStrategy} onchange=${(e) => setFailoverStrategy(e.target.value)}>
|
|
@@ -2372,7 +2485,7 @@ function App() {
|
|
|
2372
2485
|
</div>
|
|
2373
2486
|
<//>
|
|
2374
2487
|
|
|
2375
|
-
<${Section} title="
|
|
2488
|
+
<${Section} title=":compass: Workflow Engine">
|
|
2376
2489
|
<div class="executor-grid">
|
|
2377
2490
|
<div class="form-group">
|
|
2378
2491
|
<label>Default Workflow Profile</label>
|
|
@@ -2422,7 +2535,7 @@ function App() {
|
|
|
2422
2535
|
</div>
|
|
2423
2536
|
<//>
|
|
2424
2537
|
|
|
2425
|
-
<${Section} title="
|
|
2538
|
+
<${Section} title=":dot: Codex Settings">
|
|
2426
2539
|
<div class="executor-grid">
|
|
2427
2540
|
<div class="form-group">
|
|
2428
2541
|
<label>Active Model Profile</label>
|
|
@@ -2509,7 +2622,7 @@ function App() {
|
|
|
2509
2622
|
</div>
|
|
2510
2623
|
<//>
|
|
2511
2624
|
|
|
2512
|
-
<${Section} title="
|
|
2625
|
+
<${Section} title=":dot: Copilot Settings">
|
|
2513
2626
|
<div class="executor-grid">
|
|
2514
2627
|
<div class="form-group">
|
|
2515
2628
|
<label>Max Requests per Session</label>
|
|
@@ -2569,7 +2682,136 @@ function App() {
|
|
|
2569
2682
|
</div>
|
|
2570
2683
|
<//>
|
|
2571
2684
|
|
|
2572
|
-
<${Section} title="
|
|
2685
|
+
<${Section} title=":mic: Voice Assistant">
|
|
2686
|
+
<div class="form-group">
|
|
2687
|
+
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;user-select:none">
|
|
2688
|
+
<input type="checkbox" checked=${voiceEnabled}
|
|
2689
|
+
onchange=${(e) => setVoiceEnabled(e.target.checked)}
|
|
2690
|
+
style="width:auto;padding:0;margin:0;accent-color:var(--accent);cursor:pointer" />
|
|
2691
|
+
Enable Voice Mode in the UI
|
|
2692
|
+
</label>
|
|
2693
|
+
<div class="hint">Allows live voice/video calls from chat. Tier 2 browser fallback works without cloud keys.</div>
|
|
2694
|
+
</div>
|
|
2695
|
+
${voiceEnabled && html`
|
|
2696
|
+
<div class="executor-grid">
|
|
2697
|
+
<div class="form-group">
|
|
2698
|
+
<label>Voice Provider</label>
|
|
2699
|
+
<select value=${voiceProvider} onchange=${(e) => setVoiceProvider(e.target.value)}>
|
|
2700
|
+
<option value="auto">Auto Detect</option>
|
|
2701
|
+
<option value="openai">OpenAI Realtime</option>
|
|
2702
|
+
<option value="azure">Azure OpenAI Realtime</option>
|
|
2703
|
+
<option value="claude">Claude (fallback + Claude vision)</option>
|
|
2704
|
+
<option value="gemini">Gemini (fallback + Gemini vision)</option>
|
|
2705
|
+
<option value="fallback">Browser Fallback Only</option>
|
|
2706
|
+
</select>
|
|
2707
|
+
</div>
|
|
2708
|
+
<div class="form-group">
|
|
2709
|
+
<label>Voice Persona</label>
|
|
2710
|
+
<select value=${voiceId} onchange=${(e) => setVoiceId(e.target.value)}>
|
|
2711
|
+
<option value="alloy">alloy</option>
|
|
2712
|
+
<option value="ash">ash</option>
|
|
2713
|
+
<option value="ballad">ballad</option>
|
|
2714
|
+
<option value="coral">coral</option>
|
|
2715
|
+
<option value="echo">echo</option>
|
|
2716
|
+
<option value="fable">fable</option>
|
|
2717
|
+
<option value="nova">nova</option>
|
|
2718
|
+
<option value="onyx">onyx</option>
|
|
2719
|
+
<option value="sage">sage</option>
|
|
2720
|
+
<option value="shimmer">shimmer</option>
|
|
2721
|
+
<option value="verse">verse</option>
|
|
2722
|
+
</select>
|
|
2723
|
+
</div>
|
|
2724
|
+
<div class="form-group">
|
|
2725
|
+
<label>Realtime Voice Model</label>
|
|
2726
|
+
<input
|
|
2727
|
+
type="text"
|
|
2728
|
+
value=${voiceModel}
|
|
2729
|
+
oninput=${(e) => setVoiceModel(e.target.value)}
|
|
2730
|
+
placeholder="gpt-4o-realtime-preview-2024-12-17"
|
|
2731
|
+
/>
|
|
2732
|
+
</div>
|
|
2733
|
+
<div class="form-group">
|
|
2734
|
+
<label>Vision Model</label>
|
|
2735
|
+
<input
|
|
2736
|
+
type="text"
|
|
2737
|
+
value=${voiceVisionModel}
|
|
2738
|
+
oninput=${(e) => setVoiceVisionModel(e.target.value)}
|
|
2739
|
+
placeholder="gpt-4.1-mini"
|
|
2740
|
+
/>
|
|
2741
|
+
</div>
|
|
2742
|
+
<div class="form-group">
|
|
2743
|
+
<label>Turn Detection</label>
|
|
2744
|
+
<select value=${voiceTurnDetection} onchange=${(e) => setVoiceTurnDetection(e.target.value)}>
|
|
2745
|
+
<option value="server_vad">server_vad</option>
|
|
2746
|
+
<option value="semantic_vad">semantic_vad</option>
|
|
2747
|
+
<option value="none">none</option>
|
|
2748
|
+
</select>
|
|
2749
|
+
</div>
|
|
2750
|
+
<div class="form-group">
|
|
2751
|
+
<label>Fallback Mode</label>
|
|
2752
|
+
<select value=${voiceFallbackMode} onchange=${(e) => setVoiceFallbackMode(e.target.value)}>
|
|
2753
|
+
<option value="browser">browser</option>
|
|
2754
|
+
<option value="disabled">disabled</option>
|
|
2755
|
+
</select>
|
|
2756
|
+
</div>
|
|
2757
|
+
<div class="form-group">
|
|
2758
|
+
<label>Delegate Executor</label>
|
|
2759
|
+
<select value=${voiceDelegateExecutor} onchange=${(e) => setVoiceDelegateExecutor(e.target.value)}>
|
|
2760
|
+
<option value="codex-sdk">codex-sdk</option>
|
|
2761
|
+
<option value="copilot-sdk">copilot-sdk</option>
|
|
2762
|
+
<option value="claude-sdk">claude-sdk</option>
|
|
2763
|
+
<option value="gemini-sdk">gemini-sdk</option>
|
|
2764
|
+
<option value="opencode-sdk">opencode-sdk</option>
|
|
2765
|
+
</select>
|
|
2766
|
+
</div>
|
|
2767
|
+
</div>
|
|
2768
|
+
${(voiceProvider === "auto" || voiceProvider === "openai") && html`
|
|
2769
|
+
<div class="form-group">
|
|
2770
|
+
<label>OpenAI Realtime API Key</label>
|
|
2771
|
+
<input
|
|
2772
|
+
type="password"
|
|
2773
|
+
value=${openaiRealtimeApiKey}
|
|
2774
|
+
oninput=${(e) => setOpenaiRealtimeApiKey(e.target.value)}
|
|
2775
|
+
placeholder="Optional - defaults to OPENAI_API_KEY"
|
|
2776
|
+
/>
|
|
2777
|
+
<div class="hint">Leave blank to use OPENAI_API_KEY.</div>
|
|
2778
|
+
</div>
|
|
2779
|
+
`}
|
|
2780
|
+
${(voiceProvider === "auto" || voiceProvider === "azure") && html`
|
|
2781
|
+
<div class="executor-grid">
|
|
2782
|
+
<div class="form-group">
|
|
2783
|
+
<label>Azure Realtime Endpoint</label>
|
|
2784
|
+
<input
|
|
2785
|
+
type="text"
|
|
2786
|
+
value=${azureOpenaiRealtimeEndpoint}
|
|
2787
|
+
oninput=${(e) => setAzureOpenaiRealtimeEndpoint(e.target.value)}
|
|
2788
|
+
placeholder="https://<resource>.openai.azure.com"
|
|
2789
|
+
/>
|
|
2790
|
+
</div>
|
|
2791
|
+
<div class="form-group">
|
|
2792
|
+
<label>Azure Realtime Deployment</label>
|
|
2793
|
+
<input
|
|
2794
|
+
type="text"
|
|
2795
|
+
value=${azureOpenaiRealtimeDeployment}
|
|
2796
|
+
oninput=${(e) => setAzureOpenaiRealtimeDeployment(e.target.value)}
|
|
2797
|
+
placeholder="gpt-4o-realtime-preview"
|
|
2798
|
+
/>
|
|
2799
|
+
</div>
|
|
2800
|
+
</div>
|
|
2801
|
+
<div class="form-group">
|
|
2802
|
+
<label>Azure Realtime API Key</label>
|
|
2803
|
+
<input
|
|
2804
|
+
type="password"
|
|
2805
|
+
value=${azureOpenaiRealtimeApiKey}
|
|
2806
|
+
oninput=${(e) => setAzureOpenaiRealtimeApiKey(e.target.value)}
|
|
2807
|
+
placeholder="Optional - defaults to AZURE_OPENAI_API_KEY"
|
|
2808
|
+
/>
|
|
2809
|
+
</div>
|
|
2810
|
+
`}
|
|
2811
|
+
`}
|
|
2812
|
+
<//>
|
|
2813
|
+
|
|
2814
|
+
<${Section} title=":hammer: Infrastructure">
|
|
2573
2815
|
<div class="form-group">
|
|
2574
2816
|
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;user-select:none">
|
|
2575
2817
|
<input type="checkbox" checked=${containerEnabled}
|
|
@@ -2673,6 +2915,14 @@ function App() {
|
|
|
2673
2915
|
</tr>
|
|
2674
2916
|
<tr><th>Workflow Auto-Install</th><td>${workflowAutoInstall ? "Enabled" : "Disabled"}</td></tr>
|
|
2675
2917
|
<tr><th>Telegram</th><td>${telegramEnabled && telegramToken ? "Configured" : "Skipped"}</td></tr>
|
|
2918
|
+
<tr>
|
|
2919
|
+
<th>Voice</th>
|
|
2920
|
+
<td>
|
|
2921
|
+
${voiceEnabled
|
|
2922
|
+
? `${voiceProvider} (${voiceFallbackMode} fallback)`
|
|
2923
|
+
: "Disabled"}
|
|
2924
|
+
</td>
|
|
2925
|
+
</tr>
|
|
2676
2926
|
${profile === "advanced" ? html`
|
|
2677
2927
|
<tr><th>Max Parallel</th><td>${maxParallel}</td></tr>
|
|
2678
2928
|
<tr><th>Max Retries</th><td>${maxRetries}</td></tr>
|
|
@@ -2740,7 +2990,7 @@ function App() {
|
|
|
2740
2990
|
`}
|
|
2741
2991
|
${oauthStatus === "received" && html`
|
|
2742
2992
|
<div style="background:rgba(34,197,94,.08);border:1px solid rgba(34,197,94,.35);border-radius:var(--radius-sm);padding:12px 16px;margin-bottom:16px;font-size:0.85rem;color:#4ade80;display:flex;align-items:flex-start;gap:10px">
|
|
2743
|
-
<span style="font-size:1.2em;line-height:1"
|
|
2993
|
+
<span style="font-size:1.2em;line-height:1">${iconText(":check:")}</span>
|
|
2744
2994
|
<div>
|
|
2745
2995
|
<strong>GitHub App authorized!</strong>
|
|
2746
2996
|
${oauthInstallationId && html` Installation ID: <code style="font-family:var(--font-mono);font-size:0.8em;color:#86efac">${oauthInstallationId}</code>.`}
|