kopipasta 0.30.0__tar.gz → 0.31.0__tar.gz
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-0.30.0/kopipasta.egg-info → kopipasta-0.31.0}/PKG-INFO +1 -1
- kopipasta-0.31.0/kopipasta/cache.py +37 -0
- {kopipasta-0.30.0 → kopipasta-0.31.0}/kopipasta/main.py +11 -2
- {kopipasta-0.30.0 → kopipasta-0.31.0}/kopipasta/tree_selector.py +94 -12
- {kopipasta-0.30.0 → kopipasta-0.31.0/kopipasta.egg-info}/PKG-INFO +1 -1
- {kopipasta-0.30.0 → kopipasta-0.31.0}/kopipasta.egg-info/SOURCES.txt +1 -0
- {kopipasta-0.30.0 → kopipasta-0.31.0}/setup.py +1 -1
- {kopipasta-0.30.0 → kopipasta-0.31.0}/LICENSE +0 -0
- {kopipasta-0.30.0 → kopipasta-0.31.0}/MANIFEST.in +0 -0
- {kopipasta-0.30.0 → kopipasta-0.31.0}/README.md +0 -0
- {kopipasta-0.30.0 → kopipasta-0.31.0}/kopipasta/__init__.py +0 -0
- {kopipasta-0.30.0 → kopipasta-0.31.0}/kopipasta/file.py +0 -0
- {kopipasta-0.30.0 → kopipasta-0.31.0}/kopipasta/import_parser.py +0 -0
- {kopipasta-0.30.0 → kopipasta-0.31.0}/kopipasta/prompt.py +0 -0
- {kopipasta-0.30.0 → kopipasta-0.31.0}/kopipasta.egg-info/dependency_links.txt +0 -0
- {kopipasta-0.30.0 → kopipasta-0.31.0}/kopipasta.egg-info/entry_points.txt +0 -0
- {kopipasta-0.30.0 → kopipasta-0.31.0}/kopipasta.egg-info/requires.txt +0 -0
- {kopipasta-0.30.0 → kopipasta-0.31.0}/kopipasta.egg-info/top_level.txt +0 -0
- {kopipasta-0.30.0 → kopipasta-0.31.0}/requirements.txt +0 -0
- {kopipasta-0.30.0 → kopipasta-0.31.0}/setup.cfg +0 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import List, Tuple
|
|
5
|
+
|
|
6
|
+
# Define FileTuple for type hinting
|
|
7
|
+
FileTuple = Tuple[str, bool, List[str] | None, str]
|
|
8
|
+
|
|
9
|
+
def get_cache_file_path() -> Path:
|
|
10
|
+
"""Gets the cross-platform path to the cache file for the last selection."""
|
|
11
|
+
cache_dir = Path.home() / ".cache" / "kopipasta"
|
|
12
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
13
|
+
return cache_dir / "last_selection.json"
|
|
14
|
+
|
|
15
|
+
def save_selection_to_cache(files_to_include: List[FileTuple]):
|
|
16
|
+
"""Saves the list of selected file relative paths to the cache."""
|
|
17
|
+
cache_file = get_cache_file_path()
|
|
18
|
+
relative_paths = sorted([os.path.relpath(f[0]) for f in files_to_include])
|
|
19
|
+
try:
|
|
20
|
+
with open(cache_file, 'w', encoding='utf-8') as f:
|
|
21
|
+
json.dump(relative_paths, f, indent=2)
|
|
22
|
+
except IOError as e:
|
|
23
|
+
print(f"\nWarning: Could not save selection to cache: {e}")
|
|
24
|
+
|
|
25
|
+
def load_selection_from_cache() -> List[str]:
|
|
26
|
+
"""Loads the list of selected files from the cache file."""
|
|
27
|
+
cache_file = get_cache_file_path()
|
|
28
|
+
if not cache_file.exists():
|
|
29
|
+
return []
|
|
30
|
+
try:
|
|
31
|
+
with open(cache_file, 'r', encoding='utf-8') as f:
|
|
32
|
+
paths = json.load(f)
|
|
33
|
+
# Filter out paths that no longer exist
|
|
34
|
+
return [p for p in paths if os.path.exists(p)]
|
|
35
|
+
except (IOError, json.JSONDecodeError) as e:
|
|
36
|
+
print(f"\nWarning: Could not load previous selection from cache: {e}")
|
|
37
|
+
return []
|
|
@@ -18,6 +18,7 @@ from kopipasta.file import FileTuple, get_human_readable_size, is_binary, is_ign
|
|
|
18
18
|
import kopipasta.import_parser as import_parser
|
|
19
19
|
from kopipasta.tree_selector import TreeSelector
|
|
20
20
|
from kopipasta.prompt import generate_prompt_template, get_file_snippet, get_language_for_file
|
|
21
|
+
from kopipasta.cache import save_selection_to_cache
|
|
21
22
|
|
|
22
23
|
def _propose_and_add_dependencies(
|
|
23
24
|
file_just_added: str,
|
|
@@ -993,10 +994,14 @@ def open_editor_for_input(template: str, cursor_position: int) -> str:
|
|
|
993
994
|
|
|
994
995
|
def main():
|
|
995
996
|
parser = argparse.ArgumentParser(description="Generate a prompt with project structure, file contents, and web content.")
|
|
996
|
-
parser.add_argument('inputs', nargs='
|
|
997
|
+
parser.add_argument('inputs', nargs='*', help='Files, directories, or URLs to include. Defaults to current directory.')
|
|
997
998
|
parser.add_argument('-t', '--task', help='Task description for the AI prompt')
|
|
998
999
|
args = parser.parse_args()
|
|
999
1000
|
|
|
1001
|
+
# Default to the current directory if no inputs are provided
|
|
1002
|
+
if not args.inputs:
|
|
1003
|
+
args.inputs.append('.')
|
|
1004
|
+
|
|
1000
1005
|
ignore_patterns = read_gitignore()
|
|
1001
1006
|
env_vars = read_env_file()
|
|
1002
1007
|
project_root_abs = os.path.abspath(os.getcwd())
|
|
@@ -1054,7 +1059,7 @@ def main():
|
|
|
1054
1059
|
# Use tree selector for file/directory selection
|
|
1055
1060
|
if paths_for_tree:
|
|
1056
1061
|
print("\nStarting interactive file selection...")
|
|
1057
|
-
print("Use arrow keys to navigate, Space to select, 'q' to finish.
|
|
1062
|
+
print("Use arrow keys to navigate, Space to select, 'q' to finish. See all keys below.\n")
|
|
1058
1063
|
|
|
1059
1064
|
tree_selector = TreeSelector(ignore_patterns, project_root_abs)
|
|
1060
1065
|
try:
|
|
@@ -1069,6 +1074,10 @@ def main():
|
|
|
1069
1074
|
print("No files or web content were selected. Exiting.")
|
|
1070
1075
|
return
|
|
1071
1076
|
|
|
1077
|
+
# Save the final selection for the next run
|
|
1078
|
+
if files_to_include:
|
|
1079
|
+
save_selection_to_cache(files_to_include)
|
|
1080
|
+
|
|
1072
1081
|
print("\nFile and web content selection complete.")
|
|
1073
1082
|
print_char_count(current_char_count)
|
|
1074
1083
|
|
|
@@ -9,6 +9,7 @@ import click
|
|
|
9
9
|
|
|
10
10
|
from kopipasta.file import FileTuple, is_binary, is_ignored, get_human_readable_size
|
|
11
11
|
from kopipasta.prompt import get_file_snippet, get_language_for_file
|
|
12
|
+
from kopipasta.cache import load_selection_from_cache
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
class FileNode:
|
|
@@ -253,15 +254,16 @@ class TreeSelector:
|
|
|
253
254
|
def _show_help(self) -> Panel:
|
|
254
255
|
"""Create help panel"""
|
|
255
256
|
help_text = """[bold]Navigation:[/bold]
|
|
256
|
-
↑/k: Up ↓/j: Down →/l/Enter: Expand ←/h: Collapse
|
|
257
|
+
↑/k: Up ↓/j: Down →/l/Enter: Expand ←/h: Collapse
|
|
257
258
|
|
|
258
259
|
[bold]Selection:[/bold]
|
|
259
260
|
Space: Toggle file/dir a: Add all in dir s: Snippet mode
|
|
260
261
|
|
|
261
262
|
[bold]Actions:[/bold]
|
|
262
|
-
g: Grep in directory
|
|
263
|
+
r: Reuse last selection g: Grep in directory d: Show dependencies
|
|
264
|
+
q: Quit and finalize"""
|
|
263
265
|
|
|
264
|
-
return Panel(help_text, title="
|
|
266
|
+
return Panel(help_text, title="Keyboard Controls", border_style="dim", expand=False)
|
|
265
267
|
|
|
266
268
|
def _get_status_bar(self) -> str:
|
|
267
269
|
"""Create status bar with selection info"""
|
|
@@ -352,7 +354,7 @@ g: Grep in directory d: Show dependencies q: Quit selection"""
|
|
|
352
354
|
self.char_count += node.size
|
|
353
355
|
|
|
354
356
|
def _toggle_directory(self, node: FileNode):
|
|
355
|
-
"""Toggle all files in a directory"""
|
|
357
|
+
"""Toggle all files in a directory, now fully recursive."""
|
|
356
358
|
if not node.is_dir:
|
|
357
359
|
return
|
|
358
360
|
|
|
@@ -365,6 +367,9 @@ g: Grep in directory d: Show dependencies q: Quit selection"""
|
|
|
365
367
|
|
|
366
368
|
def collect_files(n: FileNode):
|
|
367
369
|
if n.is_dir:
|
|
370
|
+
# CRITICAL FIX: Ensure sub-directory children are loaded before recursing
|
|
371
|
+
if not n.children:
|
|
372
|
+
self._scan_directory(n.path, n)
|
|
368
373
|
for child in n.children:
|
|
369
374
|
collect_files(child)
|
|
370
375
|
else:
|
|
@@ -376,16 +381,92 @@ g: Grep in directory d: Show dependencies q: Quit selection"""
|
|
|
376
381
|
any_unselected = any(os.path.abspath(f.path) not in self.selected_files for f in all_files)
|
|
377
382
|
|
|
378
383
|
if any_unselected:
|
|
379
|
-
# Select all unselected
|
|
384
|
+
# Select all unselected files
|
|
380
385
|
for file_node in all_files:
|
|
381
|
-
|
|
382
|
-
|
|
386
|
+
abs_path = os.path.abspath(file_node.path)
|
|
387
|
+
if abs_path not in self.selected_files:
|
|
388
|
+
self.selected_files[abs_path] = (False, None)
|
|
389
|
+
self.char_count += file_node.size
|
|
383
390
|
else:
|
|
384
|
-
# Unselect all
|
|
391
|
+
# Unselect all files
|
|
385
392
|
for file_node in all_files:
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
393
|
+
abs_path = os.path.abspath(file_node.path)
|
|
394
|
+
if abs_path in self.selected_files:
|
|
395
|
+
is_snippet, _ = self.selected_files[abs_path]
|
|
396
|
+
del self.selected_files[abs_path]
|
|
397
|
+
if is_snippet:
|
|
398
|
+
self.char_count -= len(get_file_snippet(file_node.path))
|
|
399
|
+
else:
|
|
400
|
+
self.char_count -= file_node.size
|
|
401
|
+
|
|
402
|
+
def _propose_and_apply_last_selection(self):
|
|
403
|
+
"""Loads paths from cache, shows a confirmation dialog, and applies the selection if confirmed."""
|
|
404
|
+
cached_paths = load_selection_from_cache()
|
|
405
|
+
|
|
406
|
+
if not cached_paths:
|
|
407
|
+
self.console.print(Panel("[yellow]No cached selection found to reuse.[/yellow]", title="Info", border_style="dim"))
|
|
408
|
+
click.pause("Press any key to continue...")
|
|
409
|
+
return
|
|
410
|
+
|
|
411
|
+
# Categorize cached paths for the preview
|
|
412
|
+
files_to_add = []
|
|
413
|
+
files_already_selected = []
|
|
414
|
+
files_not_found = []
|
|
415
|
+
|
|
416
|
+
for rel_path in cached_paths:
|
|
417
|
+
abs_path = os.path.abspath(rel_path)
|
|
418
|
+
if not os.path.isfile(abs_path):
|
|
419
|
+
files_not_found.append(rel_path)
|
|
420
|
+
continue
|
|
421
|
+
|
|
422
|
+
if abs_path in self.selected_files:
|
|
423
|
+
files_already_selected.append(rel_path)
|
|
424
|
+
else:
|
|
425
|
+
files_to_add.append(rel_path)
|
|
426
|
+
|
|
427
|
+
# Build the rich text for the confirmation panel
|
|
428
|
+
preview_text = Text()
|
|
429
|
+
if files_to_add:
|
|
430
|
+
preview_text.append("The following files will be ADDED:\n", style="bold")
|
|
431
|
+
for path in sorted(files_to_add):
|
|
432
|
+
preview_text.append(" ")
|
|
433
|
+
preview_text.append("+", style="cyan")
|
|
434
|
+
preview_text.append(f" {path}\n")
|
|
435
|
+
|
|
436
|
+
if files_already_selected:
|
|
437
|
+
preview_text.append("\nAlready selected (no change):\n", style="bold dim")
|
|
438
|
+
for path in sorted(files_already_selected):
|
|
439
|
+
preview_text.append(f" ✓ {path}\n")
|
|
440
|
+
|
|
441
|
+
if files_not_found:
|
|
442
|
+
preview_text.append("\nNot found on disk (will be skipped):\n", style="bold dim")
|
|
443
|
+
for path in sorted(files_not_found):
|
|
444
|
+
preview_text.append(" ")
|
|
445
|
+
preview_text.append("-", style="red")
|
|
446
|
+
preview_text.append(f" {path}\n")
|
|
447
|
+
|
|
448
|
+
# Display the confirmation panel and prompt
|
|
449
|
+
self.console.clear()
|
|
450
|
+
self.console.print(Panel(preview_text, title="[bold cyan]Reuse Last Selection?", border_style="cyan", padding=(1, 2)))
|
|
451
|
+
|
|
452
|
+
if not files_to_add:
|
|
453
|
+
self.console.print("\n[yellow]No new files to add from the last selection.[/yellow]")
|
|
454
|
+
click.pause("Press any key to continue...")
|
|
455
|
+
return
|
|
456
|
+
|
|
457
|
+
# Use click.confirm for a simple and effective y/n prompt
|
|
458
|
+
if not click.confirm(f"\nAdd {len(files_to_add)} file(s) to your current selection?", default=True):
|
|
459
|
+
return
|
|
460
|
+
|
|
461
|
+
# If confirmed, apply the changes
|
|
462
|
+
for rel_path in files_to_add:
|
|
463
|
+
abs_path = os.path.abspath(rel_path)
|
|
464
|
+
if os.path.isfile(abs_path) and abs_path not in self.selected_files:
|
|
465
|
+
file_size = os.path.getsize(abs_path)
|
|
466
|
+
self.selected_files[abs_path] = (False, None)
|
|
467
|
+
self.char_count += file_size
|
|
468
|
+
self._ensure_path_visible(abs_path)
|
|
469
|
+
|
|
389
470
|
def _ensure_path_visible(self, file_path: str):
|
|
390
471
|
"""Ensure a file path is visible in the tree by expanding parent directories"""
|
|
391
472
|
abs_file_path = os.path.abspath(file_path)
|
|
@@ -529,10 +610,11 @@ g: Grep in directory d: Show dependencies q: Quit selection"""
|
|
|
529
610
|
self._toggle_directory(current_node)
|
|
530
611
|
|
|
531
612
|
# Handle actions
|
|
613
|
+
elif key == 'r': # Reuse last selection
|
|
614
|
+
self._propose_and_apply_last_selection()
|
|
532
615
|
elif key == 'g': # Grep
|
|
533
616
|
self.console.print() # Add some space
|
|
534
617
|
self._handle_grep(current_node)
|
|
535
|
-
click.pause("Press any key to continue...")
|
|
536
618
|
elif key == 'd': # Dependencies
|
|
537
619
|
self.console.print() # Add some space
|
|
538
620
|
self._show_dependencies(current_node)
|
|
@@ -10,7 +10,7 @@ with open("requirements.txt", "r", encoding="utf-8") as f:
|
|
|
10
10
|
|
|
11
11
|
setup(
|
|
12
12
|
name="kopipasta",
|
|
13
|
-
version="0.
|
|
13
|
+
version="0.31.0",
|
|
14
14
|
author="Mikko Korpela",
|
|
15
15
|
author_email="mikko.korpela@gmail.com",
|
|
16
16
|
description="A CLI tool to generate prompts with project structure and file contents",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|