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.
Files changed (128) hide show
  1. core/__init__.py +18 -0
  2. core/application.py +578 -0
  3. core/cli.py +193 -0
  4. core/commands/__init__.py +43 -0
  5. core/commands/executor.py +277 -0
  6. core/commands/menu_renderer.py +319 -0
  7. core/commands/parser.py +186 -0
  8. core/commands/registry.py +331 -0
  9. core/commands/system_commands.py +479 -0
  10. core/config/__init__.py +7 -0
  11. core/config/llm_task_config.py +110 -0
  12. core/config/loader.py +501 -0
  13. core/config/manager.py +112 -0
  14. core/config/plugin_config_manager.py +346 -0
  15. core/config/plugin_schema.py +424 -0
  16. core/config/service.py +399 -0
  17. core/effects/__init__.py +1 -0
  18. core/events/__init__.py +12 -0
  19. core/events/bus.py +129 -0
  20. core/events/executor.py +154 -0
  21. core/events/models.py +258 -0
  22. core/events/processor.py +176 -0
  23. core/events/registry.py +289 -0
  24. core/fullscreen/__init__.py +19 -0
  25. core/fullscreen/command_integration.py +290 -0
  26. core/fullscreen/components/__init__.py +12 -0
  27. core/fullscreen/components/animation.py +258 -0
  28. core/fullscreen/components/drawing.py +160 -0
  29. core/fullscreen/components/matrix_components.py +177 -0
  30. core/fullscreen/manager.py +302 -0
  31. core/fullscreen/plugin.py +204 -0
  32. core/fullscreen/renderer.py +282 -0
  33. core/fullscreen/session.py +324 -0
  34. core/io/__init__.py +52 -0
  35. core/io/buffer_manager.py +362 -0
  36. core/io/config_status_view.py +272 -0
  37. core/io/core_status_views.py +410 -0
  38. core/io/input_errors.py +313 -0
  39. core/io/input_handler.py +2655 -0
  40. core/io/input_mode_manager.py +402 -0
  41. core/io/key_parser.py +344 -0
  42. core/io/layout.py +587 -0
  43. core/io/message_coordinator.py +204 -0
  44. core/io/message_renderer.py +601 -0
  45. core/io/modal_interaction_handler.py +315 -0
  46. core/io/raw_input_processor.py +946 -0
  47. core/io/status_renderer.py +845 -0
  48. core/io/terminal_renderer.py +586 -0
  49. core/io/terminal_state.py +551 -0
  50. core/io/visual_effects.py +734 -0
  51. core/llm/__init__.py +26 -0
  52. core/llm/api_communication_service.py +863 -0
  53. core/llm/conversation_logger.py +473 -0
  54. core/llm/conversation_manager.py +414 -0
  55. core/llm/file_operations_executor.py +1401 -0
  56. core/llm/hook_system.py +402 -0
  57. core/llm/llm_service.py +1629 -0
  58. core/llm/mcp_integration.py +386 -0
  59. core/llm/message_display_service.py +450 -0
  60. core/llm/model_router.py +214 -0
  61. core/llm/plugin_sdk.py +396 -0
  62. core/llm/response_parser.py +848 -0
  63. core/llm/response_processor.py +364 -0
  64. core/llm/tool_executor.py +520 -0
  65. core/logging/__init__.py +19 -0
  66. core/logging/setup.py +208 -0
  67. core/models/__init__.py +5 -0
  68. core/models/base.py +23 -0
  69. core/plugins/__init__.py +13 -0
  70. core/plugins/collector.py +212 -0
  71. core/plugins/discovery.py +386 -0
  72. core/plugins/factory.py +263 -0
  73. core/plugins/registry.py +152 -0
  74. core/storage/__init__.py +5 -0
  75. core/storage/state_manager.py +84 -0
  76. core/ui/__init__.py +6 -0
  77. core/ui/config_merger.py +176 -0
  78. core/ui/config_widgets.py +369 -0
  79. core/ui/live_modal_renderer.py +276 -0
  80. core/ui/modal_actions.py +162 -0
  81. core/ui/modal_overlay_renderer.py +373 -0
  82. core/ui/modal_renderer.py +591 -0
  83. core/ui/modal_state_manager.py +443 -0
  84. core/ui/widget_integration.py +222 -0
  85. core/ui/widgets/__init__.py +27 -0
  86. core/ui/widgets/base_widget.py +136 -0
  87. core/ui/widgets/checkbox.py +85 -0
  88. core/ui/widgets/dropdown.py +140 -0
  89. core/ui/widgets/label.py +78 -0
  90. core/ui/widgets/slider.py +185 -0
  91. core/ui/widgets/text_input.py +224 -0
  92. core/utils/__init__.py +11 -0
  93. core/utils/config_utils.py +656 -0
  94. core/utils/dict_utils.py +212 -0
  95. core/utils/error_utils.py +275 -0
  96. core/utils/key_reader.py +171 -0
  97. core/utils/plugin_utils.py +267 -0
  98. core/utils/prompt_renderer.py +151 -0
  99. kollabor-0.4.9.dist-info/METADATA +298 -0
  100. kollabor-0.4.9.dist-info/RECORD +128 -0
  101. kollabor-0.4.9.dist-info/WHEEL +5 -0
  102. kollabor-0.4.9.dist-info/entry_points.txt +2 -0
  103. kollabor-0.4.9.dist-info/licenses/LICENSE +21 -0
  104. kollabor-0.4.9.dist-info/top_level.txt +4 -0
  105. kollabor_cli_main.py +20 -0
  106. plugins/__init__.py +1 -0
  107. plugins/enhanced_input/__init__.py +18 -0
  108. plugins/enhanced_input/box_renderer.py +103 -0
  109. plugins/enhanced_input/box_styles.py +142 -0
  110. plugins/enhanced_input/color_engine.py +165 -0
  111. plugins/enhanced_input/config.py +150 -0
  112. plugins/enhanced_input/cursor_manager.py +72 -0
  113. plugins/enhanced_input/geometry.py +81 -0
  114. plugins/enhanced_input/state.py +130 -0
  115. plugins/enhanced_input/text_processor.py +115 -0
  116. plugins/enhanced_input_plugin.py +385 -0
  117. plugins/fullscreen/__init__.py +9 -0
  118. plugins/fullscreen/example_plugin.py +327 -0
  119. plugins/fullscreen/matrix_plugin.py +132 -0
  120. plugins/hook_monitoring_plugin.py +1299 -0
  121. plugins/query_enhancer_plugin.py +350 -0
  122. plugins/save_conversation_plugin.py +502 -0
  123. plugins/system_commands_plugin.py +93 -0
  124. plugins/tmux_plugin.py +795 -0
  125. plugins/workflow_enforcement_plugin.py +629 -0
  126. system_prompt/default.md +1286 -0
  127. system_prompt/default_win.md +265 -0
  128. 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
+ }