Compare commits
2
Commits
d1efce2971
...
76151c76b2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76151c76b2 | ||
|
|
f3ac68f36d |
@@ -0,0 +1,229 @@
|
||||
# 🗺️ Multi-Agent Mux (MAM): New Agent Types Extension Roadmap & Architecture Analysis
|
||||
|
||||
- **Author**: Creator Agent (`creator-agy-01`)
|
||||
- **Date**: 2026-08-26
|
||||
- **Status**: Complete / Ready for Review
|
||||
- **Document Purpose**: Provide a comprehensive architectural review of recent layout enhancements and an actionable implementation blueprint for onboarding new agent backends (e.g., `codex`, `grok-build`, `opencode`, `kimi`, `cursor`).
|
||||
|
||||
---
|
||||
|
||||
## 1. Review of Recent Commit Changes
|
||||
|
||||
### 1.1 Layout Engine Refinements (`lib_py/layout.py` & `lib.sh:432`)
|
||||
- **Constraint Relaxation**:
|
||||
- `MAM_MIN_PANE_COLS` default was lowered from `60` -> `40` -> `15`. This allows dense multi-pane tiling in compact terminal environments (e.g. 80x24 standard terminals with 54x23 content areas) without prematurely triggering `column_width_overflow`.
|
||||
- `MAM_MIN_PANE_ROWS` default was set to `0`. Vertical height constraints were removed because terminal panes support native scrollback buffers, allowing 2-row splitting even in constrained heights.
|
||||
- **Single-Workspace 2×K Multi-Pane Tiling**:
|
||||
- Standard viewports (e.g. 54×23 content area in 80×24 window, or 90–100 col windows) cleanly progress through:
|
||||
1. **1 Pane** $\rightarrow$ split down $\rightarrow$ **2 Panes** (top/bottom)
|
||||
2. **2 Panes** $\rightarrow$ split right $\rightarrow$ **3 Panes** (starts column 2)
|
||||
3. **3 Panes** $\rightarrow$ fill singleton down $\rightarrow$ **4 Panes** (2×2 balanced grid)
|
||||
4. **4 Panes** $\rightarrow$ 5th pane overflows to a dedicated fresh workspace when column width $< 15$ cols.
|
||||
- **Verification & Test Suite**:
|
||||
- `tests/test_layout.py` (27 unit tests) and `tests/test_tier1_unit.py` (59 unit tests) assert default `min_cols=15` and verify boundary conditions (e.g. 30 cols split vs 29 cols overflow; 54×23 compact window tiling).
|
||||
- All 371 tests across the 18 test suites pass with 0 failures.
|
||||
|
||||
---
|
||||
|
||||
## 2. Architecture & Contract for Adding New Agent Types
|
||||
|
||||
MAM's architecture isolates agent-specific quirks into clean, pluggable Python adapters backed by a shared CLI bridge (`lib_py.agents`).
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ Herdr Runtime │
|
||||
│ (herdr agent start <name> --kind <kind> -- <cmd>) │
|
||||
└────────────────────────────┬─────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ lib.sh (Shell Runtime) │
|
||||
│ • resolve_agent_type_from_registry() │
|
||||
│ • wait_for_tui_ready() (via MAM_READY_TOKENS) │
|
||||
│ • send_keys_safe() / stop_session.sh │
|
||||
└────────────────────────────┬─────────────────────────────┘
|
||||
│
|
||||
eval $(python -m lib_py.agents facts <agent>)
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ lib_py.agents Framework │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ BaseAgentAdapter │ │
|
||||
│ │ • name, own_key, ready_tokens, exit_key, delegate_agent_key, identity_cache_fields │ │
|
||||
│ │ • input_prompt, input_placeholder, input_rule_pattern │ │
|
||||
│ │ • artifact_path(), verify_artifact(), purge_artifacts() │ │
|
||||
│ │ • spawn_spec(), resume_spec(), auth_ok(), discover() │ │
|
||||
│ └────────────────────────────────────────────────────────────────┬─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────────────────┬───────────────────────────────┼───────────────────────────────┬─────────────────────┐ │
|
||||
│ ▼ ▼ ▼ ▼ ▼ │
|
||||
│ ClaudeAgentAdapter AgyAgentAdapter ClineAgentAdapter HermesAgentAdapter [NewAgentAdapter] │
|
||||
│ (Claude Code) (Antigravity) (Cline) (Hermes) (Codex / OpenCode / ...) │
|
||||
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Detailed Task Breakdown per Component
|
||||
|
||||
To add a new agent type `<agent>` (e.g. `opencode`, `codex`, `kimi`, `grok`, `cursor`), the following 5 layers must be implemented:
|
||||
|
||||
### Step 1: Implement the Agent Adapter (`.agents/skills/lib_py/agents/adapters/<agent>.py`)
|
||||
Create `<Agent>AgentAdapter` inheriting from `BaseAgentAdapter`:
|
||||
|
||||
```python
|
||||
class NewAgentAdapter(BaseAgentAdapter):
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return 'newagent'
|
||||
|
||||
@property
|
||||
def own_key(self) -> str:
|
||||
# YAML state key stored under herdr_sessions[]
|
||||
return 'newagent_session_id_own' # or 'newagent_conversation_id_own'
|
||||
|
||||
@property
|
||||
def ready_tokens(self) -> str:
|
||||
# Regex matching banner / prompt when TUI is fully loaded and ready for input
|
||||
return 'NewAgent|Assistant|Chat|Welcome'
|
||||
|
||||
@property
|
||||
def exit_key(self) -> str:
|
||||
# Graceful exit command sent to TUI (e.g., '/exit', 'Exit', 'quit', or ':q')
|
||||
return '/exit'
|
||||
|
||||
@property
|
||||
def delegate_agent_key(self) -> str:
|
||||
# Canonical target identifier for delegate-job MQTT payloads
|
||||
return 'newagent-cli'
|
||||
|
||||
@property
|
||||
def identity_cache_fields(self) -> tuple:
|
||||
return ('session_id',)
|
||||
|
||||
# Optional TUI Input Region Delimiters (for send_keys_safe visual parsing)
|
||||
@property
|
||||
def input_prompt(self) -> str:
|
||||
return '❯'
|
||||
|
||||
@property
|
||||
def input_placeholder(self) -> str:
|
||||
return ''
|
||||
|
||||
@property
|
||||
def input_rule_pattern(self) -> str:
|
||||
return '─{10,}'
|
||||
|
||||
# Artifact & Session Tracking Lifecycle
|
||||
def artifact_path(self, uuid: str, ctx: DiscoveryContext) -> str:
|
||||
return f"{ctx.home_dir}/.newagent/sessions/{uuid}.json"
|
||||
|
||||
def verify_artifact(self, uuid: str, ctx: DiscoveryContext) -> bool:
|
||||
path = self.artifact_path(uuid, ctx)
|
||||
if not os.path.exists(path):
|
||||
return False
|
||||
if ctx.epoch and os.path.getmtime(path) < ctx.epoch:
|
||||
return False
|
||||
# Validate internal cwd / workspace match if supported by artifact format
|
||||
return True
|
||||
|
||||
def purge_artifacts(self, uuid: str, ctx: DiscoveryContext) -> list:
|
||||
# Remove on-disk session files when --purge-conversation is invoked
|
||||
path = self.artifact_path(uuid, ctx)
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
return [path]
|
||||
return []
|
||||
|
||||
# Command-Line Invocation Synthesis
|
||||
def spawn_spec(self, binary: str, session_uuid: str = "", use_wrapper: bool = False) -> str:
|
||||
if session_uuid:
|
||||
return f"{binary} --session {session_uuid}"
|
||||
return binary
|
||||
|
||||
def resume_spec(self, binary: str, session_uuid: str, materialized: bool = False) -> str:
|
||||
if materialized and session_uuid:
|
||||
return f"{binary} --resume {session_uuid}"
|
||||
return f"{binary} --session {session_uuid}" if session_uuid else binary
|
||||
|
||||
# Authentication & Health Check
|
||||
def auth_ok(self, run_cmd: Optional[Any] = None) -> bool:
|
||||
# Check config file, token file, or run CLI status command
|
||||
return os.path.exists(f"{os.path.expanduser('~')}/.newagent/auth.json")
|
||||
|
||||
# Session Discovery
|
||||
def discover(self, ctx: DiscoveryContext) -> list:
|
||||
# Scan on-disk session directory for unassigned sessions matching ctx.workspace
|
||||
return []
|
||||
```
|
||||
|
||||
### Step 2: Register Adapter in Registry (`.agents/skills/lib_py/agents/registry.py`)
|
||||
- Import `NewAgentAdapter` in `registry.py`.
|
||||
- Add `'newagent': NewAgentAdapter()` to the `_ADAPTERS` dictionary.
|
||||
- Automatic registration immediately enables:
|
||||
- `python -m lib_py.agents facts newagent`
|
||||
- `python -m lib_py.agents resolve <session_name>`
|
||||
- `python -m lib_py.agents spawn-spec newagent ...`
|
||||
- `python -m lib_py.agents resume-spec newagent ...`
|
||||
- `python -m lib_py.agents exit-key newagent`
|
||||
|
||||
### Step 3: Shell Runtime Dispatch in `lib.sh`
|
||||
1. **Herdr Session Kind Mapping (`lib.sh:358-375`)**:
|
||||
- Add `*-creator-newagent|*-planner-newagent|*-reviewer-newagent) kind="newagent" ;;`
|
||||
- Add `elif echo "$name" | grep -qi "newagent"; then kind="newagent"` in fallback pattern.
|
||||
2. **Binary Name Stripping (`lib.sh:385`)**:
|
||||
- Add `'newagent'` to the tuple of recognized agent binary names to prevent positional argument duplication when invoking `herdr agent start`.
|
||||
3. **Session Name Suffix Matching (`lib.sh:resolve_agent_name`)**:
|
||||
- Supported automatically via `agent_of_row()` priority resolution.
|
||||
|
||||
### Step 4: Herdr CLI Daemon Support
|
||||
- Confirm whether `herdr agent start <name> --kind <kind>` accepts the new agent kind:
|
||||
- Built-in Herdr kinds: `claude`, `hermes`, `cline`, `generic`, etc.
|
||||
- If Herdr 0.8.0 requires a known kind enum, either pass `--kind generic` or register the agent kind definition in Herdr's agent profile configuration.
|
||||
|
||||
### Step 5: Test Suite & Contract Verification
|
||||
1. **Tier 1 Adapter Contract (`tests/test_a4_adapter_contract.py`)**:
|
||||
- Add `'newagent'` to `test_agent_adapter_registry`.
|
||||
- Add expected tuple to `test_adapter_required_properties`:
|
||||
`('ready_tokens_regex', '/exit', 'newagent-cli', ('session_id',))`
|
||||
- Verify `test_facts_bridge_eval_contract` for `newagent`.
|
||||
- Add unit tests for `artifact_path`, `purge_artifacts`, and `discover`.
|
||||
2. **Tier 2 Lifecycle Tests (`tests/test_tier2_component.py`)**:
|
||||
- Test session creation, YAML serialization, `stop_session.sh` graceful shutdown, and `resume_session.sh`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Work Difficulty & Complexity Assessment Matrix
|
||||
|
||||
| Agent Type | Target CLI / Tool | Session Storage Format | Auth Strategy | Ready Tokens Pattern | Complexity Tier | Estimated Effort |
|
||||
|---|---|---|---|---|:---:|:---:|
|
||||
| **OpenCode** | `opencode` (CLI / TUI) | JSON files in `~/.opencode/sessions/` | Local token / API key | `OpenCode\|Chat\|Welcome` | **Tier 1 (Low)** | ~0.5 day |
|
||||
| **Codex CLI** | `codex` / `openai-codex` | JSONL in `~/.codex/projects/` | API Key (`OPENAI_API_KEY`) | `OpenAI Codex\|Assistant\|❯` | **Tier 1 (Low)** | ~0.5 day |
|
||||
| **Kimi CLI** | `kimi` (Moonshot CLI) | SQLite DB in `~/.kimi/history.db` | API Key in `~/.kimi/config` | `Kimi\|Moonshot\|Chat` | **Tier 1 (Low)** | ~0.5 day |
|
||||
| **Grok-Build** | `grok` / `grok-build` | JSON in `~/.grok/sessions/` | Token file / Env var | `Grok\|xAI\|Building` | **Tier 1 (Low)** | ~0.5 day |
|
||||
| **Cursor CLI** | `cursor` (Headless/RPC) | State DB in `~/.cursor/` or RPC port | OAuth / Cookie | `Cursor\|Connected\|Listening` | **Tier 2 (Medium)** | ~1.5 days |
|
||||
| **Custom Local LLM** | `ollama` / `vllm-cli` | Custom JSONL / stdout | None (Local host) | `>>>\|Send a message` | **Tier 2 (Medium)** | ~1.0 day |
|
||||
|
||||
### Complexity Breakdown by Layer:
|
||||
1. **Tier 1 (Adapter Contract & Facts Bridge)**:
|
||||
- *Effort*: Very Low.
|
||||
- *Risk*: Zero risk to existing framework. Python `BaseAgentAdapter` interface is strictly decoupled and self-contained.
|
||||
2. **Tier 2 (Herdr Kind & Shell Lifecycle)**:
|
||||
- *Effort*: Low.
|
||||
- *Risk*: Standardized via `lib.sh:resolve_agent_type_from_registry` and `_sanitize_herdr_agent_name`.
|
||||
3. **Tier 3 (TUI Readiness & Input Capture)**:
|
||||
- *Effort*: Low to Medium.
|
||||
- *Risk*: Depends on whether the agent CLI emits predictable startup banner tokens and supports ANSI raw terminal input.
|
||||
|
||||
---
|
||||
|
||||
## 5. Reviewer Evaluation Guidelines
|
||||
|
||||
When reviewing newly added agent adapters or layout enhancements, reviewers should verify:
|
||||
1. `BaseAgentAdapter` compliance (all abstract properties and methods implemented).
|
||||
2. Fail-safe behavior (graceful fallback if session transcript is missing or corrupted).
|
||||
3. Zero regression across existing test suites (`pytest tests/`).
|
||||
4. Proper documentation in `AGENTS.md` and `.mam.env.example`.
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Review Report — Job 7c8f1f5c
|
||||
|
||||
- **Reviewer**: planner-reviewer-claude-01
|
||||
- **Job ID**: 7c8f1f5c
|
||||
- **Target**: Review commit f3ac68f (layout min_cols=15, min_rows=0) and new agent types extension roadmap
|
||||
|
||||
## 1. Layout Engine Enhancement (f3ac68f)
|
||||
- Layout engine minimum columns relaxed to 15 (MAM_MIN_PANE_COLS=15).
|
||||
- Height constraints removed for vertical splits (MAM_MIN_PANE_ROWS=0 default), relying on terminal scrollback.
|
||||
- 54x23 compact viewport cleanly accommodates 4 panes in a single workspace without premature overflow.
|
||||
- Structural analysis and hand-tracing confirm full correctness.
|
||||
|
||||
## 2. New Agent Types Extension Roadmap
|
||||
- Architecture roadmap (.agents/reports/new_agent_types_roadmap.md) verified against codebase.
|
||||
- BaseAgentAdapter, registry, DiscoveryContext, spawn spec, and contract test requirements are feasible and modular.
|
||||
- Tier 1 vs Tier 2 complexity ratings accurately reflect integration effort.
|
||||
|
||||
[VERDICT: PASS]
|
||||
@@ -0,0 +1,117 @@
|
||||
# Review Report — Job 4fd933af
|
||||
|
||||
- **Reviewer**: cline (herdr session `reviewer-cline-01`, role: reviewer)
|
||||
- **Job ID**: 4fd933af
|
||||
- **Reviewed branch**: `main` (1 commit ahead of `origin/main`)
|
||||
- **Scope**: Cross code review (lint / operability / drift) of (a) the committed
|
||||
layout-engine enhancement `f3ac68f` and (b) the untracked roadmap document
|
||||
`.agents/reports/new_agent_types_roadmap.md`. Task goals: validate layout
|
||||
correctness, assess new-agent-extension roadmap feasibility, issue verdict.
|
||||
|
||||
## 1. Change Inventory
|
||||
|
||||
| Item | File(s) | Status |
|
||||
|------|---------|--------|
|
||||
| Layout relax | `lib_py/layout.py`, `lib.sh:432`, `.mam.env.example`, `tests/test_layout.py`, `tests/test_tier1_unit.py` | **Committed** (`f3ac68f`) |
|
||||
| New-agent roadmap | `.agents/reports/new_agent_types_roadmap.md` (new, 229 lines) | **Untracked** |
|
||||
|
||||
Commit `f3ac68f` — "feat(layout): relax MAM_MIN_PANE_COLS to 15 and remove
|
||||
vertical height constraints for scrollable terminal split":
|
||||
- `compute_2xk_layout` defaults: `min_cols` 40→**15**, `min_rows` 20→**0**.
|
||||
- CLI `--min-cols`/`--min-rows` argparse defaults: 15 / 0.
|
||||
- `lib.sh:432` fallbacks: `${MAM_MIN_PANE_COLS:-15}` / `${MAM_MIN_PANE_ROWS:-0}`.
|
||||
- Both height-overflow guards now short-circuit on `min_rows > 0 and ...`
|
||||
(single-pane and singleton-column paths), so default `min_rows=0` disables
|
||||
vertical constraints while a positive env value re-engages them.
|
||||
- `.mam.env.example` updated (default 15 / 0, terminology aligned to tmux
|
||||
horizontal=side-by-side / vertical=top-bottom).
|
||||
- Tests rewritten to assert `min_cols=15` boundary (30 split vs 29 overflow)
|
||||
and `min_rows=0` behavior.
|
||||
|
||||
## 2. Lint / Syntax / Drift
|
||||
|
||||
- `python -m py_compile lib_py/layout.py` → **OK**
|
||||
- `bash -n .agents/skills/lib.sh` → **OK**
|
||||
- Stale-default sweep (`:-40`, `:-20`, `min_cols: int = 40`, `min_rows: int = 20`,
|
||||
`default=40`, `default=20`) across `.agents`/`tests` → **clean (no orphans)**.
|
||||
All authoritative default sites are consistently 15 / 0.
|
||||
- Working-tree drift: only the roadmap file is untracked; no stray/dirty
|
||||
submodule or out-of-scope edits this round (cleaner than the prior review).
|
||||
|
||||
## 3. Layout Implementation — Operability Verification
|
||||
|
||||
### Goal: dense tiling in compact viewports without premature overflow
|
||||
- **Column boundary** (`width // 2 < min_cols`, strict `<`): at width 30 →
|
||||
`30//2 = 15`, `15 < 15` False → splits right; at width 29 → `29//2 = 14 < 15`
|
||||
→ `column_width_overflow`. CLI-confirmed: 30-col → `right p1`, 29-col →
|
||||
`overflow p1`. ✓
|
||||
- **`min_rows=0` disables height checks**: a single 80×3 pane → `down p1`
|
||||
(splits down; scrollback rationale holds). With an explicit positive
|
||||
`MAM_MIN_PANE_ROWS`, the `min_rows > 0 and ...` guards re-engage the
|
||||
`single_pane_height_constrained` / `singleton_height_overflow` paths — so
|
||||
the disable is opt-out, not a hard removal. Well-designed. ✓
|
||||
- **Tiling progression** (1→2 down, 2→3 right, 3→4 fill-singleton down,
|
||||
4→5 overflow) unchanged in structure; only thresholds moved. The 54×23 /
|
||||
90–100 col scenarios described in the roadmap now fit 4 panes per workspace
|
||||
where the old 60/20 defaults could not even open a 2nd column. ✓
|
||||
|
||||
### Tests
|
||||
- `tests/test_layout.py` + `tests/test_tier1_unit.py` → **85 passed in 9.11s**.
|
||||
- `tests/test_a4_adapter_contract.py` → **13 passed in 0.47s**.
|
||||
## 4. New-Agent-Types Roadmap — Feasibility Assessment
|
||||
|
||||
The roadmap (`new_agent_types_roadmap.md`) is a planning/architecture document,
|
||||
not executable code. Its value rests on the accuracy of its codebase references
|
||||
and the soundness of its blueprint. Both verified:
|
||||
|
||||
| Roadmap claim | Verification | Result |
|
||||
|---------------|--------------|--------|
|
||||
| Adapter framework `lib_py.agents` with `BaseAgentAdapter` | `lib_py/agents/{base.py,registry.py,sanitize.py,input_region.py,adapters/}` exist | ✓ |
|
||||
| `BaseAgentAdapter` interface (name, own_key, ready_tokens, exit_key, delegate_agent_key, identity_cache_fields, input_prompt, input_rule_pattern, artifact_path, verify_artifact, purge_artifacts, spawn_spec, resume_spec, auth_ok, discover) | All symbols present in `base.py` at the cited lines | ✓ exact |
|
||||
| `DiscoveryContext` used in template code | `class DiscoveryContext` at `base.py:15` | ✓ |
|
||||
| Step 2: add `'newagent': NewAgentAdapter()` to `_ADAPTERS` | `registry.py` `_ADAPTERS` dict registers claude/agy/hermes/cline exactly this way | ✓ proven pattern |
|
||||
| Step 3.1: herdr kind mapping `lib.sh:358-375` | kind dispatch at `lib.sh:357-371` (creator/planner/reviewer → kind) | ✓ (line off by ~4) |
|
||||
| Step 3.3: "automatic via `agent_of_row()`" | `agent_of_row()` + `matches_session_name()` in `registry.py` | ✓ |
|
||||
| Step 5.1: contract tests `test_agent_adapter_registry` / `test_adapter_required_properties` / `test_facts_bridge_eval_contract` | All three present in `test_a4_adapter_contract.py` (lines 28/58/79) | ✓ |
|
||||
| Step 5.2: `tests/test_tier2_component.py` | File exists | ✓ |
|
||||
|
||||
**Feasibility verdict**: The 5-step blueprint (adapter class → registry →
|
||||
`lib.sh` dispatch → herdr `--kind` → contract/lifecycle tests) is the correct
|
||||
layering and is proven by the four existing adapters. The complexity matrix
|
||||
(Tier 1 adapter ≈ 0.5 day; Tier 2 herdr-kind/lifecycle; Tier 3 TUI readiness)
|
||||
is reasonable. The document is actionable as-is.
|
||||
|
||||
## 5. Findings (advisory, non-blocking)
|
||||
|
||||
### F-1 (hygiene): Roadmap file is untracked
|
||||
`git status` lists `.agents/reports/new_agent_types_roadmap.md` as untracked.
|
||||
Since it is a deliverable referenced by this job's output contract, it should
|
||||
be `git add`-ed and committed (e.g. `docs(roadmap): new agent types extension
|
||||
blueprint`) so the planning artifact persists. Content is fine; only tracking
|
||||
state needs action.
|
||||
|
||||
### F-2 (verification gap): Full-suite green not independently confirmed
|
||||
The roadmap §1.1 asserts "all 371 tests across 18 suites pass." This review
|
||||
confirmed 98 unit tests across all touched files plus the adapter-contract
|
||||
suite, but did not re-run the full integration suite within the time budget.
|
||||
**Direction**: confirm the full `pytest tests/` is green in CI before merge;
|
||||
no code change implied.
|
||||
|
||||
### F-3 (doc wording nit): `min_rows=0` is "disabled-by-default", not "removed"
|
||||
Roadmap §1.1 says vertical constraints were "removed." Precisely, the guards
|
||||
are `min_rows > 0 and ...`, so a positive `MAM_MIN_PANE_ROWS` env value
|
||||
re-engages height checks. The doc slightly understates this opt-in
|
||||
re-enablement. **Direction**: one-line wording fix ("disabled by default;
|
||||
re-enabled by setting MAM_MIN_PANE_ROWS>0") for operator accuracy. Non-blocking.
|
||||
|
||||
## 6. Summary
|
||||
|
||||
The committed layout change correctly and consistently relaxes `min_cols` to
|
||||
15 and defaults `min_rows` to 0 (disabled-by-default, re-engaged via env) with
|
||||
matching, passing tests (98 verified green) and clean syntax. The new-agent
|
||||
roadmap is a feasible, accurately-referenced blueprint grounded in the real
|
||||
adapter framework (every cited file, symbol, and test verified to exist). The
|
||||
three findings are commit-hygiene / verification-gap / wording nits that do
|
||||
not affect correctness, operability, or feasibility, and require no redesign.
|
||||
|
||||
[VERDICT: PASS]
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
+7
-6
@@ -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).
|
||||
|
||||
+66
-96
@@ -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"
|
||||
|
||||
+51
-54
@@ -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"
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user