Skip to content
Open
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
12 changes: 11 additions & 1 deletion extensions/skills/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
# Skills Changelog

## [Improve Installed Indicators, Source Matching & Refresh] - {PR_MERGE_DATE}

- Replace the green "Installed" tag in "Search Skills" with a green check-circle indicator and green skill icon for installed skills
- Show the skill source (`owner/repo`) as the subtitle in "Search Skills" and "Manage Skills" to better distinguish skills between them
- Move "Search Skills" result details out of the side panel and into a full-screen detail view
- Detect skills with the same `skillId` installed from different sources. Show a warning indicator and rename the "Install Skill" action to "Replace Installed Skill" for clarity on the outcome
- Immediately refresh installed indicators in "Search Skills" after installing or replacing a skill
- Add "Refresh Installed Skills" actions with Cmd+R in "Manage Skills"
- Consolidate shared install, remove, and update action handling for more consistent confirmations, toasts, and error messages

## [Fix Windows bunx Fallback] - 2026-04-30

- Fix the Skills CLI throwing `'"bunx"' is not recognized as an internal or external command` on Windows when Bun is not installed; the extension now correctly falls back to `npx` as intended

## [Add bunx support] - 2026-04-28

- Added initial support for `bunx`, it is only called optimally if `npx` is not found
- Added initial support for `npx`, it is only called optimally if `bunx` is not found

## [Add Skills CLI Telemetry Opt-Out] - 2026-04-28

Expand Down
16 changes: 9 additions & 7 deletions extensions/skills/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ Search and manage AI agent skills from [Skills](https://skills.sh) directly in R
## Features

- Search for specific skills
- See which search results are already installed with a green "Installed" badge
- See which search results are already installed with a green check-circle indicator
- Identify skills installed from a different source with a conflict warning
- Filter available skills by owner
- Install skills for all supported agents
- View security audit status from `skills.sh` before installing
Expand All @@ -14,23 +15,24 @@ Search and manage AI agent skills from [Skills](https://skills.sh) directly in R
- Filter installed skills by agent
- View skill source, install date, and update date from the lock file
- Open installed skill repositories on GitHub
- View skill details inline with SKILL.md content, including description, license, compatibility, and allowed tools (toggle with Cmd+D)
- See GitHub star counts in the detail panel
- Open search result details in a full-screen view with SKILL.md content, including description, license, compatibility, and allowed tools
- See GitHub star counts in the detail view
- Copy install commands
- Quick access to GitHub repositories

## Commands

### Search Skills

Search for agent skills from skills.sh with real-time results. View skill details in the inline panel, including security audit status when available.
Search for agent skills from skills.sh with real-time results. Results show which skills are installed locally and flag skills that may conflict with a local install from another source. Open a result to view full-screen details, including security audit status when available.

### Manage Skills

View, update, and remove installed skills. Outdated skills are highlighted with an orange icon and grouped in the "Updates Available" section. Filter by agent to see which skills are available for each AI agent.

## Screenshots

![Search Skills](metadata/skills-1.png)
![Search Skills - Owner Filter](metadata/skills-2.png)
![Manage Skills](metadata/skills-3.png)
![Search Skills - Owner Filter](metadata/skills-1.png)
![Search Skills - Details](metadata/skills-2.png)
![Search Skills - Installed Status](metadata/skills-3.png)
![Manage Skills](metadata/skills-4.png)
Binary file modified extensions/skills/metadata/skills-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified extensions/skills/metadata/skills-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified extensions/skills/metadata/skills-3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added extensions/skills/metadata/skills-4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 9 additions & 1 deletion extensions/skills/src/components/InstalledSkillListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ interface InstalledSkillListItemProps {
isShowingDetail: boolean;
mutate: MutateSkills;
onToggleDetail: () => void;
onRefresh: () => void;
}

export function InstalledSkillListItem({
Expand All @@ -90,14 +91,15 @@ export function InstalledSkillListItem({
isShowingDetail,
mutate,
onToggleDetail,
onRefresh,
}: InstalledSkillListItemProps) {
const extraAgents = skill.agentCount - skill.agents.length;
const agentsText = extraAgents > 0 ? `${skill.agents.join(", ")} +${extraAgents} more` : skill.agents.join(", ");

return (
<List.Item
title={skill.name}
subtitle={isShowingDetail ? undefined : agentsText}
subtitle={isShowingDetail ? undefined : skill.source}
icon={{ source: Icon.Hammer, tintColor: skill.hasUpdate ? Color.Orange : Color.Purple }}
accessories={
isShowingDetail
Expand Down Expand Up @@ -153,6 +155,12 @@ export function InstalledSkillListItem({
shortcut={{ modifiers: ["cmd"], key: "d" }}
onAction={onToggleDetail}
/>
<Action
title="Refresh Installed Skills"
onAction={onRefresh}
icon={Icon.RotateClockwise}
shortcut={{ modifiers: ["cmd"], key: "r" }}
/>
</ActionPanel>
}
/>
Expand Down
206 changes: 206 additions & 0 deletions extensions/skills/src/components/SkillDetailView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import { Action, ActionPanel, Color, Detail, Icon, Keyboard } from "@raycast/api";
import { useCallback, useEffect, useRef } from "react";
import {
type AuditStatus,
AUDIT_PROVIDER_LABELS,
buildInstallCommand,
formatInstalls,
normalizeAllowedTools,
type Skill,
SKILLS_BASE_URL,
} from "../shared";
import { useRepoStats } from "../hooks/useRepoStats";
import { useSkillAudits } from "../hooks/useSkillAudits";
import { useSkillContent } from "../hooks/useSkillContent";
import { useInstalledSkillMatches } from "../hooks/useInstalledSkillMatches";
import { type SkillAuditsAvailabilityState } from "../utils/skill-audits";
import { showSkillAuditErrorToast } from "../utils/skill-audit-error-toast";
import { InstallSkillAction } from "./actions/InstallSkillAction";
import { OpenSecurityAuditActions } from "./actions/OpenSecurityAuditActions";

const AUDIT_STATUS_META: Record<AuditStatus, { emoji: string; label: string }> = {
pass: { emoji: "✅", label: "Pass" },
warn: { emoji: "⚠️", label: "Warn" },
fail: { emoji: "🛑", label: "Fail" },
unknown: { emoji: "", label: "Unknown" },
};

function formatAuditStatus(status: AuditStatus): string {
const { emoji, label } = AUDIT_STATUS_META[status];
return emoji ? `${emoji} ${label}` : label;
}

function getAuditFallbackText(isLoading: boolean, availabilityState?: SkillAuditsAvailabilityState): string {
if (isLoading) return "Loading...";
if (availabilityState === "parse-error" || availabilityState === "fetch-error") return "Unable to verify";
return "Pending";
}

interface SkillDetailViewProps {
skill: Skill;
onSkillInstalled?: () => void | Promise<void>;
}

export function SkillDetailView({ skill, onSkillInstalled }: SkillDetailViewProps) {
const { getInstalledMatch, revalidate: revalidateInstalledSkillMatches } = useInstalledSkillMatches();
const installedMatch = getInstalledMatch(skill);
const { content, frontmatter, isLoading } = useSkillContent(skill, true);
const { stats } = useRepoStats(skill, true);
const audits = useSkillAudits(skill, {
shouldFetch: true,
});
const installCommand = buildInstallCommand(skill);
const allowedTools = normalizeAllowedTools(frontmatter["allowed-tools"]);
const shownErrorTimestampRef = useRef<string | undefined>(undefined);
const refreshSkillStatus = useCallback(async () => {
await Promise.all([revalidateInstalledSkillMatches(), onSkillInstalled?.()]);
}, [revalidateInstalledSkillMatches, onSkillInstalled]);

useEffect(() => {
if (
audits.errorDetails !== undefined &&
audits.errorDetails.skillSource === skill.source &&
audits.errorDetails.skillId === skill.skillId &&
audits.errorDetails.timestamp !== shownErrorTimestampRef.current
) {
shownErrorTimestampRef.current = audits.errorDetails.timestamp;
void showSkillAuditErrorToast({
error: audits.error ?? new Error("Unknown error"),
errorDetails: audits.errorDetails,
skillName: skill.name,
onRetry: audits.revalidate,
});
}
}, [audits.error, audits.errorDetails, audits.revalidate, skill.name, skill.source, skill.skillId]);

const markdown = isLoading
? `# ${skill.name}\n\nLoading...`
: content
? content
: `# ${skill.name}

**Repository:** [${skill.source}](https://github.com/${skill.source})

**Installs:** ${formatInstalls(skill.installs)}

---

\`\`\`bash
${installCommand}
\`\`\`
`;

return (
<Detail
isLoading={isLoading}
markdown={markdown}
navigationTitle={skill.name}
metadata={
<Detail.Metadata>
{installedMatch.type === "exact" && (
<Detail.Metadata.TagList title="Installation">
<Detail.Metadata.TagList.Item text="Installed" color={Color.Green} />
</Detail.Metadata.TagList>
)}
{installedMatch.type === "conflict" && (
<Detail.Metadata.TagList title="Installation">
<Detail.Metadata.TagList.Item text="Different Source" color={Color.Orange} />
</Detail.Metadata.TagList>
)}
{installedMatch.type === "conflict" && (
<Detail.Metadata.Label title="Installed Source" text={installedMatch.source ?? "Unknown source"} />
)}
{installedMatch.type !== "none" && installedMatch.agents.length > 0 && (
<Detail.Metadata.TagList title="Agents">
{installedMatch.agents.map((agent) => (
<Detail.Metadata.TagList.Item key={agent} text={agent} color={Color.Blue} />
))}
</Detail.Metadata.TagList>
)}
{installedMatch.type !== "none" && <Detail.Metadata.Separator />}
{frontmatter.description && <Detail.Metadata.Label title="Description" text={frontmatter.description} />}
{frontmatter.description && <Detail.Metadata.Separator />}
<Detail.Metadata.Label title="Installs" text={formatInstalls(skill.installs)} icon={Icon.Download} />
{stats?.rateLimited ? (
<Detail.Metadata.Label title="GitHub Stars" text="Rate limited" icon={Icon.Warning} />
) : (
stats?.stars !== undefined &&
stats?.stars !== null && (
<Detail.Metadata.Label title="GitHub Stars" text={formatInstalls(stats.stars)} icon={Icon.Star} />
)
)}
{frontmatter.license && (
<Detail.Metadata.Label title="License" text={frontmatter.license} icon={Icon.Document} />
)}
{frontmatter.compatibility && (
<Detail.Metadata.Label title="Compatibility" text={frontmatter.compatibility} icon={Icon.Checkmark} />
)}
{allowedTools.length > 0 && (
<Detail.Metadata.TagList title="Allowed Tools">
{allowedTools.map((tool: string) => (
<Detail.Metadata.TagList.Item key={tool} text={tool} color={Color.Blue} />
))}
</Detail.Metadata.TagList>
)}
<Detail.Metadata.Separator />
<Detail.Metadata.Link title="Repository" text={skill.source} target={`https://github.com/${skill.source}`} />
<Detail.Metadata.Link
title="View on Skills"
text={`${skill.source}/${skill.skillId}`}
target={`${SKILLS_BASE_URL}/${skill.source}/${skill.skillId}`}
/>
<Detail.Metadata.Separator />
{audits.results.length > 0 ? (
audits.results.map((audit) =>
audit.url ? (
<Detail.Metadata.Link
key={audit.provider}
title={`${AUDIT_PROVIDER_LABELS[audit.provider]} Audit`}
text={formatAuditStatus(audit.status)}
target={audit.url}
/>
) : (
<Detail.Metadata.Label
key={audit.provider}
title={`${AUDIT_PROVIDER_LABELS[audit.provider]} Audit`}
text={formatAuditStatus(audit.status)}
/>
),
)
) : (
<Detail.Metadata.Label
title="Security Audits"
text={getAuditFallbackText(audits.isLoading, audits.availabilityState)}
/>
)}
<Detail.Metadata.Separator />
<Detail.Metadata.Label title="Install Command" text={installCommand} />
</Detail.Metadata>
}
actions={
<ActionPanel>
<InstallSkillAction
skill={skill}
installedMatch={installedMatch}
prefetchedAuditResult={audits.result}
onSkillInstalled={refreshSkillStatus}
/>
<Action.CopyToClipboard
title="Copy Install Command"
content={buildInstallCommand(skill)}
icon={Icon.Terminal}
shortcut={Keyboard.Shortcut.Common.Copy}
/>
<Action.OpenInBrowser title="Open Repository" url={`https://github.com/${skill.source}`} icon={Icon.Globe} />
<Action.OpenInBrowser
title="Open Skills"
url={`${SKILLS_BASE_URL}/${skill.source}/${skill.skillId}`}
icon={Icon.Link}
shortcut={Keyboard.Shortcut.Common.Open}
/>
<OpenSecurityAuditActions audits={audits.results} />
</ActionPanel>
}
/>
);
}
Loading
Loading