bosun 0.41.8 → 0.41.10

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 (58) hide show
  1. package/.env.example +1 -1
  2. package/README.md +23 -1
  3. package/agent/agent-event-bus.mjs +31 -2
  4. package/agent/agent-pool.mjs +275 -40
  5. package/agent/agent-prompts.mjs +9 -1
  6. package/agent/agent-supervisor.mjs +22 -0
  7. package/agent/autofix.mjs +1 -1
  8. package/agent/primary-agent.mjs +115 -5
  9. package/cli.mjs +3 -2
  10. package/config/config.mjs +47 -33
  11. package/config/context-shredding-config.mjs +1 -1
  12. package/config/repo-root.mjs +41 -33
  13. package/desktop/main.mjs +350 -25
  14. package/desktop/preload.cjs +8 -0
  15. package/desktop/preload.mjs +19 -0
  16. package/entrypoint.mjs +332 -0
  17. package/git/sdk-conflict-resolver.mjs +1 -1
  18. package/infra/health-status.mjs +72 -0
  19. package/infra/library-manager.mjs +58 -1
  20. package/infra/maintenance.mjs +1 -2
  21. package/infra/monitor.mjs +26 -8
  22. package/infra/session-tracker.mjs +30 -3
  23. package/package.json +12 -4
  24. package/server/bosun-mcp-server.mjs +1004 -0
  25. package/server/setup-web-server.mjs +288 -259
  26. package/server/ui-server.mjs +1323 -26
  27. package/shell/claude-shell.mjs +14 -1
  28. package/shell/codex-config.mjs +1 -1
  29. package/shell/codex-model-profiles.mjs +170 -30
  30. package/shell/codex-shell.mjs +63 -18
  31. package/shell/opencode-providers.mjs +20 -8
  32. package/task/task-executor.mjs +28 -0
  33. package/task/task-store.mjs +13 -4
  34. package/telegram/telegram-sentinel.mjs +54 -3
  35. package/tools/list-todos.mjs +7 -1
  36. package/ui/app.js +3 -2
  37. package/ui/components/agent-selector.js +127 -0
  38. package/ui/components/session-list.js +15 -10
  39. package/ui/demo-defaults.js +334 -336
  40. package/ui/modules/router.js +2 -0
  41. package/ui/modules/state.js +13 -5
  42. package/ui/tabs/chat.js +3 -0
  43. package/ui/tabs/library.js +284 -52
  44. package/ui/tabs/tasks.js +5 -13
  45. package/ui/tabs/workflows.js +766 -3
  46. package/workflow/workflow-engine.mjs +246 -5
  47. package/workflow/workflow-nodes/definitions.mjs +37 -0
  48. package/workflow/workflow-nodes.mjs +1014 -184
  49. package/workflow/workflow-templates.mjs +0 -5
  50. package/workflow-templates/_helpers.mjs +253 -0
  51. package/workflow-templates/agents.mjs +199 -226
  52. package/workflow-templates/github.mjs +106 -16
  53. package/workflow-templates/sub-workflows.mjs +233 -0
  54. package/workflow-templates/task-execution.mjs +125 -471
  55. package/workflow-templates/task-lifecycle.mjs +11 -48
  56. package/workspace/command-diagnostics.mjs +460 -0
  57. package/workspace/context-cache.mjs +396 -28
  58. package/workspace/worktree-manager.mjs +1 -1
@@ -53,9 +53,11 @@ export const agentMode = signal("ask"); // "ask" | "agent" | "plan" | "web" | "i
53
53
 
54
54
  /** Available agents loaded from API */
55
55
  export const availableAgents = signal([]); // Array<{ id, name, provider, available, busy, capabilities }>
56
+ export const manualAgents = signal([]);
56
57
 
57
58
  /** Currently active agent adapter id */
58
59
  export const activeAgent = signal("codex-sdk");
60
+ export const activeManualAgentId = signal("");
59
61
 
60
62
  /** Whether agent data is currently loading */
61
63
  export const agentSelectorLoading = signal(false);
@@ -71,6 +73,7 @@ try { if (typeof localStorage !== "undefined") yoloMode.value = localStorage.get
71
73
  /** Selected model override — empty string means "default" */
72
74
  export const selectedModel = signal("");
73
75
  try { if (typeof localStorage !== "undefined") selectedModel.value = localStorage.getItem("ve-selected-model") || ""; } catch {}
76
+ try { if (typeof localStorage !== "undefined") activeManualAgentId.value = localStorage.getItem("ve-active-manual-agent") || ""; } catch {}
74
77
 
75
78
  /** Computed: resolved active agent object */
76
79
  export const activeAgentInfo = computed(() => {
@@ -91,6 +94,8 @@ const MODES = [
91
94
  { id: "instant", label: "Instant", icon: "zap", description: "Fast back-and-forth" },
92
95
  ];
93
96
 
97
+ const VALID_MANUAL_MODES = new Set(MODES.map((mode) => mode.id));
98
+
94
99
  const AGENT_ICONS = {
95
100
  "codex-sdk": "zap",
96
101
  "copilot-sdk": "bot",
@@ -394,8 +399,10 @@ export async function loadAvailableAgents() {
394
399
  try {
395
400
  const res = await apiFetch("/api/agents/available", { _silent: true });
396
401
  const agents = Array.isArray(res) ? res : (res?.agents || res?.data || []);
402
+ const nextManualAgents = Array.isArray(res?.manualAgents) ? res.manualAgents : [];
397
403
  const reportedActive = String(res?.active || "").trim();
398
404
  availableAgents.value = agents;
405
+ manualAgents.value = nextManualAgents;
399
406
  // Prefer backend-reported active selection when present.
400
407
  if (reportedActive && agents.some((a) => a.id === reportedActive)) {
401
408
  activeAgent.value = reportedActive;
@@ -404,6 +411,10 @@ export async function loadAvailableAgents() {
404
411
  const firstEnabled = agents.find((a) => a.available);
405
412
  activeAgent.value = (firstEnabled || agents[0]).id;
406
413
  }
414
+ if (activeManualAgentId.value && !nextManualAgents.some((agent) => agent.id === activeManualAgentId.value)) {
415
+ activeManualAgentId.value = "";
416
+ try { localStorage.setItem("ve-active-manual-agent", ""); } catch {}
417
+ }
407
418
  } catch (err) {
408
419
  console.warn("[agent-selector] Failed to load agents:", err);
409
420
  // Provide sensible fallback agents for offline/dev mode
@@ -419,6 +430,12 @@ export async function loadAvailableAgents() {
419
430
  }
420
431
  }
421
432
 
433
+ function setActiveManualAgent(agentId) {
434
+ const nextId = String(agentId || "").trim();
435
+ activeManualAgentId.value = nextId;
436
+ try { localStorage.setItem("ve-active-manual-agent", nextId); } catch {}
437
+ }
438
+
422
439
  /**
423
440
  * Switch the active agent via API.
424
441
  * @param {string} agentId
@@ -861,6 +878,115 @@ export function AgentPicker() {
861
878
  `;
862
879
  }
863
880
 
881
+ export function ManualAgentPicker() {
882
+ const profiles = manualAgents.value;
883
+ const current = activeManualAgentId.value;
884
+ const [anchorEl, setAnchorEl] = useState(null);
885
+ const open = Boolean(anchorEl);
886
+
887
+ const grouped = profiles.reduce((acc, profile) => {
888
+ const key = String(profile?.sectionLabel || "Manual").trim() || "Manual";
889
+ if (!acc[key]) acc[key] = [];
890
+ acc[key].push(profile);
891
+ return acc;
892
+ }, {});
893
+ const currentProfile = profiles.find((profile) => profile.id === current) || null;
894
+ const currentLabel = currentProfile?.name || "Manual Agent";
895
+
896
+ const handleSelect = useCallback((profileId) => {
897
+ const nextId = String(profileId || "").trim();
898
+ const selected = profiles.find((profile) => profile.id === nextId) || null;
899
+ setActiveManualAgent(nextId);
900
+ if (selected?.interactiveMode && VALID_MANUAL_MODES.has(selected.interactiveMode)) {
901
+ setAgentMode(selected.interactiveMode);
902
+ }
903
+ if (selected?.model) {
904
+ selectedModel.value = selected.model;
905
+ try { localStorage.setItem("ve-selected-model", selected.model); } catch {}
906
+ }
907
+ haptic(selected ? "light" : "medium");
908
+ setAnchorEl(null);
909
+ }, [profiles]);
910
+
911
+ return html`
912
+ <${Tooltip} title="Select a library-backed manual chat agent" arrow>
913
+ <${Chip}
914
+ label=${currentLabel}
915
+ size="small"
916
+ variant=${currentProfile ? "filled" : "outlined"}
917
+ onClick=${(e) => setAnchorEl(e.currentTarget)}
918
+ icon=${html`<span style="font-size:13px;line-height:1">${resolveIcon("bot")}</span>`}
919
+ sx=${{
920
+ flexShrink: 0,
921
+ cursor: "pointer",
922
+ fontSize: 12,
923
+ fontWeight: 500,
924
+ color: "var(--tg-theme-text-color, #fff)",
925
+ borderColor: open ? "var(--tg-theme-button-color, #3b82f6)" : "rgba(255,255,255,0.08)",
926
+ bgcolor: currentProfile ? "rgba(59,130,246,0.12)" : "transparent",
927
+ "&:hover": { bgcolor: "rgba(255,255,255,0.06)" },
928
+ }}
929
+ />
930
+ </${Tooltip}>
931
+ <${Menu}
932
+ anchorEl=${anchorEl}
933
+ open=${open}
934
+ onClose=${() => setAnchorEl(null)}
935
+ anchorOrigin=${{ vertical: "top", horizontal: "left" }}
936
+ transformOrigin=${{ vertical: "bottom", horizontal: "left" }}
937
+ slotProps=${{ paper: { sx: { ...muiDarkPaper, minWidth: 260 } } }}
938
+ >
939
+ <${MenuItem} selected=${!current} onClick=${() => handleSelect("")}>
940
+ <${ListItemIcon} sx=${{ minWidth: "28px !important" }}>
941
+ ${!current ? html`<${Typography} sx=${{ color: "var(--tg-theme-button-color, #3b82f6)", fontWeight: 700, fontSize: 14 }}>✓</${Typography}>` : null}
942
+ </${ListItemIcon}>
943
+ <${ListItemText}
944
+ primary="No manual profile"
945
+ secondary="Use only the executor + mode selection"
946
+ primaryTypographyProps=${{ fontSize: 13, fontWeight: 500 }}
947
+ secondaryTypographyProps=${{ fontSize: 11 }}
948
+ />
949
+ </${MenuItem}>
950
+ ${profiles.length > 0 ? html`<${Divider} />` : null}
951
+ ${Object.entries(grouped).map(([sectionLabel, items], sectionIndex) => html`
952
+ <div key=${sectionLabel}>
953
+ ${sectionIndex > 0 ? html`<${Divider} />` : null}
954
+ <${MenuItem} disabled sx=${{ opacity: 0.7, fontSize: 11, textTransform: "uppercase", letterSpacing: 0.5 }}>
955
+ ${sectionLabel}
956
+ </${MenuItem}>
957
+ ${items.map((profile) => html`
958
+ <${MenuItem}
959
+ key=${profile.id}
960
+ selected=${profile.id === current}
961
+ onClick=${() => handleSelect(profile.id)}
962
+ >
963
+ <${ListItemIcon} sx=${{ minWidth: "28px !important" }}>
964
+ ${profile.id === current ? html`<${Typography} sx=${{ color: "var(--tg-theme-button-color, #3b82f6)", fontWeight: 700, fontSize: 14 }}>✓</${Typography}>` : null}
965
+ </${ListItemIcon}>
966
+ <${ListItemText}
967
+ primary=${profile.name}
968
+ secondary=${profile.description || profile.interactiveMode || profile.agentCategory || "Manual agent"}
969
+ primaryTypographyProps=${{ fontSize: 13, fontWeight: 500 }}
970
+ secondaryTypographyProps=${{ fontSize: 11 }}
971
+ />
972
+ </${MenuItem}>
973
+ `)}
974
+ </div>
975
+ `)}
976
+ ${profiles.length === 0 ? html`
977
+ <${MenuItem} disabled>
978
+ <${ListItemText}
979
+ primary="No manual agents"
980
+ secondary="Mark an interactive agent as visible in chat from the Library tab."
981
+ primaryTypographyProps=${{ fontSize: 13, fontWeight: 500 }}
982
+ secondaryTypographyProps=${{ fontSize: 11 }}
983
+ />
984
+ </${MenuItem}>
985
+ ` : null}
986
+ </${Menu}>
987
+ `;
988
+ }
989
+
864
990
  /* ═══════════════════════════════════════════════
865
991
  * AgentStatusBadge
866
992
  * MUI Chip showing agent runtime state
@@ -1016,6 +1142,7 @@ export function ChatInputToolbar() {
1016
1142
  <div class="chat-input-toolbar">
1017
1143
  <${AgentPicker} />
1018
1144
  <${AgentModeSelector} />
1145
+ <${ManualAgentPicker} />
1019
1146
  <${ModelPicker} />
1020
1147
  <${YoloToggle} />
1021
1148
  <${Box} sx=${{ flex: 1, minWidth: 8 }} />
@@ -493,19 +493,22 @@ export function initSessionWsListener() {
493
493
  */
494
494
  export async function createSession(options = {}) {
495
495
  const type = options?.type || "manual";
496
+ const allowReuseFresh = options?.reuseFresh !== false;
496
497
 
497
498
  // Duplicate prevention: if a fresh empty session of same type exists, reuse it
498
499
  const existing = sessionsData.value || [];
499
- const fresh = existing.find(
500
- (s) =>
501
- s.type === type &&
502
- s.status === "active" &&
503
- (s.turnCount || 0) === 0 &&
504
- (!s.preview || s.preview.trim() === ""),
505
- );
506
- if (fresh) {
507
- selectedSessionId.value = fresh.id;
508
- return { ok: true, session: fresh };
500
+ if (allowReuseFresh) {
501
+ const fresh = existing.find(
502
+ (s) =>
503
+ s.type === type &&
504
+ s.status === "active" &&
505
+ (s.turnCount || 0) === 0 &&
506
+ (!s.preview || s.preview.trim() === ""),
507
+ );
508
+ if (fresh) {
509
+ selectedSessionId.value = fresh.id;
510
+ return { ok: true, session: fresh };
511
+ }
509
512
  }
510
513
 
511
514
  try {
@@ -568,6 +571,8 @@ export async function resumeSession(id) {
568
571
  const STATUS_COLOR_MAP = {
569
572
  running: "var(--accent)",
570
573
  active: "var(--accent)",
574
+ idle: "var(--color-warning)",
575
+ stalled: "var(--color-error)",
571
576
  paused: "var(--text-hint)",
572
577
  completed: "var(--color-done)",
573
578
  done: "var(--color-done)",