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.
- package/README.md +23 -1
- package/agent/agent-event-bus.mjs +31 -2
- package/agent/agent-pool.mjs +251 -11
- package/agent/agent-prompts.mjs +5 -1
- package/agent/agent-supervisor.mjs +22 -0
- package/agent/primary-agent.mjs +115 -5
- package/cli.mjs +3 -2
- package/config/config.mjs +4 -1
- package/desktop/main.mjs +350 -25
- package/desktop/preload.cjs +8 -0
- package/desktop/preload.mjs +19 -0
- package/entrypoint.mjs +332 -0
- package/infra/health-status.mjs +72 -0
- package/infra/library-manager.mjs +58 -1
- package/infra/maintenance.mjs +1 -2
- package/infra/monitor.mjs +25 -7
- package/infra/session-tracker.mjs +30 -3
- package/package.json +10 -4
- package/server/bosun-mcp-server.mjs +1004 -0
- package/server/setup-web-server.mjs +287 -258
- package/server/ui-server.mjs +218 -23
- package/shell/claude-shell.mjs +14 -1
- package/shell/codex-model-profiles.mjs +166 -29
- package/shell/codex-shell.mjs +56 -18
- package/shell/opencode-providers.mjs +20 -8
- package/task/task-executor.mjs +28 -0
- package/task/task-store.mjs +13 -4
- package/tools/list-todos.mjs +7 -1
- package/ui/app.js +3 -2
- package/ui/components/agent-selector.js +127 -0
- package/ui/components/session-list.js +2 -0
- package/ui/demo-defaults.js +6 -6
- package/ui/modules/router.js +2 -0
- package/ui/modules/state.js +13 -5
- package/ui/tabs/chat.js +3 -0
- package/ui/tabs/library.js +284 -52
- package/ui/tabs/tasks.js +5 -13
- package/workflow/workflow-engine.mjs +16 -4
- package/workflow/workflow-nodes/definitions.mjs +37 -0
- package/workflow/workflow-nodes.mjs +489 -153
- package/workflow/workflow-templates.mjs +0 -5
- package/workflow-templates/github.mjs +106 -16
- 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
|
});
|
package/ui/tabs/library.js
CHANGED
|
@@ -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
|
|
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
|
|
455
|
-
{ value: "
|
|
456
|
-
{ value: "
|
|
457
|
-
{ value: "
|
|
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
|
|
461
|
-
const value = String(
|
|
462
|
-
if (value === "voice" || value === "task" || 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
|
|
480
|
-
const
|
|
481
|
-
if (parsedContent?.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
803
|
+
const agentCategory = normalizeAgentCategory(form.agentCategory);
|
|
804
|
+
const agentType = deriveAgentTypeFromCategory(agentCategory);
|
|
742
805
|
content.agentType = agentType;
|
|
743
|
-
|
|
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
|
|
862
|
-
<${Select} value=${
|
|
863
|
-
${
|
|
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.
|
|
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(
|
|
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
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
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
|
-
|
|
2464
|
-
}
|
|
2465
|
-
|
|
2466
|
-
|
|
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
|
|
2578
|
-
|
|
2579
|
-
style
|
|
2580
|
-
|
|
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
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
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
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
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
|
|
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);
|