pdd-cli 0.0.40__py3-none-any.whl → 0.0.42__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.
- pdd/__init__.py +1 -1
- pdd/auto_deps_main.py +1 -1
- pdd/auto_update.py +73 -78
- pdd/bug_main.py +3 -3
- pdd/bug_to_unit_test.py +46 -38
- pdd/change.py +20 -13
- pdd/change_main.py +223 -163
- pdd/cli.py +192 -95
- pdd/cmd_test_main.py +51 -36
- pdd/code_generator_main.py +3 -2
- pdd/conflicts_main.py +1 -1
- pdd/construct_paths.py +221 -19
- pdd/context_generator_main.py +27 -12
- pdd/crash_main.py +44 -50
- pdd/data/llm_model.csv +1 -1
- pdd/detect_change_main.py +1 -1
- pdd/fix_code_module_errors.py +12 -0
- pdd/fix_main.py +2 -2
- pdd/fix_verification_errors.py +13 -0
- pdd/fix_verification_main.py +3 -3
- pdd/generate_output_paths.py +113 -21
- pdd/generate_test.py +53 -16
- pdd/llm_invoke.py +162 -0
- pdd/logo_animation.py +455 -0
- pdd/preprocess_main.py +1 -1
- pdd/process_csv_change.py +1 -1
- pdd/prompts/extract_program_code_fix_LLM.prompt +2 -1
- pdd/prompts/sync_analysis_LLM.prompt +82 -0
- pdd/split_main.py +1 -1
- pdd/sync_animation.py +643 -0
- pdd/sync_determine_operation.py +1039 -0
- pdd/sync_main.py +333 -0
- pdd/sync_orchestration.py +639 -0
- pdd/trace_main.py +1 -1
- pdd/update_main.py +7 -2
- pdd/xml_tagger.py +15 -6
- pdd_cli-0.0.42.dist-info/METADATA +307 -0
- {pdd_cli-0.0.40.dist-info → pdd_cli-0.0.42.dist-info}/RECORD +42 -36
- pdd_cli-0.0.40.dist-info/METADATA +0 -269
- {pdd_cli-0.0.40.dist-info → pdd_cli-0.0.42.dist-info}/WHEEL +0 -0
- {pdd_cli-0.0.40.dist-info → pdd_cli-0.0.42.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.40.dist-info → pdd_cli-0.0.42.dist-info}/licenses/LICENSE +0 -0
- {pdd_cli-0.0.40.dist-info → pdd_cli-0.0.42.dist-info}/top_level.txt +0 -0
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
|
+
|