diff --git a/.agents/skills/lib.sh b/.agents/skills/lib.sh index 11fe96c..5f4a816 100644 --- a/.agents/skills/lib.sh +++ b/.agents/skills/lib.sh @@ -429,7 +429,7 @@ except Exception: split_dir="" if [ -n "$sample_pane" ]; then layout_raw=$(_real_herdr pane layout --pane "$sample_pane" 2>/dev/null || echo "") - read -r split_dir split_target < <(printf '%s' "$layout_raw" | python3 -m lib_py.layout --min-cols "${MAM_MIN_PANE_COLS:-40}" --min-rows "${MAM_MIN_PANE_ROWS:-20}" --sample-pane "$sample_pane" 2>/dev/null || echo "right $sample_pane") + read -r split_dir split_target < <(printf '%s' "$layout_raw" | python3 -m lib_py.layout --min-cols "${MAM_MIN_PANE_COLS:-15}" --min-rows "${MAM_MIN_PANE_ROWS:-0}" --sample-pane "$sample_pane" 2>/dev/null || echo "right $sample_pane") split_dir="${split_dir:-right}" sample_pane="${split_target:-$sample_pane}" fi diff --git a/.agents/skills/lib_py/layout.py b/.agents/skills/lib_py/layout.py index b73445c..6f4697f 100644 --- a/.agents/skills/lib_py/layout.py +++ b/.agents/skills/lib_py/layout.py @@ -70,8 +70,8 @@ def extract_panes(data: Dict[str, Any]) -> List[PaneInfo]: def compute_2xk_layout( data: Dict[str, Any], - min_cols: int = 40, - min_rows: int = 20, + min_cols: int = 15, + min_rows: int = 0, max_columns: Optional[int] = None, default_anchor_id: Optional[str] = None ) -> LayoutDecision: @@ -90,8 +90,8 @@ def compute_2xk_layout( if len(panes) == 1: p = panes[0] # In 2xK grid, 1 pane -> 2 panes: split down to create top and bottom rows - # Check height overflow if dimensions known - if p.height > 0 and p.height // 2 < min_rows: + # Check height overflow only if min_rows > 0 is explicitly configured + if min_rows > 0 and p.height > 0 and p.height // 2 < min_rows: # If height is too small for 2 rows, try splitting right if width allows if p.width > 0 and p.width // 2 >= min_cols: return LayoutDecision(target_pane_id=p.pane_id, direction="right", reason="single_pane_height_constrained") @@ -153,8 +153,8 @@ def compute_2xk_layout( if singleton_col is not None: target_p = singleton_col[0] - # Check height - if target_p.height > 0 and target_p.height // 2 < min_rows: + # Check height only if min_rows > 0 + if min_rows > 0 and target_p.height > 0 and target_p.height // 2 < min_rows: return LayoutDecision(target_pane_id=target_p.pane_id, direction="overflow", is_overflow=True, reason="singleton_height_overflow") return LayoutDecision(target_pane_id=target_p.pane_id, direction="down", reason="fill_singleton_column") @@ -176,7 +176,7 @@ def _env_int(*names: str, default: Optional[int] = None) -> Optional[int]: """First *valid* int among the env vars in *names*, else `default`. `default` is an explicit parameter rather than an `or` at the call site so a - legitimate 0 survives (MAM_MIN_PANE_COLS=0 means 0, not the 40 default). + legitimate 0 survives (MAM_MIN_PANE_COLS=0 means 0, not the 15 default). An unparsable value is skipped rather than raised or treated as terminal: a typo in an operator's shell must not take the whole layout call down (lib.sh @@ -198,8 +198,8 @@ def _env_int(*names: str, default: Optional[int] = None) -> Optional[int]: def main(): parser = argparse.ArgumentParser(description="Compute 2xK grid TUI layout split direction") - parser.add_argument("--min-cols", type=int, default=_env_int("MAM_MIN_COLS", "MAM_MIN_PANE_COLS", default=40)) - parser.add_argument("--min-rows", type=int, default=_env_int("MAM_MIN_ROWS", "MAM_MIN_PANE_ROWS", default=20)) + parser.add_argument("--min-cols", type=int, default=_env_int("MAM_MIN_COLS", "MAM_MIN_PANE_COLS", default=15)) + parser.add_argument("--min-rows", type=int, default=_env_int("MAM_MIN_ROWS", "MAM_MIN_PANE_ROWS", default=0)) parser.add_argument("--max-cols", type=int, default=_env_int("MAM_MAX_COLS", "MAM_MAX_PANE_COLS")) parser.add_argument("--sample-pane", type=str, default=None) parser.add_argument("--json", action="store_true", help="Output full JSON decision") diff --git a/.mam.env.example b/.mam.env.example index 40d486d..38338f5 100644 --- a/.mam.env.example +++ b/.mam.env.example @@ -128,13 +128,14 @@ #default: 3 # SKS_EMPTY_GIVEUP=3 -# Minimum columns a pane must retain after a vertical split (2xK layout engine). -#default: 40 -# MAM_MIN_PANE_COLS=40 +# Minimum columns a pane must retain after a horizontal/column split (2xK layout engine). +#default: 15 +# MAM_MIN_PANE_COLS=15 -# Minimum rows a pane must retain after a horizontal split (2xK layout engine). -#default: 20 -# MAM_MIN_PANE_ROWS=20 +# Minimum rows a pane must retain after a vertical split (2xK layout engine). +# Set to 0 to disable vertical row constraints (terminal scrollback handles height). +#default: 0 +# MAM_MIN_PANE_ROWS=0 # Maximum number of columns a workspace may grow to before the engine reports # 'overflow' (which makes lib.sh create a fresh workspace instead of splitting). diff --git a/tests/test_layout.py b/tests/test_layout.py index 218fafc..f2818a9 100644 --- a/tests/test_layout.py +++ b/tests/test_layout.py @@ -287,7 +287,7 @@ split_dir="" # Exact snippet from lib.sh:429-435 if [ -n "$sample_pane" ]; then layout_raw=$(_real_herdr pane layout --pane "$sample_pane" 2>/dev/null || echo "") - read -r split_dir split_target < <(printf '%s' "$layout_raw" | python3 -m lib_py.layout --min-cols "${{MAM_MIN_PANE_COLS:-40}}" --min-rows "${{MAM_MIN_PANE_ROWS:-20}}" --sample-pane "$sample_pane" 2>/dev/null || echo "right $sample_pane") + read -r split_dir split_target < <(printf '%s' "$layout_raw" | python3 -m lib_py.layout --min-cols "${{MAM_MIN_PANE_COLS:-15}}" --min-rows "${{MAM_MIN_PANE_ROWS:-0}}" --sample-pane "$sample_pane" 2>/dev/null || echo "right $sample_pane") split_dir="${{split_dir:-right}}" sample_pane="${{split_target:-$sample_pane}}" fi @@ -435,11 +435,11 @@ _ZERO_TRAP = {"result": {"panes": [ def test_j1_env_zero_min_cols_matches_flag_zero(): - """J-1: MAM_MIN_PANE_COLS=0 must mean 0, not fall through to the 40 default.""" - flag = _run_layout(_ZERO_TRAP, ("--min-cols", "0")) + """J-1: MAM_MIN_PANE_COLS=0 must mean 0, not fall through to the 15 default.""" + flag = _run_layout(_ZERO_TRAP, ("--min-cols", "0", "--min-rows", "20")) assert flag["direction"] == "right" and flag["reason"] == "single_pane_height_constrained" for var in ("MAM_MIN_COLS", "MAM_MIN_PANE_COLS"): - assert _run_layout(_ZERO_TRAP, (), {var: "0"}) == flag, var + assert _run_layout(_ZERO_TRAP, ("--min-rows", "20"), {var: "0"}) == flag, var def test_j1_env_zero_min_rows_matches_flag_zero(): @@ -452,8 +452,8 @@ def test_j1_env_zero_min_rows_matches_flag_zero(): def test_j1_nonzero_and_malformed_env_behaviour_unchanged(): """Behaviour neutrality: non-zero env still applies, and a lone typo still lands on the documented default instead of crashing on a None comparison.""" - assert _run_layout(_ZERO_TRAP, (), {"MAM_MIN_PANE_COLS": "25"}) == \ - _run_layout(_ZERO_TRAP, ("--min-cols", "25")) + assert _run_layout(_ZERO_TRAP, (), {"MAM_MIN_PANE_COLS": "25", "MAM_MIN_PANE_ROWS": "20"}) == \ + _run_layout(_ZERO_TRAP, ("--min-cols", "25", "--min-rows", "20")) assert _run_layout(_ZERO_TRAP, (), {"MAM_MIN_PANE_COLS": "abc"}) == _run_layout(_ZERO_TRAP) @@ -465,32 +465,41 @@ def test_j1b_invalid_alias_does_not_shadow_the_documented_var(): Empty values already fell through (`if raw:`); this makes invalid values behave the same way. When every candidate is unusable, `default` still wins. """ - good = _run_layout(_ZERO_TRAP, (), {"MAM_MIN_PANE_COLS": "25"}) + good = _run_layout(_ZERO_TRAP, ("--min-rows", "20"), {"MAM_MIN_PANE_COLS": "25"}) assert good["direction"] == "right" # 별칭이 깨져 있어도 문서화된 변수가 적용된다 - assert _run_layout(_ZERO_TRAP, (), {"MAM_MIN_COLS": "foo", + assert _run_layout(_ZERO_TRAP, ("--min-rows", "20"), {"MAM_MIN_COLS": "foo", "MAM_MIN_PANE_COLS": "25"}) == good # 0 도 마찬가지 (J-1 과의 상호작용) - assert _run_layout(_ZERO_TRAP, (), {"MAM_MIN_COLS": "foo", + assert _run_layout(_ZERO_TRAP, ("--min-rows", "20"), {"MAM_MIN_COLS": "foo", "MAM_MIN_PANE_COLS": "0"}) == \ - _run_layout(_ZERO_TRAP, ("--min-cols", "0")) + _run_layout(_ZERO_TRAP, ("--min-cols", "0", "--min-rows", "20")) # 모든 후보가 무효면 문서화된 기본값으로 흡수 (Rev.1 불변식 보존) assert _run_layout(_ZERO_TRAP, (), {"MAM_MIN_COLS": "foo", "MAM_MIN_PANE_COLS": "bar"}) == _run_layout(_ZERO_TRAP) -def test_default_min_cols_is_40(): - """Verify compute_2xk_layout default min_cols is 40. - With width 80 (width//2 = 40): - - min_cols=40 -> 40 >= 40 -> split right (new column). - - min_cols=60 -> 40 < 60 -> overflow. - Default invocation (no min_cols passed) must split right. +def test_default_min_cols_is_15_and_min_rows_is_0(): + """Verify compute_2xk_layout default min_cols is 15 and min_rows is 0. + With 1 pane of 54x23: + - min_rows=0 -> splits down without height overflow + With 2 panes of 30x23 (width//2 = 15): + - min_cols=15 -> 15 >= 15 -> split right (new column). + - min_cols=40 -> 15 < 40 -> overflow. + Default invocation (no min_cols/min_rows passed) must split right for 30 cols. """ + # 1. 1 pane of 54x23 splits down (no height constraint) + p1_54x23 = {"result": {"panes": [{"pane_id": "p1", "rect": {"x": 26, "y": 1, "width": 54, "height": 23}}]}} + d1 = compute_2xk_layout(p1_54x23) + assert d1.direction == "down" + assert not d1.is_overflow + + # 2. 2 panes with width 30 (30 // 2 = 15 == min_cols 15) -> splits right cleanly payload = { "result": { "panes": [ - {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 80, "height": 40}}, - {"pane_id": "p2", "rect": {"x": 0, "y": 40, "width": 80, "height": 40}}, + {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 30, "height": 20}}, + {"pane_id": "p2", "rect": {"x": 0, "y": 20, "width": 30, "height": 20}}, ] } } @@ -500,118 +509,79 @@ def test_default_min_cols_is_40(): assert decision.reason == "new_column_right" -def test_80_col_2_column_splitting_boundary(): - """Verify width >= 80 cols allows 2-column splitting with default min_cols=40, - while width < 80 (e.g. 79) triggers column_width_overflow. +def test_30_col_2_column_splitting_boundary(): + """Verify width >= 30 cols allows 2-column splitting with default min_cols=15, + while width < 30 (e.g. 29) triggers column_width_overflow. """ - # 80 cols: 80 // 2 = 40 == min_cols(40) -> splits right - payload_80 = { + # 30 cols: 30 // 2 = 15 == min_cols(15) -> splits right + payload_30 = { "result": { "panes": [ - {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 80, "height": 40}}, - {"pane_id": "p2", "rect": {"x": 0, "y": 40, "width": 80, "height": 40}}, + {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 30, "height": 20}}, + {"pane_id": "p2", "rect": {"x": 0, "y": 20, "width": 30, "height": 20}}, ] } } - d80 = compute_2xk_layout(payload_80) - assert d80.direction == "right" - assert not d80.is_overflow + d30 = compute_2xk_layout(payload_30) + assert d30.direction == "right" + assert not d30.is_overflow - # 79 cols: 79 // 2 = 39 < min_cols(40) -> overflow - payload_79 = { + # 29 cols: 29 // 2 = 14 < min_cols(15) -> overflow + payload_29 = { "result": { "panes": [ - {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 79, "height": 40}}, - {"pane_id": "p2", "rect": {"x": 0, "y": 40, "width": 79, "height": 40}}, + {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 29, "height": 20}}, + {"pane_id": "p2", "rect": {"x": 0, "y": 20, "width": 29, "height": 20}}, ] } } - d79 = compute_2xk_layout(payload_79) - assert d79.direction == "overflow" - assert d79.is_overflow - assert d79.reason == "column_width_overflow" + d29 = compute_2xk_layout(payload_29) + assert d29.direction == "overflow" + assert d29.is_overflow + assert d29.reason == "column_width_overflow" -def test_90_col_single_workspace_multi_pane_tiling(): - """Verify complete 1 -> 2 -> 3 -> 4 pane tiling in a 90-col single workspace. - - 1 pane (90x40): splits down to p1(90x20), p2(90x20) - - 2 panes: splits right to start col 2 -> p3(45x40) - - 3 panes: fills singleton col 2 down -> p4(45x20) - - 4 panes (2x2 grid): 5th agent overflows because 45 // 2 = 22 < 40 +def test_54x23_compact_viewport_single_workspace_multi_pane_tiling(): + """Verify complete 1 -> 2 -> 3 -> 4 pane tiling in standard 80x24 (54x23 content area) workspace. + - 1 pane (54x23): splits down to p1(54x11), p2(54x11) + - 2 panes: splits right (54//2 = 27 >= 15) to start col 2 -> p3(27x23) + - 3 panes: fills singleton col 2 down -> p4(27x11) + - 4 panes (2x2 grid): 5th agent overflows because 27 // 2 = 13 < 15 """ - # 1 -> 2 - p1_layout = {"result": {"panes": [{"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 90, "height": 40}}]}} + # 1 -> 2 (splits down) + p1_layout = {"result": {"panes": [{"pane_id": "p1", "rect": {"x": 26, "y": 1, "width": 54, "height": 23}}]}} d1 = compute_2xk_layout(p1_layout) assert d1.direction == "down" assert d1.target_pane_id == "p1" + assert not d1.is_overflow - # 2 -> 3 + # 2 -> 3 (splits right) p2_layout = {"result": {"panes": [ - {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 90, "height": 20}}, - {"pane_id": "p2", "rect": {"x": 0, "y": 20, "width": 90, "height": 20}}, + {"pane_id": "p1", "rect": {"x": 26, "y": 1, "width": 54, "height": 11}}, + {"pane_id": "p2", "rect": {"x": 26, "y": 12, "width": 54, "height": 11}}, ]}} d2 = compute_2xk_layout(p2_layout) assert d2.direction == "right" assert d2.target_pane_id == "p1" assert not d2.is_overflow - # 3 -> 4 + # 3 -> 4 (fills singleton col 2 down) p3_layout = {"result": {"panes": [ - {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 45, "height": 20}}, - {"pane_id": "p2", "rect": {"x": 0, "y": 20, "width": 45, "height": 20}}, - {"pane_id": "p3", "rect": {"x": 45, "y": 0, "width": 45, "height": 40}}, + {"pane_id": "p1", "rect": {"x": 26, "y": 1, "width": 27, "height": 11}}, + {"pane_id": "p2", "rect": {"x": 26, "y": 12, "width": 27, "height": 11}}, + {"pane_id": "p3", "rect": {"x": 53, "y": 1, "width": 27, "height": 23}}, ]}} d3 = compute_2xk_layout(p3_layout) assert d3.direction == "down" assert d3.target_pane_id == "p3" assert not d3.is_overflow - # 4 -> 5 (overflow to new workspace) + # 4 -> 5 (overflow to new workspace because 27 // 2 = 13 < 15) p4_layout = {"result": {"panes": [ - {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 45, "height": 20}}, - {"pane_id": "p2", "rect": {"x": 0, "y": 20, "width": 45, "height": 20}}, - {"pane_id": "p3", "rect": {"x": 45, "y": 0, "width": 45, "height": 20}}, - {"pane_id": "p4", "rect": {"x": 45, "y": 20, "width": 45, "height": 20}}, - ]}} - d4 = compute_2xk_layout(p4_layout) - assert d4.direction == "overflow" - assert d4.is_overflow - assert d4.reason == "column_width_overflow" - - -def test_100_col_single_workspace_multi_pane_tiling(): - """Verify complete 1 -> 2 -> 3 -> 4 pane tiling in a 100-col single workspace.""" - # 1 -> 2 - p1_layout = {"result": {"panes": [{"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 100, "height": 40}}]}} - d1 = compute_2xk_layout(p1_layout) - assert d1.direction == "down" - - # 2 -> 3 - p2_layout = {"result": {"panes": [ - {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 100, "height": 20}}, - {"pane_id": "p2", "rect": {"x": 0, "y": 20, "width": 100, "height": 20}}, - ]}} - d2 = compute_2xk_layout(p2_layout) - assert d2.direction == "right" - assert not d2.is_overflow - - # 3 -> 4 - p3_layout = {"result": {"panes": [ - {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 50, "height": 20}}, - {"pane_id": "p2", "rect": {"x": 0, "y": 20, "width": 50, "height": 20}}, - {"pane_id": "p3", "rect": {"x": 50, "y": 0, "width": 50, "height": 40}}, - ]}} - d3 = compute_2xk_layout(p3_layout) - assert d3.direction == "down" - assert d3.target_pane_id == "p3" - assert not d3.is_overflow - - # 4 -> 5 (overflow to new workspace) - p4_layout = {"result": {"panes": [ - {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 50, "height": 20}}, - {"pane_id": "p2", "rect": {"x": 0, "y": 20, "width": 50, "height": 20}}, - {"pane_id": "p3", "rect": {"x": 50, "y": 0, "width": 50, "height": 20}}, - {"pane_id": "p4", "rect": {"x": 50, "y": 20, "width": 50, "height": 20}}, + {"pane_id": "p1", "rect": {"x": 26, "y": 1, "width": 27, "height": 11}}, + {"pane_id": "p2", "rect": {"x": 26, "y": 12, "width": 27, "height": 11}}, + {"pane_id": "p3", "rect": {"x": 53, "y": 1, "width": 27, "height": 11}}, + {"pane_id": "p4", "rect": {"x": 53, "y": 12, "width": 27, "height": 11}}, ]}} d4 = compute_2xk_layout(p4_layout) assert d4.direction == "overflow" diff --git a/tests/test_tier1_unit.py b/tests/test_tier1_unit.py index 8ed5c20..a472104 100644 --- a/tests/test_tier1_unit.py +++ b/tests/test_tier1_unit.py @@ -865,88 +865,85 @@ def test_lib_sh_new_session_passes_mam_ws_label(mam_sandbox): # ============================================================================== -# FEATURE: 2xK Grid Layout Engine (min_cols=40 & multi-pane workspace tiling) +# FEATURE: 2xK Grid Layout Engine (min_cols=15 & min_rows=0 multi-pane workspace tiling) # ============================================================================== -def test_layout_default_min_cols_40_in_tier1(): - """Verify default min_cols=40 behavior across compute_2xk_layout in Tier 1 suite.""" +def test_layout_default_min_cols_15_in_tier1(): + """Verify default min_cols=15 behavior across compute_2xk_layout in Tier 1 suite.""" from lib_py.layout import compute_2xk_layout - # 1. 2 panes in 80 col width (80 // 2 = 40 == min_cols 40) -> splits right cleanly - payload_80 = { + # 1. 2 panes in 30 col width (30 // 2 = 15 == min_cols 15) -> splits right cleanly + payload_30 = { "result": { "panes": [ - {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 80, "height": 40}}, - {"pane_id": "p2", "rect": {"x": 0, "y": 40, "width": 80, "height": 40}} + {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 30, "height": 20}}, + {"pane_id": "p2", "rect": {"x": 0, "y": 20, "width": 30, "height": 20}} ] } } - decision = compute_2xk_layout(payload_80) + decision = compute_2xk_layout(payload_30) assert decision.direction == "right" assert not decision.is_overflow assert decision.reason == "new_column_right" - # 2. 2 panes in 79 col width (79 // 2 = 39 < min_cols 40) -> column_width_overflow - payload_79 = { + # 2. 2 panes in 29 col width (29 // 2 = 14 < min_cols 15) -> column_width_overflow + payload_29 = { "result": { "panes": [ - {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 79, "height": 40}}, - {"pane_id": "p2", "rect": {"x": 0, "y": 40, "width": 79, "height": 40}} + {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 29, "height": 20}}, + {"pane_id": "p2", "rect": {"x": 0, "y": 20, "width": 29, "height": 20}} ] } } - decision_overflow = compute_2xk_layout(payload_79) + decision_overflow = compute_2xk_layout(payload_29) assert decision_overflow.direction == "overflow" assert decision_overflow.is_overflow assert decision_overflow.reason == "column_width_overflow" -def test_layout_single_workspace_90_100_cols_tiling_tier1(): - """Verify 3-4 agents tiling in standard 90-100 col terminal windows within a single workspace.""" +def test_layout_single_workspace_54x23_compact_tiling_tier1(): + """Verify 3-4 agents tiling in standard 80x24 (54x23 content) terminal windows within a single workspace.""" from lib_py.layout import compute_2xk_layout - for total_w in [90, 100]: - half_w = total_w // 2 + # Step 1: 1 pane -> 2 panes (split down without height constraint) + p1 = {"result": {"panes": [{"pane_id": "p1", "rect": {"x": 26, "y": 1, "width": 54, "height": 23}}]}} + d1 = compute_2xk_layout(p1) + assert d1.direction == "down" + assert d1.target_pane_id == "p1" + assert not d1.is_overflow - # Step 1: 1 pane -> 2 panes (split down) - p1 = {"result": {"panes": [{"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": total_w, "height": 40}}]}} - d1 = compute_2xk_layout(p1) - assert d1.direction == "down" - assert d1.target_pane_id == "p1" - assert not d1.is_overflow + # Step 2: 2 panes -> 3 panes (split right to open 2nd column) + p2 = {"result": {"panes": [ + {"pane_id": "p1", "rect": {"x": 26, "y": 1, "width": 54, "height": 11}}, + {"pane_id": "p2", "rect": {"x": 26, "y": 12, "width": 54, "height": 11}} + ]}} + d2 = compute_2xk_layout(p2) + assert d2.direction == "right" + assert d2.target_pane_id == "p1" + assert not d2.is_overflow - # Step 2: 2 panes -> 3 panes (split right to open 2nd column) - p2 = {"result": {"panes": [ - {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": total_w, "height": 20}}, - {"pane_id": "p2", "rect": {"x": 0, "y": 20, "width": total_w, "height": 20}} - ]}} - d2 = compute_2xk_layout(p2) - assert d2.direction == "right" - assert d2.target_pane_id == "p1" - assert not d2.is_overflow + # Step 3: 3 panes -> 4 panes (split singleton 2nd column down) + p3 = {"result": {"panes": [ + {"pane_id": "p1", "rect": {"x": 26, "y": 1, "width": 27, "height": 11}}, + {"pane_id": "p2", "rect": {"x": 26, "y": 12, "width": 27, "height": 11}}, + {"pane_id": "p3", "rect": {"x": 53, "y": 1, "width": 27, "height": 23}} + ]}} + d3 = compute_2xk_layout(p3) + assert d3.direction == "down" + assert d3.target_pane_id == "p3" + assert not d3.is_overflow - # Step 3: 3 panes -> 4 panes (split singleton 2nd column down) - p3 = {"result": {"panes": [ - {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": half_w, "height": 20}}, - {"pane_id": "p2", "rect": {"x": 0, "y": 20, "width": half_w, "height": 20}}, - {"pane_id": "p3", "rect": {"x": half_w, "y": 0, "width": half_w, "height": 40}} - ]}} - d3 = compute_2xk_layout(p3) - assert d3.direction == "down" - assert d3.target_pane_id == "p3" - assert not d3.is_overflow - - # Step 4: 4 panes (2x2 complete) -> 5th agent overflows to fresh workspace - p4 = {"result": {"panes": [ - {"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": half_w, "height": 20}}, - {"pane_id": "p2", "rect": {"x": 0, "y": 20, "width": half_w, "height": 20}}, - {"pane_id": "p3", "rect": {"x": half_w, "y": 0, "width": half_w, "height": 20}}, - {"pane_id": "p4", "rect": {"x": half_w, "y": 20, "width": half_w, "height": 20}} - ]}} - d4 = compute_2xk_layout(p4) - assert d4.direction == "overflow" - assert d4.is_overflow - assert d4.reason == "column_width_overflow" + # Step 4: 4 panes (2x2 complete) -> 5th agent overflows to fresh workspace (27 // 2 = 13 < 15) + p4 = {"result": {"panes": [ + {"pane_id": "p1", "rect": {"x": 26, "y": 1, "width": 27, "height": 11}}, + {"pane_id": "p2", "rect": {"x": 26, "y": 12, "width": 27, "height": 11}}, + {"pane_id": "p3", "rect": {"x": 53, "y": 1, "width": 27, "height": 11}}, + {"pane_id": "p4", "rect": {"x": 53, "y": 12, "width": 27, "height": 11}} + ]}} + d4 = compute_2xk_layout(p4) + assert d4.direction == "overflow" + assert d4.is_overflow + assert d4.reason == "column_width_overflow"