railmux 0.4.0.dev22__tar.gz → 0.4.0.dev25__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 (180) hide show
  1. {railmux-0.4.0.dev22/src/railmux.egg-info → railmux-0.4.0.dev25}/PKG-INFO +1 -1
  2. railmux-0.4.0.dev25/src/railmux/__init__.py +1 -0
  3. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/cli.py +62 -0
  4. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/config_cli.py +12 -0
  5. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/diagnostics.py +35 -3
  6. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/fast_display_client.py +227 -8
  7. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/fast_display_server.py +5 -0
  8. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/provider_paths.py +23 -2
  9. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/remote_config.py +35 -15
  10. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/ssh_doctor.py +54 -18
  11. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/ui/app.py +24 -0
  12. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/windows_bootstrap.py +189 -10
  13. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/windows_msys2.py +501 -3
  14. railmux-0.4.0.dev25/src/railmux/windows_ui_transition.py +739 -0
  15. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25/src/railmux.egg-info}/PKG-INFO +1 -1
  16. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux.egg-info/SOURCES.txt +2 -0
  17. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_cli.py +20 -0
  18. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_config_cli.py +21 -2
  19. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_diagnostics.py +2 -1
  20. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_fast_full_window.py +277 -20
  21. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_remote_config.py +113 -6
  22. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_ssh_doctor.py +57 -49
  23. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_windows_bootstrap.py +68 -8
  24. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_windows_msys2.py +266 -1
  25. railmux-0.4.0.dev25/tests/test_windows_ui_transition.py +326 -0
  26. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tools/wheel_smoke.py +1 -1
  27. railmux-0.4.0.dev22/src/railmux/__init__.py +0 -1
  28. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/LICENSE +0 -0
  29. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/MANIFEST.in +0 -0
  30. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/README.md +0 -0
  31. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/pyproject.toml +0 -0
  32. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/setup.cfg +0 -0
  33. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/__main__.py +0 -0
  34. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/atomic_file.py +0 -0
  35. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/background_index.py +0 -0
  36. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/codex_index.py +0 -0
  37. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/config.py +0 -0
  38. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/discovery.py +0 -0
  39. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/display_transport.py +0 -0
  40. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/entrypoint.py +0 -0
  41. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/fast_display_history.py +0 -0
  42. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/fast_display_input.py +0 -0
  43. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/fast_display_protocol.py +0 -0
  44. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/favorites.py +0 -0
  45. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/fuzzy.py +0 -0
  46. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/help_workspace.py +0 -0
  47. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/launcher.py +0 -0
  48. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/legacy_sessions.py +0 -0
  49. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/local_clipboard.py +0 -0
  50. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/local_open.py +0 -0
  51. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/models.py +0 -0
  52. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/modes.py +0 -0
  53. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/mouse_manager.py +0 -0
  54. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/orphan_marker.py +0 -0
  55. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/pane_surface.py +0 -0
  56. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/path_codec.py +0 -0
  57. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/renames.py +0 -0
  58. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/restart_state.py +0 -0
  59. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/runtime_config.py +0 -0
  60. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/scroll_agent.py +0 -0
  61. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/scroll_manager.py +0 -0
  62. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/selection_isolation.py +0 -0
  63. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/self_update.py +0 -0
  64. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/session_cache.py +0 -0
  65. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/session_index.py +0 -0
  66. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/session_lease.py +0 -0
  67. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/setting_contracts.py +0 -0
  68. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/settings.py +0 -0
  69. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/ssh_args.py +0 -0
  70. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/ssh_compat.py +0 -0
  71. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/ssh_display_diagnostics.py +0 -0
  72. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/system_deps.py +0 -0
  73. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/terminal_status.py +0 -0
  74. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/tmux_binding_manager.py +0 -0
  75. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/tmux_ctl.py +0 -0
  76. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/tmux_health.py +0 -0
  77. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/tmux_server.py +0 -0
  78. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/tool_panes.py +0 -0
  79. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/transcript.py +0 -0
  80. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/ui/__init__.py +0 -0
  81. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/ui/_widgets.py +0 -0
  82. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/ui/keymap.py +0 -0
  83. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/ui/modals.py +0 -0
  84. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/ui/projects_pane.py +0 -0
  85. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/ui/running_pane.py +0 -0
  86. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/ui/sessions_pane.py +0 -0
  87. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/ui/sidebar.py +0 -0
  88. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/ui/statusbar.py +0 -0
  89. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/ui/workspace.py +0 -0
  90. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/windows_attach_relay.py +0 -0
  91. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/windows_install_log.py +0 -0
  92. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/windows_pacman.py +0 -0
  93. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux/windows_tmux_lifecycle.py +0 -0
  94. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux.egg-info/dependency_links.txt +0 -0
  95. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux.egg-info/entry_points.txt +0 -0
  96. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux.egg-info/requires.txt +0 -0
  97. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/src/railmux.egg-info/top_level.txt +0 -0
  98. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_agent_workspace.py +0 -0
  99. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_atomic_file.py +0 -0
  100. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_background_index.py +0 -0
  101. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_codex_delete.py +0 -0
  102. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_codex_index.py +0 -0
  103. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_codex_lineage.py +0 -0
  104. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_codex_transcript.py +0 -0
  105. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_config.py +0 -0
  106. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_denested_transport_benchmark.py +0 -0
  107. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_discovery.py +0 -0
  108. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_display_transport.py +0 -0
  109. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_focus_highlight.py +0 -0
  110. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_fullscreen_binding.py +0 -0
  111. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_fuzzy.py +0 -0
  112. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_help_agent.py +0 -0
  113. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_help_workspace.py +0 -0
  114. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_history_cleanup.py +0 -0
  115. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_keymap.py +0 -0
  116. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_launcher.py +0 -0
  117. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_layout_profile.py +0 -0
  118. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_legacy_sessions.py +0 -0
  119. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_local_clipboard.py +0 -0
  120. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_local_open.py +0 -0
  121. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_modals.py +0 -0
  122. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_mode_selection.py +0 -0
  123. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_modes.py +0 -0
  124. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_mouse_manager.py +0 -0
  125. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_new_project.py +0 -0
  126. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_options.py +0 -0
  127. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_orphan_recovery.py +0 -0
  128. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_pane_surface.py +0 -0
  129. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_path_codec.py +0 -0
  130. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_projects_pane.py +0 -0
  131. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_provider_paths.py +0 -0
  132. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_release_notes.py +0 -0
  133. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_release_workflow.py +0 -0
  134. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_renames.py +0 -0
  135. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_restart_state.py +0 -0
  136. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_running_pane.py +0 -0
  137. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_running_sort.py +0 -0
  138. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_runtime_config.py +0 -0
  139. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_scroll_agent.py +0 -0
  140. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_scroll_manager.py +0 -0
  141. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_selection_isolation.py +0 -0
  142. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_self_update.py +0 -0
  143. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_session_cache.py +0 -0
  144. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_session_index.py +0 -0
  145. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_session_lease.py +0 -0
  146. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_sessions_pane.py +0 -0
  147. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_setting_contracts.py +0 -0
  148. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_settings.py +0 -0
  149. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_sidebar.py +0 -0
  150. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_soft_quit.py +0 -0
  151. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_ssh_args.py +0 -0
  152. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_ssh_compat.py +0 -0
  153. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_ssh_display_benchmark.py +0 -0
  154. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_ssh_display_diagnostics.py +0 -0
  155. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_status.py +0 -0
  156. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_statusbar.py +0 -0
  157. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_system_deps.py +0 -0
  158. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_terminal_size.py +0 -0
  159. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_terminal_status.py +0 -0
  160. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_termux_local_touch.py +0 -0
  161. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_tmux_binding_manager.py +0 -0
  162. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_tmux_ctl.py +0 -0
  163. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_tmux_health.py +0 -0
  164. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_tmux_integration.py +0 -0
  165. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_tmux_server.py +0 -0
  166. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_tmux_status.py +0 -0
  167. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_tool_panes.py +0 -0
  168. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_transcript.py +0 -0
  169. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_utf8_state.py +0 -0
  170. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_web_demo_recorder.py +0 -0
  171. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_wheel_smoke.py +0 -0
  172. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_widgets.py +0 -0
  173. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_windows_attach_relay.py +0 -0
  174. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_windows_install_log.py +0 -0
  175. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_windows_pacman.py +0 -0
  176. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_windows_tmux_lifecycle.py +0 -0
  177. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_windows_wrapper_contract.py +0 -0
  178. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tests/test_yolo_prompt.py +0 -0
  179. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tools/denested_transport_benchmark.py +0 -0
  180. {railmux-0.4.0.dev22 → railmux-0.4.0.dev25}/tools/release_notes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.4.0.dev22
3
+ Version: 0.4.0.dev25
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.dev25"
@@ -442,9 +442,20 @@ def main(argv: list[str] | None = None) -> int:
442
442
  metavar="ARGS",
443
443
  help="a quoted group of ssh arguments for --remote",
444
444
  )
445
+ doctor_parser.add_argument(
446
+ "--remote-platform",
447
+ choices=("auto", "posix", "windows"),
448
+ default="auto",
449
+ help=(
450
+ "remote shell family for --remote: auto, posix, or windows "
451
+ "(default: auto)"
452
+ ),
453
+ )
445
454
  doctor_args = doctor_parser.parse_args(raw_args[1:])
446
455
  if doctor_args.ssh_arg and not doctor_args.remote:
447
456
  doctor_parser.error("--ssh-args requires --remote")
457
+ if doctor_args.remote_platform != "auto" and not doctor_args.remote:
458
+ doctor_parser.error("--remote-platform requires --remote")
448
459
  if doctor_args.remote:
449
460
  from railmux.ssh_doctor import run_remote_ssh_doctor
450
461
 
@@ -452,6 +463,7 @@ def main(argv: list[str] | None = None) -> int:
452
463
  doctor_args.remote,
453
464
  ssh_args=doctor_args.ssh_arg,
454
465
  json_output=doctor_args.json,
466
+ remote_platform=doctor_args.remote_platform,
455
467
  )
456
468
  return run_doctor(
457
469
  claude_home=Path(doctor_args.claude_home),
@@ -630,6 +642,7 @@ def main(argv: list[str] | None = None) -> int:
630
642
  tmux_server.target_session_id(dedicated_target, "railmux")
631
643
  if dedicated_target is not None else None
632
644
  )
645
+ created_detached_session = False
633
646
  if dedicated_target is not None and dedicated_session_id is None:
634
647
  from railmux.provider_paths import (
635
648
  running_in_managed_windows_wrapper,
@@ -644,6 +657,34 @@ def main(argv: list[str] | None = None) -> int:
644
657
  initial_size=_interactive_terminal_size(),
645
658
  )
646
659
  )
660
+ created_detached_session = dedicated_session_id is not None
661
+ if (
662
+ dedicated_target is not None
663
+ and dedicated_session_id is not None
664
+ and not created_detached_session
665
+ ):
666
+ from railmux.provider_paths import running_in_managed_windows_wrapper
667
+
668
+ if running_in_managed_windows_wrapper():
669
+ from railmux.windows_ui_transition import ensure_current_ui
670
+
671
+ runtime_id = os.environ.get("RAILMUX_MSYS2_RUNTIME_ID", "")
672
+ app_id = os.environ.get("RAILMUX_MSYS2_APP_ID", "")
673
+ transition = ensure_current_ui(
674
+ dedicated_target,
675
+ dedicated_session_id,
676
+ runtime=runtime_id,
677
+ target_app=app_id,
678
+ target_version=__version__,
679
+ )
680
+ if transition.status in {"legacy", "pending", "blocked"}:
681
+ detail = transition.detail or "the running UI was unchanged"
682
+ print(
683
+ "info: the Windows app-layer update is pending; "
684
+ f"{detail}. Soft Quit the existing UI, then run "
685
+ "'railmux' again to finish it.",
686
+ file=sys.stderr,
687
+ )
647
688
  cmd = tmux_server.launcher_argv(launch_prefix, raw_args)
648
689
  return _run_tmux_client_with_watchdog(
649
690
  cmd,
@@ -676,6 +717,27 @@ def main(argv: list[str] | None = None) -> int:
676
717
  ),
677
718
  )
678
719
  app.run()
720
+ request = getattr(app, "_ui_upgrade_request", None)
721
+ if request is not None:
722
+ from railmux.windows_ui_transition import UpgradeRequest, upgrade_exec_argv
723
+
724
+ # Mocked or partially constructed App objects must not manufacture an
725
+ # upgrade merely by dynamically answering arbitrary attributes.
726
+ if not isinstance(request, UpgradeRequest):
727
+ return 0
728
+
729
+ upgrade_argv = upgrade_exec_argv(request, [sys.argv[0], *raw_args])
730
+ if upgrade_argv is None:
731
+ print(
732
+ "error: the requested Windows app layer no longer validates; "
733
+ "run 'railmux runtime status --verify'",
734
+ file=sys.stderr,
735
+ )
736
+ return 2
737
+ upgrade_env = os.environ.copy()
738
+ upgrade_env["RAILMUX_MSYS2_RUNTIME_ID"] = request.runtime
739
+ upgrade_env["RAILMUX_MSYS2_APP_ID"] = request.app
740
+ os.execve(upgrade_argv[0], upgrade_argv, upgrade_env)
679
741
  return 0
680
742
 
681
743
 
@@ -782,6 +782,15 @@ def main(
782
782
  metavar="HOST",
783
783
  help="edit the configuration owned by an SSH destination",
784
784
  )
785
+ parser.add_argument(
786
+ "--remote-platform",
787
+ choices=("auto", "posix", "windows"),
788
+ default="auto",
789
+ help=(
790
+ "remote shell family for --remote: auto, posix, or windows "
791
+ "(default: auto)"
792
+ ),
793
+ )
785
794
  parser.add_argument(
786
795
  "--ssh-arg",
787
796
  action=AppendSshArgument,
@@ -810,6 +819,8 @@ def main(
810
819
  parser.error("--remote and --remote-context cannot be combined")
811
820
  if args.ssh_arg and not args.remote:
812
821
  parser.error("--ssh-args requires --remote")
822
+ if args.remote_platform != "auto" and not args.remote:
823
+ parser.error("--remote-platform requires --remote")
813
824
  except SystemExit as exc:
814
825
  return int(exc.code)
815
826
  if args.remote:
@@ -819,6 +830,7 @@ def main(
819
830
  args.remote,
820
831
  ssh_args=args.ssh_arg,
821
832
  raw_argv=tuple([] if argv is None else argv),
833
+ remote_platform=args.remote_platform,
822
834
  )
823
835
  remote_context = bool(args.remote_context)
824
836
  stdin = sys.stdin if stdin is None else stdin
@@ -38,7 +38,7 @@ from railmux.terminal_status import (
38
38
  _VERSION_RE = re.compile(
39
39
  r"(?<![A-Za-z0-9])v?(\d+(?:\.\d+){1,3}(?:[A-Za-z]|[-+][0-9A-Za-z.-]+)?)"
40
40
  )
41
- DOCTOR_SCHEMA_VERSION = 3
41
+ DOCTOR_SCHEMA_VERSION = 4
42
42
 
43
43
 
44
44
  @dataclass(frozen=True)
@@ -79,6 +79,15 @@ class DirectoryDiagnostic:
79
79
  writable: bool
80
80
 
81
81
 
82
+ @dataclass(frozen=True)
83
+ class ManagedWindowsDiagnostic:
84
+ runtime_id: str | None
85
+ app_version: str | None
86
+ base_content_id: str | None
87
+ running_ui_version: str | None
88
+ transition_status: str | None
89
+
90
+
82
91
  @dataclass(frozen=True)
83
92
  class DoctorSnapshot:
84
93
  """Versioned, privacy-safe authority shared by text and JSON output."""
@@ -103,6 +112,7 @@ class DoctorSnapshot:
103
112
  preferred_agent_display: str
104
113
  data_directories: dict[str, DirectoryDiagnostic]
105
114
  ssh_display: SshDisplayDiagnostic
115
+ managed_windows: ManagedWindowsDiagnostic | None = None
106
116
 
107
117
 
108
118
  def is_ssh_session(environ: dict[str, str] | None = None) -> bool:
@@ -350,6 +360,11 @@ def collect_doctor_snapshot(
350
360
  else _legacy_tmux_diagnostic()
351
361
  )
352
362
  )
363
+ managed_windows = None
364
+ if running_in_windows_wrapper(env):
365
+ from railmux.windows_ui_transition import diagnostic_status
366
+
367
+ managed_windows = ManagedWindowsDiagnostic(**diagnostic_status(env))
353
368
  return DoctorSnapshot(
354
369
  schema_version=DOCTOR_SCHEMA_VERSION,
355
370
  railmux_version=__version__,
@@ -376,6 +391,7 @@ def collect_doctor_snapshot(
376
391
  ),
377
392
  },
378
393
  ssh_display=read_ssh_display_diagnostic(),
394
+ managed_windows=managed_windows,
379
395
  )
380
396
 
381
397
 
@@ -495,7 +511,7 @@ def _ssh_display_text(diagnostic: SshDisplayDiagnostic) -> str:
495
511
 
496
512
  def render_doctor_text(snapshot: DoctorSnapshot) -> str:
497
513
  """Render the stable human report from the structured authority."""
498
- lines = (
514
+ lines = [
499
515
  "Railmux diagnostics",
500
516
  f"Railmux: {snapshot.railmux_version}",
501
517
  f"Python: {snapshot.python_version}",
@@ -525,6 +541,22 @@ def render_doctor_text(snapshot: DoctorSnapshot) -> str:
525
541
  f"Config: {_config_text(snapshot.config)}",
526
542
  "Settings repair: run 'railmux config' (no tmux required)",
527
543
  f"Preferred agent display: {snapshot.preferred_agent_display}",
544
+ ]
545
+ if snapshot.managed_windows is not None:
546
+ managed = snapshot.managed_windows
547
+ identity = (
548
+ managed.base_content_id[:12]
549
+ if managed.base_content_id is not None
550
+ else "unavailable"
551
+ )
552
+ lines.extend((
553
+ f"Windows managed runtime: {managed.runtime_id or 'unavailable'}; "
554
+ f"content={identity}",
555
+ f"Windows app layer: installed={managed.app_version or 'unavailable'}, "
556
+ f"running={managed.running_ui_version or 'not running'}, "
557
+ f"transition={managed.transition_status or 'none'}",
558
+ ))
559
+ lines.extend((
528
560
  (
529
561
  "Most recent railmux ssh (host not recorded): "
530
562
  f"{_ssh_display_text(snapshot.ssh_display)}"
@@ -535,7 +567,7 @@ def render_doctor_text(snapshot: DoctorSnapshot) -> str:
535
567
  "Privacy: session IDs, transcript content, credentials, hostnames, "
536
568
  "and raw custom paths are omitted; review before sharing."
537
569
  ),
538
- )
570
+ ))
539
571
  return "\n".join(lines)
540
572
 
541
573
 
@@ -281,6 +281,7 @@ class RemoteHello:
281
281
  config_status: str = "valid"
282
282
  tmux_configured: bool = False
283
283
  config_protocol: int = 0
284
+ platform: str = "posix"
284
285
 
285
286
 
286
287
  @dataclass(frozen=True)
@@ -296,6 +297,11 @@ class RemoteStartKind(Enum):
296
297
  TIMEOUT = "timeout"
297
298
 
298
299
 
300
+ class RemoteLaunchMode(Enum):
301
+ POSIX = "posix"
302
+ DIRECT = "direct"
303
+
304
+
299
305
  class RemoteAttachKind(Enum):
300
306
  ACCEPTED = "accepted"
301
307
  BUSY = "busy"
@@ -310,6 +316,15 @@ class RemoteStartup:
310
316
  returncode: int | None = None
311
317
 
312
318
 
319
+ @dataclass(frozen=True)
320
+ class RemoteProbe:
321
+ """One cooked-mode launch-family probe before any attach mutation."""
322
+
323
+ process: subprocess.Popen
324
+ startup: RemoteStartup
325
+ launch_mode: RemoteLaunchMode
326
+
327
+
313
328
  def await_remote_attach_status(
314
329
  process: subprocess.Popen,
315
330
  timeout: float = _REMOTE_ATTACH_TIMEOUT,
@@ -369,6 +384,7 @@ def parse_remote_hello(line: bytes) -> RemoteHello:
369
384
  config_status = value.get("config_status", "valid")
370
385
  tmux_configured = value.get("tmux_configured", False)
371
386
  config_protocol = value.get("config_protocol", 0)
387
+ platform = value.get("platform", "posix")
372
388
  if (
373
389
  not isinstance(version, str)
374
390
  or not version
@@ -383,6 +399,7 @@ def parse_remote_hello(line: bytes) -> RemoteHello:
383
399
  or not isinstance(config_protocol, int)
384
400
  or isinstance(config_protocol, bool)
385
401
  or not 0 <= config_protocol <= 65535
402
+ or platform not in {"posix", "windows-msys2"}
386
403
  ):
387
404
  raise ValueError("invalid Railmux remote hello")
388
405
  try:
@@ -397,6 +414,7 @@ def parse_remote_hello(line: bytes) -> RemoteHello:
397
414
  config_status,
398
415
  tmux_configured,
399
416
  config_protocol,
417
+ platform,
400
418
  )
401
419
 
402
420
 
@@ -1540,12 +1558,18 @@ def _remote_launch_command(server_args: Sequence[str]) -> str:
1540
1558
  return "; ".join(branches)
1541
1559
 
1542
1560
 
1561
+ def _remote_direct_launch_command(server_args: Sequence[str]) -> str:
1562
+ """Build a shell-neutral command accepted by Windows OpenSSH PowerShell."""
1563
+ return shlex.join(["railmux", *server_args])
1564
+
1565
+
1543
1566
  def build_remote_command_argv(
1544
1567
  destination: str,
1545
1568
  *,
1546
1569
  remote_args: Sequence[str],
1547
1570
  ssh_args: Sequence[str],
1548
1571
  force_tty: bool = False,
1572
+ launch_mode: RemoteLaunchMode = RemoteLaunchMode.POSIX,
1549
1573
  ) -> list[str]:
1550
1574
  """Build SSH argv through the shared remote Railmux discovery ladder."""
1551
1575
  return [
@@ -1556,10 +1580,83 @@ def build_remote_command_argv(
1556
1580
  # user arguments so a copied -t/-T flag cannot invert the transport.
1557
1581
  "-tt" if force_tty else "-T",
1558
1582
  destination,
1559
- _remote_launch_command(remote_args),
1583
+ (
1584
+ _remote_direct_launch_command(remote_args)
1585
+ if launch_mode is RemoteLaunchMode.DIRECT
1586
+ else _remote_launch_command(remote_args)
1587
+ ),
1560
1588
  ]
1561
1589
 
1562
1590
 
1591
+ def probe_remote_launch(
1592
+ destination: str,
1593
+ *,
1594
+ remote_args: Sequence[str],
1595
+ ssh_args: Sequence[str],
1596
+ remote_platform: str = "auto",
1597
+ force_tty: bool = False,
1598
+ timeout: float = _REMOTE_HELLO_TIMEOUT,
1599
+ ) -> RemoteProbe:
1600
+ """Probe one remote Railmux command and pin its shell launch family.
1601
+
1602
+ ``auto`` preserves the POSIX discovery ladder, then makes one direct
1603
+ shell-neutral attempt only after a non-SSH launch failure. Each shell
1604
+ family receives the full bounded startup timeout. Explicit Windows mode
1605
+ requires a managed Windows hello; auto mode accepts an authoritative POSIX
1606
+ hello reached through a shell-neutral direct command.
1607
+ """
1608
+ if remote_platform not in {"auto", "posix", "windows"}:
1609
+ raise ValueError("invalid remote platform")
1610
+ launch_mode = (
1611
+ RemoteLaunchMode.DIRECT
1612
+ if remote_platform == "windows"
1613
+ else RemoteLaunchMode.POSIX
1614
+ )
1615
+ effective_ssh_args = (
1616
+ *ssh_args,
1617
+ "-o",
1618
+ "ServerAliveInterval=5",
1619
+ "-o",
1620
+ "ServerAliveCountMax=3",
1621
+ )
1622
+
1623
+ def start(mode: RemoteLaunchMode) -> tuple[subprocess.Popen, RemoteStartup]:
1624
+ argv = build_remote_command_argv(
1625
+ destination,
1626
+ remote_args=remote_args,
1627
+ ssh_args=effective_ssh_args,
1628
+ force_tty=force_tty,
1629
+ launch_mode=mode,
1630
+ )
1631
+ process = _spawn_remote(argv)
1632
+ startup = await_remote_startup(process, timeout=timeout)
1633
+ return process, startup
1634
+
1635
+ process, startup = start(launch_mode)
1636
+ if (
1637
+ remote_platform == "auto"
1638
+ and launch_mode is RemoteLaunchMode.POSIX
1639
+ and startup.kind is RemoteStartKind.FAILED
1640
+ and startup.returncode != 255
1641
+ ):
1642
+ _stop_unstarted_remote(process)
1643
+ launch_mode = RemoteLaunchMode.DIRECT
1644
+ process, startup = start(launch_mode)
1645
+
1646
+ if (
1647
+ startup.kind is RemoteStartKind.HELLO
1648
+ and startup.hello is not None
1649
+ and remote_platform == "windows"
1650
+ and startup.hello.platform != "windows-msys2"
1651
+ ):
1652
+ _stop_unstarted_remote(process)
1653
+ raise ProbeError(
1654
+ "the direct remote launcher answered from a non-Windows Railmux "
1655
+ "runtime; use --remote-platform posix for that host"
1656
+ )
1657
+ return RemoteProbe(process, startup, launch_mode)
1658
+
1659
+
1563
1660
  def build_ssh_argv(
1564
1661
  destination: str,
1565
1662
  *,
@@ -1570,6 +1667,7 @@ def build_ssh_argv(
1570
1667
  ssh_args: Sequence[str],
1571
1668
  replace_existing_client: bool = False,
1572
1669
  existing_session_only: bool = False,
1670
+ launch_mode: RemoteLaunchMode = RemoteLaunchMode.POSIX,
1573
1671
  ) -> list[str]:
1574
1672
  server_args = _remote_server_args(
1575
1673
  session=session,
@@ -1592,6 +1690,7 @@ def build_ssh_argv(
1592
1690
  "-o",
1593
1691
  "ServerAliveCountMax=3",
1594
1692
  ),
1693
+ launch_mode=launch_mode,
1595
1694
  )
1596
1695
 
1597
1696
 
@@ -1757,6 +1856,55 @@ def remote_install_help(destination: str, version: str) -> str:
1757
1856
  )
1758
1857
 
1759
1858
 
1859
+ def remote_windows_install_help(destination: str, version: str) -> str:
1860
+ return (
1861
+ f"Update the user-level Windows preview on {destination} from "
1862
+ "PowerShell, then retry:\n"
1863
+ f" py -m pip install --upgrade railmux=={version}\n"
1864
+ " railmux runtime install --yes\n"
1865
+ "The runtime command installs only the matching Railmux app layer and "
1866
+ "reuses a verified managed MSYS2 base when its pinned version is "
1867
+ "unchanged. It does not modify Codex or Claude session files. If 'py' "
1868
+ "is unavailable, use the same Windows Python executable that installed "
1869
+ "the Railmux command."
1870
+ )
1871
+
1872
+
1873
+ def local_windows_update_help(version: str) -> str:
1874
+ """Return the native-owner update path for a managed Windows client."""
1875
+ return (
1876
+ "Update this Windows Railmux installation from PowerShell, then "
1877
+ "retry:\n"
1878
+ f" py -m pip install --upgrade railmux=={version}\n"
1879
+ " railmux runtime install --yes\n"
1880
+ "Do not update the versioned MSYS2 app environment with its private "
1881
+ "pip; the native bootstrap owns application-layer installation."
1882
+ )
1883
+
1884
+
1885
+ def _remote_launch_mode(args: argparse.Namespace) -> RemoteLaunchMode:
1886
+ selected = getattr(args, "_selected_remote_launch_mode", None)
1887
+ if isinstance(selected, RemoteLaunchMode):
1888
+ return selected
1889
+ if getattr(args, "remote_platform", "auto") == "windows":
1890
+ return RemoteLaunchMode.DIRECT
1891
+ return RemoteLaunchMode.POSIX
1892
+
1893
+
1894
+ def _remember_remote_launch_mode(
1895
+ args: argparse.Namespace, mode: RemoteLaunchMode
1896
+ ) -> None:
1897
+ args._selected_remote_launch_mode = mode
1898
+
1899
+
1900
+ def _remote_uses_windows_runtime(
1901
+ args: argparse.Namespace, hello: RemoteHello | None = None
1902
+ ) -> bool:
1903
+ if hello is not None:
1904
+ return hello.platform == "windows-msys2"
1905
+ return _remote_launch_mode(args) is RemoteLaunchMode.DIRECT
1906
+
1907
+
1760
1908
  def remote_tmux_help(destination: str) -> str:
1761
1909
  return (
1762
1910
  f"tmux is not installed or not on PATH on {destination}. Install it "
@@ -1823,8 +1971,12 @@ def _upgrade_local_and_restart(
1823
1971
  *,
1824
1972
  subcommand: str = "ssh",
1825
1973
  ) -> NoReturn:
1974
+ from railmux.provider_paths import running_in_windows_wrapper
1826
1975
  from railmux.self_update import installed_version_matches
1827
1976
 
1977
+ if running_in_windows_wrapper():
1978
+ raise ProbeError(local_windows_update_help(version))
1979
+
1828
1980
  argv = _local_upgrade_argv(version)
1829
1981
  command_label = f"railmux {subcommand}"
1830
1982
  print(
@@ -1989,6 +2141,7 @@ def _reconnect_remote_attach(
1989
2141
  ssh_args=ssh_args,
1990
2142
  replace_existing_client=replace_existing_client,
1991
2143
  existing_session_only=existing_session_only,
2144
+ launch_mode=_remote_launch_mode(args),
1992
2145
  )
1993
2146
  process = (
1994
2147
  _spawn_remote(argv, suppress_stderr=True)
@@ -2215,18 +2368,55 @@ def prepare_remote_process(
2215
2368
  if before_interaction is not None:
2216
2369
  before_interaction()
2217
2370
 
2218
- argv = build_ssh_argv(
2219
- args.destination,
2371
+ if on_stage is not None:
2372
+ on_stage("Connecting to remote host…")
2373
+ remote_args = _remote_server_args(
2220
2374
  session=args.session,
2221
2375
  width=current_size.columns,
2222
2376
  height=current_size.lines,
2223
2377
  fps=args.fps,
2378
+ )
2379
+ probe = probe_remote_launch(
2380
+ args.destination,
2381
+ remote_args=remote_args,
2224
2382
  ssh_args=args.ssh_arg,
2383
+ remote_platform=getattr(args, "remote_platform", "auto"),
2225
2384
  )
2226
- if on_stage is not None:
2227
- on_stage("Connecting to remote host…")
2228
- process = _spawn_remote(argv)
2229
- startup = await_remote_startup(process)
2385
+ process = probe.process
2386
+ startup = probe.startup
2387
+ launch_mode = probe.launch_mode
2388
+ if startup.kind is RemoteStartKind.HELLO:
2389
+ _remember_remote_launch_mode(args, launch_mode)
2390
+ elif (
2391
+ getattr(args, "remote_platform", "auto") == "auto"
2392
+ and launch_mode is RemoteLaunchMode.DIRECT
2393
+ ):
2394
+ _stop_unstarted_remote(process)
2395
+ if startup.returncode == 255:
2396
+ raise ProbeError("ssh could not connect to the remote host")
2397
+ if startup.kind is RemoteStartKind.TIMEOUT:
2398
+ raise ProbeError(
2399
+ "timed out while checking the remote host's direct Railmux launcher"
2400
+ )
2401
+ raise ProbeError(
2402
+ "the remote shell did not accept the POSIX discovery command, "
2403
+ "and a direct Railmux launch did not complete. If this is a "
2404
+ "Windows preview host:\n"
2405
+ f"{remote_windows_install_help(args.destination, __version__)}"
2406
+ )
2407
+
2408
+ if (
2409
+ getattr(args, "remote_platform", "auto") == "windows"
2410
+ and startup.kind is not RemoteStartKind.HELLO
2411
+ ):
2412
+ _stop_unstarted_remote(process)
2413
+ if startup.returncode == 255:
2414
+ raise ProbeError("ssh could not connect to the remote host")
2415
+ if startup.kind is RemoteStartKind.TIMEOUT:
2416
+ raise ProbeError(
2417
+ "timed out waiting for the Windows Railmux compatibility handshake"
2418
+ )
2419
+ raise ProbeError(remote_windows_install_help(args.destination, __version__))
2230
2420
  install_version = __version__
2231
2421
  install_reason: str | None = None
2232
2422
  if startup.kind is RemoteStartKind.MISSING:
@@ -2281,6 +2471,12 @@ def prepare_remote_process(
2281
2471
  if decision.action == "prompt" and decision.prompt == "remote_install":
2282
2472
  assert decision.reason is not None
2283
2473
  assert decision.install_version is not None
2474
+ if _remote_uses_windows_runtime(args, hello):
2475
+ _stop_unstarted_remote(process)
2476
+ raise ProbeError(
2477
+ f"{decision.reason}\n"
2478
+ f"{remote_windows_install_help(args.destination, decision.install_version)}"
2479
+ )
2284
2480
  consents["remote_install"] = _confirm_remote_install(
2285
2481
  args,
2286
2482
  decision.reason,
@@ -2295,9 +2491,14 @@ def prepare_remote_process(
2295
2491
  _stop_unstarted_remote(process)
2296
2492
  message = decision.reason or "incompatible remote Railmux"
2297
2493
  if decision.install_version is not None:
2494
+ install_help = (
2495
+ remote_windows_install_help
2496
+ if _remote_uses_windows_runtime(args, hello)
2497
+ else remote_install_help
2498
+ )
2298
2499
  message = (
2299
2500
  f"{message}\n"
2300
- f"{remote_install_help(args.destination, decision.install_version)}"
2501
+ f"{install_help(args.destination, decision.install_version)}"
2301
2502
  )
2302
2503
  raise ProbeError(message)
2303
2504
  if decision.warning is not None:
@@ -2320,6 +2521,14 @@ def prepare_remote_process(
2320
2521
  raise AssertionError(f"unknown compatibility action: {decision.action}")
2321
2522
 
2322
2523
  assert install_reason is not None
2524
+ if _remote_uses_windows_runtime(
2525
+ args, startup.hello if startup.kind is RemoteStartKind.HELLO else None
2526
+ ):
2527
+ _stop_unstarted_remote(process)
2528
+ raise ProbeError(
2529
+ f"{install_reason}\n"
2530
+ f"{remote_windows_install_help(args.destination, install_version)}"
2531
+ )
2323
2532
  if startup.kind is not RemoteStartKind.HELLO and not _confirm_remote_install(
2324
2533
  args,
2325
2534
  install_reason,
@@ -2430,6 +2639,16 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace:
2430
2639
  default=20.0,
2431
2640
  help="maximum coalesced display update rate, 1-60 (default: 20)",
2432
2641
  )
2642
+ parser.add_argument(
2643
+ "--remote-platform",
2644
+ choices=("auto", "posix", "windows"),
2645
+ default="auto",
2646
+ help=(
2647
+ "remote shell family: auto tries POSIX discovery then direct "
2648
+ "Windows launch; windows avoids the failed POSIX probe and an "
2649
+ "extra password prompt (default: auto)"
2650
+ ),
2651
+ )
2433
2652
  parser.add_argument(
2434
2653
  "--no-mouse",
2435
2654
  action="store_true",
@@ -248,6 +248,11 @@ def _emit_remote_hello(
248
248
  "config_status": config_status,
249
249
  "tmux_configured": tmux_configured,
250
250
  "config_protocol": REMOTE_CONFIG_PROTOCOL,
251
+ "platform": (
252
+ "windows-msys2"
253
+ if os.environ.get("RAILMUX_WINDOWS_RUNTIME") == "msys2"
254
+ else "posix"
255
+ ),
251
256
  "version": __version__,
252
257
  },
253
258
  separators=(",", ":"),
@@ -12,6 +12,7 @@ from pathlib import Path
12
12
 
13
13
  _DRIVE_PATH = re.compile(r"^(?:\\\\\?\\)?([A-Za-z]):[\\/](.*)\Z")
14
14
  _MANAGED_BASE_MARKER = Path("/railmux-base.json")
15
+ _MANAGED_BASE_CONTENT_MARKER = Path("/railmux-base-content-v1.json")
15
16
  _MANAGED_APP_ROOT = Path("/opt/railmux/apps")
16
17
  _MANAGED_APP_ID = re.compile(
17
18
  r"railmux-[0-9]+(?:\.[0-9]+)*(?:\.dev[0-9]+)?\Z"
@@ -59,14 +60,34 @@ def running_in_managed_windows_wrapper(
59
60
  )
60
61
  from railmux import __version__
61
62
 
62
- return base_payload == {
63
+ base_valid = base_payload == {
63
64
  "schema": 1,
64
65
  "runtime": runtime_id,
65
- } and app_payload == {
66
+ }
67
+ legacy_app = app_payload == {
66
68
  "schema": 1,
67
69
  "runtime": runtime_id,
68
70
  "railmux": __version__,
69
71
  }
72
+ content_payload = read_safe_marker(_MANAGED_BASE_CONTENT_MARKER)
73
+ content_id = (
74
+ content_payload.get("content_id")
75
+ if isinstance(content_payload, dict)
76
+ and content_payload.get("schema") == 1
77
+ and content_payload.get("runtime") == runtime_id
78
+ else None
79
+ )
80
+ exact_app = bool(
81
+ isinstance(content_id, str)
82
+ and re.fullmatch(r"[0-9a-f]{64}", content_id)
83
+ and app_payload == {
84
+ "schema": 2,
85
+ "runtime": runtime_id,
86
+ "railmux": __version__,
87
+ "base_content_id": content_id,
88
+ }
89
+ )
90
+ return base_valid and (legacy_app or exact_app)
70
91
 
71
92
 
72
93
  def private_mode_is_safe(mode: int) -> bool: