mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-14 01:50:39 +09:00
Add skill slash-command autocomplete
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
fe61e650c2
commit
94d4a01b76
9 changed files with 271 additions and 48 deletions
|
|
@ -600,14 +600,19 @@ export { deriveProjectUrlKey, normalizeProjectUrlKey, hasNonAsciiContent } from
|
|||
export {
|
||||
AGENT_MENTION_SCHEME,
|
||||
PROJECT_MENTION_SCHEME,
|
||||
SKILL_MENTION_SCHEME,
|
||||
buildAgentMentionHref,
|
||||
buildProjectMentionHref,
|
||||
buildSkillMentionHref,
|
||||
extractAgentMentionIds,
|
||||
extractSkillMentionIds,
|
||||
parseAgentMentionHref,
|
||||
parseProjectMentionHref,
|
||||
parseSkillMentionHref,
|
||||
extractProjectMentionIds,
|
||||
type ParsedAgentMention,
|
||||
type ParsedProjectMention,
|
||||
type ParsedSkillMention,
|
||||
} from "./project-mentions.js";
|
||||
|
||||
export {
|
||||
|
|
|
|||
|
|
@ -2,10 +2,13 @@ import { describe, expect, it } from "vitest";
|
|||
import {
|
||||
buildAgentMentionHref,
|
||||
buildProjectMentionHref,
|
||||
buildSkillMentionHref,
|
||||
extractAgentMentionIds,
|
||||
extractProjectMentionIds,
|
||||
extractSkillMentionIds,
|
||||
parseAgentMentionHref,
|
||||
parseProjectMentionHref,
|
||||
parseSkillMentionHref,
|
||||
} from "./project-mentions.js";
|
||||
|
||||
describe("project-mentions", () => {
|
||||
|
|
@ -26,4 +29,13 @@ describe("project-mentions", () => {
|
|||
});
|
||||
expect(extractAgentMentionIds(`[@CodexCoder](${href})`)).toEqual(["agent-123"]);
|
||||
});
|
||||
|
||||
it("round-trips skill mentions with slug metadata", () => {
|
||||
const href = buildSkillMentionHref("skill-123", "release-changelog");
|
||||
expect(parseSkillMentionHref(href)).toEqual({
|
||||
skillId: "skill-123",
|
||||
slug: "release-changelog",
|
||||
});
|
||||
expect(extractSkillMentionIds(`[/release-changelog](${href})`)).toEqual(["skill-123"]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
export const PROJECT_MENTION_SCHEME = "project://";
|
||||
export const AGENT_MENTION_SCHEME = "agent://";
|
||||
export const SKILL_MENTION_SCHEME = "skill://";
|
||||
|
||||
const HEX_COLOR_RE = /^[0-9a-f]{6}$/i;
|
||||
const HEX_COLOR_SHORT_RE = /^[0-9a-f]{3}$/i;
|
||||
|
|
@ -7,7 +8,9 @@ const HEX_COLOR_WITH_HASH_RE = /^#[0-9a-f]{6}$/i;
|
|||
const HEX_COLOR_SHORT_WITH_HASH_RE = /^#[0-9a-f]{3}$/i;
|
||||
const PROJECT_MENTION_LINK_RE = /\[[^\]]*]\((project:\/\/[^)\s]+)\)/gi;
|
||||
const AGENT_MENTION_LINK_RE = /\[[^\]]*]\((agent:\/\/[^)\s]+)\)/gi;
|
||||
const SKILL_MENTION_LINK_RE = /\[[^\]]*]\((skill:\/\/[^)\s]+)\)/gi;
|
||||
const AGENT_ICON_NAME_RE = /^[a-z0-9-]+$/i;
|
||||
const SKILL_SLUG_RE = /^[a-z0-9][a-z0-9-]*$/i;
|
||||
|
||||
export interface ParsedProjectMention {
|
||||
projectId: string;
|
||||
|
|
@ -19,6 +22,11 @@ export interface ParsedAgentMention {
|
|||
icon: string | null;
|
||||
}
|
||||
|
||||
export interface ParsedSkillMention {
|
||||
skillId: string;
|
||||
slug: string | null;
|
||||
}
|
||||
|
||||
function normalizeHexColor(input: string | null | undefined): string | null {
|
||||
if (!input) return null;
|
||||
const trimmed = input.trim();
|
||||
|
|
@ -103,6 +111,36 @@ export function parseAgentMentionHref(href: string): ParsedAgentMention | null {
|
|||
};
|
||||
}
|
||||
|
||||
export function buildSkillMentionHref(skillId: string, slug?: string | null): string {
|
||||
const trimmedSkillId = skillId.trim();
|
||||
const normalizedSlug = normalizeSkillSlug(slug ?? null);
|
||||
if (!normalizedSlug) {
|
||||
return `${SKILL_MENTION_SCHEME}${trimmedSkillId}`;
|
||||
}
|
||||
return `${SKILL_MENTION_SCHEME}${trimmedSkillId}?s=${encodeURIComponent(normalizedSlug)}`;
|
||||
}
|
||||
|
||||
export function parseSkillMentionHref(href: string): ParsedSkillMention | null {
|
||||
if (!href.startsWith(SKILL_MENTION_SCHEME)) return null;
|
||||
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(href);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (url.protocol !== "skill:") return null;
|
||||
|
||||
const skillId = `${url.hostname}${url.pathname}`.replace(/^\/+/, "").trim();
|
||||
if (!skillId) return null;
|
||||
|
||||
return {
|
||||
skillId,
|
||||
slug: normalizeSkillSlug(url.searchParams.get("s") ?? url.searchParams.get("slug")),
|
||||
};
|
||||
}
|
||||
|
||||
export function extractProjectMentionIds(markdown: string): string[] {
|
||||
if (!markdown) return [];
|
||||
const ids = new Set<string>();
|
||||
|
|
@ -127,9 +165,28 @@ export function extractAgentMentionIds(markdown: string): string[] {
|
|||
return [...ids];
|
||||
}
|
||||
|
||||
export function extractSkillMentionIds(markdown: string): string[] {
|
||||
if (!markdown) return [];
|
||||
const ids = new Set<string>();
|
||||
const re = new RegExp(SKILL_MENTION_LINK_RE);
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = re.exec(markdown)) !== null) {
|
||||
const parsed = parseSkillMentionHref(match[1]);
|
||||
if (parsed) ids.add(parsed.skillId);
|
||||
}
|
||||
return [...ids];
|
||||
}
|
||||
|
||||
function normalizeAgentIcon(input: string | null | undefined): string | null {
|
||||
if (!input) return null;
|
||||
const trimmed = input.trim().toLowerCase();
|
||||
if (!trimmed || !AGENT_ICON_NAME_RE.test(trimmed)) return null;
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function normalizeSkillSlug(input: string | null | undefined): string | null {
|
||||
if (!input) return null;
|
||||
const trimmed = input.trim().toLowerCase();
|
||||
if (!trimmed || !SKILL_SLUG_RE.test(trimmed)) return null;
|
||||
return trimmed;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue