railmux 0.4.0.dev31__tar.gz → 0.4.0.dev33__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 (183) hide show
  1. {railmux-0.4.0.dev31/src/railmux.egg-info → railmux-0.4.0.dev33}/PKG-INFO +1 -1
  2. railmux-0.4.0.dev33/src/railmux/__init__.py +1 -0
  3. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/transcript.py +46 -8
  4. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/ui/app.py +96 -44
  5. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/windows_bootstrap.py +12 -0
  6. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/windows_install_log.py +6 -1
  7. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/windows_msys2.py +339 -12
  8. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33/src/railmux.egg-info}/PKG-INFO +1 -1
  9. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_codex_delete.py +6 -3
  10. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_focus_highlight.py +122 -3
  11. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_sessions_pane.py +33 -0
  12. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_tmux_integration.py +66 -0
  13. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_transcript.py +38 -0
  14. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_windows_bootstrap.py +30 -1
  15. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_windows_install_log.py +12 -0
  16. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_windows_msys2.py +183 -0
  17. railmux-0.4.0.dev31/src/railmux/__init__.py +0 -1
  18. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/LICENSE +0 -0
  19. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/MANIFEST.in +0 -0
  20. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/README.md +0 -0
  21. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/pyproject.toml +0 -0
  22. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/setup.cfg +0 -0
  23. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/__main__.py +0 -0
  24. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/atomic_file.py +0 -0
  25. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/background_index.py +0 -0
  26. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/cli.py +0 -0
  27. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/codex_index.py +0 -0
  28. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/config.py +0 -0
  29. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/config_cli.py +0 -0
  30. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/diagnostics.py +0 -0
  31. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/discovery.py +0 -0
  32. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/display_transport.py +0 -0
  33. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/entrypoint.py +0 -0
  34. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/fast_display_client.py +0 -0
  35. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/fast_display_history.py +0 -0
  36. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/fast_display_input.py +0 -0
  37. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/fast_display_protocol.py +0 -0
  38. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/fast_display_server.py +0 -0
  39. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/favorites.py +0 -0
  40. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/fuzzy.py +0 -0
  41. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/help_workspace.py +0 -0
  42. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/launcher.py +0 -0
  43. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/legacy_sessions.py +0 -0
  44. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/local_clipboard.py +0 -0
  45. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/local_open.py +0 -0
  46. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/models.py +0 -0
  47. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/modes.py +0 -0
  48. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/mouse_manager.py +0 -0
  49. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/orphan_marker.py +0 -0
  50. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/pane_surface.py +0 -0
  51. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/path_codec.py +0 -0
  52. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/provider_paths.py +0 -0
  53. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/remote_config.py +0 -0
  54. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/renames.py +0 -0
  55. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/restart_state.py +0 -0
  56. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/runtime_config.py +0 -0
  57. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/scroll_agent.py +0 -0
  58. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/scroll_manager.py +0 -0
  59. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/selection_isolation.py +0 -0
  60. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/self_update.py +0 -0
  61. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/session_cache.py +0 -0
  62. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/session_index.py +0 -0
  63. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/session_lease.py +0 -0
  64. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/setting_contracts.py +0 -0
  65. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/settings.py +0 -0
  66. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/ssh_args.py +0 -0
  67. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/ssh_compat.py +0 -0
  68. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/ssh_display_diagnostics.py +0 -0
  69. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/ssh_doctor.py +0 -0
  70. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/system_deps.py +0 -0
  71. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/terminal_status.py +0 -0
  72. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/tmux_binding_manager.py +0 -0
  73. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/tmux_ctl.py +0 -0
  74. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/tmux_health.py +0 -0
  75. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/tmux_server.py +0 -0
  76. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/tool_panes.py +0 -0
  77. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/ui/__init__.py +0 -0
  78. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/ui/_widgets.py +0 -0
  79. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/ui/keymap.py +0 -0
  80. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/ui/modals.py +0 -0
  81. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/ui/projects_pane.py +0 -0
  82. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/ui/running_pane.py +0 -0
  83. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/ui/sessions_pane.py +0 -0
  84. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/ui/sidebar.py +0 -0
  85. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/ui/statusbar.py +0 -0
  86. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/ui/workspace.py +0 -0
  87. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/windows_attach_relay.py +0 -0
  88. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/windows_pacman.py +0 -0
  89. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/windows_paths.py +0 -0
  90. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/windows_tmux_lifecycle.py +0 -0
  91. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux/windows_ui_transition.py +0 -0
  92. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux.egg-info/SOURCES.txt +0 -0
  93. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux.egg-info/dependency_links.txt +0 -0
  94. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux.egg-info/entry_points.txt +0 -0
  95. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux.egg-info/requires.txt +0 -0
  96. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/src/railmux.egg-info/top_level.txt +0 -0
  97. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_agent_workspace.py +0 -0
  98. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_atomic_file.py +0 -0
  99. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_background_index.py +0 -0
  100. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_cli.py +0 -0
  101. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_codex_index.py +0 -0
  102. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_codex_lineage.py +0 -0
  103. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_codex_transcript.py +0 -0
  104. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_config.py +0 -0
  105. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_config_cli.py +0 -0
  106. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_denested_transport_benchmark.py +0 -0
  107. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_diagnostics.py +0 -0
  108. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_discovery.py +0 -0
  109. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_display_transport.py +0 -0
  110. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_fast_full_window.py +0 -0
  111. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_fullscreen_binding.py +0 -0
  112. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_fuzzy.py +0 -0
  113. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_help_agent.py +0 -0
  114. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_help_workspace.py +0 -0
  115. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_history_cleanup.py +0 -0
  116. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_keymap.py +0 -0
  117. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_launcher.py +0 -0
  118. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_layout_profile.py +0 -0
  119. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_legacy_sessions.py +0 -0
  120. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_local_clipboard.py +0 -0
  121. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_local_open.py +0 -0
  122. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_modals.py +0 -0
  123. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_mode_selection.py +0 -0
  124. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_modes.py +0 -0
  125. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_mouse_manager.py +0 -0
  126. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_new_project.py +0 -0
  127. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_options.py +0 -0
  128. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_orphan_recovery.py +0 -0
  129. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_pane_surface.py +0 -0
  130. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_path_codec.py +0 -0
  131. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_projects_pane.py +0 -0
  132. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_provider_paths.py +0 -0
  133. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_release_notes.py +0 -0
  134. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_release_workflow.py +0 -0
  135. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_remote_config.py +0 -0
  136. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_renames.py +0 -0
  137. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_restart_state.py +0 -0
  138. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_running_pane.py +0 -0
  139. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_running_sort.py +0 -0
  140. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_runtime_config.py +0 -0
  141. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_scroll_agent.py +0 -0
  142. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_scroll_manager.py +0 -0
  143. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_selection_isolation.py +0 -0
  144. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_self_update.py +0 -0
  145. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_session_cache.py +0 -0
  146. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_session_index.py +0 -0
  147. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_session_lease.py +0 -0
  148. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_setting_contracts.py +0 -0
  149. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_settings.py +0 -0
  150. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_sidebar.py +0 -0
  151. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_soft_quit.py +0 -0
  152. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_ssh_args.py +0 -0
  153. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_ssh_compat.py +0 -0
  154. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_ssh_display_benchmark.py +0 -0
  155. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_ssh_display_diagnostics.py +0 -0
  156. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_ssh_doctor.py +0 -0
  157. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_status.py +0 -0
  158. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_statusbar.py +0 -0
  159. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_system_deps.py +0 -0
  160. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_terminal_size.py +0 -0
  161. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_terminal_status.py +0 -0
  162. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_termux_local_touch.py +0 -0
  163. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_tmux_binding_manager.py +0 -0
  164. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_tmux_ctl.py +0 -0
  165. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_tmux_health.py +0 -0
  166. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_tmux_server.py +0 -0
  167. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_tmux_status.py +0 -0
  168. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_tool_panes.py +0 -0
  169. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_utf8_state.py +0 -0
  170. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_web_demo_recorder.py +0 -0
  171. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_wheel_smoke.py +0 -0
  172. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_widgets.py +0 -0
  173. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_windows_attach_relay.py +0 -0
  174. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_windows_pacman.py +0 -0
  175. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_windows_paths.py +0 -0
  176. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_windows_tmux_lifecycle.py +0 -0
  177. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_windows_ui_transition.py +0 -0
  178. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_windows_wrapper_contract.py +0 -0
  179. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tests/test_yolo_prompt.py +0 -0
  180. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tools/denested_transport_benchmark.py +0 -0
  181. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tools/release_notes.py +0 -0
  182. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tools/wheel_smoke.py +0 -0
  183. {railmux-0.4.0.dev31 → railmux-0.4.0.dev33}/tools/windows_archive_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.4.0.dev31
3
+ Version: 0.4.0.dev33
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.dev33"
@@ -7,6 +7,7 @@ Usage::
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ import io
10
11
  import json
11
12
  import os
12
13
  import sys
@@ -41,17 +42,46 @@ _SKIP_TYPES = frozenset({
41
42
  })
42
43
 
43
44
  # Top-level record types that only ever appear in a Codex rollout JSONL. The
44
- # preview pipeline in the UI runs ``tail -n 2000`` before piping into this
45
- # module, so for rollouts longer than 2000 lines the leading ``session_meta``
46
- # header is dropped and the first record seen here is a ``response_item`` /
47
- # ``event_msg`` / ``turn_context``. Detect Codex by the *presence* of any of
48
- # these types rather than by a leading ``session_meta`` (see issue #5).
45
+ # preview reader keeps only the final 2,000 records, so for longer rollouts the
46
+ # leading ``session_meta`` header is dropped and the first record seen here is
47
+ # a ``response_item`` / ``event_msg`` / ``turn_context``. Detect Codex by the
48
+ # *presence* of any of these types rather than by a leading ``session_meta``
49
+ # (see issue #5).
49
50
  _CODEX_TYPES = frozenset({
50
51
  "session_meta", "response_item", "event_msg", "turn_context",
51
52
  "compacted", "world_state",
52
53
  })
53
54
  _CLAUDE_TYPES = _SKIP_TYPES | frozenset({"user", "assistant"})
54
55
 
56
+ _TAIL_BLOCK_SIZE = 64 * 1024
57
+
58
+
59
+ def _tail_utf8_lines(path: Path, limit: int) -> io.StringIO:
60
+ """Return the final *limit* UTF-8 lines without scanning from the start.
61
+
62
+ The preview caller previously launched a separate ``tail`` process before
63
+ the Python renderer. Reading backwards here removes that process from the
64
+ latency-sensitive Windows path while retaining the same record boundary.
65
+ One extra newline is collected before slicing so the first returned line
66
+ can never be a block-boundary fragment.
67
+ """
68
+ chunks: list[bytes] = []
69
+ newline_count = 0
70
+ with path.open("rb") as stream:
71
+ stream.seek(0, os.SEEK_END)
72
+ position = stream.tell()
73
+ while position > 0 and newline_count <= limit:
74
+ size = min(_TAIL_BLOCK_SIZE, position)
75
+ position -= size
76
+ stream.seek(position)
77
+ chunk = stream.read(size)
78
+ chunks.append(chunk)
79
+ newline_count += chunk.count(b"\n")
80
+ payload = b"".join(reversed(chunks))
81
+ lines = payload.splitlines(keepends=True)
82
+ selected = b"".join(lines[-limit:])
83
+ return io.StringIO(selected.decode("utf-8", errors="replace"))
84
+
55
85
 
56
86
  def _sanitize_text(value: object) -> str:
57
87
  """Return display text with terminal control sequences made inert.
@@ -382,8 +412,8 @@ def format_transcript(
382
412
 
383
413
  *fmt* is an optional explicit format hint (``"codex"`` or ``"claude"``).
384
414
  When omitted (the default), the format is auto-detected from the record
385
- types actually present in the stream, so a Codex rollout whose leading
386
- ``session_meta`` header was stripped by ``tail`` still renders as Codex.
415
+ types actually present in the stream, so a bounded Codex rollout whose
416
+ leading ``session_meta`` header was omitted still renders as Codex.
387
417
 
388
418
  Callers should write each chunk to stdout, e.g.::
389
419
 
@@ -670,7 +700,15 @@ def main(argv: list[str] | None = None) -> None:
670
700
  if not jsonl_path.exists():
671
701
  print(f"File not found: {jsonl_path}", file=sys.stderr)
672
702
  sys.exit(1)
673
- stream = jsonl_path
703
+ try:
704
+ stream = (
705
+ _tail_utf8_lines(jsonl_path, preview_limit)
706
+ if preview_limit is not None
707
+ else jsonl_path
708
+ )
709
+ except OSError:
710
+ print(f"Could not read: {jsonl_path}", file=sys.stderr)
711
+ sys.exit(1)
674
712
 
675
713
  try:
676
714
  if preview_limit is not None:
@@ -100,6 +100,7 @@ from railmux.ui.workspace import (
100
100
  COMPACT_RESIZE_OPTION,
101
101
  DUAL_SIDEBAR_MIN_WIDTH,
102
102
  DUAL_SIDEBAR_PERCENT,
103
+ DisplayTransportKind,
103
104
  MINIMUM_AGENT_PANE_SIZE,
104
105
  SINGLE_SIDEBAR_PERCENT,
105
106
  SlotRestoreState,
@@ -1177,6 +1178,7 @@ class App:
1177
1178
  *,
1178
1179
  mode_key: str | None = None,
1179
1180
  project_key: str | None = None,
1181
+ sync_transcript_source: bool = True,
1180
1182
  ) -> None:
1181
1183
  """Update one slot, painting sidebar highlights only for the Target."""
1182
1184
  slot.active_session_id = session_id
@@ -1202,7 +1204,8 @@ class App:
1202
1204
  )
1203
1205
  elif session_id is None:
1204
1206
  slot.project_key = None
1205
- self._sync_slot_transcript_source(slot, session_id, tmux_name)
1207
+ if sync_transcript_source:
1208
+ self._sync_slot_transcript_source(slot, session_id, tmux_name)
1206
1209
  self._paint_slot_active_target(slot, session_id, tmux_name)
1207
1210
 
1208
1211
  def _sync_slot_transcript_source(
@@ -1296,7 +1299,8 @@ class App:
1296
1299
  def _set_active_target(self, session_id: str | None,
1297
1300
  tmux_name: str | None, *,
1298
1301
  mode_key: str | None = None,
1299
- project_key: str | None = None) -> None:
1302
+ project_key: str | None = None,
1303
+ sync_transcript_source: bool = True) -> None:
1300
1304
  """Compatibility entry point for the currently exposed primary slot."""
1301
1305
  self._set_slot_active_target(
1302
1306
  self._primary_slot,
@@ -1304,6 +1308,7 @@ class App:
1304
1308
  tmux_name,
1305
1309
  mode_key=mode_key,
1306
1310
  project_key=project_key,
1311
+ sync_transcript_source=sync_transcript_source,
1307
1312
  )
1308
1313
 
1309
1314
  def _set_active_tmux_target(
@@ -2035,21 +2040,35 @@ class App:
2035
2040
 
2036
2041
  # --- history preview (display pane shows a transcript, not an agent session) ---
2037
2042
 
2043
+ def _paint_session_row_click_now(self, session_id: str | None) -> None:
2044
+ """Acknowledge a pointer selection before synchronous tmux work."""
2045
+ pane = getattr(self, "_sessions_pane", None)
2046
+ if pane is None:
2047
+ return
2048
+ pane.set_selected_session(session_id)
2049
+ loop = getattr(self, "_loop", None)
2050
+ if loop is not None:
2051
+ loop.draw_screen()
2052
+
2038
2053
  def _on_session_row_preview(self, session: SessionMeta) -> None:
2039
2054
  """Apply click semantics after rechecking whether the row is live."""
2040
- running = self._by_session_id(session.session_id)
2041
- if (running is not None
2042
- and self._agent_session_alive(running.tmux_name)):
2043
- self._on_running_select(
2044
- RunningEntry(
2045
- tmux_name=running.tmux_name,
2046
- label=running.label,
2047
- status=running.status,
2048
- ),
2049
- steal_focus=False,
2050
- )
2051
- return
2052
- self._on_session_preview(session)
2055
+ self._paint_session_row_click_now(session.session_id)
2056
+ try:
2057
+ running = self._by_session_id(session.session_id)
2058
+ if (running is not None
2059
+ and self._agent_session_alive(running.tmux_name)):
2060
+ self._on_running_select(
2061
+ RunningEntry(
2062
+ tmux_name=running.tmux_name,
2063
+ label=running.label,
2064
+ status=running.status,
2065
+ ),
2066
+ steal_focus=False,
2067
+ )
2068
+ return
2069
+ self._on_session_preview(session)
2070
+ finally:
2071
+ self._paint_session_row_click_now(None)
2053
2072
 
2054
2073
  def _on_session_preview(self, session: SessionMeta) -> None:
2055
2074
  """Show the provider's canonical current-branch history.
@@ -2090,10 +2109,19 @@ class App:
2090
2109
  }
2091
2110
  if slot is self._primary_slot:
2092
2111
  self._set_active_target(
2093
- session.session_id, None, **target_kwargs)
2112
+ session.session_id,
2113
+ None,
2114
+ sync_transcript_source=False,
2115
+ **target_kwargs,
2116
+ )
2094
2117
  else:
2095
2118
  self._set_slot_active_target(
2096
- slot, session.session_id, None, **target_kwargs)
2119
+ slot,
2120
+ session.session_id,
2121
+ None,
2122
+ sync_transcript_source=False,
2123
+ **target_kwargs,
2124
+ )
2097
2125
  if not self._show_attention_status(session.attention):
2098
2126
  self._set_status(
2099
2127
  f"≡ Previewing {session.display_title} (current history)")
@@ -2183,11 +2211,11 @@ class App:
2183
2211
  import sys as _sys
2184
2212
  mouse = self._less_mouse_flag
2185
2213
  slot = slot or self._agent_workspace().target
2186
- # Tail the last 2000 lines so large sessions appear instantly. Tailing
2187
- # drops the leading ``session_meta`` record, so a long Codex rollout
2188
- # would otherwise be auto-detected as Claude and render blank. Pass the
2189
- # format explicitly from SessionMeta.session_type so the parser doesn't
2190
- # have to guess from the first tailed record (#5).
2214
+ # The renderer reads the last 2000 records itself so large sessions
2215
+ # appear instantly without launching a separate ``tail`` process.
2216
+ # This drops the leading ``session_meta`` record from long Codex
2217
+ # rollouts, so pass the format explicitly from SessionMeta rather than
2218
+ # asking the bounded input to auto-detect it (#5).
2191
2219
  fmt = "codex" if session_type == "codex" else "claude"
2192
2220
  path = shlex.quote(str(jsonl_path))
2193
2221
  python = shlex.quote(_sys.executable)
@@ -2196,9 +2224,8 @@ class App:
2196
2224
  # and suppress any user-configured input pre/postprocessors.
2197
2225
  less_env = ("LESSSECURE=1 LESSHISTFILE=- "
2198
2226
  "LESSOPEN= LESSCLOSE=")
2199
- cmd = (f"tail -n 2000 {path} | "
2200
- f"{python} -m railmux.transcript --format {fmt} "
2201
- f"--preview-limit 2000 - | "
2227
+ cmd = (f"{python} -m railmux.transcript --format {fmt} "
2228
+ f"--preview-limit 2000 {path} | "
2202
2229
  f"{less_env} less -R +G {mouse}").rstrip()
2203
2230
  restore = slot.restore_state
2204
2231
  controller = getattr(self, "_railmux_pane_id", None)
@@ -2211,33 +2238,58 @@ class App:
2211
2238
  f"{shlex.quote(controller)} -- "
2212
2239
  f"{shlex.quote(restore_sequence)}"
2213
2240
  )
2214
- # In swap mode ``slot.pane_id`` is the real provider pane. Return it
2215
- # home before the destructive ``respawn-pane -k`` below; only the
2216
- # display placeholder may be replaced by the transcript viewer.
2217
- if not self._display_transport().prepare_preview(slot):
2218
- self._set_status(
2219
- "failed to return the agent home before transcript preview",
2220
- "error",
2221
- )
2222
- return False
2223
- self._sync_slot_transcript_source(slot, None, None)
2224
- if slot.pane_id and tmux_ctl.pane_alive(slot.pane_id):
2241
+ # A preview-to-preview switch keeps the same Railmux-owned outer pane,
2242
+ # whose provider and SSH transcript markers were already cleared by
2243
+ # the first transition. ``respawn-pane`` is itself the bounded
2244
+ # liveness check, so do not launch redundant tmux clients on this hot
2245
+ # path. Entering preview from a live agent still uses the complete
2246
+ # return-home identity transaction below.
2247
+ replacing_preview = bool(
2248
+ slot.in_history_mode
2249
+ and slot.pane_id
2250
+ and slot.agent_tmux_name is None
2251
+ and slot.swap_state is None
2252
+ and slot.transport_kind is DisplayTransportKind.NESTED
2253
+ )
2254
+ if replacing_preview:
2255
+ assert slot.pane_id is not None
2225
2256
  if not tmux_ctl.respawn_pane(slot.pane_id, cmd):
2226
2257
  self._set_status("failed to respawn right pane for transcript")
2227
2258
  return False
2228
2259
  else:
2229
- new_id = tmux_ctl.split_window_h(cmd, size_percent=70, detached=True)
2230
- if not new_id:
2231
- self._set_status("failed to create right pane for transcript")
2260
+ # In swap mode ``slot.pane_id`` is the real provider pane. Return
2261
+ # it home before the destructive ``respawn-pane -k`` below; only
2262
+ # the display placeholder may be replaced by the viewer.
2263
+ if not self._display_transport().prepare_preview(slot):
2264
+ self._set_status(
2265
+ "failed to return the agent home before transcript preview",
2266
+ "error",
2267
+ )
2232
2268
  return False
2233
- slot.pane_id = new_id
2234
- self._set_railmux_focus(self._railmux_has_focus, force_border=True)
2269
+ self._sync_slot_transcript_source(slot, None, None)
2270
+ # prepare_preview already proved an existing pane immediately
2271
+ # before this mutation. A race now fails in respawn-pane rather
2272
+ # than paying for a second process-wide list-panes query.
2273
+ if slot.pane_id:
2274
+ if not tmux_ctl.respawn_pane(slot.pane_id, cmd):
2275
+ self._set_status("failed to respawn right pane for transcript")
2276
+ return False
2277
+ else:
2278
+ new_id = tmux_ctl.split_window_h(
2279
+ cmd, size_percent=70, detached=True)
2280
+ if not new_id:
2281
+ self._set_status("failed to create right pane for transcript")
2282
+ return False
2283
+ slot.pane_id = new_id
2284
+ self._set_railmux_focus(
2285
+ self._railmux_has_focus, force_border=True)
2235
2286
  # The display pane is now showing a transcript, not an agent session.
2236
2287
  slot.agent_tmux_name = None
2237
2288
  slot.mode_key = self._current_mode_key()
2238
- self._set_divider_active(
2239
- not getattr(self, "_railmux_has_focus", True), force=True)
2240
- self._install_tmux_bindings()
2289
+ if not replacing_preview:
2290
+ self._set_divider_active(
2291
+ not getattr(self, "_railmux_has_focus", True), force=True)
2292
+ self._install_tmux_bindings()
2241
2293
  return True
2242
2294
 
2243
2295
  def _sync_sidebar_to_agent_project(self, tmux_name: str | None) -> None:
@@ -20,6 +20,7 @@ from railmux.windows_msys2 import (
20
20
  plan_managed_runtime_prune,
21
21
  reusable_managed_base_candidate,
22
22
  )
23
+ from railmux.windows_paths import managed_windows_data_root
23
24
 
24
25
 
25
26
  _MINIMUM_WINDOWS_PYTHON = (3, 10)
@@ -49,6 +50,14 @@ def _runtime_status(
49
50
  ) -> int:
50
51
  snapshot = managed_runtime_status(
51
52
  version=__version__, environ=environ, verify=verify)
53
+ data_root = (
54
+ managed_windows_data_root(environ)
55
+ if runtime is None or runtime.managed
56
+ else None
57
+ )
58
+ if data_root is not None:
59
+ snapshot["data_root"] = str(data_root)
60
+ snapshot["log_directory"] = str(data_root / "logs")
52
61
  if json_output:
53
62
  json.dump(snapshot, sys.stdout, indent=2, sort_keys=True)
54
63
  print()
@@ -75,6 +84,9 @@ def _runtime_status(
75
84
  if root is not None:
76
85
  print(f"Managed location: {root}")
77
86
  print("Next: run 'railmux runtime install' from an interactive PowerShell.")
87
+ if data_root is not None:
88
+ print(f"Data root: {data_root}")
89
+ print(f"Install logs: {data_root / 'logs'}")
78
90
  print("Provider data: shared from the Windows user profile")
79
91
  content = snapshot.get("content_identity")
80
92
  if isinstance(content, str):
@@ -162,7 +162,12 @@ class InstallReporter:
162
162
 
163
163
  def __enter__(self) -> InstallReporter:
164
164
  self.path.parent.mkdir(parents=True, exist_ok=True)
165
- self._log = self.path.open("x", encoding="utf-8", newline="\n")
165
+ # Windows PowerShell 5 treats BOM-less UTF-8 as the active ANSI code
166
+ # page. The signature keeps Chinese systems from rendering the
167
+ # middle dot/ellipsis as mojibake while remaining ordinary UTF-8 for
168
+ # modern readers (use ``utf-8-sig`` when the exact first character
169
+ # matters).
170
+ self._log = self.path.open("x", encoding="utf-8-sig", newline="\n")
166
171
  self._write_log("Railmux Windows runtime installation\n")
167
172
  # Windows may defer the visible last-write timestamp of an open file.
168
173
  # Never let retention mistake the active evidence log for an old one.