From 3e5cbbb830aa7a505ebdcb95950a10c55463f49d Mon Sep 17 00:00:00 2001 From: morgmart <98432065+morgmart@users.noreply.github.com> Date: Thu, 20 Aug 2026 18:22:41 -0700 Subject: [PATCH 1/2] fix: start Databricks sign-in from Save --- src/features/providers/api/catalog.test.ts | 4 +- src/features/providers/api/catalog.ts | 4 +- .../providers/runtimeProviderConfig.test.ts | 3 +- .../providers/runtimeProviderConfig.ts | 5 +- .../providers/ui/ModelProviderRow.tsx | 74 +++++++--- .../settings/ui/ModelProviderPanels.tsx | 38 ++++- .../ui/__tests__/ModelProviderRow.test.tsx | 136 +++++++++++++++++- src/shared/i18n/locales/en/settings.json | 2 +- src/shared/i18n/locales/es/settings.json | 2 +- 9 files changed, 227 insertions(+), 41 deletions(-) diff --git a/src/features/providers/api/catalog.test.ts b/src/features/providers/api/catalog.test.ts index 9a06dc9d4..e2d42d040 100644 --- a/src/features/providers/api/catalog.test.ts +++ b/src/features/providers/api/catalog.test.ts @@ -154,7 +154,7 @@ describe("selectSetupCatalogModelProviders", () => { ).toEqual(["openai", "databricks_v2", "anthropic", "ollama"]); }); - it("selects only the editable Databricks host field", () => { + it("selects the Databricks setup fields when an editable host is available", () => { expect( selectDatabricksHostConfigProvider([ { @@ -175,6 +175,6 @@ describe("selectSetupCatalogModelProviders", () => { ], }, ])?.fields?.map((field) => field.key), - ).toEqual(["DATABRICKS_HOST"]); + ).toEqual(["DATABRICKS_HOST", "DATABRICKS_TOKEN"]); }); }); diff --git a/src/features/providers/api/catalog.ts b/src/features/providers/api/catalog.ts index ad9eeeaca..16f97da78 100644 --- a/src/features/providers/api/catalog.ts +++ b/src/features/providers/api/catalog.ts @@ -68,10 +68,10 @@ export function selectDatabricksHostConfigProvider( const entry = entries.find( (candidate) => candidate.id === SETUP_CATALOG_DATABRICKS_PROVIDER_ID, ); - const fields = entry?.fields?.filter( + const hasHostField = entry?.fields?.some( (field) => field.key === SETUP_CATALOG_DATABRICKS_HOST_FIELD_KEY, ); - return entry && fields?.length ? { ...entry, fields } : null; + return entry && hasHostField ? entry : null; } export async function listProviderSetupCatalog(): Promise< diff --git a/src/features/providers/runtimeProviderConfig.test.ts b/src/features/providers/runtimeProviderConfig.test.ts index 1a8e6dce9..4abbbcbec 100644 --- a/src/features/providers/runtimeProviderConfig.test.ts +++ b/src/features/providers/runtimeProviderConfig.test.ts @@ -261,7 +261,7 @@ describe("mergeRuntimeProviderCatalog", () => { expect(databricks.displayName).toBe("Databricks AI Gateway"); }); - it("keeps only the Databricks host field when runtime config has no endpoint env", () => { + it("keeps Databricks host and token fields when runtime config has no endpoint env", () => { const configWithoutEndpointEnv: RuntimeConfig = { ...DEFAULT_RUNTIME_CONFIG, goose: { @@ -301,6 +301,7 @@ describe("mergeRuntimeProviderCatalog", () => { expect(databricks?.fields?.map((field) => field.key)).toEqual([ "DATABRICKS_HOST", + "DATABRICKS_TOKEN", ]); }); }); diff --git a/src/features/providers/runtimeProviderConfig.ts b/src/features/providers/runtimeProviderConfig.ts index 55f2beced..3a6b3cf66 100644 --- a/src/features/providers/runtimeProviderConfig.ts +++ b/src/features/providers/runtimeProviderConfig.ts @@ -16,7 +16,6 @@ import type { ProviderCatalogEntry } from "@/shared/types/providers"; const GOOSE_AGENT_PROVIDER_ID = "goose"; const DATABRICKS_PROVIDER_ID = "databricks_v2"; -const DATABRICKS_HOST_FIELD_KEY = "DATABRICKS_HOST"; const DEFAULT_MODEL_INVENTORY_MODE: RuntimeModelInventoryMode = "authoritative"; export function defaultModelInventoryModeForLoadResult( @@ -108,9 +107,7 @@ export function mergeRuntimeProviderCatalog( if (databricksCatalogEntry) { databricksCatalogEntry.fields = databricks.endpointEnv ? undefined - : databricksSetupEntry.fields.filter( - (field) => field.key === DATABRICKS_HOST_FIELD_KEY, - ); + : databricksSetupEntry.fields; } } diff --git a/src/features/providers/ui/ModelProviderRow.tsx b/src/features/providers/ui/ModelProviderRow.tsx index 5babac78c..f2cabc5a4 100644 --- a/src/features/providers/ui/ModelProviderRow.tsx +++ b/src/features/providers/ui/ModelProviderRow.tsx @@ -330,7 +330,7 @@ export function ModelProviderRow({ panelRef.current?.focus({ preventScroll: true }); }); - function runNativeConnect() { + async function runNativeConnect() { if (!provider.nativeConnectQuery) { return; } @@ -340,13 +340,22 @@ export function ModelProviderRow({ setError(""); setShowSavedState(false); - // Kick off the backend-owned `goose configure` sign-in; the store mirrors - // its progress and the success effect runs the post-success refresh. The - // operation keeps running (and is observable) even if this row unmounts or - // the window reloads. - void startSetup(provider.id, { - providerLabel: provider.nativeConnectQuery, - }); + try { + // Kick off the backend-owned Berd sign-in; the store mirrors its progress + // and the success effect runs the post-success refresh. The operation + // keeps running (and is observable) even if this row unmounts or the + // window reloads. + await startSetup(provider.id, { + providerLabel: provider.nativeConnectQuery, + }); + } catch (nextError) { + setOperation(provider.id, { + phase: "idle", + status: "failed", + output: setupOutputLines, + error: formatAcpErrorMessage(nextError, "Couldn't start sign-in"), + }); + } } function handleExpandedChange(nextExpanded: boolean) { @@ -438,24 +447,39 @@ export function ModelProviderRow({ return nextValue !== (currentValue.value ?? ""); }); - if (fieldsToSave.length === 0) { - setError(""); - return; - } + const shouldStartNativeAuthentication = + provider.setupMethod === "host_with_oauth_fallback" && + supportsNativeConnect && + !fields.some( + (field) => + field.secret && (draftValues[field.key]?.trim() ?? "").length > 0, + ); setError(""); try { - await onSaveFields( - fieldsToSave.map((field) => ({ - key: field.key, - value: draftValues[field.key]?.trim() ?? "", - isSecret: field.secret, - })), - ); - fieldsToSave.forEach((field) => { - dirtyDraftKeys.current.delete(field.key); - }); - void loadConfig(); + if (fieldsToSave.length > 0) { + await onSaveFields( + fieldsToSave.map((field) => ({ + key: field.key, + value: draftValues[field.key]?.trim() ?? "", + isSecret: field.secret, + })), + ); + fieldsToSave.forEach((field) => { + dirtyDraftKeys.current.delete(field.key); + }); + void loadConfig(); + } + + if (shouldStartNativeAuthentication) { + await runNativeConnect(); + return; + } + + if (fieldsToSave.length === 0) { + return; + } + onProviderConnected?.(provider.id); setShowSavedState(false); } catch (nextError) { @@ -602,6 +626,10 @@ export function ModelProviderRow({ error={error} setupMethod={provider.setupMethod} setupMessage={setupMessage} + authenticating={authenticating} + setupOutputLines={setupOutputLines} + setupOutputRef={outputRef} + setupError={setupError} onDraftChange={handleDraftChange} onSaveSetup={() => void handleSaveSetup()} /> diff --git a/src/features/settings/ui/ModelProviderPanels.tsx b/src/features/settings/ui/ModelProviderPanels.tsx index 2020d3301..3b4c97a77 100644 --- a/src/features/settings/ui/ModelProviderPanels.tsx +++ b/src/features/settings/ui/ModelProviderPanels.tsx @@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next"; import { Button } from "@/shared/ui/button"; import { Input } from "@/shared/ui/input"; import { Spinner } from "@/shared/ui/spinner"; +import { ProviderSetupOutput } from "@/features/settings/ui/ProviderSetupOutput"; import type { ProviderField, ProviderFieldValue, @@ -240,6 +241,10 @@ interface SetupFieldsPanelProps { error: string; setupMethod: ProviderSetupMethod; setupMessage: string | null; + authenticating: boolean; + setupOutputLines: string[]; + setupOutputRef: RefObject; + setupError: string; onDraftChange: (key: string, value: string) => void; onSaveSetup: () => void; } @@ -256,23 +261,30 @@ export function SetupFieldsPanel({ error, setupMethod, setupMessage, + authenticating, + setupOutputLines, + setupOutputRef, + setupError, onDraftChange, onSaveSetup, }: SetupFieldsPanelProps) { const { t } = useTranslation(["settings", "common"]); const showInlineSave = fields.length === 1; + const busy = saving || authenticating; const saveButton = (