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,131 +1,311 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from functools import cache
|
|
1
3
|
from typing import Any, ClassVar
|
|
2
4
|
|
|
5
|
+
from pygments.lexers import get_lexer_by_name
|
|
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
|
+
(
|
|
19
|
+
r"\n?\[Command still running after [\d.]+s - showing output so far\.?"
|
|
20
|
+
r"\s*(?:Use C-c to interrupt if needed\.)?\]"
|
|
21
|
+
),
|
|
22
|
+
r"^\[Below is the output of the previous command\.\]\n?",
|
|
23
|
+
r"^No command is currently running\. Cannot send input\.$",
|
|
24
|
+
(
|
|
25
|
+
r"^A command is already running\. Use is_input=true to send input to it, "
|
|
26
|
+
r"or interrupt it first \(e\.g\., with C-c\)\.$"
|
|
27
|
+
),
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@cache
|
|
32
|
+
def _get_style_colors() -> dict[Any, str]:
|
|
33
|
+
style = get_style_by_name("native")
|
|
34
|
+
return {token: f"#{style_def['color']}" for token, style_def in style if style_def["color"]}
|
|
35
|
+
|
|
36
|
+
|
|
9
37
|
@register_tool_renderer
|
|
10
38
|
class TerminalRenderer(BaseToolRenderer):
|
|
11
39
|
tool_name: ClassVar[str] = "terminal_execute"
|
|
12
40
|
css_classes: ClassVar[list[str]] = ["tool-call", "terminal-tool"]
|
|
13
41
|
|
|
42
|
+
CONTROL_SEQUENCES: ClassVar[set[str]] = {
|
|
43
|
+
"C-c",
|
|
44
|
+
"C-d",
|
|
45
|
+
"C-z",
|
|
46
|
+
"C-a",
|
|
47
|
+
"C-e",
|
|
48
|
+
"C-k",
|
|
49
|
+
"C-l",
|
|
50
|
+
"C-u",
|
|
51
|
+
"C-w",
|
|
52
|
+
"C-r",
|
|
53
|
+
"C-s",
|
|
54
|
+
"C-t",
|
|
55
|
+
"C-y",
|
|
56
|
+
"^c",
|
|
57
|
+
"^d",
|
|
58
|
+
"^z",
|
|
59
|
+
"^a",
|
|
60
|
+
"^e",
|
|
61
|
+
"^k",
|
|
62
|
+
"^l",
|
|
63
|
+
"^u",
|
|
64
|
+
"^w",
|
|
65
|
+
"^r",
|
|
66
|
+
"^s",
|
|
67
|
+
"^t",
|
|
68
|
+
"^y",
|
|
69
|
+
}
|
|
70
|
+
SPECIAL_KEYS: ClassVar[set[str]] = {
|
|
71
|
+
"Enter",
|
|
72
|
+
"Escape",
|
|
73
|
+
"Space",
|
|
74
|
+
"Tab",
|
|
75
|
+
"BTab",
|
|
76
|
+
"BSpace",
|
|
77
|
+
"DC",
|
|
78
|
+
"IC",
|
|
79
|
+
"Up",
|
|
80
|
+
"Down",
|
|
81
|
+
"Left",
|
|
82
|
+
"Right",
|
|
83
|
+
"Home",
|
|
84
|
+
"End",
|
|
85
|
+
"PageUp",
|
|
86
|
+
"PageDown",
|
|
87
|
+
"PgUp",
|
|
88
|
+
"PgDn",
|
|
89
|
+
"PPage",
|
|
90
|
+
"NPage",
|
|
91
|
+
"F1",
|
|
92
|
+
"F2",
|
|
93
|
+
"F3",
|
|
94
|
+
"F4",
|
|
95
|
+
"F5",
|
|
96
|
+
"F6",
|
|
97
|
+
"F7",
|
|
98
|
+
"F8",
|
|
99
|
+
"F9",
|
|
100
|
+
"F10",
|
|
101
|
+
"F11",
|
|
102
|
+
"F12",
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def _get_token_color(cls, token_type: Any) -> str | None:
|
|
107
|
+
colors = _get_style_colors()
|
|
108
|
+
while token_type:
|
|
109
|
+
if token_type in colors:
|
|
110
|
+
return colors[token_type]
|
|
111
|
+
token_type = token_type.parent
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def _highlight_bash(cls, code: str) -> Text:
|
|
116
|
+
lexer = get_lexer_by_name("bash")
|
|
117
|
+
text = Text()
|
|
118
|
+
|
|
119
|
+
for token_type, token_value in lexer.get_tokens(code):
|
|
120
|
+
if not token_value:
|
|
121
|
+
continue
|
|
122
|
+
color = cls._get_token_color(token_type)
|
|
123
|
+
text.append(token_value, style=color)
|
|
124
|
+
|
|
125
|
+
return text
|
|
126
|
+
|
|
14
127
|
@classmethod
|
|
15
128
|
def render(cls, tool_data: dict[str, Any]) -> Static:
|
|
16
129
|
args = tool_data.get("args", {})
|
|
17
130
|
status = tool_data.get("status", "unknown")
|
|
18
|
-
result = tool_data.get("result"
|
|
131
|
+
result = tool_data.get("result")
|
|
19
132
|
|
|
20
133
|
command = args.get("command", "")
|
|
21
134
|
is_input = args.get("is_input", False)
|
|
22
|
-
terminal_id = args.get("terminal_id", "default")
|
|
23
|
-
timeout = args.get("timeout")
|
|
24
135
|
|
|
25
|
-
content = cls.
|
|
136
|
+
content = cls._build_content(command, is_input, status, result)
|
|
26
137
|
|
|
27
138
|
css_classes = cls.get_css_classes(status)
|
|
28
139
|
return Static(content, classes=css_classes)
|
|
29
140
|
|
|
30
141
|
@classmethod
|
|
31
|
-
def
|
|
32
|
-
cls,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
terminal_id: str, # noqa: ARG003
|
|
36
|
-
timeout: float | None, # noqa: ARG003
|
|
37
|
-
result: dict[str, Any], # noqa: ARG003
|
|
38
|
-
) -> str:
|
|
142
|
+
def _build_content(
|
|
143
|
+
cls, command: str, is_input: bool, status: str, result: dict[str, Any] | str | None
|
|
144
|
+
) -> Text:
|
|
145
|
+
text = Text()
|
|
39
146
|
terminal_icon = ">_"
|
|
40
147
|
|
|
41
148
|
if not command.strip():
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"C-a",
|
|
49
|
-
"C-e",
|
|
50
|
-
"C-k",
|
|
51
|
-
"C-l",
|
|
52
|
-
"C-u",
|
|
53
|
-
"C-w",
|
|
54
|
-
"C-r",
|
|
55
|
-
"C-s",
|
|
56
|
-
"C-t",
|
|
57
|
-
"C-y",
|
|
58
|
-
"^c",
|
|
59
|
-
"^d",
|
|
60
|
-
"^z",
|
|
61
|
-
"^a",
|
|
62
|
-
"^e",
|
|
63
|
-
"^k",
|
|
64
|
-
"^l",
|
|
65
|
-
"^u",
|
|
66
|
-
"^w",
|
|
67
|
-
"^r",
|
|
68
|
-
"^s",
|
|
69
|
-
"^t",
|
|
70
|
-
"^y",
|
|
71
|
-
}
|
|
72
|
-
special_keys = {
|
|
73
|
-
"Enter",
|
|
74
|
-
"Escape",
|
|
75
|
-
"Space",
|
|
76
|
-
"Tab",
|
|
77
|
-
"BTab",
|
|
78
|
-
"BSpace",
|
|
79
|
-
"DC",
|
|
80
|
-
"IC",
|
|
81
|
-
"Up",
|
|
82
|
-
"Down",
|
|
83
|
-
"Left",
|
|
84
|
-
"Right",
|
|
85
|
-
"Home",
|
|
86
|
-
"End",
|
|
87
|
-
"PageUp",
|
|
88
|
-
"PageDown",
|
|
89
|
-
"PgUp",
|
|
90
|
-
"PgDn",
|
|
91
|
-
"PPage",
|
|
92
|
-
"NPage",
|
|
93
|
-
"F1",
|
|
94
|
-
"F2",
|
|
95
|
-
"F3",
|
|
96
|
-
"F4",
|
|
97
|
-
"F5",
|
|
98
|
-
"F6",
|
|
99
|
-
"F7",
|
|
100
|
-
"F8",
|
|
101
|
-
"F9",
|
|
102
|
-
"F10",
|
|
103
|
-
"F11",
|
|
104
|
-
"F12",
|
|
105
|
-
}
|
|
149
|
+
text.append(terminal_icon, style="dim")
|
|
150
|
+
text.append(" ")
|
|
151
|
+
text.append("getting logs...", style="dim")
|
|
152
|
+
if result:
|
|
153
|
+
cls._append_output(text, result, status, command)
|
|
154
|
+
return text
|
|
106
155
|
|
|
107
156
|
is_special = (
|
|
108
|
-
command in
|
|
109
|
-
or command in
|
|
157
|
+
command in cls.CONTROL_SEQUENCES
|
|
158
|
+
or command in cls.SPECIAL_KEYS
|
|
110
159
|
or command.startswith(("M-", "S-", "C-S-", "C-M-", "S-M-"))
|
|
111
160
|
)
|
|
112
161
|
|
|
162
|
+
text.append(terminal_icon, style="dim")
|
|
163
|
+
text.append(" ")
|
|
164
|
+
|
|
113
165
|
if is_special:
|
|
114
|
-
|
|
166
|
+
text.append(command, style="#ef4444")
|
|
167
|
+
elif is_input:
|
|
168
|
+
text.append(">>>", style="#3b82f6")
|
|
169
|
+
text.append(" ")
|
|
170
|
+
text.append_text(cls._format_command(command))
|
|
171
|
+
else:
|
|
172
|
+
text.append("$", style="#22c55e")
|
|
173
|
+
text.append(" ")
|
|
174
|
+
text.append_text(cls._format_command(command))
|
|
115
175
|
|
|
116
|
-
if
|
|
117
|
-
|
|
118
|
-
return f"{terminal_icon} [#3b82f6]>>>[/] [#22c55e]{formatted_command}[/]"
|
|
176
|
+
if result:
|
|
177
|
+
cls._append_output(text, result, status, command)
|
|
119
178
|
|
|
120
|
-
|
|
121
|
-
return f"{terminal_icon} [#22c55e]$ {formatted_command}[/]"
|
|
179
|
+
return text
|
|
122
180
|
|
|
123
181
|
@classmethod
|
|
124
|
-
def
|
|
125
|
-
|
|
126
|
-
|
|
182
|
+
def _clean_output(cls, output: str, command: str = "") -> str:
|
|
183
|
+
cleaned = output
|
|
184
|
+
|
|
185
|
+
for pattern in STRIP_PATTERNS:
|
|
186
|
+
cleaned = re.sub(pattern, "", cleaned, flags=re.MULTILINE)
|
|
187
|
+
|
|
188
|
+
if cleaned.strip():
|
|
189
|
+
lines = cleaned.splitlines()
|
|
190
|
+
filtered_lines: list[str] = []
|
|
191
|
+
for line in lines:
|
|
192
|
+
if not filtered_lines and not line.strip():
|
|
193
|
+
continue
|
|
194
|
+
if re.match(r"^\[STRIX_\d+\]\$\s*", line):
|
|
195
|
+
continue
|
|
196
|
+
if command and line.strip() == command.strip():
|
|
197
|
+
continue
|
|
198
|
+
if command and re.match(r"^[\$#>]\s*" + re.escape(command.strip()) + r"\s*$", line):
|
|
199
|
+
continue
|
|
200
|
+
filtered_lines.append(line)
|
|
127
201
|
|
|
128
|
-
|
|
129
|
-
|
|
202
|
+
while filtered_lines and re.match(r"^\[STRIX_\d+\]\$\s*", filtered_lines[-1]):
|
|
203
|
+
filtered_lines.pop()
|
|
204
|
+
|
|
205
|
+
cleaned = "\n".join(filtered_lines)
|
|
206
|
+
|
|
207
|
+
return cleaned.strip()
|
|
208
|
+
|
|
209
|
+
@classmethod
|
|
210
|
+
def _append_output(
|
|
211
|
+
cls, text: Text, result: dict[str, Any] | str, tool_status: str, command: str = ""
|
|
212
|
+
) -> None:
|
|
213
|
+
if isinstance(result, str):
|
|
214
|
+
if result.strip():
|
|
215
|
+
text.append("\n")
|
|
216
|
+
text.append_text(cls._format_output(result))
|
|
217
|
+
return
|
|
130
218
|
|
|
131
|
-
|
|
219
|
+
raw_output = result.get("content", "")
|
|
220
|
+
output = cls._clean_output(raw_output, command)
|
|
221
|
+
error = result.get("error")
|
|
222
|
+
exit_code = result.get("exit_code")
|
|
223
|
+
result_status = result.get("status", "")
|
|
224
|
+
|
|
225
|
+
if error and not cls._is_status_message(error):
|
|
226
|
+
text.append("\n")
|
|
227
|
+
text.append(" error: ", style="bold #ef4444")
|
|
228
|
+
text.append(cls._truncate_line(error), style="#ef4444")
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
if result_status == "running" or tool_status == "running":
|
|
232
|
+
if output and output.strip():
|
|
233
|
+
text.append("\n")
|
|
234
|
+
formatted_output = cls._format_output(output)
|
|
235
|
+
text.append_text(formatted_output)
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
if not output or not output.strip():
|
|
239
|
+
if exit_code is not None and exit_code != 0:
|
|
240
|
+
text.append("\n")
|
|
241
|
+
text.append(f" exit {exit_code}", style="dim #ef4444")
|
|
242
|
+
return
|
|
243
|
+
|
|
244
|
+
text.append("\n")
|
|
245
|
+
formatted_output = cls._format_output(output)
|
|
246
|
+
text.append_text(formatted_output)
|
|
247
|
+
|
|
248
|
+
if exit_code is not None and exit_code != 0:
|
|
249
|
+
text.append("\n")
|
|
250
|
+
text.append(f" exit {exit_code}", style="dim #ef4444")
|
|
251
|
+
|
|
252
|
+
@classmethod
|
|
253
|
+
def _is_status_message(cls, message: str) -> bool:
|
|
254
|
+
status_patterns = [
|
|
255
|
+
r"No command is currently running",
|
|
256
|
+
r"A command is already running",
|
|
257
|
+
r"Cannot send input",
|
|
258
|
+
r"Use is_input=true",
|
|
259
|
+
r"Use C-c to interrupt",
|
|
260
|
+
r"showing output so far",
|
|
261
|
+
]
|
|
262
|
+
return any(re.search(pattern, message) for pattern in status_patterns)
|
|
263
|
+
|
|
264
|
+
@classmethod
|
|
265
|
+
def _format_output(cls, output: str) -> Text:
|
|
266
|
+
text = Text()
|
|
267
|
+
lines = output.splitlines()
|
|
268
|
+
total_lines = len(lines)
|
|
269
|
+
|
|
270
|
+
head_count = MAX_OUTPUT_LINES // 2
|
|
271
|
+
tail_count = MAX_OUTPUT_LINES - head_count - 1
|
|
272
|
+
|
|
273
|
+
if total_lines <= MAX_OUTPUT_LINES:
|
|
274
|
+
display_lines = lines
|
|
275
|
+
truncated = False
|
|
276
|
+
hidden_count = 0
|
|
277
|
+
else:
|
|
278
|
+
display_lines = lines[:head_count]
|
|
279
|
+
truncated = True
|
|
280
|
+
hidden_count = total_lines - head_count - tail_count
|
|
281
|
+
|
|
282
|
+
for i, line in enumerate(display_lines):
|
|
283
|
+
truncated_line = cls._truncate_line(line)
|
|
284
|
+
text.append(" ")
|
|
285
|
+
text.append(truncated_line, style="dim")
|
|
286
|
+
if i < len(display_lines) - 1 or truncated:
|
|
287
|
+
text.append("\n")
|
|
288
|
+
|
|
289
|
+
if truncated:
|
|
290
|
+
text.append(f" ... {hidden_count} lines truncated ...", style="dim italic")
|
|
291
|
+
text.append("\n")
|
|
292
|
+
tail_lines = lines[-tail_count:]
|
|
293
|
+
for i, line in enumerate(tail_lines):
|
|
294
|
+
truncated_line = cls._truncate_line(line)
|
|
295
|
+
text.append(" ")
|
|
296
|
+
text.append(truncated_line, style="dim")
|
|
297
|
+
if i < len(tail_lines) - 1:
|
|
298
|
+
text.append("\n")
|
|
299
|
+
|
|
300
|
+
return text
|
|
301
|
+
|
|
302
|
+
@classmethod
|
|
303
|
+
def _truncate_line(cls, line: str) -> str:
|
|
304
|
+
clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line)
|
|
305
|
+
if len(clean_line) > MAX_LINE_LENGTH:
|
|
306
|
+
return line[: MAX_LINE_LENGTH - 3] + "..."
|
|
307
|
+
return line
|
|
308
|
+
|
|
309
|
+
@classmethod
|
|
310
|
+
def _format_command(cls, command: str) -> Text:
|
|
311
|
+
return cls._highlight_bash(command)
|
|
@@ -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
|
|
@@ -14,16 +15,17 @@ class ThinkRenderer(BaseToolRenderer):
|
|
|
14
15
|
@classmethod
|
|
15
16
|
def render(cls, tool_data: dict[str, Any]) -> Static:
|
|
16
17
|
args = tool_data.get("args", {})
|
|
17
|
-
|
|
18
18
|
thought = args.get("thought", "")
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
text = Text()
|
|
21
|
+
text.append("🧠 ")
|
|
22
|
+
text.append("Thinking", style="bold #a855f7")
|
|
23
|
+
text.append("\n ")
|
|
21
24
|
|
|
22
25
|
if thought:
|
|
23
|
-
|
|
24
|
-
content = f"{header}\n [italic dim]{cls.escape_markup(thought_display)}[/]"
|
|
26
|
+
text.append(thought, style="italic dim")
|
|
25
27
|
else:
|
|
26
|
-
|
|
28
|
+
text.append("Thinking...", style="italic dim")
|
|
27
29
|
|
|
28
30
|
css_classes = cls.get_css_classes("completed")
|
|
29
|
-
return Static(
|
|
31
|
+
return Static(text, classes=css_classes)
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
from typing import Any, ClassVar
|
|
2
|
+
|
|
3
|
+
from rich.text import Text
|
|
4
|
+
from textual.widgets import Static
|
|
5
|
+
|
|
6
|
+
from .base_renderer import BaseToolRenderer
|
|
7
|
+
from .registry import register_tool_renderer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
STATUS_MARKERS: dict[str, str] = {
|
|
11
|
+
"pending": "[ ]",
|
|
12
|
+
"in_progress": "[~]",
|
|
13
|
+
"done": "[•]",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _format_todo_lines(text: Text, result: dict[str, Any]) -> None:
|
|
18
|
+
todos = result.get("todos")
|
|
19
|
+
if not isinstance(todos, list) or not todos:
|
|
20
|
+
text.append("\n ")
|
|
21
|
+
text.append("No todos", style="dim")
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
for todo in todos:
|
|
25
|
+
status = todo.get("status", "pending")
|
|
26
|
+
marker = STATUS_MARKERS.get(status, STATUS_MARKERS["pending"])
|
|
27
|
+
|
|
28
|
+
title = todo.get("title", "").strip() or "(untitled)"
|
|
29
|
+
|
|
30
|
+
text.append("\n ")
|
|
31
|
+
text.append(marker)
|
|
32
|
+
text.append(" ")
|
|
33
|
+
|
|
34
|
+
if status == "done":
|
|
35
|
+
text.append(title, style="dim strike")
|
|
36
|
+
elif status == "in_progress":
|
|
37
|
+
text.append(title, style="italic")
|
|
38
|
+
else:
|
|
39
|
+
text.append(title)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@register_tool_renderer
|
|
43
|
+
class CreateTodoRenderer(BaseToolRenderer):
|
|
44
|
+
tool_name: ClassVar[str] = "create_todo"
|
|
45
|
+
css_classes: ClassVar[list[str]] = ["tool-call", "todo-tool"]
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def render(cls, tool_data: dict[str, Any]) -> Static:
|
|
49
|
+
result = tool_data.get("result")
|
|
50
|
+
|
|
51
|
+
text = Text()
|
|
52
|
+
text.append("📋 ")
|
|
53
|
+
text.append("Todo", style="bold #a78bfa")
|
|
54
|
+
|
|
55
|
+
if isinstance(result, str) and result.strip():
|
|
56
|
+
text.append("\n ")
|
|
57
|
+
text.append(result.strip(), style="dim")
|
|
58
|
+
elif result and isinstance(result, dict):
|
|
59
|
+
if result.get("success"):
|
|
60
|
+
_format_todo_lines(text, result)
|
|
61
|
+
else:
|
|
62
|
+
error = result.get("error", "Failed to create todo")
|
|
63
|
+
text.append("\n ")
|
|
64
|
+
text.append(error, style="#ef4444")
|
|
65
|
+
else:
|
|
66
|
+
text.append("\n ")
|
|
67
|
+
text.append("Creating...", style="dim")
|
|
68
|
+
|
|
69
|
+
css_classes = cls.get_css_classes("completed")
|
|
70
|
+
return Static(text, classes=css_classes)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@register_tool_renderer
|
|
74
|
+
class ListTodosRenderer(BaseToolRenderer):
|
|
75
|
+
tool_name: ClassVar[str] = "list_todos"
|
|
76
|
+
css_classes: ClassVar[list[str]] = ["tool-call", "todo-tool"]
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def render(cls, tool_data: dict[str, Any]) -> Static:
|
|
80
|
+
result = tool_data.get("result")
|
|
81
|
+
|
|
82
|
+
text = Text()
|
|
83
|
+
text.append("📋 ")
|
|
84
|
+
text.append("Todos", style="bold #a78bfa")
|
|
85
|
+
|
|
86
|
+
if isinstance(result, str) and result.strip():
|
|
87
|
+
text.append("\n ")
|
|
88
|
+
text.append(result.strip(), style="dim")
|
|
89
|
+
elif result and isinstance(result, dict):
|
|
90
|
+
if result.get("success"):
|
|
91
|
+
_format_todo_lines(text, result)
|
|
92
|
+
else:
|
|
93
|
+
error = result.get("error", "Unable to list todos")
|
|
94
|
+
text.append("\n ")
|
|
95
|
+
text.append(error, style="#ef4444")
|
|
96
|
+
else:
|
|
97
|
+
text.append("\n ")
|
|
98
|
+
text.append("Loading...", style="dim")
|
|
99
|
+
|
|
100
|
+
css_classes = cls.get_css_classes("completed")
|
|
101
|
+
return Static(text, classes=css_classes)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@register_tool_renderer
|
|
105
|
+
class UpdateTodoRenderer(BaseToolRenderer):
|
|
106
|
+
tool_name: ClassVar[str] = "update_todo"
|
|
107
|
+
css_classes: ClassVar[list[str]] = ["tool-call", "todo-tool"]
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def render(cls, tool_data: dict[str, Any]) -> Static:
|
|
111
|
+
result = tool_data.get("result")
|
|
112
|
+
|
|
113
|
+
text = Text()
|
|
114
|
+
text.append("📋 ")
|
|
115
|
+
text.append("Todo Updated", style="bold #a78bfa")
|
|
116
|
+
|
|
117
|
+
if isinstance(result, str) and result.strip():
|
|
118
|
+
text.append("\n ")
|
|
119
|
+
text.append(result.strip(), style="dim")
|
|
120
|
+
elif result and isinstance(result, dict):
|
|
121
|
+
if result.get("success"):
|
|
122
|
+
_format_todo_lines(text, result)
|
|
123
|
+
else:
|
|
124
|
+
error = result.get("error", "Failed to update todo")
|
|
125
|
+
text.append("\n ")
|
|
126
|
+
text.append(error, style="#ef4444")
|
|
127
|
+
else:
|
|
128
|
+
text.append("\n ")
|
|
129
|
+
text.append("Updating...", style="dim")
|
|
130
|
+
|
|
131
|
+
css_classes = cls.get_css_classes("completed")
|
|
132
|
+
return Static(text, classes=css_classes)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@register_tool_renderer
|
|
136
|
+
class MarkTodoDoneRenderer(BaseToolRenderer):
|
|
137
|
+
tool_name: ClassVar[str] = "mark_todo_done"
|
|
138
|
+
css_classes: ClassVar[list[str]] = ["tool-call", "todo-tool"]
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def render(cls, tool_data: dict[str, Any]) -> Static:
|
|
142
|
+
result = tool_data.get("result")
|
|
143
|
+
|
|
144
|
+
text = Text()
|
|
145
|
+
text.append("📋 ")
|
|
146
|
+
text.append("Todo Completed", style="bold #a78bfa")
|
|
147
|
+
|
|
148
|
+
if isinstance(result, str) and result.strip():
|
|
149
|
+
text.append("\n ")
|
|
150
|
+
text.append(result.strip(), style="dim")
|
|
151
|
+
elif result and isinstance(result, dict):
|
|
152
|
+
if result.get("success"):
|
|
153
|
+
_format_todo_lines(text, result)
|
|
154
|
+
else:
|
|
155
|
+
error = result.get("error", "Failed to mark todo done")
|
|
156
|
+
text.append("\n ")
|
|
157
|
+
text.append(error, style="#ef4444")
|
|
158
|
+
else:
|
|
159
|
+
text.append("\n ")
|
|
160
|
+
text.append("Marking done...", style="dim")
|
|
161
|
+
|
|
162
|
+
css_classes = cls.get_css_classes("completed")
|
|
163
|
+
return Static(text, classes=css_classes)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@register_tool_renderer
|
|
167
|
+
class MarkTodoPendingRenderer(BaseToolRenderer):
|
|
168
|
+
tool_name: ClassVar[str] = "mark_todo_pending"
|
|
169
|
+
css_classes: ClassVar[list[str]] = ["tool-call", "todo-tool"]
|
|
170
|
+
|
|
171
|
+
@classmethod
|
|
172
|
+
def render(cls, tool_data: dict[str, Any]) -> Static:
|
|
173
|
+
result = tool_data.get("result")
|
|
174
|
+
|
|
175
|
+
text = Text()
|
|
176
|
+
text.append("📋 ")
|
|
177
|
+
text.append("Todo Reopened", style="bold #f59e0b")
|
|
178
|
+
|
|
179
|
+
if isinstance(result, str) and result.strip():
|
|
180
|
+
text.append("\n ")
|
|
181
|
+
text.append(result.strip(), style="dim")
|
|
182
|
+
elif result and isinstance(result, dict):
|
|
183
|
+
if result.get("success"):
|
|
184
|
+
_format_todo_lines(text, result)
|
|
185
|
+
else:
|
|
186
|
+
error = result.get("error", "Failed to reopen todo")
|
|
187
|
+
text.append("\n ")
|
|
188
|
+
text.append(error, style="#ef4444")
|
|
189
|
+
else:
|
|
190
|
+
text.append("\n ")
|
|
191
|
+
text.append("Reopening...", style="dim")
|
|
192
|
+
|
|
193
|
+
css_classes = cls.get_css_classes("completed")
|
|
194
|
+
return Static(text, classes=css_classes)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@register_tool_renderer
|
|
198
|
+
class DeleteTodoRenderer(BaseToolRenderer):
|
|
199
|
+
tool_name: ClassVar[str] = "delete_todo"
|
|
200
|
+
css_classes: ClassVar[list[str]] = ["tool-call", "todo-tool"]
|
|
201
|
+
|
|
202
|
+
@classmethod
|
|
203
|
+
def render(cls, tool_data: dict[str, Any]) -> Static:
|
|
204
|
+
result = tool_data.get("result")
|
|
205
|
+
|
|
206
|
+
text = Text()
|
|
207
|
+
text.append("📋 ")
|
|
208
|
+
text.append("Todo Removed", style="bold #94a3b8")
|
|
209
|
+
|
|
210
|
+
if isinstance(result, str) and result.strip():
|
|
211
|
+
text.append("\n ")
|
|
212
|
+
text.append(result.strip(), style="dim")
|
|
213
|
+
elif result and isinstance(result, dict):
|
|
214
|
+
if result.get("success"):
|
|
215
|
+
_format_todo_lines(text, result)
|
|
216
|
+
else:
|
|
217
|
+
error = result.get("error", "Failed to remove todo")
|
|
218
|
+
text.append("\n ")
|
|
219
|
+
text.append(error, style="#ef4444")
|
|
220
|
+
else:
|
|
221
|
+
text.append("\n ")
|
|
222
|
+
text.append("Removing...", style="dim")
|
|
223
|
+
|
|
224
|
+
css_classes = cls.get_css_classes("completed")
|
|
225
|
+
return Static(text, classes=css_classes)
|