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,100 @@
1
+ from collections.abc import Generator
2
+ from uuid import UUID
3
+
4
+ from prompt_toolkit import print_formatted_text
5
+ from prompt_toolkit.completion import CompleteEvent, Completer, Completion
6
+ from prompt_toolkit.document import Document
7
+ from prompt_toolkit.formatted_text import HTML
8
+ from prompt_toolkit.shortcuts import clear
9
+
10
+ from openhands_cli import __version__
11
+ from openhands_cli.pt_style import get_cli_style
12
+
13
+ DEFAULT_STYLE = get_cli_style()
14
+
15
+ # Available commands with descriptions
16
+ COMMANDS = {
17
+ '/exit': 'Exit the application',
18
+ '/help': 'Display available commands',
19
+ '/clear': 'Clear the screen',
20
+ '/new': 'Start a fresh conversation',
21
+ '/status': 'Display conversation details',
22
+ '/confirm': 'Toggle confirmation mode on/off',
23
+ '/resume': 'Resume a paused conversation',
24
+ '/settings': 'Display and modify current settings',
25
+ '/mcp': 'View MCP (Model Context Protocol) server configuration',
26
+ }
27
+
28
+
29
+ class CommandCompleter(Completer):
30
+ """Custom completer for commands with interactive dropdown."""
31
+
32
+ def get_completions(
33
+ self, document: Document, complete_event: CompleteEvent
34
+ ) -> Generator[Completion, None, None]:
35
+ text = document.text_before_cursor.lstrip()
36
+ if text.startswith('/'):
37
+ for command, description in COMMANDS.items():
38
+ if command.startswith(text):
39
+ yield Completion(
40
+ command,
41
+ start_position=-len(text),
42
+ display_meta=description,
43
+ style='bg:ansidarkgray fg:gold',
44
+ )
45
+
46
+
47
+ def display_banner(conversation_id: str, resume: bool = False) -> None:
48
+ print_formatted_text(
49
+ HTML(r"""<gold>
50
+ ___ _ _ _
51
+ / _ \ _ __ ___ _ __ | | | | __ _ _ __ __| |___
52
+ | | | | '_ \ / _ \ '_ \| |_| |/ _` | '_ \ / _` / __|
53
+ | |_| | |_) | __/ | | | _ | (_| | | | | (_| \__ \
54
+ \___ /| .__/ \___|_| |_|_| |_|\__,_|_| |_|\__,_|___/
55
+ |_|
56
+ </gold>"""),
57
+ style=DEFAULT_STYLE,
58
+ )
59
+
60
+ print_formatted_text('')
61
+ if not resume:
62
+ print_formatted_text(
63
+ HTML(f'<grey>Initialized conversation {conversation_id}</grey>')
64
+ )
65
+ else:
66
+ print_formatted_text(
67
+ HTML(f'<grey>Resumed conversation {conversation_id}</grey>')
68
+ )
69
+ print_formatted_text('')
70
+
71
+
72
+ def display_help() -> None:
73
+ """Display help information about available commands."""
74
+ print_formatted_text('')
75
+ print_formatted_text(HTML('<gold>🤖 OpenHands CLI Help</gold>'))
76
+ print_formatted_text(HTML('<grey>Available commands:</grey>'))
77
+ print_formatted_text('')
78
+
79
+ for command, description in COMMANDS.items():
80
+ print_formatted_text(HTML(f' <white>{command}</white> - {description}'))
81
+
82
+ print_formatted_text('')
83
+ print_formatted_text(HTML('<grey>Tips:</grey>'))
84
+ print_formatted_text(' • Type / and press Tab to see command suggestions')
85
+ print_formatted_text(' • Use arrow keys to navigate through suggestions')
86
+ print_formatted_text(' • Press Enter to select a command')
87
+ print_formatted_text('')
88
+
89
+
90
+ def display_welcome(conversation_id: UUID, resume: bool = False) -> None:
91
+ """Display welcome message."""
92
+ clear()
93
+ display_banner(str(conversation_id), resume)
94
+ print_formatted_text(HTML("<gold>Let's start building!</gold>"))
95
+ print_formatted_text(
96
+ HTML(
97
+ '<green>What do you want to build? <grey>Type /help for help</grey></green>'
98
+ )
99
+ )
100
+ print()
@@ -0,0 +1,14 @@
1
+ class StepCounter:
2
+ """Automatically manages step numbering for settings flows."""
3
+
4
+ def __init__(self, total_steps: int):
5
+ self.current_step = 0
6
+ self.total_steps = total_steps
7
+
8
+ def next_step(self, prompt: str) -> str:
9
+ """Get the next step prompt with automatic numbering."""
10
+ self.current_step += 1
11
+ return f'(Step {self.current_step}/{self.total_steps}) {prompt}'
12
+
13
+ def existing_step(self, prompt: str) -> str:
14
+ return f'(Step {self.current_step}/{self.total_steps}) {prompt}'
@@ -0,0 +1,17 @@
1
+ from openhands_cli.user_actions.agent_action import ask_user_confirmation
2
+ from openhands_cli.user_actions.exit_session import (
3
+ exit_session_confirmation,
4
+ )
5
+ from openhands_cli.user_actions.settings_action import (
6
+ choose_llm_provider,
7
+ settings_type_confirmation,
8
+ )
9
+ from openhands_cli.user_actions.types import UserConfirmation
10
+
11
+ __all__ = [
12
+ 'ask_user_confirmation',
13
+ 'exit_session_confirmation',
14
+ 'UserConfirmation',
15
+ 'settings_type_confirmation',
16
+ 'choose_llm_provider',
17
+ ]
@@ -0,0 +1,95 @@
1
+ import html
2
+ from prompt_toolkit import HTML, print_formatted_text
3
+
4
+ from openhands.sdk.security.confirmation_policy import (
5
+ ConfirmRisky,
6
+ NeverConfirm,
7
+ SecurityRisk,
8
+ )
9
+ from openhands_cli.user_actions.types import ConfirmationResult, UserConfirmation
10
+ from openhands_cli.user_actions.utils import cli_confirm, cli_text_input
11
+
12
+
13
+ def ask_user_confirmation(
14
+ pending_actions: list, using_risk_based_policy: bool = False
15
+ ) -> ConfirmationResult:
16
+ """Ask user to confirm pending actions.
17
+
18
+ Args:
19
+ pending_actions: List of pending actions from the agent
20
+
21
+ Returns:
22
+ ConfirmationResult with decision, optional policy_change, and reason
23
+ """
24
+
25
+ if not pending_actions:
26
+ return ConfirmationResult(decision=UserConfirmation.ACCEPT)
27
+
28
+ print_formatted_text(
29
+ HTML(
30
+ f'<yellow>🔍 Agent created {len(pending_actions)} action(s) and is waiting for confirmation:</yellow>'
31
+ )
32
+ )
33
+
34
+ for i, action in enumerate(pending_actions, 1):
35
+ tool_name = getattr(action, 'tool_name', '[unknown tool]')
36
+ action_content = (
37
+ str(getattr(action, 'action', ''))[:100].replace('\n', ' ')
38
+ or '[unknown action]'
39
+ )
40
+ print_formatted_text(
41
+ HTML(f'<grey> {i}. {tool_name}: {html.escape(action_content)}...</grey>')
42
+ )
43
+
44
+ question = 'Choose an option:'
45
+ options = [
46
+ 'Yes, proceed',
47
+ 'No, reject (w/o reason)',
48
+ 'No, reject with reason',
49
+ "Always proceed (don't ask again)",
50
+ ]
51
+
52
+ if not using_risk_based_policy:
53
+ options.append('Auto-confirm LOW/MEDIUM risk, ask for HIGH risk')
54
+
55
+ try:
56
+ index = cli_confirm(question, options, escapable=True)
57
+ except (EOFError, KeyboardInterrupt):
58
+ print_formatted_text(HTML('\n<red>No input received; pausing agent.</red>'))
59
+ return ConfirmationResult(decision=UserConfirmation.DEFER)
60
+
61
+ if index == 0:
62
+ return ConfirmationResult(decision=UserConfirmation.ACCEPT)
63
+ elif index == 1:
64
+ return ConfirmationResult(decision=UserConfirmation.REJECT)
65
+ elif index == 2:
66
+ try:
67
+ reason_result = cli_text_input(
68
+ 'Please enter your reason for rejecting these actions: '
69
+ )
70
+ except Exception:
71
+ return ConfirmationResult(decision=UserConfirmation.DEFER)
72
+
73
+ # Support both string return and (reason, cancelled) tuple for tests
74
+ cancelled = False
75
+ if isinstance(reason_result, tuple) and len(reason_result) >= 1:
76
+ reason = reason_result[0] or ''
77
+ cancelled = bool(reason_result[1]) if len(reason_result) > 1 else False
78
+ else:
79
+ reason = str(reason_result or '').strip()
80
+
81
+ if cancelled:
82
+ return ConfirmationResult(decision=UserConfirmation.DEFER)
83
+
84
+ return ConfirmationResult(decision=UserConfirmation.REJECT, reason=reason)
85
+ elif index == 3:
86
+ return ConfirmationResult(
87
+ decision=UserConfirmation.ACCEPT, policy_change=NeverConfirm()
88
+ )
89
+ elif index == 4:
90
+ return ConfirmationResult(
91
+ decision=UserConfirmation.ACCEPT,
92
+ policy_change=ConfirmRisky(threshold=SecurityRisk.HIGH),
93
+ )
94
+
95
+ return ConfirmationResult(decision=UserConfirmation.REJECT)
@@ -0,0 +1,18 @@
1
+ from openhands_cli.user_actions.types import UserConfirmation
2
+ from openhands_cli.user_actions.utils import cli_confirm
3
+
4
+
5
+ def exit_session_confirmation() -> UserConfirmation:
6
+ """
7
+ Ask user to confirm exiting session.
8
+ """
9
+
10
+ question = 'Terminate session?'
11
+ options = ['Yes, proceed', 'No, dismiss']
12
+ index = cli_confirm(question, options) # Blocking UI, not escapable
13
+
14
+ options_mapping = {
15
+ 0: UserConfirmation.ACCEPT, # User accepts termination session
16
+ 1: UserConfirmation.REJECT, # User does not terminate session
17
+ }
18
+ return options_mapping.get(index, UserConfirmation.REJECT)
@@ -0,0 +1,171 @@
1
+ from enum import Enum
2
+
3
+ from openhands.sdk.llm import UNVERIFIED_MODELS_EXCLUDING_BEDROCK, VERIFIED_MODELS
4
+ from prompt_toolkit.completion import FuzzyWordCompleter
5
+ from pydantic import SecretStr
6
+
7
+ from openhands_cli.tui.utils import StepCounter
8
+ from openhands_cli.user_actions.utils import (
9
+ NonEmptyValueValidator,
10
+ cli_confirm,
11
+ cli_text_input,
12
+ )
13
+
14
+
15
+ class SettingsType(Enum):
16
+ BASIC = 'basic'
17
+ ADVANCED = 'advanced'
18
+
19
+
20
+ def settings_type_confirmation(first_time: bool = False) -> SettingsType:
21
+ question = (
22
+ '\nWelcome to OpenHands! Let\'s configure your LLM settings.\n'
23
+ 'Choose your preferred setup method:'
24
+ )
25
+ choices = [
26
+ 'LLM (Basic)',
27
+ 'LLM (Advanced)'
28
+ ]
29
+ if not first_time:
30
+ question = 'Which settings would you like to modify?'
31
+ choices.append('Go back')
32
+
33
+
34
+ index = cli_confirm(question, choices, escapable=True)
35
+
36
+ if choices[index] == 'Go back':
37
+ raise KeyboardInterrupt
38
+
39
+ options_map = {0: SettingsType.BASIC, 1: SettingsType.ADVANCED}
40
+
41
+ return options_map.get(index)
42
+
43
+
44
+ def choose_llm_provider(step_counter: StepCounter, escapable=True) -> str:
45
+ question = step_counter.next_step(
46
+ 'Select LLM Provider (TAB for options, CTRL-c to cancel): '
47
+ )
48
+ options = (
49
+ list(VERIFIED_MODELS.keys()).copy()
50
+ + list(UNVERIFIED_MODELS_EXCLUDING_BEDROCK.keys()).copy()
51
+ )
52
+ alternate_option = 'Select another provider'
53
+
54
+ display_options = options[:4] + [alternate_option]
55
+
56
+ index = cli_confirm(question, display_options, escapable=escapable)
57
+ chosen_option = display_options[index]
58
+ if display_options[index] != alternate_option:
59
+ return chosen_option
60
+
61
+ question = step_counter.existing_step(
62
+ 'Type LLM Provider (TAB to complete, CTRL-c to cancel): '
63
+ )
64
+ return cli_text_input(
65
+ question, escapable=True, completer=FuzzyWordCompleter(options, WORD=True)
66
+ )
67
+
68
+
69
+ def choose_llm_model(step_counter: StepCounter, provider: str, escapable=True) -> str:
70
+ """Choose LLM model using spec-driven approach. Return (model, deferred)."""
71
+
72
+ models = VERIFIED_MODELS.get(
73
+ provider, []
74
+ ) + UNVERIFIED_MODELS_EXCLUDING_BEDROCK.get(provider, [])
75
+
76
+ if provider == 'openhands':
77
+ question = (
78
+ step_counter.next_step('Select Available OpenHands Model:\n')
79
+ + 'LLM usage is billed at the providers’ rates with no markup. Details: https://docs.all-hands.dev/usage/llms/openhands-llms'
80
+ )
81
+ else:
82
+ question = step_counter.next_step(
83
+ 'Select LLM Model (TAB for options, CTRL-c to cancel): '
84
+ )
85
+ alternate_option = 'Select another model'
86
+ display_options = models[:4] + [alternate_option]
87
+ index = cli_confirm(question, display_options, escapable=escapable)
88
+ chosen_option = display_options[index]
89
+
90
+ if chosen_option != alternate_option:
91
+ return chosen_option
92
+
93
+ question = step_counter.existing_step(
94
+ 'Type model id (TAB to complete, CTRL-c to cancel): '
95
+ )
96
+
97
+ return cli_text_input(
98
+ question, escapable=True, completer=FuzzyWordCompleter(models, WORD=True)
99
+ )
100
+
101
+
102
+ def prompt_api_key(
103
+ step_counter: StepCounter,
104
+ provider: str,
105
+ existing_api_key: SecretStr | None = None,
106
+ escapable=True,
107
+ ) -> str:
108
+ helper_text = (
109
+ '\nYou can find your OpenHands LLM API Key in the API Keys tab of OpenHands Cloud: '
110
+ 'https://app.all-hands.dev/settings/api-keys\n'
111
+ if provider == 'openhands'
112
+ else ''
113
+ )
114
+
115
+ if existing_api_key:
116
+ masked_key = existing_api_key.get_secret_value()[:3] + '***'
117
+ question = f'Enter API Key [{masked_key}] (CTRL-c to cancel, ENTER to keep current, type new to change): '
118
+ # For existing keys, allow empty input to keep current key
119
+ validator = None
120
+ else:
121
+ question = 'Enter API Key (CTRL-c to cancel): '
122
+ # For new keys, require non-empty input
123
+ validator = NonEmptyValueValidator()
124
+
125
+ question = helper_text + step_counter.next_step(question)
126
+ user_input = cli_text_input(
127
+ question, escapable=escapable, validator=validator, is_password=True
128
+ )
129
+
130
+ # If user pressed ENTER with existing key (empty input), return the existing key
131
+ if existing_api_key and not user_input.strip():
132
+ return existing_api_key.get_secret_value()
133
+
134
+ return user_input
135
+
136
+
137
+ # Advanced settings functions
138
+ def prompt_custom_model(step_counter: StepCounter, escapable=True) -> str:
139
+ """Prompt for custom model name."""
140
+ question = step_counter.next_step('Custom Model (CTRL-c to cancel): ')
141
+ return cli_text_input(question, escapable=escapable)
142
+
143
+
144
+ def prompt_base_url(step_counter: StepCounter, escapable=True) -> str:
145
+ """Prompt for base URL."""
146
+ question = step_counter.next_step('Base URL (CTRL-c to cancel): ')
147
+ return cli_text_input(
148
+ question, escapable=escapable, validator=NonEmptyValueValidator()
149
+ )
150
+
151
+
152
+ def choose_memory_condensation(step_counter: StepCounter, escapable=True) -> bool:
153
+ """Choose memory condensation setting."""
154
+ question = step_counter.next_step('Memory Condensation (CTRL-c to cancel): ')
155
+ choices = ['Enable', 'Disable']
156
+
157
+ index = cli_confirm(question, choices, escapable=escapable)
158
+ return index == 0 # True for Enable, False for Disable
159
+
160
+
161
+ def save_settings_confirmation() -> bool:
162
+ """Prompt user to confirm saving settings."""
163
+ question = 'Save new settings? (They will take effect after restart)'
164
+ discard = 'No, discard'
165
+ options = ['Yes, save', discard]
166
+
167
+ index = cli_confirm(question, options, escapable=True)
168
+ if options[index] == discard:
169
+ raise KeyboardInterrupt
170
+
171
+ return options[index]
@@ -0,0 +1,18 @@
1
+ from enum import Enum
2
+ from typing import Optional
3
+
4
+ from pydantic import BaseModel
5
+
6
+ from openhands.sdk.security.confirmation_policy import ConfirmationPolicyBase
7
+
8
+
9
+ class UserConfirmation(Enum):
10
+ ACCEPT = 'accept'
11
+ REJECT = 'reject'
12
+ DEFER = 'defer'
13
+
14
+
15
+ class ConfirmationResult(BaseModel):
16
+ decision: UserConfirmation
17
+ policy_change: Optional[ConfirmationPolicyBase] = None
18
+ reason: str = ''
@@ -0,0 +1,199 @@
1
+ from prompt_toolkit import HTML, PromptSession
2
+ from prompt_toolkit.application import Application
3
+ from prompt_toolkit.completion import Completer
4
+ from prompt_toolkit.input.base import Input
5
+ from prompt_toolkit.key_binding import KeyBindings
6
+ from prompt_toolkit.key_binding.key_processor import KeyPressEvent
7
+ from prompt_toolkit.layout.containers import HSplit, Window
8
+ from prompt_toolkit.layout.controls import FormattedTextControl
9
+ from prompt_toolkit.layout.dimension import Dimension
10
+ from prompt_toolkit.layout.layout import Layout
11
+ from prompt_toolkit.output.base import Output
12
+ from prompt_toolkit.shortcuts import prompt
13
+ from prompt_toolkit.validation import ValidationError, Validator
14
+
15
+ from openhands_cli.tui import DEFAULT_STYLE
16
+ from openhands_cli.tui.tui import CommandCompleter
17
+
18
+
19
+ def build_keybindings(
20
+ choices: list[str], selected: list[int], escapable: bool
21
+ ) -> KeyBindings:
22
+ """Create keybindings for the confirm UI. Split for testability."""
23
+ kb = KeyBindings()
24
+
25
+ @kb.add('up')
26
+ def _handle_up(event: KeyPressEvent) -> None:
27
+ selected[0] = (selected[0] - 1) % len(choices)
28
+
29
+ @kb.add('down')
30
+ def _handle_down(event: KeyPressEvent) -> None:
31
+ selected[0] = (selected[0] + 1) % len(choices)
32
+
33
+ @kb.add('enter')
34
+ def _handle_enter(event: KeyPressEvent) -> None:
35
+ event.app.exit(result=selected[0])
36
+
37
+ if escapable:
38
+
39
+ @kb.add('c-c') # Ctrl+C
40
+ def _handle_hard_interrupt(event: KeyPressEvent) -> None:
41
+ event.app.exit(exception=KeyboardInterrupt())
42
+
43
+ @kb.add('c-p') # Ctrl+P
44
+ def _handle_pause_interrupt(event: KeyPressEvent) -> None:
45
+ event.app.exit(exception=KeyboardInterrupt())
46
+
47
+ @kb.add('escape') # Escape key
48
+ def _handle_escape(event: KeyPressEvent) -> None:
49
+ event.app.exit(exception=KeyboardInterrupt())
50
+
51
+ return kb
52
+
53
+
54
+ def build_layout(question: str, choices: list[str], selected_ref: list[int]) -> Layout:
55
+ """Create the layout for the confirm UI. Split for testability."""
56
+
57
+ def get_choice_text() -> list[tuple[str, str]]:
58
+ lines: list[tuple[str, str]] = []
59
+ lines.append(('class:question', f'{question}\n\n'))
60
+ for i, choice in enumerate(choices):
61
+ is_selected = i == selected_ref[0]
62
+ prefix = '> ' if is_selected else ' '
63
+ style = 'class:selected' if is_selected else 'class:unselected'
64
+ lines.append((style, f'{prefix}{choice}\n'))
65
+ return lines
66
+
67
+ content_window = Window(
68
+ FormattedTextControl(get_choice_text),
69
+ always_hide_cursor=True,
70
+ height=Dimension(max=8),
71
+ )
72
+ return Layout(HSplit([content_window]))
73
+
74
+
75
+ def cli_confirm(
76
+ question: str = 'Are you sure?',
77
+ choices: list[str] | None = None,
78
+ initial_selection: int = 0,
79
+ escapable: bool = False,
80
+ input: Input | None = None, # strictly for unit testing
81
+ output: Output | None = None, # strictly for unit testing
82
+ ) -> int:
83
+ """Display a confirmation prompt with the given question and choices.
84
+
85
+ Returns the index of the selected choice.
86
+ """
87
+ if choices is None:
88
+ choices = ['Yes', 'No']
89
+ selected = [initial_selection] # Using list to allow modification in closure
90
+
91
+ kb = build_keybindings(choices, selected, escapable)
92
+ layout = build_layout(question, choices, selected)
93
+
94
+ app = Application(
95
+ layout=layout,
96
+ key_bindings=kb,
97
+ style=DEFAULT_STYLE,
98
+ full_screen=False,
99
+ input=input,
100
+ output=output,
101
+ )
102
+
103
+ return int(app.run(in_thread=True))
104
+
105
+
106
+ def cli_text_input(
107
+ question: str,
108
+ escapable: bool = True,
109
+ completer: Completer | None = None,
110
+ validator: Validator = None,
111
+ is_password: bool = False,
112
+ ) -> str:
113
+ """Prompt user to enter text input with optional validation.
114
+
115
+ Args:
116
+ question: The prompt question to display
117
+ escapable: Whether the user can escape with Ctrl+C or Ctrl+P
118
+ completer: Optional completer for tab completion
119
+ validator: Optional callable that takes a string and returns True if valid.
120
+ If validation fails, the callable should display error messages
121
+ and the user will be reprompted.
122
+
123
+ Returns:
124
+ The validated user input string (stripped of whitespace)
125
+ """
126
+
127
+ kb = KeyBindings()
128
+
129
+ if escapable:
130
+
131
+ @kb.add('c-c')
132
+ def _(event: KeyPressEvent) -> None:
133
+ event.app.exit(exception=KeyboardInterrupt())
134
+
135
+ @kb.add('c-p')
136
+ def _(event: KeyPressEvent) -> None:
137
+ event.app.exit(exception=KeyboardInterrupt())
138
+
139
+ @kb.add('enter')
140
+ def _handle_enter(event: KeyPressEvent):
141
+ event.app.exit(result=event.current_buffer.text)
142
+
143
+ reason = str(
144
+ prompt(
145
+ question,
146
+ style=DEFAULT_STYLE,
147
+ key_bindings=kb,
148
+ completer=completer,
149
+ is_password=is_password,
150
+ validator=validator,
151
+ )
152
+ )
153
+ return reason.strip()
154
+
155
+
156
+ def get_session_prompter(
157
+ input: Input | None = None, # strictly for unit testing
158
+ output: Output | None = None, # strictly for unit testing
159
+ ) -> PromptSession:
160
+ bindings = KeyBindings()
161
+
162
+ @bindings.add('\\', 'enter')
163
+ def _(event: KeyPressEvent) -> None:
164
+ # Typing '\' + Enter forces a newline regardless
165
+ event.current_buffer.insert_text('\n')
166
+
167
+ @bindings.add('enter')
168
+ def _handle_enter(event: KeyPressEvent):
169
+ event.app.exit(result=event.current_buffer.text)
170
+
171
+ @bindings.add('c-c')
172
+ def _keyboard_interrupt(event: KeyPressEvent):
173
+ event.app.exit(exception=KeyboardInterrupt())
174
+
175
+ session = PromptSession(
176
+ completer=CommandCompleter(),
177
+ key_bindings=bindings,
178
+ prompt_continuation=lambda width, line_number, is_soft_wrap: '...',
179
+ multiline=True,
180
+ input=input,
181
+ output=output,
182
+ style=DEFAULT_STYLE,
183
+ placeholder=HTML(
184
+ '<placeholder>'
185
+ 'Type your message… (tip: press <b>\\</b> + <b>Enter</b> to insert a newline)'
186
+ '</placeholder>'
187
+ ),
188
+ )
189
+
190
+ return session
191
+
192
+
193
+ class NonEmptyValueValidator(Validator):
194
+ def validate(self, document):
195
+ text = document.text
196
+ if not text:
197
+ raise ValidationError(
198
+ message='API key cannot be empty. Please enter a valid API key.'
199
+ )
openhands/__init__.py DELETED
@@ -1 +0,0 @@
1
- __path__ = __import__("pkgutil").extend_path(__path__, __name__)
openhands/sdk/__init__.py DELETED
@@ -1,45 +0,0 @@
1
- from importlib.metadata import PackageNotFoundError, version
2
-
3
- from openhands.sdk.agent import Agent, AgentBase
4
- from openhands.sdk.context import AgentContext
5
- from openhands.sdk.conversation import Conversation, ConversationCallbackType
6
- from openhands.sdk.event import Event, EventBase, LLMConvertibleEvent
7
- from openhands.sdk.llm import (
8
- LLM,
9
- ImageContent,
10
- LLMRegistry,
11
- Message,
12
- RegistryEvent,
13
- TextContent,
14
- )
15
- from openhands.sdk.logger import get_logger
16
- from openhands.sdk.mcp import MCPClient, MCPTool, create_mcp_tools
17
- from openhands.sdk.tool import ActionBase, ObservationBase, Tool
18
-
19
-
20
- __version__ = "1.0.0a0"
21
-
22
- __all__ = [
23
- "LLM",
24
- "LLMRegistry",
25
- "RegistryEvent",
26
- "Message",
27
- "TextContent",
28
- "ImageContent",
29
- "Tool",
30
- "AgentBase",
31
- "Agent",
32
- "ActionBase",
33
- "ObservationBase",
34
- "MCPClient",
35
- "MCPTool",
36
- "create_mcp_tools",
37
- "get_logger",
38
- "Conversation",
39
- "ConversationCallbackType",
40
- "Event",
41
- "EventBase",
42
- "LLMConvertibleEvent",
43
- "AgentContext",
44
- "__version__",
45
- ]
@@ -1,8 +0,0 @@
1
- from openhands.sdk.agent.agent import Agent
2
- from openhands.sdk.agent.base import AgentBase
3
-
4
-
5
- __all__ = [
6
- "Agent",
7
- "AgentBase",
8
- ]