janito 3.9.0__py3-none-any.whl → 3.10.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.
- janito/agent_events.py +75 -0
- janito/cli/chat_mode/session.py +1 -0
- janito/cli/chat_mode/shell/commands/__init__.py +2 -0
- janito/cli/chat_mode/shell/commands/interactive.py +33 -0
- janito/cli/chat_mode/toolbar.py +16 -1
- janito/cli/core/runner.py +33 -0
- janito/cli/main_cli.py +9 -0
- janito/cli/prompt_core.py +301 -302
- janito/cli/rich_terminal_reporter.py +170 -179
- janito/cli/single_shot_mode/handler.py +19 -0
- janito/llm/agent.py +59 -0
- janito/plugins/tools/local/__init__.py +7 -0
- janito/plugins/tools/local/create_directory.py +44 -1
- janito/tests/test_tool_adapter_case_insensitive.py +112 -0
- janito/tools/tools_adapter.py +514 -510
- {janito-3.9.0.dist-info → janito-3.10.0.dist-info}/METADATA +84 -84
- {janito-3.9.0.dist-info → janito-3.10.0.dist-info}/RECORD +21 -18
- {janito-3.9.0.dist-info → janito-3.10.0.dist-info}/WHEEL +0 -0
- {janito-3.9.0.dist-info → janito-3.10.0.dist-info}/entry_points.txt +0 -0
- {janito-3.9.0.dist-info → janito-3.10.0.dist-info}/licenses/LICENSE +0 -0
- {janito-3.9.0.dist-info → janito-3.10.0.dist-info}/top_level.txt +0 -0
@@ -1,179 +1,170 @@
|
|
1
|
-
from rich.console import Console
|
2
|
-
from rich.markdown import Markdown
|
3
|
-
from rich.pretty import Pretty
|
4
|
-
from rich.panel import Panel
|
5
|
-
from rich.text import Text
|
6
|
-
from janito.event_bus.handler import EventHandlerBase
|
7
|
-
import janito.driver_events as driver_events
|
8
|
-
from janito.report_events import ReportSubtype, ReportAction
|
9
|
-
from janito.event_bus.bus import event_bus
|
10
|
-
from janito.llm import message_parts
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
-
|
23
|
-
|
24
|
-
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
self.
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
provider
|
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
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
self.
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
if (
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
)
|
162
|
-
|
163
|
-
self.console.
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
self.console.print(msg)
|
170
|
-
self.console.file.flush()
|
171
|
-
elif subtype == ReportSubtype.STDOUT:
|
172
|
-
self.console.print(msg)
|
173
|
-
self.console.file.flush()
|
174
|
-
elif subtype == ReportSubtype.STDERR:
|
175
|
-
self.console.print(Text(msg, style="on red"))
|
176
|
-
self.console.file.flush()
|
177
|
-
else:
|
178
|
-
self.console.print(msg)
|
179
|
-
self.console.file.flush()
|
1
|
+
from rich.console import Console
|
2
|
+
from rich.markdown import Markdown
|
3
|
+
from rich.pretty import Pretty
|
4
|
+
from rich.panel import Panel
|
5
|
+
from rich.text import Text
|
6
|
+
from janito.event_bus.handler import EventHandlerBase
|
7
|
+
import janito.driver_events as driver_events
|
8
|
+
from janito.report_events import ReportSubtype, ReportAction
|
9
|
+
from janito.event_bus.bus import event_bus
|
10
|
+
from janito.llm import message_parts
|
11
|
+
import janito.agent_events as agent_events
|
12
|
+
|
13
|
+
|
14
|
+
import sys
|
15
|
+
|
16
|
+
|
17
|
+
class RichTerminalReporter(EventHandlerBase):
|
18
|
+
"""
|
19
|
+
Handles UI rendering for janito events using Rich.
|
20
|
+
|
21
|
+
- For ResponseReceived events, iterates over the 'parts' field and displays each part appropriately:
|
22
|
+
- TextMessagePart: rendered as Markdown (uses 'content' field)
|
23
|
+
- Other MessageParts: displayed using Pretty or a suitable Rich representation
|
24
|
+
- For RequestFinished events, output is printed only if raw mode is enabled (using Pretty formatting).
|
25
|
+
- Report events (info, success, error, etc.) are always printed with appropriate styling.
|
26
|
+
"""
|
27
|
+
|
28
|
+
def __init__(self, raw_mode=False):
|
29
|
+
from janito.cli.console import shared_console
|
30
|
+
|
31
|
+
self.console = shared_console
|
32
|
+
self.raw_mode = raw_mode
|
33
|
+
import janito.report_events as report_events
|
34
|
+
|
35
|
+
import janito.tools.tool_events as tool_events
|
36
|
+
|
37
|
+
super().__init__(driver_events, report_events, tool_events, agent_events)
|
38
|
+
self._waiting_printed = False
|
39
|
+
|
40
|
+
def on_RequestStarted(self, event):
|
41
|
+
# Print waiting message with provider and model name
|
42
|
+
provider = None
|
43
|
+
model = None
|
44
|
+
if hasattr(event, "payload") and isinstance(event.payload, dict):
|
45
|
+
provider = event.payload.get("provider_name")
|
46
|
+
model = event.payload.get("model") or event.payload.get("model_name")
|
47
|
+
if not provider:
|
48
|
+
provider = getattr(event, "provider_name", None)
|
49
|
+
if not provider:
|
50
|
+
provider = getattr(event, "driver_name", None)
|
51
|
+
if not provider:
|
52
|
+
provider = "LLM"
|
53
|
+
if not model:
|
54
|
+
model = getattr(event, "model", None)
|
55
|
+
if not model:
|
56
|
+
model = getattr(event, "model_name", None)
|
57
|
+
if not model:
|
58
|
+
model = "?"
|
59
|
+
self.console.print(
|
60
|
+
f"[bold cyan]Waiting for {provider} (model: {model})...[/bold cyan]", end=""
|
61
|
+
)
|
62
|
+
self._waiting_printed = True
|
63
|
+
|
64
|
+
def on_AgentWaitingForResponse(self, event):
|
65
|
+
# Agent waiting - set flag but don't print anything
|
66
|
+
self._waiting_printed = True
|
67
|
+
|
68
|
+
def on_ResponseReceived(self, event):
|
69
|
+
parts = event.parts if hasattr(event, "parts") else None
|
70
|
+
if not parts:
|
71
|
+
self.console.print("[No response parts to display]")
|
72
|
+
self.console.file.flush()
|
73
|
+
return
|
74
|
+
for part in parts:
|
75
|
+
if isinstance(part, message_parts.TextMessagePart):
|
76
|
+
self.console.print(Markdown(part.content))
|
77
|
+
self.console.file.flush()
|
78
|
+
|
79
|
+
def delete_current_line(self):
|
80
|
+
"""
|
81
|
+
Clears the entire current line in the terminal and returns the cursor to column 1.
|
82
|
+
"""
|
83
|
+
# Use raw ANSI escape sequences but write directly to the underlying file
|
84
|
+
# to bypass Rich's escaping/interpretation
|
85
|
+
if hasattr(self.console, 'file') and hasattr(self.console.file, 'write'):
|
86
|
+
self.console.file.write("\r\033[2K")
|
87
|
+
self.console.file.flush()
|
88
|
+
else:
|
89
|
+
# Fallback to sys.stdout if console.file is not available
|
90
|
+
import sys
|
91
|
+
sys.stdout.write("\r\033[2K")
|
92
|
+
sys.stdout.flush()
|
93
|
+
|
94
|
+
def on_RequestFinished(self, event):
|
95
|
+
if self._waiting_printed:
|
96
|
+
self.delete_current_line()
|
97
|
+
self._waiting_printed = False
|
98
|
+
|
99
|
+
def on_AgentReceivedResponse(self, event):
|
100
|
+
# Clear any waiting message when agent receives response
|
101
|
+
if self._waiting_printed:
|
102
|
+
self.delete_current_line()
|
103
|
+
self._waiting_printed = False
|
104
|
+
|
105
|
+
def on_ToolCallError(self, event):
|
106
|
+
# Optionally handle tool call errors in a user-friendly way
|
107
|
+
error = getattr(event, "error", None)
|
108
|
+
tool = getattr(event, "tool_name", None)
|
109
|
+
if error and tool:
|
110
|
+
self.console.print(f"[bold red]Tool Error ({tool}):[/] {error}")
|
111
|
+
self.console.file.flush()
|
112
|
+
|
113
|
+
def on_ReportEvent(self, event):
|
114
|
+
# Special handling for security-related report events
|
115
|
+
subtype = getattr(event, "subtype", None)
|
116
|
+
msg = getattr(event, "message", None)
|
117
|
+
action = getattr(event, "action", None)
|
118
|
+
tool = getattr(event, "tool", None)
|
119
|
+
context = getattr(event, "context", None)
|
120
|
+
if (
|
121
|
+
subtype == ReportSubtype.ERROR
|
122
|
+
and msg
|
123
|
+
and "[SECURITY] Path access denied" in msg
|
124
|
+
):
|
125
|
+
# Highlight security errors with a distinct style
|
126
|
+
self.console.print(
|
127
|
+
Panel(f"{msg}", title="[red]SECURITY VIOLATION[/red]", style="bold red")
|
128
|
+
)
|
129
|
+
self.console.file.flush()
|
130
|
+
return
|
131
|
+
|
132
|
+
msg = event.message if hasattr(event, "message") else None
|
133
|
+
subtype = event.subtype if hasattr(event, "subtype") else None
|
134
|
+
if not msg or not subtype:
|
135
|
+
return
|
136
|
+
if subtype == ReportSubtype.ACTION_INFO:
|
137
|
+
# Clear any waiting message before showing action info
|
138
|
+
if self._waiting_printed:
|
139
|
+
self.delete_current_line()
|
140
|
+
self._waiting_printed = False
|
141
|
+
# Use orange for all write/modification actions
|
142
|
+
modification_actions = (
|
143
|
+
getattr(ReportAction, "UPDATE", None),
|
144
|
+
getattr(ReportAction, "WRITE", None),
|
145
|
+
getattr(ReportAction, "DELETE", None),
|
146
|
+
getattr(ReportAction, "CREATE", None),
|
147
|
+
)
|
148
|
+
style = (
|
149
|
+
"orange1"
|
150
|
+
if getattr(event, "action", None) in modification_actions
|
151
|
+
else "cyan"
|
152
|
+
)
|
153
|
+
self.console.print(Text(msg, style=style), end="")
|
154
|
+
self.console.file.flush()
|
155
|
+
elif subtype in (
|
156
|
+
ReportSubtype.SUCCESS,
|
157
|
+
ReportSubtype.ERROR,
|
158
|
+
ReportSubtype.WARNING,
|
159
|
+
):
|
160
|
+
self.console.print(msg)
|
161
|
+
self.console.file.flush()
|
162
|
+
elif subtype == ReportSubtype.STDOUT:
|
163
|
+
self.console.print(msg)
|
164
|
+
self.console.file.flush()
|
165
|
+
elif subtype == ReportSubtype.STDERR:
|
166
|
+
self.console.print(Text(msg, style="on red"))
|
167
|
+
self.console.file.flush()
|
168
|
+
else:
|
169
|
+
self.console.print(msg)
|
170
|
+
self.console.file.flush()
|
@@ -45,6 +45,25 @@ class PromptHandler:
|
|
45
45
|
def handle(self) -> None:
|
46
46
|
import traceback
|
47
47
|
|
48
|
+
# Check if interactive mode is requested - if so, switch to chat mode
|
49
|
+
if getattr(self.args, "interactive", False):
|
50
|
+
from janito.cli.chat_mode.session import ChatSession
|
51
|
+
from rich.console import Console
|
52
|
+
|
53
|
+
console = Console()
|
54
|
+
session = ChatSession(
|
55
|
+
console,
|
56
|
+
self.provider_instance,
|
57
|
+
self.llm_driver_config,
|
58
|
+
role=self.role,
|
59
|
+
args=self.args,
|
60
|
+
verbose_tools=getattr(self.args, "verbose_tools", False),
|
61
|
+
verbose_agent=getattr(self.args, "verbose_agent", False),
|
62
|
+
allowed_permissions=getattr(self, 'allowed_permissions', None),
|
63
|
+
)
|
64
|
+
session.run()
|
65
|
+
return
|
66
|
+
|
48
67
|
user_prompt = " ".join(getattr(self.args, "user_prompt", [])).strip()
|
49
68
|
# UTF-8 sanitize user_prompt
|
50
69
|
sanitized = user_prompt
|
janito/llm/agent.py
CHANGED
@@ -4,6 +4,17 @@ from janito.conversation_history import LLMConversationHistory
|
|
4
4
|
from janito.tools.tools_adapter import ToolsAdapterBase
|
5
5
|
from queue import Queue, Empty
|
6
6
|
from janito.driver_events import RequestStatus
|
7
|
+
from janito.agent_events import (
|
8
|
+
AgentInitialized,
|
9
|
+
AgentChatStarted,
|
10
|
+
AgentChatFinished,
|
11
|
+
AgentProcessingResponse,
|
12
|
+
AgentToolCallStarted,
|
13
|
+
AgentToolCallFinished,
|
14
|
+
AgentWaitingForResponse,
|
15
|
+
AgentReceivedResponse,
|
16
|
+
AgentShutdown
|
17
|
+
)
|
7
18
|
from typing import Any, Optional, List, Iterator, Union
|
8
19
|
import threading
|
9
20
|
import logging
|
@@ -53,6 +64,9 @@ class LLMAgent:
|
|
53
64
|
self._latest_event = None
|
54
65
|
self.verbose_agent = verbose_agent
|
55
66
|
self.driver = None # Will be set by setup_agent if available
|
67
|
+
|
68
|
+
# Emit agent initialized event
|
69
|
+
event_bus.publish(AgentInitialized(agent_name=self.agent_name))
|
56
70
|
|
57
71
|
def get_provider_name(self):
|
58
72
|
# Try to get provider name from driver, fallback to llm_provider, else '?'
|
@@ -178,6 +192,9 @@ class LLMAgent:
|
|
178
192
|
Wait for a single event from the output queue (with timeout), process it, and return the result.
|
179
193
|
This function is intended to be called from the main agent loop, which controls the overall flow.
|
180
194
|
"""
|
195
|
+
# Emit agent waiting for response event
|
196
|
+
event_bus.publish(AgentWaitingForResponse(agent_name=self.agent_name))
|
197
|
+
|
181
198
|
if getattr(self, "verbose_agent", False):
|
182
199
|
print("[agent] [DEBUG] Entered _process_next_response")
|
183
200
|
elapsed = 0.0
|
@@ -204,6 +221,10 @@ class LLMAgent:
|
|
204
221
|
if getattr(self, "verbose_agent", False):
|
205
222
|
print(f"[agent] [DEBUG] Waiting for LLM response... ({elapsed:.1f}s elapsed)")
|
206
223
|
continue
|
224
|
+
|
225
|
+
# Emit agent received response event
|
226
|
+
event_bus.publish(AgentReceivedResponse(agent_name=self.agent_name, response=event))
|
227
|
+
|
207
228
|
if getattr(self, "verbose_agent", False):
|
208
229
|
print(f"[agent] [DEBUG] Received event from output_queue: {event}")
|
209
230
|
event_bus.publish(event)
|
@@ -233,6 +254,10 @@ class LLMAgent:
|
|
233
254
|
"""
|
234
255
|
if getattr(self, "verbose_agent", False):
|
235
256
|
print("[agent] [INFO] Handling ResponseReceived event.")
|
257
|
+
|
258
|
+
# Emit agent processing response event
|
259
|
+
event_bus.publish(AgentProcessingResponse(agent_name=self.agent_name, response=event))
|
260
|
+
|
236
261
|
from janito.llm.message_parts import FunctionCallMessagePart
|
237
262
|
|
238
263
|
# Skip tool processing if no tools adapter is available
|
@@ -249,6 +274,15 @@ class LLMAgent:
|
|
249
274
|
print(
|
250
275
|
f"[agent] [DEBUG] Tool call detected: {getattr(part, 'name', repr(part))} with arguments: {getattr(part, 'arguments', None)}"
|
251
276
|
)
|
277
|
+
|
278
|
+
# Emit agent tool call started event
|
279
|
+
event_bus.publish(AgentToolCallStarted(
|
280
|
+
agent_name=self.agent_name,
|
281
|
+
tool_call_id=getattr(part, 'tool_call_id', None),
|
282
|
+
name=getattr(part, 'name', None),
|
283
|
+
arguments=getattr(part, 'arguments', None)
|
284
|
+
))
|
285
|
+
|
252
286
|
tool_calls.append(part)
|
253
287
|
try:
|
254
288
|
result = self.tools_adapter.execute_function_call_message_part(part)
|
@@ -257,6 +291,14 @@ class LLMAgent:
|
|
257
291
|
# instead of letting it propagate to the user
|
258
292
|
result = str(e)
|
259
293
|
tool_results.append(result)
|
294
|
+
|
295
|
+
# Emit agent tool call finished event
|
296
|
+
event_bus.publish(AgentToolCallFinished(
|
297
|
+
agent_name=self.agent_name,
|
298
|
+
tool_call_id=getattr(part, 'tool_call_id', None),
|
299
|
+
name=getattr(part, 'name', None),
|
300
|
+
result=result
|
301
|
+
))
|
260
302
|
if tool_calls:
|
261
303
|
# Prepare tool_calls message for assistant
|
262
304
|
tool_calls_list = []
|
@@ -316,6 +358,14 @@ class LLMAgent:
|
|
316
358
|
role: str = "user",
|
317
359
|
config=None,
|
318
360
|
):
|
361
|
+
# Emit agent chat started event
|
362
|
+
event_bus.publish(AgentChatStarted(
|
363
|
+
agent_name=self.agent_name,
|
364
|
+
prompt=prompt,
|
365
|
+
messages=messages,
|
366
|
+
role=role
|
367
|
+
))
|
368
|
+
|
319
369
|
self._clear_driver_queues()
|
320
370
|
self._validate_and_update_history(prompt, messages, role)
|
321
371
|
self._ensure_system_prompt()
|
@@ -339,6 +389,12 @@ class LLMAgent:
|
|
339
389
|
f"[agent] [DEBUG] Returned from _process_next_response: result={result}, added_tool_results={added_tool_results}"
|
340
390
|
)
|
341
391
|
if self._should_exit_chat_loop(result, added_tool_results):
|
392
|
+
# Emit agent chat finished event
|
393
|
+
event_bus.publish(AgentChatFinished(
|
394
|
+
agent_name=self.agent_name,
|
395
|
+
result=result,
|
396
|
+
loop_count=loop_count
|
397
|
+
))
|
342
398
|
return result
|
343
399
|
loop_count += 1
|
344
400
|
|
@@ -502,6 +558,9 @@ class LLMAgent:
|
|
502
558
|
:param timeout: Optional timeout in seconds.
|
503
559
|
Handles KeyboardInterrupt gracefully.
|
504
560
|
"""
|
561
|
+
# Emit agent shutdown event
|
562
|
+
event_bus.publish(AgentShutdown(agent_name=self.agent_name))
|
563
|
+
|
505
564
|
if (
|
506
565
|
hasattr(self, "driver")
|
507
566
|
and self.driver
|
@@ -30,6 +30,7 @@ from .show_image_grid import ShowImageGridTool
|
|
30
30
|
from janito.tools.tool_base import ToolPermissions
|
31
31
|
import os
|
32
32
|
from janito.tools.permissions import get_global_allowed_permissions
|
33
|
+
from janito.platform_discovery import PlatformDiscovery
|
33
34
|
|
34
35
|
# Singleton tools adapter with all standard tools registered
|
35
36
|
local_tools_adapter = LocalToolsAdapter(workdir=os.getcwd())
|
@@ -40,6 +41,9 @@ def get_local_tools_adapter(workdir=None):
|
|
40
41
|
|
41
42
|
|
42
43
|
# Register tools
|
44
|
+
pd = PlatformDiscovery()
|
45
|
+
is_powershell = pd.detect_shell().startswith("PowerShell")
|
46
|
+
|
43
47
|
for tool_class in [
|
44
48
|
AskUserTool,
|
45
49
|
CopyFileTool,
|
@@ -68,6 +72,9 @@ for tool_class in [
|
|
68
72
|
ShowImageTool,
|
69
73
|
ShowImageGridTool,
|
70
74
|
]:
|
75
|
+
# Skip bash tools when running in PowerShell
|
76
|
+
if is_powershell and tool_class.__name__ in ["RunBashCommandTool"]:
|
77
|
+
continue
|
71
78
|
local_tools_adapter.register_tool(tool_class)
|
72
79
|
|
73
80
|
# DEBUG: Print registered tools at startup
|
@@ -6,6 +6,7 @@ from janito.report_events import ReportAction
|
|
6
6
|
from janito.i18n import tr
|
7
7
|
import os
|
8
8
|
from janito.tools.path_utils import expand_path
|
9
|
+
from pathlib import Path
|
9
10
|
|
10
11
|
|
11
12
|
@register_local_tool
|
@@ -43,6 +44,8 @@ class CreateDirectoryTool(ToolBase):
|
|
43
44
|
"❌ Path '{disp_path}' exists and is not a directory.",
|
44
45
|
disp_path=disp_path,
|
45
46
|
)
|
47
|
+
# Generate content summary
|
48
|
+
content_summary = self._get_directory_summary(path)
|
46
49
|
self.report_error(
|
47
50
|
tr(
|
48
51
|
"❗ Directory '{disp_path}' already exists.",
|
@@ -50,8 +53,9 @@ class CreateDirectoryTool(ToolBase):
|
|
50
53
|
)
|
51
54
|
)
|
52
55
|
return tr(
|
53
|
-
"❗ Cannot create directory: '{disp_path}' already exists
|
56
|
+
"❗ Cannot create directory: '{disp_path}' already exists.\n{summary}",
|
54
57
|
disp_path=disp_path,
|
58
|
+
summary=content_summary,
|
55
59
|
)
|
56
60
|
os.makedirs(path, exist_ok=True)
|
57
61
|
self.report_success(tr("✅ Directory created"))
|
@@ -68,3 +72,42 @@ class CreateDirectoryTool(ToolBase):
|
|
68
72
|
)
|
69
73
|
)
|
70
74
|
return tr("❌ Cannot create directory: {error}", error=e)
|
75
|
+
|
76
|
+
def _get_directory_summary(self, path: str) -> str:
|
77
|
+
"""Generate a summary of directory contents."""
|
78
|
+
try:
|
79
|
+
path_obj = Path(path)
|
80
|
+
if not path_obj.exists() or not path_obj.is_dir():
|
81
|
+
return ""
|
82
|
+
|
83
|
+
items = list(path_obj.iterdir())
|
84
|
+
if not items:
|
85
|
+
return "Directory is empty."
|
86
|
+
|
87
|
+
# Count files and directories
|
88
|
+
file_count = sum(1 for item in items if item.is_file())
|
89
|
+
dir_count = sum(1 for item in items if item.is_dir())
|
90
|
+
|
91
|
+
summary_parts = []
|
92
|
+
if file_count > 0:
|
93
|
+
summary_parts.append(f"{file_count} file{'s' if file_count != 1 else ''}")
|
94
|
+
if dir_count > 0:
|
95
|
+
summary_parts.append(f"{dir_count} subdirector{'y' if dir_count == 1 else 'ies'}")
|
96
|
+
|
97
|
+
# Show first few items as examples
|
98
|
+
examples = []
|
99
|
+
for item in sorted(items)[:3]: # Show up to 3 items
|
100
|
+
if item.is_dir():
|
101
|
+
examples.append(f"📁 {item.name}")
|
102
|
+
else:
|
103
|
+
examples.append(f"📄 {item.name}")
|
104
|
+
|
105
|
+
result = f"Contains: {', '.join(summary_parts)}."
|
106
|
+
if examples:
|
107
|
+
result += f"\nExamples: {', '.join(examples)}"
|
108
|
+
if len(items) > 3:
|
109
|
+
result += f" (and {len(items) - 3} more)"
|
110
|
+
|
111
|
+
return result
|
112
|
+
except Exception:
|
113
|
+
return "Unable to read directory contents."
|