diff --git a/CHANGELOG.md b/CHANGELOG.md index 047ef22fe2f..446b8b7489c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai ### 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) +- 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 diff --git a/ui/src/ui/app-render.helpers.ts b/ui/src/ui/app-render.helpers.ts index eaf94616032..a7ecb15c370 100644 --- a/ui/src/ui/app-render.helpers.ts +++ b/ui/src/ui/app-render.helpers.ts @@ -575,6 +575,7 @@ export function isCronSessionKey(key: string): boolean { type SessionOptionEntry = { key: string; label: string; + scopeLabel: string; title: string; }; @@ -625,10 +626,12 @@ export function resolveSessionOptionGroups( resolveAgentGroupLabel(state, parsed.agentId), ) : ensureGroup("other", "Other Sessions"); + const scopeLabel = parsed?.rest?.trim() || key; const label = resolveSessionScopedOptionLabel(key, row, parsed?.rest); group.options.push({ key, label, + scopeLabel, title: key, }); }; @@ -643,6 +646,19 @@ export function resolveSessionOptionGroups( addOption(row.key); } addOption(sessionKey); + + for (const group of groups.values()) { + const counts = new Map(); + 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()); } @@ -673,18 +689,14 @@ function resolveSessionScopedOptionLabel( if (!row) { return base; } - const displayName = - typeof row.displayName === "string" && row.displayName.trim().length > 0 - ? row.displayName.trim() - : null; - const label = typeof row.label === "string" ? row.label.trim() : ""; - const showDisplayName = Boolean( - displayName && displayName !== key && displayName !== label && displayName !== base, - ); - if (!showDisplayName) { - return base; + + const label = row.label?.trim() || ""; + const displayName = row.displayName?.trim() || ""; + if ((label && label !== key) || (displayName && displayName !== key)) { + return resolveSessionDisplayName(key, row); } - return `${base} · ${displayName}`; + + return base; } type ThemeOption = { id: ThemeName; label: string; icon: string }; diff --git a/ui/src/ui/views/chat.test.ts b/ui/src/ui/views/chat.test.ts index b21936e0bb8..22c141c3919 100644 --- a/ui/src/ui/views/chat.test.ts +++ b/ui/src/ui/views/chat.test.ts @@ -647,4 +647,124 @@ describe("chat view", () => { expect(rerendered?.value).toBe("gpt-5-mini"); 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("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("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("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("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"); + }); });