Investigating "weird redraw issues" surfaced by the wezterm-term swap
turned up four separate bugs in the proxy↔terminal byte path:
1. TerminfoRenderer caches CellAttributes across render_to calls.
The trailing raw `\x1b[0m` and out-of-band CLEAR_SCREEN writes
left its `current_attr` desynced from the real terminal — next
frame's SGR transitions were computed against the wrong baseline,
manifesting as bg-color smears and sticky links. Route the reset
through `Change::AllAttributes(default)` so the renderer's pen
stays in sync, and recreate the renderer whenever `force_full`
fires (startup, lookback exit, alt exit, resize) so the cached
state can't outlive an out-of-band write.
2. Proxy startup didn't clear the terminal, so the first VT diff
landed on top of whatever the user's shell had on screen. Wipe
on spawn alongside enabling bracketed paste.
3. `Drop` only disabled bracketed paste and restored termios, so an
exit mid-render leaked active styles into the shell prompt and
left Claude's last UI frame on screen. Add OSC-8 close → SGR 0 →
CLEAR_SCREEN → CURSOR_HOME before the existing teardown.
4. Under tmux, steady-state diff renders produced a palimpsest of
stacked frames because termwiz's `Surface::diff_lines` only walks
the new frame's *visible* cells — cells that became blank don't
generate clearing Changes. tmux is also tracking the grid and is
sensitive to the partial repaint stream. Detect `$TMUX` at spawn
and switch to passthrough: feed wezterm-term for state but
forward raw child bytes to stdout. Lookback's one-shot full
repaint still uses the renderer (screen is cleared first, so
the diff_lines blank-cell behavior is fine).
Test infrastructure:
- New shared `test_support::pty_mock_binary()` honors CARGO_TARGET_DIR,
tries debug/release, and panics with an actionable message instead
of returning Option<String>. All 8 `let Some(...) else { return; }`
silent-skip patterns replaced with direct destructure.
- Mirror is now advanced after `render_to` succeeds, not before.
New regression tests:
- `sgr_reemitted_on_second_frame_after_wire_reset`
- `hyperlink_reemitted_on_second_frame_after_wire_close`
- `forced_full_render_reemits_sgr_after_renderer_recreate`
- `test_lookback_exit_emits_clear_screen` asserts CLEAR/HOME precede
the SYNC-wrapped repaint payload.
122 passing / 0 failing / 4 ignored (the two pre-existing
underline-style cases plus the claude-CLI-required integration).
Swap the rendering core. The previous backend (vt100 0.16) only stored
six attributes — fg/bg color, bold, dim, italic, underline, inverse —
and silently dropped everything else when re-emitting the screen. With
modern Claude Code that meant losing OSC 8 hyperlinks, strikethrough,
fancy underline styles, underline color, and so on.
New stack:
* wezterm-term::Terminal handles the byte-driven VT emulation. Its
cell model preserves the full modern attribute set including
hyperlinks and underline color.
* termwiz::surface::Surface is used as a "mirror of last emitted
state." Each render diffs the terminal's current visible lines
against this mirror to compute the minimum change set.
* termwiz::render::TerminfoRenderer encodes those Changes as bytes
using a TrueColor + hyperlink-capable capability profile.
A defensive OSC 8 close is appended to every render to handle the case
where the diff ends inside a hyperlinked region (the renderer only
emits the close on transition to a non-hyperlinked cell).
Both termwiz and wezterm-term are pulled from the wezterm git repo at
a pinned rev so the Line types unify across the dep graph.
Test results, against the color-preservation harness from the previous
commit (run via cargo test color_preservation -- --ignored on master):
baseline (vt100): 0 / 5 passing
after this commit: 3 / 5 passing (osc8, strikethrough, double underline)
The two still-ignored tests (curly underline, underline color) are
upstream renderer limitations: termwiz's TerminfoRenderer only emits
Single/Double underline and never emits SGR 58. The cell model stores
both correctly; only the emit step drops them. Tests are kept #[ignore]
with comments calling out the upstream gap.
Total: 118 passed, 4 ignored, 0 failed.
Pin the baseline: feed OSC 8 hyperlinks, strikethrough, curly underline,
underline color, and double underline through Proxy, force a render,
and capture what the proxy emits to its stdout side. Each test asserts
the styling survives the round-trip.
All five fail on the current vt100 0.16 backend because its cell model
only stores fg/bg color, bold, dim, italic, underline, inverse — every
other attribute is dropped at render time. Tests are #[ignore]-gated so
CI stays green; run with --ignored to see the baseline failures. Once
the emulator is swapped to termwiz, remove the gates.
A truecolor-foreground sanity test runs by default and confirms the
parts of the SGR space vt100 does support are still working.
Adds Proxy::force_render as a #[cfg(test)] wrapper around the private
render_vt_screen method.
Add a pty-mock workspace crate (echo/alt-screen/paste-echo/buffer-fill/
sync-blocks subcommands) plus an in-process integration test suite that
drives Proxy directly via a new_for_test constructor and #[cfg(test)]
state accessors. Sets up the regression net needed before swapping the
underlying VT emulator from vt100 to termwiz.
Pulled from upstream draft PR #35; the alt-screen feed-order semantic
change is intentionally deferred (its dedicated test is left #[ignore]).
* fix: bracketed paste passthrough and pty write deadlock prevention
Enable bracketed paste on the real terminal at startup so tmux/Ghostty
wraps paste content in markers. Detect paste markers in process_input
and forward directly to the child pty, skipping byte-by-byte lookback
matching. Replace blocking write_all with write_to_pty_draining that
interleaves output reads to prevent the classic pty buffer deadlock
when forwarding large input.
* Fix clippy/fmt, buffer drained PTY output through VT parser, handle split paste end markers
- Fix clippy collapsible_if warning and cargo fmt issues
- Drained child output during PTY writes now buffers into pty_drain_buffer
and is processed through process_output after process_input returns,
keeping VT parser state, history, and alt-screen tracking in sync
- Add paste_remainder buffer to handle paste end markers split across
read() boundaries, preventing permanent stuck-in-paste-mode state
- Add split_trailing_marker_prefix helper with tests
---------
Co-authored-by: Dave Beesley <david.beesley@pm.me>
* Fix auto-lookback trigger and add version tracking
- Trigger on stdin inactivity instead of render time
- Only dump when new output exists since last dump
- Default timeout 5s -> 15s
- Embed git hash in version output
- Document macOS config path
* feat: add auto lookback mode
Automatically shows full terminal history after idle timeout,
returns to truncated view when new output arrives.
- Add auto_lookback_timeout_ms config option (default: 1000ms)
- Add --auto-lookback-timeout CLI flag
- Set to 0 to disable the feature
* intermediate
* auto lookback
---------
Co-authored-by: Ilya Sinkin <ilyasinkin2002@yandex.kz>