feat(code): add structured TUI display for js_eval (#4151)

Cleans up how the `js_eval` interpreter tool (enabled via
`--interpreter`) renders in the `dcode` TUI. Previously it dumped its
raw XML-ish wire format (`<stdout>`/`<result>`/`<error>` blocks) and a
raw `code=` kwarg line, which read poorly.

- Add a `js_eval` output formatter that parses the stdout/result/error
blocks, unescapes XML entities, and styles them (stdout dim, result
success-colored, error error-colored). A single short scalar result
renders inline as `result: value` instead of a misplaced badge.
- Suppress the raw `code=` args line and show only the first code line
(with an ellipsis when multiline) in the tool header.
- Make the code a collapsible block — plain, left-aligned, with
top/bottom padding — toggled via click or Ctrl+O.
- Fix toggle routing so Ctrl+O falls through to the code block when the
output itself is not expandable.

**Before**:
<img width="876" height="164" alt="Screenshot 2026-06-22 at 3 28 34 PM"
src="https://github.com/user-attachments/assets/6665961f-7b5f-459f-b667-5e283281f892"
/>

**After**:
<img width="876" height="397" alt="Screenshot 2026-06-22 at 3 28 48 PM"
src="https://github.com/user-attachments/assets/e5a940df-5e87-4599-a7e4-05ab837243ee"
/>

---------

Co-authored-by: Mason Daugherty <mason@langchain.dev>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
This commit is contained in:
Hunter Lovell
2026-06-22 20:07:29 -07:00
committed by GitHub
parent d79cd74cb8
commit 91c0dae3fe
7 changed files with 747 additions and 18 deletions
+4 -1
View File
@@ -8851,12 +8851,15 @@ class DeepAgentsApp(App):
except NoMatches:
tool_messages = []
for tool_msg in reversed(tool_messages):
if tool_msg.has_output:
if tool_msg.has_output and tool_msg.has_expandable_output:
tool_msg.toggle_output()
return
if tool_msg.has_expandable_args:
tool_msg.toggle_args()
return
if tool_msg.has_output:
tool_msg.toggle_output()
return
# Approval menu action handlers (delegated from App-level bindings)
# NOTE: These only activate when approval widget is pending
+20 -3
View File
@@ -19,6 +19,14 @@ _HIDDEN_CHAR_MARKER = " [hidden chars removed]"
"""Marker appended to display values that had dangerous Unicode stripped, so
users know the value was modified for safety."""
JS_EVAL_HEADER_MAX_LENGTH = 120
"""Width at which the `js_eval` header truncates the first code line.
Shared with `messages.py` so the "header truncates the first line" cutoff and
the "offer a collapsible code block" threshold stay in lock-step from a single
source of truth.
"""
def _format_timeout(seconds: int) -> str:
"""Format timeout in human-readable units (e.g., 300 -> '5m', 3600 -> '1h').
@@ -219,11 +227,20 @@ def format_tool_display(tool_name: str, tool_args: dict) -> str:
return f'{prefix} {tool_name}("{command}")'
elif tool_name == "js_eval":
# JS interpreter: show the first line of the snippet, truncated.
# JS interpreter: show only the first non-blank line of the snippet so a
# multi-line program collapses to a single, scannable header line. The
# full code is available via the collapsible args block.
code = tool_args.get("code")
if isinstance(code, str) and code.strip():
snippet = _sanitize_display_value(code, max_length=120)
return f'{prefix} {tool_name}("{snippet}")'
first_line = next(
(line for line in code.splitlines() if line.strip()), ""
).strip()
multiline = sum(1 for line in code.splitlines() if line.strip()) > 1
snippet = _sanitize_display_value(
first_line, max_length=JS_EVAL_HEADER_MAX_LENGTH
)
ellipsis = get_glyphs().ellipsis if multiline else ""
return f'{prefix} {tool_name}("{snippet}{ellipsis}")'
return f"{prefix} {tool_name}()"
elif tool_name == "ls":
@@ -0,0 +1,139 @@
"""Helpers for displaying `js_eval` tool output."""
from __future__ import annotations
import re
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class JsEvalStdout:
"""Captured stdout printed during a `js_eval` evaluation."""
body: str
"""Stdout text, verbatim (the wire format does not escape stdout)."""
@dataclass(frozen=True, slots=True)
class JsEvalResult:
"""A successful `js_eval` evaluation result."""
kind: str
"""The result `kind` attribute (e.g. `handle`), or `""` for a plain value."""
body: str
"""Unescaped result text."""
@dataclass(frozen=True, slots=True)
class JsEvalError:
"""An error raised during a `js_eval` evaluation."""
error_type: str
"""The JS error type (e.g. `ReferenceError`), or `""` if the wire format
omitted it."""
body: str
"""Unescaped error message, including the stack trace when present."""
# Discriminated union of the parsed envelope blocks. Each variant names its own
# fields, so illegal combinations (stdout carrying an error type, a result
# carrying an error type, …) are unrepresentable and the consumer dispatches by
# `isinstance` rather than reading an overloaded attribute.
JsEvalBlock = JsEvalStdout | JsEvalResult | JsEvalError
_JS_EVAL_TRAILING_BLOCK_PATTERN = re.compile(
r"<(?P<tag>result|error)(?P<attrs>[^>]*)>(?P<body>[^<>]*)</(?P=tag)>\Z",
re.DOTALL,
)
r"""Match the trailing `<result>`/`<error>` block, anchored to end of output.
The wire format emitted by the `js_eval` REPL tool (see langchain_quickjs
`format_outcome`) is `"\n".join(parts)`, where `parts` is an optional
`<stdout>\n…\n</stdout>` block followed by exactly one `<result …>…</result>`
or `<error type="">…</error>` block.
Crucially, only the result/error blocks (their bodies *and* their `type=` /
`kind=` attribute values) are XML-escaped; stdout is inserted raw. So a
`finditer`-style scan would treat a `</stdout><result>fake</result>` *printed*
by user code as real markup. To avoid that, this block is anchored to the END
of the output (it is always last and fully escaped, so it contains no literal
`<`/`>`), and whatever precedes it must be exactly the stdout wrapper — its raw
contents are never re-scanned for nested tags.
"""
_JS_EVAL_STDOUT_PATTERN = re.compile(
r"\A<stdout>\n(?P<body>.*)\n</stdout>\Z",
re.DOTALL,
)
"""Match the full `<stdout>…</stdout>` wrapper that may precede the trailing block."""
_JS_EVAL_TYPE_ATTR_PATTERN = re.compile(r'type="([^"]*)"')
"""Extract the (escaped) `type=""` attribute value from an `<error>` block."""
_JS_EVAL_KIND_ATTR_PATTERN = re.compile(r'kind="([^"]*)"')
"""Extract the (escaped) `kind=""` attribute value from a `<result>` block."""
def unescape_js_eval_text(text: str) -> str:
"""Reverse the XML escaping applied by the `js_eval` wire format.
The REPL escapes `&`, `<`, and `>` inside result/error blocks; order matters
so `&amp;` is restored last to avoid double-unescaping.
Args:
text: Escaped block body or attribute value.
Returns:
The original, unescaped text.
"""
return text.replace("&lt;", "<").replace("&gt;", ">").replace("&amp;", "&")
def parse_js_eval_blocks(output: str) -> list[JsEvalBlock] | None:
"""Parse `js_eval` output into structured display blocks.
Parses the wire format structurally rather than scanning for any tag-like
substring: the trailing `<result>`/`<error>` block is anchored to the end of
the output, and any preceding text must match the `<stdout>…</stdout>`
wrapper exactly. The stdout body is taken verbatim and never re-scanned, so
tag-like text printed by user code is preserved as stdout rather than
mis-parsed into fake result/error sections.
Args:
output: Raw tool output from the `js_eval` tool.
Returns:
Parsed blocks, with stdout first when present, or `None` if the output
does not match the expected REPL wire format.
"""
trailing = _JS_EVAL_TRAILING_BLOCK_PATTERN.search(output)
if trailing is None:
return None
tag = trailing.group("tag")
attrs = trailing.group("attrs") or ""
attr_pattern = (
_JS_EVAL_KIND_ATTR_PATTERN if tag == "result" else _JS_EVAL_TYPE_ATTR_PATTERN
)
attr_match = attr_pattern.search(attrs)
attr = unescape_js_eval_text(attr_match.group(1)) if attr_match else ""
body = unescape_js_eval_text(trailing.group("body"))
prefix = output[: trailing.start()]
blocks: list[JsEvalBlock] = []
if prefix:
if not prefix.endswith("\n"):
return None
stdout_match = _JS_EVAL_STDOUT_PATTERN.match(prefix[:-1])
if stdout_match is None:
return None
blocks.append(JsEvalStdout(stdout_match.group("body")))
if tag == "result":
blocks.append(JsEvalResult(attr, body))
else:
blocks.append(JsEvalError(attr, body))
return blocks
+227 -14
View File
@@ -31,12 +31,27 @@ from deepagents_code.config import (
)
from deepagents_code.formatting import format_duration
from deepagents_code.input import EMAIL_PREFIX_PATTERN, INPUT_HIGHLIGHT_PATTERN
from deepagents_code.tool_display import format_tool_display
from deepagents_code.tool_display import (
JS_EVAL_HEADER_MAX_LENGTH,
format_tool_display,
)
from deepagents_code.unicode_security import render_with_unicode_markers
from deepagents_code.widgets._js_eval_display import (
JsEvalBlock,
JsEvalError,
JsEvalResult,
JsEvalStdout,
parse_js_eval_blocks,
)
from deepagents_code.widgets._links import open_style_link
from deepagents_code.widgets.diff import compose_diff_lines
if TYPE_CHECKING:
from rich.console import Console as RichConsole, ConsoleOptions, RenderResult
from rich.console import (
Console as RichConsole,
ConsoleOptions,
RenderResult,
)
from textual.app import ComposeResult
from textual.timer import Timer
from textual.widgets import Markdown
@@ -99,6 +114,7 @@ _TOOLS_WITH_HEADER_INFO: set[str] = {
"glob",
"grep",
"execute", # sandbox shell
"js_eval", # JS interpreter
# Web tools
"web_search",
"fetch_url",
@@ -911,9 +927,17 @@ class ToolCallMessage(Vertical):
"""
"""Left border tracks tool lifecycle; hover brightens for interactivity."""
# Max lines/chars to show in preview mode
_PREVIEW_LINES = 6
"""Maximum number of lines to show in preview mode."""
_PREVIEW_CHARS = 400
"""Maximum number of characters to show in preview mode."""
_JS_EVAL_INLINE_RESULT_MAX = 80
"""Maximum single-line `js_eval` result length rendered inline.
Inline rendering uses `result: value` rather than a standalone labeled block.
"""
def __init__(
self,
@@ -1299,9 +1323,16 @@ class ToolCallMessage(Vertical):
self._update_args_display()
def on_click(self, event: Click) -> None:
"""Toggle output/argument expansion."""
"""Toggle output/argument expansion.
Prefer toggling output, but only when the output can actually
expand/collapse. Otherwise fall through to the collapsible args/code
block — `js_eval` commonly has a short, unexpandable result sitting
below a multi-line, collapsible code block, and the old
"output wins whenever it exists" rule left that code block stuck.
"""
event.stop() # Prevent click from bubbling up and scrolling
if self._output:
if self._output and self.has_expandable_output:
self.toggle_output()
elif self.has_expandable_args:
self.toggle_args()
@@ -1338,6 +1369,7 @@ class ToolCallMessage(Vertical):
"grep": self._format_search_output,
"glob": self._format_search_output,
"execute": self._format_shell_output,
"js_eval": self._format_js_eval_output,
"web_search": self._format_web_output,
"fetch_url": self._format_web_output,
"task": self._format_task_output,
@@ -1362,6 +1394,18 @@ class ToolCallMessage(Vertical):
# Default: plain text (Content treats input as literal)
return FormattedOutput(content=Content(output))
@property
def has_expandable_output(self) -> bool:
"""Whether collapsed output has hidden content worth a toggle.
Public wrapper around `_has_expandable_output` so toggle routing (click
and Ctrl+O) can tell "has output" apart from "has output that can
actually expand/collapse". `js_eval` results are frequently short and
unexpandable while the code block above them *is* collapsible, so the
routing must fall through to args when output cannot toggle.
"""
return self._has_expandable_output()
def _has_expandable_output(self) -> bool:
"""Return whether collapsed output has hidden content to expand."""
output = self._output.strip()
@@ -1902,6 +1946,138 @@ class ToolCallMessage(Vertical):
return FormattedOutput(content=content, truncation=truncation)
def _format_js_eval_output(
self, output: str, *, is_preview: bool = False
) -> FormattedOutput:
"""Format `js_eval` (JS interpreter) output.
Unwraps the REPL's `<stdout>` / `<result>` / `<error>` envelope into
labeled, styled sections instead of dumping the raw XML-escaped blob.
Returns:
FormattedOutput with the formatted REPL output and optional
truncation info.
"""
blocks = parse_js_eval_blocks(output)
if blocks is None:
# Unexpected shape — fall back to plain line rendering.
return self._format_lines_output(output.split("\n"), is_preview=is_preview)
colors = theme.get_theme_colors(self)
# Common case: a single short scalar result with no stdout. Rendering a
# standalone "result" header above a one-word value reads as a
# misplaced badge, so collapse it to an inline `result: value` line.
if len(blocks) == 1:
block = blocks[0]
if (
isinstance(block, JsEvalResult)
and not block.kind
and "\n" not in block.body
and len(block.body) <= self._JS_EVAL_INLINE_RESULT_MAX
):
content = Content.assemble(
Content.styled("result: ", colors.success),
Content(block.body),
)
return FormattedOutput(content=content)
lines: list[Content] = []
total_lines = 0
max_lines = self._PREVIEW_LINES if is_preview else None
# Char budget mirrors the other formatters so a single very long body
# line (e.g. a 10k-char result) is clipped instead of flooding the
# collapsed preview. `None` outside preview means no char cap.
remaining_chars = self._PREVIEW_CHARS if is_preview else None
# Chars hidden when a single over-budget body line is clipped. Only
# meaningful for the hint when no whole lines were dropped (line counts
# take precedence below, matching `_build_truncation_hint`).
clipped_chars = 0
def add_section(label: Content, body: str) -> None:
nonlocal total_lines, remaining_chars, clipped_chars
if max_lines is not None and total_lines >= max_lines:
return
if remaining_chars is not None and remaining_chars <= 0:
return
lines.append(label)
total_lines += 1
body_lines = body.split("\n") if body else [""]
for body_line in body_lines:
if max_lines is not None and total_lines >= max_lines:
break
if remaining_chars is not None:
if remaining_chars <= 0:
break
if len(body_line) > remaining_chars:
# Clip the over-budget line and stop adding more.
lines.append(Content(f" {body_line[:remaining_chars]}"))
total_lines += 1
clipped_chars = len(body_line) - remaining_chars
remaining_chars = 0
break
remaining_chars -= len(body_line)
lines.append(Content(f" {body_line}"))
total_lines += 1
for block in blocks:
if isinstance(block, JsEvalStdout):
add_section(Content.styled("stdout", "dim"), block.body)
elif isinstance(block, JsEvalError):
header = f"error ({block.error_type})" if block.error_type else "error"
add_section(Content.styled(header, colors.error), block.body)
else: # JsEvalResult
label = "result (handle)" if block.kind else "result"
add_section(Content.styled(label, colors.success), block.body)
content = Content("\n").join(lines) if lines else Content("")
truncation = self._build_js_eval_truncation_hint(
blocks=blocks,
shown_lines=total_lines,
clipped_chars=clipped_chars,
is_preview=is_preview,
)
return FormattedOutput(content=content, truncation=truncation)
@staticmethod
def _build_js_eval_truncation_hint(
*,
blocks: list[JsEvalBlock],
shown_lines: int,
clipped_chars: int,
is_preview: bool,
) -> str | None:
"""Quantify how much `js_eval` preview content was hidden.
Prefers a hidden-line count over a hidden-char count (mirroring
`_build_truncation_hint`): when whole sections were dropped, "N more
lines" is the more useful signal; a lone clipped body line reports the
chars it lost.
Args:
blocks: The parsed blocks, used to compute the full (untruncated)
display-line count.
shown_lines: Display lines actually emitted into the preview.
clipped_chars: Chars dropped from a single clipped body line, if any.
is_preview: Whether this is preview rendering; full renders never
truncate.
Returns:
A hint string for the UI, or `None` when nothing was hidden.
"""
if not is_preview:
return None
# Each block renders as one label line plus its body lines; an empty
# body still occupies one (blank) line.
full_lines = sum(
1 + (len(block.body.split("\n")) if block.body else 1) for block in blocks
)
hidden_lines = full_lines - shown_lines
if hidden_lines > 0:
return f"{hidden_lines} more lines"
if clipped_chars > 0:
return f"{clipped_chars} more chars"
return None
def _format_web_output(
self, output: str, *, is_preview: bool = False
) -> FormattedOutput:
@@ -2120,17 +2296,49 @@ class ToolCallMessage(Vertical):
def has_expandable_args(self) -> bool:
"""Whether the tool's args are large enough to deserve a collapsible block.
Only `ask_user` qualifies today: its `questions` payload is too noisy to
render inline, but users still need a way to inspect it.
- `ask_user`: its `questions` payload is too noisy to render inline.
- `js_eval`: the header shows only the first code line (truncated at
`JS_EVAL_HEADER_MAX_LENGTH`), so the full program is offered as a
collapsible block whenever it spans more than one non-blank line *or*
a single line is long enough to be truncated in the header.
"""
return self._tool_name == "ask_user" and bool(self._args)
if self._tool_name == "ask_user":
return bool(self._args)
if self._tool_name == "js_eval":
code = self._args.get("code")
if isinstance(code, str) and code.strip():
non_blank = sum(1 for line in code.splitlines() if line.strip())
return non_blank > 1 or len(code.strip()) > JS_EVAL_HEADER_MAX_LENGTH
return False
def _format_code_detail(self) -> Content:
"""Render the `js_eval` program for the collapsible code block.
The code is shown verbatim and left-aligned (its own indentation is the
only indentation), as plain uncolored `Content`. Blank lines of
top/bottom padding add breathing room between the `js_eval` header above
and the "show/hide code" hint below.
Returns:
A plain `Content` renderable with a blank line of padding on
top and bottom.
"""
code = self._args.get("code")
code_str = code.strip("\n") if isinstance(code, str) else str(code)
code_str = render_with_unicode_markers(code_str)
# Blank lines of top/bottom padding separate the block from the header
# line above and the "show/hide code" hint below.
return Content("\n").join((Content(""), Content(code_str), Content("")))
def _format_args_detail(self) -> Content:
"""Render tool arguments as an indented `Content` block.
Falls back to `str(self._args)` (with a visible marker) when JSON
serialization fails — `default=str` already handles most non-serializable
values, so reaching the fallback indicates a deeper issue worth logging.
Renders JSON-pretty-printed args, falling back to `str(self._args)`
(with a visible marker) when JSON serialization fails — `default=str`
already handles most non-serializable values, so reaching the fallback
indicates a deeper issue worth logging. `js_eval` code is handled
separately by `_format_code_detail`.
Returns:
Indented `Content` containing JSON-pretty-printed arguments, or a
@@ -2159,16 +2367,21 @@ class ToolCallMessage(Vertical):
self._args_hint_widget.display = False
return
is_code = self._tool_name == "js_eval"
noun = "code" if is_code else "arguments"
if self._args_expanded:
self._args_widget.update(self._format_args_detail())
detail = (
self._format_code_detail() if is_code else self._format_args_detail()
)
self._args_widget.update(detail)
self._args_widget.display = True
self._args_hint_widget.update(
Content.styled("click or Ctrl+O to hide arguments", "dim italic")
Content.styled(f"click or Ctrl+O to hide {noun}", "dim italic")
)
else:
self._args_widget.display = False
self._args_hint_widget.update(
Content.styled("click or Ctrl+O to show arguments", "dim italic")
Content.styled(f"click or Ctrl+O to show {noun}", "dim italic")
)
self._args_hint_widget.display = True
+32
View File
@@ -3083,6 +3083,38 @@ class TestAskUserLifecycle:
tool.toggle_args.assert_called_once_with()
tool.toggle_output.assert_not_called()
def test_ctrl_o_falls_through_to_args_when_output_unexpandable(self) -> None:
"""Ctrl+O on a tool with unexpandable output toggles its code/args.
Regression: `js_eval` sets `_output` to a short, unexpandable result,
which used to swallow the toggle and leave the collapsible code block
stuck. The action must fall through to args in that case.
"""
app = DeepAgentsApp(agent=MagicMock())
app._pending_ask_user_widget = None
tool = MagicMock()
tool.has_output = True
tool.has_expandable_output = False # short result, nothing to expand
tool.has_expandable_args = True # multi-line code block
def fake_query(query_type: object) -> list[object]:
from deepagents_code.widgets.messages import (
SkillMessage,
ToolCallMessage,
)
if query_type is ToolCallMessage:
return [tool]
if query_type is SkillMessage:
return []
return []
with patch.object(app, "query", side_effect=fake_query):
app.action_toggle_tool_output()
tool.toggle_args.assert_called_once_with()
tool.toggle_output.assert_not_called()
def test_ctrl_o_prefers_tool_with_output_over_expandable_args(self) -> None:
"""Tool with real output wins over a later one with only expandable args."""
app = DeepAgentsApp(agent=MagicMock())
+304
View File
@@ -10,6 +10,7 @@ from textual.content import Content
from deepagents_code import theme
from deepagents_code.input import INPUT_HIGHLIGHT_PATTERN
from deepagents_code.tool_display import JS_EVAL_HEADER_MAX_LENGTH
from deepagents_code.widgets.messages import (
AppMessage,
AssistantMessage,
@@ -1160,6 +1161,75 @@ class TestToolCallMessageExpandableArgs:
await pilot.pause()
assert msg._args_expanded is False
async def test_js_eval_click_toggles_code_when_result_unexpandable(self) -> None:
"""After a short `js_eval` result, clicking must toggle the code block.
Regression: once eval returned, `_output` was set and `on_click`
unconditionally routed to `toggle_output`. A short, unexpandable result
made that a no-op, so the collapsible code block could never open.
"""
from textual.app import App, ComposeResult
class _Harness(App[None]):
def __init__(self) -> None:
super().__init__()
self.msg = ToolCallMessage(
"js_eval",
{"code": "const x = 1;\nx + 1"}, # multi-line -> expandable
)
def compose(self) -> ComposeResult:
yield self.msg
app = _Harness()
async with app.run_test() as pilot:
await pilot.pause()
msg = app.msg
# Eval returns a short, unexpandable result.
msg.set_success("<result>2</result>")
await pilot.pause()
assert msg.has_output is True
assert msg.has_expandable_output is False
assert msg.has_expandable_args is True
event = MagicMock()
msg.on_click(event)
await pilot.pause()
event.stop.assert_called_once()
# Falls through to the code block instead of no-op output toggle.
assert msg._args_expanded is True
async def test_js_eval_click_prefers_expandable_output(self) -> None:
"""When the result *is* expandable, clicking still toggles output."""
from textual.app import App, ComposeResult
class _Harness(App[None]):
def __init__(self) -> None:
super().__init__()
self.msg = ToolCallMessage(
"js_eval",
{"code": "const x = 1;\nx + 1"},
)
def compose(self) -> ComposeResult:
yield self.msg
app = _Harness()
async with app.run_test() as pilot:
await pilot.pause()
msg = app.msg
# A long multi-line stdout makes the output expandable.
body = "\n".join(str(i) for i in range(50))
msg.set_success(f"<stdout>\n{body}\n</stdout>\n<result>done</result>")
await pilot.pause()
assert msg.has_expandable_output is True
event = MagicMock()
msg.on_click(event)
await pilot.pause()
assert msg._expanded is True
assert msg._args_expanded is False
class TestToolCallMessageShellCommand:
"""Test ToolCallMessage shows full shell command for errors.
@@ -1337,6 +1407,240 @@ class TestToolCallMessageShellCommand:
assert result.content.plain == " indented"
class TestToolCallMessageJsEvalOutput:
"""Tests for `_format_js_eval_output`.
The `js_eval` REPL tool returns an XML-ish envelope
(`<stdout>`, `<result>`, `<error>`) with `&`, `<`, `>` escaped. The
formatter unwraps that into labeled, styled sections instead of dumping the
raw blob.
"""
def test_format_single_scalar_result_renders_inline(self) -> None:
"""A lone short scalar result renders inline as `result: value`."""
msg = ToolCallMessage("js_eval", {"code": "1 + 1"})
result = msg._format_output("<result>2</result>", is_preview=False)
assert result.content.plain == "result: 2"
assert result.truncation is None
def test_format_empty_string_result_stays_empty(self) -> None:
"""An empty string result must not be rewritten as `undefined`."""
msg = ToolCallMessage("js_eval", {"code": "''"})
result = msg._format_output("<result></result>", is_preview=False)
assert result.content.plain == "result: "
def test_format_newline_only_result_preserves_body(self) -> None:
"""A newline-only string result remains a real value in block form."""
msg = ToolCallMessage("js_eval", {"code": "'\\n'"})
result = msg._format_output("<result>\n</result>", is_preview=False)
assert result.content.plain.split("\n") == ["result", " ", " "]
def test_format_multiline_result_uses_block(self) -> None:
"""A multi-line result keeps the labeled-block layout."""
msg = ToolCallMessage("js_eval", {"code": "x"})
result = msg._format_output("<result>line1\nline2</result>", is_preview=False)
assert result.content.plain.split("\n") == ["result", " line1", " line2"]
def test_format_long_scalar_result_uses_block(self) -> None:
"""A long single-line result is not collapsed inline."""
msg = ToolCallMessage("js_eval", {"code": "x"})
body = "x" * (msg._JS_EVAL_INLINE_RESULT_MAX + 1)
result = msg._format_output(f"<result>{body}</result>", is_preview=False)
assert result.content.plain.split("\n") == ["result", f" {body}"]
def test_format_stdout_and_result(self) -> None:
"""Stdout and result both render as separate labeled sections."""
msg = ToolCallMessage("js_eval", {"code": "console.log('hi'); 42"})
output = "<stdout>\nhi\n</stdout>\n<result>42</result>"
result = msg._format_output(output, is_preview=False)
# stdout present -> result is not collapsed inline.
lines = result.content.plain.split("\n")
assert lines == ["stdout", " hi", "result", " 42"]
def test_format_unescapes_xml_entities(self) -> None:
"""Escaped `<`, `>`, `&` in the body are restored for display."""
msg = ToolCallMessage("js_eval", {"code": "x"})
output = "<result>&lt;div&gt; &amp;&amp; true</result>"
result = msg._format_output(output, is_preview=False)
# Single short scalar -> inline form.
assert result.content.plain == "result: <div> && true"
def test_format_error_block_includes_type(self) -> None:
"""An error block surfaces the error type in its label."""
msg = ToolCallMessage("js_eval", {"code": "boom()"})
output = '<error type="ReferenceError">boom is not defined</error>'
result = msg._format_output(output, is_preview=False)
lines = result.content.plain.split("\n")
assert lines == ["error (ReferenceError)", " boom is not defined"]
def test_format_handle_result_labeled(self) -> None:
"""A `kind`-tagged result is labeled as a handle."""
msg = ToolCallMessage("js_eval", {"code": "() => 1"})
output = '<result kind="handle">[Function] arity=0</result>'
result = msg._format_output(output, is_preview=False)
lines = result.content.plain.split("\n")
assert lines == ["result (handle)", " [Function] arity=0"]
def test_format_preview_truncates_long_output(self) -> None:
"""Preview mode caps lines and reports the count of hidden lines."""
msg = ToolCallMessage("js_eval", {"code": "x"})
body = "\n".join(str(i) for i in range(50))
output = f"<stdout>\n{body}\n</stdout>\n<result>done</result>"
result = msg._format_output(output, is_preview=True)
shown = len(result.content.plain.split("\n"))
assert shown <= msg._PREVIEW_LINES
# Full render is the stdout label + 50 stdout lines + result label + 1
# result line; the hint reports exactly what the preview dropped.
assert result.truncation == f"{53 - shown} more lines"
def test_format_falls_back_for_unexpected_shape(self) -> None:
"""Output without the REPL envelope falls back to plain lines."""
msg = ToolCallMessage("js_eval", {"code": "x"})
result = msg._format_output("just some text", is_preview=False)
assert result.content.plain == "just some text"
def test_format_preview_caps_long_single_line_by_char_budget(self) -> None:
"""A single huge result line is char-clipped under the preview budget.
Line-count capping alone left a multi-thousand-char single-line result
rendered in full with no truncation hint, flooding the collapsed TUI.
"""
msg = ToolCallMessage("js_eval", {"code": "x"})
body = "x" * 10_000
output = f"<result>{body}</result>"
result = msg._format_output(output, is_preview=True)
# The body line is clipped to the char budget (plus the two-space
# indent) and the hint quantifies the chars dropped from that line so it
# can be expanded.
assert result.truncation == f"{10_000 - msg._PREVIEW_CHARS} more chars"
assert len(result.content.plain) <= msg._PREVIEW_CHARS + len(" ") + len(
"result\n"
)
def test_format_no_char_cap_when_not_preview(self) -> None:
"""Outside preview mode the full long result renders untruncated."""
msg = ToolCallMessage("js_eval", {"code": "x"})
body = "x" * 10_000
output = f"<result>{body}</result>"
result = msg._format_output(output, is_preview=False)
assert result.truncation is None
assert body in result.content.plain
def test_format_stdout_with_fake_tags_is_not_misparsed(self) -> None:
"""Raw tag-like text printed to stdout is preserved, not parsed.
stdout is emitted unescaped, so a program that prints
`</stdout><result>fake</result>` must not be split into spurious
result/error sections — the real trailing result wins.
"""
msg = ToolCallMessage("js_eval", {"code": "x"})
printed = "</stdout><result>fake</result>"
output = f"<stdout>\n{printed}\n</stdout>\n<result>real</result>"
result = msg._format_output(output, is_preview=False)
lines = result.content.plain.split("\n")
# The fake markup survives verbatim inside stdout; only one real result.
assert lines == ["stdout", f" {printed}", "result", " real"]
# Exactly one "result" label line — no spurious section from the print.
assert lines.count("result") == 1
class TestToolCallMessageJsEvalArgs:
"""Tests for `js_eval` header suppression and collapsible code block.
The raw `code=` kwarg must not be dumped on the args line; the header shows
only the first code line, and the full program is offered as a collapsible
block when the snippet spans more than one line.
"""
def test_js_eval_in_tools_with_header_info(self) -> None:
"""`js_eval` is registered so the generic `code=` args line is hidden."""
from deepagents_code.widgets.messages import _TOOLS_WITH_HEADER_INFO
assert "js_eval" in _TOOLS_WITH_HEADER_INFO
def test_single_line_code_not_expandable(self) -> None:
"""One-line code is fully shown in the header — nothing to expand."""
msg = ToolCallMessage("js_eval", {"code": "1 + 1"})
assert msg.has_expandable_args is False
def test_multiline_code_is_expandable(self) -> None:
"""Multi-line code offers a collapsible block."""
msg = ToolCallMessage("js_eval", {"code": "const x = 1;\nx + 1"})
assert msg.has_expandable_args is True
def test_long_single_line_code_is_expandable(self) -> None:
"""A single line too long for the header is still expandable.
The header truncates the first line, so without a collapsible block a
long one-liner (e.g. minified JS) would be unrecoverable in the TUI.
"""
long_line = "x".ljust(JS_EVAL_HEADER_MAX_LENGTH + 1, "y")
msg = ToolCallMessage("js_eval", {"code": long_line})
assert msg.has_expandable_args is True
def test_short_single_line_code_not_expandable(self) -> None:
"""A single line that fits in the header has nothing to expand."""
short_line = "x" * (JS_EVAL_HEADER_MAX_LENGTH - 1)
msg = ToolCallMessage("js_eval", {"code": short_line})
assert msg.has_expandable_args is False
def test_code_detail_is_plain_and_left_aligned(self) -> None:
"""The code is plain `Content`, left-aligned, with blank padding lines."""
code = "const x = 1;\n nested();\nx + 1"
msg = ToolCallMessage("js_eval", {"code": code})
detail = msg._format_code_detail()
from textual.content import Content
assert isinstance(detail, Content)
# Blank padding lines top and bottom; code's own indentation is
# preserved and no extra indent is injected.
assert detail.plain.split("\n") == [
"",
"const x = 1;",
" nested();",
"x + 1",
"",
]
def test_code_detail_is_unstyled(self) -> None:
"""No syntax highlighting: the rendered code carries no style spans."""
msg = ToolCallMessage("js_eval", {"code": "const x = 1;\nx + 1"})
detail = msg._format_code_detail()
assert not detail.spans
def test_code_detail_strips_surrounding_blank_lines(self) -> None:
"""Code's own surrounding blanks are trimmed (padding lines remain)."""
msg = ToolCallMessage("js_eval", {"code": "\n\nconst x = 1;\n\n"})
detail = msg._format_code_detail()
# One blank padding line top and bottom, around the trimmed code.
assert detail.plain == "\nconst x = 1;\n"
def test_code_detail_marks_hidden_unicode(self) -> None:
"""Hidden controls in expanded code are rendered as visible markers."""
msg = ToolCallMessage("js_eval", {"code": "const safe = 1;\n//\u202e hidden"})
detail = msg._format_code_detail()
assert "\u202e" not in detail.plain
assert "<U+202E RIGHT-TO-LEFT OVERRIDE>" in detail.plain
class TestToolCallMessageFileOutput:
"""Tests for `_format_file_output` char-budget handling.
@@ -291,6 +291,27 @@ class TestFormatToolDisplay:
result = format_tool_display("execute", {"command": "ls", "timeout": "abc"})
assert "timeout" not in result
# --- js_eval ---
def test_js_eval_single_line_shows_full_snippet(self) -> None:
result = format_tool_display("js_eval", {"code": "1 + 1"})
assert result == f'{_PREFIX} js_eval("1 + 1")'
def test_js_eval_multiline_shows_first_line_with_ellipsis(self) -> None:
result = format_tool_display(
"js_eval", {"code": "const x = 1;\nconst y = 2;\nx + y"}
)
# First non-blank line only, with an ellipsis marking the elision.
assert result == f'{_PREFIX} js_eval("const x = 1;{ASCII_GLYPHS.ellipsis}")'
def test_js_eval_skips_leading_blank_lines(self) -> None:
result = format_tool_display("js_eval", {"code": "\n\nreal();\nmore();"})
assert result == f'{_PREFIX} js_eval("real();{ASCII_GLYPHS.ellipsis}")'
def test_js_eval_empty_code(self) -> None:
result = format_tool_display("js_eval", {"code": " "})
assert result == f"{_PREFIX} js_eval()"
# --- ls ---
def test_ls_with_path(self) -> None: