remove skill categories (#9008)
Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
This commit is contained in:
@@ -1,294 +0,0 @@
|
|||||||
import type { SkillInfo } from "../api/skills";
|
|
||||||
|
|
||||||
const SKILL_CATEGORY_ORDER = [
|
|
||||||
"design",
|
|
||||||
"engineering",
|
|
||||||
"quality",
|
|
||||||
"research",
|
|
||||||
"writing",
|
|
||||||
"integrations",
|
|
||||||
"operations",
|
|
||||||
"productivity",
|
|
||||||
"general",
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export type SkillCategory = (typeof SKILL_CATEGORY_ORDER)[number];
|
|
||||||
|
|
||||||
export interface SkillViewInfo extends SkillInfo {
|
|
||||||
inferredCategory: SkillCategory;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DESIGN_SLUGS = new Set([
|
|
||||||
"adapt",
|
|
||||||
"animate",
|
|
||||||
"audit",
|
|
||||||
"bolder",
|
|
||||||
"clarify",
|
|
||||||
"colorize",
|
|
||||||
"critique",
|
|
||||||
"delight",
|
|
||||||
"distill",
|
|
||||||
"frontend-design",
|
|
||||||
"harden",
|
|
||||||
"impeccable",
|
|
||||||
"layout",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const ENGINEERING_SLUGS = new Set([
|
|
||||||
"cloning-squareup-repos",
|
|
||||||
"create-pr",
|
|
||||||
"plugin-creator",
|
|
||||||
"skill-creator",
|
|
||||||
"skill-installer",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const QUALITY_SLUGS = new Set([
|
|
||||||
"code-review",
|
|
||||||
"create-app-e2e-test",
|
|
||||||
"edge-case-finder",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const RESEARCH_SLUGS = new Set([
|
|
||||||
"agent-browser",
|
|
||||||
"codesearch",
|
|
||||||
"dev-guides",
|
|
||||||
"eng-ai-chat",
|
|
||||||
"go-link",
|
|
||||||
"openai-docs",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const WRITING_SLUGS = new Set(["ceo-weekly-update"]);
|
|
||||||
|
|
||||||
const OPERATIONS_SLUGS = new Set([
|
|
||||||
"check-ci",
|
|
||||||
"datadog",
|
|
||||||
"github:gh-address-comments",
|
|
||||||
"github:gh-fix-ci",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const INTEGRATION_SLUGS = new Set([
|
|
||||||
"excel",
|
|
||||||
"gdrive",
|
|
||||||
"github:github",
|
|
||||||
"launchdarkly",
|
|
||||||
"linear",
|
|
||||||
"powerpoint",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const PRODUCTIVITY_SLUGS = new Set(["grocery-list-organizer"]);
|
|
||||||
|
|
||||||
const CATEGORY_KEYWORDS: Record<SkillCategory, string[]> = {
|
|
||||||
design: [
|
|
||||||
"accessibility",
|
|
||||||
"animation",
|
|
||||||
"breakpoint",
|
|
||||||
"color",
|
|
||||||
"copy",
|
|
||||||
"design",
|
|
||||||
"frontend",
|
|
||||||
"interface",
|
|
||||||
"layout",
|
|
||||||
"mobile",
|
|
||||||
"motion",
|
|
||||||
"polish",
|
|
||||||
"responsive",
|
|
||||||
"spacing",
|
|
||||||
"theme",
|
|
||||||
"typography",
|
|
||||||
"ui",
|
|
||||||
"ux",
|
|
||||||
"visual",
|
|
||||||
],
|
|
||||||
engineering: [
|
|
||||||
"app",
|
|
||||||
"build",
|
|
||||||
"codebase",
|
|
||||||
"create",
|
|
||||||
"feature",
|
|
||||||
"implement",
|
|
||||||
"install",
|
|
||||||
"plugin",
|
|
||||||
"react",
|
|
||||||
"repository",
|
|
||||||
"rust",
|
|
||||||
"scaffold",
|
|
||||||
"skill",
|
|
||||||
"typescript",
|
|
||||||
],
|
|
||||||
quality: [
|
|
||||||
"bug",
|
|
||||||
"coverage",
|
|
||||||
"edge case",
|
|
||||||
"lint",
|
|
||||||
"quality",
|
|
||||||
"regression",
|
|
||||||
"review",
|
|
||||||
"test",
|
|
||||||
"verify",
|
|
||||||
],
|
|
||||||
research: [
|
|
||||||
"browse",
|
|
||||||
"discover",
|
|
||||||
"docs",
|
|
||||||
"documentation",
|
|
||||||
"find",
|
|
||||||
"guide",
|
|
||||||
"investigate",
|
|
||||||
"knowledge",
|
|
||||||
"look up",
|
|
||||||
"query",
|
|
||||||
"read",
|
|
||||||
"search",
|
|
||||||
],
|
|
||||||
writing: [
|
|
||||||
"copy",
|
|
||||||
"document",
|
|
||||||
"draft",
|
|
||||||
"edit",
|
|
||||||
"email",
|
|
||||||
"message",
|
|
||||||
"rewrite",
|
|
||||||
"summary",
|
|
||||||
"update",
|
|
||||||
"write",
|
|
||||||
],
|
|
||||||
integrations: [
|
|
||||||
"drive",
|
|
||||||
"excel",
|
|
||||||
"extension",
|
|
||||||
"github",
|
|
||||||
"google docs",
|
|
||||||
"google drive",
|
|
||||||
"google sheets",
|
|
||||||
"google slides",
|
|
||||||
"launchdarkly",
|
|
||||||
"linear",
|
|
||||||
"powerpoint",
|
|
||||||
"sheets",
|
|
||||||
"slides",
|
|
||||||
],
|
|
||||||
operations: [
|
|
||||||
"buildkite",
|
|
||||||
"canary",
|
|
||||||
"ci",
|
|
||||||
"flag",
|
|
||||||
"incident",
|
|
||||||
"kochiku",
|
|
||||||
"log",
|
|
||||||
"metric",
|
|
||||||
"monitor",
|
|
||||||
"observability",
|
|
||||||
"release",
|
|
||||||
"trace",
|
|
||||||
],
|
|
||||||
productivity: [
|
|
||||||
"grocery",
|
|
||||||
"meal plan",
|
|
||||||
"organize",
|
|
||||||
"organizer",
|
|
||||||
"shopping",
|
|
||||||
"weekly update",
|
|
||||||
],
|
|
||||||
general: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
function normalizeText(value: string) {
|
|
||||||
return value.toLowerCase().replace(/[^a-z0-9:+\s-]/g, " ");
|
|
||||||
}
|
|
||||||
|
|
||||||
function keywordScore(haystack: string, keywords: string[]) {
|
|
||||||
return keywords.reduce((score, keyword) => {
|
|
||||||
if (!haystack.includes(keyword)) {
|
|
||||||
return score;
|
|
||||||
}
|
|
||||||
|
|
||||||
return score + (keyword.includes(" ") ? 2 : 1);
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function inferCategoryFromSlug(slug: string): SkillCategory | null {
|
|
||||||
if (DESIGN_SLUGS.has(slug)) {
|
|
||||||
return "design";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (QUALITY_SLUGS.has(slug)) {
|
|
||||||
return "quality";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ENGINEERING_SLUGS.has(slug)) {
|
|
||||||
return "engineering";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (RESEARCH_SLUGS.has(slug)) {
|
|
||||||
return "research";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (WRITING_SLUGS.has(slug)) {
|
|
||||||
return "writing";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (OPERATIONS_SLUGS.has(slug)) {
|
|
||||||
return "operations";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (INTEGRATION_SLUGS.has(slug) || slug.startsWith("google-drive:")) {
|
|
||||||
return "integrations";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (PRODUCTIVITY_SLUGS.has(slug)) {
|
|
||||||
return "productivity";
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function inferSkillCategory(
|
|
||||||
skill: Pick<SkillInfo, "name" | "description" | "instructions">,
|
|
||||||
): SkillCategory {
|
|
||||||
const slug = skill.name.toLowerCase();
|
|
||||||
const explicitCategory = inferCategoryFromSlug(slug);
|
|
||||||
if (explicitCategory) {
|
|
||||||
return explicitCategory;
|
|
||||||
}
|
|
||||||
|
|
||||||
const haystack = normalizeText(
|
|
||||||
[skill.name, skill.description, skill.instructions].join(" "),
|
|
||||||
);
|
|
||||||
let bestCategory: SkillCategory = "general";
|
|
||||||
let bestScore = 0;
|
|
||||||
|
|
||||||
for (const category of SKILL_CATEGORY_ORDER) {
|
|
||||||
if (category === "general") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const score = keywordScore(haystack, CATEGORY_KEYWORDS[category]);
|
|
||||||
if (score > bestScore) {
|
|
||||||
bestCategory = category;
|
|
||||||
bestScore = score;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return bestScore > 0 ? bestCategory : "general";
|
|
||||||
}
|
|
||||||
|
|
||||||
function withInferredSkillCategory(skill: SkillInfo): SkillViewInfo {
|
|
||||||
return {
|
|
||||||
...skill,
|
|
||||||
inferredCategory: inferSkillCategory(skill),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function withInferredSkillCategories(
|
|
||||||
skills: SkillInfo[],
|
|
||||||
): SkillViewInfo[] {
|
|
||||||
return skills.map(withInferredSkillCategory);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function uniqueSkillCategories(
|
|
||||||
skills: SkillViewInfo[],
|
|
||||||
): SkillCategory[] {
|
|
||||||
return SKILL_CATEGORY_ORDER.filter((category) =>
|
|
||||||
skills.some((skill) => skill.inferredCategory === category),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
import type { SkillInfo } from "../api/skills";
|
import type { SkillInfo } from "../api/skills";
|
||||||
import type { SkillCategory, SkillViewInfo } from "./skillCategories";
|
|
||||||
|
|
||||||
export type SkillsFilter = "all" | "global" | `project:${string}`;
|
export type SkillsFilter = "all" | "global" | `project:${string}`;
|
||||||
|
|
||||||
export interface SkillsSection {
|
export interface SkillsSection {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
skills: SkillViewInfo[];
|
skills: SkillInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mirrors crates/goose/src/skills/mod.rs::validate_skill_name.
|
// Mirrors crates/goose/src/skills/mod.rs::validate_skill_name.
|
||||||
@@ -59,24 +58,19 @@ export function compareSkillsByName(a: SkillInfo, b: SkillInfo) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function filterSkills(
|
export function filterSkills(
|
||||||
skills: SkillViewInfo[],
|
skills: SkillInfo[],
|
||||||
filters: {
|
filters: {
|
||||||
search: string;
|
search: string;
|
||||||
activeFilter: SkillsFilter;
|
activeFilter: SkillsFilter;
|
||||||
selectedCategories: SkillCategory[];
|
|
||||||
},
|
},
|
||||||
getCategoryLabel: (category: SkillCategory) => string,
|
): SkillInfo[] {
|
||||||
): SkillViewInfo[] {
|
|
||||||
const searchTerm = filters.search.trim().toLowerCase();
|
const searchTerm = filters.search.trim().toLowerCase();
|
||||||
return skills.filter((skill) => {
|
return skills.filter((skill) => {
|
||||||
const matchesSearch =
|
const matchesSearch =
|
||||||
searchTerm.length === 0 ||
|
searchTerm.length === 0 ||
|
||||||
skill.name.toLowerCase().includes(searchTerm) ||
|
skill.name.toLowerCase().includes(searchTerm) ||
|
||||||
skill.description.toLowerCase().includes(searchTerm) ||
|
skill.description.toLowerCase().includes(searchTerm) ||
|
||||||
skill.sourceLabel.toLowerCase().includes(searchTerm) ||
|
skill.sourceLabel.toLowerCase().includes(searchTerm);
|
||||||
getCategoryLabel(skill.inferredCategory)
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(searchTerm);
|
|
||||||
|
|
||||||
const matchesFilter =
|
const matchesFilter =
|
||||||
filters.activeFilter === "all"
|
filters.activeFilter === "all"
|
||||||
@@ -87,16 +81,12 @@ export function filterSkills(
|
|||||||
(project) => `project:${project.id}` === filters.activeFilter,
|
(project) => `project:${project.id}` === filters.activeFilter,
|
||||||
);
|
);
|
||||||
|
|
||||||
const matchesCategory =
|
return matchesSearch && matchesFilter;
|
||||||
filters.selectedCategories.length === 0 ||
|
|
||||||
filters.selectedCategories.includes(skill.inferredCategory);
|
|
||||||
|
|
||||||
return matchesSearch && matchesFilter && matchesCategory;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function groupSkills(
|
export function groupSkills(
|
||||||
filteredSkills: SkillViewInfo[],
|
filteredSkills: SkillInfo[],
|
||||||
activeFilter: SkillsFilter,
|
activeFilter: SkillsFilter,
|
||||||
projectFilters: { id: string; name: string }[],
|
projectFilters: { id: string; name: string }[],
|
||||||
labels: { personalTitle: string; projectsFallback: string },
|
labels: { personalTitle: string; projectsFallback: string },
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
import { useCallback } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import {
|
|
||||||
IconAdjustmentsHorizontal,
|
|
||||||
IconChevronDown,
|
|
||||||
} from "@tabler/icons-react";
|
|
||||||
import { Button } from "@/shared/ui/button";
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuCheckboxItem,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/shared/ui/dropdown-menu";
|
|
||||||
import type { SkillCategory } from "../lib/skillCategories";
|
|
||||||
|
|
||||||
interface SkillCategoryFilterProps {
|
|
||||||
categories: SkillCategory[];
|
|
||||||
selectedCategories: SkillCategory[];
|
|
||||||
onSelectedCategoriesChange: (categories: SkillCategory[]) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SkillCategoryFilter({
|
|
||||||
categories,
|
|
||||||
selectedCategories,
|
|
||||||
onSelectedCategoriesChange,
|
|
||||||
}: SkillCategoryFilterProps) {
|
|
||||||
const { t } = useTranslation(["skills"]);
|
|
||||||
|
|
||||||
const toggleCategory = useCallback(
|
|
||||||
(category: SkillCategory) => {
|
|
||||||
onSelectedCategoriesChange(
|
|
||||||
selectedCategories.includes(category)
|
|
||||||
? selectedCategories.filter((value) => value !== category)
|
|
||||||
: [...selectedCategories, category],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[onSelectedCategoriesChange, selectedCategories],
|
|
||||||
);
|
|
||||||
|
|
||||||
const buttonLabel =
|
|
||||||
selectedCategories.length === 0
|
|
||||||
? t("view.categories.label")
|
|
||||||
: selectedCategories.length === 1
|
|
||||||
? t(`view.categories.options.${selectedCategories[0]}`)
|
|
||||||
: t("view.categories.count", { count: selectedCategories.length });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
size="xs"
|
|
||||||
variant={selectedCategories.length > 0 ? "default" : "outline-flat"}
|
|
||||||
leftIcon={<IconAdjustmentsHorizontal />}
|
|
||||||
rightIcon={<IconChevronDown />}
|
|
||||||
aria-label={t("view.categories.filter")}
|
|
||||||
>
|
|
||||||
{buttonLabel}
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="start" className="w-56">
|
|
||||||
<DropdownMenuLabel>{t("view.categories.label")}</DropdownMenuLabel>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
{categories.map((category) => (
|
|
||||||
<DropdownMenuCheckboxItem
|
|
||||||
key={category}
|
|
||||||
checked={selectedCategories.includes(category)}
|
|
||||||
onSelect={(event) => event.preventDefault()}
|
|
||||||
onCheckedChange={() => toggleCategory(category)}
|
|
||||||
>
|
|
||||||
{t(`view.categories.options.${category}`)}
|
|
||||||
</DropdownMenuCheckboxItem>
|
|
||||||
))}
|
|
||||||
{selectedCategories.length > 0 ? (
|
|
||||||
<>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem
|
|
||||||
onSelect={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
onSelectedCategoriesChange([]);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("view.categories.clear")}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -21,10 +21,9 @@ import { PageColumns } from "@/shared/ui/page-columns";
|
|||||||
import { DetailPageShell, PageHeader } from "@/shared/ui/page-shell";
|
import { DetailPageShell, PageHeader } from "@/shared/ui/page-shell";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
|
||||||
import type { SkillInfo } from "../api/skills";
|
import type { SkillInfo } from "../api/skills";
|
||||||
import type { SkillViewInfo } from "../lib/skillCategories";
|
|
||||||
|
|
||||||
interface SkillDetailPageProps {
|
interface SkillDetailPageProps {
|
||||||
skill: SkillViewInfo | null;
|
skill: SkillInfo | null;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
onEdit: (skill: SkillInfo) => void;
|
onEdit: (skill: SkillInfo) => void;
|
||||||
onReveal: (skill: SkillInfo) => void;
|
onReveal: (skill: SkillInfo) => void;
|
||||||
@@ -180,14 +179,6 @@ export function SkillDetailPage({
|
|||||||
sidebar={
|
sidebar={
|
||||||
<aside className="space-y-5">
|
<aside className="space-y-5">
|
||||||
<section className="space-y-5 border-b border-border pb-5">
|
<section className="space-y-5 border-b border-border pb-5">
|
||||||
<DetailField
|
|
||||||
label={t("view.category")}
|
|
||||||
contentAs="p"
|
|
||||||
contentClassName="text-foreground"
|
|
||||||
>
|
|
||||||
{t(`view.categories.options.${skill.inferredCategory}`)}
|
|
||||||
</DetailField>
|
|
||||||
|
|
||||||
<DetailField
|
<DetailField
|
||||||
label={t("view.source")}
|
label={t("view.source")}
|
||||||
contentClassName="space-y-1 text-foreground"
|
contentClassName="space-y-1 text-foreground"
|
||||||
|
|||||||
@@ -8,15 +8,15 @@ import {
|
|||||||
import { Button } from "@/shared/ui/button";
|
import { Button } from "@/shared/ui/button";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
|
||||||
import { IconMessagePlus } from "@tabler/icons-react";
|
import { IconMessagePlus } from "@tabler/icons-react";
|
||||||
import type { SkillViewInfo } from "../lib/skillCategories";
|
import type { SkillInfo } from "../api/skills";
|
||||||
import type { SkillsSection } from "../lib/skillsHelpers";
|
import type { SkillsSection } from "../lib/skillsHelpers";
|
||||||
|
|
||||||
interface SkillsListSectionsProps {
|
interface SkillsListSectionsProps {
|
||||||
sections: SkillsSection[];
|
sections: SkillsSection[];
|
||||||
expandedSectionIds: string[];
|
expandedSectionIds: string[];
|
||||||
onExpandedSectionIdsChange: (ids: string[]) => void;
|
onExpandedSectionIdsChange: (ids: string[]) => void;
|
||||||
onSelectSkill: (skill: SkillViewInfo) => void;
|
onSelectSkill: (skill: SkillInfo) => void;
|
||||||
onStartChat?: (skill: SkillViewInfo) => void;
|
onStartChat?: (skill: SkillInfo) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SkillsListSections({
|
export function SkillsListSections({
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import { cn } from "@/shared/lib/cn";
|
|||||||
import { SearchBar } from "@/shared/ui/SearchBar";
|
import { SearchBar } from "@/shared/ui/SearchBar";
|
||||||
import { Button } from "@/shared/ui/button";
|
import { Button } from "@/shared/ui/button";
|
||||||
import { FilterRow } from "@/shared/ui/page-shell";
|
import { FilterRow } from "@/shared/ui/page-shell";
|
||||||
import { SkillCategoryFilter } from "./SkillCategoryFilter";
|
|
||||||
import type { SkillCategory } from "../lib/skillCategories";
|
|
||||||
import type { SkillsFilter } from "../lib/skillsHelpers";
|
import type { SkillsFilter } from "../lib/skillsHelpers";
|
||||||
|
|
||||||
interface SkillsToolbarProps {
|
interface SkillsToolbarProps {
|
||||||
@@ -13,9 +11,6 @@ interface SkillsToolbarProps {
|
|||||||
activeFilter: SkillsFilter;
|
activeFilter: SkillsFilter;
|
||||||
onActiveFilterChange: (filter: SkillsFilter) => void;
|
onActiveFilterChange: (filter: SkillsFilter) => void;
|
||||||
projectFilters: { id: string; name: string }[];
|
projectFilters: { id: string; name: string }[];
|
||||||
categoryFilters: SkillCategory[];
|
|
||||||
selectedCategories: SkillCategory[];
|
|
||||||
onSelectedCategoriesChange: (categories: SkillCategory[]) => void;
|
|
||||||
dropHandlers?: React.HTMLAttributes<HTMLDivElement>;
|
dropHandlers?: React.HTMLAttributes<HTMLDivElement>;
|
||||||
isDragOver?: boolean;
|
isDragOver?: boolean;
|
||||||
}
|
}
|
||||||
@@ -47,9 +42,6 @@ export function SkillsToolbar({
|
|||||||
activeFilter,
|
activeFilter,
|
||||||
onActiveFilterChange,
|
onActiveFilterChange,
|
||||||
projectFilters,
|
projectFilters,
|
||||||
categoryFilters,
|
|
||||||
selectedCategories,
|
|
||||||
onSelectedCategoriesChange,
|
|
||||||
dropHandlers,
|
dropHandlers,
|
||||||
isDragOver,
|
isDragOver,
|
||||||
}: SkillsToolbarProps) {
|
}: SkillsToolbarProps) {
|
||||||
@@ -94,13 +86,6 @@ export function SkillsToolbar({
|
|||||||
</FilterButton>
|
</FilterButton>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{categoryFilters.length > 0 ? (
|
|
||||||
<SkillCategoryFilter
|
|
||||||
categories={categoryFilters}
|
|
||||||
selectedCategories={selectedCategories}
|
|
||||||
onSelectedCategoriesChange={onSelectedCategoriesChange}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</FilterRow>
|
</FilterRow>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,12 +25,6 @@ import {
|
|||||||
type EditingSkill,
|
type EditingSkill,
|
||||||
type SkillInfo,
|
type SkillInfo,
|
||||||
} from "../api/skills";
|
} from "../api/skills";
|
||||||
import {
|
|
||||||
uniqueSkillCategories,
|
|
||||||
withInferredSkillCategories,
|
|
||||||
type SkillCategory,
|
|
||||||
type SkillViewInfo,
|
|
||||||
} from "../lib/skillCategories";
|
|
||||||
|
|
||||||
interface SkillsViewProps {
|
interface SkillsViewProps {
|
||||||
onStartChatWithSkill?: (skill: SkillInfo, projectId?: string | null) => void;
|
onStartChatWithSkill?: (skill: SkillInfo, projectId?: string | null) => void;
|
||||||
@@ -41,21 +35,18 @@ export function SkillsView({ onStartChatWithSkill }: SkillsViewProps) {
|
|||||||
const projects = useProjectStore((state) => state.projects);
|
const projects = useProjectStore((state) => state.projects);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [activeFilter, setActiveFilter] = useState<SkillsFilter>("all");
|
const [activeFilter, setActiveFilter] = useState<SkillsFilter>("all");
|
||||||
const [selectedCategories, setSelectedCategories] = useState<SkillCategory[]>(
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const [editingSkill, setEditingSkill] = useState<EditingSkill | undefined>(
|
const [editingSkill, setEditingSkill] = useState<EditingSkill | undefined>(
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
const [skills, setSkills] = useState<SkillViewInfo[]>([]);
|
const [skills, setSkills] = useState<SkillInfo[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [deletingSkill, setDeletingSkill] = useState<SkillInfo | null>(null);
|
const [deletingSkill, setDeletingSkill] = useState<SkillInfo | null>(null);
|
||||||
const [activeSkillId, setActiveSkillId] = useState<string | null>(null);
|
const [activeSkillId, setActiveSkillId] = useState<string | null>(null);
|
||||||
const [expandedSectionIds, setExpandedSectionIds] = useState<string[]>([]);
|
const [expandedSectionIds, setExpandedSectionIds] = useState<string[]>([]);
|
||||||
const loadRequestIdRef = useRef(0);
|
const loadRequestIdRef = useRef(0);
|
||||||
|
|
||||||
const loadSkills = useCallback(async (): Promise<SkillViewInfo[]> => {
|
const loadSkills = useCallback(async (): Promise<SkillInfo[]> => {
|
||||||
const requestId = loadRequestIdRef.current + 1;
|
const requestId = loadRequestIdRef.current + 1;
|
||||||
loadRequestIdRef.current = requestId;
|
loadRequestIdRef.current = requestId;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -66,9 +57,7 @@ export function SkillsView({ onStartChatWithSkill }: SkillsViewProps) {
|
|||||||
if (loadRequestIdRef.current !== requestId) {
|
if (loadRequestIdRef.current !== requestId) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const nextSkills = withInferredSkillCategories(
|
const nextSkills = hydrateProjectNames(result, projects);
|
||||||
hydrateProjectNames(result, projects),
|
|
||||||
);
|
|
||||||
setSkills(nextSkills);
|
setSkills(nextSkills);
|
||||||
return nextSkills;
|
return nextSkills;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -89,10 +78,6 @@ export function SkillsView({ onStartChatWithSkill }: SkillsViewProps) {
|
|||||||
}, [loadSkills]);
|
}, [loadSkills]);
|
||||||
|
|
||||||
const projectFilters = useMemo(() => uniqueProjectFilters(skills), [skills]);
|
const projectFilters = useMemo(() => uniqueProjectFilters(skills), [skills]);
|
||||||
const categoryFilters = useMemo(
|
|
||||||
() => uniqueSkillCategories(skills),
|
|
||||||
[skills],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeFilter.startsWith("project:")) {
|
if (!activeFilter.startsWith("project:")) {
|
||||||
@@ -105,20 +90,9 @@ export function SkillsView({ onStartChatWithSkill }: SkillsViewProps) {
|
|||||||
}
|
}
|
||||||
}, [activeFilter, projectFilters]);
|
}, [activeFilter, projectFilters]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setSelectedCategories((current) =>
|
|
||||||
current.filter((category) => categoryFilters.includes(category)),
|
|
||||||
);
|
|
||||||
}, [categoryFilters]);
|
|
||||||
|
|
||||||
const filteredSkills = useMemo(
|
const filteredSkills = useMemo(
|
||||||
() =>
|
() => filterSkills(skills, { search, activeFilter }),
|
||||||
filterSkills(
|
[skills, search, activeFilter],
|
||||||
skills,
|
|
||||||
{ search, activeFilter, selectedCategories },
|
|
||||||
(category) => t(`view.categories.options.${category}`),
|
|
||||||
),
|
|
||||||
[skills, search, activeFilter, selectedCategories, t],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const groupedSkills = useMemo(
|
const groupedSkills = useMemo(
|
||||||
@@ -219,7 +193,7 @@ export function SkillsView({ onStartChatWithSkill }: SkillsViewProps) {
|
|||||||
handleExport,
|
handleExport,
|
||||||
} = useSkillImportExport(refreshSkills);
|
} = useSkillImportExport(refreshSkills);
|
||||||
|
|
||||||
const handleSelectSkill = (skill: SkillViewInfo) => {
|
const handleSelectSkill = (skill: SkillInfo) => {
|
||||||
setActiveSkillId(skill.id);
|
setActiveSkillId(skill.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -288,9 +262,6 @@ export function SkillsView({ onStartChatWithSkill }: SkillsViewProps) {
|
|||||||
activeFilter={activeFilter}
|
activeFilter={activeFilter}
|
||||||
onActiveFilterChange={setActiveFilter}
|
onActiveFilterChange={setActiveFilter}
|
||||||
projectFilters={projectFilters}
|
projectFilters={projectFilters}
|
||||||
categoryFilters={categoryFilters}
|
|
||||||
selectedCategories={selectedCategories}
|
|
||||||
onSelectedCategoriesChange={setSelectedCategories}
|
|
||||||
dropHandlers={dropHandlers}
|
dropHandlers={dropHandlers}
|
||||||
isDragOver={isDragOver}
|
isDragOver={isDragOver}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -244,7 +244,6 @@ describe("SkillsView", () => {
|
|||||||
expect(
|
expect(
|
||||||
screen.getByText("/tmp/alpha/.goose/skills/test-writer/SKILL.md"),
|
screen.getByText("/tmp/alpha/.goose/skills/test-writer/SKILL.md"),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(screen.getByText("Quality")).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("starts a chat with the selected skill from the list", async () => {
|
it("starts a chat with the selected skill from the list", async () => {
|
||||||
@@ -378,23 +377,6 @@ describe("SkillsView", () => {
|
|||||||
expect(screen.getByText("test-writer")).toBeInTheDocument();
|
expect(screen.getByText("test-writer")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("filters skills by inferred category from the dropdown", async () => {
|
|
||||||
listSkills.mockResolvedValue(mockSkills);
|
|
||||||
const user = userEvent.setup();
|
|
||||||
|
|
||||||
render(<SkillsView />);
|
|
||||||
await screen.findByText("code-review");
|
|
||||||
|
|
||||||
await user.click(
|
|
||||||
screen.getByRole("button", { name: "Filter by category" }),
|
|
||||||
);
|
|
||||||
await user.click(screen.getByRole("menuitemcheckbox", { name: "Design" }));
|
|
||||||
|
|
||||||
expect(screen.getByText("layout")).toBeInTheDocument();
|
|
||||||
expect(screen.queryByText("code-review")).not.toBeInTheDocument();
|
|
||||||
expect(screen.queryByText("test-writer")).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows a delete confirmation from the detail panel", async () => {
|
it("shows a delete confirmation from the detail panel", async () => {
|
||||||
listSkills.mockResolvedValue(mockSkills);
|
listSkills.mockResolvedValue(mockSkills);
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
|
|||||||
@@ -20,24 +20,6 @@
|
|||||||
"deleteError": "Failed to delete skill",
|
"deleteError": "Failed to delete skill",
|
||||||
"deleteSuccess": "Deleted \"{{name}}\"",
|
"deleteSuccess": "Deleted \"{{name}}\"",
|
||||||
"deleteTitle": "Delete \"{{name}}\" permanently?",
|
"deleteTitle": "Delete \"{{name}}\" permanently?",
|
||||||
"category": "Category",
|
|
||||||
"categories": {
|
|
||||||
"clear": "Clear categories",
|
|
||||||
"count": "{{count}} categories",
|
|
||||||
"filter": "Filter by category",
|
|
||||||
"label": "Categories",
|
|
||||||
"options": {
|
|
||||||
"design": "Design",
|
|
||||||
"engineering": "Engineering",
|
|
||||||
"general": "General",
|
|
||||||
"integrations": "Integrations",
|
|
||||||
"operations": "Operations",
|
|
||||||
"productivity": "Productivity",
|
|
||||||
"quality": "Quality",
|
|
||||||
"research": "Research",
|
|
||||||
"writing": "Writing"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Use skills to add specific instructions or behaviors to any agent.",
|
"description": "Use skills to add specific instructions or behaviors to any agent.",
|
||||||
"detailEmptyDescription": "Select a skill to inspect its source, location, and instructions.",
|
"detailEmptyDescription": "Select a skill to inspect its source, location, and instructions.",
|
||||||
"detailEmptyTitle": "Choose a skill",
|
"detailEmptyTitle": "Choose a skill",
|
||||||
|
|||||||
@@ -16,24 +16,6 @@
|
|||||||
},
|
},
|
||||||
"view": {
|
"view": {
|
||||||
"backToSkills": "Volver a skills",
|
"backToSkills": "Volver a skills",
|
||||||
"category": "Categoría",
|
|
||||||
"categories": {
|
|
||||||
"clear": "Limpiar categorías",
|
|
||||||
"count": "{{count}} categorías",
|
|
||||||
"filter": "Filtrar por categoría",
|
|
||||||
"label": "Categorías",
|
|
||||||
"options": {
|
|
||||||
"design": "Diseño",
|
|
||||||
"engineering": "Ingeniería",
|
|
||||||
"general": "General",
|
|
||||||
"integrations": "Integraciones",
|
|
||||||
"operations": "Operaciones",
|
|
||||||
"productivity": "Productividad",
|
|
||||||
"quality": "Calidad",
|
|
||||||
"research": "Investigación",
|
|
||||||
"writing": "Redacción"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"deleteDescription": "Esta habilidad y su configuración se eliminarán de forma permanente.",
|
"deleteDescription": "Esta habilidad y su configuración se eliminarán de forma permanente.",
|
||||||
"deleteError": "Error al eliminar la skill",
|
"deleteError": "Error al eliminar la skill",
|
||||||
"deleteSuccess": "Skill \"{{name}}\" eliminada",
|
"deleteSuccess": "Skill \"{{name}}\" eliminada",
|
||||||
|
|||||||
@@ -42,32 +42,11 @@ test.describe("Skills view", () => {
|
|||||||
page.getByRole("button", { name: "Back to skills" }),
|
page.getByRole("button", { name: "Back to skills" }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await expect(page.getByText("alpha").first()).toBeVisible();
|
await expect(page.getByText("alpha").first()).toBeVisible();
|
||||||
await expect(page.getByText("Quality")).toBeVisible();
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText("/tmp/alpha/.goose/skills/test-writer/SKILL.md"),
|
page.getByText("/tmp/alpha/.goose/skills/test-writer/SKILL.md"),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("category filtering isolates inferred groups", async ({
|
|
||||||
tauriMocked: page,
|
|
||||||
}) => {
|
|
||||||
await navigateToSkills(page);
|
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Filter by category" }).click();
|
|
||||||
await page.getByRole("menuitemcheckbox", { name: "Design" }).click();
|
|
||||||
await page.keyboard.press("Escape");
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
page.getByRole("button", { name: "Open layout details" }),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
page.getByRole("button", { name: "Open code-review details" }),
|
|
||||||
).not.toBeVisible();
|
|
||||||
await expect(
|
|
||||||
page.getByRole("button", { name: "Open test-writer details" }),
|
|
||||||
).not.toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("search filters the list", async ({ tauriMocked: page }) => {
|
test("search filters the list", async ({ tauriMocked: page }) => {
|
||||||
await navigateToSkills(page);
|
await navigateToSkills(page);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user