railmux 0.4.0.dev30__tar.gz → 0.4.0.dev32__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 (183) hide show
  1. {railmux-0.4.0.dev30/src/railmux.egg-info → railmux-0.4.0.dev32}/PKG-INFO +1 -1
  2. railmux-0.4.0.dev32/src/railmux/__init__.py +1 -0
  3. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/cli.py +27 -1
  4. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/ui/app.py +27 -13
  5. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/windows_bootstrap.py +12 -0
  6. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/windows_install_log.py +13 -2
  7. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/windows_msys2.py +339 -12
  8. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/windows_pacman.py +24 -3
  9. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32/src/railmux.egg-info}/PKG-INFO +1 -1
  10. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_cli.py +75 -0
  11. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_focus_highlight.py +2 -0
  12. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_sessions_pane.py +33 -0
  13. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_windows_bootstrap.py +30 -1
  14. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_windows_install_log.py +13 -0
  15. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_windows_msys2.py +183 -0
  16. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_windows_pacman.py +35 -0
  17. railmux-0.4.0.dev30/src/railmux/__init__.py +0 -1
  18. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/LICENSE +0 -0
  19. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/MANIFEST.in +0 -0
  20. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/README.md +0 -0
  21. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/pyproject.toml +0 -0
  22. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/setup.cfg +0 -0
  23. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/__main__.py +0 -0
  24. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/atomic_file.py +0 -0
  25. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/background_index.py +0 -0
  26. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/codex_index.py +0 -0
  27. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/config.py +0 -0
  28. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/config_cli.py +0 -0
  29. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/diagnostics.py +0 -0
  30. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/discovery.py +0 -0
  31. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/display_transport.py +0 -0
  32. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/entrypoint.py +0 -0
  33. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/fast_display_client.py +0 -0
  34. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/fast_display_history.py +0 -0
  35. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/fast_display_input.py +0 -0
  36. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/fast_display_protocol.py +0 -0
  37. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/fast_display_server.py +0 -0
  38. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/favorites.py +0 -0
  39. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/fuzzy.py +0 -0
  40. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/help_workspace.py +0 -0
  41. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/launcher.py +0 -0
  42. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/legacy_sessions.py +0 -0
  43. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/local_clipboard.py +0 -0
  44. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/local_open.py +0 -0
  45. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/models.py +0 -0
  46. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/modes.py +0 -0
  47. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/mouse_manager.py +0 -0
  48. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/orphan_marker.py +0 -0
  49. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/pane_surface.py +0 -0
  50. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/path_codec.py +0 -0
  51. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/provider_paths.py +0 -0
  52. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/remote_config.py +0 -0
  53. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/renames.py +0 -0
  54. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/restart_state.py +0 -0
  55. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/runtime_config.py +0 -0
  56. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/scroll_agent.py +0 -0
  57. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/scroll_manager.py +0 -0
  58. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/selection_isolation.py +0 -0
  59. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/self_update.py +0 -0
  60. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/session_cache.py +0 -0
  61. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/session_index.py +0 -0
  62. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/session_lease.py +0 -0
  63. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/setting_contracts.py +0 -0
  64. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/settings.py +0 -0
  65. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/ssh_args.py +0 -0
  66. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/ssh_compat.py +0 -0
  67. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/ssh_display_diagnostics.py +0 -0
  68. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/ssh_doctor.py +0 -0
  69. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/system_deps.py +0 -0
  70. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/terminal_status.py +0 -0
  71. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/tmux_binding_manager.py +0 -0
  72. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/tmux_ctl.py +0 -0
  73. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/tmux_health.py +0 -0
  74. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/tmux_server.py +0 -0
  75. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/tool_panes.py +0 -0
  76. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/transcript.py +0 -0
  77. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/ui/__init__.py +0 -0
  78. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/ui/_widgets.py +0 -0
  79. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/ui/keymap.py +0 -0
  80. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/ui/modals.py +0 -0
  81. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/ui/projects_pane.py +0 -0
  82. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/ui/running_pane.py +0 -0
  83. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/ui/sessions_pane.py +0 -0
  84. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/ui/sidebar.py +0 -0
  85. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/ui/statusbar.py +0 -0
  86. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/ui/workspace.py +0 -0
  87. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/windows_attach_relay.py +0 -0
  88. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/windows_paths.py +0 -0
  89. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/windows_tmux_lifecycle.py +0 -0
  90. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux/windows_ui_transition.py +0 -0
  91. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux.egg-info/SOURCES.txt +0 -0
  92. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux.egg-info/dependency_links.txt +0 -0
  93. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux.egg-info/entry_points.txt +0 -0
  94. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux.egg-info/requires.txt +0 -0
  95. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/src/railmux.egg-info/top_level.txt +0 -0
  96. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_agent_workspace.py +0 -0
  97. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_atomic_file.py +0 -0
  98. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_background_index.py +0 -0
  99. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_codex_delete.py +0 -0
  100. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_codex_index.py +0 -0
  101. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_codex_lineage.py +0 -0
  102. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_codex_transcript.py +0 -0
  103. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_config.py +0 -0
  104. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_config_cli.py +0 -0
  105. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_denested_transport_benchmark.py +0 -0
  106. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_diagnostics.py +0 -0
  107. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_discovery.py +0 -0
  108. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_display_transport.py +0 -0
  109. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_fast_full_window.py +0 -0
  110. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_fullscreen_binding.py +0 -0
  111. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_fuzzy.py +0 -0
  112. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_help_agent.py +0 -0
  113. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_help_workspace.py +0 -0
  114. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_history_cleanup.py +0 -0
  115. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_keymap.py +0 -0
  116. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_launcher.py +0 -0
  117. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_layout_profile.py +0 -0
  118. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_legacy_sessions.py +0 -0
  119. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_local_clipboard.py +0 -0
  120. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_local_open.py +0 -0
  121. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_modals.py +0 -0
  122. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_mode_selection.py +0 -0
  123. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_modes.py +0 -0
  124. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_mouse_manager.py +0 -0
  125. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_new_project.py +0 -0
  126. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_options.py +0 -0
  127. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_orphan_recovery.py +0 -0
  128. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_pane_surface.py +0 -0
  129. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_path_codec.py +0 -0
  130. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_projects_pane.py +0 -0
  131. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_provider_paths.py +0 -0
  132. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_release_notes.py +0 -0
  133. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_release_workflow.py +0 -0
  134. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_remote_config.py +0 -0
  135. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_renames.py +0 -0
  136. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_restart_state.py +0 -0
  137. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_running_pane.py +0 -0
  138. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_running_sort.py +0 -0
  139. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_runtime_config.py +0 -0
  140. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_scroll_agent.py +0 -0
  141. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_scroll_manager.py +0 -0
  142. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_selection_isolation.py +0 -0
  143. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_self_update.py +0 -0
  144. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_session_cache.py +0 -0
  145. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_session_index.py +0 -0
  146. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_session_lease.py +0 -0
  147. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_setting_contracts.py +0 -0
  148. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_settings.py +0 -0
  149. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_sidebar.py +0 -0
  150. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_soft_quit.py +0 -0
  151. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_ssh_args.py +0 -0
  152. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_ssh_compat.py +0 -0
  153. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_ssh_display_benchmark.py +0 -0
  154. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_ssh_display_diagnostics.py +0 -0
  155. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_ssh_doctor.py +0 -0
  156. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_status.py +0 -0
  157. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_statusbar.py +0 -0
  158. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_system_deps.py +0 -0
  159. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_terminal_size.py +0 -0
  160. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_terminal_status.py +0 -0
  161. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_termux_local_touch.py +0 -0
  162. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_tmux_binding_manager.py +0 -0
  163. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_tmux_ctl.py +0 -0
  164. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_tmux_health.py +0 -0
  165. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_tmux_integration.py +0 -0
  166. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_tmux_server.py +0 -0
  167. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_tmux_status.py +0 -0
  168. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_tool_panes.py +0 -0
  169. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_transcript.py +0 -0
  170. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_utf8_state.py +0 -0
  171. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_web_demo_recorder.py +0 -0
  172. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_wheel_smoke.py +0 -0
  173. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_widgets.py +0 -0
  174. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_windows_attach_relay.py +0 -0
  175. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_windows_paths.py +0 -0
  176. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_windows_tmux_lifecycle.py +0 -0
  177. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_windows_ui_transition.py +0 -0
  178. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_windows_wrapper_contract.py +0 -0
  179. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tests/test_yolo_prompt.py +0 -0
  180. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tools/denested_transport_benchmark.py +0 -0
  181. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tools/release_notes.py +0 -0
  182. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tools/wheel_smoke.py +0 -0
  183. {railmux-0.4.0.dev30 → railmux-0.4.0.dev32}/tools/windows_archive_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.4.0.dev30
3
+ Version: 0.4.0.dev32
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.dev32"
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import argparse
4
4
  import os
5
+ import signal
5
6
  import shutil
6
7
  import subprocess
7
8
  import sys
@@ -109,6 +110,9 @@ def _run_tmux_client_with_watchdog(
109
110
  attributes = termios.tcgetattr(sys.stdin.fileno())
110
111
  except (OSError, termios.error):
111
112
  pass
113
+ direct_terminal_size = (
114
+ _interactive_terminal_size() if managed_windows else None
115
+ )
112
116
  started_at = time.monotonic()
113
117
  try:
114
118
  popen_kwargs: dict[str, object] = {"env": env}
@@ -127,7 +131,7 @@ def _run_tmux_client_with_watchdog(
127
131
  *,
128
132
  asynchronous_probe: bool = False,
129
133
  ) -> tuple[int, bool]:
130
- nonlocal expected_target, expected_session_id
134
+ nonlocal direct_terminal_size, expected_target, expected_session_id
131
135
  watchdog = tmux_health.FailureWatchdog.starting(
132
136
  (
133
137
  time.monotonic()
@@ -151,6 +155,28 @@ def _run_tmux_client_with_watchdog(
151
155
  pump = getattr(process, "pump", None)
152
156
  if pump is None:
153
157
  time.sleep(0.25)
158
+ if managed_windows:
159
+ current_size = _interactive_terminal_size()
160
+ if (
161
+ current_size is not None
162
+ and current_size != direct_terminal_size
163
+ ):
164
+ direct_terminal_size = current_size
165
+ pid = getattr(process, "pid", None)
166
+ if (
167
+ isinstance(pid, int)
168
+ and not isinstance(pid, bool)
169
+ and pid > 0
170
+ ):
171
+ try:
172
+ # MSYS2 occasionally misses the native
173
+ # console's resize notification. Signal only
174
+ # this launcher's direct tmux client so it
175
+ # re-reads both dimensions; tmux remains the
176
+ # authority for shared-window sizing.
177
+ os.kill(pid, signal.SIGWINCH)
178
+ except (OSError, ValueError):
179
+ pass
154
180
  else:
155
181
  pump(0.25)
156
182
  now = time.monotonic()
@@ -2035,21 +2035,35 @@ class App:
2035
2035
 
2036
2036
  # --- history preview (display pane shows a transcript, not an agent session) ---
2037
2037
 
2038
+ def _paint_session_row_click_now(self, session_id: str | None) -> None:
2039
+ """Acknowledge a pointer selection before synchronous tmux work."""
2040
+ pane = getattr(self, "_sessions_pane", None)
2041
+ if pane is None:
2042
+ return
2043
+ pane.set_selected_session(session_id)
2044
+ loop = getattr(self, "_loop", None)
2045
+ if loop is not None:
2046
+ loop.draw_screen()
2047
+
2038
2048
  def _on_session_row_preview(self, session: SessionMeta) -> None:
2039
2049
  """Apply click semantics after rechecking whether the row is live."""
2040
- running = self._by_session_id(session.session_id)
2041
- if (running is not None
2042
- and self._agent_session_alive(running.tmux_name)):
2043
- self._on_running_select(
2044
- RunningEntry(
2045
- tmux_name=running.tmux_name,
2046
- label=running.label,
2047
- status=running.status,
2048
- ),
2049
- steal_focus=False,
2050
- )
2051
- return
2052
- self._on_session_preview(session)
2050
+ self._paint_session_row_click_now(session.session_id)
2051
+ try:
2052
+ running = self._by_session_id(session.session_id)
2053
+ if (running is not None
2054
+ and self._agent_session_alive(running.tmux_name)):
2055
+ self._on_running_select(
2056
+ RunningEntry(
2057
+ tmux_name=running.tmux_name,
2058
+ label=running.label,
2059
+ status=running.status,
2060
+ ),
2061
+ steal_focus=False,
2062
+ )
2063
+ return
2064
+ self._on_session_preview(session)
2065
+ finally:
2066
+ self._paint_session_row_click_now(None)
2053
2067
 
2054
2068
  def _on_session_preview(self, session: SessionMeta) -> None:
2055
2069
  """Show the provider's canonical current-branch history.
@@ -20,6 +20,7 @@ from railmux.windows_msys2 import (
20
20
  plan_managed_runtime_prune,
21
21
  reusable_managed_base_candidate,
22
22
  )
23
+ from railmux.windows_paths import managed_windows_data_root
23
24
 
24
25
 
25
26
  _MINIMUM_WINDOWS_PYTHON = (3, 10)
@@ -49,6 +50,14 @@ def _runtime_status(
49
50
  ) -> int:
50
51
  snapshot = managed_runtime_status(
51
52
  version=__version__, environ=environ, verify=verify)
53
+ data_root = (
54
+ managed_windows_data_root(environ)
55
+ if runtime is None or runtime.managed
56
+ else None
57
+ )
58
+ if data_root is not None:
59
+ snapshot["data_root"] = str(data_root)
60
+ snapshot["log_directory"] = str(data_root / "logs")
52
61
  if json_output:
53
62
  json.dump(snapshot, sys.stdout, indent=2, sort_keys=True)
54
63
  print()
@@ -75,6 +84,9 @@ def _runtime_status(
75
84
  if root is not None:
76
85
  print(f"Managed location: {root}")
77
86
  print("Next: run 'railmux runtime install' from an interactive PowerShell.")
87
+ if data_root is not None:
88
+ print(f"Data root: {data_root}")
89
+ print(f"Install logs: {data_root / 'logs'}")
78
90
  print("Provider data: shared from the Windows user profile")
79
91
  content = snapshot.get("content_identity")
80
92
  if isinstance(content, str):
@@ -162,7 +162,12 @@ class InstallReporter:
162
162
 
163
163
  def __enter__(self) -> InstallReporter:
164
164
  self.path.parent.mkdir(parents=True, exist_ok=True)
165
- self._log = self.path.open("x", encoding="utf-8", newline="\n")
165
+ # Windows PowerShell 5 treats BOM-less UTF-8 as the active ANSI code
166
+ # page. The signature keeps Chinese systems from rendering the
167
+ # middle dot/ellipsis as mojibake while remaining ordinary UTF-8 for
168
+ # modern readers (use ``utf-8-sig`` when the exact first character
169
+ # matters).
170
+ self._log = self.path.open("x", encoding="utf-8-sig", newline="\n")
166
171
  self._write_log("Railmux Windows runtime installation\n")
167
172
  # Windows may defer the visible last-write timestamp of an open file.
168
173
  # Never let retention mistake the active evidence log for an old one.
@@ -324,7 +329,13 @@ class InstallReporter:
324
329
  if package_count is not None:
325
330
  self._package_total = int(package_count.group(1))
326
331
  self._package_step = max(1, self._package_total // 10)
327
- self._progress_detail = f"preparing {self._package_total} packages"
332
+ self._progress_detail = (
333
+ f"waiting for the first transfer among "
334
+ f"{self._package_total} packages"
335
+ )
336
+ self.note(
337
+ f"Package transaction resolved: {self._package_total} packages."
338
+ )
328
339
  return
329
340
  total_download = _TOTAL_DOWNLOAD_RE.match(line)
330
341
  if total_download is not None:
@@ -9,6 +9,7 @@ from __future__ import annotations
9
9
  import concurrent.futures
10
10
  import hashlib
11
11
  import http.client
12
+ import importlib.metadata as importlib_metadata
12
13
  import json
13
14
  import lzma
14
15
  import os
@@ -160,6 +161,22 @@ _PACKAGE_COMMAND = (
160
161
  '--cache-dir "$cache" --only-binary=:all: '
161
162
  '--timeout "$4" --retries "$5" "railmux[ssh]==$2"'
162
163
  )
164
+ _LOCAL_APP_CHECK_COMMAND = (
165
+ 'chmod 755 "$1/venv/bin/railmux" && '
166
+ '"$1/venv/bin/python" -m pip check && '
167
+ '"$1/venv/bin/python" -c '
168
+ "'import packaging, pyte, tomlkit, typing_extensions, urwid, wcwidth, "
169
+ "railmux, sys; assert railmux.__version__ == sys.argv[1]' \"$2\""
170
+ )
171
+ _LOCAL_APP_COPY_FILE_LIMIT = 20_000
172
+ _LOCAL_APP_COPY_SIZE_LIMIT = 128 * 1024 * 1024
173
+ _DEPENDENCY_SEED_NAMES = frozenset(
174
+ {"packaging", "pyte", "tomlkit", "typing_extensions.py", "urwid", "wcwidth"}
175
+ )
176
+ _DEPENDENCY_DIST_INFO_RE = re.compile(
177
+ r"(?:packaging|pyte|tomlkit|typing_extensions|urwid|wcwidth)-.+\.dist-info\Z",
178
+ re.IGNORECASE,
179
+ )
163
180
 
164
181
 
165
182
  class RuntimeErrorBase(RuntimeError):
@@ -2454,6 +2471,289 @@ def _validate_managed_application_root(root: Path) -> None:
2454
2471
  )
2455
2472
 
2456
2473
 
2474
+ def _venv_site_packages(application: Path) -> Path | None:
2475
+ """Return one ordinary MSYS2 venv site-packages directory."""
2476
+ library = application / "venv" / "lib"
2477
+ try:
2478
+ candidates = list(library.glob("python*/site-packages"))
2479
+ except OSError:
2480
+ return None
2481
+ valid: list[Path] = []
2482
+ for candidate in candidates:
2483
+ try:
2484
+ if _path_is_link_or_reparse(candidate) or not candidate.is_dir():
2485
+ continue
2486
+ except OSError:
2487
+ continue
2488
+ valid.append(candidate)
2489
+ return valid[0] if len(valid) == 1 else None
2490
+
2491
+
2492
+ def _native_application_payload(version: str) -> tuple[Path, Path] | None:
2493
+ """Locate the currently executing pure-Python package and dist-info."""
2494
+ try:
2495
+ distribution = importlib_metadata.distribution("railmux")
2496
+ except importlib_metadata.PackageNotFoundError:
2497
+ return None
2498
+ if distribution.version != version:
2499
+ return None
2500
+ package = Path(__file__).resolve().parent
2501
+ if not package.is_dir():
2502
+ return None
2503
+ info_names = {
2504
+ Path(str(item)).parts[0]
2505
+ for item in distribution.files or ()
2506
+ if Path(str(item)).parts
2507
+ and Path(str(item)).parts[0].casefold().startswith("railmux-")
2508
+ and Path(str(item)).parts[0].casefold().endswith(".dist-info")
2509
+ }
2510
+ if len(info_names) != 1:
2511
+ return None
2512
+ info = Path(distribution.locate_file(next(iter(info_names))))
2513
+ if not info.is_dir():
2514
+ return None
2515
+ return package, info
2516
+
2517
+
2518
+ def _copy_local_tree(
2519
+ source: Path,
2520
+ destination: Path,
2521
+ *,
2522
+ budget: list[int],
2523
+ ignored_names: frozenset[str] = frozenset(),
2524
+ ) -> None:
2525
+ """Copy one bounded tree without following links or reparse points."""
2526
+ try:
2527
+ if _path_is_link_or_reparse(source) or not source.is_dir():
2528
+ raise RuntimeInstallError("a local application seed tree was unsafe")
2529
+ except OSError as exc:
2530
+ raise RuntimeInstallError(
2531
+ "a local application seed tree could not be inspected"
2532
+ ) from exc
2533
+ destination.mkdir()
2534
+ for current_text, directories, files in os.walk(source, followlinks=False):
2535
+ current = Path(current_text)
2536
+ relative = current.relative_to(source)
2537
+ target_root = destination / relative
2538
+ kept_directories: list[str] = []
2539
+ for name in directories:
2540
+ if name in ignored_names or name == "__pycache__":
2541
+ continue
2542
+ child = current / name
2543
+ try:
2544
+ if _path_is_link_or_reparse(child) or not child.is_dir():
2545
+ raise RuntimeInstallError(
2546
+ "a local application seed contained an unsafe directory"
2547
+ )
2548
+ except OSError as exc:
2549
+ raise RuntimeInstallError(
2550
+ "a local application seed could not be inspected"
2551
+ ) from exc
2552
+ kept_directories.append(name)
2553
+ (target_root / name).mkdir()
2554
+ directories[:] = kept_directories
2555
+ for name in files:
2556
+ if (
2557
+ name in ignored_names
2558
+ or name.endswith((".pyc", ".pyo"))
2559
+ or name.endswith((".pth", ".egg-link"))
2560
+ ):
2561
+ continue
2562
+ child = current / name
2563
+ try:
2564
+ info = child.lstat()
2565
+ if (
2566
+ _path_is_link_or_reparse(child)
2567
+ or not stat.S_ISREG(info.st_mode)
2568
+ ):
2569
+ raise RuntimeInstallError(
2570
+ "a local application seed contained an unsafe file"
2571
+ )
2572
+ except OSError as exc:
2573
+ raise RuntimeInstallError(
2574
+ "a local application seed could not be inspected"
2575
+ ) from exc
2576
+ budget[0] += 1
2577
+ budget[1] += info.st_size
2578
+ if (
2579
+ budget[0] > _LOCAL_APP_COPY_FILE_LIMIT
2580
+ or budget[1] > _LOCAL_APP_COPY_SIZE_LIMIT
2581
+ ):
2582
+ raise RuntimeInstallError(
2583
+ "the local application seed exceeded its safety bound"
2584
+ )
2585
+ # Do not carry an NTFS read-only attribute from a packaged-Python
2586
+ # cache or an older app layer into the new writable venv.
2587
+ shutil.copyfile(child, target_root / name)
2588
+
2589
+
2590
+ def _copy_local_file(source: Path, destination: Path, *, budget: list[int]) -> None:
2591
+ try:
2592
+ info = source.lstat()
2593
+ if _path_is_link_or_reparse(source) or not stat.S_ISREG(info.st_mode):
2594
+ raise RuntimeInstallError("a local dependency seed file was unsafe")
2595
+ except OSError as exc:
2596
+ raise RuntimeInstallError(
2597
+ "a local dependency seed file could not be inspected"
2598
+ ) from exc
2599
+ budget[0] += 1
2600
+ budget[1] += info.st_size
2601
+ if (
2602
+ budget[0] > _LOCAL_APP_COPY_FILE_LIMIT
2603
+ or budget[1] > _LOCAL_APP_COPY_SIZE_LIMIT
2604
+ ):
2605
+ raise RuntimeInstallError("the local application seed exceeded its safety bound")
2606
+ shutil.copyfile(source, destination)
2607
+
2608
+
2609
+ def _dependency_seed_entries(site_packages: Path) -> list[Path] | None:
2610
+ try:
2611
+ entries = list(site_packages.iterdir())
2612
+ except OSError:
2613
+ return None
2614
+ selected = [
2615
+ entry
2616
+ for entry in entries
2617
+ if entry.name in _DEPENDENCY_SEED_NAMES
2618
+ or _DEPENDENCY_DIST_INFO_RE.fullmatch(entry.name)
2619
+ ]
2620
+ names = {entry.name for entry in selected}
2621
+ if not _DEPENDENCY_SEED_NAMES.issubset(names):
2622
+ return None
2623
+ for dependency in _DEPENDENCY_SEED_NAMES - {"typing_extensions.py"}:
2624
+ if not any(
2625
+ name.casefold().startswith(f"{dependency.casefold()}-")
2626
+ and name.casefold().endswith(".dist-info")
2627
+ for name in names
2628
+ ):
2629
+ return None
2630
+ if not any(
2631
+ name.casefold().startswith("typing_extensions-")
2632
+ and name.casefold().endswith(".dist-info")
2633
+ for name in names
2634
+ ):
2635
+ return None
2636
+ return selected
2637
+
2638
+
2639
+ def _verified_dependency_seed(
2640
+ root: Path,
2641
+ *,
2642
+ version: str,
2643
+ environ: Mapping[str, str],
2644
+ probe: Probe,
2645
+ ) -> tuple[str, Path] | None:
2646
+ marked = _marked_app_versions(root)
2647
+ if not marked:
2648
+ return None
2649
+ for candidate_version in sorted(marked, key=_version_key, reverse=True):
2650
+ if candidate_version == version:
2651
+ continue
2652
+ app_name = _app_name(candidate_version)
2653
+ candidate = Msys2Runtime(root, managed=False, app_name=app_name)
2654
+ if not probe_runtime(
2655
+ candidate,
2656
+ version=candidate_version,
2657
+ environ=environ,
2658
+ probe=probe,
2659
+ ):
2660
+ continue
2661
+ site_packages = _venv_site_packages(marked[candidate_version])
2662
+ if (
2663
+ site_packages is not None
2664
+ and _dependency_seed_entries(site_packages) is not None
2665
+ ):
2666
+ return candidate_version, site_packages
2667
+ return None
2668
+
2669
+
2670
+ def _install_application_from_local_seed(
2671
+ root: Path,
2672
+ *,
2673
+ final_app: Path,
2674
+ final_posix: str,
2675
+ version: str,
2676
+ environ: Mapping[str, str],
2677
+ reporter: InstallReporter,
2678
+ runner: Runner | None,
2679
+ probe: Probe,
2680
+ ) -> str:
2681
+ """Build an upgrade layer locally; return ready, unavailable, or failed."""
2682
+ payload = _native_application_payload(version)
2683
+ seed = _verified_dependency_seed(
2684
+ root, version=version, environ=environ, probe=probe)
2685
+ target_site = _venv_site_packages(final_app)
2686
+ if payload is None or seed is None or target_site is None:
2687
+ return "unavailable"
2688
+ source_package, source_info = payload
2689
+ seed_version, seed_site = seed
2690
+ seed_entries = _dependency_seed_entries(seed_site)
2691
+ if seed_entries is None:
2692
+ return "unavailable"
2693
+ budget = [0, 0]
2694
+ try:
2695
+ for source in seed_entries:
2696
+ destination = target_site / source.name
2697
+ if destination.exists() or destination.is_symlink():
2698
+ raise RuntimeInstallError(
2699
+ "the new application environment unexpectedly contained "
2700
+ f"{source.name}"
2701
+ )
2702
+ if source.is_dir():
2703
+ _copy_local_tree(source, destination, budget=budget)
2704
+ else:
2705
+ _copy_local_file(source, destination, budget=budget)
2706
+ _copy_local_tree(
2707
+ source_package,
2708
+ target_site / "railmux",
2709
+ budget=budget,
2710
+ )
2711
+ _copy_local_tree(
2712
+ source_info,
2713
+ target_site / source_info.name,
2714
+ budget=budget,
2715
+ ignored_names=frozenset(
2716
+ {"RECORD", "direct_url.json", "INSTALLER", "REQUESTED"}
2717
+ ),
2718
+ )
2719
+ entrypoint = final_app / "venv" / "bin" / "railmux"
2720
+ with entrypoint.open("x", encoding="utf-8", newline="\n") as stream:
2721
+ stream.write(
2722
+ f"#!{final_posix}/venv/bin/python\n"
2723
+ "from railmux.entrypoint import main\n"
2724
+ "raise SystemExit(main())\n"
2725
+ )
2726
+ except (OSError, shutil.Error, RuntimeInstallError) as exc:
2727
+ reporter.note(
2728
+ "Verified local app-layer reuse was unavailable "
2729
+ f"({exc}); falling back to the private PyPI cache.",
2730
+ level="warning",
2731
+ )
2732
+ return "failed"
2733
+ returncode = _run_checked(
2734
+ _bash_command(root, _LOCAL_APP_CHECK_COMMAND, final_posix, version),
2735
+ env=Msys2Runtime(root, managed=False).environment(environ),
2736
+ reporter=reporter,
2737
+ runner=runner,
2738
+ label="Local Railmux application validation",
2739
+ allow_failure=True,
2740
+ )
2741
+ if returncode:
2742
+ reporter.note(
2743
+ "The locally reused dependency layer did not satisfy the current "
2744
+ "package contract; falling back to the private PyPI cache.",
2745
+ level="warning",
2746
+ )
2747
+ return "failed"
2748
+ reporter.note(
2749
+ f"Reused verified dependencies from Railmux {seed_version}; installed "
2750
+ f"Railmux {version} from the current Windows package without network "
2751
+ "access.",
2752
+ level="success",
2753
+ )
2754
+ return "ready"
2755
+
2756
+
2457
2757
  def _install_application(
2458
2758
  root: Path,
2459
2759
  *,
@@ -2534,21 +2834,48 @@ def _install_application(
2534
2834
  runner=runner,
2535
2835
  label="Railmux virtual environment creation",
2536
2836
  )
2537
- package_arguments = (
2538
- final_posix,
2539
- version,
2540
- str(cache),
2541
- str(_PIP_INITIAL_TIMEOUT_SECONDS),
2542
- str(_PIP_INITIAL_RETRIES),
2543
- )
2544
- package_returncode = _run_checked(
2545
- _bash_command(root, _PACKAGE_COMMAND, *package_arguments),
2546
- env=child_env,
2837
+ local_install = _install_application_from_local_seed(
2838
+ root,
2839
+ final_app=final_app,
2840
+ final_posix=final_posix,
2841
+ version=version,
2842
+ environ=environ,
2547
2843
  reporter=reporter,
2548
2844
  runner=runner,
2549
- label="Railmux runtime package installation",
2550
- allow_failure=True,
2845
+ probe=probe,
2551
2846
  )
2847
+ if local_install == "failed":
2848
+ try:
2849
+ shutil.rmtree(final_app / "venv")
2850
+ except OSError as exc:
2851
+ raise RuntimeInstallError(
2852
+ "could not reset the unpublished local application seed"
2853
+ ) from exc
2854
+ _run_checked(
2855
+ _bash_command(root, _VENV_COMMAND, final_posix),
2856
+ env=child_env,
2857
+ reporter=reporter,
2858
+ runner=runner,
2859
+ label="Railmux virtual environment recovery",
2860
+ )
2861
+ if local_install == "ready":
2862
+ package_returncode = 0
2863
+ else:
2864
+ package_arguments = (
2865
+ final_posix,
2866
+ version,
2867
+ str(cache),
2868
+ str(_PIP_INITIAL_TIMEOUT_SECONDS),
2869
+ str(_PIP_INITIAL_RETRIES),
2870
+ )
2871
+ package_returncode = _run_checked(
2872
+ _bash_command(root, _PACKAGE_COMMAND, *package_arguments),
2873
+ env=child_env,
2874
+ reporter=reporter,
2875
+ runner=runner,
2876
+ label="Railmux runtime package installation",
2877
+ allow_failure=True,
2878
+ )
2552
2879
  if package_returncode:
2553
2880
  network_failure = (
2554
2881
  reporter.command_had_network_failure
@@ -30,7 +30,9 @@ _MIRRORLIST_LIMIT = 256 * 1024
30
30
  _PACMAN_CONF_LIMIT = 256 * 1024
31
31
  _PROBE_BYTES = 256 * 1024
32
32
  _PROBE_TIMEOUT = 8.0
33
+ _PACKAGE_PROBE_BYTES = 256 * 1024
33
34
  _PACKAGE_PROBE_TIMEOUT = 5.0
35
+ _PACKAGE_PROBE_MIN_RATE = 128 * 1024
34
36
  _SWITCH_RATIO = 1.25
35
37
  _FRESHNESS_WINDOW_SECONDS = 6 * 60 * 60
36
38
  _ZSTD_MAGIC = b"\x28\xb5\x2f\xfd"
@@ -91,18 +93,21 @@ def _probe_package_url(
91
93
  url: str,
92
94
  *,
93
95
  opener: Callable[..., object] = urllib.request.urlopen,
96
+ clock: Callable[[], float] = time.monotonic,
94
97
  ) -> None:
95
- """Require one real package prefix instead of trusting database access."""
98
+ """Require a responsive real package path, not only database access."""
96
99
  if urllib.parse.urlsplit(url).scheme.lower() != "https":
97
100
  raise PacmanMirrorError("package URL is not HTTPS")
98
101
  request = urllib.request.Request(
99
102
  url,
100
103
  headers={
101
104
  "Accept-Encoding": "identity",
102
- "Range": "bytes=0-1023",
105
+ "Range": f"bytes=0-{_PACKAGE_PROBE_BYTES - 1}",
103
106
  "User-Agent": "Railmux-pacman-package-probe/1",
104
107
  },
105
108
  )
109
+ started = clock()
110
+ data = bytearray()
106
111
  try:
107
112
  with opener(request, timeout=_PACKAGE_PROBE_TIMEOUT) as response:
108
113
  final_url = getattr(response, "geturl", lambda: request.full_url)()
@@ -111,15 +116,31 @@ def _probe_package_url(
111
116
  status = getattr(response, "status", 200)
112
117
  if status not in (200, 206):
113
118
  raise PacmanMirrorError(f"package returned HTTP {status}")
114
- data = response.read(1024)
119
+ while len(data) < _PACKAGE_PROBE_BYTES:
120
+ chunk = response.read(min(
121
+ 64 * 1024, _PACKAGE_PROBE_BYTES - len(data)
122
+ ))
123
+ if not chunk:
124
+ break
125
+ data.extend(chunk)
126
+ if clock() - started >= _PACKAGE_PROBE_TIMEOUT:
127
+ break
115
128
  except PacmanMirrorError:
116
129
  raise
117
130
  except urllib.error.HTTPError as exc:
118
131
  raise PacmanMirrorError(f"package returned HTTP {exc.code}") from exc
119
132
  except (OSError, urllib.error.URLError, http.client.HTTPException) as exc:
120
133
  raise PacmanMirrorError("package probe failed") from exc
134
+ elapsed = max(clock() - started, 0.001)
135
+ if len(data) < 1024:
136
+ raise PacmanMirrorError("mirror returned too little package data")
121
137
  if not data.startswith(_ZSTD_MAGIC):
122
138
  raise PacmanMirrorError("mirror did not return an MSYS package")
139
+ if (
140
+ elapsed >= _PACKAGE_PROBE_TIMEOUT
141
+ or len(data) / elapsed < _PACKAGE_PROBE_MIN_RATE
142
+ ):
143
+ raise PacmanMirrorError("package transfer was too slow")
123
144
 
124
145
 
125
146
  def probe_pacman_mirror(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.4.0.dev30
3
+ Version: 0.4.0.dev32
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