Thank you for your interest in contributing to OpenVision! This document provides guidelines and information for contributors.
Be respectful, inclusive, and constructive. We welcome contributors of all skill levels.
- Check existing issues first
- Create a new issue with:
- Clear, descriptive title
- Steps to reproduce
- Expected vs actual behavior
- iOS version, device model
- Relevant logs (filter by
[OpenClaw],[Gemini], etc.)
- Check existing discussions first
- Open a new discussion describing:
- The problem you're trying to solve
- Your proposed solution
- Alternative approaches considered
- Fork the repository
- Create a feature branch from
main:git checkout -b feature/your-feature-name
- Make your changes following our code style
- Write/update tests as needed
- Commit with clear messages:
git commit -m "Add: Description of feature" git commit -m "Fix: Description of bug fix" git commit -m "Refactor: Description of refactoring"
- Push and open a Pull Request
Follow Swift API Design Guidelines.
// GOOD: Clear, descriptive names
func startVoiceSession() async throws
func capturePhoto() -> Data?
// BAD: Abbreviated, unclear names
func startVS() async throws
func capPh() -> Data?See docs/architecture.md for the full map — layout, MVVM, the
AIBackend seam, and step-by-step guides for adding a backend or a native tool.
- MVVM: Views render and forward interactions; ViewModels (
@MainActor ObservableObject) orchestrate - @MainActor: Use for all UI-related code and managers
- Singleton pattern: For shared managers (SettingsManager, GlassesManager)
- Protocol-oriented: New backends conform to
AIBackend; new tools toNativeTool - Async/await: Prefer over completion handlers
- Tests: Pure logic (date math, routing, parsing) belongs in testable enums/functions with unit tests in
OpenVisionTests
// GOOD: @MainActor isolation
@MainActor
final class MyService: ObservableObject {
@Published var state: ServiceState = .idle
func performAction() async throws {
// Implementation
}
}
// GOOD: Protocol for testability
protocol CameraServiceProtocol {
func capturePhoto() async throws -> Data
}// MARK: - Properties
@Published var isActive = false
private var webSocket: URLSessionWebSocketTask?
// MARK: - Initialization
init() {
// Setup
}
// MARK: - Public Methods
func start() async throws {
// Implementation
}
// MARK: - Private Methods
private func setupConnection() {
// Implementation
}Add documentation to public APIs:
/// Starts a voice conversation session.
///
/// This method connects to the configured AI backend and begins
/// listening for voice input.
///
/// - Throws: `AIBackendError.notConfigured` if no backend is configured
/// - Throws: `AIBackendError.connectionFailed` if connection fails
func startSession() async throws {
// Implementation
}Use typed errors with clear cases:
enum AIBackendError: LocalizedError {
case notConfigured
case connectionFailed(Error)
case authenticationFailed
case timeout
var errorDescription: String? {
switch self {
case .notConfigured:
return "AI backend is not configured"
case .connectionFailed(let error):
return "Connection failed: \(error.localizedDescription)"
case .authenticationFailed:
return "Authentication failed"
case .timeout:
return "Connection timed out"
}
}
}# In Xcode
Cmd+U
# Or via command line
xcodebuild test -scheme OpenVision -destination 'platform=iOS Simulator,name=iPhone 15'- Place tests in
Tests/directory - Mirror the source file structure
- Use mocks for external dependencies
final class OpenClawServiceTests: XCTestCase {
var sut: OpenClawService!
var mockWebSocket: MockWebSocket!
override func setUp() {
super.setUp()
mockWebSocket = MockWebSocket()
sut = OpenClawService(webSocket: mockWebSocket)
}
func testConnectSendsAuthMessage() async throws {
// Given
let expectation = expectation(description: "Auth message sent")
// When
try await sut.connect()
// Then
XCTAssertTrue(mockWebSocket.sentMessages.contains { $0.contains("auth") })
}
}Aim for coverage of:
- Public API methods
- Error handling paths
- Edge cases (empty data, network failures)
OpenVision/
├── App/ # App entry point
├── Config/ # Configuration files
├── Models/ # Data models
├── Services/ # Business logic
│ ├── AIBackend/ # AI protocol & factory
│ ├── OpenClaw/ # OpenClaw WebSocket client
│ ├── GeminiLive/ # Gemini Live client
│ ├── Voice/ # Speech recognition
│ ├── Audio/ # Audio capture/playback
│ ├── Camera/ # Camera services
│ └── TTS/ # Text-to-speech
├── Managers/ # Singleton managers
├── Views/ # SwiftUI views
│ ├── VoiceAgent/ # Voice conversation UI
│ ├── History/ # Conversation history
│ ├── Settings/ # Settings panels
│ └── Components/ # Reusable UI components
└── Tests/ # Unit tests
Use conventional commit format:
Type: Short description (max 72 chars)
Optional longer description explaining the why and how.
Closes #123
Types:
Add: New featureFix: Bug fixRefactor: Code restructuringDocs: Documentation onlyTest: Adding/updating testsChore: Maintenance tasks
- Title: Clear, descriptive title
- Description: Explain what and why
- Testing: Describe how you tested
- Screenshots: Include for UI changes
- Breaking changes: Call out any breaking changes
- Code follows style guidelines
- Self-review completed
- Comments added for complex logic
- Documentation updated if needed
- Tests added/updated
- No compiler warnings introduced
- CI is green
Every pull request is compiled by GitHub Actions (.github/workflows/ci.yml): the app and its
test target are built for a generic iOS device with code signing off, on a free macOS runner.
main requires that check to pass before a PR can merge, and that applies to maintainers too.
The workflow copies Config.xcconfig.example and OpenVision/Config/Config.swift.example into
place, so no secrets are needed to compile. If CI fails on your PR, open the job's log; the
.xcactivitylog files are attached as an artifact when the build step fails.
Releases are cut by a maintainer. Version numbers follow Semantic Versioning: a new backend or feature bumps the minor version, fixes alone bump the patch version.
-
Open a release branch from
main(for examplerelease/2.15.0). -
Update
CHANGELOG.md: move everything merged since the last release under a new## [x.y.z] - YYYY-MM-DDheading, grouped as Added / Changed / Fixed, with the PR number on each line. -
Bump
MARKETING_VERSIONandCURRENT_PROJECT_VERSIONinproject.yml, runxcodegen generate, and commit asBump version to x.y.z (build N). -
Open a pull request.
mainis protected, so the release commits go through CI like any other change; direct pushes tomainare rejected. -
After the merge, tag the merge commit and publish the release with narrative notes and a compare link:
git tag -a vx.y.z -m "OpenVision x.y.z" git push origin vx.y.z gh release create vx.y.z --title "OpenVision x.y.z" --notes-file notes.md
The app is distributed as source; there is no App Store or TestFlight step in this repository.
- Open a Discussion
- Check existing issues and PRs
- Read
CLAUDE.mdfor technical context
Thank you for contributing to OpenVision! Your efforts help make AI assistants accessible to everyone.