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
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.scan import preview_scan, is_dir_empty
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
- from .functions import (
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 handle_ask(question: str, workdir: Path, include: List[Path], raw: bool, agent: AIAgent):
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
- workdir = ensure_workdir(workdir)
27
- preview_scan(paths_to_scan, workdir)
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
- def handle_play(filepath: Path, workdir: Path, raw: bool):
30
- """Replay a saved prompt file"""
31
- workdir = ensure_workdir(workdir)
32
- replay_saved_file(filepath, workdir, raw)
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, workdir: Path, include: List[Path], raw: bool, agent: AIAgent):
52
+ def handle_request(request: str, preview_only: bool = False):
35
53
  """Process modification request"""
36
- workdir = ensure_workdir(workdir)
37
- paths_to_scan = include if include else [workdir]
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
- files_content = ""
44
- else:
45
- files_content = collect_files_content(paths_to_scan, workdir)
46
-
47
- initial_prompt = build_request_analysis_prompt(files_content, request)
48
- initial_response = progress_send_message(initial_prompt)
49
- save_to_file(initial_response, 'analysis', workdir)
50
-
51
- format_analysis(initial_response, raw, agent)
52
-
53
- handle_option_selection(initial_response, request, raw, workdir, include)
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
- from typing import Optional, List
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.prompt import Prompt, Confirm
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.common import progress_send_message
22
- from janito.prompts import build_selected_option_prompt
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(workdir: Path) -> Path:
46
+ def get_change_history_path() -> Path:
52
47
  """Create and return the changes history directory path"""
53
- changes_history_dir = workdir / '.janito' / 'change_history'
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, workdir: Path) -> Path:
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(workdir)
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 replay_saved_file(filepath: Path, workdir: Path, raw: bool = False) -> None:
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 = [workdir] if workdir else []
260
- if include:
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, raw)
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 janito.agents import AgentSingleton
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
- Send a message to the AI agent with a progress indicator and elapsed time.
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
- agent = AgentSingleton.get_agent()
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
- return response
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
- _instance = None
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
- self.verbose = enabled
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
- self.debug_line = line
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()
@@ -0,0 +1,4 @@
1
+ from .runner import DemoRunner
2
+ from .scenarios import DemoScenario
3
+
4
+ __all__ = ['DemoRunner', 'DemoScenario']
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
+ ]
@@ -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
+ ]