| author | |
| committer | |
| log | bba79cc254b65969bde6b6bfc3032c3b5b9316ae |
| tree | b33c3fe8ea3fcbd32f56c31649199252de5c391c |
| parent | 2921050c698fb195e2f5590b7cfab8fdfe0ec729 |
| signature | Signed by PGP key B5690EEEBB952194 |
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 | 146 | import { ComposerPlanFollowUpBanner } from "./ComposerPlanFollowUpBanner"; |
| 147 | 147 | import { ComposerControl, ComposerControlIcon, ComposerSelectControl } from "./ComposerControl"; |
| 148 | 148 | import { resolveComposerMenuActiveItemId } from "./composerMenuHighlight"; |
| 149 | import { searchSlashCommandItems } from "./composerSlashCommandSearch"; | |
| 149 | import { | |
| 150 | searchSlashCommandItems, | |
| 151 | slashCommandItemsForPromptPosition, | |
| 152 | } from "./composerSlashCommandSearch"; | |
| 150 | 153 | import { |
| 151 | 154 | getComposerPromptInjectionState, |
| 152 | 155 | getComposerProviderState, |
| ... | ... | @@ -1360,11 +1363,10 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 1360 | 1363 | skill.description ?? |
| 1361 | 1364 | (skill.scope ? `${skill.scope} skill` : ""), |
| 1362 | 1365 | })); |
| 1363 | const slashCommandItems = [ | |
| 1364 | ...builtInSlashCommandItems, | |
| 1365 | ...providerSlashCommandItems, | |
| 1366 | ...skillItems, | |
| 1367 | ]; | |
| 1366 | const slashCommandItems = slashCommandItemsForPromptPosition( | |
| 1367 | [...builtInSlashCommandItems, ...providerSlashCommandItems, ...skillItems], | |
| 1368 | composerTrigger.rangeStart === 0, | |
| 1369 | ); | |
| 1368 | 1370 | return searchSlashCommandItems(slashCommandItems, query); |
| 1369 | 1371 | } |
| 1370 | 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 | 2 | import { ProviderDriverKind } from "@t3tools/contracts"; |
| 3 | 3 | |
| 4 | 4 | import type { ComposerCommandItem } from "./ComposerCommandMenu"; |
| 5 | import { searchSlashCommandItems } from "./composerSlashCommandSearch"; | |
| 5 | import { | |
| 6 | searchSlashCommandItems, | |
| 7 | slashCommandItemsForPromptPosition, | |
| 8 | } from "./composerSlashCommandSearch"; | |
| 6 | 9 | |
| 7 | 10 | describe("searchSlashCommandItems", () => { |
| 8 | 11 | const claudeDriver = ProviderDriverKind.make("claudeAgent"); |
| ... | ... | @@ -173,4 +176,38 @@ describe("searchSlashCommandItems", () => { |
| 173 | 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 | 12 | { type: "slash-command" | "provider-slash-command" | "skill" } |
| 13 | 13 | >; |
| 14 | 14 | |
| 15 | export 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 | ||
| 15 | 25 | function scoreSlashCommandItem(item: SlashSearchItem, query: string): number | null { |
| 16 | 26 | if (item.type === "skill") { |
| 17 | 27 | if (query === "skill") { |