kopipasta 0.27.0__py3-none-any.whl → 0.29.0__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 kopipasta might be problematic. Click here for more details.
- kopipasta/file.py +47 -0
- kopipasta/main.py +399 -640
- kopipasta/prompt.py +163 -0
- kopipasta/tree_selector.py +510 -0
- kopipasta-0.29.0.dist-info/METADATA +111 -0
- kopipasta-0.29.0.dist-info/RECORD +12 -0
- kopipasta-0.27.0.dist-info/METADATA +0 -171
- kopipasta-0.27.0.dist-info/RECORD +0 -9
- {kopipasta-0.27.0.dist-info → kopipasta-0.29.0.dist-info}/LICENSE +0 -0
- {kopipasta-0.27.0.dist-info → kopipasta-0.29.0.dist-info}/WHEEL +0 -0
- {kopipasta-0.27.0.dist-info → kopipasta-0.29.0.dist-info}/entry_points.txt +0 -0
- {kopipasta-0.27.0.dist-info → kopipasta-0.29.0.dist-info}/top_level.txt +0 -0
kopipasta/main.py
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
import csv
|
|
3
|
-
import io
|
|
4
|
-
import json
|
|
5
2
|
import os
|
|
6
3
|
import argparse
|
|
7
|
-
import sys
|
|
8
4
|
import re
|
|
9
5
|
import subprocess
|
|
10
6
|
import tempfile
|
|
7
|
+
import shutil
|
|
11
8
|
from typing import Dict, List, Optional, Set, Tuple
|
|
12
9
|
import pyperclip
|
|
13
|
-
import fnmatch
|
|
14
10
|
from pygments import highlight
|
|
15
11
|
from pygments.lexers import get_lexer_for_filename, TextLexer
|
|
16
12
|
from pygments.formatters import TerminalFormatter
|
|
@@ -18,104 +14,10 @@ import pygments.util
|
|
|
18
14
|
|
|
19
15
|
import requests
|
|
20
16
|
|
|
21
|
-
from
|
|
22
|
-
import traceback
|
|
23
|
-
from google import genai
|
|
24
|
-
from google.genai.types import GenerateContentConfig
|
|
25
|
-
from prompt_toolkit import prompt # Added for multiline input
|
|
26
|
-
|
|
17
|
+
from kopipasta.file import FileTuple, get_human_readable_size, is_binary, is_ignored, is_large_file, read_file_contents
|
|
27
18
|
import kopipasta.import_parser as import_parser
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class SimplePatchItem(BaseModel):
|
|
32
|
-
"""A single change described by reasoning, file path, original text, and new text."""
|
|
33
|
-
reasoning: str = Field(..., description="Explanation for why this specific change is proposed.")
|
|
34
|
-
file_path: str = Field(..., description="Relative path to the file to be modified.")
|
|
35
|
-
original_text: str = Field(..., description="The exact, unique block of text to be replaced.")
|
|
36
|
-
new_text: str = Field(..., description="The text to replace the original_text with.")
|
|
37
|
-
|
|
38
|
-
class SimplePatchArgs(BaseModel):
|
|
39
|
-
"""A list of proposed code changes."""
|
|
40
|
-
patches: List[SimplePatchItem] = Field(..., description="A list of patches to apply.")
|
|
41
|
-
|
|
42
|
-
def apply_simple_patch(patch_item: SimplePatchItem) -> bool:
|
|
43
|
-
"""
|
|
44
|
-
Applies a single patch defined by replacing original_text with new_text in file_path.
|
|
45
|
-
|
|
46
|
-
Validates that the file exists and the original_text is unique.
|
|
47
|
-
"""
|
|
48
|
-
print(f"\nApplying patch to: {patch_item.file_path}")
|
|
49
|
-
print(f"Reasoning: {patch_item.reasoning}")
|
|
50
|
-
print("-" * 20)
|
|
51
|
-
|
|
52
|
-
file_path = patch_item.file_path
|
|
53
|
-
original_text = patch_item.original_text
|
|
54
|
-
new_text = patch_item.new_text
|
|
55
|
-
|
|
56
|
-
# --- Validation ---
|
|
57
|
-
if not os.path.exists(file_path):
|
|
58
|
-
print(f"❌ Error: File not found: {file_path}")
|
|
59
|
-
print("-" * 20)
|
|
60
|
-
return False
|
|
61
|
-
|
|
62
|
-
try:
|
|
63
|
-
# Read the file content, attempting to preserve line endings implicitly
|
|
64
|
-
with open(file_path, 'r', encoding='utf-8', newline='') as f:
|
|
65
|
-
content = f.read()
|
|
66
|
-
|
|
67
|
-
# Check for unique occurrence of original_text
|
|
68
|
-
occurrences = content.count(original_text)
|
|
69
|
-
if occurrences == 0:
|
|
70
|
-
print(f"❌ Error: Original text not found in {file_path}.")
|
|
71
|
-
# Optional: print a snippet of the expected text for debugging
|
|
72
|
-
# print(f" Expected original text snippet: '{original_text[:100]}...'")
|
|
73
|
-
print("-" * 20)
|
|
74
|
-
return False
|
|
75
|
-
elif occurrences > 1:
|
|
76
|
-
print(f"❌ Error: Original text is not unique in {file_path} (found {occurrences} times).")
|
|
77
|
-
print(f" Patch cannot be applied automatically due to ambiguity.")
|
|
78
|
-
# print(f" Ambiguous original text snippet: '{original_text[:100]}...'")
|
|
79
|
-
print("-" * 20)
|
|
80
|
-
return False
|
|
81
|
-
|
|
82
|
-
# --- Application ---
|
|
83
|
-
# Replace the single unique occurrence
|
|
84
|
-
new_content = content.replace(original_text, new_text, 1)
|
|
85
|
-
|
|
86
|
-
# Heuristic to check if a newline might be needed at the end
|
|
87
|
-
original_ends_with_newline = content.endswith(('\n', '\r'))
|
|
88
|
-
new_ends_with_newline = new_content.endswith(('\n', '\r'))
|
|
89
|
-
|
|
90
|
-
if original_ends_with_newline and not new_ends_with_newline and new_content:
|
|
91
|
-
# Try to determine the original newline type
|
|
92
|
-
if content.endswith('\r\n'):
|
|
93
|
-
new_content += '\r\n'
|
|
94
|
-
else: # Assume '\n' otherwise
|
|
95
|
-
new_content += '\n'
|
|
96
|
-
elif not original_ends_with_newline and new_ends_with_newline:
|
|
97
|
-
# If original didn't end with newline, remove the one added by replacement
|
|
98
|
-
# This is less common but possible if new_text ends with \n and original_text didn't
|
|
99
|
-
new_content = new_content.rstrip('\r\n')
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
# Write the modified content back
|
|
103
|
-
with open(file_path, 'w', encoding='utf-8', newline='') as f:
|
|
104
|
-
f.write(new_content)
|
|
105
|
-
|
|
106
|
-
print(f"✅ Patch applied successfully to {file_path}.")
|
|
107
|
-
print("-" * 20)
|
|
108
|
-
return True
|
|
109
|
-
|
|
110
|
-
except IOError as e:
|
|
111
|
-
print(f"❌ Error reading or writing file {file_path}: {e}")
|
|
112
|
-
print("-" * 20)
|
|
113
|
-
return False
|
|
114
|
-
except Exception as e:
|
|
115
|
-
print(f"❌ An unexpected error occurred during patch application: {e}")
|
|
116
|
-
traceback.print_exc()
|
|
117
|
-
print("-" * 20)
|
|
118
|
-
return False
|
|
19
|
+
from kopipasta.tree_selector import TreeSelector
|
|
20
|
+
from kopipasta.prompt import generate_prompt_template, get_file_snippet, get_language_for_file
|
|
119
21
|
|
|
120
22
|
def _propose_and_add_dependencies(
|
|
121
23
|
file_just_added: str,
|
|
@@ -130,7 +32,7 @@ def _propose_and_add_dependencies(
|
|
|
130
32
|
if language not in ['python', 'typescript', 'javascript', 'tsx', 'jsx']:
|
|
131
33
|
return [], 0 # Only analyze languages we can parse
|
|
132
34
|
|
|
133
|
-
print(f"Analyzing {
|
|
35
|
+
print(f"Analyzing {os.path.relpath(file_just_added)} for local dependencies...")
|
|
134
36
|
|
|
135
37
|
try:
|
|
136
38
|
file_content = read_file_contents(file_just_added)
|
|
@@ -156,7 +58,7 @@ def _propose_and_add_dependencies(
|
|
|
156
58
|
|
|
157
59
|
print(f"\nFound {len(suggested_deps)} new local {'dependency' if len(suggested_deps) == 1 else 'dependencies'}:")
|
|
158
60
|
for i, dep_path in enumerate(suggested_deps):
|
|
159
|
-
print(f" ({i+1}) {
|
|
61
|
+
print(f" ({i+1}) {os.path.relpath(dep_path)}")
|
|
160
62
|
|
|
161
63
|
while True:
|
|
162
64
|
choice = input("\nAdd dependencies? (a)ll, (n)one, or enter numbers (e.g. 1, 3-4): ").lower()
|
|
@@ -212,12 +114,12 @@ def _propose_and_add_dependencies(
|
|
|
212
114
|
file_size = os.path.getsize(dep_path)
|
|
213
115
|
newly_added_files.append((dep_path, False, None, get_language_for_file(dep_path)))
|
|
214
116
|
char_count_delta += file_size
|
|
215
|
-
print(f"Added dependency: {
|
|
117
|
+
print(f"Added dependency: {os.path.relpath(dep_path)} ({get_human_readable_size(file_size)})")
|
|
216
118
|
|
|
217
119
|
return newly_added_files, char_count_delta
|
|
218
120
|
|
|
219
121
|
except Exception as e:
|
|
220
|
-
print(f"Warning: Could not analyze dependencies for {
|
|
122
|
+
print(f"Warning: Could not analyze dependencies for {os.path.relpath(file_just_added)}: {e}")
|
|
221
123
|
return [], 0
|
|
222
124
|
|
|
223
125
|
def get_colored_code(file_path, code):
|
|
@@ -252,85 +154,6 @@ def read_gitignore():
|
|
|
252
154
|
gitignore_patterns.append(line)
|
|
253
155
|
return gitignore_patterns
|
|
254
156
|
|
|
255
|
-
def is_ignored(path, ignore_patterns):
|
|
256
|
-
path = os.path.normpath(path)
|
|
257
|
-
for pattern in ignore_patterns:
|
|
258
|
-
if fnmatch.fnmatch(os.path.basename(path), pattern) or fnmatch.fnmatch(path, pattern):
|
|
259
|
-
return True
|
|
260
|
-
return False
|
|
261
|
-
|
|
262
|
-
def is_binary(file_path):
|
|
263
|
-
try:
|
|
264
|
-
with open(file_path, 'rb') as file:
|
|
265
|
-
chunk = file.read(1024)
|
|
266
|
-
if b'\0' in chunk: # null bytes indicate binary file
|
|
267
|
-
return True
|
|
268
|
-
if file_path.lower().endswith(('.json', '.csv')):
|
|
269
|
-
return False
|
|
270
|
-
return False
|
|
271
|
-
except IOError:
|
|
272
|
-
return False
|
|
273
|
-
|
|
274
|
-
def get_human_readable_size(size):
|
|
275
|
-
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
|
276
|
-
if size < 1024.0:
|
|
277
|
-
return f"{size:.2f} {unit}"
|
|
278
|
-
size /= 1024.0
|
|
279
|
-
|
|
280
|
-
def is_large_file(file_path, threshold=102400): # 100 KB threshold
|
|
281
|
-
return os.path.getsize(file_path) > threshold
|
|
282
|
-
|
|
283
|
-
def get_project_structure(ignore_patterns):
|
|
284
|
-
tree = []
|
|
285
|
-
for root, dirs, files in os.walk('.'):
|
|
286
|
-
dirs[:] = [d for d in dirs if not is_ignored(os.path.join(root, d), ignore_patterns)]
|
|
287
|
-
files = [f for f in files if not is_ignored(os.path.join(root, f), ignore_patterns)]
|
|
288
|
-
level = root.replace('.', '').count(os.sep)
|
|
289
|
-
indent = ' ' * 4 * level + '|-- '
|
|
290
|
-
tree.append(f"{indent}{os.path.basename(root)}/")
|
|
291
|
-
subindent = ' ' * 4 * (level + 1) + '|-- '
|
|
292
|
-
for f in files:
|
|
293
|
-
tree.append(f"{subindent}{f}")
|
|
294
|
-
return '\n'.join(tree)
|
|
295
|
-
|
|
296
|
-
def read_file_contents(file_path):
|
|
297
|
-
try:
|
|
298
|
-
with open(file_path, 'r') as file:
|
|
299
|
-
return file.read()
|
|
300
|
-
except Exception as e:
|
|
301
|
-
print(f"Error reading {file_path}: {e}")
|
|
302
|
-
return ""
|
|
303
|
-
|
|
304
|
-
def get_relative_path(file_path):
|
|
305
|
-
return os.path.relpath(file_path)
|
|
306
|
-
|
|
307
|
-
def get_language_for_file(file_path):
|
|
308
|
-
extension = os.path.splitext(file_path)[1].lower()
|
|
309
|
-
language_map = {
|
|
310
|
-
'.py': 'python',
|
|
311
|
-
'.js': 'javascript',
|
|
312
|
-
'.jsx': 'jsx',
|
|
313
|
-
'.ts': 'typescript',
|
|
314
|
-
'.tsx': 'tsx',
|
|
315
|
-
'.html': 'html',
|
|
316
|
-
'.htm': 'html',
|
|
317
|
-
'.css': 'css',
|
|
318
|
-
'.json': 'json',
|
|
319
|
-
'.md': 'markdown',
|
|
320
|
-
'.sql': 'sql',
|
|
321
|
-
'.sh': 'bash',
|
|
322
|
-
'.yml': 'yaml',
|
|
323
|
-
'.yaml': 'yaml',
|
|
324
|
-
'.go': 'go',
|
|
325
|
-
'.toml': 'toml',
|
|
326
|
-
'.c': 'c',
|
|
327
|
-
'.cpp': 'cpp',
|
|
328
|
-
'.cc': 'cpp',
|
|
329
|
-
'.h': 'cpp',
|
|
330
|
-
'.hpp': 'cpp',
|
|
331
|
-
}
|
|
332
|
-
return language_map.get(extension, '')
|
|
333
|
-
|
|
334
157
|
def split_python_file(file_content):
|
|
335
158
|
"""
|
|
336
159
|
Splits Python code into logical chunks using the AST module.
|
|
@@ -626,17 +449,6 @@ def get_placeholder_comment(language):
|
|
|
626
449
|
}
|
|
627
450
|
return comments.get(language, comments['default'])
|
|
628
451
|
|
|
629
|
-
def get_file_snippet(file_path, max_lines=50, max_bytes=4096):
|
|
630
|
-
snippet = ""
|
|
631
|
-
byte_count = 0
|
|
632
|
-
with open(file_path, 'r') as file:
|
|
633
|
-
for i, line in enumerate(file):
|
|
634
|
-
if i >= max_lines or byte_count >= max_bytes:
|
|
635
|
-
break
|
|
636
|
-
snippet += line
|
|
637
|
-
byte_count += len(line.encode('utf-8'))
|
|
638
|
-
return snippet
|
|
639
|
-
|
|
640
452
|
def get_colored_file_snippet(file_path, max_lines=50, max_bytes=4096):
|
|
641
453
|
snippet = get_file_snippet(file_path, max_lines, max_bytes)
|
|
642
454
|
return get_colored_code(file_path, snippet)
|
|
@@ -645,7 +457,211 @@ def print_char_count(count):
|
|
|
645
457
|
token_estimate = count // 4
|
|
646
458
|
print(f"\rCurrent prompt size: {count} characters (~ {token_estimate} tokens)", flush=True)
|
|
647
459
|
|
|
648
|
-
def
|
|
460
|
+
def grep_files_in_directory(pattern: str, directory: str, ignore_patterns: List[str]) -> List[Tuple[str, List[str], int]]:
|
|
461
|
+
"""
|
|
462
|
+
Search for files containing a pattern using ag (silver searcher).
|
|
463
|
+
Returns list of (filepath, preview_lines, match_count).
|
|
464
|
+
"""
|
|
465
|
+
# Check if ag is available
|
|
466
|
+
if not shutil.which('ag'):
|
|
467
|
+
print("Silver Searcher (ag) not found. Install it for grep functionality:")
|
|
468
|
+
print(" - Mac: brew install the_silver_searcher")
|
|
469
|
+
print(" - Ubuntu/Debian: apt-get install silversearcher-ag")
|
|
470
|
+
print(" - Other: https://github.com/ggreer/the_silver_searcher")
|
|
471
|
+
return []
|
|
472
|
+
|
|
473
|
+
try:
|
|
474
|
+
# First get files with matches
|
|
475
|
+
cmd = [
|
|
476
|
+
'ag',
|
|
477
|
+
'--files-with-matches',
|
|
478
|
+
'--nocolor',
|
|
479
|
+
'--ignore-case',
|
|
480
|
+
pattern,
|
|
481
|
+
directory
|
|
482
|
+
]
|
|
483
|
+
|
|
484
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
485
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
486
|
+
return []
|
|
487
|
+
|
|
488
|
+
files = result.stdout.strip().split('\n')
|
|
489
|
+
grep_results = []
|
|
490
|
+
|
|
491
|
+
for file in files:
|
|
492
|
+
if is_ignored(file, ignore_patterns) or is_binary(file):
|
|
493
|
+
continue
|
|
494
|
+
|
|
495
|
+
# Get match count and preview lines
|
|
496
|
+
count_cmd = ['ag', '--count', '--nocolor', pattern, file]
|
|
497
|
+
count_result = subprocess.run(count_cmd, capture_output=True, text=True)
|
|
498
|
+
match_count = 0
|
|
499
|
+
if count_result.stdout:
|
|
500
|
+
# ag --count outputs "filename:count"
|
|
501
|
+
# We need to handle filenames that might contain colons
|
|
502
|
+
stdout_line = count_result.stdout.strip()
|
|
503
|
+
# Find the last colon to separate filename from count
|
|
504
|
+
last_colon_idx = stdout_line.rfind(':')
|
|
505
|
+
if last_colon_idx > 0:
|
|
506
|
+
try:
|
|
507
|
+
match_count = int(stdout_line[last_colon_idx + 1:])
|
|
508
|
+
except ValueError:
|
|
509
|
+
match_count = 1
|
|
510
|
+
else:
|
|
511
|
+
match_count = 1
|
|
512
|
+
|
|
513
|
+
# Get preview of matches (up to 3 lines)
|
|
514
|
+
preview_cmd = [
|
|
515
|
+
'ag',
|
|
516
|
+
'--max-count=3',
|
|
517
|
+
'--nocolor',
|
|
518
|
+
'--noheading',
|
|
519
|
+
'--numbers',
|
|
520
|
+
pattern,
|
|
521
|
+
file
|
|
522
|
+
]
|
|
523
|
+
preview_result = subprocess.run(preview_cmd, capture_output=True, text=True)
|
|
524
|
+
preview_lines = []
|
|
525
|
+
if preview_result.stdout:
|
|
526
|
+
for line in preview_result.stdout.strip().split('\n')[:3]:
|
|
527
|
+
# Format: "line_num:content"
|
|
528
|
+
if ':' in line:
|
|
529
|
+
line_num, content = line.split(':', 1)
|
|
530
|
+
preview_lines.append(f" {line_num}: {content.strip()}")
|
|
531
|
+
else:
|
|
532
|
+
preview_lines.append(f" {line.strip()}")
|
|
533
|
+
|
|
534
|
+
grep_results.append((file, preview_lines, match_count))
|
|
535
|
+
|
|
536
|
+
return sorted(grep_results)
|
|
537
|
+
|
|
538
|
+
except Exception as e:
|
|
539
|
+
print(f"Error running ag: {e}")
|
|
540
|
+
return []
|
|
541
|
+
|
|
542
|
+
def select_from_grep_results(
|
|
543
|
+
grep_results: List[Tuple[str, List[str], int]],
|
|
544
|
+
current_char_count: int
|
|
545
|
+
) -> Tuple[List[FileTuple], int]:
|
|
546
|
+
"""
|
|
547
|
+
Let user select from grep results.
|
|
548
|
+
Returns (selected_files, new_char_count).
|
|
549
|
+
"""
|
|
550
|
+
if not grep_results:
|
|
551
|
+
return [], current_char_count
|
|
552
|
+
|
|
553
|
+
print(f"\nFound {len(grep_results)} files:")
|
|
554
|
+
for i, (file_path, preview_lines, match_count) in enumerate(grep_results):
|
|
555
|
+
file_size = os.path.getsize(file_path)
|
|
556
|
+
file_size_readable = get_human_readable_size(file_size)
|
|
557
|
+
print(f"\n{i+1}. {os.path.relpath(file_path)} ({file_size_readable}) - {match_count} {'match' if match_count == 1 else 'matches'}")
|
|
558
|
+
for preview_line in preview_lines[:3]:
|
|
559
|
+
print(preview_line)
|
|
560
|
+
if match_count > 3:
|
|
561
|
+
print(f" ... and {match_count - 3} more matches")
|
|
562
|
+
|
|
563
|
+
while True:
|
|
564
|
+
print_char_count(current_char_count)
|
|
565
|
+
choice = input("\nSelect grep results: (a)ll / (n)one / (s)elect individually / numbers (e.g. 1,3-4) / (q)uit? ").lower()
|
|
566
|
+
|
|
567
|
+
selected_files: List[FileTuple] = []
|
|
568
|
+
char_count_delta = 0
|
|
569
|
+
|
|
570
|
+
if choice == 'a':
|
|
571
|
+
for file_path, _, _ in grep_results:
|
|
572
|
+
file_size = os.path.getsize(file_path)
|
|
573
|
+
selected_files.append((file_path, False, None, get_language_for_file(file_path)))
|
|
574
|
+
char_count_delta += file_size
|
|
575
|
+
print(f"Added all {len(grep_results)} files from grep results.")
|
|
576
|
+
return selected_files, current_char_count + char_count_delta
|
|
577
|
+
|
|
578
|
+
elif choice == 'n':
|
|
579
|
+
print("Skipped all grep results.")
|
|
580
|
+
return [], current_char_count
|
|
581
|
+
|
|
582
|
+
elif choice == 'q':
|
|
583
|
+
print("Cancelled grep selection.")
|
|
584
|
+
return [], current_char_count
|
|
585
|
+
|
|
586
|
+
elif choice == 's':
|
|
587
|
+
for i, (file_path, preview_lines, match_count) in enumerate(grep_results):
|
|
588
|
+
file_size = os.path.getsize(file_path)
|
|
589
|
+
file_size_readable = get_human_readable_size(file_size)
|
|
590
|
+
file_char_estimate = file_size
|
|
591
|
+
file_token_estimate = file_char_estimate // 4
|
|
592
|
+
|
|
593
|
+
print(f"\n{os.path.relpath(file_path)} ({file_size_readable}, ~{file_char_estimate} chars, ~{file_token_estimate} tokens)")
|
|
594
|
+
print(f"{match_count} {'match' if match_count == 1 else 'matches'} for search pattern")
|
|
595
|
+
|
|
596
|
+
while True:
|
|
597
|
+
print_char_count(current_char_count + char_count_delta)
|
|
598
|
+
file_choice = input("(y)es / (n)o / (q)uit? ").lower()
|
|
599
|
+
|
|
600
|
+
if file_choice == 'y':
|
|
601
|
+
if is_large_file(file_path):
|
|
602
|
+
while True:
|
|
603
|
+
snippet_choice = input(f"File is large. Use (f)ull content or (s)nippet? ").lower()
|
|
604
|
+
if snippet_choice in ['f', 's']:
|
|
605
|
+
break
|
|
606
|
+
print("Invalid choice. Please enter 'f' or 's'.")
|
|
607
|
+
if snippet_choice == 's':
|
|
608
|
+
selected_files.append((file_path, True, None, get_language_for_file(file_path)))
|
|
609
|
+
char_count_delta += len(get_file_snippet(file_path))
|
|
610
|
+
else:
|
|
611
|
+
selected_files.append((file_path, False, None, get_language_for_file(file_path)))
|
|
612
|
+
char_count_delta += file_size
|
|
613
|
+
else:
|
|
614
|
+
selected_files.append((file_path, False, None, get_language_for_file(file_path)))
|
|
615
|
+
char_count_delta += file_size
|
|
616
|
+
print(f"Added: {os.path.relpath(file_path)}")
|
|
617
|
+
break
|
|
618
|
+
elif file_choice == 'n':
|
|
619
|
+
break
|
|
620
|
+
elif file_choice == 'q':
|
|
621
|
+
print(f"Added {len(selected_files)} files from grep results.")
|
|
622
|
+
return selected_files, current_char_count + char_count_delta
|
|
623
|
+
else:
|
|
624
|
+
print("Invalid choice. Please enter 'y', 'n', or 'q'.")
|
|
625
|
+
|
|
626
|
+
print(f"Added {len(selected_files)} files from grep results.")
|
|
627
|
+
return selected_files, current_char_count + char_count_delta
|
|
628
|
+
|
|
629
|
+
else:
|
|
630
|
+
# Try to parse number selection
|
|
631
|
+
try:
|
|
632
|
+
selected_indices = set()
|
|
633
|
+
parts = choice.replace(' ', '').split(',')
|
|
634
|
+
if all(p.strip() for p in parts):
|
|
635
|
+
for part in parts:
|
|
636
|
+
if '-' in part:
|
|
637
|
+
start_str, end_str = part.split('-', 1)
|
|
638
|
+
start = int(start_str)
|
|
639
|
+
end = int(end_str)
|
|
640
|
+
if start > end:
|
|
641
|
+
start, end = end, start
|
|
642
|
+
selected_indices.update(range(start - 1, end))
|
|
643
|
+
else:
|
|
644
|
+
selected_indices.add(int(part) - 1)
|
|
645
|
+
|
|
646
|
+
if all(0 <= i < len(grep_results) for i in selected_indices):
|
|
647
|
+
for i in sorted(selected_indices):
|
|
648
|
+
file_path, _, _ = grep_results[i]
|
|
649
|
+
file_size = os.path.getsize(file_path)
|
|
650
|
+
selected_files.append((file_path, False, None, get_language_for_file(file_path)))
|
|
651
|
+
char_count_delta += file_size
|
|
652
|
+
print(f"Added {len(selected_files)} files from grep results.")
|
|
653
|
+
return selected_files, current_char_count + char_count_delta
|
|
654
|
+
else:
|
|
655
|
+
print(f"Error: Invalid number selection. Please choose numbers between 1 and {len(grep_results)}.")
|
|
656
|
+
else:
|
|
657
|
+
raise ValueError("Empty part detected in input.")
|
|
658
|
+
except ValueError:
|
|
659
|
+
print("Invalid choice. Please enter 'a', 'n', 's', 'q', or a list/range of numbers.")
|
|
660
|
+
|
|
661
|
+
def select_files_in_directory(directory: str, ignore_patterns: List[str], project_root_abs: str, current_char_count: int = 0, selected_files_set: Optional[Set[str]] = None) -> Tuple[List[FileTuple], int]:
|
|
662
|
+
if selected_files_set is None:
|
|
663
|
+
selected_files_set = set()
|
|
664
|
+
|
|
649
665
|
files = [f for f in os.listdir(directory)
|
|
650
666
|
if os.path.isfile(os.path.join(directory, f)) and not is_ignored(os.path.join(directory, f), ignore_patterns) and not is_binary(os.path.join(directory, f))]
|
|
651
667
|
|
|
@@ -660,17 +676,129 @@ def select_files_in_directory(directory: str, ignore_patterns: List[str], projec
|
|
|
660
676
|
file_size_readable = get_human_readable_size(file_size)
|
|
661
677
|
file_char_estimate = file_size # Assuming 1 byte ≈ 1 character for text files
|
|
662
678
|
file_token_estimate = file_char_estimate // 4
|
|
663
|
-
|
|
679
|
+
|
|
680
|
+
# Show if already selected
|
|
681
|
+
if os.path.abspath(file_path) in selected_files_set:
|
|
682
|
+
print(f"✓ {file} ({file_size_readable}, ~{file_char_estimate} chars, ~{file_token_estimate} tokens) [already selected]")
|
|
683
|
+
else:
|
|
684
|
+
print(f"- {file} ({file_size_readable}, ~{file_char_estimate} chars, ~{file_token_estimate} tokens)")
|
|
664
685
|
|
|
665
686
|
while True:
|
|
666
687
|
print_char_count(current_char_count)
|
|
667
|
-
choice = input("(y)es add all / (n)o ignore all / (s)elect individually / (q)uit? ").lower()
|
|
688
|
+
choice = input("(y)es add all / (n)o ignore all / (s)elect individually / (g)rep / (q)uit? ").lower()
|
|
668
689
|
selected_files: List[FileTuple] = []
|
|
669
690
|
char_count_delta = 0
|
|
691
|
+
|
|
692
|
+
if choice == 'g':
|
|
693
|
+
# Grep functionality
|
|
694
|
+
pattern = input("\nEnter search pattern: ")
|
|
695
|
+
if pattern:
|
|
696
|
+
print(f"\nSearching in {directory} for '{pattern}'...")
|
|
697
|
+
grep_results = grep_files_in_directory(pattern, directory, ignore_patterns)
|
|
698
|
+
|
|
699
|
+
if not grep_results:
|
|
700
|
+
print(f"No files found matching '{pattern}'")
|
|
701
|
+
continue
|
|
702
|
+
|
|
703
|
+
grep_selected, new_char_count = select_from_grep_results(grep_results, current_char_count)
|
|
704
|
+
|
|
705
|
+
if grep_selected:
|
|
706
|
+
selected_files.extend(grep_selected)
|
|
707
|
+
current_char_count = new_char_count
|
|
708
|
+
|
|
709
|
+
# Update selected files set
|
|
710
|
+
for file_tuple in grep_selected:
|
|
711
|
+
selected_files_set.add(os.path.abspath(file_tuple[0]))
|
|
712
|
+
|
|
713
|
+
# Analyze dependencies for grep-selected files
|
|
714
|
+
files_to_analyze = [f[0] for f in grep_selected]
|
|
715
|
+
for file_path in files_to_analyze:
|
|
716
|
+
new_deps, deps_char_count = _propose_and_add_dependencies(
|
|
717
|
+
file_path, project_root_abs, selected_files, current_char_count
|
|
718
|
+
)
|
|
719
|
+
selected_files.extend(new_deps)
|
|
720
|
+
current_char_count += deps_char_count
|
|
721
|
+
|
|
722
|
+
# Update selected files set with dependencies
|
|
723
|
+
for dep_tuple in new_deps:
|
|
724
|
+
selected_files_set.add(os.path.abspath(dep_tuple[0]))
|
|
725
|
+
|
|
726
|
+
print(f"\nReturning to directory: {directory}")
|
|
727
|
+
# Re-show the directory with updated selections
|
|
728
|
+
print("Files:")
|
|
729
|
+
for file in files:
|
|
730
|
+
file_path = os.path.join(directory, file)
|
|
731
|
+
file_size = os.path.getsize(file_path)
|
|
732
|
+
file_size_readable = get_human_readable_size(file_size)
|
|
733
|
+
if os.path.abspath(file_path) in selected_files_set:
|
|
734
|
+
print(f"✓ {file} ({file_size_readable}) [already selected]")
|
|
735
|
+
else:
|
|
736
|
+
print(f"- {file} ({file_size_readable})")
|
|
737
|
+
|
|
738
|
+
# Ask what to do with remaining files
|
|
739
|
+
remaining_files = [f for f in files if os.path.abspath(os.path.join(directory, f)) not in selected_files_set]
|
|
740
|
+
if remaining_files:
|
|
741
|
+
while True:
|
|
742
|
+
print_char_count(current_char_count)
|
|
743
|
+
remaining_choice = input("(y)es add remaining / (n)o skip remaining / (s)elect more / (g)rep again / (q)uit? ").lower()
|
|
744
|
+
if remaining_choice == 'y':
|
|
745
|
+
# Add all remaining files
|
|
746
|
+
for file in remaining_files:
|
|
747
|
+
file_path = os.path.join(directory, file)
|
|
748
|
+
file_size = os.path.getsize(file_path)
|
|
749
|
+
selected_files.append((file_path, False, None, get_language_for_file(file_path)))
|
|
750
|
+
current_char_count += file_size
|
|
751
|
+
selected_files_set.add(os.path.abspath(file_path))
|
|
752
|
+
|
|
753
|
+
# Analyze dependencies for remaining files
|
|
754
|
+
for file in remaining_files:
|
|
755
|
+
file_path = os.path.join(directory, file)
|
|
756
|
+
new_deps, deps_char_count = _propose_and_add_dependencies(
|
|
757
|
+
file_path, project_root_abs, selected_files, current_char_count
|
|
758
|
+
)
|
|
759
|
+
selected_files.extend(new_deps)
|
|
760
|
+
current_char_count += deps_char_count
|
|
761
|
+
|
|
762
|
+
print(f"Added all remaining files from {directory}")
|
|
763
|
+
return selected_files, current_char_count
|
|
764
|
+
elif remaining_choice == 'n':
|
|
765
|
+
print(f"Skipped remaining files from {directory}")
|
|
766
|
+
return selected_files, current_char_count
|
|
767
|
+
elif remaining_choice == 's':
|
|
768
|
+
# Continue to individual selection
|
|
769
|
+
choice = 's'
|
|
770
|
+
break
|
|
771
|
+
elif remaining_choice == 'g':
|
|
772
|
+
# Continue to grep again
|
|
773
|
+
choice = 'g'
|
|
774
|
+
break
|
|
775
|
+
elif remaining_choice == 'q':
|
|
776
|
+
return selected_files, current_char_count
|
|
777
|
+
else:
|
|
778
|
+
print("Invalid choice. Please try again.")
|
|
779
|
+
|
|
780
|
+
if choice == 's':
|
|
781
|
+
# Fall through to individual selection
|
|
782
|
+
pass
|
|
783
|
+
elif choice == 'g':
|
|
784
|
+
# Loop back to grep
|
|
785
|
+
continue
|
|
786
|
+
else:
|
|
787
|
+
# No remaining files
|
|
788
|
+
return selected_files, current_char_count
|
|
789
|
+
else:
|
|
790
|
+
# No files selected from grep, continue
|
|
791
|
+
continue
|
|
792
|
+
else:
|
|
793
|
+
continue
|
|
794
|
+
|
|
670
795
|
if choice == 'y':
|
|
671
796
|
files_to_add_after_loop = []
|
|
672
797
|
for file in files:
|
|
673
798
|
file_path = os.path.join(directory, file)
|
|
799
|
+
if os.path.abspath(file_path) in selected_files_set:
|
|
800
|
+
continue # Skip already selected files
|
|
801
|
+
|
|
674
802
|
if is_large_file(file_path):
|
|
675
803
|
while True:
|
|
676
804
|
snippet_choice = input(f"{file} is large. Use (f)ull content or (s)nippet? ").lower()
|
|
@@ -697,12 +825,17 @@ def select_files_in_directory(directory: str, ignore_patterns: List[str], projec
|
|
|
697
825
|
|
|
698
826
|
print(f"Added all files from {directory}")
|
|
699
827
|
return selected_files, current_char_count
|
|
828
|
+
|
|
700
829
|
elif choice == 'n':
|
|
701
830
|
print(f"Ignored all files from {directory}")
|
|
702
831
|
return [], current_char_count
|
|
832
|
+
|
|
703
833
|
elif choice == 's':
|
|
704
834
|
for file in files:
|
|
705
835
|
file_path = os.path.join(directory, file)
|
|
836
|
+
if os.path.abspath(file_path) in selected_files_set:
|
|
837
|
+
continue # Skip already selected files
|
|
838
|
+
|
|
706
839
|
file_size = os.path.getsize(file_path)
|
|
707
840
|
file_size_readable = get_human_readable_size(file_size)
|
|
708
841
|
file_char_estimate = file_size
|
|
@@ -731,6 +864,7 @@ def select_files_in_directory(directory: str, ignore_patterns: List[str], projec
|
|
|
731
864
|
|
|
732
865
|
if file_to_add:
|
|
733
866
|
selected_files.append(file_to_add)
|
|
867
|
+
selected_files_set.add(os.path.abspath(file_path))
|
|
734
868
|
# Analyze dependencies immediately after adding
|
|
735
869
|
new_deps, deps_char_count = _propose_and_add_dependencies(file_path, project_root_abs, selected_files, current_char_count)
|
|
736
870
|
selected_files.extend(new_deps)
|
|
@@ -743,6 +877,7 @@ def select_files_in_directory(directory: str, ignore_patterns: List[str], projec
|
|
|
743
877
|
if chunks:
|
|
744
878
|
selected_files.append((file_path, False, chunks, get_language_for_file(file_path)))
|
|
745
879
|
current_char_count += char_count
|
|
880
|
+
selected_files_set.add(os.path.abspath(file_path))
|
|
746
881
|
break
|
|
747
882
|
elif file_choice == 'q':
|
|
748
883
|
print(f"Quitting selection for {directory}")
|
|
@@ -751,13 +886,18 @@ def select_files_in_directory(directory: str, ignore_patterns: List[str], projec
|
|
|
751
886
|
print("Invalid choice. Please enter 'y', 'n', 'p', or 'q'.")
|
|
752
887
|
print(f"Added {len(selected_files)} files from {directory}")
|
|
753
888
|
return selected_files, current_char_count
|
|
889
|
+
|
|
754
890
|
elif choice == 'q':
|
|
755
891
|
print(f"Quitting selection for {directory}")
|
|
756
892
|
return [], current_char_count
|
|
757
893
|
else:
|
|
758
894
|
print("Invalid choice. Please try again.")
|
|
759
895
|
|
|
760
|
-
|
|
896
|
+
|
|
897
|
+
def process_directory(directory: str, ignore_patterns: List[str], project_root_abs: str, current_char_count: int = 0, selected_files_set: Optional[Set[str]] = None) -> Tuple[List[FileTuple], Set[str], int]:
|
|
898
|
+
if selected_files_set is None:
|
|
899
|
+
selected_files_set = set()
|
|
900
|
+
|
|
761
901
|
files_to_include: List[FileTuple] = []
|
|
762
902
|
processed_dirs: Set[str] = set()
|
|
763
903
|
|
|
@@ -771,10 +911,16 @@ def process_directory(directory: str, ignore_patterns: List[str], project_root_a
|
|
|
771
911
|
print(f"\nExploring directory: {root}")
|
|
772
912
|
choice = input("(y)es explore / (n)o skip / (q)uit? ").lower()
|
|
773
913
|
if choice == 'y':
|
|
774
|
-
# Pass
|
|
775
|
-
selected_files, current_char_count = select_files_in_directory(
|
|
776
|
-
|
|
914
|
+
# Pass selected_files_set to track already selected files
|
|
915
|
+
selected_files, current_char_count = select_files_in_directory(
|
|
916
|
+
root, ignore_patterns, project_root_abs, current_char_count, selected_files_set
|
|
917
|
+
)
|
|
777
918
|
files_to_include.extend(selected_files)
|
|
919
|
+
|
|
920
|
+
# Update selected_files_set
|
|
921
|
+
for file_tuple in selected_files:
|
|
922
|
+
selected_files_set.add(os.path.abspath(file_tuple[0]))
|
|
923
|
+
|
|
778
924
|
processed_dirs.add(root)
|
|
779
925
|
elif choice == 'n':
|
|
780
926
|
dirs[:] = [] # Skip all subdirectories
|
|
@@ -819,122 +965,6 @@ def read_env_file():
|
|
|
819
965
|
env_vars[key.strip()] = value.strip()
|
|
820
966
|
return env_vars
|
|
821
967
|
|
|
822
|
-
def detect_env_variables(content, env_vars):
|
|
823
|
-
detected_vars = []
|
|
824
|
-
for key, value in env_vars.items():
|
|
825
|
-
if value in content:
|
|
826
|
-
detected_vars.append((key, value))
|
|
827
|
-
return detected_vars
|
|
828
|
-
|
|
829
|
-
def handle_env_variables(content, env_vars):
|
|
830
|
-
detected_vars = detect_env_variables(content, env_vars)
|
|
831
|
-
if not detected_vars:
|
|
832
|
-
return content
|
|
833
|
-
|
|
834
|
-
print("Detected environment variables:")
|
|
835
|
-
for key, value in detected_vars:
|
|
836
|
-
print(f"- {key}={value}")
|
|
837
|
-
|
|
838
|
-
for key, value in detected_vars:
|
|
839
|
-
while True:
|
|
840
|
-
choice = input(f"How would you like to handle {key}? (m)ask / (s)kip / (k)eep: ").lower()
|
|
841
|
-
if choice in ['m', 's', 'k']:
|
|
842
|
-
break
|
|
843
|
-
print("Invalid choice. Please enter 'm', 's', or 'k'.")
|
|
844
|
-
|
|
845
|
-
if choice == 'm':
|
|
846
|
-
content = content.replace(value, '*' * len(value))
|
|
847
|
-
elif choice == 's':
|
|
848
|
-
content = content.replace(value, "[REDACTED]")
|
|
849
|
-
# If 'k', we don't modify the content
|
|
850
|
-
|
|
851
|
-
return content
|
|
852
|
-
|
|
853
|
-
def generate_prompt_template(files_to_include: List[FileTuple], ignore_patterns: List[str], web_contents: Dict[str, Tuple[FileTuple, str]], env_vars: Dict[str, str]) -> Tuple[str, int]:
|
|
854
|
-
prompt = "# Project Overview\n\n"
|
|
855
|
-
prompt += "## Project Structure\n\n"
|
|
856
|
-
prompt += "```\n"
|
|
857
|
-
prompt += get_project_structure(ignore_patterns)
|
|
858
|
-
prompt += "\n```\n\n"
|
|
859
|
-
prompt += "## File Contents\n\n"
|
|
860
|
-
for file, use_snippet, chunks, content_type in files_to_include:
|
|
861
|
-
relative_path = get_relative_path(file)
|
|
862
|
-
language = content_type if content_type else get_language_for_file(file)
|
|
863
|
-
|
|
864
|
-
if chunks is not None:
|
|
865
|
-
prompt += f"### {relative_path} (selected patches)\n\n```{language}\n"
|
|
866
|
-
for chunk in chunks:
|
|
867
|
-
prompt += f"{chunk}\n"
|
|
868
|
-
prompt += "```\n\n"
|
|
869
|
-
elif use_snippet:
|
|
870
|
-
file_content = get_file_snippet(file)
|
|
871
|
-
prompt += f"### {relative_path} (snippet)\n\n```{language}\n{file_content}\n```\n\n"
|
|
872
|
-
else:
|
|
873
|
-
file_content = read_file_contents(file)
|
|
874
|
-
file_content = handle_env_variables(file_content, env_vars)
|
|
875
|
-
prompt += f"### {relative_path}\n\n```{language}\n{file_content}\n```\n\n"
|
|
876
|
-
|
|
877
|
-
if web_contents:
|
|
878
|
-
prompt += "## Web Content\n\n"
|
|
879
|
-
for url, (file_tuple, content) in web_contents.items():
|
|
880
|
-
_, is_snippet, _, content_type = file_tuple
|
|
881
|
-
content = handle_env_variables(content, env_vars)
|
|
882
|
-
language = content_type if content_type in ['json', 'csv'] else ''
|
|
883
|
-
prompt += f"### {url}{' (snippet)' if is_snippet else ''}\n\n```{language}\n{content}\n```\n\n"
|
|
884
|
-
|
|
885
|
-
prompt += "## Task Instructions\n\n"
|
|
886
|
-
cursor_position = len(prompt)
|
|
887
|
-
prompt += "\n\n"
|
|
888
|
-
prompt += "## Instructions for Achieving the Task\n\n"
|
|
889
|
-
analysis_text = (
|
|
890
|
-
"### Your Operating Model: The Expert Council\n\n"
|
|
891
|
-
"You will operate as a council of expert personas in a direct, collaborative partnership with me, the user. Your entire thinking process should be externalized as a dialogue between these personas. Any persona can and should interact directly with me.\n\n"
|
|
892
|
-
"#### Core Personas\n"
|
|
893
|
-
"- **Facilitator**: The lead coordinator. You summarize discussions, ensure the team stays on track, and are responsible for presenting the final, consolidated plans and code to me.\n"
|
|
894
|
-
"- **Architect**: Focuses on high-level design, system structure, dependencies, scalability, and long-term maintainability. Asks 'Is this well-designed?'\n"
|
|
895
|
-
"- **Builder**: The hands-on engineer. Focuses on implementation details, writing clean and functional code, and debugging. Asks 'How do we build this?' **When presenting code obey Rules for Presenting Code**\n"
|
|
896
|
-
"- **Critique**: The quality advocate. Focuses on identifying edge cases, potential bugs, and security vulnerabilities. Asks 'What could go wrong?' and provides constructive critique on both the team's proposals and my (the user's) requests.\n\n"
|
|
897
|
-
"#### Dynamic Personas\n"
|
|
898
|
-
"When the task requires it, introduce other specialist personas. For example: UX Designer, DevOps Engineer, Data Scientist, etc.\n\n"
|
|
899
|
-
"### Development Workflow: A Structured Dialogue\n\n"
|
|
900
|
-
"Follow this conversational workflow. Prefix direct communication to me with the persona's name (e.g., 'Critique to User: ...').\n\n"
|
|
901
|
-
"1. **Understand & Clarify (Dialogue)**\n"
|
|
902
|
-
" - **Facilitator**: Start by stating the goal as you understand it.\n"
|
|
903
|
-
" - **All**: Discuss the requirements. Any persona can directly ask me for clarification. Identify and list any missing information ([MISSING]) or assumptions ([ASSUMPTION]).\n"
|
|
904
|
-
" - **Critique**: Directly challenge any part of my request that seems ambiguous, risky, or suboptimal.\n\n"
|
|
905
|
-
"2. **Plan (Dialogue)**\n"
|
|
906
|
-
" - **Architect**: Propose one or more high-level plans. Directly ask me for input on tradeoffs if necessary.\n"
|
|
907
|
-
" - **Critique**: Challenge the plans. Point out risks or edge cases.\n"
|
|
908
|
-
" - **Builder**: Comment on the implementation feasibility.\n"
|
|
909
|
-
" - **Facilitator**: Synthesize the discussion, select the best plan, and present a clear, step-by-step summary to me for approval.\n\n"
|
|
910
|
-
"3. **Implement (Code & Dialogue)**\n"
|
|
911
|
-
" - **Facilitator**: State which step of the plan is being implemented.\n"
|
|
912
|
-
" - **Builder**: Write the code. If you encounter an issue, you can ask me directly for more context.\n"
|
|
913
|
-
" - **Critique**: Review the code and the underlying assumptions.\n"
|
|
914
|
-
" - **Facilitator**: Present the final, reviewed code for that step. Ask me to test it if appropriate.\n\n"
|
|
915
|
-
"4. **Present E2E Draft (Consolidation)**\n"
|
|
916
|
-
" - When I ask you to consolidate, your goal is to provide a clear, unambiguous set of changes that I can directly copy and paste into my editor.\n"
|
|
917
|
-
" - **Present changes grouped by file.** For each change, provide the **entire updated function, class, or logical block** to make replacement easy.\n"
|
|
918
|
-
" - Use context comments to show where the new code block fits.\n"
|
|
919
|
-
" - **Do not use a diff format (`+` or `-` lines).** Provide the final, clean code.\n\n"
|
|
920
|
-
"5. **Validate & Iterate (Feedback Loop)**\n"
|
|
921
|
-
" - After presenting the consolidated draft, you will explicitly prompt me for testing and wait for my feedback.\n"
|
|
922
|
-
" - Upon receiving my feedback, the **Facilitator** will announce the next step.\n"
|
|
923
|
-
" - If the feedback indicates a minor bug, you will return to **Step 3 (Implement)** to fix it.\n"
|
|
924
|
-
" - If the feedback reveals a fundamental design flaw or misunderstanding, you will return to **Step 2 (Plan)** or even **Step 1 (Understand)** to re-evaluate the approach.\n\n"
|
|
925
|
-
"### Rules for Presenting Code\n\n"
|
|
926
|
-
"- **Always specify the filename** at the start of a code block (e.g., `// FILE: kopipasta/main.py`).\n"
|
|
927
|
-
"- **Incremental Changes**: During implementation, show only the changed functions or classes. Use comments like `// ... existing code ...` to represent unchanged parts.\n"
|
|
928
|
-
"- **Missing Files**: Never write placeholder code. Instead, state it's missing and request it.\n\n"
|
|
929
|
-
"### Your Permissions\n\n"
|
|
930
|
-
"- **You are empowered to interact with me directly.** Any persona can ask me questions or provide constructive feedback on my requests, assumptions, or the provided context.\n"
|
|
931
|
-
"- You MUST request any file shown in the project tree that you need but was not provided.\n"
|
|
932
|
-
"- You MUST ask me to run code, test integrations (APIs, databases), and share the output.\n"
|
|
933
|
-
"- You MUST ask for clarification instead of making risky assumptions.\n"
|
|
934
|
-
)
|
|
935
|
-
prompt += analysis_text
|
|
936
|
-
return prompt, cursor_position
|
|
937
|
-
|
|
938
968
|
def open_editor_for_input(template: str, cursor_position: int) -> str:
|
|
939
969
|
editor = os.environ.get('EDITOR', 'vim')
|
|
940
970
|
with tempfile.NamedTemporaryFile(mode='w+', suffix='.md', delete=False) as temp_file:
|
|
@@ -961,232 +991,26 @@ def open_editor_for_input(template: str, cursor_position: int) -> str:
|
|
|
961
991
|
finally:
|
|
962
992
|
os.unlink(temp_file_path)
|
|
963
993
|
|
|
964
|
-
def start_chat_session(initial_prompt: str):
|
|
965
|
-
"""Starts an interactive chat session with the Gemini API using google-genai."""
|
|
966
|
-
if not genai:
|
|
967
|
-
# Error message already printed during import if it failed
|
|
968
|
-
sys.exit(1)
|
|
969
|
-
|
|
970
|
-
# The google-genai library automatically uses GOOGLE_API_KEY env var if set
|
|
971
|
-
# We still check if it's set to provide a clearer error message upfront
|
|
972
|
-
if not os.environ.get('GOOGLE_API_KEY'):
|
|
973
|
-
print("Error: GOOGLE_API_KEY environment variable not set.")
|
|
974
|
-
print("Please set the GOOGLE_API_KEY environment variable with your API key.")
|
|
975
|
-
sys.exit(1)
|
|
976
|
-
|
|
977
|
-
try:
|
|
978
|
-
# Create the client - it will use the env var automatically
|
|
979
|
-
client = genai.Client()
|
|
980
|
-
print("Google GenAI Client created (using GOOGLE_API_KEY).")
|
|
981
|
-
# You could add a check here like listing models to verify the key early
|
|
982
|
-
# print("Available models:", [m.name for m in client.models.list()])
|
|
983
|
-
except Exception as e:
|
|
984
|
-
print(f"Error creating Google GenAI client: {e}")
|
|
985
|
-
print("Please ensure your GOOGLE_API_KEY is valid and has permissions.")
|
|
986
|
-
sys.exit(1)
|
|
987
|
-
|
|
988
|
-
model_name = 'gemini-2.5-pro-exp-03-25'
|
|
989
|
-
config = GenerateContentConfig(temperature=0.0)
|
|
990
|
-
print(f"Using model: {model_name}")
|
|
991
|
-
|
|
992
|
-
try:
|
|
993
|
-
# Create a chat session using the client
|
|
994
|
-
chat = client.chats.create(model=model_name, config=config)
|
|
995
|
-
# Note: History is managed by the chat object itself
|
|
996
|
-
|
|
997
|
-
print("\n--- Starting Interactive Chat with Gemini ---")
|
|
998
|
-
print("Type /q to quit, /help or /? for help, /review to make clear summary, /patch to request a diff patch.")
|
|
999
|
-
|
|
1000
|
-
# Send the initial prompt using send_message_stream
|
|
1001
|
-
print("\n🤖 Gemini:")
|
|
1002
|
-
full_response_text = ""
|
|
1003
|
-
# Use send_message_stream for streaming responses
|
|
1004
|
-
response_stream = chat.send_message_stream(initial_prompt, config=config)
|
|
1005
|
-
for chunk in response_stream:
|
|
1006
|
-
print(chunk.text, end="", flush=True)
|
|
1007
|
-
full_response_text += chunk.text
|
|
1008
|
-
print("\n" + "-"*20)
|
|
1009
|
-
|
|
1010
|
-
while True:
|
|
1011
|
-
is_patch_request = False
|
|
1012
|
-
try:
|
|
1013
|
-
# Print the header on a separate line
|
|
1014
|
-
print("👤 You (Submit with Esc+Enter):")
|
|
1015
|
-
# Get input using prompt_toolkit with a minimal indicator
|
|
1016
|
-
user_input = prompt(">> ", multiline=True)
|
|
1017
|
-
# prompt_toolkit raises EOFError on Ctrl+D, so this handler remains correct.
|
|
1018
|
-
except EOFError:
|
|
1019
|
-
print("\nExiting...")
|
|
1020
|
-
break
|
|
1021
|
-
except KeyboardInterrupt: # Handle Ctrl+C
|
|
1022
|
-
print("\nExiting...")
|
|
1023
|
-
break
|
|
1024
|
-
|
|
1025
|
-
if user_input.lower() == '/q':
|
|
1026
|
-
break
|
|
1027
|
-
elif user_input.endswith('/patch'):
|
|
1028
|
-
is_patch_request = True
|
|
1029
|
-
# Extract message before /patch
|
|
1030
|
-
user_message = user_input[:-len('/patch')].strip()
|
|
1031
|
-
print(f"\n🛠️ Requesting patches... (Context: '{user_message}' if provided)")
|
|
1032
|
-
elif user_input.lower() == '/review':
|
|
1033
|
-
user_message = user_input = "Review and reflect on the solution. Summarize and write a minimal, complete set of changes needed for the solution. Do not use + and - style diff. Instead use comments to point where to place the code. Make it easy to copy and paste the solution."
|
|
1034
|
-
elif not user_input:
|
|
1035
|
-
continue # Ignore empty input
|
|
1036
|
-
else:
|
|
1037
|
-
user_message = user_input # Regular message
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
# --- Handle Patch Request ---
|
|
1041
|
-
if is_patch_request:
|
|
1042
|
-
print("🤖 Gemini: Thinking... (generating code changes)")
|
|
1043
|
-
# Include user message part if it exists
|
|
1044
|
-
patch_context = f"Based on our conversation and specifically: \"{user_message}\"\n\n" if user_message else "Based on our conversation,\n\n"
|
|
1045
|
-
|
|
1046
|
-
patch_request_prompt = (
|
|
1047
|
-
patch_context +
|
|
1048
|
-
"Generate the necessary code changes to fulfill the request. Provide the changes as a JSON list, where each item "
|
|
1049
|
-
"is an object with the following keys:\n"
|
|
1050
|
-
"- 'reasoning': Explain why this specific change is needed.\n"
|
|
1051
|
-
"- 'file_path': The relative path to the file to modify.\n"
|
|
1052
|
-
"- 'original_text': The exact, unique block of text to replace.\n"
|
|
1053
|
-
"- 'new_text': The text to replace original_text with. Do not include any temporary comments like '// CHANGE BEGINS' or '/* PATCH START */'.\n"
|
|
1054
|
-
"Ensure 'original_text' is unique within the specified 'file_path'. "
|
|
1055
|
-
"Respond ONLY with the JSON object conforming to this structure: { \"patches\": [ { patch_item_1 }, { patch_item_2 }, ... ] }"
|
|
1056
|
-
)
|
|
1057
|
-
|
|
1058
|
-
try:
|
|
1059
|
-
# Request the response using the new schema
|
|
1060
|
-
response = chat.send_message(
|
|
1061
|
-
patch_request_prompt,
|
|
1062
|
-
config=GenerateContentConfig(
|
|
1063
|
-
response_schema=SimplePatchArgs.model_json_schema(),
|
|
1064
|
-
response_mime_type='application/json',
|
|
1065
|
-
temperature=0.0
|
|
1066
|
-
)
|
|
1067
|
-
)
|
|
1068
|
-
|
|
1069
|
-
print("🤖 Gemini: Received potential patches.")
|
|
1070
|
-
try:
|
|
1071
|
-
# Validate and parse args using the Pydantic model
|
|
1072
|
-
# Explicitly validate the dictionary returned by response.parsed
|
|
1073
|
-
if isinstance(response.parsed, dict):
|
|
1074
|
-
patch_args = SimplePatchArgs.model_validate(response.parsed)
|
|
1075
|
-
else:
|
|
1076
|
-
# Handle unexpected type if response.parsed isn't a dict
|
|
1077
|
-
print(f"❌ Error: Expected a dictionary for patches, but got type {type(response.parsed)}")
|
|
1078
|
-
print(f" Content: {response.parsed}")
|
|
1079
|
-
continue # Skip further processing for this response
|
|
1080
|
-
|
|
1081
|
-
if not patch_args or not patch_args.patches:
|
|
1082
|
-
print("🤖 Gemini: No patches were proposed in the response.")
|
|
1083
|
-
print("-" * 20)
|
|
1084
|
-
continue
|
|
1085
|
-
|
|
1086
|
-
print("\nProposed Patches:")
|
|
1087
|
-
print("=" * 30)
|
|
1088
|
-
for i, patch_item in enumerate(patch_args.patches):
|
|
1089
|
-
print(f"Patch {i+1}/{len(patch_args.patches)}:")
|
|
1090
|
-
print(f" File: {patch_item.file_path}")
|
|
1091
|
-
print(f" Reasoning: {patch_item.reasoning}")
|
|
1092
|
-
# Optionally show snippets of original/new text for review
|
|
1093
|
-
print(f" Original (snippet): '{patch_item.original_text[:80].strip()}...'")
|
|
1094
|
-
print(f" New (snippet): '{patch_item.new_text[:80].strip()}...'")
|
|
1095
|
-
print("-" * 20)
|
|
1096
|
-
|
|
1097
|
-
confirm = input(f"Apply these {len(patch_args.patches)} patches? (y/N): ").lower()
|
|
1098
|
-
if confirm == 'y':
|
|
1099
|
-
applied_count = 0
|
|
1100
|
-
failed_count = 0
|
|
1101
|
-
for patch_item in patch_args.patches:
|
|
1102
|
-
# Call the new apply function for each patch
|
|
1103
|
-
success = apply_simple_patch(patch_item)
|
|
1104
|
-
if success:
|
|
1105
|
-
applied_count += 1
|
|
1106
|
-
else:
|
|
1107
|
-
failed_count += 1
|
|
1108
|
-
|
|
1109
|
-
print("\nPatch Application Summary:")
|
|
1110
|
-
if applied_count > 0:
|
|
1111
|
-
print(f"✅ Successfully applied {applied_count} patches.")
|
|
1112
|
-
if failed_count > 0:
|
|
1113
|
-
print(f"❌ Failed to apply {failed_count} patches.")
|
|
1114
|
-
if applied_count == 0 and failed_count == 0: # Should not happen if list wasn't empty
|
|
1115
|
-
print("⚪ No patches were applied.")
|
|
1116
|
-
print("=" * 30)
|
|
1117
|
-
else:
|
|
1118
|
-
print("🤖 Gemini: Patches not applied by user.")
|
|
1119
|
-
print("-" * 20)
|
|
1120
|
-
|
|
1121
|
-
except Exception as e: # Catch Pydantic validation errors or other issues
|
|
1122
|
-
print(f"❌ Error processing patch response: {e}")
|
|
1123
|
-
# Attempt to show the raw response text if parsing failed
|
|
1124
|
-
raw_text = ""
|
|
1125
|
-
try:
|
|
1126
|
-
if response.parts:
|
|
1127
|
-
raw_text = "".join(part.text for part in response.parts if hasattr(part, 'text'))
|
|
1128
|
-
elif hasattr(response, 'text'):
|
|
1129
|
-
raw_text = response.text
|
|
1130
|
-
except Exception:
|
|
1131
|
-
pass # Ignore errors getting raw text
|
|
1132
|
-
if raw_text:
|
|
1133
|
-
print(f" Received response text:\n{raw_text}")
|
|
1134
|
-
else:
|
|
1135
|
-
print(f" Received response content: {response}") # Fallback representation
|
|
1136
|
-
|
|
1137
|
-
except Exception as e:
|
|
1138
|
-
print(f"\n❌ An error occurred while requesting patches from Gemini: {e}")
|
|
1139
|
-
print(" Please check your connection, API key, and model permissions/capabilities.")
|
|
1140
|
-
print("-" * 20)
|
|
1141
|
-
|
|
1142
|
-
continue # Go to next loop iteration after handling /patch
|
|
1143
|
-
elif user_input.strip() in ['/help', '/?']:
|
|
1144
|
-
print("🤖 Gemini: Available commands:")
|
|
1145
|
-
print(" /q - Quit the chat session.")
|
|
1146
|
-
print(" /patch - Request a diff patch (not fully implemented yet).")
|
|
1147
|
-
print(" /review - Pre-fill input with a review/summary prompt template.")
|
|
1148
|
-
print(" /help or /? - Show this help message.")
|
|
1149
|
-
print("-" * 20)
|
|
1150
|
-
continue
|
|
1151
|
-
elif not user_input.strip(): # Ignore empty input
|
|
1152
|
-
continue
|
|
1153
|
-
|
|
1154
|
-
print("\n🤖 Gemini:")
|
|
1155
|
-
full_response_text = ""
|
|
1156
|
-
try:
|
|
1157
|
-
# Use send_message_stream for subsequent messages
|
|
1158
|
-
response_stream = chat.send_message_stream(user_input, config=config)
|
|
1159
|
-
for chunk in response_stream:
|
|
1160
|
-
print(chunk.text, end="", flush=True)
|
|
1161
|
-
full_response_text += chunk.text
|
|
1162
|
-
print("\n" + "-"*20)
|
|
1163
|
-
except Exception as e:
|
|
1164
|
-
print(f"\nAn unexpected error occurred: {e}")
|
|
1165
|
-
print("Try again or type 'exit'.")
|
|
1166
|
-
|
|
1167
|
-
except Exception as e:
|
|
1168
|
-
# Catch other potential errors
|
|
1169
|
-
print(f"\nAn error occurred setting up the chat session: {e}")
|
|
1170
|
-
|
|
1171
994
|
def main():
|
|
1172
995
|
parser = argparse.ArgumentParser(description="Generate a prompt with project structure, file contents, and web content.")
|
|
1173
996
|
parser.add_argument('inputs', nargs='+', help='Files, directories, or URLs to include in the prompt')
|
|
1174
997
|
parser.add_argument('-t', '--task', help='Task description for the AI prompt')
|
|
1175
|
-
parser.add_argument('-I', '--interactive', action='store_true', help='Start an interactive chat session after generating the prompt.')
|
|
1176
998
|
args = parser.parse_args()
|
|
1177
999
|
|
|
1178
1000
|
ignore_patterns = read_gitignore()
|
|
1179
1001
|
env_vars = read_env_file()
|
|
1180
1002
|
project_root_abs = os.path.abspath(os.getcwd())
|
|
1181
1003
|
|
|
1182
|
-
|
|
1183
1004
|
files_to_include: List[FileTuple] = []
|
|
1184
|
-
processed_dirs = set()
|
|
1185
1005
|
web_contents: Dict[str, Tuple[FileTuple, str]] = {}
|
|
1186
1006
|
current_char_count = 0
|
|
1187
|
-
|
|
1007
|
+
|
|
1008
|
+
# Separate URLs from file/directory paths
|
|
1009
|
+
paths_for_tree = []
|
|
1010
|
+
|
|
1188
1011
|
for input_path in args.inputs:
|
|
1189
1012
|
if input_path.startswith(('http://', 'https://')):
|
|
1013
|
+
# Handle web content as before
|
|
1190
1014
|
result = fetch_web_content(input_path)
|
|
1191
1015
|
if result:
|
|
1192
1016
|
file_tuple, full_content, snippet = result
|
|
@@ -1220,133 +1044,68 @@ def main():
|
|
|
1220
1044
|
current_char_count += len(content)
|
|
1221
1045
|
print(f"Added {'snippet of ' if is_snippet else ''}web content from: {input_path}")
|
|
1222
1046
|
print_char_count(current_char_count)
|
|
1223
|
-
elif os.path.isfile(input_path):
|
|
1224
|
-
abs_input_path = os.path.abspath(input_path)
|
|
1225
|
-
if not is_ignored(abs_input_path, ignore_patterns) and not is_binary(abs_input_path):
|
|
1226
|
-
file_size = os.path.getsize(abs_input_path)
|
|
1227
|
-
file_size_readable = get_human_readable_size(file_size)
|
|
1228
|
-
file_char_estimate = file_size
|
|
1229
|
-
language = get_language_for_file(abs_input_path)
|
|
1230
|
-
file_to_add = None
|
|
1231
|
-
|
|
1232
|
-
if is_large_file(abs_input_path):
|
|
1233
|
-
print(f"\nFile {get_relative_path(abs_input_path)} ({file_size_readable}, ~{file_char_estimate} chars) is large.")
|
|
1234
|
-
print("Preview (first ~50 lines or 4KB):")
|
|
1235
|
-
print(get_colored_file_snippet(abs_input_path))
|
|
1236
|
-
print("-" * 40)
|
|
1237
|
-
while True:
|
|
1238
|
-
print_char_count(current_char_count)
|
|
1239
|
-
choice = input(f"How to include large file {get_relative_path(abs_input_path)}? (f)ull / (s)nippet / (p)atches / (n)o skip: ").lower()
|
|
1240
|
-
if choice == 'f':
|
|
1241
|
-
file_to_add = (abs_input_path, False, None, language)
|
|
1242
|
-
current_char_count += file_char_estimate
|
|
1243
|
-
print(f"Added full file: {get_relative_path(abs_input_path)}")
|
|
1244
|
-
break
|
|
1245
|
-
elif choice == 's':
|
|
1246
|
-
snippet_content = get_file_snippet(abs_input_path)
|
|
1247
|
-
file_to_add = (abs_input_path, True, None, language)
|
|
1248
|
-
current_char_count += len(snippet_content)
|
|
1249
|
-
print(f"Added snippet of file: {get_relative_path(abs_input_path)}")
|
|
1250
|
-
break
|
|
1251
|
-
elif choice == 'p':
|
|
1252
|
-
chunks, char_count = select_file_patches(abs_input_path)
|
|
1253
|
-
if chunks:
|
|
1254
|
-
file_to_add = (abs_input_path, False, chunks, language)
|
|
1255
|
-
current_char_count += char_count
|
|
1256
|
-
print(f"Added selected patches from file: {get_relative_path(abs_input_path)}")
|
|
1257
|
-
else:
|
|
1258
|
-
print(f"No patches selected for {get_relative_path(abs_input_path)}. Skipping file.")
|
|
1259
|
-
break
|
|
1260
|
-
elif choice == 'n':
|
|
1261
|
-
print(f"Skipped large file: {get_relative_path(abs_input_path)}")
|
|
1262
|
-
break
|
|
1263
|
-
else:
|
|
1264
|
-
print("Invalid choice. Please enter 'f', 's', 'p', or 'n'.")
|
|
1265
|
-
else:
|
|
1266
|
-
file_to_add = (abs_input_path, False, None, language)
|
|
1267
|
-
current_char_count += file_char_estimate
|
|
1268
|
-
print(f"Added file: {get_relative_path(abs_input_path)} ({file_size_readable})")
|
|
1269
|
-
|
|
1270
|
-
if file_to_add:
|
|
1271
|
-
files_to_include.append(file_to_add)
|
|
1272
|
-
# --- NEW: Call dependency analysis ---
|
|
1273
|
-
new_deps, deps_char_count = _propose_and_add_dependencies(abs_input_path, project_root_abs, files_to_include, current_char_count)
|
|
1274
|
-
files_to_include.extend(new_deps)
|
|
1275
|
-
current_char_count += deps_char_count
|
|
1276
|
-
|
|
1277
|
-
print_char_count(current_char_count)
|
|
1278
|
-
|
|
1279
|
-
else:
|
|
1280
|
-
if is_ignored(input_path, ignore_patterns):
|
|
1281
|
-
print(f"Ignoring file based on ignore patterns: {input_path}")
|
|
1282
|
-
elif is_binary(input_path):
|
|
1283
|
-
print(f"Ignoring binary file: {input_path}")
|
|
1284
|
-
else:
|
|
1285
|
-
print(f"Ignoring file: {input_path}")
|
|
1286
|
-
elif os.path.isdir(input_path):
|
|
1287
|
-
print(f"\nProcessing directory specified directly: {input_path}")
|
|
1288
|
-
# Pass project_root_abs to process_directory
|
|
1289
|
-
dir_files, dir_processed, current_char_count = process_directory(input_path, ignore_patterns, project_root_abs, current_char_count)
|
|
1290
|
-
files_to_include.extend(dir_files)
|
|
1291
|
-
processed_dirs.update(dir_processed)
|
|
1292
1047
|
else:
|
|
1293
|
-
|
|
1048
|
+
# Add to paths for tree selector
|
|
1049
|
+
if os.path.exists(input_path):
|
|
1050
|
+
paths_for_tree.append(input_path)
|
|
1051
|
+
else:
|
|
1052
|
+
print(f"Warning: {input_path} does not exist. Skipping.")
|
|
1053
|
+
|
|
1054
|
+
# Use tree selector for file/directory selection
|
|
1055
|
+
if paths_for_tree:
|
|
1056
|
+
print("\nStarting interactive file selection...")
|
|
1057
|
+
print("Use arrow keys to navigate, Space to select, 'q' to finish. Press 'h' for help.\n")
|
|
1058
|
+
|
|
1059
|
+
tree_selector = TreeSelector(ignore_patterns, project_root_abs)
|
|
1060
|
+
try:
|
|
1061
|
+
selected_files, file_char_count = tree_selector.run(paths_for_tree)
|
|
1062
|
+
files_to_include.extend(selected_files)
|
|
1063
|
+
current_char_count += file_char_count
|
|
1064
|
+
except KeyboardInterrupt:
|
|
1065
|
+
print("\nSelection cancelled.")
|
|
1066
|
+
return
|
|
1294
1067
|
|
|
1295
1068
|
if not files_to_include and not web_contents:
|
|
1296
1069
|
print("No files or web content were selected. Exiting.")
|
|
1297
1070
|
return
|
|
1298
1071
|
|
|
1299
1072
|
print("\nFile and web content selection complete.")
|
|
1300
|
-
print_char_count(current_char_count)
|
|
1073
|
+
print_char_count(current_char_count)
|
|
1301
1074
|
|
|
1302
1075
|
added_files_count = len(files_to_include)
|
|
1303
|
-
added_dirs_count = len(processed_dirs)
|
|
1304
1076
|
added_web_count = len(web_contents)
|
|
1305
|
-
print(f"Summary: Added {added_files_count} files/patches
|
|
1077
|
+
print(f"Summary: Added {added_files_count} files/patches and {added_web_count} web sources.")
|
|
1306
1078
|
|
|
1307
1079
|
prompt_template, cursor_position = generate_prompt_template(files_to_include, ignore_patterns, web_contents, env_vars)
|
|
1308
1080
|
|
|
1309
|
-
if args.
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1081
|
+
if args.task:
|
|
1082
|
+
task_description = args.task
|
|
1083
|
+
task_marker = "## Task Instructions\n\n"
|
|
1084
|
+
insertion_point = prompt_template.find(task_marker)
|
|
1085
|
+
if insertion_point != -1:
|
|
1086
|
+
final_prompt = prompt_template[:insertion_point + len(task_marker)] + task_description + "\n\n" + prompt_template[insertion_point + len(task_marker):]
|
|
1314
1087
|
else:
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
initial_chat_prompt = open_editor_for_input(editor_initial_content, cursor_position)
|
|
1319
|
-
print("Editor closed. Starting interactive chat session...")
|
|
1320
|
-
start_chat_session(initial_chat_prompt)
|
|
1088
|
+
final_prompt = prompt_template[:cursor_position] + task_description + prompt_template[cursor_position:]
|
|
1089
|
+
print("\nUsing task description from -t argument.")
|
|
1321
1090
|
else:
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
task_marker = "## Task Instructions\n\n"
|
|
1325
|
-
insertion_point = prompt_template.find(task_marker)
|
|
1326
|
-
if insertion_point != -1:
|
|
1327
|
-
final_prompt = prompt_template[:insertion_point + len(task_marker)] + task_description + "\n\n" + prompt_template[insertion_point + len(task_marker):]
|
|
1328
|
-
else:
|
|
1329
|
-
final_prompt = prompt_template[:cursor_position] + task_description + prompt_template[cursor_position:]
|
|
1330
|
-
print("\nUsing task description from -t argument.")
|
|
1331
|
-
else:
|
|
1332
|
-
print("\nOpening editor for task instructions...")
|
|
1333
|
-
final_prompt = open_editor_for_input(prompt_template, cursor_position)
|
|
1091
|
+
print("\nOpening editor for task instructions...")
|
|
1092
|
+
final_prompt = open_editor_for_input(prompt_template, cursor_position)
|
|
1334
1093
|
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1094
|
+
print("\n\nGenerated prompt:")
|
|
1095
|
+
print("-" * 80)
|
|
1096
|
+
print(final_prompt)
|
|
1097
|
+
print("-" * 80)
|
|
1339
1098
|
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1099
|
+
try:
|
|
1100
|
+
pyperclip.copy(final_prompt)
|
|
1101
|
+
separator = "\n" + "=" * 40 + "\n☕🍝 Kopipasta Complete! 🍝☕\n" + "=" * 40 + "\n"
|
|
1102
|
+
print(separator)
|
|
1103
|
+
final_char_count = len(final_prompt)
|
|
1104
|
+
final_token_estimate = final_char_count // 4
|
|
1105
|
+
print(f"Prompt has been copied to clipboard. Final size: {final_char_count} characters (~ {final_token_estimate} tokens)")
|
|
1106
|
+
except pyperclip.PyperclipException as e:
|
|
1107
|
+
print(f"\nWarning: Failed to copy to clipboard: {e}")
|
|
1108
|
+
print("You can manually copy the prompt above.")
|
|
1350
1109
|
|
|
1351
1110
|
if __name__ == "__main__":
|
|
1352
1111
|
main()
|