bosun 0.29.0 → 0.29.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosun",
3
- "version": "0.29.0",
3
+ "version": "0.29.1",
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",
@@ -127,7 +127,10 @@
127
127
  "conflict-resolver.mjs",
128
128
  "copilot-shell.mjs",
129
129
  "diff-stats.mjs",
130
- "desktop/",
130
+ "desktop/main.mjs",
131
+ "desktop/launch.mjs",
132
+ "desktop/preload.mjs",
133
+ "desktop/package.json",
131
134
  "desktop-shortcut.mjs",
132
135
  "error-detector.mjs",
133
136
  "fetch-runtime.mjs",
package/ui/app.js CHANGED
@@ -180,15 +180,15 @@ function useBackendHealth() {
180
180
  function OfflineBanner() {
181
181
  const { retry: manualRetry } = useBackendHealth();
182
182
  return html`
183
- <div class="offline-banner alert alert-error shadow-sm mx-4 my-2">
184
- <span class="text-lg">⚠️</span>
185
- <div class="flex-1">
186
- <div class="font-semibold text-sm">Backend Unreachable</div>
187
- <div class="text-xs opacity-70">${backendError.value || "Connection lost"}</div>
183
+ <div class="offline-banner">
184
+ <div class="offline-banner-icon">⚠️</div>
185
+ <div class="offline-banner-content">
186
+ <div class="offline-banner-title">Backend Unreachable</div>
187
+ <div class="offline-banner-meta">${backendError.value || "Connection lost"}</div>
188
188
  ${backendLastSeen.value
189
- ? html`<div class="text-xs opacity-70">Last connected: ${formatTimeAgo(backendLastSeen.value)}</div>`
189
+ ? html`<div class="offline-banner-meta">Last connected: ${formatTimeAgo(backendLastSeen.value)}</div>`
190
190
  : null}
191
- <div class="text-xs opacity-70">Retry attempt #${backendRetryCount.value}</div>
191
+ <div class="offline-banner-meta">Retry attempt #${backendRetryCount.value}</div>
192
192
  </div>
193
193
  <button class="btn btn-ghost btn-sm" onClick=${manualRetry}>Retry</button>
194
194
  </div>
@@ -253,19 +253,27 @@ function Header() {
253
253
  }
254
254
 
255
255
  return html`
256
- <header class="app-header navbar bg-base-200/80 backdrop-blur-sm sticky top-0 z-30 min-h-0 px-4 py-2 border-b border-base-content/5">
257
- <div class="navbar-start">
258
- <${WorkspaceSwitcher} />
256
+ <header class="app-header">
257
+ <div class="app-header-left">
258
+ <div class="app-header-workspace">
259
+ <${WorkspaceSwitcher} />
260
+ </div>
259
261
  </div>
260
- <div class="navbar-end gap-2">
261
- <div class="flex items-center gap-2">
262
- <div class="badge ${connClass === 'connected' ? 'badge-success' : connClass === 'reconnecting' ? 'badge-warning' : 'badge-error'} badge-sm gap-1">
263
- <span class="w-1.5 h-1.5 rounded-full ${connClass === 'connected' ? 'bg-success-content' : connClass === 'reconnecting' ? 'bg-warning-content' : 'bg-error-content'}"></span>
264
- ${connLabel}
262
+ <div class="app-header-right">
263
+ <div class="header-actions">
264
+ <div class="header-status">
265
+ <div class="connection-pill ${connClass}">
266
+ <span class="connection-dot"></span>
267
+ ${connLabel}
268
+ </div>
269
+ ${freshnessLabel
270
+ ? html`<div class="header-freshness">${freshnessLabel}</div>`
271
+ : null}
265
272
  </div>
266
- ${freshnessLabel ? html`<span class="text-xs opacity-50">${freshnessLabel}</span>` : null}
273
+ ${user
274
+ ? html`<div class="app-header-user">@${user.username || user.first_name}</div>`
275
+ : null}
267
276
  </div>
268
- ${user ? html`<div class="text-xs opacity-60">@${user.username || user.first_name}</div>` : null}
269
277
  </div>
270
278
  </header>
271
279
  `;
@@ -278,49 +286,51 @@ function SidebarNav() {
278
286
  const user = getTelegramUser();
279
287
  const isConn = connected.value;
280
288
  return html`
281
- <aside class="sidebar flex flex-col bg-base-200 border-r border-base-content/5 h-full w-[var(--sidebar-width)]">
282
- <div class="p-3 flex items-center gap-2">
283
- <img src="logo.png" alt="Bosun" class="w-8 h-8 rounded" />
284
- <span class="font-semibold text-sm">Bosun</span>
289
+ <aside class="sidebar">
290
+ <div class="sidebar-brand">
291
+ <div class="sidebar-logo">
292
+ <img src="logo.png" alt="Bosun" class="app-logo-img" />
293
+ </div>
294
+ <div class="sidebar-title">Bosun</div>
285
295
  </div>
286
- <div class="px-3 flex flex-col gap-1.5 mb-3">
287
- <button class="btn btn-primary btn-sm w-full gap-1" onClick=${() => createSession({ type: "primary" })}>
288
- New Session
296
+ <div class="sidebar-actions">
297
+ <button class="btn btn-primary btn-block" onClick=${() => createSession({ type: "primary" })}>
298
+ <span class="btn-icon">✨</span> New Session
289
299
  </button>
290
- <button class="btn btn-ghost btn-sm w-full gap-1" onClick=${() => navigateTo("tasks")}>
291
- 📋 View Tasks
300
+ <button class="btn btn-ghost btn-block" onClick=${() => navigateTo("tasks")}>
301
+ <span class="btn-icon">📋</span> View Tasks
292
302
  </button>
293
303
  </div>
294
- <ul class="menu menu-sm flex-1 px-2 gap-0.5">
304
+ <nav class="sidebar-nav">
295
305
  ${TAB_CONFIG.map((tab) => {
296
306
  const isActive = activeTab.value === tab.id;
297
307
  const isHome = tab.id === "dashboard";
298
308
  return html`
299
- <li key=${tab.id}>
300
- <a
301
- class=${isActive ? "active font-medium" : ""}
302
- aria-label=${tab.label}
303
- aria-current=${isActive ? "page" : null}
304
- onClick=${() => navigateTo(tab.id, {
309
+ <button
310
+ key=${tab.id}
311
+ class="sidebar-nav-item ${isActive ? "active" : ""}"
312
+ aria-label=${tab.label}
313
+ aria-current=${isActive ? "page" : null}
314
+ onClick=${() =>
315
+ navigateTo(tab.id, {
305
316
  resetHistory: isHome,
306
317
  forceRefresh: isHome && isActive,
307
318
  })}
308
- >
309
- ${ICONS[tab.icon]}
310
- <span>${tab.label}</span>
311
- </a>
312
- </li>
319
+ >
320
+ ${ICONS[tab.icon]}
321
+ <span>${tab.label}</span>
322
+ </button>
313
323
  `;
314
324
  })}
315
- </ul>
316
- <div class="p-3 border-t border-base-content/5">
317
- <div class="flex items-center gap-2 text-xs">
318
- <span class="w-2 h-2 rounded-full ${isConn ? "bg-success" : "bg-error"}"></span>
319
- <span class="opacity-70">${isConn ? "Connected" : "Offline"}</span>
325
+ </nav>
326
+ <div class="sidebar-footer">
327
+ <div class="sidebar-status ${isConn ? "online" : "offline"}">
328
+ <span class="sidebar-status-dot"></span>
329
+ ${isConn ? "Connected" : "Offline"}
320
330
  </div>
321
331
  ${user
322
- ? html`<div class="text-xs opacity-50 mt-1 truncate">@${user.username || user.first_name || "operator"}</div>`
323
- : html`<div class="text-xs opacity-50 mt-1">Operator Console</div>`}
332
+ ? html`<div class="sidebar-user">@${user.username || user.first_name || "operator"}</div>`
333
+ : html`<div class="sidebar-user">Operator Console</div>`}
324
334
  </div>
325
335
  </aside>
326
336
  `;
@@ -354,21 +364,29 @@ function SessionRail({ onResizeStart, onResizeReset, showResizer }) {
354
364
  }, [sessionsData.value, selectedSessionId.value]);
355
365
 
356
366
  return html`
357
- <aside class="session-rail flex flex-col bg-base-200 border-r border-base-content/5 overflow-hidden" style="width: var(--rail-width, 300px)">
358
- <div class="p-3 border-b border-base-content/5">
359
- <div class="font-medium text-sm">Sessions</div>
360
- <div class="text-xs opacity-50 mt-0.5">${activeCount} active · ${sessions.length} total</div>
361
- </div>
362
- <div class="flex-1 overflow-y-auto">
363
- <${SessionList} showArchived=${showArchived} onToggleArchived=${setShowArchived} defaultType="primary" />
367
+ <aside class="session-rail">
368
+ <div class="rail-header">
369
+ <div class="rail-title">Sessions</div>
370
+ <div class="rail-meta">
371
+ ${activeCount} active · ${sessions.length} total
372
+ </div>
364
373
  </div>
365
- ${showResizer ? html`
366
- <div class="w-1 cursor-col-resize hover:bg-primary/30 active:bg-primary/50 transition-colors absolute right-0 top-0 bottom-0"
367
- role="separator" aria-label="Resize sessions panel"
368
- onPointerDown=${(e) => onResizeStart("rail", e)}
369
- onDoubleClick=${() => onResizeReset("rail")}
370
- ></div>
371
- ` : null}
374
+ <${SessionList}
375
+ showArchived=${showArchived}
376
+ onToggleArchived=${setShowArchived}
377
+ defaultType="primary"
378
+ />
379
+ ${showResizer
380
+ ? html`
381
+ <div
382
+ class="rail-resizer"
383
+ role="separator"
384
+ aria-label="Resize sessions panel"
385
+ onPointerDown=${(e) => onResizeStart("rail", e)}
386
+ onDoubleClick=${() => onResizeReset("rail")}
387
+ ></div>
388
+ `
389
+ : null}
372
390
  </aside>
373
391
  `;
374
392
  }
@@ -448,34 +466,32 @@ function InspectorPanel({ onResizeStart, onResizeReset, showResizer }) {
448
466
  }, [isSessionTab, sessionId, session?.taskId, session?.branch, status]);
449
467
 
450
468
  return html`
451
- <aside class="inspector flex flex-col bg-base-200 border-l border-base-content/5 overflow-y-auto" style="width: var(--inspector-width, 300px)">
452
- <div class="inspector-section p-3 border-b border-base-content/5">
453
- <div class="inspector-title font-medium text-sm mb-2">Focus</div>
469
+ <aside class="inspector">
470
+ <div class="inspector-section">
471
+ <div class="inspector-title">Focus</div>
454
472
  ${session
455
473
  ? html`
456
- <div class="flex flex-col gap-1">
457
- <div class="inspector-kv flex justify-between text-xs"><span class="opacity-50">Session</span><strong class="truncate ml-2">${session.title || session.taskId || session.id}</strong></div>
458
- <div class="inspector-kv flex justify-between text-xs"><span class="opacity-50">Status</span><strong>${status}</strong></div>
459
- <div class="inspector-kv flex justify-between text-xs"><span class="opacity-50">Type</span><strong>${type}</strong></div>
460
- <div class="inspector-kv flex justify-between text-xs"><span class="opacity-50">Last Active</span><strong>${lastActive ? formatRelative(lastActive) : "—"}</strong></div>
461
- <div class="inspector-kv flex justify-between text-xs"><span class="opacity-50">Preview</span><strong>${preview}</strong></div>
462
- </div>
474
+ <div class="inspector-kv"><span>Session</span><strong>${session.title || session.taskId || session.id}</strong></div>
475
+ <div class="inspector-kv"><span>Status</span><strong>${status}</strong></div>
476
+ <div class="inspector-kv"><span>Type</span><strong>${type}</strong></div>
477
+ <div class="inspector-kv"><span>Last Active</span><strong>${lastActive ? formatRelative(lastActive) : "—"}</strong></div>
478
+ <div class="inspector-kv"><span>Preview</span><strong>${preview}</strong></div>
463
479
  `
464
- : html`<div class="inspector-empty text-xs opacity-40">Select a session to see context.</div>`}
480
+ : html`<div class="inspector-empty">Select a session to see context.</div>`}
465
481
  </div>
466
482
 
467
483
  ${isSessionTab
468
484
  ? html`
469
- <div class="inspector-section inspector-scroll p-3 border-b border-base-content/5">
470
- <div class="inspector-title font-medium text-sm mb-2">Latest Diff</div>
485
+ <div class="inspector-section inspector-scroll">
486
+ <div class="inspector-title">Latest Diff</div>
471
487
  <${DiffViewer} sessionId=${sessionId} />
472
488
  </div>
473
- <div class="inspector-section p-3">
474
- <div class="inspector-title font-medium text-sm mb-2">Smart Logs</div>
489
+ <div class="inspector-section">
490
+ <div class="inspector-title">Smart Logs</div>
475
491
  ${logState === "error"
476
- ? html`<div class="inspector-empty text-xs opacity-40">Log stream unavailable.</div>`
492
+ ? html`<div class="inspector-empty">Log stream unavailable.</div>`
477
493
  : smartLogs.length === 0
478
- ? html`<div class="inspector-empty text-xs opacity-40">No noteworthy logs right now.</div>`
494
+ ? html`<div class="inspector-empty">No noteworthy logs right now.</div>`
479
495
  : html`
480
496
  <div class="inspector-scroll">
481
497
  ${smartLogs.map(
@@ -498,17 +514,17 @@ function InspectorPanel({ onResizeStart, onResizeReset, showResizer }) {
498
514
  </div>
499
515
  `
500
516
  : html`
501
- <div class="inspector-section p-3">
502
- <div class="inspector-title font-medium text-sm mb-2">System Pulse</div>
503
- <div class="inspector-kv flex justify-between text-xs"><span class="opacity-50">API</span><strong>${connected.value ? "Connected" : "Offline"}</strong></div>
504
- <div class="inspector-kv flex justify-between text-xs"><span class="opacity-50">WebSocket</span><strong>${wsConnected.value ? "Live" : "Closed"}</strong></div>
505
- <div class="inspector-kv flex justify-between text-xs"><span class="opacity-50">Last Seen</span><strong>${backendLastSeen.value ? formatRelative(backendLastSeen.value) : "—"}</strong></div>
517
+ <div class="inspector-section">
518
+ <div class="inspector-title">System Pulse</div>
519
+ <div class="inspector-kv"><span>API</span><strong>${connected.value ? "Connected" : "Offline"}</strong></div>
520
+ <div class="inspector-kv"><span>WebSocket</span><strong>${wsConnected.value ? "Live" : "Closed"}</strong></div>
521
+ <div class="inspector-kv"><span>Last Seen</span><strong>${backendLastSeen.value ? formatRelative(backendLastSeen.value) : "—"}</strong></div>
506
522
  </div>
507
523
  `}
508
524
  ${showResizer
509
525
  ? html`
510
526
  <div
511
- class="inspector-resizer w-1 cursor-col-resize hover:bg-primary/30 active:bg-primary/50 transition-colors absolute left-0 top-0 bottom-0"
527
+ class="inspector-resizer"
512
528
  role="separator"
513
529
  aria-label="Resize inspector panel"
514
530
  onPointerDown=${(e) => onResizeStart("inspector", e)}
@@ -535,23 +551,37 @@ function getTabsById(ids) {
535
551
  function BottomNav({ compact, moreOpen, onToggleMore, onNavigate }) {
536
552
  const primaryTabs = getTabsById(PRIMARY_NAV_TABS);
537
553
  return html`
538
- <nav class="btm-nav btm-nav-sm bg-base-200 border-t border-base-content/5 z-40">
554
+ <nav class=${`bottom-nav ${compact ? "compact" : ""}`}>
539
555
  ${primaryTabs.map((tab) => {
540
556
  const isHome = tab.id === "dashboard";
541
557
  const isActive = activeTab.value === tab.id;
542
558
  return html`
543
- <button key=${tab.id}
544
- class=${isActive ? "active text-primary" : ""}
545
- onClick=${() => onNavigate(tab.id, { resetHistory: isHome, forceRefresh: isHome && isActive })}>
559
+ <button
560
+ key=${tab.id}
561
+ class="nav-item ${isActive ? "active" : ""}"
562
+ aria-label=${`Go to ${tab.label}`}
563
+ type="button"
564
+ onClick=${() =>
565
+ onNavigate(tab.id, {
566
+ resetHistory: isHome,
567
+ forceRefresh: isHome && isActive,
568
+ })}
569
+ >
546
570
  ${ICONS[tab.icon]}
547
- <span class="btm-nav-label text-xs">${tab.label}</span>
571
+ <span class="nav-label">${tab.label}</span>
548
572
  </button>
549
573
  `;
550
574
  })}
551
- <button class=${moreOpen ? "active text-primary" : ""}
552
- onClick=${onToggleMore}>
575
+ <button
576
+ class="nav-item nav-item-more ${moreOpen ? "active" : ""}"
577
+ aria-haspopup="dialog"
578
+ aria-expanded=${moreOpen ? "true" : "false"}
579
+ aria-label=${moreOpen ? "Close more menu" : "Open more menu"}
580
+ type="button"
581
+ onClick=${onToggleMore}
582
+ >
553
583
  ${ICONS.ellipsis}
554
- <span class="btm-nav-label text-xs">More</span>
584
+ <span class="nav-label">More</span>
555
585
  </button>
556
586
  </nav>
557
587
  `;
@@ -563,47 +593,47 @@ function MoreSheet({ open, onClose, onNavigate }) {
563
593
  return html`
564
594
  <${Modal} title="More" open=${open} onClose=${onClose}>
565
595
  <div class="more-menu" role="navigation" aria-label="More menu">
566
- <div class="more-menu-section mb-4">
567
- <div class="more-menu-section-title text-xs font-semibold opacity-50 mb-2">Quick Access</div>
568
- <div class="grid grid-cols-4 gap-2 p-2">
596
+ <div class="more-menu-section">
597
+ <div class="more-menu-section-title">Quick Access</div>
598
+ <div class="more-menu-grid">
569
599
  ${primaryTabs.map((tab) => {
570
600
  const isHome = tab.id === "dashboard";
571
601
  const isActive = activeTab.value === tab.id;
572
602
  return html`
573
603
  <button
574
604
  key=${tab.id}
575
- class="btn btn-ghost btn-sm flex flex-col items-center gap-1 h-auto py-2 ${isActive ? "btn-active" : ""}"
605
+ class="more-menu-item ${isActive ? "active" : ""}"
576
606
  aria-label=${`Open ${tab.label}`}
577
607
  onClick=${() =>
578
608
  onNavigate(tab.id, {
579
609
  resetHistory: isHome,
580
610
  })}
581
611
  >
582
- <span class="text-lg">${ICONS[tab.icon]}</span>
583
- <span class="text-xs">${tab.label}</span>
612
+ <span class="more-menu-icon">${ICONS[tab.icon]}</span>
613
+ <span class="more-menu-label">${tab.label}</span>
584
614
  </button>
585
615
  `;
586
616
  })}
587
617
  </div>
588
618
  </div>
589
619
  <div class="more-menu-section">
590
- <div class="more-menu-section-title text-xs font-semibold opacity-50 mb-2">Explore</div>
591
- <div class="grid grid-cols-4 gap-2 p-2">
620
+ <div class="more-menu-section-title">Explore</div>
621
+ <div class="more-menu-grid">
592
622
  ${moreTabs.map((tab) => {
593
623
  const isHome = tab.id === "dashboard";
594
624
  const isActive = activeTab.value === tab.id;
595
625
  return html`
596
626
  <button
597
627
  key=${tab.id}
598
- class="btn btn-ghost btn-sm flex flex-col items-center gap-1 h-auto py-2 ${isActive ? "btn-active" : ""}"
628
+ class="more-menu-item ${isActive ? "active" : ""}"
599
629
  aria-label=${`Open ${tab.label}`}
600
630
  onClick=${() =>
601
631
  onNavigate(tab.id, {
602
632
  resetHistory: isHome,
603
633
  })}
604
634
  >
605
- <span class="text-lg">${ICONS[tab.icon]}</span>
606
- <span class="text-xs">${tab.label}</span>
635
+ <span class="more-menu-icon">${ICONS[tab.icon]}</span>
636
+ <span class="more-menu-label">${tab.label}</span>
607
637
  </button>
608
638
  `;
609
639
  })}
@@ -967,7 +997,7 @@ function App() {
967
997
 
968
998
  return html`
969
999
  <div
970
- class="app-shell flex h-screen bg-base-100 overflow-hidden"
1000
+ class="app-shell"
971
1001
  style=${shellStyle}
972
1002
  data-tab=${activeTab.value}
973
1003
  data-has-rail=${showSessionRail ? "true" : "false"}
@@ -978,8 +1008,8 @@ function App() {
978
1008
  ${/* Sidebar drawer overlay for tablet */ ""}
979
1009
  ${sidebarDrawerOpen && !isDesktop
980
1010
  ? html`
981
- <div class="drawer-overlay fixed inset-0 bg-black/50 z-40" onClick=${closeDrawers}></div>
982
- <div class="drawer drawer-left fixed top-0 bottom-0 left-0 z-50 w-72">
1011
+ <div class="drawer-overlay" onClick=${closeDrawers}></div>
1012
+ <div class="drawer drawer-left">
983
1013
  <${SidebarNav} />
984
1014
  </div>
985
1015
  `
@@ -988,8 +1018,8 @@ function App() {
988
1018
  ${/* Inspector drawer overlay for tablet */ ""}
989
1019
  ${inspectorDrawerOpen && !isDesktop
990
1020
  ? html`
991
- <div class="drawer-overlay fixed inset-0 bg-black/50 z-40" onClick=${closeDrawers}></div>
992
- <div class="drawer drawer-right fixed top-0 bottom-0 right-0 z-50 w-72">
1021
+ <div class="drawer-overlay" onClick=${closeDrawers}></div>
1022
+ <div class="drawer drawer-right">
993
1023
  <${InspectorPanel}
994
1024
  onResizeStart=${handleResizeStart}
995
1025
  onResizeReset=${handleResizeReset}
@@ -1006,14 +1036,14 @@ function App() {
1006
1036
  showResizer=${isDesktop}
1007
1037
  />`
1008
1038
  : null}
1009
- <div class="app-main flex flex-col flex-1 min-w-0">
1010
- <div class="main-panel flex flex-col flex-1 min-w-0">
1039
+ <div class="app-main">
1040
+ <div class="main-panel">
1011
1041
  <${Header} />
1012
1042
 
1013
1043
  ${/* Tablet action bar with drawer toggles */ ""}
1014
1044
  ${showDrawerToggles
1015
1045
  ? html`
1016
- <div class="tablet-action-bar flex items-center gap-2 px-4 py-2 bg-base-200 border-b border-base-content/5">
1046
+ <div class="tablet-action-bar">
1017
1047
  <button
1018
1048
  class="btn btn-ghost btn-sm tablet-toggle"
1019
1049
  onClick=${toggleSidebar}
@@ -1040,14 +1070,14 @@ function App() {
1040
1070
  <${ToastContainer} />
1041
1071
  <${CommandPalette} open=${paletteOpen} onClose=${paletteClose} />
1042
1072
  <${PullToRefresh} onRefresh=${() => refreshTab(activeTab.value)}>
1043
- <main class="main-content flex-1 overflow-y-auto p-4" ref=${mainRef}>
1073
+ <main class="main-content" ref=${mainRef}>
1044
1074
  <${CurrentTab} />
1045
1075
  </main>
1046
1076
  <//>
1047
1077
  ${showScrollTop &&
1048
1078
  html`
1049
1079
  <button
1050
- class="scroll-top btn btn-circle btn-sm btn-primary fixed bottom-20 right-4 z-30 shadow-lg"
1080
+ class="scroll-top"
1051
1081
  title="Back to top"
1052
1082
  onClick=${() => {
1053
1083
  mainRef.current?.scrollTo({ top: 0, behavior: "smooth" });