railmux 0.4.0.dev20__tar.gz → 0.4.0.dev21__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 (178) hide show
  1. {railmux-0.4.0.dev20/src/railmux.egg-info → railmux-0.4.0.dev21}/PKG-INFO +4 -2
  2. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/README.md +3 -1
  3. railmux-0.4.0.dev21/src/railmux/__init__.py +1 -0
  4. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/fast_display_client.py +114 -25
  5. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/fast_display_history.py +6 -9
  6. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/ui/modals.py +47 -25
  7. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21/src/railmux.egg-info}/PKG-INFO +4 -2
  8. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_fast_full_window.py +87 -14
  9. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_modals.py +23 -11
  10. railmux-0.4.0.dev20/src/railmux/__init__.py +0 -1
  11. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/LICENSE +0 -0
  12. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/MANIFEST.in +0 -0
  13. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/pyproject.toml +0 -0
  14. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/setup.cfg +0 -0
  15. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/__main__.py +0 -0
  16. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/atomic_file.py +0 -0
  17. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/background_index.py +0 -0
  18. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/cli.py +0 -0
  19. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/codex_index.py +0 -0
  20. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/config.py +0 -0
  21. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/config_cli.py +0 -0
  22. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/diagnostics.py +0 -0
  23. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/discovery.py +0 -0
  24. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/display_transport.py +0 -0
  25. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/entrypoint.py +0 -0
  26. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/fast_display_input.py +0 -0
  27. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/fast_display_protocol.py +0 -0
  28. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/fast_display_server.py +0 -0
  29. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/favorites.py +0 -0
  30. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/fuzzy.py +0 -0
  31. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/help_workspace.py +0 -0
  32. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/launcher.py +0 -0
  33. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/legacy_sessions.py +0 -0
  34. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/local_clipboard.py +0 -0
  35. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/local_open.py +0 -0
  36. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/models.py +0 -0
  37. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/modes.py +0 -0
  38. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/mouse_manager.py +0 -0
  39. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/orphan_marker.py +0 -0
  40. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/pane_surface.py +0 -0
  41. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/path_codec.py +0 -0
  42. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/provider_paths.py +0 -0
  43. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/remote_config.py +0 -0
  44. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/renames.py +0 -0
  45. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/restart_state.py +0 -0
  46. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/runtime_config.py +0 -0
  47. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/scroll_agent.py +0 -0
  48. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/scroll_manager.py +0 -0
  49. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/selection_isolation.py +0 -0
  50. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/self_update.py +0 -0
  51. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/session_cache.py +0 -0
  52. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/session_index.py +0 -0
  53. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/session_lease.py +0 -0
  54. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/setting_contracts.py +0 -0
  55. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/settings.py +0 -0
  56. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/ssh_args.py +0 -0
  57. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/ssh_compat.py +0 -0
  58. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/ssh_display_diagnostics.py +0 -0
  59. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/ssh_doctor.py +0 -0
  60. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/system_deps.py +0 -0
  61. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/terminal_status.py +0 -0
  62. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/tmux_binding_manager.py +0 -0
  63. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/tmux_ctl.py +0 -0
  64. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/tmux_health.py +0 -0
  65. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/tmux_server.py +0 -0
  66. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/tool_panes.py +0 -0
  67. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/transcript.py +0 -0
  68. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/ui/__init__.py +0 -0
  69. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/ui/_widgets.py +0 -0
  70. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/ui/app.py +0 -0
  71. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/ui/keymap.py +0 -0
  72. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/ui/projects_pane.py +0 -0
  73. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/ui/running_pane.py +0 -0
  74. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/ui/sessions_pane.py +0 -0
  75. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/ui/sidebar.py +0 -0
  76. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/ui/statusbar.py +0 -0
  77. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/ui/workspace.py +0 -0
  78. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/windows_attach_relay.py +0 -0
  79. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/windows_bootstrap.py +0 -0
  80. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/windows_install_log.py +0 -0
  81. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/windows_msys2.py +0 -0
  82. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/windows_pacman.py +0 -0
  83. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux/windows_tmux_lifecycle.py +0 -0
  84. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux.egg-info/SOURCES.txt +0 -0
  85. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux.egg-info/dependency_links.txt +0 -0
  86. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux.egg-info/entry_points.txt +0 -0
  87. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux.egg-info/requires.txt +0 -0
  88. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/src/railmux.egg-info/top_level.txt +0 -0
  89. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_agent_workspace.py +0 -0
  90. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_atomic_file.py +0 -0
  91. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_background_index.py +0 -0
  92. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_cli.py +0 -0
  93. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_codex_delete.py +0 -0
  94. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_codex_index.py +0 -0
  95. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_codex_lineage.py +0 -0
  96. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_codex_transcript.py +0 -0
  97. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_config.py +0 -0
  98. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_config_cli.py +0 -0
  99. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_denested_transport_benchmark.py +0 -0
  100. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_diagnostics.py +0 -0
  101. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_discovery.py +0 -0
  102. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_display_transport.py +0 -0
  103. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_focus_highlight.py +0 -0
  104. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_fullscreen_binding.py +0 -0
  105. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_fuzzy.py +0 -0
  106. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_help_agent.py +0 -0
  107. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_help_workspace.py +0 -0
  108. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_history_cleanup.py +0 -0
  109. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_keymap.py +0 -0
  110. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_launcher.py +0 -0
  111. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_layout_profile.py +0 -0
  112. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_legacy_sessions.py +0 -0
  113. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_local_clipboard.py +0 -0
  114. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_local_open.py +0 -0
  115. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_mode_selection.py +0 -0
  116. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_modes.py +0 -0
  117. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_mouse_manager.py +0 -0
  118. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_new_project.py +0 -0
  119. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_options.py +0 -0
  120. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_orphan_recovery.py +0 -0
  121. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_pane_surface.py +0 -0
  122. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_path_codec.py +0 -0
  123. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_projects_pane.py +0 -0
  124. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_provider_paths.py +0 -0
  125. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_release_notes.py +0 -0
  126. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_release_workflow.py +0 -0
  127. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_remote_config.py +0 -0
  128. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_renames.py +0 -0
  129. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_restart_state.py +0 -0
  130. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_running_pane.py +0 -0
  131. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_running_sort.py +0 -0
  132. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_runtime_config.py +0 -0
  133. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_scroll_agent.py +0 -0
  134. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_scroll_manager.py +0 -0
  135. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_selection_isolation.py +0 -0
  136. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_self_update.py +0 -0
  137. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_session_cache.py +0 -0
  138. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_session_index.py +0 -0
  139. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_session_lease.py +0 -0
  140. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_sessions_pane.py +0 -0
  141. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_setting_contracts.py +0 -0
  142. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_settings.py +0 -0
  143. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_sidebar.py +0 -0
  144. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_soft_quit.py +0 -0
  145. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_ssh_args.py +0 -0
  146. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_ssh_compat.py +0 -0
  147. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_ssh_display_benchmark.py +0 -0
  148. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_ssh_display_diagnostics.py +0 -0
  149. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_ssh_doctor.py +0 -0
  150. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_status.py +0 -0
  151. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_statusbar.py +0 -0
  152. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_system_deps.py +0 -0
  153. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_terminal_size.py +0 -0
  154. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_terminal_status.py +0 -0
  155. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_termux_local_touch.py +0 -0
  156. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_tmux_binding_manager.py +0 -0
  157. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_tmux_ctl.py +0 -0
  158. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_tmux_health.py +0 -0
  159. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_tmux_integration.py +0 -0
  160. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_tmux_server.py +0 -0
  161. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_tmux_status.py +0 -0
  162. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_tool_panes.py +0 -0
  163. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_transcript.py +0 -0
  164. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_utf8_state.py +0 -0
  165. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_web_demo_recorder.py +0 -0
  166. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_wheel_smoke.py +0 -0
  167. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_widgets.py +0 -0
  168. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_windows_attach_relay.py +0 -0
  169. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_windows_bootstrap.py +0 -0
  170. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_windows_install_log.py +0 -0
  171. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_windows_msys2.py +0 -0
  172. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_windows_pacman.py +0 -0
  173. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_windows_tmux_lifecycle.py +0 -0
  174. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_windows_wrapper_contract.py +0 -0
  175. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tests/test_yolo_prompt.py +0 -0
  176. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tools/denested_transport_benchmark.py +0 -0
  177. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tools/release_notes.py +0 -0
  178. {railmux-0.4.0.dev20 → railmux-0.4.0.dev21}/tools/wheel_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.4.0.dev20
3
+ Version: 0.4.0.dev21
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
@@ -446,7 +446,9 @@ not interrupt it. To leave agents running when you quit Railmux, press `s` for
446
446
  soft quit in the confirmation popup; restarting the same Railmux instance then
447
447
  restores the usable workspace when those sessions are still available. A normal
448
448
  quit ends all running sessions instead and therefore requires a second
449
- confirmation; `y` and Enter keep their existing confirm meaning in both steps.
449
+ confirmation. The first chooser has visible mouse buttons, but the final
450
+ destructive confirmation remains keyboard-only; `y` and Enter keep their
451
+ existing confirm meaning in both steps.
450
452
 
451
453
  All terminals attached to one managed Railmux window view the same UI process.
452
454
  Soft Quit therefore closes that shared UI for every attached view, although it
@@ -403,7 +403,9 @@ not interrupt it. To leave agents running when you quit Railmux, press `s` for
403
403
  soft quit in the confirmation popup; restarting the same Railmux instance then
404
404
  restores the usable workspace when those sessions are still available. A normal
405
405
  quit ends all running sessions instead and therefore requires a second
406
- confirmation; `y` and Enter keep their existing confirm meaning in both steps.
406
+ confirmation. The first chooser has visible mouse buttons, but the final
407
+ destructive confirmation remains keyboard-only; `y` and Enter keep their
408
+ existing confirm meaning in both steps.
407
409
 
408
410
  All terminals attached to one managed Railmux window view the same UI process.
409
411
  Soft Quit therefore closes that shared UI for every attached view, although it
@@ -0,0 +1 @@
1
+ __version__ = "0.4.0.dev21"
@@ -39,7 +39,7 @@ from railmux.fast_display_history import (
39
39
  HistoryAction,
40
40
  LocalHistoryView,
41
41
  PeriodicPrefetchGate,
42
- claim_batched_wheel,
42
+ claim_batched_forwarded_wheel,
43
43
  input_may_change_routes,
44
44
  )
45
45
  from railmux.fast_display_input import (
@@ -1423,6 +1423,73 @@ class TerminalSurface:
1423
1423
  self.interaction_active = False
1424
1424
 
1425
1425
 
1426
+ @dataclass
1427
+ class _DeferredHistoryPaint:
1428
+ """Collapse one local-input batch into its final history viewport paint."""
1429
+
1430
+ render_history: bool = False
1431
+ restore_live: bool = False
1432
+
1433
+ def defer(self, action: HistoryAction) -> None:
1434
+ self.render_history = self.render_history or action.render_history
1435
+ self.restore_live = self.restore_live or action.restore_live
1436
+
1437
+ def flush(
1438
+ self,
1439
+ surface: TerminalSurface,
1440
+ screen: AppliedScreen | None,
1441
+ overlays: tuple[tuple[HistorySnapshot, tuple[bytes, ...]], ...],
1442
+ selection: tuple[SelectionSegment, ...],
1443
+ ) -> None:
1444
+ if screen is not None:
1445
+ if self.restore_live:
1446
+ # A viewport may have reached the live bottom and another may
1447
+ # still be frozen. Restore the authoritative frame and compose
1448
+ # only the final overlays in one terminal write.
1449
+ surface.paint(full_repaint(screen), overlays, selection)
1450
+ elif self.render_history:
1451
+ surface.paint_overlays(screen, overlays, selection)
1452
+ self.render_history = False
1453
+ self.restore_live = False
1454
+
1455
+
1456
+ class _LocalInputBatch:
1457
+ """Preserve local wheel distance while bounding forwarded wheel bursts."""
1458
+
1459
+ def __init__(self) -> None:
1460
+ self._forwarded_wheel_directions: set[int] = set()
1461
+ self._history_paint = _DeferredHistoryPaint()
1462
+
1463
+ def prepare(
1464
+ self,
1465
+ event: SgrMouseEvent,
1466
+ action: HistoryAction,
1467
+ ) -> tuple[HistoryAction | None, _DeferredHistoryPaint | None]:
1468
+ if event.wheel_direction == 0:
1469
+ return action, None
1470
+ if action.forwarded_input:
1471
+ if not claim_batched_forwarded_wheel(
1472
+ event,
1473
+ self._forwarded_wheel_directions,
1474
+ ):
1475
+ return None, None
1476
+ return action, None
1477
+ # Every locally owned history tick has already advanced the viewport.
1478
+ # Defer only its terminal paint so a Windows/RDP read containing
1479
+ # several SGR packets preserves the complete distance without issuing
1480
+ # one synchronous pane repaint per packet.
1481
+ return action, self._history_paint
1482
+
1483
+ def flush(
1484
+ self,
1485
+ surface: TerminalSurface,
1486
+ screen: AppliedScreen | None,
1487
+ overlays: tuple[tuple[HistorySnapshot, tuple[bytes, ...]], ...],
1488
+ selection: tuple[SelectionSegment, ...],
1489
+ ) -> None:
1490
+ self._history_paint.flush(surface, screen, overlays, selection)
1491
+
1492
+
1426
1493
  def _remote_server_args(
1427
1494
  *,
1428
1495
  session: str,
@@ -2605,22 +2672,28 @@ def run(args: argparse.Namespace) -> int:
2605
2672
  if frame is not None:
2606
2673
  send_protocol_frame(frame)
2607
2674
 
2608
- def apply_history_action(action: HistoryAction) -> None:
2675
+ def apply_history_action(
2676
+ action: HistoryAction,
2677
+ deferred_paint: _DeferredHistoryPaint | None = None,
2678
+ ) -> None:
2609
2679
  nonlocal route_refresh_needed
2610
2680
  nonlocal history_info_until, claude_history_prompt_input
2611
2681
  overlays = history.overlays()
2612
- if action.restore_live and latest_screen is not None:
2613
- surface.paint(
2614
- full_repaint(latest_screen),
2615
- overlays,
2616
- selection.segments(),
2617
- )
2618
- elif action.render_history and latest_screen is not None:
2619
- surface.paint_overlays(
2620
- latest_screen,
2621
- overlays,
2622
- selection.segments(),
2623
- )
2682
+ if deferred_paint is not None:
2683
+ deferred_paint.defer(action)
2684
+ else:
2685
+ if action.restore_live and latest_screen is not None:
2686
+ surface.paint(
2687
+ full_repaint(latest_screen),
2688
+ overlays,
2689
+ selection.segments(),
2690
+ )
2691
+ elif action.render_history and latest_screen is not None:
2692
+ surface.paint_overlays(
2693
+ latest_screen,
2694
+ overlays,
2695
+ selection.segments(),
2696
+ )
2624
2697
  if action.protocol_frame:
2625
2698
  send_protocol_frame(action.protocol_frame)
2626
2699
  if action.forwarded_input:
@@ -2637,7 +2710,7 @@ def run(args: argparse.Namespace) -> int:
2637
2710
 
2638
2711
  def handle_terminal_part(
2639
2712
  part: bytes | SgrMouseEvent,
2640
- handled_wheels: set[int],
2713
+ input_batch: _LocalInputBatch,
2641
2714
  ) -> None:
2642
2715
  nonlocal route_refresh_needed
2643
2716
  nonlocal claude_history_prompt_input, claude_history_pending_choice
@@ -2900,15 +2973,18 @@ def run(args: argparse.Namespace) -> int:
2900
2973
  selection_changed_rows(selection_before, selection_after),
2901
2974
  )
2902
2975
  for replay_event in selection_action.replay_events:
2903
- if not claim_batched_wheel(replay_event, handled_wheels):
2904
- continue
2905
2976
  replay_action = history.pointer_event(
2906
2977
  replay_event,
2907
2978
  focused_pane_id,
2908
2979
  status_row=status_row,
2909
2980
  now=time.monotonic(),
2910
2981
  )
2911
- apply_history_action(replay_action)
2982
+ replay_action, deferred_paint = input_batch.prepare(
2983
+ replay_event,
2984
+ replay_action,
2985
+ )
2986
+ if replay_action is not None:
2987
+ apply_history_action(replay_action, deferred_paint)
2912
2988
  if selection_action.copy_data is not None:
2913
2989
  surface.copy_to_clipboard(selection_action.copy_data)
2914
2990
  character_count = len(
@@ -2944,15 +3020,15 @@ def run(args: argparse.Namespace) -> int:
2944
3020
  begin_path_open(target)
2945
3021
  if selection_action.handled:
2946
3022
  return
2947
- if not claim_batched_wheel(part, handled_wheels):
2948
- return
2949
3023
  action = history.pointer_event(
2950
3024
  part,
2951
3025
  focused_pane_id,
2952
3026
  status_row=status_row,
2953
3027
  now=time.monotonic(),
2954
3028
  )
2955
- apply_history_action(action)
3029
+ action, deferred_paint = input_batch.prepare(part, action)
3030
+ if action is not None:
3031
+ apply_history_action(action, deferred_paint)
2956
3032
  return
2957
3033
  if not part:
2958
3034
  return
@@ -3327,19 +3403,32 @@ def run(args: argparse.Namespace) -> int:
3327
3403
  data, emergency_exit = split_local_escape(data)
3328
3404
  if emergency_exit:
3329
3405
  local_exit = True
3330
- handled_wheels: set[int] = set()
3406
+ input_batch = _LocalInputBatch()
3331
3407
  for part in terminal_input.feed(data):
3332
3408
  if isinstance(part, bytes):
3333
3409
  for key_part in split_page_key_input(part):
3334
- handle_terminal_part(key_part, handled_wheels)
3410
+ handle_terminal_part(key_part, input_batch)
3335
3411
  else:
3336
- handle_terminal_part(part, handled_wheels)
3412
+ handle_terminal_part(part, input_batch)
3413
+ input_batch.flush(
3414
+ surface,
3415
+ latest_screen,
3416
+ history.overlays(),
3417
+ selection.segments(),
3418
+ )
3337
3419
  if local_exit:
3338
3420
  break
3339
3421
  if not local_exit:
3340
3422
  for part in terminal_input.flush_pending():
3423
+ input_batch = _LocalInputBatch()
3341
3424
  for key_part in split_page_key_input(part):
3342
- handle_terminal_part(key_part, set())
3425
+ handle_terminal_part(key_part, input_batch)
3426
+ input_batch.flush(
3427
+ surface,
3428
+ latest_screen,
3429
+ history.overlays(),
3430
+ selection.segments(),
3431
+ )
3343
3432
  now = time.monotonic()
3344
3433
  keyboard_projected = touch_keyboard.keyboard_projected
3345
3434
  if touch_keyboard.expire(now):
@@ -1021,18 +1021,15 @@ class LocalHistoryView:
1021
1021
  return was_active
1022
1022
 
1023
1023
 
1024
- def claim_batched_wheel(
1024
+ def claim_batched_forwarded_wheel(
1025
1025
  event: SgrMouseEvent,
1026
1026
  handled_directions: set[int],
1027
1027
  ) -> bool:
1028
- """Admit at most one wheel tick per direction from one terminal read.
1029
-
1030
- Trackpads commonly report several identical SGR wheel packets together.
1031
- If painting is briefly busy with fresh agent output, those packets can
1032
- accumulate in stdin and then move a frozen local-history viewport many
1033
- lines at once. Bounding each read preserves the established one-line
1034
- local scroll feel without using a timing heuristic; a later read remains
1035
- immediately eligible.
1028
+ """Admit at most one forwarded wheel tick per direction in one read.
1029
+
1030
+ Remote sidebar/modal owners need one bounded event rather than a stale
1031
+ backlog. Locally owned agent history must not use this helper: it retains
1032
+ the complete reported distance and coalesces only its terminal paint.
1036
1033
  """
1037
1034
  direction = event.wheel_direction
1038
1035
  if direction == 0:
@@ -44,6 +44,29 @@ def _action_legend(
44
44
  return urwid.Text(markup, align=align, wrap=wrap)
45
45
 
46
46
 
47
+ def _action_button(
48
+ keys: str,
49
+ description: str,
50
+ *,
51
+ on_click: Callable[[], None],
52
+ click_key: str,
53
+ ) -> ClickableRow:
54
+ """Render a terminal-safe filled action button with a mouse target."""
55
+ label = urwid.Text(
56
+ [
57
+ ("btn_key", keys),
58
+ ("btn_label", f" = {description}"),
59
+ ],
60
+ align="center",
61
+ wrap="space",
62
+ )
63
+ return ClickableRow(
64
+ urwid.AttrMap(label, "btn_label"),
65
+ on_click=on_click,
66
+ click_key=click_key,
67
+ )
68
+
69
+
47
70
  def _attention_lines(attention: AttentionState | None) -> list:
48
71
  """Compact, generic rendering for known and future attention categories."""
49
72
  if attention is None:
@@ -141,38 +164,37 @@ class QuitConfirmModal(urwid.WidgetWrap):
141
164
  summary = "No running sessions."
142
165
 
143
166
  action_rows: list[urwid.Widget] = [
144
- _action_legend(
145
- [("y / ↵", "quit and kill all sessions")],
146
- align="center",
147
- wrap="space",
167
+ _action_button(
168
+ "y / ↵",
169
+ "hard quit (requires final confirmation)",
170
+ on_click=on_confirm,
171
+ click_key="quit:hard",
148
172
  ),
149
173
  ]
150
174
  if on_soft_quit is not None:
151
- action_rows.append(
152
- ClickableRow(
153
- _action_legend(
154
- [("s", "soft quit (keep sessions alive)")],
155
- align="center",
156
- wrap="space",
157
- ),
175
+ action_rows.extend([
176
+ urwid.Divider(),
177
+ _action_button(
178
+ "s",
179
+ "soft quit (keep sessions alive)",
158
180
  on_click=on_soft_quit,
159
181
  click_key="quit:soft",
160
- )
161
- )
162
- cancel_row = _action_legend(
163
- [("n / Esc", "cancel")],
164
- align="center",
165
- wrap="space",
166
- )
167
- action_rows.append(
168
- ClickableRow(
169
- cancel_row,
182
+ ),
183
+ ])
184
+ action_rows.append(urwid.Divider())
185
+ if on_cancel is not None:
186
+ action_rows.append(_action_button(
187
+ "n / Esc",
188
+ "cancel",
170
189
  on_click=on_cancel,
171
190
  click_key="quit:cancel",
172
- )
173
- if on_cancel is not None
174
- else cancel_row
175
- )
191
+ ))
192
+ else:
193
+ action_rows.append(_action_legend(
194
+ [("n / Esc", "cancel")],
195
+ align="center",
196
+ wrap="space",
197
+ ))
176
198
  self._title = urwid.Text("Quit railmux?", align="center")
177
199
  self._summary = urwid.Text(("live", summary), align="center")
178
200
  self._shared_notice = (
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.4.0.dev20
3
+ Version: 0.4.0.dev21
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
@@ -446,7 +446,9 @@ not interrupt it. To leave agents running when you quit Railmux, press `s` for
446
446
  soft quit in the confirmation popup; restarting the same Railmux instance then
447
447
  restores the usable workspace when those sessions are still available. A normal
448
448
  quit ends all running sessions instead and therefore requires a second
449
- confirmation; `y` and Enter keep their existing confirm meaning in both steps.
449
+ confirmation. The first chooser has visible mouse buttons, but the final
450
+ destructive confirmation remains keyboard-only; `y` and Enter keep their
451
+ existing confirm meaning in both steps.
450
452
 
451
453
  All terminals attached to one managed Railmux window view the same UI process.
452
454
  Soft Quit therefore closes that shared UI for every attached view, although it
@@ -95,7 +95,6 @@ from railmux.fast_display_history import (
95
95
  HistoryAction,
96
96
  LocalHistoryView,
97
97
  PeriodicPrefetchGate,
98
- claim_batched_wheel,
99
98
  input_may_change_routes,
100
99
  )
101
100
  from railmux.fast_display_input import (
@@ -4197,7 +4196,7 @@ def test_history_content_cache_keeps_only_recent_pane_lifetimes():
4197
4196
  assert tuple(view.content_cache) == tuple(pane_ids[-8:])
4198
4197
 
4199
4198
 
4200
- def test_local_history_wheel_burst_is_bounded_before_it_moves_viewport():
4199
+ def test_local_history_wheel_batch_preserves_distance_with_one_paint():
4201
4200
  view = LocalHistoryView()
4202
4201
  prefetch = InputFrameDecoder().feed(view.begin_prefetch(1.0))[0]
4203
4202
  request_id, _limit = decode_history_prefetch(prefetch.data)
@@ -4208,26 +4207,100 @@ def test_local_history_wheel_burst_is_bounded_before_it_moves_viewport():
4208
4207
  0,
4209
4208
  30,
4210
4209
  3,
4211
- tuple(f"line-{index}".encode() for index in range(10)),
4210
+ tuple(f"line-{index}".encode() for index in range(20)),
4211
+ )
4212
+ view.accept_prefetch(HistoryBatch(request_id, (route,)))
4213
+ batch = fast_display_client._LocalInputBatch()
4214
+ events = TerminalInputDecoder().feed(b"\x1b[<64;5;2M" * 8)
4215
+ assert len(events) == 8
4216
+ for event in events:
4217
+ assert isinstance(event, SgrMouseEvent)
4218
+ action, deferred_paint = batch.prepare(event, view.pointer_event(event))
4219
+ assert action is not None
4220
+ assert deferred_paint is not None
4221
+ deferred_paint.defer(action)
4222
+
4223
+ assert view.viewports["%8"].offset == 8
4224
+ surface = MagicMock(spec=TerminalSurface)
4225
+ screen = MagicMock(spec=AppliedScreen)
4226
+ overlays = view.overlays()
4227
+
4228
+ batch.flush(surface, screen, overlays, ())
4229
+
4230
+ surface.paint_overlays.assert_called_once_with(screen, overlays, ())
4231
+ surface.paint.assert_not_called()
4232
+
4233
+
4234
+ def test_local_history_wheel_batch_restores_live_once_at_bottom():
4235
+ view = LocalHistoryView()
4236
+ prefetch = InputFrameDecoder().feed(view.begin_prefetch(1.0))[0]
4237
+ request_id, _limit = decode_history_prefetch(prefetch.data)
4238
+ route = HistorySnapshot(
4239
+ request_id,
4240
+ "%8",
4241
+ 0,
4242
+ 0,
4243
+ 30,
4244
+ 3,
4245
+ tuple(f"line-{index}".encode() for index in range(20)),
4212
4246
  )
4213
4247
  view.accept_prefetch(HistoryBatch(request_id, (route,)))
4214
4248
  up = SgrMouseEvent(b"up", 64, 5, 2, True)
4249
+ for _ in range(3):
4250
+ view.pointer_event(up)
4251
+ assert view.viewports["%8"].offset == 3
4252
+
4253
+ batch = fast_display_client._LocalInputBatch()
4215
4254
  down = SgrMouseEvent(b"down", 65, 5, 2, True)
4216
4255
  for _ in range(5):
4217
- view.wheel(up)
4218
- assert view.viewports["%8"].offset == 5
4256
+ action, deferred_paint = batch.prepare(down, view.pointer_event(down))
4257
+ assert action is not None
4258
+ assert deferred_paint is not None
4259
+ deferred_paint.defer(action)
4260
+ assert not view.active
4261
+
4262
+ screen = ScreenModel().apply(
4263
+ ClientScreenUpdateDecoder().feed(
4264
+ encode_update(_keyframe(width=30, height=3))
4265
+ )[0],
4266
+ os.terminal_size((30, 3)),
4267
+ )
4268
+ assert screen is not None
4269
+ surface = MagicMock(spec=TerminalSurface)
4270
+ batch.flush(surface, screen, view.overlays(), ())
4271
+
4272
+ surface.paint.assert_called_once()
4273
+ surface.paint_overlays.assert_not_called()
4274
+
4275
+
4276
+ def test_forwarded_wheel_batch_remains_bounded_without_local_paint():
4277
+ batch = fast_display_client._LocalInputBatch()
4278
+ down = SgrMouseEvent(b"down", 65, 5, 2, True)
4279
+ up = SgrMouseEvent(b"up", 64, 5, 2, True)
4280
+ admitted = []
4219
4281
 
4220
- handled: set[int] = set()
4221
4282
  for _ in range(8):
4222
- if claim_batched_wheel(down, handled):
4223
- view.wheel(down)
4283
+ action, deferred_paint = batch.prepare(
4284
+ down,
4285
+ HistoryAction(forwarded_input=down.raw),
4286
+ )
4287
+ if action is not None:
4288
+ admitted.append(action.forwarded_input)
4289
+ assert deferred_paint is None
4290
+ action, deferred_paint = batch.prepare(
4291
+ up,
4292
+ HistoryAction(forwarded_input=up.raw),
4293
+ )
4294
+ assert action is not None
4295
+ admitted.append(action.forwarded_input)
4296
+ assert deferred_paint is None
4224
4297
 
4225
- # Eight packets accumulated in one read still move only one row.
4226
- assert view.viewports["%8"].offset == 4
4227
- # The opposite direction remains responsive in the same terminal read.
4228
- assert claim_batched_wheel(up, handled) is True
4229
- # A new os.read() owns a fresh set and therefore admits the next tick.
4230
- assert claim_batched_wheel(down, set()) is True
4298
+ surface = MagicMock(spec=TerminalSurface)
4299
+ batch.flush(surface, MagicMock(spec=AppliedScreen), (), ())
4300
+
4301
+ assert admitted == [b"down", b"up"]
4302
+ surface.paint.assert_not_called()
4303
+ surface.paint_overlays.assert_not_called()
4231
4304
 
4232
4305
 
4233
4306
  def test_only_bounded_layout_and_modal_keys_invalidate_live_routes():
@@ -148,7 +148,9 @@ def test_modal_action_legends_use_high_contrast_attribute(tmp_path):
148
148
  ]
149
149
 
150
150
  for modal in modals:
151
- assert "modal_key" in _rendered_attrs(modal), type(modal).__name__
151
+ assert {"modal_key", "btn_key"} & _rendered_attrs(modal), (
152
+ type(modal).__name__
153
+ )
152
154
 
153
155
 
154
156
  def test_quit_confirm_wraps_all_choices_in_narrow_sidebar():
@@ -163,11 +165,27 @@ def test_quit_confirm_wraps_all_choices_in_narrow_sidebar():
163
165
  text = " ".join(
164
166
  _rendered_text(modal, size=(22, height)).replace("│", " ").split())
165
167
 
166
- assert "quit and kill all sessions" in text
168
+ assert "hard quit (requires final confirmation)" in text
167
169
  assert "soft quit (keep sessions alive)" in text
168
170
  assert "n / Esc = cancel" in text
169
171
 
170
172
 
173
+ def test_quit_confirm_choices_have_filled_button_styling():
174
+ modal = QuitConfirmModal(
175
+ on_confirm=lambda: None,
176
+ on_soft_quit=lambda: None,
177
+ on_cancel=lambda: None,
178
+ running_count=2,
179
+ )
180
+
181
+ attrs = _rendered_attrs(
182
+ modal, size=(48, modal.preferred_height(48))
183
+ )
184
+
185
+ assert "btn_key" in attrs
186
+ assert "btn_label" in attrs
187
+
188
+
171
189
  def test_quit_confirm_warns_when_ui_is_shared_by_multiple_clients():
172
190
  modal = QuitConfirmModal(
173
191
  on_confirm=lambda: None,
@@ -185,7 +203,7 @@ def test_quit_confirm_warns_when_ui_is_shared_by_multiple_clients():
185
203
  assert "quitting closes this UI in all of them" in text
186
204
 
187
205
 
188
- def test_quit_confirm_mouse_clicks_soft_quit_and_cancel_but_not_hard_quit():
206
+ def test_quit_confirm_mouse_clicks_hard_soft_quit_and_cancel():
189
207
  called: list[str] = []
190
208
  modal = QuitConfirmModal(
191
209
  on_confirm=lambda: called.append("hard"),
@@ -206,17 +224,11 @@ def test_quit_confirm_mouse_clicks_soft_quit_and_cancel_but_not_hard_quit():
206
224
  size, "mouse press", 1, size[0] // 2, row, True
207
225
  )
208
226
 
227
+ click_row("hard quit")
209
228
  click_row("soft quit")
210
229
  click_row("cancel")
211
- hard_canvas = modal.render(size, focus=True)
212
- hard_row = next(
213
- index
214
- for index, line in enumerate(hard_canvas.text)
215
- if b"quit and kill all sessions" in line
216
- )
217
- modal.mouse_event(size, "mouse press", 1, size[0] // 2, hard_row, True)
218
230
 
219
- assert called == ["soft", "cancel"]
231
+ assert called == ["hard", "soft", "cancel"]
220
232
 
221
233
 
222
234
  def test_hard_quit_confirm_preserves_enter_and_requires_a_second_action():
@@ -1 +0,0 @@
1
- __version__ = "0.4.0.dev20"
File without changes
File without changes
File without changes