railmux 0.4.2.dev5__tar.gz → 0.4.2.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.4.2.dev5/src/railmux.egg-info → railmux-0.4.2.dev6}/PKG-INFO +1 -1
- railmux-0.4.2.dev6/src/railmux/__init__.py +1 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/fast_display_server.py +42 -21
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/transcript.py +272 -43
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6/src/railmux.egg-info}/PKG-INFO +1 -1
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_fast_display_server.py +32 -4
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_transcript.py +76 -0
- railmux-0.4.2.dev5/src/railmux/__init__.py +0 -1
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/LICENSE +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/MANIFEST.in +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/README.md +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/pyproject.toml +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/setup.cfg +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/__main__.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/atomic_file.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/background_index.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/cli.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/codex_index.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/config.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/config_cli.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/diagnostic_contract.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/diagnostics.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/discovery.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/display_transport.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/entrypoint.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/fast_display_client.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/fast_display_history.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/fast_display_input.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/fast_display_protocol.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/favorites.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/fuzzy.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/help_workspace.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/image_paste.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/launcher.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/legacy_sessions.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/local_clipboard.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/local_open.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/models.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/modes.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/mouse_manager.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/orphan_marker.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/pane_surface.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/path_codec.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/preview_pager.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/project_favorites.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/provider_paths.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/release_version.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/remote_config.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/renames.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/restart_state.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/runtime_config.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/scroll_agent.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/scroll_manager.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/selection_isolation.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/self_update.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/session_cache.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/session_index.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/session_lease.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/setting_contracts.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/settings.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/ssh_args.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/ssh_compat.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/ssh_display_diagnostics.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/ssh_doctor.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/ssh_preflight.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/system_deps.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/terminal_screen.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/terminal_status.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/tmux_binding_manager.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/tmux_capabilities.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/tmux_ctl.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/tmux_health.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/tmux_server.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/tool_panes.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/ui/__init__.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/ui/_widgets.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/ui/app.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/ui/keymap.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/ui/modals.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/ui/projects_pane.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/ui/running_pane.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/ui/sessions_pane.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/ui/sidebar.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/ui/statusbar.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/ui/workspace.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/windows_attach_relay.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/windows_bootstrap.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/windows_install_log.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/windows_msys2.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/windows_pacman.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/windows_paths.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux/windows_ui_transition.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux.egg-info/SOURCES.txt +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux.egg-info/dependency_links.txt +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux.egg-info/entry_points.txt +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux.egg-info/requires.txt +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/src/railmux.egg-info/top_level.txt +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/app_test_harness.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_agent_workspace.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_app_codex_placeholder.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_app_quit.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_app_restart_restore.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_app_running_recovery.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_atomic_file.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_background_index.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_claude_continuation.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_cli.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_code_map.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_codex_delete.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_codex_index.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_codex_lineage.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_codex_transcript.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_config.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_config_cli.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_denested_transport_benchmark.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_diagnostics.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_discovery.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_display_transport.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_fast_display_bootstrap.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_fast_display_history.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_fast_display_protocol.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_fast_display_selection.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_fast_display_terminal.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_focus_highlight.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_fullscreen_binding.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_fuzzy.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_help_agent.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_help_workspace.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_history_cleanup.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_image_paste.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_keymap.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_launcher.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_layout_profile.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_legacy_sessions.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_local_clipboard.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_local_open.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_modals.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_mode_selection.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_modes.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_mouse_manager.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_new_project.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_options.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_orphan_recovery.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_pane_surface.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_path_codec.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_preview_pager.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_project_favorites.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_projects_pane.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_provider_paths.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_release_notes.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_release_version.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_release_workflow.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_remote_config.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_renames.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_restart_state.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_running_pane.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_running_sort.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_runtime_config.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_scroll_agent.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_scroll_manager.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_selection_isolation.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_self_update.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_session_cache.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_session_index.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_session_lease.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_sessions_pane.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_setting_contracts.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_settings.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_sidebar.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_ssh_args.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_ssh_compat.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_ssh_display_benchmark.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_ssh_display_diagnostics.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_ssh_doctor.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_status.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_statusbar.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_system_deps.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_terminal_size.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_terminal_status.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_termux_local_touch.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_tmux_binding_manager.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_tmux_capabilities.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_tmux_ctl.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_tmux_health.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_tmux_integration.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_tmux_server.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_tmux_status.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_tool_panes.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_utf8_state.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_web_demo_recorder.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_wheel_smoke.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_widgets.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_windows_attach_relay.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_windows_bootstrap.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_windows_install_log.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_windows_msys2.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_windows_pacman.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_windows_paths.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_windows_synchronized_screen.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_windows_tmux_lifecycle.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_windows_ui_transition.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_windows_wrapper_contract.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tests/test_yolo_prompt.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tools/denested_transport_benchmark.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tools/release_notes.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tools/wheel_smoke.py +0 -0
- {railmux-0.4.2.dev5 → railmux-0.4.2.dev6}/tools/windows_archive_smoke.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.2.dev6"
|
|
@@ -374,7 +374,7 @@ class _TranscriptFormatCacheEntry:
|
|
|
374
374
|
_TRANSCRIPT_CACHE: OrderedDict[tuple[str, int], _TranscriptCacheEntry] = OrderedDict()
|
|
375
375
|
_TRANSCRIPT_CACHE_LIMIT = 4
|
|
376
376
|
_TRANSCRIPT_FORMAT_CACHE: OrderedDict[
|
|
377
|
-
tuple[str, str], _TranscriptFormatCacheEntry
|
|
377
|
+
tuple[str, str, int | None], _TranscriptFormatCacheEntry
|
|
378
378
|
] = OrderedDict()
|
|
379
379
|
_TRANSCRIPT_FORMAT_CACHE_LIMIT = 2
|
|
380
380
|
_TRANSCRIPT_FORMAT_CACHE_MAX_CHARS = 4 * 1024 * 1024
|
|
@@ -1699,7 +1699,9 @@ def _transcript_rows(
|
|
|
1699
1699
|
):
|
|
1700
1700
|
_TRANSCRIPT_CACHE.move_to_end(key)
|
|
1701
1701
|
return cached
|
|
1702
|
-
|
|
1702
|
+
# Claude history is pre-wrapped to the pane; Codex text is not.
|
|
1703
|
+
format_width = width if source.provider == "claude" else None
|
|
1704
|
+
format_key = (str(source.path), source.provider, format_width)
|
|
1703
1705
|
formatted_entry = _TRANSCRIPT_FORMAT_CACHE.get(format_key)
|
|
1704
1706
|
if formatted_entry is not None and formatted_entry.identity == identity:
|
|
1705
1707
|
_TRANSCRIPT_FORMAT_CACHE.move_to_end(format_key)
|
|
@@ -1715,6 +1717,7 @@ def _transcript_rows(
|
|
|
1715
1717
|
io.StringIO(raw),
|
|
1716
1718
|
source.provider,
|
|
1717
1719
|
claude_native=source.provider == "claude",
|
|
1720
|
+
width=format_width,
|
|
1718
1721
|
)
|
|
1719
1722
|
)
|
|
1720
1723
|
if len(formatted) <= _TRANSCRIPT_FORMAT_CACHE_MAX_CHARS:
|
|
@@ -1773,10 +1776,12 @@ def _transcript_rows_before_screen(
|
|
|
1773
1776
|
|
|
1774
1777
|
Transcript-backed history appends the live viewport after the transcript,
|
|
1775
1778
|
whose tail usually holds the same newest message. Locate the screen's
|
|
1776
|
-
text in the transcript tail through distinctive anchor rows
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1779
|
+
text in the transcript tail through distinctive anchor rows: the largest
|
|
1780
|
+
group of anchors that agree on where the screen starts wins, so screen
|
|
1781
|
+
regions the transcript renders differently (such as a diff) cannot veto
|
|
1782
|
+
the match. A lone anchor must be long and unopposed. From the earliest agreeing anchor
|
|
1783
|
+
the cut walks back over screen rows that also precede it in the
|
|
1784
|
+
transcript; without a credible match every transcript row is kept.
|
|
1780
1785
|
"""
|
|
1781
1786
|
total = len(transcript_rows)
|
|
1782
1787
|
window = max(
|
|
@@ -1791,26 +1796,42 @@ def _transcript_rows_before_screen(
|
|
|
1791
1796
|
parts.append(text)
|
|
1792
1797
|
offset += len(text)
|
|
1793
1798
|
haystack = "".join(parts)
|
|
1799
|
+
screen_texts = [_overlap_text(row) for row in screen_rows]
|
|
1794
1800
|
screen_offset = 0
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1801
|
+
# (screen row, transcript position, anchor length, implied screen start)
|
|
1802
|
+
anchors: list[tuple[int, int, int, int]] = []
|
|
1803
|
+
for index, text in enumerate(screen_texts):
|
|
1798
1804
|
if len(text) >= _OVERLAP_ANCHOR_CHARS and haystack.count(text) == 1:
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1805
|
+
position = haystack.index(text)
|
|
1806
|
+
anchors.append(
|
|
1807
|
+
(index, position, len(text), position - screen_offset))
|
|
1802
1808
|
screen_offset += len(text)
|
|
1803
|
-
if not
|
|
1809
|
+
if not anchors:
|
|
1804
1810
|
return total
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
return total
|
|
1808
|
-
else:
|
|
1811
|
+
|
|
1812
|
+
def agreeing(anchor: tuple[int, int, int, int]) -> list:
|
|
1809
1813
|
# Allow small rendering differences (list numbering, bullets).
|
|
1810
|
-
tolerance = 16 + abs(
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
+
tolerance = 16 + abs(anchor[3]) // 20
|
|
1815
|
+
return [
|
|
1816
|
+
other for other in anchors
|
|
1817
|
+
if abs(other[3] - anchor[3]) <= tolerance
|
|
1818
|
+
]
|
|
1819
|
+
|
|
1820
|
+
group = max(
|
|
1821
|
+
(agreeing(anchor) for anchor in anchors),
|
|
1822
|
+
key=lambda members: (len(members), max(m[3] for m in members)),
|
|
1823
|
+
)
|
|
1824
|
+
if len(group) == 1 and (
|
|
1825
|
+
len(anchors) > 1 or group[0][2] < _OVERLAP_SINGLE_ANCHOR_CHARS
|
|
1826
|
+
):
|
|
1827
|
+
return total
|
|
1828
|
+
index, start, _length, _implied = min(group)
|
|
1829
|
+
for text in reversed(screen_texts[:index]):
|
|
1830
|
+
if not text:
|
|
1831
|
+
continue
|
|
1832
|
+
if start < len(text) or haystack[start - len(text):start] != text:
|
|
1833
|
+
break
|
|
1834
|
+
start -= len(text)
|
|
1814
1835
|
# Keep a row that begins before the screen: it may carry text the screen
|
|
1815
1836
|
# no longer shows, and a partial duplicate is better than a loss.
|
|
1816
1837
|
keep = len(row_starts)
|
|
@@ -236,6 +236,186 @@ _MD_INLINE_RE = re.compile(
|
|
|
236
236
|
r"|__(?=\S)(.+?)(?<=\S)__"
|
|
237
237
|
r"|(?<![\w*])\*(?=[^\s*])(.+?)(?<=[^\s*])\*(?![\w*])"
|
|
238
238
|
)
|
|
239
|
+
_MD_TABLE_SEPARATOR_RE = re.compile(
|
|
240
|
+
r"^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?\s*$")
|
|
241
|
+
_MD_TABLE_CELL_SPLIT_RE = re.compile(r"(?<!\\)\|")
|
|
242
|
+
_LIST_HANG_RE = re.compile(r" *(?:(?:[-*+]|\d+[.)]) +)?")
|
|
243
|
+
_SGR_RE = re.compile(r"\x1b\[[0-9;]*m")
|
|
244
|
+
_WRAP_TOKEN_RE = re.compile(r"\x1b\[[0-9;]*m| +|[^ \x1b]+")
|
|
245
|
+
_TASK_SUMMARY_RE = re.compile(r"<summary>(.*?)</summary>", re.DOTALL)
|
|
246
|
+
|
|
247
|
+
_char_width_function = None
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _char_width(char: str) -> int:
|
|
251
|
+
"""Terminal cell width of *char*, matching pyte's ``wcwidth`` when present."""
|
|
252
|
+
global _char_width_function
|
|
253
|
+
if _char_width_function is None:
|
|
254
|
+
try:
|
|
255
|
+
from wcwidth import wcwidth as _char_width_function
|
|
256
|
+
except ImportError:
|
|
257
|
+
import unicodedata
|
|
258
|
+
|
|
259
|
+
def _char_width_function(value: str) -> int:
|
|
260
|
+
if unicodedata.combining(value):
|
|
261
|
+
return 0
|
|
262
|
+
return 2 if unicodedata.east_asian_width(value) in "WF" else 1
|
|
263
|
+
return max(0, _char_width_function(char))
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _visible_width(text: str) -> int:
|
|
267
|
+
# Claude pads an emoji-presentation selector (as in "⚠️") to two cells.
|
|
268
|
+
return sum(
|
|
269
|
+
1 if char == "\ufe0f" else _char_width(char)
|
|
270
|
+
for char in _SGR_RE.sub("", text)
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _wrap_styled(line: str, width: int, hang: int) -> list[str]:
|
|
275
|
+
"""Word-wrap one styled line the way Claude's Ink renderer does.
|
|
276
|
+
|
|
277
|
+
Rows break at spaces; only a word wider than a whole row is split. Rows
|
|
278
|
+
after the first are indented by *hang* columns so list items and message
|
|
279
|
+
bodies keep their hanging indent instead of restarting at column zero.
|
|
280
|
+
"""
|
|
281
|
+
line = line.expandtabs(8)
|
|
282
|
+
if width <= 0 or _visible_width(line) <= width:
|
|
283
|
+
return [line]
|
|
284
|
+
if hang >= width // 2:
|
|
285
|
+
hang = 0
|
|
286
|
+
rows: list[str] = []
|
|
287
|
+
row: list[str] = []
|
|
288
|
+
column = 0
|
|
289
|
+
|
|
290
|
+
def newline() -> None:
|
|
291
|
+
nonlocal row, column
|
|
292
|
+
rows.append("".join(row).rstrip(" "))
|
|
293
|
+
row = [" " * hang]
|
|
294
|
+
column = hang
|
|
295
|
+
|
|
296
|
+
for token in _WRAP_TOKEN_RE.findall(line):
|
|
297
|
+
if token.startswith("\x1b"):
|
|
298
|
+
row.append(token)
|
|
299
|
+
continue
|
|
300
|
+
token_width = _visible_width(token)
|
|
301
|
+
if token.startswith(" "):
|
|
302
|
+
if column + token_width <= width:
|
|
303
|
+
row.append(token)
|
|
304
|
+
column += token_width
|
|
305
|
+
else:
|
|
306
|
+
newline()
|
|
307
|
+
continue
|
|
308
|
+
if (
|
|
309
|
+
column + token_width > width
|
|
310
|
+
and column > hang
|
|
311
|
+
and token_width <= width - hang
|
|
312
|
+
):
|
|
313
|
+
newline()
|
|
314
|
+
for char in token:
|
|
315
|
+
char_width = _char_width(char)
|
|
316
|
+
if column + char_width > width and column > hang:
|
|
317
|
+
newline()
|
|
318
|
+
row.append(char)
|
|
319
|
+
column += char_width
|
|
320
|
+
rows.append("".join(row).rstrip(" "))
|
|
321
|
+
return rows
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _hang_for(row: str, prefix: int) -> int:
|
|
325
|
+
"""Hanging indent for a row whose own content starts after *prefix* cells."""
|
|
326
|
+
content = _SGR_RE.sub("", row)[prefix:]
|
|
327
|
+
return prefix + len(_LIST_HANG_RE.match(content).group(0))
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _close_fragments(fragments: list[str]) -> list[str]:
|
|
331
|
+
"""Make each wrapped fragment carry and close its own SGR state.
|
|
332
|
+
|
|
333
|
+
Table cells sit beside other cells on one row, so a style that continues
|
|
334
|
+
onto a cell's next fragment must not leak across the borders between.
|
|
335
|
+
"""
|
|
336
|
+
closed: list[str] = []
|
|
337
|
+
active: list[str] = []
|
|
338
|
+
for fragment in fragments:
|
|
339
|
+
prefix = "".join(active)
|
|
340
|
+
for code in _SGR_RE.findall(fragment):
|
|
341
|
+
if code == RESET:
|
|
342
|
+
active.clear()
|
|
343
|
+
else:
|
|
344
|
+
active.append(code)
|
|
345
|
+
closed.append(f"{prefix}{fragment}{RESET}")
|
|
346
|
+
return closed
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _table_cells(line: str) -> list[str]:
|
|
350
|
+
stripped = line.strip()
|
|
351
|
+
if stripped.startswith("|"):
|
|
352
|
+
stripped = stripped[1:]
|
|
353
|
+
if stripped.endswith("|") and not stripped.endswith("\\|"):
|
|
354
|
+
stripped = stripped[:-1]
|
|
355
|
+
return [
|
|
356
|
+
cell.strip().replace("\\|", "|")
|
|
357
|
+
for cell in _MD_TABLE_CELL_SPLIT_RE.split(stripped)
|
|
358
|
+
]
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _markdown_table(lines: list[str], width: int) -> list[str] | None:
|
|
362
|
+
"""Draw a markdown table as Claude does: a box whose cells wrap to fit.
|
|
363
|
+
|
|
364
|
+
*lines* holds the header, the separator and the body rows. ``None`` means
|
|
365
|
+
the columns cannot fit *width*, and the caller keeps the source text.
|
|
366
|
+
"""
|
|
367
|
+
header = [
|
|
368
|
+
f"{BOLD}{_markdown_inline(cell, bold=True)}{BOLD_OFF}"
|
|
369
|
+
for cell in _table_cells(lines[0])
|
|
370
|
+
]
|
|
371
|
+
count = len(header)
|
|
372
|
+
body = []
|
|
373
|
+
for line in lines[2:]:
|
|
374
|
+
cells = [_markdown_inline(cell) for cell in _table_cells(line)]
|
|
375
|
+
body.append((cells + [""] * count)[:count])
|
|
376
|
+
available = width - 1 - 3 * count
|
|
377
|
+
if available < 3 * count:
|
|
378
|
+
return None
|
|
379
|
+
natural = [
|
|
380
|
+
max(1, *(_visible_width(row[index]) for row in [header, *body]))
|
|
381
|
+
for index in range(count)
|
|
382
|
+
]
|
|
383
|
+
widths = natural
|
|
384
|
+
if sum(natural) > available:
|
|
385
|
+
low, high = 3, max(natural)
|
|
386
|
+
while low < high:
|
|
387
|
+
cap = (low + high + 1) // 2
|
|
388
|
+
if sum(min(value, cap) for value in natural) <= available:
|
|
389
|
+
low = cap
|
|
390
|
+
else:
|
|
391
|
+
high = cap - 1
|
|
392
|
+
widths = [min(value, low) for value in natural]
|
|
393
|
+
spare = available - sum(widths)
|
|
394
|
+
for index, value in enumerate(natural):
|
|
395
|
+
if spare and value > widths[index]:
|
|
396
|
+
widths[index] += 1
|
|
397
|
+
spare -= 1
|
|
398
|
+
|
|
399
|
+
def border(left: str, middle: str, right: str) -> str:
|
|
400
|
+
return left + middle.join("─" * (cell + 2) for cell in widths) + right
|
|
401
|
+
|
|
402
|
+
rendered = [border("┌", "┬", "┐")]
|
|
403
|
+
for row_index, row in enumerate([header, *body]):
|
|
404
|
+
if row_index:
|
|
405
|
+
rendered.append(border("├", "┼", "┤"))
|
|
406
|
+
fragments = [
|
|
407
|
+
_close_fragments(_wrap_styled(cell, widths[index], 0))
|
|
408
|
+
for index, cell in enumerate(row)
|
|
409
|
+
]
|
|
410
|
+
for line_index in range(max(len(cell) for cell in fragments)):
|
|
411
|
+
parts = []
|
|
412
|
+
for index, cell in enumerate(fragments):
|
|
413
|
+
text = cell[line_index] if line_index < len(cell) else ""
|
|
414
|
+
padding = " " * (widths[index] - _visible_width(text))
|
|
415
|
+
parts.append(f" {text}{padding} ")
|
|
416
|
+
rendered.append("│" + "│".join(parts) + "│")
|
|
417
|
+
rendered.append(border("└", "┴", "┘"))
|
|
418
|
+
return rendered
|
|
239
419
|
|
|
240
420
|
|
|
241
421
|
def _markdown_inline(line: str, *, bold: bool = False) -> str:
|
|
@@ -260,21 +440,39 @@ def _markdown_inline(line: str, *, bold: bool = False) -> str:
|
|
|
260
440
|
return _MD_INLINE_RE.sub(replace, line)
|
|
261
441
|
|
|
262
442
|
|
|
263
|
-
def _markdown_lines(lines: list[str]) -> list[str]:
|
|
443
|
+
def _markdown_lines(lines: list[str], width: int | None = None) -> list[str]:
|
|
264
444
|
"""Approximate Claude Code's markdown rendering for sanitized text lines.
|
|
265
445
|
|
|
266
446
|
Styled lines end with a full reset: every span is already closed, and the
|
|
267
|
-
reset keeps row wrappers from carrying an ever-growing SGR prefix.
|
|
447
|
+
reset keeps row wrappers from carrying an ever-growing SGR prefix. Tables
|
|
448
|
+
are drawn as boxes only when the target *width* is known.
|
|
268
449
|
"""
|
|
269
450
|
rendered: list[str] = []
|
|
270
451
|
in_fence = False
|
|
271
|
-
|
|
452
|
+
index = 0
|
|
453
|
+
while index < len(lines):
|
|
454
|
+
line = lines[index]
|
|
455
|
+
index += 1
|
|
272
456
|
if _MD_FENCE_RE.match(line):
|
|
273
457
|
in_fence = not in_fence
|
|
274
458
|
continue
|
|
275
459
|
if in_fence:
|
|
276
460
|
rendered.append(line)
|
|
277
461
|
continue
|
|
462
|
+
if (
|
|
463
|
+
width is not None
|
|
464
|
+
and line.lstrip().startswith("|")
|
|
465
|
+
and index < len(lines)
|
|
466
|
+
and _MD_TABLE_SEPARATOR_RE.match(lines[index])
|
|
467
|
+
):
|
|
468
|
+
end = index + 1
|
|
469
|
+
while end < len(lines) and lines[end].lstrip().startswith("|"):
|
|
470
|
+
end += 1
|
|
471
|
+
table = _markdown_table(lines[index - 1:end], width)
|
|
472
|
+
if table is not None:
|
|
473
|
+
rendered.extend(table)
|
|
474
|
+
index = end
|
|
475
|
+
continue
|
|
278
476
|
heading = _MD_HEADING_RE.match(line)
|
|
279
477
|
if heading:
|
|
280
478
|
inner = _markdown_inline(heading.group(2), bold=True)
|
|
@@ -285,23 +483,38 @@ def _markdown_lines(lines: list[str]) -> list[str]:
|
|
|
285
483
|
return rendered
|
|
286
484
|
|
|
287
485
|
|
|
486
|
+
def _wrap_rows(rows: list[str], width: int | None, prefix: int) -> list[str]:
|
|
487
|
+
"""Pre-wrap rendered rows when the pane *width* is known."""
|
|
488
|
+
if width is None:
|
|
489
|
+
return rows
|
|
490
|
+
return [
|
|
491
|
+
wrapped
|
|
492
|
+
for row in rows
|
|
493
|
+
for wrapped in _wrap_styled(row, width, _hang_for(row, prefix))
|
|
494
|
+
]
|
|
495
|
+
|
|
496
|
+
|
|
288
497
|
def _claude_native_block(
|
|
289
498
|
marker: str, text: str, *, colour: str, markdown: bool = False,
|
|
499
|
+
width: int | None = None,
|
|
290
500
|
) -> str:
|
|
291
501
|
"""Render a static, non-interactive approximation of one Claude TUI block."""
|
|
292
502
|
lines = _sanitize_text(text).strip().splitlines()
|
|
293
503
|
if markdown:
|
|
294
|
-
lines = _markdown_lines(
|
|
504
|
+
lines = _markdown_lines(
|
|
505
|
+
lines, None if width is None else width - 2)
|
|
295
506
|
if not lines:
|
|
296
507
|
return ""
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
return
|
|
508
|
+
rows = [
|
|
509
|
+
f"{colour}{marker}{RESET} {lines[0]}",
|
|
510
|
+
*(f" {line}" for line in lines[1:]),
|
|
511
|
+
]
|
|
512
|
+
return "\n" + "\n".join(_wrap_rows(rows, width, 2)) + "\n"
|
|
302
513
|
|
|
303
514
|
|
|
304
|
-
def _render_claude_native_user(
|
|
515
|
+
def _render_claude_native_user(
|
|
516
|
+
record: dict, width: int | None = None,
|
|
517
|
+
) -> str | None:
|
|
305
518
|
message = record.get("message")
|
|
306
519
|
if not isinstance(message, dict):
|
|
307
520
|
return None
|
|
@@ -316,11 +529,21 @@ def _render_claude_native_user(record: dict) -> str | None:
|
|
|
316
529
|
).strip()
|
|
317
530
|
else:
|
|
318
531
|
text = ""
|
|
319
|
-
|
|
532
|
+
if text.startswith("<task-notification>"):
|
|
533
|
+
# Claude shows a finished background task as one status line, not as
|
|
534
|
+
# a prompt carrying the notification's XML.
|
|
535
|
+
summary = _TASK_SUMMARY_RE.search(text)
|
|
536
|
+
if summary is None:
|
|
537
|
+
return None
|
|
538
|
+
return _claude_native_block(
|
|
539
|
+
"●", summary.group(1).strip(), colour=CLAUDE_MUTED, width=width,
|
|
540
|
+
) or None
|
|
541
|
+
return _claude_native_block(
|
|
542
|
+
"❯", text, colour=CLAUDE_ACCENT, width=width) or None
|
|
320
543
|
|
|
321
544
|
|
|
322
545
|
def _render_claude_native_assistant(
|
|
323
|
-
record: dict, calls: dict[str, str],
|
|
546
|
+
record: dict, calls: dict[str, str], width: int | None = None,
|
|
324
547
|
):
|
|
325
548
|
"""Yield Claude-like read-only text/tool rows from semantic JSONL records."""
|
|
326
549
|
message = record.get("message")
|
|
@@ -336,7 +559,7 @@ def _render_claude_native_assistant(
|
|
|
336
559
|
if block_type == "text":
|
|
337
560
|
rendered = _claude_native_block(
|
|
338
561
|
"●", str(block.get("text", "")), colour=CLAUDE_ACCENT,
|
|
339
|
-
markdown=True,
|
|
562
|
+
markdown=True, width=width,
|
|
340
563
|
)
|
|
341
564
|
if rendered:
|
|
342
565
|
yield rendered
|
|
@@ -352,20 +575,19 @@ def _render_claude_native_assistant(
|
|
|
352
575
|
if isinstance(tool_input, dict):
|
|
353
576
|
for key in (
|
|
354
577
|
"description", "command", "query", "pattern",
|
|
355
|
-
"file_path", "path", "url",
|
|
578
|
+
"file_path", "path", "url", "skill",
|
|
356
579
|
):
|
|
357
580
|
value = tool_input.get(key)
|
|
358
581
|
if isinstance(value, str) and value.strip():
|
|
359
582
|
summary = _sanitize_text(value.strip()).replace("\n", " ")
|
|
360
583
|
break
|
|
361
584
|
suffix = f"({_truncate(summary, 180)})" if summary else ""
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
)
|
|
585
|
+
row = f"{CLAUDE_ACCENT}●{RESET} {BOLD}{name}{RESET}{suffix}"
|
|
586
|
+
yield "\n" + "\n".join(_wrap_rows([row], width, 2)) + "\n"
|
|
365
587
|
|
|
366
588
|
|
|
367
589
|
def _render_claude_native_tool_results(
|
|
368
|
-
record: dict, calls: dict[str, str],
|
|
590
|
+
record: dict, calls: dict[str, str], width: int | None = None,
|
|
369
591
|
):
|
|
370
592
|
message = record.get("message")
|
|
371
593
|
if not isinstance(message, dict):
|
|
@@ -382,13 +604,16 @@ def _render_claude_native_tool_results(
|
|
|
382
604
|
tool_id = block.get("tool_use_id")
|
|
383
605
|
if isinstance(tool_id, str):
|
|
384
606
|
calls.pop(tool_id, None)
|
|
385
|
-
|
|
607
|
+
# _truncate sanitizes before appending its own styled ellipsis.
|
|
608
|
+
lines = _truncate(text, 500).splitlines()
|
|
386
609
|
if not lines:
|
|
387
610
|
continue
|
|
388
611
|
colour = RED if block.get("is_error") is True else CLAUDE_MUTED
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
612
|
+
rows = [f" ⎿ {lines[0]}", *(f" {line}" for line in lines[1:])]
|
|
613
|
+
yield colour
|
|
614
|
+
for row in rows:
|
|
615
|
+
for wrapped in _wrap_rows([row], width, 5):
|
|
616
|
+
yield f"{wrapped}\n"
|
|
392
617
|
yield RESET
|
|
393
618
|
|
|
394
619
|
|
|
@@ -435,36 +660,36 @@ def _claude_current_branch(records: list[dict]) -> list[dict]:
|
|
|
435
660
|
|
|
436
661
|
|
|
437
662
|
def _render_claude_records(
|
|
438
|
-
records: list[dict], *, claude_native: bool,
|
|
663
|
+
records: list[dict], *, claude_native: bool, width: int | None = None,
|
|
439
664
|
):
|
|
440
|
-
"""Render only the active Claude branch from buffered JSONL records.
|
|
665
|
+
"""Render only the active Claude branch from buffered JSONL records.
|
|
666
|
+
|
|
667
|
+
*width* pre-wraps native rows to the pane like Claude's own renderer.
|
|
668
|
+
"""
|
|
441
669
|
calls: dict[str, str] = {}
|
|
442
670
|
for record in _claude_current_branch(records):
|
|
443
671
|
rtype = record.get("type", "")
|
|
444
672
|
if rtype in _SKIP_TYPES:
|
|
445
673
|
continue
|
|
446
|
-
if
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
if
|
|
450
|
-
|
|
451
|
-
|
|
674
|
+
if not claude_native:
|
|
675
|
+
if _is_real_user(record):
|
|
676
|
+
rendered = _render_user(record)
|
|
677
|
+
if rendered:
|
|
678
|
+
yield rendered
|
|
679
|
+
if rtype == "user":
|
|
680
|
+
yield from _render_claude_tool_results(record, calls)
|
|
681
|
+
elif rtype == "assistant":
|
|
682
|
+
yield from _render_assistant_blocks(record, calls)
|
|
683
|
+
continue
|
|
684
|
+
# Claude hides meta prompts such as a loaded skill's instructions.
|
|
685
|
+
if _is_real_user(record) and record.get("isMeta") is not True:
|
|
686
|
+
rendered = _render_claude_native_user(record, width)
|
|
452
687
|
if rendered:
|
|
453
688
|
yield rendered
|
|
454
689
|
if rtype == "user":
|
|
455
|
-
|
|
456
|
-
_render_claude_native_tool_results
|
|
457
|
-
if claude_native
|
|
458
|
-
else _render_claude_tool_results
|
|
459
|
-
)
|
|
460
|
-
yield from renderer(record, calls)
|
|
690
|
+
yield from _render_claude_native_tool_results(record, calls, width)
|
|
461
691
|
elif rtype == "assistant":
|
|
462
|
-
|
|
463
|
-
_render_claude_native_assistant
|
|
464
|
-
if claude_native
|
|
465
|
-
else _render_assistant_blocks
|
|
466
|
-
)
|
|
467
|
-
yield from renderer(record, calls)
|
|
692
|
+
yield from _render_claude_native_assistant(record, calls, width)
|
|
468
693
|
|
|
469
694
|
|
|
470
695
|
def format_transcript(
|
|
@@ -472,6 +697,7 @@ def format_transcript(
|
|
|
472
697
|
fmt: str | None = None,
|
|
473
698
|
*,
|
|
474
699
|
claude_native: bool = False,
|
|
700
|
+
width: int | None = None,
|
|
475
701
|
):
|
|
476
702
|
"""Read a session JSONL and yield ANSI-formatted strings.
|
|
477
703
|
|
|
@@ -482,6 +708,9 @@ def format_transcript(
|
|
|
482
708
|
types actually present in the stream, so a bounded Codex rollout whose
|
|
483
709
|
leading ``session_meta`` header was omitted still renders as Codex.
|
|
484
710
|
|
|
711
|
+
*width* (Claude native history only) wraps rows and draws tables for a
|
|
712
|
+
pane of that many cells.
|
|
713
|
+
|
|
485
714
|
Callers should write each chunk to stdout, e.g.::
|
|
486
715
|
|
|
487
716
|
for chunk in format_transcript(path_or_file):
|
|
@@ -538,7 +767,7 @@ def format_transcript(
|
|
|
538
767
|
claude_records.append(record)
|
|
539
768
|
if _codex is False:
|
|
540
769
|
yield from _render_claude_records(
|
|
541
|
-
claude_records, claude_native=claude_native)
|
|
770
|
+
claude_records, claude_native=claude_native, width=width)
|
|
542
771
|
except OSError:
|
|
543
772
|
yield f"{YELLOW}Could not read: {source}{RESET}\n"
|
|
544
773
|
finally:
|
|
@@ -1672,6 +1672,28 @@ def test_transcript_overlap_rejects_disagreeing_anchors():
|
|
|
1672
1672
|
assert fast_display_server._transcript_rows_before_screen(rows, screen) == 3
|
|
1673
1673
|
|
|
1674
1674
|
|
|
1675
|
+
def test_transcript_overlap_ignores_screen_regions_rendered_differently():
|
|
1676
|
+
rows = (
|
|
1677
|
+
b"older message mentioning edited readme line",
|
|
1678
|
+
b"filler " * 20,
|
|
1679
|
+
b"truncated tool output",
|
|
1680
|
+
b"",
|
|
1681
|
+
b"newest message first distinctive row",
|
|
1682
|
+
b"newest message second distinctive row",
|
|
1683
|
+
)
|
|
1684
|
+
# A diff view tops the screen: one of its rows also occurs much earlier
|
|
1685
|
+
# in the transcript, disagreeing with the anchors in the newest message.
|
|
1686
|
+
screen = (
|
|
1687
|
+
b"older message mentioning edited readme line",
|
|
1688
|
+
b"+ added diff row only the screen shows",
|
|
1689
|
+
b"",
|
|
1690
|
+
b"newest message first distinctive row",
|
|
1691
|
+
b"newest message second distinctive row",
|
|
1692
|
+
)
|
|
1693
|
+
|
|
1694
|
+
assert fast_display_server._transcript_rows_before_screen(rows, screen) == 3
|
|
1695
|
+
|
|
1696
|
+
|
|
1675
1697
|
@pytest.mark.parametrize(
|
|
1676
1698
|
("policy", "choice_required"),
|
|
1677
1699
|
[("ask", True), ("native", False)],
|
|
@@ -1843,7 +1865,7 @@ def test_transcript_cache_evicts_least_recent_file_width(monkeypatch, tmp_path):
|
|
|
1843
1865
|
fast_display_server._TRANSCRIPT_CACHE.clear()
|
|
1844
1866
|
|
|
1845
1867
|
|
|
1846
|
-
def
|
|
1868
|
+
def test_transcript_format_cache_reuses_exact_identity_per_width(
|
|
1847
1869
|
monkeypatch, tmp_path,
|
|
1848
1870
|
):
|
|
1849
1871
|
pyte = fast_display_server._extended_pyte(__import__("pyte"))
|
|
@@ -1871,16 +1893,22 @@ def test_transcript_format_cache_reuses_exact_identity_across_widths(
|
|
|
1871
1893
|
assert fast_display_server._transcript_rows(
|
|
1872
1894
|
pyte, marker, 40, allow_stale=False
|
|
1873
1895
|
) is not None
|
|
1896
|
+
fast_display_server._TRANSCRIPT_CACHE.clear()
|
|
1874
1897
|
assert fast_display_server._transcript_rows(
|
|
1875
|
-
pyte, marker,
|
|
1898
|
+
pyte, marker, 40, allow_stale=False
|
|
1876
1899
|
) is not None
|
|
1877
1900
|
assert len(calls) == 1
|
|
1901
|
+
# Claude history is wrapped for the pane, so a new width re-formats.
|
|
1902
|
+
assert fast_display_server._transcript_rows(
|
|
1903
|
+
pyte, marker, 10, allow_stale=False
|
|
1904
|
+
) is not None
|
|
1905
|
+
assert len(calls) == 2
|
|
1878
1906
|
|
|
1879
1907
|
path.write_text('{"type":"user","changed":true}\n')
|
|
1880
1908
|
assert fast_display_server._transcript_rows(
|
|
1881
|
-
pyte, marker,
|
|
1909
|
+
pyte, marker, 10, allow_stale=False
|
|
1882
1910
|
) is not None
|
|
1883
|
-
assert len(calls) ==
|
|
1911
|
+
assert len(calls) == 3
|
|
1884
1912
|
finally:
|
|
1885
1913
|
fast_display_server._TRANSCRIPT_CACHE.clear()
|
|
1886
1914
|
fast_display_server._TRANSCRIPT_FORMAT_CACHE.clear()
|