Skip to content

feat: OpenCode as a first-class agent, verified on device - #1840

Open
ethicnology-agent wants to merge 3 commits into
slopus:mainfrom
ethicnology-agent:feat/opencode-first-class
Open

ethicnology-agent wants to merge 3 commits into
slopus:mainfrom
ethicnology-agent:feat/opencode-first-class

Conversation

@ethicnology-agent

Copy link
Copy Markdown

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-pickle answers 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:

  1. --happy-starting-mode is consumed, not forwarded. The daemon always adds this flag. An ACP session has no local/remote variant to select, and opencode acp exits 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.
  2. The pre-session model picker showed Claude's models. getHardcodedModelModes falls through to getClaudeModelModes() for any unnamed flavor, so an OpenCode draft offered Opus and Sonnet. See the third commit.
  3. cliAvailability.opencode in storageTypes.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.
  4. The tmux spawn path. Separate commit, separate bug — see below.
  5. The composer chip's fallback label was 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 as configOptions on the session/new response. 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 --noEmit clean on happy-app.
  • happy-app: 1904 passed, 1 skipped. The one failure is sessionPresentation.test.ts (__DEV__), which fails identically on origin/main.
  • happy-cli: new agentCommand.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.
  • On-device: agent picker, model picker, spawn, prompt, reply, and session status — the clip above.

@ethicnology-agent

Copy link
Copy Markdown
Author

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. app/(app)/new/index.tsx duplicated HomeDock's option block, and I had patched only HomeDock. Worse, my own change made that screen lose its model picker for OpenCode: the neutral single-entry fallback left modelModes.length === 1, and showModel is length > 1. Both screens now resolve through newSessionModeSelection, so the next fix cannot land on one and miss the other.

Preselection ignored the agent's own current model. findLastAgentCatalog returned currentModelCode and nothing read it. Since default is not a key in a reported catalog, resolveOption fell through to options[0] — whichever model the agent happened to list first, presented as a choice. It now sits between the saved pick and the configured default.

The --happy-starting-mode fix treated a symptom. My subcommand is the only one that forwards extra arguments to its agent, so I created the hazard and then special-cased one flag. parseHappyAgentArgs now withholds the whole --happy- namespace; the next flag the daemon adds cannot break the spawn from a distance. Extracted, so it is testable — which the inline loop was not.

Test coverage was thinner than it should have been. I had modified detectCLI.ts, modelModeOptions.ts, avatarHarness.ts and agentDefaults.ts without touching any of their existing test files, while claiming fixes. All four are covered now, plus the new modules. Red-Green on the namespace rule: disabling the --happy- branch fails 4 of the 8 new tests.

One prose comment I had left badly rewrapped.

Verification: tsc clean on both packages, app suite 1915 passed / 1 skipped (the pre-existing __DEV__ failure, identical on origin/main), happy-cli 1003 passed. The two happy-cli integration files that error out want pnpm on PATH and fail the same way before this branch. Each commit typechecks and tests green on its own.

One thing I did not change, for the record: the daemon drops modelMode at spawn for any agent that is not Claude or Codex (appendDaemonSpawnModeArgs returns early), so the picked model reaches OpenCode on the first prompt instead, via mode.model and switchModelIfRequested. That works, but an unresolvable model code is discarded with only a logger.debug — pick a model that has since disappeared from the agent's catalog and you silently get its default. Pre-existing, and out of scope here, but this change makes it easier to reach.

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.
@ethicnology-agent
ethicnology-agent force-pushed the feat/opencode-first-class branch from e34f94b to ece80f5 Compare October 1, 2026 19:03
@ethicnology-agent

Copy link
Copy Markdown
Author

Rebased onto current main (4cf54d1). Clean rebase; tsc clean on happy-app and happy-cli, and the touched modules' suites pass (74 tests). This branch was the only one of the six overlapping files main has since changed, so it got the extra check.

@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?

#1834 · #1835 · #1836 · #1837 · #1839 · #1840

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants