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,129 @@
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 ViewAgentGraphRenderer(BaseToolRenderer):
11
+ tool_name: ClassVar[str] = "view_agent_graph"
12
+ css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"]
13
+
14
+ @classmethod
15
+ def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: ARG003
16
+ content_text = "🕸️ [bold #fbbf24]Viewing agents graph[/]"
17
+
18
+ css_classes = cls.get_css_classes("completed")
19
+ return Static(content_text, classes=css_classes)
20
+
21
+
22
+ @register_tool_renderer
23
+ class CreateAgentRenderer(BaseToolRenderer):
24
+ tool_name: ClassVar[str] = "create_agent"
25
+ css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"]
26
+
27
+ @classmethod
28
+ def render(cls, tool_data: dict[str, Any]) -> Static:
29
+ args = tool_data.get("args", {})
30
+
31
+ task = args.get("task", "")
32
+ name = args.get("name", "Agent")
33
+
34
+ header = f"🤖 [bold #fbbf24]Creating {name}[/]"
35
+
36
+ if task:
37
+ task_display = task[:400] + "..." if len(task) > 400 else task
38
+ content_text = f"{header}\n [dim]{cls.escape_markup(task_display)}[/]"
39
+ else:
40
+ content_text = f"{header}\n [dim]Spawning agent...[/]"
41
+
42
+ css_classes = cls.get_css_classes("completed")
43
+ return Static(content_text, classes=css_classes)
44
+
45
+
46
+ @register_tool_renderer
47
+ class SendMessageToAgentRenderer(BaseToolRenderer):
48
+ tool_name: ClassVar[str] = "send_message_to_agent"
49
+ css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"]
50
+
51
+ @classmethod
52
+ def render(cls, tool_data: dict[str, Any]) -> Static:
53
+ args = tool_data.get("args", {})
54
+
55
+ message = args.get("message", "")
56
+
57
+ header = "💬 [bold #fbbf24]Sending message[/]"
58
+
59
+ if message:
60
+ message_display = message[:400] + "..." if len(message) > 400 else message
61
+ content_text = f"{header}\n [dim]{cls.escape_markup(message_display)}[/]"
62
+ else:
63
+ content_text = f"{header}\n [dim]Sending...[/]"
64
+
65
+ css_classes = cls.get_css_classes("completed")
66
+ return Static(content_text, classes=css_classes)
67
+
68
+
69
+ @register_tool_renderer
70
+ class AgentFinishRenderer(BaseToolRenderer):
71
+ tool_name: ClassVar[str] = "agent_finish"
72
+ css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"]
73
+
74
+ @classmethod
75
+ def render(cls, tool_data: dict[str, Any]) -> Static:
76
+ args = tool_data.get("args", {})
77
+
78
+ result_summary = args.get("result_summary", "")
79
+ findings = args.get("findings", [])
80
+ success = args.get("success", True)
81
+
82
+ header = (
83
+ "🏁 [bold #fbbf24]Agent completed[/]" if success else "🏁 [bold #fbbf24]Agent failed[/]"
84
+ )
85
+
86
+ if result_summary:
87
+ summary_display = (
88
+ result_summary[:400] + "..." if len(result_summary) > 400 else result_summary
89
+ )
90
+ content_parts = [f"{header}\n [bold]{cls.escape_markup(summary_display)}[/]"]
91
+
92
+ if findings and isinstance(findings, list):
93
+ finding_lines = [f"• {finding}" for finding in findings[:3]]
94
+ if len(findings) > 3:
95
+ finding_lines.append(f"• ... +{len(findings) - 3} more findings")
96
+
97
+ content_parts.append(
98
+ f" [dim]{chr(10).join([cls.escape_markup(line) for line in finding_lines])}[/]"
99
+ )
100
+
101
+ content_text = "\n".join(content_parts)
102
+ else:
103
+ content_text = f"{header}\n [dim]Completing task...[/]"
104
+
105
+ css_classes = cls.get_css_classes("completed")
106
+ return Static(content_text, classes=css_classes)
107
+
108
+
109
+ @register_tool_renderer
110
+ class WaitForMessageRenderer(BaseToolRenderer):
111
+ tool_name: ClassVar[str] = "wait_for_message"
112
+ css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"]
113
+
114
+ @classmethod
115
+ def render(cls, tool_data: dict[str, Any]) -> Static:
116
+ args = tool_data.get("args", {})
117
+
118
+ reason = args.get("reason", "Waiting for messages from other agents or user input")
119
+
120
+ header = "⏸️ [bold #fbbf24]Waiting for messages[/]"
121
+
122
+ if reason:
123
+ reason_display = reason[:400] + "..." if len(reason) > 400 else reason
124
+ content_text = f"{header}\n [dim]{cls.escape_markup(reason_display)}[/]"
125
+ else:
126
+ content_text = f"{header}\n [dim]Agent paused until message received...[/]"
127
+
128
+ css_classes = cls.get_css_classes("completed")
129
+ return Static(content_text, classes=css_classes)
@@ -0,0 +1,61 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any, ClassVar
3
+
4
+ from textual.widgets import Static
5
+
6
+
7
+ class BaseToolRenderer(ABC):
8
+ tool_name: ClassVar[str] = ""
9
+
10
+ css_classes: ClassVar[list[str]] = ["tool-call"]
11
+
12
+ @classmethod
13
+ @abstractmethod
14
+ def render(cls, tool_data: dict[str, Any]) -> Static:
15
+ pass
16
+
17
+ @classmethod
18
+ def escape_markup(cls, text: str) -> str:
19
+ return text.replace("[", "\\[").replace("]", "\\]")
20
+
21
+ @classmethod
22
+ def format_args(cls, args: dict[str, Any], max_length: int = 500) -> str:
23
+ if not args:
24
+ return ""
25
+
26
+ args_parts = []
27
+ for k, v in args.items():
28
+ str_v = str(v)
29
+ if len(str_v) > max_length:
30
+ str_v = str_v[: max_length - 3] + "..."
31
+ args_parts.append(f" [dim]{k}:[/] {cls.escape_markup(str_v)}")
32
+ return "\n".join(args_parts)
33
+
34
+ @classmethod
35
+ def format_result(cls, result: Any, max_length: int = 1000) -> str:
36
+ if result is None:
37
+ return ""
38
+
39
+ str_result = str(result).strip()
40
+ if not str_result:
41
+ return ""
42
+
43
+ if len(str_result) > max_length:
44
+ str_result = str_result[: max_length - 3] + "..."
45
+ return cls.escape_markup(str_result)
46
+
47
+ @classmethod
48
+ def get_status_icon(cls, status: str) -> str:
49
+ status_icons = {
50
+ "running": "[#f59e0b]●[/#f59e0b] In progress...",
51
+ "completed": "[#22c55e]✓[/#22c55e] Done",
52
+ "failed": "[#dc2626]✗[/#dc2626] Failed",
53
+ "error": "[#dc2626]✗[/#dc2626] Error",
54
+ }
55
+ return status_icons.get(status, "[dim]○[/dim] Unknown")
56
+
57
+ @classmethod
58
+ def get_css_classes(cls, status: str) -> str:
59
+ base_classes = cls.css_classes.copy()
60
+ base_classes.append(f"status-{status}")
61
+ return " ".join(base_classes)
@@ -0,0 +1,107 @@
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 BrowserRenderer(BaseToolRenderer):
11
+ tool_name: ClassVar[str] = "browser_action"
12
+ css_classes: ClassVar[list[str]] = ["tool-call", "browser-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
+ action = args.get("action", "unknown")
20
+
21
+ content = cls._build_sleek_content(action, args)
22
+
23
+ css_classes = cls.get_css_classes(status)
24
+ return Static(content, classes=css_classes)
25
+
26
+ @classmethod
27
+ def _build_sleek_content(cls, action: str, args: dict[str, Any]) -> str:
28
+ browser_icon = "🌐"
29
+
30
+ url = args.get("url")
31
+ text = args.get("text")
32
+ js_code = args.get("js_code")
33
+
34
+ if action in [
35
+ "launch",
36
+ "goto",
37
+ "new_tab",
38
+ "type",
39
+ "execute_js",
40
+ "click",
41
+ "double_click",
42
+ "hover",
43
+ ]:
44
+ if action == "launch":
45
+ display_url = cls._format_url(url) if url else None
46
+ message = (
47
+ f"launching {display_url} on browser" if display_url else "launching browser"
48
+ )
49
+ elif action == "goto":
50
+ display_url = cls._format_url(url) if url else None
51
+ message = f"navigating to {display_url}" if display_url else "navigating"
52
+ elif action == "new_tab":
53
+ display_url = cls._format_url(url) if url else None
54
+ message = f"opening tab {display_url}" if display_url else "opening tab"
55
+ elif action == "type":
56
+ display_text = cls._format_text(text) if text else None
57
+ message = f"typing {display_text}" if display_text else "typing"
58
+ elif action == "execute_js":
59
+ display_js = cls._format_js(js_code) if js_code else None
60
+ message = (
61
+ f"executing javascript\n{display_js}" if display_js else "executing javascript"
62
+ )
63
+ else:
64
+ action_words = {
65
+ "click": "clicking",
66
+ "double_click": "double clicking",
67
+ "hover": "hovering",
68
+ }
69
+ message = action_words[action]
70
+
71
+ return f"{browser_icon} [#06b6d4]{message}[/]"
72
+
73
+ simple_actions = {
74
+ "back": "going back in browser history",
75
+ "forward": "going forward in browser history",
76
+ "refresh": "refreshing browser tab",
77
+ "close_tab": "closing browser tab",
78
+ "switch_tab": "switching browser tab",
79
+ "list_tabs": "listing browser tabs",
80
+ "view_source": "viewing page source",
81
+ "screenshot": "taking screenshot of browser tab",
82
+ "wait": "waiting...",
83
+ "close": "closing browser",
84
+ }
85
+
86
+ if action in simple_actions:
87
+ return f"{browser_icon} [#06b6d4]{simple_actions[action]}[/]"
88
+
89
+ return f"{browser_icon} [#06b6d4]{action}[/]"
90
+
91
+ @classmethod
92
+ def _format_url(cls, url: str) -> str:
93
+ if len(url) > 300:
94
+ url = url[:297] + "..."
95
+ return cls.escape_markup(url)
96
+
97
+ @classmethod
98
+ def _format_text(cls, text: str) -> str:
99
+ if len(text) > 200:
100
+ text = text[:197] + "..."
101
+ return cls.escape_markup(text)
102
+
103
+ @classmethod
104
+ def _format_js(cls, js_code: str) -> str:
105
+ if len(js_code) > 200:
106
+ js_code = js_code[:197] + "..."
107
+ return f"[white]{cls.escape_markup(js_code)}[/white]"
@@ -0,0 +1,95 @@
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 StrReplaceEditorRenderer(BaseToolRenderer):
11
+ tool_name: ClassVar[str] = "str_replace_editor"
12
+ css_classes: ClassVar[list[str]] = ["tool-call", "file-edit-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
+ command = args.get("command", "")
20
+ path = args.get("path", "")
21
+
22
+ if command == "view":
23
+ header = "📖 [bold #10b981]Reading file[/]"
24
+ elif command == "str_replace":
25
+ header = "✏️ [bold #10b981]Editing file[/]"
26
+ elif command == "create":
27
+ header = "📝 [bold #10b981]Creating file[/]"
28
+ else:
29
+ header = "📄 [bold #10b981]File operation[/]"
30
+
31
+ if (result and isinstance(result, dict) and "content" in result) or path:
32
+ path_display = path[-60:] if len(path) > 60 else path
33
+ content_text = f"{header} [dim]{cls.escape_markup(path_display)}[/]"
34
+ else:
35
+ content_text = f"{header} [dim]Processing...[/]"
36
+
37
+ css_classes = cls.get_css_classes("completed")
38
+ return Static(content_text, classes=css_classes)
39
+
40
+
41
+ @register_tool_renderer
42
+ class ListFilesRenderer(BaseToolRenderer):
43
+ tool_name: ClassVar[str] = "list_files"
44
+ css_classes: ClassVar[list[str]] = ["tool-call", "file-edit-tool"]
45
+
46
+ @classmethod
47
+ def render(cls, tool_data: dict[str, Any]) -> Static:
48
+ args = tool_data.get("args", {})
49
+
50
+ path = args.get("path", "")
51
+
52
+ header = "📂 [bold #10b981]Listing files[/]"
53
+
54
+ if path:
55
+ path_display = path[-60:] if len(path) > 60 else path
56
+ content_text = f"{header} [dim]{cls.escape_markup(path_display)}[/]"
57
+ else:
58
+ content_text = f"{header} [dim]Current directory[/]"
59
+
60
+ css_classes = cls.get_css_classes("completed")
61
+ return Static(content_text, classes=css_classes)
62
+
63
+
64
+ @register_tool_renderer
65
+ class SearchFilesRenderer(BaseToolRenderer):
66
+ tool_name: ClassVar[str] = "search_files"
67
+ css_classes: ClassVar[list[str]] = ["tool-call", "file-edit-tool"]
68
+
69
+ @classmethod
70
+ def render(cls, tool_data: dict[str, Any]) -> Static:
71
+ args = tool_data.get("args", {})
72
+
73
+ path = args.get("path", "")
74
+ regex = args.get("regex", "")
75
+
76
+ header = "🔍 [bold purple]Searching files[/]"
77
+
78
+ if path and regex:
79
+ path_display = path[-30:] if len(path) > 30 else path
80
+ regex_display = regex[:30] if len(regex) > 30 else regex
81
+ content_text = (
82
+ f"{header} [dim]{cls.escape_markup(path_display)} for "
83
+ f"'{cls.escape_markup(regex_display)}'[/]"
84
+ )
85
+ elif path:
86
+ path_display = path[-60:] if len(path) > 60 else path
87
+ content_text = f"{header} [dim]{cls.escape_markup(path_display)}[/]"
88
+ elif regex:
89
+ regex_display = regex[:60] if len(regex) > 60 else regex
90
+ content_text = f"{header} [dim]'{cls.escape_markup(regex_display)}'[/]"
91
+ else:
92
+ content_text = f"{header} [dim]Searching...[/]"
93
+
94
+ css_classes = cls.get_css_classes("completed")
95
+ return Static(content_text, classes=css_classes)
@@ -0,0 +1,32 @@
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 FinishScanRenderer(BaseToolRenderer):
11
+ tool_name: ClassVar[str] = "finish_scan"
12
+ css_classes: ClassVar[list[str]] = ["tool-call", "finish-tool"]
13
+
14
+ @classmethod
15
+ def render(cls, tool_data: dict[str, Any]) -> Static:
16
+ args = tool_data.get("args", {})
17
+
18
+ content = args.get("content", "")
19
+ success = args.get("success", True)
20
+
21
+ header = (
22
+ "🏁 [bold #dc2626]Finishing Scan[/]" if success else "🏁 [bold #dc2626]Scan Failed[/]"
23
+ )
24
+
25
+ if content:
26
+ content_display = content[:600] + "..." if len(content) > 600 else content
27
+ content_text = f"{header}\n [bold]{cls.escape_markup(content_display)}[/]"
28
+ else:
29
+ content_text = f"{header}\n [dim]Generating final report...[/]"
30
+
31
+ css_classes = cls.get_css_classes("completed")
32
+ return Static(content_text, classes=css_classes)
@@ -0,0 +1,108 @@
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 CreateNoteRenderer(BaseToolRenderer):
11
+ tool_name: ClassVar[str] = "create_note"
12
+ css_classes: ClassVar[list[str]] = ["tool-call", "notes-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
+ content = args.get("content", "")
20
+
21
+ header = "📝 [bold #fbbf24]Note[/]"
22
+
23
+ if title:
24
+ title_display = title[:100] + "..." if len(title) > 100 else title
25
+ note_parts = [f"{header}\n [bold]{cls.escape_markup(title_display)}[/]"]
26
+
27
+ if content:
28
+ content_display = content[:200] + "..." if len(content) > 200 else content
29
+ note_parts.append(f" [dim]{cls.escape_markup(content_display)}[/]")
30
+
31
+ content_text = "\n".join(note_parts)
32
+ else:
33
+ content_text = f"{header}\n [dim]Creating note...[/]"
34
+
35
+ css_classes = cls.get_css_classes("completed")
36
+ return Static(content_text, classes=css_classes)
37
+
38
+
39
+ @register_tool_renderer
40
+ class DeleteNoteRenderer(BaseToolRenderer):
41
+ tool_name: ClassVar[str] = "delete_note"
42
+ css_classes: ClassVar[list[str]] = ["tool-call", "notes-tool"]
43
+
44
+ @classmethod
45
+ def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: ARG003
46
+ header = "🗑️ [bold #fbbf24]Delete Note[/]"
47
+ content_text = f"{header}\n [dim]Deleting...[/]"
48
+
49
+ css_classes = cls.get_css_classes("completed")
50
+ return Static(content_text, classes=css_classes)
51
+
52
+
53
+ @register_tool_renderer
54
+ class UpdateNoteRenderer(BaseToolRenderer):
55
+ tool_name: ClassVar[str] = "update_note"
56
+ css_classes: ClassVar[list[str]] = ["tool-call", "notes-tool"]
57
+
58
+ @classmethod
59
+ def render(cls, tool_data: dict[str, Any]) -> Static:
60
+ args = tool_data.get("args", {})
61
+
62
+ title = args.get("title", "")
63
+ content = args.get("content", "")
64
+
65
+ header = "✏️ [bold #fbbf24]Update Note[/]"
66
+
67
+ if title or content:
68
+ note_parts = [header]
69
+
70
+ if title:
71
+ title_display = title[:100] + "..." if len(title) > 100 else title
72
+ note_parts.append(f" [bold]{cls.escape_markup(title_display)}[/]")
73
+
74
+ if content:
75
+ content_display = content[:200] + "..." if len(content) > 200 else content
76
+ note_parts.append(f" [dim]{cls.escape_markup(content_display)}[/]")
77
+
78
+ content_text = "\n".join(note_parts)
79
+ else:
80
+ content_text = f"{header}\n [dim]Updating...[/]"
81
+
82
+ css_classes = cls.get_css_classes("completed")
83
+ return Static(content_text, classes=css_classes)
84
+
85
+
86
+ @register_tool_renderer
87
+ class ListNotesRenderer(BaseToolRenderer):
88
+ tool_name: ClassVar[str] = "list_notes"
89
+ css_classes: ClassVar[list[str]] = ["tool-call", "notes-tool"]
90
+
91
+ @classmethod
92
+ def render(cls, tool_data: dict[str, Any]) -> Static:
93
+ result = tool_data.get("result")
94
+
95
+ header = "📋 [bold #fbbf24]Listing notes[/]"
96
+
97
+ if result and isinstance(result, dict) and "notes" in result:
98
+ notes = result["notes"]
99
+ if isinstance(notes, list):
100
+ count = len(notes)
101
+ content_text = f"{header}\n [dim]{count} notes found[/]"
102
+ else:
103
+ content_text = f"{header}\n [dim]No notes found[/]"
104
+ else:
105
+ content_text = f"{header}\n [dim]Listing notes...[/]"
106
+
107
+ css_classes = cls.get_css_classes("completed")
108
+ return Static(content_text, classes=css_classes)