connectonion 0.5.8__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 (113) hide show
  1. connectonion/__init__.py +78 -0
  2. connectonion/address.py +320 -0
  3. connectonion/agent.py +450 -0
  4. connectonion/announce.py +84 -0
  5. connectonion/asgi.py +287 -0
  6. connectonion/auto_debug_exception.py +181 -0
  7. connectonion/cli/__init__.py +3 -0
  8. connectonion/cli/browser_agent/__init__.py +5 -0
  9. connectonion/cli/browser_agent/browser.py +243 -0
  10. connectonion/cli/browser_agent/prompt.md +107 -0
  11. connectonion/cli/commands/__init__.py +1 -0
  12. connectonion/cli/commands/auth_commands.py +527 -0
  13. connectonion/cli/commands/browser_commands.py +27 -0
  14. connectonion/cli/commands/create.py +511 -0
  15. connectonion/cli/commands/deploy_commands.py +220 -0
  16. connectonion/cli/commands/doctor_commands.py +173 -0
  17. connectonion/cli/commands/init.py +469 -0
  18. connectonion/cli/commands/project_cmd_lib.py +828 -0
  19. connectonion/cli/commands/reset_commands.py +149 -0
  20. connectonion/cli/commands/status_commands.py +168 -0
  21. connectonion/cli/docs/co-vibecoding-principles-docs-contexts-all-in-one.md +2010 -0
  22. connectonion/cli/docs/connectonion.md +1256 -0
  23. connectonion/cli/docs.md +123 -0
  24. connectonion/cli/main.py +148 -0
  25. connectonion/cli/templates/meta-agent/README.md +287 -0
  26. connectonion/cli/templates/meta-agent/agent.py +196 -0
  27. connectonion/cli/templates/meta-agent/prompts/answer_prompt.md +9 -0
  28. connectonion/cli/templates/meta-agent/prompts/docs_retrieve_prompt.md +15 -0
  29. connectonion/cli/templates/meta-agent/prompts/metagent.md +71 -0
  30. connectonion/cli/templates/meta-agent/prompts/think_prompt.md +18 -0
  31. connectonion/cli/templates/minimal/README.md +56 -0
  32. connectonion/cli/templates/minimal/agent.py +40 -0
  33. connectonion/cli/templates/playwright/README.md +118 -0
  34. connectonion/cli/templates/playwright/agent.py +336 -0
  35. connectonion/cli/templates/playwright/prompt.md +102 -0
  36. connectonion/cli/templates/playwright/requirements.txt +3 -0
  37. connectonion/cli/templates/web-research/agent.py +122 -0
  38. connectonion/connect.py +128 -0
  39. connectonion/console.py +539 -0
  40. connectonion/debug_agent/__init__.py +13 -0
  41. connectonion/debug_agent/agent.py +45 -0
  42. connectonion/debug_agent/prompts/debug_assistant.md +72 -0
  43. connectonion/debug_agent/runtime_inspector.py +406 -0
  44. connectonion/debug_explainer/__init__.py +10 -0
  45. connectonion/debug_explainer/explain_agent.py +114 -0
  46. connectonion/debug_explainer/explain_context.py +263 -0
  47. connectonion/debug_explainer/explainer_prompt.md +29 -0
  48. connectonion/debug_explainer/root_cause_analysis_prompt.md +43 -0
  49. connectonion/debugger_ui.py +1039 -0
  50. connectonion/decorators.py +208 -0
  51. connectonion/events.py +248 -0
  52. connectonion/execution_analyzer/__init__.py +9 -0
  53. connectonion/execution_analyzer/execution_analysis.py +93 -0
  54. connectonion/execution_analyzer/execution_analysis_prompt.md +47 -0
  55. connectonion/host.py +579 -0
  56. connectonion/interactive_debugger.py +342 -0
  57. connectonion/llm.py +801 -0
  58. connectonion/llm_do.py +307 -0
  59. connectonion/logger.py +300 -0
  60. connectonion/prompt_files/__init__.py +1 -0
  61. connectonion/prompt_files/analyze_contact.md +62 -0
  62. connectonion/prompt_files/eval_expected.md +12 -0
  63. connectonion/prompt_files/react_evaluate.md +11 -0
  64. connectonion/prompt_files/react_plan.md +16 -0
  65. connectonion/prompt_files/reflect.md +22 -0
  66. connectonion/prompts.py +144 -0
  67. connectonion/relay.py +200 -0
  68. connectonion/static/docs.html +688 -0
  69. connectonion/tool_executor.py +279 -0
  70. connectonion/tool_factory.py +186 -0
  71. connectonion/tool_registry.py +105 -0
  72. connectonion/trust.py +166 -0
  73. connectonion/trust_agents.py +71 -0
  74. connectonion/trust_functions.py +88 -0
  75. connectonion/tui/__init__.py +57 -0
  76. connectonion/tui/divider.py +39 -0
  77. connectonion/tui/dropdown.py +251 -0
  78. connectonion/tui/footer.py +31 -0
  79. connectonion/tui/fuzzy.py +56 -0
  80. connectonion/tui/input.py +278 -0
  81. connectonion/tui/keys.py +35 -0
  82. connectonion/tui/pick.py +130 -0
  83. connectonion/tui/providers.py +155 -0
  84. connectonion/tui/status_bar.py +163 -0
  85. connectonion/usage.py +161 -0
  86. connectonion/useful_events_handlers/__init__.py +16 -0
  87. connectonion/useful_events_handlers/reflect.py +116 -0
  88. connectonion/useful_plugins/__init__.py +20 -0
  89. connectonion/useful_plugins/calendar_plugin.py +163 -0
  90. connectonion/useful_plugins/eval.py +139 -0
  91. connectonion/useful_plugins/gmail_plugin.py +162 -0
  92. connectonion/useful_plugins/image_result_formatter.py +127 -0
  93. connectonion/useful_plugins/re_act.py +78 -0
  94. connectonion/useful_plugins/shell_approval.py +159 -0
  95. connectonion/useful_tools/__init__.py +44 -0
  96. connectonion/useful_tools/diff_writer.py +192 -0
  97. connectonion/useful_tools/get_emails.py +183 -0
  98. connectonion/useful_tools/gmail.py +1596 -0
  99. connectonion/useful_tools/google_calendar.py +613 -0
  100. connectonion/useful_tools/memory.py +380 -0
  101. connectonion/useful_tools/microsoft_calendar.py +604 -0
  102. connectonion/useful_tools/outlook.py +488 -0
  103. connectonion/useful_tools/send_email.py +205 -0
  104. connectonion/useful_tools/shell.py +97 -0
  105. connectonion/useful_tools/slash_command.py +201 -0
  106. connectonion/useful_tools/terminal.py +285 -0
  107. connectonion/useful_tools/todo_list.py +241 -0
  108. connectonion/useful_tools/web_fetch.py +216 -0
  109. connectonion/xray.py +467 -0
  110. connectonion-0.5.8.dist-info/METADATA +741 -0
  111. connectonion-0.5.8.dist-info/RECORD +113 -0
  112. connectonion-0.5.8.dist-info/WHEEL +4 -0
  113. connectonion-0.5.8.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,97 @@
1
+ """
2
+ Purpose: Shell command execution tool for running terminal commands from agent context
3
+ LLM-Note:
4
+ Dependencies: imports from [subprocess] | imported by [useful_tools/__init__.py] | tested by [tests/unit/test_shell_tool.py]
5
+ Data flow: Agent calls Shell.run(command) → subprocess.run() executes in shell → captures stdout+stderr → returns combined output string
6
+ State/Effects: executes shell commands on host system | can modify filesystem, install packages, run programs | uses working directory specified in constructor | no persistent state
7
+ Integration: exposes Shell class with run(command), run_in_dir(command, directory) | used as agent tool via Agent(tools=[Shell()])
8
+ Performance: process spawn overhead per command | command execution time varies | no caching
9
+ Errors: returns error message if command fails | captures stderr in output | no exceptions raised (returns error strings)
10
+
11
+ Shell tool for executing terminal commands.
12
+
13
+ Usage:
14
+ from connectonion import Agent, Shell
15
+
16
+ shell = Shell()
17
+ agent = Agent("coder", tools=[shell])
18
+
19
+ # Agent can now use:
20
+ # - run(command) - Execute shell command, returns output
21
+ # - run_in_dir(command, directory) - Execute in specific directory
22
+ """
23
+
24
+ import subprocess
25
+
26
+
27
+ class Shell:
28
+ """Shell command execution tool."""
29
+
30
+ def __init__(self, cwd: str = "."):
31
+ """Initialize Shell tool.
32
+
33
+ Args:
34
+ cwd: Default working directory
35
+ """
36
+ self.cwd = cwd
37
+
38
+ def run(self, command: str) -> str:
39
+ """Execute a shell command, returns output.
40
+
41
+ Args:
42
+ command: Shell command to execute (e.g., "ls -la", "git status")
43
+
44
+ Returns:
45
+ Command output (stdout + stderr)
46
+ """
47
+ result = subprocess.run(
48
+ command,
49
+ shell=True,
50
+ capture_output=True,
51
+ text=True,
52
+ cwd=self.cwd
53
+ )
54
+
55
+ parts = []
56
+ if result.stdout:
57
+ parts.append(result.stdout.rstrip())
58
+ if result.stderr:
59
+ parts.append(f"STDERR:\n{result.stderr.rstrip()}")
60
+ if result.returncode != 0:
61
+ parts.append(f"\nExit code: {result.returncode}")
62
+
63
+ output = "\n".join(parts) if parts else "(no output)"
64
+ if len(output) > 1000:
65
+ output = output[:1000] + f"\n... (truncated, {len(output):,} total chars)"
66
+ return output
67
+
68
+ def run_in_dir(self, command: str, directory: str) -> str:
69
+ """Execute command in a specific directory.
70
+
71
+ Args:
72
+ command: Shell command to execute
73
+ directory: Directory to run the command in
74
+
75
+ Returns:
76
+ Command output (stdout + stderr)
77
+ """
78
+ result = subprocess.run(
79
+ command,
80
+ shell=True,
81
+ capture_output=True,
82
+ text=True,
83
+ cwd=directory
84
+ )
85
+
86
+ parts = []
87
+ if result.stdout:
88
+ parts.append(result.stdout.rstrip())
89
+ if result.stderr:
90
+ parts.append(f"STDERR:\n{result.stderr.rstrip()}")
91
+ if result.returncode != 0:
92
+ parts.append(f"\nExit code: {result.returncode}")
93
+
94
+ output = "\n".join(parts) if parts else "(no output)"
95
+ if len(output) > 1000:
96
+ output = output[:1000] + f"\n... (truncated, {len(output):,} total chars)"
97
+ return output
@@ -0,0 +1,201 @@
1
+ """
2
+ Purpose: Load and execute slash commands from markdown files with YAML frontmatter
3
+ LLM-Note:
4
+ Dependencies: imports from [yaml, pathlib, typing] | imported by [useful_tools/__init__.py] | tested by [tests/unit/test_slash_command.py]
5
+ Data flow: SlashCommand.load(name) → searches .co/commands/*.md and commands/*.md → parses YAML frontmatter for metadata → extracts prompt body → SlashCommand.filter_tools() filters available tools → returns filtered tool list
6
+ State/Effects: reads markdown files from filesystem | no persistent state | no network I/O | command files can specify tool restrictions
7
+ Integration: exposes SlashCommand class with load(name), list_all(), filter_tools(tools) | command format: YAML frontmatter (name, description, tools) + markdown prompt body | used by CLI and agent for custom commands
8
+ Performance: file I/O per load | YAML parsing is fast | list_all() scans directories once
9
+ Errors: returns None if command not found | raises ValueError for invalid YAML | no other exceptions
10
+
11
+ SlashCommand - Load and execute slash commands from markdown files.
12
+
13
+ Usage:
14
+ from connectonion import SlashCommand
15
+
16
+ # Load command
17
+ cmd = SlashCommand.load("today")
18
+
19
+ # Get prompt and filter tools
20
+ prompt = cmd.prompt
21
+ filtered_tools = cmd.filter_tools(all_tools)
22
+
23
+ # List all commands
24
+ commands = SlashCommand.list_all()
25
+
26
+ Command file format (.co/commands/*.md or commands/*.md):
27
+ ---
28
+ name: today
29
+ description: Daily email briefing
30
+ tools:
31
+ - Gmail.search_emails # Specific method
32
+ - WebFetch # Whole class
33
+ - my_function # Standalone function
34
+ ---
35
+ Your command prompt here...
36
+ """
37
+
38
+ import yaml
39
+ from pathlib import Path
40
+ from typing import Dict, Optional, List
41
+
42
+
43
+ class SlashCommand:
44
+ """Represents a slash command loaded from a markdown file."""
45
+
46
+ def __init__(self, name: str, description: str, prompt: str, tools: Optional[List[str]] = None, is_custom: bool = False):
47
+ """Initialize a SlashCommand.
48
+
49
+ Args:
50
+ name: Command name
51
+ description: Command description
52
+ prompt: Command prompt template
53
+ tools: List of allowed tools (None = all tools allowed)
54
+ is_custom: Whether this is a user custom command
55
+ """
56
+ self.name = name
57
+ self.description = description
58
+ self.prompt = prompt
59
+ self.tools = tools
60
+ self.is_custom = is_custom
61
+
62
+ @classmethod
63
+ def load(cls, command_name: str) -> Optional["SlashCommand"]:
64
+ """Load command from .co/commands/ (user) or commands/ (built-in).
65
+
66
+ Args:
67
+ command_name: Name of the command (without .md extension)
68
+
69
+ Returns:
70
+ SlashCommand instance or None if not found
71
+ """
72
+ # Check user custom first
73
+ custom_path = Path(".co/commands") / f"{command_name}.md"
74
+ if custom_path.exists():
75
+ return cls._parse_file(custom_path, is_custom=True)
76
+
77
+ # Check built-in
78
+ builtin_path = Path("commands") / f"{command_name}.md"
79
+ if builtin_path.exists():
80
+ return cls._parse_file(builtin_path, is_custom=False)
81
+
82
+ return None
83
+
84
+ @classmethod
85
+ def _parse_file(cls, filepath: Path, is_custom: bool = False) -> "SlashCommand":
86
+ """Parse markdown file with YAML frontmatter.
87
+
88
+ Args:
89
+ filepath: Path to .md file
90
+ is_custom: Whether this is a user custom command
91
+
92
+ Returns:
93
+ SlashCommand instance
94
+
95
+ Raises:
96
+ yaml.YAMLError: If frontmatter is invalid
97
+ ValueError: If required fields missing
98
+ """
99
+ content = filepath.read_text()
100
+
101
+ # Split frontmatter and prompt
102
+ if not content.startswith("---"):
103
+ raise ValueError(f"Command file {filepath} missing YAML frontmatter")
104
+
105
+ parts = content.split("---", 2)
106
+ if len(parts) < 3:
107
+ raise ValueError(f"Command file {filepath} has malformed frontmatter")
108
+
109
+ # Parse YAML (let it raise naturally if invalid)
110
+ frontmatter = yaml.safe_load(parts[1])
111
+ prompt = parts[2].strip()
112
+
113
+ # Validate required fields
114
+ if "name" not in frontmatter:
115
+ raise ValueError(f"Command file {filepath} missing 'name' field")
116
+ if "description" not in frontmatter:
117
+ raise ValueError(f"Command file {filepath} missing 'description' field")
118
+
119
+ return cls(
120
+ name=frontmatter["name"],
121
+ description=frontmatter["description"],
122
+ prompt=prompt,
123
+ tools=frontmatter.get("tools"),
124
+ is_custom=is_custom
125
+ )
126
+
127
+ def filter_tools(self, available_tools: List) -> List:
128
+ """Filter tools based on allowed list from command file.
129
+
130
+ Args:
131
+ available_tools: List of all available tool instances or functions
132
+
133
+ Returns:
134
+ Filtered list of tools
135
+
136
+ Example:
137
+ tools = ["Gmail.search_emails", "Agent.input"]
138
+ # Only allows Gmail.search_emails and Agent.input methods
139
+ """
140
+ if self.tools is None:
141
+ return available_tools
142
+
143
+ filtered = []
144
+
145
+ for tool in available_tools:
146
+ tool_name = getattr(tool, '__name__', None)
147
+
148
+ # Handle class-based tools (have __self__ attribute)
149
+ if hasattr(tool, '__self__'):
150
+ class_name = tool.__self__.__class__.__name__
151
+ method_name = tool.__name__
152
+ full_name = f"{class_name}.{method_name}"
153
+
154
+ # Check if specific method or whole class is allowed
155
+ if full_name in self.tools or class_name in self.tools:
156
+ filtered.append(tool)
157
+
158
+ # Handle function-based tools
159
+ elif tool_name and tool_name in self.tools:
160
+ filtered.append(tool)
161
+
162
+ return filtered
163
+
164
+ @classmethod
165
+ def list_all(cls) -> Dict[str, "SlashCommand"]:
166
+ """List all available commands (built-in and custom).
167
+
168
+ Returns:
169
+ Dict mapping command names to SlashCommand instances
170
+ Custom commands override built-ins
171
+ """
172
+ commands = {}
173
+
174
+ # Load built-ins first
175
+ builtin_dir = Path("commands")
176
+ if builtin_dir.exists():
177
+ for filepath in builtin_dir.glob("*.md"):
178
+ cmd = cls._parse_file(filepath, is_custom=False)
179
+ commands[cmd.name] = cmd
180
+
181
+ # Load customs (override built-ins)
182
+ custom_dir = Path(".co/commands")
183
+ if custom_dir.exists():
184
+ for filepath in custom_dir.glob("*.md"):
185
+ cmd = cls._parse_file(filepath, is_custom=True)
186
+ commands[cmd.name] = cmd
187
+
188
+ return commands
189
+
190
+ @classmethod
191
+ def is_custom(cls, command_name: str) -> bool:
192
+ """Check if command is a custom override.
193
+
194
+ Args:
195
+ command_name: Name of the command
196
+
197
+ Returns:
198
+ True if custom command exists in .co/commands/
199
+ """
200
+ custom_path = Path(".co/commands") / f"{command_name}.md"
201
+ return custom_path.exists()
@@ -0,0 +1,285 @@
1
+ """
2
+ Purpose: CLI input utilities for single keypress selection, file browsing, and @ autocomplete
3
+ LLM-Note:
4
+ Dependencies: imports from [sys, typing, rich.console, termios/tty (Unix), msvcrt (Windows)] | imported by [useful_tools/__init__.py, tui/__init__.py] | tested by [tests/unit/test_terminal.py]
5
+ Data flow: pick(prompt, options) → displays numbered options → _getch()/_read_key() waits for keypress → returns selected option text or key | browse_files() → displays directory listing with rich.live → arrow keys navigate → returns selected file path | input_with_at() → standard input with @ triggering file browser
6
+ State/Effects: blocks on user input | uses raw terminal mode via termios/tty (Unix) or msvcrt (Windows) | no persistent state | no network I/O
7
+ Integration: exposes pick(), yes_no(), browse_files(), input_with_at(), autocomplete() | pick() accepts list (numbered) or dict (custom keys) | used for human-in-the-loop agent tools
8
+ Performance: instant response after keypress | no computation overhead | blocks on user input
9
+ Errors: returns None if user cancels | handles keyboard interrupts gracefully | no exceptions raised
10
+
11
+ CLI input utilities - single keypress select, file browser, and @ autocomplete.
12
+
13
+ Usage:
14
+ from connectonion import pick, yes_no, browse_files, input_with_at
15
+
16
+ # Recommended: Numbered options (1, 2, 3) for agent interactions
17
+ choice = pick("Apply this command?", [
18
+ "Yes, apply",
19
+ "Yes for same command",
20
+ "No, I'll tell agent how to do it"
21
+ ])
22
+ # Press 1 → "Yes, apply", 2 → "Yes for same command", 3 → "No, I'll tell agent how to do it"
23
+ # Or use arrow keys + Enter
24
+
25
+ # Pick from list (returns option text)
26
+ choice = pick("Pick a color", ["Red", "Green", "Blue"])
27
+ # Press 1 → "Red", 2 → "Green", 3 → "Blue"
28
+
29
+ # Pick with custom keys (returns key)
30
+ choice = pick("Continue?", {
31
+ "y": "Yes, continue",
32
+ "n": "No, cancel",
33
+ })
34
+ # Press y → "y", n → "n"
35
+
36
+ # Yes/No confirmation (simple binary choice)
37
+ ok = yes_no("Are you sure?")
38
+ # Press y → True, n → False
39
+
40
+ # Browse files and folders
41
+ path = browse_files()
42
+ # Navigate with arrow keys, Enter on folders to open, Enter on files to select
43
+ # Returns: "src/agent.py"
44
+
45
+ # Input with @ autocomplete
46
+ cmd = input_with_at("> ")
47
+ # User types: "edit @"
48
+ # File browser opens automatically
49
+ # Returns: "edit src/agent.py"
50
+ """
51
+
52
+ import sys
53
+ from typing import Union
54
+
55
+ from rich.console import Console
56
+
57
+
58
+ def _getch():
59
+ """Read a single character from stdin without waiting for Enter."""
60
+ try:
61
+ import termios
62
+ import tty
63
+ fd = sys.stdin.fileno()
64
+ old_settings = termios.tcgetattr(fd)
65
+ try:
66
+ tty.setraw(fd)
67
+ ch = sys.stdin.read(1)
68
+ finally:
69
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
70
+ return ch
71
+ except ImportError:
72
+ import msvcrt
73
+ return msvcrt.getch().decode('utf-8', errors='ignore')
74
+
75
+
76
+ def _read_key():
77
+ """Read a key, handling arrow key escape sequences."""
78
+ ch = _getch()
79
+ if ch == '\x1b': # Escape sequence
80
+ ch2 = _getch()
81
+ if ch2 == '[':
82
+ ch3 = _getch()
83
+ if ch3 == 'A':
84
+ return 'up'
85
+ elif ch3 == 'B':
86
+ return 'down'
87
+ elif ch3 == 'C':
88
+ return 'right'
89
+ elif ch3 == 'D':
90
+ return 'left'
91
+ return ch
92
+
93
+
94
+ def pick(
95
+ title: str,
96
+ options: Union[list, dict],
97
+ console: Console = None,
98
+ ) -> str:
99
+ """Pick one option - arrow keys or number/letter selection.
100
+
101
+ Args:
102
+ title: The question/title to display
103
+ options: Either a list (numbered 1,2,3...) or dict (custom keys)
104
+ console: Optional Rich console instance
105
+
106
+ Returns:
107
+ If list: the selected option text
108
+ If dict: the selected key
109
+
110
+ Example:
111
+ choice = pick("Pick one", ["Apple", "Banana"])
112
+ # Press 2 or arrow down + Enter → "Banana"
113
+
114
+ action = pick("Continue?", {"y": "Yes", "n": "No"})
115
+ # Press y → "y"
116
+ """
117
+ if console is None:
118
+ console = Console()
119
+
120
+ # Build key map and items list
121
+ if isinstance(options, list):
122
+ key_map = {}
123
+ items = []
124
+ for i, opt in enumerate(options, 1):
125
+ key = str(i)
126
+ key_map[key] = opt
127
+ items.append((key, opt))
128
+ else:
129
+ key_map = options
130
+ items = list(options.items())
131
+
132
+ selected = 0 # Current selection index
133
+
134
+ def render(first=False):
135
+ """Render the menu with current selection highlighted."""
136
+ if not first:
137
+ # Move cursor up to redraw
138
+ lines_to_clear = len(items) + (2 if title else 1)
139
+ sys.stdout.write(f"\033[{lines_to_clear}A") # Move up
140
+ sys.stdout.write("\033[J") # Clear from cursor to end
141
+
142
+ if title:
143
+ console.print(f"[bold]{title}[/]")
144
+ console.print()
145
+
146
+ for i, (key, desc) in enumerate(items):
147
+ if i == selected:
148
+ console.print(f" [bold cyan]❯[/] [bold cyan]{key}[/] [bold]{desc}[/]")
149
+ else:
150
+ console.print(f" [dim]{key}[/] {desc}")
151
+
152
+ # Initial render
153
+ render(first=True)
154
+
155
+ # Hide cursor
156
+ print("\033[?25l", end="", flush=True)
157
+
158
+ try:
159
+ while True:
160
+ key = _read_key()
161
+
162
+ if key == 'up':
163
+ selected = (selected - 1) % len(items)
164
+ render(first=False)
165
+ elif key == 'down':
166
+ selected = (selected + 1) % len(items)
167
+ render(first=False)
168
+ elif key in ('\r', '\n'): # Enter
169
+ print("\033[?25h", end="", flush=True)
170
+ chosen_key = items[selected][0]
171
+ if isinstance(options, list):
172
+ return key_map[chosen_key]
173
+ return chosen_key
174
+ elif key in key_map:
175
+ print("\033[?25h", end="", flush=True)
176
+ if isinstance(options, list):
177
+ return key_map[key]
178
+ return key
179
+ elif key in ("\x03", "\x04"): # Ctrl+C, Ctrl+D
180
+ print("\033[?25h", end="", flush=True)
181
+ raise KeyboardInterrupt()
182
+ finally:
183
+ print("\033[?25h", end="", flush=True)
184
+
185
+
186
+ def yes_no(
187
+ message: str,
188
+ default: bool = True,
189
+ console: Console = None,
190
+ ) -> bool:
191
+ """Yes/No confirmation - single keypress.
192
+
193
+ Args:
194
+ message: The question to ask
195
+ default: Default value if Enter is pressed
196
+ console: Optional Rich console instance
197
+
198
+ Returns:
199
+ True for yes, False for no
200
+ """
201
+ if console is None:
202
+ console = Console()
203
+
204
+ yes_key = "Y" if default else "y"
205
+ no_key = "n" if default else "N"
206
+
207
+ console.print()
208
+ console.print(f"[bold]{message}[/] [dim]({yes_key}/{no_key})[/] ", end="")
209
+
210
+ while True:
211
+ ch = _getch().lower()
212
+ if ch == "y":
213
+ console.print("yes")
214
+ return True
215
+ elif ch == "n":
216
+ console.print("no")
217
+ return False
218
+ elif ch in ("\r", "\n"): # Enter
219
+ console.print("yes" if default else "no")
220
+ return default
221
+ elif ch in ("\x03", "\x04"): # Ctrl+C, Ctrl+D
222
+ raise KeyboardInterrupt()
223
+
224
+
225
+ def autocomplete(suggestions: list, max_visible: int = 5) -> Union[str, None]:
226
+ """Show inline autocomplete dropdown with arrow key navigation.
227
+
228
+ Pure UI component - displays suggestions and handles selection.
229
+ Does NOT handle filtering or search logic - that's the caller's job.
230
+
231
+ Args:
232
+ suggestions: List of strings to display
233
+ max_visible: Maximum suggestions to show (default: 5)
234
+
235
+ Returns:
236
+ Selected string or None if cancelled (ESC pressed)
237
+
238
+ Example:
239
+ from connectonion import autocomplete
240
+
241
+ files = ["agent.py", "main.py", "utils.py"]
242
+ selected = autocomplete(files)
243
+ if selected:
244
+ print(f"Selected: {selected}")
245
+ """
246
+ from rich.live import Live
247
+ from rich.table import Table
248
+
249
+ if not suggestions:
250
+ return None
251
+
252
+ # Limit to max visible
253
+ suggestions = suggestions[:max_visible]
254
+ selected = 0
255
+
256
+ def create_table():
257
+ """Create table with current selection highlighted."""
258
+ table = Table(show_header=False, box=None, padding=(0, 1), show_edge=False)
259
+ for i, item in enumerate(suggestions):
260
+ if i == selected:
261
+ table.add_row(f"[cyan]❯ {item}[/]")
262
+ else:
263
+ table.add_row(f"[dim] {item}[/]")
264
+ return table
265
+
266
+ # Show live display with keyboard navigation
267
+ with Live(create_table(), refresh_per_second=10, auto_refresh=False) as live:
268
+ while True:
269
+ key = _read_key()
270
+
271
+ if key == '\x1b': # ESC - cancel
272
+ return None
273
+
274
+ elif key in ('\r', '\n', '\t'): # Enter/Tab - accept
275
+ return suggestions[selected]
276
+
277
+ elif key == 'up':
278
+ selected = (selected - 1) % len(suggestions)
279
+ live.update(create_table(), refresh=True)
280
+
281
+ elif key == 'down':
282
+ selected = (selected + 1) % len(suggestions)
283
+ live.update(create_table(), refresh=True)
284
+
285
+