bosun 0.41.7 → 0.41.9

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 (43) hide show
  1. package/README.md +23 -1
  2. package/agent/agent-event-bus.mjs +31 -2
  3. package/agent/agent-pool.mjs +251 -11
  4. package/agent/agent-prompts.mjs +5 -1
  5. package/agent/agent-supervisor.mjs +22 -0
  6. package/agent/primary-agent.mjs +115 -5
  7. package/cli.mjs +3 -2
  8. package/config/config.mjs +4 -1
  9. package/desktop/main.mjs +350 -25
  10. package/desktop/preload.cjs +8 -0
  11. package/desktop/preload.mjs +19 -0
  12. package/entrypoint.mjs +332 -0
  13. package/infra/health-status.mjs +72 -0
  14. package/infra/library-manager.mjs +58 -1
  15. package/infra/maintenance.mjs +1 -2
  16. package/infra/monitor.mjs +25 -7
  17. package/infra/session-tracker.mjs +30 -3
  18. package/package.json +10 -4
  19. package/server/bosun-mcp-server.mjs +1004 -0
  20. package/server/setup-web-server.mjs +287 -258
  21. package/server/ui-server.mjs +218 -23
  22. package/shell/claude-shell.mjs +14 -1
  23. package/shell/codex-model-profiles.mjs +166 -29
  24. package/shell/codex-shell.mjs +56 -18
  25. package/shell/opencode-providers.mjs +20 -8
  26. package/task/task-executor.mjs +28 -0
  27. package/task/task-store.mjs +13 -4
  28. package/tools/list-todos.mjs +7 -1
  29. package/ui/app.js +3 -2
  30. package/ui/components/agent-selector.js +127 -0
  31. package/ui/components/session-list.js +2 -0
  32. package/ui/demo-defaults.js +6 -6
  33. package/ui/modules/router.js +2 -0
  34. package/ui/modules/state.js +13 -5
  35. package/ui/tabs/chat.js +3 -0
  36. package/ui/tabs/library.js +284 -52
  37. package/ui/tabs/tasks.js +5 -13
  38. package/workflow/workflow-engine.mjs +16 -4
  39. package/workflow/workflow-nodes/definitions.mjs +37 -0
  40. package/workflow/workflow-nodes.mjs +489 -153
  41. package/workflow/workflow-templates.mjs +0 -5
  42. package/workflow-templates/github.mjs +106 -16
  43. package/workspace/worktree-manager.mjs +1 -1
package/ui/tabs/chat.js CHANGED
@@ -59,6 +59,7 @@ import {
59
59
  loadAvailableAgents,
60
60
  agentMode,
61
61
  activeAgent,
62
+ activeManualAgentId,
62
63
  activeAgentInfo,
63
64
  availableAgents,
64
65
  yoloMode,
@@ -727,6 +728,7 @@ export function ChatTab() {
727
728
  body: JSON.stringify({
728
729
  content: msg,
729
730
  mode: outboundMode,
731
+ agentProfileId: activeManualAgentId.value || undefined,
730
732
  yolo: yoloMode.peek(),
731
733
  model: selectedModel.value || undefined,
732
734
  attachments,
@@ -750,6 +752,7 @@ export function ChatTab() {
750
752
  ...(outboundContent ? { prompt: outboundContent } : {}),
751
753
  agent: activeAgent.value,
752
754
  mode: outboundMode,
755
+ agentProfileId: activeManualAgentId.value || undefined,
753
756
  yolo: yoloMode.peek(),
754
757
  model: selectedModel.value || undefined,
755
758
  });
@@ -357,8 +357,14 @@ async function testProfileMatch(criteria = {}) {
357
357
  return res?.data || { best: null, candidates: [], plan: null, auto: { shouldAutoApply: false } };
358
358
  }
359
359
 
360
- async function fetchLibrarySources() {
361
- const res = await apiFetch("/api/library/sources?probe=1");
360
+ async function fetchLibrarySources(options = {}) {
361
+ const params = new URLSearchParams();
362
+ if (options?.probe) params.set("probe", "1");
363
+ if (options?.refresh) params.set("refresh", "1");
364
+ if (options?.sourceId) params.set("sourceId", String(options.sourceId));
365
+ const qs = params.toString();
366
+ const path = qs ? "/api/library/sources?" + qs : "/api/library/sources";
367
+ const res = await apiFetch(path);
362
368
  return res?.data || [];
363
369
  }
364
370
 
@@ -451,15 +457,37 @@ const TYPE_LABELS = { prompt: "Prompt", agent: "Agent Profile", skill: "Skill",
451
457
  const TYPE_COLORS = { prompt: "#58a6ff", agent: "#af7bff", skill: "#3fb950", mcp: "#f59e0b" };
452
458
  const STORAGE_SCOPE_LABELS = { repo: "Repo", workspace: "Workspace", global: "Global" };
453
459
  const STORAGE_SCOPE_COLORS = { repo: "info", workspace: "warning", global: "default" };
454
- const AGENT_TYPE_OPTIONS = Object.freeze([
455
- { value: "voice", label: "Voice" },
456
- { value: "task", label: "Task" },
457
- { value: "chat", label: "Chat" },
460
+ const AGENT_CATEGORY_OPTIONS = Object.freeze([
461
+ { value: "task", label: "Task Template" },
462
+ { value: "interactive", label: "Manual Chat Agent" },
463
+ { value: "voice", label: "Voice Agent" },
464
+ ]);
465
+ const INTERACTIVE_MODE_OPTIONS = Object.freeze([
466
+ { value: "ask", label: "Ask" },
467
+ { value: "agent", label: "Agent" },
468
+ { value: "plan", label: "Plan" },
469
+ { value: "web", label: "Web" },
470
+ { value: "instant", label: "Instant" },
471
+ { value: "custom", label: "Custom" },
458
472
  ]);
459
473
 
460
- function normalizeAgentType(rawType) {
461
- const value = String(rawType || "").trim().toLowerCase();
462
- if (value === "voice" || value === "task" || value === "chat") return value;
474
+ function normalizeAgentCategory(rawCategory) {
475
+ const value = String(rawCategory || "").trim().toLowerCase();
476
+ if (value === "voice" || value === "task" || value === "interactive") return value;
477
+ return "task";
478
+ }
479
+
480
+ function normalizeInteractiveMode(rawMode, agentCategory = "task") {
481
+ const value = String(rawMode || "").trim().toLowerCase();
482
+ if (["ask", "agent", "plan", "web", "instant", "custom"].includes(value)) return value;
483
+ if (agentCategory === "interactive") return "agent";
484
+ if (agentCategory === "voice") return "voice";
485
+ return "";
486
+ }
487
+
488
+ function deriveAgentTypeFromCategory(agentCategory) {
489
+ if (agentCategory === "voice") return "voice";
490
+ if (agentCategory === "interactive") return "chat";
463
491
  return "task";
464
492
  }
465
493
 
@@ -476,9 +504,12 @@ function normalizeStorageScope(rawScope, fallback = "repo") {
476
504
  return fallback;
477
505
  }
478
506
 
479
- function inferAgentTypeFromEntry(entry, parsedContent) {
480
- const explicit = normalizeAgentType(parsedContent?.agentType);
481
- if (parsedContent?.agentType) return explicit;
507
+ function inferAgentCategoryFromEntry(entry, parsedContent) {
508
+ const explicitCategory = normalizeAgentCategory(parsedContent?.agentCategory || entry?.agentCategory);
509
+ if (parsedContent?.agentCategory || entry?.agentCategory) return explicitCategory;
510
+ const explicitType = String(parsedContent?.agentType || entry?.agentType || "").trim().toLowerCase();
511
+ if (explicitType === "chat") return "interactive";
512
+ if (explicitType === "voice") return "voice";
482
513
  if (parsedContent?.voiceAgent === true) return "voice";
483
514
  const id = String(entry?.id || "").trim().toLowerCase();
484
515
  const tags = Array.isArray(entry?.tags)
@@ -489,6 +520,21 @@ function inferAgentTypeFromEntry(entry, parsedContent) {
489
520
  return "task";
490
521
  }
491
522
 
523
+ function inferInteractiveModeFromEntry(entry, parsedContent) {
524
+ const category = inferAgentCategoryFromEntry(entry, parsedContent);
525
+ return normalizeInteractiveMode(parsedContent?.interactiveMode || entry?.interactiveMode, category);
526
+ }
527
+
528
+ function inferInteractiveLabelFromEntry(entry, parsedContent) {
529
+ return String(parsedContent?.interactiveLabel || entry?.interactiveLabel || "").trim();
530
+ }
531
+
532
+ function inferShowInChatDropdown(entry, parsedContent) {
533
+ const explicit = parsedContent?.showInChatDropdown;
534
+ if (typeof explicit === "boolean") return explicit;
535
+ return entry?.showInChatDropdown === true;
536
+ }
537
+
492
538
  const AUDIO_AGENT_TEMPLATES = Object.freeze({
493
539
  female: {
494
540
  type: "agent",
@@ -506,6 +552,8 @@ const AUDIO_AGENT_TEMPLATES = Object.freeze({
506
552
  promptOverride: null,
507
553
  skills: ["concise-voice-guidance", "conversation-memory"],
508
554
  agentType: "voice",
555
+ agentCategory: "voice",
556
+ interactiveMode: "voice",
509
557
  voiceAgent: true,
510
558
  voicePersona: "female",
511
559
  voiceInstructions: "You are Nova, a female voice agent. Be concise, warm, and practical. Use tools for facts and execution. Keep spoken responses short and clear.",
@@ -527,6 +575,8 @@ const AUDIO_AGENT_TEMPLATES = Object.freeze({
527
575
  promptOverride: null,
528
576
  skills: ["ops-diagnostics", "task-execution"],
529
577
  agentType: "voice",
578
+ agentCategory: "voice",
579
+ interactiveMode: "voice",
530
580
  voiceAgent: true,
531
581
  voicePersona: "male",
532
582
  voiceInstructions: "You are Atlas, a male voice agent. Be direct and execution-oriented. Prefer actionable status updates. Use tools proactively for diagnostics.",
@@ -618,7 +668,10 @@ function LibraryCard({ entry, onSelect }) {
618
668
  sx=${{ fontSize: "0.74em" }}
619
669
  />
620
670
  ${entry.type === "agent" && entry.agentType && html`
621
- <${Chip} label=${String(entry.agentType).toUpperCase()} size="small" variant="outlined" sx=${{ fontSize: "0.75em" }} />
671
+ <${Chip} label=${String(entry.agentCategory || entry.agentType).replace(/(^.|\s+.)/g, (m) => m.toUpperCase())} size="small" variant="outlined" sx=${{ fontSize: "0.75em" }} />
672
+ `}
673
+ ${entry.type === "agent" && entry.agentCategory === "interactive" && (entry.interactiveLabel || entry.interactiveMode) && html`
674
+ <${Chip} label=${entry.interactiveLabel || String(entry.interactiveMode || "").toUpperCase()} size="small" variant="outlined" sx=${{ fontSize: "0.75em" }} />
622
675
  `}
623
676
  ${(entry.tags || []).slice(0, 5).map((tag) => html`
624
677
  <${Chip} key=${tag} label=${tag} size="small" sx=${{ fontSize: "0.75em", bgcolor: "primary.main", color: "#fff", opacity: 0.8 }} />
@@ -644,7 +697,10 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
644
697
  tags: normalizeTags(entry?.tags).join(", "),
645
698
  scope: entry?.scope || "global",
646
699
  storageScope: normalizeStorageScope(entry?.storageScope, "repo"),
647
- agentType: inferAgentTypeFromEntry(entry, null),
700
+ agentCategory: inferAgentCategoryFromEntry(entry, null),
701
+ interactiveMode: inferInteractiveModeFromEntry(entry, null),
702
+ interactiveLabel: inferInteractiveLabelFromEntry(entry, null),
703
+ showInChatDropdown: inferShowInChatDropdown(entry, null),
648
704
  content: typeof entry?.content === "string" ? entry.content : "",
649
705
  };
650
706
  const [form, setForm] = useState(initialFormSnapshot);
@@ -670,7 +726,10 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
670
726
  tags: normalizeTags(entry?.tags).join(", "),
671
727
  scope: entry?.scope || "global",
672
728
  storageScope: normalizeStorageScope(entry?.storageScope, "repo"),
673
- agentType: inferAgentTypeFromEntry(entry, null),
729
+ agentCategory: inferAgentCategoryFromEntry(entry, null),
730
+ interactiveMode: inferInteractiveModeFromEntry(entry, null),
731
+ interactiveLabel: inferInteractiveLabelFromEntry(entry, null),
732
+ showInChatDropdown: inferShowInChatDropdown(entry, null),
674
733
  content: "",
675
734
  };
676
735
  setForm(next);
@@ -694,7 +753,10 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
694
753
  ...f,
695
754
  content: contentStr,
696
755
  storageScope: normalizeStorageScope(detail?.storageScope || f.storageScope, "repo"),
697
- agentType: inferAgentTypeFromEntry(detail || entry, parsed),
756
+ agentCategory: inferAgentCategoryFromEntry(detail || entry, parsed),
757
+ interactiveMode: inferInteractiveModeFromEntry(detail || entry, parsed),
758
+ interactiveLabel: inferInteractiveLabelFromEntry(detail || entry, parsed),
759
+ showInChatDropdown: inferShowInChatDropdown(detail || entry, parsed),
698
760
  };
699
761
  setBaseline(next);
700
762
  return next;
@@ -738,13 +800,28 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
738
800
  showToast("Agent profile content must be valid JSON", "error");
739
801
  return false;
740
802
  }
741
- const agentType = normalizeAgentType(form.agentType);
803
+ const agentCategory = normalizeAgentCategory(form.agentCategory);
804
+ const agentType = deriveAgentTypeFromCategory(agentCategory);
742
805
  content.agentType = agentType;
743
- if (agentType === "voice") {
806
+ content.agentCategory = agentCategory;
807
+ if (agentCategory === "voice") {
744
808
  content.voiceAgent = true;
809
+ content.interactiveMode = "voice";
810
+ delete content.showInChatDropdown;
745
811
  } else if (content.voiceAgent === true) {
746
812
  content.voiceAgent = false;
747
813
  }
814
+ if (agentCategory === "interactive") {
815
+ const interactiveMode = normalizeInteractiveMode(form.interactiveMode, agentCategory);
816
+ content.interactiveMode = interactiveMode || "agent";
817
+ if (String(form.interactiveLabel || "").trim()) content.interactiveLabel = String(form.interactiveLabel || "").trim();
818
+ else delete content.interactiveLabel;
819
+ content.showInChatDropdown = form.showInChatDropdown === true;
820
+ } else {
821
+ delete content.interactiveLabel;
822
+ if (agentCategory !== "voice") delete content.interactiveMode;
823
+ delete content.showInChatDropdown;
824
+ }
748
825
  }
749
826
  const res = await saveEntry({
750
827
  id: form.id || undefined,
@@ -810,6 +887,7 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
810
887
  promptOverride: null,
811
888
  skills: [],
812
889
  agentType: "task",
890
+ agentCategory: "task",
813
891
  tags: [],
814
892
  }, null, 2)
815
893
  : "# Skill Title\n\n## Purpose\nDescribe what this skill teaches agents.\n\n## Instructions\n...";
@@ -858,11 +936,24 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
858
936
  <//>
859
937
  ${form.type === "agent" && html`
860
938
  <${FormControl} fullWidth size="small">
861
- <${InputLabel}>Agent Type<//>
862
- <${Select} value=${normalizeAgentType(form.agentType)} onChange=${updateField("agentType")} label="Agent Type">
863
- ${AGENT_TYPE_OPTIONS.map((opt) => html`<${MenuItem} key=${opt.value} value=${opt.value}>${opt.label}<//>`)}
939
+ <${InputLabel}>Agent Category<//>
940
+ <${Select} value=${normalizeAgentCategory(form.agentCategory)} onChange=${updateField("agentCategory")} label="Agent Category">
941
+ ${AGENT_CATEGORY_OPTIONS.map((opt) => html`<${MenuItem} key=${opt.value} value=${opt.value}>${opt.label}<//>`)}
942
+ <//>
943
+ <//>
944
+ `}
945
+ ${form.type === "agent" && normalizeAgentCategory(form.agentCategory) === "interactive" && html`
946
+ <${FormControl} fullWidth size="small">
947
+ <${InputLabel}>Manual Agent Type<//>
948
+ <${Select} value=${normalizeInteractiveMode(form.interactiveMode, form.agentCategory)} onChange=${updateField("interactiveMode")} label="Manual Agent Type">
949
+ ${INTERACTIVE_MODE_OPTIONS.map((opt) => html`<${MenuItem} key=${opt.value} value=${opt.value}>${opt.label}<//>`)}
864
950
  <//>
865
951
  <//>
952
+ <${TextField} size="small" fullWidth label="Type Label / Section" value=${form.interactiveLabel} onInput=${updateField("interactiveLabel")} placeholder="Optional custom group label, e.g. Research or Reviewer" />
953
+ <${FormControlLabel}
954
+ control=${html`<${Switch} checked=${form.showInChatDropdown === true} onChange=${(e) => setForm((f) => ({ ...f, showInChatDropdown: e.target.checked }))} />`}
955
+ label="Show in chat dropdown"
956
+ />
866
957
  `}
867
958
  <${Box}>
868
959
  <${Typography} variant="caption" color="text.secondary" sx=${{ mb: 0.5, display: "block" }}>Content<//>
@@ -873,7 +964,7 @@ function EntryEditor({ entry, onClose, onSaved, onDeleted }) {
873
964
  <//>
874
965
  <${Typography} variant="caption" color="text.secondary" sx=${{ mt: -1 }}>
875
966
  ${form.type === "prompt" ? "Use {{VARIABLE_NAME}} for template variables. Reference in workflows as {{prompt:name}}."
876
- : form.type === "agent" ? "JSON format. Referenced in workflows as {{agent:name}}."
967
+ : form.type === "agent" ? "JSON format. Task templates stay in workflow resolution; interactive profiles can also appear in the chat dropdown."
877
968
  : form.type === "mcp" ? "MCP server configuration. Managed via the MCP Servers panel."
878
969
  : "Markdown format. Referenced in workflows as {{skill:name}}."}
879
970
  <//>
@@ -2432,6 +2523,20 @@ const MARKETPLACE_CATEGORIES = [
2432
2523
  { id: "mcp", label: "MCP" },
2433
2524
  ];
2434
2525
 
2526
+ const MARKETPLACE_PROBE_TTL_MS = 5 * 60 * 1000;
2527
+ let marketplaceSourcesCache = [];
2528
+ let marketplaceSourcesProbedAt = 0;
2529
+
2530
+ function normalizeMarketplaceSources(value) {
2531
+ return Array.isArray(value) ? value : [];
2532
+ }
2533
+
2534
+ function setMarketplaceSourcesCache(value, { probed = false } = {}) {
2535
+ marketplaceSourcesCache = normalizeMarketplaceSources(value);
2536
+ if (probed) marketplaceSourcesProbedAt = Date.now();
2537
+ return marketplaceSourcesCache;
2538
+ }
2539
+
2435
2540
  function getTrustTier(source) {
2436
2541
  const tier = String(source?.trustTier || "").toLowerCase();
2437
2542
  if (tier === "official" || tier === "partner") return { label: "Official", color: "#22c55e", icon: "🏢" };
@@ -2439,9 +2544,33 @@ function getTrustTier(source) {
2439
2544
  return { label: "Unknown", color: "#6b7280", icon: "❔" };
2440
2545
  }
2441
2546
 
2547
+ export function buildMarketplaceImportPayload(sourceId, previewData, selectedPaths) {
2548
+ const source = previewData?.source && typeof previewData.source === "object"
2549
+ ? previewData.source
2550
+ : {};
2551
+ const payload = {
2552
+ importAgents: true,
2553
+ importSkills: true,
2554
+ importPrompts: true,
2555
+ importTools: true,
2556
+ includeEntries: selectedPaths,
2557
+ };
2558
+
2559
+ const normalizedSourceId = String(sourceId || source.id || "").trim();
2560
+ const repoUrl = String(source.repoUrl || previewData?.repoUrl || "").trim();
2561
+ const branch = String(source.defaultBranch || source.branch || previewData?.branch || "").trim();
2562
+
2563
+ if (normalizedSourceId) payload.sourceId = normalizedSourceId;
2564
+ if (repoUrl) payload.repoUrl = repoUrl;
2565
+ if (branch) payload.branch = branch;
2566
+
2567
+ return payload;
2568
+ }
2569
+
2442
2570
  function LibraryMarketplace({ onImported }) {
2443
- const [sources, setSources] = useState([]);
2444
- const [loading, setLoading] = useState(true);
2571
+ const [sources, setSources] = useState(() => normalizeMarketplaceSources(marketplaceSourcesCache));
2572
+ const [loading, setLoading] = useState(() => marketplaceSourcesCache.length === 0);
2573
+ const [refreshing, setRefreshing] = useState(false);
2445
2574
  const [searchText, setSearchText] = useState("");
2446
2575
  const [activeCategory, setActiveCategory] = useState("all");
2447
2576
  const [expandedSource, setExpandedSource] = useState(null);
@@ -2454,18 +2583,65 @@ function LibraryMarketplace({ onImported }) {
2454
2583
  const [customBranch, setCustomBranch] = useState("main");
2455
2584
  const [showCustom, setShowCustom] = useState(false);
2456
2585
 
2586
+ const applySources = useCallback((data, options = {}) => {
2587
+ const next = setMarketplaceSourcesCache(data, options);
2588
+ setSources(next);
2589
+ }, []);
2590
+
2591
+ const refreshSources = useCallback(async ({ probe = true, refresh = false, background = false } = {}) => {
2592
+ if (background) setRefreshing(true);
2593
+ else setLoading(true);
2594
+ try {
2595
+ const data = await fetchLibrarySources({ probe, refresh });
2596
+ applySources(data, { probed: probe });
2597
+ } catch (err) {
2598
+ if (!background) {
2599
+ showToast(`Failed to load marketplace sources: ${parseApiError(err)}`, "error");
2600
+ }
2601
+ } finally {
2602
+ if (background) setRefreshing(false);
2603
+ else setLoading(false);
2604
+ }
2605
+ }, [applySources]);
2606
+
2457
2607
  useEffect(() => {
2458
2608
  let alive = true;
2459
- setLoading(true);
2460
- fetchLibrarySources()
2461
- .then((data) => {
2609
+ const load = async () => {
2610
+ if (!marketplaceSourcesCache.length) {
2611
+ try {
2612
+ const data = await fetchLibrarySources();
2613
+ if (!alive) return;
2614
+ applySources(data);
2615
+ } catch (err) {
2616
+ if (alive) {
2617
+ showToast(`Failed to load marketplace sources: ${parseApiError(err)}`, "error");
2618
+ }
2619
+ } finally {
2620
+ if (alive) setLoading(false);
2621
+ }
2622
+ } else if (alive) {
2623
+ setLoading(false);
2624
+ }
2625
+
2626
+ if (!alive) return;
2627
+ const probeIsFresh = marketplaceSourcesProbedAt > 0 && (Date.now() - marketplaceSourcesProbedAt) < MARKETPLACE_PROBE_TTL_MS;
2628
+ if (probeIsFresh) return;
2629
+
2630
+ setRefreshing(true);
2631
+ try {
2632
+ const data = await fetchLibrarySources({ probe: true });
2462
2633
  if (!alive) return;
2463
- setSources(Array.isArray(data) ? data : []);
2464
- })
2465
- .catch(() => {})
2466
- .finally(() => setLoading(false));
2634
+ applySources(data, { probed: true });
2635
+ } catch {
2636
+ // best effort background refresh
2637
+ } finally {
2638
+ if (alive) setRefreshing(false);
2639
+ }
2640
+ };
2641
+
2642
+ load();
2467
2643
  return () => { alive = false; };
2468
- }, []);
2644
+ }, [applySources]);
2469
2645
 
2470
2646
  const filteredSources = useMemo(() => {
2471
2647
  let list = [...sources];
@@ -2507,14 +2683,7 @@ function LibraryMarketplace({ onImported }) {
2507
2683
  const doImportSource = useCallback(async (sourceId, selectedPaths) => {
2508
2684
  setImporting(sourceId);
2509
2685
  try {
2510
- const payload = {
2511
- sourceId,
2512
- importAgents: true,
2513
- importSkills: true,
2514
- importPrompts: true,
2515
- importTools: true,
2516
- includeEntries: selectedPaths,
2517
- };
2686
+ const payload = buildMarketplaceImportPayload(sourceId, previewData, selectedPaths);
2518
2687
  const res = await importLibrarySource(payload);
2519
2688
  if (!res?.ok) throw new Error(res?.error || "Import failed");
2520
2689
  const count = Number(res?.data?.importedCount || 0);
@@ -2534,7 +2703,7 @@ function LibraryMarketplace({ onImported }) {
2534
2703
  showToast(`Import failed: ${parseApiError(err)}`, "error");
2535
2704
  }
2536
2705
  setImporting(null);
2537
- }, [onImported]);
2706
+ }, [onImported, previewData]);
2538
2707
 
2539
2708
  const doImportAll = useCallback(async (sourceId) => {
2540
2709
  setImporting(sourceId);
@@ -2574,11 +2743,28 @@ function LibraryMarketplace({ onImported }) {
2574
2743
  return html`
2575
2744
  <div style="margin-top:10px;padding:12px;border:1px solid var(--border,#333);border-radius:10px;">
2576
2745
  <div style="display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;margin-bottom:10px;">
2577
- <div style="font-size:0.95em;font-weight:600;">${iconText(":package: Library Marketplace")}</div>
2578
- <${Button} variant="text" size="small" onClick=${() => setShowCustom((v) => !v)}
2579
- style=${{ fontSize: "0.75em", textTransform: "none" }}>
2580
- ${showCustom ? "Hide Custom URL" : "Custom URL Import"}
2581
- <//>
2746
+ <div>
2747
+ <div style="font-size:0.95em;font-weight:600;">${iconText(":package: Library Marketplace")}</div>
2748
+ <div style="font-size:0.76em;color:var(--text-secondary);margin-top:2px;">
2749
+ Fast source metadata loads first; health and branch checks refresh separately.
2750
+ </div>
2751
+ </div>
2752
+ <div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;">
2753
+ ${refreshing && !loading ? html`
2754
+ <span style="font-size:0.74em;color:var(--text-secondary);display:inline-flex;align-items:center;gap:5px;">
2755
+ <${Spinner} size=${12} /> Refreshing source health…
2756
+ </span>
2757
+ ` : null}
2758
+ <${Button} variant="text" size="small" onClick=${() => refreshSources({ probe: true, refresh: true, background: true })}
2759
+ disabled=${refreshing || loading}
2760
+ style=${{ fontSize: "0.75em", textTransform: "none" }}>
2761
+ ${refreshing && !loading ? "Refreshing…" : "Refresh Health"}
2762
+ <//>
2763
+ <${Button} variant="text" size="small" onClick=${() => setShowCustom((v) => !v)}
2764
+ style=${{ fontSize: "0.75em", textTransform: "none" }}>
2765
+ ${showCustom ? "Hide Custom URL" : "Custom URL Import"}
2766
+ <//>
2767
+ </div>
2582
2768
  </div>
2583
2769
 
2584
2770
  ${/* ── Search Bar ── */ ""}
@@ -2823,6 +3009,18 @@ export function LibraryTab() {
2823
3009
  return list;
2824
3010
  }, [entries.value, filterType.value]);
2825
3011
 
3012
+ const groupedAgentSections = useMemo(() => {
3013
+ if (filterType.value !== "agent") return [];
3014
+ const interactive = displayed.filter((entry) => entry.agentCategory === "interactive");
3015
+ const voice = displayed.filter((entry) => entry.agentCategory === "voice");
3016
+ const task = displayed.filter((entry) => !entry.agentCategory || entry.agentCategory === "task");
3017
+ return [
3018
+ { key: "interactive", title: "Manual Chat Agents", items: interactive },
3019
+ { key: "voice", title: "Voice Agents", items: voice },
3020
+ { key: "task", title: "Task Templates", items: task },
3021
+ ].filter((section) => section.items.length > 0);
3022
+ }, [displayed, filterType.value]);
3023
+
2826
3024
  return html`
2827
3025
  <div class="library-root">
2828
3026
  <div class="library-header">
@@ -2863,7 +3061,6 @@ export function LibraryTab() {
2863
3061
  </div>
2864
3062
 
2865
3063
  ${filterType.value !== "mcp" && html`<${ProfileMatcher} />`}
2866
- ${filterType.value !== "mcp" && html`<${LibraryMarketplace} onImported=${loadEntries} />`}
2867
3064
  ${filterType.value !== "mcp" && html`<${ScopeDetector} />`}
2868
3065
 
2869
3066
  ${/* ── MCP Marketplace View ── */
@@ -2889,11 +3086,29 @@ export function LibraryTab() {
2889
3086
  `}
2890
3087
 
2891
3088
  ${filterType.value !== "mcp" && !loading && displayed.length > 0 && html`
2892
- <div class="library-grid">
2893
- ${displayed.map((e) => html`
2894
- <${LibraryCard} key=${e.id} entry=${e} onSelect=${handleSelect} />
2895
- `)}
2896
- </div>
3089
+ ${filterType.value === "agent"
3090
+ ? html`
3091
+ ${groupedAgentSections.map((section) => html`
3092
+ <${Box} key=${section.key} sx=${{ display: "flex", flexDirection: "column", gap: 1.25, mb: 2 }}>
3093
+ <${Stack} direction="row" alignItems="center" spacing=${1}>
3094
+ <${Typography} variant="subtitle2">${section.title}<//>
3095
+ <${Chip} label=${section.items.length} size="small" variant="outlined" />
3096
+ <//>
3097
+ <div class="library-grid">
3098
+ ${section.items.map((e) => html`
3099
+ <${LibraryCard} key=${e.id} entry=${e} onSelect=${handleSelect} />
3100
+ `)}
3101
+ </div>
3102
+ </${Box}>
3103
+ `)}
3104
+ `
3105
+ : html`
3106
+ <div class="library-grid">
3107
+ ${displayed.map((e) => html`
3108
+ <${LibraryCard} key=${e.id} entry=${e} onSelect=${handleSelect} />
3109
+ `)}
3110
+ </div>
3111
+ `}
2897
3112
  `}
2898
3113
 
2899
3114
  ${editing && html`
@@ -2906,3 +3121,20 @@ export function LibraryTab() {
2906
3121
  </div>
2907
3122
  `;
2908
3123
  }
3124
+
3125
+ export function LibraryMarketplaceTab() {
3126
+ injectStyles();
3127
+
3128
+ const handleImported = useCallback(() => {
3129
+ refreshTab("library", { background: true, manual: false, force: true });
3130
+ }, []);
3131
+
3132
+ return html`
3133
+ <div class="library-root">
3134
+ <div class="library-header">
3135
+ <h2>${iconText(":package: Marketplace")}</h2>
3136
+ </div>
3137
+ <${LibraryMarketplace} onImported=${handleImported} />
3138
+ </div>
3139
+ `;
3140
+ }
package/ui/tabs/tasks.js CHANGED
@@ -1382,6 +1382,7 @@ export function StartTaskModal({
1382
1382
  task,
1383
1383
  defaultSdk = "auto",
1384
1384
  allowTaskIdInput = false,
1385
+ presentation = "modal",
1385
1386
  onClose,
1386
1387
  onStart,
1387
1388
  }) {
@@ -1455,20 +1456,11 @@ export function StartTaskModal({
1455
1456
 
1456
1457
  const resetToInitial = useCallback(() => {
1457
1458
  const base = initialSnapshotRef.current || {};
1458
- setTitle(base.title || "");
1459
- setDescription(base.description || "");
1460
- setBaseBranch(base.baseBranch || "");
1461
- setPriority(base.priority || "medium");
1462
- setTaskType(base.taskType || "task");
1463
- setEpicId(base.epicId || "");
1464
- setStoryPoints(base.storyPoints || "");
1465
- setSelectedSprintId(base.sprintId || "");
1466
- setSprintOrderInput(base.sprintOrder || "");
1467
- setDependenciesInput(base.dependenciesInput || "");
1468
- setTagsInput(base.tagsInput || "");
1469
- setDraft(Boolean(base.draft));
1459
+ setSdk(base.sdk || "auto");
1460
+ setModel(base.model || "");
1461
+ setTaskIdInput(base.taskIdInput || task?.id || "");
1470
1462
  showToast("Changes discarded", "info");
1471
- }, []);
1463
+ }, [task?.id]);
1472
1464
 
1473
1465
  const handleStart = async ({ closeAfterStart = true } = {}) => {
1474
1466
  if (starting) return;
@@ -2969,6 +2969,16 @@ export class WorkflowEngine extends EventEmitter {
2969
2969
  normalized.activeNodeCount = 0;
2970
2970
  }
2971
2971
  if (normalized.status !== WorkflowStatus.RUNNING) {
2972
+ normalized.activeNodeCount = 0;
2973
+ if (!Number.isFinite(Number(normalized.endedAt))) {
2974
+ const fallbackEndedAt = Math.max(
2975
+ Number(normalized.interruptedAt) || 0,
2976
+ Number(normalized.lastProgressAt) || 0,
2977
+ Number(normalized.lastLogAt) || 0,
2978
+ Number(normalized.startedAt) || 0,
2979
+ );
2980
+ normalized.endedAt = fallbackEndedAt > 0 ? fallbackEndedAt : null;
2981
+ }
2972
2982
  normalized.isStuck = false;
2973
2983
  normalized.stuckMs = 0;
2974
2984
  return normalized;
@@ -3172,6 +3182,10 @@ export class WorkflowEngine extends EventEmitter {
3172
3182
  summary.status = WorkflowStatus.PAUSED;
3173
3183
  summary.resumable = canResume;
3174
3184
  summary.interruptedAt = now;
3185
+ summary.activeNodeCount = 0;
3186
+ if (!Number.isFinite(Number(summary.endedAt))) {
3187
+ summary.endedAt = now;
3188
+ }
3175
3189
  if (!canResume) summary.resumeResult = "recovery_cap_exceeded";
3176
3190
  }
3177
3191
  if (canResume && !forceResumable) resumableStaleRunsAssigned += 1;
@@ -3253,7 +3267,8 @@ export class WorkflowEngine extends EventEmitter {
3253
3267
  this._resumingRuns = true;
3254
3268
 
3255
3269
  try {
3256
- const runs = this._readRunIndex().filter(
3270
+ const allRuns = this._readRunIndex();
3271
+ const runs = allRuns.filter(
3257
3272
  (r) => r.status === WorkflowStatus.PAUSED && r.resumable,
3258
3273
  );
3259
3274
 
@@ -3272,9 +3287,6 @@ export class WorkflowEngine extends EventEmitter {
3272
3287
  // and mark older duplicates as not-resumable before we even try them.
3273
3288
  const runDetailCache = new Map(); // runId → parsed detail
3274
3289
  const latestByTaskId = new Map(); // taskId → run entry (highest startedAt)
3275
-
3276
- const allRuns = this._readRunIndex();
3277
-
3278
3290
  for (const run of allRuns) {
3279
3291
  const dp = resolve(this.runsDir, `${run.runId}.json`);
3280
3292
  if (!existsSync(dp)) continue;
@@ -542,6 +542,43 @@ async function createKanbanTaskWithProject(kanban, taskData = {}, projectIdValue
542
542
  payload.projectId = resolvedProjectId;
543
543
  }
544
544
 
545
+ const createTaskParamNames = (() => {
546
+ try {
547
+ const inspectTarget =
548
+ typeof kanban.createTask?.getMockImplementation === "function"
549
+ ? kanban.createTask.getMockImplementation() || kanban.createTask
550
+ : kanban.createTask;
551
+ const source = Function.prototype.toString.call(inspectTarget);
552
+ const parenMatch = source.match(/^[^(]*\(([^)]*)\)/s);
553
+ if (parenMatch) {
554
+ return String(parenMatch[1] || "")
555
+ .split(",")
556
+ .map((entry) =>
557
+ String(entry || "")
558
+ .trim()
559
+ .replace(/^\.{3}/, "")
560
+ .replace(/\s*=.*$/s, "")
561
+ .trim(),
562
+ )
563
+ .filter(Boolean);
564
+ }
565
+ const arrowMatch = source.match(/^(?:async\s+)?([A-Za-z_$][\w$]*)\s*=>/);
566
+ if (arrowMatch?.[1]) return [arrowMatch[1]];
567
+ } catch {
568
+ // Fall back to the project-aware signature when adapter source is opaque.
569
+ }
570
+ return [];
571
+ })();
572
+ const firstParamName = String(createTaskParamNames[0] || "").toLowerCase();
573
+ const payloadOnlyCreateTask =
574
+ createTaskParamNames.length === 1 &&
575
+ /(task|payload|spec|data)/i.test(firstParamName) &&
576
+ !/project/i.test(firstParamName);
577
+
578
+ if (payloadOnlyCreateTask) {
579
+ return kanban.createTask(payload);
580
+ }
581
+
545
582
  const taskPayload = { ...payload };
546
583
  delete taskPayload.projectId;
547
584
  return kanban.createTask(resolvedProjectId, taskPayload);