bosun 0.36.2 → 0.36.4

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.
Files changed (57) hide show
  1. package/agent-prompts.mjs +95 -0
  2. package/analyze-agent-work-helpers.mjs +308 -0
  3. package/analyze-agent-work.mjs +926 -0
  4. package/autofix.mjs +2 -0
  5. package/bosun.schema.json +101 -3
  6. package/codex-shell.mjs +85 -10
  7. package/desktop/main.mjs +871 -48
  8. package/desktop/preload.mjs +54 -1
  9. package/desktop-shortcut.mjs +90 -11
  10. package/git-editor-fix.mjs +273 -0
  11. package/mcp-registry.mjs +579 -0
  12. package/meeting-workflow-service.mjs +631 -0
  13. package/monitor.mjs +18 -103
  14. package/package.json +21 -2
  15. package/primary-agent.mjs +32 -12
  16. package/session-tracker.mjs +68 -0
  17. package/setup-web-server.mjs +20 -10
  18. package/setup.mjs +376 -83
  19. package/startup-service.mjs +51 -6
  20. package/stream-resilience.mjs +17 -7
  21. package/ui/app.js +164 -4
  22. package/ui/components/agent-selector.js +145 -1
  23. package/ui/components/chat-view.js +161 -15
  24. package/ui/components/session-list.js +2 -2
  25. package/ui/components/shared.js +188 -15
  26. package/ui/modules/icons.js +13 -0
  27. package/ui/modules/utils.js +44 -0
  28. package/ui/modules/voice-client-sdk.js +733 -0
  29. package/ui/modules/voice-overlay.js +128 -15
  30. package/ui/modules/voice.js +15 -6
  31. package/ui/setup.html +281 -81
  32. package/ui/styles/components.css +99 -3
  33. package/ui/styles/sessions.css +122 -14
  34. package/ui/styles.css +14 -0
  35. package/ui/tabs/agents.js +1 -1
  36. package/ui/tabs/chat.js +123 -14
  37. package/ui/tabs/control.js +16 -22
  38. package/ui/tabs/dashboard.js +85 -8
  39. package/ui/tabs/library.js +113 -17
  40. package/ui/tabs/settings.js +116 -2
  41. package/ui/tabs/tasks.js +388 -39
  42. package/ui/tabs/telemetry.js +0 -1
  43. package/ui/tabs/workflows.js +4 -0
  44. package/ui-server.mjs +400 -22
  45. package/update-check.mjs +41 -13
  46. package/voice-action-dispatcher.mjs +844 -0
  47. package/voice-agents-sdk.mjs +664 -0
  48. package/voice-auth-manager.mjs +164 -0
  49. package/voice-relay.mjs +1194 -0
  50. package/voice-tools.mjs +914 -0
  51. package/workflow-templates/agents.mjs +6 -2
  52. package/workflow-templates/github.mjs +154 -12
  53. package/workflow-templates.mjs +3 -0
  54. package/github-reconciler.mjs +0 -506
  55. package/merge-strategy.mjs +0 -1210
  56. package/pr-cleanup-daemon.mjs +0 -992
  57. package/workspace-reaper.mjs +0 -405
@@ -11,11 +11,25 @@ const html = htm.bind(h);
11
11
 
12
12
  import { haptic } from "../modules/telegram.js";
13
13
  import { apiFetch } from "../modules/api.js";
14
- import { showToast, refreshTab } from "../modules/state.js";
14
+ import {
15
+ showToast,
16
+ refreshTab,
17
+ setPendingChange,
18
+ clearPendingChange,
19
+ } from "../modules/state.js";
15
20
  import { ICONS } from "../modules/icons.js";
16
21
  import { iconText, resolveIcon } from "../modules/icon-utils.js";
17
- import { formatRelative } from "../modules/utils.js";
18
- import { Card, Badge, EmptyState, Modal, ConfirmDialog, Spinner, ListItem } from "../components/shared.js";
22
+ import { formatRelative, countChangedFields } from "../modules/utils.js";
23
+ import {
24
+ Card,
25
+ Badge,
26
+ EmptyState,
27
+ Modal,
28
+ ConfirmDialog,
29
+ Spinner,
30
+ ListItem,
31
+ SaveDiscardBar,
32
+ } from "../components/shared.js";
19
33
  import { SearchInput, SegmentedControl, Toggle } from "../components/forms.js";
20
34
 
21
35
  /* ═══════════════════════════════════════════════════════════════
@@ -41,14 +55,17 @@ const LIBRARY_STYLES = `
41
55
 
42
56
  .library-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr)); gap: 12px; }
43
57
  .library-card { background: var(--bg-card, #1a1a2e); border: 1px solid var(--border, #333);
44
- border-radius: 12px; padding: 16px; cursor: pointer; transition: all 0.15s; position: relative; }
58
+ border-radius: 12px; padding: 16px; padding-right: 96px; cursor: pointer; transition: all 0.15s; position: relative; }
45
59
  .library-card:hover { border-color: var(--accent, #58a6ff); transform: translateY(-1px);
46
60
  box-shadow: 0 4px 12px rgba(0,0,0,0.3); }
47
61
 
48
62
  .library-card-header { display: flex; align-items: flex-start; gap: 10px; margin-bottom: 8px; }
49
63
  .library-card-icon { font-size: 1.4em; flex-shrink: 0; width: 32px; text-align: center; }
50
64
  .library-card-icon svg { width: 20px; height: 20px; vertical-align: middle; }
51
- .library-card-title { font-weight: 600; font-size: 0.95em; color: var(--text-primary, #eee); }
65
+ .library-card-header > div { min-width: 0; flex: 1; }
66
+ .library-card-title { font-weight: 600; font-size: 0.95em; color: var(--text-primary, #eee);
67
+ overflow-wrap: anywhere; word-break: break-word;
68
+ display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
52
69
  .library-card-desc { font-size: 0.82em; color: var(--text-secondary, #aaa); margin-bottom: 8px;
53
70
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
54
71
  .library-card-meta { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
@@ -123,7 +140,7 @@ const LIBRARY_STYLES = `
123
140
  .library-toolbar { gap: 6px; }
124
141
  .library-toolbar .search-wrap { flex: 1 1 100%; min-width: 0; }
125
142
  .library-grid { grid-template-columns: 1fr; }
126
- .library-card { padding: 12px; }
143
+ .library-card { padding: 12px; padding-right: 84px; }
127
144
  .library-card-scope { margin-left: 0; }
128
145
  .library-actions { flex-wrap: wrap; justify-content: stretch; }
129
146
  .library-actions button { flex: 1 1 140px; padding: 10px 12px; }
@@ -307,7 +324,7 @@ function LibraryCard({ entry, onSelect }) {
307
324
 
308
325
  function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
309
326
  const isNew = !entry?.id;
310
- const [form, setForm] = useState({
327
+ const initialFormSnapshot = {
311
328
  id: entry?.id || "",
312
329
  type: entry?.type || "prompt",
313
330
  name: entry?.name || "",
@@ -315,10 +332,31 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
315
332
  tags: (entry?.tags || []).join(", "),
316
333
  scope: entry?.scope || "global",
317
334
  content: "",
318
- });
335
+ };
336
+ const [form, setForm] = useState(initialFormSnapshot);
337
+ const [baseline, setBaseline] = useState(initialFormSnapshot);
319
338
  const [loading, setLoading] = useState(false);
320
339
  const [loadingContent, setLoadingContent] = useState(!isNew && !!entry?.id);
321
340
  const [confirmDelete, setConfirmDelete] = useState(false);
341
+ const pendingKey = useMemo(
342
+ () => `modal:library-entry:${entry?.id || "new"}`,
343
+ [entry?.id],
344
+ );
345
+
346
+ useEffect(() => {
347
+ const next = {
348
+ id: entry?.id || "",
349
+ type: entry?.type || "prompt",
350
+ name: entry?.name || "",
351
+ description: entry?.description || "",
352
+ tags: (entry?.tags || []).join(", "),
353
+ scope: entry?.scope || "global",
354
+ content: "",
355
+ };
356
+ setForm(next);
357
+ setBaseline(next);
358
+ setLoadingContent(!isNew && !!entry?.id);
359
+ }, [entry?.id]);
322
360
 
323
361
  // Load content for existing entries
324
362
  useEffect(() => {
@@ -330,7 +368,11 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
330
368
  if (cancelled) return;
331
369
  let contentStr = detail?.content ?? "";
332
370
  if (typeof contentStr === "object") contentStr = JSON.stringify(contentStr, null, 2);
333
- setForm((f) => ({ ...f, content: contentStr }));
371
+ setForm((f) => {
372
+ const next = { ...f, content: contentStr };
373
+ setBaseline(next);
374
+ return next;
375
+ });
334
376
  } catch { /* ignore */ }
335
377
  setLoadingContent(false);
336
378
  })();
@@ -338,9 +380,27 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
338
380
  }, [entry?.id]);
339
381
 
340
382
  const updateField = (key) => (e) => setForm((f) => ({ ...f, [key]: e.target.value }));
383
+ const changeCount = useMemo(
384
+ () => countChangedFields(baseline, form),
385
+ [baseline, form],
386
+ );
387
+ const hasUnsaved = changeCount > 0;
341
388
 
342
- const handleSave = useCallback(async () => {
343
- if (!form.name.trim()) { showToast("Name is required", "error"); return; }
389
+ useEffect(() => {
390
+ setPendingChange(pendingKey, hasUnsaved);
391
+ return () => clearPendingChange(pendingKey);
392
+ }, [hasUnsaved, pendingKey]);
393
+
394
+ const resetToBaseline = useCallback(() => {
395
+ setForm(baseline);
396
+ showToast("Changes discarded", "info");
397
+ }, [baseline]);
398
+
399
+ const handleSave = useCallback(async ({ closeAfterSave = true } = {}) => {
400
+ if (!form.name.trim()) {
401
+ showToast("Name is required", "error");
402
+ return false;
403
+ }
344
404
  setLoading(true);
345
405
  try {
346
406
  const tags = form.tags.split(/[,\s]+/).map((t) => t.trim().toLowerCase()).filter(Boolean);
@@ -359,14 +419,23 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
359
419
  });
360
420
  if (res?.ok) {
361
421
  showToast(`${TYPE_LABELS[form.type] || "Entry"} saved`, "success");
362
- onSaved?.();
422
+ const nextBaseline = { ...form };
423
+ setBaseline(nextBaseline);
424
+ if (closeAfterSave) {
425
+ onSaved?.();
426
+ return { closed: true };
427
+ }
428
+ return true;
363
429
  } else {
364
430
  showToast(res?.error || "Save failed", "error");
431
+ return false;
365
432
  }
366
433
  } catch (err) {
367
434
  showToast(err.message, "error");
435
+ return false;
436
+ } finally {
437
+ setLoading(false);
368
438
  }
369
- setLoading(false);
370
439
  }, [form, onSaved]);
371
440
 
372
441
  const handleDelete = useCallback(async () => {
@@ -402,7 +471,17 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
402
471
  : "# Skill Title\n\n## Purpose\nDescribe what this skill teaches agents.\n\n## Instructions\n...";
403
472
 
404
473
  return html`
405
- <${Modal} title=${isNew ? "New Resource" : `Edit: ${entry.name}`} onClose=${onClose}>
474
+ <${Modal}
475
+ title=${isNew ? "New Resource" : `Edit: ${entry.name}`}
476
+ onClose=${onClose}
477
+ unsavedChanges=${changeCount}
478
+ onSaveBeforeClose=${() => handleSave({ closeAfterSave: true })}
479
+ onDiscardBeforeClose=${() => {
480
+ resetToBaseline();
481
+ return true;
482
+ }}
483
+ activeOperationLabel=${loading ? "Save/Delete request is still running" : ""}
484
+ >
406
485
  <div class="library-editor">
407
486
  ${isNew && html`
408
487
  <label>
@@ -459,10 +538,27 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
459
538
  `}
460
539
  <div style="flex:1" />
461
540
  <button class="btn-ghost" onClick=${onClose}>Cancel</button>
462
- <button class="btn-primary" onClick=${handleSave} disabled=${loading}>
541
+ <button
542
+ class="btn-primary"
543
+ onClick=${() => {
544
+ void handleSave({ closeAfterSave: true });
545
+ }}
546
+ disabled=${loading}
547
+ >
463
548
  ${loading ? html`<${Spinner} size=${14} />` : (isNew ? "Create" : "Save")}
464
549
  </button>
465
550
  </div>
551
+ <${SaveDiscardBar}
552
+ dirty=${hasUnsaved}
553
+ message=${`You have unsaved changes (${changeCount})`}
554
+ saveLabel=${isNew ? "Create" : "Save Changes"}
555
+ discardLabel="Discard"
556
+ onSave=${() => {
557
+ void handleSave({ closeAfterSave: false });
558
+ }}
559
+ onDiscard=${resetToBaseline}
560
+ saving=${loading}
561
+ />
466
562
  </div>
467
563
  ${confirmDelete && html`
468
564
  <${ConfirmDialog}
@@ -664,7 +760,7 @@ export function LibraryTab() {
664
760
  ${iconText(":refresh: Rebuild")}
665
761
  </button>
666
762
  <button class="library-type-pill active" onClick=${() => setEditing({})}>
667
- New
763
+ ${iconText("➕ New")}
668
764
  </button>
669
765
  </div>
670
766
 
@@ -704,7 +800,7 @@ export function LibraryTab() {
704
800
  : "Create your first prompt, agent profile, or skill."}
705
801
  action=${searchQuery.value
706
802
  ? { label: "Clear search", onClick: () => { searchQuery.value = ""; loadEntries(); } }
707
- : { label: " New Resource", onClick: () => setEditing({}) }} />
803
+ : { label: " New Resource", onClick: () => setEditing({}) }} />
708
804
  `}
709
805
 
710
806
  ${!loading && displayed.length > 0 && html`
@@ -59,6 +59,17 @@ import {
59
59
 
60
60
  /* ─── Scoped Styles ─── */
61
61
  const SETTINGS_STYLES = `
62
+ /* Category navigation */
63
+ .settings-category-mobile {
64
+ display: none;
65
+ margin-bottom: 10px;
66
+ }
67
+ .settings-category-mobile-label {
68
+ display: block;
69
+ font-size: 12px;
70
+ color: var(--text-tertiary, #8a8a8a);
71
+ margin: 0 0 6px 2px;
72
+ }
62
73
  /* Category pill tabs — horizontal scrollable row */
63
74
  .settings-category-tabs {
64
75
  display: flex;
@@ -111,9 +122,9 @@ const SETTINGS_STYLES = `
111
122
  flex-wrap: wrap;
112
123
  row-gap: 8px;
113
124
  padding: 10px 16px;
114
- min-width: 240px;
125
+ min-width: min(240px, calc(100vw - 24px));
115
126
  max-width: 480px;
116
- width: auto;
127
+ width: min(480px, calc(100vw - 24px));
117
128
  background: var(--glass-bg, rgba(30,30,46,0.95));
118
129
  backdrop-filter: blur(20px);
119
130
  -webkit-backdrop-filter: blur(20px);
@@ -340,6 +351,10 @@ const SETTINGS_STYLES = `
340
351
  color: var(--accent, #5a7cff);
341
352
  }
342
353
  .settings-banner-text { flex: 1; }
354
+ .settings-banner code {
355
+ overflow-wrap: anywhere;
356
+ word-break: break-word;
357
+ }
343
358
  /* Diff display for confirm dialog */
344
359
  .settings-diff {
345
360
  max-height: 300px;
@@ -388,6 +403,18 @@ const SETTINGS_STYLES = `
388
403
  width: 100%;
389
404
  box-sizing: border-box;
390
405
  padding-bottom: 80px;
406
+ overflow-x: clip;
407
+ }
408
+
409
+ .setting-row .segmented-control {
410
+ display: flex;
411
+ width: 100%;
412
+ flex-wrap: wrap;
413
+ margin-bottom: 0;
414
+ }
415
+ .setting-row .segmented-btn {
416
+ flex: 1 1 96px;
417
+ min-width: 0;
391
418
  }
392
419
 
393
420
  body.settings-save-open .main-content {
@@ -455,6 +482,78 @@ body.settings-save-open .main-content {
455
482
  color: var(--text-tertiary, #666);
456
483
  text-align: center;
457
484
  }
485
+
486
+ @media (max-width: 900px) {
487
+ .settings-category-mobile {
488
+ display: block;
489
+ }
490
+ .settings-category-tabs {
491
+ display: grid;
492
+ grid-template-columns: repeat(2, minmax(0, 1fr));
493
+ gap: 8px;
494
+ overflow-x: visible;
495
+ padding: 4px 0 8px;
496
+ }
497
+ .settings-category-tab {
498
+ width: 100%;
499
+ border-radius: 12px;
500
+ min-height: 42px;
501
+ padding: 10px 12px;
502
+ justify-content: flex-start;
503
+ white-space: normal;
504
+ line-height: 1.25;
505
+ }
506
+ }
507
+
508
+ @media (max-width: 700px) {
509
+ .settings-save-bar {
510
+ left: 12px;
511
+ right: 12px;
512
+ width: auto;
513
+ max-width: none;
514
+ transform: none;
515
+ padding: 10px 12px;
516
+ }
517
+ @keyframes slideUp {
518
+ from { transform: translateY(20px); opacity: 0; }
519
+ to { transform: translateY(0); opacity: 1; }
520
+ }
521
+ .settings-save-bar .save-bar-actions {
522
+ width: 100%;
523
+ justify-content: flex-end;
524
+ }
525
+ .setting-input-wrap {
526
+ flex-direction: column;
527
+ align-items: stretch;
528
+ gap: 6px;
529
+ }
530
+ .setting-input-wrap input[type="text"],
531
+ .setting-input-wrap input[type="number"],
532
+ .setting-input-wrap input[type="password"],
533
+ .setting-input-wrap textarea,
534
+ .setting-input-wrap select {
535
+ width: 100%;
536
+ flex: 1 1 auto;
537
+ }
538
+ .setting-unit {
539
+ align-self: flex-end;
540
+ }
541
+ .setting-help-tooltip {
542
+ left: 0;
543
+ transform: none;
544
+ min-width: 0;
545
+ max-width: min(92vw, 360px);
546
+ }
547
+ }
548
+
549
+ @media (max-width: 640px) {
550
+ .settings-category-tabs {
551
+ display: none;
552
+ }
553
+ .setting-row .segmented-btn {
554
+ flex: 1 1 calc(50% - 4px);
555
+ }
556
+ }
458
557
  `;
459
558
 
460
559
  /* ─── Inject styles once ─── */
@@ -1172,6 +1271,21 @@ function ServerConfigMode() {
1172
1271
  const activeCat = CATEGORIES.find((c) => c.id === activeCategory);
1173
1272
 
1174
1273
  return html`
1274
+ <div class="settings-category-mobile">
1275
+ <label class="settings-category-mobile-label">Category</label>
1276
+ <div class="setting-input-wrap">
1277
+ <select
1278
+ value=${activeCategory}
1279
+ onChange=${(e) => {
1280
+ setActiveCategory(e.target.value);
1281
+ haptic("light");
1282
+ }}
1283
+ >
1284
+ ${CATEGORIES.map((cat) => html`<option key=${cat.id} value=${cat.id}>${cat.label}</option>`)}
1285
+ </select>
1286
+ </div>
1287
+ </div>
1288
+
1175
1289
  <!-- Category tabs -->
1176
1290
  <div class="settings-category-tabs">
1177
1291
  ${CATEGORIES.map(