diff --git a/app-prefixable/src/context/providers.tsx b/app-prefixable/src/context/providers.tsx index d6ab82d5..98bce5bc 100644 --- a/app-prefixable/src/context/providers.tsx +++ b/app-prefixable/src/context/providers.tsx @@ -123,6 +123,7 @@ interface ProviderContextValue { } refetch: () => void connectProvider: (providerID: string, apiKey: string) => Promise + disconnectProvider: (providerID: string) => Promise startOAuth: (providerID: string, methodIndex: number) => Promise completeOAuth: (providerID: string, methodIndex: number, code?: string) => Promise } @@ -447,6 +448,34 @@ export function ProviderProvider(props: ParentProps) { } } + async function disconnectProvider(providerID: string): Promise { + try { + await client.auth.remove({ providerID }) + setStore("modelsByAgent", produce((state) => { + for (const [agent, model] of Object.entries(state)) { + if (model.providerID !== providerID) continue + delete state[agent] + } + })) + setStore("sessionModels", produce((state) => { + for (const [sessionID, model] of Object.entries(state)) { + if (model.providerID !== providerID) continue + delete state[sessionID] + } + })) + try { + await client.instance.dispose() + await refetchProviders() + } catch (e) { + console.error("Failed to refresh providers after disconnect:", e) + } + return true + } catch (e) { + console.error("Failed to disconnect provider:", e) + return false + } + } + async function startOAuth(providerID: string, methodIndex: number): Promise { try { const res = await client.provider.oauth.authorize({ @@ -526,6 +555,7 @@ export function ProviderProvider(props: ParentProps) { }, refetch, connectProvider, + disconnectProvider, startOAuth, completeOAuth, } diff --git a/app-prefixable/src/pages/settings.tsx b/app-prefixable/src/pages/settings.tsx index 89552493..6a16230c 100644 --- a/app-prefixable/src/pages/settings.tsx +++ b/app-prefixable/src/pages/settings.tsx @@ -51,6 +51,9 @@ export function Settings() { const [mcpLoading, setMcpLoading] = createSignal(null) const [mcpDeleting, setMcpDeleting] = createSignal(null) const [mcpToDelete, setMcpToDelete] = createSignal(null) + const [providerToDelete, setProviderToDelete] = createSignal(null) + const [providerDeleting, setProviderDeleting] = createSignal(null) + const [providerDeleteError, setProviderDeleteError] = createSignal(null) // Saved prompts const savedPrompts = useSavedPrompts() @@ -190,6 +193,7 @@ export function Settings() { const [oauthPending, setOauthPending] = createSignal<{ providerID: string providerName: string + updating: boolean methodIndex: number method: "auto" | "code" instructions: string @@ -217,6 +221,20 @@ export function Settings() { return providers.authMethods[id] || [] }) + const selectedProviderName = createMemo(() => { + const id = selectedProvider() + if (!id) return null + return getProviderDisplayName(id) + }) + + const selectedProviderConnected = createMemo(() => { + const id = selectedProvider() + if (!id) return false + return providers.connected.includes(id) + }) + + const providerActionBusy = createMemo(() => !!providerDeleting() || connecting() || !!oauthPending()) + // Popular providers shown first const popularProviders = ["opencode", "anthropic", "github-copilot", "openai", "google", "openrouter"] @@ -564,6 +582,7 @@ Add your project-specific instructions here. const key = apiKey().trim() if (!providerID || !key) return + const wasConnected = providers.connected.includes(providerID) setConnecting(true) setError(null) @@ -574,17 +593,55 @@ Add your project-specific instructions here. setConnecting(false) if (ok) { - setSuccess(`Connected to ${providerID}!`) + setSuccess(`${wasConnected ? "Updated" : "Connected to"} ${getProviderDisplayName(providerID)}!`) setApiKey("") setSelectedProvider(null) + setProviderSearch("") } else { setError("Failed to connect. Please check your API key.") } } + function startProviderEdit(providerID: string) { + if (providerActionBusy()) return + setError(null) + setSuccess(null) + setOauthPending(null) + setOauthCode("") + setCodeCopied(false) + setApiKey("") + setProviderSearch("") + setSelectedProvider(providerID) + } + + async function confirmProviderDelete() { + const providerID = providerToDelete() + if (!providerID || providerDeleting()) return + setProviderDeleting(providerID) + setProviderDeleteError(null) + setError(null) + setSuccess(null) + const ok = await providers.disconnectProvider(providerID) + setProviderDeleting(null) + if (ok) { + setProviderToDelete(null) + if (selectedProvider() === providerID) { + setSelectedProvider(null) + setApiKey("") + setOauthPending(null) + setOauthCode("") + setCodeCopied(false) + } + setSuccess(`Disconnected ${getProviderDisplayName(providerID)}.`) + return + } + setProviderDeleteError(`Failed to disconnect ${getProviderDisplayName(providerID)}. Please try again.`) + } + async function handleOAuthStart(providerID: string, methodIndex: number) { setError(null) setSuccess(null) + const updating = providers.connected.includes(providerID) const result = await providers.startOAuth(providerID, methodIndex) @@ -600,6 +657,7 @@ Add your project-specific instructions here. setOauthPending({ providerID, providerName, + updating, methodIndex, method: "code", instructions: result.instructions, @@ -612,6 +670,7 @@ Add your project-specific instructions here. setOauthPending({ providerID, providerName, + updating, methodIndex, method: "auto", instructions: result.instructions, @@ -630,7 +689,7 @@ Add your project-specific instructions here. setConnecting(false) if (ok) { - setSuccess(`Connected to ${providerName}!`) + setSuccess(`${updating ? "Updated" : "Connected to"} ${providerName}!`) setOauthPending(null) setSelectedProvider(null) setProviderSearch("") @@ -657,7 +716,7 @@ Add your project-specific instructions here. setConnecting(false) if (ok) { - setSuccess(`Connected to ${pending.providerName}!`) + setSuccess(`${pending.updating ? "Updated" : "Connected to"} ${pending.providerName}!`) setOauthPending(null) setOauthCode("") setSelectedProvider(null) @@ -1039,7 +1098,7 @@ Add your project-specific instructions here. {(providerID) => (
@@ -1050,9 +1109,39 @@ Add your project-specific instructions here. {getProviderDisplayName(providerID)}
- - Connected - +
+ + Connected + + + +
)}
@@ -1220,92 +1309,123 @@ Add your project-specific instructions here. {/* Search and Provider Selection */}
- {/* Search input */} -
- - setProviderSearch(e.currentTarget.value)} - placeholder="Search providers..." - class="w-full pl-9 pr-8 py-2 rounded-md text-sm" + +
- + > + Reconfiguring {selectedProviderName()} +
+
+ + {/* Search input */} + setProviderSearch("")} - class="absolute right-2 top-1/2 -translate-y-1/2 p-1" - style={{ color: "var(--text-weak)" }} + disabled={providerActionBusy()} + onClick={() => setSelectedProvider(null)} + class="text-sm" + style={{ + color: "var(--text-weak)", + ...(providerActionBusy() ? { opacity: "0.6", cursor: "not-allowed" } : {}), + }} > - + Choose a different provider - -
- - {/* Provider grid - max height with scroll */} -
- - {(provider) => ( + } + > +
+ + setProviderSearch(e.currentTarget.value)} + placeholder="Search providers..." + class="w-full pl-9 pr-8 py-2 rounded-md text-sm" + style={{ + background: "var(--background-base)", + border: "1px solid var(--border-base)", + color: "var(--text-base)", + }} + /> + - )} - -
+ +
- -

- No providers found matching "{providerSearch()}" -

-
+ {/* Provider grid - max height with scroll */} +
+ + {(provider) => ( + + )} + +
- !providers.connected.includes(p.id)).length === 0 && - !providerSearch() - } - > -

- All available providers are connected! -

+ +

+ No providers found matching "{providerSearch()}" +

+
+ + !providers.connected.includes(p.id)).length === 0 && + !providerSearch() + } + > +

+ All available providers are connected! +

+
@@ -1314,7 +1434,7 @@ Add your project-specific instructions here.
{/* Show auth method buttons */} @@ -1344,9 +1464,9 @@ Add your project-specific instructions here. color: "white", }} > - + - Connecting... + {selectedProviderConnected() ? "Updating..." : "Connecting..."}
@@ -1388,9 +1508,9 @@ Add your project-specific instructions here. color: "white", }} > - + - Connecting... + {selectedProviderConnected() ? "Updating..." : "Connecting..."} @@ -2603,6 +2723,23 @@ Add your project-specific instructions here. onCancel={() => setMcpToDelete(null)} /> + { + if (providerActionBusy()) return + setProviderToDelete(null) + setProviderDeleteError(null) + }} + /> + {/* Prompt Add/Edit Dialog */}