Repository navigation
search_panes: visual-row capture misses wrap-spanning patterns #55
Description
Problem
search_panes doesn't join visually-wrapped lines before matching, so plain-text patterns that tmux wraps at the pane's column width split across two captured rows and miss the match. Long error messages (e.g. "Build failed: module not found in /very/long/path/to/module.py") are the canonical example — they wrap mid-string and re.search against each row in isolation never sees the full string.
wait_for_text shipped the wrap fix in #45 (commit 67eed15) by passing join_wrapped=True to pane.capture_pane() — which adds tmux's -J flag. search_panes was not part of that sweep.
Repro
Render a pane with a long line that wraps:
$ pytest tests/integration/test_pipeline_with_a_very_long_test_class_name_to_force_wrap.py::TestXYZ::test_specific_assertion_that_fails
FAILED
If the pane is ~80 cols wide, the path wraps. Then:
search_panes(pattern="test_specific_assertion_that_fails")Returns the pane (the substring lives on row 2). But:
search_panes(pattern="tests/integration/test_pipeline_with_a_very_long_test_class_name_to_force_wrap.py::TestXYZ::test_specific_assertion_that_fails")Returns no match — the pattern spans the wrap boundary.
Root cause
src/libtmux_mcp/tools/pane_tools/search.py slow path:
lines = pane.capture_pane(start=content_start, end=content_end)
matched_lines = [line for line in lines if compiled.search(line)]No join_wrapped=True. Compare to src/libtmux_mcp/tools/pane_tools/wait.py:
lines = await asyncio.to_thread(
pane.capture_pane,
start=start_line,
end=None,
join_wrapped=True,
)Nuance — the fast path is structurally wrap-blind
search_panes has two paths:
- Slow path (Python-regex over captured lines): the mechanical fix is
join_wrapped=True. Drop-in. - Fast path (
#{C:pattern}format conditional): runs tmux's C-levelwindow_pane_search()against the grid by visual row. There's no-Janalog at the format-conditional level, so this path is wrap-blind by tmux design.
The fast path triggers when (a) no scrollback range is requested and (b) the pattern has no regex metacharacters or tmux format-injection characters. Long plain-text error messages from build output hit the fast path most of the time — exactly the worst case for wrap-blindness.
Proposed fix
- Slow path: add
join_wrapped=Trueto thepane.capture_pane(...)call. Same trade-offwait_for_textaccepts —matched_linesmay contain strings longer thanpane_width. - Fast path: docstring note acknowledging that plain-text patterns spanning the pane's wrap column may miss matches; suggest passing
regex=Trueor supplying scrollback range (content_start=-N) to force the slow path. - Test: regression covering a wrap-spanning pattern, parallel to
test_wait_for_text_matches_pattern_across_wrap. - CHANGES entry under
### Fixes(Published-Release Test passes —search_panesshipped in0.1.0a2).
Out of scope
- Routing wrap-spanning patterns from the fast path to the slow path automatically. Requires tmux-side detection of pane width vs. pattern length, which is heuristic and adds complexity.
- Extending
-Jto tmux's#{C:...}format conditional. That's an upstream-tmux change.
Activity
tony commentedon May 24, 2026
Triage pass (2026-05-24): keep this as a first-action correctness bug.
Current-code check: the slow path still captures rows without
join_wrapped=True, whilewait_for_textalready uses joined wrapped rows for the same class of terminal output. libtmux exposes the needed flag onPane.capture_pane(join_wrapped=True)(libtmuxf70cb62). The tmux fast path is inherently visual-row based throughwindow_pane_search()(tmux134ba6c), so the body's fast-path/slow-path split is the right framing.Overlap/value: this is not overshadowed by #60.
capture_sincewould help agents inspect output, butsearch_panesshould still be correct when users ask it to search pane text. Recommended action remains: fix the slow path, document the fast-path wrap limitation, add a wrap-spanning regression, and ship a### Fixeschangelog entry.