kollabor 0.4.9__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.
- core/__init__.py +18 -0
- core/application.py +578 -0
- core/cli.py +193 -0
- core/commands/__init__.py +43 -0
- core/commands/executor.py +277 -0
- core/commands/menu_renderer.py +319 -0
- core/commands/parser.py +186 -0
- core/commands/registry.py +331 -0
- core/commands/system_commands.py +479 -0
- core/config/__init__.py +7 -0
- core/config/llm_task_config.py +110 -0
- core/config/loader.py +501 -0
- core/config/manager.py +112 -0
- core/config/plugin_config_manager.py +346 -0
- core/config/plugin_schema.py +424 -0
- core/config/service.py +399 -0
- core/effects/__init__.py +1 -0
- core/events/__init__.py +12 -0
- core/events/bus.py +129 -0
- core/events/executor.py +154 -0
- core/events/models.py +258 -0
- core/events/processor.py +176 -0
- core/events/registry.py +289 -0
- core/fullscreen/__init__.py +19 -0
- core/fullscreen/command_integration.py +290 -0
- core/fullscreen/components/__init__.py +12 -0
- core/fullscreen/components/animation.py +258 -0
- core/fullscreen/components/drawing.py +160 -0
- core/fullscreen/components/matrix_components.py +177 -0
- core/fullscreen/manager.py +302 -0
- core/fullscreen/plugin.py +204 -0
- core/fullscreen/renderer.py +282 -0
- core/fullscreen/session.py +324 -0
- core/io/__init__.py +52 -0
- core/io/buffer_manager.py +362 -0
- core/io/config_status_view.py +272 -0
- core/io/core_status_views.py +410 -0
- core/io/input_errors.py +313 -0
- core/io/input_handler.py +2655 -0
- core/io/input_mode_manager.py +402 -0
- core/io/key_parser.py +344 -0
- core/io/layout.py +587 -0
- core/io/message_coordinator.py +204 -0
- core/io/message_renderer.py +601 -0
- core/io/modal_interaction_handler.py +315 -0
- core/io/raw_input_processor.py +946 -0
- core/io/status_renderer.py +845 -0
- core/io/terminal_renderer.py +586 -0
- core/io/terminal_state.py +551 -0
- core/io/visual_effects.py +734 -0
- core/llm/__init__.py +26 -0
- core/llm/api_communication_service.py +863 -0
- core/llm/conversation_logger.py +473 -0
- core/llm/conversation_manager.py +414 -0
- core/llm/file_operations_executor.py +1401 -0
- core/llm/hook_system.py +402 -0
- core/llm/llm_service.py +1629 -0
- core/llm/mcp_integration.py +386 -0
- core/llm/message_display_service.py +450 -0
- core/llm/model_router.py +214 -0
- core/llm/plugin_sdk.py +396 -0
- core/llm/response_parser.py +848 -0
- core/llm/response_processor.py +364 -0
- core/llm/tool_executor.py +520 -0
- core/logging/__init__.py +19 -0
- core/logging/setup.py +208 -0
- core/models/__init__.py +5 -0
- core/models/base.py +23 -0
- core/plugins/__init__.py +13 -0
- core/plugins/collector.py +212 -0
- core/plugins/discovery.py +386 -0
- core/plugins/factory.py +263 -0
- core/plugins/registry.py +152 -0
- core/storage/__init__.py +5 -0
- core/storage/state_manager.py +84 -0
- core/ui/__init__.py +6 -0
- core/ui/config_merger.py +176 -0
- core/ui/config_widgets.py +369 -0
- core/ui/live_modal_renderer.py +276 -0
- core/ui/modal_actions.py +162 -0
- core/ui/modal_overlay_renderer.py +373 -0
- core/ui/modal_renderer.py +591 -0
- core/ui/modal_state_manager.py +443 -0
- core/ui/widget_integration.py +222 -0
- core/ui/widgets/__init__.py +27 -0
- core/ui/widgets/base_widget.py +136 -0
- core/ui/widgets/checkbox.py +85 -0
- core/ui/widgets/dropdown.py +140 -0
- core/ui/widgets/label.py +78 -0
- core/ui/widgets/slider.py +185 -0
- core/ui/widgets/text_input.py +224 -0
- core/utils/__init__.py +11 -0
- core/utils/config_utils.py +656 -0
- core/utils/dict_utils.py +212 -0
- core/utils/error_utils.py +275 -0
- core/utils/key_reader.py +171 -0
- core/utils/plugin_utils.py +267 -0
- core/utils/prompt_renderer.py +151 -0
- kollabor-0.4.9.dist-info/METADATA +298 -0
- kollabor-0.4.9.dist-info/RECORD +128 -0
- kollabor-0.4.9.dist-info/WHEEL +5 -0
- kollabor-0.4.9.dist-info/entry_points.txt +2 -0
- kollabor-0.4.9.dist-info/licenses/LICENSE +21 -0
- kollabor-0.4.9.dist-info/top_level.txt +4 -0
- kollabor_cli_main.py +20 -0
- plugins/__init__.py +1 -0
- plugins/enhanced_input/__init__.py +18 -0
- plugins/enhanced_input/box_renderer.py +103 -0
- plugins/enhanced_input/box_styles.py +142 -0
- plugins/enhanced_input/color_engine.py +165 -0
- plugins/enhanced_input/config.py +150 -0
- plugins/enhanced_input/cursor_manager.py +72 -0
- plugins/enhanced_input/geometry.py +81 -0
- plugins/enhanced_input/state.py +130 -0
- plugins/enhanced_input/text_processor.py +115 -0
- plugins/enhanced_input_plugin.py +385 -0
- plugins/fullscreen/__init__.py +9 -0
- plugins/fullscreen/example_plugin.py +327 -0
- plugins/fullscreen/matrix_plugin.py +132 -0
- plugins/hook_monitoring_plugin.py +1299 -0
- plugins/query_enhancer_plugin.py +350 -0
- plugins/save_conversation_plugin.py +502 -0
- plugins/system_commands_plugin.py +93 -0
- plugins/tmux_plugin.py +795 -0
- plugins/workflow_enforcement_plugin.py +629 -0
- system_prompt/default.md +1286 -0
- system_prompt/default_win.md +265 -0
- system_prompt/example_with_trender.md +47 -0
core/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Core components for Kollabor CLI."""
|
|
2
|
+
|
|
3
|
+
# Import all subsystems for easy access
|
|
4
|
+
from .config import ConfigManager
|
|
5
|
+
from .events import EventBus, Event, EventType, Hook, HookStatus, HookPriority
|
|
6
|
+
from .io import InputHandler, TerminalRenderer
|
|
7
|
+
from .plugins import PluginRegistry
|
|
8
|
+
from .storage import StateManager
|
|
9
|
+
from .models import ConversationMessage
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
'ConfigManager',
|
|
13
|
+
'EventBus', 'Event', 'EventType', 'Hook', 'HookStatus', 'HookPriority',
|
|
14
|
+
'InputHandler', 'TerminalRenderer',
|
|
15
|
+
'PluginRegistry',
|
|
16
|
+
'StateManager',
|
|
17
|
+
'ConversationMessage'
|
|
18
|
+
]
|
core/application.py
ADDED
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
"""Main application orchestrator for Kollabor CLI."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from importlib.metadata import version as get_version, PackageNotFoundError
|
|
9
|
+
|
|
10
|
+
from .config import ConfigService
|
|
11
|
+
|
|
12
|
+
# Get version from package metadata (always authoritative)
|
|
13
|
+
try:
|
|
14
|
+
__version__ = get_version("kollabor")
|
|
15
|
+
except PackageNotFoundError:
|
|
16
|
+
__version__ = "0.4.7" # Fallback for development mode
|
|
17
|
+
from .events import EventBus
|
|
18
|
+
from .io import InputHandler, TerminalRenderer
|
|
19
|
+
from .io.visual_effects import VisualEffects
|
|
20
|
+
from .llm import LLMService, KollaborConversationLogger, LLMHookSystem, MCPIntegration, KollaborPluginSDK
|
|
21
|
+
from .logging import setup_from_config
|
|
22
|
+
from .plugins import PluginRegistry
|
|
23
|
+
from .storage import StateManager
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TerminalLLMChat:
|
|
29
|
+
"""Main Kollabor CLI application.
|
|
30
|
+
|
|
31
|
+
Orchestrates all components including rendering, input handling,
|
|
32
|
+
event processing, and plugin management.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self) -> None:
|
|
36
|
+
"""Initialize the chat application."""
|
|
37
|
+
# Get configuration directory using standard resolution
|
|
38
|
+
from .utils.config_utils import get_config_directory, ensure_config_directory, initialize_system_prompt
|
|
39
|
+
|
|
40
|
+
self.config_dir = ensure_config_directory()
|
|
41
|
+
logger.info(f"Using config directory: {self.config_dir}")
|
|
42
|
+
|
|
43
|
+
# Initialize system prompt (copies default.md to config directories)
|
|
44
|
+
initialize_system_prompt()
|
|
45
|
+
|
|
46
|
+
# Flag to indicate if we're in pipe mode (for plugins to check)
|
|
47
|
+
self.pipe_mode = False
|
|
48
|
+
|
|
49
|
+
# Initialize plugin registry and discover plugins
|
|
50
|
+
# Try package installation directory first (for pip install), then cwd (for development)
|
|
51
|
+
package_dir = Path(__file__).parent.parent # Go up from core/ to package root
|
|
52
|
+
plugins_dir = package_dir / "plugins"
|
|
53
|
+
if not plugins_dir.exists():
|
|
54
|
+
plugins_dir = Path.cwd() / "plugins" # Fallback for development mode
|
|
55
|
+
logger.info(f"Using development plugins directory: {plugins_dir}")
|
|
56
|
+
else:
|
|
57
|
+
logger.info(f"Using installed package plugins directory: {plugins_dir}")
|
|
58
|
+
|
|
59
|
+
self.plugin_registry = PluginRegistry(plugins_dir)
|
|
60
|
+
self.plugin_registry.load_all_plugins()
|
|
61
|
+
|
|
62
|
+
# Initialize configuration service with plugin registry
|
|
63
|
+
self.config = ConfigService(self.config_dir / "config.json", self.plugin_registry)
|
|
64
|
+
|
|
65
|
+
# Update config file with plugin configurations
|
|
66
|
+
self.config.update_from_plugins()
|
|
67
|
+
|
|
68
|
+
# Reconfigure logging now that config system is available
|
|
69
|
+
setup_from_config(self.config.config_manager.config)
|
|
70
|
+
|
|
71
|
+
# Initialize core components
|
|
72
|
+
self.state_manager = StateManager(str(self.config_dir / "state.db"))
|
|
73
|
+
self.event_bus = EventBus()
|
|
74
|
+
|
|
75
|
+
# Initialize status view registry for flexible status display
|
|
76
|
+
from .io.status_renderer import StatusViewRegistry
|
|
77
|
+
from .io.config_status_view import ConfigStatusView
|
|
78
|
+
self.status_registry = StatusViewRegistry(self.event_bus)
|
|
79
|
+
|
|
80
|
+
# Add config status view to registry
|
|
81
|
+
config_status_view = ConfigStatusView(self.config, self.event_bus)
|
|
82
|
+
config_view_config = config_status_view.get_status_view_config()
|
|
83
|
+
self.status_registry.register_status_view("core", config_view_config)
|
|
84
|
+
|
|
85
|
+
# Initialize renderer with status registry and config
|
|
86
|
+
self.renderer = TerminalRenderer(self.event_bus, self.config)
|
|
87
|
+
if hasattr(self.renderer, 'status_renderer'):
|
|
88
|
+
self.renderer.status_renderer.status_registry = self.status_registry
|
|
89
|
+
|
|
90
|
+
self.input_handler = InputHandler(self.event_bus, self.renderer, self.config)
|
|
91
|
+
|
|
92
|
+
# Give terminal renderer access to input handler for modal state checking
|
|
93
|
+
self.renderer.input_handler = self.input_handler
|
|
94
|
+
|
|
95
|
+
# Initialize visual effects system
|
|
96
|
+
self.visual_effects = VisualEffects()
|
|
97
|
+
|
|
98
|
+
# Initialize slash command system
|
|
99
|
+
logger.info("About to initialize slash command system")
|
|
100
|
+
self._initialize_slash_commands()
|
|
101
|
+
|
|
102
|
+
# Initialize fullscreen plugin commands
|
|
103
|
+
self._initialize_fullscreen_commands()
|
|
104
|
+
logger.info("Slash command system initialization completed")
|
|
105
|
+
|
|
106
|
+
# Initialize LLM core service components
|
|
107
|
+
conversations_dir = self.config_dir / "conversations"
|
|
108
|
+
conversations_dir.mkdir(parents=True, exist_ok=True)
|
|
109
|
+
self.conversation_logger = KollaborConversationLogger(conversations_dir)
|
|
110
|
+
self.llm_hook_system = LLMHookSystem(self.event_bus)
|
|
111
|
+
self.mcp_integration = MCPIntegration()
|
|
112
|
+
self.plugin_sdk = KollaborPluginSDK()
|
|
113
|
+
self.llm_service = LLMService(
|
|
114
|
+
config=self.config,
|
|
115
|
+
state_manager=self.state_manager,
|
|
116
|
+
event_bus=self.event_bus,
|
|
117
|
+
renderer=self.renderer
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Configure renderer with thinking effect and shimmer parameters
|
|
121
|
+
thinking_effect = self.config.get("terminal.thinking_effect", "shimmer")
|
|
122
|
+
shimmer_speed = self.config.get("terminal.shimmer_speed", 3)
|
|
123
|
+
shimmer_wave_width = self.config.get("terminal.shimmer_wave_width", 4)
|
|
124
|
+
thinking_limit = self.config.get("terminal.thinking_message_limit", 2)
|
|
125
|
+
|
|
126
|
+
self.renderer.set_thinking_effect(thinking_effect)
|
|
127
|
+
self.renderer.configure_shimmer(shimmer_speed, shimmer_wave_width)
|
|
128
|
+
self.renderer.configure_thinking_limit(thinking_limit)
|
|
129
|
+
|
|
130
|
+
# Dynamically instantiate all discovered plugins
|
|
131
|
+
self.plugin_instances = self.plugin_registry.instantiate_plugins(
|
|
132
|
+
self.state_manager, self.event_bus, self.renderer, self.config
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Task tracking for race condition prevention
|
|
136
|
+
self.running = False
|
|
137
|
+
self._startup_complete = False
|
|
138
|
+
self._background_tasks = []
|
|
139
|
+
self._task_lock = asyncio.Lock()
|
|
140
|
+
|
|
141
|
+
logger.info("Kollabor CLI initialized")
|
|
142
|
+
|
|
143
|
+
async def start(self) -> None:
|
|
144
|
+
"""Start the chat application with guaranteed cleanup."""
|
|
145
|
+
# Display startup messages using config
|
|
146
|
+
self._display_startup_messages()
|
|
147
|
+
|
|
148
|
+
logger.info("Application starting")
|
|
149
|
+
|
|
150
|
+
render_task = None
|
|
151
|
+
input_task = None
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
# Initialize LLM core service
|
|
155
|
+
await self._initialize_llm_core()
|
|
156
|
+
|
|
157
|
+
# Initialize all plugins dynamically
|
|
158
|
+
await self._initialize_plugins()
|
|
159
|
+
|
|
160
|
+
# Register default core status views
|
|
161
|
+
await self._register_core_status_views()
|
|
162
|
+
|
|
163
|
+
# Mark startup as complete
|
|
164
|
+
self._startup_complete = True
|
|
165
|
+
logger.info("Application startup complete")
|
|
166
|
+
|
|
167
|
+
# Start main loops with task tracking
|
|
168
|
+
self.running = True
|
|
169
|
+
render_task = self.create_background_task(
|
|
170
|
+
self._render_loop(), "render_loop"
|
|
171
|
+
)
|
|
172
|
+
input_task = self.create_background_task(
|
|
173
|
+
self.input_handler.start(), "input_handler"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Wait for completion
|
|
177
|
+
await asyncio.gather(render_task, input_task)
|
|
178
|
+
|
|
179
|
+
except KeyboardInterrupt:
|
|
180
|
+
print("\r\n")
|
|
181
|
+
# print("\r\nInterrupted by user")
|
|
182
|
+
logger.info("Application interrupted by user")
|
|
183
|
+
except Exception as e:
|
|
184
|
+
logger.error(f"Application error during startup: {e}")
|
|
185
|
+
raise
|
|
186
|
+
finally:
|
|
187
|
+
# Guaranteed cleanup - always runs regardless of how we exit
|
|
188
|
+
logger.info("Executing guaranteed cleanup")
|
|
189
|
+
await self.cleanup()
|
|
190
|
+
|
|
191
|
+
async def start_pipe_mode(self, piped_input: str, timeout: int = 120) -> None:
|
|
192
|
+
"""Start in pipe mode: process input and exit after response.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
piped_input: Input text from stdin/pipe
|
|
196
|
+
timeout: Maximum time to wait for processing in seconds (default: 120)
|
|
197
|
+
"""
|
|
198
|
+
# Set a flag to indicate we're in pipe mode (plugins can check this)
|
|
199
|
+
self.pipe_mode = True
|
|
200
|
+
self.renderer.pipe_mode = True # Also set on renderer for llm_service access
|
|
201
|
+
# Propagate pipe_mode to message renderer and conversation renderer
|
|
202
|
+
if hasattr(self.renderer, 'message_renderer'):
|
|
203
|
+
self.renderer.message_renderer.pipe_mode = True
|
|
204
|
+
if hasattr(self.renderer.message_renderer, 'conversation_renderer'):
|
|
205
|
+
self.renderer.message_renderer.conversation_renderer.pipe_mode = True
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
# Initialize LLM core service
|
|
209
|
+
await self._initialize_llm_core()
|
|
210
|
+
|
|
211
|
+
# Initialize plugins (they should check self.pipe_mode if needed)
|
|
212
|
+
await self._initialize_plugins()
|
|
213
|
+
|
|
214
|
+
# Mark startup as complete
|
|
215
|
+
self._startup_complete = True
|
|
216
|
+
self.running = True
|
|
217
|
+
logger.info("Pipe mode initialized with plugins")
|
|
218
|
+
|
|
219
|
+
# Send input to LLM and wait for response
|
|
220
|
+
# The LLM service will handle the response display
|
|
221
|
+
await self.llm_service.process_user_input(piped_input)
|
|
222
|
+
|
|
223
|
+
# Wait for processing to start (max 10 seconds)
|
|
224
|
+
start_timeout = 10
|
|
225
|
+
start_wait = 0
|
|
226
|
+
while not self.llm_service.is_processing and start_wait < start_timeout:
|
|
227
|
+
await asyncio.sleep(0.1)
|
|
228
|
+
start_wait += 0.1
|
|
229
|
+
|
|
230
|
+
# Wait for processing to complete (including all tool calls and continuations)
|
|
231
|
+
max_wait = timeout
|
|
232
|
+
wait_time = 0
|
|
233
|
+
while self.llm_service.is_processing and not self.llm_service.cancel_processing and wait_time < max_wait:
|
|
234
|
+
await asyncio.sleep(0.1)
|
|
235
|
+
wait_time += 0.1
|
|
236
|
+
|
|
237
|
+
# Give a tiny bit of extra time for final display rendering
|
|
238
|
+
await asyncio.sleep(0.2)
|
|
239
|
+
|
|
240
|
+
logger.info("Pipe mode processing complete")
|
|
241
|
+
|
|
242
|
+
except KeyboardInterrupt:
|
|
243
|
+
logger.info("Pipe mode interrupted by user")
|
|
244
|
+
except Exception as e:
|
|
245
|
+
logger.error(f"Pipe mode error: {e}")
|
|
246
|
+
import traceback
|
|
247
|
+
traceback.print_exc()
|
|
248
|
+
raise
|
|
249
|
+
finally:
|
|
250
|
+
# Cleanup
|
|
251
|
+
self.running = False
|
|
252
|
+
# Keep pipe_mode=True during cleanup so cancellation messages can be suppressed
|
|
253
|
+
await self.cleanup()
|
|
254
|
+
# DON'T reset pipe_mode here - let main.py's finally block check it to avoid double cleanup
|
|
255
|
+
|
|
256
|
+
def _display_startup_messages(self) -> None:
|
|
257
|
+
"""Display startup messages with plugin information."""
|
|
258
|
+
# Display Kollabor banner with version from package metadata
|
|
259
|
+
kollabor_banner = self.renderer.create_kollabor_banner(f"v{__version__}")
|
|
260
|
+
print(kollabor_banner)
|
|
261
|
+
|
|
262
|
+
# LLM Core status
|
|
263
|
+
#print(f"\033[2;35mLLM Core: \033[2;32mActive\033[0m")
|
|
264
|
+
|
|
265
|
+
# Plugin discovery section - clean and compact
|
|
266
|
+
discovered_plugins = self.plugin_registry.list_plugins()
|
|
267
|
+
if discovered_plugins:
|
|
268
|
+
# Simple plugin list
|
|
269
|
+
plugin_list = "//".join(discovered_plugins)
|
|
270
|
+
#print(f"\033[2;36mPlugins enabled: \033[2;37m{plugin_list}\033[0m")
|
|
271
|
+
print()
|
|
272
|
+
else:
|
|
273
|
+
#print("\033[2;31mNo plugins found\033[0m")
|
|
274
|
+
print()
|
|
275
|
+
|
|
276
|
+
# Ready message with gradient and bold Enter
|
|
277
|
+
ready_msg = "Ready! Type your message and press "
|
|
278
|
+
enter_text = "Enter"
|
|
279
|
+
end_text = "."
|
|
280
|
+
|
|
281
|
+
# Apply white to dim white gradient to the message
|
|
282
|
+
gradient_msg = self.visual_effects.apply_message_gradient(ready_msg, "dim_white")
|
|
283
|
+
bold_enter = f"\033[1m{enter_text}\033[0m" # Bold Enter
|
|
284
|
+
gradient_end = self.visual_effects.apply_message_gradient(end_text, "dim_white")
|
|
285
|
+
|
|
286
|
+
print(gradient_msg + bold_enter + gradient_end)
|
|
287
|
+
print()
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
async def _initialize_llm_core(self) -> None:
|
|
291
|
+
"""Initialize LLM core service components."""
|
|
292
|
+
# Initialize LLM service
|
|
293
|
+
await self.llm_service.initialize()
|
|
294
|
+
logger.info("LLM core service initialized")
|
|
295
|
+
|
|
296
|
+
# Register LLM hooks
|
|
297
|
+
await self.llm_hook_system.register_hooks()
|
|
298
|
+
logger.info("LLM hook system registered")
|
|
299
|
+
|
|
300
|
+
# Initialize conversation logger
|
|
301
|
+
await self.conversation_logger.initialize()
|
|
302
|
+
logger.info("Conversation logger initialized")
|
|
303
|
+
|
|
304
|
+
# Discover MCP servers
|
|
305
|
+
mcp_servers = await self.mcp_integration.discover_mcp_servers()
|
|
306
|
+
if mcp_servers:
|
|
307
|
+
logger.info(f"Discovered {len(mcp_servers)} MCP servers")
|
|
308
|
+
|
|
309
|
+
# Register LLM service hooks for user input processing
|
|
310
|
+
await self.llm_service.register_hooks()
|
|
311
|
+
|
|
312
|
+
async def _initialize_plugins(self) -> None:
|
|
313
|
+
"""Initialize all discovered plugins."""
|
|
314
|
+
# Deduplicate plugin instances by ID (same instance may be stored under multiple keys)
|
|
315
|
+
initialized_instances = set()
|
|
316
|
+
|
|
317
|
+
for plugin_name, plugin_instance in self.plugin_instances.items():
|
|
318
|
+
instance_id = id(plugin_instance)
|
|
319
|
+
|
|
320
|
+
# Skip if we've already initialized this instance
|
|
321
|
+
if instance_id in initialized_instances:
|
|
322
|
+
continue
|
|
323
|
+
|
|
324
|
+
initialized_instances.add(instance_id)
|
|
325
|
+
|
|
326
|
+
if hasattr(plugin_instance, 'initialize'):
|
|
327
|
+
# Pass command registry, input handler, llm_service, and renderer to plugins that might need it
|
|
328
|
+
init_kwargs = {
|
|
329
|
+
'event_bus': self.event_bus,
|
|
330
|
+
'config': self.config,
|
|
331
|
+
'command_registry': getattr(self.input_handler, 'command_registry', None),
|
|
332
|
+
'input_handler': self.input_handler,
|
|
333
|
+
'renderer': self.renderer,
|
|
334
|
+
'llm_service': self.llm_service
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
# Check if initialize method accepts keyword arguments
|
|
338
|
+
import inspect
|
|
339
|
+
sig = inspect.signature(plugin_instance.initialize)
|
|
340
|
+
if len(sig.parameters) > 0:
|
|
341
|
+
await plugin_instance.initialize(**init_kwargs)
|
|
342
|
+
else:
|
|
343
|
+
await plugin_instance.initialize()
|
|
344
|
+
logger.debug(f"Initialized plugin: {plugin_name}")
|
|
345
|
+
|
|
346
|
+
if hasattr(plugin_instance, 'register_hooks'):
|
|
347
|
+
await plugin_instance.register_hooks()
|
|
348
|
+
logger.debug(f"Registered hooks for plugin: {plugin_name}")
|
|
349
|
+
|
|
350
|
+
def _initialize_slash_commands(self) -> None:
|
|
351
|
+
"""Initialize the slash command system with core commands."""
|
|
352
|
+
logger.info("Starting slash command system initialization...")
|
|
353
|
+
try:
|
|
354
|
+
from core.commands.system_commands import SystemCommandsPlugin
|
|
355
|
+
logger.info("SystemCommandsPlugin imported successfully")
|
|
356
|
+
|
|
357
|
+
# Create and register system commands
|
|
358
|
+
system_commands = SystemCommandsPlugin(
|
|
359
|
+
command_registry=self.input_handler.command_registry,
|
|
360
|
+
event_bus=self.event_bus,
|
|
361
|
+
config_manager=self.config
|
|
362
|
+
)
|
|
363
|
+
logger.info("SystemCommandsPlugin instance created")
|
|
364
|
+
|
|
365
|
+
# Register all system commands
|
|
366
|
+
system_commands.register_commands()
|
|
367
|
+
logger.info("System commands registration completed")
|
|
368
|
+
|
|
369
|
+
stats = self.input_handler.command_registry.get_registry_stats()
|
|
370
|
+
logger.info("Slash command system initialized with system commands")
|
|
371
|
+
logger.info(f"[INFO] {stats['total_commands']} commands registered")
|
|
372
|
+
|
|
373
|
+
except Exception as e:
|
|
374
|
+
logger.error(f"Failed to initialize slash command system: {e}")
|
|
375
|
+
import traceback
|
|
376
|
+
logger.error(f"[INFO] Traceback: {traceback.format_exc()}")
|
|
377
|
+
|
|
378
|
+
def _initialize_fullscreen_commands(self) -> None:
|
|
379
|
+
"""Initialize dynamic fullscreen plugin commands."""
|
|
380
|
+
try:
|
|
381
|
+
from core.fullscreen.command_integration import FullScreenCommandIntegrator
|
|
382
|
+
|
|
383
|
+
# Create the integrator
|
|
384
|
+
self.fullscreen_integrator = FullScreenCommandIntegrator(
|
|
385
|
+
command_registry=self.input_handler.command_registry,
|
|
386
|
+
event_bus=self.event_bus
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
# Discover and register all fullscreen plugins
|
|
390
|
+
# Use same plugin directory resolution as main plugin registry
|
|
391
|
+
package_dir = Path(__file__).parent.parent
|
|
392
|
+
plugins_dir = package_dir / "plugins"
|
|
393
|
+
if not plugins_dir.exists():
|
|
394
|
+
plugins_dir = Path.cwd() / "plugins"
|
|
395
|
+
registered_count = self.fullscreen_integrator.discover_and_register_plugins(plugins_dir)
|
|
396
|
+
|
|
397
|
+
logger.info(f"Fullscreen plugin commands initialized: {registered_count} plugins registered")
|
|
398
|
+
|
|
399
|
+
except Exception as e:
|
|
400
|
+
logger.error(f"Failed to initialize fullscreen commands: {e}")
|
|
401
|
+
import traceback
|
|
402
|
+
logger.error(f"Fullscreen commands traceback: {traceback.format_exc()}")
|
|
403
|
+
|
|
404
|
+
async def _render_loop(self) -> None:
|
|
405
|
+
"""Main rendering loop for status updates."""
|
|
406
|
+
logger.info("Render loop starting...")
|
|
407
|
+
while self.running:
|
|
408
|
+
try:
|
|
409
|
+
# Update status areas dynamically from plugins
|
|
410
|
+
status_areas = {"A": [], "B": [], "C": []}
|
|
411
|
+
|
|
412
|
+
# Core system status goes to area A
|
|
413
|
+
registry_stats = self.event_bus.get_registry_stats()
|
|
414
|
+
hook_count = registry_stats.get("total_hooks", 0)
|
|
415
|
+
status_areas["A"].append(f"Hooks: {hook_count}")
|
|
416
|
+
|
|
417
|
+
# LLM Core status
|
|
418
|
+
llm_status = self.llm_service.get_status_line()
|
|
419
|
+
if llm_status:
|
|
420
|
+
for area in ["A", "B", "C"]:
|
|
421
|
+
if area in llm_status:
|
|
422
|
+
status_areas[area].extend(llm_status[area])
|
|
423
|
+
|
|
424
|
+
# Collect status from all plugins (organized by area)
|
|
425
|
+
plugin_status_areas = self.plugin_registry.collect_status_lines(self.plugin_instances)
|
|
426
|
+
|
|
427
|
+
# Merge plugin status into our areas
|
|
428
|
+
for area in ["A", "B", "C"]:
|
|
429
|
+
status_areas[area].extend(plugin_status_areas[area])
|
|
430
|
+
|
|
431
|
+
# Handle spinner for processing status across all areas
|
|
432
|
+
for area in ["A", "B", "C"]:
|
|
433
|
+
for i, line in enumerate(status_areas[area]):
|
|
434
|
+
if line.startswith("Processing: Yes"):
|
|
435
|
+
spinner = self.renderer.thinking_animation.get_next_frame()
|
|
436
|
+
status_areas[area][i] = f"Processing: {spinner} Yes"
|
|
437
|
+
elif line.startswith("Processing: ") and "tokens" in line:
|
|
438
|
+
# Extract token count and add spinner
|
|
439
|
+
spinner = self.renderer.thinking_animation.get_next_frame()
|
|
440
|
+
token_part = line.replace("Processing: ", "")
|
|
441
|
+
status_areas[area][i] = f"Processing: {spinner} {token_part}"
|
|
442
|
+
|
|
443
|
+
# Update renderer with status areas
|
|
444
|
+
self.renderer.status_areas = status_areas
|
|
445
|
+
|
|
446
|
+
# Render active area
|
|
447
|
+
await self.renderer.render_active_area()
|
|
448
|
+
|
|
449
|
+
# Use configured FPS for render timing
|
|
450
|
+
render_fps = self.config.get("terminal.render_fps", 20)
|
|
451
|
+
await asyncio.sleep(1.0 / render_fps)
|
|
452
|
+
|
|
453
|
+
except Exception as e:
|
|
454
|
+
logger.error(f"Render loop error: {e}")
|
|
455
|
+
error_delay = self.config.get("terminal.render_error_delay", 0.1)
|
|
456
|
+
await asyncio.sleep(error_delay)
|
|
457
|
+
|
|
458
|
+
async def _register_core_status_views(self) -> None:
|
|
459
|
+
"""Register default core status views."""
|
|
460
|
+
try:
|
|
461
|
+
from .io.core_status_views import CoreStatusViews
|
|
462
|
+
core_views = CoreStatusViews(self.llm_service, self.config)
|
|
463
|
+
core_views.register_all_views(self.status_registry)
|
|
464
|
+
except Exception as e:
|
|
465
|
+
logger.error(f"Failed to register core status views: {e}")
|
|
466
|
+
|
|
467
|
+
def create_background_task(self, coro, name: str = "unnamed"):
|
|
468
|
+
"""Create and track a background task with automatic cleanup.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
coro: Coroutine to run as background task
|
|
472
|
+
name: Human-readable name for the task
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
The created asyncio.Task
|
|
476
|
+
"""
|
|
477
|
+
task = asyncio.create_task(coro)
|
|
478
|
+
task.set_name(name)
|
|
479
|
+
self._background_tasks.append(task)
|
|
480
|
+
logger.debug(f"Created background task: {name}")
|
|
481
|
+
|
|
482
|
+
# Add callback to remove task from tracking when done
|
|
483
|
+
def remove_task(t):
|
|
484
|
+
try:
|
|
485
|
+
self._background_tasks.remove(t)
|
|
486
|
+
logger.debug(f"Background task completed: {name}")
|
|
487
|
+
except ValueError:
|
|
488
|
+
pass # Task already removed
|
|
489
|
+
|
|
490
|
+
task.add_done_callback(remove_task)
|
|
491
|
+
return task
|
|
492
|
+
|
|
493
|
+
async def cleanup(self) -> None:
|
|
494
|
+
"""Clean up all resources and cancel background tasks.
|
|
495
|
+
|
|
496
|
+
This method is guaranteed to run on all exit paths via finally block.
|
|
497
|
+
Ensures no orphaned tasks or resources remain.
|
|
498
|
+
"""
|
|
499
|
+
logger.info("Starting application cleanup...")
|
|
500
|
+
|
|
501
|
+
# Cancel all tracked background tasks
|
|
502
|
+
if self._background_tasks:
|
|
503
|
+
logger.info(f"Cancelling {len(self._background_tasks)} background tasks")
|
|
504
|
+
for task in self._background_tasks[:]: # Copy list to avoid modification during iteration
|
|
505
|
+
if not task.done():
|
|
506
|
+
task.cancel()
|
|
507
|
+
|
|
508
|
+
# Wait for all tasks to complete with timeout
|
|
509
|
+
if self._background_tasks:
|
|
510
|
+
try:
|
|
511
|
+
await asyncio.wait_for(
|
|
512
|
+
asyncio.gather(*self._background_tasks, return_exceptions=True),
|
|
513
|
+
timeout=5.0
|
|
514
|
+
)
|
|
515
|
+
except asyncio.TimeoutError:
|
|
516
|
+
logger.warning("Some tasks did not complete within timeout")
|
|
517
|
+
except Exception as e:
|
|
518
|
+
logger.error(f"Error during task cleanup: {e}")
|
|
519
|
+
|
|
520
|
+
# Clear task list
|
|
521
|
+
self._background_tasks.clear()
|
|
522
|
+
|
|
523
|
+
# Mark startup as incomplete
|
|
524
|
+
self._startup_complete = False
|
|
525
|
+
self.running = False
|
|
526
|
+
|
|
527
|
+
# Call full shutdown to cleanup other resources
|
|
528
|
+
await self.shutdown()
|
|
529
|
+
|
|
530
|
+
logger.info("Application cleanup complete")
|
|
531
|
+
|
|
532
|
+
def get_system_status(self):
|
|
533
|
+
"""Get current system status for monitoring and debugging.
|
|
534
|
+
|
|
535
|
+
Returns:
|
|
536
|
+
Dictionary containing system status information
|
|
537
|
+
"""
|
|
538
|
+
return {
|
|
539
|
+
"running": self.running,
|
|
540
|
+
"startup_complete": self._startup_complete,
|
|
541
|
+
"background_tasks": len(self._background_tasks),
|
|
542
|
+
"plugins_loaded": len(self.plugin_instances),
|
|
543
|
+
"task_names": [task.get_name() for task in self._background_tasks]
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async def shutdown(self) -> None:
|
|
547
|
+
"""Shutdown the application gracefully."""
|
|
548
|
+
logger.info("Application shutting down")
|
|
549
|
+
self.running = False
|
|
550
|
+
|
|
551
|
+
# Stop input handler
|
|
552
|
+
await self.input_handler.stop()
|
|
553
|
+
|
|
554
|
+
# Shutdown LLM core service
|
|
555
|
+
await self.llm_service.shutdown()
|
|
556
|
+
await self.conversation_logger.shutdown()
|
|
557
|
+
await self.mcp_integration.shutdown()
|
|
558
|
+
logger.info("LLM core service shutdown complete")
|
|
559
|
+
|
|
560
|
+
# Shutdown all plugins dynamically
|
|
561
|
+
for plugin_name, plugin_instance in self.plugin_instances.items():
|
|
562
|
+
if hasattr(plugin_instance, 'shutdown'):
|
|
563
|
+
try:
|
|
564
|
+
await plugin_instance.shutdown()
|
|
565
|
+
logger.debug(f"Shutdown plugin: {plugin_name}")
|
|
566
|
+
except Exception as e:
|
|
567
|
+
logger.warning(f"Error shutting down plugin {plugin_name}: {e}")
|
|
568
|
+
|
|
569
|
+
# Restore terminal
|
|
570
|
+
self.renderer.exit_raw_mode()
|
|
571
|
+
# Only show cursor if not in pipe mode
|
|
572
|
+
if not self.pipe_mode:
|
|
573
|
+
print("\033[?25h") # Show cursor
|
|
574
|
+
# print("Exiting...")
|
|
575
|
+
|
|
576
|
+
# Close state manager
|
|
577
|
+
self.state_manager.close()
|
|
578
|
+
logger.info("Application shutdown complete")
|