Extension Architecture

Ultra is built on the Editor Command Protocol (ECP), a JSON-RPC 2.0 based architecture that separates the UI from backend services. This design makes Ultra highly extensible — you can add new capabilities without touching existing code.

Ways to Extend Ultra

🔧

Custom Services

Add new backend functionality like database connections, API integrations, or custom tooling.

Commands

Register new commands that appear in the command palette and can be bound to shortcuts.

🎨

UI Components

Create custom panels, status bar items, or decorations in the editor.

📦

Language Support

Add syntax highlighting, snippets, or LSP configurations for new languages.

Project Structure

Ultra extensions live in the ~/.ultra/extensions/ directory. Each extension is a folder with its own package:

~/.ultra/extensions/
└── my-extension/
    ├── package.json      # Extension manifest
    ├── src/
    │   ├── index.ts      # Entry point
    │   ├── service.ts    # Custom service
    │   └── commands.ts   # Command definitions
    └── dist/             # Compiled output

Creating a Custom Service

Services are the backbone of Ultra's functionality. Each service encapsulates a domain (file system, git, database) and exposes methods through ECP.

Service Structure

Every service follows a consistent pattern:

src/services/myService/
├── interface.ts      # Type definitions and interface
├── myService.ts      # Implementation
├── adapter.ts        # ECP request handler
└── index.ts          # Public exports

Step 1: Define the Interface

interface.ts
// Define the service contract
export interface IWeatherService {
  getCurrentWeather(city: string): Promise;
  getForecast(city: string, days: number): Promise;

  // Event subscription
  onWeatherUpdate(callback: (data: WeatherData) => void): () => void;
}

export interface WeatherData {
  city: string;
  temperature: number;
  conditions: string;
  humidity: number;
}

export interface Forecast {
  date: string;
  high: number;
  low: number;
  conditions: string;
}

Step 2: Implement the Service

weatherService.ts
import { IWeatherService, WeatherData, Forecast } from './interface';

class WeatherService implements IWeatherService {
  private listeners: Set<(data: WeatherData) => void> = new Set();

  async getCurrentWeather(city: string): Promise {
    // Fetch from weather API
    const response = await fetch(
      `https://api.weather.example/current?city=${city}`
    );
    const data = await response.json();

    const weather: WeatherData = {
      city,
      temperature: data.temp,
      conditions: data.conditions,
      humidity: data.humidity
    };

    // Notify listeners
    this.notifyListeners(weather);

    return weather;
  }

  async getForecast(city: string, days: number): Promise {
    const response = await fetch(
      `https://api.weather.example/forecast?city=${city}&days=${days}`
    );
    return response.json();
  }

  onWeatherUpdate(callback: (data: WeatherData) => void): () => void {
    this.listeners.add(callback);
    return () => this.listeners.delete(callback);
  }

  private notifyListeners(data: WeatherData): void {
    this.listeners.forEach(cb => cb(data));
  }
}

// Export singleton instance
export const weatherService = new WeatherService();
export { WeatherService };

Step 3: Create the ECP Adapter

adapter.ts
import { weatherService } from './weatherService';
import { ECPAdapter, ECPRequest, ECPResponse } from '@ultra/ecp';

export class WeatherServiceAdapter implements ECPAdapter {
  readonly namespace = 'weather';

  async handleRequest(request: ECPRequest): Promise {
    const { method, params } = request;

    switch (method) {
      case 'weather/getCurrent':
        return {
          result: await weatherService.getCurrentWeather(params.city)
        };

      case 'weather/getForecast':
        return {
          result: await weatherService.getForecast(
            params.city,
            params.days ?? 5
          )
        };

      default:
        return {
          error: {
            code: -32601,
            message: `Method not found: ${method}`
          }
        };
    }
  }
}

export const weatherAdapter = new WeatherServiceAdapter();

Step 4: Register the Service

index.ts
import { registerAdapter } from '@ultra/ecp';
import { weatherAdapter } from './adapter';
import { weatherService } from './weatherService';

// Register with ECP server
registerAdapter(weatherAdapter);

// Export for direct usage
export { weatherService };
export * from './interface';

Registering Commands

Commands appear in the command palette and can be bound to keyboard shortcuts. They're the primary way users interact with your extension.

Command Definition

commands.ts
import { registerCommand, Command } from '@ultra/commands';
import { weatherService } from './weatherService';
import { showQuickPick, showInputBox, showMessage } from '@ultra/ui';

// Simple command
registerCommand({
  id: 'weather.showCurrent',
  title: 'Weather: Show Current Weather',
  category: 'Weather',

  async execute() {
    const city = await showInputBox({
      prompt: 'Enter city name',
      placeholder: 'San Francisco'
    });

    if (!city) return;

    const weather = await weatherService.getCurrentWeather(city);
    showMessage(
      `${weather.city}: ${weather.temperature}°F, ${weather.conditions}`
    );
  }
});

// Command with keyboard shortcut
registerCommand({
  id: 'weather.showForecast',
  title: 'Weather: Show 5-Day Forecast',
  category: 'Weather',
  keybinding: 'ctrl+shift+w',
  when: 'editorFocus',

  async execute() {
    const city = await showInputBox({ prompt: 'City name' });
    if (!city) return;

    const forecast = await weatherService.getForecast(city, 5);

    const items = forecast.map(day => ({
      label: day.date,
      description: `${day.high}°/${day.low}° - ${day.conditions}`
    }));

    await showQuickPick(items, {
      title: `5-Day Forecast for ${city}`
    });
  }
});

Command Properties

  • id — Unique identifier (namespace.action format)
  • title — Display name in command palette
  • category — Grouping in command palette
  • keybinding — Optional default keyboard shortcut
  • when — Context condition for when command is available
  • execute — The function to run

UI Helpers

Ultra provides several UI helpers for commands:

// Text input
const name = await showInputBox({
  prompt: 'Enter your name',
  placeholder: 'John Doe',
  validateInput: (value) => {
    if (!value) return 'Name is required';
    return undefined;
  }
});

// Selection list
const choice = await showQuickPick([
  { label: 'Option A', description: 'First option' },
  { label: 'Option B', description: 'Second option' }
], {
  title: 'Select an option',
  canPickMany: false
});

// Notifications
showMessage('Operation completed!');
showWarning('This might take a while...');
showError('Something went wrong');

UI Components

Ultra's TUI layer provides components for building interactive interfaces in the terminal.

Status Bar Items

import { createStatusBarItem, StatusBarAlignment } from '@ultra/ui';

const weatherStatus = createStatusBarItem({
  id: 'weather.status',
  alignment: StatusBarAlignment.Right,
  priority: 100
});

// Update the display
weatherStatus.text = '☀️ 72°F';
weatherStatus.tooltip = 'Current weather in San Francisco';
weatherStatus.command = 'weather.showCurrent';
weatherStatus.show();

// Update periodically
setInterval(async () => {
  const weather = await weatherService.getCurrentWeather('San Francisco');
  weatherStatus.text = `${getEmoji(weather.conditions)} ${weather.temperature}°F`;
}, 60000);

Custom Panels

import { registerPanel, Panel, renderBox } from '@ultra/ui';

registerPanel({
  id: 'weather.panel',
  title: 'Weather',
  icon: '🌤️',
  location: 'sidebar',

  render(width: number, height: number): string[] {
    const weather = this.state.weather;

    if (!weather) {
      return ['Loading...'];
    }

    return renderBox({
      title: weather.city,
      width,
      content: [
        `Temperature: ${weather.temperature}°F`,
        `Conditions: ${weather.conditions}`,
        `Humidity: ${weather.humidity}%`
      ]
    });
  },

  async onActivate() {
    this.state.weather = await weatherService.getCurrentWeather('San Francisco');
    this.refresh();
  }
});

Editor Decorations

import { createDecorationType, setDecorations } from '@ultra/editor';

// Define a decoration style
const highlightDecoration = createDecorationType({
  backgroundColor: 'rgba(255, 255, 0, 0.3)',
  borderRadius: '3px'
});

// Apply to ranges in the editor
setDecorations(editor, highlightDecoration, [
  { startLine: 10, startCol: 0, endLine: 10, endCol: 20 },
  { startLine: 15, startCol: 5, endLine: 15, endCol: 15 }
]);

// Clear decorations
clearDecorations(editor, highlightDecoration);

Testing Extensions

Ultra provides a test client for headless testing without terminal I/O.

Test Setup

import { describe, test, expect, beforeEach } from 'bun:test';
import { TestECPClient } from '@ultra/testing';
import { weatherAdapter } from './adapter';

describe('WeatherService', () => {
  let client: TestECPClient;

  beforeEach(() => {
    client = new TestECPClient();
    client.registerAdapter(weatherAdapter);
  });

  test('gets current weather', async () => {
    const response = await client.request('weather/getCurrent', {
      city: 'San Francisco'
    });

    expect(response.result).toBeDefined();
    expect(response.result.city).toBe('San Francisco');
    expect(response.result.temperature).toBeNumber();
  });

  test('returns error for invalid city', async () => {
    const response = await client.request('weather/getCurrent', {
      city: ''
    });

    expect(response.error).toBeDefined();
    expect(response.error.code).toBe(-32602);
  });
});

Running Tests

# Run all tests
bun test

# Run specific test file
bun test src/services/weather/weather.test.ts

# Watch mode
bun test --watch

Contributing to Ultra

We welcome contributions! Here's how to get started with Ultra development.

Development Setup

# Clone the repository
git clone https://github.com/zorz/ultra.git
cd ultra

# Install dependencies
bun install

# Run in development mode
bun run dev

# Run tests
bun test

# Build for production
bun run build

Code Style

  • TypeScript strict mode enabled
  • Prettier for formatting (runs on commit)
  • ESLint for linting
  • Conventional commits for commit messages

Pull Request Process

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/my-feature)
  3. Make your changes with tests
  4. Run bun test and bun run lint
  5. Commit with a descriptive message
  6. Push and open a pull request

Start Building

Clone the repo and start extending Ultra today.