janito 0.6.0__py3-none-any.whl → 0.8.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.
Files changed (116) hide show
  1. janito/__main__.py +127 -134
  2. janito/agents/__init__.py +22 -16
  3. janito/agents/agent.py +24 -20
  4. janito/agents/claudeai.py +41 -55
  5. janito/agents/deepseekai.py +47 -0
  6. janito/change/applied_blocks.py +34 -0
  7. janito/change/applier.py +167 -0
  8. janito/change/edit_blocks.py +148 -0
  9. janito/change/finder.py +72 -0
  10. janito/change/request.py +144 -0
  11. janito/change/validator.py +87 -251
  12. janito/change/view/content.py +63 -0
  13. janito/change/{viewer → view}/diff.py +44 -43
  14. janito/change/view/panels.py +201 -0
  15. janito/change/view/sections.py +69 -0
  16. janito/change/view/styling.py +140 -0
  17. janito/change/view/summary.py +37 -0
  18. janito/change/{viewer → view}/themes.py +62 -55
  19. janito/change/view/viewer.py +59 -0
  20. janito/cli/__init__.py +1 -1
  21. janito/cli/commands.py +68 -45
  22. janito/cli/functions.py +66 -111
  23. janito/common.py +132 -53
  24. janito/config.py +99 -101
  25. janito/data/change_prompt.txt +81 -0
  26. janito/data/system_prompt.txt +3 -0
  27. janito/qa.py +56 -66
  28. janito/version.py +22 -22
  29. janito/workspace/__init__.py +8 -7
  30. janito/workspace/analysis.py +120 -120
  31. janito/workspace/models.py +97 -0
  32. janito/workspace/show.py +115 -0
  33. janito/workspace/stats.py +42 -0
  34. janito/workspace/workset.py +135 -0
  35. janito/workspace/workspace.py +335 -0
  36. janito-0.8.0.dist-info/METADATA +106 -0
  37. janito-0.8.0.dist-info/RECORD +40 -0
  38. {janito-0.6.0.dist-info → janito-0.8.0.dist-info}/licenses/LICENSE +20 -20
  39. janito/__init__.py +0 -2
  40. janito/agents/openai.py +0 -53
  41. janito/agents/test.py +0 -34
  42. janito/change/__init__.py +0 -32
  43. janito/change/__main__.py +0 -0
  44. janito/change/analysis/__init__.py +0 -23
  45. janito/change/analysis/__main__.py +0 -7
  46. janito/change/analysis/analyze.py +0 -61
  47. janito/change/analysis/formatting.py +0 -78
  48. janito/change/analysis/options.py +0 -81
  49. janito/change/analysis/prompts.py +0 -98
  50. janito/change/analysis/view/__init__.py +0 -9
  51. janito/change/analysis/view/terminal.py +0 -171
  52. janito/change/applier/__init__.py +0 -5
  53. janito/change/applier/file.py +0 -58
  54. janito/change/applier/main.py +0 -156
  55. janito/change/applier/text.py +0 -245
  56. janito/change/applier/workspace_dir.py +0 -58
  57. janito/change/core.py +0 -131
  58. janito/change/history.py +0 -44
  59. janito/change/operations.py +0 -7
  60. janito/change/parser.py +0 -289
  61. janito/change/play.py +0 -54
  62. janito/change/preview.py +0 -82
  63. janito/change/prompts.py +0 -126
  64. janito/change/test.py +0 -0
  65. janito/change/viewer/__init__.py +0 -11
  66. janito/change/viewer/content.py +0 -66
  67. janito/change/viewer/pager.py +0 -56
  68. janito/change/viewer/panels.py +0 -555
  69. janito/change/viewer/styling.py +0 -103
  70. janito/clear_statement_parser/clear_statement_format.txt +0 -328
  71. janito/clear_statement_parser/examples.txt +0 -326
  72. janito/clear_statement_parser/models.py +0 -104
  73. janito/clear_statement_parser/parser.py +0 -496
  74. janito/cli/base.py +0 -30
  75. janito/cli/handlers/ask.py +0 -22
  76. janito/cli/handlers/demo.py +0 -22
  77. janito/cli/handlers/request.py +0 -24
  78. janito/cli/handlers/scan.py +0 -9
  79. janito/cli/history.py +0 -61
  80. janito/cli/registry.py +0 -26
  81. janito/demo/__init__.py +0 -4
  82. janito/demo/data.py +0 -13
  83. janito/demo/mock_data.py +0 -20
  84. janito/demo/operations.py +0 -45
  85. janito/demo/runner.py +0 -59
  86. janito/demo/scenarios.py +0 -32
  87. janito/prompts.py +0 -2
  88. janito/review.py +0 -13
  89. janito/search_replace/README.md +0 -146
  90. janito/search_replace/__init__.py +0 -6
  91. janito/search_replace/__main__.py +0 -21
  92. janito/search_replace/core.py +0 -119
  93. janito/search_replace/parser.py +0 -52
  94. janito/search_replace/play.py +0 -61
  95. janito/search_replace/replacer.py +0 -36
  96. janito/search_replace/searcher.py +0 -299
  97. janito/shell/__init__.py +0 -39
  98. janito/shell/bus.py +0 -31
  99. janito/shell/commands.py +0 -195
  100. janito/shell/handlers.py +0 -122
  101. janito/shell/history.py +0 -20
  102. janito/shell/processor.py +0 -52
  103. janito/tui/__init__.py +0 -21
  104. janito/tui/base.py +0 -22
  105. janito/tui/flows/__init__.py +0 -5
  106. janito/tui/flows/changes.py +0 -65
  107. janito/tui/flows/content.py +0 -128
  108. janito/tui/flows/selection.py +0 -117
  109. janito/tui/screens/__init__.py +0 -3
  110. janito/tui/screens/app.py +0 -1
  111. janito/workspace/manager.py +0 -48
  112. janito/workspace/scan.py +0 -232
  113. janito-0.6.0.dist-info/METADATA +0 -185
  114. janito-0.6.0.dist-info/RECORD +0 -95
  115. {janito-0.6.0.dist-info → janito-0.8.0.dist-info}/WHEEL +0 -0
  116. {janito-0.6.0.dist-info → janito-0.8.0.dist-info}/entry_points.txt +0 -0
@@ -1,121 +1,121 @@
1
- from collections import defaultdict
2
- from pathlib import Path
3
- from typing import Dict, List
4
-
5
- from rich.columns import Columns
6
- from rich.console import Console, Group
7
- from rich.panel import Panel
8
- from rich.rule import Rule
9
- from janito.config import config
10
-
11
- def analyze_workspace_content(content: str) -> None:
12
- """Show statistics about the scanned content"""
13
- if not content:
14
- return
15
-
16
- # Collect include paths
17
- paths = []
18
- if config.include:
19
- for path in config.include:
20
- is_recursive = path in config.recursive
21
- path_str = str(path.relative_to(config.workspace_dir))
22
- paths.append(f"{path_str}/*" if is_recursive else f"{path_str}/")
23
- else:
24
- # Use workspace_dir as fallback when no include paths specified
25
- paths.append("./")
26
-
27
- console = Console()
28
-
29
- dir_counts: Dict[str, int] = defaultdict(int)
30
- dir_sizes: Dict[str, int] = defaultdict(int)
31
- file_types: Dict[str, int] = defaultdict(int)
32
- current_path = None
33
- current_content = []
34
-
35
- for line in content.split('\n'):
36
- if line.startswith('<path>'):
37
- path = Path(line.replace('<path>', '').replace('</path>', '').strip())
38
- current_path = str(path.parent)
39
- dir_counts[current_path] += 1
40
- file_types[path.suffix.lower() or 'no_ext'] += 1
41
- elif line.startswith('<content>'):
42
- current_content = []
43
- elif line.startswith('</content>'):
44
- content_size = sum(len(line.encode('utf-8')) for line in current_content)
45
- if current_path:
46
- dir_sizes[current_path] += content_size
47
- current_content = []
48
- elif current_content is not None:
49
- current_content.append(line)
50
-
51
- console = Console()
52
-
53
- # Directory statistics
54
- dir_stats = [
55
- f"📁 {directory}/ [{count} file(s), {_format_size(size)}]"
56
- for directory, (count, size) in (
57
- (d, (dir_counts[d], dir_sizes[d]))
58
- for d in sorted(dir_counts.keys())
59
- )
60
- ]
61
-
62
- # File type statistics
63
- type_stats = [
64
- f"📄 .{ext.lstrip('.')} [{count} file(s)]" if ext != 'no_ext' else f"📄 {ext} [{count} file(s)]"
65
- for ext, count in sorted(file_types.items())
66
- ]
67
-
68
- # Create grouped content with styled separators
69
- content_sections = []
70
-
71
- if paths:
72
- # Group paths with their stats
73
- path_stats = []
74
- for path in sorted(set(paths)):
75
- base_path = Path(path.rstrip("/*"))
76
- total_files = sum(1 for d, count in dir_counts.items()
77
- if Path(d).is_relative_to(base_path))
78
- total_size = sum(size for d, size in dir_sizes.items()
79
- if Path(d).is_relative_to(base_path))
80
- path_stats.append(f"{path} [{total_files} file(s), {_format_size(total_size)}]")
81
-
82
- content_sections.extend([
83
- "[bold yellow]📌 Included Paths[/bold yellow]",
84
- Rule(style="yellow"),
85
- Columns(path_stats, equal=True, expand=True),
86
- "\n"
87
- ])
88
-
89
- # Add directory structure section only in verbose mode
90
- if config.verbose:
91
- content_sections.extend([
92
- "[bold magenta]📂 Directory Structure[/bold magenta]",
93
- Rule(style="magenta"),
94
- Columns(dir_stats, equal=True, expand=True),
95
- "\n"
96
- ])
97
-
98
- # Always show file types section
99
- content_sections.extend([
100
- "[bold cyan]📑 File Types[/bold cyan]",
101
- Rule(style="cyan"),
102
- Columns(type_stats, equal=True, expand=True)
103
- ])
104
-
105
- content = Group(*content_sections)
106
-
107
- # Display workspace analysis in panel
108
- console.print("\n")
109
- console.print(Panel(
110
- content,
111
- title="[bold blue]Workspace Analysis[/bold blue]",
112
- title_align="center"
113
- ))
114
-
115
- def _format_size(size_bytes: int) -> str:
116
- """Format size in bytes to human readable format"""
117
- for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
118
- if size_bytes < 1024.0:
119
- break
120
- size_bytes /= 1024.0
1
+ from collections import defaultdict
2
+ from pathlib import Path
3
+ from typing import Dict, List
4
+
5
+ from rich.columns import Columns
6
+ from rich.console import Console, Group
7
+ from rich.panel import Panel
8
+ from rich.rule import Rule
9
+ from janito.config import config
10
+
11
+ def analyze_workspace_content(content: str) -> None:
12
+ """Show statistics about the scanned content"""
13
+ if not content:
14
+ return
15
+
16
+ # Collect include paths
17
+ paths = []
18
+ if config.include:
19
+ for path in config.include:
20
+ is_recursive = path in config.recursive
21
+ path_str = str(path.relative_to(config.workspace_dir))
22
+ paths.append(f"{path_str}/*" if is_recursive else f"{path_str}/")
23
+ else:
24
+ # Use workspace_dir as fallback when no include paths specified
25
+ paths.append("./")
26
+
27
+ console = Console()
28
+
29
+ dir_counts: Dict[str, int] = defaultdict(int)
30
+ dir_sizes: Dict[str, int] = defaultdict(int)
31
+ file_types: Dict[str, int] = defaultdict(int)
32
+ current_path = None
33
+ current_content = []
34
+
35
+ for line in content.split('\n'):
36
+ if line.startswith('<path>'):
37
+ path = Path(line.replace('<path>', '').replace('</path>', '').strip())
38
+ current_path = str(path.parent)
39
+ dir_counts[current_path] += 1
40
+ file_types[path.suffix.lower() or 'no_ext'] += 1
41
+ elif line.startswith('<content>'):
42
+ current_content = []
43
+ elif line.startswith('</content>'):
44
+ content_size = sum(len(line.encode('utf-8')) for line in current_content)
45
+ if current_path:
46
+ dir_sizes[current_path] += content_size
47
+ current_content = []
48
+ elif current_content is not None:
49
+ current_content.append(line)
50
+
51
+ console = Console()
52
+
53
+ # Directory statistics
54
+ dir_stats = [
55
+ f"📁 {directory}/ [{count} file(s), {_format_size(size)}]"
56
+ for directory, (count, size) in (
57
+ (d, (dir_counts[d], dir_sizes[d]))
58
+ for d in sorted(dir_counts.keys())
59
+ )
60
+ ]
61
+
62
+ # File type statistics
63
+ type_stats = [
64
+ f"📄 .{ext.lstrip('.')} [{count} file(s)]" if ext != 'no_ext' else f"📄 {ext} [{count} file(s)]"
65
+ for ext, count in sorted(file_types.items())
66
+ ]
67
+
68
+ # Create grouped content with styled separators
69
+ content_sections = []
70
+
71
+ if paths:
72
+ # Group paths with their stats
73
+ path_stats = []
74
+ for path in sorted(set(paths)):
75
+ base_path = Path(path.rstrip("/*"))
76
+ total_files = sum(1 for d, count in dir_counts.items()
77
+ if Path(d).is_relative_to(base_path))
78
+ total_size = sum(size for d, size in dir_sizes.items()
79
+ if Path(d).is_relative_to(base_path))
80
+ path_stats.append(f"{path} [{total_files} file(s), {_format_size(total_size)}]")
81
+
82
+ content_sections.extend([
83
+ "[bold yellow]📌 Included Paths[/bold yellow]",
84
+ Rule(style="yellow"),
85
+ Columns(path_stats, equal=True, expand=True),
86
+ "\n"
87
+ ])
88
+
89
+ # Add directory structure section only in verbose mode
90
+ if config.verbose:
91
+ content_sections.extend([
92
+ "[bold magenta]📂 Directory Structure[/bold magenta]",
93
+ Rule(style="magenta"),
94
+ Columns(dir_stats, equal=True, expand=True),
95
+ "\n"
96
+ ])
97
+
98
+ # Always show file types section
99
+ content_sections.extend([
100
+ "[bold cyan]📑 File Types[/bold cyan]",
101
+ Rule(style="cyan"),
102
+ Columns(type_stats, equal=True, expand=True)
103
+ ])
104
+
105
+ content = Group(*content_sections)
106
+
107
+ # Display workset analysis in panel
108
+ console.print("\n")
109
+ console.print(Panel(
110
+ content,
111
+ title="[bold blue]Workset Analysis[/bold blue]",
112
+ title_align="center"
113
+ ))
114
+
115
+ def _format_size(size_bytes: int) -> str:
116
+ """Format size in bytes to human readable format"""
117
+ for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
118
+ if size_bytes < 1024.0:
119
+ break
120
+ size_bytes /= 1024.0
121
121
  return f"{size_bytes:.1f} {unit}"
@@ -0,0 +1,97 @@
1
+ from dataclasses import dataclass, field
2
+ from pathlib import Path
3
+ from typing import List, Dict, Set, Tuple
4
+ from sys import maxsize
5
+ from janito.config import config
6
+ from enum import auto, Enum
7
+
8
+ @dataclass
9
+ class FileInfo:
10
+ """Represents a file's basic information"""
11
+ name: str # Relative path from workspace root
12
+ content: str
13
+ seconds_ago: int = 0 # Seconds since last modification
14
+
15
+ def __lt__(self, other: 'FileInfo') -> bool:
16
+ """Enable sorting by filepath."""
17
+ if not isinstance(other, FileInfo):
18
+ return NotImplemented
19
+ return self.name < other.name
20
+
21
+ def __eq__(self, other: object) -> bool:
22
+ """Enable equality comparison by filepath."""
23
+ if not isinstance(other, FileInfo):
24
+ return NotImplemented
25
+ return self.name == other.name
26
+
27
+ def __hash__(self) -> int:
28
+ """Enable using FileInfo in sets by using filepath as hash."""
29
+ return hash(self.name)
30
+
31
+ class ScanType(Enum):
32
+ """Type of path scanning"""
33
+ PLAIN = auto()
34
+ RECURSIVE = auto()
35
+
36
+ @dataclass
37
+ class ScanPath:
38
+ """Represents a path to be scanned"""
39
+ path: Path
40
+ scan_type: ScanType = ScanType.PLAIN
41
+
42
+ @property
43
+ def is_recursive(self) -> bool:
44
+ return self.scan_type == ScanType.RECURSIVE
45
+
46
+ @classmethod
47
+ def validate(cls, path: Path) -> None:
48
+ """Validate path is relative and exists"""
49
+ if path.is_absolute():
50
+ raise ValueError(f"Path must be relative: {path}")
51
+ if not path.exists():
52
+ raise ValueError(f"Path does not exist: {path}")
53
+
54
+ @dataclass
55
+ class WorksetContent:
56
+ """Represents workset content and statistics."""
57
+ files: List[FileInfo] = field(default_factory=list)
58
+ scanned_paths: Set[Path] = field(default_factory=set)
59
+ dir_counts: Dict[str, int] = field(default_factory=dict)
60
+ dir_sizes: Dict[str, int] = field(default_factory=dict)
61
+ file_types: Dict[str, int] = field(default_factory=dict)
62
+ scan_completed: bool = False
63
+ analyzed: bool = False
64
+
65
+ def clear(self) -> None:
66
+ """Reset all content"""
67
+ self.files = []
68
+ self.scanned_paths = set()
69
+ self.dir_counts = {}
70
+ self.dir_sizes = {}
71
+ self.file_types = {}
72
+ self.scan_completed = False
73
+ self.analyzed = False
74
+
75
+ def add_file(self, file_info: FileInfo) -> None:
76
+ """Add a file to the content and update statistics"""
77
+ self.files.append(file_info)
78
+
79
+ # Update file type stats
80
+ suffix = Path(file_info.name).suffix.lower() or 'no_ext'
81
+ self.file_types[suffix] = self.file_types.get(suffix, 0) + 1
82
+
83
+ # Update directory stats
84
+ dir_path = str(Path(file_info.name).parent)
85
+ self.dir_counts[dir_path] = self.dir_counts.get(dir_path, 0) + 1
86
+ self.dir_sizes[dir_path] = self.dir_sizes.get(dir_path, 0) + len(file_info.content.encode('utf-8'))
87
+
88
+
89
+ @property
90
+ def content_size(self) -> int:
91
+ """Get total content size in bytes"""
92
+ return sum(len(f.content.encode('utf-8')) for f in self.files)
93
+
94
+ @property
95
+ def file_count(self) -> int:
96
+ """Get total number of files"""
97
+ return len(self.files)
@@ -0,0 +1,115 @@
1
+ from rich.traceback import install
2
+ install(show_locals=False)
3
+
4
+ from pathlib import Path
5
+ from typing import List, Set
6
+ from rich.columns import Columns
7
+ from rich.console import Console, Group
8
+ from rich.panel import Panel
9
+ from rich.rule import Rule
10
+ from rich.text import Text
11
+ from janito.config import config
12
+ from .models import FileInfo, ScanPath
13
+ from .stats import collect_file_stats, _format_size
14
+
15
+
16
+ def show_workset_analysis(
17
+ files: List[FileInfo],
18
+ scan_paths: List[ScanPath],
19
+ cache_blocks: List[List[FileInfo]] = None
20
+ ) -> None:
21
+ """Display analysis of workspace content and configuration."""
22
+
23
+ console = Console()
24
+ content_sections = []
25
+
26
+ # Get statistics
27
+ dir_counts, file_types = collect_file_stats(files)
28
+
29
+ # Calculate path stats using relative paths
30
+ paths_stats = []
31
+ total_files = 0
32
+ total_size = 0
33
+
34
+
35
+ # Process all paths uniformly
36
+ for scan_path in sorted(scan_paths, key=lambda p: p.path):
37
+
38
+ path = scan_path.path
39
+ is_recursive = scan_path.is_recursive
40
+ path_str = str(path)
41
+
42
+ # Calculate stats based on scan type
43
+ if is_recursive:
44
+ path_files = sum(count for d, [count, _] in dir_counts.items()
45
+ if Path(d) == path or Path(d).is_relative_to(path))
46
+ path_size = sum(size for d, [_, size] in dir_counts.items()
47
+ if Path(d) == path or Path(d).is_relative_to(path))
48
+ else:
49
+ path_files = dir_counts.get(path_str, [0, 0])[0]
50
+ path_size = dir_counts.get(path_str, [0, 0])[1]
51
+
52
+ total_files += path_files
53
+ total_size += path_size
54
+
55
+ paths_stats.append(
56
+ f"[bold cyan]{path}[/bold cyan]"
57
+ f"[yellow]{'/**' if is_recursive else '/'}[/yellow] "
58
+ f"[[green]{path_files}[/green] "
59
+ f"{'total ' if is_recursive else ''}file(s), "
60
+ f"[blue]{_format_size(path_size)}[/blue]]"
61
+ )
62
+
63
+ # Build sections - Show paths first
64
+ if paths_stats:
65
+ content_sections.extend([
66
+ "[bold yellow]📌 Included Paths[/bold yellow]",
67
+ Rule(style="yellow"),
68
+ ])
69
+
70
+ content_sections.append(
71
+ Text(" | ").join(Text.from_markup(path) for path in paths_stats)
72
+ )
73
+
74
+ # Add total summary if there are multiple paths
75
+ if len(paths_stats) > 1:
76
+ content_sections.extend([
77
+ "", # Empty line for spacing
78
+ f"[bold yellow]Total:[/bold yellow] [green]{total_files}[/green] files, "
79
+ f"[blue]{_format_size(total_size)}[/blue]"
80
+ ])
81
+ content_sections.append("\n")
82
+
83
+ # Then show directory structure if verbose
84
+ if config.verbose:
85
+ dir_stats = [
86
+ f"📁 {directory}/ [{count} file(s), {_format_size(size)}]"
87
+ for directory, (count, size) in sorted(dir_counts.items())
88
+ ]
89
+ content_sections.extend([
90
+ "[bold magenta]📂 Directory Structure[/bold magenta]",
91
+ Rule(style="magenta"),
92
+ Columns(dir_stats, equal=True, expand=True),
93
+ "\n"
94
+ ])
95
+
96
+ type_stats = [
97
+ f"[bold cyan].{ext.lstrip('.')}[/bold cyan] [[green]{count}[/green] file(s)]"
98
+ if ext != 'no_ext'
99
+ else f"[bold cyan]no ext[/bold cyan] [[green]{count}[/green] file(s)]"
100
+ for ext, count in sorted(file_types.items())
101
+ ]
102
+ content_sections.extend([
103
+ "[bold cyan]📑 File Types[/bold cyan]",
104
+ Rule(style="cyan"),
105
+ Text(" | ").join(Text.from_markup(stat) for stat in type_stats)
106
+ ])
107
+
108
+
109
+ # Display analysis
110
+ console.print("\n")
111
+ console.print(Panel(
112
+ Group(*content_sections),
113
+ title="[bold blue]Workset Analysis[/bold blue]",
114
+ title_align="center"
115
+ ))
@@ -0,0 +1,42 @@
1
+ from collections import defaultdict
2
+ from pathlib import Path
3
+ from typing import List, Dict, Tuple
4
+ from .models import FileInfo
5
+
6
+ def collect_file_stats(files: List[FileInfo]) -> Tuple[Dict[str, List[int]], Dict[str, int]]:
7
+ """Collect directory and file type statistics from files.
8
+
9
+ Args:
10
+ files: List of FileInfo objects to analyze
11
+
12
+ Returns:
13
+ Tuple containing:
14
+ - Dictionary of directory stats [count, size]
15
+ - Dictionary of file type counts
16
+ """
17
+ dir_counts = defaultdict(lambda: [0, 0]) # [count, size]
18
+ file_types = defaultdict(int)
19
+
20
+ for file_info in files:
21
+ path = Path(file_info.name)
22
+ dir_path = str(path.parent)
23
+ file_size = len(file_info.content.encode('utf-8'))
24
+
25
+ # Update directory stats
26
+ dir_counts[dir_path][0] += 1
27
+ dir_counts[dir_path][1] += file_size
28
+
29
+ # Update file type stats
30
+ file_types[path.suffix.lower() or 'no_ext'] += 1
31
+
32
+ return dir_counts, file_types
33
+
34
+ def _format_size(size_bytes: int) -> str:
35
+ """Format size in bytes to human readable format."""
36
+ size = size_bytes
37
+ for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
38
+ if size < 1024:
39
+ break
40
+ size //= 1024
41
+ return f"{size} {unit}"
42
+
@@ -0,0 +1,135 @@
1
+ from pathlib import Path
2
+ from typing import List, Set
3
+ from .show import show_workset_analysis
4
+ from rich.console import Console
5
+ from janito.config import config
6
+ from .models import WorksetContent, ScanPath, ScanType
7
+ from .workspace import Workspace
8
+
9
+ class PathNotRelativeError(Exception):
10
+ """Raised when a path is not relative."""
11
+ pass
12
+
13
+ class Workset:
14
+ _instance = None
15
+
16
+ def __new__(cls):
17
+ if cls._instance is None:
18
+ cls._instance = super().__new__(cls)
19
+ cls._instance._init()
20
+ return cls._instance
21
+
22
+ def _init(self):
23
+ self._scan_paths: List[ScanPath] = []
24
+ self._content = WorksetContent()
25
+ self._workspace = Workspace()
26
+ if not config.skip_work:
27
+ self.add_scan_path(Path("."))
28
+
29
+ def add_scan_path(self, path: Path, scan_type: ScanType = ScanType.PLAIN) -> None:
30
+ """Add a path with specific scan type.
31
+
32
+ Args:
33
+ path: Relative path to add for scanning
34
+ scan_type: Type of scanning (PLAIN or RECURSIVE)
35
+
36
+ Raises:
37
+ PathNotRelativeError: If path is absolute
38
+ """
39
+ if path.is_absolute():
40
+ raise PathNotRelativeError(f"Path must be relative: {path}")
41
+
42
+ scan_path = ScanPath(path, scan_type)
43
+ ScanPath.validate(path)
44
+ self._scan_paths.append(scan_path)
45
+
46
+ if config.debug:
47
+ Console(stderr=True).print(
48
+ f"[cyan]Debug: Added {scan_type.name.lower()} scan path: {path}[/cyan]"
49
+ )
50
+
51
+ def refresh(self) -> None:
52
+ """Refresh content by scanning configured paths"""
53
+ self.clear()
54
+ paths = self.get_scan_paths()
55
+
56
+ if config.debug:
57
+ Console(stderr=True).print(f"[cyan]Debug: Refreshing workset with paths: {paths}[/cyan]")
58
+
59
+ self._workspace.scan_files(paths, self.get_recursive_paths())
60
+ self._content = self._workspace.content
61
+
62
+ def get_scan_paths(self) -> List[Path]:
63
+ """Get effective scan paths based on configuration"""
64
+ paths = set()
65
+ paths.update(p.path for p in self._scan_paths)
66
+ return sorted(paths)
67
+
68
+ def get_recursive_paths(self) -> Set[Path]:
69
+ """Get paths that should be scanned recursively"""
70
+ return {p.path for p in self._scan_paths if p.is_recursive}
71
+
72
+ def is_path_recursive(self, path: Path) -> bool:
73
+ """Check if a path is configured for recursive scanning"""
74
+ return any(scan_path.is_recursive and scan_path.path == path
75
+ for scan_path in self._scan_paths)
76
+
77
+ @property
78
+ def paths(self) -> Set[Path]:
79
+ return {p.path for p in self._scan_paths}
80
+
81
+ @property
82
+ def recursive_paths(self) -> Set[Path]:
83
+ return self.get_recursive_paths()
84
+
85
+ def clear(self) -> None:
86
+ """Clear workspace settings while maintaining current directory in scan paths"""
87
+ self._content = WorksetContent()
88
+
89
+
90
+ def show(self) -> None:
91
+ """Display analysis of current workset content."""
92
+ show_workset_analysis(
93
+ files=self._content.files,
94
+ scan_paths=self._scan_paths,
95
+ cache_blocks=None
96
+ )
97
+
98
+ @property
99
+ def content(self) -> str:
100
+ """Return the workset content as a string.
101
+
102
+ Format:
103
+ <workspace_base_directories>
104
+ dirname1
105
+ dirname2
106
+ ...
107
+ <workset_files>
108
+ <file name=filename1>
109
+ ```
110
+ file1_content
111
+ ```
112
+ </file>
113
+ <file name=filename2>
114
+ ```
115
+ file2_content
116
+ ```
117
+ </file>
118
+ ...
119
+ """
120
+ content = "<workspace_base_directories>\n"
121
+ # Only include root directories if not skipping workspace
122
+ if not config.skip_work:
123
+ for dir in sorted(self._workspace.root_directories):
124
+ content += f"{dir}\n"
125
+
126
+ content += "<workset_files>\n"
127
+ for file in sorted(self._content.files):
128
+ content += f"<file name={file.name}>\n"
129
+ content += "```\n"
130
+ content += file.content
131
+ content += "\n```\n"
132
+ content += "</file>\n"
133
+
134
+ return content
135
+