diff --git a/apps/web/src/appSettings.test.ts b/apps/web/src/appSettings.test.ts index 8eb8b6937746a54ca6a458ae9178ab41d3db813e..b5dd31d308c974b4e93b97284b0643fea1555b8e 100644 --- a/apps/web/src/appSettings.test.ts +++ b/apps/web/src/appSettings.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { getAppModelOptions, normalizeCustomModelSlugs } from "./appSettings"; +import { getAppModelOptions, getSlashModelOptions, normalizeCustomModelSlugs } from "./appSettings"; describe("normalizeCustomModelSlugs", () => { it("normalizes aliases, removes built-ins, and deduplicates values", () => { @@ -40,3 +40,17 @@ describe("getAppModelOptions", () => { }); }); }); + +describe("getSlashModelOptions", () => { + it("includes saved custom model slugs for /model command suggestions", () => { + const options = getSlashModelOptions(["custom/internal-model"], "", "gpt-5.3-codex"); + + expect(options.some((option) => option.slug === "custom/internal-model")).toBe(true); + }); + + it("filters slash-model suggestions across built-in and custom model names", () => { + const options = getSlashModelOptions(["openai/gpt-oss-120b"], "oss", "gpt-5.3-codex"); + + expect(options.map((option) => option.slug)).toEqual(["openai/gpt-oss-120b"]); + }); +}); diff --git a/apps/web/src/appSettings.ts b/apps/web/src/appSettings.ts index 48a31c7a7cf66aa123a67571555a949397de66d7..2977b7fb3d4a223e44aa442dd79a6e69377f03d7 100644 --- a/apps/web/src/appSettings.ts +++ b/apps/web/src/appSettings.ts @@ -105,6 +105,24 @@ export function getAppModelOptions( return options; } +export function getSlashModelOptions( + customModels: readonly string[], + query: string, + selectedModel?: string | null, +): AppModelOption[] { + const normalizedQuery = query.trim().toLowerCase(); + const options = getAppModelOptions(customModels, selectedModel); + if (!normalizedQuery) { + return options; + } + + return options.filter((option) => { + const searchSlug = option.slug.toLowerCase(); + const searchName = option.name.toLowerCase(); + return searchSlug.includes(normalizedQuery) || searchName.includes(normalizedQuery); + }); +} + function emitChange(): void { for (const listener of listeners) { listener(); diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index abd8bbc202fd6dfbc28551776abfa3c5840ce138..a6f7ecc9dd592439205dab300222ca03fd2a5711 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -118,7 +118,7 @@ import { Toggle } from "./ui/toggle"; import { SidebarTrigger } from "./ui/sidebar"; import { newCommandId, newMessageId } from "~/lib/utils"; import { readNativeApi } from "~/nativeApi"; -import { getAppModelOptions, useAppSettings } from "../appSettings"; +import { getAppModelOptions, getSlashModelOptions, useAppSettings } from "../appSettings"; import { type ComposerImageAttachment, type DraftThreadEnvMode, @@ -632,6 +632,10 @@ export default function ChatView({ threadId }: ChatViewProps) { () => getAppModelOptions(settings.customCodexModels, selectedModel), [selectedModel, settings.customCodexModels], ); + const slashModelOptions = useMemo( + () => getSlashModelOptions(settings.customCodexModels, composerTrigger?.query ?? "", selectedModel), + [composerTrigger?.query, selectedModel, settings.customCodexModels], + ); const phase = derivePhase(activeThread?.session ?? null); const isSendBusy = sendPhase !== "idle"; const isPreparingWorktree = sendPhase === "preparing-worktree"; @@ -907,26 +911,14 @@ export default function ChatView({ threadId }: ChatViewProps) { ]; } - return modelOptions - .map(({ slug, name }) => ({ - slug, - name, - searchSlug: slug.toLowerCase(), - searchName: name.toLowerCase(), - })) - .filter(({ searchSlug, searchName }) => { - const query = composerTrigger.query.trim().toLowerCase(); - if (!query) return true; - return searchSlug.includes(query) || searchName.includes(query); - }) - .map(({ slug, name }) => ({ + return slashModelOptions.map(({ slug, name }) => ({ id: `model:${slug}`, type: "model" as const, model: slug, label: name, description: slug, })); - }, [composerTrigger, modelOptions, workspaceEntries]); + }, [composerTrigger, slashModelOptions, workspaceEntries]); const composerMenuOpen = Boolean(composerTrigger); const activeComposerMenuItem = useMemo( () =>