railmux 0.4.2.dev3__tar.gz → 0.4.2.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 (207) hide show
  1. {railmux-0.4.2.dev3/src/railmux.egg-info → railmux-0.4.2.dev4}/PKG-INFO +1 -1
  2. railmux-0.4.2.dev4/src/railmux/__init__.py +1 -0
  3. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/fast_display_server.py +84 -3
  4. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/tmux_server.py +66 -6
  5. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4/src/railmux.egg-info}/PKG-INFO +1 -1
  6. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_fast_display_server.py +79 -0
  7. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_tmux_server.py +60 -0
  8. railmux-0.4.2.dev3/src/railmux/__init__.py +0 -1
  9. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/LICENSE +0 -0
  10. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/MANIFEST.in +0 -0
  11. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/README.md +0 -0
  12. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/pyproject.toml +0 -0
  13. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/setup.cfg +0 -0
  14. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/__main__.py +0 -0
  15. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/atomic_file.py +0 -0
  16. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/background_index.py +0 -0
  17. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/cli.py +0 -0
  18. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/codex_index.py +0 -0
  19. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/config.py +0 -0
  20. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/config_cli.py +0 -0
  21. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/diagnostic_contract.py +0 -0
  22. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/diagnostics.py +0 -0
  23. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/discovery.py +0 -0
  24. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/display_transport.py +0 -0
  25. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/entrypoint.py +0 -0
  26. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/fast_display_client.py +0 -0
  27. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/fast_display_history.py +0 -0
  28. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/fast_display_input.py +0 -0
  29. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/fast_display_protocol.py +0 -0
  30. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/favorites.py +0 -0
  31. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/fuzzy.py +0 -0
  32. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/help_workspace.py +0 -0
  33. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/image_paste.py +0 -0
  34. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/launcher.py +0 -0
  35. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/legacy_sessions.py +0 -0
  36. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/local_clipboard.py +0 -0
  37. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/local_open.py +0 -0
  38. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/models.py +0 -0
  39. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/modes.py +0 -0
  40. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/mouse_manager.py +0 -0
  41. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/orphan_marker.py +0 -0
  42. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/pane_surface.py +0 -0
  43. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/path_codec.py +0 -0
  44. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/preview_pager.py +0 -0
  45. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/project_favorites.py +0 -0
  46. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/provider_paths.py +0 -0
  47. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/release_version.py +0 -0
  48. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/remote_config.py +0 -0
  49. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/renames.py +0 -0
  50. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/restart_state.py +0 -0
  51. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/runtime_config.py +0 -0
  52. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/scroll_agent.py +0 -0
  53. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/scroll_manager.py +0 -0
  54. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/selection_isolation.py +0 -0
  55. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/self_update.py +0 -0
  56. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/session_cache.py +0 -0
  57. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/session_index.py +0 -0
  58. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/session_lease.py +0 -0
  59. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/setting_contracts.py +0 -0
  60. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/settings.py +0 -0
  61. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/ssh_args.py +0 -0
  62. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/ssh_compat.py +0 -0
  63. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/ssh_display_diagnostics.py +0 -0
  64. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/ssh_doctor.py +0 -0
  65. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/ssh_preflight.py +0 -0
  66. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/system_deps.py +0 -0
  67. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/terminal_screen.py +0 -0
  68. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/terminal_status.py +0 -0
  69. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/tmux_binding_manager.py +0 -0
  70. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/tmux_capabilities.py +0 -0
  71. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/tmux_ctl.py +0 -0
  72. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/tmux_health.py +0 -0
  73. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/tool_panes.py +0 -0
  74. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/transcript.py +0 -0
  75. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/ui/__init__.py +0 -0
  76. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/ui/_widgets.py +0 -0
  77. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/ui/app.py +0 -0
  78. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/ui/keymap.py +0 -0
  79. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/ui/modals.py +0 -0
  80. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/ui/projects_pane.py +0 -0
  81. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/ui/running_pane.py +0 -0
  82. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/ui/sessions_pane.py +0 -0
  83. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/ui/sidebar.py +0 -0
  84. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/ui/statusbar.py +0 -0
  85. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/ui/workspace.py +0 -0
  86. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/windows_attach_relay.py +0 -0
  87. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/windows_bootstrap.py +0 -0
  88. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/windows_install_log.py +0 -0
  89. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/windows_msys2.py +0 -0
  90. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/windows_pacman.py +0 -0
  91. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/windows_paths.py +0 -0
  92. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/windows_tmux_lifecycle.py +0 -0
  93. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux/windows_ui_transition.py +0 -0
  94. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux.egg-info/SOURCES.txt +0 -0
  95. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux.egg-info/dependency_links.txt +0 -0
  96. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux.egg-info/entry_points.txt +0 -0
  97. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux.egg-info/requires.txt +0 -0
  98. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/src/railmux.egg-info/top_level.txt +0 -0
  99. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/app_test_harness.py +0 -0
  100. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_agent_workspace.py +0 -0
  101. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_app_codex_placeholder.py +0 -0
  102. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_app_quit.py +0 -0
  103. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_app_restart_restore.py +0 -0
  104. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_app_running_recovery.py +0 -0
  105. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_atomic_file.py +0 -0
  106. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_background_index.py +0 -0
  107. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_cli.py +0 -0
  108. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_code_map.py +0 -0
  109. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_codex_delete.py +0 -0
  110. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_codex_index.py +0 -0
  111. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_codex_lineage.py +0 -0
  112. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_codex_transcript.py +0 -0
  113. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_config.py +0 -0
  114. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_config_cli.py +0 -0
  115. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_denested_transport_benchmark.py +0 -0
  116. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_diagnostics.py +0 -0
  117. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_discovery.py +0 -0
  118. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_display_transport.py +0 -0
  119. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_fast_display_bootstrap.py +0 -0
  120. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_fast_display_history.py +0 -0
  121. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_fast_display_protocol.py +0 -0
  122. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_fast_display_selection.py +0 -0
  123. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_fast_display_terminal.py +0 -0
  124. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_focus_highlight.py +0 -0
  125. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_fullscreen_binding.py +0 -0
  126. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_fuzzy.py +0 -0
  127. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_help_agent.py +0 -0
  128. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_help_workspace.py +0 -0
  129. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_history_cleanup.py +0 -0
  130. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_image_paste.py +0 -0
  131. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_keymap.py +0 -0
  132. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_launcher.py +0 -0
  133. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_layout_profile.py +0 -0
  134. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_legacy_sessions.py +0 -0
  135. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_local_clipboard.py +0 -0
  136. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_local_open.py +0 -0
  137. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_modals.py +0 -0
  138. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_mode_selection.py +0 -0
  139. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_modes.py +0 -0
  140. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_mouse_manager.py +0 -0
  141. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_new_project.py +0 -0
  142. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_options.py +0 -0
  143. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_orphan_recovery.py +0 -0
  144. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_pane_surface.py +0 -0
  145. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_path_codec.py +0 -0
  146. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_preview_pager.py +0 -0
  147. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_project_favorites.py +0 -0
  148. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_projects_pane.py +0 -0
  149. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_provider_paths.py +0 -0
  150. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_release_notes.py +0 -0
  151. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_release_version.py +0 -0
  152. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_release_workflow.py +0 -0
  153. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_remote_config.py +0 -0
  154. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_renames.py +0 -0
  155. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_restart_state.py +0 -0
  156. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_running_pane.py +0 -0
  157. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_running_sort.py +0 -0
  158. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_runtime_config.py +0 -0
  159. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_scroll_agent.py +0 -0
  160. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_scroll_manager.py +0 -0
  161. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_selection_isolation.py +0 -0
  162. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_self_update.py +0 -0
  163. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_session_cache.py +0 -0
  164. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_session_index.py +0 -0
  165. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_session_lease.py +0 -0
  166. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_sessions_pane.py +0 -0
  167. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_setting_contracts.py +0 -0
  168. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_settings.py +0 -0
  169. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_sidebar.py +0 -0
  170. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_ssh_args.py +0 -0
  171. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_ssh_compat.py +0 -0
  172. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_ssh_display_benchmark.py +0 -0
  173. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_ssh_display_diagnostics.py +0 -0
  174. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_ssh_doctor.py +0 -0
  175. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_status.py +0 -0
  176. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_statusbar.py +0 -0
  177. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_system_deps.py +0 -0
  178. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_terminal_size.py +0 -0
  179. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_terminal_status.py +0 -0
  180. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_termux_local_touch.py +0 -0
  181. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_tmux_binding_manager.py +0 -0
  182. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_tmux_capabilities.py +0 -0
  183. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_tmux_ctl.py +0 -0
  184. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_tmux_health.py +0 -0
  185. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_tmux_integration.py +0 -0
  186. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_tmux_status.py +0 -0
  187. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_tool_panes.py +0 -0
  188. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_transcript.py +0 -0
  189. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_utf8_state.py +0 -0
  190. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_web_demo_recorder.py +0 -0
  191. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_wheel_smoke.py +0 -0
  192. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_widgets.py +0 -0
  193. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_windows_attach_relay.py +0 -0
  194. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_windows_bootstrap.py +0 -0
  195. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_windows_install_log.py +0 -0
  196. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_windows_msys2.py +0 -0
  197. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_windows_pacman.py +0 -0
  198. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_windows_paths.py +0 -0
  199. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_windows_synchronized_screen.py +0 -0
  200. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_windows_tmux_lifecycle.py +0 -0
  201. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_windows_ui_transition.py +0 -0
  202. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_windows_wrapper_contract.py +0 -0
  203. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tests/test_yolo_prompt.py +0 -0
  204. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tools/denested_transport_benchmark.py +0 -0
  205. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tools/release_notes.py +0 -0
  206. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tools/wheel_smoke.py +0 -0
  207. {railmux-0.4.2.dev3 → railmux-0.4.2.dev4}/tools/windows_archive_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.4.2.dev3
3
+ Version: 0.4.2.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.4.2.dev4"
@@ -1747,6 +1747,83 @@ def _transcript_rows(
1747
1747
  os.close(fd)
1748
1748
 
1749
1749
 
1750
+ _OVERLAP_STYLE_RE = re.compile(rb"\x1b\[[0-9;]*m")
1751
+ _OVERLAP_SPACE_RE = re.compile(r"\s+")
1752
+ _OVERLAP_ANCHOR_CHARS = 12
1753
+ _OVERLAP_SINGLE_ANCHOR_CHARS = 24
1754
+ _OVERLAP_SEARCH_SCREENS = 4
1755
+ _OVERLAP_MIN_SEARCH_ROWS = 200
1756
+
1757
+
1758
+ def _overlap_text(row: bytes) -> str:
1759
+ """Return a row's visible text without styling or any whitespace.
1760
+
1761
+ The transcript and the provider wrap and indent the same message
1762
+ differently, so only the whitespace-free character stream is comparable.
1763
+ """
1764
+ text = _OVERLAP_STYLE_RE.sub(b"", row).decode("utf-8", errors="replace")
1765
+ return _OVERLAP_SPACE_RE.sub("", text)
1766
+
1767
+
1768
+ def _transcript_rows_before_screen(
1769
+ transcript_rows: Sequence[bytes],
1770
+ screen_rows: Sequence[bytes],
1771
+ ) -> int:
1772
+ """Return how many transcript rows precede the live screen's content.
1773
+
1774
+ Transcript-backed history appends the live viewport after the transcript,
1775
+ whose tail usually holds the same newest message. Locate the screen's
1776
+ text in the transcript tail through distinctive anchor rows and cut the
1777
+ transcript where the screen begins. Two anchors must agree on that start
1778
+ unless one long anchor is the only candidate; without a credible match
1779
+ every transcript row is kept.
1780
+ """
1781
+ total = len(transcript_rows)
1782
+ window = max(
1783
+ _OVERLAP_MIN_SEARCH_ROWS, _OVERLAP_SEARCH_SCREENS * len(screen_rows))
1784
+ first = max(0, total - window)
1785
+ row_starts: list[int] = []
1786
+ parts: list[str] = []
1787
+ offset = 0
1788
+ for row in transcript_rows[first:]:
1789
+ row_starts.append(offset)
1790
+ text = _overlap_text(row)
1791
+ parts.append(text)
1792
+ offset += len(text)
1793
+ haystack = "".join(parts)
1794
+ screen_offset = 0
1795
+ starts: list[tuple[int, int]] = []
1796
+ for row in screen_rows:
1797
+ text = _overlap_text(row)
1798
+ if len(text) >= _OVERLAP_ANCHOR_CHARS and haystack.count(text) == 1:
1799
+ starts.append((haystack.index(text) - screen_offset, len(text)))
1800
+ if len(starts) == 2:
1801
+ break
1802
+ screen_offset += len(text)
1803
+ if not starts:
1804
+ return total
1805
+ if len(starts) == 1:
1806
+ if starts[0][1] < _OVERLAP_SINGLE_ANCHOR_CHARS:
1807
+ return total
1808
+ else:
1809
+ # Allow small rendering differences (list numbering, bullets).
1810
+ tolerance = 16 + abs(starts[0][0]) // 20
1811
+ if abs(starts[0][0] - starts[1][0]) > tolerance:
1812
+ return total
1813
+ start = max(0, starts[0][0])
1814
+ # Keep a row that begins before the screen: it may carry text the screen
1815
+ # no longer shows, and a partial duplicate is better than a loss.
1816
+ keep = len(row_starts)
1817
+ for index, row_start in enumerate(row_starts):
1818
+ if row_start >= start:
1819
+ keep = index
1820
+ break
1821
+ # Drop trailing blank rows so the screen is not preceded by a gap.
1822
+ while keep and not parts[keep - 1]:
1823
+ keep -= 1
1824
+ return first + keep
1825
+
1826
+
1750
1827
  def _capture_pane_history(
1751
1828
  pyte: object,
1752
1829
  pane: _PaneGeometry,
@@ -1852,14 +1929,18 @@ def _capture_pane_history(
1852
1929
  transcript_used = transcript_entry is not None and bool(transcript_entry.rows)
1853
1930
  if transcript_used:
1854
1931
  assert transcript_entry is not None
1855
- transcript_lines = transcript_entry.rows
1932
+ kept = _transcript_rows_before_screen(transcript_entry.rows, current_raw)
1933
+ trimmed = len(transcript_entry.rows) - kept
1934
+ transcript_lines = transcript_entry.rows[:kept]
1856
1935
  history_count = max(0, max_lines - pane.height)
1857
1936
  source_lines = (
1858
1937
  transcript_lines[-history_count:] if history_count else ()
1859
1938
  ) + current_raw
1860
1939
  timeline_end = max(
1861
1940
  pane.height,
1862
- (transcript_entry.total_rows or len(transcript_entry.rows)) + pane.height,
1941
+ (transcript_entry.total_rows or len(transcript_entry.rows))
1942
+ - trimmed
1943
+ + pane.height,
1863
1944
  )
1864
1945
  else:
1865
1946
  source_lines = rendered_raw
@@ -1910,7 +1991,7 @@ def _capture_pane_history(
1910
1991
  budget_truncated
1911
1992
  or (
1912
1993
  transcript_entry.more_available
1913
- or len(transcript_entry.rows) + pane.height > len(lines)
1994
+ or len(transcript_lines) + pane.height > len(lines)
1914
1995
  if transcript_used and transcript_entry is not None
1915
1996
  else pane.history_size + pane.height > len(lines)
1916
1997
  )
@@ -22,6 +22,8 @@ _SESSION_ID_RE = re.compile(r"[A-Za-z0-9-]{1,256}\Z")
22
22
  _HISTORY_SOURCE_SCHEMA = 1
23
23
  _TRANSCRIPT_SOURCE_SCHEMA = 1
24
24
  _MAX_TRANSCRIPT_SOURCE_BYTES = 8192
25
+ _CONTINUATION_TAIL_BYTES = 4096
26
+ _MAX_CONTINUATION_HOPS = 8
25
27
  _DEFAULT_DISCOVERY_TIMEOUT = 2.0
26
28
  _WINDOWS_DISCOVERY_TIMEOUT = 5.0
27
29
 
@@ -415,11 +417,8 @@ def decode_transcript_source(raw: str) -> TranscriptSource | None:
415
417
  return TranscriptSource(provider, session_id, path)
416
418
 
417
419
 
418
- def open_transcript_source(raw: str) -> tuple[TranscriptSource, int] | None:
419
- """Open an exact same-user transcript without following the final symlink."""
420
- source = decode_transcript_source(raw)
421
- if source is None:
422
- return None
420
+ def _open_owned_regular_file(path: Path) -> int | None:
421
+ """Open a same-user regular file without following the final symlink."""
423
422
  flags = os.O_RDONLY
424
423
  if hasattr(os, "O_CLOEXEC"):
425
424
  flags |= os.O_CLOEXEC
@@ -427,7 +426,7 @@ def open_transcript_source(raw: str) -> tuple[TranscriptSource, int] | None:
427
426
  flags |= os.O_NOFOLLOW
428
427
  fd = -1
429
428
  try:
430
- fd = os.open(source.path, flags)
429
+ fd = os.open(path, flags)
431
430
  info = os.fstat(fd)
432
431
  except OSError:
433
432
  if fd >= 0:
@@ -443,6 +442,67 @@ def open_transcript_source(raw: str) -> tuple[TranscriptSource, int] | None:
443
442
  ):
444
443
  os.close(fd)
445
444
  return None
445
+ return fd
446
+
447
+
448
+ def _claude_continuation_id(fd: int) -> str | None:
449
+ """Return the successor session named by a final ``continued-in`` record.
450
+
451
+ Claude Code can move a live conversation to a new session file, copying
452
+ the full history and ending the old file with one ``continued-in``
453
+ record. Only that exact final record is trusted.
454
+ """
455
+ try:
456
+ size = os.fstat(fd).st_size
457
+ tail = os.pread(
458
+ fd,
459
+ min(size, _CONTINUATION_TAIL_BYTES),
460
+ max(0, size - _CONTINUATION_TAIL_BYTES),
461
+ )
462
+ except OSError:
463
+ return None
464
+ lines = tail.rstrip(b"\n").rsplit(b"\n", 1)
465
+ if b"continued-in" not in lines[-1]:
466
+ return None
467
+ try:
468
+ record = json.loads(lines[-1])
469
+ except (UnicodeDecodeError, ValueError):
470
+ return None
471
+ if not isinstance(record, dict) or record.get("type") != "continued-in":
472
+ return None
473
+ successor = record.get("continuedInSessionId")
474
+ if not isinstance(successor, str) or not _SESSION_ID_RE.fullmatch(successor):
475
+ return None
476
+ return successor
477
+
478
+
479
+ def open_transcript_source(raw: str) -> tuple[TranscriptSource, int] | None:
480
+ """Open an exact same-user transcript without following the final symlink.
481
+
482
+ A Claude transcript that was continued in another session resolves to the
483
+ newest same-directory successor, so history keeps following the live pane.
484
+ """
485
+ source = decode_transcript_source(raw)
486
+ if source is None:
487
+ return None
488
+ fd = _open_owned_regular_file(source.path)
489
+ if fd is None:
490
+ return None
491
+ if source.provider != "claude":
492
+ return source, fd
493
+ seen = {source.session_id}
494
+ for _ in range(_MAX_CONTINUATION_HOPS):
495
+ successor = _claude_continuation_id(fd)
496
+ if successor is None or successor in seen:
497
+ break
498
+ path = source.path.with_name(f"{successor}.jsonl")
499
+ next_fd = _open_owned_regular_file(path)
500
+ if next_fd is None:
501
+ break
502
+ os.close(fd)
503
+ fd = next_fd
504
+ source = TranscriptSource(source.provider, successor, path)
505
+ seen.add(successor)
446
506
  return source, fd
447
507
 
448
508
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.4.2.dev3
3
+ Version: 0.4.2.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
@@ -1593,6 +1593,85 @@ def test_server_claude_history_uses_stable_transcript_suffix(monkeypatch):
1593
1593
  assert hot.lines[-2:] == (b"live-a", b"live-b")
1594
1594
 
1595
1595
 
1596
+ def test_server_claude_history_drops_transcript_tail_shown_on_screen(monkeypatch):
1597
+ pane = fast_display_server._PaneGeometry(
1598
+ "%8",
1599
+ 31,
1600
+ 0,
1601
+ 24,
1602
+ 4,
1603
+ mouse_forwardable=True,
1604
+ alternate_on=True,
1605
+ transcript_source="exact-marker",
1606
+ transcript_backed=True,
1607
+ claude_history_policy="local",
1608
+ )
1609
+ # The transcript wraps the newest answer at the pane edge while the live
1610
+ # Claude screen uses a hanging indent and styling for the same text.
1611
+ transcript_rows = (
1612
+ b"\x1b[0mold question here\x1b[0m",
1613
+ b"\x1b[0m\x1b[0m",
1614
+ b"\x1b[0m\xe2\x97\x8f first answer line alph",
1615
+ b"a beta gamma delta\x1b[0m",
1616
+ b"\x1b[0m second answer line kept\x1b[0m",
1617
+ )
1618
+ live = (
1619
+ b"\xe2\x97\x8f first answer line",
1620
+ b" \x1b[1malpha beta gamma delta\x1b[0m",
1621
+ b" second answer line kept",
1622
+ b"\xe2\x9d\xaf",
1623
+ )
1624
+ monkeypatch.setattr(
1625
+ subprocess,
1626
+ "check_output",
1627
+ lambda *_args, **_kwargs: b"\n".join(live) + b"\n",
1628
+ )
1629
+ monkeypatch.setattr(
1630
+ fast_display_server,
1631
+ "_transcript_rows",
1632
+ lambda *_args, **_kwargs: fast_display_server._TranscriptCacheEntry(
1633
+ (1, 2, 3, 4), transcript_rows, False
1634
+ ),
1635
+ )
1636
+ monkeypatch.setattr(
1637
+ fast_display_server,
1638
+ "_render_history_lines",
1639
+ lambda _pyte, lines, _width: tuple(lines),
1640
+ )
1641
+
1642
+ snapshot = fast_display_server._capture_pane_history(object(), pane, 1, 300)
1643
+
1644
+ assert snapshot is not None
1645
+ assert snapshot.lines == (transcript_rows[0],) + live
1646
+ assert snapshot.timeline_end == 1 + pane.height
1647
+
1648
+
1649
+ def test_transcript_overlap_keeps_rows_without_credible_screen_match():
1650
+ rows = tuple(
1651
+ f"repeated status line number {index % 2}".encode() for index in range(8)
1652
+ )
1653
+ unrelated = (b"nothing in common with the transcript at all", b"")
1654
+ repeated = (b"repeated status line number 0", b"repeated status line number 1")
1655
+ short = (b"tiny", b"")
1656
+
1657
+ keep = fast_display_server._transcript_rows_before_screen
1658
+ assert keep(rows, unrelated) == len(rows)
1659
+ assert keep(rows, repeated) == len(rows)
1660
+ assert keep(rows, short) == len(rows)
1661
+ assert keep((), unrelated) == 0
1662
+
1663
+
1664
+ def test_transcript_overlap_rejects_disagreeing_anchors():
1665
+ rows = (
1666
+ b"first distinctive anchor row",
1667
+ b"filler " * 20,
1668
+ b"second distinctive anchor row",
1669
+ )
1670
+ screen = (b"second distinctive anchor row", b"first distinctive anchor row")
1671
+
1672
+ assert fast_display_server._transcript_rows_before_screen(rows, screen) == 3
1673
+
1674
+
1596
1675
  @pytest.mark.parametrize(
1597
1676
  ("policy", "choice_required"),
1598
1677
  [("ask", True), ("native", False)],
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
3
4
  import os
4
5
  import subprocess
5
6
  from types import SimpleNamespace
@@ -519,6 +520,65 @@ def test_transcript_source_round_trip_opens_only_exact_same_user_file(tmp_path):
519
520
  os.close(opened[1])
520
521
 
521
522
 
523
+ def _continued_in(session_id: str) -> str:
524
+ return json.dumps({
525
+ "type": "continued-in",
526
+ "sessionId": "previous",
527
+ "continuedInSessionId": session_id,
528
+ }) + "\n"
529
+
530
+
531
+ def test_claude_transcript_source_follows_continued_in_chain(tmp_path):
532
+ first, second, third = (
533
+ "47fca075-9cb8-44fb-a314-d57ef2256ad9",
534
+ "96fd3642-6c17-4c9b-8129-08d3d9db3e8a",
535
+ "02b59000-6bb4-4ec5-bdca-24f7a96b0467",
536
+ )
537
+ (tmp_path / f"{first}.jsonl").write_text(
538
+ '{"type":"user"}\n' + _continued_in(second))
539
+ (tmp_path / f"{second}.jsonl").write_text(
540
+ '{"type":"user"}\n' + _continued_in(third))
541
+ (tmp_path / f"{third}.jsonl").write_text('{"type":"assistant"}\n')
542
+ marker = tmux_server.encode_transcript_source(
543
+ "claude", first, tmp_path / f"{first}.jsonl")
544
+
545
+ opened = tmux_server.open_transcript_source(marker)
546
+
547
+ assert opened is not None
548
+ source, fd = opened
549
+ try:
550
+ assert source.session_id == third
551
+ assert source.path == tmp_path / f"{third}.jsonl"
552
+ assert os.read(fd, 100) == b'{"type":"assistant"}\n'
553
+ finally:
554
+ os.close(fd)
555
+
556
+
557
+ @pytest.mark.parametrize(
558
+ "tail",
559
+ [
560
+ _continued_in("missing-successor"),
561
+ _continued_in("../escape"),
562
+ _continued_in("47fca075-9cb8-44fb-a314-d57ef2256ad9"),
563
+ _continued_in("96fd3642-6c17-4c9b-8129-08d3d9db3e8a") + '{"type":"user"}\n',
564
+ ],
565
+ )
566
+ def test_claude_transcript_source_keeps_file_without_valid_successor(
567
+ tmp_path, tail,
568
+ ):
569
+ session_id = "47fca075-9cb8-44fb-a314-d57ef2256ad9"
570
+ path = tmp_path / f"{session_id}.jsonl"
571
+ path.write_text('{"type":"user"}\n' + tail)
572
+ (tmp_path / "96fd3642-6c17-4c9b-8129-08d3d9db3e8a.jsonl").write_text("{}\n")
573
+ marker = tmux_server.encode_transcript_source("claude", session_id, path)
574
+
575
+ opened = tmux_server.open_transcript_source(marker)
576
+
577
+ assert opened is not None
578
+ os.close(opened[1])
579
+ assert opened[0].session_id == session_id and opened[0].path == path
580
+
581
+
522
582
  def test_codex_transcript_source_requires_matching_rollout_file(tmp_path):
523
583
  session_id = "019fcaad-27a1-70c0-8029-8a9c7803fa6b"
524
584
  path = tmp_path / f"rollout-2026-08-04T02-49-33-{session_id}.jsonl"
@@ -1 +0,0 @@
1
- __version__ = "0.4.2.dev3"
File without changes
File without changes
File without changes
File without changes