fix(dashboard): dedupe provider nodes by id on compatible-provider add (#4746) (#4768)

Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-23 08:23:43 -03:00
committed by GitHub
parent bd1bcb5aba
commit 5e1750ff07
4 changed files with 61 additions and 3 deletions

View File

@@ -14,6 +14,7 @@ _In development — bullets added per PR; finalized at release._
- **Onboarding**: add the missing `onboarding.tiers` step-title translation so the setup wizard no longer crashes with `MISSING_MESSAGE: onboarding.tiers` (#4698).
- **deepseek-web**: fold `role:"tool"` results into the single-prompt transcript (`messagesToPrompt`) so tool outputs reach the model instead of being silently dropped when a follow-up turn omits the `tools[]` array (#4712).
- **Dashboard**: remove the dead, unconditional `useLiveRequests()` call from `HomePageClient.tsx` — it crashed the `/home` page in production builds with `ReferenceError: useLiveRequests is not defined` (#4759, #4745) and opened the live-dashboard WebSocket even when Provider Topology was hidden (#4596). The live feed remains owned by the settings-gated `HomeProviderTopologySection`.
- **Providers dashboard**: dedupe provider nodes by id when adding a compatible provider (`upsertProviderNodeById`) so the same provider can no longer appear twice and no-op adds don't invalidate the compatible-provider memo (#4746).
---

View File

@@ -22,6 +22,7 @@ import {
filterConfiguredProviderEntries,
shouldFilterProviderEntriesForDisplayMode,
shouldShowFirstProviderHint,
upsertProviderNodeById,
} from "./providerPageUtils";
import type { ProviderEntry } from "./providerPageUtils";
import {
@@ -1745,7 +1746,7 @@ export default function ProvidersPage() {
mode="openai"
onClose={() => setShowAddCompatibleModal(false)}
onCreated={(node) => {
setProviderNodes((prev) => [...prev, node]);
setProviderNodes((prev) => upsertProviderNodeById(prev, node));
setShowAddCompatibleModal(false);
router.push(`/dashboard/providers/${node.id}`);
}}
@@ -1755,7 +1756,7 @@ export default function ProvidersPage() {
mode="anthropic"
onClose={() => setShowAddAnthropicCompatibleModal(false)}
onCreated={(node) => {
setProviderNodes((prev) => [...prev, node]);
setProviderNodes((prev) => upsertProviderNodeById(prev, node));
setShowAddAnthropicCompatibleModal(false);
router.push(`/dashboard/providers/${node.id}`);
}}
@@ -1767,7 +1768,7 @@ export default function ProvidersPage() {
title={addCcCompatibleLabel}
onClose={() => setShowAddCcCompatibleModal(false)}
onCreated={(node) => {
setProviderNodes((prev) => [...prev, node]);
setProviderNodes((prev) => upsertProviderNodeById(prev, node));
setShowAddCcCompatibleModal(false);
router.push(`/dashboard/providers/${node.id}`);
}}

View File

@@ -276,3 +276,24 @@ export function resolveDashboardProviderInfo(
): ResolvedProviderCatalogEntry | null {
return resolveProviderCatalogEntry(providerId, options);
}
/**
* Append or replace a provider node by `id`, never appending a duplicate (#4746).
*
* The compatible-provider "add" modals previously did `setProviderNodes((prev) => [...prev, node])`,
* so adding the same provider twice (refresh-then-add, double-click, retry, or React StrictMode
* double-invocation in dev) left the same `id` in the array twice — surfacing duplicate cards and
* invalidating the `compatibleProviderGroups` memo on every no-op add. This upsert dedups by id:
* - new id → append a new array,
* - same id, deep-equal payload → return `prev` unchanged (stable identity ⇒ memo does not re-run),
* - same id, changed payload → replace in place.
*/
export function upsertProviderNodeById<T extends { id?: string | null }>(prev: T[], node: T): T[] {
if (!node || node.id == null) return [...prev, node];
const idx = prev.findIndex((p) => p?.id === node.id);
if (idx === -1) return [...prev, node];
if (JSON.stringify(prev[idx]) === JSON.stringify(node)) return prev;
const next = prev.slice();
next[idx] = node;
return next;
}

View File

@@ -0,0 +1,35 @@
// #4746 — the compatible-provider "add" modals appended provider nodes with
// `setProviderNodes((prev) => [...prev, node])`, so the same provider id could land in the
// array more than once (refresh-then-add, double-click, retry, StrictMode double-invocation),
// producing duplicate cards and invalidating the compatibleProviderGroups memo on no-op adds.
// upsertProviderNodeById dedups by id and keeps array identity stable for no-op adds.
import { test } from "node:test";
import assert from "node:assert/strict";
import { upsertProviderNodeById } from "../../src/app/(dashboard)/dashboard/providers/providerPageUtils.ts";
test("appends a node with a new id (#4746)", () => {
const prev = [{ id: "a", name: "A" }];
const next = upsertProviderNodeById(prev, { id: "b", name: "B" });
assert.deepEqual(next.map((n) => n.id), ["a", "b"]);
});
test("does not append a duplicate id — same identical payload returns prev unchanged (#4746)", () => {
const prev = [{ id: "a", name: "A" }];
const next = upsertProviderNodeById(prev, { id: "a", name: "A" });
assert.equal(next.length, 1);
assert.equal(next, prev, "no-op add must keep the same array reference (memo stability)");
});
test("replaces an existing id when the payload changed (#4746)", () => {
const prev = [{ id: "a", name: "A" }, { id: "b", name: "B" }];
const next = upsertProviderNodeById(prev, { id: "a", name: "A2" });
assert.equal(next.length, 2);
assert.equal(next.find((n) => n.id === "a")?.name, "A2");
assert.notEqual(next, prev);
});
test("appends when id is missing/null (cannot dedup) (#4746)", () => {
const prev = [{ id: "a" }];
const next = upsertProviderNodeById(prev, { id: null } as { id: string | null });
assert.equal(next.length, 2);
});