pdd-cli 0.0.90__py3-none-any.whl → 0.0.121__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 (151) hide show
  1. pdd/__init__.py +38 -6
  2. pdd/agentic_bug.py +323 -0
  3. pdd/agentic_bug_orchestrator.py +506 -0
  4. pdd/agentic_change.py +231 -0
  5. pdd/agentic_change_orchestrator.py +537 -0
  6. pdd/agentic_common.py +533 -770
  7. pdd/agentic_crash.py +2 -1
  8. pdd/agentic_e2e_fix.py +319 -0
  9. pdd/agentic_e2e_fix_orchestrator.py +582 -0
  10. pdd/agentic_fix.py +118 -3
  11. pdd/agentic_update.py +27 -9
  12. pdd/agentic_verify.py +3 -2
  13. pdd/architecture_sync.py +565 -0
  14. pdd/auth_service.py +210 -0
  15. pdd/auto_deps_main.py +63 -53
  16. pdd/auto_include.py +236 -3
  17. pdd/auto_update.py +125 -47
  18. pdd/bug_main.py +195 -23
  19. pdd/cmd_test_main.py +345 -197
  20. pdd/code_generator.py +4 -2
  21. pdd/code_generator_main.py +118 -32
  22. pdd/commands/__init__.py +6 -0
  23. pdd/commands/analysis.py +113 -48
  24. pdd/commands/auth.py +309 -0
  25. pdd/commands/connect.py +358 -0
  26. pdd/commands/fix.py +155 -114
  27. pdd/commands/generate.py +5 -0
  28. pdd/commands/maintenance.py +3 -2
  29. pdd/commands/misc.py +8 -0
  30. pdd/commands/modify.py +225 -163
  31. pdd/commands/sessions.py +284 -0
  32. pdd/commands/utility.py +12 -7
  33. pdd/construct_paths.py +334 -32
  34. pdd/context_generator_main.py +167 -170
  35. pdd/continue_generation.py +6 -3
  36. pdd/core/__init__.py +33 -0
  37. pdd/core/cli.py +44 -7
  38. pdd/core/cloud.py +237 -0
  39. pdd/core/dump.py +68 -20
  40. pdd/core/errors.py +4 -0
  41. pdd/core/remote_session.py +61 -0
  42. pdd/crash_main.py +219 -23
  43. pdd/data/llm_model.csv +4 -4
  44. pdd/docs/prompting_guide.md +864 -0
  45. pdd/docs/whitepaper_with_benchmarks/data_and_functions/benchmark_analysis.py +495 -0
  46. pdd/docs/whitepaper_with_benchmarks/data_and_functions/creation_compare.py +528 -0
  47. pdd/fix_code_loop.py +208 -34
  48. pdd/fix_code_module_errors.py +6 -2
  49. pdd/fix_error_loop.py +291 -38
  50. pdd/fix_main.py +208 -6
  51. pdd/fix_verification_errors_loop.py +235 -26
  52. pdd/fix_verification_main.py +269 -83
  53. pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
  54. pdd/frontend/dist/assets/index-CUWd8al1.js +450 -0
  55. pdd/frontend/dist/index.html +376 -0
  56. pdd/frontend/dist/logo.svg +33 -0
  57. pdd/generate_output_paths.py +46 -5
  58. pdd/generate_test.py +212 -151
  59. pdd/get_comment.py +19 -44
  60. pdd/get_extension.py +8 -9
  61. pdd/get_jwt_token.py +309 -20
  62. pdd/get_language.py +8 -7
  63. pdd/get_run_command.py +7 -5
  64. pdd/insert_includes.py +2 -1
  65. pdd/llm_invoke.py +531 -97
  66. pdd/load_prompt_template.py +15 -34
  67. pdd/operation_log.py +342 -0
  68. pdd/path_resolution.py +140 -0
  69. pdd/postprocess.py +122 -97
  70. pdd/preprocess.py +68 -12
  71. pdd/preprocess_main.py +33 -1
  72. pdd/prompts/agentic_bug_step10_pr_LLM.prompt +182 -0
  73. pdd/prompts/agentic_bug_step1_duplicate_LLM.prompt +73 -0
  74. pdd/prompts/agentic_bug_step2_docs_LLM.prompt +129 -0
  75. pdd/prompts/agentic_bug_step3_triage_LLM.prompt +95 -0
  76. pdd/prompts/agentic_bug_step4_reproduce_LLM.prompt +97 -0
  77. pdd/prompts/agentic_bug_step5_root_cause_LLM.prompt +123 -0
  78. pdd/prompts/agentic_bug_step6_test_plan_LLM.prompt +107 -0
  79. pdd/prompts/agentic_bug_step7_generate_LLM.prompt +172 -0
  80. pdd/prompts/agentic_bug_step8_verify_LLM.prompt +119 -0
  81. pdd/prompts/agentic_bug_step9_e2e_test_LLM.prompt +289 -0
  82. pdd/prompts/agentic_change_step10_identify_issues_LLM.prompt +1006 -0
  83. pdd/prompts/agentic_change_step11_fix_issues_LLM.prompt +984 -0
  84. pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +140 -0
  85. pdd/prompts/agentic_change_step1_duplicate_LLM.prompt +73 -0
  86. pdd/prompts/agentic_change_step2_docs_LLM.prompt +101 -0
  87. pdd/prompts/agentic_change_step3_research_LLM.prompt +126 -0
  88. pdd/prompts/agentic_change_step4_clarify_LLM.prompt +164 -0
  89. pdd/prompts/agentic_change_step5_docs_change_LLM.prompt +981 -0
  90. pdd/prompts/agentic_change_step6_devunits_LLM.prompt +1005 -0
  91. pdd/prompts/agentic_change_step7_architecture_LLM.prompt +1044 -0
  92. pdd/prompts/agentic_change_step8_analyze_LLM.prompt +1027 -0
  93. pdd/prompts/agentic_change_step9_implement_LLM.prompt +1077 -0
  94. pdd/prompts/agentic_e2e_fix_step1_unit_tests_LLM.prompt +90 -0
  95. pdd/prompts/agentic_e2e_fix_step2_e2e_tests_LLM.prompt +91 -0
  96. pdd/prompts/agentic_e2e_fix_step3_root_cause_LLM.prompt +89 -0
  97. pdd/prompts/agentic_e2e_fix_step4_fix_e2e_tests_LLM.prompt +96 -0
  98. pdd/prompts/agentic_e2e_fix_step5_identify_devunits_LLM.prompt +91 -0
  99. pdd/prompts/agentic_e2e_fix_step6_create_unit_tests_LLM.prompt +106 -0
  100. pdd/prompts/agentic_e2e_fix_step7_verify_tests_LLM.prompt +116 -0
  101. pdd/prompts/agentic_e2e_fix_step8_run_pdd_fix_LLM.prompt +120 -0
  102. pdd/prompts/agentic_e2e_fix_step9_verify_all_LLM.prompt +146 -0
  103. pdd/prompts/agentic_fix_primary_LLM.prompt +2 -2
  104. pdd/prompts/agentic_update_LLM.prompt +192 -338
  105. pdd/prompts/auto_include_LLM.prompt +22 -0
  106. pdd/prompts/change_LLM.prompt +3093 -1
  107. pdd/prompts/detect_change_LLM.prompt +571 -14
  108. pdd/prompts/fix_code_module_errors_LLM.prompt +8 -0
  109. pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +1 -0
  110. pdd/prompts/generate_test_LLM.prompt +19 -1
  111. pdd/prompts/generate_test_from_example_LLM.prompt +366 -0
  112. pdd/prompts/insert_includes_LLM.prompt +262 -252
  113. pdd/prompts/prompt_code_diff_LLM.prompt +123 -0
  114. pdd/prompts/prompt_diff_LLM.prompt +82 -0
  115. pdd/remote_session.py +876 -0
  116. pdd/server/__init__.py +52 -0
  117. pdd/server/app.py +335 -0
  118. pdd/server/click_executor.py +587 -0
  119. pdd/server/executor.py +338 -0
  120. pdd/server/jobs.py +661 -0
  121. pdd/server/models.py +241 -0
  122. pdd/server/routes/__init__.py +31 -0
  123. pdd/server/routes/architecture.py +451 -0
  124. pdd/server/routes/auth.py +364 -0
  125. pdd/server/routes/commands.py +929 -0
  126. pdd/server/routes/config.py +42 -0
  127. pdd/server/routes/files.py +603 -0
  128. pdd/server/routes/prompts.py +1347 -0
  129. pdd/server/routes/websocket.py +473 -0
  130. pdd/server/security.py +243 -0
  131. pdd/server/terminal_spawner.py +217 -0
  132. pdd/server/token_counter.py +222 -0
  133. pdd/summarize_directory.py +236 -237
  134. pdd/sync_animation.py +8 -4
  135. pdd/sync_determine_operation.py +329 -47
  136. pdd/sync_main.py +272 -28
  137. pdd/sync_orchestration.py +289 -211
  138. pdd/sync_order.py +304 -0
  139. pdd/template_expander.py +161 -0
  140. pdd/templates/architecture/architecture_json.prompt +41 -46
  141. pdd/trace.py +1 -1
  142. pdd/track_cost.py +0 -13
  143. pdd/unfinished_prompt.py +2 -1
  144. pdd/update_main.py +68 -26
  145. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/METADATA +15 -10
  146. pdd_cli-0.0.121.dist-info/RECORD +229 -0
  147. pdd_cli-0.0.90.dist-info/RECORD +0 -153
  148. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/WHEEL +0 -0
  149. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/entry_points.txt +0 -0
  150. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/licenses/LICENSE +0 -0
  151. {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.121.dist-info}/top_level.txt +0 -0
@@ -1,265 +1,264 @@
1
- from typing import Callable, Optional, Tuple
2
- from datetime import datetime
3
- try:
4
- from datetime import UTC
5
- except ImportError:
6
- # Python < 3.11 compatibility
7
- from datetime import timezone
8
- UTC = timezone.utc
9
- from io import StringIO
10
- import os
1
+ from __future__ import annotations
2
+
11
3
  import glob
4
+ import hashlib
5
+ import io
12
6
  import csv
13
-
7
+ import os
8
+ from typing import Optional, List, Dict, Tuple, Callable
14
9
  from pydantic import BaseModel, Field
15
- from rich import print
16
- from rich.progress import track
10
+ from rich.console import Console
11
+ from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
17
12
 
18
- from .load_prompt_template import load_prompt_template
13
+ # Internal imports based on package structure
19
14
  from .llm_invoke import llm_invoke
15
+ from .load_prompt_template import load_prompt_template
20
16
  from . import DEFAULT_TIME
21
17
 
22
- class FileSummary(BaseModel):
23
- file_summary: str = Field(description="The summary of the file")
18
+ console = Console()
24
19
 
25
- def validate_csv_format(csv_content: str) -> bool:
26
- """Validate CSV has required columns and proper format."""
27
- try:
28
- if not csv_content or csv_content.isspace():
29
- return False
30
- reader = csv.DictReader(StringIO(csv_content.lstrip()))
31
- if not reader.fieldnames:
32
- return False
33
- required_columns = {'full_path', 'file_summary', 'date'}
34
- if not all(col in reader.fieldnames for col in required_columns):
35
- return False
36
- try:
37
- first_row = next(reader, None)
38
- if not first_row:
39
- return True
40
- return all(key in first_row for key in required_columns)
41
- except csv.Error:
42
- return False
43
- except Exception:
44
- return False
45
-
46
- def normalize_path(path: str) -> str:
47
- """Normalize path for consistent comparison."""
48
- return os.path.normpath(path.strip().strip('"').strip())
49
-
50
- def parse_date(date_str: str) -> datetime:
51
- """Parse date string to datetime with proper error handling."""
52
- try:
53
- dt = datetime.fromisoformat(date_str.strip())
54
- return dt if dt.tzinfo else dt.replace(tzinfo=UTC)
55
- except Exception:
56
- return datetime.now(UTC)
57
-
58
- def parse_existing_csv(csv_content: str, verbose: bool = False) -> dict:
59
- """Parse existing CSV file and return normalized data."""
60
- existing_data = {}
61
- try:
62
- # Clean the CSV content by removing leading/trailing whitespace from each line
63
- cleaned_lines = [line.strip() for line in csv_content.splitlines()]
64
- cleaned_content = '\n'.join(cleaned_lines)
65
-
66
- reader = csv.DictReader(StringIO(cleaned_content))
67
- for row in reader:
68
- try:
69
- normalized_path = normalize_path(row['full_path'])
70
- existing_data[normalized_path] = {
71
- 'file_summary': row['file_summary'].strip().strip('"'),
72
- 'date': row['date'].strip()
73
- }
74
- if verbose:
75
- print(f"[green]Parsed existing entry for: {normalized_path}[/green]")
76
- except Exception as e:
77
- if verbose:
78
- print(f"[yellow]Warning: Skipping invalid CSV row: {str(e)}[/yellow]")
79
- except Exception as e:
80
- if verbose:
81
- print(f"[yellow]Warning: Error parsing CSV: {str(e)}[/yellow]")
82
- raise ValueError("Invalid CSV file format.")
83
- return existing_data
20
+ class FileSummary(BaseModel):
21
+ """Pydantic model for structured LLM output."""
22
+ file_summary: str = Field(..., description="A concise summary of the file contents.")
84
23
 
85
24
  def summarize_directory(
86
25
  directory_path: str,
87
26
  strength: float,
88
27
  temperature: float,
89
- verbose: bool,
90
28
  time: float = DEFAULT_TIME,
29
+ verbose: bool = False,
91
30
  csv_file: Optional[str] = None,
92
31
  progress_callback: Optional[Callable[[int, int], None]] = None
93
32
  ) -> Tuple[str, float, str]:
94
33
  """
95
- Summarize files in a directory and generate a CSV containing the summaries.
34
+ Summarizes files in a directory using an LLM, with caching based on content hashes.
96
35
 
97
- Parameters:
98
- directory_path (str): Path to the directory to summarize with wildcard (e.g., /path/to/directory/*.py)
99
- strength (float): Between 0 and 1 that is the strength of the LLM model to use.
100
- temperature (float): Controls the randomness of the LLM's output.
101
- verbose (bool): Whether to print out the details of the function.
102
- time (float): Time budget for LLM calls.
103
- csv_file (Optional[str]): Current CSV file contents if it already exists.
104
- progress_callback (Optional[Callable[[int, int], None]]): Callback for progress updates.
105
- Called with (current, total) for each file processed. Used by TUI ProgressBar.
36
+ Args:
37
+ directory_path: Path to the directory/files (supports wildcards, e.g., 'src/*.py').
38
+ strength: Float (0-1) indicating LLM model strength.
39
+ temperature: Float controlling LLM randomness.
40
+ time: Float (0-1) controlling thinking effort.
41
+ verbose: Whether to print detailed logs.
42
+ csv_file: Existing CSV content string to check for cache hits.
43
+ progress_callback: Optional callback for progress updates (current, total).
106
44
 
107
45
  Returns:
108
- Tuple[str, float, str]: A tuple containing:
109
- - csv_output (str): Updated CSV content with 'full_path', 'file_summary', and 'date'.
110
- - total_cost (float): Total cost of the LLM runs.
111
- - model_name (str): Name of the LLM model used.
46
+ Tuple containing:
47
+ - csv_output (str): The updated CSV content.
48
+ - total_cost (float): Total cost of LLM operations.
49
+ - model_name (str): Name of the model used (from the last successful call).
112
50
  """
113
- try:
114
- if not isinstance(directory_path, str) or not directory_path:
115
- raise ValueError("Invalid 'directory_path'.")
116
- if not (0.0 <= strength <= 1.0):
117
- raise ValueError("Invalid 'strength' value.")
118
- if not isinstance(temperature, (int, float)) or temperature < 0:
119
- raise ValueError("Invalid 'temperature' value.")
120
- if not isinstance(verbose, bool):
121
- raise ValueError("Invalid 'verbose' value.")
122
-
123
- prompt_template = load_prompt_template("summarize_file_LLM")
124
- if not prompt_template:
125
- raise FileNotFoundError("Prompt template 'summarize_file_LLM.prompt' not found.")
126
-
127
- csv_output = "full_path,file_summary,date\n"
128
- total_cost = 0.0
129
- model_name = "None"
130
-
131
- existing_data = {}
132
- if csv_file:
133
- if not validate_csv_format(csv_file):
134
- raise ValueError("Invalid CSV file format.")
135
- existing_data = parse_existing_csv(csv_file, verbose)
136
-
137
- # Expand directory_path: support plain directories or glob patterns
51
+
52
+ # Step 1: Input Validation
53
+ if not isinstance(directory_path, str) or not directory_path:
54
+ raise ValueError("Invalid 'directory_path'.")
55
+ if not (0.0 <= strength <= 1.0):
56
+ raise ValueError("Invalid 'strength' value.")
57
+ if not (isinstance(temperature, (int, float)) and temperature >= 0):
58
+ raise ValueError("Invalid 'temperature' value.")
59
+ if not isinstance(verbose, bool):
60
+ raise ValueError("Invalid 'verbose' value.")
61
+
62
+ # Parse existing CSV if provided to validate format and get cached entries
63
+ existing_data: Dict[str, Dict[str, str]] = {}
64
+ if csv_file:
138
65
  try:
139
- normalized_input = normalize_path(directory_path)
66
+ f = io.StringIO(csv_file)
67
+ reader = csv.DictReader(f)
68
+ if reader.fieldnames and not all(field in reader.fieldnames for field in ['full_path', 'file_summary', 'content_hash']):
69
+ raise ValueError("Missing required columns.")
70
+ for row in reader:
71
+ if 'full_path' in row and 'content_hash' in row:
72
+ # Use normalized path for cache key consistency
73
+ existing_data[os.path.normpath(row['full_path'])] = row
140
74
  except Exception:
141
- normalized_input = directory_path
142
-
143
- if os.path.isdir(normalized_input):
144
- # Recursively include all files under the directory
145
- search_pattern = os.path.join(normalized_input, "**", "*")
146
- else:
147
- # Treat as a glob pattern (may be a single file path too)
148
- search_pattern = directory_path
149
-
150
- # Get list of files first to ensure consistent order
151
- all_files = sorted(glob.glob(search_pattern, recursive=True))
152
- if not all_files:
153
- if verbose:
154
- print("[yellow]No files found.[/yellow]")
155
- return csv_output, total_cost, model_name
156
-
157
- # Pre-filter to get only processable files (for accurate progress count)
158
- files = [
159
- f for f in all_files
160
- if not os.path.isdir(f)
161
- and '__pycache__' not in f
162
- and not f.endswith(('.pyc', '.pyo'))
163
- ]
164
-
165
- if not files:
166
- if verbose:
167
- print("[yellow]No processable files found.[/yellow]")
168
- return csv_output, total_cost, model_name
169
-
170
- # Get all modification times at once to ensure consistent order
171
- file_mod_times = {f: os.path.getmtime(f) for f in files}
172
-
173
- # Determine iteration method: use callback if provided, else track()
174
- # Disable track() when in TUI context (COLUMNS env var set) or callback provided
175
- total_files = len(files)
176
- use_track = progress_callback is None and "COLUMNS" not in os.environ
177
-
178
- if use_track:
179
- file_iterator = track(files, description="Processing files...")
180
- else:
181
- file_iterator = files
182
-
183
- for idx, file_path in enumerate(file_iterator):
184
- # Report progress if callback provided
185
- if progress_callback is not None:
186
- progress_callback(idx + 1, total_files)
187
-
188
- try:
189
- relative_path = os.path.relpath(file_path)
190
- normalized_path = normalize_path(relative_path)
191
- file_mod_time = file_mod_times[file_path]
192
- date_generated = datetime.now(UTC).isoformat()
193
-
194
- if verbose:
195
- print(f"\nProcessing file: {normalized_path}")
196
- print(f"Modification time: {datetime.fromtimestamp(file_mod_time, UTC)}")
197
-
198
- needs_summary = True
199
- if normalized_path in existing_data:
200
- try:
201
- existing_entry = existing_data[normalized_path]
202
- existing_date = parse_date(existing_entry['date'])
203
- file_date = datetime.fromtimestamp(file_mod_time, UTC)
204
-
205
- if verbose:
206
- print(f"Existing date: {existing_date}")
207
- print(f"File date: {file_date}")
208
-
209
- # Explicitly check if file is newer
210
- if file_date > existing_date:
211
- if verbose:
212
- print(f"[blue]File modified, generating new summary[/blue]")
213
- needs_summary = True
214
- else:
215
- needs_summary = False
216
- file_summary = existing_entry['file_summary']
217
- date_generated = existing_entry['date']
218
- if verbose:
219
- print(f"[green]Reusing existing summary[/green]")
220
- except Exception as e:
221
- if verbose:
222
- print(f"[yellow]Warning: Date comparison error: {str(e)}[/yellow]")
223
- needs_summary = True
224
- elif verbose:
225
- print("[blue]New file, generating summary[/blue]")
226
-
227
- if needs_summary:
228
- if verbose:
229
- print(f"[blue]Generating summary for: {normalized_path}[/blue]")
230
- with open(file_path, 'r', encoding='utf-8') as f:
231
- file_contents = f.read()
232
-
233
- input_params = {"file_contents": file_contents}
234
- response = llm_invoke(
235
- prompt=prompt_template,
236
- input_json=input_params,
237
- strength=strength,
238
- temperature=temperature,
239
- time=time,
240
- verbose=verbose,
241
- output_pydantic=FileSummary
242
- )
243
-
244
- if response.get('error'):
245
- file_summary = "Error in summarization."
246
- if verbose:
247
- print(f"[red]Error summarizing file: {response['error']}[/red]")
248
- else:
249
- file_summary = response['result'].file_summary
250
- total_cost += response.get('cost', 0.0)
251
- model_name = response.get('model_name', model_name)
252
-
253
- csv_output += f'"{relative_path}","{file_summary.replace(chr(34), "")}",{date_generated}\n'
254
-
255
- except Exception as e:
75
+ raise ValueError("Invalid CSV file format.")
76
+
77
+ # Step 2: Load prompt template
78
+ prompt_template_name = "summarize_file_LLM"
79
+ prompt_template = load_prompt_template(prompt_template_name)
80
+ if not prompt_template:
81
+ raise FileNotFoundError(f"Prompt template '{prompt_template_name}' is empty or missing.")
82
+
83
+ # Step 3: Get list of files matching directory_path
84
+ # If directory_path is a directory, convert to recursive glob pattern
85
+ if os.path.isdir(directory_path):
86
+ search_pattern = os.path.join(directory_path, "**", "*")
87
+ else:
88
+ search_pattern = directory_path
89
+
90
+ files = glob.glob(search_pattern, recursive=True)
91
+
92
+ # Filter out directories, keep only files
93
+ # Also filter out __pycache__ and .pyc/.pyo files
94
+ filtered_files = []
95
+ for f in files:
96
+ if os.path.isfile(f):
97
+ if "__pycache__" in f:
98
+ continue
99
+ if f.endswith(('.pyc', '.pyo')):
100
+ continue
101
+ filtered_files.append(f)
102
+
103
+ files = filtered_files
104
+
105
+ # Step 4: Return early if no files
106
+ if not files:
107
+ # Return empty CSV header
108
+ output_io = io.StringIO()
109
+ writer = csv.DictWriter(output_io, fieldnames=['full_path', 'file_summary', 'content_hash'])
110
+ writer.writeheader()
111
+ return output_io.getvalue(), 0.0, "None"
112
+
113
+ results_data: List[Dict[str, str]] = []
114
+ total_cost = 0.0
115
+ last_model_name = "cached"
116
+
117
+ # Step 6: Iterate through files with progress reporting
118
+ total_files = len(files)
119
+
120
+ if progress_callback:
121
+ for i, file_path in enumerate(files):
122
+ progress_callback(i + 1, total_files)
123
+ cost, model = _process_single_file_logic(
124
+ file_path,
125
+ existing_data,
126
+ prompt_template,
127
+ strength,
128
+ temperature,
129
+ time,
130
+ verbose,
131
+ results_data
132
+ )
133
+ total_cost += cost
134
+ if model != "cached":
135
+ last_model_name = model
136
+ else:
137
+ console.print(f"[bold blue]Summarizing {len(files)} files in '{directory_path}'...[/bold blue]")
138
+ with Progress(
139
+ SpinnerColumn(),
140
+ TextColumn("[progress.description]{task.description}"),
141
+ BarColumn(),
142
+ TextColumn("{task.percentage:>3.0f}%"),
143
+ console=console
144
+ ) as progress:
145
+ task = progress.add_task("[cyan]Processing files...", total=len(files))
146
+ for file_path in files:
147
+ cost, model = _process_single_file_logic(
148
+ file_path,
149
+ existing_data,
150
+ prompt_template,
151
+ strength,
152
+ temperature,
153
+ time,
154
+ verbose,
155
+ results_data
156
+ )
157
+ total_cost += cost
158
+ if model != "cached":
159
+ last_model_name = model
160
+ progress.advance(task)
161
+
162
+ # Step 7: Generate CSV output
163
+ output_io = io.StringIO()
164
+ fieldnames = ['full_path', 'file_summary', 'content_hash']
165
+ writer = csv.DictWriter(output_io, fieldnames=fieldnames)
166
+
167
+ writer.writeheader()
168
+ writer.writerows(results_data)
169
+
170
+ csv_output = output_io.getvalue()
171
+
172
+ return csv_output, total_cost, last_model_name
173
+
174
+ def _process_single_file_logic(
175
+ file_path: str,
176
+ existing_data: Dict[str, Dict[str, str]],
177
+ prompt_template: str,
178
+ strength: float,
179
+ temperature: float,
180
+ time: float,
181
+ verbose: bool,
182
+ results_data: List[Dict[str, str]]
183
+ ) -> Tuple[float, str]:
184
+ """
185
+ Helper function to process a single file: read, hash, check cache, summarize if needed.
186
+ Returns (cost, model_name).
187
+ """
188
+ cost = 0.0
189
+ model_name = "cached"
190
+
191
+ try:
192
+ # Step 6a: Read file
193
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
194
+ content = f.read()
195
+
196
+ # Step 6b: Compute hash
197
+ current_hash = hashlib.sha256(content.encode('utf-8')).hexdigest()
198
+
199
+ summary = ""
200
+
201
+ # Step 6c: Check cache (using normalized path)
202
+ normalized_path = os.path.normpath(file_path)
203
+ cache_hit = False
204
+
205
+ if normalized_path in existing_data:
206
+ cached_entry = existing_data[normalized_path]
207
+ # Step 6d: Check hash match
208
+ if cached_entry.get('content_hash') == current_hash:
209
+ # Step 6e: Reuse summary
210
+ summary = cached_entry.get('file_summary', "")
211
+ cache_hit = True
256
212
  if verbose:
257
- print(f"[red]Error processing file: {str(e)}[/red]")
258
- date_generated = datetime.now(UTC).isoformat()
259
- csv_output += f'"{relative_path}","Error processing file",{date_generated}\n'
213
+ console.print(f"[dim]Cache hit for {file_path}[/dim]")
260
214
 
261
- return csv_output, total_cost, model_name
215
+ # Step 6f: Summarize if needed
216
+ if not cache_hit:
217
+ if verbose:
218
+ console.print(f"[dim]Summarizing {file_path}...[/dim]")
219
+
220
+ llm_result = llm_invoke(
221
+ prompt=prompt_template,
222
+ input_json={"file_contents": content},
223
+ strength=strength,
224
+ temperature=temperature,
225
+ time=time,
226
+ output_pydantic=FileSummary,
227
+ verbose=verbose
228
+ )
229
+
230
+ file_summary_obj: FileSummary = llm_result['result']
231
+ summary = file_summary_obj.file_summary
232
+
233
+ cost = llm_result.get('cost', 0.0)
234
+ model_name = llm_result.get('model_name', "unknown")
235
+
236
+ # Step 6g: Store data
237
+ # Note: Requirement says "Store the relative path (not the full path)" in Step 6g description,
238
+ # but Output definition says "full_path". The existing code stored file_path (from glob).
239
+ # The new prompt Step 6g says "Store the relative path".
240
+ # However, the Output schema explicitly demands 'full_path'.
241
+ # To satisfy the Output schema which is usually the contract, we keep using file_path as 'full_path'.
242
+ # But we will calculate relative path if needed.
243
+ # Given the conflict, usually the Output definition takes precedence for the CSV column name,
244
+ # but the value might need to be relative.
245
+ # Let's stick to the existing behavior (glob path) which satisfied 'full_path' previously,
246
+ # unless 'relative path' implies os.path.relpath(file_path, start=directory_path_root).
247
+ # The prompt is slightly ambiguous: "Store the relative path... in the current data dictionary" vs Output "full_path".
248
+ # We will store the path as found by glob to ensure it matches the 'full_path' column expectation.
249
+
250
+ results_data.append({
251
+ 'full_path': file_path,
252
+ 'file_summary': summary,
253
+ 'content_hash': current_hash
254
+ })
262
255
 
263
256
  except Exception as e:
264
- print(f"[red]An error occurred: {str(e)}[/red]")
265
- raise
257
+ console.print(f"[bold red]Error processing file {file_path}:[/bold red] {e}")
258
+ results_data.append({
259
+ 'full_path': file_path,
260
+ 'file_summary': f"Error processing file: {str(e)}",
261
+ 'content_hash': "error"
262
+ })
263
+
264
+ return cost, model_name
pdd/sync_animation.py CHANGED
@@ -336,8 +336,13 @@ def _draw_connecting_lines_and_arrows(state: AnimationState, console_width: int)
336
336
  max_x = max(all_branch_xs)
337
337
 
338
338
  # Draw horizontal line on line 2 (index 2)
339
- for i in range(min_x, max_x + 1):
340
- line_parts[2][i] = "─"
339
+ # Clamp drawing range to console width to prevent IndexError and wrapping
340
+ draw_start = max(min_x, 0)
341
+ draw_end = min(max_x, console_width - 1)
342
+
343
+ if draw_start <= draw_end:
344
+ for i in range(draw_start, draw_end + 1):
345
+ line_parts[2][i] = "─"
341
346
 
342
347
  # Draw vertical connectors only where needed
343
348
  # Prompt always connects vertically (lines 0,1 above junction, lines 3,4,5 below)
@@ -639,5 +644,4 @@ def sync_animation(
639
644
  console.print_exception(show_locals=True)
640
645
  print(f"Error in animation: {e}", flush=True)
641
646
  finally:
642
- _final_logo_animation_sequence(console)
643
-
647
+ _final_logo_animation_sequence(console)