Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
250 changes: 0 additions & 250 deletions .agents/skills/ui-refactor-review/SKILL.md

This file was deleted.

49 changes: 10 additions & 39 deletions ui/goose2/src/features/skills/api/skills.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { SourceEntry } from "@aaif/goose-sdk";
import { getClient } from "@/shared/api/acpConnection";
import {
basename,
deriveProjectRoot,
getSkillFileLocation,
} from "../lib/skillsPath";

const SKILL_SOURCE_TYPE = "skill" as const;
const PROJECT_SKILLS_MARKERS = [
"/.agents/skills/",
"/.goose/skills/",
"/.claude/skills/",
];

export interface SkillProjectLink {
id: string;
Expand All @@ -23,49 +23,22 @@ export interface SkillInfo {
instructions: string;
path: string;
fileLocation: string;
directoryPath: string;
sourceKind: SkillSourceKind;
sourceLabel: string;
projectLinks: SkillProjectLink[];
editable: boolean;
}

export type EditingSkill = Pick<
SkillInfo,
"name" | "description" | "instructions" | "path" | "fileLocation"
>;

type SkillSourceEntry = SourceEntry & { type: typeof SKILL_SOURCE_TYPE };

function isSkillSource(source: SourceEntry): source is SkillSourceEntry {
return source.type === SKILL_SOURCE_TYPE;
}

function normalizePath(path: string): string {
return path.replace(/\\/g, "/");
}

function basename(path: string): string {
const trimmed = normalizePath(path).replace(/\/+$/, "");
const idx = trimmed.lastIndexOf("/");
return idx >= 0 ? trimmed.slice(idx + 1) : trimmed;
}

function getSkillFileLocation(directory: string): string {
const separator = directory.includes("\\") ? "\\" : "/";
return directory.endsWith(separator)
? `${directory}SKILL.md`
: `${directory}${separator}SKILL.md`;
}

function deriveProjectRoot(directory: string): string | null {
const normalizedDirectory = normalizePath(directory);

for (const marker of PROJECT_SKILLS_MARKERS) {
const idx = normalizedDirectory.lastIndexOf(marker);
if (idx >= 0) {
return directory.slice(0, idx);
}
}

return null;
}

function toSkillInfo(source: SkillSourceEntry): SkillInfo {
const sourceKind: SkillSourceKind = source.global ? "global" : "project";
const projectRoot = source.global
Expand All @@ -90,12 +63,10 @@ function toSkillInfo(source: SkillSourceEntry): SkillInfo {
instructions: source.content,
path: source.directory,
fileLocation: getSkillFileLocation(source.directory),
directoryPath: source.directory,
sourceKind,
sourceLabel:
sourceKind === "global" ? "Personal" : projectName || "Project",
projectLinks,
editable: true,
};
}

Expand Down
33 changes: 33 additions & 0 deletions ui/goose2/src/features/skills/hooks/useSkillImportExport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { useFileImportZone } from "@/shared/hooks/useFileImportZone";
import { exportSkill, importSkills, type SkillInfo } from "../api/skills";
import { downloadExport } from "../lib/skillsHelpers";

export function useSkillImportExport(onAfterImport: () => Promise<void>) {
const { t } = useTranslation(["skills"]);

const handleExport = async (skill: SkillInfo) => {
try {
const result = await exportSkill(skill.path);
downloadExport(result.json, result.filename);
toast.success(t("view.exportedTo", { filename: result.filename }));
} catch {
toast.error(t("view.exportError"));
}
};

const handleImport = async (fileBytes: number[], fileName: string) => {
try {
await importSkills(fileBytes, fileName);
await onAfterImport();
toast.success(t("view.importSuccess"));
} catch {
toast.error(t("view.importError"));
}
};

const fileImport = useFileImportZone({ onImportFile: handleImport });

return { ...fileImport, handleExport };
}
Loading
Loading