Skip to content

Commit 40c81e9

Browse files
luzhidongaltaywtf
andauthored
fix(ui): session dropdown shows label instead of key (#45130)
Merged via squash. Prepared head SHA: 0255e39 Co-authored-by: luzhidong <[email protected]> Co-authored-by: altaywtf <[email protected]> Reviewed-by: @altaywtf
1 parent 64e6df7 commit 40c81e9

File tree

3 files changed

+144
-11
lines changed

3 files changed

+144
-11
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
1212
### Fixes
1313

1414
- 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)
15+
- 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.
1516

1617
## 2026.3.13
1718

ui/src/ui/app-render.helpers.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,7 @@ export function isCronSessionKey(key: string): boolean {
575575
type SessionOptionEntry = {
576576
key: string;
577577
label: string;
578+
scopeLabel: string;
578579
title: string;
579580
};
580581

@@ -625,10 +626,12 @@ export function resolveSessionOptionGroups(
625626
resolveAgentGroupLabel(state, parsed.agentId),
626627
)
627628
: ensureGroup("other", "Other Sessions");
629+
const scopeLabel = parsed?.rest?.trim() || key;
628630
const label = resolveSessionScopedOptionLabel(key, row, parsed?.rest);
629631
group.options.push({
630632
key,
631633
label,
634+
scopeLabel,
632635
title: key,
633636
});
634637
};
@@ -643,6 +646,19 @@ export function resolveSessionOptionGroups(
643646
addOption(row.key);
644647
}
645648
addOption(sessionKey);
649+
650+
for (const group of groups.values()) {
651+
const counts = new Map<string, number>();
652+
for (const option of group.options) {
653+
counts.set(option.label, (counts.get(option.label) ?? 0) + 1);
654+
}
655+
for (const option of group.options) {
656+
if ((counts.get(option.label) ?? 0) > 1 && option.scopeLabel !== option.label) {
657+
option.label = `${option.label} · ${option.scopeLabel}`;
658+
}
659+
}
660+
}
661+
646662
return Array.from(groups.values());
647663
}
648664

@@ -673,18 +689,14 @@ function resolveSessionScopedOptionLabel(
673689
if (!row) {
674690
return base;
675691
}
676-
const displayName =
677-
typeof row.displayName === "string" && row.displayName.trim().length > 0
678-
? row.displayName.trim()
679-
: null;
680-
const label = typeof row.label === "string" ? row.label.trim() : "";
681-
const showDisplayName = Boolean(
682-
displayName && displayName !== key && displayName !== label && displayName !== base,
683-
);
684-
if (!showDisplayName) {
685-
return base;
692+
693+
const label = row.label?.trim() || "";
694+
const displayName = row.displayName?.trim() || "";
695+
if ((label && label !== key) || (displayName && displayName !== key)) {
696+
return resolveSessionDisplayName(key, row);
686697
}
687-
return `${base} · ${displayName}`;
698+
699+
return base;
688700
}
689701

690702
type ThemeOption = { id: ThemeName; label: string; icon: string };

ui/src/ui/views/chat.test.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,4 +647,124 @@ describe("chat view", () => {
647647
expect(rerendered?.value).toBe("gpt-5-mini");
648648
vi.unstubAllGlobals();
649649
});
650+
651+
it("prefers the session label over displayName in the grouped chat session selector", () => {
652+
const { state } = createChatHeaderState({ omitSessionFromList: true });
653+
state.sessionKey = "agent:main:subagent:4f2146de-887b-4176-9abe-91140082959b";
654+
state.settings.sessionKey = state.sessionKey;
655+
state.sessionsResult = {
656+
ts: 0,
657+
path: "",
658+
count: 1,
659+
defaults: { model: "gpt-5", contextTokens: null },
660+
sessions: [
661+
{
662+
key: state.sessionKey,
663+
kind: "direct",
664+
updatedAt: null,
665+
label: "cron-config-check",
666+
displayName: "webchat:g-agent-main-subagent-4f2146de-887b-4176-9abe-91140082959b",
667+
},
668+
],
669+
};
670+
const container = document.createElement("div");
671+
render(renderChatSessionSelect(state), container);
672+
673+
const [sessionSelect] = Array.from(container.querySelectorAll<HTMLSelectElement>("select"));
674+
const labels = Array.from(sessionSelect?.querySelectorAll("option") ?? []).map((option) =>
675+
option.textContent?.trim(),
676+
);
677+
678+
expect(labels).toContain("Subagent: cron-config-check");
679+
expect(labels).not.toContain(state.sessionKey);
680+
expect(labels).not.toContain(
681+
"subagent:4f2146de-887b-4176-9abe-91140082959b · webchat:g-agent-main-subagent-4f2146de-887b-4176-9abe-91140082959b",
682+
);
683+
});
684+
685+
it("keeps a unique scoped fallback when the current grouped session is missing from sessions.list", () => {
686+
const { state } = createChatHeaderState({ omitSessionFromList: true });
687+
state.sessionKey = "agent:main:subagent:4f2146de-887b-4176-9abe-91140082959b";
688+
state.settings.sessionKey = state.sessionKey;
689+
const container = document.createElement("div");
690+
render(renderChatSessionSelect(state), container);
691+
692+
const [sessionSelect] = Array.from(container.querySelectorAll<HTMLSelectElement>("select"));
693+
const labels = Array.from(sessionSelect?.querySelectorAll("option") ?? []).map((option) =>
694+
option.textContent?.trim(),
695+
);
696+
697+
expect(labels).toContain("subagent:4f2146de-887b-4176-9abe-91140082959b");
698+
expect(labels).not.toContain("Subagent:");
699+
});
700+
701+
it("keeps a unique scoped fallback when a grouped session row has no label or displayName", () => {
702+
const { state } = createChatHeaderState({ omitSessionFromList: true });
703+
state.sessionKey = "agent:main:subagent:4f2146de-887b-4176-9abe-91140082959b";
704+
state.settings.sessionKey = state.sessionKey;
705+
state.sessionsResult = {
706+
ts: 0,
707+
path: "",
708+
count: 1,
709+
defaults: { model: "gpt-5", contextTokens: null },
710+
sessions: [
711+
{
712+
key: state.sessionKey,
713+
kind: "direct",
714+
updatedAt: null,
715+
},
716+
],
717+
};
718+
const container = document.createElement("div");
719+
render(renderChatSessionSelect(state), container);
720+
721+
const [sessionSelect] = Array.from(container.querySelectorAll<HTMLSelectElement>("select"));
722+
const labels = Array.from(sessionSelect?.querySelectorAll("option") ?? []).map((option) =>
723+
option.textContent?.trim(),
724+
);
725+
726+
expect(labels).toContain("subagent:4f2146de-887b-4176-9abe-91140082959b");
727+
expect(labels).not.toContain("Subagent:");
728+
});
729+
730+
it("disambiguates duplicate grouped labels with the scoped key suffix", () => {
731+
const { state } = createChatHeaderState({ omitSessionFromList: true });
732+
state.sessionKey = "agent:main:subagent:4f2146de-887b-4176-9abe-91140082959b";
733+
state.settings.sessionKey = state.sessionKey;
734+
state.sessionsResult = {
735+
ts: 0,
736+
path: "",
737+
count: 2,
738+
defaults: { model: "gpt-5", contextTokens: null },
739+
sessions: [
740+
{
741+
key: "agent:main:subagent:4f2146de-887b-4176-9abe-91140082959b",
742+
kind: "direct",
743+
updatedAt: null,
744+
label: "cron-config-check",
745+
},
746+
{
747+
key: "agent:main:subagent:6fb8b84b-c31f-410f-b7df-1553c82e43c9",
748+
kind: "direct",
749+
updatedAt: null,
750+
label: "cron-config-check",
751+
},
752+
],
753+
};
754+
const container = document.createElement("div");
755+
render(renderChatSessionSelect(state), container);
756+
757+
const [sessionSelect] = Array.from(container.querySelectorAll<HTMLSelectElement>("select"));
758+
const labels = Array.from(sessionSelect?.querySelectorAll("option") ?? []).map((option) =>
759+
option.textContent?.trim(),
760+
);
761+
762+
expect(labels).toContain(
763+
"Subagent: cron-config-check · subagent:4f2146de-887b-4176-9abe-91140082959b",
764+
);
765+
expect(labels).toContain(
766+
"Subagent: cron-config-check · subagent:6fb8b84b-c31f-410f-b7df-1553c82e43c9",
767+
);
768+
expect(labels).not.toContain("Subagent: cron-config-check");
769+
});
650770
});

0 commit comments

Comments
 (0)