Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
aaff152
feat: Rich-based rendering engine with intra-line diff highlighting
KUSH42 Apr 1, 2026
bc45f30
fix(rich_output): Error token uses text colour not background
KUSH42 Apr 1, 2026
f0f7310
feat(rich_output): truncate DiffRenderer output at 80 lines
KUSH42 Apr 2, 2026
e3539af
feat(rich_output): syntax highlighting in diff output
KUSH42 Apr 2, 2026
74eb7ef
fix(rich_output): extend diff background to line numbers and sigils
KUSH42 Apr 2, 2026
578310c
fix(rich_output): preserve path-distinct diff headers and summary counts
KUSH42 Apr 5, 2026
a79001e
fix: unblock read-file guards and color assertions in tests
KUSH42 Apr 5, 2026
cb0aa3b
feat(config): expose diff and preview line limits in config.yaml
KUSH42 Apr 7, 2026
52403a8
fix(reasoning): make show_reasoning sole gate for reasoning callback
KUSH42 Apr 7, 2026
ea4b477
test(reasoning,display): fix stub isolation and add coverage for toda…
KUSH42 Apr 7, 2026
7e08f16
fix(tui): animate spinner during agent runs
KUSH42 Apr 7, 2026
e4190b2
feat(tui): configurable spinner style + terminal tab/title animation
KUSH42 Apr 7, 2026
1b2708e
fix(display): wire missing imports and suppress code snippet when hig…
KUSH42 Apr 7, 2026
0203d1f
fix(tui): write OSC title via os.write to avoid prompt_toolkit render…
KUSH42 Apr 8, 2026
8ad2fb6
feat: syntax highlighting for tool outputs and LLM responses
KUSH42 Apr 1, 2026
97c3a9c
fix(rich_output): remove fence delimiters from Panel output
KUSH42 Apr 1, 2026
9deb3ce
feat(rich_output): add line numbers to fenced code blocks
KUSH42 Apr 2, 2026
ef42602
fix(rich_output): use rich.markup.escape() to fix bracket/backslash r…
KUSH42 Apr 2, 2026
b3f506e
fix(rich_output): resolve width=0 rendering, dead import, and _intra_…
KUSH42 Apr 3, 2026
4bdbac2
test(rich_output): align PR2 diff expectations with renderer output
KUSH42 Apr 5, 2026
d01ad29
test(rich_output): align PR3 diff expectations with renderer output
KUSH42 Apr 5, 2026
02fb87a
test(rich_output): update PR3 paired-diff expectation after rebase
KUSH42 Apr 5, 2026
23ae96e
test(rich_output): relax PR3 paired-diff ANSI assertion after rebase
KUSH42 Apr 5, 2026
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)
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.
# 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