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,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
|
+
}
|