mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-27 09:21:35 +07:00
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:
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user