janito 0.7.0__py3-none-any.whl → 0.8.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 (112) hide show
  1. janito/__main__.py +127 -141
  2. janito/agents/__init__.py +22 -22
  3. janito/agents/agent.py +24 -27
  4. janito/agents/claudeai.py +41 -45
  5. janito/agents/deepseekai.py +47 -0
  6. janito/change/applied_blocks.py +34 -0
  7. janito/change/applier.py +167 -0
  8. janito/change/edit_blocks.py +148 -0
  9. janito/change/finder.py +72 -0
  10. janito/change/request.py +144 -0
  11. janito/change/validator.py +87 -269
  12. janito/change/view/content.py +63 -0
  13. janito/change/{viewer → view}/diff.py +44 -43
  14. janito/change/view/panels.py +201 -0
  15. janito/change/view/sections.py +69 -0
  16. janito/change/view/styling.py +140 -0
  17. janito/change/view/summary.py +37 -0
  18. janito/change/{viewer → view}/themes.py +62 -55
  19. janito/change/view/viewer.py +59 -0
  20. janito/cli/__init__.py +1 -1
  21. janito/cli/commands.py +68 -88
  22. janito/cli/functions.py +66 -111
  23. janito/common.py +132 -79
  24. janito/config.py +99 -101
  25. janito/data/change_prompt.txt +81 -0
  26. janito/data/system_prompt.txt +3 -0
  27. janito/qa.py +56 -57
  28. janito/version.py +22 -22
  29. janito/workspace/__init__.py +8 -6
  30. janito/workspace/analysis.py +120 -120
  31. janito/workspace/{types.py → models.py} +97 -98
  32. janito/workspace/show.py +115 -141
  33. janito/workspace/stats.py +42 -43
  34. janito/workspace/workset.py +135 -108
  35. janito/workspace/workspace.py +335 -114
  36. janito-0.8.0.dist-info/METADATA +106 -0
  37. janito-0.8.0.dist-info/RECORD +40 -0
  38. {janito-0.7.0.dist-info → janito-0.8.0.dist-info}/licenses/LICENSE +20 -20
  39. janito/__init__.py +0 -2
  40. janito/agents/openai.py +0 -57
  41. janito/agents/test.py +0 -34
  42. janito/change/__init__.py +0 -32
  43. janito/change/__main__.py +0 -0
  44. janito/change/analysis/__init__.py +0 -23
  45. janito/change/analysis/__main__.py +0 -7
  46. janito/change/analysis/analyze.py +0 -62
  47. janito/change/analysis/formatting.py +0 -78
  48. janito/change/analysis/options.py +0 -81
  49. janito/change/analysis/prompts.py +0 -90
  50. janito/change/analysis/view/__init__.py +0 -9
  51. janito/change/analysis/view/terminal.py +0 -181
  52. janito/change/applier/__init__.py +0 -5
  53. janito/change/applier/file.py +0 -58
  54. janito/change/applier/main.py +0 -156
  55. janito/change/applier/text.py +0 -247
  56. janito/change/applier/workspace_dir.py +0 -58
  57. janito/change/core.py +0 -124
  58. janito/change/history.py +0 -44
  59. janito/change/operations.py +0 -7
  60. janito/change/parser.py +0 -287
  61. janito/change/play.py +0 -54
  62. janito/change/preview.py +0 -82
  63. janito/change/prompts.py +0 -121
  64. janito/change/test.py +0 -0
  65. janito/change/viewer/__init__.py +0 -11
  66. janito/change/viewer/content.py +0 -66
  67. janito/change/viewer/panels.py +0 -533
  68. janito/change/viewer/styling.py +0 -114
  69. janito/clear_statement_parser/clear_statement_format.txt +0 -328
  70. janito/clear_statement_parser/examples.txt +0 -326
  71. janito/clear_statement_parser/models.py +0 -104
  72. janito/clear_statement_parser/parser.py +0 -496
  73. janito/cli/base.py +0 -30
  74. janito/cli/history.py +0 -61
  75. janito/cli/registry.py +0 -26
  76. janito/demo/__init__.py +0 -4
  77. janito/demo/data.py +0 -13
  78. janito/demo/mock_data.py +0 -20
  79. janito/demo/operations.py +0 -45
  80. janito/demo/runner.py +0 -59
  81. janito/demo/scenarios.py +0 -32
  82. janito/prompt.py +0 -36
  83. janito/review.py +0 -13
  84. janito/search_replace/README.md +0 -192
  85. janito/search_replace/__init__.py +0 -7
  86. janito/search_replace/__main__.py +0 -21
  87. janito/search_replace/core.py +0 -120
  88. janito/search_replace/logger.py +0 -35
  89. janito/search_replace/parser.py +0 -52
  90. janito/search_replace/play.py +0 -61
  91. janito/search_replace/replacer.py +0 -36
  92. janito/search_replace/searcher.py +0 -411
  93. janito/search_replace/strategy_result.py +0 -10
  94. janito/shell/__init__.py +0 -38
  95. janito/shell/bus.py +0 -31
  96. janito/shell/commands.py +0 -136
  97. janito/shell/history.py +0 -20
  98. janito/shell/processor.py +0 -32
  99. janito/shell/prompt.py +0 -48
  100. janito/shell/registry.py +0 -60
  101. janito/tui/__init__.py +0 -21
  102. janito/tui/base.py +0 -22
  103. janito/tui/flows/__init__.py +0 -5
  104. janito/tui/flows/changes.py +0 -65
  105. janito/tui/flows/content.py +0 -128
  106. janito/tui/flows/selection.py +0 -117
  107. janito/tui/screens/__init__.py +0 -3
  108. janito/tui/screens/app.py +0 -1
  109. janito-0.7.0.dist-info/METADATA +0 -167
  110. janito-0.7.0.dist-info/RECORD +0 -96
  111. {janito-0.7.0.dist-info → janito-0.8.0.dist-info}/WHEEL +0 -0
  112. {janito-0.7.0.dist-info → janito-0.8.0.dist-info}/entry_points.txt +0 -0
janito/cli/commands.py CHANGED
@@ -1,88 +1,68 @@
1
- from pathlib import Path
2
- from typing import Optional, List
3
- from rich.console import Console
4
- from rich.text import Text
5
-
6
- from janito.agents import AIAgent, agent
7
- from janito.workspace import workset
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
15
-
16
- console = Console()
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)
29
-
30
- def handle_scan():
31
- """Preview files that would be analyzed"""
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
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
-
52
- def handle_request(request: str, preview_only: bool = False):
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]")
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]")
1
+ from pathlib import Path
2
+ from rich.console import Console
3
+ from janito.agents import agent
4
+
5
+ from janito.workspace import workset
6
+ from janito.config import config
7
+ from janito.qa import ask_question, display_answer
8
+ from janito.change.request import request_change, replay_saved_response
9
+
10
+
11
+ console = Console()
12
+
13
+ def handle_ask(question: str):
14
+ """Process a question about the codebase
15
+
16
+ Args:
17
+ question: The question to ask about the codebase
18
+ workset: Optional Workset instance for scoped operations
19
+ """
20
+ answer = ask_question(question)
21
+ display_answer(answer)
22
+
23
+ def handle_scan():
24
+ """Preview files that would be analyzed"""
25
+ workset.show()
26
+
27
+
28
+
29
+ def is_dir_empty(path: Path) -> bool:
30
+ """Check if directory is empty or only contains empty directories."""
31
+ if not path.is_dir():
32
+ return False
33
+
34
+ for item in path.iterdir():
35
+ if item.name.startswith(('.', '__pycache__')):
36
+ continue
37
+ if item.is_file():
38
+ return False
39
+ if item.is_dir() and not is_dir_empty(item):
40
+ return False
41
+ return True
42
+
43
+ def handle_request(request: str = None, replay: bool = False):
44
+ """Process modification request
45
+
46
+ Args:
47
+ request: The modification request to process
48
+ replay: If True, triggers the replay response flow
49
+ """
50
+ if not request and not replay:
51
+ return
52
+
53
+ is_empty = is_dir_empty(config.workspace_dir)
54
+ if is_empty:
55
+ console.print("\n[bold blue]Empty directory - will create new files as needed[/bold blue]")
56
+
57
+ if replay:
58
+ replay_saved_response()
59
+ else:
60
+ request_change(request)
61
+
62
+
63
+ # Command handler functions
64
+ COMMANDS = {
65
+ 'ask': handle_ask,
66
+ 'scan': handle_scan,
67
+ 'request': handle_request
68
+ }
janito/cli/functions.py CHANGED
@@ -1,111 +1,66 @@
1
- import sys
2
- import tempfile
3
- from datetime import datetime, timezone
4
- from pathlib import Path
5
- from typing import List, Optional
6
-
7
- import typer
8
- from rich.console import Console
9
- from rich.markdown import Markdown
10
- from rich.panel import Panel
11
- from rich.prompt import Confirm, Prompt
12
- from rich.text import Text
13
-
14
- from janito.agents import AIAgent
15
- from janito.common import progress_send_message
16
- from janito.config import config
17
- from janito.qa import ask_question, display_answer
18
- from janito.workspace import collect_files_content
19
-
20
-
21
- def prompt_user(message: str, choices: List[str] = None) -> str:
22
- """Display a simple user prompt with optional choices"""
23
- console = Console()
24
-
25
- if choices:
26
- console.print(f"\n[cyan]Options: {', '.join(choices)}[/cyan]")
27
-
28
- return Prompt.ask(f"[bold cyan]> {message}[/bold cyan]")
29
-
30
- def validate_option_letter(letter: str, options: dict) -> bool:
31
- """Validate if the given letter is a valid option or 'M' for modify"""
32
- return letter.upper() in options or letter.upper() == 'M'
33
-
34
- def get_option_selection() -> str:
35
- """Get user input for option selection"""
36
- console = Console()
37
- console.print("\n[cyan]Enter option letter or 'M' to modify request[/cyan]")
38
-
39
- while True:
40
- letter = Prompt.ask("[bold cyan]Select option[/bold cyan]").strip().upper()
41
- if letter == 'M' or (letter.isalpha() and len(letter) == 1):
42
- return letter
43
-
44
- console.print("[red]Please enter a valid letter or 'M'[/red]")
45
-
46
- def get_change_history_path() -> Path:
47
- """Create and return the changes history directory path"""
48
- changes_history_dir = config.workspace_dir / '.janito' / 'change_history'
49
- changes_history_dir.mkdir(parents=True, exist_ok=True)
50
- return changes_history_dir
51
-
52
- def get_timestamp() -> str:
53
- """Get current UTC timestamp in YMD_HMS format with leading zeros"""
54
- return datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')
55
-
56
- def save_prompt_to_file(prompt: str) -> Path:
57
- """Save prompt to a named temporary file that won't be deleted"""
58
- temp_file = tempfile.NamedTemporaryFile(prefix='selected_', suffix='.txt', delete=False)
59
- temp_path = Path(temp_file.name)
60
- temp_path.write_text(prompt)
61
- return temp_path
62
-
63
- def save_to_file(content: str, prefix: str) -> Path:
64
- """Save content to a timestamped file in changes history directory"""
65
- changes_history_dir = get_change_history_path()
66
- timestamp = get_timestamp()
67
- filename = f"{timestamp}_{prefix}.txt"
68
- file_path = changes_history_dir / filename
69
- file_path.write_text(content)
70
- return file_path
71
-
72
- def modify_request(request: str) -> str:
73
- """Display current request and get modified version with improved formatting"""
74
- console = Console()
75
-
76
- # Display current request in a panel with clear formatting
77
- console.print("\n[bold cyan]Current Request:[/bold cyan]")
78
- console.print(Panel(
79
- Text(request, style="white"),
80
- border_style="blue",
81
- title="Previous Request",
82
- padding=(1, 2)
83
- ))
84
-
85
- # Get modified request with clear prompt
86
- console.print("\n[bold cyan]Enter modified request below:[/bold cyan]")
87
- console.print("[dim](Press Enter to submit, Ctrl+C to cancel)[/dim]")
88
- try:
89
- new_request = prompt_user("Modified request")
90
- if not new_request.strip():
91
- console.print("[yellow]No changes made, keeping original request[/yellow]")
92
- return request
93
- return new_request
94
- except KeyboardInterrupt:
95
- console.print("\n[yellow]Modification cancelled, keeping original request[/yellow]")
96
- return request
97
-
98
-
99
- def read_stdin() -> str:
100
- """Read input from stdin until EOF"""
101
- console = Console()
102
- console.print("[dim]Enter your input (press Ctrl+D when finished):[/dim]")
103
- return sys.stdin.read().strip()
104
-
105
- def process_question(question: str) -> None:
106
- """Process a question about the codebase"""
107
- paths_to_scan = [config.workspace_dir] if not config.include else config.include
108
- files_content = collect_files_content(paths_to_scan)
109
- answer = ask_question(question, files_content)
110
- display_answer(answer)
111
-
1
+ import tempfile
2
+ from datetime import datetime, timezone
3
+ from pathlib import Path
4
+ from typing import List, Optional
5
+ from janito.shell.user_prompt import prompt_user
6
+ from rich.console import Console
7
+ from rich.prompt import Prompt
8
+ from rich.panel import Panel
9
+ from rich.text import Text
10
+
11
+ console = Console()
12
+
13
+ from janito.config import config
14
+
15
+
16
+ def get_change_history_path() -> Path:
17
+ """Create and return the changes history directory path"""
18
+ changes_history_dir = config.workspace_dir / '.janito' / 'change_history'
19
+ changes_history_dir.mkdir(parents=True, exist_ok=True)
20
+ return changes_history_dir
21
+
22
+ def get_timestamp() -> str:
23
+ """Get current UTC timestamp in YMD_HMS format with leading zeros"""
24
+ return datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')
25
+
26
+ def save_prompt_to_file(prompt: str) -> Path:
27
+ """Save prompt to a named temporary file that won't be deleted"""
28
+ temp_file = tempfile.NamedTemporaryFile(prefix='selected_', suffix='.txt', delete=False)
29
+ temp_path = Path(temp_file.name)
30
+ temp_path.write_text(prompt, encoding='utf-8')
31
+ return temp_path
32
+
33
+ def save_to_file(content: str, prefix: str) -> Path:
34
+ """Save content to a timestamped file in changes history directory"""
35
+ changes_history_dir = get_change_history_path()
36
+ timestamp = get_timestamp()
37
+ filename = f"{timestamp}_{prefix}.txt"
38
+ file_path = changes_history_dir / filename
39
+ file_path.write_text(content)
40
+ return file_path
41
+
42
+ def modify_request(request: str) -> str:
43
+ """Display current request and get modified version with improved formatting"""
44
+ console = Console()
45
+
46
+ # Display current request in a panel with clear formatting
47
+ console.print("\n[bold cyan]Current Request:[/bold cyan]")
48
+ console.print(Panel(
49
+ Text(request, style="white"),
50
+ border_style="blue",
51
+ title="Previous Request",
52
+ padding=(1, 2)
53
+ ))
54
+
55
+ # Get modified request with clear prompt
56
+ console.print("\n[bold cyan]Enter modified request below:[/bold cyan]")
57
+ console.print("[dim](Press Enter to submit, Ctrl+C to cancel)[/dim]")
58
+ try:
59
+ new_request = prompt_user("Modified request")
60
+ if not new_request.strip():
61
+ console.print("[yellow]No changes made, keeping original request[/yellow]")
62
+ return request
63
+ return new_request
64
+ except KeyboardInterrupt:
65
+ console.print("\n[yellow]Modification cancelled, keeping original request[/yellow]")
66
+ return request
janito/common.py CHANGED
@@ -1,80 +1,133 @@
1
- from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
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()
19
-
20
- def progress_send_message(message: str) -> str:
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.
25
-
26
- Args:
27
- message: The message to send to the AI agent
28
-
29
- Returns:
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
34
- """
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
-
42
- with Progress(
43
- SpinnerColumn(),
44
- TextColumn("[progress.description]{task.description}", justify="center"),
45
- TimeElapsedColumn(),
46
- ) as progress:
47
- task = progress.add_task("Waiting for response from AI agent...", total=None)
48
- response = agent.send_message(message, system_message=system_message)
49
- progress.update(task, completed=True)
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
-
1
+ from datetime import datetime
2
+ from rich.live import Live
3
+ from rich.text import Text
4
+ from rich.console import Console
5
+ from rich.rule import Rule
6
+ from threading import Thread
7
+ from janito.agents import agent
8
+ from .config import config
9
+ from typing import Optional, List
10
+ import importlib.resources
11
+ from pathlib import Path
12
+
13
+
14
+ console = Console()
15
+
16
+
17
+ def _get_system_prompt() -> str:
18
+ """Get the system prompt from the package data or local file."""
19
+ try:
20
+ # First try to read from package data
21
+ with importlib.resources.files('janito.data').joinpath('system_prompt.txt').open('r') as f:
22
+ return f.read()
23
+ except Exception:
24
+ # Fallback to local file for development
25
+ local_path = Path(__file__).parent / 'data' / 'system_prompt.txt'
26
+ if local_path.exists():
27
+ return local_path.read_text()
28
+ raise FileNotFoundError("Could not find system_prompt.txt")
29
+
30
+
31
+ def progress_send_message(message: str) -> Optional[str]:
32
+ """Send a message to the AI agent with progress indication.
33
+
34
+ Displays a progress spinner while waiting for the agent's response and shows
35
+ token usage statistics after receiving the response. Uses a background thread
36
+ to update the elapsed time display.
37
+
38
+ Args:
39
+ system_message: The system message to send to the AI agent
40
+ message: The message to send to the AI agent
41
+
42
+ Returns:
43
+ Optional[str]: The response text from the AI agent, or None if interrupted
44
+
45
+ Note:
46
+ - Returns None if the operation is cancelled via Ctrl+C
47
+ - If the request fails, raises the original exception
48
+ """
49
+ system_message = _get_system_prompt()
50
+
51
+ if config.debug:
52
+ console.print(f"[yellow]======= Sending message via {agent.__class__.__name__.replace('AIAgent', '')}[/yellow]")
53
+ print(system_message)
54
+ print(message)
55
+ console.print("[yellow]======= End of message[/yellow]")
56
+
57
+ start_time = datetime.now()
58
+
59
+
60
+ response = None
61
+ error = None
62
+
63
+ def agent_thread():
64
+ nonlocal response, error
65
+ try:
66
+ response = agent.send_message(system_message=system_message, message=message)
67
+ except Exception as e:
68
+ error = e
69
+
70
+ agent_thread = Thread(target=agent_thread, daemon=True)
71
+ agent_thread.start()
72
+
73
+ try:
74
+ with Live(Text("Waiting for response from AI agent...", justify="center"), refresh_per_second=4) as live:
75
+ while agent_thread.is_alive():
76
+ elapsed = datetime.now() - start_time
77
+ elapsed_seconds = elapsed.seconds
78
+ elapsed_minutes = elapsed_seconds // 60
79
+ remaining_seconds = elapsed_seconds % 60
80
+ time_str = f"{elapsed_minutes}m{remaining_seconds}s" if elapsed_minutes > 0 else f"{elapsed_seconds}s"
81
+ live.update(Text.assemble(
82
+ f"Waiting for {agent.friendly_name} response... (",
83
+ (time_str, "magenta"),
84
+ ")",
85
+ justify="center"
86
+ ))
87
+ agent_thread.join(timeout=0.25)
88
+
89
+ # Calculate final stats
90
+ elapsed = datetime.now() - start_time
91
+ elapsed_seconds = elapsed.seconds
92
+ elapsed_minutes = elapsed_seconds // 60
93
+ remaining_seconds = elapsed_seconds % 60
94
+ time_str = f"{elapsed_minutes}m{remaining_seconds}s" if elapsed_minutes > 0 else f"{elapsed_seconds}s"
95
+
96
+ if hasattr(response, 'usage'):
97
+ usage = response.usage
98
+ # Get total input tokens including cache if available
99
+ total_input = (
100
+ getattr(usage, 'input_tokens', 0) +
101
+ getattr(usage, 'cache_creation_input_tokens', 0) +
102
+ getattr(usage, 'cache_read_input_tokens', 0)
103
+ )
104
+ output_tokens = getattr(usage, 'output_tokens', 0)
105
+
106
+ # Update final message with stats
107
+ stats_text = f"Got response from {agent.friendly_name} after {time_str} • [cyan]In:[/] [bold green]{total_input:,}[/] [cyan]Out:[/] [bold yellow]{output_tokens:,}[/]"
108
+ live.update(Rule(stats_text))
109
+
110
+ except KeyboardInterrupt:
111
+ console.print("\n[yellow]Operation cancelled[/yellow]")
112
+ return None
113
+
114
+ if error:
115
+ if isinstance(error, KeyboardInterrupt):
116
+ console.print("\n[yellow]Operation cancelled[/yellow]")
117
+ return None
118
+ raise error
119
+
120
+ if config.debug:
121
+ console.print("[yellow]======= Received response[/yellow]")
122
+ print(response.content[0].text)
123
+ console.print("[yellow]======= End of response[/yellow]")
124
+
125
+ # Extract response text based on response type
126
+ if hasattr(response, 'choices'):
127
+ response_text = response.choices[0].message.content
128
+ elif hasattr(response, 'content'):
129
+ response_text = response.content[0].text
130
+ else:
131
+ response_text = str(response)
132
+
80
133
  return response_text