pdd-cli 0.0.39__py3-none-any.whl → 0.0.41__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.

Potentially problematic release.


This version of pdd-cli might be problematic. Click here for more details.

pdd/sync_animation.py ADDED
@@ -0,0 +1,643 @@
1
+ import time
2
+ import os
3
+ from datetime import datetime, timedelta
4
+ import threading
5
+ from typing import List, Dict, Optional, Tuple, Any
6
+
7
+ from rich.console import Console
8
+ from rich.live import Live
9
+ from rich.layout import Layout
10
+ from rich.panel import Panel
11
+ from rich.text import Text
12
+ from rich.align import Align
13
+ from rich.table import Table
14
+ from rich.progress_bar import ProgressBar # For cost/budget display if needed
15
+
16
+ from . import logo_animation
17
+
18
+ # Assuming these might be in pdd/__init__.py or a constants module
19
+ # For this example, defining them locally based on the branding document
20
+ # Primary Colors
21
+ DEEP_NAVY = "#0A0A23"
22
+ ELECTRIC_CYAN = "#00D8FF"
23
+
24
+ # Accent Colors (can be used for boxes if specific inputs are not good)
25
+ LUMEN_PURPLE = "#8C47FF"
26
+ PROMPT_MAGENTA = "#FF2AA6"
27
+ BUILD_GREEN = "#18C07A" # Success, good for 'example' or 'tests'
28
+
29
+ # Default colors for boxes if not provided or invalid
30
+ DEFAULT_PROMPT_COLOR = LUMEN_PURPLE
31
+ DEFAULT_CODE_COLOR = ELECTRIC_CYAN
32
+ DEFAULT_EXAMPLE_COLOR = BUILD_GREEN
33
+ DEFAULT_TESTS_COLOR = PROMPT_MAGENTA
34
+
35
+ # PDD Logo ASCII Art from branding document (section 7)
36
+ PDD_LOGO_ASCII = [
37
+ " +xxxxxxxxxxxxxxx+ ",
38
+ "xxxxxxxxxxxxxxxxxxxxx+ ",
39
+ "xxx +xx+ ",
40
+ "xxx x+ xx+ ",
41
+ "xxx x+ xxx ",
42
+ "xxx x+ xx+ ",
43
+ "xxx x+ xx+ ",
44
+ "xxx x+ xxx ",
45
+ "xxx +xx+ ",
46
+ "xxx +xxxxxxxxxxx+ ",
47
+ "xxx +xx+ ",
48
+ "xxx +xx+ ",
49
+ "xxx+xx+ ",
50
+ "xxxx+ ",
51
+ "xx+ ",
52
+ ]
53
+ LOGO_HEIGHT = len(PDD_LOGO_ASCII)
54
+ LOGO_MAX_WIDTH = max(len(line) for line in PDD_LOGO_ASCII)
55
+
56
+ # Emojis for commands
57
+ EMOJIS = {
58
+ "generate": "🔨",
59
+ "example": "🌱",
60
+ "crash_code": "💀",
61
+ "crash_example": "💀",
62
+ "verify_code": "🔍",
63
+ "verify_example": "🔍",
64
+ "test": "🧪",
65
+ "fix_code": "🔧",
66
+ "fix_tests": "🔧",
67
+ "update": "⬆️",
68
+ "auto-deps": "📦",
69
+ "checking": "🔍",
70
+ }
71
+
72
+ CONSOLE_WIDTH = 80 # Target console width for layout
73
+ ANIMATION_BOX_HEIGHT = 18 # Target height for the main animation box
74
+
75
+ def _get_valid_color(color_str: Optional[str], default_color: str) -> str:
76
+ """Validates a color string or returns default."""
77
+ if not color_str:
78
+ return default_color
79
+ return color_str if isinstance(color_str, str) else default_color
80
+
81
+ def _shorten_path(path_str: Optional[str], max_len: int) -> str:
82
+ """Shortens a path string for display, trying relative path first."""
83
+ if not path_str:
84
+ return ""
85
+ try:
86
+ rel_path = os.path.relpath(path_str, start=os.getcwd())
87
+ except ValueError:
88
+ rel_path = path_str
89
+
90
+ if len(rel_path) <= max_len:
91
+ return rel_path
92
+
93
+ basename = os.path.basename(rel_path)
94
+ if len(basename) <= max_len:
95
+ return basename
96
+
97
+ return "..." + basename[-(max_len-3):]
98
+
99
+
100
+ class AnimationState:
101
+ """Holds the current state of the animation."""
102
+ def __init__(self, basename: str, budget: Optional[float]):
103
+ self.current_function_name: str = "checking"
104
+ self.basename: str = basename
105
+ self.cost: float = 0.0
106
+ self.budget: float = budget if budget is not None else float('inf')
107
+ self.start_time: datetime = datetime.now()
108
+ self.frame_count: int = 0
109
+
110
+ self.paths: Dict[str, str] = {"prompt": "", "code": "", "example": "", "tests": ""}
111
+ self.colors: Dict[str, str] = {
112
+ "prompt": DEFAULT_PROMPT_COLOR, "code": DEFAULT_CODE_COLOR,
113
+ "example": DEFAULT_EXAMPLE_COLOR, "tests": DEFAULT_TESTS_COLOR
114
+ }
115
+ self.scroll_offsets: Dict[str, int] = {"prompt": 0, "code": 0, "example": 0, "tests": 0}
116
+ self.path_box_content_width = 16 # Base chars for path inside its small box (will be dynamic)
117
+ self.auto_deps_progress: int = 0 # Progress counter for auto-deps border thickening
118
+
119
+ def update_dynamic_state(self, function_name: str, cost: float,
120
+ prompt_path: str, code_path: str, example_path: str, tests_path: str):
121
+ self.current_function_name = function_name.lower() if function_name else "checking"
122
+ self.cost = cost if cost is not None else self.cost
123
+
124
+ self.paths["prompt"] = prompt_path or ""
125
+ self.paths["code"] = code_path or ""
126
+ self.paths["example"] = example_path or ""
127
+ self.paths["tests"] = tests_path or ""
128
+
129
+ # Update auto-deps progress for border thickening animation
130
+ if self.current_function_name == "auto-deps":
131
+ self.auto_deps_progress = (self.auto_deps_progress + 1) % 120 # Cycle every 12 seconds at 10fps
132
+
133
+ def set_box_colors(self, prompt_color: str, code_color: str, example_color: str, tests_color: str):
134
+ self.colors["prompt"] = _get_valid_color(prompt_color, DEFAULT_PROMPT_COLOR)
135
+ self.colors["code"] = _get_valid_color(code_color, DEFAULT_CODE_COLOR)
136
+ self.colors["example"] = _get_valid_color(example_color, DEFAULT_EXAMPLE_COLOR)
137
+ self.colors["tests"] = _get_valid_color(tests_color, DEFAULT_TESTS_COLOR)
138
+
139
+ def get_elapsed_time_str(self) -> str:
140
+ elapsed = datetime.now() - self.start_time
141
+ return str(elapsed).split('.')[0] # Format as HH:MM:SS
142
+
143
+ def _render_scrolling_path(self, path_key: str, content_width: int) -> str:
144
+ """Renders a path, scrolling if it's too long for its display box."""
145
+ full_display_path = _shorten_path(self.paths[path_key], 100)
146
+
147
+ if not full_display_path:
148
+ return " " * content_width
149
+
150
+ if len(full_display_path) <= content_width:
151
+ return full_display_path.center(content_width)
152
+
153
+ offset = self.scroll_offsets[path_key]
154
+ padded_text = f" {full_display_path} :: {full_display_path} "
155
+ display_text = padded_text[offset : offset + content_width]
156
+
157
+ self.scroll_offsets[path_key] = (offset + 1) % (len(full_display_path) + 4)
158
+ return display_text
159
+
160
+ def get_emoji_for_box(self, box_name: str, blink_on: bool) -> str:
161
+ """Gets the emoji for a given box based on the current function."""
162
+ cmd = self.current_function_name
163
+ emoji_char = ""
164
+
165
+ if cmd == "checking":
166
+ emoji_char = EMOJIS["checking"]
167
+ elif cmd == "generate" and box_name == "code":
168
+ emoji_char = EMOJIS["generate"]
169
+ elif cmd == "example" and box_name == "example":
170
+ emoji_char = EMOJIS["example"]
171
+ elif cmd == "crash":
172
+ if box_name == "code":
173
+ emoji_char = EMOJIS["crash_code"]
174
+ elif box_name == "example":
175
+ emoji_char = EMOJIS["crash_example"]
176
+ elif cmd == "verify":
177
+ if box_name == "code":
178
+ emoji_char = EMOJIS["verify_code"]
179
+ elif box_name == "example":
180
+ emoji_char = EMOJIS["verify_example"]
181
+ elif cmd == "test" and box_name == "tests":
182
+ emoji_char = EMOJIS["test"]
183
+ elif cmd == "fix":
184
+ if box_name == "code":
185
+ emoji_char = EMOJIS["fix_code"]
186
+ elif box_name == "tests":
187
+ emoji_char = EMOJIS["fix_tests"]
188
+ elif cmd == "update" and box_name == "prompt":
189
+ emoji_char = EMOJIS["update"]
190
+ elif cmd == "auto-deps" and box_name == "prompt":
191
+ emoji_char = EMOJIS["auto-deps"]
192
+
193
+ # Always return 2 chars to prevent shifting, with space after emoji
194
+ if blink_on and emoji_char:
195
+ return emoji_char + " "
196
+ else:
197
+ return " "
198
+
199
+ def _get_path_waypoints(cmd: str, code_x: int, example_x: int, tests_x: int, prompt_x: int) -> List[Tuple[int, int, str]]:
200
+ """Returns waypoints (x, y, direction) for the arrow path based on command."""
201
+ waypoints = []
202
+
203
+ if cmd == "generate": # Prompt -> Code
204
+ waypoints = [
205
+ (prompt_x, 0, "v"), # Start at prompt, go down
206
+ (prompt_x, 1, "v"), # Continue down
207
+ (prompt_x, 2, ">"), # Turn right at junction
208
+ (code_x, 2, "v"), # Turn down at code column
209
+ (code_x, 3, "v"), # Continue down
210
+ (code_x, 4, "v"), # Final down to code box
211
+ (code_x, 5, "v") # Connect to code box
212
+ ]
213
+ elif cmd == "example": # Prompt -> Example (straight down)
214
+ waypoints = [
215
+ (prompt_x, 0, "v"), # Start at prompt, go down
216
+ (prompt_x, 1, "v"), # Continue down
217
+ (prompt_x, 2, "v"), # Continue down through junction
218
+ (prompt_x, 3, "v"), # Continue down
219
+ (prompt_x, 4, "v"), # Final down to example box
220
+ (prompt_x, 5, "v") # Connect to example box
221
+ ]
222
+ elif cmd == "test": # Prompt -> Tests
223
+ waypoints = [
224
+ (prompt_x, 0, "v"), # Start at prompt, go down
225
+ (prompt_x, 1, "v"), # Continue down
226
+ (prompt_x, 2, ">"), # Turn right at junction
227
+ (tests_x, 2, "v"), # Turn down at tests column
228
+ (tests_x, 3, "v"), # Continue down
229
+ (tests_x, 4, "v"), # Final down to tests box
230
+ (tests_x, 5, "v") # Connect to tests box
231
+ ]
232
+ elif cmd == "auto-deps": # No arrow animation - focus on border thickening
233
+ waypoints = [] # Empty waypoints means no arrow animation
234
+ elif cmd == "update": # Code -> Prompt
235
+ waypoints = [
236
+ (code_x, 5, "^"), # Start from code box, go up
237
+ (code_x, 4, "^"), # Continue up
238
+ (code_x, 3, "^"), # Continue up
239
+ (code_x, 2, ">"), # Turn right at junction
240
+ (prompt_x, 2, "^"), # Turn up at prompt column
241
+ (prompt_x, 1, "^"), # Continue up
242
+ (prompt_x, 0, "^") # Final up to prompt box
243
+ ]
244
+ elif cmd in ["crash", "verify"]: # Code <-> Example (bidirectional)
245
+ waypoints = [
246
+ (code_x, 5, "^"), # Start from code box, go up
247
+ (code_x, 4, "^"), # Continue up
248
+ (code_x, 3, "^"), # Continue up
249
+ (code_x, 2, ">"), # Turn right at junction
250
+ (example_x, 2, "v"), # Turn down at example column
251
+ (example_x, 3, "v"), # Continue down
252
+ (example_x, 4, "v"), # Continue down
253
+ (example_x, 5, "v") # Final down to example box
254
+ ]
255
+ elif cmd == "fix": # Code <-> Tests (bidirectional)
256
+ waypoints = [
257
+ (code_x, 5, "^"), # Start from code box, go up
258
+ (code_x, 4, "^"), # Continue up
259
+ (code_x, 3, "^"), # Continue up
260
+ (code_x, 2, ">"), # Turn right at junction
261
+ (tests_x, 2, "v"), # Turn down at tests column
262
+ (tests_x, 3, "v"), # Continue down
263
+ (tests_x, 4, "v"), # Continue down
264
+ (tests_x, 5, "v") # Final down to tests box
265
+ ]
266
+
267
+ return waypoints
268
+
269
+ def _draw_connecting_lines_and_arrows(state: AnimationState, console_width: int) -> List[Text]:
270
+ """Generates Text objects for lines and arrows based on current command."""
271
+ lines = []
272
+ cmd = state.current_function_name
273
+ frame = state.frame_count
274
+
275
+ # Dynamic positioning based on actual console width and auto-sized boxes
276
+ # Calculate dynamic box width (same as in main render function)
277
+ margin_space = 8 # Total margin space
278
+ inter_box_space = 4 # Space between boxes (2 spaces each side)
279
+ available_width = console_width - margin_space - inter_box_space
280
+ box_width = max(state.path_box_content_width + 4, available_width // 3)
281
+
282
+ # Calculate actual positions based on Rich's table layout
283
+ # Rich centers the table automatically, so we need to account for that
284
+ total_table_width = 3 * box_width + inter_box_space
285
+ table_start = (console_width - total_table_width) // 2
286
+
287
+ # Position connectors at the center of each box
288
+ code_x = table_start + box_width // 2
289
+ example_x = table_start + box_width + (inter_box_space // 2) + box_width // 2
290
+ tests_x = table_start + 2 * box_width + inter_box_space + box_width // 2
291
+
292
+ # Prompt should align with the center box (Example)
293
+ prompt_x = example_x
294
+
295
+ # Animation parameters
296
+ animation_cycle = 60 # Longer cycle for smoother animation
297
+ waypoints = _get_path_waypoints(cmd, code_x, example_x, tests_x, prompt_x)
298
+
299
+ # Handle bidirectional commands
300
+ if cmd in ["crash", "verify", "fix"]:
301
+ full_cycle = (frame // animation_cycle) % 2
302
+ if full_cycle == 1: # Reverse direction
303
+ if cmd in ["crash", "verify"]:
304
+ # Example -> Code
305
+ waypoints = [
306
+ (example_x, 5, "^"), # Start from example box, go up
307
+ (example_x, 4, "^"), # Continue up
308
+ (example_x, 3, "^"), # Continue up
309
+ (example_x, 2, "<"), # Turn left at junction
310
+ (code_x, 2, "v"), # Turn down at code column
311
+ (code_x, 3, "v"), # Continue down
312
+ (code_x, 4, "v"), # Continue down
313
+ (code_x, 5, "v") # Final down to code box
314
+ ]
315
+ elif cmd == "fix":
316
+ # Tests -> Code
317
+ waypoints = [
318
+ (tests_x, 5, "^"), # Start from tests box, go up
319
+ (tests_x, 4, "^"), # Continue up
320
+ (tests_x, 3, "^"), # Continue up
321
+ (tests_x, 2, "<"), # Turn left at junction
322
+ (code_x, 2, "v"), # Turn down at code column
323
+ (code_x, 3, "v"), # Continue down
324
+ (code_x, 4, "v"), # Continue down
325
+ (code_x, 5, "v") # Final down to code box
326
+ ]
327
+
328
+ # Initialize all lines with basic structure
329
+ line_parts = []
330
+ for i in range(6): # Extended to 6 lines to accommodate connections to boxes
331
+ line_parts.append([" "] * console_width)
332
+
333
+ # Draw the basic connecting line structure
334
+ all_branch_xs = sorted([code_x, example_x, tests_x, prompt_x])
335
+ min_x = min(all_branch_xs)
336
+ max_x = max(all_branch_xs)
337
+
338
+ # Draw horizontal line on line 2 (index 2)
339
+ for i in range(min_x, max_x + 1):
340
+ line_parts[2][i] = "─"
341
+
342
+ # Draw vertical connectors only where needed
343
+ # Prompt always connects vertically (lines 0,1 above junction, lines 3,4,5 below)
344
+ for line_idx in [0, 1, 3, 4, 5]:
345
+ if prompt_x >= 0 and prompt_x < console_width:
346
+ line_parts[line_idx][prompt_x] = "│"
347
+
348
+ # Code and Tests only connect below the junction (lines 3,4,5)
349
+ for line_idx in [3, 4, 5]:
350
+ if code_x >= 0 and code_x < console_width:
351
+ line_parts[line_idx][code_x] = "│"
352
+ if tests_x >= 0 and tests_x < console_width:
353
+ line_parts[line_idx][tests_x] = "│"
354
+
355
+ # Set junction points on horizontal line
356
+ if code_x >= 0 and code_x < console_width:
357
+ line_parts[2][code_x] = "┌" # Top-left corner
358
+ if example_x >= 0 and example_x < console_width:
359
+ line_parts[2][example_x] = "┼" # 4-way junction (prompt connects here)
360
+ if tests_x >= 0 and tests_x < console_width:
361
+ line_parts[2][tests_x] = "┐" # Top-right corner
362
+
363
+ # Animate single arrow along path with distance-based timing
364
+ if waypoints:
365
+ # Calculate total path distance for normalization
366
+ total_distance = 0
367
+ segment_distances = []
368
+ for i in range(len(waypoints) - 1):
369
+ start_x, start_y, _ = waypoints[i]
370
+ end_x, end_y, _ = waypoints[i + 1]
371
+ distance = abs(end_x - start_x) + abs(end_y - start_y) # Manhattan distance
372
+ segment_distances.append(distance)
373
+ total_distance += distance
374
+
375
+ if total_distance > 0:
376
+ current_pos_factor = (frame % animation_cycle) / animation_cycle
377
+ target_distance = current_pos_factor * total_distance
378
+
379
+ # Find which segment we're in based on distance traveled
380
+ current_distance = 0
381
+ current_segment = 0
382
+ segment_factor = 0
383
+
384
+ for i, seg_dist in enumerate(segment_distances):
385
+ if current_distance + seg_dist >= target_distance:
386
+ current_segment = i
387
+ if seg_dist > 0:
388
+ segment_factor = (target_distance - current_distance) / seg_dist
389
+ break
390
+ current_distance += seg_dist
391
+
392
+ if current_segment < len(waypoints) - 1:
393
+ start_waypoint = waypoints[current_segment]
394
+ end_waypoint = waypoints[current_segment + 1]
395
+
396
+ start_x, start_y, _ = start_waypoint
397
+ end_x, end_y, _ = end_waypoint
398
+
399
+ # Calculate current arrow position with consistent speed
400
+ if start_x == end_x: # Vertical movement
401
+ arrow_x = start_x
402
+ distance = abs(end_y - start_y)
403
+ if start_y < end_y: # Moving down
404
+ arrow_y = start_y + round(distance * segment_factor)
405
+ arrow_char = "v"
406
+ else: # Moving up
407
+ arrow_y = start_y - round(distance * segment_factor)
408
+ arrow_char = "^"
409
+ else: # Horizontal movement
410
+ arrow_y = start_y
411
+ distance = abs(end_x - start_x)
412
+ if start_x < end_x: # Moving right
413
+ arrow_x = start_x + round(distance * segment_factor)
414
+ arrow_char = ">"
415
+ else: # Moving left
416
+ arrow_x = start_x - round(distance * segment_factor)
417
+ arrow_char = "<"
418
+
419
+ # Place the arrow
420
+ if (0 <= arrow_x < console_width and 0 <= arrow_y < len(line_parts)):
421
+ line_parts[arrow_y][arrow_x] = arrow_char
422
+
423
+ # Convert to Text objects
424
+ for line_content in line_parts:
425
+ lines.append(Text("".join(line_content), style=ELECTRIC_CYAN))
426
+
427
+ return lines
428
+
429
+
430
+ def _render_animation_frame(state: AnimationState, console_width: int) -> Panel:
431
+ """Renders a single frame of the main animation box."""
432
+ layout = Layout(name="root")
433
+ layout.split_column(
434
+ Layout(name="header", size=1),
435
+ Layout(name="body", ratio=1, minimum_size=10),
436
+ Layout(name="footer", size=1)
437
+ )
438
+
439
+ blink_on = (state.frame_count // 5) % 2 == 0
440
+
441
+ header_table = Table.grid(expand=True, padding=(0,1))
442
+ header_table.add_column(justify="left", ratio=1)
443
+ header_table.add_column(justify="right", ratio=1)
444
+ # Make command blink in top right corner
445
+ command_text = state.current_function_name.capitalize() if blink_on else ""
446
+ header_table.add_row(
447
+ Text("Prompt Driven Development", style=f"bold {ELECTRIC_CYAN}"),
448
+ Text(command_text, style=f"bold {ELECTRIC_CYAN}")
449
+ )
450
+ layout["header"].update(header_table)
451
+
452
+ footer_table = Table.grid(expand=True, padding=(0,1))
453
+ footer_table.add_column(justify="left", ratio=1)
454
+ footer_table.add_column(justify="center", ratio=1)
455
+ footer_table.add_column(justify="right", ratio=1)
456
+
457
+ cost_str = f"${state.cost:.2f}"
458
+ budget_str = f"${state.budget:.2f}" if state.budget != float('inf') else "N/A"
459
+
460
+ footer_table.add_row(
461
+ Text(state.basename, style=ELECTRIC_CYAN),
462
+ Text(f"Elapsed: {state.get_elapsed_time_str()}", style=ELECTRIC_CYAN),
463
+ Text(f"{cost_str} / {budget_str}", style=ELECTRIC_CYAN)
464
+ )
465
+ layout["footer"].update(footer_table)
466
+
467
+ # Calculate dynamic box width based on console width
468
+ # Leave space for margins and spacing between boxes
469
+ margin_space = 8 # Total margin space
470
+ inter_box_space = 4 # Space between boxes (2 spaces each side)
471
+ available_width = console_width - margin_space - inter_box_space
472
+ box_width = max(state.path_box_content_width + 4, available_width // 3)
473
+
474
+ # Calculate the actual content width inside each panel (excluding borders)
475
+ panel_content_width = box_width - 4 # Account for panel borders (2 chars each side)
476
+
477
+ # Handle progressive border thickening for auto-deps command
478
+ prompt_border_style = state.colors["prompt"]
479
+ if state.current_function_name == "auto-deps":
480
+ # Create thicker border effect by cycling through different border styles
481
+ thickness_level = (state.auto_deps_progress // 30) % 4 # Change every 3 seconds
482
+ if thickness_level == 0:
483
+ prompt_border_style = state.colors["prompt"]
484
+ elif thickness_level == 1:
485
+ prompt_border_style = f"bold {state.colors['prompt']}"
486
+ elif thickness_level == 2:
487
+ # Use a different approach for bright colors that works with hex colors
488
+ base_color = state.colors['prompt'].replace('#', '').lower()
489
+ if base_color in ['8c47ff', 'purple']:
490
+ prompt_border_style = "bold bright_magenta"
491
+ elif base_color in ['00d8ff', 'cyan']:
492
+ prompt_border_style = "bold bright_cyan"
493
+ else:
494
+ prompt_border_style = f"bold bright_white"
495
+ else:
496
+ # Final level: reverse colors for maximum thickness effect
497
+ prompt_border_style = f"bold black on {state.colors['prompt']}"
498
+
499
+ prompt_panel = Panel(Align.center(state._render_scrolling_path("prompt", panel_content_width)),
500
+ title=Text.assemble(state.get_emoji_for_box("prompt", blink_on), "Prompt"),
501
+ border_style=prompt_border_style, width=box_width, height=3)
502
+ code_panel = Panel(Align.center(state._render_scrolling_path("code", panel_content_width)),
503
+ title=Text.assemble(state.get_emoji_for_box("code", blink_on), "Code"),
504
+ border_style=state.colors["code"], width=box_width, height=3)
505
+ example_panel = Panel(Align.center(state._render_scrolling_path("example", panel_content_width)),
506
+ title=Text.assemble(state.get_emoji_for_box("example", blink_on), "Example"),
507
+ border_style=state.colors["example"], width=box_width, height=3)
508
+ tests_panel = Panel(Align.center(state._render_scrolling_path("tests", panel_content_width)),
509
+ title=Text.assemble(state.get_emoji_for_box("tests", blink_on), "Tests"),
510
+ border_style=state.colors["tests"], width=box_width, height=3)
511
+
512
+ org_chart_layout = Layout(name="org_chart_area")
513
+ org_chart_layout.split_column(
514
+ Layout(Text(" "), size=1),
515
+ Layout(Align.center(prompt_panel), name="prompt_row", size=3),
516
+ Layout(name="lines_row_1", size=1),
517
+ Layout(name="lines_row_2", size=1),
518
+ Layout(name="lines_row_3", size=1),
519
+ Layout(name="lines_row_4", size=1),
520
+ Layout(name="lines_row_5", size=1),
521
+ Layout(name="lines_row_6", size=1),
522
+ Layout(name="bottom_boxes_row", size=3)
523
+ )
524
+
525
+ # Use full console width since we're no longer centering the lines
526
+ connecting_lines = _draw_connecting_lines_and_arrows(state, console_width)
527
+ if len(connecting_lines) > 0:
528
+ org_chart_layout["lines_row_1"].update(connecting_lines[0])
529
+ if len(connecting_lines) > 1:
530
+ org_chart_layout["lines_row_2"].update(connecting_lines[1])
531
+ if len(connecting_lines) > 2:
532
+ org_chart_layout["lines_row_3"].update(connecting_lines[2])
533
+ if len(connecting_lines) > 3:
534
+ org_chart_layout["lines_row_4"].update(connecting_lines[3])
535
+ if len(connecting_lines) > 4:
536
+ org_chart_layout["lines_row_5"].update(connecting_lines[4])
537
+ if len(connecting_lines) > 5:
538
+ org_chart_layout["lines_row_6"].update(connecting_lines[5])
539
+
540
+
541
+ bottom_boxes_table = Table.grid(expand=True)
542
+ bottom_boxes_table.add_column()
543
+ bottom_boxes_table.add_column()
544
+ bottom_boxes_table.add_column()
545
+ bottom_boxes_table.add_row(code_panel, example_panel, tests_panel)
546
+ org_chart_layout["bottom_boxes_row"].update(Align.center(bottom_boxes_table))
547
+
548
+ layout["body"].update(org_chart_layout)
549
+ state.frame_count += 1
550
+
551
+ return Panel(layout, style=f"{ELECTRIC_CYAN} on {DEEP_NAVY}",
552
+ border_style=ELECTRIC_CYAN, height=ANIMATION_BOX_HEIGHT,
553
+ width=console_width)
554
+
555
+
556
+
557
+ def _final_logo_animation_sequence(console: Console):
558
+ """Animates the PDD logo shrinking/disappearing."""
559
+ # This is called after Live exits, so console is back to normal.
560
+ console.clear()
561
+ logo_panel_content = "\n".join(line.center(LOGO_MAX_WIDTH + 4) for line in PDD_LOGO_ASCII)
562
+ logo_panel = Panel(logo_panel_content, style=f"bold {ELECTRIC_CYAN} on {DEEP_NAVY}",
563
+ border_style=ELECTRIC_CYAN, width=LOGO_MAX_WIDTH + 6, height=LOGO_HEIGHT + 2)
564
+ console.print(Align.center(logo_panel))
565
+ time.sleep(1) # Show logo briefly
566
+ console.clear() # Final clear
567
+
568
+
569
+ def sync_animation(
570
+ function_name_ref: List[str],
571
+ stop_event: threading.Event,
572
+ basename: str,
573
+ cost_ref: List[float],
574
+ budget: Optional[float],
575
+ prompt_color: List[str],
576
+ code_color: List[str],
577
+ example_color: List[str],
578
+ tests_color: List[str],
579
+ prompt_path_ref: List[str],
580
+ code_path_ref: List[str],
581
+ example_path_ref: List[str],
582
+ tests_path_ref: List[str]
583
+ ) -> None:
584
+ """
585
+ Displays an informative ASCII art animation in the terminal.
586
+ Uses mutable list references to get updates from the main thread.
587
+ The color arguments (prompt_color, code_color, example_color, tests_color) are expected to be List[str] references.
588
+ """
589
+ console = Console(legacy_windows=False)
590
+ animation_state = AnimationState(basename, budget)
591
+ # Set initial box colors
592
+ animation_state.set_box_colors(prompt_color[0], code_color[0], example_color[0], tests_color[0])
593
+
594
+ logo_animation.run_logo_animation_inline(console, stop_event)
595
+
596
+ if stop_event.is_set():
597
+ _final_logo_animation_sequence(console)
598
+ return
599
+
600
+ try:
601
+ with Live(_render_animation_frame(animation_state, console.width),
602
+ console=console,
603
+ refresh_per_second=10,
604
+ transient=False,
605
+ screen=True,
606
+ auto_refresh=True
607
+ ) as live:
608
+ while not stop_event.is_set():
609
+ current_func_name = function_name_ref[0] if function_name_ref else "checking"
610
+ current_cost = cost_ref[0] if cost_ref else 0.0
611
+
612
+ current_prompt_path = prompt_path_ref[0] if prompt_path_ref else ""
613
+ current_code_path = code_path_ref[0] if code_path_ref else ""
614
+ current_example_path = example_path_ref[0] if example_path_ref else ""
615
+ current_tests_path = tests_path_ref[0] if tests_path_ref else ""
616
+
617
+ # Update box colors from refs
618
+ animation_state.set_box_colors(
619
+ prompt_color[0],
620
+ code_color[0],
621
+ example_color[0],
622
+ tests_color[0]
623
+ )
624
+
625
+ animation_state.update_dynamic_state(
626
+ current_func_name, current_cost,
627
+ current_prompt_path, current_code_path,
628
+ current_example_path, current_tests_path
629
+ )
630
+
631
+ live.update(_render_animation_frame(animation_state, console.width))
632
+ time.sleep(0.1)
633
+ except Exception as e:
634
+ if hasattr(console, 'is_alt_screen') and console.is_alt_screen:
635
+ console.show_cursor(True)
636
+ if hasattr(console, 'alt_screen'):
637
+ console.alt_screen = False
638
+ console.clear()
639
+ console.print_exception(show_locals=True)
640
+ print(f"Error in animation: {e}", flush=True)
641
+ finally:
642
+ _final_logo_animation_sequence(console)
643
+