Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
246 changes: 244 additions & 2 deletions agent/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,36 @@
_MAX_INLINE_DIFF_FILES = 6
_MAX_INLINE_DIFF_LINES = 80

# Set to True by the CLI when code-highlight mode is active. Consumed by
# get_cute_tool_message to suppress the inline code snippet (the highlighted
# block will show the full code immediately after).
_code_highlight_active: bool = False


def set_code_highlight_active(active: bool) -> None:
global _code_highlight_active
_code_highlight_active = active


def set_diff_limits(max_lines: int, max_files: int) -> None:
global _MAX_INLINE_DIFF_LINES, _MAX_INLINE_DIFF_FILES
_MAX_INLINE_DIFF_LINES = max_lines
_MAX_INLINE_DIFF_FILES = max_files


# Rich-based rendering (syntax highlighting + enhanced diffs)
try:
from agent.rich_output import DiffRenderer as _RichDiffRenderer
from agent.rich_output import LanguageDetector as _RichLanguageDetector
from agent.rich_output import SyntaxHighlighter as _RichSyntaxHighlighter
from agent.rich_output import clean_command_output
_rich_diff = _RichDiffRenderer()
_rich_syntax = _RichSyntaxHighlighter()
_rich_detector = _RichLanguageDetector()
_RICH_OUTPUT = True
except ImportError:
_RICH_OUTPUT = False


@dataclass
class LocalEditSnapshot:
Expand Down Expand Up @@ -411,7 +441,18 @@ def _emit_inline_diff(diff_text: str, print_fn) -> bool:


def _render_inline_unified_diff(diff: str) -> list[str]:
"""Render unified diff lines in Hermes' inline transcript style."""
"""Render unified diff lines with line numbers and coloured backgrounds.

Uses rich_output.DiffRenderer when available (line numbers, green/red
background highlights). Falls back to the original ANSI-string path.
"""
if _RICH_OUTPUT:
try:
return _rich_diff.to_lines(diff, max_lines=0)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This returns ANSI-only output. Current ui-tui/src/app/createGatewayEventHandler.ts:740 strips ANSI from inline_diff, so the proposed backgrounds and intra-line emphasis do not reach the TUI. Please carry structured styling through the event or add a TUI-native renderer and integration coverage.

except Exception as exc:
logger.debug("Rich diff render failed, using ANSI fallback: %s", exc)

# Original ANSI fallback — unchanged from upstream
rendered: list[str] = []
from_file = None
to_file = None
Expand Down Expand Up @@ -443,6 +484,24 @@ def _render_inline_unified_diff(diff: str) -> list[str]:
return rendered


def highlight_code(
code: str,
language: str | None = None,
filename: str | None = None,
) -> str:
"""Return an ANSI-highlighted version of *code* for terminal display.

When rich_output is unavailable the original string is returned unchanged.
"""
if not _RICH_OUTPUT:
return code
try:
return _rich_syntax.to_ansi(code, language=language, filename=filename)
except Exception as exc:
logger.debug("highlight_code failed: %s", exc)
return code


def _split_unified_diff_sections(diff: str) -> list[str]:
"""Split a unified diff into per-file sections."""
sections: list[list[str]] = []
Expand Down Expand Up @@ -492,7 +551,7 @@ def _summarize_rendered_diff_sections(

rendered.extend(section_lines[:remaining_budget])
omitted_lines += len(section_lines) - remaining_budget
omitted_files += 1 + max(0, len(sections) - idx - 1)
omitted_files += max(0, len(sections) - idx - 1)
for leftover in sections[idx + 1:]:
omitted_lines += len(_render_inline_unified_diff(leftover))
break
Expand Down Expand Up @@ -531,6 +590,187 @@ def render_edit_diff_with_delta(
return _emit_inline_diff("\n".join(rendered_lines), print_fn)


# =========================================================================
# execute_code / read_file / terminal syntax highlight previews
# =========================================================================

_PREVIEW_MAX_LINES = 40


def set_preview_max_lines(n: int) -> None:
global _PREVIEW_MAX_LINES
_PREVIEW_MAX_LINES = n


def _emit_highlighted_lines(block: str, print_fn) -> bool:
lines = block.rstrip("\n").splitlines()
if not lines:
return False
if len(lines) > _PREVIEW_MAX_LINES:
omitted = len(lines) - _PREVIEW_MAX_LINES
lines = lines[:_PREVIEW_MAX_LINES] + [
f"\033[2m╌╌ {omitted} more line{'s' if omitted != 1 else ''} omitted ╌╌\033[0m"
]
for line in lines:
print_fn(line)
return True

def _highlight_block(header: str, content: str, language: str, print_fn) -> bool:
"""Print a labelled syntax-highlighted block aligned with the ┊ tool log.

Format::

\033[2m ┊ <header>\033[0m
<highlighted content lines>
"""
_print = print_fn or print
_print(f"\033[2m ┊ {header}\033[0m")
if not _RICH_OUTPUT:
return _emit_highlighted_lines(content, _print)
try:
highlighted = _rich_syntax.to_ansi(content, language=language).rstrip("\n")
return _emit_highlighted_lines(highlighted, _print)
except Exception as exc:
logger.debug("highlight_block failed for %s: %s", header, exc)
return False


def render_execute_code_preview(code: str, print_fn=None) -> bool:
"""Print *code* with Python syntax highlighting.

The cute_msg line already labels the tool; this function prints only the
highlighted code (no header) so the output stays compact.
Returns True if anything was printed.
"""
if not code or not code.strip():
return False
_print = print_fn or print
if not _RICH_OUTPUT:
return _emit_highlighted_lines(code, _print)
try:
highlighted = _rich_syntax.to_ansi(code, language="python").rstrip("\n")
return _emit_highlighted_lines(highlighted, _print)
except Exception as exc:
logger.debug("execute_code highlight failed: %s", exc)
return False


def render_read_file_preview(path: str, result_json: str, print_fn=None) -> bool:
"""Print the content of a read_file result with syntax highlighting.

Language is detected from *path*'s extension. Returns False (no output)
when the file type is unknown — we don't highlight plain text or binary.
"""
if not path or not result_json:
return False
try:
import json as _json
result = _json.loads(result_json)
content = result.get("content", "")
except Exception:
return False
if not content or not content.strip():
return False

from pathlib import Path as _Path
if _RICH_OUTPUT:
lang = _rich_detector.detect_from_filename(_Path(path).name)
else:
lang = None
if not lang:
return False # unknown type — skip, don't guess

header = f"📄 {_Path(path).name}"
return _highlight_block(header, content, lang, print_fn)


_FILE_READ_COMMANDS = frozenset({
"cat", "head", "tail", "less", "more", "bat",
"sed", "awk", "grep", "cut", "sort", "uniq",
"nl", "od", "xxd", "hexdump",
})

# Commands that *execute* a file rather than reading it — the terminal output
# will be runtime stdout, not source code. Never highlight for these.
_FILE_EXEC_COMMANDS = frozenset({
"python", "python3", "python2",
"node", "nodejs", "deno", "bun",
"ruby", "perl", "php", "lua",
"bash", "sh", "zsh", "fish", "dash",
"Rscript", "julia",
})


def _extract_file_language_from_command(command: str):
"""Return (filename, language) if *command* is clearly reading a known source file.

Only fires when the leading verb is a known file-reader (cat, head, sed …).
Commands that *execute* files (node, python, bash …) are explicitly excluded
— their stdout is runtime output, not source code.

Parses tokens in reverse (file arg is typically last) and returns the first
token whose extension maps to a known language. Returns (None, None) if no
match — we never fall back to content-based detection for shell output.
"""
if not command:
return None, None
try:
import shlex as _shlex
tokens = _shlex.split(command)
except ValueError:
tokens = command.split()

if not tokens:
return None, None

# Check the leading verb (strip path prefix, e.g. /usr/bin/cat → cat)
from pathlib import Path as _Path
verb = _Path(tokens[0]).name
if verb in _FILE_EXEC_COMMANDS:
return None, None
if verb not in _FILE_READ_COMMANDS:
return None, None

if not _RICH_OUTPUT:
return None, None

for tok in reversed(tokens):
if tok.startswith("-"):
continue
# Only consider tokens that look like a file path (contain a dot or slash)
if "." not in _Path(tok).name:
continue
lang = _rich_detector.detect_from_filename(_Path(tok).name)
if lang:
return _Path(tok).name, lang
return None, None


def render_terminal_preview(command: str, result_json: str, print_fn=None) -> bool:
"""Print terminal output with syntax highlighting when the command reads a source file.

Highlighting is only applied when a known-extension filename can be extracted
from *command* (e.g. ``cat foo.py``, ``sed -n '1,50p' app.ts``).
Returns False without printing anything if the language cannot be determined.
"""
if not command or not result_json:
return False
filename, lang = _extract_file_language_from_command(command)
if not lang:
return False
try:
import json as _json
result = _json.loads(result_json)
output = result.get("output", "")
except Exception:
return False
if not output or not output.strip():
return False

header = f"💻 {filename}"
return _highlight_block(header, output, lang, print_fn)


# =========================================================================
# KawaiiSpinner
# =========================================================================
Expand Down Expand Up @@ -950,6 +1190,8 @@ def _wrap(line: str) -> str:
}
return _wrap(f"┊ 🧪 rl {rl.get(tool_name, tool_name.replace('rl_', ''))} {dur}")
if tool_name == "execute_code":
if _code_highlight_active:
return _wrap(f"┊ 🐍 exec {dur}")
code = args.get("code", "")
first_line = code.strip().split("\n")[0] if code.strip() else ""
return _wrap(f"┊ 🐍 exec {_trunc(first_line, 35)} {dur}")
Expand Down
22 changes: 22 additions & 0 deletions agent/redact.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,19 @@
r"(?<![A-Za-z0-9_-])(" + "|".join(_PREFIX_PATTERNS) + r")(?![A-Za-z0-9_-])"
)

_FAST_MARKERS_CASE_SENSITIVE = (
"sk-", "ghp_", "github_pat_", "gho_", "ghu_", "ghs_", "ghr_",
"xox", "AIza", "pplx-", "fal_", "fc-", "bb_live_", "gAAAA",
"AKIA", "sk_live_", "sk_test_", "rk_live_", "SG.", "hf_", "r8_",
"npm_", "pypi-", "dop_v1_", "doo_v1_", "am_", "sk_", "tvly-",
"exa_", "Authorization:", "://", "PRIVATE KEY",
)
_FAST_MARKERS_LOWER = (
"api_key", "apikey", "token", "secret", "password", "passwd",
"credential", "authorization:", "bearer", "access_token",
"refresh_token", "auth_token",
)


def _mask_token(token: str) -> str:
"""Mask a token, preserving prefix for long tokens."""
Expand All @@ -119,6 +132,15 @@ def redact_sensitive_text(text: str) -> str:
return text
if not _REDACT_ENABLED:
return text
# Fast path for large plain text blobs with no secret-like markers.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This security-sensitive redaction optimization is unrelated to the Rich diff feature. Please split it from this renderer PR; current main has later redaction fixes that should be preserved independently during salvage.

# This avoids running several regex passes across large source files or
# logs that contain no credentials at all.
if len(text) > 8192:
lower_text = text.lower()
if not any(marker in text for marker in _FAST_MARKERS_CASE_SENSITIVE) and not any(
marker in lower_text for marker in _FAST_MARKERS_LOWER
):
return text

# Known prefixes (sk-, ghp_, etc.)
text = _PREFIX_RE.sub(lambda m: _mask_token(m.group(1)), text)
Expand Down
Loading