diff --git a/ui/src/pages/model-setup/model-setup-page.ts b/ui/src/pages/model-setup/model-setup-page.ts index c6df16bb210c..5dcf42a039d2 100644 --- a/ui/src/pages/model-setup/model-setup-page.ts +++ b/ui/src/pages/model-setup/model-setup-page.ts @@ -1,5 +1,5 @@ import { consume } from "@lit/context"; -import { initialState, Task } from "@lit/task"; +import { initialState, Task, TaskStatus } from "@lit/task"; import { html, type PropertyValues } from "lit"; import { property, state } from "lit/decorators.js"; import type { GatewayBrowserClient } from "../../api/gateway.ts"; @@ -305,15 +305,7 @@ export class ModelSetupPage extends OpenClawLightDomElement { }; if (!this.observedConnection) { this.observedConnection = connection; - if ( - connection.connected && - this.routeData && - (this.routeData.connection.client !== connection.client || - this.routeData.connection.hello !== connection.hello || - this.routeData.connection.agentId !== connection.agentId) - ) { - void this.detect(); - } + this.ensureRouteSettledDetection(); return; } if ( @@ -322,6 +314,7 @@ export class ModelSetupPage extends OpenClawLightDomElement { connection.agentId === this.observedConnection.agentId && connection.connected === this.observedConnection.connected ) { + this.ensureRouteSettledDetection(); return; } this.observedConnection = connection; @@ -344,6 +337,24 @@ export class ModelSetupPage extends OpenClawLightDomElement { } } + // Route data can settle after mount and be discarded as another + // connection's result. Nothing else re-arms detection then, so a loading + // page with a connected, capable Gateway self-heals here instead of + // dead-ending silently. + private ensureRouteSettledDetection(): void { + if ( + !this.hasUpdated || + !this.routeData || + this.pageState.phase !== "loading" || + this.detectTask.status !== TaskStatus.INITIAL + ) { + return; + } + if (this.canUseSetup(this.context.gateway.snapshot.client)) { + void this.detect(); + } + } + private canUseSetup(client: GatewayBrowserClient | null): client is GatewayBrowserClient { const snapshot = this.context.gateway.snapshot; return Boolean( diff --git a/ui/src/pages/model-setup/model-setup-reconnect.test.ts b/ui/src/pages/model-setup/model-setup-reconnect.test.ts index f39b062be26c..3d35a77a6228 100644 --- a/ui/src/pages/model-setup/model-setup-reconnect.test.ts +++ b/ui/src/pages/model-setup/model-setup-reconnect.test.ts @@ -122,6 +122,35 @@ describe("ModelSetupPage Gateway reconnect ownership", () => { vi.restoreAllMocks(); }); + it("recovers when stale route data settles after mounting under a connected gateway", async () => { + const { context, request, runtimeConfig } = createFixture(); + request.mockImplementation(async (method) => + method === "openclaw.setup.detect" ? detection : {}, + ); + const provider = createApplicationContextProvider(context); + const page = document.createElement("openclaw-model-setup-page") as TestModelSetupPage; + provider.append(page); + document.body.append(provider); + await page.updateComplete; + + expect(request).not.toHaveBeenCalled(); + + page.routeData = { + state: { phase: "loading" }, + connection: { client: null, hello: null, agentId: null }, + firstRun: false, + }; + await page.updateComplete; + + await vi.waitFor(() => { + expect( + request.mock.calls.filter(([method]) => method === "openclaw.setup.detect"), + ).toHaveLength(1); + expect(page.querySelector('[data-auth-choice="provider-auth"]')).not.toBeNull(); + }); + runtimeConfig.dispose(); + }); + it("does not expose stale route data when the page mounts during reconnect", async () => { const { client, context, request, runtimeConfig, setGatewayPhase } = createFixture(); request.mockImplementation(async (method) =>