railmux 0.4.0.dev17__tar.gz → 0.4.0.dev19__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.dev17/src/railmux.egg-info → railmux-0.4.0.dev19}/PKG-INFO +1 -1
  2. railmux-0.4.0.dev19/src/railmux/__init__.py +1 -0
  3. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/cli.py +45 -13
  4. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/tmux_server.py +84 -0
  5. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/ui/app.py +32 -5
  6. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19/src/railmux.egg-info}/PKG-INFO +1 -1
  7. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_cli.py +114 -2
  8. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_fullscreen_binding.py +61 -0
  9. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_tmux_server.py +135 -0
  10. railmux-0.4.0.dev17/src/railmux/__init__.py +0 -1
  11. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/LICENSE +0 -0
  12. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/MANIFEST.in +0 -0
  13. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/README.md +0 -0
  14. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/pyproject.toml +0 -0
  15. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/setup.cfg +0 -0
  16. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/__main__.py +0 -0
  17. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/atomic_file.py +0 -0
  18. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/background_index.py +0 -0
  19. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/codex_index.py +0 -0
  20. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/config.py +0 -0
  21. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/config_cli.py +0 -0
  22. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/diagnostics.py +0 -0
  23. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/discovery.py +0 -0
  24. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/display_transport.py +0 -0
  25. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/entrypoint.py +0 -0
  26. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/fast_display_client.py +0 -0
  27. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/fast_display_history.py +0 -0
  28. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/fast_display_input.py +0 -0
  29. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/fast_display_protocol.py +0 -0
  30. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/fast_display_server.py +0 -0
  31. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/favorites.py +0 -0
  32. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/fuzzy.py +0 -0
  33. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/help_workspace.py +0 -0
  34. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/launcher.py +0 -0
  35. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/legacy_sessions.py +0 -0
  36. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/local_clipboard.py +0 -0
  37. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/local_open.py +0 -0
  38. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/models.py +0 -0
  39. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/modes.py +0 -0
  40. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/mouse_manager.py +0 -0
  41. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/orphan_marker.py +0 -0
  42. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/pane_surface.py +0 -0
  43. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/path_codec.py +0 -0
  44. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/provider_paths.py +0 -0
  45. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/remote_config.py +0 -0
  46. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/renames.py +0 -0
  47. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/restart_state.py +0 -0
  48. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/runtime_config.py +0 -0
  49. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/scroll_agent.py +0 -0
  50. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/scroll_manager.py +0 -0
  51. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/selection_isolation.py +0 -0
  52. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/self_update.py +0 -0
  53. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/session_cache.py +0 -0
  54. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/session_index.py +0 -0
  55. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/session_lease.py +0 -0
  56. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/setting_contracts.py +0 -0
  57. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/settings.py +0 -0
  58. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/ssh_args.py +0 -0
  59. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/ssh_compat.py +0 -0
  60. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/ssh_display_diagnostics.py +0 -0
  61. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/ssh_doctor.py +0 -0
  62. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/system_deps.py +0 -0
  63. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/terminal_status.py +0 -0
  64. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/tmux_binding_manager.py +0 -0
  65. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/tmux_ctl.py +0 -0
  66. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/tmux_health.py +0 -0
  67. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/tool_panes.py +0 -0
  68. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/transcript.py +0 -0
  69. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/ui/__init__.py +0 -0
  70. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/ui/_widgets.py +0 -0
  71. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/ui/keymap.py +0 -0
  72. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/ui/modals.py +0 -0
  73. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/ui/projects_pane.py +0 -0
  74. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/ui/running_pane.py +0 -0
  75. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/ui/sessions_pane.py +0 -0
  76. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/ui/sidebar.py +0 -0
  77. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/ui/statusbar.py +0 -0
  78. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/ui/workspace.py +0 -0
  79. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/windows_attach_relay.py +0 -0
  80. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/windows_bootstrap.py +0 -0
  81. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/windows_install_log.py +0 -0
  82. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/windows_msys2.py +0 -0
  83. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/windows_pacman.py +0 -0
  84. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux/windows_tmux_lifecycle.py +0 -0
  85. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux.egg-info/SOURCES.txt +0 -0
  86. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux.egg-info/dependency_links.txt +0 -0
  87. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux.egg-info/entry_points.txt +0 -0
  88. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux.egg-info/requires.txt +0 -0
  89. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/src/railmux.egg-info/top_level.txt +0 -0
  90. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_agent_workspace.py +0 -0
  91. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_atomic_file.py +0 -0
  92. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_background_index.py +0 -0
  93. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_codex_delete.py +0 -0
  94. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_codex_index.py +0 -0
  95. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_codex_lineage.py +0 -0
  96. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_codex_transcript.py +0 -0
  97. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_config.py +0 -0
  98. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_config_cli.py +0 -0
  99. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_denested_transport_benchmark.py +0 -0
  100. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_diagnostics.py +0 -0
  101. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_discovery.py +0 -0
  102. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_display_transport.py +0 -0
  103. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_fast_full_window.py +0 -0
  104. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_focus_highlight.py +0 -0
  105. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_fuzzy.py +0 -0
  106. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_help_agent.py +0 -0
  107. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_help_workspace.py +0 -0
  108. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_history_cleanup.py +0 -0
  109. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_keymap.py +0 -0
  110. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_launcher.py +0 -0
  111. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_layout_profile.py +0 -0
  112. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_legacy_sessions.py +0 -0
  113. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_local_clipboard.py +0 -0
  114. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_local_open.py +0 -0
  115. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_modals.py +0 -0
  116. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_mode_selection.py +0 -0
  117. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_modes.py +0 -0
  118. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_mouse_manager.py +0 -0
  119. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_new_project.py +0 -0
  120. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_options.py +0 -0
  121. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_orphan_recovery.py +0 -0
  122. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_pane_surface.py +0 -0
  123. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_path_codec.py +0 -0
  124. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_projects_pane.py +0 -0
  125. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_provider_paths.py +0 -0
  126. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_release_notes.py +0 -0
  127. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_release_workflow.py +0 -0
  128. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_remote_config.py +0 -0
  129. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_renames.py +0 -0
  130. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_restart_state.py +0 -0
  131. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_running_pane.py +0 -0
  132. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_running_sort.py +0 -0
  133. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_runtime_config.py +0 -0
  134. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_scroll_agent.py +0 -0
  135. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_scroll_manager.py +0 -0
  136. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_selection_isolation.py +0 -0
  137. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_self_update.py +0 -0
  138. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_session_cache.py +0 -0
  139. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_session_index.py +0 -0
  140. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_session_lease.py +0 -0
  141. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_sessions_pane.py +0 -0
  142. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_setting_contracts.py +0 -0
  143. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_settings.py +0 -0
  144. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_sidebar.py +0 -0
  145. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_soft_quit.py +0 -0
  146. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_ssh_args.py +0 -0
  147. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_ssh_compat.py +0 -0
  148. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_ssh_display_benchmark.py +0 -0
  149. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_ssh_display_diagnostics.py +0 -0
  150. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_ssh_doctor.py +0 -0
  151. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_status.py +0 -0
  152. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_statusbar.py +0 -0
  153. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_system_deps.py +0 -0
  154. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_terminal_size.py +0 -0
  155. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_terminal_status.py +0 -0
  156. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_termux_local_touch.py +0 -0
  157. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_tmux_binding_manager.py +0 -0
  158. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_tmux_ctl.py +0 -0
  159. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_tmux_health.py +0 -0
  160. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_tmux_integration.py +0 -0
  161. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_tmux_status.py +0 -0
  162. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_tool_panes.py +0 -0
  163. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_transcript.py +0 -0
  164. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_utf8_state.py +0 -0
  165. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_web_demo_recorder.py +0 -0
  166. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_wheel_smoke.py +0 -0
  167. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_widgets.py +0 -0
  168. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_windows_attach_relay.py +0 -0
  169. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_windows_bootstrap.py +0 -0
  170. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_windows_install_log.py +0 -0
  171. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_windows_msys2.py +0 -0
  172. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_windows_pacman.py +0 -0
  173. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_windows_tmux_lifecycle.py +0 -0
  174. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_windows_wrapper_contract.py +0 -0
  175. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tests/test_yolo_prompt.py +0 -0
  176. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tools/denested_transport_benchmark.py +0 -0
  177. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/tools/release_notes.py +0 -0
  178. {railmux-0.4.0.dev17 → railmux-0.4.0.dev19}/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.dev17
3
+ Version: 0.4.0.dev19
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.dev19"
@@ -26,6 +26,7 @@ from railmux import tmux_server
26
26
  from railmux import windows_tmux_lifecycle
27
27
  from railmux.system_deps import ensure_tmux_available
28
28
  from railmux.ssh_args import AppendSshArgument, ExtendSshArguments
29
+ from railmux.terminal_status import command_status
29
30
 
30
31
 
31
32
  def _show_startup_message(
@@ -85,6 +86,9 @@ def _run_tmux_client_with_watchdog(
85
86
  expected_session_id: str | None = None,
86
87
  ) -> int:
87
88
  """Keep one monitor outside tmux so a frozen server cannot trap the TTY."""
89
+ from railmux.provider_paths import running_in_managed_windows_wrapper
90
+
91
+ managed_windows = running_in_managed_windows_wrapper()
88
92
  attributes = None
89
93
  if sys.stdin.isatty():
90
94
  try:
@@ -93,7 +97,14 @@ def _run_tmux_client_with_watchdog(
93
97
  pass
94
98
  started_at = time.monotonic()
95
99
  try:
96
- process: object = subprocess.Popen(argv, env=env)
100
+ popen_kwargs: dict[str, object] = {"env": env}
101
+ if managed_windows and expected_target is not None:
102
+ # A fast direct-client rejection is an implementation detail when
103
+ # the server-origin bridge succeeds. Suppress tmux's raw
104
+ # ``open terminal failed`` line and emit one Railmux-owned status
105
+ # below; bridge failures still receive an actionable error.
106
+ popen_kwargs["stderr"] = subprocess.DEVNULL
107
+ process: object = subprocess.Popen(argv, **popen_kwargs)
97
108
  except OSError as exc:
98
109
  print(f"error: could not start tmux client: {exc}", file=sys.stderr)
99
110
  return 2
@@ -204,10 +215,6 @@ def _run_tmux_client_with_watchdog(
204
215
  expected_session_id = tmux_server.target_session_id(
205
216
  expected_target, "railmux", timeout=0.5)
206
217
 
207
- from railmux.provider_paths import running_in_managed_windows_wrapper
208
-
209
- managed_windows = running_in_managed_windows_wrapper()
210
-
211
218
  can_relay = bool(
212
219
  current_target == expected_target
213
220
  and expected_session_id is not None
@@ -223,13 +230,7 @@ def _run_tmux_client_with_watchdog(
223
230
  start_relay_client,
224
231
  )
225
232
 
226
- print(
227
- "info: direct tmux attach was unavailable; reconnecting "
228
- "through the Windows terminal bridge",
229
- file=sys.stderr,
230
- )
231
233
  try:
232
- tty.setraw(sys.stdin.fileno())
233
234
  relay = start_relay_client(
234
235
  target=expected_target,
235
236
  session_id=expected_session_id,
@@ -237,6 +238,15 @@ def _run_tmux_client_with_watchdog(
237
238
  stdin_fd=sys.stdin.fileno(),
238
239
  stdout_fd=sys.stdout.fileno(),
239
240
  )
241
+ print(
242
+ command_status(
243
+ "info",
244
+ "connected through the Windows terminal bridge",
245
+ stream=sys.stderr,
246
+ ),
247
+ file=sys.stderr,
248
+ )
249
+ tty.setraw(sys.stdin.fileno())
240
250
  process = relay
241
251
  returncode, watchdog_failed = monitor_client(
242
252
  asynchronous_probe=True)
@@ -296,6 +306,14 @@ def _run_tmux_client_with_watchdog(
296
306
  ),
297
307
  consecutive_failures=1,
298
308
  )
309
+ if not relay_attempted:
310
+ print(
311
+ "error: direct tmux attach was rejected and the "
312
+ "Windows terminal bridge was unavailable; the "
313
+ "existing workspace was left running; run "
314
+ "'railmux doctor' for diagnostics",
315
+ file=sys.stderr,
316
+ )
299
317
  # Routine post-exit removal of a proof-authorized last-session socket
300
318
  # is housekeeping. Only pre-launch recovery is user-visible.
301
319
  windows_tmux_lifecycle.recover_abandoned_socket()
@@ -593,14 +611,28 @@ def main(argv: list[str] | None = None) -> int:
593
611
  if Path(sys.argv[0]).name == "__main__.py"
594
612
  else [sys.argv[0]]
595
613
  )
596
- cmd = tmux_server.launcher_argv(launch_prefix, raw_args)
614
+ client_env = tmux_server.exec_environment()
597
615
  dedicated_session_id = (
598
616
  tmux_server.target_session_id(dedicated_target, "railmux")
599
617
  if dedicated_target is not None else None
600
618
  )
619
+ if dedicated_target is not None and dedicated_session_id is None:
620
+ from railmux.provider_paths import (
621
+ running_in_managed_windows_wrapper,
622
+ )
623
+ if running_in_managed_windows_wrapper():
624
+ dedicated_session_id = (
625
+ tmux_server.ensure_detached_launcher_session(
626
+ dedicated_target,
627
+ launch_prefix,
628
+ raw_args,
629
+ env=client_env,
630
+ )
631
+ )
632
+ cmd = tmux_server.launcher_argv(launch_prefix, raw_args)
601
633
  return _run_tmux_client_with_watchdog(
602
634
  cmd,
603
- tmux_server.exec_environment(),
635
+ client_env,
604
636
  expected_target=dedicated_target,
605
637
  expected_session_id=dedicated_session_id,
606
638
  )
@@ -645,3 +645,87 @@ def launcher_argv(
645
645
  "--inside-tmux",
646
646
  *forwarded_args,
647
647
  )
648
+
649
+
650
+ _MANAGED_WINDOWS_SESSION_ENV = (
651
+ "RAILMUX_WINDOWS_RUNTIME",
652
+ "RAILMUX_MSYS2_RUNTIME_ID",
653
+ "RAILMUX_MSYS2_APP_ID",
654
+ )
655
+ _MANAGED_WINDOWS_SESSION_VALUE_RE = re.compile(
656
+ r"[A-Za-z0-9_.-]{1,128}\Z"
657
+ )
658
+
659
+
660
+ def _managed_windows_session_environment_args(
661
+ env: Mapping[str, str] | None,
662
+ ) -> list[str]:
663
+ """Return bounded, non-secret tmux ``new-session -e`` arguments.
664
+
665
+ A detached outer session can be created by an existing dedicated server
666
+ whose inherited environment belongs to an older Railmux app layer. Pass
667
+ only the three independently verified managed-runtime identity hints to
668
+ the new pane; provider credentials and the rest of the caller environment
669
+ must remain outside tmux metadata.
670
+ """
671
+ if env is None or env.get("RAILMUX_WINDOWS_RUNTIME") != "msys2":
672
+ return []
673
+ result: list[str] = []
674
+ for name in _MANAGED_WINDOWS_SESSION_ENV:
675
+ value = env.get(name)
676
+ if (not isinstance(value, str)
677
+ or _MANAGED_WINDOWS_SESSION_VALUE_RE.fullmatch(value) is None):
678
+ continue
679
+ result.extend(["-e", f"{name}={value}"])
680
+ return result
681
+
682
+
683
+ def ensure_detached_launcher_session(
684
+ target: TmuxServerTarget,
685
+ launch_prefix: Sequence[str],
686
+ forwarded_args: Sequence[str],
687
+ *,
688
+ env: Mapping[str, str] | None = None,
689
+ timeout: float = 5.0,
690
+ ) -> str | None:
691
+ """Create the outer UI detached on one already-proven server.
692
+
693
+ Managed Windows uses this only when a proven existing server has no outer
694
+ UI, commonly because Soft Quit left provider sessions alive in another
695
+ Terminal Services session. A new tmux client can then reject
696
+ ``new-session -A`` before the outer session exists, so the server-origin
697
+ terminal bridge has no immutable session to attach.
698
+
699
+ An exact pre/post server-PID check fences the mutation. A concurrent
700
+ launcher may win the create race; resolving one exact resulting session is
701
+ success regardless of this client's ``new-session`` return code.
702
+ """
703
+ existing = target_session_id(target, "railmux", timeout=0.5)
704
+ if existing is not None:
705
+ return existing
706
+ if not launch_prefix or not target_is_live(target, timeout=1.0, env=env):
707
+ return None
708
+ try:
709
+ subprocess.run(
710
+ target_argv(
711
+ target,
712
+ "new-session",
713
+ "-d",
714
+ "-s",
715
+ "railmux",
716
+ *_managed_windows_session_environment_args(env),
717
+ *launch_prefix,
718
+ "--inside-tmux",
719
+ *forwarded_args,
720
+ ),
721
+ stdout=subprocess.DEVNULL,
722
+ stderr=subprocess.DEVNULL,
723
+ timeout=timeout,
724
+ check=False,
725
+ env=None if env is None else dict(env),
726
+ )
727
+ except (OSError, subprocess.TimeoutExpired):
728
+ return None
729
+ if not target_is_live(target, timeout=1.0, env=env):
730
+ return None
731
+ return target_session_id(target, "railmux", timeout=1.0)
@@ -902,6 +902,7 @@ class App:
902
902
  self._projected_target_pane_id: str | None = None
903
903
  self._target_toggle_warning_shown = False
904
904
  self._status_navigation_warning_shown = False
905
+ self._status_navigation_projected = False
905
906
  self._teardown_core_done: bool = False
906
907
  self._outer_teardown_done: bool = False
907
908
  self._exit_in_progress: bool = False
@@ -2523,11 +2524,7 @@ class App:
2523
2524
  "warn",
2524
2525
  )
2525
2526
  if manager.status_navigation_available:
2526
- # The initial bar is painted before the shared binding lease is
2527
- # acquired. Repaint once ownership is known so its wide
2528
- # Mode/Layout ranges (and compact page ranges) are immediately
2529
- # live rather than waiting for the next unrelated transition.
2530
- self._apply_tmux_bar(self._tmux_error_bar)
2527
+ self._project_status_navigation()
2531
2528
  elif (running_in_windows_wrapper()
2532
2529
  and not self._status_navigation_warning_shown):
2533
2530
  self._status_navigation_warning_shown = True
@@ -2538,6 +2535,35 @@ class App:
2538
2535
  )
2539
2536
  self._sync_termux_tap_route()
2540
2537
 
2538
+ def _project_status_navigation(self) -> None:
2539
+ """Publish clickable ranges after both lease and status bar exist.
2540
+
2541
+ The Windows worker can finish before ``run`` enables the session-local
2542
+ bar. Its immediate projection is then intentionally a no-op, so the
2543
+ periodic caller retries until the first live bar has received both its
2544
+ left navigation and unchanged right-side Copy range.
2545
+ """
2546
+ if getattr(self, "_status_navigation_projected", False):
2547
+ return
2548
+ manager = getattr(self, "_tmux_binding_manager", None)
2549
+ if (not getattr(self, "_tmux_status_enabled", False)
2550
+ or getattr(manager, "status_navigation_available", False)
2551
+ is not True):
2552
+ return
2553
+ # The initial bar is painted before the shared binding lease is
2554
+ # acquired. Invalidate its cache so the same visible labels are still
2555
+ # rewritten with the newly available range metadata.
2556
+ self._applied_tmux_bar_state = None
2557
+ self._apply_tmux_bar(self._tmux_error_bar)
2558
+ status_text = getattr(self, "_rendered_status_text", "")
2559
+ if status_text:
2560
+ self._render_status_to_tmux(
2561
+ status_text,
2562
+ getattr(self, "_rendered_status_level", "info"),
2563
+ remember=False,
2564
+ )
2565
+ self._status_navigation_projected = True
2566
+
2541
2567
  def _start_windows_tmux_setup(self) -> None:
2542
2568
  """Prepare expensive shared tmux leases without delaying first paint."""
2543
2569
  if not running_in_windows_wrapper():
@@ -11154,6 +11180,7 @@ class App:
11154
11180
 
11155
11181
  def _on_tick(self, loop, _user_data) -> None:
11156
11182
  self._refresh()
11183
+ self._project_status_navigation()
11157
11184
  if self._pending_restore_state is not None:
11158
11185
  # A portable Codex preview may have been waiting for the first
11159
11186
  # immutable history generation even when no live recovery
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.4.0.dev17
3
+ Version: 0.4.0.dev19
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
@@ -1,4 +1,5 @@
1
1
  import os
2
+ import subprocess
2
3
  from unittest.mock import MagicMock, patch
3
4
 
4
5
  import pytest
@@ -472,6 +473,73 @@ def test_prelaunch_recovery_is_scoped_to_the_dedicated_server(monkeypatch):
472
473
  assert os.environ["TMUX_PANE"] == "%9"
473
474
 
474
475
 
476
+ def test_managed_windows_precreates_missing_outer_session_for_bridge(
477
+ monkeypatch,
478
+ ):
479
+ target = TmuxServerTarget("/tmp/tmux-private/railmux", 789)
480
+ monkeypatch.setattr(
481
+ "railmux.cli.tmux_server.discover_target", lambda: target)
482
+ monkeypatch.setattr(
483
+ "railmux.cli.tmux_server.is_current_server", lambda _target: False)
484
+ monkeypatch.setattr(
485
+ "railmux.cli.tmux_server.target_session_id", lambda *_a, **_kw: None)
486
+ monkeypatch.setattr("railmux.cli.sys.argv", ["/opt/railmux/bin/railmux"])
487
+ monkeypatch.setattr(
488
+ "railmux.provider_paths.running_in_managed_windows_wrapper",
489
+ lambda: True,
490
+ )
491
+ prepared = MagicMock(return_value="$9")
492
+ monkeypatch.setattr(
493
+ "railmux.cli.tmux_server.ensure_detached_launcher_session", prepared)
494
+ run_client = MagicMock(return_value=0)
495
+ monkeypatch.setattr(
496
+ "railmux.cli._run_tmux_client_with_watchdog", run_client)
497
+
498
+ assert main(["--project", "/work"]) == 0
499
+
500
+ client_env = tmux_server.exec_environment()
501
+ prepared.assert_called_once_with(
502
+ target,
503
+ ["/opt/railmux/bin/railmux"],
504
+ ["--project", "/work"],
505
+ env=client_env,
506
+ )
507
+ assert run_client.call_args.kwargs == {
508
+ "expected_target": target,
509
+ "expected_session_id": "$9",
510
+ }
511
+
512
+
513
+ def test_posix_missing_outer_session_keeps_ordinary_new_session_path(
514
+ monkeypatch,
515
+ ):
516
+ target = TmuxServerTarget("/tmp/tmux-private/railmux", 789)
517
+ monkeypatch.setattr(
518
+ "railmux.cli.tmux_server.discover_target", lambda: target)
519
+ monkeypatch.setattr(
520
+ "railmux.cli.tmux_server.is_current_server", lambda _target: False)
521
+ monkeypatch.setattr(
522
+ "railmux.cli.tmux_server.target_session_id", lambda *_a, **_kw: None)
523
+ monkeypatch.setattr(
524
+ "railmux.provider_paths.running_in_managed_windows_wrapper",
525
+ lambda: False,
526
+ )
527
+ prepared = MagicMock()
528
+ monkeypatch.setattr(
529
+ "railmux.cli.tmux_server.ensure_detached_launcher_session", prepared)
530
+ run_client = MagicMock(return_value=0)
531
+ monkeypatch.setattr(
532
+ "railmux.cli._run_tmux_client_with_watchdog", run_client)
533
+
534
+ assert main([]) == 0
535
+
536
+ prepared.assert_not_called()
537
+ assert run_client.call_args.kwargs == {
538
+ "expected_target": target,
539
+ "expected_session_id": None,
540
+ }
541
+
542
+
475
543
  def test_local_tmux_watchdog_exits_and_records_after_consecutive_failures(
476
544
  monkeypatch, capsys,
477
545
  ):
@@ -641,8 +709,8 @@ def test_managed_windows_fast_attach_rejection_uses_one_terminal_bridge(
641
709
  self.closed = True
642
710
 
643
711
  relay = SuccessfulRelay()
644
- monkeypatch.setattr(
645
- "railmux.cli.subprocess.Popen", lambda *_args, **_kwargs: ExitedClient())
712
+ popen = MagicMock(return_value=ExitedClient())
713
+ monkeypatch.setattr("railmux.cli.subprocess.Popen", popen)
646
714
  monkeypatch.setattr("railmux.cli.sys.stdin.isatty", lambda: True)
647
715
  monkeypatch.setattr("railmux.cli.sys.stdout.isatty", lambda: True)
648
716
  monkeypatch.setattr("railmux.cli.sys.stdin.fileno", lambda: 10)
@@ -682,11 +750,55 @@ def test_managed_windows_fast_attach_rejection_uses_one_terminal_bridge(
682
750
  assert relay.closed
683
751
  recover.assert_called_once_with()
684
752
  record.assert_not_called()
753
+ assert popen.call_args.kwargs["stderr"] is subprocess.DEVNULL
685
754
  stderr = capsys.readouterr().err
686
755
  assert "Windows terminal bridge" in stderr
756
+ assert "direct tmux attach was unavailable" not in stderr
757
+ assert "open terminal failed" not in stderr
687
758
  assert "cleaned an abandoned" not in stderr
688
759
 
689
760
 
761
+ def test_managed_windows_attach_without_bridge_is_actionable(
762
+ monkeypatch, capsys,
763
+ ):
764
+ target = TmuxServerTarget("/tmp/railmux", 77)
765
+
766
+ class ExitedClient:
767
+ returncode = 1
768
+
769
+ def poll(self):
770
+ return self.returncode
771
+
772
+ popen = MagicMock(return_value=ExitedClient())
773
+ monkeypatch.setattr("railmux.cli.subprocess.Popen", popen)
774
+ monkeypatch.setattr("railmux.cli.sys.stdin.isatty", lambda: False)
775
+ monkeypatch.setattr(
776
+ "railmux.cli.tmux_server.discover_target", lambda **_kw: target)
777
+ monkeypatch.setattr(
778
+ "railmux.provider_paths.running_in_managed_windows_wrapper",
779
+ lambda: True,
780
+ )
781
+ record = MagicMock(return_value=True)
782
+ monkeypatch.setattr("railmux.cli.tmux_health.record_incident", record)
783
+
784
+ assert _run_tmux_client_with_watchdog(
785
+ ["tmux", "-L", "railmux"],
786
+ {"RAILMUX_WINDOWS_RUNTIME": "msys2"},
787
+ expected_target=target,
788
+ expected_session_id="$7",
789
+ ) == 1
790
+
791
+ assert popen.call_args.kwargs["stderr"] is subprocess.DEVNULL
792
+ record.assert_called_once_with(
793
+ component="launcher",
794
+ reason="launcher-attach-rejected",
795
+ consecutive_failures=1,
796
+ )
797
+ stderr = capsys.readouterr().err
798
+ assert "bridge was unavailable" in stderr
799
+ assert "run 'railmux doctor'" in stderr
800
+
801
+
690
802
  def test_invalid_config_is_actionable_without_traceback(
691
803
  monkeypatch, tmp_path, capsys,
692
804
  ):
@@ -108,6 +108,67 @@ def test_windows_unavailable_status_clicks_warn_with_keyboard_fallback(
108
108
  )
109
109
 
110
110
 
111
+ def test_finished_status_lease_repaints_left_and_current_copy_range():
112
+ manager = MagicMock()
113
+ manager.target_toggle_available = True
114
+ manager.status_navigation_available = True
115
+ manager.selection_isolation_available = False
116
+ app = _bare_app(
117
+ _tmux_binding_manager=manager,
118
+ _selection_isolation_manager=None,
119
+ _tmux_status_enabled=False,
120
+ _tmux_error_bar=False,
121
+ _applied_tmux_bar_state=("stale",),
122
+ _rendered_status_text="Restored session safely",
123
+ _rendered_status_level="success",
124
+ )
125
+ app._apply_tmux_bar = MagicMock()
126
+ app._render_status_to_tmux = MagicMock()
127
+ app._sync_target_pane_option = MagicMock()
128
+ app._sync_termux_tap_route = MagicMock()
129
+
130
+ app._finish_tmux_bindings(True)
131
+
132
+ app._apply_tmux_bar.assert_not_called()
133
+ app._render_status_to_tmux.assert_not_called()
134
+ assert getattr(app, "_status_navigation_projected", False) is False
135
+
136
+ # The worker may finish before run() enables its session-local status bar.
137
+ # The first periodic reconciliation must project the same remembered text.
138
+ app._tmux_status_enabled = True
139
+ app._project_status_navigation()
140
+
141
+ assert app._applied_tmux_bar_state is None
142
+ assert app._status_navigation_projected is True
143
+ app._apply_tmux_bar.assert_called_once_with(False)
144
+ app._render_status_to_tmux.assert_called_once_with(
145
+ "Restored session safely", "success", remember=False,
146
+ )
147
+
148
+
149
+ def test_finished_status_lease_does_not_invent_empty_copy_source():
150
+ manager = MagicMock()
151
+ manager.target_toggle_available = True
152
+ manager.status_navigation_available = True
153
+ manager.selection_isolation_available = False
154
+ app = _bare_app(
155
+ _tmux_binding_manager=manager,
156
+ _selection_isolation_manager=None,
157
+ _tmux_status_enabled=True,
158
+ _tmux_error_bar=False,
159
+ _rendered_status_text="",
160
+ )
161
+ app._apply_tmux_bar = MagicMock()
162
+ app._render_status_to_tmux = MagicMock()
163
+ app._sync_target_pane_option = MagicMock()
164
+ app._sync_termux_tap_route = MagicMock()
165
+
166
+ app._finish_tmux_bindings(True)
167
+
168
+ app._apply_tmux_bar.assert_called_once_with(False)
169
+ app._render_status_to_tmux.assert_not_called()
170
+
171
+
111
172
  def test_slow_windows_startup_summary_mentions_read_only_cache():
112
173
  import time
113
174
 
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import os
4
4
  import subprocess
5
5
  from types import SimpleNamespace
6
+ from unittest.mock import MagicMock
6
7
 
7
8
  import pytest
8
9
 
@@ -37,6 +38,140 @@ def test_launcher_argv_preserves_multi_argument_python_module_prefix():
37
38
  ]
38
39
 
39
40
 
41
+ def test_detached_launcher_session_is_pinned_before_and_after_create(
42
+ monkeypatch,
43
+ ):
44
+ target = tmux_server.TmuxServerTarget("/tmp/private", 44)
45
+ live = []
46
+ session_ids = iter((None, "$7"))
47
+ # Another launcher can win after our initial absence check. tmux then
48
+ # rejects this duplicate create, while the exact resulting session is the
49
+ # safe shared outcome.
50
+ run = SimpleNamespace(returncode=1)
51
+ launched = []
52
+
53
+ def target_is_live(candidate, **kwargs):
54
+ live.append((candidate, kwargs))
55
+ return True
56
+
57
+ monkeypatch.setattr(tmux_server, "target_is_live", target_is_live)
58
+ monkeypatch.setattr(
59
+ tmux_server,
60
+ "target_session_id",
61
+ lambda *_args, **_kwargs: next(session_ids),
62
+ )
63
+ monkeypatch.setattr(
64
+ subprocess,
65
+ "run",
66
+ lambda argv, **kwargs: launched.append((argv, kwargs)) or run,
67
+ )
68
+
69
+ assert tmux_server.ensure_detached_launcher_session(
70
+ target,
71
+ ["/opt/railmux/bin/python", "-m", "railmux"],
72
+ ["--mode", "codex"],
73
+ env={"PATH": "/usr/bin"},
74
+ ) == "$7"
75
+
76
+ assert len(live) == 2
77
+ assert launched[0][0] == [
78
+ "tmux", "-S", "/tmp/private",
79
+ "new-session", "-d", "-s", "railmux",
80
+ "/opt/railmux/bin/python", "-m", "railmux", "--inside-tmux",
81
+ "--mode", "codex",
82
+ ]
83
+ assert launched[0][1]["env"] == {"PATH": "/usr/bin"}
84
+
85
+
86
+ def test_detached_managed_windows_session_receives_only_runtime_identity(
87
+ monkeypatch,
88
+ ):
89
+ target = tmux_server.TmuxServerTarget("/tmp/private", 44)
90
+ session_ids = iter((None, "$7"))
91
+ launched = []
92
+ monkeypatch.setattr(tmux_server, "target_is_live", lambda *_a, **_k: True)
93
+ monkeypatch.setattr(
94
+ tmux_server,
95
+ "target_session_id",
96
+ lambda *_args, **_kwargs: next(session_ids),
97
+ )
98
+ monkeypatch.setattr(
99
+ subprocess,
100
+ "run",
101
+ lambda argv, **kwargs: launched.append((argv, kwargs))
102
+ or SimpleNamespace(returncode=0),
103
+ )
104
+ env = {
105
+ "RAILMUX_WINDOWS_RUNTIME": "msys2",
106
+ "RAILMUX_MSYS2_RUNTIME_ID": "msys2-2026-03-22",
107
+ "RAILMUX_MSYS2_APP_ID": "railmux-0.4.0.dev19",
108
+ "CODEX_API_KEY": "must-not-enter-tmux",
109
+ }
110
+
111
+ assert tmux_server.ensure_detached_launcher_session(
112
+ target, ["/opt/railmux/bin/railmux"], [], env=env,
113
+ ) == "$7"
114
+
115
+ argv = launched[0][0]
116
+ assert argv == [
117
+ "tmux", "-S", "/tmp/private", "new-session", "-d", "-s",
118
+ "railmux",
119
+ "-e", "RAILMUX_WINDOWS_RUNTIME=msys2",
120
+ "-e", "RAILMUX_MSYS2_RUNTIME_ID=msys2-2026-03-22",
121
+ "-e", "RAILMUX_MSYS2_APP_ID=railmux-0.4.0.dev19",
122
+ "/opt/railmux/bin/railmux", "--inside-tmux",
123
+ ]
124
+ assert all("CODEX_API_KEY" not in value for value in argv)
125
+
126
+
127
+ def test_detached_session_rejects_unbounded_runtime_identity(monkeypatch):
128
+ target = tmux_server.TmuxServerTarget("/tmp/private", 44)
129
+ session_ids = iter((None, "$7"))
130
+ launched = []
131
+ monkeypatch.setattr(tmux_server, "target_is_live", lambda *_a, **_k: True)
132
+ monkeypatch.setattr(
133
+ tmux_server,
134
+ "target_session_id",
135
+ lambda *_args, **_kwargs: next(session_ids),
136
+ )
137
+ monkeypatch.setattr(
138
+ subprocess,
139
+ "run",
140
+ lambda argv, **kwargs: launched.append(argv)
141
+ or SimpleNamespace(returncode=0),
142
+ )
143
+
144
+ assert tmux_server.ensure_detached_launcher_session(
145
+ target,
146
+ ["railmux"],
147
+ [],
148
+ env={
149
+ "RAILMUX_WINDOWS_RUNTIME": "msys2",
150
+ "RAILMUX_MSYS2_APP_ID": "bad=value",
151
+ },
152
+ ) == "$7"
153
+
154
+ assert "RAILMUX_WINDOWS_RUNTIME=msys2" in launched[0]
155
+ assert all("bad=value" not in value for value in launched[0])
156
+
157
+
158
+ def test_detached_launcher_session_refuses_changed_server_identity(
159
+ monkeypatch,
160
+ ):
161
+ target = tmux_server.TmuxServerTarget("/tmp/private", 44)
162
+ session_id = MagicMock(return_value=None)
163
+ monkeypatch.setattr(tmux_server, "target_session_id", session_id)
164
+ monkeypatch.setattr(
165
+ tmux_server, "target_is_live", MagicMock(side_effect=(True, False)))
166
+ monkeypatch.setattr(
167
+ subprocess, "run", MagicMock(return_value=SimpleNamespace(returncode=0)))
168
+
169
+ assert tmux_server.ensure_detached_launcher_session(
170
+ target, ["railmux"], [],
171
+ ) is None
172
+ session_id.assert_called_once_with(target, "railmux", timeout=0.5)
173
+
174
+
40
175
  def test_current_socket_parser_allows_commas_in_the_path():
41
176
  env = {"TMUX": "/tmp/with,comma/railmux,123,0"}
42
177
  assert tmux_server.current_socket_path(env) == "/tmp/with,comma/railmux"
@@ -1 +0,0 @@
1
- __version__ = "0.4.0.dev17"
File without changes
File without changes
File without changes
File without changes