feat: disconnect a custom endpoint (DELETE /api/custom-provider + Disconnect in its editor)

Connecting was one-way: POST could only replace the base URL, and the only
escape was hand-editing settings.json. DELETE now mirrors POST — drop the
stored section, delete the reserved key, unregister the provider from
piModels() — and the connect editor grows a Disconnect beside Connect,
following the stored-key rows' Delete pattern (quiet text, one click, no
confirm, only once there is an endpoint to remove).
This commit is contained in:
Daniel Buckner
2026-10-27 15:56:17 -07:00
parent 1b01919482
commit 286a95ba04
2 changed files with 40 additions and 0 deletions
+14
View File
@@ -5,6 +5,8 @@
* fast), store the key, persist { baseUrl, models }, re-sync the * fast), store the key, persist { baseUrl, models }, re-sync the
* provider into piModels(). Discovery lives here because connecting * provider into piModels(). Discovery lives here because connecting
* and discovering are one user action. * and discovering are one user action.
* DELETE → disconnect: drop the stored config + the stored key and unregister
* the provider (the mirror image of POST; idempotent).
*/ */
import { import {
deleteKey, deleteKey,
@@ -56,3 +58,15 @@ export async function POST(request: Request) {
syncCustomProvider(next.customProvider); syncCustomProvider(next.customProvider);
return Response.json({ ok: true, models: next.customProvider?.models ?? [] }); return Response.json({ ok: true, models: next.customProvider?.models ?? [] });
} }
/** Disconnect: forget the endpoint AND its key, then unregister the provider.
* Deleting the section (rather than blanking baseUrl) keeps settings.json the
* same shape as a never-connected install. Idempotent by construction. */
export async function DELETE() {
const record = readSettingsRecord() ?? { version: 1 };
const { customProvider: _dropped, ...rest } = record;
const next = writeSettingsRecord(rest);
deleteKey(CUSTOM_PROVIDER.envVar);
syncCustomProvider(next.customProvider);
return Response.json({ ok: true });
}
+26
View File
@@ -444,6 +444,15 @@ export function KeysSection({
notifySettingsSaved("keys"); notifySettingsSaved("keys");
} }
/** Forget the custom endpoint (config + key server-side), same rhythm as
* deleteKey: fire, re-read, broadcast so the catalog narrows everywhere. */
async function disconnectCustom(): Promise<void> {
await fetch("/api/custom-provider", { method: "DELETE" }).catch(() => undefined);
setAddingCustom(false);
await reload();
notifySettingsSaved("keys");
}
if (loading || statuses === null) { if (loading || statuses === null) {
return <p className="px-6 py-5 text-xs text-muted-foreground/70">Loading keys…</p>; return <p className="px-6 py-5 text-xs text-muted-foreground/70">Loading keys…</p>;
} }
@@ -496,6 +505,7 @@ export function KeysSection({
await reload(); await reload();
notifySettingsSaved("keys"); notifySettingsSaved("keys");
}} }}
onDelete={disconnectCustom}
onCancel={() => setAddingCustom(false)} onCancel={() => setAddingCustom(false)}
/> />
)} )}
@@ -660,11 +670,13 @@ function CustomEndpointEditor({
keyPresent, keyPresent,
onSaved, onSaved,
onCancel, onCancel,
onDelete,
}: { }: {
baseUrl0: string; baseUrl0: string;
keyPresent: boolean; keyPresent: boolean;
onSaved: () => void | Promise<void>; onSaved: () => void | Promise<void>;
onCancel: () => void; onCancel: () => void;
onDelete?: () => void | Promise<void>;
}) { }) {
const [baseUrl, setBaseUrl] = useState(baseUrl0); const [baseUrl, setBaseUrl] = useState(baseUrl0);
const [key, setKey] = useState(""); const [key, setKey] = useState("");
@@ -742,6 +754,20 @@ function CustomEndpointEditor({
> >
Cancel Cancel
</button> </button>
{/* Disconnect rides beside Connect, exactly the stored-key rows' Delete:
quiet text, one click, no confirm — and only once there IS an
endpoint (a fresh add has nothing to delete). */}
{onDelete && baseUrl0.trim() && (
<button
type="button"
onClick={() => void onDelete()}
disabled={busy}
data-testid="custom-delete"
className="shrink-0 text-xs text-muted-foreground/70 underline-offset-2 hover:text-red-500 hover:underline"
>
Disconnect
</button>
)}
</div> </div>
{needsKey && ( {needsKey && (
<p className="text-[11px] text-muted-foreground"> <p className="text-[11px] text-muted-foreground">