railmux 0.3.3.dev6__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.dev6/src/railmux.egg-info → railmux-0.3.4.dev1}/PKG-INFO +17 -12
  2. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/README.md +16 -11
  3. railmux-0.3.4.dev1/src/railmux/__init__.py +1 -0
  4. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/fast_display_server.py +17 -3
  5. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/tmux_ctl.py +34 -59
  6. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/ui/app.py +36 -57
  7. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1/src/railmux.egg-info}/PKG-INFO +17 -12
  8. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_codex_lineage.py +49 -1
  9. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_fast_full_window.py +85 -1
  10. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_focus_highlight.py +4 -14
  11. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_tmux_ctl.py +24 -10
  12. railmux-0.3.3.dev6/src/railmux/__init__.py +0 -1
  13. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/LICENSE +0 -0
  14. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/MANIFEST.in +0 -0
  15. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/pyproject.toml +0 -0
  16. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/setup.cfg +0 -0
  17. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/__main__.py +0 -0
  18. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/atomic_file.py +0 -0
  19. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/background_index.py +0 -0
  20. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/cli.py +0 -0
  21. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/codex_index.py +0 -0
  22. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/config.py +0 -0
  23. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/config_cli.py +0 -0
  24. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/diagnostics.py +0 -0
  25. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/discovery.py +0 -0
  26. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/display_transport.py +0 -0
  27. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/fast_display_client.py +0 -0
  28. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/fast_display_history.py +0 -0
  29. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/fast_display_input.py +0 -0
  30. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/fast_display_protocol.py +0 -0
  31. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/favorites.py +0 -0
  32. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/fuzzy.py +0 -0
  33. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/help_workspace.py +0 -0
  34. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/launcher.py +0 -0
  35. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/legacy_sessions.py +0 -0
  36. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/local_clipboard.py +0 -0
  37. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/local_open.py +0 -0
  38. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/models.py +0 -0
  39. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/modes.py +0 -0
  40. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/mouse_manager.py +0 -0
  41. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/orphan_marker.py +0 -0
  42. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/pane_surface.py +0 -0
  43. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/path_codec.py +0 -0
  44. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/remote_config.py +0 -0
  45. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/renames.py +0 -0
  46. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/restart_state.py +0 -0
  47. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/runtime_config.py +0 -0
  48. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/scroll_agent.py +0 -0
  49. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/scroll_manager.py +0 -0
  50. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/selection_isolation.py +0 -0
  51. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/self_update.py +0 -0
  52. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/session_cache.py +0 -0
  53. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/session_index.py +0 -0
  54. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/setting_contracts.py +0 -0
  55. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/settings.py +0 -0
  56. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/ssh_args.py +0 -0
  57. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/ssh_compat.py +0 -0
  58. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/ssh_display_diagnostics.py +0 -0
  59. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/ssh_doctor.py +0 -0
  60. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/system_deps.py +0 -0
  61. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/terminal_status.py +0 -0
  62. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/tmux_binding_manager.py +0 -0
  63. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/tmux_health.py +0 -0
  64. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/tmux_server.py +0 -0
  65. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/tool_panes.py +0 -0
  66. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/transcript.py +0 -0
  67. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/ui/__init__.py +0 -0
  68. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/ui/_widgets.py +0 -0
  69. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/ui/keymap.py +0 -0
  70. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/ui/modals.py +0 -0
  71. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/ui/projects_pane.py +0 -0
  72. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/ui/running_pane.py +0 -0
  73. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/ui/sessions_pane.py +0 -0
  74. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/ui/sidebar.py +0 -0
  75. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/ui/statusbar.py +0 -0
  76. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux/ui/workspace.py +0 -0
  77. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux.egg-info/SOURCES.txt +0 -0
  78. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux.egg-info/dependency_links.txt +0 -0
  79. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux.egg-info/entry_points.txt +0 -0
  80. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux.egg-info/requires.txt +0 -0
  81. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/src/railmux.egg-info/top_level.txt +0 -0
  82. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_agent_workspace.py +0 -0
  83. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_atomic_file.py +0 -0
  84. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_background_index.py +0 -0
  85. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_cli.py +0 -0
  86. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_codex_delete.py +0 -0
  87. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_codex_index.py +0 -0
  88. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_codex_transcript.py +0 -0
  89. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_config.py +0 -0
  90. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_config_cli.py +0 -0
  91. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_denested_transport_benchmark.py +0 -0
  92. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_diagnostics.py +0 -0
  93. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_discovery.py +0 -0
  94. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_display_transport.py +0 -0
  95. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_fullscreen_binding.py +0 -0
  96. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_fuzzy.py +0 -0
  97. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_help_agent.py +0 -0
  98. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_help_workspace.py +0 -0
  99. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_history_cleanup.py +0 -0
  100. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_keymap.py +0 -0
  101. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_launcher.py +0 -0
  102. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_layout_profile.py +0 -0
  103. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_legacy_sessions.py +0 -0
  104. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_local_clipboard.py +0 -0
  105. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_local_open.py +0 -0
  106. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_modals.py +0 -0
  107. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_mode_selection.py +0 -0
  108. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_modes.py +0 -0
  109. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_mouse_manager.py +0 -0
  110. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_new_project.py +0 -0
  111. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_options.py +0 -0
  112. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_orphan_recovery.py +0 -0
  113. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_pane_surface.py +0 -0
  114. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_path_codec.py +0 -0
  115. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_projects_pane.py +0 -0
  116. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_release_notes.py +0 -0
  117. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_release_workflow.py +0 -0
  118. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_remote_config.py +0 -0
  119. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_renames.py +0 -0
  120. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_restart_state.py +0 -0
  121. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_running_pane.py +0 -0
  122. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_running_sort.py +0 -0
  123. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_runtime_config.py +0 -0
  124. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_scroll_agent.py +0 -0
  125. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_scroll_manager.py +0 -0
  126. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_selection_isolation.py +0 -0
  127. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_self_update.py +0 -0
  128. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_session_cache.py +0 -0
  129. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_session_index.py +0 -0
  130. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_sessions_pane.py +0 -0
  131. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_setting_contracts.py +0 -0
  132. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_settings.py +0 -0
  133. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_sidebar.py +0 -0
  134. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_soft_quit.py +0 -0
  135. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_ssh_args.py +0 -0
  136. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_ssh_compat.py +0 -0
  137. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_ssh_display_benchmark.py +0 -0
  138. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_ssh_display_diagnostics.py +0 -0
  139. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_ssh_doctor.py +0 -0
  140. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_status.py +0 -0
  141. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_statusbar.py +0 -0
  142. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_system_deps.py +0 -0
  143. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_terminal_size.py +0 -0
  144. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_terminal_status.py +0 -0
  145. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_termux_local_touch.py +0 -0
  146. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_tmux_binding_manager.py +0 -0
  147. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_tmux_health.py +0 -0
  148. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_tmux_integration.py +0 -0
  149. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_tmux_server.py +0 -0
  150. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_tmux_status.py +0 -0
  151. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_tool_panes.py +0 -0
  152. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_transcript.py +0 -0
  153. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_utf8_state.py +0 -0
  154. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_web_demo_recorder.py +0 -0
  155. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_wheel_smoke.py +0 -0
  156. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_widgets.py +0 -0
  157. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tests/test_yolo_prompt.py +0 -0
  158. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tools/denested_transport_benchmark.py +0 -0
  159. {railmux-0.3.3.dev6 → railmux-0.3.4.dev1}/tools/release_notes.py +0 -0
  160. {railmux-0.3.3.dev6 → 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.dev6
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
 
@@ -372,16 +372,21 @@ sessions move ahead of the other results.
372
372
 
373
373
  For a stopped session, left-click or press `␣` to view conversation history in
374
374
  the Target pane without starting or resuming the agent. For a running session,
375
- press `␣`, choose **Preview**, or wheel up over its live pane. Preview is
376
- read-only: it cannot send a message or change the session. User and assistant
377
- messages, tool calls, and abbreviated tool output are colour-coded, while
378
- internal context and encrypted reasoning are hidden. Codex rewind lineages and
379
- Claude Code `parentUuid` branches are projected onto the provider's current
380
- conversation: the retained prefix and replacement suffix remain visible, while
381
- the abandoned suffix is hidden. When Railmux observes a running Codex session
382
- advance after rewind or steering, it resets only the current terminal view
383
- before Codex redraws. Retained tmux scrollback is not erased, and managed
384
- history continues from the canonical rollout without its abandoned suffix.
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.
385
390
 
386
391
  Preview opens at the latest activity in `less`; large sessions are limited to
387
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
 
@@ -330,16 +330,21 @@ sessions move ahead of the other results.
330
330
 
331
331
  For a stopped session, left-click or press `␣` to view conversation history in
332
332
  the Target pane without starting or resuming the agent. For a running session,
333
- press `␣`, choose **Preview**, or wheel up over its live pane. Preview is
334
- read-only: it cannot send a message or change the session. User and assistant
335
- messages, tool calls, and abbreviated tool output are colour-coded, while
336
- internal context and encrypted reasoning are hidden. Codex rewind lineages and
337
- Claude Code `parentUuid` branches are projected onto the provider's current
338
- conversation: the retained prefix and replacement suffix remain visible, while
339
- the abandoned suffix is hidden. When Railmux observes a running Codex session
340
- advance after rewind or steering, it resets only the current terminal view
341
- before Codex redraws. Retained tmux scrollback is not erased, and managed
342
- history continues from the canonical rollout without its abandoned suffix.
333
+ press `␣` or choose **Preview**. Preview is read-only: it cannot send a message
334
+ or change the session. User and assistant messages, tool calls, and abbreviated
335
+ tool output are colour-coded, while internal context and encrypted reasoning
336
+ are hidden. Codex rewind lineages and Claude Code `parentUuid` branches are
337
+ projected onto the provider's current conversation: the retained prefix and
338
+ replacement suffix remain visible, while the abandoned suffix is hidden.
339
+
340
+ Scrolling a live agent does not enter Preview. Direct `railmux` uses the
341
+ terminal/tmux scrolling behavior; `railmux ssh` keeps its own bounded,
342
+ per-pane history and normally preserves the live terminal's captured styling.
343
+ When Railmux confirms that a running Codex session advanced after rewind or
344
+ steering, it resets only the current terminal view before Codex redraws and
345
+ invalidates that pane's SSH history generation. Retained tmux scrollback is not
346
+ erased; the SSH history for that branched generation uses the canonical
347
+ rollout so its abandoned suffix stays hidden.
343
348
 
344
349
  Preview opens at the latest activity in `less`; large sessions are limited to
345
350
  their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
@@ -0,0 +1 @@
1
+ __version__ = "0.3.4.dev1"
@@ -406,6 +406,7 @@ class _PaneGeometry:
406
406
  transcript_provider: str | None = None
407
407
  claude_history_policy: str = "ask"
408
408
  history_generation: int = 0
409
+ canonical_history: bool = False
409
410
 
410
411
 
411
412
  @dataclass(frozen=True)
@@ -432,11 +433,14 @@ _HISTORY_GENERATION_RE = re.compile(
432
433
 
433
434
  def _history_generation(marker: str) -> int:
434
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):]
435
439
  if not _HISTORY_GENERATION_RE.fullmatch(marker):
436
440
  return 0
437
441
  return int.from_bytes(
438
442
  hashlib.blake2b(
439
- marker.lower().encode("ascii"),
443
+ generation_marker.lower().encode("ascii"),
440
444
  digest_size=8,
441
445
  person=b"railmux-history",
442
446
  ).digest(),
@@ -937,6 +941,15 @@ def _list_agent_panes(
937
941
  if opened is not None:
938
942
  transcript_locator = opened[0]
939
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
+ )
940
953
  rows.append(
941
954
  (
942
955
  fields[2] == "1",
@@ -960,7 +973,8 @@ def _list_agent_panes(
960
973
  else None
961
974
  ),
962
975
  claude_history_policy=claude_history_policy,
963
- history_generation=_history_generation(fields[15]),
976
+ history_generation=_history_generation(history_marker),
977
+ canonical_history=canonical_history,
964
978
  ),
965
979
  )
966
980
  )
@@ -1347,7 +1361,7 @@ def _capture_pane_history(
1347
1361
  pane.transcript_backed
1348
1362
  and pane.transcript_source
1349
1363
  and (
1350
- transcript_provider == "codex"
1364
+ (transcript_provider == "codex" and pane.canonical_history)
1351
1365
  or (
1352
1366
  transcript_provider == "claude"
1353
1367
  and pane.claude_history_policy == "local"
@@ -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
 
@@ -1117,7 +1117,7 @@ class App:
1117
1117
  session_id: str | None,
1118
1118
  tmux_name: str | None,
1119
1119
  ) -> None:
1120
- """Stamp transcript/history routing for the displayed live agent."""
1120
+ """Stamp the explicit transcript Preview source for a live agent."""
1121
1121
  pane_id = slot.pane_id
1122
1122
  if pane_id is None or not pane_id.startswith("%"):
1123
1123
  return
@@ -1156,19 +1156,6 @@ class App:
1156
1156
  tmux_server.TRANSCRIPT_SOURCE_OPTION,
1157
1157
  marker,
1158
1158
  )
1159
- history_slot = None
1160
- if (
1161
- running is not None
1162
- and running.session_type in ("claude", "codex")
1163
- and logical_id is not None
1164
- and not running.is_placeholder
1165
- ):
1166
- history_slot = slot.key
1167
- tmux_ctl.set_pane_user_option(
1168
- pane_id,
1169
- tmux_ctl.RAILMUX_HISTORY_PREVIEW_OPTION,
1170
- history_slot,
1171
- )
1172
1159
 
1173
1160
  def _paint_slot_active_target(
1174
1161
  self,
@@ -1973,9 +1960,9 @@ class App:
1973
1960
  def _on_session_preview(self, session: SessionMeta) -> None:
1974
1961
  """Show the provider's canonical current-branch history.
1975
1962
 
1976
- Stopped-row clicks, Space, context Preview, and live-pane wheel-up use
1977
- this read-only projection. A Codex rewind lineage is represented by its
1978
- newest rollout; Claude branches are projected by ``transcript``.
1963
+ Stopped-row clicks, Space, and context Preview use this read-only
1964
+ projection. A Codex rewind lineage is represented by its newest
1965
+ rollout; Claude branches are projected by ``transcript``.
1979
1966
  """
1980
1967
  if session.session_type == "codex":
1981
1968
  representative = self._codex_representative(session.session_id)
@@ -2038,20 +2025,6 @@ class App:
2038
2025
  self._set_workspace_target(slot.key)
2039
2026
  self._on_session_preview(session)
2040
2027
 
2041
- def _preview_slot_history(self, slot: AgentSlot) -> None:
2042
- """Handle a pane-local wheel-up routed through the controller."""
2043
- tmux_name = slot.agent_tmux_name
2044
- if tmux_name is None:
2045
- return
2046
- running = self._by_tmux(tmux_name)
2047
- if running is None:
2048
- return
2049
- self._preview_running_history(RunningEntry(
2050
- tmux_name=tmux_name,
2051
- label=running.label,
2052
- status=running.status,
2053
- ))
2054
-
2055
2028
  def _restore_history_slot(self, slot: AgentSlot) -> None:
2056
2029
  """Return a closed live-history viewer to the exact saved agent."""
2057
2030
  if not slot.in_history_mode:
@@ -7458,18 +7431,6 @@ class App:
7458
7431
  if key == COMPACT_RESIZE_KEY.lower():
7459
7432
  self._handle_remote_compact_prepare()
7460
7433
  return
7461
- for slot_key, (preview_key, _sequence) in (
7462
- tmux_ctl.RAILMUX_HISTORY_PREVIEW_KEYS.items()):
7463
- if key == preview_key.lower():
7464
- if (getattr(self, "_loop", None) is not None
7465
- and self._loop.widget is not self._frame):
7466
- return
7467
- workspace = self._agent_workspace()
7468
- self._preview_slot_history(
7469
- workspace.primary
7470
- if slot_key == AgentWorkspace.PRIMARY
7471
- else workspace.secondary)
7472
- return
7473
7434
  for slot_key, (restore_key, _sequence) in (
7474
7435
  tmux_ctl.RAILMUX_HISTORY_RESTORE_KEYS.items()):
7475
7436
  if key == restore_key.lower():
@@ -8627,11 +8588,13 @@ class App:
8627
8588
  therefore survive in raw tmux scrollback even though the canonical
8628
8589
  JSONL is already correct. Baseline the first observed rollout, then
8629
8590
  reset only the live terminal view when the next canonical rollout names
8630
- that baseline as its parent. Managed local/SSH history reads the stamped
8631
- canonical transcript instead of the abandoned raw suffix. On procfs
8632
- systems the new rollout must also be open in this exact pane's process
8633
- tree; an unavailable probe degrades to the provider lineage link, while
8634
- a negative probe waits for a later refresh.
8591
+ that baseline as its parent. Local wheel scrolling remains terminal-
8592
+ native. The SSH display keeps owning wheel history and normally reads
8593
+ raw pane capture; only this confirmed transition marks that history as
8594
+ canonical-transcript-backed so the abandoned suffix stays hidden. On
8595
+ procfs systems the new rollout must also be open in this exact pane's
8596
+ process tree; an unavailable probe degrades to the provider lineage
8597
+ link, while a negative probe waits for a later refresh.
8635
8598
  """
8636
8599
  current_id = meta.session_id
8637
8600
  previous_id = running.codex_canonical_session_id
@@ -8663,7 +8626,7 @@ class App:
8663
8626
  if identity is None or not tmux_ctl.reset_pane_view(identity):
8664
8627
  return
8665
8628
  running.codex_canonical_session_id = current_id
8666
- self._stamp_codex_history_state(running, meta)
8629
+ self._stamp_codex_history_state(running, meta, canonical=True)
8667
8630
 
8668
8631
  def _codex_real_pane_identity(
8669
8632
  self, running: _Running,
@@ -8677,9 +8640,18 @@ class App:
8677
8640
  return topology.single_live_pane if topology is not None else None
8678
8641
 
8679
8642
  def _stamp_codex_history_state(
8680
- self, running: _Running, meta: SessionMeta,
8643
+ self,
8644
+ running: _Running,
8645
+ meta: SessionMeta,
8646
+ *,
8647
+ canonical: bool = False,
8681
8648
  ) -> None:
8682
- """Publish the canonical transcript and its content-free cache epoch."""
8649
+ """Publish Preview source plus the SSH history-source cache epoch.
8650
+
8651
+ A plain UUID keeps managed SSH history on raw pane capture. Only a
8652
+ confirmed rewind transition writes ``canonical:<uuid>`` and permits a
8653
+ transcript projection. Preserve that exact marker across app restarts.
8654
+ """
8683
8655
  if running.is_legacy:
8684
8656
  return
8685
8657
  identity = self._codex_real_pane_identity(running)
@@ -8704,14 +8676,21 @@ class App:
8704
8676
  ) for pane_id in sorted(pane_ids)]
8705
8677
  if marker is not None else []
8706
8678
  )
8707
- generation_results = [
8708
- tmux_ctl.set_pane_user_option(
8679
+ canonical_generation = (
8680
+ f"{tmux_ctl.RAILMUX_CANONICAL_HISTORY_PREFIX}{meta.session_id}"
8681
+ )
8682
+ generation_results = []
8683
+ for pane_id in sorted(pane_ids):
8684
+ existing = tmux_ctl.pane_user_option(
8685
+ pane_id, tmux_ctl.RAILMUX_HISTORY_GENERATION_OPTION)
8686
+ if not canonical and existing == canonical_generation:
8687
+ generation_results.append(True)
8688
+ continue
8689
+ generation_results.append(tmux_ctl.set_pane_user_option(
8709
8690
  pane_id,
8710
8691
  tmux_ctl.RAILMUX_HISTORY_GENERATION_OPTION,
8711
- meta.session_id,
8712
- )
8713
- for pane_id in sorted(pane_ids)
8714
- ]
8692
+ canonical_generation if canonical else meta.session_id,
8693
+ ))
8715
8694
  transcript_stamped = marker is not None and all(transcript_results)
8716
8695
  generation_stamped = all(generation_results)
8717
8696
  running.codex_history_generation_stamped = bool(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.3.3.dev6
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
 
@@ -372,16 +372,21 @@ sessions move ahead of the other results.
372
372
 
373
373
  For a stopped session, left-click or press `␣` to view conversation history in
374
374
  the Target pane without starting or resuming the agent. For a running session,
375
- press `␣`, choose **Preview**, or wheel up over its live pane. Preview is
376
- read-only: it cannot send a message or change the session. User and assistant
377
- messages, tool calls, and abbreviated tool output are colour-coded, while
378
- internal context and encrypted reasoning are hidden. Codex rewind lineages and
379
- Claude Code `parentUuid` branches are projected onto the provider's current
380
- conversation: the retained prefix and replacement suffix remain visible, while
381
- the abandoned suffix is hidden. When Railmux observes a running Codex session
382
- advance after rewind or steering, it resets only the current terminal view
383
- before Codex redraws. Retained tmux scrollback is not erased, and managed
384
- history continues from the canonical rollout without its abandoned suffix.
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.
385
390
 
386
391
  Preview opens at the latest activity in `less`; large sessions are limited to
387
392
  their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
@@ -106,6 +106,7 @@ def test_live_rewind_resets_view_and_stamps_canonical_transcript(
106
106
  monkeypatch.setattr(tmux_ctl, "reset_pane_view", reset)
107
107
  stamped = MagicMock(return_value=True)
108
108
  monkeypatch.setattr(tmux_ctl, "set_pane_user_option", stamped)
109
+ monkeypatch.setattr(tmux_ctl, "pane_user_option", lambda *_args: None)
109
110
  probes: dict[str, set[str] | None] = {}
110
111
 
111
112
  # An indexed child is not enough: on procfs the exact live writer must
@@ -127,7 +128,10 @@ def test_live_rewind_resets_view_and_stamps_canonical_transcript(
127
128
  )
128
129
  assert leaf_id in transcript_call.args[2]
129
130
  assert generation_call.args == (
130
- "%9", tmux_ctl.RAILMUX_HISTORY_GENERATION_OPTION, leaf_id)
131
+ "%9",
132
+ tmux_ctl.RAILMUX_HISTORY_GENERATION_OPTION,
133
+ f"{tmux_ctl.RAILMUX_CANONICAL_HISTORY_PREFIX}{leaf_id}",
134
+ )
131
135
 
132
136
 
133
137
  def test_first_codex_generation_only_establishes_scrollback_baseline(
@@ -197,9 +201,53 @@ def test_codex_history_state_stamps_nested_wrapper_and_real_pane(
197
201
  app._codex_real_pane_identity = MagicMock(return_value=identity)
198
202
  stamped = MagicMock(return_value=True)
199
203
  monkeypatch.setattr(tmux_ctl, "set_pane_user_option", stamped)
204
+ monkeypatch.setattr(tmux_ctl, "pane_user_option", lambda *_args: None)
200
205
 
201
206
  app._stamp_codex_history_state(running, meta)
202
207
 
203
208
  assert running.codex_history_generation_stamped
204
209
  assert {call.args[0] for call in stamped.call_args_list} == {"%9", "%10"}
205
210
  assert stamped.call_count == 4
211
+
212
+
213
+ def test_codex_history_state_preserves_confirmed_canonical_marker_on_restart(
214
+ monkeypatch, tmp_path,
215
+ ) -> None:
216
+ session_id = "019fc7c1-a27c-7ae0-9937-7570552a112a"
217
+ project = Project(tmp_path, "-project", Path(), 1, 0.0)
218
+ running = _Running(
219
+ key=session_id,
220
+ tmux_name="cx-live",
221
+ label="project/conversation",
222
+ project=project,
223
+ session_type="codex",
224
+ )
225
+ meta = SessionMeta(
226
+ project=project,
227
+ session_id=session_id,
228
+ jsonl_path=tmp_path / (
229
+ f"rollout-2026-08-03T13-13-03-{session_id}.jsonl"),
230
+ title="current",
231
+ message_count=2,
232
+ token_total=0,
233
+ last_mtime=2.0,
234
+ session_type="codex",
235
+ )
236
+ identity = tmux_ctl.PaneIdentity(
237
+ "%9", 123, "cx-live", "$4", "@5", False, 80, 24)
238
+ app = App.__new__(App)
239
+ app._codex_real_pane_identity = MagicMock(return_value=identity)
240
+ stamped = MagicMock(return_value=True)
241
+ monkeypatch.setattr(tmux_ctl, "set_pane_user_option", stamped)
242
+ canonical_marker = (
243
+ f"{tmux_ctl.RAILMUX_CANONICAL_HISTORY_PREFIX}{session_id}"
244
+ )
245
+ monkeypatch.setattr(
246
+ tmux_ctl, "pane_user_option", lambda *_args: canonical_marker)
247
+
248
+ app._stamp_codex_history_state(running, meta)
249
+
250
+ assert running.codex_history_generation_stamped
251
+ assert stamped.call_count == 1
252
+ assert stamped.call_args.args[:2] == (
253
+ "%9", tmux_server.TRANSCRIPT_SOURCE_OPTION)