railmux 0.3.2.dev5__tar.gz → 0.4.0.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 (192) hide show
  1. {railmux-0.3.2.dev5/src/railmux.egg-info → railmux-0.4.0.dev1}/PKG-INFO +1 -1
  2. railmux-0.4.0.dev1/src/railmux/__init__.py +1 -0
  3. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/tmux_ctl.py +3 -1
  4. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/winlocal/backend.py +24 -7
  5. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/winlocal/client.py +25 -1
  6. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/winlocal/daemon.py +42 -3
  7. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1/src/railmux.egg-info}/PKG-INFO +1 -1
  8. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_release_workflow.py +14 -0
  9. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_tmux_ctl.py +9 -0
  10. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_tmux_integration.py +9 -9
  11. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_winlocal_backend.py +42 -0
  12. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_winlocal_compositor.py +19 -0
  13. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_winlocal_daemon.py +55 -2
  14. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_winlocal_real_conpty.py +33 -0
  15. railmux-0.3.2.dev5/src/railmux/__init__.py +0 -1
  16. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/LICENSE +0 -0
  17. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/MANIFEST.in +0 -0
  18. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/README.md +0 -0
  19. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/pyproject.toml +0 -0
  20. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/setup.cfg +0 -0
  21. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/__main__.py +0 -0
  22. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/atomic_file.py +0 -0
  23. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/background_index.py +0 -0
  24. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/cli.py +0 -0
  25. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/codex_index.py +0 -0
  26. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/config.py +0 -0
  27. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/config_cli.py +0 -0
  28. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/diagnostics.py +0 -0
  29. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/discovery.py +0 -0
  30. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/display_transport.py +0 -0
  31. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/fast_display_client.py +0 -0
  32. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/fast_display_history.py +0 -0
  33. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/fast_display_input.py +0 -0
  34. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/fast_display_protocol.py +0 -0
  35. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/fast_display_server.py +0 -0
  36. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/favorites.py +0 -0
  37. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/fuzzy.py +0 -0
  38. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/help_workspace.py +0 -0
  39. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/launcher.py +0 -0
  40. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/legacy_sessions.py +0 -0
  41. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/local_clipboard.py +0 -0
  42. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/local_open.py +0 -0
  43. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/models.py +0 -0
  44. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/modes.py +0 -0
  45. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/mouse_manager.py +0 -0
  46. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/mux/__init__.py +0 -0
  47. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/mux/backend.py +0 -0
  48. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/mux/tmux_backend.py +0 -0
  49. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/orphan_marker.py +0 -0
  50. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/pane_surface.py +0 -0
  51. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/path_codec.py +0 -0
  52. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/platform/__init__.py +0 -0
  53. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/platform/config_paths.py +0 -0
  54. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/platform/console.py +0 -0
  55. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/platform/file_security.py +0 -0
  56. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/platform/filelock.py +0 -0
  57. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/platform/multiplex.py +0 -0
  58. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/platform/process.py +0 -0
  59. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/platform/runtime_paths.py +0 -0
  60. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/remote_config.py +0 -0
  61. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/renames.py +0 -0
  62. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/restart_state.py +0 -0
  63. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/runtime_config.py +0 -0
  64. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/scroll_agent.py +0 -0
  65. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/scroll_manager.py +0 -0
  66. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/selection_isolation.py +0 -0
  67. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/self_update.py +0 -0
  68. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/session_cache.py +0 -0
  69. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/session_index.py +0 -0
  70. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/setting_contracts.py +0 -0
  71. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/settings.py +0 -0
  72. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/ssh_args.py +0 -0
  73. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/ssh_compat.py +0 -0
  74. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/ssh_display_diagnostics.py +0 -0
  75. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/ssh_doctor.py +0 -0
  76. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/system_deps.py +0 -0
  77. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/terminal_emulator.py +0 -0
  78. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/terminal_status.py +0 -0
  79. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/tmux_binding_manager.py +0 -0
  80. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/tmux_health.py +0 -0
  81. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/tmux_server.py +0 -0
  82. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/tool_panes.py +0 -0
  83. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/transcript.py +0 -0
  84. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/ui/__init__.py +0 -0
  85. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/ui/_widgets.py +0 -0
  86. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/ui/app.py +0 -0
  87. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/ui/keymap.py +0 -0
  88. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/ui/modals.py +0 -0
  89. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/ui/projects_pane.py +0 -0
  90. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/ui/running_pane.py +0 -0
  91. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/ui/sessions_pane.py +0 -0
  92. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/ui/sidebar.py +0 -0
  93. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/ui/statusbar.py +0 -0
  94. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/ui/workspace.py +0 -0
  95. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/winlocal/__init__.py +0 -0
  96. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/winlocal/compositor.py +0 -0
  97. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/winlocal/conpty.py +0 -0
  98. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/winlocal/history_viewer.py +0 -0
  99. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/winlocal/ipc.py +0 -0
  100. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/winlocal/session_store.py +0 -0
  101. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux/winlocal/virtual_screen.py +0 -0
  102. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux.egg-info/SOURCES.txt +0 -0
  103. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux.egg-info/dependency_links.txt +0 -0
  104. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux.egg-info/entry_points.txt +0 -0
  105. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux.egg-info/requires.txt +0 -0
  106. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/src/railmux.egg-info/top_level.txt +0 -0
  107. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_agent_workspace.py +0 -0
  108. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_atomic_file.py +0 -0
  109. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_background_index.py +0 -0
  110. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_cli.py +0 -0
  111. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_codex_delete.py +0 -0
  112. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_codex_index.py +0 -0
  113. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_codex_lineage.py +0 -0
  114. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_codex_transcript.py +0 -0
  115. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_config.py +0 -0
  116. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_config_cli.py +0 -0
  117. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_denested_transport_benchmark.py +0 -0
  118. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_diagnostics.py +0 -0
  119. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_discovery.py +0 -0
  120. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_display_transport.py +0 -0
  121. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_fast_full_window.py +0 -0
  122. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_focus_highlight.py +0 -0
  123. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_fullscreen_binding.py +0 -0
  124. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_fuzzy.py +0 -0
  125. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_help_agent.py +0 -0
  126. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_help_workspace.py +0 -0
  127. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_history_cleanup.py +0 -0
  128. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_keymap.py +0 -0
  129. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_launcher.py +0 -0
  130. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_layout_profile.py +0 -0
  131. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_legacy_sessions.py +0 -0
  132. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_local_clipboard.py +0 -0
  133. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_local_open.py +0 -0
  134. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_modals.py +0 -0
  135. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_mode_selection.py +0 -0
  136. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_modes.py +0 -0
  137. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_mouse_manager.py +0 -0
  138. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_mux_backend.py +0 -0
  139. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_new_project.py +0 -0
  140. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_options.py +0 -0
  141. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_orphan_recovery.py +0 -0
  142. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_pane_surface.py +0 -0
  143. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_path_codec.py +0 -0
  144. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_platform.py +0 -0
  145. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_projects_pane.py +0 -0
  146. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_release_notes.py +0 -0
  147. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_remote_config.py +0 -0
  148. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_renames.py +0 -0
  149. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_restart_state.py +0 -0
  150. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_running_pane.py +0 -0
  151. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_running_sort.py +0 -0
  152. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_runtime_config.py +0 -0
  153. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_scroll_agent.py +0 -0
  154. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_scroll_manager.py +0 -0
  155. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_selection_isolation.py +0 -0
  156. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_self_update.py +0 -0
  157. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_session_cache.py +0 -0
  158. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_session_index.py +0 -0
  159. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_sessions_pane.py +0 -0
  160. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_setting_contracts.py +0 -0
  161. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_settings.py +0 -0
  162. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_sidebar.py +0 -0
  163. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_soft_quit.py +0 -0
  164. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_ssh_args.py +0 -0
  165. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_ssh_compat.py +0 -0
  166. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_ssh_display_benchmark.py +0 -0
  167. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_ssh_display_diagnostics.py +0 -0
  168. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_ssh_doctor.py +0 -0
  169. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_status.py +0 -0
  170. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_statusbar.py +0 -0
  171. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_system_deps.py +0 -0
  172. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_terminal_size.py +0 -0
  173. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_terminal_status.py +0 -0
  174. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_termux_local_touch.py +0 -0
  175. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_tmux_binding_manager.py +0 -0
  176. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_tmux_health.py +0 -0
  177. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_tmux_server.py +0 -0
  178. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_tmux_status.py +0 -0
  179. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_tool_panes.py +0 -0
  180. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_transcript.py +0 -0
  181. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_web_demo_recorder.py +0 -0
  182. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_wheel_smoke.py +0 -0
  183. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_widgets.py +0 -0
  184. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_winlocal_conpty.py +0 -0
  185. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_winlocal_history_viewer.py +0 -0
  186. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_winlocal_ipc.py +0 -0
  187. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_winlocal_session_store.py +0 -0
  188. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_winlocal_virtual_screen.py +0 -0
  189. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tests/test_yolo_prompt.py +0 -0
  190. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tools/denested_transport_benchmark.py +0 -0
  191. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tools/release_notes.py +0 -0
  192. {railmux-0.3.2.dev5 → railmux-0.4.0.dev1}/tools/wheel_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.3.2.dev5
3
+ Version: 0.4.0.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
@@ -0,0 +1 @@
1
+ __version__ = "0.4.0.dev1"
@@ -2125,7 +2125,9 @@ def _root_wheel_bindings_are_defaults(
2125
2125
  return False
2126
2126
  return (
2127
2127
  "mouse_any_flag" in body
2128
- and "send-keys -M" in body
2128
+ # tmux 3.2 serializes its stock command with the accepted ``send``
2129
+ # alias, while newer releases print the canonical ``send-keys`` name.
2130
+ and re.search(r"\bsend(?:-keys)?\s+-M\b", body) is not None
2129
2131
  and "copy-mode -e" in body
2130
2132
  and _ROOT_WHEEL_MARKER not in body
2131
2133
  )
@@ -304,12 +304,26 @@ class WinMuxBackend:
304
304
  )
305
305
 
306
306
  def server_snapshot(self):
307
- live = [session for session in self._sessions.values() if self.session_exists(session.name)]
308
- return tmux_ctl.ServerSnapshot(
309
- frozenset(session.name for session in live),
310
- frozenset(session.pane_id for session in live),
311
- tuple((session.name, session.process.pid) for session in live if session.process),
312
- )
307
+ with self._lock:
308
+ live = [
309
+ session
310
+ for session in self._sessions.values()
311
+ if self.session_exists(session.name)
312
+ ]
313
+ panes = frozenset(
314
+ pane_id
315
+ for pane_id in self._panes
316
+ if self.pane_alive(pane_id)
317
+ )
318
+ return tmux_ctl.ServerSnapshot(
319
+ frozenset(session.name for session in live),
320
+ panes,
321
+ tuple(
322
+ (session.name, session.process.pid)
323
+ for session in live
324
+ if session.process
325
+ ),
326
+ )
313
327
 
314
328
  def select_pane(self, pane_id: str) -> bool:
315
329
  with self._lock:
@@ -891,7 +905,10 @@ class WinDisplayTransport:
891
905
  return None
892
906
 
893
907
  def fallback_for_external_client(self, *_args, **_kwargs):
894
- return True
908
+ # Native Windows has no external tmux client or nested-display
909
+ # fallback. ``App`` treats ``None`` as not applicable and otherwise
910
+ # requires an AttachOutcome-compatible object.
911
+ return None
895
912
 
896
913
  def outer_session_lost(self) -> bool:
897
914
  return False
@@ -28,7 +28,13 @@ from railmux.fast_display_protocol import (
28
28
  encode_resize,
29
29
  )
30
30
  from railmux.platform.console import RawConsole
31
- from railmux.winlocal.daemon import AUTH_OK, AUTH_PREFIX, endpoint_path
31
+ from railmux.winlocal.daemon import (
32
+ AUTH_OK,
33
+ AUTH_PREFIX,
34
+ NATIVE_UI_FAILED,
35
+ endpoint_path,
36
+ ui_error_path,
37
+ )
32
38
  from railmux.winlocal.ipc import Endpoint, read_endpoint, receive_message, send_message
33
39
 
34
40
 
@@ -38,6 +44,10 @@ _MAX_COLUMNS = 1000
38
44
  _MAX_LINES = 500
39
45
 
40
46
 
47
+ class NativeUIError(RuntimeError):
48
+ """The daemon's shared UI failed while provider ConPTYs stayed alive."""
49
+
50
+
41
51
  def connect_endpoint(endpoint: Endpoint, *, timeout: float = 2.0) -> socket.socket:
42
52
  sock = socket.create_connection(("127.0.0.1", endpoint.port), timeout=timeout)
43
53
  try:
@@ -160,6 +170,8 @@ class NativeClient:
160
170
  kind, payload = self._events.get(timeout=0.1)
161
171
  except queue.Empty:
162
172
  kind, payload = "tick", None
173
+ if kind == "ui_error":
174
+ raise NativeUIError
163
175
  if kind == "closed":
164
176
  return 2
165
177
  if kind == "input":
@@ -203,6 +215,15 @@ class NativeClient:
203
215
  except ValueError as exc:
204
216
  print(f"error: {exc}", file=sys.stderr)
205
217
  return 2
218
+ except NativeUIError:
219
+ # Leave the alternate screen before printing actionable feedback.
220
+ surface.close()
221
+ print(
222
+ "error: native Railmux UI stopped unexpectedly; provider "
223
+ f"sessions are still running. Retry railmux; details: {ui_error_path()}",
224
+ file=sys.stderr,
225
+ )
226
+ return 2
206
227
  except (ConnectionError, OSError):
207
228
  return 2
208
229
  finally:
@@ -219,6 +240,9 @@ class NativeClient:
219
240
  packet = receive_message(self.sock)
220
241
  if packet is None:
221
242
  break
243
+ if packet == NATIVE_UI_FAILED:
244
+ self._events.put(("ui_error", None))
245
+ return
222
246
  self._events.put(("server", packet))
223
247
  except (ConnectionError, OSError, ValueError):
224
248
  pass
@@ -7,10 +7,12 @@ import socket
7
7
  import struct
8
8
  import threading
9
9
  import time
10
+ import traceback
10
11
  import uuid
11
12
  from pathlib import Path
12
13
  from typing import Callable
13
14
 
15
+ from railmux.atomic_file import atomic_write_text
14
16
  from railmux.config import Config, ConfigError, load_config
15
17
  from railmux.fast_display_protocol import (
16
18
  InputFrameDecoder,
@@ -33,6 +35,8 @@ from railmux.winlocal.session_store import SessionStore
33
35
  AUTH_PREFIX = b"AUTH "
34
36
  AUTH_OK = b"OK"
35
37
  AUTH_FAILED = b"ERROR authentication failed"
38
+ NATIVE_UI_FAILED = b"RAILMUX-NATIVE/1 UI_FAILED"
39
+ _UI_ERROR_LIMIT = 64 * 1024
36
40
 
37
41
 
38
42
  def native_runtime_dir() -> Path:
@@ -47,6 +51,10 @@ def session_store_path() -> Path:
47
51
  return native_runtime_dir() / "sessions.json"
48
52
 
49
53
 
54
+ def ui_error_path() -> Path:
55
+ return native_runtime_dir() / "native-ui-error.log"
56
+
57
+
50
58
  class DaemonServer:
51
59
  """Authenticated loopback server around one daemon-owned backend."""
52
60
 
@@ -60,6 +68,7 @@ class DaemonServer:
60
68
  fps: float = 20.0,
61
69
  idle_timeout: float = 300.0,
62
70
  app_factory: Callable[[], object] | None = None,
71
+ ui_error_file: Path | None = None,
63
72
  ) -> None:
64
73
  self.backend = backend
65
74
  self.endpoint_file = endpoint_file
@@ -69,6 +78,7 @@ class DaemonServer:
69
78
  self.idle_timeout = max(1.0, idle_timeout)
70
79
  self._last_nonidle = time.monotonic()
71
80
  self.app_factory = app_factory
81
+ self.ui_error_file = ui_error_file or endpoint_file.parent / ui_error_path().name
72
82
  self._listener: socket.socket | None = None
73
83
  self._clients: set[socket.socket] = set()
74
84
  self._clients_lock = threading.RLock()
@@ -194,14 +204,17 @@ class DaemonServer:
194
204
  self._app_thread.start()
195
205
 
196
206
  def _run_app(self) -> None:
207
+ failed = False
197
208
  try:
198
209
  assert self.app_factory is not None
199
210
  app = self.app_factory()
200
211
  run = getattr(app, "run")
201
212
  run()
202
- except Exception:
213
+ except Exception as exc:
214
+ failed = True
203
215
  # Keep the daemon/ConPTY authority alive so a later attachment can
204
216
  # retry the UI without converting a display fault into session loss.
217
+ self._record_ui_failure(exc)
205
218
  self.backend.set_status_text(
206
219
  "Native UI stopped unexpectedly; reconnect to retry",
207
220
  "error",
@@ -210,7 +223,28 @@ class DaemonServer:
210
223
  # Urwid returning means hard quit, soft quit, or a failed UI. The
211
224
  # daemon and any surviving ConPTYs remain authoritative, but every
212
225
  # frontend must leave instead of displaying a frozen last frame.
213
- self._detach_all_clients()
226
+ self._detach_all_clients(
227
+ final_message=NATIVE_UI_FAILED if failed else None
228
+ )
229
+
230
+ def _record_ui_failure(self, exc: BaseException) -> None:
231
+ """Persist one bounded private traceback for the local user."""
232
+ detail = "".join(
233
+ traceback.format_exception(type(exc), exc, exc.__traceback__)
234
+ )
235
+ if len(detail) > _UI_ERROR_LIMIT:
236
+ detail = "[traceback truncated]\n" + detail[-_UI_ERROR_LIMIT:]
237
+ payload = (
238
+ "Railmux native UI failure\n"
239
+ f"recorded_at={int(time.time())}\n"
240
+ f"daemon_id={self.daemon_id}\n"
241
+ f"{detail}"
242
+ )
243
+ try:
244
+ ensure_private_dir(self.ui_error_file.parent)
245
+ atomic_write_text(self.ui_error_file, payload)
246
+ except OSError:
247
+ pass
214
248
 
215
249
  def _render_loop(self) -> None:
216
250
  interval = 1.0 / self.fps
@@ -249,11 +283,16 @@ class DaemonServer:
249
283
  with self._clients_lock:
250
284
  return len(self._clients)
251
285
 
252
- def _detach_all_clients(self) -> None:
286
+ def _detach_all_clients(self, *, final_message: bytes | None = None) -> None:
253
287
  with self._clients_lock:
254
288
  clients = tuple(self._clients)
255
289
  self._clients.clear()
256
290
  for client in clients:
291
+ if final_message is not None:
292
+ try:
293
+ send_message(client, final_message)
294
+ except (ConnectionError, OSError, ValueError):
295
+ pass
257
296
  try:
258
297
  client.shutdown(socket.SHUT_RDWR)
259
298
  except OSError:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.3.2.dev5
3
+ Version: 0.4.0.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
@@ -50,3 +50,17 @@ def test_development_tag_creates_a_github_prerelease():
50
50
  assert 'if [[ "$GITHUB_REF_NAME" == *.dev* ]]' in release
51
51
  assert "prerelease+=(--prerelease)" in release
52
52
  assert '"${prerelease[@]}"' in release
53
+
54
+
55
+ def test_release_tags_are_fenced_to_their_product_branches():
56
+ release = (ROOT / ".github/workflows/release.yml").read_text()
57
+
58
+ assert "fetch-depth: 0" in release
59
+ assert "origin/main" in release
60
+ assert "origin/windows-preview" in release
61
+ assert "POSIX/WSL development release from main" in release
62
+ assert "native-Windows development release from windows-preview" in release
63
+ assert "main development releases must use the 0.3.x.devN series" in release
64
+ assert "native-Windows releases must use the 0.4.0.devN series" in release
65
+ assert "development releases must come from main or windows-preview" in release
66
+ assert "final releases must come from main" in release
@@ -514,6 +514,15 @@ def test_root_wheel_forwarding_requires_stock_bindings_and_tmux_27():
514
514
  return_value=backup):
515
515
  assert tmux_ctl.prepare_root_wheel_bindings() == backup
516
516
 
517
+ tmux_32_backup = dict(
518
+ backup,
519
+ WheelUpPane=backup["WheelUpPane"].replace("send-keys -M", "send -M"),
520
+ )
521
+ with patch("railmux.tmux_ctl.tmux_version", return_value=(3, 2)), \
522
+ patch("railmux.tmux_ctl.read_root_wheel_bindings",
523
+ return_value=tmux_32_backup):
524
+ assert tmux_ctl.prepare_root_wheel_bindings() == tmux_32_backup
525
+
517
526
  custom = dict(backup, WheelDownPane=(
518
527
  "bind-key -T root WheelDownPane display-message custom"))
519
528
  with patch("railmux.tmux_ctl.tmux_version", return_value=(3, 4)), \
@@ -1522,6 +1522,12 @@ def test_real_soft_quit_disconnects_shared_clients_and_agents_survive(
1522
1522
  for directory in (socket_root, home, runtime, claude_home):
1523
1523
  directory.mkdir(parents=True, exist_ok=True)
1524
1524
  directory.chmod(0o700)
1525
+ config_dir = home / ".config" / "railmux"
1526
+ config_dir.mkdir(parents=True)
1527
+ (config_dir / "config.toml").write_text(
1528
+ '[updates]\nauto_update = "never"\n',
1529
+ encoding="utf-8",
1530
+ )
1525
1531
 
1526
1532
  label = f"rx-soft-quit-{os.getpid()}"
1527
1533
  monkeypatch.setenv("TMUX_TMPDIR", str(socket_root))
@@ -2617,15 +2623,9 @@ def test_real_tmux_binding_manager_round_trip_and_user_reload(
2617
2623
  client_name = ""
2618
2624
 
2619
2625
  def send_client_key(key: str) -> None:
2620
- if tmux_ctl.tmux_version() >= (3, 0):
2621
- subprocess.run(
2622
- ["tmux", "send-keys", "-K", "-c", client_name, key],
2623
- check=True,
2624
- )
2625
- return
2626
- # tmux 2.7 predates send-keys -K. Feed the corresponding xterm bytes
2627
- # to the same attached client so the binding still executes through
2628
- # the real terminal input path.
2626
+ # Feed xterm bytes to the attached client on every supported version.
2627
+ # ``send-keys -K`` is absent from tmux 3.2 even though later releases
2628
+ # provide it; the raw path also exercises the real terminal decoder.
2629
2629
  sequence = {"F8": b"\x1b[19~", "C-b": b"\x02", "Tab": b"\t"}[key]
2630
2630
  assert client_process.stdin is not None
2631
2631
  client_process.stdin.write(sequence)
@@ -59,6 +59,48 @@ def test_windows_backend_starts_typed_provider_and_composes_display():
59
59
  assert update.width == 120 and update.height == 30
60
60
 
61
61
 
62
+ def test_windows_snapshot_keeps_attached_display_pane_alive():
63
+ backend = WinMuxBackend(process_factory=_factory)
64
+ launch = backend.prepare_launch(["codex", "resume", "abc"], Path("C:/repo"))
65
+ assert backend.new_detached_session("codex-abc", launch) == (True, None)
66
+ workspace = AgentWorkspace()
67
+ transport = backend.create_display_transport(workspace, "swap")
68
+
69
+ assert transport.attach(workspace.primary, "codex-abc").ok
70
+ assert workspace.primary.pane_id is not None
71
+ snapshot = backend.server_snapshot()
72
+
73
+ assert workspace.primary.pane_id in snapshot.panes
74
+ assert backend._sessions["codex-abc"].pane_id in snapshot.panes
75
+ assert transport.fallback_for_external_client(workspace.primary) is None
76
+
77
+
78
+ def test_shared_reconciliation_preserves_native_attached_agent(tmp_path):
79
+ backend = WinMuxBackend(process_factory=_factory)
80
+ app = App(tmp_path, Config(), mux_backend=backend)
81
+ launch = backend.prepare_launch(["codex", "resume", "abc"], tmp_path)
82
+ assert backend.new_detached_session("codex-abc", launch) == (True, None)
83
+ session = backend._sessions["codex-abc"]
84
+ assert session.terminal is not None
85
+ session.terminal.feed(b"AGENT_PANEL_READY")
86
+ slot = app._agent_workspace().primary
87
+ assert app._display_transport().attach(slot, "codex-abc").ok
88
+
89
+ snapshot = backend.server_snapshot()
90
+ app._reconcile_display_slots(
91
+ lambda name: name in snapshot.sessions,
92
+ lambda pane_id: pane_id in snapshot.panes,
93
+ )
94
+
95
+ assert slot.pane_id is not None
96
+ assert slot.agent_tmux_name == "codex-abc"
97
+ assert "primary" in backend._display_regions()
98
+ assert any(
99
+ b"AGENT_PANEL_READY" in row
100
+ for _index, row in backend.screen_update().rows
101
+ )
102
+
103
+
62
104
  def test_windows_backend_preserves_unsuperseded_resume_offers(tmp_path):
63
105
  store = SessionStore(tmp_path / "sessions.json", "new-daemon")
64
106
  offer = SessionRecord(
@@ -46,3 +46,22 @@ def test_sidebar_is_full_width_without_agent_and_status_cannot_scroll_frame():
46
46
  "sidebar"
47
47
  ].width == 40
48
48
  assert b"Projects" in rows[0]
49
+
50
+
51
+ def test_compositor_preserves_cjk_agent_output_and_bottom_status():
52
+ sidebar = TerminalPane(20, 6)
53
+ primary = TerminalPane(20, 6)
54
+ sidebar.feed("会话列表".encode("utf-8"))
55
+ primary.feed("代理已恢复".encode("utf-8"))
56
+ compositor = Compositor(60, 12)
57
+
58
+ update = compositor.compose(
59
+ sidebar,
60
+ primary,
61
+ status="状态栏可见".encode("utf-8"),
62
+ )
63
+ rows = dict(update.rows)
64
+
65
+ assert "会话列表".encode("utf-8") in rows[0]
66
+ assert "代理已恢复".encode("utf-8") in rows[0]
67
+ assert "状态栏可见".encode("utf-8") in rows[11]
@@ -10,8 +10,8 @@ from railmux.fast_display_protocol import (
10
10
  encode_resize,
11
11
  )
12
12
  from railmux.winlocal.backend import WinMuxBackend
13
- from railmux.winlocal.client import connect_endpoint, ensure_daemon
14
- from railmux.winlocal.daemon import AUTH_PREFIX, DaemonServer
13
+ from railmux.winlocal.client import NativeClient, connect_endpoint, ensure_daemon
14
+ from railmux.winlocal.daemon import AUTH_PREFIX, NATIVE_UI_FAILED, DaemonServer
15
15
  from railmux.winlocal.ipc import read_endpoint, receive_message, send_message
16
16
 
17
17
 
@@ -132,6 +132,59 @@ def test_app_exit_disconnects_frontend_but_keeps_daemon_available(tmp_path):
132
132
  thread.join(timeout=2)
133
133
 
134
134
 
135
+ def test_app_crash_reports_failure_and_writes_bounded_traceback(tmp_path):
136
+ class App:
137
+ def run(self):
138
+ raise RuntimeError("refresh contract failed")
139
+
140
+ error_file = tmp_path / "native-ui-error.log"
141
+ server = DaemonServer(
142
+ _backend(),
143
+ endpoint_file=tmp_path / "endpoint.json",
144
+ app_factory=App,
145
+ ui_error_file=error_file,
146
+ )
147
+ endpoint = server.start()
148
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
149
+ thread.start()
150
+ client = connect_endpoint(endpoint)
151
+ client.settimeout(2)
152
+ try:
153
+ messages = []
154
+ deadline = time.monotonic() + 2
155
+ while time.monotonic() < deadline:
156
+ message = receive_message(client)
157
+ if message is None:
158
+ break
159
+ messages.append(message)
160
+ if message == NATIVE_UI_FAILED:
161
+ break
162
+ assert NATIVE_UI_FAILED in messages
163
+ detail = error_file.read_text(encoding="utf-8")
164
+ assert "RuntimeError: refresh contract failed" in detail
165
+ assert f"daemon_id={server.daemon_id}" in detail
166
+ assert len(detail) <= 65 * 1024
167
+ finally:
168
+ client.close()
169
+ server.close()
170
+ thread.join(timeout=2)
171
+
172
+
173
+ def test_native_client_classifies_ui_failure_before_socket_close():
174
+ daemon, frontend = socket.socketpair()
175
+ client = NativeClient(frontend)
176
+ thread = threading.Thread(target=client._read_socket, daemon=True)
177
+ thread.start()
178
+ try:
179
+ send_message(daemon, NATIVE_UI_FAILED)
180
+ assert client._events.get(timeout=2) == ("ui_error", None)
181
+ finally:
182
+ client._stop.set()
183
+ daemon.close()
184
+ frontend.close()
185
+ thread.join(timeout=2)
186
+
187
+
135
188
  def test_idle_daemon_without_clients_or_sessions_exits(tmp_path):
136
189
  endpoint_file = tmp_path / "endpoint.json"
137
190
  server = DaemonServer(
@@ -1,4 +1,5 @@
1
1
  import os
2
+ import sys
2
3
  import time
3
4
 
4
5
  import pytest
@@ -59,3 +60,35 @@ def test_real_conpty_launches_cmd_shim_from_path_with_spaces(tmp_path):
59
60
  finally:
60
61
  if process.is_alive():
61
62
  process.terminate(force=True)
63
+
64
+
65
+ @pytest.mark.skipif(os.name != "nt", reason="requires Windows ConPTY")
66
+ def test_real_conpty_normalizes_cp936_child_output_to_utf8(tmp_path):
67
+ shim = tmp_path / "cp936-provider.cmd"
68
+ shim.write_text(
69
+ "@echo off\r\n"
70
+ "chcp 936 >nul\r\n"
71
+ f'"{sys.executable}" -c "print(\'\\u4e2d\\u6587\')"\r\n',
72
+ encoding="ascii",
73
+ )
74
+ process = PyWinPtyProcess.spawn(
75
+ provider_argv(str(shim), (), windows=True),
76
+ cwd=tmp_path,
77
+ env=dict(os.environ),
78
+ columns=80,
79
+ rows=24,
80
+ )
81
+ output = bytearray()
82
+ expected = "中文".encode("utf-8")
83
+ deadline = time.monotonic() + 10
84
+ try:
85
+ while time.monotonic() < deadline and expected not in output:
86
+ try:
87
+ output.extend(process.read(65536))
88
+ except EOFError:
89
+ break
90
+ assert expected in output
91
+ output.decode("utf-8")
92
+ finally:
93
+ if process.is_alive():
94
+ process.terminate(force=True)
@@ -1 +0,0 @@
1
- __version__ = "0.3.2.dev5"
File without changes
File without changes
File without changes
File without changes