strix-agent 0.4.0__py3-none-any.whl → 0.6.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.
- strix/agents/StrixAgent/strix_agent.py +3 -3
- strix/agents/StrixAgent/system_prompt.jinja +30 -26
- strix/agents/base_agent.py +159 -75
- strix/agents/state.py +5 -2
- strix/config/__init__.py +12 -0
- strix/config/config.py +172 -0
- strix/interface/assets/tui_styles.tcss +195 -230
- strix/interface/cli.py +16 -41
- strix/interface/main.py +151 -74
- strix/interface/streaming_parser.py +119 -0
- strix/interface/tool_components/__init__.py +4 -0
- strix/interface/tool_components/agent_message_renderer.py +190 -0
- strix/interface/tool_components/agents_graph_renderer.py +54 -38
- strix/interface/tool_components/base_renderer.py +68 -36
- strix/interface/tool_components/browser_renderer.py +106 -91
- strix/interface/tool_components/file_edit_renderer.py +117 -36
- strix/interface/tool_components/finish_renderer.py +43 -10
- strix/interface/tool_components/notes_renderer.py +63 -38
- strix/interface/tool_components/proxy_renderer.py +133 -92
- strix/interface/tool_components/python_renderer.py +121 -8
- strix/interface/tool_components/registry.py +19 -12
- strix/interface/tool_components/reporting_renderer.py +196 -28
- strix/interface/tool_components/scan_info_renderer.py +22 -19
- strix/interface/tool_components/terminal_renderer.py +270 -90
- strix/interface/tool_components/thinking_renderer.py +8 -6
- strix/interface/tool_components/todo_renderer.py +225 -0
- strix/interface/tool_components/user_message_renderer.py +26 -19
- strix/interface/tool_components/web_search_renderer.py +7 -6
- strix/interface/tui.py +907 -262
- strix/interface/utils.py +236 -4
- strix/llm/__init__.py +6 -2
- strix/llm/config.py +8 -5
- strix/llm/dedupe.py +217 -0
- strix/llm/llm.py +209 -356
- strix/llm/memory_compressor.py +6 -5
- strix/llm/utils.py +17 -8
- strix/runtime/__init__.py +12 -3
- strix/runtime/docker_runtime.py +121 -202
- strix/runtime/tool_server.py +55 -95
- strix/skills/README.md +64 -0
- strix/skills/__init__.py +110 -0
- strix/{prompts → skills}/frameworks/nextjs.jinja +26 -0
- strix/skills/scan_modes/deep.jinja +145 -0
- strix/skills/scan_modes/quick.jinja +63 -0
- strix/skills/scan_modes/standard.jinja +91 -0
- strix/telemetry/README.md +38 -0
- strix/telemetry/__init__.py +7 -1
- strix/telemetry/posthog.py +137 -0
- strix/telemetry/tracer.py +194 -54
- strix/tools/__init__.py +11 -4
- strix/tools/agents_graph/agents_graph_actions.py +20 -21
- strix/tools/agents_graph/agents_graph_actions_schema.xml +8 -8
- strix/tools/browser/browser_actions.py +10 -6
- strix/tools/browser/browser_actions_schema.xml +6 -1
- strix/tools/browser/browser_instance.py +96 -48
- strix/tools/browser/tab_manager.py +121 -102
- strix/tools/context.py +12 -0
- strix/tools/executor.py +63 -4
- strix/tools/file_edit/file_edit_actions.py +6 -3
- strix/tools/file_edit/file_edit_actions_schema.xml +45 -3
- strix/tools/finish/finish_actions.py +80 -105
- strix/tools/finish/finish_actions_schema.xml +121 -14
- strix/tools/notes/notes_actions.py +6 -33
- strix/tools/notes/notes_actions_schema.xml +50 -46
- strix/tools/proxy/proxy_actions.py +14 -2
- strix/tools/proxy/proxy_actions_schema.xml +0 -1
- strix/tools/proxy/proxy_manager.py +28 -16
- strix/tools/python/python_actions.py +2 -2
- strix/tools/python/python_actions_schema.xml +9 -1
- strix/tools/python/python_instance.py +39 -37
- strix/tools/python/python_manager.py +43 -31
- strix/tools/registry.py +73 -12
- strix/tools/reporting/reporting_actions.py +218 -31
- strix/tools/reporting/reporting_actions_schema.xml +256 -8
- strix/tools/terminal/terminal_actions.py +2 -2
- strix/tools/terminal/terminal_actions_schema.xml +6 -0
- strix/tools/terminal/terminal_manager.py +41 -30
- strix/tools/thinking/thinking_actions_schema.xml +27 -25
- strix/tools/todo/__init__.py +18 -0
- strix/tools/todo/todo_actions.py +568 -0
- strix/tools/todo/todo_actions_schema.xml +225 -0
- strix/utils/__init__.py +0 -0
- strix/utils/resource_paths.py +13 -0
- {strix_agent-0.4.0.dist-info → strix_agent-0.6.2.dist-info}/METADATA +90 -65
- strix_agent-0.6.2.dist-info/RECORD +134 -0
- {strix_agent-0.4.0.dist-info → strix_agent-0.6.2.dist-info}/WHEEL +1 -1
- strix/llm/request_queue.py +0 -87
- strix/prompts/README.md +0 -64
- strix/prompts/__init__.py +0 -109
- strix_agent-0.4.0.dist-info/RECORD +0 -118
- /strix/{prompts → skills}/cloud/.gitkeep +0 -0
- /strix/{prompts → skills}/coordination/root_agent.jinja +0 -0
- /strix/{prompts → skills}/custom/.gitkeep +0 -0
- /strix/{prompts → skills}/frameworks/fastapi.jinja +0 -0
- /strix/{prompts → skills}/protocols/graphql.jinja +0 -0
- /strix/{prompts → skills}/reconnaissance/.gitkeep +0 -0
- /strix/{prompts → skills}/technologies/firebase_firestore.jinja +0 -0
- /strix/{prompts → skills}/technologies/supabase.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/authentication_jwt.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/broken_function_level_authorization.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/business_logic.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/csrf.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/idor.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/information_disclosure.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/insecure_file_uploads.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/mass_assignment.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/open_redirect.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/path_traversal_lfi_rfi.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/race_conditions.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/rce.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/sql_injection.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/ssrf.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/subdomain_takeover.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/xss.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/xxe.jinja +0 -0
- {strix_agent-0.4.0.dist-info → strix_agent-0.6.2.dist-info}/entry_points.txt +0 -0
- {strix_agent-0.4.0.dist-info → strix_agent-0.6.2.dist-info/licenses}/LICENSE +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from typing import Any, ClassVar
|
|
2
2
|
|
|
3
|
+
from rich.text import Text
|
|
3
4
|
from textual.widgets import Static
|
|
4
5
|
|
|
5
6
|
from .base_renderer import BaseToolRenderer
|
|
@@ -18,38 +19,42 @@ class ListRequestsRenderer(BaseToolRenderer):
|
|
|
18
19
|
|
|
19
20
|
httpql_filter = args.get("httpql_filter")
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
text = Text()
|
|
23
|
+
text.append("📋 ")
|
|
24
|
+
text.append("Listing requests", style="bold #06b6d4")
|
|
22
25
|
|
|
23
|
-
if
|
|
26
|
+
if isinstance(result, str) and result.strip():
|
|
27
|
+
text.append("\n ")
|
|
28
|
+
text.append(result.strip(), style="dim")
|
|
29
|
+
elif result and isinstance(result, dict) and "requests" in result:
|
|
24
30
|
requests = result["requests"]
|
|
25
31
|
if isinstance(requests, list) and requests:
|
|
26
|
-
|
|
27
|
-
for req in requests[:3]:
|
|
32
|
+
for req in requests[:25]:
|
|
28
33
|
if isinstance(req, dict):
|
|
29
34
|
method = req.get("method", "?")
|
|
30
35
|
path = req.get("path", "?")
|
|
31
36
|
response = req.get("response") or {}
|
|
32
37
|
status = response.get("statusCode", "?")
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
escaped_lines = [cls.escape_markup(line) for line in request_lines]
|
|
40
|
-
content_text = f"{header}\n [dim]{chr(10).join(escaped_lines)}[/]"
|
|
38
|
+
text.append("\n ")
|
|
39
|
+
text.append(f"{method} {path} → {status}", style="dim")
|
|
40
|
+
if len(requests) > 25:
|
|
41
|
+
text.append("\n ")
|
|
42
|
+
text.append(f"... +{len(requests) - 25} more", style="dim")
|
|
41
43
|
else:
|
|
42
|
-
|
|
44
|
+
text.append("\n ")
|
|
45
|
+
text.append("No requests found", style="dim")
|
|
43
46
|
elif httpql_filter:
|
|
44
47
|
filter_display = (
|
|
45
|
-
httpql_filter[:
|
|
48
|
+
httpql_filter[:500] + "..." if len(httpql_filter) > 500 else httpql_filter
|
|
46
49
|
)
|
|
47
|
-
|
|
50
|
+
text.append("\n ")
|
|
51
|
+
text.append(filter_display, style="dim")
|
|
48
52
|
else:
|
|
49
|
-
|
|
53
|
+
text.append("\n ")
|
|
54
|
+
text.append("All requests", style="dim")
|
|
50
55
|
|
|
51
56
|
css_classes = cls.get_css_classes("completed")
|
|
52
|
-
return Static(
|
|
57
|
+
return Static(text, classes=css_classes)
|
|
53
58
|
|
|
54
59
|
|
|
55
60
|
@register_tool_renderer
|
|
@@ -64,34 +69,41 @@ class ViewRequestRenderer(BaseToolRenderer):
|
|
|
64
69
|
|
|
65
70
|
part = args.get("part", "request")
|
|
66
71
|
|
|
67
|
-
|
|
72
|
+
text = Text()
|
|
73
|
+
text.append("👀 ")
|
|
74
|
+
text.append(f"Viewing {part}", style="bold #06b6d4")
|
|
68
75
|
|
|
69
|
-
if
|
|
76
|
+
if isinstance(result, str) and result.strip():
|
|
77
|
+
text.append("\n ")
|
|
78
|
+
text.append(result.strip(), style="dim")
|
|
79
|
+
elif result and isinstance(result, dict):
|
|
70
80
|
if "content" in result:
|
|
71
81
|
content = result["content"]
|
|
72
|
-
content_preview = content[:
|
|
73
|
-
|
|
82
|
+
content_preview = content[:2000] + "..." if len(content) > 2000 else content
|
|
83
|
+
text.append("\n ")
|
|
84
|
+
text.append(content_preview, style="dim")
|
|
74
85
|
elif "matches" in result:
|
|
75
86
|
matches = result["matches"]
|
|
76
87
|
if isinstance(matches, list) and matches:
|
|
77
|
-
|
|
78
|
-
match
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
escaped_lines = [cls.escape_markup(line) for line in match_lines]
|
|
85
|
-
content_text = f"{header}\n [dim]{chr(10).join(escaped_lines)}[/]"
|
|
88
|
+
for match in matches[:25]:
|
|
89
|
+
if isinstance(match, dict) and "match" in match:
|
|
90
|
+
text.append("\n ")
|
|
91
|
+
text.append(match["match"], style="dim")
|
|
92
|
+
if len(matches) > 25:
|
|
93
|
+
text.append("\n ")
|
|
94
|
+
text.append(f"... +{len(matches) - 25} more matches", style="dim")
|
|
86
95
|
else:
|
|
87
|
-
|
|
96
|
+
text.append("\n ")
|
|
97
|
+
text.append("No matches found", style="dim")
|
|
88
98
|
else:
|
|
89
|
-
|
|
99
|
+
text.append("\n ")
|
|
100
|
+
text.append("Viewing content...", style="dim")
|
|
90
101
|
else:
|
|
91
|
-
|
|
102
|
+
text.append("\n ")
|
|
103
|
+
text.append("Loading...", style="dim")
|
|
92
104
|
|
|
93
105
|
css_classes = cls.get_css_classes("completed")
|
|
94
|
-
return Static(
|
|
106
|
+
return Static(text, classes=css_classes)
|
|
95
107
|
|
|
96
108
|
|
|
97
109
|
@register_tool_renderer
|
|
@@ -107,30 +119,39 @@ class SendRequestRenderer(BaseToolRenderer):
|
|
|
107
119
|
method = args.get("method", "GET")
|
|
108
120
|
url = args.get("url", "")
|
|
109
121
|
|
|
110
|
-
|
|
122
|
+
text = Text()
|
|
123
|
+
text.append("📤 ")
|
|
124
|
+
text.append(f"Sending {method}", style="bold #06b6d4")
|
|
111
125
|
|
|
112
|
-
if
|
|
126
|
+
if isinstance(result, str) and result.strip():
|
|
127
|
+
text.append("\n ")
|
|
128
|
+
text.append(result.strip(), style="dim")
|
|
129
|
+
elif result and isinstance(result, dict):
|
|
113
130
|
status_code = result.get("status_code")
|
|
114
131
|
response_body = result.get("body", "")
|
|
115
132
|
|
|
116
133
|
if status_code:
|
|
117
|
-
|
|
134
|
+
text.append("\n ")
|
|
135
|
+
text.append(f"Status: {status_code}", style="dim")
|
|
118
136
|
if response_body:
|
|
119
137
|
body_preview = (
|
|
120
|
-
response_body[:
|
|
138
|
+
response_body[:2000] + "..." if len(response_body) > 2000 else response_body
|
|
121
139
|
)
|
|
122
|
-
|
|
123
|
-
|
|
140
|
+
text.append("\n ")
|
|
141
|
+
text.append(body_preview, style="dim")
|
|
124
142
|
else:
|
|
125
|
-
|
|
143
|
+
text.append("\n ")
|
|
144
|
+
text.append("Response received", style="dim")
|
|
126
145
|
elif url:
|
|
127
|
-
url_display = url[:
|
|
128
|
-
|
|
146
|
+
url_display = url[:500] + "..." if len(url) > 500 else url
|
|
147
|
+
text.append("\n ")
|
|
148
|
+
text.append(url_display, style="dim")
|
|
129
149
|
else:
|
|
130
|
-
|
|
150
|
+
text.append("\n ")
|
|
151
|
+
text.append("Sending...", style="dim")
|
|
131
152
|
|
|
132
153
|
css_classes = cls.get_css_classes("completed")
|
|
133
|
-
return Static(
|
|
154
|
+
return Static(text, classes=css_classes)
|
|
134
155
|
|
|
135
156
|
|
|
136
157
|
@register_tool_renderer
|
|
@@ -145,31 +166,40 @@ class RepeatRequestRenderer(BaseToolRenderer):
|
|
|
145
166
|
|
|
146
167
|
modifications = args.get("modifications", {})
|
|
147
168
|
|
|
148
|
-
|
|
169
|
+
text = Text()
|
|
170
|
+
text.append("🔄 ")
|
|
171
|
+
text.append("Repeating request", style="bold #06b6d4")
|
|
149
172
|
|
|
150
|
-
if
|
|
173
|
+
if isinstance(result, str) and result.strip():
|
|
174
|
+
text.append("\n ")
|
|
175
|
+
text.append(result.strip(), style="dim")
|
|
176
|
+
elif result and isinstance(result, dict):
|
|
151
177
|
status_code = result.get("status_code")
|
|
152
178
|
response_body = result.get("body", "")
|
|
153
179
|
|
|
154
180
|
if status_code:
|
|
155
|
-
|
|
181
|
+
text.append("\n ")
|
|
182
|
+
text.append(f"Status: {status_code}", style="dim")
|
|
156
183
|
if response_body:
|
|
157
184
|
body_preview = (
|
|
158
|
-
response_body[:
|
|
185
|
+
response_body[:2000] + "..." if len(response_body) > 2000 else response_body
|
|
159
186
|
)
|
|
160
|
-
|
|
161
|
-
|
|
187
|
+
text.append("\n ")
|
|
188
|
+
text.append(body_preview, style="dim")
|
|
162
189
|
else:
|
|
163
|
-
|
|
190
|
+
text.append("\n ")
|
|
191
|
+
text.append("Response received", style="dim")
|
|
164
192
|
elif modifications:
|
|
165
|
-
|
|
166
|
-
mod_display =
|
|
167
|
-
|
|
193
|
+
mod_str = str(modifications)
|
|
194
|
+
mod_display = mod_str[:500] + "..." if len(mod_str) > 500 else mod_str
|
|
195
|
+
text.append("\n ")
|
|
196
|
+
text.append(mod_display, style="dim")
|
|
168
197
|
else:
|
|
169
|
-
|
|
198
|
+
text.append("\n ")
|
|
199
|
+
text.append("No modifications", style="dim")
|
|
170
200
|
|
|
171
201
|
css_classes = cls.get_css_classes("completed")
|
|
172
|
-
return Static(
|
|
202
|
+
return Static(text, classes=css_classes)
|
|
173
203
|
|
|
174
204
|
|
|
175
205
|
@register_tool_renderer
|
|
@@ -179,11 +209,14 @@ class ScopeRulesRenderer(BaseToolRenderer):
|
|
|
179
209
|
|
|
180
210
|
@classmethod
|
|
181
211
|
def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: ARG003
|
|
182
|
-
|
|
183
|
-
|
|
212
|
+
text = Text()
|
|
213
|
+
text.append("⚙️ ")
|
|
214
|
+
text.append("Updating proxy scope", style="bold #06b6d4")
|
|
215
|
+
text.append("\n ")
|
|
216
|
+
text.append("Configuring...", style="dim")
|
|
184
217
|
|
|
185
218
|
css_classes = cls.get_css_classes("completed")
|
|
186
|
-
return Static(
|
|
219
|
+
return Static(text, classes=css_classes)
|
|
187
220
|
|
|
188
221
|
|
|
189
222
|
@register_tool_renderer
|
|
@@ -195,31 +228,34 @@ class ListSitemapRenderer(BaseToolRenderer):
|
|
|
195
228
|
def render(cls, tool_data: dict[str, Any]) -> Static:
|
|
196
229
|
result = tool_data.get("result")
|
|
197
230
|
|
|
198
|
-
|
|
231
|
+
text = Text()
|
|
232
|
+
text.append("🗺️ ")
|
|
233
|
+
text.append("Listing sitemap", style="bold #06b6d4")
|
|
199
234
|
|
|
200
|
-
if
|
|
235
|
+
if isinstance(result, str) and result.strip():
|
|
236
|
+
text.append("\n ")
|
|
237
|
+
text.append(result.strip(), style="dim")
|
|
238
|
+
elif result and isinstance(result, dict) and "entries" in result:
|
|
201
239
|
entries = result["entries"]
|
|
202
240
|
if isinstance(entries, list) and entries:
|
|
203
|
-
|
|
204
|
-
for entry in entries[:4]:
|
|
241
|
+
for entry in entries[:30]:
|
|
205
242
|
if isinstance(entry, dict):
|
|
206
243
|
label = entry.get("label", "?")
|
|
207
244
|
kind = entry.get("kind", "?")
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
escaped_lines = [cls.escape_markup(line) for line in entry_lines]
|
|
215
|
-
content_text = f"{header}\n [dim]{chr(10).join(escaped_lines)}[/]"
|
|
245
|
+
text.append("\n ")
|
|
246
|
+
text.append(f"{kind}: {label}", style="dim")
|
|
247
|
+
if len(entries) > 30:
|
|
248
|
+
text.append("\n ")
|
|
249
|
+
text.append(f"... +{len(entries) - 30} more entries", style="dim")
|
|
216
250
|
else:
|
|
217
|
-
|
|
251
|
+
text.append("\n ")
|
|
252
|
+
text.append("No entries found", style="dim")
|
|
218
253
|
else:
|
|
219
|
-
|
|
254
|
+
text.append("\n ")
|
|
255
|
+
text.append("Loading...", style="dim")
|
|
220
256
|
|
|
221
257
|
css_classes = cls.get_css_classes("completed")
|
|
222
|
-
return Static(
|
|
258
|
+
return Static(text, classes=css_classes)
|
|
223
259
|
|
|
224
260
|
|
|
225
261
|
@register_tool_renderer
|
|
@@ -231,25 +267,30 @@ class ViewSitemapEntryRenderer(BaseToolRenderer):
|
|
|
231
267
|
def render(cls, tool_data: dict[str, Any]) -> Static:
|
|
232
268
|
result = tool_data.get("result")
|
|
233
269
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
270
|
+
text = Text()
|
|
271
|
+
text.append("📍 ")
|
|
272
|
+
text.append("Viewing sitemap entry", style="bold #06b6d4")
|
|
273
|
+
|
|
274
|
+
if isinstance(result, str) and result.strip():
|
|
275
|
+
text.append("\n ")
|
|
276
|
+
text.append(result.strip(), style="dim")
|
|
277
|
+
elif result and isinstance(result, dict) and "entry" in result:
|
|
278
|
+
entry = result["entry"]
|
|
279
|
+
if isinstance(entry, dict):
|
|
280
|
+
label = entry.get("label", "")
|
|
281
|
+
kind = entry.get("kind", "")
|
|
282
|
+
if label and kind:
|
|
283
|
+
text.append("\n ")
|
|
284
|
+
text.append(f"{kind}: {label}", style="dim")
|
|
247
285
|
else:
|
|
248
|
-
|
|
286
|
+
text.append("\n ")
|
|
287
|
+
text.append("Entry details loaded", style="dim")
|
|
249
288
|
else:
|
|
250
|
-
|
|
289
|
+
text.append("\n ")
|
|
290
|
+
text.append("Entry details loaded", style="dim")
|
|
251
291
|
else:
|
|
252
|
-
|
|
292
|
+
text.append("\n ")
|
|
293
|
+
text.append("Loading...", style="dim")
|
|
253
294
|
|
|
254
295
|
css_classes = cls.get_css_classes("completed")
|
|
255
|
-
return Static(
|
|
296
|
+
return Static(text, classes=css_classes)
|
|
@@ -1,34 +1,147 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from functools import cache
|
|
1
3
|
from typing import Any, ClassVar
|
|
2
4
|
|
|
5
|
+
from pygments.lexers import PythonLexer
|
|
6
|
+
from pygments.styles import get_style_by_name
|
|
7
|
+
from rich.text import Text
|
|
3
8
|
from textual.widgets import Static
|
|
4
9
|
|
|
5
10
|
from .base_renderer import BaseToolRenderer
|
|
6
11
|
from .registry import register_tool_renderer
|
|
7
12
|
|
|
8
13
|
|
|
14
|
+
MAX_OUTPUT_LINES = 50
|
|
15
|
+
MAX_LINE_LENGTH = 200
|
|
16
|
+
|
|
17
|
+
STRIP_PATTERNS = [
|
|
18
|
+
r"\.\.\. \[(stdout|stderr|result|output|error) truncated at \d+k? chars\]",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@cache
|
|
23
|
+
def _get_style_colors() -> dict[Any, str]:
|
|
24
|
+
style = get_style_by_name("native")
|
|
25
|
+
return {token: f"#{style_def['color']}" for token, style_def in style if style_def["color"]}
|
|
26
|
+
|
|
27
|
+
|
|
9
28
|
@register_tool_renderer
|
|
10
29
|
class PythonRenderer(BaseToolRenderer):
|
|
11
30
|
tool_name: ClassVar[str] = "python_action"
|
|
12
31
|
css_classes: ClassVar[list[str]] = ["tool-call", "python-tool"]
|
|
13
32
|
|
|
33
|
+
@classmethod
|
|
34
|
+
def _get_token_color(cls, token_type: Any) -> str | None:
|
|
35
|
+
colors = _get_style_colors()
|
|
36
|
+
while token_type:
|
|
37
|
+
if token_type in colors:
|
|
38
|
+
return colors[token_type]
|
|
39
|
+
token_type = token_type.parent
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def _highlight_python(cls, code: str) -> Text:
|
|
44
|
+
lexer = PythonLexer()
|
|
45
|
+
text = Text()
|
|
46
|
+
|
|
47
|
+
for token_type, token_value in lexer.get_tokens(code):
|
|
48
|
+
if not token_value:
|
|
49
|
+
continue
|
|
50
|
+
color = cls._get_token_color(token_type)
|
|
51
|
+
text.append(token_value, style=color)
|
|
52
|
+
|
|
53
|
+
return text
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def _clean_output(cls, output: str) -> str:
|
|
57
|
+
cleaned = output
|
|
58
|
+
for pattern in STRIP_PATTERNS:
|
|
59
|
+
cleaned = re.sub(pattern, "", cleaned)
|
|
60
|
+
return cleaned.strip()
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def _truncate_line(cls, line: str) -> str:
|
|
64
|
+
if len(line) > MAX_LINE_LENGTH:
|
|
65
|
+
return line[: MAX_LINE_LENGTH - 3] + "..."
|
|
66
|
+
return line
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def _format_output(cls, output: str) -> Text:
|
|
70
|
+
text = Text()
|
|
71
|
+
lines = output.splitlines()
|
|
72
|
+
total_lines = len(lines)
|
|
73
|
+
|
|
74
|
+
head_count = MAX_OUTPUT_LINES // 2
|
|
75
|
+
tail_count = MAX_OUTPUT_LINES - head_count - 1
|
|
76
|
+
|
|
77
|
+
if total_lines <= MAX_OUTPUT_LINES:
|
|
78
|
+
display_lines = lines
|
|
79
|
+
truncated = False
|
|
80
|
+
hidden_count = 0
|
|
81
|
+
else:
|
|
82
|
+
display_lines = lines[:head_count]
|
|
83
|
+
truncated = True
|
|
84
|
+
hidden_count = total_lines - head_count - tail_count
|
|
85
|
+
|
|
86
|
+
for i, line in enumerate(display_lines):
|
|
87
|
+
truncated_line = cls._truncate_line(line)
|
|
88
|
+
text.append(" ")
|
|
89
|
+
text.append(truncated_line, style="dim")
|
|
90
|
+
if i < len(display_lines) - 1 or truncated:
|
|
91
|
+
text.append("\n")
|
|
92
|
+
|
|
93
|
+
if truncated:
|
|
94
|
+
text.append(f" ... {hidden_count} lines truncated ...", style="dim italic")
|
|
95
|
+
text.append("\n")
|
|
96
|
+
tail_lines = lines[-tail_count:]
|
|
97
|
+
for i, line in enumerate(tail_lines):
|
|
98
|
+
truncated_line = cls._truncate_line(line)
|
|
99
|
+
text.append(" ")
|
|
100
|
+
text.append(truncated_line, style="dim")
|
|
101
|
+
if i < len(tail_lines) - 1:
|
|
102
|
+
text.append("\n")
|
|
103
|
+
|
|
104
|
+
return text
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def _append_output(cls, text: Text, result: dict[str, Any] | str) -> None:
|
|
108
|
+
if isinstance(result, str):
|
|
109
|
+
if result.strip():
|
|
110
|
+
text.append("\n")
|
|
111
|
+
text.append_text(cls._format_output(result))
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
stdout = result.get("stdout", "")
|
|
115
|
+
stdout = cls._clean_output(stdout) if stdout else ""
|
|
116
|
+
|
|
117
|
+
if stdout:
|
|
118
|
+
text.append("\n")
|
|
119
|
+
formatted_output = cls._format_output(stdout)
|
|
120
|
+
text.append_text(formatted_output)
|
|
121
|
+
|
|
14
122
|
@classmethod
|
|
15
123
|
def render(cls, tool_data: dict[str, Any]) -> Static:
|
|
16
124
|
args = tool_data.get("args", {})
|
|
125
|
+
status = tool_data.get("status", "unknown")
|
|
126
|
+
result = tool_data.get("result")
|
|
17
127
|
|
|
18
128
|
action = args.get("action", "")
|
|
19
129
|
code = args.get("code", "")
|
|
20
130
|
|
|
21
|
-
|
|
131
|
+
text = Text()
|
|
132
|
+
text.append("</> ", style="dim")
|
|
22
133
|
|
|
23
134
|
if code and action in ["new_session", "execute"]:
|
|
24
|
-
|
|
25
|
-
content_text = f"{header}\n [italic white]{cls.escape_markup(code_display)}[/]"
|
|
135
|
+
text.append_text(cls._highlight_python(code))
|
|
26
136
|
elif action == "close":
|
|
27
|
-
|
|
137
|
+
text.append("Closing session...", style="dim")
|
|
28
138
|
elif action == "list_sessions":
|
|
29
|
-
|
|
139
|
+
text.append("Listing sessions...", style="dim")
|
|
30
140
|
else:
|
|
31
|
-
|
|
141
|
+
text.append("Running...", style="dim")
|
|
142
|
+
|
|
143
|
+
if result and isinstance(result, dict | str):
|
|
144
|
+
cls._append_output(text, result)
|
|
32
145
|
|
|
33
|
-
css_classes = cls.get_css_classes(
|
|
34
|
-
return Static(
|
|
146
|
+
css_classes = cls.get_css_classes(status)
|
|
147
|
+
return Static(text, classes=css_classes)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from typing import Any, ClassVar
|
|
2
2
|
|
|
3
|
+
from rich.text import Text
|
|
3
4
|
from textual.widgets import Static
|
|
4
5
|
|
|
5
6
|
from .base_renderer import BaseToolRenderer
|
|
@@ -47,26 +48,32 @@ def render_tool_widget(tool_data: dict[str, Any]) -> Static:
|
|
|
47
48
|
|
|
48
49
|
|
|
49
50
|
def _render_default_tool_widget(tool_data: dict[str, Any]) -> Static:
|
|
50
|
-
tool_name =
|
|
51
|
+
tool_name = tool_data.get("tool_name", "Unknown Tool")
|
|
51
52
|
args = tool_data.get("args", {})
|
|
52
53
|
status = tool_data.get("status", "unknown")
|
|
53
54
|
result = tool_data.get("result")
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
text = Text()
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
text.append("→ Using tool ", style="dim")
|
|
59
|
+
text.append(tool_name, style="bold blue")
|
|
60
|
+
text.append("\n")
|
|
59
61
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
for k, v in list(args.items()):
|
|
63
|
+
str_v = str(v)
|
|
64
|
+
text.append(" ")
|
|
65
|
+
text.append(k, style="dim")
|
|
66
|
+
text.append(": ")
|
|
67
|
+
text.append(str_v)
|
|
68
|
+
text.append("\n")
|
|
63
69
|
|
|
64
70
|
if status in ["completed", "failed", "error"] and result is not None:
|
|
65
|
-
result_str =
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
result_str = str(result)
|
|
72
|
+
text.append("Result: ", style="bold")
|
|
73
|
+
text.append(result_str)
|
|
68
74
|
else:
|
|
69
|
-
|
|
75
|
+
icon, color = BaseToolRenderer.status_icon(status)
|
|
76
|
+
text.append(icon, style=color)
|
|
70
77
|
|
|
71
78
|
css_classes = BaseToolRenderer.get_css_classes(status)
|
|
72
|
-
return Static(
|
|
79
|
+
return Static(text, classes=css_classes)
|