Merge remote-tracking branch 'origin/main' into studio-ssm-on-demand-followups

This commit is contained in:
Daniel Han 2026-10-15 23:19:47 -07:00
commit 4a6429a654
3 changed files with 77 additions and 25 deletions

View file

@ -1 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><g clip-path="url(#prefix__clip0_8_13)"><path fill-rule="evenodd" clip-rule="evenodd" d="M358.485 41.75l154.027 87.573v1.856l-155.605 86.634.362-45.162-17.514-.64c-22.592-.598-34.368.042-48.384 2.346-22.699 3.734-43.478 12.31-67.136 28.843l-46.208 32.107c-6.059 4.16-10.56 7.168-14.507 9.706l-10.987 6.87-8.469 4.992 8.213 4.906 11.307 7.211c10.155 6.699 24.96 16.981 57.621 39.808 23.68 16.533 44.438 25.109 67.136 28.843l6.4.96c14.806 1.941 29.334 2.005 60.267.704l.469-46.059 154.027 87.573v1.856l-155.605 86.656.298-39.722-13.546.469c-29.568.896-45.59.043-66.944-3.456-36.139-5.973-69.547-19.755-104.128-43.925l-46.038-32a467.072 467.072 0 00-16.106-10.624l-9.963-5.974c-5.38-3.1-10.785-6.157-16.213-9.173C62.037 314.24 12.01 301.141 0 301.141v-90.197l2.987.085c12.032-.149 62.08-13.269 81.258-23.978l21.675-12.374 9.344-5.845c9.131-5.973 22.869-15.488 57.301-39.531 34.582-24.17 67.968-37.973 104.128-43.925 24.576-4.053 42.112-4.544 81.366-2.944l.426-40.683z" fill="#000"/></g><defs><clipPath id="prefix__clip0_8_13"><path fill="#fff" d="M0 0h512v512H0z"/></clipPath></defs></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path fill="#C6FF00" fill-rule="evenodd" d="M226 555A226 226 0 0 1 226 103H497A136 136 0 0 1 497 375L631.7 509.8A26.5 26.5 0 0 1 613 555ZM226 193A136 136 0 1 0 226 465A136 136 0 1 0 226 193Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 262 B

Before After
Before After

View file

@ -20,7 +20,9 @@ interface ApiProviderLogoProps {
title?: string;
}
const DARK_INVERT_LOGOS = new Set(["openai", "openai_codex", "ollama", "openrouter", "liquid", "typesafe"]);
const DARK_INVERT_LOGOS = new Set(["openai", "openai_codex", "ollama", "liquid", "typesafe"]);
// Pale brand marks get a hairline edge on light surfaces instead of inverting.
const LIGHT_OUTLINE_LOGOS = new Set(["openrouter"]);
/** Shared hub or connection logo; monochrome ones invert in dark mode. */
export function ApiProviderLogo({ providerType, className, title }: ApiProviderLogoProps) {
@ -43,6 +45,9 @@ export function ApiProviderLogo({ providerType, className, title }: ApiProviderL
className={cn(
"shrink-0 object-contain",
providerType && DARK_INVERT_LOGOS.has(providerType) && "dark:invert",
providerType &&
LIGHT_OUTLINE_LOGOS.has(providerType) &&
"drop-shadow-[0_0_0.5px_rgb(0_0_0/0.7)] dark:drop-shadow-none",
className,
)}
/>

View file

@ -109,6 +109,25 @@ const CUSTOM_PROVIDER_MISSING_KEY_MESSAGE =
"No API key found. Add a valid API key for this connection.";
const HIDDEN_PROVIDER_TYPES = new Set(["qwen"]);
// Ends the open provider menu just above this item so the rows from it on need a scroll.
function capProviderMenuAtItem(item: HTMLElement | null) {
const content = item?.closest<HTMLElement>("[data-slot=select-content]");
if (!item || !content) return;
// offsetTop chain, not getBoundingClientRect: the open animation scales the menu.
let top = 0;
for (
let node: HTMLElement | null = item;
node && node !== content;
node = node.offsetParent as HTMLElement | null
) {
top += node.offsetTop;
}
const viewport = item.closest<HTMLElement>("[data-radix-select-viewport]");
top -= viewport?.scrollTop ?? 0;
top += Number.parseFloat(getComputedStyle(content).paddingBottom) || 0;
content.style.maxHeight = `min(var(--radix-select-content-available-height), ${top}px)`;
}
function parseManualModelIds(text: string): string[] {
const seen = new Set<string>();
const out: string[] = [];
@ -341,6 +360,21 @@ export function ChatProvidersSettings({
() => new Map(registry.map((entry) => [entry.provider_type, entry])),
[registry],
);
// Decision-only connections (TypeSafe, Liquid AI) sit last, below the menu's fold.
const menuRegistry = useMemo(() => {
const visible = registry.filter(
(entry) => !HIDDEN_PROVIDER_TYPES.has(entry.provider_type),
);
const isDecision = (entry: ProviderRegistryEntry) =>
isDecisionConnection({ providerType: entry.provider_type });
return [
...visible.filter((entry) => !isDecision(entry)),
...visible.filter(isDecision),
];
}, [registry]);
const firstBelowFoldProviderType = menuRegistry.find((entry) =>
isDecisionConnection({ providerType: entry.provider_type }),
)?.provider_type;
const selectedProviderContract = registryByType.get(
toExternalBackendProviderType(providerType),
);
@ -381,10 +415,19 @@ export function ChatProvidersSettings({
availableModels.length > 0;
const missingModelCatalogBaseUrl =
supportsRemoteModelCatalog(providerType) && baseUrlDraft.trim().length === 0;
const editingProviderHasSavedKey = Boolean(
editingProviderId &&
providers.find((provider) => provider.id === editingProviderId)?.hasApiKey,
);
const editingProvider = editingProviderId
? providers.find((provider) => provider.id === editingProviderId)
: undefined;
const editingProviderHasSavedKey = Boolean(editingProvider?.hasApiKey);
const selectedProviderLabel = isCustomProviderType(providerType)
? customProviderDisplayName(providerType)
: selectedProviderContract?.display_name;
// A saved connection reads as its name; only an unsaved draft is "New".
const formTitle = editingProviderId
? editingProvider?.name || selectedProviderLabel || "Edit"
: selectedProviderLabel
? `New ${selectedProviderLabel}`
: "New";
const missingModelCatalogApiKey =
!isCustomProvider &&
!isCuratedModelList &&
@ -1467,8 +1510,11 @@ export function ChatProvidersSettings({
Connections
</span>
<span className="size-1 rounded-full bg-muted-foreground/35" />
<span className="truncate text-xs font-medium text-muted-foreground">
{editingProviderId ? "Edit" : "New"}
<span
className="truncate text-xs font-medium text-muted-foreground"
title={formTitle}
>
{formTitle}
</span>
</div>
</header>
@ -1510,8 +1556,19 @@ export function ChatProvidersSettings({
>
<SelectValue placeholder="Choose a connection" />
</SelectTrigger>
<SelectContent>
{/* No scroll chevrons: the list scrolls by wheel, trackpad and keyboard. */}
<SelectContent className="[&_[data-slot=select-scroll-down-button]]:hidden [&_[data-slot=select-scroll-up-button]]:hidden">
<SelectGroup>
<SelectItem value={LEGACY_CUSTOM_PROVIDER_TYPE}>
<span className="flex min-w-0 items-center gap-2">
<ApiProviderLogo
providerType={LEGACY_CUSTOM_PROVIDER_TYPE}
className="size-4"
title={CUSTOM_PROVIDER_DISPLAY_NAME}
/>
<span className="truncate">{CUSTOM_PROVIDER_DISPLAY_NAME}</span>
</span>
</SelectItem>
{CUSTOM_PROVIDER_PRESETS.map((preset) => (
<SelectItem
key={preset.providerType}
@ -1527,28 +1584,18 @@ export function ChatProvidersSettings({
</span>
</SelectItem>
))}
<SelectItem value={LEGACY_CUSTOM_PROVIDER_TYPE}>
<span className="flex min-w-0 items-center gap-2">
<ApiProviderLogo
providerType={LEGACY_CUSTOM_PROVIDER_TYPE}
className="size-4"
title={CUSTOM_PROVIDER_DISPLAY_NAME}
/>
<span className="truncate">{CUSTOM_PROVIDER_DISPLAY_NAME}</span>
</span>
</SelectItem>
</SelectGroup>
<SelectSeparator />
<SelectGroup>
{registry
.filter(
(entry) =>
!HIDDEN_PROVIDER_TYPES.has(entry.provider_type),
)
.map((entry) => (
{menuRegistry.map((entry) => (
<SelectItem
key={entry.provider_type}
value={entry.provider_type}
ref={
entry.provider_type === firstBelowFoldProviderType
? capProviderMenuAtItem
: undefined
}
>
<span className="flex min-w-0 items-center gap-2">
<ApiProviderLogo