Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { screen } from "@testing-library/react";
import { screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { postMessage } from "../../vscode-api";
Expand Down Expand Up @@ -216,4 +216,40 @@ describe("モデル選択", () => {

expect(screen.getByText("Select model")).toBeInTheDocument();
});

it("検索でモデル一覧を絞り込めること", async () => {
await setupWithProviders();
const user = userEvent.setup();

await user.click(screen.getByText("Claude 4 Opus"));
const searchInput = screen.getByPlaceholderText("Search models...");
await user.type(searchInput, "sonnet");

const panel = searchInput.parentElement?.parentElement as HTMLElement;
expect(within(panel).getByText("Claude 4 Sonnet")).toBeInTheDocument();
expect(within(panel).queryByText("Claude 4 Opus")).not.toBeInTheDocument();
});

it("検索中は未接続プロバイダーのモデルは検索結果に表示されないこと", async () => {
await setupWithProviders();
const user = userEvent.setup();

await user.click(screen.getByText("Claude 4 Opus"));
await user.type(screen.getByPlaceholderText("Search models..."), "gpt");

expect(screen.queryByText("OpenAI")).not.toBeInTheDocument();
expect(screen.queryByText("GPT-5")).not.toBeInTheDocument();
expect(screen.getByText("No matching models")).toBeInTheDocument();
});

it("検索結果がない場合は空状態が表示されること", async () => {
await setupWithProviders();
const user = userEvent.setup();

await user.click(screen.getByText("Claude 4 Opus"));
await user.type(screen.getByPlaceholderText("Search models..."), "no-such-model");

expect(screen.getByText("No matching models")).toBeInTheDocument();
expect(screen.queryByText("Claude 4 Sonnet")).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,41 @@
border-radius: 3px;
}

.searchBox {
padding: 8px;
border-top: 1px solid var(--vscode-panel-border);
flex-shrink: 0;
}

.searchInput {
width: 100%;
box-sizing: border-box;
padding: 1px 6px;
border: 1px solid var(--vscode-input-border, transparent);
border-radius: 4px;
background-color: var(--vscode-input-background);
color: var(--vscode-input-foreground);
font-family: var(--vscode-font-family);
font-size: 11px;
outline: none;
}

.searchInput:focus {
border-color: var(--vscode-focusBorder);
}

.searchInput::placeholder {
color: var(--vscode-input-placeholderForeground, #aaaaaa);
font-size: 11px;
}

.noResults {
padding: 12px;
color: var(--vscode-descriptionForeground);
font-size: 12px;
text-align: center;
}

.section {
/* wrapper for provider group */
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export function ModelSelector({ providers, allProvidersData, selectedModel, onSe
const t = useLocale();
const [collapsedProviders, setCollapsedProviders] = useState<Set<string>>(new Set());
const [showAll, setShowAll] = useState(false);
const [searchQuery, setSearchQuery] = useState("");

// 表示用プロバイダーリスト: allProvidersData があればそちらを使い、なければ従来の providers を使う
const allDisplayProviders = useMemo(() => {
Expand Down Expand Up @@ -75,6 +76,35 @@ export function ModelSelector({ providers, allProvidersData, selectedModel, onSe

const hasDisconnected = useMemo(() => allDisplayProviders.some((p) => !p.connected), [allDisplayProviders]);

const normalizedSearchQuery = searchQuery.trim().toLowerCase();
const isSearching = normalizedSearchQuery.length > 0;

const visibleProviders = useMemo(() => {
if (!isSearching) return displayProviders;

return displayProviders
.map((provider) => {
const providerMatches =
provider.name.toLowerCase().includes(normalizedSearchQuery) ||
provider.id.toLowerCase().includes(normalizedSearchQuery);

const models = providerMatches
? provider.models
: provider.models.filter((model) => {
const modelName = model.name || "";
return (
modelName.toLowerCase().includes(normalizedSearchQuery) ||
model.id.toLowerCase().includes(normalizedSearchQuery)
);
});

return { ...provider, models };
})
.filter((provider) => provider.models.length > 0);
}, [displayProviders, isSearching, normalizedSearchQuery]);

const hasSearchResults = visibleProviders.length > 0;

const selectedModelName = useMemo(() => {
if (!selectedModel) return t["model.selectModel"];
for (const p of allDisplayProviders) {
Expand Down Expand Up @@ -107,14 +137,16 @@ export function ModelSelector({ providers, allProvidersData, selectedModel, onSe
panel={({ close }) => (
<div className={styles.panel}>
<div className={styles.panelBody}>
{displayProviders.map((provider) => {
{visibleProviders.map((provider) => {
if (provider.models.length === 0) return null;
const isCollapsed = collapsedProviders.has(provider.id);
const isCollapsed = !isSearching && collapsedProviders.has(provider.id);
return (
<div key={provider.id} className={styles.section}>
<div
className={`${styles.sectionTitle} ${!provider.connected ? styles.disconnected : ""}`}
onClick={() => toggleProvider(provider.id)}
onClick={() => {
if (!isSearching) toggleProvider(provider.id);
}}
>
<span className={`${styles.chevron} ${isCollapsed ? "" : styles.expanded}`}>
<ChevronRightIcon />
Expand Down Expand Up @@ -156,8 +188,17 @@ export function ModelSelector({ providers, allProvidersData, selectedModel, onSe
</div>
);
})}
{!hasSearchResults && <div className={styles.noResults}>{t["model.noSearchResults"]}</div>}
</div>
<div className={styles.searchBox}>
<input
className={styles.searchInput}
placeholder={t["model.searchPlaceholder"]}
value={searchQuery}
onChange={(event) => setSearchQuery(event.target.value)}
/>
</div>
{hasDisconnected && (
{!isSearching && hasDisconnected && (
<div className={styles.footer}>
<LinkButton
onClick={() => setShowAll((s) => !s)}
Expand Down
2 changes: 2 additions & 0 deletions packages/platforms/vscode/webview/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ export const en = {
"model.connectedOnly": "Connected only",
"model.showAll": "Show all providers",
"model.hideDisconnected": "Hide disconnected providers",
"model.searchPlaceholder": "Search models...",
"model.noSearchResults": "No matching models",

// AgentSelector
"agent.selectAgent": "Select agent",
Expand Down
2 changes: 2 additions & 0 deletions packages/platforms/vscode/webview/locales/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ export const es: LocaleSchema = {
"model.connectedOnly": "Solo conectados",
"model.showAll": "Mostrar todos los proveedores",
"model.hideDisconnected": "Ocultar proveedores desconectados",
"model.searchPlaceholder": "Buscar modelos...",
"model.noSearchResults": "No hay modelos coincidentes",

// AgentSelector
"agent.selectAgent": "Seleccionar agente",
Expand Down
2 changes: 2 additions & 0 deletions packages/platforms/vscode/webview/locales/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ export const ja: LocaleSchema = {
"model.connectedOnly": "接続済みのみ",
"model.showAll": "すべてのプロバイダーを表示",
"model.hideDisconnected": "未接続のプロバイダーを非表示",
"model.searchPlaceholder": "モデルを検索...",
"model.noSearchResults": "一致するモデルがありません",

// AgentSelector
"agent.selectAgent": "エージェントを選択",
Expand Down
2 changes: 2 additions & 0 deletions packages/platforms/vscode/webview/locales/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ export const ko: LocaleSchema = {
"model.connectedOnly": "연결됨만",
"model.showAll": "모든 제공자 표시",
"model.hideDisconnected": "연결 안 된 제공자 숨기기",
"model.searchPlaceholder": "모델 검색...",
"model.noSearchResults": "일치하는 모델이 없습니다",

// AgentSelector
"agent.selectAgent": "에이전트 선택",
Expand Down
2 changes: 2 additions & 0 deletions packages/platforms/vscode/webview/locales/pt-br.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ export const ptBr: LocaleSchema = {
"model.connectedOnly": "Apenas conectados",
"model.showAll": "Mostrar todos os provedores",
"model.hideDisconnected": "Ocultar provedores desconectados",
"model.searchPlaceholder": "Pesquisar modelos...",
"model.noSearchResults": "Nenhum modelo encontrado",

// AgentSelector
"agent.selectAgent": "Selecionar agente",
Expand Down
2 changes: 2 additions & 0 deletions packages/platforms/vscode/webview/locales/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ export const ru: LocaleSchema = {
"model.connectedOnly": "Только подключённые",
"model.showAll": "Показать всех провайдеров",
"model.hideDisconnected": "Скрыть отключённых провайдеров",
"model.searchPlaceholder": "Поиск моделей...",
"model.noSearchResults": "Нет подходящих моделей",

// AgentSelector
"agent.selectAgent": "Выбрать агента",
Expand Down
2 changes: 2 additions & 0 deletions packages/platforms/vscode/webview/locales/zh-cn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ export const zhCn: LocaleSchema = {
"model.connectedOnly": "仅已连接",
"model.showAll": "显示所有提供者",
"model.hideDisconnected": "隐藏未连接的提供者",
"model.searchPlaceholder": "搜索模型...",
"model.noSearchResults": "没有匹配的模型",

// AgentSelector
"agent.selectAgent": "选择代理",
Expand Down
2 changes: 2 additions & 0 deletions packages/platforms/vscode/webview/locales/zh-tw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ export const zhTw: LocaleSchema = {
"model.connectedOnly": "僅已連線",
"model.showAll": "顯示所有提供者",
"model.hideDisconnected": "隱藏未連線的提供者",
"model.searchPlaceholder": "搜尋模型...",
"model.noSearchResults": "找不到相符的模型",

// AgentSelector
"agent.selectAgent": "選擇代理",
Expand Down
Loading