Skip to content

feat: integrate OpenCode as alternative coding agent#3

Merged
achimala merged 2 commits into
achimala:mainfrom
JohnConnorNPC:feat/opencode-integration
Feb 18, 2026
Merged

feat: integrate OpenCode as alternative coding agent#3
achimala merged 2 commits into
achimala:mainfrom
JohnConnorNPC:feat/opencode-integration

Conversation

@JohnConnorNPC
Copy link
Copy Markdown
Contributor

@JohnConnorNPC JohnConnorNPC commented Feb 18, 2026

Summary

Adds OpenCode as a second coding agent backend alongside Codex. Farfield can now route conversations to either backend based on a per-thread agentKind discriminator.

  • New @farfield/opencode-api package — wraps @opencode-ai/sdk with connection management, a service layer, and a mapper that translates OpenCode sessions/messages into the Codex protocol types Farfield already uses
  • Server-side routing — all existing REST+SSE endpoints now support both agents; enable with FARFIELD_OPENCODE_ENABLED=true; gracefully degrades when Codex CLI is not installed
  • Agent-aware frontend — agent selector, "OC" badges, capability-gated UI (hides Plan/Model/Effort for OpenCode), dynamic composer placeholder, auto-creates thread on first send

How to run

Prerequisites

  1. OpenCode must be running and accessible (default http://localhost:47741)
  2. Node.js 20+, pnpm 9+

Development mode

# Linux / macOS
FARFIELD_OPENCODE_ENABLED=true pnpm dev

# PowerShell
$env:FARFIELD_OPENCODE_ENABLED="true"; pnpm dev

Opens at http://localhost:5173. The server starts on port 3578.

Production mode

pnpm build

# Linux / macOS
FARFIELD_OPENCODE_ENABLED=true pnpm start

# PowerShell
$env:FARFIELD_OPENCODE_ENABLED="true"; pnpm start

Opens at http://localhost:3578.

Environment variables

Variable Default Description
FARFIELD_OPENCODE_ENABLED false Enable OpenCode agent backend
OPENCODE_API_URL http://localhost:47741 OpenCode API base URL
PORT 3578 Server port

Notes

  • If Codex CLI is not installed, the server auto-detects the ENOENT error and disables Codex endpoints — OpenCode becomes the sole backend automatically
  • When both agents are available, a dropdown in the toolbar lets you pick which agent to use for new threads
  • Existing Codex threads continue to work normally alongside OpenCode threads

Test plan

  • All 53 tests pass (pnpm test) — mapper tests, protocol tests, web render test, server schema tests
  • pnpm build succeeds with no TypeScript errors
  • Manual: start with FARFIELD_OPENCODE_ENABLED=true pnpm dev, verify OpenCode threads can be created and messages sent
  • Manual: start without Codex installed, verify graceful degradation (only OpenCode available)
  • Manual: start with both Codex and OpenCode, verify agent selector appears and both work

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Introduced multi-agent support: users can now select and switch between available agents (Codex and OpenCode) when creating new threads
    • Agent selection option added to toolbar when multiple agents are available
    • Thread responses now display which agent processed the request for improved transparency
    • Chat composer placeholder dynamically adapts based on the selected agent

Add support for OpenCode (https://github.com/anomalyco/opencode) as a second
coding agent alongside Codex. Farfield can now route conversations to either
backend based on a per-thread `agentKind` discriminator ("codex" | "opencode").

Key changes:

- **packages/opencode-api/** — New workspace package wrapping `@opencode-ai/sdk`.
  Includes connection management, a service layer (create session, send message,
  list sessions, read session), and a mapper that translates OpenCode sessions /
  messages into the Codex protocol types Farfield already understands. 10 mapper
  unit tests included.

- **apps/server/** — Server routes OpenCode traffic through the same REST+SSE
  surface used by Codex. Enable with `FARFIELD_OPENCODE_ENABLED=true`. Gracefully
  degrades when Codex CLI is not installed (detects ENOENT, disables Codex
  endpoints, auto-switches default agent to OpenCode). Thread-agent tracking via
  `agent-kind.ts` maps thread IDs to their backend.

- **apps/web/** — Frontend is now agent-aware: agent selector in toolbar, "OC"
  badge on OpenCode threads, capability-gated UI (hides Plan/Model/Effort
  selectors for OpenCode threads), dynamic composer placeholder, and auto-creates
  a new thread on first send when none exists.

- **Tests** — All 53 tests pass (mapper tests, protocol tests, web render test,
  server schema tests). Pre-existing jsdom mocks added for matchMedia,
  ResizeObserver, scrollTo.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@achimala
Copy link
Copy Markdown
Owner

Whoa this is sick. I'll take a closer look shortly. It would be nice if we could encapsulate different providers behind some abstract interface rather than mushing them all together, but we can clean that up later

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 18, 2026

📝 Walkthrough

Walkthrough

This pull request introduces dual-agent support by integrating the OpenCode AI agent alongside the existing Codex agent. A new @farfield/opencode-api package provides OpenCode session management and data mapping. The server gains agent-kind tracking, configuration for OpenCode, and conditional endpoints for both agents. The web UI adds agent selection, thread creation with agent support, and dynamic UI adaptation based on available agents.

Changes

Cohort / File(s) Summary
Workspace Dependencies
apps/server/package.json
Added workspace dependency on @farfield/opencode-api package.
Agent Infrastructure
apps/server/src/agent-kind.ts
New module that centralizes per-thread agent-type handling with registration, resolution, and listing functions for "codex" or "opencode" agents.
Server Schemas
apps/server/src/http-schemas.ts
Updated StartThreadBodySchema to include optional agentKind field with enum values for "codex" and "opencode".
Server Core Integration
apps/server/src/index.ts
Extensive integration of OpenCode support including configuration, connection management, dual-path thread/session endpoints, merged data endpoints, agent availability reporting, and startup/shutdown lifecycle.
OpenCode API Package
packages/opencode-api/package.json, packages/opencode-api/tsconfig.json
New package structure with module configuration, TypeScript setup, and dependency declarations for OpenCode integration.
OpenCode Client
packages/opencode-api/src/client.ts
New OpenCodeConnection class managing connections to OpenCode servers or clients with start/stop and status methods.
OpenCode Data Mapping
packages/opencode-api/src/mapper.ts
Comprehensive mappers translating OpenCode sessions, messages, and events into internal thread/conversation models with tool and file handling.
OpenCode Service
packages/opencode-api/src/service.ts
New OpenCodeMonitorService class wrapping OpenCodeConnection to expose session and message operations (list, create, get, send, abort, delete).
OpenCode Package Exports
packages/opencode-api/src/index.ts
Barrel file re-exporting client, mapper, and service modules.
Web API Client
apps/web/src/lib/api.ts
Extended API layer with AgentKind type, listAgents endpoint, and agent-kind fields in thread-related schemas and response types.
Web UI Core
apps/web/src/App.tsx
Added agent selection state and UI, dynamic thread creation with agent support, agent availability detection, and conditional UI rendering based on agent states.
Web Chat Component
apps/web/src/components/ChatComposer.tsx
Made placeholder text customizable via optional prop with default value "Message Codex…".
Tests
apps/web/test/app.test.tsx, packages/opencode-api/test/mapper.test.ts
Added environment polyfills for jsdom (scrollTo, ResizeObserver, matchMedia), extended fetch mocks for /api/agents, updated UI assertions; added comprehensive mapper test suite with builder factories.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant WebApp as Web App
    participant Server
    participant AgentRouter as Agent Router
    participant Codex
    participant OpenCode

    User->>WebApp: Open application
    WebApp->>Server: GET /api/agents
    Server->>Server: Check available agents
    Server-->>WebApp: {availableAgents, defaultAgent}
    WebApp->>WebApp: Render agent selector

    User->>WebApp: Select OpenCode agent & type message
    WebApp->>WebApp: Set selectedAgentKind = "opencode"
    
    alt No thread selected
        WebApp->>Server: POST /api/threads with agentKind="opencode"
        Server->>AgentRouter: Route to OpenCode
        AgentRouter->>OpenCode: Create session
        OpenCode-->>Server: sessionId
        Server-->>WebApp: threadId, agentKind="opencode"
    end
    
    WebApp->>Server: POST /api/threads/{id}/messages with agentKind="opencode"
    Server->>AgentRouter: Route message to OpenCode
    AgentRouter->>OpenCode: Send prompt message
    OpenCode->>OpenCode: Process & execute
    OpenCode-->>Server: Session state update
    Server-->>WebApp: Message response with agentKind
    WebApp->>WebApp: Render message from OpenCode

    User->>WebApp: Switch to Codex agent
    WebApp->>WebApp: Set selectedAgentKind = "codex"
    WebApp->>Server: POST /api/threads/{id}/messages with agentKind="codex"
    Server->>AgentRouter: Route to Codex
    AgentRouter->>Codex: Send message
    Codex-->>Server: Response
    Server-->>WebApp: Message response with agentKind="codex"
    WebApp->>WebApp: Render message from Codex
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 Hop, hop, code now flows both ways,
Codex and OpenCode work in tandem's maze,
Agent selection blooms with careful grace,
Dual minds speaking in each thread's space,
A rabbit's delight—two brains, twice the race! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: integrate OpenCode as alternative coding agent' is a clear, concise summary of the main change: integrating OpenCode as an alternative backend agent alongside Codex.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

@achimala
Copy link
Copy Markdown
Owner

Giving the branch a try. It works though I had to jump through some hoops to get it to pick OpenCode over Codex and find the right directory. Lemme try to make that smoother, then I'll merge this and layer my changes on top

@achimala achimala self-assigned this Feb 18, 2026
@achimala achimala self-requested a review February 18, 2026 19:20
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/src/App.tsx (1)

938-951: ⚠️ Potential issue | 🟠 Major

Default new thread creation to the selected agent.

createNewThread at line 1028 is called from the sidebar without an agentKind parameter, causing threads to be created with agentKind undefined. This means the selected agent kind is ignored when users create threads via the sidebar, which will fail in OpenCode-only setups that don't support Codex. Default to selectedAgentKind inside the helper and add it to the dependency array.

💡 Suggested fix
-  const createNewThread = useCallback(async (projectPath: string, agentKind?: AgentKind) => {
+  const createNewThread = useCallback(async (projectPath: string, agentKind?: AgentKind) => {
+    const effectiveAgentKind = agentKind ?? selectedAgentKind;
     const trimmedProjectPath = projectPath.trim();
     if (!trimmedProjectPath) {
       setError("Cannot create thread: missing project path");
       return;
     }
     setIsBusy(true);
     try {
       setError("");
       const created = await createThread({
         cwd: trimmedProjectPath,
-        ...(agentKind ? { agentKind } : {})
+        ...(effectiveAgentKind ? { agentKind: effectiveAgentKind } : {})
       });
       pendingMaterializationThreadIdsRef.current.add(created.threadId);
       setSelectedThreadId(created.threadId);
       selectedThreadIdRef.current = created.threadId;
       setMobileSidebarOpen(false);
       await refreshAll();
     } catch (e) {
       setError(toErrorMessage(e));
     } finally {
       setIsBusy(false);
     }
-  }, [refreshAll]);
+  }, [refreshAll, selectedAgentKind]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/App.tsx` around lines 938 - 951, createNewThread currently
ignores the UI's selected agent when agentKind is undefined; update
createNewThread to default agentKind to the outer selectedAgentKind (use
something like const effectiveAgentKind = agentKind ?? selectedAgentKind) when
building the payload for createThread, and add selectedAgentKind to the
useCallback dependency array so the hook updates when the selection changes;
reference the createNewThread function, the createThread call, and
selectedAgentKind when making these changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/server/src/agent-kind.ts`:
- Around line 3-11: threadAgentMap currently stores agent-kind bindings only in
memory, so on restart resolveAgentKind falls back to "codex"; persist and
rehydrate that mapping. Modify registerThreadAgent to write the mapping to
durable storage (e.g., DB/file/Thread metadata) whenever called and change
module init/bootstrap to load persisted mappings into threadAgentMap during
startup (the same place that uses appClient.listThreads()). Ensure
resolveAgentKind reads from threadAgentMap as before but after rehydration so
threads retain their original AgentKind across restarts; reference
functions/vars: threadAgentMap, registerThreadAgent, resolveAgentKind, and the
bootstrap logic that calls appClient.listThreads().

In `@apps/server/src/index.ts`:
- Around line 926-935: When merging OpenCode sessions into mergedData after
openCodeService.listSessions(), register each session ID into the in-memory
OpenCode registry so isOpenCodeThread() will return true; iterate ocResult.data
(or ocData) and call the project’s OpenCode registration API (for example
registerOpenCodeThread(sessionId) or
openCodeSessionRegistry.register(sessionId)) for each session before setting
mergedData = [...codexData, ...ocData]; this ensures thread-specific endpoints
route to OpenCode after a server restart.

In `@apps/web/src/App.tsx`:
- Around line 1416-1420: The placeholder logic uses availableAgents when no
thread is selected, which can show "Message OpenCode…" even if the user has
chosen Codex; update the conditional to base the placeholder on the currently
selected agent state instead of agent availability: when selectedThreadId is
falsy, check the app's selected agent indicator (e.g., selectedAgent or
selectedAgentId / a boolean like isSelectedAgentOpenCode) and use
isActiveThreadOpenCode equivalent for that selected agent to decide between
"Message OpenCode…" and "Message Codex…"; keep the existing branch that uses
isActiveThreadOpenCode when a thread is selected.
- Around line 532-537: The current branch only updates availableAgents when
enabledAgents.length > 0, leaving stale values like ["codex"] when there are no
enabled agents; always call setAvailableAgents(enabledAgents) (not only on
length>0) and, if enabledAgents.length === 0, clear the current selection by
calling setSelectedAgentKind(null) (and keep setDefaultAgent(nag.defaultAgent)
if you still want to remember the default). Update the block around nag to use
setAvailableAgents(enabledAgents) unconditionally and explicitly handle the
empty-case by clearing selected agent so the composer shows the correct empty
state.

In `@packages/opencode-api/src/mapper.ts`:
- Around line 127-188: The current messagesToTurns function builds
assistantByParent as Map<string, AssistantMessage> which overwrites earlier
assistant messages when multiple assistant messages share the same parentID;
change this to collect all assistants per parentID (e.g., Map<string,
AssistantMessage[]>) or explicitly pick one deterministically (e.g., the latest
by assistantMsg.time.created) and document the choice; update the loop that
currently sets assistantByParent.set(msg.parentID, msg) to push into an array or
to compare timestamps and keep the newest, then adjust later lookup
(assistantByParent.get(userMsg.id)) and the logic that builds items, status,
error, finalAssistantStartedAtMs, turnId, etc., to either emit multiple turns
per user message when multiple assistants exist or to use the selected assistant
consistently (update references to assistantMsg accordingly) and add a short
comment in messagesToTurns explaining the chosen behavior.

In `@packages/opencode-api/src/service.ts`:
- Around line 97-112: In sendMessage, avoid mutating the outgoing message by
trimming only for validation: create a trimmed copy (e.g., const trimmed =
input.text.trim()) and throw when trimmed is empty, but pass the original
input.text to client.session.prompt; update the code paths in the sendMessage
method (OpenCodeSendMessageInput handling and the client.session.prompt body
parts) so the prompt uses the original string rather than the trimmed value.

---

Outside diff comments:
In `@apps/web/src/App.tsx`:
- Around line 938-951: createNewThread currently ignores the UI's selected agent
when agentKind is undefined; update createNewThread to default agentKind to the
outer selectedAgentKind (use something like const effectiveAgentKind = agentKind
?? selectedAgentKind) when building the payload for createThread, and add
selectedAgentKind to the useCallback dependency array so the hook updates when
the selection changes; reference the createNewThread function, the createThread
call, and selectedAgentKind when making these changes.

Comment thread apps/server/src/agent-kind.ts
Comment thread apps/server/src/index.ts
Comment thread apps/web/src/App.tsx
Comment thread apps/web/src/App.tsx
Comment thread packages/opencode-api/src/mapper.ts
Comment thread packages/opencode-api/src/service.ts
@achimala achimala merged commit c8dc710 into achimala:main Feb 18, 2026
1 check 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.

2 participants