Control UI: disambiguate duplicate agent session labels (#48209)

* Control UI: disambiguate duplicate agent sessions

* Control UI: avoid prefixed session label collisions

* Control UI: align session defaults typing

---------

Co-authored-by: Val Alexander <68980965+BunsDev@users.noreply.github.com>
This commit is contained in:
wilsonIs
2026-03-22 10:11:15 +08:00
committed by GitHub
parent 17713ec988
commit 1ad3893b39
2 changed files with 158 additions and 0 deletions

View File

@@ -859,6 +859,70 @@ export function resolveSessionOptionGroups(
}
}
const allOptions = Array.from(groups.values()).flatMap((group) =>
group.options.map((option) => ({ groupLabel: group.label, option })),
);
const labels = new Map(allOptions.map(({ option }) => [option, option.label]));
const countAssignedLabels = () => {
const counts = new Map<string, number>();
for (const { option } of allOptions) {
const label = labels.get(option) ?? option.label;
counts.set(label, (counts.get(label) ?? 0) + 1);
}
return counts;
};
const labelIncludesScopeLabel = (label: string, scopeLabel: string) => {
const trimmedScope = scopeLabel.trim();
if (!trimmedScope) {
return false;
}
return (
label === trimmedScope ||
label.endsWith(` · ${trimmedScope}`) ||
label.endsWith(` / ${trimmedScope}`)
);
};
const globalCounts = countAssignedLabels();
for (const { groupLabel, option } of allOptions) {
const currentLabel = labels.get(option) ?? option.label;
if ((globalCounts.get(currentLabel) ?? 0) <= 1) {
continue;
}
const scopedPrefix = `${groupLabel} / `;
if (currentLabel.startsWith(scopedPrefix)) {
continue;
}
// Keep the agent visible once the native select collapses to a single chosen label.
labels.set(option, `${groupLabel} / ${currentLabel}`);
}
const scopedCounts = countAssignedLabels();
for (const { option } of allOptions) {
const currentLabel = labels.get(option) ?? option.label;
if ((scopedCounts.get(currentLabel) ?? 0) <= 1) {
continue;
}
if (labelIncludesScopeLabel(currentLabel, option.scopeLabel)) {
continue;
}
labels.set(option, `${currentLabel} · ${option.scopeLabel}`);
}
const finalCounts = countAssignedLabels();
for (const { option } of allOptions) {
const currentLabel = labels.get(option) ?? option.label;
if ((finalCounts.get(currentLabel) ?? 0) <= 1) {
continue;
}
// Fall back to the full key only when every friendlier disambiguator still collides.
labels.set(option, `${currentLabel} · ${option.key}`);
}
for (const { option } of allOptions) {
option.label = labels.get(option) ?? option.label;
}
return Array.from(groups.values());
}

View File

@@ -1035,4 +1035,98 @@ describe("chat view", () => {
);
expect(labels).not.toContain("Subagent: cron-config-check");
});
it("prefixes duplicate agent session labels with the agent name", () => {
const { state } = createChatHeaderState({ omitSessionFromList: true });
state.sessionKey = "agent:alpha:main";
state.settings.sessionKey = state.sessionKey;
state.agentsList = {
defaultId: "alpha",
mainKey: "agent:alpha:main",
scope: "all",
agents: [
{ id: "alpha", name: "Deep Chat" },
{ id: "beta", name: "Coding" },
],
};
state.sessionsResult = {
ts: 0,
path: "",
count: 2,
defaults: { modelProvider: "openai", model: "gpt-5", contextTokens: null },
sessions: [
{
key: "agent:alpha:main",
kind: "direct",
updatedAt: null,
},
{
key: "agent:beta:main",
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("Deep Chat (alpha) / main");
expect(labels).toContain("Coding (beta) / main");
expect(labels).not.toContain("main");
});
it("keeps agent-prefixed labels unique when a custom label already matches the prefix", () => {
const { state } = createChatHeaderState({ omitSessionFromList: true });
state.sessionKey = "agent:alpha:main";
state.settings.sessionKey = state.sessionKey;
state.agentsList = {
defaultId: "alpha",
mainKey: "agent:alpha:main",
scope: "all",
agents: [
{ id: "alpha", name: "Deep Chat" },
{ id: "beta", name: "Coding" },
],
};
state.sessionsResult = {
ts: 0,
path: "",
count: 3,
defaults: { modelProvider: "openai", model: "gpt-5", contextTokens: null },
sessions: [
{
key: "agent:alpha:main",
kind: "direct",
updatedAt: null,
},
{
key: "agent:beta:main",
kind: "direct",
updatedAt: null,
},
{
key: "agent:alpha:named-main",
kind: "direct",
updatedAt: null,
label: "Deep Chat (alpha) / main",
},
],
};
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.filter((label) => label === "Deep Chat (alpha) / main")).toHaveLength(1);
expect(labels).toContain("Deep Chat (alpha) / main · named-main");
expect(labels).toContain("Coding (beta) / main");
});
});