authorgravatar for 92423096+nateEc@users.noreply.github.comYukun Shan <92423096+nateEc@users.noreply.github.com> 2026-09-01 01:28:31+08:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-31 10:28:31-07:00
logbba79cc254b65969bde6b6bfc3032c3b5b9316ae
treeb33c3fe8ea3fcbd32f56c31649199252de5c391c
parent2921050c698fb195e2f5590b7cfab8fdfe0ec729
signature Signed by PGP key B5690EEEBB952194

fix(web): hide invalid slash skill completions (#8904)


3 files changed, 56 insertions(+), 7 deletions(-)

apps/web/src/components/chat/ChatComposer.tsx+8-6
...@@ -146,7 +146,10 @@ import { ComposerPendingUserInputPanel } from "./ComposerPendingUserInputPanel";...@@ -146,7 +146,10 @@ import { ComposerPendingUserInputPanel } from "./ComposerPendingUserInputPanel";
146import { ComposerPlanFollowUpBanner } from "./ComposerPlanFollowUpBanner";146import { ComposerPlanFollowUpBanner } from "./ComposerPlanFollowUpBanner";
147import { ComposerControl, ComposerControlIcon, ComposerSelectControl } from "./ComposerControl";147import { ComposerControl, ComposerControlIcon, ComposerSelectControl } from "./ComposerControl";
148import { resolveComposerMenuActiveItemId } from "./composerMenuHighlight";148import { resolveComposerMenuActiveItemId } from "./composerMenuHighlight";
149import { searchSlashCommandItems } from "./composerSlashCommandSearch";149import {
150 searchSlashCommandItems,
151 slashCommandItemsForPromptPosition,
152} from "./composerSlashCommandSearch";
150import {153import {
151 getComposerPromptInjectionState,154 getComposerPromptInjectionState,
152 getComposerProviderState,155 getComposerProviderState,
...@@ -1360,11 +1363,10 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -1360,11 +1363,10 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
1360 skill.description ??1363 skill.description ??
1361 (skill.scope ? `${skill.scope} skill` : ""),1364 (skill.scope ? `${skill.scope} skill` : ""),
1362 }));1365 }));
1363 const slashCommandItems = [1366 const slashCommandItems = slashCommandItemsForPromptPosition(
1364 ...builtInSlashCommandItems,1367 [...builtInSlashCommandItems, ...providerSlashCommandItems, ...skillItems],
1365 ...providerSlashCommandItems,1368 composerTrigger.rangeStart === 0,
1366 ...skillItems,1369 );
1367 ];
1368 return searchSlashCommandItems(slashCommandItems, query);1370 return searchSlashCommandItems(slashCommandItems, query);
1369 }1371 }
1370 if (composerTrigger.kind === "skill") {1372 if (composerTrigger.kind === "skill") {
apps/web/src/components/chat/composerSlashCommandSearch.test.ts+38-1
...@@ -2,7 +2,10 @@ import { describe, expect, it } from "vite-plus/test";...@@ -2,7 +2,10 @@ import { describe, expect, it } from "vite-plus/test";
2import { ProviderDriverKind } from "@t3tools/contracts";2import { ProviderDriverKind } from "@t3tools/contracts";
33
4import type { ComposerCommandItem } from "./ComposerCommandMenu";4import type { ComposerCommandItem } from "./ComposerCommandMenu";
5import { searchSlashCommandItems } from "./composerSlashCommandSearch";5import {
6 searchSlashCommandItems,
7 slashCommandItemsForPromptPosition,
8} from "./composerSlashCommandSearch";
69
7describe("searchSlashCommandItems", () => {10describe("searchSlashCommandItems", () => {
8 const claudeDriver = ProviderDriverKind.make("claudeAgent");11 const claudeDriver = ProviderDriverKind.make("claudeAgent");
...@@ -173,4 +176,38 @@ describe("searchSlashCommandItems", () => {...@@ -173,4 +176,38 @@ describe("searchSlashCommandItems", () => {
173 "skill:claudeAgent:unslop",176 "skill:claudeAgent:unslop",
174 ]);177 ]);
175 });178 });
179
180 it("hides skills from slash completion after the first message line", () => {
181 const items = [
182 {
183 id: "slash:model",
184 type: "slash-command",
185 command: "model",
186 label: "/model",
187 description: "Switch model",
188 },
189 {
190 id: "skill:claudeAgent:unslop",
191 type: "skill",
192 provider: claudeDriver,
193 skill: {
194 name: "unslop",
195 path: "/skills/unslop/SKILL.md",
196 enabled: true,
197 },
198 label: "/skill:unslop",
199 description: "Cut AI tells from writing",
200 },
201 ] satisfies Array<
202 Extract<ComposerCommandItem, { type: "slash-command" | "provider-slash-command" | "skill" }>
203 >;
204
205 expect(slashCommandItemsForPromptPosition(items, false).map((item) => item.id)).toEqual([
206 "slash:model",
207 ]);
208 expect(slashCommandItemsForPromptPosition(items, true).map((item) => item.id)).toEqual([
209 "slash:model",
210 "skill:claudeAgent:unslop",
211 ]);
212 });
176});213});
apps/web/src/components/chat/composerSlashCommandSearch.ts+10
...@@ -12,6 +12,16 @@ type SlashSearchItem = Extract<...@@ -12,6 +12,16 @@ type SlashSearchItem = Extract<
12 { type: "slash-command" | "provider-slash-command" | "skill" }12 { type: "slash-command" | "provider-slash-command" | "skill" }
13>;13>;
1414
15export function slashCommandItemsForPromptPosition(
16 items: ReadonlyArray<SlashSearchItem>,
17 isAtPromptStart: boolean,
18): SlashSearchItem[] {
19 if (isAtPromptStart) {
20 return [...items];
21 }
22 return items.filter((item) => item.type !== "skill");
23}
24
15function scoreSlashCommandItem(item: SlashSearchItem, query: string): number | null {25function scoreSlashCommandItem(item: SlashSearchItem, query: string): number | null {
16 if (item.type === "skill") {26 if (item.type === "skill") {
17 if (query === "skill") {27 if (query === "skill") {