kollabor 0.4.9__py3-none-any.whl → 0.4.15__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.
- agents/__init__.py +2 -0
- agents/coder/__init__.py +0 -0
- agents/coder/agent.json +4 -0
- agents/coder/api-integration.md +2150 -0
- agents/coder/cli-pretty.md +765 -0
- agents/coder/code-review.md +1092 -0
- agents/coder/database-design.md +1525 -0
- agents/coder/debugging.md +1102 -0
- agents/coder/dependency-management.md +1397 -0
- agents/coder/git-workflow.md +1099 -0
- agents/coder/refactoring.md +1454 -0
- agents/coder/security-hardening.md +1732 -0
- agents/coder/system_prompt.md +1448 -0
- agents/coder/tdd.md +1367 -0
- agents/creative-writer/__init__.py +0 -0
- agents/creative-writer/agent.json +4 -0
- agents/creative-writer/character-development.md +1852 -0
- agents/creative-writer/dialogue-craft.md +1122 -0
- agents/creative-writer/plot-structure.md +1073 -0
- agents/creative-writer/revision-editing.md +1484 -0
- agents/creative-writer/system_prompt.md +690 -0
- agents/creative-writer/worldbuilding.md +2049 -0
- agents/data-analyst/__init__.py +30 -0
- agents/data-analyst/agent.json +4 -0
- agents/data-analyst/data-visualization.md +992 -0
- agents/data-analyst/exploratory-data-analysis.md +1110 -0
- agents/data-analyst/pandas-data-manipulation.md +1081 -0
- agents/data-analyst/sql-query-optimization.md +881 -0
- agents/data-analyst/statistical-analysis.md +1118 -0
- agents/data-analyst/system_prompt.md +928 -0
- agents/default/__init__.py +0 -0
- agents/default/agent.json +4 -0
- agents/default/dead-code.md +794 -0
- agents/default/explore-agent-system.md +585 -0
- agents/default/system_prompt.md +1448 -0
- agents/kollabor/__init__.py +0 -0
- agents/kollabor/analyze-plugin-lifecycle.md +175 -0
- agents/kollabor/analyze-terminal-rendering.md +388 -0
- agents/kollabor/code-review.md +1092 -0
- agents/kollabor/debug-mcp-integration.md +521 -0
- agents/kollabor/debug-plugin-hooks.md +547 -0
- agents/kollabor/debugging.md +1102 -0
- agents/kollabor/dependency-management.md +1397 -0
- agents/kollabor/git-workflow.md +1099 -0
- agents/kollabor/inspect-llm-conversation.md +148 -0
- agents/kollabor/monitor-event-bus.md +558 -0
- agents/kollabor/profile-performance.md +576 -0
- agents/kollabor/refactoring.md +1454 -0
- agents/kollabor/system_prompt copy.md +1448 -0
- agents/kollabor/system_prompt.md +757 -0
- agents/kollabor/trace-command-execution.md +178 -0
- agents/kollabor/validate-config.md +879 -0
- agents/research/__init__.py +0 -0
- agents/research/agent.json +4 -0
- agents/research/architecture-mapping.md +1099 -0
- agents/research/codebase-analysis.md +1077 -0
- agents/research/dependency-audit.md +1027 -0
- agents/research/performance-profiling.md +1047 -0
- agents/research/security-review.md +1359 -0
- agents/research/system_prompt.md +492 -0
- agents/technical-writer/__init__.py +0 -0
- agents/technical-writer/agent.json +4 -0
- agents/technical-writer/api-documentation.md +2328 -0
- agents/technical-writer/changelog-management.md +1181 -0
- agents/technical-writer/readme-writing.md +1360 -0
- agents/technical-writer/style-guide.md +1410 -0
- agents/technical-writer/system_prompt.md +653 -0
- agents/technical-writer/tutorial-creation.md +1448 -0
- core/__init__.py +0 -2
- core/application.py +343 -88
- core/cli.py +229 -10
- core/commands/menu_renderer.py +463 -59
- core/commands/registry.py +14 -9
- core/commands/system_commands.py +2461 -14
- core/config/loader.py +151 -37
- core/config/service.py +18 -6
- core/events/bus.py +29 -9
- core/events/executor.py +205 -75
- core/events/models.py +27 -8
- core/fullscreen/command_integration.py +20 -24
- core/fullscreen/components/__init__.py +10 -1
- core/fullscreen/components/matrix_components.py +1 -2
- core/fullscreen/components/space_shooter_components.py +654 -0
- core/fullscreen/plugin.py +5 -0
- core/fullscreen/renderer.py +52 -13
- core/fullscreen/session.py +52 -15
- core/io/__init__.py +29 -5
- core/io/buffer_manager.py +6 -1
- core/io/config_status_view.py +7 -29
- core/io/core_status_views.py +267 -347
- core/io/input/__init__.py +25 -0
- core/io/input/command_mode_handler.py +711 -0
- core/io/input/display_controller.py +128 -0
- core/io/input/hook_registrar.py +286 -0
- core/io/input/input_loop_manager.py +421 -0
- core/io/input/key_press_handler.py +502 -0
- core/io/input/modal_controller.py +1011 -0
- core/io/input/paste_processor.py +339 -0
- core/io/input/status_modal_renderer.py +184 -0
- core/io/input_errors.py +5 -1
- core/io/input_handler.py +211 -2452
- core/io/key_parser.py +7 -0
- core/io/layout.py +15 -3
- core/io/message_coordinator.py +111 -2
- core/io/message_renderer.py +129 -4
- core/io/status_renderer.py +147 -607
- core/io/terminal_renderer.py +97 -51
- core/io/terminal_state.py +21 -4
- core/io/visual_effects.py +816 -165
- core/llm/agent_manager.py +1063 -0
- core/llm/api_adapters/__init__.py +44 -0
- core/llm/api_adapters/anthropic_adapter.py +432 -0
- core/llm/api_adapters/base.py +241 -0
- core/llm/api_adapters/openai_adapter.py +326 -0
- core/llm/api_communication_service.py +167 -113
- core/llm/conversation_logger.py +322 -16
- core/llm/conversation_manager.py +556 -30
- core/llm/file_operations_executor.py +84 -32
- core/llm/llm_service.py +934 -103
- core/llm/mcp_integration.py +541 -57
- core/llm/message_display_service.py +135 -18
- core/llm/plugin_sdk.py +1 -2
- core/llm/profile_manager.py +1183 -0
- core/llm/response_parser.py +274 -56
- core/llm/response_processor.py +16 -3
- core/llm/tool_executor.py +6 -1
- core/logging/__init__.py +2 -0
- core/logging/setup.py +34 -6
- core/models/resume.py +54 -0
- core/plugins/__init__.py +4 -2
- core/plugins/base.py +127 -0
- core/plugins/collector.py +23 -161
- core/plugins/discovery.py +37 -3
- core/plugins/factory.py +6 -12
- core/plugins/registry.py +5 -17
- core/ui/config_widgets.py +128 -28
- core/ui/live_modal_renderer.py +2 -1
- core/ui/modal_actions.py +5 -0
- core/ui/modal_overlay_renderer.py +0 -60
- core/ui/modal_renderer.py +268 -7
- core/ui/modal_state_manager.py +29 -4
- core/ui/widgets/base_widget.py +7 -0
- core/updates/__init__.py +10 -0
- core/updates/version_check_service.py +348 -0
- core/updates/version_comparator.py +103 -0
- core/utils/config_utils.py +685 -526
- core/utils/plugin_utils.py +1 -1
- core/utils/session_naming.py +111 -0
- fonts/LICENSE +21 -0
- fonts/README.md +46 -0
- fonts/SymbolsNerdFont-Regular.ttf +0 -0
- fonts/SymbolsNerdFontMono-Regular.ttf +0 -0
- fonts/__init__.py +44 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/METADATA +54 -4
- kollabor-0.4.15.dist-info/RECORD +228 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/top_level.txt +2 -0
- plugins/agent_orchestrator/__init__.py +39 -0
- plugins/agent_orchestrator/activity_monitor.py +181 -0
- plugins/agent_orchestrator/file_attacher.py +77 -0
- plugins/agent_orchestrator/message_injector.py +135 -0
- plugins/agent_orchestrator/models.py +48 -0
- plugins/agent_orchestrator/orchestrator.py +403 -0
- plugins/agent_orchestrator/plugin.py +976 -0
- plugins/agent_orchestrator/xml_parser.py +191 -0
- plugins/agent_orchestrator_plugin.py +9 -0
- plugins/enhanced_input/box_styles.py +1 -0
- plugins/enhanced_input/color_engine.py +19 -4
- plugins/enhanced_input/config.py +2 -2
- plugins/enhanced_input_plugin.py +61 -11
- plugins/fullscreen/__init__.py +6 -2
- plugins/fullscreen/example_plugin.py +1035 -222
- plugins/fullscreen/setup_wizard_plugin.py +592 -0
- plugins/fullscreen/space_shooter_plugin.py +131 -0
- plugins/hook_monitoring_plugin.py +436 -78
- plugins/query_enhancer_plugin.py +66 -30
- plugins/resume_conversation_plugin.py +1494 -0
- plugins/save_conversation_plugin.py +98 -32
- plugins/system_commands_plugin.py +70 -56
- plugins/tmux_plugin.py +154 -78
- plugins/workflow_enforcement_plugin.py +94 -92
- system_prompt/default.md +952 -886
- core/io/input_mode_manager.py +0 -402
- core/io/modal_interaction_handler.py +0 -315
- core/io/raw_input_processor.py +0 -946
- core/storage/__init__.py +0 -5
- core/storage/state_manager.py +0 -84
- core/ui/widget_integration.py +0 -222
- core/utils/key_reader.py +0 -171
- kollabor-0.4.9.dist-info/RECORD +0 -128
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/WHEEL +0 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/entry_points.txt +0 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/licenses/LICENSE +0 -0
core/commands/menu_renderer.py
CHANGED
|
@@ -1,11 +1,86 @@
|
|
|
1
1
|
"""Command menu renderer for interactive slash command display."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
from typing import List, Dict, Any, Optional
|
|
4
|
+
from typing import List, Dict, Any, Optional, Tuple
|
|
5
5
|
from ..events.models import CommandCategory
|
|
6
|
+
from ..io.visual_effects import (
|
|
7
|
+
AgnosterSegment, AgnosterColors, ColorPalette,
|
|
8
|
+
make_bg_color, make_fg_color, Powerline,
|
|
9
|
+
get_color_support, ColorSupport
|
|
10
|
+
)
|
|
6
11
|
|
|
7
12
|
logger = logging.getLogger(__name__)
|
|
8
13
|
|
|
14
|
+
# Basic ANSI codes
|
|
15
|
+
BOLD = "\033[1m"
|
|
16
|
+
DIM = "\033[2m"
|
|
17
|
+
RESET = ColorPalette.RESET
|
|
18
|
+
|
|
19
|
+
# Powerline characters
|
|
20
|
+
PL_RIGHT = "" # \ue0b0
|
|
21
|
+
|
|
22
|
+
# Menu symbols
|
|
23
|
+
ARROW_RIGHT = "▶"
|
|
24
|
+
ARROW_DOWN = "▼"
|
|
25
|
+
ARROW_UP = "▲"
|
|
26
|
+
DOT = "·"
|
|
27
|
+
GLOW = "◆"
|
|
28
|
+
|
|
29
|
+
# Category display order and icons (Unicode symbols)
|
|
30
|
+
CATEGORY_CONFIG = {
|
|
31
|
+
"system": {"name": "SYS", "icon": "⚙", "full": "System"},
|
|
32
|
+
"conversation": {"name": "CHAT", "icon": "⌘", "full": "Conversation"},
|
|
33
|
+
"agent": {"name": "AGENT", "icon": "◈", "full": "Agent"},
|
|
34
|
+
"development": {"name": "DEV", "icon": "⌥", "full": "Development"},
|
|
35
|
+
"file": {"name": "FILE", "icon": "≡", "full": "Files"},
|
|
36
|
+
"task": {"name": "TASK", "icon": "☰", "full": "Tasks"},
|
|
37
|
+
"custom": {"name": "PLUG", "icon": "⊕", "full": "Plugins"},
|
|
38
|
+
}
|
|
39
|
+
CATEGORY_ORDER = ["system", "conversation", "agent", "development", "file", "task", "custom"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def apply_bg_gradient(
|
|
43
|
+
text: str,
|
|
44
|
+
start_color: Tuple[int, int, int],
|
|
45
|
+
end_color: Tuple[int, int, int],
|
|
46
|
+
) -> str:
|
|
47
|
+
"""Apply background color gradient to text.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
text: Text to apply gradient background to.
|
|
51
|
+
start_color: RGB tuple for start of gradient.
|
|
52
|
+
end_color: RGB tuple for end of gradient.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Text with gradient background applied.
|
|
56
|
+
"""
|
|
57
|
+
if not text:
|
|
58
|
+
return text
|
|
59
|
+
|
|
60
|
+
result = []
|
|
61
|
+
text_length = len(text)
|
|
62
|
+
use_true_color = get_color_support() == ColorSupport.TRUE_COLOR
|
|
63
|
+
|
|
64
|
+
for i, char in enumerate(text):
|
|
65
|
+
position = i / max(1, text_length - 1)
|
|
66
|
+
|
|
67
|
+
# Interpolate between start and end colors
|
|
68
|
+
r = int(start_color[0] + (end_color[0] - start_color[0]) * position)
|
|
69
|
+
g = int(start_color[1] + (end_color[1] - start_color[1]) * position)
|
|
70
|
+
b = int(start_color[2] + (end_color[2] - start_color[2]) * position)
|
|
71
|
+
|
|
72
|
+
if use_true_color:
|
|
73
|
+
bg_code = f"\033[48;2;{r};{g};{b}m"
|
|
74
|
+
else:
|
|
75
|
+
# Fallback to 256-color approximation
|
|
76
|
+
color_idx = 16 + (36 * (r // 51)) + (6 * (g // 51)) + (b // 51)
|
|
77
|
+
bg_code = f"\033[48;5;{color_idx}m"
|
|
78
|
+
|
|
79
|
+
result.append(f"{bg_code}{char}")
|
|
80
|
+
|
|
81
|
+
result.append(RESET)
|
|
82
|
+
return "".join(result)
|
|
83
|
+
|
|
9
84
|
|
|
10
85
|
class CommandMenuRenderer:
|
|
11
86
|
"""Renders interactive command menu overlay.
|
|
@@ -14,19 +89,51 @@ class CommandMenuRenderer:
|
|
|
14
89
|
types '/' and allows filtering and selection of available commands.
|
|
15
90
|
"""
|
|
16
91
|
|
|
17
|
-
def __init__(self, terminal_renderer) -> None:
|
|
92
|
+
def __init__(self, terminal_renderer, max_visible_items: int = 5) -> None:
|
|
18
93
|
"""Initialize the command menu renderer.
|
|
19
94
|
|
|
20
95
|
Args:
|
|
21
96
|
terminal_renderer: Terminal renderer for display operations.
|
|
97
|
+
max_visible_items: Maximum number of menu items to show at once.
|
|
22
98
|
"""
|
|
23
99
|
self.renderer = terminal_renderer
|
|
24
100
|
self.logger = logger
|
|
25
101
|
self.menu_active = False
|
|
26
102
|
self.current_commands = []
|
|
103
|
+
self.menu_items = [] # Flattened list: commands + subcommands as selectable items
|
|
27
104
|
self.selected_index = 0
|
|
28
105
|
self.filter_text = ""
|
|
29
106
|
self.current_menu_lines = [] # Store menu content for event system
|
|
107
|
+
self.max_visible_items = max_visible_items
|
|
108
|
+
self.scroll_offset = 0 # First visible item index
|
|
109
|
+
|
|
110
|
+
def _get_menu_width(self) -> int:
|
|
111
|
+
"""Get menu width to match input box width.
|
|
112
|
+
|
|
113
|
+
Uses same calculation as enhanced_input plugin:
|
|
114
|
+
terminal_width - 4, clamped between min_width and max_width from config.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Menu width in characters.
|
|
118
|
+
"""
|
|
119
|
+
import shutil
|
|
120
|
+
try:
|
|
121
|
+
terminal_width = shutil.get_terminal_size().columns
|
|
122
|
+
except Exception:
|
|
123
|
+
terminal_width = 80
|
|
124
|
+
|
|
125
|
+
# Read constraints from same config as enhanced_input plugin
|
|
126
|
+
config = getattr(self.renderer, '_app_config', None)
|
|
127
|
+
if config and hasattr(config, 'get'):
|
|
128
|
+
min_width = config.get("plugins.enhanced_input.min_width", 60)
|
|
129
|
+
max_width = config.get("plugins.enhanced_input.max_width", 120)
|
|
130
|
+
else:
|
|
131
|
+
min_width = 60
|
|
132
|
+
max_width = 120
|
|
133
|
+
|
|
134
|
+
# Match input box: terminal_width - 4, with constraints
|
|
135
|
+
proposed_width = terminal_width - 4
|
|
136
|
+
return max(min_width, min(max_width, proposed_width))
|
|
30
137
|
|
|
31
138
|
def show_command_menu(self, commands: List[Dict[str, Any]], filter_text: str = "") -> None:
|
|
32
139
|
"""Display command menu when user types '/'.
|
|
@@ -37,28 +144,33 @@ class CommandMenuRenderer:
|
|
|
37
144
|
"""
|
|
38
145
|
try:
|
|
39
146
|
self.menu_active = True
|
|
40
|
-
self.current_commands = commands
|
|
147
|
+
self.current_commands = self._sort_commands_by_category(commands)
|
|
41
148
|
self.filter_text = filter_text
|
|
42
149
|
self.selected_index = 0
|
|
150
|
+
self.scroll_offset = 0 # Reset scroll when menu opens
|
|
151
|
+
|
|
152
|
+
# Build flattened menu items (commands + subcommands)
|
|
153
|
+
self.menu_items = self._build_menu_items()
|
|
43
154
|
|
|
44
155
|
# Render the menu
|
|
45
156
|
self._render_menu()
|
|
46
157
|
|
|
47
|
-
self.logger.info(f"Command menu shown with {len(commands)} commands")
|
|
158
|
+
self.logger.info(f"Command menu shown with {len(commands)} commands, {len(self.menu_items)} items")
|
|
48
159
|
|
|
49
160
|
except Exception as e:
|
|
50
161
|
self.logger.error(f"Error showing command menu: {e}")
|
|
51
162
|
|
|
52
163
|
def set_selected_index(self, index: int) -> None:
|
|
53
|
-
"""Set the selected
|
|
164
|
+
"""Set the selected menu item index for navigation.
|
|
54
165
|
|
|
55
166
|
Args:
|
|
56
|
-
index: Index of the
|
|
167
|
+
index: Index of the item to select (in menu_items list).
|
|
57
168
|
"""
|
|
58
|
-
if 0 <= index < len(self.
|
|
169
|
+
if 0 <= index < len(self.menu_items):
|
|
59
170
|
self.selected_index = index
|
|
171
|
+
self._ensure_selection_visible()
|
|
60
172
|
# Note: No auto-render here - caller will trigger render to avoid duplicates
|
|
61
|
-
logger.debug(f"Selected
|
|
173
|
+
logger.debug(f"Selected menu item index set to: {index}")
|
|
62
174
|
|
|
63
175
|
def hide_menu(self) -> None:
|
|
64
176
|
"""Hide command menu and return to normal input."""
|
|
@@ -68,6 +180,7 @@ class CommandMenuRenderer:
|
|
|
68
180
|
self.current_commands = []
|
|
69
181
|
self.selected_index = 0
|
|
70
182
|
self.filter_text = ""
|
|
183
|
+
self.scroll_offset = 0
|
|
71
184
|
|
|
72
185
|
# Clear menu from display
|
|
73
186
|
self._clear_menu()
|
|
@@ -89,16 +202,22 @@ class CommandMenuRenderer:
|
|
|
89
202
|
if not self.menu_active:
|
|
90
203
|
return
|
|
91
204
|
|
|
92
|
-
self.current_commands = commands
|
|
205
|
+
self.current_commands = self._sort_commands_by_category(commands)
|
|
93
206
|
self.filter_text = filter_text
|
|
94
207
|
|
|
208
|
+
# Build flattened menu items (commands + subcommands)
|
|
209
|
+
self.menu_items = self._build_menu_items()
|
|
210
|
+
|
|
95
211
|
# Only reset selection when filtering by typing, not during navigation
|
|
96
212
|
if reset_selection:
|
|
97
213
|
self.selected_index = 0 # Reset selection to top
|
|
214
|
+
self.scroll_offset = 0 # Reset scroll when filtering
|
|
98
215
|
else:
|
|
99
216
|
# Ensure selected index is still valid after filtering
|
|
100
|
-
if self.selected_index >= len(
|
|
101
|
-
self.selected_index = max(0, len(
|
|
217
|
+
if self.selected_index >= len(self.menu_items):
|
|
218
|
+
self.selected_index = max(0, len(self.menu_items) - 1)
|
|
219
|
+
# Adjust scroll if needed
|
|
220
|
+
self._ensure_selection_visible()
|
|
102
221
|
|
|
103
222
|
# Re-render with filtered commands
|
|
104
223
|
self._render_menu()
|
|
@@ -118,16 +237,19 @@ class CommandMenuRenderer:
|
|
|
118
237
|
True if navigation was handled, False otherwise.
|
|
119
238
|
"""
|
|
120
239
|
try:
|
|
121
|
-
if not self.menu_active or not self.
|
|
240
|
+
if not self.menu_active or not self.menu_items:
|
|
122
241
|
return False
|
|
123
242
|
|
|
124
243
|
if direction == "up":
|
|
125
244
|
self.selected_index = max(0, self.selected_index - 1)
|
|
126
245
|
elif direction == "down":
|
|
127
|
-
self.selected_index = min(len(self.
|
|
246
|
+
self.selected_index = min(len(self.menu_items) - 1, self.selected_index + 1)
|
|
128
247
|
else:
|
|
129
248
|
return False
|
|
130
249
|
|
|
250
|
+
# Adjust scroll to keep selection visible
|
|
251
|
+
self._ensure_selection_visible()
|
|
252
|
+
|
|
131
253
|
# Re-render with new selection
|
|
132
254
|
self._render_menu()
|
|
133
255
|
return True
|
|
@@ -136,18 +258,76 @@ class CommandMenuRenderer:
|
|
|
136
258
|
self.logger.error(f"Error navigating menu: {e}")
|
|
137
259
|
return False
|
|
138
260
|
|
|
261
|
+
def _ensure_selection_visible(self) -> None:
|
|
262
|
+
"""Adjust scroll offset to keep selected item visible."""
|
|
263
|
+
if not self.menu_items:
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
# If selection is above visible area, scroll up
|
|
267
|
+
if self.selected_index < self.scroll_offset:
|
|
268
|
+
self.scroll_offset = self.selected_index
|
|
269
|
+
|
|
270
|
+
# If selection is below visible area, scroll down
|
|
271
|
+
elif self.selected_index >= self.scroll_offset + self.max_visible_items:
|
|
272
|
+
self.scroll_offset = self.selected_index - self.max_visible_items + 1
|
|
273
|
+
|
|
274
|
+
# Clamp scroll offset to valid range
|
|
275
|
+
max_scroll = max(0, len(self.menu_items) - self.max_visible_items)
|
|
276
|
+
self.scroll_offset = max(0, min(self.scroll_offset, max_scroll))
|
|
277
|
+
|
|
139
278
|
def get_selected_command(self) -> Optional[Dict[str, Any]]:
|
|
140
|
-
"""Get currently selected command.
|
|
279
|
+
"""Get currently selected menu item (command or subcommand).
|
|
141
280
|
|
|
142
281
|
Returns:
|
|
143
|
-
Selected
|
|
282
|
+
Selected item dictionary with keys:
|
|
283
|
+
- For commands: name, description, aliases, category, etc.
|
|
284
|
+
- For subcommands: is_subcommand=True, parent_name, subcommand_name, subcommand_args
|
|
285
|
+
Returns None if no selection.
|
|
144
286
|
"""
|
|
145
287
|
if (self.menu_active and
|
|
146
|
-
self.
|
|
147
|
-
0 <= self.selected_index < len(self.
|
|
148
|
-
return self.
|
|
288
|
+
self.menu_items and
|
|
289
|
+
0 <= self.selected_index < len(self.menu_items)):
|
|
290
|
+
return self.menu_items[self.selected_index]
|
|
149
291
|
return None
|
|
150
292
|
|
|
293
|
+
def _build_menu_items(self) -> List[Dict[str, Any]]:
|
|
294
|
+
"""Build flattened list of menu items including subcommands.
|
|
295
|
+
|
|
296
|
+
Creates a flat list where each command is followed by its subcommands.
|
|
297
|
+
Subcommands only appear when filtered to a single command (not in main menu).
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
List of menu item dicts, each with is_subcommand flag.
|
|
301
|
+
"""
|
|
302
|
+
items = []
|
|
303
|
+
# Only show subcommands when filtered to a single command
|
|
304
|
+
show_subcommands = len(self.current_commands) == 1
|
|
305
|
+
|
|
306
|
+
for i, cmd in enumerate(self.current_commands):
|
|
307
|
+
# Add the command itself
|
|
308
|
+
cmd_item = cmd.copy()
|
|
309
|
+
cmd_item["is_subcommand"] = False
|
|
310
|
+
cmd_item["_cmd_index"] = i
|
|
311
|
+
items.append(cmd_item)
|
|
312
|
+
|
|
313
|
+
# Add subcommands only when this is the only command showing
|
|
314
|
+
if show_subcommands:
|
|
315
|
+
subcommands = cmd.get("subcommands", [])
|
|
316
|
+
if subcommands:
|
|
317
|
+
for sub in subcommands:
|
|
318
|
+
sub_item = {
|
|
319
|
+
"is_subcommand": True,
|
|
320
|
+
"parent_name": cmd["name"],
|
|
321
|
+
"parent_category": cmd.get("category", "custom"),
|
|
322
|
+
"subcommand_name": sub.get("name", ""),
|
|
323
|
+
"subcommand_args": sub.get("args", ""),
|
|
324
|
+
"subcommand_desc": sub.get("description", ""),
|
|
325
|
+
"_cmd_index": i,
|
|
326
|
+
}
|
|
327
|
+
items.append(sub_item)
|
|
328
|
+
|
|
329
|
+
return items
|
|
330
|
+
|
|
151
331
|
def _render_menu(self) -> None:
|
|
152
332
|
"""Render the command menu overlay."""
|
|
153
333
|
try:
|
|
@@ -164,44 +344,207 @@ class CommandMenuRenderer:
|
|
|
164
344
|
self.logger.error(f"Error rendering menu: {e}")
|
|
165
345
|
|
|
166
346
|
def _create_menu_lines(self) -> List[str]:
|
|
167
|
-
"""Create lines for menu display.
|
|
347
|
+
"""Create lines for menu display with category grouping and scroll support.
|
|
168
348
|
|
|
169
349
|
Returns:
|
|
170
|
-
List of formatted menu lines.
|
|
350
|
+
List of formatted menu lines (limited to max_visible_items).
|
|
171
351
|
"""
|
|
172
352
|
lines = []
|
|
173
353
|
|
|
174
|
-
#
|
|
175
|
-
|
|
354
|
+
# If no items, show empty state
|
|
355
|
+
if not self.menu_items:
|
|
356
|
+
lines.append(self._make_empty_state())
|
|
357
|
+
return lines
|
|
176
358
|
|
|
177
|
-
|
|
178
|
-
|
|
359
|
+
total_items = len(self.menu_items)
|
|
360
|
+
has_more_above = self.scroll_offset > 0
|
|
361
|
+
has_more_below = self.scroll_offset + self.max_visible_items < total_items
|
|
179
362
|
|
|
180
|
-
#
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
continue
|
|
363
|
+
# Scroll up indicator
|
|
364
|
+
if has_more_above:
|
|
365
|
+
lines.append(self._make_scroll_indicator("up", self.scroll_offset))
|
|
184
366
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
lines.append(f" {category_name}")
|
|
367
|
+
# Get visible items slice
|
|
368
|
+
visible_start = self.scroll_offset
|
|
369
|
+
visible_end = min(self.scroll_offset + self.max_visible_items, total_items)
|
|
189
370
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
371
|
+
# Track current category for headers
|
|
372
|
+
current_category = None
|
|
373
|
+
|
|
374
|
+
# Render visible items (commands and subcommands)
|
|
375
|
+
for i in range(visible_start, visible_end):
|
|
376
|
+
item = self.menu_items[i]
|
|
377
|
+
is_selected = (i == self.selected_index)
|
|
378
|
+
|
|
379
|
+
if item.get("is_subcommand"):
|
|
380
|
+
# Render subcommand item
|
|
381
|
+
line = self._format_subcommand_item(item, is_selected)
|
|
382
|
+
lines.append(line)
|
|
383
|
+
else:
|
|
384
|
+
# Render command item
|
|
385
|
+
cmd_category = item.get("category", "custom")
|
|
386
|
+
|
|
387
|
+
# Convert CommandCategory enum to string if needed
|
|
388
|
+
if hasattr(cmd_category, 'value'):
|
|
389
|
+
cmd_category = cmd_category.value
|
|
390
|
+
|
|
391
|
+
# Insert category header when category changes
|
|
392
|
+
if cmd_category != current_category:
|
|
393
|
+
current_category = cmd_category
|
|
394
|
+
header = self._format_category_header(cmd_category)
|
|
395
|
+
lines.append(header)
|
|
396
|
+
|
|
397
|
+
item["_is_selected"] = is_selected
|
|
398
|
+
item["_index"] = i
|
|
399
|
+
line = self._format_command_line(item, cmd_category)
|
|
193
400
|
lines.append(line)
|
|
194
401
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
402
|
+
# Scroll down indicator
|
|
403
|
+
if has_more_below:
|
|
404
|
+
remaining = total_items - visible_end
|
|
405
|
+
lines.append(self._make_scroll_indicator("down", remaining))
|
|
198
406
|
|
|
199
|
-
#
|
|
200
|
-
|
|
201
|
-
lines.append(" No matching commands found")
|
|
407
|
+
# Footer with keybind hints
|
|
408
|
+
lines.extend(self._make_footer())
|
|
202
409
|
|
|
203
410
|
return lines
|
|
204
411
|
|
|
412
|
+
def _make_empty_state(self) -> str:
|
|
413
|
+
"""Create fancy empty state message."""
|
|
414
|
+
seg = AgnosterSegment()
|
|
415
|
+
seg.add_neutral(f" {GLOW} No matches ", "dark")
|
|
416
|
+
return seg.render(separator="")
|
|
417
|
+
|
|
418
|
+
def _make_scroll_indicator(self, direction: str, count: int) -> str:
|
|
419
|
+
"""Create scroll indicator with gradient background.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
direction: "up" or "down"
|
|
423
|
+
count: Number of items in that direction
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
Formatted scroll indicator string with gradient.
|
|
427
|
+
"""
|
|
428
|
+
arrow = ARROW_UP if direction == "up" else ARROW_DOWN
|
|
429
|
+
line_width = self._get_menu_width()
|
|
430
|
+
|
|
431
|
+
# Solid start portion with arrow and count
|
|
432
|
+
cyan_bg = make_bg_color(*AgnosterColors.CYAN_DARK)
|
|
433
|
+
text_light = make_fg_color(*AgnosterColors.TEXT_LIGHT)
|
|
434
|
+
start_text = f" {arrow} {count} "
|
|
435
|
+
|
|
436
|
+
# Gradient fade portion (remaining width)
|
|
437
|
+
fade_width = line_width - len(start_text)
|
|
438
|
+
fade_spaces = " " * fade_width
|
|
439
|
+
|
|
440
|
+
# Apply gradient from cyan to near-black
|
|
441
|
+
gradient_fade = apply_bg_gradient(
|
|
442
|
+
fade_spaces,
|
|
443
|
+
start_color=AgnosterColors.CYAN_DARK,
|
|
444
|
+
end_color=(15, 25, 35), # Dark blue-grey
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
line = (
|
|
448
|
+
f"{cyan_bg}{text_light} {arrow} {BOLD}{count}{RESET}"
|
|
449
|
+
f"{gradient_fade}"
|
|
450
|
+
)
|
|
451
|
+
return line
|
|
452
|
+
|
|
453
|
+
def _make_footer(self) -> List[str]:
|
|
454
|
+
"""Create footer with keybind hints.
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
List of formatted footer lines.
|
|
458
|
+
"""
|
|
459
|
+
hint_text = "↑↓ navigate ⏎ select esc cancel"
|
|
460
|
+
return [f" {DIM} {hint_text}{RESET}"]
|
|
461
|
+
|
|
462
|
+
def _format_subcommand_item(self, item: Dict[str, Any], is_selected: bool) -> str:
|
|
463
|
+
"""Format a single subcommand as a selectable menu item.
|
|
464
|
+
|
|
465
|
+
Args:
|
|
466
|
+
item: Subcommand item dict with subcommand_name, subcommand_args, subcommand_desc.
|
|
467
|
+
is_selected: Whether this item is currently selected.
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
Formatted subcommand line.
|
|
471
|
+
"""
|
|
472
|
+
name = item.get("subcommand_name", "")
|
|
473
|
+
args = item.get("subcommand_args", "")
|
|
474
|
+
desc = item.get("subcommand_desc", "")
|
|
475
|
+
|
|
476
|
+
# Format: " new <name> <cmd> Create session..."
|
|
477
|
+
cmd_part = f"{name}"
|
|
478
|
+
if args:
|
|
479
|
+
cmd_part += f" {args}"
|
|
480
|
+
|
|
481
|
+
# Pad command part to align descriptions
|
|
482
|
+
cmd_padded = cmd_part.ljust(20)
|
|
483
|
+
|
|
484
|
+
if is_selected:
|
|
485
|
+
# Selected subcommand - highlighted
|
|
486
|
+
cyan_bg = make_bg_color(*AgnosterColors.CYAN_DARK)
|
|
487
|
+
cyan_fg = make_fg_color(*AgnosterColors.CYAN)
|
|
488
|
+
lime_fg = make_fg_color(*AgnosterColors.LIME)
|
|
489
|
+
text_light = make_fg_color(*AgnosterColors.TEXT_LIGHT)
|
|
490
|
+
dark_bg = make_bg_color(*AgnosterColors.BG_MID)
|
|
491
|
+
|
|
492
|
+
line = f" {lime_fg}{GLOW}{RESET} {cyan_fg}{BOLD}{cmd_padded}{RESET} {text_light}{desc}{RESET}"
|
|
493
|
+
else:
|
|
494
|
+
# Unselected subcommand - dimmed
|
|
495
|
+
cyan_fg = make_fg_color(*AgnosterColors.CYAN)
|
|
496
|
+
line = f" {cyan_fg}{cmd_padded}{RESET} {DIM}{desc}{RESET}"
|
|
497
|
+
|
|
498
|
+
return line
|
|
499
|
+
|
|
500
|
+
def _format_category_header(self, category: str) -> str:
|
|
501
|
+
"""Format a powerline-style category header.
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
category: Category identifier.
|
|
505
|
+
|
|
506
|
+
Returns:
|
|
507
|
+
Formatted category header string with powerline transitions.
|
|
508
|
+
"""
|
|
509
|
+
config = CATEGORY_CONFIG.get(category, {"name": "???", "icon": "?", "full": category.title()})
|
|
510
|
+
|
|
511
|
+
# Build powerline: icon segment -> name segment -> fade out
|
|
512
|
+
lime_bg = make_bg_color(*AgnosterColors.LIME)
|
|
513
|
+
lime_fg = make_fg_color(*AgnosterColors.LIME)
|
|
514
|
+
dark_bg = make_bg_color(*AgnosterColors.BG_DARK)
|
|
515
|
+
dark_fg = make_fg_color(*AgnosterColors.TEXT_DARK)
|
|
516
|
+
light_fg = make_fg_color(*AgnosterColors.TEXT_LIGHT)
|
|
517
|
+
|
|
518
|
+
# Icon in lime -> powerline separator -> name in dark
|
|
519
|
+
line = (
|
|
520
|
+
f"{lime_bg}{dark_fg}{BOLD} {config['icon']} {RESET}"
|
|
521
|
+
f"{dark_bg}{lime_fg}{PL_RIGHT}{RESET}"
|
|
522
|
+
f"{dark_bg}{light_fg} {config['full']} {RESET}"
|
|
523
|
+
f"{lime_fg}{PL_RIGHT}{RESET}"
|
|
524
|
+
)
|
|
525
|
+
return line
|
|
526
|
+
|
|
527
|
+
def _sort_commands_by_category(self, commands: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
528
|
+
"""Sort commands by category order for grouped display.
|
|
529
|
+
|
|
530
|
+
Args:
|
|
531
|
+
commands: List of command dictionaries.
|
|
532
|
+
|
|
533
|
+
Returns:
|
|
534
|
+
Sorted list of commands.
|
|
535
|
+
"""
|
|
536
|
+
def get_category_order(cmd):
|
|
537
|
+
category = cmd.get("category", "custom")
|
|
538
|
+
# Handle CommandCategory enum
|
|
539
|
+
if hasattr(category, 'value'):
|
|
540
|
+
category = category.value
|
|
541
|
+
try:
|
|
542
|
+
return CATEGORY_ORDER.index(category)
|
|
543
|
+
except ValueError:
|
|
544
|
+
return len(CATEGORY_ORDER) # Unknown categories go last
|
|
545
|
+
|
|
546
|
+
return sorted(commands, key=get_category_order)
|
|
547
|
+
|
|
205
548
|
def _group_commands_by_category(self) -> Dict[str, List[Dict[str, Any]]]:
|
|
206
549
|
"""Group commands by category for organized display.
|
|
207
550
|
|
|
@@ -244,31 +587,89 @@ class CommandMenuRenderer:
|
|
|
244
587
|
}
|
|
245
588
|
return category_names.get(category, category.title())
|
|
246
589
|
|
|
247
|
-
def _format_command_line(self, cmd: Dict[str, Any]) -> str:
|
|
248
|
-
"""Format a single command line
|
|
590
|
+
def _format_command_line(self, cmd: Dict[str, Any], category: str = "custom") -> str:
|
|
591
|
+
"""Format a single command line with powerline style.
|
|
249
592
|
|
|
250
593
|
Args:
|
|
251
594
|
cmd: Command dictionary with display info.
|
|
595
|
+
category: Category for color theming.
|
|
252
596
|
|
|
253
597
|
Returns:
|
|
254
598
|
Formatted command line string.
|
|
255
599
|
"""
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
# Command name with aliases
|
|
260
|
-
name_part = f"/{cmd['name']}"
|
|
261
|
-
if cmd.get("aliases"):
|
|
262
|
-
aliases_str = ", ".join(cmd["aliases"])
|
|
263
|
-
name_part += f" ({aliases_str})"
|
|
264
|
-
|
|
265
|
-
# Description
|
|
600
|
+
is_selected = cmd.get("_is_selected", False)
|
|
601
|
+
name = cmd['name']
|
|
266
602
|
description = cmd.get("description", "")
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
603
|
+
aliases = cmd.get("aliases", [])
|
|
604
|
+
line_width = self._get_menu_width()
|
|
605
|
+
|
|
606
|
+
if is_selected:
|
|
607
|
+
# SELECTED: Full powerline glow effect
|
|
608
|
+
# [glow] [cyan: /name] [dark: description padded] [aliases]
|
|
609
|
+
cyan_bg = make_bg_color(*AgnosterColors.CYAN)
|
|
610
|
+
cyan_fg = make_fg_color(*AgnosterColors.CYAN)
|
|
611
|
+
lime_bg = make_bg_color(*AgnosterColors.LIME)
|
|
612
|
+
lime_fg = make_fg_color(*AgnosterColors.LIME)
|
|
613
|
+
dark_bg = make_bg_color(*AgnosterColors.BG_MID)
|
|
614
|
+
dark_fg = make_fg_color(*AgnosterColors.BG_MID)
|
|
615
|
+
text_dark = make_fg_color(*AgnosterColors.TEXT_DARK)
|
|
616
|
+
text_light = make_fg_color(*AgnosterColors.TEXT_LIGHT)
|
|
617
|
+
|
|
618
|
+
# Calculate available space for description
|
|
619
|
+
# Layout: " ◆ " (4) + /name + " " (varies) + description + padding
|
|
620
|
+
name_part = f" /{name} "
|
|
621
|
+
prefix_len = 4 + len(name_part) + 2 # glow + name + separators
|
|
622
|
+
|
|
623
|
+
# Build alias hint if available
|
|
624
|
+
alias_hint = ""
|
|
625
|
+
alias_len = 0
|
|
626
|
+
if aliases:
|
|
627
|
+
alias_str = " ".join(f"/{a}" for a in aliases[:2]) # Max 2 aliases
|
|
628
|
+
alias_hint = f" {DIM}also: {alias_str}{RESET}"
|
|
629
|
+
alias_len = len(f" also: {alias_str}")
|
|
630
|
+
|
|
631
|
+
# Available for description + padding
|
|
632
|
+
desc_area = line_width - prefix_len - alias_len - 2
|
|
633
|
+
if len(description) > desc_area:
|
|
634
|
+
description = description[:desc_area-2] + ".."
|
|
635
|
+
desc_padded = description.ljust(desc_area)
|
|
636
|
+
|
|
637
|
+
line = (
|
|
638
|
+
f"{lime_bg}{text_dark}{BOLD} {GLOW} {RESET}"
|
|
639
|
+
f"{cyan_bg}{lime_fg}{PL_RIGHT}{RESET}"
|
|
640
|
+
f"{cyan_bg}{text_dark}{BOLD} /{name} {RESET}"
|
|
641
|
+
f"{dark_bg}{cyan_fg}{PL_RIGHT}{RESET}"
|
|
642
|
+
f"{dark_bg}{text_light} {desc_padded}{RESET}"
|
|
643
|
+
f"{dark_fg}{PL_RIGHT}{RESET}"
|
|
644
|
+
f"{alias_hint}"
|
|
645
|
+
)
|
|
646
|
+
return line
|
|
647
|
+
else:
|
|
648
|
+
# NOT SELECTED: Subtle powerline with full-width description
|
|
649
|
+
mid_bg = make_bg_color(*AgnosterColors.BG_DARK)
|
|
650
|
+
mid_fg = make_fg_color(*AgnosterColors.BG_DARK)
|
|
651
|
+
text_light = make_fg_color(*AgnosterColors.TEXT_LIGHT)
|
|
652
|
+
cyan_fg = make_fg_color(*AgnosterColors.CYAN)
|
|
653
|
+
|
|
654
|
+
# Dot leader between name and description
|
|
655
|
+
name_str = f"/{name}"
|
|
656
|
+
name_col_width = 14 # Fixed column for command name
|
|
657
|
+
padding_len = name_col_width - len(name_str)
|
|
658
|
+
dots = DOT * max(2, padding_len)
|
|
659
|
+
|
|
660
|
+
# Calculate description area
|
|
661
|
+
# Layout: " " (3) + name_col + " " + dots + " " + description
|
|
662
|
+
prefix_len = 3 + name_col_width + 4 # indent + name + powerline + spacing
|
|
663
|
+
desc_area = line_width - prefix_len - 2
|
|
664
|
+
if len(description) > desc_area:
|
|
665
|
+
description = description[:desc_area-2] + ".."
|
|
666
|
+
|
|
667
|
+
line = (
|
|
668
|
+
f" {mid_bg}{cyan_fg}{BOLD} {name_str} {RESET}"
|
|
669
|
+
f"{mid_fg}{PL_RIGHT}{RESET}"
|
|
670
|
+
f" {DIM}{dots} {description}{RESET}"
|
|
671
|
+
)
|
|
672
|
+
return line
|
|
272
673
|
|
|
273
674
|
def _display_menu_overlay(self, menu_lines: List[str]) -> None:
|
|
274
675
|
"""Display menu as overlay on terminal.
|
|
@@ -315,5 +716,8 @@ class CommandMenuRenderer:
|
|
|
315
716
|
"command_count": len(self.current_commands),
|
|
316
717
|
"selected_index": self.selected_index,
|
|
317
718
|
"filter_text": self.filter_text,
|
|
318
|
-
"selected_command": self.get_selected_command()
|
|
719
|
+
"selected_command": self.get_selected_command(),
|
|
720
|
+
"scroll_offset": self.scroll_offset,
|
|
721
|
+
"max_visible_items": self.max_visible_items,
|
|
722
|
+
"visible_range": f"{self.scroll_offset}-{min(self.scroll_offset + self.max_visible_items, len(self.current_commands))}"
|
|
319
723
|
}
|