mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 19:24:46 +00:00
Fix chat session picker agent switching (#81858)
* Fix chat session picker agent switching Reset the chat session picker to the selected agent main session when switching agents and hide inactive sub-agent sessions from the normal picker options. * fix(ui): preserve dashboard session on agent switch Choose the most recent eligible normal/dashboard session for the selected agent while excluding subagent/internal rows; fall back to main only when no eligible session exists. * fix(ui): avoid mutating session option sort
This commit is contained in:
@@ -534,10 +534,12 @@ describe("resolveSessionOptionGroups", () => {
|
||||
expect(labels).toEqual(["Subagent: cron-config-check"]);
|
||||
});
|
||||
|
||||
it("does not synthesize active grouped sessions without a listed row", () => {
|
||||
it("keeps the active subagent session visible when no row exists yet", () => {
|
||||
const sessionKey = "agent:main:subagent:4f2146de-887b-4176-9abe-91140082959b";
|
||||
|
||||
expect(labelsForSessionOptions({ sessionKey })).toStrictEqual([]);
|
||||
expect(labelsForSessionOptions({ sessionKey })).toEqual([
|
||||
"subagent:4f2146de-887b-4176-9abe-91140082959b",
|
||||
]);
|
||||
expect(
|
||||
labelsForSessionOptions({
|
||||
sessionKey,
|
||||
@@ -550,7 +552,7 @@ describe("resolveSessionOptionGroups", () => {
|
||||
expect(labelsForSessionOptions({ sessionKey: "agent:main:main" })).toEqual(["main"]);
|
||||
});
|
||||
|
||||
it("disambiguates duplicate grouped labels with scoped suffixes", () => {
|
||||
it("hides inactive subagent sessions from the picker", () => {
|
||||
const labels = labelsForSessionOptions({
|
||||
sessionKey: "agent:main:subagent:4f2146de-887b-4176-9abe-91140082959b",
|
||||
sessions: [
|
||||
@@ -565,10 +567,7 @@ describe("resolveSessionOptionGroups", () => {
|
||||
],
|
||||
});
|
||||
|
||||
expect(labels).toEqual([
|
||||
"Subagent: cron-config-check · subagent:4f2146de-887b-4176-9abe-91140082959b",
|
||||
"Subagent: cron-config-check · subagent:6fb8b84b-c31f-410f-b7df-1553c82e43c9",
|
||||
]);
|
||||
expect(labels).toEqual(["Subagent: cron-config-check"]);
|
||||
});
|
||||
|
||||
it("filters the chat session options to the active agent", () => {
|
||||
@@ -640,7 +639,7 @@ describe("resolveSessionOptionGroups", () => {
|
||||
expect(labels).toEqual(["Beta main"]);
|
||||
});
|
||||
|
||||
it("nests subagent sessions under their parent with visual prefix", () => {
|
||||
it("hides spawned subagent sessions under their parent", () => {
|
||||
const parentKey = "agent:main:main";
|
||||
const subagentKey = "agent:main:subagent:4f2146de-887b-4176-9abe-91140082959b";
|
||||
const labels = labelsForSessionOptions({
|
||||
@@ -651,24 +650,24 @@ describe("resolveSessionOptionGroups", () => {
|
||||
],
|
||||
});
|
||||
|
||||
expect(labels).toEqual(["Spock", "└─ PLC Coder"]);
|
||||
expect(labels).toEqual(["Spock"]);
|
||||
});
|
||||
|
||||
it("uses raw key fallback for subagent without label when nested", () => {
|
||||
it("keeps the active subagent session visible without nesting it", () => {
|
||||
const parentKey = "agent:main:main";
|
||||
const subagentKey = "agent:main:subagent:f4ac7ef1-1234-5678-9abc-def012345678";
|
||||
const labels = labelsForSessionOptions({
|
||||
sessionKey: parentKey,
|
||||
sessionKey: subagentKey,
|
||||
sessions: [
|
||||
row({ key: parentKey, label: "Spock" }),
|
||||
row({ key: subagentKey, spawnedBy: parentKey }),
|
||||
row({ key: subagentKey, label: "PLC Coder", spawnedBy: parentKey }),
|
||||
],
|
||||
});
|
||||
|
||||
expect(labels).toEqual(["Spock", "└─ f4ac7ef1-1234-5678-9abc-def012345678"]);
|
||||
expect(labels).toEqual(["Spock", "Subagent: PLC Coder"]);
|
||||
});
|
||||
|
||||
it("preserves sibling row order when nesting subagent sessions", () => {
|
||||
it("hides spawned subagent siblings regardless of row order", () => {
|
||||
const parentKey = "agent:main:main";
|
||||
const newerSubagentKey = "agent:main:subagent:newer";
|
||||
const olderSubagentKey = "agent:main:subagent:older";
|
||||
@@ -681,7 +680,7 @@ describe("resolveSessionOptionGroups", () => {
|
||||
],
|
||||
});
|
||||
|
||||
expect(labels).toEqual(["Spock", "└─ Newer", "└─ Older"]);
|
||||
expect(labels).toEqual(["Spock"]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -445,7 +445,6 @@ type SessionOptionEntry = {
|
||||
label: string;
|
||||
scopeLabel: string;
|
||||
title: string;
|
||||
parentKey?: string;
|
||||
};
|
||||
|
||||
export type SessionOptionGroup = {
|
||||
@@ -478,22 +477,28 @@ function isAgentMainSessionKey(key: string): boolean {
|
||||
|
||||
function resolvePreferredSessionForAgent(state: AppViewState, agentId: string): string {
|
||||
const normalizedAgentId = normalizeAgentId(agentId);
|
||||
const defaultAgentId = normalizeAgentId(state.agentsList?.defaultId ?? "main");
|
||||
const currentParsed = parseAgentSessionKey(state.sessionKey);
|
||||
if (normalizeAgentId(currentParsed?.agentId ?? defaultAgentId) === normalizedAgentId) {
|
||||
if (resolveChatAgentFilterId(state, state.sessionKey) === normalizedAgentId) {
|
||||
return state.sessionKey;
|
||||
}
|
||||
const rows = state.sessionsResult?.sessions ?? [];
|
||||
let row: (typeof rows)[number] | undefined;
|
||||
for (const entry of rows) {
|
||||
if (!isSessionKeyTiedToAgent(entry.key, normalizedAgentId, defaultAgentId)) {
|
||||
continue;
|
||||
}
|
||||
if (!row || (entry.updatedAt ?? 0) > (row.updatedAt ?? 0)) {
|
||||
row = entry;
|
||||
}
|
||||
const defaultAgentId = normalizeAgentId(state.agentsList?.defaultId ?? "main");
|
||||
const eligible = (state.sessionsResult?.sessions ?? [])
|
||||
.filter((row) => {
|
||||
if (!isSessionKeyTiedToAgent(row.key, normalizedAgentId, defaultAgentId)) {
|
||||
return false;
|
||||
}
|
||||
if (row.kind === "global" || row.kind === "unknown") {
|
||||
return false;
|
||||
}
|
||||
if (isCronSessionKey(row.key)) {
|
||||
return false;
|
||||
}
|
||||
return !isSubagentSessionKey(row.key) && !row.spawnedBy;
|
||||
})
|
||||
.toSorted((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
|
||||
if (eligible[0]?.key) {
|
||||
return eligible[0].key;
|
||||
}
|
||||
return row?.key ?? buildAgentMainSessionKey({ agentId: normalizedAgentId });
|
||||
return buildAgentMainSessionKey({ agentId: normalizedAgentId });
|
||||
}
|
||||
|
||||
function resolveChatAgentFilterOptions(state: AppViewState): ChatAgentFilterOption[] {
|
||||
@@ -556,7 +561,7 @@ export function resolveSessionOptionGroups(
|
||||
return created;
|
||||
};
|
||||
|
||||
const addOption = (key: string, parentKey?: string, isChild?: boolean) => {
|
||||
const addOption = (key: string) => {
|
||||
if (!key || seenKeys.has(key)) {
|
||||
return;
|
||||
}
|
||||
@@ -570,16 +575,11 @@ export function resolveSessionOptionGroups(
|
||||
)
|
||||
: ensureGroup("other", "Other Sessions");
|
||||
const scopeLabel = normalizeOptionalString(parsed?.rest) ?? key;
|
||||
let label = resolveSessionScopedOptionLabel(key, row, parsed?.rest);
|
||||
if (isChild) {
|
||||
label = `└─ ${label.replace(/^Subagent:\s*/i, "")}`;
|
||||
}
|
||||
group.options.push({
|
||||
key,
|
||||
label,
|
||||
label: resolveSessionScopedOptionLabel(key, row, parsed?.rest),
|
||||
scopeLabel,
|
||||
title: key,
|
||||
...(parentKey ? { parentKey } : {}),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -597,48 +597,17 @@ export function resolveSessionOptionGroups(
|
||||
continue;
|
||||
}
|
||||
const isSubagent = isSubagentSessionKey(row.key) || !!row.spawnedBy;
|
||||
if (isSubagent && row.spawnedBy && byKey.has(row.spawnedBy)) {
|
||||
addOption(row.key, row.spawnedBy, true);
|
||||
} else {
|
||||
addOption(row.key);
|
||||
if (isSubagent && row.key !== sessionKey) {
|
||||
continue;
|
||||
}
|
||||
addOption(row.key);
|
||||
}
|
||||
if (byKey.has(sessionKey)) {
|
||||
addOption(sessionKey);
|
||||
} else if (isAgentMainSessionKey(sessionKey)) {
|
||||
} else if (isAgentMainSessionKey(sessionKey) || isSubagentSessionKey(sessionKey)) {
|
||||
addOption(sessionKey);
|
||||
}
|
||||
|
||||
for (const group of groups.values()) {
|
||||
const options = group.options;
|
||||
const optionKeys = new Set(options.map((option) => option.key));
|
||||
const childrenByParent = new Map<string, SessionOptionEntry[]>();
|
||||
for (const option of options) {
|
||||
if (option.parentKey && optionKeys.has(option.parentKey)) {
|
||||
const siblings = childrenByParent.get(option.parentKey);
|
||||
if (siblings) {
|
||||
siblings.push(option);
|
||||
} else {
|
||||
childrenByParent.set(option.parentKey, [option]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (childrenByParent.size > 0) {
|
||||
const reordered: SessionOptionEntry[] = [];
|
||||
for (const option of options) {
|
||||
if (option.parentKey && optionKeys.has(option.parentKey)) {
|
||||
continue;
|
||||
}
|
||||
reordered.push(option);
|
||||
const children = childrenByParent.get(option.key);
|
||||
if (children) {
|
||||
reordered.push(...children);
|
||||
}
|
||||
}
|
||||
options.splice(0, options.length, ...reordered);
|
||||
}
|
||||
}
|
||||
|
||||
for (const group of groups.values()) {
|
||||
const counts = new Map<string, number>();
|
||||
for (const option of group.options) {
|
||||
|
||||
@@ -983,7 +983,7 @@ describe("chat session controls", () => {
|
||||
await i18n.setLocale("en");
|
||||
});
|
||||
|
||||
it("filters chat sessions by agent and switches to that agent's recent session", () => {
|
||||
it("filters chat sessions by agent and switches to that agent's latest eligible session", () => {
|
||||
const { state } = createChatHeaderState();
|
||||
const onSwitchSession = vi.fn();
|
||||
state.sessionKey = "agent:alpha:main";
|
||||
@@ -999,13 +999,25 @@ describe("chat session controls", () => {
|
||||
state.sessionsResult = {
|
||||
ts: 0,
|
||||
path: "",
|
||||
count: 4,
|
||||
count: 6,
|
||||
defaults: { modelProvider: "openai", model: "gpt-5", contextTokens: null },
|
||||
sessions: [
|
||||
{ key: "agent:alpha:main", kind: "direct", updatedAt: 4 },
|
||||
{ key: "agent:alpha:dashboard:alpha-recent", kind: "direct", updatedAt: 3 },
|
||||
{
|
||||
key: "agent:alpha:subagent:worker",
|
||||
kind: "direct",
|
||||
updatedAt: 5,
|
||||
spawnedBy: "agent:alpha:main",
|
||||
},
|
||||
{ key: "agent:beta:dashboard:beta-recent", kind: "direct", updatedAt: 2 },
|
||||
{ key: "agent:beta:main", kind: "direct", updatedAt: 1 },
|
||||
{
|
||||
key: "agent:beta:subagent:worker",
|
||||
kind: "direct",
|
||||
updatedAt: 6,
|
||||
spawnedBy: "agent:beta:main",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user