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.
- connectonion/__init__.py +78 -0
- connectonion/address.py +320 -0
- connectonion/agent.py +450 -0
- connectonion/announce.py +84 -0
- connectonion/asgi.py +287 -0
- connectonion/auto_debug_exception.py +181 -0
- connectonion/cli/__init__.py +3 -0
- connectonion/cli/browser_agent/__init__.py +5 -0
- connectonion/cli/browser_agent/browser.py +243 -0
- connectonion/cli/browser_agent/prompt.md +107 -0
- connectonion/cli/commands/__init__.py +1 -0
- connectonion/cli/commands/auth_commands.py +527 -0
- connectonion/cli/commands/browser_commands.py +27 -0
- connectonion/cli/commands/create.py +511 -0
- connectonion/cli/commands/deploy_commands.py +220 -0
- connectonion/cli/commands/doctor_commands.py +173 -0
- connectonion/cli/commands/init.py +469 -0
- connectonion/cli/commands/project_cmd_lib.py +828 -0
- connectonion/cli/commands/reset_commands.py +149 -0
- connectonion/cli/commands/status_commands.py +168 -0
- connectonion/cli/docs/co-vibecoding-principles-docs-contexts-all-in-one.md +2010 -0
- connectonion/cli/docs/connectonion.md +1256 -0
- connectonion/cli/docs.md +123 -0
- connectonion/cli/main.py +148 -0
- connectonion/cli/templates/meta-agent/README.md +287 -0
- connectonion/cli/templates/meta-agent/agent.py +196 -0
- connectonion/cli/templates/meta-agent/prompts/answer_prompt.md +9 -0
- connectonion/cli/templates/meta-agent/prompts/docs_retrieve_prompt.md +15 -0
- connectonion/cli/templates/meta-agent/prompts/metagent.md +71 -0
- connectonion/cli/templates/meta-agent/prompts/think_prompt.md +18 -0
- connectonion/cli/templates/minimal/README.md +56 -0
- connectonion/cli/templates/minimal/agent.py +40 -0
- connectonion/cli/templates/playwright/README.md +118 -0
- connectonion/cli/templates/playwright/agent.py +336 -0
- connectonion/cli/templates/playwright/prompt.md +102 -0
- connectonion/cli/templates/playwright/requirements.txt +3 -0
- connectonion/cli/templates/web-research/agent.py +122 -0
- connectonion/connect.py +128 -0
- connectonion/console.py +539 -0
- connectonion/debug_agent/__init__.py +13 -0
- connectonion/debug_agent/agent.py +45 -0
- connectonion/debug_agent/prompts/debug_assistant.md +72 -0
- connectonion/debug_agent/runtime_inspector.py +406 -0
- connectonion/debug_explainer/__init__.py +10 -0
- connectonion/debug_explainer/explain_agent.py +114 -0
- connectonion/debug_explainer/explain_context.py +263 -0
- connectonion/debug_explainer/explainer_prompt.md +29 -0
- connectonion/debug_explainer/root_cause_analysis_prompt.md +43 -0
- connectonion/debugger_ui.py +1039 -0
- connectonion/decorators.py +208 -0
- connectonion/events.py +248 -0
- connectonion/execution_analyzer/__init__.py +9 -0
- connectonion/execution_analyzer/execution_analysis.py +93 -0
- connectonion/execution_analyzer/execution_analysis_prompt.md +47 -0
- connectonion/host.py +579 -0
- connectonion/interactive_debugger.py +342 -0
- connectonion/llm.py +801 -0
- connectonion/llm_do.py +307 -0
- connectonion/logger.py +300 -0
- connectonion/prompt_files/__init__.py +1 -0
- connectonion/prompt_files/analyze_contact.md +62 -0
- connectonion/prompt_files/eval_expected.md +12 -0
- connectonion/prompt_files/react_evaluate.md +11 -0
- connectonion/prompt_files/react_plan.md +16 -0
- connectonion/prompt_files/reflect.md +22 -0
- connectonion/prompts.py +144 -0
- connectonion/relay.py +200 -0
- connectonion/static/docs.html +688 -0
- connectonion/tool_executor.py +279 -0
- connectonion/tool_factory.py +186 -0
- connectonion/tool_registry.py +105 -0
- connectonion/trust.py +166 -0
- connectonion/trust_agents.py +71 -0
- connectonion/trust_functions.py +88 -0
- connectonion/tui/__init__.py +57 -0
- connectonion/tui/divider.py +39 -0
- connectonion/tui/dropdown.py +251 -0
- connectonion/tui/footer.py +31 -0
- connectonion/tui/fuzzy.py +56 -0
- connectonion/tui/input.py +278 -0
- connectonion/tui/keys.py +35 -0
- connectonion/tui/pick.py +130 -0
- connectonion/tui/providers.py +155 -0
- connectonion/tui/status_bar.py +163 -0
- connectonion/usage.py +161 -0
- connectonion/useful_events_handlers/__init__.py +16 -0
- connectonion/useful_events_handlers/reflect.py +116 -0
- connectonion/useful_plugins/__init__.py +20 -0
- connectonion/useful_plugins/calendar_plugin.py +163 -0
- connectonion/useful_plugins/eval.py +139 -0
- connectonion/useful_plugins/gmail_plugin.py +162 -0
- connectonion/useful_plugins/image_result_formatter.py +127 -0
- connectonion/useful_plugins/re_act.py +78 -0
- connectonion/useful_plugins/shell_approval.py +159 -0
- connectonion/useful_tools/__init__.py +44 -0
- connectonion/useful_tools/diff_writer.py +192 -0
- connectonion/useful_tools/get_emails.py +183 -0
- connectonion/useful_tools/gmail.py +1596 -0
- connectonion/useful_tools/google_calendar.py +613 -0
- connectonion/useful_tools/memory.py +380 -0
- connectonion/useful_tools/microsoft_calendar.py +604 -0
- connectonion/useful_tools/outlook.py +488 -0
- connectonion/useful_tools/send_email.py +205 -0
- connectonion/useful_tools/shell.py +97 -0
- connectonion/useful_tools/slash_command.py +201 -0
- connectonion/useful_tools/terminal.py +285 -0
- connectonion/useful_tools/todo_list.py +241 -0
- connectonion/useful_tools/web_fetch.py +216 -0
- connectonion/xray.py +467 -0
- connectonion-0.5.8.dist-info/METADATA +741 -0
- connectonion-0.5.8.dist-info/RECORD +113 -0
- connectonion-0.5.8.dist-info/WHEEL +4 -0
- 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
|
+
|