From 2f1ba85f853e2e9655b1c2a12d0d8bbecb8b026e Mon Sep 17 00:00:00 2001 From: delusionofgrandeur Date: Tue, 12 May 2026 00:58:37 +0400 Subject: [PATCH 1/2] fix(web): show provider skills in slash menu Include provider skills alongside slash commands when users open the composer / menu, and keep selection behavior inserting the existing $skill token format. --- apps/web/src/components/ChatView.browser.tsx | 42 +++++++++++++++++++ apps/web/src/components/chat/ChatComposer.tsx | 17 +++++++- .../components/chat/ComposerCommandMenu.tsx | 4 ++ 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/ChatView.browser.tsx b/apps/web/src/components/ChatView.browser.tsx index bb62d8edbc0..a4caed392cb 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 2c4743de3c6..39dc54ade83 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -899,11 +899,24 @@ export const ChatComposer = memo( }), ); const query = composerTrigger.query.trim().toLowerCase(); + const skillItems = searchProviderSkills(selectedProviderStatus?.skills ?? [], 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"), + }), + ); 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( diff --git a/apps/web/src/components/chat/ComposerCommandMenu.tsx b/apps/web/src/components/chat/ComposerCommandMenu.tsx index f687ec7ba23..e5c3f844006 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; } From 847b12ad05e8ec5caa07f6963549b37a4dc98576 Mon Sep 17 00:00:00 2001 From: delusionofgrandeur Date: Tue, 12 May 2026 13:14:34 +0400 Subject: [PATCH 2/2] refactor(web): reuse skill menu item mapping Extract provider skill menu item formatting so slash-command and skill triggers share the same ComposerCommandItem shape. --- apps/web/src/components/chat/ChatComposer.tsx | 41 +++++++++---------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 39dc54ade83..5bf2360b8ae 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 } @@ -900,17 +917,7 @@ export const ChatComposer = memo( ); const query = composerTrigger.query.trim().toLowerCase(); const skillItems = searchProviderSkills(selectedProviderStatus?.skills ?? [], 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"), - }), + (skill) => toSkillComposerCommandItem(selectedProvider, skill), ); const slashCommandItems = [...builtInSlashCommandItems, ...providerSlashCommandItems]; if (!query) { @@ -922,17 +929,7 @@ export const ChatComposer = memo( 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]);