railmux 0.4.0.dev16__tar.gz → 0.4.0.dev17__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. {railmux-0.4.0.dev16/src/railmux.egg-info → railmux-0.4.0.dev17}/PKG-INFO +1 -1
  2. railmux-0.4.0.dev17/src/railmux/__init__.py +1 -0
  3. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/fast_display_history.py +11 -9
  4. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/tmux_binding_manager.py +5 -2
  5. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/ui/app.py +208 -41
  6. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17/src/railmux.egg-info}/PKG-INFO +1 -1
  7. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_fast_full_window.py +43 -11
  8. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_fullscreen_binding.py +206 -0
  9. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_terminal_size.py +12 -0
  10. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_tmux_binding_manager.py +25 -0
  11. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_tmux_status.py +57 -8
  12. railmux-0.4.0.dev16/src/railmux/__init__.py +0 -1
  13. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/LICENSE +0 -0
  14. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/MANIFEST.in +0 -0
  15. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/README.md +0 -0
  16. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/pyproject.toml +0 -0
  17. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/setup.cfg +0 -0
  18. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/__main__.py +0 -0
  19. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/atomic_file.py +0 -0
  20. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/background_index.py +0 -0
  21. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/cli.py +0 -0
  22. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/codex_index.py +0 -0
  23. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/config.py +0 -0
  24. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/config_cli.py +0 -0
  25. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/diagnostics.py +0 -0
  26. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/discovery.py +0 -0
  27. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/display_transport.py +0 -0
  28. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/entrypoint.py +0 -0
  29. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/fast_display_client.py +0 -0
  30. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/fast_display_input.py +0 -0
  31. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/fast_display_protocol.py +0 -0
  32. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/fast_display_server.py +0 -0
  33. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/favorites.py +0 -0
  34. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/fuzzy.py +0 -0
  35. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/help_workspace.py +0 -0
  36. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/launcher.py +0 -0
  37. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/legacy_sessions.py +0 -0
  38. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/local_clipboard.py +0 -0
  39. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/local_open.py +0 -0
  40. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/models.py +0 -0
  41. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/modes.py +0 -0
  42. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/mouse_manager.py +0 -0
  43. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/orphan_marker.py +0 -0
  44. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/pane_surface.py +0 -0
  45. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/path_codec.py +0 -0
  46. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/provider_paths.py +0 -0
  47. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/remote_config.py +0 -0
  48. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/renames.py +0 -0
  49. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/restart_state.py +0 -0
  50. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/runtime_config.py +0 -0
  51. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/scroll_agent.py +0 -0
  52. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/scroll_manager.py +0 -0
  53. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/selection_isolation.py +0 -0
  54. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/self_update.py +0 -0
  55. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/session_cache.py +0 -0
  56. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/session_index.py +0 -0
  57. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/session_lease.py +0 -0
  58. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/setting_contracts.py +0 -0
  59. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/settings.py +0 -0
  60. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/ssh_args.py +0 -0
  61. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/ssh_compat.py +0 -0
  62. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/ssh_display_diagnostics.py +0 -0
  63. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/ssh_doctor.py +0 -0
  64. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/system_deps.py +0 -0
  65. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/terminal_status.py +0 -0
  66. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/tmux_ctl.py +0 -0
  67. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/tmux_health.py +0 -0
  68. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/tmux_server.py +0 -0
  69. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/tool_panes.py +0 -0
  70. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/transcript.py +0 -0
  71. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/ui/__init__.py +0 -0
  72. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/ui/_widgets.py +0 -0
  73. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/ui/keymap.py +0 -0
  74. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/ui/modals.py +0 -0
  75. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/ui/projects_pane.py +0 -0
  76. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/ui/running_pane.py +0 -0
  77. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/ui/sessions_pane.py +0 -0
  78. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/ui/sidebar.py +0 -0
  79. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/ui/statusbar.py +0 -0
  80. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/ui/workspace.py +0 -0
  81. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/windows_attach_relay.py +0 -0
  82. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/windows_bootstrap.py +0 -0
  83. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/windows_install_log.py +0 -0
  84. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/windows_msys2.py +0 -0
  85. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/windows_pacman.py +0 -0
  86. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux/windows_tmux_lifecycle.py +0 -0
  87. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux.egg-info/SOURCES.txt +0 -0
  88. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux.egg-info/dependency_links.txt +0 -0
  89. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux.egg-info/entry_points.txt +0 -0
  90. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux.egg-info/requires.txt +0 -0
  91. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/src/railmux.egg-info/top_level.txt +0 -0
  92. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_agent_workspace.py +0 -0
  93. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_atomic_file.py +0 -0
  94. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_background_index.py +0 -0
  95. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_cli.py +0 -0
  96. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_codex_delete.py +0 -0
  97. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_codex_index.py +0 -0
  98. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_codex_lineage.py +0 -0
  99. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_codex_transcript.py +0 -0
  100. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_config.py +0 -0
  101. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_config_cli.py +0 -0
  102. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_denested_transport_benchmark.py +0 -0
  103. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_diagnostics.py +0 -0
  104. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_discovery.py +0 -0
  105. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_display_transport.py +0 -0
  106. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_focus_highlight.py +0 -0
  107. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_fuzzy.py +0 -0
  108. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_help_agent.py +0 -0
  109. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_help_workspace.py +0 -0
  110. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_history_cleanup.py +0 -0
  111. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_keymap.py +0 -0
  112. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_launcher.py +0 -0
  113. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_layout_profile.py +0 -0
  114. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_legacy_sessions.py +0 -0
  115. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_local_clipboard.py +0 -0
  116. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_local_open.py +0 -0
  117. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_modals.py +0 -0
  118. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_mode_selection.py +0 -0
  119. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_modes.py +0 -0
  120. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_mouse_manager.py +0 -0
  121. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_new_project.py +0 -0
  122. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_options.py +0 -0
  123. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_orphan_recovery.py +0 -0
  124. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_pane_surface.py +0 -0
  125. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_path_codec.py +0 -0
  126. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_projects_pane.py +0 -0
  127. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_provider_paths.py +0 -0
  128. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_release_notes.py +0 -0
  129. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_release_workflow.py +0 -0
  130. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_remote_config.py +0 -0
  131. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_renames.py +0 -0
  132. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_restart_state.py +0 -0
  133. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_running_pane.py +0 -0
  134. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_running_sort.py +0 -0
  135. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_runtime_config.py +0 -0
  136. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_scroll_agent.py +0 -0
  137. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_scroll_manager.py +0 -0
  138. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_selection_isolation.py +0 -0
  139. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_self_update.py +0 -0
  140. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_session_cache.py +0 -0
  141. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_session_index.py +0 -0
  142. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_session_lease.py +0 -0
  143. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_sessions_pane.py +0 -0
  144. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_setting_contracts.py +0 -0
  145. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_settings.py +0 -0
  146. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_sidebar.py +0 -0
  147. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_soft_quit.py +0 -0
  148. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_ssh_args.py +0 -0
  149. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_ssh_compat.py +0 -0
  150. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_ssh_display_benchmark.py +0 -0
  151. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_ssh_display_diagnostics.py +0 -0
  152. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_ssh_doctor.py +0 -0
  153. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_status.py +0 -0
  154. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_statusbar.py +0 -0
  155. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_system_deps.py +0 -0
  156. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_terminal_status.py +0 -0
  157. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_termux_local_touch.py +0 -0
  158. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_tmux_ctl.py +0 -0
  159. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_tmux_health.py +0 -0
  160. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_tmux_integration.py +0 -0
  161. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_tmux_server.py +0 -0
  162. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_tool_panes.py +0 -0
  163. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_transcript.py +0 -0
  164. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_utf8_state.py +0 -0
  165. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_web_demo_recorder.py +0 -0
  166. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_wheel_smoke.py +0 -0
  167. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_widgets.py +0 -0
  168. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_windows_attach_relay.py +0 -0
  169. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_windows_bootstrap.py +0 -0
  170. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_windows_install_log.py +0 -0
  171. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_windows_msys2.py +0 -0
  172. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_windows_pacman.py +0 -0
  173. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_windows_tmux_lifecycle.py +0 -0
  174. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_windows_wrapper_contract.py +0 -0
  175. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tests/test_yolo_prompt.py +0 -0
  176. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tools/denested_transport_benchmark.py +0 -0
  177. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tools/release_notes.py +0 -0
  178. {railmux-0.4.0.dev16 → railmux-0.4.0.dev17}/tools/wheel_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.4.0.dev16
3
+ Version: 0.4.0.dev17
4
4
  Summary: Terminal UI to navigate, resume, and start Claude Code and Codex sessions
5
5
  Author-email: Rightglow <zhang.taian@foxmail.com>
6
6
  License-Expression: MIT
@@ -0,0 +1 @@
1
+ __version__ = "0.4.0.dev17"
@@ -287,15 +287,17 @@ class LocalHistoryView:
287
287
  # intermediate output above the current screen.
288
288
  lines = incoming.lines
289
289
  else:
290
- # A full-screen TUI redraw can leave no trustworthy text
291
- # anchor. Preserve the known history and replace only the
292
- # mutable live viewport. Appending every unaligned 300-line
293
- # prefetch would duplicate a full screen every three seconds.
294
- live_count = min(incoming.height, len(incoming.lines))
295
- retained = previous.lines[
296
- : -min(previous.height, len(previous.lines))
297
- ]
298
- lines = retained + incoming.lines[-live_count:]
290
+ # Never splice two captures whose timelines cannot be aligned.
291
+ # Retaining old history and replacing only the live viewport
292
+ # looked conservative, but it silently omitted every row
293
+ # produced between those two pieces. That false seam appeared
294
+ # as discontinuous SSH history until returning to the bottom
295
+ # forced a fresh deep capture. Keep the newest internally
296
+ # contiguous hot capture instead; the first wheel-up requests
297
+ # a cumulative deep page and can recover older rows safely.
298
+ # An already-frozen viewport owns its immutable snapshot and
299
+ # therefore remains undisturbed by this cache replacement.
300
+ lines = incoming.lines
299
301
  else:
300
302
  start = min(0, delta)
301
303
  incoming_end = delta + len(incoming.lines)
@@ -540,7 +540,6 @@ class SharedTmuxBindingManager:
540
540
  )
541
541
  break
542
542
  return False
543
- self._registered = True
544
543
  self._prefix_tab_managed = state["prefix_tab_managed"]
545
544
  self._right_click_managed = (
546
545
  state["right_click_managed"])
@@ -552,6 +551,10 @@ class SharedTmuxBindingManager:
552
551
  state["status_click_managed"])
553
552
  self._termux_tap_managed = (
554
553
  state["termux_tap_managed"])
554
+ # Publish registration only after every capability is
555
+ # final so concurrent readers cannot observe a torn
556
+ # lease during managed-Windows background setup.
557
+ self._registered = True
555
558
  return True
556
559
 
557
560
  backup = tmux_ctl.prepare_root_function_bindings()
@@ -701,13 +704,13 @@ class SharedTmuxBindingManager:
701
704
  state["termux_tap_backup"], token=token)
702
705
  self._remove_state()
703
706
  return False
704
- self._registered = True
705
707
  self._prefix_tab_managed = state["prefix_tab_managed"]
706
708
  self._right_click_managed = state["right_click_managed"]
707
709
  self._selection_hook_managed = state["selection_hook_managed"]
708
710
  self._selection_hook_index = state["selection_hook_index"]
709
711
  self._status_click_managed = state["status_click_managed"]
710
712
  self._termux_tap_managed = state["termux_tap_managed"]
713
+ self._registered = True
711
714
  return True
712
715
  except OSError:
713
716
  return False
@@ -125,6 +125,20 @@ _CODEX_ROLLOUT_PROBE_TTL_S = 2.0
125
125
  _SESSION_LEASE_PROBE_TTL_S = 2.0
126
126
 
127
127
 
128
+ def _tmux_batch_argv(commands: list[list[str]]) -> list[str]:
129
+ """Encode independent tmux commands into one client invocation."""
130
+ argv = ["tmux"]
131
+ first = True
132
+ for command in commands:
133
+ if not command:
134
+ continue
135
+ if not first:
136
+ argv.append(";")
137
+ argv.extend(command)
138
+ first = False
139
+ return argv
140
+
141
+
128
142
  PALETTE = [
129
143
  # Status-bar message levels. Idle tips are dim; info neutral green;
130
144
  # warn/error escalate so failures stand out from routine feedback.
@@ -769,6 +783,11 @@ class App:
769
783
  self._delete_progress_text: str = "Deleting…"
770
784
  self._rendered_status_text: str | None = None
771
785
  self._rendered_status_level: str = "tip"
786
+ # Applying one logical status-bar frame used to spawn five tmux
787
+ # clients. Keep the last complete frame so repeated focus/resize
788
+ # callbacks do not rewrite identical options, which is especially
789
+ # expensive under MSYS2 on Windows.
790
+ self._applied_tmux_bar_state: tuple[str, str, str, int, int] | None = None
772
791
  self._status_feedback_alarm: object | None = None
773
792
  self._attention_notice_key: tuple[str, int] | None = None
774
793
  self._tip_index: int = 0
@@ -857,6 +876,16 @@ class App:
857
876
  identity.server_digest, identity.pane_id)
858
877
  if identity is not None else None
859
878
  )
879
+ # MSYS2 pays tens of milliseconds for every tmux client process. The
880
+ # crash-safe global wheel/function leases are independent from provider
881
+ # discovery and the first sidebar frame, so Windows prepares them on a
882
+ # background worker while the UI becomes interactive. Teardown joins
883
+ # that worker before restoring any lease.
884
+ self._windows_tmux_setup_lock = threading.Lock()
885
+ self._windows_tmux_setup_thread: threading.Thread | None = None
886
+ self._windows_tmux_setup_result: tuple[bool, bool] | None = None
887
+ self._windows_tmux_setup_done = False
888
+ self._windows_tmux_setup_cancelled = False
860
889
  self._termux_local_touch = bool(
861
890
  identity is not None
862
891
  and is_termux_environment()
@@ -2467,8 +2496,16 @@ class App:
2467
2496
 
2468
2497
  def _install_tmux_bindings(self) -> None:
2469
2498
  """Ensure global forwarding and the current Target projection exist."""
2499
+ worker = getattr(self, "_windows_tmux_setup_thread", None)
2500
+ if worker is not None and worker.is_alive():
2501
+ return
2470
2502
  manager = getattr(self, "_tmux_binding_manager", None)
2471
2503
  opened = manager is not None and manager.open()
2504
+ self._finish_tmux_bindings(opened)
2505
+
2506
+ def _finish_tmux_bindings(self, opened: bool) -> None:
2507
+ """Project pane-local state after the shared lease is prepared."""
2508
+ manager = getattr(self, "_tmux_binding_manager", None)
2472
2509
  selection = getattr(self, "_selection_isolation_manager", None)
2473
2510
  if selection is not None:
2474
2511
  selection.sync(
@@ -2501,6 +2538,96 @@ class App:
2501
2538
  )
2502
2539
  self._sync_termux_tap_route()
2503
2540
 
2541
+ def _start_windows_tmux_setup(self) -> None:
2542
+ """Prepare expensive shared tmux leases without delaying first paint."""
2543
+ if not running_in_windows_wrapper():
2544
+ return
2545
+ current = getattr(self, "_windows_tmux_setup_thread", None)
2546
+ if current is not None:
2547
+ return
2548
+ wheel = getattr(self, "_root_wheel_manager", None)
2549
+ manager = getattr(self, "_tmux_binding_manager", None)
2550
+
2551
+ def cancelled() -> bool:
2552
+ with self._windows_tmux_setup_lock:
2553
+ return self._windows_tmux_setup_cancelled
2554
+
2555
+ def prepare() -> None:
2556
+ try:
2557
+ if cancelled():
2558
+ return
2559
+ try:
2560
+ wheel_ok = wheel is None or wheel.open()
2561
+ except Exception:
2562
+ # These leases improve mouse/status routing but are not
2563
+ # authority for provider discovery or history. Degrade to
2564
+ # the existing keyboard paths if an unexpected adapter
2565
+ # error escapes a fail-closed manager.
2566
+ wheel_ok = False
2567
+ if cancelled():
2568
+ return
2569
+ try:
2570
+ bindings_opened = manager is not None and manager.open()
2571
+ except Exception:
2572
+ bindings_opened = False
2573
+ with self._windows_tmux_setup_lock:
2574
+ if not self._windows_tmux_setup_cancelled:
2575
+ self._windows_tmux_setup_result = (
2576
+ wheel_ok, bindings_opened)
2577
+ finally:
2578
+ # Publish terminal completion even for a BaseException so the
2579
+ # Urwid callback cannot poll a dead worker forever.
2580
+ with self._windows_tmux_setup_lock:
2581
+ self._windows_tmux_setup_done = True
2582
+
2583
+ worker = threading.Thread(
2584
+ target=prepare,
2585
+ name="railmux-windows-tmux-setup",
2586
+ daemon=False,
2587
+ )
2588
+ self._windows_tmux_setup_thread = worker
2589
+ worker.start()
2590
+
2591
+ def _finish_windows_tmux_setup(self, loop, _user_data) -> None:
2592
+ """Consume background lease setup on the Urwid thread."""
2593
+ with self._windows_tmux_setup_lock:
2594
+ result = self._windows_tmux_setup_result
2595
+ done = self._windows_tmux_setup_done
2596
+ if result is not None:
2597
+ self._windows_tmux_setup_result = None
2598
+ if result is None:
2599
+ worker = getattr(self, "_windows_tmux_setup_thread", None)
2600
+ if not done and worker is not None and worker.is_alive():
2601
+ loop.set_alarm_in(0.05, self._finish_windows_tmux_setup)
2602
+ elif not done:
2603
+ # No worker was created, or it died before publishing its
2604
+ # terminal state. Preserve the pre-deferral one-shot setup
2605
+ # without leaving a 20 Hz alarm behind.
2606
+ self._install_tmux_bindings()
2607
+ return
2608
+ wheel_ok, bindings_opened = result
2609
+ if not wheel_ok:
2610
+ self._set_status(
2611
+ "Mouse-wheel forwarding unavailable; tmux may have custom "
2612
+ "root wheel bindings.",
2613
+ "warn",
2614
+ )
2615
+ self._finish_tmux_bindings(bindings_opened)
2616
+
2617
+ def _cancel_windows_tmux_setup(self) -> None:
2618
+ """Stop starting leases and wait before main-thread cleanup."""
2619
+ worker = getattr(self, "_windows_tmux_setup_thread", None)
2620
+ if worker is None:
2621
+ return
2622
+ with self._windows_tmux_setup_lock:
2623
+ self._windows_tmux_setup_cancelled = True
2624
+ # Waiting here gives teardown sole cleanup ownership and prevents a
2625
+ # late worker write from reinstalling bindings after their originals
2626
+ # were restored. These same manager operations ran synchronously before
2627
+ # deferral; a non-daemon worker keeps that cleanup guarantee if a later
2628
+ # teardown phase raises.
2629
+ worker.join()
2630
+
2504
2631
  def _set_workspace_target(self, slot_key: str) -> AgentSlot:
2505
2632
  """Apply one Target transition and refresh its tmux projection."""
2506
2633
  slot = self._agent_workspace().set_target(slot_key)
@@ -3062,6 +3189,19 @@ class App:
3062
3189
  workspace = self._agent_workspace()
3063
3190
  if workspace.presentation is presentation:
3064
3191
  return True
3192
+ has_agent_page = any(slot.pane_id for slot in workspace.slots)
3193
+ if not has_agent_page:
3194
+ # With only the sidebar pane there is no tmux page to select,
3195
+ # zoom, or preserve. Avoid a chain of geometry subprocesses on
3196
+ # initial compact terminals; the logical presentation still
3197
+ # drives the responsive footer and status bar.
3198
+ workspace.presentation = presentation
3199
+ workspace.compact_page = WorkspacePage.SIDEBAR
3200
+ self._pre_compact_wide_zoom_pane = None
3201
+ self._pre_compact_layout_profile = None
3202
+ self._remote_compact_prepared = None
3203
+ self._apply_tmux_bar(self._tmux_error_bar)
3204
+ return True
3065
3205
  if presentation is WorkspacePresentation.COMPACT:
3066
3206
  owner = getattr(self, "_railmux_pane_id", None)
3067
3207
  prepared = getattr(self, "_remote_compact_prepared", None)
@@ -8099,6 +8239,7 @@ class App:
8099
8239
  if isinstance(index, BackgroundCodexIndex):
8100
8240
  index.close(timeout_s=0.2)
8101
8241
  self._teardown_scroll_acceleration()
8242
+ self._cancel_windows_tmux_setup()
8102
8243
  selection = getattr(self, "_selection_isolation_manager", None)
8103
8244
  if selection is not None:
8104
8245
  selection.close()
@@ -10559,7 +10700,9 @@ class App:
10559
10700
  elif (size_changed and workspace.presentation
10560
10701
  is WorkspacePresentation.COMPACT):
10561
10702
  self._apply_tmux_bar(self._tmux_error_bar)
10703
+ has_agent_page = any(slot.pane_id for slot in workspace.slots)
10562
10704
  if (workspace.presentation is WorkspacePresentation.COMPACT
10705
+ and has_agent_page
10563
10706
  and self._window_is_zoomed() is False):
10564
10707
  # Retry a transient failed zoom and heal manual/unexpected unzoom;
10565
10708
  # compact presentation is defined by exactly one visible page.
@@ -10795,16 +10938,16 @@ class App:
10795
10938
  self._rendered_status_level = level
10796
10939
  try:
10797
10940
  import subprocess as _sp
10798
- _sp.run(
10799
- ["tmux", "set-option", "-t", self._tmux_status_session,
10941
+ commands = [
10942
+ ["set-option", "-t", self._tmux_status_session,
10800
10943
  "status-right", payload],
10944
+ ]
10945
+ if refresh:
10946
+ commands.append(["refresh-client", "-S"])
10947
+ _sp.run(
10948
+ _tmux_batch_argv(commands),
10801
10949
  stdout=_sp.DEVNULL, stderr=_sp.DEVNULL,
10802
10950
  )
10803
- if refresh:
10804
- _sp.run(
10805
- ["tmux", "refresh-client", "-S"],
10806
- stdout=_sp.DEVNULL, stderr=_sp.DEVNULL,
10807
- )
10808
10951
  except Exception:
10809
10952
  pass
10810
10953
 
@@ -10880,23 +11023,35 @@ class App:
10880
11023
  self._status_layout_indicator(),
10881
11024
  wrap_action,
10882
11025
  )
11026
+ state = (
11027
+ self._tmux_status_session,
11028
+ bar,
11029
+ brand,
11030
+ left_length,
11031
+ right_length,
11032
+ )
11033
+ if getattr(self, "_applied_tmux_bar_state", None) == state:
11034
+ return
10883
11035
  try:
10884
11036
  import subprocess as _sp
10885
- for opt, val in (
10886
- ("status-style", bar),
10887
- ("status-left", brand),
10888
- ("status-left-length", str(left_length)),
10889
- ("status-right-length", str(right_length)),
10890
- ):
10891
- _sp.run(
10892
- ["tmux", "set-option", "-t", self._tmux_status_session,
10893
- opt, val],
10894
- stdout=_sp.DEVNULL, stderr=_sp.DEVNULL,
11037
+ commands = [
11038
+ ["set-option", "-t", self._tmux_status_session, opt, val]
11039
+ for opt, val in (
11040
+ ("status-style", bar),
11041
+ ("status-left", brand),
11042
+ ("status-left-length", str(left_length)),
11043
+ ("status-right-length", str(right_length)),
10895
11044
  )
11045
+ ]
10896
11046
  # Force an immediate repaint (default status-interval is 15s) so a
10897
11047
  # mode toggle or error flip shows at once, not on the next tick.
10898
- _sp.run(["tmux", "refresh-client", "-S"],
10899
- stdout=_sp.DEVNULL, stderr=_sp.DEVNULL)
11048
+ commands.append(["refresh-client", "-S"])
11049
+ result = _sp.run(
11050
+ _tmux_batch_argv(commands),
11051
+ stdout=_sp.DEVNULL, stderr=_sp.DEVNULL,
11052
+ )
11053
+ if getattr(result, "returncode", 0) == 0:
11054
+ self._applied_tmux_bar_state = state
10900
11055
  except Exception:
10901
11056
  pass
10902
11057
 
@@ -11040,28 +11195,31 @@ class App:
11040
11195
  if tmux_ctl.in_tmux():
11041
11196
  sess = tmux_ctl.current_session_name() or "railmux"
11042
11197
  _sp.run(
11043
- ["tmux", "set-option", "-t", sess, "set-clipboard", "on"],
11198
+ _tmux_batch_argv([
11199
+ ["set-option", "-t", sess, "set-clipboard", "on"],
11200
+ ["set-option", "-t", sess, "focus-events", "on"],
11201
+ ["set-option", "-t", sess, "mouse", "on"],
11202
+ ]),
11044
11203
  stdout=_sp.DEVNULL, stderr=_sp.DEVNULL,
11045
11204
  )
11046
11205
  # Force OSC 52 passthrough so a left-drag selection in either pane
11047
11206
  # copies to the *local* system clipboard (works over SSH / nested
11048
11207
  # tmux on OSC-52-capable terminals). Pairs with set-clipboard on.
11049
11208
  tmux_ctl.enable_clipboard_passthrough()
11050
- _sp.run(
11051
- ["tmux", "set-option", "-t", sess, "focus-events", "on"],
11052
- stdout=_sp.DEVNULL, stderr=_sp.DEVNULL,
11053
- )
11054
- _sp.run(
11055
- ["tmux", "set-option", "-t", sess, "mouse", "on"],
11056
- stdout=_sp.DEVNULL, stderr=_sp.DEVNULL,
11057
- )
11058
- wheel = getattr(self, "_root_wheel_manager", None)
11059
- if wheel is not None and not wheel.open():
11060
- self._set_status(
11061
- "Mouse-wheel forwarding unavailable; tmux may have "
11062
- "custom root wheel bindings.",
11063
- "warn",
11064
- )
11209
+ if running_in_windows_wrapper():
11210
+ # Root-table lease discovery is particularly expensive in
11211
+ # MSYS2 because each tmux query launches a Windows process.
11212
+ # It is independent of provider/session restoration, so
11213
+ # finish it after the first interactive frame.
11214
+ self._start_windows_tmux_setup()
11215
+ else:
11216
+ wheel = getattr(self, "_root_wheel_manager", None)
11217
+ if wheel is not None and not wheel.open():
11218
+ self._set_status(
11219
+ "Mouse-wheel forwarding unavailable; tmux may "
11220
+ "have custom root wheel bindings.",
11221
+ "warn",
11222
+ )
11065
11223
  self._railmux_pane_id = tmux_ctl.current_pane_id()
11066
11224
  if (self._railmux_pane_id is not None
11067
11225
  and tmux_ctl.current_session_name() == "railmux"
@@ -11077,7 +11235,8 @@ class App:
11077
11235
  # a saved agent-focused workspace will replace that temporary
11078
11236
  # topology shortly. Do not expose its misleading row focus.
11079
11237
  self._frame.set_window_active(False)
11080
- self._install_tmux_bindings()
11238
+ if getattr(self, "_windows_tmux_setup_thread", None) is None:
11239
+ self._install_tmux_bindings()
11081
11240
  # bracketed_paste_mode: the terminal frames pastes in begin/end markers
11082
11241
  # so _filter_input can drop them — sidebar keys are destructive commands,
11083
11242
  # not text input.
@@ -11111,11 +11270,13 @@ class App:
11111
11270
  # visually clean. These overrides remain session-scoped and
11112
11271
  # teardown reverts them to the dedicated server's hidden
11113
11272
  # baseline.
11114
- for opt, val in self._TMUX_BAR_OPTIONS:
11115
- _sp.run(
11116
- ["tmux", "set-option", "-t", sess, opt, val],
11117
- stdout=_sp.DEVNULL, stderr=_sp.DEVNULL,
11118
- )
11273
+ _sp.run(
11274
+ _tmux_batch_argv([
11275
+ ["set-option", "-t", sess, opt, val]
11276
+ for opt, val in self._TMUX_BAR_OPTIONS
11277
+ ]),
11278
+ stdout=_sp.DEVNULL, stderr=_sp.DEVNULL,
11279
+ )
11119
11280
  self._tmux_status_session = sess
11120
11281
  self._tmux_status_enabled = True
11121
11282
  self._apply_tmux_bar(error=False) # initial green bar
@@ -11144,6 +11305,12 @@ class App:
11144
11305
  if running_in_windows_wrapper():
11145
11306
  self._loop.set_alarm_in(
11146
11307
  0.05, self._report_windows_startup_summary)
11308
+ # Leave one bounded initial-paint interval before consuming
11309
+ # the lease result. Even if the worker completed immediately,
11310
+ # its status/bar projection cannot delay the first frame.
11311
+ if self._windows_tmux_setup_thread is not None:
11312
+ self._loop.set_alarm_in(
11313
+ 0.1, self._finish_windows_tmux_setup)
11147
11314
  self._loop.run()
11148
11315
  except KeyboardInterrupt:
11149
11316
  # Ctrl-C / SIGINT — fall through to teardown.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.4.0.dev16
3
+ Version: 0.4.0.dev17
4
4
  Summary: Terminal UI to navigate, resume, and start Claude Code and Codex sessions
5
5
  Author-email: Rightglow <zhang.taian@foxmail.com>
6
6
  License-Expression: MIT
@@ -3576,18 +3576,50 @@ def test_periodic_prefetch_never_moves_an_existing_frozen_viewport():
3576
3576
 
3577
3577
  assert action == HistoryAction()
3578
3578
  assert view.overlays() == before
3579
- # A redraw without safe anchors updates only the mutable live viewport;
3580
- # it must not erase already captured history behind the frozen view.
3581
- assert view.content_cache["%8"].lines == (
3582
- b"old-0",
3583
- b"old-1",
3584
- b"old-2",
3585
- b"old-3",
3586
- b"old-4",
3587
- b"new-5",
3588
- b"new-6",
3589
- b"new-7",
3579
+ # A redraw without safe anchors cannot be spliced onto the old capture:
3580
+ # doing so would manufacture a false seam and omit intermediate rows. The
3581
+ # immutable frozen viewport remains visible, while the next history entry
3582
+ # starts from the newest internally contiguous capture.
3583
+ assert view.content_cache["%8"].lines == advanced.lines
3584
+
3585
+
3586
+ def test_unaligned_hot_prefetch_never_creates_a_discontinuous_history_seam():
3587
+ view = LocalHistoryView(history_limit=2000)
3588
+ first = InputFrameDecoder().feed(view.begin_prefetch(1.0))[0]
3589
+ first_id, _limit = decode_history_prefetch(first.data)
3590
+ old = HistorySnapshot(
3591
+ first_id,
3592
+ "%8",
3593
+ 30,
3594
+ 0,
3595
+ 30,
3596
+ 3,
3597
+ tuple(f"old-{index}".encode() for index in range(300)),
3598
+ more_available=True,
3599
+ )
3600
+ view.accept_prefetch(HistoryBatch(first_id, (old,)))
3601
+
3602
+ second = InputFrameDecoder().feed(view.begin_prefetch(2.0))[0]
3603
+ second_id, _limit = decode_history_prefetch(second.data)
3604
+ current = HistorySnapshot(
3605
+ second_id,
3606
+ "%8",
3607
+ 30,
3608
+ 0,
3609
+ 30,
3610
+ 3,
3611
+ tuple(f"new-{index}".encode() for index in range(300)),
3612
+ more_available=True,
3590
3613
  )
3614
+ view.accept_prefetch(HistoryBatch(second_id, (current,)))
3615
+
3616
+ wheel = view.wheel(SgrMouseEvent(b"up", 64, 40, 2, True))
3617
+
3618
+ assert view.viewports["%8"].snapshot.lines == current.lines
3619
+ assert view.overlays()[0][1] == (b"new-296", b"new-297", b"new-298")
3620
+ assert b"old-296" not in view.viewports["%8"].snapshot.lines
3621
+ request = InputFrameDecoder().feed(wheel.protocol_frame)[0]
3622
+ assert decode_history_request(request.data)[3] == 2000
3591
3623
 
3592
3624
 
3593
3625
  def test_temporary_blank_fullscreen_tail_is_removed_after_main_view_returns():