# 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 }, } /// 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 }, /// 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); 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); fn register_menu_item(&mut self, desc: MenuItemDesc, handler: Box); fn register_status_widget(&mut self, desc: StatusWidgetDesc, render: Box); } ``` ### 1.3 Command Pattern (replaces ProviderCtx) **Removed:** `ProviderCtx` trait. Providers no longer call into the system via a context object. **New:** Providers return `Vec` 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) { 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, pub tools: HashMap>, pub menu_items: Vec>, pub status_widgets: Vec>, } pub struct RegisteredProvider { pub desc: ProviderDesc, pub factory: ProviderFactory, pub instance: Option>, } ``` Key methods: - `providers_for_panel(panel) -> Vec<&RegisteredProvider>` — filter by panel type - `ensure_instance(id) -> Option<&mut Box>` — 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, Box) ``` ### 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) 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.*