railmux 0.3.6.dev2__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 (162) hide show
  1. {railmux-0.3.6.dev2/src/railmux.egg-info → railmux-0.3.6.dev3}/PKG-INFO +1 -1
  2. railmux-0.3.6.dev3/src/railmux/__init__.py +1 -0
  3. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/fast_display_history.py +39 -52
  4. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3/src/railmux.egg-info}/PKG-INFO +1 -1
  5. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_fast_full_window.py +38 -15
  6. railmux-0.3.6.dev2/src/railmux/__init__.py +0 -1
  7. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/LICENSE +0 -0
  8. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/MANIFEST.in +0 -0
  9. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/README.md +0 -0
  10. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/pyproject.toml +0 -0
  11. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/setup.cfg +0 -0
  12. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/__main__.py +0 -0
  13. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/atomic_file.py +0 -0
  14. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/background_index.py +0 -0
  15. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/cli.py +0 -0
  16. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/codex_index.py +0 -0
  17. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/config.py +0 -0
  18. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/config_cli.py +0 -0
  19. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/diagnostics.py +0 -0
  20. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/discovery.py +0 -0
  21. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/display_transport.py +0 -0
  22. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/fast_display_client.py +0 -0
  23. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/fast_display_input.py +0 -0
  24. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/fast_display_protocol.py +0 -0
  25. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/fast_display_server.py +0 -0
  26. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/favorites.py +0 -0
  27. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/fuzzy.py +0 -0
  28. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/help_workspace.py +0 -0
  29. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/launcher.py +0 -0
  30. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/legacy_sessions.py +0 -0
  31. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/local_clipboard.py +0 -0
  32. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/local_open.py +0 -0
  33. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/models.py +0 -0
  34. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/modes.py +0 -0
  35. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/mouse_manager.py +0 -0
  36. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/orphan_marker.py +0 -0
  37. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/pane_surface.py +0 -0
  38. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/path_codec.py +0 -0
  39. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/remote_config.py +0 -0
  40. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/renames.py +0 -0
  41. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/restart_state.py +0 -0
  42. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/runtime_config.py +0 -0
  43. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/scroll_agent.py +0 -0
  44. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/scroll_manager.py +0 -0
  45. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/selection_isolation.py +0 -0
  46. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/self_update.py +0 -0
  47. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/session_cache.py +0 -0
  48. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/session_index.py +0 -0
  49. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/session_lease.py +0 -0
  50. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/setting_contracts.py +0 -0
  51. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/settings.py +0 -0
  52. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/ssh_args.py +0 -0
  53. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/ssh_compat.py +0 -0
  54. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/ssh_display_diagnostics.py +0 -0
  55. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/ssh_doctor.py +0 -0
  56. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/system_deps.py +0 -0
  57. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/terminal_status.py +0 -0
  58. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/tmux_binding_manager.py +0 -0
  59. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/tmux_ctl.py +0 -0
  60. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/tmux_health.py +0 -0
  61. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/tmux_server.py +0 -0
  62. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/tool_panes.py +0 -0
  63. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/transcript.py +0 -0
  64. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/ui/__init__.py +0 -0
  65. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/ui/_widgets.py +0 -0
  66. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/ui/app.py +0 -0
  67. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/ui/keymap.py +0 -0
  68. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/ui/modals.py +0 -0
  69. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/ui/projects_pane.py +0 -0
  70. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/ui/running_pane.py +0 -0
  71. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/ui/sessions_pane.py +0 -0
  72. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/ui/sidebar.py +0 -0
  73. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/ui/statusbar.py +0 -0
  74. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux/ui/workspace.py +0 -0
  75. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux.egg-info/SOURCES.txt +0 -0
  76. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux.egg-info/dependency_links.txt +0 -0
  77. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux.egg-info/entry_points.txt +0 -0
  78. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux.egg-info/requires.txt +0 -0
  79. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/src/railmux.egg-info/top_level.txt +0 -0
  80. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_agent_workspace.py +0 -0
  81. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_atomic_file.py +0 -0
  82. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_background_index.py +0 -0
  83. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_cli.py +0 -0
  84. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_codex_delete.py +0 -0
  85. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_codex_index.py +0 -0
  86. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_codex_lineage.py +0 -0
  87. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_codex_transcript.py +0 -0
  88. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_config.py +0 -0
  89. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_config_cli.py +0 -0
  90. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_denested_transport_benchmark.py +0 -0
  91. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_diagnostics.py +0 -0
  92. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_discovery.py +0 -0
  93. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_display_transport.py +0 -0
  94. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_focus_highlight.py +0 -0
  95. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_fullscreen_binding.py +0 -0
  96. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_fuzzy.py +0 -0
  97. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_help_agent.py +0 -0
  98. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_help_workspace.py +0 -0
  99. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_history_cleanup.py +0 -0
  100. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_keymap.py +0 -0
  101. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_launcher.py +0 -0
  102. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_layout_profile.py +0 -0
  103. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_legacy_sessions.py +0 -0
  104. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_local_clipboard.py +0 -0
  105. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_local_open.py +0 -0
  106. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_modals.py +0 -0
  107. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_mode_selection.py +0 -0
  108. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_modes.py +0 -0
  109. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_mouse_manager.py +0 -0
  110. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_new_project.py +0 -0
  111. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_options.py +0 -0
  112. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_orphan_recovery.py +0 -0
  113. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_pane_surface.py +0 -0
  114. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_path_codec.py +0 -0
  115. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_projects_pane.py +0 -0
  116. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_release_notes.py +0 -0
  117. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_release_workflow.py +0 -0
  118. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_remote_config.py +0 -0
  119. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_renames.py +0 -0
  120. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_restart_state.py +0 -0
  121. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_running_pane.py +0 -0
  122. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_running_sort.py +0 -0
  123. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_runtime_config.py +0 -0
  124. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_scroll_agent.py +0 -0
  125. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_scroll_manager.py +0 -0
  126. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_selection_isolation.py +0 -0
  127. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_self_update.py +0 -0
  128. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_session_cache.py +0 -0
  129. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_session_index.py +0 -0
  130. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_session_lease.py +0 -0
  131. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_sessions_pane.py +0 -0
  132. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_setting_contracts.py +0 -0
  133. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_settings.py +0 -0
  134. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_sidebar.py +0 -0
  135. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_soft_quit.py +0 -0
  136. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_ssh_args.py +0 -0
  137. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_ssh_compat.py +0 -0
  138. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_ssh_display_benchmark.py +0 -0
  139. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_ssh_display_diagnostics.py +0 -0
  140. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_ssh_doctor.py +0 -0
  141. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_status.py +0 -0
  142. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_statusbar.py +0 -0
  143. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_system_deps.py +0 -0
  144. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_terminal_size.py +0 -0
  145. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_terminal_status.py +0 -0
  146. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_termux_local_touch.py +0 -0
  147. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_tmux_binding_manager.py +0 -0
  148. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_tmux_ctl.py +0 -0
  149. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_tmux_health.py +0 -0
  150. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_tmux_integration.py +0 -0
  151. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_tmux_server.py +0 -0
  152. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_tmux_status.py +0 -0
  153. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_tool_panes.py +0 -0
  154. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_transcript.py +0 -0
  155. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_utf8_state.py +0 -0
  156. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_web_demo_recorder.py +0 -0
  157. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_wheel_smoke.py +0 -0
  158. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_widgets.py +0 -0
  159. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tests/test_yolo_prompt.py +0 -0
  160. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tools/denested_transport_benchmark.py +0 -0
  161. {railmux-0.3.6.dev2 → railmux-0.3.6.dev3}/tools/release_notes.py +0 -0
  162. {railmux-0.3.6.dev2 → 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.dev2
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
@@ -0,0 +1 @@
1
+ __version__ = "0.3.6.dev3"
@@ -270,8 +270,6 @@ class LocalHistoryView:
270
270
  self,
271
271
  previous: HistorySnapshot,
272
272
  incoming: HistorySnapshot,
273
- *,
274
- prefer_deeper: bool = False,
275
273
  ) -> HistorySnapshot:
276
274
  """Retain already-fetched history when a later hot capture shrinks."""
277
275
  if not self._same_geometry(
@@ -280,28 +278,13 @@ class LocalHistoryView:
280
278
  return incoming
281
279
  delta = self._timeline_delta(previous.lines, incoming.lines)
282
280
  if delta is None:
283
- if prefer_deeper and len(incoming.lines) > len(previous.lines):
284
- # A deep response reaches here only after accept() proved that
285
- # the user's immutable frozen viewport still exists in it.
286
- # Rewind/full-screen diff output can make every individual
287
- # line in that viewport non-unique, leaving the generic
288
- # timeline voter without two anchors. The larger cumulative
289
- # capture is nevertheless stronger than the 300-line hot
290
- # cache; keeping only its live tail would silently discard all
291
- # intermediate output above the current screen.
292
- lines = incoming.lines
293
- else:
294
- # Never splice two captures whose timelines cannot be aligned.
295
- # Retaining old history and replacing only the live viewport
296
- # looked conservative, but it silently omitted every row
297
- # produced between those two pieces. That false seam appeared
298
- # as discontinuous SSH history until returning to the bottom
299
- # forced a fresh deep capture. Keep the newest internally
300
- # contiguous hot capture instead; the first wheel-up requests
301
- # a cumulative deep page and can recover older rows safely.
302
- # An already-frozen viewport owns its immutable snapshot and
303
- # therefore remains undisturbed by this cache replacement.
304
- 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
305
288
  else:
306
289
  start = min(0, delta)
307
290
  incoming_end = delta + len(incoming.lines)
@@ -329,8 +312,6 @@ class LocalHistoryView:
329
312
  def _remember_content(
330
313
  self,
331
314
  snapshot: HistorySnapshot,
332
- *,
333
- prefer_deeper: bool = False,
334
315
  ) -> HistorySnapshot:
335
316
  assert snapshot.pane_id is not None
336
317
  previous = self.content_cache.get(snapshot.pane_id)
@@ -348,8 +329,7 @@ class LocalHistoryView:
348
329
  stored = self._merge_content(previous, snapshot) if anchored else snapshot
349
330
  self._unverified_after_reconnect.discard(snapshot.pane_id)
350
331
  else:
351
- stored = self._merge_content(
352
- previous, snapshot, prefer_deeper=prefer_deeper)
332
+ stored = self._merge_content(previous, snapshot)
353
333
  # Reinsert an existing pane to keep insertion order as recency order.
354
334
  self.content_cache.pop(snapshot.pane_id, None)
355
335
  self.content_cache[snapshot.pane_id] = stored
@@ -485,7 +465,10 @@ class LocalHistoryView:
485
465
  cached = route
486
466
  loaded_limit = min(len(cached.lines), self.history_limit)
487
467
  target_lines = min(self.history_limit, _HISTORY_INITIAL_LINES)
488
- if cached.more_available and loaded_limit < target_lines:
468
+ if (
469
+ cached.more_available
470
+ and loaded_limit < min(target_lines, _HISTORY_PREFETCH_LINES)
471
+ ):
489
472
  self.cancel_pane(route.pane_id)
490
473
  request_id = self._allocate_request_id()
491
474
  self._deep_pending[request_id] = _PendingHistory(
@@ -496,9 +479,9 @@ class LocalHistoryView:
496
479
  max(1, initial_offset),
497
480
  )
498
481
  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.
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.
502
485
  return HistoryAction(
503
486
  protocol_frame=encode_history_request(
504
487
  request_id, event.x, event.y, target_lines
@@ -525,22 +508,15 @@ class LocalHistoryView:
525
508
  ),
526
509
  )
527
510
  self.viewports[route.pane_id] = viewport
528
- if loaded_limit >= target_lines:
529
- return HistoryAction(
530
- protocol_frame=self._extend_history(viewport, now=now),
531
- render_history=True,
532
- )
533
- request_id = self._allocate_request_id()
534
- self._deep_pending[request_id] = _PendingHistory(
535
- self.route_epoch,
536
- route.pane_id,
537
- target_lines,
538
- time.monotonic() if now is None else now,
539
- )
540
- 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.
541
515
  return HistoryAction(
542
- protocol_frame=encode_history_request(
543
- 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),
544
520
  ),
545
521
  render_history=True,
546
522
  )
@@ -550,6 +526,7 @@ class LocalHistoryView:
550
526
  viewport: _HistoryViewport,
551
527
  *,
552
528
  now: float | None = None,
529
+ request_position: tuple[int, int] | None = None,
553
530
  ) -> bytes:
554
531
  """Request the next cumulative page when a viewport nears its top."""
555
532
  snapshot = viewport.snapshot
@@ -583,7 +560,9 @@ class LocalHistoryView:
583
560
  return b""
584
561
  target_lines = min(
585
562
  self.history_limit,
586
- 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,
587
566
  )
588
567
  request_id = self._allocate_request_id()
589
568
  self._deep_pending[request_id] = _PendingHistory(
@@ -593,10 +572,14 @@ class LocalHistoryView:
593
572
  requested_at,
594
573
  )
595
574
  self._deep_requests += 1
596
- return encode_history_request(
597
- request_id,
575
+ request_x, request_y = request_position or (
598
576
  snapshot.x + 1,
599
577
  snapshot.y + 1,
578
+ )
579
+ return encode_history_request(
580
+ request_id,
581
+ request_x,
582
+ request_y,
600
583
  target_lines,
601
584
  )
602
585
 
@@ -911,7 +894,11 @@ class LocalHistoryView:
911
894
  # snapshot instead of jumping to newer or unrelated text.
912
895
  self._anchor_rejects += 1
913
896
  return HistoryAction()
914
- 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)
915
902
  stored_offset = self._aligned_offset(stored, anchor)
916
903
  if stored_offset is None:
917
904
  # The raw response was valid, so this can only be an ambiguous
@@ -960,7 +947,7 @@ class LocalHistoryView:
960
947
  return matched_offset
961
948
 
962
949
  # Agent status rows (for example a Codex spinner) can change between
963
- # the 300-line hot snapshot and the first deep response. Requiring the
950
+ # the hot snapshot and a deep response. Requiring the
964
951
  # entire visible viewport to remain byte-identical then strands the
965
952
  # user at the hot-cache boundary. Align on a majority of stable,
966
953
  # non-blank lines instead, but only when each evidence line occurs
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.3.6.dev2
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
@@ -2707,7 +2707,7 @@ def test_forced_policy_prefetch_preserves_other_pane_history_position():
2707
2707
  assert view.viewports["%7"].offset == 1
2708
2708
 
2709
2709
 
2710
- def test_local_history_requests_deep_page_when_hot_viewport_fills_prefetch():
2710
+ def test_local_history_enters_warm_cache_immediately_then_prefetches_near_top():
2711
2711
  view = LocalHistoryView()
2712
2712
  prefetch = InputFrameDecoder().feed(view.begin_prefetch(1.0))[0]
2713
2713
  request_id, _limit = decode_history_prefetch(prefetch.data)
@@ -2717,17 +2717,26 @@ def test_local_history_requests_deep_page_when_hot_viewport_fills_prefetch():
2717
2717
  0,
2718
2718
  0,
2719
2719
  80,
2720
- 300,
2721
- (b"live",) * 300,
2720
+ 30,
2721
+ tuple(f"line-{index}".encode() for index in range(300)),
2722
2722
  more_available=True,
2723
2723
  )
2724
2724
  view.accept_prefetch(HistoryBatch(request_id, (route,)))
2725
2725
 
2726
- action = view.wheel(SgrMouseEvent(b"up", 64, 40, 2, True))
2727
- request = InputFrameDecoder().feed(action.protocol_frame)[0]
2726
+ wheel = SgrMouseEvent(b"up", 64, 40, 2, True)
2727
+ action = view.wheel(wheel)
2728
2728
 
2729
+ assert action.render_history is True
2730
+ assert action.protocol_frame == b""
2731
+ assert view.overlays()[0][1][-1] == b"line-298"
2732
+
2733
+ request_action = HistoryAction()
2734
+ for _ in range(200):
2735
+ request_action = view.wheel(wheel)
2736
+ if request_action.protocol_frame:
2737
+ break
2738
+ request = InputFrameDecoder().feed(request_action.protocol_frame)[0]
2729
2739
  assert decode_history_request(request.data)[3] == 2000
2730
- assert action.forwarded_input == b""
2731
2740
 
2732
2741
 
2733
2742
  def test_local_history_keeps_empty_plain_agent_wheel_from_tmux_copy_mode():
@@ -3594,7 +3603,7 @@ def test_unaligned_hot_prefetch_never_creates_a_discontinuous_history_seam():
3594
3603
  0,
3595
3604
  30,
3596
3605
  3,
3597
- tuple(f"old-{index}".encode() for index in range(300)),
3606
+ tuple(f"old-{index}".encode() for index in range(200)),
3598
3607
  more_available=True,
3599
3608
  )
3600
3609
  view.accept_prefetch(HistoryBatch(first_id, (old,)))
@@ -3608,14 +3617,14 @@ def test_unaligned_hot_prefetch_never_creates_a_discontinuous_history_seam():
3608
3617
  0,
3609
3618
  30,
3610
3619
  3,
3611
- tuple(f"new-{index}".encode() for index in range(300)),
3620
+ tuple(f"new-{index}".encode() for index in range(200)),
3612
3621
  more_available=True,
3613
3622
  )
3614
3623
  view.accept_prefetch(HistoryBatch(second_id, (current,)))
3615
3624
 
3616
3625
  wheel = view.wheel(SgrMouseEvent(b"up", 64, 40, 2, True))
3617
3626
 
3618
- # Do not briefly expose the 300-line hot suffix. It has no anchor to the
3627
+ # Do not expose a short/incomplete hot suffix. It has no anchor to the
3619
3628
  # previous capture and an active provider may repaint again before the deep
3620
3629
  # response, which used to leave a visible gap until returning to bottom.
3621
3630
  assert not wheel.render_history
@@ -3635,14 +3644,14 @@ def test_unaligned_hot_prefetch_never_creates_a_discontinuous_history_seam():
3635
3644
  0,
3636
3645
  30,
3637
3646
  3,
3638
- tuple(f"deep-{index}".encode() for index in range(1700)) + current.lines,
3647
+ tuple(f"deep-{index}".encode() for index in range(1800)) + current.lines,
3639
3648
  )
3640
3649
  accepted = view.accept(deep)
3641
3650
 
3642
3651
  assert accepted.render_history
3643
3652
  assert view.viewports["%8"].snapshot.lines == deep.lines
3644
- assert view.overlays()[0][1] == (b"new-295", b"new-296", b"new-297")
3645
- assert b"old-296" not in view.viewports["%8"].snapshot.lines
3653
+ assert view.overlays()[0][1] == (b"new-195", b"new-196", b"new-197")
3654
+ assert b"old-196" not in view.viewports["%8"].snapshot.lines
3646
3655
 
3647
3656
 
3648
3657
  def test_initial_deep_history_retry_and_wheel_down_are_bounded():
@@ -3654,7 +3663,7 @@ def test_initial_deep_history_retry_and_wheel_down_are_bounded():
3654
3663
  0,
3655
3664
  30,
3656
3665
  3,
3657
- tuple(f"line-{index}".encode() for index in range(300)),
3666
+ tuple(f"line-{index}".encode() for index in range(200)),
3658
3667
  more_available=True,
3659
3668
  )
3660
3669
  view.visible_routes = (route,)
@@ -3740,6 +3749,12 @@ def test_periodic_hot_prefetch_does_not_shrink_a_reusable_deep_snapshot():
3740
3749
  view.accept_prefetch(HistoryBatch(first_id, (hot,)))
3741
3750
  wheel = SgrMouseEvent(b"up", 64, 40, 2, True)
3742
3751
  deep_action = view.wheel(wheel)
3752
+ assert deep_action.render_history is True
3753
+ assert deep_action.protocol_frame == b""
3754
+ for _ in range(300):
3755
+ deep_action = view.wheel(wheel)
3756
+ if deep_action.protocol_frame:
3757
+ break
3743
3758
  deep_id = decode_history_request(
3744
3759
  InputFrameDecoder().feed(deep_action.protocol_frame)[0].data
3745
3760
  )[0]
@@ -3788,10 +3803,11 @@ def test_validated_deep_capture_recovers_unanchored_rewind_output():
3788
3803
  first_id, _limit = decode_history_prefetch(first.data)
3789
3804
  repeated = b"\033[48;2;33;58;43mrewind-row"
3790
3805
  hot_lines = (
3791
- *(f"old-{index}".encode() for index in range(296)),
3806
+ *(f"old-{index}".encode() for index in range(120)),
3792
3807
  repeated,
3793
3808
  repeated,
3794
3809
  repeated,
3810
+ *(f"new-{index}".encode() for index in range(176)),
3795
3811
  b"live-bottom",
3796
3812
  )
3797
3813
  hot = HistorySnapshot(
@@ -3805,7 +3821,14 @@ def test_validated_deep_capture_recovers_unanchored_rewind_output():
3805
3821
  more_available=True,
3806
3822
  )
3807
3823
  view.accept_prefetch(HistoryBatch(first_id, (hot,)))
3808
- wheel = view.wheel(SgrMouseEvent(b"up", 64, 40, 2, True))
3824
+ wheel_up = SgrMouseEvent(b"up", 64, 40, 2, True)
3825
+ wheel = view.wheel(wheel_up)
3826
+ assert wheel.render_history is True
3827
+ assert wheel.protocol_frame == b""
3828
+ for _ in range(300):
3829
+ wheel = view.wheel(wheel_up)
3830
+ if wheel.protocol_frame:
3831
+ break
3809
3832
  request_id = decode_history_request(
3810
3833
  InputFrameDecoder().feed(wheel.protocol_frame)[0].data
3811
3834
  )[0]
@@ -1 +0,0 @@
1
- __version__ = "0.3.6.dev2"
File without changes
File without changes
File without changes
File without changes