Maybe Runtime subsystem done
This commit is contained in:
218
doc/Architecture-Runtime-and-Plugin.md
Normal file
218
doc/Architecture-Runtime-and-Plugin.md
Normal file
@@ -0,0 +1,218 @@
|
||||
# Everjoy Architecture: Runtime Subsystem + Plugin Subsystem
|
||||
|
||||
## Design Principle
|
||||
|
||||
**Separate concerns:**
|
||||
- **Runtime Subsystem** = the engine. Defines interfaces, manages lifecycle, routes events, schedules rendering. Everything (built-in + external) depends on it.
|
||||
- **Plugin Subsystem** = the adapter. Loads external code, translates C-ABI ↔ Rust trait. Only external modules go through it.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Runtime Subsystem │
|
||||
│ (supporter for everything) │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Module Registry │
|
||||
│ - PanelProvider registry │
|
||||
│ - MenuItem registry │
|
||||
│ - StatusWidget registry │
|
||||
│ - Tool registry │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Lifecycle Manager │
|
||||
│ - init → render → event → hide → │
|
||||
│ show → shutdown │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Event Router │
|
||||
│ - Layer 1: System shortcuts │
|
||||
│ - Layer 2: Focused PanelProvider │
|
||||
│ - Layer 3: Global shortcuts / Menubar │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Render Scheduler │
|
||||
│ - Calls Provider::render() each frame │
|
||||
│ - Blits cell grid → Buffer for │
|
||||
│ external providers │
|
||||
└─────────────────────────────────────────┘
|
||||
↑ ↓ traits
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Plugin Subsystem │
|
||||
│ (loader + translator) │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Extension Loader │
|
||||
│ - Scan plugin directory │
|
||||
│ - Load .wasm / .so / static │
|
||||
│ - Symbol resolution │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Translation Layer │
|
||||
│ - C-ABI ↔ Rust trait │
|
||||
│ - EjSurface memory management │
|
||||
│ - Cell grid → Buffer blitting │
|
||||
│ - Style resolution │
|
||||
│ - Panic isolation │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Runtime Subsystem
|
||||
|
||||
Defines the **trait interfaces** that all modules must satisfy. Built-in modules implement these directly. External modules satisfy them via the Plugin Subsystem.
|
||||
|
||||
### Core Traits
|
||||
|
||||
```rust
|
||||
/// Anything that can render into a panel
|
||||
pub trait PanelProvider: Send {
|
||||
fn init(&mut self, ctx: &mut dyn ProviderCtx);
|
||||
fn render(&mut self, area: Rect, buf: &mut Buffer);
|
||||
fn handle_event(&mut self, event: Event) -> EventResult;
|
||||
fn focus_changed(&mut self, focused: bool);
|
||||
fn hide(&mut self);
|
||||
fn show(&mut self);
|
||||
fn shutdown(&mut self);
|
||||
fn can_open(&self, context: &OpenContext) -> bool;
|
||||
fn keep_alive_on_hide(&self) -> bool { false }
|
||||
}
|
||||
|
||||
/// Callable by Agents
|
||||
pub trait Tool: Send {
|
||||
fn name(&self) -> &str;
|
||||
fn description(&self) -> &str;
|
||||
fn schema(&self) -> ToolSchema; // JSON Schema for LLM function calling
|
||||
fn execute(&self, input: ToolInput) -> ToolOutput;
|
||||
}
|
||||
|
||||
/// Menubar entry
|
||||
pub trait MenuItem: Send {
|
||||
fn id(&self) -> &str;
|
||||
fn label(&self) -> &str;
|
||||
fn parent(&self) -> Option<&str>;
|
||||
fn shortcut(&self) -> Option<&str>;
|
||||
fn invoke(&self);
|
||||
}
|
||||
|
||||
/// Status bar widget
|
||||
pub trait StatusWidget: Send {
|
||||
fn id(&self) -> &str;
|
||||
fn position(&self) -> StatusPosition;
|
||||
fn render(&self, area: Rect, buf: &mut Buffer);
|
||||
}
|
||||
```
|
||||
|
||||
### Module Registry
|
||||
|
||||
```rust
|
||||
pub struct ModuleRegistry {
|
||||
panel_providers: HashMap<String, Box<dyn PanelProvider>>,
|
||||
menu_items: Vec<Box<dyn MenuItem>>,
|
||||
status_widgets: Vec<Box<dyn StatusWidget>>,
|
||||
tools: HashMap<String, Box<dyn Tool>>,
|
||||
}
|
||||
```
|
||||
|
||||
Built-in modules register directly:
|
||||
|
||||
```rust
|
||||
// In everjoy/src/main.rs or a built-in module initializer
|
||||
runtime.register_panel_provider("editor", Box::new(EditorProvider::new()));
|
||||
runtime.register_panel_provider("terminal", Box::new(TerminalProvider::new()));
|
||||
runtime.register_panel_provider("tasktree", Box::new(TaskTreeProvider::new()));
|
||||
runtime.register_tool("read_file", Box::new(ReadFileTool));
|
||||
runtime.register_menu_item(Box::new(QuitMenuItem));
|
||||
```
|
||||
|
||||
### Lifecycle (Runtime-managed)
|
||||
|
||||
```
|
||||
Load (built-in = static init, external = via Plugin Subsystem)
|
||||
↓
|
||||
bootstrap() — register all contributions
|
||||
↓
|
||||
Running — render / event / command loop
|
||||
↓
|
||||
Unload — shutdown() all providers, unregister all contributions
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Plugin Subsystem
|
||||
|
||||
Only responsible for **external modules**. Built-in modules never touch it.
|
||||
|
||||
### Extension Loader
|
||||
|
||||
```rust
|
||||
pub struct ExtensionLoader {
|
||||
plugin_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl ExtensionLoader {
|
||||
pub fn scan_and_load(&self, runtime: &mut Runtime) -> Result<()> {
|
||||
for entry in fs::read_dir(&self.plugin_dir)? {
|
||||
let path = entry?.path();
|
||||
let plugin = self.load(&path)?;
|
||||
plugin.bootstrap(runtime.registrar())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load(&self, path: &Path) -> Result<Box<dyn Plugin>> {
|
||||
match path.extension() {
|
||||
Some("wasm") => self.load_wasm(path),
|
||||
Some("so") | Some("dll") => self.load_dylib(path),
|
||||
_ => Err("Unknown plugin format"),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Translation Layer
|
||||
|
||||
For external plugins (especially WASM), the Plugin Subsystem provides a translation layer:
|
||||
|
||||
```rust
|
||||
/// Wraps a C-ABI / WASM plugin into Runtime-compatible traits
|
||||
pub struct TranslatedProvider {
|
||||
wasm_instance: wasmtime::Instance,
|
||||
// ... cached function handles
|
||||
}
|
||||
|
||||
impl PanelProvider for TranslatedProvider {
|
||||
fn render(&mut self, area: Rect, buf: &mut Buffer) {
|
||||
// 1. Allocate EjSurface
|
||||
let mut surface = EjSurface::new(area.width, area.height);
|
||||
// 2. Call plugin's C render function
|
||||
self.call_render(&mut surface);
|
||||
// 3. Blit cell grid into ratatui Buffer
|
||||
self.blit(&surface, area, buf);
|
||||
}
|
||||
// ... other methods translated similarly
|
||||
}
|
||||
```
|
||||
|
||||
### Key Rule
|
||||
|
||||
> **Built-in modules implement Runtime traits directly.**
|
||||
> **External modules are wrapped by Plugin Subsystem to satisfy Runtime traits.**
|
||||
|
||||
```rust
|
||||
// Built-in: direct trait implementation
|
||||
runtime.register_panel_provider("editor", Box::new(EditorProvider::new()));
|
||||
|
||||
// External: loaded via Plugin Subsystem, wrapped to satisfy trait
|
||||
let plugin = plugin_subsystem.load("image-viewer.wasm")?;
|
||||
let provider = plugin.create_provider("image-viewer.main")?;
|
||||
runtime.register_panel_provider("image-viewer", Box::new(provider));
|
||||
// ^^^^ provider here is a TranslatedProvider
|
||||
// that implements PanelProvider
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Concern | Runtime Subsystem | Plugin Subsystem |
|
||||
|---------|-------------------|------------------|
|
||||
| **Defines** | Trait interfaces | Loading + translation mechanism |
|
||||
| **Used by** | Everyone (built-in + external) | Only external modules |
|
||||
| **Manages** | Registry, lifecycle, events, render | File I/O, WASM runtime, FFI |
|
||||
| **Depends on** | ratatui, crossterm, tokio | wasmtime / libloading, Runtime traits |
|
||||
| **Can be omitted?** | No — without it, no app | Yes — without it, only built-in modules work |
|
||||
414
doc/Architecture.md
Normal file
414
doc/Architecture.md
Normal file
@@ -0,0 +1,414 @@
|
||||
# Everjoy Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
Everjoy is a TUI IDE with an AI-native workflow. The architecture separates concerns into four subsystems:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ UI Framework │
|
||||
│ - Layout, focus, render scheduling │
|
||||
│ - Menubar, Status Bar, Splash Screen │
|
||||
│ - Panel management (Sidebar | Main | Agent) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Runtime Subsystem │
|
||||
│ - Trait definitions (Provider, Tool, Plugin) │
|
||||
│ - Module Registry (providers, tools, menu, status) │
|
||||
│ - Lifecycle management │
|
||||
│ - Command dispatch (Provider → App) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Agent Framework │
|
||||
│ - Multi-agent orchestration │
|
||||
│ - Role system (Secretary, Driver, Executor...) │
|
||||
│ - Tool invocation via LLM function calling │
|
||||
│ - Memory & context management │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Plugin Subsystem │
|
||||
│ - Extension loader (.wasm / .so / static) │
|
||||
│ - C-ABI ↔ Rust trait translation │
|
||||
│ - Optional — built-in modules don't use it │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. Runtime Subsystem
|
||||
|
||||
The foundation. Defines what modules can do and manages their lifecycle.
|
||||
|
||||
### 1.1 Core Traits
|
||||
|
||||
```rust
|
||||
/// A plugin registers its contributions during bootstrap
|
||||
pub trait Plugin: Send {
|
||||
fn bootstrap(registrar: &mut dyn Registrar);
|
||||
}
|
||||
|
||||
/// Where content comes from (file, URL, memory)
|
||||
pub enum Source {
|
||||
File { path: PathBuf },
|
||||
Url { url: String },
|
||||
Memory { name: String, data: Vec<u8> },
|
||||
}
|
||||
|
||||
/// Actions a Provider can request from the system
|
||||
pub enum Command {
|
||||
/// Present content in a panel. System bids providers on that panel.
|
||||
Present { panel: PanelType, source: Source, mime_hint: Option<String> },
|
||||
|
||||
/// Launch a tool/session by id. Direct dispatch, no bidding.
|
||||
Launch { panel: PanelType, tool_id: String, params: serde_json::Value },
|
||||
|
||||
FocusPanel(PanelType),
|
||||
ShowMessage { msg: String, level: MessageLevel },
|
||||
}
|
||||
|
||||
/// Renders content into a panel
|
||||
pub trait PanelProvider: Send {
|
||||
fn id(&self) -> &str;
|
||||
fn init(&mut self);
|
||||
fn render(&mut self, area: Rect, buf: &mut Buffer);
|
||||
/// Returns (handled?, commands to dispatch)
|
||||
fn handle_event(&mut self, event: Event) -> (EventResult, Vec<Command>);
|
||||
fn focus_changed(&mut self, focused: bool);
|
||||
fn hide(&mut self);
|
||||
fn show(&mut self);
|
||||
fn shutdown(&mut self);
|
||||
/// Score how well this provider handles the source. 0 = cannot handle.
|
||||
fn can_open(&self, source: &Source, mime_hint: Option<&str>) -> i32;
|
||||
fn keep_alive_on_hide(&self) -> bool { false }
|
||||
}
|
||||
|
||||
/// Callable by Agents via LLM function calling
|
||||
pub trait Tool: Send {
|
||||
fn name(&self) -> &str;
|
||||
fn description(&self) -> &str;
|
||||
fn schema(&self) -> ToolSchema;
|
||||
fn execute(&self, input: ToolInput) -> ToolOutput;
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 Registrar
|
||||
|
||||
```rust
|
||||
pub trait Registrar {
|
||||
fn register_provider(&mut self, desc: ProviderDesc, factory: ProviderFactory);
|
||||
fn register_tool(&mut self, desc: ToolDesc, handler: Box<dyn Tool>);
|
||||
fn register_menu_item(&mut self, desc: MenuItemDesc, handler: Box<dyn Fn() + Send>);
|
||||
fn register_status_widget(&mut self, desc: StatusWidgetDesc, render: Box<dyn Fn(Rect, &mut Buffer) + Send>);
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 Command Pattern (replaces ProviderCtx)
|
||||
|
||||
**Removed:** `ProviderCtx` trait. Providers no longer call into the system via a context object.
|
||||
|
||||
**New:** Providers return `Vec<Command>` from `handle_event()`. The App (main.rs) is the sole dispatcher.
|
||||
|
||||
Why: `ProviderCtx::open_file()` was an implicit black-box router — the caller didn't know which provider would handle the file, and adding new providers required modifying core code. The Command pattern makes every intent explicit and traceable.
|
||||
|
||||
```rust
|
||||
// Before: implicit, hard-coded routing
|
||||
fn handle_event(&mut self, ev: Event) -> EventResult {
|
||||
self.ctx.open_file("main.rs"); // who handles this? nobody knows
|
||||
Consumed
|
||||
}
|
||||
|
||||
// After: explicit intent, App decides
|
||||
fn handle_event(&mut self, ev: Event) -> (EventResult, Vec<Command>) {
|
||||
let cmd = Command::Present {
|
||||
panel: PanelType::Main,
|
||||
source: Source::File { path: "main.rs".into() },
|
||||
mime_hint: None,
|
||||
};
|
||||
(Consumed, vec![cmd])
|
||||
}
|
||||
```
|
||||
|
||||
### 1.4 Module Registry
|
||||
|
||||
```rust
|
||||
pub struct ModuleRegistry {
|
||||
pub panel_providers: HashMap<String, RegisteredProvider>,
|
||||
pub tools: HashMap<String, Box<dyn Tool>>,
|
||||
pub menu_items: Vec<Box<dyn MenuItem>>,
|
||||
pub status_widgets: Vec<Box<dyn StatusWidget>>,
|
||||
}
|
||||
|
||||
pub struct RegisteredProvider {
|
||||
pub desc: ProviderDesc,
|
||||
pub factory: ProviderFactory,
|
||||
pub instance: Option<Box<dyn PanelProvider>>,
|
||||
}
|
||||
```
|
||||
|
||||
Key methods:
|
||||
- `providers_for_panel(panel) -> Vec<&RegisteredProvider>` — filter by panel type
|
||||
- `ensure_instance(id) -> Option<&mut Box<dyn PanelProvider>>` — lazy instantiate
|
||||
- `find_best_provider(panel, source, mime_hint) -> Option<(String, i32)>` — bid scoring
|
||||
- `shutdown_all()` — graceful teardown
|
||||
|
||||
### 1.5 Lifecycle
|
||||
|
||||
```
|
||||
Load
|
||||
├─ Built-in: registered into ModuleRegistry at App startup
|
||||
└─ External: via Plugin Subsystem
|
||||
↓
|
||||
Bootstrap (Plugin::bootstrap)
|
||||
├─ Register providers (with panel type + capabilities)
|
||||
├─ Register tools
|
||||
├─ Register menu items
|
||||
└─ Register status widgets
|
||||
↓
|
||||
Running
|
||||
├─ Render loop: App asks registry for active provider per panel, calls render()
|
||||
├─ Event loop: App routes to focused provider, receives Commands, dispatches
|
||||
├─ Tool calls: Agent Framework → Tool::execute()
|
||||
└─ Present/Launch: App bids/routes to the best provider
|
||||
↓
|
||||
Unload (atomic)
|
||||
├─ Provider::shutdown() for all active providers
|
||||
├─ Unregister all contributions
|
||||
└─ Plugin::shutdown()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Plugin Subsystem
|
||||
|
||||
Optional subsystem for external modules. Built-in modules implement Runtime traits directly.
|
||||
|
||||
### 2.1 Architecture
|
||||
|
||||
```
|
||||
External Plugin (.wasm / .so)
|
||||
↓
|
||||
Plugin Subsystem
|
||||
├─ Extension Loader: file I/O, symbol resolution
|
||||
└─ Translation Layer
|
||||
├─ C-ABI ↔ Rust trait
|
||||
├─ EjSurface memory management
|
||||
├─ Cell grid → Buffer blitting
|
||||
└─ Panic isolation
|
||||
↓
|
||||
Runtime Subsystem (receives Box<dyn PanelProvider>, Box<dyn Tool>)
|
||||
```
|
||||
|
||||
### 2.2 Key Rule
|
||||
|
||||
> Built-in modules implement Runtime traits directly.
|
||||
> External modules are wrapped by Plugin Subsystem to satisfy Runtime traits.
|
||||
|
||||
---
|
||||
|
||||
## 3. UI Framework
|
||||
|
||||
Manages what the user sees and how they interact.
|
||||
|
||||
### 3.1 Layout
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ _F_ile _E_dit _V_iew _A_gent _H_elp │ Menubar
|
||||
├──────────┬──────────────────────────────┬───────────────────┤
|
||||
│ 📁 src │ 📁 src > main.rs │ 🤖 Agent Chat │
|
||||
│ 📄 Cargo │ [File Editor] │ Claude GPT-4 Local│
|
||||
│ 📄 doc │ │ ───────────────── │
|
||||
│ 📁 targ │ fn main() { │ You: ... │
|
||||
│ │ let x = 42; │ Claude: ... │
|
||||
│ │ } │ │
|
||||
│ │ │ Quick Links: │
|
||||
│ │ │ [agents.md] ... │
|
||||
│ │ │ │
|
||||
├──────────┴──────────────────────────────┴───────────────────┤
|
||||
│ main.rs | Rust | UTF-8 | Ln 3, Col 10 | [Main] │ Status Bar
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- **Sidebar** — 20% width, FileTree (navigate files, Enter to open in Main)
|
||||
- **Main Panel** — 52% width, dynamic content (Editor, Terminal, etc.)
|
||||
- **Agent Panel** — 28% width, chat + quick links
|
||||
- **Menubar** — 1 row, underlined capitals for Alt+ shortcuts
|
||||
- **Status Bar** — 1 row, file info + focused panel
|
||||
|
||||
### 3.2 Focus Management
|
||||
|
||||
| Input | Action |
|
||||
|-------|--------|
|
||||
| `Alt` | Focus Menubar |
|
||||
| `Ctrl+J` | Focus Sidebar |
|
||||
| `Ctrl+K` | Focus Main Panel |
|
||||
| `Ctrl+L` | Focus Agent Panel |
|
||||
| Mouse click | Focus clicked panel directly |
|
||||
|
||||
### 3.3 Panel State
|
||||
|
||||
- **Main Panel** — dynamic provider, single content area
|
||||
- **Provider switching** — `hide()` old, `show()` new, old may `keep_alive_on_hide()`
|
||||
- **Navigation** — stack-based history, no tab bar (too heavy for TUI)
|
||||
- **Bidding** — when `Command::Present` arrives, all providers on the target panel score themselves via `can_open()`. Highest score wins.
|
||||
|
||||
### 3.4 Event Routing
|
||||
|
||||
```
|
||||
Layer 1: System shortcuts (Ctrl+Q quit) — intercepted by App
|
||||
Layer 2: Menubar — if active
|
||||
Layer 3: Focus shortcuts (Ctrl+J/K/L) — intercepted by App
|
||||
Layer 4: Focused PanelProvider → returns (EventResult, Vec<Command>)
|
||||
Layer 5: App dispatches returned Commands
|
||||
```
|
||||
|
||||
### 3.5 Render Order
|
||||
|
||||
Per frame:
|
||||
1. Clear
|
||||
2. Menubar
|
||||
3. Panel backgrounds (uniform dark)
|
||||
4. Sidebar Provider::render()
|
||||
5. Main Panel Provider::render()
|
||||
6. Agent Panel Provider::render()
|
||||
7. Status Bar
|
||||
|
||||
---
|
||||
|
||||
## 4. Agent Framework
|
||||
|
||||
### 4.1 Role System
|
||||
|
||||
Managed by core system, not plugins.
|
||||
|
||||
| Role | Icon | Purpose | Trigger |
|
||||
|------|------|---------|---------|
|
||||
| Secretary | 👤 | User conversation | Always |
|
||||
| Driver | 🧭 | Exploration, curiosity | Auto |
|
||||
| NegFeedback | 🛡️ | Error detection | Auto |
|
||||
| Executor | ⚡ | Command execution | Auto |
|
||||
|
||||
### 4.2 Tool Invocation
|
||||
|
||||
Agent → LLM function calling → Tool Registry → Tool::execute() → result → Agent
|
||||
|
||||
Tools are registered by plugins. The Agent Framework queries the Tool Registry for available tools and generates JSON Schema for the LLM.
|
||||
|
||||
---
|
||||
|
||||
## 5. Data Flows
|
||||
|
||||
### 5.1 User Opens a File
|
||||
|
||||
```
|
||||
User navigates to main.rs in Sidebar (FileTree)
|
||||
→ FileTreeProvider::handle_event(Enter)
|
||||
→ Returns Command::Present {
|
||||
panel: Main,
|
||||
source: Source::File { path: "src/main.rs" }
|
||||
}
|
||||
→ App::dispatch_command()
|
||||
→ App asks registry: find_best_provider(Main, File("main.rs"), "text/rust")
|
||||
→ EditorProvider::can_open(File, "text/rust") = 100
|
||||
→ TerminalProvider::can_open(File, ...) = 0
|
||||
→ Editor wins
|
||||
→ App hides old Main provider, shows Editor
|
||||
→ EditorProvider.render()
|
||||
```
|
||||
|
||||
### 5.2 Agent Opens a File
|
||||
|
||||
```
|
||||
Agent decides to open src/main.rs
|
||||
→ LLM function calling: "open_file" tool
|
||||
→ Tool Registry finds "open_file"
|
||||
→ OpenFileTool.execute(path: "src/main.rs")
|
||||
→ Read file from disk
|
||||
→ Returns Command::Present { panel: Main, source: File(...) }
|
||||
→ App dispatches → routes to EditorProvider
|
||||
→ Tool returns "File opened" to Agent
|
||||
```
|
||||
|
||||
### 5.3 Provider Renders a Frame
|
||||
|
||||
```
|
||||
UI Framework render loop
|
||||
→ Determine panel areas from layout
|
||||
→ Look up active provider id per panel from App.active_providers
|
||||
→ Ask registry for instance
|
||||
→ Sidebar area → FileTreeProvider::render(area, buf)
|
||||
→ Main area → EditorProvider::render(area, buf) (or whoever is active)
|
||||
→ Agent area → AgentChatProvider::render(area, buf)
|
||||
→ Draw Menubar and Status Bar
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Directory Structure
|
||||
|
||||
```
|
||||
everjoy/
|
||||
├── src/
|
||||
│ ├── main.rs # Entry point, App event loop, Command dispatch
|
||||
│ ├── lib.rs # Public API
|
||||
│ │
|
||||
│ ├── runtime/ # Runtime Subsystem
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── traits.rs # Plugin, PanelProvider, Tool, Command, Source
|
||||
│ │ ├── registry.rs # ModuleRegistry, provider bidding
|
||||
│ │ ├── lifecycle.rs # Provider lifecycle management
|
||||
│ │ └── ctx.rs # RuntimeServices (ProviderCtx removed)
|
||||
│ │
|
||||
│ ├── ui/ # UI Framework
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── layout.rs # Panel sizing
|
||||
│ │ ├── focus.rs # Focus management
|
||||
│ │ ├── router.rs # Event routing (legacy, to be removed)
|
||||
│ │ ├── scheduler.rs # Render scheduling
|
||||
│ │ ├── menubar.rs # Menubar rendering
|
||||
│ │ ├── statusbar.rs # Status bar rendering
|
||||
│ │ └── splash.rs # Splash screen
|
||||
│ │
|
||||
│ ├── agent/ # Agent Framework
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── roles.rs # Agent role definitions
|
||||
│ │ ├── orchestrator.rs # Multi-agent coordination
|
||||
│ │ ├── memory.rs # Short/long-term memory
|
||||
│ │ └── tools.rs # Tool invocation for LLM
|
||||
│ │
|
||||
│ ├── plugin/ # Plugin Subsystem
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── loader.rs # ExtensionLoader (.wasm / .so)
|
||||
│ │ ├── translator.rs # C-ABI ↔ Rust trait
|
||||
│ │ ├── surface.rs # EjSurface, cell grid
|
||||
│ │ └── panic.rs # Panic isolation
|
||||
│ │
|
||||
│ └── builtin/ # Built-in modules
|
||||
│ ├── editor/
|
||||
│ ├── terminal/
|
||||
│ ├── tasktree/
|
||||
│ ├── agentchat/
|
||||
│ └── filetree/
|
||||
│
|
||||
├── doc/
|
||||
│ └── Architecture.md # This file
|
||||
│
|
||||
└── Cargo.toml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Key Design Decisions
|
||||
|
||||
| Decision | Choice | Rationale |
|
||||
|----------|--------|-----------|
|
||||
| Provider → System comms | **Command pattern** | Explicit intents, no hidden routing, easy to log/intercept |
|
||||
| Content routing | **Bidding (can_open score)** | Multiple providers can compete; new providers need no core changes |
|
||||
| Provider rendering | Dual-track | Built-in direct Buffer, external cell grid |
|
||||
| Event routing | 5-layer | System → Menubar → Focus shortcuts → Provider → Command dispatch |
|
||||
| Tool lifecycle | Plugin-level | Independent of Provider activation |
|
||||
| Main Panel | Single content | No tab bar, stack nav, keep_alive_on_hide |
|
||||
| Workspaces | Not in MVP | Manual provider switching sufficient |
|
||||
| Plugin loading | Eager + config | All at startup, user-configurable blacklist |
|
||||
| Focus | Keyboard + mouse | Ctrl+J/K/L for panels (avoids terminal tab interception) |
|
||||
|
||||
---
|
||||
|
||||
*Architecture approved? If yes, this becomes the authoritative spec.*
|
||||
157
doc/Plugin-Lifecycle-Tool-vs-Provider.md
Normal file
157
doc/Plugin-Lifecycle-Tool-vs-Provider.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# 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.**
|
||||
|
||||
```rust
|
||||
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)
|
||||
|
||||
```rust
|
||||
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)
|
||||
|
||||
```rust
|
||||
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)
|
||||
|
||||
```rust
|
||||
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.**
|
||||
Reference in New Issue
Block a user