railmux 0.3.6.dev1__tar.gz → 0.3.6.dev2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {railmux-0.3.6.dev1/src/railmux.egg-info → railmux-0.3.6.dev2}/PKG-INFO +6 -6
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/README.md +5 -5
- railmux-0.3.6.dev2/src/railmux/__init__.py +1 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/fast_display_history.py +93 -25
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/tmux_binding_manager.py +5 -2
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/tmux_ctl.py +7 -41
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/app.py +244 -42
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/modals.py +81 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2/src/railmux.egg-info}/PKG-INFO +6 -6
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_agent_workspace.py +1 -0
- railmux-0.3.6.dev2/tests/test_codex_lineage.py +561 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_fast_full_window.py +177 -43
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_focus_highlight.py +6 -2
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_modals.py +68 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_soft_quit.py +101 -1
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_tmux_binding_manager.py +25 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_tmux_ctl.py +0 -24
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_tmux_integration.py +12 -49
- railmux-0.3.6.dev1/src/railmux/__init__.py +0 -1
- railmux-0.3.6.dev1/tests/test_codex_lineage.py +0 -253
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/LICENSE +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/MANIFEST.in +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/pyproject.toml +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/setup.cfg +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/__main__.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/atomic_file.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/background_index.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/cli.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/codex_index.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/config.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/config_cli.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/diagnostics.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/discovery.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/display_transport.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/fast_display_client.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/fast_display_server.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/favorites.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/fuzzy.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/help_workspace.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/launcher.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/local_open.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/models.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/modes.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/pane_surface.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/path_codec.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/remote_config.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/renames.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/restart_state.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/runtime_config.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/self_update.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/session_cache.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/session_index.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/session_lease.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/settings.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/ssh_args.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/system_deps.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/terminal_status.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/tmux_health.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/tmux_server.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/tool_panes.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/transcript.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux.egg-info/SOURCES.txt +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_atomic_file.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_background_index.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_cli.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_codex_delete.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_codex_index.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_codex_transcript.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_config.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_config_cli.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_diagnostics.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_discovery.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_display_transport.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_fuzzy.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_help_agent.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_help_workspace.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_history_cleanup.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_keymap.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_launcher.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_layout_profile.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_local_clipboard.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_local_open.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_mode_selection.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_modes.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_mouse_manager.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_new_project.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_options.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_pane_surface.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_path_codec.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_projects_pane.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_release_notes.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_release_workflow.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_remote_config.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_renames.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_restart_state.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_running_pane.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_running_sort.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_runtime_config.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_scroll_agent.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_scroll_manager.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_selection_isolation.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_self_update.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_session_cache.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_session_index.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_session_lease.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_sessions_pane.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_setting_contracts.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_settings.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_sidebar.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_ssh_args.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_ssh_compat.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_status.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_statusbar.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_system_deps.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_terminal_size.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_terminal_status.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_tmux_health.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_tmux_server.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_tmux_status.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_tool_panes.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_transcript.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_utf8_state.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_widgets.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tools/release_notes.py +0 -0
- {railmux-0.3.6.dev1 → railmux-0.3.6.dev2}/tools/wheel_smoke.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: railmux
|
|
3
|
-
Version: 0.3.6.
|
|
3
|
+
Version: 0.3.6.dev2
|
|
4
4
|
Summary: Terminal UI to navigate, resume, and start Claude Code and Codex sessions
|
|
5
5
|
Author-email: Rightglow <zhang.taian@foxmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -383,10 +383,9 @@ Scrolling a live agent does not enter Preview. Direct `railmux` uses the
|
|
|
383
383
|
terminal/tmux scrolling behavior; `railmux ssh` keeps its own bounded,
|
|
384
384
|
per-pane history and normally preserves the live terminal's captured styling.
|
|
385
385
|
When Railmux confirms that a running Codex session advanced after rewind or
|
|
386
|
-
steering, it
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
rollout so its abandoned suffix stays hidden.
|
|
386
|
+
steering, it leaves the live terminal untouched and advances only that pane's
|
|
387
|
+
SSH history generation. The SSH history for that branched generation uses the
|
|
388
|
+
canonical rollout so its abandoned suffix stays hidden.
|
|
390
389
|
|
|
391
390
|
Preview opens at the latest activity in `less`; large sessions are limited to
|
|
392
391
|
their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
|
|
@@ -445,7 +444,8 @@ Each opened agent runs in a detached tmux session, so switching sessions does
|
|
|
445
444
|
not interrupt it. To leave agents running when you quit Railmux, press `s` for
|
|
446
445
|
soft quit in the confirmation popup; restarting the same Railmux instance then
|
|
447
446
|
restores the usable workspace when those sessions are still available. A normal
|
|
448
|
-
quit
|
|
447
|
+
quit ends all running sessions instead and therefore requires a second
|
|
448
|
+
confirmation; `y` and Enter keep their existing confirm meaning in both steps.
|
|
449
449
|
|
|
450
450
|
All terminals attached to one managed Railmux window view the same UI process.
|
|
451
451
|
Soft Quit therefore closes that shared UI for every attached view, although it
|
|
@@ -341,10 +341,9 @@ Scrolling a live agent does not enter Preview. Direct `railmux` uses the
|
|
|
341
341
|
terminal/tmux scrolling behavior; `railmux ssh` keeps its own bounded,
|
|
342
342
|
per-pane history and normally preserves the live terminal's captured styling.
|
|
343
343
|
When Railmux confirms that a running Codex session advanced after rewind or
|
|
344
|
-
steering, it
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
rollout so its abandoned suffix stays hidden.
|
|
344
|
+
steering, it leaves the live terminal untouched and advances only that pane's
|
|
345
|
+
SSH history generation. The SSH history for that branched generation uses the
|
|
346
|
+
canonical rollout so its abandoned suffix stays hidden.
|
|
348
347
|
|
|
349
348
|
Preview opens at the latest activity in `less`; large sessions are limited to
|
|
350
349
|
their latest 2,000 saved records. Press `/` to search, `n`/`N` to move between
|
|
@@ -403,7 +402,8 @@ Each opened agent runs in a detached tmux session, so switching sessions does
|
|
|
403
402
|
not interrupt it. To leave agents running when you quit Railmux, press `s` for
|
|
404
403
|
soft quit in the confirmation popup; restarting the same Railmux instance then
|
|
405
404
|
restores the usable workspace when those sessions are still available. A normal
|
|
406
|
-
quit
|
|
405
|
+
quit ends all running sessions instead and therefore requires a second
|
|
406
|
+
confirmation; `y` and Enter keep their existing confirm meaning in both steps.
|
|
407
407
|
|
|
408
408
|
All terminals attached to one managed Railmux window view the same UI process.
|
|
409
409
|
Soft Quit therefore closes that shared UI for every attached view, although it
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.6.dev2"
|
|
@@ -109,6 +109,10 @@ class _PendingHistory:
|
|
|
109
109
|
pane_id: str
|
|
110
110
|
target_lines: int
|
|
111
111
|
requested_at: float
|
|
112
|
+
# A first wheel-up waits for one cumulative snapshot before freezing the
|
|
113
|
+
# viewport. This avoids exposing a short hot suffix whose later deep page
|
|
114
|
+
# may not share a stable anchor while the provider is actively repainting.
|
|
115
|
+
initial_offset: int | None = None
|
|
112
116
|
|
|
113
117
|
|
|
114
118
|
class LocalHistoryView:
|
|
@@ -353,6 +357,15 @@ class LocalHistoryView:
|
|
|
353
357
|
del self.content_cache[next(iter(self.content_cache))]
|
|
354
358
|
return stored
|
|
355
359
|
|
|
360
|
+
def _replace_content(self, snapshot: HistorySnapshot) -> HistorySnapshot:
|
|
361
|
+
"""Install one verified contiguous capture without timeline merging."""
|
|
362
|
+
assert snapshot.pane_id is not None
|
|
363
|
+
self.content_cache.pop(snapshot.pane_id, None)
|
|
364
|
+
self.content_cache[snapshot.pane_id] = snapshot
|
|
365
|
+
while len(self.content_cache) > _HISTORY_CONTENT_PANES:
|
|
366
|
+
del self.content_cache[next(iter(self.content_cache))]
|
|
367
|
+
return snapshot
|
|
368
|
+
|
|
356
369
|
def invalidate_routes(self) -> bool:
|
|
357
370
|
"""Drop pointer authority without discarding bounded pane content."""
|
|
358
371
|
was_active = self.cancel()
|
|
@@ -470,29 +483,29 @@ class LocalHistoryView:
|
|
|
470
483
|
cached = self.content_cache.get(route.pane_id, route)
|
|
471
484
|
if not self._same_geometry(cached, route):
|
|
472
485
|
cached = route
|
|
486
|
+
loaded_limit = min(len(cached.lines), self.history_limit)
|
|
487
|
+
target_lines = min(self.history_limit, _HISTORY_INITIAL_LINES)
|
|
488
|
+
if cached.more_available and loaded_limit < target_lines:
|
|
489
|
+
self.cancel_pane(route.pane_id)
|
|
490
|
+
request_id = self._allocate_request_id()
|
|
491
|
+
self._deep_pending[request_id] = _PendingHistory(
|
|
492
|
+
self.route_epoch,
|
|
493
|
+
route.pane_id,
|
|
494
|
+
target_lines,
|
|
495
|
+
time.monotonic() if now is None else now,
|
|
496
|
+
max(1, initial_offset),
|
|
497
|
+
)
|
|
498
|
+
self._deep_requests += 1
|
|
499
|
+
# Keep the live pane intact until one internally contiguous deep
|
|
500
|
+
# capture arrives. Entering history atomically from that response
|
|
501
|
+
# removes the hot/deep seam and needs no moving-live anchor.
|
|
502
|
+
return HistoryAction(
|
|
503
|
+
protocol_frame=encode_history_request(
|
|
504
|
+
request_id, event.x, event.y, target_lines
|
|
505
|
+
)
|
|
506
|
+
)
|
|
473
507
|
maximum = max(0, len(cached.lines) - cached.height)
|
|
474
508
|
if maximum == 0:
|
|
475
|
-
if cached.more_available:
|
|
476
|
-
self.cancel_pane(route.pane_id)
|
|
477
|
-
target_lines = min(self.history_limit, _HISTORY_INITIAL_LINES)
|
|
478
|
-
self.viewports[route.pane_id] = _HistoryViewport(
|
|
479
|
-
cached,
|
|
480
|
-
0,
|
|
481
|
-
min(len(cached.lines), self.history_limit),
|
|
482
|
-
)
|
|
483
|
-
request_id = self._allocate_request_id()
|
|
484
|
-
self._deep_pending[request_id] = _PendingHistory(
|
|
485
|
-
self.route_epoch,
|
|
486
|
-
route.pane_id,
|
|
487
|
-
target_lines,
|
|
488
|
-
time.monotonic() if now is None else now,
|
|
489
|
-
)
|
|
490
|
-
self._deep_requests += 1
|
|
491
|
-
return HistoryAction(
|
|
492
|
-
protocol_frame=encode_history_request(
|
|
493
|
-
request_id, event.x, event.y, target_lines
|
|
494
|
-
)
|
|
495
|
-
)
|
|
496
509
|
if cached.transcript_backed:
|
|
497
510
|
# Claude may not have written its first transcript record yet.
|
|
498
511
|
# This is a normal transient state, not a Railmux status event:
|
|
@@ -503,7 +516,6 @@ class LocalHistoryView:
|
|
|
503
516
|
forwarded_input=event.raw if cached.mouse_forwardable else b""
|
|
504
517
|
)
|
|
505
518
|
self.cancel_pane(route.pane_id)
|
|
506
|
-
loaded_limit = min(len(cached.lines), self.history_limit)
|
|
507
519
|
viewport = _HistoryViewport(
|
|
508
520
|
cached,
|
|
509
521
|
min(maximum, max(1, initial_offset)),
|
|
@@ -513,7 +525,6 @@ class LocalHistoryView:
|
|
|
513
525
|
),
|
|
514
526
|
)
|
|
515
527
|
self.viewports[route.pane_id] = viewport
|
|
516
|
-
target_lines = min(self.history_limit, _HISTORY_INITIAL_LINES)
|
|
517
528
|
if loaded_limit >= target_lines:
|
|
518
529
|
return HistoryAction(
|
|
519
530
|
protocol_frame=self._extend_history(viewport, now=now),
|
|
@@ -602,6 +613,45 @@ class LocalHistoryView:
|
|
|
602
613
|
if direction > 0 and route.history_choice_required:
|
|
603
614
|
return HistoryAction(claude_history_prompt=event.raw)
|
|
604
615
|
viewport = self.viewports.get(route.pane_id)
|
|
616
|
+
initial_request = next(
|
|
617
|
+
(
|
|
618
|
+
(request_id, pending)
|
|
619
|
+
for request_id, pending in self._deep_pending.items()
|
|
620
|
+
if pending.pane_id == route.pane_id
|
|
621
|
+
and pending.initial_offset is not None
|
|
622
|
+
),
|
|
623
|
+
None,
|
|
624
|
+
)
|
|
625
|
+
if initial_request is not None:
|
|
626
|
+
request_id, pending = initial_request
|
|
627
|
+
requested_at = time.monotonic() if now is None else now
|
|
628
|
+
if requested_at - pending.requested_at >= _HISTORY_DEEP_TIMEOUT:
|
|
629
|
+
self._deep_pending.pop(request_id, None)
|
|
630
|
+
self._timeouts += 1
|
|
631
|
+
if direction < 0:
|
|
632
|
+
return HistoryAction()
|
|
633
|
+
assert pending.initial_offset is not None
|
|
634
|
+
distance = min(
|
|
635
|
+
self.history_limit,
|
|
636
|
+
pending.initial_offset + max(1, distance),
|
|
637
|
+
)
|
|
638
|
+
initial_request = None
|
|
639
|
+
if viewport is None and initial_request is not None:
|
|
640
|
+
if direction < 0:
|
|
641
|
+
# The live pane never left the screen, so reversing direction
|
|
642
|
+
# simply cancels the pending entry. A late response is ignored.
|
|
643
|
+
self.cancel_pane(route.pane_id)
|
|
644
|
+
return HistoryAction()
|
|
645
|
+
request_id, pending = initial_request
|
|
646
|
+
assert pending.initial_offset is not None
|
|
647
|
+
self._deep_pending[request_id] = replace(
|
|
648
|
+
pending,
|
|
649
|
+
initial_offset=min(
|
|
650
|
+
self.history_limit,
|
|
651
|
+
pending.initial_offset + max(1, distance),
|
|
652
|
+
),
|
|
653
|
+
)
|
|
654
|
+
return HistoryAction()
|
|
605
655
|
if viewport is not None:
|
|
606
656
|
maximum = max(0, len(viewport.snapshot.lines) - viewport.snapshot.height)
|
|
607
657
|
viewport.offset = max(
|
|
@@ -828,8 +878,26 @@ class LocalHistoryView:
|
|
|
828
878
|
return HistoryAction()
|
|
829
879
|
viewport = self.viewports.get(pending.pane_id)
|
|
830
880
|
if viewport is None:
|
|
831
|
-
|
|
832
|
-
|
|
881
|
+
if pending.initial_offset is None:
|
|
882
|
+
self._remember_content(snapshot)
|
|
883
|
+
return HistoryAction()
|
|
884
|
+
maximum = max(0, len(snapshot.lines) - snapshot.height)
|
|
885
|
+
if maximum == 0:
|
|
886
|
+
self._remember_content(snapshot)
|
|
887
|
+
return HistoryAction()
|
|
888
|
+
stored = self._replace_content(snapshot)
|
|
889
|
+
maximum = max(0, len(stored.lines) - stored.height)
|
|
890
|
+
viewport = _HistoryViewport(
|
|
891
|
+
stored,
|
|
892
|
+
min(maximum, pending.initial_offset),
|
|
893
|
+
min(self.history_limit, max(pending.target_lines, len(stored.lines))),
|
|
894
|
+
exhausted=not snapshot.more_available,
|
|
895
|
+
)
|
|
896
|
+
self.viewports[pending.pane_id] = viewport
|
|
897
|
+
return HistoryAction(
|
|
898
|
+
protocol_frame=self._extend_history(viewport),
|
|
899
|
+
render_history=True,
|
|
900
|
+
)
|
|
833
901
|
maximum = max(0, len(snapshot.lines) - snapshot.height)
|
|
834
902
|
if maximum == 0:
|
|
835
903
|
self._remember_content(snapshot)
|
|
@@ -502,7 +502,6 @@ class SharedTmuxBindingManager:
|
|
|
502
502
|
)
|
|
503
503
|
break
|
|
504
504
|
return False
|
|
505
|
-
self._registered = True
|
|
506
505
|
self._prefix_tab_managed = state["prefix_tab_managed"]
|
|
507
506
|
self._right_click_managed = (
|
|
508
507
|
state["right_click_managed"])
|
|
@@ -514,6 +513,10 @@ class SharedTmuxBindingManager:
|
|
|
514
513
|
state["status_click_managed"])
|
|
515
514
|
self._termux_tap_managed = (
|
|
516
515
|
state["termux_tap_managed"])
|
|
516
|
+
# Publish registration only after every capability is
|
|
517
|
+
# final so concurrent readers cannot observe a torn
|
|
518
|
+
# lease during managed-Windows background setup.
|
|
519
|
+
self._registered = True
|
|
517
520
|
return True
|
|
518
521
|
|
|
519
522
|
backup = tmux_ctl.prepare_root_function_bindings()
|
|
@@ -662,13 +665,13 @@ class SharedTmuxBindingManager:
|
|
|
662
665
|
state["termux_tap_backup"], token=token)
|
|
663
666
|
self._remove_state()
|
|
664
667
|
return False
|
|
665
|
-
self._registered = True
|
|
666
668
|
self._prefix_tab_managed = state["prefix_tab_managed"]
|
|
667
669
|
self._right_click_managed = state["right_click_managed"]
|
|
668
670
|
self._selection_hook_managed = state["selection_hook_managed"]
|
|
669
671
|
self._selection_hook_index = state["selection_hook_index"]
|
|
670
672
|
self._status_click_managed = state["status_click_managed"]
|
|
671
673
|
self._termux_tap_managed = state["termux_tap_managed"]
|
|
674
|
+
self._registered = True
|
|
672
675
|
return True
|
|
673
676
|
except OSError:
|
|
674
677
|
return False
|
|
@@ -8,7 +8,6 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import os
|
|
10
10
|
import re
|
|
11
|
-
import signal
|
|
12
11
|
import shlex
|
|
13
12
|
import shutil
|
|
14
13
|
import subprocess
|
|
@@ -1840,7 +1839,13 @@ RAILMUX_HISTORY_GENERATION_OPTION = "@railmux_history_generation_v1"
|
|
|
1840
1839
|
# A confirmed branch transition prefixes the opaque SSH history cache epoch.
|
|
1841
1840
|
# Plain generations use native pane history; only this explicit marker permits
|
|
1842
1841
|
# the canonical transcript fallback that removes an abandoned rewind suffix.
|
|
1843
|
-
|
|
1842
|
+
# Only the evidence-gated v2 marker authorizes transcript-backed Codex
|
|
1843
|
+
# scrolling. The released v1 marker treated every live direct child rollout
|
|
1844
|
+
# as a rewind, but Codex can also create that shape while bootstrapping an
|
|
1845
|
+
# ordinary resume. Keeping the prefix versioned makes existing over-broad
|
|
1846
|
+
# markers fail back to raw pane history on upgrade.
|
|
1847
|
+
RAILMUX_CANONICAL_HISTORY_PREFIX = "canonical-v2:"
|
|
1848
|
+
RAILMUX_LEGACY_CANONICAL_HISTORY_PREFIX = "canonical:"
|
|
1844
1849
|
# tmux names function keys only through F12. Private restore routes use
|
|
1845
1850
|
# canonical xterm sequences in literal mode, so they cannot be confused with
|
|
1846
1851
|
# user input forwarded to an agent pane.
|
|
@@ -3330,45 +3335,6 @@ def exact_pane_alive(identity: PaneIdentity) -> bool:
|
|
|
3330
3335
|
)
|
|
3331
3336
|
|
|
3332
3337
|
|
|
3333
|
-
def reset_pane_view(identity: PaneIdentity) -> bool:
|
|
3334
|
-
"""Reset visible terminal state without deleting retained scrollback.
|
|
3335
|
-
|
|
3336
|
-
Revalidate the immutable session, pane process, and window before issuing
|
|
3337
|
-
tmux's terminal reset. Then send the pane's foreground process group an
|
|
3338
|
-
ordinary same-size ``SIGWINCH`` so its TUI repaints and reasserts terminal
|
|
3339
|
-
modes without a geometry change or injected keyboard input. The reset can
|
|
3340
|
-
push the old viewport into tmux history, but never destroys the retained
|
|
3341
|
-
prefix; managed history surfaces use the provider's canonical transcript.
|
|
3342
|
-
"""
|
|
3343
|
-
current = pane_identity(identity.pane_id)
|
|
3344
|
-
if (current is None or current.dead
|
|
3345
|
-
or current.pane_pid != identity.pane_pid
|
|
3346
|
-
or current.session_id != identity.session_id
|
|
3347
|
-
or current.window_id != identity.window_id):
|
|
3348
|
-
return False
|
|
3349
|
-
try:
|
|
3350
|
-
subprocess.check_call(
|
|
3351
|
-
["tmux", "send-keys", "-R", "-t", identity.pane_id],
|
|
3352
|
-
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
3353
|
-
)
|
|
3354
|
-
except (OSError, subprocess.CalledProcessError):
|
|
3355
|
-
return False
|
|
3356
|
-
|
|
3357
|
-
current = pane_identity(identity.pane_id)
|
|
3358
|
-
if (current is None or current.dead
|
|
3359
|
-
or current.pane_pid != identity.pane_pid
|
|
3360
|
-
or current.session_id != identity.session_id
|
|
3361
|
-
or current.window_id != identity.window_id):
|
|
3362
|
-
return True
|
|
3363
|
-
try:
|
|
3364
|
-
os.killpg(os.getpgid(current.pane_pid), signal.SIGWINCH)
|
|
3365
|
-
except (OSError, ValueError):
|
|
3366
|
-
# The presentation cache is already clean. A provider that exited or
|
|
3367
|
-
# cannot be signalled will repaint on its next ordinary terminal write.
|
|
3368
|
-
pass
|
|
3369
|
-
return True
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
3338
|
def kill_session_identity(identity: PaneIdentity) -> bool:
|
|
3373
3339
|
"""Kill an exact immutable tmux session, never a reused session name."""
|
|
3374
3340
|
current = pane_identity(identity.pane_id)
|