Fix Studio model picker toolbar overflow (#7403)

* fix(studio): contain model picker toolbar

* fix(studio): preserve model picker tab icons
This commit is contained in:
Long Yixing 2026-07-24 21:20:57 +08:00 committed by GitHub
commit 1bd080208c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 37 additions and 11 deletions

View file

@ -301,12 +301,12 @@ const HUB_SECTION_TABS: { value: string; label: string; icon?: ReactNode }[] = [
{
value: "recommended",
label: "Recommended",
icon: <HugeiconsIcon icon={StarIcon} className="size-3.5" />,
icon: <HugeiconsIcon icon={StarIcon} className="size-3.5 shrink-0" />,
},
{
value: "downloaded",
label: "On Device",
icon: <HugeiconsIcon icon={Download01Icon} className="size-3.5" />,
icon: <HugeiconsIcon icon={Download01Icon} className="size-3.5 shrink-0" />,
},
];
@ -384,7 +384,9 @@ function ModelSelectorContent({
{
value: "connected",
label: "Connected",
icon: <HugeiconsIcon icon={CloudIcon} className="size-3.5" />,
icon: (
<HugeiconsIcon icon={CloudIcon} className="size-3.5 shrink-0" />
),
},
]
: HUB_SECTION_TABS,

View file

@ -3064,13 +3064,18 @@ export function HubModelPicker({
) : null}
</div>
{/* Section tabs then the format and sort dropdowns, packed left with one
uniform gap between every control. The box is sized so the last
dropdown still lands on Search Hub's edge. Dropdowns hide on Connected. */}
<div className="flex items-center gap-2">
{/* Keep the left-packed controls on one line while they fit, then wrap
whole groups before their intrinsic widths cross the picker edge.
Dropdowns hide on Connected. */}
<div
className={cn(
"flex flex-wrap items-center gap-2",
hasConnected ? "-mr-4" : "-mr-2",
)}
>
{sectionToggle}
{showConnected ? null : (
<div className="flex items-center gap-2">
<div className="flex max-w-full min-w-0 flex-wrap items-center gap-2">
<HubOptionMenu
value={formatFilter}
options={FORMAT_FILTER_OPTIONS}

View file

@ -29,7 +29,8 @@ export function PillTabs({
className?: string;
compact?: boolean;
/** Size each tab to its label instead of equal widths. The active tab carries
* the pill background directly (the toggle never animates). */
* the pill background directly (the toggle never animates). Tabs only shrink
* when their combined intrinsic width exceeds the available space. */
fit?: boolean;
}) {
const activeIndex = Math.max(
@ -84,7 +85,7 @@ export function PillTabs({
onClick={() => onValueChange(tab.value)}
className={cn(
"relative z-10 inline-flex items-center justify-center gap-1.5 rounded-full transition-colors",
fit ? "shrink-0" : "min-w-0 flex-1",
fit ? "min-w-0 shrink" : "min-w-0 flex-1",
compact ? "h-7 px-2.5 text-ui-11" : "h-9 px-3 text-ui-12p5",
value === tab.value
? "text-foreground"
@ -97,7 +98,7 @@ export function PillTabs({
)}
>
{tab.icon}
{tab.label}
<span className="min-w-0 truncate">{tab.label}</span>
</button>
))}
</div>

View file

@ -217,6 +217,24 @@ def test_local_picker_rows_require_chat_capability():
assert "row.capabilities.canChat" in memo.group(0)
def test_model_picker_toolbar_reflows_before_crossing_picker_edge():
"""The content-sized section tabs and fixed-width dropdowns must reflow,
while an oversized tab group must shrink labels but preserve its icons."""
picker = _read("features/model-picker/components/model-selector/pickers.tsx")
assert '"flex flex-wrap items-center gap-2"' in picker
assert 'hasConnected ? "-mr-4" : "-mr-2"' in picker
assert '"flex max-w-full min-w-0 flex-wrap items-center gap-2"' in picker
tabs = _read("features/model-picker/components/model-selector/pill-tabs.tsx")
assert 'fit ? "min-w-0 shrink" : "min-w-0 flex-1"' in tabs
assert '<span className="min-w-0 truncate">{tab.label}</span>' in tabs
selector = _read("features/model-picker/components/model-selector.tsx")
assert 'icon={StarIcon} className="size-3.5 shrink-0"' in selector
assert 'icon={Download01Icon} className="size-3.5 shrink-0"' in selector
assert 'icon={CloudIcon} className="size-3.5 shrink-0"' in selector
def test_native_picked_gguf_template_read_through_lease():
"""A native (picked / drag-drop) GGUF's path lives only in its signed lease,
and the picker chat-template GET has no lease plumbing, so the default