railmux 0.3.3.dev1__tar.gz → 0.3.3.dev3__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.dev1/src/railmux.egg-info → railmux-0.3.3.dev3}/PKG-INFO +19 -11
  2. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/README.md +18 -10
  3. railmux-0.3.3.dev3/src/railmux/__init__.py +1 -0
  4. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/display_transport.py +18 -0
  5. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/mouse_manager.py +8 -0
  6. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/tmux_ctl.py +82 -7
  7. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/transcript.py +83 -28
  8. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/ui/app.py +171 -15
  9. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/ui/modals.py +1 -1
  10. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3/src/railmux.egg-info}/PKG-INFO +19 -11
  11. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_display_transport.py +21 -1
  12. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_focus_highlight.py +135 -8
  13. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_mouse_manager.py +17 -0
  14. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_release_workflow.py +22 -8
  15. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_soft_quit.py +81 -0
  16. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_tmux_ctl.py +28 -0
  17. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_tmux_integration.py +10 -2
  18. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_transcript.py +84 -0
  19. railmux-0.3.3.dev1/src/railmux/__init__.py +0 -1
  20. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/LICENSE +0 -0
  21. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/MANIFEST.in +0 -0
  22. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/pyproject.toml +0 -0
  23. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/setup.cfg +0 -0
  24. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/__main__.py +0 -0
  25. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/atomic_file.py +0 -0
  26. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/background_index.py +0 -0
  27. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/cli.py +0 -0
  28. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/codex_index.py +0 -0
  29. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/config.py +0 -0
  30. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/config_cli.py +0 -0
  31. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/diagnostics.py +0 -0
  32. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/discovery.py +0 -0
  33. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/fast_display_client.py +0 -0
  34. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/fast_display_history.py +0 -0
  35. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/fast_display_input.py +0 -0
  36. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/fast_display_protocol.py +0 -0
  37. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/fast_display_server.py +0 -0
  38. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/favorites.py +0 -0
  39. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/fuzzy.py +0 -0
  40. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/help_workspace.py +0 -0
  41. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/launcher.py +0 -0
  42. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/legacy_sessions.py +0 -0
  43. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/local_clipboard.py +0 -0
  44. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/local_open.py +0 -0
  45. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/models.py +0 -0
  46. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/modes.py +0 -0
  47. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/orphan_marker.py +0 -0
  48. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/pane_surface.py +0 -0
  49. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/path_codec.py +0 -0
  50. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/remote_config.py +0 -0
  51. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/renames.py +0 -0
  52. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/restart_state.py +0 -0
  53. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/runtime_config.py +0 -0
  54. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/scroll_agent.py +0 -0
  55. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/scroll_manager.py +0 -0
  56. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/selection_isolation.py +0 -0
  57. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/self_update.py +0 -0
  58. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/session_cache.py +0 -0
  59. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/session_index.py +0 -0
  60. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/setting_contracts.py +0 -0
  61. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/settings.py +0 -0
  62. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/ssh_args.py +0 -0
  63. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/ssh_compat.py +0 -0
  64. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/ssh_display_diagnostics.py +0 -0
  65. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/ssh_doctor.py +0 -0
  66. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/system_deps.py +0 -0
  67. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/terminal_status.py +0 -0
  68. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/tmux_binding_manager.py +0 -0
  69. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/tmux_health.py +0 -0
  70. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/tmux_server.py +0 -0
  71. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/tool_panes.py +0 -0
  72. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/ui/__init__.py +0 -0
  73. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/ui/_widgets.py +0 -0
  74. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/ui/keymap.py +0 -0
  75. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/ui/projects_pane.py +0 -0
  76. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/ui/running_pane.py +0 -0
  77. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/ui/sessions_pane.py +0 -0
  78. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/ui/sidebar.py +0 -0
  79. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/ui/statusbar.py +0 -0
  80. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux/ui/workspace.py +0 -0
  81. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux.egg-info/SOURCES.txt +0 -0
  82. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux.egg-info/dependency_links.txt +0 -0
  83. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux.egg-info/entry_points.txt +0 -0
  84. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux.egg-info/requires.txt +0 -0
  85. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/src/railmux.egg-info/top_level.txt +0 -0
  86. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_agent_workspace.py +0 -0
  87. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_atomic_file.py +0 -0
  88. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_background_index.py +0 -0
  89. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_cli.py +0 -0
  90. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_codex_delete.py +0 -0
  91. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_codex_index.py +0 -0
  92. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_codex_lineage.py +0 -0
  93. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_codex_transcript.py +0 -0
  94. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_config.py +0 -0
  95. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_config_cli.py +0 -0
  96. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_denested_transport_benchmark.py +0 -0
  97. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_diagnostics.py +0 -0
  98. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_discovery.py +0 -0
  99. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_fast_full_window.py +0 -0
  100. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_fullscreen_binding.py +0 -0
  101. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_fuzzy.py +0 -0
  102. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_help_agent.py +0 -0
  103. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_help_workspace.py +0 -0
  104. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_history_cleanup.py +0 -0
  105. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_keymap.py +0 -0
  106. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_launcher.py +0 -0
  107. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_layout_profile.py +0 -0
  108. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_legacy_sessions.py +0 -0
  109. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_local_clipboard.py +0 -0
  110. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_local_open.py +0 -0
  111. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_modals.py +0 -0
  112. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_mode_selection.py +0 -0
  113. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_modes.py +0 -0
  114. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_new_project.py +0 -0
  115. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_options.py +0 -0
  116. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_orphan_recovery.py +0 -0
  117. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_pane_surface.py +0 -0
  118. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_path_codec.py +0 -0
  119. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_projects_pane.py +0 -0
  120. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_release_notes.py +0 -0
  121. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_remote_config.py +0 -0
  122. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_renames.py +0 -0
  123. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_restart_state.py +0 -0
  124. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_running_pane.py +0 -0
  125. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_running_sort.py +0 -0
  126. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_runtime_config.py +0 -0
  127. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_scroll_agent.py +0 -0
  128. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_scroll_manager.py +0 -0
  129. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_selection_isolation.py +0 -0
  130. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_self_update.py +0 -0
  131. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_session_cache.py +0 -0
  132. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_session_index.py +0 -0
  133. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_sessions_pane.py +0 -0
  134. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_setting_contracts.py +0 -0
  135. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_settings.py +0 -0
  136. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_sidebar.py +0 -0
  137. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_ssh_args.py +0 -0
  138. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_ssh_compat.py +0 -0
  139. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_ssh_display_benchmark.py +0 -0
  140. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_ssh_display_diagnostics.py +0 -0
  141. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_ssh_doctor.py +0 -0
  142. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_status.py +0 -0
  143. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_statusbar.py +0 -0
  144. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_system_deps.py +0 -0
  145. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_terminal_size.py +0 -0
  146. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_terminal_status.py +0 -0
  147. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_termux_local_touch.py +0 -0
  148. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_tmux_binding_manager.py +0 -0
  149. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_tmux_health.py +0 -0
  150. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_tmux_server.py +0 -0
  151. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_tmux_status.py +0 -0
  152. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_tool_panes.py +0 -0
  153. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_utf8_state.py +0 -0
  154. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_web_demo_recorder.py +0 -0
  155. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_wheel_smoke.py +0 -0
  156. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_widgets.py +0 -0
  157. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tests/test_yolo_prompt.py +0 -0
  158. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tools/denested_transport_benchmark.py +0 -0
  159. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/tools/release_notes.py +0 -0
  160. {railmux-0.3.3.dev1 → railmux-0.3.3.dev3}/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.dev1
3
+ Version: 0.3.3.dev3
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,20 @@ 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.
370
377
 
371
378
  Preview opens at the latest activity in `less`; large sessions are limited to
372
379
  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.
380
+ matches, and `q` to exit. If preview began from a live pane, `q` restores that
381
+ exact session; otherwise it restores the prior Target pane. Double-click to
382
+ skip preview and open the session directly.
375
383
 
376
384
  ## Status indicators
377
385
 
@@ -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,20 @@ 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.
328
335
 
329
336
  Preview opens at the latest activity in `less`; large sessions are limited to
330
337
  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.
338
+ matches, and `q` to exit. If preview began from a live pane, `q` restores that
339
+ exact session; otherwise it restores the prior Target pane. Double-click to
340
+ skip preview and open the session directly.
333
341
 
334
342
  ## Status indicators
335
343
 
@@ -0,0 +1 @@
1
+ __version__ = "0.3.3.dev3"
@@ -738,6 +738,22 @@ class AgentDisplayTransport:
738
738
  def _with_phase(state: SwapState, phase: str) -> SwapState:
739
739
  return SwapState(**{**asdict(state), "phase": phase})
740
740
 
741
+ @staticmethod
742
+ def _fit_home_to_display(state: SwapState) -> None:
743
+ """Best-effort preserve the provider's visible geometry at home.
744
+
745
+ While a real pane is displayed, its named session contains only the
746
+ inert placeholder. Resize that detached home window to the real
747
+ pane's current size before swapping back. Otherwise background output
748
+ is formatted at the placeholder's older (often much narrower) launch
749
+ geometry and later appears as a narrow discontinuity in scrollback.
750
+
751
+ Geometry is presentation-only: failure must never weaken or abort the
752
+ identity-pinned return transaction.
753
+ """
754
+ tmux_ctl.fit_session_to_pane(
755
+ state.agent_tmux_name, state.agent_pane_id)
756
+
741
757
  def park(self, slot: AgentSlot) -> bool:
742
758
  """Move one displayed swap agent home without forgetting its slot.
743
759
 
@@ -764,6 +780,7 @@ class AgentDisplayTransport:
764
780
  parking = self._with_phase(state, "parking")
765
781
  if not _write_marker_pair(parking, slot.key):
766
782
  return False
783
+ self._fit_home_to_display(state)
767
784
  if not tmux_ctl.swap_panes(
768
785
  state.agent_pane_id, state.placeholder_pane_id):
769
786
  _write_marker_pair(state, slot.key)
@@ -857,6 +874,7 @@ class AgentDisplayTransport:
857
874
  return False
858
875
  returning = SwapState(**{**asdict(state), "phase": "returning"})
859
876
  _write_marker_pair(returning, slot.key)
877
+ self._fit_home_to_display(state)
860
878
  if not tmux_ctl.swap_panes(
861
879
  state.agent_pane_id, state.placeholder_pane_id):
862
880
  return False
@@ -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})
@@ -1817,6 +1817,18 @@ _PREFIX_TARGET_KEY = "Tab"
1817
1817
  _PREFIX_TARGET_MARKER = "railmux-target-toggle-v1"
1818
1818
  RAILMUX_CONTROLLER_OPTION = "@railmux_controller_pane"
1819
1819
  RAILMUX_TARGET_OPTION = "@railmux_target_pane"
1820
+ RAILMUX_HISTORY_PREVIEW_OPTION = "@railmux_history_preview_v1"
1821
+ # tmux names function keys only through F12. Private controller routes use
1822
+ # canonical xterm sequences in literal mode, so they cannot be confused with
1823
+ # user input forwarded to an agent pane.
1824
+ RAILMUX_HISTORY_PREVIEW_KEYS = {
1825
+ "primary": ("F21", "\x1b[35~"),
1826
+ "secondary": ("F22", "\x1b[36~"),
1827
+ }
1828
+ RAILMUX_HISTORY_RESTORE_KEYS = {
1829
+ "primary": ("F23", "\x1b[42~"),
1830
+ "secondary": ("F24", "\x1b[43~"),
1831
+ }
1820
1832
  RAILMUX_SELECTION_KEY_OPTION = "@railmux_selection_key"
1821
1833
  RAILMUX_SELECTION_PEER_OPTION = "@railmux_selection_peer"
1822
1834
  RAILMUX_SELECTION_FROZEN_OPTION = "@railmux_selection_frozen_by"
@@ -2145,28 +2157,69 @@ def set_root_wheel_forwarding(
2145
2157
  backup: RootWheelBindingBackup,
2146
2158
  token: str,
2147
2159
  ) -> bool:
2148
- """Forward both wheel directions to mouse-aware pane applications.
2160
+ """Open canonical live history on wheel-up, otherwise preserve tmux input.
2149
2161
 
2150
2162
  The literal marker makes crash recovery able to distinguish Railmux's
2151
2163
  wrapper from a binding the user installed while Railmux was running.
2164
+ A pane-local primary/secondary marker routes the first live-agent wheel-up
2165
+ to Railmux's controller; the transcript viewer clears that marker, so its
2166
+ subsequent wheel events are forwarded normally.
2152
2167
  Non-mouse-aware panes retain the exact stock fallback; WheelDownPane was
2153
2168
  unbound by default and therefore has no false branch.
2154
2169
  """
2155
2170
  marker = f"{_ROOT_WHEEL_MARKER}-{token}"
2156
- condition = (
2171
+ mouse_condition = (
2157
2172
  "#{&&:#{mouse_any_flag},"
2158
2173
  f"#{{==:{marker},{marker}}}}}"
2159
2174
  )
2175
+ controller_present = f"#{{!=:#{{{RAILMUX_CONTROLLER_OPTION}}},}}"
2176
+
2177
+ def _preview_condition(slot_key: str) -> str:
2178
+ return (
2179
+ "#{&&:"
2180
+ f"#{{==:#{{{RAILMUX_HISTORY_PREVIEW_OPTION}}},{slot_key}}},"
2181
+ f"{controller_present}}}"
2182
+ )
2183
+
2184
+ def _send_preview(slot_key: str) -> str:
2185
+ _key, sequence = RAILMUX_HISTORY_PREVIEW_KEYS[slot_key]
2186
+ return (
2187
+ f"send-keys -l -t '#{{{RAILMUX_CONTROLLER_OPTION}}}' -- "
2188
+ f"'{sequence}'"
2189
+ )
2190
+
2160
2191
  try:
2161
2192
  for key in _ROOT_WHEEL_KEYS:
2193
+ binding = backup.get(key)
2194
+ fallback = _binding_command(binding) if binding is not None else ""
2195
+ mouse_fallback = (
2196
+ f"if-shell -F -t = '{mouse_condition}' 'send-keys -M'"
2197
+ )
2198
+ if fallback:
2199
+ mouse_fallback += f" '{fallback}'"
2162
2200
  args = [
2163
2201
  "tmux", "bind-key", "-T", "root", key,
2164
- "if-shell", "-F", "-t", "=", condition,
2165
- "send-keys -M",
2166
2202
  ]
2167
- binding = backup.get(key)
2168
- if binding is not None:
2169
- args.append(_binding_command(binding))
2203
+ if key == "WheelUpPane":
2204
+ secondary_fallback = (
2205
+ "if-shell -F -t = "
2206
+ f"'{_preview_condition('secondary')}' "
2207
+ f"\"{_send_preview('secondary')}\" "
2208
+ f"\"{mouse_fallback}\""
2209
+ )
2210
+ args.extend([
2211
+ "if-shell", "-F", "-t", "=",
2212
+ _preview_condition("primary"),
2213
+ _send_preview("primary"),
2214
+ secondary_fallback,
2215
+ ])
2216
+ else:
2217
+ args.extend([
2218
+ "if-shell", "-F", "-t", "=", mouse_condition,
2219
+ "send-keys -M",
2220
+ ])
2221
+ if fallback:
2222
+ args.append(fallback)
2170
2223
  subprocess.check_call(
2171
2224
  args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
2172
2225
  except (ValueError, subprocess.CalledProcessError, FileNotFoundError):
@@ -2186,6 +2239,28 @@ def root_wheel_bindings_owned_by(token: str) -> bool:
2186
2239
  )
2187
2240
 
2188
2241
 
2242
+ def root_wheel_bindings_are_current(token: str) -> bool:
2243
+ """Whether the owned wrapper includes canonical-history routing."""
2244
+ bindings = read_root_wheel_bindings()
2245
+ up = bindings.get("WheelUpPane")
2246
+ marker = f"{_ROOT_WHEEL_MARKER}-{token}"
2247
+ return (
2248
+ all(
2249
+ binding is not None
2250
+ and marker in binding
2251
+ and "mouse_any_flag" in binding
2252
+ and "send-keys -M" in binding
2253
+ for binding in bindings.values()
2254
+ )
2255
+ and up is not None
2256
+ and RAILMUX_HISTORY_PREVIEW_OPTION in up
2257
+ and all(
2258
+ sequence in up or sequence.replace("\x1b", r"\033") in up
2259
+ for _key, sequence in RAILMUX_HISTORY_PREVIEW_KEYS.values()
2260
+ )
2261
+ )
2262
+
2263
+
2189
2264
  def root_wheel_binding_is_original_or_owned(
2190
2265
  key: str,
2191
2266
  binding: str | None,
@@ -295,6 +295,81 @@ def _render_claude_native_tool_results(
295
295
  yield RESET
296
296
 
297
297
 
298
+ def _claude_current_branch(records: list[dict]) -> list[dict]:
299
+ """Project an append-only Claude JSONL onto its current main branch.
300
+
301
+ Claude Code links semantic records with ``uuid`` / ``parentUuid``. Editing
302
+ or rewinding a prompt appends a second child instead of removing the old
303
+ suffix, so file order is not conversation order. The newest non-sidechain
304
+ UUID is the active tip; walking its parents retains the shared prefix and
305
+ the replacement suffix while excluding abandoned and sub-agent branches.
306
+
307
+ UUID-less metadata is retained for the normal renderer filters. If the
308
+ input is a bounded tail, walking stops safely at the first missing parent.
309
+ """
310
+ by_uuid: dict[str, dict] = {}
311
+ tip: str | None = None
312
+ for record in records:
313
+ uuid = record.get("uuid")
314
+ if not isinstance(uuid, str) or not uuid:
315
+ continue
316
+ by_uuid[uuid] = record
317
+ if record.get("isSidechain") is not True:
318
+ tip = uuid
319
+ if tip is None:
320
+ return records
321
+
322
+ current: set[str] = set()
323
+ cursor: str | None = tip
324
+ while cursor and cursor not in current:
325
+ record = by_uuid.get(cursor)
326
+ if record is None:
327
+ break
328
+ current.add(cursor)
329
+ parent = record.get("parentUuid")
330
+ cursor = parent if isinstance(parent, str) and parent else None
331
+
332
+ return [
333
+ record for record in records
334
+ if not isinstance(record.get("uuid"), str)
335
+ or not record.get("uuid")
336
+ or record["uuid"] in current
337
+ ]
338
+
339
+
340
+ def _render_claude_records(
341
+ records: list[dict], *, claude_native: bool,
342
+ ):
343
+ """Render only the active Claude branch from buffered JSONL records."""
344
+ calls: dict[str, str] = {}
345
+ for record in _claude_current_branch(records):
346
+ rtype = record.get("type", "")
347
+ if rtype in _SKIP_TYPES:
348
+ continue
349
+ if _is_real_user(record):
350
+ rendered = (
351
+ _render_claude_native_user(record)
352
+ if claude_native
353
+ else _render_user(record)
354
+ )
355
+ if rendered:
356
+ yield rendered
357
+ if rtype == "user":
358
+ renderer = (
359
+ _render_claude_native_tool_results
360
+ if claude_native
361
+ else _render_claude_tool_results
362
+ )
363
+ yield from renderer(record, calls)
364
+ elif rtype == "assistant":
365
+ renderer = (
366
+ _render_claude_native_assistant
367
+ if claude_native
368
+ else _render_assistant_blocks
369
+ )
370
+ yield from renderer(record, calls)
371
+
372
+
298
373
  def format_transcript(
299
374
  source: Path | object,
300
375
  fmt: str | None = None,
@@ -326,7 +401,10 @@ def format_transcript(
326
401
  # call_id -> tool name, so Codex tool outputs can be labelled with the
327
402
  # call that produced them (issue #3).
328
403
  codex_calls: dict[str, str] = {}
329
- claude_calls: dict[str, str] = {}
404
+ # Claude rewind/edit branches live in one append-only JSONL, so those
405
+ # records must be buffered until the active tip is known. Production
406
+ # callers bound the input to the latest 2,000 records.
407
+ claude_records: list[dict] = []
330
408
  try:
331
409
  if isinstance(source, Path):
332
410
  fh = open(source, "r", encoding="utf-8")
@@ -360,33 +438,10 @@ def format_transcript(
360
438
  if _codex:
361
439
  yield from _render_codex(record, codex_calls)
362
440
  continue
363
-
364
- # -- Claude format (legacy) --------------------------------
365
- if rtype in _SKIP_TYPES:
366
- continue
367
-
368
- if _is_real_user(record):
369
- rendered = (
370
- _render_claude_native_user(record)
371
- if claude_native
372
- else _render_user(record)
373
- )
374
- if rendered:
375
- yield rendered
376
- if rtype == "user":
377
- renderer = (
378
- _render_claude_native_tool_results
379
- if claude_native
380
- else _render_claude_tool_results
381
- )
382
- yield from renderer(record, claude_calls)
383
- elif rtype == "assistant":
384
- renderer = (
385
- _render_claude_native_assistant
386
- if claude_native
387
- else _render_assistant_blocks
388
- )
389
- yield from renderer(record, claude_calls)
441
+ claude_records.append(record)
442
+ if _codex is False:
443
+ yield from _render_claude_records(
444
+ claude_records, claude_native=claude_native)
390
445
  except OSError:
391
446
  yield f"{YELLOW}Could not read: {source}{RESET}\n"
392
447
  finally: