diff --git a/ui/src/e2e/new-session-page.catalog-reconnect.e2e.test.ts b/ui/src/e2e/new-session-page.catalog-reconnect.e2e.test.ts index 96e3c465f790..5c385f50a8a9 100644 --- a/ui/src/e2e/new-session-page.catalog-reconnect.e2e.test.ts +++ b/ui/src/e2e/new-session-page.catalog-reconnect.e2e.test.ts @@ -62,6 +62,61 @@ function cliAgentCatalog(startTerminal: boolean) { } suite.define(() => { + it("waits for the current roster before loading the CLI catalog", async () => { + const context = await suite.browser.newContext({ + locale: "en-US", + serviceWorkers: "block", + }); + const page = await context.newPage(); + const gateway = await installMockGateway(page, { + assistantAgentId: "roboclaw", + assistantName: "Roboclaw", + cliAgentsEnabled: true, + defaultAgentId: "roboclaw", + deferredMethods: ["agents.list"], + featureMethods: [...TERMINAL_START_FEATURE_METHODS], + methodResponses: { + "sessions.catalog.list": { catalogs: [cliAgentCatalog(false)] }, + }, + }); + + try { + await page.goto(`${suite.server.baseUrl}new`); + await gateway.waitForRequest("agents.list"); + await page.locator(".new-session-page__message").waitFor({ state: "visible" }); + expect( + (await gateway.getRequests("sessions.catalog.list")) + .filter((request) => requestHasParam(request, "limitPerHost", 1)) + .map((request) => request.params), + ).toEqual([]); + + await gateway.resolveDeferred("agents.list"); + + await page.getByRole("heading", { name: "Roboclaw" }).waitFor(); + await expect + .poll(async () => + (await gateway.getRequests("sessions.catalog.list")).filter((request) => + requestHasParam(request, "limitPerHost", 1), + ), + ) + .toHaveLength(1); + const catalogRequest = (await gateway.getRequests("sessions.catalog.list")).find((request) => + requestHasParam(request, "limitPerHost", 1), + ); + expect(catalogRequest?.params).toEqual({ + agentId: "roboclaw", + limitPerHost: 1, + }); + + await page.locator('[data-chat-model-select="true"]').click(); + const cliGroup = page.locator('[data-chat-model-target-group="cliAgents"]'); + await expect.poll(() => cliGroup.isVisible()).toBe(true); + await pollLocatorText(cliGroup).toContain("Claude Code"); + } finally { + await context.close(); + } + }); + it("routes a Labs-enabled CLI agent picker row through catalog-target mode", async () => { if (captureCliAgentsProof) { await mkdir(cliAgentsProofDir, { recursive: true }); diff --git a/ui/src/pages/new-session/catalog-target.ts b/ui/src/pages/new-session/catalog-target.ts index 0b64d8a31234..ca3dacec2300 100644 --- a/ui/src/pages/new-session/catalog-target.ts +++ b/ui/src/pages/new-session/catalog-target.ts @@ -155,12 +155,12 @@ export function resolveAgentId( ): string { const rawRequested = data?.agentId?.trim(); if (!rawRequested) { - return normalizeAgentId(fallback); + return fallback && normalizeAgentId(fallback); } const requested = normalizeAgentId(rawRequested); return availableAgents.some((candidate) => normalizeAgentId(candidate.id) === requested) ? requested - : normalizeAgentId(fallback); + : fallback && normalizeAgentId(fallback); } export function allowsSelectedAgent( diff --git a/ui/src/pages/new-session/draft-place-state.ts b/ui/src/pages/new-session/draft-place-state.ts index cfb4630e1729..5f2cf38a680b 100644 --- a/ui/src/pages/new-session/draft-place-state.ts +++ b/ui/src/pages/new-session/draft-place-state.ts @@ -245,15 +245,15 @@ export class DraftPlaceState { const agents = this.agents(); const configuredDefault = snapshot.context?.agents.state.agentsList?.defaultId; const fallback = agents.some((agent) => agent.id === configuredDefault) - ? (configuredDefault ?? "main") - : (agents[0]?.id ?? "main"); + ? (configuredDefault ?? "") + : (agents[0]?.id ?? ""); const keepSelectedAgent = options.preserveSelectedAgent && this.agentSelectedByUser && Boolean(this.selectedAgent()); if (!keepSelectedAgent) { this.agentIdValue = catalog.resolveAgentId(snapshot.data, agents, fallback); this.agentSelectedByUser = false; } - const preference = this.gateway.readPreference(this.agentIdValue); + const preference = this.agentIdValue ? this.gateway.readPreference(this.agentIdValue) : null; const keepSelectedFolder = options.preserveSelectedFolder && this.folderSelectedByUser; if (!this.execNodeValue && !keepSelectedFolder && !snapshot.pendingCloudSessionKey) { const workspace = this.workspacePath(); diff --git a/ui/src/pages/new-session/model-control.ts b/ui/src/pages/new-session/model-control.ts index 54780ed02395..a3462a2fab58 100644 --- a/ui/src/pages/new-session/model-control.ts +++ b/ui/src/pages/new-session/model-control.ts @@ -164,7 +164,7 @@ export class NewSessionModelControl { loadCatalogTargets(context: ApplicationContext | undefined, agentId: string, enabled: boolean) { const snapshot = context?.gateway.snapshot; const client = snapshot?.client; - const normalizedAgentId = normalizeAgentId(agentId); + const normalizedAgentId = agentId.trim() ? normalizeAgentId(agentId) : ""; if ( !enabled || snapshot?.phase !== "connected" || @@ -339,7 +339,7 @@ export class NewSessionModelControl { ) { const snapshot = context?.gateway.snapshot; const client = snapshot?.client; - const normalizedAgentId = normalizeAgentId(agentId); + const normalizedAgentId = agentId.trim() ? normalizeAgentId(agentId) : ""; if (this.agentId !== normalizedAgentId) { // Catalog availability belongs to an agent. A real owner change clears // the snapshot; same-agent refreshes retain it until replacement. diff --git a/ui/src/pages/new-session/new-session-page.ts b/ui/src/pages/new-session/new-session-page.ts index 983e5b7bbae6..0fd70aef4319 100644 --- a/ui/src/pages/new-session/new-session-page.ts +++ b/ui/src/pages/new-session/new-session-page.ts @@ -245,11 +245,6 @@ class NewSessionPage extends OpenClawLightDomElement { } this.gateway.retryPendingCatalogTarget(); void this.context?.agentIdentity.ensure(this.place.agents().map((agent) => agent.id)); - this.place.modelControl.loadCatalogTargets( - this.context, - this.place.agentId, - this.context?.config.current.cliAgentsEnabled === true && !catalog.isTarget(this.data), - ); const agentState = this.context?.agents.state; const agentsReady = Boolean( this.gateway.connected && @@ -258,6 +253,11 @@ class NewSessionPage extends OpenClawLightDomElement { agentState.client === this.gateway.client && this.place.agents().length > 0, ); + this.place.modelControl.loadCatalogTargets( + this.context, + agentsReady && this.place.agentId ? (this.place.selectedAgent()?.id ?? "") : "", + this.context?.config.current.cliAgentsEnabled === true && !catalog.isTarget(this.data), + ); const openKey = this.data ? catalog.routeKey(this.data) : catalog.routeKeyFromSearch(window.location.search);