railmux 0.4.0.dev31__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.dev31/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.dev31 → railmux-0.4.0.dev32}/src/railmux/ui/app.py +27 -13
  4. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/windows_bootstrap.py +12 -0
  5. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/windows_install_log.py +6 -1
  6. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/windows_msys2.py +339 -12
  7. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32/src/railmux.egg-info}/PKG-INFO +1 -1
  8. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_focus_highlight.py +2 -0
  9. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_sessions_pane.py +33 -0
  10. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_windows_bootstrap.py +30 -1
  11. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_windows_install_log.py +12 -0
  12. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_windows_msys2.py +183 -0
  13. railmux-0.4.0.dev31/src/railmux/__init__.py +0 -1
  14. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/LICENSE +0 -0
  15. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/MANIFEST.in +0 -0
  16. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/README.md +0 -0
  17. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/pyproject.toml +0 -0
  18. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/setup.cfg +0 -0
  19. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/__main__.py +0 -0
  20. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/atomic_file.py +0 -0
  21. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/background_index.py +0 -0
  22. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/cli.py +0 -0
  23. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/codex_index.py +0 -0
  24. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/config.py +0 -0
  25. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/config_cli.py +0 -0
  26. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/diagnostics.py +0 -0
  27. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/discovery.py +0 -0
  28. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/display_transport.py +0 -0
  29. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/entrypoint.py +0 -0
  30. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/fast_display_client.py +0 -0
  31. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/fast_display_history.py +0 -0
  32. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/fast_display_input.py +0 -0
  33. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/fast_display_protocol.py +0 -0
  34. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/fast_display_server.py +0 -0
  35. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/favorites.py +0 -0
  36. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/fuzzy.py +0 -0
  37. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/help_workspace.py +0 -0
  38. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/launcher.py +0 -0
  39. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/legacy_sessions.py +0 -0
  40. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/local_clipboard.py +0 -0
  41. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/local_open.py +0 -0
  42. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/models.py +0 -0
  43. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/modes.py +0 -0
  44. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/mouse_manager.py +0 -0
  45. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/orphan_marker.py +0 -0
  46. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/pane_surface.py +0 -0
  47. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/path_codec.py +0 -0
  48. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/provider_paths.py +0 -0
  49. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/remote_config.py +0 -0
  50. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/renames.py +0 -0
  51. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/restart_state.py +0 -0
  52. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/runtime_config.py +0 -0
  53. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/scroll_agent.py +0 -0
  54. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/scroll_manager.py +0 -0
  55. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/selection_isolation.py +0 -0
  56. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/self_update.py +0 -0
  57. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/session_cache.py +0 -0
  58. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/session_index.py +0 -0
  59. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/session_lease.py +0 -0
  60. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/setting_contracts.py +0 -0
  61. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/settings.py +0 -0
  62. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/ssh_args.py +0 -0
  63. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/ssh_compat.py +0 -0
  64. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/ssh_display_diagnostics.py +0 -0
  65. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/ssh_doctor.py +0 -0
  66. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/system_deps.py +0 -0
  67. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/terminal_status.py +0 -0
  68. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/tmux_binding_manager.py +0 -0
  69. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/tmux_ctl.py +0 -0
  70. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/tmux_health.py +0 -0
  71. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/tmux_server.py +0 -0
  72. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/tool_panes.py +0 -0
  73. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/transcript.py +0 -0
  74. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/ui/__init__.py +0 -0
  75. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/ui/_widgets.py +0 -0
  76. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/ui/keymap.py +0 -0
  77. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/ui/modals.py +0 -0
  78. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/ui/projects_pane.py +0 -0
  79. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/ui/running_pane.py +0 -0
  80. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/ui/sessions_pane.py +0 -0
  81. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/ui/sidebar.py +0 -0
  82. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/ui/statusbar.py +0 -0
  83. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/ui/workspace.py +0 -0
  84. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/windows_attach_relay.py +0 -0
  85. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/windows_pacman.py +0 -0
  86. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/windows_paths.py +0 -0
  87. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/windows_tmux_lifecycle.py +0 -0
  88. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux/windows_ui_transition.py +0 -0
  89. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux.egg-info/SOURCES.txt +0 -0
  90. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux.egg-info/dependency_links.txt +0 -0
  91. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux.egg-info/entry_points.txt +0 -0
  92. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux.egg-info/requires.txt +0 -0
  93. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/src/railmux.egg-info/top_level.txt +0 -0
  94. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_agent_workspace.py +0 -0
  95. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_atomic_file.py +0 -0
  96. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_background_index.py +0 -0
  97. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_cli.py +0 -0
  98. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_codex_delete.py +0 -0
  99. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_codex_index.py +0 -0
  100. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_codex_lineage.py +0 -0
  101. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_codex_transcript.py +0 -0
  102. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_config.py +0 -0
  103. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_config_cli.py +0 -0
  104. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_denested_transport_benchmark.py +0 -0
  105. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_diagnostics.py +0 -0
  106. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_discovery.py +0 -0
  107. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_display_transport.py +0 -0
  108. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_fast_full_window.py +0 -0
  109. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_fullscreen_binding.py +0 -0
  110. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_fuzzy.py +0 -0
  111. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_help_agent.py +0 -0
  112. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_help_workspace.py +0 -0
  113. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_history_cleanup.py +0 -0
  114. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_keymap.py +0 -0
  115. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_launcher.py +0 -0
  116. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_layout_profile.py +0 -0
  117. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_legacy_sessions.py +0 -0
  118. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_local_clipboard.py +0 -0
  119. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_local_open.py +0 -0
  120. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_modals.py +0 -0
  121. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_mode_selection.py +0 -0
  122. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_modes.py +0 -0
  123. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_mouse_manager.py +0 -0
  124. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_new_project.py +0 -0
  125. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_options.py +0 -0
  126. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_orphan_recovery.py +0 -0
  127. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_pane_surface.py +0 -0
  128. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_path_codec.py +0 -0
  129. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_projects_pane.py +0 -0
  130. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_provider_paths.py +0 -0
  131. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_release_notes.py +0 -0
  132. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_release_workflow.py +0 -0
  133. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_remote_config.py +0 -0
  134. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_renames.py +0 -0
  135. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_restart_state.py +0 -0
  136. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_running_pane.py +0 -0
  137. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_running_sort.py +0 -0
  138. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_runtime_config.py +0 -0
  139. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_scroll_agent.py +0 -0
  140. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_scroll_manager.py +0 -0
  141. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_selection_isolation.py +0 -0
  142. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_self_update.py +0 -0
  143. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_session_cache.py +0 -0
  144. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_session_index.py +0 -0
  145. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_session_lease.py +0 -0
  146. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_setting_contracts.py +0 -0
  147. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_settings.py +0 -0
  148. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_sidebar.py +0 -0
  149. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_soft_quit.py +0 -0
  150. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_ssh_args.py +0 -0
  151. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_ssh_compat.py +0 -0
  152. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_ssh_display_benchmark.py +0 -0
  153. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_ssh_display_diagnostics.py +0 -0
  154. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_ssh_doctor.py +0 -0
  155. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_status.py +0 -0
  156. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_statusbar.py +0 -0
  157. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_system_deps.py +0 -0
  158. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_terminal_size.py +0 -0
  159. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_terminal_status.py +0 -0
  160. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_termux_local_touch.py +0 -0
  161. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_tmux_binding_manager.py +0 -0
  162. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_tmux_ctl.py +0 -0
  163. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_tmux_health.py +0 -0
  164. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_tmux_integration.py +0 -0
  165. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_tmux_server.py +0 -0
  166. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_tmux_status.py +0 -0
  167. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_tool_panes.py +0 -0
  168. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_transcript.py +0 -0
  169. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_utf8_state.py +0 -0
  170. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_web_demo_recorder.py +0 -0
  171. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_wheel_smoke.py +0 -0
  172. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_widgets.py +0 -0
  173. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_windows_attach_relay.py +0 -0
  174. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_windows_pacman.py +0 -0
  175. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_windows_paths.py +0 -0
  176. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_windows_tmux_lifecycle.py +0 -0
  177. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_windows_ui_transition.py +0 -0
  178. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_windows_wrapper_contract.py +0 -0
  179. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tests/test_yolo_prompt.py +0 -0
  180. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tools/denested_transport_benchmark.py +0 -0
  181. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tools/release_notes.py +0 -0
  182. {railmux-0.4.0.dev31 → railmux-0.4.0.dev32}/tools/wheel_smoke.py +0 -0
  183. {railmux-0.4.0.dev31 → 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.dev31
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"
@@ -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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.4.0.dev31
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
@@ -1256,6 +1256,7 @@ def test_preview_reports_info_status():
1256
1256
  app._show_transcript = MagicMock(return_value=True)
1257
1257
  app._set_active_target = MagicMock()
1258
1258
  app._set_status = MagicMock()
1259
+ app._sessions_pane = MagicMock()
1259
1260
 
1260
1261
  session = MagicMock()
1261
1262
  session.display_title = "old chat"
@@ -1267,6 +1268,7 @@ def test_preview_reports_info_status():
1267
1268
  assert app._in_history_mode is True
1268
1269
  msg = app._set_status.call_args.args[0]
1269
1270
  assert "Previewing" in msg and "old chat" in msg
1271
+ app._sessions_pane.set_selected_session.assert_not_called()
1270
1272
 
1271
1273
 
1272
1274
  def test_preview_targets_explicit_active_secondary_slot():
@@ -2,6 +2,7 @@
2
2
 
3
3
  import time
4
4
  from pathlib import Path
5
+ from unittest.mock import MagicMock
5
6
 
6
7
  import pytest
7
8
  import urwid
@@ -273,6 +274,38 @@ def test_stale_running_registry_does_not_hide_stopped_preview(monkeypatch):
273
274
  assert previewed == [True]
274
275
 
275
276
 
277
+ def test_session_row_click_paints_before_synchronous_preview(monkeypatch):
278
+ """Windows tmux work must not postpone the pointer acknowledgement."""
279
+ from railmux.ui.app import App
280
+
281
+ project = _project()
282
+ session = _session(project)
283
+ app = App.__new__(App)
284
+ events = []
285
+ app._sessions_pane = MagicMock()
286
+ app._sessions_pane.set_selected_session.side_effect = (
287
+ lambda value: events.append(("selected", value))
288
+ )
289
+ app._loop = MagicMock()
290
+ app._loop.draw_screen.side_effect = lambda: events.append(("draw", None))
291
+ monkeypatch.setattr(app, "_by_session_id", lambda _session_id: None)
292
+ monkeypatch.setattr(
293
+ app,
294
+ "_on_session_preview",
295
+ lambda _session: events.append(("preview", _session.session_id)),
296
+ )
297
+
298
+ app._on_session_row_preview(session)
299
+
300
+ assert events == [
301
+ ("selected", session.session_id),
302
+ ("draw", None),
303
+ ("preview", session.session_id),
304
+ ("selected", None),
305
+ ("draw", None),
306
+ ]
307
+
308
+
276
309
  def test_agent_session_liveness_uses_displayed_swap_pane(monkeypatch):
277
310
  """A swap placeholder session must not mask death of the real pane."""
278
311
  from types import SimpleNamespace
@@ -72,9 +72,10 @@ def test_noninteractive_missing_runtime_is_actionable_and_never_installs(capsys)
72
72
 
73
73
 
74
74
  def test_runtime_status_reports_missing_without_installing(capsys):
75
+ environ = {"LOCALAPPDATA": r"C:\Users\u\AppData\Local"}
75
76
  result = main(
76
77
  ["runtime", "status"],
77
- environ={"LOCALAPPDATA": r"C:\Users\u\AppData\Local"},
78
+ environ=environ,
78
79
  runtime_finder=lambda **_kwargs: None,
79
80
  version_info=(3, 10),
80
81
  )
@@ -83,6 +84,34 @@ def test_runtime_status_reports_missing_without_installing(capsys):
83
84
  output = capsys.readouterr().out
84
85
  assert "Status: not installed" in output
85
86
  assert "Provider data: shared" in output
87
+ data_root = windows_bootstrap.managed_windows_data_root(environ)
88
+ assert f"Data root: {data_root}" in output
89
+ assert f"Install logs: {data_root / 'logs'}" in output
90
+
91
+
92
+ def test_runtime_status_json_exposes_the_actual_packaged_python_log_root(
93
+ capsys, tmp_path, monkeypatch
94
+ ):
95
+ data_root = tmp_path / ".railmux" / "windows"
96
+ monkeypatch.setattr(
97
+ windows_bootstrap,
98
+ "managed_windows_data_root",
99
+ lambda _environ: data_root,
100
+ )
101
+
102
+ assert main(
103
+ ["runtime", "status", "--json"],
104
+ environ={
105
+ "LOCALAPPDATA": str(tmp_path / "AppData" / "Local"),
106
+ "USERPROFILE": str(tmp_path),
107
+ },
108
+ runtime_finder=lambda **_kwargs: None,
109
+ version_info=(3, 10),
110
+ ) == 1
111
+
112
+ snapshot = json.loads(capsys.readouterr().out)
113
+ assert snapshot["data_root"] == str(data_root)
114
+ assert snapshot["log_directory"] == str(data_root / "logs")
86
115
 
87
116
 
88
117
  def test_runtime_status_labels_user_owned_override(capsys, tmp_path):
@@ -64,6 +64,18 @@ def test_verbose_reporter_streams_the_same_sanitized_output(tmp_path):
64
64
  assert "package 中文\nprogress\n" in path.read_text(encoding="utf-8")
65
65
 
66
66
 
67
+ def test_install_log_has_utf8_signature_for_windows_powershell(tmp_path):
68
+ path = tmp_path / "install-0.4.0.dev32-20260806T000000Z-2.log"
69
+
70
+ with InstallReporter(path, verbose=False, stream=io.StringIO()) as reporter:
71
+ reporter.done("共享基础 · 正常")
72
+
73
+ assert path.read_bytes().startswith(b"\xef\xbb\xbf")
74
+ rendered = path.read_text(encoding="utf-8-sig")
75
+ assert rendered.startswith("Railmux Windows runtime installation")
76
+ assert "共享基础 · 正常" in rendered
77
+
78
+
67
79
  def test_reporter_recognizes_only_the_explicit_msys2_core_restart(tmp_path):
68
80
  path = tmp_path / "install.log"
69
81
  with InstallReporter(path, verbose=False, stream=io.StringIO()) as reporter: