soothe-cli 0.1.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.
- soothe_cli/__init__.py +5 -0
- soothe_cli/cli/__init__.py +1 -0
- soothe_cli/cli/commands/__init__.py +1 -0
- soothe_cli/cli/commands/autopilot_cmd.py +410 -0
- soothe_cli/cli/commands/config_cmd.py +277 -0
- soothe_cli/cli/commands/run_cmd.py +87 -0
- soothe_cli/cli/commands/status_cmd.py +121 -0
- soothe_cli/cli/commands/subagent_names.py +17 -0
- soothe_cli/cli/commands/thread_cmd.py +657 -0
- soothe_cli/cli/execution/__init__.py +6 -0
- soothe_cli/cli/execution/daemon.py +194 -0
- soothe_cli/cli/execution/headless.py +99 -0
- soothe_cli/cli/execution/launcher.py +31 -0
- soothe_cli/cli/main.py +509 -0
- soothe_cli/cli/renderer.py +444 -0
- soothe_cli/cli/stream/__init__.py +17 -0
- soothe_cli/cli/stream/context.py +138 -0
- soothe_cli/cli/stream/display_line.py +83 -0
- soothe_cli/cli/stream/formatter.py +412 -0
- soothe_cli/cli/stream/pipeline.py +521 -0
- soothe_cli/cli/utils.py +46 -0
- soothe_cli/config/__init__.py +5 -0
- soothe_cli/config/cli_config.py +155 -0
- soothe_cli/plan/__init__.py +5 -0
- soothe_cli/plan/rich_tree.py +54 -0
- soothe_cli/shared/__init__.py +107 -0
- soothe_cli/shared/command_router.py +246 -0
- soothe_cli/shared/config_loader.py +68 -0
- soothe_cli/shared/display_policy.py +413 -0
- soothe_cli/shared/essential_events.py +68 -0
- soothe_cli/shared/event_processor.py +823 -0
- soothe_cli/shared/message_processing.py +393 -0
- soothe_cli/shared/presentation_engine.py +173 -0
- soothe_cli/shared/processor_state.py +80 -0
- soothe_cli/shared/renderer_protocol.py +158 -0
- soothe_cli/shared/rendering.py +43 -0
- soothe_cli/shared/slash_commands.py +354 -0
- soothe_cli/shared/subagent_routing.py +63 -0
- soothe_cli/shared/suppression_state.py +188 -0
- soothe_cli/shared/tool_formatters/__init__.py +27 -0
- soothe_cli/shared/tool_formatters/base.py +109 -0
- soothe_cli/shared/tool_formatters/execution.py +297 -0
- soothe_cli/shared/tool_formatters/fallback.py +128 -0
- soothe_cli/shared/tool_formatters/file_ops.py +299 -0
- soothe_cli/shared/tool_formatters/goal_formatter.py +331 -0
- soothe_cli/shared/tool_formatters/media.py +291 -0
- soothe_cli/shared/tool_formatters/structured.py +202 -0
- soothe_cli/shared/tool_formatters/web.py +143 -0
- soothe_cli/shared/tool_output_formatter.py +227 -0
- soothe_cli/shared/tui_trace_log.py +40 -0
- soothe_cli/tui/__init__.py +5 -0
- soothe_cli/tui/_ask_user_types.py +50 -0
- soothe_cli/tui/_cli_context.py +27 -0
- soothe_cli/tui/_env_vars.py +56 -0
- soothe_cli/tui/_session_stats.py +114 -0
- soothe_cli/tui/_version.py +21 -0
- soothe_cli/tui/app.py +4992 -0
- soothe_cli/tui/app.tcss +302 -0
- soothe_cli/tui/command_registry.py +310 -0
- soothe_cli/tui/config.py +2381 -0
- soothe_cli/tui/daemon_session.py +233 -0
- soothe_cli/tui/file_ops.py +409 -0
- soothe_cli/tui/formatting.py +28 -0
- soothe_cli/tui/hooks.py +23 -0
- soothe_cli/tui/input.py +782 -0
- soothe_cli/tui/media_utils.py +471 -0
- soothe_cli/tui/model_config.py +518 -0
- soothe_cli/tui/output.py +69 -0
- soothe_cli/tui/project_utils.py +188 -0
- soothe_cli/tui/sessions.py +1248 -0
- soothe_cli/tui/skills/__init__.py +5 -0
- soothe_cli/tui/skills/invocation.py +74 -0
- soothe_cli/tui/skills/load.py +93 -0
- soothe_cli/tui/textual_adapter.py +1430 -0
- soothe_cli/tui/theme.py +838 -0
- soothe_cli/tui/tool_display.py +297 -0
- soothe_cli/tui/unicode_security.py +502 -0
- soothe_cli/tui/update_check.py +447 -0
- soothe_cli/tui/widgets/__init__.py +9 -0
- soothe_cli/tui/widgets/_links.py +63 -0
- soothe_cli/tui/widgets/approval.py +430 -0
- soothe_cli/tui/widgets/ask_user.py +392 -0
- soothe_cli/tui/widgets/autocomplete.py +666 -0
- soothe_cli/tui/widgets/autopilot_dashboard.py +308 -0
- soothe_cli/tui/widgets/autopilot_screen.py +64 -0
- soothe_cli/tui/widgets/chat_input.py +1834 -0
- soothe_cli/tui/widgets/clipboard.py +128 -0
- soothe_cli/tui/widgets/diff.py +240 -0
- soothe_cli/tui/widgets/editor.py +140 -0
- soothe_cli/tui/widgets/history.py +221 -0
- soothe_cli/tui/widgets/loading.py +194 -0
- soothe_cli/tui/widgets/mcp_viewer.py +352 -0
- soothe_cli/tui/widgets/message_store.py +693 -0
- soothe_cli/tui/widgets/messages.py +1720 -0
- soothe_cli/tui/widgets/model_selector.py +988 -0
- soothe_cli/tui/widgets/notification_settings.py +155 -0
- soothe_cli/tui/widgets/status.py +403 -0
- soothe_cli/tui/widgets/theme_selector.py +158 -0
- soothe_cli/tui/widgets/thread_selector.py +1865 -0
- soothe_cli/tui/widgets/tool_renderers.py +148 -0
- soothe_cli/tui/widgets/tool_widgets.py +254 -0
- soothe_cli/tui/widgets/tools.py +165 -0
- soothe_cli/tui/widgets/welcome.py +330 -0
- soothe_cli-0.1.0.dist-info/METADATA +100 -0
- soothe_cli-0.1.0.dist-info/RECORD +107 -0
- soothe_cli-0.1.0.dist-info/WHEEL +4 -0
- soothe_cli-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Fallback formatter for unknown tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from soothe_sdk.protocol import preview_first
|
|
8
|
+
|
|
9
|
+
from soothe_cli.shared.tool_formatters.base import BaseFormatter
|
|
10
|
+
from soothe_cli.shared.tool_output_formatter import ToolBrief
|
|
11
|
+
|
|
12
|
+
# RFC-0020 display constraints
|
|
13
|
+
MAX_SUMMARY_LENGTH = 50
|
|
14
|
+
MAX_DETAIL_LENGTH = 80
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FallbackFormatter(BaseFormatter):
|
|
18
|
+
"""Fallback formatter for unknown tools.
|
|
19
|
+
|
|
20
|
+
Provides simple truncation for tools that don't have specific formatters,
|
|
21
|
+
maintaining backward compatibility with existing tool outputs.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def format(self, tool_name: str, result: Any) -> ToolBrief: # noqa: ARG002
|
|
25
|
+
"""Format unknown tool result with simple truncation.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
tool_name: Name of the tool (unused, for logging).
|
|
29
|
+
result: Tool result (can be str, dict, or other).
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
ToolBrief with truncated content.
|
|
33
|
+
|
|
34
|
+
Example:
|
|
35
|
+
>>> formatter = FallbackFormatter()
|
|
36
|
+
>>> brief = formatter.format("unknown_tool", "Some long output...")
|
|
37
|
+
>>> brief.icon
|
|
38
|
+
'✓'
|
|
39
|
+
"""
|
|
40
|
+
# Handle string results
|
|
41
|
+
if isinstance(result, str):
|
|
42
|
+
# Check for error indicators
|
|
43
|
+
error_indicators = ["error:", "failed:", "exception:", "traceback"]
|
|
44
|
+
is_error = any(indicator in result.lower() for indicator in error_indicators)
|
|
45
|
+
|
|
46
|
+
if is_error:
|
|
47
|
+
# Extract error message (first line or first 80 chars)
|
|
48
|
+
first_line = result.split("\n")[0].strip()
|
|
49
|
+
error_msg = preview_first(first_line, 80)
|
|
50
|
+
return ToolBrief(
|
|
51
|
+
icon="✗",
|
|
52
|
+
summary="Failed",
|
|
53
|
+
detail=error_msg,
|
|
54
|
+
metrics={"error": True},
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Success - truncate to 50 chars for summary
|
|
58
|
+
summary = preview_first(result.replace("\n", " ").strip(), MAX_SUMMARY_LENGTH)
|
|
59
|
+
|
|
60
|
+
# Detail is first 80 chars if longer than summary
|
|
61
|
+
detail = None
|
|
62
|
+
if len(result) > MAX_SUMMARY_LENGTH:
|
|
63
|
+
detail = preview_first(result.replace("\n", " ").strip(), MAX_DETAIL_LENGTH)
|
|
64
|
+
|
|
65
|
+
return ToolBrief(icon="✓", summary=summary, detail=detail)
|
|
66
|
+
|
|
67
|
+
# Handle dict results
|
|
68
|
+
if isinstance(result, dict):
|
|
69
|
+
# Check for error field
|
|
70
|
+
if "error" in result:
|
|
71
|
+
error_msg = preview_first(str(result["error"]), 80)
|
|
72
|
+
return ToolBrief(
|
|
73
|
+
icon="✗",
|
|
74
|
+
summary="Failed",
|
|
75
|
+
detail=error_msg,
|
|
76
|
+
metrics={"error": True},
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Success - show dict summary
|
|
80
|
+
field_count = len(result)
|
|
81
|
+
return ToolBrief(
|
|
82
|
+
icon="✓",
|
|
83
|
+
summary="Completed",
|
|
84
|
+
detail=f"{field_count} fields",
|
|
85
|
+
metrics={"field_count": field_count},
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Handle ToolOutput (if available)
|
|
89
|
+
try:
|
|
90
|
+
from soothe_sdk import ToolOutput
|
|
91
|
+
|
|
92
|
+
if isinstance(result, ToolOutput):
|
|
93
|
+
if not result.success:
|
|
94
|
+
error_msg = preview_first(result.error, 80) if result.error else "Unknown error"
|
|
95
|
+
return ToolBrief(
|
|
96
|
+
icon="✗",
|
|
97
|
+
summary="Failed",
|
|
98
|
+
detail=error_msg,
|
|
99
|
+
metrics={"error": True, "error_type": result.error_type},
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Success with ToolOutput
|
|
103
|
+
if result.is_silent_failure():
|
|
104
|
+
return ToolBrief(
|
|
105
|
+
icon="⚠",
|
|
106
|
+
summary="No result",
|
|
107
|
+
detail="Tool succeeded but returned no data",
|
|
108
|
+
metrics={"silent_failure": True},
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Has data
|
|
112
|
+
data_type = type(result.data).__name__ if result.data else "None"
|
|
113
|
+
return ToolBrief(
|
|
114
|
+
icon="✓",
|
|
115
|
+
summary="Completed",
|
|
116
|
+
detail=f"data: {data_type}",
|
|
117
|
+
metrics={"has_data": result.data is not None},
|
|
118
|
+
)
|
|
119
|
+
except ImportError:
|
|
120
|
+
pass # ToolOutput not available
|
|
121
|
+
|
|
122
|
+
# Fallback for unknown types
|
|
123
|
+
return ToolBrief(
|
|
124
|
+
icon="✓",
|
|
125
|
+
summary="Completed",
|
|
126
|
+
detail=None,
|
|
127
|
+
metrics={},
|
|
128
|
+
)
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""Formatter for file operation tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from soothe_cli.shared.tool_formatters.base import BaseFormatter
|
|
8
|
+
from soothe_cli.shared.tool_output_formatter import ToolBrief
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FileOpsFormatter(BaseFormatter):
|
|
12
|
+
"""Formatter for file operation tools.
|
|
13
|
+
|
|
14
|
+
Handles: read_file, write_file, delete_file, list_files, search_files, glob, ls
|
|
15
|
+
|
|
16
|
+
Provides semantic summaries with size, line count, and item count metrics.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def format(self, tool_name: str, result: Any) -> ToolBrief:
|
|
20
|
+
r"""Format file operation tool result.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
tool_name: Name of the file operation tool.
|
|
24
|
+
result: Tool result (typically string for file operations).
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
ToolBrief with file operation summary.
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
ValueError: If tool_name is not a recognized file operation.
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
>>> formatter = FileOpsFormatter()
|
|
34
|
+
>>> brief = formatter.format("read_file", "Hello\nWorld\n")
|
|
35
|
+
>>> brief.to_display()
|
|
36
|
+
'✓ Read 12 B (2 lines)'
|
|
37
|
+
"""
|
|
38
|
+
# Normalize tool name
|
|
39
|
+
normalized = tool_name.lower().replace("-", "_").replace(" ", "_")
|
|
40
|
+
|
|
41
|
+
# Route to specific formatter
|
|
42
|
+
if normalized == "read_file":
|
|
43
|
+
return self._format_read_file(result)
|
|
44
|
+
if normalized == "write_file":
|
|
45
|
+
return self._format_write_file(result)
|
|
46
|
+
if normalized == "delete_file":
|
|
47
|
+
return self._format_delete_file(result)
|
|
48
|
+
if normalized in ("list_files", "ls"):
|
|
49
|
+
return self._format_list_files(result)
|
|
50
|
+
if normalized == "search_files":
|
|
51
|
+
return self._format_search_files(result)
|
|
52
|
+
if normalized == "glob":
|
|
53
|
+
return self._format_glob(result)
|
|
54
|
+
msg = f"Unknown file operation tool: {tool_name}"
|
|
55
|
+
raise ValueError(msg)
|
|
56
|
+
|
|
57
|
+
def _format_read_file(self, result: str) -> ToolBrief:
|
|
58
|
+
r"""Format read_file result.
|
|
59
|
+
|
|
60
|
+
Shows file size and line count.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
result: File contents as string.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
ToolBrief with size and line count.
|
|
67
|
+
|
|
68
|
+
Example:
|
|
69
|
+
>>> brief = formatter._format_read_file("Line 1\nLine 2\nLine 3")
|
|
70
|
+
>>> brief.summary
|
|
71
|
+
'Read 18 B'
|
|
72
|
+
>>> brief.detail
|
|
73
|
+
'3 lines'
|
|
74
|
+
"""
|
|
75
|
+
# Check for error
|
|
76
|
+
if result.startswith("Error:"):
|
|
77
|
+
error_msg = result[6:].strip() # Remove "Error:" prefix
|
|
78
|
+
return ToolBrief(
|
|
79
|
+
icon="✗",
|
|
80
|
+
summary="Read failed",
|
|
81
|
+
detail=self._truncate_text(error_msg, 80),
|
|
82
|
+
metrics={"error": True},
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Calculate size
|
|
86
|
+
size_bytes = len(result.encode("utf-8"))
|
|
87
|
+
size_str = self._format_size(size_bytes)
|
|
88
|
+
|
|
89
|
+
# Count lines
|
|
90
|
+
lines = self._count_lines(result)
|
|
91
|
+
|
|
92
|
+
# Build summary
|
|
93
|
+
summary = f"Read {size_str}"
|
|
94
|
+
|
|
95
|
+
# Build detail
|
|
96
|
+
if lines == 0:
|
|
97
|
+
detail = "empty file"
|
|
98
|
+
elif lines == 1:
|
|
99
|
+
detail = "1 line"
|
|
100
|
+
else:
|
|
101
|
+
detail = f"{lines} lines"
|
|
102
|
+
|
|
103
|
+
return ToolBrief(
|
|
104
|
+
icon="✓",
|
|
105
|
+
summary=summary,
|
|
106
|
+
detail=detail,
|
|
107
|
+
metrics={"size_bytes": size_bytes, "lines": lines},
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def _format_write_file(self, result: str) -> ToolBrief:
|
|
111
|
+
"""Format write_file result.
|
|
112
|
+
|
|
113
|
+
Shows bytes written or success message.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
result: Success message or error string.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
ToolBrief with write status.
|
|
120
|
+
|
|
121
|
+
Example:
|
|
122
|
+
>>> brief = formatter._format_write_file("Successfully wrote to file")
|
|
123
|
+
>>> brief.summary
|
|
124
|
+
'Wrote 0 B'
|
|
125
|
+
"""
|
|
126
|
+
# Check for error
|
|
127
|
+
if "error" in result.lower() or "failed" in result.lower():
|
|
128
|
+
return ToolBrief(
|
|
129
|
+
icon="✗",
|
|
130
|
+
summary="Write failed",
|
|
131
|
+
detail=self._truncate_text(result, 80),
|
|
132
|
+
metrics={"error": True},
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Try to extract size from result (if available)
|
|
136
|
+
# Common patterns: "Wrote X bytes", "Successfully wrote X"
|
|
137
|
+
# For now, show simple success
|
|
138
|
+
return ToolBrief(
|
|
139
|
+
icon="✓",
|
|
140
|
+
summary="Wrote file",
|
|
141
|
+
detail=None,
|
|
142
|
+
metrics={},
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def _format_delete_file(self, result: str) -> ToolBrief:
|
|
146
|
+
"""Format delete_file result.
|
|
147
|
+
|
|
148
|
+
Shows deletion status.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
result: Success message or error string.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
ToolBrief with deletion status.
|
|
155
|
+
|
|
156
|
+
Example:
|
|
157
|
+
>>> brief = formatter._format_delete_file("File deleted successfully")
|
|
158
|
+
>>> brief.summary
|
|
159
|
+
'Deleted'
|
|
160
|
+
"""
|
|
161
|
+
# Check for error
|
|
162
|
+
if "error" in result.lower() or "failed" in result.lower():
|
|
163
|
+
return ToolBrief(
|
|
164
|
+
icon="✗",
|
|
165
|
+
summary="Delete failed",
|
|
166
|
+
detail=self._truncate_text(result, 80),
|
|
167
|
+
metrics={"error": True},
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
return ToolBrief(
|
|
171
|
+
icon="✓",
|
|
172
|
+
summary="Deleted",
|
|
173
|
+
detail=None,
|
|
174
|
+
metrics={},
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def _format_list_files(self, result: str) -> ToolBrief:
|
|
178
|
+
r"""Format list_files/ls result.
|
|
179
|
+
|
|
180
|
+
Shows count of items listed.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
result: List of files as string (newline-separated).
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
ToolBrief with item count.
|
|
187
|
+
|
|
188
|
+
Example:
|
|
189
|
+
>>> brief = formatter._format_list_files("file1.py\nfile2.py\nfile3.py")
|
|
190
|
+
>>> brief.summary
|
|
191
|
+
'Found 3 items'
|
|
192
|
+
"""
|
|
193
|
+
# Check for error
|
|
194
|
+
if "error" in result.lower() or "failed" in result.lower():
|
|
195
|
+
return ToolBrief(
|
|
196
|
+
icon="✗",
|
|
197
|
+
summary="List failed",
|
|
198
|
+
detail=self._truncate_text(result, 80),
|
|
199
|
+
metrics={"error": True},
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Count items (non-empty lines)
|
|
203
|
+
lines = [line for line in result.split("\n") if line.strip()]
|
|
204
|
+
count = len(lines)
|
|
205
|
+
|
|
206
|
+
# Build summary
|
|
207
|
+
summary = f"Found {count} item{'s' if count != 1 else ''}"
|
|
208
|
+
|
|
209
|
+
return ToolBrief(
|
|
210
|
+
icon="✓",
|
|
211
|
+
summary=summary,
|
|
212
|
+
detail=None,
|
|
213
|
+
metrics={"count": count},
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def _format_search_files(self, result: str) -> ToolBrief:
|
|
217
|
+
r"""Format search_files result.
|
|
218
|
+
|
|
219
|
+
Shows count of matches found.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
result: Search results as string (with line numbers and content).
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
ToolBrief with match count.
|
|
226
|
+
|
|
227
|
+
Example:
|
|
228
|
+
>>> brief = formatter._format_search_files("file.py:1:TODO\nfile.py:5:TODO")
|
|
229
|
+
>>> brief.summary
|
|
230
|
+
'Found 2 matches'
|
|
231
|
+
"""
|
|
232
|
+
# Check for error
|
|
233
|
+
if "error" in result.lower() or "failed" in result.lower():
|
|
234
|
+
return ToolBrief(
|
|
235
|
+
icon="✗",
|
|
236
|
+
summary="Search failed",
|
|
237
|
+
detail=self._truncate_text(result, 80),
|
|
238
|
+
metrics={"error": True},
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Check for "No matches found" or similar
|
|
242
|
+
if "no matches" in result.lower() or result.strip() == "":
|
|
243
|
+
return ToolBrief(
|
|
244
|
+
icon="✓",
|
|
245
|
+
summary="Found 0 matches",
|
|
246
|
+
detail=None,
|
|
247
|
+
metrics={"count": 0},
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# Count matches (non-empty lines)
|
|
251
|
+
lines = [line for line in result.split("\n") if line.strip()]
|
|
252
|
+
count = len(lines)
|
|
253
|
+
|
|
254
|
+
summary = f"Found {count} match{'es' if count != 1 else ''}"
|
|
255
|
+
|
|
256
|
+
return ToolBrief(
|
|
257
|
+
icon="✓",
|
|
258
|
+
summary=summary,
|
|
259
|
+
detail=None,
|
|
260
|
+
metrics={"count": count},
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
def _format_glob(self, result: str) -> ToolBrief:
|
|
264
|
+
r"""Format glob result.
|
|
265
|
+
|
|
266
|
+
Shows count of files matching pattern.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
result: List of file paths as string (newline-separated).
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
ToolBrief with file count.
|
|
273
|
+
|
|
274
|
+
Example:
|
|
275
|
+
>>> brief = formatter._format_glob("file1.py\nfile2.py")
|
|
276
|
+
>>> brief.summary
|
|
277
|
+
'Found 2 files'
|
|
278
|
+
"""
|
|
279
|
+
# Check for error
|
|
280
|
+
if "error" in result.lower() or "failed" in result.lower():
|
|
281
|
+
return ToolBrief(
|
|
282
|
+
icon="✗",
|
|
283
|
+
summary="Glob failed",
|
|
284
|
+
detail=self._truncate_text(result, 80),
|
|
285
|
+
metrics={"error": True},
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# Count files (non-empty lines)
|
|
289
|
+
lines = [line for line in result.split("\n") if line.strip()]
|
|
290
|
+
count = len(lines)
|
|
291
|
+
|
|
292
|
+
summary = f"Found {count} file{'s' if count != 1 else ''}"
|
|
293
|
+
|
|
294
|
+
return ToolBrief(
|
|
295
|
+
icon="✓",
|
|
296
|
+
summary=summary,
|
|
297
|
+
detail=None,
|
|
298
|
+
metrics={"count": count},
|
|
299
|
+
)
|