From f4e6fb12c1ed446bc7b088a7d07b51c6de991af3 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Sat, 27 Jun 2026 18:15:37 +0900 Subject: feat(concurrency): provider dropdown instead of free-text input Replace the Add-form provider text input with a bound to newProviderId; disabled + 'No providers available' when there are no models. - App.svelte: pass models={store.models}; component test updated to the dropdown (selectOptions) + a no-models case (6 component tests). Verified: typecheck 0/0, 922 tests green, biome clean, build OK. --- src/features/concurrency/ui/ConcurrencyView.svelte | 55 ++++++++++++++++------ .../concurrency/ui/ConcurrencyView.test.ts | 54 ++++++++++++++++----- 2 files changed, 83 insertions(+), 26 deletions(-) (limited to 'src/features/concurrency/ui') diff --git a/src/features/concurrency/ui/ConcurrencyView.svelte b/src/features/concurrency/ui/ConcurrencyView.svelte index e52e184..1b52f55 100644 --- a/src/features/concurrency/ui/ConcurrencyView.svelte +++ b/src/features/concurrency/ui/ConcurrencyView.svelte @@ -4,6 +4,7 @@ import { type Badge, parseLimitInput, + providerOptions, summarizeLimits, summarizeStatus, viewConcurrencyLimits, @@ -19,11 +20,14 @@ import ConcurrencyLimitRow from "./ConcurrencyLimitRow.svelte"; let { + models, loadLimits, saveLimit, deleteLimit, loadStatus, }: { + /** Available models (`/`) — the source of provider ids for the Add dropdown. */ + models: readonly string[]; loadLimits: LoadConcurrencyLimits; saveLimit: SaveConcurrencyLimit; deleteLimit: DeleteConcurrencyLimit; @@ -43,23 +47,39 @@ let limitsError = $state(null); let hasLoadedLimits = $state(false); - // Add-form state. + // Add-form state. The provider id is chosen from a dropdown of known providers + // (derived from the available models + any already-configured limit providers). let newProviderId = $state(""); let newLimitInput = $state(""); let adding = $state(false); let addError = $state(null); + const providerOpts = $derived(providerOptions(models, limits)); const limitViews = $derived(viewConcurrencyLimits(limits)); const limitsSummary = $derived(summarizeLimits(limits)); const parsedNewLimit = $derived(parseLimitInput(newLimitInput)); - const trimmedProviderId = $derived(newProviderId.trim()); const canAdd = $derived( - trimmedProviderId !== "" && + newProviderId !== "" && parsedNewLimit !== null && - !limits.some((l) => l.providerId === trimmedProviderId) && + !limits.some((l) => l.providerId === newProviderId) && !adding, ); + // Keep the dropdown selection valid: default to the first option, and if the + // selected provider is removed from the options (e.g. its limit was deleted and + // it has no models), fall back to the first remaining option. Runs untracked so + // it doesn't loop on its own assignment. + $effect(() => { + const opts = providerOpts; + untrack(() => { + if (opts.length === 0) { + if (newProviderId !== "") newProviderId = ""; + return; + } + if (!opts.includes(newProviderId)) newProviderId = opts[0] ?? ""; + }); + }); + async function refreshLimits(): Promise { limitsLoading = true; limitsError = null; @@ -74,13 +94,12 @@ } async function handleAdd(): Promise { - if (parsedNewLimit === null || trimmedProviderId === "") return; + if (parsedNewLimit === null || newProviderId === "") return; adding = true; addError = null; - const result = await saveLimit(trimmedProviderId, parsedNewLimit); + const result = await saveLimit(newProviderId, parsedNewLimit); adding = false; if (result.ok) { - newProviderId = ""; newLimitInput = ""; void refreshLimits(); void refreshStatus(); @@ -191,15 +210,21 @@ }} >