railmux 0.4.0.dev1__tar.gz → 0.4.0.dev2__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 (196) hide show
  1. {railmux-0.4.0.dev1/src/railmux.egg-info → railmux-0.4.0.dev2}/PKG-INFO +1 -1
  2. railmux-0.4.0.dev2/src/railmux/__init__.py +1 -0
  3. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/mux/__init__.py +4 -2
  4. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/mux/backend.py +16 -0
  5. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/mux/tmux_backend.py +5 -1
  6. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/ui/app.py +19 -3
  7. railmux-0.4.0.dev2/src/railmux/ui/status_chrome.py +149 -0
  8. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/winlocal/backend.py +212 -38
  9. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/winlocal/compositor.py +51 -18
  10. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2/src/railmux.egg-info}/PKG-INFO +1 -1
  11. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux.egg-info/SOURCES.txt +3 -0
  12. railmux-0.4.0.dev2/tests/test_status_chrome.py +87 -0
  13. railmux-0.4.0.dev2/tests/test_windows_parity_contract.py +79 -0
  14. railmux-0.4.0.dev2/tests/test_winlocal_backend.py +577 -0
  15. railmux-0.4.0.dev1/src/railmux/__init__.py +0 -1
  16. railmux-0.4.0.dev1/tests/test_winlocal_backend.py +0 -218
  17. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/LICENSE +0 -0
  18. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/MANIFEST.in +0 -0
  19. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/README.md +0 -0
  20. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/pyproject.toml +0 -0
  21. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/setup.cfg +0 -0
  22. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/__main__.py +0 -0
  23. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/atomic_file.py +0 -0
  24. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/background_index.py +0 -0
  25. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/cli.py +0 -0
  26. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/codex_index.py +0 -0
  27. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/config.py +0 -0
  28. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/config_cli.py +0 -0
  29. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/diagnostics.py +0 -0
  30. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/discovery.py +0 -0
  31. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/display_transport.py +0 -0
  32. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/fast_display_client.py +0 -0
  33. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/fast_display_history.py +0 -0
  34. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/fast_display_input.py +0 -0
  35. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/fast_display_protocol.py +0 -0
  36. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/fast_display_server.py +0 -0
  37. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/favorites.py +0 -0
  38. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/fuzzy.py +0 -0
  39. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/help_workspace.py +0 -0
  40. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/launcher.py +0 -0
  41. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/legacy_sessions.py +0 -0
  42. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/local_clipboard.py +0 -0
  43. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/local_open.py +0 -0
  44. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/models.py +0 -0
  45. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/modes.py +0 -0
  46. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/mouse_manager.py +0 -0
  47. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/orphan_marker.py +0 -0
  48. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/pane_surface.py +0 -0
  49. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/path_codec.py +0 -0
  50. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/platform/__init__.py +0 -0
  51. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/platform/config_paths.py +0 -0
  52. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/platform/console.py +0 -0
  53. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/platform/file_security.py +0 -0
  54. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/platform/filelock.py +0 -0
  55. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/platform/multiplex.py +0 -0
  56. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/platform/process.py +0 -0
  57. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/platform/runtime_paths.py +0 -0
  58. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/remote_config.py +0 -0
  59. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/renames.py +0 -0
  60. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/restart_state.py +0 -0
  61. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/runtime_config.py +0 -0
  62. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/scroll_agent.py +0 -0
  63. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/scroll_manager.py +0 -0
  64. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/selection_isolation.py +0 -0
  65. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/self_update.py +0 -0
  66. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/session_cache.py +0 -0
  67. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/session_index.py +0 -0
  68. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/setting_contracts.py +0 -0
  69. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/settings.py +0 -0
  70. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/ssh_args.py +0 -0
  71. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/ssh_compat.py +0 -0
  72. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/ssh_display_diagnostics.py +0 -0
  73. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/ssh_doctor.py +0 -0
  74. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/system_deps.py +0 -0
  75. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/terminal_emulator.py +0 -0
  76. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/terminal_status.py +0 -0
  77. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/tmux_binding_manager.py +0 -0
  78. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/tmux_ctl.py +0 -0
  79. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/tmux_health.py +0 -0
  80. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/tmux_server.py +0 -0
  81. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/tool_panes.py +0 -0
  82. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/transcript.py +0 -0
  83. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/ui/__init__.py +0 -0
  84. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/ui/_widgets.py +0 -0
  85. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/ui/keymap.py +0 -0
  86. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/ui/modals.py +0 -0
  87. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/ui/projects_pane.py +0 -0
  88. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/ui/running_pane.py +0 -0
  89. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/ui/sessions_pane.py +0 -0
  90. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/ui/sidebar.py +0 -0
  91. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/ui/statusbar.py +0 -0
  92. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/ui/workspace.py +0 -0
  93. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/winlocal/__init__.py +0 -0
  94. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/winlocal/client.py +0 -0
  95. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/winlocal/conpty.py +0 -0
  96. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/winlocal/daemon.py +0 -0
  97. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/winlocal/history_viewer.py +0 -0
  98. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/winlocal/ipc.py +0 -0
  99. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/winlocal/session_store.py +0 -0
  100. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux/winlocal/virtual_screen.py +0 -0
  101. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux.egg-info/dependency_links.txt +0 -0
  102. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux.egg-info/entry_points.txt +0 -0
  103. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux.egg-info/requires.txt +0 -0
  104. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/src/railmux.egg-info/top_level.txt +0 -0
  105. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_agent_workspace.py +0 -0
  106. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_atomic_file.py +0 -0
  107. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_background_index.py +0 -0
  108. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_cli.py +0 -0
  109. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_codex_delete.py +0 -0
  110. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_codex_index.py +0 -0
  111. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_codex_lineage.py +0 -0
  112. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_codex_transcript.py +0 -0
  113. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_config.py +0 -0
  114. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_config_cli.py +0 -0
  115. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_denested_transport_benchmark.py +0 -0
  116. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_diagnostics.py +0 -0
  117. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_discovery.py +0 -0
  118. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_display_transport.py +0 -0
  119. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_fast_full_window.py +0 -0
  120. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_focus_highlight.py +0 -0
  121. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_fullscreen_binding.py +0 -0
  122. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_fuzzy.py +0 -0
  123. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_help_agent.py +0 -0
  124. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_help_workspace.py +0 -0
  125. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_history_cleanup.py +0 -0
  126. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_keymap.py +0 -0
  127. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_launcher.py +0 -0
  128. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_layout_profile.py +0 -0
  129. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_legacy_sessions.py +0 -0
  130. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_local_clipboard.py +0 -0
  131. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_local_open.py +0 -0
  132. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_modals.py +0 -0
  133. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_mode_selection.py +0 -0
  134. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_modes.py +0 -0
  135. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_mouse_manager.py +0 -0
  136. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_mux_backend.py +0 -0
  137. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_new_project.py +0 -0
  138. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_options.py +0 -0
  139. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_orphan_recovery.py +0 -0
  140. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_pane_surface.py +0 -0
  141. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_path_codec.py +0 -0
  142. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_platform.py +0 -0
  143. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_projects_pane.py +0 -0
  144. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_release_notes.py +0 -0
  145. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_release_workflow.py +0 -0
  146. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_remote_config.py +0 -0
  147. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_renames.py +0 -0
  148. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_restart_state.py +0 -0
  149. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_running_pane.py +0 -0
  150. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_running_sort.py +0 -0
  151. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_runtime_config.py +0 -0
  152. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_scroll_agent.py +0 -0
  153. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_scroll_manager.py +0 -0
  154. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_selection_isolation.py +0 -0
  155. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_self_update.py +0 -0
  156. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_session_cache.py +0 -0
  157. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_session_index.py +0 -0
  158. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_sessions_pane.py +0 -0
  159. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_setting_contracts.py +0 -0
  160. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_settings.py +0 -0
  161. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_sidebar.py +0 -0
  162. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_soft_quit.py +0 -0
  163. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_ssh_args.py +0 -0
  164. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_ssh_compat.py +0 -0
  165. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_ssh_display_benchmark.py +0 -0
  166. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_ssh_display_diagnostics.py +0 -0
  167. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_ssh_doctor.py +0 -0
  168. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_status.py +0 -0
  169. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_statusbar.py +0 -0
  170. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_system_deps.py +0 -0
  171. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_terminal_size.py +0 -0
  172. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_terminal_status.py +0 -0
  173. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_termux_local_touch.py +0 -0
  174. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_tmux_binding_manager.py +0 -0
  175. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_tmux_ctl.py +0 -0
  176. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_tmux_health.py +0 -0
  177. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_tmux_integration.py +0 -0
  178. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_tmux_server.py +0 -0
  179. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_tmux_status.py +0 -0
  180. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_tool_panes.py +0 -0
  181. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_transcript.py +0 -0
  182. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_web_demo_recorder.py +0 -0
  183. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_wheel_smoke.py +0 -0
  184. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_widgets.py +0 -0
  185. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_winlocal_compositor.py +0 -0
  186. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_winlocal_conpty.py +0 -0
  187. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_winlocal_daemon.py +0 -0
  188. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_winlocal_history_viewer.py +0 -0
  189. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_winlocal_ipc.py +0 -0
  190. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_winlocal_real_conpty.py +0 -0
  191. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_winlocal_session_store.py +0 -0
  192. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_winlocal_virtual_screen.py +0 -0
  193. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tests/test_yolo_prompt.py +0 -0
  194. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tools/denested_transport_benchmark.py +0 -0
  195. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/tools/release_notes.py +0 -0
  196. {railmux-0.4.0.dev1 → railmux-0.4.0.dev2}/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.dev1
3
+ Version: 0.4.0.dev2
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.dev2"
@@ -1,5 +1,7 @@
1
1
  """Typed multiplexer boundary shared by tmux and native Windows."""
2
- from railmux.mux.backend import Capabilities, LaunchSpec, MuxBackend
2
+ from railmux.mux.backend import Capabilities, LaunchSpec, MuxBackend, StatusChrome
3
3
  from railmux.mux.tmux_backend import TmuxBackend
4
4
 
5
- __all__ = ["Capabilities", "LaunchSpec", "MuxBackend", "TmuxBackend"]
5
+ __all__ = [
6
+ "Capabilities", "LaunchSpec", "MuxBackend", "StatusChrome", "TmuxBackend",
7
+ ]
@@ -32,6 +32,20 @@ class LaunchSpec:
32
32
  login_shell: bool = False
33
33
 
34
34
 
35
+ @dataclass(frozen=True)
36
+ class StatusChrome:
37
+ """Platform-neutral state for Railmux's persistent bottom chrome.
38
+
39
+ POSIX projects this into tmux's status line; native Windows draws it in
40
+ the daemon compositor. The transient status message is intentionally a
41
+ separate value so it cannot replace the Mode/Layout controls.
42
+ """
43
+
44
+ mode_label: str
45
+ layout_indicator: str | None
46
+ error: bool = False
47
+
48
+
35
49
  class MuxBackend(Protocol):
36
50
  """Core topology/lifecycle surface consumed by the shared UI.
37
51
 
@@ -57,3 +71,5 @@ class MuxBackend(Protocol):
57
71
  def detach_client(self) -> bool: ...
58
72
  def kill_pane(self, pane_id: str) -> bool: ...
59
73
  def kill_session(self, name: str) -> bool: ...
74
+ def set_status_text(self, text: str, level: str) -> None: ...
75
+ def set_status_chrome(self, chrome: StatusChrome) -> None: ...
@@ -6,7 +6,7 @@ from types import ModuleType
6
6
  from pathlib import Path
7
7
 
8
8
  from railmux import tmux_ctl
9
- from railmux.mux.backend import Capabilities
9
+ from railmux.mux.backend import Capabilities, StatusChrome
10
10
 
11
11
 
12
12
  class TmuxBackend:
@@ -128,6 +128,10 @@ class TmuxBackend:
128
128
  # tmux option formatting; native rendering uses this backend hook.
129
129
  pass
130
130
 
131
+ def set_status_chrome(self, _chrome: StatusChrome) -> None:
132
+ # App retains the established tmux option formatting and leases.
133
+ pass
134
+
131
135
  def prepare_launch(
132
136
  self,
133
137
  argv: list[str],
@@ -56,7 +56,7 @@ from railmux.modes import (
56
56
  ProjectSource,
57
57
  )
58
58
  from railmux.models import AttentionState, Project, SessionMeta
59
- from railmux.mux import MuxBackend, TmuxBackend
59
+ from railmux.mux import MuxBackend, StatusChrome, TmuxBackend
60
60
  from railmux.tmux_binding_manager import SharedTmuxBindingManager
61
61
  from railmux.mouse_manager import RootWheelForwardingManager
62
62
  from railmux.renames import Renames
@@ -2028,8 +2028,9 @@ class App:
2028
2028
  return False
2029
2029
  slot.agent_tmux_name = None
2030
2030
  slot.mode_key = self._current_mode_key()
2031
- self.mux.select_pane(slot.pane_id)
2032
- self._set_railmux_focus(False, force_border=True)
2031
+ # A preview is the single-click/Space action. Keep keyboard focus
2032
+ # in the sidebar; only double-click/Enter is allowed to transfer
2033
+ # focus to the displayed surface (P06).
2033
2034
  return True
2034
2035
 
2035
2036
  import shlex
@@ -10033,6 +10034,21 @@ class App:
10033
10034
  initial paint, from _render_status_to_tmux on the normal↔error
10034
10035
  transition, and from mode cycling so the mode indicator repaints.
10035
10036
  Best-effort — a tmux hiccup must not raise into the UI."""
10037
+ # Publish the semantic chrome even when tmux is not the renderer.
10038
+ # Native Windows consumes this hook; POSIX keeps its established tmux
10039
+ # formatting below. Test doubles and older third-party backends remain
10040
+ # best-effort compatible.
10041
+ mux = getattr(self, "mux", None)
10042
+ publish = getattr(mux, "set_status_chrome", None)
10043
+ if callable(publish):
10044
+ try:
10045
+ publish(StatusChrome(
10046
+ mode_label=self._active_mode().label,
10047
+ layout_indicator=self._status_layout_indicator(),
10048
+ error=error,
10049
+ ))
10050
+ except (AttributeError, KeyError, TypeError, ValueError):
10051
+ pass
10036
10052
  if not self._tmux_status_enabled or not self._tmux_status_session:
10037
10053
  return
10038
10054
  bar = _TMUX_BAR_STYLE_ERROR if error else _TMUX_BAR_STYLE_NORMAL
@@ -0,0 +1,149 @@
1
+ """Semantic bottom-chrome projection shared by non-tmux frontends."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+
6
+ from urwid.str_util import calc_width
7
+
8
+ from railmux.ui.workspace import WorkspacePage
9
+
10
+
11
+ ACTION_MODE = "mode"
12
+ ACTION_LAYOUT = "layout"
13
+ ACTION_COPY = "copy"
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class StatusHit:
18
+ """One clickable half-open cell range in the bottom status row."""
19
+
20
+ start: int
21
+ end: int
22
+ action: str
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class StatusProjection:
27
+ """Plain terminal text plus semantic hit targets for one status row."""
28
+
29
+ text: str
30
+ hits: tuple[StatusHit, ...]
31
+ error: bool
32
+
33
+
34
+ def project_status_chrome(
35
+ *,
36
+ width: int,
37
+ mode_label: str,
38
+ layout_indicator: str | None,
39
+ status_text: str,
40
+ status_level: str,
41
+ compact: bool,
42
+ active_page: WorkspacePage,
43
+ page_targets: tuple[str | None, str | None, str | None],
44
+ ) -> StatusProjection:
45
+ """Build the platform-neutral visible controls for a full-width bar.
46
+
47
+ Renderers remain free to choose colours, but labels and click authority
48
+ live here so a native frontend cannot accidentally expose only status-right.
49
+ """
50
+ width = max(1, width)
51
+ parts: list[str] = []
52
+ hits: list[StatusHit] = []
53
+ cells = 0
54
+
55
+ def append(value: str, action: str | None = None) -> None:
56
+ nonlocal cells
57
+ value = _safe_text(value)
58
+ bounded = _fit_cells(value, max(0, width - cells))
59
+ if not bounded:
60
+ return
61
+ start = cells
62
+ parts.append(bounded)
63
+ cells += _cells(bounded)
64
+ if action is not None and cells > start:
65
+ hits.append(StatusHit(start, cells, action))
66
+
67
+ if compact:
68
+ if width < 52:
69
+ labels = ("R", "1", "2")
70
+ visible_mode = (
71
+ "Cx" if mode_label == "Codex" else
72
+ "CC" if mode_label == "Claude Code" else mode_label[:2]
73
+ )
74
+ elif width < 80:
75
+ labels = ("Railmux", "A1", "A2")
76
+ visible_mode = (
77
+ "Codex" if mode_label == "Codex" else
78
+ "CC" if mode_label == "Claude Code" else mode_label[:8]
79
+ )
80
+ else:
81
+ labels = ("Railmux", "Agent 1", "Agent 2")
82
+ visible_mode = mode_label
83
+ pages = (
84
+ WorkspacePage.SIDEBAR,
85
+ WorkspacePage.PRIMARY,
86
+ WorkspacePage.SECONDARY,
87
+ )
88
+ for page, target, label in zip(pages, page_targets, labels):
89
+ # Native terminals do not all expose tmux's per-range style
90
+ # channel. Keep the selected page visible in plain text too.
91
+ marker = f"<{label}>" if page is active_page else f"[{label}]"
92
+ append(marker, f"page:{target}" if target is not None else None)
93
+ append(" ")
94
+ append(visible_mode, ACTION_MODE)
95
+ else:
96
+ append(" Railmux · ")
97
+ append(mode_label, ACTION_MODE)
98
+
99
+ if layout_indicator:
100
+ append(" · ")
101
+ append(layout_indicator, ACTION_LAYOUT)
102
+ append(" ")
103
+
104
+ safe_status = _safe_text(status_text)
105
+ available = max(0, width - cells)
106
+ if safe_status and available:
107
+ visible_status = _fit_cells(safe_status, available)
108
+ status_cells = _cells(visible_status)
109
+ padding = max(0, available - status_cells)
110
+ if padding:
111
+ parts.append(" " * padding)
112
+ cells += padding
113
+ start = cells
114
+ parts.append(visible_status)
115
+ cells += status_cells
116
+ if cells > start:
117
+ hits.append(StatusHit(start, cells, ACTION_COPY))
118
+
119
+ return StatusProjection(
120
+ text="".join(parts),
121
+ hits=tuple(hits),
122
+ error=status_level == "error",
123
+ )
124
+
125
+
126
+ def _safe_text(value: str) -> str:
127
+ return "".join(
128
+ " " if ord(character) < 32 or 0x7F <= ord(character) <= 0x9F
129
+ else character
130
+ for character in value
131
+ )
132
+
133
+
134
+ def _cells(value: str) -> int:
135
+ return calc_width(value, 0, len(value))
136
+
137
+
138
+ def _fit_cells(value: str, available: int) -> str:
139
+ if available <= 0:
140
+ return ""
141
+ result: list[str] = []
142
+ used = 0
143
+ for character in value:
144
+ width = _cells(character)
145
+ if used + width > available:
146
+ break
147
+ result.append(character)
148
+ used += width
149
+ return "".join(result)
@@ -14,10 +14,22 @@ from typing import Callable
14
14
 
15
15
  from railmux import tmux_ctl
16
16
  from railmux.fast_display_input import SgrMouseEvent, TerminalInputDecoder
17
- from railmux.mux.backend import Capabilities, LaunchSpec
17
+ from railmux.mux.backend import Capabilities, LaunchSpec, StatusChrome
18
18
  from railmux.platform.process import provider_argv
19
19
  from railmux.restart_state import OuterTmuxIdentity
20
- from railmux.ui.workspace import DisplayTransportKind, WorkspaceLayout
20
+ from railmux.ui.status_chrome import (
21
+ ACTION_COPY,
22
+ ACTION_LAYOUT,
23
+ ACTION_MODE,
24
+ StatusHit,
25
+ project_status_chrome,
26
+ )
27
+ from railmux.ui.workspace import (
28
+ DisplayTransportKind,
29
+ WorkspaceLayout,
30
+ WorkspacePage,
31
+ WorkspacePresentation,
32
+ )
21
33
  from railmux.winlocal.compositor import Compositor, Region, TerminalPane
22
34
  from railmux.winlocal.conpty import ConPtyProcess, PyWinPtyProcess
23
35
  from railmux.winlocal.history_viewer import HistoryViewer
@@ -78,9 +90,21 @@ class WinMuxBackend:
78
90
  self._last_input_source: object | None = None
79
91
  self._client_count: Callable[[], int] | None = None
80
92
  self._workspace = None
81
- self._screen = VirtualScreen(max(20, width * 3 // 10), max(1, height - 1))
93
+ self._sidebar_width: int | None = None
94
+ # Divider positions are axis-specific. Keeping one value here makes
95
+ # a column width turn into a row height (and vice versa) when the user
96
+ # cycles between side-by-side and stacked layouts.
97
+ self._primary_width: int | None = None
98
+ self._primary_height: int | None = None
99
+ # The sidebar starts as the only surface and therefore owns the full
100
+ # viewport. Opening the first display pane narrows it through the same
101
+ # geometry synchronization path used by later layout changes.
102
+ self._screen = VirtualScreen(width, max(1, height - 1))
82
103
  self._compositor = Compositor(width, height)
83
- self._status = b"railmux"
104
+ self._status_text = ""
105
+ self._status_level = "tip"
106
+ self._status_chrome = StatusChrome("Claude Code", None)
107
+ self._status_hits: tuple[StatusHit, ...] = ()
84
108
  self._lock = threading.RLock()
85
109
  self._input_decoder = TerminalInputDecoder()
86
110
 
@@ -150,7 +174,13 @@ class WinMuxBackend:
150
174
 
151
175
  def set_status_text(self, text: str, level: str) -> None:
152
176
  prefix = {"error": "ERROR: ", "warn": "WARNING: "}.get(level, "")
153
- self._status = (prefix + text).encode("utf-8", errors="replace")
177
+ with self._lock:
178
+ self._status_text = prefix + text
179
+ self._status_level = level
180
+
181
+ def set_status_chrome(self, chrome: StatusChrome) -> None:
182
+ with self._lock:
183
+ self._status_chrome = chrome
154
184
 
155
185
  def create_display_pane(self, key: str) -> str:
156
186
  with self._lock:
@@ -329,7 +359,17 @@ class WinMuxBackend:
329
359
  with self._lock:
330
360
  if not self.pane_alive(pane_id):
331
361
  return False
362
+ if pane_id == self._active_pane:
363
+ return True
332
364
  self._last_pane, self._active_pane = self._active_pane, pane_id
365
+ # tmux status-page selection inside a zoomed window displays the
366
+ # newly selected pane. Preserve that semantic in the native
367
+ # compositor instead of leaving zoom ownership on an invisible
368
+ # previous page.
369
+ if self._zoomed_pane is not None:
370
+ self._zoomed_pane = pane_id
371
+ self._sync_display_sizes()
372
+ self.request_keyframe()
333
373
  return True
334
374
 
335
375
  def active_pane_id(self, _target: str | None = None) -> str:
@@ -340,7 +380,10 @@ class WinMuxBackend:
340
380
 
341
381
  def pane_size(self, pane_id: str):
342
382
  with self._lock:
343
- regions = self._display_regions()
383
+ # tmux preserves the logical sizes of panes hidden by zoom. App
384
+ # uses those sizes to retain/reflow layout profiles, so report the
385
+ # same unzoomed geometry rather than the full-window projection.
386
+ regions = self._layout_regions()
344
387
  if pane_id == "%controller":
345
388
  region = regions.get("sidebar")
346
389
  return (region.width, region.height) if region else None
@@ -364,17 +407,43 @@ class WinMuxBackend:
364
407
  def window_size(self, _pane_id: str):
365
408
  return (self.width, self.height)
366
409
 
367
- def resize_pane_width(self, _pane_id: str, _width: int) -> bool:
368
- return True
410
+ def resize_pane_width(self, pane_id: str, width: int) -> bool:
411
+ with self._lock:
412
+ workspace = self._workspace
413
+ if pane_id == "%controller":
414
+ self._sidebar_width = width
415
+ elif (
416
+ workspace is not None
417
+ and pane_id == workspace.primary.pane_id
418
+ and workspace.layout is WorkspaceLayout.SIDE_BY_SIDE
419
+ ):
420
+ self._primary_width = width
421
+ else:
422
+ return False
423
+ self._sync_display_sizes()
424
+ self.request_keyframe()
425
+ return True
369
426
 
370
- def resize_pane_height(self, _pane_id: str, _height: int) -> bool:
371
- return True
427
+ def resize_pane_height(self, pane_id: str, height: int) -> bool:
428
+ with self._lock:
429
+ workspace = self._workspace
430
+ if (
431
+ workspace is None
432
+ or pane_id != workspace.primary.pane_id
433
+ or workspace.layout is not WorkspaceLayout.STACKED
434
+ ):
435
+ return False
436
+ self._primary_height = height
437
+ self._sync_display_sizes()
438
+ self.request_keyframe()
439
+ return True
372
440
 
373
441
  def toggle_pane_zoom(self, _pane_id: str) -> bool:
374
442
  with self._lock:
375
443
  if not self.pane_alive(_pane_id):
376
444
  return False
377
445
  self._zoomed_pane = None if self._zoomed_pane == _pane_id else _pane_id
446
+ self._sync_display_sizes()
378
447
  self.request_keyframe()
379
448
  return True
380
449
 
@@ -562,6 +631,8 @@ class WinMuxBackend:
562
631
  def screen_update(self):
563
632
  with self._lock:
564
633
  self._sync_display_sizes()
634
+ status = self._project_status()
635
+ self._status_hits = status.hits
565
636
  workspace = self._workspace
566
637
  primary = secondary = None
567
638
  stacked = False
@@ -577,7 +648,9 @@ class WinMuxBackend:
577
648
  )
578
649
  if zoomed is not None:
579
650
  return self._compositor.compose_full(
580
- zoomed, status=self._status
651
+ zoomed,
652
+ status=status.text.encode("utf-8", errors="replace"),
653
+ status_error=status.error,
581
654
  )
582
655
  focus = "sidebar"
583
656
  if workspace is not None and self._active_pane == workspace.primary.pane_id:
@@ -586,7 +659,22 @@ class WinMuxBackend:
586
659
  focus = "secondary"
587
660
  return self._compositor.compose(
588
661
  self._screen.pane, primary, secondary, stacked=stacked,
589
- status=self._status, focus=focus,
662
+ status=status.text.encode("utf-8", errors="replace"),
663
+ status_error=status.error,
664
+ focus=focus,
665
+ show_primary=bool(
666
+ workspace is not None and workspace.primary.pane_id
667
+ ),
668
+ show_secondary=bool(
669
+ workspace is not None
670
+ and workspace.secondary.pane_id
671
+ and workspace.layout is not WorkspaceLayout.SINGLE
672
+ ),
673
+ layout=workspace.layout if workspace is not None else None,
674
+ sidebar_width=self._sidebar_width,
675
+ primary_size=(
676
+ self._primary_height if stacked else self._primary_width
677
+ ),
590
678
  )
591
679
 
592
680
  def request_keyframe(self) -> None:
@@ -600,23 +688,7 @@ class WinMuxBackend:
600
688
  with self._lock:
601
689
  self.width, self.height = width, height
602
690
  self._compositor.resize(width, height)
603
- regions = self._display_regions()
604
- sidebar = regions["sidebar"]
605
- self._screen.resize(sidebar.width, sidebar.height)
606
- for name in ("primary", "secondary"):
607
- region = regions.get(name)
608
- pane_id = self._pane_for_region(name)
609
- preview = self._previews.get(pane_id or "")
610
- if region is not None and preview is not None:
611
- preview.resize(region.width, region.height)
612
- session = self._session_for_region(name)
613
- if region is None or session is None:
614
- continue
615
- if session.terminal is not None:
616
- session.terminal.resize(region.width, region.height)
617
- if session.process is not None and session.process.is_alive():
618
- session.process.resize(region.width, region.height)
619
- session.process_size = (region.width, region.height)
691
+ self._sync_display_sizes()
620
692
 
621
693
  def route_input(self, data: bytes, *, source: object | None = None) -> None:
622
694
  """Route keyboard and SGR mouse reports to the focused surface."""
@@ -634,10 +706,29 @@ class WinMuxBackend:
634
706
  self._write_active(data)
635
707
 
636
708
  def _sync_display_sizes(self) -> None:
637
- """Apply authoritative compositor regions to every displayed ConPTY."""
709
+ """Apply compositor geometry to Urwid, previews, and every ConPTY.
710
+
711
+ A topology change can narrow the sidebar without an outer terminal
712
+ resize. Urwid must receive that viewport change too; otherwise it
713
+ keeps drawing its old wide canvas into a narrow terminal emulator and
714
+ wrapped fragments appear as duplicated rows.
715
+ """
638
716
  regions = self._display_regions()
717
+ sidebar = regions.get("sidebar")
718
+ if (
719
+ sidebar is not None
720
+ and self._screen.get_cols_rows() != (sidebar.width, sidebar.height)
721
+ ):
722
+ self._screen.resize(sidebar.width, sidebar.height)
639
723
  for name in ("primary", "secondary"):
640
724
  region = regions.get(name)
725
+ pane_id = self._pane_for_region(name)
726
+ preview = self._previews.get(pane_id or "")
727
+ if region is not None and preview is not None:
728
+ if (preview.width, preview.height) != (
729
+ region.width, region.height
730
+ ):
731
+ preview.resize(region.width, region.height)
641
732
  session = self._session_for_region(name)
642
733
  if region is None or session is None:
643
734
  continue
@@ -670,10 +761,24 @@ class WinMuxBackend:
670
761
  session = self._session_for_pane(self._active_pane)
671
762
  if session is not None and session.process is not None:
672
763
  session.process.write(data)
764
+ return
765
+ # An empty display slot is not an input sink. This can occur
766
+ # briefly after resetting/reaping a slot while focus still points
767
+ # at its stable pane identity.
768
+ self.select_pane("%controller")
769
+ self._screen.inject(data)
673
770
 
674
771
  def _route_mouse(self, event: SgrMouseEvent) -> None:
675
772
  x, y = event.x - 1, event.y - 1
676
773
  with self._lock:
774
+ if y == self.height - 1:
775
+ if (
776
+ event.pressed
777
+ and not event.button & (32 | 64)
778
+ and event.button & 3 == 0
779
+ ):
780
+ self._route_status_click(x)
781
+ return
677
782
  for name, region in self._display_regions().items():
678
783
  if not (
679
784
  region.x <= x < region.x + region.width
@@ -720,20 +825,79 @@ class WinMuxBackend:
720
825
  return {
721
826
  name: Region(0, 0, self.width, max(1, self.height - 1))
722
827
  }
723
- primary_pane = self._pane_for_region("primary")
724
- secondary_pane = self._pane_for_region("secondary")
725
- primary = self._terminal_for_display(primary_pane)
726
- secondary = self._terminal_for_display(secondary_pane)
828
+ return self._layout_regions()
829
+
830
+ def _layout_regions(self):
831
+ """Return persistent pane geometry without applying zoom projection."""
727
832
  workspace = self._workspace
833
+ stacked = bool(
834
+ workspace is not None
835
+ and workspace.layout is WorkspaceLayout.STACKED
836
+ )
728
837
  return self._compositor.regions(
729
- has_primary=primary is not None,
730
- has_secondary=secondary is not None,
731
- stacked=bool(
838
+ has_primary=bool(
839
+ workspace is not None and workspace.primary.pane_id
840
+ ),
841
+ has_secondary=bool(
732
842
  workspace is not None
733
- and workspace.layout is WorkspaceLayout.STACKED
843
+ and workspace.secondary.pane_id
844
+ and workspace.layout is not WorkspaceLayout.SINGLE
845
+ ),
846
+ stacked=stacked,
847
+ layout=workspace.layout if workspace is not None else None,
848
+ sidebar_width=self._sidebar_width,
849
+ primary_size=(
850
+ self._primary_height if stacked else self._primary_width
734
851
  ),
735
852
  )
736
853
 
854
+ def _project_status(self):
855
+ workspace = self._workspace
856
+ compact = bool(
857
+ workspace is not None
858
+ and workspace.presentation is WorkspacePresentation.COMPACT
859
+ )
860
+ page = (
861
+ workspace.compact_page
862
+ if workspace is not None else WorkspacePage.SIDEBAR
863
+ )
864
+ targets = (
865
+ "%controller",
866
+ workspace.primary.pane_id if workspace is not None else None,
867
+ workspace.secondary.pane_id if workspace is not None else None,
868
+ )
869
+ return project_status_chrome(
870
+ width=self.width,
871
+ mode_label=self._status_chrome.mode_label,
872
+ layout_indicator=self._status_chrome.layout_indicator,
873
+ status_text=self._status_text,
874
+ status_level=(
875
+ "error" if self._status_chrome.error else self._status_level
876
+ ),
877
+ compact=compact,
878
+ active_page=page,
879
+ page_targets=targets,
880
+ )
881
+
882
+ def _route_status_click(self, column: int) -> None:
883
+ hit = next(
884
+ (candidate for candidate in self._status_hits
885
+ if candidate.start <= column < candidate.end),
886
+ None,
887
+ )
888
+ if hit is None:
889
+ return
890
+ if hit.action == ACTION_MODE:
891
+ self._screen.inject(b"\x1b[15~") # F5
892
+ elif hit.action == ACTION_LAYOUT:
893
+ self._screen.inject(b"\x1b[18~") # F7
894
+ elif hit.action == ACTION_COPY:
895
+ self._screen.inject(b"\x1b[17~") # F6
896
+ elif hit.action.startswith("page:"):
897
+ pane_id = hit.action[5:]
898
+ if self.pane_alive(pane_id):
899
+ self.select_pane(pane_id)
900
+
737
901
  def _pane_for_region(self, name: str) -> str | None:
738
902
  workspace = self._workspace
739
903
  if workspace is None:
@@ -826,12 +990,16 @@ class WinDisplayTransport:
826
990
  def create_primary(self, **_kwargs) -> bool:
827
991
  if self.workspace.primary.pane_id is None:
828
992
  self.workspace.primary.pane_id = self.backend.create_display_pane("primary")
993
+ with self.backend._lock:
994
+ self.backend._sync_display_sizes()
829
995
  return True
830
996
 
831
997
  def create_secondary(self, _layout) -> bool:
832
998
  self.create_primary()
833
999
  if self.workspace.secondary.pane_id is None:
834
1000
  self.workspace.secondary.pane_id = self.backend.create_display_pane("secondary")
1001
+ with self.backend._lock:
1002
+ self.backend._sync_display_sizes()
835
1003
  return True
836
1004
 
837
1005
  def create_dual(self, layout, **_kwargs) -> bool:
@@ -845,6 +1013,7 @@ class WinDisplayTransport:
845
1013
  if slot.pane_id is None:
846
1014
  slot.pane_id = self.backend.create_display_pane(slot.key)
847
1015
  with self.backend._lock:
1016
+ self.backend._previews.pop(slot.pane_id, None)
848
1017
  self.backend._displayed[slot.pane_id] = agent_tmux_name
849
1018
  slot.agent_tmux_name = agent_tmux_name
850
1019
  slot.transport_kind = DisplayTransportKind.SWAP
@@ -864,6 +1033,7 @@ class WinDisplayTransport:
864
1033
  if slot.pane_id:
865
1034
  self.backend._displayed.pop(slot.pane_id, None)
866
1035
  self.backend._previews.pop(slot.pane_id, None)
1036
+ self.backend._sync_display_sizes()
867
1037
  return True
868
1038
 
869
1039
  def prepare_preview(self, slot) -> bool:
@@ -873,7 +1043,9 @@ class WinDisplayTransport:
873
1043
  with self.backend._lock:
874
1044
  if slot.pane_id:
875
1045
  self.backend._displayed.pop(slot.pane_id, None)
1046
+ self.backend._previews.pop(slot.pane_id, None)
876
1047
  slot.clear_content()
1048
+ self.backend._sync_display_sizes()
877
1049
  return True
878
1050
 
879
1051
  def prepare_kill(self, _name):
@@ -884,6 +1056,8 @@ class WinDisplayTransport:
884
1056
  if slot.pane_id:
885
1057
  self.backend.kill_pane(slot.pane_id)
886
1058
  slot.clear_display()
1059
+ with self.backend._lock:
1060
+ self.backend._sync_display_sizes()
887
1061
  return True
888
1062
 
889
1063
  def close_all(self) -> bool: