diff --git a/openhands-cli/openhands_cli/agent_chat.py b/openhands-cli/openhands_cli/agent_chat.py
index b7fcaaf359..766deec8dc 100644
--- a/openhands-cli/openhands_cli/agent_chat.py
+++ b/openhands-cli/openhands_cli/agent_chat.py
@@ -26,6 +26,7 @@ from openhands_cli.tui.tui import (
)
from openhands_cli.user_actions import UserConfirmation, exit_session_confirmation
from openhands_cli.user_actions.utils import get_session_prompter
+from openhands_cli.user_actions.working_directory_action import prompt_working_directory_configuration
def _restore_tty() -> None:
@@ -65,6 +66,16 @@ def run_cli_entry(resume_conversation_id: str | None = None) -> None:
EOFError: If EOF is encountered
"""
+ # Configure working directory before starting conversation
+ if not resume_conversation_id:
+ # Only prompt for working directory on new conversations
+ try:
+ working_dir = prompt_working_directory_configuration()
+ print_formatted_text(HTML(f'Using working directory: {working_dir}\n'))
+ except KeyboardInterrupt:
+ print_formatted_text(HTML('\nGoodbye! 👋'))
+ return
+
try:
conversation = start_fresh_conversation(resume_conversation_id)
except MissingAgentSpec:
diff --git a/openhands-cli/openhands_cli/locations.py b/openhands-cli/openhands_cli/locations.py
index fe01a30a28..30720f25ac 100644
--- a/openhands-cli/openhands_cli/locations.py
+++ b/openhands-cli/openhands_cli/locations.py
@@ -1,13 +1,68 @@
+import json
import os
+from pathlib import Path
+from typing import Optional
# Configuration directory for storing agent settings and CLI configuration
PERSISTENCE_DIR = os.path.expanduser('~/.openhands')
CONVERSATIONS_DIR = os.path.join(PERSISTENCE_DIR, 'conversations')
-# Working directory for agent operations (current directory where CLI is run)
-WORK_DIR = os.getcwd()
-
AGENT_SETTINGS_PATH = 'agent_settings.json'
# MCP configuration file (relative to PERSISTENCE_DIR)
MCP_CONFIG_FILE = 'mcp.json'
+
+# CLI settings file (relative to PERSISTENCE_DIR)
+CLI_SETTINGS_FILE = 'oh_cli_settings.json'
+
+
+def get_configured_working_directory() -> Optional[str]:
+ """Get the configured working directory from CLI settings.
+
+ Returns:
+ The configured working directory path if set, None otherwise.
+ """
+ try:
+ cli_settings_path = Path(PERSISTENCE_DIR) / CLI_SETTINGS_FILE
+ if cli_settings_path.exists():
+ with open(cli_settings_path, 'r') as f:
+ settings = json.load(f)
+ working_dir = settings.get('working_directory')
+ if working_dir and os.path.exists(working_dir):
+ return working_dir
+ except (json.JSONDecodeError, OSError):
+ pass
+ return None
+
+
+def save_working_directory(working_dir: str) -> None:
+ """Save the working directory to CLI settings.
+
+ Args:
+ working_dir: The working directory path to save.
+ """
+ # Ensure persistence directory exists
+ os.makedirs(PERSISTENCE_DIR, exist_ok=True)
+
+ cli_settings_path = Path(PERSISTENCE_DIR) / CLI_SETTINGS_FILE
+
+ # Load existing settings or create new ones
+ settings = {}
+ if cli_settings_path.exists():
+ try:
+ with open(cli_settings_path, 'r') as f:
+ settings = json.load(f)
+ except (json.JSONDecodeError, OSError):
+ settings = {}
+
+ # Update working directory
+ settings['working_directory'] = working_dir
+
+ # Save settings
+ with open(cli_settings_path, 'w') as f:
+ json.dump(settings, f, indent=2)
+
+
+# Working directory for agent operations
+# First try to get configured directory, fallback to current directory
+WORK_DIR = get_configured_working_directory() or os.getcwd()
diff --git a/openhands-cli/openhands_cli/setup.py b/openhands-cli/openhands_cli/setup.py
index 9e74fa99be..a6abfb73ef 100644
--- a/openhands-cli/openhands_cli/setup.py
+++ b/openhands-cli/openhands_cli/setup.py
@@ -1,3 +1,4 @@
+import os
import uuid
from prompt_toolkit import HTML, print_formatted_text
@@ -7,7 +8,7 @@ from openhands.tools.execute_bash import BashTool
from openhands.tools.file_editor import FileEditorTool
from openhands.tools.task_tracker import TaskTrackerTool
from openhands_cli.listeners import LoadingContext
-from openhands_cli.locations import CONVERSATIONS_DIR, WORK_DIR
+from openhands_cli.locations import CONVERSATIONS_DIR, get_configured_working_directory
from openhands_cli.tui.settings.store import AgentStore
from openhands.sdk.security.confirmation_policy import (
AlwaysConfirm,
@@ -69,10 +70,13 @@ def setup_conversation(
update={"security_analyzer": None}
)
+ # Get current working directory (may have been updated)
+ current_work_dir = get_configured_working_directory() or os.getcwd()
+
# Create conversation - agent context is now set in AgentStore.load()
conversation: BaseConversation = Conversation(
agent=agent,
- workspace=Workspace(working_dir=WORK_DIR),
+ workspace=Workspace(working_dir=current_work_dir),
# Conversation will add / to this path
persistence_dir=CONVERSATIONS_DIR,
conversation_id=conversation_id,
diff --git a/openhands-cli/openhands_cli/tui/settings/settings_screen.py b/openhands-cli/openhands_cli/tui/settings/settings_screen.py
index 35cd76e1de..5cd0c5fa12 100644
--- a/openhands-cli/openhands_cli/tui/settings/settings_screen.py
+++ b/openhands-cli/openhands_cli/tui/settings/settings_screen.py
@@ -8,7 +8,7 @@ from prompt_toolkit.shortcuts import print_container
from prompt_toolkit.widgets import Frame, TextArea
from openhands_cli.llm_utils import get_llm_metadata
-from openhands_cli.locations import AGENT_SETTINGS_PATH, PERSISTENCE_DIR
+from openhands_cli.locations import AGENT_SETTINGS_PATH, PERSISTENCE_DIR, get_configured_working_directory
from openhands_cli.pt_style import COLOR_GREY
from openhands_cli.tui.settings.store import AgentStore
from openhands_cli.tui.utils import StepCounter
@@ -23,6 +23,7 @@ from openhands_cli.user_actions.settings_action import (
save_settings_confirmation,
settings_type_confirmation,
)
+from openhands_cli.user_actions.working_directory_action import configure_working_directory_in_settings
class SettingsScreen:
@@ -61,6 +62,9 @@ class SettingsScreen:
(' Base URL', llm.base_url),
]
)
+ # Get current working directory info
+ current_work_dir = get_configured_working_directory() or os.getcwd()
+
labels_and_values.extend(
[
(' API Key', '********' if llm.api_key else 'Not Set'),
@@ -74,6 +78,10 @@ class SettingsScreen:
' Memory Condensation',
'Enabled' if agent_spec.condenser else 'Disabled',
),
+ (
+ ' Working Directory',
+ current_work_dir,
+ ),
(
' Configuration File',
os.path.join(PERSISTENCE_DIR, AGENT_SETTINGS_PATH),
@@ -124,6 +132,8 @@ class SettingsScreen:
self.handle_basic_settings()
elif settings_type == SettingsType.ADVANCED:
self.handle_advanced_settings()
+ elif settings_type == SettingsType.WORKING_DIRECTORY:
+ configure_working_directory_in_settings()
def handle_basic_settings(self):
step_counter = StepCounter(3)
diff --git a/openhands-cli/openhands_cli/tui/settings/store.py b/openhands-cli/openhands_cli/tui/settings/store.py
index 2a4f7f8321..f915dfa54c 100644
--- a/openhands-cli/openhands_cli/tui/settings/store.py
+++ b/openhands-cli/openhands_cli/tui/settings/store.py
@@ -1,6 +1,7 @@
# openhands_cli/settings/store.py
from __future__ import annotations
+import os
from pathlib import Path
from typing import Any
@@ -10,7 +11,7 @@ from openhands_cli.locations import (
AGENT_SETTINGS_PATH,
MCP_CONFIG_FILE,
PERSISTENCE_DIR,
- WORK_DIR,
+ get_configured_working_directory,
)
from prompt_toolkit import HTML, print_formatted_text
@@ -41,8 +42,11 @@ class AgentStore:
# Update tools with most recent working directory
updated_tools = get_default_tools(enable_browser=False)
+ # Get current working directory (may have been updated)
+ current_work_dir = get_configured_working_directory() or os.getcwd()
+
agent_context = AgentContext(
- system_message_suffix=f'You current working directory is: {WORK_DIR}',
+ system_message_suffix=f'You current working directory is: {current_work_dir}',
)
additional_mcp_config = self.load_mcp_configuration()
diff --git a/openhands-cli/openhands_cli/user_actions/settings_action.py b/openhands-cli/openhands_cli/user_actions/settings_action.py
index e41e08bdb0..399e9b1b16 100644
--- a/openhands-cli/openhands_cli/user_actions/settings_action.py
+++ b/openhands-cli/openhands_cli/user_actions/settings_action.py
@@ -15,6 +15,7 @@ from openhands_cli.user_actions.utils import (
class SettingsType(Enum):
BASIC = 'basic'
ADVANCED = 'advanced'
+ WORKING_DIRECTORY = 'working_directory'
def settings_type_confirmation(first_time: bool = False) -> SettingsType:
@@ -28,7 +29,7 @@ def settings_type_confirmation(first_time: bool = False) -> SettingsType:
]
if not first_time:
question = 'Which settings would you like to modify?'
- choices.append('Go back')
+ choices.extend(['Working Directory', 'Go back'])
index = cli_confirm(question, choices, escapable=True)
@@ -36,7 +37,10 @@ def settings_type_confirmation(first_time: bool = False) -> SettingsType:
if choices[index] == 'Go back':
raise KeyboardInterrupt
- options_map = {0: SettingsType.BASIC, 1: SettingsType.ADVANCED}
+ if first_time:
+ options_map = {0: SettingsType.BASIC, 1: SettingsType.ADVANCED}
+ else:
+ options_map = {0: SettingsType.BASIC, 1: SettingsType.ADVANCED, 2: SettingsType.WORKING_DIRECTORY}
return options_map.get(index)
diff --git a/openhands-cli/openhands_cli/user_actions/working_directory_action.py b/openhands-cli/openhands_cli/user_actions/working_directory_action.py
new file mode 100644
index 0000000000..4999017f47
--- /dev/null
+++ b/openhands-cli/openhands_cli/user_actions/working_directory_action.py
@@ -0,0 +1,155 @@
+"""Working directory configuration actions for OpenHands CLI."""
+
+import os
+from pathlib import Path
+
+from prompt_toolkit import HTML, print_formatted_text
+from prompt_toolkit.completion import PathCompleter
+
+from openhands_cli.locations import get_configured_working_directory, save_working_directory
+from openhands_cli.user_actions.utils import cli_confirm, cli_text_input
+
+
+def prompt_working_directory_configuration() -> str:
+ """Prompt user to configure working directory at conversation start.
+
+ Returns:
+ The selected working directory path.
+ """
+ current_configured = get_configured_working_directory()
+ current_cwd = os.getcwd()
+
+ # Display current status
+ print_formatted_text(HTML('Working Directory Configuration'))
+ print_formatted_text(HTML(f'Current directory: {current_cwd}'))
+
+ if current_configured:
+ print_formatted_text(HTML(f'Configured directory: {current_configured}'))
+
+ # Ask if user wants to use configured directory or change it
+ choices = [
+ f'Use configured directory ({current_configured})',
+ f'Use current directory ({current_cwd})',
+ 'Choose a different directory'
+ ]
+
+ question = '\nWhich working directory would you like to use?'
+ choice_index = cli_confirm(question, choices, escapable=False)
+
+ if choice_index == 0:
+ return current_configured
+ elif choice_index == 1:
+ # Save current directory as new configured directory
+ save_working_directory(current_cwd)
+ print_formatted_text(HTML(f'✓ Working directory updated to: {current_cwd}'))
+ return current_cwd
+ else:
+ # Choice 2: Choose different directory
+ return _prompt_custom_directory()
+ else:
+ # No configured directory, ask user to set one
+ print_formatted_text(HTML('No working directory configured.'))
+
+ choices = [
+ f'Use current directory ({current_cwd})',
+ 'Choose a different directory'
+ ]
+
+ question = '\nWhich working directory would you like to use?'
+ choice_index = cli_confirm(question, choices, escapable=False)
+
+ if choice_index == 0:
+ # Save current directory as configured directory
+ save_working_directory(current_cwd)
+ print_formatted_text(HTML(f'✓ Working directory set to: {current_cwd}'))
+ return current_cwd
+ else:
+ # Choice 1: Choose different directory
+ return _prompt_custom_directory()
+
+
+def _prompt_custom_directory() -> str:
+ """Prompt user to enter a custom directory path.
+
+ Returns:
+ The validated directory path.
+ """
+ while True:
+ question = '\nEnter working directory path (TAB for completion): '
+
+ try:
+ directory_path = cli_text_input(
+ question,
+ escapable=False,
+ completer=PathCompleter(only_directories=True)
+ )
+
+ # Expand user path and resolve
+ expanded_path = os.path.expanduser(directory_path)
+ resolved_path = os.path.abspath(expanded_path)
+
+ if not os.path.exists(resolved_path):
+ print_formatted_text(HTML(f'Directory does not exist: {resolved_path}'))
+
+ # Ask if user wants to create it
+ create_choices = ['Yes', 'No, choose different path']
+ create_question = f'Would you like to create the directory?'
+ create_index = cli_confirm(create_question, create_choices, escapable=False)
+
+ if create_index == 0:
+ try:
+ os.makedirs(resolved_path, exist_ok=True)
+ print_formatted_text(HTML(f'✓ Created directory: {resolved_path}'))
+ except OSError as e:
+ print_formatted_text(HTML(f'Failed to create directory: {e}'))
+ continue
+ else:
+ continue
+
+ if not os.path.isdir(resolved_path):
+ print_formatted_text(HTML(f'Path is not a directory: {resolved_path}'))
+ continue
+
+ # Save the configured directory
+ save_working_directory(resolved_path)
+ print_formatted_text(HTML(f'✓ Working directory set to: {resolved_path}'))
+ return resolved_path
+
+ except KeyboardInterrupt:
+ # If user cancels, fall back to current directory
+ current_cwd = os.getcwd()
+ save_working_directory(current_cwd)
+ print_formatted_text(HTML(f'\nUsing current directory: {current_cwd}'))
+ return current_cwd
+
+
+def configure_working_directory_in_settings() -> None:
+ """Configure working directory from the settings screen."""
+ current_configured = get_configured_working_directory()
+ current_cwd = os.getcwd()
+
+ print_formatted_text(HTML('\nWorking Directory Configuration'))
+ print_formatted_text(HTML(f'Current directory: {current_cwd}'))
+
+ if current_configured:
+ print_formatted_text(HTML(f'Configured directory: {current_configured}'))
+ else:
+ print_formatted_text(HTML('No working directory configured (using current directory)'))
+
+ choices = [
+ f'Set to current directory ({current_cwd})',
+ 'Choose a different directory',
+ 'Go back'
+ ]
+
+ question = '\nWhat would you like to do?'
+ choice_index = cli_confirm(question, choices, escapable=True)
+
+ if choice_index == 0:
+ # Set to current directory
+ save_working_directory(current_cwd)
+ print_formatted_text(HTML(f'✓ Working directory set to: {current_cwd}'))
+ elif choice_index == 1:
+ # Choose different directory
+ _prompt_custom_directory()
+ # choice_index == 2 or KeyboardInterrupt: Go back (do nothing)
\ No newline at end of file
diff --git a/openhands-cli/tests/test_working_directory.py b/openhands-cli/tests/test_working_directory.py
new file mode 100644
index 0000000000..241e8e8c61
--- /dev/null
+++ b/openhands-cli/tests/test_working_directory.py
@@ -0,0 +1,148 @@
+"""Tests for working directory configuration functionality."""
+
+import json
+import os
+import tempfile
+from pathlib import Path
+from unittest.mock import Mock, patch, MagicMock
+
+import pytest
+
+from openhands_cli.locations import (
+ get_configured_working_directory,
+ save_working_directory,
+)
+from openhands_cli.user_actions.working_directory_action import (
+ configure_working_directory_in_settings,
+ prompt_working_directory_configuration,
+)
+
+
+class TestWorkingDirectoryConfiguration:
+ """Test working directory configuration functionality."""
+
+ def setup_method(self):
+ """Set up test environment."""
+ self.temp_dir = tempfile.mkdtemp()
+ self.test_settings_file = Path(self.temp_dir) / "oh_cli_settings.json"
+
+ def teardown_method(self):
+ """Clean up test environment."""
+ if self.test_settings_file.exists():
+ self.test_settings_file.unlink()
+ os.rmdir(self.temp_dir)
+
+ def test_get_configured_working_directory_no_file(self):
+ """Test getting working directory when no settings file exists."""
+ with patch('openhands_cli.locations.PERSISTENCE_DIR', str(self.temp_dir)):
+ result = get_configured_working_directory()
+ assert result is None
+
+ def test_get_configured_working_directory_empty_file(self):
+ """Test getting working directory when settings file is empty."""
+ # Create empty settings file
+ self.test_settings_file.write_text("{}")
+
+ with patch('openhands_cli.locations.PERSISTENCE_DIR', str(self.temp_dir)):
+ with patch('openhands_cli.locations.CLI_SETTINGS_FILE', self.test_settings_file.name):
+ result = get_configured_working_directory()
+ assert result is None
+
+ def test_get_configured_working_directory_with_value(self):
+ """Test getting working directory when value is configured."""
+ # Use the temp directory as the test directory since it actually exists
+ test_dir = str(self.temp_dir)
+ settings = {"working_directory": test_dir}
+ self.test_settings_file.write_text(json.dumps(settings))
+
+ with patch('openhands_cli.locations.PERSISTENCE_DIR', str(self.temp_dir)):
+ with patch('openhands_cli.locations.CLI_SETTINGS_FILE', self.test_settings_file.name):
+ result = get_configured_working_directory()
+ assert result == test_dir
+
+ def test_save_working_directory_new_file(self):
+ """Test saving working directory to new settings file."""
+ test_dir = "/test/working/directory"
+
+ with patch('openhands_cli.locations.PERSISTENCE_DIR', str(self.temp_dir)):
+ with patch('openhands_cli.locations.CLI_SETTINGS_FILE', self.test_settings_file.name):
+ save_working_directory(test_dir)
+
+ # Verify file was created with correct content
+ assert self.test_settings_file.exists()
+ settings = json.loads(self.test_settings_file.read_text())
+ assert settings["working_directory"] == test_dir
+
+ def test_save_working_directory_existing_file(self):
+ """Test saving working directory to existing settings file."""
+ # Create existing settings file with other data
+ existing_settings = {"other_setting": "value"}
+ self.test_settings_file.write_text(json.dumps(existing_settings))
+
+ test_dir = "/test/working/directory"
+
+ with patch('openhands_cli.locations.PERSISTENCE_DIR', str(self.temp_dir)):
+ with patch('openhands_cli.locations.CLI_SETTINGS_FILE', self.test_settings_file.name):
+ save_working_directory(test_dir)
+
+ # Verify working directory was added while preserving other settings
+ settings = json.loads(self.test_settings_file.read_text())
+ assert settings["working_directory"] == test_dir
+ assert settings["other_setting"] == "value"
+
+ def test_save_working_directory_update_existing(self):
+ """Test updating existing working directory setting."""
+ # Create existing settings file with working directory
+ existing_settings = {"working_directory": "/old/directory"}
+ self.test_settings_file.write_text(json.dumps(existing_settings))
+
+ test_dir = "/new/working/directory"
+
+ with patch('openhands_cli.locations.PERSISTENCE_DIR', str(self.temp_dir)):
+ with patch('openhands_cli.locations.CLI_SETTINGS_FILE', self.test_settings_file.name):
+ save_working_directory(test_dir)
+
+ # Verify working directory was updated
+ settings = json.loads(self.test_settings_file.read_text())
+ assert settings["working_directory"] == test_dir
+
+ @patch('openhands_cli.user_actions.working_directory_action.get_configured_working_directory')
+ @patch('openhands_cli.user_actions.working_directory_action.save_working_directory')
+ def test_working_directory_functions_exist(self, mock_save, mock_get):
+ """Test that working directory functions exist and can be called."""
+ # Test that functions exist and can be imported
+ assert callable(get_configured_working_directory)
+ assert callable(save_working_directory)
+ assert callable(prompt_working_directory_configuration)
+ assert callable(configure_working_directory_in_settings)
+
+
+class TestWorkingDirectoryIntegration:
+ """Test integration of working directory with other components."""
+
+ @patch('openhands_cli.locations.get_configured_working_directory')
+ @patch('os.getcwd')
+ def test_setup_conversation_uses_configured_directory(self, mock_getcwd, mock_get_config):
+ """Test that setup_conversation uses configured working directory."""
+ from openhands_cli.setup import setup_conversation
+
+ configured_dir = "/configured/directory"
+ current_dir = "/current/directory"
+ mock_get_config.return_value = configured_dir
+ mock_getcwd.return_value = current_dir
+
+ # This would require more complex mocking to test fully
+ # but the key point is that get_configured_working_directory is called
+ mock_get_config.assert_not_called() # Not called until setup_conversation runs
+
+ @patch('openhands_cli.locations.get_configured_working_directory')
+ def test_agent_store_uses_configured_directory(self, mock_get_config):
+ """Test that AgentStore uses configured working directory."""
+ from openhands_cli.tui.settings.store import AgentStore
+
+ configured_dir = "/configured/directory"
+ mock_get_config.return_value = configured_dir
+
+ # This would require more complex mocking to test fully
+ # but the key point is that get_configured_working_directory is called
+ mock_get_config.assert_not_called() # Not called until AgentStore.load() runs
\ No newline at end of file