feat(ai): OpenCode provider via HTTP + SSE (#379)

* feat(ai): add OpenCode provider via HTTP + SSE

Bundle @opencode-ai/sdk to spawn `opencode serve` and communicate via
HTTP REST + SSE. Richest provider: fork, resume, streaming, tools, and
runtime permission approvals all work natively.

- OpenCodeProvider: lazy server spawn, shared HTTP client, dynamic model
  discovery via GET /provider
- OpenCodeSession: SSE event drain loop, system prompt via API field,
  permission reply (once/reject), abort via HTTP
- mapOpenCodeEvent: SSE → AIMessage mapping (text deltas, tool lifecycle,
  permissions, session status, errors)
- 11 new event mapping tests (78 total, 157 expect calls)

For provenance purposes, this commit was AI assisted.

* docs(ai): add OpenCode to AI features guide and code review docs

For provenance purposes, this commit was AI assisted.

* fix(ai): prevent double server spawn in OpenCode ensureServer()

Use a promise-mutex pattern so concurrent callers (e.g. fetchModels +
createSession) share a single server spawn instead of racing to create two.
Also remove empty finally block from SSE drain loop.

For provenance purposes, this commit was AI assisted.

* fix(ai): await SSE subscribe in OpenCode provider

The SDK's event.subscribe() is async — must be awaited before
destructuring the stream.

For provenance purposes, this commit was AI assisted.

* fix(ui): scrollable model dropdown with search filter

Cap dropdown height at min(320px, 50vh) so it doesn't overflow the
viewport when providers expose many models (e.g. OpenCode with 20+).
Add a search/filter input when there are more than 8 models.

For provenance purposes, this commit was AI assisted.

* fix(ai): close SSE stream, rename to opencode-sdk, move dep to root

- Close SSE stream in finally block to prevent connection leak on
  abort/early return
- Rename provider from opencode to opencode-sdk for consistency with
  claude-agent-sdk, codex-sdk, pi-sdk
- Move @opencode-ai/sdk dependency to root package.json alongside the
  other SDK dependencies

For provenance purposes, this commit was AI assisted.

* feat(ui): add first-run AI setup dialog for provider selection

Show a one-time modal on first review when AI providers are available.
Users pick a default provider and model from a 2-column grid, with a
brief feature intro and auth disclaimer. Persists via cookie, can be
changed later in Settings.

For provenance purposes, this commit was AI assisted.

* fix(ai): clear startPromise on failure so OpenCode provider can retry

If doStart() fails (port conflict, timeout, binary crash), the cached
rejected promise prevented all future ensureServer() calls from
retrying. Now cleared on failure and on dispose().

Also updates stale "future" comment in types.ts.

For provenance purposes, this commit was AI assisted.
This commit is contained in:
Michael Ramos
2026-03-23 17:37:56 -07:00
committed by GitHub
parent 877b0c78ac
commit d4ff4a8df7
16 changed files with 986 additions and 30 deletions
+18
View File
@@ -154,6 +154,24 @@ export async function startReviewServer(
// Pi not available
}
// Try OpenCode
try {
const { OpenCodeProvider } = await import("@plannotator/ai/providers/opencode-sdk");
const opencodePath = Bun.which("opencode");
if (opencodePath) {
const provider = await createProvider({
type: "opencode-sdk",
cwd: process.cwd(),
});
if (provider instanceof OpenCodeProvider) {
await provider.fetchModels();
}
aiRegistry.register(provider);
}
} catch {
// OpenCode not available
}
// Create endpoints if any provider registered
if (aiRegistry.size > 0) {
aiEndpoints = createAIEndpoints({