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
@@ -46,9 +46,9 @@ const streamConfig = readInternalExecutorStreamConfig();
46
46
  export const MAX_STREAM_RETRIES = parseNumericSetting({
47
47
  envKey: "INTERNAL_EXECUTOR_STREAM_MAX_RETRIES",
48
48
  configValue: streamConfig.maxRetries,
49
- fallback: 5,
49
+ fallback: 8,
50
50
  min: 1,
51
- max: 12,
51
+ max: 20,
52
52
  });
53
53
 
54
54
  /** Base backoff in ms. Doubles per attempt: 2 s → 4 s → 8 s → 16 s → 32 s. */
@@ -62,7 +62,7 @@ const STREAM_RETRY_BASE_MS = parseNumericSetting({
62
62
  const STREAM_RETRY_MAX_MS = parseNumericSetting({
63
63
  envKey: "INTERNAL_EXECUTOR_STREAM_RETRY_MAX_MS",
64
64
  configValue: streamConfig.retryMaxMs,
65
- fallback: 32_000,
65
+ fallback: 60_000,
66
66
  min: STREAM_RETRY_BASE_MS,
67
67
  max: 300_000,
68
68
  });
@@ -111,23 +111,33 @@ export function isTransientStreamError(err) {
111
111
  msg.includes("service_unavailable") ||
112
112
  msg.includes("529") || // Azure overloaded
113
113
  msg.includes("rate_limit_exceeded") ||
114
- msg.includes("overloaded_error") // Anthropic overloaded
114
+ msg.includes("overloaded_error") || // Anthropic overloaded
115
+ // ── Azure / Foundry specific ────────────────────────────────────────────
116
+ msg.includes("reconnecting") ||
117
+ msg.includes("upstream connect error") ||
118
+ msg.includes("no healthy upstream") ||
119
+ msg.includes("gateway timeout") ||
120
+ msg.includes("model is currently overloaded") ||
121
+ msg.includes("the server had an error") ||
122
+ msg.includes("an error occurred during streaming")
115
123
  );
116
124
  }
117
125
 
118
126
  /**
119
- * Exponential backoff delay for stream retries, with ±1 s jitter.
127
+ * Exponential backoff delay for stream retries, with ±25% jitter.
120
128
  *
121
129
  * attempt 0 → ~2 s
122
130
  * attempt 1 → ~4 s
123
131
  * attempt 2 → ~8 s
124
132
  * attempt 3 → ~16 s
125
- * attempt 4 → ~32 s (capped)
133
+ * attempt 4 → ~32 s
134
+ * attempt 5+ → ~60 s (capped)
126
135
  *
127
136
  * @param {number} attempt zero-based retry index
128
137
  * @returns {number} delay in milliseconds
129
138
  */
130
139
  export function streamRetryDelay(attempt) {
131
140
  const base = Math.min(STREAM_RETRY_BASE_MS * 2 ** attempt, STREAM_RETRY_MAX_MS);
132
- return base + Math.random() * 1_000;
141
+ // ±25% jitter to avoid thundering herd on Azure reconnect
142
+ return base + (Math.random() - 0.5) * 0.5 * base;
133
143
  }
package/ui/app.js CHANGED
@@ -74,6 +74,7 @@ const VOICE_LAUNCH_QUERY_KEYS = [
74
74
  "source",
75
75
  "chat_id",
76
76
  ];
77
+ const FLOATING_CALL_STATE_KEY = "ve-floating-call-state";
77
78
 
78
79
  function getAppLogoSource(index = 0) {
79
80
  const safeIndex = Number.isFinite(index) ? Math.trunc(index) : 0;
@@ -143,6 +144,49 @@ function scrubVoiceLaunchQuery() {
143
144
  window.history.replaceState(window.history.state, "", nextPath || "/");
144
145
  }
145
146
 
147
+ function isFollowWindowFromUrl() {
148
+ if (typeof window === "undefined") return false;
149
+ const params = new URLSearchParams(window.location.search || "");
150
+ return params.get("follow") === "1";
151
+ }
152
+
153
+ function readFloatingCallState() {
154
+ if (typeof window === "undefined") return { active: false };
155
+ try {
156
+ const raw = localStorage.getItem(FLOATING_CALL_STATE_KEY);
157
+ if (!raw) return { active: false };
158
+ const parsed = JSON.parse(raw);
159
+ return {
160
+ active: parsed?.active === true,
161
+ call: String(parsed?.call || "").trim().toLowerCase() === "video"
162
+ ? "video"
163
+ : "voice",
164
+ updatedAt: Number(parsed?.updatedAt || 0) || Date.now(),
165
+ };
166
+ } catch {
167
+ return { active: false };
168
+ }
169
+ }
170
+
171
+ function writeFloatingCallState(nextState) {
172
+ if (typeof window === "undefined") return;
173
+ try {
174
+ localStorage.setItem(
175
+ FLOATING_CALL_STATE_KEY,
176
+ JSON.stringify({
177
+ active: nextState?.active === true,
178
+ call:
179
+ String(nextState?.call || "").trim().toLowerCase() === "video"
180
+ ? "video"
181
+ : "voice",
182
+ updatedAt: Date.now(),
183
+ }),
184
+ );
185
+ } catch {
186
+ // best effort
187
+ }
188
+ }
189
+
146
190
  /* ── Module imports ── */
147
191
  import { ICONS } from "./modules/icons.js";
148
192
  import { iconText, resolveIcon } from "./modules/icon-utils.js";
@@ -665,7 +709,7 @@ function SidebarNav({ collapsed = false, onToggle }) {
665
709
  <button
666
710
  key=${tab.id}
667
711
  class="sidebar-nav-item ${isActive ? "active" : ""} ${isChild ? "sidebar-nav-child" : ""}"
668
- style=${`position:relative${isChild ? ";padding-left:28px;font-size:0.85em" : ""}`}
712
+ style="position:relative"
669
713
  aria-label=${tab.label}
670
714
  aria-current=${isActive ? "page" : null}
671
715
  title=${collapsed ? tab.label : undefined}
@@ -962,7 +1006,7 @@ function InspectorPanel({ onResizeStart, onResizeReset, showResizer }) {
962
1006
  * Bottom Navigation
963
1007
  * ═══════════════════════════════════════════════ */
964
1008
  const PRIMARY_NAV_TABS = ["dashboard", "chat", "tasks", "agents"];
965
- const MORE_NAV_TABS = ["control", "infra", "logs", "library", "workflows", "settings"];
1009
+ const MORE_NAV_TABS = ["control", "infra", "logs", "telemetry", "library", "workflows", "settings"];
966
1010
 
967
1011
  function getTabsById(ids) {
968
1012
  return ids
@@ -1364,6 +1408,11 @@ function App() {
1364
1408
  const [voiceInitialVisionSource, setVoiceInitialVisionSource] = useState(
1365
1409
  null,
1366
1410
  );
1411
+ const followWindowMode = isFollowWindowFromUrl();
1412
+ const followOverlayOpenedRef = useRef(false);
1413
+ const [floatingCallState, setFloatingCallState] = useState(() =>
1414
+ readFloatingCallState(),
1415
+ );
1367
1416
  const resizeRef = useRef(null);
1368
1417
  const [isCompactNav, setIsCompactNav] = useState(() => {
1369
1418
  const win = globalThis.window;
@@ -1723,6 +1772,29 @@ function App() {
1723
1772
  return;
1724
1773
  }
1725
1774
 
1775
+ const desktopFollowApi = globalThis?.veDesktop?.follow;
1776
+ if (!followWindowMode && desktopFollowApi?.open) {
1777
+ try {
1778
+ await desktopFollowApi.open({
1779
+ call: requestedCallType,
1780
+ initialVisionSource: requestedVisionSource,
1781
+ sessionId: currentSessionId,
1782
+ executor: currentExecutor,
1783
+ mode: currentMode,
1784
+ model: currentModel,
1785
+ });
1786
+ const nextFloatingState = {
1787
+ active: true,
1788
+ call: requestedCallType,
1789
+ };
1790
+ setFloatingCallState(nextFloatingState);
1791
+ writeFloatingCallState(nextFloatingState);
1792
+ return;
1793
+ } catch {
1794
+ // Fall through to in-window overlay if desktop companion fails.
1795
+ }
1796
+ }
1797
+
1726
1798
  setVoiceSessionId(currentSessionId);
1727
1799
  setVoiceExecutor(currentExecutor);
1728
1800
  setVoiceAgentMode(currentMode);
@@ -1749,8 +1821,39 @@ function App() {
1749
1821
  globalThis.addEventListener?.("ve:open-voice-mode", handleOpenVoiceMode);
1750
1822
  return () =>
1751
1823
  globalThis.removeEventListener?.("ve:open-voice-mode", handleOpenVoiceMode);
1824
+ }, [followWindowMode]);
1825
+
1826
+ useEffect(() => {
1827
+ const onStorage = (event) => {
1828
+ if (event?.key && event.key !== FLOATING_CALL_STATE_KEY) return;
1829
+ setFloatingCallState(readFloatingCallState());
1830
+ };
1831
+ globalThis.addEventListener?.("storage", onStorage);
1832
+ return () => {
1833
+ globalThis.removeEventListener?.("storage", onStorage);
1834
+ };
1752
1835
  }, []);
1753
1836
 
1837
+ useEffect(() => {
1838
+ if (!followWindowMode) return;
1839
+ const nextFloatingState = {
1840
+ active: Boolean(voiceOverlayOpen),
1841
+ call: voiceCallType,
1842
+ };
1843
+ setFloatingCallState(nextFloatingState);
1844
+ writeFloatingCallState(nextFloatingState);
1845
+ }, [followWindowMode, voiceOverlayOpen, voiceCallType]);
1846
+
1847
+ useEffect(() => {
1848
+ if (!followWindowMode) return;
1849
+ if (voiceOverlayOpen) {
1850
+ followOverlayOpenedRef.current = true;
1851
+ return;
1852
+ }
1853
+ if (!followOverlayOpenedRef.current) return;
1854
+ globalThis?.veDesktop?.follow?.hide?.().catch?.(() => {});
1855
+ }, [followWindowMode, voiceOverlayOpen]);
1856
+
1754
1857
  useEffect(() => {
1755
1858
  const launch = parseVoiceLaunchFromUrl();
1756
1859
  if (!launch) return;
@@ -1875,6 +1978,14 @@ function App() {
1875
1978
  const railSessionType = "primary";
1876
1979
  const showDrawerToggles = isTablet;
1877
1980
  const showInspectorToggle = isTablet && isChatOrAgents;
1981
+ const showRestoreFloatingCall =
1982
+ !followWindowMode &&
1983
+ floatingCallState?.active === true &&
1984
+ typeof globalThis?.veDesktop?.follow?.restore === "function";
1985
+ const floatingCallLabel =
1986
+ String(floatingCallState?.call || "").trim().toLowerCase() === "video"
1987
+ ? "Restore floating video call"
1988
+ : "Restore floating voice call";
1878
1989
 
1879
1990
  const shellStyle = isDesktop
1880
1991
  ? {
@@ -2053,9 +2164,42 @@ function App() {
2053
2164
  open=${isBotOpen}
2054
2165
  onClose=${closeBot}
2055
2166
  />
2167
+ ${showRestoreFloatingCall
2168
+ ? html`
2169
+ <button
2170
+ class="btn btn-primary floating-call-restore"
2171
+ title=${floatingCallLabel}
2172
+ onClick=${async () => {
2173
+ try {
2174
+ const result = await globalThis.veDesktop.follow.restore();
2175
+ if (!result?.ok) {
2176
+ const nextFloatingState = { active: false, call: floatingCallState?.call };
2177
+ setFloatingCallState(nextFloatingState);
2178
+ writeFloatingCallState(nextFloatingState);
2179
+ showToast("No floating call window is active.", "info");
2180
+ }
2181
+ } catch {
2182
+ showToast("Could not restore floating call window.", "error");
2183
+ }
2184
+ }}
2185
+ >
2186
+ ${resolveIcon("phone")}
2187
+ ${String(floatingCallState?.call || "").trim().toLowerCase() === "video"
2188
+ ? " Restore Video Call"
2189
+ : " Restore Voice Call"}
2190
+ </button>
2191
+ `
2192
+ : null}
2056
2193
  <${VoiceOverlay}
2057
2194
  visible=${voiceOverlayOpen}
2058
2195
  onClose=${() => setVoiceOverlayOpen(false)}
2196
+ onDismiss=${() => {
2197
+ if (followWindowMode && globalThis?.veDesktop?.follow?.hide) {
2198
+ globalThis.veDesktop.follow.hide().catch(() => {});
2199
+ return;
2200
+ }
2201
+ setVoiceOverlayOpen(false);
2202
+ }}
2059
2203
  tier=${voiceTier}
2060
2204
  sessionId=${voiceSessionId}
2061
2205
  executor=${voiceExecutor}
@@ -2063,11 +2207,27 @@ function App() {
2063
2207
  model=${voiceModel}
2064
2208
  callType=${voiceCallType}
2065
2209
  initialVisionSource=${voiceInitialVisionSource}
2210
+ compact=${followWindowMode}
2066
2211
  />
2067
2212
  `;
2068
2213
  }
2069
2214
 
2070
2215
  /* ─── Mount ─── */
2071
- const mountApp = () => preactRender(html`<${App} />`, document.getElementById("app"));
2072
- globalThis.__veRemountApp = mountApp;
2216
+ const mountRoot = () => document.getElementById("app");
2217
+ const mountApp = () => {
2218
+ const root = mountRoot();
2219
+ if (!root) return;
2220
+ preactRender(html`<${App} />`, root);
2221
+ };
2222
+ const remountApp = () => {
2223
+ const root = mountRoot();
2224
+ if (!root) return;
2225
+ try {
2226
+ preactRender(null, root);
2227
+ } catch {
2228
+ root.replaceChildren();
2229
+ }
2230
+ preactRender(html`<${App} />`, root);
2231
+ };
2232
+ globalThis.__veRemountApp = remountApp;
2073
2233
  mountApp();
@@ -29,7 +29,7 @@ const html = htm.bind(h);
29
29
  * ═══════════════════════════════════════════════ */
30
30
 
31
31
  /** Current agent interaction mode */
32
- export const agentMode = signal("agent"); // "ask" | "agent" | "plan"
32
+ export const agentMode = signal("ask"); // "ask" | "agent" | "plan"
33
33
 
34
34
  /** Available agents loaded from API */
35
35
  export const availableAgents = signal([]); // Array<{ id, name, provider, available, busy, capabilities }>
@@ -697,6 +697,150 @@ const AGENT_SELECTOR_STYLES = `
697
697
  .toolbar-select:focus { outline: none; border-color: var(--tg-theme-button-color, #3b82f6); }
698
698
  .toolbar-select option { background: #1a1a2e; color: #fff; }
699
699
  .toolbar-select--wide { min-width: 110px; }
700
+
701
+ /* ── Stop Button ── */
702
+ .chat-stop-btn {
703
+ display: flex;
704
+ align-items: center;
705
+ justify-content: center;
706
+ width: 36px;
707
+ height: 36px;
708
+ border-radius: 50%;
709
+ border: 2px solid #ef4444;
710
+ background: rgba(239, 68, 68, 0.12);
711
+ color: #ef4444;
712
+ cursor: pointer;
713
+ font-size: 14px;
714
+ flex-shrink: 0;
715
+ transition: background 0.2s ease, transform 0.1s ease;
716
+ -webkit-tap-highlight-color: transparent;
717
+ padding: 0;
718
+ }
719
+ .chat-stop-btn:hover {
720
+ background: rgba(239, 68, 68, 0.22);
721
+ transform: scale(1.05);
722
+ }
723
+ .chat-stop-btn:active {
724
+ transform: scale(0.95);
725
+ }
726
+
727
+ /* ── Split Send Button Group ── */
728
+ .chat-send-group {
729
+ position: relative;
730
+ display: flex;
731
+ flex-shrink: 0;
732
+ }
733
+ .chat-send-main {
734
+ display: flex;
735
+ align-items: center;
736
+ justify-content: center;
737
+ height: 36px;
738
+ padding: 0 13px;
739
+ border: none;
740
+ border-radius: 8px 0 0 8px;
741
+ background: var(--tg-theme-button-color, #3b82f6);
742
+ color: var(--tg-theme-button-text-color, #fff);
743
+ cursor: pointer;
744
+ font-size: 15px;
745
+ transition: background 0.2s ease, opacity 0.2s ease;
746
+ -webkit-tap-highlight-color: transparent;
747
+ }
748
+ .chat-send-main:disabled {
749
+ opacity: 0.4;
750
+ cursor: not-allowed;
751
+ }
752
+ .chat-send-main:not(:disabled):hover {
753
+ background: #2563eb;
754
+ }
755
+ .chat-send-chevron {
756
+ display: flex;
757
+ align-items: center;
758
+ justify-content: center;
759
+ height: 36px;
760
+ width: 22px;
761
+ border: none;
762
+ border-left: 1px solid rgba(255,255,255,0.2);
763
+ border-radius: 0 8px 8px 0;
764
+ background: var(--tg-theme-button-color, #3b82f6);
765
+ color: rgba(255,255,255,0.85);
766
+ cursor: pointer;
767
+ font-size: 10px;
768
+ transition: background 0.2s ease, opacity 0.2s ease;
769
+ -webkit-tap-highlight-color: transparent;
770
+ padding: 0;
771
+ }
772
+ .chat-send-chevron:disabled {
773
+ opacity: 0.4;
774
+ cursor: not-allowed;
775
+ }
776
+ .chat-send-chevron:not(:disabled):hover {
777
+ background: #2563eb;
778
+ color: #fff;
779
+ }
780
+
781
+ /* ── Send Options Dropdown ── */
782
+ .chat-send-menu {
783
+ position: absolute;
784
+ bottom: calc(100% + 6px);
785
+ right: 0;
786
+ min-width: 230px;
787
+ background: var(--tg-theme-bg-color, #0f0f23);
788
+ border: 1px solid rgba(255,255,255,0.1);
789
+ border-radius: 12px;
790
+ box-shadow: 0 8px 32px rgba(0,0,0,0.5);
791
+ backdrop-filter: blur(16px);
792
+ padding: 4px;
793
+ z-index: 1010;
794
+ animation: agentDropIn 0.15s ease-out;
795
+ overflow: hidden;
796
+ }
797
+ .chat-send-menu-item {
798
+ display: flex;
799
+ align-items: center;
800
+ gap: 8px;
801
+ padding: 9px 12px;
802
+ width: 100%;
803
+ border: none;
804
+ border-radius: 8px;
805
+ background: transparent;
806
+ color: var(--tg-theme-text-color, #fff);
807
+ cursor: pointer;
808
+ font-size: 13px;
809
+ text-align: left;
810
+ transition: background 0.15s ease;
811
+ -webkit-tap-highlight-color: transparent;
812
+ line-height: 1.2;
813
+ }
814
+ .chat-send-menu-item:hover {
815
+ background: rgba(255,255,255,0.07);
816
+ }
817
+ .chat-send-menu-item.active {
818
+ background: rgba(59,130,246,0.15);
819
+ color: #93c5fd;
820
+ }
821
+ .chat-send-menu-item-icon {
822
+ font-size: 14px;
823
+ width: 18px;
824
+ text-align: center;
825
+ flex-shrink: 0;
826
+ }
827
+ .chat-send-menu-item-label {
828
+ flex: 1;
829
+ font-weight: 500;
830
+ }
831
+ .chat-send-menu-item-kbd {
832
+ display: inline-flex;
833
+ align-items: center;
834
+ padding: 1px 5px;
835
+ border-radius: 4px;
836
+ background: rgba(255,255,255,0.08);
837
+ border: 1px solid rgba(255,255,255,0.12);
838
+ font-size: 10px;
839
+ color: var(--tg-theme-hint-color, #999);
840
+ font-family: monospace;
841
+ white-space: nowrap;
842
+ flex-shrink: 0;
843
+ }
700
844
  `;
701
845
 
702
846
  let _agentStylesInjected = false;