| author | |
| committer | |
| log | 09b32ac9dd43baab32b87c91f47f2a1651d1b1e8 |
| tree | 3a7336a1274931a4c2e18d1bddf2ddf773a33cc1 |
| parent | 253684e280d5250bef80891552a0ea9704701484 |
- add `getSlashModelOptions` to centralize `/model` suggestion filtering
- include custom saved model slugs in slash-command suggestions
- update ChatView to consume shared helper and add focused tests3 files changed, 40 insertions(+), 16 deletions(-)
apps/web/src/appSettings.test.ts+15-1| ... | @@ -1,6 +1,6 @@ | ... | @@ -1,6 +1,6 @@ |
| 1 | import { describe, expect, it } from "vitest"; | 1 | import { describe, expect, it } from "vitest"; |
| 2 | 2 | ||
| 3 | import { getAppModelOptions, normalizeCustomModelSlugs } from "./appSettings"; | 3 | import { getAppModelOptions, getSlashModelOptions, normalizeCustomModelSlugs } from "./appSettings"; |
| 4 | 4 | ||
| 5 | describe("normalizeCustomModelSlugs", () => { | 5 | describe("normalizeCustomModelSlugs", () => { |
| 6 | it("normalizes aliases, removes built-ins, and deduplicates values", () => { | 6 | it("normalizes aliases, removes built-ins, and deduplicates values", () => { |
| ... | @@ -40,3 +40,17 @@ describe("getAppModelOptions", () => { | ... | @@ -40,3 +40,17 @@ describe("getAppModelOptions", () => { |
| 40 | }); | 40 | }); |
| 41 | }); | 41 | }); |
| 42 | }); | 42 | }); |
| 43 | |||
| 44 | describe("getSlashModelOptions", () => { | ||
| 45 | it("includes saved custom model slugs for /model command suggestions", () => { | ||
| 46 | const options = getSlashModelOptions(["custom/internal-model"], "", "gpt-5.3-codex"); | ||
| 47 | |||
| 48 | expect(options.some((option) => option.slug === "custom/internal-model")).toBe(true); | ||
| 49 | }); | ||
| 50 | |||
| 51 | it("filters slash-model suggestions across built-in and custom model names", () => { | ||
| 52 | const options = getSlashModelOptions(["openai/gpt-oss-120b"], "oss", "gpt-5.3-codex"); | ||
| 53 | |||
| 54 | expect(options.map((option) => option.slug)).toEqual(["openai/gpt-oss-120b"]); | ||
| 55 | }); | ||
| 56 | }); |
apps/web/src/appSettings.ts+18| ... | @@ -105,6 +105,24 @@ export function getAppModelOptions( | ... | @@ -105,6 +105,24 @@ export function getAppModelOptions( |
| 105 | return options; | 105 | return options; |
| 106 | } | 106 | } |
| 107 | 107 | ||
| 108 | export function getSlashModelOptions( | ||
| 109 | customModels: readonly string[], | ||
| 110 | query: string, | ||
| 111 | selectedModel?: string | null, | ||
| 112 | ): AppModelOption[] { | ||
| 113 | const normalizedQuery = query.trim().toLowerCase(); | ||
| 114 | const options = getAppModelOptions(customModels, selectedModel); | ||
| 115 | if (!normalizedQuery) { | ||
| 116 | return options; | ||
| 117 | } | ||
| 118 | |||
| 119 | return options.filter((option) => { | ||
| 120 | const searchSlug = option.slug.toLowerCase(); | ||
| 121 | const searchName = option.name.toLowerCase(); | ||
| 122 | return searchSlug.includes(normalizedQuery) || searchName.includes(normalizedQuery); | ||
| 123 | }); | ||
| 124 | } | ||
| 125 | |||
| 108 | function emitChange(): void { | 126 | function emitChange(): void { |
| 109 | for (const listener of listeners) { | 127 | for (const listener of listeners) { |
| 110 | listener(); | 128 | listener(); |
apps/web/src/components/ChatView.tsx+7-15| ... | @@ -118,7 +118,7 @@ import { Toggle } from "./ui/toggle"; | ... | @@ -118,7 +118,7 @@ import { Toggle } from "./ui/toggle"; |
| 118 | import { SidebarTrigger } from "./ui/sidebar"; | 118 | import { SidebarTrigger } from "./ui/sidebar"; |
| 119 | import { newCommandId, newMessageId } from "~/lib/utils"; | 119 | import { newCommandId, newMessageId } from "~/lib/utils"; |
| 120 | import { readNativeApi } from "~/nativeApi"; | 120 | import { readNativeApi } from "~/nativeApi"; |
| 121 | import { getAppModelOptions, useAppSettings } from "../appSettings"; | 121 | import { getAppModelOptions, getSlashModelOptions, useAppSettings } from "../appSettings"; |
| 122 | import { | 122 | import { |
| 123 | type ComposerImageAttachment, | 123 | type ComposerImageAttachment, |
| 124 | type DraftThreadEnvMode, | 124 | type DraftThreadEnvMode, |
| ... | @@ -632,6 +632,10 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -632,6 +632,10 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 632 | () => getAppModelOptions(settings.customCodexModels, selectedModel), | 632 | () => getAppModelOptions(settings.customCodexModels, selectedModel), |
| 633 | [selectedModel, settings.customCodexModels], | 633 | [selectedModel, settings.customCodexModels], |
| 634 | ); | 634 | ); |
| 635 | const slashModelOptions = useMemo( | ||
| 636 | () => getSlashModelOptions(settings.customCodexModels, composerTrigger?.query ?? "", selectedModel), | ||
| 637 | [composerTrigger?.query, selectedModel, settings.customCodexModels], | ||
| 638 | ); | ||
| 635 | const phase = derivePhase(activeThread?.session ?? null); | 639 | const phase = derivePhase(activeThread?.session ?? null); |
| 636 | const isSendBusy = sendPhase !== "idle"; | 640 | const isSendBusy = sendPhase !== "idle"; |
| 637 | const isPreparingWorktree = sendPhase === "preparing-worktree"; | 641 | const isPreparingWorktree = sendPhase === "preparing-worktree"; |
| ... | @@ -907,26 +911,14 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -907,26 +911,14 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 907 | ]; | 911 | ]; |
| 908 | } | 912 | } |
| 909 | 913 | ||
| 910 | return modelOptions | 914 | return slashModelOptions.map(({ slug, name }) => ({ |
| 911 | .map(({ slug, name }) => ({ | ||
| 912 | slug, | ||
| 913 | name, | ||
| 914 | searchSlug: slug.toLowerCase(), | ||
| 915 | searchName: name.toLowerCase(), | ||
| 916 | })) | ||
| 917 | .filter(({ searchSlug, searchName }) => { | ||
| 918 | const query = composerTrigger.query.trim().toLowerCase(); | ||
| 919 | if (!query) return true; | ||
| 920 | return searchSlug.includes(query) || searchName.includes(query); | ||
| 921 | }) | ||
| 922 | .map(({ slug, name }) => ({ | ||
| 923 | id: `model:${slug}`, | 915 | id: `model:${slug}`, |
| 924 | type: "model" as const, | 916 | type: "model" as const, |
| 925 | model: slug, | 917 | model: slug, |
| 926 | label: name, | 918 | label: name, |
| 927 | description: slug, | 919 | description: slug, |
| 928 | })); | 920 | })); |
| 929 | }, [composerTrigger, modelOptions, workspaceEntries]); | 921 | }, [composerTrigger, slashModelOptions, workspaceEntries]); |
| 930 | const composerMenuOpen = Boolean(composerTrigger); | 922 | const composerMenuOpen = Boolean(composerTrigger); |
| 931 | const activeComposerMenuItem = useMemo( | 923 | const activeComposerMenuItem = useMemo( |
| 932 | () => | 924 | () => |