janito 0.5.0__py3-none-any.whl → 0.6.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.
- janito/__init__.py +0 -47
- janito/__main__.py +96 -15
- janito/agents/__init__.py +2 -8
- janito/agents/claudeai.py +3 -12
- janito/change/__init__.py +29 -16
- janito/change/__main__.py +0 -0
- janito/{analysis → change/analysis}/__init__.py +5 -15
- janito/change/analysis/__main__.py +7 -0
- janito/change/analysis/analyze.py +61 -0
- janito/change/analysis/formatting.py +78 -0
- janito/change/analysis/options.py +81 -0
- janito/{analysis → change/analysis}/prompts.py +35 -12
- janito/change/analysis/view/__init__.py +9 -0
- janito/change/analysis/view/terminal.py +171 -0
- janito/change/applier/__init__.py +5 -0
- janito/change/applier/file.py +58 -0
- janito/change/applier/main.py +156 -0
- janito/change/applier/text.py +245 -0
- janito/change/applier/workspace_dir.py +58 -0
- janito/change/core.py +131 -0
- janito/{changehistory.py → change/history.py} +12 -14
- janito/change/operations.py +7 -0
- janito/change/parser.py +289 -0
- janito/change/play.py +54 -0
- janito/change/preview.py +82 -0
- janito/change/prompts.py +126 -0
- janito/change/test.py +0 -0
- janito/change/validator.py +251 -0
- janito/{changeviewer → change/viewer}/__init__.py +3 -4
- janito/change/viewer/content.py +66 -0
- janito/{changeviewer → change/viewer}/diff.py +19 -4
- janito/change/viewer/pager.py +56 -0
- janito/change/viewer/panels.py +555 -0
- janito/change/viewer/styling.py +103 -0
- janito/{changeviewer → change/viewer}/themes.py +3 -5
- janito/clear_statement_parser/clear_statement_format.txt +328 -0
- janito/clear_statement_parser/examples.txt +326 -0
- janito/clear_statement_parser/models.py +104 -0
- janito/clear_statement_parser/parser.py +496 -0
- janito/cli/base.py +30 -0
- janito/cli/commands.py +30 -38
- janito/cli/functions.py +19 -194
- janito/cli/handlers/ask.py +22 -0
- janito/cli/handlers/demo.py +22 -0
- janito/cli/handlers/request.py +24 -0
- janito/cli/handlers/scan.py +9 -0
- janito/cli/history.py +61 -0
- janito/common.py +34 -3
- janito/config.py +71 -6
- janito/demo/__init__.py +4 -0
- janito/demo/data.py +13 -0
- janito/demo/mock_data.py +20 -0
- janito/demo/operations.py +45 -0
- janito/demo/runner.py +59 -0
- janito/demo/scenarios.py +32 -0
- janito/prompts.py +1 -80
- janito/qa.py +4 -3
- janito/search_replace/README.md +146 -0
- janito/search_replace/__init__.py +6 -0
- janito/search_replace/__main__.py +21 -0
- janito/search_replace/core.py +119 -0
- janito/search_replace/parser.py +52 -0
- janito/search_replace/play.py +61 -0
- janito/search_replace/replacer.py +36 -0
- janito/search_replace/searcher.py +299 -0
- janito/shell/__init__.py +39 -0
- janito/shell/bus.py +31 -0
- janito/shell/commands.py +195 -0
- janito/shell/handlers.py +122 -0
- janito/shell/history.py +20 -0
- janito/shell/processor.py +52 -0
- janito/tui/__init__.py +21 -0
- janito/tui/base.py +22 -0
- janito/tui/flows/__init__.py +5 -0
- janito/tui/flows/changes.py +65 -0
- janito/tui/flows/content.py +128 -0
- janito/tui/flows/selection.py +117 -0
- janito/tui/screens/__init__.py +3 -0
- janito/tui/screens/app.py +1 -0
- janito/workspace/__init__.py +7 -0
- janito/workspace/analysis.py +121 -0
- janito/workspace/manager.py +48 -0
- janito/workspace/scan.py +232 -0
- janito-0.6.0.dist-info/METADATA +185 -0
- janito-0.6.0.dist-info/RECORD +95 -0
- {janito-0.5.0.dist-info → janito-0.6.0.dist-info}/WHEEL +1 -1
- janito/_contextparser.py +0 -113
- janito/analysis/display.py +0 -149
- janito/analysis/options.py +0 -112
- janito/change/applier.py +0 -269
- janito/change/content.py +0 -62
- janito/change/indentation.py +0 -33
- janito/change/position.py +0 -169
- janito/changeviewer/panels.py +0 -268
- janito/changeviewer/styling.py +0 -59
- janito/console/__init__.py +0 -3
- janito/console/commands.py +0 -112
- janito/console/core.py +0 -62
- janito/console/display.py +0 -157
- janito/fileparser.py +0 -334
- janito/scan.py +0 -176
- janito/tests/test_fileparser.py +0 -26
- janito-0.5.0.dist-info/METADATA +0 -146
- janito-0.5.0.dist-info/RECORD +0 -45
- {janito-0.5.0.dist-info → janito-0.6.0.dist-info}/entry_points.txt +0 -0
- {janito-0.5.0.dist-info → janito-0.6.0.dist-info}/licenses/LICENSE +0 -0
janito/shell/handlers.py
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
"""Command handlers for Janito shell."""
|
2
|
+
from pathlib import Path
|
3
|
+
from rich.console import Console
|
4
|
+
from rich.table import Table
|
5
|
+
from prompt_toolkit import PromptSession
|
6
|
+
from prompt_toolkit.completion import PathCompleter
|
7
|
+
from prompt_toolkit.shortcuts import clear as ptk_clear
|
8
|
+
from janito.config import config
|
9
|
+
from janito.scan import collect_files_content
|
10
|
+
from janito.scan.analysis import analyze_workspace_content
|
11
|
+
from .registry import CommandRegistry, get_path_completer
|
12
|
+
|
13
|
+
def handle_request(args: str) -> None:
|
14
|
+
"""Handle a change request."""
|
15
|
+
if not args:
|
16
|
+
Console().print("[red]Error: Change request required[/red]")
|
17
|
+
return
|
18
|
+
from janito.cli.commands import handle_request as cli_handle_request
|
19
|
+
cli_handle_request(args)
|
20
|
+
|
21
|
+
def handle_exit(_: str) -> None:
|
22
|
+
"""Handle exit command."""
|
23
|
+
raise EOFError()
|
24
|
+
|
25
|
+
def handle_clear(_: str) -> None:
|
26
|
+
"""Handle clear command to clear terminal screen."""
|
27
|
+
ptk_clear()
|
28
|
+
|
29
|
+
def handle_ask(args: str) -> None:
|
30
|
+
"""Handle ask command."""
|
31
|
+
if not args:
|
32
|
+
Console().print("[red]Error: Question required[/red]")
|
33
|
+
return
|
34
|
+
from janito.cli.commands import handle_ask as cli_handle_ask
|
35
|
+
cli_handle_ask(args)
|
36
|
+
|
37
|
+
def handle_help(args: str) -> None:
|
38
|
+
"""Handle help command."""
|
39
|
+
console = Console()
|
40
|
+
registry = CommandRegistry()
|
41
|
+
|
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
|
+
return
|
48
|
+
|
49
|
+
table = Table(title="Available Commands")
|
50
|
+
table.add_column("Command", style="cyan")
|
51
|
+
table.add_column("Description")
|
52
|
+
|
53
|
+
for name, cmd in sorted(registry.get_commands().items()):
|
54
|
+
table.add_row(name, cmd.description)
|
55
|
+
|
56
|
+
console.print(table)
|
57
|
+
|
58
|
+
def handle_include(args: str) -> None:
|
59
|
+
"""Handle include command with path completion."""
|
60
|
+
console = Console()
|
61
|
+
session = PromptSession()
|
62
|
+
completer = PathCompleter()
|
63
|
+
|
64
|
+
# If no args provided, prompt with completion
|
65
|
+
if not args:
|
66
|
+
try:
|
67
|
+
args = session.prompt("Enter paths (space separated): ", completer=completer)
|
68
|
+
except (KeyboardInterrupt, EOFError):
|
69
|
+
return
|
70
|
+
|
71
|
+
paths = [p.strip() for p in args.split() if p.strip()]
|
72
|
+
if not paths:
|
73
|
+
console.print("[red]Error: At least one path required[/red]")
|
74
|
+
return
|
75
|
+
|
76
|
+
resolved_paths = []
|
77
|
+
for path_str in paths:
|
78
|
+
path = Path(path_str)
|
79
|
+
if not path.is_absolute():
|
80
|
+
path = config.workdir / path
|
81
|
+
resolved_paths.append(path.resolve())
|
82
|
+
|
83
|
+
config.set_include(resolved_paths)
|
84
|
+
content = collect_files_content(resolved_paths)
|
85
|
+
analyze_workspace_content(content)
|
86
|
+
|
87
|
+
console.print(f"[green]Updated include paths:[/green]")
|
88
|
+
for path in resolved_paths:
|
89
|
+
console.print(f" {path}")
|
90
|
+
|
91
|
+
def handle_rinclude(args: str) -> None:
|
92
|
+
"""Handle rinclude command with recursive path scanning."""
|
93
|
+
console = Console()
|
94
|
+
session = PromptSession()
|
95
|
+
completer = get_path_completer(only_directories=True)
|
96
|
+
|
97
|
+
if not args:
|
98
|
+
try:
|
99
|
+
args = session.prompt("Enter directory paths (space separated): ", completer=completer)
|
100
|
+
except (KeyboardInterrupt, EOFError):
|
101
|
+
return
|
102
|
+
|
103
|
+
paths = [p.strip() for p in args.split() if p.strip()]
|
104
|
+
if not paths:
|
105
|
+
console.print("[red]Error: At least one path required[/red]")
|
106
|
+
return
|
107
|
+
|
108
|
+
resolved_paths = []
|
109
|
+
for path_str in paths:
|
110
|
+
path = Path(path_str)
|
111
|
+
if not path.is_absolute():
|
112
|
+
path = config.workdir / path
|
113
|
+
resolved_paths.append(path.resolve())
|
114
|
+
|
115
|
+
config.set_recursive(resolved_paths)
|
116
|
+
config.set_include(resolved_paths)
|
117
|
+
content = collect_files_content(resolved_paths)
|
118
|
+
analyze_workspace_content(content)
|
119
|
+
|
120
|
+
console.print(f"[green]Updated recursive include paths:[/green]")
|
121
|
+
for path in resolved_paths:
|
122
|
+
console.print(f" {path}")
|
janito/shell/history.py
ADDED
@@ -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,52 @@
|
|
1
|
+
"""Command processor for Janito shell."""
|
2
|
+
from typing import Optional
|
3
|
+
from rich.console import Console
|
4
|
+
from .commands import CommandSystem, register_commands
|
5
|
+
|
6
|
+
class CommandProcessor:
|
7
|
+
"""Processes shell commands."""
|
8
|
+
|
9
|
+
def __init__(self):
|
10
|
+
self.console = Console()
|
11
|
+
self.workspace_content: Optional[str] = None
|
12
|
+
register_commands()
|
13
|
+
|
14
|
+
def process_command(self, command_line: str) -> None:
|
15
|
+
"""Process a command line input."""
|
16
|
+
command_line = command_line.strip()
|
17
|
+
if not command_line:
|
18
|
+
return
|
19
|
+
|
20
|
+
parts = command_line.split(maxsplit=1)
|
21
|
+
cmd = parts[0].lower()
|
22
|
+
args = parts[1] if len(parts) > 1 else ""
|
23
|
+
|
24
|
+
system = CommandSystem()
|
25
|
+
if command := system.get_command(cmd):
|
26
|
+
# Special handling for /rinc command to support interactive completion
|
27
|
+
if cmd in ["/rinc", "/rinclude"] and args:
|
28
|
+
from prompt_toolkit.completion import PathCompleter
|
29
|
+
from prompt_toolkit.document import Document
|
30
|
+
|
31
|
+
completer = PathCompleter(only_directories=True)
|
32
|
+
doc = Document(args)
|
33
|
+
completions = list(completer.get_completions(doc, None))
|
34
|
+
|
35
|
+
if completions:
|
36
|
+
if len(completions) == 1:
|
37
|
+
# Single completion - use it
|
38
|
+
args = completions[0].text
|
39
|
+
else:
|
40
|
+
# Show available completions
|
41
|
+
self.console.print("\nAvailable directories:")
|
42
|
+
for comp in completions:
|
43
|
+
self.console.print(f" {comp.text}")
|
44
|
+
return
|
45
|
+
|
46
|
+
command.handler(args)
|
47
|
+
else:
|
48
|
+
# Treat as request command
|
49
|
+
if request_cmd := system.get_command("/request"):
|
50
|
+
request_cmd.handler(command_line)
|
51
|
+
else:
|
52
|
+
self.console.print("[red]Error: Request command not registered[/red]")
|
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,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 @@
|
|
1
|
+
# This file is intentionally left empty as functionality is moved to base.py and __init__.py
|
@@ -0,0 +1,7 @@
|
|
1
|
+
from .manager import WorkspaceManager
|
2
|
+
from .scan import preview_scan, collect_files_content, is_dir_empty
|
3
|
+
|
4
|
+
# Create singleton instance
|
5
|
+
workspace = WorkspaceManager.get_instance()
|
6
|
+
|
7
|
+
__all__ = ['workspace', 'preview_scan', 'collect_files_content', 'is_dir_empty']
|