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.
Files changed (117) hide show
  1. strix/agents/StrixAgent/strix_agent.py +3 -3
  2. strix/agents/StrixAgent/system_prompt.jinja +30 -26
  3. strix/agents/base_agent.py +159 -75
  4. strix/agents/state.py +5 -2
  5. strix/config/__init__.py +12 -0
  6. strix/config/config.py +172 -0
  7. strix/interface/assets/tui_styles.tcss +195 -230
  8. strix/interface/cli.py +16 -41
  9. strix/interface/main.py +151 -74
  10. strix/interface/streaming_parser.py +119 -0
  11. strix/interface/tool_components/__init__.py +4 -0
  12. strix/interface/tool_components/agent_message_renderer.py +190 -0
  13. strix/interface/tool_components/agents_graph_renderer.py +54 -38
  14. strix/interface/tool_components/base_renderer.py +68 -36
  15. strix/interface/tool_components/browser_renderer.py +106 -91
  16. strix/interface/tool_components/file_edit_renderer.py +117 -36
  17. strix/interface/tool_components/finish_renderer.py +43 -10
  18. strix/interface/tool_components/notes_renderer.py +63 -38
  19. strix/interface/tool_components/proxy_renderer.py +133 -92
  20. strix/interface/tool_components/python_renderer.py +121 -8
  21. strix/interface/tool_components/registry.py +19 -12
  22. strix/interface/tool_components/reporting_renderer.py +196 -28
  23. strix/interface/tool_components/scan_info_renderer.py +22 -19
  24. strix/interface/tool_components/terminal_renderer.py +270 -90
  25. strix/interface/tool_components/thinking_renderer.py +8 -6
  26. strix/interface/tool_components/todo_renderer.py +225 -0
  27. strix/interface/tool_components/user_message_renderer.py +26 -19
  28. strix/interface/tool_components/web_search_renderer.py +7 -6
  29. strix/interface/tui.py +907 -262
  30. strix/interface/utils.py +236 -4
  31. strix/llm/__init__.py +6 -2
  32. strix/llm/config.py +8 -5
  33. strix/llm/dedupe.py +217 -0
  34. strix/llm/llm.py +209 -356
  35. strix/llm/memory_compressor.py +6 -5
  36. strix/llm/utils.py +17 -8
  37. strix/runtime/__init__.py +12 -3
  38. strix/runtime/docker_runtime.py +121 -202
  39. strix/runtime/tool_server.py +55 -95
  40. strix/skills/README.md +64 -0
  41. strix/skills/__init__.py +110 -0
  42. strix/{prompts → skills}/frameworks/nextjs.jinja +26 -0
  43. strix/skills/scan_modes/deep.jinja +145 -0
  44. strix/skills/scan_modes/quick.jinja +63 -0
  45. strix/skills/scan_modes/standard.jinja +91 -0
  46. strix/telemetry/README.md +38 -0
  47. strix/telemetry/__init__.py +7 -1
  48. strix/telemetry/posthog.py +137 -0
  49. strix/telemetry/tracer.py +194 -54
  50. strix/tools/__init__.py +11 -4
  51. strix/tools/agents_graph/agents_graph_actions.py +20 -21
  52. strix/tools/agents_graph/agents_graph_actions_schema.xml +8 -8
  53. strix/tools/browser/browser_actions.py +10 -6
  54. strix/tools/browser/browser_actions_schema.xml +6 -1
  55. strix/tools/browser/browser_instance.py +96 -48
  56. strix/tools/browser/tab_manager.py +121 -102
  57. strix/tools/context.py +12 -0
  58. strix/tools/executor.py +63 -4
  59. strix/tools/file_edit/file_edit_actions.py +6 -3
  60. strix/tools/file_edit/file_edit_actions_schema.xml +45 -3
  61. strix/tools/finish/finish_actions.py +80 -105
  62. strix/tools/finish/finish_actions_schema.xml +121 -14
  63. strix/tools/notes/notes_actions.py +6 -33
  64. strix/tools/notes/notes_actions_schema.xml +50 -46
  65. strix/tools/proxy/proxy_actions.py +14 -2
  66. strix/tools/proxy/proxy_actions_schema.xml +0 -1
  67. strix/tools/proxy/proxy_manager.py +28 -16
  68. strix/tools/python/python_actions.py +2 -2
  69. strix/tools/python/python_actions_schema.xml +9 -1
  70. strix/tools/python/python_instance.py +39 -37
  71. strix/tools/python/python_manager.py +43 -31
  72. strix/tools/registry.py +73 -12
  73. strix/tools/reporting/reporting_actions.py +218 -31
  74. strix/tools/reporting/reporting_actions_schema.xml +256 -8
  75. strix/tools/terminal/terminal_actions.py +2 -2
  76. strix/tools/terminal/terminal_actions_schema.xml +6 -0
  77. strix/tools/terminal/terminal_manager.py +41 -30
  78. strix/tools/thinking/thinking_actions_schema.xml +27 -25
  79. strix/tools/todo/__init__.py +18 -0
  80. strix/tools/todo/todo_actions.py +568 -0
  81. strix/tools/todo/todo_actions_schema.xml +225 -0
  82. strix/utils/__init__.py +0 -0
  83. strix/utils/resource_paths.py +13 -0
  84. {strix_agent-0.4.0.dist-info → strix_agent-0.6.2.dist-info}/METADATA +90 -65
  85. strix_agent-0.6.2.dist-info/RECORD +134 -0
  86. {strix_agent-0.4.0.dist-info → strix_agent-0.6.2.dist-info}/WHEEL +1 -1
  87. strix/llm/request_queue.py +0 -87
  88. strix/prompts/README.md +0 -64
  89. strix/prompts/__init__.py +0 -109
  90. strix_agent-0.4.0.dist-info/RECORD +0 -118
  91. /strix/{prompts → skills}/cloud/.gitkeep +0 -0
  92. /strix/{prompts → skills}/coordination/root_agent.jinja +0 -0
  93. /strix/{prompts → skills}/custom/.gitkeep +0 -0
  94. /strix/{prompts → skills}/frameworks/fastapi.jinja +0 -0
  95. /strix/{prompts → skills}/protocols/graphql.jinja +0 -0
  96. /strix/{prompts → skills}/reconnaissance/.gitkeep +0 -0
  97. /strix/{prompts → skills}/technologies/firebase_firestore.jinja +0 -0
  98. /strix/{prompts → skills}/technologies/supabase.jinja +0 -0
  99. /strix/{prompts → skills}/vulnerabilities/authentication_jwt.jinja +0 -0
  100. /strix/{prompts → skills}/vulnerabilities/broken_function_level_authorization.jinja +0 -0
  101. /strix/{prompts → skills}/vulnerabilities/business_logic.jinja +0 -0
  102. /strix/{prompts → skills}/vulnerabilities/csrf.jinja +0 -0
  103. /strix/{prompts → skills}/vulnerabilities/idor.jinja +0 -0
  104. /strix/{prompts → skills}/vulnerabilities/information_disclosure.jinja +0 -0
  105. /strix/{prompts → skills}/vulnerabilities/insecure_file_uploads.jinja +0 -0
  106. /strix/{prompts → skills}/vulnerabilities/mass_assignment.jinja +0 -0
  107. /strix/{prompts → skills}/vulnerabilities/open_redirect.jinja +0 -0
  108. /strix/{prompts → skills}/vulnerabilities/path_traversal_lfi_rfi.jinja +0 -0
  109. /strix/{prompts → skills}/vulnerabilities/race_conditions.jinja +0 -0
  110. /strix/{prompts → skills}/vulnerabilities/rce.jinja +0 -0
  111. /strix/{prompts → skills}/vulnerabilities/sql_injection.jinja +0 -0
  112. /strix/{prompts → skills}/vulnerabilities/ssrf.jinja +0 -0
  113. /strix/{prompts → skills}/vulnerabilities/subdomain_takeover.jinja +0 -0
  114. /strix/{prompts → skills}/vulnerabilities/xss.jinja +0 -0
  115. /strix/{prompts → skills}/vulnerabilities/xxe.jinja +0 -0
  116. {strix_agent-0.4.0.dist-info → strix_agent-0.6.2.dist-info}/entry_points.txt +0 -0
  117. {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
- header = "📋 [bold #06b6d4]Listing requests[/]"
22
+ text = Text()
23
+ text.append("📋 ")
24
+ text.append("Listing requests", style="bold #06b6d4")
22
25
 
23
- if result and isinstance(result, dict) and "requests" in result:
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
- request_lines = []
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
- line = f"{method} {path} → {status}"
34
- request_lines.append(line)
35
-
36
- if len(requests) > 3:
37
- request_lines.append(f"... +{len(requests) - 3} more")
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
- content_text = f"{header}\n [dim]No requests found[/]"
44
+ text.append("\n ")
45
+ text.append("No requests found", style="dim")
43
46
  elif httpql_filter:
44
47
  filter_display = (
45
- httpql_filter[:300] + "..." if len(httpql_filter) > 300 else httpql_filter
48
+ httpql_filter[:500] + "..." if len(httpql_filter) > 500 else httpql_filter
46
49
  )
47
- content_text = f"{header}\n [dim]{cls.escape_markup(filter_display)}[/]"
50
+ text.append("\n ")
51
+ text.append(filter_display, style="dim")
48
52
  else:
49
- content_text = f"{header}\n [dim]All requests[/]"
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(content_text, classes=css_classes)
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
- header = f"👀 [bold #06b6d4]Viewing {cls.escape_markup(part)}[/]"
72
+ text = Text()
73
+ text.append("👀 ")
74
+ text.append(f"Viewing {part}", style="bold #06b6d4")
68
75
 
69
- if result and isinstance(result, dict):
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[:500] + "..." if len(content) > 500 else content
73
- content_text = f"{header}\n [dim]{cls.escape_markup(content_preview)}[/]"
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
- match_lines = [
78
- match["match"]
79
- for match in matches[:3]
80
- if isinstance(match, dict) and "match" in match
81
- ]
82
- if len(matches) > 3:
83
- match_lines.append(f"... +{len(matches) - 3} more matches")
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
- content_text = f"{header}\n [dim]No matches found[/]"
96
+ text.append("\n ")
97
+ text.append("No matches found", style="dim")
88
98
  else:
89
- content_text = f"{header}\n [dim]Viewing content...[/]"
99
+ text.append("\n ")
100
+ text.append("Viewing content...", style="dim")
90
101
  else:
91
- content_text = f"{header}\n [dim]Loading...[/]"
102
+ text.append("\n ")
103
+ text.append("Loading...", style="dim")
92
104
 
93
105
  css_classes = cls.get_css_classes("completed")
94
- return Static(content_text, classes=css_classes)
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
- header = f"📤 [bold #06b6d4]Sending {cls.escape_markup(method)}[/]"
122
+ text = Text()
123
+ text.append("📤 ")
124
+ text.append(f"Sending {method}", style="bold #06b6d4")
111
125
 
112
- if result and isinstance(result, dict):
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
- response_preview = f"Status: {status_code}"
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[:300] + "..." if len(response_body) > 300 else response_body
138
+ response_body[:2000] + "..." if len(response_body) > 2000 else response_body
121
139
  )
122
- response_preview += f"\n{body_preview}"
123
- content_text = f"{header}\n [dim]{cls.escape_markup(response_preview)}[/]"
140
+ text.append("\n ")
141
+ text.append(body_preview, style="dim")
124
142
  else:
125
- content_text = f"{header}\n [dim]Response received[/]"
143
+ text.append("\n ")
144
+ text.append("Response received", style="dim")
126
145
  elif url:
127
- url_display = url[:400] + "..." if len(url) > 400 else url
128
- content_text = f"{header}\n [dim]{cls.escape_markup(url_display)}[/]"
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
- content_text = f"{header}\n [dim]Sending...[/]"
150
+ text.append("\n ")
151
+ text.append("Sending...", style="dim")
131
152
 
132
153
  css_classes = cls.get_css_classes("completed")
133
- return Static(content_text, classes=css_classes)
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
- header = "🔄 [bold #06b6d4]Repeating request[/]"
169
+ text = Text()
170
+ text.append("🔄 ")
171
+ text.append("Repeating request", style="bold #06b6d4")
149
172
 
150
- if result and isinstance(result, dict):
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
- response_preview = f"Status: {status_code}"
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[:300] + "..." if len(response_body) > 300 else response_body
185
+ response_body[:2000] + "..." if len(response_body) > 2000 else response_body
159
186
  )
160
- response_preview += f"\n{body_preview}"
161
- content_text = f"{header}\n [dim]{cls.escape_markup(response_preview)}[/]"
187
+ text.append("\n ")
188
+ text.append(body_preview, style="dim")
162
189
  else:
163
- content_text = f"{header}\n [dim]Response received[/]"
190
+ text.append("\n ")
191
+ text.append("Response received", style="dim")
164
192
  elif modifications:
165
- mod_text = str(modifications)
166
- mod_display = mod_text[:400] + "..." if len(mod_text) > 400 else mod_text
167
- content_text = f"{header}\n [dim]{cls.escape_markup(mod_display)}[/]"
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
- content_text = f"{header}\n [dim]No modifications[/]"
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(content_text, classes=css_classes)
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
- header = "⚙️ [bold #06b6d4]Updating proxy scope[/]"
183
- content_text = f"{header}\n [dim]Configuring...[/]"
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(content_text, classes=css_classes)
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
- header = "🗺️ [bold #06b6d4]Listing sitemap[/]"
231
+ text = Text()
232
+ text.append("🗺️ ")
233
+ text.append("Listing sitemap", style="bold #06b6d4")
199
234
 
200
- if result and isinstance(result, dict) and "entries" in result:
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
- entry_lines = []
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
- line = f"{kind}: {label}"
209
- entry_lines.append(line)
210
-
211
- if len(entries) > 4:
212
- entry_lines.append(f"... +{len(entries) - 4} more")
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
- content_text = f"{header}\n [dim]No entries found[/]"
251
+ text.append("\n ")
252
+ text.append("No entries found", style="dim")
218
253
  else:
219
- content_text = f"{header}\n [dim]Loading...[/]"
254
+ text.append("\n ")
255
+ text.append("Loading...", style="dim")
220
256
 
221
257
  css_classes = cls.get_css_classes("completed")
222
- return Static(content_text, classes=css_classes)
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
- header = "📍 [bold #06b6d4]Viewing sitemap entry[/]"
235
-
236
- if result and isinstance(result, dict):
237
- if "entry" in result:
238
- entry = result["entry"]
239
- if isinstance(entry, dict):
240
- label = entry.get("label", "")
241
- kind = entry.get("kind", "")
242
- if label and kind:
243
- entry_info = f"{kind}: {label}"
244
- content_text = f"{header}\n [dim]{cls.escape_markup(entry_info)}[/]"
245
- else:
246
- content_text = f"{header}\n [dim]Entry details loaded[/]"
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
- content_text = f"{header}\n [dim]Entry details loaded[/]"
286
+ text.append("\n ")
287
+ text.append("Entry details loaded", style="dim")
249
288
  else:
250
- content_text = f"{header}\n [dim]Loading entry...[/]"
289
+ text.append("\n ")
290
+ text.append("Entry details loaded", style="dim")
251
291
  else:
252
- content_text = f"{header}\n [dim]Loading...[/]"
292
+ text.append("\n ")
293
+ text.append("Loading...", style="dim")
253
294
 
254
295
  css_classes = cls.get_css_classes("completed")
255
- return Static(content_text, classes=css_classes)
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
- header = "</> [bold #3b82f6]Python[/]"
131
+ text = Text()
132
+ text.append("</> ", style="dim")
22
133
 
23
134
  if code and action in ["new_session", "execute"]:
24
- code_display = code[:600] + "..." if len(code) > 600 else code
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
- content_text = f"{header}\n [dim]Closing session...[/]"
137
+ text.append("Closing session...", style="dim")
28
138
  elif action == "list_sessions":
29
- content_text = f"{header}\n [dim]Listing sessions...[/]"
139
+ text.append("Listing sessions...", style="dim")
30
140
  else:
31
- content_text = f"{header}\n [dim]Running...[/]"
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("completed")
34
- return Static(content_text, classes=css_classes)
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 = BaseToolRenderer.escape_markup(tool_data.get("tool_name", "Unknown Tool"))
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
- status_text = BaseToolRenderer.get_status_icon(status)
56
+ text = Text()
56
57
 
57
- header = f"→ Using tool [bold blue]{BaseToolRenderer.escape_markup(tool_name)}[/]"
58
- content_parts = [header]
58
+ text.append("→ Using tool ", style="dim")
59
+ text.append(tool_name, style="bold blue")
60
+ text.append("\n")
59
61
 
60
- args_str = BaseToolRenderer.format_args(args)
61
- if args_str:
62
- content_parts.append(args_str)
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 = BaseToolRenderer.format_result(result)
66
- if result_str:
67
- content_parts.append(f"[bold]Result:[/] {result_str}")
71
+ result_str = str(result)
72
+ text.append("Result: ", style="bold")
73
+ text.append(result_str)
68
74
  else:
69
- content_parts.append(status_text)
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("\n".join(content_parts), classes=css_classes)
79
+ return Static(text, classes=css_classes)