Compare commits

...

7 Commits

Author SHA1 Message Date
rohitvinodmalhotra@gmail.com c6ba13aef2 Update uv.lock 2025-10-31 15:04:25 -04:00
rohitvinodmalhotra@gmail.com 644cd8d336 Update pyproject.toml 2025-10-31 14:52:51 -04:00
Rohit Malhotra 231019974c CLI: fix binary build (#11591) 2025-10-31 18:01:29 +00:00
Rohit Malhotra d246ab1a21 Hotfix(CLI): make settings page available even when conversation hasn't been created (#11588)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-31 17:19:53 +00:00
jpelletier1 15c207c401 Disables Copilot icon by default (#11589) 2025-10-31 17:06:15 +00:00
Rohit Malhotra cf21cfed6c Hotfix(CLI): make sure to update condenser credentials (#11587)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-31 16:37:59 +00:00
Rohit Malhotra 12d57df6ac CLI Patch release 1.0.4 (#11585) 2025-10-31 14:59:39 +00:00
9 changed files with 136 additions and 22 deletions
@@ -71,6 +71,14 @@ jobs:
echo "✅ Build & test finished without ❌ markers"
- name: Verify binary files exist
run: |
if ! ls openhands-cli/dist/openhands* 1> /dev/null 2>&1; then
echo "❌ No binaries found to upload!"
exit 1
fi
echo "✅ Found binaries to upload."
- name: Upload binary artifact
uses: actions/upload-artifact@v4
with:
+16 -12
View File
@@ -20,15 +20,6 @@ from openhands_cli.locations import AGENT_SETTINGS_PATH, PERSISTENCE_DIR
from openhands.sdk import LLM
dummy_agent = get_default_cli_agent(
llm=LLM(
model='dummy-model',
api_key='dummy-key',
metadata=get_llm_metadata(model_name='dummy-model', llm_type='openhands'),
),
cli_mode=True,
)
# =================================================
# SECTION: Build Binary
# =================================================
@@ -126,7 +117,7 @@ def _is_welcome(line: str) -> bool:
return any(marker in s for marker in WELCOME_MARKERS)
def test_executable() -> bool:
def test_executable(dummy_agent) -> bool:
"""Test the built executable, measuring boot time and total test time."""
print('🧪 Testing the built executable...')
@@ -274,7 +265,14 @@ def main() -> int:
# Test the executable
if not args.no_test:
if not test_executable():
dummy_agent = get_default_cli_agent(
llm=LLM(
model='dummy-model',
api_key='dummy-key',
metadata=get_llm_metadata(model_name='dummy-model', llm_type='openhands'),
)
)
if not test_executable(dummy_agent):
print('❌ Executable test failed, build process failed')
return 1
@@ -285,4 +283,10 @@ def main() -> int:
if __name__ == '__main__':
sys.exit(main())
try:
sys.exit(main())
except Exception as e:
print(e)
print('❌ Executable test failed')
sys.exit(1)
+1 -1
View File
@@ -127,7 +127,7 @@ def run_cli_entry(resume_conversation_id: str | None = None) -> None:
break
elif command == '/settings':
settings_screen = SettingsScreen(conversation)
settings_screen = SettingsScreen(runner.conversation if runner else None)
settings_screen.display_settings()
continue
@@ -1,6 +1,6 @@
import os
from openhands.sdk import LLM, BaseConversation, LocalFileStore
from openhands.sdk import LLM, BaseConversation, LLMSummarizingCondenser, LocalFileStore
from prompt_toolkit import HTML, print_formatted_text
from prompt_toolkit.shortcuts import print_container
from prompt_toolkit.widgets import Frame, TextArea
@@ -33,9 +33,6 @@ class SettingsScreen:
agent_spec = self.agent_store.load()
if not agent_spec:
return
assert self.conversation is not None, (
'Conversation must be set to display settings.'
)
llm = agent_spec.llm
advanced_llm_settings = True if llm.base_url else False
@@ -62,12 +59,20 @@ class SettingsScreen:
labels_and_values.extend(
[
(' API Key', '********' if llm.api_key else 'Not Set'),
]
)
if self.conversation:
labels_and_values.extend([
(
' Confirmation Mode',
'Enabled'
if self.conversation.is_confirmation_mode_active
else 'Disabled',
),
)
])
labels_and_values.extend([
(
' Memory Condensation',
'Enabled' if agent_spec.condenser else 'Disabled',
@@ -153,7 +158,7 @@ class SettingsScreen:
api_key = prompt_api_key(
step_counter,
custom_model.split('/')[0] if len(custom_model.split('/')) > 1 else '',
self.conversation.agent.llm.api_key if self.conversation else None,
self.conversation.state.agent.llm.api_key if self.conversation else None,
escapable=escapable,
)
memory_condensation = choose_memory_condensation(step_counter)
@@ -182,7 +187,14 @@ class SettingsScreen:
if not agent:
agent = get_default_cli_agent(llm=llm)
# Must update all LLMs
agent = agent.model_copy(update={'llm': llm})
condenser = LLMSummarizingCondenser(
llm=llm.model_copy(
update={"usage_id": "condenser"}
)
)
agent = agent.model_copy(update={'condenser': condenser})
self.agent_store.save(agent)
def _save_advanced_settings(
+1 -1
View File
@@ -4,7 +4,7 @@ requires = [ "hatchling>=1.25" ]
[project]
name = "openhands"
version = "1.0.3"
version = "1.0.5"
description = "OpenHands CLI - Terminal User Interface for OpenHands AI Agent"
readme = "README.md"
license = { text = "MIT" }
@@ -0,0 +1,57 @@
"""Test for the /settings command functionality."""
from unittest.mock import MagicMock, patch
from prompt_toolkit.input.defaults import create_pipe_input
from prompt_toolkit.output.defaults import DummyOutput
from openhands_cli.agent_chat import run_cli_entry
from openhands_cli.user_actions import UserConfirmation
@patch('openhands_cli.agent_chat.exit_session_confirmation')
@patch('openhands_cli.agent_chat.get_session_prompter')
@patch('openhands_cli.agent_chat.setup_conversation')
@patch('openhands_cli.agent_chat.verify_agent_exists_or_setup_agent')
@patch('openhands_cli.agent_chat.ConversationRunner')
@patch('openhands_cli.agent_chat.SettingsScreen')
def test_settings_command_works_without_conversation(
mock_settings_screen_class,
mock_runner_cls,
mock_verify_agent,
mock_setup_conversation,
mock_get_session_prompter,
mock_exit_confirm,
):
"""Test that /settings command works when no conversation is active (bug fix scenario)."""
# Auto-accept the exit prompt to avoid interactive UI
mock_exit_confirm.return_value = UserConfirmation.ACCEPT
# Mock agent verification to succeed
mock_agent = MagicMock()
mock_verify_agent.return_value = mock_agent
# Mock the SettingsScreen instance
mock_settings_screen = MagicMock()
mock_settings_screen_class.return_value = mock_settings_screen
# No runner initially (simulates starting CLI without a conversation)
mock_runner_cls.return_value = None
# Real session fed by a pipe
from openhands_cli.user_actions.utils import get_session_prompter as real_get_session_prompter
with create_pipe_input() as pipe:
output = DummyOutput()
session = real_get_session_prompter(input=pipe, output=output)
mock_get_session_prompter.return_value = session
# Trigger /settings, then /exit (exit will be auto-accepted)
for ch in "/settings\r/exit\r":
pipe.send_text(ch)
run_cli_entry(None)
# Assert SettingsScreen was created with None conversation (the bug fix)
mock_settings_screen_class.assert_called_once_with(None)
# Assert display_settings was called (settings screen was shown)
mock_settings_screen.display_settings.assert_called_once()
@@ -121,6 +121,38 @@ def test_update_existing_settings_workflow(tmp_path: Path):
assert True # If we get here, the workflow completed successfully
def test_all_llms_in_agent_are_updated():
"""Test that modifying LLM settings creates multiple LLMs with same API key but different usage_ids."""
# Create a screen with existing agent settings
screen = SettingsScreen(conversation=None)
initial_llm = LLM(model='openai/gpt-3.5-turbo', api_key=SecretStr('sk-initial'), usage_id='test-service')
initial_agent = get_default_cli_agent(llm=initial_llm)
# Mock the agent store to return the initial agent and capture the save call
with (
patch.object(screen.agent_store, 'load', return_value=initial_agent),
patch.object(screen.agent_store, 'save') as mock_save
):
# Modify the LLM settings with new API key
screen._save_llm_settings(model='openai/gpt-4o-mini', api_key='sk-updated-123')
mock_save.assert_called_once()
# Get the saved agent from the mock
saved_agent = mock_save.call_args[0][0]
all_llms = list(saved_agent.get_all_llms())
assert len(all_llms) >= 2, f"Expected at least 2 LLMs, got {len(all_llms)}"
# Verify all LLMs have the same API key
api_keys = [llm.api_key.get_secret_value() for llm in all_llms]
assert all(api_key == 'sk-updated-123' for api_key in api_keys), \
f"Not all LLMs have the same API key: {api_keys}"
# Verify none of the usage_id attributes match
usage_ids = [llm.usage_id for llm in all_llms]
assert len(set(usage_ids)) == len(usage_ids), \
f"Some usage_ids are duplicated: {usage_ids}"
@pytest.mark.parametrize(
'step_to_cancel',
['type', 'provider', 'model', 'apikey', 'save'],
+1 -1
View File
@@ -1828,7 +1828,7 @@ wheels = [
[[package]]
name = "openhands"
version = "1.0.3"
version = "1.0.5"
source = { editable = "." }
dependencies = [
{ name = "openhands-sdk" },
@@ -1,4 +1,5 @@
{
"workbench.colorTheme": "Default Dark Modern",
"workbench.startupEditor": "none"
"workbench.startupEditor": "none",
"chat.commandCenter.enabled": false
}