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
76#[cfg(test)]
77mod tests {
78 use super::*;
79
80 fn assert_layout(
81 agent_count: usize,
82 expected_rows: usize,
83 expected_top: u8,
84 expected_agent: f32,
85 ) {
86 let layout = supervisor_layout(agent_count).expect("layout should compute");
87 assert_eq!(
88 layout.agent_rows, expected_rows,
89 "agent_rows for {agent_count}"
90 );
91 assert_eq!(
92 layout.total_rows,
93 expected_rows + 1,
94 "total_rows for {agent_count}"
95 );
96 assert_eq!(
97 layout.top_row_pct, expected_top,
98 "top_row_pct for {agent_count}"
99 );
100 assert!(
101 (layout.agent_row_pct - expected_agent).abs() < 0.01,
102 "agent_row_pct for {agent_count}: expected {expected_agent}, got {}",
103 layout.agent_row_pct
104 );
105 }
106
107 #[test]
108 fn layout_for_1_agent() {
109 assert_layout(1, 1, 60, 40.0);
110 }
111
112 #[test]
113 fn layout_for_5_agents() {
114 assert_layout(5, 1, 60, 40.0);
115 }
116
117 #[test]
118 fn layout_for_6_agents() {
119 assert_layout(6, 2, 40, 30.0);
120 }
121
122 #[test]
123 fn layout_for_10_agents() {
124 assert_layout(10, 2, 40, 30.0);
125 }
126
127 #[test]
128 fn layout_for_11_agents() {
129 assert_layout(11, 3, 28, 24.0);
130 }
131
132 #[test]
133 fn layout_for_15_agents() {
134 assert_layout(15, 3, 28, 24.0);
135 }
136
137 #[test]
138 fn layout_for_16_agents() {
139 assert_layout(16, 4, 28, 18.0);
140 }
141
142 #[test]
143 fn layout_for_20_agents() {
144 assert_layout(20, 4, 28, 18.0);
145 }
146
147 #[test]
148 fn layout_for_21_agents() {
149 assert_layout(21, 5, 28, 14.4);
150 }
151
152 #[test]
153 fn layout_for_25_agents() {
154 assert_layout(25, 5, 28, 14.4);
155 }
156
157 #[test]
158 fn layout_rejects_26_agents() {
159 let err = supervisor_layout(26).expect_err("26 agents should be rejected");
160 let msg = err.to_string();
161 assert!(
162 msg.contains("26 agents requested"),
163 "error mentions count: {msg}"
164 );
165 assert!(msg.contains("maximum is 25"), "error mentions max: {msg}");
166 assert!(
167 msg.contains("--branches"),
168 "error suggests --branches workaround: {msg}"
169 );
170 }
171
172 #[test]
173 fn layout_rejects_far_above_cap() {
174 let err = supervisor_layout(100).expect_err("100 agents should be rejected");
175 assert!(err.to_string().contains("100 agents requested"));
176 }
177
178 #[test]
179 fn constants_have_expected_values() {
180 assert_eq!(SUPERVISOR_MAX_AGENTS, 25);
181 assert_eq!(SUPERVISOR_AGENTS_PER_ROW, 5);
182 assert_eq!(SUPERVISOR_PANE_OFFSET, 2);
183 }
184}