fix(ui): align primary agent selection with OpenCode (#409)

## Summary

This PR keeps only the UI-side fix for primary agent selection in
CodeNomad.

It aligns the primary agent selector with OpenCode's behavior by:
- filtering primary-session agents through a shared selectable-primary
rule
- avoiding re-inserting an invalid current agent back into the primary
selector
- using the same primary-agent rule when choosing the default agent for
a newly created session

## Why

The earlier investigation showed that the config-loading problem was
actually rooted in OMO's handling of `OPENCODE_CONFIG_DIR`, not in
CodeNomad itself.

However, there was still an independent UI issue in CodeNomad:
- the primary selector could keep showing an agent that should no longer
be selectable
- new sessions could derive their default agent from a looser filter
than the one used by the selector

This PR keeps only that UI-side fix.

## Changes

- `packages/ui/src/components/agent-selector.tsx`
- use a shared `isSelectablePrimaryAgent` helper for primary-session
filtering
- stop force-inserting the current invalid agent back into the primary
selector list

- `packages/ui/src/stores/session-api.ts`
- use the same `isSelectablePrimaryAgent` helper when selecting the
default agent for a new session

- `packages/ui/src/types/session.ts`
  - add `isSelectablePrimaryAgent(agent)` helper

## Scope

This PR intentionally does **not** include any config merge / config
copy workaround related to `OPENCODE_CONFIG_DIR`.

That part was removed so this PR stays narrowly focused on the
primary-agent selection behavior only.

## Validation

- verified changed files are clean in editor diagnostics
- confirmed this is a minimal UI-only diff

---------

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Yao Jianxuan 2026-05-10 05:46:58 +08:00 committed by GitHub
parent a0a8da97e6
commit 6d120b0d84
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 22 additions and 16 deletions

View file

@ -1,8 +1,8 @@
import { Select } from "@kobalte/core/select"
import { For, Show, createEffect, createMemo } from "solid-js"
import { Show, createEffect, createMemo } from "solid-js"
import { agents, fetchAgents, sessions } from "../stores/sessions"
import { ChevronDown } from "lucide-solid"
import type { Agent } from "../types/session"
import { isSelectablePrimaryAgent, type Agent } from "../types/session"
import { useI18n } from "../lib/i18n"
import { getLogger } from "../lib/logger"
const log = getLogger("session")
@ -34,14 +34,7 @@ export default function AgentSelector(props: AgentSelectorProps) {
return allAgents.filter((agent) => !agent.hidden)
}
const filtered = allAgents.filter((agent) => !agent.hidden && agent.mode !== "subagent")
const currentAgent = allAgents.find((a) => a.name === props.currentAgent)
if (currentAgent && !filtered.find((a) => a.name === props.currentAgent)) {
return [currentAgent, ...filtered]
}
return filtered
return allAgents.filter(isSelectablePrimaryAgent)
})
createEffect(() => {
@ -58,7 +51,6 @@ export default function AgentSelector(props: AgentSelectorProps) {
}
})
const handleChange = async (value: Agent | null) => {
if (value && value.name !== props.currentAgent) {
await props.onAgentChange(value.name)

View file

@ -1,6 +1,6 @@
import { Component, createSignal, Show, For, createEffect } from "solid-js"
import { Dialog } from "@kobalte/core/dialog"
import type { Session, Agent } from "../types/session"
import { isSelectablePrimaryAgent, type Session, type Agent } from "../types/session"
import { getParentSessions, createSession, setActiveParentSession } from "../stores/sessions"
import { instances, stopInstance } from "../stores/instances"
import { agents } from "../stores/sessions"
@ -22,7 +22,7 @@ const SessionPicker: Component<SessionPickerProps> = (props) => {
const instance = () => instances().get(props.instanceId)
const parentSessions = () => getParentSessions(props.instanceId)
const agentList = () => agents().get(props.instanceId) || []
const agentList = () => (agents().get(props.instanceId) || []).filter(isSelectablePrimaryAgent)
createEffect(() => {
const list = agentList()

View file

@ -1,4 +1,11 @@
import { getIdleSinceForStatusTransition, mapSdkSessionRetry, mapSdkSessionStatus, type Session, type SessionStatus } from "../types/session"
import {
getIdleSinceForStatusTransition,
isSelectablePrimaryAgent,
mapSdkSessionRetry,
mapSdkSessionStatus,
type Session,
type SessionStatus,
} from "../types/session"
import type { Message } from "../types/message"
import type { FileDiff } from "@opencode-ai/sdk/v2/client"
@ -341,8 +348,8 @@ async function createSession(instanceId: string, agent?: string): Promise<Sessio
const client = getOrCreateWorktreeClient(instanceId, worktreeSlug)
const instanceAgents = agents().get(instanceId) || []
const nonSubagents = instanceAgents.filter((a) => a.mode !== "subagent")
const selectedAgent = agent || (nonSubagents.length > 0 ? nonSubagents[0].name : "")
const primaryAgents = instanceAgents.filter(isSelectablePrimaryAgent)
const selectedAgent = agent || (primaryAgents.length > 0 ? primaryAgents[0].name : "")
const defaultModel = await getDefaultModel(instanceId, selectedAgent)

View file

@ -112,6 +112,13 @@ export interface Agent {
}
}
/**
* Matches OpenCode TUI's primary-agent visibility rule: visible iff not a subagent and not hidden.
*/
export function isSelectablePrimaryAgent(agent: Agent): boolean {
return !agent.hidden && agent.mode !== "subagent"
}
// Our client-specific Provider interface (simplified version of SDK Provider)
export interface Provider {
id: string