strix-agent 0.1.1__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 (99) hide show
  1. strix/__init__.py +0 -0
  2. strix/agents/StrixAgent/__init__.py +4 -0
  3. strix/agents/StrixAgent/strix_agent.py +60 -0
  4. strix/agents/StrixAgent/system_prompt.jinja +504 -0
  5. strix/agents/__init__.py +10 -0
  6. strix/agents/base_agent.py +394 -0
  7. strix/agents/state.py +139 -0
  8. strix/cli/__init__.py +4 -0
  9. strix/cli/app.py +1124 -0
  10. strix/cli/assets/cli.tcss +680 -0
  11. strix/cli/main.py +542 -0
  12. strix/cli/tool_components/__init__.py +39 -0
  13. strix/cli/tool_components/agents_graph_renderer.py +129 -0
  14. strix/cli/tool_components/base_renderer.py +61 -0
  15. strix/cli/tool_components/browser_renderer.py +107 -0
  16. strix/cli/tool_components/file_edit_renderer.py +95 -0
  17. strix/cli/tool_components/finish_renderer.py +32 -0
  18. strix/cli/tool_components/notes_renderer.py +108 -0
  19. strix/cli/tool_components/proxy_renderer.py +255 -0
  20. strix/cli/tool_components/python_renderer.py +34 -0
  21. strix/cli/tool_components/registry.py +72 -0
  22. strix/cli/tool_components/reporting_renderer.py +53 -0
  23. strix/cli/tool_components/scan_info_renderer.py +58 -0
  24. strix/cli/tool_components/terminal_renderer.py +99 -0
  25. strix/cli/tool_components/thinking_renderer.py +29 -0
  26. strix/cli/tool_components/user_message_renderer.py +43 -0
  27. strix/cli/tool_components/web_search_renderer.py +28 -0
  28. strix/cli/tracer.py +308 -0
  29. strix/llm/__init__.py +14 -0
  30. strix/llm/config.py +19 -0
  31. strix/llm/llm.py +310 -0
  32. strix/llm/memory_compressor.py +206 -0
  33. strix/llm/request_queue.py +63 -0
  34. strix/llm/utils.py +84 -0
  35. strix/prompts/__init__.py +113 -0
  36. strix/prompts/coordination/root_agent.jinja +41 -0
  37. strix/prompts/vulnerabilities/authentication_jwt.jinja +129 -0
  38. strix/prompts/vulnerabilities/business_logic.jinja +143 -0
  39. strix/prompts/vulnerabilities/csrf.jinja +168 -0
  40. strix/prompts/vulnerabilities/idor.jinja +164 -0
  41. strix/prompts/vulnerabilities/race_conditions.jinja +194 -0
  42. strix/prompts/vulnerabilities/rce.jinja +222 -0
  43. strix/prompts/vulnerabilities/sql_injection.jinja +216 -0
  44. strix/prompts/vulnerabilities/ssrf.jinja +168 -0
  45. strix/prompts/vulnerabilities/xss.jinja +221 -0
  46. strix/prompts/vulnerabilities/xxe.jinja +276 -0
  47. strix/runtime/__init__.py +19 -0
  48. strix/runtime/docker_runtime.py +298 -0
  49. strix/runtime/runtime.py +25 -0
  50. strix/runtime/tool_server.py +97 -0
  51. strix/tools/__init__.py +64 -0
  52. strix/tools/agents_graph/__init__.py +16 -0
  53. strix/tools/agents_graph/agents_graph_actions.py +610 -0
  54. strix/tools/agents_graph/agents_graph_actions_schema.xml +223 -0
  55. strix/tools/argument_parser.py +120 -0
  56. strix/tools/browser/__init__.py +4 -0
  57. strix/tools/browser/browser_actions.py +236 -0
  58. strix/tools/browser/browser_actions_schema.xml +183 -0
  59. strix/tools/browser/browser_instance.py +533 -0
  60. strix/tools/browser/tab_manager.py +342 -0
  61. strix/tools/executor.py +302 -0
  62. strix/tools/file_edit/__init__.py +4 -0
  63. strix/tools/file_edit/file_edit_actions.py +141 -0
  64. strix/tools/file_edit/file_edit_actions_schema.xml +128 -0
  65. strix/tools/finish/__init__.py +4 -0
  66. strix/tools/finish/finish_actions.py +167 -0
  67. strix/tools/finish/finish_actions_schema.xml +45 -0
  68. strix/tools/notes/__init__.py +14 -0
  69. strix/tools/notes/notes_actions.py +191 -0
  70. strix/tools/notes/notes_actions_schema.xml +150 -0
  71. strix/tools/proxy/__init__.py +20 -0
  72. strix/tools/proxy/proxy_actions.py +101 -0
  73. strix/tools/proxy/proxy_actions_schema.xml +267 -0
  74. strix/tools/proxy/proxy_manager.py +785 -0
  75. strix/tools/python/__init__.py +4 -0
  76. strix/tools/python/python_actions.py +47 -0
  77. strix/tools/python/python_actions_schema.xml +131 -0
  78. strix/tools/python/python_instance.py +172 -0
  79. strix/tools/python/python_manager.py +131 -0
  80. strix/tools/registry.py +196 -0
  81. strix/tools/reporting/__init__.py +6 -0
  82. strix/tools/reporting/reporting_actions.py +63 -0
  83. strix/tools/reporting/reporting_actions_schema.xml +30 -0
  84. strix/tools/terminal/__init__.py +4 -0
  85. strix/tools/terminal/terminal_actions.py +53 -0
  86. strix/tools/terminal/terminal_actions_schema.xml +114 -0
  87. strix/tools/terminal/terminal_instance.py +231 -0
  88. strix/tools/terminal/terminal_manager.py +191 -0
  89. strix/tools/thinking/__init__.py +4 -0
  90. strix/tools/thinking/thinking_actions.py +18 -0
  91. strix/tools/thinking/thinking_actions_schema.xml +52 -0
  92. strix/tools/web_search/__init__.py +4 -0
  93. strix/tools/web_search/web_search_actions.py +80 -0
  94. strix/tools/web_search/web_search_actions_schema.xml +83 -0
  95. strix_agent-0.1.1.dist-info/LICENSE +201 -0
  96. strix_agent-0.1.1.dist-info/METADATA +200 -0
  97. strix_agent-0.1.1.dist-info/RECORD +99 -0
  98. strix_agent-0.1.1.dist-info/WHEEL +4 -0
  99. strix_agent-0.1.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,255 @@
1
+ from typing import Any, ClassVar
2
+
3
+ from textual.widgets import Static
4
+
5
+ from .base_renderer import BaseToolRenderer
6
+ from .registry import register_tool_renderer
7
+
8
+
9
+ @register_tool_renderer
10
+ class ListRequestsRenderer(BaseToolRenderer):
11
+ tool_name: ClassVar[str] = "list_requests"
12
+ css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
13
+
14
+ @classmethod
15
+ def render(cls, tool_data: dict[str, Any]) -> Static:
16
+ args = tool_data.get("args", {})
17
+ result = tool_data.get("result")
18
+
19
+ httpql_filter = args.get("httpql_filter")
20
+
21
+ header = "📋 [bold #06b6d4]Listing requests[/]"
22
+
23
+ if result and isinstance(result, dict) and "requests" in result:
24
+ requests = result["requests"]
25
+ if isinstance(requests, list) and requests:
26
+ request_lines = []
27
+ for req in requests[:3]:
28
+ if isinstance(req, dict):
29
+ method = req.get("method", "?")
30
+ path = req.get("path", "?")
31
+ response = req.get("response") or {}
32
+ 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)}[/]"
41
+ else:
42
+ content_text = f"{header}\n [dim]No requests found[/]"
43
+ elif httpql_filter:
44
+ filter_display = (
45
+ httpql_filter[:300] + "..." if len(httpql_filter) > 300 else httpql_filter
46
+ )
47
+ content_text = f"{header}\n [dim]{cls.escape_markup(filter_display)}[/]"
48
+ else:
49
+ content_text = f"{header}\n [dim]All requests[/]"
50
+
51
+ css_classes = cls.get_css_classes("completed")
52
+ return Static(content_text, classes=css_classes)
53
+
54
+
55
+ @register_tool_renderer
56
+ class ViewRequestRenderer(BaseToolRenderer):
57
+ tool_name: ClassVar[str] = "view_request"
58
+ css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
59
+
60
+ @classmethod
61
+ def render(cls, tool_data: dict[str, Any]) -> Static:
62
+ args = tool_data.get("args", {})
63
+ result = tool_data.get("result")
64
+
65
+ part = args.get("part", "request")
66
+
67
+ header = f"👀 [bold #06b6d4]Viewing {part}[/]"
68
+
69
+ if result and isinstance(result, dict):
70
+ if "content" in result:
71
+ 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)}[/]"
74
+ elif "matches" in result:
75
+ matches = result["matches"]
76
+ 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)}[/]"
86
+ else:
87
+ content_text = f"{header}\n [dim]No matches found[/]"
88
+ else:
89
+ content_text = f"{header}\n [dim]Viewing content...[/]"
90
+ else:
91
+ content_text = f"{header}\n [dim]Loading...[/]"
92
+
93
+ css_classes = cls.get_css_classes("completed")
94
+ return Static(content_text, classes=css_classes)
95
+
96
+
97
+ @register_tool_renderer
98
+ class SendRequestRenderer(BaseToolRenderer):
99
+ tool_name: ClassVar[str] = "send_request"
100
+ css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
101
+
102
+ @classmethod
103
+ def render(cls, tool_data: dict[str, Any]) -> Static:
104
+ args = tool_data.get("args", {})
105
+ result = tool_data.get("result")
106
+
107
+ method = args.get("method", "GET")
108
+ url = args.get("url", "")
109
+
110
+ header = f"📤 [bold #06b6d4]Sending {method}[/]"
111
+
112
+ if result and isinstance(result, dict):
113
+ status_code = result.get("status_code")
114
+ response_body = result.get("body", "")
115
+
116
+ if status_code:
117
+ response_preview = f"Status: {status_code}"
118
+ if response_body:
119
+ body_preview = (
120
+ response_body[:300] + "..." if len(response_body) > 300 else response_body
121
+ )
122
+ response_preview += f"\n{body_preview}"
123
+ content_text = f"{header}\n [dim]{cls.escape_markup(response_preview)}[/]"
124
+ else:
125
+ content_text = f"{header}\n [dim]Response received[/]"
126
+ 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)}[/]"
129
+ else:
130
+ content_text = f"{header}\n [dim]Sending...[/]"
131
+
132
+ css_classes = cls.get_css_classes("completed")
133
+ return Static(content_text, classes=css_classes)
134
+
135
+
136
+ @register_tool_renderer
137
+ class RepeatRequestRenderer(BaseToolRenderer):
138
+ tool_name: ClassVar[str] = "repeat_request"
139
+ css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
140
+
141
+ @classmethod
142
+ def render(cls, tool_data: dict[str, Any]) -> Static:
143
+ args = tool_data.get("args", {})
144
+ result = tool_data.get("result")
145
+
146
+ modifications = args.get("modifications", {})
147
+
148
+ header = "🔄 [bold #06b6d4]Repeating request[/]"
149
+
150
+ if result and isinstance(result, dict):
151
+ status_code = result.get("status_code")
152
+ response_body = result.get("body", "")
153
+
154
+ if status_code:
155
+ response_preview = f"Status: {status_code}"
156
+ if response_body:
157
+ body_preview = (
158
+ response_body[:300] + "..." if len(response_body) > 300 else response_body
159
+ )
160
+ response_preview += f"\n{body_preview}"
161
+ content_text = f"{header}\n [dim]{cls.escape_markup(response_preview)}[/]"
162
+ else:
163
+ content_text = f"{header}\n [dim]Response received[/]"
164
+ 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)}[/]"
168
+ else:
169
+ content_text = f"{header}\n [dim]No modifications[/]"
170
+
171
+ css_classes = cls.get_css_classes("completed")
172
+ return Static(content_text, classes=css_classes)
173
+
174
+
175
+ @register_tool_renderer
176
+ class ScopeRulesRenderer(BaseToolRenderer):
177
+ tool_name: ClassVar[str] = "scope_rules"
178
+ css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
179
+
180
+ @classmethod
181
+ 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...[/]"
184
+
185
+ css_classes = cls.get_css_classes("completed")
186
+ return Static(content_text, classes=css_classes)
187
+
188
+
189
+ @register_tool_renderer
190
+ class ListSitemapRenderer(BaseToolRenderer):
191
+ tool_name: ClassVar[str] = "list_sitemap"
192
+ css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
193
+
194
+ @classmethod
195
+ def render(cls, tool_data: dict[str, Any]) -> Static:
196
+ result = tool_data.get("result")
197
+
198
+ header = "🗺️ [bold #06b6d4]Listing sitemap[/]"
199
+
200
+ if result and isinstance(result, dict) and "entries" in result:
201
+ entries = result["entries"]
202
+ if isinstance(entries, list) and entries:
203
+ entry_lines = []
204
+ for entry in entries[:4]:
205
+ if isinstance(entry, dict):
206
+ label = entry.get("label", "?")
207
+ 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)}[/]"
216
+ else:
217
+ content_text = f"{header}\n [dim]No entries found[/]"
218
+ else:
219
+ content_text = f"{header}\n [dim]Loading...[/]"
220
+
221
+ css_classes = cls.get_css_classes("completed")
222
+ return Static(content_text, classes=css_classes)
223
+
224
+
225
+ @register_tool_renderer
226
+ class ViewSitemapEntryRenderer(BaseToolRenderer):
227
+ tool_name: ClassVar[str] = "view_sitemap_entry"
228
+ css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
229
+
230
+ @classmethod
231
+ def render(cls, tool_data: dict[str, Any]) -> Static:
232
+ result = tool_data.get("result")
233
+
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[/]"
247
+ else:
248
+ content_text = f"{header}\n [dim]Entry details loaded[/]"
249
+ else:
250
+ content_text = f"{header}\n [dim]Loading entry...[/]"
251
+ else:
252
+ content_text = f"{header}\n [dim]Loading...[/]"
253
+
254
+ css_classes = cls.get_css_classes("completed")
255
+ return Static(content_text, classes=css_classes)
@@ -0,0 +1,34 @@
1
+ from typing import Any, ClassVar
2
+
3
+ from textual.widgets import Static
4
+
5
+ from .base_renderer import BaseToolRenderer
6
+ from .registry import register_tool_renderer
7
+
8
+
9
+ @register_tool_renderer
10
+ class PythonRenderer(BaseToolRenderer):
11
+ tool_name: ClassVar[str] = "python_action"
12
+ css_classes: ClassVar[list[str]] = ["tool-call", "python-tool"]
13
+
14
+ @classmethod
15
+ def render(cls, tool_data: dict[str, Any]) -> Static:
16
+ args = tool_data.get("args", {})
17
+
18
+ action = args.get("action", "")
19
+ code = args.get("code", "")
20
+
21
+ header = "</> [bold #3b82f6]Python[/]"
22
+
23
+ if code and action in ["new_session", "execute"]:
24
+ code_display = code[:250] + "..." if len(code) > 250 else code
25
+ content_text = f"{header}\n [italic white]{cls.escape_markup(code_display)}[/]"
26
+ elif action == "close":
27
+ content_text = f"{header}\n [dim]Closing session...[/]"
28
+ elif action == "list_sessions":
29
+ content_text = f"{header}\n [dim]Listing sessions...[/]"
30
+ else:
31
+ content_text = f"{header}\n [dim]Running...[/]"
32
+
33
+ css_classes = cls.get_css_classes("completed")
34
+ return Static(content_text, classes=css_classes)
@@ -0,0 +1,72 @@
1
+ from typing import Any, ClassVar
2
+
3
+ from textual.widgets import Static
4
+
5
+ from .base_renderer import BaseToolRenderer
6
+
7
+
8
+ class ToolTUIRegistry:
9
+ _renderers: ClassVar[dict[str, type[BaseToolRenderer]]] = {}
10
+
11
+ @classmethod
12
+ def register(cls, renderer_class: type[BaseToolRenderer]) -> None:
13
+ if not renderer_class.tool_name:
14
+ raise ValueError(f"Renderer {renderer_class.__name__} must define tool_name")
15
+
16
+ cls._renderers[renderer_class.tool_name] = renderer_class
17
+
18
+ @classmethod
19
+ def get_renderer(cls, tool_name: str) -> type[BaseToolRenderer] | None:
20
+ return cls._renderers.get(tool_name)
21
+
22
+ @classmethod
23
+ def list_tools(cls) -> list[str]:
24
+ return list(cls._renderers.keys())
25
+
26
+ @classmethod
27
+ def has_renderer(cls, tool_name: str) -> bool:
28
+ return tool_name in cls._renderers
29
+
30
+
31
+ def register_tool_renderer(renderer_class: type[BaseToolRenderer]) -> type[BaseToolRenderer]:
32
+ ToolTUIRegistry.register(renderer_class)
33
+ return renderer_class
34
+
35
+
36
+ def get_tool_renderer(tool_name: str) -> type[BaseToolRenderer] | None:
37
+ return ToolTUIRegistry.get_renderer(tool_name)
38
+
39
+
40
+ def render_tool_widget(tool_data: dict[str, Any]) -> Static:
41
+ tool_name = tool_data.get("tool_name", "")
42
+ renderer = get_tool_renderer(tool_name)
43
+
44
+ if renderer:
45
+ return renderer.render(tool_data)
46
+ return _render_default_tool_widget(tool_data)
47
+
48
+
49
+ 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
+ args = tool_data.get("args", {})
52
+ status = tool_data.get("status", "unknown")
53
+ result = tool_data.get("result")
54
+
55
+ status_text = BaseToolRenderer.get_status_icon(status)
56
+
57
+ header = f"→ Using tool [bold blue]{tool_name}[/]"
58
+ content_parts = [header]
59
+
60
+ args_str = BaseToolRenderer.format_args(args)
61
+ if args_str:
62
+ content_parts.append(args_str)
63
+
64
+ 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}")
68
+ else:
69
+ content_parts.append(status_text)
70
+
71
+ css_classes = BaseToolRenderer.get_css_classes(status)
72
+ return Static("\n".join(content_parts), classes=css_classes)
@@ -0,0 +1,53 @@
1
+ from typing import Any, ClassVar
2
+
3
+ from textual.widgets import Static
4
+
5
+ from .base_renderer import BaseToolRenderer
6
+ from .registry import register_tool_renderer
7
+
8
+
9
+ @register_tool_renderer
10
+ class CreateVulnerabilityReportRenderer(BaseToolRenderer):
11
+ tool_name: ClassVar[str] = "create_vulnerability_report"
12
+ css_classes: ClassVar[list[str]] = ["tool-call", "reporting-tool"]
13
+
14
+ @classmethod
15
+ def render(cls, tool_data: dict[str, Any]) -> Static:
16
+ args = tool_data.get("args", {})
17
+
18
+ title = args.get("title", "")
19
+ severity = args.get("severity", "")
20
+ content = args.get("content", "")
21
+
22
+ header = "🐞 [bold #ea580c]Vulnerability Report[/]"
23
+
24
+ if title:
25
+ content_parts = [f"{header}\n [bold]{cls.escape_markup(title)}[/]"]
26
+
27
+ if severity:
28
+ severity_color = cls._get_severity_color(severity.lower())
29
+ content_parts.append(
30
+ f" [dim]Severity: [{severity_color}]{severity.upper()}[/{severity_color}][/]"
31
+ )
32
+
33
+ if content:
34
+ content_preview = content[:100] + "..." if len(content) > 100 else content
35
+ content_parts.append(f" [dim]{cls.escape_markup(content_preview)}[/]")
36
+
37
+ content_text = "\n".join(content_parts)
38
+ else:
39
+ content_text = f"{header}\n [dim]Creating report...[/]"
40
+
41
+ css_classes = cls.get_css_classes("completed")
42
+ return Static(content_text, classes=css_classes)
43
+
44
+ @classmethod
45
+ def _get_severity_color(cls, severity: str) -> str:
46
+ severity_colors = {
47
+ "critical": "#dc2626",
48
+ "high": "#ea580c",
49
+ "medium": "#d97706",
50
+ "low": "#65a30d",
51
+ "info": "#0284c7",
52
+ }
53
+ return severity_colors.get(severity, "#6b7280")
@@ -0,0 +1,58 @@
1
+ from typing import Any, ClassVar
2
+
3
+ from textual.widgets import Static
4
+
5
+ from .base_renderer import BaseToolRenderer
6
+ from .registry import register_tool_renderer
7
+
8
+
9
+ @register_tool_renderer
10
+ class ScanStartInfoRenderer(BaseToolRenderer):
11
+ tool_name: ClassVar[str] = "scan_start_info"
12
+ css_classes: ClassVar[list[str]] = ["tool-call", "scan-info-tool"]
13
+
14
+ @classmethod
15
+ def render(cls, tool_data: dict[str, Any]) -> Static:
16
+ args = tool_data.get("args", {})
17
+ status = tool_data.get("status", "unknown")
18
+
19
+ target = args.get("target", {})
20
+
21
+ target_display = cls._build_target_display(target)
22
+
23
+ content = f"🚀 Starting scan on {target_display}"
24
+
25
+ css_classes = cls.get_css_classes(status)
26
+ return Static(content, classes=css_classes)
27
+
28
+ @classmethod
29
+ def _build_target_display(cls, target: dict[str, Any]) -> str:
30
+ if target_url := target.get("target_url"):
31
+ return f"[bold #22c55e]{target_url}[/bold #22c55e]"
32
+ if target_repo := target.get("target_repo"):
33
+ return f"[bold #22c55e]{target_repo}[/bold #22c55e]"
34
+ if target_path := target.get("target_path"):
35
+ return f"[bold #22c55e]{target_path}[/bold #22c55e]"
36
+ return "[dim]unknown target[/dim]"
37
+
38
+
39
+ @register_tool_renderer
40
+ class SubagentStartInfoRenderer(BaseToolRenderer):
41
+ tool_name: ClassVar[str] = "subagent_start_info"
42
+ css_classes: ClassVar[list[str]] = ["tool-call", "subagent-info-tool"]
43
+
44
+ @classmethod
45
+ def render(cls, tool_data: dict[str, Any]) -> Static:
46
+ args = tool_data.get("args", {})
47
+ status = tool_data.get("status", "unknown")
48
+
49
+ name = args.get("name", "Unknown Agent")
50
+ task = args.get("task", "")
51
+
52
+ content = f"🤖 Spawned subagent [bold #22c55e]{name}[/bold #22c55e]"
53
+ if task:
54
+ display_task = task[:80] + "..." if len(task) > 80 else task
55
+ content += f"\n Task: [dim]{display_task}[/dim]"
56
+
57
+ css_classes = cls.get_css_classes(status)
58
+ return Static(content, classes=css_classes)
@@ -0,0 +1,99 @@
1
+ from typing import Any, ClassVar
2
+
3
+ from textual.widgets import Static
4
+
5
+ from .base_renderer import BaseToolRenderer
6
+ from .registry import register_tool_renderer
7
+
8
+
9
+ @register_tool_renderer
10
+ class TerminalRenderer(BaseToolRenderer):
11
+ tool_name: ClassVar[str] = "terminal_action"
12
+ css_classes: ClassVar[list[str]] = ["tool-call", "terminal-tool"]
13
+
14
+ @classmethod
15
+ def render(cls, tool_data: dict[str, Any]) -> Static:
16
+ args = tool_data.get("args", {})
17
+ status = tool_data.get("status", "unknown")
18
+ result = tool_data.get("result", {})
19
+
20
+ action = args.get("action", "unknown")
21
+ inputs = args.get("inputs", [])
22
+ terminal_id = args.get("terminal_id", "default")
23
+
24
+ content = cls._build_sleek_content(action, inputs, terminal_id, result)
25
+
26
+ css_classes = cls.get_css_classes(status)
27
+ return Static(content, classes=css_classes)
28
+
29
+ @classmethod
30
+ def _build_sleek_content(
31
+ cls,
32
+ action: str,
33
+ inputs: list[str],
34
+ terminal_id: str, # noqa: ARG003
35
+ result: dict[str, Any], # noqa: ARG003
36
+ ) -> str:
37
+ terminal_icon = ">_"
38
+
39
+ if action in {"create", "new_terminal"}:
40
+ command = cls._format_command(inputs) if inputs else "bash"
41
+ return f"{terminal_icon} [#22c55e]${command}[/]"
42
+
43
+ if action == "send_input":
44
+ command = cls._format_command(inputs)
45
+ return f"{terminal_icon} [#22c55e]${command}[/]"
46
+
47
+ if action == "wait":
48
+ return f"{terminal_icon} [dim]waiting...[/]"
49
+
50
+ if action == "close":
51
+ return f"{terminal_icon} [dim]close[/]"
52
+
53
+ if action == "get_snapshot":
54
+ return f"{terminal_icon} [dim]snapshot[/]"
55
+
56
+ return f"{terminal_icon} [dim]{action}[/]"
57
+
58
+ @classmethod
59
+ def _format_command(cls, inputs: list[str]) -> str:
60
+ if not inputs:
61
+ return ""
62
+
63
+ command_parts = []
64
+
65
+ for input_item in inputs:
66
+ if input_item == "Enter":
67
+ break
68
+ if input_item.startswith("literal:"):
69
+ command_parts.append(input_item[8:])
70
+ elif input_item in [
71
+ "Space",
72
+ "Tab",
73
+ "Backspace",
74
+ "Up",
75
+ "Down",
76
+ "Left",
77
+ "Right",
78
+ "Home",
79
+ "End",
80
+ "PageUp",
81
+ "PageDown",
82
+ "Insert",
83
+ "Delete",
84
+ "Escape",
85
+ ] or input_item.startswith(("^", "C-", "S-", "A-", "F")):
86
+ if input_item == "Space":
87
+ command_parts.append(" ")
88
+ elif input_item == "Tab":
89
+ command_parts.append("\t")
90
+ continue
91
+ else:
92
+ command_parts.append(input_item)
93
+
94
+ command = "".join(command_parts).strip()
95
+
96
+ if len(command) > 200:
97
+ command = command[:197] + "..."
98
+
99
+ return cls.escape_markup(command) if command else "bash"
@@ -0,0 +1,29 @@
1
+ from typing import Any, ClassVar
2
+
3
+ from textual.widgets import Static
4
+
5
+ from .base_renderer import BaseToolRenderer
6
+ from .registry import register_tool_renderer
7
+
8
+
9
+ @register_tool_renderer
10
+ class ThinkRenderer(BaseToolRenderer):
11
+ tool_name: ClassVar[str] = "think"
12
+ css_classes: ClassVar[list[str]] = ["tool-call", "thinking-tool"]
13
+
14
+ @classmethod
15
+ def render(cls, tool_data: dict[str, Any]) -> Static:
16
+ args = tool_data.get("args", {})
17
+
18
+ thought = args.get("thought", "")
19
+
20
+ header = "🧠 [bold #a855f7]Thinking[/]"
21
+
22
+ if thought:
23
+ thought_display = thought[:200] + "..." if len(thought) > 200 else thought
24
+ content = f"{header}\n [italic dim]{cls.escape_markup(thought_display)}[/]"
25
+ else:
26
+ content = f"{header}\n [italic dim]Thinking...[/]"
27
+
28
+ css_classes = cls.get_css_classes("completed")
29
+ return Static(content, classes=css_classes)
@@ -0,0 +1,43 @@
1
+ from typing import Any, ClassVar
2
+
3
+ from textual.widgets import Static
4
+
5
+ from .base_renderer import BaseToolRenderer
6
+ from .registry import register_tool_renderer
7
+
8
+
9
+ @register_tool_renderer
10
+ class UserMessageRenderer(BaseToolRenderer):
11
+ tool_name: ClassVar[str] = "user_message"
12
+ css_classes: ClassVar[list[str]] = ["chat-message", "user-message"]
13
+
14
+ @classmethod
15
+ def render(cls, message_data: dict[str, Any]) -> Static:
16
+ content = message_data.get("content", "")
17
+
18
+ if not content:
19
+ return Static("", classes=cls.css_classes)
20
+
21
+ if len(content) > 300:
22
+ content = content[:297] + "..."
23
+
24
+ lines = content.split("\n")
25
+ bordered_lines = [f"[#3b82f6]▍[/#3b82f6] {line}" for line in lines]
26
+ bordered_content = "\n".join(bordered_lines)
27
+ formatted_content = f"[#3b82f6]▍[/#3b82f6] [bold]You:[/]\n{bordered_content}"
28
+
29
+ css_classes = " ".join(cls.css_classes)
30
+ return Static(formatted_content, classes=css_classes)
31
+
32
+ @classmethod
33
+ def render_simple(cls, content: str) -> str:
34
+ if not content:
35
+ return ""
36
+
37
+ if len(content) > 300:
38
+ content = content[:297] + "..."
39
+
40
+ lines = content.split("\n")
41
+ bordered_lines = [f"[#3b82f6]▍[/#3b82f6] {line}" for line in lines]
42
+ bordered_content = "\n".join(bordered_lines)
43
+ return f"[#3b82f6]▍[/#3b82f6] [bold]You:[/]\n{bordered_content}"
@@ -0,0 +1,28 @@
1
+ from typing import Any, ClassVar
2
+
3
+ from textual.widgets import Static
4
+
5
+ from .base_renderer import BaseToolRenderer
6
+ from .registry import register_tool_renderer
7
+
8
+
9
+ @register_tool_renderer
10
+ class WebSearchRenderer(BaseToolRenderer):
11
+ tool_name: ClassVar[str] = "web_search"
12
+ css_classes: ClassVar[list[str]] = ["tool-call", "web-search-tool"]
13
+
14
+ @classmethod
15
+ def render(cls, tool_data: dict[str, Any]) -> Static:
16
+ args = tool_data.get("args", {})
17
+ query = args.get("query", "")
18
+
19
+ header = "🌐 [bold #60a5fa]Searching the web...[/]"
20
+
21
+ if query:
22
+ query_display = query[:100] + "..." if len(query) > 100 else query
23
+ content_text = f"{header}\n [dim]{cls.escape_markup(query_display)}[/]"
24
+ else:
25
+ content_text = f"{header}"
26
+
27
+ css_classes = cls.get_css_classes("completed")
28
+ return Static(content_text, classes=css_classes)