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
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""Input buffer management for terminal input handling."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import List, Tuple
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BufferManager:
|
|
11
|
+
"""Manages input buffer with validation, history, and editing capabilities.
|
|
12
|
+
|
|
13
|
+
Handles text input buffer operations including character insertion, deletion,
|
|
14
|
+
cursor movement, input validation, and command history.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, buffer_limit: int = 1000, history_limit: int = 100):
|
|
18
|
+
"""Initialize the buffer manager.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
buffer_limit: Maximum characters allowed in buffer.
|
|
22
|
+
history_limit: Maximum commands to keep in history.
|
|
23
|
+
"""
|
|
24
|
+
self._buffer = ""
|
|
25
|
+
self._cursor_pos = 0
|
|
26
|
+
self._buffer_limit = buffer_limit
|
|
27
|
+
self._history: List[str] = []
|
|
28
|
+
self._history_limit = history_limit
|
|
29
|
+
self._history_index = -1
|
|
30
|
+
self._temp_buffer = "" # For history navigation
|
|
31
|
+
|
|
32
|
+
logger.debug(
|
|
33
|
+
"BufferManager initialized with limits: "
|
|
34
|
+
f"buffer={buffer_limit}, history={history_limit}"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def content(self) -> str:
|
|
39
|
+
"""Get current buffer content."""
|
|
40
|
+
return self._buffer
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def cursor_position(self) -> int:
|
|
44
|
+
"""Get current cursor position."""
|
|
45
|
+
return self._cursor_pos
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def is_empty(self) -> bool:
|
|
49
|
+
"""Check if buffer is empty or only whitespace."""
|
|
50
|
+
return not self._buffer.strip()
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def length(self) -> int:
|
|
54
|
+
"""Get current buffer length."""
|
|
55
|
+
return len(self._buffer)
|
|
56
|
+
|
|
57
|
+
def insert_char(self, char: str) -> bool:
|
|
58
|
+
"""Insert a character at the current cursor position.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
char: Character to insert.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
True if character was inserted, False if rejected.
|
|
65
|
+
"""
|
|
66
|
+
if not self._is_valid_char(char):
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
if len(self._buffer) >= self._buffer_limit:
|
|
70
|
+
logger.warning(f"Buffer limit reached: {self._buffer_limit}")
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
# Insert character at cursor position
|
|
74
|
+
self._buffer = (
|
|
75
|
+
self._buffer[: self._cursor_pos]
|
|
76
|
+
+ char
|
|
77
|
+
+ self._buffer[self._cursor_pos :]
|
|
78
|
+
)
|
|
79
|
+
self._cursor_pos += 1
|
|
80
|
+
|
|
81
|
+
return True
|
|
82
|
+
|
|
83
|
+
def delete_char(self) -> bool:
|
|
84
|
+
"""Delete character before cursor (backspace behavior).
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
True if character was deleted, False if at beginning.
|
|
88
|
+
"""
|
|
89
|
+
if self._cursor_pos == 0:
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
self._buffer = (
|
|
93
|
+
self._buffer[: self._cursor_pos - 1] + self._buffer[self._cursor_pos :]
|
|
94
|
+
)
|
|
95
|
+
self._cursor_pos -= 1
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
def delete_forward(self) -> bool:
|
|
99
|
+
"""Delete character after cursor (delete key behavior).
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
True if character was deleted, False if at end.
|
|
103
|
+
"""
|
|
104
|
+
if self._cursor_pos >= len(self._buffer):
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
self._buffer = (
|
|
108
|
+
self._buffer[: self._cursor_pos] + self._buffer[self._cursor_pos + 1 :]
|
|
109
|
+
)
|
|
110
|
+
return True
|
|
111
|
+
|
|
112
|
+
def move_cursor(self, direction: str) -> bool:
|
|
113
|
+
"""Move cursor left or right.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
direction: "left" or "right".
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
True if cursor moved, False if at boundary.
|
|
120
|
+
"""
|
|
121
|
+
if direction == "left" and self._cursor_pos > 0:
|
|
122
|
+
self._cursor_pos -= 1
|
|
123
|
+
return True
|
|
124
|
+
elif direction == "right" and self._cursor_pos < len(self._buffer):
|
|
125
|
+
self._cursor_pos += 1
|
|
126
|
+
return True
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
def move_to_start(self) -> None:
|
|
130
|
+
"""Move cursor to start of buffer."""
|
|
131
|
+
self._cursor_pos = 0
|
|
132
|
+
|
|
133
|
+
def move_to_end(self) -> None:
|
|
134
|
+
"""Move cursor to end of buffer."""
|
|
135
|
+
self._cursor_pos = len(self._buffer)
|
|
136
|
+
|
|
137
|
+
def clear(self) -> None:
|
|
138
|
+
"""Clear the buffer and reset cursor."""
|
|
139
|
+
self._buffer = ""
|
|
140
|
+
self._cursor_pos = 0
|
|
141
|
+
self._reset_history_navigation()
|
|
142
|
+
|
|
143
|
+
def get_content_and_clear(self) -> str:
|
|
144
|
+
"""Get buffer content and clear it.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
The buffer content before clearing.
|
|
148
|
+
"""
|
|
149
|
+
content = self._buffer
|
|
150
|
+
self.clear()
|
|
151
|
+
return content
|
|
152
|
+
|
|
153
|
+
def add_to_history(self, command: str) -> None:
|
|
154
|
+
"""Add a command to history.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
command: Command to add to history.
|
|
158
|
+
"""
|
|
159
|
+
if not command.strip():
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
# Remove duplicate if it exists
|
|
163
|
+
if command in self._history:
|
|
164
|
+
self._history.remove(command)
|
|
165
|
+
|
|
166
|
+
# Add to end and maintain limit
|
|
167
|
+
self._history.append(command)
|
|
168
|
+
if len(self._history) > self._history_limit:
|
|
169
|
+
self._history = self._history[-self._history_limit :]
|
|
170
|
+
|
|
171
|
+
self._reset_history_navigation()
|
|
172
|
+
logger.debug(f"Added to history: {command[:50]}...")
|
|
173
|
+
|
|
174
|
+
def navigate_history(self, direction: str) -> bool:
|
|
175
|
+
"""Navigate through command history.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
direction: "up" for previous, "down" for next.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
True if history was navigated, False if at boundary.
|
|
182
|
+
"""
|
|
183
|
+
if not self._history:
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
# Save current buffer on first history navigation
|
|
187
|
+
if self._history_index == -1:
|
|
188
|
+
self._temp_buffer = self._buffer
|
|
189
|
+
|
|
190
|
+
if direction == "up":
|
|
191
|
+
if self._history_index < len(self._history) - 1:
|
|
192
|
+
self._history_index += 1
|
|
193
|
+
self._load_from_history()
|
|
194
|
+
return True
|
|
195
|
+
elif direction == "down":
|
|
196
|
+
if self._history_index > -1:
|
|
197
|
+
self._history_index -= 1
|
|
198
|
+
if self._history_index == -1:
|
|
199
|
+
# Restore temp buffer
|
|
200
|
+
self._buffer = self._temp_buffer
|
|
201
|
+
self._cursor_pos = len(self._buffer)
|
|
202
|
+
else:
|
|
203
|
+
self._load_from_history()
|
|
204
|
+
return True
|
|
205
|
+
|
|
206
|
+
return False
|
|
207
|
+
|
|
208
|
+
def get_display_info(self) -> Tuple[str, int]:
|
|
209
|
+
"""Get buffer content and cursor position for display.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Tuple of (buffer_content, cursor_position).
|
|
213
|
+
"""
|
|
214
|
+
return self._buffer, self._cursor_pos
|
|
215
|
+
|
|
216
|
+
def validate_content(self) -> List[str]:
|
|
217
|
+
"""Validate current buffer content.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
List of validation error messages (empty if valid).
|
|
221
|
+
"""
|
|
222
|
+
errors = []
|
|
223
|
+
|
|
224
|
+
# Check for potentially dangerous content
|
|
225
|
+
dangerous_patterns = [
|
|
226
|
+
"rm -rf",
|
|
227
|
+
"sudo rm",
|
|
228
|
+
":(){ :|:& };:",
|
|
229
|
+
"fork bomb",
|
|
230
|
+
]
|
|
231
|
+
|
|
232
|
+
for pattern in dangerous_patterns:
|
|
233
|
+
if pattern in self._buffer.lower():
|
|
234
|
+
errors.append(
|
|
235
|
+
f"Potentially dangerous command pattern detected: {pattern}"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# Check for very long lines that might cause issues
|
|
239
|
+
if len(self._buffer) > self._buffer_limit * 0.9:
|
|
240
|
+
errors.append(
|
|
241
|
+
f"Input approaching buffer limit "
|
|
242
|
+
f"({len(self._buffer)}/{self._buffer_limit})"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
return errors
|
|
246
|
+
|
|
247
|
+
def _is_valid_char(self, char: str) -> bool:
|
|
248
|
+
"""Check if character is valid for input.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
char: Character to validate.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
True if character is valid.
|
|
255
|
+
"""
|
|
256
|
+
if not char or len(char) != 1:
|
|
257
|
+
return False
|
|
258
|
+
|
|
259
|
+
char_code = ord(char)
|
|
260
|
+
|
|
261
|
+
# Allow printable ASCII characters
|
|
262
|
+
if 32 <= char_code <= 126:
|
|
263
|
+
return True
|
|
264
|
+
|
|
265
|
+
# Allow some special characters like tab
|
|
266
|
+
if char_code in [9]: # Tab
|
|
267
|
+
return True
|
|
268
|
+
|
|
269
|
+
return False
|
|
270
|
+
|
|
271
|
+
def _load_from_history(self) -> None:
|
|
272
|
+
"""Load buffer from history at current index."""
|
|
273
|
+
if 0 <= self._history_index < len(self._history):
|
|
274
|
+
# History is stored newest-first, but we navigate oldest-first
|
|
275
|
+
history_item = self._history[-(self._history_index + 1)]
|
|
276
|
+
self._buffer = history_item
|
|
277
|
+
self._cursor_pos = len(self._buffer)
|
|
278
|
+
|
|
279
|
+
def _reset_history_navigation(self) -> None:
|
|
280
|
+
"""Reset history navigation state."""
|
|
281
|
+
self._history_index = -1
|
|
282
|
+
self._temp_buffer = ""
|
|
283
|
+
|
|
284
|
+
async def handle_paste(self, paste_content: str) -> bool:
|
|
285
|
+
"""Handle pasted content with proper line break and size management.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
paste_content: Content that was pasted.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
True if paste was successfully handled, False if rejected.
|
|
292
|
+
"""
|
|
293
|
+
if not paste_content:
|
|
294
|
+
return False
|
|
295
|
+
|
|
296
|
+
# Check if adding paste content would exceed buffer limit
|
|
297
|
+
if len(self._buffer) + len(paste_content) > self._buffer_limit:
|
|
298
|
+
total_len = len(self._buffer) + len(paste_content)
|
|
299
|
+
logger.warning(
|
|
300
|
+
"Paste rejected: would exceed buffer limit "
|
|
301
|
+
f"({total_len} > {self._buffer_limit})"
|
|
302
|
+
)
|
|
303
|
+
return False
|
|
304
|
+
|
|
305
|
+
# Process paste content (handle line breaks properly)
|
|
306
|
+
processed_content = self._process_paste_content(paste_content)
|
|
307
|
+
|
|
308
|
+
# Insert at current cursor position
|
|
309
|
+
self._buffer = (
|
|
310
|
+
self._buffer[: self._cursor_pos]
|
|
311
|
+
+ processed_content
|
|
312
|
+
+ self._buffer[self._cursor_pos :]
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# Move cursor to end of pasted content
|
|
316
|
+
self._cursor_pos += len(processed_content)
|
|
317
|
+
|
|
318
|
+
# Reset history navigation since buffer was modified
|
|
319
|
+
self._reset_history_navigation()
|
|
320
|
+
|
|
321
|
+
logger.debug(
|
|
322
|
+
"Paste handled successfully: " f"{len(processed_content)} chars inserted"
|
|
323
|
+
)
|
|
324
|
+
return True
|
|
325
|
+
|
|
326
|
+
def _process_paste_content(self, content: str) -> str:
|
|
327
|
+
"""Process pasted content to handle line breaks and formatting.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
content: Raw pasted content.
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
Processed content suitable for buffer insertion.
|
|
334
|
+
"""
|
|
335
|
+
# For now, convert line breaks to spaces to prevent auto-submission
|
|
336
|
+
# This preserves the content while making it safe for single-line input
|
|
337
|
+
processed = content.replace("\n", " ").replace("\r", " ")
|
|
338
|
+
|
|
339
|
+
# Normalize multiple spaces to single spaces
|
|
340
|
+
import re
|
|
341
|
+
|
|
342
|
+
processed = re.sub(r"\s+", " ", processed)
|
|
343
|
+
|
|
344
|
+
# Strip leading/trailing whitespace
|
|
345
|
+
processed = processed.strip()
|
|
346
|
+
|
|
347
|
+
return processed
|
|
348
|
+
|
|
349
|
+
def get_stats(self) -> dict:
|
|
350
|
+
"""Get buffer statistics for debugging.
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
Dictionary with buffer statistics.
|
|
354
|
+
"""
|
|
355
|
+
return {
|
|
356
|
+
"buffer_length": len(self._buffer),
|
|
357
|
+
"buffer_limit": self._buffer_limit,
|
|
358
|
+
"cursor_position": self._cursor_pos,
|
|
359
|
+
"history_count": len(self._history),
|
|
360
|
+
"history_limit": self._history_limit,
|
|
361
|
+
"history_index": self._history_index,
|
|
362
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""Configuration status view for displaying config errors and status."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Dict, Any, Optional, List
|
|
5
|
+
|
|
6
|
+
from .status_renderer import StatusViewConfig, StatusMetric
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ConfigStatusView:
|
|
12
|
+
"""Status view for configuration monitoring and error reporting."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, config_service, event_bus):
|
|
15
|
+
"""Initialize the configuration status view.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
config_service: The configuration service to monitor.
|
|
19
|
+
event_bus: Event bus for receiving notifications.
|
|
20
|
+
"""
|
|
21
|
+
self.config_service = config_service
|
|
22
|
+
self.event_bus = event_bus
|
|
23
|
+
self.view_id = "config_status"
|
|
24
|
+
self.priority = 900 # High priority for config errors
|
|
25
|
+
|
|
26
|
+
# Register for config reload notifications
|
|
27
|
+
if hasattr(config_service, "register_reload_callback"):
|
|
28
|
+
config_service.register_reload_callback(self._on_config_reload)
|
|
29
|
+
|
|
30
|
+
logger.debug("ConfigStatusView initialized")
|
|
31
|
+
|
|
32
|
+
def _on_config_reload(self) -> None:
|
|
33
|
+
"""Callback when configuration is reloaded."""
|
|
34
|
+
# Trigger status refresh by emitting event
|
|
35
|
+
if self.event_bus and hasattr(self.event_bus, "emit_with_hooks"):
|
|
36
|
+
# Note: This is called from sync context during config reload
|
|
37
|
+
# EventBus doesn't have emit_async method
|
|
38
|
+
# Skip the event emission during sync config reload
|
|
39
|
+
logger.debug("Config reloaded - status refresh skipped (sync context)")
|
|
40
|
+
|
|
41
|
+
def get_status_data(self) -> Dict[str, Any]:
|
|
42
|
+
"""Get configuration status data.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Dictionary with config status information.
|
|
46
|
+
"""
|
|
47
|
+
if not self.config_service:
|
|
48
|
+
return {"error": "No config service", "status": "ERROR"}
|
|
49
|
+
|
|
50
|
+
config_error = self.config_service.get_config_error()
|
|
51
|
+
has_error = self.config_service.has_config_error()
|
|
52
|
+
|
|
53
|
+
using_cache = has_error and self.config_service._cached_config is not None
|
|
54
|
+
status_data = {
|
|
55
|
+
"has_error": has_error,
|
|
56
|
+
"error_message": config_error,
|
|
57
|
+
"status": "ERROR" if has_error else "OK",
|
|
58
|
+
"using_cache": using_cache,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Add validation info
|
|
62
|
+
try:
|
|
63
|
+
validation_result = self.config_service.validate_config()
|
|
64
|
+
status_data.update(
|
|
65
|
+
{
|
|
66
|
+
"valid": validation_result.get("valid", True),
|
|
67
|
+
"warnings": validation_result.get("warnings", []),
|
|
68
|
+
"errors": validation_result.get("errors", []),
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
except Exception as e:
|
|
72
|
+
logger.warning(f"Could not validate config: {e}")
|
|
73
|
+
status_data["validation_error"] = str(e)
|
|
74
|
+
|
|
75
|
+
return status_data
|
|
76
|
+
|
|
77
|
+
def format_status_line(self, data: Dict[str, Any]) -> Optional[str]:
|
|
78
|
+
"""Format the configuration status line.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
data: Status data dictionary.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Formatted status line or None if no status needed.
|
|
85
|
+
"""
|
|
86
|
+
if not data:
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
# Show errors prominently
|
|
90
|
+
if data.get("has_error"):
|
|
91
|
+
error_msg = data.get("error_message", "Unknown config error")
|
|
92
|
+
if data.get("using_cache"):
|
|
93
|
+
return f" Config Error (using cache): {error_msg[:40]}..."
|
|
94
|
+
else:
|
|
95
|
+
return f"Config Error: {error_msg[:50]}..."
|
|
96
|
+
|
|
97
|
+
# Show validation warnings
|
|
98
|
+
warnings = data.get("warnings", [])
|
|
99
|
+
if warnings:
|
|
100
|
+
warning_count = len(warnings)
|
|
101
|
+
if warning_count == 1:
|
|
102
|
+
return f" Config Warning: {warnings[0][:45]}..."
|
|
103
|
+
else:
|
|
104
|
+
return f" Config: {warning_count} warnings"
|
|
105
|
+
|
|
106
|
+
# Show validation errors (different from load errors)
|
|
107
|
+
errors = data.get("errors", [])
|
|
108
|
+
if errors:
|
|
109
|
+
error_count = len(errors)
|
|
110
|
+
if error_count == 1:
|
|
111
|
+
return f"Config Validation: {errors[0][:40]}..."
|
|
112
|
+
else:
|
|
113
|
+
return f"Config: {error_count} validation errors"
|
|
114
|
+
|
|
115
|
+
# Normal status - only show if explicitly requested
|
|
116
|
+
if data.get("show_normal_status", False):
|
|
117
|
+
return "[OK] Config: OK"
|
|
118
|
+
|
|
119
|
+
# No status line needed for normal operation
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
def should_display(self, data: Dict[str, Any]) -> bool:
|
|
123
|
+
"""Determine if this status view should be displayed.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
data: Status data dictionary.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
True if status should be shown, False otherwise.
|
|
130
|
+
"""
|
|
131
|
+
if not data:
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
# Always show errors and warnings
|
|
135
|
+
return (
|
|
136
|
+
data.get("has_error", False)
|
|
137
|
+
or data.get("warnings", [])
|
|
138
|
+
or data.get("errors", [])
|
|
139
|
+
or data.get("show_normal_status", False)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def get_color_scheme(self, data: Dict[str, Any]) -> str:
|
|
143
|
+
"""Get color scheme based on config status.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
data: Status data dictionary.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Color scheme name.
|
|
150
|
+
"""
|
|
151
|
+
if data.get("has_error"):
|
|
152
|
+
return "error"
|
|
153
|
+
elif data.get("warnings") or data.get("errors"):
|
|
154
|
+
return "warning"
|
|
155
|
+
else:
|
|
156
|
+
return "success"
|
|
157
|
+
|
|
158
|
+
def get_priority(self) -> int:
|
|
159
|
+
"""Get display priority for this status view.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Priority value (higher = more important).
|
|
163
|
+
"""
|
|
164
|
+
# High priority for config issues
|
|
165
|
+
return self.priority
|
|
166
|
+
|
|
167
|
+
async def handle_status_event(
|
|
168
|
+
self, event_type: str, event_data: Dict[str, Any]
|
|
169
|
+
) -> None:
|
|
170
|
+
"""Handle status-related events.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
event_type: Type of the event.
|
|
174
|
+
event_data: Event data dictionary.
|
|
175
|
+
"""
|
|
176
|
+
if event_type in ["config_reloaded", "config_error", "config_changed"]:
|
|
177
|
+
# Refresh status display
|
|
178
|
+
await self.refresh_status()
|
|
179
|
+
|
|
180
|
+
async def refresh_status(self) -> None:
|
|
181
|
+
"""Refresh the status display."""
|
|
182
|
+
if self.event_bus and hasattr(self.event_bus, "emit_with_hooks"):
|
|
183
|
+
from ..events.models import EventType
|
|
184
|
+
|
|
185
|
+
await self.event_bus.emit_with_hooks(
|
|
186
|
+
EventType.STATUS_CONTENT_UPDATE,
|
|
187
|
+
{"view_id": self.view_id, "source": "config_status"},
|
|
188
|
+
"config_status_view",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def get_status_view_config(self) -> StatusViewConfig:
|
|
192
|
+
"""Get StatusViewConfig for registry registration.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
StatusViewConfig that can be registered with StatusViewRegistry.
|
|
196
|
+
"""
|
|
197
|
+
from .status_renderer import BlockConfig
|
|
198
|
+
|
|
199
|
+
return StatusViewConfig(
|
|
200
|
+
name="Configuration Status",
|
|
201
|
+
plugin_source="core",
|
|
202
|
+
priority=self.priority,
|
|
203
|
+
blocks=[
|
|
204
|
+
BlockConfig(
|
|
205
|
+
width_fraction=1.0,
|
|
206
|
+
content_provider=self._get_config_status_content,
|
|
207
|
+
title="Configuration Status",
|
|
208
|
+
priority=100
|
|
209
|
+
)
|
|
210
|
+
],
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
def _get_config_status_content(self) -> List[str]:
|
|
214
|
+
"""Get configuration status content for status view.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
List of status content lines.
|
|
218
|
+
"""
|
|
219
|
+
status_data = self.get_status_data()
|
|
220
|
+
|
|
221
|
+
# Show errors prominently
|
|
222
|
+
if status_data.get("has_error"):
|
|
223
|
+
error_msg = status_data.get("error_message", "Unknown config error")
|
|
224
|
+
if status_data.get("using_cache"):
|
|
225
|
+
return [f"Config: ERROR (using cache)", f"Error: {error_msg[:60]}"]
|
|
226
|
+
else:
|
|
227
|
+
return [f"Config: ERROR", f"Error: {error_msg[:60]}"]
|
|
228
|
+
|
|
229
|
+
# Show validation warnings
|
|
230
|
+
warnings = status_data.get("warnings", [])
|
|
231
|
+
if warnings:
|
|
232
|
+
lines = [f"Config: {len(warnings)} warning(s)"]
|
|
233
|
+
for warning in warnings[:3]: # Show first 3 warnings
|
|
234
|
+
lines.append(f"- {warning[:60]}")
|
|
235
|
+
return lines
|
|
236
|
+
|
|
237
|
+
# Show validation errors
|
|
238
|
+
errors = status_data.get("errors", [])
|
|
239
|
+
if errors:
|
|
240
|
+
lines = [f"Config: {len(errors)} validation error(s)"]
|
|
241
|
+
for error in errors[:3]: # Show first 3 errors
|
|
242
|
+
lines.append(f"- {error[:60]}")
|
|
243
|
+
return lines
|
|
244
|
+
|
|
245
|
+
# Normal status - show healthy config
|
|
246
|
+
return ["Config: OK", "No errors or warnings"]
|
|
247
|
+
|
|
248
|
+
def _get_config_status_metrics(self) -> List[StatusMetric]:
|
|
249
|
+
"""Get configuration status as StatusMetric objects.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
List of StatusMetric objects for display.
|
|
253
|
+
"""
|
|
254
|
+
status_data = self.get_status_data()
|
|
255
|
+
|
|
256
|
+
if not self.should_display(status_data):
|
|
257
|
+
return []
|
|
258
|
+
|
|
259
|
+
status_line = self.format_status_line(status_data)
|
|
260
|
+
if not status_line:
|
|
261
|
+
return []
|
|
262
|
+
|
|
263
|
+
color_scheme = self.get_color_scheme(status_data)
|
|
264
|
+
|
|
265
|
+
return [
|
|
266
|
+
StatusMetric(
|
|
267
|
+
name="config",
|
|
268
|
+
value=status_line,
|
|
269
|
+
color=color_scheme,
|
|
270
|
+
unit="",
|
|
271
|
+
)
|
|
272
|
+
]
|