railmux 0.3.3.dev1__tar.gz → 0.3.3.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 (160) hide show
  1. {railmux-0.3.3.dev1/src/railmux.egg-info → railmux-0.3.3.dev2}/PKG-INFO +1 -1
  2. railmux-0.3.3.dev2/src/railmux/__init__.py +1 -0
  3. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/display_transport.py +18 -0
  4. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/ui/app.py +30 -0
  5. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2/src/railmux.egg-info}/PKG-INFO +1 -1
  6. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_display_transport.py +21 -1
  7. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_soft_quit.py +39 -0
  8. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_tmux_integration.py +9 -2
  9. railmux-0.3.3.dev1/src/railmux/__init__.py +0 -1
  10. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/LICENSE +0 -0
  11. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/MANIFEST.in +0 -0
  12. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/README.md +0 -0
  13. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/pyproject.toml +0 -0
  14. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/setup.cfg +0 -0
  15. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/__main__.py +0 -0
  16. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/atomic_file.py +0 -0
  17. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/background_index.py +0 -0
  18. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/cli.py +0 -0
  19. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/codex_index.py +0 -0
  20. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/config.py +0 -0
  21. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/config_cli.py +0 -0
  22. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/diagnostics.py +0 -0
  23. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/discovery.py +0 -0
  24. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/fast_display_client.py +0 -0
  25. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/fast_display_history.py +0 -0
  26. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/fast_display_input.py +0 -0
  27. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/fast_display_protocol.py +0 -0
  28. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/fast_display_server.py +0 -0
  29. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/favorites.py +0 -0
  30. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/fuzzy.py +0 -0
  31. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/help_workspace.py +0 -0
  32. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/launcher.py +0 -0
  33. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/legacy_sessions.py +0 -0
  34. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/local_clipboard.py +0 -0
  35. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/local_open.py +0 -0
  36. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/models.py +0 -0
  37. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/modes.py +0 -0
  38. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/mouse_manager.py +0 -0
  39. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/orphan_marker.py +0 -0
  40. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/pane_surface.py +0 -0
  41. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/path_codec.py +0 -0
  42. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/remote_config.py +0 -0
  43. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/renames.py +0 -0
  44. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/restart_state.py +0 -0
  45. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/runtime_config.py +0 -0
  46. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/scroll_agent.py +0 -0
  47. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/scroll_manager.py +0 -0
  48. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/selection_isolation.py +0 -0
  49. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/self_update.py +0 -0
  50. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/session_cache.py +0 -0
  51. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/session_index.py +0 -0
  52. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/setting_contracts.py +0 -0
  53. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/settings.py +0 -0
  54. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/ssh_args.py +0 -0
  55. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/ssh_compat.py +0 -0
  56. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/ssh_display_diagnostics.py +0 -0
  57. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/ssh_doctor.py +0 -0
  58. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/system_deps.py +0 -0
  59. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/terminal_status.py +0 -0
  60. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/tmux_binding_manager.py +0 -0
  61. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/tmux_ctl.py +0 -0
  62. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/tmux_health.py +0 -0
  63. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/tmux_server.py +0 -0
  64. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/tool_panes.py +0 -0
  65. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/transcript.py +0 -0
  66. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/ui/__init__.py +0 -0
  67. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/ui/_widgets.py +0 -0
  68. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/ui/keymap.py +0 -0
  69. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/ui/modals.py +0 -0
  70. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/ui/projects_pane.py +0 -0
  71. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/ui/running_pane.py +0 -0
  72. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/ui/sessions_pane.py +0 -0
  73. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/ui/sidebar.py +0 -0
  74. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/ui/statusbar.py +0 -0
  75. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux/ui/workspace.py +0 -0
  76. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux.egg-info/SOURCES.txt +0 -0
  77. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux.egg-info/dependency_links.txt +0 -0
  78. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux.egg-info/entry_points.txt +0 -0
  79. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux.egg-info/requires.txt +0 -0
  80. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/src/railmux.egg-info/top_level.txt +0 -0
  81. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_agent_workspace.py +0 -0
  82. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_atomic_file.py +0 -0
  83. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_background_index.py +0 -0
  84. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_cli.py +0 -0
  85. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_codex_delete.py +0 -0
  86. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_codex_index.py +0 -0
  87. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_codex_lineage.py +0 -0
  88. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_codex_transcript.py +0 -0
  89. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_config.py +0 -0
  90. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_config_cli.py +0 -0
  91. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_denested_transport_benchmark.py +0 -0
  92. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_diagnostics.py +0 -0
  93. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_discovery.py +0 -0
  94. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_fast_full_window.py +0 -0
  95. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_focus_highlight.py +0 -0
  96. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_fullscreen_binding.py +0 -0
  97. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_fuzzy.py +0 -0
  98. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_help_agent.py +0 -0
  99. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_help_workspace.py +0 -0
  100. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_history_cleanup.py +0 -0
  101. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_keymap.py +0 -0
  102. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_launcher.py +0 -0
  103. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_layout_profile.py +0 -0
  104. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_legacy_sessions.py +0 -0
  105. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_local_clipboard.py +0 -0
  106. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_local_open.py +0 -0
  107. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_modals.py +0 -0
  108. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_mode_selection.py +0 -0
  109. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_modes.py +0 -0
  110. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_mouse_manager.py +0 -0
  111. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_new_project.py +0 -0
  112. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_options.py +0 -0
  113. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_orphan_recovery.py +0 -0
  114. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_pane_surface.py +0 -0
  115. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_path_codec.py +0 -0
  116. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_projects_pane.py +0 -0
  117. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_release_notes.py +0 -0
  118. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_release_workflow.py +0 -0
  119. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_remote_config.py +0 -0
  120. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_renames.py +0 -0
  121. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_restart_state.py +0 -0
  122. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_running_pane.py +0 -0
  123. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_running_sort.py +0 -0
  124. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_runtime_config.py +0 -0
  125. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_scroll_agent.py +0 -0
  126. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_scroll_manager.py +0 -0
  127. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_selection_isolation.py +0 -0
  128. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_self_update.py +0 -0
  129. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_session_cache.py +0 -0
  130. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_session_index.py +0 -0
  131. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_sessions_pane.py +0 -0
  132. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_setting_contracts.py +0 -0
  133. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_settings.py +0 -0
  134. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_sidebar.py +0 -0
  135. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_ssh_args.py +0 -0
  136. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_ssh_compat.py +0 -0
  137. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_ssh_display_benchmark.py +0 -0
  138. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_ssh_display_diagnostics.py +0 -0
  139. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_ssh_doctor.py +0 -0
  140. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_status.py +0 -0
  141. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_statusbar.py +0 -0
  142. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_system_deps.py +0 -0
  143. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_terminal_size.py +0 -0
  144. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_terminal_status.py +0 -0
  145. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_termux_local_touch.py +0 -0
  146. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_tmux_binding_manager.py +0 -0
  147. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_tmux_ctl.py +0 -0
  148. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_tmux_health.py +0 -0
  149. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_tmux_server.py +0 -0
  150. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_tmux_status.py +0 -0
  151. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_tool_panes.py +0 -0
  152. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_transcript.py +0 -0
  153. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_utf8_state.py +0 -0
  154. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_web_demo_recorder.py +0 -0
  155. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_wheel_smoke.py +0 -0
  156. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_widgets.py +0 -0
  157. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tests/test_yolo_prompt.py +0 -0
  158. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tools/denested_transport_benchmark.py +0 -0
  159. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tools/release_notes.py +0 -0
  160. {railmux-0.3.3.dev1 → railmux-0.3.3.dev2}/tools/wheel_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.3.3.dev1
3
+ Version: 0.3.3.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.3.3.dev2"
@@ -738,6 +738,22 @@ class AgentDisplayTransport:
738
738
  def _with_phase(state: SwapState, phase: str) -> SwapState:
739
739
  return SwapState(**{**asdict(state), "phase": phase})
740
740
 
741
+ @staticmethod
742
+ def _fit_home_to_display(state: SwapState) -> None:
743
+ """Best-effort preserve the provider's visible geometry at home.
744
+
745
+ While a real pane is displayed, its named session contains only the
746
+ inert placeholder. Resize that detached home window to the real
747
+ pane's current size before swapping back. Otherwise background output
748
+ is formatted at the placeholder's older (often much narrower) launch
749
+ geometry and later appears as a narrow discontinuity in scrollback.
750
+
751
+ Geometry is presentation-only: failure must never weaken or abort the
752
+ identity-pinned return transaction.
753
+ """
754
+ tmux_ctl.fit_session_to_pane(
755
+ state.agent_tmux_name, state.agent_pane_id)
756
+
741
757
  def park(self, slot: AgentSlot) -> bool:
742
758
  """Move one displayed swap agent home without forgetting its slot.
743
759
 
@@ -764,6 +780,7 @@ class AgentDisplayTransport:
764
780
  parking = self._with_phase(state, "parking")
765
781
  if not _write_marker_pair(parking, slot.key):
766
782
  return False
783
+ self._fit_home_to_display(state)
767
784
  if not tmux_ctl.swap_panes(
768
785
  state.agent_pane_id, state.placeholder_pane_id):
769
786
  _write_marker_pair(state, slot.key)
@@ -857,6 +874,7 @@ class AgentDisplayTransport:
857
874
  return False
858
875
  returning = SwapState(**{**asdict(state), "phase": "returning"})
859
876
  _write_marker_pair(returning, slot.key)
877
+ self._fit_home_to_display(state)
860
878
  if not tmux_ctl.swap_panes(
861
879
  state.agent_pane_id, state.placeholder_pane_id):
862
880
  return False
@@ -8520,6 +8520,7 @@ class App:
8520
8520
  # Codex New Project can use its in-memory synthetic project earlier.
8521
8521
  by_path = {p.real_path: p for p in projects}
8522
8522
  claimed = self._running_session_ids() | set(self._running)
8523
+ resolved_any = False
8523
8524
  for r in placeholders:
8524
8525
  # Codex New Project owns an in-memory synthetic project before its
8525
8526
  # first rollout makes that cwd visible in the Codex index. Claude
@@ -8605,15 +8606,30 @@ class App:
8605
8606
  r.project = candidate.project
8606
8607
  r.placeholder_path = None
8607
8608
  r.created_at = 0.0
8609
+ # Publish the same provider generation atomically to Running. The
8610
+ # normal refinement pass below may add process-derived status, but
8611
+ # the promoted row must never retain its stale ``(new)`` label,
8612
+ # zero mtime, or placeholder activity for one extra refresh.
8613
+ r.status = candidate.status
8614
+ r.last_mtime = candidate.last_mtime
8615
+ r.attention = candidate.attention
8608
8616
  self._running[candidate.session_id] = r
8609
8617
  self._stamp_running(r)
8610
8618
  claimed.add(candidate.session_id)
8619
+ resolved_any = True
8611
8620
  displayed_slot = self._agent_workspace().slot_for_agent(r.tmux_name)
8612
8621
  if displayed_slot is not None:
8613
8622
  self._set_slot_active_target(
8614
8623
  displayed_slot, candidate.session_id, r.tmux_name)
8615
8624
  if displayed_slot is self._agent_workspace().target:
8616
8625
  self._set_current_project(candidate.project)
8626
+ if resolved_any:
8627
+ # Placeholder launch time may already have consumed the ordinary
8628
+ # one-minute sort budget. Its first authoritative provider
8629
+ # generation is a one-time identity transition, so allow the
8630
+ # immediately following Running refresh to place it by real
8631
+ # activity without enabling per-poll row movement.
8632
+ self._running_sort_ts = 0.0
8617
8633
 
8618
8634
  def _correlate_codex_rollout(self, r: "_Running") -> set[str] | None:
8619
8635
  """Exact child→rollout correlation for a Codex placeholder (#12).
@@ -8628,6 +8644,20 @@ class App:
8628
8644
  (heuristic) rather than raising into the UI."""
8629
8645
  try:
8630
8646
  sessions_dir = self._codex_home_path() / "sessions"
8647
+ # Swap display moves the real provider pane out of its named home
8648
+ # session, leaving an inert placeholder there. Follow the exact
8649
+ # displayed pane identity just as status aggregation does;
8650
+ # querying the home session in this state can only inspect the
8651
+ # placeholder and leaves every new session unresolved until the
8652
+ # first preview happens to return it home.
8653
+ real_pane = self._display_transport().displayed_real_pane(
8654
+ r.tmux_name)
8655
+ if real_pane is not None:
8656
+ identity = tmux_ctl.pane_identity(real_pane)
8657
+ if identity is None:
8658
+ return set() if tmux_ctl.proc_fs_available() else None
8659
+ return tmux_ctl.process_tree_rollout_ids(
8660
+ identity.pane_pid, sessions_dir)
8631
8661
  return tmux_ctl.session_rollout_ids(r.tmux_name, sessions_dir)
8632
8662
  except Exception:
8633
8663
  # On a procfs platform an inspection failure is ambiguity, not
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.3.3.dev1
3
+ Version: 0.3.3.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
@@ -41,6 +41,7 @@ class FakeTmux:
41
41
  self.next_pane = 10
42
42
  self.next_session = 10
43
43
  self.swap_calls: list[tuple[str, str]] = []
44
+ self.fit_calls: list[tuple[str, str]] = []
44
45
  self.killed_sessions: list[str] = []
45
46
  self.fail_marker_window: str | None = None
46
47
  self.fail_swap_at: int | None = None
@@ -69,7 +70,7 @@ class FakeTmux:
69
70
  "split_window_v": self.split_window_h,
70
71
  "create_dual_pane_layout": self.create_dual_pane_layout,
71
72
  "respawn_pane": self.respawn_pane,
72
- "fit_session_to_pane": lambda *_args: True,
73
+ "fit_session_to_pane": self.fit_session_to_pane,
73
74
  "wait_session_detached": lambda name, timeout=1.0: (
74
75
  self.sessions[name]["attached"] == 0),
75
76
  "create_grouped_session": self.create_grouped_session,
@@ -218,6 +219,10 @@ class FakeTmux:
218
219
  self._relocate(target, source_window)
219
220
  return True
220
221
 
222
+ def fit_session_to_pane(self, session, pane):
223
+ self.fit_calls.append((session, pane))
224
+ return True
225
+
221
226
  def kill_session(self, name):
222
227
  session = self.sessions.pop(name, None)
223
228
  if session is None:
@@ -310,6 +315,7 @@ def test_successful_swap_out_and_home(rig):
310
315
  assert fake.window_options
311
316
 
312
317
  assert manager.return_home(workspace.primary)
318
+ assert fake.fit_calls == [("agent-a", "%2")]
313
319
  assert fake.panes["%2"].window_id == "@2"
314
320
  assert workspace.primary.pane_id == placeholder
315
321
  assert workspace.primary.swap_state is None
@@ -324,6 +330,7 @@ def test_compact_parking_keeps_agent_home_and_slot_placeholder_stable(rig):
324
330
  assert state is not None
325
331
 
326
332
  assert manager.park(workspace.primary)
333
+ assert fake.fit_calls == [("agent-a", "%2")]
327
334
  assert workspace.primary.display_parked is True
328
335
  assert workspace.primary.agent_tmux_name == "agent-a"
329
336
  assert workspace.primary.pane_id == state.placeholder_pane_id
@@ -356,6 +363,19 @@ def test_compact_park_swap_failure_leaves_agent_displayed(rig):
356
363
  assert fake.panes["%2"].window_id == "@1"
357
364
 
358
365
 
366
+ def test_return_geometry_failure_does_not_weaken_identity_transaction(
367
+ rig, monkeypatch,
368
+ ):
369
+ fake, workspace, manager = rig
370
+ assert manager.attach(workspace.primary, "agent-a").ok
371
+ monkeypatch.setattr(
372
+ transport_mod.tmux_ctl, "fit_session_to_pane", lambda *_args: False)
373
+
374
+ assert manager.return_home(workspace.primary)
375
+ assert fake.panes["%2"].window_id == "@2"
376
+ assert workspace.primary.swap_state is None
377
+
378
+
359
379
  def test_compact_park_marker_commit_failure_rolls_back_display(rig, monkeypatch):
360
380
  fake, workspace, manager = rig
361
381
  assert manager.attach(workspace.primary, "agent-a").ok
@@ -2817,6 +2817,7 @@ def test_resolve_placeholders_codex_rekeys_to_real_uuid():
2817
2817
  app = App.__new__(App)
2818
2818
  app._codex_mode = True
2819
2819
  app._right_pane_claude = None
2820
+ app._running_sort_ts = 1234.0
2820
2821
  app._running = {
2821
2822
  "__new__-1": _Running(
2822
2823
  key="__new__-1", tmux_name="cx-new----1",
@@ -2840,6 +2841,10 @@ def test_resolve_placeholders_codex_rekeys_to_real_uuid():
2840
2841
  entry = app._running[real_id]
2841
2842
  assert entry.tmux_name == "cx-new----1" # same tmux session, re-keyed
2842
2843
  assert not entry.is_placeholder
2844
+ assert entry.label == "test-proj/Real session"
2845
+ assert entry.status == "idle"
2846
+ assert entry.last_mtime == 1000.0
2847
+ assert app._running_sort_ts == 0.0
2843
2848
  app._codex_index.sessions_for_cwd.assert_called_once_with(
2844
2849
  proj.real_path, refresh=False)
2845
2850
 
@@ -3180,6 +3185,40 @@ def test_correlate_codex_rollout_fails_closed_without_config(monkeypatch):
3180
3185
  assert app._correlate_codex_rollout(r) == set()
3181
3186
 
3182
3187
 
3188
+ def test_correlate_codex_rollout_follows_swap_displayed_real_pane(monkeypatch):
3189
+ """A swap leaves only an inert placeholder in the named home session."""
3190
+ session_id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
3191
+ sessions_dir = Path("/codex/sessions")
3192
+ app = App.__new__(App)
3193
+ app._codex_home_path = lambda: sessions_dir.parent
3194
+ manager = MagicMock()
3195
+ manager.displayed_real_pane.return_value = "%42"
3196
+ app._display_transport_manager = manager
3197
+ running = _Running(
3198
+ key="__new__-tok-1",
3199
+ tmux_name="cx-new----tok-1",
3200
+ label="test-proj/(new)",
3201
+ session_type="codex",
3202
+ )
3203
+ monkeypatch.setattr(
3204
+ tmux_ctl,
3205
+ "pane_identity",
3206
+ lambda pane: tmux_ctl.PaneIdentity(
3207
+ pane, 4242, "railmux", "$1", "@1", False, 100, 30
3208
+ ),
3209
+ )
3210
+ process_probe = MagicMock(return_value={session_id})
3211
+ monkeypatch.setattr(tmux_ctl, "process_tree_rollout_ids", process_probe)
3212
+ home_probe = MagicMock(side_effect=AssertionError(
3213
+ "the home session contains only the swap placeholder"
3214
+ ))
3215
+ monkeypatch.setattr(tmux_ctl, "session_rollout_ids", home_probe)
3216
+
3217
+ assert app._correlate_codex_rollout(running) == {session_id}
3218
+ process_probe.assert_called_once_with(4242, sessions_dir)
3219
+ home_probe.assert_not_called()
3220
+
3221
+
3183
3222
  def test_launch_snapshots_pre_existing_ids(monkeypatch):
3184
3223
  """_launch captures the cwd's existing session ids into the placeholder's
3185
3224
  pre_launch_ids before starting the child (#12)."""
@@ -2308,7 +2308,7 @@ def test_real_compact_parking_keeps_hidden_provider_geometry_stable(
2308
2308
  check=True,
2309
2309
  )
2310
2310
  subprocess.run(
2311
- ["tmux", "new-session", "-d", "-x", "82", "-y", "39",
2311
+ ["tmux", "new-session", "-d", "-x", "46", "-y", "25",
2312
2312
  "-s", "compact-park-agent", "sleep 60"],
2313
2313
  check=True,
2314
2314
  )
@@ -2327,10 +2327,17 @@ def test_real_compact_parking_keeps_hidden_provider_geometry_stable(
2327
2327
  state = workspace.primary.swap_state
2328
2328
  assert state is not None
2329
2329
  provider_pid = tmux_ctl.pane_identity(state.agent_pane_id).pane_pid
2330
+ visible_size = tmux_ctl.pane_size(state.agent_pane_id)
2331
+ assert visible_size is not None
2332
+ assert visible_size[0] == 82
2333
+ assert visible_size != (46, 25)
2330
2334
 
2331
2335
  assert manager.park(workspace.primary)
2332
2336
  parked_size = tmux_ctl.pane_size(state.agent_pane_id)
2333
- assert parked_size is not None
2337
+ # The detached home began at 46x25. Parking must carry over the real
2338
+ # pane's visible geometry instead of narrowing background output back to
2339
+ # that stale launch size.
2340
+ assert parked_size == visible_size
2334
2341
  subprocess.run(
2335
2342
  ["tmux", "resize-window", "-t", display_session, "-x", "105", "-y", "20"],
2336
2343
  check=True,
@@ -1 +0,0 @@
1
- __version__ = "0.3.3.dev1"
File without changes
File without changes
File without changes
File without changes