Skip to content

feat(graphics): add direct pane frame streaming - #2523

Merged
ogulcancelik merged 6 commits into
masterfrom
feat/shared-pane-frames-kiss
Aug 8, 2026
Merged

feat(graphics): add direct pane frame streaming#2523
ogulcancelik merged 6 commits into
masterfrom
feat/shared-pane-frames-kiss

Conversation

@ogulcancelik

Copy link
Copy Markdown
Collaborator

Summary

  • add bounded named pane-graphics layers with streaming, placement, visibility, and exact pixel input metadata
  • add acknowledged direct Kitty regular-file frames for audited local terminal clients, with owned fallback and cleanup
  • keep ordinary terminal Kitty graphics on the legacy byte-for-byte path when pane graphics are unused
  • document the socket API and generated schema additions

Validation

  • just check (3321 Rust tests plus integration, marketplace, Windows clippy, and maintenance checks)
  • deterministic legacy terminal-image transcript comparison against pre-change origin/master
  • real Kitty and Ghostty tests on Linux, plus real Kitty tests on macOS
  • Kitty-disabled, enabled-but-unused, ordinary-image, inline-stream, direct-stream, disconnect, resize, visibility, and bounded soak checks

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: fbf2bc16-fbc4-41a5-aa01-d984212a58da

📥 Commits

Reviewing files that changed from the base of the PR and between 1ffc2a2 and c4c522f.

📒 Files selected for processing (3)
  • src/app/mod.rs
  • src/app/state.rs
  • src/main.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/main.rs
  • src/app/mod.rs
  • src/app/state.rs

📝 Walkthrough

Walkthrough

The change adds layered pane graphics, direct Kitty file transport, bounded retained rendering, BGRA normalization, frame acknowledgements, pixel-aware mouse input, and placement-only resize replay.

Changes

Pane graphics platform

Layer / File(s) Summary
Graphics contracts and runtime
docs/next/api/herdr-api.schema.json, src/api/..., src/app/...
Graphics requests support named layers, z-ordering, BGRA, direct frames, stream ownership, limits, visibility, and frame acknowledgements.
Rendering and direct files
src/kitty_graphics.rs, src/pane_graphics_files.rs, src/server/headless/pane_graphics.rs
Rendering uses bounded incremental transactions, placement replay, validated file leases, direct-transfer gates, and inline fallback.
Client and server transport
src/protocol/wire.rs, src/client/..., src/server/client_transport.rs, src/server/headless.rs
The protocol carries direct-graphics lifecycle events and pixel input. The client filters Kitty responses, and the server validates transfer and pixel-input state.

Pixel mouse input

Layer / File(s) Summary
Coordinate model and routing
src/input/..., src/pane/..., src/app/input/..., src/client/input.rs
Mouse handling uses structured cell or pixel positions, preserves SGR pixel coordinates, maps host geometry into panes, and routes validated reports.

Documentation and validation

Layer / File(s) Summary
Documentation and regression coverage
docs/next/CHANGELOG.md, docs/next/website/src/content/docs/socket-api.mdx, src/**/tests/*
Documentation describes capability discovery, direct transport, fallback behavior, acknowledgements, damage metadata, and resize replay. Tests cover limits, ownership, filtering, transfer lifecycle, rendering bounds, and pixel input.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary change: direct pane frame streaming for graphics.
Description check ✅ Passed The description directly covers the graphics streaming, fallback, protocol, documentation, and validation changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/shared-pane-frames-kiss

Comment @coderabbitai help to get the list of available commands.

@kangal-bot kangal-bot added the ai-review Trigger automated AI reviews for pull requests admitted by the PR gate label Aug 8, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/server/headless.rs (1)

3057-3072: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Pass the preserved cell size to runtime.resize, not the raw event values.

Lines 3061-3063 keep the stored cell_size when the client reports an unknown cell size. Line 3071 then calls runtime.resize(rows, cols, cell_width_px, cell_height_px) with the raw event values. When cell_width_px and cell_height_px are zero, the runtime loses its pixel geometry while client.cell_size still holds the last known value. The two then disagree.

attach_terminal_client already uses the stored value at line 2702: runtime.resize(rows, cols, cell_size.width_px, cell_size.height_px). Apply the same rule here.

🐛 Proposed fix: capture and forward the effective cell size
-                let direct_terminal_id = if let Some(ClientConnection {
+                let direct_terminal = if let Some(ClientConnection {
                     mode: ClientConnectionMode::TerminalAttach { terminal_id },
                     terminal_size,
                     cell_size,
                     render_state,
                     ..
                 }) = self.clients.get_mut(&client_id)
                 {
                     *terminal_size = (cols, rows);
                     let observed = crate::kitty_graphics::HostCellSize {
                         width_px: cell_width_px,
                         height_px: cell_height_px,
                     };
                     if observed.is_known() {
                         *cell_size = observed;
                     }
                     render_state.request_repaint();
-                    Some(terminal_id.clone())
+                    Some((terminal_id.clone(), *cell_size))
                 } else {
                     None
                 };
-                if let Some(terminal_id) = direct_terminal_id {
+                if let Some((terminal_id, effective_cell_size)) = direct_terminal {
                     if let Some(runtime) = self.runtime_for_terminal_id_string(&terminal_id) {
-                        runtime.resize(rows, cols, cell_width_px, cell_height_px);
+                        runtime.resize(
+                            rows,
+                            cols,
+                            effective_cell_size.width_px,
+                            effective_cell_size.height_px,
+                        );
                     }
                     return true;
                 }
src/client/mod.rs (1)

562-579: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reset DEC mode 1016 on Windows when disabling mouse capture.

clear_host_mouse_reporting is a no-op on Windows, so set_mouse_capture(false, ...) emits no \x1b[?1016l. If pixel reporting was enabled, Windows can continue sending pixel coordinates while host_sgr_pixels_active is false.

🧹 Nitpick comments (16)
src/server/headless/pane_graphics.rs (1)

543-555: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Record the oversized-serialization fallback.

Err(_) => return RetainedGraphicsOutcome::Fallback on Line 553 is the only fallback path in this function without a crate::render_prof::event(...) marker; the paths on Lines 475, 486, 499, 507, 511, 515 and 519 all emit one. The previous tracing::warn for oversized serialization was also removed. An oversized pane-graphics frame now triggers a full redraw with no signal in either profile events or logs.

Add an event marker, and a warn! if you want the payload size in the logs.

♻️ Proposed change
                 ) {
                     Ok(serialized) => Some(serialized),
-                    Err(_) => return RetainedGraphicsOutcome::Fallback,
+                    Err(err) => {
+                        crate::render_prof::event("retained_graphics_fallback.oversized");
+                        warn!(client_id, err = %err, "pane graphics frame exceeded the graphics frame limit");
+                        return RetainedGraphicsOutcome::Fallback;
+                    }
                 }

As per coding guidelines: "use tracing for logging".

Source: Coding guidelines

src/server/headless.rs (3)

3133-3147: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the now-dead deferred_render computation.

Both arms of the match at lines 3144-3146 return RenderImpact::Full. The lookup at lines 3134-3140 therefore has no effect on the result. It still runs a HashMap lookup on every server event.

♻️ Proposed refactor
     fn handle_server_event_with_render_impact(&mut self, ev: ServerEvent) -> RenderImpact {
-        let deferred_render = match &ev {
-            ServerEvent::ClientWriterDrained { client_id } => self
-                .clients
-                .get(client_id)
-                .map_or(DeferredRender::None, ClientConnection::deferred_render),
-            _ => DeferredRender::None,
-        };
         if !self.handle_server_event(ev) {
             return RenderImpact::None;
         }
-        match deferred_render {
-            DeferredRender::None | DeferredRender::Full => RenderImpact::Full,
-        }
+        RenderImpact::Full
     }

Check whether DeferredRender and the deferred_render accessor remain used elsewhere before you drop the import.


3767-3800: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Deduplicate the focused-pane lookup and skip it when no client supports pixel mouse.

stream_host_mouse_capture_mode runs on every event-loop iteration. Lines 3772-3794 repeat the active-workspace and focused-pane traversal that focused_pane_graphics_demand already performs at lines 3759-3764, then resolve the runtime and read its input state.

client_sgr_pixels at line 3800 is false for every client that does not set pixel_mouse. When no attached client sets it, the entire sgr_pixels computation is discarded. Compute it lazily.

♻️ Proposed refactor
+    fn focused_pane_sgr_pixels_requested(&self) -> bool {
+        let Some(ws_idx) = self.app.state.active else {
+            return false;
+        };
+        let Some(pane_id) = self
+            .app
+            .state
+            .workspaces
+            .get(ws_idx)
+            .and_then(crate::workspace::Workspace::focused_pane_id)
+        else {
+            return false;
+        };
+        if !self.app.pane_graphics.active_for_pane(pane_id) {
+            return false;
+        }
+        self.app
+            .state
+            .runtime_for_pane_in_workspace(&self.app.terminal_runtimes, ws_idx, pane_id)
+            .and_then(crate::terminal::TerminalRuntime::input_state)
+            .is_some_and(|state| {
+                state.mouse_protocol_encoding == crate::input::MouseProtocolEncoding::SgrPixels
+            })
+    }
+
     fn stream_host_mouse_capture_mode(&mut self) {
         let enabled = self
             .app
             .state
             .should_capture_host_mouse_from(&self.app.terminal_runtimes);
-        let sgr_pixels = self.focused_pane_graphics_demand()
-            && self
-                .app
-                .state
-                .active
-                .and_then(|ws_idx| {
-                    ...
-                })
-                .and_then(crate::terminal::TerminalRuntime::input_state)
-                .is_some_and(|state| {
-                    state.mouse_protocol_encoding == crate::input::MouseProtocolEncoding::SgrPixels
-                });
+        let any_pixel_mouse_client = self
+            .clients
+            .values()
+            .any(|client| client.is_full_app_client() && client.pixel_mouse);
+        let sgr_pixels = any_pixel_mouse_client && self.focused_pane_sgr_pixels_requested();

5581-5581: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for a client that negotiates direct graphics.

Every ClientConnected fixture in this module sets direct_graphics: false. No test constructs a client with direct_graphics: true, and no test exercises the ServerEvent::ClientInputPixels arm at lines 2900-2928.

That arm contains the validation chain for pixel reports, including the geometry consistency check flagged separately. Add tests that cover:

  • A client with direct_graphics: true sets both connection.direct_graphics and connection.pixel_mouse.
  • direct_graphics_available() returns true for exactly one such foreground app client, and false when a second app client attaches.
  • ClientInputPixels is rejected when host_sgr_pixels_active is not Some(true), when terminal_size disagrees with geometry, and when the geometry reports zero columns.

The AppState and Workspace test constructors already used in this module support these cases without a PTY.

Source: Coding guidelines

src/app/api/pane_graphics.rs (1)

421-443: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document why stream_active is taken rather than cloned.

Slot::drop stores false into stream_active. The insert on Line 427 drops the replaced slot. take() on Line 425 moves the handle out first, so the drop cannot deactivate the live stream. Without that take, every streamed frame would close its own stream.

The invariant is not obvious from the code. Add a short comment so a later refactor does not change take() to clone().

♻️ Proposed comment
         let (stream_owner, stream_active) = self
             .pane_graphics
             .slots
             .get_mut(&key)
+            // Move the handle out: `insert` below drops the replaced slot, and
+            // `Slot::drop` would otherwise deactivate the live stream.
             .map(|slot| (slot.stream_owner.clone(), slot.stream_active.take()))
             .unwrap_or_default();
src/app/pane_graphics.rs (1)

278-318: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for attach_stream_active and retain_live_panes.

The test module covers the two capacity bounds only. attach_stream_active and retain_live_panes carry the lifecycle rules that decide whether a layer can ever be reclaimed, and neither has a test. A test that opens two layers for one owner and then asserts both slots share the attached handle would have caught the defect flagged on Line 159-171.

src/app/state.rs (1)

1496-1497: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert host_mouse_pixels in assert_invariants_for_test.

host_mouse_pixels is transient state that must not survive a dispatch. assert_invariants_for_test already asserts that comparable transient fields such as drag, selection_autoscroll, and right_click_passthrough are absent for an empty state. Add the same assertion so a future change that leaks the provenance window fails a test.

The coding guidelines ask for the invariant test helpers to cover state refactors.

♻️ Proposed assertion in the empty-state block
             assert!(
                 self.context_menu.is_none(),
                 "empty app state must not keep context menu"
             );
+            assert!(
+                self.host_mouse_pixels.is_none(),
+                "empty app state must not keep host mouse pixel provenance"
+            );
             return;

Source: Coding guidelines

src/client/input.rs (1)

285-294: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a log for the dropped pixel mouse report.

If sgr_pixels is true and geometry is None, classify_unix_input returns None. send_unix_input_chunks then drops the chunk at Line 267 without any record. Geometry stays None when HostGeometry::current() never succeeded, so every mouse event disappears silently in that state. Add a tracing line to make the condition diagnosable.

♻️ Proposed log on the drop path
 fn classify_unix_input(
     data: Vec<u8>,
     sgr_pixels: bool,
     geometry: Option<crate::input::mouse::HostGeometry>,
 ) -> Option<ClientLoopEvent> {
     if sgr_pixels && crate::input::mouse::parse_report(&data).is_some() {
+        if geometry.is_none() {
+            tracing::debug!("dropping SGR pixel mouse report: host geometry unavailable");
+        }
         return geometry.map(|geometry| ClientLoopEvent::PixelMouse(data, geometry));
     }
     Some(ClientLoopEvent::StdinInput(data))
 }

As per coding guidelines: "use tracing for logging".

Source: Coding guidelines

src/client/mod.rs (3)

367-387: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Collapse the duplicated set_mouse_capture branches.

Both branches at Line 368-372 and Line 382-386 call set_mouse_capture with the same second argument and differ only in the first. Pass mouse_capture directly.

♻️ Proposed simplification
     if enable_client_protocols {
-        if mouse_capture {
-            set_mouse_capture(true, false)?;
-        } else {
-            set_mouse_capture(false, false)?;
-        }
+        set_mouse_capture(mouse_capture, false)?;
         execute!(io::stdout(), EnableBracketedPaste, EnableFocusChange)?;
@@
-        if mouse_capture {
-            set_mouse_capture(true, false)?;
-        } else {
-            set_mouse_capture(false, false)?;
-        }
+        set_mouse_capture(mouse_capture, false)?;
     }

718-733: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Consider blocking GNU screen alongside tmux.

direct_graphics_profile_allowed treats TMUX as a blocked transport at Line 730. GNU screen is the equivalent multiplexer and sets STY. Under screen the terminal that renders the frame can differ from the process that wrote the file, which is the same risk that the TMUX check addresses. Add STY to the blocked set.

The failure mode is a rejected Kitty file transfer and a fallback, so this is a robustness improvement rather than a correctness break.

♻️ Proposed change
         direct_attach
             || is_remote_client_process()
             || std::env::var_os("SSH_CONNECTION").is_some()
             || std::env::var_os("SSH_TTY").is_some()
-            || std::env::var_os("TMUX").is_some(),
+            || std::env::var_os("TMUX").is_some()
+            || std::env::var_os("STY").is_some(),

1825-1838: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Pass next_sgr_pixels to set_mouse_capture.

Line 1829 passes sgr_pixels, and Line 1838 stores next_sgr_pixels. The two agree today because set_mouse_capture ignores the second argument when enabled is false. Passing the same value in both places keeps the tracked state and the emitted modes aligned if the enable logic changes.

♻️ Proposed change
                     if mouse_mode_changed {
-                        set_mouse_capture(enabled, sgr_pixels)
+                        set_mouse_capture(enabled, next_sgr_pixels)
                             .map_err(ClientError::ConnectionFailed)?;
src/kitty_graphics.rs (1)

272-274: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Static pane-graphics scenes cost O(N) transactions and O(N) cache clones per frame. Two changes combine into one per-frame cost. request_placement_replay() runs unconditionally on every incremental encode, which marks every placement as needing a re-display. The incremental encoder then clones the whole cache once per candidate placement while it walks that work list, and paint_local_pane_graphics re-enters the encoder once per emitted transaction.

  • src/kitty_graphics.rs#L272-L274: gate request_placement_replay() on a view change or a full-redraw request, matching the legacy path at Line 242-244. If the host text blit overwrites the graphics surface every frame, keep the unconditional call and record that reason in a comment.
  • src/kitty_graphics.rs#L567-L607: remove the speculative cache.clone() at Line 588 by splitting encode_placement_update into a decision step and an apply step, so the cache is mutated only when a transaction is committed.
src/pane_graphics_files.rs (4)

136-144: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Stale generation directories survive an abnormal exit.

Drop removes the generation root only on a graceful shutdown. If the process receives SIGKILL or aborts, the server-{pid}-{nonce} directory and its frame files remain under the runtime base. Nothing in this module reaps them, and create_generation at Line 181 creates the base without sweeping siblings.

Each leaked directory holds raw pane pixel data at 0600. The data is same-user readable only, so this is a disk-growth and stale-data-retention concern rather than an exposure. On /var/tmp there is no reboot cleanup, unlike XDG_RUNTIME_DIR.

Add a startup sweep that removes server-* directories whose pid is no longer alive.

♻️ Sketch of a startup sweep
 fn create_generation(base: &Path) -> io::Result<Generation> {
@@
         fs::create_dir_all(base)?;
         fs::set_permissions(base, fs::Permissions::from_mode(DIRECTORY_MODE))?;
         validate_directory(base)?;
+        remove_stale_generations(base);
         let nonce = std::time::SystemTime::now()
#[cfg(unix)]
fn remove_stale_generations(base: &Path) {
    let Ok(entries) = fs::read_dir(base) else {
        return;
    };
    for entry in entries.flatten() {
        let Some(pid) = entry
            .file_name()
            .to_str()
            .and_then(|name| name.strip_prefix("server-"))
            .and_then(|rest| rest.split('-').next())
            .and_then(|pid| pid.parse::<i32>().ok())
        else {
            continue;
        };
        // SAFETY: `kill` with signal 0 only probes for process existence.
        if unsafe { libc::kill(pid, 0) } == 0 {
            continue;
        }
        if let Err(err) = fs::remove_dir_all(entry.path()) {
            tracing::warn!(path = %entry.path().display(), err = %err, "failed to remove stale pane graphics directory");
        }
    }
}

Note that pid reuse makes the kill probe conservative, not exact. It only ever keeps directories, so it cannot delete a live generation.


200-215: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add SAFETY comments to the libc::geteuid() blocks.

Line 207-209 wraps libc::geteuid() in unsafe with no justification comment. The same pattern repeats at Line 246 and Line 268. geteuid cannot fail and has no preconditions, so a one-line comment documents that and keeps the unsafe surface auditable.

♻️ Proposed comment
-        root.join(format!("herdr-pane-graphics-{}", unsafe {
-            libc::geteuid()
-        }))
+        // SAFETY: `geteuid` takes no arguments, cannot fail, and has no preconditions.
+        let euid = unsafe { libc::geteuid() };
+        root.join(format!("herdr-pane-graphics-{euid}"))

334-402: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add direct coverage for validate_direct_source.

The test module covers lease, which shares validate_metadata, open_no_follow, and validate_path_identity. It does not cover validate_direct_source at Line 147.

validate_direct_source is the client-side gate that decides whether a server-supplied path may be handed to the terminal. Its path-shape checks at Line 150-160 and its directory-privacy loop at Line 161-163 have no test. Those checks are pure logic and are cheap to exercise.

Cover at least these rejections: a relative path, a parent directory not named source, a generation directory without the server- prefix, and a path whose grandparent is not the runtime base.

Do you want me to generate these tests?


118-133: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Use positional reads in copy_rgba.

File::try_clone() shares the Unix file offset, so concurrent calls can interfere through rewind() and read_to_end(). Use FileExt::read_exact_at at offset 0, then use read_at for a one-byte probe at self.inner.len as u64 to retain growth detection. Gate the Unix-only API because this module also compiles on Windows. This removes the per-call descriptor clone and rewind.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d4bbea6a-d4ae-4dd8-ad0c-2f2d08d863b1

📥 Commits

Reviewing files that changed from the base of the PR and between 10974c8 and 137117e.

📒 Files selected for processing (41)
  • docs/next/CHANGELOG.md
  • docs/next/api/herdr-api.schema.json
  • docs/next/website/src/content/docs/socket-api.mdx
  • src/api/mod.rs
  • src/api/schema.rs
  • src/api/schema/panes.rs
  • src/api/schema/response.rs
  • src/api/server.rs
  • src/api/server/pane_graphics_stream.rs
  • src/app/actions.rs
  • src/app/api.rs
  • src/app/api/pane_graphics.rs
  • src/app/ids.rs
  • src/app/input/mod.rs
  • src/app/input/mouse.rs
  • src/app/mod.rs
  • src/app/pane_graphics.rs
  • src/app/runtime.rs
  • src/app/state.rs
  • src/client/direct_graphics.rs
  • src/client/input.rs
  • src/client/mod.rs
  • src/ghostty/mod.rs
  • src/input/encode.rs
  • src/input/mod.rs
  • src/input/model.rs
  • src/input/mouse.rs
  • src/kitty_graphics.rs
  • src/main.rs
  • src/pane.rs
  • src/pane/input.rs
  • src/pane/terminal.rs
  • src/pane_graphics_files.rs
  • src/protocol/wire.rs
  • src/server/alt_screen_read.rs
  • src/server/client_transport.rs
  • src/server/clients.rs
  • src/server/headless.rs
  • src/server/headless/pane_graphics.rs
  • src/server/headless/tests/pane_graphics.rs
  • src/terminal/runtime.rs
💤 Files with no reviewable changes (1)
  • src/app/actions.rs

Comment thread docs/next/CHANGELOG.md Outdated
Comment thread docs/next/website/src/content/docs/socket-api.mdx
Comment thread src/app/api/pane_graphics.rs
Comment thread src/app/api/pane_graphics.rs
Comment thread src/app/input/mouse.rs
Comment thread src/app/pane_graphics.rs
Comment thread src/server/client_transport.rs
Comment thread src/server/headless.rs
@ogulcancelik

Copy link
Copy Markdown
Collaborator Author

Review follow-up in a51df75a also:

  • preserves the last known cell pixel size for terminal-attach resizes that omit pixels
  • adds stale generation cleanup, positional file reads, direct-source validation, and audited unsafe calls
  • blocks GNU screen direct-file transport and skips pixel-mouse pane traversal when no client negotiated it
  • adds direct graphics negotiation and Windows-targeted lifecycle coverage

The Windows DEC 1016 reset note is not applicable to the current path: direct/pixel mouse negotiation is Unix-only, and Windows clients therefore never receive sgr_pixels: true.

Protocol stays at 20 intentionally: stable and current preview publish protocol 19, and project policy keeps multiple pre-publication incompatible changes on the same next protocol number.

@greptile-apps

greptile-apps Bot commented Aug 8, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds bounded pane-graphics layers and direct regular-file frame streaming for eligible local terminal clients, together with pixel mouse metadata and cleanup/acknowledgement handling.

  • Extends pane graphics APIs, schemas, documentation, and stream framing.
  • Adds validated frame-file storage and direct Kitty transport with owned inline fallback.
  • Coordinates graphics state, visibility, input, rendering, and client/server transport.

Confidence Score: 4/5

The PR is not yet safe to merge because mixed version-20 clients and servers can accept incompatible binary wire layouts and then misroute launch modes or disconnect.

The previously reported protocol compatibility failure remains: the wire format changes ClientLaunchMode, MouseCapture, and message variants while PROTOCOL_VERSION stays at 20, so the exact-version handshake does not reject binaries from opposite sides of the change.

Files Needing Attention: src/protocol/wire.rs

Important Files Changed

Filename Overview
src/protocol/wire.rs Adds direct-graphics launch and transport messages plus pixel mouse data, but the previously reported unchanged protocol-version incompatibility remains.
src/api/server/pane_graphics_stream.rs Extends dedicated streams with named layers and direct-file headers; downstream validation prevents arbitrary paths from reaching the terminal.
src/pane_graphics_files.rs Introduces private frame generations, strict ownership and metadata validation, identity checks, leases, and cleanup.
src/app/api/pane_graphics.rs Implements layered graphics limits, stream ownership, direct-frame leasing, acknowledgements, and fallback state.
src/server/headless/pane_graphics.rs Coordinates direct frame delivery, per-client capability checks, fallback, and transmission lifecycle.
src/client/direct_graphics.rs Adds client-side direct graphics transmission tracking and acknowledgement behavior.
src/kitty_graphics.rs Expands Kitty graphics encoding and cache behavior for layered, inline, and regular-file frames.

Sequence Diagram

sequenceDiagram
  participant Producer as Pane graphics producer
  participant API as Graphics stream API
  participant App as App graphics state
  participant Server as Headless server
  participant Client as Local terminal client
  participant Terminal as Kitty-compatible terminal
  Producer->>API: Open named layer stream
  API->>App: Claim layer
  Producer->>API: Submit inline frame or private file path
  API->>App: Validate and install frame
  App->>Server: Render transaction
  alt eligible direct RGBA file
    Server->>Client: GraphicsFile
    Client->>Client: Revalidate private frame path
    Client->>Terminal: Kitty regular-file command
    Terminal-->>Client: Transmission result
  else owned fallback
    Server->>Client: Inline graphics bytes
    Client->>Terminal: Kitty inline command
  end
  Client-->>Server: Frame accepted
  Server-->>API: Frame acknowledgement
  API-->>Producer: sequence/revision acknowledgement
Loading

Reviews (4): Last reviewed commit: "Merge branch 'master' into feat/shared-p..." | Re-trigger Greptile

@ogulcancelik

Copy link
Copy Markdown
Collaborator Author

Greptile protocol finding: not changing this. Protocol 20 is already the current source protocol on master, introduced for the terminal-bell wire change, while stable and the active preview both publish protocol 19. Per the repository wire-version rule, multiple incompatible changes before the next protocol is published stay on that same next number; bumping again to 21 here would violate that rule. Compatibility is guaranteed against published builds, not arbitrary intermediate master binaries.

@ogulcancelik
ogulcancelik force-pushed the feat/shared-pane-frames-kiss branch from f44ae95 to 1ffc2a2 Compare August 8, 2026 22:06
@ogulcancelik
ogulcancelik merged commit 1777e9b into master Aug 8, 2026
8 checks passed
@kangal-bot kangal-bot removed the ai-review Trigger automated AI reviews for pull requests admitted by the PR gate label Aug 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants