janito 3.12.1__py3-none-any.whl → 3.12.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- janito/agent/setup_agent.py +378 -377
- janito/cli/chat_mode/session.py +505 -505
- janito/cli/cli_commands/list_profiles.py +104 -107
- janito/cli/cli_commands/show_system_prompt.py +166 -166
- janito/cli/core/runner.py +250 -266
- janito/cli/main_cli.py +520 -519
- janito/cli/single_shot_mode/handler.py +167 -167
- janito/llm/__init__.py +6 -5
- janito/llm/driver.py +290 -254
- janito/llm/response_cache.py +57 -0
- janito/plugins/builtin.py +64 -88
- janito/plugins/tools/local/__init__.py +82 -80
- janito/plugins/tools/local/markdown_view.py +94 -0
- janito/plugins/tools/local/show_image.py +119 -74
- janito/plugins/tools/local/show_image_grid.py +134 -76
- janito/providers/alibaba/model_info.py +136 -105
- {janito-3.12.1.dist-info → janito-3.12.2.dist-info}/METADATA +1 -1
- {janito-3.12.1.dist-info → janito-3.12.2.dist-info}/RECORD +22 -20
- {janito-3.12.1.dist-info → janito-3.12.2.dist-info}/WHEEL +0 -0
- {janito-3.12.1.dist-info → janito-3.12.2.dist-info}/entry_points.txt +0 -0
- {janito-3.12.1.dist-info → janito-3.12.2.dist-info}/licenses/LICENSE +0 -0
- {janito-3.12.1.dist-info → janito-3.12.2.dist-info}/top_level.txt +0 -0
janito/plugins/builtin.py
CHANGED
@@ -1,88 +1,64 @@
|
|
1
|
-
"""
|
2
|
-
Builtin plugin system for janito-packaged plugins.
|
3
|
-
|
4
|
-
This module provides the infrastructure for plugins that are bundled
|
5
|
-
with janito and available by default without requiring external installation.
|
6
|
-
"""
|
7
|
-
|
8
|
-
import importlib
|
9
|
-
from typing import Dict, List, Optional, Type
|
10
|
-
from janito.plugins.base import Plugin
|
11
|
-
|
12
|
-
|
13
|
-
class BuiltinPluginRegistry:
|
14
|
-
"""Registry for builtin plugins that come packaged with janito."""
|
15
|
-
|
16
|
-
_plugins: Dict[str, Type[Plugin]] = {}
|
17
|
-
|
18
|
-
@classmethod
|
19
|
-
def register(cls, name: str, plugin_class: Type[Plugin]) -> None:
|
20
|
-
"""Register a builtin plugin."""
|
21
|
-
cls._plugins[name] = plugin_class
|
22
|
-
|
23
|
-
@classmethod
|
24
|
-
def get_plugin_class(cls, name: str) -> Optional[Type[Plugin]]:
|
25
|
-
"""Get the plugin class for a builtin plugin."""
|
26
|
-
return cls._plugins.get(name)
|
27
|
-
|
28
|
-
@classmethod
|
29
|
-
def list_builtin_plugins(cls) -> List[str]:
|
30
|
-
"""List all registered builtin plugins."""
|
31
|
-
return list(cls._plugins.keys())
|
32
|
-
|
33
|
-
@classmethod
|
34
|
-
def is_builtin(cls, name: str) -> bool:
|
35
|
-
"""Check if a plugin is builtin."""
|
36
|
-
return name in cls._plugins
|
37
|
-
|
38
|
-
|
39
|
-
def register_builtin_plugin(name: str):
|
40
|
-
"""Decorator to register a plugin as builtin."""
|
41
|
-
|
42
|
-
def decorator(plugin_class: Type[Plugin]) -> Type[Plugin]:
|
43
|
-
BuiltinPluginRegistry.register(name, plugin_class)
|
44
|
-
return plugin_class
|
45
|
-
|
46
|
-
return decorator
|
47
|
-
|
48
|
-
|
49
|
-
def load_builtin_plugin(name: str) -> Optional[Plugin]:
|
50
|
-
"""Load a builtin plugin by name."""
|
51
|
-
plugin_class = BuiltinPluginRegistry.get_plugin_class(name)
|
52
|
-
if plugin_class:
|
53
|
-
return plugin_class()
|
54
|
-
return None
|
55
|
-
|
56
|
-
|
57
|
-
#
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
LinterPlugin,
|
66
|
-
DebuggerPlugin,
|
67
|
-
PerformanceProfilerPlugin,
|
68
|
-
SecurityScannerPlugin,
|
69
|
-
DocumentationGeneratorPlugin,
|
70
|
-
)
|
71
|
-
|
72
|
-
# Register all janito-coder plugins as builtin
|
73
|
-
BuiltinPluginRegistry.register("git_analyzer", GitAnalyzerPlugin)
|
74
|
-
BuiltinPluginRegistry.register("code_navigator", CodeNavigatorPlugin)
|
75
|
-
BuiltinPluginRegistry.register("dependency_analyzer", DependencyAnalyzerPlugin)
|
76
|
-
BuiltinPluginRegistry.register("code_formatter", CodeFormatterPlugin)
|
77
|
-
BuiltinPluginRegistry.register("test_runner", TestRunnerPlugin)
|
78
|
-
BuiltinPluginRegistry.register("linter", LinterPlugin)
|
79
|
-
BuiltinPluginRegistry.register("debugger", DebuggerPlugin)
|
80
|
-
BuiltinPluginRegistry.register("performance_profiler", PerformanceProfilerPlugin)
|
81
|
-
BuiltinPluginRegistry.register("security_scanner", SecurityScannerPlugin)
|
82
|
-
BuiltinPluginRegistry.register(
|
83
|
-
"documentation_generator", DocumentationGeneratorPlugin
|
84
|
-
)
|
85
|
-
|
86
|
-
except ImportError:
|
87
|
-
# janito-coder not available, skip registration
|
88
|
-
pass
|
1
|
+
"""
|
2
|
+
Builtin plugin system for janito-packaged plugins.
|
3
|
+
|
4
|
+
This module provides the infrastructure for plugins that are bundled
|
5
|
+
with janito and available by default without requiring external installation.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import importlib
|
9
|
+
from typing import Dict, List, Optional, Type
|
10
|
+
from janito.plugins.base import Plugin
|
11
|
+
|
12
|
+
|
13
|
+
class BuiltinPluginRegistry:
|
14
|
+
"""Registry for builtin plugins that come packaged with janito."""
|
15
|
+
|
16
|
+
_plugins: Dict[str, Type[Plugin]] = {}
|
17
|
+
|
18
|
+
@classmethod
|
19
|
+
def register(cls, name: str, plugin_class: Type[Plugin]) -> None:
|
20
|
+
"""Register a builtin plugin."""
|
21
|
+
cls._plugins[name] = plugin_class
|
22
|
+
|
23
|
+
@classmethod
|
24
|
+
def get_plugin_class(cls, name: str) -> Optional[Type[Plugin]]:
|
25
|
+
"""Get the plugin class for a builtin plugin."""
|
26
|
+
return cls._plugins.get(name)
|
27
|
+
|
28
|
+
@classmethod
|
29
|
+
def list_builtin_plugins(cls) -> List[str]:
|
30
|
+
"""List all registered builtin plugins."""
|
31
|
+
return list(cls._plugins.keys())
|
32
|
+
|
33
|
+
@classmethod
|
34
|
+
def is_builtin(cls, name: str) -> bool:
|
35
|
+
"""Check if a plugin is builtin."""
|
36
|
+
return name in cls._plugins
|
37
|
+
|
38
|
+
|
39
|
+
def register_builtin_plugin(name: str):
|
40
|
+
"""Decorator to register a plugin as builtin."""
|
41
|
+
|
42
|
+
def decorator(plugin_class: Type[Plugin]) -> Type[Plugin]:
|
43
|
+
BuiltinPluginRegistry.register(name, plugin_class)
|
44
|
+
return plugin_class
|
45
|
+
|
46
|
+
return decorator
|
47
|
+
|
48
|
+
|
49
|
+
def load_builtin_plugin(name: str) -> Optional[Plugin]:
|
50
|
+
"""Load a builtin plugin by name."""
|
51
|
+
plugin_class = BuiltinPluginRegistry.get_plugin_class(name)
|
52
|
+
if plugin_class:
|
53
|
+
return plugin_class()
|
54
|
+
return None
|
55
|
+
|
56
|
+
|
57
|
+
# Note: External plugin packages can be registered here if needed
|
58
|
+
# For example, to auto-register plugins from external packages:
|
59
|
+
# try:
|
60
|
+
# from external_package.plugins import SomePlugin
|
61
|
+
# BuiltinPluginRegistry.register("some_plugin", SomePlugin)
|
62
|
+
# except ImportError:
|
63
|
+
# # external_package not available, skip registration
|
64
|
+
# pass
|
@@ -1,80 +1,82 @@
|
|
1
|
-
from .adapter import LocalToolsAdapter
|
2
|
-
|
3
|
-
from .ask_user import AskUserTool
|
4
|
-
from .copy_file import CopyFileTool
|
5
|
-
from .create_directory import CreateDirectoryTool
|
6
|
-
from .create_file import CreateFileTool
|
7
|
-
from .fetch_url import FetchUrlTool
|
8
|
-
from .find_files import FindFilesTool
|
9
|
-
from .view_file import ViewFileTool
|
10
|
-
from .read_files import ReadFilesTool
|
11
|
-
from .move_file import MoveFileTool
|
12
|
-
from .open_url import OpenUrlTool
|
13
|
-
from .open_html_in_browser import OpenHtmlInBrowserTool
|
14
|
-
from .python_code_run import PythonCodeRunTool
|
15
|
-
from .python_command_run import PythonCommandRunTool
|
16
|
-
from .python_file_run import PythonFileRunTool
|
17
|
-
from .remove_directory import RemoveDirectoryTool
|
18
|
-
from .remove_file import RemoveFileTool
|
19
|
-
from .replace_text_in_file import ReplaceTextInFileTool
|
20
|
-
from .run_bash_command import RunBashCommandTool
|
21
|
-
from .run_powershell_command import RunPowershellCommandTool
|
22
|
-
from .get_file_outline.core import GetFileOutlineTool
|
23
|
-
from .get_file_outline.search_outline import SearchOutlineTool
|
24
|
-
from .search_text.core import SearchTextTool
|
25
|
-
from .validate_file_syntax.core import ValidateFileSyntaxTool
|
26
|
-
from .read_chart import ReadChartTool
|
27
|
-
from .show_image import ShowImageTool
|
28
|
-
from .show_image_grid import ShowImageGridTool
|
29
|
-
|
30
|
-
|
31
|
-
import
|
32
|
-
|
33
|
-
from janito.
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
1
|
+
from .adapter import LocalToolsAdapter
|
2
|
+
|
3
|
+
from .ask_user import AskUserTool
|
4
|
+
from .copy_file import CopyFileTool
|
5
|
+
from .create_directory import CreateDirectoryTool
|
6
|
+
from .create_file import CreateFileTool
|
7
|
+
from .fetch_url import FetchUrlTool
|
8
|
+
from .find_files import FindFilesTool
|
9
|
+
from .view_file import ViewFileTool
|
10
|
+
from .read_files import ReadFilesTool
|
11
|
+
from .move_file import MoveFileTool
|
12
|
+
from .open_url import OpenUrlTool
|
13
|
+
from .open_html_in_browser import OpenHtmlInBrowserTool
|
14
|
+
from .python_code_run import PythonCodeRunTool
|
15
|
+
from .python_command_run import PythonCommandRunTool
|
16
|
+
from .python_file_run import PythonFileRunTool
|
17
|
+
from .remove_directory import RemoveDirectoryTool
|
18
|
+
from .remove_file import RemoveFileTool
|
19
|
+
from .replace_text_in_file import ReplaceTextInFileTool
|
20
|
+
from .run_bash_command import RunBashCommandTool
|
21
|
+
from .run_powershell_command import RunPowershellCommandTool
|
22
|
+
from .get_file_outline.core import GetFileOutlineTool
|
23
|
+
from .get_file_outline.search_outline import SearchOutlineTool
|
24
|
+
from .search_text.core import SearchTextTool
|
25
|
+
from .validate_file_syntax.core import ValidateFileSyntaxTool
|
26
|
+
from .read_chart import ReadChartTool
|
27
|
+
from .show_image import ShowImageTool
|
28
|
+
from .show_image_grid import ShowImageGridTool
|
29
|
+
from .markdown_view import MarkdownViewTool
|
30
|
+
|
31
|
+
from janito.tools.tool_base import ToolPermissions
|
32
|
+
import os
|
33
|
+
from janito.tools.permissions import get_global_allowed_permissions
|
34
|
+
from janito.platform_discovery import PlatformDiscovery
|
35
|
+
|
36
|
+
# Singleton tools adapter with all standard tools registered
|
37
|
+
local_tools_adapter = LocalToolsAdapter(workdir=os.getcwd())
|
38
|
+
|
39
|
+
|
40
|
+
def get_local_tools_adapter(workdir=None):
|
41
|
+
return LocalToolsAdapter(workdir=workdir or os.getcwd())
|
42
|
+
|
43
|
+
|
44
|
+
# Register tools
|
45
|
+
pd = PlatformDiscovery()
|
46
|
+
is_powershell = pd.detect_shell().startswith("PowerShell")
|
47
|
+
|
48
|
+
for tool_class in [
|
49
|
+
AskUserTool,
|
50
|
+
CopyFileTool,
|
51
|
+
CreateDirectoryTool,
|
52
|
+
CreateFileTool,
|
53
|
+
FetchUrlTool,
|
54
|
+
FindFilesTool,
|
55
|
+
ViewFileTool,
|
56
|
+
ReadFilesTool,
|
57
|
+
MoveFileTool,
|
58
|
+
OpenUrlTool,
|
59
|
+
OpenHtmlInBrowserTool,
|
60
|
+
PythonCodeRunTool,
|
61
|
+
PythonCommandRunTool,
|
62
|
+
PythonFileRunTool,
|
63
|
+
RemoveDirectoryTool,
|
64
|
+
RemoveFileTool,
|
65
|
+
ReplaceTextInFileTool,
|
66
|
+
RunBashCommandTool,
|
67
|
+
RunPowershellCommandTool,
|
68
|
+
GetFileOutlineTool,
|
69
|
+
SearchOutlineTool,
|
70
|
+
SearchTextTool,
|
71
|
+
ValidateFileSyntaxTool,
|
72
|
+
ReadChartTool,
|
73
|
+
ShowImageTool,
|
74
|
+
ShowImageGridTool,
|
75
|
+
MarkdownViewTool,
|
76
|
+
]:
|
77
|
+
# Skip bash tools when running in PowerShell
|
78
|
+
if is_powershell and tool_class.__name__ in ["RunBashCommandTool"]:
|
79
|
+
continue
|
80
|
+
local_tools_adapter.register_tool(tool_class)
|
81
|
+
|
82
|
+
# DEBUG: Print registered tools at startup
|
@@ -0,0 +1,94 @@
|
|
1
|
+
from janito.tools.tool_base import ToolBase, ToolPermissions
|
2
|
+
from janito.report_events import ReportAction
|
3
|
+
from janito.plugins.tools.local.adapter import register_local_tool
|
4
|
+
from janito.tools.tool_utils import display_path
|
5
|
+
from janito.i18n import tr
|
6
|
+
from janito.tools.loop_protection_decorator import protect_against_loops
|
7
|
+
|
8
|
+
|
9
|
+
@register_local_tool
|
10
|
+
class MarkdownViewTool(ToolBase):
|
11
|
+
"""
|
12
|
+
Display markdown content in the terminal using rich markdown rendering.
|
13
|
+
|
14
|
+
Args:
|
15
|
+
path (str): Path to the markdown file to display.
|
16
|
+
width (int, optional): Display width. Defaults to 80.
|
17
|
+
theme (str, optional): Markdown theme. Defaults to "github".
|
18
|
+
|
19
|
+
Returns:
|
20
|
+
str: Status message indicating the result of the markdown display.
|
21
|
+
"""
|
22
|
+
|
23
|
+
permissions = ToolPermissions(read=True)
|
24
|
+
tool_name = "markdown_view"
|
25
|
+
|
26
|
+
@protect_against_loops(max_calls=5, time_window=10.0, key_field="path")
|
27
|
+
def run(self, path: str, width: int = 80, theme: str = "github") -> str:
|
28
|
+
import os
|
29
|
+
from janito.tools.path_utils import expand_path
|
30
|
+
|
31
|
+
path = expand_path(path)
|
32
|
+
disp_path = display_path(path)
|
33
|
+
|
34
|
+
self.report_action(
|
35
|
+
tr("📖 View markdown '{disp_path}'", disp_path=disp_path),
|
36
|
+
ReportAction.READ,
|
37
|
+
)
|
38
|
+
|
39
|
+
try:
|
40
|
+
if not os.path.exists(path):
|
41
|
+
return f"❌ Error: File not found at '{path}'"
|
42
|
+
|
43
|
+
if not path.lower().endswith(('.md', '.markdown')):
|
44
|
+
return f"⚠️ Warning: File '{path}' does not appear to be a markdown file"
|
45
|
+
|
46
|
+
# Read the markdown file
|
47
|
+
with open(path, 'r', encoding='utf-8', errors='replace') as f:
|
48
|
+
markdown_content = f.read()
|
49
|
+
|
50
|
+
if not markdown_content.strip():
|
51
|
+
return f"⚠️ Warning: Markdown file '{path}' is empty"
|
52
|
+
|
53
|
+
# Import rich components for markdown rendering
|
54
|
+
try:
|
55
|
+
from rich.console import Console
|
56
|
+
from rich.markdown import Markdown
|
57
|
+
from rich.panel import Panel
|
58
|
+
from rich.text import Text
|
59
|
+
except ImportError:
|
60
|
+
return "❌ Error: rich library not available for markdown rendering"
|
61
|
+
|
62
|
+
# Create console with specified width
|
63
|
+
console = Console(width=width)
|
64
|
+
|
65
|
+
# Create markdown object
|
66
|
+
markdown = Markdown(markdown_content)
|
67
|
+
|
68
|
+
# Display the markdown with a header
|
69
|
+
console.print(f"\n[bold cyan]📄 Markdown: {disp_path}[/bold cyan]")
|
70
|
+
console.print("=" * min(len(disp_path) + 15, width))
|
71
|
+
console.print()
|
72
|
+
|
73
|
+
# Render the markdown content
|
74
|
+
console.print(markdown)
|
75
|
+
console.print()
|
76
|
+
|
77
|
+
self.report_success(
|
78
|
+
tr(" ✅ Markdown displayed: {disp_path}", disp_path=disp_path)
|
79
|
+
)
|
80
|
+
|
81
|
+
return f"✅ Markdown displayed: {disp_path}"
|
82
|
+
|
83
|
+
except FileNotFoundError:
|
84
|
+
self.report_warning(tr("❗ not found"))
|
85
|
+
return f"❌ Error: File not found at '{path}'"
|
86
|
+
except PermissionError:
|
87
|
+
self.report_error(tr(" ❌ Permission denied: {path}", path=disp_path))
|
88
|
+
return f"❌ Error: Permission denied reading '{path}'"
|
89
|
+
except UnicodeDecodeError as e:
|
90
|
+
self.report_error(tr(" ❌ Encoding error: {error}", error=e))
|
91
|
+
return f"❌ Error: Unable to decode file '{path}' - {e}"
|
92
|
+
except Exception as e:
|
93
|
+
self.report_error(tr(" ❌ Error: {error}", error=e))
|
94
|
+
return f"❌ Error displaying markdown: {e}"
|
@@ -1,74 +1,119 @@
|
|
1
|
-
from janito.tools.tool_base import ToolBase, ToolPermissions
|
2
|
-
from janito.report_events import ReportAction
|
3
|
-
from janito.plugins.tools.local.adapter import register_local_tool
|
4
|
-
from janito.i18n import tr
|
5
|
-
from janito.tools.loop_protection_decorator import protect_against_loops
|
6
|
-
|
7
|
-
|
8
|
-
@register_local_tool
|
9
|
-
class ShowImageTool(ToolBase):
|
10
|
-
"""Display an image inline in the terminal using the rich library.
|
11
|
-
|
12
|
-
Args:
|
13
|
-
path (str): Path to the image file.
|
14
|
-
width (int, optional): Target width in terminal cells. If unset, auto-fit.
|
15
|
-
height (int, optional): Target height in terminal rows. If unset, auto-fit.
|
16
|
-
preserve_aspect (bool, optional): Preserve aspect ratio. Default: True.
|
17
|
-
|
18
|
-
Returns:
|
19
|
-
str: Status message indicating display result or error details.
|
20
|
-
"""
|
21
|
-
|
22
|
-
permissions = ToolPermissions(read=True)
|
23
|
-
tool_name = "show_image"
|
24
|
-
|
25
|
-
@protect_against_loops(max_calls=5, time_window=10.0, key_field="path")
|
26
|
-
def run(
|
27
|
-
self,
|
28
|
-
path: str,
|
29
|
-
width: int | None = None,
|
30
|
-
height: int | None = None,
|
31
|
-
preserve_aspect: bool = True,
|
32
|
-
) -> str:
|
33
|
-
from janito.tools.tool_utils import display_path
|
34
|
-
from janito.tools.path_utils import expand_path
|
35
|
-
import os
|
36
|
-
|
37
|
-
try:
|
38
|
-
from rich.console import Console
|
39
|
-
from PIL import Image as PILImage
|
40
|
-
except Exception as e:
|
41
|
-
msg = tr("⚠️ Missing dependency: PIL/Pillow ({error})", error=e)
|
42
|
-
self.report_error(msg)
|
43
|
-
return msg
|
44
|
-
|
45
|
-
path = expand_path(path)
|
46
|
-
disp_path = display_path(path)
|
47
|
-
self.report_action(tr("🖼️ Show image '{disp_path}'", disp_path=disp_path), ReportAction.READ)
|
48
|
-
|
49
|
-
if not os.path.exists(path):
|
50
|
-
msg = tr("❗ not found")
|
51
|
-
self.report_warning(msg)
|
52
|
-
return tr("Error: file not found: {path}", path=disp_path)
|
53
|
-
|
54
|
-
try:
|
55
|
-
console = Console()
|
56
|
-
from rich.
|
57
|
-
from rich.text import Text
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
1
|
+
from janito.tools.tool_base import ToolBase, ToolPermissions
|
2
|
+
from janito.report_events import ReportAction
|
3
|
+
from janito.plugins.tools.local.adapter import register_local_tool
|
4
|
+
from janito.i18n import tr
|
5
|
+
from janito.tools.loop_protection_decorator import protect_against_loops
|
6
|
+
|
7
|
+
|
8
|
+
@register_local_tool
|
9
|
+
class ShowImageTool(ToolBase):
|
10
|
+
"""Display an image inline in the terminal using the rich library.
|
11
|
+
|
12
|
+
Args:
|
13
|
+
path (str): Path to the image file.
|
14
|
+
width (int, optional): Target width in terminal cells. If unset, auto-fit.
|
15
|
+
height (int, optional): Target height in terminal rows. If unset, auto-fit.
|
16
|
+
preserve_aspect (bool, optional): Preserve aspect ratio. Default: True.
|
17
|
+
|
18
|
+
Returns:
|
19
|
+
str: Status message indicating display result or error details.
|
20
|
+
"""
|
21
|
+
|
22
|
+
permissions = ToolPermissions(read=True)
|
23
|
+
tool_name = "show_image"
|
24
|
+
|
25
|
+
@protect_against_loops(max_calls=5, time_window=10.0, key_field="path")
|
26
|
+
def run(
|
27
|
+
self,
|
28
|
+
path: str,
|
29
|
+
width: int | None = None,
|
30
|
+
height: int | None = None,
|
31
|
+
preserve_aspect: bool = True,
|
32
|
+
) -> str:
|
33
|
+
from janito.tools.tool_utils import display_path
|
34
|
+
from janito.tools.path_utils import expand_path
|
35
|
+
import os
|
36
|
+
|
37
|
+
try:
|
38
|
+
from rich.console import Console
|
39
|
+
from PIL import Image as PILImage
|
40
|
+
except Exception as e:
|
41
|
+
msg = tr("⚠️ Missing dependency: PIL/Pillow ({error})", error=e)
|
42
|
+
self.report_error(msg)
|
43
|
+
return msg
|
44
|
+
|
45
|
+
path = expand_path(path)
|
46
|
+
disp_path = display_path(path)
|
47
|
+
self.report_action(tr("🖼️ Show image '{disp_path}'", disp_path=disp_path), ReportAction.READ)
|
48
|
+
|
49
|
+
if not os.path.exists(path):
|
50
|
+
msg = tr("❗ not found")
|
51
|
+
self.report_warning(msg)
|
52
|
+
return tr("Error: file not found: {path}", path=disp_path)
|
53
|
+
|
54
|
+
try:
|
55
|
+
console = Console()
|
56
|
+
from rich.panel import Panel
|
57
|
+
from rich.text import Text
|
58
|
+
import numpy as np
|
59
|
+
|
60
|
+
img = PILImage.open(path)
|
61
|
+
|
62
|
+
# Create ASCII art representation
|
63
|
+
def image_to_ascii(image, width=40, height=20):
|
64
|
+
try:
|
65
|
+
# Convert to grayscale and resize
|
66
|
+
img_gray = image.convert('L')
|
67
|
+
img_resized = img_gray.resize((width, height))
|
68
|
+
|
69
|
+
# Convert to numpy array
|
70
|
+
pixels = np.array(img_resized)
|
71
|
+
|
72
|
+
# ASCII characters from dark to light
|
73
|
+
ascii_chars = "@%#*+=-:. "
|
74
|
+
|
75
|
+
# Normalize pixels to ASCII range
|
76
|
+
ascii_art = ""
|
77
|
+
for row in pixels:
|
78
|
+
for pixel in row:
|
79
|
+
# Map pixel value (0-255) to ASCII index
|
80
|
+
ascii_index = int((pixel / 255) * (len(ascii_chars) - 1))
|
81
|
+
ascii_art += ascii_chars[ascii_index]
|
82
|
+
ascii_art += "\n"
|
83
|
+
|
84
|
+
return ascii_art.strip()
|
85
|
+
except Exception:
|
86
|
+
return None
|
87
|
+
|
88
|
+
# Calculate appropriate size for terminal display
|
89
|
+
display_width = width or min(60, img.width // 4)
|
90
|
+
display_height = height or min(30, img.height // 4)
|
91
|
+
|
92
|
+
ascii_art = image_to_ascii(img, display_width, display_height)
|
93
|
+
|
94
|
+
if ascii_art:
|
95
|
+
# Create a panel with both info and ASCII art
|
96
|
+
img_info = Text(f"🖼️ {disp_path}\nSize: {img.width}×{img.height}\nMode: {img.mode}\n", style="bold green")
|
97
|
+
ascii_text = Text(ascii_art, style="dim")
|
98
|
+
combined = Text.assemble(img_info, ascii_text)
|
99
|
+
panel = Panel(combined, title="Image Preview", border_style="blue")
|
100
|
+
else:
|
101
|
+
# Fallback to just info if ASCII art fails
|
102
|
+
img_info = Text(f"🖼️ {disp_path}\nSize: {img.width}×{img.height}\nMode: {img.mode}", style="bold green")
|
103
|
+
panel = Panel(img_info, title="Image Info", border_style="blue")
|
104
|
+
|
105
|
+
console.print(panel)
|
106
|
+
|
107
|
+
self.report_success(tr("✅ Displayed"))
|
108
|
+
details = []
|
109
|
+
if width:
|
110
|
+
details.append(f"width={width}")
|
111
|
+
if height:
|
112
|
+
details.append(f"height={height}")
|
113
|
+
if not preserve_aspect:
|
114
|
+
details.append("preserve_aspect=False")
|
115
|
+
info = ("; ".join(details)) if details else "auto-fit"
|
116
|
+
return tr("Image displayed: {disp_path} ({info})", disp_path=disp_path, info=info)
|
117
|
+
except Exception as e:
|
118
|
+
self.report_error(tr(" ❌ Error: {error}", error=e))
|
119
|
+
return tr("Error displaying image: {error}", error=e)
|