Skip to content

Conversation

@D36u99er
Copy link

@D36u99er D36u99er commented Jan 7, 2026

Summary

This PR adds support for OpenCode (https://github.com/anomalyco/opencode) as a new agent flavor, enabling mobile control of OpenCode AI terminals through the Happy app.

Key Changes

  • New happy opencode subcommand - Start OpenCode sessions with mobile control
  • HTTP/SSE API Client - Clean integration using OpenCode's native REST API (port 4096), unlike Claude/Codex PTY spawning
  • Real-time event streaming - SSE-based message synchronization for tool calls, reasoning, and todos
  • Permission system integration - Forward permission requests to mobile app with full reply options
  • Model/Provider selection - Support --model and --provider flags for model configuration

Files Added

src/opencode/
├── index.ts              # Module exports
├── types.ts              # TypeScript type definitions for OpenCode API
├── openCodeClient.ts     # HTTP/SSE client for OpenCode server
├── runOpenCode.ts        # Main entry point (similar to runClaude.ts)
├── messageMapper.ts      # Message format conversion (Happy <-> OpenCode)
└── utils/
    └── permissionHandler.ts  # Permission request handling

Usage

# Start with default settings
happy opencode

# Start with all permissions bypassed
happy opencode --yolo

# Use specific model
happy opencode -m gpt-4o -p openrouter

# Show help
happy opencode --help

Why OpenCode?

OpenCode provides:

  • Native HTTP API (cleaner than PTY-based integration)
  • Best-in-class context management
  • Support for multiple AI providers (OpenRouter, Anthropic, OpenAI, etc.)
  • Active development (52k+ stars)

Related

Testing

  • TypeScript compilation passes
  • Build succeeds
  • OpenCode API connectivity verified locally

- Add new 'happy opencode' subcommand for OpenCode AI terminal control
- Implement OpenCodeClient with HTTP/SSE API support (port 4096)
- Add message format conversion between Happy and OpenCode
- Implement permission handling with mobile app integration
- Support model/provider selection and permission modes (--yolo, --accept-edits)

OpenCode provides a native HTTP API unlike Claude/Codex PTY spawning,
enabling cleaner integration with real-time SSE event streaming.
@oribarilan
Copy link

@D36u99er is this PR still being pursued? :)

};
}

export type OpenCodePermissionReply =

Choose a reason for hiding this comment

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

// PR uses:
type OpenCodePermissionReply = 'allow' | 'allowSession' | 'allowForever' | 'deny' | 'denySession' | 'denyForever';

// OpenCode actually uses:
type PermissionReply = 'once' | 'always' | 'reject';

Impact: All permission responses will fail. The API expects once/always/reject, but the code sends allow/allowSession/etc.

Fix Required: Update OpenCodePermissionReply to 'once' | 'always' | 'reject' and update all usages.
https://github.com/anomalyco/opencode/blob/aa17729008cfeb94c550c0b1b0f14aeae54a372a/packages/web/src/content/docs/permissions.mdx#L141

this.emit('message:part', payload.properties as unknown as OpenCodeMessagePart);
break;

case 'permission.created':

Choose a reason for hiding this comment

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

case 'permission.created': // Wrong
this.emit('permission:request', ...);

OpenCode emits permission.asked, not permission.created. Permission requests will never be received.

Fix: Change to case 'permission.asked':.

Choose a reason for hiding this comment

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

Missing session.error Event Handling (openCodeClient.ts)

The SSE handler doesn't handle session.error events. When OpenCode encounters an error (API error, message aborted, output length exceeded), the client will silently ignore it.

Impact: Errors go unreported to the mobile client.

Choose a reason for hiding this comment

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

Missing Signal Handlers (runOpenCode.ts)

Unlike runClaude.ts (lines 362-375), runOpenCode.ts lacks:

  • SIGTERM/SIGINT handlers
  • uncaughtException handler
  • unhandledRejection handler

Risk: Orphaned sessions when process is killed, no cleanup performed.

parts: OpenCodeMessagePart[];
}

export interface OpenCodePermissionRequest {

Choose a reason for hiding this comment

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

Permission Request Structure Mismatch

OpenCode's PermissionRequest has different fields than defined:

// PR defines:
interface OpenCodePermissionRequest {
id: string;
sessionID: string;
messageID: string; // Not in OpenCode
partID: string; // Not in OpenCode
metadata: { ... }; // Different structure
}

// OpenCode actually has:
interface PermissionRequest {
id: string;
sessionID: string;
permission: string; // e.g., "read", "bash", "edit"
patterns: string[]; // Patterns being requested
metadata: { ... };
always: string[]; // Patterns to approve if "always"
tool?: { messageID, callID };
}

Choose a reason for hiding this comment

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

Missing Message Part Types, OpenCode has more message part types not handled:

  • CompactionPart - for context compaction
  • SubtaskPart - for agent subtasks
  • RetryPart - for retry information
  • StepFinishPart - for step completion

);
}

private getPermissionKey(request: OpenCodePermissionRequest): string {

Choose a reason for hiding this comment

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

Using JSON.stringify for key generation is fragile (object property order). Use a deterministic hash like hashObject from existing utils.

Choose a reason for hiding this comment

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

OpenCode supports HTTP basic auth via OPENCODE_SERVER_PASSWORD environment variable. The client doesn't include auth headers.

export class OpenCodeClient extends EventEmitter {
private baseUrl: string;
private timeout: number;
private eventSource: EventSource | null = null;

Choose a reason for hiding this comment

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

Unused eventSource Property, the code uses fetch-based SSE, but declares an unused EventSource property.

const sessions = await this.listSessions();
const dir = directory || process.cwd();

const existing = sessions.find(s => s.directory === dir);

Choose a reason for hiding this comment

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

This exact match may fail with trailing slashes or symlinks. Consider normalizing paths.

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.

4 participants