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.
Files changed (192) hide show
  1. agents/__init__.py +2 -0
  2. agents/coder/__init__.py +0 -0
  3. agents/coder/agent.json +4 -0
  4. agents/coder/api-integration.md +2150 -0
  5. agents/coder/cli-pretty.md +765 -0
  6. agents/coder/code-review.md +1092 -0
  7. agents/coder/database-design.md +1525 -0
  8. agents/coder/debugging.md +1102 -0
  9. agents/coder/dependency-management.md +1397 -0
  10. agents/coder/git-workflow.md +1099 -0
  11. agents/coder/refactoring.md +1454 -0
  12. agents/coder/security-hardening.md +1732 -0
  13. agents/coder/system_prompt.md +1448 -0
  14. agents/coder/tdd.md +1367 -0
  15. agents/creative-writer/__init__.py +0 -0
  16. agents/creative-writer/agent.json +4 -0
  17. agents/creative-writer/character-development.md +1852 -0
  18. agents/creative-writer/dialogue-craft.md +1122 -0
  19. agents/creative-writer/plot-structure.md +1073 -0
  20. agents/creative-writer/revision-editing.md +1484 -0
  21. agents/creative-writer/system_prompt.md +690 -0
  22. agents/creative-writer/worldbuilding.md +2049 -0
  23. agents/data-analyst/__init__.py +30 -0
  24. agents/data-analyst/agent.json +4 -0
  25. agents/data-analyst/data-visualization.md +992 -0
  26. agents/data-analyst/exploratory-data-analysis.md +1110 -0
  27. agents/data-analyst/pandas-data-manipulation.md +1081 -0
  28. agents/data-analyst/sql-query-optimization.md +881 -0
  29. agents/data-analyst/statistical-analysis.md +1118 -0
  30. agents/data-analyst/system_prompt.md +928 -0
  31. agents/default/__init__.py +0 -0
  32. agents/default/agent.json +4 -0
  33. agents/default/dead-code.md +794 -0
  34. agents/default/explore-agent-system.md +585 -0
  35. agents/default/system_prompt.md +1448 -0
  36. agents/kollabor/__init__.py +0 -0
  37. agents/kollabor/analyze-plugin-lifecycle.md +175 -0
  38. agents/kollabor/analyze-terminal-rendering.md +388 -0
  39. agents/kollabor/code-review.md +1092 -0
  40. agents/kollabor/debug-mcp-integration.md +521 -0
  41. agents/kollabor/debug-plugin-hooks.md +547 -0
  42. agents/kollabor/debugging.md +1102 -0
  43. agents/kollabor/dependency-management.md +1397 -0
  44. agents/kollabor/git-workflow.md +1099 -0
  45. agents/kollabor/inspect-llm-conversation.md +148 -0
  46. agents/kollabor/monitor-event-bus.md +558 -0
  47. agents/kollabor/profile-performance.md +576 -0
  48. agents/kollabor/refactoring.md +1454 -0
  49. agents/kollabor/system_prompt copy.md +1448 -0
  50. agents/kollabor/system_prompt.md +757 -0
  51. agents/kollabor/trace-command-execution.md +178 -0
  52. agents/kollabor/validate-config.md +879 -0
  53. agents/research/__init__.py +0 -0
  54. agents/research/agent.json +4 -0
  55. agents/research/architecture-mapping.md +1099 -0
  56. agents/research/codebase-analysis.md +1077 -0
  57. agents/research/dependency-audit.md +1027 -0
  58. agents/research/performance-profiling.md +1047 -0
  59. agents/research/security-review.md +1359 -0
  60. agents/research/system_prompt.md +492 -0
  61. agents/technical-writer/__init__.py +0 -0
  62. agents/technical-writer/agent.json +4 -0
  63. agents/technical-writer/api-documentation.md +2328 -0
  64. agents/technical-writer/changelog-management.md +1181 -0
  65. agents/technical-writer/readme-writing.md +1360 -0
  66. agents/technical-writer/style-guide.md +1410 -0
  67. agents/technical-writer/system_prompt.md +653 -0
  68. agents/technical-writer/tutorial-creation.md +1448 -0
  69. core/__init__.py +0 -2
  70. core/application.py +343 -88
  71. core/cli.py +229 -10
  72. core/commands/menu_renderer.py +463 -59
  73. core/commands/registry.py +14 -9
  74. core/commands/system_commands.py +2461 -14
  75. core/config/loader.py +151 -37
  76. core/config/service.py +18 -6
  77. core/events/bus.py +29 -9
  78. core/events/executor.py +205 -75
  79. core/events/models.py +27 -8
  80. core/fullscreen/command_integration.py +20 -24
  81. core/fullscreen/components/__init__.py +10 -1
  82. core/fullscreen/components/matrix_components.py +1 -2
  83. core/fullscreen/components/space_shooter_components.py +654 -0
  84. core/fullscreen/plugin.py +5 -0
  85. core/fullscreen/renderer.py +52 -13
  86. core/fullscreen/session.py +52 -15
  87. core/io/__init__.py +29 -5
  88. core/io/buffer_manager.py +6 -1
  89. core/io/config_status_view.py +7 -29
  90. core/io/core_status_views.py +267 -347
  91. core/io/input/__init__.py +25 -0
  92. core/io/input/command_mode_handler.py +711 -0
  93. core/io/input/display_controller.py +128 -0
  94. core/io/input/hook_registrar.py +286 -0
  95. core/io/input/input_loop_manager.py +421 -0
  96. core/io/input/key_press_handler.py +502 -0
  97. core/io/input/modal_controller.py +1011 -0
  98. core/io/input/paste_processor.py +339 -0
  99. core/io/input/status_modal_renderer.py +184 -0
  100. core/io/input_errors.py +5 -1
  101. core/io/input_handler.py +211 -2452
  102. core/io/key_parser.py +7 -0
  103. core/io/layout.py +15 -3
  104. core/io/message_coordinator.py +111 -2
  105. core/io/message_renderer.py +129 -4
  106. core/io/status_renderer.py +147 -607
  107. core/io/terminal_renderer.py +97 -51
  108. core/io/terminal_state.py +21 -4
  109. core/io/visual_effects.py +816 -165
  110. core/llm/agent_manager.py +1063 -0
  111. core/llm/api_adapters/__init__.py +44 -0
  112. core/llm/api_adapters/anthropic_adapter.py +432 -0
  113. core/llm/api_adapters/base.py +241 -0
  114. core/llm/api_adapters/openai_adapter.py +326 -0
  115. core/llm/api_communication_service.py +167 -113
  116. core/llm/conversation_logger.py +322 -16
  117. core/llm/conversation_manager.py +556 -30
  118. core/llm/file_operations_executor.py +84 -32
  119. core/llm/llm_service.py +934 -103
  120. core/llm/mcp_integration.py +541 -57
  121. core/llm/message_display_service.py +135 -18
  122. core/llm/plugin_sdk.py +1 -2
  123. core/llm/profile_manager.py +1183 -0
  124. core/llm/response_parser.py +274 -56
  125. core/llm/response_processor.py +16 -3
  126. core/llm/tool_executor.py +6 -1
  127. core/logging/__init__.py +2 -0
  128. core/logging/setup.py +34 -6
  129. core/models/resume.py +54 -0
  130. core/plugins/__init__.py +4 -2
  131. core/plugins/base.py +127 -0
  132. core/plugins/collector.py +23 -161
  133. core/plugins/discovery.py +37 -3
  134. core/plugins/factory.py +6 -12
  135. core/plugins/registry.py +5 -17
  136. core/ui/config_widgets.py +128 -28
  137. core/ui/live_modal_renderer.py +2 -1
  138. core/ui/modal_actions.py +5 -0
  139. core/ui/modal_overlay_renderer.py +0 -60
  140. core/ui/modal_renderer.py +268 -7
  141. core/ui/modal_state_manager.py +29 -4
  142. core/ui/widgets/base_widget.py +7 -0
  143. core/updates/__init__.py +10 -0
  144. core/updates/version_check_service.py +348 -0
  145. core/updates/version_comparator.py +103 -0
  146. core/utils/config_utils.py +685 -526
  147. core/utils/plugin_utils.py +1 -1
  148. core/utils/session_naming.py +111 -0
  149. fonts/LICENSE +21 -0
  150. fonts/README.md +46 -0
  151. fonts/SymbolsNerdFont-Regular.ttf +0 -0
  152. fonts/SymbolsNerdFontMono-Regular.ttf +0 -0
  153. fonts/__init__.py +44 -0
  154. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/METADATA +54 -4
  155. kollabor-0.4.15.dist-info/RECORD +228 -0
  156. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/top_level.txt +2 -0
  157. plugins/agent_orchestrator/__init__.py +39 -0
  158. plugins/agent_orchestrator/activity_monitor.py +181 -0
  159. plugins/agent_orchestrator/file_attacher.py +77 -0
  160. plugins/agent_orchestrator/message_injector.py +135 -0
  161. plugins/agent_orchestrator/models.py +48 -0
  162. plugins/agent_orchestrator/orchestrator.py +403 -0
  163. plugins/agent_orchestrator/plugin.py +976 -0
  164. plugins/agent_orchestrator/xml_parser.py +191 -0
  165. plugins/agent_orchestrator_plugin.py +9 -0
  166. plugins/enhanced_input/box_styles.py +1 -0
  167. plugins/enhanced_input/color_engine.py +19 -4
  168. plugins/enhanced_input/config.py +2 -2
  169. plugins/enhanced_input_plugin.py +61 -11
  170. plugins/fullscreen/__init__.py +6 -2
  171. plugins/fullscreen/example_plugin.py +1035 -222
  172. plugins/fullscreen/setup_wizard_plugin.py +592 -0
  173. plugins/fullscreen/space_shooter_plugin.py +131 -0
  174. plugins/hook_monitoring_plugin.py +436 -78
  175. plugins/query_enhancer_plugin.py +66 -30
  176. plugins/resume_conversation_plugin.py +1494 -0
  177. plugins/save_conversation_plugin.py +98 -32
  178. plugins/system_commands_plugin.py +70 -56
  179. plugins/tmux_plugin.py +154 -78
  180. plugins/workflow_enforcement_plugin.py +94 -92
  181. system_prompt/default.md +952 -886
  182. core/io/input_mode_manager.py +0 -402
  183. core/io/modal_interaction_handler.py +0 -315
  184. core/io/raw_input_processor.py +0 -946
  185. core/storage/__init__.py +0 -5
  186. core/storage/state_manager.py +0 -84
  187. core/ui/widget_integration.py +0 -222
  188. core/utils/key_reader.py +0 -171
  189. kollabor-0.4.9.dist-info/RECORD +0 -128
  190. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/WHEEL +0 -0
  191. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/entry_points.txt +0 -0
  192. {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