janito 0.4.0__py3-none-any.whl → 0.5.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 (49) hide show
  1. janito/__init__.py +48 -1
  2. janito/__main__.py +29 -334
  3. janito/agents/__init__.py +22 -0
  4. janito/agents/agent.py +21 -0
  5. janito/{claude.py → agents/claudeai.py} +10 -5
  6. janito/agents/openai.py +53 -0
  7. janito/agents/test.py +34 -0
  8. janito/analysis/__init__.py +33 -0
  9. janito/analysis/display.py +149 -0
  10. janito/analysis/options.py +112 -0
  11. janito/analysis/prompts.py +75 -0
  12. janito/change/__init__.py +19 -0
  13. janito/change/applier.py +269 -0
  14. janito/{contentchange.py → change/content.py} +5 -27
  15. janito/change/indentation.py +33 -0
  16. janito/change/position.py +169 -0
  17. janito/changehistory.py +46 -0
  18. janito/changeviewer/__init__.py +12 -0
  19. janito/changeviewer/diff.py +28 -0
  20. janito/changeviewer/panels.py +268 -0
  21. janito/changeviewer/styling.py +59 -0
  22. janito/changeviewer/themes.py +57 -0
  23. janito/cli/__init__.py +2 -0
  24. janito/cli/commands.py +53 -0
  25. janito/cli/functions.py +286 -0
  26. janito/cli/registry.py +26 -0
  27. janito/common.py +9 -9
  28. janito/console/__init__.py +3 -0
  29. janito/console/commands.py +112 -0
  30. janito/console/core.py +62 -0
  31. janito/console/display.py +157 -0
  32. janito/fileparser.py +292 -83
  33. janito/prompts.py +21 -6
  34. janito/qa.py +7 -5
  35. janito/review.py +13 -0
  36. janito/scan.py +44 -5
  37. janito/tests/test_fileparser.py +26 -0
  38. janito-0.5.0.dist-info/METADATA +146 -0
  39. janito-0.5.0.dist-info/RECORD +45 -0
  40. janito/analysis.py +0 -281
  41. janito/changeapplier.py +0 -436
  42. janito/changeviewer.py +0 -350
  43. janito/console.py +0 -330
  44. janito-0.4.0.dist-info/METADATA +0 -164
  45. janito-0.4.0.dist-info/RECORD +0 -21
  46. /janito/{contextparser.py → _contextparser.py} +0 -0
  47. {janito-0.4.0.dist-info → janito-0.5.0.dist-info}/WHEEL +0 -0
  48. {janito-0.4.0.dist-info → janito-0.5.0.dist-info}/entry_points.txt +0 -0
  49. {janito-0.4.0.dist-info → janito-0.5.0.dist-info}/licenses/LICENSE +0 -0
janito/review.py ADDED
@@ -0,0 +1,13 @@
1
+ from rich.console import Console
2
+ from rich.markdown import Markdown
3
+ from janito.common import progress_send_message
4
+ from janito.agents import AIAgent
5
+
6
+ def review_text(text: str, raw: bool = False) -> None:
7
+ """Review the provided text using Claude"""
8
+ console = Console()
9
+ response = progress_send_message(f"Please review this text and provide feedback:\n\n{text}")
10
+ if raw:
11
+ console.print(response)
12
+ else:
13
+ console.print(Markdown(response))
janito/scan.py CHANGED
@@ -1,10 +1,13 @@
1
1
  from pathlib import Path
2
- from typing import List, Tuple
2
+ from typing import List, Tuple, Set
3
3
  from rich.console import Console
4
4
  from rich.columns import Columns
5
+ from rich.panel import Panel
5
6
  from janito.config import config
6
7
  from pathspec import PathSpec
7
8
  from pathspec.patterns import GitWildMatchPattern
9
+ from collections import defaultdict
10
+
8
11
 
9
12
 
10
13
  SPECIAL_FILES = ["README.md", "__init__.py", "__main__.py"]
@@ -14,6 +17,7 @@ def _scan_paths(paths: List[Path], workdir: Path = None) -> Tuple[List[str], Lis
14
17
  content_parts = []
15
18
  file_items = []
16
19
  skipped_files = []
20
+ processed_files: Set[Path] = set() # Track processed files
17
21
  console = Console()
18
22
 
19
23
  # Load gitignore if it exists
@@ -34,7 +38,8 @@ def _scan_paths(paths: List[Path], workdir: Path = None) -> Tuple[List[str], Lis
34
38
  """
35
39
  if level > 1:
36
40
  return
37
-
41
+
42
+ path = path.resolve()
38
43
  relative_base = workdir
39
44
  if path.is_dir():
40
45
  relative_path = path.relative_to(relative_base)
@@ -43,8 +48,10 @@ def _scan_paths(paths: List[Path], workdir: Path = None) -> Tuple[List[str], Lis
43
48
  # Check for special files
44
49
  special_found = []
45
50
  for special_file in SPECIAL_FILES:
46
- if (path / special_file).exists():
51
+ special_path = path / special_file
52
+ if special_path.exists() and special_path.resolve() not in processed_files:
47
53
  special_found.append(special_file)
54
+ processed_files.add(special_path.resolve())
48
55
  if special_found:
49
56
  file_items[-1] = f"[blue]•[/blue] {relative_path}/ [cyan]({', '.join(special_found)})[/cyan]"
50
57
  for special_file in special_found:
@@ -63,9 +70,15 @@ def _scan_paths(paths: List[Path], workdir: Path = None) -> Tuple[List[str], Lis
63
70
  rel_path = str(item.relative_to(workdir))
64
71
  if gitignore_spec.match_file(rel_path):
65
72
  continue
66
- scan_path(item, level+1)
73
+ if item.resolve() not in processed_files: # Skip if already processed
74
+ scan_path(item, level+1)
67
75
 
68
76
  else:
77
+ resolved_path = path.resolve()
78
+ if resolved_path in processed_files: # Skip if already processed
79
+ return
80
+
81
+ processed_files.add(resolved_path)
69
82
  relative_path = path.relative_to(relative_base)
70
83
  # check if file is binary
71
84
  try:
@@ -97,6 +110,16 @@ def collect_files_content(paths: List[Path], workdir: Path = None) -> str:
97
110
  if file_items and config.verbose:
98
111
  console.print("\n[bold blue]Contents being analyzed:[/bold blue]")
99
112
  console.print(Columns(file_items, padding=(0, 4), expand=True))
113
+
114
+ if config.verbose:
115
+ for part in content_parts:
116
+ if part.startswith('<file>'):
117
+ # Extract filename from XML content
118
+ path_start = part.find('<path>') + 6
119
+ path_end = part.find('</path>')
120
+ if path_start > 5 and path_end > path_start:
121
+ filepath = part[path_start:path_end]
122
+ console.print(f"[dim]Adding content from:[/dim] {filepath}")
100
123
 
101
124
  return "\n".join(content_parts)
102
125
 
@@ -134,4 +157,20 @@ def preview_scan(paths: List[Path], workdir: Path = None) -> None:
134
157
 
135
158
  def is_dir_empty(path: Path) -> bool:
136
159
  """Check if directory is empty, ignoring hidden files"""
137
- return not any(item for item in path.iterdir() if not item.name.startswith('.'))
160
+ return not any(item for item in path.iterdir() if not item.name.startswith('.'))
161
+
162
+ def show_content_stats(content: str) -> None:
163
+ if not content:
164
+ return
165
+
166
+ dir_counts = defaultdict(int)
167
+ for line in content.split('\n'):
168
+ if line.startswith('<path>'):
169
+ path = Path(line.replace('<path>', '').replace('</path>', '').strip())
170
+ dir_counts[str(path.parent)] += 1
171
+
172
+ console = Console()
173
+ stats = [f"{directory} ({count} files)" for directory, count in dir_counts.items()]
174
+ columns = Columns(stats, equal=True, expand=True)
175
+ panel = Panel(columns, title="Work Context")
176
+ console.print(panel)
@@ -0,0 +1,26 @@
1
+ import pytest
2
+ from pathlib import Path
3
+ from janito.fileparser import validate_file_path, validate_file_content
4
+
5
+ def test_validate_file_path():
6
+ # Valid paths
7
+ assert validate_file_path(Path("test.py")) == (True, "")
8
+ assert validate_file_path(Path("folder/test.py")) == (True, "")
9
+
10
+ # Invalid paths
11
+ assert validate_file_path(Path("/absolute/path.py"))[0] == False
12
+ assert validate_file_path(Path("../escape.py"))[0] == False
13
+ assert validate_file_path(Path("test?.py"))[0] == False
14
+ assert validate_file_path(Path("test*.py"))[0] == False
15
+
16
+ def test_validate_file_content():
17
+ # Valid content
18
+ assert validate_file_content("print('hello')") == (True, "")
19
+ assert validate_file_content("# Empty file with comment\n") == (True, "")
20
+
21
+ # Invalid content
22
+ assert validate_file_content("")[0] == False
23
+
24
+ # Test large content
25
+ large_content = "x" * (1024 * 1024 + 1) # Slightly over 1MB
26
+ assert validate_file_content(large_content)[0] == False
@@ -0,0 +1,146 @@
1
+ Metadata-Version: 2.3
2
+ Name: janito
3
+ Version: 0.5.0
4
+ Summary: A CLI tool for software development tasks powered by AI
5
+ Project-URL: Homepage, https://github.com/joaompinto/janito
6
+ Project-URL: Repository, https://github.com/joaompinto/janito.git
7
+ Author-email: João Pinto <lamego.pinto@gmail.com>
8
+ License: MIT
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Topic :: Software Development
17
+ Requires-Python: >=3.8
18
+ Requires-Dist: anthropic
19
+ Requires-Dist: pathspec
20
+ Requires-Dist: rich
21
+ Requires-Dist: tomli
22
+ Requires-Dist: typer
23
+ Description-Content-Type: text/markdown
24
+
25
+ # 🤖 Janito CLI
26
+
27
+ A CLI tool for software development tasks powered by AI. Janito is your friendly AI-powered software development buddy that helps with coding tasks like refactoring, documentation updates, and code optimization.
28
+
29
+ ## 📥 Installation
30
+
31
+ 1. Install using pip:
32
+ ```bash
33
+ pip install janito
34
+ ```
35
+
36
+ 2. Verify installation:
37
+ ```bash
38
+ janito --version
39
+ ```
40
+
41
+ ## ⚙️ Setup
42
+
43
+ 1. Get your Anthropic API key from [Anthropic's website](https://www.anthropic.com/)
44
+
45
+ 2. Set your API key:
46
+ ```bash
47
+ # Linux/macOS
48
+ export ANTHROPIC_API_KEY='your-api-key-here'
49
+
50
+ # Windows (Command Prompt)
51
+ set ANTHROPIC_API_KEY=your-api-key-here
52
+
53
+ # Windows (PowerShell)
54
+ $env:ANTHROPIC_API_KEY='your-api-key-here'
55
+ ```
56
+
57
+ 3. (Optional) Configure default test command:
58
+ ```bash
59
+ export JANITO_TEST_CMD='pytest' # or your preferred test command
60
+ ```
61
+
62
+ ## 🚀 Quick Start
63
+
64
+ ### Basic Usage
65
+
66
+ ```bash
67
+ # Add docstrings to your code
68
+ janito "add docstrings to this file"
69
+
70
+ # Optimize a function
71
+ janito "optimize the main function"
72
+
73
+ # Get code explanations
74
+ janito --ask "explain this code"
75
+ ```
76
+
77
+ ### Common Scenarios
78
+
79
+ 1. **Code Refactoring**
80
+ ```bash
81
+ # Refactor with test validation
82
+ janito "refactor this code to use list comprehension" --test "pytest"
83
+
84
+ # Refactor specific directory
85
+ janito "update imports" -i ./src
86
+ ```
87
+
88
+ 2. **Documentation Updates**
89
+ ```bash
90
+ # Add or update docstrings
91
+ janito "add type hints and docstrings"
92
+
93
+ # Generate README
94
+ janito "create a README for this project"
95
+ ```
96
+
97
+ 3. **Code Analysis**
98
+ ```bash
99
+ # Get code explanations
100
+ janito --ask "what does this function do?"
101
+
102
+ # Find potential improvements
103
+ janito --ask "suggest optimizations for this code"
104
+ ```
105
+
106
+ ## 🛠️ Command Reference
107
+
108
+ ### Syntax
109
+ ```bash
110
+ janito [OPTIONS] [REQUEST]
111
+ ```
112
+
113
+ ### Key Options
114
+
115
+ | Option | Description |
116
+ |--------|-------------|
117
+ | `REQUEST` | The AI request/instruction (in quotes) |
118
+ | `-w, --working-dir PATH` | Working directory [default: current] |
119
+ | `-i, --include PATH` | Include directory int the working context (can be multiple)|
120
+ | `--ask QUESTION` | Ask questions without making changes |
121
+ | `--test COMMAND` | Run tests before applying changes |
122
+ | `--debug` | Enable debug logging |
123
+ | `--verbose` | Enable verbose mode |
124
+ | `--version` | Show version information |
125
+ | `--help` | Show help message |
126
+
127
+ ## 🔑 Key Features
128
+
129
+ - 🤖 AI-powered code analysis and modifications
130
+ - 💻 Interactive console mode
131
+ - ✅ Syntax validation for Python files
132
+ - 👀 Change preview and confirmation
133
+ - 🧪 Test command execution
134
+ - 📜 Change history tracking
135
+
136
+ ## 📚 Additional Information
137
+
138
+ - Requires Python 3.8+
139
+ - Changes are backed up in `.janito/changes_history/`
140
+ - Environment variables:
141
+ - `ANTHROPIC_API_KEY`: Required for API access
142
+ - `JANITO_TEST_CMD`: Default test command (optional)
143
+
144
+ ## 📄 License
145
+
146
+ MIT License - see [LICENSE](LICENSE) file for details.
@@ -0,0 +1,45 @@
1
+ janito/__init__.py,sha256=T1hTuIOJgEUjXNHplUuMZIgG_QxC1RH2ZcBe6WJ1XBE,1099
2
+ janito/__main__.py,sha256=ogcCBQ2YHkQhdhrXF7OV-DthLiL-Zw24XtRRYscT2Yo,2018
3
+ janito/_contextparser.py,sha256=iDX6nlqUQr-lj7lkEI4B6jHLci_Kxl-XWOaEiAQtVxA,4531
4
+ janito/changehistory.py,sha256=4Mu60og1pySlyfB36aqHnNKyH0yuSj-5rvBjmmvKGYw,1510
5
+ janito/common.py,sha256=imIwx0FCDe7p71ze9TBvn_W0Qz99Am3X5U9eHYd9vws,790
6
+ janito/config.py,sha256=ocg0lyab9ysgczKaqJTAtv0ZRa2VDMwclTJBgps7Vxw,1171
7
+ janito/fileparser.py,sha256=rJEme9QWbtnsjs9KuO3YI7B499DnfedWtzBONXqrjfU,12925
8
+ janito/prompts.py,sha256=M8IveFkG3fgLe87_9kTNdWAVkqmsD0T4eP6VmOqHAOQ,2607
9
+ janito/qa.py,sha256=MSzu9yvLCwXr9sKylTXtMfTy_2fRfPl3CU2NXf0OrS4,1977
10
+ janito/review.py,sha256=5Oc6BfxMGNmKbIeDP5_EiAKUDeQwVOD0YL7iqfgJLRE,471
11
+ janito/scan.py,sha256=QBXu64t8CjjJhiFdO-0npwdSPys9IX1oOOOyh1cGVIE,7678
12
+ janito/version.py,sha256=ylfPwGtdY8dEOFJ-DB9gKUQLggqRCvoLxhpnwjzCM94,739
13
+ janito/agents/__init__.py,sha256=VPBXIh526D11NrCSnuXUerYT7AI3rynp2klCgz94tPk,648
14
+ janito/agents/agent.py,sha256=3uGiUrvj9oCW6_oK-oMQQJ77K8jZFv7mAdXlIG1dxNY,751
15
+ janito/agents/claudeai.py,sha256=bl1MeALicf6arX_m9GmtAj0i3UoUiTbbXytVjLam2S8,2395
16
+ janito/agents/openai.py,sha256=tNtlzFJMWFv38Z62opR23u6xXlZ9L4xX_mf2f3wjrLU,2082
17
+ janito/agents/test.py,sha256=xoN1q9DUSYpUbnvTP1qZsEfxYrZfocJlt9DkIuMDvvY,1552
18
+ janito/analysis/__init__.py,sha256=QVhIoZdOc4eYhQ9ZRoZZiwowUaa-PQ0_7HV_cx4eOZU,734
19
+ janito/analysis/display.py,sha256=714423TxelieAyBe0M5A78i99qj_VfggZUWRbk2owzU,5204
20
+ janito/analysis/options.py,sha256=tgsLZOtNy35l4v4F6p_adGk3CrWhrDN0Ba_tw4Zzj5Q,3852
21
+ janito/analysis/prompts.py,sha256=7NI7XyL6gUeJWTfVP_X0ohtSnYb6gpu4Thy5kxzoeD8,2454
22
+ janito/change/__init__.py,sha256=ElyiGt6KmwufQouir-0l0ZtxNZdeLWTFRc4Vbg0RW3s,467
23
+ janito/change/applier.py,sha256=GXs0DRfb48WNoSyAH0uETjpzzFLvmUZGkb6VydKzazk,11601
24
+ janito/change/content.py,sha256=R8lbOFF2ANV_NF3S6zCV60-Q7hGFWiS-zmJBgu-r5SU,2475
25
+ janito/change/indentation.py,sha256=qvsGX9ZPm5cRftXJSKSVbK9-Po_95HCAc7sG98BxhIY,1191
26
+ janito/change/position.py,sha256=Nlkg0sGUvqYwtKA19ne1kT3ixiVYKUcy_40Cdr-gfl8,6385
27
+ janito/changeviewer/__init__.py,sha256=5kR3fsAYSgnA0Hlty0fMC6392aSK7WifH2ZIW-INjBc,298
28
+ janito/changeviewer/diff.py,sha256=2nW1Yzu7rzzIsuHWg-t2EJfsoVdbRaRJk3ewQBjQdxY,1117
29
+ janito/changeviewer/panels.py,sha256=F1yWUd6Vh535rV20NnFrVh6uvpc3w5CDBSNQUf_vOC8,9426
30
+ janito/changeviewer/styling.py,sha256=qv7DVfu57Qy93XUWIWdyqZWhgcfm-BaJVPslSuch05s,2461
31
+ janito/changeviewer/themes.py,sha256=BWa4l82fNM0pj01UYK9q_C7t_AqTwNbR8i0-F4I7KI8,1593
32
+ janito/cli/__init__.py,sha256=3gyMSaEAH2N4mXfZTLsHXKxXDxdhlYUeAPYQuhnOVBE,77
33
+ janito/cli/commands.py,sha256=6vPT60oT-1jJd_qdy-Si_pcf7yMCGmwpvQ43FZNJjUs,2001
34
+ janito/cli/functions.py,sha256=5Nn4muPv1KrInhcernByQqn_T0q2bCxPeDA1BoPw4K8,11399
35
+ janito/cli/registry.py,sha256=R1sI45YonxjMSLhAle7Dt18X_devrMsLt0ljb-rNza4,690
36
+ janito/console/__init__.py,sha256=0zxlJR88ESfuXtyzARnWNKcZ1rTyWLZwzXrfDQ7NHyw,77
37
+ janito/console/commands.py,sha256=kuI2w1LfEw0kbF_vOAYVENoMQYC42fXmDFZgxVKJKo8,3422
38
+ janito/console/core.py,sha256=DTAP_bhw_DTklUyPbGdjSzllUBMg7jXW0SklXkRNMI8,2264
39
+ janito/console/display.py,sha256=VNyHGHA_MG-dLPTCylgOKgHAl0muwhmsvg3nXFU-OZo,5380
40
+ janito/tests/test_fileparser.py,sha256=20CfwfnFtVm3_qU11qRAdcBZQXaLlkcHgbqWDOeazj4,997
41
+ janito-0.5.0.dist-info/METADATA,sha256=Jq4fJLGNs-4O8IOB2iy_8JrszlWbFzOmwrZRjQxIs_Y,3659
42
+ janito-0.5.0.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
43
+ janito-0.5.0.dist-info/entry_points.txt,sha256=wIo5zZxbmu4fC-ZMrsKD0T0vq7IqkOOLYhrqRGypkx4,48
44
+ janito-0.5.0.dist-info/licenses/LICENSE,sha256=xLIUXRPjtsgQml2zD1Pn4LpgiyZ49raw6jZDlO_gZdo,1062
45
+ janito-0.5.0.dist-info/RECORD,,
janito/analysis.py DELETED
@@ -1,281 +0,0 @@
1
- """Analysis display module for Janito.
2
-
3
- This module handles the formatting and display of analysis results, option selection,
4
- and related functionality for the Janito application.
5
- """
6
-
7
- from typing import Optional, Dict, List, Tuple
8
- from pathlib import Path
9
- from rich.console import Console
10
- from rich.markdown import Markdown
11
- from rich.panel import Panel
12
- from rich.text import Text
13
- from rich import box
14
- from rich.columns import Columns
15
- from rich.rule import Rule
16
- from rich.prompt import Prompt
17
- from janito.claude import ClaudeAPIAgent
18
- from janito.scan import collect_files_content
19
- from janito.common import progress_send_message
20
- from janito.config import config
21
- from dataclasses import dataclass
22
- import re
23
-
24
- MIN_PANEL_WIDTH = 40 # Minimum width for each panel
25
-
26
- def get_history_file_type(filepath: Path) -> str:
27
- """Determine the type of saved file based on its name"""
28
- name = filepath.name.lower()
29
- if 'changes' in name:
30
- return 'changes'
31
- elif 'selected' in name:
32
- return 'selected'
33
- elif 'analysis' in name:
34
- return 'analysis'
35
- elif 'response' in name:
36
- return 'response'
37
- return 'unknown'
38
-
39
- @dataclass
40
- class AnalysisOption:
41
- letter: str
42
- summary: str
43
- affected_files: List[str]
44
- description_items: List[str] # Changed from description to description_items
45
-
46
- CHANGE_ANALYSIS_PROMPT = """
47
- Current files:
48
- <files>
49
- {files_content}
50
- </files>
51
-
52
- Considering the above current files content, provide options for the requested change in the following format:
53
-
54
- A. Keyword summary of the change
55
- -----------------
56
- Description:
57
- - Detailed description of the change
58
-
59
- Affected files:
60
- - file1.py
61
- - file2.py (new)
62
- -----------------
63
- END_OF_OPTIONS (mandatory marker)
64
-
65
- RULES:
66
- - do NOT provide the content of the files
67
- - do NOT offer to implement the changes
68
-
69
- Request:
70
- {request}
71
- """
72
-
73
-
74
-
75
-
76
- def prompt_user(message: str, choices: List[str] = None) -> str:
77
- """Display a prominent user prompt with optional choices"""
78
- console = Console()
79
- console.print()
80
- console.print(Rule(" User Input Required ", style="bold cyan"))
81
-
82
- if choices:
83
- choice_text = f"[cyan]Options: {', '.join(choices)}[/cyan]"
84
- console.print(Panel(choice_text, box=box.ROUNDED))
85
-
86
- return Prompt.ask(f"[bold cyan]> {message}[/bold cyan]")
87
-
88
- def validate_option_letter(letter: str, options: dict) -> bool:
89
- """Validate if the given letter is a valid option or 'M' for modify"""
90
- return letter.upper() in options or letter.upper() == 'M'
91
-
92
- def get_option_selection() -> str:
93
- """Get user input for option selection with modify option"""
94
- console = Console()
95
- console.print("\n[cyan]Enter option letter or 'M' to modify request[/cyan]")
96
- while True:
97
- letter = prompt_user("Select option").strip().upper()
98
- if letter == 'M' or (letter.isalpha() and len(letter) == 1):
99
- return letter
100
- console.print("[red]Please enter a valid letter or 'M'[/red]")
101
-
102
- def _display_options(options: Dict[str, AnalysisOption]) -> None:
103
- """Display available options with left-aligned content and horizontally centered panels."""
104
- console = Console()
105
-
106
- # Display centered title using Rule
107
- console.print()
108
- console.print(Rule(" Available Options ", style="bold cyan", align="center"))
109
- console.print()
110
-
111
- # Calculate optimal width based on terminal
112
- term_width = console.width or 100
113
- panel_width = max(MIN_PANEL_WIDTH, (term_width // 2) - 10) # Width for two columns
114
-
115
- # Create panels for each option
116
- panels = []
117
- for letter, option in options.items():
118
- content = Text()
119
-
120
- # Display description as bullet points
121
- content.append("Description:\n", style="bold cyan")
122
- for item in option.description_items:
123
- content.append(f"• {item}\n", style="white")
124
- content.append("\n")
125
-
126
- # Display affected files
127
- if option.affected_files:
128
- content.append("Affected files:\n", style="bold cyan")
129
- for file in option.affected_files:
130
- content.append(f"• {file}\n", style="yellow")
131
-
132
- # Create panel with consistent styling
133
- panel = Panel(
134
- content,
135
- box=box.ROUNDED,
136
- border_style="cyan",
137
- title=f"Option {letter}: {option.summary}",
138
- title_align="center",
139
- padding=(1, 2),
140
- width=panel_width
141
- )
142
- panels.append(panel)
143
-
144
- # Display panels in columns with center alignment
145
- if panels:
146
- # Group panels into pairs for two columns
147
- for i in range(0, len(panels), 2):
148
- pair = panels[i:i+2]
149
- columns = Columns(
150
- pair,
151
- align="center",
152
- expand=True,
153
- equal=True,
154
- padding=(0, 2)
155
- )
156
- console.print(columns)
157
- console.print() # Add spacing between rows
158
-
159
- def _display_markdown(content: str) -> None:
160
- """Display content in markdown format."""
161
- console = Console()
162
- md = Markdown(content)
163
- console.print(md)
164
-
165
- def _display_raw_history(claude: ClaudeAPIAgent) -> None:
166
- """Display raw message history from Claude agent."""
167
- console = Console()
168
- console.print("\n=== Message History ===")
169
- for role, content in claude.messages_history:
170
- console.print(f"\n[bold cyan]{role.upper()}:[/bold cyan]")
171
- console.print(content)
172
- console.print("\n=== End Message History ===\n")
173
-
174
-
175
- def format_analysis(analysis: str, raw: bool = False, claude: Optional[ClaudeAPIAgent] = None, workdir: Optional[Path] = None) -> None:
176
- """Format and display the analysis output with enhanced capabilities."""
177
- console = Console()
178
-
179
- if raw and claude:
180
- _display_raw_history(claude)
181
- else:
182
- options = parse_analysis_options(analysis)
183
- if options:
184
- _display_options(options)
185
- else:
186
- console.print("\n[yellow]Warning: No valid options found in response. Displaying as markdown.[/yellow]\n")
187
- _display_markdown(analysis)
188
-
189
- def get_history_path(workdir: Path) -> Path:
190
- """Create and return the history directory path"""
191
- history_dir = workdir / '.janito' / 'history'
192
- history_dir.mkdir(parents=True, exist_ok=True)
193
- return history_dir
194
-
195
- def get_timestamp() -> str:
196
- """Get current UTC timestamp in YMD_HMS format with leading zeros"""
197
- from datetime import datetime, timezone
198
- return datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')
199
-
200
- def save_to_file(content: str, prefix: str, workdir: Path) -> Path:
201
- """Save content to a timestamped file in history directory"""
202
- history_dir = get_history_path(workdir)
203
- timestamp = get_timestamp()
204
- filename = f"{timestamp}_{prefix}.txt"
205
- file_path = history_dir / filename
206
- file_path.write_text(content)
207
- return file_path
208
-
209
-
210
-
211
- def parse_analysis_options(response: str) -> dict[str, AnalysisOption]:
212
- """Parse options from the response text using a line-based approach."""
213
- options = {}
214
-
215
- # Extract content up to END_OF_OPTIONS
216
- if 'END_OF_OPTIONS' in response:
217
- response = response.split('END_OF_OPTIONS')[0]
218
-
219
- lines = response.splitlines()
220
- current_option = None
221
- current_section = None
222
-
223
- for line in lines:
224
- line = line.strip()
225
- if not line:
226
- continue
227
-
228
- # Check for new option starting with letter
229
- if len(line) >= 2 and line[0].isalpha() and line[1] == '.' and line[0].isupper():
230
- if current_option:
231
- options[current_option.letter] = current_option
232
-
233
- letter = line[0]
234
- summary = line[2:].strip()
235
- current_option = AnalysisOption(
236
- letter=letter,
237
- summary=summary,
238
- affected_files=[],
239
- description_items=[]
240
- )
241
- current_section = None
242
- continue
243
-
244
- # Skip separator lines
245
- if line.startswith('---'):
246
- continue
247
-
248
- # Check for section headers
249
- if line.startswith('Description:'):
250
- current_section = 'description'
251
- continue
252
- elif line.startswith('Affected files:'):
253
- current_section = 'files'
254
- continue
255
-
256
- # Process content based on current section
257
- if current_option and current_section and line:
258
- if current_section == 'description':
259
- # Strip bullet points and whitespace
260
- item = line.lstrip(' -•').strip()
261
- if item:
262
- current_option.description_items.append(item)
263
- elif current_section == 'files':
264
- # Strip bullet points and (modified)/(new) annotations
265
- file_path = line.lstrip(' -')
266
- file_path = re.sub(r'\s*\([^)]+\)\s*$', '', file_path)
267
- if file_path:
268
- current_option.affected_files.append(file_path)
269
-
270
- # Add the last option if exists
271
- if current_option:
272
- options[current_option.letter] = current_option
273
-
274
- return options
275
-
276
- def build_request_analysis_prompt(files_content: str, request: str) -> str:
277
- """Build prompt for information requests"""
278
- return CHANGE_ANALYSIS_PROMPT.format(
279
- files_content=files_content,
280
- request=request
281
- )