feat(layout): supporte la création de layouts plugins (customPluginLayout)
Étend le flux de création de layout backend (DTO, usecases, store) et le frontend (sélecteur, adaptateurs, LayoutTabs/LayoutGrid) pour permettre d'ouvrir un layout déclaré par un plugin installé (ex. Android Health), sans passer par le message bloquant "extension backend pas encore livrée". Ticket #141 — QA vert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@ -1118,9 +1118,9 @@ pub fn parse_session_id(raw: &str) -> Result<SessionId, ErrorDto> {
|
||||
|
||||
use application::{
|
||||
CreateLayoutOutput, DeleteLayoutOutput, LayoutInfo, LayoutOperation, ListLayoutsOutput,
|
||||
LoadLayoutOutput, MutateLayoutOutput, SetActiveLayoutOutput,
|
||||
LoadLayoutOutput, MutateLayoutOutput, PluginLayoutOrigin, SetActiveLayoutOutput,
|
||||
};
|
||||
use domain::{AgentId, Direction, LayoutId, LayoutTree, NodeId};
|
||||
use domain::{AgentId, Direction, LayoutId, LayoutTree, NodeId, PluginId, PluginLayoutType};
|
||||
|
||||
/// Response DTO carrying a layout tree.
|
||||
///
|
||||
@ -1214,6 +1214,15 @@ pub enum LayoutOperationDto {
|
||||
#[serde(default)]
|
||||
conversation_id: Option<String>,
|
||||
},
|
||||
/// Persist opaque plugin layout state.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
SetPluginLayoutState {
|
||||
/// Custom plugin layout node.
|
||||
target: String,
|
||||
/// Opaque plugin-owned state.
|
||||
#[serde(default)]
|
||||
state: serde_json::Value,
|
||||
},
|
||||
}
|
||||
|
||||
impl LayoutOperationDto {
|
||||
@ -1264,6 +1273,10 @@ impl LayoutOperationDto {
|
||||
target: parse_node_id(&target)?,
|
||||
conversation_id,
|
||||
},
|
||||
Self::SetPluginLayoutState { target, state } => LayoutOperation::SetPluginLayoutState {
|
||||
target: parse_node_id(&target)?,
|
||||
state,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1306,7 +1319,7 @@ pub struct LayoutInfoDto {
|
||||
pub id: String,
|
||||
/// Display name.
|
||||
pub name: String,
|
||||
/// Layout kind: `"terminal"` or `"gitGraph"`.
|
||||
/// Layout kind: `"terminal"`, `"gitGraph"` or `"plugin"`.
|
||||
pub kind: String,
|
||||
}
|
||||
|
||||
@ -1315,6 +1328,7 @@ impl From<LayoutInfo> for LayoutInfoDto {
|
||||
let kind = match info.kind {
|
||||
LayoutKind::Terminal => "terminal",
|
||||
LayoutKind::GitGraph => "gitGraph",
|
||||
LayoutKind::Plugin { .. } => "plugin",
|
||||
}
|
||||
.to_owned();
|
||||
Self {
|
||||
@ -1384,9 +1398,15 @@ pub struct CreateLayoutRequestDto {
|
||||
pub project_id: String,
|
||||
/// Display name for the new layout.
|
||||
pub name: String,
|
||||
/// Optional layout kind: `"terminal"` (default) or `"gitGraph"`.
|
||||
/// Optional layout kind: `"terminal"` (default), `"gitGraph"` or `"plugin"`.
|
||||
#[serde(default)]
|
||||
pub kind: Option<String>,
|
||||
/// Plugin origin for `"plugin"` layouts.
|
||||
#[serde(default)]
|
||||
pub plugin_origin: Option<PluginLayoutOriginDto>,
|
||||
/// Opaque initial state for `"plugin"` layouts.
|
||||
#[serde(default)]
|
||||
pub state: serde_json::Value,
|
||||
}
|
||||
|
||||
impl CreateLayoutRequestDto {
|
||||
@ -1398,6 +1418,16 @@ impl CreateLayoutRequestDto {
|
||||
match self.kind.as_deref() {
|
||||
None | Some("terminal") => Ok(LayoutKind::Terminal),
|
||||
Some("gitGraph") => Ok(LayoutKind::GitGraph),
|
||||
Some("plugin") | Some("customPluginLayout") => {
|
||||
let origin = self.plugin_origin.clone().ok_or_else(|| ErrorDto {
|
||||
code: "INVALID".to_owned(),
|
||||
message: "pluginOrigin is required for plugin layouts".to_owned(),
|
||||
})?;
|
||||
Ok(LayoutKind::Plugin {
|
||||
plugin_origin: origin.try_into()?,
|
||||
state: self.state.clone(),
|
||||
})
|
||||
}
|
||||
Some(other) => Err(ErrorDto {
|
||||
code: "INVALID".to_owned(),
|
||||
message: format!("unknown layout kind: {other}"),
|
||||
@ -1406,6 +1436,35 @@ impl CreateLayoutRequestDto {
|
||||
}
|
||||
}
|
||||
|
||||
/// Stable provider identity for a plugin layout creation request.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PluginLayoutOriginDto {
|
||||
/// Provider plugin id.
|
||||
pub plugin_id: String,
|
||||
/// Layout type declared by the provider.
|
||||
pub layout_type: String,
|
||||
}
|
||||
|
||||
impl TryFrom<PluginLayoutOriginDto> for PluginLayoutOrigin {
|
||||
type Error = ErrorDto;
|
||||
|
||||
fn try_from(value: PluginLayoutOriginDto) -> Result<Self, Self::Error> {
|
||||
let plugin_id = PluginId::new(value.plugin_id).map_err(|e| ErrorDto {
|
||||
code: "INVALID".to_owned(),
|
||||
message: e.to_string(),
|
||||
})?;
|
||||
let layout_type = PluginLayoutType::new(value.layout_type).map_err(|e| ErrorDto {
|
||||
code: "INVALID".to_owned(),
|
||||
message: e.to_string(),
|
||||
})?;
|
||||
Ok(Self {
|
||||
plugin_id,
|
||||
layout_type,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Request DTO for `rename_layout`.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
||||
Reference in New Issue
Block a user