railmux 0.3.6.dev2__tar.gz → 0.3.6.dev4__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.dev4}/PKG-INFO +1 -1
  2. railmux-0.3.6.dev4/src/railmux/__init__.py +1 -0
  3. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/fast_display_history.py +39 -52
  4. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/fast_display_server.py +33 -16
  5. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4/src/railmux.egg-info}/PKG-INFO +1 -1
  6. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_fast_full_window.py +86 -33
  7. railmux-0.3.6.dev2/src/railmux/__init__.py +0 -1
  8. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/LICENSE +0 -0
  9. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/MANIFEST.in +0 -0
  10. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/README.md +0 -0
  11. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/pyproject.toml +0 -0
  12. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/setup.cfg +0 -0
  13. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/__main__.py +0 -0
  14. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/atomic_file.py +0 -0
  15. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/background_index.py +0 -0
  16. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/cli.py +0 -0
  17. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/codex_index.py +0 -0
  18. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/config.py +0 -0
  19. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/config_cli.py +0 -0
  20. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/diagnostics.py +0 -0
  21. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/discovery.py +0 -0
  22. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/display_transport.py +0 -0
  23. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/fast_display_client.py +0 -0
  24. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/fast_display_input.py +0 -0
  25. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/fast_display_protocol.py +0 -0
  26. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/favorites.py +0 -0
  27. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/fuzzy.py +0 -0
  28. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/help_workspace.py +0 -0
  29. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/launcher.py +0 -0
  30. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/legacy_sessions.py +0 -0
  31. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/local_clipboard.py +0 -0
  32. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/local_open.py +0 -0
  33. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/models.py +0 -0
  34. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/modes.py +0 -0
  35. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/mouse_manager.py +0 -0
  36. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/orphan_marker.py +0 -0
  37. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/pane_surface.py +0 -0
  38. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/path_codec.py +0 -0
  39. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/remote_config.py +0 -0
  40. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/renames.py +0 -0
  41. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/restart_state.py +0 -0
  42. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/runtime_config.py +0 -0
  43. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/scroll_agent.py +0 -0
  44. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/scroll_manager.py +0 -0
  45. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/selection_isolation.py +0 -0
  46. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/self_update.py +0 -0
  47. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/session_cache.py +0 -0
  48. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/session_index.py +0 -0
  49. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/session_lease.py +0 -0
  50. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/setting_contracts.py +0 -0
  51. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/settings.py +0 -0
  52. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/ssh_args.py +0 -0
  53. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/ssh_compat.py +0 -0
  54. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/ssh_display_diagnostics.py +0 -0
  55. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/ssh_doctor.py +0 -0
  56. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/system_deps.py +0 -0
  57. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/terminal_status.py +0 -0
  58. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/tmux_binding_manager.py +0 -0
  59. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/tmux_ctl.py +0 -0
  60. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/tmux_health.py +0 -0
  61. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/tmux_server.py +0 -0
  62. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/tool_panes.py +0 -0
  63. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/transcript.py +0 -0
  64. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/ui/__init__.py +0 -0
  65. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/ui/_widgets.py +0 -0
  66. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/ui/app.py +0 -0
  67. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/ui/keymap.py +0 -0
  68. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/ui/modals.py +0 -0
  69. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/ui/projects_pane.py +0 -0
  70. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/ui/running_pane.py +0 -0
  71. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/ui/sessions_pane.py +0 -0
  72. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/ui/sidebar.py +0 -0
  73. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/ui/statusbar.py +0 -0
  74. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux/ui/workspace.py +0 -0
  75. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux.egg-info/SOURCES.txt +0 -0
  76. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux.egg-info/dependency_links.txt +0 -0
  77. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux.egg-info/entry_points.txt +0 -0
  78. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux.egg-info/requires.txt +0 -0
  79. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/src/railmux.egg-info/top_level.txt +0 -0
  80. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_agent_workspace.py +0 -0
  81. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_atomic_file.py +0 -0
  82. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_background_index.py +0 -0
  83. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_cli.py +0 -0
  84. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_codex_delete.py +0 -0
  85. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_codex_index.py +0 -0
  86. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_codex_lineage.py +0 -0
  87. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_codex_transcript.py +0 -0
  88. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_config.py +0 -0
  89. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_config_cli.py +0 -0
  90. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_denested_transport_benchmark.py +0 -0
  91. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_diagnostics.py +0 -0
  92. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_discovery.py +0 -0
  93. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_display_transport.py +0 -0
  94. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_focus_highlight.py +0 -0
  95. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_fullscreen_binding.py +0 -0
  96. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_fuzzy.py +0 -0
  97. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_help_agent.py +0 -0
  98. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_help_workspace.py +0 -0
  99. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_history_cleanup.py +0 -0
  100. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_keymap.py +0 -0
  101. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_launcher.py +0 -0
  102. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_layout_profile.py +0 -0
  103. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_legacy_sessions.py +0 -0
  104. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_local_clipboard.py +0 -0
  105. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_local_open.py +0 -0
  106. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_modals.py +0 -0
  107. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_mode_selection.py +0 -0
  108. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_modes.py +0 -0
  109. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_mouse_manager.py +0 -0
  110. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_new_project.py +0 -0
  111. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_options.py +0 -0
  112. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_orphan_recovery.py +0 -0
  113. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_pane_surface.py +0 -0
  114. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_path_codec.py +0 -0
  115. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_projects_pane.py +0 -0
  116. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_release_notes.py +0 -0
  117. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_release_workflow.py +0 -0
  118. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_remote_config.py +0 -0
  119. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_renames.py +0 -0
  120. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_restart_state.py +0 -0
  121. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_running_pane.py +0 -0
  122. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_running_sort.py +0 -0
  123. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_runtime_config.py +0 -0
  124. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_scroll_agent.py +0 -0
  125. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_scroll_manager.py +0 -0
  126. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_selection_isolation.py +0 -0
  127. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_self_update.py +0 -0
  128. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_session_cache.py +0 -0
  129. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_session_index.py +0 -0
  130. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_session_lease.py +0 -0
  131. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_sessions_pane.py +0 -0
  132. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_setting_contracts.py +0 -0
  133. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_settings.py +0 -0
  134. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_sidebar.py +0 -0
  135. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_soft_quit.py +0 -0
  136. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_ssh_args.py +0 -0
  137. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_ssh_compat.py +0 -0
  138. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_ssh_display_benchmark.py +0 -0
  139. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_ssh_display_diagnostics.py +0 -0
  140. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_ssh_doctor.py +0 -0
  141. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_status.py +0 -0
  142. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_statusbar.py +0 -0
  143. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_system_deps.py +0 -0
  144. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_terminal_size.py +0 -0
  145. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_terminal_status.py +0 -0
  146. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_termux_local_touch.py +0 -0
  147. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_tmux_binding_manager.py +0 -0
  148. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_tmux_ctl.py +0 -0
  149. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_tmux_health.py +0 -0
  150. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_tmux_integration.py +0 -0
  151. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_tmux_server.py +0 -0
  152. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_tmux_status.py +0 -0
  153. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_tool_panes.py +0 -0
  154. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_transcript.py +0 -0
  155. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_utf8_state.py +0 -0
  156. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_web_demo_recorder.py +0 -0
  157. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_wheel_smoke.py +0 -0
  158. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_widgets.py +0 -0
  159. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tests/test_yolo_prompt.py +0 -0
  160. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tools/denested_transport_benchmark.py +0 -0
  161. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/tools/release_notes.py +0 -0
  162. {railmux-0.3.6.dev2 → railmux-0.3.6.dev4}/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.dev4
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.dev4"
@@ -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
@@ -1171,12 +1171,35 @@ def apply_path_open_request(
1171
1171
  )
1172
1172
 
1173
1173
 
1174
- def _render_history_line(pyte: object, line: bytes, width: int) -> bytes:
1175
- """Parse one physical tmux line and emit only allowlisted SGR styling."""
1174
+ def _render_history_lines(
1175
+ pyte: object,
1176
+ lines: Sequence[bytes],
1177
+ width: int,
1178
+ ) -> tuple[bytes, ...]:
1179
+ """Render physical tmux rows while retaining cross-row terminal style.
1180
+
1181
+ ``tmux capture-pane -e`` is a terminal stream, not a collection of
1182
+ independently styled strings. In particular, tmux omits an SGR prefix
1183
+ when a row inherits the previous row's foreground, background, or text
1184
+ attributes. Keep one decoder alive for the ordered capture, but clear its
1185
+ one-row cell buffer between rows. :func:`render_rows` then makes every
1186
+ result independently paintable for the local history overlay.
1187
+ """
1176
1188
  screen = pyte.Screen(width, 1)
1177
1189
  stream = pyte.ByteStream(screen)
1178
- stream.feed(line)
1179
- return render_rows(screen)[0]
1190
+ rendered: list[bytes] = []
1191
+ for line in lines:
1192
+ screen.buffer[0].clear()
1193
+ screen.cursor.x = 0
1194
+ screen.cursor.y = 0
1195
+ stream.feed(line)
1196
+ rendered.append(render_rows(screen)[0])
1197
+ return tuple(rendered)
1198
+
1199
+
1200
+ def _render_history_line(pyte: object, line: bytes, width: int) -> bytes:
1201
+ """Render one self-contained physical row with allowlisted SGR styling."""
1202
+ return _render_history_lines(pyte, (line,), width)[0]
1180
1203
 
1181
1204
 
1182
1205
  def _read_transcript_tail(fd: int) -> tuple[str, bool]:
@@ -1352,7 +1375,8 @@ def _capture_pane_history(
1352
1375
  # Retain the newest suffix when the styled representation reaches its byte
1353
1376
  # budget. Iterate backwards so an oversized response never sacrifices the
1354
1377
  # pane's current viewport merely to retain older scrollback.
1355
- current_raw = raw_lines[-pane.height :]
1378
+ rendered_raw = _render_history_lines(pyte, raw_lines, pane.width)
1379
+ current_raw = rendered_raw[-pane.height :]
1356
1380
  transcript_entry = None
1357
1381
  # ``None`` is retained only for old/test geometry that predates the
1358
1382
  # provider field; every validated live marker carries an explicit value.
@@ -1383,21 +1407,14 @@ def _capture_pane_history(
1383
1407
  transcript_lines = transcript_entry.rows
1384
1408
  history_count = max(0, max_lines - pane.height)
1385
1409
  source_lines = (
1386
- tuple((line, True) for line in transcript_lines[-history_count:])
1387
- if history_count
1388
- else ()
1389
- ) + tuple((line, False) for line in current_raw)
1410
+ transcript_lines[-history_count:] if history_count else ()
1411
+ ) + current_raw
1390
1412
  else:
1391
- source_lines = tuple((line, False) for line in raw_lines)
1413
+ source_lines = rendered_raw
1392
1414
  newest_first: list[bytes] = []
1393
1415
  packed_size = 2 # history line-count prefix
1394
1416
  budget_truncated = False
1395
- for raw_line, rendered_already in reversed(source_lines[-max_lines:]):
1396
- rendered = (
1397
- raw_line
1398
- if rendered_already
1399
- else _render_history_line(pyte, raw_line, pane.width)
1400
- )
1417
+ for rendered in reversed(source_lines[-max_lines:]):
1401
1418
  line_size = 4 + len(rendered)
1402
1419
  if packed_size + line_size > _HISTORY_SNAPSHOT_RAW_BUDGET:
1403
1420
  budget_truncated = True
@@ -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.dev4
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
+
2729
+ assert action.render_history is True
2730
+ assert action.protocol_frame == b""
2731
+ assert view.overlays()[0][1][-1] == b"line-298"
2728
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]
@@ -6954,8 +6977,8 @@ def test_server_history_capture_honours_limits_above_the_old_4096_cap(
6954
6977
  )
6955
6978
  monkeypatch.setattr(
6956
6979
  fast_display_server,
6957
- "_render_history_line",
6958
- lambda _pyte, line, _width: line,
6980
+ "_render_history_lines",
6981
+ lambda _pyte, lines, _width: tuple(lines),
6959
6982
  )
6960
6983
 
6961
6984
  snapshot = fast_display_server._capture_pane_history(object(), pane, 7, 5000)
@@ -6996,16 +7019,31 @@ def test_server_raw_styled_hot_and_deep_history_keep_codex_foreground(
6996
7019
  b"\033[38;2;205;214;244mvalue = 1\033[39m"
6997
7020
  )
6998
7021
  monochrome = (
6999
- b"\033[48;5;22m 3 \033[0m\033[38;5;2m\033[48;5;22m"
7022
+ # tmux omits the leading background because this physical row inherits
7023
+ # it from ``highlighted``. History parsing must keep that stream state
7024
+ # while still returning an independently paintable row.
7025
+ b" 3 \033[0m\033[38;5;2m\033[48;5;22m"
7000
7026
  b"+#!/bin/bash\033[39m"
7001
7027
  )
7028
+ removed = (
7029
+ b"\033[48;5;52m 4 \033[0m\033[38;5;1m\033[48;5;52m"
7030
+ b"-removed = True\033[39m"
7031
+ )
7032
+ removed_inherited = (
7033
+ b" 5 \033[0m\033[38;5;1m\033[48;5;52m"
7034
+ b"-removed = False\033[39m"
7035
+ )
7036
+ ordinary = b"\033[0m ordinary output"
7002
7037
  raw = b"\n".join((
7003
7038
  *(f"old-{index}".encode() for index in range(99)),
7004
7039
  wrap_head,
7005
7040
  wrap_continuation,
7006
- *(f"line-{index}".encode() for index in range(297)),
7041
+ *(f"line-{index}".encode() for index in range(294)),
7007
7042
  highlighted,
7008
7043
  monochrome,
7044
+ removed,
7045
+ removed_inherited,
7046
+ ordinary,
7009
7047
  )) + b"\n"
7010
7048
  monkeypatch.setattr(
7011
7049
  subprocess, "check_output", lambda *_args, **_kwargs: raw)
@@ -7031,19 +7069,32 @@ def test_server_raw_styled_hot_and_deep_history_keep_codex_foreground(
7031
7069
  terminal.ByteStream(screen).feed(row)
7032
7070
  return screen
7033
7071
 
7034
- highlighted_screen = styled_row(deep.lines[-2])
7035
- monochrome_screen = styled_row(deep.lines[-1])
7072
+ highlighted_screen = styled_row(deep.lines[-5])
7073
+ monochrome_screen = styled_row(deep.lines[-4])
7074
+ removed_screen = styled_row(deep.lines[-3])
7075
+ removed_inherited_screen = styled_row(deep.lines[-2])
7076
+ ordinary_screen = styled_row(deep.lines[-1])
7036
7077
  highlighted_start = highlighted_screen.display[0].index("value")
7037
7078
  monochrome_start = monochrome_screen.display[0].index("#!/bin/bash")
7038
7079
  assert highlighted_screen.buffer[0][highlighted_start].fg == "cdd6f4"
7039
7080
  assert monochrome_screen.buffer[0][monochrome_start].fg == "00cd00"
7081
+ assert highlighted_screen.buffer[0][0].bg == "005f00"
7082
+ assert monochrome_screen.buffer[0][0].bg == "005f00"
7083
+ assert removed_screen.buffer[0][0].bg == "5f0000"
7084
+ assert removed_inherited_screen.buffer[0][0].bg == "5f0000"
7085
+ assert ordinary_screen.buffer[0][0].bg == "default"
7040
7086
  coloured_backgrounds = {
7041
7087
  char.bg
7042
- for screen in (highlighted_screen, monochrome_screen)
7088
+ for screen in (
7089
+ highlighted_screen,
7090
+ monochrome_screen,
7091
+ removed_screen,
7092
+ removed_inherited_screen,
7093
+ )
7043
7094
  for char in screen.buffer[0].values()
7044
7095
  if char.bg != "default"
7045
7096
  }
7046
- assert coloured_backgrounds == {"005f00"}
7097
+ assert coloured_backgrounds == {"005f00", "5f0000"}
7047
7098
 
7048
7099
 
7049
7100
  def test_server_claude_history_uses_stable_transcript_suffix(monkeypatch):
@@ -7074,8 +7125,8 @@ def test_server_claude_history_uses_stable_transcript_suffix(monkeypatch):
7074
7125
  )
7075
7126
  monkeypatch.setattr(
7076
7127
  fast_display_server,
7077
- "_render_history_line",
7078
- lambda _pyte, line, _width: line,
7128
+ "_render_history_lines",
7129
+ lambda _pyte, lines, _width: tuple(lines),
7079
7130
  )
7080
7131
 
7081
7132
  hot = fast_display_server._capture_pane_history(object(), pane, 1, 300)
@@ -7117,8 +7168,8 @@ def test_server_waits_for_local_choice_before_rendering_claude_transcript(
7117
7168
  )
7118
7169
  monkeypatch.setattr(
7119
7170
  fast_display_server,
7120
- "_render_history_line",
7121
- lambda _pyte, line, _width: line,
7171
+ "_render_history_lines",
7172
+ lambda _pyte, lines, _width: tuple(lines),
7122
7173
  )
7123
7174
 
7124
7175
  snapshot = fast_display_server._capture_pane_history(object(), pane, 1, 300)
@@ -7165,8 +7216,8 @@ def test_server_codex_history_uses_canonical_transcript_after_rewind(
7165
7216
  )
7166
7217
  monkeypatch.setattr(
7167
7218
  fast_display_server,
7168
- "_render_history_line",
7169
- lambda _pyte, line, _width: line,
7219
+ "_render_history_lines",
7220
+ lambda _pyte, lines, _width: tuple(lines),
7170
7221
  )
7171
7222
 
7172
7223
  snapshot = fast_display_server._capture_pane_history(
@@ -7207,8 +7258,8 @@ def test_server_unreadable_claude_transcript_preserves_native_wheel_fallback(
7207
7258
  )
7208
7259
  monkeypatch.setattr(
7209
7260
  fast_display_server,
7210
- "_render_history_line",
7211
- lambda _pyte, line, _width: line,
7261
+ "_render_history_lines",
7262
+ lambda _pyte, lines, _width: tuple(lines),
7212
7263
  )
7213
7264
 
7214
7265
  snapshot = fast_display_server._capture_pane_history(object(), pane, 1, 300)
@@ -7291,8 +7342,10 @@ def test_server_history_capture_truncates_to_newest_styled_byte_budget(
7291
7342
  line_bytes = fast_display_server._HISTORY_SNAPSHOT_RAW_BUDGET // 3 + 100
7292
7343
  monkeypatch.setattr(
7293
7344
  fast_display_server,
7294
- "_render_history_line",
7295
- lambda _pyte, line, _width: line + b"x" * line_bytes,
7345
+ "_render_history_lines",
7346
+ lambda _pyte, lines, _width: tuple(
7347
+ line + b"x" * line_bytes for line in lines
7348
+ ),
7296
7349
  )
7297
7350
 
7298
7351
  snapshot = fast_display_server._capture_pane_history(object(), pane, 7, 20000)
@@ -1 +0,0 @@
1
- __version__ = "0.3.6.dev2"
File without changes
File without changes
File without changes
File without changes