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
4 changes: 4 additions & 0 deletions cecli/coders/agent_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -771,6 +771,10 @@ async def gather_and_await():
all_results_content.append(f"Error in tool execution: {res}")
else:
all_results_content.append(str(res))
if not getattr(tool_module, "SKIP_EXECUTE_RESULT_UI", False):
from cecli.tools.utils.output import emit_execute_result_to_ui

emit_execute_result_to_ui(self, res)

if not await HookIntegration.call_post_tool_hooks(
self, tool_name, args_string, "\n\n".join(all_results_content)
Expand Down
1 change: 1 addition & 0 deletions cecli/tools/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
class Tool(BaseTool):
NORM_NAME = "command"
TRACK_INVOCATIONS = False
SKIP_EXECUTE_RESULT_UI = True
SCHEMA = {
"type": "function",
"function": {
Expand Down
22 changes: 0 additions & 22 deletions cecli/tools/grep.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,28 +203,6 @@ def execute(

final_message = "\n\n".join(all_results)

if coder.tui and coder.tui():
# For the UI, show a summary to avoid cluttering the terminal
ui_summaries = []
for search_op, result in zip(searches, all_results):
pattern = search_op.get("pattern")
if "No matches found" in result:
ui_summaries.append(f"✗ No matches found for '{pattern}'.")
elif "Error" in result:
ui_summaries.append(f"✗ Error searching for '{pattern}'.")
else:
# Count lines in the output to give a sense of scale
# The result string contains the matches in a code block
match_count = (
result.count("\n") - 2
) # Subtracting for the markdown block markers
if match_count < 0:
match_count = 0
ui_summaries.append(f"✓ Matches found for '{pattern}'.")

ui_message = "\n".join(ui_summaries)
coder.io.tool_output(ui_message, type="tool-result")

return final_message

@classmethod
Expand Down
1 change: 1 addition & 0 deletions cecli/tools/ls.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
class Tool(BaseTool):
NORM_NAME = "ls"
TRACK_INVOCATIONS = False
SKIP_EXECUTE_RESULT_UI = True
SCHEMA = {
"type": "function",
"function": {
Expand Down
12 changes: 10 additions & 2 deletions cecli/tools/utils/base_tool.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from abc import ABC, abstractmethod

import asyncio

from cecli.tools.utils.helpers import handle_tool_error
from cecli.tools.utils.output import print_tool_response
from cecli.tools.utils.output import emit_execute_result_to_ui, print_tool_response
from cecli.tools.validations import ToolValidations


Expand All @@ -16,6 +18,9 @@ class BaseTool(ABC):
# Declarative validations (maps param paths to lists of validation method names)
VALIDATIONS = {}

# When True, execute() already streamed full output via tool_output (skip UI mirror).
SKIP_EXECUTE_RESULT_UI = False

# Invocation tracking for detecting repeated tool calls
_invocations = {} # Dict to store last 3 invocations per tool
_invocation_summary = set() # Set to track distinct tool names
Expand Down Expand Up @@ -133,7 +138,10 @@ def process_response(cls, coder, params):
params = ToolValidations.validate_params(params, cls.VALIDATIONS, cls.SCHEMA)

try:
return cls.execute(coder, **params)
result = cls.execute(coder, **params)
if not asyncio.iscoroutine(result) and not cls.SKIP_EXECUTE_RESULT_UI:
emit_execute_result_to_ui(coder, result)
return result
except Exception as e:
return handle_tool_error(coder, cls.SCHEMA.get("function").get("name"), e)

Expand Down
21 changes: 21 additions & 0 deletions cecli/tools/utils/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,27 @@
import re


def emit_execute_result_to_ui(coder, result, *, max_lines: int = 200) -> None:
"""
Mirror tool ``execute()`` return text to ``tool_output`` for headless UI consumers
(e.g. BrightVision chat tool cards). Skips Textual TUI sessions.
"""
if coder.tui and coder.tui():
return
text = ("" if result is None else str(result)).strip()
if not text:
return
if text.startswith("Error:") and "\n" not in text and len(text) < 400:
return
lines = text.splitlines()
if len(lines) > max_lines:
body = "\n".join(lines[:max_lines])
body += f"\n… ({len(lines) - max_lines} more lines)"
else:
body = text
coder.io.tool_output(body)


def print_tool_response(coder, mcp_server, tool_response, params=None):
"""
Format the output for display.
Expand Down
67 changes: 67 additions & 0 deletions tests/tools/test_tool_result_ui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from types import SimpleNamespace
from unittest.mock import Mock

from cecli.tools.utils.output import emit_execute_result_to_ui


def test_emit_execute_result_to_ui_skips_tui():
io = Mock()
coder = SimpleNamespace(tui=lambda: object(), io=io)
emit_execute_result_to_ui(coder, "hello")
io.tool_output.assert_not_called()


def test_emit_execute_result_to_ui_emits_body_for_headless():
io = Mock()
coder = SimpleNamespace(tui=lambda: None, io=io)
emit_execute_result_to_ui(coder, "line one\nline two")
io.tool_output.assert_called_once_with("line one\nline two")


def test_emit_execute_result_to_ui_truncates_long_output():
io = Mock()
coder = SimpleNamespace(tui=lambda: None, io=io)
long_text = "\n".join(f"line {i}" for i in range(250))
emit_execute_result_to_ui(coder, long_text, max_lines=200)
emitted = io.tool_output.call_args[0][0]
assert emitted.startswith("line 0")
assert "… (50 more lines)" in emitted


def test_grep_process_response_emits_matches_to_ui(monkeypatch, tmp_path):
from cecli.tools import grep

sample = tmp_path / "example.txt"
sample.write_text("hello ollama world\n")

io = Mock()
coder = SimpleNamespace(
repo=SimpleNamespace(root=str(tmp_path)),
io=io,
verbose=False,
root=str(tmp_path),
tui=lambda: None,
)

monkeypatch.setattr(grep.Tool, "_find_search_tool", lambda: ("grep", "/usr/bin/grep"))

result = grep.Tool.process_response(
coder,
{
"searches": [
{
"pattern": "ollama",
"file_glob": "*.txt",
"directory": ".",
"use_regex": False,
"case_insensitive": False,
"context_before": 0,
"context_after": 0,
}
]
},
)

assert "Matches for" in result
emitted = [str(call.args[0]) for call in io.tool_output.call_args_list]
assert any("ollama" in line for line in emitted)
Loading