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
connectonion/trust.py ADDED
@@ -0,0 +1,166 @@
1
+ """
2
+ Purpose: Factory and coordinator for creating trust verification agents with policies
3
+ LLM-Note:
4
+ Dependencies: imports from [os, pathlib, typing, trust_agents.py, trust_functions.py] | imported by [agent.py] | tested by [tests/test_trust.py]
5
+ Data flow: receives trust param from Agent.__init__ → get_default_trust_level() checks CONNECTONION_ENV → create_trust_agent() validates trust param → returns Agent with trust verification tools OR None | trust param can be: None (no trust), str ("open"/"careful"/"strict" levels OR markdown policy OR file path), Path (markdown file), Agent (custom trust agent)
6
+ State/Effects: reads markdown files if Path/file path provided | checks os.environ for CONNECTONION_ENV and CONNECTONION_TRUST | creates Agent instances with trust_verification_tools from trust_functions.py | no writes or global state
7
+ Integration: exposes create_trust_agent(trust, api_key, model), get_default_trust_level(), validate_trust_level(level), TRUST_LEVELS constant | used by Agent.__init__ to create self.trust | trust agents get tools from get_trust_verification_tools() and prompts from get_trust_prompt()
8
+ Performance: lazy creation (only when trust param provided) | environment checks are fast | file I/O only for custom policies
9
+ Errors: raises TypeError for invalid trust type | raises ValueError for invalid trust level string | raises FileNotFoundError for missing policy files | heuristic detection for ambiguous strings (checks if file exists before treating as inline policy)
10
+ """
11
+
12
+ import os
13
+ from pathlib import Path
14
+ from typing import Union, Optional
15
+
16
+ from .trust_agents import TRUST_PROMPTS, get_trust_prompt
17
+ from .trust_functions import get_trust_verification_tools
18
+
19
+
20
+ # Trust level constants
21
+ TRUST_LEVELS = ["open", "careful", "strict"]
22
+
23
+
24
+ def get_default_trust_level() -> Optional[str]:
25
+ """
26
+ Get default trust level based on environment.
27
+
28
+ Returns:
29
+ Default trust level or None
30
+ """
31
+ env = os.environ.get('CONNECTONION_ENV', '').lower()
32
+
33
+ if env == 'development':
34
+ return 'open'
35
+ elif env == 'production':
36
+ return 'strict'
37
+ elif env in ['staging', 'test']:
38
+ return 'careful'
39
+
40
+ return None
41
+
42
+
43
+ def create_trust_agent(trust: Union[str, Path, 'Agent', None], api_key: Optional[str] = None, model: str = "gpt-5-mini") -> Optional['Agent']:
44
+ """
45
+ Create or return a trust agent based on the trust parameter.
46
+
47
+ Args:
48
+ trust: Can be:
49
+ - None: No trust agent (returns None)
50
+ - str: Trust level ("open", "careful", "strict") or markdown policy/file path
51
+ - Path: Path to markdown policy file
52
+ - Agent: Custom trust agent (returned as-is)
53
+
54
+ Returns:
55
+ An Agent configured for trust verification, or None
56
+
57
+ Raises:
58
+ TypeError: If trust is not a valid type
59
+ ValueError: If trust level is invalid
60
+ FileNotFoundError: If trust policy file doesn't exist
61
+ """
62
+ from .agent import Agent # Import here to avoid circular dependency
63
+
64
+ # If None, check for environment default
65
+ if trust is None:
66
+ env_trust = os.environ.get('CONNECTONION_TRUST')
67
+ if env_trust:
68
+ trust = env_trust
69
+ else:
70
+ return None # No trust agent
71
+
72
+ # If it's already an Agent, validate and return it
73
+ if isinstance(trust, Agent):
74
+ if not trust.tools:
75
+ raise ValueError("Trust agent must have verification tools")
76
+ return trust
77
+
78
+ # Get trust verification tools
79
+ trust_tools = get_trust_verification_tools()
80
+
81
+ # Handle Path object
82
+ if isinstance(trust, Path):
83
+ if not trust.exists():
84
+ raise FileNotFoundError(f"Trust policy file not found: {trust}")
85
+ policy = trust.read_text(encoding='utf-8')
86
+ return Agent(
87
+ name="trust_agent_custom",
88
+ tools=trust_tools,
89
+ system_prompt=policy,
90
+ api_key=api_key,
91
+ model=model
92
+ )
93
+
94
+ # Handle string parameter
95
+ if isinstance(trust, str):
96
+ trust_lower = trust.lower()
97
+
98
+ # Check if it's a trust level
99
+ if trust_lower in TRUST_LEVELS:
100
+ return Agent(
101
+ name=f"trust_agent_{trust_lower}",
102
+ tools=trust_tools,
103
+ system_prompt=get_trust_prompt(trust_lower),
104
+ api_key=api_key,
105
+ model=model
106
+ )
107
+
108
+ # Check if it looks like a trust level but isn't valid
109
+ # (single word without path separators or newlines)
110
+ if '\n' not in trust and '/' not in trust and '\\' not in trust and ' ' not in trust and len(trust) < 20:
111
+ # It's a single word, probably meant to be a trust level
112
+ if not os.path.exists(trust): # And it's not a file
113
+ raise ValueError(f"Invalid trust level: {trust}. Must be one of: {', '.join(TRUST_LEVELS)}")
114
+
115
+ # Check if it's a file path (contains path separators or ends with .md)
116
+ if '/' in trust or '\\' in trust or trust.endswith('.md'):
117
+ path = Path(trust)
118
+ if not path.exists():
119
+ raise FileNotFoundError(f"Trust policy file not found: {trust}")
120
+ policy = path.read_text(encoding='utf-8')
121
+ return Agent(
122
+ name="trust_agent_custom",
123
+ tools=trust_tools,
124
+ system_prompt=policy,
125
+ api_key=api_key,
126
+ model=model
127
+ )
128
+
129
+ # Check if it's a single-line string that could be a file
130
+ if '\n' not in trust and len(trust) < 100:
131
+ # Could be a file path without obvious markers
132
+ path = Path(trust)
133
+ if path.exists():
134
+ policy = path.read_text(encoding='utf-8')
135
+ return Agent(
136
+ name="trust_agent_custom",
137
+ tools=trust_tools,
138
+ system_prompt=policy,
139
+ api_key=api_key,
140
+ model=model
141
+ )
142
+
143
+ # It's an inline markdown policy
144
+ return Agent(
145
+ name="trust_agent_custom",
146
+ tools=trust_tools,
147
+ system_prompt=trust,
148
+ api_key=api_key,
149
+ model=model
150
+ )
151
+
152
+ # Invalid type
153
+ raise TypeError(f"Trust must be a string (level/policy/path), Path, Agent, or None, not {type(trust).__name__}")
154
+
155
+
156
+ def validate_trust_level(level: str) -> bool:
157
+ """
158
+ Check if a string is a valid trust level.
159
+
160
+ Args:
161
+ level: String to check
162
+
163
+ Returns:
164
+ True if valid trust level, False otherwise
165
+ """
166
+ return level.lower() in TRUST_LEVELS
@@ -0,0 +1,71 @@
1
+ """
2
+ Purpose: Provide pre-configured system prompts for trust agents at different security levels
3
+ LLM-Note:
4
+ Dependencies: no imports | imported by [trust.py] | tested by [tests/test_trust.py]
5
+ Data flow: trust.py calls get_trust_prompt(level) → validates level → returns TRUST_PROMPTS[level] string → used as system_prompt for trust Agent
6
+ State/Effects: no state or side effects | pure data module | TRUST_PROMPTS dict contains 3 prompts: "open" (dev - trust all), "careful" (staging - basic verification), "strict" (prod - whitelist only)
7
+ Integration: exposes TRUST_PROMPTS dict, get_trust_prompt(level), get_open_trust_prompt(), get_careful_trust_prompt(), get_strict_trust_prompt() | used by create_trust_agent() in trust.py to configure trust agent behavior
8
+ Performance: constant-time dict lookup | no computation
9
+ Errors: raises ValueError if level not in ["open", "careful", "strict"]
10
+ """
11
+
12
+ # Default trust prompts for each level
13
+ TRUST_PROMPTS = {
14
+ "open": """You are an open trust agent for development environments.
15
+ You trust all agents without verification to enable rapid development and testing.
16
+ When asked to verify an agent, always approve it.
17
+ Remember: This is only suitable for development, never for production.""",
18
+
19
+ "careful": """You are a careful trust agent for staging and testing environments.
20
+ You verify agents before trusting them by:
21
+ 1. Checking if they can perform their claimed functions
22
+ 2. Verifying they respond in reasonable time
23
+ 3. Looking for any obvious malicious behavior
24
+
25
+ Be thorough but not overly restrictive. Allow agents that pass basic verification.""",
26
+
27
+ "strict": """You are a strict trust agent for production environments.
28
+ You enforce maximum security by only trusting pre-approved agents.
29
+
30
+ Requirements for trust:
31
+ 1. Agent MUST be on the whitelist
32
+ 2. Agent MUST have valid credentials
33
+ 3. Agent MUST come from trusted domains
34
+ 4. Agent MUST pass all security checks
35
+
36
+ Reject any agent that doesn't meet ALL criteria. Security is the top priority."""
37
+ }
38
+
39
+
40
+ def get_open_trust_prompt() -> str:
41
+ """Get the prompt for an open trust agent (development)."""
42
+ return TRUST_PROMPTS["open"]
43
+
44
+
45
+ def get_careful_trust_prompt() -> str:
46
+ """Get the prompt for a careful trust agent (staging/testing)."""
47
+ return TRUST_PROMPTS["careful"]
48
+
49
+
50
+ def get_strict_trust_prompt() -> str:
51
+ """Get the prompt for a strict trust agent (production)."""
52
+ return TRUST_PROMPTS["strict"]
53
+
54
+
55
+ def get_trust_prompt(level: str) -> str:
56
+ """
57
+ Get the trust prompt for a given level.
58
+
59
+ Args:
60
+ level: Trust level ("open", "careful", or "strict")
61
+
62
+ Returns:
63
+ The trust prompt for that level
64
+
65
+ Raises:
66
+ ValueError: If level is not valid
67
+ """
68
+ level_lower = level.lower()
69
+ if level_lower not in TRUST_PROMPTS:
70
+ raise ValueError(f"Invalid trust level: {level}. Must be one of: {', '.join(TRUST_PROMPTS.keys())}")
71
+ return TRUST_PROMPTS[level_lower]
@@ -0,0 +1,88 @@
1
+ """
2
+ Purpose: Provide tool functions for trust agents to verify other agents
3
+ LLM-Note:
4
+ Dependencies: imports from [pathlib, typing] | imported by [trust.py] | tested by [tests/test_trust.py]
5
+ Data flow: create_trust_agent() calls get_trust_verification_tools() → returns list of [check_whitelist, test_capability, verify_agent] functions → these become tools for trust Agent → trust agent calls tools with agent_id → functions return descriptive strings → AI interprets results per trust policy
6
+ State/Effects: check_whitelist() reads ~/.connectonion/trusted.txt file if exists | supports wildcard patterns with * | test_capability() and verify_agent() are pure (no I/O, just return descriptive strings for AI)
7
+ Integration: exposes check_whitelist(agent_id), test_capability(agent_id, test, expected), verify_agent(agent_id, agent_info), get_trust_verification_tools() | used by trust.py to equip trust agents with verification capabilities
8
+ Performance: file read only for check_whitelist | simple string operations | no network calls
9
+ Errors: check_whitelist() catches file read exceptions and returns error string | missing whitelist file returns helpful message | no exceptions raised (errors returned as strings for AI interpretation)
10
+ """
11
+
12
+ from pathlib import Path
13
+ from typing import List, Callable
14
+
15
+
16
+ def check_whitelist(agent_id: str) -> str:
17
+ """
18
+ Check if an agent is on the whitelist.
19
+
20
+ Args:
21
+ agent_id: Identifier of the agent to check
22
+
23
+ Returns:
24
+ String indicating if agent is whitelisted or not
25
+ """
26
+ whitelist_path = Path.home() / ".connectonion" / "trusted.txt"
27
+ if whitelist_path.exists():
28
+ try:
29
+ whitelist = whitelist_path.read_text(encoding='utf-8')
30
+ lines = whitelist.strip().split('\n')
31
+ for line in lines:
32
+ line = line.strip()
33
+ if not line or line.startswith('#'):
34
+ continue
35
+ if line == agent_id:
36
+ return f"{agent_id} is on the whitelist"
37
+ # Simple wildcard support
38
+ if '*' in line:
39
+ pattern = line.replace('*', '')
40
+ if pattern in agent_id:
41
+ return f"{agent_id} matches whitelist pattern: {line}"
42
+ return f"{agent_id} is NOT on the whitelist"
43
+ except Exception as e:
44
+ return f"Error reading whitelist: {e}"
45
+ return "No whitelist file found at ~/.connectonion/trusted.txt"
46
+
47
+
48
+ def test_capability(agent_id: str, test: str, expected: str) -> str:
49
+ """
50
+ Test an agent's capability with a specific test.
51
+
52
+ Args:
53
+ agent_id: Identifier of the agent to test
54
+ test: The test to perform
55
+ expected: The expected result
56
+
57
+ Returns:
58
+ Test description for the trust agent to evaluate
59
+ """
60
+ return f"Testing {agent_id} with: {test}, expecting: {expected}"
61
+
62
+
63
+ def verify_agent(agent_id: str, agent_info: str = "") -> str:
64
+ """
65
+ General verification of an agent.
66
+
67
+ Args:
68
+ agent_id: Identifier of the agent
69
+ agent_info: Additional information about the agent
70
+
71
+ Returns:
72
+ Verification context for the trust agent
73
+ """
74
+ return f"Verifying agent: {agent_id}. Info: {agent_info}"
75
+
76
+
77
+ def get_trust_verification_tools() -> List[Callable]:
78
+ """
79
+ Get the list of trust verification tools.
80
+
81
+ Returns:
82
+ List of trust verification functions
83
+ """
84
+ return [
85
+ check_whitelist,
86
+ test_capability,
87
+ verify_agent
88
+ ]
@@ -0,0 +1,57 @@
1
+ """
2
+ Purpose: Terminal UI components for interactive agent interfaces with powerline-style rendering
3
+ LLM-Note:
4
+ Dependencies: imports from [input, dropdown, providers, keys, fuzzy, status_bar, divider, pick, footer] | imported by [useful_tools/__init__.py, useful_tools/terminal.py] | requires rich library
5
+ Data flow: agent/CLI imports TUI components → components render to terminal via Rich → user interacts via keyboard → components return selected values
6
+ State/Effects: manages terminal state during interaction | raw mode for keyboard input | restores terminal on exit
7
+ Integration: exposes Input (text with autocomplete), Dropdown, FileProvider/StaticProvider (autocomplete sources), StatusBar/SimpleStatusBar/ProgressSegment, Divider, pick (single-select), Footer | keyboard handling via getch/read_key | fuzzy matching via fuzzy_match/highlight_match
8
+ Performance: renders to terminal in real-time | fuzzy matching is O(n*m) | file provider reads filesystem
9
+ Errors: KeyboardInterrupt handled for clean exit | terminal restoration on exception
10
+
11
+ Powerline-style terminal components inspired by powerlevel10k.
12
+
13
+ Usage:
14
+ from connectonion.tui import Input, FileProvider, StatusBar, Divider
15
+
16
+ # Status bar with model/context/git info
17
+ status = StatusBar([
18
+ ("🤖", "co/gemini-2.5-pro", "magenta"),
19
+ ("📊", "50%", "green"),
20
+ ("", "main", "blue"),
21
+ ])
22
+ console.print(status.render())
23
+
24
+ # Minimal input with @ file autocomplete
25
+ text = Input(triggers={"@": FileProvider()}).run()
26
+
27
+ # Divider line
28
+ console.print(Divider().render())
29
+ """
30
+
31
+ from .input import Input
32
+ from .dropdown import Dropdown, DropdownItem
33
+ from .providers import FileProvider, StaticProvider
34
+ from .keys import getch, read_key
35
+ from .fuzzy import fuzzy_match, highlight_match
36
+ from .status_bar import StatusBar, SimpleStatusBar, ProgressSegment
37
+ from .divider import Divider
38
+ from .pick import pick
39
+ from .footer import Footer
40
+
41
+ __all__ = [
42
+ "Input",
43
+ "Dropdown",
44
+ "DropdownItem",
45
+ "FileProvider",
46
+ "StaticProvider",
47
+ "getch",
48
+ "read_key",
49
+ "fuzzy_match",
50
+ "highlight_match",
51
+ "StatusBar",
52
+ "SimpleStatusBar",
53
+ "ProgressSegment",
54
+ "Divider",
55
+ "pick",
56
+ "Footer",
57
+ ]
@@ -0,0 +1,39 @@
1
+ """Divider - Simple horizontal line separator.
2
+
3
+ A minimal line to separate sections in terminal UI.
4
+ """
5
+
6
+ from rich.text import Text
7
+
8
+
9
+ class Divider:
10
+ """Simple horizontal divider line.
11
+
12
+ Usage:
13
+ from connectonion.tui import Divider
14
+
15
+ divider = Divider()
16
+ console.print(divider.render())
17
+
18
+ # Custom width
19
+ divider = Divider(width=40)
20
+ console.print(divider.render())
21
+
22
+ Output:
23
+ ────────────────────────────────────
24
+ """
25
+
26
+ def __init__(self, width: int = 40, char: str = "─", style: str = "dim"):
27
+ """
28
+ Args:
29
+ width: Width of the divider line
30
+ char: Character to use for the line
31
+ style: Rich style for the line
32
+ """
33
+ self.width = width
34
+ self.char = char
35
+ self.style = style
36
+
37
+ def render(self) -> Text:
38
+ """Render the divider line."""
39
+ return Text(self.char * self.width, style=self.style)
@@ -0,0 +1,251 @@
1
+ """Dropdown - reusable selection list component.
2
+
3
+ Modern zsh-style dropdown with icons and highlighting.
4
+ """
5
+
6
+ from dataclasses import dataclass, field
7
+ from typing import Any
8
+
9
+ from rich.text import Text
10
+ from rich.table import Table
11
+ from rich.console import Group
12
+
13
+ from .fuzzy import highlight_match
14
+
15
+
16
+ @dataclass
17
+ class DropdownItem:
18
+ """Structured item for dropdown display.
19
+
20
+ Supports rich metadata for different display styles.
21
+
22
+ Usage:
23
+ # Simple item
24
+ item = DropdownItem(display="/today", value="/today")
25
+
26
+ # With description (shows on second line or right side)
27
+ item = DropdownItem(
28
+ display="/today",
29
+ value="/today",
30
+ description="Daily email briefing",
31
+ icon="📅"
32
+ )
33
+
34
+ # Contact style
35
+ item = DropdownItem(
36
+ display="Davis Baer",
37
+ value="davis@oneupapp.io",
38
+ description="davis@oneupapp.io",
39
+ subtitle="OneUp · founder",
40
+ icon="👤"
41
+ )
42
+ """
43
+ display: str # Main text to show
44
+ value: Any # Value to return when selected
45
+ score: int = 0 # Match score for sorting
46
+ positions: list[int] = field(default_factory=list) # Matched char positions
47
+ description: str = "" # Secondary text (right side or second line)
48
+ subtitle: str = "" # Third line or additional context
49
+ icon: str = "" # Left icon (emoji or nerd font)
50
+ style: str = "" # Rich style for the display text
51
+
52
+ @classmethod
53
+ def from_tuple(cls, item: tuple) -> "DropdownItem":
54
+ """Convert old tuple format to DropdownItem for backward compatibility.
55
+
56
+ Supports:
57
+ (display, value, score, positions)
58
+ (display, value, score, positions, metadata_dict)
59
+ """
60
+ if isinstance(item, DropdownItem):
61
+ return item
62
+
63
+ display, value, score, positions = item[:4]
64
+ metadata = item[4] if len(item) > 4 else {}
65
+
66
+ return cls(
67
+ display=display,
68
+ value=value,
69
+ score=score,
70
+ positions=positions or [],
71
+ description=metadata.get("description", ""),
72
+ subtitle=metadata.get("subtitle", ""),
73
+ icon=metadata.get("icon", ""),
74
+ style=metadata.get("style", ""),
75
+ )
76
+
77
+
78
+ # File type icons (requires Nerd Font for best results, fallback to unicode)
79
+ ICONS = {
80
+ "folder": "📁",
81
+ "python": "🐍",
82
+ "javascript": "📜",
83
+ "typescript": "📘",
84
+ "json": "📋",
85
+ "markdown": "📝",
86
+ "yaml": "⚙️",
87
+ "default": "📄",
88
+ }
89
+
90
+
91
+ def get_file_icon(name: str) -> str:
92
+ """Get icon for file based on extension."""
93
+ if name.endswith('/'):
94
+ return ICONS["folder"]
95
+ ext = name.rsplit('.', 1)[-1].lower() if '.' in name else ""
96
+ if ext == "py":
97
+ return ICONS["python"]
98
+ elif ext in ("js", "jsx"):
99
+ return ICONS["javascript"]
100
+ elif ext in ("ts", "tsx"):
101
+ return ICONS["typescript"]
102
+ elif ext == "json":
103
+ return ICONS["json"]
104
+ elif ext in ("md", "mdx"):
105
+ return ICONS["markdown"]
106
+ elif ext in ("yml", "yaml"):
107
+ return ICONS["yaml"]
108
+ return ICONS["default"]
109
+
110
+
111
+ class Dropdown:
112
+ """Dropdown selection list with keyboard navigation.
113
+
114
+ Modern zsh-style with icons and fuzzy match highlighting.
115
+ Supports DropdownItem for rich metadata display.
116
+
117
+ Usage:
118
+ dropdown = Dropdown(max_visible=8)
119
+
120
+ # Old tuple format (backward compatible)
121
+ dropdown.set_items([
122
+ ("agent.py", "agent.py", 10, [0,1,2]),
123
+ ("main.py", "main.py", 5, [0,1]),
124
+ ])
125
+
126
+ # New DropdownItem format
127
+ dropdown.set_items([
128
+ DropdownItem("/today", "/today", description="Daily briefing", icon="📅"),
129
+ DropdownItem("/inbox", "/inbox", description="Show emails", icon="📥"),
130
+ ])
131
+
132
+ dropdown.down() # Move selection
133
+ selected = dropdown.selected_value # Get current selection
134
+ """
135
+
136
+ def __init__(self, max_visible: int = 8, show_icons: bool = True):
137
+ self.max_visible = max_visible
138
+ self.show_icons = show_icons
139
+ self.items: list[DropdownItem] = []
140
+ self.selected_index = 0
141
+
142
+ def set_items(self, items: list):
143
+ """Set items. Accepts DropdownItem or old tuple format."""
144
+ converted = []
145
+ for item in items[:self.max_visible]:
146
+ if isinstance(item, DropdownItem):
147
+ converted.append(item)
148
+ else:
149
+ converted.append(DropdownItem.from_tuple(item))
150
+ self.items = converted
151
+ self.selected_index = 0
152
+
153
+ def clear(self):
154
+ """Clear all items."""
155
+ self.items = []
156
+ self.selected_index = 0
157
+
158
+ @property
159
+ def is_empty(self) -> bool:
160
+ return len(self.items) == 0
161
+
162
+ @property
163
+ def selected_value(self):
164
+ """Get currently selected value."""
165
+ if self.items and self.selected_index < len(self.items):
166
+ return self.items[self.selected_index].value
167
+ return None
168
+
169
+ @property
170
+ def selected_display(self) -> str:
171
+ """Get currently selected display text."""
172
+ if self.items and self.selected_index < len(self.items):
173
+ return self.items[self.selected_index].display
174
+ return ""
175
+
176
+ def up(self):
177
+ """Move selection up."""
178
+ if self.items:
179
+ self.selected_index = (self.selected_index - 1) % len(self.items)
180
+
181
+ def down(self):
182
+ """Move selection down."""
183
+ if self.items:
184
+ self.selected_index = (self.selected_index + 1) % len(self.items)
185
+
186
+ def _get_icon(self, item: DropdownItem) -> str:
187
+ """Get icon for item - use item.icon if set, otherwise infer from filename."""
188
+ if item.icon:
189
+ return item.icon
190
+ if self.show_icons:
191
+ return get_file_icon(item.display)
192
+ return ""
193
+
194
+ def render(self) -> Table:
195
+ """Render dropdown as Rich Table.
196
+
197
+ Supports multiple display modes based on DropdownItem fields:
198
+ - Simple: just display text with icon
199
+ - With description: display + description on right
200
+ - With subtitle: two-line display
201
+
202
+ Selected item has light background for visibility on both light/dark terminals.
203
+ """
204
+ table = Table(show_header=False, box=None, padding=(0, 0), show_edge=False)
205
+
206
+ for i, item in enumerate(self.items):
207
+ is_selected = i == self.selected_index
208
+ row = Text()
209
+
210
+ # Selection indicator
211
+ if is_selected:
212
+ row.append(" ❯ ", style="bold green")
213
+ else:
214
+ row.append(" ", style="dim")
215
+
216
+ # Icon
217
+ icon = self._get_icon(item)
218
+ if icon:
219
+ if is_selected:
220
+ row.append(f"{icon} ", style="bold")
221
+ else:
222
+ row.append(f"{icon} ", style="dim")
223
+
224
+ # Main display text with highlighting
225
+ display_style = item.style if item.style else ""
226
+ highlighted = highlight_match(item.display, item.positions)
227
+ if display_style:
228
+ highlighted.stylize(display_style)
229
+ row.append_text(highlighted)
230
+
231
+ # Description (right side, dimmed)
232
+ if item.description:
233
+ row.append(" ", style="dim")
234
+ row.append(item.description, style="dim italic")
235
+
236
+ # Add background to selected row
237
+ if is_selected:
238
+ row.stylize("on bright_black")
239
+
240
+ table.add_row(row)
241
+
242
+ # Subtitle as second line (only for selected or all items with subtitle)
243
+ if item.subtitle and is_selected:
244
+ subtitle_row = Text()
245
+ subtitle_row.append(" ", style="dim") # Indent to align with text
246
+ subtitle_row.append(item.subtitle, style="dim")
247
+ if is_selected:
248
+ subtitle_row.stylize("on bright_black")
249
+ table.add_row(subtitle_row)
250
+
251
+ return table