-
Notifications
You must be signed in to change notification settings - Fork 52.8k
feat: syntax highlighting for tool outputs and LLM responses (PR2) #4471
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
aaff152
bc45f30
f0f7310
e3539af
74eb7ef
578310c
a79001e
cb0aa3b
52403a8
ea4b477
7e08f16
e4190b2
1b2708e
0203d1f
8ad2fb6
97c3a9c
9deb3ce
ef42602
b3f506e
4bdbac2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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: | ||
|
|
@@ -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 | ||
|
|
@@ -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]] = [] | ||
|
|
@@ -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 | ||
|
|
@@ -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: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This preview helper is only defined and unit-tested in this PR; no production path calls it (likewise for the read-file and terminal helpers). The current completed-tool hook is |
||
| """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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Require a read verb before syntax-highlighting terminal output This helper says it only fires for known file-reader verbs, but it never actually checks That means commands like
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed on this branch. Terminal source-highlighting now returns early unless the leading verb is in the explicit file-read set, so commands like |
||
| 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 | ||
| # ========================================================================= | ||
|
|
@@ -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}") | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cap highlighted tool previews before printing every line
_highlight_block()currently emits every highlighted line.render_read_file_preview()andrender_terminal_preview()both call this helper, so a singleread_fileon a long source file or acat/sedterminal command can dump hundreds of lines into the interactive UI before the assistant responds.This new preview path needs the same kind of truncation budget as the diff renderer to avoid flooding the session.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed on this branch. Highlighted tool previews now use a 40-line budget with an omission footer instead of dumping the full highlighted block into the interactive UI. Added regression coverage for both read-file and generic highlighted-block paths.