railmux 0.4.0.dev14__tar.gz → 0.4.0.dev16__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. {railmux-0.4.0.dev14/src/railmux.egg-info → railmux-0.4.0.dev16}/PKG-INFO +29 -12
  2. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/README.md +28 -11
  3. railmux-0.4.0.dev16/src/railmux/__init__.py +1 -0
  4. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/cli.py +177 -25
  5. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/diagnostics.py +2 -0
  6. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/fast_display_client.py +142 -40
  7. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/fast_display_server.py +17 -3
  8. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/models.py +3 -0
  9. railmux-0.4.0.dev16/src/railmux/session_lease.py +554 -0
  10. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/tmux_ctl.py +34 -59
  11. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/tmux_health.py +2 -0
  12. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/tmux_server.py +15 -1
  13. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/ui/app.py +436 -92
  14. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/ui/running_pane.py +4 -0
  15. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/ui/sessions_pane.py +10 -3
  16. railmux-0.4.0.dev16/src/railmux/windows_attach_relay.py +789 -0
  17. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16/src/railmux.egg-info}/PKG-INFO +29 -12
  18. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux.egg-info/SOURCES.txt +4 -0
  19. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_cli.py +116 -1
  20. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_codex_delete.py +4 -0
  21. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_codex_lineage.py +49 -1
  22. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_fast_full_window.py +141 -3
  23. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_focus_highlight.py +8 -14
  24. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_orphan_recovery.py +40 -1
  25. railmux-0.4.0.dev16/tests/test_session_lease.py +491 -0
  26. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_sessions_pane.py +15 -0
  27. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_soft_quit.py +10 -5
  28. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_tmux_ctl.py +24 -10
  29. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_tmux_integration.py +75 -0
  30. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_tmux_server.py +3 -1
  31. railmux-0.4.0.dev16/tests/test_windows_attach_relay.py +271 -0
  32. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_windows_msys2.py +2 -1
  33. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_windows_wrapper_contract.py +1 -0
  34. railmux-0.4.0.dev14/src/railmux/__init__.py +0 -1
  35. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/LICENSE +0 -0
  36. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/MANIFEST.in +0 -0
  37. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/pyproject.toml +0 -0
  38. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/setup.cfg +0 -0
  39. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/__main__.py +0 -0
  40. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/atomic_file.py +0 -0
  41. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/background_index.py +0 -0
  42. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/codex_index.py +0 -0
  43. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/config.py +0 -0
  44. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/config_cli.py +0 -0
  45. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/discovery.py +0 -0
  46. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/display_transport.py +0 -0
  47. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/entrypoint.py +0 -0
  48. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/fast_display_history.py +0 -0
  49. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/fast_display_input.py +0 -0
  50. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/fast_display_protocol.py +0 -0
  51. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/favorites.py +0 -0
  52. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/fuzzy.py +0 -0
  53. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/help_workspace.py +0 -0
  54. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/launcher.py +0 -0
  55. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/legacy_sessions.py +0 -0
  56. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/local_clipboard.py +0 -0
  57. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/local_open.py +0 -0
  58. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/modes.py +0 -0
  59. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/mouse_manager.py +0 -0
  60. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/orphan_marker.py +0 -0
  61. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/pane_surface.py +0 -0
  62. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/path_codec.py +0 -0
  63. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/provider_paths.py +0 -0
  64. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/remote_config.py +0 -0
  65. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/renames.py +0 -0
  66. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/restart_state.py +0 -0
  67. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/runtime_config.py +0 -0
  68. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/scroll_agent.py +0 -0
  69. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/scroll_manager.py +0 -0
  70. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/selection_isolation.py +0 -0
  71. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/self_update.py +0 -0
  72. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/session_cache.py +0 -0
  73. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/session_index.py +0 -0
  74. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/setting_contracts.py +0 -0
  75. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/settings.py +0 -0
  76. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/ssh_args.py +0 -0
  77. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/ssh_compat.py +0 -0
  78. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/ssh_display_diagnostics.py +0 -0
  79. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/ssh_doctor.py +0 -0
  80. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/system_deps.py +0 -0
  81. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/terminal_status.py +0 -0
  82. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/tmux_binding_manager.py +0 -0
  83. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/tool_panes.py +0 -0
  84. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/transcript.py +0 -0
  85. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/ui/__init__.py +0 -0
  86. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/ui/_widgets.py +0 -0
  87. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/ui/keymap.py +0 -0
  88. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/ui/modals.py +0 -0
  89. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/ui/projects_pane.py +0 -0
  90. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/ui/sidebar.py +0 -0
  91. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/ui/statusbar.py +0 -0
  92. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/ui/workspace.py +0 -0
  93. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/windows_bootstrap.py +0 -0
  94. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/windows_install_log.py +0 -0
  95. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/windows_msys2.py +0 -0
  96. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/windows_pacman.py +0 -0
  97. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux/windows_tmux_lifecycle.py +0 -0
  98. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux.egg-info/dependency_links.txt +0 -0
  99. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux.egg-info/entry_points.txt +0 -0
  100. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux.egg-info/requires.txt +0 -0
  101. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/src/railmux.egg-info/top_level.txt +0 -0
  102. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_agent_workspace.py +0 -0
  103. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_atomic_file.py +0 -0
  104. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_background_index.py +0 -0
  105. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_codex_index.py +0 -0
  106. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_codex_transcript.py +0 -0
  107. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_config.py +0 -0
  108. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_config_cli.py +0 -0
  109. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_denested_transport_benchmark.py +0 -0
  110. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_diagnostics.py +0 -0
  111. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_discovery.py +0 -0
  112. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_display_transport.py +0 -0
  113. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_fullscreen_binding.py +0 -0
  114. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_fuzzy.py +0 -0
  115. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_help_agent.py +0 -0
  116. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_help_workspace.py +0 -0
  117. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_history_cleanup.py +0 -0
  118. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_keymap.py +0 -0
  119. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_launcher.py +0 -0
  120. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_layout_profile.py +0 -0
  121. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_legacy_sessions.py +0 -0
  122. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_local_clipboard.py +0 -0
  123. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_local_open.py +0 -0
  124. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_modals.py +0 -0
  125. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_mode_selection.py +0 -0
  126. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_modes.py +0 -0
  127. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_mouse_manager.py +0 -0
  128. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_new_project.py +0 -0
  129. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_options.py +0 -0
  130. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_pane_surface.py +0 -0
  131. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_path_codec.py +0 -0
  132. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_projects_pane.py +0 -0
  133. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_provider_paths.py +0 -0
  134. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_release_notes.py +0 -0
  135. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_release_workflow.py +0 -0
  136. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_remote_config.py +0 -0
  137. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_renames.py +0 -0
  138. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_restart_state.py +0 -0
  139. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_running_pane.py +0 -0
  140. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_running_sort.py +0 -0
  141. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_runtime_config.py +0 -0
  142. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_scroll_agent.py +0 -0
  143. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_scroll_manager.py +0 -0
  144. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_selection_isolation.py +0 -0
  145. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_self_update.py +0 -0
  146. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_session_cache.py +0 -0
  147. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_session_index.py +0 -0
  148. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_setting_contracts.py +0 -0
  149. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_settings.py +0 -0
  150. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_sidebar.py +0 -0
  151. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_ssh_args.py +0 -0
  152. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_ssh_compat.py +0 -0
  153. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_ssh_display_benchmark.py +0 -0
  154. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_ssh_display_diagnostics.py +0 -0
  155. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_ssh_doctor.py +0 -0
  156. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_status.py +0 -0
  157. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_statusbar.py +0 -0
  158. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_system_deps.py +0 -0
  159. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_terminal_size.py +0 -0
  160. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_terminal_status.py +0 -0
  161. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_termux_local_touch.py +0 -0
  162. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_tmux_binding_manager.py +0 -0
  163. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_tmux_health.py +0 -0
  164. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_tmux_status.py +0 -0
  165. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_tool_panes.py +0 -0
  166. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_transcript.py +0 -0
  167. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_utf8_state.py +0 -0
  168. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_web_demo_recorder.py +0 -0
  169. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_wheel_smoke.py +0 -0
  170. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_widgets.py +0 -0
  171. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_windows_bootstrap.py +0 -0
  172. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_windows_install_log.py +0 -0
  173. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_windows_pacman.py +0 -0
  174. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_windows_tmux_lifecycle.py +0 -0
  175. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tests/test_yolo_prompt.py +0 -0
  176. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tools/denested_transport_benchmark.py +0 -0
  177. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tools/release_notes.py +0 -0
  178. {railmux-0.4.0.dev14 → railmux-0.4.0.dev16}/tools/wheel_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.4.0.dev14
3
+ Version: 0.4.0.dev16
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
@@ -170,7 +170,7 @@ for focus and layout limits.
170
170
  |--------|--------|
171
171
  | Left-click (non-running) | Preview session history in the Target pane |
172
172
  | Left-click (running) | Switch the Target pane to that session |
173
- | Wheel up over a live agent | Open its canonical current-branch history |
173
+ | Wheel over a live agent | Scroll its live terminal history (native locally, Railmux-managed over `railmux ssh`) |
174
174
  | Double-click | Open/attach in the Target pane and move focus there |
175
175
  | Right-click | Context menu (Open, Preview, Info, Rename, Star, Copy title, Kill, Term, Delete) |
176
176
 
@@ -373,16 +373,21 @@ sessions move ahead of the other results.
373
373
 
374
374
  For a stopped session, left-click or press `␣` to view conversation history in
375
375
  the Target pane without starting or resuming the agent. For a running session,
376
- press `␣`, choose **Preview**, or wheel up over its live pane. Preview is
377
- read-only: it cannot send a message or change the session. User and assistant
378
- messages, tool calls, and abbreviated tool output are colour-coded, while
379
- internal context and encrypted reasoning are hidden. Codex rewind lineages and
380
- Claude Code `parentUuid` branches are projected onto the provider's current
381
- conversation: the retained prefix and replacement suffix remain visible, while
382
- the abandoned suffix is hidden. When Railmux observes a running Codex session
383
- advance after rewind or steering, it resets only the current terminal view
384
- before Codex redraws. Retained tmux scrollback is not erased, and managed
385
- history continues from the canonical rollout without its abandoned suffix.
376
+ press `␣` or choose **Preview**. Preview is read-only: it cannot send a message
377
+ or change the session. User and assistant messages, tool calls, and abbreviated
378
+ tool output are colour-coded, while internal context and encrypted reasoning
379
+ are hidden. Codex rewind lineages and Claude Code `parentUuid` branches are
380
+ projected onto the provider's current conversation: the retained prefix and
381
+ replacement suffix remain visible, while the abandoned suffix is hidden.
382
+
383
+ Scrolling a live agent does not enter Preview. Direct `railmux` uses the
384
+ terminal/tmux scrolling behavior; `railmux ssh` keeps its own bounded,
385
+ per-pane history and normally preserves the live terminal's captured styling.
386
+ When Railmux confirms that a running Codex session advanced after rewind or
387
+ steering, it resets only the current terminal view before Codex redraws and
388
+ invalidates that pane's SSH history generation. Retained tmux scrollback is not
389
+ erased; the SSH history for that branched generation uses the canonical
390
+ rollout so its abandoned suffix stays hidden.
386
391
 
387
392
  Preview opens at the latest activity in `less`; large sessions are limited to
388
393
  their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
@@ -423,6 +428,18 @@ Railmux also indexes resumable Claude Code and Codex sessions started outside
423
428
  Railmux, so existing conversations appear in the sidebar and resume the same
424
429
  way. Non-resumable one-off runs such as `codex exec` are filtered out.
425
430
 
431
+ If the same Claude Code or Codex history root is shared across hosts, Railmux
432
+ uses a provider-session lease to prevent two machines from resuming the same
433
+ conversation concurrently. A remotely owned conversation remains in Sessions
434
+ as **running on HOST**, but is not presented as a locally attachable Running
435
+ entry; resume fails safely until that host's exact provider pane exits. The
436
+ lease follows detached agents across Soft Quit and covers every known Codex
437
+ rewind-lineage ID at resume; later rewinds retain a stable locked lineage
438
+ anchor. Delete also checks the shared lease immediately before
439
+ starting and refuses remotely owned history. The shared filesystem must provide
440
+ cross-host POSIX advisory-lock semantics; mounts configured with local-only or
441
+ disabled locking cannot provide this guarantee.
442
+
426
443
  ### Restarts and quitting
427
444
 
428
445
  Each opened agent runs in a detached tmux session, so switching sessions does
@@ -127,7 +127,7 @@ for focus and layout limits.
127
127
  |--------|--------|
128
128
  | Left-click (non-running) | Preview session history in the Target pane |
129
129
  | Left-click (running) | Switch the Target pane to that session |
130
- | Wheel up over a live agent | Open its canonical current-branch history |
130
+ | Wheel over a live agent | Scroll its live terminal history (native locally, Railmux-managed over `railmux ssh`) |
131
131
  | Double-click | Open/attach in the Target pane and move focus there |
132
132
  | Right-click | Context menu (Open, Preview, Info, Rename, Star, Copy title, Kill, Term, Delete) |
133
133
 
@@ -330,16 +330,21 @@ sessions move ahead of the other results.
330
330
 
331
331
  For a stopped session, left-click or press `␣` to view conversation history in
332
332
  the Target pane without starting or resuming the agent. For a running session,
333
- press `␣`, choose **Preview**, or wheel up over its live pane. Preview is
334
- read-only: it cannot send a message or change the session. User and assistant
335
- messages, tool calls, and abbreviated tool output are colour-coded, while
336
- internal context and encrypted reasoning are hidden. Codex rewind lineages and
337
- Claude Code `parentUuid` branches are projected onto the provider's current
338
- conversation: the retained prefix and replacement suffix remain visible, while
339
- the abandoned suffix is hidden. When Railmux observes a running Codex session
340
- advance after rewind or steering, it resets only the current terminal view
341
- before Codex redraws. Retained tmux scrollback is not erased, and managed
342
- history continues from the canonical rollout without its abandoned suffix.
333
+ press `␣` or choose **Preview**. Preview is read-only: it cannot send a message
334
+ or change the session. User and assistant messages, tool calls, and abbreviated
335
+ tool output are colour-coded, while internal context and encrypted reasoning
336
+ are hidden. Codex rewind lineages and Claude Code `parentUuid` branches are
337
+ projected onto the provider's current conversation: the retained prefix and
338
+ replacement suffix remain visible, while the abandoned suffix is hidden.
339
+
340
+ Scrolling a live agent does not enter Preview. Direct `railmux` uses the
341
+ terminal/tmux scrolling behavior; `railmux ssh` keeps its own bounded,
342
+ per-pane history and normally preserves the live terminal's captured styling.
343
+ When Railmux confirms that a running Codex session advanced after rewind or
344
+ steering, it resets only the current terminal view before Codex redraws and
345
+ invalidates that pane's SSH history generation. Retained tmux scrollback is not
346
+ erased; the SSH history for that branched generation uses the canonical
347
+ rollout so its abandoned suffix stays hidden.
343
348
 
344
349
  Preview opens at the latest activity in `less`; large sessions are limited to
345
350
  their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
@@ -380,6 +385,18 @@ Railmux also indexes resumable Claude Code and Codex sessions started outside
380
385
  Railmux, so existing conversations appear in the sidebar and resume the same
381
386
  way. Non-resumable one-off runs such as `codex exec` are filtered out.
382
387
 
388
+ If the same Claude Code or Codex history root is shared across hosts, Railmux
389
+ uses a provider-session lease to prevent two machines from resuming the same
390
+ conversation concurrently. A remotely owned conversation remains in Sessions
391
+ as **running on HOST**, but is not presented as a locally attachable Running
392
+ entry; resume fails safely until that host's exact provider pane exits. The
393
+ lease follows detached agents across Soft Quit and covers every known Codex
394
+ rewind-lineage ID at resume; later rewinds retain a stable locked lineage
395
+ anchor. Delete also checks the shared lease immediately before
396
+ starting and refuses remotely owned history. The shared filesystem must provide
397
+ cross-host POSIX advisory-lock semantics; mounts configured with local-only or
398
+ disabled locking cannot provide this guarantee.
399
+
383
400
  ### Restarts and quitting
384
401
 
385
402
  Each opened agent runs in a detached tmux session, so switching sessions does
@@ -0,0 +1 @@
1
+ __version__ = "0.4.0.dev16"
@@ -1,11 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import argparse
4
+ import os
4
5
  import shutil
5
6
  import subprocess
6
7
  import sys
7
8
  import termios
9
+ import threading
8
10
  import time
11
+ import tty
9
12
  from dataclasses import replace
10
13
  from pathlib import Path
11
14
 
@@ -50,7 +53,20 @@ def _restore_terminal(attributes: list | None) -> None:
50
53
  pass
51
54
 
52
55
 
53
- def _stop_tmux_client(process: subprocess.Popen) -> None:
56
+ def _reset_terminal_modes(fd: int) -> None:
57
+ """Best-effort recovery when a relay ends before tmux's restore tail."""
58
+ try:
59
+ os.write(
60
+ fd,
61
+ b"\x1b[?1049l\x1b[?1l\x1b>\x1b[?1000l\x1b[?1002l"
62
+ b"\x1b[?1003l\x1b[?1004l\x1b[?1006l\x1b[?2004l"
63
+ b"\x1b[?7h\x1b[?25h\x1b[0m\r",
64
+ )
65
+ except OSError:
66
+ pass
67
+
68
+
69
+ def _stop_tmux_client(process: object) -> None:
54
70
  if process.poll() is not None:
55
71
  return
56
72
  process.terminate()
@@ -75,20 +91,43 @@ def _run_tmux_client_with_watchdog(
75
91
  attributes = termios.tcgetattr(sys.stdin.fileno())
76
92
  except (OSError, termios.error):
77
93
  pass
94
+ started_at = time.monotonic()
78
95
  try:
79
- process = subprocess.Popen(argv, env=env)
96
+ process: object = subprocess.Popen(argv, env=env)
80
97
  except OSError as exc:
81
98
  print(f"error: could not start tmux client: {exc}", file=sys.stderr)
82
99
  return 2
83
- watchdog = tmux_health.FailureWatchdog.starting(
84
- time.monotonic(),
85
- interval=_LOCAL_WATCHDOG_INTERVAL,
86
- failure_limit=_LOCAL_WATCHDOG_FAILURES,
87
- )
88
- next_session_probe = watchdog.next_probe - watchdog.interval
89
- try:
100
+ def monitor_client(
101
+ monitor_started_at: float | None = None,
102
+ *,
103
+ asynchronous_probe: bool = False,
104
+ ) -> tuple[int, bool]:
105
+ nonlocal expected_target, expected_session_id
106
+ watchdog = tmux_health.FailureWatchdog.starting(
107
+ (
108
+ time.monotonic()
109
+ if monitor_started_at is None
110
+ else monitor_started_at
111
+ ),
112
+ interval=_LOCAL_WATCHDOG_INTERVAL,
113
+ failure_limit=_LOCAL_WATCHDOG_FAILURES,
114
+ )
115
+ next_session_probe = watchdog.next_probe - watchdog.interval
116
+ probe_thread: threading.Thread | None = None
117
+ probe_result: list[tmux_server.TmuxServerTarget | None] = []
118
+
119
+ def discover() -> tmux_server.TmuxServerTarget | None:
120
+ try:
121
+ return tmux_server.discover_target(timeout=1.0)
122
+ except tmux_server.TmuxServerError:
123
+ return None
124
+
90
125
  while process.poll() is None:
91
- time.sleep(0.25)
126
+ pump = getattr(process, "pump", None)
127
+ if pump is None:
128
+ time.sleep(0.25)
129
+ else:
130
+ pump(0.25)
92
131
  now = time.monotonic()
93
132
  if (
94
133
  expected_target is not None
@@ -100,10 +139,26 @@ def _run_tmux_client_with_watchdog(
100
139
  next_session_probe = now + 1.0
101
140
  if not watchdog.due(now):
102
141
  continue
103
- try:
104
- current_target = tmux_server.discover_target(timeout=1.0)
105
- except tmux_server.TmuxServerError:
106
- current_target = None
142
+ if asynchronous_probe:
143
+ if probe_thread is None:
144
+ probe_result.clear()
145
+
146
+ def run_probe() -> None:
147
+ probe_result.append(discover())
148
+
149
+ probe_thread = threading.Thread(
150
+ target=run_probe,
151
+ name="railmux-tmux-health",
152
+ daemon=True,
153
+ )
154
+ probe_thread.start()
155
+ continue
156
+ if probe_thread.is_alive():
157
+ continue
158
+ current_target = probe_result[0] if probe_result else None
159
+ probe_thread = None
160
+ else:
161
+ current_target = discover()
107
162
  if expected_target is None and current_target is not None:
108
163
  expected_target = current_target
109
164
  expected_session_id = tmux_server.target_session_id(
@@ -126,23 +181,94 @@ def _run_tmux_client_with_watchdog(
126
181
  "responding; run 'railmux doctor' for diagnostics",
127
182
  file=sys.stderr,
128
183
  )
129
- return 2
184
+ return 2, True
185
+ return process.returncode or 0, False
186
+
187
+ relay = None
188
+ relay_attempted = False
189
+ try:
190
+ returncode, watchdog_failed = monitor_client(started_at)
191
+ if watchdog_failed:
192
+ return returncode
193
+ failed_at = time.monotonic()
130
194
  # tmux may leave the client TTY in raw mode even after its process
131
195
  # exits. Restore it before a bounded recovery probe or user message.
132
196
  _restore_terminal(attributes)
133
- returncode = process.returncode or 0
134
- if windows_tmux_lifecycle.recover_abandoned_socket():
135
- print(
136
- "info: cleaned an abandoned Windows tmux socket; Codex and "
137
- "Claude session files were not changed",
138
- file=sys.stderr,
139
- )
197
+ current_target = None
140
198
  if returncode and expected_target is not None:
141
199
  try:
142
200
  current_target = tmux_server.discover_target(timeout=1.0)
143
201
  except tmux_server.TmuxServerError:
144
202
  current_target = None
145
- if current_target != expected_target:
203
+ if current_target == expected_target and expected_session_id is None:
204
+ expected_session_id = tmux_server.target_session_id(
205
+ expected_target, "railmux", timeout=0.5)
206
+
207
+ from railmux.provider_paths import running_in_managed_windows_wrapper
208
+
209
+ managed_windows = running_in_managed_windows_wrapper()
210
+
211
+ can_relay = bool(
212
+ current_target == expected_target
213
+ and expected_session_id is not None
214
+ and failed_at - started_at < 5.0
215
+ and attributes is not None
216
+ and sys.stdout.isatty()
217
+ and managed_windows
218
+ )
219
+ if can_relay:
220
+ relay_attempted = True
221
+ from railmux.windows_attach_relay import (
222
+ WindowsAttachRelayError,
223
+ start_relay_client,
224
+ )
225
+
226
+ print(
227
+ "info: direct tmux attach was unavailable; reconnecting "
228
+ "through the Windows terminal bridge",
229
+ file=sys.stderr,
230
+ )
231
+ try:
232
+ tty.setraw(sys.stdin.fileno())
233
+ relay = start_relay_client(
234
+ target=expected_target,
235
+ session_id=expected_session_id,
236
+ environ=env,
237
+ stdin_fd=sys.stdin.fileno(),
238
+ stdout_fd=sys.stdout.fileno(),
239
+ )
240
+ process = relay
241
+ returncode, watchdog_failed = monitor_client(
242
+ asynchronous_probe=True)
243
+ if returncode:
244
+ _reset_terminal_modes(sys.stdout.fileno())
245
+ if watchdog_failed:
246
+ return returncode
247
+ except (OSError, termios.error, WindowsAttachRelayError):
248
+ _reset_terminal_modes(sys.stdout.fileno())
249
+ _restore_terminal(attributes)
250
+ outcome = (
251
+ "could not attach"
252
+ if relay is None
253
+ else "connection ended unexpectedly"
254
+ )
255
+ print(
256
+ f"error: the Windows terminal bridge {outcome}; the "
257
+ "existing workspace was left running; run "
258
+ "'railmux doctor' for diagnostics",
259
+ file=sys.stderr,
260
+ )
261
+ returncode = 2
262
+ finally:
263
+ _restore_terminal(attributes)
264
+
265
+ if returncode:
266
+ try:
267
+ current_target = tmux_server.discover_target(timeout=1.0)
268
+ except tmux_server.TmuxServerError:
269
+ current_target = None
270
+
271
+ if returncode and current_target != expected_target:
146
272
  clean_exit = bool(
147
273
  expected_session_id is not None
148
274
  and tmux_health.consume_clean_exit(
@@ -156,16 +282,41 @@ def _run_tmux_client_with_watchdog(
156
282
  reason="launcher-server-exit",
157
283
  consecutive_failures=1,
158
284
  )
285
+ elif (
286
+ returncode
287
+ and current_target == expected_target
288
+ and managed_windows
289
+ ):
290
+ tmux_health.record_incident(
291
+ component="launcher",
292
+ reason=(
293
+ "launcher-relay-failed"
294
+ if relay_attempted
295
+ else "launcher-attach-rejected"
296
+ ),
297
+ consecutive_failures=1,
298
+ )
299
+ # Routine post-exit removal of a proof-authorized last-session socket
300
+ # is housekeeping. Only pre-launch recovery is user-visible.
301
+ windows_tmux_lifecycle.recover_abandoned_socket()
159
302
  return returncode
160
303
  except KeyboardInterrupt:
161
304
  _stop_tmux_client(process)
305
+ if relay is not None:
306
+ _reset_terminal_modes(sys.stdout.fileno())
162
307
  return 130
163
308
  finally:
309
+ if relay is not None:
310
+ relay.close()
164
311
  _restore_terminal(attributes)
165
312
 
166
313
 
167
314
  def main(argv: list[str] | None = None) -> int:
168
315
  raw_args = list(sys.argv[1:] if argv is None else argv)
316
+ if raw_args and raw_args[0] == "_windows-attach-relay":
317
+ from railmux.windows_attach_relay import relay_server_main
318
+
319
+ return relay_server_main(raw_args[1:])
169
320
  if raw_args and raw_args[0] == "config":
170
321
  from railmux.config_cli import main as config_main
171
322
 
@@ -379,8 +530,9 @@ def main(argv: list[str] | None = None) -> int:
379
530
  tmux_server.socket_label()
380
531
  if windows_tmux_lifecycle.recover_abandoned_socket():
381
532
  print(
382
- "info: cleaned an abandoned Windows tmux socket; Codex and "
383
- "Claude session files were not changed",
533
+ "info: recovered an unresponsive Windows tmux socket left by "
534
+ "a previous session; Codex and Claude session files were not "
535
+ "changed",
384
536
  file=sys.stderr,
385
537
  )
386
538
  dedicated_target = tmux_server.discover_target()
@@ -426,6 +426,8 @@ def _incident_text(diagnostic: IncidentDiagnostic) -> str:
426
426
  if diagnostic.status == "none":
427
427
  return "none recorded"
428
428
  descriptions = {
429
+ "launcher-attach-rejected": "Windows tmux client attach rejected",
430
+ "launcher-relay-failed": "Windows terminal bridge failed",
429
431
  "launcher-watchdog-timeout": "local client watchdog timeout",
430
432
  "launcher-server-exit": "dedicated tmux server exited",
431
433
  "remote-display-watchdog-timeout": "SSH display watchdog timeout",