bosun 0.28.4 → 0.29.0

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.28.4",
3
+ "version": "0.29.0",
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",
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">
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>
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>
188
188
  ${backendLastSeen.value
189
- ? html`<div class="offline-banner-meta">Last connected: ${formatTimeAgo(backendLastSeen.value)}</div>`
189
+ ? html`<div class="text-xs opacity-70">Last connected: ${formatTimeAgo(backendLastSeen.value)}</div>`
190
190
  : null}
191
- <div class="offline-banner-meta">Retry attempt #${backendRetryCount.value}</div>
191
+ <div class="text-xs opacity-70">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,27 +253,19 @@ function Header() {
253
253
  }
254
254
 
255
255
  return html`
256
- <header class="app-header">
257
- <div class="app-header-left">
258
- <div class="app-header-workspace">
259
- <${WorkspaceSwitcher} />
260
- </div>
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} />
261
259
  </div>
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}
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}
272
265
  </div>
273
- ${user
274
- ? html`<div class="app-header-user">@${user.username || user.first_name}</div>`
275
- : null}
266
+ ${freshnessLabel ? html`<span class="text-xs opacity-50">${freshnessLabel}</span>` : null}
276
267
  </div>
268
+ ${user ? html`<div class="text-xs opacity-60">@${user.username || user.first_name}</div>` : null}
277
269
  </div>
278
270
  </header>
279
271
  `;
@@ -286,51 +278,49 @@ function SidebarNav() {
286
278
  const user = getTelegramUser();
287
279
  const isConn = connected.value;
288
280
  return html`
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>
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>
295
285
  </div>
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
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
299
289
  </button>
300
- <button class="btn btn-ghost btn-block" onClick=${() => navigateTo("tasks")}>
301
- <span class="btn-icon">📋</span> View Tasks
290
+ <button class="btn btn-ghost btn-sm w-full gap-1" onClick=${() => navigateTo("tasks")}>
291
+ 📋 View Tasks
302
292
  </button>
303
293
  </div>
304
- <nav class="sidebar-nav">
294
+ <ul class="menu menu-sm flex-1 px-2 gap-0.5">
305
295
  ${TAB_CONFIG.map((tab) => {
306
296
  const isActive = activeTab.value === tab.id;
307
297
  const isHome = tab.id === "dashboard";
308
298
  return html`
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, {
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, {
316
305
  resetHistory: isHome,
317
306
  forceRefresh: isHome && isActive,
318
307
  })}
319
- >
320
- ${ICONS[tab.icon]}
321
- <span>${tab.label}</span>
322
- </button>
308
+ >
309
+ ${ICONS[tab.icon]}
310
+ <span>${tab.label}</span>
311
+ </a>
312
+ </li>
323
313
  `;
324
314
  })}
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"}
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>
330
320
  </div>
331
321
  ${user
332
- ? html`<div class="sidebar-user">@${user.username || user.first_name || "operator"}</div>`
333
- : html`<div class="sidebar-user">Operator Console</div>`}
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>`}
334
324
  </div>
335
325
  </aside>
336
326
  `;
@@ -364,29 +354,21 @@ function SessionRail({ onResizeStart, onResizeReset, showResizer }) {
364
354
  }, [sessionsData.value, selectedSessionId.value]);
365
355
 
366
356
  return html`
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>
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>
373
361
  </div>
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}
362
+ <div class="flex-1 overflow-y-auto">
363
+ <${SessionList} showArchived=${showArchived} onToggleArchived=${setShowArchived} defaultType="primary" />
364
+ </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}
390
372
  </aside>
391
373
  `;
392
374
  }
@@ -466,32 +448,34 @@ function InspectorPanel({ onResizeStart, onResizeReset, showResizer }) {
466
448
  }, [isSessionTab, sessionId, session?.taskId, session?.branch, status]);
467
449
 
468
450
  return html`
469
- <aside class="inspector">
470
- <div class="inspector-section">
471
- <div class="inspector-title">Focus</div>
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>
472
454
  ${session
473
455
  ? html`
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>
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>
479
463
  `
480
- : html`<div class="inspector-empty">Select a session to see context.</div>`}
464
+ : html`<div class="inspector-empty text-xs opacity-40">Select a session to see context.</div>`}
481
465
  </div>
482
466
 
483
467
  ${isSessionTab
484
468
  ? html`
485
- <div class="inspector-section inspector-scroll">
486
- <div class="inspector-title">Latest Diff</div>
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>
487
471
  <${DiffViewer} sessionId=${sessionId} />
488
472
  </div>
489
- <div class="inspector-section">
490
- <div class="inspector-title">Smart Logs</div>
473
+ <div class="inspector-section p-3">
474
+ <div class="inspector-title font-medium text-sm mb-2">Smart Logs</div>
491
475
  ${logState === "error"
492
- ? html`<div class="inspector-empty">Log stream unavailable.</div>`
476
+ ? html`<div class="inspector-empty text-xs opacity-40">Log stream unavailable.</div>`
493
477
  : smartLogs.length === 0
494
- ? html`<div class="inspector-empty">No noteworthy logs right now.</div>`
478
+ ? html`<div class="inspector-empty text-xs opacity-40">No noteworthy logs right now.</div>`
495
479
  : html`
496
480
  <div class="inspector-scroll">
497
481
  ${smartLogs.map(
@@ -514,17 +498,17 @@ function InspectorPanel({ onResizeStart, onResizeReset, showResizer }) {
514
498
  </div>
515
499
  `
516
500
  : html`
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>
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>
522
506
  </div>
523
507
  `}
524
508
  ${showResizer
525
509
  ? html`
526
510
  <div
527
- class="inspector-resizer"
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"
528
512
  role="separator"
529
513
  aria-label="Resize inspector panel"
530
514
  onPointerDown=${(e) => onResizeStart("inspector", e)}
@@ -551,37 +535,23 @@ function getTabsById(ids) {
551
535
  function BottomNav({ compact, moreOpen, onToggleMore, onNavigate }) {
552
536
  const primaryTabs = getTabsById(PRIMARY_NAV_TABS);
553
537
  return html`
554
- <nav class=${`bottom-nav ${compact ? "compact" : ""}`}>
538
+ <nav class="btm-nav btm-nav-sm bg-base-200 border-t border-base-content/5 z-40">
555
539
  ${primaryTabs.map((tab) => {
556
540
  const isHome = tab.id === "dashboard";
557
541
  const isActive = activeTab.value === tab.id;
558
542
  return html`
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
- >
543
+ <button key=${tab.id}
544
+ class=${isActive ? "active text-primary" : ""}
545
+ onClick=${() => onNavigate(tab.id, { resetHistory: isHome, forceRefresh: isHome && isActive })}>
570
546
  ${ICONS[tab.icon]}
571
- <span class="nav-label">${tab.label}</span>
547
+ <span class="btm-nav-label text-xs">${tab.label}</span>
572
548
  </button>
573
549
  `;
574
550
  })}
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
- >
551
+ <button class=${moreOpen ? "active text-primary" : ""}
552
+ onClick=${onToggleMore}>
583
553
  ${ICONS.ellipsis}
584
- <span class="nav-label">More</span>
554
+ <span class="btm-nav-label text-xs">More</span>
585
555
  </button>
586
556
  </nav>
587
557
  `;
@@ -593,47 +563,47 @@ function MoreSheet({ open, onClose, onNavigate }) {
593
563
  return html`
594
564
  <${Modal} title="More" open=${open} onClose=${onClose}>
595
565
  <div class="more-menu" role="navigation" aria-label="More menu">
596
- <div class="more-menu-section">
597
- <div class="more-menu-section-title">Quick Access</div>
598
- <div class="more-menu-grid">
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">
599
569
  ${primaryTabs.map((tab) => {
600
570
  const isHome = tab.id === "dashboard";
601
571
  const isActive = activeTab.value === tab.id;
602
572
  return html`
603
573
  <button
604
574
  key=${tab.id}
605
- class="more-menu-item ${isActive ? "active" : ""}"
575
+ class="btn btn-ghost btn-sm flex flex-col items-center gap-1 h-auto py-2 ${isActive ? "btn-active" : ""}"
606
576
  aria-label=${`Open ${tab.label}`}
607
577
  onClick=${() =>
608
578
  onNavigate(tab.id, {
609
579
  resetHistory: isHome,
610
580
  })}
611
581
  >
612
- <span class="more-menu-icon">${ICONS[tab.icon]}</span>
613
- <span class="more-menu-label">${tab.label}</span>
582
+ <span class="text-lg">${ICONS[tab.icon]}</span>
583
+ <span class="text-xs">${tab.label}</span>
614
584
  </button>
615
585
  `;
616
586
  })}
617
587
  </div>
618
588
  </div>
619
589
  <div class="more-menu-section">
620
- <div class="more-menu-section-title">Explore</div>
621
- <div class="more-menu-grid">
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">
622
592
  ${moreTabs.map((tab) => {
623
593
  const isHome = tab.id === "dashboard";
624
594
  const isActive = activeTab.value === tab.id;
625
595
  return html`
626
596
  <button
627
597
  key=${tab.id}
628
- class="more-menu-item ${isActive ? "active" : ""}"
598
+ class="btn btn-ghost btn-sm flex flex-col items-center gap-1 h-auto py-2 ${isActive ? "btn-active" : ""}"
629
599
  aria-label=${`Open ${tab.label}`}
630
600
  onClick=${() =>
631
601
  onNavigate(tab.id, {
632
602
  resetHistory: isHome,
633
603
  })}
634
604
  >
635
- <span class="more-menu-icon">${ICONS[tab.icon]}</span>
636
- <span class="more-menu-label">${tab.label}</span>
605
+ <span class="text-lg">${ICONS[tab.icon]}</span>
606
+ <span class="text-xs">${tab.label}</span>
637
607
  </button>
638
608
  `;
639
609
  })}
@@ -997,7 +967,7 @@ function App() {
997
967
 
998
968
  return html`
999
969
  <div
1000
- class="app-shell"
970
+ class="app-shell flex h-screen bg-base-100 overflow-hidden"
1001
971
  style=${shellStyle}
1002
972
  data-tab=${activeTab.value}
1003
973
  data-has-rail=${showSessionRail ? "true" : "false"}
@@ -1008,8 +978,8 @@ function App() {
1008
978
  ${/* Sidebar drawer overlay for tablet */ ""}
1009
979
  ${sidebarDrawerOpen && !isDesktop
1010
980
  ? html`
1011
- <div class="drawer-overlay" onClick=${closeDrawers}></div>
1012
- <div class="drawer drawer-left">
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">
1013
983
  <${SidebarNav} />
1014
984
  </div>
1015
985
  `
@@ -1018,8 +988,8 @@ function App() {
1018
988
  ${/* Inspector drawer overlay for tablet */ ""}
1019
989
  ${inspectorDrawerOpen && !isDesktop
1020
990
  ? html`
1021
- <div class="drawer-overlay" onClick=${closeDrawers}></div>
1022
- <div class="drawer drawer-right">
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">
1023
993
  <${InspectorPanel}
1024
994
  onResizeStart=${handleResizeStart}
1025
995
  onResizeReset=${handleResizeReset}
@@ -1036,14 +1006,14 @@ function App() {
1036
1006
  showResizer=${isDesktop}
1037
1007
  />`
1038
1008
  : null}
1039
- <div class="app-main">
1040
- <div class="main-panel">
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">
1041
1011
  <${Header} />
1042
1012
 
1043
1013
  ${/* Tablet action bar with drawer toggles */ ""}
1044
1014
  ${showDrawerToggles
1045
1015
  ? html`
1046
- <div class="tablet-action-bar">
1016
+ <div class="tablet-action-bar flex items-center gap-2 px-4 py-2 bg-base-200 border-b border-base-content/5">
1047
1017
  <button
1048
1018
  class="btn btn-ghost btn-sm tablet-toggle"
1049
1019
  onClick=${toggleSidebar}
@@ -1070,14 +1040,14 @@ function App() {
1070
1040
  <${ToastContainer} />
1071
1041
  <${CommandPalette} open=${paletteOpen} onClose=${paletteClose} />
1072
1042
  <${PullToRefresh} onRefresh=${() => refreshTab(activeTab.value)}>
1073
- <main class="main-content" ref=${mainRef}>
1043
+ <main class="main-content flex-1 overflow-y-auto p-4" ref=${mainRef}>
1074
1044
  <${CurrentTab} />
1075
1045
  </main>
1076
1046
  <//>
1077
1047
  ${showScrollTop &&
1078
1048
  html`
1079
1049
  <button
1080
- class="scroll-top"
1050
+ class="scroll-top btn btn-circle btn-sm btn-primary fixed bottom-20 right-4 z-30 shadow-lg"
1081
1051
  title="Back to top"
1082
1052
  onClick=${() => {
1083
1053
  mainRef.current?.scrollTo({ top: 0, behavior: "smooth" });