Files
Everjoy/doc/Plugin-Lifecycle-Tool-vs-Provider.md

4.9 KiB

Tool vs Provider: Different Lifecycles

The Problem

"How can text plugin receive a tool call when it is not activated to main panel?"

Answer: Tool and Provider have different lifecycles.

Plugin (EditorPlugin) ── lives as long as the app runs (or until plugin is unloaded)
├── Tool: open_file           ── always available
├── Tool: save_file           ── always available
├── Tool: find_and_replace    ── always available
│
└── Provider: EditorProvider  ── created/destroyed per Main Panel instance
    ├── Instance A (showing main.rs)
    ├── Instance B (showing lib.rs)  ← if we support split later
    └── destroyed when user closes the file

Key Insight

Tool is registered at Plugin level, not Provider level.

pub trait Plugin {
    fn bootstrap(registrar: &mut dyn Registrar);
}

pub trait Registrar {
    // Tool lives with the Plugin
    fn register_tool(&mut self, desc: ToolDesc, handler: Box<dyn Tool>);
    
    // Provider is a factory — created on demand when panel needs it
    fn register_provider(&mut self, desc: ProviderDesc, factory: ProviderFactory);
}

How It Works

Plugin State (Shared)

pub struct EditorPlugin {
    // Plugin-level state: all open documents
    documents: HashMap<PathBuf, Document>,
    config: EditorConfig,
}

impl Plugin for EditorPlugin {
    fn bootstrap(registrar: &mut dyn Registrar) {
        let plugin = Arc::new(Mutex::new(EditorPlugin::new()));
        
        // Register tools — these live with the plugin, not any provider instance
        registrar.register_tool(
            ToolDesc { name: "open_file", ... },
            Box::new(OpenFileTool { plugin: plugin.clone() }),
        );
        registrar.register_tool(
            ToolDesc { name: "find_and_replace", ... },
            Box::new(FindReplaceTool { plugin: plugin.clone() }),
        );
        
        // Register provider factory
        registrar.register_provider(
            ProviderDesc { id: "editor", panel: PanelType::Main, ... },
            Box::new(move |_| Box::new(EditorProvider::new(plugin.clone()))),
        );
    }
}

Tool Execution (Always Available)

pub struct OpenFileTool {
    plugin: Arc<Mutex<EditorPlugin>>,
}

impl Tool for OpenFileTool {
    fn execute(&self, input: ToolInput) -> ToolOutput {
        let path = input.args["path"].as_str().unwrap();
        let content = std::fs::read_to_string(path).unwrap();
        
        // 1. Store in plugin-level state
        self.plugin.lock().documents.insert(path.into(), Document::new(content));
        
        // 2. Request UI to open this file in Main Panel
        //    This goes through a UI action, not the tool system
        runtime.ui_actions().open_file(path);
        
        ToolOutput::ok("File opened")
    }
}

Provider Render (Reads from Shared State)

pub struct EditorProvider {
    plugin: Arc<Mutex<EditorPlugin>>,
    current_file: PathBuf,
}

impl PanelProvider for EditorProvider {
    fn render(&mut self, area: Rect, buf: &mut Buffer) {
        let plugin = self.plugin.lock();
        let doc = plugin.documents.get(&self.current_file).unwrap();
        // render document content
    }
}

Lifecycle Comparison

Tool Provider
Registered by Plugin Plugin
Created when Plugin bootstrap Panel needs to display content
Destroyed when Plugin unload Panel switches away (unless keep_alive)
Can be called when Always Only when panel is active (render/event)
Has state Yes, Plugin-level shared state Yes, instance-level state (cursor, scroll)

Agent Opens a File: Full Flow

Agent: "Open src/main.rs"
    → LLM decides to call tool "open_file"
    → Tool Registry finds "open_file" (registered by EditorPlugin)
    → OpenFileTool.execute(path: "src/main.rs")
        → Read file from disk
        → Store in EditorPlugin.documents
        → Call ui_actions.open_file("src/main.rs")
            → UI Framework: Main Panel provider = "editor"
            → UI Framework: EditorProvider.current_file = "src/main.rs"
            → EditorProvider.init()
            → EditorProvider.render() — reads from shared state
    → Tool returns "File opened" to Agent
    → Agent displays "I've opened src/main.rs for you"

Why This Matters

Without this separation:

  • Agent calls open_file → Editor Provider not active → crash or no-op
  • Tool must wait for Provider to be created → Agent workflow broken

With this separation:

  • Tool operates on Plugin-level state, independent of UI
  • UI switches to Provider after Tool completes
  • Provider renders from shared state

Design Rule

Plugin = shared state + tools + provider factory. Tool = operates on shared state, always callable. Provider = renders shared state into a panel, only active when visible.