feat(layout): reduce default MAM_MIN_PANE_COLS to 40 for single workspace 2xK multi-pane tiling
This commit is contained in:
@@ -0,0 +1,169 @@
|
|||||||
|
# 🔍 Cross Code Review — Job ddc8d9f1
|
||||||
|
|
||||||
|
- **Reviewer**: `planner-reviewer-claude-01` (role: `planner,reviewer`)
|
||||||
|
- **Target**: 2xK grid layout engine — `MAM_MIN_PANE_COLS` 기본값 60 → 40 및 단일 워크스페이스 다중 페인 타일링
|
||||||
|
- **Reviewed files**: `.agents/skills/lib.sh`, `.agents/skills/lib_py/layout.py`, `.mam.env.example`, `tests/test_layout.py`, `tests/test_tier1_unit.py`, `tests/test_a4_adapter_contract.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 검증 범위 고지 (Verification Scope Disclaimer)
|
||||||
|
|
||||||
|
**본 세션에서 `pytest` 스위트를 실행하지 못했습니다.** 실행 시도가 중단되어(사용자 거부) 브리프 4번 요구사항 *"Ensure full pytest test suite passes"* 는 **실측으로 확인되지 않았습니다.**
|
||||||
|
|
||||||
|
따라서 아래 판정은 다음 범위로 한정됩니다:
|
||||||
|
- ✅ 소스 정적 분석 (`layout.py` 전체 로직 판독)
|
||||||
|
- ✅ 신규 테스트의 **모든 단언을 엔진 분기에 대입한 수동 트레이스**
|
||||||
|
- ✅ `grep` 기반 상수 드리프트 전수 조사
|
||||||
|
- ❌ **테스트 실행 결과 (미수행)**
|
||||||
|
|
||||||
|
수치·분기 추적은 결정론적 정수 연산이라 수동 검증의 신뢰도가 높지만, 실행 확인은 별도로 이루어져야 합니다. §5에 잔여 항목을 명시했습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 변경 요약
|
||||||
|
|
||||||
|
| 위치 | 변경 | 판정 |
|
||||||
|
|---|---|---|
|
||||||
|
| `lib_py/layout.py:73` | `compute_2xk_layout(min_cols=60)` → `40` | ✅ |
|
||||||
|
| `lib_py/layout.py:201` | `--min-cols` 기본값 `_env_int(..., default=60)` → `40` | ✅ |
|
||||||
|
| `lib_py/layout.py:179` | 독스트링 `60 default` → `40 default` | ✅ |
|
||||||
|
| `lib.sh:432` | `${MAM_MIN_PANE_COLS:-60}` → `:-40` | ✅ |
|
||||||
|
| `.mam.env.example:132-133` | 주석 `#default: 60` 및 예시 `=60` → `40` | ✅ |
|
||||||
|
| `tests/test_layout.py:290` | lib.sh 소스 스니펫 가드 문자열 동기화 | ✅ |
|
||||||
|
|
||||||
|
**3중 기본값 동기화 확인**: 이 코드베이스는 동일한 기본값을 **세 곳**(shell 파라미터 확장, Python 시그니처, Python argparse)에 중복 보유합니다. 세 곳 모두 40으로 일치하며 `.mam.env.example` 문서값까지 4중 일치합니다. 드리프트 없음.
|
||||||
|
|
||||||
|
> **참고**: `lib.sh:432` 는 항상 `--min-cols` 를 **명시 전달**하므로 실운영 경로에서 `layout.py:201` 의 argparse 기본값은 도달하지 않습니다. 201번 줄은 CLI 직접 호출·테스트 경로용 fallback 입니다. 두 값이 어긋나도 즉시 드러나지 않는 구조이므로 §4에 가드 제안을 남깁니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 로직 정합성 — 신규 테스트 수동 트레이스
|
||||||
|
|
||||||
|
`compute_2xk_layout` 의 분기를 신규 단언에 그대로 대입해 전건 검증했습니다. 폭 판정은 `layout.py:169` 의 `width // 2 < min_cols` 단일 게이트입니다.
|
||||||
|
|
||||||
|
### 2.1 경계값 (80 / 79 cols)
|
||||||
|
|
||||||
|
| 입력 | 계산 | 도달 분기 | 기대 | 실제 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 2페인 × w=80 (x=0 동일열) | `80 // 2 = 40`, `40 < 40` = False | `:172 new_column_right` | `right`, not overflow | ✅ 일치 |
|
||||||
|
| 2페인 × w=79 | `79 // 2 = 39`, `39 < 40` = True | `:170 column_width_overflow` | `overflow` | ✅ 일치 |
|
||||||
|
|
||||||
|
**80이 정확한 하한**임이 확인됩니다(`>= 80` 에서 분할 가능). 브리프 2번 요구사항 *"width >= 80 에서 조기 overflow 금지"* 는 상수 변경만으로 산술적으로 충족되며, 별도 분기 추가가 불필요합니다 — **엔진 로직 무변경은 올바른 판단**입니다. 불필요한 특수 케이스를 넣지 않은 점을 긍정 평가합니다.
|
||||||
|
|
||||||
|
### 2.2 90 / 100 col 단일 워크스페이스 타일링 (1→2→3→4→overflow)
|
||||||
|
|
||||||
|
`total_w ∈ {90, 100}`, `half_w = total_w // 2 ∈ {45, 50}` 기준 전 단계 추적:
|
||||||
|
|
||||||
|
| 단계 | 입력 형상 | 판정 경로 | 결과 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1→2 | 1페인 `w×40` | `:94` `40//2 = 20 >= min_rows 20` → False(제약 아님) → `:102` | `down` / `single_pane_split_down`, target `p1` ✅ |
|
||||||
|
| 2→3 | 2페인 x=0 단일열 | singleton 없음 → `:169` `45//2=22`? **아니오** — 이 시점 페인 폭은 아직 `total_w`(90/100) → `90//2=45 >= 40` | `right` / `new_column_right`, target `p1`(`columns[-1][0]`) ✅ |
|
||||||
|
| 3→4 | `[p1,p2]` @x=0, `[p3]` @x=half_w | `:148-154` singleton 열 `[p3]` 탐지 → 높이 `40//2=20 >= 20` | `down` / `fill_singleton_column`, target `p3` ✅ |
|
||||||
|
| 4→5 | 2열 × 2페인 완성 | singleton 없음, `max_columns=None` → `:169` `45//2=22 < 40` (100col: `50//2=25 < 40`) | `overflow` / `column_width_overflow` ✅ |
|
||||||
|
|
||||||
|
**핵심 확인 사항 2건**:
|
||||||
|
1. **1→2 단계의 높이 경계**: `height=40` 에서 `40 // 2 = 20`, `min_rows=20` 과 **같음**. `:94` 조건은 `< min_rows` 이므로 False → 정상적으로 `down` 진입. `<=` 였다면 오분기했을 지점으로, 테스트가 이 경계를 정확히 짚고 있습니다.
|
||||||
|
2. **4페인에서의 의도적 overflow**: `min_cols=40` 에서 90~100col 워크스페이스는 **최대 4에이전트**가 상한이며 5번째는 새 워크스페이스로 넘어갑니다. 브리프 목표(*"3-4 agents in ~100-col terminal"*)와 정확히 부합하고, 테스트가 이 상한을 명시적으로 고정하고 있어 향후 회귀 시 즉시 검출됩니다. ✅
|
||||||
|
|
||||||
|
### 2.3 컬럼 그룹핑 정합성
|
||||||
|
|
||||||
|
`:129-139` 의 x좌표 퍼지 그룹핑(임계 2col)에 신규 픽스처 대입 시:
|
||||||
|
- 90col: x ∈ {0, 45} → `|0-45| = 45 > 2` → 2개 열로 정확히 분리 ✅
|
||||||
|
- 100col: x ∈ {0, 50} → 동일 ✅
|
||||||
|
|
||||||
|
퍼지 임계값 2와 충돌하는 좌표가 없어 그룹핑 오분류 위험이 없습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 회귀 영향 분석 (유실 관점)
|
||||||
|
|
||||||
|
기본값 변경은 **기본값에 의존하는 기존 테스트**에만 파급됩니다. 전수 조사 결과:
|
||||||
|
|
||||||
|
| 기존 테스트 | 기본값 의존 여부 | 영향 |
|
||||||
|
|---|---|---|
|
||||||
|
| `test_layout.py:28~262` (8건) | `min_cols=60` **명시 전달** | 영향 없음 ✅ |
|
||||||
|
| `test_layout.py:137,191` | `min_cols=30` 명시 | 영향 없음 ✅ |
|
||||||
|
| `test_layout.py:207` (subprocess) | `--min-cols 60` 명시 | 영향 없음 ✅ |
|
||||||
|
| `test_layout.py:358,374` | `--min-cols 30` 명시 | 영향 없음 ✅ |
|
||||||
|
| `test_j1_env_zero_min_cols_matches_flag_zero` | `_ZERO_TRAP` 기본값 실행 포함 | **영향 검토 필요 → 아래** |
|
||||||
|
| `test_j1b_invalid_alias_does_not_shadow...` | 기본값 실행 비교 | 동일 ✅ |
|
||||||
|
|
||||||
|
**J-1 계열 정밀 검토** (`test_layout.py:432` 주석 기준 `_ZERO_TRAP` = 단일 페인 `50×30`):
|
||||||
|
- `height // 2 = 15 < min_rows 20` → `:94` 제약 분기 진입
|
||||||
|
- `width // 2 = 25` 를 `min_cols` 와 비교: 기존 `25 < 60` → overflow / 신규 `25 < 40` → **overflow (동일)**
|
||||||
|
- 즉 기본값이 60이든 40이든 `_ZERO_TRAP` 의 결과는 `single_pane_overflow` 로 불변. **J-1/C-2 불변식 보존 확인** ✅
|
||||||
|
|
||||||
|
또한 J-1b는 "기본값 실행 == 기본값 실행" 형태의 자기참조 비교라 기본값 자체와 무관하게 성립합니다.
|
||||||
|
|
||||||
|
`test_layout.py:290` 의 lib.sh 소스 스니펫 가드는 **문자열 완전 일치** 검사이므로 `lib.sh:432` 와 함께 갱신되지 않았다면 즉시 실패했을 항목입니다. 양쪽 모두 `:-40` 으로 동기화되어 있음을 대조 확인했습니다 ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 지적 사항 (모두 비차단 / Non-blocking)
|
||||||
|
|
||||||
|
차단 결함(P0/P1)은 발견되지 않았습니다. 아래는 개선 권고입니다.
|
||||||
|
|
||||||
|
### 🟡 N-1 (P3) — `test_herdr_shim_contract.py:100` 의 `MAM_MIN_PANE_COLS=60` 미검토
|
||||||
|
`tests/test_herdr_shim_contract.py:92,100` 의 H-13 케이스가 `export MAM_MIN_PANE_COLS=60` 을 사용합니다. 이는 **환경변수 오버라이드 동작 자체**를 검증하는 케이스이므로 기본값 변경과 논리적으로 독립이며(명시 오버라이드 경로), 정상 통과가 예상됩니다. 다만 파일 본문을 열람하지 못해 **단언 내용까지는 확인하지 못했습니다.**
|
||||||
|
|
||||||
|
**개선 방향**: 이 테스트가 "60이 아닌 값이 적용됨"을 검증하는 의도라면, 이제 기본값 40과 오버라이드 값 60이 명확히 구분되어 오히려 대조가 선명해집니다. 확인만 권고합니다.
|
||||||
|
|
||||||
|
### 🟡 N-2 (P3) — `IMPROVEMENTS.md:49` 의 `min_cols=60` 잔존
|
||||||
|
```
|
||||||
|
IMPROVEMENTS.md:49: ... 해상도 오버플로 가드(`min_cols=60`, `min_rows=20`) ...
|
||||||
|
```
|
||||||
|
해당 줄은 **엔진 최초 도입 시점을 기록한 변경 이력**이므로 당시 값 60을 남기는 것이 이력 문서로서는 정확합니다. 다만 현재 이 저장소에서 **60을 기본값이라 서술하는 유일한 문서**가 되었습니다.
|
||||||
|
|
||||||
|
**개선 방향 (택1)**: (a) 그대로 두되 이번 변경을 `IMPROVEMENTS.md` 신규 항목으로 추가하여 60→40 전환 이력을 잇는다 — **권장**. (b) 해당 줄에 `(현행 40, 잡 ddc8d9f1에서 변경)` 각주를 붙인다. 이력 문서를 소급 수정하는 방식은 권장하지 않습니다.
|
||||||
|
|
||||||
|
### 🟡 N-3 (P3) — 기본값 4중 중복에 대한 파리티 가드 부재
|
||||||
|
동일 상수가 `lib.sh:432` / `layout.py:73` / `layout.py:201` / `.mam.env.example:133` 4곳에 문자열로 중복 존재합니다. `test_layout.py:290` 이 lib.sh↔테스트 스니펫 쌍만 고정할 뿐, **`layout.py:73` 시그니처 기본값과 `layout.py:201` argparse 기본값의 일치는 어떤 테스트도 강제하지 않습니다.** 두 값이 어긋나면 CLI 경로와 라이브러리 임포트 경로가 조용히 갈라집니다.
|
||||||
|
|
||||||
|
**개선 방향 (구체안)**:
|
||||||
|
```python
|
||||||
|
# tests/test_layout.py
|
||||||
|
import inspect
|
||||||
|
from lib_py.layout import compute_2xk_layout
|
||||||
|
|
||||||
|
def test_default_min_cols_parity_across_entrypoints():
|
||||||
|
"""시그니처 기본값 == argparse 기본값 == lib.sh fallback."""
|
||||||
|
sig_default = inspect.signature(compute_2xk_layout).parameters["min_cols"].default
|
||||||
|
assert sig_default == 40
|
||||||
|
# argparse 경로: env 미설정 시 동일 결정을 내야 함
|
||||||
|
assert _run_layout(_ZERO_TRAP) == _run_layout(_ZERO_TRAP, ("--min-cols", str(sig_default)))
|
||||||
|
# lib.sh fallback 문자열
|
||||||
|
lib_sh = (REPO_ROOT / ".agents/skills/lib.sh").read_text()
|
||||||
|
assert f'${{MAM_MIN_PANE_COLS:-{sig_default}}}' in lib_sh
|
||||||
|
```
|
||||||
|
이는 이전 잡에서 `ready_tokens` 가 `lib.sh`/`claude.py` 양쪽에 중복된 것과 **동일 유형의 구조적 취약점**이며, 같은 처방이 적용됩니다. 별도 잡으로 분리해도 무방합니다.
|
||||||
|
|
||||||
|
### 🟢 N-4 (P4) — 워킹트리 위생
|
||||||
|
`git status` 에 `m nats-docker` (서브모듈 dirty, `5db38da...-dirty`) 가 포함되어 있습니다. 본 변경과 무관한 오염이며 커밋 전 정리를 권고합니다. 또한 `tests/test_layout.py` 말미에 빈 줄 3개(`+++`)가 추가되어 있어 PEP8 관점의 사소한 정리 여지가 있습니다. 기능 영향 없음.
|
||||||
|
|
||||||
|
### ℹ️ N-5 (정보) — 누적 diff 내 `test_a4_adapter_contract.py` 변경
|
||||||
|
`ready_tokens` 에 `Claude Code|Opus|Sonnet|Haiku` 를 추가한 직전 잡의 변경분이 누적 diff에 포함되어 있습니다. 계약 테스트의 기대값이 `lib.sh` / `claude.py` 양쪽 구현과 3자 일치함을 대조 확인했습니다 ✅ (본 잡 범위 외)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 잔여 검증 항목 (Outstanding)
|
||||||
|
|
||||||
|
| # | 항목 | 상태 |
|
||||||
|
|---|---|---|
|
||||||
|
| V-1 | `pytest tests/ -q` 전체 통과 | ❌ **미수행** — 본 세션에서 실행 중단됨 |
|
||||||
|
| V-2 | `test_herdr_shim_contract.py` H-13 단언 내용 | ⚠️ 미열람 (영향 없음으로 추정, N-1) |
|
||||||
|
|
||||||
|
**V-1은 머지 전 반드시 실측되어야 합니다.** 정적 분석상 실패를 유발할 요인은 발견하지 못했으나(§3 회귀 영향 전무), 이는 예측이지 관측이 아닙니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 총평
|
||||||
|
|
||||||
|
변경은 **상수 1개의 값 조정과 그에 대한 4중 동기화**라는 최소 표면적을 정확히 지켰습니다. 엔진 분기 로직을 건드리지 않고 브리프의 4개 요구사항을 충족한 점, 특히 요구사항 2를 위해 불필요한 특수 분기를 추가하지 않고 산술로 해소한 점이 설계적으로 건전합니다.
|
||||||
|
|
||||||
|
신규 테스트는 단순 happy-path에 머물지 않고 **80/79 경계**, **height 40//2 == min_rows 20 동등 경계**, **4페인 상한 후 overflow** 라는 세 개의 실질적 경계를 고정합니다. 기존 J-1/C-2 불변식도 보존됩니다.
|
||||||
|
|
||||||
|
지적 사항 4건은 모두 P3 이하이며 문서 이력·테스트 위생·워킹트리 정리 범주로, 어느 것도 현재 동작을 해치거나 결함을 은폐하지 않습니다. 설계 변경이나 재계획이 필요한 사안은 없습니다.
|
||||||
|
|
||||||
|
**단, 본 PASS는 §5 V-1(전체 테스트 실행) 이 별도로 확인된다는 전제 위에 성립합니다.** 정적 검토 범위에서는 차단 사유가 없습니다.
|
||||||
|
|
||||||
|
[VERDICT: PASS]
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
# Review Report — Job bb360685
|
||||||
|
|
||||||
|
- **Reviewer**: cline (herdr session `reviewer-cline-01`, role: reviewer)
|
||||||
|
- **Job ID**: bb360685
|
||||||
|
- **Reviewed branch**: `refactor` (changes unstaged in working tree)
|
||||||
|
- **Scope**: Cross code review (lint / operability / drift) of the diff for
|
||||||
|
"Improve 2xK grid layout engine and prevent premature workspace overflow".
|
||||||
|
- **Diff stat**: 7 files, +236 / -9 (plus a dirty submodule).
|
||||||
|
|
||||||
|
## 1. Change Inventory
|
||||||
|
|
||||||
|
| File | Change | Category |
|
||||||
|
|------|--------|----------|
|
||||||
|
| `.agents/skills/lib_py/layout.py` | `compute_2xk_layout` default `min_cols` 60→40; CLI `--min-cols` default 60→40; `_env_int` docstring 60→40 | Core logic (task goal #1, #2) |
|
||||||
|
| `.agents/skills/lib.sh:432` | Fallback `${MAM_MIN_PANE_COLS:-60}` → `:-40` | Core logic (task goal #1) |
|
||||||
|
| `.mam.env.example` | Documented default `MAM_MIN_PANE_COLS` 60→40 | Config/docs (task goal #1) |
|
||||||
|
| `tests/test_layout.py` | Updated lib.sh snippet expectation (`:-60`→`:-40`); J-1 docstring 60→40; +4 new tests (default-40, 80-col boundary, 90/100-col tiling) | Tests (task goal #3) |
|
||||||
|
| `tests/test_tier1_unit.py` | +2 new Tier-1 tests (default-40, 90/100-col tiling) | Tests (task goal #3) |
|
||||||
|
| `tests/test_a4_adapter_contract.py` | Widened `claude` `ready_tokens` regex (`+|Claude Code|Opus|Sonnet|Haiku`) | **Unrelated to layout task** |
|
||||||
|
| `nats-docker` (submodule) | `PRIVATE_SERVER.md` modified → submodule marked `-dirty` | **Stray / drift, unrelated** |
|
||||||
|
|
||||||
|
## 2. Lint / Syntax
|
||||||
|
|
||||||
|
- `python -m py_compile lib_py/layout.py` → **OK**
|
||||||
|
- `bash -n .agents/skills/lib.sh` → **OK**
|
||||||
|
- No leftover `MAM_MIN_PANE_COLS:-60` fallbacks anywhere in `.sh`/`.py`. The only
|
||||||
|
remaining `60` references are *explicit* `min_cols=60` arguments in pre-existing
|
||||||
|
layout tests (legitimate — they exercise the 60 configuration, not the default)
|
||||||
|
and one contrast docstring line. The default is consistently 40 across all three
|
||||||
|
authoritative sites (function signature, CLI argparse, lib.sh fallback) and the
|
||||||
|
env example. **No orphans.**
|
||||||
|
|
||||||
|
## 3. Operability — Goal-by-Goal Verification
|
||||||
|
|
||||||
|
### Goal #1 — Default 60→40 to enable 3-4 agents in ~100-col windows
|
||||||
|
- Verified all three default sites are 40 and consistent.
|
||||||
|
- Practical effect: with `min_cols=60`, a 100-col pane split right yields 50-col
|
||||||
|
halves → `50 < 60` → immediate `column_width_overflow` (could not even open a
|
||||||
|
2nd column). With `min_cols=40`, `50 >= 40` → 2 columns (4 panes) fit before
|
||||||
|
overflow. The change materially enables 3-4 agents per ~100-col workspace, not
|
||||||
|
merely cosmetic. ✓
|
||||||
|
|
||||||
|
### Goal #2 — Clean 2-column split at width >= 80 without premature overflow
|
||||||
|
- Boundary predicate is `rightmost_top_pane.width // 2 < min_cols` (strict `<`).
|
||||||
|
At width 80: `80//2 = 40`, `40 < 40` is **False** → splits right (not overflow).
|
||||||
|
At width 79: `79//2 = 39`, `39 < 40` is **True** → `column_width_overflow`.
|
||||||
|
- CLI end-to-end confirmation (mirrors the `lib.sh` invocation path):
|
||||||
|
- 80-col payload → `right p1` ✓
|
||||||
|
- 79-col payload → `overflow p1` ✓
|
||||||
|
- The boundary is exactly at 80 and behaves as specified. ✓
|
||||||
|
|
||||||
|
### Goal #3 — Updated unit tests assert default 40 & 90-100 col tiling
|
||||||
|
- New tests present in both suites:
|
||||||
|
- `test_default_min_cols_is_40` / `test_layout_default_min_cols_40_in_tier1`
|
||||||
|
- `test_80_col_2_column_splitting_boundary`
|
||||||
|
- `test_90_col_single_workspace_multi_pane_tiling` /
|
||||||
|
`test_100_col_single_workspace_multi_pane_tiling` /
|
||||||
|
`test_layout_single_workspace_90_100_cols_tiling_tier1`
|
||||||
|
- Tiling tests verify the full 1→2→3→4→(5th overflow) progression with correct
|
||||||
|
target panes and `column_width_overflow` reason. Traced the column-grouping
|
||||||
|
logic: singleton-column fill at step 3→4 and width-constrained overflow at
|
||||||
|
step 4→5 are both reached correctly. ✓
|
||||||
|
|
||||||
|
### Goal #4 — Full pytest suite passes
|
||||||
|
- Targeted run of the three affected unit-test files:
|
||||||
|
`tests/test_layout.py tests/test_tier1_unit.py tests/test_a4_adapter_contract.py`
|
||||||
|
→ **99 passed in 10.97s**. ✓
|
||||||
|
- The complete `pytest tests/` suite could not be fully executed within this
|
||||||
|
review's time budget (integration tests are long-running), but every file
|
||||||
|
touched by the diff passes, and no unit-test regression is introduced.
|
||||||
|
|
||||||
|
## 4. Findings (advisory, non-blocking)
|
||||||
|
|
||||||
|
### F-1 (Hygiene/Scope): `test_a4_adapter_contract.py` change is out of scope
|
||||||
|
- The `claude` `ready_tokens` regex widening (`+|Claude Code|Opus|Sonnet|Haiku`)
|
||||||
|
is a correct, additive adapter fix and the contract test passes — but it is
|
||||||
|
**unrelated** to the 2xK layout-engine task. Bundling it into this diff blurs
|
||||||
|
traceability.
|
||||||
|
- **Direction**: Split into its own commit (`fix(adapter): broaden claude
|
||||||
|
ready_tokens`) before merging. No code change required for the layout work.
|
||||||
|
|
||||||
|
### F-2 (Drift): `nats-docker` submodule is dirty
|
||||||
|
- `git diff nats-docker` shows the submodule pointer unchanged but flagged
|
||||||
|
`-dirty`; `git -C nats-docker status` shows ` M PRIVATE_SERVER.md`.
|
||||||
|
- This is a stray local modification inside the submodule, unrelated to the
|
||||||
|
task, and risks being accidentally staged/committed alongside the layout
|
||||||
|
changes.
|
||||||
|
- **Direction**: Revert the stray edit (`git -C nats-docker checkout --
|
||||||
|
PRIVATE_SERVER.md`) or leave the submodule unstaged. Do not commit the
|
||||||
|
submodule pointer change with this work.
|
||||||
|
|
||||||
|
## 5. Summary
|
||||||
|
|
||||||
|
The core implementation correctly and consistently lowers the 2xK layout
|
||||||
|
`min_cols` default from 60 to 40 across `lib.sh`, `layout.py` (function +
|
||||||
|
CLI + docstring), and `.mam.env.example`, with matching, passing unit tests
|
||||||
|
covering the 80-col boundary and 90/100-col single-workspace tiling. Syntax
|
||||||
|
and CLI operability are verified. The two findings (F-1 out-of-scope adapter
|
||||||
|
regex, F-2 dirty submodule) are hygiene/drift items that do not affect the
|
||||||
|
layout engine's correctness or operability and require only commit
|
||||||
|
housekeeping, not a redesign.
|
||||||
|
|
||||||
|
[VERDICT: PASS]
|
||||||
@@ -429,7 +429,7 @@ except Exception:
|
|||||||
split_dir=""
|
split_dir=""
|
||||||
if [ -n "$sample_pane" ]; then
|
if [ -n "$sample_pane" ]; then
|
||||||
layout_raw=$(_real_herdr pane layout --pane "$sample_pane" 2>/dev/null || echo "")
|
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:-60}" --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:-40}" --min-rows "${MAM_MIN_PANE_ROWS:-20}" --sample-pane "$sample_pane" 2>/dev/null || echo "right $sample_pane")
|
||||||
split_dir="${split_dir:-right}"
|
split_dir="${split_dir:-right}"
|
||||||
sample_pane="${split_target:-$sample_pane}"
|
sample_pane="${split_target:-$sample_pane}"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ def extract_panes(data: Dict[str, Any]) -> List[PaneInfo]:
|
|||||||
|
|
||||||
def compute_2xk_layout(
|
def compute_2xk_layout(
|
||||||
data: Dict[str, Any],
|
data: Dict[str, Any],
|
||||||
min_cols: int = 60,
|
min_cols: int = 40,
|
||||||
min_rows: int = 20,
|
min_rows: int = 20,
|
||||||
max_columns: Optional[int] = None,
|
max_columns: Optional[int] = None,
|
||||||
default_anchor_id: Optional[str] = None
|
default_anchor_id: Optional[str] = None
|
||||||
@@ -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`.
|
"""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
|
`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 60 default).
|
legitimate 0 survives (MAM_MIN_PANE_COLS=0 means 0, not the 40 default).
|
||||||
|
|
||||||
An unparsable value is skipped rather than raised or treated as terminal: a
|
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
|
typo in an operator's shell must not take the whole layout call down (lib.sh
|
||||||
@@ -198,7 +198,7 @@ def _env_int(*names: str, default: Optional[int] = None) -> Optional[int]:
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description="Compute 2xK grid TUI layout split direction")
|
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=60))
|
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-rows", type=int, default=_env_int("MAM_MIN_ROWS", "MAM_MIN_PANE_ROWS", default=20))
|
||||||
parser.add_argument("--max-cols", type=int, default=_env_int("MAM_MAX_COLS", "MAM_MAX_PANE_COLS"))
|
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("--sample-pane", type=str, default=None)
|
||||||
|
|||||||
+2
-2
@@ -129,8 +129,8 @@
|
|||||||
# SKS_EMPTY_GIVEUP=3
|
# SKS_EMPTY_GIVEUP=3
|
||||||
|
|
||||||
# Minimum columns a pane must retain after a vertical split (2xK layout engine).
|
# Minimum columns a pane must retain after a vertical split (2xK layout engine).
|
||||||
#default: 60
|
#default: 40
|
||||||
# MAM_MIN_PANE_COLS=60
|
# MAM_MIN_PANE_COLS=40
|
||||||
|
|
||||||
# Minimum rows a pane must retain after a horizontal split (2xK layout engine).
|
# Minimum rows a pane must retain after a horizontal split (2xK layout engine).
|
||||||
#default: 20
|
#default: 20
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ def test_adapter_required_properties():
|
|||||||
getattr(base, prop)
|
getattr(base, prop)
|
||||||
|
|
||||||
expected = {
|
expected = {
|
||||||
'claude': ('Anthropic|Assistant|Chat|Welcome', '/exit', 'claude-code', ('session_id', 'session_jsonl', 'session_size_bytes', 'session_lines')),
|
'claude': ('Anthropic|Assistant|Chat|Welcome|Claude Code|Opus|Sonnet|Haiku', '/exit', 'claude-code', ('session_id', 'session_jsonl', 'session_size_bytes', 'session_lines')),
|
||||||
'agy': ('Antigravity', 'Exit', 'antigravity-cli', ('conversation_id', 'conversation_db', 'conversation_brain_dir')),
|
'agy': ('Antigravity', 'Exit', 'antigravity-cli', ('conversation_id', 'conversation_db', 'conversation_brain_dir')),
|
||||||
'hermes': ('Hermes', '/exit', 'hermes-agent', ('session_id',)),
|
'hermes': ('Hermes', '/exit', 'hermes-agent', ('session_id',)),
|
||||||
'cline': ('Cline|history|Chat|What can I do|slash commands', '/exit', 'cline-agent', ('session_id',)),
|
'cline': ('Cline|history|Chat|What can I do|slash commands', '/exit', 'cline-agent', ('session_id',)),
|
||||||
|
|||||||
+143
-2
@@ -287,7 +287,7 @@ split_dir=""
|
|||||||
# Exact snippet from lib.sh:429-435
|
# Exact snippet from lib.sh:429-435
|
||||||
if [ -n "$sample_pane" ]; then
|
if [ -n "$sample_pane" ]; then
|
||||||
layout_raw=$(_real_herdr pane layout --pane "$sample_pane" 2>/dev/null || echo "")
|
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:-60}}" --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:-40}}" --min-rows "${{MAM_MIN_PANE_ROWS:-20}}" --sample-pane "$sample_pane" 2>/dev/null || echo "right $sample_pane")
|
||||||
split_dir="${{split_dir:-right}}"
|
split_dir="${{split_dir:-right}}"
|
||||||
sample_pane="${{split_target:-$sample_pane}}"
|
sample_pane="${{split_target:-$sample_pane}}"
|
||||||
fi
|
fi
|
||||||
@@ -435,7 +435,7 @@ _ZERO_TRAP = {"result": {"panes": [
|
|||||||
|
|
||||||
|
|
||||||
def test_j1_env_zero_min_cols_matches_flag_zero():
|
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 60 default."""
|
"""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"))
|
flag = _run_layout(_ZERO_TRAP, ("--min-cols", "0"))
|
||||||
assert flag["direction"] == "right" and flag["reason"] == "single_pane_height_constrained"
|
assert flag["direction"] == "right" and flag["reason"] == "single_pane_height_constrained"
|
||||||
for var in ("MAM_MIN_COLS", "MAM_MIN_PANE_COLS"):
|
for var in ("MAM_MIN_COLS", "MAM_MIN_PANE_COLS"):
|
||||||
@@ -479,3 +479,144 @@ def test_j1b_invalid_alias_does_not_shadow_the_documented_var():
|
|||||||
"MAM_MIN_PANE_COLS": "bar"}) == _run_layout(_ZERO_TRAP)
|
"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.
|
||||||
|
"""
|
||||||
|
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}},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
decision = compute_2xk_layout(payload)
|
||||||
|
assert decision.direction == "right"
|
||||||
|
assert not decision.is_overflow
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
# 80 cols: 80 // 2 = 40 == min_cols(40) -> splits right
|
||||||
|
payload_80 = {
|
||||||
|
"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}},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d80 = compute_2xk_layout(payload_80)
|
||||||
|
assert d80.direction == "right"
|
||||||
|
assert not d80.is_overflow
|
||||||
|
|
||||||
|
# 79 cols: 79 // 2 = 39 < min_cols(40) -> overflow
|
||||||
|
payload_79 = {
|
||||||
|
"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}},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d79 = compute_2xk_layout(payload_79)
|
||||||
|
assert d79.direction == "overflow"
|
||||||
|
assert d79.is_overflow
|
||||||
|
assert d79.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
|
||||||
|
"""
|
||||||
|
# 1 -> 2
|
||||||
|
p1_layout = {"result": {"panes": [{"pane_id": "p1", "rect": {"x": 0, "y": 0, "width": 90, "height": 40}}]}}
|
||||||
|
d1 = compute_2xk_layout(p1_layout)
|
||||||
|
assert d1.direction == "down"
|
||||||
|
assert d1.target_pane_id == "p1"
|
||||||
|
|
||||||
|
# 2 -> 3
|
||||||
|
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}},
|
||||||
|
]}}
|
||||||
|
d2 = compute_2xk_layout(p2_layout)
|
||||||
|
assert d2.direction == "right"
|
||||||
|
assert d2.target_pane_id == "p1"
|
||||||
|
assert not d2.is_overflow
|
||||||
|
|
||||||
|
# 3 -> 4
|
||||||
|
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}},
|
||||||
|
]}}
|
||||||
|
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": 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}},
|
||||||
|
]}}
|
||||||
|
d4 = compute_2xk_layout(p4_layout)
|
||||||
|
assert d4.direction == "overflow"
|
||||||
|
assert d4.is_overflow
|
||||||
|
assert d4.reason == "column_width_overflow"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -864,5 +864,91 @@ def test_lib_sh_new_session_passes_mam_ws_label(mam_sandbox):
|
|||||||
assert '_real_herdr workspace rename "$existing_ws" "$MAM_WS_LABEL"' in content
|
assert '_real_herdr workspace rename "$existing_ws" "$MAM_WS_LABEL"' in content
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# FEATURE: 2xK Grid Layout Engine (min_cols=40 & 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."""
|
||||||
|
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 = {
|
||||||
|
"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}}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
decision = compute_2xk_layout(payload_80)
|
||||||
|
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 = {
|
||||||
|
"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}}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
decision_overflow = compute_2xk_layout(payload_79)
|
||||||
|
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."""
|
||||||
|
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)
|
||||||
|
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": 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": 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"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user