railmux 0.4.0.dev9__tar.gz → 0.4.0.dev11__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 (176) hide show
  1. {railmux-0.4.0.dev9/src/railmux.egg-info → railmux-0.4.0.dev11}/PKG-INFO +1 -1
  2. railmux-0.4.0.dev11/src/railmux/__init__.py +1 -0
  3. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/cli.py +16 -0
  4. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/codex_index.py +2 -2
  5. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/discovery.py +2 -1
  6. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/fast_display_server.py +1 -1
  7. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/mouse_manager.py +1 -1
  8. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/orphan_marker.py +2 -2
  9. railmux-0.4.0.dev11/src/railmux/provider_paths.py +102 -0
  10. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/restart_state.py +8 -2
  11. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/tmux_binding_manager.py +1 -1
  12. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/ui/app.py +21 -1
  13. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/windows_bootstrap.py +28 -7
  14. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/windows_msys2.py +399 -53
  15. railmux-0.4.0.dev11/src/railmux/windows_tmux_lifecycle.py +259 -0
  16. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11/src/railmux.egg-info}/PKG-INFO +1 -1
  17. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux.egg-info/SOURCES.txt +2 -0
  18. railmux-0.4.0.dev11/tests/test_provider_paths.py +110 -0
  19. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_restart_state.py +30 -0
  20. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_soft_quit.py +6 -0
  21. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_windows_bootstrap.py +77 -1
  22. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_windows_msys2.py +318 -23
  23. railmux-0.4.0.dev11/tests/test_windows_tmux_lifecycle.py +140 -0
  24. railmux-0.4.0.dev9/src/railmux/__init__.py +0 -1
  25. railmux-0.4.0.dev9/src/railmux/provider_paths.py +0 -41
  26. railmux-0.4.0.dev9/tests/test_provider_paths.py +0 -29
  27. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/LICENSE +0 -0
  28. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/MANIFEST.in +0 -0
  29. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/README.md +0 -0
  30. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/pyproject.toml +0 -0
  31. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/setup.cfg +0 -0
  32. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/__main__.py +0 -0
  33. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/atomic_file.py +0 -0
  34. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/background_index.py +0 -0
  35. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/config.py +0 -0
  36. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/config_cli.py +0 -0
  37. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/diagnostics.py +0 -0
  38. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/display_transport.py +0 -0
  39. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/entrypoint.py +0 -0
  40. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/fast_display_client.py +0 -0
  41. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/fast_display_history.py +0 -0
  42. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/fast_display_input.py +0 -0
  43. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/fast_display_protocol.py +0 -0
  44. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/favorites.py +0 -0
  45. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/fuzzy.py +0 -0
  46. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/help_workspace.py +0 -0
  47. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/launcher.py +0 -0
  48. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/legacy_sessions.py +0 -0
  49. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/local_clipboard.py +0 -0
  50. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/local_open.py +0 -0
  51. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/models.py +0 -0
  52. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/modes.py +0 -0
  53. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/pane_surface.py +0 -0
  54. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/path_codec.py +0 -0
  55. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/remote_config.py +0 -0
  56. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/renames.py +0 -0
  57. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/runtime_config.py +0 -0
  58. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/scroll_agent.py +0 -0
  59. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/scroll_manager.py +0 -0
  60. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/selection_isolation.py +0 -0
  61. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/self_update.py +0 -0
  62. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/session_cache.py +0 -0
  63. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/session_index.py +0 -0
  64. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/setting_contracts.py +0 -0
  65. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/settings.py +0 -0
  66. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/ssh_args.py +0 -0
  67. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/ssh_compat.py +0 -0
  68. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/ssh_display_diagnostics.py +0 -0
  69. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/ssh_doctor.py +0 -0
  70. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/system_deps.py +0 -0
  71. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/terminal_status.py +0 -0
  72. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/tmux_ctl.py +0 -0
  73. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/tmux_health.py +0 -0
  74. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/tmux_server.py +0 -0
  75. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/tool_panes.py +0 -0
  76. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/transcript.py +0 -0
  77. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/ui/__init__.py +0 -0
  78. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/ui/_widgets.py +0 -0
  79. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/ui/keymap.py +0 -0
  80. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/ui/modals.py +0 -0
  81. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/ui/projects_pane.py +0 -0
  82. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/ui/running_pane.py +0 -0
  83. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/ui/sessions_pane.py +0 -0
  84. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/ui/sidebar.py +0 -0
  85. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/ui/statusbar.py +0 -0
  86. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/ui/workspace.py +0 -0
  87. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/windows_install_log.py +0 -0
  88. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux/windows_pacman.py +0 -0
  89. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux.egg-info/dependency_links.txt +0 -0
  90. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux.egg-info/entry_points.txt +0 -0
  91. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux.egg-info/requires.txt +0 -0
  92. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/src/railmux.egg-info/top_level.txt +0 -0
  93. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_agent_workspace.py +0 -0
  94. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_atomic_file.py +0 -0
  95. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_background_index.py +0 -0
  96. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_cli.py +0 -0
  97. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_codex_delete.py +0 -0
  98. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_codex_index.py +0 -0
  99. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_codex_lineage.py +0 -0
  100. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_codex_transcript.py +0 -0
  101. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_config.py +0 -0
  102. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_config_cli.py +0 -0
  103. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_denested_transport_benchmark.py +0 -0
  104. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_diagnostics.py +0 -0
  105. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_discovery.py +0 -0
  106. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_display_transport.py +0 -0
  107. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_fast_full_window.py +0 -0
  108. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_focus_highlight.py +0 -0
  109. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_fullscreen_binding.py +0 -0
  110. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_fuzzy.py +0 -0
  111. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_help_agent.py +0 -0
  112. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_help_workspace.py +0 -0
  113. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_history_cleanup.py +0 -0
  114. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_keymap.py +0 -0
  115. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_launcher.py +0 -0
  116. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_layout_profile.py +0 -0
  117. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_legacy_sessions.py +0 -0
  118. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_local_clipboard.py +0 -0
  119. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_local_open.py +0 -0
  120. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_modals.py +0 -0
  121. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_mode_selection.py +0 -0
  122. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_modes.py +0 -0
  123. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_mouse_manager.py +0 -0
  124. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_new_project.py +0 -0
  125. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_options.py +0 -0
  126. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_orphan_recovery.py +0 -0
  127. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_pane_surface.py +0 -0
  128. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_path_codec.py +0 -0
  129. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_projects_pane.py +0 -0
  130. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_release_notes.py +0 -0
  131. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_release_workflow.py +0 -0
  132. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_remote_config.py +0 -0
  133. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_renames.py +0 -0
  134. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_running_pane.py +0 -0
  135. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_running_sort.py +0 -0
  136. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_runtime_config.py +0 -0
  137. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_scroll_agent.py +0 -0
  138. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_scroll_manager.py +0 -0
  139. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_selection_isolation.py +0 -0
  140. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_self_update.py +0 -0
  141. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_session_cache.py +0 -0
  142. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_session_index.py +0 -0
  143. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_sessions_pane.py +0 -0
  144. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_setting_contracts.py +0 -0
  145. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_settings.py +0 -0
  146. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_sidebar.py +0 -0
  147. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_ssh_args.py +0 -0
  148. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_ssh_compat.py +0 -0
  149. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_ssh_display_benchmark.py +0 -0
  150. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_ssh_display_diagnostics.py +0 -0
  151. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_ssh_doctor.py +0 -0
  152. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_status.py +0 -0
  153. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_statusbar.py +0 -0
  154. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_system_deps.py +0 -0
  155. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_terminal_size.py +0 -0
  156. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_terminal_status.py +0 -0
  157. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_termux_local_touch.py +0 -0
  158. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_tmux_binding_manager.py +0 -0
  159. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_tmux_ctl.py +0 -0
  160. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_tmux_health.py +0 -0
  161. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_tmux_integration.py +0 -0
  162. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_tmux_server.py +0 -0
  163. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_tmux_status.py +0 -0
  164. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_tool_panes.py +0 -0
  165. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_transcript.py +0 -0
  166. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_utf8_state.py +0 -0
  167. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_web_demo_recorder.py +0 -0
  168. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_wheel_smoke.py +0 -0
  169. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_widgets.py +0 -0
  170. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_windows_install_log.py +0 -0
  171. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_windows_pacman.py +0 -0
  172. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_windows_wrapper_contract.py +0 -0
  173. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tests/test_yolo_prompt.py +0 -0
  174. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tools/denested_transport_benchmark.py +0 -0
  175. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/tools/release_notes.py +0 -0
  176. {railmux-0.4.0.dev9 → railmux-0.4.0.dev11}/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.dev9
3
+ Version: 0.4.0.dev11
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.dev11"
@@ -20,6 +20,7 @@ from railmux.runtime_config import (
20
20
  )
21
21
  from railmux import tmux_health
22
22
  from railmux import tmux_server
23
+ from railmux import windows_tmux_lifecycle
23
24
  from railmux.system_deps import ensure_tmux_available
24
25
  from railmux.ssh_args import AppendSshArgument, ExtendSshArguments
25
26
 
@@ -126,7 +127,16 @@ def _run_tmux_client_with_watchdog(
126
127
  file=sys.stderr,
127
128
  )
128
129
  return 2
130
+ # tmux may leave the client TTY in raw mode even after its process
131
+ # exits. Restore it before a bounded recovery probe or user message.
132
+ _restore_terminal(attributes)
129
133
  returncode = process.returncode or 0
134
+ if windows_tmux_lifecycle.recover_abandoned_socket():
135
+ print(
136
+ "info: cleaned an abandoned Windows tmux socket; Codex and "
137
+ "Claude session files were not changed",
138
+ file=sys.stderr,
139
+ )
130
140
  if returncode and expected_target is not None:
131
141
  try:
132
142
  current_target = tmux_server.discover_target(timeout=1.0)
@@ -367,6 +377,12 @@ def main(argv: list[str] | None = None) -> int:
367
377
  # Validate before any command can address a tmux server. ``tmux -V``
368
378
  # above is server-independent.
369
379
  tmux_server.socket_label()
380
+ if windows_tmux_lifecycle.recover_abandoned_socket():
381
+ print(
382
+ "info: cleaned an abandoned Windows tmux socket; Codex and "
383
+ "Claude session files were not changed",
384
+ file=sys.stderr,
385
+ )
370
386
  dedicated_target = tmux_server.discover_target()
371
387
  on_dedicated_server = tmux_server.is_current_server(dedicated_target)
372
388
  except tmux_server.TmuxServerError as exc:
@@ -25,7 +25,7 @@ from railmux.models import (
25
25
  Project,
26
26
  SessionMeta,
27
27
  )
28
- from railmux.provider_paths import provider_path
28
+ from railmux.provider_paths import private_mode_is_safe, provider_path
29
29
  from railmux.renames import Renames
30
30
 
31
31
  # Codex has no reliable provider-neutral signal that distinguishes a long
@@ -461,7 +461,7 @@ class CodexIndex:
461
461
  if (
462
462
  not stat.S_ISREG(info.st_mode)
463
463
  or info.st_uid != os.getuid()
464
- or info.st_mode & 0o077
464
+ or not private_mode_is_safe(info.st_mode)
465
465
  or info.st_size > _PERSISTENT_CACHE_MAX_BYTES
466
466
  ):
467
467
  return
@@ -10,6 +10,7 @@ from pathlib import Path
10
10
  from railmux.atomic_file import atomic_write_text
11
11
  from railmux.models import Project
12
12
  from railmux.path_codec import decode, is_encoded_project_name
13
+ from railmux.provider_paths import private_mode_is_safe
13
14
  from railmux.session_index import _looks_like_uuid, _scan_session
14
15
 
15
16
 
@@ -55,7 +56,7 @@ def _load_persistent_validity(claude_home: Path) -> None:
55
56
  not stat.S_ISREG(info.st_mode)
56
57
  or info.st_uid != os.getuid()
57
58
  or info.st_size > _VALIDITY_CACHE_LIMIT
58
- or info.st_mode & 0o077
59
+ or not private_mode_is_safe(info.st_mode)
59
60
  ):
60
61
  return
61
62
  raw = json.loads(path.read_text(encoding="utf-8"))
@@ -1493,7 +1493,7 @@ def _acquire_display_lock(
1493
1493
  if (
1494
1494
  not stat.S_ISREG(info.st_mode)
1495
1495
  or info.st_uid != os.getuid()
1496
- or info.st_mode & 0o077
1496
+ or not restart_state.private_mode_is_safe(info.st_mode)
1497
1497
  ):
1498
1498
  raise OSError("unsafe display lock")
1499
1499
  deadline = time.monotonic() + max(0.0, timeout)
@@ -60,7 +60,7 @@ class RootWheelForwardingManager:
60
60
  info = self._state_path.lstat()
61
61
  if (not stat.S_ISREG(info.st_mode)
62
62
  or info.st_uid != os.getuid()
63
- or info.st_mode & 0o077
63
+ or not restart_state.private_mode_is_safe(info.st_mode)
64
64
  or info.st_size > _MAX_STATE_BYTES):
65
65
  return None
66
66
  raw = json.loads(self._state_path.read_text(encoding="utf-8"))
@@ -231,7 +231,7 @@ def claim_owner(
231
231
  parent = lock_path.parent.stat()
232
232
  if (not stat.S_ISDIR(parent.st_mode)
233
233
  or parent.st_uid != os.getuid()
234
- or parent.st_mode & 0o077):
234
+ or not restart_state.private_mode_is_safe(parent.st_mode)):
235
235
  return None
236
236
  flags = os.O_CREAT | os.O_RDWR
237
237
  if hasattr(os, "O_NOFOLLOW"):
@@ -243,7 +243,7 @@ def claim_owner(
243
243
  info = os.fstat(fd)
244
244
  if (not stat.S_ISREG(info.st_mode)
245
245
  or info.st_uid != os.getuid()
246
- or info.st_mode & 0o077):
246
+ or not restart_state.private_mode_is_safe(info.st_mode)):
247
247
  return None
248
248
  try:
249
249
  fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
@@ -0,0 +1,102 @@
1
+ """Translate provider-owned native Windows paths inside the MSYS2 preview."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ import re
7
+ import stat
8
+ import sys
9
+ from collections.abc import Mapping
10
+ from pathlib import Path
11
+
12
+
13
+ _DRIVE_PATH = re.compile(r"^(?:\\\\\?\\)?([A-Za-z]):[\\/](.*)\Z")
14
+ _MANAGED_BASE_MARKER = Path("/railmux-base.json")
15
+ _MANAGED_APP_ROOT = Path("/opt/railmux/apps")
16
+ _MANAGED_APP_ID = re.compile(
17
+ r"railmux-[0-9]+(?:\.[0-9]+)*(?:\.dev[0-9]+)?\Z"
18
+ )
19
+
20
+
21
+ def running_in_windows_wrapper(environ: Mapping[str, str] = os.environ) -> bool:
22
+ """Return whether Railmux is running in its native-Windows MSYS2 wrapper."""
23
+ return environ.get("RAILMUX_WINDOWS_RUNTIME") == "msys2"
24
+
25
+
26
+ def running_in_managed_windows_wrapper(
27
+ environ: Mapping[str, str] = os.environ,
28
+ ) -> bool:
29
+ """Verify the installed MSYS2 runtime, not merely its environment hint."""
30
+ if sys.platform != "cygwin" or not running_in_windows_wrapper(environ):
31
+ return False
32
+ runtime_id = environ.get("RAILMUX_MSYS2_RUNTIME_ID")
33
+ app_id = environ.get("RAILMUX_MSYS2_APP_ID")
34
+ if (
35
+ not isinstance(runtime_id, str)
36
+ or not runtime_id
37
+ or not isinstance(app_id, str)
38
+ or _MANAGED_APP_ID.fullmatch(app_id) is None
39
+ ):
40
+ return False
41
+
42
+ def read_safe_marker(marker: Path) -> object | None:
43
+ try:
44
+ info = marker.lstat()
45
+ if (
46
+ not stat.S_ISREG(info.st_mode)
47
+ or info.st_uid != os.getuid()
48
+ or info.st_mode & 0o022
49
+ or info.st_size > 4096
50
+ ):
51
+ return None
52
+ return json.loads(marker.read_text(encoding="utf-8"))
53
+ except (OSError, UnicodeError, json.JSONDecodeError):
54
+ return None
55
+
56
+ base_payload = read_safe_marker(_MANAGED_BASE_MARKER)
57
+ app_payload = read_safe_marker(
58
+ _MANAGED_APP_ROOT / app_id / "railmux-app.json"
59
+ )
60
+ from railmux import __version__
61
+
62
+ return base_payload == {
63
+ "schema": 1,
64
+ "runtime": runtime_id,
65
+ } and app_payload == {
66
+ "schema": 1,
67
+ "runtime": runtime_id,
68
+ "railmux": __version__,
69
+ }
70
+
71
+
72
+ def private_mode_is_safe(mode: int) -> bool:
73
+ """Accept strict POSIX modes or the verified MSYS2 noacl projection."""
74
+ return not mode & 0o077 or (
75
+ running_in_managed_windows_wrapper() and not mode & 0o022
76
+ )
77
+
78
+
79
+ def provider_path(
80
+ raw: str,
81
+ *,
82
+ environ: Mapping[str, str] = os.environ,
83
+ ) -> Path:
84
+ """Return a POSIX-visible path for provider metadata.
85
+
86
+ Native providers write Windows paths into session metadata. Railmux runs
87
+ under MSYS2, where the same locations are mounted as ``/c/...`` (or
88
+ ``//server/share/...`` for UNC paths). Outside the wrapper, preserve the
89
+ existing POSIX behavior exactly.
90
+ """
91
+ if not running_in_windows_wrapper(environ):
92
+ return Path(raw)
93
+
94
+ match = _DRIVE_PATH.match(raw)
95
+ if match is not None:
96
+ drive, tail = match.groups()
97
+ return Path(f"/{drive.lower()}/{tail.replace(chr(92), '/')}")
98
+
99
+ if raw.startswith("\\\\"):
100
+ return Path("//" + raw[2:].replace("\\", "/"))
101
+
102
+ return Path(raw.replace("\\", "/"))
@@ -17,6 +17,7 @@ from pathlib import Path
17
17
  from railmux import tmux_ctl
18
18
  from railmux.atomic_file import atomic_write_text
19
19
  from railmux.config import default_config_path
20
+ from railmux.provider_paths import private_mode_is_safe
20
21
 
21
22
 
22
23
  SCHEMA_VERSION = 1
@@ -164,10 +165,15 @@ def legacy_state_path() -> Path:
164
165
  def _ensure_private_dir(path: Path) -> None:
165
166
  """Create and verify a Railmux-owned 0700 runtime directory."""
166
167
  path.mkdir(mode=0o700, parents=True, exist_ok=True)
167
- info = path.stat()
168
+ info = path.lstat()
169
+ # MSYS2's default noacl projection reports user-owned NTFS directories as
170
+ # 0755 even after chmod(0700). The managed runtime itself lives below the
171
+ # Windows user's LocalAppData ACL. Accept that projection only on the real
172
+ # Cygwin/MSYS Python platform, and still reject symlinks, foreign owners,
173
+ # and any group/world-writable directory. POSIX platforms retain 0700.
168
174
  if (not stat.S_ISDIR(info.st_mode)
169
175
  or info.st_uid != os.getuid()
170
- or info.st_mode & 0o077):
176
+ or not private_mode_is_safe(info.st_mode)):
171
177
  raise OSError("Railmux runtime state directory is not private")
172
178
 
173
179
 
@@ -85,7 +85,7 @@ class SharedTmuxBindingManager:
85
85
  info = self._state_path.lstat()
86
86
  if (not stat.S_ISREG(info.st_mode)
87
87
  or info.st_uid != os.getuid()
88
- or info.st_mode & 0o077
88
+ or not restart_state.private_mode_is_safe(info.st_mode)
89
89
  or info.st_size > _MAX_STATE_BYTES):
90
90
  return None
91
91
  raw = json.loads(self._state_path.read_text(encoding="utf-8"))
@@ -25,6 +25,7 @@ from railmux import (
25
25
  tmux_ctl,
26
26
  tmux_health,
27
27
  tmux_server,
28
+ windows_tmux_lifecycle,
28
29
  )
29
30
  from railmux.atomic_file import atomic_write_text
30
31
  from railmux.background_index import BackgroundCodexIndex
@@ -713,6 +714,9 @@ class App:
713
714
  # digest plus immutable pane id namespaces local recovery state across
714
715
  # windows, sessions, and private tmux servers.
715
716
  self._restart_identity = restart_state.capture_outer_identity()
717
+ # A newly live UI revokes any cleanup proof from the preceding
718
+ # lifecycle before it can create provider sessions.
719
+ windows_tmux_lifecycle.clear_empty_server_exit()
716
720
  self._claude_home = claude_home
717
721
  self._config = config
718
722
  self._auto_launched = auto_launched
@@ -8061,10 +8065,25 @@ class App:
8061
8065
  if defer_outer or getattr(self, "_outer_teardown_done", False):
8062
8066
  return
8063
8067
  self._outer_teardown_done = True
8068
+ identity = getattr(self, "_restart_identity", None)
8069
+ if (
8070
+ self._auto_launched
8071
+ and identity is not None
8072
+ and running_in_windows_wrapper()
8073
+ and tmux_ctl.current_session_name() == "railmux"
8074
+ and tmux_ctl.current_session_id() == identity.session_id
8075
+ ):
8076
+ # MSYS2 can retain an AF_UNIX pathname after its last session
8077
+ # exits. Arm cleanup only when the server-wide immutable-ID
8078
+ # inventory proves that no provider or unknown session exists.
8079
+ windows_tmux_lifecycle.arm_empty_server_exit(
8080
+ server_pid=identity.server_pid,
8081
+ session_id=identity.session_id,
8082
+ pane_id=identity.pane_id,
8083
+ )
8064
8084
  if not self._soft_quit_flag and self._auto_launched:
8065
8085
  session_name = tmux_ctl.current_session_name()
8066
8086
  if session_name == "railmux":
8067
- identity = getattr(self, "_restart_identity", None)
8068
8087
  current_id = tmux_ctl.current_session_id()
8069
8088
  intent = bool(
8070
8089
  identity is not None
@@ -8080,6 +8099,7 @@ class App:
8080
8099
  killed = False
8081
8100
  if intent and not killed:
8082
8101
  tmux_health.clear_clean_exit()
8102
+ windows_tmux_lifecycle.clear_empty_server_exit()
8083
8103
 
8084
8104
  def _enter_filter_mode(self) -> None:
8085
8105
  # Borrow the button row (footer index 1) for a filter Edit — both are a
@@ -14,6 +14,7 @@ from railmux.windows_msys2 import (
14
14
  find_runtime,
15
15
  install_managed_runtime,
16
16
  managed_root,
17
+ reusable_managed_base_candidate,
17
18
  )
18
19
 
19
20
 
@@ -45,7 +46,7 @@ def _runtime_status(
45
46
  ownership = "Railmux-managed" if runtime.managed else "user-owned override"
46
47
  print(f"Runtime: {ownership} at {runtime.root}")
47
48
  else:
48
- root = managed_root(environ, version=__version__)
49
+ root = managed_root(environ)
49
50
  if root is not None:
50
51
  print(f"Managed location: {root}")
51
52
  print("Provider data: shared from the Windows user profile")
@@ -55,7 +56,8 @@ def _runtime_status(
55
56
  def _confirm_install(*, input_fn: Callable[[str], str] = input) -> bool:
56
57
  try:
57
58
  answer = input_fn(
58
- "Install the private MSYS2/tmux runtime now? On Windows, Railmux "
59
+ "No reusable MSYS2 base was found. Install the private MSYS2/tmux "
60
+ "runtime now? On Windows, Railmux "
59
61
  "depends on a complete private MSYS2 compatibility wrapper, "
60
62
  "including tmux and Python. This downloads a 50 MB base plus "
61
63
  "required updates and packages, "
@@ -74,14 +76,30 @@ def _install(
74
76
  verbose: bool = False,
75
77
  input_fn: Callable[[str], str] = input,
76
78
  ) -> Msys2Runtime | None:
77
- if not assume_yes and not _confirm_install(input_fn=input_fn):
78
- print("MSYS2 runtime installation cancelled.", file=sys.stderr)
79
- return None
79
+ reuse_only = False
80
+ if not assume_yes:
81
+ reusable = reusable_managed_base_candidate(environ)
82
+ if reusable is None:
83
+ if not _confirm_install(input_fn=input_fn):
84
+ print("MSYS2 runtime installation cancelled.", file=sys.stderr)
85
+ return None
86
+ else:
87
+ _root, source_version = reusable
88
+ detail = (
89
+ f" from Railmux {source_version}" if source_version else ""
90
+ )
91
+ print(
92
+ f"Reusing the matching MSYS2 {MSYS2_RELEASE} private base"
93
+ f"{detail}; only the Railmux {__version__} app layer will be "
94
+ "installed."
95
+ )
96
+ reuse_only = True
80
97
  try:
81
98
  runtime = install_managed_runtime(
82
99
  version=__version__,
83
100
  environ=environ,
84
101
  verbose=verbose,
102
+ reuse_only=reuse_only,
85
103
  )
86
104
  except RuntimeInstallError as exc:
87
105
  print(f"error: {exc}", file=sys.stderr)
@@ -166,7 +184,7 @@ def main(
166
184
  return 0 if runtime is not None else 2
167
185
 
168
186
  if runtime is None:
169
- root = managed_root(environ, version=__version__)
187
+ root = managed_root(environ)
170
188
  print(
171
189
  "Railmux for Windows needs its private MSYS2/tmux runtime.\n"
172
190
  "The Windows Codex/Claude executables and their existing session "
@@ -175,7 +193,10 @@ def main(
175
193
  )
176
194
  if root is not None:
177
195
  print(f"Managed location: {root}", file=sys.stderr)
178
- if not stdin_isatty():
196
+ if (
197
+ not stdin_isatty()
198
+ and reusable_managed_base_candidate(environ) is None
199
+ ):
179
200
  print(
180
201
  "Run 'railmux runtime install' interactively or "
181
202
  "'railmux runtime install --yes' to install it.",