agentmux_common/layout_types.rs
1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Shared layout tree types used in both the srv wire protocol (ipc.rs
5//! Command/Event variants) and the srv object store (obj.rs LayoutState).
6//!
7//! Living in agentmux-common so agentmux-srv, agentmux-cef, and agentmux-
8//! launcher can all reference the same type definitions without circular
9//! dependencies or duplication.
10//!
11//! Part of srv Phase E.4.B (Phase 3 — wire types).
12//! See docs/specs/srv-phase-e4b-formal-spec-2026-05-03.md §4.1, §5, §6.
13
14use serde::{Deserialize, Serialize};
15
16// ── Core tree types ─────────────────────────────────────────────────────────
17
18/// Direction children flow within a layout node (row = horizontal split,
19/// column = vertical split). Defaults to `Row` when absent in JSON for
20/// tolerance of older blobs.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
22#[serde(rename_all = "lowercase")]
23pub enum FlexDirection {
24 #[default]
25 Row,
26 Column,
27}
28
29/// Leaf-only payload — references the block this layout leaf renders.
30/// Group nodes (those with `children`) carry no `data`.
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
32pub struct LayoutNodeData {
33 #[serde(rename = "blockId")]
34 pub block_id: String,
35 /// Catch-all for unknown fields — preserves forward-compat when the
36 /// frontend writes additional fields we don't yet model. Uses
37 /// `serde_json::Map` (insertion-ordered) for deterministic round-trips.
38 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
39 pub extra: serde_json::Map<String, serde_json::Value>,
40}
41
42/// One node in the layout tree. Stable UUID-keyed; `size` is a relative
43/// flex unit; `children` form the recursive structure (empty for leaves).
44///
45/// JSON shape (matches frontend `LayoutNode` in `frontend/layout/lib/types.ts`):
46/// ```json
47/// { "id": "...", "flexDirection": "row", "size": 1, "children": [...], "data": { "blockId": "..." } }
48/// ```
49///
50/// Note: `Default::size` is 0.0 (Rust f32 default), while the serde
51/// deserialization default is 1.0. The `Default` derive is only for the
52/// `..Default::default()` spread trick at construction sites — size is
53/// always set explicitly in practice.
54#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
55pub struct LayoutNode {
56 pub id: String,
57 #[serde(rename = "flexDirection", default)]
58 pub flex_direction: FlexDirection,
59 /// Flex size — relative units within the parent's children array.
60 /// Custom serializer emits whole-number sizes as integers (`10` not
61 /// `10.0`) to preserve byte-equal compat with prior JSON output.
62 #[serde(
63 default = "default_layout_size",
64 serialize_with = "serialize_size_smallest",
65 )]
66 pub size: f32,
67 #[serde(default, skip_serializing_if = "Vec::is_empty")]
68 pub children: Vec<LayoutNode>,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub data: Option<LayoutNodeData>,
71 /// Catch-all for unknown top-level fields. Uses `serde_json::Map`
72 /// (insertion-ordered) for deterministic round-trips.
73 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
74 pub extra: serde_json::Map<String, serde_json::Value>,
75}
76
77fn default_layout_size() -> f32 {
78 1.0
79}
80
81fn serialize_size_smallest<S>(value: &f32, serializer: S) -> Result<S::Ok, S::Error>
82where
83 S: serde::Serializer,
84{
85 if value.fract() == 0.0
86 && value.is_finite()
87 && (i64::MIN as f32..=i64::MAX as f32).contains(value)
88 {
89 serializer.serialize_i64(*value as i64)
90 } else {
91 serializer.serialize_f32(*value)
92 }
93}
94
95// ── Command helper types ────────────────────────────────────────────────────
96
97/// A single resize operation: target node + new flex size (0–100 relative units).
98#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
99pub struct ResizeOp {
100 pub node_id: String,
101 /// New flex size. Must be in 0.0..=100.0; reducer validates.
102 pub size: f32,
103}
104
105/// Position for split commands: where to insert the new node relative to the target.
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
107#[serde(rename_all = "lowercase")]
108pub enum SplitPosition {
109 Before,
110 After,
111}