git_paw/supervisor/
layout.rs1use crate::error::PawError;
14
15pub const SUPERVISOR_MAX_AGENTS: usize = 25;
19
20pub const SUPERVISOR_AGENTS_PER_ROW: usize = 5;
22
23pub const SUPERVISOR_PANE_OFFSET: usize = 2;
26
27#[derive(Debug, Clone, Copy, PartialEq)]
29pub struct SupervisorLayout {
30 pub agent_rows: usize,
32 pub total_rows: usize,
34 pub top_row_pct: u8,
36 pub agent_row_pct: f32,
39}
40
41pub fn supervisor_layout(agent_count: usize) -> Result<SupervisorLayout, PawError> {
45 if agent_count > SUPERVISOR_MAX_AGENTS {
46 return Err(PawError::ConfigError(format!(
47 "{agent_count} agents requested; maximum is {SUPERVISOR_MAX_AGENTS} per session.\n\
48 \n\
49 Split into multiple sessions:\n \
50 git paw start --branches <subset>\n\
51 \n\
52 (Configurable max_agents is planned for v1.0.0 — see milestone.)"
53 )));
54 }
55
56 let agent_rows = agent_count.div_ceil(SUPERVISOR_AGENTS_PER_ROW).max(1);
57 let total_rows = agent_rows + 1;
58
59 let (top_row_pct, agent_row_pct) = match total_rows {
60 2 => (60u8, 40.0_f32),
61 3 => (40u8, 30.0_f32),
62 4 => (28u8, 24.0_f32),
63 5 => (28u8, 18.0_f32),
64 6 => (28u8, 14.4_f32),
65 _ => unreachable!("agent_count > SUPERVISOR_MAX_AGENTS is rejected above"),
66 };
67
68 Ok(SupervisorLayout {
69 agent_rows,
70 total_rows,
71 top_row_pct,
72 agent_row_pct,
73 })
74}
75
76pub fn layout_for(agent_count: usize) -> Result<SupervisorLayout, PawError> {
86 supervisor_layout(agent_count)
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 fn assert_layout(
94 agent_count: usize,
95 expected_rows: usize,
96 expected_top: u8,
97 expected_agent: f32,
98 ) {
99 let layout = supervisor_layout(agent_count).expect("layout should compute");
100 assert_eq!(
101 layout.agent_rows, expected_rows,
102 "agent_rows for {agent_count}"
103 );
104 assert_eq!(
105 layout.total_rows,
106 expected_rows + 1,
107 "total_rows for {agent_count}"
108 );
109 assert_eq!(
110 layout.top_row_pct, expected_top,
111 "top_row_pct for {agent_count}"
112 );
113 assert!(
114 (layout.agent_row_pct - expected_agent).abs() < 0.01,
115 "agent_row_pct for {agent_count}: expected {expected_agent}, got {}",
116 layout.agent_row_pct
117 );
118 }
119
120 #[test]
121 fn layout_for_1_agent() {
122 assert_layout(1, 1, 60, 40.0);
123 }
124
125 #[test]
126 fn layout_for_5_agents() {
127 assert_layout(5, 1, 60, 40.0);
128 }
129
130 #[test]
131 fn layout_for_6_agents() {
132 assert_layout(6, 2, 40, 30.0);
133 }
134
135 #[test]
136 fn layout_for_10_agents() {
137 assert_layout(10, 2, 40, 30.0);
138 }
139
140 #[test]
141 fn layout_for_11_agents() {
142 assert_layout(11, 3, 28, 24.0);
143 }
144
145 #[test]
146 fn layout_for_15_agents() {
147 assert_layout(15, 3, 28, 24.0);
148 }
149
150 #[test]
151 fn layout_for_16_agents() {
152 assert_layout(16, 4, 28, 18.0);
153 }
154
155 #[test]
156 fn layout_for_20_agents() {
157 assert_layout(20, 4, 28, 18.0);
158 }
159
160 #[test]
161 fn layout_for_21_agents() {
162 assert_layout(21, 5, 28, 14.4);
163 }
164
165 #[test]
166 fn layout_for_25_agents() {
167 assert_layout(25, 5, 28, 14.4);
168 }
169
170 #[test]
171 fn layout_rejects_26_agents() {
172 let err = supervisor_layout(26).expect_err("26 agents should be rejected");
173 let msg = err.to_string();
174 assert!(
175 msg.contains("26 agents requested"),
176 "error mentions count: {msg}"
177 );
178 assert!(msg.contains("maximum is 25"), "error mentions max: {msg}");
179 assert!(
180 msg.contains("--branches"),
181 "error suggests --branches workaround: {msg}"
182 );
183 }
184
185 #[test]
186 fn layout_rejects_far_above_cap() {
187 let err = supervisor_layout(100).expect_err("100 agents should be rejected");
188 assert!(err.to_string().contains("100 agents requested"));
189 }
190
191 #[test]
192 fn layout_for_matches_supervisor_layout_across_the_range() {
193 for n in 1..=SUPERVISOR_MAX_AGENTS {
196 assert_eq!(
197 layout_for(n).expect("layout_for should compute"),
198 supervisor_layout(n).expect("supervisor_layout should compute"),
199 "layout_for({n}) should match supervisor_layout({n})"
200 );
201 }
202 assert!(
203 layout_for(SUPERVISOR_MAX_AGENTS + 1).is_err(),
204 "layout_for should reject above the cap like supervisor_layout"
205 );
206 }
207
208 #[test]
209 fn constants_have_expected_values() {
210 assert_eq!(SUPERVISOR_MAX_AGENTS, 25);
211 assert_eq!(SUPERVISOR_AGENTS_PER_ROW, 5);
212 assert_eq!(SUPERVISOR_PANE_OFFSET, 2);
213 }
214}