mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
fix(pet): detect terminals more broadly
incl. windows terminal and ghostty
This commit is contained in:
parent
3c8d3ecfa0
commit
64dc6b91b0
2 changed files with 75 additions and 44 deletions
|
|
@ -17,6 +17,7 @@ empty string rather than raising.
|
|||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from agent.pet.terminal import detect_terminal_graphics
|
||||
|
||||
import base64
|
||||
import io
|
||||
|
|
@ -45,50 +46,6 @@ RENDER_MODES = ("auto", "kitty", "iterm", "sixel", "unicode", "off")
|
|||
# Terminal capability detection
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def detect_terminal_graphics() -> str:
|
||||
"""Best-effort detection of the richest graphics protocol available.
|
||||
|
||||
Env-based (non-blocking — we never issue a DA1/terminal query that could
|
||||
hang a pipe). Returns one of ``kitty`` / ``iterm`` / ``sixel`` /
|
||||
``unicode``. Conservative: unknown terminals get ``unicode``, which works
|
||||
anywhere with truecolor.
|
||||
"""
|
||||
term = os.environ.get("TERM", "").lower()
|
||||
term_program = os.environ.get("TERM_PROGRAM", "").lower()
|
||||
|
||||
# The VS Code / Cursor integrated terminal sets TERM_PROGRAM=vscode
|
||||
# authoritatively but does NOT scrub the terminal env vars it inherits when
|
||||
# launched from another emulator (ITERM_SESSION_ID, KITTY_WINDOW_ID, …).
|
||||
# Trusting those leaks emits an image protocol the embedded xterm.js can't
|
||||
# display — you get a blank frame. Inline images there are opt-in
|
||||
# (terminal.integrated.enableImages), so default to half-blocks, which
|
||||
# always render in its truecolor grid. Users who enabled images can pin
|
||||
# display.pet.render_mode explicitly.
|
||||
if term_program == "vscode":
|
||||
return "unicode"
|
||||
|
||||
# kitty graphics protocol
|
||||
if os.environ.get("KITTY_WINDOW_ID") or "kitty" in term or "ghostty" in term:
|
||||
return "kitty"
|
||||
if term_program in {"ghostty"}:
|
||||
return "kitty"
|
||||
|
||||
# WezTerm speaks both kitty and iterm; prefer kitty (richer placement).
|
||||
if term_program == "wezterm" or os.environ.get("WEZTERM_PANE"):
|
||||
return "kitty"
|
||||
|
||||
# iTerm2 inline images
|
||||
if term_program == "iterm.app" or os.environ.get("ITERM_SESSION_ID"):
|
||||
return "iterm"
|
||||
|
||||
# sixel-capable terminals (env heuristics only)
|
||||
if term_program in {"mintty"} or "foot" in term or "mlterm" in term:
|
||||
return "sixel"
|
||||
if "sixel" in term:
|
||||
return "sixel"
|
||||
|
||||
return "unicode"
|
||||
|
||||
|
||||
def resolve_mode(configured: str | None, *, stream=None) -> str:
|
||||
"""Resolve the effective render mode from config + the environment.
|
||||
|
|
|
|||
74
agent/pet/terminal.py
Normal file
74
agent/pet/terminal.py
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
from __future__ import annotations
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
RENDER_MODES = ("auto", "kitty", "iterm", "sixel", "unicode", "off")
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TerminalProfile:
|
||||
"""Declarative description of a terminal's detection fingerprint."""
|
||||
mode: str # one of RENDER_MODES
|
||||
# Any of these env vars being set (non-empty) is a match
|
||||
env_vars: tuple[str, ...] = ()
|
||||
# $TERM_PROGRAM must equal one of these (case-insensitive)
|
||||
term_program_vals: tuple[str, ...] = ()
|
||||
# Any of these strings appearing in $TERM is a match
|
||||
term_substrings: tuple[str, ...] = ()
|
||||
|
||||
def matches(self) -> bool:
|
||||
if any(os.environ.get(v) for v in self.env_vars):
|
||||
return True
|
||||
tp = os.environ.get("TERM_PROGRAM", "").lower()
|
||||
if tp and tp in self.term_program_vals:
|
||||
return True
|
||||
term = os.environ.get("TERM", "").lower()
|
||||
if any(s in term for s in self.term_substrings):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# ── Registry (priority order — first match wins) ───────────────────────────
|
||||
#
|
||||
# VS Code / Cursor override comes first: their embedded xterm.js leaks
|
||||
# KITTY_WINDOW_ID / ITERM_SESSION_ID from the parent shell, so we must
|
||||
# short-circuit before any of the real-graphics entries.
|
||||
#
|
||||
_TERMINAL_PROFILES: tuple[TerminalProfile, ...] = (
|
||||
TerminalProfile(
|
||||
mode="unicode",
|
||||
term_program_vals=("vscode",),
|
||||
),
|
||||
|
||||
TerminalProfile(
|
||||
mode="kitty",
|
||||
env_vars=("KITTY_WINDOW_ID","GHOSTTY_RESOURCES_DIR","WEZTERM_PANE",),
|
||||
term_program_vals=("kitty", "ghostty",),
|
||||
term_substrings=("kitty","ghostty","wezterm", ),
|
||||
),
|
||||
TerminalProfile(
|
||||
mode="iterm",
|
||||
env_vars=("ITERM_SESSION_ID",),
|
||||
term_program_vals=("iterm.app",),
|
||||
),
|
||||
TerminalProfile(
|
||||
mode="sixel",
|
||||
env_vars=("WT_SESSION",),
|
||||
term_substrings=("mlterm","contour","sixel",),
|
||||
term_program_vals=("mintty","foot","contour",),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def detect_terminal_graphics() -> str:
|
||||
"""Return the richest graphics mode available in the current terminal.
|
||||
|
||||
Non-blocking — no DA1/terminal query is issued, as those can break pipes
|
||||
in strange ways.
|
||||
Walks ``_TERMINAL_PROFILES` in priority order; returns the first match's mode, or ``"unicode"`` as the
|
||||
safe universal fallback.
|
||||
"""
|
||||
for profile in _TERMINAL_PROFILES:
|
||||
if profile.matches():
|
||||
return profile.mode
|
||||
return "unicode"
|
||||
Loading…
Add table
Add a link
Reference in a new issue