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
core/io/layout.py ADDED
@@ -0,0 +1,587 @@
1
+ """Layout management system for terminal rendering."""
2
+
3
+ import re
4
+ from collections import deque
5
+ from dataclasses import dataclass, field
6
+ from enum import Enum
7
+ from typing import List, Optional, Dict, Any, Tuple
8
+
9
+
10
+ class LayoutMode(Enum):
11
+ """Layout rendering modes."""
12
+
13
+ HORIZONTAL = "horizontal"
14
+ VERTICAL = "vertical"
15
+ STACKED = "stacked"
16
+ ADAPTIVE = "adaptive"
17
+
18
+
19
+ class AreaAlignment(Enum):
20
+ """Alignment options for layout areas."""
21
+
22
+ LEFT = "left"
23
+ CENTER = "center"
24
+ RIGHT = "right"
25
+ JUSTIFY = "justify"
26
+
27
+
28
+ @dataclass
29
+ class ScreenRegion:
30
+ """Represents a region of the screen."""
31
+
32
+ x: int
33
+ y: int
34
+ width: int
35
+ height: int
36
+
37
+ def contains_point(self, x: int, y: int) -> bool:
38
+ """Check if point is within this region."""
39
+ return (
40
+ self.x <= x < self.x + self.width and self.y <= y < self.y + self.height
41
+ )
42
+
43
+ def intersects(self, other: "ScreenRegion") -> bool:
44
+ """Check if this region intersects with another."""
45
+ return not (
46
+ self.x + self.width <= other.x
47
+ or other.x + other.width <= self.x
48
+ or self.y + self.height <= other.y
49
+ or other.y + other.height <= self.y
50
+ )
51
+
52
+
53
+ @dataclass
54
+ class LayoutArea:
55
+ """Represents a layout area with content and configuration."""
56
+
57
+ name: str
58
+ content: List[str] = field(default_factory=list)
59
+ region: Optional[ScreenRegion] = None
60
+ alignment: AreaAlignment = AreaAlignment.LEFT
61
+ visible: bool = True
62
+ priority: int = 0
63
+ min_width: int = 10
64
+ min_height: int = 1
65
+ max_width: Optional[int] = None
66
+ max_height: Optional[int] = None
67
+ padding: int = 0
68
+
69
+ def get_content_width(self) -> int:
70
+ """Get the maximum width of content in this area."""
71
+ if not self.content:
72
+ return 0
73
+ # Account for ANSI codes when measuring width
74
+ return max(len(self._strip_ansi(line)) for line in self.content)
75
+
76
+ def get_content_height(self) -> int:
77
+ """Get the height of content in this area."""
78
+ return len(self.content)
79
+
80
+ def _strip_ansi(self, text: str) -> str:
81
+ """Remove ANSI escape codes from text for width calculation."""
82
+ return re.sub(r"\033\[[0-9;]*m", "", text)
83
+
84
+
85
+ class ThinkingAnimationManager:
86
+ """Manages thinking animation state and display."""
87
+
88
+ def __init__(self, spinner_frames: List[str] = None):
89
+ """Initialize thinking animation manager.
90
+
91
+ Args:
92
+ spinner_frames: Custom spinner frames (uses default if None).
93
+ """
94
+ self.spinner_frames = spinner_frames or [
95
+ "⠋",
96
+ "⠙",
97
+ "⠹",
98
+ "⠸",
99
+ "⠼",
100
+ "⠴",
101
+ "⠦",
102
+ "⠧",
103
+ ]
104
+ self.current_frame = 0
105
+ self.is_active = False
106
+ self.start_time = None
107
+ self.messages = deque(maxlen=2)
108
+
109
+ def start_thinking(self, message: str = "") -> None:
110
+ """Start thinking animation with optional message.
111
+
112
+ Args:
113
+ message: Thinking message to display.
114
+ """
115
+ import time
116
+
117
+ self.is_active = True
118
+ if message:
119
+ # Clear previous messages and set the new one
120
+ self.messages.clear()
121
+ self.messages.append(message)
122
+ if not self.start_time:
123
+ self.start_time = time.time()
124
+
125
+ def stop_thinking(self) -> Optional[str]:
126
+ """Stop thinking animation and return completion message.
127
+
128
+ Returns:
129
+ Completion message if thinking was active.
130
+ """
131
+ import time
132
+
133
+ if not self.is_active:
134
+ return None
135
+
136
+ self.is_active = False
137
+ completion_msg = None
138
+
139
+ if self.start_time:
140
+ duration = time.time() - self.start_time
141
+ completion_msg = f"Thought for {duration:.1f} seconds"
142
+ self.start_time = None
143
+
144
+ self.messages.clear()
145
+ return completion_msg
146
+
147
+ def get_next_frame(self) -> str:
148
+ """Get next spinner frame for animation.
149
+
150
+ Returns:
151
+ Current spinner frame character.
152
+ """
153
+ if not self.is_active:
154
+ return ""
155
+
156
+ frame = self.spinner_frames[self.current_frame]
157
+ self.current_frame = (self.current_frame + 1) % len(self.spinner_frames)
158
+ return frame
159
+
160
+ def get_display_lines(self, apply_effect_func) -> List[str]:
161
+ """Get formatted display lines for thinking animation.
162
+
163
+ Args:
164
+ apply_effect_func: Function to apply visual effects to text.
165
+
166
+ Returns:
167
+ List of formatted display lines.
168
+ """
169
+ if not self.is_active or not self.messages:
170
+ return []
171
+
172
+ lines = []
173
+ spinner = self.get_next_frame()
174
+
175
+ for i, msg in enumerate(self.messages):
176
+ if i == len(self.messages) - 1:
177
+ # Main thinking line with spinner
178
+ formatted_text = apply_effect_func(f"{spinner} Thinking: {msg}")
179
+ lines.append(formatted_text)
180
+ else:
181
+ # Secondary thinking line
182
+ formatted_text = apply_effect_func(f" {msg}")
183
+ lines.append(formatted_text)
184
+
185
+ return lines
186
+
187
+
188
+ class LayoutManager:
189
+ """Manages terminal layout with multiple areas and adaptive sizing."""
190
+
191
+ def __init__(self, terminal_width: int = 80, terminal_height: int = 24):
192
+ """Initialize layout manager.
193
+
194
+ Args:
195
+ terminal_width: Terminal width in characters.
196
+ terminal_height: Terminal height in characters.
197
+ """
198
+ self.terminal_width = terminal_width
199
+ self.terminal_height = terminal_height
200
+
201
+ # Layout areas
202
+ self._areas: Dict[str, LayoutArea] = {}
203
+
204
+ # Layout state
205
+ self._dirty = True
206
+ self._last_render_lines = 0
207
+
208
+ # Initialize standard areas
209
+ self._initialize_standard_areas()
210
+
211
+ def _initialize_standard_areas(self) -> None:
212
+ """Initialize standard layout areas (status, input, thinking)."""
213
+ self._areas["status_a"] = LayoutArea("status_a", priority=10)
214
+ self._areas["status_b"] = LayoutArea("status_b", priority=10)
215
+ self._areas["status_c"] = LayoutArea("status_c", priority=10)
216
+ self._areas["input"] = LayoutArea("input", priority=20)
217
+ self._areas["thinking"] = LayoutArea("thinking", priority=30)
218
+
219
+ def set_terminal_size(self, width: int, height: int) -> None:
220
+ """Update terminal dimensions.
221
+
222
+ Args:
223
+ width: New terminal width.
224
+ height: New terminal height.
225
+ """
226
+ if self.terminal_width != width or self.terminal_height != height:
227
+ self.terminal_width = width
228
+ self.terminal_height = height
229
+ self._dirty = True
230
+
231
+ def add_area(self, name: str, area: LayoutArea) -> None:
232
+ """Add a layout area.
233
+
234
+ Args:
235
+ name: Area name.
236
+ area: LayoutArea instance.
237
+ """
238
+ self._areas[name] = area
239
+ self._dirty = True
240
+
241
+ def get_area(self, name: str) -> Optional[LayoutArea]:
242
+ """Get a layout area by name.
243
+
244
+ Args:
245
+ name: Area name.
246
+
247
+ Returns:
248
+ LayoutArea instance or None if not found.
249
+ """
250
+ return self._areas.get(name)
251
+
252
+ def update_area_content(self, name: str, content: List[str]) -> None:
253
+ """Update content for a specific area.
254
+
255
+ Args:
256
+ name: Area name.
257
+ content: New content lines.
258
+ """
259
+ area = self._areas.get(name)
260
+ if area:
261
+ area.content = content.copy()
262
+ self._dirty = True
263
+
264
+ def set_area_visibility(self, name: str, visible: bool) -> None:
265
+ """Set visibility for a specific area.
266
+
267
+ Args:
268
+ name: Area name.
269
+ visible: Whether area should be visible.
270
+ """
271
+ area = self._areas.get(name)
272
+ if area and area.visible != visible:
273
+ area.visible = visible
274
+ self._dirty = True
275
+
276
+ def calculate_layout(
277
+ self, mode: LayoutMode = LayoutMode.ADAPTIVE
278
+ ) -> Dict[str, ScreenRegion]:
279
+ """Calculate layout regions for all visible areas.
280
+
281
+ Args:
282
+ mode: Layout mode to use.
283
+
284
+ Returns:
285
+ Dictionary mapping area names to screen regions.
286
+ """
287
+ visible_areas = {
288
+ name: area
289
+ for name, area in self._areas.items()
290
+ if area.visible and area.content
291
+ }
292
+
293
+ if not visible_areas:
294
+ return {}
295
+
296
+ if mode == LayoutMode.ADAPTIVE:
297
+ return self._calculate_adaptive_layout(visible_areas)
298
+ elif mode == LayoutMode.HORIZONTAL:
299
+ return self._calculate_horizontal_layout(visible_areas)
300
+ elif mode == LayoutMode.VERTICAL:
301
+ return self._calculate_vertical_layout(visible_areas)
302
+ else:
303
+ return self._calculate_stacked_layout(visible_areas)
304
+
305
+ def _calculate_adaptive_layout(
306
+ self, areas: Dict[str, LayoutArea]
307
+ ) -> Dict[str, ScreenRegion]:
308
+ """Calculate adaptive layout based on terminal size and content.
309
+
310
+ Args:
311
+ areas: Dictionary of visible areas.
312
+
313
+ Returns:
314
+ Dictionary mapping area names to screen regions.
315
+ """
316
+ regions = {}
317
+ current_y = 0
318
+
319
+ # Sort areas by priority (higher priority first)
320
+ _ = sorted(areas.items(), key=lambda x: x[1].priority, reverse=True)
321
+
322
+ # Handle thinking area first (if present)
323
+ if "thinking" in areas and areas["thinking"].content:
324
+ thinking_height = areas["thinking"].get_content_height()
325
+ regions["thinking"] = ScreenRegion(
326
+ 0, current_y, self.terminal_width, thinking_height
327
+ )
328
+ current_y += thinking_height + 1 # Add spacing
329
+
330
+ # Handle input area
331
+ if "input" in areas and areas["input"].content:
332
+ input_height = areas["input"].get_content_height()
333
+ regions["input"] = ScreenRegion(
334
+ 0, current_y, self.terminal_width, input_height
335
+ )
336
+ current_y += input_height + 1
337
+
338
+ # Handle status areas
339
+ status_areas = {
340
+ name: area
341
+ for name, area in areas.items()
342
+ if name.startswith("status_") and area.content
343
+ }
344
+
345
+ if status_areas:
346
+ status_regions = self._layout_status_areas(status_areas, current_y)
347
+ regions.update(status_regions)
348
+
349
+ return regions
350
+
351
+ def _calculate_horizontal_layout(
352
+ self, areas: Dict[str, LayoutArea]
353
+ ) -> Dict[str, ScreenRegion]:
354
+ """Calculate horizontal layout (side-by-side areas).
355
+
356
+ Args:
357
+ areas: Dictionary of visible areas.
358
+
359
+ Returns:
360
+ Dictionary mapping area names to screen regions.
361
+ """
362
+ regions = {}
363
+ area_count = len(areas)
364
+
365
+ if area_count == 0:
366
+ return regions
367
+
368
+ area_width = max(1, (self.terminal_width - (area_count - 1)) // area_count)
369
+ current_x = 0
370
+
371
+ for i, (name, area) in enumerate(areas.items()):
372
+ # Last area gets remaining width
373
+ if i == area_count - 1:
374
+ width = self.terminal_width - current_x
375
+ else:
376
+ width = area_width
377
+
378
+ regions[name] = ScreenRegion(
379
+ current_x, 0, width, area.get_content_height()
380
+ )
381
+ current_x += width + 1 # Add spacing
382
+
383
+ return regions
384
+
385
+ def _calculate_vertical_layout(
386
+ self, areas: Dict[str, LayoutArea]
387
+ ) -> Dict[str, ScreenRegion]:
388
+ """Calculate vertical layout (stacked areas).
389
+
390
+ Args:
391
+ areas: Dictionary of visible areas.
392
+
393
+ Returns:
394
+ Dictionary mapping area names to screen regions.
395
+ """
396
+ regions = {}
397
+ current_y = 0
398
+
399
+ for name, area in areas.items():
400
+ height = area.get_content_height()
401
+ regions[name] = ScreenRegion(0, current_y, self.terminal_width, height)
402
+ current_y += height + 1 # Add spacing
403
+
404
+ return regions
405
+
406
+ def _calculate_stacked_layout(
407
+ self, areas: Dict[str, LayoutArea]
408
+ ) -> Dict[str, ScreenRegion]:
409
+ """Calculate stacked layout (overlapping areas).
410
+
411
+ Args:
412
+ areas: Dictionary of visible areas.
413
+
414
+ Returns:
415
+ Dictionary mapping area names to screen regions.
416
+ """
417
+ regions = {}
418
+
419
+ # Simple stacked layout - each area takes full width
420
+ for name, area in areas.items():
421
+ height = area.get_content_height()
422
+ regions[name] = ScreenRegion(0, 0, self.terminal_width, height)
423
+
424
+ return regions
425
+
426
+ def _layout_status_areas(
427
+ self, status_areas: Dict[str, LayoutArea], start_y: int
428
+ ) -> Dict[str, ScreenRegion]:
429
+ """Layout status areas with adaptive column management.
430
+
431
+ Args:
432
+ status_areas: Dictionary of status areas.
433
+ start_y: Starting Y position.
434
+
435
+ Returns:
436
+ Dictionary mapping status area names to screen regions.
437
+ """
438
+ regions = {}
439
+
440
+ if not status_areas:
441
+ return regions
442
+
443
+ # Determine layout based on terminal width
444
+ if self.terminal_width >= 80:
445
+ # Three-column layout for wide terminals
446
+ column_width = (self.terminal_width - 6) // 3 # 6 for spacing
447
+
448
+ # Get areas A, B, C in order
449
+ area_names = ["status_a", "status_b", "status_c"]
450
+ areas_with_content = [
451
+ (name, status_areas.get(name))
452
+ for name in area_names
453
+ if name in status_areas
454
+ ]
455
+
456
+ max_height = (
457
+ max(area.get_content_height() for _, area in areas_with_content)
458
+ if areas_with_content
459
+ else 1
460
+ )
461
+
462
+ for i, (name, area) in enumerate(areas_with_content):
463
+ x_pos = i * (column_width + 2) # 2 for spacing
464
+ regions[name] = ScreenRegion(
465
+ x_pos, start_y, column_width, max_height
466
+ )
467
+ else:
468
+ # Vertical layout for narrow terminals
469
+ current_y = start_y
470
+ for name, area in status_areas.items():
471
+ height = area.get_content_height()
472
+ regions[name] = ScreenRegion(
473
+ 0, current_y, self.terminal_width, height
474
+ )
475
+ current_y += height + 1
476
+
477
+ return regions
478
+
479
+ def render_areas(self, regions: Dict[str, ScreenRegion]) -> List[str]:
480
+ """Render all areas into display lines.
481
+
482
+ Args:
483
+ regions: Dictionary mapping area names to screen regions.
484
+
485
+ Returns:
486
+ List of formatted display lines.
487
+ """
488
+ lines = []
489
+ max_y = (
490
+ max(region.y + region.height for region in regions.values())
491
+ if regions
492
+ else 0
493
+ )
494
+
495
+ for y in range(max_y):
496
+ line_parts = {}
497
+
498
+ # Collect content for this line from all areas
499
+ for name, region in regions.items():
500
+ if region.y <= y < region.y + region.height:
501
+ area = self._areas[name]
502
+ content_index = y - region.y
503
+
504
+ if content_index < len(area.content):
505
+ content = area.content[content_index]
506
+ line_parts[region.x] = (
507
+ region,
508
+ content,
509
+ area.alignment,
510
+ )
511
+
512
+ # Build the line
513
+ if line_parts:
514
+ line = self._build_line_from_parts(line_parts)
515
+ lines.append(line)
516
+ else:
517
+ lines.append("")
518
+
519
+ self._last_render_lines = len(lines)
520
+ self._dirty = False
521
+ return lines
522
+
523
+ def _build_line_from_parts(
524
+ self, line_parts: Dict[int, Tuple[ScreenRegion, str, AreaAlignment]]
525
+ ) -> str:
526
+ """Build a single display line from area parts.
527
+
528
+ Args:
529
+ line_parts: Dictionary mapping x-position to (region, content, alignment).
530
+
531
+ Returns:
532
+ Formatted line string.
533
+ """
534
+ if not line_parts:
535
+ return ""
536
+
537
+ # Sort by x position
538
+ sorted_parts = sorted(line_parts.items())
539
+ line_chars = [" "] * self.terminal_width
540
+
541
+ for x_pos, (region, content, alignment) in sorted_parts:
542
+ # Remove ANSI codes for width calculation
543
+ visible_content = re.sub(r"\033\[[0-9;]*m", "", content)
544
+
545
+ # Apply alignment within the region
546
+ if alignment == AreaAlignment.CENTER:
547
+ padding = max(0, (region.width - len(visible_content)) // 2)
548
+ start_x = region.x + padding
549
+ elif alignment == AreaAlignment.RIGHT:
550
+ padding = max(0, region.width - len(visible_content))
551
+ start_x = region.x + padding
552
+ else: # LEFT or JUSTIFY
553
+ start_x = region.x
554
+
555
+ # Place content in line, handling ANSI codes
556
+ content_chars = list(content)
557
+ for i, char in enumerate(content_chars):
558
+ pos = start_x + i
559
+ if 0 <= pos < self.terminal_width and pos < region.x + region.width:
560
+ line_chars[pos] = char
561
+
562
+ return "".join(line_chars).rstrip()
563
+
564
+ def get_render_info(self) -> Dict[str, Any]:
565
+ """Get layout rendering information for debugging.
566
+
567
+ Returns:
568
+ Dictionary with layout information.
569
+ """
570
+ return {
571
+ "terminal_size": (self.terminal_width, self.terminal_height),
572
+ "areas_count": len(self._areas),
573
+ "visible_areas": [
574
+ name for name, area in self._areas.items() if area.visible
575
+ ],
576
+ "dirty": self._dirty,
577
+ "last_render_lines": self._last_render_lines,
578
+ "area_stats": {
579
+ name: {
580
+ "content_lines": len(area.content),
581
+ "content_width": area.get_content_width(),
582
+ "visible": area.visible,
583
+ "priority": area.priority,
584
+ }
585
+ for name, area in self._areas.items()
586
+ },
587
+ }