railmux 0.3.3.dev5__tar.gz → 0.3.3.dev6__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.dev5/src/railmux.egg-info → railmux-0.3.3.dev6}/PKG-INFO +7 -5
  2. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/README.md +6 -4
  3. railmux-0.3.3.dev6/src/railmux/__init__.py +1 -0
  4. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/fast_display_server.py +29 -8
  5. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/tmux_ctl.py +8 -10
  6. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/tmux_server.py +22 -4
  7. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/ui/app.py +77 -26
  8. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6/src/railmux.egg-info}/PKG-INFO +7 -5
  9. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_codex_delete.py +1 -1
  10. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_codex_lineage.py +59 -11
  11. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_fast_full_window.py +73 -0
  12. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_focus_highlight.py +23 -3
  13. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_statusbar.py +12 -3
  14. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_tmux_ctl.py +4 -7
  15. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_tmux_integration.py +14 -6
  16. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_tmux_server.py +22 -0
  17. railmux-0.3.3.dev5/src/railmux/__init__.py +0 -1
  18. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/LICENSE +0 -0
  19. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/MANIFEST.in +0 -0
  20. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/pyproject.toml +0 -0
  21. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/setup.cfg +0 -0
  22. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/__main__.py +0 -0
  23. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/atomic_file.py +0 -0
  24. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/background_index.py +0 -0
  25. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/cli.py +0 -0
  26. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/codex_index.py +0 -0
  27. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/config.py +0 -0
  28. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/config_cli.py +0 -0
  29. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/diagnostics.py +0 -0
  30. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/discovery.py +0 -0
  31. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/display_transport.py +0 -0
  32. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/fast_display_client.py +0 -0
  33. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/fast_display_history.py +0 -0
  34. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/fast_display_input.py +0 -0
  35. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/fast_display_protocol.py +0 -0
  36. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/favorites.py +0 -0
  37. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/fuzzy.py +0 -0
  38. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/help_workspace.py +0 -0
  39. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/launcher.py +0 -0
  40. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/legacy_sessions.py +0 -0
  41. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/local_clipboard.py +0 -0
  42. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/local_open.py +0 -0
  43. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/models.py +0 -0
  44. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/modes.py +0 -0
  45. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/mouse_manager.py +0 -0
  46. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/orphan_marker.py +0 -0
  47. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/pane_surface.py +0 -0
  48. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/path_codec.py +0 -0
  49. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/remote_config.py +0 -0
  50. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/renames.py +0 -0
  51. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/restart_state.py +0 -0
  52. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/runtime_config.py +0 -0
  53. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/scroll_agent.py +0 -0
  54. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/scroll_manager.py +0 -0
  55. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/selection_isolation.py +0 -0
  56. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/self_update.py +0 -0
  57. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/session_cache.py +0 -0
  58. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/session_index.py +0 -0
  59. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/setting_contracts.py +0 -0
  60. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/settings.py +0 -0
  61. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/ssh_args.py +0 -0
  62. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/ssh_compat.py +0 -0
  63. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/ssh_display_diagnostics.py +0 -0
  64. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/ssh_doctor.py +0 -0
  65. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/system_deps.py +0 -0
  66. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/terminal_status.py +0 -0
  67. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/tmux_binding_manager.py +0 -0
  68. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/tmux_health.py +0 -0
  69. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/tool_panes.py +0 -0
  70. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/transcript.py +0 -0
  71. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/ui/__init__.py +0 -0
  72. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/ui/_widgets.py +0 -0
  73. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/ui/keymap.py +0 -0
  74. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/ui/modals.py +0 -0
  75. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/ui/projects_pane.py +0 -0
  76. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/ui/running_pane.py +0 -0
  77. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/ui/sessions_pane.py +0 -0
  78. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/ui/sidebar.py +0 -0
  79. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/ui/statusbar.py +0 -0
  80. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/ui/workspace.py +0 -0
  81. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux.egg-info/SOURCES.txt +0 -0
  82. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux.egg-info/dependency_links.txt +0 -0
  83. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux.egg-info/entry_points.txt +0 -0
  84. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux.egg-info/requires.txt +0 -0
  85. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux.egg-info/top_level.txt +0 -0
  86. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_agent_workspace.py +0 -0
  87. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_atomic_file.py +0 -0
  88. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_background_index.py +0 -0
  89. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_cli.py +0 -0
  90. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_codex_index.py +0 -0
  91. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_codex_transcript.py +0 -0
  92. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_config.py +0 -0
  93. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_config_cli.py +0 -0
  94. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_denested_transport_benchmark.py +0 -0
  95. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_diagnostics.py +0 -0
  96. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_discovery.py +0 -0
  97. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_display_transport.py +0 -0
  98. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_fullscreen_binding.py +0 -0
  99. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_fuzzy.py +0 -0
  100. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_help_agent.py +0 -0
  101. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_help_workspace.py +0 -0
  102. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_history_cleanup.py +0 -0
  103. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_keymap.py +0 -0
  104. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_launcher.py +0 -0
  105. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_layout_profile.py +0 -0
  106. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_legacy_sessions.py +0 -0
  107. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_local_clipboard.py +0 -0
  108. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_local_open.py +0 -0
  109. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_modals.py +0 -0
  110. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_mode_selection.py +0 -0
  111. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_modes.py +0 -0
  112. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_mouse_manager.py +0 -0
  113. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_new_project.py +0 -0
  114. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_options.py +0 -0
  115. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_orphan_recovery.py +0 -0
  116. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_pane_surface.py +0 -0
  117. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_path_codec.py +0 -0
  118. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_projects_pane.py +0 -0
  119. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_release_notes.py +0 -0
  120. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_release_workflow.py +0 -0
  121. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_remote_config.py +0 -0
  122. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_renames.py +0 -0
  123. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_restart_state.py +0 -0
  124. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_running_pane.py +0 -0
  125. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_running_sort.py +0 -0
  126. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_runtime_config.py +0 -0
  127. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_scroll_agent.py +0 -0
  128. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_scroll_manager.py +0 -0
  129. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_selection_isolation.py +0 -0
  130. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_self_update.py +0 -0
  131. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_session_cache.py +0 -0
  132. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_session_index.py +0 -0
  133. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_sessions_pane.py +0 -0
  134. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_setting_contracts.py +0 -0
  135. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_settings.py +0 -0
  136. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_sidebar.py +0 -0
  137. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_soft_quit.py +0 -0
  138. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_ssh_args.py +0 -0
  139. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_ssh_compat.py +0 -0
  140. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_ssh_display_benchmark.py +0 -0
  141. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_ssh_display_diagnostics.py +0 -0
  142. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_ssh_doctor.py +0 -0
  143. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_status.py +0 -0
  144. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_system_deps.py +0 -0
  145. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_terminal_size.py +0 -0
  146. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_terminal_status.py +0 -0
  147. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_termux_local_touch.py +0 -0
  148. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_tmux_binding_manager.py +0 -0
  149. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_tmux_health.py +0 -0
  150. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_tmux_status.py +0 -0
  151. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_tool_panes.py +0 -0
  152. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_transcript.py +0 -0
  153. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_utf8_state.py +0 -0
  154. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_web_demo_recorder.py +0 -0
  155. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_wheel_smoke.py +0 -0
  156. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_widgets.py +0 -0
  157. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_yolo_prompt.py +0 -0
  158. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tools/denested_transport_benchmark.py +0 -0
  159. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tools/release_notes.py +0 -0
  160. {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/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.dev5
3
+ Version: 0.3.3.dev6
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
@@ -243,8 +243,9 @@ The rename popup starts with the current title pre-filled. Press
243
243
  to cancel.
244
244
 
245
245
  After a delete is confirmed, its popup closes immediately. The bottom-right
246
- status shows **Deleting…** while Railmux stops the exact provider session and
247
- removes its history in the background, then reports the final result.
246
+ status shows **Deleting “session name”…** while Railmux stops the exact
247
+ provider session and removes its history in the background, then reports the
248
+ final result.
248
249
 
249
250
  Press `/` in Projects, Sessions, or Running to filter that section. Active
250
251
  filters are marked with `[filtered]` and survive a soft restart; reopening `/`
@@ -378,8 +379,9 @@ internal context and encrypted reasoning are hidden. Codex rewind lineages and
378
379
  Claude Code `parentUuid` branches are projected onto the provider's current
379
380
  conversation: the retained prefix and replacement suffix remain visible, while
380
381
  the abandoned suffix is hidden. When Railmux observes a running Codex session
381
- advance to its rewind child, it also removes the abandoned terminal screen and
382
- tmux scrollback before Codex redraws the live pane.
382
+ advance after rewind or steering, it resets only the current terminal view
383
+ before Codex redraws. Retained tmux scrollback is not erased, and managed
384
+ history continues from the canonical rollout without its abandoned suffix.
383
385
 
384
386
  Preview opens at the latest activity in `less`; large sessions are limited to
385
387
  their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
@@ -201,8 +201,9 @@ The rename popup starts with the current title pre-filled. Press
201
201
  to cancel.
202
202
 
203
203
  After a delete is confirmed, its popup closes immediately. The bottom-right
204
- status shows **Deleting…** while Railmux stops the exact provider session and
205
- removes its history in the background, then reports the final result.
204
+ status shows **Deleting “session name”…** while Railmux stops the exact
205
+ provider session and removes its history in the background, then reports the
206
+ final result.
206
207
 
207
208
  Press `/` in Projects, Sessions, or Running to filter that section. Active
208
209
  filters are marked with `[filtered]` and survive a soft restart; reopening `/`
@@ -336,8 +337,9 @@ internal context and encrypted reasoning are hidden. Codex rewind lineages and
336
337
  Claude Code `parentUuid` branches are projected onto the provider's current
337
338
  conversation: the retained prefix and replacement suffix remain visible, while
338
339
  the abandoned suffix is hidden. When Railmux observes a running Codex session
339
- advance to its rewind child, it also removes the abandoned terminal screen and
340
- tmux scrollback before Codex redraws the live pane.
340
+ advance after rewind or steering, it resets only the current terminal view
341
+ before Codex redraws. Retained tmux scrollback is not erased, and managed
342
+ history continues from the canonical rollout without its abandoned suffix.
341
343
 
342
344
  Preview opens at the latest activity in `less`; large sessions are limited to
343
345
  their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
@@ -0,0 +1 @@
1
+ __version__ = "0.3.3.dev6"
@@ -403,6 +403,7 @@ class _PaneGeometry:
403
403
  alternate_on: bool = False
404
404
  transcript_source: str | None = None
405
405
  transcript_backed: bool = False
406
+ transcript_provider: str | None = None
406
407
  claude_history_policy: str = "ask"
407
408
  history_generation: int = 0
408
409
 
@@ -916,10 +917,11 @@ def _list_agent_panes(
916
917
  if history_pane_id is None:
917
918
  history_server = None
918
919
  transcript_marker = fields[14] or None
919
- transcript_backed = (
920
- transcript_marker is not None
921
- and tmux_server.decode_transcript_source(transcript_marker) is not None
920
+ transcript_locator = (
921
+ tmux_server.decode_transcript_source(transcript_marker)
922
+ if transcript_marker is not None else None
922
923
  )
924
+ transcript_backed = transcript_locator is not None
923
925
  if not transcript_backed and fields[11] == "1" and fields[12] == "1":
924
926
  transcript_backed, transcript_marker = _inferred_transcript_source(
925
927
  session_id=session_id,
@@ -933,6 +935,7 @@ def _list_agent_panes(
933
935
  opened = tmux_server.open_transcript_source(transcript_marker)
934
936
  transcript_backed = opened is not None
935
937
  if opened is not None:
938
+ transcript_locator = opened[0]
936
939
  os.close(opened[1])
937
940
  rows.append(
938
941
  (
@@ -951,6 +954,11 @@ def _list_agent_panes(
951
954
  alternate_on=fields[11] == "1",
952
955
  transcript_source=transcript_marker,
953
956
  transcript_backed=transcript_backed,
957
+ transcript_provider=(
958
+ transcript_locator.provider
959
+ if transcript_backed and transcript_locator is not None
960
+ else None
961
+ ),
954
962
  claude_history_policy=claude_history_policy,
955
963
  history_generation=_history_generation(fields[15]),
956
964
  ),
@@ -1267,7 +1275,9 @@ def _transcript_rows(
1267
1275
  return None
1268
1276
  formatted = "".join(
1269
1277
  transcript_renderer.format_transcript(
1270
- io.StringIO(raw), "claude", claude_native=True
1278
+ io.StringIO(raw),
1279
+ source.provider,
1280
+ claude_native=source.provider == "claude",
1271
1281
  )
1272
1282
  )
1273
1283
  rows, dropped = _wrap_transcript_rows(pyte, formatted, width)
@@ -1330,12 +1340,21 @@ def _capture_pane_history(
1330
1340
  # pane's current viewport merely to retain older scrollback.
1331
1341
  current_raw = raw_lines[-pane.height :]
1332
1342
  transcript_entry = None
1343
+ # ``None`` is retained only for old/test geometry that predates the
1344
+ # provider field; every validated live marker carries an explicit value.
1345
+ transcript_provider = pane.transcript_provider or "claude"
1333
1346
  if (
1334
1347
  pane.transcript_backed
1335
- and pane.claude_history_policy == "local"
1336
- and pane.alternate_on
1337
- and pane.history_size == 0
1338
1348
  and pane.transcript_source
1349
+ and (
1350
+ transcript_provider == "codex"
1351
+ or (
1352
+ transcript_provider == "claude"
1353
+ and pane.claude_history_policy == "local"
1354
+ and pane.alternate_on
1355
+ and pane.history_size == 0
1356
+ )
1357
+ )
1339
1358
  ):
1340
1359
  transcript_entry = _transcript_rows(
1341
1360
  pyte,
@@ -1387,7 +1406,9 @@ def _capture_pane_history(
1387
1406
  transcript_backed=transcript_used,
1388
1407
  transcript_available=pane.transcript_backed,
1389
1408
  history_choice_required=(
1390
- pane.transcript_backed and pane.claude_history_policy == "ask"
1409
+ pane.transcript_backed
1410
+ and transcript_provider == "claude"
1411
+ and pane.claude_history_policy == "ask"
1391
1412
  ),
1392
1413
  more_available=(
1393
1414
  budget_truncated
@@ -3355,14 +3355,15 @@ def exact_pane_alive(identity: PaneIdentity) -> bool:
3355
3355
  )
3356
3356
 
3357
3357
 
3358
- def reset_pane_history(identity: PaneIdentity) -> bool:
3359
- """Reset visible bytes and scrollback for the exact observed live pane.
3358
+ def reset_pane_view(identity: PaneIdentity) -> bool:
3359
+ """Reset visible terminal state without deleting retained scrollback.
3360
3360
 
3361
3361
  Revalidate the immutable session, pane process, and window before issuing
3362
- one tmux command queue that resets the terminal model and clears history.
3363
- Then send the pane's foreground process group an ordinary same-size
3364
- ``SIGWINCH`` so its TUI repaints and reasserts terminal modes without a
3365
- geometry change or injected keyboard input.
3362
+ tmux's terminal reset. Then send the pane's foreground process group an
3363
+ ordinary same-size ``SIGWINCH`` so its TUI repaints and reasserts terminal
3364
+ modes without a geometry change or injected keyboard input. The reset can
3365
+ push the old viewport into tmux history, but never destroys the retained
3366
+ prefix; managed history surfaces use the provider's canonical transcript.
3366
3367
  """
3367
3368
  current = pane_identity(identity.pane_id)
3368
3369
  if (current is None or current.dead
@@ -3372,10 +3373,7 @@ def reset_pane_history(identity: PaneIdentity) -> bool:
3372
3373
  return False
3373
3374
  try:
3374
3375
  subprocess.check_call(
3375
- [
3376
- "tmux", "send-keys", "-R", "-t", identity.pane_id,
3377
- ";", "clear-history", "-t", identity.pane_id,
3378
- ],
3376
+ ["tmux", "send-keys", "-R", "-t", identity.pane_id],
3379
3377
  stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
3380
3378
  )
3381
3379
  except (OSError, subprocess.CalledProcessError):
@@ -307,11 +307,21 @@ def encode_transcript_source(
307
307
  ) -> str | None:
308
308
  """Encode an exact transcript hint for a displayed managed agent pane."""
309
309
  raw_path = str(path)
310
+ valid_name = (
311
+ path.name == f"{session_id}.jsonl"
312
+ if provider == "claude"
313
+ else (
314
+ path.name.startswith("rollout-")
315
+ and path.name.endswith(f"-{session_id}.jsonl")
316
+ )
317
+ if provider == "codex"
318
+ else False
319
+ )
310
320
  if (
311
- provider != "claude"
321
+ provider not in {"claude", "codex"}
312
322
  or not _SESSION_ID_RE.fullmatch(session_id)
313
323
  or not path.is_absolute()
314
- or path.name != f"{session_id}.jsonl"
324
+ or not valid_name
315
325
  or len(raw_path) > 4096
316
326
  ):
317
327
  return None
@@ -348,7 +358,7 @@ def decode_transcript_source(raw: str) -> TranscriptSource | None:
348
358
  raw_path = payload.get("path")
349
359
  if (
350
360
  payload.get("schema_version") != _TRANSCRIPT_SOURCE_SCHEMA
351
- or provider != "claude"
361
+ or provider not in {"claude", "codex"}
352
362
  or not isinstance(session_id, str)
353
363
  or not _SESSION_ID_RE.fullmatch(session_id)
354
364
  or not isinstance(raw_path, str)
@@ -357,7 +367,15 @@ def decode_transcript_source(raw: str) -> TranscriptSource | None:
357
367
  ):
358
368
  return None
359
369
  path = Path(raw_path)
360
- if not path.is_absolute() or path.name != f"{session_id}.jsonl":
370
+ valid_name = (
371
+ path.name == f"{session_id}.jsonl"
372
+ if provider == "claude"
373
+ else (
374
+ path.name.startswith("rollout-")
375
+ and path.name.endswith(f"-{session_id}.jsonl")
376
+ )
377
+ )
378
+ if not path.is_absolute() or not valid_name:
361
379
  return None
362
380
  return TranscriptSource(provider, session_id, path)
363
381
 
@@ -553,6 +553,7 @@ class App:
553
553
  _status_text: str | None = None
554
554
  _status_level: str = "info"
555
555
  _status_since: float = 0.0
556
+ _delete_progress_text: str = "Deleting…"
556
557
  _attention_notice_key: tuple[str, int] | None = None
557
558
  _tip_index: int = 0
558
559
  _tip_since: float = 0.0
@@ -748,6 +749,7 @@ class App:
748
749
  self._status_text: str | None = None
749
750
  self._status_level: str = "info"
750
751
  self._status_since: float = 0.0
752
+ self._delete_progress_text: str = "Deleting…"
751
753
  self._rendered_status_text: str | None = None
752
754
  self._rendered_status_level: str = "tip"
753
755
  self._status_feedback_alarm: object | None = None
@@ -1136,6 +1138,19 @@ class App:
1136
1138
  logical_id,
1137
1139
  running.project.claude_dir / f"{logical_id}.jsonl",
1138
1140
  )
1141
+ elif (
1142
+ running is not None
1143
+ and running.session_type == "codex"
1144
+ and logical_id is not None
1145
+ and not running.is_placeholder
1146
+ ):
1147
+ representative = self._codex_representative(logical_id)
1148
+ if representative is not None:
1149
+ marker = tmux_server.encode_transcript_source(
1150
+ "codex",
1151
+ representative.session_id,
1152
+ representative.jsonl_path,
1153
+ )
1139
1154
  tmux_ctl.set_pane_user_option(
1140
1155
  pane_id,
1141
1156
  tmux_server.TRANSCRIPT_SOURCE_OPTION,
@@ -8605,26 +8620,28 @@ class App:
8605
8620
  server: tmux_ctl.ServerSnapshot | None,
8606
8621
  codex_rollout_probes: dict[str, set[str] | None] | None,
8607
8622
  ) -> None:
8608
- """Drop stale tmux bytes after an exact live Codex rewind transition.
8623
+ """Advance managed history after an exact live Codex branch transition.
8609
8624
 
8610
8625
  The provider rollout is the history authority, but Codex keeps using
8611
8626
  the same terminal pane after a rewind. Its abandoned suffix can
8612
- therefore survive in tmux scrollback even though the canonical JSONL
8613
- is already correct. Baseline the first observed rollout, then clear
8614
- only when the next canonical rollout names that baseline as its parent.
8615
- On procfs systems the new rollout must also be open in this exact pane's
8616
- process tree; an unavailable probe degrades to the provider lineage
8617
- link, while a negative probe waits for a later refresh.
8627
+ therefore survive in raw tmux scrollback even though the canonical
8628
+ JSONL is already correct. Baseline the first observed rollout, then
8629
+ reset only the live terminal view when the next canonical rollout names
8630
+ that baseline as its parent. Managed local/SSH history reads the stamped
8631
+ canonical transcript instead of the abandoned raw suffix. On procfs
8632
+ systems the new rollout must also be open in this exact pane's process
8633
+ tree; an unavailable probe degrades to the provider lineage link, while
8634
+ a negative probe waits for a later refresh.
8618
8635
  """
8619
8636
  current_id = meta.session_id
8620
8637
  previous_id = running.codex_canonical_session_id
8621
8638
  if previous_id is None:
8622
8639
  running.codex_canonical_session_id = current_id
8623
- self._stamp_codex_history_generation(running, current_id)
8640
+ self._stamp_codex_history_state(running, meta)
8624
8641
  return
8625
8642
  if current_id == previous_id:
8626
8643
  if not running.codex_history_generation_stamped:
8627
- self._stamp_codex_history_generation(running, current_id)
8644
+ self._stamp_codex_history_state(running, meta)
8628
8645
  return
8629
8646
  if (running.is_legacy or meta.forked_from_id != previous_id
8630
8647
  or current_id not in self._codex_lineage_ids(previous_id)):
@@ -8643,14 +8660,10 @@ class App:
8643
8660
  return
8644
8661
 
8645
8662
  identity = self._codex_real_pane_identity(running)
8646
- if identity is None or not tmux_ctl.reset_pane_history(identity):
8663
+ if identity is None or not tmux_ctl.reset_pane_view(identity):
8647
8664
  return
8648
8665
  running.codex_canonical_session_id = current_id
8649
- running.codex_history_generation_stamped = tmux_ctl.set_pane_user_option(
8650
- identity.pane_id,
8651
- tmux_ctl.RAILMUX_HISTORY_GENERATION_OPTION,
8652
- current_id,
8653
- )
8666
+ self._stamp_codex_history_state(running, meta)
8654
8667
 
8655
8668
  def _codex_real_pane_identity(
8656
8669
  self, running: _Running,
@@ -8663,20 +8676,46 @@ class App:
8663
8676
  topology = tmux_ctl.session_topology(running.tmux_name)
8664
8677
  return topology.single_live_pane if topology is not None else None
8665
8678
 
8666
- def _stamp_codex_history_generation(
8667
- self, running: _Running, session_id: str,
8679
+ def _stamp_codex_history_state(
8680
+ self, running: _Running, meta: SessionMeta,
8668
8681
  ) -> None:
8669
- """Publish a content-free epoch for full-window SSH history caches."""
8682
+ """Publish the canonical transcript and its content-free cache epoch."""
8670
8683
  if running.is_legacy:
8671
8684
  return
8672
8685
  identity = self._codex_real_pane_identity(running)
8673
8686
  if identity is None:
8674
8687
  return
8675
- running.codex_history_generation_stamped = tmux_ctl.set_pane_user_option(
8676
- identity.pane_id,
8677
- tmux_ctl.RAILMUX_HISTORY_GENERATION_OPTION,
8678
- session_id,
8688
+ marker = tmux_server.encode_transcript_source(
8689
+ "codex", meta.session_id, meta.jsonl_path)
8690
+ pane_ids = {identity.pane_id}
8691
+ workspace = getattr(self, "_workspace", None)
8692
+ if workspace is not None:
8693
+ pane_ids.update(
8694
+ slot.pane_id
8695
+ for slot in workspace.slots
8696
+ if slot.agent_tmux_name == running.tmux_name
8697
+ and slot.pane_id is not None
8698
+ )
8699
+ transcript_results = (
8700
+ [tmux_ctl.set_pane_user_option(
8701
+ pane_id,
8702
+ tmux_server.TRANSCRIPT_SOURCE_OPTION,
8703
+ marker,
8704
+ ) for pane_id in sorted(pane_ids)]
8705
+ if marker is not None else []
8679
8706
  )
8707
+ generation_results = [
8708
+ tmux_ctl.set_pane_user_option(
8709
+ pane_id,
8710
+ tmux_ctl.RAILMUX_HISTORY_GENERATION_OPTION,
8711
+ meta.session_id,
8712
+ )
8713
+ for pane_id in sorted(pane_ids)
8714
+ ]
8715
+ transcript_stamped = marker is not None and all(transcript_results)
8716
+ generation_stamped = all(generation_results)
8717
+ running.codex_history_generation_stamped = bool(
8718
+ transcript_stamped and generation_stamped)
8680
8719
 
8681
8720
  def _maybe_resort_running(self) -> None:
8682
8721
  """Re-order the Running registry by recency, at most once per minute.
@@ -9159,7 +9198,8 @@ class App:
9159
9198
  # Paint the closed confirmation and durable progress message before
9160
9199
  # even the bounded tmux preparation probes run. This makes Enter feel
9161
9200
  # immediate on slow homes and provider installations.
9162
- self._set_status("Deleting…", "info", force=True)
9201
+ self._delete_progress_text = self._deleting_status(label)
9202
+ self._set_status(self._delete_progress_text, "info", force=True)
9163
9203
  self._redraw_focus_state_now()
9164
9204
 
9165
9205
  if tmux_name is None and session_id is not None:
@@ -9248,6 +9288,16 @@ class App:
9248
9288
  self._delete_thread = thread
9249
9289
  thread.start()
9250
9290
 
9291
+ @staticmethod
9292
+ def _deleting_status(label: str) -> str:
9293
+ """Return one stable, bounded progress label for the status bar."""
9294
+ display = " ".join(label.split())
9295
+ if not display:
9296
+ return "Deleting…"
9297
+ if len(display) > 40:
9298
+ display = display[:39].rstrip() + "…"
9299
+ return f"Deleting “{display}”…"
9300
+
9251
9301
  def _execute_delete_task(self, task: _DeleteTask) -> _DeleteResult:
9252
9302
  """Perform blocking exact kill and provider cleanup without UI state."""
9253
9303
  if task.pane_identity is not None:
@@ -10489,9 +10539,10 @@ class App:
10489
10539
  # This is operation state, not a one-shot acknowledgement. Keep it
10490
10540
  # alive past the ordinary info TTL. A newly raised warning/error
10491
10541
  # retains its short severity hold, after which progress returns.
10492
- if self._status_text != "Deleting…":
10493
- self._set_status("Deleting…", "info")
10494
- if self._status_text == "Deleting…":
10542
+ progress = getattr(self, "_delete_progress_text", "Deleting…")
10543
+ if self._status_text != progress:
10544
+ self._set_status(progress, "info")
10545
+ if self._status_text == progress:
10495
10546
  self._status_since = now
10496
10547
  return
10497
10548
  if self._status_text is not None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: railmux
3
- Version: 0.3.3.dev5
3
+ Version: 0.3.3.dev6
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
@@ -243,8 +243,9 @@ The rename popup starts with the current title pre-filled. Press
243
243
  to cancel.
244
244
 
245
245
  After a delete is confirmed, its popup closes immediately. The bottom-right
246
- status shows **Deleting…** while Railmux stops the exact provider session and
247
- removes its history in the background, then reports the final result.
246
+ status shows **Deleting “session name”…** while Railmux stops the exact
247
+ provider session and removes its history in the background, then reports the
248
+ final result.
248
249
 
249
250
  Press `/` in Projects, Sessions, or Running to filter that section. Active
250
251
  filters are marked with `[filtered]` and survive a soft restart; reopening `/`
@@ -378,8 +379,9 @@ internal context and encrypted reasoning are hidden. Codex rewind lineages and
378
379
  Claude Code `parentUuid` branches are projected onto the provider's current
379
380
  conversation: the retained prefix and replacement suffix remain visible, while
380
381
  the abandoned suffix is hidden. When Railmux observes a running Codex session
381
- advance to its rewind child, it also removes the abandoned terminal screen and
382
- tmux scrollback before Codex redraws the live pane.
382
+ advance after rewind or steering, it resets only the current terminal view
383
+ before Codex redraws. Retained tmux scrollback is not erased, and managed
384
+ history continues from the canonical rollout without its abandoned suffix.
383
385
 
384
386
  Preview opens at the latest activity in `less`; large sessions are limited to
385
387
  their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
@@ -295,7 +295,7 @@ def test_cleanup_runs_blocking_work_in_background_and_publishes_on_refresh(
295
295
  assert started.wait(0.5)
296
296
  assert app._delete_thread is not None
297
297
  assert app._delete_thread.is_alive()
298
- assert statuses[-1] == ("Deleting…", "info")
298
+ assert statuses[-1] == ("Deleting “x”…", "info")
299
299
  assert redraws == [True]
300
300
 
301
301
  # A second destructive request cannot race the in-flight transaction.
@@ -4,10 +4,11 @@ from __future__ import annotations
4
4
  from pathlib import Path
5
5
  from unittest.mock import MagicMock
6
6
 
7
- from railmux import tmux_ctl
7
+ from railmux import tmux_ctl, tmux_server
8
8
  from railmux.favorites import Favorites
9
9
  from railmux.models import Project, SessionMeta
10
10
  from railmux.ui.app import App, _Running
11
+ from railmux.ui.workspace import AgentWorkspace
11
12
 
12
13
 
13
14
  class _LineageIndex:
@@ -66,7 +67,7 @@ def test_existing_root_favorite_marks_visible_leaf(
66
67
  assert app._favorite_ids_for_view() == {"root", "middle", "leaf"}
67
68
 
68
69
 
69
- def test_live_rewind_clears_only_exact_new_rollout_scrollback(
70
+ def test_live_rewind_resets_view_and_stamps_canonical_transcript(
70
71
  monkeypatch, tmp_path,
71
72
  ) -> None:
72
73
  root_id = "019fc605-5188-7212-bc48-ea023fe8b73c"
@@ -101,8 +102,8 @@ def test_live_rewind_clears_only_exact_new_rollout_scrollback(
101
102
  transport.displayed_real_pane.return_value = "%9"
102
103
  app._display_transport = MagicMock(return_value=transport)
103
104
  monkeypatch.setattr(tmux_ctl, "pane_identity", lambda _pane: identity)
104
- cleared = MagicMock(return_value=True)
105
- monkeypatch.setattr(tmux_ctl, "reset_pane_history", cleared)
105
+ reset = MagicMock(return_value=True)
106
+ monkeypatch.setattr(tmux_ctl, "reset_pane_view", reset)
106
107
  stamped = MagicMock(return_value=True)
107
108
  monkeypatch.setattr(tmux_ctl, "set_pane_user_option", stamped)
108
109
  probes: dict[str, set[str] | None] = {}
@@ -110,16 +111,22 @@ def test_live_rewind_clears_only_exact_new_rollout_scrollback(
110
111
  # An indexed child is not enough: on procfs the exact live writer must
111
112
  # first expose that rollout through its open file descriptors.
112
113
  app._sync_codex_rewind_scrollback(running, meta, None, probes)
113
- cleared.assert_not_called()
114
+ reset.assert_not_called()
114
115
  assert running.codex_canonical_session_id == root_id
115
116
 
116
117
  probes.clear()
117
118
  app._codex_rollout_ids.return_value = {root_id, leaf_id}
118
119
  app._sync_codex_rewind_scrollback(running, meta, None, probes)
119
120
 
120
- cleared.assert_called_once_with(identity)
121
+ reset.assert_called_once_with(identity)
121
122
  assert running.codex_canonical_session_id == leaf_id
122
- stamped.assert_called_once_with(
123
+ assert stamped.call_count == 2
124
+ transcript_call, generation_call = stamped.call_args_list
125
+ assert transcript_call.args[:2] == (
126
+ "%9", tmux_server.TRANSCRIPT_SOURCE_OPTION,
127
+ )
128
+ assert leaf_id in transcript_call.args[2]
129
+ assert generation_call.args == (
123
130
  "%9", tmux_ctl.RAILMUX_HISTORY_GENERATION_OPTION, leaf_id)
124
131
 
125
132
 
@@ -147,11 +154,52 @@ def test_first_codex_generation_only_establishes_scrollback_baseline(
147
154
  session_type="codex",
148
155
  )
149
156
  app = App.__new__(App)
150
- app._stamp_codex_history_generation = MagicMock()
151
- cleared = MagicMock(return_value=True)
152
- monkeypatch.setattr(tmux_ctl, "reset_pane_history", cleared)
157
+ app._stamp_codex_history_state = MagicMock()
158
+ reset = MagicMock(return_value=True)
159
+ monkeypatch.setattr(tmux_ctl, "reset_pane_view", reset)
153
160
 
154
161
  app._sync_codex_rewind_scrollback(running, meta, None, {})
155
162
 
156
163
  assert running.codex_canonical_session_id == session_id
157
- cleared.assert_not_called()
164
+ reset.assert_not_called()
165
+ app._stamp_codex_history_state.assert_called_once_with(running, meta)
166
+
167
+
168
+ def test_codex_history_state_stamps_nested_wrapper_and_real_pane(
169
+ monkeypatch, tmp_path,
170
+ ) -> None:
171
+ session_id = "019fc7c1-a27c-7ae0-9937-7570552a112a"
172
+ project = Project(tmp_path, "-project", Path(), 1, 0.0)
173
+ running = _Running(
174
+ key=session_id,
175
+ tmux_name="cx-live",
176
+ label="project/conversation",
177
+ project=project,
178
+ session_type="codex",
179
+ )
180
+ meta = SessionMeta(
181
+ project=project,
182
+ session_id=session_id,
183
+ jsonl_path=tmp_path / (
184
+ f"rollout-2026-08-03T13-13-03-{session_id}.jsonl"),
185
+ title="current",
186
+ message_count=2,
187
+ token_total=0,
188
+ last_mtime=2.0,
189
+ session_type="codex",
190
+ )
191
+ identity = tmux_ctl.PaneIdentity(
192
+ "%9", 123, "cx-live", "$4", "@5", False, 80, 24)
193
+ app = App.__new__(App)
194
+ app._workspace = AgentWorkspace()
195
+ app._workspace.primary.pane_id = "%10"
196
+ app._workspace.primary.agent_tmux_name = "cx-live"
197
+ app._codex_real_pane_identity = MagicMock(return_value=identity)
198
+ stamped = MagicMock(return_value=True)
199
+ monkeypatch.setattr(tmux_ctl, "set_pane_user_option", stamped)
200
+
201
+ app._stamp_codex_history_state(running, meta)
202
+
203
+ assert running.codex_history_generation_stamped
204
+ assert {call.args[0] for call in stamped.call_args_list} == {"%9", "%10"}
205
+ assert stamped.call_count == 4
@@ -6827,6 +6827,57 @@ def test_server_waits_for_local_choice_before_rendering_claude_transcript(
6827
6827
  transcript_rows.assert_not_called()
6828
6828
 
6829
6829
 
6830
+ def test_server_codex_history_uses_canonical_transcript_after_rewind(
6831
+ monkeypatch,
6832
+ ):
6833
+ pane = fast_display_server._PaneGeometry(
6834
+ "%8",
6835
+ 31,
6836
+ 0,
6837
+ 40,
6838
+ 2,
6839
+ mouse_forwardable=True,
6840
+ history_size=500,
6841
+ alternate_on=False,
6842
+ transcript_source="codex-marker",
6843
+ transcript_backed=True,
6844
+ transcript_provider="codex",
6845
+ history_generation=19,
6846
+ )
6847
+ canonical = (b"retained prompt", b"replacement answer")
6848
+ monkeypatch.setattr(
6849
+ subprocess,
6850
+ "check_output",
6851
+ lambda *_args, **_kwargs: (
6852
+ b"abandoned red interruption\nlive-a\nlive-b\n"
6853
+ ),
6854
+ )
6855
+ monkeypatch.setattr(
6856
+ fast_display_server,
6857
+ "_transcript_rows",
6858
+ lambda *_args, **_kwargs: fast_display_server._TranscriptCacheEntry(
6859
+ (1, 2, 3, 4), canonical, False
6860
+ ),
6861
+ )
6862
+ monkeypatch.setattr(
6863
+ fast_display_server,
6864
+ "_render_history_line",
6865
+ lambda _pyte, line, _width: line,
6866
+ )
6867
+
6868
+ snapshot = fast_display_server._capture_pane_history(
6869
+ object(), pane, 7, 100)
6870
+
6871
+ assert snapshot is not None
6872
+ assert snapshot.transcript_backed
6873
+ assert not snapshot.history_choice_required
6874
+ assert snapshot.generation == 19
6875
+ assert b"retained prompt" in snapshot.lines
6876
+ assert snapshot.lines[-2:] == (b"live-a", b"live-b")
6877
+ assert all(b"abandoned red interruption" not in line
6878
+ for line in snapshot.lines)
6879
+
6880
+
6830
6881
  def test_server_unreadable_claude_transcript_preserves_native_wheel_fallback(
6831
6882
  monkeypatch,
6832
6883
  ):
@@ -6902,6 +6953,28 @@ def test_transcript_cache_evicts_least_recent_file_width(monkeypatch, tmp_path):
6902
6953
  fast_display_server._TRANSCRIPT_CACHE.clear()
6903
6954
 
6904
6955
 
6956
+ def test_transcript_rows_render_codex_locator_with_codex_formatter(tmp_path):
6957
+ pyte = fast_display_server._extended_pyte(__import__("pyte"))
6958
+ session_id = "019fcaad-27a1-70c0-8029-8a9c7803fa6b"
6959
+ path = tmp_path / f"rollout-2026-08-04T02-49-33-{session_id}.jsonl"
6960
+ path.write_text(
6961
+ '{"type":"response_item","payload":{"type":"message",'
6962
+ '"role":"user","content":[{"type":"input_text",'
6963
+ '"text":"retained question"}]}}\n'
6964
+ )
6965
+ marker = fast_display_server.tmux_server.encode_transcript_source(
6966
+ "codex", session_id, path)
6967
+ assert marker is not None
6968
+ fast_display_server._TRANSCRIPT_CACHE.clear()
6969
+ try:
6970
+ entry = fast_display_server._transcript_rows(
6971
+ pyte, marker, 60, allow_stale=False)
6972
+ assert entry is not None
6973
+ assert any(b"retained question" in row for row in entry.rows)
6974
+ finally:
6975
+ fast_display_server._TRANSCRIPT_CACHE.clear()
6976
+
6977
+
6905
6978
  def test_server_history_capture_truncates_to_newest_styled_byte_budget(
6906
6979
  monkeypatch,
6907
6980
  ):