Skip to content

Add inline snapshot rendering for supported terminals (#125)#143

Draft
obj-p wants to merge 3 commits intomainfrom
worktree-issue-125-inline-snapshots
Draft

Add inline snapshot rendering for supported terminals (#125)#143
obj-p wants to merge 3 commits intomainfrom
worktree-issue-125-inline-snapshots

Conversation

@obj-p
Copy link
Copy Markdown
Owner

@obj-p obj-p commented Apr 22, 2026

Closes #125.

Summary

  • preview snapshot now renders the PNG inline in the terminal via iTerm2's OSC 1337 protocol when stdout is a TTY and the terminal is iTerm2, WezTerm, or Ghostty. The on-disk file behavior is unchanged.
  • New --inline {auto,always,never} (default auto); honors PREVIEWSMCP_INLINE.
  • tmux-aware: when $TMUX is set, probes allow-passthrough and either DCS-wraps the escape (if on) or prints a one-line stderr hint (if off).
  • Large images (>1200px on widest side) are downscaled to 1000px via CoreGraphics for display only — the file at --output stays full-resolution.

Behavior matrix

stdout mode outer tmux state result
TTY auto iTerm2/WezTerm/Ghostty no tmux image + path
TTY auto supported passthrough on wrapped image + path
TTY auto supported passthrough off path + stderr hint
TTY auto Terminal.app path only
pipe auto any any path only
any --json any any JSON only (unchanged)
any never any any path only
any always any any image + path (may garble on unsupported)

Test plan

  • 28 unit tests in Tests/PreviewsCLITests/TerminalImagesTests.swift — all pass. Covers: golden-byte iTerm2 encoding, golden-byte tmux DCS wrap with ESC-doubling, 8-case capability classification, tmux passthrough state transitions, full decision matrix (mode × TTY × capability × env-var overrides × json).
  • swift build clean.
  • Manual: in local iTerm2 (no SSH/tmux): previewsmcp snapshot Foo.swift — expect image inline, path below.
  • Manual: same, piped to cat — expect path only, no escape bytes.
  • Manual: same, with --json — expect JSON only.
  • Manual: same, with --inline never — expect path only.
  • Manual: in tmux with allow-passthrough on, local iTerm2 — expect image inline.
  • Manual: in tmux with allow-passthrough off, local iTerm2 — expect path + stderr hint, no garbage.
  • Manual: Terminal.app — expect path only (unsupported fallback).
  • Manual: preview at 2400px wide — expect inline renders downscaled; file on disk at full res.

Notes for reviewer

  • Decision logic is a pure function with injected TerminalEnvironment, TmuxProbe, and stdoutIsTTY flag — unit tests run without a real terminal. The one real I/O point (ProcessTmuxProbe) is swapped out in tests via StubTmuxProbe.
  • Kitty graphics protocol and Sixel deliberately deferred; rationale in the plan comment. Will file a follow-up for Kitty once v1 is stable.
  • tmux DCS wrapping was in scope per the plan amendment — without it the feature is broken for tmux users even when they've enabled passthrough.

🤖 Generated with Claude Code

obj-p and others added 3 commits April 29, 2026 21:04
Renders snapshot PNGs directly in the terminal via iTerm2's OSC 1337
inline-image protocol when stdout is a TTY and the terminal is
iTerm2/WezTerm/Ghostty. Falls back to the existing path-only output
everywhere else (Terminal.app, pipes/redirects, --json). Adds
`--inline {auto,always,never}` (default auto) and honors
`PREVIEWSMCP_INLINE`.

Handles tmux: when `$TMUX` is set, probes `tmux show-options -gv
allow-passthrough` and wraps the OSC in the DCS passthrough envelope
(`\ePtmux;…\e\\`) when passthrough is on; otherwise emits a one-line
stderr hint telling the user how to enable it. Large images (>1200px)
are downscaled to 1000px via CoreGraphics for display only — the file
on disk stays full resolution.

Decision logic is a pure function with injected env + tmux probe +
TTY flag, so the 28 new tests cover the full behavior matrix
(json/mode/tty × capability × tmux state) deterministically without a
real terminal. Golden-byte tests lock the iTerm2 and tmux-wrap
framings.

Kitty graphics protocol and Sixel are out of scope for this change;
filing as a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
scripts/test-inline-image.sh emits Sources/PreviewsIOS/AppIcon.png
through the same OSC 1337 / DCS-wrap encoding the CLI uses, so a
reviewer can verify their terminal setup in one command without
spinning up the daemon + a simulator + a real preview file.

Detects tmux and warns when allow-passthrough is off.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Changes from the five-axis review:

- --inline always on an unrecognized terminal now surfaces a stderr
  hint ("note: emitting inline image on an unrecognized terminal
  because --inline always") so garbled output doesn't look like a
  silent bug. Previously emitted bytes unconditionally.
- Move MockTerminalEnvironment out of Sources/ into the test file.
  @testable import already exposes the protocol; shipping the mock in
  the release binary was a mistake.
- Extract tmuxPassthroughOffHint and forcedOnUnsupportedHint as static
  constants on TerminalCapabilityDetector so tests assert equality
  instead of substring containment.
- DownscaleCG now logs a one-line stderr note on genuine failure
  branches (CGImageSource / properties / thumbnail / destination /
  finalize). Silent short-circuit on the under-threshold path
  unchanged.
- Switch ITerm2Encoder to Data(header.utf8) + Data([0x07]) — drops two
  force-unwraps without changing emitted bytes (golden-byte test
  still passes).
- Name the 0x0A newline as a static constant in SnapshotCommand.
- Tighten existing hint assertions from `.contains(...)` to `==
  constant`.
- Add 9-case parameterized test for all PREVIEWSMCP_INLINE string
  variants ("0"/"1"/"true"/"false"/"never"/"always"/"auto"/""/garbage)
  — previously only "0" and "1" were covered.

All 29 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@obj-p obj-p marked this pull request as draft April 30, 2026 01:05
@obj-p obj-p force-pushed the worktree-issue-125-inline-snapshots branch from 310408b to d3371b4 Compare April 30, 2026 01:05
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.

Investigate inline snapshot rendering in capable terminals

1 participant