Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ function appendRun(
return runs;
}

const SKILL_TOKEN_REGEX = /(^|\s)\$([a-zA-Z][a-zA-Z0-9:_-]*)(?=\s|$)/g;
const SKILL_TOKEN_REGEX = /(^|\s)\$([a-zA-Z0-9][a-zA-Z0-9:_-]*)(?=\s|$)/g;

function formatSkillLabel(skill: SelectableMarkdownSkill): string {
const displayName = skill.displayName?.trim();
Expand Down
23 changes: 23 additions & 0 deletions apps/mobile/src/lib/nativeMarkdownText.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,29 @@ describe("nativeMarkdownDocumentRuns", () => {
]);
});

it("decorates known skill references that begin with a digit", () => {
const node: MarkdownNode = {
type: "document",
children: [
{
type: "paragraph",
children: [{ type: "text", content: "Use $2spec for this." }],
},
],
};

expect(nativeMarkdownDocumentRuns(node, [{ name: "2spec", displayName: "2Spec" }])).toEqual([
{ text: "Use ", role: "body" },
{
text: "$2spec",
role: "body",
skillName: "2spec",
skillLabel: "2Spec",
},
{ text: " for this.", role: "body" },
]);
});

it("decorates known skill references inside blockquotes", () => {
const node: MarkdownNode = {
type: "blockquote",
Expand Down
10 changes: 9 additions & 1 deletion apps/server/src/provider/Drivers/ClaudeSkillDispatch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, expect, it } from "vite-plus/test";

import { planClaudeSkillDispatch } from "./ClaudeSkillDispatch.ts";

const SKILLS = new Set(["implement", "review", "re-release-version"]);
const SKILLS = new Set(["2spec", "implement", "review", "re-release-version"]);

describe("planClaudeSkillDispatch", () => {
it("leaves a prompt without a known skill untouched", () => {
Expand All @@ -27,6 +27,14 @@ describe("planClaudeSkillDispatch", () => {
});
});

it("dispatches a known skill whose name begins with a digit", () => {
expect(planClaudeSkillDispatch("use $2spec for this", SKILLS)).toEqual({
leadingText: "use",
commandText: "/2spec for this",
skillName: "2spec",
});
});

it("dispatches the last mention and rewrites earlier ones inline", () => {
expect(planClaudeSkillDispatch("$review the diff, then $implement the fixes", SKILLS)).toEqual({
leadingText: "/review the diff, then",
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/provider/Drivers/ClaudeSkillDispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
* (`packages/shared/src/composerInlineTokens.ts`), so a rendered chip and a
* dispatched skill are always the same set.
*/
const SKILL_MENTION_PATTERN = /(^|\s)\$([a-zA-Z][a-zA-Z0-9:_-]*)(?=\s|$)/g;
const SKILL_MENTION_PATTERN = /(^|\s)\$([a-zA-Z0-9][a-zA-Z0-9:_-]*)(?=\s|$)/g;

export interface ClaudeSkillDispatch {
/** Text before the dispatched mention, or `undefined` when it opens the prompt. */
Expand Down
15 changes: 15 additions & 0 deletions apps/web/src/components/ChatMarkdown.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,21 @@ describe("hasMarkdownFilePrimaryAction", () => {
});
});

describe("ChatMarkdown skill chips", () => {
it("renders known skill references that begin with a digit", () => {
const html = renderToStaticMarkup(
<ChatMarkdown
cwd="/tmp/project"
text="Use $2spec for this."
skills={[{ name: "2spec", displayName: "2Spec" }]}
/>,
);

expect(html).toContain('data-markdown-copy="$2spec"');
expect(html).toContain("2Spec");
});
});

describe("ChatMarkdown file option chips", () => {
it("keeps the fallback button text selectable", () => {
const html = renderToStaticMarkup(
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/chat/SkillInlineText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from "../composerInlineChip";
import { cn } from "~/lib/utils";

const SKILL_TOKEN_REGEX = /(^|\s)\$([a-zA-Z][a-zA-Z0-9:_-]*)(?=\s|$)/g;
const SKILL_TOKEN_REGEX = /(^|\s)\$([a-zA-Z0-9][a-zA-Z0-9:_-]*)(?=\s|$)/g;

type InlineSkill = Pick<ServerProviderSkill, "name" | "displayName">;

Expand Down
8 changes: 8 additions & 0 deletions apps/web/src/composer-editor-mentions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,14 @@ describe("splitPromptIntoComposerSegments", () => {
]);
});

it("splits digit-leading skill tokens into skill segments", () => {
expect(splitPromptIntoComposerSegments("Use $2spec please")).toEqual([
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
{ type: "text", text: "Use " },
{ type: "skill", name: "2spec" },
{ type: "text", text: " please" },
]);
});

it("does not convert an incomplete trailing skill token", () => {
expect(splitPromptIntoComposerSegments("Use $review-follow-up")).toEqual([
{ type: "text", text: "Use $review-follow-up" },
Expand Down
12 changes: 12 additions & 0 deletions packages/shared/src/composerInlineTokens.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ describe("collectComposerInlineTokens", () => {
]);
});

it("collects skill names that begin with a digit", () => {
expect(collectComposerInlineTokens("Use $2spec next")).toEqual([
{
type: "skill",
value: "2spec",
source: "$2spec",
start: 4,
end: 10,
},
]);
});

it("does not convert incomplete trailing tokens", () => {
expect(collectComposerInlineTokens("Use $ui")).toEqual([]);
expect(collectComposerInlineTokens("Inspect @AGENTS.md")).toEqual([]);
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/src/composerInlineTokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface CollectComposerInlineTokensOptions {
readonly preserveTrailingFrom?: ReadonlyArray<ComposerInlineToken>;
}

const SKILL_TOKEN_REGEX = /(^|\s)\$([a-zA-Z][a-zA-Z0-9:_-]*)(?=\s)/g;
const SKILL_TOKEN_REGEX = /(^|\s)\$([a-zA-Z0-9][a-zA-Z0-9:_-]*)(?=\s)/g;
const MENTION_TOKEN_REGEX = /(^|\s)@(?:"((?:\\.|[^"\\])*)"|([^\s@"]+))(?=\s)/g;
/**
* The label body is bounded rather than `*`. Unbounded, every whitespace in
Expand Down
Loading