From b4b525f3362cee46860b8e55ec462f32ae2cfd87 Mon Sep 17 00:00:00 2001 From: lcy Date: Sat, 17 Oct 2026 14:50:28 +0800 Subject: [PATCH 1/2] fix(opencode): decode agents whose hidden flag is null With oh-my-opencode-slim installed, OpenCode's GET /agent reports the plugin agents (orchestrator, fixer, explorer, ...) with "hidden": null. OpenCodeAgent.hidden was a non-null Boolean, so kotlinx.serialization rejected the whole list, the catalogue fell back to no agents, and the mode chip opened an empty menu - tapping "build" appeared to do nothing. Make hidden nullable (V1 and V2 DTOs) and treat only true as hidden. Verified against a live OpenCode 1.19.3 server with the plugin enabled. --- .../andcode/core/api/OpenCodeApiModels.kt | 9 +++++-- .../andcode/core/api/OpenCodeV2ApiClient.kt | 5 ++-- .../core/api/OpenCodeAgentSelectionTest.kt | 24 +++++++++++++++++-- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/yugahashimoto/andcode/core/api/OpenCodeApiModels.kt b/app/src/main/java/com/yugahashimoto/andcode/core/api/OpenCodeApiModels.kt index c898e79ed..d9ffedcc1 100644 --- a/app/src/main/java/com/yugahashimoto/andcode/core/api/OpenCodeApiModels.kt +++ b/app/src/main/java/com/yugahashimoto/andcode/core/api/OpenCodeApiModels.kt @@ -165,7 +165,12 @@ data class OpenCodeAgent( val description: String? = null, val mode: String? = null, val native: Boolean = false, - val hidden: Boolean = false, + /** + * Nullable on purpose: OpenCode sends `"hidden": null` for plugin agents (oh-my-opencode-slim's + * `orchestrator`, `fixer`, ...), and a non-null Boolean made the whole `GET /agent` list fail to + * decode, leaving the mode picker with nothing to offer. Only `true` hides an agent. + */ + val hidden: Boolean? = null, ) /** @@ -178,7 +183,7 @@ data class OpenCodeAgent( * hard-coding `build`. */ fun List.selectablePrimaryAgents(): List = - filter { (it.mode == null || it.mode == "primary" || it.mode == "all") && !it.hidden } + filter { (it.mode == null || it.mode == "primary" || it.mode == "all") && it.hidden != true } /** The server's default agent: the first selectable primary it lists, or null when it offers none. */ fun List.defaultAgentName(): String? = selectablePrimaryAgents().firstOrNull()?.name diff --git a/app/src/main/java/com/yugahashimoto/andcode/core/api/OpenCodeV2ApiClient.kt b/app/src/main/java/com/yugahashimoto/andcode/core/api/OpenCodeV2ApiClient.kt index acccd4bf6..d36dd8390 100644 --- a/app/src/main/java/com/yugahashimoto/andcode/core/api/OpenCodeV2ApiClient.kt +++ b/app/src/main/java/com/yugahashimoto/andcode/core/api/OpenCodeV2ApiClient.kt @@ -190,7 +190,7 @@ class OpenCodeV2ApiClient( * keep them out of the mode picker and settings. */ override suspend fun agents(): List = - getData>("api/agent").filterNot { it.hidden }.map { agent -> + getData>("api/agent").filterNot { it.hidden == true }.map { agent -> OpenCodeAgent( name = agent.id, description = agent.description, @@ -1565,7 +1565,8 @@ private data class V2Agent( val id: String, val description: String? = null, val mode: String? = null, - val hidden: Boolean = false, + /** Nullable: an explicit `null` must decode as visible rather than fail the whole list. */ + val hidden: Boolean? = null, ) @Serializable diff --git a/app/src/test/java/com/yugahashimoto/andcode/core/api/OpenCodeAgentSelectionTest.kt b/app/src/test/java/com/yugahashimoto/andcode/core/api/OpenCodeAgentSelectionTest.kt index cbb34e6f6..fc3ff1815 100644 --- a/app/src/test/java/com/yugahashimoto/andcode/core/api/OpenCodeAgentSelectionTest.kt +++ b/app/src/test/java/com/yugahashimoto/andcode/core/api/OpenCodeAgentSelectionTest.kt @@ -3,7 +3,6 @@ package com.yugahashimoto.andcode.core.api import kotlinx.serialization.json.Json import org.junit.Assert.assertEquals import org.junit.Assert.assertNull -import org.junit.Assert.assertTrue import org.junit.Test class OpenCodeAgentSelectionTest { @@ -71,7 +70,28 @@ class OpenCodeAgentSelectionTest { """[{"name":"title","mode":"primary","hidden":true,"native":true},{"name":"orchestrator","mode":"primary"}]""", ) - assertTrue(agents[0].hidden) + assertEquals(true, agents[0].hidden) assertEquals(listOf("orchestrator"), agents.selectablePrimaryAgents().map { it.name }) } + + @Test + fun `decodes plugin agents that send an explicit null hidden flag`() { + // Shape of OpenCode 1.19.3's GET /agent with oh-my-opencode-slim installed: plugin agents + // carry "hidden": null, which used to fail decoding of the whole list. + val agents = + OpenCodeApiClient.defaultJson.decodeFromString>( + """ + [ + {"name":"orchestrator","mode":"primary","native":false,"hidden":null,"variant":"high"}, + {"name":"build","description":"Default agent","mode":"primary","native":true}, + {"name":"compaction","mode":"primary","native":true,"hidden":true}, + {"name":"fixer","description":"Fast implementation","mode":"subagent","native":false,"hidden":null}, + {"name":"plan","description":"Plan mode","mode":"primary","native":true} + ] + """.trimIndent(), + ) + + assertEquals(listOf("orchestrator", "build", "plan"), agents.selectablePrimaryAgents().map { it.name }) + assertEquals("orchestrator", agents.defaultAgentName()) + } } From 51ba08d84bf8a7f936c6f07abd22cbd20d20c834 Mon Sep 17 00:00:00 2001 From: lcy Date: Sat, 17 Oct 2026 14:53:23 +0800 Subject: [PATCH 2/2] test(opencode): cover a null hidden flag on V2 agents --- .../yugahashimoto/andcode/core/api/OpenCodeV2ApiClient.kt | 5 +++-- .../andcode/core/api/OpenCodeV2ApiClientTest.kt | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/yugahashimoto/andcode/core/api/OpenCodeV2ApiClient.kt b/app/src/main/java/com/yugahashimoto/andcode/core/api/OpenCodeV2ApiClient.kt index d36dd8390..3f5df52f3 100644 --- a/app/src/main/java/com/yugahashimoto/andcode/core/api/OpenCodeV2ApiClient.kt +++ b/app/src/main/java/com/yugahashimoto/andcode/core/api/OpenCodeV2ApiClient.kt @@ -186,8 +186,9 @@ class OpenCodeV2ApiClient( /** * Internal V2 agents (compaction, title, summary) are reported as `mode: primary` with - * `hidden: true`; the app's agent model has no hidden flag yet, so they are dropped here to - * keep them out of the mode picker and settings. + * `hidden: true`. V2 does not copy the flag onto [OpenCodeAgent]; it drops those agents here so + * they stay out of the mode picker and settings. Only `true` hides: a missing or `null` flag + * (as plugin agents send it) is visible. */ override suspend fun agents(): List = getData>("api/agent").filterNot { it.hidden == true }.map { agent -> diff --git a/app/src/test/java/com/yugahashimoto/andcode/core/api/OpenCodeV2ApiClientTest.kt b/app/src/test/java/com/yugahashimoto/andcode/core/api/OpenCodeV2ApiClientTest.kt index 1d2daf03b..af68371f5 100644 --- a/app/src/test/java/com/yugahashimoto/andcode/core/api/OpenCodeV2ApiClientTest.kt +++ b/app/src/test/java/com/yugahashimoto/andcode/core/api/OpenCodeV2ApiClientTest.kt @@ -799,7 +799,7 @@ class OpenCodeV2ApiClientTest { val agents = client().agents() - assertEquals(listOf("build"), agents.map { it.name }) + assertEquals(listOf("build", "orchestrator"), agents.map { it.name }) assertEquals("primary", agents[0].mode) } @@ -1139,6 +1139,7 @@ class OpenCodeV2ApiClientTest { val AGENT_LIST = """{"data":[{"id":"build","name":"Build","description":"The default agent.","mode":"primary","hidden":false,"permissions":[]},""" + + """{"id":"orchestrator","name":"Orchestrator","mode":"primary","hidden":null,"permissions":[]},""" + """{"id":"compaction","name":"Compaction","description":"internal","mode":"primary","hidden":true,"permissions":[]}]}""" val INTEGRATION_LIST =