Merge pull request #7132 from vincentjiang777/feat/colony-session-transfer
feat: redesign configuration UI for sidebar, prompts, skills, and tools
This commit is contained in:
@@ -210,18 +210,18 @@ def _catalog_from_live_session(session: Any) -> dict[str, list[dict[str, Any]]]:
|
||||
return {}
|
||||
mcp_names = getattr(phase_state, "mcp_tool_names_all", set()) or set()
|
||||
independent_tools = getattr(phase_state, "independent_tools", []) or []
|
||||
result: dict[str, list[dict[str, Any]]] = {"(unknown)": []}
|
||||
result: dict[str, list[dict[str, Any]]] = {"MCP Tools": []}
|
||||
for tool in independent_tools:
|
||||
if tool.name not in mcp_names:
|
||||
continue
|
||||
result["(unknown)"].append(
|
||||
result["MCP Tools"].append(
|
||||
{
|
||||
"name": tool.name,
|
||||
"description": tool.description,
|
||||
"input_schema": tool.parameters,
|
||||
}
|
||||
)
|
||||
return result if result["(unknown)"] else {}
|
||||
return result if result["MCP Tools"] else {}
|
||||
|
||||
server_map = getattr(registry, "_mcp_server_tools", {}) or {}
|
||||
tools_by_name = {t.name: t for t in registry.get_tools().values()}
|
||||
|
||||
@@ -166,12 +166,19 @@ export default function Sidebar() {
|
||||
<Network className="w-4 h-4" />
|
||||
<span>Org Chart</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigate("/credentials")}
|
||||
className="flex items-center gap-2.5 px-3 py-1.5 rounded-md text-sm text-foreground/70 hover:bg-sidebar-item-hover hover:text-foreground transition-colors"
|
||||
>
|
||||
<KeyRound className="w-4 h-4" />
|
||||
<span>Credentials</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setLibraryExpanded((v) => !v)}
|
||||
className="flex items-center gap-2.5 px-3 py-1.5 rounded-md text-sm text-foreground/70 hover:bg-sidebar-item-hover hover:text-foreground transition-colors"
|
||||
>
|
||||
<Library className="w-4 h-4" />
|
||||
<span className="flex-1 text-left">Library</span>
|
||||
<span className="flex-1 text-left">Configuration</span>
|
||||
<ChevronDown
|
||||
className={`w-3.5 h-3.5 transition-transform ${
|
||||
libraryExpanded ? "" : "-rotate-90"
|
||||
@@ -180,18 +187,18 @@ export default function Sidebar() {
|
||||
</button>
|
||||
{libraryExpanded && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => navigate("/skills-library")}
|
||||
className="flex items-center gap-2.5 pl-9 pr-3 py-1.5 rounded-md text-sm text-foreground/70 hover:bg-sidebar-item-hover hover:text-foreground transition-colors"
|
||||
>
|
||||
<span>Skills</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigate("/prompt-library")}
|
||||
className="flex items-center gap-2.5 pl-9 pr-3 py-1.5 rounded-md text-sm text-foreground/70 hover:bg-sidebar-item-hover hover:text-foreground transition-colors"
|
||||
>
|
||||
<span>Prompts</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigate("/skills-library")}
|
||||
className="flex items-center gap-2.5 pl-9 pr-3 py-1.5 rounded-md text-sm text-foreground/70 hover:bg-sidebar-item-hover hover:text-foreground transition-colors"
|
||||
>
|
||||
<span>Skills</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigate("/tool-library")}
|
||||
className="flex items-center gap-2.5 pl-9 pr-3 py-1.5 rounded-md text-sm text-foreground/70 hover:bg-sidebar-item-hover hover:text-foreground transition-colors"
|
||||
@@ -200,13 +207,6 @@ export default function Sidebar() {
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
onClick={() => navigate("/credentials")}
|
||||
className="flex items-center gap-2.5 px-3 py-1.5 rounded-md text-sm text-foreground/70 hover:bg-sidebar-item-hover hover:text-foreground transition-colors"
|
||||
>
|
||||
<KeyRound className="w-4 h-4" />
|
||||
<span>Credentials</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* COLONIES section */}
|
||||
|
||||
@@ -213,6 +213,12 @@ export default function ToolsEditor({
|
||||
};
|
||||
}, [subjectKey, fetchSnapshot]);
|
||||
|
||||
const allMcpNames = useMemo(() => {
|
||||
const s = new Set<string>();
|
||||
data?.mcp_servers.forEach((srv) => srv.tools.forEach((t) => s.add(t.name)));
|
||||
return s;
|
||||
}, [data]);
|
||||
|
||||
const dirty = useMemo(() => {
|
||||
const a = draftAllowed;
|
||||
const b = baselineRef.current;
|
||||
@@ -223,11 +229,27 @@ export default function ToolsEditor({
|
||||
return false;
|
||||
}, [draftAllowed]);
|
||||
|
||||
const allMcpNames = useMemo(() => {
|
||||
const s = new Set<string>();
|
||||
data?.mcp_servers.forEach((srv) => srv.tools.forEach((t) => s.add(t.name)));
|
||||
return s;
|
||||
}, [data]);
|
||||
const applyResult = (updated: string[] | null, isRoleDefault: boolean) => {
|
||||
baselineRef.current = updated === null ? null : new Set(updated);
|
||||
setDraftAllowed(updated === null ? null : new Set(updated));
|
||||
if (data) {
|
||||
const u = updated === null ? null : new Set(updated);
|
||||
setData({
|
||||
...data,
|
||||
enabled_mcp_tools: updated,
|
||||
is_role_default: isRoleDefault,
|
||||
mcp_servers: data.mcp_servers.map((srv) => ({
|
||||
...srv,
|
||||
tools: srv.tools.map((t) => ({
|
||||
...t,
|
||||
enabled: u === null ? true : u.has(t.name),
|
||||
})),
|
||||
})),
|
||||
});
|
||||
}
|
||||
setSavedRecently(true);
|
||||
setTimeout(() => setSavedRecently(false), 2500);
|
||||
};
|
||||
|
||||
const toggleOne = (name: string, next: boolean) => {
|
||||
setDraftAllowed((prev) => {
|
||||
@@ -249,41 +271,7 @@ export default function ToolsEditor({
|
||||
});
|
||||
};
|
||||
|
||||
const handleDraftAllowAll = () => setDraftAllowed(null);
|
||||
|
||||
const handleResetToRoleDefault = async () => {
|
||||
if (!resetToRoleDefault) return;
|
||||
setSaving(true);
|
||||
setSaveError(null);
|
||||
try {
|
||||
const result = await resetToRoleDefault();
|
||||
const updated = result.enabled_mcp_tools;
|
||||
baselineRef.current = updated === null ? null : new Set(updated);
|
||||
setDraftAllowed(updated === null ? null : new Set(updated));
|
||||
if (data) {
|
||||
const u = updated === null ? null : new Set(updated);
|
||||
setData({
|
||||
...data,
|
||||
enabled_mcp_tools: updated,
|
||||
is_role_default: true,
|
||||
mcp_servers: data.mcp_servers.map((srv) => ({
|
||||
...srv,
|
||||
tools: srv.tools.map((t) => ({
|
||||
...t,
|
||||
enabled: u === null ? true : u.has(t.name),
|
||||
})),
|
||||
})),
|
||||
});
|
||||
}
|
||||
setSavedRecently(true);
|
||||
setTimeout(() => setSavedRecently(false), 2500);
|
||||
} catch (e: unknown) {
|
||||
const err = e as { body?: { error?: string } };
|
||||
setSaveError(err.body?.error || "Reset failed");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
const handleAllowAll = () => setDraftAllowed(null);
|
||||
|
||||
const handleCancel = () => {
|
||||
const baseline = baselineRef.current;
|
||||
@@ -295,29 +283,17 @@ export default function ToolsEditor({
|
||||
setSaving(true);
|
||||
setSaveError(null);
|
||||
try {
|
||||
// Only send tool names the server knows about (MCP tools).
|
||||
// The draft may contain lifecycle/synthetic names from the
|
||||
// baseline — strip those to avoid "Unknown MCP tool name" errors.
|
||||
const payload =
|
||||
draftAllowed === null ? null : Array.from(draftAllowed).sort();
|
||||
draftAllowed === null
|
||||
? null
|
||||
: Array.from(draftAllowed)
|
||||
.filter((name) => allMcpNames.has(name))
|
||||
.sort();
|
||||
const result = await saveAllowlist(payload);
|
||||
const updated = result.enabled_mcp_tools;
|
||||
baselineRef.current = updated === null ? null : new Set(updated);
|
||||
setDraftAllowed(updated === null ? null : new Set(updated));
|
||||
if (data) {
|
||||
const u = updated === null ? null : new Set(updated);
|
||||
setData({
|
||||
...data,
|
||||
enabled_mcp_tools: updated,
|
||||
is_role_default: false,
|
||||
mcp_servers: data.mcp_servers.map((srv) => ({
|
||||
...srv,
|
||||
tools: srv.tools.map((t) => ({
|
||||
...t,
|
||||
enabled: u === null ? true : u.has(t.name),
|
||||
})),
|
||||
})),
|
||||
});
|
||||
}
|
||||
setSavedRecently(true);
|
||||
setTimeout(() => setSavedRecently(false), 2500);
|
||||
applyResult(result.enabled_mcp_tools, false);
|
||||
} catch (e: unknown) {
|
||||
const err = e as { body?: { error?: string; unknown?: string[] } };
|
||||
const extra = err.body?.unknown
|
||||
@@ -329,6 +305,21 @@ export default function ToolsEditor({
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetToRoleDefault = async () => {
|
||||
if (!resetToRoleDefault) return;
|
||||
setSaving(true);
|
||||
setSaveError(null);
|
||||
try {
|
||||
const result = await resetToRoleDefault();
|
||||
applyResult(result.enabled_mcp_tools, true);
|
||||
} catch (e: unknown) {
|
||||
const err = e as { body?: { error?: string } };
|
||||
setSaveError(err.body?.error || "Reset failed");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground py-3">
|
||||
@@ -423,7 +414,7 @@ export default function ToolsEditor({
|
||||
return (
|
||||
<CollapsibleGroup
|
||||
key={srv.name}
|
||||
title={srv.name}
|
||||
title={srv.name === "(unknown)" ? "MCP Tools" : srv.name}
|
||||
count={srv.tools.length}
|
||||
badge={`${enabledInServer}/${srv.tools.length}`}
|
||||
expanded={!!expanded[srv.name]}
|
||||
@@ -457,65 +448,50 @@ export default function ToolsEditor({
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="flex items-center gap-2 pt-3">
|
||||
<div className="flex items-center gap-2 pt-3 flex-wrap">
|
||||
{/* Primary actions */}
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={!dirty || saving}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md bg-primary text-primary-foreground text-xs font-medium hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{saving ? (
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
) : (
|
||||
<Check className="w-3 h-3" />
|
||||
)}
|
||||
{saving ? <Loader2 className="w-3 h-3 animate-spin" /> : <Check className="w-3 h-3" />}
|
||||
{saving ? "Saving…" : "Save"}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCancel}
|
||||
disabled={!dirty || saving}
|
||||
className="px-3 py-1.5 rounded-md text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted/30 disabled:opacity-50"
|
||||
className="px-3 py-1.5 rounded-md border border-border/60 text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted/30 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
{/* Status */}
|
||||
{savedRecently && !dirty && (
|
||||
<span className="text-[11px] text-green-500 flex items-center gap-1">
|
||||
<Check className="w-3 h-3" /> Saved
|
||||
</span>
|
||||
)}
|
||||
<div className="ml-auto flex items-center gap-3">
|
||||
{data.is_role_default !== undefined && (
|
||||
<span
|
||||
className={`text-[10px] uppercase tracking-wider px-1.5 py-0.5 rounded ${
|
||||
data.is_role_default
|
||||
? "bg-muted/40 text-muted-foreground"
|
||||
: "bg-primary/15 text-primary"
|
||||
}`}
|
||||
title={
|
||||
data.is_role_default
|
||||
? "Using the default allowlist for this role."
|
||||
: "Custom allowlist saved by you."
|
||||
}
|
||||
>
|
||||
{data.is_role_default ? "Role default" : "Custom"}
|
||||
</span>
|
||||
)}
|
||||
{resetToRoleDefault && !data.is_role_default && (
|
||||
{dirty && !saving && (
|
||||
<span className="text-[11px] text-amber-500">Unsaved changes</span>
|
||||
)}
|
||||
|
||||
{/* Quick actions */}
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleAllowAll}
|
||||
disabled={saving || draftAllowed === null}
|
||||
className="px-3 py-1.5 rounded-md border border-border/60 text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted/30 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Allow all
|
||||
</button>
|
||||
{resetToRoleDefault && (
|
||||
<button
|
||||
onClick={handleResetToRoleDefault}
|
||||
disabled={saving}
|
||||
className="text-[11px] text-muted-foreground hover:text-foreground underline underline-offset-2 disabled:opacity-50"
|
||||
disabled={saving || !!data.is_role_default}
|
||||
className="px-3 py-1.5 rounded-md border border-border/60 text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted/30 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Reset to role default
|
||||
</button>
|
||||
)}
|
||||
{draftAllowed !== null && (
|
||||
<button
|
||||
onClick={handleDraftAllowAll}
|
||||
disabled={saving}
|
||||
className="text-[11px] text-muted-foreground hover:text-foreground underline underline-offset-2 disabled:opacity-50"
|
||||
title="Draft 'allow all' — click Save to persist."
|
||||
>
|
||||
Allow all
|
||||
Reset to defaults
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -140,13 +140,26 @@ export default function PromptLibrary() {
|
||||
promptsApi.list().then((r) => setCustomPrompts(r.prompts)).catch(() => {});
|
||||
}, []);
|
||||
|
||||
// Merge built-in + custom prompts
|
||||
const allPrompts = useMemo(() => [...customPrompts, ...prompts], [customPrompts]);
|
||||
// Filtered custom (my) prompts
|
||||
const filteredCustom = useMemo(() => {
|
||||
let result: (Prompt | CustomPrompt)[] = customPrompts;
|
||||
if (selectedCategory && selectedCategory !== "custom") {
|
||||
result = result.filter((p) => p.category === selectedCategory);
|
||||
}
|
||||
if (searchQuery.trim()) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
result = result.filter(
|
||||
(p) => p.title.toLowerCase().includes(query) || p.content.toLowerCase().includes(query),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}, [customPrompts, searchQuery, selectedCategory]);
|
||||
|
||||
const filteredPrompts = useMemo(() => {
|
||||
let result = allPrompts;
|
||||
// Filtered built-in (community) prompts
|
||||
const filteredBuiltIn = useMemo(() => {
|
||||
let result: Prompt[] = prompts;
|
||||
if (selectedCategory === "custom") {
|
||||
result = result.filter((p) => "custom" in p && p.custom);
|
||||
result = [];
|
||||
} else if (selectedCategory) {
|
||||
result = result.filter((p) => p.category === selectedCategory);
|
||||
}
|
||||
@@ -157,13 +170,13 @@ export default function PromptLibrary() {
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}, [allPrompts, searchQuery, selectedCategory]);
|
||||
}, [searchQuery, selectedCategory]);
|
||||
|
||||
// Reset page when filters change
|
||||
useEffect(() => setPage(0), [searchQuery, selectedCategory]);
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(filteredPrompts.length / PAGE_SIZE));
|
||||
const pagedPrompts = filteredPrompts.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
|
||||
const totalPages = Math.max(1, Math.ceil(filteredBuiltIn.length / PAGE_SIZE));
|
||||
const pagedBuiltIn = filteredBuiltIn.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
|
||||
|
||||
const handleUsePrompt = (content: string, category: string) => {
|
||||
const queenId = categoryToQueen[category];
|
||||
@@ -196,7 +209,7 @@ export default function PromptLibrary() {
|
||||
Prompt Library
|
||||
</h2>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{allPrompts.length} prompts across {promptCategories.length + (customCount > 0 ? 1 : 0)} categories
|
||||
{customCount > 0 ? `${customCount} custom · ` : ""}{prompts.length} community prompts
|
||||
</span>
|
||||
</div>
|
||||
<button onClick={() => setAddModalOpen(true)}
|
||||
@@ -240,23 +253,47 @@ export default function PromptLibrary() {
|
||||
|
||||
{/* Prompts grid */}
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
{pagedPrompts.length > 0 ? (
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{pagedPrompts.map((prompt) => (
|
||||
<PromptCard
|
||||
key={typeof prompt.id === "string" ? prompt.id : `builtin-${prompt.id}`}
|
||||
prompt={prompt}
|
||||
onUse={handleUsePrompt}
|
||||
onDelete={"custom" in prompt && prompt.custom ? () => handleDeletePrompt(prompt.id as string) : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
{filteredCustom.length === 0 && pagedBuiltIn.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center">
|
||||
<Sparkles className="w-10 h-10 text-muted-foreground/30 mb-3" />
|
||||
<p className="text-sm text-muted-foreground">No prompts found</p>
|
||||
<p className="text-xs text-muted-foreground/60 mt-1">Try adjusting your search or category filter</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* My Prompts section */}
|
||||
{filteredCustom.length > 0 && (
|
||||
<div className="mb-8">
|
||||
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">My Prompts</h3>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{filteredCustom.map((prompt) => (
|
||||
<PromptCard
|
||||
key={prompt.id as string}
|
||||
prompt={prompt}
|
||||
onUse={handleUsePrompt}
|
||||
onDelete={"custom" in prompt && prompt.custom ? () => handleDeletePrompt(prompt.id as string) : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Community Prompts section */}
|
||||
{pagedBuiltIn.length > 0 && selectedCategory !== "custom" && (
|
||||
<div>
|
||||
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Community Prompts</h3>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{pagedBuiltIn.map((prompt) => (
|
||||
<PromptCard
|
||||
key={`builtin-${prompt.id}`}
|
||||
prompt={prompt}
|
||||
onUse={handleUsePrompt}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -264,7 +301,7 @@ export default function PromptLibrary() {
|
||||
{totalPages > 1 && (
|
||||
<div className="px-6 py-3 border-t border-border/60 flex items-center justify-between">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{page * PAGE_SIZE + 1}–{Math.min((page + 1) * PAGE_SIZE, filteredPrompts.length)} of {filteredPrompts.length}
|
||||
{page * PAGE_SIZE + 1}–{Math.min((page + 1) * PAGE_SIZE, filteredBuiltIn.length)} of {filteredBuiltIn.length}
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<button onClick={() => setPage((p) => Math.max(0, p - 1))} disabled={page === 0}
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { queensApi } from "@/api/queens";
|
||||
import { coloniesApi, type ColonySummary } from "@/api/colonies";
|
||||
import { slugToDisplayName } from "@/lib/colony-registry";
|
||||
import { slugToDisplayName, sortQueenProfiles } from "@/lib/colony-registry";
|
||||
import { ApiError } from "@/api/client";
|
||||
import {
|
||||
skillsApi,
|
||||
@@ -72,7 +72,7 @@ export default function SkillsLibrary() {
|
||||
<div className="flex items-baseline gap-3 mb-3">
|
||||
<h2 className="text-lg font-semibold text-foreground flex items-center gap-2">
|
||||
<Library className="w-5 h-5 text-primary" />
|
||||
Skills Library
|
||||
Skills Configuration
|
||||
</h2>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Curate which skills each queen and colony exposes, upload your own, or browse the full catalog.
|
||||
@@ -141,8 +141,9 @@ function QueensTab() {
|
||||
queensApi
|
||||
.list()
|
||||
.then((r) => {
|
||||
setQueens(r.queens);
|
||||
if (r.queens.length > 0) setSelected((prev) => prev ?? r.queens[0].id);
|
||||
const sorted = sortQueenProfiles(r.queens);
|
||||
setQueens(sorted);
|
||||
if (sorted.length > 0) setSelected((prev) => prev ?? sorted[0].id);
|
||||
})
|
||||
.catch((e: Error) => setError(e.message || "Failed to load queens"));
|
||||
}, []);
|
||||
@@ -354,17 +355,46 @@ function SkillsPerScopeSection({
|
||||
<p className="text-sm text-muted-foreground">No skills match your filter.</p>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{filtered.map((row) => (
|
||||
<SkillCard
|
||||
key={row.name}
|
||||
row={row}
|
||||
onToggle={() => toggle(row)}
|
||||
onOpen={() => setDetailName(row.name)}
|
||||
onRemove={row.deletable ? () => remove(row) : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{(() => {
|
||||
const active = filtered.filter((r) => r.enabled);
|
||||
const inactive = filtered.filter((r) => !r.enabled);
|
||||
return (
|
||||
<>
|
||||
{active.length > 0 && (
|
||||
<div className="mb-6">
|
||||
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Active</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{active.map((row) => (
|
||||
<SkillCard
|
||||
key={row.name}
|
||||
row={row}
|
||||
onToggle={() => toggle(row)}
|
||||
onOpen={() => setDetailName(row.name)}
|
||||
onRemove={row.deletable ? () => remove(row) : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{inactive.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Inactive</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{inactive.map((row) => (
|
||||
<SkillCard
|
||||
key={row.name}
|
||||
row={row}
|
||||
onToggle={() => toggle(row)}
|
||||
onOpen={() => setDetailName(row.name)}
|
||||
onRemove={row.deletable ? () => remove(row) : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
|
||||
<CreateSkillModal
|
||||
open={createOpen}
|
||||
@@ -430,9 +460,9 @@ function CatalogTab() {
|
||||
<div className="flex-1" />
|
||||
<button
|
||||
onClick={() => setUploadOpen(true)}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md border border-border/60 bg-card text-sm font-medium text-foreground hover:bg-muted/50"
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md bg-primary/10 text-primary text-sm font-medium hover:bg-primary/20"
|
||||
>
|
||||
<Upload className="w-3.5 h-3.5" /> Upload
|
||||
<Upload className="w-3.5 h-3.5" /> Upload Skill
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from "react";
|
||||
import { Wrench, Crown, Network, Server, Loader2, AlertCircle } from "lucide-react";
|
||||
import { queensApi } from "@/api/queens";
|
||||
import { coloniesApi, type ColonySummary } from "@/api/colonies";
|
||||
import { slugToDisplayName } from "@/lib/colony-registry";
|
||||
import { slugToDisplayName, sortQueenProfiles } from "@/lib/colony-registry";
|
||||
import QueenToolsSection from "@/components/QueenToolsSection";
|
||||
import ColonyToolsSection from "@/components/ColonyToolsSection";
|
||||
import McpServersPanel from "@/components/McpServersPanel";
|
||||
@@ -19,7 +19,7 @@ export default function ToolLibrary() {
|
||||
<div className="flex items-baseline gap-3 mb-3">
|
||||
<h2 className="text-lg font-semibold text-foreground flex items-center gap-2">
|
||||
<Wrench className="w-5 h-5 text-primary" />
|
||||
Tool Library
|
||||
Tool Configuration
|
||||
</h2>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Curate which tools each queen and colony can call, and register your own MCP servers.
|
||||
@@ -88,8 +88,9 @@ function QueensTab() {
|
||||
queensApi
|
||||
.list()
|
||||
.then((r) => {
|
||||
setQueens(r.queens);
|
||||
if (r.queens.length > 0) setSelected((prev) => prev ?? r.queens[0].id);
|
||||
const sorted = sortQueenProfiles(r.queens);
|
||||
setQueens(sorted);
|
||||
if (sorted.length > 0) setSelected((prev) => prev ?? sorted[0].id);
|
||||
})
|
||||
.catch((e: Error) => setError(e.message || "Failed to load queens"));
|
||||
}, []);
|
||||
|
||||
Reference in New Issue
Block a user