Files
Everjoy/doc/Architecture.md

16 KiB

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

/// 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

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.

// 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

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 switchinghide() 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.