# PTY 包装与内联媒体 · 让复制穿透 SSH、让图片进终端

> Part 5 收官，两个「终端里居然能做到」的把戏：grok wrap 用 PTY 拦截 OSC 52 让复制穿透 SSH/容器；内联图片用 Kitty 图形协议在滚动区里显示、还能随滚动裁切滑入滑出。这一章拆这两套机制。

- 作者：David（道雾轩）
- 专栏：Grok Build 源码解读（https://daiw.org/manual/grok-build.md）
- 最后更新：2026-09-15
- 原文：https://daiw.org/manual/grok-build/pty-wrap-media
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# PTY 包装与内联媒体 · 让复制穿透 SSH、让图片进终端

Part 5 收官，看两个「终端里居然能做到」的把戏。

## `grok wrap`：让复制穿透 SSH

先纠正一个容易望文生义的点：`pty_wrap.rs` **不是**内嵌终端视图，而是 **`grok wrap` 命令的剪贴板/SSH 桥**。

问题背景：你在远程机器上、或容器里跑 grok，想把它输出的东西复制到**本地**剪贴板——终端本身不支持 OSC 52 时(比如 Apple Terminal)，这条路就断了。`grok wrap <命令>` 把命令包在一个本地 PTY 里跑(`portable-pty`)，在输出流上装一个 `Osc52Filter`：**拦截 OSC 52 剪贴板写序列**，把内容复制进**本地**剪贴板。它还设 `GROK_OSC52_SINK` + `LC_GROK_OSC52_SINK` 环境变量(后者能搭 SSH 的 `SendEnv/AcceptEnv LC_*` 顺风车)，好让远端里层的 grok **信任** OSC 52。于是「远端 agent 输出 → 本地剪贴板」就通了。顺带，它还把本地的系统明暗写进 `GROK_APPEARANCE` / `LC_GROK_APPEARANCE`，远端设成 `auto` 的主题也能跟着本地变。

<Callout type="info">
  退出安全做得很足：`TerminalRestoreGuard`(Drop)、信号循环(SIGHUP/INT/TERM 转发给子进程、恢复终端模式)、绕过 Rust stdout 锁的写(免得信号路径上死锁)。**包一个 PTY 看似简单，把终端状态在各种退出路径上都恢复干净才是真功夫。**
</Callout>

## 内联图片：Kitty 图形协议

在终端里**显示图片**靠图形协议。`GraphicsProtocol` 三选一：`Kitty` / `ITerm2` / `None`，但实际策略很保守：

- **Windows → None**(ConPTY 会吞掉 APC 转义)；
- **iTerm2 → 滚动区里关掉**(它的 OSC-1337 缺放置与清除原语，图跟不住网格)→ 退回文字占位；只有 prompt 输入框里的图片预览单独放行 iTerm2；
- 真正启用滚动区内联叠加的，只有 Kitty 协议 + Kitty / Ghostty / WezTerm 这几个终端(Warp 虽然也走 Kitty 协议，却被排除在外)；在 tmux 里则一律不开。

滚动区的图分两步：每个文件只**传输**一次(`transmit_inline_image`，各有独立的 image id，从 2 起分配)，之后每帧只发**放置**转义；图片被部分滚出屏幕时，`place_kitty_image_cropped` 在放置转义里带上**源裁切**(`x/y/w/h`)——这样一张图能随滚动**裁切着滑入滑出**。数据按 base64 切成 4096 字节一块发送。`render_kitty_image` 那种「传输 + 显示」一次到位的转义，则只给弹窗、预览这类叠加用：它们共享放置 id 1，由 `overlay.rs` 按渲染方记录所有权——同一渲染方在同一位置重复请求就不重发，所有权变更只在字节真正写出(`PostFlush`)后才提交，防止滚动时残留像素花屏。新拆出的 `xai-grok-image` crate 只管图片的校验与转码，终端显示逻辑仍在 `terminal/image.rs`。

## 视频海报：可选的 ffmpeg

视频显示不了，但能抽一帧当**海报**：`extract_poster_frame` 调 `ffmpeg -ss 1 -frames:v 1 ...` 抽首秒一帧(失败就退回取第一帧；按协议选 png/jpg)。ffmpeg 是**可选**的——没装就显示一行「装 ffmpeg」的横幅(还给出按系统的安装命令)，装上后中途也能恢复(re-probe)。高度预留和实际放置共用同一个 `inline_media_reserved_rows`，保证两者不会算不一致。

## 超链接路由

顺带一提 OSC 8 超链接：`hyperlink_route()` 按终端能力决定发不发(Apple Terminal、Warp、老 VTE(低于 0.50.4)、GNU screen、老 tmux(低于 3.4)以及认不出的终端都不发，`skip_reason` 记着原因)。链接**参与 cell diff**(在 `draw_frame` 里 `set_frame_links`)，不是带外发的。

## 和《从 LLM 到 Coding Agent》对照

这一章几乎全是「终端里怎么可能做到」的产品魔法——教学 agent 根本不碰。让复制穿透 SSH、让图片在滚动区里裁切滑动、让视频有海报，每一个都是深挖终端协议 + 兜住各种退化的硬活。它们不改变 agent 的智能，却实实在在决定了「用起来爽不爽」。

Part 5 终端 UI 深潜到此为止。下一部分[运行形态与工程](https://daiw.org/manual/grok-build/sessions)，看 grok 的会话持久化、plan/headless、ACP、后台任务与 Rust 工程底座。

---

上一篇：[主题与终端适配](https://daiw.org/manual/grok-build/theming-terminal)
