openhands 0.0.0__py3-none-any.whl → 1.0.1__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.

Potentially problematic release.


This version of openhands might be problematic. Click here for more details.

Files changed (124) hide show
  1. openhands-1.0.1.dist-info/METADATA +52 -0
  2. openhands-1.0.1.dist-info/RECORD +31 -0
  3. {openhands-0.0.0.dist-info → openhands-1.0.1.dist-info}/WHEEL +1 -2
  4. openhands-1.0.1.dist-info/entry_points.txt +2 -0
  5. openhands_cli/__init__.py +8 -0
  6. openhands_cli/agent_chat.py +186 -0
  7. openhands_cli/argparsers/main_parser.py +56 -0
  8. openhands_cli/argparsers/serve_parser.py +31 -0
  9. openhands_cli/gui_launcher.py +220 -0
  10. openhands_cli/listeners/__init__.py +4 -0
  11. openhands_cli/listeners/loading_listener.py +63 -0
  12. openhands_cli/listeners/pause_listener.py +83 -0
  13. openhands_cli/llm_utils.py +57 -0
  14. openhands_cli/locations.py +13 -0
  15. openhands_cli/pt_style.py +30 -0
  16. openhands_cli/runner.py +178 -0
  17. openhands_cli/setup.py +116 -0
  18. openhands_cli/simple_main.py +59 -0
  19. openhands_cli/tui/__init__.py +5 -0
  20. openhands_cli/tui/settings/mcp_screen.py +217 -0
  21. openhands_cli/tui/settings/settings_screen.py +202 -0
  22. openhands_cli/tui/settings/store.py +93 -0
  23. openhands_cli/tui/status.py +109 -0
  24. openhands_cli/tui/tui.py +100 -0
  25. openhands_cli/tui/utils.py +14 -0
  26. openhands_cli/user_actions/__init__.py +17 -0
  27. openhands_cli/user_actions/agent_action.py +95 -0
  28. openhands_cli/user_actions/exit_session.py +18 -0
  29. openhands_cli/user_actions/settings_action.py +171 -0
  30. openhands_cli/user_actions/types.py +18 -0
  31. openhands_cli/user_actions/utils.py +199 -0
  32. openhands/__init__.py +0 -1
  33. openhands/sdk/__init__.py +0 -45
  34. openhands/sdk/agent/__init__.py +0 -8
  35. openhands/sdk/agent/agent/__init__.py +0 -6
  36. openhands/sdk/agent/agent/agent.py +0 -349
  37. openhands/sdk/agent/base.py +0 -103
  38. openhands/sdk/context/__init__.py +0 -28
  39. openhands/sdk/context/agent_context.py +0 -153
  40. openhands/sdk/context/condenser/__init__.py +0 -5
  41. openhands/sdk/context/condenser/condenser.py +0 -73
  42. openhands/sdk/context/condenser/no_op_condenser.py +0 -13
  43. openhands/sdk/context/manager.py +0 -5
  44. openhands/sdk/context/microagents/__init__.py +0 -26
  45. openhands/sdk/context/microagents/exceptions.py +0 -11
  46. openhands/sdk/context/microagents/microagent.py +0 -345
  47. openhands/sdk/context/microagents/types.py +0 -70
  48. openhands/sdk/context/utils/__init__.py +0 -8
  49. openhands/sdk/context/utils/prompt.py +0 -52
  50. openhands/sdk/context/view.py +0 -116
  51. openhands/sdk/conversation/__init__.py +0 -12
  52. openhands/sdk/conversation/conversation.py +0 -207
  53. openhands/sdk/conversation/state.py +0 -50
  54. openhands/sdk/conversation/types.py +0 -6
  55. openhands/sdk/conversation/visualizer.py +0 -300
  56. openhands/sdk/event/__init__.py +0 -27
  57. openhands/sdk/event/base.py +0 -148
  58. openhands/sdk/event/condenser.py +0 -49
  59. openhands/sdk/event/llm_convertible.py +0 -265
  60. openhands/sdk/event/types.py +0 -5
  61. openhands/sdk/event/user_action.py +0 -12
  62. openhands/sdk/event/utils.py +0 -30
  63. openhands/sdk/llm/__init__.py +0 -19
  64. openhands/sdk/llm/exceptions.py +0 -108
  65. openhands/sdk/llm/llm.py +0 -867
  66. openhands/sdk/llm/llm_registry.py +0 -116
  67. openhands/sdk/llm/message.py +0 -216
  68. openhands/sdk/llm/metadata.py +0 -34
  69. openhands/sdk/llm/utils/fn_call_converter.py +0 -1049
  70. openhands/sdk/llm/utils/metrics.py +0 -311
  71. openhands/sdk/llm/utils/model_features.py +0 -153
  72. openhands/sdk/llm/utils/retry_mixin.py +0 -122
  73. openhands/sdk/llm/utils/telemetry.py +0 -252
  74. openhands/sdk/logger.py +0 -167
  75. openhands/sdk/mcp/__init__.py +0 -20
  76. openhands/sdk/mcp/client.py +0 -113
  77. openhands/sdk/mcp/definition.py +0 -69
  78. openhands/sdk/mcp/tool.py +0 -104
  79. openhands/sdk/mcp/utils.py +0 -59
  80. openhands/sdk/tests/llm/test_llm.py +0 -447
  81. openhands/sdk/tests/llm/test_llm_fncall_converter.py +0 -691
  82. openhands/sdk/tests/llm/test_model_features.py +0 -221
  83. openhands/sdk/tool/__init__.py +0 -30
  84. openhands/sdk/tool/builtins/__init__.py +0 -34
  85. openhands/sdk/tool/builtins/finish.py +0 -57
  86. openhands/sdk/tool/builtins/think.py +0 -60
  87. openhands/sdk/tool/schema.py +0 -236
  88. openhands/sdk/tool/security_prompt.py +0 -5
  89. openhands/sdk/tool/tool.py +0 -142
  90. openhands/sdk/utils/__init__.py +0 -14
  91. openhands/sdk/utils/discriminated_union.py +0 -210
  92. openhands/sdk/utils/json.py +0 -48
  93. openhands/sdk/utils/truncate.py +0 -44
  94. openhands/tools/__init__.py +0 -44
  95. openhands/tools/execute_bash/__init__.py +0 -30
  96. openhands/tools/execute_bash/constants.py +0 -31
  97. openhands/tools/execute_bash/definition.py +0 -166
  98. openhands/tools/execute_bash/impl.py +0 -38
  99. openhands/tools/execute_bash/metadata.py +0 -101
  100. openhands/tools/execute_bash/terminal/__init__.py +0 -22
  101. openhands/tools/execute_bash/terminal/factory.py +0 -113
  102. openhands/tools/execute_bash/terminal/interface.py +0 -189
  103. openhands/tools/execute_bash/terminal/subprocess_terminal.py +0 -412
  104. openhands/tools/execute_bash/terminal/terminal_session.py +0 -492
  105. openhands/tools/execute_bash/terminal/tmux_terminal.py +0 -160
  106. openhands/tools/execute_bash/utils/command.py +0 -150
  107. openhands/tools/str_replace_editor/__init__.py +0 -17
  108. openhands/tools/str_replace_editor/definition.py +0 -158
  109. openhands/tools/str_replace_editor/editor.py +0 -683
  110. openhands/tools/str_replace_editor/exceptions.py +0 -41
  111. openhands/tools/str_replace_editor/impl.py +0 -66
  112. openhands/tools/str_replace_editor/utils/__init__.py +0 -0
  113. openhands/tools/str_replace_editor/utils/config.py +0 -2
  114. openhands/tools/str_replace_editor/utils/constants.py +0 -9
  115. openhands/tools/str_replace_editor/utils/encoding.py +0 -135
  116. openhands/tools/str_replace_editor/utils/file_cache.py +0 -154
  117. openhands/tools/str_replace_editor/utils/history.py +0 -122
  118. openhands/tools/str_replace_editor/utils/shell.py +0 -72
  119. openhands/tools/task_tracker/__init__.py +0 -16
  120. openhands/tools/task_tracker/definition.py +0 -336
  121. openhands/tools/utils/__init__.py +0 -1
  122. openhands-0.0.0.dist-info/METADATA +0 -3
  123. openhands-0.0.0.dist-info/RECORD +0 -94
  124. openhands-0.0.0.dist-info/top_level.txt +0 -1
@@ -0,0 +1,52 @@
1
+ Metadata-Version: 2.4
2
+ Name: openhands
3
+ Version: 1.0.1
4
+ Summary: OpenHands CLI - Terminal User Interface for OpenHands AI Agent
5
+ Author-email: OpenHands Team <contact@all-hands.dev>
6
+ License: MIT
7
+ Classifier: Programming Language :: Python :: 3 :: Only
8
+ Classifier: Programming Language :: Python :: 3.12
9
+ Classifier: Programming Language :: Python :: 3.13
10
+ Requires-Python: >=3.12
11
+ Requires-Dist: openhands-sdk
12
+ Requires-Dist: openhands-tools
13
+ Requires-Dist: prompt-toolkit>=3
14
+ Requires-Dist: typer>=0.17.4
15
+ Description-Content-Type: text/markdown
16
+
17
+ # OpenHands V1 CLI
18
+
19
+ A **lightweight, modern CLI** to interact with the OpenHands agent (powered by [agent-sdk](https://github.com/All-Hands-AI/agent-sdk)).
20
+
21
+ The [OpenHands V0 CLI (legacy)](https://github.com/All-Hands-AI/OpenHands/tree/main/openhands/cli) is being deprecated.
22
+
23
+ ---
24
+
25
+ ## Quickstart
26
+
27
+ - Prerequisites: Python 3.12+, curl
28
+ - Install uv (package manager):
29
+ ```bash
30
+ curl -LsSf https://astral.sh/uv/install.sh | sh
31
+ # Restart your shell so "uv" is on PATH, or follow the installer hint
32
+ ```
33
+
34
+ ### Run the CLI locally
35
+ ```bash
36
+ make install
37
+
38
+ # Start the CLI
39
+ make run
40
+ # or
41
+ uv run openhands
42
+ ```
43
+
44
+ ### Build a standalone executable
45
+ ```bash
46
+ # Build (installs PyInstaller if needed)
47
+ ./build.sh --install-pyinstaller
48
+
49
+ # The binary will be in dist/
50
+ ./dist/openhands # macOS/Linux
51
+ # dist/openhands.exe # Windows
52
+ ```
@@ -0,0 +1,31 @@
1
+ openhands_cli/__init__.py,sha256=AqdjE-Up0uBSDyHvNjF0zWHNmA4ps-lsQWFpUWCWNEc,187
2
+ openhands_cli/agent_chat.py,sha256=FZ1NC31xJPXqyFGUtS-JqDI_HVP5155xEz59AHp3DIw,6224
3
+ openhands_cli/gui_launcher.py,sha256=EnZGcZMQzOHLwkmiHcYvt4WX2HfK0aT6L0t5ADWEfT0,7198
4
+ openhands_cli/llm_utils.py,sha256=eRhAjysm77GvzArKb-75agqTlR_4zzxTrYD1eQbgqbg,1606
5
+ openhands_cli/locations.py,sha256=zG5OnpJiX0kQ1fprEsOSi_cNqydEvxunPBCTpNrVaJ0,438
6
+ openhands_cli/pt_style.py,sha256=UxCxV-t88n5JjjYYMD0jf5apg4B5-WkfQ4EX_Eusj1I,1266
7
+ openhands_cli/runner.py,sha256=Oc6WEaJ0S-zfsPf30U4ozWrnqquvD2bpwX3AUnStpfs,6157
8
+ openhands_cli/setup.py,sha256=HWb2_6N6qgXvhIzh7jLecS7HA5EEcp7b3VroDxGiGz0,3799
9
+ openhands_cli/simple_main.py,sha256=7MmX8DgSvlFURzYsNqcE2vQ2MtdYTr-V88JlBmJlWvo,1697
10
+ openhands_cli/argparsers/main_parser.py,sha256=ceYNnktzp4xMwrsvQZl0Py6uG3atUgBsPFfqM7Cig9k,1677
11
+ openhands_cli/argparsers/serve_parser.py,sha256=WU2ClG5idbs966zJ5GfBXiaMH50e0ryGC1vD24JyJJ8,900
12
+ openhands_cli/listeners/__init__.py,sha256=T251L6mUEQdfk1x7-AWITlDjgVDwfW8XkyKQYK6Z5O0,180
13
+ openhands_cli/listeners/loading_listener.py,sha256=9sbuy64iAPqgvNojS-3RlWvepmaMC3R6FqlhTdYFKv8,1995
14
+ openhands_cli/listeners/pause_listener.py,sha256=FtWxj8T5r2lYIoquueeCf-3kF3zFMJ2e25wqOeF7QtM,2622
15
+ openhands_cli/tui/__init__.py,sha256=2KA7nugQxpIj4sEVDJavBbGHqcnySJFSvbPu1WRonPg,84
16
+ openhands_cli/tui/status.py,sha256=QNbS9OEK6d_c0P1eEQKfytuUF1R7uPxFl5nN-yv3cdA,3628
17
+ openhands_cli/tui/tui.py,sha256=JBahocaunF0LwJJLkcCmwSZN9xydb3SmH7fb8Jw8rFc,3545
18
+ openhands_cli/tui/utils.py,sha256=t7vJHU_AcLzuLTPku88Aqd51jyWY8HwH5Y2n-Bpa-WU,536
19
+ openhands_cli/tui/settings/mcp_screen.py,sha256=LMdcGD6DfwZQ4jnp-KM4gYiPXfNGHHPAqtEAXLKZtmA,8215
20
+ openhands_cli/tui/settings/settings_screen.py,sha256=CRekM2ZgWJ_YgzC-Jg1SgX2YeviU69Hb1cuzi5pVdio,7087
21
+ openhands_cli/tui/settings/store.py,sha256=tTMvYbdu9gs4GDNBoHFGFJnwm1PfjShgsVoMDjpQJ2Y,3485
22
+ openhands_cli/user_actions/__init__.py,sha256=Tf-lQj7-6bNl5rohSdcAFApkKbQb-N9JcKZgHa1HF60,501
23
+ openhands_cli/user_actions/agent_action.py,sha256=0v6kQPh8iE4CGjARA8vVD_TqSpS-aiscE3ucLC7jlZA,3283
24
+ openhands_cli/user_actions/exit_session.py,sha256=3u1CqWeYAEYfZi9BFh_dUwvy9aXXuvFbsZUHNGjVzuY,624
25
+ openhands_cli/user_actions/settings_action.py,sha256=QqWj37CAlrkV6FW9SEmmwsvKzdGyUYv4DlGjUweKsmg,5707
26
+ openhands_cli/user_actions/types.py,sha256=zMHoJqxmtbxAIq6O3RC-XoGzgbZzWZvRFNMOGW4MViQ,407
27
+ openhands_cli/user_actions/utils.py,sha256=qPTgP_1shnWgA7itrzTyfEGb_meXO-zq98JzFMUqCx4,6362
28
+ openhands-1.0.1.dist-info/METADATA,sha256=GV83vM7e_TD4avAskm3v-ZbwzJ_1EZ-A9jEAOzEF94I,1370
29
+ openhands-1.0.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
30
+ openhands-1.0.1.dist-info/entry_points.txt,sha256=CltyhTfx4lPndNPQXrZZDj5LmXrYXykVyIqoedPXsas,61
31
+ openhands-1.0.1.dist-info/RECORD,,
@@ -1,5 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: hatchling 1.27.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
-
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ openhands = openhands_cli.simple_main:main
@@ -0,0 +1,8 @@
1
+ """OpenHands package."""
2
+
3
+ from importlib.metadata import version, PackageNotFoundError
4
+
5
+ try:
6
+ __version__ = version("openhands")
7
+ except PackageNotFoundError:
8
+ __version__ = "0.0.0"
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Agent chat functionality for OpenHands CLI.
4
+ Provides a conversation interface with an AI agent using OpenHands patterns.
5
+ """
6
+
7
+ import sys
8
+ from datetime import datetime
9
+
10
+ from openhands.sdk import (
11
+ Message,
12
+ TextContent,
13
+ )
14
+ from openhands.sdk.conversation.state import AgentExecutionStatus
15
+ from prompt_toolkit import print_formatted_text
16
+ from prompt_toolkit.formatted_text import HTML
17
+
18
+ from openhands_cli.runner import ConversationRunner
19
+ from openhands_cli.setup import MissingAgentSpec, setup_conversation, start_fresh_conversation
20
+ from openhands_cli.tui.settings.mcp_screen import MCPScreen
21
+ from openhands_cli.tui.settings.settings_screen import SettingsScreen
22
+ from openhands_cli.tui.status import display_status
23
+ from openhands_cli.tui.tui import (
24
+ display_help,
25
+ display_welcome,
26
+ )
27
+ from openhands_cli.user_actions import UserConfirmation, exit_session_confirmation
28
+ from openhands_cli.user_actions.utils import get_session_prompter
29
+
30
+
31
+ def _restore_tty() -> None:
32
+ """
33
+ Ensure terminal modes are reset in case prompt_toolkit cleanup didn't run.
34
+ - Turn off application cursor keys (DECCKM): ESC[?1l
35
+ - Turn off bracketed paste: ESC[?2004l
36
+ """
37
+ try:
38
+ sys.stdout.write('\x1b[?1l\x1b[?2004l')
39
+ sys.stdout.flush()
40
+ except Exception:
41
+ pass
42
+
43
+
44
+ def _print_exit_hint(conversation_id: str) -> None:
45
+ """Print a resume hint with the current conversation ID."""
46
+ print_formatted_text(
47
+ HTML(f'<grey>Conversation ID:</grey> <yellow>{conversation_id}</yellow>')
48
+ )
49
+ print_formatted_text(
50
+ HTML(
51
+ f'<grey>Hint:</grey> run <gold>openhands --resume {conversation_id}</gold> '
52
+ 'to resume this conversation.'
53
+ )
54
+ )
55
+
56
+
57
+
58
+ def run_cli_entry(resume_conversation_id: str | None = None) -> None:
59
+ """Run the agent chat session using the agent SDK.
60
+
61
+
62
+ Raises:
63
+ AgentSetupError: If agent setup fails
64
+ KeyboardInterrupt: If user interrupts the session
65
+ EOFError: If EOF is encountered
66
+ """
67
+
68
+ try:
69
+ conversation = start_fresh_conversation(resume_conversation_id)
70
+ except MissingAgentSpec:
71
+ print_formatted_text(HTML('\n<yellow>Setup is required to use OpenHands CLI.</yellow>'))
72
+ print_formatted_text(HTML('\n<yellow>Goodbye! 👋</yellow>'))
73
+ return
74
+
75
+
76
+ display_welcome(conversation.id, bool(resume_conversation_id))
77
+
78
+ # Track session start time for uptime calculation
79
+ session_start_time = datetime.now()
80
+
81
+ # Create conversation runner to handle state machine logic
82
+ runner = ConversationRunner(conversation)
83
+ session = get_session_prompter()
84
+
85
+ # Main chat loop
86
+ while True:
87
+ try:
88
+ # Get user input
89
+ user_input = session.prompt(
90
+ HTML('<gold>> </gold>'),
91
+ multiline=False,
92
+ )
93
+
94
+ if not user_input.strip():
95
+ continue
96
+
97
+ # Handle commands
98
+ command = user_input.strip().lower()
99
+
100
+ message = Message(
101
+ role='user',
102
+ content=[TextContent(text=user_input)],
103
+ )
104
+
105
+ if command == '/exit':
106
+ exit_confirmation = exit_session_confirmation()
107
+ if exit_confirmation == UserConfirmation.ACCEPT:
108
+ print_formatted_text(HTML('\n<yellow>Goodbye! 👋</yellow>'))
109
+ _print_exit_hint(conversation.id)
110
+ break
111
+
112
+ elif command == '/settings':
113
+ settings_screen = SettingsScreen(conversation)
114
+ settings_screen.display_settings()
115
+ continue
116
+
117
+ elif command == '/mcp':
118
+ mcp_screen = MCPScreen()
119
+ mcp_screen.display_mcp_info(conversation.agent)
120
+ continue
121
+
122
+ elif command == '/clear':
123
+ display_welcome(conversation.id)
124
+ continue
125
+
126
+ elif command == '/new':
127
+ try:
128
+ # Start a fresh conversation (no resume ID = new conversation)
129
+ conversation = setup_conversation()
130
+ runner = ConversationRunner(conversation)
131
+ display_welcome(conversation.id, resume=False)
132
+ print_formatted_text(
133
+ HTML('<green>✓ Started fresh conversation</green>')
134
+ )
135
+ continue
136
+ except Exception as e:
137
+ print_formatted_text(
138
+ HTML(f'<red>Error starting fresh conversation: {e}</red>')
139
+ )
140
+ continue
141
+
142
+ elif command == '/help':
143
+ display_help()
144
+ continue
145
+
146
+ elif command == '/status':
147
+ display_status(conversation, session_start_time=session_start_time)
148
+ continue
149
+
150
+ elif command == '/confirm':
151
+ runner.toggle_confirmation_mode()
152
+ new_status = (
153
+ 'enabled' if runner.is_confirmation_mode_active else 'disabled'
154
+ )
155
+ print_formatted_text(
156
+ HTML(f'<yellow>Confirmation mode {new_status}</yellow>')
157
+ )
158
+ continue
159
+
160
+ elif command == '/resume':
161
+ if not (
162
+ conversation.state.agent_status == AgentExecutionStatus.PAUSED
163
+ or conversation.state.agent_status
164
+ == AgentExecutionStatus.WAITING_FOR_CONFIRMATION
165
+ ):
166
+ print_formatted_text(
167
+ HTML('<red>No paused conversation to resume...</red>')
168
+ )
169
+ continue
170
+
171
+ # Resume without new message
172
+ message = None
173
+
174
+ runner.process_message(message)
175
+
176
+ print() # Add spacing
177
+
178
+ except KeyboardInterrupt:
179
+ exit_confirmation = exit_session_confirmation()
180
+ if exit_confirmation == UserConfirmation.ACCEPT:
181
+ print_formatted_text(HTML('\n<yellow>Goodbye! 👋</yellow>'))
182
+ _print_exit_hint(conversation.id)
183
+ break
184
+
185
+ # Clean up terminal state
186
+ _restore_tty()
@@ -0,0 +1,56 @@
1
+ """Main argument parser for OpenHands CLI."""
2
+
3
+ import argparse
4
+
5
+
6
+ def create_main_parser() -> argparse.ArgumentParser:
7
+ """Create the main argument parser with CLI as default and serve as subcommand.
8
+
9
+ Returns:
10
+ The configured argument parser
11
+ """
12
+ parser = argparse.ArgumentParser(
13
+ description='OpenHands CLI - Terminal User Interface for OpenHands AI Agent',
14
+ formatter_class=argparse.RawDescriptionHelpFormatter,
15
+ epilog="""
16
+ By default, OpenHands runs in CLI mode (terminal interface).
17
+ Use 'serve' subcommand to launch the GUI server instead.
18
+
19
+ Examples:
20
+ openhands # Start CLI mode
21
+ openhands --resume conversation-id # Resume a conversation in CLI mode
22
+ openhands serve # Launch GUI server
23
+ openhands serve --gpu # Launch GUI server with GPU support
24
+ """
25
+ )
26
+
27
+ # CLI arguments at top level (default mode)
28
+ parser.add_argument(
29
+ '--resume',
30
+ type=str,
31
+ help='Conversation ID to resume'
32
+ )
33
+
34
+ # Only serve as subcommand
35
+ subparsers = parser.add_subparsers(
36
+ dest='command',
37
+ help='Additional commands'
38
+ )
39
+
40
+ # Add serve subcommand
41
+ serve_parser = subparsers.add_parser(
42
+ 'serve',
43
+ help='Launch the OpenHands GUI server using Docker (web interface)'
44
+ )
45
+ serve_parser.add_argument(
46
+ '--mount-cwd',
47
+ action='store_true',
48
+ help='Mount the current working directory in the Docker container'
49
+ )
50
+ serve_parser.add_argument(
51
+ '--gpu',
52
+ action='store_true',
53
+ help='Enable GPU support in the Docker container'
54
+ )
55
+
56
+ return parser
@@ -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',
17
+ help='Launch the OpenHands GUI server using Docker (web interface)'
18
+ )
19
+ serve_parser.add_argument(
20
+ '--mount-cwd',
21
+ help='Mount the current working directory into the GUI server container',
22
+ action='store_true',
23
+ default=False,
24
+ )
25
+ serve_parser.add_argument(
26
+ '--gpu',
27
+ help='Enable GPU support by mounting all GPUs into the Docker container via nvidia-docker',
28
+ action='store_true',
29
+ default=False,
30
+ )
31
+ return serve_parser
@@ -0,0 +1,220 @@
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
+ from openhands_cli.locations import PERSISTENCE_DIR
12
+
13
+
14
+ def _format_docker_command_for_logging(cmd: list[str]) -> str:
15
+ """Format a Docker command for logging with grey color.
16
+
17
+ Args:
18
+ cmd (list[str]): The Docker command as a list of strings
19
+
20
+ Returns:
21
+ str: The formatted command string in grey HTML color
22
+ """
23
+ cmd_str = ' '.join(cmd)
24
+ return f'<grey>Running Docker command: {cmd_str}</grey>'
25
+
26
+
27
+ def check_docker_requirements() -> bool:
28
+ """Check if Docker is installed and running.
29
+
30
+ Returns:
31
+ bool: True if Docker is available and running, False otherwise.
32
+ """
33
+ # Check if Docker is installed
34
+ if not shutil.which('docker'):
35
+ print_formatted_text(
36
+ HTML('<ansired>❌ Docker is not installed or not in PATH.</ansired>')
37
+ )
38
+ print_formatted_text(
39
+ HTML(
40
+ '<grey>Please install Docker first: https://docs.docker.com/get-docker/</grey>'
41
+ )
42
+ )
43
+ return False
44
+
45
+ # Check if Docker daemon is running
46
+ try:
47
+ result = subprocess.run(
48
+ ['docker', 'info'], capture_output=True, text=True, timeout=10
49
+ )
50
+ if result.returncode != 0:
51
+ print_formatted_text(
52
+ HTML('<ansired>❌ Docker daemon is not running.</ansired>')
53
+ )
54
+ print_formatted_text(
55
+ HTML('<grey>Please start Docker and try again.</grey>')
56
+ )
57
+ return False
58
+ except (subprocess.TimeoutExpired, subprocess.SubprocessError) as e:
59
+ print_formatted_text(
60
+ HTML('<ansired>❌ Failed to check Docker status.</ansired>')
61
+ )
62
+ print_formatted_text(HTML(f'<grey>Error: {e}</grey>'))
63
+ return False
64
+
65
+ return True
66
+
67
+
68
+ def ensure_config_dir_exists() -> Path:
69
+ """Ensure the OpenHands configuration directory exists and return its path."""
70
+ path = Path(PERSISTENCE_DIR)
71
+ path.mkdir(exist_ok=True, parents=True)
72
+ return path
73
+
74
+
75
+ def get_openhands_version() -> str:
76
+ """Get the OpenHands version for Docker images.
77
+
78
+ Returns:
79
+ str: The version string to use for Docker images
80
+ """
81
+ # For now, use 'latest' as the default version
82
+ # In the future, this could be read from a version file or environment variable
83
+ return os.environ.get('OPENHANDS_VERSION', 'latest')
84
+
85
+
86
+ def launch_gui_server(mount_cwd: bool = False, gpu: bool = False) -> None:
87
+ """Launch the OpenHands GUI server using Docker.
88
+
89
+ Args:
90
+ mount_cwd: If True, mount the current working directory into the container.
91
+ gpu: If True, enable GPU support by mounting all GPUs into the container via nvidia-docker.
92
+ """
93
+ print_formatted_text(
94
+ HTML('<ansiblue>🚀 Launching OpenHands GUI server...</ansiblue>')
95
+ )
96
+ print_formatted_text('')
97
+
98
+ # Check Docker requirements
99
+ if not check_docker_requirements():
100
+ sys.exit(1)
101
+
102
+ # Ensure config directory exists
103
+ config_dir = ensure_config_dir_exists()
104
+
105
+ # Get the current version for the Docker image
106
+ version = get_openhands_version()
107
+ runtime_image = f'docker.all-hands.dev/all-hands-ai/runtime:{version}-nikolaik'
108
+ app_image = f'docker.all-hands.dev/all-hands-ai/openhands:{version}'
109
+
110
+ print_formatted_text(HTML('<grey>Pulling required Docker images...</grey>'))
111
+
112
+ # Pull the runtime image first
113
+ pull_cmd = ['docker', 'pull', runtime_image]
114
+ print_formatted_text(HTML(_format_docker_command_for_logging(pull_cmd)))
115
+ try:
116
+ subprocess.run(pull_cmd, check=True)
117
+ except subprocess.CalledProcessError:
118
+ print_formatted_text(
119
+ HTML('<ansired>❌ Failed to pull runtime image.</ansired>')
120
+ )
121
+ sys.exit(1)
122
+
123
+ print_formatted_text('')
124
+ print_formatted_text(
125
+ HTML('<ansigreen>✅ Starting OpenHands GUI server...</ansigreen>')
126
+ )
127
+ print_formatted_text(
128
+ HTML('<grey>The server will be available at: http://localhost:3000</grey>')
129
+ )
130
+ print_formatted_text(HTML('<grey>Press Ctrl+C to stop the server.</grey>'))
131
+ print_formatted_text('')
132
+
133
+ # Build the Docker command
134
+ docker_cmd = [
135
+ 'docker',
136
+ 'run',
137
+ '-it',
138
+ '--rm',
139
+ '--pull=always',
140
+ '-e',
141
+ f'SANDBOX_RUNTIME_CONTAINER_IMAGE={runtime_image}',
142
+ '-e',
143
+ 'LOG_ALL_EVENTS=true',
144
+ '-v',
145
+ '/var/run/docker.sock:/var/run/docker.sock',
146
+ '-v',
147
+ f'{config_dir}:/.openhands',
148
+ ]
149
+
150
+ # Add GPU support if requested
151
+ if gpu:
152
+ print_formatted_text(
153
+ HTML('<ansigreen>🖥️ Enabling GPU support via nvidia-docker...</ansigreen>')
154
+ )
155
+ # Add the --gpus all flag to enable all GPUs
156
+ docker_cmd.insert(2, '--gpus')
157
+ docker_cmd.insert(3, 'all')
158
+ # Add environment variable to pass GPU support to sandbox containers
159
+ docker_cmd.extend(
160
+ [
161
+ '-e',
162
+ 'SANDBOX_ENABLE_GPU=true',
163
+ ]
164
+ )
165
+
166
+ # Add current working directory mount if requested
167
+ if mount_cwd:
168
+ cwd = Path.cwd()
169
+ # Following the documentation at https://docs.all-hands.dev/usage/runtimes/docker#connecting-to-your-filesystem
170
+ docker_cmd.extend(
171
+ [
172
+ '-e',
173
+ f'SANDBOX_VOLUMES={cwd}:/workspace:rw',
174
+ ]
175
+ )
176
+
177
+ # Set user ID for Unix-like systems only
178
+ if os.name != 'nt': # Not Windows
179
+ try:
180
+ user_id = subprocess.check_output(['id', '-u'], text=True).strip()
181
+ docker_cmd.extend(['-e', f'SANDBOX_USER_ID={user_id}'])
182
+ except (subprocess.CalledProcessError, FileNotFoundError):
183
+ # If 'id' command fails or doesn't exist, skip setting user ID
184
+ pass
185
+ # Print the folder that will be mounted to inform the user
186
+ print_formatted_text(
187
+ HTML(
188
+ f'<ansigreen>📂 Mounting current directory:</ansigreen> <ansiyellow>{cwd}</ansiyellow> <ansigreen>to</ansigreen> <ansiyellow>/workspace</ansiyellow>'
189
+ )
190
+ )
191
+
192
+ docker_cmd.extend(
193
+ [
194
+ '-p',
195
+ '3000:3000',
196
+ '--add-host',
197
+ 'host.docker.internal:host-gateway',
198
+ '--name',
199
+ 'openhands-app',
200
+ app_image,
201
+ ]
202
+ )
203
+
204
+ try:
205
+ # Log and run the Docker command
206
+ print_formatted_text(HTML(_format_docker_command_for_logging(docker_cmd)))
207
+ subprocess.run(docker_cmd, check=True)
208
+ except subprocess.CalledProcessError as e:
209
+ print_formatted_text('')
210
+ print_formatted_text(
211
+ HTML('<ansired>❌ Failed to start OpenHands GUI server.</ansired>')
212
+ )
213
+ print_formatted_text(HTML(f'<grey>Error: {e}</grey>'))
214
+ sys.exit(1)
215
+ except KeyboardInterrupt:
216
+ print_formatted_text('')
217
+ print_formatted_text(
218
+ HTML('<ansigreen>✓ OpenHands GUI server stopped successfully.</ansigreen>')
219
+ )
220
+ sys.exit(0)
@@ -0,0 +1,4 @@
1
+ from openhands_cli.listeners.loading_listener import LoadingContext
2
+ from openhands_cli.listeners.pause_listener import PauseListener
3
+
4
+ __all__ = ['PauseListener', 'LoadingContext']
@@ -0,0 +1,63 @@
1
+ """
2
+ Loading animation utilities for OpenHands CLI.
3
+ Provides animated loading screens during agent initialization.
4
+ """
5
+
6
+ import sys
7
+ import threading
8
+ import time
9
+
10
+
11
+ def display_initialization_animation(text: str, is_loaded: threading.Event) -> None:
12
+ """Display a spinning animation while agent is being initialized.
13
+
14
+ Args:
15
+ text: The text to display alongside the animation
16
+ is_loaded: Threading event that signals when loading is complete
17
+ """
18
+ ANIMATION_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
19
+
20
+ i = 0
21
+ while not is_loaded.is_set():
22
+ sys.stdout.write('\n')
23
+ sys.stdout.write(
24
+ f'\033[s\033[J\033[38;2;255;215;0m[{ANIMATION_FRAMES[i % len(ANIMATION_FRAMES)]}] {text}\033[0m\033[u\033[1A'
25
+ )
26
+ sys.stdout.flush()
27
+ time.sleep(0.1)
28
+ i += 1
29
+
30
+ sys.stdout.write('\r' + ' ' * (len(text) + 10) + '\r')
31
+ sys.stdout.flush()
32
+
33
+
34
+ class LoadingContext:
35
+ """Context manager for displaying loading animations in a separate thread."""
36
+
37
+ def __init__(self, text: str):
38
+ """Initialize the loading context.
39
+
40
+ Args:
41
+ text: The text to display during loading
42
+ """
43
+ self.text = text
44
+ self.is_loaded = threading.Event()
45
+ self.loading_thread: threading.Thread | None = None
46
+
47
+ def __enter__(self) -> 'LoadingContext':
48
+ """Start the loading animation in a separate thread."""
49
+ self.loading_thread = threading.Thread(
50
+ target=display_initialization_animation,
51
+ args=(self.text, self.is_loaded),
52
+ daemon=True,
53
+ )
54
+ self.loading_thread.start()
55
+ return self
56
+
57
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
58
+ """Stop the loading animation and clean up the thread."""
59
+ self.is_loaded.set()
60
+ if self.loading_thread:
61
+ self.loading_thread.join(
62
+ timeout=1.0
63
+ ) # Wait up to 1 second for thread to finish