From 7a3ede69e05a91d080ab5748418f383fe2bf6e86 Mon Sep 17 00:00:00 2001 From: Quickbeard Date: Tue, 25 Aug 2026 11:36:08 +0700 Subject: [PATCH 1/2] fix(app): clear a provider's disabled mark when configure reconnects it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Disconnect provider" in Settings writes the provider id into the global config's disabled_providers, and that flag outranks everything the configure flows write: the provider registry skips a disabled provider entirely, so after a disconnect every SSO sign-in "succeeded" (auth entry written, provider block rewritten, success toast shown) while no provider ever connected — and the setup gate reopened its dialog right over the toast, forever, surviving restarts. codevhub config appeared to fix it only because the hub rewrites codev.json wholesale, dropping the flag. The app's custom-provider dialog already filtered its id back out of disabled_providers on save; the desktop SSO dialog, the TUI gateway flow, and the TUI custom-provider flow now do the same — each clears only its own provider id, so other deliberately disconnected providers stay disconnected. Reproduced against a live server: disconnect then the exact configure sequence (auth.set, config update, dispose) leaves connected empty until disabled_providers is cleared. The new TUI view test drives the custom-provider flow against a config with disabled_providers seeded and fails without the fix. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 11 +++++ .../components/gateway/dialog-configure.tsx | 15 +++++- .../tui/src/component/dialog-configure.tsx | 20 ++++++-- .../src/component/dialog-custom-provider.tsx | 19 ++++--- .../tui/test/custom-provider.view.test.tsx | 49 +++++++++++++++++++ 5 files changed, 102 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b78d4fdee5..1d088d06de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -438,6 +438,17 @@ already in use` and no way forward short of finding the file in a temporary ### Provider setup +- **Signing in after "Disconnect provider" works again** — disconnecting a + provider in Settings marks it disabled in the global config, and that mark + outranked everything the sign-in flow wrote: logging in again rewrote the + provider setup but never cleared the mark, so no provider ever came back, + and the setup dialog reopened right over its own "CoDev configured" toast — + every time, surviving restarts, until something rewrote the config file + wholesale (which is why `codevhub config` fixed it). Completing a sign-in — + SSO or custom provider, desktop app or terminal — now clears that provider's + disabled mark, and only its own: other providers you disconnected on purpose + stay disconnected. + ([#324](https://github.com/quickbeard/codev-code/pull/324)) - **The gateway API key no longer sits in codev.json** — every configure flow (the TUI and desktop SSO login, the manual-key fallback) wrote the key in plain sight into the global `codev.json`, the same file people open to diff --git a/packages/app/src/components/gateway/dialog-configure.tsx b/packages/app/src/components/gateway/dialog-configure.tsx index a5ee62fa66..bff75d4828 100644 --- a/packages/app/src/components/gateway/dialog-configure.tsx +++ b/packages/app/src/components/gateway/dialog-configure.tsx @@ -126,11 +126,24 @@ export function DialogConfigure(props: { dismissable?: boolean } = {}) { // — codev.json must not carry the credential. Auth first: a keyless // config with no auth entry is a provider that 401s on the first chat. await client.auth.set({ providerID: result.providerID, auth: { type: "api", key: result.apiKey } }) + // A provider the user disconnected earlier sits in disabled_providers, + // which outranks everything this flow writes: the registry skips it, the + // gate sees nothing connected, and this dialog reopens right over the + // success toast — forever, since each retry rewrites the block but never + // the flag. Same guard as the custom-provider dialog's save. + const disabled = serverSync().data.config.disabled_providers ?? [] // Raw update instead of serverSync().updateConfig: its onSuccess refetch // races the forked server-side instance reload. The explicit dispose // below emits global.disposed, which refreshes every window (mirrors the // TUI's update -> dispose -> bootstrap ordering). - await client.global.config.update({ config: result.config as Config }) + await client.global.config.update({ + config: { + ...result.config, + ...(disabled.includes(result.providerID) + ? { disabled_providers: disabled.filter((id) => id !== result.providerID) } + : {}), + } as Config, + }) await client.global.dispose() const arrived = await waitForModel(result.providerID, model) if (arrived) { diff --git a/packages/tui/src/component/dialog-configure.tsx b/packages/tui/src/component/dialog-configure.tsx index 54fa84eb2f..c36698964d 100644 --- a/packages/tui/src/component/dialog-configure.tsx +++ b/packages/tui/src/component/dialog-configure.tsx @@ -325,12 +325,22 @@ async function runModelFlow(ctx: FlowContext, creds: GatewayCreds) { // codev.json must not carry the credential. Auth first: a keyless config // with no auth entry is a provider that 401s on the first chat. await ctx.sdk.client.auth.set({ providerID: GATEWAY_PROVIDER.id, auth: { type: "api", key: creds.apiKey } }) + // A provider the user disconnected earlier sits in disabled_providers, + // which outranks everything this flow writes: the registry skips it, no + // provider connects, and the gate reopens right over the success toast — + // forever, since each retry rewrites the block but never the flag. + const disabled = ctx.sync.data.config.disabled_providers ?? [] const result = await ctx.sdk.client.global.config.update({ - config: buildGatewayConfig({ - baseUrl: creds.gatewayBaseUrl, - model, - models, - }), + config: { + ...buildGatewayConfig({ + baseUrl: creds.gatewayBaseUrl, + model, + models, + }), + ...(disabled.includes(GATEWAY_PROVIDER.id) + ? { disabled_providers: disabled.filter((id) => id !== GATEWAY_PROVIDER.id) } + : {}), + }, }) if (result.error) throw new Error(JSON.stringify(result.error)) }) diff --git a/packages/tui/src/component/dialog-custom-provider.tsx b/packages/tui/src/component/dialog-custom-provider.tsx index 14ae8b899a..7cf9b8750b 100644 --- a/packages/tui/src/component/dialog-custom-provider.tsx +++ b/packages/tui/src/component/dialog-custom-provider.tsx @@ -57,13 +57,20 @@ export async function runCustomProviderFlow(ctx: CustomProviderFlowContext) { if (models) { setStatus(`Configuring ${provider.id}...`) + // Reconnecting a provider the user disconnected earlier must clear it from + // disabled_providers, or the flag keeps the rewritten block dead (the app's + // custom-provider dialog does the same on save). + const disabled = ctx.sync.data.config.disabled_providers ?? [] const result = await ctx.sdk.client.global.config.update({ - config: buildCustomProviderConfig({ - providerID: provider.id, - name: provider.name, - baseUrl: baseUrl.trim(), - models, - }), + config: { + ...buildCustomProviderConfig({ + providerID: provider.id, + name: provider.name, + baseUrl: baseUrl.trim(), + models, + }), + ...(disabled.includes(provider.id) ? { disabled_providers: disabled.filter((id) => id !== provider.id) } : {}), + }, }) if (result.error) { ctx.toast.show({ variant: "error", message: JSON.stringify(result.error) }) diff --git a/packages/tui/test/custom-provider.view.test.tsx b/packages/tui/test/custom-provider.view.test.tsx index 39ca1cb8b1..532fdb5148 100644 --- a/packages/tui/test/custom-provider.view.test.tsx +++ b/packages/tui/test/custom-provider.view.test.tsx @@ -223,6 +223,55 @@ test("Custom provider with a base URL discovers models and writes the codev.json } }, 60_000) +// A provider the user disconnected earlier sits in disabled_providers, which +// outranks the rewritten block: the registry skips it and the gate reopens +// over the success toast, forever. Reconnecting must clear the flag — and only +// its own id, not another provider's opt-out. +test("reconnecting a disconnected provider clears it from disabled_providers", async () => { + const models = Bun.serve({ + port: 0, + fetch: () => Response.json({ data: [{ id: "mock-small" }] }), + }) + const modelsBase = `http://127.0.0.1:${models.port}` + let configured = false + const provider = { id: "mockgw", name: "mockgw", models: { "mock-small": { name: "mock-small" } } } + const boot_ = await boot( + (url) => { + if (url.pathname === "/config") return json({ disabled_providers: ["mockgw", "othergw"] }) + if (url.pathname === "/config/providers") + return json( + configured ? { providers: [provider], default: { mockgw: "mock-small" } } : { providers: [], default: {} }, + ) + if (url.pathname === "/provider") + return json( + configured + ? { all: [provider], default: {}, connected: ["mockgw"] } + : { all: [], default: {}, connected: [] }, + ) + return undefined + }, + () => (configured = true), + ) + try { + await waitFrame(boot_.setup, (f) => f.includes(GATE)) + boot_.setup.mockInput.pressArrow("down") // Custom provider + boot_.setup.mockInput.pressEnter() + + await waitFrame(boot_.setup, (f) => f.includes("Provider name")) + await answer(boot_.setup, "mockgw") + await waitFrame(boot_.setup, (f) => f.includes("API key")) + await answer(boot_.setup, "sk-mock") + await waitFrame(boot_.setup, (f) => f.includes("OpenAI-compatible endpoint")) + await answer(boot_.setup, modelsBase) + + await waitUntil(boot_.setup, () => boot_.captured.config.length > 0) + expect(boot_.captured.config[0].disabled_providers).toEqual(["othergw"]) + } finally { + models.stop(true) + await teardown(boot_) + } +}, 60_000) + // The Windows path: ctrl+v arrives as a key event (no bracketed paste), so the // dialog's paste command must read the clipboard itself. CR/CRLF from the // Windows clipboard must not survive into the field. From 152b3f4a55e72577301ed50b58ac80ceb66a5ea7 Mon Sep 17 00:00:00 2001 From: Quickbeard Date: Tue, 25 Aug 2026 11:36:53 +0700 Subject: [PATCH 2/2] docs: point the changelog entry at the real PR number --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d088d06de..fbbef2b81f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -448,7 +448,7 @@ already in use` and no way forward short of finding the file in a temporary SSO or custom provider, desktop app or terminal — now clears that provider's disabled mark, and only its own: other providers you disconnected on purpose stay disconnected. - ([#324](https://github.com/quickbeard/codev-code/pull/324)) + ([#325](https://github.com/quickbeard/codev-code/pull/325)) - **The gateway API key no longer sits in codev.json** — every configure flow (the TUI and desktop SSO login, the manual-key fallback) wrote the key in plain sight into the global `codev.json`, the same file people open to