janito 0.5.0__py3-none-any.whl → 0.7.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 (110) hide show
  1. janito/__init__.py +0 -47
  2. janito/__main__.py +105 -17
  3. janito/agents/__init__.py +9 -9
  4. janito/agents/agent.py +10 -3
  5. janito/agents/claudeai.py +15 -34
  6. janito/agents/openai.py +5 -1
  7. janito/change/__init__.py +29 -16
  8. janito/change/__main__.py +0 -0
  9. janito/{analysis → change/analysis}/__init__.py +5 -15
  10. janito/change/analysis/__main__.py +7 -0
  11. janito/change/analysis/analyze.py +62 -0
  12. janito/change/analysis/formatting.py +78 -0
  13. janito/change/analysis/options.py +81 -0
  14. janito/{analysis → change/analysis}/prompts.py +33 -18
  15. janito/change/analysis/view/__init__.py +9 -0
  16. janito/change/analysis/view/terminal.py +181 -0
  17. janito/change/applier/__init__.py +5 -0
  18. janito/change/applier/file.py +58 -0
  19. janito/change/applier/main.py +156 -0
  20. janito/change/applier/text.py +247 -0
  21. janito/change/applier/workspace_dir.py +58 -0
  22. janito/change/core.py +124 -0
  23. janito/{changehistory.py → change/history.py} +12 -14
  24. janito/change/operations.py +7 -0
  25. janito/change/parser.py +287 -0
  26. janito/change/play.py +54 -0
  27. janito/change/preview.py +82 -0
  28. janito/change/prompts.py +121 -0
  29. janito/change/test.py +0 -0
  30. janito/change/validator.py +269 -0
  31. janito/{changeviewer → change/viewer}/__init__.py +3 -4
  32. janito/change/viewer/content.py +66 -0
  33. janito/{changeviewer → change/viewer}/diff.py +19 -4
  34. janito/change/viewer/panels.py +533 -0
  35. janito/change/viewer/styling.py +114 -0
  36. janito/{changeviewer → change/viewer}/themes.py +3 -5
  37. janito/clear_statement_parser/clear_statement_format.txt +328 -0
  38. janito/clear_statement_parser/examples.txt +326 -0
  39. janito/clear_statement_parser/models.py +104 -0
  40. janito/clear_statement_parser/parser.py +496 -0
  41. janito/cli/base.py +30 -0
  42. janito/cli/commands.py +75 -40
  43. janito/cli/functions.py +19 -194
  44. janito/cli/history.py +61 -0
  45. janito/common.py +65 -8
  46. janito/config.py +70 -5
  47. janito/demo/__init__.py +4 -0
  48. janito/demo/data.py +13 -0
  49. janito/demo/mock_data.py +20 -0
  50. janito/demo/operations.py +45 -0
  51. janito/demo/runner.py +59 -0
  52. janito/demo/scenarios.py +32 -0
  53. janito/prompt.py +36 -0
  54. janito/qa.py +6 -14
  55. janito/search_replace/README.md +192 -0
  56. janito/search_replace/__init__.py +7 -0
  57. janito/search_replace/__main__.py +21 -0
  58. janito/search_replace/core.py +120 -0
  59. janito/search_replace/logger.py +35 -0
  60. janito/search_replace/parser.py +52 -0
  61. janito/search_replace/play.py +61 -0
  62. janito/search_replace/replacer.py +36 -0
  63. janito/search_replace/searcher.py +411 -0
  64. janito/search_replace/strategy_result.py +10 -0
  65. janito/shell/__init__.py +38 -0
  66. janito/shell/bus.py +31 -0
  67. janito/shell/commands.py +136 -0
  68. janito/shell/history.py +20 -0
  69. janito/shell/processor.py +32 -0
  70. janito/shell/prompt.py +48 -0
  71. janito/shell/registry.py +60 -0
  72. janito/tui/__init__.py +21 -0
  73. janito/tui/base.py +22 -0
  74. janito/tui/flows/__init__.py +5 -0
  75. janito/tui/flows/changes.py +65 -0
  76. janito/tui/flows/content.py +128 -0
  77. janito/tui/flows/selection.py +117 -0
  78. janito/tui/screens/__init__.py +3 -0
  79. janito/tui/screens/app.py +1 -0
  80. janito/workspace/__init__.py +6 -0
  81. janito/workspace/analysis.py +121 -0
  82. janito/workspace/show.py +141 -0
  83. janito/workspace/stats.py +43 -0
  84. janito/workspace/types.py +98 -0
  85. janito/workspace/workset.py +108 -0
  86. janito/workspace/workspace.py +114 -0
  87. janito-0.7.0.dist-info/METADATA +167 -0
  88. janito-0.7.0.dist-info/RECORD +96 -0
  89. {janito-0.5.0.dist-info → janito-0.7.0.dist-info}/WHEEL +1 -1
  90. janito/_contextparser.py +0 -113
  91. janito/analysis/display.py +0 -149
  92. janito/analysis/options.py +0 -112
  93. janito/change/applier.py +0 -269
  94. janito/change/content.py +0 -62
  95. janito/change/indentation.py +0 -33
  96. janito/change/position.py +0 -169
  97. janito/changeviewer/panels.py +0 -268
  98. janito/changeviewer/styling.py +0 -59
  99. janito/console/__init__.py +0 -3
  100. janito/console/commands.py +0 -112
  101. janito/console/core.py +0 -62
  102. janito/console/display.py +0 -157
  103. janito/fileparser.py +0 -334
  104. janito/prompts.py +0 -81
  105. janito/scan.py +0 -176
  106. janito/tests/test_fileparser.py +0 -26
  107. janito-0.5.0.dist-info/METADATA +0 -146
  108. janito-0.5.0.dist-info/RECORD +0 -45
  109. {janito-0.5.0.dist-info → janito-0.7.0.dist-info}/entry_points.txt +0 -0
  110. {janito-0.5.0.dist-info → janito-0.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,136 @@
1
+ """Command system for Janito shell."""
2
+ from rich.console import Console
3
+ from rich.table import Table
4
+ from prompt_toolkit import PromptSession
5
+ from prompt_toolkit.shortcuts import clear as ptk_clear
6
+ from prompt_toolkit.completion import PathCompleter
7
+ from prompt_toolkit.document import Document
8
+ from pathlib import Path
9
+ from janito.config import config
10
+ from janito.workspace import workset # Updated import
11
+ from janito.workspace.analysis import analyze_workspace_content
12
+ from .registry import CommandRegistry, Command, get_path_completer
13
+
14
+ def handle_request(args: str) -> None:
15
+ """Handle a change request."""
16
+ if not args:
17
+ Console().print("[red]Error: Change request required[/red]")
18
+ return
19
+ from janito.cli.commands import handle_request as cli_handle_request
20
+ cli_handle_request(args)
21
+
22
+ def handle_exit(_: str) -> None:
23
+ """Handle exit command."""
24
+ raise EOFError()
25
+
26
+ def handle_clear(_: str) -> None:
27
+ """Handle clear command."""
28
+ ptk_clear()
29
+
30
+ def handle_ask(args: str) -> None:
31
+ """Handle ask command."""
32
+ if not args:
33
+ Console().print("[red]Error: Question required[/red]")
34
+ return
35
+ from janito.cli.commands import handle_ask as cli_handle_ask
36
+ cli_handle_ask(args)
37
+
38
+ def handle_help(args: str) -> None:
39
+ """Handle help command."""
40
+ console = Console()
41
+ registry = CommandRegistry()
42
+ command = args.strip() if args else None
43
+ if command and (cmd := registry.get_command(command)):
44
+ console.print(f"\n[bold]{command}[/bold]: {cmd.description}")
45
+ if cmd.usage:
46
+ console.print(f"Usage: {cmd.usage}")
47
+ else:
48
+ table = Table(title="Available Commands")
49
+ table.add_column("Command", style="cyan")
50
+ table.add_column("Description")
51
+
52
+ for name, cmd in sorted(registry.get_commands().items()):
53
+ table.add_row(name, cmd.description)
54
+
55
+ console.print(table)
56
+
57
+ def handle_include(args: str) -> None:
58
+ """Handle include command."""
59
+ console = Console()
60
+ session = PromptSession()
61
+
62
+ if not args:
63
+ try:
64
+ args = session.prompt("Enter paths (space separated): ")
65
+ except (KeyboardInterrupt, EOFError):
66
+ return
67
+
68
+ paths = [p.strip() for p in args.split() if p.strip()]
69
+ if not paths:
70
+ console.print("[red]Error: At least one path required[/red]")
71
+ return
72
+
73
+ resolved_paths = []
74
+ for path_str in paths:
75
+ path = Path(path_str)
76
+ if not path.is_absolute():
77
+ path = config.workspace_dir / path
78
+ resolved_paths.append(path.resolve())
79
+
80
+ workset.include(resolved_paths)
81
+ workset.show()
82
+
83
+ console.print("[green]Updated include paths:[/green]")
84
+ for path in resolved_paths:
85
+ console.print(f" {path}")
86
+
87
+ def handle_rinclude(args: str) -> None:
88
+ """Handle recursive include command."""
89
+ console = Console()
90
+ session = PromptSession()
91
+ completer = get_path_completer(only_directories=True)
92
+
93
+ if not args:
94
+ try:
95
+ args = session.prompt("Enter directory paths (space separated): ", completer=completer)
96
+ except (KeyboardInterrupt, EOFError):
97
+ return
98
+
99
+ paths = [p.strip() for p in args.split() if p.strip()]
100
+ if not paths:
101
+ console.print("[red]Error: At least one path required[/red]")
102
+ return
103
+
104
+ resolved_paths = []
105
+ for path_str in paths:
106
+ path = Path(path_str)
107
+ if not path.is_absolute():
108
+ path = config.workspace_dir / path
109
+ resolved_paths.append(path.resolve())
110
+
111
+ workset.recursive(resolved_paths)
112
+ workset.include(resolved_paths) # Add recursive paths to include paths
113
+ workset.refresh()
114
+ workset.show()
115
+
116
+ console.print("[green]Updated recursive include paths:[/green]")
117
+ for path in resolved_paths:
118
+ console.print(f" {path}")
119
+
120
+ def register_commands(registry: CommandRegistry) -> None:
121
+ """Register all available commands."""
122
+ # Register main commands
123
+ registry.register(Command("/clear", "Clear the terminal screen", None, handle_clear))
124
+ registry.register(Command("/request", "Submit a change request", "/request <change request text>", handle_request))
125
+ registry.register(Command("/ask", "Ask a question about the codebase", "/ask <question>", handle_ask))
126
+ registry.register(Command("/quit", "Exit the shell", None, handle_exit))
127
+ registry.register(Command("/help", "Show help for commands", "/help [command]", handle_help))
128
+ registry.register(Command("/include", "Set paths to include in analysis", "/include <path1> [path2 ...]", handle_include, get_path_completer()))
129
+ registry.register(Command("/rinclude", "Set paths to include recursively", "/rinclude <path1> [path2 ...]", handle_rinclude, get_path_completer(True)))
130
+
131
+ # Register aliases
132
+ registry.register_alias("clear", "/clear")
133
+ registry.register_alias("quit", "/quit")
134
+ registry.register_alias("help", "/help")
135
+ registry.register_alias("/inc", "/include")
136
+ registry.register_alias("/rinc", "/rinclude")
@@ -0,0 +1,20 @@
1
+ """Command history management for Janito shell."""
2
+ from pathlib import Path
3
+ from typing import List, Optional
4
+ from prompt_toolkit.history import FileHistory
5
+
6
+ class CommandHistory:
7
+ """Manages shell command history."""
8
+
9
+ def __init__(self, history_file: Optional[Path] = None):
10
+ if history_file is None:
11
+ history_file = Path.home() / ".janito_history"
12
+ self.history = FileHistory(str(history_file))
13
+
14
+ def add(self, command: str) -> None:
15
+ """Add a command to history."""
16
+ self.history.append_string(command)
17
+
18
+ def get_last(self, n: int = 10) -> List[str]:
19
+ """Get last n commands from history."""
20
+ return list(self.history.get_strings())[-n:]
@@ -0,0 +1,32 @@
1
+ """Command processor for Janito shell."""
2
+ from typing import Optional
3
+ from rich.console import Console
4
+ from .registry import CommandRegistry
5
+
6
+ class CommandProcessor:
7
+ """Processes shell commands."""
8
+
9
+ def __init__(self, registry: CommandRegistry) -> None:
10
+ """Initialize command processor with registry."""
11
+ super().__init__()
12
+ self.console = Console()
13
+ self.registry = registry
14
+
15
+ def process_command(self, command_line: str) -> None:
16
+ """Process a command line input."""
17
+ command_line = command_line.strip()
18
+ if not command_line:
19
+ return
20
+
21
+ parts = command_line.split(maxsplit=1)
22
+ cmd = parts[0].lower()
23
+ args = parts[1] if len(parts) > 1 else ""
24
+
25
+ if command := self.registry.get_command(cmd):
26
+ command.handler(args)
27
+ else:
28
+ # Treat as request command
29
+ if request_cmd := self.registry.get_command("/request"):
30
+ request_cmd.handler(command_line)
31
+ else:
32
+ self.console.print("[red]Error: Request command not registered[/red]")
janito/shell/prompt.py ADDED
@@ -0,0 +1,48 @@
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
+ )
@@ -0,0 +1,60 @@
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 ADDED
@@ -0,0 +1,21 @@
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 ADDED
@@ -0,0 +1,22 @@
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
@@ -0,0 +1,5 @@
1
+ from .selection import SelectionFlow
2
+ from .content import ContentFlow
3
+ from .changes import ChangesFlow
4
+
5
+ __all__ = ['SelectionFlow', 'ContentFlow', 'ChangesFlow']
@@ -0,0 +1,65 @@
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()
@@ -0,0 +1,128 @@
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()
@@ -0,0 +1,117 @@
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()
@@ -0,0 +1,3 @@
1
+ from .selection import SelectionScreen
2
+
3
+ __all__ = ['SelectionScreen']
@@ -0,0 +1 @@
1
+ # This file is intentionally left empty as functionality is moved to base.py and __init__.py
@@ -0,0 +1,6 @@
1
+ from .workset import Workset
2
+
3
+ # Create and export singleton instance
4
+ workset = Workset()
5
+
6
+ __all__ = ['workset']