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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: kopipasta
3
- Version: 0.30.0
3
+ Version: 0.31.0
4
4
  Summary: A CLI tool to generate prompts with project structure and file contents
5
5
  Home-page: https://github.com/mkorpela/kopipasta
6
6
  Author: Mikko Korpela
@@ -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='+', help='Files, directories, or URLs to include in the prompt')
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. Press 'h' for help.\n")
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 PgUp/PgDn: Page G/End: Bottom
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 d: Show dependencies q: Quit selection"""
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="Keys", border_style="dim", expand=False)
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
- if file_node.path not in self.selected_files:
382
- self._toggle_selection(file_node)
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
- if file_node.path in self.selected_files:
387
- self._toggle_selection(file_node)
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: kopipasta
3
- Version: 0.30.0
3
+ Version: 0.31.0
4
4
  Summary: A CLI tool to generate prompts with project structure and file contents
5
5
  Home-page: https://github.com/mkorpela/kopipasta
6
6
  Author: Mikko Korpela
@@ -4,6 +4,7 @@ README.md
4
4
  requirements.txt
5
5
  setup.py
6
6
  kopipasta/__init__.py
7
+ kopipasta/cache.py
7
8
  kopipasta/file.py
8
9
  kopipasta/import_parser.py
9
10
  kopipasta/main.py
@@ -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.30.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