openhands 1.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. openhands-1.3.0.dist-info/METADATA +56 -0
  2. openhands-1.3.0.dist-info/RECORD +43 -0
  3. openhands-1.3.0.dist-info/WHEEL +4 -0
  4. openhands-1.3.0.dist-info/entry_points.txt +3 -0
  5. openhands-1.3.0.dist-info/licenses/LICENSE +21 -0
  6. openhands_cli/__init__.py +9 -0
  7. openhands_cli/acp_impl/README.md +68 -0
  8. openhands_cli/acp_impl/__init__.py +1 -0
  9. openhands_cli/acp_impl/agent.py +483 -0
  10. openhands_cli/acp_impl/event.py +512 -0
  11. openhands_cli/acp_impl/main.py +21 -0
  12. openhands_cli/acp_impl/test_utils.py +174 -0
  13. openhands_cli/acp_impl/utils/__init__.py +14 -0
  14. openhands_cli/acp_impl/utils/convert.py +103 -0
  15. openhands_cli/acp_impl/utils/mcp.py +66 -0
  16. openhands_cli/acp_impl/utils/resources.py +189 -0
  17. openhands_cli/agent_chat.py +236 -0
  18. openhands_cli/argparsers/main_parser.py +78 -0
  19. openhands_cli/argparsers/serve_parser.py +31 -0
  20. openhands_cli/gui_launcher.py +224 -0
  21. openhands_cli/listeners/__init__.py +4 -0
  22. openhands_cli/listeners/pause_listener.py +83 -0
  23. openhands_cli/locations.py +14 -0
  24. openhands_cli/pt_style.py +33 -0
  25. openhands_cli/runner.py +190 -0
  26. openhands_cli/setup.py +136 -0
  27. openhands_cli/simple_main.py +71 -0
  28. openhands_cli/tui/__init__.py +6 -0
  29. openhands_cli/tui/settings/mcp_screen.py +225 -0
  30. openhands_cli/tui/settings/settings_screen.py +226 -0
  31. openhands_cli/tui/settings/store.py +132 -0
  32. openhands_cli/tui/status.py +110 -0
  33. openhands_cli/tui/tui.py +120 -0
  34. openhands_cli/tui/utils.py +14 -0
  35. openhands_cli/tui/visualizer.py +22 -0
  36. openhands_cli/user_actions/__init__.py +18 -0
  37. openhands_cli/user_actions/agent_action.py +82 -0
  38. openhands_cli/user_actions/exit_session.py +18 -0
  39. openhands_cli/user_actions/settings_action.py +176 -0
  40. openhands_cli/user_actions/types.py +17 -0
  41. openhands_cli/user_actions/utils.py +199 -0
  42. openhands_cli/utils.py +122 -0
  43. openhands_cli/version_check.py +83 -0
@@ -0,0 +1,31 @@
1
+ """Argument parser for serve subcommand."""
2
+
3
+ import argparse
4
+
5
+
6
+ def add_serve_parser(subparsers: argparse._SubParsersAction) -> argparse.ArgumentParser:
7
+ """Add serve subcommand parser.
8
+
9
+ Args:
10
+ subparsers: The subparsers object to add the serve parser to
11
+
12
+ Returns:
13
+ The serve argument parser
14
+ """
15
+ serve_parser = subparsers.add_parser(
16
+ "serve", help="Launch the OpenHands GUI server using Docker (web interface)"
17
+ )
18
+ serve_parser.add_argument(
19
+ "--mount-cwd",
20
+ help="Mount the current working directory into the GUI server container",
21
+ action="store_true",
22
+ default=False,
23
+ )
24
+ serve_parser.add_argument(
25
+ "--gpu",
26
+ help="Enable GPU support by mounting all GPUs into the Docker "
27
+ "container via nvidia-docker",
28
+ action="store_true",
29
+ default=False,
30
+ )
31
+ return serve_parser
@@ -0,0 +1,224 @@
1
+ """GUI launcher for OpenHands CLI."""
2
+
3
+ import os
4
+ import shutil
5
+ import subprocess
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from prompt_toolkit import print_formatted_text
10
+ from prompt_toolkit.formatted_text import HTML
11
+
12
+ from openhands_cli.locations import PERSISTENCE_DIR
13
+
14
+
15
+ def _format_docker_command_for_logging(cmd: list[str]) -> str:
16
+ """Format a Docker command for logging with grey color.
17
+
18
+ Args:
19
+ cmd (list[str]): The Docker command as a list of strings
20
+
21
+ Returns:
22
+ str: The formatted command string in grey HTML color
23
+ """
24
+ cmd_str = " ".join(cmd)
25
+ return f"<grey>Running Docker command: {cmd_str}</grey>"
26
+
27
+
28
+ def check_docker_requirements() -> bool:
29
+ """Check if Docker is installed and running.
30
+
31
+ Returns:
32
+ bool: True if Docker is available and running, False otherwise.
33
+ """
34
+ # Check if Docker is installed
35
+ if not shutil.which("docker"):
36
+ print_formatted_text(
37
+ HTML("<ansired>❌ Docker is not installed or not in PATH.</ansired>")
38
+ )
39
+ print_formatted_text(
40
+ HTML(
41
+ "<grey>Please install Docker first: https://docs.docker.com/get-docker/</grey>"
42
+ )
43
+ )
44
+ return False
45
+
46
+ # Check if Docker daemon is running
47
+ try:
48
+ result = subprocess.run(
49
+ ["docker", "info"], capture_output=True, text=True, timeout=10
50
+ )
51
+ if result.returncode != 0:
52
+ print_formatted_text(
53
+ HTML("<ansired>❌ Docker daemon is not running.</ansired>")
54
+ )
55
+ print_formatted_text(
56
+ HTML("<grey>Please start Docker and try again.</grey>")
57
+ )
58
+ return False
59
+ except (subprocess.TimeoutExpired, subprocess.SubprocessError) as e:
60
+ print_formatted_text(
61
+ HTML("<ansired>❌ Failed to check Docker status.</ansired>")
62
+ )
63
+ print_formatted_text(HTML(f"<grey>Error: {e}</grey>"))
64
+ return False
65
+
66
+ return True
67
+
68
+
69
+ def ensure_config_dir_exists() -> Path:
70
+ """Ensure the OpenHands configuration directory exists and return its path."""
71
+ path = Path(PERSISTENCE_DIR)
72
+ path.mkdir(exist_ok=True, parents=True)
73
+ return path
74
+
75
+
76
+ def get_openhands_version() -> str:
77
+ """Get the OpenHands version for Docker images.
78
+
79
+ Returns:
80
+ str: The version string to use for Docker images
81
+ """
82
+ # For now, use 'latest' as the default version
83
+ # In the future, this could be read from a version file or environment variable
84
+ return os.environ.get("OPENHANDS_VERSION", "latest")
85
+
86
+
87
+ def launch_gui_server(mount_cwd: bool = False, gpu: bool = False) -> None:
88
+ """Launch the OpenHands GUI server using Docker.
89
+
90
+ Args:
91
+ mount_cwd: If True, mount the current working directory into the container.
92
+ gpu: If True, enable GPU support by mounting all GPUs into the
93
+ container via nvidia-docker.
94
+ """
95
+ print_formatted_text(
96
+ HTML("<ansiblue>🚀 Launching OpenHands GUI server...</ansiblue>")
97
+ )
98
+ print_formatted_text("")
99
+
100
+ # Check Docker requirements
101
+ if not check_docker_requirements():
102
+ sys.exit(1)
103
+
104
+ # Ensure config directory exists
105
+ config_dir = ensure_config_dir_exists()
106
+
107
+ # Get the current version for the Docker image
108
+ version = get_openhands_version()
109
+ runtime_image = f"docker.openhands.dev/openhands/runtime:{version}-nikolaik"
110
+ app_image = f"docker.openhands.dev/openhands/openhands:{version}"
111
+
112
+ print_formatted_text(HTML("<grey>Pulling required Docker images...</grey>"))
113
+
114
+ # Pull the runtime image first
115
+ pull_cmd = ["docker", "pull", runtime_image]
116
+ print_formatted_text(HTML(_format_docker_command_for_logging(pull_cmd)))
117
+ try:
118
+ subprocess.run(pull_cmd, check=True)
119
+ except subprocess.CalledProcessError:
120
+ print_formatted_text(
121
+ HTML("<ansired>❌ Failed to pull runtime image.</ansired>")
122
+ )
123
+ sys.exit(1)
124
+
125
+ print_formatted_text("")
126
+ print_formatted_text(
127
+ HTML("<ansigreen>✅ Starting OpenHands GUI server...</ansigreen>")
128
+ )
129
+ print_formatted_text(
130
+ HTML("<grey>The server will be available at: http://localhost:3000</grey>")
131
+ )
132
+ print_formatted_text(HTML("<grey>Press Ctrl+C to stop the server.</grey>"))
133
+ print_formatted_text("")
134
+
135
+ # Build the Docker command
136
+ docker_cmd = [
137
+ "docker",
138
+ "run",
139
+ "-it",
140
+ "--rm",
141
+ "--pull=always",
142
+ "-e",
143
+ f"SANDBOX_RUNTIME_CONTAINER_IMAGE={runtime_image}",
144
+ "-e",
145
+ "LOG_ALL_EVENTS=true",
146
+ "-v",
147
+ "/var/run/docker.sock:/var/run/docker.sock",
148
+ "-v",
149
+ f"{config_dir}:/.openhands",
150
+ ]
151
+
152
+ # Add GPU support if requested
153
+ if gpu:
154
+ print_formatted_text(
155
+ HTML("<ansigreen>🖥️ Enabling GPU support via nvidia-docker...</ansigreen>")
156
+ )
157
+ # Add the --gpus all flag to enable all GPUs
158
+ docker_cmd.insert(2, "--gpus")
159
+ docker_cmd.insert(3, "all")
160
+ # Add environment variable to pass GPU support to sandbox containers
161
+ docker_cmd.extend(
162
+ [
163
+ "-e",
164
+ "SANDBOX_ENABLE_GPU=true",
165
+ ]
166
+ )
167
+
168
+ # Add current working directory mount if requested
169
+ if mount_cwd:
170
+ cwd = Path.cwd()
171
+ # Following the documentation at https://docs.all-hands.dev/usage/runtimes/docker#connecting-to-your-filesystem
172
+ docker_cmd.extend(
173
+ [
174
+ "-e",
175
+ f"SANDBOX_VOLUMES={cwd}:/workspace:rw",
176
+ ]
177
+ )
178
+
179
+ # Set user ID for Unix-like systems only
180
+ if os.name != "nt": # Not Windows
181
+ try:
182
+ user_id = subprocess.check_output(["id", "-u"], text=True).strip()
183
+ docker_cmd.extend(["-e", f"SANDBOX_USER_ID={user_id}"])
184
+ except (subprocess.CalledProcessError, FileNotFoundError):
185
+ # If 'id' command fails or doesn't exist, skip setting user ID
186
+ pass
187
+ # Print the folder that will be mounted to inform the user
188
+ print_formatted_text(
189
+ HTML(
190
+ f"<ansigreen>📂 Mounting current directory:</ansigreen> "
191
+ f"<ansiyellow>{cwd}</ansiyellow> <ansigreen>to</ansigreen> "
192
+ f"<ansiyellow>/workspace</ansiyellow>"
193
+ )
194
+ )
195
+
196
+ docker_cmd.extend(
197
+ [
198
+ "-p",
199
+ "3000:3000",
200
+ "--add-host",
201
+ "host.docker.internal:host-gateway",
202
+ "--name",
203
+ "openhands-app",
204
+ app_image,
205
+ ]
206
+ )
207
+
208
+ try:
209
+ # Log and run the Docker command
210
+ print_formatted_text(HTML(_format_docker_command_for_logging(docker_cmd)))
211
+ subprocess.run(docker_cmd, check=True)
212
+ except subprocess.CalledProcessError as e:
213
+ print_formatted_text("")
214
+ print_formatted_text(
215
+ HTML("<ansired>❌ Failed to start OpenHands GUI server.</ansired>")
216
+ )
217
+ print_formatted_text(HTML(f"<grey>Error: {e}</grey>"))
218
+ sys.exit(1)
219
+ except KeyboardInterrupt:
220
+ print_formatted_text("")
221
+ print_formatted_text(
222
+ HTML("<ansigreen>✓ OpenHands GUI server stopped successfully.</ansigreen>")
223
+ )
224
+ sys.exit(0)
@@ -0,0 +1,4 @@
1
+ from openhands_cli.listeners.pause_listener import PauseListener
2
+
3
+
4
+ __all__ = ["PauseListener"]
@@ -0,0 +1,83 @@
1
+ import threading
2
+ from collections.abc import Callable, Iterator
3
+ from contextlib import contextmanager
4
+
5
+ from prompt_toolkit import HTML, print_formatted_text
6
+ from prompt_toolkit.input import Input, create_input
7
+ from prompt_toolkit.keys import Keys
8
+
9
+ from openhands.sdk import BaseConversation
10
+
11
+
12
+ class PauseListener(threading.Thread):
13
+ """Background key listener that triggers pause on Ctrl-P.
14
+
15
+ Starts and stops around agent run() loops to avoid interfering with user prompts.
16
+ """
17
+
18
+ def __init__(
19
+ self,
20
+ on_pause: Callable,
21
+ input_source: Input | None = None, # used to pipe inputs for unit tests
22
+ ):
23
+ super().__init__(daemon=True)
24
+ self.on_pause = on_pause
25
+ self._stop_event = threading.Event()
26
+ self._pause_event = threading.Event()
27
+ self._input = input_source or create_input()
28
+
29
+ def _detect_pause_key_presses(self) -> bool:
30
+ pause_detected = False
31
+
32
+ for key_press in self._input.read_keys():
33
+ pause_detected = pause_detected or key_press.key == Keys.ControlP
34
+ pause_detected = pause_detected or key_press.key == Keys.ControlC
35
+ pause_detected = pause_detected or key_press.key == Keys.ControlD
36
+
37
+ return pause_detected
38
+
39
+ def _execute_pause(self) -> None:
40
+ self._pause_event.set() # Mark pause event occurred
41
+ print_formatted_text(HTML(""))
42
+ print_formatted_text(
43
+ HTML("<gold>Pausing agent once step is completed...</gold>")
44
+ )
45
+ try:
46
+ self.on_pause()
47
+ except Exception:
48
+ pass
49
+
50
+ def run(self) -> None:
51
+ try:
52
+ with self._input.raw_mode():
53
+ # User hasn't paused and pause listener hasn't been shut down
54
+ while not (self.is_paused() or self.is_stopped()):
55
+ if self._detect_pause_key_presses():
56
+ self._execute_pause()
57
+ finally:
58
+ try:
59
+ self._input.close()
60
+ except Exception:
61
+ pass
62
+
63
+ def stop(self) -> None:
64
+ self._stop_event.set()
65
+
66
+ def is_stopped(self) -> bool:
67
+ return self._stop_event.is_set()
68
+
69
+ def is_paused(self) -> bool:
70
+ return self._pause_event.is_set()
71
+
72
+
73
+ @contextmanager
74
+ def pause_listener(
75
+ conversation: BaseConversation, input_source: Input | None = None
76
+ ) -> Iterator[PauseListener]:
77
+ """Ensure PauseListener always starts/stops cleanly."""
78
+ listener = PauseListener(on_pause=conversation.pause, input_source=input_source)
79
+ listener.start()
80
+ try:
81
+ yield listener
82
+ finally:
83
+ listener.stop()
@@ -0,0 +1,14 @@
1
+ import os
2
+
3
+
4
+ # Configuration directory for storing agent settings and CLI configuration
5
+ PERSISTENCE_DIR = os.path.expanduser("~/.openhands")
6
+ CONVERSATIONS_DIR = os.path.join(PERSISTENCE_DIR, "conversations")
7
+
8
+ # Working directory for agent operations (current directory where CLI is run)
9
+ WORK_DIR = os.getcwd()
10
+
11
+ AGENT_SETTINGS_PATH = "agent_settings.json"
12
+
13
+ # MCP configuration file (relative to PERSISTENCE_DIR)
14
+ MCP_CONFIG_FILE = "mcp.json"
@@ -0,0 +1,33 @@
1
+ from prompt_toolkit.styles import Style, merge_styles
2
+ from prompt_toolkit.styles.base import BaseStyle
3
+ from prompt_toolkit.styles.defaults import default_ui_style
4
+
5
+
6
+ # Centralized helper for CLI styles so we can safely merge our custom colors
7
+ # with prompt_toolkit's default UI style. This preserves completion menu and
8
+ # fuzzy-match visibility across different terminal themes (e.g., Ubuntu).
9
+
10
+ COLOR_GOLD = "#FFD700"
11
+ COLOR_GREY = "#808080"
12
+ COLOR_AGENT_BLUE = "#4682B4" # Steel blue - readable on light/dark backgrounds
13
+
14
+
15
+ def get_cli_style() -> BaseStyle:
16
+ base = default_ui_style()
17
+ custom = Style.from_dict(
18
+ {
19
+ "gold": COLOR_GOLD,
20
+ "grey": COLOR_GREY,
21
+ "prompt": f"{COLOR_GOLD} bold",
22
+ # Ensure good contrast for fuzzy matches on the selected completion row
23
+ # across terminals/themes (e.g., Ubuntu GNOME, Alacritty, Kitty).
24
+ # See https://github.com/OpenHands/OpenHands/issues/10330
25
+ "completion-menu.completion.current fuzzymatch.outside": (
26
+ "fg:#ffffff bg:#888888"
27
+ ),
28
+ "selected": COLOR_GOLD,
29
+ "risk-high": "#FF0000 bold", # Red bold for HIGH risk
30
+ "placeholder": "#888888 italic",
31
+ }
32
+ )
33
+ return merge_styles([base, custom])
@@ -0,0 +1,190 @@
1
+ from prompt_toolkit import HTML, print_formatted_text
2
+
3
+ from openhands.sdk import BaseConversation, Message
4
+ from openhands.sdk.conversation.state import (
5
+ ConversationExecutionStatus,
6
+ ConversationState,
7
+ )
8
+ from openhands.sdk.security.confirmation_policy import (
9
+ AlwaysConfirm,
10
+ ConfirmationPolicyBase,
11
+ ConfirmRisky,
12
+ NeverConfirm,
13
+ )
14
+ from openhands_cli.listeners.pause_listener import PauseListener, pause_listener
15
+ from openhands_cli.setup import setup_conversation
16
+ from openhands_cli.user_actions import ask_user_confirmation
17
+ from openhands_cli.user_actions.types import UserConfirmation
18
+
19
+
20
+ class ConversationRunner:
21
+ """Handles the conversation state machine logic cleanly."""
22
+
23
+ def __init__(self, conversation: BaseConversation):
24
+ self.conversation = conversation
25
+
26
+ @property
27
+ def is_confirmation_mode_active(self):
28
+ return self.conversation.is_confirmation_mode_active
29
+
30
+ def toggle_confirmation_mode(self):
31
+ new_confirmation_mode_state = not self.is_confirmation_mode_active
32
+
33
+ self.conversation = setup_conversation(
34
+ self.conversation.id, include_security_analyzer=new_confirmation_mode_state
35
+ )
36
+
37
+ if new_confirmation_mode_state:
38
+ # Enable confirmation mode: set AlwaysConfirm policy
39
+ self.set_confirmation_policy(AlwaysConfirm())
40
+ else:
41
+ # Disable confirmation mode: set NeverConfirm policy and remove
42
+ # security analyzer
43
+ self.set_confirmation_policy(NeverConfirm())
44
+
45
+ def set_confirmation_policy(
46
+ self, confirmation_policy: ConfirmationPolicyBase
47
+ ) -> None:
48
+ self.conversation.set_confirmation_policy(confirmation_policy)
49
+
50
+ def _start_listener(self) -> None:
51
+ self.listener = PauseListener(on_pause=self.conversation.pause)
52
+ self.listener.start()
53
+
54
+ def _print_run_status(self) -> None:
55
+ print_formatted_text("")
56
+ if (
57
+ self.conversation.state.execution_status
58
+ == ConversationExecutionStatus.PAUSED
59
+ ):
60
+ print_formatted_text(
61
+ HTML(
62
+ "<yellow>Resuming paused conversation...</yellow>"
63
+ "<grey> (Press Ctrl-P to pause)</grey>"
64
+ )
65
+ )
66
+
67
+ else:
68
+ print_formatted_text(
69
+ HTML(
70
+ "<yellow>Agent running...</yellow>"
71
+ "<grey> (Press Ctrl-P to pause)</grey>"
72
+ )
73
+ )
74
+ print_formatted_text("")
75
+
76
+ def process_message(self, message: Message | None) -> None:
77
+ """Process a user message through the conversation.
78
+
79
+ Args:
80
+ message: The user message to process
81
+ """
82
+
83
+ self._print_run_status()
84
+
85
+ # Send message to conversation
86
+ if message:
87
+ self.conversation.send_message(message)
88
+
89
+ if self.is_confirmation_mode_active:
90
+ self._run_with_confirmation()
91
+ else:
92
+ self._run_without_confirmation()
93
+
94
+ def _run_without_confirmation(self) -> None:
95
+ with pause_listener(self.conversation):
96
+ self.conversation.run()
97
+
98
+ def _run_with_confirmation(self) -> None:
99
+ # If agent was paused, resume with confirmation request
100
+ if (
101
+ self.conversation.state.execution_status
102
+ == ConversationExecutionStatus.WAITING_FOR_CONFIRMATION
103
+ ):
104
+ user_confirmation = self._handle_confirmation_request()
105
+ if user_confirmation == UserConfirmation.DEFER:
106
+ return
107
+
108
+ while True:
109
+ with pause_listener(self.conversation) as listener:
110
+ self.conversation.run()
111
+
112
+ if listener.is_paused():
113
+ break
114
+
115
+ # In confirmation mode, agent either finishes or waits for user confirmation
116
+ if (
117
+ self.conversation.state.execution_status
118
+ == ConversationExecutionStatus.FINISHED
119
+ ):
120
+ break
121
+
122
+ elif (
123
+ self.conversation.state.execution_status
124
+ == ConversationExecutionStatus.WAITING_FOR_CONFIRMATION
125
+ ):
126
+ user_confirmation = self._handle_confirmation_request()
127
+ if user_confirmation == UserConfirmation.DEFER:
128
+ return
129
+
130
+ else:
131
+ raise Exception("Infinite loop")
132
+
133
+ def _handle_confirmation_request(self) -> UserConfirmation:
134
+ """Handle confirmation request from user.
135
+
136
+ Returns:
137
+ UserConfirmation indicating the user's choice
138
+ """
139
+
140
+ pending_actions = ConversationState.get_unmatched_actions(
141
+ self.conversation.state.events
142
+ )
143
+ if not pending_actions:
144
+ return UserConfirmation.ACCEPT
145
+
146
+ result = ask_user_confirmation(
147
+ pending_actions,
148
+ isinstance(self.conversation.state.confirmation_policy, ConfirmRisky),
149
+ )
150
+ decision = result.decision
151
+ policy_change = result.policy_change
152
+
153
+ if decision == UserConfirmation.REJECT:
154
+ self.conversation.reject_pending_actions(
155
+ result.reason or "User rejected the actions"
156
+ )
157
+ return decision
158
+
159
+ if decision == UserConfirmation.DEFER:
160
+ self.conversation.pause()
161
+ return decision
162
+
163
+ if isinstance(policy_change, NeverConfirm):
164
+ print_formatted_text(
165
+ HTML(
166
+ "<yellow>Confirmation mode disabled. Agent will proceed "
167
+ "without asking.</yellow>"
168
+ )
169
+ )
170
+
171
+ # Remove security analyzer when policy is never confirm
172
+ self.toggle_confirmation_mode()
173
+ return decision
174
+
175
+ if isinstance(policy_change, ConfirmRisky):
176
+ print_formatted_text(
177
+ HTML(
178
+ "<yellow>Security-based confirmation enabled. "
179
+ "LOW/MEDIUM risk actions will auto-confirm, HIGH risk actions "
180
+ "will ask for confirmation.</yellow>"
181
+ )
182
+ )
183
+
184
+ # Keep security analyzer, change existing policy
185
+ self.set_confirmation_policy(policy_change)
186
+ return decision
187
+
188
+ # Accept action without changing existing policies
189
+ assert decision == UserConfirmation.ACCEPT
190
+ return decision