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,601 @@
1
+ """Message rendering system for conversation display."""
2
+
3
+ import logging
4
+ from collections import deque
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+ from typing import List, Optional, Dict, Any, Callable
8
+
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class MessageType(Enum):
14
+ """Types of messages in conversation."""
15
+
16
+ USER = "user"
17
+ ASSISTANT = "assistant"
18
+ SYSTEM = "system"
19
+ ERROR = "error"
20
+ INFO = "info"
21
+ DEBUG = "debug"
22
+
23
+
24
+ class MessageFormat(Enum):
25
+ """Message formatting styles."""
26
+
27
+ PLAIN = "plain"
28
+ GRADIENT = "gradient"
29
+ HIGHLIGHTED = "highlighted"
30
+ DIMMED = "dimmed"
31
+
32
+
33
+ @dataclass
34
+ class ConversationMessage:
35
+ """Represents a message in the conversation."""
36
+
37
+ content: str
38
+ message_type: MessageType
39
+ format_style: MessageFormat = MessageFormat.PLAIN
40
+ timestamp: Optional[float] = None
41
+ metadata: Dict[str, Any] = None
42
+
43
+ def __post_init__(self):
44
+ if self.metadata is None:
45
+ self.metadata = {}
46
+ if self.timestamp is None:
47
+ import time
48
+
49
+ self.timestamp = time.time()
50
+
51
+
52
+ class MessageFormatter:
53
+ """Handles formatting of individual messages."""
54
+
55
+ def __init__(self, visual_effects=None):
56
+ """Initialize message formatter.
57
+
58
+ Args:
59
+ visual_effects: VisualEffects instance for applying effects.
60
+ """
61
+ self.visual_effects = visual_effects
62
+ self._format_functions: Dict[MessageFormat, Callable] = {
63
+ MessageFormat.PLAIN: self._format_plain,
64
+ MessageFormat.GRADIENT: self._format_gradient,
65
+ MessageFormat.HIGHLIGHTED: self._format_highlighted,
66
+ MessageFormat.DIMMED: self._format_dimmed,
67
+ }
68
+
69
+ def format_message(self, message: ConversationMessage) -> str:
70
+ """Format a conversation message for display.
71
+
72
+ Args:
73
+ message: ConversationMessage to format.
74
+
75
+ Returns:
76
+ Formatted message string.
77
+ """
78
+ formatter = self._format_functions.get(
79
+ message.format_style, self._format_plain
80
+ )
81
+ return formatter(message)
82
+
83
+ def _format_plain(self, message: ConversationMessage) -> str:
84
+ """Format message with no special effects.
85
+
86
+ Args:
87
+ message: Message to format.
88
+
89
+ Returns:
90
+ Plain formatted message.
91
+ """
92
+ return message.content
93
+
94
+ def _format_gradient(self, message: ConversationMessage) -> str:
95
+ """Format message with gradient effects.
96
+
97
+ Args:
98
+ message: Message to format.
99
+
100
+ Returns:
101
+ Gradient formatted message.
102
+ """
103
+ if not self.visual_effects:
104
+ return message.content
105
+
106
+ # Apply appropriate gradient based on message type
107
+ if message.message_type == MessageType.USER:
108
+ return self.visual_effects.apply_message_gradient(
109
+ message.content, "white_to_grey"
110
+ )
111
+ elif message.message_type == MessageType.ASSISTANT:
112
+ return self.visual_effects.apply_message_gradient(
113
+ message.content, "white_to_grey"
114
+ )
115
+ else:
116
+ return self.visual_effects.apply_message_gradient(
117
+ message.content, "dim_white"
118
+ )
119
+
120
+ def _format_highlighted(self, message: ConversationMessage) -> str:
121
+ """Format message with highlighting effects.
122
+
123
+ Args:
124
+ message: Message to format.
125
+
126
+ Returns:
127
+ Highlighted formatted message.
128
+ """
129
+ # Apply highlighting based on message type
130
+ if message.message_type == MessageType.ERROR:
131
+ return f"\033[1;31m{message.content}\033[0m" # Bright red
132
+ elif message.message_type == MessageType.SYSTEM:
133
+ return f"\033[1;33m{message.content}\033[0m" # Bright yellow
134
+ elif message.message_type == MessageType.INFO:
135
+ return f"\033[1;36m{message.content}\033[0m" # Bright cyan
136
+ elif message.message_type == MessageType.DEBUG:
137
+ return f"\033[2;37m{message.content}\033[0m" # Dim white
138
+ else:
139
+ return f"\033[1m{message.content}\033[0m" # Bright
140
+
141
+ def _format_dimmed(self, message: ConversationMessage) -> str:
142
+ """Format message with dimmed appearance.
143
+
144
+ Args:
145
+ message: Message to format.
146
+
147
+ Returns:
148
+ Dimmed formatted message.
149
+ """
150
+ return f"\033[2m{message.content}\033[0m"
151
+
152
+
153
+ class ConversationBuffer:
154
+ """Manages conversation message history with formatting."""
155
+
156
+ def __init__(self, max_messages: int = 1000):
157
+ """Initialize conversation buffer.
158
+
159
+ Args:
160
+ max_messages: Maximum messages to keep in buffer.
161
+ """
162
+ self.max_messages = max_messages
163
+ self.messages = deque(maxlen=max_messages)
164
+ self._message_counter = 0
165
+
166
+ def add_message(
167
+ self,
168
+ content: str,
169
+ message_type: MessageType,
170
+ format_style: MessageFormat = MessageFormat.PLAIN,
171
+ **metadata,
172
+ ) -> None:
173
+ """Add a message to the conversation buffer.
174
+
175
+ Args:
176
+ content: Message content.
177
+ message_type: Type of message.
178
+ format_style: Formatting style to apply.
179
+ **metadata: Additional metadata for the message.
180
+ """
181
+ message = ConversationMessage(
182
+ content=content,
183
+ message_type=message_type,
184
+ format_style=format_style,
185
+ metadata=metadata,
186
+ )
187
+
188
+ self.messages.append(message)
189
+ self._message_counter += 1
190
+ logger.debug(f"Added {message_type.value} message to conversation buffer")
191
+
192
+ def get_recent_messages(self, count: int) -> List[ConversationMessage]:
193
+ """Get the most recent messages from buffer.
194
+
195
+ Args:
196
+ count: Number of recent messages to retrieve.
197
+
198
+ Returns:
199
+ List of recent ConversationMessage objects.
200
+ """
201
+ if count <= 0:
202
+ return []
203
+
204
+ return list(self.messages)[-count:]
205
+
206
+ def get_messages_by_type(
207
+ self, message_type: MessageType
208
+ ) -> List[ConversationMessage]:
209
+ """Get all messages of a specific type.
210
+
211
+ Args:
212
+ message_type: Type of messages to retrieve.
213
+
214
+ Returns:
215
+ List of ConversationMessage objects of specified type.
216
+ """
217
+ return [msg for msg in self.messages if msg.message_type == message_type]
218
+
219
+ def clear(self) -> None:
220
+ """Clear all messages from buffer."""
221
+ self.messages.clear()
222
+ self._message_counter = 0
223
+ logger.debug("Conversation buffer cleared")
224
+
225
+ def get_stats(self) -> Dict[str, Any]:
226
+ """Get conversation buffer statistics.
227
+
228
+ Returns:
229
+ Dictionary with buffer statistics.
230
+ """
231
+ type_counts = {}
232
+ for msg in self.messages:
233
+ type_counts[msg.message_type.value] = (
234
+ type_counts.get(msg.message_type.value, 0) + 1
235
+ )
236
+
237
+ return {
238
+ "total_messages": len(self.messages),
239
+ "max_messages": self.max_messages,
240
+ "messages_added": self._message_counter,
241
+ "type_counts": type_counts,
242
+ }
243
+
244
+
245
+ class ConversationRenderer:
246
+ """Handles rendering of conversation messages to terminal."""
247
+
248
+ def __init__(self, terminal_state, visual_effects=None):
249
+ """Initialize conversation renderer.
250
+
251
+ Args:
252
+ terminal_state: TerminalState instance for output operations.
253
+ visual_effects: VisualEffects instance for formatting.
254
+ """
255
+ self.terminal_state = terminal_state
256
+ self.visual_effects = visual_effects
257
+ self.formatter = MessageFormatter(visual_effects)
258
+ self.buffer = ConversationBuffer()
259
+
260
+ # Rendering configuration
261
+ self.auto_format = True
262
+ self.flush_immediately = True
263
+
264
+ # State tracking
265
+ self._last_render_count = 0
266
+
267
+ def write_message(
268
+ self,
269
+ content: str,
270
+ message_type: MessageType = MessageType.ASSISTANT,
271
+ format_style: MessageFormat = MessageFormat.GRADIENT,
272
+ immediate_display: bool = True,
273
+ **metadata,
274
+ ) -> None:
275
+ """Write a message to the conversation.
276
+
277
+ Args:
278
+ content: Message content to write.
279
+ message_type: Type of message.
280
+ format_style: How to format the message.
281
+ immediate_display: Whether to display immediately.
282
+ **metadata: Additional message metadata.
283
+ """
284
+ # Add to buffer
285
+ self.buffer.add_message(content, message_type, format_style, **metadata)
286
+
287
+ # Display immediately if requested
288
+ if immediate_display:
289
+ self._display_message_immediately(content, message_type, format_style)
290
+
291
+ def start_streaming_response(self) -> None:
292
+ """Start a streaming response by setting up the display area."""
293
+ # Use the existing message display infrastructure
294
+ # Write to conversation area using proper positioning
295
+ self._display_message_immediately(
296
+ "\n∴ ", MessageType.ASSISTANT, MessageFormat.PLAIN
297
+ )
298
+
299
+ def write_streaming_chunk(self, chunk: str) -> None:
300
+ """Write a streaming chunk directly to the conversation area."""
301
+ # Use the display infrastructure to write in the conversation area
302
+ self._display_chunk_immediately(chunk)
303
+
304
+ def write_user_message(self, content: str, **metadata) -> None:
305
+ """Write a user message (convenience method).
306
+
307
+ Args:
308
+ content: User message content.
309
+ **metadata: Additional metadata.
310
+ """
311
+ self.write_message(
312
+ content, MessageType.USER, MessageFormat.GRADIENT, **metadata
313
+ )
314
+
315
+ def write_system_message(self, content: str, **metadata) -> None:
316
+ """Write a system message (convenience method).
317
+
318
+ Args:
319
+ content: System message content.
320
+ **metadata: Additional metadata.
321
+ """
322
+ self.write_message(
323
+ content, MessageType.SYSTEM, MessageFormat.HIGHLIGHTED, **metadata
324
+ )
325
+
326
+ def write_error_message(self, content: str, **metadata) -> None:
327
+ """Write an error message (convenience method).
328
+
329
+ Args:
330
+ content: Error message content.
331
+ **metadata: Additional metadata.
332
+ """
333
+ self.write_message(
334
+ content, MessageType.ERROR, MessageFormat.HIGHLIGHTED, **metadata
335
+ )
336
+
337
+ def _display_message_immediately(
338
+ self,
339
+ content: str,
340
+ message_type: MessageType,
341
+ format_style: MessageFormat,
342
+ ) -> None:
343
+ """Display a message immediately to the terminal.
344
+
345
+ Args:
346
+ content: Message content.
347
+ message_type: Type of message.
348
+ format_style: Formatting style.
349
+ """
350
+ # Skip display if content is empty (fix for duplicate display issue)
351
+ if not content or not content.strip():
352
+ return
353
+
354
+ # Check if we're in pipe mode (no formatting/symbols)
355
+ pipe_mode = getattr(self, 'pipe_mode', False)
356
+
357
+ # Store symbol info for later application (after gradient)
358
+ add_symbol = None
359
+ if not pipe_mode:
360
+ if message_type == MessageType.ASSISTANT and content.strip():
361
+ if not content.startswith("∴") and not content.startswith("\033[36m∴"):
362
+ add_symbol = "llm"
363
+ elif message_type == MessageType.USER:
364
+ add_symbol = "user"
365
+
366
+ # Create temporary message for formatting
367
+ temp_message = ConversationMessage(content, message_type, format_style)
368
+
369
+ # Format the message (apply gradient first) - skip in pipe mode
370
+ if self.auto_format and format_style != MessageFormat.PLAIN and not pipe_mode:
371
+ formatted_content = self.formatter.format_message(temp_message)
372
+ else:
373
+ formatted_content = content
374
+
375
+ # Add symbols AFTER gradient processing - skip in pipe mode
376
+ if add_symbol == "user":
377
+ formatted_content = f"\033[2;33m>\033[0m {formatted_content}"
378
+ elif add_symbol == "llm":
379
+ formatted_content = f"\033[36m∴\033[0m {formatted_content}"
380
+
381
+ # Exit raw mode temporarily for writing
382
+ # Handle both enum and string cases for current_mode
383
+ current_mode = getattr(
384
+ self.terminal_state.current_mode,
385
+ "value",
386
+ self.terminal_state.current_mode,
387
+ )
388
+ was_raw = current_mode == "raw"
389
+ if was_raw:
390
+ self.terminal_state.exit_raw_mode()
391
+
392
+ try:
393
+ # Write to terminal
394
+ print(formatted_content, flush=self.flush_immediately)
395
+ # Add blank line for visual separation between messages
396
+ print("", flush=self.flush_immediately)
397
+ logger.debug(
398
+ f"Displayed {message_type.value} message: {content[:50]}..."
399
+ )
400
+ finally:
401
+ # Restore raw mode if it was active
402
+ if was_raw:
403
+ self.terminal_state.enter_raw_mode()
404
+
405
+ def _display_chunk_immediately(self, chunk: str) -> None:
406
+ """Display a streaming chunk immediately without line breaks.
407
+
408
+ Args:
409
+ chunk: Text chunk to display.
410
+ """
411
+ # Exit raw mode temporarily for writing
412
+ current_mode = getattr(
413
+ self.terminal_state.current_mode,
414
+ "value",
415
+ self.terminal_state.current_mode,
416
+ )
417
+ was_raw = current_mode == "raw"
418
+ if was_raw:
419
+ self.terminal_state.exit_raw_mode()
420
+
421
+ try:
422
+ # Write chunk without newline and flush immediately
423
+ print(chunk, end="", flush=True)
424
+ finally:
425
+ # Restore raw mode if it was active
426
+ if was_raw:
427
+ self.terminal_state.enter_raw_mode()
428
+
429
+ def render_conversation_history(self, count: Optional[int] = None) -> List[str]:
430
+ """Render conversation history as formatted lines.
431
+
432
+ Args:
433
+ count: Number of recent messages to render (None for all).
434
+
435
+ Returns:
436
+ List of formatted message lines.
437
+ """
438
+ if count is None:
439
+ messages = list(self.buffer.messages)
440
+ else:
441
+ messages = self.buffer.get_recent_messages(count)
442
+
443
+ formatted_lines = []
444
+ for message in messages:
445
+ if self.auto_format:
446
+ formatted_content = self.formatter.format_message(message)
447
+ else:
448
+ formatted_content = message.content
449
+
450
+ # Split multi-line messages
451
+ lines = formatted_content.split("\n")
452
+ formatted_lines.extend(lines)
453
+
454
+ return formatted_lines
455
+
456
+ def clear_conversation_area(self) -> None:
457
+ """Clear the conversation display area."""
458
+ # This would typically involve clearing the terminal screen
459
+ # or specific regions, depending on layout management
460
+ if self.terminal_state.is_terminal:
461
+ self.terminal_state.write_raw(
462
+ "\033[2J\033[H"
463
+ ) # Clear screen, move to home
464
+ logger.debug("Cleared conversation area")
465
+
466
+ def set_auto_formatting(self, enabled: bool) -> None:
467
+ """Enable or disable automatic message formatting.
468
+
469
+ Args:
470
+ enabled: Whether to apply automatic formatting.
471
+ """
472
+ self.auto_format = enabled
473
+ logger.debug(f"Auto formatting {'enabled' if enabled else 'disabled'}")
474
+
475
+ def set_visual_effects(self, visual_effects) -> None:
476
+ """Update the visual effects instance.
477
+
478
+ Args:
479
+ visual_effects: New VisualEffects instance.
480
+ """
481
+ self.visual_effects = visual_effects
482
+ self.formatter = MessageFormatter(visual_effects)
483
+ logger.debug("Visual effects updated")
484
+
485
+ def get_render_stats(self) -> Dict[str, Any]:
486
+ """Get conversation rendering statistics.
487
+
488
+ Returns:
489
+ Dictionary with rendering statistics.
490
+ """
491
+ buffer_stats = self.buffer.get_stats()
492
+
493
+ return {
494
+ "buffer": buffer_stats,
495
+ "auto_format": self.auto_format,
496
+ "flush_immediately": self.flush_immediately,
497
+ "last_render_count": self._last_render_count,
498
+ "terminal_mode": (
499
+ getattr(
500
+ self.terminal_state.current_mode,
501
+ "value",
502
+ self.terminal_state.current_mode,
503
+ )
504
+ if self.terminal_state
505
+ else "unknown"
506
+ ),
507
+ }
508
+
509
+
510
+ class MessageRenderer:
511
+ """Main message rendering coordinator."""
512
+
513
+ def __init__(self, terminal_state, visual_effects=None):
514
+ """Initialize message renderer.
515
+
516
+ Args:
517
+ terminal_state: TerminalState instance.
518
+ visual_effects: VisualEffects instance.
519
+ """
520
+ self.conversation_renderer = ConversationRenderer(
521
+ terminal_state, visual_effects
522
+ )
523
+ self.terminal_state = terminal_state
524
+ self.visual_effects = visual_effects
525
+
526
+ def write_message(self, content: str, apply_gradient: bool = True) -> None:
527
+ """Write a message with optional gradient (backward compatibility).
528
+
529
+ Args:
530
+ content: Message content.
531
+ apply_gradient: Whether to apply gradient effect.
532
+ """
533
+ format_style = (
534
+ MessageFormat.GRADIENT if apply_gradient else MessageFormat.PLAIN
535
+ )
536
+ self.conversation_renderer.write_message(content, format_style=format_style)
537
+
538
+ def write_streaming_chunk(self, chunk: str) -> None:
539
+ """Write a streaming chunk without buffering for real-time display.
540
+
541
+ Args:
542
+ chunk: Text chunk to write immediately.
543
+ """
544
+ # Initialize streaming state if needed
545
+ if not hasattr(self, "_streaming_buffer"):
546
+ self._streaming_buffer = ""
547
+ # Use the conversation renderer to start streaming properly
548
+ self.conversation_renderer.start_streaming_response()
549
+
550
+ # Add chunk to buffer and display it through conversation renderer
551
+ self._streaming_buffer += chunk
552
+ self.conversation_renderer.write_streaming_chunk(chunk)
553
+
554
+ def finish_streaming_message(self) -> None:
555
+ """Finish streaming and properly format the complete message."""
556
+ if hasattr(self, "_streaming_buffer"):
557
+ # End the streaming line
558
+ self.terminal_state.write_raw("\n\n")
559
+ # Reset streaming state
560
+ del self._streaming_buffer
561
+
562
+ def write_user_message(self, content: str) -> None:
563
+ """Write a user message (backward compatibility).
564
+
565
+ Args:
566
+ content: User message content.
567
+ """
568
+ self.conversation_renderer.write_user_message(content)
569
+
570
+ def write_system_message(self, content: str, **metadata) -> None:
571
+ """Write a system message (delegated to conversation renderer).
572
+
573
+ Args:
574
+ content: System message content.
575
+ **metadata: Additional metadata.
576
+ """
577
+ self.conversation_renderer.write_system_message(content, **metadata)
578
+
579
+ def get_conversation_buffer(self) -> ConversationBuffer:
580
+ """Get the conversation buffer for direct access.
581
+
582
+ Returns:
583
+ ConversationBuffer instance.
584
+ """
585
+ return self.conversation_renderer.buffer
586
+
587
+ def get_stats(self) -> Dict[str, Any]:
588
+ """Get comprehensive message rendering statistics.
589
+
590
+ Returns:
591
+ Dictionary with all rendering statistics.
592
+ """
593
+ return {
594
+ "conversation": self.conversation_renderer.get_render_stats(),
595
+ "terminal_state": (
596
+ self.terminal_state.get_status() if self.terminal_state else {}
597
+ ),
598
+ "visual_effects": (
599
+ self.visual_effects.get_effect_stats() if self.visual_effects else {}
600
+ ),
601
+ }