Skip to content

feat(opencode): support the OpenCode V2 server API alongside V1 - #3

Merged
doublepi123 merged 2 commits into
mainfrom
feat/opencode-v2
Oct 15, 2026
Merged

doublepi123 merged 2 commits into
mainfrom
feat/opencode-v2

Conversation

@doublepi123

Copy link
Copy Markdown
Owner

Summary

OpenCode 2.x replaced the whole server contract (routes under /api, {data} envelopes, a typed message union, a rewritten event stream, questions → forms). A V1 client pointed at a V2 server only sees the SPA HTML. Upstream (yuga-hashimoto/and-code) has no V2 support yet.

  • ProtocolDetectingOpenCodeApi probes GET /api/info and falls back to the unchanged V1 OpenCodeApiClient (interface extraction only) when the server is not V2. Transient 5xx never decide the protocol; V1 is confirmed only through global/health; a re-detected protocol restarts the event stream.
  • OpenCodeV2ApiClient maps V2 onto the existing DTOs, so chat / activity / schedule layers are untouched:
    • sessions & messages walk cursor pages newest-first (order only on the first page; the server rejects it alongside a cursor)
    • archive is emulated with session metadata (V2 has no archive field)
    • agent/model are applied on every prompt under a per-session lock
    • providers merge V2 integrations so key/OAuth providers stay connectable; integration forms map to auth prompts
    • forms map to question cards and answer with option values
  • OpenCodeV2EventTranslator keeps part ids aligned with the REST transcript, maps execution lifecycle to busy / idle / abort, and keeps long compactions alive for stall detection. Reconnects reconcile pending permissions, forms and running sessions because V2 does not replay events.
  • V2 has no equivalent for message delete, todos, MCP OAuth or general config writes (PATCH /api/experimental/config only accepts shell): editMessages / new configEditing capabilities are gated off on V2.

Verification

  • ./gradlew spotlessCheck :app:testGithubDebugUnitTest (JDK 17 + SDK 35 in Docker): 1312 tests, 0 failures.
  • OpenCodeV2LiveTest against a real OpenCode v2.0.28 server (keyless Zen free model): 8/8 passed, none skipped — health/401, session CRUD + archive, catalog/files/vcs, a real streamed turn with a shell tool reloaded without duplicate parts, permission ask → once, question form answer, abort, summarize. It is skipped unless OPENCODE_LIVE_URL is set, so CI is unaffected.
  • detekt reports NO-SOURCE at the root (pre-existing configuration, same as CI).
  • Local on-device runtime stays pinned to OpenCode 1.18.5 (V1); detection is a no-op there.

Pre-PR review

判定: APPROVE

ブロッカー

なし

提案(非ブロッキング)

  • 前回の提案のうち、次の 3 点は未対応のままです。どれもマージを止める理由にはなりません。
    • capability ゲーティングのテスト: RemoteRuntimeTarget.capabilities と LocalRuntimeTarget.capabilities が、V2 判定後に editMessages / configEditing を false にすることを確認するテストがありません。
    • 既定値と再評価: ServerInfoViewModel.kt:48 の既定値は ?: false、SettingsViewModel.kt:248 は ?: true で食い違っています。また capability は getter なので、プローブ後に値が変わっても画面状態は自動で再計算されません。
    • プローブのバックオフ: プロトコル未確定のまま api/info が 5xx を返し続けると、current() が呼び出しのたびに再プローブします。
  • LocalOpenCodeBackend.protocol(LocalOpenCodeBackend.kt:93-94)を cached?.backend?.protocol に変えたので、invalidate() 直後や最初の delegate 生成前は null を返し、各 Target は V1 として capability を出します。ローカルランタイムは V1 固定(LocalRuntimeTarget.kt:54-58 のコメントどおり)なので実害はありません。将来ローカルに V2 を同梱する場合は、この一時的な窓に注意してください。

チェック済み項目

  • 前提: .git/worktrees/opencode-v2/logs/HEAD で、HEAD が 7d0b10fa(fix(opencode): confirm V1 servers with the profile's username)であることを確認しました。シェルが使えないため git show / git diff は実行できず、変更後のファイルを直接読んで確認しています。

  • ブロッカーの修正: ProtocolDetectingOpenCodeApi.kt:318-353 を確認しました。

    • fetchStatus に username 引数が追加されています。既定値は V2 の固定ユーザー OpenCodeV2ApiClient.USERNAME です。
    • confirmLegacy() の global/health は profile.username.ifBlank { "opencode" } で認証します。V1 クライアントの OpenCodeApiClient.kt:743 と同じ扱いです。
    • パスワードが空ならヘッダーを付けない挙動は変わっていません。
  • カスタムユーザー名の V1 サーバーの判定: api/info が 401 を返し、本文に _tag がなければ、:309 の分岐で confirmLegacy() に進み、alice で認証して V1 と判定されます。前回指摘したリグレッションは解消しています。

  • 回帰テスト: OpenCodeV2ApiClientTest.kt:110-128 の probe confirms a V1 server that uses a custom username が、次の 3 点を明示的に検証しています。

    • api/info が opencode:secret で認証されること
    • global/health が alice:secret で認証されること
    • 判定結果が OpenCodeProtocol.V1 になること

    profile() ヘルパーに追加された username 引数の既定値は "opencode" なので、既存のテストには影響しません。

  • LocalOpenCodeBackend: protocol getter が delegate() を呼ばなくなり、readMetadata() / LocalServerAuth.password() のディスク I/O が Compose のメインスレッドで走らなくなったことを確認しました(LocalOpenCodeBackend.kt:88-94)。KDoc もこの変更に合わせて更新されています。

  • その他: 修正範囲に新しいユーザー向け文字列、リソースキー、シークレットはありません。i18n にも影響しません。

  • CI 結果: spotlessCheck と :app:testGithubDebugUnitTest 1312 件の成功(live テスト 8 件、スキップ 0)は依頼者の報告です。こちらでは再実行していません。

OpenCode 2.x replaced the whole server contract: every route moved under
/api, responses are wrapped in {data}, messages became a typed union, the
event stream was rewritten into lifecycle events and questions became
forms. A V1 client pointed at a V2 server only ever sees the SPA HTML.

Connections now go through ProtocolDetectingOpenCodeApi, which probes
GET /api/info and falls back to the unchanged V1 client when the server
is not V2. The V2 client maps the new shapes onto the existing app DTOs
and OpenCodeEvent stream, so the chat, activity and schedule layers keep
working unmodified:

- sessions/messages walk cursor pages newest-first; archive is emulated
  with session metadata because V2 has no archive field
- prompts apply the chosen agent/model per prompt under a per-session lock
- forms map to question cards (labels answered as option values)
- the event translator keeps part ids aligned with the REST transcript,
  maps execution lifecycle to busy/idle/abort, and keeps long compactions
  alive for stall detection; reconnects reconcile pending permissions,
  forms and running sessions since V2 does not replay events
- message delete and config editing are gated off on V2, where the server
  offers no equivalent

OpenCodeV2LiveTest exercises the backend against a real server and only
runs when OPENCODE_LIVE_URL is set.
The protocol probe authenticated both api/info and the V1 global/health
confirmation as V2's fixed 'opencode' user, so a V1 server running with a
custom OPENCODE_SERVER_USERNAME could no longer be reached. Only api/info
keeps the fixed user now.

LocalOpenCodeBackend.protocol also stops building a delegate: it is read
from Compose on the main thread and building one reads from disk.
@doublepi123
doublepi123 merged commit f4bc87a into main Oct 15, 2026
5 checks passed
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.

1 participant