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.
- janito/__init__.py +0 -47
- janito/__main__.py +105 -17
- janito/agents/__init__.py +9 -9
- janito/agents/agent.py +10 -3
- janito/agents/claudeai.py +15 -34
- janito/agents/openai.py +5 -1
- 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 +62 -0
- janito/change/analysis/formatting.py +78 -0
- janito/change/analysis/options.py +81 -0
- janito/{analysis → change/analysis}/prompts.py +33 -18
- janito/change/analysis/view/__init__.py +9 -0
- janito/change/analysis/view/terminal.py +181 -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 +247 -0
- janito/change/applier/workspace_dir.py +58 -0
- janito/change/core.py +124 -0
- janito/{changehistory.py → change/history.py} +12 -14
- janito/change/operations.py +7 -0
- janito/change/parser.py +287 -0
- janito/change/play.py +54 -0
- janito/change/preview.py +82 -0
- janito/change/prompts.py +121 -0
- janito/change/test.py +0 -0
- janito/change/validator.py +269 -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/panels.py +533 -0
- janito/change/viewer/styling.py +114 -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 +75 -40
- janito/cli/functions.py +19 -194
- janito/cli/history.py +61 -0
- janito/common.py +65 -8
- janito/config.py +70 -5
- 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/prompt.py +36 -0
- janito/qa.py +6 -14
- janito/search_replace/README.md +192 -0
- janito/search_replace/__init__.py +7 -0
- janito/search_replace/__main__.py +21 -0
- janito/search_replace/core.py +120 -0
- janito/search_replace/logger.py +35 -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 +411 -0
- janito/search_replace/strategy_result.py +10 -0
- janito/shell/__init__.py +38 -0
- janito/shell/bus.py +31 -0
- janito/shell/commands.py +136 -0
- janito/shell/history.py +20 -0
- janito/shell/processor.py +32 -0
- janito/shell/prompt.py +48 -0
- janito/shell/registry.py +60 -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 +6 -0
- janito/workspace/analysis.py +121 -0
- janito/workspace/show.py +141 -0
- janito/workspace/stats.py +43 -0
- janito/workspace/types.py +98 -0
- janito/workspace/workset.py +108 -0
- janito/workspace/workspace.py +114 -0
- janito-0.7.0.dist-info/METADATA +167 -0
- janito-0.7.0.dist-info/RECORD +96 -0
- {janito-0.5.0.dist-info → janito-0.7.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/prompts.py +0 -81
- 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.7.0.dist-info}/entry_points.txt +0 -0
- {janito-0.5.0.dist-info → janito-0.7.0.dist-info}/licenses/LICENSE +0 -0
janito/cli/commands.py
CHANGED
@@ -1,53 +1,88 @@
|
|
1
1
|
from pathlib import Path
|
2
2
|
from typing import Optional, List
|
3
3
|
from rich.console import Console
|
4
|
+
from rich.text import Text
|
4
5
|
|
5
|
-
from janito.agents import AIAgent
|
6
|
-
from janito.
|
6
|
+
from janito.agents import AIAgent, agent
|
7
|
+
from janito.workspace import workset
|
7
8
|
from janito.config import config
|
9
|
+
from janito.change.core import process_change_request
|
10
|
+
from janito.change.play import play_saved_changes
|
11
|
+
from janito.cli.history import save_to_history
|
12
|
+
from janito.qa import ask_question, display_answer
|
13
|
+
from janito.demo import DemoRunner
|
14
|
+
from janito.demo.data import get_demo_scenarios
|
8
15
|
|
9
|
-
|
10
|
-
process_question, replay_saved_file, ensure_workdir,
|
11
|
-
review_text, save_to_file, collect_files_content,
|
12
|
-
build_request_analysis_prompt, progress_send_message,
|
13
|
-
format_analysis, handle_option_selection, read_stdin
|
14
|
-
)
|
16
|
+
console = Console()
|
15
17
|
|
18
|
+
def handle_ask(question: str):
|
19
|
+
"""Process a question about the codebase"""
|
20
|
+
|
21
|
+
if config.tui:
|
22
|
+
answer = ask_question(question)
|
23
|
+
from janito.tui import TuiApp
|
24
|
+
app = TuiApp(content=answer)
|
25
|
+
app.run()
|
26
|
+
else:
|
27
|
+
answer = ask_question(question)
|
28
|
+
display_answer(answer)
|
16
29
|
|
17
|
-
def
|
18
|
-
"""Ask a question about the codebase"""
|
19
|
-
workdir = ensure_workdir(workdir)
|
20
|
-
if question == ".":
|
21
|
-
question = read_stdin()
|
22
|
-
process_question(question, workdir, include, raw, agent)
|
23
|
-
|
24
|
-
def handle_scan(paths_to_scan: List[Path], workdir: Path):
|
30
|
+
def handle_scan():
|
25
31
|
"""Preview files that would be analyzed"""
|
26
|
-
|
27
|
-
|
32
|
+
workset.show()
|
33
|
+
|
34
|
+
def handle_play(filepath: Path):
|
35
|
+
"""Replay a saved changes or debug file"""
|
36
|
+
play_saved_changes(filepath)
|
37
|
+
|
38
|
+
def is_dir_empty(path: Path) -> bool:
|
39
|
+
"""Check if directory is empty or only contains empty directories."""
|
40
|
+
if not path.is_dir():
|
41
|
+
return False
|
28
42
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
43
|
+
for item in path.iterdir():
|
44
|
+
if item.name.startswith(('.', '__pycache__')):
|
45
|
+
continue
|
46
|
+
if item.is_file():
|
47
|
+
return False
|
48
|
+
if item.is_dir() and not is_dir_empty(item):
|
49
|
+
return False
|
50
|
+
return True
|
33
51
|
|
34
|
-
def handle_request(request: str,
|
52
|
+
def handle_request(request: str, preview_only: bool = False):
|
35
53
|
"""Process modification request"""
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
is_empty = is_dir_empty(workdir)
|
40
|
-
if is_empty and not include:
|
41
|
-
console = Console()
|
54
|
+
is_empty = is_dir_empty(config.workspace_dir)
|
55
|
+
if is_empty and not config.include:
|
42
56
|
console.print("\n[bold blue]Empty directory - will create new files as needed[/bold blue]")
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
57
|
+
|
58
|
+
success, history_file = process_change_request(request, preview_only)
|
59
|
+
|
60
|
+
if success and history_file and config.verbose:
|
61
|
+
try:
|
62
|
+
rel_path = history_file.relative_to(config.workspace_dir)
|
63
|
+
console.print(f"\nChanges saved to: ./{rel_path}")
|
64
|
+
except ValueError:
|
65
|
+
console.print(f"\nChanges saved to: {history_file}")
|
66
|
+
elif not success:
|
67
|
+
console.print("[red]Failed to process change request[/red]")
|
68
|
+
|
69
|
+
# Save request and response to history
|
70
|
+
if agent.last_response:
|
71
|
+
save_to_history(request, agent.last_response)
|
72
|
+
|
73
|
+
def handle_demo():
|
74
|
+
"""Run demo scenarios"""
|
75
|
+
runner = DemoRunner()
|
76
|
+
|
77
|
+
# Add predefined scenarios
|
78
|
+
for scenario in get_demo_scenarios():
|
79
|
+
runner.add_scenario(scenario)
|
80
|
+
|
81
|
+
# Preview and run scenarios
|
82
|
+
console.print("\n[bold cyan]Demo Scenarios Preview:[/bold cyan]")
|
83
|
+
runner.preview_changes()
|
84
|
+
|
85
|
+
console.print("\n[bold cyan]Running Demo Scenarios:[/bold cyan]")
|
86
|
+
runner.run_all()
|
87
|
+
|
88
|
+
console.print("\n[green]Demo completed successfully![/green]")
|
janito/cli/functions.py
CHANGED
@@ -1,27 +1,22 @@
|
|
1
|
-
|
1
|
+
import sys
|
2
|
+
import tempfile
|
3
|
+
from datetime import datetime, timezone
|
2
4
|
from pathlib import Path
|
5
|
+
from typing import List, Optional
|
6
|
+
|
7
|
+
import typer
|
3
8
|
from rich.console import Console
|
4
|
-
from rich.
|
9
|
+
from rich.markdown import Markdown
|
5
10
|
from rich.panel import Panel
|
11
|
+
from rich.prompt import Confirm, Prompt
|
6
12
|
from rich.text import Text
|
7
|
-
from rich.markdown import Markdown
|
8
|
-
from datetime import datetime, timezone
|
9
|
-
import tempfile
|
10
|
-
import typer
|
11
|
-
import sys
|
12
13
|
|
13
14
|
from janito.agents import AIAgent
|
15
|
+
from janito.common import progress_send_message
|
14
16
|
from janito.config import config
|
15
|
-
from janito.scan import collect_files_content, show_content_stats
|
16
|
-
from janito.analysis import (
|
17
|
-
format_analysis, build_request_analysis_prompt,
|
18
|
-
parse_analysis_options, get_history_file_type, AnalysisOption
|
19
|
-
)
|
20
17
|
from janito.qa import ask_question, display_answer
|
21
|
-
from janito.
|
22
|
-
|
23
|
-
from janito.fileparser import parse_block_changes
|
24
|
-
from janito.change.applier import preview_and_apply_changes
|
18
|
+
from janito.workspace import collect_files_content
|
19
|
+
|
25
20
|
|
26
21
|
def prompt_user(message: str, choices: List[str] = None) -> str:
|
27
22
|
"""Display a simple user prompt with optional choices"""
|
@@ -48,9 +43,9 @@ def get_option_selection() -> str:
|
|
48
43
|
|
49
44
|
console.print("[red]Please enter a valid letter or 'M'[/red]")
|
50
45
|
|
51
|
-
def get_change_history_path(
|
46
|
+
def get_change_history_path() -> Path:
|
52
47
|
"""Create and return the changes history directory path"""
|
53
|
-
changes_history_dir =
|
48
|
+
changes_history_dir = config.workspace_dir / '.janito' / 'change_history'
|
54
49
|
changes_history_dir.mkdir(parents=True, exist_ok=True)
|
55
50
|
return changes_history_dir
|
56
51
|
|
@@ -65,9 +60,9 @@ def save_prompt_to_file(prompt: str) -> Path:
|
|
65
60
|
temp_path.write_text(prompt)
|
66
61
|
return temp_path
|
67
62
|
|
68
|
-
def save_to_file(content: str, prefix: str
|
63
|
+
def save_to_file(content: str, prefix: str) -> Path:
|
69
64
|
"""Save content to a timestamped file in changes history directory"""
|
70
|
-
changes_history_dir = get_change_history_path(
|
65
|
+
changes_history_dir = get_change_history_path()
|
71
66
|
timestamp = get_timestamp()
|
72
67
|
filename = f"{timestamp}_{prefix}.txt"
|
73
68
|
file_path = changes_history_dir / filename
|
@@ -100,107 +95,6 @@ def modify_request(request: str) -> str:
|
|
100
95
|
console.print("\n[yellow]Modification cancelled, keeping original request[/yellow]")
|
101
96
|
return request
|
102
97
|
|
103
|
-
def format_option_text(option: AnalysisOption) -> str:
|
104
|
-
"""Format an AnalysisOption into a string representation"""
|
105
|
-
option_text = f"Option {option.letter}:\n"
|
106
|
-
option_text += f"Summary: {option.summary}\n\n"
|
107
|
-
option_text += "Description:\n"
|
108
|
-
for item in option.description_items:
|
109
|
-
option_text += f"- {item}\n"
|
110
|
-
option_text += "\nAffected files:\n"
|
111
|
-
for file in option.affected_files:
|
112
|
-
option_text += f"- {file}\n"
|
113
|
-
return option_text
|
114
|
-
|
115
|
-
def handle_option_selection(initial_response: str, request: str, raw: bool = False, workdir: Optional[Path] = None, include: Optional[List[Path]] = None) -> None:
|
116
|
-
"""Handle option selection and implementation details"""
|
117
|
-
options = parse_analysis_options(initial_response)
|
118
|
-
if not options:
|
119
|
-
console = Console()
|
120
|
-
console.print("[red]No valid options found in the response[/red]")
|
121
|
-
return
|
122
|
-
|
123
|
-
while True:
|
124
|
-
option = get_option_selection()
|
125
|
-
|
126
|
-
if option == 'M':
|
127
|
-
# Use the new modify_request function for better UX
|
128
|
-
new_request = modify_request(request)
|
129
|
-
if new_request == request:
|
130
|
-
continue
|
131
|
-
|
132
|
-
# Rerun analysis with new request
|
133
|
-
# Only scan files listed in the selected option's affected_files
|
134
|
-
selected_option = options[option]
|
135
|
-
files_to_scan = selected_option.affected_files
|
136
|
-
|
137
|
-
# Convert relative paths to absolute paths
|
138
|
-
absolute_paths = []
|
139
|
-
for file_path in files_to_scan:
|
140
|
-
# Remove (new) suffix if present
|
141
|
-
clean_path = file_path.split(' (')[0].strip()
|
142
|
-
path = workdir / clean_path if workdir else Path(clean_path)
|
143
|
-
if path.exists():
|
144
|
-
absolute_paths.append(path)
|
145
|
-
|
146
|
-
files_content = collect_files_content(absolute_paths, workdir) if absolute_paths else ""
|
147
|
-
show_content_stats(files_content)
|
148
|
-
initial_prompt = build_request_analysis_prompt(files_content, new_request)
|
149
|
-
initial_response = progress_send_message(initial_prompt)
|
150
|
-
save_to_file(initial_response, 'analysis', workdir)
|
151
|
-
|
152
|
-
format_analysis(initial_response, raw)
|
153
|
-
options = parse_analysis_options(initial_response)
|
154
|
-
if not options:
|
155
|
-
console = Console()
|
156
|
-
console.print("[red]No valid options found in the response[/red]")
|
157
|
-
return
|
158
|
-
continue
|
159
|
-
|
160
|
-
if not validate_option_letter(option, options):
|
161
|
-
console = Console()
|
162
|
-
console.print(f"[red]Invalid option '{option}'. Valid options are: {', '.join(options.keys())} or 'M' to modify[/red]")
|
163
|
-
continue
|
164
|
-
|
165
|
-
break
|
166
|
-
|
167
|
-
# Only scan files listed in the selected option's affected_files
|
168
|
-
selected_option = options[option]
|
169
|
-
files_to_scan = selected_option.affected_files
|
170
|
-
|
171
|
-
# Convert relative paths to absolute paths
|
172
|
-
absolute_paths = []
|
173
|
-
for file_path in files_to_scan:
|
174
|
-
# Remove (new) suffix if present
|
175
|
-
clean_path = file_path.split(' (')[0].strip()
|
176
|
-
path = workdir / clean_path if workdir else Path(clean_path)
|
177
|
-
if path.exists():
|
178
|
-
absolute_paths.append(path)
|
179
|
-
|
180
|
-
files_content = collect_files_content(absolute_paths, workdir) if absolute_paths else ""
|
181
|
-
show_content_stats(files_content)
|
182
|
-
|
183
|
-
# Format the selected option before building prompt
|
184
|
-
selected_option = options[option]
|
185
|
-
option_text = format_option_text(selected_option)
|
186
|
-
|
187
|
-
selected_prompt = build_selected_option_prompt(option_text, request, files_content)
|
188
|
-
prompt_file = save_to_file(selected_prompt, 'selected', workdir)
|
189
|
-
if config.verbose:
|
190
|
-
print(f"\nSelected prompt saved to: {prompt_file}")
|
191
|
-
|
192
|
-
selected_response = progress_send_message(selected_prompt)
|
193
|
-
changes_file = save_to_file(selected_response, 'changes', workdir)
|
194
|
-
|
195
|
-
if config.verbose:
|
196
|
-
try:
|
197
|
-
rel_path = changes_file.relative_to(workdir)
|
198
|
-
print(f"\nChanges saved to: ./{rel_path}")
|
199
|
-
except ValueError:
|
200
|
-
print(f"\nChanges saved to: {changes_file}")
|
201
|
-
|
202
|
-
changes = parse_block_changes(selected_response)
|
203
|
-
preview_and_apply_changes(changes, workdir, config.test_cmd)
|
204
98
|
|
205
99
|
def read_stdin() -> str:
|
206
100
|
"""Read input from stdin until EOF"""
|
@@ -208,79 +102,10 @@ def read_stdin() -> str:
|
|
208
102
|
console.print("[dim]Enter your input (press Ctrl+D when finished):[/dim]")
|
209
103
|
return sys.stdin.read().strip()
|
210
104
|
|
211
|
-
def
|
212
|
-
"""Process a saved prompt file and display the response"""
|
213
|
-
if not filepath.exists():
|
214
|
-
raise FileNotFoundError(f"File {filepath} not found")
|
215
|
-
|
216
|
-
content = filepath.read_text()
|
217
|
-
|
218
|
-
# Add debug output of file content
|
219
|
-
if config.debug:
|
220
|
-
console = Console()
|
221
|
-
console.print("\n[bold blue]Debug: File Content[/bold blue]")
|
222
|
-
console.print(Panel(
|
223
|
-
content,
|
224
|
-
title=f"Content of {filepath.name}",
|
225
|
-
border_style="blue",
|
226
|
-
padding=(1, 2)
|
227
|
-
))
|
228
|
-
console.print()
|
229
|
-
|
230
|
-
file_type = get_history_file_type(filepath)
|
231
|
-
|
232
|
-
if file_type == 'changes':
|
233
|
-
changes = parse_block_changes(content)
|
234
|
-
success = preview_and_apply_changes(changes, workdir, config.test_cmd)
|
235
|
-
if not success:
|
236
|
-
raise typer.Exit(1)
|
237
|
-
elif file_type == 'analysis':
|
238
|
-
format_analysis(content, raw)
|
239
|
-
handle_option_selection(content, content, raw, workdir)
|
240
|
-
elif file_type == 'selected':
|
241
|
-
if raw:
|
242
|
-
console = Console()
|
243
|
-
console.print("\n=== Prompt Content ===")
|
244
|
-
console.print(content)
|
245
|
-
console.print("=== End Prompt Content ===\n")
|
246
|
-
|
247
|
-
response = progress_send_message(content)
|
248
|
-
changes_file = save_to_file(response, 'changes_', workdir)
|
249
|
-
print(f"\nChanges saved to: {changes_file}")
|
250
|
-
|
251
|
-
changes = parse_block_changes(response)
|
252
|
-
preview_and_apply_changes(changes, workdir, config.test_cmd)
|
253
|
-
else:
|
254
|
-
response = progress_send_message(content)
|
255
|
-
format_analysis(response, raw)
|
256
|
-
|
257
|
-
def process_question(question: str, workdir: Path, include: List[Path], raw: bool, agent: AIAgent) -> None:
|
105
|
+
def process_question(question: str) -> None:
|
258
106
|
"""Process a question about the codebase"""
|
259
|
-
paths_to_scan = [
|
260
|
-
|
261
|
-
paths_to_scan.extend(include)
|
262
|
-
files_content = collect_files_content(paths_to_scan, workdir)
|
107
|
+
paths_to_scan = [config.workspace_dir] if not config.include else config.include
|
108
|
+
files_content = collect_files_content(paths_to_scan)
|
263
109
|
answer = ask_question(question, files_content)
|
264
|
-
display_answer(answer
|
110
|
+
display_answer(answer)
|
265
111
|
|
266
|
-
def ensure_workdir(workdir: Path) -> Path:
|
267
|
-
"""Ensure working directory exists, prompt for creation if it doesn't"""
|
268
|
-
if workdir.exists():
|
269
|
-
return workdir.absolute()
|
270
|
-
|
271
|
-
console = Console()
|
272
|
-
console.print(f"\n[yellow]Directory does not exist:[/yellow] {workdir}")
|
273
|
-
if Confirm.ask("Create directory?"):
|
274
|
-
workdir.mkdir(parents=True)
|
275
|
-
console.print(f"[green]Created directory:[/green] {workdir}")
|
276
|
-
return workdir.absolute()
|
277
|
-
raise typer.Exit(1)
|
278
|
-
|
279
|
-
def review_text(text: str, raw: bool = False) -> None:
|
280
|
-
"""Review the provided text using Claude"""
|
281
|
-
console = Console()
|
282
|
-
response = progress_send_message(f"Please review this text and provide feedback:\n\n{text}")
|
283
|
-
if raw:
|
284
|
-
console.print(response)
|
285
|
-
else:
|
286
|
-
console.print(Markdown(response))
|
janito/cli/history.py
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
from datetime import datetime, timezone
|
3
|
+
from typing import List
|
4
|
+
from rich.console import Console
|
5
|
+
from rich.table import Table
|
6
|
+
from janito.config import config
|
7
|
+
|
8
|
+
def get_history_path() -> Path:
|
9
|
+
"""Get the path to the history directory"""
|
10
|
+
history_dir = config.workspace_dir / '.janito' / 'history'
|
11
|
+
history_dir.mkdir(parents=True, exist_ok=True)
|
12
|
+
return history_dir
|
13
|
+
|
14
|
+
def save_to_history(request: str, response: str) -> None:
|
15
|
+
"""Save a request and its response to the history file"""
|
16
|
+
history_dir = get_history_path()
|
17
|
+
timestamp = datetime.now(timezone.utc).strftime('%Y-%m-%d_%H%M%S')
|
18
|
+
history_file = history_dir / f"{timestamp}_request.txt"
|
19
|
+
|
20
|
+
content = f"""Request: {request}
|
21
|
+
Timestamp: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}
|
22
|
+
|
23
|
+
Response:
|
24
|
+
{response}
|
25
|
+
"""
|
26
|
+
history_file.write_text(content)
|
27
|
+
|
28
|
+
def display_history() -> None:
|
29
|
+
"""Display the history of requests"""
|
30
|
+
console = Console()
|
31
|
+
history_dir = get_history_path()
|
32
|
+
|
33
|
+
if not history_dir.exists():
|
34
|
+
console.print("[yellow]No history found[/yellow]")
|
35
|
+
return
|
36
|
+
|
37
|
+
table = Table(title="Request History")
|
38
|
+
table.add_column("Timestamp", style="cyan")
|
39
|
+
table.add_column("Request", style="white")
|
40
|
+
table.add_column("File", style="dim")
|
41
|
+
|
42
|
+
history_files = sorted(history_dir.glob("*_request.txt"), reverse=True)
|
43
|
+
|
44
|
+
if not history_files:
|
45
|
+
console.print("[yellow]No history found[/yellow]")
|
46
|
+
return
|
47
|
+
|
48
|
+
for history_file in history_files:
|
49
|
+
try:
|
50
|
+
content = history_file.read_text()
|
51
|
+
request_line = next(line for line in content.splitlines() if line.startswith("Request:"))
|
52
|
+
timestamp_line = next(line for line in content.splitlines() if line.startswith("Timestamp:"))
|
53
|
+
|
54
|
+
request = request_line.replace("Request:", "").strip()
|
55
|
+
timestamp = timestamp_line.replace("Timestamp:", "").strip()
|
56
|
+
|
57
|
+
table.add_row(timestamp, request, history_file.name)
|
58
|
+
except Exception as e:
|
59
|
+
console.print(f"[red]Error reading {history_file}: {e}[/red]")
|
60
|
+
|
61
|
+
console.print(table)
|
janito/common.py
CHANGED
@@ -1,23 +1,80 @@
|
|
1
1
|
from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
|
2
|
-
from
|
2
|
+
from rich.console import Console
|
3
|
+
from rich.rule import Rule
|
4
|
+
from janito.agents import agent
|
5
|
+
from .config import config
|
6
|
+
from rich import print
|
7
|
+
from threading import Event
|
8
|
+
|
9
|
+
""" CACHE USAGE SUMMARY
|
10
|
+
https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching
|
11
|
+
cache_creation_input_tokens: Number of tokens written to the cache when creating a new entry.
|
12
|
+
cache_read_input_tokens: Number of tokens retrieved from the cache for this request.
|
13
|
+
input_tokens: Number of input tokens which were not read from or used to create a cache.
|
14
|
+
"""
|
15
|
+
|
16
|
+
from janito.prompt import build_system_prompt
|
17
|
+
|
18
|
+
console = Console()
|
3
19
|
|
4
20
|
def progress_send_message(message: str) -> str:
|
5
|
-
"""
|
6
|
-
|
21
|
+
"""Send a message to the AI agent with progress indication.
|
22
|
+
|
23
|
+
Displays a progress spinner while waiting for the agent's response and shows
|
24
|
+
token usage statistics after receiving the response.
|
7
25
|
|
8
26
|
Args:
|
9
|
-
message: The message to send
|
27
|
+
message: The message to send to the AI agent
|
10
28
|
|
11
29
|
Returns:
|
12
|
-
The response from the AI agent
|
30
|
+
str: The response text from the AI agent
|
31
|
+
|
32
|
+
Note:
|
33
|
+
If the request fails or is canceled, returns the error message as a string
|
13
34
|
"""
|
14
|
-
|
35
|
+
system_message = build_system_prompt()
|
36
|
+
if config.debug:
|
37
|
+
console.print("[yellow]======= Sending message[/yellow]")
|
38
|
+
print(system_message)
|
39
|
+
print(message)
|
40
|
+
console.print("[yellow]======= End of message[/yellow]")
|
41
|
+
|
15
42
|
with Progress(
|
16
43
|
SpinnerColumn(),
|
17
44
|
TextColumn("[progress.description]{task.description}", justify="center"),
|
18
45
|
TimeElapsedColumn(),
|
19
46
|
) as progress:
|
20
47
|
task = progress.add_task("Waiting for response from AI agent...", total=None)
|
21
|
-
response = agent.send_message(message)
|
48
|
+
response = agent.send_message(message, system_message=system_message)
|
22
49
|
progress.update(task, completed=True)
|
23
|
-
|
50
|
+
|
51
|
+
if config.debug:
|
52
|
+
console.print("[yellow]======= Received response[/yellow]")
|
53
|
+
print(response)
|
54
|
+
console.print("[yellow]======= End of response[/yellow]")
|
55
|
+
|
56
|
+
response_text = response.content[0].text if hasattr(response, 'content') else str(response)
|
57
|
+
|
58
|
+
# Add token usage summary with detailed cache info
|
59
|
+
if hasattr(response, 'usage'):
|
60
|
+
usage = response.usage
|
61
|
+
|
62
|
+
direct_input = usage.input_tokens
|
63
|
+
cache_create = usage.cache_creation_input_tokens or 0
|
64
|
+
cache_read = usage.cache_read_input_tokens or 0
|
65
|
+
total_input = direct_input + cache_create + cache_read
|
66
|
+
|
67
|
+
# Calculate percentages relative to total input
|
68
|
+
create_pct = (cache_create / total_input * 100) if cache_create else 0
|
69
|
+
read_pct = (cache_read / total_input * 100) if cache_read else 0
|
70
|
+
direct_pct = (direct_input / total_input * 100) if direct_input else 0
|
71
|
+
output_ratio = (usage.output_tokens / total_input * 100)
|
72
|
+
|
73
|
+
# Compact single-line token usage summary
|
74
|
+
usage_text = f"[cyan]In: [/][bold green]{total_input:,} - direct: {direct_input} ({direct_pct:.1f}%))[/] [cyan]Out:[/] [bold yellow]{usage.output_tokens:,}[/][dim]({output_ratio:.1f}%)[/]"
|
75
|
+
if cache_create or cache_read:
|
76
|
+
cache_text = f" [magenta]Input Cache:[/] [blue]Write:{cache_create:,}[/][dim]({create_pct:.1f}%)[/] [green]Read:{cache_read:,}[/][dim]({read_pct:.1f}%)[/]"
|
77
|
+
usage_text += cache_text
|
78
|
+
console.print(Rule(usage_text, style="cyan"))
|
79
|
+
|
80
|
+
return response_text
|
janito/config.py
CHANGED
@@ -1,30 +1,59 @@
|
|
1
1
|
from typing import Optional
|
2
2
|
import os
|
3
|
+
from pathlib import Path
|
3
4
|
|
4
5
|
class ConfigManager:
|
5
|
-
|
6
|
+
"""Singleton configuration manager for the application."""
|
6
7
|
|
8
|
+
_instance = None
|
9
|
+
|
7
10
|
def __init__(self):
|
11
|
+
"""Initialize configuration with default values."""
|
8
12
|
self.debug = False
|
9
13
|
self.verbose = False
|
10
14
|
self.debug_line = None
|
11
15
|
self.test_cmd = os.getenv('JANITO_TEST_CMD')
|
12
|
-
|
16
|
+
self.workspace_dir = Path.cwd()
|
17
|
+
self.raw = False
|
18
|
+
self.auto_apply: bool = False
|
19
|
+
self.tui: bool = False
|
20
|
+
self.skip_work: bool = False
|
21
|
+
|
13
22
|
@classmethod
|
14
23
|
def get_instance(cls) -> "ConfigManager":
|
24
|
+
"""Return the singleton instance of ConfigManager.
|
25
|
+
|
26
|
+
Returns:
|
27
|
+
ConfigManager: The singleton instance
|
28
|
+
"""
|
15
29
|
if cls._instance is None:
|
16
30
|
cls._instance = cls()
|
17
31
|
return cls._instance
|
18
|
-
|
32
|
+
|
19
33
|
def set_debug(self, enabled: bool) -> None:
|
34
|
+
"""Set debug mode.
|
35
|
+
|
36
|
+
Args:
|
37
|
+
enabled: True to enable debug mode, False to disable
|
38
|
+
"""
|
20
39
|
self.debug = enabled
|
21
40
|
|
22
41
|
def set_verbose(self, enabled: bool) -> None:
|
23
|
-
|
42
|
+
"""Set verbose output mode.
|
24
43
|
|
44
|
+
Args:
|
45
|
+
enabled: True to enable verbose output, False to disable
|
46
|
+
"""
|
47
|
+
self.verbose = enabled
|
48
|
+
|
25
49
|
def set_debug_line(self, line: Optional[int]) -> None:
|
26
|
-
|
50
|
+
"""Set specific line number for debug output.
|
27
51
|
|
52
|
+
Args:
|
53
|
+
line: Line number to debug, or None for all lines
|
54
|
+
"""
|
55
|
+
self.debug_line = line
|
56
|
+
|
28
57
|
def should_debug_line(self, line: int) -> bool:
|
29
58
|
"""Return True if we should show debug for this line number"""
|
30
59
|
return self.debug and (self.debug_line is None or self.debug_line == line)
|
@@ -33,5 +62,41 @@ class ConfigManager:
|
|
33
62
|
"""Set the test command, overriding environment variable"""
|
34
63
|
self.test_cmd = cmd if cmd is not None else os.getenv('JANITO_TEST_CMD')
|
35
64
|
|
65
|
+
def set_workspace_dir(self, path: Optional[Path]) -> None:
|
66
|
+
"""Set the workspace directory"""
|
67
|
+
self.workspace_dir = path if path is not None else Path.cwd()
|
68
|
+
|
69
|
+
def set_raw(self, enabled: bool) -> None:
|
70
|
+
"""Set raw output mode.
|
71
|
+
|
72
|
+
Args:
|
73
|
+
enabled: True to enable raw output mode, False to disable
|
74
|
+
"""
|
75
|
+
self.raw = enabled
|
76
|
+
|
77
|
+
def set_auto_apply(self, enabled: bool) -> None:
|
78
|
+
"""Set auto apply mode for changes.
|
79
|
+
|
80
|
+
Args:
|
81
|
+
enabled: True to enable auto apply mode, False to disable
|
82
|
+
"""
|
83
|
+
self.auto_apply = enabled
|
84
|
+
|
85
|
+
def set_tui(self, enabled: bool) -> None:
|
86
|
+
"""Set Text User Interface mode.
|
87
|
+
|
88
|
+
Args:
|
89
|
+
enabled: True to enable TUI mode, False to disable
|
90
|
+
"""
|
91
|
+
self.tui = enabled
|
92
|
+
|
93
|
+
def set_skip_work(self, enabled: bool) -> None:
|
94
|
+
"""Set whether to skip scanning the workspace directory.
|
95
|
+
|
96
|
+
Args:
|
97
|
+
enabled: True to skip workspace directory, False to include it
|
98
|
+
"""
|
99
|
+
self.skip_work = enabled
|
100
|
+
|
36
101
|
# Create a singleton instance
|
37
102
|
config = ConfigManager.get_instance()
|
janito/demo/__init__.py
ADDED
janito/demo/data.py
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
from typing import Dict, List
|
2
|
+
from .scenarios import DemoScenario
|
3
|
+
from .mock_data import get_mock_changes
|
4
|
+
|
5
|
+
def get_demo_scenarios() -> List[DemoScenario]:
|
6
|
+
"""Get list of predefined demo scenarios"""
|
7
|
+
return [
|
8
|
+
DemoScenario(
|
9
|
+
name="File Operations Demo",
|
10
|
+
description="Demonstrate various file operations with change viewer",
|
11
|
+
changes=get_mock_changes()
|
12
|
+
)
|
13
|
+
]
|
janito/demo/mock_data.py
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
from typing import List
|
2
|
+
from .operations import CreateOperation, ModifyOperation, RemoveOperation, MockOperation
|
3
|
+
|
4
|
+
def get_mock_changes() -> List[MockOperation]:
|
5
|
+
"""Get predefined mock changes for demo"""
|
6
|
+
return [
|
7
|
+
CreateOperation(
|
8
|
+
name="example/hello.py",
|
9
|
+
content="def greet():\n print('Hello, World!')\n"
|
10
|
+
),
|
11
|
+
ModifyOperation(
|
12
|
+
name="example/utils.py",
|
13
|
+
content="def process():\n return 'Processed'\n",
|
14
|
+
original_content="def old_process():\n return 'Old'\n"
|
15
|
+
),
|
16
|
+
RemoveOperation(
|
17
|
+
name="example/obsolete.py",
|
18
|
+
original_content="# Obsolete code\n"
|
19
|
+
)
|
20
|
+
]
|