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.
- {railmux-0.3.3.dev5/src/railmux.egg-info → railmux-0.3.3.dev6}/PKG-INFO +7 -5
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/README.md +6 -4
- railmux-0.3.3.dev6/src/railmux/__init__.py +1 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/fast_display_server.py +29 -8
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/tmux_ctl.py +8 -10
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/tmux_server.py +22 -4
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/ui/app.py +77 -26
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6/src/railmux.egg-info}/PKG-INFO +7 -5
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_codex_delete.py +1 -1
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_codex_lineage.py +59 -11
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_fast_full_window.py +73 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_focus_highlight.py +23 -3
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_statusbar.py +12 -3
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_tmux_ctl.py +4 -7
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_tmux_integration.py +14 -6
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_tmux_server.py +22 -0
- railmux-0.3.3.dev5/src/railmux/__init__.py +0 -1
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/LICENSE +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/MANIFEST.in +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/pyproject.toml +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/setup.cfg +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/__main__.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/atomic_file.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/background_index.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/cli.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/codex_index.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/config.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/config_cli.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/diagnostics.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/discovery.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/display_transport.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/fast_display_client.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/favorites.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/fuzzy.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/help_workspace.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/launcher.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/local_open.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/models.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/modes.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/pane_surface.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/path_codec.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/remote_config.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/renames.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/restart_state.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/runtime_config.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/self_update.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/session_cache.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/session_index.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/settings.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/ssh_args.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/system_deps.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/terminal_status.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/tmux_health.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/tool_panes.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/transcript.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/ui/modals.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux.egg-info/SOURCES.txt +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_agent_workspace.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_atomic_file.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_background_index.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_cli.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_codex_index.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_codex_transcript.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_config.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_config_cli.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_diagnostics.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_discovery.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_display_transport.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_fuzzy.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_help_agent.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_help_workspace.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_history_cleanup.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_keymap.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_launcher.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_layout_profile.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_local_clipboard.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_local_open.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_modals.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_mode_selection.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_modes.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_mouse_manager.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_new_project.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_options.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_pane_surface.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_path_codec.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_projects_pane.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_release_notes.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_release_workflow.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_remote_config.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_renames.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_restart_state.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_running_pane.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_running_sort.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_runtime_config.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_scroll_agent.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_scroll_manager.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_selection_isolation.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_self_update.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_session_cache.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_session_index.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_sessions_pane.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_setting_contracts.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_settings.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_sidebar.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_soft_quit.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_ssh_args.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_ssh_compat.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_status.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_system_deps.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_terminal_size.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_terminal_status.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_tmux_health.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_tmux_status.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_tool_panes.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_transcript.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_utf8_state.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_widgets.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.3.3.dev5 → railmux-0.3.3.dev6}/tools/release_notes.py +0 -0
- {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.
|
|
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
|
|
247
|
-
removes its history in the background, then reports the
|
|
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
|
|
382
|
-
tmux scrollback
|
|
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
|
|
205
|
-
removes its history in the background, then reports the
|
|
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
|
|
340
|
-
tmux scrollback
|
|
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
|
-
|
|
920
|
-
transcript_marker
|
|
921
|
-
|
|
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),
|
|
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
|
|
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
|
|
3359
|
-
"""Reset visible
|
|
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
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
"""
|
|
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
|
|
8613
|
-
is already correct.
|
|
8614
|
-
only when the next canonical rollout names
|
|
8615
|
-
|
|
8616
|
-
|
|
8617
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
8667
|
-
self, running: _Running,
|
|
8679
|
+
def _stamp_codex_history_state(
|
|
8680
|
+
self, running: _Running, meta: SessionMeta,
|
|
8668
8681
|
) -> None:
|
|
8669
|
-
"""Publish
|
|
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
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
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.
|
|
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
|
-
|
|
10493
|
-
|
|
10494
|
-
|
|
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.
|
|
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
|
|
247
|
-
removes its history in the background, then reports the
|
|
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
|
|
382
|
-
tmux scrollback
|
|
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
|
|
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
|
|
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
|
-
|
|
105
|
-
monkeypatch.setattr(tmux_ctl, "
|
|
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
|
-
|
|
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
|
-
|
|
121
|
+
reset.assert_called_once_with(identity)
|
|
121
122
|
assert running.codex_canonical_session_id == leaf_id
|
|
122
|
-
stamped.
|
|
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.
|
|
151
|
-
|
|
152
|
-
monkeypatch.setattr(tmux_ctl, "
|
|
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
|
-
|
|
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
|
):
|