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,586 @@
|
|
|
1
|
+
"""Terminal rendering system for Kollabor CLI."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from collections import deque
|
|
5
|
+
from typing import List, Optional, TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from .visual_effects import VisualEffects
|
|
8
|
+
from .terminal_state import TerminalState
|
|
9
|
+
from .layout import LayoutManager, ThinkingAnimationManager
|
|
10
|
+
from .status_renderer import StatusRenderer
|
|
11
|
+
from .message_renderer import MessageRenderer
|
|
12
|
+
from .message_coordinator import MessageDisplayCoordinator
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from ..config.manager import ConfigManager
|
|
16
|
+
from .input_handler import InputHandler
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TerminalRenderer:
|
|
22
|
+
"""Advanced terminal renderer with modular architecture.
|
|
23
|
+
|
|
24
|
+
Features:
|
|
25
|
+
- Modular visual effects system
|
|
26
|
+
- Advanced layout management
|
|
27
|
+
- Comprehensive status rendering
|
|
28
|
+
- Message formatting and display
|
|
29
|
+
- Terminal state management
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self, event_bus=None, config: Optional["ConfigManager"] = None
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Initialize the terminal renderer with modern architecture."""
|
|
36
|
+
self.event_bus = event_bus
|
|
37
|
+
self._app_config: Optional["ConfigManager"] = (
|
|
38
|
+
config # Store config for render cache settings
|
|
39
|
+
)
|
|
40
|
+
self.input_handler: Optional["InputHandler"] = (
|
|
41
|
+
None # Will be set externally if needed
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Initialize core components
|
|
45
|
+
self.terminal_state = TerminalState()
|
|
46
|
+
self.visual_effects = VisualEffects()
|
|
47
|
+
self.layout_manager = LayoutManager()
|
|
48
|
+
self.status_renderer = StatusRenderer()
|
|
49
|
+
self.message_renderer = MessageRenderer(
|
|
50
|
+
self.terminal_state, self.visual_effects
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Initialize thinking animation manager
|
|
54
|
+
self.thinking_animation = ThinkingAnimationManager()
|
|
55
|
+
|
|
56
|
+
# Initialize message display coordinator for unified message handling
|
|
57
|
+
self.message_coordinator = MessageDisplayCoordinator(self)
|
|
58
|
+
|
|
59
|
+
# Interface properties
|
|
60
|
+
self.input_buffer = ""
|
|
61
|
+
self.cursor_position = 0
|
|
62
|
+
self.status_areas = {"A": [], "B": [], "C": []}
|
|
63
|
+
self.thinking_active = False
|
|
64
|
+
|
|
65
|
+
# State management
|
|
66
|
+
self.conversation_active = False
|
|
67
|
+
self.writing_messages = False
|
|
68
|
+
self.input_line_written = False
|
|
69
|
+
self.last_line_count = 0
|
|
70
|
+
self.active_area_start_position = (
|
|
71
|
+
None # Track where active area starts for clean clearing
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Render optimization: cache to prevent unnecessary writes
|
|
75
|
+
self._last_render_content: List[str] = [] # Cache of last rendered content
|
|
76
|
+
self._render_cache_enabled = True # Enable/disable render caching
|
|
77
|
+
|
|
78
|
+
# Configuration (will be updated by config methods)
|
|
79
|
+
self.thinking_effect = "shimmer"
|
|
80
|
+
|
|
81
|
+
logger.info("Advanced terminal renderer initialized")
|
|
82
|
+
|
|
83
|
+
def enter_raw_mode(self) -> None:
|
|
84
|
+
"""Enter raw terminal mode for character-by-character input."""
|
|
85
|
+
success = self.terminal_state.enter_raw_mode()
|
|
86
|
+
if not success:
|
|
87
|
+
logger.warning("Failed to enter raw mode")
|
|
88
|
+
|
|
89
|
+
def exit_raw_mode(self) -> None:
|
|
90
|
+
"""Exit raw terminal mode and restore settings."""
|
|
91
|
+
success = self.terminal_state.exit_raw_mode()
|
|
92
|
+
if not success:
|
|
93
|
+
logger.warning("Failed to exit raw mode")
|
|
94
|
+
|
|
95
|
+
def create_kollabor_banner(self, version: str = "v1.0.0") -> str:
|
|
96
|
+
"""Create a beautiful Kollabor ASCII banner with gradient.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
version: Version string to display next to the banner.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Formatted banner string with gradient colors and version.
|
|
103
|
+
"""
|
|
104
|
+
return self.visual_effects.create_banner(version)
|
|
105
|
+
|
|
106
|
+
def write_message(self, message: str, apply_gradient: bool = True) -> None:
|
|
107
|
+
"""Write a message to the conversation area.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
message: The message to write.
|
|
111
|
+
apply_gradient: Whether to apply gradient effect.
|
|
112
|
+
"""
|
|
113
|
+
self.message_renderer.write_message(message, apply_gradient)
|
|
114
|
+
logger.debug(f"Wrote message: {message[:50]}...")
|
|
115
|
+
|
|
116
|
+
def write_streaming_chunk(self, chunk: str) -> None:
|
|
117
|
+
"""Write a streaming chunk to the conversation area immediately.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
chunk: The text chunk to write without buffering.
|
|
121
|
+
"""
|
|
122
|
+
# Use message renderer for proper formatting
|
|
123
|
+
self.message_renderer.write_streaming_chunk(chunk)
|
|
124
|
+
logger.debug(f"Wrote streaming chunk: {chunk[:20]}...")
|
|
125
|
+
|
|
126
|
+
def write_user_message(self, message: str) -> None:
|
|
127
|
+
"""Write a user message with gradient effect.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
message: The user message to write.
|
|
131
|
+
"""
|
|
132
|
+
self.message_renderer.write_user_message(message)
|
|
133
|
+
|
|
134
|
+
def write_hook_message(self, content: str, **metadata) -> None:
|
|
135
|
+
"""Write a hook message using coordinated display.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
content: Hook message content.
|
|
139
|
+
**metadata: Additional metadata.
|
|
140
|
+
"""
|
|
141
|
+
# Route hook messages through the coordinator to prevent conflicts
|
|
142
|
+
self.message_coordinator.display_message_sequence(
|
|
143
|
+
[("system", content, metadata)]
|
|
144
|
+
)
|
|
145
|
+
logger.debug(f"Wrote hook message: {content[:50]}...")
|
|
146
|
+
|
|
147
|
+
def update_thinking(self, active: bool, message: str = "") -> None:
|
|
148
|
+
"""Update the thinking animation state.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
active: Whether thinking animation should be active.
|
|
152
|
+
message: Optional thinking message to display.
|
|
153
|
+
"""
|
|
154
|
+
self.thinking_active = active
|
|
155
|
+
|
|
156
|
+
if active and message:
|
|
157
|
+
self.thinking_animation.start_thinking(message)
|
|
158
|
+
logger.debug(f"Started thinking: {message}")
|
|
159
|
+
elif not active:
|
|
160
|
+
completion_msg = self.thinking_animation.stop_thinking()
|
|
161
|
+
if completion_msg:
|
|
162
|
+
logger.info(completion_msg)
|
|
163
|
+
|
|
164
|
+
def set_thinking_effect(self, effect: str) -> None:
|
|
165
|
+
"""Set the thinking text effect.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
effect: Effect type - "dim", "shimmer", or "normal"
|
|
169
|
+
"""
|
|
170
|
+
if effect in ["dim", "shimmer", "normal"]:
|
|
171
|
+
self.thinking_effect = effect
|
|
172
|
+
self.visual_effects.configure_effect("thinking", enabled=True)
|
|
173
|
+
logger.debug(f"Set thinking effect to: {effect}")
|
|
174
|
+
else:
|
|
175
|
+
logger.warning(f"Invalid thinking effect: {effect}")
|
|
176
|
+
|
|
177
|
+
def configure_shimmer(self, speed: int, wave_width: int) -> None:
|
|
178
|
+
"""Configure shimmer effect parameters.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
speed: Number of frames between shimmer updates
|
|
182
|
+
wave_width: Number of characters in the shimmer wave
|
|
183
|
+
"""
|
|
184
|
+
self.visual_effects.configure_effect(
|
|
185
|
+
"thinking", speed=speed, width=wave_width
|
|
186
|
+
)
|
|
187
|
+
logger.debug(f"Configured shimmer: speed={speed}, wave_width={wave_width}")
|
|
188
|
+
|
|
189
|
+
def configure_thinking_limit(self, limit: int) -> None:
|
|
190
|
+
"""Configure the thinking message limit.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
limit: Maximum number of thinking messages to keep
|
|
194
|
+
"""
|
|
195
|
+
self.thinking_animation.messages = deque(maxlen=limit)
|
|
196
|
+
logger.debug(f"Configured thinking message limit: {limit}")
|
|
197
|
+
|
|
198
|
+
async def render_active_area(self) -> None:
|
|
199
|
+
"""Render the active input/status area using modern components.
|
|
200
|
+
|
|
201
|
+
This method renders dynamic interface parts:
|
|
202
|
+
thinking animation, input prompt, and status lines.
|
|
203
|
+
"""
|
|
204
|
+
# logger.info("[START] render_active_area() called")
|
|
205
|
+
|
|
206
|
+
# CRITICAL: Skip ALL rendering when modal is active to prevent interference
|
|
207
|
+
if hasattr(self, "input_handler") and self.input_handler:
|
|
208
|
+
try:
|
|
209
|
+
from ..events.models import CommandMode
|
|
210
|
+
|
|
211
|
+
if self.input_handler.command_mode in (CommandMode.MODAL, CommandMode.LIVE_MODAL):
|
|
212
|
+
return
|
|
213
|
+
except Exception as e:
|
|
214
|
+
logger.error(f"Error checking modal state: {e}")
|
|
215
|
+
pass # Continue with normal rendering if check fails
|
|
216
|
+
|
|
217
|
+
# Skip rendering if currently writing messages, UNLESS we have command menu to display
|
|
218
|
+
if self.writing_messages:
|
|
219
|
+
# Check if any plugin wants to provide enhanced input (like command menu)
|
|
220
|
+
has_enhanced_input = False
|
|
221
|
+
if self.event_bus:
|
|
222
|
+
try:
|
|
223
|
+
from ..events import EventType
|
|
224
|
+
|
|
225
|
+
result = await self.event_bus.emit_with_hooks(
|
|
226
|
+
EventType.INPUT_RENDER,
|
|
227
|
+
{"input_buffer": self.input_buffer},
|
|
228
|
+
"renderer",
|
|
229
|
+
)
|
|
230
|
+
# Check if any plugin provided enhanced input
|
|
231
|
+
if "main" in result:
|
|
232
|
+
for hook_result in result["main"].values():
|
|
233
|
+
if (
|
|
234
|
+
isinstance(hook_result, dict)
|
|
235
|
+
and "fancy_input_lines" in hook_result
|
|
236
|
+
):
|
|
237
|
+
has_enhanced_input = True
|
|
238
|
+
break
|
|
239
|
+
except Exception:
|
|
240
|
+
pass
|
|
241
|
+
|
|
242
|
+
# Only skip rendering if no enhanced input (command menu) is available
|
|
243
|
+
if not has_enhanced_input:
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
# Update terminal size and invalidate cache if resized
|
|
247
|
+
old_size = self.terminal_state.get_size()
|
|
248
|
+
self.terminal_state.update_size()
|
|
249
|
+
terminal_width, terminal_height = self.terminal_state.get_size()
|
|
250
|
+
|
|
251
|
+
# Check for terminal resize and invalidate cache if needed
|
|
252
|
+
if old_size != (terminal_width, terminal_height):
|
|
253
|
+
self.invalidate_render_cache()
|
|
254
|
+
logger.debug(
|
|
255
|
+
f"Terminal resize detected: {old_size} -> ({terminal_width}, {terminal_height})"
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
self.layout_manager.set_terminal_size(terminal_width, terminal_height)
|
|
259
|
+
self.status_renderer.set_terminal_width(terminal_width)
|
|
260
|
+
|
|
261
|
+
lines = []
|
|
262
|
+
|
|
263
|
+
# Add thinking animation if active
|
|
264
|
+
if self.thinking_active:
|
|
265
|
+
thinking_lines = self.thinking_animation.get_display_lines(
|
|
266
|
+
lambda text: self.visual_effects.apply_thinking_effect(
|
|
267
|
+
text, self.thinking_effect
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
lines.extend(thinking_lines)
|
|
271
|
+
|
|
272
|
+
# Add blank line before input if we have thinking content
|
|
273
|
+
if lines:
|
|
274
|
+
lines.append("")
|
|
275
|
+
|
|
276
|
+
# Render input area
|
|
277
|
+
await self._render_input_area(lines)
|
|
278
|
+
|
|
279
|
+
# Check if command menu should replace status area
|
|
280
|
+
# logger.info("Checking for command menu lines...")
|
|
281
|
+
command_menu_lines = await self._get_command_menu_lines()
|
|
282
|
+
# logger.info(f"Got {len(command_menu_lines)} command menu lines")
|
|
283
|
+
if command_menu_lines:
|
|
284
|
+
# Replace status with command menu
|
|
285
|
+
lines.extend(command_menu_lines)
|
|
286
|
+
else:
|
|
287
|
+
# Check if status modal should replace status area
|
|
288
|
+
status_modal_lines = await self._get_status_modal_lines()
|
|
289
|
+
if status_modal_lines:
|
|
290
|
+
# Replace status with status modal
|
|
291
|
+
lines.extend(status_modal_lines)
|
|
292
|
+
else:
|
|
293
|
+
# Update status areas and render normally
|
|
294
|
+
self._update_status_areas()
|
|
295
|
+
status_lines = self.status_renderer.render_horizontal_layout(
|
|
296
|
+
self.visual_effects.apply_status_colors
|
|
297
|
+
)
|
|
298
|
+
lines.extend(status_lines)
|
|
299
|
+
|
|
300
|
+
# Clear previous render and write new content
|
|
301
|
+
await self._render_lines(lines)
|
|
302
|
+
|
|
303
|
+
async def _render_input_area(self, lines: List[str]) -> None:
|
|
304
|
+
"""Render the input area, checking for plugin overrides.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
lines: List of lines to append input rendering to.
|
|
308
|
+
"""
|
|
309
|
+
# Try to get enhanced input from plugins
|
|
310
|
+
if self.event_bus:
|
|
311
|
+
try:
|
|
312
|
+
from ..events import EventType
|
|
313
|
+
|
|
314
|
+
result = await self.event_bus.emit_with_hooks(
|
|
315
|
+
EventType.INPUT_RENDER,
|
|
316
|
+
{"input_buffer": self.input_buffer},
|
|
317
|
+
"renderer",
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
# Check if any plugin provided enhanced input
|
|
321
|
+
if "main" in result:
|
|
322
|
+
for hook_result in result["main"].values():
|
|
323
|
+
if (
|
|
324
|
+
isinstance(hook_result, dict)
|
|
325
|
+
and "fancy_input_lines" in hook_result
|
|
326
|
+
):
|
|
327
|
+
lines.extend(hook_result["fancy_input_lines"])
|
|
328
|
+
return
|
|
329
|
+
except Exception as e:
|
|
330
|
+
logger.warning(f"Error rendering enhanced input: {e}")
|
|
331
|
+
|
|
332
|
+
# Fallback to default input rendering
|
|
333
|
+
if self.thinking_active:
|
|
334
|
+
lines.append(f"> {self.input_buffer}")
|
|
335
|
+
else:
|
|
336
|
+
# Insert cursor at the correct position
|
|
337
|
+
cursor_pos = getattr(self, "cursor_position", 0)
|
|
338
|
+
buffer_text = self.input_buffer
|
|
339
|
+
|
|
340
|
+
# Ensure cursor position is within bounds
|
|
341
|
+
cursor_pos = max(0, min(cursor_pos, len(buffer_text)))
|
|
342
|
+
|
|
343
|
+
# Debug logging
|
|
344
|
+
logger.debug(
|
|
345
|
+
f"Rendering cursor at position {cursor_pos} in buffer '{buffer_text}'"
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
# Insert cursor character at position
|
|
349
|
+
text_with_cursor = (
|
|
350
|
+
buffer_text[:cursor_pos] + "▌" + buffer_text[cursor_pos:]
|
|
351
|
+
)
|
|
352
|
+
lines.append(f"> {text_with_cursor}")
|
|
353
|
+
|
|
354
|
+
def _write(self, text: str) -> None:
|
|
355
|
+
"""Write text directly to terminal.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
text: Text to write.
|
|
359
|
+
"""
|
|
360
|
+
# Collect in buffer if buffered mode is active
|
|
361
|
+
if hasattr(self, '_write_buffer') and self._write_buffer is not None:
|
|
362
|
+
self._write_buffer.append(text)
|
|
363
|
+
else:
|
|
364
|
+
self.terminal_state.write_raw(text)
|
|
365
|
+
|
|
366
|
+
def _start_buffered_write(self) -> None:
|
|
367
|
+
"""Start buffered write mode - collects all writes until flush."""
|
|
368
|
+
self._write_buffer = []
|
|
369
|
+
|
|
370
|
+
def _flush_buffered_write(self) -> None:
|
|
371
|
+
"""Flush all buffered writes at once to reduce flickering."""
|
|
372
|
+
if hasattr(self, '_write_buffer') and self._write_buffer:
|
|
373
|
+
# Join all buffered content and write in one operation
|
|
374
|
+
self.terminal_state.write_raw(''.join(self._write_buffer))
|
|
375
|
+
self._write_buffer = None
|
|
376
|
+
|
|
377
|
+
def _get_terminal_width(self) -> int:
|
|
378
|
+
"""Get terminal width, with fallback."""
|
|
379
|
+
width, _ = self.terminal_state.get_size()
|
|
380
|
+
return width
|
|
381
|
+
|
|
382
|
+
def _apply_status_colors(self, text: str) -> str:
|
|
383
|
+
"""Apply semantic colors to status line text (legacy compatibility).
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
text: The status text to colorize.
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
Colorized text with appropriate ANSI codes.
|
|
390
|
+
"""
|
|
391
|
+
return self.visual_effects.apply_status_colors(text)
|
|
392
|
+
|
|
393
|
+
async def _get_command_menu_lines(self) -> List[str]:
|
|
394
|
+
"""Get command menu lines if menu is active.
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
List of command menu lines, or empty list if not active.
|
|
398
|
+
"""
|
|
399
|
+
if not self.event_bus:
|
|
400
|
+
return []
|
|
401
|
+
|
|
402
|
+
try:
|
|
403
|
+
# Check for command menu via COMMAND_MENU_RENDER event
|
|
404
|
+
from ..events import EventType
|
|
405
|
+
|
|
406
|
+
# logger.info("🔥 Emitting COMMAND_MENU_RENDER event...")
|
|
407
|
+
result = await self.event_bus.emit_with_hooks(
|
|
408
|
+
EventType.COMMAND_MENU_RENDER,
|
|
409
|
+
{"request": "get_menu_lines"},
|
|
410
|
+
"renderer",
|
|
411
|
+
)
|
|
412
|
+
# logger.info(f"🔥 COMMAND_MENU_RENDER result: {result}")
|
|
413
|
+
|
|
414
|
+
# Check if any component provided menu lines
|
|
415
|
+
if "main" in result and "hook_results" in result["main"]:
|
|
416
|
+
for hook_result in result["main"]["hook_results"]:
|
|
417
|
+
if (
|
|
418
|
+
isinstance(hook_result, dict)
|
|
419
|
+
and "result" in hook_result
|
|
420
|
+
and isinstance(hook_result["result"], dict)
|
|
421
|
+
and "menu_lines" in hook_result["result"]
|
|
422
|
+
):
|
|
423
|
+
return hook_result["result"]["menu_lines"]
|
|
424
|
+
|
|
425
|
+
except Exception as e:
|
|
426
|
+
logger.debug(f"No command menu available: {e}")
|
|
427
|
+
|
|
428
|
+
return []
|
|
429
|
+
|
|
430
|
+
async def _get_status_modal_lines(self) -> List[str]:
|
|
431
|
+
"""Get status modal lines if status modal is active.
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
List of status modal lines, or empty list if not active.
|
|
435
|
+
"""
|
|
436
|
+
if not self.event_bus:
|
|
437
|
+
return []
|
|
438
|
+
|
|
439
|
+
try:
|
|
440
|
+
# Check for status modal via input handler
|
|
441
|
+
from ..events import EventType
|
|
442
|
+
|
|
443
|
+
result = await self.event_bus.emit_with_hooks(
|
|
444
|
+
EventType.STATUS_MODAL_RENDER,
|
|
445
|
+
{"request": "get_status_modal_lines"},
|
|
446
|
+
"renderer",
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
# Check if any component provided status modal lines
|
|
450
|
+
if "main" in result and "hook_results" in result["main"]:
|
|
451
|
+
for hook_result in result["main"]["hook_results"]:
|
|
452
|
+
if (
|
|
453
|
+
isinstance(hook_result, dict)
|
|
454
|
+
and "result" in hook_result
|
|
455
|
+
and isinstance(hook_result["result"], dict)
|
|
456
|
+
and "status_modal_lines" in hook_result["result"]
|
|
457
|
+
):
|
|
458
|
+
return hook_result["result"]["status_modal_lines"]
|
|
459
|
+
|
|
460
|
+
except Exception as e:
|
|
461
|
+
logger.debug(f"No status modal available: {e}")
|
|
462
|
+
|
|
463
|
+
return []
|
|
464
|
+
|
|
465
|
+
def _update_status_areas(self) -> None:
|
|
466
|
+
"""Update status areas for rendering."""
|
|
467
|
+
for area_name, content in self.status_areas.items():
|
|
468
|
+
self.status_renderer.update_area_content(area_name, content)
|
|
469
|
+
|
|
470
|
+
async def _render_lines(self, lines: List[str]) -> None:
|
|
471
|
+
"""Render lines to terminal with proper clearing.
|
|
472
|
+
|
|
473
|
+
Args:
|
|
474
|
+
lines: Lines to render.
|
|
475
|
+
"""
|
|
476
|
+
# RENDER OPTIMIZATION: Only render if content actually changed
|
|
477
|
+
# Check if render caching is enabled via config
|
|
478
|
+
if self._app_config is not None:
|
|
479
|
+
cache_enabled = self._app_config.get(
|
|
480
|
+
"terminal.render_cache_enabled", True
|
|
481
|
+
)
|
|
482
|
+
else:
|
|
483
|
+
cache_enabled = self._render_cache_enabled # Fallback to local setting
|
|
484
|
+
|
|
485
|
+
if cache_enabled and self._last_render_content == lines:
|
|
486
|
+
# Content unchanged - skip rendering entirely
|
|
487
|
+
return
|
|
488
|
+
|
|
489
|
+
# Content changed - update cache and proceed with render
|
|
490
|
+
self._last_render_content = lines.copy()
|
|
491
|
+
|
|
492
|
+
current_line_count = len(lines)
|
|
493
|
+
|
|
494
|
+
# Check if terminal was resized - if so, use aggressive clearing
|
|
495
|
+
resize_occurred = self.terminal_state.check_and_clear_resize_flag()
|
|
496
|
+
|
|
497
|
+
# Use buffered write to reduce flickering (especially on Windows)
|
|
498
|
+
# Start buffering BEFORE clearing so clear+redraw happens atomically
|
|
499
|
+
self._start_buffered_write()
|
|
500
|
+
|
|
501
|
+
# Clear previous active area (now buffered to reduce flicker)
|
|
502
|
+
if self.input_line_written and hasattr(self, "last_line_count"):
|
|
503
|
+
if resize_occurred:
|
|
504
|
+
# RESIZE FIX: On resize, restore to saved cursor position (where active area started)
|
|
505
|
+
# and clear everything from there to bottom of screen
|
|
506
|
+
logger.debug(
|
|
507
|
+
"🔄 Terminal resize detected - restoring cursor and clearing"
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
if self.active_area_start_position:
|
|
511
|
+
# Restore to where active area started before resize
|
|
512
|
+
self._write("\033[u") # Restore cursor position
|
|
513
|
+
# Clear from that position to end of screen
|
|
514
|
+
self._write("\033[J") # Clear from cursor to end of screen
|
|
515
|
+
else:
|
|
516
|
+
# Fallback: just clear current line if we don't have saved position
|
|
517
|
+
self._write("\r\033[2K") # Clear line
|
|
518
|
+
else:
|
|
519
|
+
# Normal line-by-line clearing when no resize
|
|
520
|
+
self._write("\r\033[2K") # Clear current line
|
|
521
|
+
for _ in range(self.last_line_count - 1):
|
|
522
|
+
self._write("\033[A") # Move cursor up
|
|
523
|
+
self._write("\r\033[2K") # Clear line
|
|
524
|
+
|
|
525
|
+
# Save cursor position before rendering active area (for future resize handling)
|
|
526
|
+
self._write("\033[s") # Save cursor position
|
|
527
|
+
self.active_area_start_position = True # Mark that we have a saved position
|
|
528
|
+
|
|
529
|
+
# Write all lines
|
|
530
|
+
for i, line in enumerate(lines):
|
|
531
|
+
if i > 0:
|
|
532
|
+
self._write("\n")
|
|
533
|
+
self._write(f"\r{line}")
|
|
534
|
+
|
|
535
|
+
# Hide cursor
|
|
536
|
+
self._write("\033[?25l") # Write hide cursor to buffer too
|
|
537
|
+
|
|
538
|
+
# Flush all writes at once
|
|
539
|
+
self._flush_buffered_write()
|
|
540
|
+
|
|
541
|
+
# Remember line count for next render
|
|
542
|
+
self.last_line_count = current_line_count
|
|
543
|
+
self.input_line_written = True
|
|
544
|
+
|
|
545
|
+
def clear_active_area(self) -> None:
|
|
546
|
+
"""Clear the active area before writing conversation messages."""
|
|
547
|
+
if self.input_line_written and hasattr(self, "last_line_count"):
|
|
548
|
+
self.terminal_state.clear_line()
|
|
549
|
+
for _ in range(self.last_line_count - 1):
|
|
550
|
+
self.terminal_state.move_cursor_up(1)
|
|
551
|
+
self.terminal_state.clear_line()
|
|
552
|
+
self.input_line_written = False
|
|
553
|
+
self.invalidate_render_cache() # Force re-render after clearing
|
|
554
|
+
logger.debug("Cleared active area")
|
|
555
|
+
|
|
556
|
+
def invalidate_render_cache(self) -> None:
|
|
557
|
+
"""Invalidate the render cache to force next render.
|
|
558
|
+
|
|
559
|
+
Call this when external changes should force a re-render
|
|
560
|
+
(e.g., terminal resize, configuration changes, manual refresh).
|
|
561
|
+
"""
|
|
562
|
+
self._last_render_content.clear()
|
|
563
|
+
logger.debug("Render cache invalidated")
|
|
564
|
+
|
|
565
|
+
def set_render_cache_enabled(self, enabled: bool) -> None:
|
|
566
|
+
"""Enable or disable render caching.
|
|
567
|
+
|
|
568
|
+
Args:
|
|
569
|
+
enabled: True to enable caching, False to disable.
|
|
570
|
+
"""
|
|
571
|
+
self._render_cache_enabled = enabled
|
|
572
|
+
if not enabled:
|
|
573
|
+
self._last_render_content.clear() # Clear cache when disabling
|
|
574
|
+
logger.debug(f"Render cache {'enabled' if enabled else 'disabled'}")
|
|
575
|
+
|
|
576
|
+
def get_render_cache_status(self) -> dict:
|
|
577
|
+
"""Get render cache status for debugging.
|
|
578
|
+
|
|
579
|
+
Returns:
|
|
580
|
+
Dictionary with cache status information.
|
|
581
|
+
"""
|
|
582
|
+
return {
|
|
583
|
+
"enabled": self._render_cache_enabled,
|
|
584
|
+
"cached_lines": len(self._last_render_content),
|
|
585
|
+
"last_cached_content": self._last_render_content.copy(),
|
|
586
|
+
}
|