bosun 0.41.2 → 0.41.3
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/.env.example +1 -1
- package/agent/agent-prompt-catalog.mjs +971 -0
- package/agent/agent-prompts.mjs +2 -970
- package/agent/agent-supervisor.mjs +6 -3
- package/agent/autofix-git.mjs +33 -0
- package/agent/autofix-prompts.mjs +151 -0
- package/agent/autofix.mjs +11 -175
- package/agent/bosun-skills.mjs +3 -2
- package/bosun.config.example.json +17 -0
- package/bosun.schema.json +87 -188
- package/cli.mjs +34 -1
- package/config/config-doctor.mjs +5 -250
- package/config/config-file-names.mjs +5 -0
- package/config/config.mjs +89 -493
- package/config/executor-config.mjs +493 -0
- package/config/repo-root.mjs +1 -2
- package/config/workspace-health.mjs +242 -0
- package/git/git-safety.mjs +15 -0
- package/github/github-oauth-portal.mjs +46 -0
- package/infra/library-manager-utils.mjs +22 -0
- package/infra/library-manager-well-known-sources.mjs +578 -0
- package/infra/library-manager.mjs +512 -1030
- package/infra/monitor.mjs +28 -9
- package/infra/session-tracker.mjs +10 -7
- package/kanban/kanban-adapter.mjs +17 -1
- package/lib/codebase-audit-manifests.mjs +117 -0
- package/lib/codebase-audit.mjs +18 -115
- package/package.json +18 -3
- package/server/ui-server.mjs +1194 -79
- package/shell/codex-config-file.mjs +178 -0
- package/shell/codex-config.mjs +538 -575
- package/task/task-cli.mjs +54 -3
- package/task/task-executor.mjs +143 -13
- package/task/task-store.mjs +409 -1
- package/telegram/telegram-bot.mjs +127 -0
- package/tools/apply-pr-suggestions.mjs +401 -0
- package/tools/syntax-check.mjs +21 -9
- package/ui/app.js +3 -14
- package/ui/components/kanban-board.js +227 -4
- package/ui/components/session-list.js +85 -5
- package/ui/demo-defaults.js +334 -80
- package/ui/demo.html +155 -0
- package/ui/modules/session-api.js +96 -0
- package/ui/modules/settings-schema.js +1 -2
- package/ui/modules/state.js +21 -3
- package/ui/setup.html +4 -5
- package/ui/styles/components.css +58 -4
- package/ui/tabs/agents.js +12 -15
- package/ui/tabs/control.js +1 -0
- package/ui/tabs/library.js +484 -22
- package/ui/tabs/manual-flows.js +105 -29
- package/ui/tabs/tasks.js +785 -140
- package/ui/tabs/telemetry.js +129 -11
- package/ui/tabs/workflow-canvas-utils.mjs +130 -0
- package/ui/tabs/workflows.js +293 -23
- package/voice/voice-tool-definitions.mjs +757 -0
- package/voice/voice-tools.mjs +34 -778
- package/workflow/manual-flow-audit.mjs +165 -0
- package/workflow/manual-flows.mjs +164 -259
- package/workflow/workflow-engine.mjs +147 -58
- package/workflow/workflow-nodes/definitions.mjs +1207 -0
- package/workflow/workflow-nodes/transforms.mjs +612 -0
- package/workflow/workflow-nodes.mjs +304 -52
- package/workflow/workflow-templates.mjs +313 -191
- package/workflow-templates/_helpers.mjs +154 -0
- package/workflow-templates/agents.mjs +61 -4
- package/workflow-templates/code-quality.mjs +7 -7
- package/workflow-templates/github.mjs +20 -10
- package/workflow-templates/task-batch.mjs +20 -9
- package/workflow-templates/task-lifecycle.mjs +31 -6
- package/workspace/worktree-manager.mjs +277 -3
package/ui/tabs/library.js
CHANGED
|
@@ -238,6 +238,34 @@ const LIBRARY_STYLES = `
|
|
|
238
238
|
}
|
|
239
239
|
`;
|
|
240
240
|
|
|
241
|
+
function normalizeLibraryTaskStatus(status) {
|
|
242
|
+
return String(status || "").trim().toLowerCase();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function isSelectableLibraryTask(task) {
|
|
246
|
+
const status = normalizeLibraryTaskStatus(task?.status);
|
|
247
|
+
return (
|
|
248
|
+
status === "draft" ||
|
|
249
|
+
status === "todo" ||
|
|
250
|
+
status === "backlog" ||
|
|
251
|
+
status === "planned" ||
|
|
252
|
+
status === "open" ||
|
|
253
|
+
status === "new" ||
|
|
254
|
+
status === ""
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function extractSelectableLibraryTasks(payload) {
|
|
259
|
+
const tasks = Array.isArray(payload?.tasks)
|
|
260
|
+
? payload.tasks
|
|
261
|
+
: Array.isArray(payload?.data)
|
|
262
|
+
? payload.data
|
|
263
|
+
: Array.isArray(payload)
|
|
264
|
+
? payload
|
|
265
|
+
: [];
|
|
266
|
+
return tasks.filter(isSelectableLibraryTask).slice(0, 100);
|
|
267
|
+
}
|
|
268
|
+
|
|
241
269
|
let stylesInjected = false;
|
|
242
270
|
function injectStyles() {
|
|
243
271
|
if (stylesInjected) return;
|
|
@@ -1530,6 +1558,39 @@ function ScopeDetector() {
|
|
|
1530
1558
|
`;
|
|
1531
1559
|
}
|
|
1532
1560
|
|
|
1561
|
+
/* ─ Workflow Step Resolver ─────────────────────────────────── */
|
|
1562
|
+
|
|
1563
|
+
async function resolveWorkflowSteps(title, description) {
|
|
1564
|
+
const steps = ["tdd", "implementation", "review"];
|
|
1565
|
+
const results = {};
|
|
1566
|
+
for (const step of steps) {
|
|
1567
|
+
const stepTitle = `${step}: ${title}`;
|
|
1568
|
+
try {
|
|
1569
|
+
const resp = await testProfileMatch({ title: stepTitle, description, topN: 1 });
|
|
1570
|
+
results[step] = resp;
|
|
1571
|
+
} catch {
|
|
1572
|
+
results[step] = { best: null, candidates: [], plan: null };
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
return results;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
/* ─ Score Breakdown Definitions ────────────────────────────── */
|
|
1579
|
+
|
|
1580
|
+
const SIGNAL_DEFS = [
|
|
1581
|
+
{ key: "titlePattern", label: "Title Pattern", max: 10, color: "#3b82f6" },
|
|
1582
|
+
{ key: "scope", label: "Scope", max: 6, color: "#22c55e" },
|
|
1583
|
+
{ key: "tags", label: "Tags", max: 6, color: "#eab308" },
|
|
1584
|
+
{ key: "voice", label: "Voice", max: 3, color: "#a855f7" },
|
|
1585
|
+
{ key: "paths", label: "Paths", max: 8, color: "#f97316" },
|
|
1586
|
+
{ key: "repoCtx", label: "Repo Ctx", max: 6, color: "#14b8a6" },
|
|
1587
|
+
{ key: "fileType", label: "File Type", max: 4, color: "#ec4899" },
|
|
1588
|
+
{ key: "descMatch", label: "Desc Match", max: 8, color: "#06b6d4" },
|
|
1589
|
+
{ key: "taskType", label: "Task Type", max: 5, color: "#6366f1" },
|
|
1590
|
+
];
|
|
1591
|
+
|
|
1592
|
+
const MAX_SCORE_TOTAL = SIGNAL_DEFS.reduce((sum, s) => sum + s.max, 0);
|
|
1593
|
+
|
|
1533
1594
|
/* ─ Profile Matcher Panel ─────────────────────────────────── */
|
|
1534
1595
|
|
|
1535
1596
|
function ProfileMatcher() {
|
|
@@ -1542,6 +1603,9 @@ function ProfileMatcher() {
|
|
|
1542
1603
|
const [importSkills, setImportSkills] = useState(true);
|
|
1543
1604
|
const [importPrompts, setImportPrompts] = useState(true);
|
|
1544
1605
|
const [importTools, setImportTools] = useState(true);
|
|
1606
|
+
const [expandedAlts, setExpandedAlts] = useState({});
|
|
1607
|
+
const [workflowSteps, setWorkflowSteps] = useState(null);
|
|
1608
|
+
const [workflowLoading, setWorkflowLoading] = useState(false);
|
|
1545
1609
|
|
|
1546
1610
|
// ── Execution Plan state ──────────────────────────────────────────────
|
|
1547
1611
|
const [execPlan, setExecPlan] = useState(null);
|
|
@@ -1558,11 +1622,10 @@ function ProfileMatcher() {
|
|
|
1558
1622
|
useEffect(() => {
|
|
1559
1623
|
setTaskListLoading(true);
|
|
1560
1624
|
const wsParam = typeof window !== "undefined" && window.__bosunWorkspaceId ? `&workspace=${encodeURIComponent(window.__bosunWorkspaceId)}` : "";
|
|
1561
|
-
fetch(`/api/tasks?status=
|
|
1625
|
+
fetch(`/api/tasks?pageSize=100&status=library${wsParam}`)
|
|
1562
1626
|
.then((r) => r.json())
|
|
1563
1627
|
.then((data) => {
|
|
1564
|
-
|
|
1565
|
-
setTaskList(tasks.slice(0, 100));
|
|
1628
|
+
setTaskList(extractSelectableLibraryTasks(data));
|
|
1566
1629
|
})
|
|
1567
1630
|
.catch(() => {})
|
|
1568
1631
|
.finally(() => setTaskListLoading(false));
|
|
@@ -1677,27 +1740,140 @@ function ProfileMatcher() {
|
|
|
1677
1740
|
<//>
|
|
1678
1741
|
</div>
|
|
1679
1742
|
|
|
1680
|
-
${/* ── Library Profile Match Results ── */ ""}
|
|
1743
|
+
${/* ── Library Profile Match Results (Rich Visualization) ── */ ""}
|
|
1681
1744
|
${best && html`
|
|
1682
|
-
<div class="library-profile-match" style="margin-top:
|
|
1683
|
-
|
|
1684
|
-
<div>
|
|
1685
|
-
<span
|
|
1686
|
-
|
|
1745
|
+
<div class="library-profile-match" style="margin-top:12px;padding:12px;border:1px solid var(--border,#333);border-radius:10px;background:var(--bg-card,rgba(255,255,255,0.03));">
|
|
1746
|
+
${/* ── Header with name, score, confidence ── */ ""}
|
|
1747
|
+
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
|
|
1748
|
+
<span style="font-weight:600;font-size:0.95em;">${iconText(`${TYPE_ICONS.agent} ${best.name}`)}</span>
|
|
1749
|
+
<${Chip} size="small" label=${`Score: ${best.score}/${MAX_SCORE_TOTAL}`} color="primary" variant="outlined" />
|
|
1750
|
+
<${Chip} size="small" label=${`${Math.round(Number(best.confidence || 0) * 100)}% confidence`}
|
|
1751
|
+
color=${Number(best.confidence || 0) >= 0.45 ? "success" : "warning"} variant="outlined" />
|
|
1687
1752
|
</div>
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
<
|
|
1753
|
+
${best.description && html`<div style="font-size:0.82em;color:var(--text-secondary);margin-top:6px;">${best.description}</div>`}
|
|
1754
|
+
|
|
1755
|
+
${/* ── Auto-trigger status ── */ ""}
|
|
1756
|
+
<div style="display:flex;align-items:center;gap:6px;margin-top:8px;padding:6px 10px;border-radius:8px;background:${auto.shouldAutoApply ? "rgba(34,197,94,0.1)" : Number(best.confidence || 0) >= 0.35 ? "rgba(245,158,11,0.1)" : "rgba(239,68,68,0.1)"};">
|
|
1757
|
+
<span style="font-size:1.1em;">${auto.shouldAutoApply ? "✅" : Number(best.confidence || 0) >= 0.35 ? "⚠️" : "❌"}</span>
|
|
1758
|
+
<span style="font-size:0.82em;font-weight:500;color:${auto.shouldAutoApply ? "#22c55e" : Number(best.confidence || 0) >= 0.35 ? "#f59e0b" : "#ef4444"};">
|
|
1759
|
+
Auto-trigger: ${auto.shouldAutoApply ? "Eligible" : "Not eligible"}
|
|
1760
|
+
</span>
|
|
1761
|
+
<span style="font-size:0.75em;color:var(--text-secondary);">(${auto.reason || "n/a"})</span>
|
|
1762
|
+
</div>
|
|
1763
|
+
|
|
1764
|
+
${/* ── Score Breakdown Bar ── */ ""}
|
|
1765
|
+
<div style="margin-top:10px;">
|
|
1766
|
+
<div style="font-size:0.78em;font-weight:600;color:var(--text-secondary);margin-bottom:4px;">Score Breakdown</div>
|
|
1767
|
+
<div style="display:flex;height:22px;border-radius:6px;overflow:hidden;background:rgba(255,255,255,0.05);border:1px solid var(--border,#333);">
|
|
1768
|
+
${SIGNAL_DEFS.map((sig) => {
|
|
1769
|
+
const val = Number(best.breakdown?.[sig.key] || 0);
|
|
1770
|
+
const pct = (sig.max / MAX_SCORE_TOTAL) * 100;
|
|
1771
|
+
return html`
|
|
1772
|
+
<${Tooltip} title=${`${sig.label}: ${val}/${sig.max}`} key=${sig.key}>
|
|
1773
|
+
<div style="width:${pct}%;height:100%;position:relative;border-right:1px solid rgba(0,0,0,0.2);">
|
|
1774
|
+
<div style="position:absolute;bottom:0;left:0;right:0;height:${sig.max > 0 ? (val / sig.max) * 100 : 0}%;background:${sig.color};opacity:0.85;transition:height 0.3s;"></div>
|
|
1775
|
+
<div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:0.6em;color:#fff;white-space:nowrap;text-shadow:0 1px 2px rgba(0,0,0,0.6);z-index:1;">${val > 0 ? val : ""}</div>
|
|
1776
|
+
</div>
|
|
1777
|
+
<//>
|
|
1778
|
+
`;
|
|
1779
|
+
})}
|
|
1780
|
+
</div>
|
|
1781
|
+
<div style="display:flex;flex-wrap:wrap;gap:6px;margin-top:4px;">
|
|
1782
|
+
${SIGNAL_DEFS.map((sig) => {
|
|
1783
|
+
const val = Number(best.breakdown?.[sig.key] || 0);
|
|
1784
|
+
return html`<span key=${sig.key} style="font-size:0.65em;display:flex;align-items:center;gap:3px;color:var(--text-secondary);">
|
|
1785
|
+
<span style="display:inline-block;width:8px;height:8px;border-radius:2px;background:${sig.color};opacity:${val > 0 ? 1 : 0.3};"></span>
|
|
1786
|
+
${sig.label}
|
|
1787
|
+
</span>`;
|
|
1788
|
+
})}
|
|
1789
|
+
</div>
|
|
1790
|
+
</div>
|
|
1791
|
+
|
|
1792
|
+
${/* ── Detected Task Types ── */ ""}
|
|
1793
|
+
${result?.context?.detectedTaskTypes?.length > 0 && html`
|
|
1794
|
+
<div style="margin-top:8px;display:flex;align-items:center;gap:6px;flex-wrap:wrap;">
|
|
1795
|
+
<span style="font-size:0.78em;color:var(--text-secondary);font-weight:500;">Detected:</span>
|
|
1796
|
+
${result.context.detectedTaskTypes.map((t) => html`
|
|
1797
|
+
<${Chip} key=${t} size="small" label=${t} variant="outlined"
|
|
1798
|
+
style=${{ fontSize: "0.72em", height: "20px" }} />
|
|
1799
|
+
`)}
|
|
1800
|
+
</div>
|
|
1694
1801
|
`}
|
|
1802
|
+
|
|
1803
|
+
${/* ── Skills Preview ── */ ""}
|
|
1695
1804
|
${plan && html`
|
|
1696
|
-
<div style="
|
|
1697
|
-
|
|
1805
|
+
<div style="margin-top:10px;padding:8px 10px;border:1px solid var(--border,#333);border-radius:8px;background:rgba(255,255,255,0.02);">
|
|
1806
|
+
<div style="font-size:0.8em;font-weight:600;margin-bottom:4px;">📚 Skills (${(plan.skillIds || []).length} resolved)</div>
|
|
1807
|
+
${(plan.skillIds || []).length > 0 ? (plan.skillIds || []).map((s) => html`
|
|
1808
|
+
<div key=${s} style="font-size:0.78em;color:var(--text-secondary);padding:2px 0;"> ✓ ${s} (profile-skill)</div>
|
|
1809
|
+
`) : html`<div style="font-size:0.78em;color:var(--text-secondary);"> No skills resolved</div>`}
|
|
1810
|
+
<div style="font-size:0.78em;color:var(--text-secondary);margin-top:4px;">
|
|
1811
|
+
Prompt: ${plan.prompt?.name || "none"} · Tools: ${(plan.builtinToolIds || []).slice(0, 6).join(", ") || "none"} · MCP: ${(plan.enabledMcpServers || []).slice(0, 4).join(", ") || "none"}
|
|
1812
|
+
</div>
|
|
1813
|
+
</div>
|
|
1698
1814
|
`}
|
|
1815
|
+
|
|
1816
|
+
${/* ── Workflow Step Preview ── */ ""}
|
|
1817
|
+
<div style="margin-top:10px;">
|
|
1818
|
+
<div style="display:flex;align-items:center;gap:8px;">
|
|
1819
|
+
<span style="font-size:0.8em;font-weight:600;">Workflow Steps</span>
|
|
1820
|
+
<${Button} variant="text" size="small" onClick=${async () => {
|
|
1821
|
+
if (!title.trim()) return;
|
|
1822
|
+
setWorkflowLoading(true);
|
|
1823
|
+
try {
|
|
1824
|
+
const steps = await resolveWorkflowSteps(title.trim(), description.trim());
|
|
1825
|
+
setWorkflowSteps(steps);
|
|
1826
|
+
} catch { setWorkflowSteps(null); }
|
|
1827
|
+
setWorkflowLoading(false);
|
|
1828
|
+
}} disabled=${workflowLoading || !title.trim()}
|
|
1829
|
+
style=${{ fontSize: "0.72em", minWidth: 0, padding: "2px 8px" }}>
|
|
1830
|
+
${workflowLoading ? html`<${Spinner} size=${12} />` : "Resolve"}
|
|
1831
|
+
<//>
|
|
1832
|
+
</div>
|
|
1833
|
+
${workflowSteps && html`
|
|
1834
|
+
<div style="margin-top:4px;padding:8px 10px;border-left:3px solid var(--border,#555);font-size:0.78em;font-family:monospace;">
|
|
1835
|
+
${Object.entries(workflowSteps).map(([step, stepResult], i, arr) => {
|
|
1836
|
+
const connector = i === 0 ? "┌" : i === arr.length - 1 ? "└" : "├";
|
|
1837
|
+
const stepBest = stepResult?.best;
|
|
1838
|
+
const stepPlan = stepResult?.plan;
|
|
1839
|
+
const skills = stepPlan?.skillIds?.slice(0, 3)?.join(", ") || "none";
|
|
1840
|
+
return html`<div key=${step} style="color:var(--text-secondary);padding:1px 0;">
|
|
1841
|
+
${connector} <span style="text-transform:capitalize;font-weight:500;color:var(--text-primary);">${step}</span> → ${stepBest?.name || "No match"} + [${skills}]
|
|
1842
|
+
</div>`;
|
|
1843
|
+
})}
|
|
1844
|
+
</div>
|
|
1845
|
+
`}
|
|
1846
|
+
</div>
|
|
1847
|
+
|
|
1848
|
+
${/* ── Alternatives as expandable cards ── */ ""}
|
|
1699
1849
|
${candidates.length > 1 && html`
|
|
1700
|
-
<div style="
|
|
1850
|
+
<div style="margin-top:10px;">
|
|
1851
|
+
<div style="font-size:0.8em;font-weight:600;color:var(--text-secondary);margin-bottom:6px;">Alternatives (${candidates.length - 1})</div>
|
|
1852
|
+
${candidates.slice(1).map((c, i) => html`
|
|
1853
|
+
<div key=${c.id || i} style="margin-bottom:6px;border:1px solid var(--border,#333);border-radius:8px;overflow:hidden;background:rgba(255,255,255,0.02);">
|
|
1854
|
+
<div onClick=${() => setExpandedAlts((prev) => ({ ...prev, [i]: !prev[i] }))}
|
|
1855
|
+
style="display:flex;align-items:center;gap:8px;padding:6px 10px;cursor:pointer;user-select:none;">
|
|
1856
|
+
<span style="font-size:0.82em;font-weight:500;flex:1;">${iconText(`${TYPE_ICONS.agent} ${c.name}`)}</span>
|
|
1857
|
+
<div style="width:60px;height:6px;border-radius:3px;background:rgba(255,255,255,0.08);overflow:hidden;">
|
|
1858
|
+
<div style="height:100%;width:${Math.round(Number(c.confidence || 0) * 100)}%;background:#3b82f6;border-radius:3px;"></div>
|
|
1859
|
+
</div>
|
|
1860
|
+
<span style="font-size:0.72em;color:var(--text-secondary);min-width:40px;text-align:right;">${c.score}pts</span>
|
|
1861
|
+
<span style="font-size:0.7em;transform:${expandedAlts[i] ? "rotate(180deg)" : "none"};transition:transform 0.2s;">▼</span>
|
|
1862
|
+
</div>
|
|
1863
|
+
${expandedAlts[i] && html`
|
|
1864
|
+
<div style="padding:6px 10px 8px;border-top:1px solid var(--border,#333);font-size:0.78em;color:var(--text-secondary);">
|
|
1865
|
+
<div>Confidence: ${Math.round(Number(c.confidence || 0) * 100)}%</div>
|
|
1866
|
+
${c.description && html`<div style="margin-top:2px;">${c.description}</div>`}
|
|
1867
|
+
${Array.isArray(c.reasons) && c.reasons.length > 0 && html`
|
|
1868
|
+
<div style="margin-top:4px;display:flex;flex-wrap:wrap;gap:4px;">
|
|
1869
|
+
${c.reasons.map((r) => html`<${Chip} key=${r} size="small" label=${r} variant="outlined" style=${{ fontSize: "0.68em", height: "18px" }} />`)}
|
|
1870
|
+
</div>
|
|
1871
|
+
`}
|
|
1872
|
+
</div>
|
|
1873
|
+
`}
|
|
1874
|
+
</div>
|
|
1875
|
+
`)}
|
|
1876
|
+
</div>
|
|
1701
1877
|
`}
|
|
1702
1878
|
</div>
|
|
1703
1879
|
`}
|
|
@@ -2245,9 +2421,295 @@ function AgentLibraryImporter({ onImported }) {
|
|
|
2245
2421
|
`;
|
|
2246
2422
|
}
|
|
2247
2423
|
|
|
2248
|
-
/*
|
|
2249
|
-
|
|
2250
|
-
|
|
2424
|
+
/* ─ Library Marketplace ───────────────────────────────────── */
|
|
2425
|
+
|
|
2426
|
+
const MARKETPLACE_CATEGORIES = [
|
|
2427
|
+
{ id: "all", label: "All" },
|
|
2428
|
+
{ id: "official", label: "Official" },
|
|
2429
|
+
{ id: "community", label: "Community" },
|
|
2430
|
+
{ id: "agents", label: "Agents" },
|
|
2431
|
+
{ id: "skills", label: "Skills" },
|
|
2432
|
+
{ id: "mcp", label: "MCP" },
|
|
2433
|
+
];
|
|
2434
|
+
|
|
2435
|
+
function getTrustTier(source) {
|
|
2436
|
+
const tier = String(source?.trustTier || "").toLowerCase();
|
|
2437
|
+
if (tier === "official" || tier === "partner") return { label: "Official", color: "#22c55e", icon: "🏢" };
|
|
2438
|
+
if (tier === "community") return { label: "Community", color: "#3b82f6", icon: "👥" };
|
|
2439
|
+
return { label: "Unknown", color: "#6b7280", icon: "❔" };
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
function LibraryMarketplace({ onImported }) {
|
|
2443
|
+
const [sources, setSources] = useState([]);
|
|
2444
|
+
const [loading, setLoading] = useState(true);
|
|
2445
|
+
const [searchText, setSearchText] = useState("");
|
|
2446
|
+
const [activeCategory, setActiveCategory] = useState("all");
|
|
2447
|
+
const [expandedSource, setExpandedSource] = useState(null);
|
|
2448
|
+
const [previewData, setPreviewData] = useState(null);
|
|
2449
|
+
const [previewSourceId, setPreviewSourceId] = useState(null);
|
|
2450
|
+
const [scanning, setScanning] = useState(null);
|
|
2451
|
+
const [importing, setImporting] = useState(null);
|
|
2452
|
+
const [importedSources, setImportedSources] = useState(new Set());
|
|
2453
|
+
const [customUrl, setCustomUrl] = useState("");
|
|
2454
|
+
const [customBranch, setCustomBranch] = useState("main");
|
|
2455
|
+
const [showCustom, setShowCustom] = useState(false);
|
|
2456
|
+
|
|
2457
|
+
useEffect(() => {
|
|
2458
|
+
let alive = true;
|
|
2459
|
+
setLoading(true);
|
|
2460
|
+
fetchLibrarySources()
|
|
2461
|
+
.then((data) => {
|
|
2462
|
+
if (!alive) return;
|
|
2463
|
+
setSources(Array.isArray(data) ? data : []);
|
|
2464
|
+
})
|
|
2465
|
+
.catch(() => {})
|
|
2466
|
+
.finally(() => setLoading(false));
|
|
2467
|
+
return () => { alive = false; };
|
|
2468
|
+
}, []);
|
|
2469
|
+
|
|
2470
|
+
const filteredSources = useMemo(() => {
|
|
2471
|
+
let list = [...sources];
|
|
2472
|
+
if (searchText.trim()) {
|
|
2473
|
+
const q = searchText.trim().toLowerCase();
|
|
2474
|
+
list = list.filter((s) => {
|
|
2475
|
+
const haystack = `${s.name || ""} ${s.description || ""} ${(s.focuses || []).join(" ")} ${s.id || ""}`.toLowerCase();
|
|
2476
|
+
return haystack.includes(q);
|
|
2477
|
+
});
|
|
2478
|
+
}
|
|
2479
|
+
if (activeCategory !== "all") {
|
|
2480
|
+
if (activeCategory === "official") {
|
|
2481
|
+
list = list.filter((s) => s.trustTier === "official" || s.trustTier === "partner");
|
|
2482
|
+
} else if (activeCategory === "community") {
|
|
2483
|
+
list = list.filter((s) => s.trustTier === "community" || !s.trustTier);
|
|
2484
|
+
} else if (activeCategory === "agents" || activeCategory === "skills" || activeCategory === "mcp") {
|
|
2485
|
+
list = list.filter((s) => {
|
|
2486
|
+
const focuses = (s.focuses || []).map((f) => f.toLowerCase());
|
|
2487
|
+
return focuses.includes(activeCategory) || String(s.description || "").toLowerCase().includes(activeCategory);
|
|
2488
|
+
});
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
return list.sort((a, b) => (Number(b.estimatedPlugins || 0) - Number(a.estimatedPlugins || 0)) || String(a.name || "").localeCompare(String(b.name || "")));
|
|
2492
|
+
}, [sources, searchText, activeCategory]);
|
|
2493
|
+
|
|
2494
|
+
const doPreviewSource = useCallback(async (sourceId) => {
|
|
2495
|
+
setScanning(sourceId);
|
|
2496
|
+
try {
|
|
2497
|
+
const res = await previewLibrarySource({ sourceId });
|
|
2498
|
+
if (!res?.ok) throw new Error(res?.error || "Preview failed");
|
|
2499
|
+
setPreviewData(res?.data);
|
|
2500
|
+
setPreviewSourceId(sourceId);
|
|
2501
|
+
} catch (err) {
|
|
2502
|
+
showToast(`Preview failed: ${parseApiError(err)}`, "error");
|
|
2503
|
+
}
|
|
2504
|
+
setScanning(null);
|
|
2505
|
+
}, []);
|
|
2506
|
+
|
|
2507
|
+
const doImportSource = useCallback(async (sourceId, selectedPaths) => {
|
|
2508
|
+
setImporting(sourceId);
|
|
2509
|
+
try {
|
|
2510
|
+
const payload = {
|
|
2511
|
+
sourceId,
|
|
2512
|
+
importAgents: true,
|
|
2513
|
+
importSkills: true,
|
|
2514
|
+
importPrompts: true,
|
|
2515
|
+
importTools: true,
|
|
2516
|
+
includeEntries: selectedPaths,
|
|
2517
|
+
};
|
|
2518
|
+
const res = await importLibrarySource(payload);
|
|
2519
|
+
if (!res?.ok) throw new Error(res?.error || "Import failed");
|
|
2520
|
+
const count = Number(res?.data?.importedCount || 0);
|
|
2521
|
+
const byType = res?.data?.importedByType || {};
|
|
2522
|
+
const details = [
|
|
2523
|
+
`agents ${Number(byType?.agent || 0)}`,
|
|
2524
|
+
`prompts ${Number(byType?.prompt || 0)}`,
|
|
2525
|
+
`skills ${Number(byType?.skill || 0)}`,
|
|
2526
|
+
`tools ${Number(byType?.mcp || 0)}`,
|
|
2527
|
+
].join(", ");
|
|
2528
|
+
showToast(`Imported ${count} entries (${details})`, "success");
|
|
2529
|
+
setImportedSources((prev) => new Set([...prev, sourceId]));
|
|
2530
|
+
setPreviewData(null);
|
|
2531
|
+
setPreviewSourceId(null);
|
|
2532
|
+
if (typeof onImported === "function") onImported();
|
|
2533
|
+
} catch (err) {
|
|
2534
|
+
showToast(`Import failed: ${parseApiError(err)}`, "error");
|
|
2535
|
+
}
|
|
2536
|
+
setImporting(null);
|
|
2537
|
+
}, [onImported]);
|
|
2538
|
+
|
|
2539
|
+
const doImportAll = useCallback(async (sourceId) => {
|
|
2540
|
+
setImporting(sourceId);
|
|
2541
|
+
try {
|
|
2542
|
+
const res = await importLibrarySource({
|
|
2543
|
+
sourceId,
|
|
2544
|
+
importAgents: true,
|
|
2545
|
+
importSkills: true,
|
|
2546
|
+
importPrompts: true,
|
|
2547
|
+
importTools: true,
|
|
2548
|
+
});
|
|
2549
|
+
if (!res?.ok) throw new Error(res?.error || "Import failed");
|
|
2550
|
+
const count = Number(res?.data?.importedCount || 0);
|
|
2551
|
+
showToast(`Imported ${count} entries from ${sourceId}`, "success");
|
|
2552
|
+
setImportedSources((prev) => new Set([...prev, sourceId]));
|
|
2553
|
+
if (typeof onImported === "function") onImported();
|
|
2554
|
+
} catch (err) {
|
|
2555
|
+
showToast(`Import failed: ${parseApiError(err)}`, "error");
|
|
2556
|
+
}
|
|
2557
|
+
setImporting(null);
|
|
2558
|
+
}, [onImported]);
|
|
2559
|
+
|
|
2560
|
+
const doCustomImport = useCallback(async () => {
|
|
2561
|
+
if (!customUrl.trim()) return;
|
|
2562
|
+
setScanning("custom");
|
|
2563
|
+
try {
|
|
2564
|
+
const res = await previewLibrarySource({ repoUrl: customUrl.trim(), branch: customBranch.trim() || "main" });
|
|
2565
|
+
if (!res?.ok) throw new Error(res?.error || "Preview failed");
|
|
2566
|
+
setPreviewData(res?.data);
|
|
2567
|
+
setPreviewSourceId("custom");
|
|
2568
|
+
} catch (err) {
|
|
2569
|
+
showToast(`Preview failed: ${parseApiError(err)}`, "error");
|
|
2570
|
+
}
|
|
2571
|
+
setScanning(null);
|
|
2572
|
+
}, [customUrl, customBranch]);
|
|
2573
|
+
|
|
2574
|
+
return html`
|
|
2575
|
+
<div style="margin-top:10px;padding:12px;border:1px solid var(--border,#333);border-radius:10px;">
|
|
2576
|
+
<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
|
+
<//>
|
|
2582
|
+
</div>
|
|
2583
|
+
|
|
2584
|
+
${/* ── Search Bar ── */ ""}
|
|
2585
|
+
<input type="text" value=${searchText} onInput=${(e) => setSearchText(e.currentTarget.value)}
|
|
2586
|
+
placeholder="Search sources, descriptions, focuses…"
|
|
2587
|
+
style="width:100%;padding:8px 12px;border-radius:8px;border:1px solid var(--border,#333);background:var(--bg-input,#0d1117);color:var(--text-primary,#eee);margin-bottom:8px;font-size:0.85em;" />
|
|
2588
|
+
|
|
2589
|
+
${/* ── Category Filter Pills ── */ ""}
|
|
2590
|
+
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px;">
|
|
2591
|
+
${MARKETPLACE_CATEGORIES.map((cat) => html`
|
|
2592
|
+
<${Chip} key=${cat.id} label=${cat.label} size="small"
|
|
2593
|
+
variant=${activeCategory === cat.id ? "filled" : "outlined"}
|
|
2594
|
+
color=${activeCategory === cat.id ? "primary" : "default"}
|
|
2595
|
+
onClick=${() => setActiveCategory(cat.id)}
|
|
2596
|
+
style=${{ cursor: "pointer", fontSize: "0.78em" }} />
|
|
2597
|
+
`)}
|
|
2598
|
+
</div>
|
|
2599
|
+
|
|
2600
|
+
${/* ── Custom URL Import ── */ ""}
|
|
2601
|
+
${showCustom && html`
|
|
2602
|
+
<div style="margin-bottom:10px;padding:10px;border:1px solid var(--border,#333);border-radius:8px;background:rgba(255,255,255,0.02);">
|
|
2603
|
+
<div style="font-size:0.82em;font-weight:500;margin-bottom:6px;">Custom Repository Import</div>
|
|
2604
|
+
<div style="display:grid;grid-template-columns:1fr auto;gap:8px;align-items:end;">
|
|
2605
|
+
<input type="text" value=${customUrl} onInput=${(e) => setCustomUrl(e.currentTarget.value)}
|
|
2606
|
+
placeholder="https://github.com/org/repo.git"
|
|
2607
|
+
style="width:100%;padding:6px 10px;border-radius:6px;border:1px solid var(--border,#333);background:var(--bg-input,#0d1117);color:var(--text-primary);font-size:0.82em;" />
|
|
2608
|
+
<input type="text" value=${customBranch} onInput=${(e) => setCustomBranch(e.currentTarget.value)}
|
|
2609
|
+
placeholder="main" style="width:80px;padding:6px 10px;border-radius:6px;border:1px solid var(--border,#333);background:var(--bg-input,#0d1117);color:var(--text-primary);font-size:0.82em;" />
|
|
2610
|
+
</div>
|
|
2611
|
+
<div style="margin-top:6px;">
|
|
2612
|
+
<${Button} variant="outlined" size="small" onClick=${doCustomImport} disabled=${scanning === "custom" || !customUrl.trim()}>
|
|
2613
|
+
${scanning === "custom" ? html`<${Spinner} size=${14} /> Scanning…` : iconText(":mag: Preview")}
|
|
2614
|
+
<//>
|
|
2615
|
+
</div>
|
|
2616
|
+
</div>
|
|
2617
|
+
`}
|
|
2618
|
+
|
|
2619
|
+
${/* ── Loading State ── */ ""}
|
|
2620
|
+
${loading && html`
|
|
2621
|
+
<div style="text-align:center;padding:20px;">
|
|
2622
|
+
<${Spinner} size=${24} />
|
|
2623
|
+
<div style="font-size:0.82em;color:var(--text-secondary);margin-top:8px;">Loading marketplace sources…</div>
|
|
2624
|
+
</div>
|
|
2625
|
+
`}
|
|
2626
|
+
|
|
2627
|
+
${/* ── Source Cards Grid ── */ ""}
|
|
2628
|
+
${!loading && html`
|
|
2629
|
+
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:10px;">
|
|
2630
|
+
${filteredSources.map((source) => {
|
|
2631
|
+
const trust = getTrustTier(source);
|
|
2632
|
+
const trustScore = Number(source?.trust?.score || 0);
|
|
2633
|
+
const est = Number(source.estimatedPlugins || 0);
|
|
2634
|
+
const isImported = importedSources.has(source.id);
|
|
2635
|
+
const isScanning = scanning === source.id;
|
|
2636
|
+
const isImporting = importing === source.id;
|
|
2637
|
+
const isExpanded = expandedSource === source.id;
|
|
2638
|
+
const focuses = (source.focuses || []).slice(0, 4);
|
|
2639
|
+
|
|
2640
|
+
return html`
|
|
2641
|
+
<div key=${source.id} style="border:1px solid var(--border,#333);border-radius:10px;overflow:hidden;background:var(--bg-card,rgba(255,255,255,0.03));transition:border-color 0.2s;${isImported ? "border-color:#22c55e;" : ""}">
|
|
2642
|
+
<div style="padding:10px 12px;">
|
|
2643
|
+
${/* ── Card Header ── */ ""}
|
|
2644
|
+
<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-bottom:6px;">
|
|
2645
|
+
<span style="font-size:1em;">${trust.icon}</span>
|
|
2646
|
+
<span style="font-size:0.88em;font-weight:600;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${source.name}</span>
|
|
2647
|
+
<span style="font-size:0.68em;padding:2px 8px;border-radius:999px;background:${trust.color}22;color:${trust.color};font-weight:500;">${trust.label}</span>
|
|
2648
|
+
${isImported && html`<span style="font-size:0.68em;padding:2px 8px;border-radius:999px;background:rgba(34,197,94,0.15);color:#22c55e;">✓ Imported</span>`}
|
|
2649
|
+
</div>
|
|
2650
|
+
|
|
2651
|
+
${/* ── Stats and description ── */ ""}
|
|
2652
|
+
<div style="font-size:0.78em;color:var(--text-secondary);margin-bottom:4px;">
|
|
2653
|
+
${est > 0 ? `~${est} plugins` : ""}${est > 0 && focuses.length > 0 ? " · " : ""}${focuses.join(", ")}
|
|
2654
|
+
</div>
|
|
2655
|
+
${source.description && html`
|
|
2656
|
+
<div style="font-size:0.78em;color:var(--text-secondary);margin-bottom:6px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">${source.description}</div>
|
|
2657
|
+
`}
|
|
2658
|
+
|
|
2659
|
+
${/* ── Trust Score Bar ── */ ""}
|
|
2660
|
+
<div style="display:flex;align-items:center;gap:6px;margin-bottom:8px;">
|
|
2661
|
+
<span style="font-size:0.72em;color:var(--text-secondary);min-width:55px;">Trust ${trustScore}/100</span>
|
|
2662
|
+
<div style="flex:1;height:4px;border-radius:2px;background:rgba(255,255,255,0.08);overflow:hidden;">
|
|
2663
|
+
<div style="height:100%;width:${trustScore}%;border-radius:2px;background:${trustScore >= 70 ? "#22c55e" : trustScore >= 40 ? "#f59e0b" : "#ef4444"};"></div>
|
|
2664
|
+
</div>
|
|
2665
|
+
</div>
|
|
2666
|
+
|
|
2667
|
+
${/* ── Action Buttons ── */ ""}
|
|
2668
|
+
<div style="display:flex;gap:6px;flex-wrap:wrap;">
|
|
2669
|
+
<${Button} variant="outlined" size="small" onClick=${() => doPreviewSource(source.id)} disabled=${isScanning || isImporting || source.enabled === false}
|
|
2670
|
+
style=${{ fontSize: "0.72em", padding: "3px 10px", textTransform: "none" }}>
|
|
2671
|
+
${isScanning ? html`<${Spinner} size=${12} />` : "Preview"}
|
|
2672
|
+
<//>
|
|
2673
|
+
<${Button} variant="outlined" size="small" onClick=${() => doImportAll(source.id)} disabled=${isScanning || isImporting || source.enabled === false}
|
|
2674
|
+
style=${{ fontSize: "0.72em", padding: "3px 10px", textTransform: "none" }}>
|
|
2675
|
+
${isImporting ? html`<${Spinner} size=${12} />` : "Import All"}
|
|
2676
|
+
<//>
|
|
2677
|
+
</div>
|
|
2678
|
+
|
|
2679
|
+
${/* ── Low trust warning ── */ ""}
|
|
2680
|
+
${source?.trust?.lowTrust && source.enabled !== false ? html`
|
|
2681
|
+
<div style="font-size:0.72em;color:#f59e0b;margin-top:6px;padding:3px 6px;border-radius:4px;background:rgba(245,158,11,0.08);">⚠ Low trust — review before use</div>
|
|
2682
|
+
` : null}
|
|
2683
|
+
${source.enabled === false ? html`
|
|
2684
|
+
<div style="font-size:0.72em;color:#ef4444;margin-top:6px;padding:3px 6px;border-radius:4px;background:rgba(239,68,68,0.08);">Unavailable</div>
|
|
2685
|
+
` : null}
|
|
2686
|
+
</div>
|
|
2687
|
+
</div>
|
|
2688
|
+
`;
|
|
2689
|
+
})}
|
|
2690
|
+
</div>
|
|
2691
|
+
${filteredSources.length === 0 && html`
|
|
2692
|
+
<div style="text-align:center;padding:20px;color:var(--text-secondary);font-size:0.85em;">
|
|
2693
|
+
No sources match your search.
|
|
2694
|
+
</div>
|
|
2695
|
+
`}
|
|
2696
|
+
`}
|
|
2697
|
+
|
|
2698
|
+
${/* ── Preview Modal ── */ ""}
|
|
2699
|
+
${previewData ? html`
|
|
2700
|
+
<${ImportPreviewModal}
|
|
2701
|
+
candidates=${previewData.candidates}
|
|
2702
|
+
source=${previewData.source}
|
|
2703
|
+
duplicates=${previewData.duplicates}
|
|
2704
|
+
intraDuplicates=${previewData.intraDuplicates}
|
|
2705
|
+
onConfirm=${(selected) => doImportSource(previewSourceId, selected)}
|
|
2706
|
+
onClose=${() => { setPreviewData(null); setPreviewSourceId(null); }}
|
|
2707
|
+
loading=${importing != null}
|
|
2708
|
+
/>
|
|
2709
|
+
` : null}
|
|
2710
|
+
</div>
|
|
2711
|
+
`;
|
|
2712
|
+
}
|
|
2251
2713
|
|
|
2252
2714
|
export function LibraryTab() {
|
|
2253
2715
|
injectStyles();
|
|
@@ -2401,7 +2863,7 @@ export function LibraryTab() {
|
|
|
2401
2863
|
</div>
|
|
2402
2864
|
|
|
2403
2865
|
${filterType.value !== "mcp" && html`<${ProfileMatcher} />`}
|
|
2404
|
-
${filterType.value !== "mcp" && html`<${
|
|
2866
|
+
${filterType.value !== "mcp" && html`<${LibraryMarketplace} onImported=${loadEntries} />`}
|
|
2405
2867
|
${filterType.value !== "mcp" && html`<${ScopeDetector} />`}
|
|
2406
2868
|
|
|
2407
2869
|
${/* ── MCP Marketplace View ── */
|