Provider auth redesign: joined segments, method panels, Vertex methods

Segmented track + inset per-method panel with its own Test & save footer.
Vertex gains the same treatment: Google Cloud login (default), service account,
and API key (express mode, Gemini-only with a clear error elsewhere).
This commit is contained in:
Rohit C Prasad
2026-07-26 21:10:59 -07:00
parent b3a2b130d2
commit f281b29ff1
5 changed files with 401 additions and 116 deletions
+3 -1
View File
@@ -1265,7 +1265,9 @@ export interface ProviderField {
help: string;
placeholder: string;
default?: string; // pre-filled editable value (e.g. an OpenAI-compatible vendor's endpoint)
choices?: { value: string; label: string }[]; // non-empty → segmented choice, not a text input
// Non-empty → segmented choice, not a text input. tag = tiny badge ("Easiest");
// desc = one-liner atop the method panel; command = copyable terminal command.
choices?: { value: string; label: string; tag?: string; desc?: string; command?: string }[];
show_when?: Record<string, string> | null; // render only while these fields hold these values
}
+144 -88
View File
@@ -4,6 +4,7 @@ import {
removeProvider,
setProvider,
verifyProvider,
type ProviderField as ProviderFieldT,
type ProviderInfo,
} from "../api";
import { openExternal } from "../tauri";
@@ -321,7 +322,73 @@ export function ProviderForm({
const label = "block text-[12px] text-muted mt-3 mb-1";
const input =
"w-full px-3 py-2 rounded-lg border bg-panel text-[13.5px] outline-none focus:border-accent";
const fieldsAll = info?.fields || [];
const keyed = fieldsAll.some((x) => x.secret);
// Cloud providers declare a segmented auth-method choice; the selected method's
// credential fields render inside a panel with its own Test & save footer.
const choice = fieldsAll.find((f) => f.choices && f.choices.length);
const method = choice ? ps.fields[choice.key] || choice.default || "" : "";
const selected = choice?.choices?.find((c) => c.value === method);
const methodFields = choice
? fieldsAll.filter(
(f) =>
f.show_when &&
Object.entries(f.show_when).every(([k, v]) => (ps.fields[k] || "") === v),
)
: [];
// Without a choice control, Test lives next to the required secret (the API key), or
// the first field for keyless providers (Ollama's Detect).
const requiredSecret = fieldsAll.find((x) => x.secret && x.required);
const testKey = requiredSecret ? requiredSecret.key : fieldsAll[0]?.key;
if (!sel) return null;
const fieldRow = (f: ProviderFieldT, testable: boolean) => (
<div key={f.key}>
<label className={label}>{f.label}</label>
<div className="flex gap-2">
<div className="relative flex-1 min-w-0">
<input
className={input + (ps.savedState && testable ? " border-ok pr-32" : " border-line")}
type={f.secret ? "password" : "text"}
placeholder={f.secret && ps.credentialed && !ps.dirty ? "••••••••" : f.placeholder}
value={ps.fields[f.key] || ""}
data-testid={`${tp}-field-${f.key}`}
onChange={(e) => ps.setFieldValue(f.key, e.target.value)}
onBlur={f.secret ? undefined : () => void ps.saveField(f.key)}
/>
{ps.fieldSaved === f.key && (
<span
className="absolute right-2 top-1/2 -translate-y-1/2 text-[11px] font-medium text-ok bg-okSoft rounded-full px-2 py-0.5 pointer-events-none"
data-testid={`${tp}-field-saved-${f.key}`}
>
Saved
</span>
)}
{/* §39: state lives IN the field — no status lines below. */}
{ps.savedState && testable && (
<span
className="absolute right-2 top-1/2 -translate-y-1/2 text-[11px] font-medium text-ok bg-okSoft rounded-full px-2 py-0.5 pointer-events-none"
data-testid={`${tp}-saved-pill`}
>
{info?.needs_key ? <> Tested &amp; saved</> : <> Detected</>}
</span>
)}
</div>
{testable && (
<button
className="px-4 rounded-lg border border-line text-[13px] font-medium text-ink hover:border-lineStrong shrink-0 disabled:opacity-40"
onClick={() => ps.runTestAndSave()}
disabled={ps.verify.state === "testing" || (!ps.secretFilled && !ps.credentialed)}
data-testid={`${tp}-test`}
>
{ps.verify.state === "testing" ? "…" : info?.needs_key ? "Test" : "Detect"}
</button>
)}
</div>
{f.help && <p className="text-[11.5px] text-faint mt-1">{f.help}</p>}
</div>
);
return (
<div>
<button className="text-[12.5px] text-muted hover:text-ink" onClick={ps.backToGallery} data-testid={`${tp}-back`}>
@@ -336,98 +403,87 @@ export function ProviderForm({
</div>
{info?.blurb && <p className="text-[11.5px] text-faint mt-1">{info.blurb}</p>}
{(info?.fields || []).map((f) => {
const keyed = (info?.fields || []).some((x) => x.secret);
// A keyed provider's base_url is an expert option — it renders BELOW the key-help
// line as its own advanced section (owner nit 2026-07-19), not inside the loop.
if (f.key === "base_url" && keyed) return null;
// Conditional fields (Bedrock's per-auth-method inputs) render only while their
// controlling field holds the matching value.
if (f.show_when && !Object.entries(f.show_when).every(([k, v]) => (ps.fields[k] || "") === v))
return null;
// Segmented choice (e.g. Bedrock's "Connect with"): a button per option, stored
// like any field value. Switching methods requires a fresh Test to save.
if (f.choices?.length)
return (
<div key={f.key}>
<label className={label}>{f.label}</label>
<div className="flex gap-1.5" role="radiogroup" aria-label={f.label}>
{f.choices.map((c) => {
const active = (ps.fields[f.key] || "") === c.value;
return (
<button
key={c.value}
role="radio"
aria-checked={active}
className={
"px-3 py-1.5 rounded-lg border text-[12.5px] transition-colors " +
(active
? "border-accent text-ink font-medium"
: "border-line text-muted hover:border-lineStrong")
}
data-testid={`${tp}-choice-${f.key}-${c.value}`}
onClick={() => ps.setFieldValue(f.key, c.value)}
>
{c.label}
</button>
);
})}
</div>
{f.help && <p className="text-[11.5px] text-faint mt-1">{f.help}</p>}
</div>
);
// Test lives next to the required secret (the API key) when there is one; cloud
// providers whose secrets are all optional (Bedrock, Vertex) test from the first
// field instead — their credentials may be ambient (~/.aws, ADC).
const requiredSecret = (info?.fields || []).find((x) => x.secret && x.required);
const testable = requiredSecret ? f.key === requiredSecret.key : f.key === (info?.fields || [])[0]?.key;
return (
<div key={f.key}>
<label className={label}>{f.label}</label>
<div className="flex gap-2">
<div className="relative flex-1 min-w-0">
<input
className={input + (ps.savedState && testable ? " border-ok pr-32" : " border-line")}
type={f.secret ? "password" : "text"}
placeholder={f.secret && ps.credentialed && !ps.dirty ? "••••••••" : f.placeholder}
value={ps.fields[f.key] || ""}
data-testid={`${tp}-field-${f.key}`}
onChange={(e) => ps.setFieldValue(f.key, e.target.value)}
onBlur={f.secret ? undefined : () => void ps.saveField(f.key)}
/>
{ps.fieldSaved === f.key && (
<span
className="absolute right-2 top-1/2 -translate-y-1/2 text-[11px] font-medium text-ok bg-okSoft rounded-full px-2 py-0.5 pointer-events-none"
data-testid={`${tp}-field-saved-${f.key}`}
>
Saved
</span>
)}
{/* §39: state lives IN the field — no status lines below. */}
{ps.savedState && testable && (
<span
className="absolute right-2 top-1/2 -translate-y-1/2 text-[11px] font-medium text-ok bg-okSoft rounded-full px-2 py-0.5 pointer-events-none"
data-testid={`${tp}-saved-pill`}
>
{info?.needs_key ? <> Tested &amp; saved</> : <> Detected</>}
</span>
)}
</div>
{testable && (
{fieldsAll
.filter(
(f) =>
!f.show_when &&
!(f.choices && f.choices.length) &&
!(f.key === "base_url" && keyed),
)
.map((f) => fieldRow(f, !choice && f.key === testKey))}
{/* Auth-method segmented control + the selected method's panel (owner call
2026-07-26): one joined track, then a soft inset card holding only that
method's description, fields, and its own Test & save footer. */}
{choice && (
<div>
<label className={label}>{choice.label}</label>
<div
className="inline-flex gap-0.5 rounded-[10px] border border-line bg-line/40 p-[3px]"
role="radiogroup"
aria-label={choice.label}
>
{(choice.choices || []).map((c) => {
const active = method === c.value;
return (
<button
className="px-4 rounded-lg border border-line text-[13px] font-medium text-ink hover:border-lineStrong shrink-0 disabled:opacity-40"
onClick={() => ps.runTestAndSave()}
disabled={ps.verify.state === "testing" || (!ps.secretFilled && !ps.credentialed)}
data-testid={`${tp}-test`}
key={c.value}
role="radio"
aria-checked={active}
className={
"flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[12.5px] whitespace-nowrap transition-colors " +
(active
? "bg-panel text-ink font-medium shadow-sm ring-1 ring-line"
: "text-muted hover:text-ink")
}
data-testid={`${tp}-choice-${choice.key}-${c.value}`}
onClick={() => ps.setFieldValue(choice.key, c.value)}
>
{ps.verify.state === "testing" ? "…" : info?.needs_key ? "Test" : "Detect"}
{c.label}
{c.tag && (
<span className="text-[9.5px] font-semibold uppercase tracking-wide text-accent bg-accentSoft rounded-full px-1.5 py-px">
{c.tag}
</span>
)}
</button>
)}
</div>
{f.help && <p className="text-[11.5px] text-faint mt-1">{f.help}</p>}
);
})}
</div>
);
})}
<div className="mt-2.5 rounded-xl border border-line bg-paper/60 px-4 pb-3.5 pt-3">
{selected?.desc && <p className="text-[12px] text-muted">{selected.desc}</p>}
{selected?.command && (
<button
className="mt-2.5 inline-flex items-center gap-2 rounded-lg border border-line bg-panel px-2.5 py-1.5 text-[12px] font-mono text-ink hover:border-lineStrong"
onClick={() => void navigator.clipboard?.writeText(selected.command || "")}
title="Copy command"
data-testid={`${tp}-cmd-copy`}
>
{selected.command}
<span className="font-sans text-[11px] text-faint"></span>
</button>
)}
{methodFields.map((f) => fieldRow(f, false))}
<div className="mt-3.5 flex items-center justify-between gap-3 border-t border-line pt-3">
{ps.savedState ? (
<span className="text-[11.5px] font-medium text-ok" data-testid={`${tp}-saved-pill`}>
Tested &amp; saved
</span>
) : (
<span className="text-[11.5px] text-faint">Runs one read-only check, then saves.</span>
)}
<button
className="shrink-0 rounded-lg border border-accent bg-accent px-4 py-1.5 text-[13px] font-medium text-white hover:brightness-105 disabled:opacity-40"
onClick={() => ps.runTestAndSave()}
disabled={ps.verify.state === "testing"}
data-testid={`${tp}-test`}
>
{ps.verify.state === "testing" ? "…" : <>Test &amp; save</>}
</button>
</div>
</div>
</div>
)}
{info?.needs_key && KEY_HELP[sel] && (
<p className="text-[11.5px] text-faint mt-2">