railmux 0.3.3.dev2__tar.gz → 0.3.3.dev4__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 (161) hide show
  1. {railmux-0.3.3.dev2/src/railmux.egg-info → railmux-0.3.3.dev4}/PKG-INFO +21 -11
  2. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/README.md +20 -10
  3. railmux-0.3.3.dev4/src/railmux/__init__.py +1 -0
  4. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/fast_display_client.py +2 -2
  5. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/fast_display_history.py +12 -3
  6. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/fast_display_protocol.py +20 -7
  7. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/fast_display_server.py +26 -3
  8. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/mouse_manager.py +8 -0
  9. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/tmux_ctl.py +131 -7
  10. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/transcript.py +83 -28
  11. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/ui/app.py +230 -15
  12. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/ui/modals.py +1 -1
  13. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4/src/railmux.egg-info}/PKG-INFO +21 -11
  14. railmux-0.3.3.dev4/tests/test_codex_lineage.py +157 -0
  15. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_fast_full_window.py +125 -12
  16. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_focus_highlight.py +135 -8
  17. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_mouse_manager.py +17 -0
  18. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_release_workflow.py +25 -8
  19. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_soft_quit.py +42 -0
  20. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_tmux_ctl.py +65 -0
  21. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_tmux_integration.py +42 -0
  22. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_transcript.py +84 -0
  23. railmux-0.3.3.dev2/src/railmux/__init__.py +0 -1
  24. railmux-0.3.3.dev2/tests/test_codex_lineage.py +0 -64
  25. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/LICENSE +0 -0
  26. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/MANIFEST.in +0 -0
  27. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/pyproject.toml +0 -0
  28. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/setup.cfg +0 -0
  29. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/__main__.py +0 -0
  30. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/atomic_file.py +0 -0
  31. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/background_index.py +0 -0
  32. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/cli.py +0 -0
  33. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/codex_index.py +0 -0
  34. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/config.py +0 -0
  35. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/config_cli.py +0 -0
  36. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/diagnostics.py +0 -0
  37. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/discovery.py +0 -0
  38. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/display_transport.py +0 -0
  39. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/fast_display_input.py +0 -0
  40. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/favorites.py +0 -0
  41. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/fuzzy.py +0 -0
  42. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/help_workspace.py +0 -0
  43. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/launcher.py +0 -0
  44. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/legacy_sessions.py +0 -0
  45. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/local_clipboard.py +0 -0
  46. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/local_open.py +0 -0
  47. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/models.py +0 -0
  48. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/modes.py +0 -0
  49. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/orphan_marker.py +0 -0
  50. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/pane_surface.py +0 -0
  51. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/path_codec.py +0 -0
  52. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/remote_config.py +0 -0
  53. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/renames.py +0 -0
  54. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/restart_state.py +0 -0
  55. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/runtime_config.py +0 -0
  56. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/scroll_agent.py +0 -0
  57. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/scroll_manager.py +0 -0
  58. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/selection_isolation.py +0 -0
  59. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/self_update.py +0 -0
  60. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/session_cache.py +0 -0
  61. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/session_index.py +0 -0
  62. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/setting_contracts.py +0 -0
  63. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/settings.py +0 -0
  64. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/ssh_args.py +0 -0
  65. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/ssh_compat.py +0 -0
  66. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/ssh_display_diagnostics.py +0 -0
  67. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/ssh_doctor.py +0 -0
  68. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/system_deps.py +0 -0
  69. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/terminal_status.py +0 -0
  70. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/tmux_binding_manager.py +0 -0
  71. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/tmux_health.py +0 -0
  72. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/tmux_server.py +0 -0
  73. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/tool_panes.py +0 -0
  74. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/ui/__init__.py +0 -0
  75. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/ui/_widgets.py +0 -0
  76. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/ui/keymap.py +0 -0
  77. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/ui/projects_pane.py +0 -0
  78. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/ui/running_pane.py +0 -0
  79. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/ui/sessions_pane.py +0 -0
  80. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/ui/sidebar.py +0 -0
  81. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/ui/statusbar.py +0 -0
  82. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux/ui/workspace.py +0 -0
  83. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux.egg-info/SOURCES.txt +0 -0
  84. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux.egg-info/dependency_links.txt +0 -0
  85. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux.egg-info/entry_points.txt +0 -0
  86. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux.egg-info/requires.txt +0 -0
  87. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/src/railmux.egg-info/top_level.txt +0 -0
  88. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_agent_workspace.py +0 -0
  89. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_atomic_file.py +0 -0
  90. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_background_index.py +0 -0
  91. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_cli.py +0 -0
  92. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_codex_delete.py +0 -0
  93. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_codex_index.py +0 -0
  94. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_codex_transcript.py +0 -0
  95. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_config.py +0 -0
  96. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_config_cli.py +0 -0
  97. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_denested_transport_benchmark.py +0 -0
  98. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_diagnostics.py +0 -0
  99. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_discovery.py +0 -0
  100. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_display_transport.py +0 -0
  101. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_fullscreen_binding.py +0 -0
  102. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_fuzzy.py +0 -0
  103. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_help_agent.py +0 -0
  104. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_help_workspace.py +0 -0
  105. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_history_cleanup.py +0 -0
  106. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_keymap.py +0 -0
  107. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_launcher.py +0 -0
  108. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_layout_profile.py +0 -0
  109. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_legacy_sessions.py +0 -0
  110. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_local_clipboard.py +0 -0
  111. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_local_open.py +0 -0
  112. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_modals.py +0 -0
  113. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_mode_selection.py +0 -0
  114. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_modes.py +0 -0
  115. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_new_project.py +0 -0
  116. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_options.py +0 -0
  117. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_orphan_recovery.py +0 -0
  118. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_pane_surface.py +0 -0
  119. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_path_codec.py +0 -0
  120. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_projects_pane.py +0 -0
  121. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_release_notes.py +0 -0
  122. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_remote_config.py +0 -0
  123. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_renames.py +0 -0
  124. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_restart_state.py +0 -0
  125. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_running_pane.py +0 -0
  126. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_running_sort.py +0 -0
  127. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_runtime_config.py +0 -0
  128. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_scroll_agent.py +0 -0
  129. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_scroll_manager.py +0 -0
  130. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_selection_isolation.py +0 -0
  131. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_self_update.py +0 -0
  132. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_session_cache.py +0 -0
  133. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_session_index.py +0 -0
  134. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_sessions_pane.py +0 -0
  135. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_setting_contracts.py +0 -0
  136. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_settings.py +0 -0
  137. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_sidebar.py +0 -0
  138. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_ssh_args.py +0 -0
  139. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_ssh_compat.py +0 -0
  140. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_ssh_display_benchmark.py +0 -0
  141. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_ssh_display_diagnostics.py +0 -0
  142. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_ssh_doctor.py +0 -0
  143. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_status.py +0 -0
  144. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_statusbar.py +0 -0
  145. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_system_deps.py +0 -0
  146. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_terminal_size.py +0 -0
  147. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_terminal_status.py +0 -0
  148. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_termux_local_touch.py +0 -0
  149. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_tmux_binding_manager.py +0 -0
  150. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_tmux_health.py +0 -0
  151. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_tmux_server.py +0 -0
  152. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_tmux_status.py +0 -0
  153. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_tool_panes.py +0 -0
  154. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_utf8_state.py +0 -0
  155. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_web_demo_recorder.py +0 -0
  156. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_wheel_smoke.py +0 -0
  157. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_widgets.py +0 -0
  158. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tests/test_yolo_prompt.py +0 -0
  159. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tools/denested_transport_benchmark.py +0 -0
  160. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tools/release_notes.py +0 -0
  161. {railmux-0.3.3.dev2 → railmux-0.3.3.dev4}/tools/wheel_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.3.3.dev2
3
+ Version: 0.3.3.dev4
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
@@ -169,12 +169,14 @@ for focus and layout limits.
169
169
  |--------|--------|
170
170
  | Left-click (non-running) | Preview session history in the Target pane |
171
171
  | Left-click (running) | Switch the Target pane to that session |
172
+ | Wheel up over a live agent | Open its canonical current-branch history |
172
173
  | Double-click | Open/attach in the Target pane and move focus there |
173
174
  | Right-click | Context menu (Open, Preview, Info, Rename, Star, Copy title, Kill, Term, Delete) |
174
175
 
175
176
  Unavailable context-menu actions are hidden for the selected session state.
176
- Single-click and `␣` act in the Target pane without moving keyboard focus;
177
- double-click and `Enter` also move focus there. The terminal must report mouse
177
+ Single-click and `␣` act in the Target pane without moving keyboard focus, but
178
+ `␣` always means history while a click on a running row means live attach.
179
+ Double-click and `Enter` also move focus there. The terminal must report mouse
178
180
  buttons to applications for these actions to reach Railmux. Right-click
179
181
  reporting is sometimes a separate setting from ordinary mouse reporting; see
180
182
  [FAQ 2](#2-mouse-buttons-or-f8f9-dont-work--whats-wrong).
@@ -207,7 +209,7 @@ see [FAQ 1](#1-how-do-i-copy-text-from-the-agent-pane).
207
209
  | `T` | Return to that agent's managed Vim viewer, when open |
208
210
  | `m` | Switch agent mode (Claude Code ⇄ Codex) |
209
211
  | `o` | Open persistent Railmux options |
210
- | `␣` | Preview stopped or switch running target (like single-click) |
212
+ | `␣` | Preview canonical history for the selected stopped or running session |
211
213
  | `F8` | Cycle agent layout: single → side-by-side → stacked |
212
214
  | `F9` | Fullscreen the focused agent, managed terminal, or Vim (toggle) |
213
215
  | `?` | Full help popup with all keybindings |
@@ -299,7 +301,8 @@ to single leaves Pane 2's agent running in the background.
299
301
 
300
302
  In a split, focus an agent pane to make it the Target pane. After focus returns
301
303
  to the sidebar, single-click or `␣` acts in that pane without moving keyboard
302
- focus; double-click or `Enter` opens there and transfers focus. The status bar
304
+ focus; a running-row click attaches live while `␣` previews history.
305
+ Double-click or `Enter` opens there and transfers focus. The status bar
303
306
  at the bottom-left shows the current layout and Target pane:
304
307
 
305
308
  | Symbol | Meaning |
@@ -363,15 +366,22 @@ sessions move ahead of the other results.
363
366
  ## History preview
364
367
 
365
368
  For a stopped session, left-click or press `␣` to view conversation history in
366
- the Target pane without starting or resuming the agent. Preview is read-only: it
367
- cannot send a message or change the session. User and assistant messages, tool
368
- calls, and abbreviated tool output are colour-coded, while internal context and
369
- encrypted reasoning are hidden.
369
+ the Target pane without starting or resuming the agent. For a running session,
370
+ press `␣`, choose **Preview**, or wheel up over its live pane. Preview is
371
+ read-only: it cannot send a message or change the session. User and assistant
372
+ messages, tool calls, and abbreviated tool output are colour-coded, while
373
+ internal context and encrypted reasoning are hidden. Codex rewind lineages and
374
+ Claude Code `parentUuid` branches are projected onto the provider's current
375
+ conversation: the retained prefix and replacement suffix remain visible, while
376
+ the abandoned suffix is hidden. When Railmux observes a running Codex session
377
+ advance to its rewind child, it also removes the abandoned terminal screen and
378
+ tmux scrollback before Codex redraws the live pane.
370
379
 
371
380
  Preview opens at the latest activity in `less`; large sessions are limited to
372
381
  their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
373
- matches, and `q` to exit and restore the pane. Double-click to skip preview and
374
- open the session directly.
382
+ matches, and `q` to exit. If preview began from a live pane, `q` restores that
383
+ exact session; otherwise it restores the prior Target pane. Double-click to
384
+ skip preview and open the session directly.
375
385
 
376
386
  ## Status indicators
377
387
 
@@ -127,12 +127,14 @@ 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
131
  | Double-click | Open/attach in the Target pane and move focus there |
131
132
  | Right-click | Context menu (Open, Preview, Info, Rename, Star, Copy title, Kill, Term, Delete) |
132
133
 
133
134
  Unavailable context-menu actions are hidden for the selected session state.
134
- Single-click and `␣` act in the Target pane without moving keyboard focus;
135
- double-click and `Enter` also move focus there. The terminal must report mouse
135
+ Single-click and `␣` act in the Target pane without moving keyboard focus, but
136
+ `␣` always means history while a click on a running row means live attach.
137
+ Double-click and `Enter` also move focus there. The terminal must report mouse
136
138
  buttons to applications for these actions to reach Railmux. Right-click
137
139
  reporting is sometimes a separate setting from ordinary mouse reporting; see
138
140
  [FAQ 2](#2-mouse-buttons-or-f8f9-dont-work--whats-wrong).
@@ -165,7 +167,7 @@ see [FAQ 1](#1-how-do-i-copy-text-from-the-agent-pane).
165
167
  | `T` | Return to that agent's managed Vim viewer, when open |
166
168
  | `m` | Switch agent mode (Claude Code ⇄ Codex) |
167
169
  | `o` | Open persistent Railmux options |
168
- | `␣` | Preview stopped or switch running target (like single-click) |
170
+ | `␣` | Preview canonical history for the selected stopped or running session |
169
171
  | `F8` | Cycle agent layout: single → side-by-side → stacked |
170
172
  | `F9` | Fullscreen the focused agent, managed terminal, or Vim (toggle) |
171
173
  | `?` | Full help popup with all keybindings |
@@ -257,7 +259,8 @@ to single leaves Pane 2's agent running in the background.
257
259
 
258
260
  In a split, focus an agent pane to make it the Target pane. After focus returns
259
261
  to the sidebar, single-click or `␣` acts in that pane without moving keyboard
260
- focus; double-click or `Enter` opens there and transfers focus. The status bar
262
+ focus; a running-row click attaches live while `␣` previews history.
263
+ Double-click or `Enter` opens there and transfers focus. The status bar
261
264
  at the bottom-left shows the current layout and Target pane:
262
265
 
263
266
  | Symbol | Meaning |
@@ -321,15 +324,22 @@ sessions move ahead of the other results.
321
324
  ## History preview
322
325
 
323
326
  For a stopped session, left-click or press `␣` to view conversation history in
324
- the Target pane without starting or resuming the agent. Preview is read-only: it
325
- cannot send a message or change the session. User and assistant messages, tool
326
- calls, and abbreviated tool output are colour-coded, while internal context and
327
- encrypted reasoning are hidden.
327
+ the Target pane without starting or resuming the agent. For a running session,
328
+ press `␣`, choose **Preview**, or wheel up over its live pane. Preview is
329
+ read-only: it cannot send a message or change the session. User and assistant
330
+ messages, tool calls, and abbreviated tool output are colour-coded, while
331
+ internal context and encrypted reasoning are hidden. Codex rewind lineages and
332
+ Claude Code `parentUuid` branches are projected onto the provider's current
333
+ conversation: the retained prefix and replacement suffix remain visible, while
334
+ the abandoned suffix is hidden. When Railmux observes a running Codex session
335
+ advance to its rewind child, it also removes the abandoned terminal screen and
336
+ tmux scrollback before Codex redraws the live pane.
328
337
 
329
338
  Preview opens at the latest activity in `less`; large sessions are limited to
330
339
  their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
331
- matches, and `q` to exit and restore the pane. Double-click to skip preview and
332
- open the session directly.
340
+ matches, and `q` to exit. If preview began from a live pane, `q` restores that
341
+ exact session; otherwise it restores the prior Target pane. Double-click to
342
+ skip preview and open the session directly.
333
343
 
334
344
  ## Status indicators
335
345
 
@@ -0,0 +1 @@
1
+ __version__ = "0.3.3.dev4"
@@ -1074,7 +1074,7 @@ class TerminalSurface:
1074
1074
  self,
1075
1075
  requested: TerminalMode,
1076
1076
  ) -> TerminalMode:
1077
- """Mirror only input-affecting modes explicitly carried by protocol v14."""
1077
+ """Mirror only input-affecting modes explicitly carried by protocol v15."""
1078
1078
  disabled = self.terminal_modes & ~requested
1079
1079
  enabled = requested & ~self.terminal_modes
1080
1080
  controls: list[bytes] = []
@@ -2013,7 +2013,7 @@ def _finish_remote_attach(
2013
2013
  raise ProbeError("remote display helper failed before attaching")
2014
2014
 
2015
2015
  _stop_unstarted_remote(process)
2016
- # A current v14 helper holds the mutex only while registering its exact tmux
2016
+ # A current v15 helper holds the mutex only while registering its exact tmux
2017
2017
  # child. Give that ordinary race one fresh SSH process before presenting
2018
2018
  # the explicit legacy-lock takeover choice.
2019
2019
  time.sleep(_REMOTE_ATTACH_RETRY_DELAY)
@@ -207,7 +207,11 @@ class LocalHistoryView:
207
207
  restore_live = False
208
208
  for pane_id, viewport in tuple(self.viewports.items()):
209
209
  route = routes.get(pane_id)
210
- if route is None or not self._same_geometry(viewport.snapshot, route):
210
+ if (
211
+ route is None
212
+ or not self._same_geometry(viewport.snapshot, route)
213
+ or not self._history_source_matches(viewport.snapshot, route)
214
+ ):
211
215
  self.cancel_pane(pane_id)
212
216
  restore_live = True
213
217
  return HistoryAction(restore_live=restore_live)
@@ -217,10 +221,11 @@ class LocalHistoryView:
217
221
  left: HistorySnapshot,
218
222
  right: HistorySnapshot,
219
223
  ) -> bool:
220
- """Do not combine native, transcript, and undecided history."""
224
+ """Do not combine different providers or terminal generations."""
221
225
  return (
222
226
  left.transcript_backed == right.transcript_backed
223
227
  and left.history_choice_required == right.history_choice_required
228
+ and left.generation == right.generation
224
229
  )
225
230
 
226
231
  @staticmethod
@@ -813,7 +818,11 @@ class LocalHistoryView:
813
818
  ),
814
819
  None,
815
820
  )
816
- if route is None or not self._same_geometry(route, snapshot):
821
+ if (
822
+ route is None
823
+ or not self._same_geometry(route, snapshot)
824
+ or not self._history_source_matches(route, snapshot)
825
+ ):
817
826
  return HistoryAction()
818
827
  viewport = self.viewports.get(pending.pane_id)
819
828
  if viewport is None:
@@ -1,4 +1,4 @@
1
- """Private v14 framing for the coalesced full-window SSH display."""
1
+ """Private v15 framing for the coalesced full-window SSH display."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -8,9 +8,9 @@ from dataclasses import dataclass
8
8
  from enum import IntEnum, IntFlag
9
9
 
10
10
 
11
- DISPLAY_MAGIC = b"RMUXD14\x00"
12
- INPUT_MAGIC = b"RMUXK14\x00"
13
- PROTOCOL_VERSION = 14
11
+ DISPLAY_MAGIC = b"RMUXD15\x00"
12
+ INPUT_MAGIC = b"RMUXK15\x00"
13
+ PROTOCOL_VERSION = 15
14
14
  REMOTE_CONFIG_PROTOCOL = 1
15
15
  LENGTH_BYTES = 4
16
16
  REMOTE_HELLO_PREFIX = b"RAILMUX-REMOTE/1 "
@@ -28,10 +28,10 @@ MAX_HISTORY_PANES = 8
28
28
  MAX_CLIPBOARD_BYTES = 64 * 1024
29
29
  MAX_PATH_BYTES = 4096
30
30
  _UPDATE_METADATA = struct.Struct(">BIHHHHBHI")
31
- _HISTORY_METADATA = struct.Struct(">IIHHHHBI")
31
+ _HISTORY_METADATA = struct.Struct(">IIHHHHBQI")
32
32
  _HISTORY_REQUEST = struct.Struct(">IHHH")
33
33
  _HISTORY_BATCH_METADATA = struct.Struct(">II")
34
- _HISTORY_PANE_METADATA = struct.Struct(">IHHHHB")
34
+ _HISTORY_PANE_METADATA = struct.Struct(">IHHHHBQ")
35
35
  _PREFETCH_HISTORY_REQUEST = struct.Struct(">IH")
36
36
  _PATH_REQUEST = struct.Struct(">IIH")
37
37
  _PATH_RESULT = struct.Struct(">IBBH")
@@ -143,6 +143,7 @@ class HistorySnapshot:
143
143
  more_available: bool = False
144
144
  transcript_available: bool = False
145
145
  history_choice_required: bool = False
146
+ generation: int = 0
146
147
 
147
148
 
148
149
  @dataclass(frozen=True)
@@ -302,6 +303,7 @@ def encode_history_snapshot(snapshot: HistorySnapshot) -> bytes:
302
303
  snapshot.more_available,
303
304
  snapshot.transcript_available,
304
305
  snapshot.history_choice_required,
306
+ snapshot.generation,
305
307
  )):
306
308
  raise ValueError("a rejected history response must be empty")
307
309
  pane_number = 0
@@ -317,6 +319,8 @@ def encode_history_snapshot(snapshot: HistorySnapshot) -> bytes:
317
319
  raise ValueError("invalid history position")
318
320
  if len(snapshot.lines) < snapshot.height:
319
321
  raise ValueError("history snapshot is shorter than its viewport")
322
+ if not 0 <= snapshot.generation <= 0xFFFFFFFFFFFFFFFF:
323
+ raise ValueError("invalid history generation")
320
324
  raw_lines = _pack_history_lines(snapshot.lines)
321
325
  compressed = zlib.compress(raw_lines, level=3)
322
326
  metadata = _HISTORY_METADATA.pack(
@@ -327,6 +331,7 @@ def encode_history_snapshot(snapshot: HistorySnapshot) -> bytes:
327
331
  snapshot.width,
328
332
  snapshot.height,
329
333
  _history_capabilities(snapshot),
334
+ snapshot.generation,
330
335
  len(raw_lines),
331
336
  )
332
337
  payload = bytes((int(OutputKind.HISTORY),)) + metadata + compressed
@@ -360,6 +365,8 @@ def encode_history_batch(batch: HistoryBatch) -> bytes:
360
365
  raise ValueError("invalid history position")
361
366
  if len(snapshot.lines) < snapshot.height:
362
367
  raise ValueError("history snapshot is shorter than its viewport")
368
+ if not 0 <= snapshot.generation <= 0xFFFFFFFFFFFFFFFF:
369
+ raise ValueError("invalid history generation")
363
370
  packed_lines = _pack_history_lines(snapshot.lines)
364
371
  pane_metadata = _HISTORY_PANE_METADATA.pack(
365
372
  pane_number,
@@ -368,6 +375,7 @@ def encode_history_batch(batch: HistoryBatch) -> bytes:
368
375
  snapshot.width,
369
376
  snapshot.height,
370
377
  _history_capabilities(snapshot),
378
+ snapshot.generation,
371
379
  )
372
380
  total += len(pane_metadata) + len(packed_lines)
373
381
  if total > MAX_SCREEN_BYTES:
@@ -611,6 +619,7 @@ class ServerMessageDecoder:
611
619
  width,
612
620
  height,
613
621
  raw_capabilities,
622
+ generation,
614
623
  raw_size,
615
624
  ) = (
616
625
  _HISTORY_METADATA.unpack(body[:_HISTORY_METADATA.size])
@@ -628,6 +637,7 @@ class ServerMessageDecoder:
628
637
  height,
629
638
  lines,
630
639
  raw_capabilities,
640
+ generation,
631
641
  )):
632
642
  raise ValueError("invalid rejected history response")
633
643
  pane_id = None
@@ -662,6 +672,7 @@ class ServerMessageDecoder:
662
672
  history_choice_required=bool(
663
673
  raw_capabilities & _HISTORY_CHOICE_REQUIRED
664
674
  ),
675
+ generation=generation,
665
676
  ))
666
677
  continue
667
678
  if output_kind is OutputKind.HISTORY_BATCH:
@@ -691,6 +702,7 @@ class ServerMessageDecoder:
691
702
  width,
692
703
  height,
693
704
  raw_capabilities,
705
+ generation,
694
706
  ) = _HISTORY_PANE_METADATA.unpack(
695
707
  raw[offset:offset + _HISTORY_PANE_METADATA.size]
696
708
  )
@@ -729,6 +741,7 @@ class ServerMessageDecoder:
729
741
  history_choice_required=bool(
730
742
  raw_capabilities & _HISTORY_CHOICE_REQUIRED
731
743
  ),
744
+ generation=generation,
732
745
  ))
733
746
  if offset != len(raw):
734
747
  raise ValueError("trailing history batch data")
@@ -843,7 +856,7 @@ class ServerMessageDecoder:
843
856
 
844
857
 
845
858
  class ScreenUpdateDecoder:
846
- """Compatibility view which ignores v14 control-response messages."""
859
+ """Compatibility view which ignores v15 control-response messages."""
847
860
 
848
861
  def __init__(self) -> None:
849
862
  self._decoder = ServerMessageDecoder()
@@ -45,6 +45,7 @@ from railmux import (
45
45
  __version__,
46
46
  local_open,
47
47
  restart_state,
48
+ tmux_ctl,
48
49
  tmux_health,
49
50
  tmux_server,
50
51
  )
@@ -403,6 +404,7 @@ class _PaneGeometry:
403
404
  transcript_source: str | None = None
404
405
  transcript_backed: bool = False
405
406
  claude_history_policy: str = "ask"
407
+ history_generation: int = 0
406
408
 
407
409
 
408
410
  @dataclass(frozen=True)
@@ -421,6 +423,24 @@ _INFERRED_TRANSCRIPT_LIMIT = 8
421
423
  _TRANSCRIPT_MAX_BYTES = 32 * 1024 * 1024
422
424
  _SESSION_BINDING_OPTION = "@railmux_binding_v1"
423
425
  _SWAP_OPTIONS = ("@railmux_swap_primary", "@railmux_swap_secondary")
426
+ _HISTORY_GENERATION_RE = re.compile(
427
+ r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-"
428
+ r"[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
429
+ )
430
+
431
+
432
+ def _history_generation(marker: str) -> int:
433
+ """Map one validated provider UUID to a bounded opaque wire epoch."""
434
+ if not _HISTORY_GENERATION_RE.fullmatch(marker):
435
+ return 0
436
+ return int.from_bytes(
437
+ hashlib.blake2b(
438
+ marker.lower().encode("ascii"),
439
+ digest_size=8,
440
+ person=b"railmux-history",
441
+ ).digest(),
442
+ "big",
443
+ )
424
444
 
425
445
 
426
446
  _ANSI_FG = {
@@ -834,6 +854,7 @@ def _list_agent_panes(
834
854
  "#{history_size} #{alternate_on} #{mouse_any_flag} "
835
855
  f"#{{{tmux_server.HISTORY_SOURCE_OPTION}}} "
836
856
  f"#{{{tmux_server.TRANSCRIPT_SOURCE_OPTION}}} "
857
+ f"#{{{tmux_ctl.RAILMUX_HISTORY_GENERATION_OPTION}}} "
837
858
  f"#{{{TOOL_PANE_OPTION}}}",
838
859
  ),
839
860
  stderr=subprocess.DEVNULL,
@@ -849,7 +870,7 @@ def _list_agent_panes(
849
870
  for raw_row in output.splitlines():
850
871
  fields = raw_row.split(" ")
851
872
  if (
852
- len(fields) != 16
873
+ len(fields) != 17
853
874
  or fields[0] != session_id
854
875
  or fields[2] not in ("0", "1")
855
876
  or fields[3] not in ("0", "1")
@@ -880,7 +901,7 @@ def _list_agent_panes(
880
901
  ):
881
902
  return ()
882
903
  seen.add(pane_id)
883
- if is_tool_pane_marker(fields[15], outer_session_id=session_id):
904
+ if is_tool_pane_marker(fields[16], outer_session_id=session_id):
884
905
  continue
885
906
  history_server = None
886
907
  history_pane_id = None
@@ -931,6 +952,7 @@ def _list_agent_panes(
931
952
  transcript_source=transcript_marker,
932
953
  transcript_backed=transcript_backed,
933
954
  claude_history_policy=claude_history_policy,
955
+ history_generation=_history_generation(fields[15]),
934
956
  ),
935
957
  )
936
958
  )
@@ -1376,6 +1398,7 @@ def _capture_pane_history(
1376
1398
  else pane.history_size + pane.height > len(lines)
1377
1399
  )
1378
1400
  ),
1401
+ generation=pane.history_generation,
1379
1402
  )
1380
1403
 
1381
1404
 
@@ -1916,7 +1939,7 @@ def render_rows(screen: object) -> tuple[bytes, ...]:
1916
1939
 
1917
1940
 
1918
1941
  def terminal_modes_for_screen(screen: object) -> TerminalMode:
1919
- """Project pyte's private-mode set onto the bounded v14 wire allowlist."""
1942
+ """Project pyte's private-mode set onto the bounded v15 wire allowlist."""
1920
1943
  terminal_modes = TerminalMode.NONE
1921
1944
  if 2004 << 5 in screen.mode:
1922
1945
  terminal_modes |= TerminalMode.BRACKETED_PASTE
@@ -157,6 +157,14 @@ class RootWheelForwardingManager:
157
157
  state["backup"], token=token)
158
158
  self._remove_state()
159
159
  state = None
160
+ elif not tmux_ctl.root_wheel_bindings_are_current(token):
161
+ # The ownership transaction can survive a soft upgrade
162
+ # in the same tmux pane. Refresh only an exactly-owned
163
+ # wrapper so the new pane-local history route becomes
164
+ # active without treating it as a user binding.
165
+ if not tmux_ctl.set_root_wheel_forwarding(
166
+ state["backup"], token):
167
+ return False
160
168
  if state is not None:
161
169
  state["owners"] = sorted(
162
170
  {*state["owners"], self._owner_pane_id})
@@ -8,6 +8,7 @@ from __future__ import annotations
8
8
 
9
9
  import os
10
10
  import re
11
+ import signal
11
12
  import shlex
12
13
  import shutil
13
14
  import subprocess
@@ -302,6 +303,12 @@ _ROLLOUT_UUID_RE = re.compile(
302
303
  )
303
304
 
304
305
 
306
+ def rollout_uuid_from_path(path: Path) -> str | None:
307
+ """Return the filename UUID for a canonical Codex rollout path."""
308
+ match = _ROLLOUT_UUID_RE.search(path.name)
309
+ return match.group(1) if match is not None else None
310
+
311
+
305
312
  def pane_pid_for_session(session_name: str) -> int | None:
306
313
  """Return the first pane's PID for *session_name*, or None.
307
314
 
@@ -1817,6 +1824,19 @@ _PREFIX_TARGET_KEY = "Tab"
1817
1824
  _PREFIX_TARGET_MARKER = "railmux-target-toggle-v1"
1818
1825
  RAILMUX_CONTROLLER_OPTION = "@railmux_controller_pane"
1819
1826
  RAILMUX_TARGET_OPTION = "@railmux_target_pane"
1827
+ RAILMUX_HISTORY_PREVIEW_OPTION = "@railmux_history_preview_v1"
1828
+ RAILMUX_HISTORY_GENERATION_OPTION = "@railmux_history_generation_v1"
1829
+ # tmux names function keys only through F12. Private controller routes use
1830
+ # canonical xterm sequences in literal mode, so they cannot be confused with
1831
+ # user input forwarded to an agent pane.
1832
+ RAILMUX_HISTORY_PREVIEW_KEYS = {
1833
+ "primary": ("F21", "\x1b[35~"),
1834
+ "secondary": ("F22", "\x1b[36~"),
1835
+ }
1836
+ RAILMUX_HISTORY_RESTORE_KEYS = {
1837
+ "primary": ("F23", "\x1b[42~"),
1838
+ "secondary": ("F24", "\x1b[43~"),
1839
+ }
1820
1840
  RAILMUX_SELECTION_KEY_OPTION = "@railmux_selection_key"
1821
1841
  RAILMUX_SELECTION_PEER_OPTION = "@railmux_selection_peer"
1822
1842
  RAILMUX_SELECTION_FROZEN_OPTION = "@railmux_selection_frozen_by"
@@ -2145,28 +2165,69 @@ def set_root_wheel_forwarding(
2145
2165
  backup: RootWheelBindingBackup,
2146
2166
  token: str,
2147
2167
  ) -> bool:
2148
- """Forward both wheel directions to mouse-aware pane applications.
2168
+ """Open canonical live history on wheel-up, otherwise preserve tmux input.
2149
2169
 
2150
2170
  The literal marker makes crash recovery able to distinguish Railmux's
2151
2171
  wrapper from a binding the user installed while Railmux was running.
2172
+ A pane-local primary/secondary marker routes the first live-agent wheel-up
2173
+ to Railmux's controller; the transcript viewer clears that marker, so its
2174
+ subsequent wheel events are forwarded normally.
2152
2175
  Non-mouse-aware panes retain the exact stock fallback; WheelDownPane was
2153
2176
  unbound by default and therefore has no false branch.
2154
2177
  """
2155
2178
  marker = f"{_ROOT_WHEEL_MARKER}-{token}"
2156
- condition = (
2179
+ mouse_condition = (
2157
2180
  "#{&&:#{mouse_any_flag},"
2158
2181
  f"#{{==:{marker},{marker}}}}}"
2159
2182
  )
2183
+ controller_present = f"#{{!=:#{{{RAILMUX_CONTROLLER_OPTION}}},}}"
2184
+
2185
+ def _preview_condition(slot_key: str) -> str:
2186
+ return (
2187
+ "#{&&:"
2188
+ f"#{{==:#{{{RAILMUX_HISTORY_PREVIEW_OPTION}}},{slot_key}}},"
2189
+ f"{controller_present}}}"
2190
+ )
2191
+
2192
+ def _send_preview(slot_key: str) -> str:
2193
+ _key, sequence = RAILMUX_HISTORY_PREVIEW_KEYS[slot_key]
2194
+ return (
2195
+ f"send-keys -l -t '#{{{RAILMUX_CONTROLLER_OPTION}}}' -- "
2196
+ f"'{sequence}'"
2197
+ )
2198
+
2160
2199
  try:
2161
2200
  for key in _ROOT_WHEEL_KEYS:
2201
+ binding = backup.get(key)
2202
+ fallback = _binding_command(binding) if binding is not None else ""
2203
+ mouse_fallback = (
2204
+ f"if-shell -F -t = '{mouse_condition}' 'send-keys -M'"
2205
+ )
2206
+ if fallback:
2207
+ mouse_fallback += f" '{fallback}'"
2162
2208
  args = [
2163
2209
  "tmux", "bind-key", "-T", "root", key,
2164
- "if-shell", "-F", "-t", "=", condition,
2165
- "send-keys -M",
2166
2210
  ]
2167
- binding = backup.get(key)
2168
- if binding is not None:
2169
- args.append(_binding_command(binding))
2211
+ if key == "WheelUpPane":
2212
+ secondary_fallback = (
2213
+ "if-shell -F -t = "
2214
+ f"'{_preview_condition('secondary')}' "
2215
+ f"\"{_send_preview('secondary')}\" "
2216
+ f"\"{mouse_fallback}\""
2217
+ )
2218
+ args.extend([
2219
+ "if-shell", "-F", "-t", "=",
2220
+ _preview_condition("primary"),
2221
+ _send_preview("primary"),
2222
+ secondary_fallback,
2223
+ ])
2224
+ else:
2225
+ args.extend([
2226
+ "if-shell", "-F", "-t", "=", mouse_condition,
2227
+ "send-keys -M",
2228
+ ])
2229
+ if fallback:
2230
+ args.append(fallback)
2170
2231
  subprocess.check_call(
2171
2232
  args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
2172
2233
  except (ValueError, subprocess.CalledProcessError, FileNotFoundError):
@@ -2186,6 +2247,28 @@ def root_wheel_bindings_owned_by(token: str) -> bool:
2186
2247
  )
2187
2248
 
2188
2249
 
2250
+ def root_wheel_bindings_are_current(token: str) -> bool:
2251
+ """Whether the owned wrapper includes canonical-history routing."""
2252
+ bindings = read_root_wheel_bindings()
2253
+ up = bindings.get("WheelUpPane")
2254
+ marker = f"{_ROOT_WHEEL_MARKER}-{token}"
2255
+ return (
2256
+ all(
2257
+ binding is not None
2258
+ and marker in binding
2259
+ and "mouse_any_flag" in binding
2260
+ and "send-keys -M" in binding
2261
+ for binding in bindings.values()
2262
+ )
2263
+ and up is not None
2264
+ and RAILMUX_HISTORY_PREVIEW_OPTION in up
2265
+ and all(
2266
+ sequence in up or sequence.replace("\x1b", r"\033") in up
2267
+ for _key, sequence in RAILMUX_HISTORY_PREVIEW_KEYS.values()
2268
+ )
2269
+ )
2270
+
2271
+
2189
2272
  def root_wheel_binding_is_original_or_owned(
2190
2273
  key: str,
2191
2274
  binding: str | None,
@@ -3272,6 +3355,47 @@ def exact_pane_alive(identity: PaneIdentity) -> bool:
3272
3355
  )
3273
3356
 
3274
3357
 
3358
+ def reset_pane_history(identity: PaneIdentity) -> bool:
3359
+ """Reset visible bytes and scrollback for the exact observed live pane.
3360
+
3361
+ Revalidate the immutable session, pane process, and window before issuing
3362
+ one tmux command queue that resets the terminal model and clears history.
3363
+ Then send the pane's foreground process group an ordinary same-size
3364
+ ``SIGWINCH`` so its TUI repaints and reasserts terminal modes without a
3365
+ geometry change or injected keyboard input.
3366
+ """
3367
+ current = pane_identity(identity.pane_id)
3368
+ if (current is None or current.dead
3369
+ or current.pane_pid != identity.pane_pid
3370
+ or current.session_id != identity.session_id
3371
+ or current.window_id != identity.window_id):
3372
+ return False
3373
+ try:
3374
+ subprocess.check_call(
3375
+ [
3376
+ "tmux", "send-keys", "-R", "-t", identity.pane_id,
3377
+ ";", "clear-history", "-t", identity.pane_id,
3378
+ ],
3379
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
3380
+ )
3381
+ except (OSError, subprocess.CalledProcessError):
3382
+ return False
3383
+
3384
+ current = pane_identity(identity.pane_id)
3385
+ if (current is None or current.dead
3386
+ or current.pane_pid != identity.pane_pid
3387
+ or current.session_id != identity.session_id
3388
+ or current.window_id != identity.window_id):
3389
+ return True
3390
+ try:
3391
+ os.killpg(os.getpgid(current.pane_pid), signal.SIGWINCH)
3392
+ except (OSError, ValueError):
3393
+ # The presentation cache is already clean. A provider that exited or
3394
+ # cannot be signalled will repaint on its next ordinary terminal write.
3395
+ pass
3396
+ return True
3397
+
3398
+
3275
3399
  def kill_session_identity(identity: PaneIdentity) -> bool:
3276
3400
  """Kill an exact immutable tmux session, never a reused session name."""
3277
3401
  current = pane_identity(identity.pane_id)