bosun 0.31.0 → 0.31.3

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/monitor.mjs CHANGED
@@ -9372,6 +9372,7 @@ function formatCodexResult(result) {
9372
9372
  }
9373
9373
  if (typeof result === "object") {
9374
9374
  const candidates = [
9375
+ result.finalResponse,
9375
9376
  result.output,
9376
9377
  result.text,
9377
9378
  result.message,
@@ -12748,6 +12749,8 @@ export {
12748
12749
  appendKnowledgeEntry,
12749
12750
  buildKnowledgeEntry,
12750
12751
  formatKnowledgeSummary,
12752
+ extractPlannerTasksFromOutput,
12753
+ formatCodexResult,
12751
12754
  // Container runner re-exports
12752
12755
  getContainerStatus,
12753
12756
  isContainerEnabled,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosun",
3
- "version": "0.31.0",
3
+ "version": "0.31.3",
4
4
  "description": "AI-powered orchestrator supervisor — manages AI agent executors with failover, auto-restarts on failure, analyzes crashes with Codex SDK, creates PRs via Vibe-Kanban API, and sends Telegram notifications. Supports N executors with weighted distribution, multi-repo projects, and auto-setup.",
5
5
  "type": "module",
6
6
  "license": "Apache 2.0",
@@ -4,6 +4,7 @@
4
4
  * ────────────────────────────────────────────────────────────── */
5
5
 
6
6
  import { h } from "preact";
7
+ import { createPortal } from "preact/compat";
7
8
  import {
8
9
  useState,
9
10
  useEffect,
@@ -319,7 +320,7 @@ export function Modal({ title, open = true, onClose, children, contentClassName
319
320
  ? `transform: translateY(${dragY}px); opacity: ${Math.max(0.2, 1 - dragY / 400)}`
320
321
  : "";
321
322
 
322
- return html`
323
+ const content = html`
323
324
  <div
324
325
  class="modal-overlay ${visible ? "modal-overlay-visible" : ""}"
325
326
  onClick=${(e) => {
@@ -354,6 +355,7 @@ export function Modal({ title, open = true, onClose, children, contentClassName
354
355
  </div>
355
356
  </div>
356
357
  `;
358
+ return createPortal(content, document.body);
357
359
  }
358
360
 
359
361
  /* ═══════════════════════════════════════════════
package/ui/tabs/agents.js CHANGED
@@ -1280,9 +1280,6 @@ export function AgentsTab() {
1280
1280
  </div>
1281
1281
  `}
1282
1282
 
1283
- <div class="fleet-span">
1284
- <${SessionsPanel} />
1285
- </div>
1286
1283
  </div>
1287
1284
 
1288
1285
  ${selectedAgent && html`
@@ -90,26 +90,44 @@ const SETTINGS_STYLES = `
90
90
  /* Floating save bar */
91
91
  .settings-save-bar {
92
92
  position: fixed;
93
- bottom: calc(var(--nav-height) + var(--safe-bottom) + 10px);
94
- left: 0; right: 0;
95
- z-index: 1000;
93
+ bottom: calc(var(--nav-height, 60px) + var(--safe-bottom, 0px) + 12px);
94
+ left: 50%;
95
+ transform: translateX(-50%);
96
+ z-index: 999;
96
97
  display: flex;
97
98
  align-items: center;
98
99
  justify-content: space-between;
99
100
  gap: 12px;
100
101
  flex-wrap: wrap;
101
102
  row-gap: 8px;
102
- padding: 12px 16px;
103
- padding-bottom: max(12px, env(safe-area-inset-bottom, 0px));
103
+ padding: 10px 16px;
104
+ min-width: 240px;
105
+ max-width: 480px;
106
+ width: auto;
104
107
  background: var(--glass-bg, rgba(30,30,46,0.95));
105
108
  backdrop-filter: blur(20px);
106
109
  -webkit-backdrop-filter: blur(20px);
107
- border-top: 1px solid var(--border, rgba(255,255,255,0.08));
110
+ border: 1px solid var(--border, rgba(255,255,255,0.08));
111
+ border-radius: 12px;
112
+ box-shadow: 0 4px 20px rgba(0,0,0,0.3);
113
+ transition: all 0.2s ease;
108
114
  animation: slideUp 0.25s ease;
109
115
  }
116
+ .settings-save-bar--dirty {
117
+ border-color: var(--accent, #5b6eae);
118
+ box-shadow: 0 4px 20px rgba(91, 110, 174, 0.25);
119
+ }
120
+ .settings-save-bar--clean .save-bar-info {
121
+ color: var(--text-hint, #666);
122
+ font-size: 12px;
123
+ }
124
+ .setting-modified-dot--clean {
125
+ background: var(--text-hint, #666) !important;
126
+ opacity: 0.4;
127
+ }
110
128
  @keyframes slideUp {
111
- from { transform: translateY(100%); opacity: 0; }
112
- to { transform: translateY(0); opacity: 1; }
129
+ from { transform: translateX(-50%) translateY(20px); opacity: 0; }
130
+ to { transform: translateX(-50%) translateY(0); opacity: 1; }
113
131
  }
114
132
  .settings-save-bar .save-bar-info {
115
133
  display: flex;
@@ -129,7 +147,7 @@ const SETTINGS_STYLES = `
129
147
  }
130
148
  @media (min-width: 1200px) {
131
149
  .settings-save-bar {
132
- bottom: 0;
150
+ bottom: 16px;
133
151
  }
134
152
  }
135
153
  /* Individual setting row */
@@ -359,6 +377,7 @@ const SETTINGS_STYLES = `
359
377
  margin-right: auto;
360
378
  width: 100%;
361
379
  box-sizing: border-box;
380
+ padding-bottom: 80px;
362
381
  }
363
382
 
364
383
  body.settings-save-open .main-content {
@@ -1069,15 +1088,14 @@ function ServerConfigMode() {
1069
1088
  <//>
1070
1089
  `}
1071
1090
 
1072
- <!-- Floating save bar -->
1073
- ${changeCount > 0 &&
1074
- html`
1075
- <div class="settings-save-bar">
1076
- <div class="save-bar-info">
1077
- <span class="setting-modified-dot"></span>
1078
- <span>${changeCount} unsaved change${changeCount !== 1 ? "s" : ""}</span>
1079
- </div>
1080
- <div class="save-bar-actions">
1091
+ <!-- Floating save bar - always visible -->
1092
+ <div class=${`settings-save-bar ${changeCount > 0 ? 'settings-save-bar--dirty' : 'settings-save-bar--clean'}`}>
1093
+ <div class="save-bar-info">
1094
+ <span class=${`setting-modified-dot ${changeCount === 0 ? 'setting-modified-dot--clean' : ''}`}></span>
1095
+ <span>${changeCount > 0 ? `${changeCount} unsaved change${changeCount !== 1 ? "s" : ""}` : "All changes saved"}</span>
1096
+ </div>
1097
+ <div class="save-bar-actions">
1098
+ ${changeCount > 0 && html`
1081
1099
  <button class="btn btn-ghost btn-sm" onClick=${handleDiscard}>
1082
1100
  Discard
1083
1101
  </button>
@@ -1088,9 +1106,9 @@ function ServerConfigMode() {
1088
1106
  >
1089
1107
  ${saving ? html`<${Spinner} size=${14} /> Saving…` : "Save Changes"}
1090
1108
  </button>
1091
- </div>
1109
+ `}
1092
1110
  </div>
1093
- `}
1111
+ </div>
1094
1112
 
1095
1113
  <!-- Confirm dialog with diff -->
1096
1114
  ${confirmOpen &&
@@ -1176,33 +1194,38 @@ function AppPreferencesMode() {
1176
1194
  /* Load prefs from CloudStorage on mount */
1177
1195
  useEffect(() => {
1178
1196
  (async () => {
1179
- const [fs, ct, nu, ne, nc, dm, dmp, ds, dr] = await Promise.all([
1180
- cloudGet("fontSize"),
1181
- cloudGet("colorTheme"),
1182
- cloudGet("notifyUpdates"),
1183
- cloudGet("notifyErrors"),
1184
- cloudGet("notifyComplete"),
1185
- cloudGet("debugMode"),
1186
- cloudGet("defaultMaxParallel"),
1187
- cloudGet("defaultSdk"),
1188
- cloudGet("defaultRegion"),
1189
- ]);
1190
- if (fs) {
1191
- setFontSize(fs);
1192
- applyFontSize(fs);
1193
- }
1194
- if (ct) {
1195
- setColorTheme(ct);
1196
- applyColorTheme(ct);
1197
+ try {
1198
+ const [fs, ct, nu, ne, nc, dm, dmp, ds, dr] = await Promise.all([
1199
+ cloudGet("fontSize"),
1200
+ cloudGet("colorTheme"),
1201
+ cloudGet("notifyUpdates"),
1202
+ cloudGet("notifyErrors"),
1203
+ cloudGet("notifyComplete"),
1204
+ cloudGet("debugMode"),
1205
+ cloudGet("defaultMaxParallel"),
1206
+ cloudGet("defaultSdk"),
1207
+ cloudGet("defaultRegion"),
1208
+ ]);
1209
+ if (fs) {
1210
+ setFontSize(fs);
1211
+ applyFontSize(fs);
1212
+ }
1213
+ if (ct) {
1214
+ setColorTheme(ct);
1215
+ applyColorTheme(ct);
1216
+ }
1217
+ if (nu != null) setNotifyUpdates(nu);
1218
+ if (ne != null) setNotifyErrors(ne);
1219
+ if (nc != null) setNotifyComplete(nc);
1220
+ if (dm != null) setDebugMode(dm);
1221
+ if (dmp != null) setDefaultMaxParallel(dmp);
1222
+ if (ds) setDefaultSdk(ds);
1223
+ if (dr) setDefaultRegion(dr);
1224
+ } catch (err) {
1225
+ console.warn('[AppPrefs] Failed to load preferences:', err);
1226
+ } finally {
1227
+ setLoaded(true);
1197
1228
  }
1198
- if (nu != null) setNotifyUpdates(nu);
1199
- if (ne != null) setNotifyErrors(ne);
1200
- if (nc != null) setNotifyComplete(nc);
1201
- if (dm != null) setDebugMode(dm);
1202
- if (dmp != null) setDefaultMaxParallel(dmp);
1203
- if (ds) setDefaultSdk(ds);
1204
- if (dr) setDefaultRegion(dr);
1205
- setLoaded(true);
1206
1229
  })();
1207
1230
  }, []);
1208
1231
 
@@ -1211,6 +1234,7 @@ function AppPreferencesMode() {
1211
1234
  const next = !getter;
1212
1235
  setter(next);
1213
1236
  cloudSet(key, next);
1237
+ console.log('[AppPrefs] Saved:', key, next);
1214
1238
  haptic();
1215
1239
  showToast("Preference saved", "success");
1216
1240
  }, []);
@@ -1218,6 +1242,7 @@ function AppPreferencesMode() {
1218
1242
  const handleFontSize = (v) => {
1219
1243
  setFontSize(v);
1220
1244
  cloudSet("fontSize", v);
1245
+ console.log('[AppPrefs] Saved: fontSize', v);
1221
1246
  haptic();
1222
1247
  applyFontSize(v);
1223
1248
  showToast("Font size saved", "success");
@@ -1226,6 +1251,7 @@ function AppPreferencesMode() {
1226
1251
  const handleColorTheme = (v) => {
1227
1252
  setColorTheme(v);
1228
1253
  cloudSet("colorTheme", v);
1254
+ console.log('[AppPrefs] Saved: colorTheme', v);
1229
1255
  haptic();
1230
1256
  applyColorTheme(v);
1231
1257
  showToast("Theme saved", "success");
@@ -1235,6 +1261,7 @@ function AppPreferencesMode() {
1235
1261
  const val = Math.max(1, Math.min(20, Number(v)));
1236
1262
  setDefaultMaxParallel(val);
1237
1263
  cloudSet("defaultMaxParallel", val);
1264
+ console.log('[AppPrefs] Saved: defaultMaxParallel', val);
1238
1265
  haptic();
1239
1266
  showToast("Preference saved", "success");
1240
1267
  };
@@ -1242,6 +1269,7 @@ function AppPreferencesMode() {
1242
1269
  const handleDefaultSdk = (v) => {
1243
1270
  setDefaultSdk(v);
1244
1271
  cloudSet("defaultSdk", v);
1272
+ console.log('[AppPrefs] Saved: defaultSdk', v);
1245
1273
  haptic();
1246
1274
  showToast("Preference saved", "success");
1247
1275
  };
@@ -1249,6 +1277,7 @@ function AppPreferencesMode() {
1249
1277
  const handleDefaultRegion = (v) => {
1250
1278
  setDefaultRegion(v);
1251
1279
  cloudSet("defaultRegion", v);
1280
+ console.log('[AppPrefs] Saved: defaultRegion', v);
1252
1281
  haptic();
1253
1282
  showToast("Preference saved", "success");
1254
1283
  };
@@ -1312,7 +1341,7 @@ function AppPreferencesMode() {
1312
1341
  : "";
1313
1342
 
1314
1343
  return html`
1315
- ${!loaded && html`<${Card} title="Loading Settings…"><${SkeletonCard} /><//>`}
1344
+
1316
1345
 
1317
1346
  <!-- ─── Account ─── -->
1318
1347
  <${Collapsible} title="👤 Account" defaultOpen=${true}>
package/ui-server.mjs CHANGED
@@ -546,6 +546,8 @@ let uiServer = null;
546
546
  let uiServerUrl = null;
547
547
  let uiServerTls = false;
548
548
  let wsServer = null;
549
+ /** Auto-open browser: only once per process, never during tests */
550
+ let _browserOpened = false;
549
551
  const wsClients = new Set();
550
552
  let sessionListenerAttached = false;
551
553
  /** @type {ReturnType<typeof setInterval>|null} */
@@ -5468,8 +5470,19 @@ export async function startTelegramUiServer(options = {}) {
5468
5470
  console.log(`[telegram-ui] LAN access: ${protocol}://${lanIp}:${actualPort}`);
5469
5471
  console.log(`[telegram-ui] Browser access: ${protocol}://${lanIp}:${actualPort}/?token=${sessionToken}`);
5470
5472
 
5471
- // Auto-open browser (skip in desktop/embedded mode)
5472
- if (process.env.BOSUN_DESKTOP !== "1" && !options.skipAutoOpen) {
5473
+ // Auto-open browser:
5474
+ // - skip in desktop/Electron mode (BOSUN_DESKTOP=1)
5475
+ // - skip when caller passes skipAutoOpen
5476
+ // - skip during Vitest / Jest test runs (avoids opening 20+ tabs during `npm test`)
5477
+ // - only open ONCE per process (singleton guard — prevents loops on server restart)
5478
+ const isTestRun = process.env.VITEST || process.env.NODE_ENV === "test" || process.env.JEST_WORKER_ID;
5479
+ if (
5480
+ process.env.BOSUN_DESKTOP !== "1" &&
5481
+ !options.skipAutoOpen &&
5482
+ !_browserOpened &&
5483
+ !isTestRun
5484
+ ) {
5485
+ _browserOpened = true;
5473
5486
  const openUrl = `${protocol}://${lanIp}:${actualPort}/?token=${sessionToken}`;
5474
5487
  try {
5475
5488
  const { exec } = await import("node:child_process");