From 17a04d06d233b0c07d4e42cf3755428cacac7f13 Mon Sep 17 00:00:00 2001 From: Jude Wang Date: Tue, 7 Apr 2026 02:23:58 +0800 Subject: [PATCH] Improve workspace skill filtering --- app/skills/page.tsx | 217 +++++++++++++++++++++++++++-------------- lib/i18n.tsx | 12 +++ lib/openclaw-skills.ts | 2 +- 3 files changed, 159 insertions(+), 72 deletions(-) diff --git a/app/skills/page.tsx b/app/skills/page.tsx index 7371eb0..f689d48 100644 --- a/app/skills/page.tsx +++ b/app/skills/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { useI18n } from "@/lib/i18n"; @@ -18,6 +18,8 @@ interface AgentInfo { emoji: string; } +type SkillFilter = "all" | "builtin" | "extension" | "custom" | "workspace"; + function normalizeSkill(raw: unknown): Skill | null { if (!raw || typeof raw !== "object") return null; const value = raw as Record; @@ -59,13 +61,19 @@ function normalizeAgents(raw: unknown): Record { return Object.fromEntries(entries); } +function getWorkspaceIdFromSource(source: string): string | null { + if (!source.startsWith("workspace:")) return null; + return source.slice("workspace:".length) || null; +} + export default function SkillsPage() { const { t } = useI18n(); const [skills, setSkills] = useState([]); const [agents, setAgents] = useState>({}); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); - const [filter, setFilter] = useState<"all" | "builtin" | "extension" | "custom">("all"); + const [filter, setFilter] = useState("all"); + const [workspaceFilter, setWorkspaceFilter] = useState("all"); const [search, setSearch] = useState(""); const [selectedSkill, setSelectedSkill] = useState(null); const [skillContent, setSkillContent] = useState>({}); @@ -170,30 +178,63 @@ export default function SkillsPage() { return () => window.removeEventListener("keydown", onKeyDown); }, [selectedSkill]); + const workspaceOptions = useMemo(() => { + const ids = Array.from( + new Set( + skills + .map((skill) => getWorkspaceIdFromSource(skill.source)) + .filter((workspaceId): workspaceId is string => Boolean(workspaceId)) + ) + ).sort((a, b) => a.localeCompare(b)); + + return ids.map((id) => ({ + id, + label: agents[id]?.name || (id === "main" ? t("skills.workspace.main") : id), + emoji: agents[id]?.emoji || (id === "main" ? "🏠" : "📁"), + })); + }, [agents, skills, t]); + const filtered = skills.filter((skill) => { if (filter === "builtin" && skill.source !== "builtin") return false; if (filter === "extension" && !skill.source.startsWith("extension:")) return false; if (filter === "custom" && skill.source !== "custom") return false; + if (filter === "workspace" && !skill.source.startsWith("workspace:")) return false; + + if (workspaceFilter !== "all") { + const workspaceId = getWorkspaceIdFromSource(skill.source); + if (workspaceId !== workspaceFilter) return false; + } if (!search) return true; const query = search.toLowerCase(); + const workspaceId = getWorkspaceIdFromSource(skill.source) || ""; + const workspaceName = workspaceId ? (agents[workspaceId]?.name || workspaceId).toLowerCase() : ""; return ( skill.name.toLowerCase().includes(query) || skill.description.toLowerCase().includes(query) || - skill.id.toLowerCase().includes(query) + skill.id.toLowerCase().includes(query) || + workspaceName.includes(query) || + workspaceId.toLowerCase().includes(query) ); }); const sourceLabel = (source: string) => { if (source === "builtin") return t("skills.source.builtin"); if (source.startsWith("extension:")) return source.replace("extension:", `${t("skills.extension")}:`); + if (source.startsWith("workspace:")) { + const workspaceId = getWorkspaceIdFromSource(source); + if (!workspaceId) return t("skills.source.workspace"); + if (workspaceId === "main") return `${t("skills.source.workspace")}: ${t("skills.workspace.main")}`; + return `${t("skills.source.workspace")}: ${agents[workspaceId]?.name || workspaceId}`; + } return t("skills.source.custom"); }; const sourceBadgeClass = (source: string) => { if (source === "builtin") return "bg-blue-500/20 text-blue-400"; if (source.startsWith("extension:")) return "bg-purple-500/20 text-purple-400"; + if (source.startsWith("workspace:")) return "bg-amber-500/20 text-amber-300"; return "bg-green-500/20 text-green-400"; }; @@ -219,6 +260,7 @@ export default function SkillsPage() { const builtinCount = skills.filter((skill) => skill.source === "builtin").length; const extensionCount = skills.filter((skill) => skill.source.startsWith("extension:")).length; const customCount = skills.filter((skill) => skill.source === "custom").length; + const workspaceCount = skills.filter((skill) => skill.source.startsWith("workspace:")).length; return (
@@ -226,7 +268,7 @@ export default function SkillsPage() {

{t("skills.title")}

- 共 {skills.length} {t("skills.count")}({t("skills.builtin")} {builtinCount} / {t("skills.extension")} {extensionCount} / {t("skills.custom")} {customCount}) + 共 {skills.length} {t("skills.count")}({t("skills.builtin")} {builtinCount} / {t("skills.extension")} {extensionCount} / {t("skills.custom")} {customCount} / {t("skills.workspace")} {workspaceCount})

@@ -239,38 +281,58 @@ export default function SkillsPage() {
-
-
- {(["all", "builtin", "extension", "custom"] as const).map((nextFilter) => ( - + ))} +
+ + {workspaceOptions.length > 0 && ( + + )} + + setSearch(event.target.value)} + className="px-3 py-1.5 rounded-lg border border-[var(--border)] bg-[var(--card)] text-sm outline-none focus:border-[var(--accent)] transition w-full md:w-64" + /> + + {t("skills.showing")} {filtered.length} {t("skills.unit")} +
- setSearch(event.target.value)} - className="px-3 py-1.5 rounded-lg border border-[var(--border)] bg-[var(--card)] text-sm outline-none focus:border-[var(--accent)] transition w-full md:w-64" - /> - - {t("skills.showing")} {filtered.length} {t("skills.unit")} -
@@ -279,45 +341,57 @@ export default function SkillsPage() { {t("common.noData")}
) : ( - filtered.map((skill) => ( - - )) + {skill.usedBy.length > 0 && ( +
+ {skill.usedBy.map((agentId) => { + const agent = agents[agentId]; + return ( + + {agent?.emoji || "🤖"} {agent?.name || agentId} + + ); + })} +
+ )} + + ); + }) )} @@ -338,6 +412,7 @@ export default function SkillsPage() {
{selectedSkill.emoji} {selectedSkill.name}
{t("skills.contentTitle")}
+
{sourceLabel(selectedSkill.source)}