Skip to content
Closed
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
43 changes: 42 additions & 1 deletion openhands_cli/agent_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,48 @@ def setup_agent() -> tuple[LLM | None, CodeActAgent | None, Conversation | None]

# Setup tools
cwd = os.getcwd()
bash = BashExecutor(working_dir=cwd)
try:
bash = BashExecutor(working_dir=cwd)
except Exception as e:
# Provide a friendly message when tmux is missing locally
tmux_missing = e.__class__.__name__ == "TmuxCommandNotFound"
if not tmux_missing:
try:
from libtmux.exc import (
TmuxCommandNotFound as _TmuxCommandNotFound, # type: ignore
)

tmux_missing = isinstance(e, _TmuxCommandNotFound)
except Exception:
pass
if not tmux_missing:
msg = str(e)
if msg:
lower = msg.lower()
tmux_missing = (
"tmuxcommandnotfound" in lower
or "tmux command not found" in lower
or "tmux" in lower
)
if tmux_missing:
print_formatted_text(
HTML(
"<red>Error: tmux is not installed or not found in PATH.</red>"
)
)
print_formatted_text(
HTML(
"<yellow>OpenHands CLI requires tmux to manage the local shell session.</yellow>"
)
)
print_formatted_text(
HTML(
"<grey>Install examples — macOS: brew install tmux | Ubuntu/Debian: sudo apt-get install tmux | Fedora: sudo dnf install tmux | Arch: sudo pacman -S tmux</grey>"
)
)
return None, None, None
# Re-raise to be handled by the outer exception handler
raise
file_editor = FileEditorExecutor()
tools: list[Tool] = [
execute_bash_tool.set_executor(executor=bash),
Expand Down
56 changes: 56 additions & 0 deletions tests/test_agent_chat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""Tests for agent chat error handling and setup behavior."""

import os
from unittest.mock import MagicMock, patch

import pytest
from libtmux.exc import TmuxCommandNotFound


class TestAgentChatTmuxHandling:
"""Test friendly error handling when tmux is missing."""

@patch.dict(os.environ, {"LITELLM_API_KEY": "dummy-key"}, clear=False)
@patch("openhands_cli.agent_chat.print_formatted_text")
@patch("openhands_cli.agent_chat.BashExecutor", side_effect=TmuxCommandNotFound)
@patch("openhands_cli.agent_chat.LLM")
def test_setup_agent_handles_missing_tmux(
self,
mock_llm: MagicMock,
_mock_bash: MagicMock,
mock_print: MagicMock,
) -> None:
"""setup_agent should catch TmuxCommandNotFound and print helpful guidance."""
from openhands_cli.agent_chat import setup_agent

llm, agent, conv = setup_agent()

assert (llm, agent, conv) == (None, None, None)

# Combine printed HTML strings for assertion
printed = "\n".join(str(call.args[0]) for call in mock_print.call_args_list)
assert "tmux is not installed or not found in PATH" in printed
assert "requires tmux" in printed
assert "Install examples" in printed

@patch.dict(os.environ, {"LITELLM_API_KEY": "dummy-key"}, clear=False)
@patch("openhands_cli.agent_chat.print_formatted_text")
@patch(
"openhands_cli.agent_chat.BashExecutor",
side_effect=Exception("TmuxCommandNotFound"),
)
@patch("openhands_cli.agent_chat.LLM")
def test_setup_agent_handles_missing_tmux_by_name(
self,
mock_llm: MagicMock,
_mock_bash: MagicMock,
mock_print: MagicMock,
) -> None:
"""Also handle when only the exception class name matches (defensive)."""
from openhands_cli.agent_chat import setup_agent

llm, agent, conv = setup_agent()

assert (llm, agent, conv) == (None, None, None)
printed = "\n".join(str(call.args[0]) for call in mock_print.call_args_list)
assert "tmux is not installed or not found in PATH" in printed
Loading