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
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
"""Command mode handler component for Kollabor CLI.
|
|
2
|
+
|
|
3
|
+
Responsible for managing slash command mode interactions including:
|
|
4
|
+
- Command menu popup navigation
|
|
5
|
+
- Status view cycling
|
|
6
|
+
- Command execution
|
|
7
|
+
- Modal transitions
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Dict, Any, List, Callable, Optional
|
|
12
|
+
|
|
13
|
+
from ...events.models import CommandMode, EventType
|
|
14
|
+
from ..key_parser import KeyPress
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CommandModeHandler:
|
|
20
|
+
"""Handles slash command mode interactions and navigation.
|
|
21
|
+
|
|
22
|
+
This component manages:
|
|
23
|
+
- Entering/exiting command mode
|
|
24
|
+
- Command menu popup with filtering
|
|
25
|
+
- Arrow key navigation in menu
|
|
26
|
+
- Command execution
|
|
27
|
+
- Status view cycling (Ctrl+Left/Right)
|
|
28
|
+
- Status takeover mode
|
|
29
|
+
|
|
30
|
+
Attributes:
|
|
31
|
+
buffer_manager: Buffer manager for command text.
|
|
32
|
+
renderer: Terminal renderer for status access.
|
|
33
|
+
event_bus: Event bus for emitting command events.
|
|
34
|
+
command_registry: Registry of available commands.
|
|
35
|
+
command_executor: Executor for running commands.
|
|
36
|
+
command_menu_renderer: Renderer for command menu display.
|
|
37
|
+
slash_parser: Parser for slash command syntax.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
buffer_manager: Any,
|
|
43
|
+
renderer: Any,
|
|
44
|
+
event_bus: Any,
|
|
45
|
+
command_registry: Any,
|
|
46
|
+
command_executor: Any,
|
|
47
|
+
command_menu_renderer: Any,
|
|
48
|
+
slash_parser: Any,
|
|
49
|
+
error_handler: Optional[Any] = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Initialize the command mode handler.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
buffer_manager: Buffer manager for command text.
|
|
55
|
+
renderer: Terminal renderer for status access.
|
|
56
|
+
event_bus: Event bus for emitting command events.
|
|
57
|
+
command_registry: Registry of available commands.
|
|
58
|
+
command_executor: Executor for running commands.
|
|
59
|
+
command_menu_renderer: Renderer for command menu display.
|
|
60
|
+
slash_parser: Parser for slash command syntax.
|
|
61
|
+
error_handler: Optional error handler for command errors.
|
|
62
|
+
"""
|
|
63
|
+
self.buffer_manager = buffer_manager
|
|
64
|
+
self.renderer = renderer
|
|
65
|
+
self.event_bus = event_bus
|
|
66
|
+
self.command_registry = command_registry
|
|
67
|
+
self.command_executor = command_executor
|
|
68
|
+
self.command_menu_renderer = command_menu_renderer
|
|
69
|
+
self.slash_parser = slash_parser
|
|
70
|
+
self.error_handler = error_handler
|
|
71
|
+
|
|
72
|
+
# Command mode state
|
|
73
|
+
self.command_mode = CommandMode.NORMAL
|
|
74
|
+
self.command_menu_active = False
|
|
75
|
+
self.selected_command_index = 0
|
|
76
|
+
|
|
77
|
+
# Callbacks for operations that require access to parent InputHandler
|
|
78
|
+
self._update_display_callback: Optional[Callable] = None
|
|
79
|
+
self._exit_modal_callback: Optional[Callable] = None
|
|
80
|
+
|
|
81
|
+
# Callbacks for modal mode handling (delegated to ModalController)
|
|
82
|
+
self._handle_modal_keypress_callback: Optional[Callable] = None
|
|
83
|
+
self._handle_status_modal_keypress_callback: Optional[Callable] = None
|
|
84
|
+
self._handle_live_modal_keypress_callback: Optional[Callable] = None
|
|
85
|
+
|
|
86
|
+
logger.debug("CommandModeHandler initialized")
|
|
87
|
+
|
|
88
|
+
def set_update_display_callback(self, callback: Callable) -> None:
|
|
89
|
+
"""Set callback for updating display.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
callback: Async function to call for display updates.
|
|
93
|
+
"""
|
|
94
|
+
self._update_display_callback = callback
|
|
95
|
+
|
|
96
|
+
def set_exit_modal_callback(self, callback: Callable) -> None:
|
|
97
|
+
"""Set callback for exiting modal mode.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
callback: Async function to call for modal exit.
|
|
101
|
+
"""
|
|
102
|
+
self._exit_modal_callback = callback
|
|
103
|
+
|
|
104
|
+
def set_modal_callbacks(
|
|
105
|
+
self,
|
|
106
|
+
handle_modal_keypress: Optional[Callable] = None,
|
|
107
|
+
handle_status_modal_keypress: Optional[Callable] = None,
|
|
108
|
+
handle_live_modal_keypress: Optional[Callable] = None,
|
|
109
|
+
) -> None:
|
|
110
|
+
"""Set callbacks for modal mode handling.
|
|
111
|
+
|
|
112
|
+
These callbacks delegate to ModalController for actual handling.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
handle_modal_keypress: Callback for MODAL mode key handling.
|
|
116
|
+
handle_status_modal_keypress: Callback for STATUS_MODAL mode key handling.
|
|
117
|
+
handle_live_modal_keypress: Callback for LIVE_MODAL mode key handling.
|
|
118
|
+
"""
|
|
119
|
+
self._handle_modal_keypress_callback = handle_modal_keypress
|
|
120
|
+
self._handle_status_modal_keypress_callback = handle_status_modal_keypress
|
|
121
|
+
self._handle_live_modal_keypress_callback = handle_live_modal_keypress
|
|
122
|
+
|
|
123
|
+
async def enter_command_mode(self) -> None:
|
|
124
|
+
"""Enter slash command mode and show command menu."""
|
|
125
|
+
try:
|
|
126
|
+
logger.info("Entering slash command mode")
|
|
127
|
+
self.command_mode = CommandMode.MENU_POPUP
|
|
128
|
+
self.command_menu_active = True
|
|
129
|
+
|
|
130
|
+
# Reset selection to first command
|
|
131
|
+
self.selected_command_index = 0
|
|
132
|
+
|
|
133
|
+
# Add the '/' character to buffer for visual feedback
|
|
134
|
+
self.buffer_manager.insert_char("/")
|
|
135
|
+
|
|
136
|
+
# Show command menu via renderer
|
|
137
|
+
available_commands = self._get_available_commands()
|
|
138
|
+
self.command_menu_renderer.show_command_menu(available_commands, "")
|
|
139
|
+
|
|
140
|
+
# Emit command menu show event
|
|
141
|
+
await self.event_bus.emit_with_hooks(
|
|
142
|
+
EventType.COMMAND_MENU_SHOW,
|
|
143
|
+
{"available_commands": available_commands, "filter_text": ""},
|
|
144
|
+
"commands",
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Update display to show command mode
|
|
148
|
+
if self._update_display_callback:
|
|
149
|
+
await self._update_display_callback(force_render=True)
|
|
150
|
+
|
|
151
|
+
logger.info("Command menu activated")
|
|
152
|
+
|
|
153
|
+
except Exception as e:
|
|
154
|
+
logger.error(f"Error entering command mode: {e}")
|
|
155
|
+
await self.exit_command_mode()
|
|
156
|
+
|
|
157
|
+
async def exit_command_mode(self) -> None:
|
|
158
|
+
"""Exit command mode and restore normal input."""
|
|
159
|
+
try:
|
|
160
|
+
import traceback
|
|
161
|
+
|
|
162
|
+
logger.info("Exiting slash command mode")
|
|
163
|
+
logger.debug(f"Exit called from: {traceback.format_stack()[-2].strip()}")
|
|
164
|
+
|
|
165
|
+
# Hide command menu via renderer
|
|
166
|
+
self.command_menu_renderer.hide_menu()
|
|
167
|
+
|
|
168
|
+
# Emit command menu hide event
|
|
169
|
+
if self.command_menu_active:
|
|
170
|
+
await self.event_bus.emit_with_hooks(
|
|
171
|
+
EventType.COMMAND_MENU_HIDE,
|
|
172
|
+
{"reason": "manual_exit"},
|
|
173
|
+
"commands",
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
self.command_mode = CommandMode.NORMAL
|
|
177
|
+
self.command_menu_active = False
|
|
178
|
+
|
|
179
|
+
# Clear command buffer (remove the '/' and any partial command)
|
|
180
|
+
self.buffer_manager.clear()
|
|
181
|
+
|
|
182
|
+
# Update display
|
|
183
|
+
if self._update_display_callback:
|
|
184
|
+
await self._update_display_callback(force_render=True)
|
|
185
|
+
|
|
186
|
+
logger.info("Returned to normal input mode")
|
|
187
|
+
|
|
188
|
+
except Exception as e:
|
|
189
|
+
logger.error(f"Error exiting command mode: {e}")
|
|
190
|
+
|
|
191
|
+
async def handle_command_mode_keypress(self, key_press: KeyPress) -> bool:
|
|
192
|
+
"""Handle KeyPress while in command mode (supports arrow keys).
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
key_press: Parsed key press to process.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
True if key was handled, False to fall through to normal processing.
|
|
199
|
+
"""
|
|
200
|
+
try:
|
|
201
|
+
if self.command_mode == CommandMode.MENU_POPUP:
|
|
202
|
+
return await self.handle_menu_popup_keypress(key_press)
|
|
203
|
+
elif self.command_mode == CommandMode.STATUS_TAKEOVER:
|
|
204
|
+
return await self.handle_status_takeover_keypress(key_press)
|
|
205
|
+
elif self.command_mode == CommandMode.MODAL:
|
|
206
|
+
# Delegate to ModalController via callback
|
|
207
|
+
if self._handle_modal_keypress_callback:
|
|
208
|
+
return await self._handle_modal_keypress_callback(key_press)
|
|
209
|
+
else:
|
|
210
|
+
logger.warning("MODAL mode active but no callback set")
|
|
211
|
+
return False
|
|
212
|
+
elif self.command_mode == CommandMode.STATUS_MODAL:
|
|
213
|
+
# Delegate to ModalController via callback
|
|
214
|
+
if self._handle_status_modal_keypress_callback:
|
|
215
|
+
return await self._handle_status_modal_keypress_callback(key_press)
|
|
216
|
+
else:
|
|
217
|
+
logger.warning("STATUS_MODAL mode active but no callback set")
|
|
218
|
+
return False
|
|
219
|
+
elif self.command_mode == CommandMode.LIVE_MODAL:
|
|
220
|
+
# Delegate to ModalController via callback
|
|
221
|
+
if self._handle_live_modal_keypress_callback:
|
|
222
|
+
return await self._handle_live_modal_keypress_callback(key_press)
|
|
223
|
+
else:
|
|
224
|
+
logger.warning("LIVE_MODAL mode active but no callback set")
|
|
225
|
+
return False
|
|
226
|
+
else:
|
|
227
|
+
# Unknown command mode, exit to normal
|
|
228
|
+
await self.exit_command_mode()
|
|
229
|
+
return False
|
|
230
|
+
|
|
231
|
+
except Exception as e:
|
|
232
|
+
logger.error(f"Error handling command mode keypress: {e}")
|
|
233
|
+
await self.exit_command_mode()
|
|
234
|
+
return False
|
|
235
|
+
|
|
236
|
+
async def handle_command_mode_input(self, char: str) -> bool:
|
|
237
|
+
"""Handle input while in command mode.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
char: Character input to process.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
True if input was handled, False to fall through to normal processing.
|
|
244
|
+
"""
|
|
245
|
+
try:
|
|
246
|
+
if self.command_mode == CommandMode.MENU_POPUP:
|
|
247
|
+
return await self.handle_menu_popup_input(char)
|
|
248
|
+
elif self.command_mode == CommandMode.STATUS_TAKEOVER:
|
|
249
|
+
return await self.handle_status_takeover_input(char)
|
|
250
|
+
elif self.command_mode == CommandMode.STATUS_MODAL:
|
|
251
|
+
# STATUS_MODAL input is handled via keypress callback
|
|
252
|
+
# Character input falls through to keypress handler
|
|
253
|
+
return False
|
|
254
|
+
elif self.command_mode == CommandMode.LIVE_MODAL:
|
|
255
|
+
# LIVE_MODAL input is handled via keypress callback
|
|
256
|
+
# Character input falls through to keypress handler
|
|
257
|
+
return False
|
|
258
|
+
else:
|
|
259
|
+
# Unknown command mode, exit to normal
|
|
260
|
+
await self.exit_command_mode()
|
|
261
|
+
return False
|
|
262
|
+
|
|
263
|
+
except Exception as e:
|
|
264
|
+
logger.error(f"Error handling command mode input: {e}")
|
|
265
|
+
await self.exit_command_mode()
|
|
266
|
+
return False
|
|
267
|
+
|
|
268
|
+
async def handle_menu_popup_input(self, char: str) -> bool:
|
|
269
|
+
"""Handle input during menu popup mode.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
char: Character input to process.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
True if input was handled.
|
|
276
|
+
"""
|
|
277
|
+
# Handle special keys first
|
|
278
|
+
if ord(char) == 27: # Escape key
|
|
279
|
+
await self.exit_command_mode()
|
|
280
|
+
return True
|
|
281
|
+
elif ord(char) == 13: # Enter key
|
|
282
|
+
await self._execute_selected_command()
|
|
283
|
+
return True
|
|
284
|
+
elif ord(char) == 8 or ord(char) == 127: # Backspace or Delete
|
|
285
|
+
# If buffer only has '/', exit command mode
|
|
286
|
+
if len(self.buffer_manager.content) <= 1:
|
|
287
|
+
await self.exit_command_mode()
|
|
288
|
+
return True
|
|
289
|
+
else:
|
|
290
|
+
# Remove character and update command filter
|
|
291
|
+
self.buffer_manager.delete_char()
|
|
292
|
+
await self._update_command_filter()
|
|
293
|
+
return True
|
|
294
|
+
|
|
295
|
+
# Handle printable characters (add to command filter)
|
|
296
|
+
if char.isprintable():
|
|
297
|
+
self.buffer_manager.insert_char(char)
|
|
298
|
+
await self._update_command_filter()
|
|
299
|
+
return True
|
|
300
|
+
|
|
301
|
+
# Let other keys fall through for now
|
|
302
|
+
return False
|
|
303
|
+
|
|
304
|
+
async def handle_menu_popup_keypress(self, key_press: KeyPress) -> bool:
|
|
305
|
+
"""Handle KeyPress during menu popup mode with arrow key navigation.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
key_press: Parsed key press to process.
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
True if key was handled.
|
|
312
|
+
"""
|
|
313
|
+
try:
|
|
314
|
+
# Handle arrow key navigation
|
|
315
|
+
if key_press.name == "ArrowUp":
|
|
316
|
+
await self._navigate_menu("up")
|
|
317
|
+
return True
|
|
318
|
+
elif key_press.name == "ArrowDown":
|
|
319
|
+
await self._navigate_menu("down")
|
|
320
|
+
return True
|
|
321
|
+
elif key_press.name == "Enter":
|
|
322
|
+
await self._execute_selected_command()
|
|
323
|
+
return True
|
|
324
|
+
elif key_press.name == "Escape":
|
|
325
|
+
await self.exit_command_mode()
|
|
326
|
+
return True
|
|
327
|
+
|
|
328
|
+
# Handle printable characters (for filtering)
|
|
329
|
+
elif key_press.char and key_press.char.isprintable():
|
|
330
|
+
self.buffer_manager.insert_char(key_press.char)
|
|
331
|
+
await self._update_command_filter()
|
|
332
|
+
return True
|
|
333
|
+
|
|
334
|
+
# Handle backspace/delete
|
|
335
|
+
elif key_press.name in ["Backspace", "Delete"]:
|
|
336
|
+
# If buffer only has '/', exit command mode
|
|
337
|
+
if len(self.buffer_manager.content) <= 1:
|
|
338
|
+
await self.exit_command_mode()
|
|
339
|
+
return True
|
|
340
|
+
else:
|
|
341
|
+
# Remove character and update command filter
|
|
342
|
+
self.buffer_manager.delete_char()
|
|
343
|
+
await self._update_command_filter()
|
|
344
|
+
return True
|
|
345
|
+
|
|
346
|
+
# Other keys not handled
|
|
347
|
+
return False
|
|
348
|
+
|
|
349
|
+
except Exception as e:
|
|
350
|
+
logger.error(f"Error handling menu popup keypress: {e}")
|
|
351
|
+
await self.exit_command_mode()
|
|
352
|
+
return False
|
|
353
|
+
|
|
354
|
+
async def handle_status_takeover_input(self, char: str) -> bool:
|
|
355
|
+
"""Handle input during status area takeover mode.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
char: Character input to process.
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
True if input was handled.
|
|
362
|
+
"""
|
|
363
|
+
# For now, just handle Escape to exit
|
|
364
|
+
if ord(char) == 27: # Escape key
|
|
365
|
+
await self.exit_command_mode()
|
|
366
|
+
return True
|
|
367
|
+
|
|
368
|
+
# TODO: Implement status area navigation
|
|
369
|
+
return True
|
|
370
|
+
|
|
371
|
+
async def handle_status_takeover_keypress(self, key_press: KeyPress) -> bool:
|
|
372
|
+
"""Handle KeyPress during status area takeover mode.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
key_press: Parsed key press to process.
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
True if key was handled.
|
|
379
|
+
"""
|
|
380
|
+
# For now, just handle Escape to exit
|
|
381
|
+
if key_press.name == "Escape":
|
|
382
|
+
await self.exit_command_mode()
|
|
383
|
+
return True
|
|
384
|
+
|
|
385
|
+
# TODO: Implement status area navigation
|
|
386
|
+
return True
|
|
387
|
+
|
|
388
|
+
async def handle_status_view_previous(self) -> None:
|
|
389
|
+
"""Handle comma key press for previous status view."""
|
|
390
|
+
try:
|
|
391
|
+
logger.info("Attempting to switch to previous status view")
|
|
392
|
+
# Check if renderer has a status registry
|
|
393
|
+
if (
|
|
394
|
+
hasattr(self.renderer, "status_renderer")
|
|
395
|
+
and self.renderer.status_renderer
|
|
396
|
+
):
|
|
397
|
+
status_renderer = self.renderer.status_renderer
|
|
398
|
+
logger.info(
|
|
399
|
+
f"[ok] Found status_renderer: {type(status_renderer).__name__}"
|
|
400
|
+
)
|
|
401
|
+
if (
|
|
402
|
+
hasattr(status_renderer, "status_registry")
|
|
403
|
+
and status_renderer.status_registry
|
|
404
|
+
):
|
|
405
|
+
registry = status_renderer.status_registry
|
|
406
|
+
logger.info(
|
|
407
|
+
f"[ok] Found status_registry with {len(registry.views)} views"
|
|
408
|
+
)
|
|
409
|
+
if hasattr(registry, "cycle_previous"):
|
|
410
|
+
previous_view = registry.cycle_previous()
|
|
411
|
+
if previous_view:
|
|
412
|
+
logger.info(
|
|
413
|
+
f"[ok] Switched to previous status view: '{previous_view.name}'"
|
|
414
|
+
)
|
|
415
|
+
else:
|
|
416
|
+
logger.info("No status views available to cycle to")
|
|
417
|
+
else:
|
|
418
|
+
logger.info("cycle_previous method not found in registry")
|
|
419
|
+
else:
|
|
420
|
+
logger.info("No status registry available for view cycling")
|
|
421
|
+
else:
|
|
422
|
+
logger.info("No status renderer available for view cycling")
|
|
423
|
+
|
|
424
|
+
except Exception as e:
|
|
425
|
+
if self.error_handler:
|
|
426
|
+
from ..input_errors import ErrorType, ErrorSeverity
|
|
427
|
+
await self.error_handler.handle_error(
|
|
428
|
+
ErrorType.EVENT_ERROR,
|
|
429
|
+
f"Error handling status view previous: {e}",
|
|
430
|
+
ErrorSeverity.LOW,
|
|
431
|
+
{"key": "Ctrl+ArrowLeft"},
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
async def handle_status_view_next(self) -> None:
|
|
435
|
+
"""Handle Ctrl+Right arrow key press for next status view."""
|
|
436
|
+
try:
|
|
437
|
+
# Check if renderer has a status registry
|
|
438
|
+
if (
|
|
439
|
+
hasattr(self.renderer, "status_renderer")
|
|
440
|
+
and self.renderer.status_renderer
|
|
441
|
+
):
|
|
442
|
+
status_renderer = self.renderer.status_renderer
|
|
443
|
+
if (
|
|
444
|
+
hasattr(status_renderer, "status_registry")
|
|
445
|
+
and status_renderer.status_registry
|
|
446
|
+
):
|
|
447
|
+
next_view = status_renderer.status_registry.cycle_next()
|
|
448
|
+
if next_view:
|
|
449
|
+
logger.debug(
|
|
450
|
+
f"Switched to next status view: '{next_view.name}'"
|
|
451
|
+
)
|
|
452
|
+
else:
|
|
453
|
+
logger.debug("No status views available to cycle to")
|
|
454
|
+
else:
|
|
455
|
+
logger.debug("No status registry available for view cycling")
|
|
456
|
+
else:
|
|
457
|
+
logger.debug("No status renderer available for view cycling")
|
|
458
|
+
|
|
459
|
+
except Exception as e:
|
|
460
|
+
if self.error_handler:
|
|
461
|
+
from ..input_errors import ErrorType, ErrorSeverity
|
|
462
|
+
await self.error_handler.handle_error(
|
|
463
|
+
ErrorType.EVENT_ERROR,
|
|
464
|
+
f"Error handling status view next: {e}",
|
|
465
|
+
ErrorSeverity.LOW,
|
|
466
|
+
{"key": "Ctrl+ArrowRight"},
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
# ==================== PRIVATE HELPER METHODS ====================
|
|
470
|
+
|
|
471
|
+
async def _navigate_menu(self, direction: str) -> None:
|
|
472
|
+
"""Navigate the command menu up or down.
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
direction: "up" or "down"
|
|
476
|
+
"""
|
|
477
|
+
try:
|
|
478
|
+
# Use menu_items count from renderer (includes subcommands)
|
|
479
|
+
menu_items = self.command_menu_renderer.menu_items
|
|
480
|
+
if not menu_items:
|
|
481
|
+
return
|
|
482
|
+
|
|
483
|
+
# Update selection index
|
|
484
|
+
if direction == "up":
|
|
485
|
+
self.selected_command_index = max(0, self.selected_command_index - 1)
|
|
486
|
+
elif direction == "down":
|
|
487
|
+
self.selected_command_index = min(
|
|
488
|
+
len(menu_items) - 1, self.selected_command_index + 1
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
# Update menu renderer with new selection
|
|
492
|
+
self.command_menu_renderer.set_selected_index(
|
|
493
|
+
self.selected_command_index
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
# Re-render the menu with new selection
|
|
497
|
+
self.command_menu_renderer._render_menu()
|
|
498
|
+
|
|
499
|
+
except Exception as e:
|
|
500
|
+
logger.error(f"Error navigating menu: {e}")
|
|
501
|
+
|
|
502
|
+
async def _update_command_filter(self) -> None:
|
|
503
|
+
"""Update command menu based on current buffer content."""
|
|
504
|
+
try:
|
|
505
|
+
# Get current input (minus the leading '/')
|
|
506
|
+
current_input = self.buffer_manager.content
|
|
507
|
+
filter_text = (
|
|
508
|
+
current_input[1:] if current_input.startswith("/") else current_input
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
# Update menu renderer with filtered commands
|
|
512
|
+
filtered_commands = self._filter_commands(filter_text)
|
|
513
|
+
|
|
514
|
+
# Reset selection when filtering
|
|
515
|
+
self.selected_command_index = 0
|
|
516
|
+
self.command_menu_renderer.set_selected_index(
|
|
517
|
+
self.selected_command_index
|
|
518
|
+
)
|
|
519
|
+
self.command_menu_renderer.filter_commands(
|
|
520
|
+
filtered_commands, filter_text
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
# Emit filter update event
|
|
524
|
+
await self.event_bus.emit_with_hooks(
|
|
525
|
+
EventType.COMMAND_MENU_FILTER,
|
|
526
|
+
{
|
|
527
|
+
"filter_text": filter_text,
|
|
528
|
+
"available_commands": self._get_available_commands(),
|
|
529
|
+
"filtered_commands": filtered_commands,
|
|
530
|
+
},
|
|
531
|
+
"commands",
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
# Update display
|
|
535
|
+
if self._update_display_callback:
|
|
536
|
+
await self._update_display_callback(force_render=True)
|
|
537
|
+
|
|
538
|
+
except Exception as e:
|
|
539
|
+
logger.error(f"Error updating command filter: {e}")
|
|
540
|
+
|
|
541
|
+
async def _execute_selected_command(self) -> None:
|
|
542
|
+
"""Execute the currently selected command or insert subcommand."""
|
|
543
|
+
try:
|
|
544
|
+
# PRIORITY 1: If menu is active with a selection, use the highlighted item
|
|
545
|
+
if self.command_menu_active:
|
|
546
|
+
selected_item = self.command_menu_renderer.get_selected_command()
|
|
547
|
+
if selected_item:
|
|
548
|
+
# Check if it's a subcommand
|
|
549
|
+
if selected_item.get("is_subcommand"):
|
|
550
|
+
parent_name = selected_item.get("parent_name", "")
|
|
551
|
+
subcommand_name = selected_item.get("subcommand_name", "")
|
|
552
|
+
subcommand_args = selected_item.get("subcommand_args", "")
|
|
553
|
+
|
|
554
|
+
# If subcommand has no args, execute immediately
|
|
555
|
+
if not subcommand_args:
|
|
556
|
+
command_string = f"/{parent_name} {subcommand_name}"
|
|
557
|
+
logger.info(f"Executing no-arg subcommand: {command_string}")
|
|
558
|
+
else:
|
|
559
|
+
# Has args - insert text for user to complete
|
|
560
|
+
new_content = f"/{parent_name} {subcommand_name} "
|
|
561
|
+
|
|
562
|
+
# Update buffer with new content
|
|
563
|
+
self.buffer_manager.clear()
|
|
564
|
+
for char in new_content:
|
|
565
|
+
self.buffer_manager.insert_char(char)
|
|
566
|
+
|
|
567
|
+
logger.info(f"Inserted subcommand: {new_content}")
|
|
568
|
+
|
|
569
|
+
# Hide menu - user is now typing arguments
|
|
570
|
+
self.command_menu_renderer.hide_menu()
|
|
571
|
+
self.command_menu_active = False
|
|
572
|
+
|
|
573
|
+
if self._update_display_callback:
|
|
574
|
+
await self._update_display_callback(force_render=True)
|
|
575
|
+
return
|
|
576
|
+
else:
|
|
577
|
+
# Regular command - execute it
|
|
578
|
+
# But preserve any arguments the user typed
|
|
579
|
+
buffer_content = self.buffer_manager.content
|
|
580
|
+
# Extract args from buffer (everything after first space)
|
|
581
|
+
if " " in buffer_content:
|
|
582
|
+
args_part = buffer_content.split(" ", 1)[1]
|
|
583
|
+
command_string = f"/{selected_item['name']} {args_part}"
|
|
584
|
+
else:
|
|
585
|
+
command_string = f"/{selected_item['name']}"
|
|
586
|
+
logger.info(f"Executing highlighted menu command: {command_string}")
|
|
587
|
+
else:
|
|
588
|
+
# No menu selection - fall through to parse buffer content
|
|
589
|
+
# User may have typed valid command with args that don't match filter
|
|
590
|
+
command_string = self.buffer_manager.content
|
|
591
|
+
if not command_string or command_string == "/":
|
|
592
|
+
logger.warning("Menu active but no command to execute")
|
|
593
|
+
await self.exit_command_mode()
|
|
594
|
+
return
|
|
595
|
+
else:
|
|
596
|
+
# FALLBACK: Menu not active, use buffer content
|
|
597
|
+
command_string = self.buffer_manager.content
|
|
598
|
+
if not command_string or command_string == "/":
|
|
599
|
+
logger.warning("No command to execute")
|
|
600
|
+
await self.exit_command_mode()
|
|
601
|
+
return
|
|
602
|
+
|
|
603
|
+
# Parse the command
|
|
604
|
+
command = self.slash_parser.parse_command(command_string)
|
|
605
|
+
if command:
|
|
606
|
+
logger.info(f"Executing selected command: {command.name}")
|
|
607
|
+
|
|
608
|
+
# Exit command mode first
|
|
609
|
+
await self.exit_command_mode()
|
|
610
|
+
|
|
611
|
+
# Execute the command
|
|
612
|
+
result = await self.command_executor.execute_command(
|
|
613
|
+
command, self.event_bus
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
# Handle the result
|
|
617
|
+
if result.success:
|
|
618
|
+
logger.info(f"Command {command.name} completed successfully")
|
|
619
|
+
|
|
620
|
+
# Modal display is handled by event bus trigger, not here
|
|
621
|
+
if result.message:
|
|
622
|
+
# Display success message in status area
|
|
623
|
+
logger.info(f"Command result: {result.message}")
|
|
624
|
+
# TODO: Display in status area
|
|
625
|
+
else:
|
|
626
|
+
logger.warning(
|
|
627
|
+
f"Command {command.name} failed: {result.message}"
|
|
628
|
+
)
|
|
629
|
+
# TODO: Display error message in status area
|
|
630
|
+
else:
|
|
631
|
+
logger.warning("Failed to parse selected command")
|
|
632
|
+
await self.exit_command_mode()
|
|
633
|
+
|
|
634
|
+
except Exception as e:
|
|
635
|
+
logger.error(f"Error executing command: {e}")
|
|
636
|
+
await self.exit_command_mode()
|
|
637
|
+
|
|
638
|
+
def _get_available_commands(self) -> List[Dict[str, Any]]:
|
|
639
|
+
"""Get list of available commands for menu display.
|
|
640
|
+
|
|
641
|
+
Returns:
|
|
642
|
+
List of command dictionaries for menu rendering.
|
|
643
|
+
"""
|
|
644
|
+
commands = []
|
|
645
|
+
command_defs = self.command_registry.list_commands()
|
|
646
|
+
|
|
647
|
+
for cmd_def in command_defs:
|
|
648
|
+
# Convert subcommands to dicts for JSON serialization
|
|
649
|
+
subcommands = []
|
|
650
|
+
if cmd_def.subcommands:
|
|
651
|
+
for sub in cmd_def.subcommands:
|
|
652
|
+
subcommands.append({
|
|
653
|
+
"name": sub.name,
|
|
654
|
+
"args": sub.args,
|
|
655
|
+
"description": sub.description,
|
|
656
|
+
})
|
|
657
|
+
|
|
658
|
+
commands.append(
|
|
659
|
+
{
|
|
660
|
+
"name": cmd_def.name,
|
|
661
|
+
"description": cmd_def.description,
|
|
662
|
+
"aliases": cmd_def.aliases,
|
|
663
|
+
"category": cmd_def.category.value,
|
|
664
|
+
"plugin": cmd_def.plugin_name,
|
|
665
|
+
"icon": cmd_def.icon,
|
|
666
|
+
"subcommands": subcommands,
|
|
667
|
+
}
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
return commands
|
|
671
|
+
|
|
672
|
+
def _filter_commands(self, filter_text: str) -> List[Dict[str, Any]]:
|
|
673
|
+
"""Filter commands based on input text.
|
|
674
|
+
|
|
675
|
+
Args:
|
|
676
|
+
filter_text: Text to filter commands by.
|
|
677
|
+
|
|
678
|
+
Returns:
|
|
679
|
+
List of filtered command dictionaries.
|
|
680
|
+
"""
|
|
681
|
+
if not filter_text:
|
|
682
|
+
return self._get_available_commands()
|
|
683
|
+
|
|
684
|
+
# Use registry search functionality
|
|
685
|
+
matching_defs = self.command_registry.search_commands(filter_text)
|
|
686
|
+
|
|
687
|
+
filtered_commands = []
|
|
688
|
+
for cmd_def in matching_defs:
|
|
689
|
+
# Convert subcommands to dicts for JSON serialization
|
|
690
|
+
subcommands = []
|
|
691
|
+
if cmd_def.subcommands:
|
|
692
|
+
for sub in cmd_def.subcommands:
|
|
693
|
+
subcommands.append({
|
|
694
|
+
"name": sub.name,
|
|
695
|
+
"args": sub.args,
|
|
696
|
+
"description": sub.description,
|
|
697
|
+
})
|
|
698
|
+
|
|
699
|
+
filtered_commands.append(
|
|
700
|
+
{
|
|
701
|
+
"name": cmd_def.name,
|
|
702
|
+
"description": cmd_def.description,
|
|
703
|
+
"aliases": cmd_def.aliases,
|
|
704
|
+
"category": cmd_def.category.value,
|
|
705
|
+
"plugin": cmd_def.plugin_name,
|
|
706
|
+
"icon": cmd_def.icon,
|
|
707
|
+
"subcommands": subcommands,
|
|
708
|
+
}
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
return filtered_commands
|