mirror of https://github.com/openclaw/openclaw.git
fix(ui): session dropdown shows label instead of key (#45130)
Merged via squash.
Prepared head SHA: 0255e3971b
Co-authored-by: luzhidong <15848762+luzhidong@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
This commit is contained in:
parent
64e6df7eea
commit
40c81e9cd3
|
|
@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
- Z.AI/onboarding: detect a working default model even for explicit `zai-coding-*` endpoint choices, so Coding Plan setup can keep the selected endpoint while defaulting to `glm-5` when available or `glm-4.7` as fallback. (#45969)
|
- Z.AI/onboarding: detect a working default model even for explicit `zai-coding-*` endpoint choices, so Coding Plan setup can keep the selected endpoint while defaulting to `glm-5` when available or `glm-4.7` as fallback. (#45969)
|
||||||
|
- Control UI/chat sessions: show human-readable labels in the grouped session dropdown again, keep unique scoped fallbacks when metadata is missing, and disambiguate duplicate labels only when needed. (#45130) thanks @luzhidong.
|
||||||
|
|
||||||
## 2026.3.13
|
## 2026.3.13
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -575,6 +575,7 @@ export function isCronSessionKey(key: string): boolean {
|
||||||
type SessionOptionEntry = {
|
type SessionOptionEntry = {
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
scopeLabel: string;
|
||||||
title: string;
|
title: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -625,10 +626,12 @@ export function resolveSessionOptionGroups(
|
||||||
resolveAgentGroupLabel(state, parsed.agentId),
|
resolveAgentGroupLabel(state, parsed.agentId),
|
||||||
)
|
)
|
||||||
: ensureGroup("other", "Other Sessions");
|
: ensureGroup("other", "Other Sessions");
|
||||||
|
const scopeLabel = parsed?.rest?.trim() || key;
|
||||||
const label = resolveSessionScopedOptionLabel(key, row, parsed?.rest);
|
const label = resolveSessionScopedOptionLabel(key, row, parsed?.rest);
|
||||||
group.options.push({
|
group.options.push({
|
||||||
key,
|
key,
|
||||||
label,
|
label,
|
||||||
|
scopeLabel,
|
||||||
title: key,
|
title: key,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -643,6 +646,19 @@ export function resolveSessionOptionGroups(
|
||||||
addOption(row.key);
|
addOption(row.key);
|
||||||
}
|
}
|
||||||
addOption(sessionKey);
|
addOption(sessionKey);
|
||||||
|
|
||||||
|
for (const group of groups.values()) {
|
||||||
|
const counts = new Map<string, number>();
|
||||||
|
for (const option of group.options) {
|
||||||
|
counts.set(option.label, (counts.get(option.label) ?? 0) + 1);
|
||||||
|
}
|
||||||
|
for (const option of group.options) {
|
||||||
|
if ((counts.get(option.label) ?? 0) > 1 && option.scopeLabel !== option.label) {
|
||||||
|
option.label = `${option.label} · ${option.scopeLabel}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Array.from(groups.values());
|
return Array.from(groups.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -673,18 +689,14 @@ function resolveSessionScopedOptionLabel(
|
||||||
if (!row) {
|
if (!row) {
|
||||||
return base;
|
return base;
|
||||||
}
|
}
|
||||||
const displayName =
|
|
||||||
typeof row.displayName === "string" && row.displayName.trim().length > 0
|
const label = row.label?.trim() || "";
|
||||||
? row.displayName.trim()
|
const displayName = row.displayName?.trim() || "";
|
||||||
: null;
|
if ((label && label !== key) || (displayName && displayName !== key)) {
|
||||||
const label = typeof row.label === "string" ? row.label.trim() : "";
|
return resolveSessionDisplayName(key, row);
|
||||||
const showDisplayName = Boolean(
|
|
||||||
displayName && displayName !== key && displayName !== label && displayName !== base,
|
|
||||||
);
|
|
||||||
if (!showDisplayName) {
|
|
||||||
return base;
|
|
||||||
}
|
}
|
||||||
return `${base} · ${displayName}`;
|
|
||||||
|
return base;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ThemeOption = { id: ThemeName; label: string; icon: string };
|
type ThemeOption = { id: ThemeName; label: string; icon: string };
|
||||||
|
|
|
||||||
|
|
@ -647,4 +647,124 @@ describe("chat view", () => {
|
||||||
expect(rerendered?.value).toBe("gpt-5-mini");
|
expect(rerendered?.value).toBe("gpt-5-mini");
|
||||||
vi.unstubAllGlobals();
|
vi.unstubAllGlobals();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("prefers the session label over displayName in the grouped chat session selector", () => {
|
||||||
|
const { state } = createChatHeaderState({ omitSessionFromList: true });
|
||||||
|
state.sessionKey = "agent:main:subagent:4f2146de-887b-4176-9abe-91140082959b";
|
||||||
|
state.settings.sessionKey = state.sessionKey;
|
||||||
|
state.sessionsResult = {
|
||||||
|
ts: 0,
|
||||||
|
path: "",
|
||||||
|
count: 1,
|
||||||
|
defaults: { model: "gpt-5", contextTokens: null },
|
||||||
|
sessions: [
|
||||||
|
{
|
||||||
|
key: state.sessionKey,
|
||||||
|
kind: "direct",
|
||||||
|
updatedAt: null,
|
||||||
|
label: "cron-config-check",
|
||||||
|
displayName: "webchat:g-agent-main-subagent-4f2146de-887b-4176-9abe-91140082959b",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const container = document.createElement("div");
|
||||||
|
render(renderChatSessionSelect(state), container);
|
||||||
|
|
||||||
|
const [sessionSelect] = Array.from(container.querySelectorAll<HTMLSelectElement>("select"));
|
||||||
|
const labels = Array.from(sessionSelect?.querySelectorAll("option") ?? []).map((option) =>
|
||||||
|
option.textContent?.trim(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(labels).toContain("Subagent: cron-config-check");
|
||||||
|
expect(labels).not.toContain(state.sessionKey);
|
||||||
|
expect(labels).not.toContain(
|
||||||
|
"subagent:4f2146de-887b-4176-9abe-91140082959b · webchat:g-agent-main-subagent-4f2146de-887b-4176-9abe-91140082959b",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps a unique scoped fallback when the current grouped session is missing from sessions.list", () => {
|
||||||
|
const { state } = createChatHeaderState({ omitSessionFromList: true });
|
||||||
|
state.sessionKey = "agent:main:subagent:4f2146de-887b-4176-9abe-91140082959b";
|
||||||
|
state.settings.sessionKey = state.sessionKey;
|
||||||
|
const container = document.createElement("div");
|
||||||
|
render(renderChatSessionSelect(state), container);
|
||||||
|
|
||||||
|
const [sessionSelect] = Array.from(container.querySelectorAll<HTMLSelectElement>("select"));
|
||||||
|
const labels = Array.from(sessionSelect?.querySelectorAll("option") ?? []).map((option) =>
|
||||||
|
option.textContent?.trim(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(labels).toContain("subagent:4f2146de-887b-4176-9abe-91140082959b");
|
||||||
|
expect(labels).not.toContain("Subagent:");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps a unique scoped fallback when a grouped session row has no label or displayName", () => {
|
||||||
|
const { state } = createChatHeaderState({ omitSessionFromList: true });
|
||||||
|
state.sessionKey = "agent:main:subagent:4f2146de-887b-4176-9abe-91140082959b";
|
||||||
|
state.settings.sessionKey = state.sessionKey;
|
||||||
|
state.sessionsResult = {
|
||||||
|
ts: 0,
|
||||||
|
path: "",
|
||||||
|
count: 1,
|
||||||
|
defaults: { model: "gpt-5", contextTokens: null },
|
||||||
|
sessions: [
|
||||||
|
{
|
||||||
|
key: state.sessionKey,
|
||||||
|
kind: "direct",
|
||||||
|
updatedAt: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const container = document.createElement("div");
|
||||||
|
render(renderChatSessionSelect(state), container);
|
||||||
|
|
||||||
|
const [sessionSelect] = Array.from(container.querySelectorAll<HTMLSelectElement>("select"));
|
||||||
|
const labels = Array.from(sessionSelect?.querySelectorAll("option") ?? []).map((option) =>
|
||||||
|
option.textContent?.trim(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(labels).toContain("subagent:4f2146de-887b-4176-9abe-91140082959b");
|
||||||
|
expect(labels).not.toContain("Subagent:");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("disambiguates duplicate grouped labels with the scoped key suffix", () => {
|
||||||
|
const { state } = createChatHeaderState({ omitSessionFromList: true });
|
||||||
|
state.sessionKey = "agent:main:subagent:4f2146de-887b-4176-9abe-91140082959b";
|
||||||
|
state.settings.sessionKey = state.sessionKey;
|
||||||
|
state.sessionsResult = {
|
||||||
|
ts: 0,
|
||||||
|
path: "",
|
||||||
|
count: 2,
|
||||||
|
defaults: { model: "gpt-5", contextTokens: null },
|
||||||
|
sessions: [
|
||||||
|
{
|
||||||
|
key: "agent:main:subagent:4f2146de-887b-4176-9abe-91140082959b",
|
||||||
|
kind: "direct",
|
||||||
|
updatedAt: null,
|
||||||
|
label: "cron-config-check",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "agent:main:subagent:6fb8b84b-c31f-410f-b7df-1553c82e43c9",
|
||||||
|
kind: "direct",
|
||||||
|
updatedAt: null,
|
||||||
|
label: "cron-config-check",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const container = document.createElement("div");
|
||||||
|
render(renderChatSessionSelect(state), container);
|
||||||
|
|
||||||
|
const [sessionSelect] = Array.from(container.querySelectorAll<HTMLSelectElement>("select"));
|
||||||
|
const labels = Array.from(sessionSelect?.querySelectorAll("option") ?? []).map((option) =>
|
||||||
|
option.textContent?.trim(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(labels).toContain(
|
||||||
|
"Subagent: cron-config-check · subagent:4f2146de-887b-4176-9abe-91140082959b",
|
||||||
|
);
|
||||||
|
expect(labels).toContain(
|
||||||
|
"Subagent: cron-config-check · subagent:6fb8b84b-c31f-410f-b7df-1553c82e43c9",
|
||||||
|
);
|
||||||
|
expect(labels).not.toContain("Subagent: cron-config-check");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue