railmux 0.4.1.dev5__tar.gz → 0.4.2.dev1__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.1.dev5/src/railmux.egg-info → railmux-0.4.2.dev1}/PKG-INFO +20 -6
  2. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/README.md +19 -5
  3. railmux-0.4.2.dev1/src/railmux/__init__.py +1 -0
  4. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/fast_display_client.py +233 -10
  5. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/fast_display_history.py +2 -2
  6. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/fast_display_input.py +149 -28
  7. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/fast_display_protocol.py +220 -8
  8. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/fast_display_server.py +406 -13
  9. railmux-0.4.2.dev1/src/railmux/image_paste.py +448 -0
  10. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/terminal_screen.py +167 -1
  11. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/windows_attach_relay.py +102 -6
  12. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1/src/railmux.egg-info}/PKG-INFO +20 -6
  13. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux.egg-info/SOURCES.txt +2 -0
  14. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_fast_display_protocol.py +99 -1
  15. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_fast_display_selection.py +60 -0
  16. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_fast_display_server.py +214 -3
  17. railmux-0.4.2.dev1/tests/test_image_paste.py +74 -0
  18. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_windows_attach_relay.py +143 -1
  19. railmux-0.4.1.dev5/src/railmux/__init__.py +0 -1
  20. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/LICENSE +0 -0
  21. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/MANIFEST.in +0 -0
  22. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/pyproject.toml +0 -0
  23. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/setup.cfg +0 -0
  24. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/__main__.py +0 -0
  25. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/atomic_file.py +0 -0
  26. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/background_index.py +0 -0
  27. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/cli.py +0 -0
  28. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/codex_index.py +0 -0
  29. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/config.py +0 -0
  30. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/config_cli.py +0 -0
  31. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/diagnostic_contract.py +0 -0
  32. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/diagnostics.py +0 -0
  33. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/discovery.py +0 -0
  34. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/display_transport.py +0 -0
  35. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/entrypoint.py +0 -0
  36. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/favorites.py +0 -0
  37. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/fuzzy.py +0 -0
  38. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/help_workspace.py +0 -0
  39. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/launcher.py +0 -0
  40. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/legacy_sessions.py +0 -0
  41. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/local_clipboard.py +0 -0
  42. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/local_open.py +0 -0
  43. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/models.py +0 -0
  44. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/modes.py +0 -0
  45. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/mouse_manager.py +0 -0
  46. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/orphan_marker.py +0 -0
  47. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/pane_surface.py +0 -0
  48. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/path_codec.py +0 -0
  49. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/preview_pager.py +0 -0
  50. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/project_favorites.py +0 -0
  51. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/provider_paths.py +0 -0
  52. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/release_version.py +0 -0
  53. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/remote_config.py +0 -0
  54. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/renames.py +0 -0
  55. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/restart_state.py +0 -0
  56. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/runtime_config.py +0 -0
  57. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/scroll_agent.py +0 -0
  58. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/scroll_manager.py +0 -0
  59. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/selection_isolation.py +0 -0
  60. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/self_update.py +0 -0
  61. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/session_cache.py +0 -0
  62. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/session_index.py +0 -0
  63. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/session_lease.py +0 -0
  64. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/setting_contracts.py +0 -0
  65. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/settings.py +0 -0
  66. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/ssh_args.py +0 -0
  67. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/ssh_compat.py +0 -0
  68. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/ssh_display_diagnostics.py +0 -0
  69. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/ssh_doctor.py +0 -0
  70. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/ssh_preflight.py +0 -0
  71. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/system_deps.py +0 -0
  72. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/terminal_status.py +0 -0
  73. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/tmux_binding_manager.py +0 -0
  74. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/tmux_capabilities.py +0 -0
  75. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/tmux_ctl.py +0 -0
  76. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/tmux_health.py +0 -0
  77. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/tmux_server.py +0 -0
  78. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/tool_panes.py +0 -0
  79. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/transcript.py +0 -0
  80. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/ui/__init__.py +0 -0
  81. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/ui/_widgets.py +0 -0
  82. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/ui/app.py +0 -0
  83. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/ui/keymap.py +0 -0
  84. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/ui/modals.py +0 -0
  85. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/ui/projects_pane.py +0 -0
  86. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/ui/running_pane.py +0 -0
  87. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/ui/sessions_pane.py +0 -0
  88. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/ui/sidebar.py +0 -0
  89. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/ui/statusbar.py +0 -0
  90. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/ui/workspace.py +0 -0
  91. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/windows_bootstrap.py +0 -0
  92. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/windows_install_log.py +0 -0
  93. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/windows_msys2.py +0 -0
  94. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/windows_pacman.py +0 -0
  95. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/windows_paths.py +0 -0
  96. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/windows_tmux_lifecycle.py +0 -0
  97. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux/windows_ui_transition.py +0 -0
  98. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux.egg-info/dependency_links.txt +0 -0
  99. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux.egg-info/entry_points.txt +0 -0
  100. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux.egg-info/requires.txt +0 -0
  101. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/src/railmux.egg-info/top_level.txt +0 -0
  102. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/app_test_harness.py +0 -0
  103. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_agent_workspace.py +0 -0
  104. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_app_codex_placeholder.py +0 -0
  105. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_app_quit.py +0 -0
  106. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_app_restart_restore.py +0 -0
  107. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_app_running_recovery.py +0 -0
  108. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_atomic_file.py +0 -0
  109. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_background_index.py +0 -0
  110. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_cli.py +0 -0
  111. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_code_map.py +0 -0
  112. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_codex_delete.py +0 -0
  113. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_codex_index.py +0 -0
  114. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_codex_lineage.py +0 -0
  115. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_codex_transcript.py +0 -0
  116. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_config.py +0 -0
  117. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_config_cli.py +0 -0
  118. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_denested_transport_benchmark.py +0 -0
  119. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_diagnostics.py +0 -0
  120. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_discovery.py +0 -0
  121. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_display_transport.py +0 -0
  122. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_fast_display_bootstrap.py +0 -0
  123. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_fast_display_history.py +0 -0
  124. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_fast_display_terminal.py +0 -0
  125. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_focus_highlight.py +0 -0
  126. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_fullscreen_binding.py +0 -0
  127. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_fuzzy.py +0 -0
  128. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_help_agent.py +0 -0
  129. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_help_workspace.py +0 -0
  130. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_history_cleanup.py +0 -0
  131. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_keymap.py +0 -0
  132. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_launcher.py +0 -0
  133. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_layout_profile.py +0 -0
  134. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_legacy_sessions.py +0 -0
  135. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_local_clipboard.py +0 -0
  136. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_local_open.py +0 -0
  137. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_modals.py +0 -0
  138. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_mode_selection.py +0 -0
  139. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_modes.py +0 -0
  140. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_mouse_manager.py +0 -0
  141. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_new_project.py +0 -0
  142. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_options.py +0 -0
  143. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_orphan_recovery.py +0 -0
  144. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_pane_surface.py +0 -0
  145. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_path_codec.py +0 -0
  146. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_preview_pager.py +0 -0
  147. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_project_favorites.py +0 -0
  148. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_projects_pane.py +0 -0
  149. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_provider_paths.py +0 -0
  150. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_release_notes.py +0 -0
  151. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_release_version.py +0 -0
  152. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_release_workflow.py +0 -0
  153. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_remote_config.py +0 -0
  154. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_renames.py +0 -0
  155. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_restart_state.py +0 -0
  156. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_running_pane.py +0 -0
  157. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_running_sort.py +0 -0
  158. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_runtime_config.py +0 -0
  159. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_scroll_agent.py +0 -0
  160. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_scroll_manager.py +0 -0
  161. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_selection_isolation.py +0 -0
  162. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_self_update.py +0 -0
  163. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_session_cache.py +0 -0
  164. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_session_index.py +0 -0
  165. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_session_lease.py +0 -0
  166. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_sessions_pane.py +0 -0
  167. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_setting_contracts.py +0 -0
  168. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_settings.py +0 -0
  169. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_sidebar.py +0 -0
  170. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_ssh_args.py +0 -0
  171. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_ssh_compat.py +0 -0
  172. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_ssh_display_benchmark.py +0 -0
  173. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_ssh_display_diagnostics.py +0 -0
  174. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_ssh_doctor.py +0 -0
  175. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_status.py +0 -0
  176. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_statusbar.py +0 -0
  177. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_system_deps.py +0 -0
  178. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_terminal_size.py +0 -0
  179. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_terminal_status.py +0 -0
  180. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_termux_local_touch.py +0 -0
  181. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_tmux_binding_manager.py +0 -0
  182. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_tmux_capabilities.py +0 -0
  183. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_tmux_ctl.py +0 -0
  184. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_tmux_health.py +0 -0
  185. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_tmux_integration.py +0 -0
  186. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_tmux_server.py +0 -0
  187. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_tmux_status.py +0 -0
  188. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_tool_panes.py +0 -0
  189. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_transcript.py +0 -0
  190. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_utf8_state.py +0 -0
  191. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_web_demo_recorder.py +0 -0
  192. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_wheel_smoke.py +0 -0
  193. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_widgets.py +0 -0
  194. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_windows_bootstrap.py +0 -0
  195. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_windows_install_log.py +0 -0
  196. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_windows_msys2.py +0 -0
  197. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_windows_pacman.py +0 -0
  198. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_windows_paths.py +0 -0
  199. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_windows_synchronized_screen.py +0 -0
  200. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_windows_tmux_lifecycle.py +0 -0
  201. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_windows_ui_transition.py +0 -0
  202. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_windows_wrapper_contract.py +0 -0
  203. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tests/test_yolo_prompt.py +0 -0
  204. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tools/denested_transport_benchmark.py +0 -0
  205. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tools/release_notes.py +0 -0
  206. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/tools/wheel_smoke.py +0 -0
  207. {railmux-0.4.1.dev5 → railmux-0.4.2.dev1}/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.1.dev5
3
+ Version: 0.4.2.dev1
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
@@ -662,7 +662,21 @@ Selection → "Applications in terminal may access clipboard"*.)
662
662
  > `Ctrl-B z` also toggles fullscreen (built into tmux) but zooms whichever
663
663
  > pane has focus — it may fullscreen the sidebar instead of the agent.
664
664
 
665
- ### 2. Mouse buttons or F8/F9 don't work — what's wrong?
665
+ ### 2. How do I paste an image into Codex or Claude Code?
666
+
667
+ With native Windows or `railmux ssh`, copy an image, focus the agent input,
668
+ then press **Ctrl+V**. If the terminal reserves Ctrl+V for text paste, use
669
+ **Alt+V**. Railmux transfers a bounded PNG to private storage visible to that
670
+ agent and pastes it into the current input without pressing Enter. Codex shows
671
+ its normal image attachment; Claude Code receives the image path it supports.
672
+
673
+ The shortcut is intercepted only when the local clipboard actually contains
674
+ an image and the current agent pane is still verified. Otherwise the original
675
+ key reaches Codex or Claude unchanged. macOS/Linux clipboard capture uses the
676
+ available native image reader (`osascript`/`pngpaste`, `wl-paste`, or `xclip`);
677
+ ordinary local macOS/Linux Railmux keeps the provider's native paste behavior.
678
+
679
+ ### 3. Mouse buttons or F8/F9 don't work — what's wrong?
666
680
 
667
681
  These are usually terminal‑side settings, not tmux or railmux.
668
682
 
@@ -710,7 +724,7 @@ has its own shortcut or key-mapping editor, remove the conflicting mapping or
710
724
  configure it to send the corresponding F8/F9 function-key sequence to the
711
725
  terminal session.
712
726
 
713
- ### 3. Using railmux over SSH
727
+ ### 4. Using railmux over SSH
714
728
 
715
729
  #### Choose a connection mode
716
730
 
@@ -856,7 +870,7 @@ do not kill tmux, the shared workspace, or its agents. See
856
870
  [the architecture document](https://github.com/Rightglow/Railmux/blob/main/docs/ARCHITECTURE.md) for protocol, lease,
857
871
  history-cache, and failure-containment details.
858
872
 
859
- ### 4. Will automated review sessions pollute my session list?
873
+ ### 5. Will automated review sessions pollute my session list?
860
874
 
861
875
  **Codex**: sessions created by `codex exec` (headless automation, pre‑commit
862
876
  hooks, CI) are filtered automatically — railmux only shows interactive
@@ -872,7 +886,7 @@ claude -p --no-session-persistence "review this diff"
872
886
 
873
887
  (`--no-session-persistence` is a Claude Code print-mode option.)
874
888
 
875
- ### 5. pip reports "externally-managed-environment"
889
+ ### 6. pip reports "externally-managed-environment"
876
890
 
877
891
  Create a virtual environment, then install Railmux with that environment's
878
892
  `pip`. This works on macOS, Linux, and WSL without modifying the system Python:
@@ -886,7 +900,7 @@ pip install railmux
886
900
  `pipx install railmux` is an optional convenience for a globally available CLI;
887
901
  it is not required on macOS or any other platform.
888
902
 
889
- ### 6. Can I open Railmux in multiple terminal windows?
903
+ ### 7. Can I open Railmux in multiple terminal windows?
890
904
 
891
905
  Yes, as multiple views of one shared workspace. Normal launches and current
892
906
  `railmux ssh` helpers may attach to the same managed `railmux` tmux session.
@@ -619,7 +619,21 @@ Selection → "Applications in terminal may access clipboard"*.)
619
619
  > `Ctrl-B z` also toggles fullscreen (built into tmux) but zooms whichever
620
620
  > pane has focus — it may fullscreen the sidebar instead of the agent.
621
621
 
622
- ### 2. Mouse buttons or F8/F9 don't work — what's wrong?
622
+ ### 2. How do I paste an image into Codex or Claude Code?
623
+
624
+ With native Windows or `railmux ssh`, copy an image, focus the agent input,
625
+ then press **Ctrl+V**. If the terminal reserves Ctrl+V for text paste, use
626
+ **Alt+V**. Railmux transfers a bounded PNG to private storage visible to that
627
+ agent and pastes it into the current input without pressing Enter. Codex shows
628
+ its normal image attachment; Claude Code receives the image path it supports.
629
+
630
+ The shortcut is intercepted only when the local clipboard actually contains
631
+ an image and the current agent pane is still verified. Otherwise the original
632
+ key reaches Codex or Claude unchanged. macOS/Linux clipboard capture uses the
633
+ available native image reader (`osascript`/`pngpaste`, `wl-paste`, or `xclip`);
634
+ ordinary local macOS/Linux Railmux keeps the provider's native paste behavior.
635
+
636
+ ### 3. Mouse buttons or F8/F9 don't work — what's wrong?
623
637
 
624
638
  These are usually terminal‑side settings, not tmux or railmux.
625
639
 
@@ -667,7 +681,7 @@ has its own shortcut or key-mapping editor, remove the conflicting mapping or
667
681
  configure it to send the corresponding F8/F9 function-key sequence to the
668
682
  terminal session.
669
683
 
670
- ### 3. Using railmux over SSH
684
+ ### 4. Using railmux over SSH
671
685
 
672
686
  #### Choose a connection mode
673
687
 
@@ -813,7 +827,7 @@ do not kill tmux, the shared workspace, or its agents. See
813
827
  [the architecture document](https://github.com/Rightglow/Railmux/blob/main/docs/ARCHITECTURE.md) for protocol, lease,
814
828
  history-cache, and failure-containment details.
815
829
 
816
- ### 4. Will automated review sessions pollute my session list?
830
+ ### 5. Will automated review sessions pollute my session list?
817
831
 
818
832
  **Codex**: sessions created by `codex exec` (headless automation, pre‑commit
819
833
  hooks, CI) are filtered automatically — railmux only shows interactive
@@ -829,7 +843,7 @@ claude -p --no-session-persistence "review this diff"
829
843
 
830
844
  (`--no-session-persistence` is a Claude Code print-mode option.)
831
845
 
832
- ### 5. pip reports "externally-managed-environment"
846
+ ### 6. pip reports "externally-managed-environment"
833
847
 
834
848
  Create a virtual environment, then install Railmux with that environment's
835
849
  `pip`. This works on macOS, Linux, and WSL without modifying the system Python:
@@ -843,7 +857,7 @@ pip install railmux
843
857
  `pipx install railmux` is an optional convenience for a globally available CLI;
844
858
  it is not required on macOS or any other platform.
845
859
 
846
- ### 6. Can I open Railmux in multiple terminal windows?
860
+ ### 7. Can I open Railmux in multiple terminal windows?
847
861
 
848
862
  Yes, as multiple views of one shared workspace. Normal launches and current
849
863
  `railmux ssh` helpers may attach to the same managed `railmux` tmux session.
@@ -0,0 +1 @@
1
+ __version__ = "0.4.2.dev1"
@@ -11,6 +11,7 @@ from __future__ import annotations
11
11
 
12
12
  import argparse
13
13
  import base64
14
+ import errno
14
15
  import os
15
16
  import re
16
17
  import select
@@ -22,12 +23,13 @@ import sys
22
23
  import termios
23
24
  import time
24
25
  import tty
26
+ from collections import deque
25
27
  from collections.abc import Callable, Mapping
26
28
  from dataclasses import dataclass
27
29
  from enum import Enum
28
30
  from typing import BinaryIO, Optional, Sequence
29
31
 
30
- from railmux import __version__, local_clipboard, local_open
32
+ from railmux import __version__, image_paste, local_clipboard, local_open
31
33
  from railmux.config import (
32
34
  ConfigError,
33
35
  load_config,
@@ -59,6 +61,8 @@ from railmux.fast_display_protocol import (
59
61
  ClaudeHistoryPolicyResult,
60
62
  HistoryBatch,
61
63
  HistorySnapshot,
64
+ ImagePasteResult,
65
+ MAX_IMAGE_CHUNK_BYTES,
62
66
  PathKind,
63
67
  PathOpenResult,
64
68
  PathResult,
@@ -74,6 +78,9 @@ from railmux.fast_display_protocol import (
74
78
  encode_heartbeat,
75
79
  encode_claude_history_policy,
76
80
  encode_input,
81
+ encode_image_begin,
82
+ encode_image_chunk,
83
+ encode_image_commit,
77
84
  encode_keyframe_request,
78
85
  encode_path_request,
79
86
  encode_path_open_request,
@@ -120,9 +127,108 @@ _WINDOWS_HISTORY_WHEEL_LINES = 3
120
127
  _SELECTION_HIGHLIGHT_SECONDS = 2.0
121
128
  _CLICK_FLASH_SECONDS = 0.18
122
129
  _PATH_OPEN_TIMEOUT = 5.0
130
+ _IMAGE_WRITER_LOW_WATER = 96 * 1024
131
+ _PROTOCOL_WRITER_MAX_BYTES = 512 * 1024
132
+ _IMAGE_BUFFERED_INPUT_LIMIT = 64 * 1024
123
133
  _TERMUX_TOUCH_HINT = "Tap the prompt again to open the keyboard"
124
134
  _REMOTE_ATTACH_TIMEOUT = 30.0
125
135
  _REMOTE_INSTALL_TIMEOUT = 300.0
136
+
137
+
138
+ class _OutboundProtocolWriter:
139
+ """Nonblocking ordered writes so image upload cannot stall rendering."""
140
+
141
+ def __init__(self, stream: BinaryIO) -> None:
142
+ self._queue: deque[bytes] = deque()
143
+ self._offset = 0
144
+ self._pending_bytes = 0
145
+ self._fd = -1
146
+ self.failed = False
147
+ self.replace(stream)
148
+
149
+ @property
150
+ def pending_bytes(self) -> int:
151
+ return self._pending_bytes
152
+
153
+ def replace(self, stream: BinaryIO) -> None:
154
+ self._queue.clear()
155
+ self._offset = 0
156
+ self._pending_bytes = 0
157
+ self._fd = stream.fileno()
158
+ self.failed = False
159
+ os.set_blocking(self._fd, False)
160
+
161
+ def enqueue(self, frame: bytes) -> None:
162
+ if not frame or self.failed:
163
+ return
164
+ if self._pending_bytes + len(frame) > _PROTOCOL_WRITER_MAX_BYTES:
165
+ self.failed = True
166
+ return
167
+ self._queue.append(frame)
168
+ self._pending_bytes += len(frame)
169
+
170
+ def flush(self, budget: int = 256 * 1024) -> None:
171
+ written_total = 0
172
+ while self._queue and written_total < budget and not self.failed:
173
+ frame = self._queue[0]
174
+ try:
175
+ written = os.write(self._fd, frame[self._offset :])
176
+ except BlockingIOError:
177
+ return
178
+ except OSError as exc:
179
+ if exc.errno in (errno.EPIPE, errno.EBADF):
180
+ self.failed = True
181
+ return
182
+ raise
183
+ if written <= 0:
184
+ return
185
+ written_total += written
186
+ self._pending_bytes -= written
187
+ self._offset += written
188
+ if self._offset == len(frame):
189
+ self._queue.popleft()
190
+ self._offset = 0
191
+
192
+
193
+ @dataclass
194
+ class _ClientImageUpload:
195
+ request_id: int
196
+ pane_id: str
197
+ generation: int
198
+ image: image_paste.ClipboardImage
199
+ offset: int = 0
200
+ begun: bool = False
201
+ committed: bool = False
202
+
203
+ def pump(self, writer: _OutboundProtocolWriter) -> None:
204
+ if self.committed:
205
+ return
206
+ if not self.begun:
207
+ writer.enqueue(
208
+ encode_image_begin(
209
+ self.request_id,
210
+ self.pane_id,
211
+ self.generation,
212
+ len(self.image.data),
213
+ self.image.digest,
214
+ )
215
+ )
216
+ self.begun = True
217
+ while (
218
+ self.offset < len(self.image.data)
219
+ and writer.pending_bytes < _IMAGE_WRITER_LOW_WATER
220
+ ):
221
+ chunk = self.image.data[
222
+ self.offset : self.offset + MAX_IMAGE_CHUNK_BYTES
223
+ ]
224
+ writer.enqueue(encode_image_chunk(self.request_id, self.offset, chunk))
225
+ self.offset += len(chunk)
226
+ if (
227
+ self.offset == len(self.image.data)
228
+ and writer.pending_bytes < _IMAGE_WRITER_LOW_WATER
229
+ ):
230
+ writer.enqueue(encode_image_commit(self.request_id))
231
+ self.committed = True
126
232
  _FIRST_FRAME_TIMEOUT = 30.0
127
233
  _REMOTE_ATTACH_RETRY_DELAY = 0.2
128
234
  _HEARTBEAT_INTERVAL = 5.0
@@ -1031,7 +1137,7 @@ class TerminalSurface:
1031
1137
  self,
1032
1138
  requested: TerminalMode,
1033
1139
  ) -> TerminalMode:
1034
- """Mirror only input-affecting modes explicitly carried by protocol v16."""
1140
+ """Mirror only input-affecting modes explicitly carried by protocol v17."""
1035
1141
  disabled = self.terminal_modes & ~requested
1036
1142
  enabled = requested & ~self.terminal_modes
1037
1143
  controls: list[bytes] = []
@@ -1935,7 +2041,7 @@ def _finish_remote_attach(
1935
2041
  raise ProbeError("remote display helper failed before attaching")
1936
2042
 
1937
2043
  _stop_unstarted_remote(process)
1938
- # A current v16 helper holds the mutex only while registering its exact tmux
2044
+ # A current v17 helper holds the mutex only while registering its exact tmux
1939
2045
  # child. Give that ordinary race one fresh SSH process before presenting
1940
2046
  # the explicit legacy-lock takeover choice.
1941
2047
  time.sleep(_REMOTE_ATTACH_RETRY_DELAY)
@@ -2442,17 +2548,84 @@ def run(args: argparse.Namespace) -> int:
2442
2548
  path_open_prompt_mouse_button: int | None = None
2443
2549
  local_status_mouse_button: int | None = None
2444
2550
  periodic_prefetch = PeriodicPrefetchGate()
2551
+ protocol_writer = _OutboundProtocolWriter(process.stdin)
2552
+ image_request_id = 0
2553
+ image_upload: _ClientImageUpload | None = None
2554
+ image_buffered_input: list[bytes] = []
2555
+ image_buffered_bytes = 0
2445
2556
 
2446
2557
  def send_protocol_frame(frame: bytes) -> None:
2447
2558
  nonlocal remote_closed
2448
2559
  if remote_closed:
2449
2560
  return
2450
- try:
2451
- process.stdin.write(frame)
2452
- process.stdin.flush()
2453
- except BrokenPipeError:
2561
+ protocol_writer.enqueue(frame)
2562
+ if protocol_writer.failed:
2454
2563
  remote_closed = True
2455
2564
 
2565
+ def begin_image_paste() -> bool:
2566
+ nonlocal image_request_id, image_upload, history_info_until
2567
+ if image_upload is not None:
2568
+ surface.show_local_status(
2569
+ "Another image paste is still uploading",
2570
+ level="warning",
2571
+ )
2572
+ history_info_until = time.monotonic() + _HISTORY_INFO_SECONDS
2573
+ return True
2574
+ if latest_screen is None:
2575
+ return False
2576
+ pane_id = history.pane_id_at_position(
2577
+ latest_screen.cursor_x,
2578
+ latest_screen.cursor_y,
2579
+ )
2580
+ route = next(
2581
+ (
2582
+ candidate
2583
+ for candidate in history.visible_routes
2584
+ if candidate.pane_id == pane_id
2585
+ ),
2586
+ None,
2587
+ )
2588
+ if route is None or route.route_generation == 0:
2589
+ return False
2590
+ try:
2591
+ image = image_paste.read_clipboard_png()
2592
+ except image_paste.ImagePasteError as exc:
2593
+ surface.show_local_status(str(exc), level="warning")
2594
+ history_info_until = time.monotonic() + _HISTORY_INFO_SECONDS
2595
+ return False
2596
+ if image is None:
2597
+ return False
2598
+ image_request_id = (image_request_id + 1) & 0xFFFFFFFF
2599
+ if image_request_id == 0:
2600
+ image_request_id = 1
2601
+ image_upload = _ClientImageUpload(
2602
+ image_request_id,
2603
+ route.pane_id,
2604
+ route.route_generation,
2605
+ image,
2606
+ )
2607
+ size_mib = len(image.data) / (1024 * 1024)
2608
+ surface.show_local_status(
2609
+ f"Uploading {image.width}×{image.height} image ({size_mib:.1f} MiB)…"
2610
+ )
2611
+ history_info_until = None
2612
+ return True
2613
+
2614
+ def buffer_provider_input(data: bytes) -> bool:
2615
+ nonlocal image_buffered_bytes, history_info_until
2616
+ if image_upload is None:
2617
+ return False
2618
+ if image_buffered_bytes + len(data) > _IMAGE_BUFFERED_INPUT_LIMIT:
2619
+ surface.show_local_status(
2620
+ "Image upload is still finishing; additional input was not sent",
2621
+ level="warning",
2622
+ )
2623
+ history_info_until = time.monotonic() + _HISTORY_INFO_SECONDS
2624
+ return True
2625
+ image_buffered_input.append(data)
2626
+ image_buffered_bytes += len(data)
2627
+ return True
2628
+
2456
2629
  def show_open_result(result: local_open.OpenResult) -> None:
2457
2630
  nonlocal history_info_until
2458
2631
  if result.copy_data is not None:
@@ -2881,6 +3054,16 @@ def run(args: argparse.Namespace) -> int:
2881
3054
  return
2882
3055
  if not part:
2883
3056
  return
3057
+ if not opaque_paste and isinstance(part, bytes):
3058
+ image_parts = image_paste.split_shortcuts(part)
3059
+ if len(image_parts) > 1:
3060
+ for image_part in image_parts:
3061
+ handle_terminal_part(image_part, input_batch)
3062
+ return
3063
+ if part in image_paste.IMAGE_PASTE_KEYS and begin_image_paste():
3064
+ restore = history.cancel()
3065
+ apply_history_action(HistoryAction(restore_live=restore))
3066
+ return
2884
3067
  if not opaque_paste and touch_keyboard.consumes_focus_out(part):
2885
3068
  # Opening Android's input view can briefly remove focus from the
2886
3069
  # terminal View even though the user is still interacting with
@@ -2965,7 +3148,8 @@ def run(args: argparse.Namespace) -> int:
2965
3148
  # types. Reassert only the cached cursor on Termux focus-in; the
2966
3149
  # remote focus event remains authoritative and is still forwarded.
2967
3150
  surface.reassert_cursor()
2968
- send_protocol_frame(encode_input(part))
3151
+ if not buffer_provider_input(part):
3152
+ send_protocol_frame(encode_input(part))
2969
3153
 
2970
3154
  surface.show_local_status(
2971
3155
  "Ctrl-] disconnects · Ctrl-B d detaches · "
@@ -2982,6 +3166,11 @@ def run(args: argparse.Namespace) -> int:
2982
3166
  ) as windows_interrupts,
2983
3167
  ):
2984
3168
  while True:
3169
+ if image_upload is not None:
3170
+ image_upload.pump(protocol_writer)
3171
+ protocol_writer.flush()
3172
+ if protocol_writer.failed:
3173
+ remote_closed = True
2985
3174
  pending_interrupts = windows_interrupts.consume()
2986
3175
  if pending_interrupts:
2987
3176
  input_batch = _LocalInputBatch(history_paint)
@@ -3166,6 +3355,25 @@ def run(args: argparse.Namespace) -> int:
3166
3355
  time.monotonic() + _HISTORY_INFO_SECONDS
3167
3356
  )
3168
3357
  continue
3358
+ if isinstance(message, ImagePasteResult):
3359
+ if (
3360
+ image_upload is None
3361
+ or message.request_id != image_upload.request_id
3362
+ ):
3363
+ continue
3364
+ image_upload = None
3365
+ surface.show_local_status(
3366
+ message.message,
3367
+ level=message.level,
3368
+ )
3369
+ history_info_until = (
3370
+ time.monotonic() + _HISTORY_INFO_SECONDS
3371
+ )
3372
+ for buffered in image_buffered_input:
3373
+ send_protocol_frame(encode_input(buffered))
3374
+ image_buffered_input.clear()
3375
+ image_buffered_bytes = 0
3376
+ continue
3169
3377
  if isinstance(message, HistoryBatch):
3170
3378
  accepted_prefetch_id = history.prefetch_pending_id
3171
3379
  action = history.accept_prefetch(message)
@@ -3320,6 +3528,11 @@ def run(args: argparse.Namespace) -> int:
3320
3528
  now=time.monotonic(),
3321
3529
  )
3322
3530
  now = time.monotonic()
3531
+ if image_upload is not None:
3532
+ image_upload.pump(protocol_writer)
3533
+ protocol_writer.flush()
3534
+ if protocol_writer.failed:
3535
+ remote_closed = True
3323
3536
  history_paint.flush(
3324
3537
  surface,
3325
3538
  latest_screen,
@@ -3430,7 +3643,7 @@ def run(args: argparse.Namespace) -> int:
3430
3643
  history_info_until = now + _HISTORY_INFO_SECONDS
3431
3644
  if local_exit:
3432
3645
  break
3433
- connection_ended = remote_closed or (
3646
+ connection_ended = remote_closed or protocol_writer.failed or (
3434
3647
  process.poll() is not None and not events
3435
3648
  )
3436
3649
  if connection_ended:
@@ -3467,6 +3680,11 @@ def run(args: argparse.Namespace) -> int:
3467
3680
  selectors.EVENT_READ,
3468
3681
  "remote",
3469
3682
  )
3683
+ image_was_cancelled = image_upload is not None
3684
+ image_upload = None
3685
+ image_buffered_input.clear()
3686
+ image_buffered_bytes = 0
3687
+ protocol_writer.replace(process.stdin)
3470
3688
  decoder = ServerMessageDecoder()
3471
3689
  model = ScreenModel()
3472
3690
  terminal_input = TerminalInputDecoder()
@@ -3510,7 +3728,12 @@ def run(args: argparse.Namespace) -> int:
3510
3728
  reconnect_metrics["successes"] += 1
3511
3729
  periodic_prefetch.reset()
3512
3730
  surface.show_local_status(
3513
- "Reconnected; waiting for a fresh screen"
3731
+ (
3732
+ "Reconnected; image paste was cancelled safely"
3733
+ if image_was_cancelled
3734
+ else "Reconnected; waiting for a fresh screen"
3735
+ ),
3736
+ level="warning" if image_was_cancelled else "info",
3514
3737
  )
3515
3738
  continue
3516
3739
  break
@@ -1199,7 +1199,7 @@ class LocalHistoryView:
1199
1199
  # preserves the strongest ambiguity check when the live pane did not
1200
1200
  # change while the deeper capture was in flight.
1201
1201
  # KMP keeps this compatibility path linear even for long repeated
1202
- # captures. Protocol-v16 coordinates normally bypass it, but bounded
1202
+ # captures. Protocol-v17 coordinates normally bypass it, but bounded
1203
1203
  # transcript tails and older peers still require exact content proof.
1204
1204
  prefix = [0] * len(anchor)
1205
1205
  matched = 0
@@ -1273,7 +1273,7 @@ class LocalHistoryView:
1273
1273
  incoming: HistorySnapshot,
1274
1274
  viewport: _HistoryViewport,
1275
1275
  ) -> int | None:
1276
- """Map a frozen viewport through v16 absolute timeline coordinates."""
1276
+ """Map a frozen viewport through v17 absolute timeline coordinates."""
1277
1277
  previous = viewport.snapshot
1278
1278
  if not cls._has_timeline(previous) or not cls._has_timeline(incoming):
1279
1279
  return None