railmux 0.3.3.dev5__tar.gz → 0.3.4.dev1__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 (160) hide show
  1. {railmux-0.3.3.dev5/src/railmux.egg-info → railmux-0.3.4.dev1}/PKG-INFO +20 -13
  2. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/README.md +19 -12
  3. railmux-0.3.4.dev1/src/railmux/__init__.py +1 -0
  4. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/fast_display_server.py +45 -10
  5. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/tmux_ctl.py +42 -69
  6. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/tmux_server.py +22 -4
  7. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/ui/app.py +95 -65
  8. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1/src/railmux.egg-info}/PKG-INFO +20 -13
  9. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_codex_delete.py +1 -1
  10. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_codex_lineage.py +108 -12
  11. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_fast_full_window.py +158 -1
  12. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_focus_highlight.py +25 -15
  13. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_statusbar.py +12 -3
  14. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_tmux_ctl.py +28 -17
  15. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_tmux_integration.py +14 -6
  16. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_tmux_server.py +22 -0
  17. railmux-0.3.3.dev5/src/railmux/__init__.py +0 -1
  18. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/LICENSE +0 -0
  19. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/MANIFEST.in +0 -0
  20. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/pyproject.toml +0 -0
  21. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/setup.cfg +0 -0
  22. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/__main__.py +0 -0
  23. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/atomic_file.py +0 -0
  24. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/background_index.py +0 -0
  25. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/cli.py +0 -0
  26. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/codex_index.py +0 -0
  27. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/config.py +0 -0
  28. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/config_cli.py +0 -0
  29. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/diagnostics.py +0 -0
  30. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/discovery.py +0 -0
  31. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/display_transport.py +0 -0
  32. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/fast_display_client.py +0 -0
  33. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/fast_display_history.py +0 -0
  34. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/fast_display_input.py +0 -0
  35. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/fast_display_protocol.py +0 -0
  36. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/favorites.py +0 -0
  37. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/fuzzy.py +0 -0
  38. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/help_workspace.py +0 -0
  39. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/launcher.py +0 -0
  40. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/legacy_sessions.py +0 -0
  41. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/local_clipboard.py +0 -0
  42. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/local_open.py +0 -0
  43. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/models.py +0 -0
  44. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/modes.py +0 -0
  45. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/mouse_manager.py +0 -0
  46. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/orphan_marker.py +0 -0
  47. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/pane_surface.py +0 -0
  48. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/path_codec.py +0 -0
  49. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/remote_config.py +0 -0
  50. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/renames.py +0 -0
  51. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/restart_state.py +0 -0
  52. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/runtime_config.py +0 -0
  53. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/scroll_agent.py +0 -0
  54. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/scroll_manager.py +0 -0
  55. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/selection_isolation.py +0 -0
  56. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/self_update.py +0 -0
  57. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/session_cache.py +0 -0
  58. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/session_index.py +0 -0
  59. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/setting_contracts.py +0 -0
  60. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/settings.py +0 -0
  61. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/ssh_args.py +0 -0
  62. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/ssh_compat.py +0 -0
  63. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/ssh_display_diagnostics.py +0 -0
  64. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/ssh_doctor.py +0 -0
  65. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/system_deps.py +0 -0
  66. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/terminal_status.py +0 -0
  67. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/tmux_binding_manager.py +0 -0
  68. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/tmux_health.py +0 -0
  69. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/tool_panes.py +0 -0
  70. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/transcript.py +0 -0
  71. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/ui/__init__.py +0 -0
  72. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/ui/_widgets.py +0 -0
  73. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/ui/keymap.py +0 -0
  74. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/ui/modals.py +0 -0
  75. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/ui/projects_pane.py +0 -0
  76. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/ui/running_pane.py +0 -0
  77. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/ui/sessions_pane.py +0 -0
  78. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/ui/sidebar.py +0 -0
  79. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/ui/statusbar.py +0 -0
  80. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux/ui/workspace.py +0 -0
  81. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux.egg-info/SOURCES.txt +0 -0
  82. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux.egg-info/dependency_links.txt +0 -0
  83. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux.egg-info/entry_points.txt +0 -0
  84. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux.egg-info/requires.txt +0 -0
  85. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/src/railmux.egg-info/top_level.txt +0 -0
  86. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_agent_workspace.py +0 -0
  87. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_atomic_file.py +0 -0
  88. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_background_index.py +0 -0
  89. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_cli.py +0 -0
  90. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_codex_index.py +0 -0
  91. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_codex_transcript.py +0 -0
  92. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_config.py +0 -0
  93. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_config_cli.py +0 -0
  94. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_denested_transport_benchmark.py +0 -0
  95. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_diagnostics.py +0 -0
  96. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_discovery.py +0 -0
  97. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_display_transport.py +0 -0
  98. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_fullscreen_binding.py +0 -0
  99. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_fuzzy.py +0 -0
  100. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_help_agent.py +0 -0
  101. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_help_workspace.py +0 -0
  102. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_history_cleanup.py +0 -0
  103. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_keymap.py +0 -0
  104. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_launcher.py +0 -0
  105. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_layout_profile.py +0 -0
  106. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_legacy_sessions.py +0 -0
  107. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_local_clipboard.py +0 -0
  108. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_local_open.py +0 -0
  109. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_modals.py +0 -0
  110. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_mode_selection.py +0 -0
  111. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_modes.py +0 -0
  112. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_mouse_manager.py +0 -0
  113. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_new_project.py +0 -0
  114. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_options.py +0 -0
  115. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_orphan_recovery.py +0 -0
  116. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_pane_surface.py +0 -0
  117. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_path_codec.py +0 -0
  118. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_projects_pane.py +0 -0
  119. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_release_notes.py +0 -0
  120. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_release_workflow.py +0 -0
  121. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_remote_config.py +0 -0
  122. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_renames.py +0 -0
  123. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_restart_state.py +0 -0
  124. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_running_pane.py +0 -0
  125. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_running_sort.py +0 -0
  126. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_runtime_config.py +0 -0
  127. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_scroll_agent.py +0 -0
  128. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_scroll_manager.py +0 -0
  129. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_selection_isolation.py +0 -0
  130. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_self_update.py +0 -0
  131. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_session_cache.py +0 -0
  132. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_session_index.py +0 -0
  133. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_sessions_pane.py +0 -0
  134. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_setting_contracts.py +0 -0
  135. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_settings.py +0 -0
  136. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_sidebar.py +0 -0
  137. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_soft_quit.py +0 -0
  138. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_ssh_args.py +0 -0
  139. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_ssh_compat.py +0 -0
  140. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_ssh_display_benchmark.py +0 -0
  141. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_ssh_display_diagnostics.py +0 -0
  142. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_ssh_doctor.py +0 -0
  143. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_status.py +0 -0
  144. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_system_deps.py +0 -0
  145. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_terminal_size.py +0 -0
  146. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_terminal_status.py +0 -0
  147. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_termux_local_touch.py +0 -0
  148. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_tmux_binding_manager.py +0 -0
  149. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_tmux_health.py +0 -0
  150. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_tmux_status.py +0 -0
  151. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_tool_panes.py +0 -0
  152. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_transcript.py +0 -0
  153. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_utf8_state.py +0 -0
  154. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_web_demo_recorder.py +0 -0
  155. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_wheel_smoke.py +0 -0
  156. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_widgets.py +0 -0
  157. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tests/test_yolo_prompt.py +0 -0
  158. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tools/denested_transport_benchmark.py +0 -0
  159. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/tools/release_notes.py +0 -0
  160. {railmux-0.3.3.dev5 → railmux-0.3.4.dev1}/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.dev5
3
+ Version: 0.3.4.dev1
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,7 +169,7 @@ 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
+ | Wheel over a live agent | Scroll its live terminal history (native locally, Railmux-managed over `railmux ssh`) |
173
173
  | Double-click | Open/attach in the Target pane and move focus there |
174
174
  | Right-click | Context menu (Open, Preview, Info, Rename, Star, Copy title, Kill, Term, Delete) |
175
175
 
@@ -243,8 +243,9 @@ The rename popup starts with the current title pre-filled. Press
243
243
  to cancel.
244
244
 
245
245
  After a delete is confirmed, its popup closes immediately. The bottom-right
246
- status shows **Deleting…** while Railmux stops the exact provider session and
247
- removes its history in the background, then reports the final result.
246
+ status shows **Deleting “session name”…** while Railmux stops the exact
247
+ provider session and removes its history in the background, then reports the
248
+ final result.
248
249
 
249
250
  Press `/` in Projects, Sessions, or Running to filter that section. Active
250
251
  filters are marked with `[filtered]` and survive a soft restart; reopening `/`
@@ -371,15 +372,21 @@ sessions move ahead of the other results.
371
372
 
372
373
  For a stopped session, left-click or press `␣` to view conversation history in
373
374
  the Target pane without starting or resuming the agent. For a running session,
374
- press `␣`, choose **Preview**, or wheel up over its live pane. Preview is
375
- read-only: it cannot send a message or change the session. User and assistant
376
- messages, tool calls, and abbreviated tool output are colour-coded, while
377
- internal context and encrypted reasoning are hidden. Codex rewind lineages and
378
- Claude Code `parentUuid` branches are projected onto the provider's current
379
- conversation: the retained prefix and replacement suffix remain visible, while
380
- the abandoned suffix is hidden. When Railmux observes a running Codex session
381
- advance to its rewind child, it also removes the abandoned terminal screen and
382
- tmux scrollback before Codex redraws the live pane.
375
+ press `␣` or choose **Preview**. Preview is read-only: it cannot send a message
376
+ or change the session. User and assistant messages, tool calls, and abbreviated
377
+ tool output are colour-coded, while internal context and encrypted reasoning
378
+ are hidden. Codex rewind lineages and Claude Code `parentUuid` branches are
379
+ projected onto the provider's current conversation: the retained prefix and
380
+ replacement suffix remain visible, while the abandoned suffix is hidden.
381
+
382
+ Scrolling a live agent does not enter Preview. Direct `railmux` uses the
383
+ terminal/tmux scrolling behavior; `railmux ssh` keeps its own bounded,
384
+ per-pane history and normally preserves the live terminal's captured styling.
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.
383
390
 
384
391
  Preview opens at the latest activity in `less`; large sessions are limited to
385
392
  their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
@@ -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
 
@@ -201,8 +201,9 @@ The rename popup starts with the current title pre-filled. Press
201
201
  to cancel.
202
202
 
203
203
  After a delete is confirmed, its popup closes immediately. The bottom-right
204
- status shows **Deleting…** while Railmux stops the exact provider session and
205
- removes its history in the background, then reports the final result.
204
+ status shows **Deleting “session name”…** while Railmux stops the exact
205
+ provider session and removes its history in the background, then reports the
206
+ final result.
206
207
 
207
208
  Press `/` in Projects, Sessions, or Running to filter that section. Active
208
209
  filters are marked with `[filtered]` and survive a soft restart; reopening `/`
@@ -329,15 +330,21 @@ sessions move ahead of the other results.
329
330
 
330
331
  For a stopped session, left-click or press `␣` to view conversation history in
331
332
  the Target pane without starting or resuming the agent. For a running session,
332
- press `␣`, choose **Preview**, or wheel up over its live pane. Preview is
333
- read-only: it cannot send a message or change the session. User and assistant
334
- messages, tool calls, and abbreviated tool output are colour-coded, while
335
- internal context and encrypted reasoning are hidden. Codex rewind lineages and
336
- Claude Code `parentUuid` branches are projected onto the provider's current
337
- conversation: the retained prefix and replacement suffix remain visible, while
338
- the abandoned suffix is hidden. When Railmux observes a running Codex session
339
- advance to its rewind child, it also removes the abandoned terminal screen and
340
- tmux scrollback before Codex redraws the live pane.
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.
341
348
 
342
349
  Preview opens at the latest activity in `less`; large sessions are limited to
343
350
  their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
@@ -0,0 +1 @@
1
+ __version__ = "0.3.4.dev1"
@@ -403,8 +403,10 @@ class _PaneGeometry:
403
403
  alternate_on: bool = False
404
404
  transcript_source: str | None = None
405
405
  transcript_backed: bool = False
406
+ transcript_provider: str | None = None
406
407
  claude_history_policy: str = "ask"
407
408
  history_generation: int = 0
409
+ canonical_history: bool = False
408
410
 
409
411
 
410
412
  @dataclass(frozen=True)
@@ -431,11 +433,14 @@ _HISTORY_GENERATION_RE = re.compile(
431
433
 
432
434
  def _history_generation(marker: str) -> int:
433
435
  """Map one validated provider UUID to a bounded opaque wire epoch."""
436
+ generation_marker = marker
437
+ if marker.startswith(tmux_ctl.RAILMUX_CANONICAL_HISTORY_PREFIX):
438
+ marker = marker[len(tmux_ctl.RAILMUX_CANONICAL_HISTORY_PREFIX):]
434
439
  if not _HISTORY_GENERATION_RE.fullmatch(marker):
435
440
  return 0
436
441
  return int.from_bytes(
437
442
  hashlib.blake2b(
438
- marker.lower().encode("ascii"),
443
+ generation_marker.lower().encode("ascii"),
439
444
  digest_size=8,
440
445
  person=b"railmux-history",
441
446
  ).digest(),
@@ -916,10 +921,11 @@ def _list_agent_panes(
916
921
  if history_pane_id is None:
917
922
  history_server = None
918
923
  transcript_marker = fields[14] or None
919
- transcript_backed = (
920
- transcript_marker is not None
921
- and tmux_server.decode_transcript_source(transcript_marker) is not None
924
+ transcript_locator = (
925
+ tmux_server.decode_transcript_source(transcript_marker)
926
+ if transcript_marker is not None else None
922
927
  )
928
+ transcript_backed = transcript_locator is not None
923
929
  if not transcript_backed and fields[11] == "1" and fields[12] == "1":
924
930
  transcript_backed, transcript_marker = _inferred_transcript_source(
925
931
  session_id=session_id,
@@ -933,7 +939,17 @@ def _list_agent_panes(
933
939
  opened = tmux_server.open_transcript_source(transcript_marker)
934
940
  transcript_backed = opened is not None
935
941
  if opened is not None:
942
+ transcript_locator = opened[0]
936
943
  os.close(opened[1])
944
+ history_marker = fields[15]
945
+ canonical_history = bool(
946
+ transcript_backed
947
+ and transcript_locator is not None
948
+ and history_marker == (
949
+ f"{tmux_ctl.RAILMUX_CANONICAL_HISTORY_PREFIX}"
950
+ f"{transcript_locator.session_id}"
951
+ )
952
+ )
937
953
  rows.append(
938
954
  (
939
955
  fields[2] == "1",
@@ -951,8 +967,14 @@ def _list_agent_panes(
951
967
  alternate_on=fields[11] == "1",
952
968
  transcript_source=transcript_marker,
953
969
  transcript_backed=transcript_backed,
970
+ transcript_provider=(
971
+ transcript_locator.provider
972
+ if transcript_backed and transcript_locator is not None
973
+ else None
974
+ ),
954
975
  claude_history_policy=claude_history_policy,
955
- history_generation=_history_generation(fields[15]),
976
+ history_generation=_history_generation(history_marker),
977
+ canonical_history=canonical_history,
956
978
  ),
957
979
  )
958
980
  )
@@ -1267,7 +1289,9 @@ def _transcript_rows(
1267
1289
  return None
1268
1290
  formatted = "".join(
1269
1291
  transcript_renderer.format_transcript(
1270
- io.StringIO(raw), "claude", claude_native=True
1292
+ io.StringIO(raw),
1293
+ source.provider,
1294
+ claude_native=source.provider == "claude",
1271
1295
  )
1272
1296
  )
1273
1297
  rows, dropped = _wrap_transcript_rows(pyte, formatted, width)
@@ -1330,12 +1354,21 @@ def _capture_pane_history(
1330
1354
  # pane's current viewport merely to retain older scrollback.
1331
1355
  current_raw = raw_lines[-pane.height :]
1332
1356
  transcript_entry = None
1357
+ # ``None`` is retained only for old/test geometry that predates the
1358
+ # provider field; every validated live marker carries an explicit value.
1359
+ transcript_provider = pane.transcript_provider or "claude"
1333
1360
  if (
1334
1361
  pane.transcript_backed
1335
- and pane.claude_history_policy == "local"
1336
- and pane.alternate_on
1337
- and pane.history_size == 0
1338
1362
  and pane.transcript_source
1363
+ and (
1364
+ (transcript_provider == "codex" and pane.canonical_history)
1365
+ or (
1366
+ transcript_provider == "claude"
1367
+ and pane.claude_history_policy == "local"
1368
+ and pane.alternate_on
1369
+ and pane.history_size == 0
1370
+ )
1371
+ )
1339
1372
  ):
1340
1373
  transcript_entry = _transcript_rows(
1341
1374
  pyte,
@@ -1387,7 +1420,9 @@ def _capture_pane_history(
1387
1420
  transcript_backed=transcript_used,
1388
1421
  transcript_available=pane.transcript_backed,
1389
1422
  history_choice_required=(
1390
- pane.transcript_backed and pane.claude_history_policy == "ask"
1423
+ pane.transcript_backed
1424
+ and transcript_provider == "claude"
1425
+ and pane.claude_history_policy == "ask"
1391
1426
  ),
1392
1427
  more_available=(
1393
1428
  budget_truncated
@@ -1434,6 +1434,18 @@ def set_pane_user_option(target_pane: str, name: str,
1434
1434
  return False
1435
1435
 
1436
1436
 
1437
+ def pane_user_option(target_pane: str, name: str) -> str | None:
1438
+ """Read one pane-local user option, returning ``None`` when unavailable."""
1439
+ try:
1440
+ value = subprocess.check_output(
1441
+ ["tmux", "show-options", "-pv", "-t", target_pane, name],
1442
+ stderr=subprocess.DEVNULL,
1443
+ ).decode().strip()
1444
+ except (OSError, subprocess.CalledProcessError, UnicodeError):
1445
+ return None
1446
+ return value or None
1447
+
1448
+
1437
1449
  def unset_pane_user_option_if_value(target_pane: str, name: str,
1438
1450
  expected: str) -> bool:
1439
1451
  """Unset a pane option only when Railmux still owns its exact value."""
@@ -1824,15 +1836,14 @@ _PREFIX_TARGET_KEY = "Tab"
1824
1836
  _PREFIX_TARGET_MARKER = "railmux-target-toggle-v1"
1825
1837
  RAILMUX_CONTROLLER_OPTION = "@railmux_controller_pane"
1826
1838
  RAILMUX_TARGET_OPTION = "@railmux_target_pane"
1827
- RAILMUX_HISTORY_PREVIEW_OPTION = "@railmux_history_preview_v1"
1828
1839
  RAILMUX_HISTORY_GENERATION_OPTION = "@railmux_history_generation_v1"
1829
- # tmux names function keys only through F12. Private controller routes use
1840
+ # A confirmed branch transition prefixes the opaque SSH history cache epoch.
1841
+ # Plain generations use native pane history; only this explicit marker permits
1842
+ # the canonical transcript fallback that removes an abandoned rewind suffix.
1843
+ RAILMUX_CANONICAL_HISTORY_PREFIX = "canonical:"
1844
+ # tmux names function keys only through F12. Private restore routes use
1830
1845
  # canonical xterm sequences in literal mode, so they cannot be confused with
1831
1846
  # user input forwarded to an agent pane.
1832
- RAILMUX_HISTORY_PREVIEW_KEYS = {
1833
- "primary": ("F21", "\x1b[35~"),
1834
- "secondary": ("F22", "\x1b[36~"),
1835
- }
1836
1847
  RAILMUX_HISTORY_RESTORE_KEYS = {
1837
1848
  "primary": ("F23", "\x1b[42~"),
1838
1849
  "secondary": ("F24", "\x1b[43~"),
@@ -2165,69 +2176,30 @@ def set_root_wheel_forwarding(
2165
2176
  backup: RootWheelBindingBackup,
2166
2177
  token: str,
2167
2178
  ) -> bool:
2168
- """Open canonical live history on wheel-up, otherwise preserve tmux input.
2179
+ """Preserve terminal-native wheel behavior under shared ownership.
2169
2180
 
2170
2181
  The literal marker makes crash recovery able to distinguish Railmux's
2171
2182
  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.
2175
- Non-mouse-aware panes retain the exact stock fallback; WheelDownPane was
2176
- unbound by default and therefore has no false branch.
2183
+ Mouse-aware panes receive the original report, while non-mouse-aware panes
2184
+ retain the exact stock fallback (including native copy-mode on wheel-up).
2185
+ A live wheel must never be converted into Railmux's transcript Preview.
2177
2186
  """
2178
2187
  marker = f"{_ROOT_WHEEL_MARKER}-{token}"
2179
2188
  mouse_condition = (
2180
2189
  "#{&&:#{mouse_any_flag},"
2181
2190
  f"#{{==:{marker},{marker}}}}}"
2182
2191
  )
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
-
2199
2192
  try:
2200
2193
  for key in _ROOT_WHEEL_KEYS:
2201
2194
  binding = backup.get(key)
2202
2195
  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}'"
2208
2196
  args = [
2209
2197
  "tmux", "bind-key", "-T", "root", key,
2198
+ "if-shell", "-F", "-t", "=", mouse_condition,
2199
+ "send-keys -M",
2210
2200
  ]
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)
2201
+ if fallback:
2202
+ args.append(fallback)
2231
2203
  subprocess.check_call(
2232
2204
  args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
2233
2205
  except (ValueError, subprocess.CalledProcessError, FileNotFoundError):
@@ -2248,7 +2220,7 @@ def root_wheel_bindings_owned_by(token: str) -> bool:
2248
2220
 
2249
2221
 
2250
2222
  def root_wheel_bindings_are_current(token: str) -> bool:
2251
- """Whether the owned wrapper includes canonical-history routing."""
2223
+ """Whether the owned wrapper preserves native live-pane scrolling."""
2252
2224
  bindings = read_root_wheel_bindings()
2253
2225
  up = bindings.get("WheelUpPane")
2254
2226
  marker = f"{_ROOT_WHEEL_MARKER}-{token}"
@@ -2261,11 +2233,14 @@ def root_wheel_bindings_are_current(token: str) -> bool:
2261
2233
  for binding in bindings.values()
2262
2234
  )
2263
2235
  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
- )
2236
+ and "copy-mode -e" in up
2237
+ # Reject the 0.3.3 wrapper during a soft upgrade. It routed live wheel
2238
+ # input through private F21/F22 keys into transcript Preview.
2239
+ and "@railmux_history_preview_v1" not in up
2240
+ and r"\033[35~" not in up
2241
+ and r"\033[36~" not in up
2242
+ and "\x1b[35~" not in up
2243
+ and "\x1b[36~" not in up
2269
2244
  )
2270
2245
 
2271
2246
 
@@ -3355,14 +3330,15 @@ def exact_pane_alive(identity: PaneIdentity) -> bool:
3355
3330
  )
3356
3331
 
3357
3332
 
3358
- def reset_pane_history(identity: PaneIdentity) -> bool:
3359
- """Reset visible bytes and scrollback for the exact observed live pane.
3333
+ def reset_pane_view(identity: PaneIdentity) -> bool:
3334
+ """Reset visible terminal state without deleting retained scrollback.
3360
3335
 
3361
3336
  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.
3337
+ tmux's terminal reset. Then send the pane's foreground process group an
3338
+ ordinary same-size ``SIGWINCH`` so its TUI repaints and reasserts terminal
3339
+ modes without a geometry change or injected keyboard input. The reset can
3340
+ push the old viewport into tmux history, but never destroys the retained
3341
+ prefix; managed history surfaces use the provider's canonical transcript.
3366
3342
  """
3367
3343
  current = pane_identity(identity.pane_id)
3368
3344
  if (current is None or current.dead
@@ -3372,10 +3348,7 @@ def reset_pane_history(identity: PaneIdentity) -> bool:
3372
3348
  return False
3373
3349
  try:
3374
3350
  subprocess.check_call(
3375
- [
3376
- "tmux", "send-keys", "-R", "-t", identity.pane_id,
3377
- ";", "clear-history", "-t", identity.pane_id,
3378
- ],
3351
+ ["tmux", "send-keys", "-R", "-t", identity.pane_id],
3379
3352
  stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
3380
3353
  )
3381
3354
  except (OSError, subprocess.CalledProcessError):
@@ -307,11 +307,21 @@ def encode_transcript_source(
307
307
  ) -> str | None:
308
308
  """Encode an exact transcript hint for a displayed managed agent pane."""
309
309
  raw_path = str(path)
310
+ valid_name = (
311
+ path.name == f"{session_id}.jsonl"
312
+ if provider == "claude"
313
+ else (
314
+ path.name.startswith("rollout-")
315
+ and path.name.endswith(f"-{session_id}.jsonl")
316
+ )
317
+ if provider == "codex"
318
+ else False
319
+ )
310
320
  if (
311
- provider != "claude"
321
+ provider not in {"claude", "codex"}
312
322
  or not _SESSION_ID_RE.fullmatch(session_id)
313
323
  or not path.is_absolute()
314
- or path.name != f"{session_id}.jsonl"
324
+ or not valid_name
315
325
  or len(raw_path) > 4096
316
326
  ):
317
327
  return None
@@ -348,7 +358,7 @@ def decode_transcript_source(raw: str) -> TranscriptSource | None:
348
358
  raw_path = payload.get("path")
349
359
  if (
350
360
  payload.get("schema_version") != _TRANSCRIPT_SOURCE_SCHEMA
351
- or provider != "claude"
361
+ or provider not in {"claude", "codex"}
352
362
  or not isinstance(session_id, str)
353
363
  or not _SESSION_ID_RE.fullmatch(session_id)
354
364
  or not isinstance(raw_path, str)
@@ -357,7 +367,15 @@ def decode_transcript_source(raw: str) -> TranscriptSource | None:
357
367
  ):
358
368
  return None
359
369
  path = Path(raw_path)
360
- if not path.is_absolute() or path.name != f"{session_id}.jsonl":
370
+ valid_name = (
371
+ path.name == f"{session_id}.jsonl"
372
+ if provider == "claude"
373
+ else (
374
+ path.name.startswith("rollout-")
375
+ and path.name.endswith(f"-{session_id}.jsonl")
376
+ )
377
+ )
378
+ if not path.is_absolute() or not valid_name:
361
379
  return None
362
380
  return TranscriptSource(provider, session_id, path)
363
381