vibetunnel/docs/development.md
2025-07-18 08:37:16 +02:00

14 KiB

VibeTunnel Development Guide

Overview

VibeTunnel follows modern Swift 6 and TypeScript development practices with a focus on async/await patterns, protocol-oriented design, and reactive UI architectures. The codebase is organized into three main components: macOS app (Swift/SwiftUI), iOS app (Swift/SwiftUI), and web dashboard (TypeScript/Lit).

Key architectural principles:

  • Protocol-oriented design for flexibility and testability
  • Async/await throughout for clean asynchronous code
  • Observable pattern for reactive state management
  • Dependency injection via environment values in SwiftUI

Code Style

Swift Conventions

Modern Swift 6 patterns - From mac/VibeTunnel/Core/Services/ServerManager.swift:

@MainActor
@Observable
class ServerManager {
    @MainActor static let shared = ServerManager()
    
    private(set) var serverType: ServerType = .bun
    private(set) var isSwitchingServer = false
    
    var port: String {
        get { UserDefaults.standard.string(forKey: "serverPort") ?? "4020" }
        set { UserDefaults.standard.set(newValue, forKey: "serverPort") }
    }
}

Error handling - From mac/VibeTunnel/Core/Protocols/VibeTunnelServer.swift:

enum ServerError: LocalizedError {
    case binaryNotFound(String)
    case startupFailed(String)
    case portInUse(Int)
    case invalidConfiguration(String)
    
    var errorDescription: String? {
        switch self {
        case .binaryNotFound(let binary):
            return "Server binary not found: \(binary)"
        case .startupFailed(let reason):
            return "Server failed to start: \(reason)"
        }
    }
}

SwiftUI view patterns - From mac/VibeTunnel/Presentation/Views/Settings/GeneralSettingsView.swift:

struct GeneralSettingsView: View {
    @AppStorage("autostart")
    private var autostart = false
    
    @State private var isCheckingForUpdates = false
    
    private let startupManager = StartupManager()
    
    var body: some View {
        NavigationStack {
            Form {
                Section {
                    VStack(alignment: .leading, spacing: 4) {
                        Toggle("Launch at Login", isOn: launchAtLoginBinding)
                        Text("Automatically start VibeTunnel when you log in.")
                            .font(.caption)
                            .foregroundStyle(.secondary)
                    }
                }
            }
        }
    }
}

TypeScript Conventions

Class-based services - From web/src/server/services/buffer-aggregator.ts:

interface BufferAggregatorConfig {
  terminalManager: TerminalManager;
  remoteRegistry: RemoteRegistry | null;
  isHQMode: boolean;
}

export class BufferAggregator {
  private config: BufferAggregatorConfig;
  private remoteConnections: Map<string, RemoteWebSocketConnection> = new Map();
  
  constructor(config: BufferAggregatorConfig) {
    this.config = config;
  }
  
  async handleClientConnection(ws: WebSocket): Promise<void> {
    console.log(chalk.blue('[BufferAggregator] New client connected'));
    // ...
  }
}

Lit components - From web/src/client/components/vibe-terminal-buffer.ts:

@customElement('vibe-terminal-buffer')
export class VibeTerminalBuffer extends LitElement {
  // Disable shadow DOM for Tailwind compatibility
  createRenderRoot() {
    return this as unknown as HTMLElement;
  }
  
  @property({ type: String }) sessionId = '';
  @state() private buffer: BufferSnapshot | null = null;
  @state() private error: string | null = null;
}

Common Patterns

Service Architecture

Protocol-based services - Services define protocols for testability:

// mac/VibeTunnel/Core/Protocols/VibeTunnelServer.swift
@MainActor
protocol VibeTunnelServer: AnyObject {
    var isRunning: Bool { get }
    var port: String { get set }
    var logStream: AsyncStream<ServerLogEntry> { get }
    
    func start() async throws
    func stop() async
    func checkHealth() async -> Bool
}

Singleton managers - Core services use thread-safe singletons:

// mac/VibeTunnel/Core/Services/ServerManager.swift:14
@MainActor static let shared = ServerManager()

// ios/VibeTunnel/Services/APIClient.swift:93
static let shared = APIClient()

Async/Await Patterns

Swift async operations - From ios/VibeTunnel/Services/APIClient.swift:

func getSessions() async throws -> [Session] {
    guard let url = makeURL(path: "/api/sessions") else {
        throw APIError.invalidURL
    }
    
    let (data, response) = try await session.data(from: url)
    
    guard let httpResponse = response as? HTTPURLResponse else {
        throw APIError.invalidResponse
    }
    
    if httpResponse.statusCode != 200 {
        throw APIError.serverError(httpResponse.statusCode, nil)
    }
    
    return try decoder.decode([Session].self, from: data)
}

TypeScript async patterns - From web/src/server/services/buffer-aggregator.ts:

async handleClientMessage(
  clientWs: WebSocket,
  data: { type: string; sessionId?: string }
): Promise<void> {
  const subscriptions = this.clientSubscriptions.get(clientWs);
  if (!subscriptions) return;
  
  if (data.type === 'subscribe' && data.sessionId) {
    // Handle subscription
  }
}

Error Handling

Swift error enums - Comprehensive error types with localized descriptions:

// ios/VibeTunnel/Services/APIClient.swift:4-70
enum APIError: LocalizedError {
    case invalidURL
    case serverError(Int, String?)
    case networkError(Error)
    
    var errorDescription: String? {
        switch self {
        case .serverError(let code, let message):
            if let message { return message }
            switch code {
            case 400: return "Bad request"
            case 401: return "Unauthorized"
            default: return "Server error: \(code)"
            }
        }
    }
}

TypeScript error handling - Structured error responses:

// web/src/server/middleware/auth.ts
try {
  // Operation
} catch (error) {
  console.error('[Auth] Error:', error);
  res.status(500).json({ 
    error: 'Internal server error',
    message: error instanceof Error ? error.message : 'Unknown error'
  });
}

State Management

SwiftUI Observable - From mac/VibeTunnel/Core/Services/ServerManager.swift:

@Observable
class ServerManager {
    private(set) var isRunning = false
    private(set) var isRestarting = false
    private(set) var lastError: Error?
}

AppStorage for persistence:

// mac/VibeTunnel/Presentation/Views/Settings/GeneralSettingsView.swift:5
@AppStorage("autostart") private var autostart = false
@AppStorage("updateChannel") private var updateChannelRaw = UpdateChannel.stable.rawValue

UI Patterns

SwiftUI form layouts - From mac/VibeTunnel/Presentation/Views/Settings/GeneralSettingsView.swift:

Form {
    Section {
        VStack(alignment: .leading, spacing: 4) {
            Toggle("Launch at Login", isOn: launchAtLoginBinding)
            Text("Description")
                .font(.caption)
                .foregroundStyle(.secondary)
        }
    } header: {
        Text("Application")
            .font(.headline)
    }
}
.formStyle(.grouped)

Lit reactive properties:

// web/src/client/components/vibe-terminal-buffer.ts:22-24
@property({ type: String }) sessionId = '';
@state() private buffer: BufferSnapshot | null = null;
@state() private error: string | null = null;

Workflows

Adding a New Service

  1. Define the protocol in mac/VibeTunnel/Core/Protocols/:
@MainActor
protocol MyServiceProtocol {
    func performAction() async throws
}
  1. Implement the service in mac/VibeTunnel/Core/Services/:
@MainActor
class MyService: MyServiceProtocol {
    static let shared = MyService()
    
    func performAction() async throws {
        // Implementation
    }
}
  1. Add to environment if needed in mac/VibeTunnel/Core/Extensions/EnvironmentValues+Services.swift

Creating UI Components

SwiftUI views follow this pattern:

struct MyView: View {
    @Environment(\.myService) private var service
    @State private var isLoading = false
    
    var body: some View {
        // View implementation
    }
}

Lit components use decorators:

@customElement('my-component')
export class MyComponent extends LitElement {
    @property({ type: String }) value = '';
    
    render() {
        return html`<div>${this.value}</div>`;
    }
}

Testing Patterns

Swift unit tests - From mac/VibeTunnelTests/ServerManagerTests.swift:

@MainActor
final class ServerManagerTests: XCTestCase {
    override func setUp() async throws {
        await super.setUp()
        // Setup
    }
    
    func testServerStart() async throws {
        let manager = ServerManager.shared
        await manager.start()
        XCTAssertTrue(manager.isRunning)
    }
}

TypeScript tests use Vitest:

// web/src/test/setup.ts
import { describe, it, expect } from 'vitest';

describe('BufferAggregator', () => {
  it('should handle client connections', async () => {
    // Test implementation
  });
});

Reference

File Organization

Swift packages:

  • mac/VibeTunnel/Core/ - Core business logic, protocols, services
  • mac/VibeTunnel/Presentation/ - SwiftUI views and view models
  • mac/VibeTunnel/Utilities/ - Helper classes and extensions
  • ios/VibeTunnel/Services/ - iOS-specific services
  • ios/VibeTunnel/Views/ - iOS UI components

TypeScript modules:

  • web/src/client/ - Frontend components and utilities
  • web/src/server/ - Backend services and routes
  • web/src/server/pty/ - Terminal handling
  • web/src/test/ - Test files and utilities

Naming Conventions

Swift:

  • Services: *Manager, *Service (e.g., ServerManager, APIClient)
  • Protocols: *Protocol, *able (e.g., VibeTunnelServer, HTTPClientProtocol)
  • Views: *View (e.g., GeneralSettingsView, TerminalView)
  • Errors: *Error enum (e.g., ServerError, APIError)

TypeScript:

  • Services: *Service, *Manager (e.g., BufferAggregator, TerminalManager)
  • Components: vibe-* custom elements (e.g., vibe-terminal-buffer)
  • Types: PascalCase interfaces (e.g., BufferSnapshot, ServerConfig)

Common Issues

Port conflicts - Handled in mac/VibeTunnel/Core/Utilities/PortConflictResolver.swift Permission management - See mac/VibeTunnel/Core/Services/*PermissionManager.swift WebSocket reconnection - Implemented in ios/VibeTunnel/Services/BufferWebSocketClient.swift Terminal resizing - Handled in both Swift and TypeScript terminal components

VibeTunnel CLI Wrapper (vt)

The vt command is a bash wrapper script that allows users to run commands through VibeTunnel's terminal forwarding. It's installed at /usr/local/bin/vt when the Mac app is built.

Source location: mac/VibeTunnel/vt

Usage:

# Run a command through VibeTunnel
vt ls -la

# Run an aliased command (e.g., if 'claude' is an alias)
vt claude --version

# Launch interactive shell
vt --shell
vt -i

# Run command without shell wrapping (bypass alias resolution)
vt --no-shell-wrap command
vt -S command

How it works:

  1. Locates the VibeTunnel.app bundle (checks standard locations and uses Spotlight if needed)
  2. Finds the vibetunnel binary within the app bundle's Resources
  3. Determines if the command is a binary or alias/function
  4. For binaries: executes directly through vibetunnel fwd
  5. For aliases/functions: wraps in appropriate shell (zsh -i -c or bash -c) for proper resolution

Technical Details:

  • The -- separator should not be passed to fwd as it was being misinterpreted as a command
  • Aliases require interactive shell mode to be resolved properly
  • The script prevents recursive VibeTunnel sessions by checking VIBETUNNEL_SESSION_ID
  • The fwd binary now properly handles -- as an argument separator when needed

Web Development

Code Quality Tools

VibeTunnel uses several tools to maintain code quality:

Running All Checks

To run all code quality checks (read-only checks run in parallel):

pnpm run check

This runs format checking, linting, and type checking in parallel and reports any issues.

Individual Tools

Formatting (Biome):

pnpm run format        # Fix formatting issues
pnpm run format:check  # Check formatting without fixing

Linting (Biome + TypeScript):

pnpm run lint      # Check for lint errors
pnpm run lint:fix  # Fix auto-fixable lint errors

Type Checking (TypeScript):

pnpm run typecheck  # Run type checking on all configs

Auto-fix All Issues

To automatically fix all formatting and linting issues:

pnpm run check:fix

This runs format and lint:fix sequentially to avoid file conflicts.

Why Sequential Fixes?

Running multiple file-modifying tools in parallel can cause race conditions where:

  • Both tools try to write to the same file simultaneously
  • One tool's changes get overwritten by another
  • Git operations fail due to file locks

Best practices from the JavaScript community recommend:

  1. Parallel for checks: Read-only operations can run simultaneously
  2. Sequential for fixes: File modifications should happen one after another
  3. Biome as unified tool: Reduces conflicts by combining formatting and linting

Why Multiple Tools?

  1. Biome: Fast, modern formatter and linter for JavaScript/TypeScript
  2. TypeScript: Type checking across server, client, and service worker contexts
  3. Parallel execution: Saves time by running independent checks simultaneously

Tips for Faster Development

  1. Use pnpm run check before committing - Catches all issues at once
  2. Enable format-on-save in your editor - Prevents formatting issues
  3. Run pnpm run check:fix to quickly fix issues - Handles problems sequentially

Continuous Development

When developing, you typically want:

# Terminal 1: Run the dev server
pnpm run dev

# Terminal 2: Run tests in watch mode (when needed)
pnpm test

# Before committing: Run all checks
pnpm run check