railmux 0.3.6.dev1__tar.gz → 0.3.6.dev2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. {railmux-0.3.6.dev1/src/railmux.egg-info → railmux-0.3.6.dev2}/PKG-INFO +6 -6
  2. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/README.md +5 -5
  3. railmux-0.3.6.dev2/src/railmux/__init__.py +1 -0
  4. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/fast_display_history.py +93 -25
  5. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/tmux_binding_manager.py +5 -2
  6. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/tmux_ctl.py +7 -41
  7. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/app.py +244 -42
  8. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/modals.py +81 -0
  9. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2/src/railmux.egg-info}/PKG-INFO +6 -6
  10. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_agent_workspace.py +1 -0
  11. railmux-0.3.6.dev2/tests/test_codex_lineage.py +561 -0
  12. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_fast_full_window.py +177 -43
  13. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_focus_highlight.py +6 -2
  14. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_modals.py +68 -0
  15. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_soft_quit.py +101 -1
  16. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_tmux_binding_manager.py +25 -0
  17. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_tmux_ctl.py +0 -24
  18. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_tmux_integration.py +12 -49
  19. railmux-0.3.6.dev1/src/railmux/__init__.py +0 -1
  20. railmux-0.3.6.dev1/tests/test_codex_lineage.py +0 -253
  21. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/LICENSE +0 -0
  22. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/MANIFEST.in +0 -0
  23. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/pyproject.toml +0 -0
  24. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/setup.cfg +0 -0
  25. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/__main__.py +0 -0
  26. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/atomic_file.py +0 -0
  27. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/background_index.py +0 -0
  28. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/cli.py +0 -0
  29. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/codex_index.py +0 -0
  30. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/config.py +0 -0
  31. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/config_cli.py +0 -0
  32. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/diagnostics.py +0 -0
  33. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/discovery.py +0 -0
  34. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/display_transport.py +0 -0
  35. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/fast_display_client.py +0 -0
  36. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/fast_display_input.py +0 -0
  37. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/fast_display_protocol.py +0 -0
  38. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/fast_display_server.py +0 -0
  39. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/favorites.py +0 -0
  40. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/fuzzy.py +0 -0
  41. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/help_workspace.py +0 -0
  42. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/launcher.py +0 -0
  43. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/legacy_sessions.py +0 -0
  44. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/local_clipboard.py +0 -0
  45. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/local_open.py +0 -0
  46. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/models.py +0 -0
  47. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/modes.py +0 -0
  48. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/mouse_manager.py +0 -0
  49. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/orphan_marker.py +0 -0
  50. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/pane_surface.py +0 -0
  51. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/path_codec.py +0 -0
  52. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/remote_config.py +0 -0
  53. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/renames.py +0 -0
  54. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/restart_state.py +0 -0
  55. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/runtime_config.py +0 -0
  56. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/scroll_agent.py +0 -0
  57. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/scroll_manager.py +0 -0
  58. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/selection_isolation.py +0 -0
  59. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/self_update.py +0 -0
  60. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/session_cache.py +0 -0
  61. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/session_index.py +0 -0
  62. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/session_lease.py +0 -0
  63. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/setting_contracts.py +0 -0
  64. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/settings.py +0 -0
  65. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/ssh_args.py +0 -0
  66. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/ssh_compat.py +0 -0
  67. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/ssh_display_diagnostics.py +0 -0
  68. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/ssh_doctor.py +0 -0
  69. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/system_deps.py +0 -0
  70. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/terminal_status.py +0 -0
  71. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/tmux_health.py +0 -0
  72. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/tmux_server.py +0 -0
  73. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/tool_panes.py +0 -0
  74. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/transcript.py +0 -0
  75. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/__init__.py +0 -0
  76. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/_widgets.py +0 -0
  77. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/keymap.py +0 -0
  78. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/projects_pane.py +0 -0
  79. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/running_pane.py +0 -0
  80. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/sessions_pane.py +0 -0
  81. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/sidebar.py +0 -0
  82. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/statusbar.py +0 -0
  83. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/workspace.py +0 -0
  84. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux.egg-info/SOURCES.txt +0 -0
  85. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux.egg-info/dependency_links.txt +0 -0
  86. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux.egg-info/entry_points.txt +0 -0
  87. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux.egg-info/requires.txt +0 -0
  88. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux.egg-info/top_level.txt +0 -0
  89. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_atomic_file.py +0 -0
  90. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_background_index.py +0 -0
  91. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_cli.py +0 -0
  92. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_codex_delete.py +0 -0
  93. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_codex_index.py +0 -0
  94. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_codex_transcript.py +0 -0
  95. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_config.py +0 -0
  96. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_config_cli.py +0 -0
  97. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_denested_transport_benchmark.py +0 -0
  98. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_diagnostics.py +0 -0
  99. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_discovery.py +0 -0
  100. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_display_transport.py +0 -0
  101. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_fullscreen_binding.py +0 -0
  102. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_fuzzy.py +0 -0
  103. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_help_agent.py +0 -0
  104. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_help_workspace.py +0 -0
  105. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_history_cleanup.py +0 -0
  106. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_keymap.py +0 -0
  107. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_launcher.py +0 -0
  108. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_layout_profile.py +0 -0
  109. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_legacy_sessions.py +0 -0
  110. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_local_clipboard.py +0 -0
  111. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_local_open.py +0 -0
  112. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_mode_selection.py +0 -0
  113. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_modes.py +0 -0
  114. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_mouse_manager.py +0 -0
  115. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_new_project.py +0 -0
  116. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_options.py +0 -0
  117. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_orphan_recovery.py +0 -0
  118. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_pane_surface.py +0 -0
  119. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_path_codec.py +0 -0
  120. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_projects_pane.py +0 -0
  121. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_release_notes.py +0 -0
  122. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_release_workflow.py +0 -0
  123. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_remote_config.py +0 -0
  124. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_renames.py +0 -0
  125. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_restart_state.py +0 -0
  126. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_running_pane.py +0 -0
  127. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_running_sort.py +0 -0
  128. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_runtime_config.py +0 -0
  129. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_scroll_agent.py +0 -0
  130. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_scroll_manager.py +0 -0
  131. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_selection_isolation.py +0 -0
  132. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_self_update.py +0 -0
  133. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_session_cache.py +0 -0
  134. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_session_index.py +0 -0
  135. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_session_lease.py +0 -0
  136. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_sessions_pane.py +0 -0
  137. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_setting_contracts.py +0 -0
  138. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_settings.py +0 -0
  139. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_sidebar.py +0 -0
  140. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_ssh_args.py +0 -0
  141. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_ssh_compat.py +0 -0
  142. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_ssh_display_benchmark.py +0 -0
  143. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_ssh_display_diagnostics.py +0 -0
  144. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_ssh_doctor.py +0 -0
  145. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_status.py +0 -0
  146. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_statusbar.py +0 -0
  147. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_system_deps.py +0 -0
  148. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_terminal_size.py +0 -0
  149. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_terminal_status.py +0 -0
  150. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_termux_local_touch.py +0 -0
  151. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_tmux_health.py +0 -0
  152. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_tmux_server.py +0 -0
  153. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_tmux_status.py +0 -0
  154. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_tool_panes.py +0 -0
  155. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_transcript.py +0 -0
  156. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_utf8_state.py +0 -0
  157. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_web_demo_recorder.py +0 -0
  158. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_wheel_smoke.py +0 -0
  159. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_widgets.py +0 -0
  160. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_yolo_prompt.py +0 -0
  161. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tools/denested_transport_benchmark.py +0 -0
  162. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tools/release_notes.py +0 -0
  163. {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tools/wheel_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.3.6.dev1
3
+ Version: 0.3.6.dev2
4
4
  Summary: Terminal UI to navigate, resume, and start Claude Code and Codex sessions
5
5
  Author-email: Rightglow <zhang.taian@foxmail.com>
6
6
  License-Expression: MIT
@@ -383,10 +383,9 @@ Scrolling a live agent does not enter Preview. Direct `railmux` uses the
383
383
  terminal/tmux scrolling behavior; `railmux ssh` keeps its own bounded,
384
384
  per-pane history and normally preserves the live terminal's captured styling.
385
385
  When Railmux confirms that a running Codex session advanced after rewind or
386
- steering, it resets only the current terminal view before Codex redraws and
387
- invalidates that pane's SSH history generation. Retained tmux scrollback is not
388
- erased; the SSH history for that branched generation uses the canonical
389
- rollout so its abandoned suffix stays hidden.
386
+ steering, it leaves the live terminal untouched and advances only that pane's
387
+ SSH history generation. The SSH history for that branched generation uses the
388
+ canonical rollout so its abandoned suffix stays hidden.
390
389
 
391
390
  Preview opens at the latest activity in `less`; large sessions are limited to
392
391
  their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
@@ -445,7 +444,8 @@ Each opened agent runs in a detached tmux session, so switching sessions does
445
444
  not interrupt it. To leave agents running when you quit Railmux, press `s` for
446
445
  soft quit in the confirmation popup; restarting the same Railmux instance then
447
446
  restores the usable workspace when those sessions are still available. A normal
448
- quit confirmation ends all running sessions instead.
447
+ quit ends all running sessions instead and therefore requires a second
448
+ confirmation; `y` and Enter keep their existing confirm meaning in both steps.
449
449
 
450
450
  All terminals attached to one managed Railmux window view the same UI process.
451
451
  Soft Quit therefore closes that shared UI for every attached view, although it
@@ -341,10 +341,9 @@ Scrolling a live agent does not enter Preview. Direct `railmux` uses the
341
341
  terminal/tmux scrolling behavior; `railmux ssh` keeps its own bounded,
342
342
  per-pane history and normally preserves the live terminal's captured styling.
343
343
  When Railmux confirms that a running Codex session advanced after rewind or
344
- steering, it resets only the current terminal view before Codex redraws and
345
- invalidates that pane's SSH history generation. Retained tmux scrollback is not
346
- erased; the SSH history for that branched generation uses the canonical
347
- rollout so its abandoned suffix stays hidden.
344
+ steering, it leaves the live terminal untouched and advances only that pane's
345
+ SSH history generation. The SSH history for that branched generation uses the
346
+ canonical rollout so its abandoned suffix stays hidden.
348
347
 
349
348
  Preview opens at the latest activity in `less`; large sessions are limited to
350
349
  their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
@@ -403,7 +402,8 @@ Each opened agent runs in a detached tmux session, so switching sessions does
403
402
  not interrupt it. To leave agents running when you quit Railmux, press `s` for
404
403
  soft quit in the confirmation popup; restarting the same Railmux instance then
405
404
  restores the usable workspace when those sessions are still available. A normal
406
- quit confirmation ends all running sessions instead.
405
+ quit ends all running sessions instead and therefore requires a second
406
+ confirmation; `y` and Enter keep their existing confirm meaning in both steps.
407
407
 
408
408
  All terminals attached to one managed Railmux window view the same UI process.
409
409
  Soft Quit therefore closes that shared UI for every attached view, although it
@@ -0,0 +1 @@
1
+ __version__ = "0.3.6.dev2"
@@ -109,6 +109,10 @@ class _PendingHistory:
109
109
  pane_id: str
110
110
  target_lines: int
111
111
  requested_at: float
112
+ # A first wheel-up waits for one cumulative snapshot before freezing the
113
+ # viewport. This avoids exposing a short hot suffix whose later deep page
114
+ # may not share a stable anchor while the provider is actively repainting.
115
+ initial_offset: int | None = None
112
116
 
113
117
 
114
118
  class LocalHistoryView:
@@ -353,6 +357,15 @@ class LocalHistoryView:
353
357
  del self.content_cache[next(iter(self.content_cache))]
354
358
  return stored
355
359
 
360
+ def _replace_content(self, snapshot: HistorySnapshot) -> HistorySnapshot:
361
+ """Install one verified contiguous capture without timeline merging."""
362
+ assert snapshot.pane_id is not None
363
+ self.content_cache.pop(snapshot.pane_id, None)
364
+ self.content_cache[snapshot.pane_id] = snapshot
365
+ while len(self.content_cache) > _HISTORY_CONTENT_PANES:
366
+ del self.content_cache[next(iter(self.content_cache))]
367
+ return snapshot
368
+
356
369
  def invalidate_routes(self) -> bool:
357
370
  """Drop pointer authority without discarding bounded pane content."""
358
371
  was_active = self.cancel()
@@ -470,29 +483,29 @@ class LocalHistoryView:
470
483
  cached = self.content_cache.get(route.pane_id, route)
471
484
  if not self._same_geometry(cached, route):
472
485
  cached = route
486
+ loaded_limit = min(len(cached.lines), self.history_limit)
487
+ target_lines = min(self.history_limit, _HISTORY_INITIAL_LINES)
488
+ if cached.more_available and loaded_limit < target_lines:
489
+ self.cancel_pane(route.pane_id)
490
+ request_id = self._allocate_request_id()
491
+ self._deep_pending[request_id] = _PendingHistory(
492
+ self.route_epoch,
493
+ route.pane_id,
494
+ target_lines,
495
+ time.monotonic() if now is None else now,
496
+ max(1, initial_offset),
497
+ )
498
+ self._deep_requests += 1
499
+ # Keep the live pane intact until one internally contiguous deep
500
+ # capture arrives. Entering history atomically from that response
501
+ # removes the hot/deep seam and needs no moving-live anchor.
502
+ return HistoryAction(
503
+ protocol_frame=encode_history_request(
504
+ request_id, event.x, event.y, target_lines
505
+ )
506
+ )
473
507
  maximum = max(0, len(cached.lines) - cached.height)
474
508
  if maximum == 0:
475
- if cached.more_available:
476
- self.cancel_pane(route.pane_id)
477
- target_lines = min(self.history_limit, _HISTORY_INITIAL_LINES)
478
- self.viewports[route.pane_id] = _HistoryViewport(
479
- cached,
480
- 0,
481
- min(len(cached.lines), self.history_limit),
482
- )
483
- request_id = self._allocate_request_id()
484
- self._deep_pending[request_id] = _PendingHistory(
485
- self.route_epoch,
486
- route.pane_id,
487
- target_lines,
488
- time.monotonic() if now is None else now,
489
- )
490
- self._deep_requests += 1
491
- return HistoryAction(
492
- protocol_frame=encode_history_request(
493
- request_id, event.x, event.y, target_lines
494
- )
495
- )
496
509
  if cached.transcript_backed:
497
510
  # Claude may not have written its first transcript record yet.
498
511
  # This is a normal transient state, not a Railmux status event:
@@ -503,7 +516,6 @@ class LocalHistoryView:
503
516
  forwarded_input=event.raw if cached.mouse_forwardable else b""
504
517
  )
505
518
  self.cancel_pane(route.pane_id)
506
- loaded_limit = min(len(cached.lines), self.history_limit)
507
519
  viewport = _HistoryViewport(
508
520
  cached,
509
521
  min(maximum, max(1, initial_offset)),
@@ -513,7 +525,6 @@ class LocalHistoryView:
513
525
  ),
514
526
  )
515
527
  self.viewports[route.pane_id] = viewport
516
- target_lines = min(self.history_limit, _HISTORY_INITIAL_LINES)
517
528
  if loaded_limit >= target_lines:
518
529
  return HistoryAction(
519
530
  protocol_frame=self._extend_history(viewport, now=now),
@@ -602,6 +613,45 @@ class LocalHistoryView:
602
613
  if direction > 0 and route.history_choice_required:
603
614
  return HistoryAction(claude_history_prompt=event.raw)
604
615
  viewport = self.viewports.get(route.pane_id)
616
+ initial_request = next(
617
+ (
618
+ (request_id, pending)
619
+ for request_id, pending in self._deep_pending.items()
620
+ if pending.pane_id == route.pane_id
621
+ and pending.initial_offset is not None
622
+ ),
623
+ None,
624
+ )
625
+ if initial_request is not None:
626
+ request_id, pending = initial_request
627
+ requested_at = time.monotonic() if now is None else now
628
+ if requested_at - pending.requested_at >= _HISTORY_DEEP_TIMEOUT:
629
+ self._deep_pending.pop(request_id, None)
630
+ self._timeouts += 1
631
+ if direction < 0:
632
+ return HistoryAction()
633
+ assert pending.initial_offset is not None
634
+ distance = min(
635
+ self.history_limit,
636
+ pending.initial_offset + max(1, distance),
637
+ )
638
+ initial_request = None
639
+ if viewport is None and initial_request is not None:
640
+ if direction < 0:
641
+ # The live pane never left the screen, so reversing direction
642
+ # simply cancels the pending entry. A late response is ignored.
643
+ self.cancel_pane(route.pane_id)
644
+ return HistoryAction()
645
+ request_id, pending = initial_request
646
+ assert pending.initial_offset is not None
647
+ self._deep_pending[request_id] = replace(
648
+ pending,
649
+ initial_offset=min(
650
+ self.history_limit,
651
+ pending.initial_offset + max(1, distance),
652
+ ),
653
+ )
654
+ return HistoryAction()
605
655
  if viewport is not None:
606
656
  maximum = max(0, len(viewport.snapshot.lines) - viewport.snapshot.height)
607
657
  viewport.offset = max(
@@ -828,8 +878,26 @@ class LocalHistoryView:
828
878
  return HistoryAction()
829
879
  viewport = self.viewports.get(pending.pane_id)
830
880
  if viewport is None:
831
- self._remember_content(snapshot)
832
- return HistoryAction()
881
+ if pending.initial_offset is None:
882
+ self._remember_content(snapshot)
883
+ return HistoryAction()
884
+ maximum = max(0, len(snapshot.lines) - snapshot.height)
885
+ if maximum == 0:
886
+ self._remember_content(snapshot)
887
+ return HistoryAction()
888
+ stored = self._replace_content(snapshot)
889
+ maximum = max(0, len(stored.lines) - stored.height)
890
+ viewport = _HistoryViewport(
891
+ stored,
892
+ min(maximum, pending.initial_offset),
893
+ min(self.history_limit, max(pending.target_lines, len(stored.lines))),
894
+ exhausted=not snapshot.more_available,
895
+ )
896
+ self.viewports[pending.pane_id] = viewport
897
+ return HistoryAction(
898
+ protocol_frame=self._extend_history(viewport),
899
+ render_history=True,
900
+ )
833
901
  maximum = max(0, len(snapshot.lines) - snapshot.height)
834
902
  if maximum == 0:
835
903
  self._remember_content(snapshot)
@@ -502,7 +502,6 @@ class SharedTmuxBindingManager:
502
502
  )
503
503
  break
504
504
  return False
505
- self._registered = True
506
505
  self._prefix_tab_managed = state["prefix_tab_managed"]
507
506
  self._right_click_managed = (
508
507
  state["right_click_managed"])
@@ -514,6 +513,10 @@ class SharedTmuxBindingManager:
514
513
  state["status_click_managed"])
515
514
  self._termux_tap_managed = (
516
515
  state["termux_tap_managed"])
516
+ # Publish registration only after every capability is
517
+ # final so concurrent readers cannot observe a torn
518
+ # lease during managed-Windows background setup.
519
+ self._registered = True
517
520
  return True
518
521
 
519
522
  backup = tmux_ctl.prepare_root_function_bindings()
@@ -662,13 +665,13 @@ class SharedTmuxBindingManager:
662
665
  state["termux_tap_backup"], token=token)
663
666
  self._remove_state()
664
667
  return False
665
- self._registered = True
666
668
  self._prefix_tab_managed = state["prefix_tab_managed"]
667
669
  self._right_click_managed = state["right_click_managed"]
668
670
  self._selection_hook_managed = state["selection_hook_managed"]
669
671
  self._selection_hook_index = state["selection_hook_index"]
670
672
  self._status_click_managed = state["status_click_managed"]
671
673
  self._termux_tap_managed = state["termux_tap_managed"]
674
+ self._registered = True
672
675
  return True
673
676
  except OSError:
674
677
  return False
@@ -8,7 +8,6 @@ from __future__ import annotations
8
8
 
9
9
  import os
10
10
  import re
11
- import signal
12
11
  import shlex
13
12
  import shutil
14
13
  import subprocess
@@ -1840,7 +1839,13 @@ RAILMUX_HISTORY_GENERATION_OPTION = "@railmux_history_generation_v1"
1840
1839
  # A confirmed branch transition prefixes the opaque SSH history cache epoch.
1841
1840
  # Plain generations use native pane history; only this explicit marker permits
1842
1841
  # the canonical transcript fallback that removes an abandoned rewind suffix.
1843
- RAILMUX_CANONICAL_HISTORY_PREFIX = "canonical:"
1842
+ # Only the evidence-gated v2 marker authorizes transcript-backed Codex
1843
+ # scrolling. The released v1 marker treated every live direct child rollout
1844
+ # as a rewind, but Codex can also create that shape while bootstrapping an
1845
+ # ordinary resume. Keeping the prefix versioned makes existing over-broad
1846
+ # markers fail back to raw pane history on upgrade.
1847
+ RAILMUX_CANONICAL_HISTORY_PREFIX = "canonical-v2:"
1848
+ RAILMUX_LEGACY_CANONICAL_HISTORY_PREFIX = "canonical:"
1844
1849
  # tmux names function keys only through F12. Private restore routes use
1845
1850
  # canonical xterm sequences in literal mode, so they cannot be confused with
1846
1851
  # user input forwarded to an agent pane.
@@ -3330,45 +3335,6 @@ def exact_pane_alive(identity: PaneIdentity) -> bool:
3330
3335
  )
3331
3336
 
3332
3337
 
3333
- def reset_pane_view(identity: PaneIdentity) -> bool:
3334
- """Reset visible terminal state without deleting retained scrollback.
3335
-
3336
- Revalidate the immutable session, pane process, and window before issuing
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.
3342
- """
3343
- current = pane_identity(identity.pane_id)
3344
- if (current is None or current.dead
3345
- or current.pane_pid != identity.pane_pid
3346
- or current.session_id != identity.session_id
3347
- or current.window_id != identity.window_id):
3348
- return False
3349
- try:
3350
- subprocess.check_call(
3351
- ["tmux", "send-keys", "-R", "-t", identity.pane_id],
3352
- stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
3353
- )
3354
- except (OSError, subprocess.CalledProcessError):
3355
- return False
3356
-
3357
- current = pane_identity(identity.pane_id)
3358
- if (current is None or current.dead
3359
- or current.pane_pid != identity.pane_pid
3360
- or current.session_id != identity.session_id
3361
- or current.window_id != identity.window_id):
3362
- return True
3363
- try:
3364
- os.killpg(os.getpgid(current.pane_pid), signal.SIGWINCH)
3365
- except (OSError, ValueError):
3366
- # The presentation cache is already clean. A provider that exited or
3367
- # cannot be signalled will repaint on its next ordinary terminal write.
3368
- pass
3369
- return True
3370
-
3371
-
3372
3338
  def kill_session_identity(identity: PaneIdentity) -> bool:
3373
3339
  """Kill an exact immutable tmux session, never a reused session name."""
3374
3340
  current = pane_identity(identity.pane_id)