Skip to content

Extension Guide

This guide documents VIBE's extension system: how extensions work, how to create them, and the roadmap for a more complete extension API.

Overview

VIBE supports extensions through several mechanisms:

  1. CLI Extensions - Add subcommands to the vibe CLI
  2. Web Extensions - Add Flask blueprints and routes
  3. Handler Registry - Register custom data type handlers
  4. Interview Mode Layouts - Custom UI layouts (currently partially hardcoded)

Current Extension APIs

CLI Extensions

CLI extensions add subcommands to the vibe command. They follow a protocol-based registration pattern.

Location: vibe/cli/extensions.py

Protocol:

class CLIExtension(Protocol):
    """Protocol for CLI extensions."""

    name: str                    # Command name (e.g., "review")
    help: str                    # Help text for the command
    quiet_by_default: bool       # Whether to set VIBE_QUIET_MODE

    def setup_parser(self, subparsers: _SubParsersAction) -> ArgumentParser:
        """Add extension's parser(s) to the CLI subparsers."""
        ...

    def dispatch(self, args: Namespace) -> int:
        """Handle parsed arguments. Returns exit code."""
        ...

Registration:

# In your extension's cli.py
from vibe.cli.extensions import register_extension

class MyExtension:
    name = "myext"
    help = "My extension commands"
    quiet_by_default = False

    def setup_parser(self, subparsers):
        parser = subparsers.add_parser(self.name, help=self.help)
        parser.add_argument("--option", help="An option")
        return parser

    def dispatch(self, args):
        # Handle the command
        return 0

# Register on module import
register_extension(MyExtension())

Discovery:

Extensions are discovered by importing known module paths in discover_extensions(). New extensions must be added to the discovery list:

# vibe/cli/extensions.py
extension_modules = [
    "vibe.review.cli",
    "your.extension.cli",  # Add here
]

Example: See vibe/review/cli.py for a complete implementation with subcommands.

Web Extensions

Web extensions add Flask blueprints to the application. They follow a similar protocol-based pattern.

Location: vibe/web/extensions.py

Protocol:

class WebExtension(Protocol):
    """Protocol for web extensions."""

    name: str

    @property
    def blueprints(self) -> Sequence[tuple[Blueprint, str | None]]:
        """Return blueprints to register as (blueprint, url_prefix) tuples."""
        ...

    def init_app(self, app: Flask) -> None:
        """Initialize extension with the Flask app (optional setup)."""
        ...

Registration:

# In your extension's web/__init__.py
from flask import Blueprint
from vibe.web.extensions import register_extension

review_bp = Blueprint("review", __name__, template_folder="templates")

class ReviewWebExtension:
    name = "review"

    @property
    def blueprints(self):
        return [(review_bp, "/review")]

    def init_app(self, app):
        # Optional: Add template search paths, context processors, etc.
        pass

register_extension(ReviewWebExtension())

Discovery:

Same pattern as CLI - modules are listed in discover_extensions():

# vibe/web/extensions.py
extension_modules = [
    "vibe.review.web",
]

Example: See vibe/review/web/__init__.py for a complete implementation.

Handler Registry

Data type handlers process different field types (Text, Bool, Enum, etc.). They use a decorator-based registry.

Location: vibe/handlers/base.py

Registration:

from vibe.handlers.base import DataTypeHandler, register_handler

@register_handler("mytype")
class MyTypeHandler(DataTypeHandler):
    """Handler for 'type: mytype' fields."""

    def validate(self, value, field_config, context):
        # Validate the value
        return value

    def get_widget_context(self, field_name, field_config, value, context):
        # Return template context for rendering
        return {"field_name": field_name, "value": value}

Handlers are automatically discovered when the handler modules are imported.

Interview Mode Extensions

Interview modes control the UI layout for templates. The Interview Mode Extension API allows extensions to register custom modes without modifying core VIBE code.

Location: vibe/interview_modes/

Available Modes

Mode Type Description Layout Template
standard Built-in Two-column: questions + preview layout/standard.html
assistant Extension Conversational AI workbench assistant/layout.html
review Extension Document review workbench layout/review.html

Configuration

Modes are specified in config.yml:

interview_mode: assistant  # or: standard, review

Protocol

# vibe/interview_modes/base.py

from typing import Protocol, Optional, Any
from flask import Response
from vibe.structures import TemplateData

class InterviewModeExtension(Protocol):
    """Protocol for interview mode extensions."""

    name: str  # Mode name used in config.yml (e.g., "review", "assistant")

    def get_layout_template(self) -> str:
        """Return the Jinja template path for this mode's layout.

        Example: 'layout/review.html' or 'assistant/layout.html'
        """
        ...

    def handle_start(
        self,
        template: TemplateData,
        version: Optional[str]
    ) -> Optional[Response]:
        """Handle the /interview/<template_id>/ route for this mode.

        Return a Response to override default behavior (e.g., redirect).
        Return None to use the standard interview page with this mode's layout.
        """
        ...

    def get_template_context(
        self,
        template: TemplateData,
        base_context: dict
    ) -> dict:
        """Extend the template context for this mode's layout.

        Args:
            template: The template being rendered
            base_context: Standard context (questions, preview, etc.)

        Returns:
            Extended context dict for the layout template
        """
        ...

Proposed Registration

# vibe/interview_modes/registry.py (proposed)

_modes: dict[str, InterviewModeExtension] = {}

def register_interview_mode(mode: InterviewModeExtension) -> None:
    """Register an interview mode extension."""
    _modes[mode.name] = mode

def get_interview_mode(name: str) -> InterviewModeExtension | None:
    """Get a registered interview mode by name."""
    return _modes.get(name)

def get_layout_for_mode(name: str) -> str:
    """Get the layout template path for a mode."""
    mode = _modes.get(name)
    if mode:
        return mode.get_layout_template()
    # Fallback to built-in modes
    return {
        "standard": "layout/standard.html",
    }.get(name, "layout/standard.html")

Example: Review Mode Extension

# vibe/review/interview_mode.py

from flask import redirect, url_for
from vibe.interview_modes import register_interview_mode

class ReviewModeExtension:
    name = "review"

    def get_layout_template(self) -> str:
        return "layout/review.html"

    def handle_start(self, template, version):
        # Redirect to review module's session form
        return redirect(url_for(
            'review.new_session_form',
            template_id=template.template_id
        ))

    def get_template_context(self, template, base_context):
        # Add review-specific context
        return {
            **base_context,
            "review_mode": True,
        }

# Register on module import
def _register():
    register_interview_mode(ReviewModeExtension())

_register()

Example: Assistant Mode Extension

# vibe/assistant/interview_mode.py

class AssistantModeExtension:
    name = "assistant"

    def get_layout_template(self) -> str:
        return "assistant/layout.html"

    def handle_start(self, template, version):
        # Use standard interview page with assistant layout
        return None

    def get_template_context(self, template, base_context):
        # Add assistant mode flags
        context = {**base_context, "assistant_mode": True}
        if template.assistant_prompts:
            context["skip_question_rendering"] = True
            context["skip_preview_rendering"] = True
        return context

# Register on module import
def _register():
    register_interview_mode(AssistantModeExtension())

_register()

Core Integration

The core uses the extension API to dispatch to modes:

# vibe/web/routes/interview.py

from vibe.interview_modes import get_interview_mode

@interview_bp.route("/interview/<template_id>/")
def start_interview(template, **kwargs):
    # Check for interview mode extension that overrides entry behavior
    mode_ext = get_interview_mode(template.interview_mode)
    if mode_ext:
        response = mode_ext.handle_start(template, version)
        if response:
            return response

    # Standard rendering with mode's layout
    return render_interview_page(template, mode=template.interview_mode)

The pages/interview.html template uses dynamic layout selection:

{# pages/interview.html #}
{% extends 'layout/shell.html' %}

{% block content %}
{# Use layout_template from context (set by interview_modes extension API) #}
{% if layout_template is defined %}
    {% include layout_template %}
{% else %}
    {# Fallback for built-in modes #}
    {% include 'layout/standard.html' %}
{% endif %}
{% endblock %}

Type Definition

The interview_mode field in structures.py uses Optional[str] with runtime validation:

# vibe/structures.py

@dataclass
class TemplateData:
    # Interview mode - validated at runtime via interview_modes.is_valid_interview_mode()
    # Built-in modes: "standard"
    # Extension modes: "assistant", "review", and any registered via register_interview_mode()
    interview_mode: Optional[str] = None

The registry provides validation via is_valid_interview_mode():

from vibe.interview_modes.registry import is_valid_interview_mode

if not is_valid_interview_mode(mode):
    raise ValueError(f"Unknown interview_mode: {mode}")

Creating a New Extension

Checklist

  1. Create module structure:

    vibe/myext/
    ├── __init__.py
    ├── cli.py              # CLI extension
    ├── interview_mode.py   # Interview mode extension
    ├── web/
    │   ├── __init__.py     # Web extension
    │   └── routes.py       # Flask routes
    └── templates/          # Jinja templates
    
  2. Implement CLI extension (if needed):

    • Create class implementing CLIExtension protocol
    • Call register_extension() at module level
    • Add module to vibe/cli/extensions.py discovery list
  3. Implement Web extension (if needed):

    • Create Flask blueprint(s)
    • Create class implementing WebExtension protocol
    • Call register_extension() at module level
    • Add module to vibe/web/extensions.py discovery list
  4. Implement Interview Mode (if creating a new UI mode):

    • Create layout template extending shell.html
    • Create class implementing InterviewModeExtension protocol
    • Call register_interview_mode() at module level
    • Add module to vibe/interview_modes/registry.py discovery list

Best Practices

  • Use protocol-based design - Don't subclass, implement protocols
  • Register on import - Extensions auto-register when modules are imported
  • Keep templates separate - Use template_folder in blueprints
  • Extend, don't replace - Build on top of shell.html and foundation.css
  • Handle missing dependencies - Extensions should gracefully fail if deps aren't installed

See Also

  • vibe/interview_modes/ - Interview mode extension API
  • vibe/review/ - Complete extension example (CLI, Web, Interview Mode)
  • vibe/assistant/ - Complex extension with LLM integration
  • vibe/handlers/ - Handler registry pattern
  • doc/ui-customization.md - Template customization guide