diff --git a/apps/web/src/components/ChatView.browser.tsx b/apps/web/src/components/ChatView.browser.tsx index bb62d8edbc..a4caed392c 100644 --- a/apps/web/src/components/ChatView.browser.tsx +++ b/apps/web/src/components/ChatView.browser.tsx @@ -5952,6 +5952,48 @@ describe("ChatView timeline estimator parity (full app)", () => { } }); + it("shows provider skills in the slash-command menu", async () => { + const mounted = await mountChatView({ + viewport: DEFAULT_VIEWPORT, + snapshot: createSnapshotForTargetUser({ + targetMessageId: "msg-user-slash-skill-target" as MessageId, + targetText: "slash skill menu thread", + }), + configureFixture: (nextFixture) => { + const provider = nextFixture.serverConfig.providers[0]; + if (!provider) { + throw new Error("Expected default provider in test fixture."); + } + ( + provider as { + skills: ServerConfig["providers"][number]["skills"]; + } + ).skills = [ + { + name: "agent-browser", + displayName: "Agent Browser", + shortDescription: "Open pages, click around, and inspect web apps.", + path: "/Users/test/.agents/skills/agent-browser/SKILL.md", + enabled: true, + }, + ]; + }, + }); + + try { + await waitForComposerEditor(); + await page.getByTestId("composer-editor").fill("/"); + + const skillItem = await waitForComposerMenuItem("skill:codex:agent-browser"); + expect(skillItem.textContent).toContain("Agent Browser"); + + await skillItem.click(); + await waitForComposerText("$agent-browser "); + } finally { + await mounted.cleanup(); + } + }); + it("opens the model picker when selecting /model", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 2c4743de3c..5bf2360b8a 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -115,6 +115,23 @@ import { useMediaQuery } from "../../hooks/useMediaQuery"; const IMAGE_SIZE_LIMIT_LABEL = `${Math.round(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES / (1024 * 1024))}MB`; +function toSkillComposerCommandItem( + provider: ProviderDriverKind, + skill: ServerProvider["skills"][number], +): Extract { + return { + id: `skill:${provider}:${skill.name}`, + type: "skill", + provider, + skill, + label: formatProviderSkillDisplayName(skill), + description: + skill.shortDescription ?? + skill.description ?? + (skill.scope ? `${skill.scope} skill` : "Run provider skill"), + }; +} + const runtimeModeConfig: Record< RuntimeMode, { label: string; description: string; icon: LucideIcon } @@ -899,27 +916,20 @@ export const ChatComposer = memo( }), ); const query = composerTrigger.query.trim().toLowerCase(); + const skillItems = searchProviderSkills(selectedProviderStatus?.skills ?? [], query).map( + (skill) => toSkillComposerCommandItem(selectedProvider, skill), + ); const slashCommandItems = [...builtInSlashCommandItems, ...providerSlashCommandItems]; if (!query) { - return slashCommandItems; + return [...slashCommandItems, ...skillItems]; } - return searchSlashCommandItems(slashCommandItems, query); + return [...searchSlashCommandItems(slashCommandItems, query), ...skillItems]; } if (composerTrigger.kind === "skill") { return searchProviderSkills( selectedProviderStatus?.skills ?? [], composerTrigger.query, - ).map((skill) => ({ - id: `skill:${selectedProvider}:${skill.name}`, - type: "skill" as const, - provider: selectedProvider, - skill, - label: formatProviderSkillDisplayName(skill), - description: - skill.shortDescription ?? - skill.description ?? - (skill.scope ? `${skill.scope} skill` : "Run provider skill"), - })); + ).map((skill) => toSkillComposerCommandItem(selectedProvider, skill)); } return []; }, [composerTrigger, selectedProvider, selectedProviderStatus, workspaceEntries]); diff --git a/apps/web/src/components/chat/ComposerCommandMenu.tsx b/apps/web/src/components/chat/ComposerCommandMenu.tsx index f687ec7ba2..e5c3f84400 100644 --- a/apps/web/src/components/chat/ComposerCommandMenu.tsx +++ b/apps/web/src/components/chat/ComposerCommandMenu.tsx @@ -92,6 +92,7 @@ function groupCommandItems( const builtInItems = items.filter((item) => item.type === "slash-command"); const providerItems = items.filter((item) => item.type === "provider-slash-command"); + const skillItems = items.filter((item) => item.type === "skill"); const groups: ComposerCommandGroup[] = []; if (builtInItems.length > 0) { @@ -100,6 +101,9 @@ function groupCommandItems( if (providerItems.length > 0) { groups.push({ id: "provider", label: "Provider", items: providerItems }); } + if (skillItems.length > 0) { + groups.push({ id: "skills", label: "Skills", items: skillItems }); + } return groups; }