janito 0.6.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 (54) hide show
  1. janito/__main__.py +37 -30
  2. janito/agents/__init__.py +8 -2
  3. janito/agents/agent.py +10 -3
  4. janito/agents/claudeai.py +13 -23
  5. janito/agents/openai.py +5 -1
  6. janito/change/analysis/analyze.py +8 -7
  7. janito/change/analysis/prompts.py +4 -12
  8. janito/change/analysis/view/terminal.py +21 -11
  9. janito/change/applier/text.py +7 -5
  10. janito/change/core.py +22 -29
  11. janito/change/parser.py +0 -2
  12. janito/change/prompts.py +16 -21
  13. janito/change/validator.py +27 -9
  14. janito/change/viewer/content.py +1 -1
  15. janito/change/viewer/panels.py +93 -115
  16. janito/change/viewer/styling.py +15 -4
  17. janito/cli/commands.py +63 -20
  18. janito/common.py +44 -18
  19. janito/config.py +44 -44
  20. janito/prompt.py +36 -0
  21. janito/qa.py +5 -14
  22. janito/search_replace/README.md +63 -17
  23. janito/search_replace/__init__.py +2 -1
  24. janito/search_replace/core.py +15 -14
  25. janito/search_replace/logger.py +35 -0
  26. janito/search_replace/searcher.py +160 -48
  27. janito/search_replace/strategy_result.py +10 -0
  28. janito/shell/__init__.py +15 -16
  29. janito/shell/commands.py +38 -97
  30. janito/shell/processor.py +7 -27
  31. janito/shell/prompt.py +48 -0
  32. janito/shell/registry.py +60 -0
  33. janito/workspace/__init__.py +4 -5
  34. janito/workspace/analysis.py +2 -2
  35. janito/workspace/show.py +141 -0
  36. janito/workspace/stats.py +43 -0
  37. janito/workspace/types.py +98 -0
  38. janito/workspace/workset.py +108 -0
  39. janito/workspace/workspace.py +114 -0
  40. janito-0.7.0.dist-info/METADATA +167 -0
  41. {janito-0.6.0.dist-info → janito-0.7.0.dist-info}/RECORD +44 -43
  42. janito/change/viewer/pager.py +0 -56
  43. janito/cli/handlers/ask.py +0 -22
  44. janito/cli/handlers/demo.py +0 -22
  45. janito/cli/handlers/request.py +0 -24
  46. janito/cli/handlers/scan.py +0 -9
  47. janito/prompts.py +0 -2
  48. janito/shell/handlers.py +0 -122
  49. janito/workspace/manager.py +0 -48
  50. janito/workspace/scan.py +0 -232
  51. janito-0.6.0.dist-info/METADATA +0 -185
  52. {janito-0.6.0.dist-info → janito-0.7.0.dist-info}/WHEEL +0 -0
  53. {janito-0.6.0.dist-info → janito-0.7.0.dist-info}/entry_points.txt +0 -0
  54. {janito-0.6.0.dist-info → janito-0.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -3,7 +3,8 @@ from rich.panel import Panel
3
3
  from rich.columns import Columns
4
4
  from rich import box
5
5
  from rich.text import Text
6
- from typing import List, Optional
6
+ from rich.syntax import Syntax
7
+ from typing import List, Union
7
8
  from ..parser import FileChange, ChangeOperation
8
9
  from .styling import format_content, create_legend_items
9
10
  from .content import create_content_preview
@@ -11,9 +12,8 @@ from rich.rule import Rule
11
12
  import shutil
12
13
  import sys
13
14
  from rich.live import Live
14
- from .pager import check_pager # Add this import
15
+ from pathlib import Path
15
16
 
16
- # Remove clear_last_line, wait_for_space, and check_pager functions since they've been moved
17
17
 
18
18
  def preview_all_changes(console: Console, changes: List[FileChange]) -> None:
19
19
  """Show a summary of all changes with side-by-side comparison and continuous flow."""
@@ -38,47 +38,28 @@ def preview_all_changes(console: Console, changes: List[FileChange]) -> None:
38
38
  grouped_changes[change.operation] = []
39
39
  grouped_changes[change.operation].append(change)
40
40
 
41
- # Track content height
42
- current_height = 2 # Account for legend and newline
43
-
44
- # Show file operations with rule lines and track height
45
- current_height = _show_file_operations(console, grouped_changes)
46
-
47
41
  # Then show side-by-side panels for replacements
48
42
  console.print("\n[bold blue]File Changes:[/bold blue]")
49
- current_height += 2
50
43
 
51
44
  for i, change in enumerate(changes):
52
45
  if change.operation in (ChangeOperation.REPLACE_FILE, ChangeOperation.MODIFY_FILE):
53
46
  show_side_by_side_diff(console, change, i, total_changes)
54
47
 
55
- def _show_file_operations(console: Console, grouped_changes: dict) -> int:
56
- """Display file operation summaries with content preview for new files.
57
-
58
- Tracks current file being displayed and manages continuous flow.
59
- """
48
+ def _show_file_operations(console: Console, grouped_changes: dict) -> None:
60
49
  """Display file operation summaries with content preview for new files."""
61
- height = 0
62
50
  for operation, group in grouped_changes.items():
63
51
  for change in group:
64
52
  if operation == ChangeOperation.CREATE_FILE:
65
53
  console.print(Rule(f"[green]Creating new file: {change.name}[/green]", style="green"))
66
- height += 1
67
54
  if change.content:
68
- preview = create_content_preview(change.name, change.content)
55
+ preview = create_content_preview(Path(change.name), change.content)
69
56
  console.print(preview)
70
- height += len(change.content.splitlines()) + 4 # Account for panel borders
71
57
  elif operation == ChangeOperation.REMOVE_FILE:
72
58
  console.print(Rule(f"[red]Removing file: {change.name}[/red]", style="red"))
73
- height += 1
74
59
  elif operation == ChangeOperation.RENAME_FILE:
75
60
  console.print(Rule(f"[yellow]Renaming file: {change.name} → {change.target}[/yellow]", style="yellow"))
76
- height += 1
77
61
  elif operation == ChangeOperation.MOVE_FILE:
78
62
  console.print(Rule(f"[blue]Moving file: {change.name} → {change.target}[/blue]", style="blue"))
79
- height += 1
80
- height = check_pager(console, height)
81
- return height
82
63
 
83
64
  def show_side_by_side_diff(console: Console, change: FileChange, change_index: int = 0, total_changes: int = 1) -> None:
84
65
  """Show side-by-side diff panels for a file change with continuous flow.
@@ -98,14 +79,9 @@ def show_side_by_side_diff(console: Console, change: FileChange, change_index: i
98
79
  total_changes: Total number of changes
99
80
  """
100
81
  # Track current file name to prevent unnecessary paging
101
- from .pager import set_current_file, get_current_file
102
- current_file = get_current_file()
103
- new_file = str(change.name)
104
-
105
- # Only update paging state for different files
106
- if current_file != new_file:
107
- set_current_file(new_file)
108
- # Handle delete operations with special formatting
82
+ # Get terminal dimensions for layout
83
+ term_width = console.width or 120
84
+ min_panel_width = 60 # Minimum width for readable content
109
85
  if change.operation == ChangeOperation.REMOVE_FILE:
110
86
  show_delete_panel(console, change, change_index, total_changes)
111
87
  return
@@ -122,33 +98,30 @@ def show_side_by_side_diff(console: Console, change: FileChange, change_index: i
122
98
  original_lines = original.splitlines()
123
99
  new_lines = new_content.splitlines()
124
100
 
125
- # Track accumulated height
126
- current_height = 0
127
-
128
- # Track content height
129
- current_height += 1
130
-
131
- # Check if we need to page before showing header
132
- header_height = 3 # Account for panel borders and content
133
- current_height = check_pager(console, current_height, header_height)
134
-
101
+ # Show the header with reason and progress
135
102
  # Show the header with reason and progress
136
103
  operation = change.operation.name.replace('_', ' ').title()
137
- progress = f"Change {change_index + 1}/{total_changes}"
138
- # Create centered reason text if present
139
- reason_text = Text()
140
- if change.reason:
141
- reason_text.append("\n")
142
- reason_text.append(change.reason, style="italic")
143
- # Build header with operation and progress
104
+
105
+ # Create centered header with file info and progress
144
106
  header = Text()
145
107
  header.append(f"{operation}:", style="bold cyan")
146
- header.append(f" {change.name} ")
147
- header.append(f"({progress})", style="dim")
148
- header.append(reason_text)
108
+ header.append(f" {change.name}\n\n", style="white")
109
+
110
+ # Create centered progress indicator
111
+ progress_text = Text()
112
+ progress_text.append("Change ", style="bold blue")
113
+ progress_text.append(f"{change_index + 1}", style="bold yellow")
114
+ progress_text.append("/", style="white")
115
+ progress_text.append(f"{total_changes}", style="bold white")
116
+
117
+ # Add reason if present
118
+ if change.reason:
119
+ header.append(f"{progress_text}\n\n", style="white")
120
+ header.append(change.reason, style="italic")
121
+ else:
122
+ header.append(progress_text)
149
123
  # Display panel with centered content
150
124
  console.print(Panel(header, box=box.HEAVY, style="cyan", title_align="center"))
151
- current_height += header_height
152
125
 
153
126
  # Show layout mode indicator
154
127
  if not can_do_side_by_side:
@@ -178,34 +151,41 @@ def show_side_by_side_diff(console: Console, change: FileChange, change_index: i
178
151
  left_panel = format_content(orig_section, orig_section, new_section, True)
179
152
  right_panel = format_content(new_section, orig_section, new_section, False)
180
153
 
181
- # Calculate upcoming content height
182
- content_height = len(orig_section) + len(new_section) + 4 # Account for panel borders and padding
183
154
 
184
- # Check if we need to page before showing content
185
- current_height = check_pager(console, current_height, content_height)
186
-
187
- # Create panels with adaptive width
155
+ # Calculate optimal panel widths based on content
188
156
  if can_do_side_by_side:
189
- # Calculate panel width for side-by-side layout
190
- panel_width = (term_width - 4) // 2 # Account for padding
157
+ # Get max line lengths for each panel
158
+ left_max_width = max((len(line) for line in str(left_panel).splitlines()), default=0)
159
+ right_max_width = max((len(line) for line in str(right_panel).splitlines()), default=0)
160
+
161
+ # Add padding and margins
162
+ left_width = min(left_max_width + 4, (term_width - 4) // 2)
163
+ right_width = min(right_max_width + 4, (term_width - 4) // 2)
164
+
165
+ # Ensure minimum width
166
+ min_width = 30
167
+ left_width = max(left_width, min_width)
168
+ right_width = max(right_width, min_width)
169
+
170
+ # Create panels with content-aware widths
191
171
  panels = [
192
172
  Panel(
193
173
  left_panel or "",
194
- title="[red]Original Content[/red]",
174
+ title="[red]Original[/red]",
195
175
  title_align="center",
196
176
  subtitle=str(change.name),
197
177
  subtitle_align="center",
198
178
  padding=(0, 1),
199
- width=panel_width
179
+ width=left_width
200
180
  ),
201
181
  Panel(
202
182
  right_panel or "",
203
- title="[green]Modified Content[/green]",
183
+ title="[green]Modified[/green]",
204
184
  title_align="center",
205
185
  subtitle=str(change.name),
206
186
  subtitle_align="center",
207
187
  padding=(0, 1),
208
- width=panel_width
188
+ width=right_width
209
189
  )
210
190
  ]
211
191
 
@@ -248,7 +228,6 @@ def show_side_by_side_diff(console: Console, change: FileChange, change_index: i
248
228
  console.print(Rule(" Section Break ", style="cyan dim", align="center"))
249
229
 
250
230
  # Update height after displaying content
251
- current_height += content_height
252
231
  else:
253
232
  # For non-text changes, show full content side by side
254
233
  sections = find_modified_sections(original_lines, new_lines)
@@ -256,10 +235,6 @@ def show_side_by_side_diff(console: Console, change: FileChange, change_index: i
256
235
  left_panel = format_content(orig_section, orig_section, new_section, True)
257
236
  right_panel = format_content(new_section, orig_section, new_section, False)
258
237
 
259
- # Calculate content height for full file diff
260
- content_height = len(orig_section) + len(new_section) + 4 # Account for panels
261
- current_height = check_pager(console, current_height, content_height)
262
-
263
238
  # Format content with appropriate width
264
239
  left_panel = format_content(orig_section, orig_section, new_section, True)
265
240
  right_panel = format_content(new_section, orig_section, new_section, False)
@@ -275,37 +250,28 @@ def show_side_by_side_diff(console: Console, change: FileChange, change_index: i
275
250
  console.print("[yellow]Terminal width is limited. Using vertical layout for better readability.[/yellow]")
276
251
  console.print(f"[dim]Recommended terminal width: {min_panel_width * 2 + 4} or greater[/dim]")
277
252
 
278
- # Create unified header panel
279
- header_text = Text()
280
- header_text.append("[red]Original[/red]", style="bold")
281
- header_text.append(" vs ")
282
- header_text.append("[green]Modified[/green]", style="bold")
283
- header_text.append(f" - {change.name}")
284
-
285
- header_panel = Panel(
286
- header_text,
287
- box=box.HEAVY,
288
- style="cyan",
289
- padding=(0, 1)
290
- )
291
-
292
- # Create content panels without individual titles
253
+ # Create content panels with consistent titles
293
254
  panels = [
294
255
  Panel(
295
256
  left_panel,
257
+ title="[red]Original[/red]",
258
+ title_align="center",
259
+ subtitle=str(change.name),
260
+ subtitle_align="center",
296
261
  padding=(0, 1),
297
262
  width=None if can_do_side_by_side else term_width - 2
298
263
  ),
299
264
  Panel(
300
265
  right_panel,
266
+ title="[green]Modified[/green]",
267
+ title_align="center",
268
+ subtitle=str(change.name),
269
+ subtitle_align="center",
301
270
  padding=(0, 1),
302
271
  width=None if can_do_side_by_side else term_width - 2
303
272
  )
304
273
  ]
305
274
 
306
- # Display unified header
307
- console.print(header_panel, justify="center")
308
-
309
275
  # Render panels based on layout
310
276
  if can_do_side_by_side:
311
277
  # Create centered columns with fixed width
@@ -326,7 +292,6 @@ def show_side_by_side_diff(console: Console, change: FileChange, change_index: i
326
292
  console.print(Rule(style="dim"))
327
293
 
328
294
  # Update height after displaying content
329
- current_height += content_height
330
295
 
331
296
  # Add final separator and success message
332
297
  console.print(Rule(title="End Of Changes", style="bold blue"))
@@ -427,7 +392,18 @@ def create_replace_panel(name: Text, change: FileChange) -> Panel:
427
392
  new_content = change.content or ""
428
393
 
429
394
  term_width = Console().width or 120
430
- panel_width = max(60, (term_width - 10) // 2)
395
+
396
+ # Calculate content-based widths
397
+ orig_lines = original.splitlines()
398
+ new_lines = new_content.splitlines()
399
+
400
+ orig_width = max((len(line) for line in orig_lines), default=0)
401
+ new_width = max((len(line) for line in new_lines), default=0)
402
+
403
+ # Add padding and ensure minimum width
404
+ min_width = 30
405
+ left_width = max(min_width, min(orig_width + 4, (term_width - 10) // 2))
406
+ right_width = max(min_width, min(new_width + 4, (term_width - 10) // 2))
431
407
 
432
408
  panels = [
433
409
  Panel(
@@ -484,7 +460,7 @@ def create_change_panel(search_content: Text, replace_content: Text, description
484
460
  box=box.HEAVY
485
461
  )
486
462
  def show_delete_panel(console: Console, change: FileChange, change_index: int = 0, total_changes: int = 1) -> None:
487
- """Show a specialized panel for file deletion operations
463
+ """Show a simplified panel for file deletion operations
488
464
 
489
465
  Args:
490
466
  console: Rich console instance
@@ -492,49 +468,51 @@ def show_delete_panel(console: Console, change: FileChange, change_index: int =
492
468
  change_index: Current change number (0-based)
493
469
  total_changes: Total number of changes
494
470
  """
495
- # Track content height for panel display
496
- current_height = 0
497
-
498
471
  # Show the header with reason and progress
499
472
  operation = change.operation.name.replace('_', ' ').title()
500
473
  progress = f"Change {change_index + 1}/{total_changes}"
501
474
 
502
- # Create centered reason text if present
503
- reason_text = Text()
504
- if change.reason:
505
- reason_text.append("\n")
506
- reason_text.append(change.reason, style="italic")
507
-
508
- # Build header with operation and progress
475
+ # Create header text
509
476
  header = Text()
510
477
  header.append(f"{operation}:", style="bold red")
511
478
  header.append(f" {change.name} ")
512
479
  header.append(f"({progress})", style="dim")
513
- header.append(reason_text)
514
480
 
515
- # Display panel with centered content
516
- console.print(Panel(header, box=box.HEAVY, style="red", title_align="center"))
481
+ # Add reason if present
482
+ if change.reason:
483
+ header.append("\n")
484
+ header.append(change.reason, style="italic")
485
+
486
+ # Create content text
487
+ content = Text()
488
+ content.append("This file will be removed", style="bold red")
517
489
 
518
- # Create deletion panel
519
- delete_text = Text()
520
- delete_text.append("This file will be removed", style="bold red")
490
+ # Show file preview if content exists
521
491
  if change.original_content:
522
- delete_text.append("\n\nOriginal file path: ", style="dim")
523
- delete_text.append(str(change.name), style="red")
492
+ content.append("\n\n")
493
+ content.append("Original Content:", style="dim red")
494
+ content.append("\n")
495
+ syntax = Syntax(
496
+ change.original_content,
497
+ "python",
498
+ theme="monokai",
499
+ line_numbers=True,
500
+ word_wrap=True,
501
+ background_color="red"
502
+ )
503
+ content.append(syntax)
524
504
 
505
+ # Display panels
506
+ console.print(Panel(header, box=box.HEAVY, style="red", title_align="center"))
525
507
  console.print(Panel(
526
- delete_text,
527
- title="[red]File Deletion[/red]",
508
+ content,
509
+ title="[red]File Deletion Preview[/red]",
528
510
  title_align="center",
529
511
  border_style="red",
530
512
  padding=(1, 2)
531
513
  ))
532
-
533
- # Add final separator
534
514
  console.print(Rule(title="End Of Changes", style="bold red"))
535
515
  console.print()
536
- import os
537
- from typing import Union
538
516
 
539
517
  def get_human_size(size_bytes: int) -> str:
540
518
  """Convert bytes to human readable format"""
@@ -13,16 +13,27 @@ def set_theme(theme: ColorTheme) -> None:
13
13
  current_theme = theme
14
14
 
15
15
  def format_content(lines: List[str], search_lines: List[str], replace_lines: List[str], is_search: bool, width: int = 80, is_delete: bool = False) -> Text:
16
- """Format content with unified highlighting and indicators"""
16
+ """Format content with unified highlighting and indicators with full-width padding
17
+
18
+ Args:
19
+ lines: Lines to format
20
+ search_lines: Original content lines for comparison
21
+ replace_lines: New content lines for comparison
22
+ is_search: Whether this is search content (vs replace content)
23
+ width: Target width for padding
24
+ is_delete: Whether this is a deletion operation
25
+ """
17
26
  text = Text()
18
27
 
19
- # For delete operations, show all lines as deleted
28
+ # For delete operations, show all lines as deleted with full-width padding
20
29
  if is_delete:
21
30
  for line in lines:
22
31
  bg_color = current_theme.line_backgrounds['deleted']
23
32
  style = f"{current_theme.text_color} on {bg_color}"
24
- content = f"✕ {line}"
25
- padding = " " * max(0, width - len(content))
33
+ # Calculate padding to fill width
34
+ content_width = len(f" {line}")
35
+ padding = " " * max(0, width - content_width)
36
+ # Add content with consistent background
26
37
  text.append("✕ ", style=style)
27
38
  text.append(line, style=style)
28
39
  text.append(padding, style=style)
janito/cli/commands.py CHANGED
@@ -1,45 +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.workspace.analysis import analyze_workspace_content
6
+ from janito.agents import AIAgent, agent
7
+ from janito.workspace import workset
7
8
  from janito.config import config
8
9
  from janito.change.core import process_change_request
9
10
  from janito.change.play import play_saved_changes
10
11
  from janito.cli.history import save_to_history
11
- from janito.agents import agent
12
+ from janito.qa import ask_question, display_answer
13
+ from janito.demo import DemoRunner
14
+ from janito.demo.data import get_demo_scenarios
12
15
 
13
-
14
- from .handlers.ask import AskHandler
15
- from .handlers.request import RequestHandler
16
- from .handlers.scan import ScanHandler
17
- from janito.change.play import play_saved_changes
16
+ console = Console()
18
17
 
19
18
  def handle_ask(question: str):
20
- """Ask a question about the codebase"""
21
- handler = AskHandler()
22
- handler.handle(question)
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)
23
29
 
24
- def handle_scan(paths_to_scan: List[Path]):
30
+ def handle_scan():
25
31
  """Preview files that would be analyzed"""
26
- from janito.workspace import collect_files_content, preview_scan
27
- from janito.workspace.analysis import analyze_workspace_content
28
- preview_scan(paths_to_scan)
29
- files_content = collect_files_content(paths_to_scan)
30
- analyze_workspace_content(files_content)
32
+ workset.show()
31
33
 
32
34
  def handle_play(filepath: Path):
33
35
  """Replay a saved changes or debug file"""
34
36
  play_saved_changes(filepath)
35
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
42
+
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
51
+
36
52
  def handle_request(request: str, preview_only: bool = False):
37
53
  """Process modification request"""
54
+ is_empty = is_dir_empty(config.workspace_dir)
55
+ if is_empty and not config.include:
56
+ console.print("\n[bold blue]Empty directory - will create new files as needed[/bold blue]")
38
57
 
58
+ success, history_file = process_change_request(request, preview_only)
39
59
 
40
- handler = RequestHandler()
41
- handler.handle(request, preview_only)
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]")
42
68
 
43
69
  # Save request and response to history
44
70
  if agent.last_response:
45
- save_to_history(request, 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/common.py CHANGED
@@ -4,21 +4,38 @@ from rich.rule import Rule
4
4
  from janito.agents import agent
5
5
  from .config import config
6
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
7
17
 
8
18
  console = Console()
9
19
 
10
20
  def progress_send_message(message: str) -> str:
11
- """
12
- 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.
13
25
 
14
26
  Args:
15
- message: The message to send
27
+ message: The message to send to the AI agent
16
28
 
17
29
  Returns:
18
- 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
19
34
  """
35
+ system_message = build_system_prompt()
20
36
  if config.debug:
21
37
  console.print("[yellow]======= Sending message[/yellow]")
38
+ print(system_message)
22
39
  print(message)
23
40
  console.print("[yellow]======= End of message[/yellow]")
24
41
 
@@ -28,27 +45,36 @@ def progress_send_message(message: str) -> str:
28
45
  TimeElapsedColumn(),
29
46
  ) as progress:
30
47
  task = progress.add_task("Waiting for response from AI agent...", total=None)
31
- response = agent.send_message(message)
48
+ response = agent.send_message(message, system_message=system_message)
32
49
  progress.update(task, completed=True)
33
50
 
34
51
  if config.debug:
35
52
  console.print("[yellow]======= Received response[/yellow]")
36
53
  print(response)
37
54
  console.print("[yellow]======= End of response[/yellow]")
38
- response_text = response.content[0].text
39
55
 
40
- # Add token usage summary with detailed cache info
41
- usage = response.usage
56
+ response_text = response.content[0].text if hasattr(response, 'content') else str(response)
42
57
 
43
- # Format cache info
44
- cache_str = "(no cache used)"
45
- if usage.cache_creation_input_tokens or usage.cache_read_input_tokens:
46
- create_pct = (usage.cache_creation_input_tokens / usage.input_tokens) * 100
47
- read_pct = (usage.cache_read_input_tokens / usage.input_tokens) * 100
48
- cache_str = f"(cached in/out: {usage.cache_creation_input_tokens}[{create_pct:.1f}%]/{usage.cache_read_input_tokens}[{read_pct:.1f}%])"
49
-
50
- percentage = (usage.output_tokens / usage.input_tokens) * 100
51
- usage_text = f"Tokens: {usage.input_tokens} sent {cache_str}, {usage.output_tokens} received ({percentage:.1f}% ratio)"
52
- console.print(Rule(usage_text, style="blue", align="center"))
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"))
53
79
 
54
80
  return response_text