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.
Files changed (107) hide show
  1. soothe_cli/__init__.py +5 -0
  2. soothe_cli/cli/__init__.py +1 -0
  3. soothe_cli/cli/commands/__init__.py +1 -0
  4. soothe_cli/cli/commands/autopilot_cmd.py +410 -0
  5. soothe_cli/cli/commands/config_cmd.py +277 -0
  6. soothe_cli/cli/commands/run_cmd.py +87 -0
  7. soothe_cli/cli/commands/status_cmd.py +121 -0
  8. soothe_cli/cli/commands/subagent_names.py +17 -0
  9. soothe_cli/cli/commands/thread_cmd.py +657 -0
  10. soothe_cli/cli/execution/__init__.py +6 -0
  11. soothe_cli/cli/execution/daemon.py +194 -0
  12. soothe_cli/cli/execution/headless.py +99 -0
  13. soothe_cli/cli/execution/launcher.py +31 -0
  14. soothe_cli/cli/main.py +509 -0
  15. soothe_cli/cli/renderer.py +444 -0
  16. soothe_cli/cli/stream/__init__.py +17 -0
  17. soothe_cli/cli/stream/context.py +138 -0
  18. soothe_cli/cli/stream/display_line.py +83 -0
  19. soothe_cli/cli/stream/formatter.py +412 -0
  20. soothe_cli/cli/stream/pipeline.py +521 -0
  21. soothe_cli/cli/utils.py +46 -0
  22. soothe_cli/config/__init__.py +5 -0
  23. soothe_cli/config/cli_config.py +155 -0
  24. soothe_cli/plan/__init__.py +5 -0
  25. soothe_cli/plan/rich_tree.py +54 -0
  26. soothe_cli/shared/__init__.py +107 -0
  27. soothe_cli/shared/command_router.py +246 -0
  28. soothe_cli/shared/config_loader.py +68 -0
  29. soothe_cli/shared/display_policy.py +413 -0
  30. soothe_cli/shared/essential_events.py +68 -0
  31. soothe_cli/shared/event_processor.py +823 -0
  32. soothe_cli/shared/message_processing.py +393 -0
  33. soothe_cli/shared/presentation_engine.py +173 -0
  34. soothe_cli/shared/processor_state.py +80 -0
  35. soothe_cli/shared/renderer_protocol.py +158 -0
  36. soothe_cli/shared/rendering.py +43 -0
  37. soothe_cli/shared/slash_commands.py +354 -0
  38. soothe_cli/shared/subagent_routing.py +63 -0
  39. soothe_cli/shared/suppression_state.py +188 -0
  40. soothe_cli/shared/tool_formatters/__init__.py +27 -0
  41. soothe_cli/shared/tool_formatters/base.py +109 -0
  42. soothe_cli/shared/tool_formatters/execution.py +297 -0
  43. soothe_cli/shared/tool_formatters/fallback.py +128 -0
  44. soothe_cli/shared/tool_formatters/file_ops.py +299 -0
  45. soothe_cli/shared/tool_formatters/goal_formatter.py +331 -0
  46. soothe_cli/shared/tool_formatters/media.py +291 -0
  47. soothe_cli/shared/tool_formatters/structured.py +202 -0
  48. soothe_cli/shared/tool_formatters/web.py +143 -0
  49. soothe_cli/shared/tool_output_formatter.py +227 -0
  50. soothe_cli/shared/tui_trace_log.py +40 -0
  51. soothe_cli/tui/__init__.py +5 -0
  52. soothe_cli/tui/_ask_user_types.py +50 -0
  53. soothe_cli/tui/_cli_context.py +27 -0
  54. soothe_cli/tui/_env_vars.py +56 -0
  55. soothe_cli/tui/_session_stats.py +114 -0
  56. soothe_cli/tui/_version.py +21 -0
  57. soothe_cli/tui/app.py +4992 -0
  58. soothe_cli/tui/app.tcss +302 -0
  59. soothe_cli/tui/command_registry.py +310 -0
  60. soothe_cli/tui/config.py +2381 -0
  61. soothe_cli/tui/daemon_session.py +233 -0
  62. soothe_cli/tui/file_ops.py +409 -0
  63. soothe_cli/tui/formatting.py +28 -0
  64. soothe_cli/tui/hooks.py +23 -0
  65. soothe_cli/tui/input.py +782 -0
  66. soothe_cli/tui/media_utils.py +471 -0
  67. soothe_cli/tui/model_config.py +518 -0
  68. soothe_cli/tui/output.py +69 -0
  69. soothe_cli/tui/project_utils.py +188 -0
  70. soothe_cli/tui/sessions.py +1248 -0
  71. soothe_cli/tui/skills/__init__.py +5 -0
  72. soothe_cli/tui/skills/invocation.py +74 -0
  73. soothe_cli/tui/skills/load.py +93 -0
  74. soothe_cli/tui/textual_adapter.py +1430 -0
  75. soothe_cli/tui/theme.py +838 -0
  76. soothe_cli/tui/tool_display.py +297 -0
  77. soothe_cli/tui/unicode_security.py +502 -0
  78. soothe_cli/tui/update_check.py +447 -0
  79. soothe_cli/tui/widgets/__init__.py +9 -0
  80. soothe_cli/tui/widgets/_links.py +63 -0
  81. soothe_cli/tui/widgets/approval.py +430 -0
  82. soothe_cli/tui/widgets/ask_user.py +392 -0
  83. soothe_cli/tui/widgets/autocomplete.py +666 -0
  84. soothe_cli/tui/widgets/autopilot_dashboard.py +308 -0
  85. soothe_cli/tui/widgets/autopilot_screen.py +64 -0
  86. soothe_cli/tui/widgets/chat_input.py +1834 -0
  87. soothe_cli/tui/widgets/clipboard.py +128 -0
  88. soothe_cli/tui/widgets/diff.py +240 -0
  89. soothe_cli/tui/widgets/editor.py +140 -0
  90. soothe_cli/tui/widgets/history.py +221 -0
  91. soothe_cli/tui/widgets/loading.py +194 -0
  92. soothe_cli/tui/widgets/mcp_viewer.py +352 -0
  93. soothe_cli/tui/widgets/message_store.py +693 -0
  94. soothe_cli/tui/widgets/messages.py +1720 -0
  95. soothe_cli/tui/widgets/model_selector.py +988 -0
  96. soothe_cli/tui/widgets/notification_settings.py +155 -0
  97. soothe_cli/tui/widgets/status.py +403 -0
  98. soothe_cli/tui/widgets/theme_selector.py +158 -0
  99. soothe_cli/tui/widgets/thread_selector.py +1865 -0
  100. soothe_cli/tui/widgets/tool_renderers.py +148 -0
  101. soothe_cli/tui/widgets/tool_widgets.py +254 -0
  102. soothe_cli/tui/widgets/tools.py +165 -0
  103. soothe_cli/tui/widgets/welcome.py +330 -0
  104. soothe_cli-0.1.0.dist-info/METADATA +100 -0
  105. soothe_cli-0.1.0.dist-info/RECORD +107 -0
  106. soothe_cli-0.1.0.dist-info/WHEEL +4 -0
  107. soothe_cli-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,331 @@
1
+ """Formatter for goal management 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 GoalFormatter(BaseFormatter):
12
+ """Formatter for goal management tools.
13
+
14
+ Handles: create_goal, list_goals, complete_goal, fail_goal
15
+
16
+ Provides semantic summaries with goal IDs, counts, and status.
17
+ """
18
+
19
+ def format(self, tool_name: str, result: Any) -> ToolBrief:
20
+ """Format goal management tool result.
21
+
22
+ Args:
23
+ tool_name: Name of the goal tool.
24
+ result: Tool result (dict with goal data).
25
+
26
+ Returns:
27
+ ToolBrief with goal summary.
28
+
29
+ Raises:
30
+ ValueError: If tool_name is not a recognized goal tool.
31
+
32
+ Example:
33
+ >>> formatter = GoalFormatter()
34
+ >>> brief = formatter.format("create_goal", {"created": {"id": "g1"}})
35
+ >>> brief.to_display()
36
+ '✓ Created goal g1'
37
+ """
38
+ # Normalize tool name
39
+ normalized = tool_name.lower().replace("-", "_").replace(" ", "_")
40
+
41
+ # Route to specific formatter
42
+ if normalized == "create_goal":
43
+ return self._format_create_goal(result)
44
+ if normalized == "list_goals":
45
+ return self._format_list_goals(result)
46
+ if normalized == "complete_goal":
47
+ return self._format_complete_goal(result)
48
+ if normalized == "fail_goal":
49
+ return self._format_fail_goal(result)
50
+ msg = f"Unknown goal tool: {tool_name}"
51
+ raise ValueError(msg)
52
+
53
+ def _format_create_goal(self, result: dict[str, Any]) -> ToolBrief:
54
+ """Format create_goal result.
55
+
56
+ Shows created goal ID.
57
+
58
+ Args:
59
+ result: Dict with 'created' field containing goal object.
60
+
61
+ Returns:
62
+ ToolBrief with goal ID.
63
+
64
+ Example:
65
+ >>> brief = formatter._format_create_goal({"created": {"id": "g1", "priority": 80}})
66
+ >>> brief.summary
67
+ 'Created goal g1'
68
+ """
69
+ # Handle dict result
70
+ if isinstance(result, dict):
71
+ # Check for error
72
+ if "error" in result:
73
+ error_msg = str(result["error"])
74
+ return ToolBrief(
75
+ icon="✗",
76
+ summary="Create failed",
77
+ detail=self._truncate_text(error_msg, 80),
78
+ metrics={"error": True},
79
+ )
80
+
81
+ # Extract goal data
82
+ created = result.get("created", {})
83
+ goal_id = created.get("id", "unknown")
84
+ priority = created.get("priority")
85
+
86
+ # Build summary
87
+ summary = f"Created goal {goal_id}"
88
+
89
+ # Build detail
90
+ detail = None
91
+ if priority is not None:
92
+ detail = f"priority: {priority}"
93
+
94
+ return ToolBrief(
95
+ icon="✓",
96
+ summary=summary,
97
+ detail=detail,
98
+ metrics={"goal_id": goal_id, "priority": priority},
99
+ )
100
+
101
+ # Handle string result (fallback)
102
+ if isinstance(result, str):
103
+ if "error" in result.lower() or "failed" in result.lower():
104
+ return ToolBrief(
105
+ icon="✗",
106
+ summary="Create failed",
107
+ detail=self._truncate_text(result, 80),
108
+ metrics={"error": True},
109
+ )
110
+
111
+ return ToolBrief(
112
+ icon="✓",
113
+ summary="Created goal",
114
+ detail=None,
115
+ metrics={},
116
+ )
117
+
118
+ # Unknown type
119
+ return ToolBrief(
120
+ icon="✓",
121
+ summary="Created goal",
122
+ detail=None,
123
+ metrics={},
124
+ )
125
+
126
+ def _format_list_goals(self, result: dict[str, Any]) -> ToolBrief:
127
+ """Format list_goals result.
128
+
129
+ Shows count of goals.
130
+
131
+ Args:
132
+ result: Dict with 'goals' field containing list of goal objects.
133
+
134
+ Returns:
135
+ ToolBrief with goal count.
136
+
137
+ Example:
138
+ >>> brief = formatter._format_list_goals({"goals": [{"id": "g1"}, {"id": "g2"}]})
139
+ >>> brief.summary
140
+ 'Found 2 goals'
141
+ """
142
+ # Handle dict result
143
+ if isinstance(result, dict):
144
+ # Check for error
145
+ if "error" in result:
146
+ error_msg = str(result["error"])
147
+ return ToolBrief(
148
+ icon="✗",
149
+ summary="List failed",
150
+ detail=self._truncate_text(error_msg, 80),
151
+ metrics={"error": True},
152
+ )
153
+
154
+ # Extract goals
155
+ goals = result.get("goals", [])
156
+ count = len(goals)
157
+
158
+ # Build summary
159
+ summary = f"Found {count} goal{'s' if count != 1 else ''}"
160
+
161
+ return ToolBrief(
162
+ icon="✓",
163
+ summary=summary,
164
+ detail=None,
165
+ metrics={"count": count},
166
+ )
167
+
168
+ # Handle string result (fallback)
169
+ if isinstance(result, str):
170
+ if "error" in result.lower() or "failed" in result.lower():
171
+ return ToolBrief(
172
+ icon="✗",
173
+ summary="List failed",
174
+ detail=self._truncate_text(result, 80),
175
+ metrics={"error": True},
176
+ )
177
+
178
+ return ToolBrief(
179
+ icon="✓",
180
+ summary="Listed goals",
181
+ detail=None,
182
+ metrics={},
183
+ )
184
+
185
+ # Unknown type
186
+ return ToolBrief(
187
+ icon="✓",
188
+ summary="Listed goals",
189
+ detail=None,
190
+ metrics={},
191
+ )
192
+
193
+ def _format_complete_goal(self, result: dict[str, Any]) -> ToolBrief:
194
+ """Format complete_goal result.
195
+
196
+ Shows completed goal ID.
197
+
198
+ Args:
199
+ result: Dict with 'completed' field containing goal object.
200
+
201
+ Returns:
202
+ ToolBrief with goal ID.
203
+
204
+ Example:
205
+ >>> brief = formatter._format_complete_goal({"completed": {"id": "g1"}})
206
+ >>> brief.summary
207
+ 'Completed goal g1'
208
+ """
209
+ # Handle dict result
210
+ if isinstance(result, dict):
211
+ # Check for error
212
+ if "error" in result:
213
+ error_msg = str(result["error"])
214
+ return ToolBrief(
215
+ icon="✗",
216
+ summary="Complete failed",
217
+ detail=self._truncate_text(error_msg, 80),
218
+ metrics={"error": True},
219
+ )
220
+
221
+ # Extract goal data
222
+ completed = result.get("completed", {})
223
+ goal_id = completed.get("id", "unknown")
224
+
225
+ # Build summary
226
+ summary = f"Completed goal {goal_id}"
227
+
228
+ return ToolBrief(
229
+ icon="✓",
230
+ summary=summary,
231
+ detail=None,
232
+ metrics={"goal_id": goal_id},
233
+ )
234
+
235
+ # Handle string result (fallback)
236
+ if isinstance(result, str):
237
+ if "error" in result.lower() or "failed" in result.lower():
238
+ return ToolBrief(
239
+ icon="✗",
240
+ summary="Complete failed",
241
+ detail=self._truncate_text(result, 80),
242
+ metrics={"error": True},
243
+ )
244
+
245
+ return ToolBrief(
246
+ icon="✓",
247
+ summary="Completed goal",
248
+ detail=None,
249
+ metrics={},
250
+ )
251
+
252
+ # Unknown type
253
+ return ToolBrief(
254
+ icon="✓",
255
+ summary="Completed goal",
256
+ detail=None,
257
+ metrics={},
258
+ )
259
+
260
+ def _format_fail_goal(self, result: dict[str, Any]) -> ToolBrief:
261
+ """Format fail_goal result.
262
+
263
+ Shows failed goal ID and reason.
264
+
265
+ Args:
266
+ result: Dict with 'failed' field containing goal object.
267
+
268
+ Returns:
269
+ ToolBrief with goal ID and failure reason.
270
+
271
+ Example:
272
+ >>> brief = formatter._format_fail_goal({"failed": {"id": "g1", "reason": "blocked"}})
273
+ >>> brief.summary
274
+ 'Failed goal g1'
275
+ >>> brief.detail
276
+ 'reason: blocked'
277
+ """
278
+ # Handle dict result
279
+ if isinstance(result, dict):
280
+ # Check for error
281
+ if "error" in result:
282
+ error_msg = str(result["error"])
283
+ return ToolBrief(
284
+ icon="✗",
285
+ summary="Fail operation failed",
286
+ detail=self._truncate_text(error_msg, 80),
287
+ metrics={"error": True},
288
+ )
289
+
290
+ # Extract goal data
291
+ failed = result.get("failed", {})
292
+ goal_id = failed.get("id", "unknown")
293
+ reason = failed.get("reason", "unknown reason")
294
+
295
+ # Build summary
296
+ summary = f"Failed goal {goal_id}"
297
+
298
+ # Build detail
299
+ detail = f"reason: {reason}"
300
+
301
+ return ToolBrief(
302
+ icon="✗",
303
+ summary=summary,
304
+ detail=detail,
305
+ metrics={"goal_id": goal_id, "reason": reason},
306
+ )
307
+
308
+ # Handle string result (fallback)
309
+ if isinstance(result, str):
310
+ if "error" in result.lower() or "failed" in result.lower():
311
+ return ToolBrief(
312
+ icon="✗",
313
+ summary="Fail operation failed",
314
+ detail=self._truncate_text(result, 80),
315
+ metrics={"error": True},
316
+ )
317
+
318
+ return ToolBrief(
319
+ icon="✗",
320
+ summary="Failed goal",
321
+ detail=None,
322
+ metrics={},
323
+ )
324
+
325
+ # Unknown type
326
+ return ToolBrief(
327
+ icon="✗",
328
+ summary="Failed goal",
329
+ detail=None,
330
+ metrics={},
331
+ )
@@ -0,0 +1,291 @@
1
+ """Formatter for media 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 MediaFormatter(BaseFormatter):
12
+ """Formatter for media tools.
13
+
14
+ Handles: transcribe_audio, get_video_info, analyze_image
15
+
16
+ Provides semantic summaries with duration, resolution, and format metrics.
17
+ """
18
+
19
+ def format(self, tool_name: str, result: Any) -> ToolBrief:
20
+ """Format media tool result.
21
+
22
+ Args:
23
+ tool_name: Name of the media tool.
24
+ result: Tool result (dict with media metadata).
25
+
26
+ Returns:
27
+ ToolBrief with media summary.
28
+
29
+ Raises:
30
+ ValueError: If tool_name is not a recognized media tool.
31
+
32
+ Example:
33
+ >>> formatter = MediaFormatter()
34
+ >>> brief = formatter.format("transcribe_audio", {"duration": 45.2, "language": "en"})
35
+ >>> brief.to_display()
36
+ '✓ Transcribed 45.2s (en)'
37
+ """
38
+ # Normalize tool name
39
+ normalized = tool_name.lower().replace("-", "_").replace(" ", "_")
40
+
41
+ # Route to specific formatter
42
+ if normalized == "transcribe_audio":
43
+ return self._format_transcribe_audio(result)
44
+ if normalized == "get_video_info":
45
+ return self._format_get_video_info(result)
46
+ if normalized == "analyze_image":
47
+ return self._format_analyze_image(result)
48
+ msg = f"Unknown media tool: {tool_name}"
49
+ raise ValueError(msg)
50
+
51
+ def _format_transcribe_audio(self, result: dict[str, Any]) -> ToolBrief:
52
+ """Format transcribe_audio result.
53
+
54
+ Shows duration and language.
55
+
56
+ Args:
57
+ result: Dict with 'text', 'duration', 'language', and optional 'error'.
58
+
59
+ Returns:
60
+ ToolBrief with transcription summary.
61
+
62
+ Example:
63
+ >>> brief = formatter._format_transcribe_audio(
64
+ ... {"duration": 45.2, "language": "en", "text": "hello"}
65
+ ... )
66
+ >>> brief.summary
67
+ 'Transcribed 45.2s'
68
+ >>> brief.detail
69
+ 'language: en'
70
+ """
71
+ # Handle dict result
72
+ if isinstance(result, dict):
73
+ # Check for error
74
+ if "error" in result:
75
+ error_msg = str(result["error"])
76
+ return ToolBrief(
77
+ icon="✗",
78
+ summary="Transcription failed",
79
+ detail=self._truncate_text(error_msg, 80),
80
+ metrics={"error": True},
81
+ )
82
+
83
+ # Extract metadata
84
+ duration = result.get("duration", 0.0)
85
+ language = result.get("language", "unknown")
86
+ text_length = len(result.get("text", ""))
87
+
88
+ # Build summary
89
+ summary = f"Transcribed {duration:.1f}s"
90
+
91
+ # Build detail
92
+ detail = f"language: {language}"
93
+
94
+ return ToolBrief(
95
+ icon="✓",
96
+ summary=summary,
97
+ detail=detail,
98
+ metrics={
99
+ "duration": duration,
100
+ "language": language,
101
+ "text_length": text_length,
102
+ },
103
+ )
104
+
105
+ # Handle string result (fallback)
106
+ if isinstance(result, str):
107
+ if "error" in result.lower() or "failed" in result.lower():
108
+ return ToolBrief(
109
+ icon="✗",
110
+ summary="Transcription failed",
111
+ detail=self._truncate_text(result, 80),
112
+ metrics={"error": True},
113
+ )
114
+
115
+ return ToolBrief(
116
+ icon="✓",
117
+ summary="Transcribed",
118
+ detail=f"{len(result)} chars",
119
+ metrics={},
120
+ )
121
+
122
+ # Unknown type
123
+ return ToolBrief(
124
+ icon="✓",
125
+ summary="Transcribed",
126
+ detail=None,
127
+ metrics={},
128
+ )
129
+
130
+ def _format_get_video_info(self, result: dict[str, Any]) -> ToolBrief:
131
+ """Format get_video_info result.
132
+
133
+ Shows duration, resolution, and format.
134
+
135
+ Args:
136
+ result: Dict with 'duration_seconds', 'format', 'codec', and optional 'error'.
137
+
138
+ Returns:
139
+ ToolBrief with video info summary.
140
+
141
+ Example:
142
+ >>> brief = formatter._format_get_video_info({"duration_seconds": 120, "format": "mp4"})
143
+ >>> brief.summary
144
+ 'Video: 120s'
145
+ """
146
+ # Handle dict result
147
+ if isinstance(result, dict):
148
+ # Check for error
149
+ if "error" in result:
150
+ error_msg = str(result["error"])
151
+ return ToolBrief(
152
+ icon="✗",
153
+ summary="Video info failed",
154
+ detail=self._truncate_text(error_msg, 80),
155
+ metrics={"error": True},
156
+ )
157
+
158
+ # Extract metadata
159
+ duration = result.get("duration_seconds", 0.0)
160
+ video_format = result.get("format", "unknown")
161
+ codec = result.get("codec", "unknown")
162
+ size_bytes = result.get("size_bytes", 0)
163
+
164
+ # Build summary
165
+ summary = f"Video: {duration:.0f}s"
166
+
167
+ # Build detail with resolution if available (not in basic schema)
168
+ # Just show format for now
169
+ detail = f"{video_format}, {codec}"
170
+
171
+ return ToolBrief(
172
+ icon="✓",
173
+ summary=summary,
174
+ detail=detail,
175
+ metrics={
176
+ "duration": duration,
177
+ "format": video_format,
178
+ "codec": codec,
179
+ "size_bytes": size_bytes,
180
+ },
181
+ )
182
+
183
+ # Handle string result (fallback)
184
+ if isinstance(result, str):
185
+ if "error" in result.lower() or "failed" in result.lower():
186
+ return ToolBrief(
187
+ icon="✗",
188
+ summary="Video info failed",
189
+ detail=self._truncate_text(result, 80),
190
+ metrics={"error": True},
191
+ )
192
+
193
+ return ToolBrief(
194
+ icon="✓",
195
+ summary="Video info retrieved",
196
+ detail=self._truncate_text(result, 80),
197
+ metrics={},
198
+ )
199
+
200
+ # Unknown type
201
+ return ToolBrief(
202
+ icon="✓",
203
+ summary="Video info retrieved",
204
+ detail=None,
205
+ metrics={},
206
+ )
207
+
208
+ def _format_analyze_image(self, result: dict[str, Any]) -> ToolBrief:
209
+ """Format analyze_image result.
210
+
211
+ Shows size and format.
212
+
213
+ Args:
214
+ result: Dict with image metadata and optional 'error'.
215
+
216
+ Returns:
217
+ ToolBrief with image analysis summary.
218
+
219
+ Example:
220
+ >>> brief = formatter._format_analyze_image({"size_bytes": 2400000, "format": "PNG"})
221
+ >>> brief.summary
222
+ 'Analyzed image'
223
+ >>> brief.detail
224
+ '2.3 MB, PNG'
225
+ """
226
+ # Handle dict result
227
+ if isinstance(result, dict):
228
+ # Check for error
229
+ if "error" in result:
230
+ error_msg = str(result["error"])
231
+ return ToolBrief(
232
+ icon="✗",
233
+ summary="Image analysis failed",
234
+ detail=self._truncate_text(error_msg, 80),
235
+ metrics={"error": True},
236
+ )
237
+
238
+ # Extract metadata
239
+ size_bytes = result.get("size_bytes", 0)
240
+ image_format = result.get("format", "unknown")
241
+ width = result.get("width")
242
+ height = result.get("height")
243
+
244
+ # Build summary
245
+ summary = "Analyzed image"
246
+
247
+ # Build detail
248
+ size_str = self._format_size(size_bytes)
249
+ detail_parts = [size_str, image_format]
250
+
251
+ if width and height:
252
+ detail_parts.append(f"{width}x{height}")
253
+
254
+ detail = ", ".join(detail_parts)
255
+
256
+ return ToolBrief(
257
+ icon="✓",
258
+ summary=summary,
259
+ detail=detail,
260
+ metrics={
261
+ "size_bytes": size_bytes,
262
+ "format": image_format,
263
+ "width": width,
264
+ "height": height,
265
+ },
266
+ )
267
+
268
+ # Handle string result (fallback)
269
+ if isinstance(result, str):
270
+ if "error" in result.lower() or "failed" in result.lower():
271
+ return ToolBrief(
272
+ icon="✗",
273
+ summary="Image analysis failed",
274
+ detail=self._truncate_text(result, 80),
275
+ metrics={"error": True},
276
+ )
277
+
278
+ return ToolBrief(
279
+ icon="✓",
280
+ summary="Analyzed image",
281
+ detail=self._truncate_text(result, 80),
282
+ metrics={},
283
+ )
284
+
285
+ # Unknown type
286
+ return ToolBrief(
287
+ icon="✓",
288
+ summary="Analyzed image",
289
+ detail=None,
290
+ metrics={},
291
+ )