railmux 0.4.2.dev6__tar.gz → 0.4.2.dev8__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 (208) hide show
  1. {railmux-0.4.2.dev6/src/railmux.egg-info → railmux-0.4.2.dev8}/PKG-INFO +1 -1
  2. railmux-0.4.2.dev8/src/railmux/__init__.py +1 -0
  3. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/fast_display_server.py +31 -6
  4. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/transcript.py +76 -21
  5. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8/src/railmux.egg-info}/PKG-INFO +1 -1
  6. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_fast_display_server.py +16 -0
  7. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_transcript.py +52 -7
  8. railmux-0.4.2.dev6/src/railmux/__init__.py +0 -1
  9. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/LICENSE +0 -0
  10. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/MANIFEST.in +0 -0
  11. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/README.md +0 -0
  12. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/pyproject.toml +0 -0
  13. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/setup.cfg +0 -0
  14. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/__main__.py +0 -0
  15. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/atomic_file.py +0 -0
  16. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/background_index.py +0 -0
  17. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/cli.py +0 -0
  18. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/codex_index.py +0 -0
  19. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/config.py +0 -0
  20. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/config_cli.py +0 -0
  21. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/diagnostic_contract.py +0 -0
  22. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/diagnostics.py +0 -0
  23. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/discovery.py +0 -0
  24. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/display_transport.py +0 -0
  25. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/entrypoint.py +0 -0
  26. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/fast_display_client.py +0 -0
  27. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/fast_display_history.py +0 -0
  28. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/fast_display_input.py +0 -0
  29. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/fast_display_protocol.py +0 -0
  30. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/favorites.py +0 -0
  31. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/fuzzy.py +0 -0
  32. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/help_workspace.py +0 -0
  33. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/image_paste.py +0 -0
  34. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/launcher.py +0 -0
  35. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/legacy_sessions.py +0 -0
  36. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/local_clipboard.py +0 -0
  37. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/local_open.py +0 -0
  38. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/models.py +0 -0
  39. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/modes.py +0 -0
  40. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/mouse_manager.py +0 -0
  41. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/orphan_marker.py +0 -0
  42. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/pane_surface.py +0 -0
  43. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/path_codec.py +0 -0
  44. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/preview_pager.py +0 -0
  45. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/project_favorites.py +0 -0
  46. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/provider_paths.py +0 -0
  47. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/release_version.py +0 -0
  48. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/remote_config.py +0 -0
  49. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/renames.py +0 -0
  50. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/restart_state.py +0 -0
  51. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/runtime_config.py +0 -0
  52. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/scroll_agent.py +0 -0
  53. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/scroll_manager.py +0 -0
  54. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/selection_isolation.py +0 -0
  55. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/self_update.py +0 -0
  56. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/session_cache.py +0 -0
  57. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/session_index.py +0 -0
  58. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/session_lease.py +0 -0
  59. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/setting_contracts.py +0 -0
  60. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/settings.py +0 -0
  61. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/ssh_args.py +0 -0
  62. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/ssh_compat.py +0 -0
  63. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/ssh_display_diagnostics.py +0 -0
  64. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/ssh_doctor.py +0 -0
  65. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/ssh_preflight.py +0 -0
  66. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/system_deps.py +0 -0
  67. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/terminal_screen.py +0 -0
  68. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/terminal_status.py +0 -0
  69. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/tmux_binding_manager.py +0 -0
  70. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/tmux_capabilities.py +0 -0
  71. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/tmux_ctl.py +0 -0
  72. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/tmux_health.py +0 -0
  73. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/tmux_server.py +0 -0
  74. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/tool_panes.py +0 -0
  75. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/ui/__init__.py +0 -0
  76. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/ui/_widgets.py +0 -0
  77. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/ui/app.py +0 -0
  78. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/ui/keymap.py +0 -0
  79. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/ui/modals.py +0 -0
  80. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/ui/projects_pane.py +0 -0
  81. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/ui/running_pane.py +0 -0
  82. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/ui/sessions_pane.py +0 -0
  83. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/ui/sidebar.py +0 -0
  84. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/ui/statusbar.py +0 -0
  85. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/ui/workspace.py +0 -0
  86. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/windows_attach_relay.py +0 -0
  87. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/windows_bootstrap.py +0 -0
  88. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/windows_install_log.py +0 -0
  89. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/windows_msys2.py +0 -0
  90. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/windows_pacman.py +0 -0
  91. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/windows_paths.py +0 -0
  92. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/windows_tmux_lifecycle.py +0 -0
  93. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux/windows_ui_transition.py +0 -0
  94. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux.egg-info/SOURCES.txt +0 -0
  95. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux.egg-info/dependency_links.txt +0 -0
  96. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux.egg-info/entry_points.txt +0 -0
  97. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux.egg-info/requires.txt +0 -0
  98. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/src/railmux.egg-info/top_level.txt +0 -0
  99. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/app_test_harness.py +0 -0
  100. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_agent_workspace.py +0 -0
  101. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_app_codex_placeholder.py +0 -0
  102. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_app_quit.py +0 -0
  103. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_app_restart_restore.py +0 -0
  104. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_app_running_recovery.py +0 -0
  105. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_atomic_file.py +0 -0
  106. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_background_index.py +0 -0
  107. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_claude_continuation.py +0 -0
  108. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_cli.py +0 -0
  109. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_code_map.py +0 -0
  110. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_codex_delete.py +0 -0
  111. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_codex_index.py +0 -0
  112. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_codex_lineage.py +0 -0
  113. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_codex_transcript.py +0 -0
  114. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_config.py +0 -0
  115. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_config_cli.py +0 -0
  116. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_denested_transport_benchmark.py +0 -0
  117. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_diagnostics.py +0 -0
  118. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_discovery.py +0 -0
  119. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_display_transport.py +0 -0
  120. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_fast_display_bootstrap.py +0 -0
  121. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_fast_display_history.py +0 -0
  122. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_fast_display_protocol.py +0 -0
  123. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_fast_display_selection.py +0 -0
  124. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_fast_display_terminal.py +0 -0
  125. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_focus_highlight.py +0 -0
  126. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_fullscreen_binding.py +0 -0
  127. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_fuzzy.py +0 -0
  128. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_help_agent.py +0 -0
  129. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_help_workspace.py +0 -0
  130. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_history_cleanup.py +0 -0
  131. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_image_paste.py +0 -0
  132. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_keymap.py +0 -0
  133. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_launcher.py +0 -0
  134. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_layout_profile.py +0 -0
  135. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_legacy_sessions.py +0 -0
  136. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_local_clipboard.py +0 -0
  137. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_local_open.py +0 -0
  138. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_modals.py +0 -0
  139. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_mode_selection.py +0 -0
  140. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_modes.py +0 -0
  141. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_mouse_manager.py +0 -0
  142. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_new_project.py +0 -0
  143. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_options.py +0 -0
  144. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_orphan_recovery.py +0 -0
  145. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_pane_surface.py +0 -0
  146. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_path_codec.py +0 -0
  147. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_preview_pager.py +0 -0
  148. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_project_favorites.py +0 -0
  149. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_projects_pane.py +0 -0
  150. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_provider_paths.py +0 -0
  151. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_release_notes.py +0 -0
  152. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_release_version.py +0 -0
  153. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_release_workflow.py +0 -0
  154. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_remote_config.py +0 -0
  155. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_renames.py +0 -0
  156. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_restart_state.py +0 -0
  157. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_running_pane.py +0 -0
  158. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_running_sort.py +0 -0
  159. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_runtime_config.py +0 -0
  160. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_scroll_agent.py +0 -0
  161. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_scroll_manager.py +0 -0
  162. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_selection_isolation.py +0 -0
  163. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_self_update.py +0 -0
  164. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_session_cache.py +0 -0
  165. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_session_index.py +0 -0
  166. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_session_lease.py +0 -0
  167. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_sessions_pane.py +0 -0
  168. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_setting_contracts.py +0 -0
  169. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_settings.py +0 -0
  170. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_sidebar.py +0 -0
  171. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_ssh_args.py +0 -0
  172. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_ssh_compat.py +0 -0
  173. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_ssh_display_benchmark.py +0 -0
  174. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_ssh_display_diagnostics.py +0 -0
  175. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_ssh_doctor.py +0 -0
  176. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_status.py +0 -0
  177. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_statusbar.py +0 -0
  178. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_system_deps.py +0 -0
  179. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_terminal_size.py +0 -0
  180. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_terminal_status.py +0 -0
  181. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_termux_local_touch.py +0 -0
  182. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_tmux_binding_manager.py +0 -0
  183. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_tmux_capabilities.py +0 -0
  184. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_tmux_ctl.py +0 -0
  185. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_tmux_health.py +0 -0
  186. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_tmux_integration.py +0 -0
  187. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_tmux_server.py +0 -0
  188. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_tmux_status.py +0 -0
  189. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_tool_panes.py +0 -0
  190. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_utf8_state.py +0 -0
  191. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_web_demo_recorder.py +0 -0
  192. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_wheel_smoke.py +0 -0
  193. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_widgets.py +0 -0
  194. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_windows_attach_relay.py +0 -0
  195. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_windows_bootstrap.py +0 -0
  196. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_windows_install_log.py +0 -0
  197. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_windows_msys2.py +0 -0
  198. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_windows_pacman.py +0 -0
  199. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_windows_paths.py +0 -0
  200. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_windows_synchronized_screen.py +0 -0
  201. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_windows_tmux_lifecycle.py +0 -0
  202. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_windows_ui_transition.py +0 -0
  203. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_windows_wrapper_contract.py +0 -0
  204. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tests/test_yolo_prompt.py +0 -0
  205. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tools/denested_transport_benchmark.py +0 -0
  206. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tools/release_notes.py +0 -0
  207. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/tools/wheel_smoke.py +0 -0
  208. {railmux-0.4.2.dev6 → railmux-0.4.2.dev8}/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.dev6
3
+ Version: 0.4.2.dev8
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.dev8"
@@ -89,6 +89,7 @@ from railmux.terminal_screen import (
89
89
  HyperlinkTrackingStream,
90
90
  Osc52ClipboardDecoder as _Osc52ClipboardDecoder,
91
91
  ScreenState as _ScreenState,
92
+ _safe_hyperlink,
92
93
  build_screen_update,
93
94
  extended_pyte as _extended_pyte,
94
95
  render_rows,
@@ -1616,11 +1617,17 @@ def _read_transcript_tail(fd: int) -> tuple[str, bool]:
1616
1617
  def _wrap_transcript_rows(
1617
1618
  pyte: object, text: str, width: int
1618
1619
  ) -> tuple[tuple[bytes, ...], bool, int]:
1619
- """Wrap allowlisted transcript ANSI into independently paintable rows."""
1620
+ """Wrap allowlisted transcript ANSI into independently paintable rows.
1621
+
1622
+ SGR styles and HTTP(S) OSC 8 links are kept; a link that crosses a row
1623
+ break is closed at the row end and reopened on the next row, as
1624
+ :func:`render_rows` emits links for captured screen rows.
1625
+ """
1620
1626
  rows: deque[bytes] = deque(maxlen=MAX_HISTORY_LINES)
1621
1627
  row = bytearray(b"\033[0m")
1622
1628
  column = 0
1623
1629
  active: list[bytes] = []
1630
+ link = b""
1624
1631
  dropped = False
1625
1632
  total_rows = 0
1626
1633
 
@@ -1629,12 +1636,28 @@ def _wrap_transcript_rows(
1629
1636
  total_rows += 1
1630
1637
  if len(rows) == rows.maxlen:
1631
1638
  dropped = True
1632
- rows.append(bytes(row) + b"\033[0m")
1633
- row = bytearray(b"\033[0m" + b"".join(active))
1639
+ rows.append(bytes(row) + (_LINK_END if link else b"") + b"\033[0m")
1640
+ row = bytearray(b"\033[0m" + b"".join(active) + link)
1634
1641
  column = 0
1635
1642
 
1636
- for match in re.finditer(r"\x1b\[[0-9;]*m|[^\x1b]+", text):
1643
+ for match in re.finditer(
1644
+ r"\x1b\[[0-9;]*m|\x1b\]8;;([^\x1b]*)\x1b\\|[^\x1b]+", text
1645
+ ):
1637
1646
  token = match.group(0)
1647
+ if token.startswith("\x1b]"):
1648
+ target = match.group(1)
1649
+ if target and _safe_hyperlink(target.encode("utf-8")) is None:
1650
+ continue
1651
+ encoded = token.encode("utf-8")
1652
+ if link and target:
1653
+ row.extend(_LINK_END)
1654
+ if target:
1655
+ link = encoded
1656
+ row.extend(encoded)
1657
+ elif link:
1658
+ link = b""
1659
+ row.extend(_LINK_END)
1660
+ continue
1638
1661
  if token.startswith("\x1b["):
1639
1662
  encoded = token.encode("ascii")
1640
1663
  params = token[2:-1].split(";")
@@ -1672,7 +1695,7 @@ def _wrap_transcript_rows(
1672
1695
  finish()
1673
1696
  row.extend(char.encode("utf-8"))
1674
1697
  column += max(0, cell_width)
1675
- if column or len(row) > len(b"\033[0m" + b"".join(active)):
1698
+ if column or len(row) > len(b"\033[0m" + b"".join(active) + link):
1676
1699
  finish()
1677
1700
  return tuple(rows), dropped, total_rows
1678
1701
 
@@ -1750,7 +1773,9 @@ def _transcript_rows(
1750
1773
  os.close(fd)
1751
1774
 
1752
1775
 
1753
- _OVERLAP_STYLE_RE = re.compile(rb"\x1b\[[0-9;]*m")
1776
+ _LINK_END = b"\033]8;;\033\\"
1777
+ _OVERLAP_STYLE_RE = re.compile(
1778
+ rb"\x1b\[[0-9;]*m|\x1b\]8;;[^\x1b]*\x1b\\")
1754
1779
  _OVERLAP_SPACE_RE = re.compile(r"\s+")
1755
1780
  _OVERLAP_ANCHOR_CHARS = 12
1756
1781
  _OVERLAP_SINGLE_ANCHOR_CHARS = 24
@@ -13,6 +13,7 @@ import os
13
13
  import re
14
14
  import sys
15
15
  from pathlib import Path
16
+ from urllib.parse import urlsplit
16
17
 
17
18
  # ── ANSI styles ──────────────────────────────────────────────────────────
18
19
  BOLD = "\033[1m"
@@ -28,6 +29,7 @@ ITALIC = "\033[3m"
28
29
  BOLD_OFF = "\033[22m"
29
30
  ITALIC_OFF = "\033[23m"
30
31
  FG_DEFAULT = "\033[39m"
32
+ LINK_END = "\033]8;;\033\\"
31
33
 
32
34
  _CODEX_SYNTHETIC_PREFIXES = (
33
35
  "<environment_context>",
@@ -235,13 +237,16 @@ _MD_INLINE_RE = re.compile(
235
237
  r"|\*\*(?=\S)(.+?)(?<=\S)\*\*"
236
238
  r"|__(?=\S)(.+?)(?<=\S)__"
237
239
  r"|(?<![\w*])\*(?=[^\s*])(.+?)(?<=[^\s*])\*(?![\w*])"
240
+ r"|\[([^\]\n]+)\]\(([^)\s]+)\)"
238
241
  )
239
242
  _MD_TABLE_SEPARATOR_RE = re.compile(
240
243
  r"^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?\s*$")
241
244
  _MD_TABLE_CELL_SPLIT_RE = re.compile(r"(?<!\\)\|")
242
245
  _LIST_HANG_RE = re.compile(r" *(?:(?:[-*+]|\d+[.)]) +)?")
243
- _SGR_RE = re.compile(r"\x1b\[[0-9;]*m")
244
- _WRAP_TOKEN_RE = re.compile(r"\x1b\[[0-9;]*m| +|[^ \x1b]+")
246
+ # Zero-width escapes Railmux emits itself: SGR styles and OSC 8 links.
247
+ _ESCAPE_RE = re.compile(r"\x1b\[[0-9;]*m|\x1b\]8;;[^\x1b]*\x1b\\")
248
+ _WRAP_TOKEN_RE = re.compile(
249
+ r"\x1b\[[0-9;]*m|\x1b\]8;;[^\x1b]*\x1b\\| +|[^ \x1b]+")
245
250
  _TASK_SUMMARY_RE = re.compile(r"<summary>(.*?)</summary>", re.DOTALL)
246
251
 
247
252
  _char_width_function = None
@@ -267,7 +272,7 @@ def _visible_width(text: str) -> int:
267
272
  # Claude pads an emoji-presentation selector (as in "⚠️") to two cells.
268
273
  return sum(
269
274
  1 if char == "\ufe0f" else _char_width(char)
270
- for char in _SGR_RE.sub("", text)
275
+ for char in _ESCAPE_RE.sub("", text)
271
276
  )
272
277
 
273
278
 
@@ -305,12 +310,16 @@ def _wrap_styled(line: str, width: int, hang: int) -> list[str]:
305
310
  else:
306
311
  newline()
307
312
  continue
308
- if (
309
- column + token_width > width
310
- and column > hang
311
- and token_width <= width - hang
312
- ):
313
- newline()
313
+ capacity = width - hang
314
+ if column + token_width > width and column > hang:
315
+ # Like wrap-ansi: start a word on a fresh row when that fits it,
316
+ # or when splitting it from there needs fewer breaks.
317
+ breaks_here = 1 + (token_width - (width - column) - 1) // capacity
318
+ if (
319
+ token_width <= capacity
320
+ or (token_width - 1) // capacity < breaks_here
321
+ ):
322
+ newline()
314
323
  for char in token:
315
324
  char_width = _char_width(char)
316
325
  if column + char_width > width and column > hang:
@@ -323,29 +332,49 @@ def _wrap_styled(line: str, width: int, hang: int) -> list[str]:
323
332
 
324
333
  def _hang_for(row: str, prefix: int) -> int:
325
334
  """Hanging indent for a row whose own content starts after *prefix* cells."""
326
- content = _SGR_RE.sub("", row)[prefix:]
335
+ content = _ESCAPE_RE.sub("", row)[prefix:]
327
336
  return prefix + len(_LIST_HANG_RE.match(content).group(0))
328
337
 
329
338
 
330
339
  def _close_fragments(fragments: list[str]) -> list[str]:
331
- """Make each wrapped fragment carry and close its own SGR state.
340
+ """Make each wrapped fragment carry and close its own SGR and link state.
332
341
 
333
- Table cells sit beside other cells on one row, so a style that continues
334
- onto a cell's next fragment must not leak across the borders between.
342
+ Table cells sit beside other cells on one row, so a style or link that
343
+ continues onto a cell's next fragment must not leak across the borders.
335
344
  """
336
345
  closed: list[str] = []
337
346
  active: list[str] = []
347
+ link = ""
338
348
  for fragment in fragments:
339
- prefix = "".join(active)
340
- for code in _SGR_RE.findall(fragment):
341
- if code == RESET:
349
+ prefix = "".join(active) + link
350
+ for code in _ESCAPE_RE.findall(fragment):
351
+ if code.startswith("\x1b]"):
352
+ link = "" if code == LINK_END else code
353
+ elif code == RESET:
342
354
  active.clear()
343
355
  else:
344
356
  active.append(code)
345
- closed.append(f"{prefix}{fragment}{RESET}")
357
+ suffix = LINK_END if link else ""
358
+ closed.append(f"{prefix}{fragment}{suffix}{RESET}")
346
359
  return closed
347
360
 
348
361
 
362
+ def _link(text: str, url: str) -> str:
363
+ """Wrap *text* in an OSC 8 link when *url* is a plain HTTP(S) target."""
364
+ try:
365
+ parsed = urlsplit(url)
366
+ valid = (
367
+ parsed.scheme.lower() in ("http", "https")
368
+ and parsed.hostname is not None
369
+ and len(url) <= 8192
370
+ )
371
+ except ValueError:
372
+ valid = False
373
+ if not valid:
374
+ return text
375
+ return f"\033]8;;{url}\033\\{text}{LINK_END}"
376
+
377
+
349
378
  def _table_cells(line: str) -> list[str]:
350
379
  stripped = line.strip()
351
380
  if stripped.startswith("|"):
@@ -373,15 +402,34 @@ def _markdown_table(lines: list[str], width: int) -> list[str] | None:
373
402
  for line in lines[2:]:
374
403
  cells = [_markdown_inline(cell) for cell in _table_cells(line)]
375
404
  body.append((cells + [""] * count)[:count])
376
- available = width - 1 - 3 * count
405
+ # Claude keeps two spare cells beside the borders and padding.
406
+ available = width - 3 - 3 * count
377
407
  if available < 3 * count:
378
408
  return None
409
+ rows = [header, *body]
379
410
  natural = [
380
- max(1, *(_visible_width(row[index]) for row in [header, *body]))
411
+ max(1, *(_visible_width(row[index]) for row in rows))
412
+ for index in range(count)
413
+ ]
414
+ longest_word = [
415
+ max(
416
+ _visible_width(word)
417
+ for row in rows
418
+ for word in [*_ESCAPE_RE.sub("", row[index]).split(" "), ""]
419
+ )
381
420
  for index in range(count)
382
421
  ]
422
+ minimum = [max(1, min(a, b)) for a, b in zip(natural, longest_word)]
383
423
  widths = natural
384
- if sum(natural) > available:
424
+ if sum(natural) > available and sum(minimum) <= available:
425
+ # Claude's split: every column fits its longest word, and the spare
426
+ # cells go to each column in proportion to how much more it wants.
427
+ spare = available - sum(minimum)
428
+ wanted = sum(n - m for n, m in zip(natural, minimum))
429
+ widths = [
430
+ m + spare * (n - m) // wanted for n, m in zip(natural, minimum)
431
+ ]
432
+ elif sum(natural) > available:
385
433
  low, high = 3, max(natural)
386
434
  while low < high:
387
435
  cap = (low + high + 1) // 2
@@ -419,11 +467,12 @@ def _markdown_table(lines: list[str], width: int) -> list[str] | None:
419
467
 
420
468
 
421
469
  def _markdown_inline(line: str, *, bold: bool = False) -> str:
422
- """Style inline code, bold and italic spans like Claude's markdown view.
470
+ """Style inline code, bold, italic and link spans like Claude's view.
423
471
 
424
472
  Only targeted "off" codes close each span, so an enclosing style (for
425
473
  example a bold heading) survives an inner code or italic span. *bold*
426
474
  marks an already-bold context whose nested strong spans must not end it.
475
+ A link shows only its text and stays clickable through OSC 8.
427
476
  """
428
477
 
429
478
  def replace(match: re.Match[str]) -> str:
@@ -431,6 +480,9 @@ def _markdown_inline(line: str, *, bold: bool = False) -> str:
431
480
  match.group(2), match.group(3), match.group(4), match.group(5))
432
481
  if code is not None:
433
482
  return f"{CLAUDE_ACCENT}{code}{FG_DEFAULT}"
483
+ if match.group(6) is not None:
484
+ return _link(_markdown_inline(match.group(6), bold=bold),
485
+ match.group(7))
434
486
  if strong is not None or strong_alt is not None:
435
487
  inner = _markdown_inline(
436
488
  strong if strong is not None else strong_alt, bold=True)
@@ -477,6 +529,9 @@ def _markdown_lines(lines: list[str], width: int | None = None) -> list[str]:
477
529
  if heading:
478
530
  inner = _markdown_inline(heading.group(2), bold=True)
479
531
  rendered.append(f"{BOLD}{inner}{RESET}")
532
+ # Claude always leaves a blank line below a heading.
533
+ if index < len(lines) and lines[index].strip():
534
+ rendered.append("")
480
535
  continue
481
536
  styled = _markdown_inline(line)
482
537
  rendered.append(styled + RESET if styled != line else line)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.4.2.dev6
3
+ Version: 0.4.2.dev8
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
@@ -2357,3 +2357,19 @@ def test_server_projects_only_allowlisted_private_terminal_modes():
2357
2357
  mode = {1000 << 5, 1006 << 5, 9999 << 5}
2358
2358
 
2359
2359
  assert terminal_modes_for_screen(OtherModes()) is TerminalMode.NONE
2360
+
2361
+
2362
+ def test_transcript_rows_reopen_links_across_wraps_and_drop_unsafe_targets():
2363
+ pyte = fast_display_server._extended_pyte(__import__("pyte"))
2364
+ link = "\x1b]8;;https://example.com/a\x1b\\"
2365
+ close = "\x1b]8;;\x1b\\"
2366
+ text = f"{link}abcdef{close} \x1b]8;;file:///etc/passwd\x1b\\x{close}\n"
2367
+
2368
+ rows, _dropped, _total = fast_display_server._wrap_transcript_rows(
2369
+ pyte, text, 4)
2370
+
2371
+ encoded_link, encoded_close = link.encode(), close.encode()
2372
+ assert rows[0] == b"\x1b[0m" + encoded_link + b"abcd" + encoded_close + b"\x1b[0m"
2373
+ assert rows[1].startswith(b"\x1b[0m" + encoded_link + b"ef" + encoded_close)
2374
+ assert b"file:" not in b"".join(rows)
2375
+ assert fast_display_server._overlap_text(rows[0]) == "abcd"
@@ -905,7 +905,9 @@ def _native(records, width=None):
905
905
 
906
906
 
907
907
  def _plain_rows(rendered: str) -> list[str]:
908
- return re.sub(r"\x1b\[[0-9;]*m", "", rendered).split("\n")
908
+ return re.sub(
909
+ r"\x1b\[[0-9;]*m|\x1b\]8;;[^\x1b]*\x1b\\", "", rendered
910
+ ).split("\n")
909
911
 
910
912
 
911
913
  def _assistant(text: str) -> dict:
@@ -962,12 +964,55 @@ def test_claude_native_history_draws_tables_that_fit_the_pane():
962
964
  rows = _plain_rows(_native([_assistant(table)], width=30))[1:-1]
963
965
 
964
966
  assert rows == [
965
- "● ┌──────┬───────────────────┐",
966
- " │ Name │ Notes │",
967
- " ├──────┼───────────────────┤",
968
- " │ a │ one two three │",
969
- " │ │ four │",
970
- " └──────┴───────────────────┘",
967
+ "● ┌──────┬─────────────────┐",
968
+ " │ Name │ Notes │",
969
+ " ├──────┼─────────────────┤",
970
+ " │ a │ one two three │",
971
+ " │ │ four │",
972
+ " └──────┴─────────────────┘",
971
973
  ]
972
974
  assert all(len(row) <= 30 for row in rows)
973
975
  assert table.splitlines()[0] in _native([_assistant(table)])
976
+
977
+
978
+ def test_claude_native_history_matches_claude_table_and_link_layout():
979
+ table = "\n".join([
980
+ "### Title",
981
+ "| PR | Notes |",
982
+ "|---|---|",
983
+ "| [#1](https://example.com/pull/1) | "
984
+ + "alpha beta gamma delta epsilon zeta eta theta |",
985
+ ])
986
+ rows = _plain_rows(_native([_assistant(table)], width=40))[1:-1]
987
+
988
+ assert rows == [
989
+ "● Title",
990
+ " ",
991
+ " ┌────┬─────────────────────────────┐",
992
+ " │ PR │ Notes │",
993
+ " ├────┼─────────────────────────────┤",
994
+ " │ #1 │ alpha beta gamma delta │",
995
+ " │ │ epsilon zeta eta theta │",
996
+ " └────┴─────────────────────────────┘",
997
+ ]
998
+
999
+
1000
+ def test_claude_native_wrap_moves_long_word_when_that_saves_a_break():
1001
+ moved = _native([_assistant("intro " + "x" * 36)], width=20)
1002
+ kept = _native([_assistant("intro " + "x" * 30)], width=20)
1003
+
1004
+ assert _plain_rows(moved)[1:-1] == [
1005
+ "● intro", " " + "x" * 18, " " + "x" * 18,
1006
+ ]
1007
+ assert _plain_rows(kept)[1:-1] == [
1008
+ "● intro " + "x" * 12, " " + "x" * 18,
1009
+ ]
1010
+
1011
+
1012
+ def test_claude_native_links_stay_clickable_and_reject_other_schemes():
1013
+ rendered = _native([_assistant(
1014
+ "See [#1](https://example.com/pull/1) and [x](javascript:alert).")])
1015
+
1016
+ assert "\x1b]8;;https://example.com/pull/1\x1b\\#1\x1b]8;;\x1b\\" in rendered
1017
+ assert "javascript" not in rendered
1018
+ assert "See #1 and x." in "\n".join(_plain_rows(rendered))
@@ -1 +0,0 @@
1
- __version__ = "0.4.2.dev6"
File without changes
File without changes
File without changes
File without changes