mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-16 02:46:38 +03:00
be01eb2a95
VS Code's terminal draws images only with terminal.integrated.enableImages on, and nothing in the environment says whether it is. `auto` therefore used half-blocks there even for users who had turned images on. At startup (same pre-prompt_toolkit window as the light-mode OSC 11 probe) the classic CLI now sends one XTSMGRAPHICS sixel query fenced by DA1. Only VS Code's image addon answers XTSMGRAPHICS, so a reply means images are on and `auto` uses the full-resolution iTerm2 path; DA1 alone means off. Timeout, measured live in VS Code 1.145 (30 runs per deadline): the reply lands in ~7 ms; at 8 ms some runs missed it and the late reply leaked into the next read, at 10 ms and above every run was right with no leak. The deadline is 50 ms for headroom; the read returns as soon as the DA1 fence arrives, so a normal start pays ~7-10 ms. Skipped over SSH (a remote round trip can exceed the deadline and leak into the prompt) and on Windows. When the probe says images are off, roughly one launch in five shows a tip instead of the random one: "VS Code can show full-resolution images here: turn on terminal.integrated.enableImages in settings." A pinned display.inline_images value skips both the probe and the tip.
96 lines
4.3 KiB
Python
96 lines
4.3 KiB
Python
"""Inline images in the classic CLI scrollback: the user's attachments and image tool results.
|
|
|
|
Images print as ordinary text rows (kitty Unicode placeholders, half-blocks, or blank rows an
|
|
iTerm2 image is drawn over) so the scrollback replay and its row accounting keep working. The
|
|
transmit itself goes out once, raw, beside its row (``_cprint(raw=...)``) and never enters the
|
|
output history: a resize replay repaints the rows alone, so it can't crop a 1 MB escape into
|
|
text or re-send it. kitty keeps the image by id, so replayed placeholders still show it. An
|
|
iTerm2 image is pixels drawn over its reserved rows; a replay repaints those rows blank, so the
|
|
picture is lost in the redrawn viewport (scrollback above keeps it) rather than painted over
|
|
whatever text the replay put above a partly kept image.
|
|
|
|
The kitty id color rides in prompt_toolkit's zero-width ``\\001…\\002`` brackets: its ANSI
|
|
printer would otherwise quantize the truecolor that carries the image id to the output's color
|
|
depth (256 by default). Half-block colors go through that quantization on purpose, like the rest
|
|
of the scrollback.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import random
|
|
import sys
|
|
from collections.abc import Iterable
|
|
|
|
from agent.terminal_image import (
|
|
InlineImage,
|
|
half_block,
|
|
local_image_path,
|
|
probe_vscode_images,
|
|
render_inline_image,
|
|
resolve_inline_mode,
|
|
)
|
|
|
|
_INDENT = " "
|
|
# Mirrors ui-tui/src/config/limits.ts INLINE_IMAGE_MAX_COLS/ROWS (the TUI's budget); keep them equal.
|
|
_MAX_COLS = 64
|
|
_MAX_ROWS = 16
|
|
# Odds per launch of the "VS Code can show full images" hint: occasional, never every start.
|
|
_VSCODE_HINT_ODDS = 0.2
|
|
|
|
|
|
def _zero_width(seq: str) -> str:
|
|
return f"\001{seq}\002"
|
|
|
|
|
|
def _passthrough(seq: str) -> str:
|
|
from hermes_cli.cli_terminal_mixin import _tmux_passthrough
|
|
return _tmux_passthrough(seq) if os.environ.get("TMUX") else seq
|
|
|
|
|
|
def image_rows(image: InlineImage) -> list[tuple[str, str]]:
|
|
"""*image* as ``(row, raw)`` pairs: ``row`` is history-safe scrollback text, ``raw`` the
|
|
escape written verbatim right after it (the transmit: first kitty row, last iTerm2 row)."""
|
|
if image.mode == "kitty":
|
|
r, g, b = bytes.fromhex(image.color[1:])
|
|
color, reset = _zero_width(f"\x1b[38;2;{r};{g};{b}m"), _zero_width("\x1b[39m")
|
|
rows = [(_INDENT + color + row + reset, "") for row in image.placeholder]
|
|
rows[0] = (rows[0][0], _passthrough(image.transmit))
|
|
return rows
|
|
if image.mode == "iterm":
|
|
# Reserve the rows as empty lines (text written over an image cell erases it), then from
|
|
# the start of the last one jump to the first, past the indent, draw, and restore.
|
|
up = f"\x1b[{image.rows - 1}A" if image.rows > 1 else ""
|
|
draw = f"\x1b7{up}\x1b[{len(_INDENT)}C{_passthrough(image.transmit)}\x1b8"
|
|
return [("", "")] * (image.rows - 1) + [("", draw)]
|
|
return [(_INDENT + "".join(half_block(t, b) for t, b in row) + "\x1b[0m", "") for row in image.cells]
|
|
|
|
|
|
def vscode_images_hint(configured) -> bool:
|
|
"""Probe VS Code's image support before prompt_toolkit owns the tty; True when the
|
|
occasional "turn on VS Code images" hint should print this launch.
|
|
|
|
Only ``auto`` probes and hints: a pinned mode is the user's own choice.
|
|
"""
|
|
if str(configured).strip().lower() != "auto" and configured not in (None, True):
|
|
return False
|
|
return probe_vscode_images() is False and random.random() < _VSCODE_HINT_ODDS
|
|
|
|
|
|
def print_inline_images(cli, paths: Iterable) -> None:
|
|
"""Print each image in *paths* under the current scrollback line, per ``display.inline_images``."""
|
|
from cli import _cprint, _terminal_columns
|
|
stdout = sys.__stdout__
|
|
if stdout is None or not stdout.isatty():
|
|
return
|
|
mode = resolve_inline_mode(getattr(cli, "_inline_images_mode", "auto"), overlays=True)
|
|
if mode == "off":
|
|
return
|
|
max_cols = min(_MAX_COLS, _terminal_columns() - len(_INDENT) - 1)
|
|
for path in filter(None, map(local_image_path, map(str, paths))): # the same files the TUI draws
|
|
image = render_inline_image(path, mode=mode, max_cols=max_cols, max_rows=_MAX_ROWS)
|
|
# One print per row, like the diff printer: a whole truecolor image in a single write
|
|
# overruns the Windows console write and the tail rows are lost.
|
|
for row, raw in image_rows(image) if image is not None else ():
|
|
_cprint(row, raw=raw)
|