diff --git a/ui/src/ui/app-render.helpers.node.test.ts b/ui/src/ui/app-render.helpers.node.test.ts index a57f7ed891a..b0c4897dc76 100644 --- a/ui/src/ui/app-render.helpers.node.test.ts +++ b/ui/src/ui/app-render.helpers.node.test.ts @@ -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"]); }); }); diff --git a/ui/src/ui/chat/session-controls.ts b/ui/src/ui/chat/session-controls.ts index 4de6190d04e..40774511c6c 100644 --- a/ui/src/ui/chat/session-controls.ts +++ b/ui/src/ui/chat/session-controls.ts @@ -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(); - 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(); for (const option of group.options) { diff --git a/ui/src/ui/views/chat.test.ts b/ui/src/ui/views/chat.test.ts index d018d74ca00..76ab11c22d1 100644 --- a/ui/src/ui/views/chat.test.ts +++ b/ui/src/ui/views/chat.test.ts @@ -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", + }, ], };