Maybe Runtime subsystem done

This commit is contained in:
2026-06-04 08:19:50 +08:00
parent 06100b0561
commit 6b17a884e5
43 changed files with 7687 additions and 0 deletions

414
doc/Architecture.md Normal file
View 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.*