mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-21 21:31:12 +02:00
fix: pin picker search input to top and connect providers in a single click
Bug 1: the composer picker search input lived inside the scrolling dropdown body as a sticky element, so container padding and Radix's open-focus scroll left it slightly below the top. Moved it out of the scroll area into a fixed header with the model list in an inner scroller, so it's flush at the top and always visible. Bug 2: Connect gated on resolvedModel, which is empty while the debounced key-change fetch is still in flight right after pasting a key, so the first click was a no-op. Connect now gates on credentials only, and the save handler resolves the model on demand (one fetch, same silent precedence) when it hasn't loaded yet. Save payload, models:test gate, and per-function fields unchanged.
This commit is contained in:
parent
1a8367ada4
commit
e74ce4e9a5
2 changed files with 40 additions and 5 deletions
|
|
@ -1532,12 +1532,17 @@ function ChatInputInner({
|
|||
<ChevronDown className="h-3 w-3 shrink-0" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="max-h-80 overflow-y-auto">
|
||||
<DropdownMenuContent align="end" className="p-0 overflow-hidden">
|
||||
{modelGroups.length === 0 && !standaloneDefault ? (
|
||||
<DropdownMenuItem disabled>Connect a provider in Settings</DropdownMenuItem>
|
||||
<div className="p-1">
|
||||
<DropdownMenuItem disabled>Connect a provider in Settings</DropdownMenuItem>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="sticky top-0 z-10 -mx-1 -mt-1 bg-popover p-1">
|
||||
{/* Fixed search header — lives OUTSIDE the scroll area (the
|
||||
inner div below scrolls), so it's flush at the very top
|
||||
and always visible without any scroll. */}
|
||||
<div className="bg-popover p-1">
|
||||
<input
|
||||
ref={modelFilterInputRef}
|
||||
value={modelFilter}
|
||||
|
|
@ -1553,6 +1558,7 @@ function ChatInputInner({
|
|||
className="h-7 w-full rounded-sm border border-input bg-transparent px-2 text-xs outline-none placeholder:text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-80 overflow-y-auto p-1 pt-0">
|
||||
<DropdownMenuRadioGroup
|
||||
value={activeModelKey || (defaultModel ? `${defaultModel.provider}/${defaultModel.model}` : '')}
|
||||
onValueChange={handleModelChange}
|
||||
|
|
@ -1609,6 +1615,7 @@ function ChatInputInner({
|
|||
<div className="px-2 py-1.5 text-xs text-muted-foreground">No models match</div>
|
||||
)}
|
||||
</DropdownMenuRadioGroup>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
|
|
|
|||
|
|
@ -414,8 +414,11 @@ function ModelSettings({ dialogOpen, rowboatConnected = false }: { dialogOpen: b
|
|||
}
|
||||
return saved
|
||||
})()
|
||||
// Gate Connect on credentials only, NOT on resolvedModel — that derives
|
||||
// from the live fetch, which hasn't settled the instant a key is pasted, so
|
||||
// gating on it made the first click a no-op (the model is resolved, fetching
|
||||
// on demand if needed, inside handleTestAndSave).
|
||||
const canTest =
|
||||
resolvedModel.length > 0 &&
|
||||
(!requiresApiKey || activeConfig.apiKey.trim().length > 0) &&
|
||||
(!requiresBaseURL || activeConfig.baseURL.trim().length > 0)
|
||||
|
||||
|
|
@ -567,10 +570,35 @@ function ModelSettings({ dialogOpen, rowboatConnected = false }: { dialogOpen: b
|
|||
if (!canTest) return
|
||||
setTestState({ status: "testing" })
|
||||
try {
|
||||
// Normally providerModels has loaded by click time and resolvedModel is
|
||||
// set. But a Connect click right after pasting a key can beat the
|
||||
// debounced key-change fetch — so when nothing is resolved yet, fetch
|
||||
// the list on demand (one fetch, then save) instead of forcing a second
|
||||
// click. Same silent precedence as resolvedModel, minus the customModel
|
||||
// branch (that path only runs when resolvedModel is already empty).
|
||||
let model = resolvedModel
|
||||
if (!model) {
|
||||
const listRes = await window.ipc.invoke("models:listForProvider", {
|
||||
provider: {
|
||||
flavor: provider,
|
||||
apiKey: activeConfig.apiKey.trim() || undefined,
|
||||
baseURL: activeConfig.baseURL.trim() || undefined,
|
||||
},
|
||||
})
|
||||
if (listRes.success && listRes.models && listRes.models.length > 0) {
|
||||
const preferred = preferredDefaults[provider]
|
||||
model = preferred && listRes.models.includes(preferred) ? preferred : listRes.models[0]
|
||||
}
|
||||
}
|
||||
if (!model) {
|
||||
setTestState({ status: "error", error: "Enter a model to connect" })
|
||||
toast.error("Enter a model to connect")
|
||||
return
|
||||
}
|
||||
// The silently resolved model takes the primary slot; the rest of the
|
||||
// saved list is preserved (same semantics setPrimaryModel had).
|
||||
const existing = activeConfig.models.map(m => m.trim())
|
||||
const allModels = [resolvedModel, ...existing.slice(1).filter(m => m && m !== resolvedModel)]
|
||||
const allModels = [model, ...existing.slice(1).filter(m => m && m !== model)]
|
||||
const providerConfig = {
|
||||
provider: {
|
||||
flavor: provider,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue