railmux 0.3.5.dev1__tar.gz → 0.3.6.dev2__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 (163) hide show
  1. {railmux-0.3.5.dev1/src/railmux.egg-info → railmux-0.3.6.dev2}/PKG-INFO +18 -6
  2. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/README.md +17 -5
  3. railmux-0.3.6.dev2/src/railmux/__init__.py +1 -0
  4. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/fast_display_history.py +104 -34
  5. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/models.py +3 -0
  6. railmux-0.3.6.dev2/src/railmux/session_lease.py +554 -0
  7. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/tmux_binding_manager.py +5 -2
  8. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/tmux_ctl.py +7 -41
  9. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/app.py +702 -87
  10. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/modals.py +81 -0
  11. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/running_pane.py +4 -0
  12. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/sessions_pane.py +10 -3
  13. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2/src/railmux.egg-info}/PKG-INFO +18 -6
  14. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux.egg-info/SOURCES.txt +2 -0
  15. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_agent_workspace.py +1 -0
  16. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_codex_delete.py +4 -0
  17. railmux-0.3.6.dev2/tests/test_codex_lineage.py +561 -0
  18. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_fast_full_window.py +216 -50
  19. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_focus_highlight.py +10 -2
  20. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_modals.py +68 -0
  21. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_orphan_recovery.py +40 -1
  22. railmux-0.3.6.dev2/tests/test_session_lease.py +491 -0
  23. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_sessions_pane.py +15 -0
  24. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_soft_quit.py +101 -1
  25. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_terminal_size.py +12 -0
  26. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_tmux_binding_manager.py +25 -0
  27. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_tmux_ctl.py +0 -24
  28. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_tmux_integration.py +12 -49
  29. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_tmux_status.py +57 -8
  30. railmux-0.3.5.dev1/src/railmux/__init__.py +0 -1
  31. railmux-0.3.5.dev1/tests/test_codex_lineage.py +0 -253
  32. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/LICENSE +0 -0
  33. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/MANIFEST.in +0 -0
  34. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/pyproject.toml +0 -0
  35. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/setup.cfg +0 -0
  36. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/__main__.py +0 -0
  37. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/atomic_file.py +0 -0
  38. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/background_index.py +0 -0
  39. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/cli.py +0 -0
  40. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/codex_index.py +0 -0
  41. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/config.py +0 -0
  42. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/config_cli.py +0 -0
  43. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/diagnostics.py +0 -0
  44. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/discovery.py +0 -0
  45. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/display_transport.py +0 -0
  46. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/fast_display_client.py +0 -0
  47. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/fast_display_input.py +0 -0
  48. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/fast_display_protocol.py +0 -0
  49. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/fast_display_server.py +0 -0
  50. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/favorites.py +0 -0
  51. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/fuzzy.py +0 -0
  52. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/help_workspace.py +0 -0
  53. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/launcher.py +0 -0
  54. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/legacy_sessions.py +0 -0
  55. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/local_clipboard.py +0 -0
  56. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/local_open.py +0 -0
  57. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/modes.py +0 -0
  58. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/mouse_manager.py +0 -0
  59. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/orphan_marker.py +0 -0
  60. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/pane_surface.py +0 -0
  61. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/path_codec.py +0 -0
  62. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/remote_config.py +0 -0
  63. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/renames.py +0 -0
  64. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/restart_state.py +0 -0
  65. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/runtime_config.py +0 -0
  66. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/scroll_agent.py +0 -0
  67. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/scroll_manager.py +0 -0
  68. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/selection_isolation.py +0 -0
  69. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/self_update.py +0 -0
  70. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/session_cache.py +0 -0
  71. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/session_index.py +0 -0
  72. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/setting_contracts.py +0 -0
  73. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/settings.py +0 -0
  74. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/ssh_args.py +0 -0
  75. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/ssh_compat.py +0 -0
  76. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/ssh_display_diagnostics.py +0 -0
  77. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/ssh_doctor.py +0 -0
  78. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/system_deps.py +0 -0
  79. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/terminal_status.py +0 -0
  80. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/tmux_health.py +0 -0
  81. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/tmux_server.py +0 -0
  82. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/tool_panes.py +0 -0
  83. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/transcript.py +0 -0
  84. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/__init__.py +0 -0
  85. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/_widgets.py +0 -0
  86. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/keymap.py +0 -0
  87. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/projects_pane.py +0 -0
  88. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/sidebar.py +0 -0
  89. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/statusbar.py +0 -0
  90. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/workspace.py +0 -0
  91. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux.egg-info/dependency_links.txt +0 -0
  92. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux.egg-info/entry_points.txt +0 -0
  93. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux.egg-info/requires.txt +0 -0
  94. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/src/railmux.egg-info/top_level.txt +0 -0
  95. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_atomic_file.py +0 -0
  96. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_background_index.py +0 -0
  97. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_cli.py +0 -0
  98. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_codex_index.py +0 -0
  99. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_codex_transcript.py +0 -0
  100. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_config.py +0 -0
  101. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_config_cli.py +0 -0
  102. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_denested_transport_benchmark.py +0 -0
  103. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_diagnostics.py +0 -0
  104. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_discovery.py +0 -0
  105. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_display_transport.py +0 -0
  106. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_fullscreen_binding.py +0 -0
  107. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_fuzzy.py +0 -0
  108. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_help_agent.py +0 -0
  109. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_help_workspace.py +0 -0
  110. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_history_cleanup.py +0 -0
  111. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_keymap.py +0 -0
  112. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_launcher.py +0 -0
  113. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_layout_profile.py +0 -0
  114. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_legacy_sessions.py +0 -0
  115. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_local_clipboard.py +0 -0
  116. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_local_open.py +0 -0
  117. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_mode_selection.py +0 -0
  118. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_modes.py +0 -0
  119. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_mouse_manager.py +0 -0
  120. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_new_project.py +0 -0
  121. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_options.py +0 -0
  122. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_pane_surface.py +0 -0
  123. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_path_codec.py +0 -0
  124. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_projects_pane.py +0 -0
  125. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_release_notes.py +0 -0
  126. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_release_workflow.py +0 -0
  127. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_remote_config.py +0 -0
  128. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_renames.py +0 -0
  129. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_restart_state.py +0 -0
  130. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_running_pane.py +0 -0
  131. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_running_sort.py +0 -0
  132. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_runtime_config.py +0 -0
  133. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_scroll_agent.py +0 -0
  134. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_scroll_manager.py +0 -0
  135. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_selection_isolation.py +0 -0
  136. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_self_update.py +0 -0
  137. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_session_cache.py +0 -0
  138. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_session_index.py +0 -0
  139. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_setting_contracts.py +0 -0
  140. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_settings.py +0 -0
  141. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_sidebar.py +0 -0
  142. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_ssh_args.py +0 -0
  143. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_ssh_compat.py +0 -0
  144. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_ssh_display_benchmark.py +0 -0
  145. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_ssh_display_diagnostics.py +0 -0
  146. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_ssh_doctor.py +0 -0
  147. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_status.py +0 -0
  148. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_statusbar.py +0 -0
  149. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_system_deps.py +0 -0
  150. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_terminal_status.py +0 -0
  151. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_termux_local_touch.py +0 -0
  152. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_tmux_health.py +0 -0
  153. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_tmux_server.py +0 -0
  154. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_tool_panes.py +0 -0
  155. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_transcript.py +0 -0
  156. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_utf8_state.py +0 -0
  157. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_web_demo_recorder.py +0 -0
  158. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_wheel_smoke.py +0 -0
  159. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_widgets.py +0 -0
  160. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tests/test_yolo_prompt.py +0 -0
  161. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tools/denested_transport_benchmark.py +0 -0
  162. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tools/release_notes.py +0 -0
  163. {railmux-0.3.5.dev1 → railmux-0.3.6.dev2}/tools/wheel_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.3.5.dev1
3
+ Version: 0.3.6.dev2
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
@@ -383,10 +383,9 @@ Scrolling a live agent does not enter Preview. Direct `railmux` uses the
383
383
  terminal/tmux scrolling behavior; `railmux ssh` keeps its own bounded,
384
384
  per-pane history and normally preserves the live terminal's captured styling.
385
385
  When Railmux confirms that a running Codex session advanced after rewind or
386
- steering, it resets only the current terminal view before Codex redraws and
387
- invalidates that pane's SSH history generation. Retained tmux scrollback is not
388
- erased; the SSH history for that branched generation uses the canonical
389
- rollout so its abandoned suffix stays hidden.
386
+ steering, it leaves the live terminal untouched and advances only that pane's
387
+ SSH history generation. The SSH history for that branched generation uses the
388
+ canonical rollout so its abandoned suffix stays hidden.
390
389
 
391
390
  Preview opens at the latest activity in `less`; large sessions are limited to
392
391
  their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
@@ -427,13 +426,26 @@ Railmux also indexes resumable Claude Code and Codex sessions started outside
427
426
  Railmux, so existing conversations appear in the sidebar and resume the same
428
427
  way. Non-resumable one-off runs such as `codex exec` are filtered out.
429
428
 
429
+ If the same Claude Code or Codex history root is shared across hosts, Railmux
430
+ uses a provider-session lease to prevent two machines from resuming the same
431
+ conversation concurrently. A remotely owned conversation remains in Sessions
432
+ as **running on HOST**, but is not presented as a locally attachable Running
433
+ entry; resume fails safely until that host's exact provider pane exits. The
434
+ lease follows detached agents across Soft Quit and covers every known Codex
435
+ rewind-lineage ID at resume; later rewinds retain a stable locked lineage
436
+ anchor. Delete also checks the shared lease immediately before
437
+ starting and refuses remotely owned history. The shared filesystem must provide
438
+ cross-host POSIX advisory-lock semantics; mounts configured with local-only or
439
+ disabled locking cannot provide this guarantee.
440
+
430
441
  ### Restarts and quitting
431
442
 
432
443
  Each opened agent runs in a detached tmux session, so switching sessions does
433
444
  not interrupt it. To leave agents running when you quit Railmux, press `s` for
434
445
  soft quit in the confirmation popup; restarting the same Railmux instance then
435
446
  restores the usable workspace when those sessions are still available. A normal
436
- quit confirmation ends all running sessions instead.
447
+ quit ends all running sessions instead and therefore requires a second
448
+ confirmation; `y` and Enter keep their existing confirm meaning in both steps.
437
449
 
438
450
  All terminals attached to one managed Railmux window view the same UI process.
439
451
  Soft Quit therefore closes that shared UI for every attached view, although it
@@ -341,10 +341,9 @@ Scrolling a live agent does not enter Preview. Direct `railmux` uses the
341
341
  terminal/tmux scrolling behavior; `railmux ssh` keeps its own bounded,
342
342
  per-pane history and normally preserves the live terminal's captured styling.
343
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.
344
+ steering, it leaves the live terminal untouched and advances only that pane's
345
+ SSH history generation. The SSH history for that branched generation uses the
346
+ canonical rollout so its abandoned suffix stays hidden.
348
347
 
349
348
  Preview opens at the latest activity in `less`; large sessions are limited to
350
349
  their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
@@ -385,13 +384,26 @@ Railmux also indexes resumable Claude Code and Codex sessions started outside
385
384
  Railmux, so existing conversations appear in the sidebar and resume the same
386
385
  way. Non-resumable one-off runs such as `codex exec` are filtered out.
387
386
 
387
+ If the same Claude Code or Codex history root is shared across hosts, Railmux
388
+ uses a provider-session lease to prevent two machines from resuming the same
389
+ conversation concurrently. A remotely owned conversation remains in Sessions
390
+ as **running on HOST**, but is not presented as a locally attachable Running
391
+ entry; resume fails safely until that host's exact provider pane exits. The
392
+ lease follows detached agents across Soft Quit and covers every known Codex
393
+ rewind-lineage ID at resume; later rewinds retain a stable locked lineage
394
+ anchor. Delete also checks the shared lease immediately before
395
+ starting and refuses remotely owned history. The shared filesystem must provide
396
+ cross-host POSIX advisory-lock semantics; mounts configured with local-only or
397
+ disabled locking cannot provide this guarantee.
398
+
388
399
  ### Restarts and quitting
389
400
 
390
401
  Each opened agent runs in a detached tmux session, so switching sessions does
391
402
  not interrupt it. To leave agents running when you quit Railmux, press `s` for
392
403
  soft quit in the confirmation popup; restarting the same Railmux instance then
393
404
  restores the usable workspace when those sessions are still available. A normal
394
- quit confirmation ends all running sessions instead.
405
+ quit ends all running sessions instead and therefore requires a second
406
+ confirmation; `y` and Enter keep their existing confirm meaning in both steps.
395
407
 
396
408
  All terminals attached to one managed Railmux window view the same UI process.
397
409
  Soft Quit therefore closes that shared UI for every attached view, although it
@@ -0,0 +1 @@
1
+ __version__ = "0.3.6.dev2"
@@ -109,6 +109,10 @@ class _PendingHistory:
109
109
  pane_id: str
110
110
  target_lines: int
111
111
  requested_at: float
112
+ # A first wheel-up waits for one cumulative snapshot before freezing the
113
+ # viewport. This avoids exposing a short hot suffix whose later deep page
114
+ # may not share a stable anchor while the provider is actively repainting.
115
+ initial_offset: int | None = None
112
116
 
113
117
 
114
118
  class LocalHistoryView:
@@ -287,15 +291,17 @@ class LocalHistoryView:
287
291
  # intermediate output above the current screen.
288
292
  lines = incoming.lines
289
293
  else:
290
- # A full-screen TUI redraw can leave no trustworthy text
291
- # anchor. Preserve the known history and replace only the
292
- # mutable live viewport. Appending every unaligned 300-line
293
- # prefetch would duplicate a full screen every three seconds.
294
- live_count = min(incoming.height, len(incoming.lines))
295
- retained = previous.lines[
296
- : -min(previous.height, len(previous.lines))
297
- ]
298
- lines = retained + incoming.lines[-live_count:]
294
+ # Never splice two captures whose timelines cannot be aligned.
295
+ # Retaining old history and replacing only the live viewport
296
+ # looked conservative, but it silently omitted every row
297
+ # produced between those two pieces. That false seam appeared
298
+ # as discontinuous SSH history until returning to the bottom
299
+ # forced a fresh deep capture. Keep the newest internally
300
+ # contiguous hot capture instead; the first wheel-up requests
301
+ # a cumulative deep page and can recover older rows safely.
302
+ # An already-frozen viewport owns its immutable snapshot and
303
+ # therefore remains undisturbed by this cache replacement.
304
+ lines = incoming.lines
299
305
  else:
300
306
  start = min(0, delta)
301
307
  incoming_end = delta + len(incoming.lines)
@@ -351,6 +357,15 @@ class LocalHistoryView:
351
357
  del self.content_cache[next(iter(self.content_cache))]
352
358
  return stored
353
359
 
360
+ def _replace_content(self, snapshot: HistorySnapshot) -> HistorySnapshot:
361
+ """Install one verified contiguous capture without timeline merging."""
362
+ assert snapshot.pane_id is not None
363
+ self.content_cache.pop(snapshot.pane_id, None)
364
+ self.content_cache[snapshot.pane_id] = snapshot
365
+ while len(self.content_cache) > _HISTORY_CONTENT_PANES:
366
+ del self.content_cache[next(iter(self.content_cache))]
367
+ return snapshot
368
+
354
369
  def invalidate_routes(self) -> bool:
355
370
  """Drop pointer authority without discarding bounded pane content."""
356
371
  was_active = self.cancel()
@@ -468,29 +483,29 @@ class LocalHistoryView:
468
483
  cached = self.content_cache.get(route.pane_id, route)
469
484
  if not self._same_geometry(cached, route):
470
485
  cached = route
486
+ loaded_limit = min(len(cached.lines), self.history_limit)
487
+ target_lines = min(self.history_limit, _HISTORY_INITIAL_LINES)
488
+ if cached.more_available and loaded_limit < target_lines:
489
+ self.cancel_pane(route.pane_id)
490
+ request_id = self._allocate_request_id()
491
+ self._deep_pending[request_id] = _PendingHistory(
492
+ self.route_epoch,
493
+ route.pane_id,
494
+ target_lines,
495
+ time.monotonic() if now is None else now,
496
+ max(1, initial_offset),
497
+ )
498
+ self._deep_requests += 1
499
+ # Keep the live pane intact until one internally contiguous deep
500
+ # capture arrives. Entering history atomically from that response
501
+ # removes the hot/deep seam and needs no moving-live anchor.
502
+ return HistoryAction(
503
+ protocol_frame=encode_history_request(
504
+ request_id, event.x, event.y, target_lines
505
+ )
506
+ )
471
507
  maximum = max(0, len(cached.lines) - cached.height)
472
508
  if maximum == 0:
473
- if cached.more_available:
474
- self.cancel_pane(route.pane_id)
475
- target_lines = min(self.history_limit, _HISTORY_INITIAL_LINES)
476
- self.viewports[route.pane_id] = _HistoryViewport(
477
- cached,
478
- 0,
479
- min(len(cached.lines), self.history_limit),
480
- )
481
- request_id = self._allocate_request_id()
482
- self._deep_pending[request_id] = _PendingHistory(
483
- self.route_epoch,
484
- route.pane_id,
485
- target_lines,
486
- time.monotonic() if now is None else now,
487
- )
488
- self._deep_requests += 1
489
- return HistoryAction(
490
- protocol_frame=encode_history_request(
491
- request_id, event.x, event.y, target_lines
492
- )
493
- )
494
509
  if cached.transcript_backed:
495
510
  # Claude may not have written its first transcript record yet.
496
511
  # This is a normal transient state, not a Railmux status event:
@@ -501,7 +516,6 @@ class LocalHistoryView:
501
516
  forwarded_input=event.raw if cached.mouse_forwardable else b""
502
517
  )
503
518
  self.cancel_pane(route.pane_id)
504
- loaded_limit = min(len(cached.lines), self.history_limit)
505
519
  viewport = _HistoryViewport(
506
520
  cached,
507
521
  min(maximum, max(1, initial_offset)),
@@ -511,7 +525,6 @@ class LocalHistoryView:
511
525
  ),
512
526
  )
513
527
  self.viewports[route.pane_id] = viewport
514
- target_lines = min(self.history_limit, _HISTORY_INITIAL_LINES)
515
528
  if loaded_limit >= target_lines:
516
529
  return HistoryAction(
517
530
  protocol_frame=self._extend_history(viewport, now=now),
@@ -600,6 +613,45 @@ class LocalHistoryView:
600
613
  if direction > 0 and route.history_choice_required:
601
614
  return HistoryAction(claude_history_prompt=event.raw)
602
615
  viewport = self.viewports.get(route.pane_id)
616
+ initial_request = next(
617
+ (
618
+ (request_id, pending)
619
+ for request_id, pending in self._deep_pending.items()
620
+ if pending.pane_id == route.pane_id
621
+ and pending.initial_offset is not None
622
+ ),
623
+ None,
624
+ )
625
+ if initial_request is not None:
626
+ request_id, pending = initial_request
627
+ requested_at = time.monotonic() if now is None else now
628
+ if requested_at - pending.requested_at >= _HISTORY_DEEP_TIMEOUT:
629
+ self._deep_pending.pop(request_id, None)
630
+ self._timeouts += 1
631
+ if direction < 0:
632
+ return HistoryAction()
633
+ assert pending.initial_offset is not None
634
+ distance = min(
635
+ self.history_limit,
636
+ pending.initial_offset + max(1, distance),
637
+ )
638
+ initial_request = None
639
+ if viewport is None and initial_request is not None:
640
+ if direction < 0:
641
+ # The live pane never left the screen, so reversing direction
642
+ # simply cancels the pending entry. A late response is ignored.
643
+ self.cancel_pane(route.pane_id)
644
+ return HistoryAction()
645
+ request_id, pending = initial_request
646
+ assert pending.initial_offset is not None
647
+ self._deep_pending[request_id] = replace(
648
+ pending,
649
+ initial_offset=min(
650
+ self.history_limit,
651
+ pending.initial_offset + max(1, distance),
652
+ ),
653
+ )
654
+ return HistoryAction()
603
655
  if viewport is not None:
604
656
  maximum = max(0, len(viewport.snapshot.lines) - viewport.snapshot.height)
605
657
  viewport.offset = max(
@@ -826,8 +878,26 @@ class LocalHistoryView:
826
878
  return HistoryAction()
827
879
  viewport = self.viewports.get(pending.pane_id)
828
880
  if viewport is None:
829
- self._remember_content(snapshot)
830
- return HistoryAction()
881
+ if pending.initial_offset is None:
882
+ self._remember_content(snapshot)
883
+ return HistoryAction()
884
+ maximum = max(0, len(snapshot.lines) - snapshot.height)
885
+ if maximum == 0:
886
+ self._remember_content(snapshot)
887
+ return HistoryAction()
888
+ stored = self._replace_content(snapshot)
889
+ maximum = max(0, len(stored.lines) - stored.height)
890
+ viewport = _HistoryViewport(
891
+ stored,
892
+ min(maximum, pending.initial_offset),
893
+ min(self.history_limit, max(pending.target_lines, len(stored.lines))),
894
+ exhausted=not snapshot.more_available,
895
+ )
896
+ self.viewports[pending.pane_id] = viewport
897
+ return HistoryAction(
898
+ protocol_frame=self._extend_history(viewport),
899
+ render_history=True,
900
+ )
831
901
  maximum = max(0, len(snapshot.lines) - snapshot.height)
832
902
  if maximum == 0:
833
903
  self._remember_content(snapshot)
@@ -75,6 +75,9 @@ class SessionMeta:
75
75
  # conversation without changing the provider-owned UUID used for resume.
76
76
  # Always None for Claude sessions and older Codex rollouts.
77
77
  forked_from_id: str | None = None
78
+ # A Railmux lease observed in the same shared provider history root. This
79
+ # is a read-time UI overlay, never provider transcript metadata.
80
+ remote_owner: str | None = None
78
81
 
79
82
  @property
80
83
  def display_title(self) -> str: