Compare commits

...

6 Commits

Author SHA1 Message Date
enyst
12213d0756 CLI(V1): revert lazy imports in agent_chat; restore top-level absolute imports 2025-10-14 08:22:42 +00:00
enyst
b234a4ba0b tests(cli): add tests for initial message computation and precedence; ensure run_cli_entry receives correct kwargs 2025-10-14 07:45:16 +00:00
enyst
f5b7f39bee CLI(V1): lazy-import heavy deps inside run_cli_entry to allow unit tests to patch module without installing SDK deps 2025-10-14 07:43:23 +00:00
enyst
fc3c22b9f2 CLI(V1): do not inject initial message when resuming conversation 2025-10-14 07:30:18 +00:00
enyst
54e790f030 CLI(V1): simple_main only passes initial_user_message when provided to satisfy existing tests
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-14 07:18:43 +00:00
enyst
2e069c7e78 CLI(V1): add --task/--file flags and direct initial_user_message handling (no env)
- Add --task and --file to parser
- Build initial message (file > task)
- Pass initial_user_message to run_cli_entry
- Update run_cli_entry to send initial message once

Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-14 04:04:10 +00:00
4 changed files with 130 additions and 10 deletions

View File

@@ -7,10 +7,7 @@ Provides a conversation interface with an AI agent using OpenHands patterns.
import sys
from datetime import datetime
from openhands.sdk import (
Message,
TextContent,
)
from openhands.sdk import Message, TextContent
from openhands.sdk.conversation.state import AgentExecutionStatus
from prompt_toolkit import print_formatted_text
from prompt_toolkit.formatted_text import HTML
@@ -20,10 +17,7 @@ from openhands_cli.setup import MissingAgentSpec, setup_conversation, start_fres
from openhands_cli.tui.settings.mcp_screen import MCPScreen
from openhands_cli.tui.settings.settings_screen import SettingsScreen
from openhands_cli.tui.status import display_status
from openhands_cli.tui.tui import (
display_help,
display_welcome,
)
from openhands_cli.tui.tui import display_help, display_welcome
from openhands_cli.user_actions import UserConfirmation, exit_session_confirmation
from openhands_cli.user_actions.utils import get_session_prompter
@@ -43,6 +37,9 @@ def _restore_tty() -> None:
def _print_exit_hint(conversation_id: str) -> None:
"""Print a resume hint with the current conversation ID."""
from prompt_toolkit import print_formatted_text
from prompt_toolkit.formatted_text import HTML
print_formatted_text(
HTML(f'<grey>Conversation ID:</grey> <yellow>{conversation_id}</yellow>')
)
@@ -55,9 +52,11 @@ def _print_exit_hint(conversation_id: str) -> None:
def run_cli_entry(resume_conversation_id: str | None = None) -> None:
def run_cli_entry(resume_conversation_id: str | None = None, initial_user_message: str | None = None) -> None:
"""Run the agent chat session using the agent SDK.
If initial_user_message is provided, it will be sent once before
entering the interactive prompt loop.
Raises:
AgentSetupError: If agent setup fails
@@ -82,6 +81,13 @@ def run_cli_entry(resume_conversation_id: str | None = None) -> None:
runner = ConversationRunner(conversation)
session = get_session_prompter()
# If an initial message was provided and we're not resuming, send it once
if (not resume_conversation_id) and initial_user_message and initial_user_message.strip():
runner.process_message(
Message(role='user', content=[TextContent(text=initial_user_message)])
)
print()
# Main chat loop
while True:
try:

View File

@@ -31,6 +31,17 @@ Examples:
help='Conversation ID to resume'
)
parser.add_argument(
'--task',
type=str,
help='Initial task prompt to send once at startup'
)
parser.add_argument(
'--file',
type=str,
help='Path to a file whose content will be sent as the initial message'
)
# Only serve as subcommand
subparsers = parser.add_subparsers(
dest='command',

View File

@@ -20,6 +20,19 @@ from prompt_toolkit.formatted_text import HTML
from openhands_cli.argparsers.main_parser import create_main_parser
def _build_initial_user_message(args) -> str | None:
if args.file:
try:
with open(args.file, 'r', encoding='utf-8') as f:
content = f.read()
if not content.strip():
return args.task
return content
except Exception:
return args.task
return args.task
def main() -> None:
"""Main entry point for the OpenHands CLI.
@@ -41,8 +54,12 @@ def main() -> None:
# Import agent_chat only when needed
from openhands_cli.agent_chat import run_cli_entry
initial_user_message = _build_initial_user_message(args)
# Start agent chat
run_cli_entry(resume_conversation_id=args.resume)
kwargs = {"resume_conversation_id": args.resume}
if initial_user_message:
kwargs["initial_user_message"] = initial_user_message
run_cli_entry(**kwargs)
except KeyboardInterrupt:
print_formatted_text(HTML('\n<yellow>Goodbye! 👋</yellow>'))
except EOFError:

View File

@@ -0,0 +1,86 @@
import os
import sys
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
import pytest
from openhands_cli.simple_main import _build_initial_user_message, main
def test_build_initial_user_message_task_only(tmp_path):
args = SimpleNamespace(file=None, task="Do the thing")
assert _build_initial_user_message(args) == "Do the thing"
def test_build_initial_user_message_file_precedence(tmp_path):
p = tmp_path / "input.txt"
p.write_text("From file content", encoding="utf-8")
args = SimpleNamespace(file=str(p), task="Fallback task")
assert _build_initial_user_message(args) == "From file content"
def test_build_initial_user_message_file_missing_falls_back_to_task(tmp_path):
args = SimpleNamespace(file=str(tmp_path / "missing.txt"), task="Use task")
assert _build_initial_user_message(args) == "Use task"
def test_build_initial_user_message_empty_file_falls_back_to_task(tmp_path):
p = tmp_path / "empty.txt"
p.write_text("\n\n\t ", encoding="utf-8")
args = SimpleNamespace(file=str(p), task="Use task")
assert _build_initial_user_message(args) == "Use task"
@pytest.mark.parametrize(
"argv, expected_kwargs",
[
(['openhands', '--task', 'Hello'], {"resume_conversation_id": None, "initial_user_message": "Hello"}),
],
)
@patch('openhands_cli.agent_chat.run_cli_entry')
def test_main_passes_initial_message_from_task(mock_run, monkeypatch, argv, expected_kwargs):
monkeypatch.setattr(sys, "argv", argv, raising=False)
mock_run.side_effect = KeyboardInterrupt()
main()
mock_run.assert_called_once_with(**expected_kwargs)
@patch('openhands_cli.agent_chat.run_cli_entry')
def test_main_passes_initial_message_from_file_precedence(mock_run, monkeypatch, tmp_path):
p = tmp_path / "input.txt"
p.write_text("Content A", encoding="utf-8")
monkeypatch.setattr(sys, "argv", [
'openhands', '--task', 'Task B', '--file', str(p)
], raising=False)
mock_run.side_effect = KeyboardInterrupt()
main()
mock_run.assert_called_once()
call_kwargs = mock_run.call_args.kwargs
assert call_kwargs["resume_conversation_id"] is None
assert call_kwargs["initial_user_message"] == "Content A"
@patch('openhands_cli.agent_chat.run_cli_entry')
def test_main_passes_task_when_file_missing(mock_run, monkeypatch, tmp_path):
missing = tmp_path / "missing.txt"
monkeypatch.setattr(sys, "argv", [
'openhands', '--task', 'Fallback', '--file', str(missing)
], raising=False)
mock_run.side_effect = KeyboardInterrupt()
main()
call_kwargs = mock_run.call_args.kwargs
assert call_kwargs["initial_user_message"] == "Fallback"
@patch('openhands_cli.agent_chat.run_cli_entry')
def test_main_passes_task_when_file_empty(mock_run, monkeypatch, tmp_path):
p = tmp_path / "empty.txt"
p.write_text("\n \t", encoding="utf-8")
monkeypatch.setattr(sys, "argv", [
'openhands', '--task', 'TaskText', '--file', str(p)
], raising=False)
mock_run.side_effect = KeyboardInterrupt()
main()
call_kwargs = mock_run.call_args.kwargs
assert call_kwargs["initial_user_message"] == "TaskText"