X Tutup
Skip to content

fix(workspace/builder): Wait for shell prompt before layout and commands#1018

Merged
tony merged 7 commits intomasterfrom
pane-readiness
Mar 8, 2026
Merged

fix(workspace/builder): Wait for shell prompt before layout and commands#1018
tony merged 7 commits intomasterfrom
pane-readiness

Conversation

@tony
Copy link
Member

@tony tony commented Mar 8, 2026

Summary

Fixes #365 (open since March 2018). WorkspaceBuilder sends SIGWINCH (via select_layout) and
commands (via send_keys) to panes before their shell finishes initializing. With zsh, this
produces the PROMPT_SP partial-line marker — an inverse+bold %.

Root cause traced through tmux and zsh source:

  • tmux screen_reinit() initializes new pane cursors at (0, 0) (screen.c:107-108).
    select_layout triggers ioctl(TIOCSWINSZ) on the pane PTY (window.c:449), delivering
    SIGWINCH to the child shell.
  • zsh preprompt() (utils.c:1530) runs the PROMPT_SP heuristic (utils.c:1545-1566)
    before every prompt, outputting the PROMPT_EOL_MARK (default %B%S%#%s%b). When SIGWINCH
    arrives mid-init, the interrupted redraw leaves the % marker visible.

Fix: Poll pane cursor position until it moves from origin (0, 0) — indicating the shell
has drawn its prompt — before calling select_layout. This ensures SIGWINCH only arrives
after the shell can handle resize gracefully.

Changes

  • Add _wait_for_pane_ready() that polls cursor_x/cursor_y via pane.refresh() (2s timeout, 50ms interval)
  • Call readiness check before select_layout in iter_create_panes() for all default-shell panes
  • Wait for blank panes too (- pane shorthand expands to shell_command: [])
  • Remove redundant select_layout call in build() that doubled SIGWINCH signals
  • Add 6 tests: readiness detection, timeout, parametrized call counts, layout deduplication

Test plan

  • uv run ruff check . --fix --show-fixes — clean
  • uv run ruff format . — unchanged
  • uv run mypy — no issues in 120 files
  • uv run py.test -vvv — 1027 passed, 2 skipped
  • Manual: tmuxp load projects with 12-window × 4-pane zsh config — no % markers

tony added 2 commits March 8, 2026 07:02
…layout and commands

why: WorkspaceBuilder sends SIGWINCH (via select_layout) and commands
(via send_keys) to panes before their shell finishes initializing (#365,
open since 2018). With zsh, this triggers the PROMPT_SP partial-line
marker — an inverse+bold "%" — because:

  tmux: screen_reinit() sets cx=cy=0 on new panes (screen.c:107-108).
    select_layout → layout_fix_panes → window_pane_resize enqueues a
    resize, and window_pane_send_resize delivers SIGWINCH via
    ioctl(TIOCSWINSZ) on the pane's PTY (window.c:449).

  zsh: preprompt() (utils.c:1530) runs the PROMPT_SP heuristic
    (utils.c:1545-1566) before every prompt. It outputs the
    PROMPT_EOL_MARK (default: "%B%S%#%s%b") to detect partial lines,
    then pads with spaces and returns the cursor. When SIGWINCH arrives
    mid-init, the interrupted redraw leaves the marker visible.

what:
- Add _wait_for_pane_ready() that polls cursor position until it moves
  from origin (0,0), indicating the shell has drawn its prompt
- Call readiness check before select_layout in iter_create_panes() so
  SIGWINCH only arrives after the shell can handle resize gracefully
- Wait for all default-shell panes, not just those with commands (blank
  panes via "- pane" shorthand expand to shell_command: [])
- Remove redundant select_layout call in build() that doubled SIGWINCH
  signals after each pane yield
- Add tests for readiness detection, timeout, call count, and layout
  deduplication
@codecov
Copy link

codecov bot commented Mar 8, 2026

Codecov Report

❌ Patch coverage is 86.95652% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.32%. Comparing base (3785ebc) to head (9debb02).
⚠️ Report is 8 commits behind head on master.

Files with missing lines Patch % Lines
src/tmuxp/workspace/builder.py 82.35% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1018      +/-   ##
==========================================
+ Coverage   80.11%   80.32%   +0.21%     
==========================================
  Files          28       28              
  Lines        2409     2430      +21     
  Branches      457      462       +5     
==========================================
+ Hits         1930     1952      +22     
- Misses        356      357       +1     
+ Partials      123      121       -2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment on lines -326 to -328
if "layout" in window_config:
window.select_layout(window_config["layout"])

Copy link
Member Author

@tony tony Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did some code archaeology on this because I had the same concern about reintroducing old layout/space bugs.

This specific select_layout() call looks like the redundant one, not the essential one. The builder still applies select_layout() per-pane in iter_create_panes(), which is the part that actually preserves
the incremental rebalance during pane creation. The call removed here was the second pass in build(), so it was effectively doubling the layout application after each yielded pane.

The historical “no space for new pane” regressions were more closely tied to session geometry than to this duplicate call. In 2022, #793 changed both layout timing and default-size, and the fallout in #800
was mainly from the server starting at 80x24 before a client attached. The later fix path was to size new_session() correctly (8a59203, then 0777c3b/63ac676d), not to keep a second select_layout()
here.

I also checked the current regressions: test_issue_800_default_size_many_windows still passes, and this branch adds test_select_layout_not_called_after_yield specifically to lock in the intended behavior:
call select_layout() once per pane, not twice. So I don’t think removing this duplicate call should reintroduce the lack-of-space issue.

@tony tony changed the title fix(workspace/builder): Wait for shell prompt before layout and commands (#365) fix(workspace/builder): Wait for shell prompt before layout and commands Mar 8, 2026
tony added 5 commits March 8, 2026 07:35
…iness check

why: The except block in _wait_for_pane_ready discarded the exception
traceback, making it impossible to diagnose pane refresh failures.
what:
- Add exc_info=True to logger.debug call in the except block
…ness skip coverage

why: The window_config.get("window_shell") fallback path at builder.py:566
was untested. Only pane-level shell: had fixture coverage.
what:
- Add skips_all_panes_with_window_shell fixture to PANE_READINESS_FIXTURES
- Verifies expected_wait_count=0 when window_shell is set
…c scope

why: The readiness skip for panes with shell/window_shell was undocumented,
making it unclear to future maintainers why pane_shell is None is the check.
what:
- Add comment explaining that shell/window_shell runs a command launcher,
  not an interactive shell, so there is no prompt to wait for
…o 5s

why: The 2s timeout was too tight for slow environments, causing
flaky doctest failures when the shell hadn't drawn its prompt yet.
what:
- Bump _wait_for_pane_ready doctest timeout from 2.0 to 5.0 seconds
why: Tmux doctests are inherently timing-sensitive — shell startup
races can cause spurious failures even with generous timeouts.
what:
- Add pytest_collection_modifyitems hook to apply flaky(reruns=2)
  marker to DoctestItems from DOCTEST_NEEDS_TMUX modules
@tony tony merged commit 1ac73ea into master Mar 8, 2026
13 checks passed
@tony tony deleted the pane-readiness branch March 8, 2026 13:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Outputs % / percent sign after every command in some panes

1 participant

X Tutup