railmux 0.4.0.dev3__tar.gz → 0.4.0.dev4__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 (171) hide show
  1. {railmux-0.4.0.dev3/src/railmux.egg-info → railmux-0.4.0.dev4}/PKG-INFO +25 -16
  2. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/README.md +18 -10
  3. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/pyproject.toml +6 -5
  4. railmux-0.4.0.dev4/src/railmux/__init__.py +1 -0
  5. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/codex_index.py +5 -3
  6. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/config.py +2 -1
  7. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/discovery.py +2 -2
  8. railmux-0.4.0.dev4/src/railmux/entrypoint.py +21 -0
  9. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/mouse_manager.py +8 -0
  10. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/path_codec.py +16 -0
  11. railmux-0.4.0.dev4/src/railmux/provider_paths.py +41 -0
  12. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/self_update.py +5 -0
  13. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/tmux_ctl.py +82 -7
  14. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/transcript.py +83 -28
  15. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/ui/app.py +141 -15
  16. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/ui/modals.py +1 -1
  17. railmux-0.4.0.dev4/src/railmux/windows_bootstrap.py +174 -0
  18. railmux-0.4.0.dev4/src/railmux/windows_msys2.py +431 -0
  19. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4/src/railmux.egg-info}/PKG-INFO +25 -16
  20. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux.egg-info/SOURCES.txt +4 -0
  21. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux.egg-info/requires.txt +7 -3
  22. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_codex_index.py +19 -0
  23. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_config.py +8 -0
  24. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_focus_highlight.py +135 -8
  25. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_mouse_manager.py +17 -0
  26. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_path_codec.py +17 -1
  27. railmux-0.4.0.dev4/tests/test_provider_paths.py +29 -0
  28. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_release_workflow.py +5 -2
  29. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_self_update.py +11 -0
  30. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_soft_quit.py +42 -0
  31. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_tmux_ctl.py +28 -0
  32. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_tmux_integration.py +1 -0
  33. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_transcript.py +84 -0
  34. railmux-0.4.0.dev4/tests/test_windows_bootstrap.py +143 -0
  35. railmux-0.4.0.dev4/tests/test_windows_msys2.py +216 -0
  36. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_windows_wrapper_contract.py +16 -11
  37. railmux-0.4.0.dev3/src/railmux/__init__.py +0 -1
  38. railmux-0.4.0.dev3/src/railmux/entrypoint.py +0 -26
  39. railmux-0.4.0.dev3/src/railmux/windows_bootstrap.py +0 -253
  40. railmux-0.4.0.dev3/tests/test_windows_bootstrap.py +0 -236
  41. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/LICENSE +0 -0
  42. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/MANIFEST.in +0 -0
  43. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/setup.cfg +0 -0
  44. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/__main__.py +0 -0
  45. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/atomic_file.py +0 -0
  46. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/background_index.py +0 -0
  47. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/cli.py +0 -0
  48. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/config_cli.py +0 -0
  49. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/diagnostics.py +0 -0
  50. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/display_transport.py +0 -0
  51. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/fast_display_client.py +0 -0
  52. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/fast_display_history.py +0 -0
  53. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/fast_display_input.py +0 -0
  54. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/fast_display_protocol.py +0 -0
  55. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/fast_display_server.py +0 -0
  56. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/favorites.py +0 -0
  57. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/fuzzy.py +0 -0
  58. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/help_workspace.py +0 -0
  59. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/launcher.py +0 -0
  60. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/legacy_sessions.py +0 -0
  61. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/local_clipboard.py +0 -0
  62. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/local_open.py +0 -0
  63. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/models.py +0 -0
  64. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/modes.py +0 -0
  65. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/orphan_marker.py +0 -0
  66. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/pane_surface.py +0 -0
  67. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/remote_config.py +0 -0
  68. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/renames.py +0 -0
  69. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/restart_state.py +0 -0
  70. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/runtime_config.py +0 -0
  71. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/scroll_agent.py +0 -0
  72. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/scroll_manager.py +0 -0
  73. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/selection_isolation.py +0 -0
  74. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/session_cache.py +0 -0
  75. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/session_index.py +0 -0
  76. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/setting_contracts.py +0 -0
  77. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/settings.py +0 -0
  78. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/ssh_args.py +0 -0
  79. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/ssh_compat.py +0 -0
  80. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/ssh_display_diagnostics.py +0 -0
  81. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/ssh_doctor.py +0 -0
  82. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/system_deps.py +0 -0
  83. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/terminal_status.py +0 -0
  84. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/tmux_binding_manager.py +0 -0
  85. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/tmux_health.py +0 -0
  86. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/tmux_server.py +0 -0
  87. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/tool_panes.py +0 -0
  88. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/ui/__init__.py +0 -0
  89. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/ui/_widgets.py +0 -0
  90. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/ui/keymap.py +0 -0
  91. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/ui/projects_pane.py +0 -0
  92. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/ui/running_pane.py +0 -0
  93. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/ui/sessions_pane.py +0 -0
  94. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/ui/sidebar.py +0 -0
  95. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/ui/statusbar.py +0 -0
  96. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux/ui/workspace.py +0 -0
  97. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux.egg-info/dependency_links.txt +0 -0
  98. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux.egg-info/entry_points.txt +0 -0
  99. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/src/railmux.egg-info/top_level.txt +0 -0
  100. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_agent_workspace.py +0 -0
  101. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_atomic_file.py +0 -0
  102. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_background_index.py +0 -0
  103. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_cli.py +0 -0
  104. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_codex_delete.py +0 -0
  105. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_codex_lineage.py +0 -0
  106. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_codex_transcript.py +0 -0
  107. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_config_cli.py +0 -0
  108. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_denested_transport_benchmark.py +0 -0
  109. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_diagnostics.py +0 -0
  110. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_discovery.py +0 -0
  111. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_display_transport.py +0 -0
  112. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_fast_full_window.py +0 -0
  113. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_fullscreen_binding.py +0 -0
  114. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_fuzzy.py +0 -0
  115. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_help_agent.py +0 -0
  116. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_help_workspace.py +0 -0
  117. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_history_cleanup.py +0 -0
  118. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_keymap.py +0 -0
  119. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_launcher.py +0 -0
  120. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_layout_profile.py +0 -0
  121. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_legacy_sessions.py +0 -0
  122. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_local_clipboard.py +0 -0
  123. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_local_open.py +0 -0
  124. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_modals.py +0 -0
  125. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_mode_selection.py +0 -0
  126. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_modes.py +0 -0
  127. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_new_project.py +0 -0
  128. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_options.py +0 -0
  129. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_orphan_recovery.py +0 -0
  130. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_pane_surface.py +0 -0
  131. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_projects_pane.py +0 -0
  132. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_release_notes.py +0 -0
  133. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_remote_config.py +0 -0
  134. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_renames.py +0 -0
  135. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_restart_state.py +0 -0
  136. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_running_pane.py +0 -0
  137. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_running_sort.py +0 -0
  138. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_runtime_config.py +0 -0
  139. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_scroll_agent.py +0 -0
  140. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_scroll_manager.py +0 -0
  141. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_selection_isolation.py +0 -0
  142. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_session_cache.py +0 -0
  143. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_session_index.py +0 -0
  144. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_sessions_pane.py +0 -0
  145. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_setting_contracts.py +0 -0
  146. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_settings.py +0 -0
  147. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_sidebar.py +0 -0
  148. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_ssh_args.py +0 -0
  149. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_ssh_compat.py +0 -0
  150. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_ssh_display_benchmark.py +0 -0
  151. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_ssh_display_diagnostics.py +0 -0
  152. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_ssh_doctor.py +0 -0
  153. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_status.py +0 -0
  154. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_statusbar.py +0 -0
  155. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_system_deps.py +0 -0
  156. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_terminal_size.py +0 -0
  157. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_terminal_status.py +0 -0
  158. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_termux_local_touch.py +0 -0
  159. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_tmux_binding_manager.py +0 -0
  160. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_tmux_health.py +0 -0
  161. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_tmux_server.py +0 -0
  162. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_tmux_status.py +0 -0
  163. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_tool_panes.py +0 -0
  164. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_utf8_state.py +0 -0
  165. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_web_demo_recorder.py +0 -0
  166. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_wheel_smoke.py +0 -0
  167. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_widgets.py +0 -0
  168. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tests/test_yolo_prompt.py +0 -0
  169. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tools/denested_transport_benchmark.py +0 -0
  170. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/tools/release_notes.py +0 -0
  171. {railmux-0.4.0.dev3 → railmux-0.4.0.dev4}/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.dev3
3
+ Version: 0.4.0.dev4
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
@@ -14,6 +14,7 @@ Classifier: Environment :: Console :: Curses
14
14
  Classifier: Intended Audience :: Developers
15
15
  Classifier: Operating System :: POSIX :: Linux
16
16
  Classifier: Operating System :: MacOS
17
+ Classifier: Operating System :: Microsoft :: Windows
17
18
  Classifier: Programming Language :: Python :: 3
18
19
  Classifier: Programming Language :: Python :: 3.9
19
20
  Classifier: Programming Language :: Python :: 3.10
@@ -26,12 +27,12 @@ Classifier: Topic :: Utilities
26
27
  Requires-Python: >=3.9
27
28
  Description-Content-Type: text/markdown
28
29
  License-File: LICENSE
29
- Requires-Dist: packaging>=23.0
30
- Requires-Dist: tomlkit<1,>=0.13.2
31
- Requires-Dist: urwid>=2.6.16
32
- Requires-Dist: tomli>=2.0.1; python_version < "3.11"
30
+ Requires-Dist: packaging>=23.0; sys_platform != "win32"
31
+ Requires-Dist: tomlkit<1,>=0.13.2; sys_platform != "win32"
32
+ Requires-Dist: urwid>=2.6.16; sys_platform != "win32"
33
+ Requires-Dist: tomli>=2.0.1; python_version < "3.11" and sys_platform != "win32"
33
34
  Provides-Extra: ssh
34
- Requires-Dist: pyte<0.9,>=0.8.2; extra == "ssh"
35
+ Requires-Dist: pyte<0.9,>=0.8.2; sys_platform != "win32" and extra == "ssh"
35
36
  Provides-Extra: dev
36
37
  Requires-Dist: pytest>=8.0; extra == "dev"
37
38
  Requires-Dist: build>=1.2; extra == "dev"
@@ -169,12 +170,14 @@ for focus and layout limits.
169
170
  |--------|--------|
170
171
  | Left-click (non-running) | Preview session history in the Target pane |
171
172
  | Left-click (running) | Switch the Target pane to that session |
173
+ | Wheel up over a live agent | Open its canonical current-branch history |
172
174
  | Double-click | Open/attach in the Target pane and move focus there |
173
175
  | Right-click | Context menu (Open, Preview, Info, Rename, Star, Copy title, Kill, Term, Delete) |
174
176
 
175
177
  Unavailable context-menu actions are hidden for the selected session state.
176
- Single-click and `␣` act in the Target pane without moving keyboard focus;
177
- double-click and `Enter` also move focus there. The terminal must report mouse
178
+ Single-click and `␣` act in the Target pane without moving keyboard focus, but
179
+ `␣` always means history while a click on a running row means live attach.
180
+ Double-click and `Enter` also move focus there. The terminal must report mouse
178
181
  buttons to applications for these actions to reach Railmux. Right-click
179
182
  reporting is sometimes a separate setting from ordinary mouse reporting; see
180
183
  [FAQ 2](#2-mouse-buttons-or-f8f9-dont-work--whats-wrong).
@@ -207,7 +210,7 @@ see [FAQ 1](#1-how-do-i-copy-text-from-the-agent-pane).
207
210
  | `T` | Return to that agent's managed Vim viewer, when open |
208
211
  | `m` | Switch agent mode (Claude Code ⇄ Codex) |
209
212
  | `o` | Open persistent Railmux options |
210
- | `␣` | Preview stopped or switch running target (like single-click) |
213
+ | `␣` | Preview canonical history for the selected stopped or running session |
211
214
  | `F8` | Cycle agent layout: single → side-by-side → stacked |
212
215
  | `F9` | Fullscreen the focused agent, managed terminal, or Vim (toggle) |
213
216
  | `?` | Full help popup with all keybindings |
@@ -299,7 +302,8 @@ to single leaves Pane 2's agent running in the background.
299
302
 
300
303
  In a split, focus an agent pane to make it the Target pane. After focus returns
301
304
  to the sidebar, single-click or `␣` acts in that pane without moving keyboard
302
- focus; double-click or `Enter` opens there and transfers focus. The status bar
305
+ focus; a running-row click attaches live while `␣` previews history.
306
+ Double-click or `Enter` opens there and transfers focus. The status bar
303
307
  at the bottom-left shows the current layout and Target pane:
304
308
 
305
309
  | Symbol | Meaning |
@@ -363,15 +367,20 @@ sessions move ahead of the other results.
363
367
  ## History preview
364
368
 
365
369
  For a stopped session, left-click or press `␣` to view conversation history in
366
- the Target pane without starting or resuming the agent. Preview is read-only: it
367
- cannot send a message or change the session. User and assistant messages, tool
368
- calls, and abbreviated tool output are colour-coded, while internal context and
369
- encrypted reasoning are hidden.
370
+ the Target pane without starting or resuming the agent. For a running session,
371
+ press `␣`, choose **Preview**, or wheel up over its live pane. Preview is
372
+ read-only: it cannot send a message or change the session. User and assistant
373
+ messages, tool calls, and abbreviated tool output are colour-coded, while
374
+ internal context and encrypted reasoning are hidden. Codex rewind lineages and
375
+ Claude Code `parentUuid` branches are projected onto the provider's current
376
+ conversation: the retained prefix and replacement suffix remain visible, while
377
+ the abandoned suffix is hidden.
370
378
 
371
379
  Preview opens at the latest activity in `less`; large sessions are limited to
372
380
  their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
373
- matches, and `q` to exit and restore the pane. Double-click to skip preview and
374
- open the session directly.
381
+ matches, and `q` to exit. If preview began from a live pane, `q` restores that
382
+ exact session; otherwise it restores the prior Target pane. Double-click to
383
+ skip preview and open the session directly.
375
384
 
376
385
  ## Status indicators
377
386
 
@@ -127,12 +127,14 @@ for focus and layout limits.
127
127
  |--------|--------|
128
128
  | Left-click (non-running) | Preview session history in the Target pane |
129
129
  | Left-click (running) | Switch the Target pane to that session |
130
+ | Wheel up over a live agent | Open its canonical current-branch history |
130
131
  | Double-click | Open/attach in the Target pane and move focus there |
131
132
  | Right-click | Context menu (Open, Preview, Info, Rename, Star, Copy title, Kill, Term, Delete) |
132
133
 
133
134
  Unavailable context-menu actions are hidden for the selected session state.
134
- Single-click and `␣` act in the Target pane without moving keyboard focus;
135
- double-click and `Enter` also move focus there. The terminal must report mouse
135
+ Single-click and `␣` act in the Target pane without moving keyboard focus, but
136
+ `␣` always means history while a click on a running row means live attach.
137
+ Double-click and `Enter` also move focus there. The terminal must report mouse
136
138
  buttons to applications for these actions to reach Railmux. Right-click
137
139
  reporting is sometimes a separate setting from ordinary mouse reporting; see
138
140
  [FAQ 2](#2-mouse-buttons-or-f8f9-dont-work--whats-wrong).
@@ -165,7 +167,7 @@ see [FAQ 1](#1-how-do-i-copy-text-from-the-agent-pane).
165
167
  | `T` | Return to that agent's managed Vim viewer, when open |
166
168
  | `m` | Switch agent mode (Claude Code ⇄ Codex) |
167
169
  | `o` | Open persistent Railmux options |
168
- | `␣` | Preview stopped or switch running target (like single-click) |
170
+ | `␣` | Preview canonical history for the selected stopped or running session |
169
171
  | `F8` | Cycle agent layout: single → side-by-side → stacked |
170
172
  | `F9` | Fullscreen the focused agent, managed terminal, or Vim (toggle) |
171
173
  | `?` | Full help popup with all keybindings |
@@ -257,7 +259,8 @@ to single leaves Pane 2's agent running in the background.
257
259
 
258
260
  In a split, focus an agent pane to make it the Target pane. After focus returns
259
261
  to the sidebar, single-click or `␣` acts in that pane without moving keyboard
260
- focus; double-click or `Enter` opens there and transfers focus. The status bar
262
+ focus; a running-row click attaches live while `␣` previews history.
263
+ Double-click or `Enter` opens there and transfers focus. The status bar
261
264
  at the bottom-left shows the current layout and Target pane:
262
265
 
263
266
  | Symbol | Meaning |
@@ -321,15 +324,20 @@ sessions move ahead of the other results.
321
324
  ## History preview
322
325
 
323
326
  For a stopped session, left-click or press `␣` to view conversation history in
324
- the Target pane without starting or resuming the agent. Preview is read-only: it
325
- cannot send a message or change the session. User and assistant messages, tool
326
- calls, and abbreviated tool output are colour-coded, while internal context and
327
- encrypted reasoning are hidden.
327
+ the Target pane without starting or resuming the agent. For a running session,
328
+ press `␣`, choose **Preview**, or wheel up over its live pane. Preview is
329
+ read-only: it cannot send a message or change the session. User and assistant
330
+ messages, tool calls, and abbreviated tool output are colour-coded, while
331
+ internal context and encrypted reasoning are hidden. Codex rewind lineages and
332
+ Claude Code `parentUuid` branches are projected onto the provider's current
333
+ conversation: the retained prefix and replacement suffix remain visible, while
334
+ the abandoned suffix is hidden.
328
335
 
329
336
  Preview opens at the latest activity in `less`; large sessions are limited to
330
337
  their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
331
- matches, and `q` to exit and restore the pane. Double-click to skip preview and
332
- open the session directly.
338
+ matches, and `q` to exit. If preview began from a live pane, `q` restores that
339
+ exact session; otherwise it restores the prior Target pane. Double-click to
340
+ skip preview and open the session directly.
333
341
 
334
342
  ## Status indicators
335
343
 
@@ -20,6 +20,7 @@ classifiers = [
20
20
  "Intended Audience :: Developers",
21
21
  "Operating System :: POSIX :: Linux",
22
22
  "Operating System :: MacOS",
23
+ "Operating System :: Microsoft :: Windows",
23
24
  "Programming Language :: Python :: 3",
24
25
  "Programming Language :: Python :: 3.9",
25
26
  "Programming Language :: Python :: 3.10",
@@ -31,15 +32,15 @@ classifiers = [
31
32
  "Topic :: Utilities",
32
33
  ]
33
34
  dependencies = [
34
- "packaging>=23.0",
35
- "tomlkit>=0.13.2,<1",
36
- "urwid>=2.6.16",
37
- "tomli>=2.0.1; python_version < '3.11'",
35
+ "packaging>=23.0; sys_platform != 'win32'",
36
+ "tomlkit>=0.13.2,<1; sys_platform != 'win32'",
37
+ "urwid>=2.6.16; sys_platform != 'win32'",
38
+ "tomli>=2.0.1; python_version < '3.11' and sys_platform != 'win32'",
38
39
  ]
39
40
 
40
41
  [project.optional-dependencies]
41
42
  ssh = [
42
- "pyte>=0.8.2,<0.9",
43
+ "pyte>=0.8.2,<0.9; sys_platform != 'win32'",
43
44
  ]
44
45
  dev = [
45
46
  "pytest>=8.0",
@@ -0,0 +1 @@
1
+ __version__ = "0.4.0.dev4"
@@ -25,6 +25,7 @@ from railmux.models import (
25
25
  Project,
26
26
  SessionMeta,
27
27
  )
28
+ from railmux.provider_paths import provider_path
28
29
  from railmux.renames import Renames
29
30
 
30
31
  # Codex has no reliable provider-neutral signal that distinguishes a long
@@ -97,9 +98,10 @@ class HiddenCodexStatus:
97
98
  def persistent_cache_path(codex_home: Path) -> Path:
98
99
  """Return a private cache path unique to one Codex sessions root."""
99
100
  raw_base = os.environ.get("XDG_CACHE_HOME")
101
+ requested_base = provider_path(raw_base).expanduser() if raw_base else None
100
102
  base = (
101
- Path(raw_base).expanduser()
102
- if raw_base and Path(raw_base).expanduser().is_absolute()
103
+ requested_base
104
+ if requested_base is not None and requested_base.is_absolute()
103
105
  else Path.home() / ".cache"
104
106
  )
105
107
  try:
@@ -1036,7 +1038,7 @@ def _parse_codex_session(
1036
1038
  cwd_str = payload.get("cwd")
1037
1039
  if not isinstance(cwd_str, str) or not cwd_str.strip():
1038
1040
  return None
1039
- cwd = Path(cwd_str)
1041
+ cwd = provider_path(cwd_str)
1040
1042
  forked_from = payload.get("forked_from_id")
1041
1043
  forked_from_id = (
1042
1044
  forked_from
@@ -18,6 +18,7 @@ from railmux.setting_contracts import (
18
18
  bounds_for,
19
19
  choices_for,
20
20
  )
21
+ from railmux.provider_paths import provider_path
21
22
 
22
23
 
23
24
  class ConfigError(ValueError):
@@ -47,7 +48,7 @@ class Config:
47
48
  and relative paths are made absolute before a launched Codex changes to
48
49
  its project cwd. The directory is not required to exist.
49
50
  """
50
- path = Path(self.codex_home).expanduser()
51
+ path = provider_path(self.codex_home).expanduser()
51
52
  try:
52
53
  return path.resolve()
53
54
  except OSError:
@@ -7,7 +7,7 @@ from pathlib import Path
7
7
 
8
8
  from railmux.atomic_file import atomic_write_text
9
9
  from railmux.models import Project
10
- from railmux.path_codec import decode
10
+ from railmux.path_codec import decode, is_encoded_project_name
11
11
  from railmux.session_index import _looks_like_uuid, _scan_session
12
12
 
13
13
 
@@ -94,7 +94,7 @@ def list_projects(claude_home: Path) -> list[Project]:
94
94
  for entry in scan:
95
95
  if not entry.is_dir(follow_symlinks=False):
96
96
  continue
97
- if not entry.name.startswith("-"):
97
+ if not is_encoded_project_name(entry.name):
98
98
  continue
99
99
  seen.add(entry.name)
100
100
 
@@ -0,0 +1,21 @@
1
+ """Platform-safe public console entry point."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ from collections.abc import Sequence
6
+
7
+
8
+ def main(
9
+ argv: Sequence[str] | None = None,
10
+ *,
11
+ platform_name: str = os.name,
12
+ ) -> int:
13
+ """Dispatch before importing modules that require a POSIX terminal."""
14
+ if platform_name == "nt":
15
+ from railmux.windows_bootstrap import main as windows_main
16
+
17
+ return windows_main(argv)
18
+
19
+ from railmux.cli import main as posix_main
20
+
21
+ return posix_main(list(argv) if argv is not None else None)
@@ -157,6 +157,14 @@ class RootWheelForwardingManager:
157
157
  state["backup"], token=token)
158
158
  self._remove_state()
159
159
  state = None
160
+ elif not tmux_ctl.root_wheel_bindings_are_current(token):
161
+ # The ownership transaction can survive a soft upgrade
162
+ # in the same tmux pane. Refresh only an exactly-owned
163
+ # wrapper so the new pane-local history route becomes
164
+ # active without treating it as a user binding.
165
+ if not tmux_ctl.set_root_wheel_forwarding(
166
+ state["backup"], token):
167
+ return False
160
168
  if state is not None:
161
169
  state["owners"] = sorted(
162
170
  {*state["owners"], self._owner_pane_id})
@@ -9,6 +9,7 @@ directory listings to recover segments with non-ASCII characters.
9
9
  from __future__ import annotations
10
10
 
11
11
  import functools
12
+ import re
12
13
  from pathlib import Path
13
14
 
14
15
 
@@ -21,6 +22,20 @@ from pathlib import Path
21
22
  _exists_memo: dict[str, bool] = {}
22
23
  _is_dir_memo: dict[str, bool] = {}
23
24
  _iterdir_memo: dict[str, list[Path]] = {}
25
+ _WINDOWS_PROJECT_NAME = re.compile(r"^([A-Za-z])--")
26
+
27
+
28
+ def is_encoded_project_name(name: str) -> bool:
29
+ """Return whether *name* is a POSIX or native-Windows Claude project key."""
30
+ return name.startswith("-") or _WINDOWS_PROJECT_NAME.match(name) is not None
31
+
32
+
33
+ def normalize_encoded_project_name(name: str) -> str:
34
+ """Map Claude's ``C--Users-...`` form to MSYS2's ``-c-Users-...`` form."""
35
+ match = _WINDOWS_PROJECT_NAME.match(name)
36
+ if match is None:
37
+ return name
38
+ return f"-{match.group(1).lower()}-{name[3:]}"
24
39
 
25
40
 
26
41
  def _m_exists(p: Path) -> bool:
@@ -200,6 +215,7 @@ def decode(encoded: str) -> Path:
200
215
  filesystem scan — see ``_scan_recover`` — to recover segments whose
201
216
  non-ASCII characters were replaced by dashes during encoding.
202
217
  """
218
+ encoded = normalize_encoded_project_name(encoded)
203
219
  if not encoded.startswith("-"):
204
220
  raise ValueError(f"encoded name must start with '-': {encoded!r}")
205
221
 
@@ -0,0 +1,41 @@
1
+ """Translate provider-owned native Windows paths inside the MSYS2 preview."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import re
6
+ from collections.abc import Mapping
7
+ from pathlib import Path
8
+
9
+
10
+ _DRIVE_PATH = re.compile(r"^(?:\\\\\?\\)?([A-Za-z]):[\\/](.*)\Z")
11
+
12
+
13
+ def running_in_windows_wrapper(environ: Mapping[str, str] = os.environ) -> bool:
14
+ """Return whether Railmux is running in its native-Windows MSYS2 wrapper."""
15
+ return environ.get("RAILMUX_WINDOWS_RUNTIME") == "msys2"
16
+
17
+
18
+ def provider_path(
19
+ raw: str,
20
+ *,
21
+ environ: Mapping[str, str] = os.environ,
22
+ ) -> Path:
23
+ """Return a POSIX-visible path for provider metadata.
24
+
25
+ Native providers write Windows paths into session metadata. Railmux runs
26
+ under MSYS2, where the same locations are mounted as ``/c/...`` (or
27
+ ``//server/share/...`` for UNC paths). Outside the wrapper, preserve the
28
+ existing POSIX behavior exactly.
29
+ """
30
+ if not running_in_windows_wrapper(environ):
31
+ return Path(raw)
32
+
33
+ match = _DRIVE_PATH.match(raw)
34
+ if match is not None:
35
+ drive, tail = match.groups()
36
+ return Path(f"/{drive.lower()}/{tail.replace(chr(92), '/')}")
37
+
38
+ if raw.startswith("\\\\"):
39
+ return Path("//" + raw[2:].replace("\\", "/"))
40
+
41
+ return Path(raw.replace("\\", "/"))
@@ -141,6 +141,11 @@ def maybe_upgrade_before_launch(
141
141
  settings: Settings,
142
142
  ) -> None:
143
143
  """Check once in the outer launcher and restart after an accepted update."""
144
+ # Native Windows owns this private venv through the verified bootstrap.
145
+ # Updating only the inner copy would break its exact-version marker and
146
+ # could replace files while a tmux generation is still alive.
147
+ if os.environ.get("RAILMUX_WINDOWS_RUNTIME") == "msys2":
148
+ return
144
149
  policy = settings.update_policy
145
150
  if policy == "never":
146
151
  return
@@ -1817,6 +1817,18 @@ _PREFIX_TARGET_KEY = "Tab"
1817
1817
  _PREFIX_TARGET_MARKER = "railmux-target-toggle-v1"
1818
1818
  RAILMUX_CONTROLLER_OPTION = "@railmux_controller_pane"
1819
1819
  RAILMUX_TARGET_OPTION = "@railmux_target_pane"
1820
+ RAILMUX_HISTORY_PREVIEW_OPTION = "@railmux_history_preview_v1"
1821
+ # tmux names function keys only through F12. Private controller routes use
1822
+ # canonical xterm sequences in literal mode, so they cannot be confused with
1823
+ # user input forwarded to an agent pane.
1824
+ RAILMUX_HISTORY_PREVIEW_KEYS = {
1825
+ "primary": ("F21", "\x1b[35~"),
1826
+ "secondary": ("F22", "\x1b[36~"),
1827
+ }
1828
+ RAILMUX_HISTORY_RESTORE_KEYS = {
1829
+ "primary": ("F23", "\x1b[42~"),
1830
+ "secondary": ("F24", "\x1b[43~"),
1831
+ }
1820
1832
  RAILMUX_SELECTION_KEY_OPTION = "@railmux_selection_key"
1821
1833
  RAILMUX_SELECTION_PEER_OPTION = "@railmux_selection_peer"
1822
1834
  RAILMUX_SELECTION_FROZEN_OPTION = "@railmux_selection_frozen_by"
@@ -2145,28 +2157,69 @@ def set_root_wheel_forwarding(
2145
2157
  backup: RootWheelBindingBackup,
2146
2158
  token: str,
2147
2159
  ) -> bool:
2148
- """Forward both wheel directions to mouse-aware pane applications.
2160
+ """Open canonical live history on wheel-up, otherwise preserve tmux input.
2149
2161
 
2150
2162
  The literal marker makes crash recovery able to distinguish Railmux's
2151
2163
  wrapper from a binding the user installed while Railmux was running.
2164
+ A pane-local primary/secondary marker routes the first live-agent wheel-up
2165
+ to Railmux's controller; the transcript viewer clears that marker, so its
2166
+ subsequent wheel events are forwarded normally.
2152
2167
  Non-mouse-aware panes retain the exact stock fallback; WheelDownPane was
2153
2168
  unbound by default and therefore has no false branch.
2154
2169
  """
2155
2170
  marker = f"{_ROOT_WHEEL_MARKER}-{token}"
2156
- condition = (
2171
+ mouse_condition = (
2157
2172
  "#{&&:#{mouse_any_flag},"
2158
2173
  f"#{{==:{marker},{marker}}}}}"
2159
2174
  )
2175
+ controller_present = f"#{{!=:#{{{RAILMUX_CONTROLLER_OPTION}}},}}"
2176
+
2177
+ def _preview_condition(slot_key: str) -> str:
2178
+ return (
2179
+ "#{&&:"
2180
+ f"#{{==:#{{{RAILMUX_HISTORY_PREVIEW_OPTION}}},{slot_key}}},"
2181
+ f"{controller_present}}}"
2182
+ )
2183
+
2184
+ def _send_preview(slot_key: str) -> str:
2185
+ _key, sequence = RAILMUX_HISTORY_PREVIEW_KEYS[slot_key]
2186
+ return (
2187
+ f"send-keys -l -t '#{{{RAILMUX_CONTROLLER_OPTION}}}' -- "
2188
+ f"'{sequence}'"
2189
+ )
2190
+
2160
2191
  try:
2161
2192
  for key in _ROOT_WHEEL_KEYS:
2193
+ binding = backup.get(key)
2194
+ fallback = _binding_command(binding) if binding is not None else ""
2195
+ mouse_fallback = (
2196
+ f"if-shell -F -t = '{mouse_condition}' 'send-keys -M'"
2197
+ )
2198
+ if fallback:
2199
+ mouse_fallback += f" '{fallback}'"
2162
2200
  args = [
2163
2201
  "tmux", "bind-key", "-T", "root", key,
2164
- "if-shell", "-F", "-t", "=", condition,
2165
- "send-keys -M",
2166
2202
  ]
2167
- binding = backup.get(key)
2168
- if binding is not None:
2169
- args.append(_binding_command(binding))
2203
+ if key == "WheelUpPane":
2204
+ secondary_fallback = (
2205
+ "if-shell -F -t = "
2206
+ f"'{_preview_condition('secondary')}' "
2207
+ f"\"{_send_preview('secondary')}\" "
2208
+ f"\"{mouse_fallback}\""
2209
+ )
2210
+ args.extend([
2211
+ "if-shell", "-F", "-t", "=",
2212
+ _preview_condition("primary"),
2213
+ _send_preview("primary"),
2214
+ secondary_fallback,
2215
+ ])
2216
+ else:
2217
+ args.extend([
2218
+ "if-shell", "-F", "-t", "=", mouse_condition,
2219
+ "send-keys -M",
2220
+ ])
2221
+ if fallback:
2222
+ args.append(fallback)
2170
2223
  subprocess.check_call(
2171
2224
  args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
2172
2225
  except (ValueError, subprocess.CalledProcessError, FileNotFoundError):
@@ -2186,6 +2239,28 @@ def root_wheel_bindings_owned_by(token: str) -> bool:
2186
2239
  )
2187
2240
 
2188
2241
 
2242
+ def root_wheel_bindings_are_current(token: str) -> bool:
2243
+ """Whether the owned wrapper includes canonical-history routing."""
2244
+ bindings = read_root_wheel_bindings()
2245
+ up = bindings.get("WheelUpPane")
2246
+ marker = f"{_ROOT_WHEEL_MARKER}-{token}"
2247
+ return (
2248
+ all(
2249
+ binding is not None
2250
+ and marker in binding
2251
+ and "mouse_any_flag" in binding
2252
+ and "send-keys -M" in binding
2253
+ for binding in bindings.values()
2254
+ )
2255
+ and up is not None
2256
+ and RAILMUX_HISTORY_PREVIEW_OPTION in up
2257
+ and all(
2258
+ sequence in up or sequence.replace("\x1b", r"\033") in up
2259
+ for _key, sequence in RAILMUX_HISTORY_PREVIEW_KEYS.values()
2260
+ )
2261
+ )
2262
+
2263
+
2189
2264
  def root_wheel_binding_is_original_or_owned(
2190
2265
  key: str,
2191
2266
  binding: str | None,
@@ -295,6 +295,81 @@ def _render_claude_native_tool_results(
295
295
  yield RESET
296
296
 
297
297
 
298
+ def _claude_current_branch(records: list[dict]) -> list[dict]:
299
+ """Project an append-only Claude JSONL onto its current main branch.
300
+
301
+ Claude Code links semantic records with ``uuid`` / ``parentUuid``. Editing
302
+ or rewinding a prompt appends a second child instead of removing the old
303
+ suffix, so file order is not conversation order. The newest non-sidechain
304
+ UUID is the active tip; walking its parents retains the shared prefix and
305
+ the replacement suffix while excluding abandoned and sub-agent branches.
306
+
307
+ UUID-less metadata is retained for the normal renderer filters. If the
308
+ input is a bounded tail, walking stops safely at the first missing parent.
309
+ """
310
+ by_uuid: dict[str, dict] = {}
311
+ tip: str | None = None
312
+ for record in records:
313
+ uuid = record.get("uuid")
314
+ if not isinstance(uuid, str) or not uuid:
315
+ continue
316
+ by_uuid[uuid] = record
317
+ if record.get("isSidechain") is not True:
318
+ tip = uuid
319
+ if tip is None:
320
+ return records
321
+
322
+ current: set[str] = set()
323
+ cursor: str | None = tip
324
+ while cursor and cursor not in current:
325
+ record = by_uuid.get(cursor)
326
+ if record is None:
327
+ break
328
+ current.add(cursor)
329
+ parent = record.get("parentUuid")
330
+ cursor = parent if isinstance(parent, str) and parent else None
331
+
332
+ return [
333
+ record for record in records
334
+ if not isinstance(record.get("uuid"), str)
335
+ or not record.get("uuid")
336
+ or record["uuid"] in current
337
+ ]
338
+
339
+
340
+ def _render_claude_records(
341
+ records: list[dict], *, claude_native: bool,
342
+ ):
343
+ """Render only the active Claude branch from buffered JSONL records."""
344
+ calls: dict[str, str] = {}
345
+ for record in _claude_current_branch(records):
346
+ rtype = record.get("type", "")
347
+ if rtype in _SKIP_TYPES:
348
+ continue
349
+ if _is_real_user(record):
350
+ rendered = (
351
+ _render_claude_native_user(record)
352
+ if claude_native
353
+ else _render_user(record)
354
+ )
355
+ if rendered:
356
+ yield rendered
357
+ if rtype == "user":
358
+ renderer = (
359
+ _render_claude_native_tool_results
360
+ if claude_native
361
+ else _render_claude_tool_results
362
+ )
363
+ yield from renderer(record, calls)
364
+ elif rtype == "assistant":
365
+ renderer = (
366
+ _render_claude_native_assistant
367
+ if claude_native
368
+ else _render_assistant_blocks
369
+ )
370
+ yield from renderer(record, calls)
371
+
372
+
298
373
  def format_transcript(
299
374
  source: Path | object,
300
375
  fmt: str | None = None,
@@ -326,7 +401,10 @@ def format_transcript(
326
401
  # call_id -> tool name, so Codex tool outputs can be labelled with the
327
402
  # call that produced them (issue #3).
328
403
  codex_calls: dict[str, str] = {}
329
- claude_calls: dict[str, str] = {}
404
+ # Claude rewind/edit branches live in one append-only JSONL, so those
405
+ # records must be buffered until the active tip is known. Production
406
+ # callers bound the input to the latest 2,000 records.
407
+ claude_records: list[dict] = []
330
408
  try:
331
409
  if isinstance(source, Path):
332
410
  fh = open(source, "r", encoding="utf-8")
@@ -360,33 +438,10 @@ def format_transcript(
360
438
  if _codex:
361
439
  yield from _render_codex(record, codex_calls)
362
440
  continue
363
-
364
- # -- Claude format (legacy) --------------------------------
365
- if rtype in _SKIP_TYPES:
366
- continue
367
-
368
- if _is_real_user(record):
369
- rendered = (
370
- _render_claude_native_user(record)
371
- if claude_native
372
- else _render_user(record)
373
- )
374
- if rendered:
375
- yield rendered
376
- if rtype == "user":
377
- renderer = (
378
- _render_claude_native_tool_results
379
- if claude_native
380
- else _render_claude_tool_results
381
- )
382
- yield from renderer(record, claude_calls)
383
- elif rtype == "assistant":
384
- renderer = (
385
- _render_claude_native_assistant
386
- if claude_native
387
- else _render_assistant_blocks
388
- )
389
- yield from renderer(record, claude_calls)
441
+ claude_records.append(record)
442
+ if _codex is False:
443
+ yield from _render_claude_records(
444
+ claude_records, claude_native=claude_native)
390
445
  except OSError:
391
446
  yield f"{YELLOW}Could not read: {source}{RESET}\n"
392
447
  finally: