Skip to content
This repository was archived by the owner on Sep 18, 2025. It is now read-only.

Latest commit

 

History

History
729 lines (582 loc) · 17.4 KB

File metadata and controls

729 lines (582 loc) · 17.4 KB

OpenCode Development Guide

Overview

This guide provides comprehensive information for developers working on the OpenCode project. It covers development setup, code conventions, testing strategies, and contribution guidelines.

Prerequisites

  • Go: Version 1.24.0 or higher
  • SQLite: For database operations (usually system-provided)
  • Git: For version control

Development Setup

1. Clone and Build

# Clone the repository
git clone https://github.com/opencode-ai/opencode.git
cd opencode

# Build the application
go build -o opencode

# Run the application
./opencode

2. Development Commands

Based on the project's OpenCode.md file:

# Build
go build -o opencode ./main.go

# Lint/Check
go vet ./...

# Test all packages
go test ./...

# Test single package
go test -v ./internal/tui/theme/...

# Test specific test
go test -v ./internal/tui/theme/... -run TestThemeRegistration

# Generate database code
sqlc generate

# Snapshot build (for releases)
goreleaser build --clean --snapshot --skip validate

3. Environment Variables

Set up API keys for LLM providers you want to test:

export ANTHROPIC_API_KEY="your-key"
export OPENAI_API_KEY="your-key"
export GEMINI_API_KEY="your-key"
export GITHUB_TOKEN="your-token"  # For Copilot
export GROQ_API_KEY="your-key"

Code Style and Conventions

Import Organization

Follow Go standards with clear separation:

import (
    // Standard library first
    "context"
    "fmt"
    "os"

    // External packages second
    "github.com/spf13/cobra"
    "github.com/charmbracelet/bubbletea"

    // Internal packages third
    "github.com/opencode-ai/opencode/internal/app"
    "github.com/opencode-ai/opencode/internal/config"
)

Naming Conventions

  • Exported functions/types: PascalCase (ConfigManager, LoadConfig)
  • Unexported functions/types: camelCase (configManager, loadConfig)
  • Constants: UPPER_SNAKE_CASE (MAX_TOKENS, DEFAULT_MODEL)
  • Interfaces: Use descriptive names, often ending in -er (Provider, Service)

Error Handling

Use structured error handling with context:

// Good
func processFile(path string) error {
    if _, err := os.Stat(path); err != nil {
        return fmt.Errorf("failed to access file %s: %w", path, err)
    }
    // ... processing logic
    return nil
}

// Context with errors
func runAgent(ctx context.Context, prompt string) error {
    select {
    case <-ctx.Done():
        return fmt.Errorf("agent execution cancelled: %w", ctx.Err())
    default:
        // ... agent logic
    }
    return nil
}

Database Patterns

Use SQLC for type-safe database operations:

// Generated by SQLC
func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error) {
    // ... generated code
}

// Usage in service
func (s *SessionService) CreateSession(ctx context.Context, title string) (*Session, error) {
    session, err := s.queries.CreateSession(ctx, CreateSessionParams{
        Title: title,
        CreatedAt: time.Now(),
    })
    if err != nil {
        return nil, fmt.Errorf("failed to create session: %w", err)
    }
    return &session, nil
}

Context Usage

Always pass context.Context as the first parameter:

func (p *Provider) GenerateCompletion(ctx context.Context, params GenerationParams) (<-chan ProviderEvent, error) {
    // Use context for cancellation and timeouts
    select {
    case <-ctx.Done():
        return nil, ctx.Err()
    default:
        // ... implementation
    }
}

Architecture Patterns

Service Pattern

Each major component follows the service pattern:

type Service interface {
    Method(ctx context.Context, params SomeParams) (Result, error)
    Subscribe(ctx context.Context) <-chan pubsub.Event[EventType]
}

type serviceImpl struct {
    db      *sql.DB
    config  *config.Config
    logger  *logging.Logger
}

func NewService(db *sql.DB, config *config.Config) Service {
    return &serviceImpl{
        db:     db,
        config: config,
        logger: logging.NewLogger("service-name"),
    }
}

Provider Pattern

For LLM providers, implement the common interface:

type Provider interface {
    GenerateCompletion(ctx context.Context, params GenerationParams) (<-chan ProviderEvent, error)
    GetModels() []models.Model
    SupportsModel(modelID models.ModelID) bool
}

type anthropicProvider struct {
    client *anthropic.Client
    config ProviderConfig
}

func (p *anthropicProvider) GenerateCompletion(ctx context.Context, params GenerationParams) (<-chan ProviderEvent, error) {
    eventChan := make(chan ProviderEvent, 100)
    
    go func() {
        defer close(eventChan)
        // ... streaming implementation
    }()
    
    return eventChan, nil
}

Tool Pattern

Tools follow a consistent interface:

type BaseTool interface {
    Info() ToolInfo
    Run(ctx context.Context, params ToolCall) (ToolResponse, error)
}

type FileTool struct {
    permissions permission.Service
}

func (t *FileTool) Info() ToolInfo {
    return ToolInfo{
        Name:        "write",
        Description: "Write content to a file",
        Parameters: map[string]interface{}{
            "file_path": map[string]interface{}{
                "type":        "string",
                "description": "Path to the file to write",
            },
            "content": map[string]interface{}{
                "type":        "string", 
                "description": "Content to write to the file",
            },
        },
        Required: []string{"file_path", "content"},
    }
}

func (t *FileTool) Run(ctx context.Context, params ToolCall) (ToolResponse, error) {
    // Permission check
    if !t.permissions.HasPermission(ctx, "file.write") {
        return ToolResponse{}, ErrPermissionDenied
    }
    
    // Implementation
    // ...
    
    return ToolResponse{
        Success: true,
        Content: "File written successfully",
    }, nil
}

Testing Strategy

Unit Tests

Place tests in _test.go files alongside source files:

func TestSessionCreation(t *testing.T) {
    // Setup
    db := setupTestDB(t)
    service := NewSessionService(db, testConfig())
    
    // Test
    session, err := service.CreateSession(context.Background(), "Test Session")
    
    // Assertions
    assert.NoError(t, err)
    assert.NotNil(t, session)
    assert.Equal(t, "Test Session", session.Title)
}

func TestProviderSelection(t *testing.T) {
    tests := []struct {
        name     string
        config   ProviderConfig
        expected models.ModelProvider
        wantErr  bool
    }{
        {
            name: "anthropic preferred",
            config: ProviderConfig{
                Providers: map[models.ModelProvider]Provider{
                    models.ProviderAnthropic: {Disabled: false},
                    models.ProviderOpenAI:    {Disabled: false},
                },
            },
            expected: models.ProviderAnthropic,
            wantErr:  false,
        },
        // ... more test cases
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result, err := SelectProvider(tt.config)
            if tt.wantErr {
                assert.Error(t, err)
                return
            }
            assert.NoError(t, err)
            assert.Equal(t, tt.expected, result)
        })
    }
}

Integration Tests

For components that interact with external services:

func TestLSPIntegration(t *testing.T) {
    if testing.Short() {
        t.Skip("Skipping integration test in short mode")
    }
    
    // Setup LSP client
    client, err := lsp.NewClient("gopls")
    require.NoError(t, err)
    defer client.Shutdown()
    
    // Test diagnostics
    diagnostics, err := client.GetDiagnostics(context.Background(), "test.go")
    assert.NoError(t, err)
    assert.IsType(t, []lsp.Diagnostic{}, diagnostics)
}

TUI Testing

For TUI components, test the underlying logic:

func TestChatMessage(t *testing.T) {
    msg := NewChatMessage("user", "Hello, world!")
    
    // Test rendering
    rendered := msg.View()
    assert.Contains(t, rendered, "Hello, world!")
    
    // Test updates
    msg.SetContent("Updated content")
    updated := msg.View()
    assert.Contains(t, updated, "Updated content")
}

Database Development

Migrations

Use Goose for database migrations:

# Create new migration
goose -dir internal/db/migrations create add_new_table sql

# Apply migrations
goose -dir internal/db/migrations sqlite3 ./opencode.db up

# Rollback migration
goose -dir internal/db/migrations sqlite3 ./opencode.db down

SQLC Integration

Define queries in internal/db/sql/ directory:

-- name: CreateSession :one
INSERT INTO sessions (id, title, created_at, updated_at, model_provider, model_id)
VALUES (?, ?, ?, ?, ?, ?)
RETURNING *;

-- name: GetSessionByID :one
SELECT * FROM sessions WHERE id = ? LIMIT 1;

-- name: ListSessions :many
SELECT * FROM sessions ORDER BY updated_at DESC LIMIT ? OFFSET ?;

Generate Go code:

sqlc generate

LLM Provider Development

Adding a New Provider

  1. Create provider file: internal/llm/provider/newprovider.go
type NewProvider struct {
    client APIClient
    config ProviderConfig
}

func NewNewProvider(config ProviderConfig) Provider {
    return &NewProvider{
        client: NewAPIClient(config.APIKey),
        config: config,
    }
}

func (p *NewProvider) GenerateCompletion(ctx context.Context, params GenerationParams) (<-chan ProviderEvent, error) {
    eventChan := make(chan ProviderEvent, 100)
    
    go func() {
        defer close(eventChan)
        
        // Streaming implementation
        stream, err := p.client.CreateChatCompletionStream(ctx, params)
        if err != nil {
            eventChan <- ProviderEvent{Type: EventError, Error: err}
            return
        }
        
        for {
            response, err := stream.Recv()
            if err == io.EOF {
                eventChan <- ProviderEvent{Type: EventDone}
                break
            }
            if err != nil {
                eventChan <- ProviderEvent{Type: EventError, Error: err}
                break
            }
            
            eventChan <- ProviderEvent{
                Type:    EventContentDelta,
                Content: response.Choices[0].Delta.Content,
            }
        }
    }()
    
    return eventChan, nil
}
  1. Add models: internal/llm/models/newprovider.go
func init() {
    // Register models with the global registry
    registerModel(ModelID("newprovider.model-name"), Model{
        ID:          "newprovider.model-name",
        Name:        "New Provider Model",
        Provider:    ProviderNewProvider,
        InputCost:   0.001,  // per 1k tokens
        OutputCost:  0.002,  // per 1k tokens
        ContextWindow: 32000,
        Capabilities: ModelCapabilities{
            SupportsTools:       true,
            SupportsVision:      false,
            SupportsReasoning:   false,
        },
    })
}
  1. Register provider: Add to provider registry in the appropriate initialization code.

Tool Development

  1. Create tool file: internal/llm/tools/newtool.go
type NewTool struct {
    permissions permission.Service
}

func NewNewTool(permissions permission.Service) BaseTool {
    return &NewTool{permissions: permissions}
}

func (t *NewTool) Info() ToolInfo {
    return ToolInfo{
        Name:        "new_tool",
        Description: "Description of what the tool does",
        Parameters: map[string]interface{}{
            "param1": map[string]interface{}{
                "type":        "string",
                "description": "Description of parameter",
            },
        },
        Required: []string{"param1"},
    }
}

func (t *NewTool) Run(ctx context.Context, params ToolCall) (ToolResponse, error) {
    // Permission check
    permitted, err := t.permissions.RequestPermission(ctx, permission.Request{
        Tool:        "new_tool",
        Description: "Execute new tool operation",
        Sensitive:   false,
    })
    if err != nil {
        return ToolResponse{}, err
    }
    if !permitted {
        return ToolResponse{}, ErrPermissionDenied
    }
    
    // Extract parameters
    param1, ok := params.Parameters["param1"].(string)
    if !ok {
        return ToolResponse{}, fmt.Errorf("param1 must be a string")
    }
    
    // Tool implementation
    result, err := performToolOperation(param1)
    if err != nil {
        return ToolResponse{}, fmt.Errorf("tool operation failed: %w", err)
    }
    
    return ToolResponse{
        Success: true,
        Content: result,
    }, nil
}
  1. Register tool: Add to tool registry in internal/llm/agent/tools.go

TUI Component Development

Creating New Components

  1. Component structure:
type NewComponent struct {
    width  int
    height int
    theme  theme.Theme
    model  ComponentModel
}

func NewNewComponent(theme theme.Theme) *NewComponent {
    return &NewComponent{
        theme: theme,
        model: initComponentModel(),
    }
}

func (c *NewComponent) Init() tea.Cmd {
    return nil
}

func (c *NewComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.WindowSizeMsg:
        c.width = msg.Width
        c.height = msg.Height
    case tea.KeyMsg:
        return c.handleKeyPress(msg)
    }
    return c, nil
}

func (c *NewComponent) View() string {
    return c.renderComponent()
}
  1. Theme integration:
func (c *NewComponent) renderComponent() string {
    style := lipgloss.NewStyle().
        Foreground(c.theme.Colors().Text).
        Background(c.theme.Colors().Background).
        Border(lipgloss.RoundedBorder()).
        BorderForeground(c.theme.Colors().Border)
    
    return style.Render(c.model.content)
}

Adding New Themes

  1. Create theme file: internal/tui/theme/newtheme.go
type NewTheme struct {
    colors ThemeColors
}

func NewNewTheme() Theme {
    return &NewTheme{
        colors: ThemeColors{
            Background:    lipgloss.Color("#000000"),
            Text:          lipgloss.Color("#ffffff"),
            Primary:       lipgloss.Color("#ff0000"),
            Secondary:     lipgloss.Color("#00ff00"),
            Border:        lipgloss.Color("#333333"),
            // ... more colors
        },
    }
}

func (t *NewTheme) Name() string {
    return "newtheme"
}

func (t *NewTheme) Colors() ThemeColors {
    return t.colors
}
  1. Register theme: Add to theme manager registration.

Debugging and Troubleshooting

Debug Mode

Run with debug logging:

./opencode -d

LSP Debugging

Enable LSP-specific debugging:

{
  "debugLSP": true,
  "lsp": {
    "go": {
      "command": "gopls",
      "args": ["-logfile", "/tmp/gopls.log", "-v"]
    }
  }
}

Database Issues

Check database state:

sqlite3 .opencode/opencode.db ".tables"
sqlite3 .opencode/opencode.db "SELECT * FROM sessions LIMIT 5;"

Provider Issues

Test provider configuration:

./opencode -p "test prompt" -f json

Contributing Guidelines

Pull Request Process

  1. Fork and branch: Create a feature branch from main
  2. Implement changes: Follow code style and add tests
  3. Test thoroughly: Run all tests and manual testing
  4. Update documentation: Update relevant documentation
  5. Submit PR: Create pull request with clear description

Commit Messages

Follow conventional commit format:

feat: add support for new LLM provider
fix: resolve memory leak in message streaming  
docs: update architecture documentation
test: add integration tests for LSP client
refactor: simplify provider configuration

Code Review Checklist

  • Code follows established patterns and conventions
  • Tests added for new functionality
  • Documentation updated
  • Error handling is comprehensive
  • Performance implications considered
  • Security implications reviewed
  • Backward compatibility maintained

Release Process

Version Management

Versions follow semantic versioning (semver):

  • Major: Breaking changes
  • Minor: New features, backward compatible
  • Patch: Bug fixes, backward compatible

Release Steps

  1. Update version: Update internal/version/version.go
  2. Update changelog: Document changes
  3. Tag release: git tag v1.2.3
  4. Push tag: git push origin v1.2.3
  5. GoReleaser: Automated release via GitHub Actions

Distribution

OpenCode is distributed through:

  • GitHub Releases: Binaries for multiple platforms
  • Homebrew: macOS and Linux package manager
  • AUR: Arch Linux User Repository
  • Go Install: Direct Go installation

Helpful Resources

External Dependencies

AI Provider APIs

LSP Resources

This development guide should help both new and experienced contributors understand the OpenCode codebase and contribute effectively to the project.