railmux 0.3.6.dev1__tar.gz → 0.3.6.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 (163) hide show
  1. {railmux-0.3.6.dev1/src/railmux.egg-info → railmux-0.3.6.dev3}/PKG-INFO +6 -6
  2. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/README.md +5 -5
  3. railmux-0.3.6.dev3/src/railmux/__init__.py +1 -0
  4. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/fast_display_history.py +128 -73
  5. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/tmux_binding_manager.py +5 -2
  6. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/tmux_ctl.py +7 -41
  7. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/ui/app.py +244 -42
  8. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/ui/modals.py +81 -0
  9. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3/src/railmux.egg-info}/PKG-INFO +6 -6
  10. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_agent_workspace.py +1 -0
  11. railmux-0.3.6.dev3/tests/test_codex_lineage.py +561 -0
  12. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_fast_full_window.py +210 -53
  13. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_focus_highlight.py +6 -2
  14. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_modals.py +68 -0
  15. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_soft_quit.py +101 -1
  16. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_tmux_binding_manager.py +25 -0
  17. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_tmux_ctl.py +0 -24
  18. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/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.dev3}/LICENSE +0 -0
  22. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/MANIFEST.in +0 -0
  23. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/pyproject.toml +0 -0
  24. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/setup.cfg +0 -0
  25. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/__main__.py +0 -0
  26. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/atomic_file.py +0 -0
  27. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/background_index.py +0 -0
  28. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/cli.py +0 -0
  29. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/codex_index.py +0 -0
  30. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/config.py +0 -0
  31. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/config_cli.py +0 -0
  32. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/diagnostics.py +0 -0
  33. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/discovery.py +0 -0
  34. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/display_transport.py +0 -0
  35. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/fast_display_client.py +0 -0
  36. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/fast_display_input.py +0 -0
  37. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/fast_display_protocol.py +0 -0
  38. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/fast_display_server.py +0 -0
  39. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/favorites.py +0 -0
  40. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/fuzzy.py +0 -0
  41. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/help_workspace.py +0 -0
  42. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/launcher.py +0 -0
  43. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/legacy_sessions.py +0 -0
  44. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/local_clipboard.py +0 -0
  45. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/local_open.py +0 -0
  46. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/models.py +0 -0
  47. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/modes.py +0 -0
  48. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/mouse_manager.py +0 -0
  49. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/orphan_marker.py +0 -0
  50. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/pane_surface.py +0 -0
  51. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/path_codec.py +0 -0
  52. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/remote_config.py +0 -0
  53. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/renames.py +0 -0
  54. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/restart_state.py +0 -0
  55. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/runtime_config.py +0 -0
  56. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/scroll_agent.py +0 -0
  57. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/scroll_manager.py +0 -0
  58. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/selection_isolation.py +0 -0
  59. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/self_update.py +0 -0
  60. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/session_cache.py +0 -0
  61. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/session_index.py +0 -0
  62. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/session_lease.py +0 -0
  63. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/setting_contracts.py +0 -0
  64. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/settings.py +0 -0
  65. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/ssh_args.py +0 -0
  66. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/ssh_compat.py +0 -0
  67. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/ssh_display_diagnostics.py +0 -0
  68. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/ssh_doctor.py +0 -0
  69. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/system_deps.py +0 -0
  70. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/terminal_status.py +0 -0
  71. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/tmux_health.py +0 -0
  72. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/tmux_server.py +0 -0
  73. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/tool_panes.py +0 -0
  74. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/transcript.py +0 -0
  75. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/ui/__init__.py +0 -0
  76. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/ui/_widgets.py +0 -0
  77. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/ui/keymap.py +0 -0
  78. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/ui/projects_pane.py +0 -0
  79. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/ui/running_pane.py +0 -0
  80. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/ui/sessions_pane.py +0 -0
  81. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/ui/sidebar.py +0 -0
  82. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/ui/statusbar.py +0 -0
  83. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux/ui/workspace.py +0 -0
  84. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux.egg-info/SOURCES.txt +0 -0
  85. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux.egg-info/dependency_links.txt +0 -0
  86. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux.egg-info/entry_points.txt +0 -0
  87. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux.egg-info/requires.txt +0 -0
  88. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/src/railmux.egg-info/top_level.txt +0 -0
  89. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_atomic_file.py +0 -0
  90. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_background_index.py +0 -0
  91. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_cli.py +0 -0
  92. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_codex_delete.py +0 -0
  93. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_codex_index.py +0 -0
  94. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_codex_transcript.py +0 -0
  95. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_config.py +0 -0
  96. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_config_cli.py +0 -0
  97. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_denested_transport_benchmark.py +0 -0
  98. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_diagnostics.py +0 -0
  99. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_discovery.py +0 -0
  100. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_display_transport.py +0 -0
  101. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_fullscreen_binding.py +0 -0
  102. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_fuzzy.py +0 -0
  103. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_help_agent.py +0 -0
  104. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_help_workspace.py +0 -0
  105. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_history_cleanup.py +0 -0
  106. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_keymap.py +0 -0
  107. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_launcher.py +0 -0
  108. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_layout_profile.py +0 -0
  109. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_legacy_sessions.py +0 -0
  110. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_local_clipboard.py +0 -0
  111. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_local_open.py +0 -0
  112. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_mode_selection.py +0 -0
  113. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_modes.py +0 -0
  114. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_mouse_manager.py +0 -0
  115. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_new_project.py +0 -0
  116. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_options.py +0 -0
  117. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_orphan_recovery.py +0 -0
  118. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_pane_surface.py +0 -0
  119. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_path_codec.py +0 -0
  120. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_projects_pane.py +0 -0
  121. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_release_notes.py +0 -0
  122. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_release_workflow.py +0 -0
  123. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_remote_config.py +0 -0
  124. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_renames.py +0 -0
  125. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_restart_state.py +0 -0
  126. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_running_pane.py +0 -0
  127. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_running_sort.py +0 -0
  128. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_runtime_config.py +0 -0
  129. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_scroll_agent.py +0 -0
  130. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_scroll_manager.py +0 -0
  131. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_selection_isolation.py +0 -0
  132. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_self_update.py +0 -0
  133. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_session_cache.py +0 -0
  134. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_session_index.py +0 -0
  135. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_session_lease.py +0 -0
  136. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_sessions_pane.py +0 -0
  137. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_setting_contracts.py +0 -0
  138. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_settings.py +0 -0
  139. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_sidebar.py +0 -0
  140. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_ssh_args.py +0 -0
  141. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_ssh_compat.py +0 -0
  142. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_ssh_display_benchmark.py +0 -0
  143. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_ssh_display_diagnostics.py +0 -0
  144. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_ssh_doctor.py +0 -0
  145. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_status.py +0 -0
  146. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_statusbar.py +0 -0
  147. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_system_deps.py +0 -0
  148. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_terminal_size.py +0 -0
  149. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_terminal_status.py +0 -0
  150. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_termux_local_touch.py +0 -0
  151. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_tmux_health.py +0 -0
  152. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_tmux_server.py +0 -0
  153. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_tmux_status.py +0 -0
  154. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_tool_panes.py +0 -0
  155. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_transcript.py +0 -0
  156. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_utf8_state.py +0 -0
  157. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_web_demo_recorder.py +0 -0
  158. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_wheel_smoke.py +0 -0
  159. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_widgets.py +0 -0
  160. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tests/test_yolo_prompt.py +0 -0
  161. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tools/denested_transport_benchmark.py +0 -0
  162. {railmux-0.3.6.dev1 → railmux-0.3.6.dev3}/tools/release_notes.py +0 -0
  163. {railmux-0.3.6.dev1 → railmux-0.3.6.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.6.dev1
3
+ Version: 0.3.6.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
@@ -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.dev3"
@@ -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:
@@ -266,8 +270,6 @@ class LocalHistoryView:
266
270
  self,
267
271
  previous: HistorySnapshot,
268
272
  incoming: HistorySnapshot,
269
- *,
270
- prefer_deeper: bool = False,
271
273
  ) -> HistorySnapshot:
272
274
  """Retain already-fetched history when a later hot capture shrinks."""
273
275
  if not self._same_geometry(
@@ -276,28 +278,13 @@ class LocalHistoryView:
276
278
  return incoming
277
279
  delta = self._timeline_delta(previous.lines, incoming.lines)
278
280
  if delta is None:
279
- if prefer_deeper and len(incoming.lines) > len(previous.lines):
280
- # A deep response reaches here only after accept() proved that
281
- # the user's immutable frozen viewport still exists in it.
282
- # Rewind/full-screen diff output can make every individual
283
- # line in that viewport non-unique, leaving the generic
284
- # timeline voter without two anchors. The larger cumulative
285
- # capture is nevertheless stronger than the 300-line hot
286
- # cache; keeping only its live tail would silently discard all
287
- # intermediate output above the current screen.
288
- lines = incoming.lines
289
- else:
290
- # Never splice two captures whose timelines cannot be aligned.
291
- # Retaining old history and replacing only the live viewport
292
- # looked conservative, but it silently omitted every row
293
- # produced between those two pieces. That false seam appeared
294
- # as discontinuous SSH history until returning to the bottom
295
- # forced a fresh deep capture. Keep the newest internally
296
- # contiguous hot capture instead; the first wheel-up requests
297
- # a cumulative deep page and can recover older rows safely.
298
- # An already-frozen viewport owns its immutable snapshot and
299
- # therefore remains undisturbed by this cache replacement.
300
- lines = incoming.lines
281
+ # Never splice two captures whose timelines cannot be aligned.
282
+ # Retaining old history and replacing only the live viewport
283
+ # looked conservative, but it silently omitted every row produced
284
+ # between those two pieces. Keep the newest internally contiguous
285
+ # capture instead. An already-frozen viewport owns its immutable
286
+ # snapshot and therefore remains undisturbed by cache replacement.
287
+ lines = incoming.lines
301
288
  else:
302
289
  start = min(0, delta)
303
290
  incoming_end = delta + len(incoming.lines)
@@ -325,8 +312,6 @@ class LocalHistoryView:
325
312
  def _remember_content(
326
313
  self,
327
314
  snapshot: HistorySnapshot,
328
- *,
329
- prefer_deeper: bool = False,
330
315
  ) -> HistorySnapshot:
331
316
  assert snapshot.pane_id is not None
332
317
  previous = self.content_cache.get(snapshot.pane_id)
@@ -344,8 +329,7 @@ class LocalHistoryView:
344
329
  stored = self._merge_content(previous, snapshot) if anchored else snapshot
345
330
  self._unverified_after_reconnect.discard(snapshot.pane_id)
346
331
  else:
347
- stored = self._merge_content(
348
- previous, snapshot, prefer_deeper=prefer_deeper)
332
+ stored = self._merge_content(previous, snapshot)
349
333
  # Reinsert an existing pane to keep insertion order as recency order.
350
334
  self.content_cache.pop(snapshot.pane_id, None)
351
335
  self.content_cache[snapshot.pane_id] = stored
@@ -353,6 +337,15 @@ class LocalHistoryView:
353
337
  del self.content_cache[next(iter(self.content_cache))]
354
338
  return stored
355
339
 
340
+ def _replace_content(self, snapshot: HistorySnapshot) -> HistorySnapshot:
341
+ """Install one verified contiguous capture without timeline merging."""
342
+ assert snapshot.pane_id is not None
343
+ self.content_cache.pop(snapshot.pane_id, None)
344
+ self.content_cache[snapshot.pane_id] = snapshot
345
+ while len(self.content_cache) > _HISTORY_CONTENT_PANES:
346
+ del self.content_cache[next(iter(self.content_cache))]
347
+ return snapshot
348
+
356
349
  def invalidate_routes(self) -> bool:
357
350
  """Drop pointer authority without discarding bounded pane content."""
358
351
  was_active = self.cancel()
@@ -470,29 +463,32 @@ class LocalHistoryView:
470
463
  cached = self.content_cache.get(route.pane_id, route)
471
464
  if not self._same_geometry(cached, route):
472
465
  cached = route
466
+ loaded_limit = min(len(cached.lines), self.history_limit)
467
+ target_lines = min(self.history_limit, _HISTORY_INITIAL_LINES)
468
+ if (
469
+ cached.more_available
470
+ and loaded_limit < min(target_lines, _HISTORY_PREFETCH_LINES)
471
+ ):
472
+ self.cancel_pane(route.pane_id)
473
+ request_id = self._allocate_request_id()
474
+ self._deep_pending[request_id] = _PendingHistory(
475
+ self.route_epoch,
476
+ route.pane_id,
477
+ target_lines,
478
+ time.monotonic() if now is None else now,
479
+ max(1, initial_offset),
480
+ )
481
+ self._deep_requests += 1
482
+ # A byte-budgeted or older peer may return less than the normal
483
+ # coherent routing capture. Keep the live pane intact until one
484
+ # cumulative response arrives rather than expose a short suffix.
485
+ return HistoryAction(
486
+ protocol_frame=encode_history_request(
487
+ request_id, event.x, event.y, target_lines
488
+ )
489
+ )
473
490
  maximum = max(0, len(cached.lines) - cached.height)
474
491
  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
492
  if cached.transcript_backed:
497
493
  # Claude may not have written its first transcript record yet.
498
494
  # This is a normal transient state, not a Railmux status event:
@@ -503,7 +499,6 @@ class LocalHistoryView:
503
499
  forwarded_input=event.raw if cached.mouse_forwardable else b""
504
500
  )
505
501
  self.cancel_pane(route.pane_id)
506
- loaded_limit = min(len(cached.lines), self.history_limit)
507
502
  viewport = _HistoryViewport(
508
503
  cached,
509
504
  min(maximum, max(1, initial_offset)),
@@ -513,23 +508,15 @@ class LocalHistoryView:
513
508
  ),
514
509
  )
515
510
  self.viewports[route.pane_id] = viewport
516
- target_lines = min(self.history_limit, _HISTORY_INITIAL_LINES)
517
- if loaded_limit >= target_lines:
518
- return HistoryAction(
519
- protocol_frame=self._extend_history(viewport, now=now),
520
- render_history=True,
521
- )
522
- request_id = self._allocate_request_id()
523
- self._deep_pending[request_id] = _PendingHistory(
524
- self.route_epoch,
525
- route.pane_id,
526
- target_lines,
527
- time.monotonic() if now is None else now,
528
- )
529
- self._deep_requests += 1
511
+ # A complete 300-row routing capture is already one coherent history
512
+ # source. Paint it synchronously, then fetch a cumulative page only as
513
+ # the viewport approaches its top. This removes an SSH round trip from
514
+ # the first wheel tick without reintroducing a hot/deep splice.
530
515
  return HistoryAction(
531
- protocol_frame=encode_history_request(
532
- request_id, event.x, event.y, target_lines
516
+ protocol_frame=self._extend_history(
517
+ viewport,
518
+ now=now,
519
+ request_position=(event.x, event.y),
533
520
  ),
534
521
  render_history=True,
535
522
  )
@@ -539,6 +526,7 @@ class LocalHistoryView:
539
526
  viewport: _HistoryViewport,
540
527
  *,
541
528
  now: float | None = None,
529
+ request_position: tuple[int, int] | None = None,
542
530
  ) -> bytes:
543
531
  """Request the next cumulative page when a viewport nears its top."""
544
532
  snapshot = viewport.snapshot
@@ -572,7 +560,9 @@ class LocalHistoryView:
572
560
  return b""
573
561
  target_lines = min(
574
562
  self.history_limit,
575
- max(_HISTORY_INITIAL_LINES, viewport.loaded_limit) + _HISTORY_PAGE_LINES,
563
+ _HISTORY_INITIAL_LINES
564
+ if viewport.loaded_limit < _HISTORY_INITIAL_LINES
565
+ else viewport.loaded_limit + _HISTORY_PAGE_LINES,
576
566
  )
577
567
  request_id = self._allocate_request_id()
578
568
  self._deep_pending[request_id] = _PendingHistory(
@@ -582,10 +572,14 @@ class LocalHistoryView:
582
572
  requested_at,
583
573
  )
584
574
  self._deep_requests += 1
585
- return encode_history_request(
586
- request_id,
575
+ request_x, request_y = request_position or (
587
576
  snapshot.x + 1,
588
577
  snapshot.y + 1,
578
+ )
579
+ return encode_history_request(
580
+ request_id,
581
+ request_x,
582
+ request_y,
589
583
  target_lines,
590
584
  )
591
585
 
@@ -602,6 +596,45 @@ class LocalHistoryView:
602
596
  if direction > 0 and route.history_choice_required:
603
597
  return HistoryAction(claude_history_prompt=event.raw)
604
598
  viewport = self.viewports.get(route.pane_id)
599
+ initial_request = next(
600
+ (
601
+ (request_id, pending)
602
+ for request_id, pending in self._deep_pending.items()
603
+ if pending.pane_id == route.pane_id
604
+ and pending.initial_offset is not None
605
+ ),
606
+ None,
607
+ )
608
+ if initial_request is not None:
609
+ request_id, pending = initial_request
610
+ requested_at = time.monotonic() if now is None else now
611
+ if requested_at - pending.requested_at >= _HISTORY_DEEP_TIMEOUT:
612
+ self._deep_pending.pop(request_id, None)
613
+ self._timeouts += 1
614
+ if direction < 0:
615
+ return HistoryAction()
616
+ assert pending.initial_offset is not None
617
+ distance = min(
618
+ self.history_limit,
619
+ pending.initial_offset + max(1, distance),
620
+ )
621
+ initial_request = None
622
+ if viewport is None and initial_request is not None:
623
+ if direction < 0:
624
+ # The live pane never left the screen, so reversing direction
625
+ # simply cancels the pending entry. A late response is ignored.
626
+ self.cancel_pane(route.pane_id)
627
+ return HistoryAction()
628
+ request_id, pending = initial_request
629
+ assert pending.initial_offset is not None
630
+ self._deep_pending[request_id] = replace(
631
+ pending,
632
+ initial_offset=min(
633
+ self.history_limit,
634
+ pending.initial_offset + max(1, distance),
635
+ ),
636
+ )
637
+ return HistoryAction()
605
638
  if viewport is not None:
606
639
  maximum = max(0, len(viewport.snapshot.lines) - viewport.snapshot.height)
607
640
  viewport.offset = max(
@@ -828,8 +861,26 @@ class LocalHistoryView:
828
861
  return HistoryAction()
829
862
  viewport = self.viewports.get(pending.pane_id)
830
863
  if viewport is None:
831
- self._remember_content(snapshot)
832
- return HistoryAction()
864
+ if pending.initial_offset is None:
865
+ self._remember_content(snapshot)
866
+ return HistoryAction()
867
+ maximum = max(0, len(snapshot.lines) - snapshot.height)
868
+ if maximum == 0:
869
+ self._remember_content(snapshot)
870
+ return HistoryAction()
871
+ stored = self._replace_content(snapshot)
872
+ maximum = max(0, len(stored.lines) - stored.height)
873
+ viewport = _HistoryViewport(
874
+ stored,
875
+ min(maximum, pending.initial_offset),
876
+ min(self.history_limit, max(pending.target_lines, len(stored.lines))),
877
+ exhausted=not snapshot.more_available,
878
+ )
879
+ self.viewports[pending.pane_id] = viewport
880
+ return HistoryAction(
881
+ protocol_frame=self._extend_history(viewport),
882
+ render_history=True,
883
+ )
833
884
  maximum = max(0, len(snapshot.lines) - snapshot.height)
834
885
  if maximum == 0:
835
886
  self._remember_content(snapshot)
@@ -843,7 +894,11 @@ class LocalHistoryView:
843
894
  # snapshot instead of jumping to newer or unrelated text.
844
895
  self._anchor_rejects += 1
845
896
  return HistoryAction()
846
- stored = self._remember_content(snapshot, prefer_deeper=True)
897
+ # The response is one cumulative capture and its exact visible anchor
898
+ # was just proved above. Replace the cached timeline as a whole instead
899
+ # of merging it with the older hot generation; this keeps every row's
900
+ # text and background styling from one server render.
901
+ stored = self._replace_content(snapshot)
847
902
  stored_offset = self._aligned_offset(stored, anchor)
848
903
  if stored_offset is None:
849
904
  # The raw response was valid, so this can only be an ambiguous
@@ -892,7 +947,7 @@ class LocalHistoryView:
892
947
  return matched_offset
893
948
 
894
949
  # Agent status rows (for example a Codex spinner) can change between
895
- # the 300-line hot snapshot and the first deep response. Requiring the
950
+ # the hot snapshot and a deep response. Requiring the
896
951
  # entire visible viewport to remain byte-identical then strands the
897
952
  # user at the hot-cache boundary. Align on a majority of stable,
898
953
  # non-blank lines instead, but only when each evidence line occurs
@@ -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)