railmux 0.4.0.dev17__tar.gz → 0.4.0.dev18__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.dev18}/PKG-INFO +1 -1
  2. railmux-0.4.0.dev18/src/railmux/__init__.py +1 -0
  3. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/cli.py +16 -2
  4. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/tmux_server.py +50 -0
  5. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18/src/railmux.egg-info}/PKG-INFO +1 -1
  6. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_cli.py +67 -0
  7. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_tmux_server.py +63 -0
  8. railmux-0.4.0.dev17/src/railmux/__init__.py +0 -1
  9. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/LICENSE +0 -0
  10. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/MANIFEST.in +0 -0
  11. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/README.md +0 -0
  12. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/pyproject.toml +0 -0
  13. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/setup.cfg +0 -0
  14. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/__main__.py +0 -0
  15. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/atomic_file.py +0 -0
  16. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/background_index.py +0 -0
  17. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/codex_index.py +0 -0
  18. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/config.py +0 -0
  19. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/config_cli.py +0 -0
  20. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/diagnostics.py +0 -0
  21. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/discovery.py +0 -0
  22. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/display_transport.py +0 -0
  23. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/entrypoint.py +0 -0
  24. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/fast_display_client.py +0 -0
  25. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/fast_display_history.py +0 -0
  26. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/fast_display_input.py +0 -0
  27. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/fast_display_protocol.py +0 -0
  28. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/fast_display_server.py +0 -0
  29. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/favorites.py +0 -0
  30. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/fuzzy.py +0 -0
  31. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/help_workspace.py +0 -0
  32. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/launcher.py +0 -0
  33. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/legacy_sessions.py +0 -0
  34. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/local_clipboard.py +0 -0
  35. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/local_open.py +0 -0
  36. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/models.py +0 -0
  37. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/modes.py +0 -0
  38. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/mouse_manager.py +0 -0
  39. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/orphan_marker.py +0 -0
  40. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/pane_surface.py +0 -0
  41. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/path_codec.py +0 -0
  42. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/provider_paths.py +0 -0
  43. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/remote_config.py +0 -0
  44. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/renames.py +0 -0
  45. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/restart_state.py +0 -0
  46. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/runtime_config.py +0 -0
  47. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/scroll_agent.py +0 -0
  48. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/scroll_manager.py +0 -0
  49. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/selection_isolation.py +0 -0
  50. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/self_update.py +0 -0
  51. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/session_cache.py +0 -0
  52. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/session_index.py +0 -0
  53. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/session_lease.py +0 -0
  54. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/setting_contracts.py +0 -0
  55. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/settings.py +0 -0
  56. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/ssh_args.py +0 -0
  57. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/ssh_compat.py +0 -0
  58. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/ssh_display_diagnostics.py +0 -0
  59. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/ssh_doctor.py +0 -0
  60. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/system_deps.py +0 -0
  61. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/terminal_status.py +0 -0
  62. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/tmux_binding_manager.py +0 -0
  63. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/tmux_ctl.py +0 -0
  64. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/tmux_health.py +0 -0
  65. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/tool_panes.py +0 -0
  66. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/transcript.py +0 -0
  67. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/ui/__init__.py +0 -0
  68. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/ui/_widgets.py +0 -0
  69. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/ui/app.py +0 -0
  70. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/ui/keymap.py +0 -0
  71. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/ui/modals.py +0 -0
  72. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/ui/projects_pane.py +0 -0
  73. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/ui/running_pane.py +0 -0
  74. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/ui/sessions_pane.py +0 -0
  75. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/ui/sidebar.py +0 -0
  76. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/ui/statusbar.py +0 -0
  77. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/ui/workspace.py +0 -0
  78. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/windows_attach_relay.py +0 -0
  79. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/windows_bootstrap.py +0 -0
  80. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/windows_install_log.py +0 -0
  81. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/windows_msys2.py +0 -0
  82. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/windows_pacman.py +0 -0
  83. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux/windows_tmux_lifecycle.py +0 -0
  84. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux.egg-info/SOURCES.txt +0 -0
  85. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux.egg-info/dependency_links.txt +0 -0
  86. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux.egg-info/entry_points.txt +0 -0
  87. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux.egg-info/requires.txt +0 -0
  88. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/src/railmux.egg-info/top_level.txt +0 -0
  89. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_agent_workspace.py +0 -0
  90. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_atomic_file.py +0 -0
  91. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_background_index.py +0 -0
  92. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_codex_delete.py +0 -0
  93. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_codex_index.py +0 -0
  94. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_codex_lineage.py +0 -0
  95. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_codex_transcript.py +0 -0
  96. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_config.py +0 -0
  97. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_config_cli.py +0 -0
  98. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_denested_transport_benchmark.py +0 -0
  99. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_diagnostics.py +0 -0
  100. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_discovery.py +0 -0
  101. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_display_transport.py +0 -0
  102. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_fast_full_window.py +0 -0
  103. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_focus_highlight.py +0 -0
  104. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_fullscreen_binding.py +0 -0
  105. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_fuzzy.py +0 -0
  106. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_help_agent.py +0 -0
  107. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_help_workspace.py +0 -0
  108. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_history_cleanup.py +0 -0
  109. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_keymap.py +0 -0
  110. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_launcher.py +0 -0
  111. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_layout_profile.py +0 -0
  112. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_legacy_sessions.py +0 -0
  113. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_local_clipboard.py +0 -0
  114. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_local_open.py +0 -0
  115. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_modals.py +0 -0
  116. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_mode_selection.py +0 -0
  117. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_modes.py +0 -0
  118. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_mouse_manager.py +0 -0
  119. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_new_project.py +0 -0
  120. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_options.py +0 -0
  121. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_orphan_recovery.py +0 -0
  122. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_pane_surface.py +0 -0
  123. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_path_codec.py +0 -0
  124. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_projects_pane.py +0 -0
  125. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_provider_paths.py +0 -0
  126. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_release_notes.py +0 -0
  127. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_release_workflow.py +0 -0
  128. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_remote_config.py +0 -0
  129. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_renames.py +0 -0
  130. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_restart_state.py +0 -0
  131. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_running_pane.py +0 -0
  132. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_running_sort.py +0 -0
  133. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_runtime_config.py +0 -0
  134. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_scroll_agent.py +0 -0
  135. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_scroll_manager.py +0 -0
  136. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_selection_isolation.py +0 -0
  137. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_self_update.py +0 -0
  138. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_session_cache.py +0 -0
  139. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_session_index.py +0 -0
  140. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_session_lease.py +0 -0
  141. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_sessions_pane.py +0 -0
  142. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_setting_contracts.py +0 -0
  143. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_settings.py +0 -0
  144. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_sidebar.py +0 -0
  145. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_soft_quit.py +0 -0
  146. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_ssh_args.py +0 -0
  147. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_ssh_compat.py +0 -0
  148. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_ssh_display_benchmark.py +0 -0
  149. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_ssh_display_diagnostics.py +0 -0
  150. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_ssh_doctor.py +0 -0
  151. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_status.py +0 -0
  152. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_statusbar.py +0 -0
  153. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_system_deps.py +0 -0
  154. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_terminal_size.py +0 -0
  155. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_terminal_status.py +0 -0
  156. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_termux_local_touch.py +0 -0
  157. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_tmux_binding_manager.py +0 -0
  158. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_tmux_ctl.py +0 -0
  159. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_tmux_health.py +0 -0
  160. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_tmux_integration.py +0 -0
  161. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_tmux_status.py +0 -0
  162. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_tool_panes.py +0 -0
  163. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_transcript.py +0 -0
  164. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_utf8_state.py +0 -0
  165. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_web_demo_recorder.py +0 -0
  166. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_wheel_smoke.py +0 -0
  167. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_widgets.py +0 -0
  168. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_windows_attach_relay.py +0 -0
  169. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_windows_bootstrap.py +0 -0
  170. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_windows_install_log.py +0 -0
  171. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_windows_msys2.py +0 -0
  172. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_windows_pacman.py +0 -0
  173. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_windows_tmux_lifecycle.py +0 -0
  174. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_windows_wrapper_contract.py +0 -0
  175. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tests/test_yolo_prompt.py +0 -0
  176. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tools/denested_transport_benchmark.py +0 -0
  177. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/tools/release_notes.py +0 -0
  178. {railmux-0.4.0.dev17 → railmux-0.4.0.dev18}/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.dev18
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.dev18"
@@ -593,14 +593,28 @@ def main(argv: list[str] | None = None) -> int:
593
593
  if Path(sys.argv[0]).name == "__main__.py"
594
594
  else [sys.argv[0]]
595
595
  )
596
- cmd = tmux_server.launcher_argv(launch_prefix, raw_args)
596
+ client_env = tmux_server.exec_environment()
597
597
  dedicated_session_id = (
598
598
  tmux_server.target_session_id(dedicated_target, "railmux")
599
599
  if dedicated_target is not None else None
600
600
  )
601
+ if dedicated_target is not None and dedicated_session_id is None:
602
+ from railmux.provider_paths import (
603
+ running_in_managed_windows_wrapper,
604
+ )
605
+ if running_in_managed_windows_wrapper():
606
+ dedicated_session_id = (
607
+ tmux_server.ensure_detached_launcher_session(
608
+ dedicated_target,
609
+ launch_prefix,
610
+ raw_args,
611
+ env=client_env,
612
+ )
613
+ )
614
+ cmd = tmux_server.launcher_argv(launch_prefix, raw_args)
601
615
  return _run_tmux_client_with_watchdog(
602
616
  cmd,
603
- tmux_server.exec_environment(),
617
+ client_env,
604
618
  expected_target=dedicated_target,
605
619
  expected_session_id=dedicated_session_id,
606
620
  )
@@ -645,3 +645,53 @@ def launcher_argv(
645
645
  "--inside-tmux",
646
646
  *forwarded_args,
647
647
  )
648
+
649
+
650
+ def ensure_detached_launcher_session(
651
+ target: TmuxServerTarget,
652
+ launch_prefix: Sequence[str],
653
+ forwarded_args: Sequence[str],
654
+ *,
655
+ env: Mapping[str, str] | None = None,
656
+ timeout: float = 5.0,
657
+ ) -> str | None:
658
+ """Create the outer UI detached on one already-proven server.
659
+
660
+ Managed Windows uses this only when a proven existing server has no outer
661
+ UI, commonly because Soft Quit left provider sessions alive in another
662
+ Terminal Services session. A new tmux client can then reject
663
+ ``new-session -A`` before the outer session exists, so the server-origin
664
+ terminal bridge has no immutable session to attach.
665
+
666
+ An exact pre/post server-PID check fences the mutation. A concurrent
667
+ launcher may win the create race; resolving one exact resulting session is
668
+ success regardless of this client's ``new-session`` return code.
669
+ """
670
+ existing = target_session_id(target, "railmux", timeout=0.5)
671
+ if existing is not None:
672
+ return existing
673
+ if not launch_prefix or not target_is_live(target, timeout=1.0, env=env):
674
+ return None
675
+ try:
676
+ subprocess.run(
677
+ target_argv(
678
+ target,
679
+ "new-session",
680
+ "-d",
681
+ "-s",
682
+ "railmux",
683
+ *launch_prefix,
684
+ "--inside-tmux",
685
+ *forwarded_args,
686
+ ),
687
+ stdout=subprocess.DEVNULL,
688
+ stderr=subprocess.DEVNULL,
689
+ timeout=timeout,
690
+ check=False,
691
+ env=None if env is None else dict(env),
692
+ )
693
+ except (OSError, subprocess.TimeoutExpired):
694
+ return None
695
+ if not target_is_live(target, timeout=1.0, env=env):
696
+ return None
697
+ return target_session_id(target, "railmux", timeout=1.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.dev18
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
@@ -472,6 +472,73 @@ def test_prelaunch_recovery_is_scoped_to_the_dedicated_server(monkeypatch):
472
472
  assert os.environ["TMUX_PANE"] == "%9"
473
473
 
474
474
 
475
+ def test_managed_windows_precreates_missing_outer_session_for_bridge(
476
+ monkeypatch,
477
+ ):
478
+ target = TmuxServerTarget("/tmp/tmux-private/railmux", 789)
479
+ monkeypatch.setattr(
480
+ "railmux.cli.tmux_server.discover_target", lambda: target)
481
+ monkeypatch.setattr(
482
+ "railmux.cli.tmux_server.is_current_server", lambda _target: False)
483
+ monkeypatch.setattr(
484
+ "railmux.cli.tmux_server.target_session_id", lambda *_a, **_kw: None)
485
+ monkeypatch.setattr("railmux.cli.sys.argv", ["/opt/railmux/bin/railmux"])
486
+ monkeypatch.setattr(
487
+ "railmux.provider_paths.running_in_managed_windows_wrapper",
488
+ lambda: True,
489
+ )
490
+ prepared = MagicMock(return_value="$9")
491
+ monkeypatch.setattr(
492
+ "railmux.cli.tmux_server.ensure_detached_launcher_session", prepared)
493
+ run_client = MagicMock(return_value=0)
494
+ monkeypatch.setattr(
495
+ "railmux.cli._run_tmux_client_with_watchdog", run_client)
496
+
497
+ assert main(["--project", "/work"]) == 0
498
+
499
+ client_env = tmux_server.exec_environment()
500
+ prepared.assert_called_once_with(
501
+ target,
502
+ ["/opt/railmux/bin/railmux"],
503
+ ["--project", "/work"],
504
+ env=client_env,
505
+ )
506
+ assert run_client.call_args.kwargs == {
507
+ "expected_target": target,
508
+ "expected_session_id": "$9",
509
+ }
510
+
511
+
512
+ def test_posix_missing_outer_session_keeps_ordinary_new_session_path(
513
+ monkeypatch,
514
+ ):
515
+ target = TmuxServerTarget("/tmp/tmux-private/railmux", 789)
516
+ monkeypatch.setattr(
517
+ "railmux.cli.tmux_server.discover_target", lambda: target)
518
+ monkeypatch.setattr(
519
+ "railmux.cli.tmux_server.is_current_server", lambda _target: False)
520
+ monkeypatch.setattr(
521
+ "railmux.cli.tmux_server.target_session_id", lambda *_a, **_kw: None)
522
+ monkeypatch.setattr(
523
+ "railmux.provider_paths.running_in_managed_windows_wrapper",
524
+ lambda: False,
525
+ )
526
+ prepared = MagicMock()
527
+ monkeypatch.setattr(
528
+ "railmux.cli.tmux_server.ensure_detached_launcher_session", prepared)
529
+ run_client = MagicMock(return_value=0)
530
+ monkeypatch.setattr(
531
+ "railmux.cli._run_tmux_client_with_watchdog", run_client)
532
+
533
+ assert main([]) == 0
534
+
535
+ prepared.assert_not_called()
536
+ assert run_client.call_args.kwargs == {
537
+ "expected_target": target,
538
+ "expected_session_id": None,
539
+ }
540
+
541
+
475
542
  def test_local_tmux_watchdog_exits_and_records_after_consecutive_failures(
476
543
  monkeypatch, capsys,
477
544
  ):
@@ -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,68 @@ 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_launcher_session_refuses_changed_server_identity(
87
+ monkeypatch,
88
+ ):
89
+ target = tmux_server.TmuxServerTarget("/tmp/private", 44)
90
+ session_id = MagicMock(return_value=None)
91
+ monkeypatch.setattr(tmux_server, "target_session_id", session_id)
92
+ monkeypatch.setattr(
93
+ tmux_server, "target_is_live", MagicMock(side_effect=(True, False)))
94
+ monkeypatch.setattr(
95
+ subprocess, "run", MagicMock(return_value=SimpleNamespace(returncode=0)))
96
+
97
+ assert tmux_server.ensure_detached_launcher_session(
98
+ target, ["railmux"], [],
99
+ ) is None
100
+ session_id.assert_called_once_with(target, "railmux", timeout=0.5)
101
+
102
+
40
103
  def test_current_socket_parser_allows_commas_in_the_path():
41
104
  env = {"TMUX": "/tmp/with,comma/railmux,123,0"}
42
105
  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