feat(backend): système de plugins — domaine, application, infrastructure (#43)
Lots B1-B4 : modèle de domaine des plugins (manifeste, capacités menus/layouts/MCP), port et registre applicatif, chargement/validation en infrastructure, exposition DTO et commandes Tauri. Tests cargo verts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@ -10,6 +10,7 @@ use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::ids::{AgentId, NodeId, SessionId, TabId, WindowId};
|
||||
use crate::plugin::{PluginId, PluginLayoutType};
|
||||
|
||||
/// Direction of a [`SplitContainer`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@ -160,6 +161,24 @@ pub struct GridContainer {
|
||||
pub cells: Vec<GridCell>,
|
||||
}
|
||||
|
||||
/// Persisted custom layout cell provided by an installed plugin.
|
||||
///
|
||||
/// The domain stores only the stable provider identity, layout type, and opaque
|
||||
/// state. It never stores or resolves the React component used to render it.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CustomPluginLayoutCell {
|
||||
/// Node identifier.
|
||||
pub id: NodeId,
|
||||
/// Provider plugin id.
|
||||
pub plugin_id: PluginId,
|
||||
/// Persisted layout type declared by the provider plugin.
|
||||
pub layout_type: PluginLayoutType,
|
||||
/// Opaque plugin-owned state.
|
||||
#[serde(default)]
|
||||
pub state: serde_json::Value,
|
||||
}
|
||||
|
||||
/// A node in the layout tree.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", tag = "type", content = "node")]
|
||||
@ -170,6 +189,8 @@ pub enum LayoutNode {
|
||||
Split(SplitContainer),
|
||||
/// A spreadsheet-style grid.
|
||||
Grid(GridContainer),
|
||||
/// A plugin-provided custom layout leaf.
|
||||
CustomPluginLayout(CustomPluginLayoutCell),
|
||||
}
|
||||
|
||||
/// The root of a layout (one per tab).
|
||||
@ -663,6 +684,7 @@ impl LayoutTree {
|
||||
out.push((leaf.id, agent));
|
||||
}
|
||||
}
|
||||
LayoutNode::CustomPluginLayout(_) => {}
|
||||
LayoutNode::Split(split) => {
|
||||
for child in &split.children {
|
||||
walk(&child.node, out);
|
||||
@ -770,6 +792,7 @@ impl LayoutTree {
|
||||
match n {
|
||||
LayoutNode::Leaf(leaf) if leaf.id == id => Some(leaf),
|
||||
LayoutNode::Leaf(_) => None,
|
||||
LayoutNode::CustomPluginLayout(_) => None,
|
||||
LayoutNode::Split(split) => split.children.iter().find_map(|c| find(&c.node, id)),
|
||||
LayoutNode::Grid(grid) => grid.cells.iter().find_map(|c| find(&c.node, id)),
|
||||
}
|
||||
@ -784,6 +807,7 @@ impl LayoutTree {
|
||||
match node {
|
||||
LayoutNode::Leaf(leaf) if leaf.id == id => Some(leaf.session),
|
||||
LayoutNode::Leaf(_) => None,
|
||||
LayoutNode::CustomPluginLayout(_) => None,
|
||||
LayoutNode::Split(split) => split.children.iter().find_map(|c| find(&c.node, id)),
|
||||
LayoutNode::Grid(grid) => grid.cells.iter().find_map(|c| find(&c.node, id)),
|
||||
}
|
||||
@ -797,6 +821,7 @@ impl LayoutTree {
|
||||
fn map_node(node: &LayoutNode, f: &mut impl FnMut(&LayoutNode) -> LayoutNode) -> LayoutNode {
|
||||
let rebuilt = match node {
|
||||
LayoutNode::Leaf(_) => node.clone(),
|
||||
LayoutNode::CustomPluginLayout(_) => node.clone(),
|
||||
LayoutNode::Split(split) => LayoutNode::Split(SplitContainer {
|
||||
id: split.id,
|
||||
direction: split.direction,
|
||||
@ -844,6 +869,7 @@ fn validate_node(
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
LayoutNode::CustomPluginLayout(_) => Ok(()),
|
||||
LayoutNode::Split(split) => {
|
||||
if split.children.is_empty() {
|
||||
return Err(LayoutError::EmptySplit);
|
||||
|
||||
Reference in New Issue
Block a user