fix(dashboard): restore home topology live in-flight pulse (#3507) (#3667)

Closes #3507
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-11 12:55:32 -03:00
committed by GitHub
parent 7e7fdbd30d
commit 34c278324b
4 changed files with 106 additions and 1 deletions

View File

@@ -34,6 +34,7 @@
### 🔧 Bug Fixes
- **fix(home topology): restore live in-flight request pulse** ([#3507]): the animated "pulse" edges in the home Provider Topology panel went dead after PR #3401 unified request visibility, because `activeRequests` was hardcoded to `[]`. Re-wired to `useLiveRequests()` (the existing WebSocket hook on port 20129) so that every pending/running request drives the animation in real time. A pure `selectActiveRequests` mapping helper was extracted to `home/topologyUtils.ts` with 5 unit tests.
- **Electron desktop**: launch the peer-stamping `server-ws.mjs` entrypoint so local-only routes (AgentBridge, MCP, services) no longer return 403 LOCAL_ONLY (#3386)
- **Provider Topology**: stop flagging healthy providers as errored based on stale historical failures; use current request status (#3619)
- **OpenCode Free**: fetch the live model catalog from the provider's `modelsUrl` for the no-auth model picker instead of serving a stale hardcoded list (#3611)

View File

@@ -13,6 +13,8 @@ import { useNotificationStore } from "@/store/notificationStore";
import { copyToClipboard } from "@/shared/utils/clipboard";
import { getProviderDisplayLabel } from "@/shared/utils/providerDisplayLabel";
import { useIsElectron, useOpenExternal } from "@/shared/hooks/useElectron";
import { useLiveRequests } from "@/hooks/useLiveDashboard";
import { selectActiveRequests } from "../home/topologyUtils";
const ProviderTopology = dynamic(() => import("../home/ProviderTopology"), { ssr: false });
const ProviderQuotaWidget = dynamic(() => import("../home/ProviderQuotaWidget"), { ssr: false });
@@ -114,6 +116,9 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
Array<{ id?: string; prefix?: string; name?: string }>
>([]);
// Live in-flight requests for Provider Topology pulse animation (#3507)
const { activeRequests: liveActiveRequests } = useLiveRequests();
const [versionInfo, setVersionInfo] = useState<VersionInfo | null>(null);
const [updating, setUpdating] = useState(false);
@@ -1178,7 +1183,7 @@ export default function HomePageClient({ machineId }: HomePageClientProps) {
</div>
<ProviderTopology
providers={topologyProviders}
activeRequests={[]}
activeRequests={selectActiveRequests(liveActiveRequests)}
lastProvider={lastProvider}
errorProvider={errorProvider}
/>

View File

@@ -0,0 +1,27 @@
/**
* Pure helpers for the home-page Provider Topology panel.
*/
/** Minimal shape expected by <ProviderTopology activeRequests={...}> */
export interface TopologyActiveRequest {
provider: string;
model: string;
}
/** Minimal in-flight request shape (subset of LiveRequest from useLiveRequests) */
interface InFlightRequest {
provider: string;
model: string;
}
/**
* Maps an array of in-flight LiveRequest entries to the flat
* { provider, model }[] shape consumed by <ProviderTopology>.
*
* The input is expected to contain only pending/running entries — the
* useLiveRequests hook already filters out completed and failed requests
* before exposing them via `activeRequests`.
*/
export function selectActiveRequests(requests: InFlightRequest[]): TopologyActiveRequest[] {
return requests.map(({ provider, model }) => ({ provider, model }));
}

View File

@@ -0,0 +1,72 @@
/**
* #3507 — the home Provider Topology pulse was dead because activeRequests was
* hardcoded to []. Fix: map the in-flight LiveRequest entries (pending/running)
* from useLiveRequests to the { provider, model }[] shape expected by
* <ProviderTopology>.
*
* selectActiveRequests() is a pure mapping function — no React required.
*/
import test from "node:test";
import assert from "node:assert/strict";
import { selectActiveRequests } from "../../src/app/(dashboard)/home/topologyUtils.ts";
// ── helpers ──────────────────────────────────────────────────────────────────
function makeRequest(
overrides: Partial<{
id: string;
model: string;
provider: string;
status: "pending" | "running" | "success" | "error";
}>
) {
return {
id: "req-1",
model: "gpt-4",
provider: "openai",
timestamp: Date.now(),
status: "pending" as const,
...overrides,
};
}
// ── tests ─────────────────────────────────────────────────────────────────────
test("selectActiveRequests: maps pending requests to {provider, model}", () => {
const input = [makeRequest({ id: "a", provider: "anthropic", model: "claude-opus-4", status: "pending" })];
const result = selectActiveRequests(input);
assert.deepEqual(result, [{ provider: "anthropic", model: "claude-opus-4" }]);
});
test("selectActiveRequests: maps running requests to {provider, model}", () => {
const input = [makeRequest({ id: "b", provider: "openai", model: "gpt-4o", status: "running" })];
const result = selectActiveRequests(input);
assert.deepEqual(result, [{ provider: "openai", model: "gpt-4o" }]);
});
test("selectActiveRequests: returns empty array for empty input", () => {
assert.deepEqual(selectActiveRequests([]), []);
});
test("selectActiveRequests: maps multiple concurrent in-flight requests", () => {
const input = [
makeRequest({ id: "c1", provider: "anthropic", model: "claude-sonnet-4", status: "pending" }),
makeRequest({ id: "c2", provider: "gemini", model: "gemini-2.5-pro", status: "running" }),
];
const result = selectActiveRequests(input);
assert.deepEqual(result, [
{ provider: "anthropic", model: "claude-sonnet-4" },
{ provider: "gemini", model: "gemini-2.5-pro" },
]);
});
test("selectActiveRequests: only extracts provider and model fields (not id/timestamp/etc)", () => {
const input = [makeRequest({ id: "d", provider: "groq", model: "llama3-70b", status: "running" })];
const result = selectActiveRequests(input);
assert.equal(Object.keys(result[0]).length, 2);
assert.ok("provider" in result[0]);
assert.ok("model" in result[0]);
assert.ok(!("id" in result[0]));
assert.ok(!("timestamp" in result[0]));
});