Plugin System

clitic provides a flexible plugin system for extending functionality.

ContentPlugin

Base class for content rendering plugins. Content plugins allow you to render different content types (Markdown, code, etc.) in the Conversation widget.

class clitic.plugins.ContentPlugin[source]

Bases: ABC

Abstract base class for content renderers.

Content plugins are responsible for rendering specific types of content in the conversation display. Each plugin declares what content types it can handle and provides both synchronous and asynchronous rendering.

name

Human-readable name of the plugin.

priority

Priority for plugin ordering (higher = preferred).

abstract property name: str

Return the human-readable name of this plugin.

property priority: int

Return the priority for this plugin.

Higher priority plugins are checked first when determining which plugin should render content. Default is 0.

Returns:

Priority value (higher = more preferred).

abstractmethod can_render(content_type: str, content: str | Renderable) bool[source]

Check if this plugin can render the given content.

Parameters:
  • content_type – MIME type or identifier for the content.

  • content – The content to potentially render.

Returns:

True if this plugin can render the content, False otherwise.

abstractmethod render(content: str | Renderable) object[source]

Render content to a renderable object.

Parameters:

content – The content to render.

Returns:

A Rich renderable (e.g., rich.markdown.Markdown, rich.text.Text) or Textual Widget that can be rendered by Conversation.

Raises:

RenderError – If rendering fails.

async render_async(content: str | Renderable) object[source]

Asynchronously render content to a renderable object.

Default implementation calls the synchronous render method. Subclasses may override for async rendering (e.g., fetching resources).

Parameters:

content – The content to render.

Returns:

A Rich renderable or Textual Widget.

Raises:

RenderError – If rendering fails.

on_register(app: TextualApp) None[source]

Lifecycle hook called when plugin is registered with an app.

Override this method to perform initialization that requires access to the app instance (e.g., loading styles, subscribing to events).

Parameters:

app – The Textual App instance.

on_unregister(app: TextualApp) None[source]

Lifecycle hook called when plugin is unregistered from an app.

Override this method to perform cleanup (e.g., unsubscribing from events, releasing resources).

Parameters:

app – The Textual App instance.

Built-in Plugins

MarkdownPlugin

clitic includes a built-in Markdown plugin that renders Markdown content using Rich’s Markdown renderer:

from clitic import App, Conversation
from clitic.plugins import MarkdownPlugin

app = App(title="My App")
app.register_plugin(MarkdownPlugin())

# Pass plugins to Conversation
conversation = Conversation(plugins=app.get_plugins())
conversation.append(
    "assistant",
    "# Hello\n\nThis is **bold** text.",
    metadata={"content_type": "text/markdown"}
)

The MarkdownPlugin handles these content types:

  • text/markdown (standard MIME type)

  • markdown (short form)

  • markdown/* (any variant)

Creating a Custom Content Plugin

from clitic.plugins import ContentPlugin
from rich.text import Text
from rich.markdown import Markdown

class MyPlugin(ContentPlugin):
    @property
    def name(self) -> str:
        """Human-readable name of the plugin."""
        return "MyPlugin"

    @property
    def priority(self) -> int:
        """Priority for plugin ordering (higher = preferred)."""
        return 10

    def can_render(self, content_type: str, content: str) -> bool:
        """Check if this plugin can render the given content type."""
        return content_type == "my-type"

    def render(self, content: str):
        """Render content to a Rich renderable.

        Returns:
            A Rich renderable (e.g., Text, Markdown, Table)
        """
        # Return Rich renderable for the content
        return Text(f"[MyPlugin] {content}")

    async def render_async(self, content: str):
        """Optional async rendering (defaults to sync render)."""
        return self.render(content)

    def on_register(self, app):
        """Called when registered with app."""
        pass

    def on_unregister(self, app):
        """Called when unregistered."""
        pass

Using Plugins with Conversation

from clitic import App, Conversation
from clitic.plugins import MarkdownPlugin

# Create app and register plugins
app = App(title="My App")
app.register_plugin(MarkdownPlugin())

# Pass plugins to Conversation
conversation = Conversation(plugins=app.get_plugins())

# Add content with content_type metadata
conversation.append(
    "assistant",
    "# Welcome\n\n- Item 1\n- Item 2\n\n```python\nprint('Hello')\n```",
    metadata={"content_type": "text/markdown"}
)

ModeProvider

Base class for input mode providers.

class clitic.plugins.ModeProvider[source]

Bases: ABC

Abstract base class for input mode providers.

Mode providers detect and handle different input modes (e.g., markdown, code blocks, plain text). They provide syntax highlighting and transform input text when entering or exiting the mode.

name

Human-readable name of the mode.

indicator

Short indicator displayed in the input bar.

priority

Priority for mode detection (higher = preferred).

abstract property name: str

Return the human-readable name of this mode.

abstract property indicator: str

Return the short indicator for this mode.

This is displayed in the input bar to show the current mode.

property priority: int

Return the priority for this mode provider.

Higher priority providers are checked first when detecting the current input mode. Default is 0.

Returns:

Priority value (higher = more preferred).

abstractmethod detect(text: str, cursor_position: int) bool[source]

Detect if this mode should be active.

Parameters:
  • text – Current input text.

  • cursor_position – Current cursor position in the text.

Returns:

True if this mode should be active, False otherwise.

abstractmethod get_highlighter() Highlighter | None[source]

Get the syntax highlighter for this mode.

Returns:

A Highlighter instance, or None if no highlighting is available.

on_enter(text: str) str[source]

Lifecycle hook called when entering this mode.

Override to transform text when the mode becomes active. Default returns text unchanged.

Parameters:

text – Current input text.

Returns:

unchanged).

Return type:

Transformed text (default

on_exit(text: str) str[source]

Lifecycle hook called when exiting this mode.

Override to transform text when leaving the mode. Default returns text unchanged.

Parameters:

text – Current input text.

Returns:

unchanged).

Return type:

Transformed text (default

Creating a Mode Provider

from clitic.plugins import ModeProvider, Highlighter

class ShellModeProvider(ModeProvider):
    @property
    def name(self) -> str:
        return "shell"

    @property
    def indicator(self) -> str:
        return "$"

    @property
    def priority(self) -> int:
        return 5

    def detect(self, text: str, cursor_position: int) -> bool:
        # Return True if this mode should activate
        return text.startswith("$ ")

    def get_highlighter(self) -> Highlighter | None:
        return ShellHighlighter()

    def on_enter(self, text: str) -> str:
        # Transform text on mode entry
        return text

    def on_exit(self, text: str) -> str:
        # Transform text on mode exit
        return text

Protocols

Renderable

class clitic.plugins.Renderable(*args, **kwargs)[source]

Bases: Protocol

Protocol for renderable content types.

Any object that implements __str__ can be used as renderable content, allowing flexibility in content types beyond just strings.

Highlighter

class clitic.plugins.Highlighter(*args, **kwargs)[source]

Bases: Protocol

Protocol for syntax highlighters.

Highlighters transform plain text into syntax-highlighted strings, typically using Rich markup or similar formatting.

highlight(text: str) str[source]

Apply syntax highlighting to text.

Parameters:

text – Plain text to highlight.

Returns:

Highlighted text with markup (e.g., Rich markup).

Plugin Priority

Plugins are checked in priority order (highest first). Use the priority property to control ordering:

Priority

Use Case

100+

Custom overrides

50-99

Specialized renderers

10-49

Standard renderers (MarkdownPlugin uses 10)

1-9

Fallback renderers

0

Default (checked last)

Plugin Architecture

Content Flow

When content is added to a Conversation with content_type metadata:

  1. Conversation checks registered plugins for a match

  2. Plugins are sorted by priority (highest first)

  3. First plugin with can_render(content_type, content) == True is selected

  4. Plugin’s render() method converts content to Rich renderable

  5. Rich renderable is converted to Strips for virtual rendering

  6. If no plugin matches or rendering fails, falls back to plain text

Error Handling

If a plugin’s render() method raises an exception, the Conversation falls back to plain text rendering and logs the error. This ensures the application remains functional even with broken plugins.

Performance

Plugin content is pre-rendered to Strips when added to the Conversation, maintaining O(1) virtual rendering performance. The Strips are cached and only re-rendered on resize events.