janito 0.7.0__py3-none-any.whl → 0.9.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 (114) hide show
  1. janito/__init__.py +5 -2
  2. janito/__main__.py +151 -142
  3. janito/callbacks.py +130 -0
  4. janito/cli.py +202 -0
  5. janito/config.py +57 -96
  6. janito/data/instructions.txt +6 -0
  7. janito/test_file.py +4 -0
  8. janito/token_report.py +73 -0
  9. janito/tools/__init__.py +10 -0
  10. janito/tools/decorators.py +84 -0
  11. janito/tools/delete_file.py +44 -0
  12. janito/tools/find_files.py +154 -0
  13. janito/tools/search_text.py +197 -0
  14. janito/tools/str_replace_editor/__init__.py +6 -0
  15. janito/tools/str_replace_editor/editor.py +43 -0
  16. janito/tools/str_replace_editor/handlers.py +338 -0
  17. janito/tools/str_replace_editor/utils.py +88 -0
  18. {janito-0.7.0.dist-info/licenses → janito-0.9.0.dist-info}/LICENSE +20 -20
  19. janito-0.9.0.dist-info/METADATA +9 -0
  20. janito-0.9.0.dist-info/RECORD +23 -0
  21. {janito-0.7.0.dist-info → janito-0.9.0.dist-info}/WHEEL +2 -1
  22. janito-0.9.0.dist-info/entry_points.txt +2 -0
  23. janito-0.9.0.dist-info/top_level.txt +1 -0
  24. janito/agents/__init__.py +0 -22
  25. janito/agents/agent.py +0 -28
  26. janito/agents/claudeai.py +0 -45
  27. janito/agents/openai.py +0 -57
  28. janito/agents/test.py +0 -34
  29. janito/change/__init__.py +0 -32
  30. janito/change/__main__.py +0 -0
  31. janito/change/analysis/__init__.py +0 -23
  32. janito/change/analysis/__main__.py +0 -7
  33. janito/change/analysis/analyze.py +0 -62
  34. janito/change/analysis/formatting.py +0 -78
  35. janito/change/analysis/options.py +0 -81
  36. janito/change/analysis/prompts.py +0 -90
  37. janito/change/analysis/view/__init__.py +0 -9
  38. janito/change/analysis/view/terminal.py +0 -181
  39. janito/change/applier/__init__.py +0 -5
  40. janito/change/applier/file.py +0 -58
  41. janito/change/applier/main.py +0 -156
  42. janito/change/applier/text.py +0 -247
  43. janito/change/applier/workspace_dir.py +0 -58
  44. janito/change/core.py +0 -124
  45. janito/change/history.py +0 -44
  46. janito/change/operations.py +0 -7
  47. janito/change/parser.py +0 -287
  48. janito/change/play.py +0 -54
  49. janito/change/preview.py +0 -82
  50. janito/change/prompts.py +0 -121
  51. janito/change/test.py +0 -0
  52. janito/change/validator.py +0 -269
  53. janito/change/viewer/__init__.py +0 -11
  54. janito/change/viewer/content.py +0 -66
  55. janito/change/viewer/diff.py +0 -43
  56. janito/change/viewer/panels.py +0 -533
  57. janito/change/viewer/styling.py +0 -114
  58. janito/change/viewer/themes.py +0 -55
  59. janito/clear_statement_parser/clear_statement_format.txt +0 -328
  60. janito/clear_statement_parser/examples.txt +0 -326
  61. janito/clear_statement_parser/models.py +0 -104
  62. janito/clear_statement_parser/parser.py +0 -496
  63. janito/cli/__init__.py +0 -2
  64. janito/cli/base.py +0 -30
  65. janito/cli/commands.py +0 -88
  66. janito/cli/functions.py +0 -111
  67. janito/cli/history.py +0 -61
  68. janito/cli/registry.py +0 -26
  69. janito/common.py +0 -80
  70. janito/demo/__init__.py +0 -4
  71. janito/demo/data.py +0 -13
  72. janito/demo/mock_data.py +0 -20
  73. janito/demo/operations.py +0 -45
  74. janito/demo/runner.py +0 -59
  75. janito/demo/scenarios.py +0 -32
  76. janito/prompt.py +0 -36
  77. janito/qa.py +0 -57
  78. janito/review.py +0 -13
  79. janito/search_replace/README.md +0 -192
  80. janito/search_replace/__init__.py +0 -7
  81. janito/search_replace/__main__.py +0 -21
  82. janito/search_replace/core.py +0 -120
  83. janito/search_replace/logger.py +0 -35
  84. janito/search_replace/parser.py +0 -52
  85. janito/search_replace/play.py +0 -61
  86. janito/search_replace/replacer.py +0 -36
  87. janito/search_replace/searcher.py +0 -411
  88. janito/search_replace/strategy_result.py +0 -10
  89. janito/shell/__init__.py +0 -38
  90. janito/shell/bus.py +0 -31
  91. janito/shell/commands.py +0 -136
  92. janito/shell/history.py +0 -20
  93. janito/shell/processor.py +0 -32
  94. janito/shell/prompt.py +0 -48
  95. janito/shell/registry.py +0 -60
  96. janito/tui/__init__.py +0 -21
  97. janito/tui/base.py +0 -22
  98. janito/tui/flows/__init__.py +0 -5
  99. janito/tui/flows/changes.py +0 -65
  100. janito/tui/flows/content.py +0 -128
  101. janito/tui/flows/selection.py +0 -117
  102. janito/tui/screens/__init__.py +0 -3
  103. janito/tui/screens/app.py +0 -1
  104. janito/version.py +0 -23
  105. janito/workspace/__init__.py +0 -6
  106. janito/workspace/analysis.py +0 -121
  107. janito/workspace/show.py +0 -141
  108. janito/workspace/stats.py +0 -43
  109. janito/workspace/types.py +0 -98
  110. janito/workspace/workset.py +0 -108
  111. janito/workspace/workspace.py +0 -114
  112. janito-0.7.0.dist-info/METADATA +0 -167
  113. janito-0.7.0.dist-info/RECORD +0 -96
  114. janito-0.7.0.dist-info/entry_points.txt +0 -2
janito/shell/prompt.py DELETED
@@ -1,48 +0,0 @@
1
- """Prompt creation and configuration for Janito shell."""
2
- from typing import Dict, Any
3
- from pathlib import Path
4
- from prompt_toolkit import PromptSession
5
- from prompt_toolkit.history import FileHistory
6
- from prompt_toolkit.completion import NestedCompleter
7
- from rich import print as rich_print
8
- from janito.config import config
9
- from .registry import CommandRegistry
10
-
11
- def create_shell_completer(registry: CommandRegistry):
12
- """Create command completer for shell with nested completions."""
13
- if config.debug:
14
- rich_print("[yellow]Creating shell completer...[/yellow]")
15
-
16
- commands = registry.get_commands()
17
-
18
- if config.debug:
19
- rich_print(f"[yellow]Found {len(commands)} commands for completion[/yellow]")
20
-
21
- # Create nested completions for commands
22
- completions: Dict[str, Any] = {}
23
-
24
- for cmd_name, cmd in commands.items():
25
- if config.debug:
26
- rich_print(f"[yellow]Setting up completion for command: {cmd_name}[/yellow]")
27
- completions[cmd_name] = cmd.completer
28
-
29
- if config.debug:
30
- rich_print("[yellow]Creating nested completer from completions dictionary[/yellow]")
31
- return NestedCompleter.from_nested_dict(completions)
32
-
33
- def create_shell_session(registry: CommandRegistry) -> PromptSession:
34
- """Create and configure the shell prompt session."""
35
- if config.debug:
36
- rich_print("[yellow]Creating shell session...[/yellow]")
37
-
38
- history_file = Path.home() / ".janito_history"
39
- if config.debug:
40
- rich_print(f"[yellow]Using history file: {history_file}[/yellow]")
41
-
42
- completer = create_shell_completer(registry)
43
-
44
- return PromptSession(
45
- history=FileHistory(str(history_file)),
46
- completer=completer,
47
- complete_while_typing=True
48
- )
janito/shell/registry.py DELETED
@@ -1,60 +0,0 @@
1
- """Command registry and validation system for Janito shell."""
2
- from dataclasses import dataclass
3
- from typing import Optional, Callable, Dict, Any
4
- from pathlib import Path
5
- from prompt_toolkit.completion import PathCompleter
6
-
7
- @dataclass
8
- class Command:
9
- """Command definition with handler and metadata."""
10
- name: str
11
- description: str
12
- usage: Optional[str]
13
- handler: Callable[[str], None]
14
- completer: Optional[Any] = None
15
-
16
- class CommandRegistry:
17
- """Centralized command registry with validation."""
18
- def __init__(self):
19
- """Initialize registry."""
20
- if not hasattr(self, '_commands'):
21
- self._commands = {}
22
-
23
-
24
- def register(self, command: Command) -> None:
25
- """Register a command with validation."""
26
- if command.name in self._commands:
27
- raise ValueError(f"Command '{command.name}' already registered")
28
- if not callable(command.handler):
29
- raise ValueError(f"Handler for command '{command.name}' must be callable")
30
- self._commands[command.name] = command
31
-
32
- def register_alias(self, alias: str, command_name: str) -> None:
33
- """Register an alias for an existing command."""
34
- if alias in self._commands:
35
- raise ValueError(f"Alias '{alias}' already registered")
36
- if command := self.get_command(command_name):
37
- self._commands[alias] = command
38
- else:
39
- raise ValueError(f"Command '{command_name}' not found")
40
-
41
- def get_command(self, name: str) -> Optional[Command]:
42
- """Get a command by name."""
43
- return self._commands.get(name)
44
-
45
- def get_commands(self) -> Dict[str, Command]:
46
- """Get all registered commands."""
47
- return self._commands.copy()
48
-
49
- def validate_command(self, command: Command) -> None:
50
- """Validate command properties."""
51
- if not command.name:
52
- raise ValueError("Command name cannot be empty")
53
- if not command.description:
54
- raise ValueError(f"Command '{command.name}' must have a description")
55
- if not callable(command.handler):
56
- raise ValueError(f"Command '{command.name}' handler must be callable")
57
-
58
- def get_path_completer(only_directories: bool = False) -> PathCompleter:
59
- """Get a configured path completer."""
60
- return PathCompleter(only_directories=only_directories)
janito/tui/__init__.py DELETED
@@ -1,21 +0,0 @@
1
- """Terminal User Interface package for Janito."""
2
- from .base import BaseTuiApp
3
- from typing import Dict, Optional
4
- from janito.change.analysis.options import AnalysisOption
5
-
6
- class TuiApp(BaseTuiApp):
7
- """Main TUI application with flow-based navigation"""
8
-
9
- def on_mount(self) -> None:
10
- """Initialize appropriate flow based on input"""
11
- if self.options:
12
- from .flows.selection import SelectionFlow
13
- self.push_screen(SelectionFlow(self.options))
14
- elif self.changes:
15
- from .flows.changes import ChangesFlow
16
- self.push_screen(ChangesFlow(self.changes))
17
- elif self.content:
18
- from .flows.content import ContentFlow
19
- self.push_screen(ContentFlow(self.content))
20
-
21
- __all__ = ['TuiApp']
janito/tui/base.py DELETED
@@ -1,22 +0,0 @@
1
- from textual.app import App
2
- from typing import List, Optional, Dict
3
- from janito.change.parser import FileChange
4
- from janito.change.analysis.options import AnalysisOption
5
-
6
- class BaseTuiApp(App):
7
- """Base class for TUI applications with common functionality"""
8
- CSS = """
9
- Screen {
10
- align: center middle;
11
- }
12
- """
13
-
14
- def __init__(self,
15
- content: Optional[str] = None,
16
- options: Optional[Dict[str, AnalysisOption]] = None,
17
- changes: Optional[List[FileChange]] = None):
18
- super().__init__()
19
- self.content = content
20
- self.options = options
21
- self.changes = changes
22
- self.selected_option = None
@@ -1,5 +0,0 @@
1
- from .selection import SelectionFlow
2
- from .content import ContentFlow
3
- from .changes import ChangesFlow
4
-
5
- __all__ = ['SelectionFlow', 'ContentFlow', 'ChangesFlow']
@@ -1,65 +0,0 @@
1
- from textual.app import ComposeResult
2
- from textual.containers import ScrollableContainer
3
- from textual.screen import Screen
4
- from textual.binding import Binding
5
- from textual.widgets import Header, Footer, Static
6
- from typing import List
7
- from ...change.viewer.styling import format_content, create_legend_items
8
- from ...change.viewer.panels import create_change_panel, create_new_file_panel, create_replace_panel, create_remove_file_panel
9
- from ...change.parser import FileChange
10
-
11
- class ChangesFlow(Screen):
12
- """Screen for changes preview flow"""
13
- CSS = """
14
- ScrollableContainer {
15
- width: 100%;
16
- height: 100%;
17
- border: solid green;
18
- background: $surface;
19
- color: $text;
20
- padding: 1;
21
- }
22
-
23
- Container.panel {
24
- margin: 1;
25
- padding: 1;
26
- border: solid $primary;
27
- width: 100%;
28
- }
29
- """
30
-
31
- BINDINGS = [
32
- Binding("q", "quit", "Quit"),
33
- Binding("escape", "quit", "Quit"),
34
- Binding("up", "previous", "Previous"),
35
- Binding("down", "next", "Next"),
36
- ]
37
-
38
- def __init__(self, changes: List[FileChange]):
39
- super().__init__()
40
- self.changes = changes
41
- self.current_index = 0
42
-
43
- def compose(self) -> ComposeResult:
44
- yield Header()
45
- with ScrollableContainer():
46
- for change in self.changes:
47
- if change.operation == 'create_file':
48
- yield Static(create_new_file_panel(change.name, change.content))
49
- elif change.operation == 'replace_file':
50
- yield Static(create_replace_panel(change.name, change))
51
- elif change.operation == 'remove_file':
52
- yield Static(create_remove_file_panel(change.name))
53
- elif change.operation == 'modify_file':
54
- for mod in change.text_changes:
55
- yield Static(create_change_panel(mod.search_content, mod.replace_content, change.description, 1))
56
- yield Footer()
57
-
58
- def action_quit(self):
59
- self.app.exit()
60
-
61
- def action_previous(self):
62
- self.scroll_up()
63
-
64
- def action_next(self):
65
- self.scroll_down()
@@ -1,128 +0,0 @@
1
- from textual.app import ComposeResult
2
- from textual.containers import ScrollableContainer
3
- from textual.screen import Screen
4
- from textual.binding import Binding
5
- from textual.widgets import Header, Footer, Static
6
- from rich.panel import Panel
7
- from rich.text import Text
8
- from rich import box
9
- from pathlib import Path
10
- from typing import Dict, List
11
-
12
- class ContentFlow(Screen):
13
- """Screen for content viewing flow with unified display format"""
14
- CSS = """
15
- ScrollableContainer {
16
- width: 100%;
17
- height: 100%;
18
- border: solid green;
19
- background: $surface;
20
- color: $text;
21
- padding: 1;
22
- }
23
-
24
- Container.panel {
25
- margin: 1;
26
- padding: 1;
27
- border: solid $primary;
28
- width: 100%;
29
- }
30
- """
31
-
32
- BINDINGS = [
33
- Binding("q", "quit", "Quit"),
34
- Binding("escape", "quit", "Quit"),
35
- Binding("up", "previous", "Previous"),
36
- Binding("down", "next", "Next"),
37
- ]
38
-
39
- def __init__(self, content: str):
40
- super().__init__()
41
- self.content = content
42
- self.files_by_type = self._organize_content()
43
-
44
- def _organize_content(self) -> Dict[str, List[str]]:
45
- """Organize content into file groups"""
46
- files = {
47
- 'Modified': [],
48
- 'New': [],
49
- 'Deleted': []
50
- }
51
-
52
- # Parse content to extract file information
53
- for line in self.content.split('\n'):
54
- if line.strip().startswith('- '):
55
- file_path = line[2:].strip()
56
- if '(new)' in file_path:
57
- files['New'].append(file_path)
58
- elif '(removed)' in file_path:
59
- files['Deleted'].append(file_path)
60
- else:
61
- files['Modified'].append(file_path)
62
-
63
- return files
64
-
65
- def _format_files_group(self, group_name: str, files: List[str], style: str) -> Text:
66
- """Format a group of files with consistent styling"""
67
- content = Text()
68
- if files:
69
- content.append(Text(f"\n─── {group_name} ───\n", style="cyan"))
70
-
71
- # Group files by directory
72
- files_by_dir = {}
73
- for file_path in files:
74
- clean_path = file_path.split(' (')[0]
75
- path = Path(clean_path)
76
- dir_path = str(path.parent)
77
- if dir_path not in files_by_dir:
78
- files_by_dir[dir_path] = []
79
- files_by_dir[dir_path].append(path)
80
-
81
- # Display files by directory
82
- for dir_path, paths in sorted(files_by_dir.items()):
83
- first_in_dir = True
84
- for path in sorted(paths):
85
- if first_in_dir:
86
- display_path = dir_path
87
- else:
88
- pad_left = (len(dir_path) - 3) // 2
89
- pad_right = len(dir_path) - 3 - pad_left
90
- display_path = " " * pad_left + "..." + " " * pad_right
91
-
92
- content.append(Text(f"• {display_path}/{path.name}\n", style=style))
93
- first_in_dir = False
94
-
95
- return content
96
-
97
- def compose(self) -> ComposeResult:
98
- yield Header()
99
- with ScrollableContainer():
100
- # Format content with consistent styling
101
- content = Text()
102
-
103
- # Add each file group with appropriate styling
104
- content.append(self._format_files_group("Modified", self.files_by_type['Modified'], "yellow"))
105
- content.append(self._format_files_group("New", self.files_by_type['New'], "green"))
106
- content.append(self._format_files_group("Deleted", self.files_by_type['Deleted'], "red"))
107
-
108
- # Create panel with formatted content
109
- panel = Panel(
110
- content,
111
- box=box.ROUNDED,
112
- border_style="cyan",
113
- title="Content Changes",
114
- title_align="center",
115
- padding=(1, 2)
116
- )
117
-
118
- yield Static(panel)
119
- yield Footer()
120
-
121
- def action_quit(self):
122
- self.app.exit()
123
-
124
- def action_previous(self):
125
- self.scroll_up()
126
-
127
- def action_next(self):
128
- self.scroll_down()
@@ -1,117 +0,0 @@
1
- from textual.app import ComposeResult
2
- from textual.containers import Container
3
- from textual.screen import Screen
4
- from textual.binding import Binding
5
- from textual.widgets import Header, Footer, Static
6
- from typing import Dict, Optional
7
- from rich.panel import Panel
8
- from rich import box
9
- from janito.change.analysis.options import AnalysisOption
10
- from janito.agents import agent
11
- from janito.common import progress_send_message
12
- from janito.change.parser import parse_response
13
- from .changes import ChangesFlow
14
-
15
- class SelectionFlow(Screen):
16
- """Selection screen with direct navigation to changes preview"""
17
-
18
- CSS = """
19
- #options-container {
20
- layout: horizontal;
21
- height: 100%;
22
- margin: 1;
23
- align: center middle;
24
- }
25
-
26
- .option-panel {
27
- width: 1fr;
28
- height: 100%;
29
- border: solid $primary;
30
- margin: 0 1;
31
- padding: 1;
32
- }
33
-
34
- .option-panel.selected {
35
- border: double $secondary;
36
- background: $boost;
37
- }
38
- """
39
-
40
- BINDINGS = [
41
- Binding("left", "previous", "Previous"),
42
- Binding("right", "next", "Next"),
43
- Binding("enter", "select", "Select"),
44
- Binding("escape", "quit", "Quit"),
45
- ]
46
-
47
- def __init__(self, options: Dict[str, AnalysisOption]):
48
- super().__init__()
49
- self.options = options
50
- self.current_index = 0
51
- self.panels = []
52
-
53
- def compose(self) -> ComposeResult:
54
- yield Header()
55
- with Container(id="options-container"):
56
- for letter, option in self.options.items():
57
- panel = Static(self._format_option(letter, option), classes="option-panel")
58
- self.panels.append(panel)
59
- yield panel
60
- yield Footer()
61
- # Set initial selection
62
- if self.panels:
63
- self.panels[0].add_class("selected")
64
-
65
- def _format_option(self, letter: str, option: AnalysisOption) -> str:
66
- """Format option content"""
67
- content = [f"Option {letter}: {option.summary}\n"]
68
- content.append("\nDescription:")
69
- for item in option.description_items:
70
- content.append(f"• {item}")
71
- content.append("\nAffected files:")
72
- for file in option.affected_files:
73
- content.append(f"• {file}")
74
- return "\n".join(content)
75
-
76
- def action_previous(self) -> None:
77
- """Handle left arrow key"""
78
- if self.panels:
79
- self.panels[self.current_index].remove_class("selected")
80
- self.current_index = (self.current_index - 1) % len(self.panels)
81
- self.panels[self.current_index].add_class("selected")
82
-
83
- def action_next(self) -> None:
84
- """Handle right arrow key"""
85
- if self.panels:
86
- self.panels[self.current_index].remove_class("selected")
87
- self.current_index = (self.current_index + 1) % len(self.panels)
88
- self.panels[self.current_index].add_class("selected")
89
-
90
- def action_select(self) -> None:
91
- """Handle enter key - request changes and show preview"""
92
- if self.panels:
93
- letter = list(self.options.keys())[self.current_index]
94
- option = self.options[letter]
95
-
96
- # Build and send change request
97
- from janito.change import build_change_request_prompt
98
- from janito.workspace import collect_files_content
99
-
100
- files_content = collect_files_content([option.get_clean_path(f) for f in option.affected_files])
101
- prompt = build_change_request_prompt(option.format_option_text(), "", files_content)
102
- response = progress_send_message(prompt)
103
-
104
- if response:
105
- changes = parse_response(response)
106
- if changes:
107
- # Show changes preview
108
- self.app.push_screen(ChangesFlow(changes))
109
- return
110
-
111
- self.app.selected_option = option
112
- self.app.exit()
113
-
114
- def action_quit(self) -> None:
115
- """Handle escape key"""
116
- self.app.selected_option = None
117
- self.app.exit()
@@ -1,3 +0,0 @@
1
- from .selection import SelectionScreen
2
-
3
- __all__ = ['SelectionScreen']
janito/tui/screens/app.py DELETED
@@ -1 +0,0 @@
1
- # This file is intentionally left empty as functionality is moved to base.py and __init__.py
janito/version.py DELETED
@@ -1,23 +0,0 @@
1
- """Version management module for Janito."""
2
- from pathlib import Path
3
- from typing import Optional
4
- import tomli
5
- from importlib.metadata import version as pkg_version
6
-
7
- def get_version() -> str:
8
- """
9
- Get Janito version from package metadata or pyproject.toml.
10
-
11
- Returns:
12
- str: The version string
13
- """
14
- try:
15
- return pkg_version("janito")
16
- except Exception:
17
- # Fallback to pyproject.toml
18
- pyproject_path = Path(__file__).parent.parent / "pyproject.toml"
19
- if pyproject_path.exists():
20
- with open(pyproject_path, "rb") as f:
21
- pyproject_data = tomli.load(f)
22
- return pyproject_data.get("project", {}).get("version", "unknown")
23
- return "unknown"
@@ -1,6 +0,0 @@
1
- from .workset import Workset
2
-
3
- # Create and export singleton instance
4
- workset = Workset()
5
-
6
- __all__ = ['workset']
@@ -1,121 +0,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
- return f"{size_bytes:.1f} {unit}"