railmux 0.4.2.dev3__tar.gz → 0.4.2.dev5__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.4.2.dev3/src/railmux.egg-info → railmux-0.4.2.dev5}/PKG-INFO +1 -1
- railmux-0.4.2.dev5/src/railmux/__init__.py +1 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/fast_display_server.py +84 -3
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/models.py +6 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/session_cache.py +54 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/session_index.py +20 -6
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/tmux_server.py +66 -6
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/ui/app.py +82 -2
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5/src/railmux.egg-info}/PKG-INFO +1 -1
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux.egg-info/SOURCES.txt +1 -0
- railmux-0.4.2.dev5/tests/test_claude_continuation.py +177 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_fast_display_server.py +79 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_tmux_server.py +60 -0
- railmux-0.4.2.dev3/src/railmux/__init__.py +0 -1
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/LICENSE +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/MANIFEST.in +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/README.md +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/pyproject.toml +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/setup.cfg +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/__main__.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/atomic_file.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/background_index.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/cli.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/codex_index.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/config.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/config_cli.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/diagnostic_contract.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/diagnostics.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/discovery.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/display_transport.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/entrypoint.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/fast_display_client.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/favorites.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/fuzzy.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/help_workspace.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/image_paste.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/launcher.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/local_open.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/modes.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/pane_surface.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/path_codec.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/preview_pager.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/project_favorites.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/provider_paths.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/release_version.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/remote_config.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/renames.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/restart_state.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/runtime_config.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/self_update.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/session_lease.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/settings.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/ssh_args.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/ssh_preflight.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/system_deps.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/terminal_screen.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/terminal_status.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/tmux_capabilities.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/tmux_ctl.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/tmux_health.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/tool_panes.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/transcript.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/ui/modals.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/windows_attach_relay.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/windows_bootstrap.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/windows_install_log.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/windows_msys2.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/windows_pacman.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/windows_paths.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux/windows_ui_transition.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/app_test_harness.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_agent_workspace.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_app_codex_placeholder.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_app_quit.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_app_restart_restore.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_app_running_recovery.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_atomic_file.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_background_index.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_cli.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_code_map.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_codex_delete.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_codex_index.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_codex_lineage.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_codex_transcript.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_config.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_config_cli.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_diagnostics.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_discovery.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_display_transport.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_fast_display_bootstrap.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_fast_display_history.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_fast_display_protocol.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_fast_display_selection.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_fast_display_terminal.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_focus_highlight.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_fuzzy.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_help_agent.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_help_workspace.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_history_cleanup.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_image_paste.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_keymap.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_launcher.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_layout_profile.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_local_clipboard.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_local_open.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_modals.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_mode_selection.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_modes.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_mouse_manager.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_new_project.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_options.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_pane_surface.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_path_codec.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_preview_pager.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_project_favorites.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_projects_pane.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_provider_paths.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_release_notes.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_release_version.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_release_workflow.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_remote_config.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_renames.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_restart_state.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_running_pane.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_running_sort.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_runtime_config.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_scroll_agent.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_scroll_manager.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_selection_isolation.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_self_update.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_session_cache.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_session_index.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_session_lease.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_sessions_pane.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_setting_contracts.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_settings.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_sidebar.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_ssh_args.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_ssh_compat.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_status.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_statusbar.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_system_deps.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_terminal_size.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_terminal_status.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_tmux_capabilities.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_tmux_ctl.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_tmux_health.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_tmux_integration.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_tmux_status.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_tool_panes.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_transcript.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_utf8_state.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_widgets.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_windows_attach_relay.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_windows_bootstrap.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_windows_install_log.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_windows_msys2.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_windows_pacman.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_windows_paths.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_windows_synchronized_screen.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_windows_ui_transition.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_windows_wrapper_contract.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tools/release_notes.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tools/wheel_smoke.py +0 -0
- {railmux-0.4.2.dev3 → railmux-0.4.2.dev5}/tools/windows_archive_smoke.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.2.dev5"
|
|
@@ -1747,6 +1747,83 @@ def _transcript_rows(
|
|
|
1747
1747
|
os.close(fd)
|
|
1748
1748
|
|
|
1749
1749
|
|
|
1750
|
+
_OVERLAP_STYLE_RE = re.compile(rb"\x1b\[[0-9;]*m")
|
|
1751
|
+
_OVERLAP_SPACE_RE = re.compile(r"\s+")
|
|
1752
|
+
_OVERLAP_ANCHOR_CHARS = 12
|
|
1753
|
+
_OVERLAP_SINGLE_ANCHOR_CHARS = 24
|
|
1754
|
+
_OVERLAP_SEARCH_SCREENS = 4
|
|
1755
|
+
_OVERLAP_MIN_SEARCH_ROWS = 200
|
|
1756
|
+
|
|
1757
|
+
|
|
1758
|
+
def _overlap_text(row: bytes) -> str:
|
|
1759
|
+
"""Return a row's visible text without styling or any whitespace.
|
|
1760
|
+
|
|
1761
|
+
The transcript and the provider wrap and indent the same message
|
|
1762
|
+
differently, so only the whitespace-free character stream is comparable.
|
|
1763
|
+
"""
|
|
1764
|
+
text = _OVERLAP_STYLE_RE.sub(b"", row).decode("utf-8", errors="replace")
|
|
1765
|
+
return _OVERLAP_SPACE_RE.sub("", text)
|
|
1766
|
+
|
|
1767
|
+
|
|
1768
|
+
def _transcript_rows_before_screen(
|
|
1769
|
+
transcript_rows: Sequence[bytes],
|
|
1770
|
+
screen_rows: Sequence[bytes],
|
|
1771
|
+
) -> int:
|
|
1772
|
+
"""Return how many transcript rows precede the live screen's content.
|
|
1773
|
+
|
|
1774
|
+
Transcript-backed history appends the live viewport after the transcript,
|
|
1775
|
+
whose tail usually holds the same newest message. Locate the screen's
|
|
1776
|
+
text in the transcript tail through distinctive anchor rows and cut the
|
|
1777
|
+
transcript where the screen begins. Two anchors must agree on that start
|
|
1778
|
+
unless one long anchor is the only candidate; without a credible match
|
|
1779
|
+
every transcript row is kept.
|
|
1780
|
+
"""
|
|
1781
|
+
total = len(transcript_rows)
|
|
1782
|
+
window = max(
|
|
1783
|
+
_OVERLAP_MIN_SEARCH_ROWS, _OVERLAP_SEARCH_SCREENS * len(screen_rows))
|
|
1784
|
+
first = max(0, total - window)
|
|
1785
|
+
row_starts: list[int] = []
|
|
1786
|
+
parts: list[str] = []
|
|
1787
|
+
offset = 0
|
|
1788
|
+
for row in transcript_rows[first:]:
|
|
1789
|
+
row_starts.append(offset)
|
|
1790
|
+
text = _overlap_text(row)
|
|
1791
|
+
parts.append(text)
|
|
1792
|
+
offset += len(text)
|
|
1793
|
+
haystack = "".join(parts)
|
|
1794
|
+
screen_offset = 0
|
|
1795
|
+
starts: list[tuple[int, int]] = []
|
|
1796
|
+
for row in screen_rows:
|
|
1797
|
+
text = _overlap_text(row)
|
|
1798
|
+
if len(text) >= _OVERLAP_ANCHOR_CHARS and haystack.count(text) == 1:
|
|
1799
|
+
starts.append((haystack.index(text) - screen_offset, len(text)))
|
|
1800
|
+
if len(starts) == 2:
|
|
1801
|
+
break
|
|
1802
|
+
screen_offset += len(text)
|
|
1803
|
+
if not starts:
|
|
1804
|
+
return total
|
|
1805
|
+
if len(starts) == 1:
|
|
1806
|
+
if starts[0][1] < _OVERLAP_SINGLE_ANCHOR_CHARS:
|
|
1807
|
+
return total
|
|
1808
|
+
else:
|
|
1809
|
+
# Allow small rendering differences (list numbering, bullets).
|
|
1810
|
+
tolerance = 16 + abs(starts[0][0]) // 20
|
|
1811
|
+
if abs(starts[0][0] - starts[1][0]) > tolerance:
|
|
1812
|
+
return total
|
|
1813
|
+
start = max(0, starts[0][0])
|
|
1814
|
+
# Keep a row that begins before the screen: it may carry text the screen
|
|
1815
|
+
# no longer shows, and a partial duplicate is better than a loss.
|
|
1816
|
+
keep = len(row_starts)
|
|
1817
|
+
for index, row_start in enumerate(row_starts):
|
|
1818
|
+
if row_start >= start:
|
|
1819
|
+
keep = index
|
|
1820
|
+
break
|
|
1821
|
+
# Drop trailing blank rows so the screen is not preceded by a gap.
|
|
1822
|
+
while keep and not parts[keep - 1]:
|
|
1823
|
+
keep -= 1
|
|
1824
|
+
return first + keep
|
|
1825
|
+
|
|
1826
|
+
|
|
1750
1827
|
def _capture_pane_history(
|
|
1751
1828
|
pyte: object,
|
|
1752
1829
|
pane: _PaneGeometry,
|
|
@@ -1852,14 +1929,18 @@ def _capture_pane_history(
|
|
|
1852
1929
|
transcript_used = transcript_entry is not None and bool(transcript_entry.rows)
|
|
1853
1930
|
if transcript_used:
|
|
1854
1931
|
assert transcript_entry is not None
|
|
1855
|
-
|
|
1932
|
+
kept = _transcript_rows_before_screen(transcript_entry.rows, current_raw)
|
|
1933
|
+
trimmed = len(transcript_entry.rows) - kept
|
|
1934
|
+
transcript_lines = transcript_entry.rows[:kept]
|
|
1856
1935
|
history_count = max(0, max_lines - pane.height)
|
|
1857
1936
|
source_lines = (
|
|
1858
1937
|
transcript_lines[-history_count:] if history_count else ()
|
|
1859
1938
|
) + current_raw
|
|
1860
1939
|
timeline_end = max(
|
|
1861
1940
|
pane.height,
|
|
1862
|
-
(transcript_entry.total_rows or len(transcript_entry.rows))
|
|
1941
|
+
(transcript_entry.total_rows or len(transcript_entry.rows))
|
|
1942
|
+
- trimmed
|
|
1943
|
+
+ pane.height,
|
|
1863
1944
|
)
|
|
1864
1945
|
else:
|
|
1865
1946
|
source_lines = rendered_raw
|
|
@@ -1910,7 +1991,7 @@ def _capture_pane_history(
|
|
|
1910
1991
|
budget_truncated
|
|
1911
1992
|
or (
|
|
1912
1993
|
transcript_entry.more_available
|
|
1913
|
-
or len(
|
|
1994
|
+
or len(transcript_lines) + pane.height > len(lines)
|
|
1914
1995
|
if transcript_used and transcript_entry is not None
|
|
1915
1996
|
else pane.history_size + pane.height > len(lines)
|
|
1916
1997
|
)
|
|
@@ -84,6 +84,12 @@ class SessionMeta:
|
|
|
84
84
|
# A Railmux lease observed in the same shared provider history root. This
|
|
85
85
|
# is a read-time UI overlay, never provider transcript metadata.
|
|
86
86
|
remote_owner: str | None = None
|
|
87
|
+
# Successor named by a final Claude ``continued-in`` record: the live
|
|
88
|
+
# conversation moved to that session file. Always None for Codex.
|
|
89
|
+
continued_in_id: str | None = None
|
|
90
|
+
# Claude Code background-job (``sessionKind: bg``) session. Listings hide
|
|
91
|
+
# these unless one is the ``continued-in`` successor of a visible session.
|
|
92
|
+
background: bool = False
|
|
87
93
|
|
|
88
94
|
@property
|
|
89
95
|
def display_title(self) -> str:
|
|
@@ -33,6 +33,9 @@ class SessionCache:
|
|
|
33
33
|
# User-assigned titles, overlaid at read time so they survive Claude
|
|
34
34
|
# Code rewriting its own ai-title record every turn.
|
|
35
35
|
self._renames = renames
|
|
36
|
+
# Claude ``continued-in`` links seen by listings: predecessor id ->
|
|
37
|
+
# successor id. The App uses them to move live bindings and favorites.
|
|
38
|
+
self.continuations: dict[str, str] = {}
|
|
36
39
|
|
|
37
40
|
def list_sessions(
|
|
38
41
|
self, project: Project, top_n: int = _DEFAULT_TOP_N
|
|
@@ -86,9 +89,60 @@ class SessionCache:
|
|
|
86
89
|
if stale.parent == project.claude_dir and stale not in current_paths:
|
|
87
90
|
del self._entries[stale]
|
|
88
91
|
|
|
92
|
+
results = self._hide_continued(project, results)
|
|
89
93
|
results.sort(key=lambda s: s.last_mtime, reverse=True)
|
|
90
94
|
return results
|
|
91
95
|
|
|
96
|
+
def _hide_continued(
|
|
97
|
+
self, project: Project, results: list[SessionMeta]
|
|
98
|
+
) -> list[SessionMeta]:
|
|
99
|
+
"""Drop background sessions and sessions continued in a successor.
|
|
100
|
+
|
|
101
|
+
Claude Code copies the whole conversation into the successor, so the
|
|
102
|
+
predecessor is a stale duplicate. A user rename moves forward with it.
|
|
103
|
+
A background successor of a visible session is the same interactive
|
|
104
|
+
conversation now hosted by Claude's daemon, so it stays visible.
|
|
105
|
+
"""
|
|
106
|
+
by_id = {meta.session_id: meta for meta in results}
|
|
107
|
+
visible = {meta.session_id for meta in results if not meta.background}
|
|
108
|
+
pending = list(visible)
|
|
109
|
+
while pending:
|
|
110
|
+
successor = by_id[pending.pop()].continued_in_id
|
|
111
|
+
if successor in by_id and successor not in visible:
|
|
112
|
+
visible.add(successor)
|
|
113
|
+
pending.append(successor)
|
|
114
|
+
results = [meta for meta in results if meta.session_id in visible]
|
|
115
|
+
hidden: dict[str, str] = {}
|
|
116
|
+
for meta in results:
|
|
117
|
+
successor = meta.continued_in_id
|
|
118
|
+
if (
|
|
119
|
+
successor is not None
|
|
120
|
+
and successor != meta.session_id
|
|
121
|
+
and (project.claude_dir / f"{successor}.jsonl").is_file()
|
|
122
|
+
):
|
|
123
|
+
hidden[meta.session_id] = successor
|
|
124
|
+
if not hidden:
|
|
125
|
+
return results
|
|
126
|
+
self.continuations.update(hidden)
|
|
127
|
+
carried: set[str] = set()
|
|
128
|
+
if self._renames is not None:
|
|
129
|
+
# Walk each chain forward so A -> B -> C carries A's rename to C.
|
|
130
|
+
for _ in range(len(hidden)):
|
|
131
|
+
changed = False
|
|
132
|
+
for predecessor, successor in hidden.items():
|
|
133
|
+
title = self._renames.get(predecessor)
|
|
134
|
+
if title and not self._renames.get(successor):
|
|
135
|
+
self._renames.set(successor, title)
|
|
136
|
+
carried.add(successor)
|
|
137
|
+
changed = True
|
|
138
|
+
if not changed:
|
|
139
|
+
break
|
|
140
|
+
return [
|
|
141
|
+
self._with_override(meta) if meta.session_id in carried else meta
|
|
142
|
+
for meta in results
|
|
143
|
+
if meta.session_id not in hidden
|
|
144
|
+
]
|
|
145
|
+
|
|
92
146
|
def get(self, project: Project, session_id: str) -> SessionMeta | None:
|
|
93
147
|
"""Cache-backed lookup of a single session's metadata by id.
|
|
94
148
|
|
|
@@ -43,6 +43,7 @@ class _SessionScanState:
|
|
|
43
43
|
last_rtype: str = ""
|
|
44
44
|
last_stop_reason: str = ""
|
|
45
45
|
background: bool = False
|
|
46
|
+
continued_in: str | None = None
|
|
46
47
|
|
|
47
48
|
def clone(self) -> _SessionScanState:
|
|
48
49
|
return _SessionScanState(
|
|
@@ -62,6 +63,7 @@ class _SessionScanState:
|
|
|
62
63
|
last_rtype=self.last_rtype,
|
|
63
64
|
last_stop_reason=self.last_stop_reason,
|
|
64
65
|
background=self.background,
|
|
66
|
+
continued_in=self.continued_in,
|
|
65
67
|
)
|
|
66
68
|
|
|
67
69
|
|
|
@@ -169,14 +171,25 @@ def _can_resume(fd: int, st: os.stat_result, state: _SessionScanState) -> bool:
|
|
|
169
171
|
|
|
170
172
|
|
|
171
173
|
def _consume_record(state: _SessionScanState, rec: object) -> None:
|
|
172
|
-
if not isinstance(rec, dict)
|
|
174
|
+
if not isinstance(rec, dict):
|
|
173
175
|
return
|
|
174
176
|
# Background-job sessions are not interactive — they can't be resumed in
|
|
175
|
-
# a terminal and shouldn't appear in the sidebar.
|
|
177
|
+
# a terminal and shouldn't appear in the sidebar. Their metadata is still
|
|
178
|
+
# collected because Claude Code also hosts a live conversation that was
|
|
179
|
+
# ``continued-in`` a daemon session this way; listings decide visibility.
|
|
176
180
|
if rec.get("sessionKind") == "bg":
|
|
177
181
|
state.background = True
|
|
178
|
-
return
|
|
179
182
|
rtype = rec.get("type")
|
|
183
|
+
# Claude Code ends a session file with ``continued-in`` when the live
|
|
184
|
+
# conversation moves to a new session. Only a final record counts.
|
|
185
|
+
successor = rec.get("continuedInSessionId")
|
|
186
|
+
state.continued_in = (
|
|
187
|
+
successor
|
|
188
|
+
if rtype == "continued-in"
|
|
189
|
+
and isinstance(successor, str)
|
|
190
|
+
and _looks_like_uuid(successor)
|
|
191
|
+
else None
|
|
192
|
+
)
|
|
180
193
|
if rtype == "ai-title":
|
|
181
194
|
state.title = rec.get("aiTitle") or state.title
|
|
182
195
|
# NOTE: "last-prompt" is deliberately NOT treated as a turn. Claude Code
|
|
@@ -235,8 +248,6 @@ def _meta_from_state(
|
|
|
235
248
|
state: _SessionScanState,
|
|
236
249
|
st: os.stat_result,
|
|
237
250
|
) -> SessionMeta | None:
|
|
238
|
-
if state.background:
|
|
239
|
-
return None
|
|
240
251
|
assistant_count = len(state.assistant_token_totals)
|
|
241
252
|
message_count = state.user_count + assistant_count
|
|
242
253
|
if message_count == 0 or (state.user_count > 0 and assistant_count == 0):
|
|
@@ -277,6 +288,8 @@ def _meta_from_state(
|
|
|
277
288
|
last_user_message=preview,
|
|
278
289
|
status=status,
|
|
279
290
|
pending_tool=pending_tool,
|
|
291
|
+
continued_in_id=state.continued_in,
|
|
292
|
+
background=state.background,
|
|
280
293
|
)
|
|
281
294
|
|
|
282
295
|
|
|
@@ -352,7 +365,8 @@ def _scan_session_incremental(
|
|
|
352
365
|
|
|
353
366
|
|
|
354
367
|
def _scan_session(project: Project, jsonl_path: Path) -> SessionMeta | None:
|
|
355
|
-
|
|
368
|
+
meta = _scan_session_incremental(project, jsonl_path).meta
|
|
369
|
+
return None if meta is not None and meta.background else meta
|
|
356
370
|
|
|
357
371
|
|
|
358
372
|
def _looks_like_uuid(s: str) -> bool:
|
|
@@ -22,6 +22,8 @@ _SESSION_ID_RE = re.compile(r"[A-Za-z0-9-]{1,256}\Z")
|
|
|
22
22
|
_HISTORY_SOURCE_SCHEMA = 1
|
|
23
23
|
_TRANSCRIPT_SOURCE_SCHEMA = 1
|
|
24
24
|
_MAX_TRANSCRIPT_SOURCE_BYTES = 8192
|
|
25
|
+
_CONTINUATION_TAIL_BYTES = 4096
|
|
26
|
+
_MAX_CONTINUATION_HOPS = 8
|
|
25
27
|
_DEFAULT_DISCOVERY_TIMEOUT = 2.0
|
|
26
28
|
_WINDOWS_DISCOVERY_TIMEOUT = 5.0
|
|
27
29
|
|
|
@@ -415,11 +417,8 @@ def decode_transcript_source(raw: str) -> TranscriptSource | None:
|
|
|
415
417
|
return TranscriptSource(provider, session_id, path)
|
|
416
418
|
|
|
417
419
|
|
|
418
|
-
def
|
|
419
|
-
"""Open
|
|
420
|
-
source = decode_transcript_source(raw)
|
|
421
|
-
if source is None:
|
|
422
|
-
return None
|
|
420
|
+
def _open_owned_regular_file(path: Path) -> int | None:
|
|
421
|
+
"""Open a same-user regular file without following the final symlink."""
|
|
423
422
|
flags = os.O_RDONLY
|
|
424
423
|
if hasattr(os, "O_CLOEXEC"):
|
|
425
424
|
flags |= os.O_CLOEXEC
|
|
@@ -427,7 +426,7 @@ def open_transcript_source(raw: str) -> tuple[TranscriptSource, int] | None:
|
|
|
427
426
|
flags |= os.O_NOFOLLOW
|
|
428
427
|
fd = -1
|
|
429
428
|
try:
|
|
430
|
-
fd = os.open(
|
|
429
|
+
fd = os.open(path, flags)
|
|
431
430
|
info = os.fstat(fd)
|
|
432
431
|
except OSError:
|
|
433
432
|
if fd >= 0:
|
|
@@ -443,6 +442,67 @@ def open_transcript_source(raw: str) -> tuple[TranscriptSource, int] | None:
|
|
|
443
442
|
):
|
|
444
443
|
os.close(fd)
|
|
445
444
|
return None
|
|
445
|
+
return fd
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _claude_continuation_id(fd: int) -> str | None:
|
|
449
|
+
"""Return the successor session named by a final ``continued-in`` record.
|
|
450
|
+
|
|
451
|
+
Claude Code can move a live conversation to a new session file, copying
|
|
452
|
+
the full history and ending the old file with one ``continued-in``
|
|
453
|
+
record. Only that exact final record is trusted.
|
|
454
|
+
"""
|
|
455
|
+
try:
|
|
456
|
+
size = os.fstat(fd).st_size
|
|
457
|
+
tail = os.pread(
|
|
458
|
+
fd,
|
|
459
|
+
min(size, _CONTINUATION_TAIL_BYTES),
|
|
460
|
+
max(0, size - _CONTINUATION_TAIL_BYTES),
|
|
461
|
+
)
|
|
462
|
+
except OSError:
|
|
463
|
+
return None
|
|
464
|
+
lines = tail.rstrip(b"\n").rsplit(b"\n", 1)
|
|
465
|
+
if b"continued-in" not in lines[-1]:
|
|
466
|
+
return None
|
|
467
|
+
try:
|
|
468
|
+
record = json.loads(lines[-1])
|
|
469
|
+
except (UnicodeDecodeError, ValueError):
|
|
470
|
+
return None
|
|
471
|
+
if not isinstance(record, dict) or record.get("type") != "continued-in":
|
|
472
|
+
return None
|
|
473
|
+
successor = record.get("continuedInSessionId")
|
|
474
|
+
if not isinstance(successor, str) or not _SESSION_ID_RE.fullmatch(successor):
|
|
475
|
+
return None
|
|
476
|
+
return successor
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def open_transcript_source(raw: str) -> tuple[TranscriptSource, int] | None:
|
|
480
|
+
"""Open an exact same-user transcript without following the final symlink.
|
|
481
|
+
|
|
482
|
+
A Claude transcript that was continued in another session resolves to the
|
|
483
|
+
newest same-directory successor, so history keeps following the live pane.
|
|
484
|
+
"""
|
|
485
|
+
source = decode_transcript_source(raw)
|
|
486
|
+
if source is None:
|
|
487
|
+
return None
|
|
488
|
+
fd = _open_owned_regular_file(source.path)
|
|
489
|
+
if fd is None:
|
|
490
|
+
return None
|
|
491
|
+
if source.provider != "claude":
|
|
492
|
+
return source, fd
|
|
493
|
+
seen = {source.session_id}
|
|
494
|
+
for _ in range(_MAX_CONTINUATION_HOPS):
|
|
495
|
+
successor = _claude_continuation_id(fd)
|
|
496
|
+
if successor is None or successor in seen:
|
|
497
|
+
break
|
|
498
|
+
path = source.path.with_name(f"{successor}.jsonl")
|
|
499
|
+
next_fd = _open_owned_regular_file(path)
|
|
500
|
+
if next_fd is None:
|
|
501
|
+
break
|
|
502
|
+
os.close(fd)
|
|
503
|
+
fd = next_fd
|
|
504
|
+
source = TranscriptSource(source.provider, successor, path)
|
|
505
|
+
seen.add(successor)
|
|
446
506
|
return source, fd
|
|
447
507
|
|
|
448
508
|
|
|
@@ -426,6 +426,7 @@ def _session_lease_failure_description(category: str | None) -> str:
|
|
|
426
426
|
# Unlike the short-lived runtime state file, a session option lives exactly as
|
|
427
427
|
# long as the tmux session and survives Railmux restarts without renaming it.
|
|
428
428
|
_SESSION_BINDING_OPTION = "@railmux_binding_v1"
|
|
429
|
+
_CLAUDE_CONTINUATION_MAX_HOPS = 8
|
|
429
430
|
_HELP_SESSION_OPTION = "@railmux_help_v1"
|
|
430
431
|
_HELP_POLICY_VERSION = "read-only-auto-v2"
|
|
431
432
|
|
|
@@ -9194,6 +9195,7 @@ class App:
|
|
|
9194
9195
|
# stays stuck True, defeating the 3s project-scan cache. Codex
|
|
9195
9196
|
# resolution must run too or neither ever clears.
|
|
9196
9197
|
self._resolve_placeholders(projects, server)
|
|
9198
|
+
self._follow_claude_continuations()
|
|
9197
9199
|
running_ids = self._running_session_ids()
|
|
9198
9200
|
if self._selected_project is not None:
|
|
9199
9201
|
matched = next((p for p in projects if p.encoded_name == self._selected_project.encoded_name), None)
|
|
@@ -10228,6 +10230,83 @@ class App:
|
|
|
10228
10230
|
# activity without enabling per-poll row movement.
|
|
10229
10231
|
self._running_sort_ts = 0.0
|
|
10230
10232
|
|
|
10233
|
+
def _follow_claude_continuations(self) -> None:
|
|
10234
|
+
"""Move live Claude bindings and favorites to ``continued-in`` successors.
|
|
10235
|
+
|
|
10236
|
+
Claude Code can continue a live conversation in a new session file and
|
|
10237
|
+
end the old file with one ``continued-in`` record. The pane is the
|
|
10238
|
+
same, so re-key its running entry to the newest successor: the binding,
|
|
10239
|
+
Preview marker, lease and Sessions row then follow the live writer.
|
|
10240
|
+
"""
|
|
10241
|
+
cache = getattr(self, "_session_cache", None)
|
|
10242
|
+
if cache is None:
|
|
10243
|
+
return
|
|
10244
|
+
continuations = getattr(cache, "continuations", {})
|
|
10245
|
+
if continuations and getattr(self, "_favorites", None) is not None:
|
|
10246
|
+
favorites = self._favorites.get_ids()
|
|
10247
|
+
carried: set[str] = set()
|
|
10248
|
+
for predecessor in favorites & continuations.keys():
|
|
10249
|
+
successor = continuations[predecessor]
|
|
10250
|
+
for _ in range(_CLAUDE_CONTINUATION_MAX_HOPS):
|
|
10251
|
+
if successor not in continuations:
|
|
10252
|
+
break
|
|
10253
|
+
successor = continuations[successor]
|
|
10254
|
+
if successor not in favorites:
|
|
10255
|
+
carried.add(successor)
|
|
10256
|
+
if carried:
|
|
10257
|
+
self._favorites.set_many(carried, True)
|
|
10258
|
+
claimed = self._running_session_ids() | set(self._running)
|
|
10259
|
+
for r in list(self._running.values()):
|
|
10260
|
+
registered_mode = self._modes().for_tmux_name(r.tmux_name)
|
|
10261
|
+
session_type = (
|
|
10262
|
+
registered_mode.session_type if registered_mode else r.session_type)
|
|
10263
|
+
if (
|
|
10264
|
+
session_type != "claude"
|
|
10265
|
+
or r.is_placeholder
|
|
10266
|
+
or r.is_legacy
|
|
10267
|
+
or r.project is None
|
|
10268
|
+
):
|
|
10269
|
+
continue
|
|
10270
|
+
meta = cache.get(r.project, r.key)
|
|
10271
|
+
successor_meta = None
|
|
10272
|
+
seen = {r.key}
|
|
10273
|
+
while (
|
|
10274
|
+
meta is not None
|
|
10275
|
+
and meta.continued_in_id is not None
|
|
10276
|
+
and meta.continued_in_id not in seen
|
|
10277
|
+
and len(seen) <= _CLAUDE_CONTINUATION_MAX_HOPS
|
|
10278
|
+
):
|
|
10279
|
+
seen.add(meta.continued_in_id)
|
|
10280
|
+
meta = cache.get(r.project, meta.continued_in_id)
|
|
10281
|
+
if meta is not None:
|
|
10282
|
+
successor_meta = meta
|
|
10283
|
+
if successor_meta is None:
|
|
10284
|
+
continue
|
|
10285
|
+
successor = successor_meta.session_id
|
|
10286
|
+
if successor in claimed:
|
|
10287
|
+
continue
|
|
10288
|
+
if r.orphan is not None:
|
|
10289
|
+
resolved_marker = r.orphan.resolved(successor)
|
|
10290
|
+
if not self._write_orphan_marker(resolved_marker):
|
|
10291
|
+
continue
|
|
10292
|
+
r.orphan = resolved_marker
|
|
10293
|
+
del self._running[r.key]
|
|
10294
|
+
r.key = successor
|
|
10295
|
+
r.provider_session_id = None
|
|
10296
|
+
r.label = f"{r.project.display_name}/{successor_meta.display_title}"
|
|
10297
|
+
r.status = successor_meta.status
|
|
10298
|
+
r.last_mtime = successor_meta.last_mtime
|
|
10299
|
+
r.attention = successor_meta.attention
|
|
10300
|
+
self._running[successor] = r
|
|
10301
|
+
self._stamp_running(r)
|
|
10302
|
+
self._record_session_lease_result(
|
|
10303
|
+
r, self._ensure_running_session_lease(r, successor_meta))
|
|
10304
|
+
claimed.add(successor)
|
|
10305
|
+
displayed_slot = self._agent_workspace().slot_for_agent(r.tmux_name)
|
|
10306
|
+
if displayed_slot is not None:
|
|
10307
|
+
self._set_slot_active_target(
|
|
10308
|
+
displayed_slot, successor, r.tmux_name)
|
|
10309
|
+
|
|
10231
10310
|
def _correlate_codex_rollout(self, r: "_Running") -> set[str] | None:
|
|
10232
10311
|
"""Exact child→rollout correlation for a Codex placeholder (#12).
|
|
10233
10312
|
|
|
@@ -10284,11 +10363,12 @@ class App:
|
|
|
10284
10363
|
return self._codex_representative(session_id)
|
|
10285
10364
|
if project is None:
|
|
10286
10365
|
return None
|
|
10287
|
-
from railmux.session_index import
|
|
10366
|
+
from railmux.session_index import _scan_session_incremental
|
|
10288
10367
|
jsonl_path = project.claude_dir / f"{session_id}.jsonl"
|
|
10289
10368
|
if not jsonl_path.exists():
|
|
10290
10369
|
return None
|
|
10291
|
-
|
|
10370
|
+
# An explicit id may name a background ``continued-in`` successor.
|
|
10371
|
+
return _scan_session_incremental(project, jsonl_path).meta
|
|
10292
10372
|
|
|
10293
10373
|
# --- kill / delete session ---
|
|
10294
10374
|
|