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/telegram-sentinel.mjs
CHANGED
|
@@ -446,7 +446,7 @@ async function runRepairAgent(triggerReason, details = "") {
|
|
|
446
446
|
await sendTelegram(
|
|
447
447
|
telegramChatId,
|
|
448
448
|
[
|
|
449
|
-
"
|
|
449
|
+
":settings: Crash-loop detected. Launching repair agent.",
|
|
450
450
|
`Trigger: ${triggerReason}`,
|
|
451
451
|
details ? `Context: ${details}` : "",
|
|
452
452
|
]
|
|
@@ -483,7 +483,7 @@ async function runRepairAgent(triggerReason, details = "") {
|
|
|
483
483
|
await sendTelegram(
|
|
484
484
|
telegramChatId,
|
|
485
485
|
[
|
|
486
|
-
|
|
486
|
+
`:check: Repair agent completed via ${agentInfo.adapter}.`,
|
|
487
487
|
"",
|
|
488
488
|
summary.slice(0, 3500),
|
|
489
489
|
].join("\n"),
|
|
@@ -492,7 +492,7 @@ async function runRepairAgent(triggerReason, details = "") {
|
|
|
492
492
|
} catch (err) {
|
|
493
493
|
await sendTelegram(
|
|
494
494
|
telegramChatId,
|
|
495
|
-
|
|
495
|
+
`:close: Repair agent failed: ${err?.message || err}`,
|
|
496
496
|
);
|
|
497
497
|
return false;
|
|
498
498
|
} finally {
|
|
@@ -510,7 +510,7 @@ async function runPrimaryAgentFallback(chatId, text, command) {
|
|
|
510
510
|
const agentInfo = getPrimaryAgentInfo();
|
|
511
511
|
await sendTelegram(
|
|
512
512
|
chatId,
|
|
513
|
-
|
|
513
|
+
`:bot: bosun is down. Running via sentinel fallback (${agentInfo.adapter})...`,
|
|
514
514
|
);
|
|
515
515
|
|
|
516
516
|
const prompt = [
|
|
@@ -536,7 +536,7 @@ async function runPrimaryAgentFallback(chatId, text, command) {
|
|
|
536
536
|
} catch (err) {
|
|
537
537
|
await sendTelegram(
|
|
538
538
|
chatId,
|
|
539
|
-
|
|
539
|
+
`:close: Sentinel fallback failed: ${err?.message || err}`,
|
|
540
540
|
);
|
|
541
541
|
return false;
|
|
542
542
|
}
|
|
@@ -556,7 +556,7 @@ async function attemptMonitorRecovery(triggerReason) {
|
|
|
556
556
|
await sendTelegram(
|
|
557
557
|
telegramChatId,
|
|
558
558
|
[
|
|
559
|
-
"
|
|
559
|
+
":alert: Monitor crash-loop detected.",
|
|
560
560
|
`Window: ${Math.round(sentinelConfig.crashLoopWindowMs / 60000)}m | threshold: ${sentinelConfig.crashLoopThreshold}`,
|
|
561
561
|
`Monitor-monitor: ${mmHealth.ok ? "healthy" : "degraded"} (${mmHealth.reason})`,
|
|
562
562
|
"Attempting autonomous repair before restart.",
|
|
@@ -575,12 +575,12 @@ async function attemptMonitorRecovery(triggerReason) {
|
|
|
575
575
|
const pidSuffix = pid ? ` (PID ${pid})` : "";
|
|
576
576
|
await sendTelegram(
|
|
577
577
|
telegramChatId,
|
|
578
|
-
|
|
578
|
+
`:check: bosun recovered${pidSuffix}.`,
|
|
579
579
|
);
|
|
580
580
|
} catch (err) {
|
|
581
581
|
await sendTelegram(
|
|
582
582
|
telegramChatId,
|
|
583
|
-
|
|
583
|
+
`:close: bosun auto-restart failed: ${err?.message || err}`,
|
|
584
584
|
);
|
|
585
585
|
}
|
|
586
586
|
}
|
|
@@ -1013,14 +1013,14 @@ async function handleStandaloneCommand(chatId, command, fullText) {
|
|
|
1013
1013
|
*/
|
|
1014
1014
|
async function handlePing(chatId) {
|
|
1015
1015
|
const monPid = readAlivePid(MONITOR_PID_FILE);
|
|
1016
|
-
const monStatus = monPid ?
|
|
1016
|
+
const monStatus = monPid ? `:check: running (PID ${monPid})` : ":close: not running";
|
|
1017
1017
|
const uptime = formatUptime(Date.now() - new Date(startedAt).getTime());
|
|
1018
1018
|
await sendTelegram(
|
|
1019
1019
|
chatId,
|
|
1020
1020
|
[
|
|
1021
|
-
"
|
|
1021
|
+
":target: *Pong!*",
|
|
1022
1022
|
"",
|
|
1023
|
-
`Sentinel:
|
|
1023
|
+
`Sentinel: :check: alive (${uptime})`,
|
|
1024
1024
|
`Mode: ${mode}`,
|
|
1025
1025
|
`Monitor: ${monStatus}`,
|
|
1026
1026
|
`Host: \`${os.hostname()}\``,
|
|
@@ -1038,14 +1038,14 @@ async function handleStatus(chatId) {
|
|
|
1038
1038
|
if (!existsSync(STATUS_FILE)) {
|
|
1039
1039
|
await sendTelegram(
|
|
1040
1040
|
chatId,
|
|
1041
|
-
"
|
|
1041
|
+
":chart: No status file found. bosun may not have run yet.",
|
|
1042
1042
|
);
|
|
1043
1043
|
return;
|
|
1044
1044
|
}
|
|
1045
1045
|
const raw = await readFile(STATUS_FILE, "utf8");
|
|
1046
1046
|
const data = JSON.parse(raw);
|
|
1047
1047
|
|
|
1048
|
-
const lines = ["
|
|
1048
|
+
const lines = [":chart: *Orchestrator Status*", ""];
|
|
1049
1049
|
|
|
1050
1050
|
if (data.executor_mode) lines.push(`Mode: \`${data.executor_mode}\``);
|
|
1051
1051
|
if (data.active_slots) lines.push(`Slots: \`${data.active_slots}\``);
|
|
@@ -1074,7 +1074,7 @@ async function handleStatus(chatId) {
|
|
|
1074
1074
|
|
|
1075
1075
|
await sendTelegram(chatId, lines.join("\n"), { parseMode: "Markdown" });
|
|
1076
1076
|
} catch (err) {
|
|
1077
|
-
await sendTelegram(chatId,
|
|
1077
|
+
await sendTelegram(chatId, `:close: Error reading status: ${err.message}`);
|
|
1078
1078
|
}
|
|
1079
1079
|
}
|
|
1080
1080
|
|
|
@@ -1085,7 +1085,7 @@ async function handleStatus(chatId) {
|
|
|
1085
1085
|
async function handleSentinelInfo(chatId) {
|
|
1086
1086
|
const status = getSentinelStatus();
|
|
1087
1087
|
const lines = [
|
|
1088
|
-
"
|
|
1088
|
+
":shield: *Telegram Sentinel*",
|
|
1089
1089
|
"",
|
|
1090
1090
|
`PID: \`${process.pid}\``,
|
|
1091
1091
|
`Mode: ${status.mode}`,
|
|
@@ -1112,22 +1112,22 @@ async function handleStartMonitor(chatId) {
|
|
|
1112
1112
|
if (monPid) {
|
|
1113
1113
|
await sendTelegram(
|
|
1114
1114
|
chatId,
|
|
1115
|
-
|
|
1115
|
+
`:check: bosun is already running (PID ${monPid}).`,
|
|
1116
1116
|
);
|
|
1117
1117
|
return;
|
|
1118
1118
|
}
|
|
1119
|
-
await sendTelegram(chatId, "
|
|
1119
|
+
await sendTelegram(chatId, ":rocket: Starting bosun...");
|
|
1120
1120
|
try {
|
|
1121
1121
|
await ensureMonitorRunning("manual /start command");
|
|
1122
1122
|
const pid = readAlivePid(MONITOR_PID_FILE);
|
|
1123
1123
|
await sendTelegram(
|
|
1124
1124
|
chatId,
|
|
1125
|
-
|
|
1125
|
+
`:check: bosun started${pid ? ` (PID ${pid})` : ""}.`,
|
|
1126
1126
|
);
|
|
1127
1127
|
} catch (err) {
|
|
1128
1128
|
await sendTelegram(
|
|
1129
1129
|
chatId,
|
|
1130
|
-
|
|
1130
|
+
`:close: Failed to start bosun: ${err.message}`,
|
|
1131
1131
|
);
|
|
1132
1132
|
}
|
|
1133
1133
|
}
|
|
@@ -1139,10 +1139,10 @@ async function handleStartMonitor(chatId) {
|
|
|
1139
1139
|
async function handleStopMonitor(chatId) {
|
|
1140
1140
|
const monPid = readAlivePid(MONITOR_PID_FILE);
|
|
1141
1141
|
if (!monPid) {
|
|
1142
|
-
await sendTelegram(chatId, "
|
|
1142
|
+
await sendTelegram(chatId, ":help: bosun is not running.");
|
|
1143
1143
|
return;
|
|
1144
1144
|
}
|
|
1145
|
-
await sendTelegram(chatId,
|
|
1145
|
+
await sendTelegram(chatId, `:close: Stopping bosun (PID ${monPid})...`);
|
|
1146
1146
|
try {
|
|
1147
1147
|
process.kill(monPid, "SIGTERM");
|
|
1148
1148
|
// Wait for process to die
|
|
@@ -1162,13 +1162,13 @@ async function handleStopMonitor(chatId) {
|
|
|
1162
1162
|
}
|
|
1163
1163
|
}
|
|
1164
1164
|
removePidFile(MONITOR_PID_FILE);
|
|
1165
|
-
await sendTelegram(chatId, "
|
|
1165
|
+
await sendTelegram(chatId, ":check: bosun stopped.");
|
|
1166
1166
|
monitorManualStopUntil = Date.now() + sentinelConfig.manualStopHoldMs;
|
|
1167
1167
|
saveRecoveryState();
|
|
1168
1168
|
// Transition to standalone mode after stopping monitor
|
|
1169
1169
|
await transitionToStandalone("monitor manually stopped");
|
|
1170
1170
|
} catch (err) {
|
|
1171
|
-
await sendTelegram(chatId,
|
|
1171
|
+
await sendTelegram(chatId, `:close: Error stopping monitor: ${err.message}`);
|
|
1172
1172
|
}
|
|
1173
1173
|
}
|
|
1174
1174
|
|
|
@@ -1181,7 +1181,7 @@ async function handleHelp(chatId) {
|
|
|
1181
1181
|
const monStatus = monPid ? "running" : "stopped";
|
|
1182
1182
|
|
|
1183
1183
|
const lines = [
|
|
1184
|
-
"
|
|
1184
|
+
":shield: *Sentinel Commands* (always available)",
|
|
1185
1185
|
"",
|
|
1186
1186
|
"/ping — Check sentinel + monitor liveness",
|
|
1187
1187
|
"/status — Show cached orchestrator status",
|
|
@@ -1232,7 +1232,7 @@ async function handleMonitorCommand(chatId, text, command) {
|
|
|
1232
1232
|
return;
|
|
1233
1233
|
}
|
|
1234
1234
|
|
|
1235
|
-
await sendTelegram(chatId, "
|
|
1235
|
+
await sendTelegram(chatId, ":clock: Starting bosun in the background...");
|
|
1236
1236
|
|
|
1237
1237
|
try {
|
|
1238
1238
|
await ensureMonitorRunning(`command: ${command}`);
|
|
@@ -1247,7 +1247,7 @@ async function handleMonitorCommand(chatId, text, command) {
|
|
|
1247
1247
|
if (!fallbackHandled) {
|
|
1248
1248
|
await sendTelegram(
|
|
1249
1249
|
chatId,
|
|
1250
|
-
|
|
1250
|
+
`:close: Failed to start bosun: ${err.message}\n\nYour command was not processed.`,
|
|
1251
1251
|
);
|
|
1252
1252
|
}
|
|
1253
1253
|
// Clear the failed commands
|
|
@@ -1559,7 +1559,7 @@ async function healthCheck() {
|
|
|
1559
1559
|
await sendTelegram(
|
|
1560
1560
|
telegramChatId,
|
|
1561
1561
|
[
|
|
1562
|
-
|
|
1562
|
+
`:zap: ${tag} bosun crashed`,
|
|
1563
1563
|
"",
|
|
1564
1564
|
`Host: \`${host}\``,
|
|
1565
1565
|
`Time: ${new Date().toISOString()}`,
|
|
@@ -1747,7 +1747,7 @@ export async function startSentinel(options = {}) {
|
|
|
1747
1747
|
// Attempt crash notification
|
|
1748
1748
|
sendTelegram(
|
|
1749
1749
|
telegramChatId,
|
|
1750
|
-
|
|
1750
|
+
`:shield::close: Sentinel crashed: ${err.message}\nHost: \`${os.hostname()}\``,
|
|
1751
1751
|
{ parseMode: "Markdown" },
|
|
1752
1752
|
).catch(() => {});
|
|
1753
1753
|
stopSentinel();
|
package/ui/app.js
CHANGED
|
@@ -61,6 +61,87 @@ const TABLET_MIN_WIDTH = 768;
|
|
|
61
61
|
const COMPACT_NAV_MAX_WIDTH = 520;
|
|
62
62
|
const RAIL_ICON_WIDTH = 54;
|
|
63
63
|
const SIDEBAR_ICON_WIDTH = 54;
|
|
64
|
+
const APP_LOGO_SOURCES = ["/logo.png", "/logo.svg", "/favicon.png"];
|
|
65
|
+
const VOICE_LAUNCH_QUERY_KEYS = [
|
|
66
|
+
"launch",
|
|
67
|
+
"call",
|
|
68
|
+
"autostart",
|
|
69
|
+
"sessionId",
|
|
70
|
+
"executor",
|
|
71
|
+
"mode",
|
|
72
|
+
"model",
|
|
73
|
+
"vision",
|
|
74
|
+
"source",
|
|
75
|
+
"chat_id",
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
function getAppLogoSource(index = 0) {
|
|
79
|
+
const safeIndex = Number.isFinite(index) ? Math.trunc(index) : 0;
|
|
80
|
+
if (safeIndex <= 0) return APP_LOGO_SOURCES[0];
|
|
81
|
+
if (safeIndex >= APP_LOGO_SOURCES.length) {
|
|
82
|
+
return APP_LOGO_SOURCES[APP_LOGO_SOURCES.length - 1];
|
|
83
|
+
}
|
|
84
|
+
return APP_LOGO_SOURCES[safeIndex];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function handleAppLogoLoadError(event) {
|
|
88
|
+
const target = event?.currentTarget;
|
|
89
|
+
if (!target) return;
|
|
90
|
+
|
|
91
|
+
const currentIndex = Number.parseInt(
|
|
92
|
+
String(target.dataset?.logoFallbackIndex || "0"),
|
|
93
|
+
10,
|
|
94
|
+
);
|
|
95
|
+
const nextIndex = Number.isFinite(currentIndex) ? currentIndex + 1 : 1;
|
|
96
|
+
if (nextIndex >= APP_LOGO_SOURCES.length) return;
|
|
97
|
+
|
|
98
|
+
target.dataset.logoFallbackIndex = String(nextIndex);
|
|
99
|
+
target.src = getAppLogoSource(nextIndex);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parseVoiceLaunchFromUrl() {
|
|
103
|
+
if (typeof window === "undefined") return null;
|
|
104
|
+
const params = new URLSearchParams(window.location.search || "");
|
|
105
|
+
const launch = String(params.get("launch") || "").trim().toLowerCase();
|
|
106
|
+
if (launch !== "meeting" && launch !== "voice") return null;
|
|
107
|
+
|
|
108
|
+
const callRaw = String(params.get("call") || "").trim().toLowerCase();
|
|
109
|
+
const call = callRaw === "video" ? "video" : "voice";
|
|
110
|
+
const explicitVision = String(params.get("vision") || "").trim().toLowerCase();
|
|
111
|
+
const initialVisionSource =
|
|
112
|
+
explicitVision === "camera" || explicitVision === "screen"
|
|
113
|
+
? explicitVision
|
|
114
|
+
: call === "video"
|
|
115
|
+
? "camera"
|
|
116
|
+
: null;
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
tab: "chat",
|
|
120
|
+
detail: {
|
|
121
|
+
call,
|
|
122
|
+
initialVisionSource,
|
|
123
|
+
sessionId: String(params.get("sessionId") || "").trim() || null,
|
|
124
|
+
executor: String(params.get("executor") || "").trim() || null,
|
|
125
|
+
mode: String(params.get("mode") || "").trim() || null,
|
|
126
|
+
model: String(params.get("model") || "").trim() || null,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function scrubVoiceLaunchQuery() {
|
|
132
|
+
if (typeof window === "undefined" || !window.history?.replaceState) return;
|
|
133
|
+
const url = new URL(window.location.href);
|
|
134
|
+
let changed = false;
|
|
135
|
+
for (const key of VOICE_LAUNCH_QUERY_KEYS) {
|
|
136
|
+
if (url.searchParams.has(key)) {
|
|
137
|
+
url.searchParams.delete(key);
|
|
138
|
+
changed = true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (!changed) return;
|
|
142
|
+
const nextPath = `${url.pathname}${url.search}${url.hash}`;
|
|
143
|
+
window.history.replaceState(window.history.state, "", nextPath || "/");
|
|
144
|
+
}
|
|
64
145
|
|
|
65
146
|
/* ── Module imports ── */
|
|
66
147
|
import { ICONS } from "./modules/icons.js";
|
|
@@ -111,12 +192,18 @@ import {
|
|
|
111
192
|
sessionsData,
|
|
112
193
|
initSessionWsListener,
|
|
113
194
|
} from "./components/session-list.js";
|
|
195
|
+
import {
|
|
196
|
+
activeAgent,
|
|
197
|
+
agentMode,
|
|
198
|
+
selectedModel,
|
|
199
|
+
} from "./components/agent-selector.js";
|
|
114
200
|
import { WorkspaceSwitcher } from "./components/workspace-switcher.js";
|
|
115
201
|
import { DiffViewer } from "./components/diff-viewer.js";
|
|
116
202
|
import {
|
|
117
203
|
CommandPalette,
|
|
118
204
|
useCommandPalette,
|
|
119
205
|
} from "./components/command-palette.js";
|
|
206
|
+
import { VoiceOverlay } from "./modules/voice-overlay.js";
|
|
120
207
|
|
|
121
208
|
/* ── Tab imports ── */
|
|
122
209
|
import { DashboardTab } from "./tabs/dashboard.js";
|
|
@@ -393,7 +480,7 @@ class TabErrorBoundary extends Component {
|
|
|
393
480
|
return html`
|
|
394
481
|
<div class="tab-error-boundary">
|
|
395
482
|
<div class="tab-error-pulse">
|
|
396
|
-
<span style="font-size:20px;color:#ef4444;">${resolveIcon("
|
|
483
|
+
<span style="font-size:20px;color:#ef4444;">${resolveIcon(":alert:")}</span>
|
|
397
484
|
</div>
|
|
398
485
|
<div>
|
|
399
486
|
<div style="font-size:14px;font-weight:600;margin-bottom:4px;color:var(--text-primary);">
|
|
@@ -524,7 +611,13 @@ function SidebarNav({ collapsed = false, onToggle }) {
|
|
|
524
611
|
<div class="sidebar-brand-row">
|
|
525
612
|
<div class="sidebar-brand">
|
|
526
613
|
<div class="sidebar-logo">
|
|
527
|
-
<img
|
|
614
|
+
<img
|
|
615
|
+
src=${getAppLogoSource(0)}
|
|
616
|
+
alt="Bosun"
|
|
617
|
+
class="app-logo-img"
|
|
618
|
+
data-logo-fallback-index="0"
|
|
619
|
+
onError=${handleAppLogoLoadError}
|
|
620
|
+
/>
|
|
528
621
|
</div>
|
|
529
622
|
${!collapsed && html`<div class="sidebar-title">Bosun</div>`}
|
|
530
623
|
</div>
|
|
@@ -540,10 +633,10 @@ function SidebarNav({ collapsed = false, onToggle }) {
|
|
|
540
633
|
${!collapsed && html`
|
|
541
634
|
<div class="sidebar-actions">
|
|
542
635
|
<button class="btn btn-primary btn-block" onClick=${() => createSession({ type: "primary" })}>
|
|
543
|
-
<span class="btn-icon">${resolveIcon("
|
|
636
|
+
<span class="btn-icon">${resolveIcon(":plus:")}</span> New Session
|
|
544
637
|
</button>
|
|
545
638
|
<button class="btn btn-ghost btn-block" onClick=${() => navigateTo("tasks")}>
|
|
546
|
-
<span class="btn-icon">${resolveIcon("
|
|
639
|
+
<span class="btn-icon">${resolveIcon(":clipboard:")}</span> View Tasks
|
|
547
640
|
</button>
|
|
548
641
|
</div>
|
|
549
642
|
`}
|
|
@@ -554,7 +647,7 @@ function SidebarNav({ collapsed = false, onToggle }) {
|
|
|
554
647
|
onClick=${() => createSession({ type: "primary" })}
|
|
555
648
|
title="New Session"
|
|
556
649
|
aria-label="New Session"
|
|
557
|
-
>${resolveIcon("
|
|
650
|
+
>${resolveIcon(":plus:")}</button>
|
|
558
651
|
</div>
|
|
559
652
|
`}
|
|
560
653
|
<nav class="sidebar-nav" aria-label="Main navigation">
|
|
@@ -1027,25 +1120,25 @@ function MoreSheet({ open, onClose, onNavigate, onOpenBot }) {
|
|
|
1027
1120
|
* ═══════════════════════════════════════════════ */
|
|
1028
1121
|
const BOT_SCREENS = {
|
|
1029
1122
|
home: {
|
|
1030
|
-
title: "
|
|
1123
|
+
title: ":sliders: Bosun Control Center",
|
|
1031
1124
|
body: "Manage your automation fleet.",
|
|
1032
1125
|
keyboard: [
|
|
1033
|
-
[{ text: "
|
|
1034
|
-
[{ text: "
|
|
1035
|
-
[{ text: "
|
|
1126
|
+
[{ text: ":chart: Status", cmd: "/status" }, { text: ":clipboard: Tasks", cmd: "/tasks" }, { text: ":bot: Agents", cmd: "/agents" }],
|
|
1127
|
+
[{ text: ":settings: Executor", go: "executor" }, { text: ":server: Routing", go: "routing" }, { text: ":git: Workspaces", go: "workspaces" }],
|
|
1128
|
+
[{ text: ":folder: Logs", cmd: "/logs" }, { text: ":heart: Health", cmd: "/health" }, { text: ":refresh: Refresh", cmd: "/status" }],
|
|
1036
1129
|
],
|
|
1037
1130
|
},
|
|
1038
1131
|
executor: {
|
|
1039
|
-
title: "
|
|
1132
|
+
title: ":settings: Executor",
|
|
1040
1133
|
parent: "home",
|
|
1041
1134
|
body: "Task execution slots, pause, resume, and parallelism.",
|
|
1042
1135
|
keyboard: [
|
|
1043
|
-
[{ text: "
|
|
1044
|
-
[{ text: "
|
|
1136
|
+
[{ text: ":chart: Status", cmd: "/executor" }, { text: ":pause: Pause", cmd: "/pause" }, { text: ":play: Resume", cmd: "/resume" }],
|
|
1137
|
+
[{ text: ":hash: Max Parallel", go: "maxparallel" }, { text: ":repeat: Retry Active", cmd: "/retrytask" }],
|
|
1045
1138
|
],
|
|
1046
1139
|
},
|
|
1047
1140
|
maxparallel: {
|
|
1048
|
-
title: "
|
|
1141
|
+
title: ":hash: Max Parallel Slots",
|
|
1049
1142
|
parent: "executor",
|
|
1050
1143
|
body: "Set the maximum number of concurrent task slots.",
|
|
1051
1144
|
keyboard: [
|
|
@@ -1055,25 +1148,25 @@ const BOT_SCREENS = {
|
|
|
1055
1148
|
],
|
|
1056
1149
|
},
|
|
1057
1150
|
routing: {
|
|
1058
|
-
title: "
|
|
1151
|
+
title: ":server: Routing & SDKs",
|
|
1059
1152
|
parent: "home",
|
|
1060
1153
|
body: "SDK routing, kanban binding, and version info.",
|
|
1061
1154
|
keyboard: [
|
|
1062
|
-
[{ text: "
|
|
1063
|
-
[{ text: "
|
|
1155
|
+
[{ text: ":bot: SDK Status", cmd: "/sdk" }, { text: ":clipboard: Kanban", cmd: "/kanban" }],
|
|
1156
|
+
[{ text: ":globe: Version", cmd: "/version" }, { text: ":help: Help", cmd: "/help" }],
|
|
1064
1157
|
],
|
|
1065
1158
|
},
|
|
1066
1159
|
workspaces: {
|
|
1067
|
-
title: "
|
|
1160
|
+
title: ":git: Workspaces",
|
|
1068
1161
|
parent: "home",
|
|
1069
1162
|
body: "Git worktrees, logs, and task planning.",
|
|
1070
1163
|
keyboard: [
|
|
1071
|
-
[{ text: "
|
|
1072
|
-
[{ text: "
|
|
1164
|
+
[{ text: ":chart: Fleet Status", cmd: "/status" }, { text: ":folder: Logs", cmd: "/logs" }],
|
|
1165
|
+
[{ text: ":grid: Planner", go: "planner" }, { text: ":check: Start Task", cmd: "/starttask" }],
|
|
1073
1166
|
],
|
|
1074
1167
|
},
|
|
1075
1168
|
planner: {
|
|
1076
|
-
title: "
|
|
1169
|
+
title: ":grid: Task Planner",
|
|
1077
1170
|
parent: "workspaces",
|
|
1078
1171
|
body: "Seed new tasks from the backlog into the active queue.",
|
|
1079
1172
|
keyboard: [
|
|
@@ -1145,7 +1238,7 @@ function BotControlsSheet({ open, onClose }) {
|
|
|
1145
1238
|
} else if (d?.executed === false && d?.error) {
|
|
1146
1239
|
setCmdError(d.error);
|
|
1147
1240
|
} else {
|
|
1148
|
-
setCmdOutput(
|
|
1241
|
+
setCmdOutput(`:check: ${cmd} sent.`);
|
|
1149
1242
|
}
|
|
1150
1243
|
} else {
|
|
1151
1244
|
setCmdError(result?.error || "Command failed");
|
|
@@ -1172,7 +1265,7 @@ function BotControlsSheet({ open, onClose }) {
|
|
|
1172
1265
|
</button>
|
|
1173
1266
|
${navStack.length > 1 ? html`
|
|
1174
1267
|
<button class="btn btn-ghost btn-sm" type="button" onClick=${botGoHome} aria-label="Go to home">
|
|
1175
|
-
${iconText("
|
|
1268
|
+
${iconText(":home: Home")}
|
|
1176
1269
|
</button>
|
|
1177
1270
|
` : null}
|
|
1178
1271
|
</div>
|
|
@@ -1188,7 +1281,7 @@ function BotControlsSheet({ open, onClose }) {
|
|
|
1188
1281
|
` : null}
|
|
1189
1282
|
|
|
1190
1283
|
${cmdError && !cmdLoading ? html`
|
|
1191
|
-
<div class="bot-controls-result bot-controls-result-error">${iconText(
|
|
1284
|
+
<div class="bot-controls-result bot-controls-result-error">${iconText(`:close: ${cmdError}`)}</div>
|
|
1192
1285
|
` : null}
|
|
1193
1286
|
|
|
1194
1287
|
${cmdOutput && !cmdLoading && !cmdError ? html`
|
|
@@ -1261,6 +1354,16 @@ function App() {
|
|
|
1261
1354
|
};
|
|
1262
1355
|
}, [isLoading]);
|
|
1263
1356
|
const [isMoreOpen, setIsMoreOpen] = useState(false);
|
|
1357
|
+
const [voiceOverlayOpen, setVoiceOverlayOpen] = useState(false);
|
|
1358
|
+
const [voiceTier, setVoiceTier] = useState(2);
|
|
1359
|
+
const [voiceSessionId, setVoiceSessionId] = useState(null);
|
|
1360
|
+
const [voiceExecutor, setVoiceExecutor] = useState(null);
|
|
1361
|
+
const [voiceAgentMode, setVoiceAgentMode] = useState(null);
|
|
1362
|
+
const [voiceModel, setVoiceModel] = useState(null);
|
|
1363
|
+
const [voiceCallType, setVoiceCallType] = useState("voice");
|
|
1364
|
+
const [voiceInitialVisionSource, setVoiceInitialVisionSource] = useState(
|
|
1365
|
+
null,
|
|
1366
|
+
);
|
|
1264
1367
|
const resizeRef = useRef(null);
|
|
1265
1368
|
const [isCompactNav, setIsCompactNav] = useState(() => {
|
|
1266
1369
|
const win = globalThis.window;
|
|
@@ -1566,6 +1669,125 @@ function App() {
|
|
|
1566
1669
|
return () => globalThis.removeEventListener?.("beforeunload", onBeforeUnload);
|
|
1567
1670
|
}, []);
|
|
1568
1671
|
|
|
1672
|
+
useEffect(() => {
|
|
1673
|
+
const handleOpenVoiceMode = async (event) => {
|
|
1674
|
+
try {
|
|
1675
|
+
const requestedCallType =
|
|
1676
|
+
String(event?.detail?.call || "").trim().toLowerCase() === "video"
|
|
1677
|
+
? "video"
|
|
1678
|
+
: "voice";
|
|
1679
|
+
const requestedVisionSourceRaw = String(
|
|
1680
|
+
event?.detail?.initialVisionSource || "",
|
|
1681
|
+
)
|
|
1682
|
+
.trim()
|
|
1683
|
+
.toLowerCase();
|
|
1684
|
+
const requestedVisionSource =
|
|
1685
|
+
requestedVisionSourceRaw === "camera" ||
|
|
1686
|
+
requestedVisionSourceRaw === "screen"
|
|
1687
|
+
? requestedVisionSourceRaw
|
|
1688
|
+
: requestedCallType === "video"
|
|
1689
|
+
? "camera"
|
|
1690
|
+
: null;
|
|
1691
|
+
const currentExecutor =
|
|
1692
|
+
String(event?.detail?.executor || activeAgent.value || "").trim() ||
|
|
1693
|
+
null;
|
|
1694
|
+
const currentMode =
|
|
1695
|
+
String(event?.detail?.mode || agentMode.value || "").trim() || null;
|
|
1696
|
+
const currentModel =
|
|
1697
|
+
String(event?.detail?.model || selectedModel.value || "").trim() ||
|
|
1698
|
+
null;
|
|
1699
|
+
const explicitSessionId =
|
|
1700
|
+
String(event?.detail?.sessionId || "").trim() || null;
|
|
1701
|
+
let currentSessionId =
|
|
1702
|
+
explicitSessionId ||
|
|
1703
|
+
(selectedSessionId.value ? String(selectedSessionId.value) : null);
|
|
1704
|
+
|
|
1705
|
+
// Ensure voice calls always bind to a real chat session so transcript +
|
|
1706
|
+
// delegated agent output are persisted in shared history.
|
|
1707
|
+
if (!currentSessionId) {
|
|
1708
|
+
const created = await createSession({
|
|
1709
|
+
type: "primary",
|
|
1710
|
+
agent: currentExecutor || undefined,
|
|
1711
|
+
mode: currentMode || undefined,
|
|
1712
|
+
model: currentModel || undefined,
|
|
1713
|
+
});
|
|
1714
|
+
const createdId = String(created?.session?.id || "").trim();
|
|
1715
|
+
currentSessionId = createdId || null;
|
|
1716
|
+
if (currentSessionId) {
|
|
1717
|
+
selectedSessionId.value = currentSessionId;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
if (!currentSessionId) {
|
|
1722
|
+
showToast("Could not create a chat session for voice mode.", "error");
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
setVoiceSessionId(currentSessionId);
|
|
1727
|
+
setVoiceExecutor(currentExecutor);
|
|
1728
|
+
setVoiceAgentMode(currentMode);
|
|
1729
|
+
setVoiceModel(currentModel);
|
|
1730
|
+
setVoiceCallType(requestedCallType);
|
|
1731
|
+
setVoiceInitialVisionSource(requestedVisionSource);
|
|
1732
|
+
|
|
1733
|
+
const response = await fetch("/api/voice/config", { method: "GET" });
|
|
1734
|
+
const cfg = response.ok ? await response.json() : null;
|
|
1735
|
+
if (!cfg?.available) {
|
|
1736
|
+
showToast(cfg?.reason || "Voice mode is not available.", "error");
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
setVoiceTier(Number(cfg?.tier) === 1 ? 1 : 2);
|
|
1740
|
+
setVoiceOverlayOpen(true);
|
|
1741
|
+
} catch (err) {
|
|
1742
|
+
showToast(
|
|
1743
|
+
`Could not open voice mode: ${err?.message || "unknown error"}`,
|
|
1744
|
+
"error",
|
|
1745
|
+
);
|
|
1746
|
+
}
|
|
1747
|
+
};
|
|
1748
|
+
|
|
1749
|
+
globalThis.addEventListener?.("ve:open-voice-mode", handleOpenVoiceMode);
|
|
1750
|
+
return () =>
|
|
1751
|
+
globalThis.removeEventListener?.("ve:open-voice-mode", handleOpenVoiceMode);
|
|
1752
|
+
}, []);
|
|
1753
|
+
|
|
1754
|
+
useEffect(() => {
|
|
1755
|
+
const launch = parseVoiceLaunchFromUrl();
|
|
1756
|
+
if (!launch) return;
|
|
1757
|
+
let cancelled = false;
|
|
1758
|
+
|
|
1759
|
+
const start = async () => {
|
|
1760
|
+
if (launch.tab === "chat") {
|
|
1761
|
+
const launchSessionId = String(launch.detail?.sessionId || "").trim();
|
|
1762
|
+
if (launchSessionId) {
|
|
1763
|
+
selectedSessionId.value = launchSessionId;
|
|
1764
|
+
navigateTo("chat", {
|
|
1765
|
+
params: { sessionId: launchSessionId },
|
|
1766
|
+
replace: true,
|
|
1767
|
+
skipGuard: true,
|
|
1768
|
+
});
|
|
1769
|
+
} else {
|
|
1770
|
+
navigateTo("chat", { replace: true, skipGuard: true });
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
1774
|
+
if (cancelled) return;
|
|
1775
|
+
globalThis.dispatchEvent?.(
|
|
1776
|
+
new CustomEvent("ve:open-voice-mode", { detail: launch.detail }),
|
|
1777
|
+
);
|
|
1778
|
+
};
|
|
1779
|
+
|
|
1780
|
+
start()
|
|
1781
|
+
.catch(() => {})
|
|
1782
|
+
.finally(() => {
|
|
1783
|
+
scrubVoiceLaunchQuery();
|
|
1784
|
+
});
|
|
1785
|
+
|
|
1786
|
+
return () => {
|
|
1787
|
+
cancelled = true;
|
|
1788
|
+
};
|
|
1789
|
+
}, []);
|
|
1790
|
+
|
|
1569
1791
|
useEffect(() => {
|
|
1570
1792
|
const el = mainRef.current;
|
|
1571
1793
|
if (!el) return;
|
|
@@ -1831,6 +2053,17 @@ function App() {
|
|
|
1831
2053
|
open=${isBotOpen}
|
|
1832
2054
|
onClose=${closeBot}
|
|
1833
2055
|
/>
|
|
2056
|
+
<${VoiceOverlay}
|
|
2057
|
+
visible=${voiceOverlayOpen}
|
|
2058
|
+
onClose=${() => setVoiceOverlayOpen(false)}
|
|
2059
|
+
tier=${voiceTier}
|
|
2060
|
+
sessionId=${voiceSessionId}
|
|
2061
|
+
executor=${voiceExecutor}
|
|
2062
|
+
mode=${voiceAgentMode}
|
|
2063
|
+
model=${voiceModel}
|
|
2064
|
+
callType=${voiceCallType}
|
|
2065
|
+
initialVisionSource=${voiceInitialVisionSource}
|
|
2066
|
+
/>
|
|
1834
2067
|
`;
|
|
1835
2068
|
}
|
|
1836
2069
|
|
package/ui/app.monolith.js
CHANGED
|
@@ -2398,7 +2398,7 @@ function Header() {
|
|
|
2398
2398
|
return html`
|
|
2399
2399
|
<header class="app-header">
|
|
2400
2400
|
<div class="app-header-brand">
|
|
2401
|
-
<img src="logo.png" alt="Bosun" class="app-logo-img" />
|
|
2401
|
+
<img src="/logo.png" alt="Bosun" class="app-logo-img" />
|
|
2402
2402
|
<div class="app-header-title">Bosun</div>
|
|
2403
2403
|
</div>
|
|
2404
2404
|
<div class="connection-pill ${isConn ? "connected" : "disconnected"}">
|
|
@@ -913,10 +913,11 @@ export function AgentPicker() {
|
|
|
913
913
|
>
|
|
914
914
|
${loading && html`<option disabled value="">Loading…</option>`}
|
|
915
915
|
${enabledAgents.map((agent) => {
|
|
916
|
-
const
|
|
917
|
-
const
|
|
916
|
+
const rawName = EXECUTOR_DISPLAY_NAMES[agent.id] || agent.name || "";
|
|
917
|
+
const name =
|
|
918
|
+
String(rawName).replace(/\s*\(busy\)\s*$/i, "").trim() || "Executor";
|
|
918
919
|
return html`
|
|
919
|
-
<option key=${agent.id} value=${agent.id}>${name}
|
|
920
|
+
<option key=${agent.id} value=${agent.id}>${name}</option>
|
|
920
921
|
`;
|
|
921
922
|
})}
|
|
922
923
|
</select>
|