Repository navigation
feat: OpenCode as a first-class agent, verified on device - #1840
ethicnology-agent wants to merge 3 commits into
Conversation
fac09a3 to
e34f94b
Compare
|
Pushed a revision after auditing my own diff. Five things, all found by reading rather than by review feedback — recording them here so the delta is legible. Only one of the two composers was wired. Preselection ignored the agent's own current model. The Test coverage was thinner than it should have been. I had modified One prose comment I had left badly rewrapped. Verification: One thing I did not change, for the record: the daemon drops |
The daemon resolved the agent subcommand twice, in two different styles. The plain-process path used a switch whose default returned "Unsupported agent type", while the tmux path a hundred lines above ended a ternary chain with 'claude'. An agent the app knows and this CLI does not therefore started correctly on a machine without tmux and silently started Claude Code on a machine with it — no error, no log, the wrong agent reading the wrong files. Resolve through one table instead, so the two paths cannot drift again, and give the tmux path the same loud failure the other one already had.
OpenCode already runs through the generic ACP path — KNOWN_ACP_AGENTS maps it to `opencode acp`, resolveSessionFlavor already reports the `opencode` flavor, and runAcp already publishes the models, modes and slash commands OpenCode advertises over ACP. What was missing was everything that makes an agent visible: a named subcommand, daemon spawn, CLI detection, and the app lists. happy-cli gains a `happy opencode` subcommand that resolves through KNOWN_ACP_AGENTS rather than repeating the command line, an `opencode` entry in the daemon's spawn allowlist and shared spawn options, and a detection probe whose boolean rides in machine metadata as an optional field so older daemons keep validating. Unlike its siblings, this subcommand forwards its extra arguments to the agent, so it must first separate Happy's own. The daemon spawns every agent with `--happy-starting-mode remote --started-by daemon`, and an ACP agent given a flag it does not know exits non-zero — spawning OpenCode from the app failed outright before this. parseHappyAgentArgs therefore withholds the whole `--happy-` namespace rather than today's two flags: fixing one flag would leave the next one the daemon invents to break the same way, at a distance, in someone else's commit. happy-app gains the agent id, the harness name and pick order, the spawn option unions, a picker entry and badge icon, neutral stored defaults, the settings label, and the CLI-availability row. OpenCode follows Antigravity's rule and is offered only on an explicit installation report, because a daemon predating this detection reports nothing for it and the app-side schema drops keys it does not declare. modelModeOptions gets an OpenCode branch holding a single neutral `default` entry, not a catalog. Its lists are consulted only when no session has reported one over ACP, and getAvailableModels prefers the reported catalog as soon as it exists — so naming OpenCode here cannot discard what it publishes. What it does fix is the fallthrough: every unnamed flavor landed on Claude's lists, which put Opus and Sonnet in front of someone starting an OpenCode chat, and the CLI silently ignores a model its agent does not know. The composer chip's fallback label was Gemini, so an unlisted agent read as "Gemini"; OpenCode now has its own branch and string in every locale. The icon is a placeholder terminal glyph. It should be replaced with OpenCode's own mark before this ships.
An ACP agent has no catalog to offer until a session exists — OpenCode sends its models and modes as configOptions on the session/new response. The new-chat composer runs before that, so it falls back to a hardcoded list, and for any agent whose catalog is not hardcoded there is nothing truthful to show. The choice is then between another vendor's model names and a lone "default" that hides the choice until the chat has already started. Neither is necessary. The same agent reported a real catalog the last time it ran on this computer, and that is the closest honest answer available before the session exists. Reuse it, and let the session's own catalog take over the moment it arrives — the fallback order is reported, then last-known, then hardcoded, so this can only fill a gap, never override. Scoped to the same machine on purpose: a catalog learned elsewhere can name models this computer has no credentials or local weights for, and offering those is worse than offering nothing. Preselection follows the same reasoning. `default` is not a key in a reported catalog, so landing on the agent's own current model is what keeps the composer from presenting whichever model the agent happened to list first as though it were a choice. An explicit saved pick still wins over both. Two screens compose a new session — the home dock and the full /new page — and they derived these lists independently with the same three-line expression. They had already drifted once. Both now resolve through newSessionModeSelection, so a fix to one cannot silently leave the other showing a different catalog for the same agent. This is what lets the model be picked in the same breath as the agent. On a Galaxy S10e the OpenCode draft lists Big Pickle and the Muse Spark models before the session starts, where it previously listed Claude's.
e34f94b to
ece80f5
Compare
|
Rebased onto current @bra1nDump no rush at all, but these six have been open since Sept 23 and no CI has run on them yet — fork PRs need a maintainer to approve the workflows. Whenever you have a moment, could you kick those off, or tell me if you'd rather I split or drop any of them? |
Summary
Makes OpenCode a first-class agent — a named subcommand, daemon spawn, CLI detection, and every app list — and fixes the two things that stopped it working once it was visible.
Verified end to end on a real phone (Galaxy S10e, Android 12, dev APK against a local server), not only in unit tests. A 31-second clip of the whole path, recorded on that device:
https://github.com/ethicnology-agent/happy/releases/download/opencode-demo/opencode-30s.mp4
Sessions list → new chat → agent picker → OpenCode → model picker → OpenCode Zen / Big Pickle → send → reply. No credentials of any kind:
opencode/big-pickleanswers on OpenCode Zen's free tier, which makes this reproducible by any reviewer.Relationship to #1741
@jdistler's draft #1741 covers the same ground and landed first. I did not see a way to contribute to a draft on someone else's fork, so this is a separate branch rather than a competing claim — if #1741 is the one you want, close this and I will reopen with only the delta below. Say the word and I will do that instead.
What is here and not in #1741:
--happy-starting-modeis consumed, not forwarded. The daemon always adds this flag. An ACP session has no local/remote variant to select, andopencode acpexits 1 on an unknown flag — so spawning OpenCode from the app failed outright. This is a device-only failure; it cannot show up in a unit test, and I only found it by pressing the button.getHardcodedModelModesfalls through togetClaudeModelModes()for any unnamed flavor, so an OpenCode draft offered Opus and Sonnet. See the third commit.cliAvailability.opencodeinstorageTypes.ts. The app-side zod schema strips keys it does not declare, so without this the harness is never offered however well the daemon detects it.Gemini, so any unlisted agent read as "Gemini".Commits
fix(cli): fail loudly when the tmux spawn path meets an unknown agent— pre-existing bug, independent of OpenCode. The daemon resolved the agent subcommand twice in two styles: the plain-process path returned "Unsupported agent type" on an unknown agent, while the tmux path ended a ternary chain with'claude'. An agent the app knows and the CLI does not therefore started correctly without tmux and silently started Claude Code with tmux — no error, no log, the wrong agent reading the wrong files. Both paths now resolve through one table.feat: add OpenCode as a first-class agent— the plumbing. OpenCode follows Antigravity's rule and is offered only on an explicit installation report, since a daemon predating this detection reports nothing for it.feat(app): offer an ACP agent's own models before its first session— implements the idea in #1227. An ACP agent has no catalog until a session exists; OpenCode sends its models and modes asconfigOptionson thesession/newresponse. The composer runs before that, so the choice was between another vendor's model names and a lone "default" that hides the choice until the chat has already started. Neither is necessary: the same agent reported a real catalog the last time it ran on this computer. Fallback order is reported → last-known → hardcoded, so this can only fill a gap, never override. Scoped to the same machine on purpose — a catalog learned elsewhere can name models this computer has no credentials for.What I did not do
The agent icon is a placeholder terminal glyph. I have no right to OpenCode's mark and did not want to guess at it; it should be replaced before this ships. #1741 may already have a better one.
Verification
tsc --noEmitclean onhappy-app.happy-app: 1904 passed, 1 skipped. The one failure issessionPresentation.test.ts(__DEV__), which fails identically onorigin/main.happy-cli: newagentCommand.test.ts, 4 tests, covering the unknown-agent case that previously fell through to Claude.lastAgentCatalog.test.ts, 5 tests, including that OpenCode never inherits Claude's models and that a catalog from another machine is ignored.