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,120 +1,135 @@
1
+ from functools import cache
1
2
  from typing import Any, ClassVar
2
3
 
4
+ from pygments.lexers import get_lexer_by_name
5
+ from pygments.styles import get_style_by_name
6
+ from rich.text import Text
3
7
  from textual.widgets import Static
4
8
 
5
9
  from .base_renderer import BaseToolRenderer
6
10
  from .registry import register_tool_renderer
7
11
 
8
12
 
13
+ @cache
14
+ def _get_style_colors() -> dict[Any, str]:
15
+ style = get_style_by_name("native")
16
+ return {token: f"#{style_def['color']}" for token, style_def in style if style_def["color"]}
17
+
18
+
9
19
  @register_tool_renderer
10
20
  class BrowserRenderer(BaseToolRenderer):
11
21
  tool_name: ClassVar[str] = "browser_action"
12
22
  css_classes: ClassVar[list[str]] = ["tool-call", "browser-tool"]
13
23
 
24
+ SIMPLE_ACTIONS: ClassVar[dict[str, str]] = {
25
+ "back": "going back in browser history",
26
+ "forward": "going forward in browser history",
27
+ "scroll_down": "scrolling down",
28
+ "scroll_up": "scrolling up",
29
+ "refresh": "refreshing browser tab",
30
+ "close_tab": "closing browser tab",
31
+ "switch_tab": "switching browser tab",
32
+ "list_tabs": "listing browser tabs",
33
+ "view_source": "viewing page source",
34
+ "get_console_logs": "getting console logs",
35
+ "screenshot": "taking screenshot of browser tab",
36
+ "wait": "waiting...",
37
+ "close": "closing browser",
38
+ }
39
+
40
+ @classmethod
41
+ def _get_token_color(cls, token_type: Any) -> str | None:
42
+ colors = _get_style_colors()
43
+ while token_type:
44
+ if token_type in colors:
45
+ return colors[token_type]
46
+ token_type = token_type.parent
47
+ return None
48
+
49
+ @classmethod
50
+ def _highlight_js(cls, code: str) -> Text:
51
+ lexer = get_lexer_by_name("javascript")
52
+ text = Text()
53
+
54
+ for token_type, token_value in lexer.get_tokens(code):
55
+ if not token_value:
56
+ continue
57
+ color = cls._get_token_color(token_type)
58
+ text.append(token_value, style=color)
59
+
60
+ return text
61
+
14
62
  @classmethod
15
63
  def render(cls, tool_data: dict[str, Any]) -> Static:
16
64
  args = tool_data.get("args", {})
17
65
  status = tool_data.get("status", "unknown")
18
66
 
19
67
  action = args.get("action", "unknown")
20
-
21
- content = cls._build_sleek_content(action, args)
68
+ content = cls._build_content(action, args)
22
69
 
23
70
  css_classes = cls.get_css_classes(status)
24
71
  return Static(content, classes=css_classes)
25
72
 
26
73
  @classmethod
27
- def _build_sleek_content(cls, action: str, args: dict[str, Any]) -> str:
28
- browser_icon = "🌐"
74
+ def _build_url_action(cls, text: Text, label: str, url: str | None, suffix: str = "") -> None:
75
+ text.append(label, style="#06b6d4")
76
+ if url:
77
+ text.append(url, style="#06b6d4")
78
+ if suffix:
79
+ text.append(suffix, style="#06b6d4")
29
80
 
30
- url = args.get("url")
31
- text = args.get("text")
32
- js_code = args.get("js_code")
33
- key = args.get("key")
34
- file_path = args.get("file_path")
35
-
36
- if action in [
37
- "launch",
38
- "goto",
39
- "new_tab",
40
- "type",
41
- "execute_js",
42
- "click",
43
- "double_click",
44
- "hover",
45
- "press_key",
46
- "save_pdf",
47
- ]:
48
- if action == "launch":
49
- display_url = cls._format_url(url) if url else None
50
- message = (
51
- f"launching {display_url} on browser" if display_url else "launching browser"
52
- )
53
- elif action == "goto":
54
- display_url = cls._format_url(url) if url else None
55
- message = f"navigating to {display_url}" if display_url else "navigating"
56
- elif action == "new_tab":
57
- display_url = cls._format_url(url) if url else None
58
- message = f"opening tab {display_url}" if display_url else "opening tab"
59
- elif action == "type":
60
- display_text = cls._format_text(text) if text else None
61
- message = f"typing {display_text}" if display_text else "typing"
62
- elif action == "execute_js":
63
- display_js = cls._format_js(js_code) if js_code else None
64
- message = (
65
- f"executing javascript\n{display_js}" if display_js else "executing javascript"
66
- )
67
- elif action == "press_key":
68
- display_key = cls.escape_markup(key) if key else None
69
- message = f"pressing key {display_key}" if display_key else "pressing key"
70
- elif action == "save_pdf":
71
- display_path = cls.escape_markup(file_path) if file_path else None
72
- message = f"saving PDF to {display_path}" if display_path else "saving PDF"
73
- else:
74
- action_words = {
75
- "click": "clicking",
76
- "double_click": "double clicking",
77
- "hover": "hovering",
78
- }
79
- message = cls.escape_markup(action_words[action])
80
-
81
- return f"{browser_icon} [#06b6d4]{message}[/]"
82
-
83
- simple_actions = {
84
- "back": "going back in browser history",
85
- "forward": "going forward in browser history",
86
- "scroll_down": "scrolling down",
87
- "scroll_up": "scrolling up",
88
- "refresh": "refreshing browser tab",
89
- "close_tab": "closing browser tab",
90
- "switch_tab": "switching browser tab",
91
- "list_tabs": "listing browser tabs",
92
- "view_source": "viewing page source",
93
- "get_console_logs": "getting console logs",
94
- "screenshot": "taking screenshot of browser tab",
95
- "wait": "waiting...",
96
- "close": "closing browser",
97
- }
98
-
99
- if action in simple_actions:
100
- return f"{browser_icon} [#06b6d4]{cls.escape_markup(simple_actions[action])}[/]"
81
+ @classmethod
82
+ def _build_content(cls, action: str, args: dict[str, Any]) -> Text:
83
+ text = Text()
84
+ text.append("🌐 ")
101
85
 
102
- return f"{browser_icon} [#06b6d4]{cls.escape_markup(action)}[/]"
86
+ if action in cls.SIMPLE_ACTIONS:
87
+ text.append(cls.SIMPLE_ACTIONS[action], style="#06b6d4")
88
+ return text
103
89
 
104
- @classmethod
105
- def _format_url(cls, url: str) -> str:
106
- if len(url) > 300:
107
- url = url[:297] + "..."
108
- return cls.escape_markup(url)
90
+ url = args.get("url")
109
91
 
110
- @classmethod
111
- def _format_text(cls, text: str) -> str:
112
- if len(text) > 200:
113
- text = text[:197] + "..."
114
- return cls.escape_markup(text)
92
+ url_actions = {
93
+ "launch": ("launching ", " on browser" if url else "browser"),
94
+ "goto": ("navigating to ", ""),
95
+ "new_tab": ("opening tab ", ""),
96
+ }
97
+ if action in url_actions:
98
+ label, suffix = url_actions[action]
99
+ if action == "launch" and not url:
100
+ text.append("launching browser", style="#06b6d4")
101
+ else:
102
+ cls._build_url_action(text, label, url, suffix)
103
+ return text
115
104
 
116
- @classmethod
117
- def _format_js(cls, js_code: str) -> str:
118
- if len(js_code) > 200:
119
- js_code = js_code[:197] + "..."
120
- return f"[white]{cls.escape_markup(js_code)}[/white]"
105
+ click_actions = {
106
+ "click": "clicking",
107
+ "double_click": "double clicking",
108
+ "hover": "hovering",
109
+ }
110
+ if action in click_actions:
111
+ text.append(click_actions[action], style="#06b6d4")
112
+ return text
113
+
114
+ handlers: dict[str, tuple[str, str | None]] = {
115
+ "type": ("typing ", args.get("text")),
116
+ "press_key": ("pressing key ", args.get("key")),
117
+ "save_pdf": ("saving PDF to ", args.get("file_path")),
118
+ }
119
+ if action in handlers:
120
+ label, value = handlers[action]
121
+ text.append(label, style="#06b6d4")
122
+ if value:
123
+ text.append(str(value), style="#06b6d4")
124
+ return text
125
+
126
+ if action == "execute_js":
127
+ text.append("executing javascript", style="#06b6d4")
128
+ js_code = args.get("js_code")
129
+ if js_code:
130
+ text.append("\n")
131
+ text.append_text(cls._highlight_js(js_code))
132
+ return text
133
+
134
+ text.append(action, style="#06b6d4")
135
+ return text
@@ -1,16 +1,56 @@
1
+ from functools import cache
1
2
  from typing import Any, ClassVar
2
3
 
4
+ from pygments.lexers import get_lexer_by_name, get_lexer_for_filename
5
+ from pygments.styles import get_style_by_name
6
+ from pygments.util import ClassNotFound
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
+ @cache
15
+ def _get_style_colors() -> dict[Any, str]:
16
+ style = get_style_by_name("native")
17
+ return {token: f"#{style_def['color']}" for token, style_def in style if style_def["color"]}
18
+
19
+
20
+ def _get_lexer_for_file(path: str) -> Any:
21
+ try:
22
+ return get_lexer_for_filename(path)
23
+ except ClassNotFound:
24
+ return get_lexer_by_name("text")
25
+
26
+
9
27
  @register_tool_renderer
10
28
  class StrReplaceEditorRenderer(BaseToolRenderer):
11
29
  tool_name: ClassVar[str] = "str_replace_editor"
12
30
  css_classes: ClassVar[list[str]] = ["tool-call", "file-edit-tool"]
13
31
 
32
+ @classmethod
33
+ def _get_token_color(cls, token_type: Any) -> str | None:
34
+ colors = _get_style_colors()
35
+ while token_type:
36
+ if token_type in colors:
37
+ return colors[token_type]
38
+ token_type = token_type.parent
39
+ return None
40
+
41
+ @classmethod
42
+ def _highlight_code(cls, code: str, path: str) -> Text:
43
+ lexer = _get_lexer_for_file(path)
44
+ text = Text()
45
+
46
+ for token_type, token_value in lexer.get_tokens(code):
47
+ if not token_value:
48
+ continue
49
+ color = cls._get_token_color(token_type)
50
+ text.append(token_value, style=color)
51
+
52
+ return text
53
+
14
54
  @classmethod
15
55
  def render(cls, tool_data: dict[str, Any]) -> Static:
16
56
  args = tool_data.get("args", {})
@@ -18,28 +58,67 @@ class StrReplaceEditorRenderer(BaseToolRenderer):
18
58
 
19
59
  command = args.get("command", "")
20
60
  path = args.get("path", "")
61
+ old_str = args.get("old_str", "")
62
+ new_str = args.get("new_str", "")
63
+ file_text = args.get("file_text", "")
21
64
 
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
- elif command == "insert":
29
- header = "✏️ [bold #10b981]Inserting text[/]"
30
- elif command == "undo_edit":
31
- header = "↩️ [bold #10b981]Undoing edit[/]"
32
- else:
33
- header = "📄 [bold #10b981]File operation[/]"
65
+ text = Text()
66
+
67
+ icons_and_labels = {
68
+ "view": ("📖 ", "Reading file", "#10b981"),
69
+ "str_replace": ("✏️ ", "Editing file", "#10b981"),
70
+ "create": ("📝 ", "Creating file", "#10b981"),
71
+ "insert": ("✏️ ", "Inserting text", "#10b981"),
72
+ "undo_edit": ("↩️ ", "Undoing edit", "#10b981"),
73
+ }
34
74
 
35
- if (result and isinstance(result, dict) and "content" in result) or path:
75
+ icon, label, color = icons_and_labels.get(command, ("📄 ", "File operation", "#10b981"))
76
+ text.append(icon)
77
+ text.append(label, style=f"bold {color}")
78
+
79
+ if path:
36
80
  path_display = path[-60:] if len(path) > 60 else path
37
- content_text = f"{header} [dim]{cls.escape_markup(path_display)}[/]"
38
- else:
39
- content_text = f"{header} [dim]Processing...[/]"
81
+ text.append(" ")
82
+ text.append(path_display, style="dim")
83
+
84
+ if command == "str_replace" and (old_str or new_str):
85
+ if old_str:
86
+ highlighted_old = cls._highlight_code(old_str, path)
87
+ for line in highlighted_old.plain.split("\n"):
88
+ text.append("\n")
89
+ text.append("-", style="#ef4444")
90
+ text.append(" ")
91
+ text.append(line)
92
+
93
+ if new_str:
94
+ highlighted_new = cls._highlight_code(new_str, path)
95
+ for line in highlighted_new.plain.split("\n"):
96
+ text.append("\n")
97
+ text.append("+", style="#22c55e")
98
+ text.append(" ")
99
+ text.append(line)
100
+
101
+ elif command == "create" and file_text:
102
+ text.append("\n")
103
+ text.append_text(cls._highlight_code(file_text, path))
104
+
105
+ elif command == "insert" and new_str:
106
+ highlighted_new = cls._highlight_code(new_str, path)
107
+ for line in highlighted_new.plain.split("\n"):
108
+ text.append("\n")
109
+ text.append("+", style="#22c55e")
110
+ text.append(" ")
111
+ text.append(line)
112
+
113
+ elif isinstance(result, str) and result.strip():
114
+ text.append("\n ")
115
+ text.append(result.strip(), style="dim")
116
+ elif not (result and isinstance(result, dict) and "content" in result) and not path:
117
+ text.append(" ")
118
+ text.append("Processing...", style="dim")
40
119
 
41
120
  css_classes = cls.get_css_classes("completed")
42
- return Static(content_text, classes=css_classes)
121
+ return Static(text, classes=css_classes)
43
122
 
44
123
 
45
124
  @register_tool_renderer
@@ -50,19 +129,21 @@ class ListFilesRenderer(BaseToolRenderer):
50
129
  @classmethod
51
130
  def render(cls, tool_data: dict[str, Any]) -> Static:
52
131
  args = tool_data.get("args", {})
53
-
54
132
  path = args.get("path", "")
55
133
 
56
- header = "📂 [bold #10b981]Listing files[/]"
134
+ text = Text()
135
+ text.append("📂 ")
136
+ text.append("Listing files", style="bold #10b981")
137
+ text.append(" ")
57
138
 
58
139
  if path:
59
140
  path_display = path[-60:] if len(path) > 60 else path
60
- content_text = f"{header} [dim]{cls.escape_markup(path_display)}[/]"
141
+ text.append(path_display, style="dim")
61
142
  else:
62
- content_text = f"{header} [dim]Current directory[/]"
143
+ text.append("Current directory", style="dim")
63
144
 
64
145
  css_classes = cls.get_css_classes("completed")
65
- return Static(content_text, classes=css_classes)
146
+ return Static(text, classes=css_classes)
66
147
 
67
148
 
68
149
  @register_tool_renderer
@@ -73,27 +154,27 @@ class SearchFilesRenderer(BaseToolRenderer):
73
154
  @classmethod
74
155
  def render(cls, tool_data: dict[str, Any]) -> Static:
75
156
  args = tool_data.get("args", {})
76
-
77
157
  path = args.get("path", "")
78
158
  regex = args.get("regex", "")
79
159
 
80
- header = "🔍 [bold purple]Searching files[/]"
160
+ text = Text()
161
+ text.append("🔍 ")
162
+ text.append("Searching files", style="bold purple")
163
+ text.append(" ")
81
164
 
82
165
  if path and regex:
83
- path_display = path[-30:] if len(path) > 30 else path
84
- regex_display = regex[:30] if len(regex) > 30 else regex
85
- content_text = (
86
- f"{header} [dim]{cls.escape_markup(path_display)} for "
87
- f"'{cls.escape_markup(regex_display)}'[/]"
88
- )
166
+ text.append(path, style="dim")
167
+ text.append(" for '", style="dim")
168
+ text.append(regex, style="dim")
169
+ text.append("'", style="dim")
89
170
  elif path:
90
- path_display = path[-60:] if len(path) > 60 else path
91
- content_text = f"{header} [dim]{cls.escape_markup(path_display)}[/]"
171
+ text.append(path, style="dim")
92
172
  elif regex:
93
- regex_display = regex[:60] if len(regex) > 60 else regex
94
- content_text = f"{header} [dim]'{cls.escape_markup(regex_display)}'[/]"
173
+ text.append("'", style="dim")
174
+ text.append(regex, style="dim")
175
+ text.append("'", style="dim")
95
176
  else:
96
- content_text = f"{header} [dim]Searching...[/]"
177
+ text.append("Searching...", style="dim")
97
178
 
98
179
  css_classes = cls.get_css_classes("completed")
99
- return Static(content_text, classes=css_classes)
180
+ return Static(text, classes=css_classes)
@@ -1,11 +1,17 @@
1
1
  from typing import Any, ClassVar
2
2
 
3
+ from rich.padding import Padding
4
+ from rich.text import Text
3
5
  from textual.widgets import Static
4
6
 
5
7
  from .base_renderer import BaseToolRenderer
6
8
  from .registry import register_tool_renderer
7
9
 
8
10
 
11
+ FIELD_STYLE = "bold #4ade80"
12
+ BG_COLOR = "#141414"
13
+
14
+
9
15
  @register_tool_renderer
10
16
  class FinishScanRenderer(BaseToolRenderer):
11
17
  tool_name: ClassVar[str] = "finish_scan"
@@ -15,17 +21,44 @@ class FinishScanRenderer(BaseToolRenderer):
15
21
  def render(cls, tool_data: dict[str, Any]) -> Static:
16
22
  args = tool_data.get("args", {})
17
23
 
18
- content = args.get("content", "")
19
- success = args.get("success", True)
24
+ executive_summary = args.get("executive_summary", "")
25
+ methodology = args.get("methodology", "")
26
+ technical_analysis = args.get("technical_analysis", "")
27
+ recommendations = args.get("recommendations", "")
28
+
29
+ text = Text()
30
+ text.append("🏁 ")
31
+ text.append("Finishing Scan", style="bold #dc2626")
32
+
33
+ if executive_summary:
34
+ text.append("\n\n")
35
+ text.append("Executive Summary", style=FIELD_STYLE)
36
+ text.append("\n")
37
+ text.append(executive_summary)
38
+
39
+ if methodology:
40
+ text.append("\n\n")
41
+ text.append("Methodology", style=FIELD_STYLE)
42
+ text.append("\n")
43
+ text.append(methodology)
44
+
45
+ if technical_analysis:
46
+ text.append("\n\n")
47
+ text.append("Technical Analysis", style=FIELD_STYLE)
48
+ text.append("\n")
49
+ text.append(technical_analysis)
50
+
51
+ if recommendations:
52
+ text.append("\n\n")
53
+ text.append("Recommendations", style=FIELD_STYLE)
54
+ text.append("\n")
55
+ text.append(recommendations)
20
56
 
21
- header = (
22
- "🏁 [bold #dc2626]Finishing Scan[/]" if success else "🏁 [bold #dc2626]Scan Failed[/]"
23
- )
57
+ if not (executive_summary or methodology or technical_analysis or recommendations):
58
+ text.append("\n ")
59
+ text.append("Generating final report...", style="dim")
24
60
 
25
- if content:
26
- content_text = f"{header}\n [bold]{cls.escape_markup(content)}[/]"
27
- else:
28
- content_text = f"{header}\n [dim]Generating final report...[/]"
61
+ padded = Padding(text, 2, style=f"on {BG_COLOR}")
29
62
 
30
63
  css_classes = cls.get_css_classes("completed")
31
- return Static(content_text, classes=css_classes)
64
+ return Static(padded, 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
@@ -17,23 +18,28 @@ class CreateNoteRenderer(BaseToolRenderer):
17
18
 
18
19
  title = args.get("title", "")
19
20
  content = args.get("content", "")
21
+ category = args.get("category", "general")
20
22
 
21
- header = "📝 [bold #fbbf24]Note[/]"
23
+ text = Text()
24
+ text.append("📝 ")
25
+ text.append("Note", style="bold #fbbf24")
26
+ text.append(" ")
27
+ text.append(f"({category})", style="dim")
22
28
 
23
29
  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)}[/]"]
30
+ text.append("\n ")
31
+ text.append(title.strip())
26
32
 
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)}[/]")
33
+ if content:
34
+ text.append("\n ")
35
+ text.append(content.strip(), style="dim")
30
36
 
31
- content_text = "\n".join(note_parts)
32
- else:
33
- content_text = f"{header}\n [dim]Creating note...[/]"
37
+ if not title and not content:
38
+ text.append("\n ")
39
+ text.append("Capturing...", style="dim")
34
40
 
35
41
  css_classes = cls.get_css_classes("completed")
36
- return Static(content_text, classes=css_classes)
42
+ return Static(text, classes=css_classes)
37
43
 
38
44
 
39
45
  @register_tool_renderer
@@ -43,11 +49,12 @@ class DeleteNoteRenderer(BaseToolRenderer):
43
49
 
44
50
  @classmethod
45
51
  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...[/]"
52
+ text = Text()
53
+ text.append("📝 ")
54
+ text.append("Note Removed", style="bold #94a3b8")
48
55
 
49
56
  css_classes = cls.get_css_classes("completed")
50
- return Static(content_text, classes=css_classes)
57
+ return Static(text, classes=css_classes)
51
58
 
52
59
 
53
60
  @register_tool_renderer
@@ -59,28 +66,27 @@ class UpdateNoteRenderer(BaseToolRenderer):
59
66
  def render(cls, tool_data: dict[str, Any]) -> Static:
60
67
  args = tool_data.get("args", {})
61
68
 
62
- title = args.get("title", "")
63
- content = args.get("content", "")
64
-
65
- header = "✏️ [bold #fbbf24]Update Note[/]"
69
+ title = args.get("title")
70
+ content = args.get("content")
66
71
 
67
- if title or content:
68
- note_parts = [header]
72
+ text = Text()
73
+ text.append("📝 ")
74
+ text.append("Note Updated", style="bold #fbbf24")
69
75
 
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)}[/]")
76
+ if title:
77
+ text.append("\n ")
78
+ text.append(title)
73
79
 
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)}[/]")
80
+ if content:
81
+ text.append("\n ")
82
+ text.append(content.strip(), style="dim")
77
83
 
78
- content_text = "\n".join(note_parts)
79
- else:
80
- content_text = f"{header}\n [dim]Updating...[/]"
84
+ if not title and not content:
85
+ text.append("\n ")
86
+ text.append("Updating...", style="dim")
81
87
 
82
88
  css_classes = cls.get_css_classes("completed")
83
- return Static(content_text, classes=css_classes)
89
+ return Static(text, classes=css_classes)
84
90
 
85
91
 
86
92
  @register_tool_renderer
@@ -92,17 +98,36 @@ class ListNotesRenderer(BaseToolRenderer):
92
98
  def render(cls, tool_data: dict[str, Any]) -> Static:
93
99
  result = tool_data.get("result")
94
100
 
95
- header = "📋 [bold #fbbf24]Listing notes[/]"
101
+ text = Text()
102
+ text.append("📝 ")
103
+ text.append("Notes", style="bold #fbbf24")
104
+
105
+ if isinstance(result, str) and result.strip():
106
+ text.append("\n ")
107
+ text.append(result.strip(), style="dim")
108
+ elif result and isinstance(result, dict) and result.get("success"):
109
+ count = result.get("total_count", 0)
110
+ notes = result.get("notes", []) or []
96
111
 
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[/]"
112
+ if count == 0:
113
+ text.append("\n ")
114
+ text.append("No notes", style="dim")
102
115
  else:
103
- content_text = f"{header}\n [dim]No notes found[/]"
116
+ for note in notes:
117
+ title = note.get("title", "").strip() or "(untitled)"
118
+ category = note.get("category", "general")
119
+ note_content = note.get("content", "").strip()
120
+
121
+ text.append("\n - ")
122
+ text.append(title)
123
+ text.append(f" ({category})", style="dim")
124
+
125
+ if note_content:
126
+ text.append("\n ")
127
+ text.append(note_content, style="dim")
104
128
  else:
105
- content_text = f"{header}\n [dim]Listing notes...[/]"
129
+ text.append("\n ")
130
+ text.append("Loading...", style="dim")
106
131
 
107
132
  css_classes = cls.get_css_classes("completed")
108
- return Static(content_text, classes=css_classes)
133
+ return Static(text, classes=css_classes)