mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
fix(ui): key the new-session draft on the requested agent (#114415)
The route loader reports an empty agent id whenever it cannot verify one through the Gateway, so an offline catalog target keyed as ["", catalog] and became [agent, catalog] on reconnect. That looked like navigation and cleared whatever the user had typed. Key the draft identity on the agent the URL requested, which only a navigation can change, and re-derive agent defaults when the resolved id finally arrives. Fixes #114398
This commit is contained in:
committed by
GitHub
parent
afb1b014e2
commit
65cf9c1d65
@@ -11,7 +11,13 @@ describe("new-session catalog target", () => {
|
||||
const agents = [{ id: "main" }, { id: "research" }];
|
||||
|
||||
it("keeps the draft identity stable while target metadata resolves", () => {
|
||||
const pending = { agentId: "main", catalogId: "claude", model: "", catalogLabel: "" };
|
||||
const pending = {
|
||||
agentId: "main",
|
||||
requestedAgentId: "main",
|
||||
catalogId: "claude",
|
||||
model: "",
|
||||
catalogLabel: "",
|
||||
};
|
||||
const ready = {
|
||||
...pending,
|
||||
model: "anthropic/claude-opus-4-8",
|
||||
@@ -23,6 +29,22 @@ describe("new-session catalog target", () => {
|
||||
expect(allowsSelectedAgent(ready, { id: "main" })).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps the draft identity stable while the target agent resolves", () => {
|
||||
const requested = {
|
||||
requestedAgentId: "research",
|
||||
catalogId: "claude",
|
||||
model: "",
|
||||
catalogLabel: "",
|
||||
};
|
||||
const unresolved = { ...requested, agentId: "" };
|
||||
const resolved = { ...requested, agentId: "research" };
|
||||
|
||||
expect(routeKey(unresolved)).toBe(routeKey(resolved));
|
||||
// Only a navigation changes the requested agent or the target.
|
||||
expect(routeKey({ ...resolved, requestedAgentId: "main" })).not.toBe(routeKey(resolved));
|
||||
expect(routeKey({ ...resolved, catalogId: "codex" })).not.toBe(routeKey(resolved));
|
||||
});
|
||||
|
||||
it("fails closed when the requested creation capability is unavailable", async () => {
|
||||
const request = vi.fn(async () => ({
|
||||
catalogs: [
|
||||
|
||||
@@ -6,8 +6,14 @@ import { t } from "../../i18n/index.ts";
|
||||
import { normalizeAgentId } from "../../lib/sessions/session-key.ts";
|
||||
import type { NewSessionRouteData } from "./location.ts";
|
||||
|
||||
/**
|
||||
* Which draft a new-session route has open. This keys on the requested agent,
|
||||
* not the resolved one: a catalog route resolves its agent through the Gateway
|
||||
* and reports it empty until the roster arrives, so keying on the resolved id
|
||||
* would make that fill-in look like a navigation and discard the draft.
|
||||
*/
|
||||
export function routeKey(data?: NewSessionRouteData): string {
|
||||
return JSON.stringify([data?.agentId ?? "", data?.catalogId ?? ""]);
|
||||
return JSON.stringify([data?.requestedAgentId ?? "", data?.catalogId ?? ""]);
|
||||
}
|
||||
|
||||
export function isTarget(data?: NewSessionRouteData): boolean {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
export type NewSessionRouteData = {
|
||||
/** The agent the loader resolved; empty until the Gateway can name one. */
|
||||
agentId: string;
|
||||
/** The agent the URL asked for, which only a navigation can change. */
|
||||
requestedAgentId: string;
|
||||
catalogId: string;
|
||||
model: string;
|
||||
catalogLabel: string;
|
||||
|
||||
@@ -97,6 +97,7 @@ class NewSessionPage extends OpenClawLightDomElement {
|
||||
@state() private restoredFolderValidation: "none" | "checking" | "failed" = "none";
|
||||
|
||||
private openedFor: string | null = null;
|
||||
private openedAgentId = "";
|
||||
private agentsHydrated = false;
|
||||
private nodesHydrated = false;
|
||||
// Discovery retry provenance separates user choices from Gateway-derived defaults.
|
||||
@@ -383,12 +384,20 @@ class NewSessionPage extends OpenClawLightDomElement {
|
||||
this.agents().length > 0,
|
||||
);
|
||||
const openKey = catalog.routeKey(this.data);
|
||||
const resolvedAgentId = this.data?.agentId ?? "";
|
||||
if (this.openedFor !== openKey) {
|
||||
this.openedFor = openKey;
|
||||
this.openedAgentId = resolvedAgentId;
|
||||
this.agentsHydrated = agentsReady;
|
||||
this.resetDraft();
|
||||
return;
|
||||
}
|
||||
if (this.openedAgentId !== resolvedAgentId) {
|
||||
// The route named the target agent after the draft opened, so the page is
|
||||
// still holding the fallback agent it adopted while the id was unknown.
|
||||
this.openedAgentId = resolvedAgentId;
|
||||
this.agentsHydrated = false;
|
||||
}
|
||||
// A hard reload can land here before agents.list resolves. Once the list
|
||||
// arrives, adopt only agent-derived defaults; a full reset would discard
|
||||
// anything the user already typed while the list was loading.
|
||||
|
||||
@@ -11,13 +11,15 @@ async function loadNewSessionData(
|
||||
search: string,
|
||||
): Promise<NewSessionRouteData> {
|
||||
const requestedLocation = newSessionLocationFromSearch(search);
|
||||
const requestedAgentId = requestedLocation.agentId.trim();
|
||||
if (!requestedLocation.catalogId) {
|
||||
return { ...requestedLocation, model: "", catalogLabel: "" };
|
||||
return { ...requestedLocation, requestedAgentId, model: "", catalogLabel: "" };
|
||||
}
|
||||
const { resolveAgentId, resolveCreateTarget } = await import("./catalog-target.ts");
|
||||
const unresolved = (agentId = ""): NewSessionRouteData => ({
|
||||
...requestedLocation,
|
||||
agentId,
|
||||
requestedAgentId,
|
||||
model: "",
|
||||
catalogLabel: "",
|
||||
});
|
||||
@@ -50,7 +52,6 @@ async function loadNewSessionData(
|
||||
const availableAgents = listSelectableAgents(agentsList?.agents ?? []);
|
||||
const gatewayDefaultId =
|
||||
gateway.phase === "connected" && gateway.hello ? gateway.assistantAgentId : null;
|
||||
const requestedAgentId = requestedLocation.agentId.trim();
|
||||
if (
|
||||
!agentsList &&
|
||||
requestedAgentId &&
|
||||
|
||||
Reference in New Issue
Block a user