{
  "answer": "Investigated upstream `ogulcancelik/herdr` v0.7.4 at commit `02a6e874f67800891b5a549297219ed6f3ce0f2f`. Herdr has substantial, directly relevant terminal/TUI/PTY implementation; this is not a case where the project merely delegates everything to dependencies.\n\n## Executive conclusion\n\nThe strongest directly reusable pattern for SPT stale-cell bugs is Herdr’s semantic-frame + per-client differential encoder in `src/protocol/render_ansi.rs`, combined with its explicit baseline reset and commit-after-enqueue logic in `src/server/render_stream.rs` / `src/server/headless.rs`. It preserves whitespace because a blank is an ordinary cell symbol (`\" \"`) and changed blank cells are written just like other changed cells. It does not trim rows or output lines. When there is no valid baseline—or the dimensions differ—it emits `CSI 2 J` plus home and repaints every non-skip cell. This is a robust answer to stale cells left behind when replacement content is shorter.\n\nThe rendering architecture is useful, but should not be copied wholesale into SPT. Herdr is a full multiplexer with a semantic cell grid, Ratatui rendering, client/server frame protocol, Kitty graphics, hyperlinks, cursor-shape tracking, and a PTY actor. SPT likely needs only a much smaller terminal-surface contract: retain the last acknowledged rectangular frame, compare all cells including spaces, explicitly erase or overwrite vacated suffix cells, invalidate the baseline after resize/reconnect/focus surface loss, and commit the new baseline only after successful output delivery.\n\n## Full-frame versus differential rendering\n\n* `BlitEncoder` owns `last_frame: Option<FrameData>`. First paint, a forced paint, and any width/height change are full redraws; otherwise the encoder compares current and previous cells and emits only changed cells.\n* Full redraw uses `ESC [ 2 J ESC [ H`, then visits the entire rectangular frame and writes every non-`skip` cell at an explicit cursor position. Differential redraw scans every row/column and writes any visually changed cell.\n* Frames are wrapped in synchronized-output mode (`CSI ? 2026 h` / `CSI ? 2026 l`) and the hardware cursor is hidden before cell writes, then positioned and restored at the end. This reduces visible intermediate states and cursor artifacts; it is useful polish but is not the core stale-cell fix.\n* Semantic clients skip byte-identical frames. Terminal-ANSI clients also skip a frame when their encoder baseline equals it.\n* Server-side baselines are transactional at the queue boundary: `prepare_frame` computes against the current baseline without mutating it; `commit_sent_frame` runs only after `try_send` succeeds. A full render is deferred if the one-slot render lane is full rather than falsely advancing the baseline.\n\n**SPT applicability:** directly reusable conceptually. Maintain a rectangular cell model or equivalent line-width-aware snapshot. Diff against the last successfully delivered frame, not the last attempted render. A simpler SPT implementation can operate row-wise, but it must represent trailing blanks/vacated cells explicitly.\n\n## Whitespace preservation and stale-cell prevention\n\n* `FrameData::from_buffer` copies each Ratatui cell’s symbol verbatim with `cell.symbol().to_owned()`; the wire type describes it as a grapheme cluster. There is no trimming in this conversion.\n* A blank cell is represented as `symbol: \" \"`. `cells_visually_equal` compares the symbol, foreground/background, modifier, and sanitized hyperlink. Therefore a transition from `x` to ` ` is a change and causes an actual space byte to be emitted.\n* `write_cell` writes `cell.symbol.as_bytes()` with no `trim`, `trim_end`, newline normalization, or omission of spaces. Full-frame writing does the same.\n* Wide graphemes are handled conservatively: Herdr tracks display width with `unicode-width`, skips continuation cells, and invalidates the maximum width occupied by the old or new grapheme. That prevents remnants when replacing a wide glyph with a narrow glyph.\n* `skip` is treated as Ratatui internal continuation metadata rather than visual equality. This distinction matters for double-width glyphs.\n\n**SPT applicability:** this is the most important reusable behavior. Never run terminal-render payloads through line-oriented normalization such as `trim_end()`, and never infer that trailing spaces are insignificant. For each changed row, either (a) emit its complete target width, including spaces, or (b) emit the changed prefix and an explicit erase-to-end-of-line. If SPT accepts arbitrary Unicode, compute display-cell width rather than byte/character count.\n\n## Erase-in-line / erase-screen behavior\n\n* Herdr explicitly uses erase-screen only for a full redraw: `ESC[2J ESC[H`.\n* Its differential path does **not** use EL (`CSI K`). It overwrites each changed cell, including cells that became spaces. This works because the frame is a complete fixed-size rectangular grid.\n* No relevant production implementation of erase-to-end-of-line was found in the renderer. Consequently, Herdr’s exact differential approach assumes the producer supplies every cell in the target rectangle.\n\n**SPT recommendation:** choose one invariant and enforce it. If SPT has complete rows, Herdr-style blank-cell overwrite is deterministic and styling-safe. If SPT has variable-length logical lines, append `CSI 0 K` after painting a row whose previous display width exceeded its new width. On a baseline reset, use `CSI 2 J` + home followed by a full frame. Do not rely on newline movement or a shorter string to clear old terminal cells.\n\n## Alternate screen and raw mode\n\n* Herdr depends on `ratatui = 0.30` and `crossterm = 0.29`. Both its monolithic app and thin client call `ratatui::init()` and `ratatui::restore()`.\n* Ratatui 0.30’s official API documentation states that `init()` enables raw mode and the alternate screen, while `restore()` restores terminal state. Thus Herdr uses an alternate-screen full-screen TUI rather than painting into normal shell scrollback.\n* The thin client additionally disables line wrap while active, and restores line wrap, focus reporting, bracketed paste, mouse capture, host mouse modes, keyboard enhancement state, color-scheme reporting, cursor visibility, and cursor shape on teardown.\n\n**SPT applicability:** reuse only if SPT owns a full-screen surface. If SPT renders inside another harness’s owned terminal region, it should not independently enter/leave alternate screen or toggle global raw mode. Ownership must be singular. The cleanup principle is reusable: every mode enabled must have an idempotent inverse in one guard.\n\n## Raw-mode teardown, panic and exit guards\n\n* Thin-client setup returns `TerminalGuard`; its `Drop` calls `restore_terminal_state`, so normal return and error unwinding restore terminal modes.\n* The restore path is best-effort and comprehensive: clear received graphics, reset modifyOtherKeys, pop keyboard flags, enable wrapping, disable focus/bracketed paste/mouse, clear all known host mouse reporting modes, restore Windows console input mode, call `ratatui::restore()`, then show the cursor and reset DECSCUSR.\n* The monolithic path installs an explicit panic hook which disables enhanced modes, clears graphics, calls `ratatui::restore()`, and then invokes the previous hook. Ratatui 0.30 also documents that its initialization functions install a terminal-restoring panic hook.\n* The normal monolithic exit performs equivalent restoration before dropping application state.\n\n**SPT applicability:** directly reusable lifecycle pattern. Put terminal restoration in an idempotent RAII/finally guard, retain a panic/uncaught-exception/signal fallback appropriate to SPT’s runtime, and restore cursor visibility even if earlier cleanup steps fail. Avoid teardown code that returns early after one failed escape write.\n\n## Resize handling\n\n* Thin clients poll terminal geometry every 100 ms and send `ClientMessage::Resize` when columns, rows, or cell pixel dimensions change.\n* Server input clamps the size and routes it as `ServerEvent::ClientResize`.\n* PTY resize requests are coalesced in `SharedPtyControls.resize: Option<PtyResizeRequest>`; the actor takes the latest request, calls the PTY resize operation, and then enqueues terminal-query responses generated by the terminal emulator.\n* The blitter automatically full-redraws whenever prior and current frame dimensions differ. Focus-surface redraw requests replace the encoder with a new one, invalidating the old baseline.\n* The server recalculates effective shared runtime size when a foreground client disappears or foreground ownership changes.\n\n**SPT applicability:** directly reuse the invalidation rule: resize is a screen-generation boundary. Discard the old differential baseline and full repaint after the new geometry is established. Coalescing resize events to latest-wins is appropriate; rendering every intermediate size is unnecessary and increases races.\n\n## Disconnect handling\n\n* Client socket EOF, oversized messages, and other framing errors all become `ClientDisconnected` events. The client reader similarly converts server EOF/error into `ServerDisconnected`, producing a connection-lost result; the terminal guard then restores local modes.\n* The headless server is explicitly designed to continue after client disconnect, so PTY processes are not coupled to a viewing terminal.\n* Broken render clients are removed, and removal can trigger shared PTY resize recalculation.\n* PTY actors exit when either command channel disconnects, when shutdown is requested, or when a PTY read returns zero/error; the optional reader-exit callback is then invoked.\n\n**SPT applicability:** reuse the separation between PTY lifetime and renderer/client lifetime if SPT sessions are intended to survive UI disconnects. On reconnect, never reuse the old client’s differential baseline: initialize a new encoder/full frame for the new terminal surface.\n\n## Dependencies and architecture relevance\n\n* `ratatui 0.30`: creates the semantic cell buffer and supplies full-screen terminal initialization/restoration. Relevant if SPT already uses or can adopt a rectangular frame abstraction; otherwise the whole dependency is excessive.\n* `crossterm 0.29`: cross-platform terminal events and mode control. Relevant for cleanup and input/resize portability, but not necessary merely to fix stale cells.\n* patched `portable-pty 0.9.0`: actual PTY creation/resizing, with vendored platform code. Relevant to PTY lifecycle and resize, not to cell clearing.\n* `unicode-width 0.2`: critical if SPT’s stale artifacts involve wide Unicode/grapheme replacement.\n* Herdr’s client/server protocol, Kitty graphics cache, OSC 8 hyperlinks, agent/workspace architecture, and multiplexer persistence are irrelevant to the narrow SPT stale-cell problem.\n\n## Concrete recommendations for SPT\n\n1. Define an explicit terminal-surface generation containing `(width, height, cells/rows, cursor state)`; do not treat output as an append-only string stream.\n2. Preserve spaces exactly. Remove any `trim()`, `trim_end()`, line-splitting/rejoining, or JSON-to-text normalization on the render path.\n3. Keep one baseline per connected terminal surface. New connection, reconnect, resize, alternate-screen transition, focus-triggered surface loss, or detected write failure must invalidate it.\n4. First frame after invalidation: synchronized-output begin (where supported), hide cursor, `CSI 2 J`, home, emit the full rectangle, restore final cursor, synchronized-output end.\n5. Subsequent frames: compare display cells. Emit changed cells, including spaces. If SPT stores variable-length lines rather than a rectangle, use `CSI K` to erase the vacated suffix.\n6. Track wide-cell occupancy using terminal display width. Repaint the full union of old/new glyph widths when either side is wide.\n7. Separate encode from commit. Advance the baseline only after the transport has accepted—and ideally successfully flushed—the bytes. A dropped/backpressured render must remain pending or force the next frame full.\n8. Install one idempotent teardown guard covering raw mode, alternate screen, line wrap, cursor visibility/shape, mouse/focus/paste modes, and any keyboard enhancements. Run it on normal exit, exceptions/panics, disconnect, and termination signals that the runtime permits.\n9. Treat PTY resize as ordered with frame generation: resize PTY/emulator, obtain a frame for the new dimensions, reset baseline, then full repaint. Coalesce rapid resize notifications.\n10. Do **not** copy Herdr’s entire Ratatui/client-server stack for this bug. Port the encoder invariants and lifecycle boundaries, not the multiplexer architecture.\n\n## Caveats found\n\n* Herdr’s thin semantic client ignores errors from `write_encoded_frame_with_graphics`, `stdout.write_all`, and `flush`, then commits its local `BlitEncoder` baseline. That is weaker than its server queue logic and can theoretically produce a false baseline after a partial/failed host-terminal write. SPT should improve on this: commit only on successful write/flush, otherwise invalidate and full-redraw on recovery.\n* Herdr’s differential renderer does not emit EL. It is safe only because `FrameData` is rectangular and blanks are real cells. Copying only the diff loop while feeding variable-length strings would reproduce stale suffixes.\n* `ratatui::init()` panics on initialization failure according to the official 0.30 docs. A load-bearing SPT path may prefer `try_init()`/explicit error handling.\n* Synchronized output is terminal-dependent. It reduces tearing but must not be considered the mechanism that clears stale cells.\n* The repository is AGPL-3.0-or-later. Reusing design ideas is straightforward, but copying implementation code into a differently licensed SPT project requires license review.",
  "sources": [
    {
      "repo": "https://github.com/ogulcancelik/herdr/tree/02a6e874f67800891b5a549297219ed6f3ce0f2f",
      "path": "Cargo.toml",
      "line_start": 24,
      "line_end": 46,
      "excerpt": "crossterm = \"0.29\"\n...\nportable-pty = \"=0.9.0\"\n...\nratatui = { version = \"0.30\", features = [\"unstable-rendered-line-info\"] }\n...\nunicode-width = \"0.2\"\n...\n[patch.crates-io]\nportable-pty = { path = \"vendor/portable-pty\" }"
    },
    {
      "repo": "https://github.com/ogulcancelik/herdr/tree/02a6e874f67800891b5a549297219ed6f3ce0f2f",
      "path": "src/protocol/render_ansi.rs",
      "line_start": 1,
      "line_end": 27,
      "excerpt": "//! Frame blitting — renders FrameData to the terminal using diff-based updates.\n//!\n//! The blitting strategy:\n//! 1. On the first frame, write the entire buffer (full redraw).\n//! 2. On subsequent frames, diff against the last frame and only write\n//!    the cells that changed.\n//! 3. Wrap each frame in synchronized output so terminals that support it do\n//!    not expose intermediate cursor positions while the frame is painted.\n...\n//! The goal is minimal output: skip unchanged cells, batch adjacent changes,\n//! and minimize cursor movement."
    },
    {
      "repo": "https://github.com/ogulcancelik/herdr/tree/02a6e874f67800891b5a549297219ed6f3ce0f2f",
      "path": "src/protocol/render_ansi.rs",
      "line_start": 48,
      "line_end": 120,
      "excerpt": "/// Stateful encoder that diffs semantic frames into terminal ANSI bytes.\n#[derive(Default)]\npub(crate) struct BlitEncoder {\n    last_frame: Option<FrameData>,\n    last_visible_cursor: Option<(u16, u16)>,\n    last_cursor_shape: u8,\n}\n...\nlet full = force_full\n    || prev.is_none()\n    || prev.is_some_and(|p| p.width != frame.width || p.height != frame.height);\n...\npub(crate) fn commit(&mut self, frame: FrameData, encoded: EncodedBlit) {\n    self.last_visible_cursor = encoded.next_last_visible_cursor;\n    self.last_cursor_shape = encoded.next_last_cursor_shape;\n    self.last_frame = Some(frame);\n}"
    },
    {
      "repo": "https://github.com/ogulcancelik/herdr/tree/02a6e874f67800891b5a549297219ed6f3ce0f2f",
      "path": "src/protocol/render_ansi.rs",
      "line_start": 417,
      "line_end": 468,
      "excerpt": "// On first frame or size change, do a full redraw.\nlet full_redraw =\n    prev.is_none() || prev.is_some_and(|p| p.width != frame.width || p.height != frame.height);\n...\nlet _ = writer.write_all(b\"\\x1b[?2026h\");\n...\nlet _ = writer.write_all(b\"\\x1b[?25l\");\n...\nif full_redraw {\n    // Clear the screen and write all cells.\n    let _ = writer.write_all(b\"\\x1b[2J\\x1b[H\");\n    write_all_cells(&mut writer, frame);\n} else {\n    // Diff-based update: only write changed cells.\n    let prev = prev.unwrap();\n    write_changed_cells(&mut writer, frame, prev);\n}\n...\nlet _ = writer.write_all(b\"\\x1b[?2026l\");"
    },
    {
      "repo": "https://github.com/ogulcancelik/herdr/tree/02a6e874f67800891b5a549297219ed6f3ce0f2f",
      "path": "src/protocol/render_ansi.rs",
      "line_start": 683,
      "line_end": 768,
      "excerpt": "fn write_cell(\n    writer: &mut impl Write,\n    cursor_position: Option<(u16, u16)>,\n    cell: &CellData,\n...\n) {\n    if cell.skip {\n        return;\n    }\n...\n    let _ = writer.write_all(cell.symbol.as_bytes());\n}\n...\nif !cell.skip\n    && (!cells_visually_equal(..., cell, ..., prev_cell) || invalidated > 0)\n    && to_skip == 0\n{\n    ...\n    write_cell(..., cell, ...);\n...\n}\n...\nlet affected_width = cmp::max(cell_width(cell), cell_width(prev_cell));\ninvalidated = cmp::max(affected_width, invalidated).saturating_sub(1);"
    },
    {
      "repo": "https://github.com/ogulcancelik/herdr/tree/02a6e874f67800891b5a549297219ed6f3ce0f2f",
      "path": "src/protocol/wire.rs",
      "line_start": 421,
      "line_end": 424,
      "excerpt": "pub struct CellData {\n    /// Grapheme cluster displayed in this cell (usually 1–2 chars).\n    pub symbol: String,"
    },
    {
      "repo": "https://github.com/ogulcancelik/herdr/tree/02a6e874f67800891b5a549297219ed6f3ce0f2f",
      "path": "src/protocol/wire.rs",
      "line_start": 475,
      "line_end": 533,
      "excerpt": "/// Creates a `FrameData` from a ratatui `Buffer` and optional cursor.\n...\ncells.push(CellData {\n    symbol: cell.symbol().to_owned(),\n    fg: color_to_u32(cell.fg),\n    bg: color_to_u32(cell.bg),\n    modifier: modifier_to_u16(cell.modifier),\n...\nFrameData {\n    cells,\n    width,\n    height,"
    },
    {
      "repo": "https://github.com/ogulcancelik/herdr/tree/02a6e874f67800891b5a549297219ed6f3ce0f2f",
      "path": "src/server/render_stream.rs",
      "line_start": 12,
      "line_end": 62,
      "excerpt": "/// Per-client render baseline for the negotiated render encoding.\npub(crate) enum ClientRenderState {\n    /// Semantic clients compare full frame data and skip identical frames.\n    Semantic { last_frame: Option<FrameData> },\n    /// Terminal-ANSI clients keep a terminal diff encoder and sequence number.\n    TerminalAnsi { blit_encoder: BlitEncoder, seq: u64 },\n}\n...\npub(crate) fn reset_baseline(&mut self) {\n    match self {\n        Self::Semantic { last_frame } => *last_frame = None,\n        Self::TerminalAnsi { blit_encoder, .. } => *blit_encoder = BlitEncoder::new(),\n    }\n}"
    },
    {
      "repo": "https://github.com/ogulcancelik/herdr/tree/02a6e874f67800891b5a549297219ed6f3ce0f2f",
      "path": "src/server/headless.rs",
      "line_start": 3518,
      "line_end": 3531,
      "excerpt": "match writer.render.try_send(serialized) {\n    Ok(()) => {\n        client.clear_deferred_render();\n        client.render_state.commit_sent_frame(prepared);\n        crate::render_prof::event(\"retained_send.sent\");\n        ...\n        true\n    }\n    Err(std::sync::mpsc::TrySendError::Full(_)) => {\n        client.defer_full_render();"
    },
    {
      "repo": "https://github.com/ogulcancelik/herdr/tree/02a6e874f67800891b5a549297219ed6f3ce0f2f",
      "path": "src/client/mod.rs",
      "line_start": 339,
      "line_end": 423,
      "excerpt": "fn setup_terminal_with_capabilities(\n    enable_client_protocols: bool,\n    mouse_capture: bool,\n) -> io::Result<TerminalGuard> {\n    ratatui::init();\n...\n    execute!(io::stdout(), DisableLineWrap)?;\n\n    Ok(TerminalGuard {\n...\n/// Guard that restores the terminal when dropped.\nstruct TerminalGuard {"
    },
    {
      "repo": "https://github.com/ogulcancelik/herdr/tree/02a6e874f67800891b5a549297219ed6f3ce0f2f",
      "path": "src/client/mod.rs",
      "line_start": 561,
      "line_end": 652,
      "excerpt": "let _ = execute!(\n    io::stdout(),\n    EnableLineWrap,\n    DisableFocusChange,\n    DisableBracketedPaste,\n    DisableMouseCapture\n);\nlet _ = crate::terminal_modes::clear_host_mouse_reporting(&mut io::stdout());\n...\nratatui::restore();\nlet _ = write_terminal_restore_postlude(&mut io::stdout(), reset_host_color_scheme_reports);\n...\nimpl Drop for TerminalGuard {\n    fn drop(&mut self) {\n        restore_terminal_state(...);\n    }\n}"
    },
    {
      "repo": "https://github.com/ogulcancelik/herdr/tree/02a6e874f67800891b5a549297219ed6f3ce0f2f",
      "path": "src/main.rs",
      "line_start": 750,
      "line_end": 770,
      "excerpt": "let original_hook = std::panic::take_hook();\n...\nstd::panic::set_hook(Box::new(move |info| {\n    tracing::error!(\"PANIC: {info}\");\n...\n    let _ = execute!(\n        io::stdout(),\n        DisableFocusChange,\n        DisableBracketedPaste,\n        DisableMouseCapture\n    );\n...\n    ratatui::restore();\n    original_hook(info);\n}));"
    },
    {
      "repo": "https://github.com/ogulcancelik/herdr/tree/02a6e874f67800891b5a549297219ed6f3ce0f2f",
      "path": "src/client/mod.rs",
      "line_start": 1496,
      "line_end": 1532,
      "excerpt": "ClientLoopEvent::Resize(new_cols, new_rows, cell_width_px, cell_height_px) => {\n    state.reported_size = (new_cols, new_rows);\n    let msg = ClientMessage::Resize {\n        cols: new_cols,\n        rows: new_rows,\n        cell_width_px,\n        cell_height_px,\n    };\n...\nlet _ = write_encoded_frame_with_graphics(&mut stdout, &encoded.bytes, graphics);\nlet _ = stdout.flush();\nstate.blit_encoder.commit(frame_data, encoded);"
    },
    {
      "repo": "https://github.com/ogulcancelik/herdr/tree/02a6e874f67800891b5a549297219ed6f3ce0f2f",
      "path": "src/client/mod.rs",
      "line_start": 2078,
      "line_end": 2106,
      "excerpt": "/// Polls the terminal size and sends resize events when it changes.\nfn resize_poll_loop(...)\n{\n...\nwhile !should_quit.load(Ordering::Acquire) {\n    std::thread::sleep(Duration::from_millis(100));\n    let new_size = current_terminal_geometry(kitty_graphics_enabled);\n    if new_size != last_size {\n        last_size = new_size;\n        if resize_tx.blocking_send(ClientLoopEvent::Resize(...)).is_err() {\n            break;\n        }\n    }\n}"
    },
    {
      "repo": "https://github.com/ogulcancelik/herdr/tree/02a6e874f67800891b5a549297219ed6f3ce0f2f",
      "path": "src/server/client_transport.rs",
      "line_start": 619,
      "line_end": 645,
      "excerpt": "let msg: ClientMessage = match protocol::read_message(&mut stream, MAX_GRAPHICS_FRAME_SIZE) {\n    Ok(msg) => msg,\n    Err(protocol::FramingError::UnexpectedEof) => {\n        // Client disconnected.\n        let _ = server_event_tx.blocking_send(ServerEvent::ClientDisconnected { client_id });\n        break;\n    }\n...\n    Err(err) => {\n        debug!(client_id, err = %err, \"client read error, closing\");\n        let _ = server_event_tx.blocking_send(ServerEvent::ClientDisconnected { client_id });\n        break;\n    }\n};"
    },
    {
      "repo": "https://github.com/ogulcancelik/herdr/tree/02a6e874f67800891b5a549297219ed6f3ce0f2f",
      "path": "src/pty/actor/unix.rs",
      "line_start": 607,
      "line_end": 646,
      "excerpt": "fn apply_pending_controls(&mut self) {\n    let (resize, nudge) = {\n        let mut controls = self.controls.lock().unwrap_or_else(|poisoned| poisoned.into_inner());\n        (controls.resize.take(), controls.nudge.take())\n    };\n...\n    if let Some(request) = resize {\n        self.resize(request.resize);\n        self.enqueue_terminal_responses(request.terminal_responses);\n    }\n...\nfn read_once(&mut self) -> bool {\n...\n    match self.file.read(&mut buf) {\n        Ok(0) => false,\n...\n        Err(err) => {\n            debug!(pane = self.pane_id, err = %err, \"PTY actor read failed\");\n            false\n        }"
    },
    {
      "repo": "https://docs.rs/ratatui/0.30.0/ratatui/fn.init.html",
      "path": "ratatui 0.30.0 official API documentation: init module",
      "line_start": 1,
      "line_end": 1,
      "excerpt": "`init` - Creates a terminal with reasonable defaults including alternate screen and raw mode. Panics on failure. ... `restore` - Restores the terminal to its original state. Prints errors to stderr but does not panic. ... All initialization functions install a panic hook that automatically restores the terminal state before panicking."
    }
  ],
  "api": [
    {
      "signature": "pub(crate) fn encode(&self, frame: &FrameData, force_full: bool) -> EncodedBlit",
      "description": "Encodes a semantic frame as a full or differential ANSI blit without changing the stored baseline."
    },
    {
      "signature": "pub(crate) fn encode_with_suppressed_visible_cursor(\n        &self,\n        frame: &FrameData,\n        force_full: bool,\n    ) -> EncodedBlit",
      "description": "Encodes a frame while suppressing the visible host cursor."
    },
    {
      "signature": "pub(crate) fn commit(&mut self, frame: FrameData, encoded: EncodedBlit)",
      "description": "Commits the frame and cursor metadata as the new differential baseline."
    },
    {
      "signature": "pub(crate) fn is_current(&self, frame: &FrameData) -> bool",
      "description": "Checks whether a semantic frame matches the committed baseline."
    },
    {
      "signature": "pub(crate) fn reset_baseline(&mut self)",
      "description": "Invalidates a per-client semantic or ANSI render baseline."
    },
    {
      "signature": "pub(crate) fn prepare_frame(&mut self, frame: FrameData) -> Option<PreparedRender>",
      "description": "Prepares a semantic or terminal-ANSI render while allowing identical frames to be skipped."
    },
    {
      "signature": "pub(crate) fn commit_sent_frame(&mut self, prepared: PreparedRender)",
      "description": "Commits a prepared frame after successful render-channel enqueue."
    },
    {
      "signature": "fn setup_terminal(mouse_capture: bool) -> io::Result<TerminalGuard>",
      "description": "Initializes the thin-client terminal and returns an RAII restoration guard."
    },
    {
      "signature": "fn restore_terminal_state(\n    reset_modify_other_keys: bool,\n    reset_host_color_scheme_reports: bool,\n    #[cfg(windows)] restore_windows_input_mode: Option<u32>,\n)",
      "description": "Best-effort comprehensive terminal mode restoration used by TerminalGuard::drop."
    },
    {
      "signature": "pub(crate) fn resize(\n        &self,\n        rows: u16,\n        cols: u16,\n        cell_width_px: u32,\n        cell_height_px: u32,\n        terminal_responses: Vec<Bytes>,\n    )",
      "description": "Coalesces and wakes the Unix PTY actor for a size/pixel-geometry change."
    }
  ],
  "version": "herdr 0.7.4, commit 02a6e874f67800891b5a549297219ed6f3ce0f2f (dependencies: ratatui 0.30, crossterm 0.29, patched portable-pty 0.9.0, unicode-width 0.2)",
  "breaking_changes": [],
  "caveats": [
    "Herdr is AGPL-3.0-or-later; copying code into SPT requires license compatibility review.",
    "The differential path has no erase-in-line command; correctness depends on complete rectangular FrameData and explicit blank cells.",
    "The thin semantic client ignores stdout write/flush errors and still commits its baseline; SPT should not copy that behavior.",
    "Synchronized-output support varies by terminal and is a tearing mitigation, not a stale-cell clearing mechanism.",
    "Ratatui 0.30 `init()` panics on initialization failure; a load-bearing integration may prefer its fallible initialization API.",
    "Herdr’s wide-glyph correctness depends on display-width tracking and continuation-cell metadata; a string-index diff is not equivalent."
  ]
}