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:
Gio Della-Libera
2026-05-16 10:25:15 -07:00
committed by GitHub
parent ba103c56a2
commit f22c26a6cd
3 changed files with 53 additions and 73 deletions

View File

@@ -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"]);
});
});

View File

@@ -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) {

View File

@@ -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",
},
],
};