From 8bcf365fc3679fd7eff65f829a78799252adb931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Sun, 7 Jun 2026 18:19:21 +0200 Subject: [PATCH] fix(ui): scope primary agent selector to selectable agents (#528) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - rescope the UI portion of #385 to the remaining main-session selector issue from #384 - keep main-session primary selectors limited to selectable primary agents - preserve child/subagent behavior by keeping the current hidden agent available for steering - add focused tests for selector eligibility behavior This intentionally excludes the OMO/config-directory merge and plugin retry scope from #385, since that part was determined to belong outside CodeNomad. Credit to @jollyxenon for the original #385 investigation and UI direction. ## Validation - ode --test packages/ui/src/types/session.test.ts ✅ - pm run typecheck --workspace @codenomad/ui ⚠️ fails on existing SDK/type drift outside this diff Fixes #384 Based on #385 --- packages/ui/src/components/agent-selector.tsx | 9 +--- packages/ui/src/types/session.test.ts | 45 +++++++++++++++++++ packages/ui/src/types/session.ts | 17 +++++++ 3 files changed, 64 insertions(+), 7 deletions(-) create mode 100644 packages/ui/src/types/session.test.ts diff --git a/packages/ui/src/components/agent-selector.tsx b/packages/ui/src/components/agent-selector.tsx index b3ef06b3..be3616d3 100644 --- a/packages/ui/src/components/agent-selector.tsx +++ b/packages/ui/src/components/agent-selector.tsx @@ -2,7 +2,7 @@ import { Select } from "@kobalte/core/select" import { Show, createEffect, createMemo } from "solid-js" import { agents, fetchAgents, sessions } from "../stores/sessions" import { ChevronDown } from "lucide-solid" -import { isSelectablePrimaryAgent, type Agent } from "../types/session" +import { getSelectableAgentsForSession, type Agent } from "../types/session" import { useI18n } from "../lib/i18n" import { getLogger } from "../lib/logger" const log = getLogger("session") @@ -29,12 +29,7 @@ export default function AgentSelector(props: AgentSelectorProps) { }) const availableAgents = createMemo(() => { - const allAgents = instanceAgents() - if (isChildSession()) { - return allAgents.filter((agent) => !agent.hidden) - } - - return allAgents.filter(isSelectablePrimaryAgent) + return getSelectableAgentsForSession(instanceAgents(), props.currentAgent, isChildSession()) }) createEffect(() => { diff --git a/packages/ui/src/types/session.test.ts b/packages/ui/src/types/session.test.ts new file mode 100644 index 00000000..0f924c97 --- /dev/null +++ b/packages/ui/src/types/session.test.ts @@ -0,0 +1,45 @@ +import assert from "node:assert/strict" +import { describe, it } from "node:test" + +import { getSelectableAgentsForSession, isSelectablePrimaryAgent, type Agent } from "./session.ts" + +const visiblePrimary: Agent = { name: "plan", description: "", mode: "primary" } +const visibleSubagent: Agent = { name: "review", description: "", mode: "subagent" } +const hiddenPrimary: Agent = { name: "build", description: "", mode: "primary", hidden: true } +const hiddenSubagent: Agent = { name: "debug", description: "", mode: "subagent", hidden: true } + +describe("agent selectability", () => { + it("matches primary-session selector rules", () => { + assert.equal(isSelectablePrimaryAgent(visiblePrimary), true) + assert.equal(isSelectablePrimaryAgent(visibleSubagent), false) + assert.equal(isSelectablePrimaryAgent(hiddenPrimary), false) + assert.equal(isSelectablePrimaryAgent(hiddenSubagent), false) + }) + + it("excludes hidden and subagent entries from main-session selectors", () => { + const agents = [hiddenPrimary, visibleSubagent, visiblePrimary] + + assert.deepEqual( + getSelectableAgentsForSession(agents, "build", false).map((agent) => agent.name), + ["plan"], + ) + }) + + it("preserves a child session's current hidden agent for steering", () => { + const agents = [hiddenPrimary, visibleSubagent, visiblePrimary] + + assert.deepEqual( + getSelectableAgentsForSession(agents, "build", true).map((agent) => agent.name), + ["review", "plan", "build"], + ) + }) + + it("does not add unrelated hidden agents to child-session selectors", () => { + const agents = [hiddenPrimary, visibleSubagent, visiblePrimary] + + assert.deepEqual( + getSelectableAgentsForSession(agents, "review", true).map((agent) => agent.name), + ["review", "plan"], + ) + }) +}) diff --git a/packages/ui/src/types/session.ts b/packages/ui/src/types/session.ts index 1d51be05..067ec63e 100644 --- a/packages/ui/src/types/session.ts +++ b/packages/ui/src/types/session.ts @@ -120,6 +120,23 @@ export function isSelectablePrimaryAgent(agent: Agent): boolean { return !agent.hidden && agent.mode !== "subagent" } +export function getSelectableAgentsForSession( + agentList: Agent[], + currentAgentName: string, + isChildSession: boolean, +): Agent[] { + if (!isChildSession) { + return agentList.filter(isSelectablePrimaryAgent) + } + + const visibleAgents = agentList.filter((agent) => !agent.hidden) + const currentHiddenAgent = agentList.find((agent) => agent.hidden && agent.name === currentAgentName) + + return currentHiddenAgent && !visibleAgents.some((agent) => agent.name === currentHiddenAgent.name) + ? [...visibleAgents, currentHiddenAgent] + : visibleAgents +} + // Our client-specific Provider interface (simplified version of SDK Provider) export interface Provider { id: string